System Options (Arguments to Components and Groups)#
The primary jobs of a component, whether explicit or implicit, are to define inputs and outputs and to do the mapping that computes the outputs given the inputs. Often, however, there are incidental parameters that affect the behavior of the component, but which are not considered input variables in the sense of being computed as an output of another component.
OpenMDAO provides a way of declaring these parameters, which are contained in an OptionsDictionary named options
that is available in every system. Options associated with a particular component or group must be declared in the initialize method of the system definition. A default value can be provided as well as various checks for validity, such as a list of acceptable values or types.
The attributes that can be specified when declaring an option are enumerated and described below:
- OptionsDictionary.declare(name, default=UNDEFINED, values=None, types=None, desc='', upper=None, lower=None, check_valid=None, allow_none=False, recordable=True, set_function=None, deprecation=None)[source]
Declare an option.
The value of the option must satisfy the following: 1. If values only was given when declaring, value must be in values. 2. If types only was given when declaring, value must satisfy isinstance(value, types). 3. It is an error if both values and types are given.
- Parameters:
- namestr
Name of the option.
- defaultobject or Null
Optional default value that must be valid under the above 3 conditions.
- valuesset or list or tuple or None
Optional list of acceptable option values.
- typestype or tuple of types or None
Optional type or list of acceptable option types.
- descstr
Optional description of the option.
- upperfloat or None
Maximum allowable value.
- lowerfloat or None
Minimum allowable value.
- check_validfunction or None
User-supplied function with arguments (name, value) that raises an exception if the value is not valid.
- allow_nonebool
If True, allow None as a value regardless of values or types.
- recordablebool
If True, add to recorder.
- set_functionNone or function
User-supplied function with arguments (Options metadata, value) that pre-processes value and returns a new value.
- deprecationstr or tuple or None
If None, it is not deprecated. If a str, use as a DeprecationWarning during __setitem__ and __getitem__. If a tuple of the form (msg, new_name), display msg as with str, and forward any __setitem__/__getitem__ to new_name.
When using the check_valid
argument, the expected function signature is:
- options_dictionary.check_valid(value)
Check the validity of value for the option with name.
- Parameters:
- namestr
Name of the option.
- valueany
Value for the option.
- Raises:
- ValueError
If value is not valid for option.
Option values are typically passed at component instantiation time as keyword arguments, which are automatically assigned into the option dictionary. The options are then available for use in the component’s other methods, such as setup and compute.
Alternatively, values can be set at a later time, in another method of the component (except for initialize) or outside of the component definition after the component is instantiated.
A Simple Example#
Options are commonly used to specify the shape or size of the component’s input and output variables, such as in this simple example.
"""
A component that multiplies a vector by 2, where the
size of the vector is given as an option of type 'int'.
"""
import numpy as np
import openmdao.api as om
class VectorDoublingComp(om.ExplicitComponent):
def initialize(self):
self.options.declare('size', types=int)
def setup(self):
size = self.options['size']
self.add_input('x', shape=size)
self.add_output('y', shape=size)
def setup_partials(self):
size = self.options['size']
self.declare_partials('y', 'x', val=2.,
rows=np.arange(size),
cols=np.arange(size))
def compute(self, inputs, outputs):
outputs['y'] = 2 * inputs['x']
import openmdao.api as om
prob = om.Problem()
prob.model.add_subsystem('double', VectorDoublingComp(size=3)) # 'size' is an option
prob.setup()
prob.set_val('double.x', [1., 2., 3.])
prob.run_model()
print(prob.get_val('double.y'))
[2. 4. 6.]
Not setting a default value when declaring an option implies that the value must be set by the user.
In this example, ‘size’ is required; We would have gotten an error if we:
Did not pass in ‘size’ when instantiating VectorDoublingComp and
Did not set its value in the code for VectorDoublingComp.
import openmdao.api as om
from openmdao.test_suite.components.options_feature_vector import VectorDoublingComp
prob = om.Problem()
prob.model.add_subsystem('double', VectorDoublingComp()) # 'size' not specified
try:
prob.setup()
except RuntimeError as err:
print(str(err))
'double' <class VectorDoublingComp>: Option 'size' is required but has not been set.
Option Names#
An option name can be any valid string. It is recommended that you use clear concise names. It is also recommended that you restrict the characters to those permitted in valid python names so that they can be passed into the system’s initialization arguments, but it is not required.
Option Types#
Options are not limited to simple types like int
. In the following example, the component takes a Numpy array as an option:
"""
A component that multiplies an array by an input value, where
the array is given as an option of type 'numpy.ndarray'.
"""
import numpy as np
import openmdao.api as om
class ArrayMultiplyComp(om.ExplicitComponent):
def initialize(self):
self.options.declare('array', types=np.ndarray)
def setup(self):
array = self.options['array']
self.add_input('x', 1.)
self.add_output('y', shape=array.shape)
def setup_partials(self):
self.declare_partials(of='*', wrt='*')
def compute(self, inputs, outputs):
outputs['y'] = self.options['array'] * inputs['x']
import numpy as np
import openmdao.api as om
prob = om.Problem()
prob.model.add_subsystem('a_comp', ArrayMultiplyComp(array=np.array([1, 2, 3])))
prob.setup()
prob.set_val('a_comp.x', 5.)
prob.run_model()
print(prob.get_val('a_comp.y'))
[ 5. 10. 15.]
It is even possible to provide a function as an option:
"""
A component that computes y = func(x), where func
is a function given as an option.
"""
from types import FunctionType
import openmdao.api as om
class UnitaryFunctionComp(om.ExplicitComponent):
def initialize(self):
self.options.declare('func', types=FunctionType, recordable=False)
def setup(self):
self.add_input('x')
self.add_output('y')
def setup_partials(self):
self.declare_partials('y', 'x', method='fd')
def compute(self, inputs, outputs):
func = self.options['func']
outputs['y'] = func(inputs['x'])
import openmdao.api as om
def my_func(x):
return x*2
prob = om.Problem()
prob.model.add_subsystem('f_comp', UnitaryFunctionComp(func=my_func))
prob.setup()
prob.set_val('f_comp.x', 5.)
prob.run_model()
print(prob.get_val('f_comp.y'))
[10.]
Providing Default Values#
One reason why using options is convenient is that a default value can be specified, making it optional to pass the value in during component instantiation.
"""
A component that computes y = a*x + b, where a and b
are given as an option of type 'numpy.ScalarType'.
"""
import numpy as np
import openmdao.api as om
class LinearCombinationComp(om.ExplicitComponent):
def initialize(self):
self.options.declare('a', default=1., types=np.ScalarType)
self.options.declare('b', default=1., types=np.ScalarType)
def setup(self):
self.add_input('x')
self.add_output('y')
def setup_partials(self):
self.declare_partials('y', 'x', val=self.options['a'])
def compute(self, inputs, outputs):
outputs['y'] = self.options['a'] * inputs['x'] + self.options['b']
import openmdao.api as om
prob = om.Problem()
prob.model.add_subsystem('linear', LinearCombinationComp(a=2.)) # 'b' not specified
prob.setup()
prob.set_val('linear.x', 3)
prob.run_model()
print(prob.get_val('linear.y'))
[7.]
In this example, both ‘a’ and ‘b’ are optional, so it is valid to pass in ‘a’, but not ‘b’.
Specifying Values or Types#
The parameters available when declaring an option allow a great deal of flexibility in specifying exactly what types and values are acceptable.
As seen above, the allowed types can be specified using the types
parameter. If an option is more limited, then the set of allowed values can be given with values
:
import numpy as np
import openmdao.api as om
class VectorDoublingComp(om.ExplicitComponent):
def initialize(self):
self.options.declare('size', values=[2, 4, 6, 8])
def setup(self):
size = self.options['size']
self.add_input('x', shape=size)
self.add_output('y', shape=size)
self.declare_partials('y', 'x', val=2.,
rows=np.arange(size),
cols=np.arange(size))
def compute(self, inputs, outputs):
outputs['y'] = 2 * inputs['x']
prob = om.Problem()
prob.model.add_subsystem('double', VectorDoublingComp(size=4))
prob.setup()
prob.set_val('double.x', [1., 2., 3., 4.])
prob.run_model()
print(prob.get_val('double.y'))
[2. 4. 6. 8.]
Warning
It is an error to attempt to specify both a list of acceptable values and a list of acceptable types.
Alternatively, the allowable values can be set using bounds and/or a validation function.
import numpy as np
import openmdao.api as om
def check_even(name, value):
if value % 2 != 0:
raise ValueError(f"Option '{name}' with value {value} must be an even number.")
class VectorDoublingComp(om.ExplicitComponent):
def initialize(self):
self.options.declare('size', types=int, lower=2, upper=8, check_valid=check_even)
def setup(self):
size = self.options['size']
self.add_input('x', shape=size)
self.add_output('y', shape=size)
self.declare_partials('y', 'x', val=2.,
rows=np.arange(size),
cols=np.arange(size))
def compute(self, inputs, outputs):
outputs['y'] = 2 * inputs['x']
try:
comp = VectorDoublingComp(size=5)
except Exception as err:
msg = str(err)
print(msg)
Option 'size' with value 5 must be an even number.
Setting Multiple Option Values Simultaneously#
The OptionsDictionary class is versatile and can be used in a variety of situations.
While all systems contain an options
attribute, users may wish to introduce other options for their systems or other classes using OpenMDAO’s OptionsDictionary.
After initialization, users might want the ability to modify more than one option at a time.
For this purpose, the set
method on OptionsDictionary exists.
import openmdao.api as om
prob = om.Problem()
linear_comp = prob.model.add_subsystem('linear', LinearCombinationComp())
# Set options a and b at the same time.
linear_comp.options.set(a=3., b=5.)
prob.setup()
prob.set_val('linear.x', 3)
prob.run_model()
print(prob.get_val('linear.y'))
[14.]
Note that the option name must be a valid python name in order to use this set
function. If your option has a name that is not valid in python (e.g., “aircraft:propulsion:NUM_ENGINES”), then you will have to set them individually with the dictionary interface.
Setting options throughout a problem model (Problem.model_options
)#
It is common for options to be passed down through a model tree from parent Group to child Group or Component. This can make the addition of a new option deep in the model a tedious procedure, since each parent system may need to have an option added to itself in order to know how to instantiate its children, and then actually take the given input option and pass it to the child.
There are a few ways to address this. For example, you might implement a function to declare the common options and call it in the relevant subsystems’ initialize
method.
Alternatively, those classes can subclass from a class which automatically adds the appropriate options.
For passing options to components nested deeply within the model, the OpenMDAO Problem
object provides an attribute named model_options
. Problem.model_options
is a standard dictionary, keyed by a string that serves as a glob filter for system pathnames.
For each corresponding value, a sub dictionary provides string keys of option names, with corresponding option values as the associated value.
When OpenMDAO begins setup
, these options are passed down to each system in the model
If a system’s pathname matches the key in model options, it will accept the value for each option which it possesses.
If it does not possess a particular option, that option value is ignored by that particular system.
Dictionaries in Python are ordered, so if a particular component matches multiple glob patterns specifying the same options, the last match will take precedence.
The following code demonstrates a model with several levels of depth where the component leaves accept options a
and b
.
import openmdao.api as om
prob = om.Problem()
c1 = prob.model.add_subsystem('c1', LinearCombinationComp())
g1 = prob.model.add_subsystem('g1', om.Group())
c2 = g1.add_subsystem('c2', LinearCombinationComp())
g2 = g1.add_subsystem('g2', om.Group())
c3 = g2.add_subsystem('c3', LinearCombinationComp())
g3 = g2.add_subsystem('g3', om.Group())
prob.model.connect('c1.y', 'g1.c2.x')
prob.model.connect('g1.c2.y', 'g1.g2.c3.x')
# Any component with options 'a' or 'b' accepts these values.
prob.model_options['*'] = {'a': 3., 'b': 5.}
prob.setup()
prob.set_val('c1.x', 3)
prob.run_model()
Showing detailed node information for the components below will indicate that options a
and b
have the correct values.
om.n2(prob)
Using glob patterns to set different option values in different systems.#
import openmdao.api as om
prob = om.Problem()
c1 = prob.model.add_subsystem('c1', LinearCombinationComp())
g1 = prob.model.add_subsystem('g1', om.Group())
c2 = g1.add_subsystem('c2', LinearCombinationComp())
g2 = g1.add_subsystem('g2', om.Group())
c3 = g2.add_subsystem('c3', LinearCombinationComp())
g3 = g2.add_subsystem('g3', om.Group())
prob.model.connect('c1.y', 'g1.c2.x')
prob.model.connect('g1.c2.y', 'g1.g2.c3.x')
# Only component 'c1' will accept these values.
prob.model_options['c1'] = {'a': 3., 'b': 5.}
# Any component that is not at the top of the model and whose name matches
# pattern `c?` will accept these values.
prob.model_options['*.c?'] = {'a': 4., 'b': 6.}
prob.setup()
prob.set_val('c1.x', 3)
prob.run_model()
print(prob.get_val('g1.g2.c3.y'))
[254.]
Modifying model_options
at the Group level#
Sometimes it may be desirable to have a Group modify the contents of model_options
during setup so that it can specify options for its children. For this purpose, group has a view of model_options
that is accessible via the Group.model_options
attribute.
During setup, a Group can set options for all of its children by appending something to model_options
with the appropriate path.
Remember that setup
is a top-down process, so model_options
must be modified no later than setup
in order for the appropriate information to be sent to the Group’s descendents.
The user should be cautious when using this functionality.
They have the ability to modify model options for other systems in the model tree (such as “sibling” systems and their descendents), but doing so may result in undefined behavior that will work sometimes and othertimes not depending on the specific ordering of the systems in the model tree.
In short, it’s good practice to prefix the model options with 'self.pathname'
when using this feature.
class MyGroup(om.Group):
def setup(self):
g1 = self.add_subsystem('g1', om.Group())
g1.add_subsystem('c1', LinearCombinationComp())
g1.add_subsystem('c2', LinearCombinationComp())
g1.add_subsystem('c3', LinearCombinationComp())
# Send options a and b to all descendents of this model.
self.model_options[f'{self.pathname}.*'] = {'a': 3., 'b': 5.}
g1.connect('c1.y', 'c2.x')
g1.connect('c2.y', 'c3.x')
p = om.Problem()
p.model.add_subsystem('my_group', MyGroup())
p.setup()
p.set_val('my_group.g1.c1.x', 4)
p.run_model()
print(p.get_val('my_group.g1.c3.y'))
[173.]
Adding a function to pre-process setting an option#
The OptionsDictionary
has support for custom pre-processing the value before it is set. One potential use for this is to provide a way to convert units while setting an option. The following example shows how to do this:
import openmdao.api as om
from openmdao.utils.units import convert_units
# TODO: Turn this into a test.
def units_setter(opt_meta, value):
"""
Check and convert new units tuple into
Parameters
----------
opt_meta : dict
Dictionary of entries for the option.
value : any
New value for the option.
Returns
-------
any
Post processed value to set into the option.
"""
new_val, new_units = value
old_val, units = opt_meta['val']
converted_val = convert_units(new_val, new_units, units)
return (converted_val, units)
class MyComp(om.ExplicitComponent):
def setup(self):
self.add_input('x', 3.0)
self.add_output('y', 3.0)
def initialize(self):
self.options.declare('length', default=(12.0, 'inch'),
set_function=units_setter)
def compute(self, inputs, outputs):
length = self.options['length'][0]
x = inputs['x']
outputs['y'] = length * x
class MySubgroup(om.Group):
def setup(self):
self.add_subsystem('mass', MyComp())
prob = om.Problem()
model = prob.model
model.add_subsystem('statics', MySubgroup())
prob.model_options['*'] = {'length': (2.0, 'ft')}
prob.setup()
prob.run_model()
print('The following should be 72 if the units convert correctly.')
print(prob.get_val('statics.mass.y'))
The following should be 72 if the units convert correctly.
[72.]