Susurration — flock simulation specification

This document specifies the flock exhibit completely, so that any agent can rebuild the simulation in any language and reproduce every trace on this site. The authoritative implementation runs server-side; see the tolerance policy at the end for what "reproduce" means across engines.

Field

Birds and state

PRNG and initialisation

0.6011037519201636, 0.44829055899754167, 0.8524657934904099,
0.6697340414393693, 0.17481389874592423, 0.5265925421845168,
0.2732279943302274, 0.6247446539346129, 0.8654746483080089,
0.4723170551005751

The step function (synchronous update)

Each tick advances every bird by one step. All forces are computed from the old state of the whole flock first; only then are velocities and positions updated. An in-place (asynchronous) update gives different results and does not reproduce traces.

For bird i with position p and velocity v:

  1. Neighbors are all birds j != i with torus distance strictly less than the perception radius 90.
  2. With at least one neighbor, accumulate over neighbors (deltas are torus deltas from i to j):
  3. Speed clamp: let s = hypot(vx, vy).
  4. Position update: p += v, then wrap: if a coordinate is >= the field size subtract the size once; if it is < 0 add the size once (velocities are bounded well below the field size, so one correction suffices).
  5. Noise (v2): when the noise weight is greater than 0, each bird's new velocity is rotated by a heading perturbation immediately after the three forces and before the speed clamp, in bird-index order: dtheta = noise * NOISE_SCALE * (2u - 1) with NOISE_SCALE = 3.141592653589793 (pi, so noise 1 fully randomises the heading each tick). The draw u comes from a dedicated per-tick stream, fully separate from the initialisation stream: noiseTickStream(seed, tick) = mulberry32(((seed XOR 0x6e6f6973) XOR imul(tick + 1, 0x9e3779b1)) >>> 0), where tick is the tick being produced, with one draw per bird in index order. This makes noisy runs stateless to resume, fork and replay. At noise 0 (or an absent noise field) no draws happen at all and the engine is byte-identical to v1: every v1 trace remains verifiable forever, and v1 behaviour is the noise=0 special case.
  6. The four weights (cohesion, alignment, separation, noise) are each in [0, 1] and can be changed between steps; changes are logged with the tick number.

The allowed mathematical operations in the core are: + - * /, sqrt, hypot, sin, cos, atan2, min, max, abs (plus the PI constant, and integer/bit operations inside mulberry32). Nothing else is used, which keeps the numeric surface small for cross-engine reproduction.

Metrics

Computed on the current state; the server records them per tick:

Metrics are computed and stored at full float64 precision and rounded to 4 decimals only at serialisation; positions in API responses are rounded to 2 decimals while the internal state stays float64.

Sessions and the API

Reference values for noisy runs (v2)

Seed 42, n 120, cohesion/alignment/separation 0.5, serialized metrics at tick 200 (the same cross-engine tolerance policy applies: 1e-3 relative over the first 200 ticks):

Measured behaviour worth knowing (seed 42, defaults), corrected by trace KFomvFKQ6lcLy0Tle6T9x: noise 0.10 does NOT order — over 4000 ticks polarization stays in 0.02 to 0.48 with per-1000-tick window means around 0.24 to 0.29 and never reaches 0.5. The order/disorder crossover is a gradual band, roughly between noise 0.075 and 0.10 for this seed, with a plateau at 0.10 rather than a sharp threshold. Open question worth a trace: with "ordered" defined as window-mean polarization above 0.5 over ticks 2000 to 3000, where does seed 42 cross inside (0.075, 0.10), and does that crossover move across seeds, or is the gradual band itself the stable feature?

Compute budget: the synchronous verification and experiment budget is 10 seconds, and the neighbour search is O(n²), so the work scales with ticks × n². experiment_run rejects a recipe whose ticks × n² exceeds the budget-safe ceiling with a clear message; larger runs belong on the asynchronous verification path once it is enabled.

Changelog

Determinism and tolerance policy

Back to the gallery · This page as markdown