Blog Content

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.

Go Python React TimescaleDB Docker License


Quick start · Product tour · Architecture · API · Limitations


Reliability Center — fleet health, active risks, ranked diagnoses and maintenance actions on one screen

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.

Three analytics model scopes on bfp_001: Mechanical decisively critical, Motor Reliability clean, Process carrying a standing critical alert with a currently clean reading

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.

bfp_004: Mechanical and Process reading clean, Motor Reliability critical at 27% anomaly with Motor bearing degradation the leading candidate

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
Model governance page: per-regime NBM statistics, training history, version trail and CNN validation

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.

Reliability Center

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.

The full asset register
Fleet asset register with health, regime and standing alerts

2 · Asset overview — health, horizon and the leading mechanism

Asset overview with health index, failure-horizon risk and 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

Mechanical signal trends: bearing vibration in three directions on both bearings, impulsiveness and envelope bands

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.

Ranked candidate failure modes with per-variable supporting evidence

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.

Fault match trend chart plotting each mechanism's confidence over time

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

Acceleration spectrum with order markers, envelope spectrum with BPFO and BPFI bands, and CNN class probabilities

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 list with severity, mechanism and acknowledge/resolve actions

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

New work order form pre-filled with the diagnosis evidence

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

Inspection finding form with confirmed failure mode and diagnosis verdict

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.

Case discussion thread on a work order, tagged Diagnosis

9 · Retraining — relearning normal from the plant’s own history

Model governance page with retraining in progress, model versions, NBM statistics and CNN validation

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

Failure library: eight mechanisms with severity, consequence category, RCM maintenance strategy, typical evidence, false-positive risk and recommended action

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

Role permission matrix Audit log of the walkthrough

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

3D asset view with the drive-end bearing selected, showing its live readings and the diagnosis behind bfp_001's current state

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

Reports page: MTBF, MTTR, work order throughput, strategic call-outs, fleet health trend and leading failure modes with their RCM strategy

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

Command palette over assets, alerts and actions

⌘K searches assets, alerts and actions from anywhere.

Every chart has a hover toolbar: reset zoom, save PNG, copy PNG, export CSV. Exported images carry a caption band with the measure, units, time range and point count — so a chart pasted into a report is not an anonymous squiggle.

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:3000

Sign in with admin@example.com / admin123.

Sign-in screen

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?

Playback report showing first detection 14.5 days before the recorded failure

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_versions records 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

Leave a Reply

Your email address will not be published. Required fields are marked *