Search code examples
openmdao

Define variable in problem which is not used as input, and equals a previous input/output


I would like to define a variable which, depending on certain options, will be equal to a previous output (as if the previous output had two names) or will be the output of a new component.

A trivial solution is to just omit the definition of the value when the component which would define it is not implemented, but I would prefer it to be defined for readability/traceability reasons (to simplify if statements in the code, and to provide it as timeseries output).

The problem is that when using the connect statement, if the subsequent condition does not lead to the variable being used as an input to another component, it provides an error mentioning that it attempted to connect but the variable does not exist.

I made a temporal fix with a sort of link statement (LinkVarComp bellow) which creates an explicit component with the output being equal to the input (and some additional things as scaling and a shift which could be useful for linear equations), but i am worried that this would add unnecessary computations/design variables/constraints.

Is there an easier/better workaround? (maybe by allowing variables to have multiple names?) what could be the best practice to just have a variable with a different name equal to a previous output/input?

A simple example:

import openmdao.api as om
model = om.Group()
    model.add_subsystem('xcomp',subsys=om.IndepVarComp(name='x',val=np.zeros((3,2))),promotes_outputs=['*'])
model.connect('x','y')
p = om.Problem(model)
p.setup(force_alloc_complex=True)

p.set_val('x', np.array([[1.0 ,3],[10 ,-5],[0,3.1]])) 
p.run_model()

Crashes with error

NameError: <model> <class Group>: Attempted to connect from 'x' to 'y', but 'y' doesn't exist.

While this works if using the following LinkVarComp component (but i suppose that adding new variables and computations)

import openmdao.api as om 
import numpy as np
from math import prod

class LinkVarComp(om.ExplicitComponent):
    """
    Component containing
    """
    def initialize(self):
        """
        Declare component options.
        """
        self.options.declare('shape', types=(int,tuple),default=1)
        self.options.declare('scale', types=int,default=1)
        self.options.declare('shift', types=float,default=0.)
        self.options.declare('input_default', types=float,default=0.)
        self.options.declare('input_name', types=str,default='x')
        self.options.declare('output_name', types=str,default='y')
        self.options.declare('output_default', types=float,default=0.)
        self.options.declare('input_units', types=(str,None),default=None)
        self.options.declare('output_units', types=(str,None),default=None)

    def setup(self):
        self.add_input(name=self.options['input_name'],val=self.options['input_default'],shape=self.options['shape'],units=self.options['input_units'])
        self.add_output(name=self.options['output_name'],val=self.options['output_default'],shape=self.options['shape'],units=self.options['output_units'])
        if type(self.options['shape']) == int:
            n = self.options['shape']
        else:
            n =prod( self.options['shape'])
        ar = np.arange(n)
        self.declare_partials(of=self.options['output_name'] , wrt=self.options['input_name'], rows=ar, cols=ar,val=self.options['scale'])

    def compute(self, inputs, outputs):
        outputs[self.options['output_name']] = self.options['scale']*inputs[self.options['input_name']] + self.options['shift']

model = om.Group()
model.add_subsystem('xcomp',subsys=om.IndepVarComp(name='x',val=np.zeros((3,2))),promotes_outputs=['*'])
model.add_subsystem('link', LinkVarComp(shape=(3,2)),
                        promotes_inputs=['*'],
                        promotes_outputs=['*'])

p = om.Problem(model)
p.setup(force_alloc_complex=True)

p.set_val('x', np.array([[1.0 ,3],[10 ,-5],[0,3.1]]))
p.run_model()
print(p['y'])

Outputing the expected:

[[ 1.   3. ]
 [10.  -5. ]
 [ 0.   3.1]]

Solution

  • In OpenMDAO, you can not have the same variable take on two separate names. Thats simply not allowed.

    The solution you came up with is effectively creating a separate component to hold a copy of the output. That works. You could use an ExecComp to have the same effect with a little less code:

    import numpy as np
    import openmdao.api as om
    
    model = om.Group()
    model.add_subsystem('xcomp',subsys=om.IndepVarComp(name='x',val=np.zeros((3,2))),promotes_outputs=['*'])
    model.add_subsystem('ycomp', om.ExecComp("y=x", shape=(3,2)), promotes=['*'])
    
    
    p = om.Problem(model)
    p.setup(force_alloc_complex=True)
    
    p.set_val('x', np.array([[1.0 ,3],[10 ,-5],[0,3.1]])) 
    p.run_model()
    
    print(p['x'])
    print(p['y'])
    

    In general, I probably wouldn't actually do this myself. It seems kind of wasteful. Instead, I would modify my post-processing script to look for y and if it didn't find it to then grab x instead.