v0.1 · Apache 2.0

A differentiable physics kernel for verifiable domain operations.

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.

starting engine…
↗ open
avbd_stack_50000_ball.lagra
starting engine…
↗ open
lagrangian_dam_break.lagra

Same engine binary, different active LagrangianNodes. Drag any block on the left; click play on the right to release the dam.

view source · avbd_stack_1000 (block)
# 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()
view source · lagrangian_dam_break (water)
# 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()

Write once in Python, ship performant everywhere

The entire source for the two-bead oscillator on the right — authored in Python, lowered to .lagra, evaluated by the same Rust kernel that runs in your browser right now. Nothing in between, no second model to keep in sync.

Python is the front door. Arbitrary control flow, loops, generators, NumPy, file I/O — everything you need to build a 50,000-block stack with a wrecking ball, a 21×21 fluid lattice, or a 60-particle DNA helix without writing each entity by hand. You stay in Python.

.lagra is the strict IR underneath. Every Python add_entity / add_bond / set_solver call emits typed .lagra nodes — the same nodes the parser produces from a hand-written file. The DSL is Rhai (a Rust scripting language) constrained to a Lagra-specific schema: deliberately narrow, so an LLM agent can't take shortcuts that bypass the verifier, and so the kernel can always trace every force back to a named node.

One binary, every target. Once the graph is built, the same Rust runtime — kernel, integrator, contact solver, and CPU rasterizer — compiles to native (Linux/Mac/Windows) and wasm32 with identical bit-equivalent stepping. The renderer ships with the engine, so a Python-authored scene runs and paints itself in the browser at native-grade performance. No glue, no client-server split, no second model.

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
starting engine…
↗ open
solver_walkthrough_oscillator.lagra

Every token on the left maps to a typed object the engine evolves on the right.

Eight harmonic bonds, one Lagrangian

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

starting engine…
↗ open
pendulum_chain.lagra
view source · pendulum_chain
# 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()

One kernel walks every .lagra node

SCENE · .lagra SOURCE LagrangianGraph · TYPED NODES scene oscillator { uses newtonian.classical_mechanics entity ball { pos:[-1,0,0], m:1.0 } entity ball { pos:[ 1,0,0], m:1.0 } bond harmonic [0,1] { k:2.0, r0:2.0 } environment classical_gravity @expect energy_conservation solver { dt:0.002 } } + runtime intervention (user / agent drag · not in .lagra) PRESET · activates KineticEnergy + Gravity + … vertex q₁ ∈ ℝ³ · m=1.0 · pos=(-1,0,0) vertex q₂ ∈ ℝ³ · m=1.0 · pos=( 1,0,0) HarmonicBond(q₁,q₂) · k=2.0 · r₀=2.0 · ∂V/∂q FieldGravity · acceleration_field=[0,-9.81,0] EnergyGate · |H(t) − H(0)|/|H(0)| · 1 of N Noether gates stepper · velocity_verlet · dt=0.002 (config) DragForce(q) · runtime · pointer / agent event ↓ action kernel walks every active node, sums ∂V/∂q ↓ F = − Σ ∂Vn / ∂q
ACTION KERNEL · ONE LOOP, EVERY STEP entity bond field intervention optional · user / agent action action kernel for node in active · sum ∂V/∂q F = − Σ ∂V/∂q integrate → q′, q̇′ new state loop · next step render → pixels optional VerifierGate → @expect optional · conservation check
preset entity bond field (in environment block) intervention (runtime · not in .lagra) verifier · @expect config / render

A master Lagrangian, verifiably consistent with physics at every step

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.

MASTER EQUATION · .lagra TERMS PLUGGED IN ENTITY .lagra entity ball { mass:1.0, q₀=(-1,0,0) } entity ball { mass:1.0, q₁=( 1,0,0) } T = ½ Σᵢ mᵢ q̇ᵢ² kinetic · one term per entity PLUGGED IN ↓ T = ½(1.0)q̇₀² + ½(1.0)q̇₁² BOND · ENVIRONMENT .lagra bond harmonic [0,1] { k:2.0, r0:2.0 } environment classical_gravity ↪ environments.lagra: acceleration_field: [0, -9.81, 0] + runtime intervention (drag) — injected at sim time, not in .lagra V = Σₙ Vₙ(q) potential · one Vₙ per bond / environment-field PLUGGED IN ↓ V = ½(2.0)(|q₁−q₀|−2.0)² + 9.81(m₀y₀ + m₁y₁) L = T − V the Lagrangian · S = ∫ L dt kernel · F = − ∂V/∂q one loop over active nodes per step + state (q, q̇) solver velocity_verlet { // configs/solvers/velocity_verlet.lagra — a .lagra program program { // per particle, per step let ax = fx / mass; // fx from the kernel output above let vx' = vx + ax * dt; let px' = px + vx * dt + 0.5 * ax * dt * dt; } contract: separable_hamiltonian requires: [state, force, mass] } ↻ swap per scene · symplectic_euler · avbd · discrete_action · boris · mujoco_action_descent · … → q′, q̇′

Advancing Science & ML

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.

starting engine…
↗ open
dna_double_helix.lagra
view source · dna_double_helix
# 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()

Games & performance

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.

Jenga — pull a block, drop the tower

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.

starting engine…
↗ open
avbd_jenga_tower.lagra
view source · avbd_jenga_tower
# 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()

One open kernel and architecture under Apache 2.0

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 scene · bundle compile wasm + native one binary desktop native mobile browser HPC py env RL / py
consumed by
Developerswrite .lagra scenes, pull lagra-core as a crate, ship native + wasm from one source tree.
End usersresearchers + students open the desktop IDE, share mobile demo links, run scenes from the live page.
AgentsClaude Code, Codex, Albert — all of them speak the MCP server, hold typed handles to a live World.
Training pipelinesPython bridge → PyTorch / JAX. Trajectories are differentiable tensors of (particles × dims × steps); gradients flow back through the engine.

Drive it from anywhere

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.

starting engine…
↗ open
avbd_bye_dominos.lagra