---
title: Discipline contract
summary: The interface a multidisciplinary framework integrates against is itself verified. Forward and reverse products are exact transposes, the linear solves invert them, the products agree with finite differences at the solution and at a point the framework names, a stalled linear solve is never silent, and the OpenMDAO implicit component passes OpenMDAO's own derivative checks.
section: verification
order: 8
level: 2
study: discipline_contract
keywords: [MDO, discipline, apply_linear, solve_linear, dot product, OpenMDAO, check_partials, coupled adjoint]
---
<!-- gyre docs: expanded Markdown of verification/discipline-contract.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. -->

A design framework never sees the flow solver. It sees a handful of operations
([MDO integration](../guide/mdo-integration.md)): evaluate the residual, apply its
linearisation forwards and backwards, solve with it, and return outputs. If one of them is
slightly inconsistent with the others, the framework's coupled derivatives are wrong and no
test of the flow solver alone would show it. This study checks the operations against each
other.

**Verification status: Discipline contract for MDO integration** (study `discipline_contract`): **PASSED**, 15 of 15 checks passed, 249.9 s.

The interface a multidisciplinary framework integrates against is checked on a small inviscid aerofoil case (NACA 0012, M = 0.5, α = 2°, 64 elements of degree 2). (i) The partial-derivative products of residual and outputs (apply_linear) in forward and reverse mode must be transposes of each other, with respect to the state and to all inputs including the surface displacement field, both at the discipline's solution and at a point named by the caller that is not a solution. (ii) The forward products must agree with central finite differences of the residual (apply_nonlinear) and of the outputs, taken along smooth directions (the numerical flux is only piecewise smooth, so a rough direction would test the finite difference and not the derivative). (iii) solve_linear must invert apply_linear in both modes, and the two modes must be transposes of each other. (iv) The reduced (total) products tangent_product and adjoint_product must be transposes of each other. (v) A linear solve that cannot reach its tolerance must raise, or be flagged when the caller asks for that; and adjoint and direct totals, which solve different linear systems of the same linearisation, must approach each other as the tolerance of those solves is tightened. (vi) Wrapped as an OpenMDAO implicit component, the discipline must pass OpenMDAO's own check of partial derivatives, reproduce the discipline's total derivatives, and, coupled to a linear structure in a block Gauss–Seidel cycle, give a coupled total derivative that agrees with a finite difference of re-converged coupled analyses.

| Check | Value | Expected | Result |
|---|---|---|---|
| flow solve converged | yes | true | pass |
| apply_linear forward vs reverse: largest relative dot-product defect over both points | 2.5091e-16 | < 1e-11 (round-off) | pass |
| solve_linear forward vs reverse: largest relative dot-product defect | 4.0510e-12 | < 1e-8 (two Krylov solves to 1e-11) | pass |
| solve_linear inverts apply_linear in both modes: largest relative residual, recomputed with apply_linear | 9.5505e-12 | < 1e-9 (solves to 1e-11; preconditioned residual) | pass |
| partials (residual, cl, cd, cm, f_surface) with respect to state and inputs vs central differences: largest relative difference | 1.1538e-08 | < 1e-6 (the bound of the linearization study for JVP vs central differences; smooth directions) | pass |
| total products, tangent_product vs adjoint_product: relative dot-product defect | 6.6374e-10 | < 1e-7 (two linear solves to 1e-10) | pass |
| a linear solve that misses its tolerance raises LinearSolveError and reports the reduction it achieved | 1.6484e-14 | raised, with 0 < reduction < 1e-10 | pass |
| with on_failure='flag' the same solve returns with info.converged | no | false | pass |
| adjoint and direct totals meet as the linear tolerance tightens: largest relative difference at 1e-8, 1e-10, 1e-12 | 2.9e-05 / 1.3e-06 / 5.8e-09 | decreasing, below 1e-7 at 1e-12 | pass |
| OpenMDAO is installed (extra 'mdo') | yes | true | pass |
| OpenMDAO check_partials of the implicit flow component and the functionals component (forward and reverse vs central differences): largest relative error | 5.6471e-09 | < 1e-6 | pass |
| OpenMDAO totals through the implicit component vs the discipline's adjoint totals (rel., largest) | 4.1434e-16 | < 1e-6 (linear solves to 1e-10) | pass |
| coupled flow–structure adjoint through the implicit component vs finite difference (rel.) | 1.2307e-07 | < 5e-6 (finite-difference step 1e-4) | pass |
| coupled totals, forward vs reverse mode (rel.) | 9.7061e-10 | < 1e-6 (linear block Gauss–Seidel to 1e-9) | pass |
| the coupling is active: largest deflection [chords] | 0.00555541 | > 1e-5 | pass |

Run notes: OpenMDAO 3.45.2. The coupled derivative differs from the rigid one by +0.31 %. Measured in this run: largest dot-product defect of apply_linear 2.5e-16, of solve_linear 4.1e-12, of the total products 6.6e-10; largest difference between partials and central differences 1.2e-08; the solve with an unreachable tolerance stopped at a reduction of 1.6e-14.

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).

**Table: Linear operators of the discipline** (Discipline contract for MDO integration)

| point | check | left side / value | right side / reference | relative difference |
|---|---|---|---|---|
| solution (the discipline's point) | apply_linear: forward vs reverse (dot product) | 1812.39 | 1812.39 | 2.5091e-16 |
| solution (the discipline's point) | residual partials vs central difference (step 1e-06) | 44.4916 | 44.4916 | 1.3197e-09 |
| solution (the discipline's point) | partials of cl vs central difference (step 1e-06) | 0.0919221 | 0.0919221 | 2.7630e-09 |
| solution (the discipline's point) | partials of cd vs central difference (step 1e-06) | 0.191244 | 0.191244 | 1.2509e-10 |
| solution (the discipline's point) | partials of cm vs central difference (step 1e-06) | 0.0144446 | 0.0144446 | 8.9697e-09 |
| solution (the discipline's point) | partials of f_surface vs central difference (step 1e-06) | 0.516143 | 0.516143 | 5.4903e-10 |
| solution (the discipline's point) | solve_linear fwd: \|A x − b\| / \|b\| by apply_linear | 35 | 9.5501e-12 | 9.5505e-12 |
| solution (the discipline's point) | solve_linear rev: \|Aᵀ y − c\| / \|c\| by apply_linear | 35 | 6.3456e-12 | 6.3456e-12 |
| solution (the discipline's point) | solve_linear: forward vs reverse (dot product) | 7684.66 | 7684.66 | 1.3401e-12 |
| named point (perturbed state and inputs) | apply_linear: forward vs reverse (dot product) | 1883.39 | 1883.39 | 0 |
| named point (perturbed state and inputs) | residual partials vs central difference (step 1e-06) | 50.8941 | 50.8941 | 1.0737e-09 |
| named point (perturbed state and inputs) | partials of cl vs central difference (step 1e-06) | 0.0943423 | 0.0943423 | 4.3592e-09 |
| named point (perturbed state and inputs) | partials of cd vs central difference (step 1e-06) | 0.190731 | 0.190731 | 1.8486e-10 |
| named point (perturbed state and inputs) | partials of cm vs central difference (step 1e-06) | 0.0146643 | 0.0146643 | 1.1538e-08 |
| named point (perturbed state and inputs) | partials of f_surface vs central difference (step 1e-06) | 0.517176 | 0.517176 | 2.2628e-10 |
| named point (perturbed state and inputs) | solve_linear fwd: \|A x − b\| / \|b\| by apply_linear | 35 | 6.8446e-12 | 6.8460e-12 |
| named point (perturbed state and inputs) | solve_linear rev: \|Aᵀ y − c\| / \|c\| by apply_linear | 36 | 3.7119e-12 | 3.7120e-12 |
| named point (perturbed state and inputs) | solve_linear: forward vs reverse (dot product) | 7763.84 | 7763.84 | 4.0510e-12 |
| solution (the discipline's point) | tangent_product vs adjoint_product (dot product) | -4.14465 | -4.14465 | 6.6374e-10 |

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).

<!-- begin level 2 (Practice) -->
**[L2 Practice] What each group of rows establishes**

* **Forward versus reverse.** For random directions the product of the forward derivative
  with a reverse seed equals the product of the reverse derivative with the forward
  direction, to round-off. This holds with respect to the state and to every input,
  including the surface displacement field and the load field, so a framework may use either
  mode, or mix them, and get the same coupled derivative.
* **Two points.** Every check is made at the discipline's own converged solution and at a
  point the caller names that is *not* a solution (a perturbed state and other inputs). A
  framework that owns the state, as in a full-space or a block-coupled solve, linearises
  about its own iterate; the discipline must not assume that iterate is converged.
* **Against finite differences.** The forward products agree with central differences of the
  residual and of the outputs along smooth directions. The numerical flux is only piecewise
  smooth, so a rough direction would measure the finite difference, not the derivative.
* **The linear solves.** The solution returned by `solve_linear` is put back through
  `apply_linear`: the residual of that test is the tolerance asked for, in both modes, and
  the two modes are transposes of each other.

<!-- end level 2 -->

**Table: Adjoint and direct totals against the tolerance of their linear solves** (Discipline contract for MDO integration)

| linear tolerance | derivative | adjoint | direct | relative difference |
|---|---|---|---|---|
| 1.0000e-08 | d cl / d alpha | 7.51815 | 7.51815 | 5.2558e-08 |
| 1.0000e-08 | d cl / d mach | 0.186709 | 0.186704 | 2.8636e-05 |
| 1.0000e-10 | d cl / d alpha | 7.51815 | 7.51815 | 1.4219e-09 |
| 1.0000e-10 | d cl / d mach | 0.186709 | 0.186709 | 1.3386e-06 |
| 1.0000e-12 | d cl / d alpha | 7.51815 | 7.51815 | 1.5087e-11 |
| 1.0000e-12 | d cl / d mach | 0.186709 | 0.186709 | 5.7951e-09 |

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).

<!-- begin level 3 (Mechanics) -->
**[L3 Mechanics] What the linear tolerance buys, and why the two modes differ**

The adjoint and the direct mode solve different linear systems of the same linearisation.
The error each leaves in a total derivative is the residual of its solve multiplied by the
solution of the *other* system: the sensitivity of the state for the adjoint mode, the
adjoint times the input derivative of the residual for the direct mode. These factors
differ by orders of magnitude between inputs, so at one and the same tolerance the modes
reach different accuracies; the table shows them approaching each other as the tolerance
tightens. A framework that compares modes should do so at a tolerance tight enough for the
comparison it wants, and a failure to reach a tolerance is reported, never absorbed:
`LinearSolveError` by default, a flag in the solve record when the caller asks for that.

<!-- end level 3 -->

**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).

<!-- begin level 2 (Practice) -->
**[L2 Practice] OpenMDAO**

The same discipline wrapped as an OpenMDAO implicit component
([adapter](../examples/openmdao.md)) passes OpenMDAO's `check_partials` in forward and
reverse mode, reproduces the discipline's own total derivatives through OpenMDAO's linear
solver, and, coupled to a linear structural model in a block Gauss–Seidel cycle, returns a
coupled total derivative that agrees with a finite difference of re-converged coupled
analyses. The record names the OpenMDAO version it ran with.

<!-- end level 2 -->
