///|
/// A station-local trigger with an explicit station identifier.
pub struct StationTrigger {
station : String
trigger : Trigger
peak_amplitude : Double
quality_score : Double
}
///|
/// A network-associated event hypothesis.
pub struct NetworkEvent {
origin_time : Double
station_count : Int
duration : Double
magnitude : Double
stations : Array[String]
confidence : Double
}
///|
/// Convert local detector results into station-tagged triggers.
pub fn station_triggers(
station : String,
trace : Trace,
config : EventDetectionConfig,
) -> Array[StationTrigger] {
let ratio = classic_sta_lta(
trace.data,
config.sta_samples,
config.lta_samples,
)
let triggers = trigger_onset(
ratio,
config.onset_threshold,
config.offset_threshold,
config.dt,
config.start_time,
)
let result = Array::new()
let quality = assess_quality(trace, 1.0e6, 0.0000001)
for trigger in triggers {
let (_, peak) = event_peak(trace.data, trigger.on_index, trigger.off_index)
result.push({
station,
trigger,
peak_amplitude: peak,
quality_score: quality.score,
})
}
result
}
///|
/// Associate triggers whose onset times fall within a tolerance window.
pub fn associate_station_triggers(
triggers : Array[StationTrigger],
tolerance_seconds : Double,
) -> Array[NetworkEvent] {
let result = Array::new()
let tolerance = if tolerance_seconds < 0.0 { 0.0 } else { tolerance_seconds }
let used = Array::make(triggers.length(), false)
for seed = 0; seed < triggers.length(); seed = seed + 1 {
if used[seed] {
continue
}
used[seed] = true
let origin = triggers[seed].trigger.on_time
let stations = Array::new()
stations.push(triggers[seed].station)
let mut total_peak = triggers[seed].peak_amplitude
let mut total_quality = triggers[seed].quality_score
let mut earliest = triggers[seed].trigger.on_time
let mut latest = triggers[seed].trigger.off_time
for candidate = seed + 1
candidate < triggers.length()
candidate = candidate + 1 {
if !used[candidate] &&
(triggers[candidate].trigger.on_time - origin).abs() <= tolerance {
used[candidate] = true
stations.push(triggers[candidate].station)
total_peak += triggers[candidate].peak_amplitude
total_quality += triggers[candidate].quality_score
if triggers[candidate].trigger.on_time < earliest {
earliest = triggers[candidate].trigger.on_time
}
if triggers[candidate].trigger.off_time > latest {
latest = triggers[candidate].trigger.off_time
}
}
}
let count = stations.length()
result.push({
origin_time: earliest,
station_count: count,
duration: latest - earliest,
magnitude: if count == 0 {
0.0
} else {
@math.log10(total_peak / count.to_double() + 1.0)
},
stations,
confidence: if count == 0 {
0.0
} else {
total_quality / count.to_double()
},
})
}
result
}
///|
/// Return only network events seen by at least a station quorum.
pub fn quorum_events(
events : Array[NetworkEvent],
minimum_stations : Int,
) -> Array[NetworkEvent] {
let quorum = if minimum_stations < 1 { 1 } else { minimum_stations }
events.filter(event => event.station_count >= quorum)
}
///|
/// Return the most confident network event.
pub fn strongest_network_event(events : Array[NetworkEvent]) -> NetworkEvent? {
if events.is_empty() {
return None
}
let mut best = events[0]
for index = 1; index < events.length(); index = index + 1 {
if events[index].confidence > best.confidence {
best = events[index]
}
}
Some(best)
}
///|
/// Estimate a network magnitude as the median station local magnitude.
pub fn network_local_magnitude(
traces : Array[Trace],
distance_km : Double,
) -> Result[Double, String] {
let values = Array::new()
for trace in traces {
match local_magnitude(wood_anderson_peak(trace), distance_km) {
Ok(value) => values.push(value)
Err(_) => ()
}
}
if values.is_empty() {
Err("No trace produced a valid local magnitude")
} else {
Ok(median_samples(values))
}
}