EQConstraintComp

EQConstraintComp is a specialized component that provides a simple way to implement equality constraints.

You can add one or more outputs to an EQConstraintComp that compute the difference between a pair of input values for the purposes of driving the two inputs to equality. It computes the output value as:

\[ name_{output} = \frac{ name_{mult} \times name_{lhs} - name_{rhs} }{f_{norm}(name_{rhs})} \]

The normalization function \(f_{norm}(name_{rhs})\) takes one of the following forms:

\[\begin{split} f_{norm}(name_{rhs}) = \begin{cases} \left| name_{rhs} \right|, & \text{if normalize and } \left| name_{rhs} \right| \geq 2 \\ 0.25 name_{rhs}^2 + 1, & \text{if normalize and } \left| name_{rhs} \right| < 2 \\ 1, & \text{if not normalize} \end{cases} \end{split}\]

The following inputs and outputs are associated with each output variable.

Name

I/O

Description

{name}

output

output variable

lhs:{name}

input

left-hand side of difference equation

rhs:{name}

input

right-hand side of difference equation

mult:{name}

input

left-hand side multiplier of difference equation

The default value for the rhs:{name} input can be set to via the rhs_val argument (see arguments below). If the rhs value is fixed (e.g. 0), then the input can be left unconnected. The lhs:{name} input must always have something connected to it.

The multiplier is optional and will default to 1.0 if not connected.

EQConstraintComp supports vectorized outputs. Simply provide a default value or shape when adding the output that reflects the correct shape.

You can provide the arguments to create an output variable when instantiating an EQConstraintComp or you can use the add_eq_output method to create one or more outputs after instantiation. The constructor accepts all the same arguments as the add_eq_output method:

EQConstraintComp.add_eq_output(name, eq_units=None, lhs_name=None, rhs_name=None, rhs_val=0.0, use_mult=False, mult_name=None, mult_val=1.0, normalize=True, add_constraint=False, ref=None, ref0=None, adder=None, scaler=None, **kwargs)[source]

Add a new output variable computed via the difference equation.

This will create new inputs lhs:name, rhs:name, and mult:name that will define the left and right sides of the difference equation, and a multiplier for the left-hand-side.

Parameters
namestr

The name of the output variable to be created.

eq_unitsstr or None

Units for the left-hand-side and right-hand-side of the difference equation.

lhs_namestr or None

Optional name for the LHS variable associated with the difference equation. If None, the default will be used: ‘lhs:{name}’.

rhs_namestr or None

Optional name for the RHS variable associated with the difference equation. If None, the default will be used: ‘rhs:{name}’.

rhs_valint, float, or np.array

Default value for the RHS. Must be compatible with the shape (optionally) given by the val or shape option in kwargs.

use_multbool

Specifies whether the LHS multiplier is to be used. If True, then an additional input mult_name is created, with the default value given by mult_val, that multiplies lhs. Default is False.

mult_namestr or None

Optional name for the LHS multiplier variable associated with the output variable. If None, the default will be used: ‘mult:{name}’.

mult_valint, float, or np.array

Default value for the LHS multiplier. Must be compatible with the shape (optionally) given by the val or shape option in kwargs.

normalizebool

Specifies whether or not the resulting output should be normalized by a quadratic function of the RHS. When this option is True, the user-provided ref/ref0 scaler/adder options below are typically unnecessary.

add_constraintbool

Specifies whether to add an equality constraint.

reffloat or ndarray, optional

Value of response variable that scales to 1.0 in the driver. This option is only meaningful when add_constraint=True.

ref0float or ndarray, optional

Value of response variable that scales to 0.0 in the driver. This option is only meaningful when add_constraint=True.

adderfloat or ndarray, optional

Value to add to the model value to get the scaled value for the driver. adder is first in precedence. This option is only meaningful when add_constraint=True.

scalerfloat or ndarray, optional

Value to multiply the model value to get the scaled value for the driver. scaler is second in precedence. This option is only meaningful when add_constraint=True.

**kwargsdict

Additional arguments to be passed for the creation of the output variable. (see add_output method).

Note that the kwargs arguments can include any of the keyword arguments normally available when creating an output variable with the add_output method of a Component.

Example: Sellar IDF

The following example shows an Individual Design Feasible (IDF) architecture for the Sellar problem that demonstrates the use of an EQConstraintComp.

In IDF, the direct coupling between the disciplines is removed and the coupling variables are added to the optimizer’s design variables. The algorithm calls for two new equality constraints that enforce the coupling between the disciplines. This ensures that the final optimized solution is feasible, though it is achieved through the optimizer instead of using a solver. The two new equality constraints are implemented in this example with an EQConstraintComp.

import openmdao.api as om


class SellarIDF(om.Group):
    """
    Individual Design Feasible (IDF) architecture for the Sellar problem.
    """
    def setup(self):
        # construct the Sellar model with `y1` and `y2` as independent variables

        self.set_input_defaults('x', 5.)
        self.set_input_defaults('y1', 5.)
        self.set_input_defaults('y2', 5.)
        self.set_input_defaults('z', np.array([2., 0.]))

        self.add_subsystem('d1', SellarDis1withDerivatives(), promotes_inputs=['x', 'z', 'y2'])
        self.add_subsystem('d2', SellarDis2withDerivatives(), promotes_inputs=['y1', 'z'])

        self.add_subsystem('obj_cmp', om.ExecComp('obj = x**2 + z[1] + y1 + exp(-y2)',
                           x=0., z=np.array([0., 0.])), promotes_inputs=['x', 'z', 'y1', 'y2'])

        self.add_subsystem('con_cmp1', om.ExecComp('con1 = 3.16 - y1'), promotes_inputs=['y1'])
        self.add_subsystem('con_cmp2', om.ExecComp('con2 = y2 - 24.0'), promotes_inputs=['y2'])

        # rather than create a cycle by connecting d1.y1 to d2.y1 and d2.y2 to d1.y2
        # we will constrain y1 and y2 to be equal for the two disciplines

        equal = om.EQConstraintComp()
        self.add_subsystem('equal', equal, promotes_inputs=[('lhs:y1', 'y1'), ('lhs:y2', 'y2')])

        equal.add_eq_output('y1', add_constraint=True)
        equal.add_eq_output('y2', add_constraint=True)

        self.connect('d1.y1', 'equal.rhs:y1')
        self.connect('d2.y2', 'equal.rhs:y2')

        # the driver will effectively solve the cycle
        # by satisfying the equality constraints

        self.add_design_var('x', lower=0., upper=5.)
        self.add_design_var('y1', lower=0., upper=5.)
        self.add_design_var('y2', lower=0., upper=5.)
        self.add_design_var('z', lower=np.array([-5., 0.]), upper=np.array([5., 5.]))
        self.add_objective('obj_cmp.obj')
        self.add_constraint('con_cmp1.con1', upper=0.)
        self.add_constraint('con_cmp2.con2', upper=0.)
from openmdao.test_suite.components.sellar_feature import SellarIDF

prob = om.Problem(model=SellarIDF())
prob.driver = om.ScipyOptimizeDriver(optimizer='SLSQP', disp=True)
prob.setup()
prob.run_driver();
Optimization terminated successfully.    (Exit mode 0)
            Current function value: 3.1833939516939864
            Iterations: 6
            Function evaluations: 6
            Gradient evaluations: 6
Optimization Complete
-----------------------------------
print(prob.get_val('x'))
[0.]
print([prob.get_val('y1'), prob.get_val('d1.y1')])
print([prob.get_val('y2'), prob.get_val('y2')])
[array([3.16]), array([3.15999999])]
[array([3.75527776]), array([3.75527776])]
print(prob.get_val('z'))
[1.97763888 0.        ]
print(prob.get_val('obj_cmp.obj'))
[3.18339395]