{
"cells": [
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"tags": [
"active-ipynb",
"remove-input",
"remove-output"
]
},
"outputs": [],
"source": [
"# This cell is mandatory in all Dymos documentation notebooks.\n",
"missing_packages = []\n",
"try:\n",
" import openmdao.api as om\n",
"except ImportError:\n",
" if 'google.colab' in str(get_ipython()):\n",
" !python -m pip install openmdao[notebooks]\n",
" else:\n",
" missing_packages.append('openmdao')\n",
"try:\n",
" import dymos as dm\n",
"except ImportError:\n",
" if 'google.colab' in str(get_ipython()):\n",
" !python -m pip install dymos\n",
" else:\n",
" missing_packages.append('dymos')\n",
"try:\n",
" import pyoptsparse\n",
"except ImportError:\n",
" if 'google.colab' in str(get_ipython()):\n",
" !pip install -q condacolab\n",
" import condacolab\n",
" condacolab.install_miniconda()\n",
" !conda install -c conda-forge pyoptsparse\n",
" else:\n",
" missing_packages.append('pyoptsparse')\n",
"if missing_packages:\n",
" raise EnvironmentError('This notebook requires the following packages '\n",
" 'please install them and restart this notebook\\'s runtime: {\",\".join(missing_packages)}')"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# Exploiting Sparsity for Faster Derivative Calculation\n",
"\n",
"A key feature of collocation algorithms such as high-order Gauss-Lobatto collocation or the Radau pseudospectral method is that they exhibit a large degree of *sparsity* in the total Jacobian.\n",
"By modeling the state-time histories as a series of polynomial segments, the collocation defect constraints within each segment are largely dependent only on the state and control values within the same segment.\n",
"Pseudospectral optimization tools such as SOCS, OTIS, and GPOPS-II have long used the notion of *sparse finite differences* to perturb multiple independent variables simultaneously when approximating the constraint Jacobian.\n",
"This can significantly reduce the computational effort required to approximate the entire Jacobian via finite difference.\n",
"\n",
"The unique way in which OpenMDAO assembles analytic derivatives across the problem allows us to use a similar approach to provide the analytic constraint Jacobian.\n",
"This approach can reduce the time required to solve moderately-sized optimal control problems by orders of magnitude and make the convergence more robust.\n",
"\n",
"OpenMDAO uses a [\"coloring\" algorithm](https://openmdao.org/newdocs/versions/latest/features/core_features/working_with_derivatives/simul_derivs.html) to determine which variables can be perturbed simultaneously to determine the total constraint Jacobian.\n",
"Variables in the same \"color\" each impact a unique constraint, such that when all the variables are perturbed we can be assured that any change in the constraint vector is due to at most one scalar variable.\n",
"Since OpenMDAO uses a linear solver to assemble to total derivative Jacobian, coloring reduces the number of linear solves from one per variable (or constraint in adjoint mode) to one per *color*.\n",
"In the brachistochrone example below this reduces the number of linear solves from 1001 to 9.\n",
"This capability makes problems of moderate size run orders of magnitude faster, and can make intractably large problems tractable.\n",
"\n",
"```{Note}\n",
"While some optimizers (SNOPT and IPOPT) are particularly adept at dealing with large, sparse nonlinear programming problems, coloring can still benefit drivers which do not account for sparsity (such as SLSQP) since it significantly reduces the cost of computing the Jacobian.\n",
"```\n",
"\n",
"## Step 1: Using OpenMDAO's Coloring Capability\n",
"\n",
"OpenMDAO supports dynamic coloring, meaning it can automatically run the Jacobian coloring algorithm before handing the problem to the optimizer.\n",
"To enable this capability, simply add the following line to the driver."
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"```python\n",
" driver.declare_coloring()\n",
"```"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"By default the coloring algorithm will attempt to determine the sparsity pattern of the total jacobian by filling the partial jacobian matrices with random noise and searching for nonzeros in the resulting total jacobian.\n",
"At times this might report that it failed to converge on a number of nonzero entries.\n",
"This is due to the introduction of noise during the matrix inversion by the system's linear solver.\n",
"This can be remedied by using a different linear solver, such as PETScKrylov, or by telling the coloring algorithm to accept a given tolerance on the nonzero elements rather than trying to determine it automatically.\n",
"This can be accomplished with the following options to declare coloring:"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"```python\n",
" driver.declare_coloring(tol=1.0E-12, orders=None)\n",
"```"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Setting `orders` to None prevents the automatic tolerance detection.\n",
"The value of `tol` is up to the user.\n",
"If it is too large, then some nonzero values will erroneously be determined to be zeros and the total derivative will be incorrect.\n",
"If `tol` is too small then the sparsity pattern may be overly conservative and degrade performance somewhat.\n",
"We recommend letting the coloring algorithm detect the sparsity automatically and only resorting to a fixed tolerance if necessary.\n",
"\n",
"## Running the coloring algorithm separately\n",
"\n",
"OpenMDAO supports the ability to run the coloring algorithm \"offline\" and then provide the data to the driver via a saved file.\n",
"This is potentially useful to users whose model is particularly expensive to color, and whom aren't changing the problem structure between runs.\n",
"\n",
"```{Note}\n",
"Adding or removing constraints, controls, states, or parameters, as well as changing the objective variable or changing the grid will all affect the fundamental size of the optimization problem and therefore will require a new coloring.\n",
"Simply changing constraint bounds or scaling on variables and constraints will not fundamentally change the size of the problem and will therefore not require a recoloring.\n",
"```\n",
"\n",
"The `use_static_coloring` Driver method in OpenMDAO is documented [here](https://openmdao.org/newdocs/versions/latest/features/core_features/working_with_derivatives/simul_derivs.html#static-coloring).\n",
"\n",
"## Viewing the sparsity\n",
"\n",
"The summary of the sparsity pattern can be obtained following a run with the [OpenMDAO command line view_coloring utility](https://openmdao.org/newdocs/versions/latest/other_useful_docs/om_command.html)."
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"where the `coloring_files/total_coloring.pkl` file is automatically generated in the run directory by OpenMDAO when coloring is used.\n",
"For this example, the [Brachistochrone example](../examples/brachistochrone/brachistochrone.ipynb) is used with 100 3rd-order segments in the Radau transcription.\n",
"The output is:\n",
"\n",
"```\n",
"Jacobian shape: (1099, 1200) ( 0.54% nonzero)\n",
"FWD solves: 11 REV solves: 0\n",
"Total colors vs. total size: 11 vs 1200 (99.1% improvement)\n",
"\n",
"Sparsity computed using tolerance: 1e-08\n",
"Time to compute sparsity: 1.706023 sec.\n",
"Time to compute coloring: 0.038774 sec.\n",
"```\n",
"\n",
"In addition, adding the `--view` flag to the command will generate a visualization of the sparsity using matplotlib.\n",
"\n",
"The gray checkerboard represents the vectorized variables (columns) and constraints (rows).\n",
"In this case, the column groupings correspond to `t_duration`, followed by the states `x`, `y`, and `v`, and finally the control `theta`.\n",
"The row groupings correspond to the state defects on `x`, `y`, and `v`, followed by the `theta` control value continuity constraints and the `theta` control rate continuity constraints.\n",
"The ordering of these variables and constraints is the same as output by the pyOptSparseDriver.\n",
"\n",
"In this case we can see that there is a single dense column on the left.\n",
"This is the phase duration, which impacts all of the defect constraints in the problem.\n",
"\n",
"![Sparsity plot for the 100-segment brachistochrone problem](figures/sparsity_plot.png)\n",
"\n",
"## Reverse-mode coloring\n",
"\n",
"Pseudospectral optimal control problems often have roughly equal numbers of constraints and design variables.\n",
"As a result, there doesn't tend to be a significant increase in performance by using adjoint (reverse) differentiation.\n",
"\n",
"In Dymos, when the `solve_segments` flag on a state or transcription is used, an underlying nonlinear solver is used to satisfy the constraint defects.\n",
"The optimizer only controls the initial or final state values in a phase as the design variables, and the solver is responsible for satisfying the corresponding state collocation defects.\n",
"As a result, the problem is effectively transformed into a shooting problem (single shooting when compressed transcription is used, or multiple shooting when it is not).\n",
"These problems tend to exhibit many more design variables than constraints, and thus adjoint differentiation can be advantageous.\n",
"Fortunately, the coloring algorithm used in OpenMDAO automatically detects this and colors the problem in reverse mode.\n",
"\n",
"If we run the 100-segment Radau brachistochrone problem with `solve_segments=forward` (taking care to replace the terminal state bounds with nonlinear boundary constraints), the following coloring is obtained:\n",
"\n",
"```\n",
"Jacobian shape: (201, 302) ( 2.79% nonzero)\n",
"FWD solves: 0 REV solves: 102\n",
"Total colors vs. total size: 102 vs 201 (49.3% improvement)\n",
"\n",
"Sparsity computed using tolerance: 1e-25\n",
"Time to compute sparsity: 0.414226 sec.\n",
"Time to compute coloring: 0.017739 sec.\n",
"```\n",
"\n",
"![Sparsity plot for the 100-segment brachistochrone problem with solve_segments enabled.](figures/sparsity_plot_solve_segments.png)\n",
"\n",
"In this case, the majority of the constraints pertain to the control value and rate continuity.\n",
"The majority of the design variables are the control values at the control input nodes.\n",
"The two dense rows at the bottom are the nonlinear boundary constraints on `x` and `y`.\n",
"The entirety of the control history impacts these values in a shooting formulation.\n",
"\n",
"## Performance comparison with and without simultaneous derivatives\n",
"\n",
"In comparison, the 100-segment, Radau-based brachistochrone problem _without_ coloring solves in roughly two minutes with a 2.4 GHz Intel Core i9 processor using the IPOPT optimizer.\n",
"While computing the sensitivity itself takes only 16 seconds, the large amount of data being used to store knowably-zero quantities is quite large, which results in a lot of time spent in the optimizer dealing with that data.\n",
"\n",
"For the `solve_segments` case, we see that it significantly reduces the size of the problem from the optimizer's perspective, driving the optimization time down significantly.\n",
"The additional cost of solving the collocation problem with a Newton solver is reflected in the somewhat greater objective and sensitivity time compared to the nominal case.\n",
"\n",
"The key takeaways from the following table are:\n",
"\n",
"- Optimizer-driven pseudospectral approaches rely on the exploitation of sparsity to be efficient\n",
"- Shooting approaches, when numerically appropriate, can be reasonably competitive with pseudospectral approaches under adjoint differentiation.\n",
"\n",
"| | With Dynamic Coloring | Without Coloring | `solve_segments` With Coloring | `solve_segments` Without Coloring |\n",
"|--------------------------------|-----------------------|------------------|--------------------------------|-----------------------------------|\n",
"| Total Time | 1.5692 | 127.9437 | 2.2911 | 4.6551 |\n",
"| User Objective Time | 0.0510 | 0.0957 | 0.4937 | 0.6104 |\n",
"| User Sensitivity Time | 0.6618 | 16.5347 | 1.6315 | 2.9759 |\n",
"| Interface Time | 0.5668 | 2.1390 | 0.1016 | 0.1558 |\n",
"| Opt Solver Time | 0.2896 | 109.1743 | 0.0643 | 0.9129 |\n",
"| Calls to Objective Function | 24 | 28 | 23 | 23 |\n",
"| Calls to Sens Function | 24 | 28 | 23 | 23 |\n",
"\n",
"Performance aside, there are other factors to consider when choosing between shooting methods or optimizer-driven pseudospectral approaches.\n",
"Primarily, the shooting methods propagate the entirety of the trajectory at each iteration.\n",
"If there are singularities in the equations of motion, and a given control profile causes the propagation to encounter a singularity, the solver will fail to converge and it can stall the entire solution process.\n",
"Optimizer-driven approaches can better avoid these issues because the trajectory is not required to be physically realizable at each iteration.\n",
"\n",
"## Use of Components with Finite-Differencing in the ODE\n",
"\n",
"Check out [this page](../faq/use_partial_coloring.ipynb) to learn more about how to more efficiently use finite-differencing in ODE components."
]
}
],
"metadata": {
"jupytext": {
"cell_metadata_filter": "-all",
"notebook_metadata_filter": "-all",
"text_representation": {
"extension": ".md",
"format_name": "markdown"
}
},
"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.9.7"
}
},
"nbformat": 4,
"nbformat_minor": 4
}