Integrating with an MDO framework
In one minuteGyre presents itself as an implicit discipline — inputs, a state, a residual, outputs — with exact matrix-free derivative products. This page maps that contract onto the concepts MDO frameworks use and shows the three ways to connect.
Gyre does not optimize. It supplies what an optimizer’s framework needs from the aerodynamics discipline and leaves problem formulation, design variables, geometry parameterisation and multidisciplinary solvers to the framework.
The contract#
| Framework concept | Gyre |
|---|---|
| inputs | flow parameters mach, alpha (rad), beta (rad, 3-D), re, with the turbulence model chi_inf and prt, … and the field x_surface: displacements of the wall geometry nodes, shape (n_surface_nodes, dim) |
| state | the DG solution , shape disc.state_shape. In reduced-space use it stays inside Gyre; at residual level the framework owns it (set_state, or state= on every call below) |
| residual (apply nonlinear) | apply_nonlinear(inputs=None, state=None): the weak-form residual at the discipline’s point or at the one named |
| outputs | outputs(names, state=None): scalars cl, cd, cm, cx, cy, heat, entropy_error, … (in 3-D also cz, cs, cmx, cmy, cmz) and the field f_surface: consistent nodal loads on the same surface nodes |
| solve nonlinear | solve(), or solve_nonlinear(inputs) to set inputs and solve — warm-started from the last converged state; a solve that fails is reported in the returned history and does not replace that state (revert_inputs() goes back to the point it belongs to) |
| linearize | linearize(state=None, inputs=None) |
| apply linear (fwd / rev) | apply_linear("fwd" \| "rev", …), which is jvp(d_inputs, d_state) / vjp(residual_bar, outputs_bar): partial products with respect to state and inputs |
| solve linear (fwd / rev) | solve_linear(rhs, mode="fwd" \| "rev"); a solve that misses its tolerance raises LinearSolveError (or is flagged, with on_failure="flag") |
| totals of scalar outputs | totals(of, wrt, mode="auto" \| "adjoint" \| "direct") |
| reduced products for coupling | tangent_product(d_inputs) / adjoint_product(seeds) |
| save and restore a point | checkpoint() / restore(ckpt): inputs and state, as arrays without pickle |
Every residual-level call takes state= and inputs=: without them it acts at the
discipline’s own point, with them at the point the caller names, without changing the
discipline. A framework-owned Newton or coupled solver can therefore evaluate, linearise and
solve about its own iterate. The operations are checked against each other in the
discipline contract study.
describe() returns this interface — names, shapes, current values — as data, so adapters can be
generated rather than written.
1. In-process (Python). Construct a case and use its discipline directly:
from gyre.cases import Case, preset
flow = Case(preset("naca0012_euler")).discipline
flow.set_inputs(alpha=0.04, mach=0.55)
flow.solve()
out = flow.outputs(("cl", "cd", "f_surface"))
grad = flow.totals(of=("cl", "cd"), wrt=("alpha", "x_surface"))["derivatives"]
2. Remote session (HTTP). The framework runs on another machine; a warm solver process lives in a Gyre deployment:
from gyre.client import GyreClient
with GyreClient("http://gyre-host:8600").session("naca0012_euler") as flow:
out = flow.evaluate(alpha_deg=2.5)["outputs"]
grad = flow.gradients(of=["cl"], wrt=["alpha_deg", "x_surface"])
back = flow.adjoint({"f_surface": seed}, wrt=["x_surface", "alpha"])
The calls a driver makes on such a session — at fixed design points, without an optimizer —
are shown by examples/remote_session.py, described under
A remote session called at fixed design points.
RemoteDiscipline exposes the reduced-space contract under its own names: evaluate(**inputs)
(set inputs, solve, outputs), gradients(of, wrt) (the derivatives of totals),
adjoint(seeds, wrt) and tangent(d_inputs, outputs) (the reverse and forward total
products), residual(**inputs) (norms of the residual of the session’s state), revert()
after a failed evaluation, get_state / set_state, checkpoint() / restore() (a
checkpoint also rebuilds a session that was closed or reaped:
GyreClient.restore_session), plus describe, surface_coordinates, error_estimate and
distributions. The partial products jvp, vjp and solve_linear are available
in-process only. Fields travel as JSON arrays.
3. Framework adapter. gyre.integrations.openmdao wraps a discipline for OpenMDAO in
two forms (see the OpenMDAO example): GyreFlowComp, a
matrix-free ExplicitComponent (reduced space: the framework sees inputs and outputs), and
GyreFlowGroup, an ImplicitComponent whose output is the flow state plus a functionals
component (residual level: the framework sees the state, the residual and its linear
solves). Both turn a flow solve that fails and a linear solve that stalls into an OpenMDAO
analysis error.
- Non-dimensional free stream: ; coefficients are
with in 2-D. Spanwise-extruded 3-D cases use
$S_\text{ref} = $ span, so their coefficients, and the error estimates of those coefficients,
are per unit span.
reference.area,reference.lengthandreference.moment_centerof the case set other reference quantities, which a case on a mesh file usually needs. alphais in radians. Through the servicealpha_degis accepted as a session input (hence byRemoteDiscipline.evaluate) and in thewrtof sessiongradientsandadjointand of jobgradients; the derivative is then per degree. The in-processFlowDisciplineknows onlyalpha.x_surfaceis a displacement relative to the baseline surface nodes returned bysurface_coordinates();f_surfaceis a force per node (not a traction), already integrated, and sums to the total force:2 * f_surface.sum(0) == (cx, cy).- Loads of components.
reference.componentsof the case names sets of wall groups, for example{wing: [wing], tail: [htail, vtail]}on a mesh with those groups, and the discipline then returns every coefficient per component as well (cd_wing,cmy_tail, …) from the same solve; each can be an objective or a constraint with its own adjoint.reference.groupsinstead changes what the plain coefficients cover (default: all walls).f_surfaceandx_surfacealways cover every wall, so the identity above holds for the coefficients of all walls only. - Derivatives with respect to
x_surfacehave the same shape asx_surface. - On a periodic mesh (spanwise-extruded cases) the surface nodes on the two periodic planes
are one topological node. Both rows remain inputs, but the pair moves by the mean of the two
prescribed displacements, and the derivative with respect to
x_surfaceis shared equally between them. Give both rows the same value.
Two styles of multidisciplinary derivative computation exist, and Gyre supports both.
Reduced space (the flow state is eliminated inside the discipline). The framework sees an
explicit map inputs → outputs with total derivative products. Each reverse product costs one
flow-adjoint solve; this is what adjoint_product, tangent_product, the session endpoint
/adjoint and the OpenMDAO adapter provide. It is the simplest correct integration and works
across a network boundary because the state never leaves the solver.
Full space (the framework owns the coupled linear solve). The framework treats the flow as an
implicit component and needs the residual, partial products of the residual and outputs, and
a preconditioned linear solve, all about a state it owns: apply_nonlinear, apply_linear
(jvp, vjp) and solve_linear with state= and inputs=. This is the MPhys-style
contract; it is available in-process, and GyreFlowGroup is its OpenMDAO form.
For a structure coupled to the flow and an objective :
tot = flow.totals(of=("cl",), wrt=("alpha", "x_surface"))["derivatives"]["cl"]
lam = tot["x_surface"].ravel()
for _ in range(max_iter): # transposed block Gauss–Seidel
seed = (C.T @ lam).reshape(shape)
back = flow.adjoint_product({"f_surface": seed}, wrt=("x_surface",))["derivatives"]
lam = tot["x_surface"].ravel() + back["x_surface"].ravel()
cross = flow.adjoint_product({"f_surface": (C.T @ lam).reshape(shape)}, wrt=("alpha",))
dJ_dalpha = tot["alpha"] + cross["derivatives"]["alpha"]
The aero-structural example runs exactly this and verifies it against finite differences of the re-converged coupled system. The derivation is in Adjoints and sensitivities.
Gyre differentiates with respect to surface-node positions because that is the one interface every geometry engine can target. If your design variables move the surface by — free-form deformation, CAD parameters, Hicks–Henne bumps — then
one matrix–vector product with the field Gyre returns. Volume-mesh deformation is handled inside Gyre and is already part of .
- Use sessions. The first evaluation of a mesh/order compiles kernels (tens of seconds); later evaluations reuse them and warm-start from the previous flow.
- Converge tightly when you need gradients. The adjoint is exact for the converged discrete problem; a loosely converged flow gives a gradient of something slightly different. Presets converge ten orders of magnitude by default.
- One adjoint per output. Requesting
of=["cl","cd","cm"]costs three transposed solves; request only what the optimizer uses, or seed a linear combination withadjoint_product. - On wall-resolved meshes, precondition the linear solves along the wall-normal lines.
Every output of every design point costs one transposed solve to a tight tolerance, and
solver.smoother: lineis made for such solves (how, what it gained on the flat plate). The flow solve itself need not get faster with it, and no study measures how a design loop divides its time between flow solves and transposed solves: that depends on the number of outputs and on the step between design points. The smoother belongs to the case, so a session keeps it for every evaluation. - Check the mesh for the objective, not in general.
error_estimatetells you whether a change in the objective between two designs exceeds its discretization error. - Failures are data. A non-converged evaluation returns
converged: falsewith the reason and the outputs and residual of the iterate it ended at. The failed iterate is not accepted: the state stays the last converged solution and the next evaluation warm-starts from it. In Python,flow.Uis unchanged,flow.historydescribes the failed attempt andflow.failed_stateholds its last iterate. Repeating the same evaluation repeats the same warm start, so take a smaller step, or in Python start elsewhere withflow.solve(U0=...). - No derivatives of a failed point. After a solve that did not converge the kept state
belongs to the previous inputs, so
totals,tangent_product,adjoint_product,jvp,vjp,solve_linearandlinearizeraiseRuntimeErroruntil a solve converges (or the caller declares a state withset_state). A session answersgradients,adjoint,error_estimate,distributionsand the field request with 422RuntimeErrorin the same situation, andGET /sessions/{id}/statereportsconverged: false.outputs()raises as well, because the kept state belongs to the previous inputs: name a state to get numbers anyway (outputs(state=flow.failed_state)is the failed iterate), or go back to the last converged point withrevert_inputs()(revert()on a session). - Choose the degree once, then keep it.
adapton a job,--target-erroron the command line orgyre.adapt.adaptive_orderselect the polynomial degree from an error tolerance on one output (Gradients and error estimates). A session keeps the degree of its case.
All partial derivatives are obtained by automatic differentiation of the discrete operators, so there is no approximation in the Jacobian, the boundary conditions, the metric terms or the mesh deformation. Total derivatives are exact up to the tolerance of the linear solve (default relative ) and the convergence of the nonlinear solve. They are derivatives of the discrete outputs — the function the optimizer actually evaluates — and therefore consistent with line searches and convergence checks at any resolution.