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:

  1. Did not pass in ‘size’ when instantiating VectorDoublingComp and

  2. 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 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.]

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 unittest

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.]