# Monte Carlo (Experiments)

`monte-carlo/` — runs many independent simulations of the same net with bounded memory, aggregates metrics inside the worker, and ships only small JSON metric frames to the UI. Frame buffers never cross the thread boundary.

Core
  `createMonteCarloSimulator(config)` → `MonteCarloSimulator` (synchronous, thread-agnostic)
Worker
  `monte-carlo.worker.ts` + `worker/messages.ts`
Host handle
  `createMonteCarloExperiment()` → stores: `status · progress · metrics` + events; `start/cancel/dispose`
Inputs
  SDCPN, marking, parameters, seed, dt, maxTime (required), runCount, metric specs
Outputs
  `MonteCarloWorkerProgress` (run counters) + `MonteCarloUserDefinedMetricFrame[]`

## Layers

Host application One experiment record per run of `createMonteCarloExperiment()`; subscribes to the handle’s status/progress/metrics stores and renders the metric frames.
Experiment handle (main) `runtime/experiment.ts` — worker mode (`createWorker`/`transport`) or local mode (runs the simulator on the calling thread, used by tests/embedding).
monte-carlo.worker Builds the simulator + compiles metric specs, then loops: `advanceAll()` × batchSize (default 4), post `progress` + pending `metricFrames`, yield, repeat. `cancel` is honoured between batches.
MonteCarloSimulator Owns N × `MonteCarloRun`; deterministic round-robin `advanceAll()` advances every active run one frame per call, so long runs don’t starve short ones.
Engine functions, reused Same enablement/lambda/kernel/dynamics code as quick sim (`transition-effect.ts` adapts them to the MC buffers).

## Run model

Per run | Meaning
--- | ---
`seed`, `parameterValues`, `initialMarking` | Defaults derived from the experiment config; overridable per run (`runs[]`), e.g. seed = base seed + index.
`status` | `ready → running → complete | error` — errors are per run, other runs continue.
`frameNumber · currentTime · rngState · completionReason` | Progress; a run completes on its own deadlock or the shared `maxTime`.
`tokenByteCount · tokenByteCapacity · reallocations` | Buffer telemetry, exposed in `MonteCarloRunSummary`.

## Memory — two buffers per run, swapped every step

#### step k

currentFrame (read) state at frame k
nextFrame (write) dynamics + transitions write frame k+1 here

#### after the step

swap pointers `current ⇄ next` — no allocation, no history. If the next token count doesn’t fit, the target buffer alone reallocates: `nextCapacityBytes = max(requiredBytes, capacity × 2, 64)`.
metrics observe frame k+1 then the data may be overwritten — readers are only valid during `observeFrame`.

The buffer layout (`frame-buffer.ts`) is a leaner sibling of the quick-sim `EngineFrame`: same sections, no 64-byte header, one extra `transitionElapsedFrames` section, and a token region with spare capacity:

place countsu32 × P
place offsetsu32 × P
transition elapsedf64 × T
elapsed framesf64 × T
firing countsu32 × T
fired flagsu8 × T
token structsbytes, used ≤ capacity
sparecapacity

All views (`Uint32Array`/`Float64Array`/`Uint8Array`) are created once per buffer over a single `ArrayBuffer`; IDs resolve to dense indices through the shared `EngineFrameLayout` (`layout.ts`).

## Metrics pipeline — computed where the data is

Specs `MonteCarloMetricSpec`: `expression` (metric code body) · `placeTokenCountMean` · `transitionFiringCount` — serializable, sent in `init`.
→
Compile before worker start Expression metrics carry HIR artifacts; built-in specs create a `measure(run)` function directly.
→
Sample per frame `observeFrame(ctx)` visits every run’s current frame as a `SimulationFrameReader` (`forEachRunFrame`); `sampleRuns` filters active/completed/all.
→
Aggregate Across runs: `mean·sum·min·max·last` → scalar, or keep the run axis and bin it → distribution (`exact` or bin `width`). Optionally aggregate over time.
→
Metric frames `MonteCarloUserDefinedMetricFrame` — scalar (`value/frameValue/timeValue`) or distribution (`bins: [value, frequency][]`) per frame. Small JSON.

## Protocol (`monte-carlo/worker/messages.ts`)

Direction | Type | Payload
--- | --- | ---
Host → Worker | `init` | `sdcpn, extensions?, initialMarking, parameterValues, seed, dt, maxTime, runCount, batchSize?, metricSpecs?`
`start` | —
`cancel` | — (checked between batches)
Worker → Host | `ready` | —
`progress` | `MonteCarloWorkerProgress` = advance counters (`advancedRuns, completedRuns, erroredRuns, activeRuns, allFinished`) + `frameNumber, time, runCount`
`metricFrames` | Pending `MonteCarloUserDefinedMetricFrame[]` batch
`complete` / `cancelled` | Final (or last-known) progress
`error` | `message, itemId`

Contrast with quick sim: no ack/backpressure — the worker free-runs to completion in small batches; the only upstream control is `cancel`. And the payloads are aggregates, not frames: the UI never holds Monte Carlo simulation state.

## What the host receives

- The handle exposes `status` / `progress` / `metrics` stores (`frames` + `latestByMetricId`); completion disposes the worker while the accumulated metric frames remain available for display.
- Scalar metric frames carry `value` / `frameValue` / `timeValue` — directly plottable as a per-frame series or a time-aggregated number.
- Distribution metric frames keep the run axis as `bins: [value, frequency][]` — hosts can paint a bins × frames heatmap and derive run aggregations (mean, median, percentiles) from the bins, all without ever holding simulation state.
- How Petrinaut’s React UI renders these is described in `@hashintel/petrinaut/ARCHITECTURE.md`.

Refactoring seams. (1) Runs are round-robin on one worker thread; the run-state model was designed so runs can shard across multiple workers later. (2) User code still receives decoded `TokenRecord` objects per firing — the README’s planned “IR compilation” would let lambdas/kernels operate directly on the numeric buffers. (3) Buffer growth is a naive ×2 doubling; arc-weight static analysis could size buffers up front and eliminate reallocations.