Credibility and applicability
A VAFT result is trusted for six different reasons, and they fail independently. A reconstruction can fit every probe and still be unidentified. A solve can converge and still violate an ordering its model assumed. A datum can be valid and still be extrapolated. This page fixes the vocabulary for those questions (issue #1639) and the evaluation of one of them, approximation applicability (#1628, with the ordering quantities from #1627).
The rule this page protects
Scientific assessment surrounds the fast path; it never burdens it. A process computes its
result and returns it. Assessment is a separate, optional call over that result, and so is
uncertainty propagation. Nothing on this page is imported by vaft.process, and
test/test_validation_credibility.py checks that in a clean interpreter.
The six axes
vaft.validation.credibility.AXES:
| axis | question | typical evidence |
|---|---|---|
source_evidence (E) |
Does the source datum itself provide credible evidence? | channel validity, calibration, drift, saturation |
transformation (T) |
How faithfully was it turned into what is consumed? | time mismatch, mapping distance, fraction extrapolated |
inference (I) |
Does the evidence determine the requested state? | fit residuals, Jacobian conditioning, singular values |
applicability (A) |
Do the model’s assumptions hold for this state? | ordering margins (below) |
numerical (N) |
Was the calculation performed reliably? | convergence, Grad–Shafranov residual, identity closure |
independent_validation (V) |
Does evidence not used in the inference support it? | withheld diagnostic, unused identity, alternative solver |
The axes are a classification, not a result schema. A domain module still returns its own plain
dict. Evidence(axis, key, status, metrics, reasons, cost, role) exists only so that evidence from
different domains can be laid side by side. compose() returns one status per axis and never
one overall score. For example, fit = pass, independent validation = not available is a
different statement from independent validation = fail.
Rules carried over from #253 and #337:
- A metric is not a verdict, and a verdict is not a policy. Whether a result may be used is decided by the workflow.
- Missing evidence is not failure.
- A precondition (“can this algorithm run?”) is not credibility (“is the result trustworthy?”).
Fitted data is not independent validation. Every piece of evidence can carry a role from
EVIDENCE_ROLES, such as used_for_inference or independent_validation.
- VAFT’s EFIT fits the diamagnetic flux by default (
EFITConfig.use_diamagnetic_flux; #891, #1440). So when the reconstruction fitted the flux – the report’sdiagnostic_fit.diamagnetic_fluxrecordsfit_role: fitted, i.e. the constraint’s weight was non-zero, whatever grade the entry received – every check inDIAMAGNETIC_CHECKSmoves off the V axis automatically. - Name any other fitted check in
used_for_inference=. An unknown key or a bare string is refused rather than ignored.
Cost classes
COST_CLASSES = ("cheap", "moderate", "expensive"):
- cheap: read from results that already exist, such as a convergence flag or an ordering parameter.
- moderate: local computation, but no solver rerun.
- expensive: solver ensembles, Monte Carlo, continuation. Always opt-in.
Pilots
| pilot | where | what it shows |
|---|---|---|
| A: equilibrium reconstruction | evidence_from_equilibrium_report |
the validate_equilibrium report on the axes; fit quality and plausibility (I), solution consistency (N) and independent validation (V) stay apart |
| A: the #891/#1331 study criteria | evidence_from_efit_criteria |
criteria v2 (#1521); Thomson stays on V and never enters fit quality |
| B: virial closures | the virial checks inside pilot A | identity residual (N), leave-one-identity-out (V), closure conditioning (I) |
| C: current moments (#943) | successive_discrepancy |
relative change of an observable along the moment order |
| D: asymptotic validity | vaft.validation.applicability |
ordering margins and contracts, below |
How the mappings are chosen.
CATEGORY_AXESmaps each existing validation category to its default axis.CHECK_AXESrecords the registry checks placed elsewhere, each with its reason in a source comment. For example, plausibility of the inferred state isinference, notnumerical.CRITERIA_AXESis the complete mapping for the study criteria. There,virialcompares two β_p of the same g-file, so it isnumerical, and only Thomson is on V.
Applicability: margins first, labels second
A reduced model is derived under orderings. For example, ideal single-fluid MHD needs all of the following:
- $S \gg 1$;
- $d_i/L \ll 1$;
- $\rho_i/L \ll 1$;
- $\tau_{\rm evol}/\tau_A \gg 1$.
These test different things, so an ApproximationContract keeps them as separate
OrderingAssumptions. Each assumption names its quantity, its characteristic scale and its
scope, so $d_i/a$ can never silently stand in for $d_i/\delta_{\rm layer}$.
The primary output is the continuous ordering margin against the assumption’s threshold $x_0$:
\[m = \log_{10}(x_0/x) \quad (x \ll x_0), \qquad m = \log_{10}(x/x_0) \quad (x \gg x_0).\]- $m > 0$: on the permitted side, with $m$ decades to spare.
SUPPORTEDmeans only this: $x = 0.99$ against $x_0 = 1$ is supported with $m = 0.004$, so read the margin, not the label. - $m \le 0$: violated.
- The default $x_0 = 1$ is where the expansion parameter reaches order unity. This is the only
cutoff applied without a source. Any other threshold must name its source in
threshold_source, or the assumption refuses to construct (#1639 §12).
Statuses. They come second, from the same vocabulary as the operational-space renderer (#1664):
| status | meaning |
|---|---|
SUPPORTED |
every assumption evaluated and satisfied |
OUTSIDE |
at least one evaluated assumption violated, whatever else is missing |
UNASSESSED |
nothing violated, but some quantity is missing, non-finite or non-positive, or the state lacks a field the contract’s scope is defined on |
NOT_APPLICABLE |
the state is outside the contract’s applies_to scope |
There is no MARGINAL: a margin band would be a cutoff nobody derived, and the margin itself is
there for anyone who wants to see how close a state is.
Functions:
evaluate_contract(contract, state)returns every assumption’s value, margin and status, plus the limiting assumption.evaluate_population(contract, table)keeps every row and every individual check.as_evidence(result)places a result on the A axis.
The strongest applicability evidence pairs the ordering with the convergence it predicts (#1639
§12): successive_discrepancy([Q_reduced, ..., Q_full]).
Ownership
| concern | owner |
|---|---|
| ordering quantities ($S$, $d_i/L$, $Kn$, $\Omega_c/\nu$, …) | vaft.formula / vaft.process (#1627) |
| which orderings a model assumes (the contracts) | declared beside the model (#1627 for ordering families, #1628 for formulas, codes, workflows) |
| margin, status and composition semantics | vaft.validation.applicability (this page) |
the credibility axes and Evidence |
vaft.validation.credibility (this page) |
| drawing statuses on a diagram | vaft.plot.operational_space (#1664) |
| acceptance policy (what a study accepts) | the workflow or notebook |
| sensitivity, Jacobians, uncertainty propagation | #1642, opt-in |
| the VEST population study | notebooks/vest_ordering_and_model_applicability.ipynb (#1629) |
Sensitivity, linearization and uncertainty (#1642)
The domain that owns an operation owns its derivatives. EFIT’s native response matrix stays in
vaft.code.efit.linearization, and a process’s Jacobian stays beside the process. Two shared
pieces sit around them:
- the algebra, in
vaft.formula.sensitivity; - what the numbers mean, in
vaft.validation.sensitivity.
There is no sensitivity package, and nothing in this section runs unless it is called.
| function | layer | cost |
|---|---|---|
finite_difference_jacobian(f, x) |
formula | 2n + 1 model calls (central), n + 1 (forward) |
linear_covariance_propagation(J, Σ) |
formula | $J\Sigma J^{\mathsf T}$, correlations kept |
monte_carlo_propagation(f, μ, Σ, samples, seed) |
formula | one model call per sample: opt-in only; failed draws are counted as rejected |
singular_value_spectrum(J, column_scale) |
formula | SVD of $JD$, never of $J^{\mathsf T}J$ |
linearity_ratio(f, x, δ, J) |
formula | share of a finite response the Jacobian misses |
propagate_formula_uncertainty(formula, inputs, covariance= \| std=) |
formula | one public formula by parameter name: the kernels above plus a $\pm1\sigma$ linearity check (#1874) |
Jacobian(provenance, kind, perturbation, inputs, outputs, matrix \| jvp) |
validation | explicit or matrix-free |
compare_jacobians(reference, candidate, tolerance=) |
validation | one derivative verified by another |
compare_linear_to_monte_carlo(J Σ Jᵀ, sampled, tolerance=) |
validation | local-to-global escalation test |
scan_evidence(report) |
validation | a targeted scan (#1663) as model-form evidence |
Propagating through one formula (#1874)
A formula can state how uncertainty passes through it. Its docstring then has an Uncertainty propagation
section: the input order, the Jacobian, what is held fixed, and where first order stops being valid. It may
also carry a domain-owned analytic Jacobian. The decorator that attaches one returns the formula itself, so
its signature, values and cost do not change. describe(name).uncertainty_propagation and
.analytic_jacobian report both, and the formula reference shows the section. The pilots are
equilibrium.confinement_time_from_P_loss_W_th and equilibrium.ohmic_heating_power_from_I_p_V_res.
import numpy as np
from vaft.formula import equilibrium, sensitivity
# P_loss = 100 kW +/- 10 %, W_th = 2 kJ +/- 5 %, correlated through one reconstruction (rho = 0.5)
cov = np.array([[1.0e4**2, 0.5 * 1.0e4 * 1.0e2],
[0.5 * 1.0e4 * 1.0e2, 1.0e2**2]])
result = sensitivity.propagate_formula_uncertainty(
equilibrium.confinement_time_from_P_loss_W_th,
inputs={"P_loss": 1.0e5, "W_th": 2.0e3},
input_names=("P_loss", "W_th"), # the covariance axes; any other input is fixed
covariance=cov, # or std=[...] for independent inputs, never both
)
result.value, result.std # 0.02 s, about 1.7 ms
result.derivative_method # "analytic" (else "finite_difference")
result.linearity # largest linearity_ratio over +/- 1 sigma: about 0.1 here
The facade’s rules:
- An unstated uncertainty is an error, not zero. Pass explicit zeros for an exact input.
- A 1-D
covarianceholds variances;stdholds standard deviations. - The finite-difference step follows each input’s own magnitude.
- A formula may declare its domain next to its Jacobian ($P_{loss} > 0$, $W_{th} \ge 0$ here): a finite
$W/P$ at negative $P$ is still not a confinement time. Pass
domain=to override it.- Input means outside the domain are refused rather than given a covariance.
- A $\pm1\sigma$ step that leaves the domain (here $P_{loss} - \sigma \le 0$) reports
linearity = inf.
method="monte_carlo"samples the formula itself from Gaussian inputs (no other distribution is supported). It counts out-of-domain draws inrejected, and refuses a run that accepts fewer than two.
The result is local and first order. It carries only measured-input uncertainty, not the fitted coefficients
or scatter of an empirical law. compare_linear_to_monte_carlo and compare_jacobians interpret it.
Operational boundaries. vaft.formula.boundaries.propagate_boundary_uncertainty(boundary, x, inputs=..., std= | covariance=)
carries measurement uncertainty into both the boundary value $b$ and the signed margin of an operating point.
The operating value can share a covariance with the boundary inputs, for example a density and the current
from one shot. Three kinds of uncertainty stay apart:
- measurement:
boundary(on $b$) andmargin, both full propagation results; - source parameters:
source_parameters_relative, the relative spread of $b$ from the coefficient and exponent uncertainty the source reports, taken as uncorrelated because no covariance is published.source_parameters_completeis False when the source reports only some of them; - source scatter:
source_scatter_relative, the source’s RMS scatter about its fit.
The last two are also given on the margin’s scale (margin_std_from_source_*). A part the source does not
report, or reports in a form that cannot be propagated, is None: unknown rather than zero. source_note
says which applies. warnings repeats the applicability and kind warnings of evaluate_boundary, and flags a
margin whose linearity check fails. boundary.uncertainty is passed
through unchanged, and nothing returns a permitted/forbidden verdict. A margin within its spread is an
assessment for the caller or vaft.validation, and crossing a soft boundary is not a prediction of failure.
Distinctions kept apart:
- Provenance:
native,finite_difference,autodifforanalytic. Comparing a Jacobian with one of the same provenance is refused: that is repetition, not validation. - Kind: an
observationJacobian (identifiability), agoverning_operatorone (uniqueness, branches) and aforwardone (local sensitivity) answer different questions. - Perturbation:
physicalversusnumerical. Physical sensitivity is never mixed with convergence.compare_jacobiansrefuses a pair that differs in kind or perturbation unlessallow_mixed=True.
Tolerances. A comparison returns metrics. It carries a status only when the caller passes a
tolerance=(warn, fail); no tolerance is invented here.
- An infinite discrepancy fails. Examples: a zero Jacobian against a real spread, or a non-zero row against a zero one.
- A comparison over Monte Carlo draws that rejected any sample is never better than
warn: the sampled covariance is then conditioned on the model succeeding.
test/test_sensitivity_contract.py demonstrates the #1642 acceptance points at unit-test cost:
- A derivative checked by another method. The exact Green’s-function field (an analytic derivative already in VAFT, $B = \nabla\times\psi$) matches the finite-difference derivative of its flux to below $10^{-6}$.
- Local propagation against sampling.
vaft.process.confinement.dimensionless_confinement_indicesalready propagates the exponent covariance through a central-difference Jacobian.- Its covariance equals the generic kernels’.
- At $\alpha_P = -0.6$ it passes the test’s tolerance (0.1 in $\ln\sigma$) against 4000-draw Monte Carlo.
- It is not exact. The linear standard deviation is 5–8 % low across seeds, against a sampling error of 1 %: the curvature of $1/(1+\alpha_P)$ shows even here.
- Where the linearization fails. At $\alpha_P = -0.93$, $1+\alpha_P = 0.07$ puts the pole
1.4 standard errors away, inside the input distribution.
- $1/(1+\alpha_P)$ of a Gaussian $\alpha_P$ has infinite variance. The sampled spread does not converge with the sample count, and only its size, not its sign, depends on the seed.
- The linear propagation fails against it every time, as that function’s docstring warns.
- The cheap
linearity_ratioflags the problem before any sampling is paid for. - The reported
sampling_standard_errorassumes finite variance, so it means nothing in this regime.
Workflow graduation audit
#1642 §21 asks which workflow-held logic is reusable. These are the candidates found so far. None is moved by this change; each move belongs to the owning lane.
| workflow logic | what is reusable | owning API |
|---|---|---|
workflow/efit_identifiability/identifiability_study.py (#664) |
column scaling, null space, singular classes, direction linearity | already partly in vaft.code.efit.analyze_efit_identifiability; the generic SVD is singular_value_spectrum |
workflow/efit_uncertainty_calibration/weight_scan.py stage 6 and sensitivity_report.py (#1663) |
targeted scan over σ and basis; model-form spread | stays a study; read through scan_evidence |
workflow/confinement_scaling/closures.py |
bootstrap over shots | vaft.process.confinement.bootstrap_confinement_scaling (already graduated) |
vaft.process.confinement.dimensionless_confinement_indices |
inline central-difference $J\Sigma J^{\mathsf T}$ | could call the formula kernels; left untouched (Lane D) |
Two notes on this table:
- “Already graduated” means only that the library function exists.
closures.pykeeps its own bootstrap loop. sensitivity_report.pyand the stage-6 grid exist only on the #1663 branch for now.