A Guide to Using Complex Step to Compute Derivatives

The intent of this guide is to summarize in detail how to use complex step to compute derivatives. It is assumed that you’re already familiar with OpenMDAO usage in general.

Setting Up Complex Step

When complex step is used somewhere in an OpenMDAO model, OpenMDAO must allocate sufficient memory to contain a complex version of the nonlinear vector, and in many cases also a complex version of the linear vector. OpenMDAO can figure this out automatically most of the time. For example, if any component in your model calls either declare_partials or declare_coloring with method='cs', complex vectors will be allocated automatically.

The main situation where complex vectors are needed but are not allocated automatically is when you call either check_totals or check_partials with method='cs' and nothing in your model natively uses complex step. In that case, you must tell OpenMDAO that complex vectors are required by passing a force_alloc_complex=True argument when calling setup on your Problem. The force_alloc_complex flag will force OpenMDAO to allocate complex nonlinear vectors regardless of what it detects in the model.

Note that while ExecComp components use complex step to compute derivatives by default, they do not require that the OpenMDAO nonlinear vectors are complex because they perform their own internal complex step operation. However, if you declare your own partials on an ExecComp using declare_partials or declare_coloring with method='cs', then that component will use the framework level complex step routines and will be treated as any other component with partials declared in that manner.

How to Tell if a Component is Running Under Complex Step

A component can tell when it’s running under complex step by checking the value of its under_complex_step attribute. A similar flag, under_finite_difference can be used to tell if a component is running under finite difference. Here’s an example of a component that checks its complex step status in its compute method:

import openmdao.api as om

class MyCheckComp(om.ExplicitComponent):
    def setup(self):
        self.add_input('a', 1.0)
        self.add_output('x', 0.0)
        # because we set method='cs' here, OpenMDAO automatically knows to allocate
        # complex nonlinear vectors
        self.declare_partials(of='*', wrt='*', method='cs')

    def compute(self, inputs, outputs):
        a = inputs['a']
        if self.under_complex_step:
            print('under complex step')
        else:
            print('not under complex step')
        outputs['x'] = a * 2.

p = om.Problem()
p.model.add_subsystem('comp', MyCheckComp())
# don't need to set force_alloc_complex=True here since we call declare_partials with
# method='cs' in our model.
p.setup()

# during run_model, our component's compute will *not* be running under complex step
p.run_model()

# during compute_partials, our component's compute *will* be running under complex step
J = p.compute_totals(of=['comp.x'], wrt=['comp.a'])
print(J['comp.x', 'comp.a'])
not under complex step
under complex step
[[2.]]

Complex Stepping through Solvers

See Complex Step Guidelines for important issues to consider when your model has nonlinear solvers under a group that is performing complex step.

Using Complex Step to Compute a Partial Jacobian Coloring

OpenMDAO can compute a coloring of the partial jacobian matrix for a component that uses complex step or finite difference to compute its derivatives. For a detailed explanation of this, see Simultaneous Coloring of Approximated Derivatives. Assuming your component is complex safe, generally using complex step is more accurate than finite difference and should be preferred when computing a coloring of the partial jacobian.

The cs_safe Module

The openmdao.utils.cs_safe module contains complex-safe versions of a few common functions, namely, abs, norm, and arctan2. The numpy versions of these functions are not complex-safe and so must be replaced with the safe versions if you intend to use such functions in your component under complex step. Note that the ExecComp component, which uses complex step by default, automatically uses the complex safe versions of these functions if they are referenced in one of its expressions.

The following example shows how to make a component that uses abs safe to use under complex step:

import openmdao.api as om
from openmdao.utils.cs_safe import abs as cs_abs

class MyComp(om.ExplicitComponent):
    def setup(self):
        self.add_input('a', 1.0)
        self.add_input('b', -2.0)
        self.add_output('x', 0.0)
        # because we set method='cs' here, OpenMDAO automatically knows to allocate
        # complex nonlinear vectors
        self.declare_partials(of='*', wrt='*', method='cs')

    def compute(self, inputs, outputs):
        a, b = inputs.values()
        # normal abs isn't complex safe, so use cs_abs
        outputs['x'] = a * cs_abs(b) * 2.

p = om.Problem()
p.model.add_subsystem('comp', MyComp())
# don't need to set force_alloc_complex=True here since we call declare_partials with
# method='cs' in our model.
p.setup()
p.run_model()

J = p.compute_totals(of=['comp.x'], wrt=['comp.a', 'comp.b'])
print(J['comp.x', 'comp.a'])
print(J['comp.x', 'comp.b'])
[[4.]]
[[-2.]]

Model Level Complex Step Mode for Debugging

Sometimes when debugging it may be useful to run a model or part of a model using complex inputs. This can be done by calling set_complex_step_mode(True) on the Problem instance. You can then call

prob['some_var'] = a_complex_val

or

prob.set_val('some_var', a_complex_val)

then run the model by calling

prob.run_model()

The complex values will carry through the model as it runs. Note that this only works if all of the components in the model are complex-safe. After the model run has completed, the outputs can then be inspected using one of the following:

x = prob['some_output']

or

x = prob.get_val('some_output')

or

prob.model.list_outputs()

Here’s a short example:

p = om.Problem()
p.model.add_subsystem('comp', MyComp())
p.setup()

p['comp.a'] = 1.5
p['comp.b'] = -3.

p.run_model()

# output x should be a float here
print('float', p['comp.x'])

# now we're setting the problem to use complex step
p.set_complex_step_mode(True)

p['comp.a'] = 1.5+2j
p['comp.b'] = -3.-7j

p.run_model()

# output x should be complex here
print('complex', p['comp.x'])
float [9.]
complex [-19.+33.j]