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]>
1079 lines
32 KiB
Rust
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);
|
|
}
|
|
}
|