Rotating Equipment PHM Platform
Physics-guided, regime-aware predictive health management for rotating machinery.
Ranked failure mechanisms with the evidence behind them — not a black-box score.
Quick start · Product tour · Architecture · API · Limitations
Table of contents
| Section | What’s in it |
|---|---|
| Why this exists | The argument this platform makes runnable |
| Analytics models | Why one score per machine is not enough |
| Product tour | The full operator flow, screen by screen |
| Architecture | How the layers fit together |
| What each layer does | Layer → implementation → report section |
| Quick start | docker compose up -d |
| Driving a demonstration | Injecting mechanisms and what should appear |
| Retraining from plant data | Relearning normal from the historian |
| Validation posture | Why one accuracy number is not a result |
| Playback | Proving the models on a failure that already happened |
| API | Backend and analytics endpoints |
| Access control | RBAC, site scoping, audit |
| Repository layout | Where everything lives |
| Known limitations | What does and does not transfer |
🎯 Why this exists
This is an implementation of the layered PHM/APM architecture described in Artificial Intelligence for Rotating Equipment Health (Lam Van Phong, 2026), built around a Boiler Feedwater Pump pilot.
The report’s central argument:
A physics-guided, regime-aware architecture built on normal-behaviour modelling and evidence fusion is a more defensible industrial starting point than an end-to-end fault classifier.
An end-to-end classifier needs labelled failures that a well-run plant does not have, gives no reasoning an engineer can challenge, and fails silently when the operating point moves. This platform is the alternative, made runnable:
|
🏭 Physics, not curve-fitting A simulated BFP train with a real pump head curve, BEP efficiency, NPSH margin and shaft power. Six failure mechanisms are injected as causes — the symptoms follow from the hydraulics, so the diagnostic layer has to infer the mechanism rather than being handed it. |
📐 Regime before residual A run-state machine plus load bands decide whether a reading is even scoreable. Start-up, shutdown and load ramps are refused, not scored — §7 names this as the dominant cause of false alarms in condition monitoring. |
|
🔍 Ranked mechanisms with evidence An FMEA rule-weight matrix fuses residuals into a ranked list, carrying both supporting and contradicting evidence. Every diagnosis shows which variable moved, what the model expected, and by how many sigma. |
📅 Honest prognostics A 7/30/90-day crossing probability instead of a point RUL the data cannot support. When the health index has already crossed the threshold, the platform says so rather than inventing a negative remaining life. |
Note
Every number the console shows can be traced back to the residual, the rule and the regime that produced it. A diagnosis an engineer cannot interrogate is a diagnosis an engineer will not act on.
An alert requires a mechanism. A scope can be numerically far from normal
with no known mode matching — a machine running six degrees cooler than
expected is far from normal in the Mahalanobis sense — and anomaly score 1.00
with nothing attached is precisely the bare-threshold alarm this platform exists
to replace. Those deviations stay visible in the model health trend without
becoming work orders. A thermal finding within twelve minutes of a regime change
is withheld too, because bearing temperatures take minutes to reach their new
steady value and the residual is measuring the clock, not the machine — and it
is withheld at the source, not only from the alert: a scope in that window is
recorded as unscored in the health trend and the ranked diagnosis alike, the
same as a bad sensor reading, rather than showing a confident number nobody
should act on.
A standing alert stays current. Evidence legitimately moves while an alert is open — an early motor fault can look like a cooling problem before the current signature has diverged from the thermal one, and settle into its real mechanism an hour later as severity grows. The alert’s message, mechanism and evidence are refreshed every time the ingest layer re-evaluates a standing alert, not only at the moment it opened, so a client reading it sees what the platform currently believes rather than its first guess.
🔬 Analytics models
A single score for a whole machine answers “something is wrong” and nothing else. The ordinary case on a pump is that one half of it is in trouble and the other half is fine, and a machine-wide number cannot say which.
So an asset is watched through scopes. Each one aggregates only its own tags, carries its own alert state, and offers only the failure modes that belong to it.
That is bfp_001. Mechanical is decisively critical — 100% anomaly,
envelope_bpfo at +377σ against its own healthy baseline — and Motor
Reliability reads clean at 0%. Before the scopes existed this machine showed
one red number and an engineer had to read the residual table to find out
which half was in trouble.
Process is the more interesting card: it carries a standing critical
badge from an earlier performance_degradation alert, but its live reading
right now is 0% anomaly, nothing above threshold. That is not a bug in the
screenshot — it is the platform’s own alerts-are-not-auto-resolved
posture made visible: the badge is the standing alert
state, the number below it is the instant reading, and showing both rather
than collapsing them into one figure is the honest option even when it looks
momentarily inconsistent.
| Scope | Watches | Raises |
|---|---|---|
| BFP Mechanical | 16 tags — vibration in all three directions on both bearings, waveform impulsiveness and order content, envelope fault bands, bearing and oil temperatures | Bearing degradation, imbalance, misalignment |
| BFP Motor Reliability | 4 tags — stator current, absorbed power, motor bearing temperatures at both ends | Motor bearing degradation, motor cooling degradation |
| BFP Process | 5 tags — developed head, efficiency, discharge pressure, NPSH margin, absorbed power | Cavitation, recirculation, performance degradation |
Scopes overlap where the physics overlaps: absorbed power is evidence of a motor problem and of a hydraulic one, and excluding it from either would blind that scope.
Important
Calibration is per scope, and it had to be. A sub-matrix of an inverse is not the inverse of a sub-matrix, so each scope’s covariance is built from its own columns. And a Mahalanobis distance over four variables simply does not reach the magnitude of one over twenty-four, so each scope carries its own healthy p99 rather than the full-model figure, which would have made every small scope look permanently healthy.
The covariance itself is shrunk, not just ridge-stabilised. Sixteen correlated vibration and thermal channels routinely produce a sample correlation matrix with a near-zero eigenvalue — three axes on one bearing move together, so do a bearing’s two temperatures — and a fixed small ridge does not move that eigenvalue enough. Its inverse then turns ordinary noise into an extreme apparent distance: this is precisely what made the mechanical scope report a bearing fault on the one machine in the fleet with no fault at all, every individual residual under 3σ, the aggregate distance six times its own calibrated healthy p99. Ledoit-Wolf shrinkage — pulling the estimate back toward independence by however much the sample size can actually support — is the standard fix for more correlated dimensions than the data can reliably resolve, and it is what the trainer uses now.
Ranking is restricted to a scope’s own modes, because a bearing fault and a wear-ring fault are not competing for the same first place. Evidence is not restricted: contradicting rules deliberately reach across scopes — “the efficiency is down and there are no bearing tones” — so the residual map stays whole.
Scopes are defined per asset class, which is what makes them a template: a fourth pump of a known class deploys the same three models with no configuration.
The template claim, checked rather than asserted. bfp_004 was added with
nothing but a row in machines — no scope, rule or threshold configured for
it — and a driver-side mechanism injected: both motor bearing temperatures
rise and the unit draws more current for the same duty, deliberately silent on
the pump side.
Mechanical and Process both read a clean live reading (0%, nothing above
threshold — Process still carries its own standing badge from earlier, the
same distinction as above); Motor Reliability alone shows genuine current
anomaly and correctly names the mechanism: 27% anomaly, motor_nde_temp at
+7.0σ, Motor bearing degradation the leading candidate at 52%. Earlier,
before the current signature had diverged from the thermal one, this scope
legitimately raced between motor cooling degradation and motor bearing
degradation — both modes are supported by the same rising bearing
temperatures, and only current tells them apart. The standing alert tracked
that race as it happened and settled on the right mechanism as severity grew,
rather than freezing on its first guess.
Model governance — what is deployed and how it was calibrated
The page reads the deployed manifest, not a static list, so the console can never claim a scope the running model cannot actually score. A scope without calibration is reported as unscored rather than as zero — an absent answer and a clean bill of health are different claims.
📸 Product tour
What follows is one continuous walkthrough — the same path an operator takes from noticing a problem to closing the loop on it. Every screenshot below is the running system, captured end to end in one session.
1 · Reliability Center — what needs attention
The landing page answers one question: where should I look first? A KPI strip, a ranked asset table, fleet health trajectories on one comparable scale, live alert activity, and what the diagnostic layer is finding across the fleet.
bfp_001 sits at the bottom of the table with the lowest health in the fleet
and bearing_degradation as its active mechanism. That is where the tour goes
next.
2 · Asset overview — health, horizon and the leading mechanism
The failure-horizon panel reads 100% at 7, 30 and 90 days — and says why:
The health index is already at or below the maintenance threshold. This is no longer a forecast — the decision point has arrived.
That refusal to extrapolate a point RUL past the threshold is §9’s posture, not a UI accident, and the urgency dial says which half of the claim it is standing on: Extreme, on the basis that the threshold has already been crossed rather than on a forecast.
Below, the health index components read Vibration 1.00, Thermal 0.41, Process and Performance both 0.00 — the damage is mechanical, and the index says so before any human reads the diagnosis. The thermal contribution is the bearing heating through the shaft, which is a consequence, not a second fault.
3 · Signals — the measurements behind the index
Grouped by domain (hydraulic, mechanical, thermal & electrical). Each chart reports how many points fell outside the plotted range rather than silently clipping them, and every chart carries drag-to-zoom, PNG capture and CSV export of exactly what is plotted.
4 · Diagnosis — ranked mechanisms and their evidence
This is the layer the whole architecture exists to serve.
Candidates are ranked within each analytics model, so a bearing fault and a
wear-ring fault never compete for the same first place. On bfp_001 the
mechanical scope reads:
| Rank | Mechanism | Confidence | Evidence |
|---|---|---|---|
| #1 | Bearing degradation / lubrication distress | 100% | envelope_bpfo +378.5σ · envelope_bpfi +170.1σ · accel_rms +29.5σ · kurtosis +23.0σ · crest_factor +9.9σ · pump_de_temp +7.3σ |
| #2 | Pump / coupling shaft misalignment | 16% | supported by vib_nde_a +5.0σ and vib_de_a +2.8σ — contradicted by envelope_bpfo and kurtosis |
The runner-up is where the argument lives. Axial vibration on both bearings genuinely is consistent with misalignment, and a classifier that only picked a winner would have hidden that. What puts it down to 16% is not weak support but active contradiction: envelope fault-band tones and impulsiveness are bearing evidence, and they argue against misalignment rather than merely failing to argue for it.
That is the §8 claim made concrete — a ranked list with the reasoning for and against each candidate, not a single class.
Fault match trending. A confidence number at one instant cannot tell a diagnosis that is settling in from one that is flickering, and that difference is what decides whether anyone raises work. The Diagnostics tab plots every mechanism’s confidence across the whole machine over time, so a candidate that has held 90% for six hours reads differently from one that touches 90% twice an hour and falls back.
Gaps in the line are readings the models refused to score — transients, bad sensor quality — not a candidate silently dropping to zero. The two are not the same, and the chart does not pretend otherwise.
5 · Waveform — spectrum, envelope and the CNN as one input
Order components (1X/2X/3X) and the envelope fault bands (BPFO/BPFI) are marked explicitly. The 1D CNN’s class probabilities sit beside them, labelled “one evidence source” — it contributes to the fusion, it does not decide.
6 · Alerts — raised from fused evidence
Alerts carry the mechanism and the evidence that raised them, not a bare threshold breach. Acknowledging one is audited.
7 · Work order — carrying the evidence into the field
The maintenance action inherits the reasoning: head 1028 m against an expected 1094 m, efficiency 0.741 against 0.793, no mechanical or thermal evidence — which is precisely what tells the fitter to look at wear rings rather than bearings.
8 · Record the finding — closing the learning loop
This is §13’s closed loop and the reason the platform earns its keep over time. The inspector records what was actually found, the confirmed failure mode, and whether the diagnosis was correct. Those become labels — the labels a plant never has at the start.
The verdict is not the whole story. By the time a finding is written, the reasoning that produced it happened in a corridor, a phone call and somebody’s inbox — not “wear ring at 0.62 mm” but “we nearly took this for cavitation until the NPSH trend came back clean.” Every case carries a discussion thread for exactly that, tagged General, Site feedback or Diagnosis, so the next engineer inherits how the conclusion was reached, not only what it was.
9 · Retraining — relearning normal from the plant’s own history
One screen covers model governance end to end: healthy-sample counts per regime, a readiness check, live training progress, the version audit trail, per-regime NBM statistics, and the CNN’s validation design.
Important
Models are hot-swapped on success. The previous version keeps scoring right up until the new one is ready, so a retrain never creates a gap in monitoring.
10 · Failure library — the FMEA and RCM behind every diagnosis
Eight mechanisms, each with its FMEA severity, typical evidence, recommended action — and, unusually, its false-positive risk: what else produces the same symptom. That column is what keeps the ranked list from overclaiming.
An FMEA catalogue answers what fails and how you’d recognise it. It does not answer what maintenance a failure justifies — every mode here previously defaulted to condition monitoring by construction, because that is what the analytics layer happens to do, not because RCM logic said so. Two fields close that gap, following SAE JA1011:
- Consequence category — safety, environmental, hidden, operational or economic. Cavitation and both bearing-degradation modes carry an operational consequence (a forced outage), not merely a cost; the rest are economic.
- Maintenance strategy — the task the consequence actually justifies: condition monitoring, scheduled restoration, scheduled discard, redesign, or run-to-failure. Recirculation is a control/operating-point problem, not a wearing component, so its strategy is redesign of the setpoint — not inspection. Performance degradation wears predictably over an overhaul cycle, so its strategy is scheduled restoration, not condition monitoring, even though the trend is diagnostic.
Every call is reasoned in the migration that seeds it
(015_rcm_taxonomy.sql), not asserted — the same standard the evidence-fusion
rules are held to.
11 · Roles and audit — who may do what, and who did
The audit log on the right is real entries from the running platform, not placeholder rows, and it now paginates — 200 entries, 50 to a page — instead of dumping the whole history at once:
16:21:37 alert.acknowledge {"machine_code":"bfp_004","severity":"warning"}
16:09:55 workorder.updated {"confirmed_failure_mode":"performance_degradation","machine_code":"bfp_002","status":"done"}
00:03:13 model.train_started {"min_health":85,"window_days":30}
12 · Physical view — the 3D asset, live
A ranked mechanism is still an abstraction until it names a part a fitter can put a hand on. Each asset renders as a representative 3D model — 53 named components, independently authored, not an OEM replica — and clicking one shows exactly what the platform knows about it: live readings for the telemetry tags that component carries, and, if the analytics scope covering it has a current finding, the diagnosis and evidence behind it.
bfp_001‘s drive-end bearing reads back its own live temperature and
vibration next to the same explanation the Diagnostics tab gives — this is
another lens on the same evidence, not a second source of truth. A part with
no recent reading says so explicitly rather than defaulting to green, and a
scope the analytics layer hasn’t scored yet says “not yet scored” rather
than inventing a healthy state — the same refusal-to-guess posture as
everywhere else in the platform, just spatial this time.
Geometry is deliberately representative: six illustrative stages, dimensions
and stage count are the model author’s choices, not an OEM’s. What is real is
the mapping — every component carries the same analytics_scope and
telemetry field names the rest of the platform already scores against, so the
highlight a reader sees is never invented for the picture.
13 · Reports — the fleet-level strategy view
Everything above answers what is wrong with this machine, right now. This page answers the question a plant manager actually asks at the end of a period: MTBF, MTTR, work order throughput, and which failure modes are actually driving the fleet’s risk — with the RCM consequence and strategy from §10 carried straight through, so a report line reads as a maintenance decision, not just a tally.
Strategic call-outs are threshold rules over the numbers already on the page — MTTR past 48 hours, an overdue work order, a safety- or environmental-consequence mode that isn’t on condition monitoring, a mechanism repeating across multiple assets — never a generated narrative. Same standard as the evidence-fusion rules: traceable back to a number a reader can check, not asserted.
Everywhere · ⌘K and chart export
Guided demo mode · ⌘⌥⇧D
For recording a walkthrough without narrating clicks in real time: press ⌘+⌥+⇧+D anywhere in the app, Enter to start after a 3‑2‑1 countdown, Esc to abort. It plays a fixed ~90-second script — real navigation, real clicks on real elements, real data — through the Reliability Center, an at-risk asset, its explainable diagnosis, the 3D component view, the RCM failure library, work orders and the reports page, each beat carrying an on-screen caption so the message survives with the sound off.
Purely a presentation aid: reachable only by that shortcut, not linked
anywhere in the UI, and inert unless invoked (frontend/src/demo/).
🧭 Architecture
Each layer is validated separately and degrades independently. If the horizon model is not credible, anomaly detection and diagnosis still work.
🧩 What each layer does
| Layer | Implementation | Report |
|---|---|---|
| 🏭 Machine physics / FMEA | Pump curve, NPSH, BEP efficiency; 6 failure modes with deliberately separable signatures | §3, §14.2 |
| 📟 Sensing | Full §14.3 tag schema — context, hydraulic, control, speed, electrical, thermal, lubrication, vibration trend, waveform-derived, quality | §14.3 |
| ✅ Data quality | sensor_quality_flag gates scoring: a bad sensor cannot manufacture a diagnosis |
§14.3 |
| 📐 Operating regime | Run-state machine plus load bands; start-up, shutdown and load ramps are refused, not scored | §7 |
| 🧠 Normal Behaviour Model | XGBoost regressor per monitored variable per steady regime, bootstrapped on 1.08M healthy samples | §6, §14.4 |
| 📊 Anomaly score | Mahalanobis distance over standardised residuals, calibrated against the healthy p99 | §6 |
| 🔬 Analytics models | Three monitoring scopes per pump, each with its own residual aggregate, alert state and candidate modes; calibrated separately | §14.2 |
| 🔍 Evidence fusion | FMEA rule-weight matrix in the database, including contradicting evidence and operating-point rules, producing a ranked list per scope | §8 |
| ❤️ Health index | HI = 100·(1 − g(Σwᵢ·Sᵢ)) with weights derived from FMEA severity; EWMA smoothed, CUSUM for onset |
§9 |
| 📅 Prognostics | 7/30/90-day crossing probability; point RUL withheld unless the trajectory earns it | §9 |
| 〰️ Waveform DL | Spectrum, envelope bands, 1D CNN as one evidence source — never the answer | §14.4 |
| 💡 Explanation | Residual contribution + TreeSHAP (/explain) + spectra |
§14.4 |
| 🧪 Validation | Machine-level hold-out, per-class recall, noise-rejection sweep | §12 |
| ⏪ Playback | Replays stored history through the deployed models and measures the lead time against a failure the plant already recorded | §12 |
| 🔁 CMMS loop | Alert → evidence → work order → inspection finding → confirmed failure mode | §13 |
| ♻️ Retraining | Relearns the NBM from the plant’s own healthy history, read-only, hot-swapped | §6, §13 |
| 📋 Model governance | Training jobs, model versions, who asked and on what data; every alert carries the model version that raised it | §13 |
| 🧭 RCM decision layer | Consequence category (safety/environmental/hidden/operational/economic) and the maintenance strategy that consequence justifies, per failure mode | — |
| 📈 Fleet reporting | MTBF, MTTR, work order throughput and leading failure modes over a period, with threshold-based strategic call-outs | — |
| 🧊 Physical view | Live readings and diagnosis mapped onto a component-level 3D asset model | — |
The legacy LSTM classifier from the project’s first iteration is retained but demoted to one supplementary evidence input, per §2’s “no single model needs to understand all physical phenomena”.
🚀 Quick start
Prerequisites · Docker with Compose v2 · ~6 GB free disk for the TensorFlow image · an AVX-capable CPU for the waveform models.
git clone https://github.com/hades0312/iot-predictive-maintenance
cd iot-predictive-maintenance
docker compose up -d
open http://localhost:3000Sign in with admin@example.com / admin123.
On a fresh database the simulator replays 30 days of history through the real
ingest pipeline (BACKFILL_DAYS), so the health trajectory, the horizon model
and the long chart windows have something real to work with. Nothing is inserted
behind the pipeline’s back. Set BACKFILL_DAYS=0 to start empty.
The seeded fleet arrives with four different stories: one pump well into a bearing defect, one drifting on hydraulic performance, one healthy, and one with a driver-side fault — so all three analytics scopes have a genuine positive case, not just the two mechanical/process ones.
Warning
Change the demo credentials before running this anywhere but your laptop.
Set JWT_SECRET, ADMIN_EMAIL and ADMIN_PASSWORD. The bootstrap account is
created only when it does not already exist.
Ports
| Service | Host port | Notes |
|---|---|---|
| 🖥️ Operations console | 3000 |
React, served by nginx |
| ⚙️ Backend API | 8081 |
Go; container-internal port is 8080 |
| 🧠 Analytics service | 5000 |
Flask: NBM, fusion inputs, horizon, explanations |
| 📡 MQTT broker | 1883 |
Mosquitto; 9001 for websockets |
| 🗄️ TimescaleDB | 5433 |
PostgreSQL 16 + Timescale |
Environment
Every value below has a working default in docker-compose.yml.
| Variable | Default | Purpose |
|---|---|---|
BACKFILL_DAYS |
30 |
Days of history replayed on a fresh database |
MACHINE_IDS |
bfp_001,bfp_002,bfp_003 |
Simulated fleet |
JWT_SECRET |
dev-secret-change-me |
Token signing key — change this |
ADMIN_EMAIL / ADMIN_PASSWORD |
admin@example.com / admin123 |
Bootstrap account — change this |
DATABASE_URL |
local Timescale | Shared by backend and analytics |
ML_SERVICE_URL |
http://ml-service:5000 |
Base URL, no path |
🎬 Driving a demonstration
The control panel injects a mechanism, not sensor values. The symptoms follow from the pump physics, so the diagnostic layer has to infer the cause.
| Inject | Expected evidence | What separates it |
|---|---|---|
| 🔩 Bearing degradation | Envelope BPFO/BPFI growth, rising kurtosis and crest factor, DE thermal rise | No order content, no hydraulic change |
| ⚖️ Imbalance | Dominant 1X, radial direction, consistent phase | No 2X, no axial, low impulsiveness |
| 📏 Misalignment | 2X plus axial on both bearings, thermal rise | No envelope tones |
| 💨 Cavitation | NPSH margin collapse, broadband noise, head loss | No discrete orders, no envelope tones |
| 📉 Performance degradation | Head and efficiency loss at the same duty point | No mechanical evidence at all |
Recirculation is not injected — it emerges when the operating point sits far below the BEP, which is how it behaves on a real pump.
Tip
A worked check: injecting three different mechanisms into three machines ranked each one correctly at #1 — bearing degradation scored 1.000 against a runner-up of 0.086 — with physically honest runners-up rather than a confident single class.
🔄 Retraining from plant data
The normal-behaviour model ships with a bootstrap version trained on the physics model. Once the platform has been running, Models → Retrain relearns it from the historian instead — which is what §6’s argument actually calls for, since by then the abundant healthy data is the plant’s own.
Selecting that data is the delicate part, and the filters are the method:
| Filter | Why |
|---|---|
| Steady regimes only | A residual during a transient means nothing (§7) |
sensor_quality_flag = 'good' |
§14.3’s governance flag — a bad sensor must not teach the model |
| Health index above a floor | Training on a degrading period teaches the model that degradation is normal |
| No alert standing at that moment | The same reason, from the other direction |
The analytics service holds a read-only connection to the historian, per §13
(“the AI service should initially use read-only access to plant data”), and sets
default_transaction_read_only on it.
Every run is recorded with who asked, what window, how many samples and what came out.
🧪 Validation posture
A single accuracy number is not a validation result. — §12
The Models page reports the validation design, not a headline score. The waveform CNN is held out by machine, so a model cannot pass by memorising a particular unit. Its noise-rejection sweep is the informative result:
| SNR | Accuracy | |
|---|---|---|
| clean | 0.957 |
🟢 |
| 20 dB | 0.957 |
🟢 |
| 10 dB | 0.945 |
🟢 |
| 4 dB | 0.517 |
🟠 |
| 0 dB | 0.242 |
🔴 |
| −4 dB | 0.200 |
🔴 |
Near-perfect on clean data, collapsing under plant-realistic noise. This is the effect §12 warns about, reproduced deliberately rather than hidden.
⏪ Playback
A monitoring pilot has an awkward problem: if nothing breaks during the trial, there is nothing to show. Waiting for a real failure is not a plan, and “the models ran and nothing alarmed” is not evidence that they would have caught anything.
Playback replays a window of stored history through the models deployed right now, and asks a question with a checkable answer: on an asset that already failed, did the anomaly rise beforehand, and how many days of warning would it have given?
On bfp_001 over thirty days, sampled every fifteen minutes:
| Analytics model | First detection | Lead time | Peak | Most abnormal at peak |
|---|---|---|---|---|
| bfp_mechanical | 10 Sep | 14.5 days early | 100% | envelope_bpfo 19.8σ |
| bfp_motor | never | — | 100% | motor_current 15.2σ |
| bfp_process | never | — | 100% | discharge_pressure 18.5σ |
The two scopes that touched 100% but never registered a detection are the point of the discipline, not a failure of it: they spiked without ever holding above the threshold for five consecutive samples.
Note
Nothing is fitted. The models are never shown the event date — it is used only to measure the distance from detection to failure afterwards. Detection uses the same five-sample confirmation the live path uses, because a back-test that is more trigger-happy than production would be measuring the wrong system. Readings the models refuse to score — transients, bad sensor quality — break the run, exactly as they would in production.
🔌 API
POST /api/auth/login returns a JWT. Every other endpoint takes
Authorization: Bearer <token> and is checked against a permission code,
resolved from the database per request.
⚙️ Backend — :8081/api
| Method | Path | Permission |
|---|---|---|
GET |
/health |
public |
POST |
/auth/login |
public |
GET |
/auth/me |
authenticated |
POST |
/auth/password |
authenticated |
GET |
/dashboard |
machines.read |
GET |
/dashboard/health-trends |
diagnostics.read |
GET |
/dashboard/alert-activity |
alerts.read |
GET |
/dashboard/mode-distribution |
diagnostics.read |
GET |
/machines, /machines/{id} |
machines.read |
GET |
/machines/{id}/readings |
telemetry.read |
GET |
/machines/{id}/residuals[/{variable}] |
diagnostics.read |
GET |
/machines/{id}/diagnoses |
diagnostics.read |
GET |
/analytics-models |
diagnostics.read |
GET |
/machines/{id}/model-health[/{model}] |
diagnostics.read |
GET |
/machines/{id}/health |
diagnostics.read |
GET |
/machines/{id}/waveforms, /spectrum |
waveform.read |
POST |
/machines/{id}/control |
machines.control |
GET |
/alerts |
alerts.read |
POST |
/alerts/{id}/acknowledge · /resolve |
alerts.ack · alerts.resolve |
GET/POST/PATCH/DELETE |
/work-orders |
workorders.read / workorders.write |
GET |
/failure-modes |
diagnostics.read |
GET |
/reports/reliability |
diagnostics.read |
GET |
/models, /models/versions, /models/training |
models.read |
POST |
/models/training |
models.train |
GET |
/playback, /playback/{id} |
diagnostics.read |
POST |
/playback |
playback.run |
GET/POST/PATCH/DELETE |
/users, /roles, /sites |
*.read / *.write |
GET |
/audit |
audit.read |
ws://localhost:8081/ws streams live readings, health updates and alerts.
🧠 Analytics service — :5000
| Method | Path | Returns |
|---|---|---|
GET |
/health |
Which models are loaded |
POST |
/analyze |
Regime, residuals, anomaly score, per-scope aggregates — or a refusal with its reason |
POST |
/analyze/batch |
The same for many readings at once, for playback |
POST |
/waveform |
Spectrum, envelope bands, CNN probabilities |
POST |
/horizon |
7/30/90-day crossing probability; RUL only when defensible |
POST |
/explain |
TreeSHAP contributions for a residual |
GET |
/models |
Version manifest and validation metrics |
POST |
/train · GET /train/status |
Start a retrain, poll its progress |
🔐 Access control
JWT bearer tokens, with permission codes checked on every request. Four built-in roles whose permissions are editable in the UI, plus site scoping that works independently of role.
| Role | Intent |
|---|---|
🛡️ admin |
Full access to every part of the platform |
👷 supervisor |
Runs the fleet: machines, alerts, work orders |
🎛️ operator |
Monitors and controls machines, handles alerts |
👁️ viewer |
Read-only |
Permissions are resolved from the database per request behind a short-lived cache, not read from token claims — so granting or revoking a permission takes effect without the user signing out again.
Sign-ins, configuration changes and every control command are audited.
📁 Repository layout
backend/ Go 1.24 — API, auth, MQTT ingest, PHM engine, WebSocket hub
internal/phm/ evidence fusion (§8), health index (§9)
internal/ingest/ orchestration: regime → NBM → fusion → health → horizon
internal/db/ migrations: FMEA rule-weight matrix, RCM taxonomy
ml/ Python — analytics service and trainers
phm_service.py NBM scoring, waveform, horizon, explanations, retraining
train_nbm.py bootstrap normal-behaviour model
train_waveform_cnn.py 1D CNN + machine-level hold-out + noise sweep
simulator/ Python — BFP physics, fault mechanisms, MQTT publisher
bfp/physics.py head curve, efficiency, NPSH, shaft power
bfp/faults.py the six mechanisms and their signatures
frontend/ React 19 + Recharts + Tailwind + Three.js
src/pages/Reports.jsx fleet-level MTBF/MTTR/work-order/RCM report
src/components/
Machine3DView.jsx the 3D asset view, wired to live readings/diagnoses
src/demo/ guided auto-play walkthrough (⌘⌥⇧D), see below
docs/screenshots/ the product tour above
🚧 Known limitations
Stated rather than hidden, because which parts transfer to a real plant is exactly the question a reader should be asking.
- The simulator is synthetic. Failure signatures are separable by construction, so model scores here are not evidence of plant performance. The noise sweep and the withheld-RUL logic are the parts that transfer.
- Thermal transients take real minutes to clear. Bearing and oil temperatures are a first-order lag with a ~4-minute time constant, so after a genuine regime change the residual against them stays large for as long as the physics takes to catch up — that is not a modelling defect, it is the machine. A scope whose leading evidence is thermal is marked unscored for the twelve minutes after an observed regime change, in the model-health trend and the ranked diagnosis alike, not only in the alert — but a fresh container (simulator or backend) has no observed “before” to measure a change against, so the very first regime it sees carries no protection. A cold-started asset’s thermal channels can read several sigma off for a few minutes while they converge, exactly like a real plant’s would after its monitoring came back online.
- Extreme z-scores, partly addressed. Residuals are now judged against an instrument noise floor rather than the spread the model happened to fit, because the simulator computes temperature and efficiency almost deterministically and no real RTD resolves 0.2 °C. That removed a whole class of false positives — a five-degree consequential offset used to read as 22σ and is now 6σ. Waveform-derived features are deliberately left unfloored, since their spread is genuinely noisy, so an envelope fault band can still present at hundreds of sigma when a tone emerges from the noise. That number is real; a deployment would still cap the display.
- Backfill drops readings. The historical replay is paced, but the backend still cannot absorb every message; roughly half of a 30-day replay lands. Enough for the trajectory, not lossless.
- No true model rollback.
model_versionsrecords the history, but the artefacts on disk are overwritten by a retrain. Alerts do carry the version that raised them and are flagged when it is no longer the deployed one, so the staleness is at least visible. - Alerts are not auto-resolved. A model that stops seeing something is not proof that it went away — that judgement belongs to an engineer. In practice this means a container restart mid-transient, or a code change during development, can leave a stale alert standing on evidence the platform no longer produces; an operator has to notice and resolve it. The alert correctly reports which model version raised it either way.
- AVX CPU required for TensorFlow (the waveform CNN and the legacy LSTM). The other layers run without it.
MIT licensed — see LICENSE
Built from Artificial Intelligence for Rotating Equipment Health · Lam Van Phong · 2026





