Leviathan Engine
Leviathan is the physics substrate underneath every Arboria experiment. It is a C++ core with Python bindings that advances agent state forward in time under a chosen integrator, subjects the agents to an environment field, resolves collisions between them, meters what their communication actually costs, drains their batteries, and occasionally kills one.
It does not decide what agents do. That is Gossamer’s job. Leviathan receives an acceleration per agent per step and is otherwise indifferent to where it came from — which is precisely what makes it usable as a controlled substrate for comparing coordination primitives against each other.
Availability. Leviathan is proprietary and is not distributed. This page documents its behaviour, configuration surface, and the models it implements, in enough detail to interpret our published results and to reimplement the experiments elsewhere. See Reproducibility and Data Availability.
Why it exists
Off-the-shelf multi-agent simulators are built around the assumption that communication is free and instantaneous. They give you neighbour queries, not a link budget. When the research question is “what does delay cost coordination,” a simulator that hands each agent a fresh view of its neighbours every step has silently answered the question before you asked it.
Leviathan exists because we needed communication to be a first-class physical process — with range, bandwidth, propagation delay, loss, and a per-bit energy cost that grows with distance — rather than an abstraction layered on top of a physics loop. Everything else in the engine is in service of keeping that measurement clean.
Architecture
The engine ships in three forms. The base image carries the C++ core plus
the pybind11 extension module, and is what
downstream consumers link against. The runtime service wraps the binding in
an HTTP API exposing create, step, checkpoint, restore, metrics, and destroy.
The PettingZoo wrapper presents the in-process binding as a
ParallelEnv, so any multi-agent
reinforcement-learning trainer that speaks PettingZoo — RLlib, MARLlib, CleanRL,
TorchRL — can drive it directly.
The Python-facing surface is a single Simulation class. It exposes step,
run, reset, metrics, set_state, positional and velocity accessors,
per-agent energy and fault flags, the step index, and RNG state capture and
restore.
Integration
Two paths, chosen by the caller. Over HTTP, the runtime service is a
conventional stateless-ish simulation server; this is what a multi-tenant
deployment uses. In-process, the caller imports the pybind extension
directly and skips a full HTTP round-trip per step. Long rollouts use the
in-process path, and it is what produced every published result — a detail that
matters for provenance, because the in-process worker runs inside a prebuilt
container with no source checkout, so its git provenance fields are null by
construction.
Integrators
Three, selected by configuration.
Semi-implicit Euler is the default and the cheapest. It is also, over long horizons, wrong in a specific way: it does not conserve energy, so orbital and other long-lived periodic trajectories drift outward. On our orbital-debris example, Euler accumulates over 20% drift across 10⁵ steps.
Velocity-Verlet is symplectic : it conserves a nearby Hamiltonian rather than the true one, which bounds energy error instead of letting it grow. Drift on the same example falls below 1%. This is the correct default for anything long-horizon, and it is what both delay-coordination papers ran on.
Runge–Kutta 4 is available for cases where local truncation error matters more than long-term energy behaviour. It is not symplectic, and it costs four force evaluations per step.
The communication model
This is the part of Leviathan that does not exist in other simulators, so it is worth describing precisely.
Each step, every agent attempts to transmit one status bundle to every neighbour
inside comm_range. Each attempted transmission is then subject to, in order: a
range test (out-of-range attempts are counted as dropped_range); a
per-link bandwidth cap, so a link that cannot carry comm_bundle_kb within
one step of comm_bw_kbps does not; Bernoulli packet loss at
comm_loss_prob, counted as dropped_loss; and finally a fixed propagation
delay, expressed either directly in simulation steps or in milliseconds and
converted using dt. Delayed bundles sit in a ring buffer and arrive in the
step their delay dictates.
Transmission costs energy, and the cost depends on how far you shout. Per-bit transmit energy follows a free-space path-loss law:
with defaulting to 0.2 nJ/bit at a reference range of 1 km. The consequence is that energy per bit is measured from the geometry of who talked to whom, rather than asserted as a constant — which is what makes an energy-versus-freshness tradeoff meaningful rather than tautological.
Neighbour-finding for the channel uses a uniform spatial grid whose cell edge
equals comm_range, so any in-range pair lands in the same or an adjacent cell
and a 3×3×3 cell sweep is exhaustive. This makes the channel linear in agent
count rather than quadratic. Collision resolution uses the same primitive at its
own interaction distance. Both of the engine’s former hotspots are gone.
The whole model is a no-op when none of its keys are set: an experiment that does not care about communication pays nothing for it.
A caution on interpretation. In the current harness the channel is a cost meter, not a gate. Coordination primitives read the delayed peer view directly; the channel independently accounts what that view would have cost in bits and joules. Bandwidth therefore moves the cost columns without affecting coordination quality. This is why our cost-frontier study is deferred rather than published — measuring a frontier on a decoupled harness would be measuring an artifact. Gating the peer view on actual bundle delivery is a change to the engine and the runner, not a configuration setting.
Environment, collisions, energy, and faults
The environment field is none, a uniform vector field, or a central
inverse-square attractor toward the origin.
Collisions are sphere–sphere, resolved when collision_radius is nonzero,
with a restitution coefficient from 0 (perfectly inelastic — agents stick and
share momentum) to 1 (perfectly elastic). The count of collisions resolved in
the last step is surfaced in metrics.
The energy module drains each agent’s battery in proportion to
, scaled by energy_rate. An agent whose
energy reaches zero is marked faulty.
The fault module independently marks each agent faulty with probability
fault_prob per step — a fail-stop model.
Two footguns worth stating loudly.
fault_probdefaults to 0.01 per agent per step. Over a 1,500-step run that is a near-certain death sentence for the entire swarm, and it will look like your coordination algorithm degraded. Set it explicitly. Likewiseenergy_rate: because drain scales with speed squared, any experiment whose independent variable makes agents move faster will silently convert that into agent attrition. Both delay-coordination papers setenergy_rate=0andfault_prob=0for exactly this reason — so that agent loss could not confound the delay axis.
Determinism and checkpointing
Every stochastic subsystem draws from an explicitly seeded generator, and the
Mersenne Twister state is serializable: get_rng_state and set_rng_state
round-trip the engine’s std::mt19937, so a run resumed from a checkpoint is
byte-identical to the run that was never interrupted. The communication channel
carries its own independent seed, so changing the physics seed does not perturb
the loss pattern. This is exposed through both the binding and the HTTP
checkpoint and restore endpoints.
Configuration reference
Leviathan reads a flat, string-valued configuration map — as an HTTP body, a
keyword dictionary through the binding, or a key: value file. Every key is
optional.
Core physics. dt (seconds, default 1.0); num_agents (default 10);
bound (periodic-box half-extent, default 100.0); integrator (euler |
rk4 | velocity_verlet, default euler); init_spread (half-width of the
initial uniform position fill); seed (deterministic initialization).
acc_min and acc_max bound random actions on the legacy demo path and are
ignored whenever a caller supplies actions, which the Maneuver.Map runner always
does.
Noise. velocity_noise and actuator_noise inject Gaussian perturbation
into agent velocity and into the applied action respectively. velocity_noise
doubles as the angular-noise knob when running a Vicsek-faithful update.
Environment field. field_type (none | uniform | central);
field_vx, field_vy, field_vz for uniform fields; field_strength for
either.
Communication — inert when all keys are default. comm_range (metres);
comm_bw_kbps (per-link cap); comm_bundle_kb (payload, default 0.1);
comm_loss_prob (0–1); comm_latency_steps or comm_latency_ms (the
latter converted using dt); comm_eb0_j_per_bit and comm_d0_m (the two
constants of the path-loss energy law); comm_seed (independent channel RNG).
Collisions — inert when collision_radius is 0. collision_radius
(metres); collision_restitution (0–1).
Modules. energy_rate (drain coefficient; 0 disables); fault_prob
(per-agent per-step fail-stop probability; default 0.01).
Output, on the legacy command-line runner only — the Maneuver.Map runner
has its own writer. output_path, output_frequency (write every N steps),
output_format (csv or parquet).
Metrics
metrics() returns a flat name-to-number map, sampled at the current step:
num_agents and num_faulty; avg_energy; collisions_last_step; and the
communication counters comm_attempted, comm_delivered, comm_dropped_loss,
comm_dropped_range, comm_bytes_in_flight, comm_energy_j, and
comm_eb_mean_j_per_bit.
Output schema
The engine’s own CSV schema is timestamp, agent_id, position_x, position_y, position_z. When Parquet is enabled, the same columns are written as part files
under <output_path>.parts/, one per logged step.
Maneuver.Map extends this when it orchestrates, adding
per-algorithm columns such as role, soc, aoi, density, and pheromone
where the running primitive produces them. Its frame API streams the Parquet
parts with predicate pushdown on timestamp and column pruning, so a viewer
scrubbing a million-row run fetches the slice it asked for rather than the file.
Performance notes
Per-step cost is linear in agent count. The practical ceiling is memory:
positions, velocities, and the delay ring dominate, and the ring grows with
comm_latency_steps. Raise output_frequency to cut I/O on long runs; prefer
Parquet when the output is destined for analysis, since the columnar layout is
what makes server-side slicing cheap.
Related
The coordination primitives that drive Leviathan are documented in Gossamer; the orchestration, provenance, and visualization layer is Maneuver.Map. For the physics of what the engine simulates, see agent-based modeling.