# Multidisciplinary Design Feasible (MDF)¶

In a Multidisciplinary Design Feasible (MDF) problem, the disciplines are directly coupled via some kind of solver, and the design variables are optimized all at the top level. The following spaghetti diagram illustrates MDF applied to the Sellar problem. Data Flow for MDF Applied to the Sellar Problem

Notice in this diagram, that the optimizer at the top has some data that passes from it to each of the disciplines. In MDF, both the global and local design variables are all controlled by the top level solver. So the data connections you see represent both.

The diagram also shows a solver that takes the output of the component dataflow and feeds it back into the input. OpenMDAO has two solvers in the standard library: FixedPointIterator and BroydenSolver. The FixedPointIterator is a solver that performs fixed point iteration, which means that it keeps driving x_new = f(x_old) until convergence is achieved. In other words, y2 is passed from the output of SellarDiscipline2 to the input of SellarDiscipline1, and the loop keeps executing until the change in the value of y2 between iterations is smaller than a tolerance. The BroydenSolver is based on a quasi-Newton-Raphson algorithm that uses a Broyden update to approximate the Jacobian. This solver reads the output and calculates a new input each iteration. Convergence is achieved when the residual between the output and input is driven to zero.

An important thing to take note of in the problem setup for MDF is the presence of nested driver and multiple workflows. Drivers can be nested in OpenMDAO using WorkFlows in the iteration hierarchy. A WorkFlow is an object that determines execution order for a group of Components. Each driver contains a single WorkFlow. For each iteration, a Driver will execute one pass through the WorkFlow, executing the components contained therein in the order the WorkFlow prescribes. Although in many cases a WorkFlow contains just Components, it can also contain Drivers, which then have thier own workflows. This allows nested iterative processes to be created. The following diagram shows an iteration hierarchy for the MDF problem. Iteration Hierarchy for the MDF Problem

Note that this iteration hierarchy does not contain any information about the data connections necessary to complete the MDF implementation. Workflows describe only process.

Now, let’s take the iteration hierarchy we just discussed and put in into an assembly, so we can actually run it.

from openmdao.main.api import Assembly, set_as_top
from openmdao.lib.drivers.api import SLSQPdriver, FixedPointIterator

from openmdao.lib.optproblems import sellar

class SellarMDF(Assembly):
""" Optimization of the Sellar problem using MDF
Disciplines coupled with FixedPointIterator.
"""

def configure(self):
""" Creates a new Assembly with this problem

Optimal Design at (1.9776, 0, 0)

Optimal Objective = 3.18339"""

# create Optimizer instance

# Outer Loop - Global Optimization


So far nothing is really new in terms of syntax. Note that the top level driver, in this case an instance of SLSQPdriver, is always named ‘driver’. However, all other drivers can be given any valid name. For this model, we’ve chosen to use the FixedPointIterator for our solver and we named it ‘solver’ in the code.

Next, we need to create the workflow for the solver. Add instances of SellarDiscipline1 and SellarDiscipline2 to the assembly. Then add those instances to the workflow of 'solver'

# Inner Loop - Full Multidisciplinary Solve via fixed point iteration


Now the iteration hierarchy pictured above is finished. To complete the MDF architecture though, we still need to hook up the data connections and configure the optimization and the fixed point iteration.

Recall that there are two global design variables, z1 and z2. In the model we constructed, you find z1 in two places: dis1.z1 and dis2.z1. The same is true for z2: dis1.z2 and dis2.z2. This means that when you add a parameter to the driver for z1 or z2, it needs to point to both locations in the model. We accomplish that below, by just passing a tuple of variable names, as the first argument to the add_parameter method.

# Add Parameters to optimizer
self.driver.add_parameter(('dis1.z1','dis2.z1'), low = -10.0, high = 10.0)
self.driver.add_parameter(('dis1.z2','dis2.z2'), low = 0.0,   high = 10.0)


There is only one local design variable for this problem, x1, which is found in dis1.x1. Since local design variables point to only one place in the model, we just add them using add_parameter with a single name as the first argument (just like we’ve shown you in previous tutorials).

self.driver.add_parameter('dis1.x1', low = 0.0,   high = 10.0)


Since we’re using a fixed point iteration to converge the disciplines, only one of the coupling variables (y2) is directly varied by the solver. The other one (y1) is just passed from discipline 1 to discipline 2 directly each iteration. The choice of which variable to let the solver vary and which to pass directly is arbitrary. You could have swapped the two, and the problem would still converge.

To tell a FixedPointIterator which variable to vary, we just use add_parameter again. During iteration, this is the variable that is going to be sent to the input of SellarDiscipline1, which is 'dis1y2'. We specify very small and large values for the low and high arguments because solvers shouldn’t really be constrained like that. Similarly, we setup the convergence constraint as an equality constraint. A solver essentially tries to drive something to zero. In this case, we want to drive the residual error in the coupled variable y2 to zero. An equality constraint is defined with an expression string which is parsed for the equals sign. In the above example, you see that 'dis2.y2 = dis1.y2' is equivalent to 'dis2.y2 - dis1.y2 = 0'. We also set the maximum number of iterations and a convergence tolerance.

# Make all connections
self.connect('dis1.y1','dis2.y1')

# Iteration loop
self.solver.max_iteration = 1000
self.solver.tolerance = .0001


Finally, the optimization is set up. We add the objective function as well as the constraints, from the problem formulation, to the driver. The objective function includes references to the global design variables. When this happens, you can pick any of the locations that the global design variable points to. In this case, we used dis1.z2, but we could have just as easily picked dis2.z2.

# Optimization parameters
self.driver.add_objective('(dis1.x1)**2 + dis1.z2 + dis1.y1 + math.exp(-dis2.y2)')

#Or use any of the equivalent forms below



As before, the add_constraint method is used to add our constraints. This time however, we used a more general expression for the first constraint. Alternate examples of the same constraint, composed slightly differently, are commented out in the example below.

Finally, putting it all together gives:

from openmdao.main.api import Assembly, set_as_top
from openmdao.lib.drivers.api import SLSQPdriver, FixedPointIterator

from openmdao.lib.optproblems import sellar

class SellarMDF(Assembly):
""" Optimization of the Sellar problem using MDF
Disciplines coupled with FixedPointIterator.
"""

def configure(self):
""" Creates a new Assembly with this problem

Optimal Design at (1.9776, 0, 0)

Optimal Objective = 3.18339"""

# create Optimizer instance

# Outer Loop - Global Optimization

# Inner Loop - Full Multidisciplinary Solve via fixed point iteration

self.driver.add_parameter(('dis1.z1','dis2.z1'), low = -10.0, high = 10.0)
self.driver.add_parameter(('dis1.z2','dis2.z2'), low = 0.0,   high = 10.0)
self.driver.add_parameter('dis1.x1', low = 0.0,   high = 10.0)

# Make all connections
self.connect('dis1.y1','dis2.y1')

# Iteration loop
# equivalent form
# self.solver.add_constraint('dis2.y2 - dis1.y2 = 0')

#Driver settings
self.solver.max_iteration = 1000
self.solver.tolerance = .0001

# Optimization parameters
self.driver.add_objective('(dis1.x1)**2 + dis1.z2 + dis1.y1 + math.exp(-dis2.y2)')

if __name__ == "__main__": # pragma: no cover

import time

prob = set_as_top(SellarMDF())
prob.name = "top"

prob.dis1.z1 = prob.dis2.z1 = 5.0
prob.dis1.z2 = prob.dis2.z2 = 2.0
prob.dis1.x1 = 1.0

tt = time.time()
prob.run()
print "\n"
print "Minimum found at (%f, %f, %f)" % (prob.dis1.z1, \
prob.dis1.z2, \
prob.dis1.x1)
print "Couping vars: %f, %f" % (prob.dis1.y1, prob.dis2.y2)
print "Minimum objective: ", prob.driver.eval_objective()
print "Elapsed time: ", time.time()-tt, "seconds"

# End sellar_MDF.py


This problem is contained in sellar_MDF.py. We added just a few lines at the end to instantiate the assembly class we defined and then run it and print out some useful information. Executing it at the command line should produce output that resembles this:

\$ python sellar_MDF.py
Minimum found at (1.977657, 0.000000, 0.000000)
Couping vars: 3.160068, 3.755315
Minimum objective:  3.18346116811
Elapsed time:  0.121051073074 seconds

We initially chose to use FixedPointIterator for our solver, but you could replace that with a different one. Fixed point iteration works for some problems, including this one, but sometimes another type of solver might be preferred. OpenMDAO also contains a Broyden solver called BroydenSolver. This solver is based on a quasi-Newton-Raphson algorithm found in scipy.nonlinear. It uses a Broyden update to approximate the Jacobian. If we replace FixedPointIterator with BroydenSolver, the optimizer’s workflow looks like this:

# Don't forget to put the import in your header
from openmdao.lib.drivers.api import BroydenSolver

# Outer Loop - Global Optimization


Next, we set up our parameters for the inner loop. The Broyden solver is connected using the same interface as the fixed point iterator, so that code does not change at all. We just change some of solver specific settings.

# Iteration loop
# equivalent form
# self.solver.add_constraint('dis2.y2 - dis1.y2 = 0')

self.solver.itmax = 10
self.solver.alpha = .4
self.solver.tol = .0000001
self.solver.algorithm = "broyden2"


The rest of the file does not change at all either. So you can see that it’s pretty easy to reconfigure drivers using this setup. Here is the new file, with the modifications: sellar_MDF_solver.py. #### Previous topic

The Sellar Problem

#### Next topic

Individual Design Feasible (IDF)