use serde::Deserialize;
#[derive(Debug, Clone, Copy, PartialEq, Deserialize)]
pub enum Curve {
Linear(f32, f32),
Power(f32),
Logistic(f32, f32),
Step(f32),
Hours(f32, f32),
}
impl Curve {
pub fn eval(self, x: f32) -> f32 {
match self {
Curve::Linear(m, b) => (m * x + b).clamp(0.0, 1.0),
Curve::Power(k) => x.clamp(0.0, 1.0).powf(k),
Curve::Logistic(k, x0) => logistic(k, x0, x),
Curve::Step(at) => {
if x >= at {
1.0
} else {
0.0
}
}
Curve::Hours(from, to) => hour_window(from, to, x * 24.0),
}
}
}
pub fn logistic(k: f32, x0: f32, x: f32) -> f32 {
1.0 / (1.0 + (-k * (x - x0)).exp())
}
pub fn hour_window(from: f32, to: f32, hour: f32) -> f32 {
let h = hour.rem_euclid(24.0);
let inside = if from < to {
h >= from && h < to
} else if from > to {
h >= from || h < to
} else {
false
};
if inside { 1.0 } else { 0.0 }
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn linear_clamps_to_unit_interval() {
assert!((Curve::Linear(-1.0, 1.0).eval(0.25) - 0.75).abs() < 1e-6);
assert_eq!(Curve::Linear(2.0, 0.0).eval(0.9), 1.0);
assert_eq!(Curve::Linear(-1.0, 0.0).eval(0.5), 0.0);
}
#[test]
fn logistic_is_half_at_its_midpoint() {
assert!((logistic(8.0, 0.7, 0.7) - 0.5).abs() < 1e-6);
assert!(logistic(8.0, 0.7, 1.0) > 0.9);
assert!(logistic(8.0, 0.7, 0.0) < 0.01);
}
#[test]
fn hour_window_wraps_past_midnight() {
assert_eq!(hour_window(22.0, 6.0, 23.0), 1.0);
assert_eq!(hour_window(22.0, 6.0, 3.0), 1.0);
assert_eq!(hour_window(22.0, 6.0, 12.0), 0.0);
assert_eq!(hour_window(8.0, 17.0, 17.0), 0.0);
assert_eq!(hour_window(8.0, 8.0, 8.0), 0.0);
}
#[test]
fn hours_curve_reads_a_normalised_hour() {
assert_eq!(Curve::Hours(22.0, 6.0).eval(23.0 / 24.0), 1.0);
assert_eq!(Curve::Hours(8.0, 17.0).eval(12.0 / 24.0), 1.0);
}
#[test]
fn step_and_power() {
assert_eq!(Curve::Step(0.5).eval(0.5), 1.0);
assert_eq!(Curve::Step(0.5).eval(0.49), 0.0);
assert!((Curve::Power(2.0).eval(0.5) - 0.25).abs() < 1e-6);
}
}