# 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.