---
title: Inside OpenMDAO
summary: Wrap the flow discipline as a matrix-free OpenMDAO component, in reduced space or at residual level, and let the framework assemble total derivatives in forward or reverse mode. No optimizer involved.
section: examples
order: 9
level: 2
audience: [MDO integrators]
keywords: [OpenMDAO, adapter, compute_jacvec_product, totals, example]
---
<!-- gyre docs: expanded Markdown of examples/openmdao.md (generated tables, verification numbers and chart data are written out). Base level of this page: L2 Practice; passages at another level are bracketed by 'begin level N' / 'end level N' comments and start with [LN Name]. Built from revision ceb1f4b0be. -->

```python
import numpy as np
import openmdao.api as om

from gyre.cases import Case, preset
from gyre.integrations.openmdao import GyreFlowComp

flow = Case(preset("naca0012_euler", **{"solver.tol_rel": 1e-12})).discipline

prob = om.Problem()
prob.model.add_subsystem(
    "flow", GyreFlowComp(discipline=flow, inputs=("alpha", "mach"), outputs=("cl", "cd")),
    promotes=["*"])
prob.setup(mode="rev")
prob.set_val("alpha", np.radians(2.0))
prob.run_model()

totals = prob.compute_totals(of=["cl", "cd"], wrt=["alpha", "mach", "x_surface"])
```

`examples/openmdao_wrapper.py` is a runnable script built on this adapter
(`.venv/bin/python examples/openmdao_wrapper.py`).

The adapter needs OpenMDAO, which the `mdo` extra installs (`pip install -e ".[mdo]"`); the
`all` extra, which `make bootstrap` installs, includes it.

<!-- begin level 2 (Practice) -->
**[L2 Practice] What the adapter does**

| OpenMDAO calls | Adapter does |
|---|---|
| `compute` | `set_inputs`, warm-started `solve` (both skipped when the discipline already holds the converged solution for these inputs), `outputs`; raises `om.AnalysisError` if the flow does not converge or the inputs are outside the range of the flow model; after a failed point the next call always solves again, also for inputs that had converged before |
| `compute_jacvec_product(mode="rev")` | one `adjoint_product` for all seeded outputs |
| `compute_jacvec_product(mode="fwd")` | one `tangent_product` for the perturbed inputs |

Options: `inputs` (scalar flow parameters), `outputs` (scalar functionals), `shape=True`
(expose `x_surface`), `loads=True` (expose `f_surface`), `linear_tol`.

<!-- end level 2 -->

<!-- begin level 2 (Practice) -->
**[L2 Practice] Residual level: the flow as an implicit component**

```python
from gyre.integrations.openmdao import GyreFlowGroup

prob.model.add_subsystem(
    "flow", GyreFlowGroup(discipline=flow, inputs=("alpha", "mach"), outputs=("cl", "cd"),
                          loads=True),
    promotes=["*"])
```

`GyreFlowGroup` contains two components. The implicit one has the flow state as its output:
OpenMDAO asks it for the residual (`apply_nonlinear`), for products with the linearised
residual in either mode (`apply_linear`) and for linear solves (`solve_linear`), each about
the state and inputs OpenMDAO holds; its `solve_nonlinear` is the flow solve. The second
component evaluates the functionals and the load field from inputs and state. The framework
then owns the coupled linear system, which is what a hierarchical or monolithic coupled
adjoint needs.

| OpenMDAO calls | Adapter does |
|---|---|
| `solve_nonlinear` | `solve_nonlinear(inputs)` of the discipline; the state becomes the component's output |
| `apply_nonlinear` | `apply_nonlinear(inputs, state)`: the weak-form residual at OpenMDAO's values |
| `apply_linear` (fwd / rev) | `jvp` / `vjp` about that point |
| `solve_linear` (fwd / rev) | `solve_linear`; a solve that misses `linear_tol` raises `om.AnalysisError` |

**Table: OpenMDAO integration** (Discipline contract for MDO integration)

| quantity | value |
|---|---|
| check_partials flow.solver: 4 (of, wrt) pairs, directional | 3.0315e-09 |
| check_partials flow.functionals: 12 (of, wrt) pairs, directional | 5.6471e-09 |
| totals through the implicit component (reverse) vs FlowDiscipline.totals | 4.1434e-16 |
| coupled dcl/dα, OpenMDAO reverse mode | 7.54117 |
| coupled dcl/dα, OpenMDAO forward mode | 7.54117 |
| coupled dcl/dα, central difference of re-converged coupled analyses | 7.54117 |
| relative difference, reverse mode vs finite difference | 1.2307e-07 |
| dcl/dα of the rigid aerofoil | 7.51815 |
| largest deflection [chords] | 0.00555541 |

Provenance: gyre 0.4.0; revision `39979fcf90` (commit `39979fcf902ad6116eb550b881b4278d4599c8e2`); 2026-11-12T11:40:21-0500; device cpu; quick suite. **Revision check: Same code as this build.** Produced by 39979fcf902ad6116eb550b881b4278d4599c8e2. Every commit between it and the commit this site was built from (ceb1f4b0bed41de53e586df10858079f5a2bcdd8) changes only documentation text, evidence records or bookkeeping: the code is the same. Full record: [discipline_contract.json](../data/verification/discipline_contract.json).

The table is from the [discipline contract](../verification/discipline-contract.md) study:
OpenMDAO's own `check_partials` of both components, totals through the implicit component
against the discipline's adjoint totals, and the coupled derivative of a flow–structure
cycle built on the group (`examples/openmdao_wrapper.py`) against a finite difference of
re-converged coupled analyses.

<!-- end level 2 -->

<!-- begin level 3 (Mechanics) -->
**[L3 Mechanics] Verified behaviour**

`tests/test_integrations.py::test_openmdao_component_totals` checks that the framework's
reverse-mode totals equal Gyre's own adjoint totals (including the shape sensitivity field),
that forward mode agrees with reverse mode, and that a finite difference through the
framework's model evaluation matches.
`tests/test_integrations.py::test_openmdao_component_solves_again_after_a_failed_point` checks
that a point that does not converge raises `AnalysisError` and that a return to the previous,
converged inputs solves again instead of reusing the cached point. For the implicit form,
`test_openmdao_implicit_component_passes_check_partials`,
`test_openmdao_implicit_group_totals_match_the_reduced_space` and
`test_openmdao_coupled_adjoint_through_the_implicit_component` check the partial products,
the totals mode for mode against the discipline's own, and the coupled adjoint. The tests skip themselves when OpenMDAO is not installed;
`make bootstrap` and the hosted CI workflow install `all`, so it runs there.

<!-- end level 3 -->

<!-- begin level 3 (Mechanics) -->
**[L3 Mechanics] Coupling in a framework**

With `loads=True` and `shape=True` the component exposes the two coupling fields. Connected to
a structural component in an OpenMDAO group with a nonlinear block solver, the framework's
linear solver assembles the coupled adjoint from the same `compute_jacvec_product` calls —
the framework-owned version of the [hand-written coupled adjoint](aerostructural-coupling.md).

<!-- end level 3 -->
