///|
/// A numeric diagnostic for a recording and its clean-up result.
pub(all) struct SignalDiagnostic {
sample_count : Int
duration_seconds : Double
valid_ratio : Double
artifact_ratio : Double
mean_rr : Double
median_rr : Double
mean_hr : Double
hr_range : Double
rmssd : Double
sdnn : Double
drift_slope : Double
gap_candidates : Int
grade : String
} derive(FromJson, ToJson, Debug, Eq)
///|
/// Build a dashboard diagnostic without running the full spectral pipeline.
pub fn diagnose_signal(
intervals : Array[Double],
config : HrvConfig,
) -> SignalDiagnostic {
let validation = validate_intervals(intervals, config)
let hr = summarize_heart_rate(intervals, 0.0)
let trend = fit_linear_trend(intervals)
{
sample_count: intervals.length(),
duration_seconds: sum_values(intervals) / 1000.0,
valid_ratio: if intervals.length() == 0 {
0.0
} else {
validation.valid.to_double() / intervals.length().to_double()
},
artifact_ratio: if intervals.length() == 0 {
0.0
} else {
validation.invalid.to_double() / intervals.length().to_double()
},
mean_rr: mean_value(intervals),
median_rr: median_value(intervals),
mean_hr: hr.mean_bpm,
hr_range: hr.maximum_bpm - hr.minimum_bpm,
rmssd: calculate_rmssd(intervals),
sdnn: calculate_sdnn(intervals),
drift_slope: trend.slope,
gap_candidates: count_gap_candidates(intervals, config, 2.0),
grade: quality_grade(validation),
}
}
///|
/// Return whether a signal is acceptable for recovery scoring.
pub fn is_recovery_signal_usable(diagnostic : SignalDiagnostic) -> Bool {
diagnostic.sample_count >= 30 &&
diagnostic.valid_ratio >= 0.85 &&
diagnostic.rmssd >= 0.0
}
///|
/// Return a one-line recommendation for a signal-quality card.
pub fn diagnostic_recommendation(diagnostic : SignalDiagnostic) -> String {
if diagnostic.sample_count == 0 {
"collect an RR recording before analysis"
} else if diagnostic.valid_ratio < 0.75 {
"recording is too noisy; repeat the measurement"
} else if diagnostic.gap_candidates > diagnostic.sample_count / 10 {
"check sensor contact and remove long gaps"
} else if diagnostic.drift_slope.abs() > 5.0 {
"check for posture or sensor changes during the recording"
} else {
"recording is suitable for recovery analysis"
}
}