///|
/// Deterministic scenario generators for regression and benchmark testing.
///|
/// Scenario type for synthetic RR data.
pub(all) enum SimulationScenario {
Stable
RespiratoryModulated
GradualDrift
ExerciseTransition
ArtifactBurst
} derive(FromJson, ToJson, Debug, Eq)
///|
/// Simulation parameters.
pub(all) struct SimulationConfig {
length : Int
baseline_rr : Double
variability_ms : Double
sample_rate_hz : Double
scenario : SimulationScenario
artifact_period : Int
} derive(FromJson, ToJson, Debug, Eq)
///|
/// Default deterministic simulation configuration.
pub fn SimulationConfig::default() -> SimulationConfig {
{
length: 300,
baseline_rr: 800.0,
variability_ms: 25.0,
sample_rate_hz: 4.0,
scenario: Stable,
artifact_period: 37,
}
}
///|
fn simulation_wave(index : Int, sample_rate_hz : Double) -> Double {
let safe_rate = if sample_rate_hz <= 0.0 { 1.0 } else { sample_rate_hz }
let time = index.to_double() / safe_rate
0.60 * @math.sin(2.0 * 3.141592653589793 * 0.10 * time) +
0.30 * @math.sin(2.0 * 3.141592653589793 * 0.25 * time + 0.3) +
0.10 * @math.sin(2.0 * 3.141592653589793 * 0.03 * time)
}
///|
/// Generate one scenario without random state.
pub fn simulate_rr(config : SimulationConfig) -> Array[Double] {
let length = if config.length < 0 { 0 } else { config.length }
let result = []
for i in 0.. config.baseline_rr + config.variability_ms * wave
RespiratoryModulated =>
config.baseline_rr + config.variability_ms * 2.0 * wave
GradualDrift =>
config.baseline_rr +
config.variability_ms * wave +
120.0 * (drift - 0.5)
ExerciseTransition =>
if drift < 0.5 {
config.baseline_rr + config.variability_ms * wave
} else {
config.baseline_rr - 180.0 + config.variability_ms * wave
}
ArtifactBurst =>
if config.artifact_period > 0 && i % config.artifact_period == 0 {
config.baseline_rr * 3.0
} else {
config.baseline_rr + config.variability_ms * wave
}
}
result.push(value)
}
result
}
///|
/// Generate a direct sinusoidal respiratory trace.
pub fn simulate_respiratory_trace(
length : Int,
sample_rate_hz : Double,
rate_bpm : Double,
) -> Array[Double] {
let result = []
if length <= 0 || sample_rate_hz <= 0.0 {
return result
}
let frequency = rate_bpm / 60.0
for i in 0.. Array[AmbulatorySample] {
let intervals = simulate_rr(config)
let result = []
let mut timestamp = 0.0
for i in 0.. intervals.length() / 2 {
0.6
} else {
0.02
}
result.push({
timestamp_seconds: timestamp,
rr_ms: rr,
heart_rate_bpm: hr,
movement,
temperature_c: 36.5 + 0.1 * simulation_wave(i, config.sample_rate_hz),
quality_ratio: if rr > 2000.0 {
0.3
} else {
0.98
},
})
timestamp += rr / 1000.0
}
result
}
///|
/// Inject a deterministic missing-value run.
pub fn inject_missing_run(
values : Array[Double],
start : Int,
length : Int,
) -> Array[Double] {
let result = []
for value in values {
result.push(value)
}
let from = if start < 0 { 0 } else { start }
let safe_length = if length < 0 { 0 } else { length }
let to = if from + safe_length > result.length() {
result.length()
} else {
from + safe_length
}
for i in from.. Array[Double] {
if resolution_ms <= 0.0 {
return values
}
let result = []
for i in 0.. Array[String] {
["stable", "respiratory", "drift", "exercise_transition", "artifact_burst"]
}
///|
/// Parse a scenario name with stable fallback behavior.
pub fn parse_simulation_scenario(name : String) -> SimulationScenario {
match name {
"respiratory" => RespiratoryModulated
"drift" => GradualDrift
"exercise_transition" => ExerciseTransition
"artifact_burst" => ArtifactBurst
_ => Stable
}
}
///|
/// Return a compact set of scenario validation metrics.
pub fn simulation_summary(
values : Array[Double],
config : HrvConfig,
) -> Array[Double] {
let validation = validate_intervals(values, config)
let quality = evaluate_signal_quality(
values,
config,
SignalQualityWeights::default(),
30,
300,
)
[
values.length().to_double(),
validation.valid.to_double(),
validation.invalid.to_double(),
quality.score,
mean_value(values),
calculate_rmssd(values),
calculate_sdnn(values),
]
}
///|
/// Run all scenarios with one shared length for smoke tests.
pub fn simulate_scenario_matrix(
length : Int,
baseline_rr : Double,
) -> Array[Array[Double]] {
let result = []
for
scenario in [
Stable,
RespiratoryModulated,
GradualDrift,
ExerciseTransition,
ArtifactBurst,
] {
let config : SimulationConfig = {
length,
baseline_rr,
variability_ms: 25.0,
sample_rate_hz: 4.0,
scenario,
artifact_period: 37,
}
result.push(simulation_summary(simulate_rr(config), HrvConfig::default()))
}
result
}