//! Shared types for the SeismicAI earthquake simulation and early warning demo. //! //! This crate provides IPC types for seismic wave propagation simulation //! using neural operators for fast ground motion prediction. use serde::{Deserialize, Serialize}; // ============================================================================ // Geographic Types // ============================================================================ /// Geographic location (latitude, longitude). #[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)] pub struct GeoLocation { /// Latitude in degrees (-90 to 90). pub latitude: f64, /// Longitude in degrees (-180 to 180). pub longitude: f64, } impl GeoLocation { pub fn new(latitude: f64, longitude: f64) -> Self { Self { latitude, longitude, } } /// Calculate distance to another location in kilometers (Haversine formula). pub fn distance_km(&self, other: &GeoLocation) -> f64 { const EARTH_RADIUS_KM: f64 = 6371.0; let lat1 = self.latitude.to_radians(); let lat2 = other.latitude.to_radians(); let dlat = (other.latitude - self.latitude).to_radians(); let dlon = (other.longitude - self.longitude).to_radians(); let a = (dlat / 2.0).sin().powi(2) + lat1.cos() * lat2.cos() * (dlon / 2.0).sin().powi(2); let c = 2.0 * a.sqrt().asin(); EARTH_RADIUS_KM * c } /// Calculate azimuth to another location in degrees. pub fn azimuth_deg(&self, other: &GeoLocation) -> f64 { let lat1 = self.latitude.to_radians(); let lat2 = other.latitude.to_radians(); let dlon = (other.longitude - self.longitude).to_radians(); let x = dlon.sin() * lat2.cos(); let y = lat1.cos() * lat2.sin() - lat1.sin() * lat2.cos() * dlon.cos(); x.atan2(y).to_degrees().rem_euclid(360.0) } } impl Default for GeoLocation { fn default() -> Self { // San Francisco, CA Self { latitude: 37.7749, longitude: -122.4194, } } } /// 3D position including depth. #[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)] pub struct Position3D { /// Geographic location. pub location: GeoLocation, /// Depth in kilometers (positive downward). pub depth_km: f64, } impl Position3D { pub fn new(latitude: f64, longitude: f64, depth_km: f64) -> Self { Self { location: GeoLocation::new(latitude, longitude), depth_km, } } /// Calculate 3D distance to another position in kilometers. pub fn distance_3d_km(&self, other: &Position3D) -> f64 { let horizontal = self.location.distance_km(&other.location); let vertical = (self.depth_km - other.depth_km).abs(); (horizontal.powi(2) + vertical.powi(2)).sqrt() } } // ============================================================================ // Earthquake Source Types // ============================================================================ /// Focal mechanism (beach ball) parameters. #[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)] pub struct FocalMechanism { /// Strike angle in degrees (0-360). pub strike: f64, /// Dip angle in degrees (0-90). pub dip: f64, /// Rake angle in degrees (-180 to 180). pub rake: f64, } impl FocalMechanism { pub fn new(strike: f64, dip: f64, rake: f64) -> Self { Self { strike, dip, rake } } /// Strike-slip mechanism (vertical fault, horizontal slip). pub fn strike_slip() -> Self { Self { strike: 0.0, dip: 90.0, rake: 0.0, } } /// Normal fault mechanism (extensional). pub fn normal() -> Self { Self { strike: 0.0, dip: 60.0, rake: -90.0, } } /// Reverse/thrust fault mechanism (compressional). pub fn reverse() -> Self { Self { strike: 0.0, dip: 30.0, rake: 90.0, } } /// Calculate slip vector direction in local coordinates. pub fn slip_direction(&self) -> (f64, f64, f64) { let strike_rad = self.strike.to_radians(); let dip_rad = self.dip.to_radians(); let rake_rad = self.rake.to_radians(); let x = rake_rad.cos() * strike_rad.cos() + rake_rad.sin() * dip_rad.cos() * strike_rad.sin(); let y = rake_rad.cos() * strike_rad.sin() - rake_rad.sin() * dip_rad.cos() * strike_rad.cos(); let z = -rake_rad.sin() * dip_rad.sin(); (x, y, z) } } impl Default for FocalMechanism { fn default() -> Self { Self::strike_slip() } } /// Earthquake source parameters. #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct EarthquakeSource { /// Event identifier. pub event_id: String, /// Hypocenter location. pub hypocenter: Position3D, /// Moment magnitude (Mw). pub magnitude: f64, /// Focal mechanism. pub mechanism: FocalMechanism, /// Origin time in seconds from simulation start. pub origin_time: f64, /// Rupture area in km^2 (estimated from magnitude if not provided). pub rupture_area_km2: Option, /// Average slip in meters (estimated from magnitude if not provided). pub average_slip_m: Option, /// Rupture duration in seconds. pub rupture_duration_s: Option, /// Stress drop in MPa. pub stress_drop_mpa: Option, } impl EarthquakeSource { pub fn new( latitude: f64, longitude: f64, depth_km: f64, magnitude: f64, mechanism: FocalMechanism, ) -> Self { Self { event_id: format!("EQ_{:.4}_{:.4}_{:.1}", latitude, longitude, magnitude), hypocenter: Position3D::new(latitude, longitude, depth_km), magnitude, mechanism, origin_time: 0.0, rupture_area_km2: None, average_slip_m: None, rupture_duration_s: None, stress_drop_mpa: None, } } /// Estimate seismic moment in N*m from magnitude. pub fn seismic_moment(&self) -> f64 { 10.0_f64.powf(1.5 * self.magnitude + 9.1) } /// Estimate rupture area in km^2 from magnitude (Wells & Coppersmith 1994). pub fn estimated_rupture_area(&self) -> f64 { self.rupture_area_km2 .unwrap_or_else(|| 10.0_f64.powf(self.magnitude - 4.0)) } /// Estimate rupture length in km. pub fn estimated_rupture_length(&self) -> f64 { 10.0_f64.powf(0.5 * self.magnitude - 1.85) } /// Estimate rupture duration in seconds. pub fn estimated_rupture_duration(&self) -> f64 { self.rupture_duration_s .unwrap_or_else(|| 10.0_f64.powf(0.5 * self.magnitude - 2.5)) } /// Corner frequency in Hz. pub fn corner_frequency(&self) -> f64 { 1.0 / self.estimated_rupture_duration() } } impl Default for EarthquakeSource { fn default() -> Self { Self::new(37.0, -122.0, 10.0, 5.0, FocalMechanism::strike_slip()) } } // ============================================================================ // Ground Motion Types // ============================================================================ /// Ground motion parameters at a station. #[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)] pub struct GroundMotion { /// Peak Ground Acceleration in g. pub pga: f64, /// Peak Ground Velocity in cm/s. pub pgv: f64, /// Peak Ground Displacement in cm. pub pgd: f64, /// P-wave arrival time in seconds from origin. pub p_arrival_time: f64, /// S-wave arrival time in seconds from origin. pub s_arrival_time: f64, /// Duration of strong shaking (5-95% Arias intensity) in seconds. pub duration_5_95: f64, /// Spectral acceleration at 0.3s period in g. pub sa_03: f64, /// Spectral acceleration at 1.0s period in g. pub sa_10: f64, /// Spectral acceleration at 3.0s period in g. pub sa_30: f64, /// Modified Mercalli Intensity (estimated). pub mmi: f64, } impl GroundMotion { /// Calculate MMI from PGA (Wald et al., 1999). pub fn estimate_mmi_from_pga(pga_g: f64) -> f64 { let pga_cm_s2 = pga_g * 980.665; if pga_cm_s2 < 0.0017 { 1.0 } else { (3.66 * pga_cm_s2.log10() - 1.66).clamp(1.0, 12.0) } } /// Calculate MMI from PGV (Wald et al., 1999). pub fn estimate_mmi_from_pgv(pgv_cm_s: f64) -> f64 { if pgv_cm_s < 0.1 { 1.0 } else { (3.47 * pgv_cm_s.log10() + 2.35).clamp(1.0, 12.0) } } /// Create ground motion from basic parameters. pub fn from_pga_pgv(pga: f64, pgv: f64, p_arrival: f64, s_arrival: f64) -> Self { let mmi = f64::midpoint( Self::estimate_mmi_from_pga(pga), Self::estimate_mmi_from_pgv(pgv), ); Self { pga, pgv, pgd: pgv * 0.1, // Rough estimate p_arrival_time: p_arrival, s_arrival_time: s_arrival, duration_5_95: 10.0 + (s_arrival - p_arrival) * 2.0, sa_03: pga * 2.5, // Amplification factor estimate sa_10: pga * 1.0, sa_30: pga * 0.3, mmi, } } } impl Default for GroundMotion { fn default() -> Self { Self { pga: 0.0, pgv: 0.0, pgd: 0.0, p_arrival_time: 0.0, s_arrival_time: 0.0, duration_5_95: 0.0, sa_03: 0.0, sa_10: 0.0, sa_30: 0.0, mmi: 1.0, } } } // ============================================================================ // Station Types // ============================================================================ /// Seismic instrument type. #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)] pub enum InstrumentType { /// Strong motion accelerometer. #[default] Accelerometer, /// Broadband seismometer. Broadband, /// Short-period seismometer. ShortPeriod, /// MEMS accelerometer (low-cost). Mems, /// Rotational sensor. Rotational, } /// Site class based on Vs30 (average shear-wave velocity in top 30m). #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)] pub enum SiteClass { /// Hard rock (Vs30 > 1500 m/s). A, /// Rock (760 < Vs30 <= 1500 m/s). B, /// Very dense soil / soft rock (360 < Vs30 <= 760 m/s). #[default] C, /// Stiff soil (180 < Vs30 <= 360 m/s). D, /// Soft soil (Vs30 <= 180 m/s). E, } impl SiteClass { /// Get approximate Vs30 for this site class. pub fn typical_vs30(&self) -> f64 { match self { SiteClass::A => 2000.0, SiteClass::B => 1000.0, SiteClass::C => 500.0, SiteClass::D => 270.0, SiteClass::E => 150.0, } } /// Get site amplification factor. pub fn amplification_factor(&self) -> f64 { match self { SiteClass::A => 0.8, SiteClass::B => 1.0, SiteClass::C => 1.2, SiteClass::D => 1.6, SiteClass::E => 2.5, } } } /// Seismic station configuration. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct StationConfig { /// Station code. pub code: String, /// Station name. pub name: String, /// Station location. pub location: GeoLocation, /// Instrument type. pub instrument: InstrumentType, /// Site class. pub site_class: SiteClass, /// Vs30 in m/s (if measured). pub vs30: Option, /// Basin depth (Z1.0) in km. pub z1_0: Option, /// Basin depth (Z2.5) in km. pub z2_5: Option, /// Network code. pub network: String, /// Whether station is operational. pub operational: bool, } impl StationConfig { pub fn new(code: &str, latitude: f64, longitude: f64) -> Self { Self { code: code.to_string(), name: format!("Station {}", code), location: GeoLocation::new(latitude, longitude), instrument: InstrumentType::Accelerometer, site_class: SiteClass::C, vs30: None, z1_0: None, z2_5: None, network: "XX".to_string(), operational: true, } } /// Get Vs30 (use measured or site class default). pub fn get_vs30(&self) -> f64 { self.vs30.unwrap_or_else(|| self.site_class.typical_vs30()) } } impl Default for StationConfig { fn default() -> Self { Self::new("STA01", 37.8, -122.4) } } // ============================================================================ // Velocity Model Types // ============================================================================ /// 1D velocity model layer. #[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)] pub struct VelocityLayer { /// Depth to top of layer in km. pub depth_km: f64, /// Thickness of layer in km. pub thickness_km: f64, /// P-wave velocity in km/s. pub vp: f64, /// S-wave velocity in km/s. pub vs: f64, /// Density in g/cm^3. pub density: f64, /// P-wave quality factor (attenuation). pub qp: f64, /// S-wave quality factor (attenuation). pub qs: f64, } impl VelocityLayer { pub fn new(depth_km: f64, thickness_km: f64, vp: f64, vs: f64, density: f64) -> Self { Self { depth_km, thickness_km, vp, vs, density, qp: 500.0, qs: 250.0, } } /// Vp/Vs ratio. pub fn vp_vs_ratio(&self) -> f64 { self.vp / self.vs } /// Poisson's ratio. pub fn poisson_ratio(&self) -> f64 { let ratio = self.vp_vs_ratio(); (ratio.powi(2) - 2.0) / (2.0 * (ratio.powi(2) - 1.0)) } /// Shear modulus in GPa. pub fn shear_modulus(&self) -> f64 { self.density * self.vs.powi(2) } } /// 1D layered velocity model. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct VelocityModel { /// Model name. pub name: String, /// Layers from surface to depth. pub layers: Vec, /// Reference latitude. pub reference_lat: f64, /// Reference longitude. pub reference_lon: f64, } impl VelocityModel { pub fn new(name: &str) -> Self { Self { name: name.to_string(), layers: Vec::new(), reference_lat: 0.0, reference_lon: 0.0, } } /// Add a layer to the model. pub fn add_layer(&mut self, layer: VelocityLayer) { self.layers.push(layer); } /// Get velocity at depth. pub fn velocity_at_depth(&self, depth_km: f64) -> Option<(f64, f64)> { let mut cumulative_depth = 0.0; for layer in &self.layers { cumulative_depth += layer.thickness_km; if depth_km <= cumulative_depth { return Some((layer.vp, layer.vs)); } } self.layers.last().map(|l| (l.vp, l.vs)) } /// Calculate travel time for P-wave. pub fn p_wave_travel_time(&self, distance_km: f64, depth_km: f64) -> f64 { // Simplified calculation using average velocity let avg_vp = self.layers.iter().map(|l| l.vp).sum::() / self.layers.len() as f64; let path_length = (distance_km.powi(2) + depth_km.powi(2)).sqrt(); path_length / avg_vp } /// Calculate travel time for S-wave. pub fn s_wave_travel_time(&self, distance_km: f64, depth_km: f64) -> f64 { let avg_vs = self.layers.iter().map(|l| l.vs).sum::() / self.layers.len() as f64; let path_length = (distance_km.powi(2) + depth_km.powi(2)).sqrt(); path_length / avg_vs } } impl Default for VelocityModel { fn default() -> Self { let mut model = Self::new("Generic"); model.add_layer(VelocityLayer::new(0.0, 5.0, 5.5, 3.2, 2.6)); model.add_layer(VelocityLayer::new(5.0, 15.0, 6.5, 3.7, 2.8)); model.add_layer(VelocityLayer::new(20.0, 15.0, 7.8, 4.5, 3.2)); model } } // ============================================================================ // Simulation Configuration // ============================================================================ /// Wave propagation simulation configuration. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct SimulationConfig { /// Time step in seconds. pub dt: f64, /// Total simulation duration in seconds. pub duration: f64, /// Spatial resolution in km. pub dx: f64, /// Domain size in km (x, y, z). pub domain_size: (f64, f64, f64), /// Maximum frequency in Hz. pub max_frequency: f64, /// Include anelastic attenuation. pub include_attenuation: bool, /// Include site effects. pub include_site_effects: bool, /// Use neural operator for acceleration. pub use_neural_operator: bool, /// Number of Fourier modes for FNO. pub fno_modes: usize, /// Hidden dimension for neural networks. pub hidden_dim: usize, } impl Default for SimulationConfig { fn default() -> Self { Self { dt: 0.01, duration: 60.0, dx: 0.5, domain_size: (100.0, 100.0, 50.0), max_frequency: 10.0, include_attenuation: true, include_site_effects: true, use_neural_operator: true, fno_modes: 16, hidden_dim: 64, } } } // ============================================================================ // Early Warning Configuration // ============================================================================ /// Warning alert level. #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize, Default)] pub enum AlertLevel { /// No alert. #[default] None, /// Information only. Advisory, /// Prepare for shaking. Watch, /// Shaking imminent. Warning, /// Severe shaking expected. Severe, } impl AlertLevel { /// Get alert level from MMI. pub fn from_mmi(mmi: f64) -> Self { if mmi < 3.0 { AlertLevel::None } else if mmi < 4.0 { AlertLevel::Advisory } else if mmi < 5.0 { AlertLevel::Watch } else if mmi < 7.0 { AlertLevel::Warning } else { AlertLevel::Severe } } /// Get alert level from PGA. pub fn from_pga(pga: f64) -> Self { if pga < 0.01 { AlertLevel::None } else if pga < 0.05 { AlertLevel::Advisory } else if pga < 0.1 { AlertLevel::Watch } else if pga < 0.3 { AlertLevel::Warning } else { AlertLevel::Severe } } } /// Early warning system configuration. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct WarningConfig { /// Minimum magnitude to issue warning. pub min_magnitude: f64, /// Minimum number of stations for detection. pub min_stations: usize, /// Alert threshold (seconds of warning time). pub alert_threshold_seconds: f64, /// Enable sound alerts. pub enable_sound: bool, /// Enable push notifications. pub enable_notifications: bool, /// Alert radius in km. pub alert_radius_km: f64, } impl Default for WarningConfig { fn default() -> Self { Self { min_magnitude: 4.0, min_stations: 3, alert_threshold_seconds: 3.0, enable_sound: true, enable_notifications: true, alert_radius_km: 100.0, } } } /// Earthquake early warning alert. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct EarthquakeWarning { /// Event identifier. pub event_id: String, /// Alert level. pub alert_level: AlertLevel, /// Estimated magnitude. pub estimated_magnitude: f64, /// Estimated location. pub estimated_location: GeoLocation, /// Estimated depth in km. pub estimated_depth_km: f64, /// Number of stations detecting event. pub detecting_stations: usize, /// Time to S-wave arrival in seconds. pub time_to_shaking: f64, /// Expected peak ground motion. pub expected_ground_motion: GroundMotion, /// Warning issued time (seconds from simulation start). pub warning_time: f64, /// Origin time (seconds from simulation start). pub origin_time: f64, /// Confidence level (0-1). pub confidence: f64, } impl Default for EarthquakeWarning { fn default() -> Self { Self { event_id: String::new(), alert_level: AlertLevel::None, estimated_magnitude: 0.0, estimated_location: GeoLocation::default(), estimated_depth_km: 10.0, detecting_stations: 0, time_to_shaking: 0.0, expected_ground_motion: GroundMotion::default(), warning_time: 0.0, origin_time: 0.0, confidence: 0.0, } } } // ============================================================================ // Simulation Results // ============================================================================ /// Wave field snapshot. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct WaveFieldSnapshot { /// Time of snapshot in seconds. pub time: f64, /// X-displacement field. pub displacement_x: Vec>, /// Y-displacement field. pub displacement_y: Vec>, /// Z-displacement field. pub displacement_z: Vec>, /// Grid X coordinates. pub grid_x: Vec, /// Grid Y coordinates. pub grid_y: Vec, } /// Seismogram at a station. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Seismogram { /// Station code. pub station_code: String, /// Time samples in seconds. pub time: Vec, /// East-West component (acceleration in g). pub east: Vec, /// North-South component (acceleration in g). pub north: Vec, /// Vertical component (acceleration in g). pub vertical: Vec, /// Sample rate in Hz. pub sample_rate: f64, } impl Seismogram { pub fn new(station_code: &str, num_samples: usize, sample_rate: f64) -> Self { let time: Vec = (0..num_samples).map(|i| i as f64 / sample_rate).collect(); Self { station_code: station_code.to_string(), time, east: vec![0.0; num_samples], north: vec![0.0; num_samples], vertical: vec![0.0; num_samples], sample_rate, } } /// Calculate PGA from all components. pub fn pga(&self) -> f64 { let max_e = self.east.iter().map(|v| v.abs()).fold(0.0_f64, f64::max); let max_n = self.north.iter().map(|v| v.abs()).fold(0.0_f64, f64::max); let max_v = self .vertical .iter() .map(|v| v.abs()) .fold(0.0_f64, f64::max); (max_e.powi(2) + max_n.powi(2) + max_v.powi(2)).sqrt() } /// Calculate horizontal PGA. pub fn pga_horizontal(&self) -> f64 { self.east .iter() .zip(self.north.iter()) .map(|(e, n)| (e.powi(2) + n.powi(2)).sqrt()) .fold(0.0_f64, f64::max) } } /// Complete simulation result. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct SimulationResult { /// Earthquake source. pub source: EarthquakeSource, /// Station ground motions. pub ground_motions: Vec<(StationConfig, GroundMotion)>, /// Seismograms (optional, may be large). pub seismograms: Option>, /// Wave field snapshots (optional). pub snapshots: Option>, /// Early warning issued. pub warning: Option, /// Computation time in milliseconds. pub computation_time_ms: f64, } // ============================================================================ // Sample Data Functions // ============================================================================ /// Create a sample local earthquake (shallow, nearby). pub fn sample_local_earthquake() -> EarthquakeSource { EarthquakeSource::new( 37.8044, // Near Berkeley, CA -122.2712, 8.0, // 8 km depth 4.5, // M4.5 FocalMechanism::strike_slip(), ) } /// Create a sample regional earthquake (moderate, distant). pub fn sample_regional_earthquake() -> EarthquakeSource { EarthquakeSource::new( 36.5, // Central California -121.0, 15.0, // 15 km depth 6.0, // M6.0 FocalMechanism::reverse(), ) } /// Create a sample major earthquake. pub fn sample_major_earthquake() -> EarthquakeSource { EarthquakeSource::new( 37.4, // Near San Jose -122.1, 10.0, // 10 km depth 7.0, // M7.0 FocalMechanism::strike_slip(), ) } /// Create a sample dense station network. pub fn sample_dense_network() -> Vec { let mut stations = Vec::new(); let center_lat = 37.8; let center_lon = -122.4; for i in 0..10 { for j in 0..10 { let lat = center_lat - 0.5 + (i as f64) * 0.1; let lon = center_lon - 0.5 + (j as f64) * 0.1; let code = format!("S{:02}{:02}", i, j); let mut station = StationConfig::new(&code, lat, lon); station.network = "CI".to_string(); stations.push(station); } } stations } /// Create a sample sparse station network. pub fn sample_sparse_network() -> Vec { vec![ StationConfig::new("BK.BKS", 37.8764, -122.2356), StationConfig::new("BK.CMB", 38.0346, -120.3865), StationConfig::new("BK.SAO", 36.7640, -121.4472), StationConfig::new("CI.SLA", 35.8900, -117.2833), StationConfig::new("NC.KRP", 40.4916, -124.2834), ] } /// Create a California velocity model. pub fn sample_california_velocity_model() -> VelocityModel { let mut model = VelocityModel::new("California Generic"); model.reference_lat = 37.0; model.reference_lon = -122.0; // Simplified California crust model.add_layer(VelocityLayer::new(0.0, 2.0, 4.0, 2.3, 2.4)); model.add_layer(VelocityLayer::new(2.0, 5.0, 5.5, 3.2, 2.6)); model.add_layer(VelocityLayer::new(7.0, 8.0, 6.3, 3.6, 2.8)); model.add_layer(VelocityLayer::new(15.0, 10.0, 6.7, 3.9, 2.9)); model.add_layer(VelocityLayer::new(25.0, 10.0, 7.8, 4.5, 3.2)); model } /// Create default simulation configuration. pub fn sample_simulation_config() -> SimulationConfig { SimulationConfig::default() } /// Create default warning configuration. pub fn sample_warning_config() -> WarningConfig { WarningConfig::default() } // ============================================================================ // Tests // ============================================================================ #[cfg(test)] mod tests { use super::*; #[test] fn test_geo_location_distance() { let sf = GeoLocation::new(37.7749, -122.4194); let la = GeoLocation::new(34.0522, -118.2437); let distance = sf.distance_km(&la); // SF to LA is about 560 km assert!((distance - 560.0).abs() < 10.0); } #[test] fn test_geo_location_azimuth() { let origin = GeoLocation::new(0.0, 0.0); let north = GeoLocation::new(1.0, 0.0); let east = GeoLocation::new(0.0, 1.0); let azimuth_to_north = origin.azimuth_deg(&north); let azimuth_to_east = origin.azimuth_deg(&east); assert!((azimuth_to_north - 0.0).abs() < 1.0); assert!((azimuth_to_east - 90.0).abs() < 1.0); } #[test] fn test_focal_mechanism() { let ss = FocalMechanism::strike_slip(); assert_eq!(ss.dip, 90.0); assert_eq!(ss.rake, 0.0); let reverse = FocalMechanism::reverse(); assert_eq!(reverse.rake, 90.0); let normal = FocalMechanism::normal(); assert_eq!(normal.rake, -90.0); } #[test] fn test_earthquake_source() { let eq = EarthquakeSource::new(37.0, -122.0, 10.0, 6.0, FocalMechanism::strike_slip()); // M6.0 should have seismic moment around 10^18 N*m let moment = eq.seismic_moment(); assert!(moment > 1e17 && moment < 1e19); // Rupture area should be reasonable let area = eq.estimated_rupture_area(); assert!(area > 1.0 && area < 1000.0); } #[test] fn test_ground_motion_mmi() { let mmi_pga = GroundMotion::estimate_mmi_from_pga(0.1); assert!(mmi_pga > 5.0 && mmi_pga < 8.0); let mmi_pgv = GroundMotion::estimate_mmi_from_pgv(10.0); assert!(mmi_pgv > 5.0 && mmi_pgv < 8.0); } #[test] fn test_site_class() { assert!(SiteClass::A.typical_vs30() > SiteClass::E.typical_vs30()); assert!(SiteClass::E.amplification_factor() > SiteClass::A.amplification_factor()); } #[test] fn test_velocity_model() { let model = sample_california_velocity_model(); assert!(!model.layers.is_empty()); let (vp, vs) = model.velocity_at_depth(10.0).unwrap(); assert!(vp > vs); assert!(vp / vs > 1.5 && vp / vs < 2.0); } #[test] fn test_velocity_layer() { let layer = VelocityLayer::new(0.0, 5.0, 6.0, 3.5, 2.7); let poisson = layer.poisson_ratio(); assert!(poisson > 0.2 && poisson < 0.35); let vp_vs = layer.vp_vs_ratio(); assert!((vp_vs - 6.0 / 3.5).abs() < 0.01); } #[test] fn test_alert_level() { assert_eq!(AlertLevel::from_mmi(2.0), AlertLevel::None); assert_eq!(AlertLevel::from_mmi(6.0), AlertLevel::Warning); assert_eq!(AlertLevel::from_mmi(8.0), AlertLevel::Severe); assert_eq!(AlertLevel::from_pga(0.005), AlertLevel::None); assert_eq!(AlertLevel::from_pga(0.2), AlertLevel::Warning); assert_eq!(AlertLevel::from_pga(0.5), AlertLevel::Severe); } #[test] fn test_seismogram() { let mut seis = Seismogram::new("TEST", 1000, 100.0); seis.east[500] = 0.1; seis.north[500] = 0.1; let pga_h = seis.pga_horizontal(); assert!((pga_h - (0.02_f64).sqrt()).abs() < 0.001); } #[test] fn test_sample_networks() { let dense = sample_dense_network(); assert_eq!(dense.len(), 100); let sparse = sample_sparse_network(); assert!(sparse.len() < 10); } #[test] fn test_sample_earthquakes() { let local = sample_local_earthquake(); assert!(local.magnitude < 5.0); let regional = sample_regional_earthquake(); assert!(regional.magnitude >= 5.0); let major = sample_major_earthquake(); assert!(major.magnitude >= 7.0); } #[test] fn test_serialization() { let eq = sample_local_earthquake(); let json = serde_json::to_string(&eq).unwrap(); let _: EarthquakeSource = serde_json::from_str(&json).unwrap(); let station = StationConfig::default(); let json = serde_json::to_string(&station).unwrap(); let _: StationConfig = serde_json::from_str(&json).unwrap(); } #[test] fn test_position_3d() { let p1 = Position3D::new(37.0, -122.0, 10.0); let p2 = Position3D::new(37.0, -122.0, 20.0); let dist = p1.distance_3d_km(&p2); assert!((dist - 10.0).abs() < 0.1); } #[test] fn test_config_defaults() { let sim_config = SimulationConfig::default(); assert!(sim_config.duration > 0.0); assert!(sim_config.use_neural_operator); let warn_config = WarningConfig::default(); assert!(warn_config.min_magnitude > 0.0); } }