Modifying Children of a Group with Configure Method¶
Most of the time, the
setup method is the only one you need to define on a group.
The main exception is the case where you want to modify a solver that was set in one of
your children groups. When you call
add_subsystem, the system you add is instantiated
setup method is not called until after the parent group’s
is finished with its execution. That means that anything you do with that subsystem
(e.g., changing the nonlinear solver) will potentially be overwritten by the child system’s
setup if it is assigned there as well.
To get around this timing problem, there is a second setup method called
that runs after the
setup on all subsystems has completed. While
from the top down,
configure recurses from the bottom up, so that the highest
system in the hierarchy takes precedence over all lower ones for any modifications.
Here is a simple example where a lower system sets a solver, but we want to change it to a different one in the top-most system.
import openmdao.api as om class ImplSimple(om.ImplicitComponent): def setup(self): self.add_input('a', val=1.) self.add_output('x', val=0.) def apply_nonlinear(self, inputs, outputs, residuals): residuals['x'] = np.exp(outputs['x']) - \ inputs['a']**2 * outputs['x']**2 def linearize(self, inputs, outputs, jacobian): jacobian['x', 'x'] = np.exp(outputs['x']) - \ 2 * inputs['a']**2 * outputs['x'] jacobian['x', 'a'] = -2 * inputs['a'] * outputs['x']**2 class Sub(om.Group): def setup(self): self.add_subsystem('comp', ImplSimple()) def configure(self): # This solver won't solve the system. We want # to override it in the parent. self.nonlinear_solver = om.NonlinearBlockGS() class Super(om.Group): def setup(self): self.add_subsystem('sub', Sub()) def configure(self): # This will solve it. self.sub.nonlinear_solver = om.NewtonSolver(solve_subsystems=False) self.sub.linear_solver = om.ScipyKrylov() top = om.Problem(model=Super()) top.setup() print(isinstance(top.model.sub.nonlinear_solver, om.NewtonSolver)) print(isinstance(top.model.sub.linear_solver, om.ScipyKrylov))
Configuring Setup-Dependent I/O¶
Another situation in which the
configure method might be useful is if the inputs
and outputs of a component or subsystem are dependent on the
setup of another system.
Collecting variable metadata information during configure can be done via the
get_io_metadata(iotypes=('input', 'output'), metadata_keys=None, includes=None, excludes=None, tags=(), get_remote=False, rank=None, return_rel_names=True)
Retrieve metdata for a filtered list of variables.
- iotypesstr or iter of str
Will contain either ‘input’, ‘output’, or both. Defaults to both.
- metadata_keysiter of str or None
Names of metadata entries to be retrieved or None, meaning retrieve all available ‘allprocs’ metadata. If ‘val’ or ‘src_indices’ are required, their keys must be provided explicitly since they are not found in the ‘allprocs’ metadata and must be retrieved from local metadata located in each process.
- includesstr, iter of str or None
Collection of glob patterns for pathnames of variables to include. Default is None, which includes all variables.
- excludesstr, iter of str or None
Collection of glob patterns for pathnames of variables to exclude. Default is None.
- tagsstr or iter of strs
User defined tags that can be used to filter what gets listed. Only inputs with the given tags will be listed. Default is None, which means there will be no filtering based on tags.
If True, retrieve variables from other MPI processes as well.
- rankint or None
If None, and get_remote is True, retrieve values from all MPI process to all other MPI processes. Otherwise, if get_remote is True, retrieve values from all MPI processes only to the specified rank.
If True, the names returned will be relative to the scope of this System. Otherwise they will be absolute names.
A dict of metadata keyed on name, where name is either absolute or relative based on the value of the return_rel_names arg, and metadata is a dict containing entries based on the value of the metadata_keys arg. Every metadata dict will always contain two entries, ‘promoted_name’ and ‘discrete’, to indicate a given variable’s promoted name and whether or not it is discrete.
The following example is a variation on the model used to illustrate use of an
AddSubtractComp. Here we assume the component that
provides the vectorized data must be
setup before the shape of that data is known.
The shape information is collected using
class FlightDataComp(om.ExplicitComponent): """ Simulate data generated by an external source/code """ def setup(self): # number of points may not be known a priori n = 3 # The vector represents forces at n time points (rows) in 2 dimensional plane (cols) self.add_output(name='thrust', shape=(n, 2), units='kN') self.add_output(name='drag', shape=(n, 2), units='kN') self.add_output(name='lift', shape=(n, 2), units='kN') self.add_output(name='weight', shape=(n, 2), units='kN') def compute(self, inputs, outputs): outputs['thrust'][:, 0] = [500, 600, 700] outputs['drag'][:, 0] = [400, 400, 400] outputs['weight'][:, 1] = [1000, 1001, 1002] outputs['lift'][:, 1] = [1000, 1000, 1000] class ForceModel(om.Group): def setup(self): self.add_subsystem('flightdatacomp', FlightDataComp(), promotes_outputs=['thrust', 'drag', 'lift', 'weight']) self.add_subsystem('totalforcecomp', om.AddSubtractComp()) def configure(self): # Some models that require self-interrogation need to be able to add # I/O in components from the configure method of their containing groups. # In this case, we can only determine the 'vec_size' for totalforcecomp # after flightdatacomp has been setup. meta = self.flightdatacomp.get_io_metadata('output', includes='thrust') data_shape = meta['thrust']['shape'] self.totalforcecomp.add_equation('total_force', input_names=['thrust', 'drag', 'lift', 'weight'], vec_size=data_shape, length=data_shape, scaling_factors=[1, -1, 1, -1], units='kN') self.connect('thrust', 'totalforcecomp.thrust') self.connect('drag', 'totalforcecomp.drag') self.connect('lift', 'totalforcecomp.lift') self.connect('weight', 'totalforcecomp.weight') p = om.Problem(model=ForceModel()) p.setup() p.run_model() print(p.get_val('totalforcecomp.total_force', units='kN'))
[[100. 0.] [200. -1.] [300. -2.]]
Variable information may also be collected using list_inputs and list_outputs which provide a somewhat simpler interface with a little less flexibility and a little more overhead. Also, list_inputs and list_outputs return their data as a list of (name, metadata) tuples rather than as a dictionary.