Linking Variables with Promotion vs. Connection#
In the previous tutorial, we built up a model of the Sellar problem using two disciplinary components and a few ExecComps
.
In order to get OpenMDAO to pass the data between all the components,
we linked everything up using promoted variables so that data passed from outputs to inputs with the same promoted name.
Promoting variables is often a convenient way to establish the data passing links from outputs to inputs.
However, you can also use calls to the connect
method in order to link outputs to inputs without having to
promote anything.
Here is how you would define the same Sellar model using:
Variable promotion
Connect statements
Both variable promotion and connect statements
All three will give the exact same answer, but the way you address the variables will be slightly different in each one.
Variable Promotion#
Input and output variables can be promoted when a subsystem is added:
SellarDis1
class definition
class SellarDis1(om.ExplicitComponent):
"""
Component containing Discipline 1 -- no derivatives version.
"""
def __init__(self, units=None, scaling=None):
super().__init__()
self.execution_count = 0
self._units = units
self._do_scaling = scaling
def setup(self):
if self._units:
units = 'ft'
else:
units = None
if self._do_scaling:
ref = .1
else:
ref = 1.
# Global Design Variable
self.add_input('z', val=np.zeros(2), units=units)
# Local Design Variable
self.add_input('x', val=0., units=units)
# Coupling parameter
self.add_input('y2', val=1.0, units=units)
# Coupling output
self.add_output('y1', val=1.0, lower=0.1, upper=1000., units=units, ref=ref)
def setup_partials(self):
# Finite difference everything
self.declare_partials('*', '*', method='fd')
def compute(self, inputs, outputs):
"""
Evaluates the equation
y1 = z1**2 + z2 + x1 - 0.2*y2
"""
z1 = inputs['z'][0]
z2 = inputs['z'][1]
x1 = inputs['x']
y2 = inputs['y2']
outputs['y1'] = z1**2 + z2 + x1 - 0.2*y2
self.execution_count += 1
SellarDis2
class definition
class SellarDis2(om.ExplicitComponent):
"""
Component containing Discipline 2 -- no derivatives version.
"""
def __init__(self, units=None, scaling=None):
super().__init__()
self.execution_count = 0
self._units = units
self._do_scaling = scaling
def setup(self):
if self._units:
units = 'inch'
else:
units = None
if self._do_scaling:
ref = .18
else:
ref = 1.
# Global Design Variable
self.add_input('z', val=np.zeros(2), units=units)
# Coupling parameter
self.add_input('y1', val=1.0, units=units)
# Coupling output
self.add_output('y2', val=1.0, lower=0.1, upper=1000., units=units, ref=ref)
def setup_partials(self):
# Finite difference everything
self.declare_partials('*', '*', method='fd')
def compute(self, inputs, outputs):
"""
Evaluates the equation
y2 = y1**(.5) + z1 + z2
"""
z1 = inputs['z'][0]
z2 = inputs['z'][1]
y1 = inputs['y1']
# Note: this may cause some issues. However, y1 is constrained to be
# above 3.16, so lets just let it converge, and the optimizer will
# throw it out
if y1.real < 0.0:
y1 *= -1
outputs['y2'] = y1**.5 + z1 + z2
self.execution_count += 1
import numpy as np
import openmdao.api as om
from openmdao.test_suite.components.sellar import SellarDis1, SellarDis2
class SellarMDA(om.Group):
"""
Group containing the Sellar MDA.
"""
def setup(self):
cycle = self.add_subsystem('cycle', om.Group(), promotes=['*'])
cycle.add_subsystem('d1', SellarDis1(),
promotes_inputs=['x', 'z', 'y2'],
promotes_outputs=['y1'])
cycle.add_subsystem('d2', SellarDis2(),
promotes_inputs=['z', 'y1'],
promotes_outputs=['y2'])
cycle.set_input_defaults('x', 1.0)
cycle.set_input_defaults('z', np.array([5.0, 2.0]))
# Nonlinear Block Gauss Seidel is a gradient free solver
cycle.nonlinear_solver = om. NonlinearBlockGS()
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', 'y1', 'y2', 'obj'])
self.add_subsystem('con_cmp1', om.ExecComp('con1 = 3.16 - y1'),
promotes=['con1', 'y1'])
self.add_subsystem('con_cmp2', om.ExecComp('con2 = y2 - 24.0'),
promotes=['con2', 'y2'])
prob = om.Problem()
prob.model = SellarMDA()
prob.setup()
prob.set_val('x', 2.0)
prob.set_val('z', [-1., -1.])
prob.run_model()
=====
cycle
=====
NL: NLBGS Converged in 10 iterations
Alternatively, variables can be promoted when a group is configured:
import numpy as np
from openmdao.test_suite.components.sellar import SellarDis1, SellarDis2
class SellarMDA(om.Group):
"""
Group containing the Sellar MDA.
"""
def setup(self):
# set up model hierarchy
cycle = self.add_subsystem('cycle', om.Group())
cycle.add_subsystem('d1', SellarDis1())
cycle.add_subsystem('d2', SellarDis2())
cycle.nonlinear_solver = om. NonlinearBlockGS()
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))
self.add_subsystem('con_cmp1', om.ExecComp('con1 = 3.16 - y1'))
self.add_subsystem('con_cmp2', om.ExecComp('con2 = y2 - 24.0'))
def configure(self):
# connect everything via promotes
self.cycle.promotes('d1', inputs=['x', 'z', 'y2'], outputs=['y1'])
self.cycle.promotes('d2', inputs=['z', 'y1'], outputs=['y2'])
self.promotes('cycle', any=['*'])
self.promotes('obj_cmp', any=['x', 'z', 'y1', 'y2', 'obj'])
self.promotes('con_cmp1', any=['con1', 'y1'])
self.promotes('con_cmp2', any=['con2', 'y2'])
prob = om.Problem()
prob.model = SellarMDA()
prob.setup()
prob.set_val('x', 2.0)
prob.set_val('z', [-1., -1.])
prob.run_model()
=====
cycle
=====
NL: NLBGS Converged in 10 iterations
There are a few important details to note:
The promoted name of an output has to be unique within that level of the hierarchy (i.e. you can’t have two outputs with the same name)
You are allowed to have multiple inputs promoted to the same name.
You can use glob patterns to promote lots of variables without specifying them all, but try to limit your usage of
promotes=['*']
. Though it may seem like a convenient way to do things, it can make it difficult for other people who are reading your code to understand which variables are connected to each other. It is acceptable to usepromotes=['*']
in cases where it won’t cause confusion, for example withcycle
, which only exists to allow for the nonlinear solver to converge the two components. Another example of when it would be safe to usepromotes=['*']
would be if you haveExecComps
that make it clear what the I/O of that component is anyway.
Note
For a more detailed set of examples for how to promote variables, check out the feature doc on adding sub-systems to a group.There are some more advanced things you can do, such as variable name aliasing and connecting a sub-set of indices from the output array of one component to the input of another.
Connect Statements#
The exact same model results can be achieved using connect
statements instead of promotions. However, take careful note of how the variables are addressed in those connect and print statements.
import numpy as np
from openmdao.test_suite.components.sellar import SellarDis1, SellarDis2
class SellarMDAConnect(om.Group):
"""
Group containing the Sellar MDA. This version uses the disciplines without derivatives.
"""
def setup(self):
cycle = self.add_subsystem('cycle', om.Group(),
promotes_inputs=['x', 'z'])
cycle.add_subsystem('d1', SellarDis1(),
promotes_inputs=['x', 'z'])
cycle.add_subsystem('d2', SellarDis2(),
promotes_inputs=['z'])
cycle.connect('d1.y1', 'd2.y1')
cycle.connect('d2.y2', 'd1.y2')
cycle.set_input_defaults('x', 1.0)
cycle.set_input_defaults('z', np.array([5.0, 2.0]))
# Nonlinear Block Gauss Seidel is a gradient free solver
cycle.nonlinear_solver = om.NonlinearBlockGS()
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_inputs=['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('cycle.d1.y1', ['obj_cmp.y1', 'con_cmp1.y1'])
self.connect('cycle.d2.y2', ['obj_cmp.y2', 'con_cmp2.y2'])
prob = om.Problem()
prob.model = SellarMDAConnect()
prob.setup()
prob.set_val('x', 2.0)
prob.set_val('z', [-1., -1.])
prob.run_model()
=====
cycle
=====
NL: NLBGS Converged in 10 iterations
Variable Promotion and Connect Statements#
It is also possible to combine promotion and connection in a single model. Here, notice that we do not have to add “cycle” in front of anything, because we promoted all the variables up from that group.
import numpy as np
from openmdao.test_suite.components.sellar import SellarDis1, SellarDis2
class SellarMDAPromoteConnect(om.Group):
"""
Group containing the Sellar MDA. This version uses the disciplines without derivatives.
"""
def setup(self):
cycle = self.add_subsystem('cycle', om.Group(), promotes=['*'])
cycle.add_subsystem('d1', SellarDis1(),
promotes_inputs=['x', 'z'])
cycle.add_subsystem('d2', SellarDis2(),
promotes_inputs=['z'])
cycle.connect('d1.y1', 'd2.y1')
cycle.connect('d2.y2', 'd1.y2')
cycle.set_input_defaults('x', 1.0)
cycle.set_input_defaults('z', np.array([5.0, 2.0]))
# Nonlinear Block Gauss Seidel is a gradient free solver
cycle.nonlinear_solver = om.NonlinearBlockGS()
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_inputs=['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', ['con_cmp1.y1', 'obj_cmp.y1'])
self.connect('d2.y2', ['con_cmp2.y2', 'obj_cmp.y2'])
prob = om.Problem()
prob.model = SellarMDAPromoteConnect()
prob.setup()
prob.set_val('x', 2.0)
prob.set_val('z', [-1., -1.])
prob.run_model()
=====
cycle
=====
NL: NLBGS Converged in 10 iterations