---
title: Python API
summary: Use Gyre as a library — build a case, drive the discipline, access the discretization and its differentiable kernels directly.
section: guide
order: 9
level: 2
keywords: [Python, API, FlowDiscipline, Discretization, library]
---
<!-- gyre docs: expanded Markdown of guide/python-api.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. -->

## Cases and the discipline

```python
from gyre.cases import Case, preset, load_spec

case = Case(preset("naca0012_euler", **{"flow.mach": 0.55}))   # or load_spec("case.yaml")
flow = case.discipline                                         # FlowDiscipline

hist = flow.solve()                         # converged? hist.converged, hist.steps
out = flow.outputs()                        # scalars and "f_surface"
flow.set_inputs(alpha=0.05)                 # radians; keeps the state for a warm start
flow.solve()
tot = flow.totals(of=("cl", "cd"), wrt=("alpha", "mach", "x_surface"))
```

`examples/quickstart.py` is a runnable version of this block that also prints an error
estimate (`.venv/bin/python examples/quickstart.py`). The in-process discipline takes the
angle of attack as `alpha` in **radians** and knows no `alpha_deg`; what each output means is
defined under [Outputs and functionals](../theory/outputs-and-functionals.md).

<!-- begin level 2 (Practice) -->
**[L2 Practice] `FlowDiscipline` at a glance**

| Method | Returns |
|---|---|
| `describe()` | inputs, outputs, shapes, discretization summary |
| `set_inputs(**inputs)` | — (flow parameters and/or `x_surface`) |
| `solve(U0=None, callback=None)` | `SolveHistory`; on failure `U` keeps the last converged state, and `history` and `failed_state` describe the failed attempt |
| `set_state(U, converged=True)` | — (restore a saved solution, or declare an initial guess with `converged=False`) |
| `outputs(names=None, state=None)` | dict of floats / arrays, at the current state or at `state`, e.g. `failed_state` |
| `totals(of, wrt, mode)` | `{"derivatives": {of: {wrt: value}}, "info": …}` |
| `tangent_product(d_inputs, outputs)` | forward total product |
| `adjoint_product(seeds, wrt)` | reverse total product |
| `jvp(d_inputs, d_state)`, `vjp(residual_bar, outputs_bar)` | partial products |
| `solve_linear(rhs, mode)` | forward / transposed linear solve |
| `surface_coordinates()` | baseline surface nodes |
| `residual_norm()` | current nonlinear residual |
| `require_solution(what)` | — (raises `RuntimeError` unless the state belongs to the current inputs) |

<!-- end level 2 -->

<!-- begin level 2 (Practice) -->
**[L2 Practice] After a solve that does not converge**

`solve()` reports failure in the history (`hist.converged`, `hist.reason`); it does not raise.
A failed solve never replaces a converged state: `flow.U` remains the last converged solution,
which belongs to the inputs it was converged for, and is the warm start of the next solve.
`flow.failed_state` holds the last iterate of the failed attempt (`None` after a converged
solve). While nothing has converged yet, the iterate also becomes `flow.U`.

Until a solve converges again, `linearize`, `solve_linear`, `jvp`, `vjp`, `tangent_product`,
`adjoint_product` and `totals` raise `RuntimeError`. Ways on:

```python
hist = flow.solve()
if not hist.converged:
    flow.outputs(("cl",), state=flow.failed_state)   # where the failed attempt ended
    flow.solve(U0=flow.failed_state)                 # continue from there, or
    flow.set_inputs(alpha=0.03); flow.solve()        # take a smaller step from the kept state
```

A plain repeat of `flow.solve()` restarts from the same kept state and fails the same way.
`flow.set_state(flow.failed_state, converged=False)` makes the failed iterate the state and
lifts the refusal: the caller then takes responsibility for differentiating it.
`gyre.error.estimate_error` reads the state directly and does not make this check; call
`flow.require_solution("the error estimate")` before it.

<!-- end level 2 -->

## Meshes from files

```python
from gyre import meshio
from gyre.cases import Case, CaseSpec

info = meshio.inspect_mesh("examples/meshes/cylinder_channel_p3.msh")   # reads and validates
info["boundaries"]            # faces per boundary group: cylinder, inflow, outflow, walls

spec = CaseSpec.model_validate({
    "name": "cylinder_channel",
    "mesh": {"generator": "file",
             "params": {"path": "examples/meshes/cylinder_channel_p3.msh", "scale": 1.0}},
    "flow": {"mach": 0.2, "reynolds": 40.0},
    "discretization": {"order": 2, "boundaries": {
        "cylinder": "noslip_adiabatic", "inflow": "farfield",
        "outflow": "farfield", "walls": "slip_wall"}},
    "reference": {"area": 1.0, "length": 1.0, "moment_center": [0.0, 0.0]},
})
case = Case(spec)             # raises ValueError if a boundary group has no condition
```

<!-- begin level 2 (Practice) -->
**[L2 Practice] `gyre.meshio` and the case fields for mesh files**

| Object | Purpose |
|---|---|
| `meshio.read_mesh(path, scale=1.0)` | read `.msh` or `.npz` (by extension) into a `Mesh`; reading validates |
| `meshio.read_gmsh(path, scale=1.0)` | Gmsh MSH 2.2 or 4.1, ASCII: quadrilaterals of degree 1 to 5, hexahedra of degree 1 to 4 |
| `meshio.write_gmsh(mesh, path, volume_name="fluid")` | write MSH 2.2 (ASCII), periodic node pairs included |
| `meshio.save_mesh(mesh, path)`, `meshio.load_mesh(path, scale=1.0)` | Gyre NPZ format: the arrays of a `Mesh`, at any geometry degree |
| `meshio.inspect_mesh(path, scale=1.0, lines=False)` | summary dict: `dim`, `elements`, `geometry_order`, `nodes`, `boundaries`, quality numbers, `bounding_box`, `sha256`, `bytes`; with `lines=True` also `lines`, as `gyre mesh info` prints it |
| `meshio.line_summary(mesh)` | the [lines of the line smoother](meshes.md#mesh-lines) on a mesh and the anisotropy of its cells |
| `meshio.validate_mesh(mesh)` | quality summary; raises for inverted elements and unmatched, untagged faces |
| `meshio.file_digest(path)` | SHA-256 of the file's contents (hex); the value `CaseSpec.digest()` uses for a mesh file |
| `meshio.resolve_mesh_path(name)` | the file a case names in `mesh.params.path` |
| `meshio.MeshFormatError` | a `ValueError`: the file is not a mesh Gyre can use; the message says why |
| `CaseSpec.mesh_file()` | path of the mesh file of a `generator: file` case, `None` for generators |
| `CaseSpec.reference` | `area` (default 1 in 2-D, the span of an extruded generator mesh in 3-D), `length` (default 1) and `moment_center` (default `[0.25, 0, 0]`) of the force and moment coefficients; `groups`, the wall groups they are integrated over (default: all walls), and `components`, named sets of wall groups whose coefficients are additional outputs `cd_<name>`, … (`FlowDiscipline(..., groups=(...), components={...})` when a discipline is built by hand) |

`mesh.params` of a file case accepts `path` and `scale` only. A relative `path` that does not
exist as given is looked up in the directory named by the environment variable
`GYRE_MESH_DIR`; with `GYRE_MESH_RESTRICT=1`, which the service sets for itself and its
workers, only bare file names inside that directory are accepted. `CaseSpec.digest()` hashes
the contents of the file, not its name. Reading an `.npz` file (`load_mesh`, `read_mesh`,
`inspect_mesh`) raises `MeshFormatError` when its arrays take more than 1 GiB uncompressed;
the environment variable `GYRE_MESH_MAX_EXPANDED_BYTES` (a positive integer, in bytes) sets
another limit, see [Meshes](meshes.md). The boundary groups of a mesh file have no default
conditions: `discretization.boundaries` must name a condition for every group and no group the
mesh does not have.

<!-- end level 2 -->

## Order selection

```python
from gyre.adapt import adaptive_order
from gyre.runner import run_case

res = adaptive_order(spec, output="cd", tolerance=1e-3, max_order=5, min_order=None)
result = run_case(spec, adapt={"output": "cd", "tolerance": 1e-3, "max_order": 5})
```

<!-- begin level 2 (Practice) -->
**[L2 Practice] `adaptive_order` and `run_case(adapt=...)`**

`adaptive_order(spec, output, tolerance, max_order=5, min_order=None, callback=None)` raises
the polynomial degree from `spec.discretization.order` (or `min_order`) until the estimated
error of `output` is at or below `tolerance`, and returns `output`, `tolerance`, `met`, `order`,
`value`, `corrected`, `error`, `levels` and `case`, the solved `Case` of the final degree. The
arguments are checked before any flow is solved: a tolerance that is not a positive finite
number, a degree that is not an integer, a starting degree below 1, a `max_order` below the
starting degree or above 8 raise `ValueError`; an output that is not a scalar output of the
case raises `KeyError`. Per degree the callback receives a `case` event, the `iteration`
events of its solve (each with `order`), `{"event": "adapt_retry", ...}` when the degree is
tried again from another start, and one `{"event": "adapt", ...}` record that closes it.

`run_case(spec, adapt={...})` does the same inside the complete result record: the result is
that of the final degree and gains `adaptation` (the entries above without `case`);
`timing["solve"]` is the wall time of the whole selection and `timing["setup"]` is 0.
`max_order` may be omitted (5). Keys other than `output`, `tolerance` and `max_order`, a
missing `output` or `tolerance`, or `adapt` together with `case=` raise `ValueError`. See
[Gradients and error estimates](gradients.md#letting-the-error-estimate-choose-the-order).

<!-- end level 2 -->

## One level down: the discretization

```python
import jax, jax.numpy as jnp
from gyre import meshgen, physics as ph
from gyre.dg import Discretization

mesh = meshgen.airfoil_omesh(16, 4, pg=3)       # 64 curved elements
disc = Discretization(mesh, p=2, phys=ph.Compressible(dim=2),
                      bcs={"wall": "slip_wall", "farfield": "farfield"})
th = ph.flow_params(mach=0.5, alpha_deg=2.0)
geom = disc.geometry()                          # differentiable in the node coordinates
U = disc.uniform_state(th)
Rw = disc.residual(U, geom, th, weak=True)      # (4, 64, 3, 3)

# Derivatives are matrix-free JAX calls on the same kernel.
# Forward mode: one Jacobian-vector product for one direction dU.
Jv = jax.jvp(lambda V: disc.residual(V, geom, th, weak=True), (U,), (jnp.ones_like(U),))[1]
# Reverse mode: the gradient of ONE scalar with respect to ALL mesh nodes in one sweep.
dJ_dX = jax.grad(lambda X: disc.residual(U, disc.geometry(X), th, weak=True).sum())(disc.X0)
```

This block is executed by `tests/test_docs_content.py`. Run on its own on a four-core CPU it
took 40 s with a peak memory of 0.7 GB while the machine was heavily loaded (load average
above 30); a single earlier run took 15 s, under a load that was not recorded. Most of the
time is compilation.

> **Warning: Forward or reverse mode: count the inputs**
>
> Forward mode (`jax.jvp`, `jax.jacfwd`) costs one tangent per **input** direction; reverse mode
> (`jax.grad`, `jax.vjp`) one sweep per **output**. A scalar that depends on every mesh node has
> thousands of inputs and one output: use `jax.grad`. `jax.jacfwd` on the same function would
> carry one tangent per node coordinate through the residual at once — on the 256-element mesh
> of the `naca0012_euler` preset that is 8448 tangents, which exhausted the memory of a 16 GB
> host. Never form a full Jacobian with respect to the state or the mesh; the solver, the
> adjoint and `FlowDiscipline` use products only.

<!-- begin level 3 (Mechanics) -->
**[L3 Mechanics] Useful entry points**

| Object | Purpose |
|---|---|
| `gyre.physics.flow_params` | build the differentiable parameter dict |
| `Discretization.jitted("residual_weak" | "geometry" | "dt_local")` | compiled kernels with operator tables as runtime arguments |
| `gyre.implicit.SteadySolver`, `SolverOptions` | nonlinear solver |
| `gyre.implicit.Hierarchy` | p-multigrid: `setup`, `solve(mode="fwd" | "adj")` |
| `gyre.functionals.SurfaceOps` | surface forces, loads, distributions |
| `gyre.deform.Deformer` | surface displacement → volume nodes |
| `gyre.error.estimate_error` | adjoint-weighted-residual estimates |
| `gyre.adapt.adaptive_order`, `prolong_state` | output-driven order selection; interpolation of a state to another degree |
| `gyre.meshio` | mesh files: Gmsh reader and writer, NPZ format, inspection |
| `gyre.mms.manufactured_source` | forcing for manufactured solutions |
| `gyre.runner.run_case` | the end-to-end record used by the service |
| `gyre.io.write_vtu`, `save_checkpoint` | output |

<!-- end level 3 -->

<!-- begin level 3 (Mechanics) -->
**[L3 Mechanics] Device and precision**

Import `gyre` before using JAX: it enables double precision and applies `GYRE_DEVICE`. To pin a
process to the CPU set `GYRE_DEVICE=cpu` in the environment before the import.

<!-- end level 3 -->
