{
"cells": [
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"tags": [
"remove-input",
"active-ipynb",
"remove-output"
]
},
"outputs": [],
"source": [
"try:\n",
" from openmdao.utils.notebook_utils import notebook_mode\n",
"except ImportError:\n",
" !python -m pip install openmdao[notebooks]"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# A Guide to Using Complex Step to Compute Derivatives\n",
"\n",
"The intent of this guide is to summarize in detail how to use complex step to compute derivatives.\n",
"It is assumed that you're already familiar with OpenMDAO usage in general. "
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# Setting Up Complex Step\n",
"\n",
"When complex step is used somewhere in an OpenMDAO model, OpenMDAO must allocate sufficient memory to\n",
"contain a complex version of the nonlinear vector, and in many cases also a complex version of the\n",
"linear vector. OpenMDAO can figure this out automatically most of the time. For example, if any \n",
"component in your model calls either `declare_partials` or `declare_coloring` with `method='cs'`,\n",
"complex vectors will be allocated automatically. \n",
"\n",
"The main situation where complex vectors are \n",
"needed but are *not* allocated automatically is when you call either `check_totals` or `check_partials`\n",
"with `method='cs'` and nothing in your model natively uses complex step. In that case, you must\n",
"tell OpenMDAO that complex vectors are required by passing a `force_alloc_complex=True` argument\n",
"when calling `setup` on your `Problem`. The `force_alloc_complex` flag will force OpenMDAO to \n",
"allocate complex nonlinear vectors regardless of what it detects in the model.\n",
"\n",
"Note that while `ExecComp` components use complex step to compute derivatives by default, they do not \n",
"require that the OpenMDAO nonlinear vectors are complex because they perform their own internal\n",
"complex step operation. However, if you declare your own partials on an `ExecComp` using \n",
"`declare_partials` or `declare_coloring` with `method='cs'`, then that component will use the\n",
"framework level complex step routines and will be treated as any other component with partials\n",
"declared in that manner."
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# How to Tell if a Component is Running Under Complex Step\n",
"\n",
"A component can tell when it's running under complex step by checking the value of its `under_complex_step`\n",
"attribute. A similar flag, `under_finite_difference` can be used to tell if a component is running\n",
"under finite difference. Here's an example of a component that checks its complex step status in \n",
"its `compute` method:"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"import openmdao.api as om\n",
"\n",
"class MyCheckComp(om.ExplicitComponent):\n",
" def setup(self):\n",
" self.add_input('a', 1.0)\n",
" self.add_output('x', 0.0)\n",
" # because we set method='cs' here, OpenMDAO automatically knows to allocate\n",
" # complex nonlinear vectors\n",
" self.declare_partials(of='*', wrt='*', method='cs')\n",
"\n",
" def compute(self, inputs, outputs):\n",
" a = inputs['a']\n",
" if self.under_complex_step:\n",
" print('under complex step')\n",
" else:\n",
" print('not under complex step')\n",
" outputs['x'] = a * 2.\n",
"\n",
"p = om.Problem()\n",
"p.model.add_subsystem('comp', MyCheckComp())\n",
"# don't need to set force_alloc_complex=True here since we call declare_partials with\n",
"# method='cs' in our model.\n",
"p.setup()\n",
"\n",
"# during run_model, our component's compute will *not* be running under complex step\n",
"p.run_model()\n",
"\n",
"# during compute_partials, our component's compute *will* be running under complex step\n",
"J = p.compute_totals(of=['comp.x'], wrt=['comp.a'])\n",
"print(J['comp.x', 'comp.a'])"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# Complex Stepping through Solvers\n",
"\n",
"See [Complex Step Guidelines](complex-step-guidelines) for important issues to consider when your model has nonlinear solvers \n",
"under a group that is performing complex step."
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# Using Complex Step to Compute a Partial Jacobian Coloring\n",
"\n",
"OpenMDAO can compute a coloring of the partial jacobian matrix for a component that uses complex step\n",
"or finite difference to compute its derivatives. For a detailed explanation of this, see \n",
"[Simultaneous Coloring of Approximated Derivatives](../features/experimental/approx_coloring.ipynb).\n",
"Assuming your component is complex safe, generally using complex step is more accurate than finite difference\n",
"and should be preferred when computing a coloring of the partial jacobian."
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# The cs_safe Module\n",
"\n",
"The `openmdao.utils.cs_safe` module contains complex-safe versions of a few common functions, namely,\n",
"`abs`, `norm`, and `arctan2`. The `numpy` versions of these functions are not complex-safe and so\n",
"must be replaced with the safe versions if you intend to use such functions in your component under\n",
"complex step. Note that the `ExecComp` component, which uses complex step by default, automatically\n",
"uses the complex safe versions of these functions if they are referenced in one of its expressions.\n",
"\n",
"The following example shows how to make a component that uses `abs` safe to use under complex step:"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"import openmdao.api as om\n",
"from openmdao.utils.cs_safe import abs as cs_abs\n",
"\n",
"class MyComp(om.ExplicitComponent):\n",
" def setup(self):\n",
" self.add_input('a', 1.0)\n",
" self.add_input('b', -2.0)\n",
" self.add_output('x', 0.0)\n",
" # because we set method='cs' here, OpenMDAO automatically knows to allocate\n",
" # complex nonlinear vectors\n",
" self.declare_partials(of='*', wrt='*', method='cs')\n",
"\n",
" def compute(self, inputs, outputs):\n",
" a, b = inputs.values()\n",
" # normal abs isn't complex safe, so use cs_abs\n",
" outputs['x'] = a * cs_abs(b) * 2.\n",
"\n",
"p = om.Problem()\n",
"p.model.add_subsystem('comp', MyComp())\n",
"# don't need to set force_alloc_complex=True here since we call declare_partials with\n",
"# method='cs' in our model.\n",
"p.setup()\n",
"p.run_model()\n",
"\n",
"J = p.compute_totals(of=['comp.x'], wrt=['comp.a', 'comp.b'])\n",
"print(J['comp.x', 'comp.a'])\n",
"print(J['comp.x', 'comp.b'])"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# Model Level Complex Step Mode for Debugging\n",
"\n",
"Sometimes when debugging it may be useful to run a model or part of a model using complex inputs. This\n",
"can be done by calling `set_complex_step_mode(True)` on the `Problem` instance. You can then call\n",
"\n",
"```python\n",
"prob['some_var'] = a_complex_val\n",
"```\n",
"\n",
"or\n",
"\n",
"```python\n",
"prob.set_val('some_var', a_complex_val)\n",
"```\n",
"\n",
"then run the model by calling\n",
"\n",
"```python\n",
"prob.run_model()\n",
"```\n",
"\n",
"The complex values will carry through the model as it runs. Note that this only works if all of\n",
"the components in the model are complex-safe. After the model run has completed, the outputs\n",
"can then be inspected using one of the following:\n",
"\n",
"```python\n",
"x = prob['some_output']\n",
"```\n",
"\n",
"or\n",
"\n",
"```python\n",
"x = prob.get_val('some_output')\n",
"```\n",
"\n",
"or \n",
"\n",
"```python\n",
"prob.model.list_outputs()\n",
"```\n",
"\n",
"Here's a short example:"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"p = om.Problem()\n",
"p.model.add_subsystem('comp', MyComp())\n",
"p.setup()\n",
"\n",
"p['comp.a'] = 1.5\n",
"p['comp.b'] = -3.\n",
"\n",
"p.run_model()\n",
"\n",
"# output x should be a float here\n",
"print('float', p['comp.x'])\n",
"\n",
"# now we're setting the problem to use complex step\n",
"p.set_complex_step_mode(True)\n",
"\n",
"p['comp.a'] = 1.5+2j\n",
"p['comp.b'] = -3.-7j\n",
"\n",
"p.run_model()\n",
"\n",
"# output x should be complex here\n",
"print('complex', p['comp.x'])"
]
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3 (ipykernel)",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.8.12"
},
"orphan": true
},
"nbformat": 4,
"nbformat_minor": 4
}