Skip to Content
ToolchainLeviathan Engine

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:

Eb(d)=Eb,0(dd0)2E_b(d) = E_{b,0} \left( \frac{d}{d_0} \right)^{2}

with Eb,0E_{b,0} defaulting to 0.2 nJ/bit at a reference range d0d_0 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 O(N2)O(N^2) 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 speed2dt\text{speed}^2 \cdot \mathrm{dt}, 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_prob defaults 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. Likewise energy_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 set energy_rate=0 and fault_prob=0 for 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.

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.

Last updated on