164 lines
8.1 KiB
Markdown
164 lines
8.1 KiB
Markdown
# LINE-1 Digital Twin — customer demo
|
||
|
||
A live digital twin of a five-station production line: 3D view, streaming
|
||
telemetry, OEE, trend-based failure prediction, injectable faults, what-if
|
||
controls, and an LLM copilot that diagnoses the line from its own telemetry.
|
||
|
||
Built to be presented from a laptop, offline if necessary.
|
||
|
||
```bash
|
||
npm install
|
||
npm run dev
|
||
```
|
||
|
||
Dashboard on **http://localhost:5173**, API on **http://localhost:8787**.
|
||
For a single-port build with no dev server: `npm run serve`, then
|
||
**http://localhost:8787**. Full setup detail is in
|
||
[docs/installation.md](docs/installation.md).
|
||
|
||
The line starts **pre-warmed** — 30 simulated minutes of history, a full OEE
|
||
window at a realistic ~88%, and anomaly baselines already learned. You are never
|
||
presenting a dashboard that just booted.
|
||
|
||
## Read this first
|
||
|
||
- **[docs/installation.md](docs/installation.md)** — requirements, install, how to
|
||
run it, the demo sequence, switching models, and troubleshooting.
|
||
- **[docs/LINE-1-Digital-Twin-Demo.pptx](docs/LINE-1-Digital-Twin-Demo.pptx)** — the
|
||
step-by-step demonstration deck. 16 slides, one per beat, with the presenter
|
||
actions and script in the speaker notes. Every chart is measured data.
|
||
- **[docs/demo-script.md](docs/demo-script.md)** — the same 6-minute run of show in
|
||
text form: what to click, what to say, and the questions you will get.
|
||
- **[docs/architecture.md](docs/architecture.md)** — where the customer's MQTT,
|
||
OPC-UA or historian data plugs in.
|
||
|
||
## The demo in one paragraph
|
||
|
||
The line runs at a believable 88% OEE. You change the oven setpoint and watch a
|
||
thermal model respond with a real time constant — that is what makes it a twin
|
||
rather than a dashboard. You inject a bearing fault, run the clock at 60×, and the
|
||
twin projects the alarm threshold *before* it is crossed. The copilot — which is
|
||
never told what you injected — traces vibration to tool wear to reject rate to
|
||
OEE. It drafts the work order. You replace the bearing and watch OEE recover. Then
|
||
you switch the copilot to its offline rule engine and get the same diagnosis with
|
||
no model and no network, which answers "is the AI making this up?" better than any
|
||
claim could.
|
||
|
||
## What is honest about this
|
||
|
||
Worth knowing, because it is what makes the demo survive an engineer in the room.
|
||
|
||
- **OEE is ~88%, not 100%.** Micro-stops and unplanned stops are modelled, so the
|
||
number moves for reasons you can name. Performance is capped at 100% by
|
||
definition, so draining a WIP buffer can never produce an OEE above 100%.
|
||
- **Prediction is trend extrapolation, not AI.** A least-squares fit over recent
|
||
run time, projected to the threshold, suppressed when r² is too low to trust.
|
||
Call it what it is; the AI story is the copilot.
|
||
- **The copilot is not told which fault was injected.** Operator injection log
|
||
lines are filtered out of its context. The diagnosis is earned from telemetry.
|
||
- **Stations block and starve each other** through finite WIP buffers. Stop the
|
||
packer and the backup propagates upstream until the whole line is blocked.
|
||
- **A dropped sensor holds its last value and says so.** Charts stop rather than
|
||
drawing a flat line that would read as a healthy steady state.
|
||
- **The copilot always answers.** A dead key, an unreachable host, or a model that
|
||
returns an empty response all degrade to the rule engine.
|
||
|
||
## Copilot providers and the settings page
|
||
|
||
Click **AI ⚙** in the header to open the settings page. It lists the models each
|
||
provider actually offers — pulled live from the OpenRouter catalogue (400+, with
|
||
context length and price per million) and from your local Ollama library — and
|
||
lets you switch provider, model, temperature, output budget and reasoning effort
|
||
without restarting anything. The dashboard keeps streaming behind it.
|
||
|
||
| Mode | First token | Notes |
|
||
|---|---|---|
|
||
| **Cloud** — OpenRouter | 2–35 s, highly variable | Best analysis. Default `stealth/ox-alpha`. |
|
||
| **Local** — Ollama | ~1–2 s | Run `ollama serve`. Best for the live beat. |
|
||
| **Offline rules** | instant | No network, no model. Deterministic. |
|
||
|
||
**Measure before you present.** Each model panel has a **Test** button that
|
||
reports time-to-first-token and grades it: under 4 s is presentable live, 4–12 s
|
||
means a noticeable pause, above that is follow-up-only. Cloud reasoning-model
|
||
latency was measured between 2 s and 36 s on consecutive identical calls, so this
|
||
is worth checking on the day rather than assuming.
|
||
|
||
Changes persist to `.copilot-settings.json` (gitignored), so a model chosen while
|
||
preparing survives a restart. **Reset to defaults** discards it and falls back to
|
||
`.env`. Settings never contain the API key — that stays in the environment.
|
||
|
||
Resolution order when provider is **Auto**: OpenRouter if a key is set, else
|
||
Ollama if it answers, else the rule engine. The copilot panel's own selector still
|
||
overrides per question.
|
||
|
||
Copy `.env.example` to `.env` for the initial defaults. Notes:
|
||
|
||
- `OLLAMA_HOST` is often set machine-wide to `0.0.0.0` by Ollama itself. That is a
|
||
*bind* address and not dialable — the server normalizes it to `127.0.0.1`.
|
||
- Thinking models (the Qwen3 family) are sent `think: false`. Without it they
|
||
spend the whole token budget reasoning and return empty content.
|
||
- `COPILOT_PROVIDER=fallback` forces the rule engine, for rehearsing the worst case.
|
||
- Anything set in the settings page takes precedence over `.env` until you reset it.
|
||
|
||
## Verification
|
||
|
||
Two suites, no browser needed. Run both after any change to the model.
|
||
|
||
```bash
|
||
node scripts/simcheck.mjs
|
||
```
|
||
|
||
Checks the model tells the truth: signals stay in physical range, faults move OEE
|
||
the right way in a controlled A/B against an unfaulted line, packer stoppage
|
||
propagates upstream as blocking, a dropped sensor goes stale rather than to zero,
|
||
setpoint changes are lagged, runs are reproducible from a seed, and the prediction
|
||
arrives *before* the threshold is crossed (~435 s of simulated lead time).
|
||
|
||
```bash
|
||
node scripts/apicheck.mjs # server must be running
|
||
```
|
||
|
||
Checks the WebSocket stream, the control API, fault propagation into the stream,
|
||
and that the copilot answers in every provider mode with answers grounded in real
|
||
readings.
|
||
|
||
## Layout
|
||
|
||
```
|
||
server/
|
||
sim/ line.js (part flow, WIP, blocking) · stations.js (physics)
|
||
faults.js (injectable profiles) · kpi.js (OEE)
|
||
analytics/ trend.js (least squares) · anomaly.js (frozen baselines)
|
||
alarms.js (thresholds, latching, predictions)
|
||
ingest/ source.js (the seam) · simulatedSource.js
|
||
mqttSource.js, opcuaSource.js (documented stubs)
|
||
ai/ copilot.js (providers) · context.js (prompt) · rules.js (offline)
|
||
settings.js (validated, persisted runtime config)
|
||
web/src/
|
||
three/ Scene.jsx, Station.jsx, Conveyor.jsx
|
||
panels/ KPIs, station strip, trend charts, alarms, what-if, copilot
|
||
SettingsPage.jsx (provider/model picker + latency test)
|
||
scripts/ dev.mjs, simcheck.mjs, apicheck.mjs
|
||
```
|
||
|
||
The 3D scene loads **no external assets** — no HDRI, no CDN font, no texture. It
|
||
is primitives and analytic lights, so it works in a room with no wifi.
|
||
|
||
## Notable design decisions
|
||
|
||
- **A server owns the simulation**, not the browser. It keeps the API key server-
|
||
side, gives the ingest layer a real seam, and makes "swap in your historian"
|
||
true rather than aspirational.
|
||
- **Signals declare their analytics eligibility.** `cumulative` and `volatile`
|
||
flags in `stations.js` exclude a signal from anomaly testing — normal tool wear
|
||
accumulation is not an anomaly.
|
||
- **Anomalies only alarm in the direction that is bad**, inferred from which
|
||
thresholds a signal declares. A tool change dropping wear from 18% to 2% must
|
||
not raise an alarm for being *better* than baseline.
|
||
- **Separate RNG streams** for signal noise, stall decisions and reject rolls.
|
||
Sharing one stream put the reject roll at a correlated phase in the sequence and
|
||
measurably biased it — a 2.0% reject rate was firing at 5.7%.
|
||
- **Single dark theme, deliberately.** This is an ops dashboard on a projector.
|
||
Chart colours come from a validated palette; status colours are reserved and
|
||
always paired with a text label, never carrying meaning by hue alone.
|