---
title: Coupled aero-structural adjoint
summary: A flow coupled to an elastic skin, with the coupled derivative assembled outside Gyre from loads, displacements and reverse total products — the pattern an MDO framework uses, in sixty lines.
section: examples
order: 8
level: 2
study: example_coupling
audience: [MDO integrators]
keywords: [coupled adjoint, aeroelastic, loads, displacements, MDO, example]
---
<!-- gyre docs: expanded Markdown of examples/aerostructural-coupling.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. -->

This example exists to show an *interface*, not a structural model. The structure is a toy —
a smooth linear compliance mapping surface loads to surface displacements — but the coupling is
real: loads deform the surface, the deformed surface changes the loads.

**Verification status: Example: coupled aero-structural adjoint** (study `example_coupling`): **PASSED**, 2 of 2 checks passed, 823 s.

The flow is coupled to a linear elastic skin (loads in, displacements out) by a fixed-point iteration written outside Gyre. The coupled derivative of lift with respect to angle of attack is assembled from Gyre's reduced reverse products and compared with a finite difference of the fully re-converged coupled system.

| Check | Value | Expected | Result |
|---|---|---|---|
| coupled adjoint vs finite difference (rel.) | 9.6349e-08 | < 1e-5 | pass |
| coupling is active: max deflection [chords] | 0.00555919 | > 1e-5 | pass |

Provenance: gyre 0.4.0; revision `39979fcf90` (commit `39979fcf902ad6116eb550b881b4278d4599c8e2`); 2026-11-12T12:02:29-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: [example_coupling.json](../data/verification/example_coupling.json).

**Table: Coupled derivative** (Example: coupled aero-structural adjoint)

| cl (flexible) | max deflection [c] | dcl/dα coupled adjoint | dcl/dα finite difference | relative difference | dcl/dα frozen shape | aeroelastic effect [%] |
|---|---|---|---|---|---|---|
| 0.279698 | 0.00555919 | 7.76502 | 7.76502 | 9.6349e-08 | 7.71574 | 0.638621 |

Provenance: gyre 0.4.0; revision `39979fcf90` (commit `39979fcf902ad6116eb550b881b4278d4599c8e2`); 2026-11-12T12:02:29-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: [example_coupling.json](../data/verification/example_coupling.json).

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

The derivative of lift with respect to angle of attack for the *coupled* system differs from
the frozen-shape value by the aeroelastic effect in the last column. The coupled adjoint
reproduces a finite difference of the fully re-converged coupled system to the relative
difference shown — using only operations any MDO framework can call.

<!-- end level 2 -->

## The coupled analysis

```python
def coupled_solve(flow, C, tol=1e-9, relax=1.0, max_iter=40):
    d = np.asarray(flow.d_surface).copy()
    for it in range(max_iter):
        flow.set_inputs(x_surface=d)                 # displacements in
        converge(flow)                               # warm-started flow solve; raises if it does not converge
        f = flow.outputs(("f_surface",))["f_surface"]    # consistent nodal loads out
        d_new = (C @ f.reshape(-1)).reshape(d.shape)     # the "structure"
        ...
```

`converge` is a small helper of the example: it calls `flow.solve()` and raises if the history
says the solve did not converge. A failed solve leaves the discipline's last converged state
in place, the flow of the previous shape, so loads read after it would belong to another
design.

## The coupled adjoint

```python
tot = flow.totals(of=("cl",), wrt=("alpha", "x_surface"))["derivatives"]["cl"]
dJ_dd = tot["x_surface"].reshape(-1)
lam = dJ_dd.copy()
for _ in range(max_iter):                            # transposed fixed-point iteration
    seed = (C.T @ lam).reshape(shape)
    back = flow.adjoint_product({"f_surface": seed}, wrt=("x_surface",))["derivatives"]
    lam_new = dJ_dd + back["x_surface"].reshape(-1)
    ...
cross = flow.adjoint_product({"f_surface": (C.T @ lam).reshape(shape)}, wrt=("alpha",))
dJ_dalpha = tot["alpha"] + cross["derivatives"]["alpha"]
```

<!-- begin level 3 (Mechanics) -->
**[L3 Mechanics] What Gyre contributed**

Three things, all part of the [discipline contract](../guide/mdo-integration.md):

1. `f_surface`, loads that are conservative and work-conjugate to `x_surface`;
2. `totals(…, wrt=["alpha", "x_surface"])`, the reduced gradient of a scalar;
3. `adjoint_product({"f_surface": seed}, …)`, the reverse product
   $(\mathrm d\mathbf f/\mathrm d\mathbf x)^{\mathsf T}\mathbf{seed}$ — one flow-adjoint solve regardless of
   the number of load components.

The loop, the structure and the choice of solver for the coupled system live outside. The
derivation is in [Adjoints and sensitivities](../theory/adjoint-and-sensitivities.md); the full
script is `examples/aerostructural_coupling.py`, and the same logic is exercised by
`tests/test_integrations.py`.

<!-- end level 3 -->

<!-- begin level 3 (Mechanics) -->
**[L3 Mechanics] Over the network**

The same calls exist on a remote session: `flow.adjoint({"f_surface": seed}, wrt=[...])` on a
`RemoteDiscipline`, or `POST /api/v1/sessions/{id}/adjoint`. A structural code on another
tailnet machine can therefore own the coupled iteration.

<!-- end level 3 -->
