///|
/// Sensor calibration parameters for vendor-neutral RR streams.
pub(all) struct SensorCalibration {
scale : Double
offset_ms : Double
minimum_rr : Double
maximum_rr : Double
timestamp_jitter_ms : Double
name : String
} derive(FromJson, ToJson, Debug, Eq)
///|
/// A calibration audit record.
pub(all) struct CalibrationReport {
input_count : Int
output_count : Int
changed_count : Int
rejected_count : Int
mean_shift_ms : Double
max_shift_ms : Double
jitter_rms_ms : Double
passed : Bool
} derive(FromJson, ToJson, Debug, Eq)
///|
/// Return a neutral calibration profile.
pub fn SensorCalibration::identity() -> SensorCalibration {
{
scale: 1.0,
offset_ms: 0.0,
minimum_rr: 300.0,
maximum_rr: 2000.0,
timestamp_jitter_ms: 0.0,
name: "identity",
}
}
///|
/// Return a calibration profile for a common millisecond sensor feed.
pub fn SensorCalibration::millisecond_sensor() -> SensorCalibration {
{
scale: 1.0,
offset_ms: 0.0,
minimum_rr: 250.0,
maximum_rr: 2500.0,
timestamp_jitter_ms: 2.0,
name: "millisecond_sensor",
}
}
///|
/// Convert one raw sensor reading into milliseconds.
pub fn calibrate_value(raw : Double, calibration : SensorCalibration) -> Double {
raw * calibration.scale + calibration.offset_ms
}
///|
/// Convert a calibrated value into beats per minute.
pub fn rr_to_bpm(interval_ms : Double) -> Double {
if interval_ms <= 0.0 || interval_ms.is_nan() {
0.0
} else {
60000.0 / interval_ms
}
}
///|
/// Convert beats per minute into milliseconds per beat.
pub fn bpm_to_rr(rate_bpm : Double) -> Double {
if rate_bpm <= 0.0 || rate_bpm.is_nan() {
0.0
} else {
60000.0 / rate_bpm
}
}
///|
/// Calibrate and retain only values in the profile's range.
pub fn calibrate_intervals(
raw : Array[Double],
calibration : SensorCalibration,
) -> Array[Double] {
let result = []
for value in raw {
let calibrated = calibrate_value(value, calibration)
if calibrated >= calibration.minimum_rr &&
calibrated <= calibration.maximum_rr {
result.push(calibrated)
}
}
result
}
///|
/// Calculate the audit report for one calibration pass.
pub fn audit_calibration(
raw : Array[Double],
calibration : SensorCalibration,
) -> CalibrationReport {
let calibrated = []
let mut changed = 0
let mut rejected = 0
let mut shift_sum = 0.0
let mut max_shift = 0.0
for value in raw {
let converted = calibrate_value(value, calibration)
let shift = absolute_difference(converted, value)
if shift > 0.0 {
changed += 1
}
shift_sum += shift
if shift > max_shift {
max_shift = shift
}
if converted >= calibration.minimum_rr &&
converted <= calibration.maximum_rr {
calibrated.push(converted)
} else {
rejected += 1
}
}
let jitter = if calibration.timestamp_jitter_ms < 0.0 {
0.0
} else {
calibration.timestamp_jitter_ms
}
{
input_count: raw.length(),
output_count: calibrated.length(),
changed_count: changed,
rejected_count: rejected,
mean_shift_ms: if raw.length() == 0 {
0.0
} else {
shift_sum / raw.length().to_double()
},
max_shift_ms: max_shift,
jitter_rms_ms: jitter,
passed: rejected == 0 && max_shift <= 100.0 && jitter <= 10.0,
}
}
///|
/// Apply an offset correction estimated from a reference sequence.
pub fn estimate_offset(
reference : Array[Double],
observed : Array[Double],
) -> Double {
let n = if reference.length() < observed.length() {
reference.length()
} else {
observed.length()
}
if n == 0 {
0.0
} else {
let differences = []
for i in 0.. Double {
let n = if reference.length() < observed.length() {
reference.length()
} else {
observed.length()
}
if n == 0 {
1.0
} else {
let ratios = []
for i in 0.. Double {
if resolution_ms <= 0.0 {
value
} else {
(value / resolution_ms).round() * resolution_ms
}
}
///|
/// Quantize an entire recording.
pub fn quantize_intervals(
values : Array[Double],
resolution_ms : Double,
) -> Array[Double] {
let result = []
for value in values {
result.push(quantize_interval(value, resolution_ms))
}
result
}