Research-Stack/5-Applications/tools-scripts/demo/gwl_oscillator_step4_stochastic.py

501 lines
16 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/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)