Metamorphic tests: checking a surrogate without ground truth¶
Most physics gives relations between inputs and outputs that hold whatever the true answer is: mirror the geometry
and the flow mirrors; keep the Reynolds number and the dimensionless answer stays; add heat and the peak temperature
does not fall; double the source of a linear problem and the answer doubles. These are metamorphic relations, and
they test a surrogate where no reference solution exists. pinneapple_analysis.validation.metamorphic runs them on
any callable model (NumPy arrays or dicts of columns in, arrays or dicts out).
from pinneapple_analysis.validation.metamorphic import box_sampler, check_relations, relations_from_spec
def peak(x): # any model: here the exact peak temperature of a heated slab
return {"T": x["T0"] + x["q"] * x["L"] ** 2 / (8 * x["k"])}
relations = relations_from_spec([
{"name": "more-heat-hotter", "kind": "monotonic", "input": "q", "output": "T", "direction": "increasing"},
{"name": "better-conductor-cooler", "kind": "monotonic", "input": "k", "output": "T", "direction": "decreasing"},
{"name": "same-q-L2", "kind": "invariant", "inputs": {"q": "4 * q", "L": "L / 2"}, "output": "T", "tol": 1e-9},
])
box = box_sampler({"q": (1e3, 1e5), "L": (0.01, 0.1), "k": (1, 400), "T0": (250, 350)})
report = check_relations(peak, relations, box, n=64, search="hypothesis")
print(report.summary())
assert report.passed
Relations¶
| Relation | Class / spec kind |
Holds when |
|---|---|---|
| Geometric symmetry | Symmetry / symmetric |
f(T x) = S(x, f(x)); reflect_field, rotate_field for gridded inputs |
| Similarity scaling | Similarity |
the dimensionless output is unchanged when the dimensionless groups are (same Re at another size, Darcy with a and f scaled together) |
| Invariance | Invariance / invariant |
an input change that must not matter does not move the output |
| Monotonicity | Monotonicity / monotonic |
more of an input gives no less (no more) of an output; the violation is the wrong-way move over the spread of the output, default tolerance 1e-3 |
| Superposition | Superposition / superposition |
f(a + b) = f(a) + f(b) and f(2a) = 2 f(a) for linear problems (affine=True subtracts f(0)) |
| Resolution consistency | ResolutionConsistency |
an integral of an operator's output is the same with the input at another resolution |
Violations are relative: to the RMS of the output over the samples (default), to each sample's own output
(per_sample, for positive quantities) or to a fixed scale such as the model's accepted error. The worst sample is
returned as the counterexample. With search="hypothesis" (and the hypothesis package installed) a property-based
search for a violating sample runs as well; without the package the report says it used random search only.
Zoo models¶
pinneapple_hub.zoo.relations.check_zoo_relations(model) runs the relations each zoo model's physics implies:
| Model | Relation | Worst violation (128 samples) | Tolerance |
|---|---|---|---|
airfoil_cst_rans_mlp |
mirror camber and angle of attack: lift flips | 0.83 x accepted Cl error (0.01) | 2 |
| moment flips | 0.91 x accepted Cm error (0.002) | 2 | |
| drag unchanged | 1.4 % | 4 % | |
| lift grows with angle of attack (-2 to 8 deg) | 0 | 1e-3 | |
heatsink_forced_mlp |
resistance falls with air speed; pressure drop grows with it; resistance falls with conductivity | 0 | 1e-3 |
| resistance independent of power (air speed 2-6 m/s, per sample) | 1.1 % | 2 % |
The mirror tolerance is twice the card's accepted mean error because both sides of the relation are predictions. The power invariance is not tested below 2 m/s: there the physics engine that generated the training data is not power-linear either (its resistance rises 6.5 % when the power doubles at 0.6 m/s), and the model follows it.
In pinneapple check¶
Declare relations on a [[model]] with a domain (rule PC303):
[[model]]
name = "peak-temperature"
path = "surrogates/peak_temperature.py:predict"
inputs = ["q", "L", "k", "T0"]
outputs = ["T"]
domain = { q = [1e3, 1e5], L = [0.01, 0.1], k = [1, 400], T0 = [250, 350] }
relations = [
{ name = "better-conductor-cooler", kind = "monotonic", input = "k", output = "T", direction = "decreasing" },
]
How it was checked¶
tests/test_metamorphic.py, with the finite-volume Darcy solver (operator_benchmarks.darcy_2d) as the reference:
- the solver obeys superposition in the source to 4.7e-16, reflection and 90-degree rotation of
(a, f)to round-off, andu(c a, c f) = u(a, f); the mean pressure on a 32² grid and its 16² cell average agree to 1.6 %; - an FNO trained on the same map (
FNO2d, test error 10.8 %) violates superposition by 0.68 of the output scale; - a model that ignores the source fails similarity scaling, a tilted Darcy solution fails reflection, an airfoil
model with a 0.03 lift offset fails the camber mirror (about 6 x the accepted error), and a slab model with a unit
slip in
kfails monotonicity; each reports its counterexample; - the zoo models pass the relations above.
Limits¶
- A relation only tests what it states. A model can satisfy every relation and still be wrong by a constant factor that the relations do not constrain; use it alongside residual and reference checks.
- Random and property-based search find violations, they do not prove their absence.
- Tolerances must come from the physics and the model's stated accuracy. A relation that does not hold for the real physics in part of the box (the heat sink's power invariance at low air speed) is restricted to where it holds, not loosened.