Setting and Getting Component Variables#
You will both set and get the values in the dimensional and unscaled form via the Problem class. If you have promoted both inputs and outputs to the same name, then the output takes precedence and it determines the units you should work in.
Outputs and Independent Variables#
To set or get the output variable, you reference it by its promoted name. In the regular Sellar problem, all the variables have been promoted to the top of the model. So to get the value of the “y1” output defined in the SellarDis1WithDerivatives component, you would do the following:
SellarDerivatives
class definition
class SellarDerivatives(om.Group):
"""
Group containing the Sellar MDA. This version uses the disciplines with derivatives.
"""
def setup(self):
self.add_subsystem('d1', SellarDis1withDerivatives(), promotes=['x', 'z', 'y1', 'y2'])
self.add_subsystem('d2', SellarDis2withDerivatives(), promotes=['z', 'y1', 'y2'])
self.add_subsystem('obj_cmp', om.ExecComp('obj = x**2 + z[1] + y1 + exp(-y2)', obj=0.0,
x=0.0, z=np.array([0.0, 0.0]), y1=0.0, y2=0.0),
promotes=['obj', 'x', 'z', 'y1', 'y2'])
self.add_subsystem('con_cmp1', om.ExecComp('con1 = 3.16 - y1', con1=0.0, y1=0.0),
promotes=['con1', 'y1'])
self.add_subsystem('con_cmp2', om.ExecComp('con2 = y2 - 24.0', con2=0.0, y2=0.0),
promotes=['con2', 'y2'])
self.set_input_defaults('x', 1.0)
self.set_input_defaults('z', np.array([5.0, 2.0]))
import openmdao.api as om
from openmdao.test_suite.components.sellar_feature import SellarDerivatives
prob = om.Problem(model=SellarDerivatives())
prob.model.nonlinear_solver = om.NonlinearBlockGS()
prob.setup()
prob.set_val('x', 2.75)
prob.run_model()
print(prob.get_val('y1'))
NL: NLBGS Converged in 8 iterations
[27.30491784]
You use the same syntax when working with the independent variables of your problem. Independent variables can be used as design variables by a Driver or set directly by a user. OpenMDAO requires that every input variable must have a source. The ultimate source for any flow of data in an OpenMDAO model is a special component, IndepVarComp, that does not have any inputs. You can leave some of your inputs unconnected and OpenMDAO will automatically create an IndepVarComp called _auto_ivc
with an output that connects to each input, or you can create your own IndepVarComp
manually. For example, consider our paraboloid tutorial problem which has two independent variables: x
and y
.
These could be defined manually and set as follows:
Paraboloid
class definition
class Paraboloid(om.ExplicitComponent):
"""
Evaluates the equation f(x,y) = (x-3)^2 + xy + (y+4)^2 - 3.
"""
def setup(self):
self.add_input('x', val=0.0)
self.add_input('y', val=0.0)
self.add_output('f_xy', val=0.0)
def setup_partials(self):
self.declare_partials('*', '*')
def compute(self, inputs, outputs):
"""
f(x,y) = (x-3)^2 + xy + (y+4)^2 - 3
Optimal solution (minimum): x = 6.6667; y = -7.3333
"""
x = inputs['x']
y = inputs['y']
outputs['f_xy'] = (x-3.0)**2 + x*y + (y+4.0)**2 - 3.0
def compute_partials(self, inputs, partials):
"""
Jacobian for our paraboloid.
"""
x = inputs['x']
y = inputs['y']
partials['f_xy', 'x'] = 2.0*x - 6.0 + y
partials['f_xy', 'y'] = 2.0*y + 8.0 + x
from openmdao.test_suite.components.paraboloid import Paraboloid
prob = om.Problem()
model = prob.model
model.add_subsystem('p1', om.IndepVarComp('x', 0.0), promotes=['x'])
model.add_subsystem('p2', om.IndepVarComp('y', 0.0), promotes=['y'])
model.add_subsystem('comp', Paraboloid(), promotes=['x', 'y', 'f_xy'])
prob.setup()
prob['x'] = 2.
prob['y'] = 10.
prob.run_model()
print(prob.get_val('f_xy'))
[214.]
or, the inputs x
and y
could be left unconnected and OpenMDAO would connect them to
outputs on _auto_ivc
. The names of the output variables on _auto_ivc
are sequentially
named as they are created and are of the form v0
, v1
, etc., but you don’t really need to know
those names. You can just interact with the inputs that those outputs are connected to
and the framework will ensure that the proper values are set into the outputs (and into any other
inputs connected to the same output). Here’s what the paraboloid tutorial problem looks like
without declaring the IndepVarComp:
from openmdao.test_suite.components.paraboloid import Paraboloid
prob = om.Problem()
prob.model.add_subsystem('comp', Paraboloid(), promotes=['x', 'y', 'f_xy'])
prob.setup()
prob.set_val('x', 2.)
prob.set_val('y', 10.)
prob.run_model()
print(prob.get_val('f_xy'))
[214.]
As we said above, outputs are always referenced via their promoted name. So if you built the Sellar problem using connections (see Sellar), instead of promoting everything, then you would access the variables like this:
SellarDerivativesConnected
class definition
class SellarDerivativesConnected(om.Group):
"""
Group containing the Sellar MDA. This version uses the disciplines with derivatives.
"""
def setup(self):
self.add_subsystem('d1', SellarDis1withDerivatives(), promotes=['x', 'z'])
self.add_subsystem('d2', SellarDis2withDerivatives(), promotes=['z'])
self.add_subsystem('obj_cmp', om.ExecComp('obj = x**2 + z[1] + y1 + exp(-y2)',
z=np.array([0.0, 0.0]), x=0.0),
promotes=['x', 'z'])
self.add_subsystem('con_cmp1', om.ExecComp('con1 = 3.16 - y1'))
self.add_subsystem('con_cmp2', om.ExecComp('con2 = y2 - 24.0'))
self.connect('d1.y1', ['d2.y1', 'obj_cmp.y1', 'con_cmp1.y1'])
self.connect('d2.y2', ['d1.y2', 'obj_cmp.y2', 'con_cmp2.y2'])
self.set_input_defaults('x', 1.0)
self.set_input_defaults('z', np.array([5.0, 2.0]))
from openmdao.test_suite.components.sellar import SellarDerivativesConnected
prob = om.Problem(model=SellarDerivativesConnected())
prob.model.nonlinear_solver = om.NonlinearBlockGS()
prob.setup()
prob.set_val('x', 2.75)
prob.run_model()
print(prob.get_val('x'))
NL: NLBGS Converged in 8 iterations
[2.75]
print(prob.get_val('d1.y1'))
[27.30491784]
Working with Array Variables#
When you have an array variable, for convenience we allow you to set the value with any properly-sized array, list, or tuple. In other words, the shape of the list has to match the shape of the actual data.
from openmdao.test_suite.components.sellar_feature import SellarDerivatives
prob = om.Problem(model=SellarDerivatives())
prob.model.nonlinear_solver = om.NonlinearBlockGS()
prob.setup()
# default value from the class definition
print(prob.get_val('x'))
[1.]
prob.set_val('x', 2.75)
print(prob.get_val('x'))
2.75
# default value from the class definition
print(prob.get_val('z'))
[5. 2.]
prob.set_val('z', [1.5, 1.5])
print(prob.get_val('z'))
[1.5 1.5]
prob.run_model()
print(prob.get_val('y1'))
print(prob.get_val('y2'))
NL: NLBGS Converged in 9 iterations
[5.43379017]
[5.33104916]
import numpy as np
prob.set_val('z', np.array([2.5, 2.5])) # for convenience we convert the list to an array.
print(prob.get_val('z'))
[2.5 2.5]
prob.run_model()
print(prob.get_val('y1'))
print(prob.get_val('y2'))
NL: NLBGS Converged in 9 iterations
[9.8716174]
[8.14191302]
prob = om.Problem()
model = prob.model
model.add_subsystem(name='indeps',
subsys=om.IndepVarComp(name='X_c', shape=(3, 1)))
prob.setup()
new_val = -5*np.ones((3, 1))
prob['indeps.X_c'] = new_val
prob.final_setup()
print(prob['indeps.X_c'])
[[-5.]
[-5.]
[-5.]]
new_val = 2.5*np.ones(3)
prob['indeps.X_c'][:, 0] = new_val
prob.final_setup()
print(prob['indeps.X_c'])
print(prob['indeps.X_c'][:, 0])
[[2.5]
[2.5]
[2.5]]
[2.5 2.5 2.5]
Residuals#
If you want to look at the residual values associated with any particular output variable, you will reference them using the same naming conventions the outputs. Also like outputs, you will be given the residuals in the unscaled dimensional form.
from openmdao.test_suite.components.sellar_feature import SellarDerivatives
model=SellarDerivatives()
model.nonlinear_solver = om.NonlinearBlockGS()
model.linear_solver = om.ScipyKrylov()
prob = om.Problem(model)
prob.setup()
prob.set_val('z', [1.5, 1.5])
prob.run_model()
inputs, outputs, residuals = prob.model.get_nonlinear_vectors()
print(residuals['y1'])
print(residuals['y2'])
NL: NLBGS Converged in 9 iterations
[7.66033903e-10]
[1.97471373e-10]
Inputs#
You can get or set the value of an input variable using either its promoted name or its absolute
name. If you reference it by its promoted name, however, and that
input is connected to an output because the input and output are promoted to the same name, then
the promoted name will be interpreted as that of the output, and the units will be assumed to be
those of the output as well. If the input has not been connected to an output then the framework
will connect it automatically to an output of _auto_ivc
. In this case, setting or getting using
the input name will cause the framework to assume the units are those of the input, assuming
there is no abiguity in units for example.
Connected Inputs Without a Source#
If multiple inputs have been promoted to the same name but not connected manually to an output or promoted
to the same name as an output, then again the framework will connect all of those inputs to an
_auto_ivc
output. If, however, there is any difference between the units or values of any of those inputs,
then you must tell the framework what units and/or values to use when creating the corresponding
_auto_ivc
output. You do this by calling the set_input_defaults function using the promoted
input name on a Group that contains all of the promoted inputs.
Below is an example of what you’ll see if you do not call set_input_defaults
to disambiguate
your units and/or values:
prob = om.Problem(name='no_set_input_defaults')
prob.model.add_subsystem('C1', om.ExecComp('y=x*2.',
x={'val': 1.0, 'units': 'ft'},
y={'val': 0.0, 'units': 'ft'}),
promotes=['x'])
prob.model.add_subsystem('C2', om.ExecComp('y=x*3.',
x={'val': 1.0, 'units': 'inch'},
y={'val': 0.0, 'units': 'inch'}),
promotes=['x'])
try:
prob.setup()
except RuntimeError as err:
print(str(err))
Collected errors for problem 'no_set_input_defaults':
<model> <class Group>: The following inputs, ['C1.x', 'C2.x'], promoted to 'x', are connected but their metadata entries ['units', 'val'] differ. Call <group>.set_input_defaults('x', units=?, val=?), where <group> is the model to remove the ambiguity.
The next example shows a successful run after calling set_input_defaults
:
prob = om.Problem()
G1 = prob.model.add_subsystem('G1', om.Group())
G1.add_subsystem('C1', om.ExecComp('y=x*2.',
x={'val': 1.0, 'units': 'cm'},
y={'val': 0.0, 'units': 'cm'}),
promotes=['x'])
G1.add_subsystem('C2', om.ExecComp('y=x*3.',
x={'val': 1.0, 'units': 'mm'},
y={'val': 0.0, 'units': 'mm'}),
promotes=['x'])
# units and value to use for the _auto_ivc output are ambiguous. This fixes that.
G1.set_input_defaults('x', units='m', val=1.0)
prob.setup()
# set G1.x to 2.0 m, based on the units we gave in the set_input_defaults call
prob.set_val('G1.x', 2.)
prob.run_model()
# we gave 'G1.x' units of 'm' in the set_input_defaults call
print(prob.get_val('G1.x'))
[2.]
# using absolute value will give us the value of the input C1.x, in its units of 'cm'
print(prob.get_val('G1.C1.x'))
[200.]
# using absolute value will give us the value of the input C2.x, in its units of 'mm'
print(prob.get_val('G1.C2.x'))
[2000.]
Another possible scenario is to have multiple inputs promoted to the same name when those inputs have
different units, but then connecting them manually to an output using the connect
function.
In this case, the framework will not raise an exception during setup if set_input_defaults
was not
called as it does in the case of multiple promoted inputs that connected to _auto_ivc
. However,
if the user attempts to set or get the input using the promoted name, the framework will raise an
exception if set_input_defaults
has not been called to disambiguate the units of the promoted
input. The reason for this difference is that in the unconnected case, the framework won’t know
what value and units to assign to the _auto_ivc
output if they’re ambiguous. In the manually
connected case, the value and units of the output have already been supplied by the user, and
the only time that there’s an ambiguity is if the user tries to access the inputs using their
promoted name.
Specifying Units
You can also set an input or request the value of any variable in a different unit than its declared
unit, and OpenMDAO will
perform the conversion for you. This is done with the Problem
methods get_val
and set_val
.
prob = om.Problem()
prob.model.add_subsystem('comp', om.ExecComp('y=x+1.',
x={'val': 100.0, 'units': 'cm'},
y={'units': 'm'}))
prob.setup()
prob.run_model()
print(prob.get_val('comp.x'))
[100.]
print(prob.get_val('comp.x', 'm'))
[1.]
prob.set_val('comp.x', 10.0, 'mm')
print(prob.get_val('comp.x'))
[1.]
print(prob.get_val('comp.x', 'm'))
[0.01]
When dealing with arrays, you can set or get specific indices or index ranges by adding the “indices” argument to the calls:
prob = om.Problem()
prob.model.add_subsystem('comp', om.ExecComp('y=x+1.',
x={'val': np.array([100.0, 33.3]), 'units': 'cm'},
y={'shape': (2, ), 'units': 'm'}))
prob.setup()
prob.run_model()
print(prob.get_val('comp.x'))
print(prob.get_val('comp.x', 'm'))
print(prob.get_val('comp.x', 'km', indices=[0]))
[100. 33.3]
[1. 0.333]
[0.001]
prob.set_val('comp.x', 10.0, 'mm')
print(prob.get_val('comp.x'))
print(prob.get_val('comp.x', 'm', indices=0))
[1. 1.]
0.01
prob.set_val('comp.x', 50.0, 'mm', indices=[1])
print(prob.get_val('comp.x'))
print(prob.get_val('comp.x', 'm', indices=1))
[1. 5.]
0.05
An alternate method of specifying the indices is by making use of the slicer
object. This
object serves as a helper function allowing the user to specify the indices value using the same syntax as you would when accessing a numpy array. This example shows that usage.
prob = om.Problem()
prob.model.add_subsystem('comp', om.ExecComp('y=x+1.',
x={'val': np.array([[1., 2.], [3., 4.]]), },
y={'shape': (2, 2), }))
prob.setup()
prob.run_model()
print(prob.get_val('comp.x', indices=om.slicer[:, 0]))
[1. 3.]
print(prob.get_val('comp.x', indices=om.slicer[0, 1]))
2.0
print(prob.get_val('comp.x', indices=om.slicer[1, -1]))
4.0
prob.set_val('comp.x', [5., 6.], indices=om.slicer[:,0])
print(prob.get_val('comp.x', indices=om.slicer[:, 0]))
[5. 6.]
prob.run_model()
print(prob.get_val('comp.y', indices=om.slicer[:, 0]))
[6. 7.]
Retrieving Remote Variables#
If you’re running under MPI, the Problem.get_val
method also has a get_remote arg that allows
you to get the value of a variable even if it’s not local to the current MPI process. For example,
the code below will retrieve the value of foo.bar.x
in all processes, whether the variable is
local or not.
val = prob.get_val('foo.bar.x', get_remote=True)
Warning
If get_remote
is True, get_val
makes a collective MPI call, so make sure to call it
in all ranks of the Problem’s MPI communicator. Otherwise, collective calls made
in different ranks will get out of sync and result in cryptic MPI errors.
Testing if a Variable or System is Local#
If you want to know if a given variable or system is local to the current process, the
Problem.is_local
method will tell you. For example:
if prob.is_local('foo.bar.x'):
print("foo.bar.x is local!")