mirror of
https://github.com/allaunthefox/Research-Stack.git
synced 2026-07-31 03:05:21 +00:00
501 lines
16 KiB
Python
501 lines
16 KiB
Python
#!/usr/bin/env python3
|
||
"""
|
||
gwl_oscillator_step4_stochastic.py
|
||
|
||
STEP 4: Stochastically Driven Harmonic Oscillator (Langevin Equation)
|
||
|
||
Base equation: d²x/dt² + 2ζω₀·dx/dt + ω₀²·x = (F₀/m)·cos(ω_d·t) + ξ(t)/m
|
||
|
||
where ξ(t) is white noise with ⟨ξ(t)ξ(t')⟩ = 2D·δ(t-t')
|
||
|
||
Key physical constraints:
|
||
- Fluctuation-dissipation: D = γk_B T (Einstein relation)
|
||
- Mean trajectory: ⟨x(t)⟩ follows deterministic solution
|
||
- Equilibrium variance: σ² = k_B T / k
|
||
- Correlation: ⟨x(t)x(0)⟩ = (k_B T/k)·e^(-γ|t|/2m)·cos(ω₁t)
|
||
|
||
This tests: noise resilience, ensemble statistics, energy equipartition
|
||
"""
|
||
|
||
import numpy as np
|
||
from dataclasses import dataclass
|
||
from typing import Tuple, List, Optional
|
||
import math
|
||
|
||
|
||
@dataclass
|
||
class StochasticOscillatorState:
|
||
"""State with noise realization."""
|
||
x: float
|
||
v: float
|
||
t: float
|
||
F_drive: float
|
||
xi: float # Noise sample
|
||
|
||
def to_vector(self) -> Tuple[float, float]:
|
||
return (self.x, self.v)
|
||
|
||
|
||
class GWL_StochasticOscillator:
|
||
"""
|
||
Langevin oscillator: deterministic backbone + stochastic perturbation.
|
||
|
||
Uses validated Step 3 as backbone, adds bounded noise.
|
||
"""
|
||
|
||
def __init__(self, omega0: float = 1.0, mass: float = 1.0,
|
||
zeta: float = 0.1, F0: float = 0.0, omega_d: float = 1.0,
|
||
temperature: float = 1.0, dt: float = 0.01,
|
||
seed: Optional[int] = None):
|
||
"""
|
||
Args:
|
||
omega0: Natural frequency
|
||
mass: Mass
|
||
zeta: Damping ratio
|
||
F0: Driving amplitude (0 for pure thermal)
|
||
omega_d: Driving frequency
|
||
temperature: k_B T (thermal energy scale)
|
||
dt: Time step
|
||
seed: RNG seed for reproducibility
|
||
"""
|
||
self.omega0 = omega0
|
||
self.mass = mass
|
||
self.zeta = zeta
|
||
self.F0 = F0
|
||
self.omega_d = omega_d
|
||
self.temperature = temperature
|
||
self.dt = dt
|
||
|
||
# Derived
|
||
self.k = mass * omega0**2
|
||
self.gamma = 2 * zeta * mass * omega0
|
||
|
||
# Fluctuation-dissipation: D = γk_B T
|
||
self.diffusion = self.gamma * temperature
|
||
self.noise_amp = math.sqrt(2 * self.diffusion / dt) # For discrete update
|
||
|
||
# RNG
|
||
self.rng = np.random.RandomState(seed)
|
||
|
||
# State
|
||
self.state = StochasticOscillatorState(x=0.0, v=0.0, t=0.0, F_drive=0.0, xi=0.0)
|
||
self.history: List[StochasticOscillatorState] = []
|
||
|
||
def initialize(self, x0: float = 0.0, v0: float = 0.0):
|
||
"""Set initial conditions."""
|
||
self.state = StochasticOscillatorState(x=x0, v=v0, t=0.0, F_drive=self.F0, xi=0.0)
|
||
self.history = []
|
||
|
||
def driving_force(self, t: float) -> float:
|
||
"""Deterministic driving."""
|
||
return self.F0 * math.cos(self.omega_d * t)
|
||
|
||
def step(self):
|
||
"""
|
||
Stochastic update: deterministic backbone + Wiener increment.
|
||
|
||
v_new = v_det + ξ·√Δt/m
|
||
where ξ has variance 2D = 2γk_B T
|
||
"""
|
||
x_n = self.state.x
|
||
v_n = self.state.v
|
||
t_n = self.state.t
|
||
|
||
# Deterministic part (from validated Step 3)
|
||
F_det = self.driving_force(t_n)
|
||
|
||
v_temp = v_n - self.omega0**2 * x_n * self.dt
|
||
v_temp *= math.exp(-self.gamma * self.dt / self.mass)
|
||
v_det = v_temp + (F_det / self.mass) * self.dt
|
||
|
||
# Stochastic perturbation (Wiener increment)
|
||
# ⟨ξ²⟩ = 2D·Δt, so ξ = √(2D·Δt)·N(0,1)
|
||
xi = math.sqrt(2 * self.diffusion * self.dt) * self.rng.randn()
|
||
|
||
v_new = v_det + xi / self.mass
|
||
x_new = x_n + v_new * self.dt
|
||
t_new = t_n + self.dt
|
||
|
||
self.state = StochasticOscillatorState(
|
||
x=x_new, v=v_new, t=t_new, F_drive=F_det, xi=xi
|
||
)
|
||
self.history.append(self.state)
|
||
|
||
def run(self, steps: int):
|
||
"""Run simulation."""
|
||
for _ in range(steps):
|
||
self.step()
|
||
|
||
def energy(self) -> float:
|
||
"""Total mechanical energy."""
|
||
return 0.5 * self.mass * self.state.v**2 + 0.5 * self.k * self.state.x**2
|
||
|
||
def equilibrium_variance(self) -> float:
|
||
"""Theoretical equilibrium variance: σ² = k_B T / k"""
|
||
return self.temperature / self.k
|
||
|
||
|
||
class EnsembleSimulator:
|
||
"""Run multiple realizations for ensemble statistics."""
|
||
|
||
def __init__(self, num_realizations: int = 1000, **oscillator_kwargs):
|
||
self.num_realizations = num_realizations
|
||
self.oscillator_kwargs = oscillator_kwargs
|
||
self.ensembles: List[GWL_StochasticOscillator] = []
|
||
|
||
def run_ensemble(self, steps: int, x0: float = 0.0, v0: float = 0.0):
|
||
"""Run N realizations, collect statistics."""
|
||
self.ensembles = []
|
||
|
||
for i in range(self.num_realizations):
|
||
osc = GWL_StochasticOscillator(**self.oscillator_kwargs, seed=i)
|
||
osc.initialize(x0, v0)
|
||
osc.run(steps)
|
||
self.ensembles.append(osc)
|
||
|
||
return self.compute_statistics()
|
||
|
||
def compute_statistics(self) -> dict:
|
||
"""Compute ensemble statistics at each time step."""
|
||
if not self.ensembles:
|
||
return {}
|
||
|
||
num_steps = len(self.ensembles[0].history)
|
||
|
||
# Extract trajectories
|
||
x_trajs = np.array([[h.x for h in osc.history] for osc in self.ensembles])
|
||
v_trajs = np.array([[h.v for h in osc.history] for osc in self.ensembles])
|
||
t_vals = [h.t for h in self.ensembles[0].history]
|
||
|
||
# Statistics
|
||
mean_x = np.mean(x_trajs, axis=0)
|
||
mean_v = np.mean(v_trajs, axis=0)
|
||
var_x = np.var(x_trajs, axis=0)
|
||
var_v = np.var(v_trajs, axis=0)
|
||
|
||
return {
|
||
't': t_vals,
|
||
'mean_x': mean_x,
|
||
'mean_v': mean_v,
|
||
'var_x': var_x,
|
||
'var_v': var_v,
|
||
'x_trajs': x_trajs,
|
||
'v_trajs': v_trajs
|
||
}
|
||
|
||
|
||
class StochasticValidationSuite:
|
||
"""Validation for Step 4: Langevin dynamics."""
|
||
|
||
def __init__(self, omega0: float = 1.0, mass: float = 1.0, zeta: float = 0.2):
|
||
self.omega0 = omega0
|
||
self.mass = mass
|
||
self.zeta = zeta
|
||
self.results = {}
|
||
|
||
def test_fluctuation_dissipation(self) -> Tuple[bool, dict]:
|
||
"""
|
||
Test 1: Fluctuation-dissipation theorem.
|
||
|
||
D = γk_B T should give correct equilibrium variance.
|
||
"""
|
||
temperature = 1.0
|
||
k = self.mass * self.omega0**2
|
||
|
||
# Equilibrium variance: σ² = k_B T / k
|
||
expected_var = temperature / k
|
||
|
||
# Run ensemble to equilibrium
|
||
ensemble = EnsembleSimulator(
|
||
num_realizations=500,
|
||
omega0=self.omega0,
|
||
mass=self.mass,
|
||
zeta=self.zeta,
|
||
F0=0.0, # No driving, pure thermal
|
||
omega_d=self.omega0,
|
||
temperature=temperature,
|
||
dt=0.01
|
||
)
|
||
|
||
# Run for several decay times
|
||
tau = 1.0 / (self.zeta * self.omega0)
|
||
steps = int(10 * tau / 0.01)
|
||
stats = ensemble.run_ensemble(steps, x0=1.0, v0=0.0)
|
||
|
||
# Measure late-time variance
|
||
late_var = np.mean(stats['var_x'][-100:])
|
||
|
||
error = abs(late_var - expected_var) / expected_var
|
||
passed = error < 0.15
|
||
|
||
return passed, {
|
||
'expected_var': expected_var,
|
||
'measured_var': late_var,
|
||
'error': error,
|
||
'temperature': temperature,
|
||
'k': k
|
||
}
|
||
|
||
def test_mean_trajectory(self) -> Tuple[bool, dict]:
|
||
"""
|
||
Test 2: Mean trajectory follows deterministic solution.
|
||
|
||
⟨x(t)⟩ should match Step 3 deterministic oscillator.
|
||
"""
|
||
F0 = 1.0
|
||
omega_d = 0.8 * self.omega0
|
||
|
||
# Run ensemble with driving
|
||
ensemble = EnsembleSimulator(
|
||
num_realizations=300,
|
||
omega0=self.omega0,
|
||
mass=self.mass,
|
||
zeta=self.zeta,
|
||
F0=F0,
|
||
omega_d=omega_d,
|
||
temperature=0.5, # Small noise
|
||
dt=0.01
|
||
)
|
||
|
||
tau = 1.0 / (self.zeta * self.omega0)
|
||
steps = int(8 * tau / 0.01)
|
||
stats = ensemble.run_ensemble(steps, x0=0.0, v0=0.0)
|
||
|
||
# Compare late-time mean to deterministic steady-state
|
||
late_mean = np.mean(stats['mean_x'][-100:])
|
||
|
||
# Expected steady-state amplitude
|
||
# A = (F₀/m) / √((ω₀²-ω_d²)² + (2ζω₀ω_d)²)
|
||
numerator = F0 / self.mass
|
||
denominator = math.sqrt(
|
||
(self.omega0**2 - omega_d**2)**2 +
|
||
(2 * self.zeta * self.omega0 * omega_d)**2
|
||
)
|
||
expected_amp = numerator / denominator
|
||
|
||
error = abs(late_mean - expected_amp) / expected_amp if expected_amp > 0 else abs(late_mean)
|
||
passed = error < 0.2 # Ensemble mean has variance
|
||
|
||
return passed, {
|
||
'mean_trajectory': late_mean,
|
||
'expected_amplitude': expected_amp,
|
||
'error': error,
|
||
'temperature': 0.5
|
||
}
|
||
|
||
def test_variance_evolution(self) -> Tuple[bool, dict]:
|
||
"""
|
||
Test 3: Variance grows and saturates to equilibrium value.
|
||
|
||
σ²(t) → k_B T / k as t → ∞
|
||
"""
|
||
temperature = 1.0
|
||
k = self.mass * self.omega0**2
|
||
expected_var = temperature / k
|
||
|
||
ensemble = EnsembleSimulator(
|
||
num_realizations=400,
|
||
omega0=self.omega0,
|
||
mass=self.mass,
|
||
zeta=self.zeta,
|
||
F0=0.0,
|
||
omega_d=self.omega0,
|
||
temperature=temperature,
|
||
dt=0.01
|
||
)
|
||
|
||
tau = 1.0 / (self.zeta * self.omega0)
|
||
steps = int(8 * tau / 0.01)
|
||
stats = ensemble.run_ensemble(steps, x0=0.0, v0=0.0)
|
||
|
||
var_x = stats['var_x']
|
||
|
||
# Check growth from near-zero to equilibrium
|
||
early_var = np.mean(var_x[:50])
|
||
late_var = np.mean(var_x[-100:])
|
||
|
||
growth = late_var / (early_var + 1e-10)
|
||
saturation = abs(late_var - expected_var) / expected_var
|
||
|
||
# Variance should grow and saturate
|
||
passed = growth > 5 and saturation < 0.2
|
||
|
||
return passed, {
|
||
'early_var': early_var,
|
||
'late_var': late_var,
|
||
'growth_factor': growth,
|
||
'saturation_error': saturation,
|
||
'expected_var': expected_var
|
||
}
|
||
|
||
def test_energy_equipartition(self) -> Tuple[bool, dict]:
|
||
"""
|
||
Test 4: Energy equipartition at equilibrium.
|
||
|
||
⟨E_kinetic⟩ = ⟨E_potential⟩ = ½k_B T
|
||
"""
|
||
temperature = 1.0
|
||
|
||
ensemble = EnsembleSimulator(
|
||
num_realizations=400,
|
||
omega0=self.omega0,
|
||
mass=self.mass,
|
||
zeta=self.zeta,
|
||
F0=0.0,
|
||
omega_d=self.omega0,
|
||
temperature=temperature,
|
||
dt=0.01
|
||
)
|
||
|
||
tau = 1.0 / (self.zeta * self.omega0)
|
||
steps = int(10 * tau / 0.01)
|
||
stats = ensemble.run_ensemble(steps, x0=1.0, v0=1.0)
|
||
|
||
# Extract late-time energies
|
||
k = self.mass * self.omega0**2
|
||
late_x = stats['x_trajs'][:, -100:]
|
||
late_v = stats['v_trajs'][:, -100:]
|
||
|
||
E_pot = 0.5 * k * late_x**2
|
||
E_kin = 0.5 * self.mass * late_v**2
|
||
|
||
mean_E_pot = np.mean(E_pot)
|
||
mean_E_kin = np.mean(E_kin)
|
||
|
||
expected = 0.5 * temperature
|
||
error_pot = abs(mean_E_pot - expected) / expected
|
||
error_kin = abs(mean_E_kin - expected) / expected
|
||
|
||
# Both should be ≈ ½k_B T
|
||
passed = error_pot < 0.2 and error_kin < 0.2
|
||
|
||
return passed, {
|
||
'mean_E_potential': mean_E_pot,
|
||
'mean_E_kinetic': mean_E_kin,
|
||
'expected': expected,
|
||
'error_pot': error_pot,
|
||
'error_kin': error_kin
|
||
}
|
||
|
||
def test_deterministic_backbone_preserved(self) -> Tuple[bool, dict]:
|
||
"""
|
||
Test 5: As T → 0, recover deterministic solution exactly.
|
||
"""
|
||
from gwl_oscillator_step3_driven import GWL_DrivenOscillator
|
||
|
||
F0 = 1.0
|
||
omega_d = self.omega0
|
||
x0, v0 = 0.0, 0.0
|
||
steps = 500
|
||
|
||
# Deterministic (Step 3)
|
||
det_osc = GWL_DrivenOscillator(
|
||
omega0=self.omega0, mass=self.mass, zeta=self.zeta,
|
||
F0=F0, omega_d=omega_d, dt=0.01
|
||
)
|
||
det_osc.initialize(x0, v0)
|
||
det_osc.run(steps)
|
||
x_det = [h.x for h in det_osc.history]
|
||
|
||
# Stochastic with T ≈ 0
|
||
stoch_osc = GWL_StochasticOscillator(
|
||
omega0=self.omega0, mass=self.mass, zeta=self.zeta,
|
||
F0=F0, omega_d=omega_d, temperature=1e-6, dt=0.01, seed=42
|
||
)
|
||
stoch_osc.initialize(x0, v0)
|
||
stoch_osc.run(steps)
|
||
x_stoch = [h.x for h in stoch_osc.history]
|
||
|
||
# Should match closely
|
||
max_diff = max(abs(a - b) for a, b in zip(x_det, x_stoch))
|
||
|
||
passed = max_diff < 0.01
|
||
|
||
return passed, {
|
||
'max_diff': max_diff,
|
||
'deterministic_final': x_det[-1],
|
||
'stochastic_final': x_stoch[-1]
|
||
}
|
||
|
||
def run_all(self):
|
||
"""Run complete validation suite."""
|
||
print("=" * 80)
|
||
print("STEP 4 VALIDATION: STOCHASTIC LANGEVIN DYNAMICS")
|
||
print("=" * 80)
|
||
print(f"Base equation: m·d²x/dt² + γ·dx/dt + k·x = F(t) + ξ(t)")
|
||
print(f"Noise: ⟨ξ(t)ξ(t')⟩ = 2D·δ(t-t'), D = γk_B T")
|
||
print(f"Validation: Einstein fluctuation-dissipation, equipartition")
|
||
print(f"Parameters: ω₀={self.omega0}, ζ={self.zeta}")
|
||
print()
|
||
|
||
tests = [
|
||
('Fluctuation-Dissipation', self.test_fluctuation_dissipation),
|
||
('Mean Trajectory', self.test_mean_trajectory),
|
||
('Variance Evolution', self.test_variance_evolution),
|
||
('Energy Equipartition', self.test_energy_equipartition),
|
||
('Deterministic Backbone', self.test_deterministic_backbone_preserved),
|
||
]
|
||
|
||
all_passed = True
|
||
for name, test_fn in tests:
|
||
print(f"\n[Test] {name}")
|
||
print("-" * 60)
|
||
try:
|
||
passed, details = test_fn()
|
||
status = "✓ PASS" if passed else "✗ FAIL"
|
||
print(f"Status: {status}")
|
||
for key, val in details.items():
|
||
if isinstance(val, float):
|
||
print(f" {key}: {val:.6f}")
|
||
elif isinstance(val, np.ndarray):
|
||
print(f" {key}: array[{len(val)}]")
|
||
else:
|
||
print(f" {key}: {val}")
|
||
self.results[name] = {'passed': passed, 'details': details}
|
||
all_passed = all_passed and passed
|
||
except Exception as e:
|
||
print(f"Status: ✗ ERROR - {e}")
|
||
import traceback
|
||
traceback.print_exc()
|
||
self.results[name] = {'passed': False, 'error': str(e)}
|
||
all_passed = False
|
||
|
||
# Summary
|
||
print("\n" + "=" * 80)
|
||
print("SUMMARY")
|
||
print("=" * 80)
|
||
for name, result in self.results.items():
|
||
status = "✓ PASS" if result.get('passed') else "✗ FAIL"
|
||
print(f"{name:35s}: {status}")
|
||
|
||
print("\n" + "=" * 80)
|
||
if all_passed:
|
||
print("ALL TESTS PASSED - STEP 4 VALIDATED")
|
||
print("=" * 80)
|
||
print("""
|
||
The Langevin oscillator is now validated.
|
||
Physical constraints verified:
|
||
✓ Fluctuation-dissipation: D = γk_B T → correct σ²
|
||
✓ Mean trajectory: follows deterministic backbone
|
||
✓ Variance evolution: grows and saturates
|
||
✓ Energy equipartition: ⟨E_kin⟩ = ⟨E_pot⟩ = ½k_B T
|
||
✓ T → 0 limit: recovers deterministic exactly
|
||
|
||
STOCHASTIC EXTENSION VALIDATED
|
||
Structure: Deterministic backbone + bounded perturbation
|
||
Safety: Cannot destabilize proven backbone
|
||
Physics: Satisfies Einstein relation, equipartition
|
||
|
||
READY FOR STEP 5: Multi-projection consensus
|
||
""")
|
||
else:
|
||
print("SOME TESTS FAILED - DO NOT PROCEED")
|
||
print("=" * 80)
|
||
|
||
return all_passed
|
||
|
||
|
||
if __name__ == "__main__":
|
||
validator = StochasticValidationSuite(omega0=1.0, mass=1.0, zeta=0.2)
|
||
success = validator.run_all()
|
||
exit(0 if success else 1)
|