OpenMDAO2 restricting modular software architecture - openmdao

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.

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

Related

openmdao compute gradient of constraints but not objective

How can I compute the gradient of my constraints without computing the gradient of my objective? I've tried this,
self._problem().compute_totals(of=self.consList, wrt=self._dvlist,
return_format='array')
but this still calls the objective function gradients. (self.consList is a list with one element, which is the name of my constraint component).
Here is an example of this issue:
import openmdao.api as om
prob = om.Problem()
model = prob.model
class comp1(om.ExplicitComponent):
def setup(self):
self.add_input('x', 2, units="degF")
self.add_output('y1', 2, units="degF")
def setup_partials(self):
self.declare_partials('y1', ['x'])
def compute(self, inputs, outputs):
outputs['y1'] = 2.0 * inputs['x']
def compute_partials(self, inputs, J):
print('I need these partials')
J['y1', 'x'] = 2.0
class comp2(om.ExplicitComponent):
def setup(self):
self.add_input('x', 2, units="degF")
self.add_output('y2', 2, units="degF")
def setup_partials(self):
self.declare_partials('y2', ['x'])
def compute(self, inputs, outputs):
outputs['y2'] = 3.0 * inputs['x']
def compute_partials(self, inputs, J):
print('I dont need these partials')
J['y2', 'x'] = 3.0
model.add_subsystem('comp1', comp1(), promotes=['*'])
model.add_subsystem('comp2', comp2(), promotes=['*'])
model.set_input_defaults('x', 35.0, units='degF')
model.add_design_var('x', units='degC', lower=0.0, upper=100.0)
model.add_constraint('y1', units='degC', lower=0.0, upper=100.0)
model.add_objective('y2', units='degC')
prob.setup()
prob.run_model()
print('computing constraint partials')
res = prob.driver._compute_totals(of='y1', wrt='x')
You could add a custom attribute to stop your model from computing the gradient of the second component:
import openmdao.api as om
prob = om.Problem()
model = prob.model
class comp1(om.ExplicitComponent):
def setup(self):
self.add_input('x', 2, units="degF")
self.add_output('y1', 2, units="degF")
def setup_partials(self):
self.declare_partials('y1', ['x'])
def compute(self, inputs, outputs):
outputs['y1'] = 2.0 * inputs['x']
def compute_partials(self, inputs, J):
print('I need these partials')
J['y1', 'x'] = 2.0
class comp2(om.ExplicitComponent):
def setup(self):
self.add_input('x', 2, units="degF")
self.add_output('y2', 2, units="degF")
self.skip_partial = False
def setup_partials(self):
self.declare_partials('y2', ['x'])
def compute(self, inputs, outputs):
outputs['y2'] = 3.0 * inputs['x']
def compute_partials(self, inputs, J):
if self.skip_partial: return
print('I dont need these partials')
J['y2', 'x'] = 3.0
model.add_subsystem('comp1', comp1(), promotes=['*'])
model.add_subsystem('comp2', comp2(), promotes=['*'])
model.set_input_defaults('x', 35.0, units='degF')
model.add_design_var('x', units='degC', lower=0.0, upper=100.0)
model.add_constraint('y1', units='degC', lower=0.0, upper=100.0)
model.add_objective('y2', units='degC')
prob.setup()
prob.run_model()
prob.model._subsystems_myproc[2].skip_partial = True
print('computing constraint partials')
res = prob.driver._compute_totals(of='y1', wrt='x')

Openmdao - compute finite difference in parallel when optimizing

I have made a simple example (see below) which does the fd in serial. What is the best way to do this in parallel? I am using Python 3.6 and OpenMDAO 2.4.0.
import numpy as np
from openmdao.api import Problem, ScipyOptimizeDriver, ExecComp, IndepVarComp, ExplicitComponent, Group
class WorkFlow(ExplicitComponent):
def setup(self):
self.add_input('x', np.ones(5))
self.add_output('y', 2.0)
self.declare_partials('y', 'x', method='fd')
def compute(self, inputs, outputs):
print('comm:', self.comm.rank, inputs['x'])
print()
outputs['y'] = abs(np.sum(inputs['x']**2) - 9)
prob = Problem()
indeps = prob.model.add_subsystem('indeps', IndepVarComp(), promotes=['*'])
indeps.add_output('x', np.ones(5))
prob.model.add_subsystem('wf', WorkFlow(), promotes_inputs=['x'])
prob.driver = ScipyOptimizeDriver()
prob.driver.options['optimizer'] = 'SLSQP'
prob.driver.options['tol'] = 1e-9
prob.model.add_design_var('x', lower=-10.0, upper=10.0)
prob.model.add_objective('wf.y')
prob.setup()
prob.run_driver()
print(prob['x'])
print(prob['wf.y'])
As of OpenMDAO v2.4 you could not do parallel finite-difference. However, the feature has been recently added to the master branch of OpenMDAO and will be officially released in OpenMDAO V2.5 soon.
To use the feature right now, install the repository version of openMDAO (You can not do pip install openmdao. instead clone the repository from github and then pip install -e <location of the cloned repo>).
Then you can follow the instructions on the documentation page for parallel FD.
Here is your actual code though. The only change to the component is that when you instantiate it you provide the num_par_fd=5 argument to the component. Then when you call the file, you should run it under mpi like this:
mpiexec -n 5 python test.py
Here is what test.py should look like:
import numpy as np
from openmdao.api import Problem, ScipyOptimizeDriver, ExecComp, IndepVarComp, ExplicitComponent, Group
class WorkFlow(ExplicitComponent):
def setup(self):
self.add_input('x', np.ones(5))
self.add_output('y', 2.0)
self.declare_partials('y', 'x', method='fd')
def compute(self, inputs, outputs):
print('comm:', self.comm.rank, inputs['x'])
print()
outputs['y'] = abs(np.sum(inputs['x']**2) - 9)
prob = Problem()
indeps = prob.model.add_subsystem('indeps', IndepVarComp(), promotes=['*'])
indeps.add_output('x', np.ones(5))
prob.model.add_subsystem('wf', WorkFlow(num_par_fd=5), promotes_inputs=['x'])
prob.driver = ScipyOptimizeDriver()
prob.driver.options['optimizer'] = 'SLSQP'
prob.driver.options['tol'] = 1e-9
prob.model.add_design_var('x', lower=-10.0, upper=10.0)
prob.model.add_objective('wf.y')
prob.setup()
prob.run_driver()
print(prob['x'])
print(prob['wf.y'])

Passing gradients between components; pass_by_obj output

I have a situation where the gradient of one component is by necessity calculated in another component. What I have attempted to do is just have the gradient be an output from the first component and an input to the second component. I have set it to be pass_by_obj so that it doesn't affect other calculations. Any recommendations on whether or not this would be the best way to do it would be appreciated. Nevertheless, I am getting an error when using check_partial_derivatives(). It seems to be an error for any output that is specified as pass_by_obj. Here is a simple case:
import numpy as np
from openmdao.api import Group, Problem, Component, ScipyGMRES, ExecComp, IndepVarComp
class Comp1(Component):
def __init__(self):
super(Comp1, self).__init__()
self.add_param('x', shape=1)
self.add_output('y', shape=1)
self.add_output('dz_dy', shape=1, pass_by_obj=True)
def solve_nonlinear(self, params, unknowns, resids):
x = params['x']
unknowns['y'] = 4.0*x + 1.0
unknowns['dz_dy'] = 2.0*x
def linearize(self, params, unknowns, resids):
J = {}
J['y', 'x'] = 4.0
return J
class Comp2(Component):
def __init__(self):
super(Comp2, self).__init__()
self.add_param('y', shape=1)
self.add_param('dz_dy', shape=1, pass_by_obj=True)
self.add_output('z', shape=1)
def solve_nonlinear(self, params, unknowns, resids):
y = params['y']
unknowns['z'] = y*2.0
def linearize(self, params, unknowns, resids):
J = {}
J['z', 'y'] = params['dz_dy']
return J
class TestGroup(Group):
def __init__(self):
super(TestGroup, self).__init__()
self.add('x', IndepVarComp('x', 0.0), promotes=['*'])
self.add('c1', Comp1(), promotes=['*'])
self.add('c2', Comp2(), promotes=['*'])
p = Problem()
p.root = TestGroup()
p.setup(check=False)
p['x'] = 2.0
p.run()
print p['z']
print 'gradients'
test_grad = open('partial_gradients_test.txt', 'w')
partial = p.check_partial_derivatives(out_stream=test_grad)
I get the following error message:
partial = p.check_partial_derivatives(out_stream=test_grad)
File "/usr/local/lib/python2.7/site-packages/openmdao/core/problem.py", line 1699, in check_partial_derivatives
dresids._dat[u_name].val[idx] = 1.0
TypeError: '_ByObjWrapper' object does not support item assignment
I asked before about the params being checked for pass_by_obj in check_partial_derivatives() and it might be simply a matter of checking the unknowns for pass_by_obj as well.
the error you're getting is another bug related to check_partial_derivatives function. It should be easy enough to fix, but in the meantime you can just remove the pass_by_obj setting. Since you're computing a value in one component and passing it to another, there isn't a need to do pass_by_obj at all (and it will be more efficient if you don't).
You said that you did it so that it "doesn't affect other calculations", but I don't quite know what you mean by that. It won't affect anything unless you use it in the solve_nonlinear method.

Check Partial Derivatives with pass_by_obj

I have a component that has an input that is an int so I am setting pass_by_obj = True. However, when I check derivatives with check_partial_derivatives(), it throws this error:
data = prob.check_partial_derivatives(out_stream=sys.stdout)
File "/usr/local/lib/python2.7/site-packages/openmdao/core/problem.py", line 1711, in check_partial_derivatives
jac_rev[(u_name, p_name)][idx, :] = dinputs._dat[p_name].val
TypeError: float() argument must be a string or a number
It appears to be trying to take the derivative even though it cannot. Here is a simple example:
import sys
from openmdao.api import IndepVarComp, Problem, Group, Component
class Comp(Component):
def __init__(self):
super(Comp, self).__init__()
self.add_param('x', val=0.0)
self.add_param('y', val=3, pass_by_obj=True)
self.add_output('z', val=0.0)
def solve_nonlinear(self, params, unknowns, resids):
unknowns['z'] = params['y']*params['x']
def linearize(self, params, unknowns, resids):
J = {}
J['z', 'x'] = params['y']
return J
prob = Problem()
prob.root = Group()
prob.root.add('comp', Comp(), promotes=['*'])
prob.root.add('p1', IndepVarComp('x', 0.0), promotes=['x'])
prob.root.add('p2', IndepVarComp('y', 3, pass_by_obj=True), promotes=['y'])
prob.setup(check=False)
prob['x'] = 2.0
prob['y'] = 3
prob.run()
print prob['z']
data = prob.check_partial_derivatives(out_stream=sys.stdout)
It is possible to use the check_partial_derivatives() method with components that have inputs that are specified as pass_by_obj? I don't care about the derivatives for the inputs that are specified as pass_by_obj, but I care about the other inputs.
Thanks for the report and test. This was a bug where we weren't excluding the design variables that were declared pass_by_obj. I've got a pull request up on the OpenMDAO repo with a fix. It'll probably be merged to master within a day.
EDIT -- The fix is merged. https://github.com/OpenMDAO/OpenMDAO/commit/b123b284e46aac7e15fa9bce3751f9ad9bb63b95

Connecting an element of array to another Component's input

I am trying to connect the last element of an output of one component to the input of another component. An example is shown below:
import numpy as np
from openmdao.api import Component, Problem, Group
class C1(Component):
def __init__(self):
super(C1, self).__init__()
self.add_param('fin', val=1.0)
self.add_output('arr', val=np.zeros(5))
def solve_nonlinear(self, params, unknowns, resids):
fin = params['fin']
unknowns['arr'] = np.array([2*fin])
class C2(Component):
def __init__(self):
super(C2, self).__init__()
self.add_param('flt', val=0.0)
self.add_output('fout', val=0.0)
def solve_nonlinear(self, params, unknowns, resids):
flt = params['flt']
unknowns['fout'] = 2*flt
class A(Group):
def __init__(self):
super(A, self).__init__()
self.add('c1', C1())
self.add('c2', C2())
self.connect('c1.arr[-1]', 'c2.flt')
if __name__ == '__main__':
a = Problem()
a.root = A()
a.setup()
a.run()
print a.root.c2.unknowns['fout']
I am given the error:
openmdao.core.checks.ConnectError: Source 'c1.arr[-1]' cannot be connected to target 'c2.flt': 'c1.arr[-1]' does not exist.
Is there a way to do this? I know it worked in the old version of OpenMDAO.
OpenMDAO supports connection to specific indices of a source by using the 'src_indices' arg. For example:
self.connect('c1.arr', 'c2.flt', src_indices=[4])
Negative indices are not currently supported.
There are a number of small issues here. First the solve_nonlinear method of C1 has the wrong size for its array computation. It ends up working out, but you should really set the array to the right size (length 5).
To part of an array (see docs and more advanced docs) , you specify src_indices argument to connect.
import numpy as np
from openmdao.api import Component, Problem, Group
class C1(Component):
def __init__(self):
super(C1, self).__init__()
self.add_param('fin', val=1.0)
self.add_output('arr', val=np.zeros(5))
def solve_nonlinear(self, params, unknowns, resids):
fin = params['fin']
unknowns['arr'] = fin*np.arange(5)
class C2(Component):
def __init__(self):
super(C2, self).__init__()
self.add_param('flt', val=0.0)
self.add_output('fout', val=0.0)
def solve_nonlinear(self, params, unknowns, resids):
flt = params['flt']
unknowns['fout'] = 2*flt
class A(Group):
def __init__(self):
super(A, self).__init__()
self.add('c1', C1())
self.add('c2', C2())
self.connect('c1.arr', 'c2.flt', src_indices=[4,])
if __name__ == '__main__':
a = Problem()
a.root = A()
a.setup()
a.run()
print a.root.c2.unknowns['fout']

Resources