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