///|
/// Configuration for a multi-characteristic arrival picker.
pub struct AdvancedPickerConfig {
amplitude_threshold : Double
energy_threshold : Double
kurtosis_threshold : Double
aic_window : Int
characteristic_window : Int
agreement_seconds : Double
minimum_confidence : Double
}
///|
/// A characteristic function summary used for picker diagnostics.
pub struct PickerCharacteristicSummary {
name : String
peak_index : Int
peak_time : Double
peak_value : Double
baseline : Double
contrast : Double
confidence : Double
}
///|
/// A consensus arrival assembled from multiple independent methods.
pub struct ConsensusPick {
index : Int
time : Double
method_count : Int
confidence : Double
amplitude_pick : ArrivalPick?
energy_pick : ArrivalPick?
aic_pick : ArrivalPick?
kurtosis_pick : ArrivalPick?
}
///|
/// Picker diagnostics for a trace.
pub struct PickerDiagnostics {
trace_station : String
sample_count : Int
amplitude : PickerCharacteristicSummary
energy : PickerCharacteristicSummary
aic : PickerCharacteristicSummary
kurtosis : PickerCharacteristicSummary
consensus : ConsensusPick?
}
///|
/// Return a conservative multi-method picker configuration.
pub fn default_advanced_picker_config() -> AdvancedPickerConfig {
{
amplitude_threshold: 3.0,
energy_threshold: 1.5,
kurtosis_threshold: 2.0,
aic_window: 8,
characteristic_window: 16,
agreement_seconds: 0.5,
minimum_confidence: 0.45,
}
}
///|
/// Find the peak and baseline contrast of a characteristic function.
pub fn summarize_picker_characteristic(
name : String,
characteristic : Array[Double],
trace : Trace,
baseline_samples : Int,
) -> PickerCharacteristicSummary {
if characteristic.is_empty() {
return {
name,
peak_index: -1,
peak_time: trace.start_time,
peak_value: 0.0,
baseline: 0.0,
contrast: 0.0,
confidence: 0.0,
}
}
let baseline = mean_samples(
slice_samples(
characteristic,
0,
baseline_samples.max(1).min(characteristic.length()),
),
)
let mut peak_index = 0
for index = 1; index < characteristic.length(); index = index + 1 {
if characteristic[index] > characteristic[peak_index] {
peak_index = index
}
}
let peak_value = characteristic[peak_index]
let contrast = peak_value - baseline
let confidence = if peak_value.abs() <= 1.0e-12 {
0.0
} else {
(contrast / peak_value.abs()).max(0.0).min(1.0)
}
{
name,
peak_index,
peak_time: sample_to_time(trace, peak_index),
peak_value,
baseline,
contrast,
confidence,
}
}
///|
/// Calculate normalized characteristic functions for the primary methods.
pub fn picker_characteristics(
trace : Trace,
config : AdvancedPickerConfig,
) -> Array[Array[Double]] {
let amplitude = amplitude_envelope(trace.data)
let energy = energy_ratio(
trace.data,
config.characteristic_window.max(1),
config.characteristic_window.max(1) * 4,
)
let kurtosis = running_kurtosis(trace.data, config.characteristic_window)
let aic = aic_characteristic(trace.data, config.aic_window)
[amplitude, energy, aic, kurtosis]
}
///|
/// Pick the strongest characteristic after applying a quiet pre-event period.
pub fn pick_from_characteristic(
trace : Trace,
characteristic : Array[Double],
threshold : Double,
strategy : PickMethod,
quiet_samples : Int,
) -> ArrivalPick? {
if characteristic.is_empty() {
return None
}
let start = quiet_samples.max(0).min(characteristic.length())
let suffix = slice_samples(characteristic, start, characteristic.length())
match
pick_characteristic(
suffix,
trace_slice(trace, start, trace.npts()),
threshold,
strategy,
) {
None => None
Some(pick) =>
Some({
index: pick.index + start,
time: sample_to_time(trace, pick.index + start),
strategy: pick.strategy,
confidence: pick.confidence,
characteristic_value: pick.characteristic_value,
})
}
}
///|
/// Combine picks that fall inside an agreement window.
pub fn consensus_from_picks(
trace : Trace,
picks : Array[ArrivalPick],
agreement_seconds : Double,
minimum_confidence : Double,
) -> ConsensusPick? {
let candidates = picks.filter(pick => pick.confidence >= minimum_confidence)
if candidates.is_empty() {
return None
}
let tolerance = agreement_seconds.abs()
let mut best_index = 0
let mut best_count = 0
let mut best_confidence = 0.0
for seed = 0; seed < candidates.length(); seed = seed + 1 {
let mut count = 0
let mut total_confidence = 0.0
for pick in candidates {
if (pick.time - candidates[seed].time).abs() <= tolerance {
count += 1
total_confidence += pick.confidence
}
}
if count > best_count ||
(count == best_count && total_confidence > best_confidence) {
best_index = seed
best_count = count
best_confidence = total_confidence
}
}
let selected = candidates[best_index]
if best_count == 0 {
None
} else {
let mut index_total = 0.0
for pick in candidates {
if (pick.time - selected.time).abs() <= tolerance {
index_total += pick.index.to_double() * pick.confidence
}
}
let index = (index_total / best_confidence.max(1.0e-12))
.round()
.to_int()
.max(0)
.min(trace.npts().max(1) - 1)
Some({
index,
time: sample_to_time(trace, index),
method_count: best_count,
confidence: (best_confidence / candidates.length().to_double()).min(1.0),
amplitude_pick: find_consensus_pick(
picks,
selected,
tolerance,
AmplitudeThreshold,
),
energy_pick: find_consensus_pick(picks, selected, tolerance, EnergyRatio),
aic_pick: find_consensus_pick(
picks,
selected,
tolerance,
AkaikeInformationCriterion,
),
kurtosis_pick: find_consensus_pick(
picks,
selected,
tolerance,
KurtosisChange,
),
})
}
}
///|
/// Find one pick of a requested method in an agreement cluster.
fn find_consensus_pick(
picks : Array[ArrivalPick],
selected : ArrivalPick,
tolerance : Double,
strategy : PickMethod,
) -> ArrivalPick? {
for pick in picks {
if same_pick_strategy(pick.strategy, strategy) &&
(pick.time - selected.time).abs() <= tolerance {
return Some(pick)
}
}
None
}
///|
/// Compare picker strategy constructors without requiring enum equality.
fn same_pick_strategy(left : PickMethod, right : PickMethod) -> Bool {
match (left, right) {
(AmplitudeThreshold, AmplitudeThreshold) => true
(EnergyRatio, EnergyRatio) => true
(AkaikeInformationCriterion, AkaikeInformationCriterion) => true
(KurtosisChange, KurtosisChange) => true
_ => false
}
}
///|
/// Run all four methods and construct a consensus pick.
pub fn advanced_pick(
trace : Trace,
config : AdvancedPickerConfig,
) -> ConsensusPick? {
let amplitude = pick_from_characteristic(
trace,
amplitude_envelope(trace.data),
config.amplitude_threshold,
AmplitudeThreshold,
config.characteristic_window,
)
let energy = pick_from_characteristic(
trace,
energy_ratio(
trace.data,
config.characteristic_window.max(1),
config.characteristic_window.max(1) * 4,
),
config.energy_threshold,
EnergyRatio,
config.characteristic_window,
)
let aic = pick_from_characteristic(
trace,
aic_characteristic(trace.data, config.aic_window),
config.aic_window.to_double(),
AkaikeInformationCriterion,
config.characteristic_window,
)
let kurtosis = pick_from_characteristic(
trace,
running_kurtosis(trace.data, config.characteristic_window),
config.kurtosis_threshold,
KurtosisChange,
config.characteristic_window,
)
let picks = Array::new()
match amplitude {
Some(value) => picks.push(value)
None => ()
}
match energy {
Some(value) => picks.push(value)
None => ()
}
match aic {
Some(value) => picks.push(value)
None => ()
}
match kurtosis {
Some(value) => picks.push(value)
None => ()
}
consensus_from_picks(
trace,
picks,
config.agreement_seconds,
config.minimum_confidence,
)
}
///|
/// Build a complete picker diagnostic record.
pub fn picker_diagnostics(
trace : Trace,
config : AdvancedPickerConfig,
) -> PickerDiagnostics {
let characteristics = picker_characteristics(trace, config)
let amplitude = summarize_picker_characteristic(
"amplitude",
characteristics[0],
trace,
config.characteristic_window,
)
let energy = summarize_picker_characteristic(
"energy",
characteristics[1],
trace,
config.characteristic_window,
)
let aic = summarize_picker_characteristic(
"aic",
characteristics[2].map(value => -value),
trace,
config.characteristic_window,
)
let kurtosis = summarize_picker_characteristic(
"kurtosis",
characteristics[3],
trace,
config.characteristic_window,
)
{
trace_station: trace.station,
sample_count: trace.npts(),
amplitude,
energy,
aic,
kurtosis,
consensus: advanced_pick(trace, config),
}
}
///|
/// Return whether a consensus pick has sufficient independent evidence.
pub fn consensus_is_reliable(
pick : ConsensusPick,
minimum_methods : Int,
) -> Bool {
pick.method_count >= minimum_methods.max(1) && pick.confidence > 0.0
}
///|
/// Return pick latency relative to a trace start.
pub fn consensus_latency(trace : Trace, pick : ConsensusPick) -> Double {
pick.time - trace.start_time
}
///|
/// Format a consensus pick for an event table.
pub fn consensus_pick_text(pick : ConsensusPick) -> String {
"index=" +
pick.index.to_string() +
" time=" +
pick.time.to_string() +
" methods=" +
pick.method_count.to_string() +
" confidence=" +
pick.confidence.to_string()
}
///|
/// Format picker diagnostics for JSON-like logs.
pub fn picker_diagnostics_text(diagnostics : PickerDiagnostics) -> String {
let consensus = match diagnostics.consensus {
Some(pick) => consensus_pick_text(pick)
None => "none"
}
"station=" +
diagnostics.trace_station +
" samples=" +
diagnostics.sample_count.to_string() +
" amplitude_confidence=" +
diagnostics.amplitude.confidence.to_string() +
" energy_confidence=" +
diagnostics.energy.confidence.to_string() +
" aic_confidence=" +
diagnostics.aic.confidence.to_string() +
" kurtosis_confidence=" +
diagnostics.kurtosis.confidence.to_string() +
" consensus=" +
consensus
}