Replacing a group with a component
In this demonstration we show how we can replace a group of components with a single component by using the functionality implemented in the standard_evaluator library.
We first load all required libraries and methods.
import typing
import openmdao.api as om
import openmdao.core.system as oms
from standard_evaluator import get_interface, show_structure, create_problem, get_state, set_state, load_state, save_state
from standard_evaluator import load_assembly, save_assembly, convert_om_var, get_opt_problem, set_opt_problem
from standard_evaluator import Variable, FloatVariable, IntVariable, ArrayVariable, OptProblem
matlabengine not available.
Creating the OpenMDAO assembly
Next we generate an OpenMDAO assembly with wo sub-groups, one of which represents an aero analyses with two components. The first component is a stand-in for a geometry builder, and the second component is a stand-in for a Computational Fluid Dynamics (CFD) component. There is a link between those two components which can be thought of as a large, multi-dimensional grid. In this example we are just using a 1-D array, but this can be thought of as a large three dimensional grid.
The idea is that the geometry builder takes as inputs a small number of geometry parameters (in this case all individual floats), and produces a large, even multi-dimensional output. The CFD component takes that output from the geometry builder and some parameters and performs calculations that together with a post-processing step (which we did not model) creates a small number of outputs (in this example we only use a single float output).
After building the assembly we add optimization design variables, objectives, and constraints, run the assembly, and show the values of all inputs and outputs.
grid_size = 20
class Sub1(om.Group):
def setup(self):
self.add_subsystem('z1', om.ExecComp("y = a + b + c + d",
),
promotes_inputs=[
'a',
('b', 'my_alias'),
'd',
])
self.add_subsystem('z2', om.ExecComp("v = a + b + c + d",
),
promotes_inputs=['c', 'd'],
promotes_outputs=['*'])
self.connect('z1.y', 'c')
class Aero(om.Group):
def setup(self):
self.add_subsystem('geometry', om.ExecComp([f"grid = (q + z *l)*ones({grid_size})", f"bla = outer(ones({grid_size}), ones({grid_size}))"],
grid={'tags': 'internal', 'shape': (grid_size)},
bla ={'tags': 'internal', 'shape': (grid_size, grid_size)}
),
promotes_inputs=['q', 'z', 'l'])
self.add_subsystem('cfd', om.ExecComp(f"drag = (inner(grid, ones({grid_size})) + r * k)/1000",
grid={'tags': 'internal', 'shape_by_conn': True},
drag={'units': 'N'}),
promotes_inputs=['r', 'k'],
promotes_outputs=['drag'])
self.connect('geometry.grid', 'cfd.grid')
class Intermediate3(om.Group):
def setup(self):
self.add_subsystem('sub1', Sub1(),
promotes_inputs=['a'])
self.add_subsystem('aero', Aero())
self.connect('sub1.v', 'aero.k')
prob = om.Problem(model=Intermediate3())
# Define an optimization problem
prob.model.add_design_var('a', lower=-50, upper=50, scaler= .1, adder=30.)
prob.model.add_design_var('sub1.my_alias', lower=-50, upper=50)
prob.model.add_objective('aero.drag')
prob.model.add_constraint('sub1.v', lower=0, upper=10.)
prob.model.add_constraint('sub1.z1.y', lower=-32.32, upper=1829.)
prob.setup()
prob.final_setup()
prob.run_model()
_ = prob.model.list_inputs(shape=True)
_ = prob.model.list_outputs(shape=True)
14 Input(s) in 'model'
varname val shape prom_name
---------- ------------------ ----- -------------
sub1
z1
a [1.] (1,) a
b [1.] (1,) sub1.my_alias
c [1.] (1,) sub1.z1.c
d [1.] (1,) sub1.d
z2
a [1.] (1,) sub1.z2.a
b [1.] (1,) sub1.z2.b
c [4.] (1,) sub1.c
d [1.] (1,) sub1.d
aero
geometry
l [1.] (1,) aero.l
q [1.] (1,) aero.q
z [1.] (1,) aero.z
cfd
grid |8.94427191| (20,) aero.cfd.grid
k [7.] (1,) aero.k
r [1.] (1,) aero.r
5 Explicit Output(s) in 'model'
varname val shape prom_name
---------- ------------------ -------- ------------------
sub1
z1
y [4.] (1,) sub1.z1.y
z2
v [7.] (1,) sub1.v
aero
geometry
bla |20.0| (20, 20) aero.geometry.bla
grid |8.94427191| (20,) aero.geometry.grid
cfd
drag [0.047] (1,) aero.drag
0 Implicit Output(s) in 'model'
Next we look at the N2 diagram of this assembly. Note that the grid output from the geometry component only goes to the cfd component, which means we can mark it as an internal variable.
import os
# Check if running in VS Code
if 'VSCODE_PID' in os.environ:
display_in_notebook = False
else:
display_in_notebook = True
om.n2(prob, 'replace.html', display_in_notebook=display_in_notebook, )
We can now take this OpenMDAO assembly and create a Pydantic description of this assembly using the get_interface method. This description can be easily saved to a JSON file, and we can also load the information from a JSON file. We also get the state of the OpenMDAO problem which will be saved in a different, binary HDF5 formatted file. The state variable right now is just a dictionary with values of all inputs and outputs of the problem at the top level.
In this demonstration we do not need to save either the assembly information or the state to disk since we can manipulate them in memory. See the other demonstrations for how to store and load assemblies and state.
info = get_interface(prob.model)
state = get_state(prob, info)
Replacing a group with a Surrogate or simplified model
In the next step we creata a new OpenMDAO component that we will want to use to replace the complex aero group in the original model with. In this example since we have two simple equations we can replace those two equations with a single equation where we substitute the first equation into the second equation. Note that because we used a simple way of creating the one dimensional grid and using the grid in the second formula we can calculate what the inner product of two vectors of length 20 with all ones in them is, which is 20.
We then take this component and add it to an OpenMDAO Problem and run setup() to fully instantiate the problem.
Note that this step could easily be replaced with a process where we take the original aero group, instantiate it as an OpenMDAO problem, run a Design of Experiment against it, and build a surrogate model which we can then expose as an OpenMDAO component. This is not part of the contract, but Boeing has some internal tools that demonstrate this process.
class Surrogate(om.Group):
def setup(self):
self.add_subsystem('aero',
om.ExecComp("drag = ((q + z *l)*20 + r * k)/1000",
drag={'units': 'N'}),
promotes_inputs=['q', 'z', 'l', 'r', 'k'],
promotes_outputs=['drag'])
prob_replace = om.Problem(model=Surrogate())
prob_replace.setup()
<openmdao.core.problem.Problem at 0x7f7cb327ac90>
In the next step we get the Pydantic representation of the new surrogate using the get_interface method from the standard evaluator. We give it the same name as the component we are trying to replace.
surrogate_info = get_interface(prob_replace.model)
We can now replace the original group with the surrogate component / group in the Pydantic description.
This is the key step in this, we are replacing the interface description of the group in the overall interface description with the interface description of the simplified component.
We show the process in the below graphic.
info.components['aero'] = surrogate_info
Create a new OpenMDAO problem from the assembly information where the original aero group is replaced by a simplified group / component.
new_prob5 = create_problem(info)
** This is a group, special handling needed for sub1 in group
Adding z1 component
Building Equation class z1
Adding z2 component
Building Equation class z2
** This is a group, special handling needed for aero in group
Adding aero component
Building Equation class aero
We can now save and show the N2 diagram of the new OpenMDAO Problem.
om.n2(new_prob5, 'replace_after_surrogate.html', display_in_notebook=display_in_notebook)
Finally, we are setting values for all inputs to the two different assemblies, and run both of them. You will notice that the calculated drag is the same for the two calculations.
prob.set_val('a', 34.)
prob.set_val('sub1.my_alias', 5)
prob.set_val('sub1.z1.c', 24.)
prob.set_val('sub1.d', 3.)
prob.set_val('sub1.c', 12.)
prob.set_val('aero.l', 400.)
prob.set_val('aero.q', 5.)
prob.set_val('aero.r', 6.)
prob.set_val('aero.z', 7.)
prob.run_model()
_ = prob.model.list_outputs(shape=True, units=True)
5 Explicit Output(s) in 'model'
varname val units shape prom_name
---------- ------------------- ----- -------- ------------------
sub1
z1
y [66.] None (1,) sub1.z1.y
z2
v [71.] None (1,) sub1.v
aero
geometry
bla |20.0| None (20, 20) aero.geometry.bla
grid |12544.34135377| None (20,) aero.geometry.grid
cfd
drag [56.526] N (1,) aero.drag
0 Implicit Output(s) in 'model'
To transfer the state from the original problem to the new problem with the simplified component we use the get_state and set_state methods from the standard_evalautor library.
# We can get a Python dictionary with the current state of the problem using the `get_state` method
new_state = get_state(prob, info)
# Load the state from the HDF5 file using the load_state helper method. Note that we need to capture the info from the new
# instance so that variables and responses that were part of the old components are not seen anymore.
new_info = get_interface(new_prob5.model)
set_state(new_prob5, new_info, new_state)
Now when we run the problem with the simplified component we can see that we get the same value for drag.
new_prob5.run_model()
_ = new_prob5.model.list_inputs(shape=True, units=True)
_ = new_prob5.model.list_outputs(shape=True, units=True)
13 Input(s) in 'model'
varname val units shape prom_name
------- ------ ----- ----- -------------
sub1
z1
a [34.] None (1,) a
b [5.] None (1,) sub1.my_alias
c [24.] None (1,) sub1.z1.c
d [3.] None (1,) sub1.d
z2
a [1.] None (1,) sub1.z2.a
b [1.] None (1,) sub1.z2.b
c [66.] None (1,) sub1.c
d [3.] None (1,) sub1.d
aero
aero
k [71.] None (1,) aero.k
l [400.] None (1,) aero.l
q [5.] None (1,) aero.q
r [6.] None (1,) aero.r
z [7.] None (1,) aero.z
3 Explicit Output(s) in 'model'
varname val units shape prom_name
-------- -------- ----- ----- ---------
sub1
z1
y [66.] None (1,) sub1.z1.y
z2
v [71.] None (1,) sub1.v
aero
aero
drag [56.526] N (1,) aero.drag
0 Implicit Output(s) in 'model'