Labels¶
Real plant data comes without answers: nobody knows exactly when a fault started to show, how long a loop took to settle, or which feed a lab sample came from. homeostat.labelers derives these answers from the ground truth of a run. Labelers are plugins built on the public core API.
Fault visibility¶
visibility(scenario) answers, for every fault in a scenario, when it becomes visible in the recorded data, and on which signal.
labels = visibility("scenario.yaml", window=1800)
labels.summary() # per fault: onset, first_visible, delay, first_signal, visible_on
labels.table # per fault and recorded signal: first_visible, delay, visible, max_z
labels.effects # per fault: its exact effect on every recorded signal
It runs the scenario with one extra lane per fault: a twin without that fault, on exactly the same noise. The difference between the two lanes is the fault's exact effect on each signal, zero before its onset. A fault is visible on a signal once its effect stands out from the signal's own variation over a moving window, either as a shift or drift of the mean or as a change of variance, and stays visible for persistence seconds (half the window by default). first_visible is the start of that period.
This is oracle detectability: an upper bound for any detector, which knows neither the twin nor the fault. Faults too small to stand out are labelled not visible, so a benchmark does not count them as missed detections.
The labels often surprise. A transmitter bias is never visible on the transmitter itself, because the loop holds that reading at its setpoint; it shows on the controller output instead.
visibility needs a single-lane scenario; it adds the twin lanes itself.
Settle times¶
settle_times(run) measures, for each transition in the plan's timeline and each loop, how long the loop's true regulated variable takes to reach its new setpoint and stay there:
The variable must stay within band of the new setpoint (by default 1 % of it) for hold seconds. The settle time counts from the start of the transition, so it is never shorter than the ramp; it is NaN when the loop does not settle before the next transition.
Feasibility¶
feasibility(run) checks, per lane and loop, whether the loop starts at its setpoint without a saturated controller output. A plan or a drawn plant can ask for more than the plant can do, for example a setpoint its valve cannot reach; this finds such lanes. In a family the result is in Family.lanes["feasible"].
Material lineage¶
A lab result describes material that passed through the plant earlier, at times that depend on the flows. lineage(scenario, sample) gives that alignment exactly:
lin = lineage("examples/product_quality.yaml", sample="AT-301")
lin.table # per reported value: reported_at, value, sampled_at, entered.<unit>, residence.<unit>
For every unit upstream of the sample, a passive tracer (the clock at the unit's inlet) is carried by copies of every step that shapes timing on the material's way to the sample: plug flow, back-mixing, a sample-and-hold, an analysis delay. At the sample, the tracer is the time the sampled material entered that unit. sampled_at is when the sample was taken, from the instrument's sampling stage on, and residence.<unit> is the time the material spent between entering the unit and being sampled.
The tracers are ordinary signals built from core operators, marked so that they never appear among the recorded signals. They read 0 while the plant settles, so their values are exact once the material present at the start has left the path.