///|
/// Longitudinal recovery records and robust baselines for real-world use.
/// Missing or low-quality mornings remain visible in the audit trail instead
/// of being silently converted into zero-valued physiology.
pub(all) enum RecoveryTrajectory {
RecoveryImproving
RecoveryStable
RecoveryWorsening
RecoveryInsufficient
} derive(FromJson, ToJson, Debug, Eq)
///|
pub(all) enum RecoveryDayStatus {
RecoveryReady
RecoveryModerate
RecoveryStrained
RecoveryUnavailable
} derive(FromJson, ToJson, Debug, Eq)
///|
pub(all) struct RecoveryDayRecord {
date : String
mean_rr_ms : Double
rmssd_ms : Double
sdnn_ms : Double
resting_hr_bpm : Double
sleep_hours : Double
sleep_efficiency : Double
respiratory_rate : Double
training_load : Double
signal_quality : Double
source : String
missing : Bool
} derive(FromJson, ToJson, Debug, Eq)
///|
pub(all) struct RecoveryBaseline {
sample_count : Int
mean_rr : Double
median_rr : Double
mad_rr : Double
median_rmssd : Double
mad_rmssd : Double
median_resting_hr : Double
mad_resting_hr : Double
median_sleep_hours : Double
quality_ratio : Double
} derive(FromJson, ToJson, Debug, Eq)
///|
pub(all) struct RecoveryDayAssessment {
record : RecoveryDayRecord
baseline : RecoveryBaseline
rr_z : Double
rmssd_z : Double
heart_rate_z : Double
sleep_z : Double
readiness_score : Double
status : RecoveryDayStatus
confidence : Double
reasons : Array[String]
} derive(FromJson, ToJson, Debug, Eq)
///|
pub(all) struct RecoveryWeekSummary {
week_index : Int
start_date : String
end_date : String
record_count : Int
usable_count : Int
average_readiness : Double
minimum_readiness : Double
average_rmssd : Double
average_sleep_hours : Double
total_training_load : Double
quality_ratio : Double
missing_count : Int
mut trajectory : RecoveryTrajectory
} derive(FromJson, ToJson, Debug, Eq)
///|
pub(all) struct LongitudinalRecoveryReport {
records : Array[RecoveryDayRecord]
assessments : Array[RecoveryDayAssessment]
baseline : RecoveryBaseline
trajectory : RecoveryTrajectory
current_score : Double
current_status : RecoveryDayStatus
missing_days : Int
quality_ratio : Double
stable_streak : Int
} derive(FromJson, ToJson, Debug, Eq)
///|
pub(all) struct RecoveryTrendPoint {
date : String
score : Double
rmssd : Double
resting_hr : Double
load : Double
status : RecoveryDayStatus
} derive(FromJson, ToJson, Debug, Eq)
///|
fn recovery_safe(value : Double, fallback : Double) -> Double {
if value.is_nan() || value.is_inf() {
fallback
} else {
value
}
}
///|
fn recovery_bound(value : Double, low : Double, high : Double) -> Double {
recovery_safe(value, low).clamp(min=low, max=high)
}
///|
pub fn recovery_trajectory_name(value : RecoveryTrajectory) -> String {
match value {
RecoveryImproving => "improving"
RecoveryStable => "stable"
RecoveryWorsening => "worsening"
RecoveryInsufficient => "insufficient"
}
}
///|
pub fn recovery_status_name(value : RecoveryDayStatus) -> String {
match value {
RecoveryReady => "ready"
RecoveryModerate => "moderate"
RecoveryStrained => "strained"
RecoveryUnavailable => "unavailable"
}
}
///|
pub fn make_recovery_day_record(
date : String,
mean_rr_ms : Double,
rmssd_ms : Double,
sdnn_ms : Double,
resting_hr_bpm : Double,
sleep_hours : Double,
sleep_efficiency : Double,
respiratory_rate : Double,
training_load : Double,
signal_quality : Double,
source : String,
) -> RecoveryDayRecord {
{
date,
mean_rr_ms: recovery_bound(mean_rr_ms, 0.0, 3000.0),
rmssd_ms: recovery_bound(rmssd_ms, 0.0, 1000.0),
sdnn_ms: recovery_bound(sdnn_ms, 0.0, 2000.0),
resting_hr_bpm: recovery_bound(resting_hr_bpm, 0.0, 240.0),
sleep_hours: recovery_bound(sleep_hours, 0.0, 24.0),
sleep_efficiency: recovery_bound(sleep_efficiency, 0.0, 1.0),
respiratory_rate: recovery_bound(respiratory_rate, 0.0, 80.0),
training_load: recovery_bound(training_load, 0.0, 100000.0),
signal_quality: recovery_bound(signal_quality, 0.0, 1.0),
source,
missing: false,
}
}
///|
pub fn missing_recovery_day(date : String) -> RecoveryDayRecord {
{
date,
mean_rr_ms: 0.0,
rmssd_ms: 0.0,
sdnn_ms: 0.0,
resting_hr_bpm: 0.0,
sleep_hours: 0.0,
sleep_efficiency: 0.0,
respiratory_rate: 0.0,
training_load: 0.0,
signal_quality: 0.0,
source: "missing",
missing: true,
}
}
///|
pub fn recovery_record_is_usable(record : RecoveryDayRecord) -> Bool {
!record.missing &&
record.date.length() > 0 &&
record.mean_rr_ms > 0.0 &&
record.rmssd_ms >= 0.0 &&
record.resting_hr_bpm > 0.0 &&
record.signal_quality > 0.0
}
///|
pub fn recovery_record_quality(record : RecoveryDayRecord) -> Double {
if record.missing {
0.0
} else {
let completeness = [
record.mean_rr_ms > 0.0,
record.rmssd_ms >= 0.0,
record.resting_hr_bpm > 0.0,
record.sleep_hours > 0.0,
record.signal_quality > 0.0,
]
let count = completeness.filter(value => value).length()
record.signal_quality * 0.70 + count.to_double() / 5.0 * 0.30
}
}
///|
fn recovery_copy_records(
values : Array[RecoveryDayRecord],
) -> Array[RecoveryDayRecord] {
let result = []
for value in values {
result.push(value)
}
result
}
///|
pub fn sort_recovery_records(
values : Array[RecoveryDayRecord],
) -> Array[RecoveryDayRecord] {
let result = recovery_copy_records(values)
result.sort_by((left, right) => {
if left.date < right.date {
-1
} else if left.date > right.date {
1
} else {
0
}
})
result
}
///|
fn recovery_valid_values(
records : Array[RecoveryDayRecord],
selector : (RecoveryDayRecord) -> Double,
) -> Array[Double] {
let values = []
for record in records {
if recovery_record_is_usable(record) {
values.push(selector(record))
}
}
values
}
///|
pub fn build_recovery_baseline(
records : Array[RecoveryDayRecord],
) -> RecoveryBaseline {
let usable = records.filter(record => recovery_record_is_usable(record))
let rr = recovery_valid_values(records, record => record.mean_rr_ms)
let rmssd = recovery_valid_values(records, record => record.rmssd_ms)
let heart = recovery_valid_values(records, record => record.resting_hr_bpm)
let sleep = recovery_valid_values(records, record => record.sleep_hours)
let qualities = records.map(record => recovery_record_quality(record))
{
sample_count: usable.length(),
mean_rr: mean_value(rr),
median_rr: median_value(rr),
mad_rr: median_absolute_deviation(rr),
median_rmssd: median_value(rmssd),
mad_rmssd: median_absolute_deviation(rmssd),
median_resting_hr: median_value(heart),
mad_resting_hr: median_absolute_deviation(heart),
median_sleep_hours: median_value(sleep),
quality_ratio: mean_value(qualities),
}
}
///|
fn recovery_robust_z(value : Double, center : Double, mad : Double) -> Double {
if mad <= 0.000001 {
if value > center {
0.5
} else if value < center {
-0.5
} else {
0.0
}
} else {
(value - center) / (1.4826 * mad)
}
}
///|
pub fn recovery_rr_z(
record : RecoveryDayRecord,
baseline : RecoveryBaseline,
) -> Double {
recovery_robust_z(record.mean_rr_ms, baseline.median_rr, baseline.mad_rr)
}
///|
pub fn recovery_rmssd_z(
record : RecoveryDayRecord,
baseline : RecoveryBaseline,
) -> Double {
recovery_robust_z(record.rmssd_ms, baseline.median_rmssd, baseline.mad_rmssd)
}
///|
pub fn recovery_heart_rate_z(
record : RecoveryDayRecord,
baseline : RecoveryBaseline,
) -> Double {
recovery_robust_z(
record.resting_hr_bpm,
baseline.median_resting_hr,
baseline.mad_resting_hr,
)
}
///|
pub fn recovery_sleep_z(
record : RecoveryDayRecord,
baseline : RecoveryBaseline,
) -> Double {
recovery_robust_z(record.sleep_hours, baseline.median_sleep_hours, 0.5)
}
///|
fn recovery_score_component(value : Double, weight : Double) -> Double {
(50.0 + value * 12.0).clamp(min=0.0, max=100.0) * weight
}
///|
pub fn calculate_recovery_score(
record : RecoveryDayRecord,
baseline : RecoveryBaseline,
) -> Double {
if !recovery_record_is_usable(record) || baseline.sample_count == 0 {
0.0
} else {
let rr = recovery_rr_z(record, baseline).clamp(min=-4.0, max=4.0)
let rmssd = recovery_rmssd_z(record, baseline).clamp(min=-4.0, max=4.0)
let heart = (-recovery_heart_rate_z(record, baseline)).clamp(
min=-4.0,
max=4.0,
)
let sleep = recovery_sleep_z(record, baseline).clamp(min=-4.0, max=4.0)
let raw = recovery_score_component(rr, 0.30) +
recovery_score_component(rmssd, 0.35) +
recovery_score_component(heart, 0.20) +
recovery_score_component(sleep, 0.15)
raw * (0.65 + record.signal_quality * 0.35)
}
}
///|
pub fn recovery_status_from_score(
score : Double,
record : RecoveryDayRecord,
) -> RecoveryDayStatus {
if !recovery_record_is_usable(record) || score <= 0.0 {
RecoveryUnavailable
} else if score >= 70.0 {
RecoveryReady
} else if score >= 48.0 {
RecoveryModerate
} else {
RecoveryStrained
}
}
///|
pub fn recovery_confidence(
record : RecoveryDayRecord,
baseline : RecoveryBaseline,
) -> Double {
if baseline.sample_count == 0 || record.missing {
0.0
} else {
let sample_factor = (baseline.sample_count.to_double() / 14.0)
.sqrt()
.clamp(min=0.25, max=1.0)
recovery_record_quality(record) * sample_factor
}
}
///|
pub fn assess_recovery_day(
record : RecoveryDayRecord,
baseline : RecoveryBaseline,
) -> RecoveryDayAssessment {
let score = calculate_recovery_score(record, baseline)
let status = recovery_status_from_score(score, record)
let confidence = recovery_confidence(record, baseline)
let reasons = []
if record.missing {
reasons.push("record is missing")
}
if record.signal_quality < 0.70 {
reasons.push("signal quality is below the preferred floor")
}
if recovery_rr_z(record, baseline) < -1.5 {
reasons.push("mean RR is below baseline")
}
if recovery_rmssd_z(record, baseline) < -1.5 {
reasons.push("RMSSD is below baseline")
}
if recovery_heart_rate_z(record, baseline) > 1.5 {
reasons.push("resting heart rate is above baseline")
}
if record.sleep_hours > 0.0 && record.sleep_hours < 6.0 {
reasons.push("sleep duration is short")
}
{
record,
baseline,
rr_z: recovery_rr_z(record, baseline),
rmssd_z: recovery_rmssd_z(record, baseline),
heart_rate_z: recovery_heart_rate_z(record, baseline),
sleep_z: recovery_sleep_z(record, baseline),
readiness_score: score,
status,
confidence,
reasons,
}
}
///|
pub fn recovery_missing_days(records : Array[RecoveryDayRecord]) -> Int {
records.filter(record => record.missing).length()
}
///|
pub fn recovery_quality_ratio(records : Array[RecoveryDayRecord]) -> Double {
if records.length() == 0 {
0.0
} else {
records
.filter(record => recovery_record_is_usable(record))
.length()
.to_double() /
records.length().to_double()
}
}
///|
pub fn recovery_trajectory_from_scores(
scores : Array[Double],
) -> RecoveryTrajectory {
if scores.length() < 2 {
RecoveryInsufficient
} else {
let slope = fit_linear_trend(scores).slope
if slope > 1.5 {
RecoveryImproving
} else if slope < -1.5 {
RecoveryWorsening
} else {
RecoveryStable
}
}
}
///|
pub fn recovery_trajectory_from_assessments(
assessments : Array[RecoveryDayAssessment],
) -> RecoveryTrajectory {
recovery_trajectory_from_scores(
assessments
.filter(item => item.status != RecoveryUnavailable)
.map(item => item.readiness_score),
)
}
///|
pub fn recovery_stable_streak(
assessments : Array[RecoveryDayAssessment],
) -> Int {
let mut streak = 0
for i in 0.. LongitudinalRecoveryReport {
let ordered = sort_recovery_records(records)
let safe_window = baseline_window.clamp(min=1, max=ordered.length().max(1))
let start = (ordered.length() - safe_window).max(0)
let baseline_records = []
for i in start.. Int {
match (parse_date(first), parse_date(date)) {
(Some(left), Some(right)) => (date_distance(left, right) / 7).max(0)
_ => 0
}
}
///|
fn recovery_bool_int(value : Bool) -> Int {
if value {
1
} else {
0
}
}
///|
fn recovery_week_position(
weeks : Array[RecoveryWeekSummary],
index : Int,
) -> Int? {
for i in 0.. Array[RecoveryWeekSummary] {
let ordered = sort_recovery_records(records)
if ordered.length() == 0 {
return []
}
let report = build_longitudinal_recovery_report(ordered, baseline_window)
let weeks : Array[RecoveryWeekSummary] = []
for i in 0.. {
let old = weeks[position]
let count = old.record_count + 1
let usable = old.usable_count +
recovery_bool_int(recovery_record_is_usable(record))
let missing = old.missing_count + recovery_bool_int(record.missing)
weeks[position] = {
week_index: index,
start_date: old.start_date,
end_date: record.date,
record_count: count,
usable_count: usable,
average_readiness: (
old.average_readiness * old.record_count.to_double() +
assessment.readiness_score
) /
count.to_double(),
minimum_readiness: old.minimum_readiness.min(
assessment.readiness_score,
),
average_rmssd: (
old.average_rmssd * old.record_count.to_double() + record.rmssd_ms
) /
count.to_double(),
average_sleep_hours: (
old.average_sleep_hours * old.record_count.to_double() +
record.sleep_hours
) /
count.to_double(),
total_training_load: old.total_training_load + record.training_load,
quality_ratio: (
old.quality_ratio * old.record_count.to_double() +
record.signal_quality
) /
count.to_double(),
missing_count: missing,
trajectory: old.trajectory,
}
}
None =>
weeks.push({
week_index: index,
start_date: record.date,
end_date: record.date,
record_count: 1,
usable_count: recovery_bool_int(recovery_record_is_usable(record)),
average_readiness: assessment.readiness_score,
minimum_readiness: assessment.readiness_score,
average_rmssd: record.rmssd_ms,
average_sleep_hours: record.sleep_hours,
total_training_load: record.training_load,
quality_ratio: record.signal_quality,
missing_count: recovery_bool_int(record.missing),
trajectory: RecoveryStable,
})
}
}
weeks
}
///|
pub fn recovery_trend_points(
report : LongitudinalRecoveryReport,
) -> Array[RecoveryTrendPoint] {
let result = []
for assessment in report.assessments {
result.push({
date: assessment.record.date,
score: assessment.readiness_score,
rmssd: assessment.record.rmssd_ms,
resting_hr: assessment.record.resting_hr_bpm,
load: assessment.record.training_load,
status: assessment.status,
})
}
result
}
///|
fn recovery_trajectory_value(value : RecoveryTrajectory) -> Double {
match value {
RecoveryImproving => 1.0
RecoveryStable => 0.0
RecoveryWorsening => -1.0
RecoveryInsufficient => 0.0
}
}
///|
pub fn recovery_report_feature_vector(
report : LongitudinalRecoveryReport,
) -> Array[Double] {
[
report.current_score,
report.baseline.mean_rr,
report.baseline.median_rmssd,
report.baseline.median_resting_hr,
report.baseline.median_sleep_hours,
report.baseline.quality_ratio,
recovery_trajectory_value(report.trajectory),
report.missing_days.to_double(),
report.quality_ratio,
report.stable_streak.to_double(),
recovery_report_risk_score(report),
]
}
///|
pub fn recovery_report_csv(report : LongitudinalRecoveryReport) -> String {
let grid = [
[
"date", "mean_rr_ms", "rmssd_ms", "resting_hr_bpm", "sleep_hours", "training_load",
"quality", "score", "status", "confidence",
],
]
for assessment in report.assessments {
let record = assessment.record
grid.push([
record.date,
record.mean_rr_ms.to_string(),
record.rmssd_ms.to_string(),
record.resting_hr_bpm.to_string(),
record.sleep_hours.to_string(),
record.training_load.to_string(),
record.signal_quality.to_string(),
assessment.readiness_score.to_string(),
recovery_status_name(assessment.status),
assessment.confidence.to_string(),
])
}
to_csv(grid)
}
///|
pub fn recovery_week_csv(weeks : Array[RecoveryWeekSummary]) -> String {
let grid = [
[
"week_index", "start_date", "end_date", "record_count", "usable_count", "average_readiness",
"minimum_readiness", "average_rmssd", "average_sleep", "training_load", "quality_ratio",
"missing_count", "trajectory",
],
]
for week in weeks {
grid.push([
week.week_index.to_string(),
week.start_date,
week.end_date,
week.record_count.to_string(),
week.usable_count.to_string(),
week.average_readiness.to_string(),
week.minimum_readiness.to_string(),
week.average_rmssd.to_string(),
week.average_sleep_hours.to_string(),
week.total_training_load.to_string(),
week.quality_ratio.to_string(),
week.missing_count.to_string(),
recovery_trajectory_name(week.trajectory),
])
}
to_csv(grid)
}
///|
pub fn recovery_compare_days(
current : RecoveryDayRecord,
previous : RecoveryDayRecord,
) -> Array[Double] {
[
current.mean_rr_ms - previous.mean_rr_ms,
current.rmssd_ms - previous.rmssd_ms,
current.sdnn_ms - previous.sdnn_ms,
current.resting_hr_bpm - previous.resting_hr_bpm,
current.sleep_hours - previous.sleep_hours,
current.training_load - previous.training_load,
current.signal_quality - previous.signal_quality,
]
}
///|
pub fn recovery_day_is_better(
current : RecoveryDayAssessment,
previous : RecoveryDayAssessment,
) -> Bool {
current.readiness_score > previous.readiness_score &&
current.confidence >= 0.30
}
///|
pub fn recovery_report_message(report : LongitudinalRecoveryReport) -> String {
match report.current_status {
RecoveryReady => "Recovery profile supports the planned session."
RecoveryModerate =>
"Recovery is moderate; keep intensity controlled and monitor symptoms."
RecoveryStrained =>
"Recovery is strained; prioritize low-intensity work or rest."
RecoveryUnavailable =>
"Recovery is unavailable because the recent signal is incomplete."
}
}
///|
pub fn recovery_report_is_usable(report : LongitudinalRecoveryReport) -> Bool {
report.assessments.length() > 0 &&
report.baseline.sample_count > 0 &&
report.current_status != RecoveryUnavailable
}
///|
pub fn recovery_report_risk_score(
report : LongitudinalRecoveryReport,
) -> Double {
if report.current_score <= 0.0 {
1.0
} else {
(1.0 - report.current_score / 100.0).clamp(min=0.0, max=1.0) *
(0.70 + (1.0 - report.quality_ratio) * 0.30)
}
}
///|
pub fn recovery_report_load_adjustment(
report : LongitudinalRecoveryReport,
) -> Double {
match report.current_status {
RecoveryReady => 1.0
RecoveryModerate => 0.75
RecoveryStrained => 0.45
RecoveryUnavailable => 0.25
}
}
///|
pub fn recovery_record_to_row(record : RecoveryDayRecord) -> Array[String] {
[
record.date,
record.mean_rr_ms.to_string(),
record.rmssd_ms.to_string(),
record.sdnn_ms.to_string(),
record.resting_hr_bpm.to_string(),
record.sleep_hours.to_string(),
record.sleep_efficiency.to_string(),
record.respiratory_rate.to_string(),
record.training_load.to_string(),
record.signal_quality.to_string(),
record.source,
record.missing.to_string(),
]
}
///|
pub fn recovery_records_csv(records : Array[RecoveryDayRecord]) -> String {
let grid = [
[
"date", "mean_rr_ms", "rmssd_ms", "sdnn_ms", "resting_hr_bpm", "sleep_hours",
"sleep_efficiency", "respiratory_rate", "training_load", "signal_quality",
"source", "missing",
],
]
for record in records {
grid.push(recovery_record_to_row(record))
}
to_csv(grid)
}
///|
pub fn recovery_recent_records(
records : Array[RecoveryDayRecord],
count : Int,
) -> Array[RecoveryDayRecord] {
let ordered = sort_recovery_records(records)
let safe_count = count.max(0)
if ordered.length() <= safe_count {
ordered
} else {
let result = []
for i in (ordered.length() - safe_count).. Double {
let values = assessments
.filter(item => item.status != RecoveryUnavailable)
.map(item => item.readiness_score)
mean_value(values)
}
///|
pub fn recovery_score_sd(assessments : Array[RecoveryDayAssessment]) -> Double {
let values = assessments
.filter(item => item.status != RecoveryUnavailable)
.map(item => item.readiness_score)
standard_deviation(values)
}
///|
pub fn recovery_score_quantile(
assessments : Array[RecoveryDayAssessment],
proportion : Double,
) -> Double {
let values = assessments
.filter(item => item.status != RecoveryUnavailable)
.map(item => item.readiness_score)
quantile_value(values, proportion.clamp(min=0.0, max=1.0))
}
///|
pub fn recovery_best_day(
assessments : Array[RecoveryDayAssessment],
) -> RecoveryDayAssessment? {
if assessments.length() == 0 {
None
} else {
let mut best = assessments[0]
for item in assessments {
if item.readiness_score > best.readiness_score {
best = item
}
}
Some(best)
}
}
///|
pub fn recovery_worst_day(
assessments : Array[RecoveryDayAssessment],
) -> RecoveryDayAssessment? {
if assessments.length() == 0 {
None
} else {
let mut worst = assessments[0]
for item in assessments {
if item.readiness_score < worst.readiness_score {
worst = item
}
}
Some(worst)
}
}
///|
pub fn recovery_data_completeness(
records : Array[RecoveryDayRecord],
) -> Array[Double] {
if records.length() == 0 {
[0.0, 0.0, 0.0, 0.0, 0.0]
} else {
[
records.filter(record => record.mean_rr_ms > 0.0).length().to_double() /
records.length().to_double(),
records
.filter(record => record.rmssd_ms >= 0.0 && !record.missing)
.length()
.to_double() /
records.length().to_double(),
records.filter(record => record.resting_hr_bpm > 0.0).length().to_double() /
records.length().to_double(),
records.filter(record => record.sleep_hours > 0.0).length().to_double() /
records.length().to_double(),
recovery_quality_ratio(records),
]
}
}
///|
pub fn recovery_status_is_actionable(status : RecoveryDayStatus) -> Bool {
status == RecoveryStrained || status == RecoveryUnavailable
}
///|
pub fn recovery_trajectory_is_positive(trajectory : RecoveryTrajectory) -> Bool {
trajectory == RecoveryImproving || trajectory == RecoveryStable
}
///|
pub fn recovery_report_headline(report : LongitudinalRecoveryReport) -> String {
"\{recovery_status_name(report.current_status)} / \{report.current_score.to_string()} / \{recovery_trajectory_name(report.trajectory)}"
}