08:12
67
84
6
83

Architecture

The simulation engine, optimization brain, live store, design system, and how the pieces fit together.

Pulse — Architecture

Pulse is a Next.js (App Router) client-rendered command center over a deterministic in-browser simulation. The design constraint that shapes everything: the boundary between the data source and the UI is the same contract a real hospital feed would use, so the simulation is swappable.

┌──────────────────────────────────────────────────────────────────────────┐
│  src/lib/sim  (the "hospital")                                             │
│                                                                            │
│   seed.ts ──▶ initial WorldSnapshot ──┐                                    │
│   engine.tick(world, dt, rng):        │                                    │
│     advance clocks ─▶ fire stochastic events ─▶ apply reducers            │
│     ─▶ append to eventLog ─▶ recompute predictors ─▶ computeMetrics       │
│     ─▶ runBrain() ─▶ NEW WorldSnapshot                                     │
└───────────────┬───────────────────────────────────────────────────────────┘
                │ commit (once per tick)
                ▼
        src/lib/store.ts  (Zustand: the single live snapshot + sim controls)
                │ fine-grained selectors  (src/lib/selectors.ts)
                ▼
        src/components/*  ('use client' islands)   ◀── user actions (src/lib/sim/actions.ts)

Data model — src/lib/types.ts

One canonical model, all timestamps as sim-minutes since epoch. Core entities: Unit, Bed (+BedAttributes), Patient (+WaitForecast, Barrier), StaffMember (+Credential), Gap (+CascadeStep), EvsJob, TransportJob, Alert (+EscalationStep), Recommendation (+ScoreFactor, alternatives), Metrics, SimEvent, Scenario, and the top-level WorldSnapshot that holds entity maps (Record<id, T>), the alert/recommendation arrays, the immutable eventLog, derived metrics, plus scenarioId and seed.

Simulation engine — src/lib/sim/

  • rng.ts — mulberry32 PRNG (Rng) so seed + scenario deterministically replays the same timeline; plus name generators and a uid.
  • scenarios.ts — presets (Calm Tuesday, Friday ED Surge, Flu Season + 3 Sick Calls, Code Influx) that reweight event rates (arrivals/hour, admit bias, sick-call/deterioration/ discharge rates, high-acuity share).
  • seed.ts — builds ~186 beds across 8 units, ~80 staff, an ED queue, boarders, coverage gaps, and EVS jobs, then runs one metrics + brain pass so the UI has content on first paint.
  • engine.tstick(world, dt, rng) returns a new snapshot each tick. It shallow-clones via clone.ts (new map/array containers, reused unchanged entity refs), advances EVS/transport clocks, fires Poisson-ish events (ED arrival, triage, room, admit, discharge order, bed vacate, sick call, deterioration, lab result), applies them as reducers, recomputes ED-facing predictors and census, then computeMetrics + runBrain.
  • actions.ts — pure user-action mutators (assignBed, acceptRecommendation, overrideRecommendation, fillGap, advanceCascade, expediteDischarge, resolveBarrier, acknowledgeAlert, resolveAlert, escalateAlert, requestEvs, advanceEvsJob). assignBed is the canonical closed-loop example: assign target bed → vacate & dispatch EVS for the old bed → create transport → raise a receiving-unit alert → tally boarding-hours/moves saved → mark the recommendation executed.

Optimization brain — src/lib/brain/

  • predictors.ts — explainable weighted-sum predictors; each returns {score, breakdown} so the UI renders factor × weight = contribution. (admitProbability, waitForecast, predictedLOSHrs, deteriorationTier, acceptLikelihood, scoreBedForPatient with hard-constraint gating.)
  • solvers.tsrankBedsForPatient, buildCascade / rankCandidatesForGap, unitRatio (acuity-weighted), computeMetrics (occupancy, boarders, ratio compliance, capacity/surge index, boarding forecast, closed-loop tallies).
  • brain.tsrunBrain(world): regenerates proposed recommendations for current needs while preserving anything the user has acted on, sorts by urgency × score, and refreshes standing alerts (ratio breach, EVS stall, boarding pressure) deduped by stable id. Discrete event alerts (sick call, deterioration crossing critical) are raised inline in the engine.

Store & realtime — src/lib/store.ts, src/lib/selectors.ts

A single Zustand store holds world, running, speed, and the Rng. SimulationProvider (src/components/providers/) runs a 1s interval calling tick() while running (1 real-sec = 2 sim-min × speed), and reads ?seed=/?scenario= on mount. Components subscribe through small selector hooks (useEdPatients, usePlacementQueue, useOpenGaps, useProposedRecs, useMetrics, …). The store contract — a snapshot plus an event stream — is identical to what an SSE/WebSocket/FHIR-Subscription bridge would push; going live means replacing the engine tick with a network source and keeping the store, selectors, and every component unchanged.

Design system — src/app/globals.css, src/components/ui/, src/lib/ui.ts

Tailwind v4, CSS-first. Tokens live in @theme: a near-black slate surface ramp, hairline borders, and the clinical urgency scale as the only saturated color (stable/info/caution/warning/urgent/ critical), with a separate sequential acuity ramp (ESI 1–5) so the two never collide. src/lib/ui.ts maps enums → color/label (urgencyVar, bedStatusMeta, esiMeta, severityTone, recTypeMeta). Primitives are hand-written (shadcn-style, no UI dependency): Card, Badge, Button, StatTile, UrgencyPill, StatusDot, CountdownPill, RingGauge, Sparkline, Meter, KanbanColumn, DataTable, Sheet, Tabs, AreaTrend (Recharts). Density-first, tabular-nums everywhere counts matter, motion minimal (only critical pulses).

App structure — src/app/

Root layout.tsx mounts <SimulationProvider> + <CommandShell> (nav rail + status ribbon). / redirects to /command. Flagship routes: /command, /flow, /beds, /staffing, /feed. Roadmap routes (/discharge, /perioperative, /ancillary, /safety, /transport, /revenue) render a ModuleShelf wired to the same stores. Feature components are grouped by domain under src/components/{command,flow,beds,staffing,feed,shared}.

Conventions

  • Files using hooks/state/store start with "use client"; static chrome stays server-rendered.
  • Colors only via tokens/helpers — never hardcoded hex in components.
  • The simulation is deterministic and side-effect-free except the provider's interval; the same seed + scenario always replays identically (no Date.now/Math.random in the model).