Approximating Semi-Total Derivatives

There are times where it makes sense to approximate the derivatives for an entire group in one shot. You can turn on the approximation by calling approx_totals on any Group.

Group.approx_totals(method='fd', step=None, form=None, step_calc=None)[source]

Approximate derivatives for a Group using the specified approximation method.

Parameters:
method : str

The type of approximation that should be used. Valid options include: ‘fd’: Finite Difference, ‘cs’: Complex Step

step : float

Step size for approximation. Defaults to None, in which case, the approximation method provides its default value.

form : string

Form for finite difference, can be ‘forward’, ‘backward’, or ‘central’. Defaults to None, in which case, the approximation method provides its default value.

step_calc : string

Step type for finite difference, can be ‘abs’ for absolute’, or ‘rel’ for relative. Defaults to None, in which case, the approximation method provides its default value.

The default method for approximating semi-total derivatives is the finite difference method. When you call the approx_totals method on a group, OpenMDAO will generate an approximate Jacobian for the entire group during the linearization step before derivatives are calculated. OpenMDAO automatically figures out which inputs and output pairs are needed in this Jacobian. When solve_linear is called from any system that contains this system, the approximated Jacobian is used for the derivatives in this system.

The derivatives approximated in this manner are total derivatives of outputs of the group with respect to inputs. If any components in the group contain implicit states, then you must have an appropriate solver (such as NewtonSolver) inside the group to solve the implicit relationships.

Here is a classic example of where you might use an approximation like finite difference. In this example, we could just approximate the partials on components CompOne and CompTwo separately. However, CompTwo has a vector input that is 25 wide, so it would require 25 separate executions under finite difference. If we instead approximate the total derivatives on the whole group, we only have one input, so just one extra execution.

import numpy as np

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

class CompOne(ExplicitComponent):

        def setup(self):
            self.add_input('x', val=0.0)
            self.add_output('y', val=np.zeros(25))
            self._exec_count = 0

        def compute(self, inputs, outputs):
            x = inputs['x']
            outputs['y'] = np.arange(25) * x
            self._exec_count += 1


class CompTwo(ExplicitComponent):

        def setup(self):
            self.add_input('y', val=np.zeros(25))
            self.add_output('z', val=0.0)
            self._exec_count = 0

        def compute(self, inputs, outputs):
            y = inputs['y']
            outputs['z'] = np.sum(y)
            self._exec_count += 1


prob = Problem()
model = prob.model = Group()
model.add_subsystem('p1', IndepVarComp('x', 0.0), promotes=['x'])
model.add_subsystem('comp1', CompOne(), promotes=['x', 'y'])
comp2 = model.add_subsystem('comp2', CompTwo(), promotes=['y', 'z'])

model.linear_solver = ScipyKrylov()
model.approx_totals()

prob.setup()
prob.run_model()

of = ['z']
wrt = ['x']
derivs = prob.compute_totals(of=of, wrt=wrt)

print(derivs['z', 'x'])
[[ 300.]]
print(comp2._exec_count)
2

The same arguments are used for both partial and total derivative approximation specifications. Here we set the finite difference step size, the form to central differences, and the step_calc to relative instead of absolute.

import numpy as np

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

class CompOne(ExplicitComponent):

        def setup(self):
            self.add_input('x', val=0.0)
            self.add_output('y', val=np.zeros(25))
            self._exec_count = 0

        def compute(self, inputs, outputs):
            x = inputs['x']
            outputs['y'] = np.arange(25) * x
            self._exec_count += 1


class CompTwo(ExplicitComponent):

        def setup(self):
            self.add_input('y', val=np.zeros(25))
            self.add_output('z', val=0.0)
            self._exec_count = 0

        def compute(self, inputs, outputs):
            y = inputs['y']
            outputs['z'] = np.sum(y)
            self._exec_count += 1


prob = Problem()
model = prob.model = Group()
model.add_subsystem('p1', IndepVarComp('x', 1.0), promotes=['x'])
model.add_subsystem('comp1', CompOne(), promotes=['x', 'y'])
comp2 = model.add_subsystem('comp2', CompTwo(), promotes=['y', 'z'])

model.linear_solver = ScipyKrylov()
model.approx_totals(method='fd', step=1e-7, form='central', step_calc='rel')

prob.setup()
prob.run_model()

of = ['z']
wrt = ['x']
derivs = prob.compute_totals(of=of, wrt=wrt)

print(derivs['z', 'x'])
[[ 300.00000048]]

Complex Step

You can also complex step your model or group, though there are some important restrictions.

All components must support complex calculations in solve_nonlinear:
Under complex step, a component’s inputs are complex, all stages of the calculation will operate on complex inputs to produce complex outputs, and the final value placed into outputs is complex. Most Python functions already support complex numbers, so pure Python components will generally satisfy this requirement. Take care with functions like abs, which effectively squelches the complex part of the argument.
Solvers like Newton that require gradients are not supported:
Complex stepping a model causes it to run with complex inputs. When there is a nonlinear solver at some level, the solver must be able to converge. Some solvers such as NonlinearBlockGS can handle this. However, the Newton solver must linearize and initiate a gradient solve about a complex point. This is not possible to do at present (though we are working on some ideas to make this work.)
import numpy as np

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

class CompOne(ExplicitComponent):

        def setup(self):
            self.add_input('x', val=0.0)
            self.add_output('y', val=np.zeros(25))
            self._exec_count = 0

        def compute(self, inputs, outputs):
            x = inputs['x']
            outputs['y'] = np.arange(25) * x
            self._exec_count += 1


class CompTwo(ExplicitComponent):

        def setup(self):
            self.add_input('y', val=np.zeros(25))
            self.add_output('z', val=0.0)
            self._exec_count = 0

        def compute(self, inputs, outputs):
            y = inputs['y']
            outputs['z'] = np.sum(y)
            self._exec_count += 1


prob = Problem()
model = prob.model = Group()
model.add_subsystem('p1', IndepVarComp('x', 0.0), promotes=['x'])
model.add_subsystem('comp1', CompOne(), promotes=['x', 'y'])
comp2 = model.add_subsystem('comp2', CompTwo(), promotes=['y', 'z'])

model.linear_solver = ScipyKrylov()
model.approx_totals(method='cs')

prob.setup()
prob.run_model()

of = ['z']
wrt = ['x']
derivs = prob.compute_totals(of=of, wrt=wrt)

print(derivs['z', 'x'])
[[ 300.]]