642 lines
21 KiB
Rust
642 lines
21 KiB
Rust
//! SeismicAI: Earthquake Simulation & Early Warning with Neural Operators
|
|
//!
|
|
//! This demo showcases neural operator-based seismic wave propagation for
|
|
//! earthquake simulation and early warning systems. It demonstrates:
|
|
//! - Fourier Neural Operators for elastic wave propagation
|
|
//! - Ground motion prediction equations (GMPEs)
|
|
//! - Real-time earthquake early warning
|
|
//! - Site effects and attenuation modeling
|
|
|
|
pub mod attenuation;
|
|
pub mod sample_data;
|
|
pub mod wave_operator;
|
|
|
|
use attenuation::{AttenuationModel, SiteEffects, GMPE};
|
|
use seismic_shared::{
|
|
AlertLevel, EarthquakeSource, EarthquakeWarning, GeoLocation, GroundMotion, SimulationConfig,
|
|
SimulationResult, StationConfig, VelocityModel, WarningConfig, WaveFieldSnapshot,
|
|
};
|
|
use wave_operator::{WaveFNO, WaveField};
|
|
|
|
// ============================================================================
|
|
// SeismicAI System
|
|
// ============================================================================
|
|
|
|
/// Main SeismicAI system for earthquake simulation and early warning.
|
|
#[derive(Debug)]
|
|
pub struct SeismicAI {
|
|
/// Wave operator for full waveform simulation.
|
|
wave_operator: WaveFNO,
|
|
/// Source encoder for earthquake parametrization.
|
|
source_encoder: SourceEncoder,
|
|
/// Arrival predictor for travel time estimation.
|
|
arrival_predictor: ArrivalPredictor,
|
|
/// Attenuation model for ground motion prediction.
|
|
attenuation_model: AttenuationModel,
|
|
/// Simulation configuration.
|
|
config: SimulationConfig,
|
|
/// Warning configuration.
|
|
warning_config: WarningConfig,
|
|
/// RNG state for simulations.
|
|
rng_state: u64,
|
|
}
|
|
|
|
/// Encodes earthquake source parameters for neural network input.
|
|
#[derive(Debug)]
|
|
pub struct SourceEncoder {
|
|
/// Hidden dimension.
|
|
hidden_dim: usize,
|
|
/// Encoding weights.
|
|
weights: Vec<f32>,
|
|
}
|
|
|
|
impl SourceEncoder {
|
|
pub fn new(hidden_dim: usize) -> Self {
|
|
Self {
|
|
hidden_dim,
|
|
weights: vec![0.01; hidden_dim * 10], // 10 source parameters
|
|
}
|
|
}
|
|
|
|
/// Encode earthquake source to feature vector.
|
|
pub fn encode(&self, source: &EarthquakeSource) -> Vec<f32> {
|
|
let mut features = Vec::with_capacity(self.hidden_dim);
|
|
|
|
// Extract key source parameters
|
|
let lat = source.hypocenter.location.latitude as f32;
|
|
let lon = source.hypocenter.location.longitude as f32;
|
|
let depth = source.hypocenter.depth_km as f32;
|
|
let mag = source.magnitude as f32;
|
|
let strike = source.mechanism.strike as f32 / 360.0;
|
|
let dip = source.mechanism.dip as f32 / 90.0;
|
|
let rake = (source.mechanism.rake as f32 + 180.0) / 360.0;
|
|
|
|
// Simple linear encoding
|
|
for i in 0..self.hidden_dim {
|
|
let w_idx = i * 7;
|
|
let feature = self.weights[w_idx] * lat
|
|
+ self.weights[w_idx + 1] * lon
|
|
+ self.weights[w_idx + 2] * depth
|
|
+ self.weights[w_idx + 3] * mag
|
|
+ self.weights[w_idx + 4] * strike
|
|
+ self.weights[w_idx + 5] * dip
|
|
+ self.weights[w_idx + 6] * rake;
|
|
|
|
features.push(feature.tanh());
|
|
}
|
|
|
|
features
|
|
}
|
|
}
|
|
|
|
/// Predicts P and S wave arrival times.
|
|
#[derive(Debug)]
|
|
pub struct ArrivalPredictor {
|
|
/// Average P-wave velocity (km/s).
|
|
avg_vp: f64,
|
|
/// Average S-wave velocity (km/s).
|
|
avg_vs: f64,
|
|
}
|
|
|
|
impl ArrivalPredictor {
|
|
pub fn new() -> Self {
|
|
Self {
|
|
avg_vp: 6.0,
|
|
avg_vs: 3.5,
|
|
}
|
|
}
|
|
|
|
/// Update velocities from a velocity model.
|
|
pub fn from_velocity_model(model: &VelocityModel) -> Self {
|
|
let n = model.layers.len() as f64;
|
|
let avg_vp = model.layers.iter().map(|l| l.vp).sum::<f64>() / n;
|
|
let avg_vs = model.layers.iter().map(|l| l.vs).sum::<f64>() / n;
|
|
Self { avg_vp, avg_vs }
|
|
}
|
|
|
|
/// Predict P-wave arrival time.
|
|
pub fn predict_p_arrival(&self, distance_km: f64, depth_km: f64) -> f64 {
|
|
let path = (distance_km.powi(2) + depth_km.powi(2)).sqrt();
|
|
path / self.avg_vp
|
|
}
|
|
|
|
/// Predict S-wave arrival time.
|
|
pub fn predict_s_arrival(&self, distance_km: f64, depth_km: f64) -> f64 {
|
|
let path = (distance_km.powi(2) + depth_km.powi(2)).sqrt();
|
|
path / self.avg_vs
|
|
}
|
|
|
|
/// Predict S-P time (useful for distance estimation).
|
|
pub fn predict_sp_time(&self, distance_km: f64, depth_km: f64) -> f64 {
|
|
self.predict_s_arrival(distance_km, depth_km)
|
|
- self.predict_p_arrival(distance_km, depth_km)
|
|
}
|
|
|
|
/// Estimate distance from S-P time.
|
|
pub fn estimate_distance(&self, sp_time: f64) -> f64 {
|
|
// Simplified: assumes horizontal distance >> depth
|
|
sp_time / (1.0 / self.avg_vs - 1.0 / self.avg_vp)
|
|
}
|
|
}
|
|
|
|
impl Default for ArrivalPredictor {
|
|
fn default() -> Self {
|
|
Self::new()
|
|
}
|
|
}
|
|
|
|
impl SeismicAI {
|
|
/// Create a new SeismicAI system.
|
|
pub fn new(config: SimulationConfig) -> Self {
|
|
Self {
|
|
wave_operator: WaveFNO::new(config.fno_modes, config.hidden_dim, 4),
|
|
source_encoder: SourceEncoder::new(config.hidden_dim),
|
|
arrival_predictor: ArrivalPredictor::new(),
|
|
attenuation_model: AttenuationModel::nga_west2(),
|
|
config,
|
|
warning_config: WarningConfig::default(),
|
|
rng_state: 42,
|
|
}
|
|
}
|
|
|
|
/// Create with specific warning configuration.
|
|
pub fn with_warning_config(mut self, warning_config: WarningConfig) -> Self {
|
|
self.warning_config = warning_config;
|
|
self
|
|
}
|
|
|
|
/// Set the attenuation model.
|
|
pub fn with_attenuation_model(mut self, model: AttenuationModel) -> Self {
|
|
self.attenuation_model = model;
|
|
self
|
|
}
|
|
|
|
/// Simulate seismic wave propagation.
|
|
pub fn simulate(
|
|
&mut self,
|
|
source: &EarthquakeSource,
|
|
stations: &[StationConfig],
|
|
velocity_model: &VelocityModel,
|
|
) -> SimulationResult {
|
|
let start = std::time::Instant::now();
|
|
|
|
// Update arrival predictor with velocity model
|
|
self.arrival_predictor = ArrivalPredictor::from_velocity_model(velocity_model);
|
|
|
|
// Calculate grid dimensions
|
|
let (dx, dy, dz) = (self.config.dx, self.config.dx, self.config.dx);
|
|
let (lx, ly, lz) = self.config.domain_size;
|
|
let nx = (lx / dx) as usize;
|
|
let ny = (ly / dy) as usize;
|
|
let nz = (lz / dz) as usize;
|
|
|
|
// Initialize wave field
|
|
let mut wave_field = WaveField::new(nx, ny, nz);
|
|
|
|
// Inject source
|
|
self.wave_operator
|
|
.inject_source(&mut wave_field, source, &self.config, velocity_model);
|
|
|
|
// Propagate waves
|
|
let num_steps = (self.config.duration / self.config.dt) as usize;
|
|
let fields = self
|
|
.wave_operator
|
|
.propagate(&wave_field, self.config.dt, num_steps);
|
|
|
|
// Compute seismograms at stations
|
|
let seismograms =
|
|
self.wave_operator
|
|
.compute_seismograms(&fields, stations, &self.config, velocity_model);
|
|
|
|
// Predict ground motion at each station
|
|
let ground_motions = self.predict_ground_motion(source, stations, velocity_model);
|
|
|
|
// Generate warning
|
|
let warning = self.generate_warning(source, stations, velocity_model);
|
|
|
|
// Create snapshots (sample every N steps)
|
|
let snapshot_interval = num_steps / 10;
|
|
let snapshots: Vec<WaveFieldSnapshot> = fields
|
|
.iter()
|
|
.enumerate()
|
|
.filter(|(i, _)| i % snapshot_interval == 0)
|
|
.take(10)
|
|
.map(|(_, field)| self.create_snapshot(field))
|
|
.collect();
|
|
|
|
let computation_time_ms = start.elapsed().as_secs_f64() * 1000.0;
|
|
|
|
SimulationResult {
|
|
source: source.clone(),
|
|
ground_motions,
|
|
seismograms: Some(seismograms),
|
|
snapshots: Some(snapshots),
|
|
warning: Some(warning),
|
|
computation_time_ms,
|
|
}
|
|
}
|
|
|
|
/// Predict ground motion at stations using GMPE.
|
|
pub fn predict_ground_motion(
|
|
&self,
|
|
source: &EarthquakeSource,
|
|
stations: &[StationConfig],
|
|
velocity_model: &VelocityModel,
|
|
) -> Vec<(StationConfig, GroundMotion)> {
|
|
stations
|
|
.iter()
|
|
.map(|station| {
|
|
// Get base prediction from GMPE
|
|
let mut gm = self
|
|
.attenuation_model
|
|
.predict(source, station, velocity_model);
|
|
|
|
// Apply site effects
|
|
if self.config.include_site_effects {
|
|
let site_effects = SiteEffects::from_station(station);
|
|
gm = site_effects.apply(&gm);
|
|
}
|
|
|
|
(station.clone(), gm)
|
|
})
|
|
.collect()
|
|
}
|
|
|
|
/// Generate earthquake early warning.
|
|
pub fn generate_warning(
|
|
&mut self,
|
|
source: &EarthquakeSource,
|
|
stations: &[StationConfig],
|
|
velocity_model: &VelocityModel,
|
|
) -> EarthquakeWarning {
|
|
// Find stations that would detect the event
|
|
let detecting_stations: Vec<_> = stations
|
|
.iter()
|
|
.filter(|s| {
|
|
let dist = source.hypocenter.location.distance_km(&s.location);
|
|
dist < self.warning_config.alert_radius_km
|
|
})
|
|
.collect();
|
|
|
|
if detecting_stations.len() < self.warning_config.min_stations
|
|
|| source.magnitude < self.warning_config.min_magnitude
|
|
{
|
|
return EarthquakeWarning::default();
|
|
}
|
|
|
|
// Estimate magnitude (with uncertainty)
|
|
let estimated_magnitude = source.magnitude + (self.random() - 0.5) * 0.3;
|
|
|
|
// Estimate location (with uncertainty)
|
|
let estimated_location = GeoLocation::new(
|
|
source.hypocenter.location.latitude + (self.random() - 0.5) * 0.1,
|
|
source.hypocenter.location.longitude + (self.random() - 0.5) * 0.1,
|
|
);
|
|
|
|
let estimated_depth = source.hypocenter.depth_km + (self.random() - 0.5) * 5.0;
|
|
|
|
// Calculate warning time for a typical target location
|
|
let target_distance = 50.0; // km
|
|
let p_arrival = self
|
|
.arrival_predictor
|
|
.predict_p_arrival(target_distance, source.hypocenter.depth_km);
|
|
let s_arrival = self
|
|
.arrival_predictor
|
|
.predict_s_arrival(target_distance, source.hypocenter.depth_km);
|
|
|
|
// Time to shaking at target
|
|
let processing_delay = 3.0; // seconds for detection and processing
|
|
let time_to_shaking = (s_arrival - p_arrival - processing_delay).max(0.0);
|
|
|
|
// Predict ground motion at target
|
|
let target_station = StationConfig::new(
|
|
"TARGET",
|
|
source.hypocenter.location.latitude + 0.45,
|
|
source.hypocenter.location.longitude,
|
|
);
|
|
let expected_gm = self
|
|
.attenuation_model
|
|
.predict(source, &target_station, velocity_model);
|
|
|
|
// Determine alert level
|
|
let alert_level = if expected_gm.mmi >= 7.0 {
|
|
AlertLevel::Severe
|
|
} else if expected_gm.mmi >= 5.0 {
|
|
AlertLevel::Warning
|
|
} else if expected_gm.mmi >= 4.0 {
|
|
AlertLevel::Watch
|
|
} else if expected_gm.mmi >= 3.0 {
|
|
AlertLevel::Advisory
|
|
} else {
|
|
AlertLevel::None
|
|
};
|
|
|
|
// Calculate confidence based on number of detecting stations
|
|
let confidence = (detecting_stations.len() as f64 / 10.0).min(0.95);
|
|
|
|
EarthquakeWarning {
|
|
event_id: source.event_id.clone(),
|
|
alert_level,
|
|
estimated_magnitude,
|
|
estimated_location,
|
|
estimated_depth_km: estimated_depth,
|
|
detecting_stations: detecting_stations.len(),
|
|
time_to_shaking,
|
|
expected_ground_motion: expected_gm,
|
|
warning_time: p_arrival + processing_delay,
|
|
origin_time: 0.0,
|
|
confidence,
|
|
}
|
|
}
|
|
|
|
/// Create a wave field snapshot for visualization.
|
|
fn create_snapshot(&self, field: &WaveField) -> WaveFieldSnapshot {
|
|
let (nx, ny, _) = field.dimensions;
|
|
|
|
// Extract surface (z=0) displacement
|
|
let mut disp_x = vec![vec![0.0_f32; ny]; nx];
|
|
let mut disp_y = vec![vec![0.0_f32; ny]; nx];
|
|
let mut disp_z = vec![vec![0.0_f32; ny]; nx];
|
|
|
|
for i in 0..nx {
|
|
for j in 0..ny {
|
|
let (dx, dy, dz) = field.displacement_at(i, j, 0);
|
|
disp_x[i][j] = dx;
|
|
disp_y[i][j] = dy;
|
|
disp_z[i][j] = dz;
|
|
}
|
|
}
|
|
|
|
// Grid coordinates
|
|
let grid_x: Vec<f32> = (0..nx).map(|i| i as f32 * self.config.dx as f32).collect();
|
|
let grid_y: Vec<f32> = (0..ny).map(|j| j as f32 * self.config.dx as f32).collect();
|
|
|
|
WaveFieldSnapshot {
|
|
time: field.time,
|
|
displacement_x: disp_x,
|
|
displacement_y: disp_y,
|
|
displacement_z: disp_z,
|
|
grid_x,
|
|
grid_y,
|
|
}
|
|
}
|
|
|
|
/// 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
|
|
}
|
|
}
|
|
|
|
// ============================================================================
|
|
// Demo Entry Point
|
|
// ============================================================================
|
|
|
|
/// Run the SeismicAI demo.
|
|
pub fn run_demo() -> SimulationResult {
|
|
// Create demo scenario
|
|
let (source, stations, velocity_model, config) = sample_data::local_test_scenario();
|
|
|
|
// Create SeismicAI system
|
|
let mut seismic_ai = SeismicAI::new(config)
|
|
.with_warning_config(sample_data::urban_warning_config())
|
|
.with_attenuation_model(AttenuationModel::california());
|
|
|
|
// Run simulation
|
|
seismic_ai.simulate(&source, &stations, &velocity_model)
|
|
}
|
|
|
|
/// Run a major earthquake scenario.
|
|
pub fn run_major_scenario() -> SimulationResult {
|
|
let (source, stations, velocity_model, config) = sample_data::hayward_scenario();
|
|
|
|
let mut seismic_ai =
|
|
SeismicAI::new(config).with_warning_config(sample_data::urban_warning_config());
|
|
|
|
seismic_ai.simulate(&source, &stations, &velocity_model)
|
|
}
|
|
|
|
// ============================================================================
|
|
// Tests
|
|
// ============================================================================
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn test_seismic_ai_creation() {
|
|
let config = SimulationConfig::default();
|
|
let seismic_ai = SeismicAI::new(config);
|
|
assert_eq!(seismic_ai.arrival_predictor.avg_vp, 6.0);
|
|
}
|
|
|
|
#[test]
|
|
fn test_source_encoder() {
|
|
let encoder = SourceEncoder::new(32);
|
|
let source = sample_data::local_earthquake();
|
|
let features = encoder.encode(&source);
|
|
|
|
assert_eq!(features.len(), 32);
|
|
// All features should be in tanh range [-1, 1]
|
|
for f in &features {
|
|
assert!(*f >= -1.0 && *f <= 1.0);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn test_arrival_predictor() {
|
|
let predictor = ArrivalPredictor::new();
|
|
|
|
let p_time = predictor.predict_p_arrival(60.0, 10.0);
|
|
let s_time = predictor.predict_s_arrival(60.0, 10.0);
|
|
|
|
// S should arrive after P
|
|
assert!(s_time > p_time);
|
|
|
|
// S-P time increases with distance
|
|
let sp_near = predictor.predict_sp_time(30.0, 10.0);
|
|
let sp_far = predictor.predict_sp_time(100.0, 10.0);
|
|
assert!(sp_far > sp_near);
|
|
}
|
|
|
|
#[test]
|
|
fn test_arrival_predictor_from_model() {
|
|
let model = sample_data::california_velocity_model();
|
|
let predictor = ArrivalPredictor::from_velocity_model(&model);
|
|
|
|
// Should use model velocities
|
|
assert!(predictor.avg_vp > 4.0 && predictor.avg_vp < 8.0);
|
|
assert!(predictor.avg_vs > 2.0 && predictor.avg_vs < 5.0);
|
|
}
|
|
|
|
#[test]
|
|
fn test_distance_estimation() {
|
|
let predictor = ArrivalPredictor::new();
|
|
|
|
// Round-trip test
|
|
let true_distance = 80.0;
|
|
let sp_time = predictor.predict_sp_time(true_distance, 10.0);
|
|
let estimated = predictor.estimate_distance(sp_time);
|
|
|
|
// Should be approximately correct (ignoring depth effects)
|
|
assert!((estimated - true_distance).abs() < 20.0);
|
|
}
|
|
|
|
#[test]
|
|
fn test_ground_motion_prediction() {
|
|
let config = sample_data::fast_config();
|
|
let seismic_ai = SeismicAI::new(config);
|
|
|
|
let source = sample_data::local_earthquake();
|
|
let stations = sample_data::sparse_network();
|
|
let velocity_model = sample_data::generic_velocity_model();
|
|
|
|
let gm = seismic_ai.predict_ground_motion(&source, &stations, &velocity_model);
|
|
|
|
assert_eq!(gm.len(), stations.len());
|
|
|
|
for (station, motion) in &gm {
|
|
assert!(motion.pga > 0.0);
|
|
assert!(motion.p_arrival_time > 0.0);
|
|
assert!(motion.s_arrival_time > motion.p_arrival_time);
|
|
assert!(motion.mmi >= 1.0);
|
|
// Station should be returned
|
|
assert!(!station.code.is_empty());
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn test_warning_generation() {
|
|
let config = sample_data::fast_config();
|
|
let mut seismic_ai = SeismicAI::new(config).with_warning_config(WarningConfig {
|
|
min_magnitude: 3.0,
|
|
min_stations: 2,
|
|
..Default::default()
|
|
});
|
|
|
|
let source = sample_data::local_earthquake();
|
|
let stations = sample_data::sparse_network();
|
|
let velocity_model = sample_data::generic_velocity_model();
|
|
|
|
let warning = seismic_ai.generate_warning(&source, &stations, &velocity_model);
|
|
|
|
// Should generate a warning for this magnitude
|
|
assert!(!warning.event_id.is_empty());
|
|
assert!(warning.detecting_stations >= 2);
|
|
assert!(warning.confidence > 0.0);
|
|
}
|
|
|
|
#[test]
|
|
fn test_no_warning_for_small_event() {
|
|
let config = sample_data::fast_config();
|
|
let mut seismic_ai = SeismicAI::new(config).with_warning_config(WarningConfig {
|
|
min_magnitude: 5.0, // Higher threshold
|
|
..Default::default()
|
|
});
|
|
|
|
let mut source = sample_data::local_earthquake();
|
|
source.magnitude = 3.0; // Below threshold
|
|
|
|
let stations = sample_data::sparse_network();
|
|
let velocity_model = sample_data::generic_velocity_model();
|
|
|
|
let warning = seismic_ai.generate_warning(&source, &stations, &velocity_model);
|
|
|
|
// Should not generate alert
|
|
assert_eq!(warning.alert_level, AlertLevel::None);
|
|
}
|
|
|
|
#[test]
|
|
fn test_simulation() {
|
|
let (source, stations, velocity_model, config) = sample_data::local_test_scenario();
|
|
let mut seismic_ai = SeismicAI::new(config);
|
|
|
|
let result = seismic_ai.simulate(&source, &stations, &velocity_model);
|
|
|
|
// Check result completeness
|
|
assert_eq!(result.ground_motions.len(), stations.len());
|
|
assert!(result.seismograms.is_some());
|
|
assert!(result.snapshots.is_some());
|
|
assert!(result.warning.is_some());
|
|
assert!(result.computation_time_ms > 0.0);
|
|
}
|
|
|
|
#[test]
|
|
fn test_seismogram_output() {
|
|
let (source, stations, velocity_model, mut config) = sample_data::local_test_scenario();
|
|
config.duration = 10.0; // Shorter for testing
|
|
let mut seismic_ai = SeismicAI::new(config);
|
|
|
|
let result = seismic_ai.simulate(&source, &stations, &velocity_model);
|
|
|
|
let seismograms = result.seismograms.unwrap();
|
|
assert_eq!(seismograms.len(), stations.len());
|
|
|
|
for seis in &seismograms {
|
|
assert!(!seis.station_code.is_empty());
|
|
assert!(!seis.time.is_empty());
|
|
assert_eq!(seis.east.len(), seis.time.len());
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn test_wave_field_snapshot() {
|
|
let (source, stations, velocity_model, mut config) = sample_data::local_test_scenario();
|
|
config.duration = 5.0;
|
|
let mut seismic_ai = SeismicAI::new(config);
|
|
|
|
let result = seismic_ai.simulate(&source, &stations, &velocity_model);
|
|
|
|
let snapshots = result.snapshots.unwrap();
|
|
assert!(!snapshots.is_empty());
|
|
|
|
for snap in &snapshots {
|
|
assert!(!snap.displacement_x.is_empty());
|
|
assert!(!snap.grid_x.is_empty());
|
|
assert!(snap.time >= 0.0);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn test_run_demo() {
|
|
let result = run_demo();
|
|
assert!(result.computation_time_ms > 0.0);
|
|
assert!(!result.ground_motions.is_empty());
|
|
}
|
|
|
|
#[test]
|
|
fn test_attenuation_model_config() {
|
|
let config = sample_data::fast_config();
|
|
let seismic_ai = SeismicAI::new(config).with_attenuation_model(AttenuationModel::japan());
|
|
|
|
assert_eq!(seismic_ai.attenuation_model.name(), "Japan");
|
|
}
|
|
|
|
#[test]
|
|
fn test_warning_config() {
|
|
let config = sample_data::fast_config();
|
|
let warning_config = WarningConfig {
|
|
min_magnitude: 4.0,
|
|
min_stations: 5,
|
|
..Default::default()
|
|
};
|
|
let seismic_ai = SeismicAI::new(config).with_warning_config(warning_config);
|
|
|
|
assert_eq!(seismic_ai.warning_config.min_magnitude, 4.0);
|
|
assert_eq!(seismic_ai.warning_config.min_stations, 5);
|
|
}
|
|
|
|
#[test]
|
|
fn test_major_scenario() {
|
|
// Just test it runs without panicking
|
|
let (source, stations, velocity_model, _) = sample_data::hayward_scenario();
|
|
assert!(source.magnitude >= 6.5);
|
|
assert!(stations.len() >= 50);
|
|
assert!(!velocity_model.layers.is_empty());
|
|
}
|
|
}
|