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]