///|
/// Options for the end-to-end HRV analysis pipeline.
pub(all) struct AnalysisOptions {
cleaning_method : CleaningMethod
sample_rate_hz : Double
remove_trend : Bool
window_function : WindowFunction
segment_size : Int
segment_hop : Int
nonlinear_enabled : Bool
} derive(FromJson, ToJson, Debug, Eq)
///|
/// Full report for a single RR recording.
pub(all) struct AnalysisReport {
raw_count : Int
cleaned_intervals : Array[Double]
raw_validation : IntervalValidation
quality : QualityReport
metrics : HrvMetrics
distribution : DistributionStats
poincare : PoincareMetrics
geometric : GeometricMetrics
heart_rate : HeartRateSummary
frequency : FrequencyMetrics
nonlinear : NonlinearMetrics
segments : Array[SegmentSummary]
feature_vector : Array[Double]
} derive(FromJson, ToJson, Debug, Eq)
///|
/// Conservative defaults suitable for short wearable recordings.
pub fn AnalysisOptions::default() -> AnalysisOptions {
{
cleaning_method: InterpolateLocalMedian,
sample_rate_hz: 4.0,
remove_trend: true,
window_function: Hann,
segment_size: 60,
segment_hop: 30,
nonlinear_enabled: true,
}
}
///|
/// Analyze an RR sequence through cleaning, quality, time, frequency, and
/// non-linear layers.
pub fn analyze_rr(
intervals : Array[Double],
config : HrvConfig,
options : AnalysisOptions,
) -> AnalysisReport {
let raw_validation = validate_intervals(intervals, config)
let (cleaned, quality) = clean_rr_intervals(
intervals,
options.cleaning_method,
config,
)
let metrics = calculate_metrics(cleaned, config, quality)
let distribution = summarize_distribution(cleaned)
let poincare = calculate_poincare(cleaned)
let geometric = calculate_geometric_metrics(cleaned, 7.8125)
let heart_rate = summarize_heart_rate(cleaned, 0.0)
let frequency = calculate_frequency_metrics(cleaned, options.sample_rate_hz)
let nonlinear = if options.nonlinear_enabled {
calculate_nonlinear_metrics(cleaned)
} else {
{
sample_entropy: 0.0,
approximate_entropy: 0.0,
dfa_alpha: 0.0,
turning_point_ratio: 0.0,
recurrence_rate: 0.0,
histogram_entropy: 0.0,
lag1_autocorrelation: autocorrelation(cleaned, 1),
complexity_index: 0.0,
}
}
let segments = summarize_segments(
cleaned,
options.segment_size,
options.segment_hop,
config,
)
let feature_vector = []
for value in advanced_feature_vector(cleaned) {
feature_vector.push(value)
}
for value in frequency_feature_vector(cleaned, options.sample_rate_hz) {
feature_vector.push(value)
}
if options.nonlinear_enabled {
for value in nonlinear_feature_vector(cleaned) {
feature_vector.push(value)
}
}
{
raw_count: intervals.length(),
cleaned_intervals: cleaned,
raw_validation,
quality,
metrics,
distribution,
poincare,
geometric,
heart_rate,
frequency,
nonlinear,
segments,
feature_vector,
}
}
///|
/// Analyze using the package defaults.
pub fn analyze_rr_default(intervals : Array[Double]) -> AnalysisReport {
let config = HrvConfig::default()
analyze_rr(intervals, config, AnalysisOptions::default())
}
///|
/// Compare two reports using normalized feature distance.
pub fn compare_analysis_reports(
left : AnalysisReport,
right : AnalysisReport,
) -> Double {
normalized_sequence_distance(left.feature_vector, right.feature_vector)
}
///|
/// Return a short human-readable quality summary.
pub fn analysis_quality_summary(report : AnalysisReport) -> String {
"\{quality_grade(report.raw_validation)}: \{report.quality.valid_beats}/\{report.quality.total_beats} valid intervals"
}
///|
/// Return the first n cleaned intervals for preview cards.
pub fn analysis_preview(report : AnalysisReport, limit : Int) -> Array[Double] {
let result = []
let bound = if limit < 0 {
0
} else if limit > report.cleaned_intervals.length() {
report.cleaned_intervals.length()
} else {
limit
}
for i in 0..