The test harness compares each run's output to a recorded reference using a DG element-L2 relative norm that is invariant to MPI partition assignment and tolerant to platform numerics drift (e.g. Apple Accelerate vs Intel MKL).
relative L2 = sqrt( sum_e ||u_run - u_baseline||² / sum_e ||u_baseline||² )
Pass threshold: < 1e-3. Well above platform-numerics drift (typical
~1e-7), well below anything that would mask a real solver bug
(typical >1e-2).
This file documents the sidecar format, the comparison script, and how to record a new baseline.
- Partition-invariant. Aggregate by global element ID and sort
before comparing. Different
np, different ParMETIS choices, different load balance — same metric. - FP-order-tolerant. Relative L2 with a
1e-3threshold absorbs last-bit drift between BLAS implementations / SIMD widths / FMA policies that accumulates through GMRES + Newton iterations.
When SaveSolutions writes outudg_np<r>.bin, it also writes
outelemid_np<r>.bin — ne_local int64 values, one per local
element, holding the global element ID of each element in the
order outudg stored them.
outudg_np<r>.bin (header[3 doubles]) + (npe × ncu × ne_local) doubles
outelemid_np<r>.bin (ne_local int64 values)
The global IDs come from mesh.elempart[0..ne_local), which is the
same array ParMETIS / DMD uses internally to map local element index
→ global element index.
The runtime function that writes the sidecar lives in
backend/Solution/solution.hpp CSolution<M>::SaveSolutions.
apps/library_example/element_l2_diff.py aggregates and compares.
python3 apps/library_example/element_l2_diff.py \
--baseline-dir <path> \
--current-dir <path> \
--np <N> \
[--baseline-np <N>] # default 1 (serial baseline)
[--rtol 1e-3] # pass threshold
[--stem outudg] # default: outudgFlow:
- Read all
outudg_np<r>.binforr in [0, np)and the matchingoutelemid_np<r>.bin. Strip the 3-double header. - Slice the
outudgpayload into per-element blocks (size = total doubles / ne_local). - Build a dict
global_id → element_blockfor both baseline and current. - Sort the union of keys; complain on missing pairs.
- For each common key, accumulate
||a - b||²and||a||²(sum of squares of nodal values inside that element block). - Return
sqrt(err_sq / ref_sq)and pass if< rtol.
Exit code 0 on pass, non-zero on fail. The summary line is the relative L2 magnitude, which the test harness echoes:
[ OK ] poisson2d (element-L2: outudg: relative element-L2 = 4.13e-12 over 256 elements)
The threshold of 1e-3 is conservative — typical lockstep-pass
magnitudes are 1e-12 to 1e-7, real bugs show up at >1e-2.
For a new example app:
- Make sure the binary works in serial (run any output-producing
variant with
np=1):cmake --build build_cpu --target <name>_codegen bash apps/library_example/regenerate.sh <name> bash apps/library_example/validate_codegen.sh <name> # FAIL — no baseline
- Copy the freshly-produced output bins as the canonical baseline:
mkdir -p baseline/<name>_serial cp apps/library_example/<name>_codegen/dataout_codegen/outudg_np0.bin \ apps/library_example/<name>_codegen/dataout_codegen/outelemid_np0.bin \ baseline/<name>_serial/ # If the model defines QoIs, copy that too — outqoi.txt is # partition-invariant by nature and is a stronger gate when # available: cp apps/library_example/<name>_codegen/dataout_codegen/outqoi.txt \ baseline/<name>_serial/ # optional
- Re-run the validator — should pass with element-L2 ~ 0:
bash apps/library_example/validate_codegen.sh <name> # → [ OK ] <name> (element-L2: outudg: relative element-L2 = 0.000e+00 …)
- Commit
baseline/<name>_serial/(the bin + sidecar files).
For an existing app whose recorded baseline is bit-fragile (drifts across re-runs even on the same machine), record it once with element-L2 sidecars and never look back.
After recording on Mac and pushing, on lanka:
cmake --build build_cpu --target <name>_codegen
ctest --test-dir build_cpu -R "<name>:.*:cpu" --output-on-failureExpected: passes with element-L2 in the 1e-12 to 1e-7 range. If it gates at 1e-3 (the threshold), the magnitude tells you whether it's platform-numerics or a real bug — the loose threshold catches real divergence at 10-100× over the platform-drift baseline.
Still supported as a fallback. If a baseline directory has only
outudg_np0.bin + md5.txt (no sidecar), the harness drops to bin-
md5 comparison. New baselines should record the sidecar.
When the baseline lacks outqoi.txt AND lacks outelemid_np0.bin
(the truly-old baselines), MPI variants fall back to the structural
"each rank wrote its own bins" check — weaker than numerical
validation but catches the common failure modes (binary abort,
missing SaveSolutions, wrong rank count). Once you re-record with
the sidecar, the strong element-L2 check kicks in.