///|
/// Summary of a trace's timing and sample layout.
pub struct TraceSummary {
station : String
channel : String
sample_count : Int
sample_rate : Double
start_time : Double
end_time : Double
duration : Double
mean : Double
standard_deviation : Double
peak : Double
}
///|
/// Create a compact summary suitable for logs and QC tables.
pub fn summarize_trace(trace : Trace) -> TraceSummary {
{
station: trace.station,
channel: trace.channel,
sample_count: trace.npts(),
sample_rate: trace.sampling_rate,
start_time: trace.start_time,
end_time: if trace.npts() == 0 {
trace.start_time
} else {
trace.start_time + trace.duration()
},
duration: trace.duration(),
mean: mean_samples(trace.data),
standard_deviation: standard_deviation(trace.data),
peak: maximum_sample(trace.data).abs().max(minimum_sample(trace.data).abs()),
}
}
///|
/// Return a trace with a replacement data array and preserved metadata.
pub fn trace_with_data(trace : Trace, data : Array[Double]) -> Trace {
Trace::new(
trace.station,
trace.channel,
trace.sampling_rate,
trace.start_time,
data,
)
}
///|
/// Add two aligned traces sample by sample.
pub fn add_traces(left : Trace, right : Trace) -> Result[Trace, String] {
if left.sampling_rate != right.sampling_rate {
return Err("Cannot add traces with different sample rates")
}
if left.start_time != right.start_time {
return Err("Cannot add traces with different start times")
}
if left.npts() != right.npts() {
return Err("Cannot add traces with different lengths")
}
let data = Array::new(capacity=left.npts())
for index = 0; index < left.npts(); index = index + 1 {
data.push(left.data[index] + right.data[index])
}
Ok(
Trace::new(
left.station,
left.channel,
left.sampling_rate,
left.start_time,
data,
),
)
}
///|
/// Subtract two aligned traces sample by sample.
pub fn subtract_traces(left : Trace, right : Trace) -> Result[Trace, String] {
if left.sampling_rate != right.sampling_rate ||
left.start_time != right.start_time {
return Err("Traces are not time aligned")
}
if left.npts() != right.npts() {
return Err("Cannot subtract traces with different lengths")
}
let data = Array::new(capacity=left.npts())
for index = 0; index < left.npts(); index = index + 1 {
data.push(left.data[index] - right.data[index])
}
Ok(
Trace::new(
left.station,
left.channel,
left.sampling_rate,
left.start_time,
data,
),
)
}
///|
/// Concatenate traces when their sampling grids meet exactly.
pub fn concatenate_traces(
first : Trace,
second : Trace,
) -> Result[Trace, String] {
if first.sampling_rate != second.sampling_rate {
return Err("Cannot concatenate traces with different sample rates")
}
let expected = first.start_time + first.npts().to_double() * first.delta()
if (second.start_time - expected).abs() > first.delta() * 0.001 {
return Err("Trace timing contains a gap or overlap")
}
let data = first.data.copy()
for value in second.data {
data.push(value)
}
Ok(
Trace::new(
first.station,
first.channel,
first.sampling_rate,
first.start_time,
data,
),
)
}
///|
/// Return a trace aligned to an integer sample offset.
pub fn shift_trace(trace : Trace, sample_offset : Int) -> Trace {
let data = Array::make(trace.npts(), 0.0)
for index = 0; index < trace.npts(); index = index + 1 {
let source = index - sample_offset
if source >= 0 && source < trace.npts() {
data[index] = trace.data[source]
}
}
Trace::new(
trace.station,
trace.channel,
trace.sampling_rate,
trace.start_time + sample_offset.to_double() * trace.delta(),
data,
)
}
///|
/// Align a trace to a reference using normalized cross-correlation.
pub fn align_to_reference(
trace : Trace,
reference : Trace,
max_lag : Int,
) -> Result[Trace, String] {
if trace.sampling_rate != reference.sampling_rate {
return Err("Cannot align traces with different sample rates")
}
let result = cross_correlate(trace.data, reference.data, max_lag)
Ok(shift_trace(trace, -result.max_lag))
}
///|
/// Return the overlapping sample interval for two traces.
pub fn common_time_range(left : Trace, right : Trace) -> (Double, Double)? {
if left.sampling_rate != right.sampling_rate {
return None
}
let left_end = left.start_time + left.duration()
let right_end = right.start_time + right.duration()
let start = if left.start_time > right.start_time {
left.start_time
} else {
right.start_time
}
let end = if left_end < right_end { left_end } else { right_end }
if end < start {
None
} else {
Some((start, end))
}
}
///|
/// Return an amplitude envelope using a quadrature approximation from a
/// centered first difference. It is stable for short traces and sufficient
/// for event-duration and amplitude measurements.
pub fn amplitude_envelope(data : Array[Double]) -> Array[Double] {
let result = Array::make(data.length(), 0.0)
if data.is_empty() {
return result
}
for index = 0; index < data.length(); index = index + 1 {
let previous = if index == 0 { data[index] } else { data[index - 1] }
let next = if index + 1 >= data.length() {
data[index]
} else {
data[index + 1]
}
let quadrature = 0.5 * (next - previous)
result[index] = (data[index] * data[index] + quadrature * quadrature).sqrt()
}
result
}
///|
/// Return a trace containing an amplitude envelope.
pub fn envelope_trace(trace : Trace) -> Trace {
trace_with_data(trace, amplitude_envelope(trace.data))
}
///|
/// Integrate a trace and preserve its physical sampling metadata.
pub fn integrate_trace(trace : Trace) -> Trace {
trace_with_data(trace, cumulative_integral(trace.data, trace.sampling_rate))
}
///|
/// Differentiate a trace with a finite-difference operator.
pub fn differentiate_trace(trace : Trace) -> Trace {
trace_with_data(trace, differentiate_samples(trace.data, trace.sampling_rate))
}
///|
/// Apply common preprocessing operations to a trace copy.
pub fn preprocess_trace(
trace : Trace,
remove_mean : Bool,
remove_trend : Bool,
taper_fraction : Double,
) -> Trace {
let result = trace.clone()
if remove_mean {
result.demean()
}
if remove_trend {
result.detrend()
}
cosine_taper(result.data, taper_fraction)
result
}
///|
/// Return a trace normalized to unit peak amplitude.
pub fn normalize_trace_peak(trace : Trace) -> Trace {
trace_with_data(trace, normalize_peak(trace.data))
}
///|
/// Return a trace normalized to zero mean and unit standard deviation.
pub fn normalize_trace_zscore(trace : Trace) -> Trace {
trace_with_data(trace, normalize_zscore(trace.data))
}
///|
/// Return a trace with all samples clipped to a physical range.
pub fn clamp_trace(trace : Trace, minimum : Double, maximum : Double) -> Trace {
let data = trace.data.copy()
clamp_samples(data, minimum, maximum)
trace_with_data(trace, data)
}
///|
/// Compute a signal-to-noise ratio using two time windows.
pub fn trace_snr_db(
trace : Trace,
noise_start : Double,
noise_end : Double,
signal_start : Double,
signal_end : Double,
) -> Double {
let noise = slice_samples(
trace.data,
((noise_start - trace.start_time) * trace.sampling_rate).to_int(),
((noise_end - trace.start_time) * trace.sampling_rate).to_int(),
)
let signal = slice_samples(
trace.data,
((signal_start - trace.start_time) * trace.sampling_rate).to_int(),
((signal_end - trace.start_time) * trace.sampling_rate).to_int(),
)
let noise_rms = rms_samples(noise)
let signal_rms = rms_samples(signal)
if noise_rms <= 0.0 {
0.0
} else {
20.0 * @math.log10(signal_rms / noise_rms)
}
}
///|
/// Return true when trace metadata and samples satisfy basic invariants.
pub fn trace_is_well_formed(trace : Trace) -> Bool {
validate_trace(trace) is Ok(_) && trace.npts() >= 0
}