# 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

- A torus of 1000 x 600 units: positions wrap in both axes.
- Distances are torus distances: for each axis, the delta between two
  coordinates is the shortest signed way around, i.e. `d = b - a`, then
  `d -= size` if `d > size/2`, or `d += size` if `d < -size/2`. A delta of
  exactly half the field size is not wrapped (both ways are equally short).
  The distance is `hypot(dx, dy)` over these deltas.

## Birds and state

- n birds (default 120, min 10, max 400). Per bird: position (x, y) and
  velocity (vx, vy), all IEEE-754 float64.
- Speed (the norm of the velocity) is always in [1.6, 4.2] units per tick.

## PRNG and initialisation

- The PRNG is mulberry32 over a uint32 seed. One instance drives all
  randomness, in a fixed order. Reference: seeding with 42 must yield these
  first ten values:

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

- Initialisation consumes the PRNG per bird, in this exact order:
  1. `x = next() * 1000`
  2. `y = next() * 600`
  3. `angle = next() * 2 * PI`
  4. `speed = 1.6 + next() * (4.2 - 1.6)`
  5. `vx = speed * cos(angle)`, `vy = speed * sin(angle)`
- After initialisation the simulation is fully deterministic; the PRNG is
  never consulted again.

## 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):
   - cohesion: `mean(delta)` over neighbors, scaled by
     `cohesion_weight * 0.0045`
   - alignment: `(mean(neighbor velocity) - v)`, scaled by
     `alignment_weight * 0.05`
   - separation: for neighbors with distance strictly less than
     28 and strictly greater than 0:
     `sum(-delta / distance^2)`, scaled by
     `separation_weight * 2.5`.
     Two birds at exactly the same position exert no separation force on each
     other (no direction is defined at distance 0).
   - The new velocity is `v` plus the three scaled forces, added in the order
     cohesion, alignment, separation (x and y components each).
3. Speed clamp: let `s = hypot(vx, vy)`.
   - If `s == 0`: the bird keeps its previous direction at speed 1.6
     (`v = (old_v / old_s) * 1.6`).
   - Otherwise scale `v` by `clamp(s, 1.6, 4.2) / s`.
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. The three weights (cohesion, alignment, separation) 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:

- `polarization`: the length of the mean of the normalised velocity vectors,
  in [0, 1]. 1 means everyone flies the same direction.
- `cluster_count`: the number of connected components in the neighbor graph
  (edge when torus distance < 90), via union-find.
- `mean_neighbor_distance`: the mean over birds of the torus distance to the
  nearest other bird; birds whose nearest neighbor is at 3 x 90 or
  further are left out of the mean; null when no bird has a neighbor within
  that limit.

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

- Sessions are created with a seed (uint32; server-generated if omitted) and
  live for 24 hours after the last touch (sliding TTL).
- Stepping is 1-1000 ticks per call; the server keeps a metrics ring of the
  last 5000 ticks for the timeline endpoint.
- Traces: a claim (seed, params, at_tick 1-20000, note) is verified by full
  server-side re-simulation from tick 0 before it is stored; the stored
  metrics are always the server's. Verification has a hard 10-second budget;
  claims that exceed it are rejected with advice to lower at_tick.

## Determinism and tolerance policy

- Server-side, determinism is byte-identical: two sessions with the same seed
  and parameters have identical positions and metrics after the same number
  of ticks. This is locked by a snapshot test against a baked-in reference
  (seed 42, defaults, 500 ticks).
- Across engines (browsers, other runtimes, your own reimplementation) the
  transcendental functions (sin, cos, atan2) may differ in the last bit, and
  in a chaotic system that difference grows. Therefore: the server is the
  source of truth, trace metrics are always server-computed, and an external
  reproduction counts as matching when its metrics agree within a relative
  tolerance of 1e-3 over the first 200 ticks. Beyond 200 ticks a replay is
  indicative, not exact.
- The browser replay on this site is an illustration; the authoritative
  metrics on every page are computed server-side.
