///|
/// Parameters for a deterministic earthquake scenario.
pub struct ScenarioConfig {
origin_time : Double
latitude : Double
longitude : Double
depth_km : Double
magnitude : Double
sample_rate : Double
duration_seconds : Double
p_frequency : Double
noise_level : Double
seed : Int
}
///|
/// A simulated station channel and its theoretical arrivals.
pub struct ScenarioStationResult {
station : String
trace : Trace
p_arrival : Double
s_arrival : Double
distance_km : Double
pga : Double
snr_db : Double
}
///|
/// Complete deterministic scenario output.
pub struct ScenarioResult {
config : ScenarioConfig
stations : Array[ScenarioStationResult]
event : NetworkEvent
catalog_entry : CatalogEntry
}
///|
/// Return a realistic small-network scenario configuration.
pub fn default_scenario_config() -> ScenarioConfig {
{
origin_time: 0.0,
latitude: 30.0,
longitude: 120.0,
depth_km: 8.0,
magnitude: 3.2,
sample_rate: 100.0,
duration_seconds: 60.0,
p_frequency: 8.0,
noise_level: 0.01,
seed: 20260819,
}
}
///|
/// Convert magnitude to a deterministic displacement-like amplitude.
pub fn scenario_amplitude(magnitude : Double, distance_km : Double) -> Double {
let geometric = @math.pow(1.0 + distance_km, 0.75)
@math.pow(10.0, magnitude * 0.5 - 2.0) / geometric
}
///|
/// Generate one station waveform from a source scenario.
pub fn simulate_scenario_station(
config : ScenarioConfig,
station : StationPosition,
station_index : Int,
) -> ScenarioStationResult {
let source = StationPosition::new(
"source",
config.latitude,
config.longitude,
0.0,
)
let distance = surface_distance_km(source, station)
let path = (distance * distance + config.depth_km * config.depth_km).sqrt()
let p_arrival = config.origin_time + path / 6.0
let s_arrival = config.origin_time + path / 3.5
let length = (config.duration_seconds * config.sample_rate).to_int().max(1)
let onset = p_arrival - config.origin_time
let amplitude = scenario_amplitude(config.magnitude, distance)
let signal = synthetic_event_waveform(
length,
config.sample_rate,
config.p_frequency,
onset,
amplitude,
config.noise_level,
config.seed + station_index * 31,
)
let trace = Trace::new(
station.station,
"HHZ",
config.sample_rate,
config.origin_time,
signal,
)
let baseline = slice_samples(
trace.data,
0,
(onset * config.sample_rate).to_int().max(1).min(trace.npts()),
)
let snr = arrays_snr_db(baseline, trace.data)
{
station: station.station,
trace,
p_arrival,
s_arrival,
distance_km: distance,
pga: absolute_peak(trace.data),
snr_db: snr,
}
}
///|
/// Simulate an event across a station array.
pub fn simulate_scenario(
config : ScenarioConfig,
stations : Array[StationPosition],
) -> ScenarioResult {
let results = Array::new(capacity=stations.length())
let network_stations = Array::new()
let mut total_peak = 0.0
let mut total_quality = 0.0
let mut earliest = config.origin_time + config.duration_seconds
for index = 0; index < stations.length(); index = index + 1 {
let simulated = simulate_scenario_station(config, stations[index], index)
results.push(simulated)
network_stations.push(simulated.station)
total_peak += simulated.pga
total_quality += (simulated.snr_db + 10.0).max(0.0).min(40.0) / 40.0
earliest = earliest.min(simulated.p_arrival)
}
let count = results.length()
let event = {
origin_time: earliest,
station_count: count,
duration: config.duration_seconds,
magnitude: @math.log10(1.0 + total_peak),
stations: network_stations,
confidence: if count == 0 {
0.0
} else {
total_quality / count.to_double()
},
}
let catalog_entry = {
id: 0,
origin_time: config.origin_time,
latitude: config.latitude,
longitude: config.longitude,
depth_km: config.depth_km,
magnitude: config.magnitude,
station_count: count,
confidence: event.confidence,
}
{ config, stations: results, event, catalog_entry }
}
///|
/// Compute theoretical P/S arrival separation at one station.
pub fn scenario_phase_separation(result : ScenarioStationResult) -> Double {
result.s_arrival - result.p_arrival
}
///|
/// Return a station list for a regular local array.
pub fn regular_scenario_stations(
reference : StationPosition,
count : Int,
spacing_km : Double,
) -> Array[StationPosition] {
let result = Array::new()
for index = 0; index < count.max(0); index = index + 1 {
let offset = index.to_double() - (count - 1).to_double() / 2.0
result.push(
StationPosition::new(
"SC" + index.to_string(),
reference.latitude + offset * spacing_km / 111.32,
reference.longitude,
reference.elevation_km,
),
)
}
result
}
///|
/// Convert scenario station results to a quality report set.
pub fn scenario_quality_reports(
result : ScenarioResult,
) -> Array[StationQualityReport] {
result.stations.map(station => {
station_quality_report(
station.trace,
station.trace.npts(),
1.0e300,
1.0e-12,
station.trace.data,
)
})
}
///|
/// Format scenario headline information.
pub fn scenario_result_text(result : ScenarioResult) -> String {
"origin=" +
result.config.origin_time.to_string() +
" magnitude=" +
result.config.magnitude.to_string() +
" stations=" +
result.stations.length().to_string() +
" event_confidence=" +
result.event.confidence.to_string() +
" depth=" +
result.config.depth_km.to_string()
}