0
votes

OpenMDAO2 restrictions on group connections and input/output variables is throwing a wrench into my desire to write clean, modular software

from openmdao.api import Group, ExplicitComponent, IndepVarComp, Problem

class A(ExplicitComponent):
    def setup(self):
        self.add_input('x', val=0.0)
        self.add_input('y', val=0.0)
        self.add_output('Az', val=0.0)
    def compute(self, inputs, outputs):
        outputs['Az'] = inputs['x'] + inputs['y']

class B(ExplicitComponent):
    def setup(self):
        self.add_input('x', val=0.0)
        self.add_input('y', val=0.0)
        self.add_output('Bz', val=0.0)
    def compute(self, inputs, outputs):
        outputs['Bz'] = 2*inputs['x'] - inputs['y']

class AB(Group):
    def setup(self):
        self.add_subsystem('A', A(), promotes=['*'])
        self.add_subsystem('B', B(), promotes=['*'])

        indeps = IndepVarComp()
        indeps.add_output('x', 0.0)
        indeps.add_output('y', 0.0)
        self.add_subsystem('indeps', indeps, promotes=['*'])


class C(ExplicitComponent):
    def setup(self):
        self.add_input('x', val=0.0)
        self.add_input('y', val=0.0)
        self.add_output('Cz', val=0.0)
    def compute(self, inputs, outputs):
        outputs['Cz'] = 3*inputs['x'] - 2*inputs['y']

class D(ExplicitComponent):
    def setup(self):
        self.add_input('x', val=0.0)
        self.add_input('y', val=0.0)
        self.add_output('Dz', val=0.0)
    def compute(self, inputs, outputs):
        outputs['Dz'] = 4*inputs['x'] - 2.5*inputs['y']

class CD(Group):
    def setup(self):
        self.add_subsystem('C', C(), promotes=['*'])
        self.add_subsystem('D', D(), promotes=['*'])

        indeps = IndepVarComp()
        indeps.add_output('x', 0.0)
        indeps.add_output('y', 0.0)
        self.add_subsystem('indeps', indeps, promotes=['*'])

Sometimes I would like to work with Group AB only (run scenarios, optimize, etc.) and sometimes I would like to work with Group CD only. I can do that with,

prob = Problem()
prob.model = AB()
prob.setup()
prob['x'] = 10.0
prob['y'] = 20.0
prob.run_model()
print(prob['Az'],prob['Bz'])

However, sometimes I would like to work with Group ABCD:

class ABCD(Group):
    def setup(self):
        self.add_subsystem('AB', AB())
        self.add_subsystem('CD', CD())

        indeps = IndepVarComp()
        indeps.add_output('xx', 0.0)
        indeps.add_output('yy', 0.0)
        self.add_subsystem('indeps', indeps, promotes=['*'])
        self.connect('xx', ['AB.x', 'CD.x'])
        self.connect('yy', ['AB.y', 'CD.y'])

In that case, there is no combination of variable promotion, connections, or usage of IndepVarComps that doesn't give me an error of "inputs with multiple connections".

In OpenMDAO 1.x I was able to get around that by removing the IndepVarComps from the lower level groups (AB, CD) and only using them at the highest level group (see answer). However, OpenMDAO 2.x throws an error that two inputs are connected without an output. For example:

class AB(Group):
    def setup(self):
        self.add_subsystem('A', A(), promotes=['*'])
        self.add_subsystem('B', B(), promotes=['*'])

Now I am stuck with maintaining multiple copies of the same code, one copy for AB and another for ABCD, or just manually connecting my modules in pure python and moving away from OpenMDAO. Am I missing something? Any help or guidance would be welcome.

1

1 Answers

3
votes

In your simple example, I can see two ways of solving the problem. Both involve the use of options on the groups. Since im not sure which way will work better, I included both in my sample below.

One way is that you make it optional if AB/CD own their own indeps. Then you can toggle the needed behavior as you see fit. This works, but I personally think its messy.

The second option is to make just a single group, but use the options to control what is created by the mode argument. I think this way is cleaner, since you just have the 4 components and the one group.

from openmdao.api import Group, ExplicitComponent, IndepVarComp

class A(ExplicitComponent):
    def setup(self):
        self.add_input('x', val=0.0)
        self.add_input('y', val=0.0)
        self.add_output('Az', val=0.0)
    def compute(self, inputs, outputs):
        outputs['Az'] = inputs['x'] + inputs['y']

class B(ExplicitComponent):
    def setup(self):
        self.add_input('x', val=0.0)
        self.add_input('y', val=0.0)
        self.add_output('Bz', val=0.0)
    def compute(self, inputs, outputs):
        outputs['Bz'] = 2*inputs['x'] - inputs['y']

class AB(Group):

    def initialize(self): 
        self.options.declare('owns_indeps', types=bool, default=True)

    def setup(self):

        if self.options['owns_indeps']: 
            indeps = IndepVarComp()
            indeps.add_output('x', 0.0)
            indeps.add_output('y', 0.0)
            self.add_subsystem('indeps', indeps, promotes=['*'])

        self.add_subsystem('A', A(), promotes=['*'])
        self.add_subsystem('B', B(), promotes=['*'])


class C(ExplicitComponent):
    def setup(self):
        self.add_input('x', val=0.0)
        self.add_input('y', val=0.0)
        self.add_output('Cz', val=0.0)
    def compute(self, inputs, outputs):
        outputs['Cz'] = 3*inputs['x'] - 2*inputs['y']

class D(ExplicitComponent):
    def setup(self):
        self.add_input('x', val=0.0)
        self.add_input('y', val=0.0)
        self.add_output('Dz', val=0.0)
    def compute(self, inputs, outputs):
        outputs['Dz'] = 4*inputs['x'] - 2.5*inputs['y']

class CD(Group):

    def initialize(self): 
        self.options.declare('owns_indeps', types=bool, default=True)

    def setup(self):
        if self.options['owns_indeps']: 
            indeps = IndepVarComp()
            indeps.add_output('x', 0.0)
            indeps.add_output('y', 0.0)
            self.add_subsystem('indeps', indeps, promotes=['*'])

        self.add_subsystem('C', C(), promotes=['*'])
        self.add_subsystem('D', D(), promotes=['*'])


class ABCD(Group):
    def setup(self):
        self.add_subsystem('AB', AB(owns_indeps=False))
        self.add_subsystem('CD', CD(owns_indeps=False))

        indeps = IndepVarComp()
        indeps.add_output('xx', 0.0)
        indeps.add_output('yy', 0.0)
        self.add_subsystem('indeps', indeps, promotes=['*'])
        self.connect('xx', ['AB.x', 'CD.x'])
        self.connect('yy', ['AB.y', 'CD.y'])


class ABCD_ALT(Group): 
    """Alternate approach that would not require more than one group class at all""" 

    def initialize(self): 
        self.options.declare('mode', values=['AB', 'CD', 'ABCD'], default='AB')

    def setup(self): 
        mode = self.options['mode']

        indeps = IndepVarComp()
        indeps.add_output('xx', 0.0)
        indeps.add_output('yy', 0.0)
        self.add_subsystem('indeps', indeps, promotes=['*'])


        if 'AB' in mode: 
            self.add_subsystem('A', A(), promotes=['*'])
            self.add_subsystem('B', B(), promotes=['*'])

        if 'CD' in mode: 
            self.add_subsystem('C', C(), promotes=['*'])
            self.add_subsystem('D', D(), promotes=['*'])
        self.connect('xx', 'x')
        self.connect('yy', 'y')


if __name__ == "__main__": 

    from openmdao.api import Problem

    p = Problem()

    # p.model = AB()
    # p.model = CD()
    p.model = ABCD()


    # p.model = ABCD_ALT(mode='AB')

    p.setup()