Simulated time
Simulated time
Simulation evaluates the resident model repeatedly over a hypothetical clock. It is separate from ledger time: a direct time-lane run is recorded as one replayable ledger event, while its internal steps do not create revisions or modify ordinary binding values.
Algebraic models without state cells are also supported. Each requested clock point evaluates the same compiled model with that run’s scenario inputs and clock context; zero steps evaluates the initial point once. No synthetic state is introduced.
Use STOCK(initial, net_flow) for integrated state, PREV(tracked, initial)
for a one-step memory, and DELAY(tracked, lag_steps, initial) for a fixed
history. SIM_TIME() and SIM_STEP() expose the current clock position.
Outside a simulation resolve, state functions return their initializer and the
clock functions return #N/A, so adding simulation formulas does not alter a
point-estimate resolve.
{
"simulation": { "t0": 0, "dt": 1, "steps": 52, "timeUnit": "week" }
}Persisted clock defaults can be overridden per request. scenarioInputs
apply only to that run. Euler is the default integrator; RK4 is available when
every state is a STOCK and is rejected for models mixing discrete
PREV/DELAY state. A materialize.frame request returns an immutable
model-local frame with column t and one column per requested output.
Every run has a stable runId and records its normalized request plus result
digest in model history. Simulation requests enforce configured step, sample,
event, population, and wall-time limits.
The same simulation surface supports seeded Monte Carlo fan charts, parameter-matrix experiments and calibration, queue/process models, and spatial-agent playback. Each experiment row has a stable run ID in its persisted experiment record. The host also attempts a temporal replay entry, but that append is best-effort; replay is available only when the entry exists, and neither record is a generic governed artifact. See the executable model-time warehouse example for a complete portable model.
Experiment parameter, scenario-input and requested-output cell addresses use
the same public coordinates as ordinary model edits. A1 and R1C1 addresses are
bound once when the experiment is created and retained as explicit cell_<id>
identities in results and replay. Named outputs and explicitly supplied cell IDs
keep their identities; experiment runs do not mutate the base model inputs.
For an original retained experiment result,
GET /api/models/:id/experiments/:eid/runs/:runId/inspection reads its immutable
native output without executing the model. This covers time simulation,
discrete-event and spatial-agent results, with explicit owner, run, source and
content identities. An older row without retained output is unavailable through
this query; it does not silently start replay. Each retained output is bounded
to 64 MiB and each model's output sidecars to 256 MiB. Archive ingestion verifies
the complete payload against its original-run digest before establishing the
restored native owner.
Authenticated native-document owners use the same experiment operations through the native document RPC. Admission, retained storage and worker publication are bound to the selected document's FileStore and exact owner incarnation. The local native-document matrix is capped at 256 runs and never falls back to a hosted model with the same name. Completed/cancelled experiments travel with a portable model home; import verifies them before replacement and binds retained Frames to the final published model home. Interrupted reads do not restart jobs. Receiving documents and commercial execution sessions do not yet support this adapter. Scene also supports bounded manual playback of retained WGS84 positions. Its source configuration names the saved experiment and run in the enclosing document/model; it never starts simulation or replay:
kind = "scene"
version = 1
[source]
symbol = ""
[source.recorded]
experimentId = "<saved experiment ID>"
runId = "<saved run ID>"The view displays at most 5,000 agents per selected step with a fixed angular origin, retained selection and camera. Longitude/latitude offsets use degrees and zero display elevation; this is not metric projection or general 3D state. Camera navigation does not advance recorded or realtime physics time. Print and saved recorded-view preferences are not supported in this first view.
Saved Time experiments also capture complete native-table and materialized Frame populations at admission. Their immutable source sidecar is bounded to 128 sources, 100,000 combined rows and 64 MiB of serialized input (256 MiB per model). Source revisions and content bind the experiment identity. Table edits or source Frame removal after admission do not affect the saved run or replay; missing or changed experiment-owned input bytes refuse replay.
New WGS84 authored runs retain a geometry Frame plus a population/time manifest.
The inspection response keeps numeric outputs and supplies recordedAgentPlayback
in place of full agent histories for Map presentation. The native original
output remains complete. The read-only
POST /api/models/:id/experiments/:eid/runs/:runId/agent-page accepts
{populationCell, step, offset, limit, sampleCap?} and returns recorded identities,
coordinates, time, original/sampled row counts and the next offset. limit and
optional sampleCap are 1–5,000; a page also fits 2 MiB of encoded output.
Oversized identity text refuses without clipping or changing its meaning. Offsets enumerate the selected sampled sequence;
omitting sampleCap enumerates every position at that step. Sampling uses the
same uniform stride as the Map's existing display cap. No page opens a live
source or starts replay. Older saved JSON and projected-coordinate inspection
keep their existing path; Map playback requires EPSG:4326 without reprojection.
The Frame has at most 100,000 combined rows and 64 MiB IPC bytes. Playback
manifests fit 2 MiB each and 64 MiB per model. Their recorded Frames fit a
256 MiB model budget with a conservative 64 MiB reserve before publication; multiple populations in one
retained Frame require a common CRS. Saved Frames survive recomputable
simulation pruning. Archive checks bind their bytes and metadata to the original
result before restore acquires native ownership.
Separately, for a completed successful simulate experiment row, the read-only
GET /api/models/:id/experiments/:eid/runs/:runId/trajectory query can replay
the stored admitted configuration without accepting new inputs or overrides.
New experiments capture a bounded canonical effective-input snapshot,
compiled-model fingerprint, semantic executor identity, and an exact private
compiled-model sidecar; each successful row retains a digest of its complete
canonical simulation result. The host size/digest-verifies those
experiment-owned bytes, evaluates in a fresh isolated session, and requires
both the full-result digest and stored summary to match. Legacy rows without
that evidence or owned bytes fail as unverifiable. Public disconnect and
private-RPC deadlines cooperatively interrupt bounded native simulation work
through the facade's reserved control connection; the native HTTP pool enforces
the two-worker minimum required for that request to arrive concurrently.
The response contains the
complete simulation trajectory but is capped at 64 MiB; the
Experiments surface renders no more than 500 evenly sampled points while
retaining that complete response. Discrete-event, spatial-agent, active, and
failed rows are not eligible. This query changes no model or experiment state
and creates no Frame, artifact, evidence, approval, graduation, or release.
Model-scoped POST /api/models/:id/calibrate returns an ephemeral final
optimum and counts. It publishes nothing. A deliberate
POST /api/models/:id/calibration/evidence action reruns the same bounded
solver against one exact resident model revision and pins the request, result,
optimizer identity, and a bounded/hash-complete evaluation trace as
grid.simulation.calibration-evidence.v1. That artifact is evidence input; it
does not approve, promote, or release the model. Formula-context CALIBRATE
still has no Solver Sim evaluator.
The Experiments surface also offers retained calibration through
POST /api/models/:id/calibration/runs. It uses the same native optimizer with
bounded iteration/evaluation progress, explicit cancellation and durable
checkpoints. The request may name up to 16 residual output symbols captured in
the original objective evaluation; those values remain distinct from objective
loss. Read the original run or its terminal result through the corresponding
GET endpoint. POST .../calibration/runs/:runId/evidence publishes that
completed run's retained result without solving again. Restart may mark
unfinished work interrupted; observation never silently resumes a solve.
Workbook archives carry terminal journals and their original evidence. Active
calibrations must settle before export, and imported published journals must
match their immutable artifact payloads.
See the function catalog for signatures and
rules-and-schedules.md for wall-clock reactions.
Authored agent populations on the workbook clock
SIM_AGENTS(rows, (agent, t) => patch, config) declares a population on the
same simulation clock as STOCK, PREV and DELAY. This first authored path
accepts an array of row objects. Every row has a unique nonempty text _id,
a native CRS-tagged point location, optional boolean alive, and matching
state columns with one frozen scalar type per column. Primitive state accepts
finite numbers, booleans and text. The expanded profile also retains dates,
datetimes, exact integers/decimals, finite complex values and admitted scalar
refinements such as currency, physical units and durations. All locations share
a CRS. Null state requires an explicit nullable schema.
input increment = 2
agents = SIM_AGENTS([
{_id: "a", location: GEOM_POINT(0, 0), energy: 1},
{_id: "b", location: GEOM_POINT(10, 0), energy: 3}
], (agent, t) => {energy: agent.energy + increment + accumulated + t},
{threads: 4, stateColumns: ["energy"]})
ready = SIM_AGENT_COUNT(agents, (agent) => agent.energy > 2)
accumulated = STOCK(0, ready)
time = SIM_TIME()Request ready, accumulated and time through the time-simulation API with
t0: 10, dt: 0.5, steps: 2. The count is [1, 2, 2], the stock is
[0, 0.5, 1.5], and the final energies are 26 and 28. Agents read the same
completed workbook/population snapshot; the stock integrates that snapshot's
count. Thread count and population input order do not change the result.
Outside a simulation, SIM_AGENTS exposes its initial rows or source handle
without stepping or reading population storage. Population aggregates for a
Table/Frame source require selected Analysis; ordinary resolution does not
hydrate those handles. SIM_AGENT_COUNT evaluates its one-argument boolean
predicate over alive rows.
Behaviors receive the previous row and logical time and return an object patch;
omitted fields persist. Patches may update declared state, location or alive;
they cannot change identity, CRS, state column names or semantic type tags. A
nullable column keeps its declared type while blank.
Dead rows retain history and stop executing behavior.
The population configuration admits stride, stateColumns, stateSchema, maxAgents,
timeoutMs and threads (up to 64). Clock and seed belong to the simulation
request. Behavior admission accepts direct lambdas and checked portable
closures using arithmetic, records, existing core collection/text
operations, GEOM_POINT, GEO_MOVE_TOWARD, time reads and population counts. Unknown functions,
unscoped volatile reads, provider calls and unsupported callable shapes refuse, including in
an unexecuted branch. Authored populations support deterministic and seeded
Monte Carlo Euler analysis. RK4 and exact sensitivities still refuse.
Scalar state and explicit schemas
Without stateSchema, the initial rows infer matching nonnullable column types.
Dates/datetimes, exact integers/decimals, complex numbers and admitted typed
scalars retain their types when behaviors read or update them. A bigint larger
than 2^53 stays exact; it is never widened to a float. Exact decimal digits,
datetime precision/offset text, complex components and unit/currency tags survive
history, replicas, source-free execution, restart and portable import.
Updates must return the declared tag. Ordinary compiled arithmetic can return an untagged number even from tagged operands; explicitly tag that result before assigning it to a tagged state column. Schema admission does not change formula arithmetic or infer a replacement currency/unit tag.
Use a complete schema to declare nullable columns or pin the admitted types:
stateSchema: {
visits: "bigint",
reserve: "decimal",
due: {type: "date", nullable: true},
budget: "currency:usd"
}This is a SIM_AGENTS configuration field. Each entry is a scalar type tag or an
object containing type and optional boolean nullable. text aliases string.
Author an absent nullable value as BLANK; retained JSON represents it as null.
Semantic labels use lowercase; qualified physical unit labels retain their case,
as in unit:K.
The map names every state column, excluding _id, location and alive. A
missing/extra column, unknown descriptor field, unsupported tag, changed semantic
tag or undeclared null refuses. Schema admission happens before agent workers
start; changing the config later cannot change an active run. stateColumns
continues to select recorded columns and does not declare state.
The expanded profile admits at most 128 state columns; names/type tags are each at most 128 UTF-8 bytes, and each tagged textual payload is at most 64 KiB. Primitive-only inferred row-record models retain earlier schema limits and result wires. Native captures use the bounded source schema below. Errors, nested objects/arrays, collections, handles, patterns and execution capabilities remain outside scalar state. Invalid dates, noncanonical exact payloads, nonfinite complex components and unsupported scalar versions refuse. Active state must fit the admitted per-population byte ceiling (64 MiB by default), accounted separately from history and checked even for unsampled steps and unrecorded columns. Recorded history also retains its row/byte bounds. These are logical payload bounds, not process RSS guarantees.
Expanded results include stateSchema. Their state values use a closed
{schemaVersion: 1, typeTag, value} carrier, with exact numeric digits retained
as text and complex values as {re, im}. Nullable blanks remain JSON null;
the schema preserves their intended type. Primitive number/boolean/text values
keep their existing JSON shape. Older readers refuse the new scalar/null payloads;
readers that ignore unknown fields can omit stateSchema on an otherwise
primitive snapshot. Older runtimes refuse the unsupported configuration. The
bytecode uses the existing selected compiled format minor version 28 and adds no
alternate evaluator. Numeric simulation outputs still require the existing
finite-number observation contract; recorded agent state is separate.
See 18-spatial-state-schema.grid
for exact counters, decimal updates, complex state, a nullable date, preserved
nanosecond/offset text and seeded replicas.
SIM_AGENT_RANDOM(stream) is available in an authored behavior lambda or a
portable helper with an admitted simulation-context contract.
It returns a uniform draw in [0, 1) keyed by the request seed, stable agent
identity, logical step and explicit stream number. A stream must be an integer
from 0 through 2^53−1; text, fractions, negative numbers and larger numbers refuse.
Two reads of the same stream in one agent-step return the same draw. Give
independent choices different stream numbers. Population order, thread count,
other agents' draw counts and the clock's time origin do not alter that key.
Dead agents do not draw. Ordinary cells and aggregate predicates cannot borrow
an agent's random context. Portable DEF helpers retain a fingerprinted
agent_random context requirement through calls and imports. Reading a helper
definition does not sample or start a run.
Seeded authored populations can now use the request’s samples ensemble.
See 10-agent-random-streams.grid.
The native movement/SIR callback and authored draws share the agent random-key implementation. This increment separates the step and stream roles and retains leading NUL bytes in identities, correcting aliases in the earlier key. Native SIR runs therefore have different draws from earlier builds at the same seed. Repeatability applies to the admitted model, seed and implementation version; retained trajectory playback continues to read its stored values.
GEO_MOVE_TOWARD(location, target, distance) moves a native point toward a
native target point with the same nonzero CRS. For EPSG:4326, coordinates are
longitude/latitude and distance is in metres along the WGS84 shortest geodesic,
including antimeridian and polar crossings. Other CRSs use Euclidean distance
in their coordinate units; the function does not project or assume metres.
Zero distance preserves the exact location, and reaching or passing the target
preserves the exact target. Coordinates and distance must be finite, distance
must be a nonnegative number, and WGS84 coordinates must be in range. Mixed
CRSs, non-point geometries and text distances refuse. An upstream error keeps
its originating code. Failed agent movement publishes no partial population.
Movement works in ordinary formulas, direct behavior lambdas and pure portable
DEF behaviors. The behavior's distance is per step; compute it from the intended
speed and the run's configured step duration. This function does not choose the
simulation clock or tie stepping to display cadence. See
11-geo-agent-movement.grid,
which counts arrivals and integrates their occupancy on the shared clock.
Requested workbook trajectories must be finite numeric aggregates. Sampled
population history returns separately as agentPopulations, with declaring
cellId, final population, sampled rows and spatial-work counters. Coercion
warnings are retained in each population result, bounded to 128 messages and
64 KiB; diagnostic saturation refuses the run. Failed or cancelled runs
publish no successful result and leave the resident workbook unchanged.
Immutable, model-owned geometry-bearing Frames can also supply the population.
Attach the materialized FrameHandle to an input such as Residents, then author
SIM_AGENTS(Residents, behavior, config). The Frame must contain _id text and
location point WKB with native geometry metadata and a nonzero CRS. State
columns accept Float64, boolean and text; the expanded adapter also admits
Int64/UInt64 as exact bigints, Date32 dates, and second/millisecond/microsecond/
nanosecond timestamps with no timezone or explicit UTC/Z. Timestamp precision
and negative epochs are preserved. Nullable state requires both a nullable
Frame field and an explicit nullable stateSchema. Declared type:<tag> column
metadata is retained and checked. Other timestamp zones, list/struct state and
unsupported physical types refuse. The host captures complete
rows once at Analysis ingress, before stepping; it does not use a preview or
read storage again on each tick. The result includes a bounded sourceRevision
for that capture. The unchanged handle remains the ordinary workbook value.
Frame capture refuses foreign-model paths, symlink escapes, missing or malformed
schemas, changed sources, nonfinite values, mismatched counts and row/byte
budget overages. Source IPC bytes and captured logical record payload each have
a 64 MiB ceiling; declared columns are capped at 128. Compressed IPC currently
refuses before batch decoding. Lazy or remote Frames must be materialized first;
a missing source adapter is an explicit refusal. Requested history sampling
never truncates the initial population. See
08-frame-agent-population.grid.
Mutable model-owned tables can supply populations too. Use a TableHandle for a
table whose location column has type geometry; native point geometry keeps
its CRS in versioned EWKB storage. The real table primary key (recId) becomes
agent _id. An editable state attribute never supplies identity. State columns
currently admit text, boolean, number and integers that widen exactly to the
runtime's numeric representation. Dates/datetimes and numeric currency/unit/
percentage metadata are also preserved. Nullable state requires an explicit
nullable stateSchema; _id, location and alive cannot be null. Wide stored
Table integers that cannot widen exactly still refuse; use exact authored or
Int64/UInt64 Frame state for those values. Optional alive must be boolean, and
non-point locations refuse spatial admission.
Native captures retain their complete state-column types alongside the rows, including all-null and empty columns. The authored schema must preserve those source names/types, with state names/type tags capped at 128 UTF-8 bytes. Frame tags must inhabit their physical column; exact integer and date/timestamp columns keep their corresponding exact/date type. Saved experiment inputs retain this metadata under the existing input digest, so restart/replay does not infer a different type from nulls. Expanded external state requires source-schema metadata; legacy row-only snapshots/providers retain the primitive inferred profile. Older snapshot readers reject the new metadata.
Analysis captures the complete table once in a read transaction, including its
current schema, persisted revision and rows. Source-byte preflight uses UTF-8
byte lengths and accounted stored-row structures under the same transaction,
with a 64 MiB limit and at most 128 columns; logical native records have a
separate 64 MiB limit. These are accounted payload bounds, not a process RSS limit.
Concurrent committed edits after capture apply to a later Analysis, while this
run keeps its admitted snapshot. Simulation never writes its resulting state
back to the mutable table. Individual stored geometries have a 1 MiB limit;
corrupt or noncanonical geometry storage refuses. Geometry fields can be supplied
through native values and imports; Table cells display them without opening a
text replacement editor. See
09-table-agent-population.grid.
Explicit Sim population/step/query bindings and portable context helpers use the existing compiled evaluator. Nested state, broader physical source types, optimized scale and complete Scene authoring remain under development. The native JSON movement/SIR path remains available separately.
Spatial-agent trajectory recording
The native simAgentRegions JSON path currently supports movement toward a
fixed target and spatial SIR. The authored array-population path above uses
the same bounded recording policy.
Native movement and neighbor queries now share the native Geo point primitives.
For EPSG:4326, speed * dt is a WGS84 geodesic distance in metres and SIR's
contact_radius is in metres. Other CRSs use their coordinate units. Target
coordinates use the population's CRS; no projection or CRS guessing occurs.
Queries include only live rows, exclude the acting row, and read one immutable
previous-population index per step. Nearest-k orders by distance and then stable
identity, including boundary ties. Invalid coordinates or queries refuse.
This corrects the older native JSON path's use of raw longitude/latitude distance. Existing WGS84 JSON runs using degree-sized speeds or radii need adjusted parameters. Cartesian examples now specify EPSG:3857 explicitly. Native seeded SIR outcomes can change with the corrected metric as well as the random-key correction described above. Authored sensing uses the same index and metrics under the selected behavior context described below.
Omitting config.stride (or setting it to zero) selects automatic sampling.
The initial and final population are recorded, with intermediate frames sampled
to fit the default ceiling of 100,000 trajectory rows and 64 MiB of accounted
retained payload. For 10,000 agents over 1,000 steps with small state, this means
ten frames rather than ten million rows. Automatic sampling selects its stride
once from the initial population; growing state can still exhaust the byte
ceiling and refuse the run. If the initial and final frames cannot fit, admission
refuses rather than dropping agents.
An explicit positive stride retains its existing meaning: record the initial
population and every kth completed step. It does not automatically append a
final off-stride frame. Requests exceeding the row ceiling refuse before stepping;
the byte ceiling is checked before each recorded frame is copied. A trajectory
budget failure does not commit that step. stateColumns selects retained state
columns; an empty list retains all state columns.
Accounted bytes include row/state value structures, scalar metadata and retained UTF-8 payloads. Active state is bounded independently of recording selection. They exclude allocator overhead, spare vector capacity, index storage, population buffers and serialized Frame output, and therefore are not a process-memory or wire-size guarantee. The run's population, work and timeout limits remain separate.
Authored spatial sensing
Direct agent behavior lambdas can query the completed population snapshot:
agents = SIM_AGENTS([
{_id: "a", location: GEOM_POINT(179.999, 0), nearby: 0},
{_id: "b", location: GEOM_POINT(-179.999, 0), nearby: 0}
], (agent, t) => {
nearby: NEIGHBORS_WITHIN(agent.location, 250)
}, {})
nearby_agents = SIM_AGENT_COUNT(agents, (agent) => agent.nearby > 0)NEIGHBORS_WITHIN(location, radius) counts live neighbors in the acting
agent's own population. NEIGHBORS_WITHIN(population, location, radius) queries
another admitted previous population or an alias of one. Both use closed radius
boundaries, WGS84 geodesic metres for EPSG:4326 and Euclidean coordinate units
otherwise. The query point and population must share a nonzero CRS.
NEAREST_AGENTS(population, location, k) returns up to k complete live row
records, sorted by exact Geo distance and then stable _id. k must be a
genuine finite integer from 0 through 1024. Zero returns an empty array. Ties
at the last position retain stable identity order. Queries exclude the acting
agent only when querying its own population; matching IDs in another population
remain eligible. Dead rows remain in histories but do not appear in queries.
All populations prepare their indexes before any population advances. An alias
retains the admitted population's immutable storage, so it reuses the same
index. A newly constructed or transformed array is not an admitted query source
and refuses with SIM/AGENT_QUERY_POPULATION; queries never build an index or
scan a new population at operation ingress. Result rows may be consumed by
ordinary INDEX, FILTER and SIM_AGENT_COUNT expressions.
Spatial queries require the selected authored agent invocation. Ordinary
formulas refuse with SIM/AGENT_QUERY_CONTEXT. Portable DEF behaviors and
helpers can query the admitted previous-population snapshot: their callable
contracts retain an agent_snapshot context requirement. Seeded draws retain
agent_random; SIM_TIME/SIM_STEP retain simulation_clock. Requirements
propagate through helpers and checked imports and are included in semantic
fingerprints. They grant no network, connector or storage authority. Aggregate
predicates cannot hide query/draw requirements inside a helper.
The compiler emits explicit Sim population, step and query bindings and uses the
same compiled evaluator and run-owned agent stepper. Source-free bytecode needs no
source lookup at execution. These selected artifacts use compiled format minor version 28; older
readers refuse them. Ordinary artifacts keep their existing version. Missing,
forged or unsupported context contracts refuse, including in an unexecuted
branch. See portable spatial behavior
for movement, sensing, nearest-agent selection, draws and clock reads in reusable
helpers. Existing state, query, recording and Euler-only limits still apply.
Pure native point construction, CRS assignment and X/Y extraction are admitted
through the existing callable evaluator. Invalid point geometry, mismatched CRS,
nonfinite/negative radius, lossy text arguments and invalid k refuse before a
population step commits. The caller scope is restored on return, refusal and
panic. These explicit bindings remain on the Workbook execution path. Full native
Sim/SpaceQuery execution beyond that selected profile, nested state, optimized
scale and physical-cost evidence, and complete Scene/map integration remain open.
Rigid-body physics worlds
PHYSICS_WORLD declares a world of rigid bodies that fall, collide, slide,
and bounce under a fixed-step clock. The world runs on request (the physics
lane or a physics experiment); an ordinary recalculation never steps it.
# Inputs: launch speed in m/s and ball restitution
launch_speed = 6
bounce = 0.6
# World: a ball launched across a floor toward a crate
test_rig = PHYSICS_WORLD({
bodies: [
{ id: "floor", body: "fixed", position: [0, -0.5, 0], shape: { kind: "cuboid", halfExtents: [20, 0.5, 20] } },
{ id: "ball", position: [-4, 0.25, 0], shape: { kind: "ball", radius: 0.25 }, density: 200 },
{ id: "crate", position: [2, 0.4, 0], shape: { kind: "cuboid", halfExtents: [0.4, 0.4, 0.4] }, density: 250 }
],
config: { steps: 240, dt: 0.016666666666666666, stride: 4, maxBodies: 100, timeoutMs: 10000 },
bindings: [
{ symbol: "launch_speed", body: "ball", field: "linvel.x" },
{ symbol: "bounce", body: "ball", field: "restitution" }
]
})The test_rig cell keeps its specification as an ordinary value. The
specification must be a literal; one-row arrays such as [0, -0.5, 0] are
vectors.
- Bodies. Each body has a unique
id, abodykind (dynamic, the default;fixed; orkinematic, which moves only as told, by its authored velocity or by live inputs, and pushes dynamic bodies without ever being pushed, as a player's paddle or a moving platform should), aposition, an optionalrotationquaternion[x, y, z, w], optionallinvel/angvel, and ashape:ball(radius),cuboid(halfExtents),capsule_y,cylinder_yorcone_y(halfHeight,radius; a cone's apex is at+halfHeight),convex_hull(points),trimesh(vertices,triangles),compound(parts), orcell(symbol). A convex hull wraps a cloud of at least four points that are not all in one plane, and is the way to give a moving body an arbitrary convex shape. A compound joins up to 256 convex pieces into one body with one mass, which is how a concave solid moves; each part is{ shape, position, rotation }in the body's frame, and its shape is any kind excepttrimesh,compoundandcell. A triangle mesh is exact and may be concave, so it has no well-defined mass and is for fixed bodies such as terrain. Acellshape is read from a model value when the run starts; see CAD solids as colliders. Mesh coordinates are in the body's own frame; a shape holds at most 20,000 points and 40,000 triangles. Materials aredensity(default 1),friction(default 0.5), andrestitutionin [0, 1] (default 0). Setccd: trueon small, fast bodies so they cannot pass through thin ones. - Joints. Optional
jointsconnect two bodies:{ id, bodyA, bodyB, anchorA, anchorB, joint }. Anchors are points in each body's own frame (default[0, 0, 0]). Thejointis{ kind: "fixed" }(welded),{ kind: "spherical" }(ball-and-socket),{ kind: "revolute", axis }(hinge), or{ kind: "prismatic", axis }(slider); hinges and sliders take optionallimits: [min, max]in radians or distance. Jointed bodies still collide with each other, so place their shapes so they do not overlap. - Clock.
configholdssteps,dt,gravity(default[0, -9.81, 0]),stride,maxBodies,timeoutMs, andsolverIterations(default 4, which is also the number of internal substeps per step). Stride0keeps the initial and final frames and samples between them within 100,000 rows. - Size.
lengthUnit(default 1) is the typical size of a moving body in metres. Contact tolerances are absolute distances scaled by it: at the default the engine tolerates 1 mm of penetration and predicts contacts 2 mm out. That is fine for crates and wrong for small parts: a 5 mm ball rests a quarter of its radius into the floor. SetlengthUnitnear the size of the parts (0.005for that ball) and it rests within about 1%. The contact stiffness follows automatically;contactStiffnessHzoverrides it (Rapier's default is 30). A world whose thinnest moving body is under 20 tolerances compiles with aMIR_SIM_PHYSICS_SMALL_BODIESwarning that names the body. - Elastic play. Restitution 1 keeps a ball's speed against a single
surface, but not against a field of adjacent fixed boxes: where two boxes
meet, the solver sees an edge that is not really there and the ball gains or
loses speed, sometimes a lot. A game that needs a constant ball speed holds
it with a rule each tick, as every breakout does; a wall that must be seamless
is one body (one
cuboid, or atrimesh), not a row of them. - Bindings. A binding reads a model value when the run starts. The fields
are
position.x|y|z,linvel.x|y|z,angvel.x|y|z,density,friction,restitution, and the world fieldsgravity.x|y|z(which omitbody). A bound value passes the same checks as an authored one, so a restitution of 1.5 refuses the run instead of producing an impossible world.
Run a world with POST /api/models/:id/simulate/physics and
{ "regionIndex": 0, "scenarioInputs": [{ "symbol": "launch_speed", "value": 8 }], "materialize": "rig_playback" }.
Scenario inputs apply only to that run. The response contains every body's
final pose and velocity, the sampled trajectories of the dynamic bodies, and the
number of active contacts. materialize also writes a playback frame (step,
body_id, t, position, rotation quaternion, and velocity columns). Each run
is one replayable ledger event, and simulate/runs/:runId/verify re-executes it
and compares digests.
To use a result in formulas, call PHYSICS_RUN(world, inputs). It runs the
world and returns its final state, so the answer recalculates whenever an
input changes:
# Run the rig with the model's current inputs
outcome = PHYSICS_RUN(test_rig, { launch_speed: launch_speed, bounce: bounce })
# Where the crate ends up, and how fast the ball is still moving
crate_travel = outcome.bodies.crate.x - 2
ball_speed = outcome.bodies.ball.speedinputs gives a number for every binding symbol the world declares; a missing
one is an error. The result holds completedSteps, activeContacts, and for
each body x, y, z, vx, vy, vz, speed, and sleeping. Unlike the
physics lane, PHYSICS_RUN is an ordinary formula: a model that calls it
simulates during recalculation, and only when the world or its inputs change.
It keeps no trajectory; use the lane or an experiment for playback.
Because a run is a function, numeric solvers can search through it. This finds the launch speed that moves the crate 0.2 m (it starts at x = 2):
needed_speed = ROOT((speed) => PHYSICS_RUN(test_rig, { launch_speed: speed, bounce: bounce }).bodies.crate.x - 2.2, 0, 20, 0.001, 60)SOLVE statements need a differentiable objective, so they cannot search
through a physics run; use ROOT, NSOLVE, or MINIMIZE.GLOBAL.
In the Experiments surface, choose Rigid-body physics to sweep bound
inputs. Outputs are body_count, active_contacts, completed_steps,
trajectory_rows, or a body's final <id>.x|y|z|vx|vy|vz|speed. An inspected
run opens a 3D playback.
CAD solids as colliders
A solid designed in the Sketch surface can be a body in a physics world. In the Sketch surface, Publish collider to cell writes the solid's collision shape into a cell; a body then names that cell:
# Published from the Sketch surface; written out here so the example stands alone
bracket_collider = { kind: "compound", parts: [
{ shape: { kind: "convex_hull", points: [[-0.2, -0.2, -0.05], [0.2, -0.2, -0.05], [0.2, 0, -0.05], [-0.2, 0, -0.05], [-0.2, -0.2, 0.05], [0.2, -0.2, 0.05], [0.2, 0, 0.05], [-0.2, 0, 0.05]] } },
{ shape: { kind: "convex_hull", points: [[-0.2, 0, -0.05], [0, 0, -0.05], [0, 0.2, -0.05], [-0.2, 0.2, -0.05], [-0.2, 0, 0.05], [0, 0, 0.05], [0, 0.2, 0.05], [-0.2, 0.2, 0.05]] } }
] }
# World: the bracket dropped flat onto a bench, with gravity along -z
bench_test = PHYSICS_WORLD({
bodies: [
{ id: "bench", body: "fixed", position: [0, 0, -0.5], shape: { kind: "cuboid", halfExtents: [2, 2, 0.5] } },
{ id: "bracket", position: [0, 0, 0.4], density: 2700, shape: { kind: "cell", symbol: "bracket_collider" } }
],
config: { steps: 180, dt: 0.016666666666666666, gravity: [0, 0, -9.81], maxBodies: 10, timeoutMs: 10000 }
})
# Where the bracket's centre comes to rest
bracket_height = PHYSICS_RUN(bench_test, { bracket_collider: bracket_collider }).bodies.bracket.zA published collider is a physics shape, so it can be read, stored and edited
like any other value. It carries a cad record naming the solid and the
sketch revision it came from.
- Exact, not approximated. A solid with flat faces is published as the same convex pieces the Sketch surface uses for interference checks, so a concave part keeps its notches and holes. Cylinders, cones, spheres and capsules are published as those shapes. Nothing is sampled.
- Fixed-only fallback. A solid with no such form, such as a general
revolve, is published as its triangle mesh when the preview mesh is
complete. The
cad.movablefield is thenfalse, and the body must befixed. - Units and origin. Coordinates are converted to metres from the sketch's
length unit. The origin is the centre of the solid's bounding box, so a
body's
positionsays where the middle of the part goes. - Read at run start. The physics lane, an experiment and a live session
read the cell when the run starts.
PHYSICS_RUNtakes the value in its inputs, keyed by the cell's name, as above.
Contact tolerances suit bodies from a few centimetres to tens of metres. A part only millimetres across will collide, but with visible overlap.
Live sessions
A run steps a fixed number of times and returns. A live session steps the same world in real time and takes input while it runs:
POST /api/models/:id/simulate/physics/livewith{ "regionIndex": 0, "scenarioInputs": [...] }starts a session and returns itssessionId, the stepdt, and every body's initial state. Bound cells are read once, at start; later model edits do not reach a running session.GET .../live/:sessionIdreturns the latesttick, simulated timet, andposes: seven numbers per body (x, y, zthen the rotation quaternion), in the order the start response listed the bodies.removedlists the bodies taken out of play, which keep the pose they left at.contactslists which bodies started or stopped touching ({ "tick", "a", "b", "started" }, ids withabeforeb) after?since=<tick>, or every retained contact when no tick is given; a session retains the last 4,096, andcontactsTruncatedis set when some that were asked for are gone.GET .../live/:sessionId/streamsends binary pose frames on one response for as long as the session runs, athzframes per second (default 30, at most 60). The first frame carries every body; each later one carries only the bodies that moved.GET .../live/:sessionId/poses?since=<tick>returns one such frame for a viewer that already has that tick. See Pose frames.POST .../live/:sessionId/inputapplies an input before the next tick:{ "kind": "impulse", "body": "ball", "impulse": [1.5, 0, 0] }pushes a dynamic body;{ "kind": "set_velocity", "body": "paddle", "linvel": [3, 0, 0] }sets a velocity, which a kinematic body keeps until told otherwise; and{ "kind": "set_pose", "body": "ball", "position": [0, 0.9, 0] }(with an optionalrotation) places a body, a kinematic one at the next tick and a dynamic one at once, keeping its velocity.{ "kind": "remove", "body": "brick_2_3" }takes a body of any kind out of the world for the rest of the session: it keeps its last pose, later ticks skip it, whatever rested on it is unsupported, and it refuses every further input. Otherwise fixed bodies refuse every input and kinematic bodies refuse impulses: they are steered, not pushed.DELETE .../live/:sessionIdstops the session and records it.
Engineering inspection export
Engineers can explicitly select a bounded capture through the native host's
start_physics_live_inspection API and export its retained data to a local
Rerun recording. Physics poses, computed observations, accepted inputs,
contacts, rule firing markers and metrics share a simulation timeline. Export
requires the separately installed optional SDK; ordinary sessions and model
resolves create no capture or Rerun connection. Existing HTTP/UI behavior is
unchanged, and older recordings lack the per-tick signals needed for this export.
The canonical support-removal diagnostic shows a contact-driven rule removing the floor, followed by falling height and velocity. See Optional Rerun simulation inspection for capture/export commands, timestamp conventions and limitations. The explicit export command supports body/observation plot focus. The unchanged Breakout controller also has a retained multi-body recording showing contact-related speed changes, accepted relaunches and speed correction.
Live cells
A world can bind model cells to its live sessions, so a game or an experiment's control logic is written in cells rather than in a client:
observewrites readings into input cells after every tick:{ symbol: "ball_y", body: "ball", field: "position.y" }. Fields areposition.x|y|z,linvel.x|y|z,angvel.x|y|z,removed(1 once the body is gone),contactswith a partner ({ symbol: "gutter_hits", body: "ball", field: "contacts", with: "gutter" }counts how many times the two have started touching), and the world fieldstickandt, which take no body. Declare the targets as inputs: a reading lands on its cell like an edit, so formulas recompute andWHENrules fire on the session's tick.drivewatches cells after every tick and turns a changed value into an input to the session.{ symbol: "ball_hold", body: "ball", field: "linvel" }sendsset_velocityfrom a[x, y, z]cell;positionsendsset_pose;linvel.xorposition.yreplaces one component and keeps the others;removedremoves the body once its cell is true or nonzero. A fixed body can only be removed.BLANKsends nothing and forgets the last value, so a formula such asIF(ball_y < 0.3, [paddle_x, 0.9, 0], BLANK), or a rule that writes the cell and another thatRESETs it, sends once per occasion. Driven inputs are logged, counted and replayed exactly like inputs sent by hand. A driven cell is read on the ticks where a reading or a rule-written cell it depends on changed, and every driven cell is read once a second regardless, so an edit made outside the session reaches it within a second.
The exchange runs on the session's own tick, only for a world that declares
bindings, and holds the host for the time it takes to write the readings,
fire the model's due rules and resolve the driven cells
(grid_simulation_physics_live_exchange_seconds_total{phase} accounts for
each part). A tick that cannot take the host within 5 ms skips its exchange
and the next tick carries the latest readings
(grid_simulation_physics_live_exchanges_skipped_total counts them). Rules
fire on every tick, but the temporal ledger captures the model at most once a
second during a session, as it would under the rule scheduler, and once more
when the session stops. A batch PHYSICS_RUN has no live cells, so these
bindings do not apply to it.
The breakout example
plays its whole game this way: a struck brick leaves play, the ball is held
at launch speed, and a lost ball costs a life and returns to the paddle.
In the Experiments surface, the Rigid-body physics lane shows a Live session panel: run the region live, drag to orbit, and click a body to push it away from you.
A stopped session is recorded as one run with its complete input log, so
simulate/runs/:runId/verify replays it and confirms the same final state.
A session that ends on its own can still be recorded by stopping it; one that
is discarded without being stopped is not recorded.
A host runs at most four live sessions. A session ends after ten minutes, or fifteen seconds after its last viewer stops reading, and accepts at most 50,000 inputs, driven ones included. If the host cannot keep up with real time, the session runs slower; it never skips a step.
Pose frames
The live view reads the stream, which is what makes a world of thousands of
bodies watchable: a frame for 10,000 bodies is about 280 kB against roughly
1 MB of JSON, and once the world settles a frame is 28 bytes. On the stream
each frame is preceded by its length as a little-endian u32. A frame is
little-endian:
| Offset | Type | Field |
|---|---|---|
| 0 | 4 bytes | GPF2 |
| 4 | u32 |
tick |
| 8 | f64 |
simulated time |
| 16 | u8 |
flags: 1 = running, 2 = keyframe, 4 = contacts truncated |
| 17 | u8 |
end reason: 0 none, 1 stopped, 2 idle, 3 finished, 4 failed |
| 20 | u32 |
bodies in the session |
| 24 | u32 |
entries that follow |
A keyframe has one entry per body, in the start response's order: seven f32
(x, y, z, then the quaternion). Any other frame has one entry per moved
body: its u32 index, then the same seven f32. A viewer that falls behind
is not sent a backlog; its next frame carries everything that moved since the
last one it received.
A frame that ends there reports no removals and no contacts. Otherwise two
trailers follow: a u32 count of removed bodies and their u32 indices
(every removed body in a keyframe, those removed since the viewer's tick in a
delta), then a u32 count of contacts and, for each, u32 tick, u32 body
a, u32 body b, and u32 1 if they started touching or 0 if they
stopped. Contacts are sent only to a viewer that named a tick, and only those
after it; the truncated flag says some after it were no longer retained.
Frames are for display. Recorded results and replay stay in double precision.
Repeatable results
Results repeat bit for bit, on one machine and across machines: the same world
gives identical results on macOS and Linux, on Arm and x86-64. A run or a live
session recorded on one computer therefore verifies on another. Bodies are
added in id order, so listing them in a different order does not change any
result. Results can change when a Grid release updates the physics engine, so
a recorded run names the engine that produced it, and verifying it on a
different engine is refused with that reason instead of a failed comparison.
Recorded authored-agent output
The bounded 0.69.0 MVP is demonstrated by
the evacuation model. Run it as a Time simulation
experiment with dt = 1, steps = 5, then choose Inspect original output
to select a population and scrub its recorded positions on the Map.
An empty history cannot establish a Frame CRS and refuses materialization;
the retained empty run remains inspectable.
Saved Time experiments also capture admitted native Table/Frame populations.
Their frozen rows and revisions belong to the run; editing a source afterward
does not change the original output or replay inputs.
Run an authored agent model as a Time simulation experiment. Inspect original output reads the retained run and opens a Map with a time scrubber for each selected population. Scrubbing reads recorded positions; it never advances the model or re-resolves cells. Map playback requires EPSG:4326 and shows at most 5,000 recorded agents per time, including recorded positions of stopped agents. Other CRSs remain supported by simulation but refuse this Map playback without reprojection. Scene can display saved WGS84 steps through the explicit recorded-run source described above, independently of realtime physics.
For an authored run, materialize: {frame: "evacuation_path"} now records the
agent trajectories in one immutable Frame. Its columns include population_cell,
step, t, agent_id, x, y, crs, alive, state (JSON text), and native
point location. Numeric workbook outputs become series:<symbol> columns,
repeated at the corresponding recorded step. Frame populations must share one
CRS. A run without agents keeps the ordinary numeric trajectory Frame shape.
The experiment inspector uses the original retained run JSON; the geometry Frame
is separately readable through the native Frame APIs.
Retained original experiment outputs share a separate 256 MiB quota per model
across runs. Repeated large runs can exhaust that quota even when each response
and Frame fits its own limit. Retention then refuses explicitly with
ORIGINAL_OUTPUT_RETENTION_FAILED; existing originals are not silently discarded
to admit the next result.
The complete authored response is limited to 64 MiB of JSON, with 16 KiB reserved before Frame materialization for metadata. The combined Frame is limited to 100,000 rows, 118 requested workbook outputs and 64 MiB of actual IPC bytes. Its name must be nonempty and at most 128 UTF-8 bytes. Overages refuse; these limits do not claim a process-memory bound. The evacuation example demonstrates staggered departures and capacity feedback. The scalar-schema profile above and replicated Euler follow-up below extend this original MVP subset; nested state and broader source adapters remain outside it.
Authored capacity and epidemic reference models
The simultaneous evacuation model
uses separate request and travel ticks. All residents read the same prior claims;
stable identity breaks request-time ties. A moving resident reserves capacity,
and a rejected resident can request another available shelter on a later tick.
Run dt = 1, steps = 8: shelter claims never exceed 2 and 3, five residents
arrive and one waits because total capacity is five. The occupancy STOCK reads
the previous sheltered count, so its final value is 23.
The authored SIR model
uses native Geo contacts and separate identity-keyed infection/recovery streams.
Newly infected residents cannot recover in the same tick. Its independent
susceptible/infected/recovered STOCK cells supply a well-mixed Euler comparison;
a stochastic path need not equal that curve. Set Time simulation dt to the
model's time_step input (default 0.1), choose 200 steps and an explicit seed.
The model supports at most 1,024 people because its contact-record lookup uses
the bounded nearest-query API. A radius of zero gives no contacts for its distinct
points. Set samples to run an ensemble in one request; numeric fan charts
aggregate the separate authored paths. The retained paths identify their exact
replica index and seed.
Both examples use the existing evaluation semantics and recorded Map/Scene journey. They do not establish the full 10,000-agent/500-step performance envelope.
The moving neighborhood SIR model
adds movement, eight nearest contacts, radius counts and workbook prevalence
feedback on that same clock. Each person moves toward a common destination;
when more than half the population is infected, the next movement step uses
half speed. Independent identity-keyed draws govern infection and recovery, and
infected_person_time integrates the previous infected count. This finite-contact
model has different epidemic dynamics from the well-mixed STOCK reference.
The default inputs declare 10,000 residents and four workers. Run Euler with
dt: 0.1, steps: 500 and seed: 42, selecting susceptible, infected,
recovered, agent_total and infected_person_time. Stride 100 retains six
complete population snapshots (60,000 rows); materializing the result uses the
existing bounded geometry Frame and original Map/Scene pages. Returned nearest
contacts stay bounded at eight even as population size grows. Reducing the
stride may exceed recording limits and explicitly refuse the run.
Operator deadlines apply independently of the model's timeoutMs. The outer
simulation owner defaults to 30,000 ms; increasing a population deadline does
not override it. An operator must select a sufficient overall deadline for a
full-size run, and Host jobs retain their own deadlines. The local Performance
observer explicitly uses 600,000 ms owner/stepping limits for both worker arms;
that comparison profile does not change product defaults or promise completion
within its deadline on every machine. Smaller populations are useful for
exploring the model with existing operator limits.
Authored stochastic replications
A request such as {dt: 0.1, steps: 200, samples: 8, seed: 42, retainSamples: true} runs eight authored Euler populations through the same
native evaluator and workbook/agent clock. Workbook uncertainty draws keep
existing per-run/per-step semantics. Agent seeds are derived from the root seed
and zero-based replica index using the versioned AGENTRP1 domain; increasing
samples preserves earlier replicas. Each replica’s published decimal-text seed
can be supplied to a deterministic request with the same initial inputs to
replay its agent draws. Workbook uncertainty also needs its original scenario
and step draws; replaying the original ensemble request reproduces both. Numeric seed
inputs remain accepted; canonical unsigned decimal strings preserve all 64 bits.
The response’s numeric outputs are medians and bands contain quantiles, means,
standard deviations and final distributions. Actual agent paths live separately
in agentReplications, each with index, lossless seed and agentPopulations.
There is no aggregate agent population or median geometry. With
retainSamples: false, replica identities remain but histories and numeric
replicationTrajectories are omitted; numeric summaries are unchanged.
External population sources are captured once per distinct handle at ensemble
admission. Replicas share those frozen rows and revisions even if a table changes
later. Every initial population is validated before agent workers start. Combined
initial populations and retained history each have a 100,000-row and 64 MiB
accounted-payload budget; source captures have separate combined limits of the
same size. At most 128 population declarations and 128 distinct external sources
are admitted. Agent evaluations across the ensemble must fit
GRID_SIM_MAX_EVALUATIONS (default 10 million). Numeric buffers and quantile
buffers, retained replica configurations and workbook uncertainty draw payloads
each have a 64 MiB bound, at most 128 quantiles are accepted,
and the whole serialized ensemble must fit 64 MiB. These are payload/work
bounds, not a process RSS guarantee. Automatic recording stride divides the
history budget across replicas; an explicit stride that exceeds its quota
refuses rather than silently changing the requested recording policy.
For a spatial Frame, explicitly select a retained path:
materialize: {frame: "sir_replica_2", replication: 2}. Its point geometry and
series:<symbol> values come from replica 2 on its exact recorded clock.
replication_index and decimal-text replication_seed columns preserve its
identity. The Frame allows at most 116 requested workbook outputs, keeping the
existing 128-column bound. Missing or invalid selection refuses; summary-only
ensembles can still materialize numeric band Frames. Materialization does not
mutate the resident workbook or write agent state back to a source table.
Saved Time experiments retain all original replicas and their numeric series
through source replacement and restart. Retained ensemble playback now owns one
immutable Arrow Frame containing original paths, with replica index and exact
seed columns. Its manifest lists each replica’s populations and recorded clock;
the top-level population list is empty because no aggregate geometry exists.
A page request must name replication together with population and recorded
step. Each response echoes the index and decimal-text replicationSeed.
Missing or invalid selection refuses without rerunning the model.
The combined recording admits at most 100,000 rows and 64 MiB of prepared IPC payloads before publication, then checks the final combined IPC against 64 MiB. All replicas must share one CRS and observation schema. Manifests remain within 2 MiB, pages within 2 MiB and 5,000 displayed positions. A page reads and scans the bounded combined Frame; this is not an indexed large-population claim. Portable import verifies the metadata, schema and exact IPC against the original retained result, then rebinds owned storage under the imported model name.
The recorded Map and saved Scene let you select an original replica and show its exact seed. Open in Scene carries that selection. Replica identity accompanies population, step, agent selection and camera in saved Scene view data. Map R1 and Scene R3 were approved on 2026-10-07. The complete physical-cost proof, optimized scale envelope and wider authoring contract remain outstanding.