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

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.