895 lines
28 KiB
Markdown
895 lines
28 KiB
Markdown
|
|
# 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 `dt` barely 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:
|
|
|
|
```python
|
|
# 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`
|
|
|
|
```python
|
|
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)
|
|
|
|
```python
|
|
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.
|
|
|
|
```python
|
|
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
|
|
|
|
```python
|
|
# 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.
|
|
|
|
```python
|
|
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.
|
|
|
|
```python
|
|
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.
|
|
|
|
```python
|
|
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:
|
|
|
|
```python
|
|
# 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
|
|
|
|
```python
|
|
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**:
|
|
1. Try step with current `dt`
|
|
2. Estimate error (typically by comparing two different methods or resolutions)
|
|
3. If error > tolerance: reject step, reduce `dt`, retry
|
|
4. If error < tolerance: accept step, possibly increase `dt` for 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:
|
|
|
|
```python
|
|
# 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 `dt` barely improves stability
|
|
- "Unconditionally stable" methods suddenly become conditionally stable
|
|
- Tiny timesteps needed despite smooth solution
|
|
|
|
**Solutions**:
|
|
|
|
1. **Use semi-implicit or symplectic** (best for constrained systems like cloth)
|
|
2. **Use implicit Euler** (solves with Newton iterations)
|
|
3. **Use specialized stiff solver** (LSODA, Radau, etc.)
|
|
4. **Reduce stiffness** if possible (lower spring constants, increase damping)
|
|
|
|
|
|
## Implementation Patterns
|
|
|
|
### Pattern 1: Generic Integrator Interface
|
|
|
|
```python
|
|
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
|
|
|
|
```python
|
|
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:
|
|
|
|
```python
|
|
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
|
|
|
|
```python
|
|
# 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
|
|
|
|
```python
|
|
# 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
|
|
|
|
```python
|
|
# 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
|
|
|
|
```python
|
|
# 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
|
|
|
|
```python
|
|
# 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)
|
|
|
|
```python
|
|
# 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)
|
|
|
|
```python
|
|
# 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)
|
|
|
|
```python
|
|
# 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)
|
|
|
|
```python
|
|
# 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)
|
|
|
|
```python
|
|
# 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)
|
|
|
|
```python
|
|
# 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
|
|
|
|
```python
|
|
# 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
|
|
|
|
```python
|
|
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
|
|
|
|
```python
|
|
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
|
|
|
|
```python
|
|
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:**
|
|
|
|
1. **Semi-implicit Euler** (default): Fast, energy-conserving, simple
|
|
2. **Symplectic Verlet** (orbital/cloth): Explicit energy preservation
|
|
3. **RK4** (research): High accuracy, not symplectic
|
|
4. **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.
|