//! 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, /// Y component. pub y: Vec, /// Z component. pub z: Vec, } 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, /// Normal stress yy. pub syy: Vec, /// Normal stress zz. pub szz: Vec, /// Shear stress xy. pub sxy: Vec, /// Shear stress xz. pub sxz: Vec, /// Shear stress yz. pub syz: Vec, } 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::() * 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, /// Fourier layer weights for each layer. fourier_weights: Vec>, /// Projection layer weights (hidden_dim -> output_dim). projection_weights: Vec, /// 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 { 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(¤t, 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 { 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 = (0..lifting_len).map(|_| self.random() as f32).collect(); let fourier_sizes: Vec = self.fourier_weights.iter().map(std::vec::Vec::len).collect(); let random_fourier: Vec> = 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); } }