///|
/// Recording protocol used to validate an analysis request.
pub(all) struct RecordingProtocol {
minimum_beats : Int
target_duration_seconds : Double
minimum_duration_seconds : Double
maximum_artifact_ratio : Double
required_sample_rate_hz : Double
require_ordered_dates : Bool
} derive(FromJson, ToJson, Debug, Eq)
///|
/// Protocol validation result with actionable reasons.
pub(all) struct ProtocolValidation {
passed : Bool
reasons : Array[String]
sample_count : Int
duration_seconds : Double
artifact_ratio : Double
date_gaps : Int
quality_grade : String
} derive(FromJson, ToJson, Debug, Eq)
///|
/// Standard resting protocol: five minutes, at least 300 intervals.
pub fn RecordingProtocol::resting() -> RecordingProtocol {
{
minimum_beats: 300,
target_duration_seconds: 300.0,
minimum_duration_seconds: 120.0,
maximum_artifact_ratio: 0.15,
required_sample_rate_hz: 4.0,
require_ordered_dates: false,
}
}
///|
/// Short field protocol suitable for a morning readiness check.
pub fn RecordingProtocol::morning() -> RecordingProtocol {
{
minimum_beats: 60,
target_duration_seconds: 60.0,
minimum_duration_seconds: 30.0,
maximum_artifact_ratio: 0.20,
required_sample_rate_hz: 4.0,
require_ordered_dates: true,
}
}
///|
/// Validate a recording against a protocol.
pub fn validate_recording_protocol(
intervals : Array[Double],
config : HrvConfig,
protocol : RecordingProtocol,
dates : Array[String],
) -> ProtocolValidation {
let validation = validate_intervals(intervals, config)
let duration = sum_values(valid_intervals(intervals, config)) / 1000.0
let artifact_ratio = if intervals.length() == 0 {
1.0
} else {
validation.invalid.to_double() / intervals.length().to_double()
}
let gaps = missing_calendar_days(dates)
let reasons = []
if intervals.length() < protocol.minimum_beats {
reasons.push("recording has too few intervals")
}
if duration < protocol.minimum_duration_seconds {
reasons.push("recording is shorter than the protocol minimum")
}
if artifact_ratio > protocol.maximum_artifact_ratio {
reasons.push("artifact ratio exceeds protocol limit")
}
if protocol.require_ordered_dates && !dates_are_ordered(dates) {
reasons.push("dates must be valid and strictly ordered")
}
{
passed: reasons.length() == 0,
reasons,
sample_count: intervals.length(),
duration_seconds: duration,
artifact_ratio,
date_gaps: gaps,
quality_grade: quality_grade(validation),
}
}
///|
/// Return a protocol-specific expected recording duration.
pub fn protocol_duration_error(
duration_seconds : Double,
protocol : RecordingProtocol,
) -> Double {
absolute_difference(duration_seconds, protocol.target_duration_seconds)
}
///|
/// Return a useful protocol recommendation.
pub fn protocol_recommendation(validation : ProtocolValidation) -> String {
if validation.passed {
"recording meets the selected protocol"
} else if validation.artifact_ratio > 0.30 {
"repeat the recording with improved sensor contact"
} else if validation.duration_seconds < 30.0 {
"record for at least the protocol minimum duration"
} else {
"review the protocol reasons before using the score"
}
}