Gyredocs
Depth Practice
API

Gyre/User Guide

Gradients and error estimates

In one minuteRequest 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.

Written at L2 Practice goes to L3 Mechanics 1474 words Markdown

Asking for derivatives#

bash
gyre run naca0012_euler --gradients --of cl,cd --wrt alpha,mach
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"]}
}'
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"])
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)
  • 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.
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.

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 for the method and its verification.

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.

An error estimate builds the p+1p+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.

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.

bash
gyre run bump_euler --set discretization.order=1 --target-error cd=1e-3 --max-order 4
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}
}'
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"]
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}
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"

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.

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 pp it becomes pp followed by the listed degrees below pp.

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.

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.

    ↑ ↓ moveEnter open[ ] change depthResults show section and depth level