mirror of
https://github.com/allaunthefox/Research-Stack.git
synced 2026-07-31 03:05:21 +00:00
345 lines
12 KiB
Python
345 lines
12 KiB
Python
#!/usr/bin/env python3
|
||
"""
|
||
gwl_yee_em_fixed.py
|
||
|
||
Fixed GWL/TSM electromagnetic field propagation using Yee FDTD (1966) algorithm.
|
||
|
||
Key fix: Staggered E/B updates instead of naive explicit.
|
||
|
||
Yee Algorithm (proven stable since 1966):
|
||
B^{n+1/2} = B^{n-1/2} - (Δt/μ) ∇ × E^n
|
||
E^{n+1} = E^n + (Δt/ε) ∇ × B^{n+1/2}
|
||
"""
|
||
|
||
import numpy as np
|
||
from typing import Tuple, List
|
||
import math
|
||
|
||
|
||
class GWL_YeeEM_1D:
|
||
"""
|
||
GWL Electromagnetic field using Yee FDTD staggered update.
|
||
|
||
Maps to GWL/TSM primitives:
|
||
- E field → μ-seed E component (full time steps)
|
||
- B field → μ-seed B component (half time steps)
|
||
- ∇ × operator → π-field weighted coupling
|
||
- Temporal staggering → τ phase offset
|
||
"""
|
||
|
||
def __init__(self, nx: int = 200, dx: float = 0.01, dt: float = 0.005,
|
||
epsilon: float = 1.0, mu: float = 1.0):
|
||
self.nx = nx
|
||
self.dx = dx
|
||
self.dt = dt
|
||
self.epsilon = epsilon
|
||
self.mu = mu
|
||
|
||
# Courant number (c * dt / dx)
|
||
c = 1.0 / math.sqrt(epsilon * mu)
|
||
self.courant = c * dt / dx
|
||
|
||
# Fields
|
||
# E at integer grid points (0, 1, 2, ..., nx-1)
|
||
self.E = np.zeros(nx)
|
||
|
||
# B at half-integer points (-0.5, 0.5, 1.5, ..., nx-1.5)
|
||
# Represented as array of size nx (with boundary handling)
|
||
self.B = np.zeros(nx)
|
||
|
||
# Time step counter (for determining full/half steps)
|
||
self.step_count = 0
|
||
|
||
# History
|
||
self.energy_history = []
|
||
|
||
def initialize_gaussian_pulse(self, center: int, width: int, amplitude: float = 1.0):
|
||
"""Initialize E field with Gaussian pulse."""
|
||
for i in range(self.nx):
|
||
dist = abs(i - center)
|
||
self.E[i] = amplitude * math.exp(-dist**2 / (2 * (width/3)**2))
|
||
# B starts at zero (consistent with initial conditions)
|
||
self.B.fill(0.0)
|
||
|
||
def curl_E(self, i: int) -> float:
|
||
"""
|
||
Compute ∂E/∂x at point i (for B update).
|
||
|
||
E is at integer points, we need derivative at half-integer.
|
||
Use central difference: (E[i] - E[i-1]) / dx
|
||
"""
|
||
if i == 0:
|
||
return (self.E[i] - self.E[self.nx-1]) / self.dx # Periodic
|
||
else:
|
||
return (self.E[i] - self.E[i-1]) / self.dx
|
||
|
||
def curl_B(self, i: int) -> float:
|
||
"""
|
||
Compute ∂B/∂x at point i (for E update).
|
||
|
||
B is at half-integer points, we need derivative at integer.
|
||
Use central difference: (B[i+1] - B[i]) / dx
|
||
"""
|
||
if i == self.nx - 1:
|
||
return (self.B[0] - self.B[i]) / self.dx # Periodic
|
||
else:
|
||
return (self.B[i+1] - self.B[i]) / self.dx
|
||
|
||
def step(self):
|
||
"""
|
||
One Yee FDTD step.
|
||
|
||
In 1D, the curl reduces to a single derivative component.
|
||
For E_z and B_y (propagating in x):
|
||
∂B_y/∂t = - (1/μ) ∂E_z/∂x
|
||
∂E_z/∂t = - (1/ε) ∂B_y/∂x
|
||
"""
|
||
# Update B at half step (t + dt/2)
|
||
coeff_B = self.dt / (self.mu * self.dx)
|
||
for i in range(self.nx):
|
||
# B_y^{n+1/2} = B_y^{n-1/2} - (dt/μ) * (E_z[i] - E_z[i-1])/dx
|
||
self.B[i] -= coeff_B * (self.E[i] - self.E[(i-1) % self.nx])
|
||
|
||
# Update E at full step (t + dt)
|
||
coeff_E = self.dt / (self.epsilon * self.dx)
|
||
for i in range(self.nx):
|
||
# E_z^{n+1} = E_z^n - (dt/ε) * (B_y[i+1] - B_y[i])/dx
|
||
self.E[i] -= coeff_E * (self.B[(i+1) % self.nx] - self.B[i])
|
||
|
||
self.step_count += 1
|
||
|
||
# Record energy
|
||
energy = self.compute_energy()
|
||
self.energy_history.append(energy)
|
||
|
||
def compute_energy(self) -> float:
|
||
"""Compute total EM energy (ε E² + B²/μ)."""
|
||
electric = self.epsilon * np.sum(self.E**2)
|
||
magnetic = np.sum(self.B**2) / self.mu
|
||
return electric + magnetic
|
||
|
||
def run(self, steps: int = 500):
|
||
"""Run simulation for specified steps."""
|
||
for _ in range(steps):
|
||
self.step()
|
||
|
||
def find_pulse_center(self) -> int:
|
||
"""Find center of pulse (position of max |E|)."""
|
||
return int(np.argmax(np.abs(self.E)))
|
||
|
||
def get_energy_stats(self) -> Tuple[float, float, float]:
|
||
"""Return (initial, min, max, final) energy."""
|
||
if not self.energy_history:
|
||
return 0, 0, 0, 0
|
||
return (self.energy_history[0],
|
||
min(self.energy_history),
|
||
max(self.energy_history),
|
||
self.energy_history[-1])
|
||
|
||
|
||
class GWL_YeeEM_Tests:
|
||
"""Test suite for Yee-based GWL EM."""
|
||
|
||
def __init__(self):
|
||
self.results = {}
|
||
|
||
def test_energy_conservation(self, steps: int = 400) -> Tuple[bool, dict]:
|
||
"""
|
||
Test 1: Energy should be conserved (< 1% drift).
|
||
"""
|
||
sim = GWL_YeeEM_1D(nx=200, dx=0.01, dt=0.005)
|
||
sim.initialize_gaussian_pulse(center=100, width=20, amplitude=1.0)
|
||
|
||
initial = sim.compute_energy()
|
||
sim.run(steps=steps)
|
||
final = sim.compute_energy()
|
||
|
||
drift = abs(final - initial) / initial if initial > 0 else 0
|
||
max_ratio = max(sim.energy_history) / initial if initial > 0 else 0
|
||
|
||
passed = drift < 0.01 and max_ratio < 1.5 # < 1% drift, < 50% variation
|
||
|
||
return passed, {
|
||
'initial_energy': initial,
|
||
'final_energy': final,
|
||
'energy_drift': drift,
|
||
'max_ratio': max_ratio,
|
||
'threshold': 0.01
|
||
}
|
||
|
||
def test_stable_propagation(self, steps: int = 400) -> Tuple[bool, dict]:
|
||
"""
|
||
Test 2: Pulse should propagate without blowup.
|
||
With periodic boundaries, symmetric pulse splits and wraps.
|
||
Check: no blowup, energy bounded, field remains finite.
|
||
"""
|
||
sim = GWL_YeeEM_1D(nx=200, dx=0.01, dt=0.005)
|
||
sim.initialize_gaussian_pulse(center=100, width=20, amplitude=1.0)
|
||
|
||
initial_energy = sim.compute_energy()
|
||
max_field_initial = np.max(np.abs(sim.E))
|
||
|
||
sim.run(steps=steps)
|
||
|
||
final_energy = sim.compute_energy()
|
||
max_field_final = np.max(np.abs(sim.E))
|
||
|
||
energy_ratio = final_energy / initial_energy if initial_energy > 0 else 0
|
||
field_growth = max_field_final / max_field_initial if max_field_initial > 0 else 0
|
||
|
||
# Criteria: no energy blowup, field remains bounded
|
||
stable = energy_ratio < 2.0 and field_growth < 2.0
|
||
|
||
return stable, {
|
||
'initial_energy': initial_energy,
|
||
'final_energy': final_energy,
|
||
'energy_ratio': energy_ratio,
|
||
'field_growth': field_growth,
|
||
'max_field_final': max_field_final
|
||
}
|
||
|
||
def test_frequency_separability(self) -> Tuple[bool, dict]:
|
||
"""
|
||
Test 3: Low and high frequency modes should propagate differently.
|
||
"""
|
||
# Low frequency (broad pulse)
|
||
sim_low = GWL_YeeEM_1D(nx=200, dx=0.01, dt=0.005)
|
||
sim_low.initialize_gaussian_pulse(center=100, width=40, amplitude=1.0)
|
||
sim_low.run(steps=200)
|
||
spread_low = np.std(sim_low.E)
|
||
|
||
# High frequency (narrow pulse)
|
||
sim_high = GWL_YeeEM_1D(nx=200, dx=0.01, dt=0.005)
|
||
sim_high.initialize_gaussian_pulse(center=100, width=5, amplitude=1.0)
|
||
sim_high.run(steps=200)
|
||
spread_high = np.std(sim_high.E)
|
||
|
||
# They should behave differently
|
||
different = abs(spread_low - spread_high) > 0.01
|
||
|
||
return different, {
|
||
'low_spread': spread_low,
|
||
'high_spread': spread_high,
|
||
'difference': abs(spread_low - spread_high)
|
||
}
|
||
|
||
def test_determinism(self) -> Tuple[bool, dict]:
|
||
"""
|
||
Test 4: Same initial conditions → same results.
|
||
"""
|
||
# Run 1
|
||
sim1 = GWL_YeeEM_1D(nx=200, dx=0.01, dt=0.005)
|
||
sim1.initialize_gaussian_pulse(center=100, width=20, amplitude=1.0)
|
||
sim1.run(steps=100)
|
||
E1 = sim1.E.copy()
|
||
|
||
# Run 2
|
||
sim2 = GWL_YeeEM_1D(nx=200, dx=0.01, dt=0.005)
|
||
sim2.initialize_gaussian_pulse(center=100, width=20, amplitude=1.0)
|
||
sim2.run(steps=100)
|
||
E2 = sim2.E.copy()
|
||
|
||
# Should be identical
|
||
max_diff = np.max(np.abs(E1 - E2))
|
||
|
||
return max_diff < 1e-10, {'max_diff': max_diff}
|
||
|
||
def test_courant_stability(self) -> Tuple[bool, dict]:
|
||
"""
|
||
Test 5: Courant number <= 1 for stability.
|
||
"""
|
||
# Stable: courant = 0.5
|
||
sim_stable = GWL_YeeEM_1D(nx=200, dx=0.01, dt=0.005)
|
||
sim_stable.initialize_gaussian_pulse(center=100, width=20, amplitude=1.0)
|
||
sim_stable.run(steps=200)
|
||
stable_energy = sim_stable.compute_energy()
|
||
stable_ratio = stable_energy / sim_stable.energy_history[0]
|
||
|
||
# Unstable: courant = 1.2 (should still work with small enough dt)
|
||
# Actually Yee is stable for courant <= 1
|
||
# For courant > 1, we expect issues
|
||
|
||
results = {
|
||
'courant_stable': sim_stable.courant,
|
||
'energy_ratio': stable_ratio,
|
||
'stable': stable_ratio < 2.0
|
||
}
|
||
|
||
return results['stable'], results
|
||
|
||
def run_all(self):
|
||
"""Run complete test suite."""
|
||
print("=" * 80)
|
||
print("GWL YEE FDTD EM FIELD TEST SUITE")
|
||
print("=" * 80)
|
||
print(f"\nUsing Yee FDTD algorithm (1966, 58 years proven)")
|
||
print(f"Staggered E/B update with symplectic structure\n")
|
||
|
||
tests = [
|
||
('Energy Conservation', self.test_energy_conservation),
|
||
('Stable Propagation', self.test_stable_propagation),
|
||
('Frequency Separability', self.test_frequency_separability),
|
||
('Determinism', self.test_determinism),
|
||
('Courant Stability', self.test_courant_stability),
|
||
]
|
||
|
||
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}")
|
||
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}")
|
||
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:30s}: {status}")
|
||
|
||
print("\n" + "=" * 80)
|
||
if all_passed:
|
||
print("ALL TESTS PASSED")
|
||
print("=" * 80)
|
||
print("""
|
||
The Yee FDTD implementation provides a stable deterministic backbone
|
||
for GWL/TSM electromagnetic field evolution.
|
||
|
||
Key properties verified:
|
||
✓ Energy conserved (< 1% drift)
|
||
✓ Stable propagation (no blowup)
|
||
✓ Frequency separability
|
||
✓ Deterministic reproducibility
|
||
✓ Courant-stable
|
||
|
||
NEXT STEPS:
|
||
1. Add stochastic perturbations on top of this stable backbone
|
||
2. Extend to 2D/3D with proper curl operator
|
||
3. Add medium coupling (ε, μ variations)
|
||
4. Validate speed of light in medium
|
||
""")
|
||
else:
|
||
print("SOME TESTS FAILED")
|
||
print("=" * 80)
|
||
|
||
return all_passed
|
||
|
||
|
||
if __name__ == "__main__":
|
||
test_suite = GWL_YeeEM_Tests()
|
||
success = test_suite.run_all()
|
||
exit(0 if success else 1)
|