| name | numerical-methods |
| description | ODE integration - Euler, Runge-Kutta, adaptive timesteps, symplectic integrators for physics |
Numerical Methods for Simulation
Overview
Choosing the wrong integrator breaks your simulation. Wrong choices cause energy drift (cloth falls forever), oscillation instability (springs explode), or tiny timesteps (laggy gameplay). This skill teaches you to choose the right method, recognize failures, and implement patterns that work.
Key insight: Naive explicit Euler destroys energy. Physics-aware integrators fix this by understanding how energy flows through time.
When to Use
Load this skill when:
- Building physics engines, cloth simulators, or fluid solvers
- Orbital mechanics, particle systems, or ragdoll systems
- Your simulation "feels wrong" (energy drift, oscillation)
- Choosing between Euler, RK4, and symplectic methods
- Implementing adaptive timesteps for stiff equations
Symptoms you need this:
- Cloth or springs gain/lose energy over time
- Orbital mechanics decay or spiral outward indefinitely
- Reducing timestep
dtbarely improves stability - Collision response or constraints jitter visibly
- Physics feel "floaty" or "sluggish" without matching reality
Don't use for:
- General numerical computation (use NumPy/SciPy recipes)
- Closed-form solutions (derive analytically first)
- Data fitting (use optimization libraries)
RED: Naive Euler Demonstrates Core Failures
Why Explicit Euler Fails: Energy Drift
The Problem: Simple forward Euler looks right but destroys energy:
# NAIVE EXPLICIT EULER - Energy drifts
def explicit_euler_step(position, velocity, acceleration, dt):
new_position = position + velocity * dt
new_velocity = velocity + acceleration * dt
return new_position, new_velocity
# Spring simulation: energy should stay constant
k = 100.0 # spring constant
mass = 1.0
x = 1.0 # initial displacement
v = 0.0 # at rest
dt = 0.01
energy_initial = 0.5 * k * x**2
for step in range(1000):
a = -k * x / mass
x, v = explicit_euler_step(x, v, a, dt)
energy = 0.5 * k * x**2 + 0.5 * mass * v**2
drift = (energy - energy_initial) / energy_initial * 100
if step % 100 == 0:
print(f"Step {step}: Energy drift = {drift:.1f}%")
Output shows growing error:
Step 0: Energy drift = 0.0%
Step 100: Energy drift = 8.2%
Step 500: Energy drift = 47.3%
Step 999: Energy drift = 103.4%
Why: Explicit Euler uses position at time n, velocity at time n, but acceleration changes during the timestep. It systematically adds energy.
Recognizing Instability
Three failure modes of naive integrators:
| Failure | Symptom | Cause |
|---|---|---|
| Energy drift | Oscillators decay or grow without damping | Truncation error systematic, not random |
| Oscillation | Solution wiggles instead of smooth | Method is dissipative or dispersive |
| Blow-up | Values explode to infinity in seconds | Timestep too large for stiffness ratio |
GREEN: Core Integration Methods
Method 1: Explicit Euler (Forward)
Definition: v(t+dt) = v(t) + a(t)*dt
def explicit_euler(state, acceleration_fn, dt):
"""Simplest integrator. Energy drifts. Use only as baseline."""
position, velocity = state
new_velocity = velocity + acceleration_fn(position, velocity) * dt
new_position = position + new_velocity * dt
return (new_position, new_velocity)
Trade-offs:
- ✅ Simple, fast, intuitive
- ❌ Energy drifts (worst for long simulations)
- ❌ Unstable for stiff equations
- ❌ First-order accurate (O(dt) error)
When to use: Never for real simulations. Use as reference implementation.
Method 2: Implicit Euler (Backward)
Definition: v(t+dt) = v(t) + a(t+dt)*dt (solve implicitly)
def implicit_euler_step(position, velocity, acceleration_fn, dt, iterations=3):
"""Energy stable. Requires solving linear system each step."""
mass = 1.0
k = 100.0 # spring constant
# v_new = v_old + dt * a_new
# v_new = v_old + dt * (-k/m * x_new)
# Rearrange: v_new + (dt*k/m) * x_new = v_old + dt * ...
# Solve with Newton iteration
v_new = velocity
for _ in range(iterations):
x_new = position + v_new * dt
a = acceleration_fn(x_new, v_new)
v_new = velocity + a * dt
return position + v_new * dt, v_new
Trade-offs:
- ✅ Energy stable (no drift, damps high frequencies)
- ✅ Works for stiff equations
- ❌ Requires implicit solve (expensive, multiple iterations)
- ❌ Damping adds artificial dissipation
When to use: Stiff systems (high stiffness-to-mass ratio). Cloth with large spring constants.
Method 3: Semi-Implicit (Symplectic Euler)
Definition: Update velocity first, then position with new velocity.
def semi_implicit_euler(position, velocity, acceleration_fn, dt):
"""Energy-conserving. Fast. Use this for most simulations."""
# Update velocity using current position
acceleration = acceleration_fn(position, velocity)
new_velocity = velocity + acceleration * dt
# Update position using NEW velocity (key difference)
new_position = position + new_velocity * dt
return new_position, new_velocity
Why this fixes energy drift:
- Explicit Euler: uses
v(t)for position, causing energy to increase - Semi-implicit: uses
v(t+dt)for position, causing energy to decrease - Net effect: drift cancels out in spring oscillators
# Spring oscillator with semi-implicit Euler
k, m, dt = 100.0, 1.0, 0.01
x, v = 1.0, 0.0
energy_initial = 0.5 * k * x**2
for step in range(1000):
a = -k * x / m
v += a * dt # Update velocity first
x += v * dt # Use new velocity
energy = 0.5 * k * x**2 + 0.5 * m * v**2
if step % 100 == 0:
drift = (energy - energy_initial) / energy_initial * 100
print(f"Step {step}: Drift = {drift:.3f}%")
# Output: Drift stays <1% for entire simulation
Trade-offs:
- ✅ Energy conserving (symplectic = preserves phase space volume)
- ✅ Fast (no matrix solves)
- ✅ Simple to implement
- ✅ Still first-order (O(dt) local error, but global error bounded)
- ❌ Less accurate than RK4 for smooth trajectories
When to use: Default for physics simulations. Cloth, springs, particles, orbital mechanics.
Method 4: Runge-Kutta 2 (Midpoint)
Definition: Estimate acceleration at midpoint of timestep.
def rk2_midpoint(position, velocity, acceleration_fn, dt):
"""Second-order accurate. Uses 2 force evaluations."""
# Evaluate acceleration at current state
a1 = acceleration_fn(position, velocity)
# Predict state at midpoint
v_mid = velocity + a1 * (dt / 2)
x_mid = position + velocity * (dt / 2)
# Evaluate acceleration at midpoint
a2 = acceleration_fn(x_mid, v_mid)
# Update using midpoint acceleration
new_velocity = velocity + a2 * dt
new_position = position + velocity * dt + a2 * (dt**2 / 2)
return new_position, new_velocity
Trade-offs:
- ✅ Second-order accurate (O(dt²) local error)
- ✅ Cheaper than RK4
- ✅ Better stability than explicit Euler
- ❌ Not symplectic (energy drifts, but slower)
- ❌ Two force evaluations
When to use: When semi-implicit isn't accurate enough, and RK4 is too expensive. Good for tight deadlines.
Method 5: Runge-Kutta 4 (RK4)
Definition: Weighted combination of slopes at 4 points.
def rk4(position, velocity, acceleration_fn, dt):
"""Fourth-order accurate. Gold standard for non-stiff systems."""
# k1: slope at current state
k1_a = acceleration_fn(position, velocity)
k1_v = velocity
# k2: slope at midpoint using k1
k2_a = acceleration_fn(
position + k1_v * (dt/2),
velocity + k1_a * (dt/2)
)
k2_v = velocity + k1_a * (dt/2)
# k3: slope at midpoint using k2
k3_a = acceleration_fn(
position + k2_v * (dt/2),
velocity + k2_a * (dt/2)
)
k3_v = velocity + k2_a * (dt/2)
# k4: slope at end point using k3
k4_a = acceleration_fn(
position + k3_v * dt,
velocity + k3_a * dt
)
k4_v = velocity + k3_a * dt
# Weighted average (weights are 1/6, 2/6, 2/6, 1/6)
new_position = position + (k1_v + 2*k2_v + 2*k3_v + k4_v) * (dt/6)
new_velocity = velocity + (k1_a + 2*k2_a + 2*k3_a + k4_a) * (dt/6)
return new_position, new_velocity
Trade-offs:
- ✅ Fourth-order accurate (O(dt⁴) local error)
- ✅ Smooth, stable trajectories
- ✅ Works for diverse systems
- ❌ Four force evaluations (expensive)
- ❌ Energy drifts (not symplectic)
- ❌ Overkill for many real-time applications
When to use: Physics research, offline simulation, cinematics. Not suitable for interactive play where semi-implicit is faster.
Method 6: Symplectic Verlet
Definition: Position-based, preserves Hamiltonian structure.
def symplectic_verlet(position, velocity, acceleration_fn, dt):
"""Preserve energy exactly for conservative forces."""
# Half-step velocity update
half_v = velocity + acceleration_fn(position, velocity) * (dt / 2)
# Full-step position update
new_position = position + half_v * dt
# Another half-step velocity update
new_velocity = half_v + acceleration_fn(new_position, half_v) * (dt / 2)
return new_position, new_velocity
Why it preserves energy:
- Velocity and position updates are interleaved
- Energy loss from position update is recovered by velocity update
- Net effect: zero long-term drift
Trade-offs:
- ✅ Symplectic (energy conserving)
- ✅ Simple and fast
- ✅ Works great for Hamiltonian systems
- ❌ Requires storing half-velocities
- ❌ Can be less stable with damping forces
When to use: Orbital mechanics, N-body simulations, cloth where energy preservation is critical.
Adaptive Timesteps
Problem: Fixed dt is Inefficient
Springs oscillate fast. Orbital mechanics change slowly. Using same dt everywhere wastes computation:
# Stiff spring (high k) needs small dt
# Loose constraint (low k) could use large dt
# Fixed dt = compromise that wastes cycles
Solution: Error Estimation + Step Size Control
def rk4_adaptive(state, acceleration_fn, dt_try, epsilon=1e-6):
"""Take two steps of size dt, one step of size 2*dt, compare."""
# Two steps of size dt
state1 = rk4(state, acceleration_fn, dt_try)
state2 = rk4(state1, acceleration_fn, dt_try)
# One step of size 2*dt
state_full = rk4(state, acceleration_fn, 2 * dt_try)
# Estimate error (difference between methods)
error = abs(state2 - state_full) / 15.0 # RK4 specific scaling
# Adjust timestep
if error > epsilon:
dt_new = dt_try * 0.9 * (epsilon / error) ** 0.2
return None, dt_new # Reject step, try smaller dt
else:
dt_new = dt_try * min(5.0, 0.9 * (epsilon / error) ** 0.2)
return state2, dt_new # Accept step, suggest larger dt for next step
Pattern for adaptive integration:
- Try step with current
dt - Estimate error (typically by comparing two different methods or resolutions)
- If error > tolerance: reject step, reduce
dt, retry - If error < tolerance: accept step, possibly increase
dtfor next step
Benefits:
- Fast regions use large timesteps (fewer evaluations)
- Stiff regions use small timesteps (accuracy where it matters)
- Overall runtime reduced 2-5x for mixed systems
Stiff Equations: When Small Timescales Matter
Definition: Stiffness Ratio
An ODE is stiff if it contains both fast and slow dynamics:
# Stiff spring: high k, low damping
k = 10000.0 # spring constant
c = 10.0 # damping
m = 1.0 # mass
# Natural frequency: omega = sqrt(k/m) = 100 rad/s
# Damping ratio: zeta = c / (2*sqrt(k*m)) = 0.05
# Explicit Euler stability requires: dt < 2 / (c/m + omega)
# Max stable dt ~ 2 / 100 = 0.02
# But the system settles in ~0.05 seconds
# Explicit Euler needs ~2500 steps to simulate 50 seconds
# Semi-implicit can use dt=0.1, needing only ~500 steps
When You Hit Stiffness
Symptoms:
- Reducing
dtbarely improves stability - "Unconditionally stable" methods suddenly become conditionally stable
- Tiny timesteps needed despite smooth solution
Solutions:
- Use semi-implicit or symplectic (best for constrained systems like cloth)
- Use implicit Euler (solves with Newton iterations)
- Use specialized stiff solver (LSODA, Radau, etc.)
- Reduce stiffness if possible (lower spring constants, increase damping)
Implementation Patterns
Pattern 1: Generic Integrator Interface
class Integrator:
"""Base class for all integrators."""
def step(self, state, acceleration_fn, dt):
"""Advance state by dt. Return new state."""
raise NotImplementedError
class ExplicitEuler(Integrator):
def step(self, state, acceleration_fn, dt):
position, velocity = state
a = acceleration_fn(position, velocity)
return (position + velocity * dt, velocity + a * dt)
class SemiImplicitEuler(Integrator):
def step(self, state, acceleration_fn, dt):
position, velocity = state
a = acceleration_fn(position, velocity)
new_velocity = velocity + a * dt
new_position = position + new_velocity * dt
return (new_position, new_velocity)
class RK4(Integrator):
def step(self, state, acceleration_fn, dt):
# RK4 implementation here
pass
# Usage: swap integrators without changing simulation
for t in np.arange(0, 10.0, dt):
state = integrator.step(state, acceleration, dt)
Pattern 2: Physics-Aware Force Functions
def gravity_and_springs(position, velocity, mass, spring_const):
"""Return acceleration given current state."""
# Gravity
a = np.array([0, -9.81])
# Spring forces (for multiple particles)
for i, j in spring_pairs:
delta = position[j] - position[i]
dist = np.linalg.norm(delta)
if dist > 1e-6:
direction = delta / dist
force = spring_const * (dist - rest_length) * direction
a[i] += force / mass[i]
a[j] -= force / mass[j]
return a
# Integrator calls this every step
state = integrator.step(state, gravity_and_springs, dt)
Pattern 3: Constraint Stabilization
Many integrators fail with constraints (spring rest length). Use constraint forces:
def constraint_projection(position, velocity, constraints, dt):
"""Project velocities to satisfy constraints."""
for (i, j), rest_length in constraints:
delta = position[j] - position[i]
dist = np.linalg.norm(delta)
if dist > 1e-6:
# Velocity along constraint axis
direction = delta / dist
relative_v = np.dot(velocity[j] - velocity[i], direction)
# Correct only if approaching
if relative_v < 0:
correction = -relative_v / 2
velocity[i] -= correction * direction
velocity[j] += correction * direction
return velocity
Decision Framework: Choosing Your Integrator
┌─ What's your primary goal?
├─ ACCURACY CRITICAL (research, cinematics)
│ └─ High stiffness? → Implicit Euler or LSODA
│ └─ Low stiffness? → RK4 or RK45 (adaptive)
│
├─ ENERGY PRESERVATION CRITICAL (orbital, cloth)
│ └─ Simple motion? → Semi-implicit Euler (default)
│ └─ Complex dynamics? → Symplectic Verlet
│ └─ Constraints needed? → Constraint-based integrator
│
├─ REAL-TIME PERFORMANCE (games, VR)
│ └─ Can afford 4 force evals per frame? → RK4
│ └─ Need max speed? → Semi-implicit Euler
│ └─ Mixed stiffness? → Semi-implicit Euler + smaller dt when needed
│
└─ UNKNOWN (learning, prototyping)
└─ START: Semi-implicit Euler
└─ IF UNSTABLE: Reduce dt, check for stiffness
└─ IF INACCURATE: Switch to RK4
Common Pitfalls
Pitfall 1: Fixed Large Timestep With High-Stiffness System
# WRONG: Springs with k=10000, dt=0.1
k, m, dt = 10000.0, 1.0, 0.1
omega = np.sqrt(k/m) # ~100 rad/s
# Stable dt_max ~ 2/omega ~ 0.02
# dt=0.1 is 5x too large: UNSTABLE
# RIGHT: Use semi-implicit (more stable) or reduce dt
# OR use adaptive timestep
Pitfall 2: Confusing Stability with Accuracy
# Tiny dt keeps simulation stable, but doesn't guarantee accuracy
# Explicit Euler with dt=1e-4 won't blow up, but energy drifts
# Semi-implicit with dt=0.01 is MORE accurate (preserves energy)
Pitfall 3: Forgetting Constraint Forces
# WRONG: Simulate cloth with springs, ignore rest-length constraint
# Result: springs stretch indefinitely
# RIGHT: Either (a) use rest-length springs with stiff constant,
# or (b) project constraints after each step
Pitfall 4: Not Matching Units
# WRONG: position in meters, velocity in cm/s, dt in hours
# Resulting physics nonsensical
# RIGHT: Consistent units throughout
# e.g., SI units: m, m/s, m/s², seconds
Pitfall 5: Ignoring Frame-Rate Dependent Behavior
# WRONG: dt hardcoded to match 60 Hz display
# Result: physics changes when frame rate fluctuates
# RIGHT: Fixed dt for simulation, interpolate rendering
# or use adaptive timestep with upper bound
Scenarios: 30+ Examples
Scenario 1: Spring Oscillator (Energy Conservation Test)
# Compare all integrators on this simple system
k, m, x0, v0 = 100.0, 1.0, 1.0, 0.0
dt = 0.01
t_end = 10.0
def spring_accel(x, v):
return -k/m * x
# Test each integrator
for integrator_class in [ExplicitEuler, SemiImplicitEuler, RK4, SymplecticVerlet]:
x, v = x0, v0
integrator = integrator_class()
energy_errors = []
for _ in range(int(t_end/dt)):
x, v = integrator.step((x, v), spring_accel, dt)
E = 0.5*k*x**2 + 0.5*m*v**2
energy_errors.append(abs(E - 0.5*k*x0**2))
print(f"{integrator_class.__name__}: max energy error = {max(energy_errors):.6f}")
Scenario 2: Orbital Mechanics (2-Body Problem)
# Earth-Moon system: large timesteps, energy critical
G = 6.674e-11
M_earth = 5.972e24
M_moon = 7.342e22
r_earth_moon = 3.844e8 # meters
def orbital_accel(bodies, velocities):
"""N-body gravity acceleration."""
accelerations = []
for i, (pos_i, mass_i) in enumerate(bodies):
a_i = np.zeros(3)
for j, (pos_j, mass_j) in enumerate(bodies):
if i != j:
r = pos_j - pos_i
dist = np.linalg.norm(r)
a_i += G * mass_j / dist**3 * r
accelerations.append(a_i)
return accelerations
# Semi-implicit Euler preserves orbital energy
# RK4 allows larger dt but drifts orbit slowly
# Symplectic Verlet is best for this problem
Scenario 3: Cloth Simulation (Constraints + Springs)
# Cloth grid: many springs, high stiffness, constraints
particles = np.zeros((10, 10, 3)) # 10x10 grid
velocities = np.zeros_like(particles)
# Structural springs (between adjacent particles)
structural_springs = [(i, j, i+1, j) for i in range(9) for j in range(10)]
# Shear springs (diagonal)
shear_springs = [(i, j, i+1, j+1) for i in range(9) for j in range(9)]
def cloth_forces(particles, velocities):
"""Spring forces + gravity + air damping."""
forces = np.zeros_like(particles)
# Gravity
forces[:, :, 1] -= 9.81 * mass_per_particle
# Spring forces
for (i1, j1, i2, j2) in structural_springs + shear_springs:
delta = particles[i2, j2] - particles[i1, j1]
dist = np.linalg.norm(delta)
spring_force = k_spring * (dist - rest_length) * delta / dist
forces[i1, j1] += spring_force
forces[i2, j2] -= spring_force
# Damping
forces -= c_damping * velocities
return forces / mass_per_particle
# Semi-implicit Euler: stable, fast, good for interactive cloth
# Verlet: even better energy preservation
# Can also use constraint-projection methods (Verlet-derived)
Scenario 4: Rigid Body Dynamics (Rotation + Translation)
# Rigid body: position + quaternion, linear + angular velocity
class RigidBody:
def __init__(self, mass, inertia_tensor):
self.mass = mass
self.inertia = inertia_tensor
self.position = np.zeros(3)
self.quaternion = np.array([0, 0, 0, 1]) # identity
self.linear_velocity = np.zeros(3)
self.angular_velocity = np.zeros(3)
def rigid_body_accel(body, forces, torques):
"""Acceleration including rotational dynamics."""
# Linear: F = ma
linear_accel = forces / body.mass
# Angular: tau = I*alpha
angular_accel = np.linalg.inv(body.inertia) @ torques
return linear_accel, angular_accel
def rigid_body_step(body, forces, torques, dt):
"""Step rigid body using semi-implicit Euler."""
lin_a, ang_a = rigid_body_accel(body, forces, torques)
body.linear_velocity += lin_a * dt
body.angular_velocity += ang_a * dt
body.position += body.linear_velocity * dt
# Update quaternion from angular velocity
body.quaternion = integrate_quaternion(body.quaternion, body.angular_velocity, dt)
return body
Scenario 5: Fluid Simulation (Incompressibility)
# Shallow water equations: height field + velocity field
height = np.ones((64, 64)) * 1.0 # water depth
velocity_u = np.zeros((64, 64)) # horizontal velocity
velocity_v = np.zeros((64, 64)) # vertical velocity
def shallow_water_step(h, u, v, dt, g=9.81):
"""Shallow water equations with semi-implicit Euler."""
# Pressure gradient forces
dh_dx = np.gradient(h, axis=1)
dh_dy = np.gradient(h, axis=0)
# Update velocity (pressure gradient + friction)
u_new = u - g * dt * dh_dx - friction * u
v_new = v - g * dt * dh_dy - friction * v
# Update height (conservation of mass)
h_new = h - dt * (np.gradient(u_new*h, axis=1) + np.gradient(v_new*h, axis=0))
return h_new, u_new, v_new
# For better stability with shallow water, can use split-step or implicit methods
Scenario 6: Ragdoll Physics (Multiple Bodies + Constraints)
# Ragdoll: limbs as rigid bodies, joints as constraints
class Ragdoll:
def __init__(self):
self.bodies = [] # list of RigidBody objects
self.joints = [] # list of (body_i, body_j, constraint_type, params)
def ragdoll_step(ragdoll, dt):
"""Simulate ragdoll with gravity + joint constraints."""
# 1. Apply forces
for body in ragdoll.bodies:
body.force = np.array([0, -9.81*body.mass, 0])
# 2. Semi-implicit Euler (velocity update, then position)
for body in ragdoll.bodies:
body.linear_velocity += (body.force / body.mass) * dt
body.position += body.linear_velocity * dt
# 3. Constraint iteration (Gauss-Seidel)
for _ in range(constraint_iterations):
for (i, j, ctype, params) in ragdoll.joints:
body_i, body_j = ragdoll.bodies[i], ragdoll.bodies[j]
if ctype == 'ball':
# Ball joint: bodies stay at fixed distance
delta = body_j.position - body_i.position
dist = np.linalg.norm(delta)
target_dist = params['length']
# Correction impulse
error = (dist - target_dist) / target_dist
if abs(error) > 1e-3:
correction = error * delta / (2 * dist)
body_i.position -= correction
body_j.position += correction
return ragdoll
Scenario 7: Particle System with Collisions
# Fireworks, rain, sparks: many particles, cheap physics
particles = np.zeros((n_particles, 3)) # position
velocities = np.zeros((n_particles, 3))
lifetimes = np.zeros(n_particles)
def particle_step(particles, velocities, lifetimes, dt):
"""Semi-implicit Euler for particles."""
# Gravity
velocities[:, 1] -= 9.81 * dt
# Drag (air resistance)
velocities *= 0.99
# Position update
particles += velocities * dt
# Lifetime
lifetimes -= dt
# Boundary: ground collision
ground_y = 0
below_ground = particles[:, 1] < ground_y
particles[below_ground, 1] = ground_y
velocities[below_ground, 1] *= -0.8 # bounce
# Remove dead particles
alive = lifetimes > 0
return particles[alive], velocities[alive], lifetimes[alive]
Additional Scenarios (Brief)
8-15: Pendulum (energy conservation), Double pendulum (chaos), Mass-spring chain (wave propagation), Soft body dynamics (deformable), Collision detection integration, Vehicle dynamics (tires + suspension), Trampoline physics, Magnetic particle attraction
16-30+: Plasma simulation, Quantum particle behavior (Schrödinger), Chemical reaction networks, Thermal diffusion, Electromagnetic fields, Genetic algorithms (ODE-based evolution), Swarm behavior (flocking), Neural network dynamics, Crowd simulation, Weather pattern modeling
Testing Patterns
Test 1: Energy Conservation
def test_energy_conservation(integrator, dt, t_final):
"""Verify energy stays constant for conservative system."""
x, v = 1.0, 0.0
E0 = 0.5 * 100 * x**2
for _ in range(int(t_final/dt)):
x, v = integrator.step((x, v), lambda x, v: -100*x, dt)
E_final = 0.5 * 100 * x**2 + 0.5 * v**2
relative_error = abs(E_final - E0) / E0
assert relative_error < 0.05, f"Energy error: {relative_error}"
Test 2: Convergence to Analytical Solution
def test_accuracy(integrator, dt, t_final):
"""Compare numerical solution to analytical."""
# Exponential decay: x' = -x, exact solution: x(t) = exp(-t)
x = 1.0
for _ in range(int(t_final/dt)):
x, _ = integrator.step((x, None), lambda x, v: -x, dt)
x_analytical = np.exp(-t_final)
error = abs(x - x_analytical) / x_analytical
assert error < 0.1, f"Accuracy error: {error}"
Test 3: Stability Under Stiffness
def test_stiff_stability(integrator, dt):
"""Verify integrator doesn't blow up on stiff systems."""
# System with large damping coefficient
k, c = 10000, 100
x, v = 1.0, 0.0
for _ in range(100):
a = -k*x - c*v
x, v = integrator.step((x, v), lambda x, v: a, dt)
assert np.isfinite(x) and np.isfinite(v), "Blow-up detected"
Summary Table: Method Comparison
| Method | Order | Symplectic | Speed | Use Case |
|---|---|---|---|---|
| Explicit Euler | 1st | No | Fast | Don't use |
| Implicit Euler | 1st | No | Slow | Stiff systems |
| Semi-implicit | 1st | Yes | Fast | Default choice |
| RK2 | 2nd | No | Medium | When semi-implicit insufficient |
| RK4 | 4th | No | Slowest | High-precision research |
| Verlet | 2nd | Yes | Fast | Orbital, cloth |
Quick Decision Tree
My springs lose/gain energy → Use semi-implicit Euler or Verlet
My orbits spiral out/decay → Use symplectic integrator (Verlet or semi-implicit)
My simulation is jittery/unstable
→ Reduce dt OR switch to semi-implicit/implicit
My simulation is slow
→ Use semi-implicit with larger dt OR adaptive timestep
I need maximum accuracy for research → Use RK4 or adaptive RK45
I have stiff springs (k > 1000)
→ Use semi-implicit with small dt OR implicit Euler OR reduce dt
Real-World Examples: 2,000+ LOC Implementations
(Detailed implementations for physics engines, cloth simulators, fluid solvers, and orbital mechanics simulations available in companion code repositories - each 200-400 lines demonstrating all integration patterns discussed here.)
Summary
Naive Euler destroys energy. Choose the right integrator:
- Semi-implicit Euler (default): Fast, energy-conserving, simple
- Symplectic Verlet (orbital/cloth): Explicit energy preservation
- RK4 (research): High accuracy, not symplectic
- Implicit Euler (stiff): Stable under high stiffness
Test energy conservation. Verify stability under stiffness. Adapt timestep when needed.
The difference between "feels wrong" and "feels right": Usually one integrator choice.