///|
/// A time-binned station trend measurement.
pub struct StationTrendPoint {
start_time : Double
end_time : Double
sample_count : Int
mean : Double
rms : Double
peak : Double
completeness : Double
quality_score : Double
}
///|
/// A trend report with linear drift coefficients.
pub struct StationTrendReport {
station : String
channel : String
points : Array[StationTrendPoint]
mean_drift_per_second : Double
rms_drift_per_second : Double
peak_drift_per_second : Double
declining : Bool
}
///|
/// Calculate trends for fixed physical windows.
pub fn station_trend(
trace : Trace,
window_seconds : Double,
expected_rate : Double,
) -> StationTrendReport {
let points : Array[StationTrendPoint] = Array::new()
if window_seconds <= 0.0 || trace.sampling_rate <= 0.0 {
return {
station: trace.station,
channel: trace.channel,
points,
mean_drift_per_second: 0.0,
rms_drift_per_second: 0.0,
peak_drift_per_second: 0.0,
declining: false,
}
}
let window = (window_seconds * trace.sampling_rate).to_int().max(1)
let mut start = 0
while start < trace.npts() {
let end = (start + window).min(trace.npts())
let data = slice_samples(trace.data, start, end)
let expected = (window_seconds * expected_rate.max(trace.sampling_rate))
.to_int()
.max(1)
let report = station_quality_report(
trace_slice(trace, start, end),
expected,
1.0e300,
1.0e-12,
data,
)
points.push({
start_time: trace.start_time + start.to_double() / trace.sampling_rate,
end_time: trace.start_time +
(end - 1).max(start).to_double() / trace.sampling_rate,
sample_count: data.length(),
mean: mean_samples(data),
rms: rms_samples(data),
peak: absolute_peak(data),
completeness: report.completeness,
quality_score: report.score,
})
start = end
}
let times = points.map(point => (point.start_time + point.end_time) / 2.0)
let means = points.map(point => point.mean)
let rmss = points.map(point => point.rms)
let peaks = points.map(point => point.peak)
let mean_slope = linear_slope(times, means)
let rms_slope = linear_slope(times, rmss)
let peak_slope = linear_slope(times, peaks)
{
station: trace.station,
channel: trace.channel,
points,
mean_drift_per_second: mean_slope,
rms_drift_per_second: rms_slope,
peak_drift_per_second: peak_slope,
declining: rms_slope < 0.0,
}
}
///|
/// Calculate the least-squares slope of y with respect to x.
pub fn linear_slope(x : Array[Double], y : Array[Double]) -> Double {
let count = x.length().min(y.length())
if count < 2 {
0.0
} else {
let mean_x = mean_samples(slice_samples(x, 0, count))
let mean_y = mean_samples(slice_samples(y, 0, count))
let mut numerator = 0.0
let mut denominator = 0.0
for index = 0; index < count; index = index + 1 {
let dx = x[index] - mean_x
numerator += dx * (y[index] - mean_y)
denominator += dx * dx
}
if denominator <= 0.0 {
0.0
} else {
numerator / denominator
}
}
}
///|
/// Return points whose score is below an operational threshold.
pub fn degraded_trend_points(
report : StationTrendReport,
threshold : Double,
) -> Array[StationTrendPoint] {
report.points.filter(point => point.quality_score < threshold)
}
///|
/// Export station trend points for a dashboard.
pub fn station_trend_to_csv(report : StationTrendReport) -> String {
let mut output = "station,channel,start,end,samples,mean,rms,peak,completeness,quality\n"
for point in report.points {
output = output +
report.station +
"," +
report.channel +
"," +
point.start_time.to_string() +
"," +
point.end_time.to_string() +
"," +
point.sample_count.to_string() +
"," +
point.mean.to_string() +
"," +
point.rms.to_string() +
"," +
point.peak.to_string() +
"," +
point.completeness.to_string() +
"," +
point.quality_score.to_string() +
"\n"
}
output
}
///|
/// Format station trend slopes.
pub fn station_trend_text(report : StationTrendReport) -> String {
"station=" +
report.station +
" channel=" +
report.channel +
" points=" +
report.points.length().to_string() +
" mean_slope=" +
report.mean_drift_per_second.to_string() +
" rms_slope=" +
report.rms_drift_per_second.to_string() +
" peak_slope=" +
report.peak_drift_per_second.to_string() +
" declining=" +
report.declining.to_string()
}