Behaviors and phases
Behaviors
Section titled “Behaviors”Behaviors define reusable runtime rules.
behavior HeatedLiquidFlow for Liquid + LbmD3Q7 + Thermal { update when self.density > 0.02 { let evaporation = clamp( self.density * (0.0004 + self.temperature * 0.0010), 0.0, self.f0 );
set self.density = clamp(self.density - evaporation, 0.0, 1.0); set self.f0 = clamp(self.f0 - evaporation, 0.0, 1.0); }}The for clause declares the required model capabilities:
behavior Name for PropertyA + PropertyB { ...}The behavior may run on any model that contains those properties.
A scheduled run is valid only when its concrete model composes every
property in the behavior signature. Behaviors never target a model directly.
Model-specific scheduling happens in a phase with run Behavior on Model.
Behavior Context
Section titled “Behavior Context”Behaviors declare non-model runtime dependencies with needs.
behavior SunDrying for MoistureCarrierneeds inputs(sun_intensity: Float){ update when exposed[+Z] { set self.moisture = clamp(self.moisture - sun_intensity * 0.035, 0.0, 1.0); }}Input contract syntax:
needs inputs(wind_speed: Float, sun_intensity: Float)needs inputs(wind_speed: Float [0.0 1.0] ~1e-3)Input names and types are mandatory inside needs inputs(...). Bounds and
precision are optional.
The simulation provides these input dependencies through its in declarations. A required input must exist and have the exact scalar type. When
the behavior specifies bounds, the simulation input range must be contained in
that range. When it specifies precision, the simulation input must be at least
as precise.
Neighborhood dependencies are inferred from using Name in neighbor queries and
expansion operations. VonNeumann is always available; other names must be
declared in the resolved project. There is no separate neighborhood requirement
clause. Each use is checked by the compiler.
Directions are spatial operators and are validated where they are used; they
are not declared as separate names through needs.
Coordinate directions are written with axis signs:
+X-X+Y-Y+Z-ZNeighborhoods define reachable offsets. They do not define direction names:
neighborhood SmokeRise { offset [0, 0, 1];}State Access
Section titled “State Access”Inside behavior and visual bodies:
self.fieldreads the current cell.set self.field = expr;writes the current cell’s next state.set target.field = expr;overrides fields on a created or transformed cell.n.fieldreads a neighbor binding.- Bare identifiers are locals, inputs, functions, or artifact names.
Current-cell fields should not be accessed as bare identifiers.
Unqualified field access is valid only when exactly one property in the current model contains that field. If multiple properties contain the same field name, qualify the access with the property:
self.Plume.heatself.HeatSource.heattarget.Plume.heatNeighbor selectors may name either a concrete model or a property:
neighbors[Fire]neighbors[Thermal]neighbors[Fire, +X].temperature else 0.0neighbors[Thermal, +X].Thermal.temperature else 0.0neighbors[Fire] selects cells of that model. neighbors[Thermal] selects
cells of every model that composes Thermal. A selector name that matches both
a model and a property is rejected as ambiguous.
Update writes commit atomically. Every right-hand side reads the old state, not
the result of previous writes in the same update block.
let bindings inside an operation body are local rule computations:
they are evaluated once for the current rule context and can be reused by every
following set. They must appear before the first set, and local names may
not be duplicated.
Rule Operations
Section titled “Rule Operations”Update
Section titled “Update”behavior PlumeDissipate for Plume { update { let density_loss = self.dissipation - self.heat * 0.012; let cooling = self.dissipation * 0.65;
set self.density = clamp(self.density - density_loss, 0.0, 1.0); set self.heat = clamp(self.heat - cooling, 0.0, 1.0); }}Transform
Section titled “Transform”behavior FireBurnout for Combustible + HeatSource { transform into Ash when self.fuel <= 0.05 || self.HeatSource.heat <= 0.02 { set target.amount = clamp(1.0 - self.fuel, 0.0, 1.0); }}transform into Ash constructs an Ash from its model definition, then applies
the target overrides in the transform body.
If no overrides are needed, the body can be omitted:
transform into Ashwhen self.fuel <= 0.05;Expand
Section titled “Expand”behavior FireSmokeTrail for Combustible + HeatSource{ expand with Smoke using SmokeRise toward +Z when self.HeatSource.heat > 0.30 && self.fuel > 0.02 { set target.density = clamp(self.HeatSource.heat * 0.62, 0.0, 1.0); set target.Plume.heat = clamp(self.HeatSource.heat * 0.35, 0.0, 1.0); }}expand creates a target cell while preserving the source cell.
Plain expand only creates a cell when the destination is empty. If the
destination is occupied, the operation does nothing.
When expand uses a named neighborhood, the compiler resolves that name directly.
Omitting using still selects the world’s default neighborhood.
If no target overrides are needed, the body can be omitted:
expand with Smoke using SmokeRise toward +Zwhen self.HeatSource.heat > 0.30;Use expand replace when the expansion should atomically replace the cell at
the destination:
behavior FireSpread for Combustible + HeatSource { expand replace with Fire toward +X when self.HeatSource.heat > 0.60 { set target.HeatSource.heat = clamp(self.HeatSource.heat * 0.75, 0.0, 1.0); }}expand replace is distinct from scheduling a separate expand and delete.
It expresses one atomic operation: create the target cell by consuming the
destination cell.
Delete
Section titled “Delete”behavior ResidueCleanup for Residue { delete when self.amount <= 0.01;}Stream
Section titled “Stream”behavior LbmD3Q7Stream for Liquid + LbmD3Q7 { stream self.fE toward +X when self.fE > 0.01 on block reflect into self.fW;}stream transfers a nonnegative field quantity directionally through the lattice.
The destination accepts only what fits within its field bounds; unaccepted quantity
stays at the source. Competing transfers consume capacity in rule order, then source
coordinate order. Received quantity cannot be streamed again in the same phase.
An empty destination becomes a cell of the source model with zeroed fields. A blocked transfer can keep its quantity, explicitly delete it, or reflect it into another source field. Reflection also respects capacity. Negative quantities are rejected; floating-point conservation is subject to rounding.
Phases
Section titled “Phases”Phases define execution order.
phase exchange { run ThermalExchange on Fire; run ThermalExchange on Grass; run ThermalExchange on Water;}run Behavior on Model schedules a behavior for a concrete model. Importing a
behavior does not run it.
Phase order is semantic order. Runs execute in assembled source order, and
later writes win when two scheduled rules write the same destination. Rules in one phase read the same pre-phase state. Inside a
single update block, writes still commit atomically; a later phase sees the
previous phase’s committed state.
For competing ordinary expansions, the first eligible operation wins because later operations see an occupied destination. For replacing expansions, the last eligible operation wins. Source cells are visited in lexicographic x/y/z order and expansion offsets retain declaration order. Deleted or transformed sources cannot perform later effects as their old model; newly created cells begin acting next phase.