Asset-integrity teams live with a recurring failure mode: a corrosion or remaining-life model arrives with impressive demo numbers, and then the conversation stops. What is the model actually doing? Is its rate within published literature for X65 carbon steel in atmospheric service? Does it hold up when you replay real survey history? Can your buyer’s engineer reproduce your numbers, not just trust a screenshot?
“Trust the vendor” is not a validation strategy. It’s how you get a surprise corrosion event three years after commissioning — and a very awkward conversation with whoever signs the integrity plan.
The Model Validation Framework (MVF) is a staged, evidence-producing pipeline that puts a degradation engine under test before it earns a seat in your integrity workflow:
| Stage | Question answered | Evidence produced |
|---|---|---|
| S0 — Synthetic correctness | Does the engine do arithmetic? | Constant/noisy/step campaigns with exact expected outcomes; per-case pass/fail |
| S1 — Literature calibration | Are its rates defensible against published bands? | Every zone × material × rate cell checked against cited literature, PASS/FAIL per cell |
| S2 — Survey-record backtest | Does it replay history correctly? | Hold-out protocol: fit on survey history, predict the final survey; TTF hits, MAE, RMSE, Brier |
| S3 — Field campaign validation | Does it work on your licensed field data? | Same pipeline, same guarantees — campaigns ingest through a versioned schema and score identically |
Two design decisions matter more than any metric:
- The engine is imported as a pinned library. MVF does not fork, wrap, or “improve” vendor math. The report names the exact engine commit, so there is no ambiguity about what was tested.
- Every report carries a provenance block — framework version, git SHA, engine commit, dataset checksums, config hash, run stamp — and is deterministic for a given stamp. A buyer re-running the pipeline gets byte-identical numbers. That sentence is the whole business case in miniature.
The numbers we publish, not the ones we hope you believe
From the current pinned run:
- S0: 33/34 synthetic campaigns pass (the one “fail” is a genuine 2σ tail draw that the harness correctly flags — correctness including the flagging is the point)
- S1: 27/27 literature cells PASS across zones, materials, and min/typ/max rate bounds
- S2: per-vessel backtest with hit-rate, MAE, RMSE and class-level Brier scores — plus honest signal-to-noise caveats where template data is the limiting factor, not the engine
A validation framework that can’t show a failure is just report generation. Ours renders failures in red on the dashboard and explains them in the artifact.
Built to be re-run by someone who doesn’t trust you
That’s deliberate. The operational surface is small enough to audit:
python -m mvf generate --cases 50 --seed 42 # S0
python -m mvf run --stage S1 # S1
python -m mvf run --stage S2 # S2
python -m mvf ingest --campaign <dir> # S3 — licensed campaigns
python -m mvf report # master report
python -m mvf serve # dashboard at :8765
Backed by a 132-test suite covering golden scoring values, per-band engine conformance, the hold-out protocol, provenance determinism, and the ingest validator. CI-gateable, because reproducibility claims without a gate are marketing.
What this means for you
- Procurement: vendor claims become reproducible artifacts instead of slide decks
- Integrity teams: a standing, auditable answer to “how do we know the model is right?”
- Regulators & clients: evidence trails with hashes, not adjectives
S3 is where this becomes part of your operating cycle: licensed field campaigns arrive through the NDA-ready data license template and the versioned campaign schema, run through the same scored pipeline, and land in the same provenance-stamped report. When the data is under contract, the evidence is a command away.
Bottom line
Model validation has three failure modes: it’s not done, it’s done once and forgotten, or it can’t be independently reproduced. MVF is designed against all three — staged evidence, deterministic artifacts, and a provenance block that makes “I ran it myself” the default posture of every stakeholder who sees it.
Model Validation Framework
MVF 1.0.2 · git fd39e57 · engine commit pinned-checkout (no git) · stamp unpinned-timestamp
33/50 pass
27/27 cells PASS
3 cases · 0 TTF hits · MAE 1.45 mm · 0 excluded
S0 · synthetic correctness
| case_id | type | true_final_mm | predicted_mm | error_mm | criterion | result |
|---|---|---|---|---|---|---|
| S0-C000 | constant | 7.78 | 7.78 | -0.0 | |err|<=0.1mm | PASS |
| S0-C001 | noisy | 9.63 | 9.819 | 0.189 | |err|<=2σ (0.3mm) | PASS |
| S0-C002 | step | 3.917 | 5.8 | 1.883 | invariant-only | INVARIANT-OK |
| S0-C003 | constant | 9.29 | 9.29 | 0.0 | |err|<=0.1mm | PASS |
| S0-C004 | noisy | 8.3 | 8.252 | -0.048 | |err|<=2σ (0.3mm) | PASS |
| S0-C005 | step | 3.965 | 4.7 | 0.735 | invariant-only | INVARIANT-OK |
| S0-C006 | constant | 5.76 | 5.76 | 0.0 | |err|<=0.1mm | PASS |
| S0-C007 | noisy | 9.82 | 10.012 | 0.192 | |err|<=2σ (0.3mm) | PASS |
| S0-C008 | step | 9.536 | 9.536 | 0.0 | invariant-only | INVARIANT-OK |
| S0-C009 | constant | 9.89 | 9.89 | 0.0 | |err|<=0.1mm | PASS |
| S0-C010 | noisy | 5.67 | 5.636 | -0.034 | |err|<=2σ (0.3mm) | PASS |
| S0-C011 | step | 7.092 | 7.566 | 0.474 | invariant-only | INVARIANT-OK |
| S0-C012 | constant | 9.69 | 9.69 | 0.0 | |err|<=0.1mm | PASS |
| S0-C013 | noisy | 13.04 | 13.135 | 0.095 | |err|<=2σ (0.3mm) | PASS |
| S0-C014 | step | 8.214 | 8.514 | 0.3 | invariant-only | INVARIANT-OK |
| S0-C015 | constant | 8.44 | 8.44 | 0.0 | |err|<=0.1mm | PASS |
| S0-C016 | noisy | 6.73 | 6.696 | -0.034 | |err|<=2σ (0.3mm) | PASS |
| S0-C017 | step | 2.864 | 4.7 | 1.836 | invariant-only | INVARIANT-OK |
| S0-C018 | constant | 9.22 | 9.22 | 0.0 | |err|<=0.1mm | PASS |
| S0-C019 | noisy | 8.41 | 8.183 | -0.227 | |err|<=2σ (0.3mm) | PASS |
| S0-C020 | step | 7.675 | 7.774 | 0.099 | invariant-only | INVARIANT-OK |
| S0-C021 | constant | 8.97 | 8.97 | 0.0 | |err|<=0.1mm | PASS |
| S0-C022 | noisy | 5.12 | 5.141 | 0.021 | |err|<=2σ (0.3mm) | PASS |
| S0-C023 | step | 4.708 | 5.415 | 0.707 | invariant-only | INVARIANT-OK |
| S0-C024 | constant | 9.66 | 9.66 | 0.0 | |err|<=0.1mm | PASS |
| S0-C025 | noisy | 8.23 | 8.339 | 0.109 | |err|<=2σ (0.3mm) | PASS |
| S0-C026 | step | 7.688 | 8.43 | 0.742 | invariant-only | INVARIANT-OK |
| S0-C027 | constant | 8.68 | 8.68 | 0.0 | |err|<=0.1mm | PASS |
| S0-C028 | noisy | 9.12 | 9.106 | -0.014 | |err|<=2σ (0.3mm) | PASS |
| S0-C029 | step | 9.592 | 9.592 | 0.0 | invariant-only | INVARIANT-OK |
| S0-C030 | constant | 10.21 | 10.21 | 0.0 | |err|<=0.1mm | PASS |
| S0-C031 | noisy | 7.81 | 7.675 | -0.135 | |err|<=2σ (0.3mm) | PASS |
| S0-C032 | step | 8.69 | 8.746 | 0.056 | invariant-only | INVARIANT-OK |
| S0-C033 | constant | 9.67 | 9.67 | 0.0 | |err|<=0.1mm | PASS |
| S0-C034 | noisy | 9.37 | 9.49 | 0.12 | |err|<=2σ (0.3mm) | PASS |
| S0-C035 | step | 8.755 | 8.713 | -0.042 | invariant-only | INVARIANT-OK |
| S0-C036 | constant | 6.18 | 6.18 | -0.0 | |err|<=0.1mm | PASS |
| S0-C037 | noisy | 8.19 | 8.23 | 0.04 | |err|<=2σ (0.3mm) | PASS |
| S0-C038 | step | 5.048 | 5.5 | 0.452 | invariant-only | INVARIANT-OK |
| S0-C039 | constant | 6.19 | 6.19 | 0.0 | |err|<=0.1mm | PASS |
| S0-C040 | noisy | 9.85 | 9.983 | 0.133 | |err|<=2σ (0.3mm) | PASS |
| S0-C041 | step | 10.364 | 10.51 | 0.146 | invariant-only | INVARIANT-OK |
| S0-C042 | constant | 12.37 | 12.37 | -0.0 | |err|<=0.1mm | PASS |
| S0-C043 | noisy | 11.41 | 11.135 | -0.275 | |err|<=2σ (0.3mm) | PASS |
| S0-C044 | step | 9.685 | 9.728 | 0.043 | invariant-only | INVARIANT-OK |
| S0-C045 | constant | 9.69 | 9.69 | 0.0 | |err|<=0.1mm | PASS |
| S0-C046 | noisy | 11.77 | 11.439 | -0.331 | |err|<=2σ (0.3mm) | FAIL |
| S0-C047 | step | 8.045 | 8.255 | 0.21 | invariant-only | INVARIANT-OK |
| S0-C048 | constant | 10.92 | 10.92 | 0.0 | |err|<=0.1mm | PASS |
| S0-C049 | noisy | 9.25 | 9.229 | -0.021 | |err|<=2σ (0.3mm) | PASS |
S1 · literature calibration
| zone | material | label | rate mm/yr | predicted_loss | expected_loss | pass |
|---|---|---|---|---|---|---|
| atmospheric | carbon_steel | min | 0.02 | 0.5 | 0.5 | PASS |
| atmospheric | carbon_steel | typ | 0.08 | 2.0 | 2.0 | PASS |
| atmospheric | carbon_steel | max | 0.2 | 5.0 | 5.0 | PASS |
| atmospheric | stainless_316L | min | 0.005 | 0.125 | 0.125 | PASS |
| atmospheric | stainless_316L | typ | 0.02 | 0.5 | 0.5 | PASS |
| atmospheric | stainless_316L | max | 0.05 | 1.25 | 1.25 | PASS |
| splash | carbon_steel | min | 0.1 | 2.5 | 2.5 | PASS |
| splash | carbon_steel | typ | 0.3 | 7.5 | 7.5 | PASS |
| splash | carbon_steel | max | 0.5 | 9.0 | 9.0 | PASS |
| splash | stainless_316L | min | 0.01 | 0.25 | 0.25 | PASS |
| splash | stainless_316L | typ | 0.03 | 0.75 | 0.75 | PASS |
| splash | stainless_316L | max | 0.08 | 2.0 | 2.0 | PASS |
| immersion | carbon_steel | min | 0.05 | 1.25 | 1.25 | PASS |
| immersion | carbon_steel | typ | 0.15 | 3.75 | 3.75 | PASS |
| immersion | carbon_steel | max | 0.4 | 9.0 | 9.0 | PASS |
| immersion | stainless_316L | min | 0.005 | 0.125 | 0.125 | PASS |
| immersion | stainless_316L | typ | 0.02 | 0.5 | 0.5 | PASS |
| immersion | stainless_316L | max | 0.06 | 1.5 | 1.5 | PASS |
| ballast_tank | carbon_steel | min | 0.05 | 1.25 | 1.25 | PASS |
| ballast_tank | carbon_steel | typ | 0.2 | 5.0 | 5.0 | PASS |
| ballast_tank | carbon_steel | max | 0.6 | 9.0 | 9.0 | PASS |
| subsea | carbon_steel | min | 0.02 | 0.5 | 0.5 | PASS |
| subsea | carbon_steel | typ | 0.08 | 2.0 | 2.0 | PASS |
| subsea | carbon_steel | max | 0.2 | 5.0 | 5.0 | PASS |
| subsea | stainless_316L | min | 0.002 | 0.05 | 0.05 | PASS |
| subsea | stainless_316L | typ | 0.01 | 0.25 | 0.25 | PASS |
| subsea | stainless_316L | max | 0.03 | 0.75 | 0.75 | PASS |
S2 · survey-record backtest
| case_id | rate_fit mm/yr | predicted_mm | true_mm | error_mm | pred_ttf_yr | true_ttf_yr | ttf_hit | pred_class | true_class |
|---|---|---|---|---|---|---|---|---|---|
| S2-MV-ATLANTIC-VOYAGER | 2.02303 | 19.943 | 21.482 | -1.539 | 8.37 | 12.23 | MISS | good | good |
| S2-MV-OCEAN-PIONEER | 1.16351 | 18.454 | 17.445 | 1.009 | 14.03 | 12.55 | MISS | good | good |
| S2-MV-SEA-EXPLORER | 0.80639 | 14.553 | 16.361 | -1.808 | 13.07 | 27.37 | MISS | good | good |
S3 · field validation
PENDING CONTRACT — no licensed field data held yet. Schema + NDA template ready; run python -m mvf ingest --campaign <dir> when campaigns arrive.
