///|
/// Reliability prior for one station in network association.
pub struct StationReliability {
station : String
detections : Int
confirmations : Int
false_alarms : Int
mean_quality : Double
reliability : Double
}
///|
/// A cluster of station events before final catalog insertion.
pub struct EventCluster {
member_indexes : Array[Int]
origin_time : Double
magnitude : Double
station_count : Int
coherence : Double
}
///|
/// Network event consensus after reliability weighting.
pub struct NetworkConsensus {
origin_time : Double
magnitude : Double
station_count : Int
confidence : Double
members : Array[NetworkEvent]
supporting_stations : Array[String]
rejected_members : Int
}
///|
/// Controls temporal clustering and reliability thresholds.
pub struct NetworkConsensusConfig {
time_tolerance : Double
minimum_stations : Int
minimum_confidence : Double
false_alarm_penalty : Double
}
///|
/// Return conservative consensus defaults.
pub fn default_network_consensus_config() -> NetworkConsensusConfig {
{
time_tolerance: 3.0,
minimum_stations: 2,
minimum_confidence: 0.45,
false_alarm_penalty: 0.25,
}
}
///|
/// Compute station reliability from detection outcomes.
pub fn station_reliability(
station : String,
detections : Int,
confirmations : Int,
false_alarms : Int,
mean_quality : Double,
) -> StationReliability {
let total = detections.max(0).to_double()
let precision = if total <= 0.0 {
0.0
} else {
(confirmations.max(0).to_double() / total).min(1.0)
}
let penalty = if total <= 0.0 {
0.0
} else {
false_alarms.max(0).to_double() / total
}
let reliability = (0.55 * precision +
0.45 * mean_quality.max(0.0).min(1.0) -
penalty * 0.35)
.max(0.0)
.min(1.0)
{
station,
detections,
confirmations,
false_alarms,
mean_quality,
reliability,
}
}
///|
/// Find temporal event clusters with single-linkage grouping.
pub fn cluster_network_events(
events : Array[NetworkEvent],
tolerance_seconds : Double,
) -> Array[EventCluster] {
let result : Array[EventCluster] = Array::new()
let used = Array::make(events.length(), false)
let tolerance = tolerance_seconds.abs()
for seed = 0; seed < events.length(); seed = seed + 1 {
if used[seed] {
continue
}
let members = Array::new()
let stations = Array::new()
let mut total_time = 0.0
let mut total_magnitude = 0.0
let mut total_confidence = 0.0
let anchor = events[seed].origin_time
for index = seed; index < events.length(); index = index + 1 {
if !used[index] && (events[index].origin_time - anchor).abs() <= tolerance {
used[index] = true
members.push(index)
total_time += events[index].origin_time * events[index].confidence
total_magnitude += events[index].magnitude * events[index].confidence
total_confidence += events[index].confidence
for station in events[index].stations {
if !stations.contains(station) {
stations.push(station)
}
}
}
}
result.push({
member_indexes: members,
origin_time: if total_confidence <= 0.0 {
anchor
} else {
total_time / total_confidence
},
magnitude: if total_confidence <= 0.0 {
0.0
} else {
total_magnitude / total_confidence
},
station_count: stations.length(),
coherence: if events.is_empty() {
0.0
} else {
members.length().to_double() / events.length().to_double()
},
})
}
result
}
///|
/// Merge one cluster into a reliability-weighted consensus event.
pub fn consensus_from_cluster(
cluster : EventCluster,
events : Array[NetworkEvent],
reliabilities : Array[StationReliability],
config : NetworkConsensusConfig,
) -> NetworkConsensus {
let members : Array[NetworkEvent] = Array::new()
let stations : Array[String] = Array::new()
let mut time_total = 0.0
let mut magnitude_total = 0.0
let mut weight_total = 0.0
let mut rejected = 0
for index in cluster.member_indexes {
if index < 0 || index >= events.length() {
rejected += 1
continue
}
let event = events[index]
let mut station_weight = 0.0
for station in event.stations {
let mut found = 0.75
for reliability in reliabilities {
if reliability.station == station {
found = reliability.reliability
}
}
station_weight += found
if !stations.contains(station) {
stations.push(station)
}
}
let weight = station_weight.max(event.confidence).max(1.0e-6)
if event.confidence >= config.minimum_confidence {
members.push(event)
time_total += event.origin_time * weight
magnitude_total += event.magnitude * weight
weight_total += weight
} else {
rejected += 1
}
}
let origin_time = if weight_total <= 0.0 {
cluster.origin_time
} else {
time_total / weight_total
}
let magnitude = if weight_total <= 0.0 {
cluster.magnitude
} else {
magnitude_total / weight_total
}
let confidence = (cluster.coherence *
(stations.length().to_double() / config.minimum_stations.max(1).to_double()).min(
1.0,
) *
(weight_total / members.length().max(1).to_double()).min(1.0)).min(1.0)
{
origin_time,
magnitude,
station_count: stations.length(),
confidence,
members,
supporting_stations: stations,
rejected_members: rejected,
}
}
///|
/// Select the strongest consensus from all temporal clusters.
pub fn strongest_consensus(
events : Array[NetworkEvent],
reliabilities : Array[StationReliability],
config : NetworkConsensusConfig,
) -> NetworkConsensus? {
let clusters = cluster_network_events(events, config.time_tolerance)
let mut best : NetworkConsensus? = None
for cluster in clusters {
if cluster.station_count < config.minimum_stations {
continue
}
let candidate = consensus_from_cluster(
cluster, events, reliabilities, config,
)
match best {
None => best = Some(candidate)
Some(current) =>
if candidate.confidence > current.confidence {
best = Some(candidate)
}
}
}
best
}
///|
/// Return the stability of a consensus origin-time estimate.
pub fn consensus_time_spread(consensus : NetworkConsensus) -> Double {
if consensus.members.is_empty() {
0.0
} else {
standard_deviation(consensus.members.map(event => event.origin_time))
}
}
///|
/// Return a human-readable consensus quality band.
pub fn consensus_quality(consensus : NetworkConsensus) -> String {
if consensus.confidence >= 0.8 {
"high"
} else if consensus.confidence >= 0.55 {
"medium"
} else {
"low"
}
}
///|
/// Serialize network consensus information.
pub fn network_consensus_text(consensus : NetworkConsensus) -> String {
"origin=" +
consensus.origin_time.to_string() +
" magnitude=" +
consensus.magnitude.to_string() +
" stations=" +
consensus.station_count.to_string() +
" confidence=" +
consensus.confidence.to_string() +
" spread=" +
consensus_time_spread(consensus).to_string() +
" rejected=" +
consensus.rejected_members.to_string() +
" quality=" +
consensus_quality(consensus)
}