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.
Asking for derivatives#
gyre run naca0012_euler --gradients --of cl,cd --wrt alpha,mach
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"]}
}'
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"])
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 fieldf_surfaceis differentiated throughadjoint_product/tangent_productwith a seed. - Inputs (
wrt):mach,alpha(radians),re,gamma,pr, any boundary datum present in the case, andx_surface. The CLI, jobs and sessions also acceptalpha_degand return that derivative per degree. - Mode:
adjoint(one solve per output),direct(one per scalar input; nox_surface), orauto.
{"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#
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 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.
gyre run bump_euler --set discretization.order=1 --target-error cd=1e-3 --max-order 4
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}
}'
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"]
- 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}
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 runprints one line per degree (order P: N DOF, cd = …, estimated error … (target …), ororder P: N DOF, did not converge (reason);-qsuppresses them). Its JSON summary lists per levelorder,degrees_of_freedom,convergedanderror; the complete level records are inresult.jsonwith-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-orderis 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-orderwithout--target-erroris an error.- Jobs validate
adaptwhen the request arrives: unknown keys, a tolerance that is not positive or amax_orderoutside 1 to 8 are a 422ValidationError; a case of order 0 or amax_orderbelowdiscretization.orderis a 422InvalidCase. Anadapt.outputthat is not a scalar output of the case is a 422UnknownNamethat lists the scalar outputs; nothing is queued. The job’s event log shows every degree in turn (acaseevent, theiterationevents of its solve and oneadaptevent with the level record),result_summary.adaptationcarries the selection withoutlevels, andadaptis part of the result-cache key. - Pipeline
solvestages putadaptationinto the stage outputs. The stage fails when the solve does not converge, but not when the tolerance was not met: gate onadaptation.metas in the example if the pipeline needs the accuracy. adaptive_orderalso returnscase, the solvedCaseof the final degree. Withrun_case,adaptcannot be combined withcase=, and keys other thanoutput,toleranceandmax_orderraiseValueError.
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 it becomes followed
by the listed degrees below .
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.