Lean put mathematics into a verifiable domain, rapidly accelerating the field by making discovery available to machines via machine-verifiable proofs. We make progress towards the domain of physics: the entire engine is a Lagrangian-graph implementation, allowing close fits to observational data. In our search for the most elegant solution, we created Lagra, where every solver is a program in a graph.
Same engine binary, different active LagrangianNodes. Drag any block on the left; click play on the right to release the dam.
avbd_stack_1000 (block)// AVBD Stack 1000 — one hundred 10-body columns for scaling benchmark. scene avbd_stack_1000 { uses newtonian.classical_mechanics.avbd environment classical_gravity description "1000 unit rigid bodies: 100 columns of 10 stacked blocks for scaling analysis." wasm true floor { y: 0.5, k: 1400.0, damping: 2.0, friction: 0.5 } render "rigid_body" entity { type: avbd_block, position: [-13.5, 14.5, -13.5], mass: 1.0, size: 1.0 } entity { type: avbd_block, position: [-13.5, 13.0, -13.5], mass: 1.0, size: 1.0 } entity { type: avbd_block, position: [-13.5, 11.5, -13.5], mass: 1.0, size: 1.0 } // … 997 more avbd_block entities laid out in 100 columns of 10 … optimization { contact_broadphase: "sweep_prune_fast" } solver { backend: "lagra_native", integrator: "avbd", contact_model: "augmented_lagrangian", contact_solver: "variational_block_descent", iterations: 10, warmstart: true, dt: 0.001, steps_per_frame: 16 } display { unit: "m" } }
# AVBD Stack 1000 — one hundred 10-body columns for scaling benchmark. import lagra s = lagra.Scene( name="avbd_stack_1000", uses="newtonian.classical_mechanics.avbd", description="1000 unit rigid bodies: 100 columns of 10 stacked blocks.", ) s.set_environment("classical_gravity") s.set_floor(y=0.5, k=1400.0, damping=2.0, friction=0.5) s.set_render_style("rigid_body") # 100 columns × 10 stacked blocks each — written as a loop, not 1000 lines. for col in range(100): cx, cz = -13.5 + (col % 10) * 3.0, -13.5 + (col // 10) * 3.0 for layer in range(10): s.add_entity(particle="avbd_block", position=[cx, 14.5 - 1.5*layer, cz], mass=1.0, size=1.0) s.set_solver( backend="lagra_native", integrator="avbd", contact_model="augmented_lagrangian", contact_solver="variational_block_descent", iterations=10, warmstart=True, dt=0.001, steps_per_frame=16, ) sim = s.build() # → LagraSimulator (same World the .lagra path produces) for _ in range(10_000): sim.step()
lagrangian_dam_break (water)// Dam break: left half raised, no wind, pure bond-driven collapse. // Validates wavefront speed against lattice dispersion relation. scene lagrangian_dam_break { uses newtonian.classical_mechanics.fluid_dynamics environment none description "Dam break: left half of water surface raised. Released under bond tension forces. Wavefront propagates rightward at c = a·sqrt(k/m)." wasm true render "fluid_water" floor { y: -0.500, k: 900.0, damping: 1.6, friction: 0.05 } entity { name: "water_00_00", type: water_surface_sample, position: [-2.800, 0.0, -2.800], mass: 1e6, size: 0.08 } entity { name: "water_00_01", type: water_surface_sample, position: [-2.520, 0.0, -2.800], mass: 1e6, size: 0.08 } // … 439 more water_surface_sample particles on a 21×21 lattice … bond harmonic [0, 1] { k: 22.0, r0: 0.280, damping: 0.024 } bond harmonic [1, 2] { k: 22.0, r0: 0.280, damping: 0.024 } // … ~1600 more harmonic bonds wiring nearest neighbours + diagonals … @expect fluid_surface_consistency @expect subluminal_velocity solver { backend: "lagra_native", integrator: "symplectic_euler", dt: 0.005, steps_per_frame: 7 } display { unit: "m" } }
# Dam break: 21×21 water surface lattice, left half raised. Pure bond-driven collapse. import lagra s = lagra.Scene(name="lagrangian_dam_break", uses="newtonian.classical_mechanics.fluid_dynamics") s.set_render_style("fluid_water") s.set_floor(y=-0.5, k=900.0, damping=1.6, friction=0.05) # 21×21 lattice — start raised on x < 0, level on x ≥ 0. N, A = 21, 0.28 for j in range(N): for i in range(N): x, z = -2.8 + i*A, -2.8 + j*A y = 0.25 if x < 0 else 0.0 # raised dam s.add_entity(particle="water_surface_sample", name=f"water_{j:02}_{i:02}", position=[x, y, z], mass=1e6, size=0.08) # Wire nearest neighbours + diagonals across the lattice. def idx(i, j): return j*N + i for j in range(N): for i in range(N): if i+1 < N: s.add_bond(idx(i,j), idx(i+1,j), k=22.0, r0=0.28, damping=0.024) if j+1 < N: s.add_bond(idx(i,j), idx(i,j+1), k=22.0, r0=0.28, damping=0.024) s.expect(["fluid_surface_consistency", "subluminal_velocity"]) s.set_solver(backend="lagra_native", integrator="symplectic_euler", dt=0.005, steps_per_frame=7) sim = s.build()
.lagra) to live renderThe entire source for the two-bead oscillator on the right. The compiler reads it, the kernel walks the resulting graph, the solver advances state, the renderer reads pixels. Nothing else.
Two authoring surfaces, one IR. Python is the default: arbitrary control flow, loops, generators, file I/O — everything you need to build a 1000-block stack or a 21×21 lattice without writing it out by hand. .lagra is the strict, declarative form underneath — it's Rhai (a Rust scripting language) with a Lagra-specific schema, deliberately narrow so an LLM agent can't take shortcuts that bypass the verifier. Python builds emit the same .lagra nodes the parser produces; the engine sees no difference.
scene solver_walkthrough_oscillator { uses newtonian.classical_mechanics description "Two-bead harmonic oscillator." wasm true // landing-page bundle entity { type: ball_small, position: [-1.35, 0, 0], mass: 1.0, size: 0.35 } entity { type: ball_small, position: [ 1.35, 0, 0], mass: 1.0, size: 0.35 } bond harmonic [0, 1] { k: 2.0, r0: 2.0, damping: 0.04 } solver { dt: 0.002, steps_per_frame: 12 } }
import lagra s = lagra.Scene( name="solver_walkthrough_oscillator", uses="newtonian.classical_mechanics", description="Two-bead harmonic oscillator.", ) s.add_entity(particle="ball_small", position=[-1.35, 0, 0], mass=1.0, size=0.35) s.add_entity(particle="ball_small", position=[ 1.35, 0, 0], mass=1.0, size=0.35) s.add_bond(0, 1, k=2.0, r0=2.0, damping=0.04) s.set_solver(dt=0.002, steps_per_frame=12) sim = s.build() # → LagraSimulator
Every token on the left maps to a typed object the engine evolves on the right.
Nine particles, eight HarmonicBond nodes, one UniformAcceleration for gravity. The kernel doesn't know it's a "chain" — it just walks eight pairwise terms.
arrows show net force per bead · drag any bead · ▮▮ pauses · reset re-seeds
pendulum_chain// Pendulum chain — 8 beads + 1 anchor, harmonic bonds, gravity. entity bead { mass: 1.0, size: 0.4 } entity anchor { mass: 1.0e8, size: 0.15 } // huge mass → pinned scene pendulum_chain { uses newtonian.classical_mechanics environment classical_gravity description "8-bead harmonic-bond chain swinging under gravity." wasm true // index 0 = anchor; indices 1..=8 = beads, each 0.55m below the previous. entity { type: anchor, position: [0.0, 3.5, 0.0] } entity { type: bead, position: [0.0, 2.95, 0.0] } entity { type: bead, position: [0.0, 2.40, 0.0] } // … 6 more beads stepping down by 0.55m each … bond harmonic [0, 1] { k: 120.0, r0: 0.55, damping: 0.10 } bond harmonic [1, 2] { k: 120.0, r0: 0.55, damping: 0.10 } // … 6 more bonds wiring consecutive beads … @expect momentum_conservation solver { dt: 0.003, steps_per_frame: 12 } }
# Pendulum chain — 1 anchor + 8 beads, harmonic bonds, gravity. import lagra s = lagra.Scene(name="pendulum_chain", uses="newtonian.classical_mechanics", description="8-bead harmonic-bond chain swinging under gravity.") s.set_environment("classical_gravity") # Anchor (huge mass → effectively pinned). s.add_entity(particle="anchor", position=[0, 3.5, 0], mass=1e8, size=0.15) # 8 beads, each 0.55m below the previous. for i in range(8): s.add_entity(particle="bead", position=[0, 2.95 - 0.55*i, 0], mass=1.0, size=0.4) # Wire consecutive entities (0-1, 1-2, … 7-8). for i in range(8): s.add_bond(i, i+1, k=120.0, r0=0.55, damping=0.10) s.expect(["momentum_conservation"]) s.set_solver(dt=0.003, steps_per_frame=12) sim = s.build()
.lagra nodeenvironment block)
intervention (runtime · not in .lagra)
verifier · @expect
config / render
Every major theory in physics is already a Lagrangian. The Standard Model, general relativity, classical mechanics, Navier–Stokes — each was accepted only after decades of consistency checks against everything else physics knows. Lagra speaks the language physics already speaks, so any scene composed in it inherits that consistency: the terms on the right have been validated by the field; only their composition is new.
It’s also cheap to step. Minimizing the action gives a trajectory directly: per step, the kernel walks the active nodes and sums local gradients — no global solve, no implicit per-step optimization. On average, one tick of the integrator is the cost of summing ∂V/∂q over active nodes.
And the loop is verifiable in both directions. Forward: a scene’s predictions must match experiment or reference data. Backward: conservation laws — energy, momentum, charge — fall out of the trajectory and can be derived from the engine’s output, or checked statistically frame-by-frame. If the kernel ever drifts off a Noether-derived invariant, the verifier fires.
Because the entire engine is governed by a single equation, that equation itself can be changed — and the consequences measured everywhere at once. Every change becomes a falsifiable claim against existing verifiers and lagra scenes.
The DNA helix on the right folds from first principles — 60 coarse-grained particles under Coulomb + harmonic bonds + base-pair stacking, no empirical Turner parameters in the force field.
Because the Lagrangian is differentiable end-to-end, gradients flow back through the kernel — the engine can train ML models against its own outputs, and pair with agentic harnesses like Lean for verifiable scene work. Measured recovery rows and exclusions live on the benchmarks page; see the Lagra harness for the CLI, MCP, and IDE entrypoints.
dna_double_helix// CG Double Helix: 2 backbone strands (green) + 15 complementary base pairs (magenta-yellow). scene dna_double_helix { uses molecular_dynamics environment none description "Simplified DNA double helix: 2 backbone strands with 15 complementary base pairs." wasm true entity { type: particle_green, position: [8.5, 7.5, 4.5] } entity { type: particle_magenta, position: [7.8, 7.5, 4.5] } entity { type: particle_green, position: [6.5, 7.5, 4.5] } entity { type: particle_yellow, position: [7.2, 7.5, 4.5] } // … 56 more CG particles, arranged on the helical backbone … bond harmonic [0, 1] { k: 5.0, r0: 0.7 } // backbone bond harmonic [2, 3] { k: 5.0, r0: 0.7 } bond harmonic [1, 3] { k: 2.0, r0: 0.6 } // base-pair stack bond harmonic [0, 4] { k: 8.0, r0: 0.48 } // rise // … dozens more bonds wiring the helical structure … nonbonded { type: lennard_jones, epsilon: 0.0003, sigma: 0.4, cutoff: 1.5 } thermostat { kT: 0, friction: 0.5 } @expect subluminal_velocity solver { dt: 0.005, steps_per_frame: 50 } display { unit: "fm" } }
# CG double helix: 2 backbone strands + 15 base pairs. import lagra, math s = lagra.Scene(name="dna_double_helix", uses="molecular_dynamics") # 15 base pairs along the helical axis, 0.34 nm rise / 36° twist per step. RISE, TWIST, R = 0.34, math.radians(36), 0.85 for k in range(15): y = 7.5 + k*RISE a = k*TWIST # strand A: green backbone + magenta base. s.add_entity(particle="particle_green", position=[7.5 + R*math.cos(a), y, 4.5 + R*math.sin(a)]) s.add_entity(particle="particle_magenta", position=[7.5 + 0.45*math.cos(a), y, 4.5 + 0.45*math.sin(a)]) # strand B: green backbone + yellow base, π out of phase. s.add_entity(particle="particle_green", position=[7.5 - R*math.cos(a), y, 4.5 - R*math.sin(a)]) s.add_entity(particle="particle_yellow", position=[7.5 - 0.45*math.cos(a), y, 4.5 - 0.45*math.sin(a)]) # Backbone bonds (within each strand) + base-pair bonds (across strands) + rise. for k in range(15): g_a, m, g_b, y = 4*k, 4*k+1, 4*k+2, 4*k+3 s.add_bond(g_a, m, k=5.0, r0=0.7) s.add_bond(g_b, y, k=5.0, r0=0.7) s.add_bond(m, y, k=2.0, r0=0.6) # base pair if k+1 < 15: s.add_bond(g_a, g_a+4, k=8.0, r0=0.48) # rise A s.add_bond(g_b, g_b+4, k=8.0, r0=0.48) # rise B s.nonbonded("lennard_jones", epsilon=0.0003, sigma=0.4, cutoff=1.5) s.set_thermostat(k_t=0.0, friction=0.5) s.expect(["subluminal_velocity"]) s.set_solver(dt=0.005, steps_per_frame=50) sim = s.build()
Interventions are a natural fit for game-style state. Instead of coding mechanics, you insert entities and forces into the equation — gameplay logic and physics share the same scripting surface.
One codebase: native, mobile, wasm. The bundle on this page is ~800 KB gzipped, full parser + solver + CPU rasterizer included. On native, an optional gpu feature adds a wgpu compute-shader overlay without changing the pipeline.
36 rigid bodies, 12 layers of 3, settled on a friction floor. Drag any block — the red dot attaches a damped-spring intervention through the same surface a gameplay scripting hook would use. AVBD's contact solver keeps the stack stable through small perturbations and topples it gracefully when you yank too hard.
avbd_jenga_tower// AVBD Jenga Tower — 36 blocks (12 layers × 3), alternating 90° per layer. scene avbd_jenga_tower { uses newtonian.classical_mechanics.avbd environment classical_gravity description "Jenga tower: 12 layers of 3 blocks, alternating orientation." wasm true floor { y: 0.0, k: 1400.0, damping: 2.0, friction: 0.6 } render "rigid_body" entity { type: avbd_block, position: [-0.85, 0.25, 0.00], mass: 2.0, size: [0.80, 0.50, 2.40] } entity { type: avbd_block, position: [ 0.00, 0.25, 0.00], mass: 2.0, size: [0.80, 0.50, 2.40] } entity { type: avbd_block, position: [ 0.85, 0.25, 0.00], mass: 2.0, size: [0.80, 0.50, 2.40] } // … layer 2 rotates 90° via orientation quaternion [0, 0.707, 0, 0.707] … // … 33 more avbd_block entities stacked 12 layers high … @expect charge_conservation solver { backend: "lagra_native", integrator: "avbd", contact_model: "augmented_lagrangian", contact_solver: "variational_block_descent", iterations: 10, warmstart: true, dt: 0.001, steps_per_frame: 16 } display { unit: "m" } }
# AVBD Jenga tower — 36 blocks, 12 layers of 3, alternating 90° per layer. import lagra s = lagra.Scene(name="avbd_jenga_tower", uses="newtonian.classical_mechanics.avbd") s.set_environment("classical_gravity") s.set_floor(y=0.0, k=1400.0, damping=2.0, friction=0.6) s.set_render_style("rigid_body") # 12 layers; even layers run along Z, odd layers run along X (90° rotated). Q_ID = [0, 0, 0, 1] Q_90 = [0, 0.707, 0, 0.707] for layer in range(12): y = 0.25 + layer * 0.50 rot = (layer % 2 == 1) for k in range(3): off = (k - 1) * 0.85 pos = [0.0, y, off] if rot else [off, y, 0.0] s.add_entity(particle="avbd_block", position=pos, mass=2.0, size=0.5, orientation=Q_90 if rot else Q_ID) s.expect(["charge_conservation"]) s.set_solver( backend="lagra_native", integrator="avbd", contact_model="augmented_lagrangian", contact_solver="variational_block_descent", iterations=10, warmstart=True, dt=0.001, steps_per_frame=16, ) sim = s.build()
Physics engines are fragmented by license and by platform. PhysX, MuJoCo, ViennaRNA, Rosetta — each ships under a different non-commercial clause, and none of them target the same runtime as another.
Rust collapses that. One codebase compiles to native, mobile, and wasm — same kernel, same numerics, same renderer. The 6 MB bundle running in your tab right now is byte-identical to the desktop binary.
Lagra is the open substrate underneath — Apache 2.0, no per-seat clause, no commercial gate. Compose freely.
.lagra scenes, pull lagra-core as a crate, ship native + wasm from one source tree.World.(particles × dims × steps); gradients flow back through the engine.CLI harness for agent loops, a desktop IDE, and an MCP gateway so Claude Code / Codex / Albert can hold typed handles to a running World. Quick-start commands + the project catalog live in one place.
Lagra's source ships once the core hits its first public-ready cut. Until then, ping pim@medal.tv for early access — happy to share.