Physics audit: which model to use, when to trust it, when to revisit the physics¶
"It is in the fundamental knowledge that we must look for the cause of a model's imperfections."
An adaptive ensemble picks the best model from the errors each one made on earlier cases. That leaves two questions open:
- Is the current case still inside the physics each model was built on?
- When a model fails, why did it fail?
pinneapple_physics.physics_audit (also pp.physics_audit) adds those layers on top of the predictive models. It is
plain numpy and does not import torch.
| Layer | What it does | Module |
|---|---|---|
| 1. Predictive models | PINN, FNO, GNN, DeepONet, CNN, solvers, closed forms | pinneapple_physics.ensemble experts |
| 2. Physical evaluation | Residuals, conservation, bounds, boundary conditions and invariants, without ground truth. Each model's assumptions and the regime of the case | monitor, assumptions, regime |
| 3. Adaptive decision | Compatibility of the case with each model's physics, used as a prior on the ensemble weights. Models outside their physics sleep, and the router escalates to the solver | routing |
| 4. Diagnosis and evolution | Probable cause of a failure, with its evidence and a confidence. Next experiment or change of highest value. Graph of phenomena, equations, assumptions and models | attribution, improvement, knowledge |
Assumptions and their validity (Physics Assumption Auditor)¶
A ModelProfile is the model card that the audit reads. It holds:
- the model's equations;
- its assumptions, taken from the catalogue or written by you;
- the mechanisms it represents;
- its training envelope.
Each assumption holds inside a range of dimensionless numbers, with a soft band around the edges. Near a transition
the audit says MARGINAL; it does not flip from valid to violated. A feature that is not given is UNKNOWN, never a
silent pass.
from pinneapple_physics.physics_audit import ModelProfile, Envelope, laminar, audit_assumptions, regime_features
f = regime_features(velocity=0.12, length=0.05, nu=1e-6) # {"Re": 6000, "Fr": ...}
lam = ModelProfile("laminar", "closed form", ("Hagen-Poiseuille / laminar correlations",),
(laminar(2300), "incompressible"), envelope=Envelope({"Re": (1, 2300)}))
print(audit_assumptions(lam, {**f, "Ma": 0.05}))
# laminar: VIOLATED (compatibility 0.00)
# [VIOLATED] laminar: Re <= 2.3e+03 (Re = 6e+03): VIOLATED
# [VALID] incompressible: Ma <= 0.3 (Ma = 0.05): VALID
# [OUTSIDE] training envelope: Re = 6e+03, trained on 1 .. 2.3e+03 (outside)
# probable phenomena: turbulence
Catalogue. ASSUMPTIONS lists the common simplifications. The factory functions build them with other
thresholds.
- Flow regime:
laminar,turbulent,hydraulically_smooth,creeping_flow,attached_flow - Fluid behaviour:
incompressible,continuum,boussinesq,constant_properties,shallow_water - Structures:
small_strain,linear_elastic - Heat and transport:
lumped_capacitance,forced_convection,diffusion_dominated,advection_dominated
Features. regime_features and the functions in regime compute these numbers:
- Re, Ma, Pe, Kn, Ri, Ra, Gr, Pr, Bi, Fr, St and Da;
- β·ΔT (
beta_dT) and ΔT/T (dT_over_T); - the roughness Reynolds number ε⁺.
Training envelope. An Envelope can come from training samples (Envelope.from_samples). It then also measures
the distance to the nearest training case, so a case that sits in a hole of the data is flagged even when it is
inside every range.
Validity monitor (no ground truth needed)¶
PhysicsValidityMonitor runs the checks below on every prediction:
| Check | What it measures |
|---|---|
residual_check |
Norm of the residual of the governing equations |
conservation_check |
Conserved total against its expected value |
bounds_check |
Admissible values, such as non-negative density (critical by default) |
boundary_check |
The prediction against the boundary condition |
constitutive_check |
Violation of a material or constitutive law |
invariant_check |
Change of a quantity that must stay constant |
envelope_check |
Distance outside the training envelope (extrapolation) |
The status goes from OK to WARN and then ALARM. An alarm is raised when:
- a critical check fails;
- the same check fails
persistencetimes in a row; - a residual drifts to
drift_ratiotimes its own baseline, even while it is still below its tolerance.
Physics-aware routing¶
PhysicsAwareRouter turns the compatibility of the current case with each model into a prior on its weight:
w_i ∝ w_learned_i · max(c_i, floor)^strength · exp(-cost_weight · cost_i / max cost)
- Sleeping models. A model below
abstain_belowis asleep. It is not used for that case, and inside the ensemble the case does not change its weight: its loss is set to the mixture's. A laminar correlation is therefore not punished for the turbulent cases it was never meant for. It is ready again as soon as the flow returns to its regime. - Escalation. When no model fits (best compatibility below
escalate_below), the router escalates to the solver named inescalate_to.
from pinneapple_physics.ensemble import PhysicsEnsemble
from pinneapple_physics.physics_audit import PhysicsAwareRouter
router = PhysicsAwareRouter(profiles, features_fn=lambda case: case["features"],
escalate_below=0.9, escalate_to="solver")
ens = PhysicsEnsemble(experts, mode="select", router=router)
out = ens.predict(case) # also returns "prior", "awake" and "escalate"
print(router.route(case)) # standalone decision, with the reasons
The learned weights, the residual look-ahead and the cost term of the ensemble still apply on top of the prior. So history and the current residual can correct a compatibility estimate that is wrong.
Failure attribution¶
attribute_failure weighs the evidence of one case and returns the probable causes, each with a probability and its
evidence. The confidence of the diagnosis drops when evidence types are missing, and the report lists which ones
were.
| Cause | Evidence |
|---|---|
pde_violation |
The residual fails its tolerance or jumps above its in-domain level |
conservation |
A conserved total is off |
boundary_condition |
The prediction does not meet the boundary condition |
regime_change |
The case violates an assumption of the model |
extrapolation |
The assumptions hold, but the case is outside the training envelope, far from the training cases, or the models disagree |
numerical_resolution |
Grid convergence index, change under refinement, CFL above its limit, or non-finite or out-of-bounds values |
measurement |
The reference reading failed its own physical consistency check |
missing_physics |
The model satisfies its equations, inside its assumptions and envelope, and still disagrees with the reference (model-form error) |
Low residuals do not guarantee a correct prediction, and high residuals alone do not identify the cause. The last row is the case where the residual misleads. When there is no reference, a low residual comes with a note that it does not prove the prediction right.
Knowledge graph and guided improvement¶
default_graph(profiles) links phenomena, equations, assumptions, parameters, mechanisms and models. It answers
questions such as:
unrepresented(model, features): Re went up, so is turbulence represented in this model?models_for(mechanism): which models represent this mechanism?explain(model, features): a readable account of what the model misses for this case.
to_mermaid() draws the graph.
recommend(attribution, audit, graph, features, model) ranks the next steps by
probability × confidence × gain / cost:
- switch to a model or equation that represents the missing mechanism (the graph names it);
- run high-fidelity simulations in the new regime, with design points around the case;
- train a specialised model;
- add collocation points or change the loss;
- add a conservation projection;
- enforce the boundary condition exactly;
- refine the mesh;
- check the sensor;
- add the missing term or fit a discrepancy model.
improvement_plan(cases) aggregates many cases, so the actions that fix the most frequent and most confident causes
come first.
diagnose(profile, features, prediction, query, monitor, error) runs all of this for one case and returns a
Diagnosis with trusted and a Markdown report().
Example: pipe friction across regimes¶

examples/physics_audit/pipe_regimes.py predicts the Darcy friction factor of a rough pipe (ε/D = 1e-3) for 160 cases.
The Reynolds number goes through the laminar, transitional, turbulent and high-Re rough regimes, then back to laminar.
The truth is Churchill's correlation. The models:
| Model | Formula or source | Assumes | Training envelope | Cost |
|---|---|---|---|---|
laminar |
64/Re | Laminar flow | Re ≤ 2300 | 1 |
blasius |
Smooth-pipe Blasius | Turbulent flow, smooth wall | 4000 ≤ Re ≤ 1e5 | 1 |
surrogate |
Learned surrogate | Turbulent flow | 4000 ≤ Re ≤ 2e5 | 1 |
solver |
High-fidelity, 1 % noise | — | — | 100 |
Mean relative error over the 160 cases:
| laminar | transition | turbulent | high Re, rough | laminar again | all | solver share | |
|---|---|---|---|---|---|---|---|
History only (PhysicsEnsemble) |
0.01 % | 5.9 % | 0.33 % | 1.0 % | 1.9 % | 1.49 % | 21 % |
| History + physics router | 0.01 % | 2.1 % | 0.03 % | 0.59 % | 0.00 % | 0.38 % | 25 % |
| Physics router alone | 2.27 % | 27 % | |||||
| Best single model (surrogate) | 12.3 % |
Back in laminar flow. On the first five laminar cases, the history-only ensemble keeps the turbulent surrogate, with errors of 17 %, 11 % and 47 %, and then falls back to the expensive solver. With the router, the laminar law answers from the first case, with no error, because it was asleep through the turbulent segment and kept its weight.
Physics and history complement each other. The router alone is worse than the router with history. Inside its envelope, Blasius looks compatible with the case, but the pipe is rough. Only the errors it makes reveal that.
The example then diagnoses three failures:
laminarat Re = 6000. Regime change: thelaminarassumption is violated. Turbulent transport is not represented. The recommendations are to use a model that represents it (solverorsurrogate) or to run high-fidelity simulations in the new regime.blasiusat Re = 5e5. Regime change:hydraulically_smoothis violated with ε⁺ = 25, so the flow is in rough-wall turbulence and the model lacks the wall-roughness mechanism. The case is also outside the training envelope, and the smooth-pipe law residual fails.blasiusat Re = 6e4. Every assumption is valid, the case is inside the training envelope and the residual of the smooth-pipe law is 0.3 %, yet the error is 14.5 %. The cause is missing physics (p = 0.87): the model satisfies its own equations, but they do not describe this case. A residual check alone would have trusted this prediction.