3
$\begingroup$

I am writing a package featuring several functions which depend on a large number of parameters. I'm trying to use Options so that I can define default values for these parameters and only specify parameters in function calls when the parameters differ from the default values. However, there is some interdependence among the parameters (the value of some parameters depends on the values of other parameters). I'm currently doing something like this:

BeginPackage["MyModel`"];

Unprotect["MyModel`*"];

Begin["`Private`"];

Options[MyModel]=
    {
        param1->someDefaultValue,
        param2->someOtherDefaultValue,
        param3:>OptionValue[MyModel,param1]*OptionValue[MyModel,param2];
    };

(* define my functions for the model *)

End[];

Protect["MyModel`*"];

EndPackage[];

When I load the package into a notebook and examine the value of param3, it evaluates and gives the correct answer in numerical form.

<< MyModel`

result1 = OptionValue[MyModel,param3];

If I change the value of one of the options that param3 depends upon and reevaluate param3, the result changes as expected (result1 != result2).

SetOptions[MyModel,param1->aNewValue];

result2 = OptionValue[MyModel,param3];

What I want to do is allow a set of fundamental parameters to be changed (param1, param2), but write protect derived parameters (param3) so that the user cannot inadvertently change them (violating the relationships between the parameters).

With the code as is, if I run

SetOptions[MyModel,param3->1234];

Mathematica will allow me to clobber the original value of param3. I would like to let the user get the value of param3, but I want to prohibit them from setting it. How can this be done?

Update:

Perhaps some additional background would help explain what I'm trying to accomplish. All of the options for MyModel were originally defined as variables:

param1 = 2.034;
param2 = 5.258;
param3 = param1*param2;

These variables were treated as constants and used in several other functions:

f[x_,v_,q_]:=param1*x^2+(param3-param2*q)/v;
g[l_,m_,z_,y_,r_]:=param3*Sin[l*m]+param2*(z-y+r);

etc., etc.

Initially, I was only varying a limited number of parameters, which were passed in as arguments to the functions (such as x, v, q, l, m, z, y, and r in the example above). Now, I want to do Monte Carlo simulations, sensitivity analysis, etc. on the system defined by those functions. Because of this, I will be varying more parameters, including many of the ones that I'd originally defined as simple variables like param1, param2, and param3.

Faced with the prospect of greatly expanding the number of input arguments for my functions, I went looking for another way of doing it and thought Mathematica's Options would be a good solution.

Instead of having a bunch of functions with complicated call signatures featuring more than 50 input parameters in many cases, I'd instead have a default set of Options for the model and pass in only those options that differ from the default values.

So, the function definitions become something like:

f[x_,v_,q_,OptionsPattern[MyModel]]:=OptionValue[param1]*x^2+(OptionValue[param3]-OptionValue[param2]*q)/v;
g[l_,m_,z_,y_,r_,OptionsPattern[MyModel]]:=OptionValue[param3]*Sin[l*m]+OptionValue[param2]*(z-y+r);

Instead of:

f[x_,v_,q_,param1_,param2_,param3_]:=param1*x^2+(param3-param2*q)/v;
g[l_,m_,z_,y_,r_,param2_,param3_]:=param3*Sin[l*m]+param2*(z-y+r);

Likewise, function invocation becomes something like:

f[2.0, 1.1, 9.2]  (* gives the same result as the expression below because default values are defined for the other parameters in Options[MyModel] *)

instead of:

f[2.0, 1.1, 9.2, 2.034, 5.258, 2.034*5.258]

It doesn't make a lot of difference in this explanatory example, but in the actual model this simplifies the signature for a typical function call from:

someFunction[param1, param2, param3, param4, param5, param6, ..., param52]

to:

someFunction[param6->someNonDefaultValue, param32->anotherNonDefaultValue]
$\endgroup$
1
  • 3
    $\begingroup$ Sounds like param3 should be a variable, not an option. You can use Protect[] to keep the user from inadvertently changing its value. You could also write a function to return its value. $\endgroup$
    – Michael E2
    Jan 9, 2019 at 0:18

2 Answers 2

5
$\begingroup$

Revised recommendation

In light of your updated question here is my proposed solution. We first define a helper function to simplify application:

setProtectedOption[fn_Symbol, par_ :> val_] :=
   fn /: OptionValue[fn, op : OptionsPattern[fn], par] :=
      val /. Flatten[{op, Options @ fn}]

We then define our Options, using the function above for derived values:

Options[MyModel] = {param1 -> 3, param2 -> 4};

setProtectedOption[MyModel, param3 :> param1*param2]

Proceed as normal for the user functions:

f[x_, v_, q_, OptionsPattern[MyModel]] := 
 OptionValue[param1]*x^2 + (OptionValue[param3] - OptionValue[param2]*q)/v

f[3, 4, 5]
f[3, 4, 5, param2 -> 8]
25

23

If you have derived parameters that are themselves dependent on derived parameters you will need to wrap the RHS derived parameters with OptionValue:

setProtectedOption[MyModel, param4 :> OptionValue[param3]*param1]

g[OptionsPattern[MyModel]] := {OptionValue[param1], OptionValue[param2], 
  OptionValue[param3], OptionValue[param4]}

g[]
g[param2 -> 8]
g[param1 -> 2, param2 -> 5]
{3, 4, 12, 36}

{3, 8, 24, 72}

{2, 5, 10, 20}

Supplemental utilities

If adopting the method proposed above here are two supplemental functions to get and clear these special options respectively.

getProtectedOptions[fn_Symbol] :=
  Cases[
    UpValues[fn],
    HoldPattern[_[OptionValue[fn, _, par_]] :> (val_ /. _Flatten)] :> (par :> val)
  ]

clearProtectedOptions[fn_Symbol] :=
  With[{uv := UpValues[fn]}, 
    uv = DeleteCases[uv, HoldPattern[_[OptionValue[fn, _, _]] :> (_ /. Flatten_)];
  ]

Examples:

getProtectedOptions[MyModel]
{param3 :> param1 param2, param4 :> OptionValue[param3] param1}
clearProtectedOptions[MyModel]

g[]

OptionValue::optnf: Option name param3 not found in defaults for MyModel. >>

OptionValue::optnf: Option name param4 not found in defaults for MyModel. >>

{3, 4, param3, param4}

Original answer

In my opinion your goal contravenes the specific intent of options in Mathematica, which is mutable, user configurable parameters. I can see no reason to make param3 an option at all and you do not seem to have provided one. The normal procedure would be something like this:

Options[MyModel] = {param1 -> someDefaultValue, param2 -> someOtherDefaultValue};

param3[] := Times @@ OptionValue[MyModel, {param1, param2}]

SetOptions[MyModel, {param1 -> 3, param2 -> 4}];

param3[]
12

If you can explain how this fails you I can try to provide an improved approach.

$\endgroup$
2
  • $\begingroup$ The primary reason that I wanted to make param3 an option is simply so that it could be treated in exactly the same way as the other parameters in subsequent calculations. I currently have 40+ fundamental parameters and another 17 derived parameters. Although param3 could be a function instead, that requires that I remember which parameters are the fundamental ones (use OptionValue[paramName] to get the value) vs. which ones are derived (call paramName[] to get the value). $\endgroup$
    – Matt
    Jan 9, 2019 at 14:52
  • $\begingroup$ @Matt Thanks for Accepting my answer. Please see my update for supplemental functions. $\endgroup$
    – Mr.Wizard
    Jan 11, 2019 at 5:06
2
$\begingroup$

If you're concerned about uniform access to dependent and independent parameters, you can define a helper function that handles dependent parameters differently:

param[name_,o___]:=OptionValue[MyModel,{o},name] 
param[param3,o___]:=param[param1,o]*param[param2,o]

Options[MyModel]={param1 -> 3, param2 -> 4};

MyModel[o:OptionsPattern[]]:={param[param1,o],param[param2,o],param[param3,o]}

MyModel[]
(* {3, 4, 12} *)
MyModel[param1->5]
(* {5, 4, 20} *)

Or, slightly fancier:

param[f_, o_, name_] := param@OptionValue[f, o, name]
param[o_OptionValue] := o
param[OptionValue[f_, o_, param3]] := 
  param[f, o, param1]*param[f, o, param2]

Options[MyModel] = {param1 -> 3, param2 -> 4};

MyModel[OptionsPattern[]] := {
  param@OptionValue@param1, 
  param@OptionValue@param2,
  param@OptionValue@param3
}

MyModel[]
(* {3, 4, 12} *)
MyModel[param1->5]
(* {5, 4, 20} *)

This way, you only have to wrap any OptionValue call with param, which you could even automate:

Attributes[InjectParam] = {HoldFirst}
InjectParam[def_] := Unevaluated@def /. o_OptionValue :> param@o

InjectParam[
  MyModel[OptionsPattern[]] :=
    {OptionValue@param1, OptionValue@param2, OptionValue@param3}
]

MyModel[]
(* {3, 4, 12} *)
MyModel[param1->5]
(* {5, 4, 20} *)
$\endgroup$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.