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

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.