Part IV · The whole engine · Chapter 12
available

Simulation architecture

A simulator is not one equation. It is an ordered data pipeline whose choices decide what the controller can influence, what collisions see, what sensors report, and whether a saved state can be replayed.

Level
Advanced
Hands-on
180–240 minutes
Before you start
Chapters 1, 7–11; Python processes and browser WebAssembly

By the end, you can…

  • Trace control through actuation, contact, integration, sensing, and telemetry.
  • Identify which state each stage reads and writes.
  • Separate timing measurements from deterministic work counters.
  • Compare the same diagnostic contract across native and WebAssembly.
  • Use snapshots for exact replay and counterfactual branching.
  • Connect one environment, 32 workers, and an RL-ready quadruped loop.

One public step contains several physical substeps

An RL policy commonly acts at 60 Hz, but LavenderSim limits the native physics timestep to 1/240 s. One control step therefore performs four physical substeps. The control is held while forces, collision detection, constraint solving, integration, correction, and sensing repeat. At the end, telemetry is copied into flat buffers for Python or JavaScript.

Δtcontrol=NΔtphysics,xk+1=FN(xk,uk)\Delta t_{control}=N\Delta t_{physics},\qquad x_{k+1}=F^{N}(x_k,u_k)

Here xkx_k is complete simulator state, uku_k is the command held for the control interval, FF is one substep, and N=4N=4 at 60 Hz with 240 Hz physics. Changing the ordering changes FF even when every individual formula stays the same.

Nine-stage LavenderSim dataflow from controller input through telemetry output
The arrows are data dependencies, not decoration: contacts must exist before they can be solved, integration consumes solved velocity, and sensors sample the corrected state.

Experiment 1: single-step the architecture

The reference lab writes every read and write explicitly. Select a stage below and ask: what would become one-step stale if it moved earlier?

STAGE 01

Control

Reads
policy command, joint state
Writes
controller targets
Experiment 01verified

Trace a transparent toy substep

Where does time advance, and which pose reaches the sensors?

python
for stage in pipeline:
    event = stage(state, control, dt)
    trace.append({"reads": event.reads,
                  "writes": event.writes})

Expected: nine ordered events; time advances at integration; sensing follows position projection. Failure modes: sample sensors before correction, integrate before contact impulses, treat telemetry copying as physics, or assume one policy action means one native substep.

Expected result
The command exits successfully and reproduces the numerical or visual relationship described in this card.
Failure modes
Non-finite values, a reversed trend, a failed assertion, or a materially different plot means the assumptions, seed, timestep, build, or backend should be inspected before continuing.
Implemented by LavenderSim: sim_step() and substep() define this order. The public pipeline observation is a 9 × 3 matrix: stage ID, backend-comparable work units, and optional native microseconds.

Experiment 2: profile work, not your intuition

Native timing is opt-in because clocks perturb tiny workloads. Calling set_profiling(True) fills the third trace column with elapsed microseconds. The second column is a deterministic work counter—bodies, candidate pairs, solver row-visits, samples, or copied records depending on the stage. Work units compare runs; they are deliberately not interchangeable durations.

Experiment 02verified

Profile the quadruped pipeline

Which stages grow with articulated contact?

Quadruped deterministic work-unit profile for all nine pipeline stages
The committed cross-platform figure uses reproducible work counters: the iterative velocity solve dominates counted work. Run the command locally to collect hardware-specific native timing in the third column.
python
env.sim.set_profiling(True)
observation, reward, terminated, truncated, info = env.step(action)
trace = env.sim.pipeline_trace()
# columns: stage_id, work_units, native_microseconds

Failure modes: benchmark a debug build, infer time from work units, include process startup, compare different scenes, or report one sample without distribution or hardware context.

Expected result
The command exits successfully and reproduces the numerical or visual relationship described in this card.
Failure modes
Non-finite values, a reversed trend, a failed assertion, or a materially different plot means the assumptions, seed, timestep, build, or backend should be inspected before continuing.

Experiment 3: one contract, two backends

The browser build runs the same C++ source compiled to freestanding WebAssembly. It exposes stage IDs and work units, but records zero in the timing column because the module has no host clock import. Browser wall-clock profiling belongs around the JavaScript call; semantic engine parity belongs inside the trace.

Experiment 03verified

Compare a resting sphere in native and WASM

Do both builds execute the same stage contract?

Native and WebAssembly pipeline work counts match for every stage
The checked scene matches exactly in stage order, work counts, body telemetry, and solver diagnostics. That is a scoped parity test—not a claim that every possible long rollout is bit-identical.
9 / 9stage IDs equal
0maximum body error
0maximum solver error

Failure modes: compare native time with a missing WASM clock, verify only exported symbol names, use different scene defaults, or generalize one exact trace to all hardware and all trajectories.

Expected result
The command exits successfully and reproduces the numerical or visual relationship described in this card.
Failure modes
Non-finite values, a reversed trend, a failed assertion, or a materially different plot means the assumptions, seed, timestep, build, or backend should be inspected before continuing.

Experiment 4: rewind, replay, branch

A snapshot turns simulation into an experiment tree. Save at state xtx_t, apply action sequence UU, restore, and repeat UU to test determinism. Restore again and apply UU' to isolate the action's causal effect.

restore(xt);F(xt,U)=F(xt,U)whileF(xt,U)F(xt,U)\operatorname{restore}(x_t);F(x_t,U)=F(x_t,U)\quad\text{while}\quad F(x_t,U)\ne F(x_t,U')
Experiment 04verified

Replay one force and branch to its opposite

Can serialized state reproduce the same trajectory before a counterfactual diverges?

Exact overlapping snapshot replay and diverging positive and negative force branches
The repeated +8 N branch overlaps exactly; restoring the same 608-byte snapshot and applying −8 N produces a measurable separation.

The current snapshot covers bodies, joints, controls, actuator activation, Python sensor pipeline state and RNG, solver controls, task state, and time. The demonstrated exact replay intentionally avoids persistent contacts; contact-cache and native sensor-filter serialization remain a boundary to test before claiming arbitrary contact-rich bitwise replay.

Expected result
The command exits successfully and reproduces the numerical or visual relationship described in this card.
Failure modes
Non-finite values, a reversed trend, a failed assertion, or a materially different plot means the assumptions, seed, timestep, build, or backend should be inspected before continuing.

Experiment 5: make one environment become 32

PPO needs many decorrelated transitions. ProcessVectorEnv creates 32 independent registry instances, sends a batch of actions, then stacks their flat observations and rewards. It is 32 copies of one registered task—not 32 different task types.

Experiment 05verified

Roll out 32 registry-created workers

Does the environment contract remain finite and rectangular across processes?

Dataflow for 32 independent quadruped environments producing one batched observation and reward tensor
The verified rollout returns observations shaped 32 × 37, and two steps of rewards and done flags shaped 2 × 32.
python
with ProcessVectorEnv(32, task="QuadrupedCommand-v0",
                          control_hz=60, horizon_seconds=3) as env:
    obs = env.reset(seed=12)       # (32, 37)
    obs, reward, done, info = env.step(actions)

Failure modes: reuse one RNG stream in every worker, forget to close processes, silently flatten the environment axis, launch a browser per worker, or confuse simulator throughput with PPO sample quality.

Expected result
The command exits successfully and reproduces the numerical or visual relationship described in this card.
Failure modes
Non-finite values, a reversed trend, a failed assertion, or a materially different plot means the assumptions, seed, timestep, build, or backend should be inspected before continuing.

Capstone: follow one quadruped action end to end

LavenderSim quadruped command-following environment with body, legs, floor grid, and command marker
A deterministic gait drives this capstone so engine behavior can be tested without requiring a checkpoint. A learned PPO policy uses the identical action, observation, stepping, and telemetry path.
  1. The environment samples a body-relative planar direction and speed, then assembles 37 proprioceptive and command values.
  2. A controller or policy emits 12 joint actions at 60 Hz.
  3. Actuator forces, gravity, damping, and user wrenches update four 240 Hz substeps.
  4. Collision manifolds and joint rows feed the warm-started sequential-impulse solve.
  5. Velocity integration and position projection produce the physical state sampled by IMU and joint sensors.
  6. The task computes command-tracking reward; telemetry and overlays can be published to the Python-controlled browser UI.
  7. The process vectorizer repeats that contract across 32 isolated workers for PPO.
Engine physics versus task policy: contact, integration, actuator dynamics, and sensing belong to LavenderSim. Command sampling, the reward equation, episode termination, and the deterministic gait or learned network belong to the environment and agent.

Paw-check: reason across stage boundaries

  1. Move sensing before projection in the toy lab. Which quantities become inconsistent?
  2. Profile a contact-free scene and explain the changed work vector.
  3. Add host-side browser timing around sim_step without changing the backend parity contract.
  4. Snapshot a noisy delayed sensor and prove its replay behavior.
  5. Increase physics frequency while holding 60 Hz control constant; predict work and stability changes.
  6. Profile 32 workers without including process creation in per-step latency.
  7. Replace the deterministic quadruped gait with a checkpoint and keep the engine assertions unchanged.
What should I be able to say now?

A simulation step is an ordered state transformation, not a monolithic black box. Controls affect forces; collision creates constraint data; iterative impulses change velocity; integration changes pose and time; projection corrects drift; sensors and telemetry observe that resulting state. Deterministic counters test backend semantics, clocks measure platform-specific cost, snapshots enable causal branches, and a vector environment batches the same verified contract for RL.