Determining Variable Units at Runtime#

It’s sometimes useful to create a component where the units of its inputs and/or outputs are determined by their connections. This allows us to create components representing general purpose vector or matrix operations such as norms, summations, integrators, etc., that set their units appropriately based on the model that they’re added to.

Turning on dynamic unit computation is straightforward. You just specify units_by_conn, copy_units and/or compute_units in your add_input or add_output calls when you add variables to your component.

Setting units_by_conn=True when adding an input or output variable will allow the units of that variable to be determined at runtime based on the variable that connects to it.

Setting copy_units=<var_name>, where <var_name> is the local name of another variable in your component, will take the units of the variable specified in <var_name> and use those units for the variable you’re adding.

Setting compute_units=<func>, where <func> is a function taking a dict arg that maps variable names to PhysicalUnits and returning the computed units, will set the units of the variable you’re adding as a function of the other variables in the same component of the opposite io type. For example, setting compute_units for an output z on a component with inputs x and y, would cause the supplied function to be called with a dict of the form {x: x_units, y: y_units}, so the computed units of z could be a function of x_units and y_units. Note that the compute_units function is not called until all units of the opposite io type are known for that component.

PhysicalUnits can be combined into expressions, and the result of the expression will be a PhysicalUnit with the correct combined units. This is generally the simplest way to compute the units inside of compute_units. To get the actual unit string, you can use the name() method of the PhysicalUnit, but generally you won’t need to do that since you can just return the PhysicalUnit directly and the framework will convert it to a unit string automatically.

Note that units_by_conn can be specified for outputs as well as for inputs, as can copy_units and compute_units. This means that units information can propagate through the model in either forward or reverse. If you specify both units_by_conn and either copy_units or compute_units for your component’s variables, it will allow their units to be resolved whether known units have been defined upstream or downstream of your component in the model.

The following component with input x and output y can have its units set by known units that are either upstream or downstream.

import openmdao.api as om


class DynPartialsComp(om.ExplicitComponent):
    def setup(self):
        self.add_input('x', units_by_conn=True, copy_units='y')
        self.add_output('y', units_by_conn=True, copy_units='x')

    def compute(self, inputs, outputs):
        outputs['y'] = inputs['x'] * 3.

The following example demonstrates the flow of units information in the forward direction, where the IndepVarComp has known units, and the DynPartialsComp and the ExecComp have units set dynamically.

p = om.Problem()
p.model.add_subsystem('indeps', om.IndepVarComp('x', units='ft'))
p.model.add_subsystem('comp', DynPartialsComp())
sink = p.model.add_subsystem('sink', om.ExecComp('y=x',
                                                 x={'units_by_conn': True, 'copy_units': 'y'},
                                                 y={'units_by_conn': True, 'copy_units': 'x'}))
p.model.connect('indeps.x', 'comp.x')
p.model.connect('comp.y', 'sink.x')
p.setup()
p.run_model()

print(sink._get_var_meta('y', 'units'))
ft

And the following shows units information flowing in reverse, from the known units of sink.x to the unknown units of the output comp.y, then to the input comp.x, then on to the connected auto-IndepVarComp output.

p = om.Problem()
comp = p.model.add_subsystem('comp', DynPartialsComp())
p.model.add_subsystem('sink', om.ExecComp('y=x', units='m'))
p.model.connect('comp.y', 'sink.x')
p.setup()
p.run_model()

print(comp._get_var_meta('x', 'units'))
m

Finally, an example use of compute_units is shown below. We have a component with dynamic units that multiplies two matrices, so the output O units are a combination of the units of both inputs, M and N. In this case we use a lambda function to compute the output units.

class DynComputeComp(om.ExplicitComponent):
    def setup(self):
        self.add_input('M', units_by_conn=True)
        self.add_input('N', units_by_conn=True)

        # use a lambda function to compute the output units based on the input units
        self.add_output('O', compute_units=lambda units: units['M'] * units['N'])

    def compute(self, inputs, outputs):
        outputs['O'] = inputs['M'] @ inputs['N']

p = om.Problem()
indeps = p.model.add_subsystem('indeps', om.IndepVarComp())
indeps.add_output('M', units='ft')
indeps.add_output('N', units='lbf')
comp = p.model.add_subsystem('comp', DynComputeComp())
p.model.connect('indeps.M', 'comp.M')
p.model.connect('indeps.N', 'comp.N')
p.setup()
p.run_model()
print('input units:', comp._get_var_meta('M', 'units'), 'and', comp._get_var_meta('N', 'units'))
print('output units:', comp._get_var_meta('O', 'units'))
input units: ft and lbf
output units: ft*lbf

Debugging#

Sometimes, when the units of some variables are unresolvable, it can be difficult to understand why. There is an OpenMDAO command line tool, openmdao view_dyn_units, that can be used to show a graph of the variables with dynamic units and any variables with known units that connect directly to them. Each node in the graph is a variable, and each edge is a connection between that variable and another. Note that this connection does not have to be a connection in the normal OpenMDAO sense. It could be a connection internal to a component created by declaring a copy_units in the metadata of one variable that refers to another variable.

The nodes in the graph are colored to make it easier to locate static/dynamic/unresolved variable units. Variables with ‘static’ known units are colored green, variables with dynamic units that have been resolved are colored blue, and any variables with unresolved units are colored red. Each node is labeled with the units of the variable, if known, or a ‘?’ if unknown, followed by the absolute pathname of the variable in the model.

The plot is somewhat crude and the node labels sometimes overlap, but it’s possible to zoom in to part of the graph to make it more readable using the button that looks like a magnifying glass.