Skip to content

Hc1012/SafePlant-Agent

Repository files navigation

SafePlant Agent

Tests

AI proposes. Physics simulator validates. Safety gate decides.

A simulator-gated fault-diagnosis and action-selection system for chemical process control, built on a first-principles exothermic CSTR with a cooling jacket. The project combines classical monitoring, physics residuals, fault diagnosis, an LLM proposer, counterfactual simulator rollouts, and an independent true-state interlock.

Status: complete — final sealed evaluation finished. The frozen system was evaluated once on untouched seeds 100–109: 110 plant runs, 101 first-alarm agent scenarios, 149 cached LLM responses, and zero response failures. No post-reveal tuning or reruns are permitted. Canonical evidence is under results/final/; the release summary is in docs/FINAL_RESULTS.md.

Final sealed evaluation safety comparison

The final result is deliberately honest: the simulator gate blocked 31 unsafe first LLM proposals, but the LLM proposer did not outperform the simple baselines overall. It tied always-continue on safety (0.6436), while always-max-cool achieved the best safety (0.6832) and the fixed rule table the lowest mean regret (224.26). The main engineering result is therefore not “the LLM won”; it is that simulator gating made proposed actions auditable and prevented many unsafe executions, while still exposing model/hypothesis misses instead of hiding them.

Final SafePlant architecture

Quickstart

python -m venv .venv
# Windows: .\.venv\Scripts\Activate.ps1
pip install -r requirements-lock.txt
pip install -e .
pytest -q                 # 105 tests in the frozen release
# On a source-only clone, data-dependent integration tests skip until artifacts are generated.
python scripts/day1_verification.py       # steady state, eigenvalues, S-curve (the gate)
python scripts/make_figures.py            # week-1 headline runs + figure
python scripts/calibrate_confounders.py   # benign-disturbance survivability sweep
python scripts/make_week2_figures.py      # sensor-drift headline + fault fingerprints
python scripts/generate_dataset.py --split train   # 110-run labelled dataset
python scripts/run_detection.py                   # detection benchmark (Part A+B+C)
python scripts/run_diagnosis.py                   # diagnosis: support table + ablation
python scripts/make_week4_figure.py               # diagnosis ablation + per-severity recall
python scripts/develop_llm_prompt.py              # LLM: build/freeze bundle + plan (API only with --confirm)
python scripts/run_llm_diagnosis.py               # Week 5: single validation pass (already frozen)
python scripts/build_agent_scenarios.py             # Week 6: first-alarm scenarios (offline)
python scripts/run_agent_offline.py                 # Week 6: dry plan; add --confirm for baselines/oracle
python scripts/develop_agent_prompt.py              # Week 6: dry plan; add --confirm for LLM dev
python scripts/run_agent_eval.py                    # Week 6 validation harness (already completed)
python scripts/make_final_figures.py                # rebuild release figures from frozen JSON only
python scripts/run_scenario.py configs/scenarios/faults/f1_drift_severe.yaml
# Final sealed evaluation is already complete. Do not rerun it for a new score.

Every run writes trajectory.parquet, a provenance manifest.json (config SHA-256s, seed, git SHA + dirty flag, SHA-256 of the parquet file itself, and numpy/scipy/pandas/PyYAML/pyarrow versions), and an envelope_report.json (including the dense solver-resolution peak temperature) to out/runs/<run_id>/. Same config + same seed → bit-identical trajectory, enforced by test.

The plant, briefly

Three-state exothermic CSTR (A → B) with a dynamic cooling jacket: states (C_A, T, T_j), manipulated variable q_c (coolant flow), canonical literature reactor parameters plus a constructed-and-verified jacket. The design point (0.5 mol/L, 350 K, 300 K at q_c = 59.81 L/min) is an open-loop-unstable saddle (leading eigenvalue +3.10 /min, doubling time 13 s) with three coexisting steady states — the controller is load-bearing and cooling loss is genuinely dangerous. Sensors are noisy and sampled (composition analyser deliberately slow at 1 min); the safety interlock is a separate layer acting on true state with a latching max-cooling + feed-cut action.

What week 1 found

Single-loop PI from T to q_c cannot stabilise this plant — every tuning ends in a growing reactor–jacket limit cycle; a cascade (the real-world architecture for jacketed reactors) is required. The loop has essentially no phase budget: a 0.2-min measurement filter or any valve slew limit destabilises it. Setpoint authority is asymmetric (+2 K achievable, +5 K physically not). The dynamic coolant-supply-temperature margin (~+2–3 K) is about a third of the static one (+7 K). And the trip system's real guarantee is termination to cold shutdown from every studied state — peak temperature is set by the state at trip, not by cooling capacity. Numbers, methods, and the two anti-windup bugs found along the way: docs/DESIGN.md. Every departure from the original spec — including the formal proof that the specified single-loop PI cannot work — is registered with evidence in docs/DEVIATIONS.md.

What week 2 found

Five fault classes, each entering the loop through one of three hooks (sensor transform_measurement, plant on_step, actuator transform_actuation), with the interlock called on true state in between so it can never be blinded by a faulty sensor. Ground truth is recorded at injection — a manifest fault block plus per-step fault_active/fault_value columns — never inferred from the trajectory.

Confounder calibration was the week's sharpest correction. The roadmap's ±10 % feed-composition confounder trips the plant 10/10 (the week-1 dynamic-margin finding predicted it); even +5 % trips 1/10, and the +5 K feed-temperature confounder trips 2/10 — contradicting a week-1 single-seed pass that happened to survive. Both positive bounds were cut to the measured trip-free limit (+4 % and +4 K). Other measured findings: F2 fouling exhausts control authority at f ≈ 0.88–0.91 (dynamic, slightly earlier than the static prediction), and its rate trip out-runs the saturation alarm meant to warn of it; F3 stiction is invisible in temperature until a cliff at J≈40 locks the valve, after which the outcome is a stochastic quench-vs-runaway split (3/10 seeds trip). Full numbers, the fault matrix, and the calibration table: docs/DESIGN.md §§8–12. Every bound reduction and interface change is registered in docs/DEVIATIONS.md (D5–D7).

The train dataset is 110 runs (11 conditions × 10 seeds): on seeds 0–9, every mild fault was a clean 0/10 negative, the runaway severe faults were 10/10, and F3 severe was a deliberate 3/10. The sealed eval later showed that realised trip outcomes can shift across unseen seeds. The train/eval seed split (0–9 / 100–109) is fixed before any detector exists — no detector, in any week, is tuned on eval seeds.

What the final sealed evaluation found

The full architecture was frozen before seeds 100–109 were generated. The reveal produced 110 runs, 39 trips, 101 first-alarm scenarios, and 9 no-alarm runs. All learned components were fit only from the train root; the LLM action bundle remained fixed at 04568a87e823f2d0d267fae4fb126dbfd775c9dd7d68e575b575e92880ebab85.

proposer safety rate ↑ mean regret ↓ trips averted
B1 always max-cool 0.6832 241.02 11
B2 diagnosis→action table 0.6634 224.26 9
LLM first proposal 0.6436 243.67 7
B0 always continue 0.6436 251.86 7
LLM after one retry 0.6337 244.40 6

The strong 31-scenario validation result did not generalise: LLM-first safety fell from 0.7742 to 0.6436 and no longer beat the baselines. The retry again made the policy worse. This is recorded as a failed prediction, not reframed. The gate nevertheless made 31 genuine saves against LLM-first proposals, with 7 gate misses and 37 over-blocks. F4 severe remained unmitigable for every proposer, confirming that catastrophic fast coolant loss belongs to the independent interlock rather than the action agent.

Final mean regret comparison

Final gate outcomes

Supporting final perception results remained coherent: the frozen RF diagnoser scored macro-F1 0.6832 on 1,025 eligible eval windows; F1/F2/F5 were strong, mild F3/F4 remained weak, and F4 severe was still not scorable. The final actuator residual again caught F4 severe 10/10 fault-only at a median 0.3 min while MSPC caught 0/10. Full tables, hashes, and interpretation: docs/FINAL_RESULTS.md and docs/DESIGN.md §§28–32.

What week 6 validation found

Before the sealed reveal, the frozen action layer was validated once on 31 first-alarm scenarios from seeds 7–9. LLM-first achieved safety 0.7742, mean regret 98.71, and 5 trips averted; it tied always-max-cool on safety while using less costly actions. That result justified the final sealed test, but it is not the final headline because it did not survive the larger 101-scenario evaluation. The validation cache and audit remain frozen under results/week6/ as evidence of the development-to-generalisation gap.

What week 5 found

The LLM (claude-sonnet-5, forced structured outputs, a frozen six-example prompt bundle developed only on seeds 0–6) was dropped into the frozen Week-4 harness and validated exactly once on seeds 7–9: 640 calls, zero failures. Two findings that cut in opposite directions. (1) The LLM lands clearly below the classical baseline on pooled macro-F1 — 0.595 vs the random forest's 0.706 — with the gap concentrated in the mild actuator faults, which the cautious prompt folds into nominal (F3 mild recall 0.0). (2) Physics residuals help the LLM far more than they helped the classical models: +0.121 macro-F1 (2.2× the RF's gain, 7× the logistic regression's), rescuing the dangerous F1-severe case from 0.091 to 0.545, and the LLM is meaningfully more confounder-robust than the classical models (nominal recall 0.847 vs 0.72). The frozen bundle, the full 640-response cache, and the validation results are committed in results/week5/; offline re-scoring from the cache reproduces every number and the results hash (asserted in tests). Full analysis: docs/DESIGN.md §§21–23; refuted predictions and process notes in docs/DEVIATIONS.md (D15–D16).

What week 4 found

Detection answers "is something wrong"; diagnosis answers "which fault". A window-level classifier ({nominal, F1–F5}, mild+severe pooled, 5-min windows) is trained classically — logistic regression and random forest — on train seeds 0–6 and validated on 7–9, eval sealed. The support table is emitted and verified first: F4 severe has no complete pre-trip window (it trips too fast) and is marked NOT SCORABLE rather than claimed as diagnosed.

The primary ablation is raw vs raw+residual (same raw block in both; residuals added only in the second). Physics residuals add modest value on top of raw (+0.017 logreg, +0.054 random forest macro-F1). Raw+residual performs best for both models; residual-only is below raw for logistic regression but slightly above raw for random forest, so residuals are complementary overall but their standalone value depends on the classifier. Recall is reported by severity, which cuts both ways: F1 mild 0.94 but F1 severe only 0.55 (the dangerous case is the weaker one — D13); F3 severe 0.76 but F3 mild 0.31. F5 is trivially diagnosed (recall 1.0) via the frozen sensor's zero variance — the honest inverse of Week 3, where F5 was the hard detection case (D14). Nominal recall is 0.72, so ~28 % of benign confounder windows are misdiagnosed as faults, mostly the mild actuator faults. Full numbers, the support and ablation tables, and confirmed/refuted predictions: docs/DESIGN.md §§17–20; refutations in docs/DEVIATIONS.md (D13–D14).

What week 3 found

The detection layer, classical baseline first. A static PCA / T² / SPE monitor is fit on nominal data; physics residuals (one-step model prediction + a command-vs-flow actuator residual) are built alongside. Both are calibrated under a locked split (fit 0–6, calibrate 7–8, report false-alarm rate on seed 9), scored pre-trip, eval sealed.

Three honest findings. (1) The classical baseline can't hold its 1 % false-alarm budget on held-out nominal — it comes in at 4.0 %, driven by T², because benign confounders shift the operating point along normal correlation directions and look like faults (a documented DoD deviation, D12). Physics residuals are ~3× cleaner (1.25 % union; process/actuator individually pass). (2) Raw per-run detection rate is false-alarm-contaminated, so results are reported on a paired, confounder-controlled metric — fault run alarms while its matched same-seed nominal (identical confounders + noise) stays silent. The benchmark proves the contamination intrinsically: the actuator residual "detects" the analyser-freeze fault 10/10 while being mechanically blind to it (bit-identical to nominal on the actuator channel). (3) The clean, confounder-controlled win is the fast fault: on severe coolant loss the plant trips before the classical monitor's persistence gate can fire (MSPC 0/10), while the actuator residual catches it 10/10 fault-only at ~0.4 min. No single family dominates (F1 severe → T², F2 → SPE + energy residual, F3 severe → SPE, F4 severe → actuator residual, F5 → essentially nobody) — that measured complementarity is the empirical case for the eventual LLM-with-residuals ablation, which must use the paired metric. Numbers, the full table, and the confirmed/refuted predictions: docs/DESIGN.md §§13–16; deviations and the metric correction in docs/DEVIATIONS.md (D9–D12).

Repository map

configs/            plant, envelope, scenarios (all parameters live here)
safeplant/models/   CSTR ODEs, Jacobian, steady-state tools
safeplant/sim/      sensors (seeded noise), closed-loop integrator
safeplant/control/  PI + cascade with anti-windup, ramping, filtering
safeplant/safety/   envelope checker (pure), independent latching interlock
safeplant/run/      scenario runner: parquet log + provenance manifest
safeplant/faults/   three-hook fault interface + F1–F5 library + registry
safeplant/detect/   MSPC baseline (PCA/T²/SPE) + physics residuals + benchmark
safeplant/diagnose/ honest windowing + feature blocks + classical classifiers
safeplant/llm_diagnose/ renderer, schema, frozen bundle, cache, client, scorer
results/week5/      FROZEN: diagnosis bundle, 640-response cache, validation results
results/week6/      FROZEN: action bundle, caches, audits, scenarios, validation results
results/final/      FROZEN: sealed eval dataset summaries, scenarios, baselines, LLM cache/audits
scripts/            verification, dataset/evaluation harnesses, and reproducible figures
configs/scenarios/faults/   the 11-condition fault matrix (+ nominal)
configs/confounders.yaml     calibrated benign-disturbance bounds (locked)
tests/              105 tests, including frozen-artifact and final-eval integrity checks
docs/               specs, FINAL_RESULTS.md, DESIGN.md, DEVIATIONS.md, figures

Scope and limitations

  • Simulation only. No real plant connection or actuation exists; the LLM cannot import or call the executor directly.
  • Temperature is the safety proxy. Pressure, relief, vessel integrity, quench systems, and human operating procedures are not modelled.
  • The gate is not a formal safety proof. On the final split it saved 31 unsafe LLM-first proposals but missed 7 because the hypothesis set and plant model can be wrong.
  • Fast catastrophic utility loss remains outside agent authority. F4 severe was unsafe for every proposer; the independent interlock is the backstop.
  • The action menu is deliberately small. It tests bounded action selection, not free-form operating procedure generation or multi-step planning.
  • API responses are nondeterministic. The committed caches are the canonical evidence; rerunning for a preferable result would violate the evaluation protocol.
  • This is an engineering research prototype, not a certified safety system.

Reproducibility and frozen evidence

The release manifest at results/final/release_manifest.json hashes every canonical final artifact. Key anchors:

  • final dataset data hash: 9fdc048da32dc4f0b9941c81468be8b3188f263d677eade17f1a363ce4664682
  • frozen action bundle: 04568a87e823f2d0d267fae4fb126dbfd775c9dd7d68e575b575e92880ebab85
  • final LLM results hash: 5709d2d358a24eebf8fc116b366851528581136d2cb8fcc97b07c9f622f5f955

cache_llm.jsonl and llm_audit.jsonl preserve the exact final API outputs and executed outcomes. scripts/make_final_figures.py reads only frozen JSON artifacts; it performs no fitting, simulation, or API calls.

About

Simulator gated AI fault diagnosis and action selection for chemical process control.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Packages

 
 
 

Contributors