Files
rustytorch/demos/seismic-shared/src/lib.rs
T
osobhandClaude Opus 4.6 02d382d5f6 style: apply rustfmt across all crates and demos
Consistent formatting pass: line wrapping, import sorting, trailing
whitespace removal, let-chain indentation, merged derive attributes,
and unsafe block reformatting.

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
2026-04-12 07:01:58 -07:00

1079 lines
32 KiB
Rust

//! 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<f64>,
/// Average slip in meters (estimated from magnitude if not provided).
pub average_slip_m: Option<f64>,
/// Rupture duration in seconds.
pub rupture_duration_s: Option<f64>,
/// Stress drop in MPa.
pub stress_drop_mpa: Option<f64>,
}
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<f64>,
/// Basin depth (Z1.0) in km.
pub z1_0: Option<f64>,
/// Basin depth (Z2.5) in km.
pub z2_5: Option<f64>,
/// 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<VelocityLayer>,
/// 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::<f64>() / 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::<f64>() / 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<Vec<f32>>,
/// Y-displacement field.
pub displacement_y: Vec<Vec<f32>>,
/// Z-displacement field.
pub displacement_z: Vec<Vec<f32>>,
/// Grid X coordinates.
pub grid_x: Vec<f32>,
/// Grid Y coordinates.
pub grid_y: Vec<f32>,
}
/// 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<f64>,
/// East-West component (acceleration in g).
pub east: Vec<f64>,
/// North-South component (acceleration in g).
pub north: Vec<f64>,
/// Vertical component (acceleration in g).
pub vertical: Vec<f64>,
/// 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<f64> = (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<Vec<Seismogram>>,
/// Wave field snapshots (optional).
pub snapshots: Option<Vec<WaveFieldSnapshot>>,
/// Early warning issued.
pub warning: Option<EarthquakeWarning>,
/// 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<StationConfig> {
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<StationConfig> {
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);
}
}