///|
/// Coverage and coherence measures for a station network event.
pub struct NetworkEventMetrics {
station_count : Int
unique_station_count : Int
mean_distance_km : Double
maximum_distance_km : Double
origin_spread_seconds : Double
amplitude_spread : Double
magnitude_consensus : Double
network_score : Double
}
///|
/// A station amplitude observation attached to an event hypothesis.
pub struct StationAmplitudeObservation {
station : String
distance_km : Double
amplitude : Double
local_magnitude : Double
confidence : Double
}
///|
/// Construct a station amplitude observation.
pub fn StationAmplitudeObservation::new(
station : String,
distance_km : Double,
amplitude : Double,
local_magnitude : Double,
confidence : Double,
) -> StationAmplitudeObservation {
{ station, distance_km, amplitude, local_magnitude, confidence }
}
///|
/// Compute an amplitude-weighted magnitude consensus.
pub fn amplitude_magnitude_consensus(
observations : Array[StationAmplitudeObservation],
) -> Double {
let values = observations.map(item => item.local_magnitude)
let weights = observations.map(item => item.confidence.max(0.0))
let mut total = 0.0
let mut weight = 0.0
for index = 0; index < values.length(); index = index + 1 {
total += values[index] * weights[index]
weight += weights[index]
}
if weight <= 0.0 {
median_samples(values)
} else {
total / weight
}
}
///|
/// Return unique station names in first-seen order.
pub fn unique_station_names(
observations : Array[StationAmplitudeObservation],
) -> Array[String] {
let result : Array[String] = Array::new()
for observation in observations {
if !result.contains(observation.station) {
result.push(observation.station)
}
}
result
}
///|
/// Compute spatial coverage and amplitude spread.
pub fn summarize_station_amplitudes(
observations : Array[StationAmplitudeObservation],
) -> Array[Double] {
let distances = observations.map(item => item.distance_km)
let amplitudes = observations.map(item => item.amplitude)
[
mean_samples(distances),
maximum_sample(distances),
standard_deviation(amplitudes),
amplitude_magnitude_consensus(observations),
]
}
///|
/// Compute event metrics from network events and station observations.
pub fn network_event_metrics(
events : Array[NetworkEvent],
observations : Array[StationAmplitudeObservation],
) -> NetworkEventMetrics {
let unique = unique_station_names(observations)
let coverage = summarize_station_amplitudes(observations)
let origins = events.map(event => event.origin_time)
let magnitudes = events.map(event => event.magnitude)
let spread = standard_deviation(origins)
let amplitude_spread = coverage[2]
let station_score = (unique.length().to_double() / 4.0).min(1.0)
let coherence_score = @math.exp(-spread / 2.0)
let amplitude_score = @math.exp(
-amplitude_spread /
(mean_samples(observations.map(item => item.amplitude.abs())) + 1.0e-12),
)
{
station_count: observations.length(),
unique_station_count: unique.length(),
mean_distance_km: coverage[0],
maximum_distance_km: coverage[1],
origin_spread_seconds: spread,
amplitude_spread,
magnitude_consensus: if magnitudes.is_empty() {
0.0
} else {
median_samples(magnitudes)
},
network_score: (0.45 * station_score +
0.35 * coherence_score +
0.2 * amplitude_score).min(1.0),
}
}
///|
/// Calculate expected station count from a circular network radius.
pub fn expected_station_coverage(
stations : Array[StationPosition],
source : StationPosition,
radius_km : Double,
) -> Double {
if stations.is_empty() {
0.0
} else {
stations
.filter(station => surface_distance_km(source, station) <= radius_km.abs())
.length()
.to_double() /
stations.length().to_double()
}
}
///|
/// Estimate network detection probability from station quality scores.
pub fn network_detection_probability(qualities : Array[Double]) -> Double {
if qualities.is_empty() {
0.0
} else {
let mut miss_probability = 1.0
for quality in qualities {
miss_probability *= 1.0 - quality.max(0.0).min(1.0)
}
1.0 - miss_probability
}
}
///|
/// Format network metrics for the event report.
pub fn network_event_metrics_text(metrics : NetworkEventMetrics) -> String {
"stations=" +
metrics.station_count.to_string() +
" unique=" +
metrics.unique_station_count.to_string() +
" mean_distance=" +
metrics.mean_distance_km.to_string() +
" max_distance=" +
metrics.maximum_distance_km.to_string() +
" origin_spread=" +
metrics.origin_spread_seconds.to_string() +
" amplitude_spread=" +
metrics.amplitude_spread.to_string() +
" magnitude=" +
metrics.magnitude_consensus.to_string() +
" score=" +
metrics.network_score.to_string()
}