pub const WEIGHT_EPS: f32 = 1.0e-3;
pub const PHASE_RATE_CLAMP: (f32, f32) = (0.5, 2.0);
pub fn gait_cycles_per_sec(
speed: f32,
weight_sum: f32,
weighted_distance: f32,
weighted_cadence: f32,
) -> f32 {
if weight_sum <= WEIGHT_EPS || weighted_distance <= 1.0e-6 || weighted_cadence <= 1.0e-6 {
return 0.0;
}
let mean_distance = weighted_distance / weight_sum;
let nominal = weighted_cadence / weight_sum;
let (lo, hi) = PHASE_RATE_CLAMP;
(speed / mean_distance).clamp(lo * nominal, hi * nominal)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_pure_clip_at_its_authored_speed_runs_at_cadence_one() {
let walk_cps = gait_cycles_per_sec(1.388 / 1.417, 1.0, 1.388, 1.0 / 1.417);
assert!(
(walk_cps - 1.0 / 1.417).abs() < 1.0e-4,
"walk at its authored speed should play at 1×: {walk_cps}"
);
let run_cps = gait_cycles_per_sec(2.135 / 0.750, 1.0, 2.135, 1.0 / 0.750);
assert!(
(run_cps - 1.0 / 0.750).abs() < 1.0e-4,
"run at its authored speed should play at 1×: {run_cps}"
);
}
#[test]
fn a_mixture_at_its_own_authored_speed_stays_near_cadence_one() {
let (ww, wr) = (0.5, 0.5);
let weight_sum = ww + wr;
let weighted_distance = ww * 1.388 + wr * 2.135;
let weighted_cadence = ww / 1.417 + wr / 0.750;
let nominal = weighted_cadence / weight_sum;
let authored_speed = weighted_distance / weight_sum * nominal;
let cps = gait_cycles_per_sec(authored_speed, weight_sum, weighted_distance, weighted_cadence);
assert!((cps - nominal).abs() < 1.0e-4, "mixture cadence drifted: {cps} vs {nominal}");
}
#[test]
fn the_gait_rate_is_clamped_at_both_ends() {
let (lo, hi) = PHASE_RATE_CLAMP;
let nominal = 1.0 / 1.417;
let fast = gait_cycles_per_sec(1000.0, 1.0, 1.388, nominal);
assert!((fast - hi * nominal).abs() < 1.0e-5, "runaway speed must clamp: {fast}");
let slow = gait_cycles_per_sec(0.001, 1.0, 1.388, nominal);
assert!((slow - lo * nominal).abs() < 1.0e-5, "a crawl must not freeze the legs: {slow}");
}
#[test]
fn no_gait_weight_freezes_the_phase_instead_of_dividing_by_zero() {
assert_eq!(gait_cycles_per_sec(6.0, 0.0, 0.0, 0.0), 0.0);
assert_eq!(gait_cycles_per_sec(6.0, 1.0, 0.0, 1.0), 0.0);
}
}