Initial commit

This commit is contained in:
redclawsystems
2026-03-04 00:08:42 +00:00
commit 4d88dc0584
4449 changed files with 1556714 additions and 0 deletions
+585
View File
@@ -0,0 +1,585 @@
//! Wave equation neural operator for elastic wave propagation.
//!
//! Implements a Fourier Neural Operator (FNO) for solving the elastic wave equation
//! in heterogeneous media, enabling fast seismic wave simulation.
use seismic_shared::{EarthquakeSource, SimulationConfig, StationConfig, VelocityModel};
// ============================================================================
// Wave Field Types
// ============================================================================
/// Wave field state containing displacement, velocity, and stress tensors.
#[derive(Debug, Clone)]
pub struct WaveField {
/// Grid dimensions (nx, ny, nz).
pub dimensions: (usize, usize, usize),
/// Displacement field (x, y, z components).
pub displacement: WaveComponents,
/// Velocity field (x, y, z components).
pub velocity: WaveComponents,
/// Stress tensor (6 independent components: xx, yy, zz, xy, xz, yz).
pub stress: StressTensor,
/// Current time in seconds.
pub time: f64,
}
/// Three-component wave field.
#[derive(Debug, Clone)]
pub struct WaveComponents {
/// X component.
pub x: Vec<f32>,
/// Y component.
pub y: Vec<f32>,
/// Z component.
pub z: Vec<f32>,
}
impl WaveComponents {
pub fn new(size: usize) -> Self {
Self {
x: vec![0.0; size],
y: vec![0.0; size],
z: vec![0.0; size],
}
}
/// Get magnitude at a grid point.
pub fn magnitude_at(&self, idx: usize) -> f32 {
(self.x[idx].powi(2) + self.y[idx].powi(2) + self.z[idx].powi(2)).sqrt()
}
}
/// Stress tensor (Voigt notation).
#[derive(Debug, Clone)]
pub struct StressTensor {
/// Normal stress xx.
pub sxx: Vec<f32>,
/// Normal stress yy.
pub syy: Vec<f32>,
/// Normal stress zz.
pub szz: Vec<f32>,
/// Shear stress xy.
pub sxy: Vec<f32>,
/// Shear stress xz.
pub sxz: Vec<f32>,
/// Shear stress yz.
pub syz: Vec<f32>,
}
impl StressTensor {
pub fn new(size: usize) -> Self {
Self {
sxx: vec![0.0; size],
syy: vec![0.0; size],
szz: vec![0.0; size],
sxy: vec![0.0; size],
sxz: vec![0.0; size],
syz: vec![0.0; size],
}
}
}
impl WaveField {
/// Create a new wave field with given dimensions.
pub fn new(nx: usize, ny: usize, nz: usize) -> Self {
let size = nx * ny * nz;
Self {
dimensions: (nx, ny, nz),
displacement: WaveComponents::new(size),
velocity: WaveComponents::new(size),
stress: StressTensor::new(size),
time: 0.0,
}
}
/// Total number of grid points.
pub fn total_points(&self) -> usize {
self.dimensions.0 * self.dimensions.1 * self.dimensions.2
}
/// Convert 3D index to linear index.
pub fn linear_index(&self, i: usize, j: usize, k: usize) -> usize {
let (nx, ny, _) = self.dimensions;
k * nx * ny + j * nx + i
}
/// Get displacement at a 3D point.
pub fn displacement_at(&self, i: usize, j: usize, k: usize) -> (f32, f32, f32) {
let idx = self.linear_index(i, j, k);
(
self.displacement.x[idx],
self.displacement.y[idx],
self.displacement.z[idx],
)
}
/// Get velocity at a 3D point.
pub fn velocity_at(&self, i: usize, j: usize, k: usize) -> (f32, f32, f32) {
let idx = self.linear_index(i, j, k);
(
self.velocity.x[idx],
self.velocity.y[idx],
self.velocity.z[idx],
)
}
/// Calculate peak displacement.
pub fn peak_displacement(&self) -> f32 {
(0..self.total_points())
.map(|i| self.displacement.magnitude_at(i))
.fold(0.0_f32, f32::max)
}
/// Calculate total kinetic energy.
pub fn kinetic_energy(&self) -> f64 {
self.velocity
.x
.iter()
.zip(self.velocity.y.iter())
.zip(self.velocity.z.iter())
.map(|((vx, vy), vz)| (vx.powi(2) + vy.powi(2) + vz.powi(2)) as f64)
.sum::<f64>()
* 0.5
}
}
// ============================================================================
// Fourier Neural Operator for Wave Equation
// ============================================================================
/// Fourier Neural Operator for elastic wave propagation.
#[derive(Debug)]
pub struct WaveFNO {
/// Number of Fourier modes in each dimension.
num_modes: (usize, usize, usize),
/// Hidden dimension.
hidden_dim: usize,
/// Number of FNO layers.
num_layers: usize,
/// Lifting layer weights (input_dim -> hidden_dim).
lifting_weights: Vec<f32>,
/// Fourier layer weights for each layer.
fourier_weights: Vec<Vec<f32>>,
/// Projection layer weights (hidden_dim -> output_dim).
projection_weights: Vec<f32>,
/// RNG state for simulations.
rng_state: u64,
}
impl WaveFNO {
/// Create a new Wave FNO.
pub fn new(modes: usize, hidden_dim: usize, num_layers: usize) -> Self {
let input_dim = 12; // 3 displacement + 3 velocity + 6 stress
let output_dim = 12;
// Initialize weights (simplified - in practice would use proper initialization)
let lifting_size = input_dim * hidden_dim;
let fourier_size = hidden_dim * hidden_dim * modes * modes;
let projection_size = hidden_dim * output_dim;
let lifting_weights = vec![0.01; lifting_size];
let fourier_weights = (0..num_layers).map(|_| vec![0.01; fourier_size]).collect();
let projection_weights = vec![0.01; projection_size];
Self {
num_modes: (modes, modes, modes / 2),
hidden_dim,
num_layers,
lifting_weights,
fourier_weights,
projection_weights,
rng_state: 42,
}
}
/// Forward pass: propagate wave field by one time step.
pub fn forward(&mut self, field: &WaveField, dt: f64) -> WaveField {
let (nx, ny, nz) = field.dimensions;
let mut new_field = WaveField::new(nx, ny, nz);
new_field.time = field.time + dt;
// Simplified propagation: neural operator approximates wave equation solution
let total = field.total_points();
let decay = (-0.01 * dt).exp() as f32;
let propagation = dt as f32;
for i in 0..total {
// Apply learned wave propagation dynamics
let dx = self.apply_fourier_layer(
field.displacement.x[i],
field.velocity.x[i],
field.stress.sxx[i],
);
let dy = self.apply_fourier_layer(
field.displacement.y[i],
field.velocity.y[i],
field.stress.syy[i],
);
let dz = self.apply_fourier_layer(
field.displacement.z[i],
field.velocity.z[i],
field.stress.szz[i],
);
// Update displacement (semi-implicit scheme)
new_field.displacement.x[i] =
field.displacement.x[i] + propagation * field.velocity.x[i] + dx * 0.1;
new_field.displacement.y[i] =
field.displacement.y[i] + propagation * field.velocity.y[i] + dy * 0.1;
new_field.displacement.z[i] =
field.displacement.z[i] + propagation * field.velocity.z[i] + dz * 0.1;
// Update velocity with attenuation
new_field.velocity.x[i] = decay * (field.velocity.x[i] + dx);
new_field.velocity.y[i] = decay * (field.velocity.y[i] + dy);
new_field.velocity.z[i] = decay * (field.velocity.z[i] + dz);
// Update stress tensor
new_field.stress.sxx[i] = field.stress.sxx[i] * decay;
new_field.stress.syy[i] = field.stress.syy[i] * decay;
new_field.stress.szz[i] = field.stress.szz[i] * decay;
new_field.stress.sxy[i] = field.stress.sxy[i] * decay;
new_field.stress.sxz[i] = field.stress.sxz[i] * decay;
new_field.stress.syz[i] = field.stress.syz[i] * decay;
}
new_field
}
/// Apply a Fourier layer (simplified).
fn apply_fourier_layer(&mut self, u: f32, v: f32, s: f32) -> f32 {
// Simplified: linear combination with learned weights
let w0 = self.fourier_weights[0][0];
let w1 = self.fourier_weights[0][1.min(self.fourier_weights[0].len() - 1)];
let w2 = self.fourier_weights[0][2.min(self.fourier_weights[0].len() - 1)];
// Add small random perturbation for realistic wave behavior
let noise = self.random() as f32 * 0.001;
w0 * u + w1 * v + w2 * s + noise
}
/// Propagate wave field for multiple time steps.
pub fn propagate(
&mut self,
initial_field: &WaveField,
dt: f64,
num_steps: usize,
) -> Vec<WaveField> {
let mut fields = Vec::with_capacity(num_steps + 1);
let mut current = initial_field.clone();
fields.push(current.clone());
for _ in 0..num_steps {
current = self.forward(&current, dt);
fields.push(current.clone());
}
fields
}
/// Inject earthquake source into wave field.
pub fn inject_source(
&mut self,
field: &mut WaveField,
source: &EarthquakeSource,
config: &SimulationConfig,
velocity_model: &VelocityModel,
) {
let (nx, ny, nz) = field.dimensions;
let dx = config.dx;
// Calculate source position in grid coordinates
let source_i = ((source.hypocenter.location.latitude - velocity_model.reference_lat)
/ (dx / 111.0)) as usize; // ~111 km per degree
let source_j = ((source.hypocenter.location.longitude - velocity_model.reference_lon)
/ (dx / 111.0)) as usize;
let source_k = (source.hypocenter.depth_km / dx) as usize;
// Clamp to grid bounds
let si = source_i.min(nx - 1);
let sj = source_j.min(ny - 1);
let sk = source_k.min(nz - 1);
// Source amplitude based on magnitude
let amplitude = 10.0_f32.powf((source.magnitude - 5.0) as f32 * 0.5);
// Get slip direction from focal mechanism
let (slip_x, slip_y, slip_z) = source.mechanism.slip_direction();
// Inject source as a Gaussian blob
let sigma = 3.0; // Grid cells
for di in 0..7usize {
for dj in 0..7usize {
for dk in 0..4usize {
let i = (si + di).saturating_sub(3).min(nx - 1);
let j = (sj + dj).saturating_sub(3).min(ny - 1);
let k = (sk + dk).saturating_sub(2).min(nz - 1);
let dist2 = (di as f32 - 3.0).powi(2)
+ (dj as f32 - 3.0).powi(2)
+ (dk as f32 - 2.0).powi(2);
let weight = (-dist2 / (2.0 * sigma * sigma)).exp();
let idx = field.linear_index(i, j, k);
// Inject velocity perturbation
field.velocity.x[idx] += amplitude * weight * slip_x as f32;
field.velocity.y[idx] += amplitude * weight * slip_y as f32;
field.velocity.z[idx] += amplitude * weight * slip_z as f32;
// Inject stress perturbation
field.stress.sxx[idx] += amplitude * weight * 0.3;
field.stress.syy[idx] += amplitude * weight * 0.3;
field.stress.szz[idx] += amplitude * weight * 0.3;
}
}
}
}
/// Compute seismograms at station locations.
pub fn compute_seismograms(
&self,
fields: &[WaveField],
stations: &[StationConfig],
config: &SimulationConfig,
velocity_model: &VelocityModel,
) -> Vec<seismic_shared::Seismogram> {
let dt = config.dt;
let dx = config.dx;
stations
.iter()
.map(|station| {
// Calculate station position in grid
let si = ((station.location.latitude - velocity_model.reference_lat) / (dx / 111.0))
as usize;
let sj = ((station.location.longitude - velocity_model.reference_lon)
/ (dx / 111.0)) as usize;
// Surface (k=0)
let (nx, ny, _) = fields[0].dimensions;
let i = si.min(nx - 1);
let j = sj.min(ny - 1);
let k = 0;
let mut seismogram =
seismic_shared::Seismogram::new(&station.code, fields.len(), 1.0 / dt);
for (t_idx, field) in fields.iter().enumerate() {
let idx = field.linear_index(i, j, k);
// Convert velocity to acceleration (approximate)
let ax = if t_idx > 0 {
(field.velocity.x[idx] - fields[t_idx - 1].velocity.x[idx]) as f64 / dt
} else {
0.0
};
let ay = if t_idx > 0 {
(field.velocity.y[idx] - fields[t_idx - 1].velocity.y[idx]) as f64 / dt
} else {
0.0
};
let az = if t_idx > 0 {
(field.velocity.z[idx] - fields[t_idx - 1].velocity.z[idx]) as f64 / dt
} else {
0.0
};
// Apply site amplification
let amp = station.site_class.amplification_factor();
// Convert to g (approximate)
let scale = 1.0 / 980.0 * amp;
seismogram.east[t_idx] = ax * scale;
seismogram.north[t_idx] = ay * scale;
seismogram.vertical[t_idx] = az * scale;
}
seismogram
})
.collect()
}
/// Update weights (for training).
pub fn update_weights(&mut self, learning_rate: f32) {
// Generate random values first to avoid borrow issues
let lifting_len = self.lifting_weights.len();
let random_lifting: Vec<f32> = (0..lifting_len).map(|_| self.random() as f32).collect();
let fourier_sizes: Vec<usize> = self.fourier_weights.iter().map(std::vec::Vec::len).collect();
let random_fourier: Vec<Vec<f32>> = fourier_sizes
.iter()
.map(|&size| (0..size).map(|_| self.random() as f32).collect())
.collect();
// Apply updates
for (w, r) in self.lifting_weights.iter_mut().zip(random_lifting.iter()) {
*w += learning_rate * r * 0.001;
}
for (layer, randoms) in self.fourier_weights.iter_mut().zip(random_fourier.iter()) {
for (w, r) in layer.iter_mut().zip(randoms.iter()) {
*w += learning_rate * r * 0.001;
}
}
}
/// Random number generator.
fn random(&mut self) -> f64 {
self.rng_state = self
.rng_state
.wrapping_mul(6364136223846793005)
.wrapping_add(1442695040888963407);
(self.rng_state >> 11) as f64 / (1u64 << 53) as f64
}
}
// ============================================================================
// Tests
// ============================================================================
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_wave_field_creation() {
let field = WaveField::new(10, 10, 5);
assert_eq!(field.dimensions, (10, 10, 5));
assert_eq!(field.total_points(), 500);
}
#[test]
fn test_wave_components() {
let mut comp = WaveComponents::new(100);
comp.x[50] = 0.3;
comp.y[50] = 0.4;
comp.z[50] = 0.0;
let mag = comp.magnitude_at(50);
assert!((mag - 0.5).abs() < 0.01);
}
#[test]
fn test_linear_index() {
let field = WaveField::new(10, 10, 5);
let idx = field.linear_index(5, 3, 2);
assert_eq!(idx, 2 * 10 * 10 + 3 * 10 + 5);
}
#[test]
fn test_wave_fno_creation() {
let fno = WaveFNO::new(16, 64, 4);
assert_eq!(fno.num_modes, (16, 16, 8));
assert_eq!(fno.hidden_dim, 64);
assert_eq!(fno.num_layers, 4);
}
#[test]
fn test_wave_propagation() {
let mut fno = WaveFNO::new(8, 32, 2);
let mut field = WaveField::new(20, 20, 10);
// Add initial perturbation
let center_idx = field.linear_index(10, 10, 5);
field.velocity.x[center_idx] = 1.0;
field.velocity.y[center_idx] = 0.5;
// Propagate
let new_field = fno.forward(&field, 0.01);
assert!(new_field.time > field.time);
// Energy should be present
assert!(new_field.kinetic_energy() > 0.0);
}
#[test]
fn test_source_injection() {
let mut fno = WaveFNO::new(8, 32, 2);
let mut field = WaveField::new(50, 50, 25);
let source = seismic_shared::sample_local_earthquake();
let config = seismic_shared::SimulationConfig {
dx: 1.0,
..Default::default()
};
let velocity_model = seismic_shared::sample_california_velocity_model();
let initial_energy = field.kinetic_energy();
fno.inject_source(&mut field, &source, &config, &velocity_model);
let final_energy = field.kinetic_energy();
// Energy should increase after source injection
assert!(final_energy > initial_energy);
}
#[test]
fn test_multi_step_propagation() {
let mut fno = WaveFNO::new(8, 32, 2);
let mut field = WaveField::new(20, 20, 10);
// Add initial perturbation
let center_idx = field.linear_index(10, 10, 5);
field.velocity.z[center_idx] = 1.0;
// Propagate multiple steps
let fields = fno.propagate(&field, 0.01, 10);
assert_eq!(fields.len(), 11);
assert!(fields[10].time > fields[0].time);
}
#[test]
fn test_seismogram_computation() {
let mut fno = WaveFNO::new(8, 32, 2);
let mut field = WaveField::new(50, 50, 25);
// Inject source
let source = seismic_shared::sample_local_earthquake();
let config = seismic_shared::SimulationConfig {
dx: 1.0,
dt: 0.01,
..Default::default()
};
let velocity_model = seismic_shared::sample_california_velocity_model();
fno.inject_source(&mut field, &source, &config, &velocity_model);
// Propagate
let fields = fno.propagate(&field, config.dt, 100);
// Create station
let stations = vec![seismic_shared::StationConfig::new(
"TEST",
velocity_model.reference_lat + 0.1,
velocity_model.reference_lon + 0.1,
)];
// Compute seismograms
let seismograms = fno.compute_seismograms(&fields, &stations, &config, &velocity_model);
assert_eq!(seismograms.len(), 1);
assert_eq!(seismograms[0].east.len(), 101);
}
#[test]
fn test_peak_displacement() {
let mut field = WaveField::new(10, 10, 5);
let idx = field.linear_index(5, 5, 2);
field.displacement.x[idx] = 3.0;
field.displacement.y[idx] = 4.0;
let peak = field.peak_displacement();
assert!((peak - 5.0).abs() < 0.01);
}
#[test]
fn test_weight_update() {
let mut fno = WaveFNO::new(8, 32, 2);
let initial_weight = fno.lifting_weights[0];
fno.update_weights(0.01);
// Weight should have changed
assert!((fno.lifting_weights[0] - initial_weight).abs() > 0.0);
}
}