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 but its setup method is not called until after the parent group’s setup method 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 configure that runs after the setup on all subsystems has completed. While setup recurses 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.

Configuring Solvers#

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))
True
True

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 method.

System.get_io_metadata(iotypes=('input', 'output'), metadata_keys=None, includes=None, excludes=None, is_indep_var=None, is_design_var=None, tags=(), get_remote=False, rank=None, return_rel_names=True)[source]

Retrieve metadata for a filtered list of variables.

Parameters:
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.

is_indep_varbool or None

If None (the default), do no additional filtering of the inputs. If True, list only inputs connected to an output tagged openmdao:indep_var. If False, list only inputs _not_ connected to outputs tagged openmdao:indep_var.

is_design_varbool or None

If None (the default), do no additional filtering of the inputs. If True, list only inputs connected to outputs that are driver design variables. If False, list only inputs _not_ connected to outputs that are driver design variables.

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.

get_remotebool

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.

return_rel_namesbool

If True, the names returned will be relative to the scope of this System. Otherwise they will be absolute names.

Returns:
dict

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 get_io_metadata.

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[0], length=data_shape[1],
                                         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.

Uses of setup vs. configure#

To understand when to use setup and when to use configure, see the Theory Manual entry on how the setup stack works.