---
title: Gradients and error estimates
summary: Request total derivatives of any scalar output with respect to flow parameters and surface shape from the CLI, a job, a session or Python. One adjoint solve per output; an adjoint-based error estimate costs one more linear solve.
section: guide
order: 4
level: 2
keywords: [gradients, adjoint, totals, x_surface, error estimate]
---
<!-- gyre docs: expanded Markdown of guide/gradients.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. -->

## Asking for derivatives

**[Alternative: CLI]**

```bash
gyre run naca0012_euler --gradients --of cl,cd --wrt alpha,mach
```

**[Alternative: Job (REST)]**

```bash
curl -s http://HOST:8600/api/v1/jobs -H 'content-type: application/json' -d '{
  "case": "naca0012_euler",
  "gradients": {"of": ["cl", "cd"], "wrt": ["alpha", "mach", "x_surface"]}
}'
```

**[Alternative: Session (Python client)]**

```python
with GyreClient("http://HOST:8600").session("naca0012_euler") as flow:
    flow.evaluate(alpha_deg=2.0)
    g = flow.gradients(of=["cl", "cd"], wrt=["alpha_deg", "mach", "x_surface"])
```

**[Alternative: In-process]**

```python
tot = flow.totals(of=("cl", "cd"), wrt=("alpha", "mach", "x_surface"))
tot["derivatives"]["cl"]["alpha"]        # float
tot["derivatives"]["cd"]["x_surface"]    # array (n_surface_nodes, dim)
```

<!-- begin level 2 (Practice) -->
**[L2 Practice] Inputs and outputs you can differentiate**

* **Outputs (`of`)**: any scalar functional — `cl`, `cd`, `cm`, `cx`, `cy`, `heat` (viscous),
  `entropy_error`, `kinetic_energy`, `total_entropy`. The load field `f_surface` is
  differentiated through `adjoint_product` / `tangent_product` with a seed.
* **Inputs (`wrt`)**: `mach`, `alpha` (radians), `re`, `gamma`, `pr`, any boundary datum present
  in the case, and `x_surface`. The CLI, jobs and sessions also accept `alpha_deg` and return
  that derivative per degree.
* **Mode**: `adjoint` (one solve per output), `direct` (one per scalar input; no `x_surface`),
  or `auto`.

<!-- end level 2 -->

<!-- begin level 3 (Mechanics) -->
**[L3 Mechanics] What comes back**

```json
{"derivatives": {"cl": {"alpha": 7.7383, "mach": 0.23041},
                 "cd": {"alpha": 0.011770, "mach": 7.908e-05}},
 "info": {"mode": "adjoint",
          "linear_solves": [{"output": "cl", "iterations": 51, "reduction": 7.09e-11, "converged": true},
                            {"output": "cd", "iterations": 50, "reduction": 7.18e-11, "converged": true}],
          "wall_time": 127.6, "residual_norm": 7.06e-13}}
```

This is the `gradients` section of the result of the unmodified preset `naca0012_euler`
(`--gradients --of cl,cd --wrt alpha,mach`), recorded at revision `5ee1cdc` on the CPU and
shortened to the digits shown. `alpha` derivatives are per radian. The `wall_time` of that
record includes compiling the adjoint kernels on a busy machine; it says nothing about the
cost of a warm adjoint solve. `residual_norm` is the nonlinear residual of the state the
derivative was taken at: check it when gradients look noisy. The outputs themselves are
defined under [Outputs and functionals](../theory/outputs-and-functionals.md).

<!-- end level 3 -->

## Error estimates

```bash
gyre run naca0012_euler --error cd
```

prints, for each requested output, the value, the estimate of its change at the next polynomial
degree, the corrected value and the estimated error. The per-element `indicators` are written
to `error_estimate` in `result.json` when `-o <dir>` is given, and are returned in full by jobs,
sessions and `run_case()`. See
[Output error estimation](../theory/error-estimation.md) for the method and
[its verification](../verification/output-error.md).

The estimate is defined for the built-in scalar outputs of the case (force and moment
coefficients, `heat`, the volume diagnostics). A user-defined functional or the load field
`f_surface` raises `KeyError`, because the output has to be rebuilt in the enriched space. For
spanwise-extruded cases `value`, `corrected` and `error` are per unit span, like the
coefficients themselves.

Derivatives and error estimates belong to a converged state. When the solve of a job, of
`gyre run` or of `run_case()` does not converge, the result has `converged: false`, the outputs
of the iterate the solve ended at, and no `gradients` or `error_estimate`. `skipped` names each
requested section that is missing, with the reason: it is in the result of `run_case()`, in the
job result and the job's `result_summary`, and for `gyre run` in the `result.json` written with
`-o <dir>`. The JSON summary that `gyre run` prints has no `skipped` entry; there the
`gradients` and `error_estimate` sections are absent and the exit status is 2.

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

An error estimate builds the $p+1$ discretization and solves one transposed linear system
there. The first call compiles the enriched hierarchy (the dominant cost on small cases); the
linear solve itself is comparable to one adjoint solve at the higher degree.

<!-- end level 3 -->

## Letting the error estimate choose the order

Instead of guessing the polynomial degree, state the accuracy an output needs and let the
solver raise the degree until its estimated error is at or below that tolerance. The selection
starts at the case's `discretization.order` (at least 1) and tries degrees up to `max_order`
(default 5, at most 8). The tolerance is absolute and positive.

**[Alternative: CLI]**

```bash
gyre run bump_euler --set discretization.order=1 --target-error cd=1e-3 --max-order 4
```

**[Alternative: Job (REST)]**

```bash
curl -s http://HOST:8600/api/v1/jobs -H 'content-type: application/json' -d '{
  "case": "bump_euler",
  "overrides": {"discretization.order": 1},
  "adapt": {"output": "cd", "tolerance": 1e-3, "max_order": 4}
}'
```

**[Alternative: Python client]**

```python
res = GyreClient("http://HOST:8600").run(
    "bump_euler", overrides={"discretization.order": 1},
    adapt={"output": "cd", "tolerance": 1e-3, "max_order": 4})
res["adaptation"]["met"], res["adaptation"]["order"]
```

**[Alternative: Pipeline stage]**

```yaml
- id: solve
  uses: solve
  with:
    case: bump_euler
    overrides: {discretization.order: 1}
    adapt: {output: cd, tolerance: 1.0e-3, max_order: 4}
- id: accurate
  uses: assert
  needs: [solve]
  with:
    checks:
      - {value: "${stages.solve.outputs.adaptation.met}", equals: true}
```

**[Alternative: In-process]**

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

spec = preset("bump_euler", **{"discretization.order": 1})
res = adaptive_order(spec, output="cd", tolerance=1e-3, max_order=4)
res["met"], res["order"], res["value"], res["error"]
for level in res["levels"]:            # one record per degree tried; no value/error if not converged
    print(level["order"], level["degrees_of_freedom"], level.get("value"), level.get("error"))
flow = res["case"].discipline          # the final, solved discipline (gradients, loads, …)

# or as part of the complete result record:
result = run_case(spec, adapt={"output": "cd", "tolerance": 1e-3, "max_order": 4})
result["adaptation"]                   # the same entries without "case"
```

<!-- begin level 2 (Practice) -->
**[L2 Practice] What comes back**

Every interface reports the selection as `adaptation`: `output`, `tolerance`, `met`, `order`,
`value`, `corrected`, `error` and `levels`, one record per degree tried. Everything else in the
result — outputs, gradients, error estimates, fields, the case summary — is that of the final
degree.

* **`gyre run`** prints one line per degree (`order P: N DOF, cd = …, estimated error … (target …)`,
  or `order P: N DOF, did not converge (reason)`; `-q` suppresses them). Its JSON summary lists
  per level `order`, `degrees_of_freedom`, `converged` and `error`; the complete level records
  are in `result.json` with `-o <dir>`. The exit status is 0 when the solve converged and the
  tolerance was met, 2 when it did not converge, and 3 when it converged but the tolerance was
  not met at `--max-order`. A request that cannot start (the output is not a scalar output of
  the case, `--max-order` is below the case's order or above 8, the case has order 0) ends with a
  message and exit status 1 before any flow is solved. `--max-order` without `--target-error` is
  an error.
* **Jobs** validate `adapt` when the request arrives: unknown keys, a tolerance that is not
  positive or a `max_order` outside 1 to 8 are a 422 `ValidationError`; a case of order 0 or a
  `max_order` below `discretization.order` is a 422 `InvalidCase`. An `adapt.output`
  that is not a scalar output of the case is a 422 `UnknownName` that lists the scalar
  outputs; nothing is queued. The job's event log shows every degree in turn (a `case`
  event, the `iteration` events of its solve and one `adapt` event with the level record),
  `result_summary.adaptation` carries the selection without `levels`, and `adapt` is part of
  the result-cache key.
* **Pipeline `solve` stages** put `adaptation` into the stage outputs. The stage fails when the
  solve does not converge, but not when the tolerance was not met: gate on
  `adaptation.met` as in the example if the pipeline needs the accuracy.
* **`adaptive_order`** also returns `case`, the solved `Case` of the final degree. With
  `run_case`, `adapt` cannot be combined with `case=`, and keys other than `output`,
  `tolerance` and `max_order` raise `ValueError`.

<!-- end level 2 -->

<!-- begin level 3 (Mechanics) -->
**[L3 Mechanics] How it proceeds**

For each degree, starting at the case's own: solve, estimate the error of the chosen output
relative to the next degree, and stop when the magnitude of the estimate is at or below the
tolerance, when `max_order` is reached, or when a degree does not converge. From the second
degree on, the solve starts from the interpolated solution of the previous degree, directly in
the Newton regime. If that fails, the same degree is tried again from the interpolated solution
with the full pseudo-transient continuation (skipped when the interpolated state is not
admissible, for example a negative pressure next to a shock), and then from the free stream;
only then does the degree count as failed.

`met` is true only for a finite estimate whose adjoint solve converged. When `met` is false,
`order`, `value`, `corrected` and `error` are those of the last degree that converged; if the
starting degree itself did not converge they are `null` and the result reports
`converged: false` like any failed solve. An explicit multigrid schedule in `solver.levels`
belongs to the degree the case was written for; at another degree $p$ it becomes $p$ followed
by the listed degrees below $p$.

This is uniform enrichment on a fixed mesh. It controls the discretization error of one
analysis and nothing else: the per-element indicators of the estimate are not used, the mesh is
not adapted, and no input of the case is changed. The cost is that of every degree tried,
each with its own compilation and one error estimate at the degree above.

<!-- end level 3 -->

<!-- begin level 3 (Mechanics) -->
**[L3 Mechanics] When it helps in a design loop**

Run it once at the baseline design to pick the degree the objective actually needs, and again
when the design has moved far (for example, when a shock appears). Between those points, keep
the degree fixed: changing the discretization changes the discrete objective, and an optimizer
should not see that as a change of the design.

<!-- end level 3 -->
