Files
Digitaltwin/README.md
T

164 lines
8.3 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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.