Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Introduction

What the Tennessee Eastman Process is

The Tennessee Eastman Process is the chemical engineering profession's standard open benchmark for process control and for fault detection. Downs and Vogel published it in 1993 as a challenge problem: a model of an industrial chemical plant whose components are identified only by the letters A to H, distributed as Fortran so that competing control schemes and competing detectors could be compared on identical dynamics rather than on identical prose descriptions. Thirty years of published results rest on that code.

The plant makes two liquid products, G and H, from four gaseous feeds, A, C, D and E, with an inert B and a liquid byproduct F. Four irreversible, exothermic gas-phase reactions run in the reactor's vapour space (teprob.f:503-528):

1:  A + C + D -> G          2:  A + C + E -> H
3:  A + E     -> F          4:  3 D       -> 2 F

B takes part in none of them. It arrives with the mixed feed and leaves only through the purge, which is the entire reason the plant has a purge. Around those reactions sit five unit operations, described one page each later in this book: a feed mixing zone, the reactor with its cooling coil and agitator, a condenser and vapour-liquid separator, a compressor with a recycle loop, and a steam stripper. The whole plant is fifty integrated states (teprob.f:24-26), forty-one measurements, twelve manipulated variables, and twenty programmed disturbances (teprob.f:340, which loops DO 500 I=1,20).

Those five units, the four feeds and the thirteen streams between them are the whole plant, and it is worth seeing them laid out once before reading anything else. The diagram below was drawn from teprob.f rather than from the paper's figure, so every stream carries both of the numbers it goes by: the FTM index the source uses, and, where one exists, the stream number Downs and Vogel print. Those two disagree on three of the four feeds, which is the most common way to wire up a reimplementation incorrectly.

Flowsheet of the Tennessee Eastman ProcessFour feeds enter a plant of five unit operations. A mixing zone receives the A, D and E feeds, the compressor recycle and the stripper overhead, and discharges to the reactor. Reactor effluent passes through a condenser into the vapour-liquid separator. Separator vapour is compressed and recycled, with a purge bleeding the inert B. Separator liquid falls to a steam stripper, which is also fed the mixed A and C feed at its base and which makes the liquid product. Each stream carries its Fortran FTM index and the paper stream number, and each unit carries the XMEAS and XMV tags that sit on it. stream 1 · A feed FTM(3), XMV(3), XMEAS(1) stream 2 · D feed FTM(1), XMV(1), XMEAS(2) stream 3 · E feed FTM(2), XMV(2), XMEAS(3) mixing zone stream 6 AT FTM(6), FTM(7) reactor cw FTM(8) · stream 7 condenser cw separator AT FTM(10) · stream 9 purge, XMV(6), XMEAS(10) K FTM(9) · stream 8 · recycle · XMEAS(5) compressor XMEAS(20) work, XMV(5) recycle valve XMEAS(6) reactor feed rate XMEAS(16) pressure published as stripper pressure XMEAS(7) reactor pressure XMEAS(8) reactor level XMEAS(9) reactor temperature XMEAS(21) cooling water out T XMV(10) cooling water flow XMV(12) agitator speed XMEAS(11) separator temperature XMEAS(12) separator level XMEAS(13) separator pressure XMEAS(14) underflow rate XMV(7) underflow valve XMV(11) condenser cooling water XMEAS(22) condenser water out T FTM(11) stream 10 stripper FTM(12) FTM(5) · stream 5 · stripper overhead steam XMEAS(15) stripper level XMEAS(18) stripper temperature XMEAS(19) steam flow XMV(9) steam valve XMEAS(17) product rate XMV(8) product valve AT FTM(13) · stream 11 · product stream 4 · A and C feed FTM(4), XMV(4), XMEAS(4) enters the stripper base, not the mixing zone Composition analysers, mole % XMEAS(23-28) A to F, stream 6 reactor feed, 0.1 h XMEAS(29-36) A to H, stream 9 purge, 0.1 h XMEAS(37-41) D to H, stream 11 product, 0.25 h each reports its previous sample FTM(12), the stripper downflow, has no paper number
The Tennessee Eastman Process as teprob.f wires it. Solid lines are the thirteen internal streams, each labelled with its Fortran FTM index and, where one exists, the stream number of the paper. Dashed lines are utilities: cooling water through the reactor coil and the condenser, and steam to the stripper reboiler. Beside each vessel are the XMEAS instruments and XMV valves that sit on it, and the three AT bubbles mark the streams the composition analysers sample.

Two properties make it a good benchmark and a hard one. It is open-loop unstable, so a run with the valves held still does not merely drift, it trips: this port measures that trip at 3.060 simulated hours on reactor pressure (LOG entry B-0041). And its composition analysers report their previous sample rather than the current one (teprob.f:711-761), so the measurements a controller sees carry real dead time.

What this port is

tepsim is a pure-Rust reimplementation of that Fortran, ported from the original source rather than from any later reimplementation. The reference material is the Braatz group release, vendored unmodified under reference/: teprob.f, 1594 lines holding the plant model and eight utility routines, and temain_mod.f, 1413 lines holding the Euler driver and the decentralised PI control suite.

Two design commitments shape everything. The first is that the port is checked against the Fortran continuously rather than at the end: a development-only crate, tepsim-oracle, compiles the unmodified Fortran with gfortran and links it into the test binary, so a Rust test can force both implementations into the same state and compare them in the same process. The second is determinism. The core crate is no_std, forbids unsafe, uses no f32, no SIMD, no clock and no randomness outside the model's own generator, and answers exp, pow and ln from a vendored pure-Rust libm, so that the same scenario produces the same bits on x86-64, on aarch64 and in a browser.

Every place the port knowingly departs from the original is written down, with its class, its measured effect and the test that measured it, in the quirk and delta register.

The headline result

A complete 48-hour closed-loop run, 172,800 integrator steps at a one-second step, is bit-identical to the Fortran in all 41 measurements and all 12 manipulated variables, at every one of those steps, when both are given the same exp and pow.

That measurement is from the LOG.org entry for B-0041, "Phase 4 acceptance: the driver's full 48-hour run", with gfortran 15.2.0:

libmworst XMEASworst XMV (fraction of range)within XNS
platform0048.000 h
vendored1.705e-10 at XMEAS(1)1.246e-11 at XMV(10)48.000 h

The two rows are the two halves of one claim. Under libm-system, where the transcendental functions are the ones gfortran itself calls, the port and the original agree exactly: not to a tolerance, to the bit. Under the vendored libm that actually ships, the only difference is transcendental rounding, and after two simulated days it has not reached a tenth of a billionth of any instrument's noise standard deviation.

The size of that transcendental difference is measured too. The first transcendental call in the model is the Antoine vapour pressure at teprob.f:485 and teprob.f:488; over the whole range of arguments this model reaches, the vendored libm and gfortran's disagree on 9.945% of them, by exactly one ULP each time (LOG entry B-0018). Everything downstream of a condensible partial pressure therefore carries about 1.1e-16 of relative difference that no care in the algebra removes. Rather than accept a tolerance and lose the sharper claim, every differential test runs twice, once against each libm, and the second run is held to zero ULP.

Since B-0052 that claim attaches to the public API and not only to the test harness: tepsim::Simulation reproduces the validated loop bit for bit over the nominal scenario and IDV(1), IDV(6), IDV(13) and IDV(20), on all 60 samples and all 53 channels, in both libm configurations.

Run it in the browser first

TEP Studio is this simulator compiled to WebAssembly, running entirely in your browser with no server and nothing to install. Start the plant, switch a disturbance on, and watch the controllers fight it. Every scenario is shareable as a URL, because the whole scenario is in the link.

It is the same code the rest of this book documents, not a demonstration model: the page prints a determinism digest that matches the one the native build computes, and apps/studio/node/deployed.test.mjs asserts that on the module that actually ships.

How to read this book

Getting started is the working Rust API and the command line tool, and is the fastest way to a CSV of plant data. The Python package is the same ground for the tepsim wheel, with installation, a quickstart and the API reference, and the four tutorials are the narrative around the executed notebooks in notebooks/. The plant and the right-hand side explain how the fifty states, the thirteen internal streams and the six hundred lines of TEFUNC are organised here. The five unit-operation pages carry the governing equations, the variable tables and the teprob.f line ranges they were derived from. Validation is the ten-tier ladder and the numbers actually measured at each rung. Status says what is finished and what is not, which for a port in progress is the page to read second.

Every claim about the model in this book cites the teprob.f or temain_mod.f line range it came from, so a sceptical reader can check it against the vendored source. Every validation number cites the LOG.org iteration that measured it.

Status

Everything planned is built. Every phase has landed, every validation tier has a harness, and the four decisions that were once open have been taken. The backlog is 85 items done, none open and none blocked.

That makes this page shorter than it used to be, and its job is now to say where the limits are rather than where the edge of construction is. Numbers here come from the current-state block of BACKLOG.org and the closing entries of LOG.org, and each names the iteration that measured it.

What exists

The plant model, the control layer, the public API, the command line, the Python wheel and the browser app are all complete and validated. tepsim-core ports the whole of TEFUNC (teprob.f:196-816); tepsim-control ports the twenty control loops and the driver's own scheduling; tepsim is the API most callers want; tep drives it from a terminal, including tep dataset for generating d00-d21 shaped files; the wheel carries no C dependency; and TEP Studio runs the whole simulator in a browser tab with no server.

The differential harness is tepsim-oracle, which compiles and links the unmodified Fortran, and tepsim-stats, which implements every statistic the ladder needs in Rust with known-answer tests, so no part of the validation depends on numpy or scipy.

All ten tiers have a harness and all ten have run. Tier 8, differential fuzzing with shrinking, and Tier 9, cross-platform determinism, were the last two and landed together. The validation chapters are generated from the suite's own output for the tiers that have a generator; the narrative in Validation is hand-written and transcribed from LOG.org, and each number there names the iteration that measured it so the transcription can be checked.

What is genuinely still missing

These are limits, not unfinished work, and most of them are limits of the machine this was built on rather than of the code.

Tier 9 has no x86-64 leg and no real-browser leg. Six committed digests are identical on aarch64 and on wasm32 under Node, across three build profiles, and the browser app's transport path reproduces them independently. Nobody has run them on x86-64, Windows or aarch64 Linux, and Node is not a browser. The table is committed constants rather than a value computed twice in one process, precisely so that running cargo xtask tier9 on another machine completes the claim with no code change.

Tier 8 has one open counterexample. In five million tuples, fuzz#863105 misses the 1e-12 gate at 4.607e-12 of the scale of its terms. It is recorded and attributed rather than fixed, because there is nothing to fix: it is an accepted one-ULP exp difference amplified by 973 simulated hours of IDV(13)'s kinetic drift, and it is bit-identical under libm-system.

Two IDV faults miss the Tier 5 margin under the vendored libm. IDV(14) and IDV(19), on exactly the valves those faults stick, and bit-identical under the platform libm. The cause is teprob.f:801, a discontinuous branch on a floating-point comparison. Those valves are judged on their distribution rather than on a mean, because a series of plateaux has no meaningful centre. No margin was widened.

Apache Arrow and Parquet sinks are not implemented. The recorder sinks that exist are Columnar, Csv, Ring, Decimating and Selecting, and Simulation::run_into streams into one rather than collecting. Arrow's dependency cost was judged not to be worth paying until someone has a dataset large enough that CSV is the bottleneck; the right home would be tepsim-cli, never tepsim, which is no_std and compiles to wasm32 under a size budget.

A historian export does not exist. Everything is wide tabular. A long-format tag,timestamp,value,quality record would need a caller-supplied epoch, since the core may not read a clock, a decision about what quality code a dead-time analyser and a frozen channel deserve, and units promoted from prose in measurements.rs to data on a channel.

The step size is not adapted. Three integrators exist as of B-0053: fixed step explicit Euler, classical RK4, and Dormand-Prince 5(4) with an embedded error estimate. Only Euler reproduces the original, and it is the default. A variable step changes when the discrete phases run, which is a decision about fidelity rather than about numerics, so it has not been taken.

That comparison produced a result about the original rather than about the port. RK4 and Dormand-Prince agree with each other to 1.5e-6 while both differ from Euler by about 1.1e-2, so the published Tennessee Eastman data carries roughly one percent of integration error against an accurate solution of the same equations. Reproducing that is the point, but it means "the TEP" names a particular discretisation and not only a set of differential equations.

The two Class C quirks, and what a default Scenario now does

Both were signed off on 2026-08-28, so a default Scenario fixes them and Scenario::faithful() reproduces them.

A trip ends the run (delta D-007, teprob.f:807-811). The original freezes the plant and keeps reporting, which is where four of the forty-four published files get their frozen tails: 1,832 rows in total, and 363 of d06.dat's 480. The fix is pure truncation, since every sample before the trip is bit-identical either way, and the argument for keeping the freeze was that it preserved an option it does not preserve, because the plant cannot be restarted in either case.

The driver does not force IDV(12) at hour eight (delta D-011, temain_mod.f:366-368). Tier 7 established that the published files were generated with that line replaced rather than kept: every dNN_te except d12_te sits at the nominal operating point straight across row 160.

Every comparison against the Fortran or against published data runs Scenario::faithful(), and tier5::run_port pins it so no differential can lose it. From the command line the flags are tep run --faithful, or --force-idv12 and --freeze-on-trip individually.

Getting started

This page is the Rust API and the command line tool. If you want to drive the simulator from Python, The Python package is the equivalent page for the tepsim wheel, and the tutorials and the notebooks they link to are all Python.

Building

The workspace pins its toolchain in rust-toolchain.toml, so a rustup install picks the right compiler on its own. Nothing in the shipping crates needs Fortran; gfortran is required only to build tepsim-oracle, the development-only differential harness, which sits behind the oracle feature and is never a dependency of tepsim.

$ cargo build --release
$ cargo xtask ci          # fmt, clippy, tests, docs, cargo-deny

The library

The whole public surface is three types. A Scenario describes a run, a Simulation performs one, and a Run holds the output as columns.

use tepsim::{Scenario, Simulation};

// Twenty-four hours with disturbance IDV(4), the reactor cooling water
// inlet temperature step.
let run = Simulation::new(Scenario::fault(4).with_hours(24.0)).run();

// One measurement across the run, one-based as XMEAS(n) is.
let reactor_pressure = run.measurement(7);

// One manipulated variable, one-based as XMV(n) is.
let coolant_valve = run.manipulated(10);

assert!(run.outcome.is_completed());

Scenario is a plain Copy struct, so a caller can build one, tweak it, and keep both. Its builders are with_seed, with_hours, sampling_every, with_fault and open_loop, and the two constructors are Scenario::baseline() and Scenario::fault(n).

FieldDefaultMeaning
seed4651207995the generator word compiled into teprob.f:1187
hours48.0NPTS = 172800 at a one-second step, the run temain_mod.f was written to do
step_hours1/3600the step the original's INTGTR uses
sample_every180temain_mod.f:401 writes every 180 seconds, and d00 through d21 are at that spacing
disturbancesall offtwenty flags, one-based as IDV(n) is
controlledtrueclosed loop under the published control scheme, or open loop with the valves held
quirksall fixedwhich Class C quirks are fixed rather than reproduced
driver_forces_idv12falsewhether the driver switches IDV(12) on at hour eight, as temain_mod.f:366-368 does

Three of those defaults deserve a sentence. Open loop is not a useful operating mode, it is a diagnostic one: the plant trips on reactor pressure after about three hours, and the difference between the two settings is the clearest single statement of what the control layer does.

The other two are the Class C quirks, signed off on 2026-08-28 and fixed by default: a trip ends the run (delta D-007) and the driver does not force IDV(12) (delta D-011). Both are one call away. Scenario::faithful() gives the original's behaviour on both, and it is what every comparison against the Fortran or against published data runs. See the delta register for the evidence behind each.

A Run holds samples, each carrying step, hours, measurements (XMEAS(1..41)), manipulated (XMV(1..12)) and labels. Run::column, Run::columns, Run::measurement and Run::manipulated reshape it into the columns a statistic or a detector wants, and Sample::row gives all 53 channels in one array, measurements first, which is the layout every downstream consumer uses. channel_names() returns matching names in the same order.

Run::outcome is how a run ended: Completed, Tripped with the step, the hour and the first shutdown condition that fired, or SolveFailed with the step at which a temperature solve failed to converge. A trip ends the run by default, so a tripped run is shorter than its scenario planned. teprob.f:807-811 instead freezes the plant and keeps reporting, which is what Scenario::faithful() gives and what the constant tails in d06 and d18 are.

Labels is ground truth: which disturbances were active at that instant, and how long each had been. The original records nothing of the sort, so every detection-delay figure in the literature is computed against an onset the author assumed.

Stepping a run by hand

Simulation::run() is the whole scenario in one call. For an online detector, a live display or a reinforcement learning loop, Simulation::step() advances one integrator step and hands back a Sample on the steps where one is due.

use tepsim::{Scenario, Simulation};

let mut sim = Simulation::new(Scenario::baseline().with_hours(2.0));
while let Some(sample) = sim.step() {
    println!("{:.3} h  pressure {:.1}", sample.hours, sample.measurements[6]);
}

The order inside a step is temain_mod.f's: force IDV(12) if it is due, run the controllers on the previous step's measurements, integrate, then clamp. That one plant step of dead time in every loop is delta D-010, and getting it backwards leaves XMEAS(14) 23% out after four hours.

The command line

$ tep run --fault 4 --hours 24 --labels

tep has four subcommands, run, dataset, faults and help, and run takes these flags:

FlagDefaultEffect
--fault <1-20>noneinject a disturbance
--hours <h>48simulated duration
--seed <n>4651207995generator word
--every <steps>180sample every N steps, so three minutes at the default step
--open-loopoffhold the valves instead of controlling
--force-idv12offswitch IDV(12) on at hour eight whatever was asked for, as temain_mod.f:367 does
--freeze-on-tripoffon a trip, freeze the plant and keep reporting rather than ending the run, as teprob.f:807-811 does
--faithfuloffboth of the above: reproduce every Class C quirk
--labelsoffinclude the ground-truth columns

tep faults prints the twenty disturbances with the shape of each and the teprob.f line it acts on; the same table appears in The twenty disturbances.

Output is CSV on stdout, with progress and the outcome on stderr, so that redirecting stdout to a file gives a clean file. The header is step,hours followed by the 53 channel names, plus fault and hours_since_onset when --labels is given. Values are written with seventeen significant digits, which round-trips an f64 exactly, so a CSV written here is reproducible rather than approximately reproducible.

A trip is reported on stderr and exits successfully, because a trip is a result rather than a malfunction; the message says whether the run ended there or the plant froze and carried on. Only a failed temperature solve is an error exit.

Generating a dataset

$ tep dataset --out ./data
$ tep dataset --set all --faults 0,4,6 --format csv --out ./small
$ tep dataset --list

tep dataset writes files with the same geometry as the published d00 through d21: 960 rows of 52 columns for the _te half, 480 (500 for d00) for the training half, at the seeds teprob.f:1187-1256 records, in the same fixed sixteen-column layout the shipped files use. --set chooses te (the default), training or all; --faults takes all or a comma list; --format is dat or csv, the latter at full precision with a header.

It is emphatically not the published data, and it says so on stderr every run. Four things stand in the way and none is a matter of trying harder: the toolchain that made the shipped files is unrecorded and its exp was not this one's, the output was rounded to five significant figures before anyone saw it, IDV(21) is not in this revision of the model so d21 cannot be generated at all, and the protocol behind the twenty-two training files is written down nowhere. Tier 7 measures the gap rather than closing it.

Reproducibility

A run is a pure function of its Scenario. There is no clock, no thread-local state and no global, so the same scenario gives the same bits on x86-64, aarch64 and wasm32. That is what makes a recorded dataset reproducible from its description rather than from a file, and it is the property Tier 9 will assert across the CI matrix.

The Python package

The simulator ships as a Python package called tepsim. It is not a reimplementation and not a wrapper around a subprocess: it is the same Rust core the rest of this book documents, compiled into an extension module with PyO3. There is no C in the build, no Fortran, and no runtime dependency except NumPy.

A run started from Python is bit-identical to the same run started from Rust or from the browser, because all three call the same code with the same scenario. That is what makes the worked examples in the notebooks usable as evidence rather than as illustrations.

Installing it

It is not on PyPI. The version is still 0.0.0 and no release tag has been pushed, so nothing has ever been published. The publish job in .github/workflows/wheels.yml is wired up and gated on a v* tag, and the name is free, so pip install tepsim will be the line once there is a release. It is not the line today.

Install it from the repository instead:

$ pip install "git+https://github.com/jkitchin/tep-rust#subdirectory=crates/tepsim-py"

Two things about that command, both of which will bite otherwise.

It builds from source, so you need a Rust toolchain. There is no published wheel to fall back on, so pip clones the repository, invokes maturin, and compiles the whole core in release mode. That takes a couple of minutes on a warm machine and it fails partway through if cargo is not on your PATH. Install Rust from rustup.rs first. NumPy is pulled in automatically as a declared dependency, so nothing else is needed.

The quotes are not optional. Most shells treat # as the start of a comment, and without the quotes the #subdirectory= fragment is silently dropped, at which point pip tries to build the workspace root and fails with an error that does not mention the fragment at all.

The result is a cp39-abi3 wheel, so one build covers every GIL-enabled CPython from 3.9 upwards. Free-threaded interpreters have no stable ABI to target and need a version-specific build; 3.14t is the earliest that works, because PyO3 0.29 does not support the free-threaded build of anything below 3.14.

From a checkout

If you have the repository already, cargo xtask python is the supported path. It builds a release wheel, creates a throwaway virtualenv at .xtask-python/venv, installs the wheel and NumPy into it, proves that import tepsim resolves inside that virtualenv rather than somewhere else on the machine, and runs the binding's pytest suite against it.

$ cargo xtask python
$ .xtask-python/venv/bin/python -c "import tepsim; print(tepsim.__version__)"

That interpreter is the one the book's own tests use to check the quickstart transcript below. Note that cargo xtask python deletes and rebuilds the virtualenv every time it runs, so anything else installed into it, jupyter and matplotlib for the notebooks in particular, has to be reinstalled afterwards.

To build a wheel without installing it, maturin build --release -m crates/tepsim-py/Cargo.toml writes one and prints the path.

Quickstart

from concurrent.futures import ThreadPoolExecutor

import numpy as np

import tepsim as tep

print("tepsim %s: XMEAS(1..%d), XMV(1..%d), %d channels, IDV(1..%d)"
      % (tep.__version__, tep.MEASUREMENTS, tep.MANIPULATED, tep.CHANNELS,
         tep.DISTURBANCES))

# A Scenario says what to simulate, a Simulation does it, a Run holds what
# came out. That is the whole API.
run = tep.Simulation(tep.Scenario.baseline(seed=42, hours=48)).run()
print()
print("run:      %r" % run)
print("matrix:   %s %s" % (run.to_numpy().shape, run.to_numpy().dtype))
print("outcome:  %s" % run.outcome)
print("XMEAS(7): mean %.2f kPa over %.0f h"
      % (run.measurement(7).mean(), run.hours[-1]))

# The twenty disturbances say what they do, not only what the original header
# called them. Five of the published descriptions are the word "Unknown".
print()
print("the five the original leaves unexplained")
for fault in tep.faults():
    if fault.published == "Unknown":
        print("  IDV(%2d) %-9s %s" % (fault.index, fault.shape, fault.effect))

# Ground truth travels with the data, which the original records nowhere.
faulted = tep.Simulation(tep.Scenario.fault(1, hours=8)).run()
labels = faulted.labels()
print()
print("IDV(1) over 8 h: active at the last sample %s, %.2f h since onset"
      % (labels["active"][-1, 0], labels["since_onset"][-1, 0]))

# run() releases the GIL, so an ensemble is a thread pool and nothing has to be
# pickled.
sims = [tep.Simulation(tep.Scenario.fault(n, hours=8)) for n in range(1, 21)]
with ThreadPoolExecutor() as pool:
    runs = list(pool.map(tep.Simulation.run, sims))
print()
print("twenty 8-hour faulted runs: %d completed, %d tripped"
      % (sum(r.outcome == "completed" for r in runs),
         sum(r.outcome == "tripped" for r in runs)))

# A run is a pure function of its scenario, and a scenario is one line of text
# that parses back to an equal scenario.
scenario = tep.Scenario.fault(4, hours=8)
print()
print("digest:   %s" % scenario.digest)
print("text:     %s" % scenario.to_text())
print("parses back equal:      %s"
      % (tep.Scenario.from_text(scenario.to_text()) == scenario))
print("two runs bit-identical: %s"
      % np.array_equal(tep.Simulation(scenario).run().to_numpy(),
                       tep.Simulation(scenario).run().to_numpy()))
tepsim 0.0.0: XMEAS(1..41), XMV(1..12), 53 channels, IDV(1..20)

run:      <tepsim.Run 960 samples x 53 channels, 48.0 h, completed>
matrix:   (960, 53) float64
outcome:  completed
XMEAS(7): mean 2706.16 kPa over 48 h

the five the original leaves unexplained
  IDV(16) random    enables walk channel 9, the stripper steam valve capacity
  IDV(17) random    enables spike channel 10, the reactor coolant duty
  IDV(18) random    enables spike channel 11, the condenser coolant duty
  IDV(19) sticking  sticks valves 5, 7, 8 and 9; touches no equation in the model
  IDV(20) random    enables spike channel 12, the reactor outlet flow

IDV(1) over 8 h: active at the last sample True, 8.00 h since onset

twenty 8-hour faulted runs: 19 completed, 1 tripped

digest:   7f9accc04bb61e7f
text:     tepsim.scenario.v1;seed=4651207995;hours=8;step=2.777777777777778e-4;every=180;faults=4;controlled=1;idv12=0;trip=1;continuous=0;integrator=euler;events=
parses back equal:      True
two runs bit-identical: True

The API

Three classes carry the whole surface, and they divide the way the problem does. A Scenario is a description and holds no state. A Simulation is the machinery. A Run is the result.

Module level

NameValueMeaning
MEASUREMENTS41XMEAS(1..41)
MANIPULATED12XMV(1..12)
CHANNELS53a row: measurements then manipulated variables
DISTURBANCES20IDV(1..20)
DEFAULT_SEED4651207995.0the generator word compiled into teprob.f:1187
DEFAULT_STEP_HOURS1/3600one simulated second, the step INTGTR uses
DEFAULT_SAMPLE_EVERY180the three-minute spacing of d00 through d21
FORCED_DISTURBANCE_STEP28800the step the driver forces IDV(12) on at

channel_names() returns the 53 names in row order, so a CSV header and a matrix column cannot disagree about which is which. faults() returns the twenty Fault records.

Scenario

Immutable and cheap to copy, so a caller can build one, derive variants from it, and keep all of them.

There are four constructors: the bare Scenario(...), Scenario.baseline(...), Scenario.fault(n, ...) and Scenario.from_text(text). All but the last take the same keyword arguments, which are seed, hours, step_hours, sample_every, controlled, driver_forces_idv12 and trip_ends_the_run, plus faults on the bare one.

MemberWhat it is
seed, hours, step_hours, sample_everythe four numbers a run is measured in
controlledclosed loop under the published control scheme, or open loop with the valves held
driver_forces_idv12whether the driver switches IDV(12) on at hour eight, delta D-011
trip_ends_the_runwhether a shutdown stops the run, delta D-007
faultsthe active disturbances, one-based and ascending
steps, sampleshow many integrator steps and how many recorded rows
digesta content hash over everything affecting the run, sixteen hex characters
to_text(), from_text()the canonical one-line form, and its strict parser
with_seed, with_hours, with_fault, sampling_every, open_loopderive a variant

The two defaults worth knowing are that a trip ends the run and that the driver does not force IDV(12). Both are Class C quirks of the original that this port fixes by default and can reproduce on request; see the delta register.

Simulation

Simulation(scenario) holds a plant, a controller stack and an integrator state, all of it owned. The original keeps its whole working set in six Fortran COMMON blocks, which allows exactly one simulation per process and no reentrancy at all. This allows as many as there are threads.

run() runs the whole scenario and returns a Run. It runs a copy, so calling it twice gives two equal runs rather than one run and one empty one, and it never raises for a plant that misbehaves: a trip or a failed temperature solve is reported through Run.outcome, because a run that ended early is data.

run() releases the GIL for the entire integration, which is where all the time goes, so an ensemble is a ThreadPoolExecutor and not a ProcessPoolExecutor. Nothing has to be pickled, and the plant does no I/O and touches no Python object while the GIL is down.

Run

MemberWhat it is
to_numpy()the whole run, one (n_samples, 53) float64 array, C-contiguous
hours, stepssimulated time and integrator step per row
measurement(n), manipulated(n)one channel, one-based as XMEAS(n) and XMV(n) are
column(i), columns()one channel by zero-based position, or all 53 keyed by name
labels()ground truth: 'active' and 'since_onset', both (n_samples, 20)
outcome'completed', 'tripped' or 'solve_failed'
tripped_at, tripped_hours, trip_causewhere and why the plant shut down, or None
solve_failed_atthe step a temperature solve failed at, or None; delta D-001
scenariowhat was asked for

Every array is a read-only view over one buffer, which is filled once when the run finishes and moved into NumPy rather than copied. to_numpy() returns the same object every call, and measurement, manipulated, column and columns are strided views into it. Call .copy() for something writable, or numpy.ascontiguousarray if contiguity matters. columns() is a dict in channel order, which makes it a pandas.DataFrame constructor argument as it stands.

labels() is the part with no counterpart in the original. A published Tennessee Eastman dataset is a matrix and a filename, so every detection-delay figure in the literature is computed against whatever onset its author assumed. Here the onset is in the data.

Fault

faults() returns twenty of these. published is the description at teprob.f:172-191 verbatim, five of which say only "Unknown". effect is what the source actually does, which for those five is perfectly explicit: only the physical interpretation was withheld. shape is 'step', 'random' or 'sticking', line names the teprob.f line the fault acts on, and affects_the_plant is False for the three sticking faults, which touch no equation in the model at all. The same table is in The twenty disturbances.

What Scenario(...) cannot say

Three of a scenario's eleven fields are out of the constructors' reach: the event schedule, the continuous extension that allows a fault at a fraction of its full strength, and the choice of integrator. Scenario.from_text is the way to them from Python, and since the text is the scenario's real serialisation, the thing the digest is taken over and the browser app puts in a URL fragment, building through it is building in the form the run will be described by.

text = tep.Scenario.baseline(hours=18).to_text()
scheduled = tep.Scenario.from_text(text.replace("events=", "events=6:start:4,12:stop:4"))

repr() knows about this. A scenario the constructor can express is printed as a constructor call, because that is what is readable at a prompt, and anything else is printed as Scenario.from_text(...). Both round-trip, so eval(repr(s)) == s holds for either shape. The alternative, printing only the constructor's arguments, produced valid Python that evaluated to a different scenario with a different digest and said nothing about it.

notebooks/04-custom-scenarios.ipynb works through all three fields, with a helper that rebuilds the text line from a dictionary rather than patching it, so that a mistyped field name is an error rather than a silently different run.

What is not in the package

tepsim-stats, the crate the repository's own validation ladder runs its statistics on, is development-only. cargo xtask ci asserts that no shipped crate so much as names it, and it is therefore not reachable from Python. That is deliberate: the ladder's statistics have to be free of any dependency the port itself does not have, so they can never be the reason a validation number changed.

For monitoring work the notebooks use notebooks/pcamon.py instead, which is PCA, both monitoring statistics and their control limits in NumPy and the Python standard library alone, with no SciPy and no scikit-learn. Where the two could differ they agree: 02-fault-detection-pca.ipynb checks every printed digit of its detector against the Rust one.

Types

The package ships py.typed, and _tepsim.pyi beside __init__.py describes the compiled half, so mypy and an editor see the real signatures rather than Any.

Worked examples

The four notebooks in notebooks/ are where the package is actually used. They are executed and committed with their outputs and plots, and rendered copies are published beside this book:

Those four links resolve on the published site, where .github/workflows/pages.yml renders the notebooks beside the book with jupyter nbconvert. In a local mdbook build they point at files that are not there; the sources are notebooks/*.ipynb in the repository, and notebooks/README.md says how to run them.

The tutorials in this book are the narrative around them.

The twenty disturbances

IDV(1..20) is a bare integer array in the original, and the header at teprob.f:172-191 names each entry in prose. Nothing there connects a name to the line that implements it, and five of the twenty are called only "Unknown". This page makes the connection: every fault names the teprob.f line it acts on, where it is injected, and what shape it has.

There are twenty, not the twenty-one of the later literature. teprob.f:340 is DO 500 I=1,20. The extra one comes from later versions of the model, and this port follows the vendored source.

Three shapes, and the third is not a plant disturbance at all

Step faults change a feed condition the moment they are switched on and hold it. Seven of the twenty, at teprob.f:407-414, 567 and 568.

Random faults enable a walk channel through IDVWLK (teprob.f:347-358). Ten of the twenty, of which the last three drive spike trains rather than walks.

Sticking faults do not touch the model. They set IVST (teprob.f:793-798), which widens the dead band a valve command must cross before the valve follows it. Three of the twenty.

That third kind matters more than its size. A sticking fault is not a disturbance to the plant; it is a disturbance to the controller's authority over the plant. In an open-loop run, where the command never moves, it does nothing whatever, and a scenario engine that treated it as a plant fault would report an injected disturbance with no effect and look broken. Tier 4 confirmed this from a direction that could not have been arranged: over a four-hour run, IDV(14), IDV(15) and IDV(19) report worst errors identical to the nominal case to every digit, because with the command held still their trajectory is the nominal trajectory.

The table

IDVPublished description (teprob.f:172-191, verbatim)What the source doesShapeLine
1A/C Feed Ratio, B Composition Constant (Stream 4)steps the mixed feed's A fraction down by 0.03stepteprob.f:407
2B Composition, A/C Ratio Constant (Stream 4)steps B up by 0.005 and A down by 2.43719e-3, on two linesstepteprob.f:408-409
3D Feed Temperature (Stream 2)steps the D feed temperature up by 5 Cstepteprob.f:411
4Reactor Cooling Water Inlet Temperaturesteps the reactor coolant inlet up by 5 Cstepteprob.f:413
5Condenser Cooling Water Inlet Temperaturesteps the condenser coolant inlet up by 5 Cstepteprob.f:414
6A Feed Loss (Stream 1)shuts the A feed off entirely, not partiallystepteprob.f:567
7C Header Pressure Loss - Reduced Availability (Stream 4)reduces the mixed feed's capacity by 20%stepteprob.f:568
8A, B, C Feed Composition (Stream 4)enables two walk channels, on A and on Brandom, channels 1 and 2teprob.f:347-348
9D Feed Temperature (Stream 2)enables the D feed temperature walkrandom, channel 3teprob.f:349
10C Feed Temperature (Stream 4)enables the mixed feed temperature walkrandom, channel 4teprob.f:350
11Reactor Cooling Water Inlet Temperatureenables the reactor coolant inlet walkrandom, channel 5teprob.f:351
12Condenser Cooling Water Inlet Temperatureenables the condenser coolant inlet walkrandom, channel 6teprob.f:352
13Reaction Kineticsenables two walks, one per rate constant of reactions 1 and 2random, channels 7 and 8teprob.f:353-354
14Reactor Cooling Water Valvesticks valve 10; touches no equation in the modelstickingteprob.f:793
15Condenser Cooling Water Valvesticks valve 11; touches no equation in the modelstickingteprob.f:794
16Unknownenables walk channel 9, the stripper steam valve capacityrandom, channel 9teprob.f:355
17Unknownenables spike channel 10, the reactor coolant dutyrandom, spiking, channel 10teprob.f:356
18Unknownenables spike channel 11, the condenser coolant dutyrandom, spiking, channel 11teprob.f:357
19Unknownsticks valves 5, 7, 8 and 9; touches no equation in the modelstickingteprob.f:795-798
20Unknownenables spike channel 12, the reactor outlet flowrandom, spiking, channel 12teprob.f:358

The channel column is not decoration. A test asserts that this table agrees with the code that maps IDV flags to channel flags, that every one of the twelve channels is driven by exactly one fault, that the spiking flag is set exactly for channels 10 and above, and that exactly IDV(14), IDV(15) and IDV(19) fail to reach the plant. Two statements of one fact, which is the point.

The five "Unknown" faults are not unknown

The header calls IDV(16) through IDV(20) unknown, and every paper on TEP repeats it. The source is perfectly explicit about what they do; only their physical interpretation was withheld. They enter the model at these points:

FaultWhere it lands
IDV(16)the stripper steam valve capacity, UAC at teprob.f:572
IDV(17)the reactor coil duty, the drift factor at teprob.f:673
IDV(18)the condenser duty, the drift factor at teprob.f:676
IDV(19)sticks valves 5, 7, 8 and 9
IDV(20)the reactor outlet flow resistance, at teprob.f:582-583

So IDV(19) is a sticking fault and the other four are not, which the shared label hides. Three of the four are the spike channels, which is why they are reported in the literature as the hardest to detect: they are intermittent rather than sustained.

Nine walks and three spike trains

The twelve channels are not the same kind of object, which the shared array names hide (teprob.f:340-406).

Channels 1 to 9 are random walks. When one runs out, teprob.f:359-371 evaluates the old segment at its endpoint, takes the value and the slope there, and builds a segment that continues smoothly from them. TESUB5 (teprob.f:1506-1537) chooses the next knot value and slope from the uniform generator and fits a Hermite cubic; TESUB8 (teprob.f:1300-1359) evaluates the cubic at the current time.

Channels 10 to 12 are spike trains, and teprob.f:372-396 gives them their own rule. They alternate between two states. Dwelling: the channel sits at zero for a randomly drawn interval, its segment being a parabola rising from zero, and the dwell ends when the value reaches 0.1. Spiking: once the value exceeds 0.1, the channel is given a cubic through the current value and slope that lasts exactly 0.1 hours, with coefficients that drive it hard and then back down. So a spike channel is off, off, off, then briefly on.

The flag scales the dwell, not the schedule. CDIST(I) = IDVWLK(I) / h^2 at teprob.f:391 is the only place the flag enters a spike channel. With the disturbance off it is zero, the parabola is flat at zero, the channel never reaches 0.1, and it dwells forever, drawing a fresh interval each time. With it on, the parabola climbs and the channel eventually spikes. Either way it keeps drawing at the same rate.

That last property is load-bearing and is easy to optimise away. IDVWLK multiplies the two endpoint draws at teprob.f:1529-1530 but not the duration draw at teprob.f:1528, so an inactive walk channel still consumes all three draws. Skipping the two endpoint draws when the flag is zero produces identical segment values, because an inactive channel lands on SZERO with zero slope either way, and leaves the generator two steps behind. Every subsequent draw in the run then differs: the noise, the other channels, everything. That mutation was implemented deliberately to check the tests have teeth, and it was caught on the stream position rather than on any value. It is the entire argument for Tier 3 demonstrated at Tier 1.

Using them

$ tep faults                       # the table above, from the source
$ tep run --fault 4 --hours 24 --labels

or, from Rust, Scenario::fault(4) and Scenario::baseline().with_fault(4). Faults can be combined; the flags are independent.

One caveat, though it is no longer the default. The original driver switches IDV(12) on at eight hours whatever the scenario asked for (temain_mod.f:366-368). That is delta D-011, and it is off here: Tier 7 established that the published files were generated with that line replaced rather than kept, so a request for IDV(4) gets IDV(4) and nothing else. Scenario::faithful() and the driver_forces_idv12 field turn it back on, and the ground-truth labels record it either way, so a run carrying it is visibly fault-free for eight hours and then not.

A first run, and the 53 channels

The worked example is notebook 1, Getting started. It runs the plant, plots what normal operation looks like, walks the twenty faults, injects one, reads the ground truth back, checks reproducibility from a seed, and finishes with a trip. Source: notebooks/01-getting-started.ipynb.

The whole library is three objects. A Scenario says what to simulate, a Simulation does it, and a Run holds what came out. The notebook uses all three in its first cell; this page is the part of the story that is worth stating once in prose rather than re-deriving from a plot.

The examples are Python, and The Python package is how to install it. The simulator itself is Rust, and its Rust API is Getting started, but a run started from either binding is bit-identical, because both call the same core.

Why forty samples in two hours, and not 7200

The integrator takes one step per simulated second, so two hours is 7200 steps. A sample is written every 180 of them, which is the three-minute spacing temain_mod.f:401 writes at and the spacing of the published d00 through d21 files. Two hours is therefore forty rows, and a 48-hour run is 960.

The first sample of a run is at 0.0497 hours rather than at 0.05, and that is not an off-by-one. run.steps[0] on that row is 180, and run.hours[0] is the time at which step 180 began, which is 179 seconds. The simulated clock is advanced at the end of a step, after the row has been written, because that is the order temain_mod.f writes in, and a row that carried the post-step time would be labelled with a clock the plant had not reached when it was measured. The last row of a two-hour run is at 1.9997 hours for the same reason. If you want the initial condition itself, it is the model's nominal state and not a row of the run.

What the 53 channels are

A row is the plant as an operator sees it: 41 measurements and then the 12 valve positions the controllers are holding. run.to_numpy() returns exactly that as one (n_samples, 53) array, measurements first, and channel_names() returns names in the same order, so a CSV header and a matrix column can never disagree about which is which.

The 41 measurements split into two groups that behave quite differently.

XMEAS(1) through XMEAS(22) are continuous instruments: flows, pressures, levels, temperatures, the compressor work. They are read every step and carry Gaussian measurement noise whose standard deviation is the XNS table in teprob.f.

XMEAS(23) through XMEAS(41) are the three gas chromatographs, and they are not instruments in the same sense at all. They sample on a schedule, take time to run, and then hold the answer until the next result arrives. The notebook plots this, and it is the single most surprising thing about the data the first time you see it: a composition channel is a staircase where a flow meter is a curve. The reactor feed and purge analysers run every 0.1 hours and the product analyser every 0.25, so at the three-minute output cadence each answer appears twice or five times. A detector that treats those repeats as independent observations is counting the same measurement several times, and that is worth knowing before it produces a p-value.

Both 0.1 literals in teprob.f are single precision, so the gas interval is really 0.10000000149011612 and a step landing on exactly 0.1 does not sample. That is faithfully reproduced here; see the delta register.

XMV(1) through XMV(12) are the manipulated variables. Eleven of them move. XMV(12), the agitator speed, sits at 50 forever, because the published control scheme never touches it. A statistical model fitted to all 53 channels has to cope with that constant column, and the detector tutorial shows what a PCA model has to do about it.

Getting at the data

run.to_numpy() is the record itself, one read-only (n_samples, 53) array of float64. Three views on top of it cover most uses. run.measurement(n) and run.manipulated(n) take the one-based indices of the original, so run.measurement(7) is XMEAS(7) and needs no mental arithmetic. run.column(i) takes the zero-based row position, and run.columns() returns all 53 at once keyed by name, which is the shape a covariance matrix, a chart or a pandas.DataFrame wants. Alongside them, run.hours and run.steps are the simulated time and the integrator step each row was written at.

Every one of those is a view into the same buffer rather than a copy, so .copy() is needed before writing into one.

The digest

scenario.digest is a content hash over everything the run's output depends on: the seed, the duration, the step, the cadence, the disturbances, the control mode, the quirk flags, the schedule and the integrator. Two scenarios that describe the same experiment produce the same sixteen characters, and two that differ in any respect do not.

It is worth putting in a filename or a file header, because it turns "this is the fault 4 data, I think" into something checkable. scenario.to_text() writes the whole scenario out in one line that Scenario.from_text reads back, so a dataset can carry its own description rather than a memory of one. The notebook checks both properties rather than asserting them: the same scenario run twice is bit-identical, a different seed is not, and the text round-trips.

What a trip looks like

The plant has eight shutdown conditions, on reactor pressure, reactor level, reactor temperature, separator level and stripper level. Cross one and the simulation is over. run() never raises for a plant that misbehaves, because a run that ended early is data, and throwing it away would hide the difference between a port that trips where the original does and one that does not. The outcome, the step, the hour and the condition that fired are all on the Run.

The notebook's survey of all twenty faults at 48 hours is the useful version of this: at the default seed exactly one of them trips, IDV(6), the total loss of the A feed. That is a property of the seed as much as of the fault, which is the kind of thing worth measuring before designing an experiment around it.

The same run from the command line

$ tep run --hours 2

writes the same 40 rows as CSV on stdout, with seventeen significant digits, so the file round-trips an f64 exactly.

Injecting a fault, and finding it in the data

The worked example is the second half of notebook 1, Getting started, which switches IDV(4) on eight hours into a 24-hour run, plots the reactor temperature and the cooling water valve side by side, and reads the ground truth back. Source: notebooks/01-getting-started.ipynb.

A disturbance is one line: Scenario.fault(n) is the baseline with IDV(n) switched on for the whole run, one-based exactly as the Fortran's IDV(n) is. That is what the original does, and it is the right thing when what you want is a faulted record. It is the wrong thing when what you want is to watch the fault arrive, and arrival is what a detector is judged on, so the notebook schedules it instead: IDV(4) off for eight hours and on afterwards, which is the layout the published test sets use.

IDV(4) is a five degree step in the reactor cooling water inlet temperature. The interesting part is that it is nearly invisible where you would first look.

The fault is in the valve, not in the temperature

Over the fourteen hours after the fault settles, the mean reactor temperature is 120.399542 degrees on the fault-free run and 120.399514 degrees on the faulted one. The difference is 28 millionths of a degree, against a fault-free standard deviation of 0.018728 degrees: two thousandths of one standard deviation. To any instrument, and to any detector watching that channel, nothing happened.

The cooling water valve tells the other half of the story. It sits at 41.103% open without the fault and 44.868% with it, a shift of 3.766 percentage points of valve travel. That is the whole of IDV(4): the cooling water arrives hotter, the temperature controller notices immediately, and it opens the valve until the temperature comes back. The controller has converted a disturbance in a measured variable into a disturbance in a manipulated one, which is what a controller is for.

The lesson generalises well past this fault, and it is the single most important thing to understand before building a monitor for a plant under closed-loop control. The evidence of a disturbance often sits in the manipulated variables rather than in the measurements, because the controller has been busy cleaning up the measurements. Both halves are in the array run.to_numpy() returns, and the detector in the next tutorial uses all 53 channels for exactly this reason.

Ground truth

run.labels() records what was actually true at each instant. It returns two (n_samples, 20) arrays indexed by IDV(n) - 1: active says whether that disturbance was on at that sample, and since_onset says how many hours it had been on, nan where it was not on at all.

The original records nothing of the sort. A published Tennessee Eastman dataset is a matrix and a filename, so every detection delay in the literature is measured against an onset its author knew from the experimental protocol rather than from the data. That is fine until two papers disagree about where sample 160 falls. Here the onset is in the data, and finding it is int(np.argmax(labels["active"][:, 3])) rather than a constant somebody has to remember.

The precision is deliberate too. A fault live from the first step reports 0.04972222222222225 hours at the first sample, not "about 0.05": the label carries the same simulated clock the row does, so it is 179 seconds. That is what lets a delay of one sample be distinguished from a delay of zero.

The disturbance you did not ask for, and no longer get

Pass driver_forces_idv12=True and run for longer than eight hours, and a second fault appears at hour eight without being asked for. That is not a bug in the scheduler. It is temain_mod.f:366-368, which switches IDV(12) on at eight hours whatever the scenario said, and both IDV(4) and IDV(12) act on cooling water, so a run nominally labelled IDV(4) was really the two together.

It was the default here until 2026-08-28, on the belief that reproducing d01 through d21 required it. Tier 7 showed the opposite: every dNN_te file except d12_te sits at the nominal operating point straight across row 160, which is hour eight, so the published files were made with that line replaced. The default now follows the files.

It is delta D-011 in the register, and it used to be the single most common way a comparison against the published files went quietly wrong, because it was the default. It is not any more: a request for IDV(4) gets IDV(4), and driver_forces_idv12=True is how to ask for the driver's version. The Rust equivalent is Scenario::faithful(), which sets that quirk and the freeze-on-trip one together, and the command line spelling is tep run --force-idv12. Say which one you used when you report numbers. The labels make the difference visible either way, which is the point of recording ground truth rather than assuming it.

From the command line

$ tep run --fault 4 --hours 8 --labels

--labels adds the fault and hours_since_onset columns to the CSV, so the ground truth travels with the data.

Building a detector, and measuring it

The worked example is notebook 2, Fault detection with PCA, which fits the model, plots both statistics against their limits on fault-free and faulted records, scores eight disturbances, and then diagnoses the one limit that does not work. Its sequel, notebook 3, The three hard faults, measures the benchmark's detectability floor three separate ways. Sources: notebooks/02-fault-detection-pca.ipynb and notebooks/03-hard-faults.ipynb.

This is the canonical Tennessee Eastman monitoring experiment. Fit a principal component model to a record of the fault-free plant, watch two statistics on new data, and raise an alarm when either leaves its control limit. Hotelling's T-squared measures distance from the training mean inside the subspace the model retained. The squared prediction error, written SPE or Q, measures how much of an observation the model could not reconstruct at all.

The detector is notebooks/pcamon.py, about four hundred lines of NumPy and the Python standard library with no SciPy and no scikit-learn: the eigendecomposition is numpy.linalg.eigh, the normal quantile is statistics.NormalDist, and the F quantile the T-squared limit needs is a continued-fraction incomplete beta with a bisection on top.

That file, rather than a crate, is deliberate. The repository has a Rust implementation of exactly this detector in tepsim-stats, and this page used to show it. tepsim-stats is development-only, and cargo xtask ci asserts that no shipped crate so much as names it, so a reader who installed tepsim and followed the page could not build what the page was teaching. Everything the notebooks do runs against the package you can install.

The two implementations agree. Notebook 2 opens by reproducing the Rust transcript, and every printed digit matches: the same 33 components, the same limits to three decimals, and the same four detection rates and delays, from a hand-written cyclic Jacobi sweep on one side and LAPACK on the other. What the notebook measures after that is therefore a property of the plant and the method, not of the linear algebra.

The measurement is the point

Any detector can be made to look good by reporting only the faults it catches. Every detection rate in the notebook is reported next to the false alarm rate on held-out fault-free data the model never saw, and one of the two statistics comes out badly.

Three numbers do the work, and each has a trap in it.

The fault detection rate is the fraction of post-onset samples that raised an alarm. It is a rate over samples, not a per-run yes or no, so a detector that catches a fault and then loses it scores badly. That is the intent: a statistic that drops back inside its limit while the fault is still running is flickering, not detecting. Its complement, the missed detection rate, is what the tables in the literature report.

The false alarm rate is the same fraction over the pre-onset samples, and it is the number that makes a detection rate meaningful. A detector with a 20% false alarm rate that achieves a 20% detection rate has detected nothing.

The detection delay is the number of samples from the onset to the first run of three consecutive alarms. The persistence requirement is not decoration. With a run length of one the delay is just the first alarm after the onset, and on a detector with any false alarm rate at all that is mostly luck: at a 3% false alarm rate the first post-onset sample alarms by chance one time in thirty, and calling that a delay of zero flatters the detector. The literature uses three and six and does not agree, so the run length travels with the number. Delays are in samples, and one sample is three minutes.

What the model does with a constant column

pcamon.fit standardises each column to zero mean and unit sample variance before forming the correlation matrix. Standardisation rather than mere centring is not optional here: reactor pressure lives near 2705 kPa and a composition is a percentage, so a covariance model would be a model of the pressure and nothing else.

How many components to keep is passed as a named rule rather than a bare k, because two detectors that retain different numbers of components are different detectors and a result that does not say which rule produced it cannot be reproduced. At 90% of the variance the notebook keeps 33 of 52 components. That is two thirds of them, and it is worth pausing on: the largest eigenvalue is 5.910, only 11.4% of the total of 52, so this plant has no small handful of dominant directions and a monitoring scheme on it works in a fairly high-dimensional retained subspace.

XMV(12), the agitator speed, never moves, so its standard deviation is zero and it cannot be standardised. Rather than divide by zero or quietly drop the column, pcamon records its index, zeroes its row and column of the correlation matrix, and says so when asked. A silent drop here is how a 53-variable model becomes a 52-variable model that nobody can reproduce.

The number the detector would rather you did not see

Both limits are drawn at 99% confidence, so the nominal false alarm rate is 1%. On a fresh 48-hour fault-free run the model never saw, T-squared alarms on 32 of 960 samples, a rate of 0.0333, which is high but recognisable. SPE alarms on 174 of 960, a rate of 0.1812. That is a factor of eighteen.

It is not an arithmetic error. pcamon.spe_limit computes the Jackson-Mudholkar expression exactly as stated. It is the assumption underneath the expression failing: the limit is derived for residuals that are normal and independent, and the plant's are neither. Several feed conditions are driven by slow random walks that never stop, so a 48-hour record wanders somewhere a 25-hour training record did not go, and when it does, every sample in the excursion alarms together. The notebook's plot shows exactly that shape, with the SPE alarms arriving in long blocks rather than as isolated points.

The tempting response is to lower the confidence level until the number looks right. The notebook does the experiment instead, holding everything else fixed and lengthening the training record:

Training recordSamplesSPE false alarm rate
25 hours5000.1812
100 hours20000.0219
200 hours40000.0177
500 hours100000.0115

That settles it. With enough fault-free training data both statistics land on the nominal 1%, so Jackson-Mudholkar was never the problem. A 25-hour record simply does not contain the tails of a process whose feed conditions are driven by random walks that never stop, and the limit it produces is therefore too tight.

This has a direct consequence for the literature, and not a comfortable one. The published training file d00 holds 500 samples, which at a three-minute interval is exactly 25 hours: the first row of that table. Every SPE false alarm rate reported for static PCA on the published Tennessee Eastman data inherits this.

Estimating the limit empirically from the training residuals is the usual advice, and the notebook checks that too rather than assuming it. It does not help: the empirical SPE limit gives 0.1448 against the analytic 0.1812, and the empirical T-squared limit is markedly worse than the analytic one at 0.1042 against 0.0333. Both are drawn from the same 25 hours that were too short in the first place, and neither can know about the excursions it never saw.

The fix is more data, or a method that models the serial correlation rather than assuming it away. Dynamic PCA, which augments each observation with lagged copies of itself, and canonical variate analysis are the two the literature reaches for, and Russell, Chiang and Braatz's 2000 comparison of both against static PCA is on exactly these files.

The floor under the benchmark

Notebook 3 is about the best known empirical result on this problem: IDV(3), IDV(9) and IDV(15) are effectively undetectable by these methods. It measures that on the published d00 through d21 files, on simulated runs at the seeds those files record, and over a ten-seed ensemble, and the three measurements agree.

The plainest form of the result is distributional. The median post-onset T-squared for those three faults is within one percent of the fault-free median, and their median SPE within four percent, on quantities whose fault-free values span two orders of magnitude. No threshold separates them because there is nothing to separate.

That is worth stating carefully, because it reads like a criticism of PCA and is not one. The plant really is running normally under those three disturbances. A detector that alarmed on them would be reporting a fault with no consequence, and the correct behaviour for a monitoring scheme is what it does. What the result measures is that this benchmark has a detectability floor set by the process rather than by the method, which is part of what makes it a good benchmark: any paper claiming to detect all twenty is claiming something about its false alarm rate that it has probably not measured.

A fault that arrives, and clears

The worked example is notebook 4, Custom scenarios, which schedules IDV(4) between hours six and twelve and plots the valve moving and coming back, composes two faults, sweeps a fault's magnitude from zero to one, and compares the three integrators. Source: notebooks/04-custom-scenarios.ipynb.

Every published Tennessee Eastman dataset is the same shape of experiment: set some IDV flags before the run, then leave them. That is all the original admits, which is why the literature's fault onsets are all in the same place and why almost nobody studies a fault that arrives, persists, and then goes away.

A Scenario here carries a schedule of events, each at a time and each doing one thing, so a disturbance can arrive at a stated hour, several can arrive independently of each other, and any of them can clear. Two further things the original cannot express sit beside it: a fault applied at a fraction of its full strength, and a choice of integrator. All three live in the scenario's canonical text form, which is where Python reaches them, and none of it would be worth much if the description of a run could not travel with the run.

The schedule is text, and that is the point

The Python constructors cover eight of a scenario's eleven fields. The other three, events, continuous and integrator, are reached by rendering a scenario to text, editing a field, and parsing it back. That sounds like a workaround and is closer to the opposite. The text is the scenario's real serialisation: it is what the digest is taken over, what the browser app puts in a URL fragment, and what the Rust and wasm sides read and write, so building a schedule through it is building it in the form the run will be described by.

An event is time:verb followed by the verb's own fields, so an events field of 6:start:4,12:stop:4 switches IDV(4) on at hour six and off at hour twelve. The notebook's helper rebuilds the whole line from a dictionary rather than patching it with a string replacement, so a mistyped field name is a KeyError and not a silently different run.

An event is applied on the first step whose time is at or past its own. The window is half-open at the start and closed at the end, so an event is applied exactly once however the step size divides its time, and an event at time zero is applied on the first step rather than never. Two events at the same instant keep the order they were written, because stop then start on one fault is a different scenario from the reverse and the digest has to be able to tell them apart.

What the trace shows

The reactor cooling water valve sits at 41.063% open before the fault. Half an hour after it arrives the valve is at 45.492%, it holds near 44.5% for the six hours the fault is live, and half an hour after it clears it is back at 41.045%, within a fiftieth of a percentage point of where it started. The controller does not know a fault happened in either direction; it is rejecting a disturbance both times.

That return is worth noticing rather than assuming. It is this loop doing its job on a step disturbance that was removed, and it is not guaranteed in general: a fault that shifts an inventory leaves the plant somewhere else even after it clears, and the plant after a fault is not the plant before it, because the controllers have moved. The recovery half of the problem is expressible here and barely studied, which is most of the reason the schedule exists.

The labels follow the schedule exactly. Once the fault clears, since_onset goes back to nan, which means the ground truth describes the plant's current condition rather than its history. A study of recovery has to look at transitions in active, not at that column.

Composition

Several disturbances can be described independently, each with its own arrival time. The original allows more than one IDV flag to be set, but not when each arrives, and interactions are usually where the interesting behaviour is.

IDV(1) steps the A/C feed ratio and IDV(8) enables random walks on the A and B feed compositions. Individually the plant absorbs both: reactor pressure means of 2709.58 and 2702.19 kPa against a fault-free 2706.89. Together the mean is 2731.92 with a standard deviation of 113.07, where adding the two individual shifts would predict 2704.87. The effect of the pair is not the sum of the effects of the parts, which is the whole reason to be able to compose them.

Half a fault

teprob.f:341-346 opens TEFUNC by forcing every IDV to zero or one, so the original has exactly two states per disturbance. The disturbances are then used multiplicatively, XST(1,4) = TESUB8(1,TIME) - IDV(1)*0.03 at teprob.f:407 being the pattern, so a magnitude between zero and one scales a fault smoothly and needs no other change to the model.

That is the continuous extension, and it is off by default. With it on, a sweep of IDV(4) from magnitude 0 to 1 moves the mean cooling water valve from 41.023% to 44.779% in steps of about 0.9 percentage points, linear in the magnitude to within 0.042 percentage points of valve travel, while the reactor temperature it is defending stays at 120.400 degrees at every magnitude. A detection threshold study is then a sweep over one number rather than an argument about what "harder" means.

Two things are worth saying plainly. A run with the extension on is not comparable to any published dataset, and none of the validation ladder applies to it. And a fractional magnitude without the extension is refused rather than rounded, with an error naming the line of Fortran that makes it impossible. Silently turning a request for half a fault into a whole one would produce a run that does not match its own description, which is exactly what the content hash exists to prevent. A magnitude of exactly 1.0 is bit-identical to the faithful path, which is asserted, so the extension can be left on for a study that mixes full and partial faults.

The trap in refining the step

The notebook also compares Euler, RK4 and Dormand-Prince, and the result is about the original rather than about the port: RK4 and Dormand-Prince agree with each other to about 2e-6 relative while both differ from Euler by about 1.5e-2 on the worst channel. Two independent methods agreeing that closely and disagreeing with the third is what convergence looks like. Euler is not the accurate choice, it is the faithful one, and everything the validation ladder claims is a claim about Euler.

The obvious next move, keeping Euler and halving the step, does not do what you expect. Halving the step moves the answer by more than changing the method does, and it does not converge as the step shrinks. That is neither a bug nor stiffness: the disturbance walks and the measurement noise advance once per step, so a run at half the step draws twice as many random numbers and is a different realisation of the stochastic forcing. The step size is part of the disturbance model in this plant and not only a numerical parameter. Change the method to integrate the same realisation more accurately; change the step only when you mean to change the noise.

The description travels with the run

A scenario, schedule included, writes out as one line of text, that line parses back to an equal scenario, and the digest is unchanged across the round trip. A dataset generated from a scenario can carry the line in its header, and a reader can reconstruct the exact run rather than the description of a run. The same line is what TEP Studio puts in a URL fragment, so a scheduled experiment can be handed to somebody as a link.

The format is versioned and its parser is strict: a missing field, an unknown field, a value out of range or a version this build does not know is an error that says what was wrong, rather than a default quietly substituted.

repr() handles the two shapes separately, and knowing why is useful. A scenario the constructors can express prints as a constructor call, because that is what is readable at a prompt. Anything else prints as Scenario.from_text(...), which round-trips by construction. Both satisfy eval(repr(s)) == s. The obvious implementation, printing only the constructor's arguments, produced valid Python that evaluated to a different scenario with a different digest and said nothing about it.

A schedule holds up to 32 events, which is far more than any experiment in the literature uses. The published datasets have exactly one event each: the fault, switched on before the run.

The plant

This page is the vocabulary the rest of the process chapters use: the eight components, the fifty states, and the thirteen internal streams. All three are recovered from teprob.f rather than from the 1993 paper, because on two of the three the paper and the source disagree.

The flowsheet is the map for all of it. The mixing zone, the reactor, the separator and the stripper are the four vessels that hold state, and the state table below is their contents; the condenser and the compressor hold none. Every solid line is one of the thirteen FTM entries the stream table enumerates, and every tag is an XMEAS or XMV index whose teprob.f line is cited on the instrumentation page. Nothing on the drawing was taken from the paper's figure. Where a stream carries two numbers they are both shown, because that disagreement is the subject of the third section of this page.

Flowsheet of the Tennessee Eastman ProcessFour feeds enter a plant of five unit operations. A mixing zone receives the A, D and E feeds, the compressor recycle and the stripper overhead, and discharges to the reactor. Reactor effluent passes through a condenser into the vapour-liquid separator. Separator vapour is compressed and recycled, with a purge bleeding the inert B. Separator liquid falls to a steam stripper, which is also fed the mixed A and C feed at its base and which makes the liquid product. Each stream carries its Fortran FTM index and the paper stream number, and each unit carries the XMEAS and XMV tags that sit on it. stream 1 · A feed FTM(3), XMV(3), XMEAS(1) stream 2 · D feed FTM(1), XMV(1), XMEAS(2) stream 3 · E feed FTM(2), XMV(2), XMEAS(3) mixing zone stream 6 AT FTM(6), FTM(7) reactor cw FTM(8) · stream 7 condenser cw separator AT FTM(10) · stream 9 purge, XMV(6), XMEAS(10) K FTM(9) · stream 8 · recycle · XMEAS(5) compressor XMEAS(20) work, XMV(5) recycle valve XMEAS(6) reactor feed rate XMEAS(16) pressure published as stripper pressure XMEAS(7) reactor pressure XMEAS(8) reactor level XMEAS(9) reactor temperature XMEAS(21) cooling water out T XMV(10) cooling water flow XMV(12) agitator speed XMEAS(11) separator temperature XMEAS(12) separator level XMEAS(13) separator pressure XMEAS(14) underflow rate XMV(7) underflow valve XMV(11) condenser cooling water XMEAS(22) condenser water out T FTM(11) stream 10 stripper FTM(12) FTM(5) · stream 5 · stripper overhead steam XMEAS(15) stripper level XMEAS(18) stripper temperature XMEAS(19) steam flow XMV(9) steam valve XMEAS(17) product rate XMV(8) product valve AT FTM(13) · stream 11 · product stream 4 · A and C feed FTM(4), XMV(4), XMEAS(4) enters the stripper base, not the mixing zone Composition analysers, mole % XMEAS(23-28) A to F, stream 6 reactor feed, 0.1 h XMEAS(29-36) A to H, stream 9 purge, 0.1 h XMEAS(37-41) D to H, stream 11 product, 0.25 h each reports its previous sample FTM(12), the stripper downflow, has no paper number
The Tennessee Eastman Process as teprob.f wires it. Solid lines are the thirteen internal streams, each labelled with its Fortran FTM index and, where one exists, the stream number of the paper. Dashed lines are utilities: cooling water through the reactor coil and the condenser, and steam to the stripper reboiler. Beside each vessel are the XMEAS instruments and XMV valves that sit on it, and the three AT bubbles mark the streams the composition analysers sample.

Three things on it are worth reading twice, because each is a place the source and the received description of the plant part company. The mixed A and C feed, stream 4, does not reach the mixing zone at all: it enters the stripper base as the stripping gas (teprob.f:614-662). The pressure published as stripper pressure, XMEAS(16), is the mixing zone's PTV (teprob.f:694), because the model carries no separate stripper vapour space. And FTM(12), the liquid that fails to strip out and falls into the stripper sump, has no number in the paper at all.

Components

Eight, A through H. A, B and C are non-condensible and are treated as ideal gases throughout (teprob.f:478). D through H are condensible and get an Antoine vapour pressure (teprob.f:484). B is the inert: it appears in none of the four reactions, arrives with the mixed feed, and leaves only through the purge.

A, B and C have no real liquid density correlation. AD is 1.0 with BD and CD zero for all three (teprob.f:973, 983, 993), so they contribute a flat, temperature-independent term. That is a placeholder keeping the mixing rule finite rather than a fitted number, because the model never puts them in a liquid phase in quantity.

The fifty states

The original carries the state in a bare YY(50) and unpacks it by index arithmetic inside TEFUNC (teprob.f:417-440). Recovering that mapping is the first act of the port, and here it becomes typed structure, pinned by a test that reads the corresponding COMMON/TEPROC/ variables back out of the Fortran rather than trusting a comment.

YY (1-based)FortranMeaningCount
1-3UCVR(1:3)reactor vapour holdup, A, B, C3
4-8UCLR(4:8)reactor liquid holdup, D through H5
9ETRreactor internal energy1
10-12UCVS(1:3)separator vapour holdup, A, B, C3
13-17UCLS(4:8)separator liquid holdup, D through H5
18ETSseparator internal energy1
19-26UCLC(1:8)stripper liquid holdup, all eight8
27ETCstripper internal energy1
28-35UCVV(1:8)mixing zone vapour holdup, all eight8
36ETVmixing zone internal energy1
37TWRreactor cooling water outlet temperature1
38TWScondenser cooling water outlet temperature1
39-50VPOS(1:12)valve positions, one first-order lag each12

The eight slots do not mean the same thing in every vessel, and this is the part that is easy to get wrong. For the reactor and the separator the array is split by phase: slots 1 to 3 are the vapour holdups of A, B and C, and slots 4 to 8 are the liquid holdups of D through H. UCLR(1..3) is set to zero at teprob.f:420-421 because the non-condensibles never form a liquid, and UCVR(4..8) does not come from the state at all: it is derived from the vapour-liquid equilibrium later in the same call (teprob.f:500-501). For the stripper all eight slots are liquid, and for the mixing zone all eight are vapour.

The four temperatures are state, not derived quantities

TESUB2 takes its temperature argument as both the initial guess and the result (teprob.f:1432, 1438), and the four call sites at teprob.f:460-465 pass TCR, TCS, TCC and TCV straight out of COMMON. Every evaluation therefore starts its Newton solves from the previous evaluation's answers, and since the iteration stops on a step below 1e-12 the converged value depends on where it started.

That is not a detail. B-0015 measured the cost of getting it wrong: seeding the solves from a different point on the nominal trajectory moves up to 21 of the 50 derivatives. A port that solved from a fixed guess would be tidier and would not be bit-exact. The warm-start temperatures are carried explicitly here, and B-0034 found the same thing again from the other end, where a trajectory started from the nominal literals instead of from the values TEINIT's own evaluation leaves behind is a different trajectory rather than a rounding of the same one.

vesselafter TEINITnominal literal
reactor120.3999996050374120.4
separator80.109403994558280.109
stripper65.731029771801865.731
mixing zone86.120111977106686.120

Those four values are asserted against the oracle bit for bit; they are from the LOG.org entry for B-0052.

The thirteen internal streams

The Fortran's stream indices are not the stream numbers in the paper. FTM(1) is the D feed, which Downs and Vogel call stream 2. FTM(3) is the A feed, which they call stream 1. Nothing in the source says so, and every reimplementation of TEP has to rediscover it; getting it wrong produces a plant that runs, looks plausible, and is wired up incorrectly.

InternalPaperStream
12D feed
23E feed
31A feed
44A and C feed
55stripper overhead vapour to the mixing zone
66mixing zone outlet to the reactor
76reactor inlet, an alias of 6
87reactor outlet to the condenser and separator
98separator vapour through the compressor, the recycle
109purge
1110separator liquid underflow to the stripper
12nonestripper liquid downflow, internal only
1311product

The mapping was established from the source, not from the paper: teprob.f:565 drives FTM(1) from valve 1 and XMV(1) is documented as "D Feed Flow (stream 2)"; teprob.f:567 gates FTM(3) on IDV(6), documented as "A Feed Loss (Stream 1)"; teprob.f:688 reports FTM(10) as XMEAS(10), "Purge Rate (stream 9)"; teprob.f:683 reports FTM(9) as XMEAS(5), "Recycle Flow (stream 8)"; and so on for the remaining six.

Streams 6 and 7 are the same fluid. teprob.f:656-661 copies flow, enthalpy, temperature, composition and component flows from 6 to 7 wholesale, with no mixing, no pressure drop and no heat loss. Stream 7 exists so that the reactor's balance at teprob.f:763-772 can name its own inlet.

Vessel volumes

Four vessels, four fixed total volumes, all four written in the original as single-precision literals (teprob.f:1118-1121):

VesselFortranValue, cubic feet
reactorVTR1300
separatorVTS3500
stripperVTC156.5
mixing zoneVTV5000

The reactor and the separator hold two phases, so their vapour space is whatever the liquid does not occupy. The stripper is treated as liquid only and the mixing zone as vapour only.

Precision is a property of each literal

The line above is not pedantry. 182 of the assignments in TEINIT are single-precision literals, and a literal written without a D suffix is stored by gfortran as a single-precision value widened to double, which differs from the decimal number by up to about 6e-8 relative. Since the port must reproduce the original's arithmetic bit for bit, every constant has to be transcribed according to the suffix on its own line.

The original is not consistent, so the precision cannot be inferred from elsewhere in the file. teprob.f:1411 writes 273.15 and teprob.f:594 writes 273.15D0. The 1.8 at teprob.f:790 and 792 is single while every other occurrence in the file (1396, 1404, 1464, 1471) is 1.8D0. The gas constant at teprob.f:475 is RG=998.9, single, and it multiplies six of the eight partial pressures in every vessel.

The canary the constants table was built around is XMW(2), which must come out 25.399999618530273 and not 25.4.

The right-hand side

TEFUNC occupies teprob.f:196-816, six hundred lines that present themselves as a derivative evaluation. They are not one. Understanding why is the single most consequential structural decision in this port, so it comes before the unit operations rather than after them.

Three phases, because TEFUNC is not a pure function of (t, y)

Inside what looks like a right-hand side, the original also advances the disturbance random walks, draws measurement noise, ticks the three sampled analysers, and latches the valve commands. That is harmless for the fixed-step Euler integrator the original uses, which evaluates the right-hand side exactly once per step. It is wrong for anything else: an RK4 step would advance the walks four times and draw four sets of noise.

The impure work does not sit in one place, which is the part that is not obvious until you look. Some of it must happen before the derivative and some can only happen after it. The walks are read at teprob.f:407-416, so they must be advanced first. The measurement vector is assembled at teprob.f:679-701 out of flows the evaluation computes, so noise cannot be added until afterwards. One impure call cannot sit on both sides.

The port therefore splits the routine in three:

Phaseteprob.fWhat it does
advance_discrete341-406, 793-804the IDV clamp, the IDVWLK mapping, the walk advance and spike draws, the TIME = 0 initialisation, and the valve-command latch
derivatives407-710, 762-792, 805the entire physical model, the noise-free measurements, the shutdown test, and the fifty balances
sample_measurements711-761additive measurement noise, and the three sampled analysers with their dead time

The pure phase hands back the signals it computed alongside the derivative, so the post-phase does not have to re-run the model to find out what to add noise to.

The valve latch is hoisted, and the hoist is proved mechanically

teprob.f:793-798 sets IVST from IDV and 799-804 latches VCV from XMV, at the very end of the routine. That block reads only XMV, VST, IVST, IDV and TIME, and nothing in 345-792 writes any of them, so moving it into the pre-phase changes no number.

That is a claim about four hundred and fifty lines of Fortran, which is too large to check by eye, so it is checked by machine instead: a dedicated oracle test drives both orderings and asserts they agree. The latch shares the DO 9020 loop with the valve derivative at teprob.f:805, so the port splits that loop, sending the latch to the pre-phase and leaving YP(I+38) in the pure one.

What each stage of the pure phase does

The pure phase runs in the original's order, and each block is a module in tepsim-core with its own page or section in this chapter.

teprob.fStageWhere it is documented
417-472unpack the state into per-vessel inventories, fractions, temperatures, densities and volumesThe plant
473-502vapour-liquid equilibrium and the three vessel pressuresreactor, separator, mixing zone
503-528the four reactions, their rates and the heat of reactionThe reactor
529-564the stream table: compositions, molecular weights, temperatures, enthalpiesThe plant
565-613valve-lagged flows, pressure-driven flows, the compressorThe condenser and separator
614-662the stripper, and the reactor-inlet aliasThe stripper
663-678the reactor coil, the condenser, the stripper reboilerthe three vessel pages
679-710the twenty-two continuous measurements and the shutdown detectorInstrumentation
762-811the fifty balancesbelow

The fifty balances

Everything above exists to feed teprob.f:762-811. Four vessels, each with eight component balances and one energy balance, plus two cooling-water wall temperatures and twelve valve lags.

\[ \frac{dn_i}{dt} = \sum_{\text{in}} \dot n_i - \sum_{\text{out}} \dot n_i + r_i \]

\[ \frac{dE}{dt} = \sum_{\text{in}} h F - \sum_{\text{out}} h F + Q \]

The reactor is the only vessel with a reaction term. The cooling water walls follow

\[ \frac{dT_w}{dt} = \frac{F_w \times 500.53 \times (T_{in} - T_w) - Q \times 10^6 / 1.8}{H_w} \]

where 500.53 converts a cooling water flow to a heat capacity rate and the factor \(10^6 / 1.8\) undoes the scaling the enthalpy correlations carry (teprob.f:789-792). The 1.8 on those two lines is single precision and is the only such occurrence in the file, which is why it has to be read off the line rather than inferred from teprob.f:1396, 1404, 1464 or 1471, where it is written 1.8D0.

Each valve is a first-order lag toward its latched command (teprob.f:805):

\[ \frac{dv_i}{dt} = \frac{c_i - v_i}{\tau_i} \]

YPVessel
1-8, 9reactor: component balances, then energy
10-17, 18separator
19-26, 27stripper
28-35, 36mixing zone
37, 38cooling water outlet temperatures
39-50valve lags

A shutdown freezes the plant

teprob.f:807-811 zeroes all fifty derivatives whenever any shutdown condition holds. That does not stop the plant, it freezes it: the state stops moving, the clock keeps running, and nothing in the original says so.

PLAN.org classes this as a Class C quirk, "behaviour-defining and benchmark-relevant", so the fix needed a measured delta and a sign-off. Both arrived on 2026-08-28, and a default Scenario now ends the run at the trip. It is delta D-007. The port reports the trip and its cause either way, rather than leaving a caller to infer a freeze from a vector of zeros.

Scenario::faithful() reproduces the freeze, and Tier 2 needs it to: the adversarial sampling pool contains states that trip, and a port that did not freeze would disagree with the oracle on all fifty components for every one of them. So does any comparison against d06 or d18, whose published files are between 45% and 76% frozen tail.

Determinism, and the two libm builds

tepsim-core is no_std, forbids unsafe, and contains no f32, no SIMD, no rayon, no reordered reductions and no source of time or randomness outside the model's own generator. exp, pow and ln come from a vendored pure-Rust libm so that the answer does not depend on the host's C library.

That choice costs something measurable, and the project measures it rather than hoping. Over the range of Antoine arguments this model reaches, the vendored libm and gfortran's disagree on 9.945% of them, by exactly one ULP (LOG entry B-0018). So a differential against the default build can only assert a tolerance. Every Tier 2 comparison therefore runs a second time under a libm-system feature, where exp is the one gfortran calls, and is held to zero ULP there. Both runs are in the CI gate. Without the second, a reassociation worth one or two ULP would pass silently.

Integer powers are a related trap and are not routed through pow at all. gfortran expands X**4 into multiplications, and the shape of the expansion is load-bearing. Measured over 200,000 values with this project's pinned flags (LOG entry B-0023): (x*x)*(x*x) matches gfortran on 200,000 of 200,000, ((x*x)*x)*x on 132,040, and pow(x, 4.0) on 99,523. So it is binary exponentiation, squaring twice, and the two plausible alternatives are each wrong a third to half of the time.

The mixing zone

The feed mixing zone is a single vapour volume in which the three pure feeds, the compressor recycle and the stripper overhead combine before entering the reactor. It is the simplest of the four vessels: one phase, a fixed volume, no reaction, no heat duty.

Source: teprob.f:428 and 434 for its states, teprob.f:465-466 for its temperature, teprob.f:492 for its pressure, teprob.f:576-579 for its outlet flow, and teprob.f:783-788 for its energy balance.

Equations

All eight components are vapour and the volume is fixed at VTV, so the equilibrium block needs only the ideal gas law applied to the mixture (teprob.f:492):

\[ P_v = \frac{N_v R T_K}{V_{tv}} \]

The temperature comes from the specific internal energy, not from a state directly. The block totals the holdups, forms the mole fractions and the energy per mole, and then solves for whatever temperature makes the mixture's specific internal energy equal to it (teprob.f:465-466):

\[ N = \sum_i n_i, \qquad x_i = \frac{n_i}{N}, \qquad e = \frac{E}{N} \]

That solve is Newton's method in TESUB2 (teprob.f:1415-1442), warm-started from the previous evaluation's answer, which is why TCV is state rather than a derived quantity. See The plant.

Flow out of the mixing zone is not valve-driven. It is a square-root resistance across the pressure difference to the reactor, converted from mass to moles by the stream's mean molecular weight (teprob.f:576-579):

\[ F_6 = \frac{1937.6 \, \sqrt{\max(P_v - P_r,\, 0)}}{\overline{M}_6} \]

The clamp at zero is what stops a reversed pressure gradient from producing a NaN out of the square root, and it is reachable from an adversarial state though not from the nominal trajectory.

The component and energy balances have five inlets and one outlet (teprob.f:762-770 and 783-788):

\[ \frac{dn_i}{dt} = \dot n_{i,1} + \dot n_{i,2} + \dot n_{i,3}

  • \dot n_{i,5} + \dot n_{i,9} - \dot n_{i,6} \]

\[ \frac{dE}{dt} = h_1 F_1 + h_2 F_2 + h_3 F_3 + h_5 F_5 + h_9 F_9 - h_6 F_6 \]

There is no Q term: the mixing zone is adiabatic.

Variables

FortranMeaningWhere
UCVV(1:8)vapour component holdup, YY(28..35)teprob.f:428
ETVinternal energy, YY(36)teprob.f:434
UTVVtotal vapour molesteprob.f:443-449
XVVvapour mole fractionsteprob.f:450-455
ESVspecific internal energyteprob.f:459
TCVtemperature, degrees Celsiusteprob.f:465-466
PTVtotal pressure, mmHgteprob.f:492
VTVvessel volume, 5000 cubic feet, single precisionteprob.f:1121
FTM(6)outlet molar flowteprob.f:576-579
YP(28..35), YP(36)the nine derivativesteprob.f:762-770, 783-788

Three things the source settles

The mixed A/C feed does not pass through here. Stream 4 goes directly to the stripper, and it appears in the stripper's energy balance at teprob.f:778-782 and in the stripper's feed at teprob.f:614-662, never in the mixing zone's. The three feeds that do enter are streams 1, 2 and 3, the D, E and A feeds.

Stream 7 is an alias of stream 6, made in the stripper block. teprob.f:656-661 copies flow, enthalpy, temperature, composition and component flows from 6 to 7 wholesale. There is no mixing, no pressure drop and no heat loss between them; stream 7 exists so that the reactor's balance at teprob.f:763-772 can name its own inlet.

The pressure published as stripper pressure is this vessel's. teprob.f:694 reports PTV as XMEAS(16), which Downs and Vogel's measurement table names stripper pressure. The model carries no separate stripper vapour space: the stripper's overhead discharges into the mixing zone as stream 5, and PTV is the pressure of that shared vapour node. The mixing zone's own outlet flow is reported as XMEAS(6), the reactor feed rate (teprob.f:684).

The reactor

The reactor is a two-phase vessel with an internal cooling coil and an agitator. Four exothermic gas-phase reactions run in its vapour space, its liquid holdup sets how much of the coil is wetted, and it is the only vessel in the plant with a reaction term in its balances.

Source: teprob.f:473-502 for the vapour-liquid equilibrium, teprob.f:503-528 for the kinetics, teprob.f:663-673 for the coil, and teprob.f:762-772 for the balances.

Vapour-liquid equilibrium

The vapour space is what the liquid does not occupy (teprob.f:473):

\[ V_{vr} = V_{tr} - V_{lr} \]

A, B and C are non-condensible and are treated as ideal gases, so their partial pressures come from the holdup directly (teprob.f:478-483):

\[ p_i = \frac{n_i R T_K}{V_{vr}}, \qquad i \in \{A, B, C\} \]

D through H are condensible, so their partial pressures come from Raoult's law with an Antoine vapour pressure in degrees Celsius (teprob.f:484-491):

\[ p_i = x_i \exp\!\left(A_i + \frac{B_i}{T_c + C_i}\right), \qquad i \in \{D \ldots H\} \]

The total is the sum of all eight, the vapour composition is \(y_i = p_i / P\) (teprob.f:493-496), and the vapour holdup follows from the ideal gas law applied to the mixture (teprob.f:497 and 500):

\[ N_v = \frac{P V_{vr}}{R T_K}, \qquad n_i = N_v y_i \]

UCVR is both an input and an output

teprob.f:418 fills UCVR(1..3) from the state and teprob.f:500 fills UCVR(4..8) from the equilibrium computed here. It is one Fortran array written by two different mechanisms, and the halves are not interchangeable: the non-condensibles are integrated, the condensibles are derived. Read in that order it also explains why the A, B and C partial pressures are computed first, at teprob.f:478-483: they need UCVR before it is overwritten.

This is where bit equality with gfortran ends

DEXP at teprob.f:485 and teprob.f:488 is the model's first transcendental call. The port answers it from the vendored pure-Rust libm rather than the platform's, for the determinism reason set out in The right-hand side. Measured over the whole Antoine range this model reaches, the two disagree on 9.945% of arguments, by exactly one ULP.

Everything downstream of a condensible partial pressure therefore carries about 1.1e-16 of relative difference from the Fortran that no care in the algebra removes. The bit-exactness claim does not disappear, it moves: under the libm-system feature the transcendental is the one gfortran calls, and the port is bit-identical again, so the algebra is still held to zero ULP rather than to a tolerance.

Kinetics

1:  A + C + D -> G          2:  A + C + E -> H
3:  A + E     -> F          4:  3 D       -> 2 F

Rates 1 and 2 are Arrhenius in reactor temperature with fractional pressure orders on A and C, multiplied by a disturbance drift factor (teprob.f:503-504, 508-511):

\[ r_1 = f_1 \, e^{\,a_1 - E_1/T_K} \; p_A^{1.1544} \, p_C^{0.3735} \, p_D \, V_{vr} \]

\[ r_2 = f_2 \, e^{\,a_2 - E_2/T_K} \; p_A^{1.1544} \, p_C^{0.3735} \, p_E \, V_{vr} \]

Both are guarded: teprob.f:507 requires \(p_A > 0\) and \(p_C > 0\), and sets \(r_1 = r_2 = 0\) otherwise. Rates 3 and 4 are first order in each reactant, and rate 4 shares rate 3's exponential rather than having one of its own (teprob.f:505-506, 516-517):

\[ r_3 = e^{\,a_3 - E_3/T_K} \, p_A \, p_E \, V_{vr}, \qquad r_4 = 0.767488334 \; e^{\,a_3 - E_3/T_K} \, p_A \, p_D \, V_{vr} \]

All four are multiplied by the vapour volume at teprob.f:518-520, so a rate is an extent in moles per hour rather than a volumetric rate. Net production per species follows the stoichiometry (teprob.f:521-527), and the heat release comes from reactions 1 and 2 only (teprob.f:528):

\[ Q_{rxn} = r_1 h_1 + r_2 h_2 \]

with \(h_1 = 0.06899381054\) and \(h_2 = 0.05\) (teprob.f:1122-1123). Reactions 3 and 4 contribute no heat: the original simply does not include them in RH, and HTR(3) is declared but never assigned or read.

R1F and R2F are two different quantities under one name

They arrive from TESUB8(7) and TESUB8(8) at teprob.f:415-416 as the IDV(13) kinetics-drift multipliers, are consumed at teprob.f:503-504, and are then reassigned in place at teprob.f:508-509 to hold the fractional pressure powers. The two meanings share nothing but the storage.

Reading teprob.f:510 as though R1F were still the drift factor gives a plausible and completely wrong rate law, so the port gives the two roles separate names. That is delta D-002: no numerical effect, and the only defence against a misreading no test would catch, because a wrong-but-consistent reading still reproduces itself.

CRXR(2) is never assigned

Seven of the eight slots are written at teprob.f:521-527. CRXR(2), the inert, is not, and it is read anyway at teprob.f:763. It works because COMMON is zero-initialised and nothing ever writes it, so B's net production is zero by static initialisation rather than by statement. That is delta D-003, class A: the value is right, the mechanism is an accident. Here the slot is explicitly zero and a test asserts the oracle agrees.

Precision hazards in this range

Every literal except 0.767488334D0 and 1.5D0 is single precision: the three pre-exponentials, the three activation energies, the gas constant 1.987, and both fractional exponents.

Worse, 40000.0/1.987 at teprob.f:503 is a quotient of two single-precision literals, so Fortran evaluates the division itself in single precision. Widening the operands first and dividing in double is wrong by 4e-9 relative, inside a DEXP argument.

The cooling coil

The coil's effective area ramps with liquid level, because the coil is only wetted over part of its height. VLR/7.8 is the level as a percentage, and the ramp is piecewise linear between 10% and 50% (teprob.f:663-669):

\[ \lambda = \begin{cases} 1 & \ell > 50 \\ 0 & \ell < 10 \\ 0.025\,\ell - 0.25 & \text{otherwise} \end{cases} \qquad \ell = V_{lr} / 7.8 \]

The overall coefficient is quadratic in agitator speed (teprob.f:670-671), and the duty is the coefficient times the driving temperature difference, scaled by a disturbance drift factor (teprob.f:672-673):

\[ U A_r = \lambda \left(-0.5\,\omega^2 + 2.75\,\omega - 2.5\right) \times 855490 \times 10^{-6} \]

\[ Q_r = U A_r \, (T_w - T_c) \, (1 - 0.35 \, d_{10}) \]

That parabola peaks at \(\omega = 2.75\), which is above the agitator's whole range: teprob.f:575 puts it between 1.5 and 2.5. So the coefficient rises monotonically with speed everywhere the plant can go, from 0.5 to 1.25 times the scale, and the falling half of the parabola is unreachable. Its roots are at 1.149 and 4.351, both outside that range too, so the coefficient never reaches zero from the agitator alone. The model is a fit, not a mechanism.

The ramp does not quite meet its flat sections

0.025 at teprob.f:668 is single precision, so it is stored as 0.02500000037252903 and the ramp misses both of its endpoints:

levelramp givesflat section givesgap
103.725290298461914e-903.7e-9
501.000000018626451511.9e-8

Both breakpoint comparisons are strict, so a level of exactly 10 or exactly 50 takes the ramp, and UARLEV is discontinuous by those amounts as the level crosses either one. This is faithful reproduction rather than a delta: it is what the original computes, the gaps are eight orders below the quantity itself, and the coefficient they scale is an empirical fit in the first place. It is written down because "the ramp meets the flat sections" is the obvious assumption, it is false, and a test asserting it would fail for a reason that looks like a porting error.

Balances

Inlet is stream 7, outlet is stream 8, and the reaction term is the only one of its kind in the plant (teprob.f:762-772):

\[ \frac{dn_i}{dt} = \dot n_{i,7} - \dot n_{i,8} + r_i, \qquad \frac{dE}{dt} = h_7 F_7 - h_8 F_8 + Q_{rxn} + Q_r \]

QUR is heat removed, so it enters positive here because \(U A_r (T_w - T_c)\) is already negative when the coil is cooling. The reactor's cooling water wall temperature is YP(37) (teprob.f:789-790).

Variables

FortranMeaningWhere
VTR, VLR, VVRtotal, liquid and vapour volumeteprob.f:1118, 470, 473
PPR(1:8), PTRpartial and total pressure, mmHgteprob.f:479, 486, 487
XVR, XLRvapour and liquid mole fractionsteprob.f:494, 451
UTVR, UCVRtotal and per-component vapour molesteprob.f:497, 500
TCR, TKRtemperature, Celsius and kelvinteprob.f:460-461
RR(1:4)extent of each reactionteprob.f:503-520
CRXR(1:8)net production per speciesteprob.f:521-527
RHheat of reactionteprob.f:528
HTR(1:2)heats of reactions 1 and 2teprob.f:1122-1123
AGSPagitator speed, fraction of nominalteprob.f:575
UARLEV, UAR, QURwetted fraction, coefficient, dutyteprob.f:663-673
TWRcooling water outlet temperature, YY(37)teprob.f:435
YP(1..8), YP(9)component and energy derivativesteprob.f:762-772

Three of the eight shutdown conditions belong to this vessel: reactor pressure above 3000 kPa gauge (teprob.f:703), liquid volume outside 2 to 24 cubic metres (teprob.f:704-705), and temperature above 175 degrees Celsius (teprob.f:706). See Instrumentation.

The condenser and separator

The condenser cools the reactor effluent and the separator splits it into a vapour and a liquid. The vapour leaves through two paths, the compressor recycle and the purge; the liquid goes to the stripper. The compressor and its recycle valve belong here too, because their whole job is deciding how much of the separator's vapour goes back around the loop.

Source: teprob.f:473-502 for the equilibrium, teprob.f:585-601 for the flow network and the compressor, teprob.f:674-676 for the condenser duty, and teprob.f:773-777 for the balances.

Vapour-liquid equilibrium

The separator's equilibrium has exactly the shape of the reactor's and shares its code path. The vapour space is VTS less the liquid volume (teprob.f:474); A, B and C get ideal gas partial pressures (teprob.f:481); D through H get Raoult's law with an Antoine vapour pressure (teprob.f:488-490); the composition is \(y_i = p_i / P\) (teprob.f:495); and the vapour holdup comes back out of the ideal gas law at teprob.f:498 and 501.

The one number worth keeping in mind is that PTS floors around 811 mmHg over the whole sampled domain. That matters for the purge clamp below.

The condenser

A smooth saturating function of reactor outlet flow, approaching 0.404655 as the flow grows (teprob.f:674), with the duty taken against the stream 8 temperature rather than the separator's own, and scaled by a disturbance drift factor (teprob.f:675-676):

\[ U A_s = 0.404655 \left(1 - \frac{1}{1 + (F_8/3528.73)^4}\right) \]

\[ Q_s = U A_s \, (T_{ws} - T_{st,8}) \, (1 - 0.25 \, d_{11}) \]

**2 and **4 are integer powers and must not go through pow

gfortran expands an integer exponent into multiplications rather than calling libm, and the shape of that expansion is load-bearing. Measured over 200,000 values with this project's pinned flags:

candidate for X**4matches gfortran
(x*x)*(x*x)200,000 of 200,000
((x*x)*x)*x132,040
pow(x, 4.0)99,523

So it is binary exponentiation, squaring twice, and the two plausible alternatives are each wrong about a third and a half of the time. X**2 is x*x on all 200,000, which is the only thing it could be.

Three ways out

The underflow to the stripper is valve-lagged, and is the simple case (teprob.f:570):

\[ F_{11} = \frac{v_7 R_7}{100} \]

The purge is pressure-driven to atmosphere through valve 6 (teprob.f:585-588):

\[ F_{10} = \frac{v_6 \times 0.151169 \times \sqrt{\max(P_s - 760,\, 0)}}{\overline{M}_{10}} \]

That clamp at zero is the one clamp in the whole flow network that cannot be reached. PTS is a sum of eight partial pressures in a vessel that always holds material, and it floors around 811 mmHg against a threshold of 760, so no trajectory state, no random perturbation and no adversarial boundary takes that branch. It is therefore covered by a unit test at a composition chosen to reach it, rather than by the differential, on the principle that a branch no test enters is indistinguishable from a branch that is wrong.

The recycle goes through the compressor. The operating point on the curve is the pressure ratio between the mixing zone and the separator, clamped at both ends (teprob.f:589-591), and the machine is fixed-speed with a cubic pressure-ratio curve (teprob.f:592-593). The recycle valve then bleeds flow back and the result has a floor (teprob.f:596-599):

\[ \dot m = \max\!\left( F_{\max}\left(1 + \frac{1 - r^3}{1.197}\right) - v_5 \times 53.349 \times \sqrt{\max(P_v - P_s,\, 0)}, \; 10^{-3} \right) \]

with \(r = \mathrm{clamp}(P_v / P_s,\, 1,\, 1.3)\), \(F_{\max} = 280275\) (teprob.f:1170) and the ratio ceiling CPPRMX at 1.3 (teprob.f:1171). The floor at \(10^{-3}\) exists so that the division at teprob.f:600-601 cannot blow up.

PR**3 at teprob.f:593 is an integer power, for the same reason **4 is above, and is written out as three multiplications rather than routed through pow.

The compressor work appears twice: as an enthalpy bump on the recycle stream, and as measurement 20 (teprob.f:594-595, 601, 699):

\[ W = \dot m \, (T_{cs} + 273.15) \times 1.8 \times 10^{-6} \times 1.9872 \times \frac{P_v - P_s}{\overline{M}_9 P_s} \]

Note that the 273.15 on that line is written 273.15D0, double precision, unlike the one in TESUB2 at teprob.f:1411. The original is not consistent about that constant and each occurrence has to be read off its own line.

HST(10) = HST(9) is a snapshot, not an alias

teprob.f:562 copies the separator vapour enthalpy into the purge. Both streams leave the separator vapour space, so at that moment they are the same fluid at the same temperature and the copy is exact.

Then teprob.f:601 adds the compressor work to HST(9). The recycle gains the work; the purge does not, because it was copied first. Reading line 562 as an alias rather than as a copy would give the purge a share of compressor work it never receives, and the two lines are seventy apart, so the ordering is easy to miss. Both energy balances that read stream 9 come after line 601 and therefore see the bumped value.

Balances

One inlet, three outlets, and the condenser duty (teprob.f:762-770 and 773-777):

\[ \frac{dn_i}{dt} = \dot n_{i,8} - \dot n_{i,9} - \dot n_{i,10} - \dot n_{i,11} \]

\[ \frac{dE}{dt} = h_8 F_8 - h_9 F_9 - h_{10} F_{10} - h_{11} F_{11} + Q_s \]

The condenser cooling water wall temperature is YP(38) (teprob.f:791-792).

Variables

FortranMeaningWhere
VTS, VLS, VVStotal, liquid and vapour volumeteprob.f:1119, 471, 474
PPS(1:8), PTSpartial and total pressure, mmHgteprob.f:481, 489, 490
XVS, XLSvapour and liquid mole fractionsteprob.f:495, 452
UTVS, UCVStotal and per-component vapour molesteprob.f:498, 501
TCS, TKStemperature, Celsius and kelvinteprob.f:462-463
DLSliquid molar densityteprob.f:468
UAS, QUScondenser coefficient and dutyteprob.f:674-676
PR, CPPRMXcompressor pressure ratio and its ceilingteprob.f:589-591, 1171
CPFLMXmaximum compressor flowteprob.f:1170
CPDHcompressor enthalpy bumpteprob.f:594-595
FTM(9), FTM(10), FTM(11)recycle, purge, underflowteprob.f:600, 588, 570
TWScooling water outlet temperature, YY(38)teprob.f:436
YP(10..17), YP(18)component and energy derivativesteprob.f:762-770, 773-777

Two of the eight shutdown conditions belong to this vessel: separator liquid volume above 12 or below 1 cubic metre (teprob.f:707-708).

The stripper

Steam strips light components out of the separator liquid. What leaves overhead rejoins the mixing zone as stream 5; what does not becomes the stripper's own liquid, stream 12, which leaves as the product, stream 13. The vessel is treated as liquid only: it has no vapour space of its own in the model.

Source: teprob.f:614-662 for the column, teprob.f:677-678 for the reboiler, and teprob.f:778-782 for the energy balance.

Equations

The feed is the mixed A/C feed plus the separator underflow (teprob.f:635-639). Note that stream 4 arrives here directly rather than through the mixing zone:

\[ f_i = \dot n_{i,4} + \dot n_{i,11} \]

A vapour-to-liquid ratio sets how hard the column strips, scaled by a temperature factor (teprob.f:622):

\[ \Lambda = \frac{F_4}{F_{11}} \, \tau(T_c) \]

Each condensible then strips according to a Langmuir-shaped saturating function of that ratio (teprob.f:623-627):

\[ s_i = \frac{k_i \Lambda}{1 + k_i \Lambda}, \qquad i \in \{D \ldots H\} \]

species\(k_i\)
D8.5010
E11.402
F11.795
G0.0480
H0.0242

and the split is simply (teprob.f:643-644)

\[ \dot n_{i,5} = s_i f_i, \qquad \dot n_{i,12} = f_i - \dot n_{i,5} \]

Both product streams leave at the stripper's own temperature (teprob.f:652-653), and their enthalpies are taken on different bases: stream 5 with ITY = 1, the vapour basis, and stream 12 with ITY = 0, the liquid one (teprob.f:654-655).

The temperature factor has a pole at 177 C

\[ \tau(T) = \begin{cases} T - 120.262 & T > 170 \\ 0.1 & T < 5.292 \\ \dfrac{363.744}{177 - T} - 2.22579488 & \text{otherwise} \end{cases} \]

from teprob.f:615-621. The middle branch diverges at 177 C, which is inside the range the two outer branches leave for it only if TCC exceeds 170, and it does not: the T > 170 branch takes over first. So the pole is unreachable by seven degrees, and the two branches are continuous to within 0.1% at 170.

The adversarial state catalogue built for Tier 2 places a state at 176 C anyway, to sit near the pole and confirm that it stays on the linear branch. That state is coverage of the guard, not of the pole.

FTM(11) > 0.1 switches the whole block

Below that threshold the column is not really running, and teprob.f:629-633 substitutes five fixed stripping factors (0.9999, 0.999, 0.999, 0.99, 0.98) rather than evaluating the correlation. The reason is visible in the arithmetic: \(\Lambda = F_4 / F_{11}\) diverges as \(F_{11} \to 0\).

Both sides are covered by the adversarial catalogue, which places a state exactly on FTM(11) = 0.1. Since the test at teprob.f:614 is .GT., that state takes the fixed-factor branch.

SFR(1..3) are never recomputed

teprob.f:623-627 and 629-633 both write slots 4 through 8 only. Slots 1, 2 and 3 are set once in TEINIT (teprob.f:1126-1128) and are read at teprob.f:643 on every evaluation, so A, B and C strip at a fixed 99.5%, 99.1% and 99.0% no matter what the column is doing.

That is the intended physics rather than an oversight: the non-condensibles are gases, they leave overhead essentially completely, and no temperature or flow ratio in the plant's range would change that. It is worth stating because the loop at teprob.f:643 runs I=1,8 and looks as though all eight factors come from the branch above it.

The reboiler

Steam is at 100 C, so above that there is nothing to transfer and the original sets the duty to zero rather than letting it go negative (teprob.f:677-678):

\[ Q_c = \begin{cases} U A_c \, (100 - T_c) & T_c < 100 \\ 0 & \text{otherwise} \end{cases} \]

The coefficient UAC is not computed here. It is a valve-lagged capacity with a disturbance drift factor, set at teprob.f:572:

\[ U A_c = \frac{v_9 R_9 (1 + d_9)}{100} \]

which is the point at which IDV(16), published as "Unknown", enters the model.

The nominal trajectory sits near 65 C, so the cutoff is the branch at risk of never being exercised. B-0021 measured 300 of 300 nominal states below 100.

The reactor inlet is an alias, and this is where it is made

teprob.f:656-661 copies flow, enthalpy, temperature, composition and component flows from stream 6 to stream 7 wholesale. There is no mixing, no pressure drop and no heat loss between them: stream 7 exists so that the reactor's balance at teprob.f:763-772 can name its own inlet. It lives in the stripper block for no reason other than that is where the original put it.

Balances, and an asymmetry between them

The component balances are a straight pass-through of the two streams the column produced (teprob.f:762-770), while the energy balance names the column's inputs instead (teprob.f:778-782):

\[ \frac{dn_i}{dt} = \dot n_{i,12} - \dot n_{i,13} \]

\[ \frac{dE}{dt} = h_4 F_4 + h_{11} F_{11} - h_5 F_5 - h_{13} F_{13} + Q_c \]

That is what the source does, and the two forms describe the same vessel: the component split at teprob.f:643-644 conserves moles by construction, so streams 4 and 11 in, less stream 5 out, is stream 12.

Variables

FortranMeaningWhere
VTC, VLCvessel and liquid volumeteprob.f:1120, 472
UCLC(1:8)liquid component holdup, YY(19..26)teprob.f:427
ETCinternal energy, YY(27)teprob.f:433
XLCliquid mole fractionsteprob.f:453
TCCtemperature, degrees Celsiusteprob.f:464
DLCliquid molar densityteprob.f:469
TMPFACtemperature scalingteprob.f:615-621
VOVRLvapour-to-liquid ratioteprob.f:622
SFR(1:8)fraction of each species strippedteprob.f:623-633, 1126-1128
FIN(1:8)combined feed to the columnteprob.f:635-639
UAC, QUCreboiler coefficient and dutyteprob.f:572, 677-678
YP(19..26), YP(27)component and energy derivativesteprob.f:762-770, 778-782

Two of the eight shutdown conditions belong to this vessel: stripper liquid volume above 8 or below 1 cubic metre (teprob.f:709-710). Its level measurement is also the odd one out among the three, because its span is the vessel volume VTC itself rather than a separately hard-coded range (teprob.f:693).

Instrumentation

Forty-one measurements come out of the plant, and they are not all the same kind of thing. XMEAS(1..22) are continuous instruments, read every step, computed at teprob.f:679-701 and given additive noise at teprob.f:711-735. XMEAS(23..41) are composition analysers, read on a schedule and reporting the composition from their previous sample (teprob.f:736-761).

Nothing in the continuous block is physics. Every one of the twenty-two lines takes a quantity the model has already computed and converts it into the unit an operator's instrument would read. Getting a conversion wrong changes no state and no derivative; it changes only what the controller sees, which is worse, because the plant then runs correctly and is controlled wrongly.

The conversion factors

FactorWhereMeaning
0.359679, 682-684, 688standard cubic feet per lbmol
35.3145the same lines, and 692, 695, 704-710cubic feet per cubic metre
0.454680, 681, 697kilograms per pound
760685, 691, 694mmHg per atmosphere
101.325the same threekPa per atmosphere

So FTM * 0.359 / 35.3145 is lbmol/h to standard cubic metres per hour, and (P - 760)/760 * 101.325 is mmHg absolute to kPa gauge. Levels are reported as a percentage of a span, and the three vessels do not do it the same way: the reactor and the separator carry hard-coded ranges (teprob.f:686, 690) while the stripper's span is the vessel volume VTC itself (teprob.f:693).

XMEAS(20) is assigned twice

      XMEAS(20)=CPDH*0.0003927D6
      XMEAS(20)=CPDH*0.29307D3

at teprob.f:698-699. The first is dead, and the two factors are not equal: 392.7 against 293.07, a third apart. So this is not a harmless duplicate but a superseded conversion, and a port that took the first line would report compressor work 34% high. That is delta D-006.

The shutdown detector

Eight limits, checked at teprob.f:702-710:

ConditionLimitLine
reactor pressure highabove 3000 kPa gauge703
reactor level highabove 24 cubic metres704
reactor level lowbelow 2 cubic metres705
reactor temperature highabove 175 C706
separator level highabove 12 cubic metres707
separator level lowbelow 1 cubic metre708
stripper level highabove 8 cubic metres709
stripper level lowbelow 1 cubic metre710

The original records only that something tripped, in a single integer ISD. This port reports which, because "the plant tripped" without a reason is nearly useless to a caller and the information is free.

All eight comparisons are strict, so a state exactly on a limit does not trip. That matters for how the adversarial sampling pool is built: states placed on the limits exercise the not-tripped side, and the tripping side needs states past them. Both were built.

Two of the eight are phrased in terms of XMEAS rather than the underlying quantity. teprob.f:703 tests the converted reactor pressure against 3000 kPa gauge, and teprob.f:706 tests XMEAS(9), which is TCR unconverted. Testing PTR against an equivalent mmHg threshold instead would be arithmetically different in the last bits.

What a trip does is described in The right-hand side: it freezes all fifty derivatives (teprob.f:807-811), which is delta D-007.

Noise and dead time

Noise is drawn by TESUB6 (teprob.f:1538-1546), twelve uniform draws summed and scaled, and is skipped entirely at TIME = 0 and on a tripped plant (teprob.f:711). Only the continuous noise is skipped, though. The analyser blocks at teprob.f:744-761 have no such guard, so a tripped plant still draws: 258 draws in a tripped evaluation against 522 in a healthy one, measured in B-0027. A port that silenced everything on a trip would leave the generator 264 steps behind and desynchronise every later draw.

The dead time is a latch, and the order of two lines makes it:

      XMEAS(I)=XDEL(I)
      CALL TESUB6(XNS(I),XMNS)
      XMEAS(I)=XMEAS(I)+XMNS
      XDEL(I)=XCMP(I)

The reported value is taken from the store before the store is updated. Swapping those two lines gives an analyser with no dead time at all, which produces entirely plausible numbers and a plant that is much easier to control than the real one.

Three further details are about when rather than about what. The schedules advance from their own previous value rather than from the current time (TGAS = TGAS + 0.1 at teprob.f:751), so a step arriving late does not shift the schedule. Both 0.1 literals, at teprob.f:741 and 751, are single precision, so the gas interval is 0.10000000149011612 and a step landing on exactly 0.1 does not sample; 0.25 is exactly representable, so the product analyser is unaffected, which is precisely the kind of inconsistency that has to be read off the line rather than inferred from its neighbour. And at TIME = 0 the analysers are primed rather than sampled (teprob.f:736-743): the store and the reported value are both set to the current composition, with no noise and no draw.

The 53 channels

Measurements and manipulated variables together are the 53 columns every downstream consumer sees, in this order. The measurement names follow Downs and Vogel's Table 4 and the manipulated ones their Table 3.

#XMEAS#XMEAS
1A feed22condenser cooling water outlet
2D feed23reactor feed, A
3E feed24reactor feed, B
4total feed25reactor feed, C
5recycle flow26reactor feed, D
6reactor feed rate27reactor feed, E
7reactor pressure28reactor feed, F
8reactor level29purge, A
9reactor temperature30purge, B
10purge rate31purge, C
11separator temperature32purge, D
12separator level33purge, E
13separator pressure34purge, F
14separator underflow35purge, G
15stripper level36purge, H
16stripper pressure37product, D
17stripper underflow38product, E
18stripper temperature39product, F
19stripper steam flow40product, G
20compressor work41product, H
21reactor cooling water outlet

XMEAS(23..28) and XMEAS(29..36) are the two gas analysers, on a 0.1 hour schedule; XMEAS(37..41) is the product analyser, on 0.25 hours.

#XMV#XMV
1D feed flow7separator underflow
2E feed flow8stripper underflow
3A feed flow9stripper steam
4total feed flow10reactor cooling water flow
5compressor recycle11condenser cooling water flow
6purge valve12agitator speed

XMV(12), the agitator, is never written by any controller the driver calls, so in a closed-loop run it has zero variance. That is a fact about the control scheme rather than about the plant, and it is the reason the Tier 5 harness had to learn to report a degenerate ensemble as NaN rather than as a number.

Validation

This is the part of the project that determines whether anyone trusts the result, so it gets the most care. The strategy is a ladder: prove the pieces exactly, prove the derivative to near machine precision, prove the stochastic call order exactly, then prove the long-run behaviour statistically and, finally, prove it on the downstream task people actually care about.

Every number on this page was measured, and each one names the LOG.org iteration that measured it. None of them is a target, a plan, or a rounded recollection. The project's operating rule is to record numbers rather than verdicts, precisely so that a degradation inside tolerance is still visible: if a maximum relative error moves from 3e-14 to 8e-13, both pass a 1e-12 gate and something has broken, and the only place that is visible is the logged history.

The figures are held to the same rule. Each one is drawn by cargo xtask validate from the run's own output, committed alongside the generated chapters, and captioned with the condition that would make it false. Nothing in one is placed by hand: a marker is orange because its value is on the wrong side of a gate, never because a test's name was recognised, so a genuine regression and a deliberate positive control are drawn identically and the caption is what tells them apart.

All numbers below were produced with gfortran 15.2.0 and the pinned rustc 1.97.1. The oracle's compiler flags are fixed in build.rs and asserted by a test; changing them invalidates every Tier 1 and Tier 2 number here, so it is a logged re-baseline rather than a casual edit.

The oracle harness

tepsim-oracle is a development-only crate whose build.rs compiles the unmodified teprob.f and temain_mod.f with gfortran and links them through a small C shim exposing TEINIT, TEFUNC, each TESUBn, and read and write access to every COMMON block. A Rust test can therefore set the Fortran into an arbitrary state, call it, and compare against the Rust implementation in the same process.

That crate is never a dependency of tepsim, tepsim-py or tepsim-wasm. It runs on Linux and macOS runners where gfortran is available, and building it first is what converts the whole port from "read carefully and hope" into a differential testing exercise.

Alongside it sits a cheaper check that needs no Fortran at all. cargo xtask fidelity runs the port forward 100 steps from the nominal state and diffs states, derivatives, measurements and the generator word against a golden trace committed to the repository. It takes about a second and runs at the top of every session. Since B-0026 it has reported 100 of 100 steps diffed, worst 3.521e-14 at YP(12) step 77 against a 1e-12 gate, and that same number has now been recorded unchanged for eleven consecutive iterations, most recently in the entry for B-0052.

Tier 1: the utility routines, exactly

TESUB1 (enthalpy), TESUB2 (temperature from enthalpy by Newton), TESUB3 (heat capacity) and TESUB4 (liquid density) are swept over a simplex grid, ten million random Dirichlet samples, and a boundary pool, at every temperature in the physical range, for each of the three ITY modes. The gate PLAN.org sets is a maximum relative error below 1e-13 with a ULP histogram reported rather than a pass or fail.

The measured result is not "inside 1e-13". It is zero.

routineITYcasesmax relative errormax ULPhistogramfrom
TESUB109,987,4900.000e000:9987490B-0009
TESUB119,987,4900.000e000:9987490B-0009
TESUB129,987,4900.000e000:9987490B-0009
TESUB309,987,4900.000e000:9987490B-0009
TESUB319,987,4900.000e000:9987490B-0009
TESUB329,987,4900.000e000:9987490B-0009
TESUB4n/a9,987,4900.000e000:9987490B-0010

That is 59,924,940 evaluations across TESUB1 and TESUB3 with zero differing bits, p50 = p90 = p99 = p100 = 0 in all six sweeps. A separate test asserts bit equality directly, because a 1e-13 threshold would let a drift to 1e-15 pass unnoticed forever.

The 1e-13 gate is therefore not what is holding the line, and the entry for B-0009 says why that is the right expectation: both routines are straight-line arithmetic over constants already proved bit-identical, so once the association and the literal precisions are right there is nothing left to differ by. Any future Tier 1 routine that lands at 1e-15 rather than at zero has something wrong with it that a tolerance would hide.

The sabotage check. Substituting a double-precision 273.15 for the widened f32 at teprob.f:1411, and changing nothing else, gives a maximum relative error of 1.597e-5 at grid#704 T=21.875, 103,098,852,352 ULP (B-0009). That is the size of the error a single mis-transcribed literal produces, and it is why constants in this project are transcribed and asserted rather than retyped.

TESUB2 is the Newton solve, and it is measured from two starting strategies (B-0011). A warm start, which is what every call site actually does, converges in one step and recovers the answer exactly: round-trip error 0e0. A cold start from the far end of the range is what exercises the iteration, and its worst round trip is 5.12e-13 C, at dirichlet#720561 T=171.86, face#1309309 T=146.56 and face#96 T=144.14. Over 59,924,940 solves there were zero differing bits and zero abandoned iterations, which is the measurement that makes delta D-001's effect zero on the physical domain.

TESUB7, the random generator, must be exact and is (B-0005): 10^7 draws from the compiled-in seed with the XOR fold and the final state both matching; 10^6 draws each from five dataset seeds; draw-by-draw comparison over 200,000 draws for four seeds with every draw and every intermediate state bit-identical; and interleaved output modes over 50,000 draws on an irregular pattern. Its exactness rests on reproducing the rounding rather than removing it: the product exceeds 2^53 on 0.7716 of draws, against 0.7728 predicted from the arithmetic, so a "fixed" integer recurrence diverges at draw 0.

TESUB5 and TESUB6 are exact in both libm configurations, since neither touches a transcendental, and their draw counts were recovered independently by stepping a port-side generator from the word before each call to the word after: 3 draws for TESUB5, 12 for TESUB6, on every case and for both flag values (B-0028).

Tier 2: single-step derivative equivalence

Both implementations are forced into an identical state, all fifty states, the twelve manipulated variables, the twenty IDV flags, the full walk state, the generator word and the nineteen held analyser readings, evaluated once, and compared on all fifty components. Sampling is from three pools: states along the nominal closed-loop trajectory, random perturbations of those states scaled across several orders of magnitude, and adversarial states placed deliberately at every discontinuity and clamp in the model.

The tolerance is relative to the scale of the terms, not to the result, and that is a decision with a measurement behind it rather than a relaxation. A balance is inflow minus outflow, and near steady state those nearly agree. YP(2), the inert's reactor balance, is a difference of two flows around 660 whose result is a few parts in ten thousand of either. One ULP of difference in each term, which is all the vendored libm costs, is 1e-16 of the terms and 1e-4 of the result. Measured against the result, 28 of the 50 components exceed 1e-12 while the whole right-hand side is bit-identical to gfortran under libm-system. Each balance therefore reports the magnitude of its largest term alongside its value, and the gate is the error over that.

Acceptance, from B-0026: all fifty components, 2,412 running states, all three pools, worst 6.093e-14 at YP(7), perturbed#300, against a 1e-12 gate.

The whole tier fits in one picture. Every comparison it makes, run twice, once against each libm, plotted at its own maximum relative error:

GENERATED by `cargo xtask validate --tiers 1,2,3 --smoke` from commit `b6ef4ee-dirty`. Do not edit by hand: the next run overwrites it. Tier 2: every comparison against the 1e-12 gateA logarithmic strip plot. Each dot is one comparison against the Fortran, placed at its maximum relative error, in a lane named for the test target that produced it. 115 of 141 comparisons are exactly zero and sit in the separate lane at the left. The gate at 1e-12 is a dashed vertical line with the region beyond it shaded; 1 dots lie beyond it. Tier 2: every comparison, against the gate 141 comparisons over 9 target(s). 115 are bit-identical to the Fortran. 1 lies beyond the gate. 1e-16 1e-14 1e-12 1e-10 1e-8 1e-6 1e-4 1e-2 maximum relative error against the Fortran = 0 gate 1e-12 tier2_unpack tier1 TCV with the carried seed: exactly 0 (vendored libm, solving_from_a_fixed_guess_instead_of_the_seed_breaks_bit_equality) tier1 TCV from a fixed guess: 9.901e-16 (vendored libm, solving_from_a_fixed_guess_instead_of_the_seed_breaks_bit_equality) tier1 unpack UCLR: exactly 0 (vendored libm, the_unpacking_matches_the_fortran_over_all_three_pools) tier1 unpack UCLS: exactly 0 (vendored libm, the_unpacking_matches_the_fortran_over_all_three_pools) tier1 unpack UCLC: exactly 0 (vendored libm, the_unpacking_matches_the_fortran_over_all_three_pools) tier1 unpack UCVV: exactly 0 (vendored libm, the_unpacking_matches_the_fortran_over_all_three_pools) tier1 unpack UTLR/UTLS/UTLC/UTVV: exactly 0 (vendored libm, the_unpacking_matches_the_fortran_over_all_three_pools) tier1 unpack XLR: exactly 0 (vendored libm, the_unpacking_matches_the_fortran_over_all_three_pools) tier1 unpack XLS: exactly 0 (vendored libm, the_unpacking_matches_the_fortran_over_all_three_pools) tier1 unpack XLC: exactly 0 (vendored libm, the_unpacking_matches_the_fortran_over_all_three_pools) tier1 unpack XVV: exactly 0 (vendored libm, the_unpacking_matches_the_fortran_over_all_three_pools) tier1 unpack ESR/ESS/ESC/ESV: exactly 0 (vendored libm, the_unpacking_matches_the_fortran_over_all_three_pools) tier1 unpack TCR/TCS/TCC/TCV: exactly 0 (vendored libm, the_unpacking_matches_the_fortran_over_all_three_pools) tier1 unpack TKR/TKS/TKV: exactly 0 (vendored libm, the_unpacking_matches_the_fortran_over_all_three_pools) tier1 unpack DLR/DLS/DLC: exactly 0 (vendored libm, the_unpacking_matches_the_fortran_over_all_three_pools) tier1 unpack VLR/VLS/VLC: exactly 0 (vendored libm, the_unpacking_matches_the_fortran_over_all_three_pools) tier2_equilibrium tier1 equilibrium VVR/VVS: exactly 0 (vendored libm, the_equilibrium_matches_the_fortran_over_all_three_pools) tier1 equilibrium PPR: 2.802e-16 (vendored libm, the_equilibrium_matches_the_fortran_over_all_three_pools) tier1 equilibrium PPS: 2.784e-16 (vendored libm, the_equilibrium_matches_the_fortran_over_all_three_pools) tier1 equilibrium PTR/PTS/PTV: 3.578e-16 (vendored libm, the_equilibrium_matches_the_fortran_over_all_three_pools) tier1 equilibrium XVR: 4.843e-16 (vendored libm, the_equilibrium_matches_the_fortran_over_all_three_pools) tier1 equilibrium XVS: 4.302e-16 (vendored libm, the_equilibrium_matches_the_fortran_over_all_three_pools) tier1 equilibrium UTVR/UTVS: 4.443e-16 (vendored libm, the_equilibrium_matches_the_fortran_over_all_three_pools) tier1 equilibrium UCVR: 5.077e-16 (vendored libm, the_equilibrium_matches_the_fortran_over_all_three_pools) tier1 equilibrium UCVS: 6.230e-16 (vendored libm, the_equilibrium_matches_the_fortran_over_all_three_pools) tier1 PPR/PPS for A, B and C: exactly 0 (vendored libm, the_ideal_gas_partial_pressures_are_bit_identical_under_the_vendored_libm) tier1 PTV: exactly 0 (vendored libm, the_ideal_gas_partial_pressures_are_bit_identical_under_the_vendored_libm) tier1 equilibrium VVR/VVS: exactly 0 (platform libm, the_algebra_is_bit_identical_once_exp_agrees) tier1 equilibrium PPR: exactly 0 (platform libm, the_algebra_is_bit_identical_once_exp_agrees) tier1 equilibrium PPS: exactly 0 (platform libm, the_algebra_is_bit_identical_once_exp_agrees) tier1 equilibrium PTR/PTS/PTV: exactly 0 (platform libm, the_algebra_is_bit_identical_once_exp_agrees) tier1 equilibrium XVR: exactly 0 (platform libm, the_algebra_is_bit_identical_once_exp_agrees) tier1 equilibrium XVS: exactly 0 (platform libm, the_algebra_is_bit_identical_once_exp_agrees) tier1 equilibrium UTVR/UTVS: exactly 0 (platform libm, the_algebra_is_bit_identical_once_exp_agrees) tier1 equilibrium UCVR: exactly 0 (platform libm, the_algebra_is_bit_identical_once_exp_agrees) tier1 equilibrium UCVS: exactly 0 (platform libm, the_algebra_is_bit_identical_once_exp_agrees) tier1 equilibrium VVR/VVS: exactly 0 (platform libm, the_equilibrium_matches_the_fortran_over_all_three_pools) tier1 equilibrium PPR: exactly 0 (platform libm, the_equilibrium_matches_the_fortran_over_all_three_pools) tier1 equilibrium PPS: exactly 0 (platform libm, the_equilibrium_matches_the_fortran_over_all_three_pools) tier1 equilibrium PTR/PTS/PTV: exactly 0 (platform libm, the_equilibrium_matches_the_fortran_over_all_three_pools) tier1 equilibrium XVR: exactly 0 (platform libm, the_equilibrium_matches_the_fortran_over_all_three_pools) tier1 equilibrium XVS: exactly 0 (platform libm, the_equilibrium_matches_the_fortran_over_all_three_pools) tier1 equilibrium UTVR/UTVS: exactly 0 (platform libm, the_equilibrium_matches_the_fortran_over_all_three_pools) tier1 equilibrium UCVR: exactly 0 (platform libm, the_equilibrium_matches_the_fortran_over_all_three_pools) tier1 equilibrium UCVS: exactly 0 (platform libm, the_equilibrium_matches_the_fortran_over_all_three_pools) tier2_kinetics tier1 CRXR(2), never assigned: exactly 0 (vendored libm, the_inert_has_no_net_production_in_either_implementation) tier1 kinetics RR: 8.492e-16 (vendored libm, the_kinetics_match_the_fortran_over_all_three_pools) tier1 kinetics CRXR: 8.492e-16 (vendored libm, the_kinetics_match_the_fortran_over_all_three_pools) tier1 kinetics RH: 7.419e-16 (vendored libm, the_kinetics_match_the_fortran_over_all_three_pools) tier1 kinetics RR: exactly 0 (platform libm, the_algebra_is_bit_identical_once_exp_and_pow_agree) tier1 kinetics CRXR: exactly 0 (platform libm, the_algebra_is_bit_identical_once_exp_and_pow_agree) tier1 kinetics RH: exactly 0 (platform libm, the_algebra_is_bit_identical_once_exp_and_pow_agree) tier1 CRXR(2), never assigned: exactly 0 (platform libm, the_inert_has_no_net_production_in_either_implementation) tier1 kinetics RR: exactly 0 (platform libm, the_kinetics_match_the_fortran_over_all_three_pools) tier1 kinetics CRXR: exactly 0 (platform libm, the_kinetics_match_the_fortran_over_all_three_pools) tier1 kinetics RH: exactly 0 (platform libm, the_kinetics_match_the_fortran_over_all_three_pools) tier2_streams tier1 streams XST (10 streams x 8): 4.736e-16 (vendored libm, the_stream_table_matches_the_fortran_over_all_three_pools) tier1 streams XMWS (the 6 that exist): 4.370e-16 (vendored libm, the_stream_table_matches_the_fortran_over_all_three_pools) tier1 streams TST: exactly 0 (vendored libm, the_stream_table_matches_the_fortran_over_all_three_pools) tier1 streams HST: 5.050e-16 (vendored libm, the_stream_table_matches_the_fortran_over_all_three_pools) tier1 streams XST (10 streams x 8): exactly 0 (platform libm, the_algebra_is_bit_identical_once_exp_and_pow_agree) tier1 streams XMWS (the 6 that exist): exactly 0 (platform libm, the_algebra_is_bit_identical_once_exp_and_pow_agree) tier1 streams TST: exactly 0 (platform libm, the_algebra_is_bit_identical_once_exp_and_pow_agree) tier1 streams HST: exactly 0 (platform libm, the_algebra_is_bit_identical_once_exp_and_pow_agree) tier1 streams XST (10 streams x 8): exactly 0 (platform libm, the_stream_table_matches_the_fortran_over_all_three_pools) tier1 streams XMWS (the 6 that exist): exactly 0 (platform libm, the_stream_table_matches_the_fortran_over_all_three_pools) tier1 streams TST: exactly 0 (platform libm, the_stream_table_matches_the_fortran_over_all_three_pools) tier1 streams HST: exactly 0 (platform libm, the_stream_table_matches_the_fortran_over_all_three_pools) tier2_flows tier1 flows FTM (the 10 assembled streams): 2.246e-14 (vendored libm, the_flow_network_matches_the_fortran_over_all_three_pools) tier1 flows FCM (10 streams x 8): 2.268e-14 (vendored libm, the_flow_network_matches_the_fortran_over_all_three_pools) tier1 flows FWR/FWS/AGSP: exactly 0 (vendored libm, the_flow_network_matches_the_fortran_over_all_three_pools) tier1 flows CPDH: 2.034e-15 (vendored libm, the_flow_network_matches_the_fortran_over_all_three_pools) tier1 flows HST(9) after the compressor bump: 6.522e-15 (vendored libm, the_flow_network_matches_the_fortran_over_all_three_pools) tier1 UAC, via QUC: exactly 0 (vendored libm, the_steam_coefficient_matches_through_the_condenser_duty) tier1 flows FTM (the 10 assembled streams): exactly 0 (platform libm, the_algebra_is_bit_identical_once_exp_and_pow_agree) tier1 flows FCM (10 streams x 8): exactly 0 (platform libm, the_algebra_is_bit_identical_once_exp_and_pow_agree) tier1 flows FWR/FWS/AGSP: exactly 0 (platform libm, the_algebra_is_bit_identical_once_exp_and_pow_agree) tier1 flows CPDH: exactly 0 (platform libm, the_algebra_is_bit_identical_once_exp_and_pow_agree) tier1 flows HST(9) after the compressor bump: exactly 0 (platform libm, the_algebra_is_bit_identical_once_exp_and_pow_agree) tier1 flows FTM (the 10 assembled streams): exactly 0 (platform libm, the_flow_network_matches_the_fortran_over_all_three_pools) tier1 flows FCM (10 streams x 8): exactly 0 (platform libm, the_flow_network_matches_the_fortran_over_all_three_pools) tier1 flows FWR/FWS/AGSP: exactly 0 (platform libm, the_flow_network_matches_the_fortran_over_all_three_pools) tier1 flows CPDH: exactly 0 (platform libm, the_flow_network_matches_the_fortran_over_all_three_pools) tier1 flows HST(9) after the compressor bump: exactly 0 (platform libm, the_flow_network_matches_the_fortran_over_all_three_pools) tier1 UAC, via QUC: exactly 0 (platform libm, the_steam_coefficient_matches_through_the_condenser_duty) tier2_stripper tier1 stripper SFR: exactly 0 (vendored libm, the_stripper_matches_the_fortran_over_all_three_pools) tier1 stripper FTM (5, 12): the column's own outlets: exactly 0 (vendored libm, the_stripper_matches_the_fortran_over_all_three_pools) tier1 stripper FTM (7): the reactor-inlet alias: 1.484e-15 (vendored libm, the_stripper_matches_the_fortran_over_all_three_pools) tier1 stripper FCM (5, 12): the column's own outlets: exactly 0 (vendored libm, the_stripper_matches_the_fortran_over_all_three_pools) tier1 stripper FCM (7): the reactor-inlet alias: 1.610e-15 (vendored libm, the_stripper_matches_the_fortran_over_all_three_pools) tier1 stripper XST (5, 12): the column's own outlets: exactly 0 (vendored libm, the_stripper_matches_the_fortran_over_all_three_pools) tier1 stripper XST (7): the reactor-inlet alias: exactly 0 (vendored libm, the_stripper_matches_the_fortran_over_all_three_pools) tier1 stripper TST (5, 7, 12): exactly 0 (vendored libm, the_stripper_matches_the_fortran_over_all_three_pools) tier1 stripper HST (5, 12): the column's own outlets: exactly 0 (vendored libm, the_stripper_matches_the_fortran_over_all_three_pools) tier1 stripper HST (7): the reactor-inlet alias: exactly 0 (vendored libm, the_stripper_matches_the_fortran_over_all_three_pools) tier1 stripper SFR: exactly 0 (platform libm, the_algebra_is_bit_identical_once_exp_and_pow_agree) tier1 stripper FTM (5, 12): the column's own outlets: exactly 0 (platform libm, the_algebra_is_bit_identical_once_exp_and_pow_agree) tier1 stripper FTM (7): the reactor-inlet alias: exactly 0 (platform libm, the_algebra_is_bit_identical_once_exp_and_pow_agree) tier1 stripper FCM (5, 12): the column's own outlets: exactly 0 (platform libm, the_algebra_is_bit_identical_once_exp_and_pow_agree) tier1 stripper FCM (7): the reactor-inlet alias: exactly 0 (platform libm, the_algebra_is_bit_identical_once_exp_and_pow_agree) tier1 stripper XST (5, 12): the column's own outlets: exactly 0 (platform libm, the_algebra_is_bit_identical_once_exp_and_pow_agree) tier1 stripper XST (7): the reactor-inlet alias: exactly 0 (platform libm, the_algebra_is_bit_identical_once_exp_and_pow_agree) tier1 stripper TST (5, 7, 12): exactly 0 (platform libm, the_algebra_is_bit_identical_once_exp_and_pow_agree) tier1 stripper HST (5, 12): the column's own outlets: exactly 0 (platform libm, the_algebra_is_bit_identical_once_exp_and_pow_agree) tier1 stripper HST (7): the reactor-inlet alias: exactly 0 (platform libm, the_algebra_is_bit_identical_once_exp_and_pow_agree) tier1 stripper SFR: exactly 0 (platform libm, the_stripper_matches_the_fortran_over_all_three_pools) tier1 stripper FTM (5, 12): the column's own outlets: exactly 0 (platform libm, the_stripper_matches_the_fortran_over_all_three_pools) tier1 stripper FTM (7): the reactor-inlet alias: exactly 0 (platform libm, the_stripper_matches_the_fortran_over_all_three_pools) tier1 stripper FCM (5, 12): the column's own outlets: exactly 0 (platform libm, the_stripper_matches_the_fortran_over_all_three_pools) tier1 stripper FCM (7): the reactor-inlet alias: exactly 0 (platform libm, the_stripper_matches_the_fortran_over_all_three_pools) tier1 stripper XST (5, 12): the column's own outlets: exactly 0 (platform libm, the_stripper_matches_the_fortran_over_all_three_pools) tier1 stripper XST (7): the reactor-inlet alias: exactly 0 (platform libm, the_stripper_matches_the_fortran_over_all_three_pools) tier1 stripper TST (5, 7, 12): exactly 0 (platform libm, the_stripper_matches_the_fortran_over_all_three_pools) tier1 stripper HST (5, 12): the column's own outlets: exactly 0 (platform libm, the_stripper_matches_the_fortran_over_all_three_pools) tier1 stripper HST (7): the reactor-inlet alias: exactly 0 (platform libm, the_stripper_matches_the_fortran_over_all_three_pools) tier2_heat tier1 heat UAR: exactly 0 (vendored libm, heat_transfer_matches_the_fortran_over_all_three_pools) tier1 heat QUR: exactly 0 (vendored libm, heat_transfer_matches_the_fortran_over_all_three_pools) tier1 heat QUS: 9.572e-13 (vendored libm, heat_transfer_matches_the_fortran_over_all_three_pools) tier1 heat QUC: exactly 0 (vendored libm, heat_transfer_matches_the_fortran_over_all_three_pools) tier1 heat UAR: exactly 0 (platform libm, heat_transfer_matches_the_fortran_over_all_three_pools) tier1 heat QUR: exactly 0 (platform libm, heat_transfer_matches_the_fortran_over_all_three_pools) tier1 heat QUS: exactly 0 (platform libm, heat_transfer_matches_the_fortran_over_all_three_pools) tier1 heat QUC: exactly 0 (platform libm, heat_transfer_matches_the_fortran_over_all_three_pools) tier1 heat UAR: exactly 0 (platform libm, the_algebra_is_bit_identical_once_exp_and_pow_agree) tier1 heat QUR: exactly 0 (platform libm, the_algebra_is_bit_identical_once_exp_and_pow_agree) tier1 heat QUS: exactly 0 (platform libm, the_algebra_is_bit_identical_once_exp_and_pow_agree) tier1 heat QUC: exactly 0 (platform libm, the_algebra_is_bit_identical_once_exp_and_pow_agree) tier2_measurements tier1 XMEAS(1..22), noise-free: 8.774e-15 (vendored libm, the_measurements_match_the_fortran_over_all_three_pools) tier1 ISD as 0 or 1: exactly 0 (vendored libm, the_shutdown_detector_agrees_with_the_fortran_on_every_state) tier1 XMEAS(1..22), noise-free: exactly 0 (platform libm, the_algebra_is_bit_identical_once_exp_and_pow_agree) tier1 XMEAS(1..22), noise-free: exactly 0 (platform libm, the_measurements_match_the_fortran_over_all_three_pools) tier1 ISD as 0 or 1: exactly 0 (platform libm, the_shutdown_detector_agrees_with_the_fortran_on_every_state) tier2_balances tier1 YP(1..50), relative to the derivative (reported): 1.393e-4 (vendored libm, all_fifty_derivatives_match_the_fortran_over_all_three_pools) tier1 YP(1..50), relative to the scale of the terms (the gate): 6.093e-14 (vendored libm, all_fifty_derivatives_match_the_fortran_over_all_three_pools) tier1 YP(1..50), plant frozen: exactly 0 (vendored libm, all_fifty_derivatives_match_the_fortran_over_all_three_pools) tier1 YP(1..50), relative to the scale of the terms (the gate): 6.093e-14 (vendored libm, tier2_acceptance_table) tier1 YP(1..50), relative to the derivative (reported): exactly 0 (platform libm, all_fifty_derivatives_match_the_fortran_over_all_three_pools) tier1 YP(1..50), relative to the scale of the terms (the gate): exactly 0 (platform libm, all_fifty_derivatives_match_the_fortran_over_all_three_pools) tier1 YP(1..50), plant frozen: exactly 0 (platform libm, all_fifty_derivatives_match_the_fortran_over_all_three_pools) tier1 YP(1..50), relative to the derivative (reported): exactly 0 (platform libm, the_whole_right_hand_side_is_bit_identical_once_exp_and_pow_agree) tier1 YP(1..50), relative to the scale of the terms (the gate): exactly 0 (platform libm, tier2_acceptance_table) vendored libm vendored libm platform libm platform libm, bit-exact claim beyond the gate beyond the gate hover a dot for the comparison it came from
Every Tier 2 comparison, against the 1e-12 gate. One lane per test target, one dot per comparison, on a logarithmic axis with a separate lane at the left for the comparisons that are exactly bit-identical. Blue dots are the libm-system build, where both sides call the same exp and the claim is bit equality rather than a tolerance; every one of them is in the zero lane, and one leaving it would falsify that claim. Grey dots are the vendored libm, and every one of them is inside the gate, though one in tier2_heat sits close enough to it to be worth hovering over: a table of maxima hides which comparison is nearest the line, and this does not. Any dot crossing into the shaded region would be a failure. The figure is written by cargo xtask validate whenever it runs Tier 2, from that run's own output; the measurements are the ones recorded for B-0026 in LOG.org, and the generated Tier 2 chapter carries the same figure with the exact command and commit that drew it, plus the same data as a table.
YPerror / scaleerror / valueratio
23.811e-141.393e-43.7e9
304.552e-152.487e-65.5e8
388.696e-151.187e-61.4e8
143.113e-142.618e-68.4e7
76.093e-147.835e-71.3e7
19-27, 37, 39-500.000e00.000e01x

28 of 50 components cancel by more than 100x, and the 22 that do not are exactly 0.000e0: bit-identical rather than merely inside the gate. The acceptance test asserts that contrast and fails if the count of heavily cancelling components drops below twenty, so the reasoning behind the decision expires loudly if the model ever stops cancelling.

Tier 3: RNG call-order equivalence

Both sides are instrumented to emit every draw, and the traces are diffed. This test exists specifically because it is the one the existing Python port would fail: its documented divergence is attributed to "the exact sequence of calls differs due to implementation details", which is exactly the defect a trace diff catches on the first run instead of after a 48-hour statistical comparison.

From B-0029, draw counts at four points in a run, with exact agreement at every one:

timedrawswhat is drawing
00noise skipped, walks reset
1e-6264noise only
0.15462noise, walk advance, gas analysers
0.30522and the product analyser

Scalings in one real evaluation: 30 signed and 432 unit. The 30 is 9 x 3 + 3, the walk advance; the 432 is 36 compositions at twelve draws each. Worst evaluation is 462 draws against a trace buffer capacity of 4096. The instrument is checked against the generator word rather than only against the values, which covers completeness, ordering and fidelity at once, and it holds over 113,088 draws.

Two independent methods agree on the counts. B-0027 measured them with no instrumentation at all, by stepping a port-side generator from the word before a call to the word after, and the trace lengths match that census exactly. Either method alone would be a claim; the two agreeing is evidence.

Tier 4: trajectory equivalence, diagnostic

Tier 4 is a diagnostic, not a gate. Long-horizon divergence between two programs that use different exp implementations is expected. PLAN.org asks for two things: that the error stay below the corresponding measurement noise standard deviation XNS(i) for at least the first several hours, and that the onset of divergence be explained by showing that switching libm moves it.

Open loop, nominal, 8 simulated hours (28,800 steps), from B-0034:

libmworst error, as a fraction of XNS(i)ever outside XNS
vendored5.149e-5never
platform0.000e0never

All 21 scenarios at 4 hours each stay within XNS for the whole run in both configurations, and every one is exactly 0.00e0 on the platform libm. Worst error by scenario on the vendored libm at 4 hours: nominal 2.45e-5, IDV(10) 3.66e-8, IDV(8) 1.42e-8, IDV(13) 1.05e-8, and the rest below 1e-8.

GENERATED by `cargo xtask validate --tiers 4` from commit `b6ef4ee-dirty`. Do not edit by hand: the next run overwrites it. Tier 4: trajectory error against the instrument noiseA logarithmic strip plot with one row per scenario. Each marker is the worst disagreement between the port and the Fortran over a 4 hour run, divided by the measurement noise standard deviation of the channel it occurred on. The band at one, where the error would equal the instrument noise, is shaded; 0 of 42 markers reach it. 21 are exactly zero. Tier 4: how far apart the trajectories get, in units of instrument noise 21 scenarios, 4 h each. Worst error over the run divided by XNS(i). 21 of 42 runs are bit-identical. 1e-12 1e-10 1e-8 1e-6 1e-4 1e-2 1 worst error over the run, as a fraction of that channel's noise = 0 1 x XNS(i) nominal nominal, vendored libm: worst 2.450e-5, inside the noise for 4.000 h nominal, platform libm: worst exactly 0, inside the noise for 4.000 h IDV(1) IDV(1), vendored libm: worst 1.600e-9, inside the noise for 4.000 h IDV(1), platform libm: worst exactly 0, inside the noise for 4.000 h IDV(2) IDV(2), vendored libm: worst 2.660e-9, inside the noise for 4.000 h IDV(2), platform libm: worst exactly 0, inside the noise for 4.000 h IDV(3) IDV(3), vendored libm: worst 2.490e-9, inside the noise for 4.000 h IDV(3), platform libm: worst exactly 0, inside the noise for 4.000 h IDV(4) IDV(4), vendored libm: worst 2.120e-11, inside the noise for 4.000 h IDV(4), platform libm: worst exactly 0, inside the noise for 4.000 h IDV(5) IDV(5), vendored libm: worst 4.160e-10, inside the noise for 4.000 h IDV(5), platform libm: worst exactly 0, inside the noise for 4.000 h IDV(6) IDV(6), vendored libm: worst 1.490e-10, inside the noise for 4.000 h IDV(6), platform libm: worst exactly 0, inside the noise for 4.000 h IDV(7) IDV(7), vendored libm: worst 5.120e-11, inside the noise for 4.000 h IDV(7), platform libm: worst exactly 0, inside the noise for 4.000 h IDV(8) IDV(8), vendored libm: worst 1.420e-8, inside the noise for 4.000 h IDV(8), platform libm: worst exactly 0, inside the noise for 4.000 h IDV(9) IDV(9), vendored libm: worst 1.310e-9, inside the noise for 4.000 h IDV(9), platform libm: worst exactly 0, inside the noise for 4.000 h IDV(10) IDV(10), vendored libm: worst 3.660e-8, inside the noise for 4.000 h IDV(10), platform libm: worst exactly 0, inside the noise for 4.000 h IDV(11) IDV(11), vendored libm: worst 2.410e-9, inside the noise for 4.000 h IDV(11), platform libm: worst exactly 0, inside the noise for 4.000 h IDV(12) IDV(12), vendored libm: worst 4.870e-10, inside the noise for 4.000 h IDV(12), platform libm: worst exactly 0, inside the noise for 4.000 h IDV(13) IDV(13), vendored libm: worst 2.070e-8, inside the noise for 4.000 h IDV(13), platform libm: worst exactly 0, inside the noise for 4.000 h IDV(14) IDV(14), vendored libm: worst 2.450e-5, inside the noise for 4.000 h IDV(14), platform libm: worst exactly 0, inside the noise for 4.000 h IDV(15) IDV(15), vendored libm: worst 2.450e-5, inside the noise for 4.000 h IDV(15), platform libm: worst exactly 0, inside the noise for 4.000 h IDV(16) IDV(16), vendored libm: worst 1.170e-9, inside the noise for 4.000 h IDV(16), platform libm: worst exactly 0, inside the noise for 4.000 h IDV(17) IDV(17), vendored libm: worst 1.210e-9, inside the noise for 4.000 h IDV(17), platform libm: worst exactly 0, inside the noise for 4.000 h IDV(18) IDV(18), vendored libm: worst 2.420e-9, inside the noise for 4.000 h IDV(18), platform libm: worst exactly 0, inside the noise for 4.000 h IDV(19) IDV(19), vendored libm: worst 2.450e-5, inside the noise for 4.000 h IDV(19), platform libm: worst exactly 0, inside the noise for 4.000 h IDV(20) IDV(20), vendored libm: worst 5.780e-8, inside the noise for 4.000 h IDV(20), platform libm: worst exactly 0, inside the noise for 4.000 h vendored libm vendored libm platform libm: the same exp gfortran calls worst anywhere: 2.450e-5 The shaded band is where the disagreement would be as large as the channel's own measurement noise.
All twenty-one scenarios, in units of instrument noise. The worst disagreement anywhere in each run, divided by the noise standard deviation XNS(i) of the channel it happened on, so the reference line is not zero but the point at which the plant's own instruments could resolve the difference. Filled dots are the vendored libm; rings are the platform one, and all twenty-one of those sit in the exactly-zero lane. The claim is false if a marker reaches the shaded band at one, and the explanation is false if a ring ever leaves the zero lane: that would mean the divergence is something other than transcendental rounding. Written by cargo xtask validate --tiers 4 from that run's own output, at the sweep's own horizon of four hours rather than the eight-hour nominal run tabulated above; the same sweep is recorded for B-0034 in LOG.org, and the generated index names the run that drew this copy.

The explanation is better than the one asked for. With identical transcendentals the two trajectories are not merely close, they are bit-identical for 28,800 steps on all 41 measurements. So the divergence is transcendental rounding and nothing else, and the residual under the vendored libm is twenty thousand times below what the instruments could resolve after eight hours.

Closed loop, 48 hours, 172,800 steps, nominal scenario with the driver's forced IDV(12) from hour eight, from B-0041:

libmworst XMEASworst XMV (fraction of range)within XNSat the end
platform0048.000 h0
vendored1.705e-10 at XMEAS(1)1.246e-11 at XMV(10)48.000 h7.882e-11 x XNS(13)

Bit-identical for all 172,800 closed-loop steps under the platform libm. Not one measurement, not one valve, not one step. Under the vendored libm the error never reaches a tenth of a billionth of any instrument's noise over two simulated days.

Compare the open-loop figure above: nominal at 8 hours ends at 5.149e-5 of XNS, six orders of magnitude worse. Closing the loop suppresses the amplification, which is what a controller pulling toward a setpoint should do and is worth having measured rather than assumed.

The same run measures what the control layer is actually buying. Open loop from the same start, with the valves held, trips at step 11,017 (3.060 h) on reactor pressure high. So "48 hours without tripping" is a statement about the control layer, not about a placid plant. B-0052 reproduced that trip from the public API and the CLI, at exactly the same step, without either being told about the other.

Tier 5: statistical equivalence, the real gate

Tier 5 tests equivalence rather than difference, because a failure to reject the null of no difference is not evidence of equivalence and two one-sided tests are. Every statistic ships in Rust, in tepsim-stats, with known-answer tests: Welch's t and the TOST wrapper, the two-sample Kolmogorov-Smirnov statistic, energy distance, autocorrelation, Welch power spectra and the Pearson correlation matrix. Nothing calls out to numpy or scipy.

The harness

From B-0047a: a 48-hour run produces 960 samples of 53 variables, a port run takes 766 ms in release, and a full battery of 2100 runs takes about 27 minutes per source. Over three scenarios the two sources agree bit-identically (0) under the platform libm and to 2.056e-13 under the vendored one, while five nominal seeds spread by 1.366e1, which is the scale that makes those first two numbers meaningful.

The same entry checked that the scenarios are not vacuous. All twenty disturbances move the plant within two hours; the smallest departures are IDV(3) at 4.076e-3, IDV(9) at 3.531e-3 and IDV(15) at 9.839e-3, and the largest are IDV(6) at 3.467e0 and IDV(1) at 2.456e0.

The battery

Four of the six statistics have margins that are measured rather than chosen (B-0047b). The reference's seeds are split in half, the statistic is computed against itself over twenty deterministic splits, and the cross-source value is treated as one more draw from that null, giving p = (1 + #{within >= cross}) / (K + 1) gated at 0.05. With twenty splits the cross-source value fails exactly when it is the strict maximum of the twenty-one.

The smoke battery in the CI gate is 3 scenarios, 4 seeds, 2 hours, 12 runs per source, 14 seconds. The full battery is partial: 3 of 21 scenarios at 100 seeds by 48 hours, stopped by direction after about 25 minutes.

scenarioworst mean powerFrobenius, crosswithin, maxp
nominal1.591.186e-27.5951.0000
IDV(1)1.617.080e-35.7031.0000
IDV(2)0.232.827e-51.9241.0000

Every calibrated statistic passed at p = 1.0000 on all three: the cross-source value was never the maximum of its null. The correlation matrix's Frobenius distance is three to five orders of magnitude inside the within-source spread, which matters because that matrix is exactly what a PCA-based detector consumes.

That relationship, rather than any single number, is what Tier 5 claims, so it is worth drawing:

GENERATED by `cargo xtask validate --tiers 5 --smoke` from commit `03ec3d6-dirty`. Do not edit by hand: the next run overwrites it. Tier 5: the cross-source difference against the reference's own spreadA logarithmic scatter plot. The horizontal axis is a statistic measured between the Fortran and the Rust port; the vertical axis is the same statistic measured between two halves of the Fortran's own runs. Both axes share one scale, so the diagonal is the line of equality. 3 of 5 points lie above it, meaning the two implementations differ by less than the reference differs from itself. Tier 5: the two sources against the reference's own run-to-run spread 3 scenarios x 4 seeds x 2 h, 40 samples per run, vendored libm. A point above the diagonal differs across sources by less than the reference differs from itself. 1e-12 1e-10 1e-8 1e-6 1e-4 1e-2 1 1e2 1e4 1e-12 1e-10 1e-8 1e-6 1e-4 1e-2 1 1e2 1e4 equal: the port is as different as a rerun measured between the Fortran and the port measured inside the Fortran alone KS, one variable shifted 10 sd: across sources 1.000e0, inside the Fortran 7.500e-2 KS, one variable shifted 10 sd energy, one variable shifted 10 sd: across sources 1.228e2, inside the Fortran 3.036e-2 energy, one variable shifted 10 sd correlation matrix, nominal: across sources 6.973e-12, inside the Fortran 1.091e1 correlation matrix, nominal correlation matrix, IDV(1): across sources 3.586e-12, inside the Fortran 9.873e0 correlation matrix, IDV(1) correlation matrix, IDV(2): across sources 4.248e-12, inside the Fortran 1.279e1 correlation matrix, IDV(2) inside the null above the diagonal: inside the reference's own spread outside the null below it: a difference the calibration can see The points below the diagonal are the battery's own positive control: one variable of the reference shifted by ten standard deviations and compared with the unshifted reference.
The two sources against the reference's own run-to-run spread. Horizontally, a statistic measured between the Fortran and the port. Vertically, the same statistic measured between two halves of the Fortran's own runs, which is the null the battery calibrates against. Both axes share one scale, so the diagonal is exactly the line of equality: a point above it means the two implementations differ from each other by less than the reference differs from itself. The claim is false if a cross-source point crosses to the low side. The two points that already sit there are the battery's own positive control, where one variable of the reference was shifted by ten standard deviations before comparing, and they show what a real difference looks like on the same axes. Written by cargo xtask validate --tiers 5 --smoke, so the points are the smoke battery's, 3 scenarios by 4 seeds by 2 h and fourteen seconds of running, not the full one: the figure's own subtitle says which, and the full-battery numbers are the ones tabulated above and recorded for B-0047b in LOG.org. At smoke size the permutation tests cannot reject at all, which is why the picture shows the gap between the cross-source value and its null rather than a verdict.

That plot answers "are the two sources closer to each other than the reference is to itself", which is the calibrated question. It does not answer "how much room is left", and neither does a TOST verdict: a p-value reports that a test did not reject, not by what distance. The margin is a stated quantity, a tenth of the reference's standard deviation for each variable, so the distance can simply be drawn.

GENERATED by `cargo xtask validate --tiers 5 --smoke` from commit `03ec3d6-dirty`. Do not edit by hand: the next run overwrites it. Tier 5: each variable's mean difference against its equivalence marginA logarithmic strip plot with one row per scenario. Each marker is one of the 53 variables: the paired difference between the Fortran's mean and the port's, divided by that variable's own equivalence margin. One is the gate, and the region past it is shaded. 0 of 156 gated markers reach it. 3 are exactly zero. A further 3 variables are not judged this way and are drawn hollow; a constant reference has no margin, so it sits in the zero lane. Tier 5: how far inside its margin each variable sits 3 scenarios x 4 seeds x 2 h, 40 samples per run, vendored libm, 53 variables per scenario. Paired mean difference divided by that variable's equivalence margin. 1e-30 1e-27 1e-24 1e-21 1e-18 1e-15 1e-12 1e-9 1e-6 1e-3 1 paired mean difference, as a fraction of the equivalence margin = 0 the margin nominal nominal, XMEAS/XMV column 1: difference -3.969e-15, margin 2.497e-3, ratio 1.589e-12 (judged against the margin) nominal, XMEAS/XMV column 2: difference 1.364e-12, margin 3.554e0, ratio 3.839e-13 (judged against the margin) nominal, XMEAS/XMV column 3: difference 2.501e-12, margin 3.354e0, ratio 7.458e-13 (judged against the margin) nominal, XMEAS/XMV column 4: difference 7.994e-15, margin 7.635e-3, ratio 1.047e-12 (judged against the margin) nominal, XMEAS/XMV column 5: difference 6.217e-15, margin 2.142e-2, ratio 2.902e-13 (judged against the margin) nominal, XMEAS/XMV column 6: difference -1.776e-14, margin 2.214e-2, ratio 8.022e-13 (judged against the margin) nominal, XMEAS/XMV column 7: difference 7.958e-13, margin 3.860e-1, ratio 2.062e-12 (judged against the margin) nominal, XMEAS/XMV column 8: difference 2.132e-14, margin 4.297e-2, ratio 4.961e-13 (judged against the margin) nominal, XMEAS/XMV column 9: difference -3.553e-15, margin 1.858e-3, ratio 1.912e-12 (judged against the margin) nominal, XMEAS/XMV column 10: difference -1.263e-15, margin 1.131e-3, ratio 1.116e-12 (judged against the margin) nominal, XMEAS/XMV column 11: difference -3.553e-15, margin 1.515e-2, ratio 2.345e-13 (judged against the margin) nominal, XMEAS/XMV column 12: difference 7.105e-15, margin 1.039e-1, ratio 6.837e-14 (judged against the margin) nominal, XMEAS/XMV column 13: difference 6.821e-13, margin 4.006e-1, ratio 1.703e-12 (judged against the margin) nominal, XMEAS/XMV column 14: difference -9.770e-15, margin 1.069e-1, ratio 9.137e-14 (judged against the margin) nominal, XMEAS/XMV column 15: difference 1.776e-14, margin 9.759e-2, ratio 1.820e-13 (judged against the margin) nominal, XMEAS/XMV column 16: difference 3.411e-13, margin 3.617e-1, ratio 9.429e-13 (judged against the margin) nominal, XMEAS/XMV column 17: difference 8.882e-16, margin 6.300e-2, ratio 1.410e-14 (judged against the margin) nominal, XMEAS/XMV column 18: difference -3.197e-14, margin 9.227e-3, ratio 3.465e-12 (judged against the margin) nominal, XMEAS/XMV column 19: difference -8.527e-14, margin 3.553e-1, ratio 2.400e-13 (judged against the margin) nominal, XMEAS/XMV column 20: difference 2.416e-13, margin 5.518e-2, ratio 4.379e-12 (judged against the margin) nominal, XMEAS/XMV column 21: difference -1.421e-14, margin 1.125e-2, ratio 1.264e-12 (judged against the margin) nominal, XMEAS/XMV column 22: difference -1.066e-14, margin 2.295e-2, ratio 4.644e-13 (judged against the margin) nominal, XMEAS/XMV column 23: difference 1.954e-14, margin 2.542e-2, ratio 7.686e-13 (judged against the margin) nominal, XMEAS/XMV column 24: difference -5.329e-15, margin 9.114e-3, ratio 5.847e-13 (judged against the margin) nominal, XMEAS/XMV column 25: difference 1.776e-15, margin 2.438e-2, ratio 7.285e-14 (judged against the margin) nominal, XMEAS/XMV column 26: difference -1.332e-15, margin 1.121e-2, ratio 1.188e-13 (judged against the margin) nominal, XMEAS/XMV column 27: difference -2.043e-14, margin 2.517e-2, ratio 8.115e-13 (judged against the margin) nominal, XMEAS/XMV column 28: difference -2.220e-15, margin 2.574e-3, ratio 8.625e-13 (judged against the margin) nominal, XMEAS/XMV column 29: difference 3.020e-14, margin 2.657e-2, ratio 1.137e-12 (judged against the margin) nominal, XMEAS/XMV column 30: difference -1.066e-14, margin 1.011e-2, ratio 1.055e-12 (judged against the margin) nominal, XMEAS/XMV column 31: difference 1.421e-14, margin 2.702e-2, ratio 5.260e-13 (judged against the margin) nominal, XMEAS/XMV column 32: difference -3.331e-16, margin 9.541e-3, ratio 3.491e-14 (judged against the margin) nominal, XMEAS/XMV column 33: difference -2.931e-14, margin 2.357e-2, ratio 1.244e-12 (judged against the margin) nominal, XMEAS/XMV column 34: difference -2.220e-15, margin 2.600e-3, ratio 8.541e-13 (judged against the margin) nominal, XMEAS/XMV column 35: difference -2.887e-15, margin 5.031e-3, ratio 5.738e-13 (judged against the margin) nominal, XMEAS/XMV column 36: difference -1.332e-15, margin 4.308e-3, ratio 3.093e-13 (judged against the margin) nominal, XMEAS/XMV column 37: difference -3.730e-17, margin 9.354e-4, ratio 3.987e-14 (judged against the margin) nominal, XMEAS/XMV column 38: difference -1.138e-15, margin 9.852e-4, ratio 1.155e-12 (judged against the margin) nominal, XMEAS/XMV column 39: difference -1.076e-16, margin 8.378e-4, ratio 1.284e-13 (judged against the margin) nominal, XMEAS/XMV column 40: difference 5.329e-15, margin 4.061e-2, ratio 1.312e-13 (judged against the margin) nominal, XMEAS/XMV column 41: difference 3.553e-15, margin 4.198e-2, ratio 8.463e-14 (judged against the margin) nominal, XMEAS/XMV column 42: difference 1.243e-14, margin 6.154e-2, ratio 2.020e-13 (judged against the margin) nominal, XMEAS/XMV column 43: difference 2.665e-14, margin 4.072e-2, ratio 6.544e-13 (judged against the margin) nominal, XMEAS/XMV column 44: difference -3.872e-13, margin 2.464e-1, ratio 1.572e-12 (judged against the margin) nominal, XMEAS/XMV column 45: difference 3.908e-14, margin 1.162e-1, ratio 3.364e-13 (judged against the margin) nominal, XMEAS/XMV column 46: difference 8.438e-14, margin 2.702e-2, ratio 3.123e-12 (judged against the margin) nominal, XMEAS/XMV column 47: difference -1.705e-13, margin 1.359e-1, ratio 1.255e-12 (judged against the margin) nominal, XMEAS/XMV column 48: difference -7.105e-15, margin 2.878e-1, ratio 2.469e-14 (judged against the margin) nominal, XMEAS/XMV column 49: difference -1.776e-15, margin 2.174e-1, ratio 8.172e-15 (judged against the margin) nominal, XMEAS/XMV column 50: difference -3.375e-14, margin 8.273e-2, ratio 4.079e-13 (judged against the margin) nominal, XMEAS/XMV column 51: difference 8.882e-15, margin 4.854e-2, ratio 1.830e-13 (judged against the margin) nominal, XMEAS/XMV column 52: difference 1.421e-14, margin 1.399e-1, ratio 1.016e-13 (judged against the margin) nominal, XMEAS/XMV column 53: difference exactly 0, margin exactly 0, no ratio: the margin is zero (reference never moved: no mean to compare) IDV(1) IDV(1), XMEAS/XMV column 1: difference -5.551e-16, margin 2.005e-2, ratio 2.768e-14 (judged against the margin) IDV(1), XMEAS/XMV column 2: difference 1.933e-12, margin 3.633e0, ratio 5.319e-13 (judged against the margin) IDV(1), XMEAS/XMV column 3: difference 3.183e-12, margin 3.713e0, ratio 8.574e-13 (judged against the margin) IDV(1), XMEAS/XMV column 4: difference 2.043e-14, margin 3.042e-2, ratio 6.715e-13 (judged against the margin) IDV(1), XMEAS/XMV column 5: difference 1.421e-14, margin 2.145e-2, ratio 6.625e-13 (judged against the margin) IDV(1), XMEAS/XMV column 6: difference 3.375e-14, margin 2.306e-2, ratio 1.463e-12 (judged against the margin) IDV(1), XMEAS/XMV column 7: difference 1.705e-12, margin 3.978e0, ratio 4.287e-13 (judged against the margin) IDV(1), XMEAS/XMV column 8: difference -7.461e-14, margin 1.051e-1, ratio 7.100e-13 (judged against the margin) IDV(1), XMEAS/XMV column 9: difference -3.553e-15, margin 1.973e-3, ratio 1.801e-12 (judged against the margin) IDV(1), XMEAS/XMV column 10: difference 2.498e-16, margin 3.019e-3, ratio 8.275e-14 (judged against the margin) IDV(1), XMEAS/XMV column 11: difference -4.619e-14, margin 1.125e-1, ratio 4.107e-13 (judged against the margin) IDV(1), XMEAS/XMV column 12: difference 2.309e-14, margin 1.041e-1, ratio 2.218e-13 (judged against the margin) IDV(1), XMEAS/XMV column 13: difference 1.364e-12, margin 3.968e0, ratio 3.438e-13 (judged against the margin) IDV(1), XMEAS/XMV column 14: difference -6.217e-15, margin 1.068e-1, ratio 5.823e-14 (judged against the margin) IDV(1), XMEAS/XMV column 15: difference 1.243e-14, margin 9.910e-2, ratio 1.255e-13 (judged against the margin) IDV(1), XMEAS/XMV column 16: difference 2.160e-12, margin 4.180e0, ratio 5.168e-13 (judged against the margin) IDV(1), XMEAS/XMV column 17: difference exactly 0, margin 6.513e-2, ratio exactly 0 (judged against the margin) IDV(1), XMEAS/XMV column 18: difference -3.197e-14, margin 4.795e-2, ratio 6.668e-13 (judged against the margin) IDV(1), XMEAS/XMV column 19: difference 1.350e-13, margin 9.887e-1, ratio 1.365e-13 (judged against the margin) IDV(1), XMEAS/XMV column 20: difference 1.705e-13, margin 2.542e-1, ratio 6.708e-13 (judged against the margin) IDV(1), XMEAS/XMV column 21: difference -6.040e-14, margin 1.362e-2, ratio 4.433e-12 (judged against the margin) IDV(1), XMEAS/XMV column 22: difference exactly 0, margin 7.191e-2, ratio exactly 0 (judged against the margin) IDV(1), XMEAS/XMV column 23: difference 6.217e-15, margin 8.721e-2, ratio 7.129e-14 (judged against the margin) IDV(1), XMEAS/XMV column 24: difference -3.109e-15, margin 1.481e-2, ratio 2.099e-13 (judged against the margin) IDV(1), XMEAS/XMV column 25: difference 3.819e-14, margin 1.329e-1, ratio 2.873e-13 (judged against the margin) IDV(1), XMEAS/XMV column 26: difference -3.775e-15, margin 1.143e-2, ratio 3.302e-13 (judged against the margin) IDV(1), XMEAS/XMV column 27: difference -3.464e-14, margin 2.711e-2, ratio 1.278e-12 (judged against the margin) IDV(1), XMEAS/XMV column 28: difference -1.554e-15, margin 4.351e-3, ratio 3.572e-13 (judged against the margin) IDV(1), XMEAS/XMV column 29: difference 6.217e-15, margin 1.415e-1, ratio 4.393e-14 (judged against the margin) IDV(1), XMEAS/XMV column 30: difference -8.882e-16, margin 1.873e-2, ratio 4.743e-14 (judged against the margin) IDV(1), XMEAS/XMV column 31: difference 5.151e-14, margin 2.158e-1, ratio 2.387e-13 (judged against the margin) IDV(1), XMEAS/XMV column 32: difference -2.665e-15, margin 9.819e-3, ratio 2.714e-13 (judged against the margin) IDV(1), XMEAS/XMV column 33: difference -4.086e-14, margin 3.639e-2, ratio 1.123e-12 (judged against the margin) IDV(1), XMEAS/XMV column 34: difference -2.554e-15, margin 5.717e-3, ratio 4.467e-13 (judged against the margin) IDV(1), XMEAS/XMV column 35: difference -8.216e-15, margin 2.191e-2, ratio 3.749e-13 (judged against the margin) IDV(1), XMEAS/XMV column 36: difference -3.331e-15, margin 1.209e-2, ratio 2.756e-13 (judged against the margin) IDV(1), XMEAS/XMV column 37: difference -4.597e-17, margin 9.403e-4, ratio 4.889e-14 (judged against the margin) IDV(1), XMEAS/XMV column 38: difference -1.249e-15, margin 3.665e-3, ratio 3.407e-13 (judged against the margin) IDV(1), XMEAS/XMV column 39: difference -1.249e-16, margin 8.646e-4, ratio 1.445e-13 (judged against the margin) IDV(1), XMEAS/XMV column 40: difference -3.553e-15, margin 4.165e-2, ratio 8.529e-14 (judged against the margin) IDV(1), XMEAS/XMV column 41: difference 3.553e-15, margin 4.252e-2, ratio 8.356e-14 (judged against the margin) IDV(1), XMEAS/XMV column 42: difference 3.375e-14, margin 6.168e-2, ratio 5.472e-13 (judged against the margin) IDV(1), XMEAS/XMV column 43: difference 4.263e-14, margin 4.234e-2, ratio 1.007e-12 (judged against the margin) IDV(1), XMEAS/XMV column 44: difference -6.217e-14, margin 1.971e0, ratio 3.154e-14 (judged against the margin) IDV(1), XMEAS/XMV column 45: difference 1.243e-13, margin 2.137e-1, ratio 5.819e-13 (judged against the margin) IDV(1), XMEAS/XMV column 46: difference 4.086e-14, margin 8.073e-2, ratio 5.061e-13 (judged against the margin) IDV(1), XMEAS/XMV column 47: difference -8.882e-15, margin 3.736e-1, ratio 2.377e-14 (judged against the margin) IDV(1), XMEAS/XMV column 48: difference -1.421e-14, margin 2.890e-1, ratio 4.917e-14 (judged against the margin) IDV(1), XMEAS/XMV column 49: difference -1.421e-14, margin 2.198e-1, ratio 6.464e-14 (judged against the margin) IDV(1), XMEAS/XMV column 50: difference -1.954e-14, margin 1.935e-1, ratio 1.010e-13 (judged against the margin) IDV(1), XMEAS/XMV column 51: difference 1.954e-13, margin 5.172e-2, ratio 3.778e-12 (judged against the margin) IDV(1), XMEAS/XMV column 52: difference 1.066e-14, margin 1.539e-1, ratio 6.924e-14 (judged against the margin) IDV(1), XMEAS/XMV column 53: difference exactly 0, margin exactly 0, no ratio: the margin is zero (reference never moved: no mean to compare) IDV(2) IDV(2), XMEAS/XMV column 1: difference -4.302e-16, margin 2.562e-3, ratio 1.679e-13 (judged against the margin) IDV(2), XMEAS/XMV column 2: difference -1.137e-12, margin 3.680e0, ratio 3.089e-13 (judged against the margin) IDV(2), XMEAS/XMV column 3: difference -2.274e-12, margin 3.573e0, ratio 6.364e-13 (judged against the margin) IDV(2), XMEAS/XMV column 4: difference 3.553e-15, margin 9.253e-3, ratio 3.839e-13 (judged against the margin) IDV(2), XMEAS/XMV column 5: difference 1.954e-14, margin 2.143e-2, ratio 9.118e-13 (judged against the margin) IDV(2), XMEAS/XMV column 6: difference 1.066e-14, margin 2.405e-2, ratio 4.432e-13 (judged against the margin) IDV(2), XMEAS/XMV column 7: difference -2.274e-13, margin 1.015e0, ratio 2.239e-13 (judged against the margin) IDV(2), XMEAS/XMV column 8: difference -5.684e-14, margin 4.790e-2, ratio 1.187e-12 (judged against the margin) IDV(2), XMEAS/XMV column 9: difference -1.066e-14, margin 1.907e-3, ratio 5.590e-12 (judged against the margin) IDV(2), XMEAS/XMV column 10: difference -6.106e-16, margin 6.669e-3, ratio 9.156e-14 (judged against the margin) IDV(2), XMEAS/XMV column 11: difference 7.105e-15, margin 1.801e-2, ratio 3.945e-13 (judged against the margin) IDV(2), XMEAS/XMV column 12: difference -3.553e-15, margin 1.039e-1, ratio 3.420e-14 (judged against the margin) IDV(2), XMEAS/XMV column 13: difference 1.137e-13, margin 1.025e0, ratio 1.109e-13 (judged against the margin) IDV(2), XMEAS/XMV column 14: difference 1.776e-15, margin 1.069e-1, ratio 1.661e-14 (judged against the margin) IDV(2), XMEAS/XMV column 15: difference 2.842e-14, margin 9.763e-2, ratio 2.911e-13 (judged against the margin) IDV(2), XMEAS/XMV column 16: difference -6.821e-13, margin 1.009e0, ratio 6.758e-13 (judged against the margin) IDV(2), XMEAS/XMV column 17: difference 3.553e-15, margin 6.300e-2, ratio 5.639e-14 (judged against the margin) IDV(2), XMEAS/XMV column 18: difference 1.421e-14, margin 1.158e-2, ratio 1.227e-12 (judged against the margin) IDV(2), XMEAS/XMV column 19: difference -1.137e-13, margin 3.842e-1, ratio 2.959e-13 (judged against the margin) IDV(2), XMEAS/XMV column 20: difference 7.105e-14, margin 1.103e-1, ratio 6.444e-13 (judged against the margin) IDV(2), XMEAS/XMV column 21: difference 7.105e-15, margin 1.261e-2, ratio 5.633e-13 (judged against the margin) IDV(2), XMEAS/XMV column 22: difference 2.487e-14, margin 2.757e-2, ratio 9.021e-13 (judged against the margin) IDV(2), XMEAS/XMV column 23: difference -1.776e-15, margin 2.537e-2, ratio 7.002e-14 (judged against the margin) IDV(2), XMEAS/XMV column 24: difference -1.332e-15, margin 2.779e-2, ratio 4.794e-14 (judged against the margin) IDV(2), XMEAS/XMV column 25: difference 1.776e-15, margin 2.538e-2, ratio 6.999e-14 (judged against the margin) IDV(2), XMEAS/XMV column 26: difference -2.220e-16, margin 1.144e-2, ratio 1.940e-14 (judged against the margin) IDV(2), XMEAS/XMV column 27: difference -3.553e-15, margin 2.675e-2, ratio 1.328e-13 (judged against the margin) IDV(2), XMEAS/XMV column 28: difference -1.110e-16, margin 3.105e-3, ratio 3.575e-14 (judged against the margin) IDV(2), XMEAS/XMV column 29: difference 5.329e-15, margin 2.755e-2, ratio 1.934e-13 (judged against the margin) IDV(2), XMEAS/XMV column 30: difference -3.109e-15, margin 4.138e-2, ratio 7.512e-14 (judged against the margin) IDV(2), XMEAS/XMV column 31: difference -8.882e-16, margin 2.903e-2, ratio 3.059e-14 (judged against the margin) IDV(2), XMEAS/XMV column 32: difference -1.110e-16, margin 9.546e-3, ratio 1.163e-14 (judged against the margin) IDV(2), XMEAS/XMV column 33: difference 3.944e-31, margin 2.819e-2, ratio 1.399e-29 (judged against the margin) IDV(2), XMEAS/XMV column 34: difference 6.661e-16, margin 3.329e-3, ratio 2.001e-13 (judged against the margin) IDV(2), XMEAS/XMV column 35: difference 1.554e-15, margin 5.579e-3, ratio 2.786e-13 (judged against the margin) IDV(2), XMEAS/XMV column 36: difference 1.665e-15, margin 4.433e-3, ratio 3.756e-13 (judged against the margin) IDV(2), XMEAS/XMV column 37: difference -5.204e-18, margin 9.349e-4, ratio 5.566e-15 (judged against the margin) IDV(2), XMEAS/XMV column 38: difference 1.665e-16, margin 1.004e-3, ratio 1.659e-13 (judged against the margin) IDV(2), XMEAS/XMV column 39: difference 1.388e-17, margin 8.411e-4, ratio 1.650e-14 (judged against the margin) IDV(2), XMEAS/XMV column 40: difference exactly 0, margin 4.051e-2, ratio exactly 0 (judged against the margin) IDV(2), XMEAS/XMV column 41: difference 5.329e-15, margin 4.173e-2, ratio 1.277e-13 (judged against the margin) IDV(2), XMEAS/XMV column 42: difference -1.243e-14, margin 6.259e-2, ratio 1.987e-13 (judged against the margin) IDV(2), XMEAS/XMV column 43: difference -3.020e-14, margin 4.172e-2, ratio 7.239e-13 (judged against the margin) IDV(2), XMEAS/XMV column 44: difference -3.642e-14, margin 2.522e-1, ratio 1.444e-13 (judged against the margin) IDV(2), XMEAS/XMV column 45: difference 1.066e-14, margin 1.262e-1, ratio 8.443e-14 (judged against the margin) IDV(2), XMEAS/XMV column 46: difference -1.243e-14, margin 3.655e-2, ratio 3.402e-13 (judged against the margin) IDV(2), XMEAS/XMV column 47: difference -7.105e-14, margin 7.709e-1, ratio 9.218e-14 (judged against the margin) IDV(2), XMEAS/XMV column 48: difference 5.329e-15, margin 2.874e-1, ratio 1.854e-14 (judged against the margin) IDV(2), XMEAS/XMV column 49: difference 7.105e-15, margin 2.173e-1, ratio 3.270e-14 (judged against the margin) IDV(2), XMEAS/XMV column 50: difference -2.665e-14, margin 8.568e-2, ratio 3.110e-13 (judged against the margin) IDV(2), XMEAS/XMV column 51: difference 7.994e-14, margin 5.097e-2, ratio 1.568e-12 (judged against the margin) IDV(2), XMEAS/XMV column 52: difference -7.889e-31, margin 1.399e-1, ratio 5.637e-30 (judged against the margin) IDV(2), XMEAS/XMV column 53: difference exactly 0, margin exactly 0, no ratio: the margin is zero (reference never moved: no mean to compare) judged against the margin judged against the margin not judged this way: a constant reference, or a stuck valve worst gated: 5.590e-12 A marker past the shaded line would be a variable whose mean moved by more than the margin allows.
Each variable's mean difference against its own equivalence margin. One marker per variable per scenario: the paired difference between the Fortran's mean and the port's, divided by that variable's equivalence margin. One is the gate and the region past it is shaded, so the claim is false the moment a solid marker reaches the shaded band. Nothing is close: the worst gated variable sits at 5.590e-12 of its margin, which is about eleven orders of magnitude of headroom. Hollow markers are the variables the moment gate does not apply to, drawn rather than dropped, because omitting them would quietly shrink the denominator a reader counts against. A constant reference has no margin at all and so has no ratio, and sits in the zero lane; a valve stuck by the scenario's own fault is judged on its distribution instead, which is the decision recorded as B-0047d. Written by cargo xtask validate --tiers 5 --smoke, at the same smoke size as the figure above and for the same reason.

TOST power against battery size, measured in the same entry:

batteryworst power
4 seeds, 2 h (smoke)11.93
8 seeds, 12 h3.38
8 seeds, 48 h1.04
100 seeds, 48 h, nominal1.59
100 seeds, 48 h, IDV(2)0.23

A power above 1 means the battery is underpowered for the margin, not that the sources differ. PLAN.org sets the mean margin at a tenth of the pooled standard deviation, and a disturbed plant has a much larger pooled spread than a quiet one, so the same absolute run-to-run wander sits comfortably inside the margin under IDV(2) and outside it at the nominal operating point. At the nominal plant the margin needs about 255 seeds, not 100. The variables that run out of power are the manipulated ones: an integrating controller's output has a random-walk component, so its mean over 48 hours varies between seeds by much more than a tenth of its own within-run spread. That is a fact about the plant, not about the port, and the battery reports it as undecided rather than as a difference while still asserting on the measured gap.

Physics invariants

These are the only tests in the whole ladder that can catch an error the port faithfully inherited, because every other tier compares against the Fortran and an error in the Fortran is invisible to all of them. From B-0046:

invariantFortranportgate
I-1 reaction mass, 200 states4.664e-163.498e-161e-14
I-2 plant mass balance, nominal2.079e-162.344e-161e-13
I-2 along 2 h open loop6.556e-16not run1e-13
I-3 inert reaction termexactly 0exactly 0exact
I-4 per-component moles, 1 h7.436e-152.892e-151e-13

All four reactions balance exactly with the published molecular weights, `2 + 28

  • 32 = 62, 2 + 28 + 46 = 76, 2 + 46 = 48and3 x 32 = 2 x 48`, which is why I-1 is an equality rather than a tolerance.

The invariants' teeth were checked by mutation, and one result is worth repeating because it is not obvious. Deleting the reaction term from the reactor's component balance leaves the total mass balance passing at 2e-16. That is not a weak test, it is a true fact about the invariant: I-2 is the molecular-weight-weighted sum of I-4, and I-1 says the reaction is mass-neutral, so the reaction term cancels out of I-2 exactly. Total mass conservation is blind to stoichiometry by construction. The unweighted per-component balance, I-4, sees it immediately. An invariant that is a sum of other invariants is weaker than the set, and how much weaker is not obvious from reading it.

Tiers 6 through 10

These have not run. They are listed here so that the shape of the remaining claim is visible rather than implied.

Tier 6, downstream-task equivalence, is the operational definition of "practically equivalent" for this project's research audience: train a detector suite on Fortran d00 data, evaluate on Fortran and on Rust test data, then reverse it, and compare detection rate, false alarm rate and detection delay. The claim to be able to make is that cross-source performance matches within-source performance within its own run-to-run variability. Backlog item B-0050.

Tier 7, published dataset reproduction, attempts direct reproduction of the bundled d00 through d21 files under the documented generation protocol, reporting per-file agreement with the Tier 5 machinery. The groundwork exists: teprob.f:1187-1256 carries fifty-four generator words in comments, one per published dataset, and B-0047a transcribed and asserted them. Three facts about that table are worth knowing in advance. Thirty-four of the fifty-four exceed 2^32, which is not a transcription error because TESUB7 reduces any seed on the first draw. Twenty-seven are even, and a multiplicative generator modulo a power of two keeps the factors of two its seed has, so those runs have a shorter period and low bits that never move. Reproducing the published files means doing the same rather than fixing it. Backlog item B-0051.

Tier 8 is differential fuzzing, Tier 9 is cross-platform determinism by golden BLAKE3 digest including wasm in a real browser, and Tier 10 requires that every quirk fix ship with a measured delta from the full Tier 5 battery with the fix on and off. None has started.

Two bugs that only long runs found

Worth recording because they are the argument for running the battery long rather than often (B-0047b).

TRCN, the Tier 3 trace counter, is a Fortran INTEGER that nothing clears between evaluations. At about 264 draws per step over 172,800 steps it passes 2^31 after roughly fifty runs, goes negative, passes the capacity guard, and writes outside the array. The symptom was a SIGSEGV deep inside the Fortran, tens of millions of steps into the battery, with nothing nearby to suggest why: the same seed ran perfectly in isolation, and starting at seed 40 moved the crash to seed 90, which is what identified it as cumulative rather than data-dependent. Nothing in Tiers 1 to 4 was ever affected, because no shorter run approached the overflow.

welch_t computed (n - 1) on a summary with no observations. In debug that panics; in release it wraps and returns a degrees-of-freedom figure that looks like a number. The case is XMV(12), the agitator, which no controller ever writes, so every run has zero variance and the log-variance ensemble is empty.

Both are the same lesson. A validation harness is code, it has bugs, and the bugs it has are the ones only its longest runs reach.

Validation, measured

This page is generated. cargo xtask validate wrote it from commit 03ec3d6-dirty. Every number on it was captured from that run's own output. To change what it says, change what the suite measures and run the command again.

The narrative version of this material, with the reasoning behind each tier and the history of what it caught, is in Validation. This section is the other half: the numbers, written by the command that ran the suite, from the suite's own output. Nothing here was transcribed.

A tier with no chapter has not been generated. That is stated rather than left to be inferred from an absence, because a missing page and a page nobody updated look the same from the table of contents.

Toolchain on the machine that ran this: rustc 1.97.1 (8bab26f4f 2026-07-14), gfortran 15.2.0.

Fidelity preflight

cargo xtask fidelity runs the port forward from the nominal state and diffs states, derivatives, measurements and the generator word against a golden oracle trace committed to the repository. It needs no Fortran toolchain, so it runs everywhere in about a second.

steps diffedworstwheregatetrace recorded with
100 of 1003.521323734565789e-14YP(12) at step 771e-12gfortran 15.2.0

What the long tiers look like

Tiers 4 and 5 run but write no chapter yet, for the reason GENERATED_TIERS gives in xtask. Their figures are here, each drawn by the run named under it.

GENERATED by `cargo xtask validate --tiers 4` from commit `b6ef4ee-dirty`. Do not edit by hand: the next run overwrites it. Tier 4: trajectory error against the instrument noiseA logarithmic strip plot with one row per scenario. Each marker is the worst disagreement between the port and the Fortran over a 4 hour run, divided by the measurement noise standard deviation of the channel it occurred on. The band at one, where the error would equal the instrument noise, is shaded; 0 of 42 markers reach it. 21 are exactly zero. Tier 4: how far apart the trajectories get, in units of instrument noise 21 scenarios, 4 h each. Worst error over the run divided by XNS(i). 21 of 42 runs are bit-identical. 1e-12 1e-10 1e-8 1e-6 1e-4 1e-2 1 worst error over the run, as a fraction of that channel's noise = 0 1 x XNS(i) nominal nominal, vendored libm: worst 2.450e-5, inside the noise for 4.000 h nominal, platform libm: worst exactly 0, inside the noise for 4.000 h IDV(1) IDV(1), vendored libm: worst 1.600e-9, inside the noise for 4.000 h IDV(1), platform libm: worst exactly 0, inside the noise for 4.000 h IDV(2) IDV(2), vendored libm: worst 2.660e-9, inside the noise for 4.000 h IDV(2), platform libm: worst exactly 0, inside the noise for 4.000 h IDV(3) IDV(3), vendored libm: worst 2.490e-9, inside the noise for 4.000 h IDV(3), platform libm: worst exactly 0, inside the noise for 4.000 h IDV(4) IDV(4), vendored libm: worst 2.120e-11, inside the noise for 4.000 h IDV(4), platform libm: worst exactly 0, inside the noise for 4.000 h IDV(5) IDV(5), vendored libm: worst 4.160e-10, inside the noise for 4.000 h IDV(5), platform libm: worst exactly 0, inside the noise for 4.000 h IDV(6) IDV(6), vendored libm: worst 1.490e-10, inside the noise for 4.000 h IDV(6), platform libm: worst exactly 0, inside the noise for 4.000 h IDV(7) IDV(7), vendored libm: worst 5.120e-11, inside the noise for 4.000 h IDV(7), platform libm: worst exactly 0, inside the noise for 4.000 h IDV(8) IDV(8), vendored libm: worst 1.420e-8, inside the noise for 4.000 h IDV(8), platform libm: worst exactly 0, inside the noise for 4.000 h IDV(9) IDV(9), vendored libm: worst 1.310e-9, inside the noise for 4.000 h IDV(9), platform libm: worst exactly 0, inside the noise for 4.000 h IDV(10) IDV(10), vendored libm: worst 3.660e-8, inside the noise for 4.000 h IDV(10), platform libm: worst exactly 0, inside the noise for 4.000 h IDV(11) IDV(11), vendored libm: worst 2.410e-9, inside the noise for 4.000 h IDV(11), platform libm: worst exactly 0, inside the noise for 4.000 h IDV(12) IDV(12), vendored libm: worst 4.870e-10, inside the noise for 4.000 h IDV(12), platform libm: worst exactly 0, inside the noise for 4.000 h IDV(13) IDV(13), vendored libm: worst 2.070e-8, inside the noise for 4.000 h IDV(13), platform libm: worst exactly 0, inside the noise for 4.000 h IDV(14) IDV(14), vendored libm: worst 2.450e-5, inside the noise for 4.000 h IDV(14), platform libm: worst exactly 0, inside the noise for 4.000 h IDV(15) IDV(15), vendored libm: worst 2.450e-5, inside the noise for 4.000 h IDV(15), platform libm: worst exactly 0, inside the noise for 4.000 h IDV(16) IDV(16), vendored libm: worst 1.170e-9, inside the noise for 4.000 h IDV(16), platform libm: worst exactly 0, inside the noise for 4.000 h IDV(17) IDV(17), vendored libm: worst 1.210e-9, inside the noise for 4.000 h IDV(17), platform libm: worst exactly 0, inside the noise for 4.000 h IDV(18) IDV(18), vendored libm: worst 2.420e-9, inside the noise for 4.000 h IDV(18), platform libm: worst exactly 0, inside the noise for 4.000 h IDV(19) IDV(19), vendored libm: worst 2.450e-5, inside the noise for 4.000 h IDV(19), platform libm: worst exactly 0, inside the noise for 4.000 h IDV(20) IDV(20), vendored libm: worst 5.780e-8, inside the noise for 4.000 h IDV(20), platform libm: worst exactly 0, inside the noise for 4.000 h vendored libm vendored libm platform libm: the same exp gfortran calls worst anywhere: 2.450e-5 The shaded band is where the disagreement would be as large as the channel's own measurement noise.
How far apart the trajectories get, in units of instrument noise. Every disturbance scenario, run on both implementations, with the worst disagreement anywhere in the run divided by the noise standard deviation XNS(i) of the channel it happened on. The reference line is therefore not zero but the point at which the plant's own instruments could resolve the difference. The claim is false if any marker reaches the shaded band, and the explanation is false if the platform libm markers ever leave the = 0 lane: that would mean the divergence is something other than transcendental rounding. Drawn by cargo xtask validate --tiers 4 at commit b6ef4ee-dirty; the measurement it repeats was first recorded in B-0034 (LOG.org).
GENERATED by `cargo xtask validate --tiers 5 --smoke` from commit `03ec3d6-dirty`. Do not edit by hand: the next run overwrites it. Tier 5: the cross-source difference against the reference's own spreadA logarithmic scatter plot. The horizontal axis is a statistic measured between the Fortran and the Rust port; the vertical axis is the same statistic measured between two halves of the Fortran's own runs. Both axes share one scale, so the diagonal is the line of equality. 3 of 5 points lie above it, meaning the two implementations differ by less than the reference differs from itself. Tier 5: the two sources against the reference's own run-to-run spread 3 scenarios x 4 seeds x 2 h, 40 samples per run, vendored libm. A point above the diagonal differs across sources by less than the reference differs from itself. 1e-12 1e-10 1e-8 1e-6 1e-4 1e-2 1 1e2 1e4 1e-12 1e-10 1e-8 1e-6 1e-4 1e-2 1 1e2 1e4 equal: the port is as different as a rerun measured between the Fortran and the port measured inside the Fortran alone KS, one variable shifted 10 sd: across sources 1.000e0, inside the Fortran 7.500e-2 KS, one variable shifted 10 sd energy, one variable shifted 10 sd: across sources 1.228e2, inside the Fortran 3.036e-2 energy, one variable shifted 10 sd correlation matrix, nominal: across sources 6.973e-12, inside the Fortran 1.091e1 correlation matrix, nominal correlation matrix, IDV(1): across sources 3.586e-12, inside the Fortran 9.873e0 correlation matrix, IDV(1) correlation matrix, IDV(2): across sources 4.248e-12, inside the Fortran 1.279e1 correlation matrix, IDV(2) inside the null above the diagonal: inside the reference's own spread outside the null below it: a difference the calibration can see The points below the diagonal are the battery's own positive control: one variable of the reference shifted by ten standard deviations and compared with the unshifted reference.
The two sources against the reference's own run-to-run spread. There is no absolute scale on which a Frobenius distance or a Kolmogorov-Smirnov statistic is small, so the battery measures one: it splits the Fortran's own runs in half and computes the same statistic Fortran against Fortran. The horizontal axis is the statistic across the two sources, the vertical axis is that null, and both axes share a scale so the diagonal is equality. The claim is false if a cross-source point crosses to the low side of the diagonal; the two that already sit there are the battery's own positive control, where one variable of the reference was shifted by ten standard deviations before comparing. Drawn by cargo xtask validate --tiers 5 --smoke at commit 03ec3d6-dirty; the measurement it repeats was first recorded in B-0047b (LOG.org).
GENERATED by `cargo xtask validate --tiers 5 --smoke` from commit `03ec3d6-dirty`. Do not edit by hand: the next run overwrites it. Tier 5: each variable's mean difference against its equivalence marginA logarithmic strip plot with one row per scenario. Each marker is one of the 53 variables: the paired difference between the Fortran's mean and the port's, divided by that variable's own equivalence margin. One is the gate, and the region past it is shaded. 0 of 156 gated markers reach it. 3 are exactly zero. A further 3 variables are not judged this way and are drawn hollow; a constant reference has no margin, so it sits in the zero lane. Tier 5: how far inside its margin each variable sits 3 scenarios x 4 seeds x 2 h, 40 samples per run, vendored libm, 53 variables per scenario. Paired mean difference divided by that variable's equivalence margin. 1e-30 1e-27 1e-24 1e-21 1e-18 1e-15 1e-12 1e-9 1e-6 1e-3 1 paired mean difference, as a fraction of the equivalence margin = 0 the margin nominal nominal, XMEAS/XMV column 1: difference -3.969e-15, margin 2.497e-3, ratio 1.589e-12 (judged against the margin) nominal, XMEAS/XMV column 2: difference 1.364e-12, margin 3.554e0, ratio 3.839e-13 (judged against the margin) nominal, XMEAS/XMV column 3: difference 2.501e-12, margin 3.354e0, ratio 7.458e-13 (judged against the margin) nominal, XMEAS/XMV column 4: difference 7.994e-15, margin 7.635e-3, ratio 1.047e-12 (judged against the margin) nominal, XMEAS/XMV column 5: difference 6.217e-15, margin 2.142e-2, ratio 2.902e-13 (judged against the margin) nominal, XMEAS/XMV column 6: difference -1.776e-14, margin 2.214e-2, ratio 8.022e-13 (judged against the margin) nominal, XMEAS/XMV column 7: difference 7.958e-13, margin 3.860e-1, ratio 2.062e-12 (judged against the margin) nominal, XMEAS/XMV column 8: difference 2.132e-14, margin 4.297e-2, ratio 4.961e-13 (judged against the margin) nominal, XMEAS/XMV column 9: difference -3.553e-15, margin 1.858e-3, ratio 1.912e-12 (judged against the margin) nominal, XMEAS/XMV column 10: difference -1.263e-15, margin 1.131e-3, ratio 1.116e-12 (judged against the margin) nominal, XMEAS/XMV column 11: difference -3.553e-15, margin 1.515e-2, ratio 2.345e-13 (judged against the margin) nominal, XMEAS/XMV column 12: difference 7.105e-15, margin 1.039e-1, ratio 6.837e-14 (judged against the margin) nominal, XMEAS/XMV column 13: difference 6.821e-13, margin 4.006e-1, ratio 1.703e-12 (judged against the margin) nominal, XMEAS/XMV column 14: difference -9.770e-15, margin 1.069e-1, ratio 9.137e-14 (judged against the margin) nominal, XMEAS/XMV column 15: difference 1.776e-14, margin 9.759e-2, ratio 1.820e-13 (judged against the margin) nominal, XMEAS/XMV column 16: difference 3.411e-13, margin 3.617e-1, ratio 9.429e-13 (judged against the margin) nominal, XMEAS/XMV column 17: difference 8.882e-16, margin 6.300e-2, ratio 1.410e-14 (judged against the margin) nominal, XMEAS/XMV column 18: difference -3.197e-14, margin 9.227e-3, ratio 3.465e-12 (judged against the margin) nominal, XMEAS/XMV column 19: difference -8.527e-14, margin 3.553e-1, ratio 2.400e-13 (judged against the margin) nominal, XMEAS/XMV column 20: difference 2.416e-13, margin 5.518e-2, ratio 4.379e-12 (judged against the margin) nominal, XMEAS/XMV column 21: difference -1.421e-14, margin 1.125e-2, ratio 1.264e-12 (judged against the margin) nominal, XMEAS/XMV column 22: difference -1.066e-14, margin 2.295e-2, ratio 4.644e-13 (judged against the margin) nominal, XMEAS/XMV column 23: difference 1.954e-14, margin 2.542e-2, ratio 7.686e-13 (judged against the margin) nominal, XMEAS/XMV column 24: difference -5.329e-15, margin 9.114e-3, ratio 5.847e-13 (judged against the margin) nominal, XMEAS/XMV column 25: difference 1.776e-15, margin 2.438e-2, ratio 7.285e-14 (judged against the margin) nominal, XMEAS/XMV column 26: difference -1.332e-15, margin 1.121e-2, ratio 1.188e-13 (judged against the margin) nominal, XMEAS/XMV column 27: difference -2.043e-14, margin 2.517e-2, ratio 8.115e-13 (judged against the margin) nominal, XMEAS/XMV column 28: difference -2.220e-15, margin 2.574e-3, ratio 8.625e-13 (judged against the margin) nominal, XMEAS/XMV column 29: difference 3.020e-14, margin 2.657e-2, ratio 1.137e-12 (judged against the margin) nominal, XMEAS/XMV column 30: difference -1.066e-14, margin 1.011e-2, ratio 1.055e-12 (judged against the margin) nominal, XMEAS/XMV column 31: difference 1.421e-14, margin 2.702e-2, ratio 5.260e-13 (judged against the margin) nominal, XMEAS/XMV column 32: difference -3.331e-16, margin 9.541e-3, ratio 3.491e-14 (judged against the margin) nominal, XMEAS/XMV column 33: difference -2.931e-14, margin 2.357e-2, ratio 1.244e-12 (judged against the margin) nominal, XMEAS/XMV column 34: difference -2.220e-15, margin 2.600e-3, ratio 8.541e-13 (judged against the margin) nominal, XMEAS/XMV column 35: difference -2.887e-15, margin 5.031e-3, ratio 5.738e-13 (judged against the margin) nominal, XMEAS/XMV column 36: difference -1.332e-15, margin 4.308e-3, ratio 3.093e-13 (judged against the margin) nominal, XMEAS/XMV column 37: difference -3.730e-17, margin 9.354e-4, ratio 3.987e-14 (judged against the margin) nominal, XMEAS/XMV column 38: difference -1.138e-15, margin 9.852e-4, ratio 1.155e-12 (judged against the margin) nominal, XMEAS/XMV column 39: difference -1.076e-16, margin 8.378e-4, ratio 1.284e-13 (judged against the margin) nominal, XMEAS/XMV column 40: difference 5.329e-15, margin 4.061e-2, ratio 1.312e-13 (judged against the margin) nominal, XMEAS/XMV column 41: difference 3.553e-15, margin 4.198e-2, ratio 8.463e-14 (judged against the margin) nominal, XMEAS/XMV column 42: difference 1.243e-14, margin 6.154e-2, ratio 2.020e-13 (judged against the margin) nominal, XMEAS/XMV column 43: difference 2.665e-14, margin 4.072e-2, ratio 6.544e-13 (judged against the margin) nominal, XMEAS/XMV column 44: difference -3.872e-13, margin 2.464e-1, ratio 1.572e-12 (judged against the margin) nominal, XMEAS/XMV column 45: difference 3.908e-14, margin 1.162e-1, ratio 3.364e-13 (judged against the margin) nominal, XMEAS/XMV column 46: difference 8.438e-14, margin 2.702e-2, ratio 3.123e-12 (judged against the margin) nominal, XMEAS/XMV column 47: difference -1.705e-13, margin 1.359e-1, ratio 1.255e-12 (judged against the margin) nominal, XMEAS/XMV column 48: difference -7.105e-15, margin 2.878e-1, ratio 2.469e-14 (judged against the margin) nominal, XMEAS/XMV column 49: difference -1.776e-15, margin 2.174e-1, ratio 8.172e-15 (judged against the margin) nominal, XMEAS/XMV column 50: difference -3.375e-14, margin 8.273e-2, ratio 4.079e-13 (judged against the margin) nominal, XMEAS/XMV column 51: difference 8.882e-15, margin 4.854e-2, ratio 1.830e-13 (judged against the margin) nominal, XMEAS/XMV column 52: difference 1.421e-14, margin 1.399e-1, ratio 1.016e-13 (judged against the margin) nominal, XMEAS/XMV column 53: difference exactly 0, margin exactly 0, no ratio: the margin is zero (reference never moved: no mean to compare) IDV(1) IDV(1), XMEAS/XMV column 1: difference -5.551e-16, margin 2.005e-2, ratio 2.768e-14 (judged against the margin) IDV(1), XMEAS/XMV column 2: difference 1.933e-12, margin 3.633e0, ratio 5.319e-13 (judged against the margin) IDV(1), XMEAS/XMV column 3: difference 3.183e-12, margin 3.713e0, ratio 8.574e-13 (judged against the margin) IDV(1), XMEAS/XMV column 4: difference 2.043e-14, margin 3.042e-2, ratio 6.715e-13 (judged against the margin) IDV(1), XMEAS/XMV column 5: difference 1.421e-14, margin 2.145e-2, ratio 6.625e-13 (judged against the margin) IDV(1), XMEAS/XMV column 6: difference 3.375e-14, margin 2.306e-2, ratio 1.463e-12 (judged against the margin) IDV(1), XMEAS/XMV column 7: difference 1.705e-12, margin 3.978e0, ratio 4.287e-13 (judged against the margin) IDV(1), XMEAS/XMV column 8: difference -7.461e-14, margin 1.051e-1, ratio 7.100e-13 (judged against the margin) IDV(1), XMEAS/XMV column 9: difference -3.553e-15, margin 1.973e-3, ratio 1.801e-12 (judged against the margin) IDV(1), XMEAS/XMV column 10: difference 2.498e-16, margin 3.019e-3, ratio 8.275e-14 (judged against the margin) IDV(1), XMEAS/XMV column 11: difference -4.619e-14, margin 1.125e-1, ratio 4.107e-13 (judged against the margin) IDV(1), XMEAS/XMV column 12: difference 2.309e-14, margin 1.041e-1, ratio 2.218e-13 (judged against the margin) IDV(1), XMEAS/XMV column 13: difference 1.364e-12, margin 3.968e0, ratio 3.438e-13 (judged against the margin) IDV(1), XMEAS/XMV column 14: difference -6.217e-15, margin 1.068e-1, ratio 5.823e-14 (judged against the margin) IDV(1), XMEAS/XMV column 15: difference 1.243e-14, margin 9.910e-2, ratio 1.255e-13 (judged against the margin) IDV(1), XMEAS/XMV column 16: difference 2.160e-12, margin 4.180e0, ratio 5.168e-13 (judged against the margin) IDV(1), XMEAS/XMV column 17: difference exactly 0, margin 6.513e-2, ratio exactly 0 (judged against the margin) IDV(1), XMEAS/XMV column 18: difference -3.197e-14, margin 4.795e-2, ratio 6.668e-13 (judged against the margin) IDV(1), XMEAS/XMV column 19: difference 1.350e-13, margin 9.887e-1, ratio 1.365e-13 (judged against the margin) IDV(1), XMEAS/XMV column 20: difference 1.705e-13, margin 2.542e-1, ratio 6.708e-13 (judged against the margin) IDV(1), XMEAS/XMV column 21: difference -6.040e-14, margin 1.362e-2, ratio 4.433e-12 (judged against the margin) IDV(1), XMEAS/XMV column 22: difference exactly 0, margin 7.191e-2, ratio exactly 0 (judged against the margin) IDV(1), XMEAS/XMV column 23: difference 6.217e-15, margin 8.721e-2, ratio 7.129e-14 (judged against the margin) IDV(1), XMEAS/XMV column 24: difference -3.109e-15, margin 1.481e-2, ratio 2.099e-13 (judged against the margin) IDV(1), XMEAS/XMV column 25: difference 3.819e-14, margin 1.329e-1, ratio 2.873e-13 (judged against the margin) IDV(1), XMEAS/XMV column 26: difference -3.775e-15, margin 1.143e-2, ratio 3.302e-13 (judged against the margin) IDV(1), XMEAS/XMV column 27: difference -3.464e-14, margin 2.711e-2, ratio 1.278e-12 (judged against the margin) IDV(1), XMEAS/XMV column 28: difference -1.554e-15, margin 4.351e-3, ratio 3.572e-13 (judged against the margin) IDV(1), XMEAS/XMV column 29: difference 6.217e-15, margin 1.415e-1, ratio 4.393e-14 (judged against the margin) IDV(1), XMEAS/XMV column 30: difference -8.882e-16, margin 1.873e-2, ratio 4.743e-14 (judged against the margin) IDV(1), XMEAS/XMV column 31: difference 5.151e-14, margin 2.158e-1, ratio 2.387e-13 (judged against the margin) IDV(1), XMEAS/XMV column 32: difference -2.665e-15, margin 9.819e-3, ratio 2.714e-13 (judged against the margin) IDV(1), XMEAS/XMV column 33: difference -4.086e-14, margin 3.639e-2, ratio 1.123e-12 (judged against the margin) IDV(1), XMEAS/XMV column 34: difference -2.554e-15, margin 5.717e-3, ratio 4.467e-13 (judged against the margin) IDV(1), XMEAS/XMV column 35: difference -8.216e-15, margin 2.191e-2, ratio 3.749e-13 (judged against the margin) IDV(1), XMEAS/XMV column 36: difference -3.331e-15, margin 1.209e-2, ratio 2.756e-13 (judged against the margin) IDV(1), XMEAS/XMV column 37: difference -4.597e-17, margin 9.403e-4, ratio 4.889e-14 (judged against the margin) IDV(1), XMEAS/XMV column 38: difference -1.249e-15, margin 3.665e-3, ratio 3.407e-13 (judged against the margin) IDV(1), XMEAS/XMV column 39: difference -1.249e-16, margin 8.646e-4, ratio 1.445e-13 (judged against the margin) IDV(1), XMEAS/XMV column 40: difference -3.553e-15, margin 4.165e-2, ratio 8.529e-14 (judged against the margin) IDV(1), XMEAS/XMV column 41: difference 3.553e-15, margin 4.252e-2, ratio 8.356e-14 (judged against the margin) IDV(1), XMEAS/XMV column 42: difference 3.375e-14, margin 6.168e-2, ratio 5.472e-13 (judged against the margin) IDV(1), XMEAS/XMV column 43: difference 4.263e-14, margin 4.234e-2, ratio 1.007e-12 (judged against the margin) IDV(1), XMEAS/XMV column 44: difference -6.217e-14, margin 1.971e0, ratio 3.154e-14 (judged against the margin) IDV(1), XMEAS/XMV column 45: difference 1.243e-13, margin 2.137e-1, ratio 5.819e-13 (judged against the margin) IDV(1), XMEAS/XMV column 46: difference 4.086e-14, margin 8.073e-2, ratio 5.061e-13 (judged against the margin) IDV(1), XMEAS/XMV column 47: difference -8.882e-15, margin 3.736e-1, ratio 2.377e-14 (judged against the margin) IDV(1), XMEAS/XMV column 48: difference -1.421e-14, margin 2.890e-1, ratio 4.917e-14 (judged against the margin) IDV(1), XMEAS/XMV column 49: difference -1.421e-14, margin 2.198e-1, ratio 6.464e-14 (judged against the margin) IDV(1), XMEAS/XMV column 50: difference -1.954e-14, margin 1.935e-1, ratio 1.010e-13 (judged against the margin) IDV(1), XMEAS/XMV column 51: difference 1.954e-13, margin 5.172e-2, ratio 3.778e-12 (judged against the margin) IDV(1), XMEAS/XMV column 52: difference 1.066e-14, margin 1.539e-1, ratio 6.924e-14 (judged against the margin) IDV(1), XMEAS/XMV column 53: difference exactly 0, margin exactly 0, no ratio: the margin is zero (reference never moved: no mean to compare) IDV(2) IDV(2), XMEAS/XMV column 1: difference -4.302e-16, margin 2.562e-3, ratio 1.679e-13 (judged against the margin) IDV(2), XMEAS/XMV column 2: difference -1.137e-12, margin 3.680e0, ratio 3.089e-13 (judged against the margin) IDV(2), XMEAS/XMV column 3: difference -2.274e-12, margin 3.573e0, ratio 6.364e-13 (judged against the margin) IDV(2), XMEAS/XMV column 4: difference 3.553e-15, margin 9.253e-3, ratio 3.839e-13 (judged against the margin) IDV(2), XMEAS/XMV column 5: difference 1.954e-14, margin 2.143e-2, ratio 9.118e-13 (judged against the margin) IDV(2), XMEAS/XMV column 6: difference 1.066e-14, margin 2.405e-2, ratio 4.432e-13 (judged against the margin) IDV(2), XMEAS/XMV column 7: difference -2.274e-13, margin 1.015e0, ratio 2.239e-13 (judged against the margin) IDV(2), XMEAS/XMV column 8: difference -5.684e-14, margin 4.790e-2, ratio 1.187e-12 (judged against the margin) IDV(2), XMEAS/XMV column 9: difference -1.066e-14, margin 1.907e-3, ratio 5.590e-12 (judged against the margin) IDV(2), XMEAS/XMV column 10: difference -6.106e-16, margin 6.669e-3, ratio 9.156e-14 (judged against the margin) IDV(2), XMEAS/XMV column 11: difference 7.105e-15, margin 1.801e-2, ratio 3.945e-13 (judged against the margin) IDV(2), XMEAS/XMV column 12: difference -3.553e-15, margin 1.039e-1, ratio 3.420e-14 (judged against the margin) IDV(2), XMEAS/XMV column 13: difference 1.137e-13, margin 1.025e0, ratio 1.109e-13 (judged against the margin) IDV(2), XMEAS/XMV column 14: difference 1.776e-15, margin 1.069e-1, ratio 1.661e-14 (judged against the margin) IDV(2), XMEAS/XMV column 15: difference 2.842e-14, margin 9.763e-2, ratio 2.911e-13 (judged against the margin) IDV(2), XMEAS/XMV column 16: difference -6.821e-13, margin 1.009e0, ratio 6.758e-13 (judged against the margin) IDV(2), XMEAS/XMV column 17: difference 3.553e-15, margin 6.300e-2, ratio 5.639e-14 (judged against the margin) IDV(2), XMEAS/XMV column 18: difference 1.421e-14, margin 1.158e-2, ratio 1.227e-12 (judged against the margin) IDV(2), XMEAS/XMV column 19: difference -1.137e-13, margin 3.842e-1, ratio 2.959e-13 (judged against the margin) IDV(2), XMEAS/XMV column 20: difference 7.105e-14, margin 1.103e-1, ratio 6.444e-13 (judged against the margin) IDV(2), XMEAS/XMV column 21: difference 7.105e-15, margin 1.261e-2, ratio 5.633e-13 (judged against the margin) IDV(2), XMEAS/XMV column 22: difference 2.487e-14, margin 2.757e-2, ratio 9.021e-13 (judged against the margin) IDV(2), XMEAS/XMV column 23: difference -1.776e-15, margin 2.537e-2, ratio 7.002e-14 (judged against the margin) IDV(2), XMEAS/XMV column 24: difference -1.332e-15, margin 2.779e-2, ratio 4.794e-14 (judged against the margin) IDV(2), XMEAS/XMV column 25: difference 1.776e-15, margin 2.538e-2, ratio 6.999e-14 (judged against the margin) IDV(2), XMEAS/XMV column 26: difference -2.220e-16, margin 1.144e-2, ratio 1.940e-14 (judged against the margin) IDV(2), XMEAS/XMV column 27: difference -3.553e-15, margin 2.675e-2, ratio 1.328e-13 (judged against the margin) IDV(2), XMEAS/XMV column 28: difference -1.110e-16, margin 3.105e-3, ratio 3.575e-14 (judged against the margin) IDV(2), XMEAS/XMV column 29: difference 5.329e-15, margin 2.755e-2, ratio 1.934e-13 (judged against the margin) IDV(2), XMEAS/XMV column 30: difference -3.109e-15, margin 4.138e-2, ratio 7.512e-14 (judged against the margin) IDV(2), XMEAS/XMV column 31: difference -8.882e-16, margin 2.903e-2, ratio 3.059e-14 (judged against the margin) IDV(2), XMEAS/XMV column 32: difference -1.110e-16, margin 9.546e-3, ratio 1.163e-14 (judged against the margin) IDV(2), XMEAS/XMV column 33: difference 3.944e-31, margin 2.819e-2, ratio 1.399e-29 (judged against the margin) IDV(2), XMEAS/XMV column 34: difference 6.661e-16, margin 3.329e-3, ratio 2.001e-13 (judged against the margin) IDV(2), XMEAS/XMV column 35: difference 1.554e-15, margin 5.579e-3, ratio 2.786e-13 (judged against the margin) IDV(2), XMEAS/XMV column 36: difference 1.665e-15, margin 4.433e-3, ratio 3.756e-13 (judged against the margin) IDV(2), XMEAS/XMV column 37: difference -5.204e-18, margin 9.349e-4, ratio 5.566e-15 (judged against the margin) IDV(2), XMEAS/XMV column 38: difference 1.665e-16, margin 1.004e-3, ratio 1.659e-13 (judged against the margin) IDV(2), XMEAS/XMV column 39: difference 1.388e-17, margin 8.411e-4, ratio 1.650e-14 (judged against the margin) IDV(2), XMEAS/XMV column 40: difference exactly 0, margin 4.051e-2, ratio exactly 0 (judged against the margin) IDV(2), XMEAS/XMV column 41: difference 5.329e-15, margin 4.173e-2, ratio 1.277e-13 (judged against the margin) IDV(2), XMEAS/XMV column 42: difference -1.243e-14, margin 6.259e-2, ratio 1.987e-13 (judged against the margin) IDV(2), XMEAS/XMV column 43: difference -3.020e-14, margin 4.172e-2, ratio 7.239e-13 (judged against the margin) IDV(2), XMEAS/XMV column 44: difference -3.642e-14, margin 2.522e-1, ratio 1.444e-13 (judged against the margin) IDV(2), XMEAS/XMV column 45: difference 1.066e-14, margin 1.262e-1, ratio 8.443e-14 (judged against the margin) IDV(2), XMEAS/XMV column 46: difference -1.243e-14, margin 3.655e-2, ratio 3.402e-13 (judged against the margin) IDV(2), XMEAS/XMV column 47: difference -7.105e-14, margin 7.709e-1, ratio 9.218e-14 (judged against the margin) IDV(2), XMEAS/XMV column 48: difference 5.329e-15, margin 2.874e-1, ratio 1.854e-14 (judged against the margin) IDV(2), XMEAS/XMV column 49: difference 7.105e-15, margin 2.173e-1, ratio 3.270e-14 (judged against the margin) IDV(2), XMEAS/XMV column 50: difference -2.665e-14, margin 8.568e-2, ratio 3.110e-13 (judged against the margin) IDV(2), XMEAS/XMV column 51: difference 7.994e-14, margin 5.097e-2, ratio 1.568e-12 (judged against the margin) IDV(2), XMEAS/XMV column 52: difference -7.889e-31, margin 1.399e-1, ratio 5.637e-30 (judged against the margin) IDV(2), XMEAS/XMV column 53: difference exactly 0, margin exactly 0, no ratio: the margin is zero (reference never moved: no mean to compare) judged against the margin judged against the margin not judged this way: a constant reference, or a stuck valve worst gated: 5.590e-12 A marker past the shaded line would be a variable whose mean moved by more than the margin allows.
Each variable's mean difference against its own equivalence margin. The calibration figure above says the two sources are closer to each other than the reference is to itself. It does not say how much room is left, and a TOST verdict does not either: a p-value reports that a test did not reject, not by what distance. This does. Each marker is one of the 53 variables in one scenario, its paired mean difference divided by that variable's own equivalence margin, which is a tenth of the reference's standard deviation for that variable. One is the gate and the region past it is shaded, so the claim is false if any solid marker reaches the shaded band. Hollow markers are the variables the moment gate does not apply to and are drawn rather than dropped: a reference that never moved has no mean to compare, and a valve stuck by the scenario's own fault is judged on its distribution instead, which is the decision recorded as B-0047d. Drawn by cargo xtask validate --tiers 5 --smoke at commit 03ec3d6-dirty; the measurement it repeats was first recorded in B-0047b (LOG.org).

The ladder

tierwhat it proveschapter
1TESUB1 to TESUB8 match the oracletier 1, from b6ef4ee-dirty by cargo xtask validate --tiers 1,2,3 --smoke
2single-step derivatives match, over all three poolstier 2, from b6ef4ee-dirty by cargo xtask validate --tiers 1,2,3 --smoke
3the generator call order matches, draw for drawtier 3, from b6ef4ee-dirty by cargo xtask validate --tiers 1,2,3 --smoke
4trajectories stay inside the measurement noise (diagnostic)runs, but writes no chapter yet
5statistical equivalence: TOST, KS, ACF, spectra, correlationsruns, but writes no chapter yet
6downstream detectors cannot tell the two sources apartruns, but writes no chapter yet
7the published d00 to d21 files are reproducedruns, but writes no chapter yet
8differential fuzzing finds no counterexampleno harness yet
9identical digests across platforms, wasm includedruns, but writes no chapter yet
10every quirk fix ships with a measured deltaruns, but writes no chapter yet

This run selected tier(s) [5] and wrote chapter(s) for []. Tiers 4 to 7, 9 and 10 run but do not write a chapter yet; tier 8 has no harness. The delta index is generated separately, by cargo xtask deltas.

Tier 1: the utility routines

This page is generated. cargo xtask validate --tiers 1,2,3 --smoke wrote it from commit b6ef4ee-dirty. Every number on it was captured from that run's own output. To change what it says, change what the suite measures and run the command again.

TESUB1 (enthalpy), TESUB2 (temperature from enthalpy by Newton), TESUB3 (heat capacity) and TESUB4 (liquid density) are swept against the Fortran over a simplex grid, a Dirichlet sample and a boundary pool, at every temperature in the physical range, for each of the three ITY modes. PLAN.org sets the gate at a maximum relative error below 1e-13, with a ULP histogram reported rather than a verdict.

Reduced volume. This run passed --smoke, so the sweeps are the short ones the CI gate uses rather than the full ones PLAN.org specifies. The case counts in the tables below are what actually ran. Drop --smoke for the gate volume.

Produced with rustc 1.97.1 (8bab26f4f 2026-07-14), gfortran 15.2.0. The oracle's compiler flags are fixed in crates/tepsim-oracle/build.rs and asserted by a test; changing them invalidates every number on this page, which is why it is a logged re-baseline and not an edit.

What ran

2 test binaries: 7 test(s) passed, 0 failed, 0 ignored.

targetlibmpassedfailedignored
tier1_enthalpyvendored500
tier1_temperaturevendored200

Figures

GENERATED by `cargo xtask validate --tiers 1,2,3 --smoke` from commit `b6ef4ee-dirty`. Do not edit by hand: the next run overwrites it. Tier 1: every comparison against the 1e-13 gateA logarithmic strip plot. Each dot is one comparison against the Fortran, placed at its maximum relative error, in a lane named for the test target that produced it. 13 of 14 comparisons are exactly zero and sit in the separate lane at the left. The gate at 1e-13 is a dashed vertical line with the region beyond it shaded; 1 dots lie beyond it. Tier 1: every comparison, against the gate 14 comparisons over 2 target(s). 13 are bit-identical to the Fortran. 1 lies beyond the gate. 1e-14 1e-13 1e-12 1e-11 1e-10 1e-9 1e-8 1e-7 1e-6 1e-5 1e-4 1e-3 maximum relative error against the Fortran = 0 gate 1e-13 tier1_enthalpy tier1 TESUB1 ity=2, offset as f64: 1.597e-5 (vendored libm, reading_the_offset_as_double_precision_would_fail_the_gate_by_orders) tier1 TESUB1 ity=0: exactly 0 (vendored libm, tesub1_matches_the_fortran_over_the_full_sweep) tier1 TESUB1 ity=1: exactly 0 (vendored libm, tesub1_matches_the_fortran_over_the_full_sweep) tier1 TESUB1 ity=2: exactly 0 (vendored libm, tesub1_matches_the_fortran_over_the_full_sweep) tier1 TESUB3 ity=0: exactly 0 (vendored libm, tesub3_matches_the_fortran_over_the_full_sweep) tier1 TESUB3 ity=1: exactly 0 (vendored libm, tesub3_matches_the_fortran_over_the_full_sweep) tier1 TESUB3 ity=2: exactly 0 (vendored libm, tesub3_matches_the_fortran_over_the_full_sweep) tier1 TESUB4: exactly 0 (vendored libm, tesub4_matches_the_fortran_over_the_full_sweep) tier1_temperature tier1 TESUB2 ity=0 start=warm: exactly 0 (vendored libm, tesub2_matches_the_fortran_and_the_silent_failure_never_fires) tier1 TESUB2 ity=0 start=cold: exactly 0 (vendored libm, tesub2_matches_the_fortran_and_the_silent_failure_never_fires) tier1 TESUB2 ity=1 start=warm: exactly 0 (vendored libm, tesub2_matches_the_fortran_and_the_silent_failure_never_fires) tier1 TESUB2 ity=1 start=cold: exactly 0 (vendored libm, tesub2_matches_the_fortran_and_the_silent_failure_never_fires) tier1 TESUB2 ity=2 start=warm: exactly 0 (vendored libm, tesub2_matches_the_fortran_and_the_silent_failure_never_fires) tier1 TESUB2 ity=2 start=cold: exactly 0 (vendored libm, tesub2_matches_the_fortran_and_the_silent_failure_never_fires) vendored libm vendored libm beyond the gate beyond the gate hover a dot for the comparison it came from
Every comparison, against the gate. Every comparison this tier made, at its own maximum relative error, in a lane named for the target that ran it. Hovering a dot names the comparison. A dot is orange because its value is past the 1e-13 gate and for no other reason: no test is recognised by name, so a regression and a deliberate positive control are drawn identically. Beyond it in this run: tier1 TESUB1 ity=2, offset as f64, and nothing else. Any other dot crossing the line is a failure. The bit-equality half of the claim is falsified separately, by any dot leaving the = 0 lane under the platform libm, where the two sides call the same exp. Drawn by cargo xtask validate --tiers 1,2,3 --smoke at commit b6ef4ee-dirty; the measurement it repeats was first recorded in B-0009, B-0010 and B-0011 (LOG.org).
GENERATED by `cargo xtask validate --tiers 1,2,3 --smoke` from commit `b6ef4ee-dirty`. Do not edit by hand: the next run overwrites it. Tier 1: how many bits differA bar chart on a logarithmic count axis. Each bar is the number of comparisons whose result differed from the Fortran's by that many units in the last place, grouped by which libm the port was built against. 103,350 comparisons in total. Tier 1: how many bits actually differ 103,350 comparisons, by units in the last place. The count axis is logarithmic. One block beyond the gate is left out. vendored libm: 103,350 of 103,350 identical to the last bit 0 ULP 103,350 1 10 1e2 1e3 1e4 1e5 1e6 A bar exists only where the count is not zero, so a tier with one bar differed nowhere.
How many bits actually differ. The same comparisons counted by how many bits differ rather than by how much. A relative error is a ratio, and dividing by a large number makes any difference look small; a count of differing units in the last place cannot be flattered that way. This tier is straight-line arithmetic over constants already proved bit-identical, so the claim is that the only bar is the one at zero. A second bar appearing anywhere is a regression to chase rather than a tolerance to widen. Drawn by cargo xtask validate --tiers 1,2,3 --smoke at commit b6ef4ee-dirty; the measurement it repeats was first recorded in B-0009, B-0010 and B-0011 (LOG.org).

Measurements

15 block(s), lifted from the transcripts below. The columns are whatever fields the run printed, so a new field in the reporter becomes a new column here rather than data this page drops.

The test column matters as much as the numbers, because not every row is the port being measured. Some tests deliberately mis-type a constant, or solve from the wrong guess, to show what that would cost; a row from one of those is supposed to be enormous, and its test name says so. The what column carries a tier1 prefix in every tier, because it is the shared comparison reporter's own label rather than a claim about which tier printed it.

targetfrom testwhatcasesmax rel errmax ulpulp percentilesulp histogramnon-finiteabandonedround tripelapsedFortranport
tier1_enthalpyreading_the_offset_as_double_precision_would_fail_the_gate_by_orderstier1 TESUB1 ity=2, offset as f6479501.597e-5 at grid#704 T=21.875103098852352 at grid#704 T=21.875p50>=16 p90>=16 p99>=16 p100>=16>=16:79500 seen, 0 mismatched
tier1_enthalpytesub1_matches_the_fortran_over_the_full_sweeptier1 TESUB1 ity=079500.000e0 at grid#0 T=00 at grid#0 T=0p50=0 p90=0 p99=0 p100=00:79500 seen, 0 mismatched [0.0 s]
tier1_enthalpytesub1_matches_the_fortran_over_the_full_sweeptier1 TESUB1 ity=179500.000e0 at grid#0 T=00 at grid#0 T=0p50=0 p90=0 p99=0 p100=00:79500 seen, 0 mismatched [0.0 s]
tier1_enthalpytesub1_matches_the_fortran_over_the_full_sweeptier1 TESUB1 ity=279500.000e0 at grid#0 T=00 at grid#0 T=0p50=0 p90=0 p99=0 p100=00:79500 seen, 0 mismatched [0.0 s]
tier1_enthalpytesub3_matches_the_fortran_over_the_full_sweeptier1 TESUB3 ity=079500.000e0 at grid#0 T=00 at grid#0 T=0p50=0 p90=0 p99=0 p100=00:79500 seen, 0 mismatched [0.0 s]
tier1_enthalpytesub3_matches_the_fortran_over_the_full_sweeptier1 TESUB3 ity=179500.000e0 at grid#0 T=00 at grid#0 T=0p50=0 p90=0 p99=0 p100=00:79500 seen, 0 mismatched [0.0 s]
tier1_enthalpytesub3_matches_the_fortran_over_the_full_sweeptier1 TESUB3 ity=279500.000e0 at grid#0 T=00 at grid#0 T=0p50=0 p90=0 p99=0 p100=00:79500 seen, 0 mismatched [0.0 s]
tier1_enthalpytesub4_matches_the_fortran_over_the_full_sweeptier1 TESUB479500.000e0 at grid#0 T=00 at grid#0 T=0p50=0 p90=0 p99=0 p100=00:79500 seen, 0 mismatched [0.0 s]
tier1_temperaturetesub2_matches_the_fortran_and_the_silent_failure_never_firestier1 TESUB2 ity=0 start=warm79500.000e0 at grid#0 T=00 at grid#0 T=0p50=0 p90=0 p99=0 p100=00:79500 seen, 0 mismatched0 (delta D-001)max |solved - true| = 0e0 C0.0 s
tier1_temperaturetesub2_matches_the_fortran_and_the_silent_failure_never_firestier1 TESUB2 ity=0 start=cold79500.000e0 at grid#0 T=00 at grid#0 T=0p50=0 p90=0 p99=0 p100=00:79500 seen, 0 mismatched0 (delta D-001)max |solved - true| = 5.684341886080802e-14 C at grid#3637 T=131.250.0 s
tier1_temperaturetesub2_matches_the_fortran_and_the_silent_failure_never_firestier1 TESUB2 ity=1 start=warm79500.000e0 at grid#0 T=00 at grid#0 T=0p50=0 p90=0 p99=0 p100=00:79500 seen, 0 mismatched0 (delta D-001)max |solved - true| = 0e0 C0.0 s
tier1_temperaturetesub2_matches_the_fortran_and_the_silent_failure_never_firestier1 TESUB2 ity=1 start=cold79500.000e0 at grid#0 T=00 at grid#0 T=0p50=0 p90=0 p99=0 p100=00:79500 seen, 0 mismatched0 (delta D-001)max |solved - true| = 1.9895196601282805e-13 C at grid#4617 T=1700.0 s
tier1_temperaturetesub2_matches_the_fortran_and_the_silent_failure_never_firestier1 TESUB2 ity=2 start=warm79500.000e0 at grid#0 T=00 at grid#0 T=0p50=0 p90=0 p99=0 p100=00:79500 seen, 0 mismatched0 (delta D-001)max |solved - true| = 0e0 C0.0 s
tier1_temperaturetesub2_matches_the_fortran_and_the_silent_failure_never_firestier1 TESUB2 ity=2 start=cold79500.000e0 at grid#0 T=00 at grid#0 T=0p50=0 p90=0 p99=0 p100=00:79500 seen, 0 mismatched0 (delta D-001)max |solved - true| = 5.115907697472721e-13 C at face#96 T=144.143050562349230.0 s
tier1_temperaturethe_fortran_silently_returns_the_guess_where_the_port_reports_failureD-001 demonstrated:returned 120.4 silentlyNewton did not converge in 100 iterations from a guess of 120.4 C: reached -48598544002334720 C with a final step of 24299272000832196, which is not below 1e-12

Transcripts

Each block below is a test binary's own output, verbatim, with the command that produced it. The summary above is derived from these; they are not derived from it.

tier1_enthalpy, vendored libm

cargo test -p tepsim-oracle --features oracle --release --test tier1_enthalpy -- --nocapture --test-threads 1
running 5 tests
test both_routines_are_bit_identical_to_the_fortran ... sweep: SMOKE, 7950 cases. The 9987490-case gate is `cargo xtask validate --tiers 1`.
ok
test reading_the_offset_as_double_precision_would_fail_the_gate_by_orders ... tier1 TESUB1 ity=2, offset as f64
  cases          : 7950
  max rel err    : 1.597e-5 at grid#704 T=21.875
  max ulp        : 103098852352 at grid#704 T=21.875
  ulp percentiles: p50>=16 p90>=16 p99>=16 p100>=16
  ulp histogram  : >=16:7950
  non-finite     : 0 seen, 0 mismatched
ok
test tesub1_matches_the_fortran_over_the_full_sweep ... sweep: SMOKE, 7950 cases. The 9987490-case gate is `cargo xtask validate --tiers 1`.
tier1 TESUB1 ity=0
  cases          : 7950
  max rel err    : 0.000e0 at grid#0 T=0
  max ulp        : 0 at grid#0 T=0
  ulp percentiles: p50=0 p90=0 p99=0 p100=0
  ulp histogram  : 0:7950
  non-finite     : 0 seen, 0 mismatched  [0.0 s]
tier1 TESUB1 ity=1
  cases          : 7950
  max rel err    : 0.000e0 at grid#0 T=0
  max ulp        : 0 at grid#0 T=0
  ulp percentiles: p50=0 p90=0 p99=0 p100=0
  ulp histogram  : 0:7950
  non-finite     : 0 seen, 0 mismatched  [0.0 s]
tier1 TESUB1 ity=2
  cases          : 7950
  max rel err    : 0.000e0 at grid#0 T=0
  max ulp        : 0 at grid#0 T=0
  ulp percentiles: p50=0 p90=0 p99=0 p100=0
  ulp histogram  : 0:7950
  non-finite     : 0 seen, 0 mismatched  [0.0 s]
ok
test tesub3_matches_the_fortran_over_the_full_sweep ... sweep: SMOKE, 7950 cases. The 9987490-case gate is `cargo xtask validate --tiers 1`.
tier1 TESUB3 ity=0
  cases          : 7950
  max rel err    : 0.000e0 at grid#0 T=0
  max ulp        : 0 at grid#0 T=0
  ulp percentiles: p50=0 p90=0 p99=0 p100=0
  ulp histogram  : 0:7950
  non-finite     : 0 seen, 0 mismatched  [0.0 s]
tier1 TESUB3 ity=1
  cases          : 7950
  max rel err    : 0.000e0 at grid#0 T=0
  max ulp        : 0 at grid#0 T=0
  ulp percentiles: p50=0 p90=0 p99=0 p100=0
  ulp histogram  : 0:7950
  non-finite     : 0 seen, 0 mismatched  [0.0 s]
tier1 TESUB3 ity=2
  cases          : 7950
  max rel err    : 0.000e0 at grid#0 T=0
  max ulp        : 0 at grid#0 T=0
  ulp percentiles: p50=0 p90=0 p99=0 p100=0
  ulp histogram  : 0:7950
  non-finite     : 0 seen, 0 mismatched  [0.0 s]
ok
test tesub4_matches_the_fortran_over_the_full_sweep ... sweep: SMOKE, 7950 cases. The 9987490-case gate is `cargo xtask validate --tiers 1`.
tier1 TESUB4
  cases          : 7950
  max rel err    : 0.000e0 at grid#0 T=0
  max ulp        : 0 at grid#0 T=0
  ulp percentiles: p50=0 p90=0 p99=0 p100=0
  ulp histogram  : 0:7950
  non-finite     : 0 seen, 0 mismatched  [0.0 s]
ok

test result: ok. 5 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.01s

tier1_temperature, vendored libm

cargo test -p tepsim-oracle --features oracle --release --test tier1_temperature -- --nocapture --test-threads 1
running 2 tests
test tesub2_matches_the_fortran_and_the_silent_failure_never_fires ... sweep: SMOKE, 7950 cases. The 9987490-case gate is `cargo xtask validate --tiers 1`.
tier1 TESUB2 ity=0 start=warm
  cases          : 7950
  max rel err    : 0.000e0 at grid#0 T=0
  max ulp        : 0 at grid#0 T=0
  ulp percentiles: p50=0 p90=0 p99=0 p100=0
  ulp histogram  : 0:7950
  non-finite     : 0 seen, 0 mismatched
  abandoned      : 0 (delta D-001)
  round trip     : max |solved - true| = 0e0 C
  elapsed        : 0.0 s
tier1 TESUB2 ity=0 start=cold
  cases          : 7950
  max rel err    : 0.000e0 at grid#0 T=0
  max ulp        : 0 at grid#0 T=0
  ulp percentiles: p50=0 p90=0 p99=0 p100=0
  ulp histogram  : 0:7950
  non-finite     : 0 seen, 0 mismatched
  abandoned      : 0 (delta D-001)
  round trip     : max |solved - true| = 5.684341886080802e-14 C at grid#3637 T=131.25
  elapsed        : 0.0 s
tier1 TESUB2 ity=1 start=warm
  cases          : 7950
  max rel err    : 0.000e0 at grid#0 T=0
  max ulp        : 0 at grid#0 T=0
  ulp percentiles: p50=0 p90=0 p99=0 p100=0
  ulp histogram  : 0:7950
  non-finite     : 0 seen, 0 mismatched
  abandoned      : 0 (delta D-001)
  round trip     : max |solved - true| = 0e0 C
  elapsed        : 0.0 s
tier1 TESUB2 ity=1 start=cold
  cases          : 7950
  max rel err    : 0.000e0 at grid#0 T=0
  max ulp        : 0 at grid#0 T=0
  ulp percentiles: p50=0 p90=0 p99=0 p100=0
  ulp histogram  : 0:7950
  non-finite     : 0 seen, 0 mismatched
  abandoned      : 0 (delta D-001)
  round trip     : max |solved - true| = 1.9895196601282805e-13 C at grid#4617 T=170
  elapsed        : 0.0 s
tier1 TESUB2 ity=2 start=warm
  cases          : 7950
  max rel err    : 0.000e0 at grid#0 T=0
  max ulp        : 0 at grid#0 T=0
  ulp percentiles: p50=0 p90=0 p99=0 p100=0
  ulp histogram  : 0:7950
  non-finite     : 0 seen, 0 mismatched
  abandoned      : 0 (delta D-001)
  round trip     : max |solved - true| = 0e0 C
  elapsed        : 0.0 s
tier1 TESUB2 ity=2 start=cold
  cases          : 7950
  max rel err    : 0.000e0 at grid#0 T=0
  max ulp        : 0 at grid#0 T=0
  ulp percentiles: p50=0 p90=0 p99=0 p100=0
  ulp histogram  : 0:7950
  non-finite     : 0 seen, 0 mismatched
  abandoned      : 0 (delta D-001)
  round trip     : max |solved - true| = 5.115907697472721e-13 C at face#96 T=144.14305056234923
  elapsed        : 0.0 s
ok
test the_fortran_silently_returns_the_guess_where_the_port_reports_failure ... D-001 demonstrated:
  Fortran: returned 120.4 silently
  port:    Newton did not converge in 100 iterations from a guess of 120.4 C: reached -48598544002334720 C with a final step of 24299272000832196, which is not below 1e-12
ok

test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.03s

Tier 2: the plant model

This page is generated. cargo xtask validate --tiers 1,2,3 --smoke wrote it from commit b6ef4ee-dirty. Every number on it was captured from that run's own output. To change what it says, change what the suite measures and run the command again.

Both implementations are forced into an identical state, evaluated once, and compared on all fifty derivative components. Sampling is from three pools: states along the nominal closed-loop trajectory, random perturbations of those states, and adversarial states placed at every discontinuity and clamp in the model.

Every comparison runs twice. The vendored libm disagrees with gfortran's by an ULP on about a tenth of exp and pow calls, so the default build can only be held to 1e-12; the libm-system build removes the transcendental from the comparison and is held to bit equality. Both runs are below, and the libm column says which is which.

The tolerance is relative to the scale of the terms rather than to the result. A balance is inflow minus outflow, and near steady state those nearly cancel, so an error that is 1e-16 of either term can be 1e-4 of their difference.

Reduced volume. This run passed --smoke, so the sweeps are the short ones the CI gate uses rather than the full ones PLAN.org specifies. The case counts in the tables below are what actually ran. Drop --smoke for the gate volume.

Produced with rustc 1.97.1 (8bab26f4f 2026-07-14), gfortran 15.2.0. The oracle's compiler flags are fixed in crates/tepsim-oracle/build.rs and asserted by a test; changing them invalidates every number on this page, which is why it is a logged re-baseline and not an edit.

What ran

20 test binaries: 94 test(s) passed, 0 failed, 0 ignored.

targetlibmpassedfailedignored
tier2_unpackvendored300
tier2_equilibriumvendored300
tier2_kineticsvendored300
tier2_streamsvendored300
tier2_flowsvendored500
tier2_strippervendored400
tier2_heatvendored400
tier2_measurementsvendored400
tier2_balancesvendored400
tier4_closed_loopplatform500
tier5_invariantsplatform900
tier5_runsplatform1000
tier2_equilibriumplatform300
tier2_kineticsplatform400
tier2_streamsplatform400
tier2_flowsplatform600
tier2_stripperplatform500
tier2_heatplatform500
tier2_measurementsplatform500
tier2_balancesplatform500

Figures

GENERATED by `cargo xtask validate --tiers 1,2,3 --smoke` from commit `b6ef4ee-dirty`. Do not edit by hand: the next run overwrites it. Tier 2: every comparison against the 1e-12 gateA logarithmic strip plot. Each dot is one comparison against the Fortran, placed at its maximum relative error, in a lane named for the test target that produced it. 115 of 141 comparisons are exactly zero and sit in the separate lane at the left. The gate at 1e-12 is a dashed vertical line with the region beyond it shaded; 1 dots lie beyond it. Tier 2: every comparison, against the gate 141 comparisons over 9 target(s). 115 are bit-identical to the Fortran. 1 lies beyond the gate. 1e-16 1e-14 1e-12 1e-10 1e-8 1e-6 1e-4 1e-2 maximum relative error against the Fortran = 0 gate 1e-12 tier2_unpack tier1 TCV with the carried seed: exactly 0 (vendored libm, solving_from_a_fixed_guess_instead_of_the_seed_breaks_bit_equality) tier1 TCV from a fixed guess: 9.901e-16 (vendored libm, solving_from_a_fixed_guess_instead_of_the_seed_breaks_bit_equality) tier1 unpack UCLR: exactly 0 (vendored libm, the_unpacking_matches_the_fortran_over_all_three_pools) tier1 unpack UCLS: exactly 0 (vendored libm, the_unpacking_matches_the_fortran_over_all_three_pools) tier1 unpack UCLC: exactly 0 (vendored libm, the_unpacking_matches_the_fortran_over_all_three_pools) tier1 unpack UCVV: exactly 0 (vendored libm, the_unpacking_matches_the_fortran_over_all_three_pools) tier1 unpack UTLR/UTLS/UTLC/UTVV: exactly 0 (vendored libm, the_unpacking_matches_the_fortran_over_all_three_pools) tier1 unpack XLR: exactly 0 (vendored libm, the_unpacking_matches_the_fortran_over_all_three_pools) tier1 unpack XLS: exactly 0 (vendored libm, the_unpacking_matches_the_fortran_over_all_three_pools) tier1 unpack XLC: exactly 0 (vendored libm, the_unpacking_matches_the_fortran_over_all_three_pools) tier1 unpack XVV: exactly 0 (vendored libm, the_unpacking_matches_the_fortran_over_all_three_pools) tier1 unpack ESR/ESS/ESC/ESV: exactly 0 (vendored libm, the_unpacking_matches_the_fortran_over_all_three_pools) tier1 unpack TCR/TCS/TCC/TCV: exactly 0 (vendored libm, the_unpacking_matches_the_fortran_over_all_three_pools) tier1 unpack TKR/TKS/TKV: exactly 0 (vendored libm, the_unpacking_matches_the_fortran_over_all_three_pools) tier1 unpack DLR/DLS/DLC: exactly 0 (vendored libm, the_unpacking_matches_the_fortran_over_all_three_pools) tier1 unpack VLR/VLS/VLC: exactly 0 (vendored libm, the_unpacking_matches_the_fortran_over_all_three_pools) tier2_equilibrium tier1 equilibrium VVR/VVS: exactly 0 (vendored libm, the_equilibrium_matches_the_fortran_over_all_three_pools) tier1 equilibrium PPR: 2.802e-16 (vendored libm, the_equilibrium_matches_the_fortran_over_all_three_pools) tier1 equilibrium PPS: 2.784e-16 (vendored libm, the_equilibrium_matches_the_fortran_over_all_three_pools) tier1 equilibrium PTR/PTS/PTV: 3.578e-16 (vendored libm, the_equilibrium_matches_the_fortran_over_all_three_pools) tier1 equilibrium XVR: 4.843e-16 (vendored libm, the_equilibrium_matches_the_fortran_over_all_three_pools) tier1 equilibrium XVS: 4.302e-16 (vendored libm, the_equilibrium_matches_the_fortran_over_all_three_pools) tier1 equilibrium UTVR/UTVS: 4.443e-16 (vendored libm, the_equilibrium_matches_the_fortran_over_all_three_pools) tier1 equilibrium UCVR: 5.077e-16 (vendored libm, the_equilibrium_matches_the_fortran_over_all_three_pools) tier1 equilibrium UCVS: 6.230e-16 (vendored libm, the_equilibrium_matches_the_fortran_over_all_three_pools) tier1 PPR/PPS for A, B and C: exactly 0 (vendored libm, the_ideal_gas_partial_pressures_are_bit_identical_under_the_vendored_libm) tier1 PTV: exactly 0 (vendored libm, the_ideal_gas_partial_pressures_are_bit_identical_under_the_vendored_libm) tier1 equilibrium VVR/VVS: exactly 0 (platform libm, the_algebra_is_bit_identical_once_exp_agrees) tier1 equilibrium PPR: exactly 0 (platform libm, the_algebra_is_bit_identical_once_exp_agrees) tier1 equilibrium PPS: exactly 0 (platform libm, the_algebra_is_bit_identical_once_exp_agrees) tier1 equilibrium PTR/PTS/PTV: exactly 0 (platform libm, the_algebra_is_bit_identical_once_exp_agrees) tier1 equilibrium XVR: exactly 0 (platform libm, the_algebra_is_bit_identical_once_exp_agrees) tier1 equilibrium XVS: exactly 0 (platform libm, the_algebra_is_bit_identical_once_exp_agrees) tier1 equilibrium UTVR/UTVS: exactly 0 (platform libm, the_algebra_is_bit_identical_once_exp_agrees) tier1 equilibrium UCVR: exactly 0 (platform libm, the_algebra_is_bit_identical_once_exp_agrees) tier1 equilibrium UCVS: exactly 0 (platform libm, the_algebra_is_bit_identical_once_exp_agrees) tier1 equilibrium VVR/VVS: exactly 0 (platform libm, the_equilibrium_matches_the_fortran_over_all_three_pools) tier1 equilibrium PPR: exactly 0 (platform libm, the_equilibrium_matches_the_fortran_over_all_three_pools) tier1 equilibrium PPS: exactly 0 (platform libm, the_equilibrium_matches_the_fortran_over_all_three_pools) tier1 equilibrium PTR/PTS/PTV: exactly 0 (platform libm, the_equilibrium_matches_the_fortran_over_all_three_pools) tier1 equilibrium XVR: exactly 0 (platform libm, the_equilibrium_matches_the_fortran_over_all_three_pools) tier1 equilibrium XVS: exactly 0 (platform libm, the_equilibrium_matches_the_fortran_over_all_three_pools) tier1 equilibrium UTVR/UTVS: exactly 0 (platform libm, the_equilibrium_matches_the_fortran_over_all_three_pools) tier1 equilibrium UCVR: exactly 0 (platform libm, the_equilibrium_matches_the_fortran_over_all_three_pools) tier1 equilibrium UCVS: exactly 0 (platform libm, the_equilibrium_matches_the_fortran_over_all_three_pools) tier2_kinetics tier1 CRXR(2), never assigned: exactly 0 (vendored libm, the_inert_has_no_net_production_in_either_implementation) tier1 kinetics RR: 8.492e-16 (vendored libm, the_kinetics_match_the_fortran_over_all_three_pools) tier1 kinetics CRXR: 8.492e-16 (vendored libm, the_kinetics_match_the_fortran_over_all_three_pools) tier1 kinetics RH: 7.419e-16 (vendored libm, the_kinetics_match_the_fortran_over_all_three_pools) tier1 kinetics RR: exactly 0 (platform libm, the_algebra_is_bit_identical_once_exp_and_pow_agree) tier1 kinetics CRXR: exactly 0 (platform libm, the_algebra_is_bit_identical_once_exp_and_pow_agree) tier1 kinetics RH: exactly 0 (platform libm, the_algebra_is_bit_identical_once_exp_and_pow_agree) tier1 CRXR(2), never assigned: exactly 0 (platform libm, the_inert_has_no_net_production_in_either_implementation) tier1 kinetics RR: exactly 0 (platform libm, the_kinetics_match_the_fortran_over_all_three_pools) tier1 kinetics CRXR: exactly 0 (platform libm, the_kinetics_match_the_fortran_over_all_three_pools) tier1 kinetics RH: exactly 0 (platform libm, the_kinetics_match_the_fortran_over_all_three_pools) tier2_streams tier1 streams XST (10 streams x 8): 4.736e-16 (vendored libm, the_stream_table_matches_the_fortran_over_all_three_pools) tier1 streams XMWS (the 6 that exist): 4.370e-16 (vendored libm, the_stream_table_matches_the_fortran_over_all_three_pools) tier1 streams TST: exactly 0 (vendored libm, the_stream_table_matches_the_fortran_over_all_three_pools) tier1 streams HST: 5.050e-16 (vendored libm, the_stream_table_matches_the_fortran_over_all_three_pools) tier1 streams XST (10 streams x 8): exactly 0 (platform libm, the_algebra_is_bit_identical_once_exp_and_pow_agree) tier1 streams XMWS (the 6 that exist): exactly 0 (platform libm, the_algebra_is_bit_identical_once_exp_and_pow_agree) tier1 streams TST: exactly 0 (platform libm, the_algebra_is_bit_identical_once_exp_and_pow_agree) tier1 streams HST: exactly 0 (platform libm, the_algebra_is_bit_identical_once_exp_and_pow_agree) tier1 streams XST (10 streams x 8): exactly 0 (platform libm, the_stream_table_matches_the_fortran_over_all_three_pools) tier1 streams XMWS (the 6 that exist): exactly 0 (platform libm, the_stream_table_matches_the_fortran_over_all_three_pools) tier1 streams TST: exactly 0 (platform libm, the_stream_table_matches_the_fortran_over_all_three_pools) tier1 streams HST: exactly 0 (platform libm, the_stream_table_matches_the_fortran_over_all_three_pools) tier2_flows tier1 flows FTM (the 10 assembled streams): 2.246e-14 (vendored libm, the_flow_network_matches_the_fortran_over_all_three_pools) tier1 flows FCM (10 streams x 8): 2.268e-14 (vendored libm, the_flow_network_matches_the_fortran_over_all_three_pools) tier1 flows FWR/FWS/AGSP: exactly 0 (vendored libm, the_flow_network_matches_the_fortran_over_all_three_pools) tier1 flows CPDH: 2.034e-15 (vendored libm, the_flow_network_matches_the_fortran_over_all_three_pools) tier1 flows HST(9) after the compressor bump: 6.522e-15 (vendored libm, the_flow_network_matches_the_fortran_over_all_three_pools) tier1 UAC, via QUC: exactly 0 (vendored libm, the_steam_coefficient_matches_through_the_condenser_duty) tier1 flows FTM (the 10 assembled streams): exactly 0 (platform libm, the_algebra_is_bit_identical_once_exp_and_pow_agree) tier1 flows FCM (10 streams x 8): exactly 0 (platform libm, the_algebra_is_bit_identical_once_exp_and_pow_agree) tier1 flows FWR/FWS/AGSP: exactly 0 (platform libm, the_algebra_is_bit_identical_once_exp_and_pow_agree) tier1 flows CPDH: exactly 0 (platform libm, the_algebra_is_bit_identical_once_exp_and_pow_agree) tier1 flows HST(9) after the compressor bump: exactly 0 (platform libm, the_algebra_is_bit_identical_once_exp_and_pow_agree) tier1 flows FTM (the 10 assembled streams): exactly 0 (platform libm, the_flow_network_matches_the_fortran_over_all_three_pools) tier1 flows FCM (10 streams x 8): exactly 0 (platform libm, the_flow_network_matches_the_fortran_over_all_three_pools) tier1 flows FWR/FWS/AGSP: exactly 0 (platform libm, the_flow_network_matches_the_fortran_over_all_three_pools) tier1 flows CPDH: exactly 0 (platform libm, the_flow_network_matches_the_fortran_over_all_three_pools) tier1 flows HST(9) after the compressor bump: exactly 0 (platform libm, the_flow_network_matches_the_fortran_over_all_three_pools) tier1 UAC, via QUC: exactly 0 (platform libm, the_steam_coefficient_matches_through_the_condenser_duty) tier2_stripper tier1 stripper SFR: exactly 0 (vendored libm, the_stripper_matches_the_fortran_over_all_three_pools) tier1 stripper FTM (5, 12): the column's own outlets: exactly 0 (vendored libm, the_stripper_matches_the_fortran_over_all_three_pools) tier1 stripper FTM (7): the reactor-inlet alias: 1.484e-15 (vendored libm, the_stripper_matches_the_fortran_over_all_three_pools) tier1 stripper FCM (5, 12): the column's own outlets: exactly 0 (vendored libm, the_stripper_matches_the_fortran_over_all_three_pools) tier1 stripper FCM (7): the reactor-inlet alias: 1.610e-15 (vendored libm, the_stripper_matches_the_fortran_over_all_three_pools) tier1 stripper XST (5, 12): the column's own outlets: exactly 0 (vendored libm, the_stripper_matches_the_fortran_over_all_three_pools) tier1 stripper XST (7): the reactor-inlet alias: exactly 0 (vendored libm, the_stripper_matches_the_fortran_over_all_three_pools) tier1 stripper TST (5, 7, 12): exactly 0 (vendored libm, the_stripper_matches_the_fortran_over_all_three_pools) tier1 stripper HST (5, 12): the column's own outlets: exactly 0 (vendored libm, the_stripper_matches_the_fortran_over_all_three_pools) tier1 stripper HST (7): the reactor-inlet alias: exactly 0 (vendored libm, the_stripper_matches_the_fortran_over_all_three_pools) tier1 stripper SFR: exactly 0 (platform libm, the_algebra_is_bit_identical_once_exp_and_pow_agree) tier1 stripper FTM (5, 12): the column's own outlets: exactly 0 (platform libm, the_algebra_is_bit_identical_once_exp_and_pow_agree) tier1 stripper FTM (7): the reactor-inlet alias: exactly 0 (platform libm, the_algebra_is_bit_identical_once_exp_and_pow_agree) tier1 stripper FCM (5, 12): the column's own outlets: exactly 0 (platform libm, the_algebra_is_bit_identical_once_exp_and_pow_agree) tier1 stripper FCM (7): the reactor-inlet alias: exactly 0 (platform libm, the_algebra_is_bit_identical_once_exp_and_pow_agree) tier1 stripper XST (5, 12): the column's own outlets: exactly 0 (platform libm, the_algebra_is_bit_identical_once_exp_and_pow_agree) tier1 stripper XST (7): the reactor-inlet alias: exactly 0 (platform libm, the_algebra_is_bit_identical_once_exp_and_pow_agree) tier1 stripper TST (5, 7, 12): exactly 0 (platform libm, the_algebra_is_bit_identical_once_exp_and_pow_agree) tier1 stripper HST (5, 12): the column's own outlets: exactly 0 (platform libm, the_algebra_is_bit_identical_once_exp_and_pow_agree) tier1 stripper HST (7): the reactor-inlet alias: exactly 0 (platform libm, the_algebra_is_bit_identical_once_exp_and_pow_agree) tier1 stripper SFR: exactly 0 (platform libm, the_stripper_matches_the_fortran_over_all_three_pools) tier1 stripper FTM (5, 12): the column's own outlets: exactly 0 (platform libm, the_stripper_matches_the_fortran_over_all_three_pools) tier1 stripper FTM (7): the reactor-inlet alias: exactly 0 (platform libm, the_stripper_matches_the_fortran_over_all_three_pools) tier1 stripper FCM (5, 12): the column's own outlets: exactly 0 (platform libm, the_stripper_matches_the_fortran_over_all_three_pools) tier1 stripper FCM (7): the reactor-inlet alias: exactly 0 (platform libm, the_stripper_matches_the_fortran_over_all_three_pools) tier1 stripper XST (5, 12): the column's own outlets: exactly 0 (platform libm, the_stripper_matches_the_fortran_over_all_three_pools) tier1 stripper XST (7): the reactor-inlet alias: exactly 0 (platform libm, the_stripper_matches_the_fortran_over_all_three_pools) tier1 stripper TST (5, 7, 12): exactly 0 (platform libm, the_stripper_matches_the_fortran_over_all_three_pools) tier1 stripper HST (5, 12): the column's own outlets: exactly 0 (platform libm, the_stripper_matches_the_fortran_over_all_three_pools) tier1 stripper HST (7): the reactor-inlet alias: exactly 0 (platform libm, the_stripper_matches_the_fortran_over_all_three_pools) tier2_heat tier1 heat UAR: exactly 0 (vendored libm, heat_transfer_matches_the_fortran_over_all_three_pools) tier1 heat QUR: exactly 0 (vendored libm, heat_transfer_matches_the_fortran_over_all_three_pools) tier1 heat QUS: 9.572e-13 (vendored libm, heat_transfer_matches_the_fortran_over_all_three_pools) tier1 heat QUC: exactly 0 (vendored libm, heat_transfer_matches_the_fortran_over_all_three_pools) tier1 heat UAR: exactly 0 (platform libm, heat_transfer_matches_the_fortran_over_all_three_pools) tier1 heat QUR: exactly 0 (platform libm, heat_transfer_matches_the_fortran_over_all_three_pools) tier1 heat QUS: exactly 0 (platform libm, heat_transfer_matches_the_fortran_over_all_three_pools) tier1 heat QUC: exactly 0 (platform libm, heat_transfer_matches_the_fortran_over_all_three_pools) tier1 heat UAR: exactly 0 (platform libm, the_algebra_is_bit_identical_once_exp_and_pow_agree) tier1 heat QUR: exactly 0 (platform libm, the_algebra_is_bit_identical_once_exp_and_pow_agree) tier1 heat QUS: exactly 0 (platform libm, the_algebra_is_bit_identical_once_exp_and_pow_agree) tier1 heat QUC: exactly 0 (platform libm, the_algebra_is_bit_identical_once_exp_and_pow_agree) tier2_measurements tier1 XMEAS(1..22), noise-free: 8.774e-15 (vendored libm, the_measurements_match_the_fortran_over_all_three_pools) tier1 ISD as 0 or 1: exactly 0 (vendored libm, the_shutdown_detector_agrees_with_the_fortran_on_every_state) tier1 XMEAS(1..22), noise-free: exactly 0 (platform libm, the_algebra_is_bit_identical_once_exp_and_pow_agree) tier1 XMEAS(1..22), noise-free: exactly 0 (platform libm, the_measurements_match_the_fortran_over_all_three_pools) tier1 ISD as 0 or 1: exactly 0 (platform libm, the_shutdown_detector_agrees_with_the_fortran_on_every_state) tier2_balances tier1 YP(1..50), relative to the derivative (reported): 1.393e-4 (vendored libm, all_fifty_derivatives_match_the_fortran_over_all_three_pools) tier1 YP(1..50), relative to the scale of the terms (the gate): 6.093e-14 (vendored libm, all_fifty_derivatives_match_the_fortran_over_all_three_pools) tier1 YP(1..50), plant frozen: exactly 0 (vendored libm, all_fifty_derivatives_match_the_fortran_over_all_three_pools) tier1 YP(1..50), relative to the scale of the terms (the gate): 6.093e-14 (vendored libm, tier2_acceptance_table) tier1 YP(1..50), relative to the derivative (reported): exactly 0 (platform libm, all_fifty_derivatives_match_the_fortran_over_all_three_pools) tier1 YP(1..50), relative to the scale of the terms (the gate): exactly 0 (platform libm, all_fifty_derivatives_match_the_fortran_over_all_three_pools) tier1 YP(1..50), plant frozen: exactly 0 (platform libm, all_fifty_derivatives_match_the_fortran_over_all_three_pools) tier1 YP(1..50), relative to the derivative (reported): exactly 0 (platform libm, the_whole_right_hand_side_is_bit_identical_once_exp_and_pow_agree) tier1 YP(1..50), relative to the scale of the terms (the gate): exactly 0 (platform libm, tier2_acceptance_table) vendored libm vendored libm platform libm platform libm, bit-exact claim beyond the gate beyond the gate hover a dot for the comparison it came from
Every comparison, against the gate. Every comparison this tier made, at its own maximum relative error, in a lane named for the target that ran it. Hovering a dot names the comparison. A dot is orange because its value is past the 1e-12 gate and for no other reason: no test is recognised by name, so a regression and a deliberate positive control are drawn identically. Beyond it in this run: tier1 YP(1..50), relative to the derivative (reported), and nothing else. Any other dot crossing the line is a failure. The bit-equality half of the claim is falsified separately, by any dot leaving the = 0 lane under the platform libm, where the two sides call the same exp. Drawn by cargo xtask validate --tiers 1,2,3 --smoke at commit b6ef4ee-dirty; the measurement it repeats was first recorded in B-0026 (LOG.org).
GENERATED by `cargo xtask validate --tiers 1,2,3 --smoke` from commit `b6ef4ee-dirty`. Do not edit by hand: the next run overwrites it. Tier 2: how many bits differA bar chart on a logarithmic count axis. Each bar is the number of comparisons whose result differed from the Fortran's by that many units in the last place, grouped by which libm the port was built against. 2,860,725 comparisons in total. Tier 2: how many bits actually differ 2,860,725 comparisons, by units in the last place. The count axis is logarithmic. One block beyond the gate is left out. vendored libm: 1,267,964 of 1,325,500 identical to the last bit 0 ULP 1,267,964 1 ULP 20,728 2 ULP 9,290 3 ULP 2,013 4 ULP 3,697 5 ULP 361 6 ULP 513 7 ULP 154 8 ULP 2,666 9 ULP 117 10 ULP 124 11 ULP 59 12 ULP 367 13 ULP 65 14 ULP 122 15 ULP 114 >=16 ULP 17,146 1 10 1e2 1e3 1e4 1e5 1e6 1e7 platform libm: 1,535,225 of 1,535,225 identical to the last bit 0 ULP 1,535,225 1 10 1e2 1e3 1e4 1e5 1e6 1e7 A bar exists only where the count is not zero, so a tier with one bar differed nowhere.
How many bits actually differ. The same comparisons counted by how many bits differ rather than by how much. A relative error is a ratio, and dividing by a large number makes any difference look small; a count of differing units in the last place cannot be flattered that way. The two groups are the whole argument: under the platform libm, where both sides call the same exp, the distribution is one bar at zero, and every comparison in the tier is identical to the last bit. Under the vendored libm a tail appears, and it is the transcendentals rather than the algebra. The claim is false the moment the platform group grows a second bar. Drawn by cargo xtask validate --tiers 1,2,3 --smoke at commit b6ef4ee-dirty; the measurement it repeats was first recorded in B-0026 (LOG.org).

Measurements

143 block(s), lifted from the transcripts below. The columns are whatever fields the run printed, so a new field in the reporter becomes a new column here rather than data this page drops.

The test column matters as much as the numbers, because not every row is the port being measured. Some tests deliberately mis-type a constant, or solve from the wrong guess, to show what that would cost; a row from one of those is supposed to be enormous, and its test name says so. The what column carries a tier1 prefix in every tier, because it is the shared comparison reporter's own label rather than a claim about which tier printed it.

targetfrom testwhatcasesmax rel errmax ulpulp percentilesulp histogramnon-finiteworst XMEASworst XMVwithin XNSfirst split, portfirst split, fortranworst over the runworst at the end
tier2_unpacksolving_from_a_fixed_guess_instead_of_the_seed_breaks_bit_equalitytier1 TCV with the carried seed1000.000e0 at nominal#0[4]0 at nominal#0[4]p50=0 p90=0 p99=0 p100=00:1000 seen, 0 mismatched
tier2_unpacksolving_from_a_fixed_guess_instead_of_the_seed_breaks_bit_equalitytier1 TCV from a fixed guess1009.901e-16 at nominal#25[4]6 at nominal#25[4]p50=1 p90=4 p99=6 p100=60:24 1:28 2:18 3:19 4:7 5:1 6:30 seen, 0 mismatched
tier2_unpackthe_unpacking_matches_the_fortran_over_all_three_poolstier1 unpack UCLR194000.000e0 at nominal#0[1]0 at nominal#0[1]p50=0 p90=0 p99=0 p100=00:194000 seen, 0 mismatched
tier2_unpackthe_unpacking_matches_the_fortran_over_all_three_poolstier1 unpack UCLS194000.000e0 at nominal#0[1]0 at nominal#0[1]p50=0 p90=0 p99=0 p100=00:194000 seen, 0 mismatched
tier2_unpackthe_unpacking_matches_the_fortran_over_all_three_poolstier1 unpack UCLC194000.000e0 at nominal#0[1]0 at nominal#0[1]p50=0 p90=0 p99=0 p100=00:194000 seen, 0 mismatched
tier2_unpackthe_unpacking_matches_the_fortran_over_all_three_poolstier1 unpack UCVV194000.000e0 at nominal#0[1]0 at nominal#0[1]p50=0 p90=0 p99=0 p100=00:194000 seen, 0 mismatched
tier2_unpackthe_unpacking_matches_the_fortran_over_all_three_poolstier1 unpack UTLR/UTLS/UTLC/UTVV97000.000e0 at nominal#0[1]0 at nominal#0[1]p50=0 p90=0 p99=0 p100=00:97000 seen, 0 mismatched
tier2_unpackthe_unpacking_matches_the_fortran_over_all_three_poolstier1 unpack XLR194000.000e0 at nominal#0[1]0 at nominal#0[1]p50=0 p90=0 p99=0 p100=00:194000 seen, 0 mismatched
tier2_unpackthe_unpacking_matches_the_fortran_over_all_three_poolstier1 unpack XLS194000.000e0 at nominal#0[1]0 at nominal#0[1]p50=0 p90=0 p99=0 p100=00:194000 seen, 0 mismatched
tier2_unpackthe_unpacking_matches_the_fortran_over_all_three_poolstier1 unpack XLC194000.000e0 at nominal#0[1]0 at nominal#0[1]p50=0 p90=0 p99=0 p100=00:194000 seen, 0 mismatched
tier2_unpackthe_unpacking_matches_the_fortran_over_all_three_poolstier1 unpack XVV194000.000e0 at nominal#0[1]0 at nominal#0[1]p50=0 p90=0 p99=0 p100=00:194000 seen, 0 mismatched
tier2_unpackthe_unpacking_matches_the_fortran_over_all_three_poolstier1 unpack ESR/ESS/ESC/ESV97000.000e0 at nominal#0[1]0 at nominal#0[1]p50=0 p90=0 p99=0 p100=00:97000 seen, 0 mismatched
tier2_unpackthe_unpacking_matches_the_fortran_over_all_three_poolstier1 unpack TCR/TCS/TCC/TCV97000.000e0 at nominal#0[1]0 at nominal#0[1]p50=0 p90=0 p99=0 p100=00:97000 seen, 0 mismatched
tier2_unpackthe_unpacking_matches_the_fortran_over_all_three_poolstier1 unpack TKR/TKS/TKV72750.000e0 at nominal#0[1]0 at nominal#0[1]p50=0 p90=0 p99=0 p100=00:72750 seen, 0 mismatched
tier2_unpackthe_unpacking_matches_the_fortran_over_all_three_poolstier1 unpack DLR/DLS/DLC72750.000e0 at nominal#0[1]0 at nominal#0[1]p50=0 p90=0 p99=0 p100=00:72750 seen, 0 mismatched
tier2_unpackthe_unpacking_matches_the_fortran_over_all_three_poolstier1 unpack VLR/VLS/VLC72750.000e0 at nominal#0[1]0 at nominal#0[1]p50=0 p90=0 p99=0 p100=00:72750 seen, 0 mismatched
tier2_equilibriumthe_equilibrium_matches_the_fortran_over_all_three_poolstier1 equilibrium VVR/VVS48500.000e0 at nominal#0[1]0 at nominal#0[1]p50=0 p90=0 p99=0 p100=00:48500 seen, 0 mismatched
tier2_equilibriumthe_equilibrium_matches_the_fortran_over_all_three_poolstier1 equilibrium PPR194002.802e-16 at perturbed#1264[4]2 at nominal#21[4]p50=0 p90=0 p99=1 p100=20:17917 1:1305 2:1780 seen, 0 mismatched
tier2_equilibriumthe_equilibrium_matches_the_fortran_over_all_three_poolstier1 equilibrium PPS194002.784e-16 at perturbed#609[8]2 at nominal#26[8]p50=0 p90=0 p99=1 p100=20:18322 1:899 2:1790 seen, 0 mismatched
tier2_equilibriumthe_equilibrium_matches_the_fortran_over_all_three_poolstier1 equilibrium PTR/PTS/PTV72753.578e-16 at perturbed#576[2]2 at nominal#188[1]p50=0 p90=0 p99=1 p100=20:7130 1:133 2:120 seen, 0 mismatched
tier2_equilibriumthe_equilibrium_matches_the_fortran_over_all_three_poolstier1 equilibrium XVR194004.843e-16 at perturbed#88[4]3 at nominal#44[7]p50=0 p90=0 p99=2 p100=30:17705 1:1305 2:368 3:220 seen, 0 mismatched
tier2_equilibriumthe_equilibrium_matches_the_fortran_over_all_three_poolstier1 equilibrium XVS194004.302e-16 at perturbed#1121[7]3 at nominal#287[3]p50=0 p90=0 p99=2 p100=30:18174 1:978 2:238 3:100 seen, 0 mismatched
tier2_equilibriumthe_equilibrium_matches_the_fortran_over_all_three_poolstier1 equilibrium UTVR/UTVS48504.443e-16 at perturbed#169[2]3 at nominal#303[2]p50=0 p90=0 p99=1 p100=30:4733 1:82 2:32 3:30 seen, 0 mismatched
tier2_equilibriumthe_equilibrium_matches_the_fortran_over_all_three_poolstier1 equilibrium UCVR194005.077e-16 at perturbed#1281[8]4 at perturbed#1281[8]p50=0 p90=0 p99=2 p100=40:18105 1:920 2:330 3:44 4:10 seen, 0 mismatched
tier2_equilibriumthe_equilibrium_matches_the_fortran_over_all_three_poolstier1 equilibrium UCVS194006.230e-16 at perturbed#169[6]3 at perturbed#169[6]p50=0 p90=0 p99=1 p100=30:18585 1:713 2:98 3:40 seen, 0 mismatched
tier2_equilibriumthe_ideal_gas_partial_pressures_are_bit_identical_under_the_vendored_libmtier1 PPR/PPS for A, B and C12000.000e0 at nominal#0[1]0 at nominal#0[1]p50=0 p90=0 p99=0 p100=00:12000 seen, 0 mismatched
tier2_equilibriumthe_ideal_gas_partial_pressures_are_bit_identical_under_the_vendored_libmtier1 PTV2000.000e0 at nominal#0[1]0 at nominal#0[1]p50=0 p90=0 p99=0 p100=00:2000 seen, 0 mismatched
tier2_kineticsthe_inert_has_no_net_production_in_either_implementationtier1 CRXR(2), never assigned2000.000e0 at nominal#0[2]0 at nominal#0[2]p50=0 p90=0 p99=0 p100=00:2000 seen, 0 mismatched
tier2_kineticsthe_kinetics_match_the_fortran_over_all_three_poolstier1 kinetics RR97008.492e-16 at perturbed#618[2]7 at perturbed#1279[1]p50=0 p90=2 p99=3 p100=70:7255 1:1447 2:732 3:235 4:22 5:7 6:1 7:10 seen, 0 mismatched
tier2_kineticsthe_kinetics_match_the_fortran_over_all_three_poolstier1 kinetics CRXR194008.492e-16 at perturbed#618[8]7 at perturbed#1279[4]p50=0 p90=2 p99=3 p100=70:14063 1:3030 2:1763 3:450 4:72 5:16 6:4 7:20 seen, 0 mismatched
tier2_kineticsthe_kinetics_match_the_fortran_over_all_three_poolstier1 kinetics RH24257.419e-16 at perturbed#293[1]6 at perturbed#293[1]p50=0 p90=2 p99=3 p100=60:1594 1:472 2:299 3:50 4:8 5:1 6:10 seen, 0 mismatched
tier2_streamsthe_stream_table_matches_the_fortran_over_all_three_poolstier1 streams XST (10 streams x 8)1940004.736e-16 at perturbed#1378[44]4 at perturbed#1923[47]p50=0 p90=0 p99=1 p100=40:189849 1:3316 2:804 3:30 4:10 seen, 0 mismatched
tier2_streamsthe_stream_table_matches_the_fortran_over_all_three_poolstier1 streams XMWS (the 6 that exist)145504.370e-16 at perturbed#791[4]3 at perturbed#431[4]p50=0 p90=0 p99=1 p100=30:14073 1:392 2:79 3:60 seen, 0 mismatched
tier2_streamsthe_stream_table_matches_the_fortran_over_all_three_poolstier1 streams TST242500.000e0 at nominal#0[1]0 at nominal#0[1]p50=0 p90=0 p99=0 p100=00:242500 seen, 0 mismatched
tier2_streamsthe_stream_table_matches_the_fortran_over_all_three_poolstier1 streams HST242505.050e-16 at perturbed#1923[6]4 at perturbed#1923[6]p50=0 p90=0 p99=1 p100=40:23683 1:461 2:94 3:10 4:20 seen, 0 mismatched
tier2_flowsthe_flow_network_matches_the_fortran_over_all_three_poolstier1 flows FTM (the 10 assembled streams)242502.246e-14 at perturbed#1790[7]125 at perturbed#1453[6]p50=0 p90=0 p99=5 p100>=160:23639 1:224 2:85 3:47 4:12 5:13 6:17 7:17 8:10 9:7 10:5 11:6 12:3 13:2 15:1 >=16:1620 seen, 0 mismatched
tier2_flowsthe_flow_network_matches_the_fortran_over_all_three_poolstier1 flows FCM (10 streams x 8)1940002.268e-14 at perturbed#1790[53]178 at perturbed#1106[43]p50=0 p90=0 p99=4 p100>=160:187622 1:2738 2:1090 3:426 4:288 5:119 6:88 7:62 8:43 9:57 10:38 11:31 12:25 13:47 14:66 15:90 >=16:11700 seen, 0 mismatched
tier2_flowsthe_flow_network_matches_the_fortran_over_all_three_poolstier1 flows FWR/FWS/AGSP72750.000e0 at nominal#0[1]0 at nominal#0[1]p50=0 p90=0 p99=0 p100=00:72750 seen, 0 mismatched
tier2_flowsthe_flow_network_matches_the_fortran_over_all_three_poolstier1 flows CPDH24252.034e-15 at perturbed#1790[1]18 at perturbed#1790[1]p50=0 p90=0 p99=2 p100>=160:2312 1:71 2:20 3:6 4:5 5:6 6:1 7:3 >=16:10 seen, 0 mismatched
tier2_flowsthe_flow_network_matches_the_fortran_over_all_three_poolstier1 flows HST(9) after the compressor bump24256.522e-15 at perturbed#1790[1]42 at perturbed#1790[1]p50=0 p90=0 p99=2 p100>=160:2253 1:133 2:24 3:10 4:1 5:1 6:1 9:1 >=16:10 seen, 0 mismatched
tier2_flowsthe_steam_coefficient_matches_through_the_condenser_dutytier1 UAC, via QUC3000.000e0 at nominal#0[1]0 at nominal#0[1]p50=0 p90=0 p99=0 p100=00:3000 seen, 0 mismatched
tier2_stripperthe_stripper_matches_the_fortran_over_all_three_poolstier1 stripper SFR194000.000e0 at nominal#0[1]0 at nominal#0[1]p50=0 p90=0 p99=0 p100=00:194000 seen, 0 mismatched
tier2_stripperthe_stripper_matches_the_fortran_over_all_three_poolstier1 stripper FTM (5, 12): the column's own outlets48500.000e0 at nominal#0[1]0 at nominal#0[1]p50=0 p90=0 p99=0 p100=00:48500 seen, 0 mismatched
tier2_stripperthe_stripper_matches_the_fortran_over_all_three_poolstier1 stripper FTM (7): the reactor-inlet alias24251.484e-15 at perturbed#817[1]12 at perturbed#591[1]p50=0 p90=0 p99=5 p100=120:2322 1:1 2:19 3:46 4:5 5:8 6:15 7:6 8:1 12:20 seen, 0 mismatched
tier2_stripperthe_stripper_matches_the_fortran_over_all_three_poolstier1 stripper FCM (5, 12): the column's own outlets388000.000e0 at nominal#0[1]0 at nominal#0[1]p50=0 p90=0 p99=0 p100=00:388000 seen, 0 mismatched
tier2_stripperthe_stripper_matches_the_fortran_over_all_three_poolstier1 stripper FCM (7): the reactor-inlet alias194001.610e-15 at perturbed#817[3]13 at perturbed#591[8]p50=0 p90=0 p99=4 p100=130:18576 1:6 2:100 3:313 4:228 5:103 6:33 7:15 8:11 9:8 10:1 11:3 12:2 13:10 seen, 0 mismatched
tier2_stripperthe_stripper_matches_the_fortran_over_all_three_poolstier1 stripper XST (5, 12): the column's own outlets388000.000e0 at nominal#0[1]0 at nominal#0[1]p50=0 p90=0 p99=0 p100=00:388000 seen, 0 mismatched
tier2_stripperthe_stripper_matches_the_fortran_over_all_three_poolstier1 stripper XST (7): the reactor-inlet alias194000.000e0 at nominal#0[1]0 at nominal#0[1]p50=0 p90=0 p99=0 p100=00:194000 seen, 0 mismatched
tier2_stripperthe_stripper_matches_the_fortran_over_all_three_poolstier1 stripper TST (5, 7, 12)72750.000e0 at nominal#0[1]0 at nominal#0[1]p50=0 p90=0 p99=0 p100=00:72750 seen, 0 mismatched
tier2_stripperthe_stripper_matches_the_fortran_over_all_three_poolstier1 stripper HST (5, 12): the column's own outlets48500.000e0 at nominal#0[1]0 at nominal#0[1]p50=0 p90=0 p99=0 p100=00:48500 seen, 0 mismatched
tier2_stripperthe_stripper_matches_the_fortran_over_all_three_poolstier1 stripper HST (7): the reactor-inlet alias24250.000e0 at nominal#0[1]0 at nominal#0[1]p50=0 p90=0 p99=0 p100=00:24250 seen, 0 mismatched
tier2_heatheat_transfer_matches_the_fortran_over_all_three_poolstier1 heat UAR24250.000e0 at nominal#0[1]0 at nominal#0[1]p50=0 p90=0 p99=0 p100=00:24250 seen, 0 mismatched
tier2_heatheat_transfer_matches_the_fortran_over_all_three_poolstier1 heat QUR24250.000e0 at nominal#0[1]0 at nominal#0[1]p50=0 p90=0 p99=0 p100=00:24250 seen, 0 mismatched
tier2_heatheat_transfer_matches_the_fortran_over_all_three_poolstier1 heat QUS24259.572e-13 at perturbed#50[1]8009 at perturbed#50[1]p50=0 p90=0 p99>=16 p100>=160:2196 1:23 2:14 3:10 4:21 5:9 6:3 7:10 8:4 9:2 10:3 11:1 12:3 13:3 14:6 15:1 >=16:1160 seen, 0 mismatched
tier2_heatheat_transfer_matches_the_fortran_over_all_three_poolstier1 heat QUC24250.000e0 at nominal#0[1]0 at nominal#0[1]p50=0 p90=0 p99=0 p100=00:24250 seen, 0 mismatched
tier2_measurementsthe_measurements_match_the_fortran_over_all_three_poolstier1 XMEAS(1..22), noise-free533508.774e-15 at perturbed#1851[5]78 at perturbed#1851[5]p50=0 p90=0 p99=1 p100>=160:52801 1:226 2:132 3:58 4:44 5:23 6:7 7:7 8:3 9:4 13:1 14:3 >=16:410 seen, 0 mismatched
tier2_measurementsthe_shutdown_detector_agrees_with_the_fortran_on_every_statetier1 ISD as 0 or 124250.000e0 at nominal#0[1]0 at nominal#0[1]p50=0 p90=0 p99=0 p100=00:24250 seen, 0 mismatched
tier2_balancesall_fifty_derivatives_match_the_fortran_over_all_three_poolstier1 YP(1..50), relative to the derivative (reported)1206001.393e-4 at nominal#303[2]738734374912 at nominal#303[2]p50=0 p90=2 p99>=16 p100>=160:107101 1:935 2:1289 3:101 4:1512 5:22 6:176 7:14 8:1354 9:19 10:35 11:7 12:166 13:4 14:21 15:11 >=16:78330 seen, 0 mismatched
tier2_balancesall_fifty_derivatives_match_the_fortran_over_all_three_poolstier1 YP(1..50), relative to the scale of the terms (the gate)1206006.093e-14 at perturbed#300[7]738734374912 at nominal#303[2]p50=0 p90=2 p99>=16 p100>=160:107101 1:935 2:1289 3:101 4:1512 5:22 6:176 7:14 8:1354 9:19 10:35 11:7 12:166 13:4 14:21 15:11 >=16:78330 seen, 0 mismatched
tier2_balancesall_fifty_derivatives_match_the_fortran_over_all_three_poolstier1 YP(1..50), plant frozen6500.000e0 at perturbed#369[1]0 at perturbed#369[1]p50=0 p90=0 p99=0 p100=00:6500 seen, 0 mismatched
tier2_balancestier2_acceptance_tabletier1 YP(1..50), relative to the scale of the terms (the gate)1206006.093e-14 at perturbed#300[7]206158430208 at nominal#296[2]p50=0 p90=2 p99>=16 p100>=160:107280 1:890 2:1293 3:113 4:1468 5:32 6:163 7:17 8:1240 9:19 10:42 11:11 12:166 13:7 14:26 15:11 >=16:78220 seen, 0 mismatched
tier4_closed_loopthe_closed_loop_plant_matches_the_fortran_driverclosed loop, platform libm, 10 h (36000 steps)0.000e0 at XMEAS(0)0.000e0 %range at XMV(0)10.000 h of 10, ending at 0.000e0 x XNS(0)
tier4_closed_loopthe_forced_disturbance_changes_the_plant_measurablyD-011, 10 h, faithful against fixed:Some((29390, 22))Some((29390, 22))1.092e-1 at XMEAS(37)3.262e-2 at XMEAS(38)
tier2_equilibriumthe_algebra_is_bit_identical_once_exp_agreestier1 equilibrium VVR/VVS14500.000e0 at nominal#0[1]0 at nominal#0[1]p50=0 p90=0 p99=0 p100=00:14500 seen, 0 mismatched
tier2_equilibriumthe_algebra_is_bit_identical_once_exp_agreestier1 equilibrium PPR58000.000e0 at nominal#0[1]0 at nominal#0[1]p50=0 p90=0 p99=0 p100=00:58000 seen, 0 mismatched
tier2_equilibriumthe_algebra_is_bit_identical_once_exp_agreestier1 equilibrium PPS58000.000e0 at nominal#0[1]0 at nominal#0[1]p50=0 p90=0 p99=0 p100=00:58000 seen, 0 mismatched
tier2_equilibriumthe_algebra_is_bit_identical_once_exp_agreestier1 equilibrium PTR/PTS/PTV21750.000e0 at nominal#0[1]0 at nominal#0[1]p50=0 p90=0 p99=0 p100=00:21750 seen, 0 mismatched
tier2_equilibriumthe_algebra_is_bit_identical_once_exp_agreestier1 equilibrium XVR58000.000e0 at nominal#0[1]0 at nominal#0[1]p50=0 p90=0 p99=0 p100=00:58000 seen, 0 mismatched
tier2_equilibriumthe_algebra_is_bit_identical_once_exp_agreestier1 equilibrium XVS58000.000e0 at nominal#0[1]0 at nominal#0[1]p50=0 p90=0 p99=0 p100=00:58000 seen, 0 mismatched
tier2_equilibriumthe_algebra_is_bit_identical_once_exp_agreestier1 equilibrium UTVR/UTVS14500.000e0 at nominal#0[1]0 at nominal#0[1]p50=0 p90=0 p99=0 p100=00:14500 seen, 0 mismatched
tier2_equilibriumthe_algebra_is_bit_identical_once_exp_agreestier1 equilibrium UCVR58000.000e0 at nominal#0[1]0 at nominal#0[1]p50=0 p90=0 p99=0 p100=00:58000 seen, 0 mismatched
tier2_equilibriumthe_algebra_is_bit_identical_once_exp_agreestier1 equilibrium UCVS58000.000e0 at nominal#0[1]0 at nominal#0[1]p50=0 p90=0 p99=0 p100=00:58000 seen, 0 mismatched
tier2_equilibriumthe_equilibrium_matches_the_fortran_over_all_three_poolstier1 equilibrium VVR/VVS48500.000e0 at nominal#0[1]0 at nominal#0[1]p50=0 p90=0 p99=0 p100=00:48500 seen, 0 mismatched
tier2_equilibriumthe_equilibrium_matches_the_fortran_over_all_three_poolstier1 equilibrium PPR194000.000e0 at nominal#0[1]0 at nominal#0[1]p50=0 p90=0 p99=0 p100=00:194000 seen, 0 mismatched
tier2_equilibriumthe_equilibrium_matches_the_fortran_over_all_three_poolstier1 equilibrium PPS194000.000e0 at nominal#0[1]0 at nominal#0[1]p50=0 p90=0 p99=0 p100=00:194000 seen, 0 mismatched
tier2_equilibriumthe_equilibrium_matches_the_fortran_over_all_three_poolstier1 equilibrium PTR/PTS/PTV72750.000e0 at nominal#0[1]0 at nominal#0[1]p50=0 p90=0 p99=0 p100=00:72750 seen, 0 mismatched
tier2_equilibriumthe_equilibrium_matches_the_fortran_over_all_three_poolstier1 equilibrium XVR194000.000e0 at nominal#0[1]0 at nominal#0[1]p50=0 p90=0 p99=0 p100=00:194000 seen, 0 mismatched
tier2_equilibriumthe_equilibrium_matches_the_fortran_over_all_three_poolstier1 equilibrium XVS194000.000e0 at nominal#0[1]0 at nominal#0[1]p50=0 p90=0 p99=0 p100=00:194000 seen, 0 mismatched
tier2_equilibriumthe_equilibrium_matches_the_fortran_over_all_three_poolstier1 equilibrium UTVR/UTVS48500.000e0 at nominal#0[1]0 at nominal#0[1]p50=0 p90=0 p99=0 p100=00:48500 seen, 0 mismatched
tier2_equilibriumthe_equilibrium_matches_the_fortran_over_all_three_poolstier1 equilibrium UCVR194000.000e0 at nominal#0[1]0 at nominal#0[1]p50=0 p90=0 p99=0 p100=00:194000 seen, 0 mismatched
tier2_equilibriumthe_equilibrium_matches_the_fortran_over_all_three_poolstier1 equilibrium UCVS194000.000e0 at nominal#0[1]0 at nominal#0[1]p50=0 p90=0 p99=0 p100=00:194000 seen, 0 mismatched
tier2_kineticsthe_algebra_is_bit_identical_once_exp_and_pow_agreetier1 kinetics RR29000.000e0 at nominal#0[1]0 at nominal#0[1]p50=0 p90=0 p99=0 p100=00:29000 seen, 0 mismatched
tier2_kineticsthe_algebra_is_bit_identical_once_exp_and_pow_agreetier1 kinetics CRXR58000.000e0 at nominal#0[1]0 at nominal#0[1]p50=0 p90=0 p99=0 p100=00:58000 seen, 0 mismatched
tier2_kineticsthe_algebra_is_bit_identical_once_exp_and_pow_agreetier1 kinetics RH7250.000e0 at nominal#0[1]0 at nominal#0[1]p50=0 p90=0 p99=0 p100=00:7250 seen, 0 mismatched
tier2_kineticsthe_inert_has_no_net_production_in_either_implementationtier1 CRXR(2), never assigned2000.000e0 at nominal#0[2]0 at nominal#0[2]p50=0 p90=0 p99=0 p100=00:2000 seen, 0 mismatched
tier2_kineticsthe_kinetics_match_the_fortran_over_all_three_poolstier1 kinetics RR97000.000e0 at nominal#0[1]0 at nominal#0[1]p50=0 p90=0 p99=0 p100=00:97000 seen, 0 mismatched
tier2_kineticsthe_kinetics_match_the_fortran_over_all_three_poolstier1 kinetics CRXR194000.000e0 at nominal#0[1]0 at nominal#0[1]p50=0 p90=0 p99=0 p100=00:194000 seen, 0 mismatched
tier2_kineticsthe_kinetics_match_the_fortran_over_all_three_poolstier1 kinetics RH24250.000e0 at nominal#0[1]0 at nominal#0[1]p50=0 p90=0 p99=0 p100=00:24250 seen, 0 mismatched
tier2_streamsthe_algebra_is_bit_identical_once_exp_and_pow_agreetier1 streams XST (10 streams x 8)580000.000e0 at nominal#0[1]0 at nominal#0[1]p50=0 p90=0 p99=0 p100=00:580000 seen, 0 mismatched
tier2_streamsthe_algebra_is_bit_identical_once_exp_and_pow_agreetier1 streams XMWS (the 6 that exist)43500.000e0 at nominal#0[1]0 at nominal#0[1]p50=0 p90=0 p99=0 p100=00:43500 seen, 0 mismatched
tier2_streamsthe_algebra_is_bit_identical_once_exp_and_pow_agreetier1 streams TST72500.000e0 at nominal#0[1]0 at nominal#0[1]p50=0 p90=0 p99=0 p100=00:72500 seen, 0 mismatched
tier2_streamsthe_algebra_is_bit_identical_once_exp_and_pow_agreetier1 streams HST72500.000e0 at nominal#0[1]0 at nominal#0[1]p50=0 p90=0 p99=0 p100=00:72500 seen, 0 mismatched
tier2_streamsthe_stream_table_matches_the_fortran_over_all_three_poolstier1 streams XST (10 streams x 8)1940000.000e0 at nominal#0[1]0 at nominal#0[1]p50=0 p90=0 p99=0 p100=00:1940000 seen, 0 mismatched
tier2_streamsthe_stream_table_matches_the_fortran_over_all_three_poolstier1 streams XMWS (the 6 that exist)145500.000e0 at nominal#0[1]0 at nominal#0[1]p50=0 p90=0 p99=0 p100=00:145500 seen, 0 mismatched
tier2_streamsthe_stream_table_matches_the_fortran_over_all_three_poolstier1 streams TST242500.000e0 at nominal#0[1]0 at nominal#0[1]p50=0 p90=0 p99=0 p100=00:242500 seen, 0 mismatched
tier2_streamsthe_stream_table_matches_the_fortran_over_all_three_poolstier1 streams HST242500.000e0 at nominal#0[1]0 at nominal#0[1]p50=0 p90=0 p99=0 p100=00:242500 seen, 0 mismatched
tier2_flowsthe_algebra_is_bit_identical_once_exp_and_pow_agreetier1 flows FTM (the 10 assembled streams)72500.000e0 at nominal#0[1]0 at nominal#0[1]p50=0 p90=0 p99=0 p100=00:72500 seen, 0 mismatched
tier2_flowsthe_algebra_is_bit_identical_once_exp_and_pow_agreetier1 flows FCM (10 streams x 8)580000.000e0 at nominal#0[1]0 at nominal#0[1]p50=0 p90=0 p99=0 p100=00:580000 seen, 0 mismatched
tier2_flowsthe_algebra_is_bit_identical_once_exp_and_pow_agreetier1 flows FWR/FWS/AGSP21750.000e0 at nominal#0[1]0 at nominal#0[1]p50=0 p90=0 p99=0 p100=00:21750 seen, 0 mismatched
tier2_flowsthe_algebra_is_bit_identical_once_exp_and_pow_agreetier1 flows CPDH7250.000e0 at nominal#0[1]0 at nominal#0[1]p50=0 p90=0 p99=0 p100=00:7250 seen, 0 mismatched
tier2_flowsthe_algebra_is_bit_identical_once_exp_and_pow_agreetier1 flows HST(9) after the compressor bump7250.000e0 at nominal#0[1]0 at nominal#0[1]p50=0 p90=0 p99=0 p100=00:7250 seen, 0 mismatched
tier2_flowsthe_flow_network_matches_the_fortran_over_all_three_poolstier1 flows FTM (the 10 assembled streams)242500.000e0 at nominal#0[1]0 at nominal#0[1]p50=0 p90=0 p99=0 p100=00:242500 seen, 0 mismatched
tier2_flowsthe_flow_network_matches_the_fortran_over_all_three_poolstier1 flows FCM (10 streams x 8)1940000.000e0 at nominal#0[1]0 at nominal#0[1]p50=0 p90=0 p99=0 p100=00:1940000 seen, 0 mismatched
tier2_flowsthe_flow_network_matches_the_fortran_over_all_three_poolstier1 flows FWR/FWS/AGSP72750.000e0 at nominal#0[1]0 at nominal#0[1]p50=0 p90=0 p99=0 p100=00:72750 seen, 0 mismatched
tier2_flowsthe_flow_network_matches_the_fortran_over_all_three_poolstier1 flows CPDH24250.000e0 at nominal#0[1]0 at nominal#0[1]p50=0 p90=0 p99=0 p100=00:24250 seen, 0 mismatched
tier2_flowsthe_flow_network_matches_the_fortran_over_all_three_poolstier1 flows HST(9) after the compressor bump24250.000e0 at nominal#0[1]0 at nominal#0[1]p50=0 p90=0 p99=0 p100=00:24250 seen, 0 mismatched
tier2_flowsthe_steam_coefficient_matches_through_the_condenser_dutytier1 UAC, via QUC3000.000e0 at nominal#0[1]0 at nominal#0[1]p50=0 p90=0 p99=0 p100=00:3000 seen, 0 mismatched
tier2_stripperthe_algebra_is_bit_identical_once_exp_and_pow_agreetier1 stripper SFR58000.000e0 at nominal#0[1]0 at nominal#0[1]p50=0 p90=0 p99=0 p100=00:58000 seen, 0 mismatched
tier2_stripperthe_algebra_is_bit_identical_once_exp_and_pow_agreetier1 stripper FTM (5, 12): the column's own outlets14500.000e0 at nominal#0[1]0 at nominal#0[1]p50=0 p90=0 p99=0 p100=00:14500 seen, 0 mismatched
tier2_stripperthe_algebra_is_bit_identical_once_exp_and_pow_agreetier1 stripper FTM (7): the reactor-inlet alias7250.000e0 at nominal#0[1]0 at nominal#0[1]p50=0 p90=0 p99=0 p100=00:7250 seen, 0 mismatched
tier2_stripperthe_algebra_is_bit_identical_once_exp_and_pow_agreetier1 stripper FCM (5, 12): the column's own outlets116000.000e0 at nominal#0[1]0 at nominal#0[1]p50=0 p90=0 p99=0 p100=00:116000 seen, 0 mismatched
tier2_stripperthe_algebra_is_bit_identical_once_exp_and_pow_agreetier1 stripper FCM (7): the reactor-inlet alias58000.000e0 at nominal#0[1]0 at nominal#0[1]p50=0 p90=0 p99=0 p100=00:58000 seen, 0 mismatched
tier2_stripperthe_algebra_is_bit_identical_once_exp_and_pow_agreetier1 stripper XST (5, 12): the column's own outlets116000.000e0 at nominal#0[1]0 at nominal#0[1]p50=0 p90=0 p99=0 p100=00:116000 seen, 0 mismatched
tier2_stripperthe_algebra_is_bit_identical_once_exp_and_pow_agreetier1 stripper XST (7): the reactor-inlet alias58000.000e0 at nominal#0[1]0 at nominal#0[1]p50=0 p90=0 p99=0 p100=00:58000 seen, 0 mismatched
tier2_stripperthe_algebra_is_bit_identical_once_exp_and_pow_agreetier1 stripper TST (5, 7, 12)21750.000e0 at nominal#0[1]0 at nominal#0[1]p50=0 p90=0 p99=0 p100=00:21750 seen, 0 mismatched
tier2_stripperthe_algebra_is_bit_identical_once_exp_and_pow_agreetier1 stripper HST (5, 12): the column's own outlets14500.000e0 at nominal#0[1]0 at nominal#0[1]p50=0 p90=0 p99=0 p100=00:14500 seen, 0 mismatched
tier2_stripperthe_algebra_is_bit_identical_once_exp_and_pow_agreetier1 stripper HST (7): the reactor-inlet alias7250.000e0 at nominal#0[1]0 at nominal#0[1]p50=0 p90=0 p99=0 p100=00:7250 seen, 0 mismatched
tier2_stripperthe_stripper_matches_the_fortran_over_all_three_poolstier1 stripper SFR194000.000e0 at nominal#0[1]0 at nominal#0[1]p50=0 p90=0 p99=0 p100=00:194000 seen, 0 mismatched
tier2_stripperthe_stripper_matches_the_fortran_over_all_three_poolstier1 stripper FTM (5, 12): the column's own outlets48500.000e0 at nominal#0[1]0 at nominal#0[1]p50=0 p90=0 p99=0 p100=00:48500 seen, 0 mismatched
tier2_stripperthe_stripper_matches_the_fortran_over_all_three_poolstier1 stripper FTM (7): the reactor-inlet alias24250.000e0 at nominal#0[1]0 at nominal#0[1]p50=0 p90=0 p99=0 p100=00:24250 seen, 0 mismatched
tier2_stripperthe_stripper_matches_the_fortran_over_all_three_poolstier1 stripper FCM (5, 12): the column's own outlets388000.000e0 at nominal#0[1]0 at nominal#0[1]p50=0 p90=0 p99=0 p100=00:388000 seen, 0 mismatched
tier2_stripperthe_stripper_matches_the_fortran_over_all_three_poolstier1 stripper FCM (7): the reactor-inlet alias194000.000e0 at nominal#0[1]0 at nominal#0[1]p50=0 p90=0 p99=0 p100=00:194000 seen, 0 mismatched
tier2_stripperthe_stripper_matches_the_fortran_over_all_three_poolstier1 stripper XST (5, 12): the column's own outlets388000.000e0 at nominal#0[1]0 at nominal#0[1]p50=0 p90=0 p99=0 p100=00:388000 seen, 0 mismatched
tier2_stripperthe_stripper_matches_the_fortran_over_all_three_poolstier1 stripper XST (7): the reactor-inlet alias194000.000e0 at nominal#0[1]0 at nominal#0[1]p50=0 p90=0 p99=0 p100=00:194000 seen, 0 mismatched
tier2_stripperthe_stripper_matches_the_fortran_over_all_three_poolstier1 stripper TST (5, 7, 12)72750.000e0 at nominal#0[1]0 at nominal#0[1]p50=0 p90=0 p99=0 p100=00:72750 seen, 0 mismatched
tier2_stripperthe_stripper_matches_the_fortran_over_all_three_poolstier1 stripper HST (5, 12): the column's own outlets48500.000e0 at nominal#0[1]0 at nominal#0[1]p50=0 p90=0 p99=0 p100=00:48500 seen, 0 mismatched
tier2_stripperthe_stripper_matches_the_fortran_over_all_three_poolstier1 stripper HST (7): the reactor-inlet alias24250.000e0 at nominal#0[1]0 at nominal#0[1]p50=0 p90=0 p99=0 p100=00:24250 seen, 0 mismatched
tier2_heatheat_transfer_matches_the_fortran_over_all_three_poolstier1 heat UAR24250.000e0 at nominal#0[1]0 at nominal#0[1]p50=0 p90=0 p99=0 p100=00:24250 seen, 0 mismatched
tier2_heatheat_transfer_matches_the_fortran_over_all_three_poolstier1 heat QUR24250.000e0 at nominal#0[1]0 at nominal#0[1]p50=0 p90=0 p99=0 p100=00:24250 seen, 0 mismatched
tier2_heatheat_transfer_matches_the_fortran_over_all_three_poolstier1 heat QUS24250.000e0 at nominal#0[1]0 at nominal#0[1]p50=0 p90=0 p99=0 p100=00:24250 seen, 0 mismatched
tier2_heatheat_transfer_matches_the_fortran_over_all_three_poolstier1 heat QUC24250.000e0 at nominal#0[1]0 at nominal#0[1]p50=0 p90=0 p99=0 p100=00:24250 seen, 0 mismatched
tier2_heatthe_algebra_is_bit_identical_once_exp_and_pow_agreetier1 heat UAR7250.000e0 at nominal#0[1]0 at nominal#0[1]p50=0 p90=0 p99=0 p100=00:7250 seen, 0 mismatched
tier2_heatthe_algebra_is_bit_identical_once_exp_and_pow_agreetier1 heat QUR7250.000e0 at nominal#0[1]0 at nominal#0[1]p50=0 p90=0 p99=0 p100=00:7250 seen, 0 mismatched
tier2_heatthe_algebra_is_bit_identical_once_exp_and_pow_agreetier1 heat QUS7250.000e0 at nominal#0[1]0 at nominal#0[1]p50=0 p90=0 p99=0 p100=00:7250 seen, 0 mismatched
tier2_heatthe_algebra_is_bit_identical_once_exp_and_pow_agreetier1 heat QUC7250.000e0 at nominal#0[1]0 at nominal#0[1]p50=0 p90=0 p99=0 p100=00:7250 seen, 0 mismatched
tier2_measurementsthe_algebra_is_bit_identical_once_exp_and_pow_agreetier1 XMEAS(1..22), noise-free159500.000e0 at nominal#0[1]0 at nominal#0[1]p50=0 p90=0 p99=0 p100=00:159500 seen, 0 mismatched
tier2_measurementsthe_measurements_match_the_fortran_over_all_three_poolstier1 XMEAS(1..22), noise-free533500.000e0 at nominal#0[1]0 at nominal#0[1]p50=0 p90=0 p99=0 p100=00:533500 seen, 0 mismatched
tier2_measurementsthe_shutdown_detector_agrees_with_the_fortran_on_every_statetier1 ISD as 0 or 124250.000e0 at nominal#0[1]0 at nominal#0[1]p50=0 p90=0 p99=0 p100=00:24250 seen, 0 mismatched
tier2_balancesall_fifty_derivatives_match_the_fortran_over_all_three_poolstier1 YP(1..50), relative to the derivative (reported)1206000.000e0 at nominal#0[1]0 at nominal#0[1]p50=0 p90=0 p99=0 p100=00:1206000 seen, 0 mismatched
tier2_balancesall_fifty_derivatives_match_the_fortran_over_all_three_poolstier1 YP(1..50), relative to the scale of the terms (the gate)1206000.000e0 at nominal#0[1]0 at nominal#0[1]p50=0 p90=0 p99=0 p100=00:1206000 seen, 0 mismatched
tier2_balancesall_fifty_derivatives_match_the_fortran_over_all_three_poolstier1 YP(1..50), plant frozen6500.000e0 at perturbed#369[1]0 at perturbed#369[1]p50=0 p90=0 p99=0 p100=00:6500 seen, 0 mismatched
tier2_balancesthe_whole_right_hand_side_is_bit_identical_once_exp_and_pow_agreetier1 YP(1..50), relative to the derivative (reported)358500.000e0 at nominal#0[1]0 at nominal#0[1]p50=0 p90=0 p99=0 p100=00:358500 seen, 0 mismatched
tier2_balancestier2_acceptance_tabletier1 YP(1..50), relative to the scale of the terms (the gate)1206000.000e0 at nominal#0[1]0 at nominal#0[1]p50=0 p90=0 p99=0 p100=00:1206000 seen, 0 mismatched

Transcripts

Each block below is a test binary's own output, verbatim, with the command that produced it. The summary above is derived from these; they are not derived from it.

tier2_unpack, vendored libm

cargo test -p tepsim-oracle --features oracle --release --test tier2_unpack -- --nocapture --test-threads 1
running 3 tests
test solving_from_a_fixed_guess_instead_of_the_seed_breaks_bit_equality ... tier1 TCV with the carried seed
  cases          : 100
  max rel err    : 0.000e0 at nominal#0[4]
  max ulp        : 0 at nominal#0[4]
  ulp percentiles: p50=0 p90=0 p99=0 p100=0
  ulp histogram  : 0:100
  non-finite     : 0 seen, 0 mismatched
tier1 TCV from a fixed guess
  cases          : 100
  max rel err    : 9.901e-16 at nominal#25[4]
  max ulp        : 6 at nominal#25[4]
  ulp percentiles: p50=1 p90=4 p99=6 p100=6
  ulp histogram  : 0:24 1:28 2:18 3:19 4:7 5:1 6:3
  non-finite     : 0 seen, 0 mismatched
ok
test the_unpacking_is_bit_identical_to_the_fortran ... ok
test the_unpacking_matches_the_fortran_over_all_three_pools ... tier1 unpack UCLR
  cases          : 19400
  max rel err    : 0.000e0 at nominal#0[1]
  max ulp        : 0 at nominal#0[1]
  ulp percentiles: p50=0 p90=0 p99=0 p100=0
  ulp histogram  : 0:19400
  non-finite     : 0 seen, 0 mismatched
tier1 unpack UCLS
  cases          : 19400
  max rel err    : 0.000e0 at nominal#0[1]
  max ulp        : 0 at nominal#0[1]
  ulp percentiles: p50=0 p90=0 p99=0 p100=0
  ulp histogram  : 0:19400
  non-finite     : 0 seen, 0 mismatched
tier1 unpack UCLC
  cases          : 19400
  max rel err    : 0.000e0 at nominal#0[1]
  max ulp        : 0 at nominal#0[1]
  ulp percentiles: p50=0 p90=0 p99=0 p100=0
  ulp histogram  : 0:19400
  non-finite     : 0 seen, 0 mismatched
tier1 unpack UCVV
  cases          : 19400
  max rel err    : 0.000e0 at nominal#0[1]
  max ulp        : 0 at nominal#0[1]
  ulp percentiles: p50=0 p90=0 p99=0 p100=0
  ulp histogram  : 0:19400
  non-finite     : 0 seen, 0 mismatched
tier1 unpack UTLR/UTLS/UTLC/UTVV
  cases          : 9700
  max rel err    : 0.000e0 at nominal#0[1]
  max ulp        : 0 at nominal#0[1]
  ulp percentiles: p50=0 p90=0 p99=0 p100=0
  ulp histogram  : 0:9700
  non-finite     : 0 seen, 0 mismatched
tier1 unpack XLR
  cases          : 19400
  max rel err    : 0.000e0 at nominal#0[1]
  max ulp        : 0 at nominal#0[1]
  ulp percentiles: p50=0 p90=0 p99=0 p100=0
  ulp histogram  : 0:19400
  non-finite     : 0 seen, 0 mismatched
tier1 unpack XLS
  cases          : 19400
  max rel err    : 0.000e0 at nominal#0[1]
  max ulp        : 0 at nominal#0[1]
  ulp percentiles: p50=0 p90=0 p99=0 p100=0
  ulp histogram  : 0:19400
  non-finite     : 0 seen, 0 mismatched
tier1 unpack XLC
  cases          : 19400
  max rel err    : 0.000e0 at nominal#0[1]
  max ulp        : 0 at nominal#0[1]
  ulp percentiles: p50=0 p90=0 p99=0 p100=0
  ulp histogram  : 0:19400
  non-finite     : 0 seen, 0 mismatched
tier1 unpack XVV
  cases          : 19400
  max rel err    : 0.000e0 at nominal#0[1]
  max ulp        : 0 at nominal#0[1]
  ulp percentiles: p50=0 p90=0 p99=0 p100=0
  ulp histogram  : 0:19400
  non-finite     : 0 seen, 0 mismatched
tier1 unpack ESR/ESS/ESC/ESV
  cases          : 9700
  max rel err    : 0.000e0 at nominal#0[1]
  max ulp        : 0 at nominal#0[1]
  ulp percentiles: p50=0 p90=0 p99=0 p100=0
  ulp histogram  : 0:9700
  non-finite     : 0 seen, 0 mismatched
tier1 unpack TCR/TCS/TCC/TCV
  cases          : 9700
  max rel err    : 0.000e0 at nominal#0[1]
  max ulp        : 0 at nominal#0[1]
  ulp percentiles: p50=0 p90=0 p99=0 p100=0
  ulp histogram  : 0:9700
  non-finite     : 0 seen, 0 mismatched
tier1 unpack TKR/TKS/TKV
  cases          : 7275
  max rel err    : 0.000e0 at nominal#0[1]
  max ulp        : 0 at nominal#0[1]
  ulp percentiles: p50=0 p90=0 p99=0 p100=0
  ulp histogram  : 0:7275
  non-finite     : 0 seen, 0 mismatched
tier1 unpack DLR/DLS/DLC
  cases          : 7275
  max rel err    : 0.000e0 at nominal#0[1]
  max ulp        : 0 at nominal#0[1]
  ulp percentiles: p50=0 p90=0 p99=0 p100=0
  ulp histogram  : 0:7275
  non-finite     : 0 seen, 0 mismatched
tier1 unpack VLR/VLS/VLC
  cases          : 7275
  max rel err    : 0.000e0 at nominal#0[1]
  max ulp        : 0 at nominal#0[1]
  ulp percentiles: p50=0 p90=0 p99=0 p100=0
  ulp histogram  : 0:7275
  non-finite     : 0 seen, 0 mismatched
states skipped because the port would not converge: 0
ok

test result: ok. 3 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.11s

tier2_equilibrium, vendored libm

cargo test -p tepsim-oracle --features oracle --release --test tier2_equilibrium -- --nocapture --test-threads 1
running 3 tests
test the_equilibrium_matches_the_fortran_over_all_three_pools ... exp comes from the vendored libm
tier1 equilibrium VVR/VVS
  cases          : 4850
  max rel err    : 0.000e0 at nominal#0[1]
  max ulp        : 0 at nominal#0[1]
  ulp percentiles: p50=0 p90=0 p99=0 p100=0
  ulp histogram  : 0:4850
  non-finite     : 0 seen, 0 mismatched
tier1 equilibrium PPR
  cases          : 19400
  max rel err    : 2.802e-16 at perturbed#1264[4]
  max ulp        : 2 at nominal#21[4]
  ulp percentiles: p50=0 p90=0 p99=1 p100=2
  ulp histogram  : 0:17917 1:1305 2:178
  non-finite     : 0 seen, 0 mismatched
tier1 equilibrium PPS
  cases          : 19400
  max rel err    : 2.784e-16 at perturbed#609[8]
  max ulp        : 2 at nominal#26[8]
  ulp percentiles: p50=0 p90=0 p99=1 p100=2
  ulp histogram  : 0:18322 1:899 2:179
  non-finite     : 0 seen, 0 mismatched
tier1 equilibrium PTR/PTS/PTV
  cases          : 7275
  max rel err    : 3.578e-16 at perturbed#576[2]
  max ulp        : 2 at nominal#188[1]
  ulp percentiles: p50=0 p90=0 p99=1 p100=2
  ulp histogram  : 0:7130 1:133 2:12
  non-finite     : 0 seen, 0 mismatched
tier1 equilibrium XVR
  cases          : 19400
  max rel err    : 4.843e-16 at perturbed#88[4]
  max ulp        : 3 at nominal#44[7]
  ulp percentiles: p50=0 p90=0 p99=2 p100=3
  ulp histogram  : 0:17705 1:1305 2:368 3:22
  non-finite     : 0 seen, 0 mismatched
tier1 equilibrium XVS
  cases          : 19400
  max rel err    : 4.302e-16 at perturbed#1121[7]
  max ulp        : 3 at nominal#287[3]
  ulp percentiles: p50=0 p90=0 p99=2 p100=3
  ulp histogram  : 0:18174 1:978 2:238 3:10
  non-finite     : 0 seen, 0 mismatched
tier1 equilibrium UTVR/UTVS
  cases          : 4850
  max rel err    : 4.443e-16 at perturbed#169[2]
  max ulp        : 3 at nominal#303[2]
  ulp percentiles: p50=0 p90=0 p99=1 p100=3
  ulp histogram  : 0:4733 1:82 2:32 3:3
  non-finite     : 0 seen, 0 mismatched
tier1 equilibrium UCVR
  cases          : 19400
  max rel err    : 5.077e-16 at perturbed#1281[8]
  max ulp        : 4 at perturbed#1281[8]
  ulp percentiles: p50=0 p90=0 p99=2 p100=4
  ulp histogram  : 0:18105 1:920 2:330 3:44 4:1
  non-finite     : 0 seen, 0 mismatched
tier1 equilibrium UCVS
  cases          : 19400
  max rel err    : 6.230e-16 at perturbed#169[6]
  max ulp        : 3 at perturbed#169[6]
  ulp percentiles: p50=0 p90=0 p99=1 p100=3
  ulp histogram  : 0:18585 1:713 2:98 3:4
  non-finite     : 0 seen, 0 mismatched
states skipped because the port would not converge: 0
ok
test the_ideal_gas_partial_pressures_are_bit_identical_under_the_vendored_libm ... tier1 PPR/PPS for A, B and C
  cases          : 1200
  max rel err    : 0.000e0 at nominal#0[1]
  max ulp        : 0 at nominal#0[1]
  ulp percentiles: p50=0 p90=0 p99=0 p100=0
  ulp histogram  : 0:1200
  non-finite     : 0 seen, 0 mismatched
tier1 PTV
  cases          : 200
  max rel err    : 0.000e0 at nominal#0[1]
  max ulp        : 0 at nominal#0[1]
  ulp percentiles: p50=0 p90=0 p99=0 p100=0
  ulp histogram  : 0:200
  non-finite     : 0 seen, 0 mismatched
ok
test the_vendored_and_platform_exp_differ_only_by_rounding ... exp over the Antoine range [0.5868, 13.0825]: 15000 arguments, 1507 differ (10.047%), worst -1 ulp
ok

test result: ok. 3 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.09s

tier2_kinetics, vendored libm

cargo test -p tepsim-oracle --features oracle --release --test tier2_kinetics -- --nocapture --test-threads 1
running 3 tests
test the_inert_has_no_net_production_in_either_implementation ... tier1 CRXR(2), never assigned
  cases          : 200
  max rel err    : 0.000e0 at nominal#0[2]
  max ulp        : 0 at nominal#0[2]
  ulp percentiles: p50=0 p90=0 p99=0 p100=0
  ulp histogram  : 0:200
  non-finite     : 0 seen, 0 mismatched
ok
test the_kinetics_match_the_fortran_over_all_three_pools ... exp and pow come from the vendored libm
tier1 kinetics RR
  cases          : 9700
  max rel err    : 8.492e-16 at perturbed#618[2]
  max ulp        : 7 at perturbed#1279[1]
  ulp percentiles: p50=0 p90=2 p99=3 p100=7
  ulp histogram  : 0:7255 1:1447 2:732 3:235 4:22 5:7 6:1 7:1
  non-finite     : 0 seen, 0 mismatched
tier1 kinetics CRXR
  cases          : 19400
  max rel err    : 8.492e-16 at perturbed#618[8]
  max ulp        : 7 at perturbed#1279[4]
  ulp percentiles: p50=0 p90=2 p99=3 p100=7
  ulp histogram  : 0:14063 1:3030 2:1763 3:450 4:72 5:16 6:4 7:2
  non-finite     : 0 seen, 0 mismatched
tier1 kinetics RH
  cases          : 2425
  max rel err    : 7.419e-16 at perturbed#293[1]
  max ulp        : 6 at perturbed#293[1]
  ulp percentiles: p50=0 p90=2 p99=3 p100=6
  ulp histogram  : 0:1594 1:472 2:299 3:50 4:8 5:1 6:1
  non-finite     : 0 seen, 0 mismatched
states skipped because the port would not converge: 0
ok
test the_vendored_and_platform_pow_differ_only_by_rounding ... pow over [0.25, 5000] at orders 1.1544 and 0.3735: 40000 cases, 3992 differ (9.980%), worst -1 ulp
ok

test result: ok. 3 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.09s

tier2_streams, vendored libm

cargo test -p tepsim-oracle --features oracle --release --test tier2_streams -- --nocapture --test-threads 1
running 3 tests
test the_compressor_makes_the_recycle_enthalpy_differ_from_the_purge ... HST(9) differs from HST(10) on 200 of 200 nominal states
ok
test the_stream_table_matches_the_fortran_over_all_three_pools ... exp and pow come from the vendored libm
tier1 streams XST (10 streams x 8)
  cases          : 194000
  max rel err    : 4.736e-16 at perturbed#1378[44]
  max ulp        : 4 at perturbed#1923[47]
  ulp percentiles: p50=0 p90=0 p99=1 p100=4
  ulp histogram  : 0:189849 1:3316 2:804 3:30 4:1
  non-finite     : 0 seen, 0 mismatched
tier1 streams XMWS (the 6 that exist)
  cases          : 14550
  max rel err    : 4.370e-16 at perturbed#791[4]
  max ulp        : 3 at perturbed#431[4]
  ulp percentiles: p50=0 p90=0 p99=1 p100=3
  ulp histogram  : 0:14073 1:392 2:79 3:6
  non-finite     : 0 seen, 0 mismatched
tier1 streams TST
  cases          : 24250
  max rel err    : 0.000e0 at nominal#0[1]
  max ulp        : 0 at nominal#0[1]
  ulp percentiles: p50=0 p90=0 p99=0 p100=0
  ulp histogram  : 0:24250
  non-finite     : 0 seen, 0 mismatched
tier1 streams HST
  cases          : 24250
  max rel err    : 5.050e-16 at perturbed#1923[6]
  max ulp        : 4 at perturbed#1923[6]
  ulp percentiles: p50=0 p90=0 p99=1 p100=4
  ulp histogram  : 0:23683 1:461 2:94 3:10 4:2
  non-finite     : 0 seen, 0 mismatched
states skipped because the port would not converge: 0
ok
test the_unweighed_streams_are_zero_in_both_implementations ... 7 unweighed streams, all zero over 200 states
ok

test result: ok. 3 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.10s

tier2_flows, vendored libm

cargo test -p tepsim-oracle --features oracle --release --test tier2_flows -- --nocapture --test-threads 1
running 5 tests
test component_flows_sum_to_the_stream_total ... ok
test no_sampled_state_reaches_the_purge_clamp ... lowest PTS over the whole pool: 19153.09 mmHg against a 760 threshold
ok
test the_compressor_ratio_clamps_are_exercised_by_the_adversarial_pool ... VLR at the 10% heat-transfer breakpoint              None
VLR at the 50% heat-transfer breakpoint              None
TCC at the lower stripping-factor branch             None
TCC below the lower stripping-factor branch          None
TCC at the upper stripping-factor branch             None
TCC approaching the 177 C pole                       None
TCR at the shutdown limit                            None
TCR above the shutdown limit                         None
FTM(11) at the stripping-factor threshold            None
VLR at the upper shutdown limit                      None
VLR at the lower shutdown limit                      None
VLS at the upper shutdown limit                      None
VLS at the lower shutdown limit                      None
VLC at the upper shutdown limit                      None
VLC at the lower shutdown limit                      None
PTR at the reactor pressure shutdown limit           None
PTV = PTR, the mixing-to-reactor flow clamp          None
PTR = PTS, the reactor-to-separator flow clamp       None
PTV = PTS, the recycle flow clamp                    Low
PR = 1, the compressor reverse-flow clamp            Low
PR = CPPRMX, the compressor maximum-ratio clamp      None
PR above CPPRMX, inside the clamped region           High
VLR below the lower shutdown limit                   None
VLS below the lower shutdown limit                   None
VLC below the lower shutdown limit                   None
ok
test the_flow_network_matches_the_fortran_over_all_three_pools ... exp, pow and sqrt come from the vendored libm
tier1 flows FTM (the 10 assembled streams)
  cases          : 24250
  max rel err    : 2.246e-14 at perturbed#1790[7]
  max ulp        : 125 at perturbed#1453[6]
  ulp percentiles: p50=0 p90=0 p99=5 p100>=16
  ulp histogram  : 0:23639 1:224 2:85 3:47 4:12 5:13 6:17 7:17 8:10 9:7 10:5 11:6 12:3 13:2 15:1 >=16:162
  non-finite     : 0 seen, 0 mismatched
tier1 flows FCM (10 streams x 8)
  cases          : 194000
  max rel err    : 2.268e-14 at perturbed#1790[53]
  max ulp        : 178 at perturbed#1106[43]
  ulp percentiles: p50=0 p90=0 p99=4 p100>=16
  ulp histogram  : 0:187622 1:2738 2:1090 3:426 4:288 5:119 6:88 7:62 8:43 9:57 10:38 11:31 12:25 13:47 14:66 15:90 >=16:1170
  non-finite     : 0 seen, 0 mismatched
tier1 flows FWR/FWS/AGSP
  cases          : 7275
  max rel err    : 0.000e0 at nominal#0[1]
  max ulp        : 0 at nominal#0[1]
  ulp percentiles: p50=0 p90=0 p99=0 p100=0
  ulp histogram  : 0:7275
  non-finite     : 0 seen, 0 mismatched
tier1 flows CPDH
  cases          : 2425
  max rel err    : 2.034e-15 at perturbed#1790[1]
  max ulp        : 18 at perturbed#1790[1]
  ulp percentiles: p50=0 p90=0 p99=2 p100>=16
  ulp histogram  : 0:2312 1:71 2:20 3:6 4:5 5:6 6:1 7:3 >=16:1
  non-finite     : 0 seen, 0 mismatched
tier1 flows HST(9) after the compressor bump
  cases          : 2425
  max rel err    : 6.522e-15 at perturbed#1790[1]
  max ulp        : 42 at perturbed#1790[1]
  ulp percentiles: p50=0 p90=0 p99=2 p100>=16
  ulp histogram  : 0:2253 1:133 2:24 3:10 4:1 5:1 6:1 9:1 >=16:1
  non-finite     : 0 seen, 0 mismatched
states skipped because the port would not converge: 0
ok
test the_steam_coefficient_matches_through_the_condenser_duty ... tier1 UAC, via QUC
  cases          : 300
  max rel err    : 0.000e0 at nominal#0[1]
  max ulp        : 0 at nominal#0[1]
  ulp percentiles: p50=0 p90=0 p99=0 p100=0
  ulp histogram  : 0:300
  non-finite     : 0 seen, 0 mismatched
300 states below 100 C, 0 at or above and excluded
ok

test result: ok. 5 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.16s

tier2_stripper, vendored libm

cargo test -p tepsim-oracle --features oracle --release --test tier2_stripper -- --nocapture --test-threads 1
running 4 tests
test every_stripper_branch_is_exercised_by_the_pool ... VLR at the 10% heat-transfer breakpoint              Hyperbolic
VLR at the 50% heat-transfer breakpoint              Hyperbolic
TCC at the lower stripping-factor branch             Hyperbolic
TCC below the lower stripping-factor branch          Pinned
TCC at the upper stripping-factor branch             Hyperbolic
TCC approaching the 177 C pole                       Linear
TCR at the shutdown limit                            Hyperbolic
TCR above the shutdown limit                         Hyperbolic
FTM(11) at the stripping-factor threshold            Idle
VLR at the upper shutdown limit                      Hyperbolic
VLR at the lower shutdown limit                      Hyperbolic
VLS at the upper shutdown limit                      Hyperbolic
VLS at the lower shutdown limit                      Hyperbolic
VLC at the upper shutdown limit                      Hyperbolic
VLC at the lower shutdown limit                      Hyperbolic
PTR at the reactor pressure shutdown limit           Hyperbolic
PTV = PTR, the mixing-to-reactor flow clamp          Hyperbolic
PTR = PTS, the reactor-to-separator flow clamp       Hyperbolic
PTV = PTS, the recycle flow clamp                    Hyperbolic
PR = 1, the compressor reverse-flow clamp            Hyperbolic
PR = CPPRMX, the compressor maximum-ratio clamp      Hyperbolic
PR above CPPRMX, inside the clamped region           Hyperbolic
VLR below the lower shutdown limit                   Hyperbolic
VLS below the lower shutdown limit                   Hyperbolic
VLC below the lower shutdown limit                   Hyperbolic
branches reached: {"hyperbolic", "idle", "linear", "pinned"}
ok
test the_non_condensible_factors_never_move_in_the_fortran_either ... SFR(1..3) fixed at [0.9950000047683716, 0.9909999966621399, 0.9900000095367432] across 300 nominal and 20 adversarial states
ok
test the_reactor_inlet_is_an_alias_in_the_fortran_too ... ok
test the_stripper_matches_the_fortran_over_all_three_pools ... transcendentals come from the vendored libm
tier1 stripper SFR
  cases          : 19400
  max rel err    : 0.000e0 at nominal#0[1]
  max ulp        : 0 at nominal#0[1]
  ulp percentiles: p50=0 p90=0 p99=0 p100=0
  ulp histogram  : 0:19400
  non-finite     : 0 seen, 0 mismatched
tier1 stripper FTM (5, 12): the column's own outlets
  cases          : 4850
  max rel err    : 0.000e0 at nominal#0[1]
  max ulp        : 0 at nominal#0[1]
  ulp percentiles: p50=0 p90=0 p99=0 p100=0
  ulp histogram  : 0:4850
  non-finite     : 0 seen, 0 mismatched
tier1 stripper FTM (7): the reactor-inlet alias
  cases          : 2425
  max rel err    : 1.484e-15 at perturbed#817[1]
  max ulp        : 12 at perturbed#591[1]
  ulp percentiles: p50=0 p90=0 p99=5 p100=12
  ulp histogram  : 0:2322 1:1 2:19 3:46 4:5 5:8 6:15 7:6 8:1 12:2
  non-finite     : 0 seen, 0 mismatched
tier1 stripper FCM (5, 12): the column's own outlets
  cases          : 38800
  max rel err    : 0.000e0 at nominal#0[1]
  max ulp        : 0 at nominal#0[1]
  ulp percentiles: p50=0 p90=0 p99=0 p100=0
  ulp histogram  : 0:38800
  non-finite     : 0 seen, 0 mismatched
tier1 stripper FCM (7): the reactor-inlet alias
  cases          : 19400
  max rel err    : 1.610e-15 at perturbed#817[3]
  max ulp        : 13 at perturbed#591[8]
  ulp percentiles: p50=0 p90=0 p99=4 p100=13
  ulp histogram  : 0:18576 1:6 2:100 3:313 4:228 5:103 6:33 7:15 8:11 9:8 10:1 11:3 12:2 13:1
  non-finite     : 0 seen, 0 mismatched
tier1 stripper XST (5, 12): the column's own outlets
  cases          : 38800
  max rel err    : 0.000e0 at nominal#0[1]
  max ulp        : 0 at nominal#0[1]
  ulp percentiles: p50=0 p90=0 p99=0 p100=0
  ulp histogram  : 0:38800
  non-finite     : 0 seen, 0 mismatched
tier1 stripper XST (7): the reactor-inlet alias
  cases          : 19400
  max rel err    : 0.000e0 at nominal#0[1]
  max ulp        : 0 at nominal#0[1]
  ulp percentiles: p50=0 p90=0 p99=0 p100=0
  ulp histogram  : 0:19400
  non-finite     : 0 seen, 0 mismatched
tier1 stripper TST (5, 7, 12)
  cases          : 7275
  max rel err    : 0.000e0 at nominal#0[1]
  max ulp        : 0 at nominal#0[1]
  ulp percentiles: p50=0 p90=0 p99=0 p100=0
  ulp histogram  : 0:7275
  non-finite     : 0 seen, 0 mismatched
tier1 stripper HST (5, 12): the column's own outlets
  cases          : 4850
  max rel err    : 0.000e0 at nominal#0[1]
  max ulp        : 0 at nominal#0[1]
  ulp percentiles: p50=0 p90=0 p99=0 p100=0
  ulp histogram  : 0:4850
  non-finite     : 0 seen, 0 mismatched
tier1 stripper HST (7): the reactor-inlet alias
  cases          : 2425
  max rel err    : 0.000e0 at nominal#0[1]
  max ulp        : 0 at nominal#0[1]
  ulp percentiles: p50=0 p90=0 p99=0 p100=0
  ulp histogram  : 0:2425
  non-finite     : 0 seen, 0 mismatched
states skipped because the port would not converge: 0
ok

test result: ok. 4 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.14s

tier2_heat, vendored libm

cargo test -p tepsim-oracle --features oracle --release --test tier2_heat -- --nocapture --test-threads 1
running 4 tests
test both_sides_of_the_steam_cutoff_are_exercised ... steam on: 423 states, off: 2 states
ok
test every_level_branch_is_exercised_by_the_pool ... VLR at the 10% heat-transfer breakpoint              dry
VLR at the 50% heat-transfer breakpoint              ramp
TCC at the lower stripping-factor branch             fully wetted
TCC below the lower stripping-factor branch          fully wetted
TCC at the upper stripping-factor branch             fully wetted
TCC approaching the 177 C pole                       fully wetted
TCR at the shutdown limit                            fully wetted
TCR above the shutdown limit                         fully wetted
FTM(11) at the stripping-factor threshold            fully wetted
VLR at the upper shutdown limit                      fully wetted
VLR at the lower shutdown limit                      dry
VLS at the upper shutdown limit                      fully wetted
VLS at the lower shutdown limit                      fully wetted
VLC at the upper shutdown limit                      fully wetted
VLC at the lower shutdown limit                      fully wetted
PTR at the reactor pressure shutdown limit           fully wetted
PTV = PTR, the mixing-to-reactor flow clamp          fully wetted
PTR = PTS, the reactor-to-separator flow clamp       fully wetted
PTV = PTS, the recycle flow clamp                    fully wetted
PR = 1, the compressor reverse-flow clamp            fully wetted
PR = CPPRMX, the compressor maximum-ratio clamp      fully wetted
PR above CPPRMX, inside the clamped region           fully wetted
VLR below the lower shutdown limit                   dry
VLS below the lower shutdown limit                   fully wetted
VLC below the lower shutdown limit                   fully wetted
level branches reached: {"dry", "fully wetted", "ramp"}
ok
test heat_transfer_matches_the_fortran_over_all_three_pools ... transcendentals come from the vendored libm
tier1 heat UAR
  cases          : 2425
  max rel err    : 0.000e0 at nominal#0[1]
  max ulp        : 0 at nominal#0[1]
  ulp percentiles: p50=0 p90=0 p99=0 p100=0
  ulp histogram  : 0:2425
  non-finite     : 0 seen, 0 mismatched
tier1 heat QUR
  cases          : 2425
  max rel err    : 0.000e0 at nominal#0[1]
  max ulp        : 0 at nominal#0[1]
  ulp percentiles: p50=0 p90=0 p99=0 p100=0
  ulp histogram  : 0:2425
  non-finite     : 0 seen, 0 mismatched
tier1 heat QUS
  cases          : 2425
  max rel err    : 9.572e-13 at perturbed#50[1]
  max ulp        : 8009 at perturbed#50[1]
  ulp percentiles: p50=0 p90=0 p99>=16 p100>=16
  ulp histogram  : 0:2196 1:23 2:14 3:10 4:21 5:9 6:3 7:10 8:4 9:2 10:3 11:1 12:3 13:3 14:6 15:1 >=16:116
  non-finite     : 0 seen, 0 mismatched
tier1 heat QUC
  cases          : 2425
  max rel err    : 0.000e0 at nominal#0[1]
  max ulp        : 0 at nominal#0[1]
  ulp percentiles: p50=0 p90=0 p99=0 p100=0
  ulp histogram  : 0:2425
  non-finite     : 0 seen, 0 mismatched
states skipped because the port would not converge: 0
ok
test the_condenser_driving_difference_is_large_enough_to_discriminate ... smallest gap between TST(8) and TCS as the driving temperature: 40.2906 C
ok

test result: ok. 4 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.17s

tier2_measurements, vendored libm

cargo test -p tepsim-oracle --features oracle --release --test tier2_measurements -- --nocapture --test-threads 1
running 4 tests
test the_measurements_match_the_fortran_over_all_three_pools ... transcendentals come from the vendored libm
tier1 XMEAS(1..22), noise-free
  cases          : 53350
  max rel err    : 8.774e-15 at perturbed#1851[5]
  max ulp        : 78 at perturbed#1851[5]
  ulp percentiles: p50=0 p90=0 p99=1 p100>=16
  ulp histogram  : 0:52801 1:226 2:132 3:58 4:44 5:23 6:7 7:7 8:3 9:4 13:1 14:3 >=16:41
  non-finite     : 0 seen, 0 mismatched
states skipped because the port would not converge: 0
ok
test the_pool_reaches_several_distinct_shutdown_causes ... TCR at the shutdown limit                            ["reactor pressure high"]
TCR above the shutdown limit                         ["reactor pressure high", "reactor temperature high"]
VLR at the upper shutdown limit                      ["reactor pressure high", "reactor level high"]
VLS at the upper shutdown limit                      ["separator level high"]
VLC at the upper shutdown limit                      ["stripper level high"]
VLR below the lower shutdown limit                   ["reactor level low"]
VLS below the lower shutdown limit                   ["separator level low"]
VLC below the lower shutdown limit                   ["stripper level low"]
shutdown causes reached: {"reactor level high", "reactor level low", "reactor pressure high", "reactor temperature high", "separator level high", "separator level low", "stripper level high", "stripper level low"}
ok
test the_shutdown_detector_agrees_with_the_fortran_on_every_state ... tier1 ISD as 0 or 1
  cases          : 2425
  max rel err    : 0.000e0 at nominal#0[1]
  max ulp        : 0 at nominal#0[1]
  ulp percentiles: p50=0 p90=0 p99=0 p100=0
  ulp histogram  : 0:2425
  non-finite     : 0 seen, 0 mismatched
12 states trip, 2413 do not
ok
test time_zero_is_what_suppresses_the_noise ... 22 of 22 measurements differ with the clock running
ok

test result: ok. 4 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.12s

tier2_balances, vendored libm

cargo test -p tepsim-oracle --features oracle --release --test tier2_balances -- --nocapture --test-threads 1
running 4 tests
test all_fifty_derivatives_match_the_fortran_over_all_three_pools ... transcendentals come from the vendored libm
tier1 YP(1..50), relative to the derivative (reported)
  cases          : 120600
  max rel err    : 1.393e-4 at nominal#303[2]
  max ulp        : 738734374912 at nominal#303[2]
  ulp percentiles: p50=0 p90=2 p99>=16 p100>=16
  ulp histogram  : 0:107101 1:935 2:1289 3:101 4:1512 5:22 6:176 7:14 8:1354 9:19 10:35 11:7 12:166 13:4 14:21 15:11 >=16:7833
  non-finite     : 0 seen, 0 mismatched
tier1 YP(1..50), relative to the scale of the terms (the gate)
  cases          : 120600
  max rel err    : 6.093e-14 at perturbed#300[7]
  max ulp        : 738734374912 at nominal#303[2]
  ulp percentiles: p50=0 p90=2 p99>=16 p100>=16
  ulp histogram  : 0:107101 1:935 2:1289 3:101 4:1512 5:22 6:176 7:14 8:1354 9:19 10:35 11:7 12:166 13:4 14:21 15:11 >=16:7833
  non-finite     : 0 seen, 0 mismatched
tier1 YP(1..50), plant frozen
  cases          : 650
  max rel err    : 0.000e0 at perturbed#369[1]
  max ulp        : 0 at perturbed#369[1]
  ulp percentiles: p50=0 p90=0 p99=0 p100=0
  ulp histogram  : 0:650
  non-finite     : 0 seen, 0 mismatched
2412 states running, 13 frozen, 0 skipped
ok
test every_derivative_slot_actually_moves_somewhere_in_the_pool ... 50 of 50 slots move somewhere in the pool
ok
test the_quirk_fix_changes_only_the_frozen_states ... the fix changes 8 tripping boundaries and leaves 17 alone
ok
test tier2_acceptance_table ... Tier 2 acceptance, 2412 running states
gate: error / scale-of-terms < 1e-12

  YP   err/scale    err/value   ratio
   1   2.537e-14   8.181e-8    3224593x
   2   3.826e-14   3.401e-5   889000607x
   3   2.187e-14   7.030e-8    3214801x
   4   5.557e-15   2.377e-8    4277976x
   5   2.688e-14   5.845e-8    2174388x
   6   4.370e-14   2.889e-7    6610686x
   7   6.093e-14   1.920e-5   315124861x
   8   5.039e-14   8.304e-7   16478948x
   9   3.115e-14   1.445e-8     463960x
  10   2.910e-14   3.896e-8    1338742x
  11   2.899e-14   2.981e-7   10282117x
  12   2.918e-14   2.702e-8     925989x
  13   2.755e-14   3.689e-8    1339161x
  14   3.113e-14   1.779e-7    5713202x
  15   3.415e-14   1.068e-7    3128911x
  16   5.759e-14   1.020e-6   17702099x
  17   4.644e-14   7.407e-9     159498x
  18   5.136e-14   2.280e-8     444028x
  19   0.000e0   0.000e0          1x
  20   0.000e0   0.000e0          1x
  21   0.000e0   0.000e0          1x
  22   0.000e0   0.000e0          1x
  23   0.000e0   0.000e0          1x
  24   0.000e0   0.000e0          1x
  25   0.000e0   0.000e0          1x
  26   0.000e0   0.000e0          1x
  27   0.000e0   0.000e0          1x
  28   5.276e-15   3.361e-8    6370459x
  29   7.713e-15   1.852e-7   24013923x
  30   4.567e-15   3.551e-8    7774685x
  31   1.241e-15   4.665e-9    3760128x
  32   5.087e-15   3.403e-8    6689516x
  33   7.093e-15   3.078e-7   43390364x
  34   6.921e-15   2.810e-8    4059475x
  35   7.220e-15   6.372e-9     882549x
  36   5.298e-15   9.721e-7   183496207x
  37   0.000e0   0.000e0          1x
  38   7.969e-15   5.112e-7   64146903x
  39   0.000e0   0.000e0          1x
  40   0.000e0   0.000e0          1x
  41   0.000e0   0.000e0          1x
  42   0.000e0   0.000e0          1x
  43   0.000e0   0.000e0          1x
  44   0.000e0   0.000e0          1x
  45   0.000e0   0.000e0          1x
  46   0.000e0   0.000e0          1x
  47   0.000e0   0.000e0          1x
  48   0.000e0   0.000e0          1x
  49   0.000e0   0.000e0          1x
  50   0.000e0   0.000e0          1x

worst component: YP(7) at 6.093e-14 of its own scale
tier1 YP(1..50), relative to the scale of the terms (the gate)
  cases          : 120600
  max rel err    : 6.093e-14 at perturbed#300[7]
  max ulp        : 206158430208 at nominal#296[2]
  ulp percentiles: p50=0 p90=2 p99>=16 p100>=16
  ulp histogram  : 0:107280 1:890 2:1293 3:113 4:1468 5:32 6:163 7:17 8:1240 9:19 10:42 11:11 12:166 13:7 14:26 15:11 >=16:7822
  non-finite     : 0 seen, 0 mismatched
28 of 50 components cancel by more than 100x
ok

test result: ok. 4 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.20s

tier4_closed_loop, platform libm

cargo test -p tepsim-oracle --features oracle,libm-system --release --test tier4_closed_loop -- --nocapture --test-threads 1
running 5 tests
test the_closed_loop_plant_matches_the_fortran_driver ... closed loop, platform libm, 10 h (36000 steps)
  worst XMEAS  : 0.000e0 at XMEAS(0)
  worst XMV    : 0.000e0 %range at XMV(0)
  within XNS   : 10.000 h of 10, ending at 0.000e0 x XNS(0)
  first split  : never
ok
test the_controlled_plant_runs_the_full_horizon_without_tripping ... 10 h (36000 steps): fortran up, port closed-loop up, port open-loop tripped at step 11017 (3.060 h) on Some(ReactorPressureHigh)
ok
test the_controllers_read_the_previous_steps_measurements ... XMV(7) on the first fire: previous-step 34.147823842, current-step 35.623679189, fortran 34.147823842
ok
test the_driver_forces_idv12_at_the_eight_hour_mark ... ok
test the_forced_disturbance_changes_the_plant_measurably ... D-011, 10 h, faithful against fixed:
  first split, port   : Some((29390, 22))
  first split, fortran: Some((29390, 22))
  worst over the run  : 1.092e-1 at XMEAS(37)
  worst at the end    : 3.262e-2 at XMEAS(38)
ok

test result: ok. 5 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 4.29s

tier5_invariants, platform libm

cargo test -p tepsim-oracle --features oracle,libm-system --release --test tier5_invariants -- --nocapture --test-threads 1
running 9 tests
test each_reaction_balances_on_paper ... ok
test every_component_balances_on_moles ... I-4 on the Fortran over 1 h: worst 7.436e-15 relative, on F at step          627; largest reaction term seen 457.5 lbmol/h
ok
test every_component_balances_on_moles_in_the_port ... I-4 on the port at the nominal state: worst 2.892e-15 relative, on F
ok
test the_fortran_conserves_mass_across_the_reaction ... I-1 on the Fortran: 200 states, worst residual 4.664e-16 of the term scale, at pool index 130
ok
test the_inert_has_no_reaction_term ... I-3: CRXR(2) is zero across 100 reacting states
ok
test the_plant_balances_mass_away_from_steady_state ... I-2 along 2 h open loop: worst 6.556e-16 relative at step 6570, where the accumulation term is 0.1 lb/h
  accumulation 0.0613 lb/h against a residual of about 2.10e-11 lb/h: a factor of 2.9e9
ok
test the_port_conserves_mass_across_the_reaction ... I-1 on the port: 200 states, worst residual 4.664e-16 of the term scale, at pool index 22
ok
test the_ported_plant_balances_mass ... I-2 on the port at the nominal state: residual 6.693535e-12 lb/h against 32191.8 lb/h, 2.079e-16 relative
ok
test the_whole_plant_balances_mass ... I-2 on the Fortran at the nominal state: residual 6.693535e-12 lb/h against a throughput of 32191.8 lb/h, 2.079e-16 relative
ok

test result: ok. 9 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.29s

tier5_runs, platform libm

cargo test -p tepsim-oracle --features oracle,libm-system --release --test tier5_runs -- --nocapture --test-threads 1
running 10 tests
test a_run_is_reproducible_from_its_scenario_and_seed ... ok
test both_sources_produce_the_same_shape ... 2 h gives 40 samples of 53 variables
ok
test different_seeds_give_different_runs ... five nominal seeds: worst relative spread 1.366e1
ok
test every_disturbance_moves_the_plant ...   IDV(1)   worst departure from nominal 2.456e0
  IDV(2)   worst departure from nominal 5.964e-1
  IDV(3)   worst departure from nominal 4.076e-3
  IDV(4)   worst departure from nominal 1.396e-1
  IDV(5)   worst departure from nominal 3.522e-1
  IDV(6)   worst departure from nominal 3.467e0
  IDV(7)   worst departure from nominal 7.595e-1
  IDV(8)   worst departure from nominal 3.927e-1
  IDV(9)   worst departure from nominal 3.531e-3
  IDV(10)  worst departure from nominal 1.801e-2
  IDV(11)  worst departure from nominal 1.798e-1
  IDV(12)  worst departure from nominal 1.413e-1
  IDV(13)  worst departure from nominal 1.956e-1
  IDV(14)  worst departure from nominal 2.626e-1
  IDV(15)  worst departure from nominal 9.839e-3
  IDV(16)  worst departure from nominal 7.772e-2
  IDV(17)  worst departure from nominal 7.203e-2
  IDV(18)  worst departure from nominal 3.782e-1
  IDV(19)  worst departure from nominal 7.098e-2
  IDV(20)  worst departure from nominal 9.324e-2
ok
test every_seed_is_a_valid_generator_word ... ok
test the_battery_sizes_are_what_they_claim ... full battery: 2100 runs per source, 960 samples each, about 27 min per source at 766 ms per 48 h run
ok
test the_published_seed_table_is_transcribed_correctly ... published seeds: 34 of 54 exceed 2^32, 27 of 54 are          even
ok
test the_sticking_valve_divergence_is_the_transcendentals ... IDV(14), XMV(10): worst absolute difference 0.000e0 with the platform libm
ok
test the_two_sources_agree_over_a_whole_run ... nominal: worst 0.000e0 at sample 0 variable 1 (platform libm)
IDV(1): worst 0.000e0 at sample 0 variable 1 (platform libm)
IDV(13): worst 0.000e0 at sample 0 variable 1 (platform libm)
IDV(14): worst 0.000e0 at sample 0 variable 1 (platform libm)
IDV(19): worst 0.000e0 at sample 0 variable 1 (platform libm)
ok
test there_are_twenty_faults_and_twenty_one_scenarios ... ok

test result: ok. 10 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 2.87s

tier2_equilibrium, platform libm

cargo test -p tepsim-oracle --features oracle,libm-system --release --test tier2_equilibrium -- --nocapture --test-threads 1
running 3 tests
test the_algebra_is_bit_identical_once_exp_agrees ... tier1 equilibrium VVR/VVS
  cases          : 1450
  max rel err    : 0.000e0 at nominal#0[1]
  max ulp        : 0 at nominal#0[1]
  ulp percentiles: p50=0 p90=0 p99=0 p100=0
  ulp histogram  : 0:1450
  non-finite     : 0 seen, 0 mismatched
tier1 equilibrium PPR
  cases          : 5800
  max rel err    : 0.000e0 at nominal#0[1]
  max ulp        : 0 at nominal#0[1]
  ulp percentiles: p50=0 p90=0 p99=0 p100=0
  ulp histogram  : 0:5800
  non-finite     : 0 seen, 0 mismatched
tier1 equilibrium PPS
  cases          : 5800
  max rel err    : 0.000e0 at nominal#0[1]
  max ulp        : 0 at nominal#0[1]
  ulp percentiles: p50=0 p90=0 p99=0 p100=0
  ulp histogram  : 0:5800
  non-finite     : 0 seen, 0 mismatched
tier1 equilibrium PTR/PTS/PTV
  cases          : 2175
  max rel err    : 0.000e0 at nominal#0[1]
  max ulp        : 0 at nominal#0[1]
  ulp percentiles: p50=0 p90=0 p99=0 p100=0
  ulp histogram  : 0:2175
  non-finite     : 0 seen, 0 mismatched
tier1 equilibrium XVR
  cases          : 5800
  max rel err    : 0.000e0 at nominal#0[1]
  max ulp        : 0 at nominal#0[1]
  ulp percentiles: p50=0 p90=0 p99=0 p100=0
  ulp histogram  : 0:5800
  non-finite     : 0 seen, 0 mismatched
tier1 equilibrium XVS
  cases          : 5800
  max rel err    : 0.000e0 at nominal#0[1]
  max ulp        : 0 at nominal#0[1]
  ulp percentiles: p50=0 p90=0 p99=0 p100=0
  ulp histogram  : 0:5800
  non-finite     : 0 seen, 0 mismatched
tier1 equilibrium UTVR/UTVS
  cases          : 1450
  max rel err    : 0.000e0 at nominal#0[1]
  max ulp        : 0 at nominal#0[1]
  ulp percentiles: p50=0 p90=0 p99=0 p100=0
  ulp histogram  : 0:1450
  non-finite     : 0 seen, 0 mismatched
tier1 equilibrium UCVR
  cases          : 5800
  max rel err    : 0.000e0 at nominal#0[1]
  max ulp        : 0 at nominal#0[1]
  ulp percentiles: p50=0 p90=0 p99=0 p100=0
  ulp histogram  : 0:5800
  non-finite     : 0 seen, 0 mismatched
tier1 equilibrium UCVS
  cases          : 5800
  max rel err    : 0.000e0 at nominal#0[1]
  max ulp        : 0 at nominal#0[1]
  ulp percentiles: p50=0 p90=0 p99=0 p100=0
  ulp histogram  : 0:5800
  non-finite     : 0 seen, 0 mismatched
ok
test the_equilibrium_matches_the_fortran_over_all_three_pools ... exp comes from the platform libm
tier1 equilibrium VVR/VVS
  cases          : 4850
  max rel err    : 0.000e0 at nominal#0[1]
  max ulp        : 0 at nominal#0[1]
  ulp percentiles: p50=0 p90=0 p99=0 p100=0
  ulp histogram  : 0:4850
  non-finite     : 0 seen, 0 mismatched
tier1 equilibrium PPR
  cases          : 19400
  max rel err    : 0.000e0 at nominal#0[1]
  max ulp        : 0 at nominal#0[1]
  ulp percentiles: p50=0 p90=0 p99=0 p100=0
  ulp histogram  : 0:19400
  non-finite     : 0 seen, 0 mismatched
tier1 equilibrium PPS
  cases          : 19400
  max rel err    : 0.000e0 at nominal#0[1]
  max ulp        : 0 at nominal#0[1]
  ulp percentiles: p50=0 p90=0 p99=0 p100=0
  ulp histogram  : 0:19400
  non-finite     : 0 seen, 0 mismatched
tier1 equilibrium PTR/PTS/PTV
  cases          : 7275
  max rel err    : 0.000e0 at nominal#0[1]
  max ulp        : 0 at nominal#0[1]
  ulp percentiles: p50=0 p90=0 p99=0 p100=0
  ulp histogram  : 0:7275
  non-finite     : 0 seen, 0 mismatched
tier1 equilibrium XVR
  cases          : 19400
  max rel err    : 0.000e0 at nominal#0[1]
  max ulp        : 0 at nominal#0[1]
  ulp percentiles: p50=0 p90=0 p99=0 p100=0
  ulp histogram  : 0:19400
  non-finite     : 0 seen, 0 mismatched
tier1 equilibrium XVS
  cases          : 19400
  max rel err    : 0.000e0 at nominal#0[1]
  max ulp        : 0 at nominal#0[1]
  ulp percentiles: p50=0 p90=0 p99=0 p100=0
  ulp histogram  : 0:19400
  non-finite     : 0 seen, 0 mismatched
tier1 equilibrium UTVR/UTVS
  cases          : 4850
  max rel err    : 0.000e0 at nominal#0[1]
  max ulp        : 0 at nominal#0[1]
  ulp percentiles: p50=0 p90=0 p99=0 p100=0
  ulp histogram  : 0:4850
  non-finite     : 0 seen, 0 mismatched
tier1 equilibrium UCVR
  cases          : 19400
  max rel err    : 0.000e0 at nominal#0[1]
  max ulp        : 0 at nominal#0[1]
  ulp percentiles: p50=0 p90=0 p99=0 p100=0
  ulp histogram  : 0:19400
  non-finite     : 0 seen, 0 mismatched
tier1 equilibrium UCVS
  cases          : 19400
  max rel err    : 0.000e0 at nominal#0[1]
  max ulp        : 0 at nominal#0[1]
  ulp percentiles: p50=0 p90=0 p99=0 p100=0
  ulp histogram  : 0:19400
  non-finite     : 0 seen, 0 mismatched
states skipped because the port would not converge: 0
ok
test the_vendored_and_platform_exp_differ_only_by_rounding ... exp over the Antoine range [0.5868, 13.0825]: 15000 arguments, 0 differ (0.000%), worst 0 ulp
ok

test result: ok. 3 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.11s

tier2_kinetics, platform libm

cargo test -p tepsim-oracle --features oracle,libm-system --release --test tier2_kinetics -- --nocapture --test-threads 1
running 4 tests
test the_algebra_is_bit_identical_once_exp_and_pow_agree ... tier1 kinetics RR
  cases          : 2900
  max rel err    : 0.000e0 at nominal#0[1]
  max ulp        : 0 at nominal#0[1]
  ulp percentiles: p50=0 p90=0 p99=0 p100=0
  ulp histogram  : 0:2900
  non-finite     : 0 seen, 0 mismatched
tier1 kinetics CRXR
  cases          : 5800
  max rel err    : 0.000e0 at nominal#0[1]
  max ulp        : 0 at nominal#0[1]
  ulp percentiles: p50=0 p90=0 p99=0 p100=0
  ulp histogram  : 0:5800
  non-finite     : 0 seen, 0 mismatched
tier1 kinetics RH
  cases          : 725
  max rel err    : 0.000e0 at nominal#0[1]
  max ulp        : 0 at nominal#0[1]
  ulp percentiles: p50=0 p90=0 p99=0 p100=0
  ulp histogram  : 0:725
  non-finite     : 0 seen, 0 mismatched
ok
test the_inert_has_no_net_production_in_either_implementation ... tier1 CRXR(2), never assigned
  cases          : 200
  max rel err    : 0.000e0 at nominal#0[2]
  max ulp        : 0 at nominal#0[2]
  ulp percentiles: p50=0 p90=0 p99=0 p100=0
  ulp histogram  : 0:200
  non-finite     : 0 seen, 0 mismatched
ok
test the_kinetics_match_the_fortran_over_all_three_pools ... exp and pow come from the platform libm
tier1 kinetics RR
  cases          : 9700
  max rel err    : 0.000e0 at nominal#0[1]
  max ulp        : 0 at nominal#0[1]
  ulp percentiles: p50=0 p90=0 p99=0 p100=0
  ulp histogram  : 0:9700
  non-finite     : 0 seen, 0 mismatched
tier1 kinetics CRXR
  cases          : 19400
  max rel err    : 0.000e0 at nominal#0[1]
  max ulp        : 0 at nominal#0[1]
  ulp percentiles: p50=0 p90=0 p99=0 p100=0
  ulp histogram  : 0:19400
  non-finite     : 0 seen, 0 mismatched
tier1 kinetics RH
  cases          : 2425
  max rel err    : 0.000e0 at nominal#0[1]
  max ulp        : 0 at nominal#0[1]
  ulp percentiles: p50=0 p90=0 p99=0 p100=0
  ulp histogram  : 0:2425
  non-finite     : 0 seen, 0 mismatched
states skipped because the port would not converge: 0
ok
test the_vendored_and_platform_pow_differ_only_by_rounding ... pow over [0.25, 5000] at orders 1.1544 and 0.3735: 40000 cases, 0 differ (0.000%), worst 0 ulp
ok

test result: ok. 4 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.12s

tier2_streams, platform libm

cargo test -p tepsim-oracle --features oracle,libm-system --release --test tier2_streams -- --nocapture --test-threads 1
running 4 tests
test the_algebra_is_bit_identical_once_exp_and_pow_agree ... tier1 streams XST (10 streams x 8)
  cases          : 58000
  max rel err    : 0.000e0 at nominal#0[1]
  max ulp        : 0 at nominal#0[1]
  ulp percentiles: p50=0 p90=0 p99=0 p100=0
  ulp histogram  : 0:58000
  non-finite     : 0 seen, 0 mismatched
tier1 streams XMWS (the 6 that exist)
  cases          : 4350
  max rel err    : 0.000e0 at nominal#0[1]
  max ulp        : 0 at nominal#0[1]
  ulp percentiles: p50=0 p90=0 p99=0 p100=0
  ulp histogram  : 0:4350
  non-finite     : 0 seen, 0 mismatched
tier1 streams TST
  cases          : 7250
  max rel err    : 0.000e0 at nominal#0[1]
  max ulp        : 0 at nominal#0[1]
  ulp percentiles: p50=0 p90=0 p99=0 p100=0
  ulp histogram  : 0:7250
  non-finite     : 0 seen, 0 mismatched
tier1 streams HST
  cases          : 7250
  max rel err    : 0.000e0 at nominal#0[1]
  max ulp        : 0 at nominal#0[1]
  ulp percentiles: p50=0 p90=0 p99=0 p100=0
  ulp histogram  : 0:7250
  non-finite     : 0 seen, 0 mismatched
ok
test the_compressor_makes_the_recycle_enthalpy_differ_from_the_purge ... HST(9) differs from HST(10) on 200 of 200 nominal states
ok
test the_stream_table_matches_the_fortran_over_all_three_pools ... exp and pow come from the platform libm
tier1 streams XST (10 streams x 8)
  cases          : 194000
  max rel err    : 0.000e0 at nominal#0[1]
  max ulp        : 0 at nominal#0[1]
  ulp percentiles: p50=0 p90=0 p99=0 p100=0
  ulp histogram  : 0:194000
  non-finite     : 0 seen, 0 mismatched
tier1 streams XMWS (the 6 that exist)
  cases          : 14550
  max rel err    : 0.000e0 at nominal#0[1]
  max ulp        : 0 at nominal#0[1]
  ulp percentiles: p50=0 p90=0 p99=0 p100=0
  ulp histogram  : 0:14550
  non-finite     : 0 seen, 0 mismatched
tier1 streams TST
  cases          : 24250
  max rel err    : 0.000e0 at nominal#0[1]
  max ulp        : 0 at nominal#0[1]
  ulp percentiles: p50=0 p90=0 p99=0 p100=0
  ulp histogram  : 0:24250
  non-finite     : 0 seen, 0 mismatched
tier1 streams HST
  cases          : 24250
  max rel err    : 0.000e0 at nominal#0[1]
  max ulp        : 0 at nominal#0[1]
  ulp percentiles: p50=0 p90=0 p99=0 p100=0
  ulp histogram  : 0:24250
  non-finite     : 0 seen, 0 mismatched
states skipped because the port would not converge: 0
ok
test the_unweighed_streams_are_zero_in_both_implementations ... 7 unweighed streams, all zero over 200 states
ok

test result: ok. 4 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.13s

tier2_flows, platform libm

cargo test -p tepsim-oracle --features oracle,libm-system --release --test tier2_flows -- --nocapture --test-threads 1
running 6 tests
test component_flows_sum_to_the_stream_total ... ok
test no_sampled_state_reaches_the_purge_clamp ... lowest PTS over the whole pool: 19153.09 mmHg against a 760 threshold
ok
test the_algebra_is_bit_identical_once_exp_and_pow_agree ... tier1 flows FTM (the 10 assembled streams)
  cases          : 7250
  max rel err    : 0.000e0 at nominal#0[1]
  max ulp        : 0 at nominal#0[1]
  ulp percentiles: p50=0 p90=0 p99=0 p100=0
  ulp histogram  : 0:7250
  non-finite     : 0 seen, 0 mismatched
tier1 flows FCM (10 streams x 8)
  cases          : 58000
  max rel err    : 0.000e0 at nominal#0[1]
  max ulp        : 0 at nominal#0[1]
  ulp percentiles: p50=0 p90=0 p99=0 p100=0
  ulp histogram  : 0:58000
  non-finite     : 0 seen, 0 mismatched
tier1 flows FWR/FWS/AGSP
  cases          : 2175
  max rel err    : 0.000e0 at nominal#0[1]
  max ulp        : 0 at nominal#0[1]
  ulp percentiles: p50=0 p90=0 p99=0 p100=0
  ulp histogram  : 0:2175
  non-finite     : 0 seen, 0 mismatched
tier1 flows CPDH
  cases          : 725
  max rel err    : 0.000e0 at nominal#0[1]
  max ulp        : 0 at nominal#0[1]
  ulp percentiles: p50=0 p90=0 p99=0 p100=0
  ulp histogram  : 0:725
  non-finite     : 0 seen, 0 mismatched
tier1 flows HST(9) after the compressor bump
  cases          : 725
  max rel err    : 0.000e0 at nominal#0[1]
  max ulp        : 0 at nominal#0[1]
  ulp percentiles: p50=0 p90=0 p99=0 p100=0
  ulp histogram  : 0:725
  non-finite     : 0 seen, 0 mismatched
ok
test the_compressor_ratio_clamps_are_exercised_by_the_adversarial_pool ... VLR at the 10% heat-transfer breakpoint              None
VLR at the 50% heat-transfer breakpoint              None
TCC at the lower stripping-factor branch             None
TCC below the lower stripping-factor branch          None
TCC at the upper stripping-factor branch             None
TCC approaching the 177 C pole                       None
TCR at the shutdown limit                            None
TCR above the shutdown limit                         None
FTM(11) at the stripping-factor threshold            None
VLR at the upper shutdown limit                      None
VLR at the lower shutdown limit                      None
VLS at the upper shutdown limit                      None
VLS at the lower shutdown limit                      None
VLC at the upper shutdown limit                      None
VLC at the lower shutdown limit                      None
PTR at the reactor pressure shutdown limit           None
PTV = PTR, the mixing-to-reactor flow clamp          None
PTR = PTS, the reactor-to-separator flow clamp       None
PTV = PTS, the recycle flow clamp                    Low
PR = 1, the compressor reverse-flow clamp            Low
PR = CPPRMX, the compressor maximum-ratio clamp      None
PR above CPPRMX, inside the clamped region           High
VLR below the lower shutdown limit                   None
VLS below the lower shutdown limit                   None
VLC below the lower shutdown limit                   None
ok
test the_flow_network_matches_the_fortran_over_all_three_pools ... exp, pow and sqrt come from the platform libm
tier1 flows FTM (the 10 assembled streams)
  cases          : 24250
  max rel err    : 0.000e0 at nominal#0[1]
  max ulp        : 0 at nominal#0[1]
  ulp percentiles: p50=0 p90=0 p99=0 p100=0
  ulp histogram  : 0:24250
  non-finite     : 0 seen, 0 mismatched
tier1 flows FCM (10 streams x 8)
  cases          : 194000
  max rel err    : 0.000e0 at nominal#0[1]
  max ulp        : 0 at nominal#0[1]
  ulp percentiles: p50=0 p90=0 p99=0 p100=0
  ulp histogram  : 0:194000
  non-finite     : 0 seen, 0 mismatched
tier1 flows FWR/FWS/AGSP
  cases          : 7275
  max rel err    : 0.000e0 at nominal#0[1]
  max ulp        : 0 at nominal#0[1]
  ulp percentiles: p50=0 p90=0 p99=0 p100=0
  ulp histogram  : 0:7275
  non-finite     : 0 seen, 0 mismatched
tier1 flows CPDH
  cases          : 2425
  max rel err    : 0.000e0 at nominal#0[1]
  max ulp        : 0 at nominal#0[1]
  ulp percentiles: p50=0 p90=0 p99=0 p100=0
  ulp histogram  : 0:2425
  non-finite     : 0 seen, 0 mismatched
tier1 flows HST(9) after the compressor bump
  cases          : 2425
  max rel err    : 0.000e0 at nominal#0[1]
  max ulp        : 0 at nominal#0[1]
  ulp percentiles: p50=0 p90=0 p99=0 p100=0
  ulp histogram  : 0:2425
  non-finite     : 0 seen, 0 mismatched
states skipped because the port would not converge: 0
ok
test the_steam_coefficient_matches_through_the_condenser_duty ... tier1 UAC, via QUC
  cases          : 300
  max rel err    : 0.000e0 at nominal#0[1]
  max ulp        : 0 at nominal#0[1]
  ulp percentiles: p50=0 p90=0 p99=0 p100=0
  ulp histogram  : 0:300
  non-finite     : 0 seen, 0 mismatched
300 states below 100 C, 0 at or above and excluded
ok

test result: ok. 6 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.19s

tier2_stripper, platform libm

cargo test -p tepsim-oracle --features oracle,libm-system --release --test tier2_stripper -- --nocapture --test-threads 1
running 5 tests
test every_stripper_branch_is_exercised_by_the_pool ... VLR at the 10% heat-transfer breakpoint              Hyperbolic
VLR at the 50% heat-transfer breakpoint              Hyperbolic
TCC at the lower stripping-factor branch             Hyperbolic
TCC below the lower stripping-factor branch          Pinned
TCC at the upper stripping-factor branch             Hyperbolic
TCC approaching the 177 C pole                       Linear
TCR at the shutdown limit                            Hyperbolic
TCR above the shutdown limit                         Hyperbolic
FTM(11) at the stripping-factor threshold            Idle
VLR at the upper shutdown limit                      Hyperbolic
VLR at the lower shutdown limit                      Hyperbolic
VLS at the upper shutdown limit                      Hyperbolic
VLS at the lower shutdown limit                      Hyperbolic
VLC at the upper shutdown limit                      Hyperbolic
VLC at the lower shutdown limit                      Hyperbolic
PTR at the reactor pressure shutdown limit           Hyperbolic
PTV = PTR, the mixing-to-reactor flow clamp          Hyperbolic
PTR = PTS, the reactor-to-separator flow clamp       Hyperbolic
PTV = PTS, the recycle flow clamp                    Hyperbolic
PR = 1, the compressor reverse-flow clamp            Hyperbolic
PR = CPPRMX, the compressor maximum-ratio clamp      Hyperbolic
PR above CPPRMX, inside the clamped region           Hyperbolic
VLR below the lower shutdown limit                   Hyperbolic
VLS below the lower shutdown limit                   Hyperbolic
VLC below the lower shutdown limit                   Hyperbolic
branches reached: {"hyperbolic", "idle", "linear", "pinned"}
ok
test the_algebra_is_bit_identical_once_exp_and_pow_agree ... tier1 stripper SFR
  cases          : 5800
  max rel err    : 0.000e0 at nominal#0[1]
  max ulp        : 0 at nominal#0[1]
  ulp percentiles: p50=0 p90=0 p99=0 p100=0
  ulp histogram  : 0:5800
  non-finite     : 0 seen, 0 mismatched
tier1 stripper FTM (5, 12): the column's own outlets
  cases          : 1450
  max rel err    : 0.000e0 at nominal#0[1]
  max ulp        : 0 at nominal#0[1]
  ulp percentiles: p50=0 p90=0 p99=0 p100=0
  ulp histogram  : 0:1450
  non-finite     : 0 seen, 0 mismatched
tier1 stripper FTM (7): the reactor-inlet alias
  cases          : 725
  max rel err    : 0.000e0 at nominal#0[1]
  max ulp        : 0 at nominal#0[1]
  ulp percentiles: p50=0 p90=0 p99=0 p100=0
  ulp histogram  : 0:725
  non-finite     : 0 seen, 0 mismatched
tier1 stripper FCM (5, 12): the column's own outlets
  cases          : 11600
  max rel err    : 0.000e0 at nominal#0[1]
  max ulp        : 0 at nominal#0[1]
  ulp percentiles: p50=0 p90=0 p99=0 p100=0
  ulp histogram  : 0:11600
  non-finite     : 0 seen, 0 mismatched
tier1 stripper FCM (7): the reactor-inlet alias
  cases          : 5800
  max rel err    : 0.000e0 at nominal#0[1]
  max ulp        : 0 at nominal#0[1]
  ulp percentiles: p50=0 p90=0 p99=0 p100=0
  ulp histogram  : 0:5800
  non-finite     : 0 seen, 0 mismatched
tier1 stripper XST (5, 12): the column's own outlets
  cases          : 11600
  max rel err    : 0.000e0 at nominal#0[1]
  max ulp        : 0 at nominal#0[1]
  ulp percentiles: p50=0 p90=0 p99=0 p100=0
  ulp histogram  : 0:11600
  non-finite     : 0 seen, 0 mismatched
tier1 stripper XST (7): the reactor-inlet alias
  cases          : 5800
  max rel err    : 0.000e0 at nominal#0[1]
  max ulp        : 0 at nominal#0[1]
  ulp percentiles: p50=0 p90=0 p99=0 p100=0
  ulp histogram  : 0:5800
  non-finite     : 0 seen, 0 mismatched
tier1 stripper TST (5, 7, 12)
  cases          : 2175
  max rel err    : 0.000e0 at nominal#0[1]
  max ulp        : 0 at nominal#0[1]
  ulp percentiles: p50=0 p90=0 p99=0 p100=0
  ulp histogram  : 0:2175
  non-finite     : 0 seen, 0 mismatched
tier1 stripper HST (5, 12): the column's own outlets
  cases          : 1450
  max rel err    : 0.000e0 at nominal#0[1]
  max ulp        : 0 at nominal#0[1]
  ulp percentiles: p50=0 p90=0 p99=0 p100=0
  ulp histogram  : 0:1450
  non-finite     : 0 seen, 0 mismatched
tier1 stripper HST (7): the reactor-inlet alias
  cases          : 725
  max rel err    : 0.000e0 at nominal#0[1]
  max ulp        : 0 at nominal#0[1]
  ulp percentiles: p50=0 p90=0 p99=0 p100=0
  ulp histogram  : 0:725
  non-finite     : 0 seen, 0 mismatched
ok
test the_non_condensible_factors_never_move_in_the_fortran_either ... SFR(1..3) fixed at [0.9950000047683716, 0.9909999966621399, 0.9900000095367432] across 300 nominal and 20 adversarial states
ok
test the_reactor_inlet_is_an_alias_in_the_fortran_too ... ok
test the_stripper_matches_the_fortran_over_all_three_pools ... transcendentals come from the platform libm
tier1 stripper SFR
  cases          : 19400
  max rel err    : 0.000e0 at nominal#0[1]
  max ulp        : 0 at nominal#0[1]
  ulp percentiles: p50=0 p90=0 p99=0 p100=0
  ulp histogram  : 0:19400
  non-finite     : 0 seen, 0 mismatched
tier1 stripper FTM (5, 12): the column's own outlets
  cases          : 4850
  max rel err    : 0.000e0 at nominal#0[1]
  max ulp        : 0 at nominal#0[1]
  ulp percentiles: p50=0 p90=0 p99=0 p100=0
  ulp histogram  : 0:4850
  non-finite     : 0 seen, 0 mismatched
tier1 stripper FTM (7): the reactor-inlet alias
  cases          : 2425
  max rel err    : 0.000e0 at nominal#0[1]
  max ulp        : 0 at nominal#0[1]
  ulp percentiles: p50=0 p90=0 p99=0 p100=0
  ulp histogram  : 0:2425
  non-finite     : 0 seen, 0 mismatched
tier1 stripper FCM (5, 12): the column's own outlets
  cases          : 38800
  max rel err    : 0.000e0 at nominal#0[1]
  max ulp        : 0 at nominal#0[1]
  ulp percentiles: p50=0 p90=0 p99=0 p100=0
  ulp histogram  : 0:38800
  non-finite     : 0 seen, 0 mismatched
tier1 stripper FCM (7): the reactor-inlet alias
  cases          : 19400
  max rel err    : 0.000e0 at nominal#0[1]
  max ulp        : 0 at nominal#0[1]
  ulp percentiles: p50=0 p90=0 p99=0 p100=0
  ulp histogram  : 0:19400
  non-finite     : 0 seen, 0 mismatched
tier1 stripper XST (5, 12): the column's own outlets
  cases          : 38800
  max rel err    : 0.000e0 at nominal#0[1]
  max ulp        : 0 at nominal#0[1]
  ulp percentiles: p50=0 p90=0 p99=0 p100=0
  ulp histogram  : 0:38800
  non-finite     : 0 seen, 0 mismatched
tier1 stripper XST (7): the reactor-inlet alias
  cases          : 19400
  max rel err    : 0.000e0 at nominal#0[1]
  max ulp        : 0 at nominal#0[1]
  ulp percentiles: p50=0 p90=0 p99=0 p100=0
  ulp histogram  : 0:19400
  non-finite     : 0 seen, 0 mismatched
tier1 stripper TST (5, 7, 12)
  cases          : 7275
  max rel err    : 0.000e0 at nominal#0[1]
  max ulp        : 0 at nominal#0[1]
  ulp percentiles: p50=0 p90=0 p99=0 p100=0
  ulp histogram  : 0:7275
  non-finite     : 0 seen, 0 mismatched
tier1 stripper HST (5, 12): the column's own outlets
  cases          : 4850
  max rel err    : 0.000e0 at nominal#0[1]
  max ulp        : 0 at nominal#0[1]
  ulp percentiles: p50=0 p90=0 p99=0 p100=0
  ulp histogram  : 0:4850
  non-finite     : 0 seen, 0 mismatched
tier1 stripper HST (7): the reactor-inlet alias
  cases          : 2425
  max rel err    : 0.000e0 at nominal#0[1]
  max ulp        : 0 at nominal#0[1]
  ulp percentiles: p50=0 p90=0 p99=0 p100=0
  ulp histogram  : 0:2425
  non-finite     : 0 seen, 0 mismatched
states skipped because the port would not converge: 0
ok

test result: ok. 5 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.17s

tier2_heat, platform libm

cargo test -p tepsim-oracle --features oracle,libm-system --release --test tier2_heat -- --nocapture --test-threads 1
running 5 tests
test both_sides_of_the_steam_cutoff_are_exercised ... steam on: 423 states, off: 2 states
ok
test every_level_branch_is_exercised_by_the_pool ... VLR at the 10% heat-transfer breakpoint              dry
VLR at the 50% heat-transfer breakpoint              ramp
TCC at the lower stripping-factor branch             fully wetted
TCC below the lower stripping-factor branch          fully wetted
TCC at the upper stripping-factor branch             fully wetted
TCC approaching the 177 C pole                       fully wetted
TCR at the shutdown limit                            fully wetted
TCR above the shutdown limit                         fully wetted
FTM(11) at the stripping-factor threshold            fully wetted
VLR at the upper shutdown limit                      fully wetted
VLR at the lower shutdown limit                      dry
VLS at the upper shutdown limit                      fully wetted
VLS at the lower shutdown limit                      fully wetted
VLC at the upper shutdown limit                      fully wetted
VLC at the lower shutdown limit                      fully wetted
PTR at the reactor pressure shutdown limit           fully wetted
PTV = PTR, the mixing-to-reactor flow clamp          fully wetted
PTR = PTS, the reactor-to-separator flow clamp       fully wetted
PTV = PTS, the recycle flow clamp                    fully wetted
PR = 1, the compressor reverse-flow clamp            fully wetted
PR = CPPRMX, the compressor maximum-ratio clamp      fully wetted
PR above CPPRMX, inside the clamped region           fully wetted
VLR below the lower shutdown limit                   dry
VLS below the lower shutdown limit                   fully wetted
VLC below the lower shutdown limit                   fully wetted
level branches reached: {"dry", "fully wetted", "ramp"}
ok
test heat_transfer_matches_the_fortran_over_all_three_pools ... transcendentals come from the platform libm
tier1 heat UAR
  cases          : 2425
  max rel err    : 0.000e0 at nominal#0[1]
  max ulp        : 0 at nominal#0[1]
  ulp percentiles: p50=0 p90=0 p99=0 p100=0
  ulp histogram  : 0:2425
  non-finite     : 0 seen, 0 mismatched
tier1 heat QUR
  cases          : 2425
  max rel err    : 0.000e0 at nominal#0[1]
  max ulp        : 0 at nominal#0[1]
  ulp percentiles: p50=0 p90=0 p99=0 p100=0
  ulp histogram  : 0:2425
  non-finite     : 0 seen, 0 mismatched
tier1 heat QUS
  cases          : 2425
  max rel err    : 0.000e0 at nominal#0[1]
  max ulp        : 0 at nominal#0[1]
  ulp percentiles: p50=0 p90=0 p99=0 p100=0
  ulp histogram  : 0:2425
  non-finite     : 0 seen, 0 mismatched
tier1 heat QUC
  cases          : 2425
  max rel err    : 0.000e0 at nominal#0[1]
  max ulp        : 0 at nominal#0[1]
  ulp percentiles: p50=0 p90=0 p99=0 p100=0
  ulp histogram  : 0:2425
  non-finite     : 0 seen, 0 mismatched
states skipped because the port would not converge: 0
ok
test the_algebra_is_bit_identical_once_exp_and_pow_agree ... tier1 heat UAR
  cases          : 725
  max rel err    : 0.000e0 at nominal#0[1]
  max ulp        : 0 at nominal#0[1]
  ulp percentiles: p50=0 p90=0 p99=0 p100=0
  ulp histogram  : 0:725
  non-finite     : 0 seen, 0 mismatched
tier1 heat QUR
  cases          : 725
  max rel err    : 0.000e0 at nominal#0[1]
  max ulp        : 0 at nominal#0[1]
  ulp percentiles: p50=0 p90=0 p99=0 p100=0
  ulp histogram  : 0:725
  non-finite     : 0 seen, 0 mismatched
tier1 heat QUS
  cases          : 725
  max rel err    : 0.000e0 at nominal#0[1]
  max ulp        : 0 at nominal#0[1]
  ulp percentiles: p50=0 p90=0 p99=0 p100=0
  ulp histogram  : 0:725
  non-finite     : 0 seen, 0 mismatched
tier1 heat QUC
  cases          : 725
  max rel err    : 0.000e0 at nominal#0[1]
  max ulp        : 0 at nominal#0[1]
  ulp percentiles: p50=0 p90=0 p99=0 p100=0
  ulp histogram  : 0:725
  non-finite     : 0 seen, 0 mismatched
ok
test the_condenser_driving_difference_is_large_enough_to_discriminate ... smallest gap between TST(8) and TCS as the driving temperature: 40.2906 C
ok

test result: ok. 5 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.20s

tier2_measurements, platform libm

cargo test -p tepsim-oracle --features oracle,libm-system --release --test tier2_measurements -- --nocapture --test-threads 1
running 5 tests
test the_algebra_is_bit_identical_once_exp_and_pow_agree ... tier1 XMEAS(1..22), noise-free
  cases          : 15950
  max rel err    : 0.000e0 at nominal#0[1]
  max ulp        : 0 at nominal#0[1]
  ulp percentiles: p50=0 p90=0 p99=0 p100=0
  ulp histogram  : 0:15950
  non-finite     : 0 seen, 0 mismatched
ok
test the_measurements_match_the_fortran_over_all_three_pools ... transcendentals come from the platform libm
tier1 XMEAS(1..22), noise-free
  cases          : 53350
  max rel err    : 0.000e0 at nominal#0[1]
  max ulp        : 0 at nominal#0[1]
  ulp percentiles: p50=0 p90=0 p99=0 p100=0
  ulp histogram  : 0:53350
  non-finite     : 0 seen, 0 mismatched
states skipped because the port would not converge: 0
ok
test the_pool_reaches_several_distinct_shutdown_causes ... TCR at the shutdown limit                            ["reactor pressure high"]
TCR above the shutdown limit                         ["reactor pressure high", "reactor temperature high"]
VLR at the upper shutdown limit                      ["reactor pressure high", "reactor level high"]
VLS at the upper shutdown limit                      ["separator level high"]
VLC at the upper shutdown limit                      ["stripper level high"]
VLR below the lower shutdown limit                   ["reactor level low"]
VLS below the lower shutdown limit                   ["separator level low"]
VLC below the lower shutdown limit                   ["stripper level low"]
shutdown causes reached: {"reactor level high", "reactor level low", "reactor pressure high", "reactor temperature high", "separator level high", "separator level low", "stripper level high", "stripper level low"}
ok
test the_shutdown_detector_agrees_with_the_fortran_on_every_state ... tier1 ISD as 0 or 1
  cases          : 2425
  max rel err    : 0.000e0 at nominal#0[1]
  max ulp        : 0 at nominal#0[1]
  ulp percentiles: p50=0 p90=0 p99=0 p100=0
  ulp histogram  : 0:2425
  non-finite     : 0 seen, 0 mismatched
12 states trip, 2413 do not
ok
test time_zero_is_what_suppresses_the_noise ... 22 of 22 measurements differ with the clock running
ok

test result: ok. 5 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.13s

tier2_balances, platform libm

cargo test -p tepsim-oracle --features oracle,libm-system --release --test tier2_balances -- --nocapture --test-threads 1
running 5 tests
test all_fifty_derivatives_match_the_fortran_over_all_three_pools ... transcendentals come from the platform libm
tier1 YP(1..50), relative to the derivative (reported)
  cases          : 120600
  max rel err    : 0.000e0 at nominal#0[1]
  max ulp        : 0 at nominal#0[1]
  ulp percentiles: p50=0 p90=0 p99=0 p100=0
  ulp histogram  : 0:120600
  non-finite     : 0 seen, 0 mismatched
tier1 YP(1..50), relative to the scale of the terms (the gate)
  cases          : 120600
  max rel err    : 0.000e0 at nominal#0[1]
  max ulp        : 0 at nominal#0[1]
  ulp percentiles: p50=0 p90=0 p99=0 p100=0
  ulp histogram  : 0:120600
  non-finite     : 0 seen, 0 mismatched
tier1 YP(1..50), plant frozen
  cases          : 650
  max rel err    : 0.000e0 at perturbed#369[1]
  max ulp        : 0 at perturbed#369[1]
  ulp percentiles: p50=0 p90=0 p99=0 p100=0
  ulp histogram  : 0:650
  non-finite     : 0 seen, 0 mismatched
2412 states running, 13 frozen, 0 skipped
ok
test every_derivative_slot_actually_moves_somewhere_in_the_pool ... 50 of 50 slots move somewhere in the pool
ok
test the_quirk_fix_changes_only_the_frozen_states ... the fix changes 8 tripping boundaries and leaves 17 alone
ok
test the_whole_right_hand_side_is_bit_identical_once_exp_and_pow_agree ... tier1 YP(1..50), relative to the derivative (reported)
  cases          : 35850
  max rel err    : 0.000e0 at nominal#0[1]
  max ulp        : 0 at nominal#0[1]
  ulp percentiles: p50=0 p90=0 p99=0 p100=0
  ulp histogram  : 0:35850
  non-finite     : 0 seen, 0 mismatched
ok
test tier2_acceptance_table ... Tier 2 acceptance, 2412 running states
gate: error / scale-of-terms < 1e-12

  YP   err/scale    err/value   ratio
   1   0.000e0   0.000e0          1x
   2   0.000e0   0.000e0          1x
   3   0.000e0   0.000e0          1x
   4   0.000e0   0.000e0          1x
   5   0.000e0   0.000e0          1x
   6   0.000e0   0.000e0          1x
   7   0.000e0   0.000e0          1x
   8   0.000e0   0.000e0          1x
   9   0.000e0   0.000e0          1x
  10   0.000e0   0.000e0          1x
  11   0.000e0   0.000e0          1x
  12   0.000e0   0.000e0          1x
  13   0.000e0   0.000e0          1x
  14   0.000e0   0.000e0          1x
  15   0.000e0   0.000e0          1x
  16   0.000e0   0.000e0          1x
  17   0.000e0   0.000e0          1x
  18   0.000e0   0.000e0          1x
  19   0.000e0   0.000e0          1x
  20   0.000e0   0.000e0          1x
  21   0.000e0   0.000e0          1x
  22   0.000e0   0.000e0          1x
  23   0.000e0   0.000e0          1x
  24   0.000e0   0.000e0          1x
  25   0.000e0   0.000e0          1x
  26   0.000e0   0.000e0          1x
  27   0.000e0   0.000e0          1x
  28   0.000e0   0.000e0          1x
  29   0.000e0   0.000e0          1x
  30   0.000e0   0.000e0          1x
  31   0.000e0   0.000e0          1x
  32   0.000e0   0.000e0          1x
  33   0.000e0   0.000e0          1x
  34   0.000e0   0.000e0          1x
  35   0.000e0   0.000e0          1x
  36   0.000e0   0.000e0          1x
  37   0.000e0   0.000e0          1x
  38   0.000e0   0.000e0          1x
  39   0.000e0   0.000e0          1x
  40   0.000e0   0.000e0          1x
  41   0.000e0   0.000e0          1x
  42   0.000e0   0.000e0          1x
  43   0.000e0   0.000e0          1x
  44   0.000e0   0.000e0          1x
  45   0.000e0   0.000e0          1x
  46   0.000e0   0.000e0          1x
  47   0.000e0   0.000e0          1x
  48   0.000e0   0.000e0          1x
  49   0.000e0   0.000e0          1x
  50   0.000e0   0.000e0          1x

worst component: YP(0) at 0.000e0 of its own scale
tier1 YP(1..50), relative to the scale of the terms (the gate)
  cases          : 120600
  max rel err    : 0.000e0 at nominal#0[1]
  max ulp        : 0 at nominal#0[1]
  ulp percentiles: p50=0 p90=0 p99=0 p100=0
  ulp histogram  : 0:120600
  non-finite     : 0 seen, 0 mismatched
0 of 50 components cancel by more than 100x
ok

test result: ok. 5 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.23s

Tier 3: the generator stream

This page is generated. cargo xtask validate --tiers 1,2,3 --smoke wrote it from commit b6ef4ee-dirty. Every number on it was captured from that run's own output. To change what it says, change what the suite measures and run the command again.

Both sides are instrumented to emit every generator draw, and the traces are diffed. This is the tier that catches a port whose arithmetic is right and whose call order is not, which no statistical comparison would find until after a 48-hour run.

Reduced volume. This run passed --smoke, so the sweeps are the short ones the CI gate uses rather than the full ones PLAN.org specifies. The case counts in the tables below are what actually ran. Drop --smoke for the gate volume.

Produced with rustc 1.97.1 (8bab26f4f 2026-07-14), gfortran 15.2.0. The oracle's compiler flags are fixed in crates/tepsim-oracle/build.rs and asserted by a test; changing them invalidates every number on this page, which is why it is a logged re-baseline and not an edit.

What ran

7 test binaries: 34 test(s) passed, 0 failed, 0 ignored.

targetlibmpassedfailedignored
rng_call_ordervendored700
tier3_harnessvendored600
tier1_disturbancevendored400
tier3_walkvendored400
tier3_walk_inputsvendored400
tier3_analysersvendored400
fault_tablevendored500

Figures

Every one of this tier's comparisons is bit-identical to the Fortran, so there is nothing to place on a logarithmic error axis and no error figure is drawn. The figure below states the same result in the units that suit it.

GENERATED by `cargo xtask validate --tiers 1,2,3 --smoke` from commit `b6ef4ee-dirty`. Do not edit by hand: the next run overwrites it. Tier 3: how many bits differA bar chart on a logarithmic count axis. Each bar is the number of comparisons whose result differed from the Fortran's by that many units in the last place, grouped by which libm the port was built against. 120,000 comparisons in total. Tier 3: how many bits actually differ 120,000 comparisons, by units in the last place. The count axis is logarithmic. vendored libm: 120,000 of 120,000 identical to the last bit 0 ULP 120,000 1 10 1e2 1e3 1e4 1e5 1e6 A bar exists only where the count is not zero, so a tier with one bar differed nowhere.
How many bits actually differ. The same comparisons counted by how many bits differ rather than by how much. A relative error is a ratio, and dividing by a large number makes any difference look small; a count of differing units in the last place cannot be flattered that way. This tier is straight-line arithmetic over constants already proved bit-identical, so the claim is that the only bar is the one at zero. A second bar appearing anywhere is a regression to chase rather than a tolerance to widen. Drawn by cargo xtask validate --tiers 1,2,3 --smoke at commit b6ef4ee-dirty; the measurement it repeats was first recorded in B-0028 and B-0029 (LOG.org).

Measurements

6 block(s), lifted from the transcripts below. The columns are whatever fields the run printed, so a new field in the reporter becomes a new column here rather than data this page drops.

The test column matters as much as the numbers, because not every row is the port being measured. Some tests deliberately mis-type a constant, or solve from the wrong guess, to show what that would cost; a row from one of those is supposed to be enormous, and its test name says so. The what column carries a tier1 prefix in every tier, because it is the shared comparison reporter's own label rather than a claim about which tier printed it.

targetfrom testwhatcasesmax rel errmax ulpulp percentilesulp histogramnon-finite
tier1_disturbancetesub5_matches_the_fortran_over_the_state_spacetier1 TESUB5 ADIST200000.000e0 at seed#0[ADIST]0 at seed#0[ADIST]p50=0 p90=0 p99=0 p100=00:200000 seen, 0 mismatched
tier1_disturbancetesub5_matches_the_fortran_over_the_state_spacetier1 TESUB5 BDIST200000.000e0 at seed#0[BDIST]0 at seed#0[BDIST]p50=0 p90=0 p99=0 p100=00:200000 seen, 0 mismatched
tier1_disturbancetesub5_matches_the_fortran_over_the_state_spacetier1 TESUB5 CDIST200000.000e0 at seed#0[CDIST]0 at seed#0[CDIST]p50=0 p90=0 p99=0 p100=00:200000 seen, 0 mismatched
tier1_disturbancetesub5_matches_the_fortran_over_the_state_spacetier1 TESUB5 DDIST200000.000e0 at seed#0[DDIST]0 at seed#0[DDIST]p50=0 p90=0 p99=0 p100=00:200000 seen, 0 mismatched
tier1_disturbancetesub5_matches_the_fortran_over_the_state_spacetier1 TESUB5 TNEXT200000.000e0 at seed#0[TNEXT]0 at seed#0[TNEXT]p50=0 p90=0 p99=0 p100=00:200000 seen, 0 mismatched
tier1_disturbancetesub6_matches_the_fortran_over_the_state_spacetier1 TESUB6 noise sample200000.000e0 at seed#0[X]0 at seed#0[X]p50=0 p90=0 p99=0 p100=00:200000 seen, 0 mismatched

Transcripts

Each block below is a test binary's own output, verbatim, with the command that produced it. The summary above is derived from these; they are not derived from it.

rng_call_order, vendored libm

cargo test -p tepsim-oracle --features oracle --release --test rng_call_order -- --nocapture --test-threads 1
running 7 tests
test a_time_zero_evaluation_draws_far_less_than_a_running_one ... draws at TIME=0: 0; at the scenario's own time: 264
ok
test a_tripped_plant_skips_the_noise_and_so_the_stream_position_depends_on_it ... tripped: [258, 258, 258, 258, 258, 258, 258, 258]
healthy: [522, 522, 522, 522, 522, 522, 522, 522, 522, 522, 522, 522, 522, 522, 522, 522, 522]
ok
test the_analysers_add_their_own_draws_on_schedule ... draws: t=0.05 264, t=0.15 462, t=0.30 522
ok
test the_draw_count_varies_across_the_trajectory ... draw counts over 400 nominal states: {0: 1, 264: 359, 294: 1, 462: 39}
ok
test the_draw_counter_agrees_with_the_oracle_generator ... ok
test the_measurement_noise_costs_exactly_264_draws ... with noise 264, without 0, difference 264
ok
test the_walk_advance_costs_thirty_draws_of_which_three_are_conditional ... t=0.15 with the walk frozen: 432; running: 462
ok

test result: ok. 7 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.04s

tier3_harness, vendored libm

cargo test -p tepsim-oracle --features oracle --release --test tier3_harness -- --nocapture --test-threads 1
running 6 tests
test both_scalings_appear_in_a_real_evaluation ... 30 signed draws, 432 unit draws
ok
test replaying_the_trace_reproduces_the_generator_word ... 113088 draws traced and replayed across 400 evaluations
ok
test the_differ_reports_the_first_divergence_and_its_kind ... ok
test the_trace_capacity_has_real_headroom ... worst evaluation: 462 draws against a capacity of 4096
ok
test the_trace_length_agrees_with_the_uninstrumented_census ... t=0: 0 draws, TIME=0: noise skipped, walks reset
t=0.000001: 264 draws, noise only
t=0.15: 462 draws, noise, walk advance and the gas analysers
t=0.3: 522 draws, and the product analyser
ok
test the_tracing_generator_records_without_disturbing ... ok

test result: ok. 6 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.04s

tier1_disturbance, vendored libm

cargo test -p tepsim-oracle --features oracle --release --test tier1_disturbance -- --nocapture --test-threads 1
running 4 tests
test an_inactive_channel_lands_exactly_on_its_centre_in_the_fortran ... ok
test tesub5_matches_the_fortran_over_the_state_space ... tier1 TESUB5 ADIST
  cases          : 20000
  max rel err    : 0.000e0 at seed#0[ADIST]
  max ulp        : 0 at seed#0[ADIST]
  ulp percentiles: p50=0 p90=0 p99=0 p100=0
  ulp histogram  : 0:20000
  non-finite     : 0 seen, 0 mismatched
tier1 TESUB5 BDIST
  cases          : 20000
  max rel err    : 0.000e0 at seed#0[BDIST]
  max ulp        : 0 at seed#0[BDIST]
  ulp percentiles: p50=0 p90=0 p99=0 p100=0
  ulp histogram  : 0:20000
  non-finite     : 0 seen, 0 mismatched
tier1 TESUB5 CDIST
  cases          : 20000
  max rel err    : 0.000e0 at seed#0[CDIST]
  max ulp        : 0 at seed#0[CDIST]
  ulp percentiles: p50=0 p90=0 p99=0 p100=0
  ulp histogram  : 0:20000
  non-finite     : 0 seen, 0 mismatched
tier1 TESUB5 DDIST
  cases          : 20000
  max rel err    : 0.000e0 at seed#0[DDIST]
  max ulp        : 0 at seed#0[DDIST]
  ulp percentiles: p50=0 p90=0 p99=0 p100=0
  ulp histogram  : 0:20000
  non-finite     : 0 seen, 0 mismatched
tier1 TESUB5 TNEXT
  cases          : 20000
  max rel err    : 0.000e0 at seed#0[TNEXT]
  max ulp        : 0 at seed#0[TNEXT]
  ulp percentiles: p50=0 p90=0 p99=0 p100=0
  ulp histogram  : 0:20000
  non-finite     : 0 seen, 0 mismatched
ok
test tesub6_matches_the_fortran_over_the_state_space ... tier1 TESUB6 noise sample
  cases          : 20000
  max rel err    : 0.000e0 at seed#0[X]
  max ulp        : 0 at seed#0[X]
  ulp percentiles: p50=0 p90=0 p99=0 p100=0
  ulp histogram  : 0:20000
  non-finite     : 0 seen, 0 mismatched
ok
test the_flag_changes_the_segment_but_never_the_draw_count ... ok

test result: ok. 4 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.03s

tier3_walk, vendored libm

cargo test -p tepsim-oracle --features oracle --release --test tier3_walk -- --nocapture --test-threads 1
running 4 tests
test every_walk_disturbance_matches_the_fortran_over_a_long_run ... IDV(8): 1572 walk draws over 30 hours
IDV(9): 1572 walk draws over 30 hours
IDV(10): 1572 walk draws over 30 hours
IDV(11): 1572 walk draws over 30 hours
IDV(12): 1572 walk draws over 30 hours
IDV(13): 1572 walk draws over 30 hours
IDV(16): 1572 walk draws over 30 hours
IDV(17): 1563 walk draws over 30 hours
IDV(18): 1543 walk draws over 30 hours
IDV(20): 1588 walk draws over 30 hours
ok
test the_spike_branch_is_reached_by_an_active_disturbance ... channel 10 over 75 hours: 66 fired segments, 1432 dwells
ok
test the_time_zero_reset_matches_including_its_draws ... the t=0 reset still drew 30 times
ok
test the_walk_advance_matches_the_fortran_along_the_nominal_trajectory ... 40 evaluations advanced a channel, 360 did not
ok

test result: ok. 4 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.36s

tier3_walk_inputs, vendored libm

cargo test -p tepsim-oracle --features oracle --release --test tier3_walk_inputs -- --nocapture --test-threads 1
running 4 tests
test every_disturbance_matches_over_a_long_run ... all twenty disturbances matched over 10 simulated hours each
ok
test the_generator_moves_once_per_step_not_once_per_evaluation ... ok
test the_two_composition_faults_move_different_amounts ... nominal [0.485, 0.005, 0.51]
IDV(1)  [0.45499999999999996, 0.005, 0.54]
IDV(2)  [0.48256281, 0.01, 0.50743719]
ok
test the_walk_driven_inputs_match_along_the_nominal_trajectory ... ok

test result: ok. 4 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.15s

tier3_analysers, vendored libm

cargo test -p tepsim-oracle --features oracle --release --test tier3_analysers -- --nocapture --test-threads 1
running 4 tests
test a_trip_silences_the_continuous_noise_and_not_the_analysers ... tripped 258 draws, healthy 522
ok
test a_whole_step_matches_the_fortran_along_the_nominal_trajectory ... 113088 draws over 400 whole steps; worst measurement 8.141509710325246e-15
ok
test a_whole_step_matches_with_every_disturbance_active ... all twenty disturbances, 6 simulated hours each; worst 0e0
ok
test the_port_stays_in_step_when_it_carries_its_own_state ... 2,000 steps carried on both sides, generators in lockstep throughout
ok

test result: ok. 4 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.18s

fault_table, vendored libm

cargo test -p tepsim-oracle --features oracle --release --test fault_table -- --nocapture --test-threads 1
running 5 tests
test a_sticking_fault_changes_nothing_when_the_command_never_moves ... ok
test step_faults_act_at_once_and_random_ones_do_not ... ok
test the_claimed_channels_are_the_ones_the_fortran_enables ... ok
test the_claimed_valves_are_the_ones_the_fortran_sticks ... ok
test the_five_unknown_faults_are_not_one_kind ... IDV(16) 'Unknown': channels [9], valves [] -- enables walk channel 9, the stripper steam valve capacity
IDV(17) 'Unknown': channels [10], valves [] -- enables spike channel 10, the reactor coolant duty
IDV(18) 'Unknown': channels [11], valves [] -- enables spike channel 11, the condenser coolant duty
IDV(19) 'Unknown': channels [], valves [5, 7, 8, 9] -- sticks valves 5, 7, 8 and 9; touches no equation in the model
IDV(20) 'Unknown': channels [12], valves [] -- enables spike channel 12, the reactor outlet flow
ok

test result: ok. 5 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

Quirk and delta register

The two halves of this register are cross-checked. cargo xtask deltas collects every @delta marker in the source, matches it against the ## D-0NN headings below, and fails if an entry has no marker, if a marker has no entry, or if the two disagree about the class. The collected table is the generated delta marker index. The prose below is still written by hand, which is what the closing paragraph of the next section is about; the cross-check and the index are not.

Every deliberate deviation from the original Fortran gets an entry here: what the original does, what this port does instead, the class, the measured effect, and the test that measures it. An entry with a description but no number is not finished.

The classes come from PLAN.org, "Quirk and delta register", and they decide how much caution an entry warrants:

ClassMeaningDisposition
ANo numerical effect. Dead code, naming confusion, reentrancy.Fixed without discussion.
BNumerically observable, semantically clearly wrong.Fixed, with a measured delta. Expected to measure as zero under normal operation, which is itself the thing to demonstrate.
CBehaviour-defining and benchmark-relevant.Implemented behind a flag, never the default, and released only on explicit sign-off after a full Tier 5 and Tier 6 delta report.

Each entry is anchored in the source by a @delta comment immediately above the item it applies to, in the same spirit as the @port claims that cargo xtask provenance collects:

#![allow(unused)]
fn main() {
// @port  teprob.f:1415-1442
// @delta D-001 class=B teprob.f:1439-1440
pub fn temperature_from_enthalpy(/* ... */) { /* ... */ }
}

PLAN.org calls for this page to be generated from those annotations. It is written by hand for now; the generator is a Phase 9 item, and the annotation convention exists from the first entry so that there is something to generate from when it arrives.

Open questions

Read this section before re-litigating a quirk. It is the list of things that have been noticed but not yet decided.

  • Nothing is open. D-001 through D-011 are all decided and measured. The last three decisions were taken on 2026-08-28: D-007 (B-0065), D-011 (B-0066), and what the Tier 5 battery does about a stuck valve (B-0067).
  • D-007's measure question, which was open here, was settled on 2026-08-27: Tier 2 measures error against the scale of the terms entering a balance, not against the balance's result, because a balance is inflow minus outflow and cancels. See B-0026a and the module documentation of crates/tepsim-oracle/tests/tier2_balances.rs, which carries the numbers.

D-001 — TESUB2 reports success after failing to converge

Class B. teprob.f:1439-1440, in TESUB2 (teprob.f:1415-1442).

What the original does

The Newton loop is written so that the convergence test is the loop-terminal statement:

      TIN=T
      DO 250 J=1,100
      ...
      T=T+DT
 250  IF(DABS(DT).LT.1.D-12)GO TO 300
      T=TIN
 300  RETURN

On convergence, GO TO 300 leaves the loop with the solved temperature. On failure the loop simply runs out, control falls through to T=TIN, and the routine restores the caller's original guess and returns exactly as it does on success. There is no error code, no status flag and no output argument that distinguishes the two. A caller receiving a temperature cannot tell whether it solved the problem or is holding its own input back.

The consequence is not confined to one number. TESUB2 is what converts the reactor, separator and stripper energy states into temperatures (teprob.f:460-465), so a silently abandoned solve seeds every downstream pressure, flow and heat-transfer term for that step with a stale temperature, and the run continues as though nothing happened.

What this port does

temperature_from_enthalpy returns Result<f64, TemperatureError>. Convergence returns Ok; exhausting the hundred iterations returns TemperatureError::DidNotConverge, carrying the guess, the last iterate and the final step, so a caller can report or recover. The iteration itself is unchanged: same evaluation order, same step, same criterion, same cap.

Measured effect

Zero. Across the full Tier 1 sweep the non-convergence path never fires, so the two behaviours never differ on the physical domain.

BasisStartCasesAbandonedRust vs Fortran
ITY=0warm9,987,49000 ULP
ITY=0cold9,987,49000 ULP
ITY=1warm9,987,49000 ULP
ITY=1cold9,987,49000 ULP
ITY=2warm9,987,49000 ULP
ITY=2cold9,987,49000 ULP

59,924,940 solves, none abandoned, every returned temperature bit-identical to the Fortran's. "Warm" starts Newton from the temperature the target enthalpy was built from, which is what every call site in the plant does; "cold" starts it from the opposite end of the 0-175 °C range, which nothing in the plant does and which is there to make the iteration work for its answer.

Round-trip accuracy, as a diagnostic rather than a gate: the warm start recovers the original temperature exactly, and the cold start lands within 3.7e-13 °C of it. Newton is quadratic on this problem, so the 1e-12 criterion on the step certifies an error far below itself.

Disposition

Adopted as the default. The fix is free: it changes no number the model can reach, and it converts a failure mode that is invisible by construction into one the type system forces a caller to handle.

The measurement is not a one-off. It is the assertion total_abandoned == 0 in the Tier 1 test below, so if a future change to the sweep, the constants or the iteration ever reaches the non-convergence path, it fails there rather than going unnoticed.

Measured by

crates/tepsim-oracle/tests/tier1_temperature.rs, run at full volume by cargo xtask validate --tiers 1. Two tests: one sweeps and counts, the other demonstrates the divergence directly on an unreachable target, asserting that the Fortran hands back the guess verbatim while the port reports an error.


D-002 — R1F and R2F name two unrelated quantities

Class A. teprob.f:503-511, in TEFUNC.

What the original does

R1F and R2F are set at teprob.f:415-416 from TESUB8(7, TIME) and TESUB8(8, TIME): the IDV(13) slow-drift multipliers on the reaction kinetics. They are used in that meaning at 503-504.

Five lines later they are reassigned in place:

      RR(1)=DEXP(31.5859536-40000.0/1.987/TKR)*R1F     ! drift factor
      RR(2)=DEXP(3.00094014-20000.0/1.987/TKR)*R2F     ! drift factor
      ...
      R1F=PPR(1)**1.1544                               ! now a pressure power
      R2F=PPR(3)**0.3735                               ! now a pressure power
      RR(1)=RR(1)*R1F*R2F*PPR(4)
      RR(2)=RR(2)*R1F*R2F*PPR(5)

The two roles share nothing but the storage. RR(1) at line 510 carries the drift factor once, from line 503, and the pressure powers once, from line 508.

Why it matters

There is no numerical effect, but there is a large reading effect. Taking R1F at line 510 to still be the drift factor gives

r1 = f1 · e^(a1 - E1/T) · f1 · pC^0.3735 · pD · Vv

which is a coherent-looking rate law, second order in the disturbance and missing the pressure order on A entirely. Nothing in the source contradicts it locally, and a port written that way is self-consistent: it reproduces its own answer on every run.

Measured: reading it that way moves RR by 100% relative on the adversarial pool, so the differential does catch it. But only because there is a differential. This is the kind of misreading that would have propagated through every later item in a port without one.

What this port does

Gives the two roles separate names. The drift factors arrive as kinetics::ReactionDrift, and the pressure powers are locals called order_a and order_c.

Measured effect

None. The arithmetic is identical; only the names differ.

The visible consequence is for harnesses, not for the model: after TEFUNC returns, COMMON's R1F holds a pressure power. Tier 2 therefore fetches the drift from TESUB8 directly, which is sound because nothing after teprob.f:406 writes the walk state.

Measured by

crates/tepsim-oracle/tests/tier2_kinetics.rs. Under --features oracle,libm-system the whole range is bit-identical to the Fortran, which is what confirms the reading: a wrong-but-consistent reading cannot be bit-identical to a right one.


D-003 — CRXR(2) is read but never assigned

Class A. teprob.f:521-527 and teprob.f:763.

What the original does

Seven of the eight net-production slots are written:

      CRXR(1)=-RR(1)-RR(2)-RR(3)
      CRXR(3)=-RR(1)-RR(2)
      CRXR(4)=-RR(1)-1.5D0*RR(4)
      CRXR(5)=-RR(2)-RR(3)
      CRXR(6)=RR(3)+RR(4)
      CRXR(7)=RR(1)
      CRXR(8)=RR(2)

CRXR(2) is not among them. It is read anyway, in the reactor component balance at teprob.f:763:

      YP(I)=FCM(I,7)-FCM(I,8)+CRXR(I)

for I = 1..8.

Why it works

CRXR lives in COMMON/TEPROC/, which is static storage and therefore zero-initialised, and nothing anywhere in the file ever writes slot 2. So B's net production is zero for the life of the process.

That is the correct physics: B is inert and takes part in none of the four reactions. But it is correct by static initialisation, not by statement. The guarantee comes from the linker, not from the model.

What this port does

States it. Kinetics::production is built from an explicit array of zeros and B is simply never written, which reproduces the value while making the reason local.

Measured effect

None, and that is asserted rather than assumed: the oracle is required to report exactly 0.0 for CRXR(2) on every sampled state. If it ever did not, the benign reading here would be wrong and the reactor's B balance would be picking up whatever was last left in that word.

Measured by

the_inert_has_no_net_production_in_either_implementation in crates/tepsim-oracle/tests/tier2_kinetics.rs. 200 nominal states, all exactly zero.


D-004 — the mixed feed carries 1e-10 lbmol/h that nothing accounts for

Class B. teprob.f:568-569, in TEFUNC.

What the original does

Every other valve-lagged flow is a clean proportionality:

      FTM(1)=VPOS(1)*VRNG(1)/100.0

The mixed A and C feed is not:

      FTM(4)=VPOS(4)*(1.D0-IDV(7)*0.2D0)
     .*VRNG(4)/100.0+1.D-10

The trailing +1.D-10 has no physical meaning. It is there so that FTM(4) is never exactly zero.

Why it is there

teprob.f:606 computes FCM(I,4)=XST(I,4)*FTM(4), and the mixing zone balance at teprob.f:783-788 sums those component flows. None of that divides by FTM(4), so the guard is not protecting a division in this range.

It protects the composition measurement path. With the valve shut and no epsilon, stream 4 contributes exactly nothing, and a controller reading a composition ratio off it would see 0/0. The epsilon keeps the stream infinitely dilute rather than absent.

Why it is Class B and not Class A

Because it is numerically observable, permanently. The plant receives 1e-10 lbmol/h of A, B and C that no feed valve delivers and no mass balance accounts for, for the entire run. Under normal operation FTM(4) is around 9.35 lbmol/h, so the addend is 1.1e-11 relative and utterly negligible; with the valve shut it is the entire flow.

That is the shape of a Class B quirk exactly: wrong in principle, invisible in practice, and the thing to demonstrate is that removing it measures as zero rather than assuming so.

What this port does

Reproduces it, as [tepsim_core::flows::FEED_FLOW_EPSILON], on the faithful path. The eventual fix belongs in Phase 6 behind a flag, with a Tier 5 delta report, like every other Class B item.

Measured effect

Not yet. The delta is measured when the fix lands (Phase 6, Tier 10). What is pinned now is the behaviour it produces: with valve 4 shut, FTM(4) is exactly 1e-10 and not zero.

Measured by

the_mixed_feed_never_reaches_exactly_zero in crates/tepsim-core/src/flows.rs, and the Tier 2 differential in crates/tepsim-oracle/tests/tier2_flows.rs, which is bit-identical to the Fortran under libm-system and would not be if the addend were dropped.


D-005 — SFR(4..8)'s initial values are dead

Class A. teprob.f:1129-1133, set in TEINIT.

What the original does

TEINIT sets all eight stripping factors:

      SFR(1)=0.99500
      SFR(2)=0.99100
      SFR(3)=0.99000
      SFR(4)=0.91600
      SFR(5)=0.93600
      SFR(6)=0.93800
      SFR(7)=5.80000D-02
      SFR(8)=3.01000D-02

The first three are load-bearing: nothing ever writes them again, and teprob.f:643 reads all eight on every evaluation, so A, B and C strip at those fixed fractions for the life of the run.

The last five are dead. teprob.f:614-634 writes SFR(4) through SFR(8) unconditionally, through whichever of its two branches it takes, before teprob.f:643 reads them. So the values on lines 1129-1133 are overwritten on the first evaluation and never observed.

Why it is worth an entry

Because the block looks uniform and is not. A reader checking the eight TEINIT lines against the eight slots would reasonably conclude that all eight are initial conditions, and a port that omitted lines 1129-1133 as dead would be correct while a port that omitted 1126-1128 would be silently wrong in the third decimal place of every product composition.

The split is also the only thing that explains why the loop at teprob.f:643 runs I=1,8 when the branch above it writes only five slots.

Note the precision changes across the boundary too: SFR(1..6) are single precision and SFR(7..8) are written 5.80000D-02 and 3.01000D-02, in double. Since the last five are dead, that inconsistency has no effect, which is itself worth knowing before someone spends time on it.

What this port does

Carries the three live values as [tepsim_core::stripper::NON_CONDENSIBLE_STRIPPING] and does not carry the five dead ones, with the reasoning stated where the constant is defined.

Measured effect

None. The five values are unobservable.

The three live ones are asserted rather than assumed: the oracle must report exactly the TEINIT constants for SFR(1..3) on every sampled state. If it ever did not, they would be recomputed somewhere the port has not found.

Measured by

the_non_condensible_factors_never_move_in_the_fortran_either in crates/tepsim-oracle/tests/tier2_stripper.rs. 300 nominal states and all 21 adversarial boundaries, bit-identical.


D-006 — XMEAS(20) is assigned twice, with different factors

Class A. teprob.f:698-699, in TEFUNC.

What the original does

      XMEAS(20)=CPDH*0.0003927D6
      XMEAS(20)=CPDH*0.29307D3

The first assignment is dead. The second overwrites it on the next line, before anything reads the slot.

Why it is not a harmless duplicate

The two factors are not the same number. 0.0003927D6 is 392.7 and 0.29307D3 is 293.07: a ratio of 1.34. So this is a superseded conversion rather than a repeated one, and a port that transcribed the first line would report compressor work 34% high on every sample, forever, while every other measurement stayed correct.

Both are plausible as unit conversions, which is what makes it a trap. Compressor duty in the model's internal units times 293.07 gives kilowatts, and XMEAS(20) is documented as "Compressor Work (kW)". The dead factor appears to be an earlier attempt at the same conversion.

What this port does

Takes the second, which is what the original computes, and states in [tepsim_core::measurements] that the first is dead and why the difference matters.

Measured effect

None on the model: the value is overwritten before use, so the derivative and every state are untouched. The effect would be entirely on a controller reading measurement 20, and the delta against the correct value would be 34%.

Measured by

the_compressor_work_uses_the_second_conversion_factor in crates/tepsim-core/src/measurements.rs, which also asserts the two factors are far enough apart that taking the dead one would be visible. The Tier 2 differential in crates/tepsim-oracle/tests/tier2_measurements.rs covers it against the oracle, and is bit-identical under libm-system.


D-007 — a shutdown freezes the plant instead of stopping it

Class C. teprob.f:807-811, in TEFUNC. Fixed by default since the sign-off of 2026-08-28 (B-0065); reproduced by QuirkFixes::faithful.

What the original does

      IF(ISD.NE.0)THEN
      DO 9030 I=1,NN
      YP(I)=0.0
 9030 CONTINUE
      ENDIF

When any of the eight shutdown conditions holds, all fifty derivatives become zero. The state stops moving, the clock keeps running, and the caller is told nothing: ISD is a local, and the returned vector is indistinguishable from a plant at perfect steady state.

Why it is Class C rather than B

Because published results depend on it. Every d00-d21 dataset was generated by a driver that kept integrating through a trip, and a run that ended instead would produce a shorter, different file. Changing this changes benchmark comparability, which is the definition PLAN.org gives for the class.

What this port does

Reproduces it, and says so. [tepsim_core::balances::Balances] carries the trip and a frozen flag alongside the derivative, so a caller never has to infer a freeze from a vector of zeros. B-0024a's typed [tepsim_core::measurements::ShutdownCause] means it can also say which limit fired, which the original cannot.

The fix is [tepsim_core::balances::QuirkFixes::trip_ends_the_run], on by default since 2026-08-28. PLAN.org required "a full Tier 5 and Tier 6 delta report and an explicit sign-off before it becomes the default"; both are below.

Reproducing the quirk is one call: QuirkFixes::faithful(), or Scenario::faithful() for a whole run, or tep run --freeze-on-trip. Every comparison against the Fortran or against published data uses it, and tier5::run_port pins it so no differential can accidentally run the fix.

The sign-off, and what decided it

Two facts, one from the delta measurement and one from the published files.

The fix is pure truncation. d007_changes_nothing_before_the_trip shows every sample up to the trip is bit-identical either way, so the flag decides how many numbers there are and never what they are. That removes the usual worry about a Class C fix, which is that it quietly changes results.

And the frozen tail is not a small artefact. Four of the forty-four published files carry one, 1,832 rows in total:

filerowsfrozen tailsharefrom row
d06_te.dat96068271.0%278
d06.dat48036375.6%117
d18_te.dat96057159.5%389
d18.dat48021645.0%264

Across those rows twenty-one continuous channels repeat the same five-digit values without any change at all, because the measurement noise at teprob.f:711-716 is inside the shutdown guard and stops with everything else. Only the three dead-time analysers keep moving. So three quarters of d06.dat is a stopped plant that reads as an unusually steady one, and a detector trained on that file spends most of its evidence on it. Data that cannot be told apart from good data, and is not, is worse than no data.

The counter-argument was restartability, and it does not survive contact: the plant cannot be restarted after a freeze either. The freeze does not preserve an option, it only withholds the fact that the run is over.

Note that B-0025's backlog entry originally said the freeze should not be the default. That is the opposite of what PLAN.org says, and PLAN.org is the design of record. It is also the only reading Tier 2 can live with: the adversarial pool contains thirteen states that trip, and a port that did not freeze would disagree with the oracle on all fifty components for each of them.

Measured effect

  • The freeze fires on exactly the states the Fortran freezes, over all three pools: 2,412 running and 13 frozen, with no disagreement.
  • Turning the fix on changes the derivative on all 8 tripping adversarial boundaries and on none of the 17 that do not trip.
  • Every sample before the trip is bit-identical with the fix on and off, so the delta is entirely truncation. tier10_quirk_deltas.rs tabulates which scenarios trip, at which step, and what fraction of the run is discarded.
  • The Tier 5 battery cannot measure this delta, and that is a property of the delta rather than of the battery: every statistic compares two ensembles of the same shape, and the fix makes one of them shorter. A KS statistic between a 960-sample run and a 278-sample one measures the truncation.

A second, open question this exposed

Comparing the assembled derivative revealed that 28 of the 50 components exceed Tier 2's 1e-12 relative gate under the vendored libm, worst YP(2) at 1.393e-4 — while the whole right-hand side is bit-identical to the Fortran under libm-system.

That is cancellation, not error: a balance is inflow minus outflow, and near steady state those nearly agree, so a one-ULP difference in each term is 1e-16 of the terms and 1e-4 of the result. The 22 components that do meet 1e-12 are exactly the ones that do not cancel.

Choosing what Tier 2 should measure against for a cancelling quantity changes what Tier 2 means, so it is recorded here and left open. B-0026a.

Measured by

crates/tepsim-oracle/tests/tier2_balances.rs, and the_fix_is_off_by_default_and_changes_the_answer_when_on in crates/tepsim-core/src/balances.rs.


D-008 — CONTRL22 is defined, tuned, and never called

Class A. temain_mod.f:1295-1332, with its constants at 246-317.

What the original does

Twenty controller subroutines are defined. The main loop calls nineteen of them. CONTRL22 is not among the calls.

It is not a stub. The main program initialises its full tuning alongside every other loop's:

      SETPT(12)=2633.7
      GAIN22=-1.0	  * 5.
      TAUI22=1000./3600.
      ERROLD22=0.0

and the subroutine is a complete PI controller reading XMEAS(13), the separator pressure, and writing XMV(6), the purge valve.

What it looks like it was

XMV(6) is the valve CONTRL6 already owns, and CONTRL6 carries a latching pressure override (temain_mod.f:710-731) that does the same job by a different mechanism. The override's release threshold is 2633.7, which is exactly CONTRL22's setpoint.

CONTRL22 is also the only one of the twenty whose error is not normalised by a span: every other loop computes (SETPT - XMEAS) * 100 / span, and this one computes the raw difference.

Two loops on one valve, one of them differently shaped from all the others, and one of them disconnected. The straightforward reading is that CONTRL22 was the separator-pressure controller, that the override in CONTRL6 replaced it, and that it was left in place with its tuning intact rather than deleted.

Why it matters

Because "nineteen control loops" and "twenty control subroutines" are both true and a port has to pick. Counting the subroutines gives a plant with two controllers fighting over the purge valve. Counting the calls gives the published behaviour.

It also shifts the reading of the control structure. Separator pressure is not under continuous control in this scheme; it is under an on-off override with a 650 kPa deadband. Anyone reasoning about why the plant behaves as it does around the pressure limits needs that.

What this port does

Ports it, and does not schedule it. It is in [tepsim_control]'s tuning table and in the Tier 1 differential, because covering it costs nothing and it is the only check that will ever exercise it, but the scheduler does not call it.

Measured effect

None: it is not called, so it computes nothing.

The differential covers it anyway, at 0 ULP over 500 tunings, which is the only evidence anywhere that the subroutine is correct.

Measured by

every_controller_matches_the_fortran_over_a_sweep in crates/tepsim-oracle/tests/tier1_control.rs includes CONTRL22. crates/tepsim-oracle/tests/driver_binding.rs shows it is callable and that the driver's setpoint array has a slot for it.

D-009 — the driver starts from rounded valve positions, not TEINIT's

Class A. temain_mod.f:322-332.

What the original does

TEINIT leaves the twelve valve commands at the values YY(39..50) carries, which are written to eight significant figures:

      DATA YY /
     .  ...
     .  6.3053638D+01, 5.3980356D+01, 2.4644630D+01, ...

The driver then overwrites eleven of the twelve, by hand, at five:

 	XMV(1) = 63.053 + 0.
	XMV(2) = 53.980 + 0.
	XMV(3) = 24.644 + 0.
	...
	XMV(11)= 18.114 + 0.

XMV(12), the agitator, is not in the list and keeps TEINIT's exact 50.

Every literal is fixed-form Fortran with no exponent letter, so each is a REAL(4) widened to double. 63.053 reaches the plant as 63.053001403808594, not as 63.053.

Why it matters

A closed-loop run and an open-loop run do not start from the same plant. The difference is in the fourth decimal place of ten valve commands, which is far too small to see in any plot and far too large to ignore in a differential: a port that starts a closed-loop run from TEINIT's values disagrees with the Fortran from step 1, and the disagreement then grows through the controllers.

Ten of the eleven, not all eleven: TEINIT leaves YY(43) = 22.21000000, which is already five significant figures, so 22.210 rounds to the same f32. That coincidence is the clearest available evidence that these numbers are TEINIT's rounded rather than an independently chosen operating point.

The + 0. on every line appears to be a placeholder for a perturbation. It changes nothing, and it is transcribed rather than simplified away, because single(63.053 + 0.) and single(63.053) + single(0.) are the same value only because the addend is zero.

What this port does

Reproduces it, as [tepsim_control::DRIVER_INITIAL_VALVES], with each literal passed through single(). Driver::new starts there.

Measured effect

Ten of the twelve valve commands differ from TEINIT's, all by less than one part in a thousand of range:

123456789101112
gap3.70e-42.94e-44.41e-47.63e-502.52e-43.43e-41.56e-42.63e-41.87e-45.09e-40

The largest is XMV(11) at 5.09e-4; the smallest non-zero is XMV(4) at 7.63e-5. XMV(5) and XMV(12) are bit-identical, for the two different reasons above.

Measured by

the_driver_starts_from_rounded_valve_positions in crates/tepsim-control/src/lib.rs, which asserts the count of differing valves and the two exceptions individually, and the_rounding_gaps_are_recorded beside it, which prints the table above.

D-010 — the controllers read the previous step's measurements

Class A, and load-bearing. temain_mod.f:366-411.

What the original does

The main loop, in order:

        DO 1000 I = 1, NPTS
	  TEST=MOD(I,3)
	  IF (TEST.EQ.0) THEN
		CALL CONTRL1
	  	...
	  ENDIF
          ...
	  CALL INTGTR(NN,TIME,DELTAT,YY,YP)
 	  CALL CONSHAND
 1000 CONTINUE

XMEAS is written by TEFUNC, which INTGTR calls. So on iteration I the controllers read the measurements iteration I - 1 produced. Every loop in the scheme carries one plant step of dead time that is nowhere in any controller.

CONSHAND, the valve clamp, likewise runs after the integration rather than after the controllers.

Why it matters

This is not a subtlety that costs a few ULP. Feeding the controllers the measurements of the step they are about to cause makes every loop one sample tighter than the original. B-0039 measured it: XMV(7) lands on 35.62 instead of 34.15 on the very first controller fire, a 1.5% of range error before the plant has run three seconds, and XMEAS(14) is 23% out four hours later. That is a different plant, not a rounding of the same one.

The clamp's placement is subtler. TEFUNC clamps its own copy of the valve positions at teprob.f:803-804, so with no sticking fault active it makes no observable difference where CONSHAND runs. It stops being unobservable under IDV(14), IDV(15) or IDV(19): teprob.f:801 only moves VCV toward XMV when the two differ by more than the stick threshold, and an unclamped XMV of 105 crosses that threshold at a different moment than a clamped 100 does.

What this port does

Reproduces both, in [tepsim_control::Driver] rather than in Scheme. Driver::control takes the previous step's measurements and does not clamp; Driver::settle is CONSHAND and is called with the plant already advanced. The split exists so that the ordering is a thing the type makes explicit rather than a convention a caller has to remember.

Measured effect

With the two orderings otherwise identical, the first CONTRL7 fire gives 34.147823842 (previous-step, matching the Fortran bit for bit) against 35.623679189 (current-step).

Measured by

the_controllers_read_the_previous_steps_measurements in crates/tepsim-oracle/tests/tier4_closed_loop.rs, which requires the two orderings to give different answers before checking which one is right.

D-011 — the driver switches IDV(12) on eight hours in, whatever you asked for

Class C. Not reproduced by default since the sign-off of 2026-08-28 (B-0066); reproduced by Scenario::faithful. temain_mod.f:366-368, with SSPTS at line 226.

What the original does

The first statement in the simulation loop body, before any controller:

        DO 1000 I = 1, NPTS
         IF (I.GE.SSPTS) THEN
                 IDV(12)=1
          ENDIF

with

      SSPTS = 3600 * 8

At a one-second step that is eight simulated hours. IDV(12) is the condenser cooling water inlet temperature random variation. The assignment is unconditional: it does not consult the scenario, and there is no way to run this driver past eight hours without it.

The header comment at line 98 explains SSPTS as "the number of data points to simulate in steady state operation before implementing the disturbance", so a disturbance was clearly meant. But which disturbance is not a parameter; it is IDV(12), written into the loop.

Why it matters

Every published closed-loop dataset generated with this driver and longer than eight hours carries IDV(12), including the runs nominally labelled fault-free. Anyone comparing against published data needs the quirk; anyone running a controlled experiment on some other disturbance needs it gone, because after hour eight they are running two disturbances and reporting one.

It also interacts with the fault the caller asked for. If the scenario is IDV(4) (a step in reactor cooling water inlet temperature), then from hour eight the run is IDV(4) and IDV(12), and both act on cooling water.

The effect does not begin at hour eight

IDV(12) reaches the plant through IDVWLK(6) (teprob.f:351), and the walk that gates is only redrawn when TIME passes that channel's TNEXT (teprob.f:359-360). So switching the flag on at step 28,800 changes nothing until channel 6's next segment boundary, which for the nominal seed is step 29,390, about ten minutes later. Both the port and the Fortran part from their fixed counterparts at exactly that step.

That matters for anyone trying to locate the quirk in a dataset by eye: the visible onset is not at the eight-hour mark, and it moves with the seed.

What this port does

Can do either, and does not by default. [tepsim_control::Driver] forces IDV(12) on at [tepsim_control::STEADY_STATE_STEPS], and [tepsim_control::DriverQuirks::only_the_requested_disturbances] turns that off; [tepsim::Scenario::driver_forces_idv12] is the facade's control and is false by default. Scenario::faithful() and tep run --force-idv12 reproduce the driver as shipped, and tier5::run_port pins it on so every differential against the Fortran carries it. [tepsim_control::Driver::scenario_is_overridden] reports when the driver has gone beyond what was asked for, so a caller never has to infer it.

Measured effect

Ten simulated hours, nominal scenario, one-second Euler, faithful against fixed, everything else identical:

first differencestep 29,390, XMEAS(22) (condenser cooling water outlet temperature)
identical before thatevery bit of all 41 measurements, all 29,389 steps
worst over the run1.092e-1 relative, at XMEAS(37)
worst at hour ten3.262e-2 relative, at XMEAS(38)

Ten percent on a product-composition measurement is not a rounding difference. It is a different experiment.

The sign-off, and what decided it

The question was whether the shipped default should be temain_mod.f's behaviour or the caller's scenario. Decided on 2026-08-28: the caller's scenario.

One of the two original arguments had already been shown to be false. The case for keeping the quirk was that reproducing published data requires it. Tier 7 (B-0051) established that the published d00 through d21 files carry IDV(12) only in d12 and d12_te: they were generated with temain_mod.f:367 replaced, not kept.

Two independent predictions support that. d12_te's spread jumps at row 160, where IDV(12) would arrive, by at least 5.3 times what d00_te's does on every channel the disturbance drives. And keeping the line inflates the port's spread against d00 by up to 9.9 times, against 1.55 times for replacing it. Removing it roughly triples the agreement across the whole comparison table, for example d17_te from a Kolmogorov-Smirnov median of 0.658 to 0.044.

So the decision is now between two different goals rather than between fidelity and convenience:

  • Reproduce the source. temain_mod.f as shipped does force IDV(12), and this project is a port of that source. Keeping the default means the port does what the code it was ported from does.
  • Reproduce the data. Most people who use the Tennessee Eastman problem use the published datasets, not the driver. Matching them means turning it off.

The second was chosen, on three grounds. The evidence is one-sided: the only argument for forcing was that the shipped line exists, and the published bytes say the line was replaced when the data was made. The prose at temain_mod.f:101-102 agrees, telling the reader to "go to line 367" and put their own disturbance there, which is an instruction to replace. And the cost of being wrong is asymmetric: a caller who asks for IDV(4) and silently gets IDV(4) and IDV(12) has no way to notice, whereas a caller who wants the shipped driver's behaviour asks for it by name and gets exactly that.

Both remain reachable, which was the one firm constraint: Scenario::faithful, tep run --force-idv12, driver_forces_idv12=True from Python, and the idv12 field of the scenario text, which still parses old links to the scenario they always named.

The numbers above are Tier 4. The Tier 5 measurement PLAN.org asks for on a Class C delta is d011_the_forced_disturbance_moves_the_plant_by_a_tenth in crates/tepsim-oracle/tests/tier10_quirk_deltas.rs, which runs the paired battery with the flag on and off and requires the shift to exceed a tenth of a margin on some channel: forcing IDV(12) is not cosmetic, and the test fails if it ever looks that way.

Measured by

the_driver_forces_idv12_at_the_eight_hour_mark and the_forced_disturbance_changes_the_plant_measurably in crates/tepsim-oracle/tests/tier4_closed_loop.rs. The second cross-checks the onset step against two Fortran runs that differ in the same way, so 29,390 is ground truth rather than an artifact of the port.

the_published_files_were_not_generated_with_the_forced_idv12 in crates/tepsim-oracle/tests/tier7_published.rs is what established that the published datasets do not contain it.

Delta marker index

This page is generated. cargo xtask deltas wrote it from commit 7c13d33-dirty. Every row was collected from the source and from the register by that run.

Every deliberate deviation from the original Fortran carries two things: an entry in the quirk and delta register, and a @delta marker on the code it applies to. This table is collected from the markers and checked against the register. cargo xtask deltas fails if an entry has no marker, if a marker has no entry, or if the two disagree about the class.

11 register entries, 11 markers across 11 source location(s): 7 class A, 2 class B, 2 class C.

deltaclassdeviates frommarked atregister
D-001Bteprob.f:1439-1440crates/tepsim-core/src/thermo.rs:370TESUB2 reports success after failing to converge
D-002Ateprob.f:503-511crates/tepsim-core/src/kinetics.rs:217R1F and R2F name two unrelated quantities
D-003Ateprob.f:521-527crates/tepsim-core/src/kinetics.rs:218CRXR(2) is read but never assigned
D-004Bteprob.f:568-569crates/tepsim-core/src/flows.rs:255the mixed feed carries 1e-10 lbmol/h that nothing accounts for
D-005Ateprob.f:1129-1133crates/tepsim-core/src/stripper.rs:121SFR(4..8)'s initial values are dead
D-006Ateprob.f:698crates/tepsim-core/src/measurements.rs:238XMEAS(20) is assigned twice, with different factors
D-007Cteprob.f:807-811crates/tepsim-core/src/balances.rs:298a shutdown freezes the plant instead of stopping it
D-008Atemain_mod.f:1295-1332crates/tepsim-control/src/lib.rs:942CONTRL22 is defined, tuned, and never called
D-009Atemain_mod.f:322-332crates/tepsim-control/src/lib.rs:1076the driver starts from rounded valve positions, not TEINIT's
D-010Atemain_mod.f:366-411crates/tepsim-control/src/lib.rs:1135the controllers read the previous step's measurements
D-011Ctemain_mod.f:366-368crates/tepsim-control/src/lib.rs:1274the driver switches IDV(12) on eight hours in, whatever you asked for

The class column comes from the register. A row reading unmarked or undocumented is not a formatting artefact: it is a half of the register that is missing, and the run that wrote this page exited non-zero saying so. The page is written before the cross-check so that the evidence survives the failure.

Class A has no numerical effect, class B is numerically observable and fixed with a measured delta, and class C is behaviour-defining, reproduced by default and changed only behind a flag on explicit sign-off. PLAN.org defines them.