Quickstart#

This guide gets you from zero to a running simulation in under 5 minutes.

Install#

pip install maddening[viz]    # simulation engine + matplotlib plots

For GPU acceleration, use maddening[cuda12,viz] instead.

Your First Simulation#

MADDENING simulations are graphs. Nodes simulate physics. Edges couple them.

import jax.numpy as jnp
from maddening import GraphManager, SimulationNode


# 1. Define a node
class BounceNode(SimulationNode):
    """Ball bouncing under gravity."""

    def __init__(self, **kwargs):
        super().__init__(**kwargs)

    @property
    def requires_halo(self) -> bool:
        return False  # pointwise (no spatial neighbors)

    def initial_state(self):
        return {
            "position": jnp.array(5.0),
            "velocity": jnp.array(0.0),
        }

    def update(self, state, boundary_inputs, dt):
        gravity = -9.81
        new_vel = state["velocity"] + gravity * dt
        new_pos = state["position"] + new_vel * dt
        # Bounce off the floor
        new_vel = jnp.where(new_pos < 0, jnp.abs(new_vel) * 0.8, new_vel)
        new_pos = jnp.maximum(new_pos, 0.0)
        return {"position": new_pos, "velocity": new_vel}


# 2. Build the graph
gm = GraphManager()
gm.add_node(BounceNode(name="ball", timestep=0.01))
gm.compile()

# 3. Run
final_state, history = gm.run_scan_with_history(n_steps=500)

# 4. Plot
import matplotlib.pyplot as plt
positions = history["ball"]["position"]
plt.plot(positions)
plt.xlabel("Step")
plt.ylabel("Height")
plt.title("Bouncing Ball")
plt.savefig("bounce.png")
print(f"Final height: {float(final_state['ball']['position']):.3f}")

Coupled Simulation#

Connect nodes with edges to exchange data each timestep:

from maddening import GraphManager, EdgeSpec

gm = GraphManager()
gm.add_node(ball_node)       # produces "position"
gm.add_node(spring_node)     # consumes "anchor_position", produces "force"

# Ball position feeds into spring, spring force feeds back
gm.add_edge("ball", "spring", source_field="position", target_field="anchor_position")
gm.add_edge("spring", "ball", source_field="force", target_field="external_force")

gm.compile()
gm.run(n_steps=1000)

Differentiable Everything#

The entire graph step is JIT-compiled and differentiable:

import jax

# Gradient of final position w.r.t. initial velocity
def loss(initial_velocity):
    gm.set_node_state("ball", {"position": jnp.array(5.0),
                                "velocity": initial_velocity})
    state = gm.run_scan(n_steps=100)
    return state["ball"]["position"]

grad_fn = jax.grad(loss)
print(f"d(final_pos)/d(init_vel) = {grad_fn(jnp.array(0.0))}")

Deploy to Cloud#

Run your simulation on a cloud GPU:

pip install maddening[runpod]

# Set up credentials (one-time)
mkdir -p ~/.maddening
cp src/maddening/examples/cloud/cloud_credentials.example.yaml ~/.maddening/cloud_credentials.yaml
# Edit ~/.maddening/cloud_credentials.yaml with your RunPod API key
# You can also choose a different path, in which case use the `creds` argument in the CloudLauncher constructor.
from maddening.cloud.launcher import CloudLauncher

launcher = CloudLauncher() 
# launcher = CloudLauncher(credentials_path='path/to/cloud_credentials.yaml')
info = launcher.validate("job_config.yaml")
print(f"Instance: {info['instance_type']}, ${info['hourly_cost']:.2f}/hr")

job = launcher.launch("job_config.yaml")
job.stream_logs()
print(f"VM IP: {job.vm_ip}")
job.teardown()

See src/maddening/examples/cloud/ for complete examples and config templates.

Next Steps#

  • Installation Guide — all extras, GPU setup, cloud providers

  • DESIGN.md — architecture decisions, node authoring contract

  • examples/ — coupling, adaptive timestepping, surrogates, servers