Browse documentation
Docs/Language

Queue and Process Simulation

A SIMULATION defines entity flow through sources, queues, services, routes and sinks. Reading or importing the definition runs nothing. An explicit RUN executes a finite, seeded native…

Queue and process simulation

A SIMULATION defines entity flow through sources, queues, services, routes and sinks. Reading or importing the definition runs nothing. An explicit RUN executes a finite, seeded native discrete-event run using ordinary compiled LIR expressions. Scene rendering does not advance its clock.

checkout = SIMULATION(cashiers: number)
  RESOURCE cashier CAPACITY cashiers
  SOURCE arrivals EVERY EXPONENTIAL(3min)
  QUEUE waiting FIFO CAPACITY 100 ON FULL TO turned_away
  SERVICE payment USING cashier FOR EXPONENTIAL(2min)
  SINK completed
  SINK turned_away

  arrivals -> waiting -> payment -> completed
END

reference = checkout
definition = checkout.DEFINITION()
run = checkout.RUN({cashiers: 2, seed: 42, horizon: 8h})

SIMULATION checkout(cashiers: number) … END is equivalent. Both spellings retain owning syntax through formatting, checked edits and source-free loading. The reference is data, not execution or publication authority. RUN requires one explicit argument record. DEFINITION() describes the checked signature, expanded topology and semantic identity without running it.

Entities and stations

ENTITY {id: string, urgent: boolean, passes: number} declares a closed immutable payload schema. Without a declaration, the first payload establishes its shape; no payload expressions means an empty record. Engine identity and visits remain separate from business fields. Numbers, units, symbols and bounded native collections may be data; callables, live handles and external authorities may not.

Clause Meaning
RESOURCE pool CAPACITY expression [CALENDAR rows] Exact nonnegative capacity; calendars contain at duration and integer capacity
SOURCE name EVERY timing [WITH record] First arrival follows its first delay; payload sees arrival.index and arrival.time
SOURCE name FROM rows AS row TIME row.at KEY row.id WITH row.payload Immutable recorded arrivals, sorted by time then original ordinal, with unique typed scalar keys
UPDATE name WITH {field: expression, …} Simultaneous pure patch: all fields read the same incoming entity
QUEUE name FIFO Queue-entry order
QUEUE name PRIORITY expression Finite numeric priority, lower first; entry order breaks ties
SERVICE name USING pool: count, other: count FOR timing [WITH patch] Atomic whole-pool grant; completion applies its patch before releasing resources
SINK name Count a departure at this named destination
a -> b -> c Static deterministic edges

All service entry uses an explicit queue, and each queue feeds exactly one service. USING pool means one unit. A waiter holds no partial resources. Demand is captured at queue entry, service duration at grant, and patience once per queue visit. Calendar decreases do not preempt active work; new grants wait for sufficient available capacity.

A finite queue adds CAPACITY n ON FULL TO refused or ON FULL ERROR. Capacity counts waiting entities only. A new arrival gets a service offer before its capacity check, so capacity zero permits immediate service. Only the new arrival can overflow; existing waiters are never evicted. PATIENCE duration ON EXPIRE TO abandoned gives each visit a deadline. A grant at that deadline wins.

Default arbitration is ARBITRATE FIFO FIT: eligible queue heads compete by request sequence; an unavailable whole demand may be bypassed by another queue head. ARBITRATE PRIORITY expression FIT adds a captured grant priority. Neither skips within a queue or promises starvation freedom.

Expressions, timing and routing

Definition parameters and pure helpers are frozen; callbacks cannot read live mutable cells. Pass changing inputs explicitly. Station expressions see the incoming entity, visit.index, and logical duration time. Recorded source projections see their row alias. Only route expressions may additionally read queues.name.length and resources.name.available; those are observations, not reservations.

Duration slots require explicit time dimensions. They accept durations or EXPONENTIAL(mean), UNIFORM(low, high) and TRIANGULAR(low, high, mode) over durations, including supported pure helper composition. Negative/nonfinite values and positive delays that cannot advance the clock fail. Zero durations are bounded by same-time work limits; the engine inserts no epsilon.

Routes end with END and name their destinations statically:

ROUTE inspect MATCH FIRST
  RULE repeat WHEN entity.passes < 2 TO waiting
  ELSE TO completed
END
Route Selection
MATCH FIRST First true rule; missing match without ELSE fails
MATCH UNIQUE Exactly one true rule; overlap fails
CHOOSE WEIGHTED with TO station WEIGHT expression Nonnegative finite weights, positive total, one categorical draw
CHOOSE MIN with TO station SCORE expression Lowest finite score; authored order breaks ties
BY expression with CASE :label TO station and optional ELSE One typed symbol maps to a checked destination

Conditions require booleans. Ordinary Result errors remain data and require an explicit routing policy. Rework preserves entity identity and creates new visits. There is no fork/join, preemption or automatic spatial/physics coupling.

At each time, apply capacity changes and due service releases, arbitrate existing waiters, then deliver arrivals/transfers in stable sequence. Drain bounded zero-time closure before expiring the whole remaining deadline cohort. A timeout route cannot rescue another already expired visit.

Each qualified station and draw purpose has an independent deterministic stream bound to seed and replication. Tracing consumes no draws. Existing unannotated simple-chain definitions retain their original v2 profile and seeded answers; typed parameters or any richer clause select grid.process.v3. Host definition selection uses this explicit process profile; add ENTITY {} to select it for an otherwise simple empty-payload chain.

Reusable flows

service_desk = FLOW(duration)
  RESOURCE worker SHARED
  INPUT incoming
  OUTPUT completed
  QUEUE waiting FIFO
  SERVICE serve USING worker FOR duration
  incoming -> waiting -> serve -> completed
END

Inside a simulation, USE desk = service_desk(1min) RESOURCES {worker: staff} creates independent qualified stations and internal resources, sharing only the explicitly mapped pool. Connect desk.incoming and desk.completed in chains. Local and imported FLOWs keep their helper scope. Port schemas must agree. Recursive USE, missing/duplicate mappings and disconnected outputs fail. A FLOW is an inert template and cannot run by itself.

Explicit runs and retained experiments

RUN requires a positive duration horizon, an exact unsigned 64-bit seed, and the declared parameters. Optional warmup must be below the horizon; replication identifies the independent stream. Unknown arguments fail. The horizon includes its same-time closure and never silently drains later work. Pure RUN accepts recorded Lists and complete inline native Frames. Use the Host operation below to snapshot external model-owned Frames; a formula never opens an external Frame handle itself.

The existing Host createExperiment(model, spec) accepts:

{
  "name": "Compare workshop capacity",
  "run": {
    "kind": "discrete_event",
    "seed": 42,
    "samples": 2,
    "symbols": ["counters.departed", "queues.waiting.wait.mean"],
    "process": {
      "definition": "workshop",
      "arguments": {"jobs": "<typed rows or model-owned Frame>", "technicians": 2, "benches": 1, "horizon": "<typed 16min duration>"}
    }
  },
  "scenarios": [
    {"name": "baseline"},
    {"name": "more_staff", "inputs": [{"symbol": "technicians", "value": 3}]},
    {"name": "more_benches", "inputs": [{"symbol": "benches", "value": 2}]}
  ],
  "materialize": {"frame": "repair_comparison"}
}

In the Experiments surface, select Process simulation, enter the definition name and Model argument bindings, for example jobs=jobs, technicians=staff, benches=bench_count, horizon=window. Define staff, bench_count and the duration window in the model. The Host resolves all bound symbols together once, freezes their native values (including complete owned Frame rows), and clears the live bindings from the admitted record. It refuses missing/error values and duplicate, unknown or nonmatching arguments. No worker or replay reads those symbols again. API clients use process.argumentSymbols:

{"definition":"workshop","argumentSymbols":{"jobs":"jobs","technicians":"staff","benches":"bench_count","horizon":"window"}}

Bindings and native literal arguments may be combined using distinct argument names. The experiment owns seed and replication; provide neither as a binding. The seed control accepts the complete unsigned-64 decimal range without conversion to a JavaScript number. Numeric parameter/scenario axes name the process argument (technicians), not its source model symbol (staff).

Inspect original output selects a retained matrix row and replica. It shows status/reason, unfinished work, sink totals, resource accounting, cohort-labeled statistics and contributing-replica counts; unavailable Options stay unavailable. Event history filters by entity/station and pages 50 rows at a time. Publish original trace writes a named analytical Frame including native entity payloads. Analyze original events selects a monitor in captured LIR and accepts optional numeric parameter bindings. Process-trace completeness and the monitor’s temporal coverage are separate: a complete event trace does not establish every temporal obligation. Unavailable or incomplete monitor conclusions remain explicit. The native Host API continues to accept arbitrary admitted typed monitor parameters. Verify captured replay explicitly compares all complete replicas against the entire retained original. Incomplete outcomes remain inspectable but cannot be certified as a complete replay. These controls use the existing presentation and never drive a Scene or physics clock.

Desktop document and hosted operations use the same selected Rust implementation. Hosted trace publication is a write; monitor analysis and replay are reads. Responses bind model, experiment, run and, for trace/monitor, replica. A native owner change refuses the operation and stale results are discarded.

The angle-bracket strings illustrate native typed inputs; they are not duration or Frame encodings. Supply real Grid values through the Host API. Numeric scenario and parameter axes overlay explicit numeric definition arguments. The existing experiment owner expands the matrix and independent replications. It freezes arguments, immutable Frame rows, exact input identities and captured LIR once. Workers never read live source or Frames. Frame inputs require a complete, schema-consistent snapshot; a row window is not silently promoted to a complete input. Pass an explicit record List when the selected sample is the intended schedule. Pure inline Frames freeze as data without an external storage read; external Frame handles retain their existing model-local access checks. Input identities retain hashes of the original IPC bytes or canonical inline value and of the exact frozen rows. Named metrics are finite numbers or Option numbers; only complete contributing replicas enter their summaries. Empty cohorts remain unavailable instead of becoming zero.

Inspect original output with getExperimentRunInspection. Process replay uses getExperimentRunTrajectory or the Rust replay_process_experiment_run API; it verifies the complete original result against exact captured LIR, arguments, semantic/sampler versions and input artifacts. Live replacement, restart and renamed portable import cannot substitute current code or inputs. Incomplete prefixes are inspectable but cannot be certified as complete replay.

Results, trace and interruption

The process result is grid.simulation.run.v3. It contains definition, semantic, sampler, seed and replication identities; status (:complete, :limited, :cancelled, :failed), complete, reason/phase and observed boundary; entity and visit counters; named sinks; queue and resource statistics; and unfinished age/wait summaries. Completion means the horizon closed, not that all jobs left. Waiting, service and transfer counts reconcile admitted minus departed entities.

Occupancy and busy/capacity integrals include idle tails over [warmup, observedBoundary]. Capacity changes use their actual integral. Wait/cycle cohorts are service starts/departures at or after warmup, retaining their full duration. Every tally names its cohort; observationInterval identifies the positive-duration interval, or NONE when it has not been observed. Empty tallies and zero denominators have Option summaries; no observation interval is claimed before warmup. Exact quantiles use a bounded collector and report whether that collector is complete.

TRACE EVENTS LIMIT n retains bounded events, including entity/visit, station or resource, sequence/microstep, cause, time and immutable payload. Trace truncation does not stop execution or change answers; it clears trace completeness. materializeProcessExperimentTrace(experiment, run, request) on the document owner publishes an original replica as a typed Frame; the request names replication, frame and optional payload projections with name, path, dtype and optional semanticTag. Numbers, booleans and text use primitive Arrow columns. dtype: {"object": "grid.value.v1"} selects the existing lossless native value carrier for records, choices, collections and refined scalars; an empty path selects the complete entity payload. Its value envelopes and declared semantic tags survive Frame publication, including empty and all-null columns. No executable or live authority is admitted as data. The Rust Host offers materialize_process_experiment_trace with the same contract.

evaluateProcessExperimentMonitor(experiment, run, request) explicitly evaluates a monitor from captured LIR over the original entity events. The request names replication, monitor and parameters. Resource-only events are outside this declared domain. The result binds original-result and monitor identities. A partial run or truncated trace never supplies complete absence coverage. Neither adapter reruns or advances the simulation.

Reading an original result verifies both its retained bytes and its canonical result digest. A matching byte descriptor alone does not establish that the result matches its admitted run. Saving a newly completed result computes that digest once; reading or importing it verifies it again without rerunning RUN.

Native failures and safety limits retain a reconciled partial outcome and an incomplete instant, with boundaryComplete: FALSE. Explicit local cancellation revokes the worker's publication permit first; the cancellation owner may retain a safely available bounded prefix before terminalizing missing matrix rows. Admission interruption or a lost model/document owner can leave no publishable prefix; the operation reports that interruption instead. Hard lifecycle cancellation never permits a late worker write. Replay is not resume.

Bounds and examples

Expanded admission ceilings are 4,096 stations, 1,024 pools, 16,384 edges, 1,024 component instances and depth 16. Default selected run ceilings are 1,000,000 events, 8,000,000 work units, 100,000 same-time work units, 4,096 visits per entity, 100,000 live entities, 200,000 scheduled events, 64 MiB inputs, 64 KiB per payload at depth 32, 100,000 collector samples and 10 seconds. The maximum horizon is ten years. limits may lower admitted ceilings; it cannot increase operator caps. Trace is bounded by 100,000 events and 64 MiB; original combined replication output is bounded by 64 MiB. Existing experiment model and record retention ceilings still apply. These are safety bounds, not throughput or latency promises.

The repair workshop compares one bench with two benches using three recorded jobs. Its exact mean waits are 180 seconds with one bench and 40 seconds with two; adding a technician alone does not remove that bottleneck. See also the reusable service desks and rework policy. Existing simulation operations retain their separate clock, spatial and physics contracts.

Source libraries for service comparisons and recorded reports

Supply service_flows.gs and process_reports.gs to the model's import resolver. These are repository source assets, not compiler-embedded modules or Registry publications.

USE "shared/service_flows.gs" AS desks
USE "shared/process_reports.gs" AS reports
baseline = desks.two_stage.RUN({jobs: jobs, workers: 1, intake_time: 1min, service_time: 2min, seed: 42, horizon: 10min})
summary = reports.summary(baseline)

The service comparison example uses the same recorded jobs for one-worker and two-worker scenarios. The imported simulation composes two service_desk FLOW instances: each owns its queue and service, while both explicitly map worker to the simulation's staff pool. Its empty entity schema makes this a scheduling recipe, not a payload-processing workflow. Durations, resource counts, horizon and recorded-row validity remain subject to existing simulation admission. It retains at most 1,000 trace events; a truncated trace does not imply an incomplete simulation horizon.

The report library accepts existing run values and does not invoke RUN, publish Frames, access external storage, or replay a retained experiment. summary keeps status, horizon completion, admitted/departed/unfinished counts, trace completeness and drained. The latter requires both a complete horizon and zero unfinished entities. queue_metrics preserves the native Option mean wait; resource_metrics returns raw busy/capacity integrals without inventing a ratio for a zero denominator.

completed_delta returns SOME(candidate departures minus baseline departures) only when both horizons completed; otherwise it returns NONE. This is a count difference, not a statistical estimate. The caller must ensure matching jobs, horizons and cohort definitions. Seed equality alone does not establish paired random streams when model topology or draw consumption differs.

The recorded analysis example uses explicitly illustrative result projections: its complete candidate still has unfinished work, its trace is truncated, and a cancelled comparison remains unavailable. Pass retained original run values to the same functions when using real records. Compiler acceptance covers imports and source/owning round-trips; fresh end-to-end runtime validation of these new recipes remains outstanding.