Skip to main content

oxihuman_core/
decay_counter.rs

1// Copyright (C) 2026 COOLJAPAN OU (Team KitaSan)
2// SPDX-License-Identifier: Apache-2.0
3#![allow(dead_code)]
4
5use std::f32::consts::E;
6
7/// A counter that decays exponentially over time.
8#[allow(dead_code)]
9#[derive(Debug, Clone, Copy)]
10pub struct DecayCounter {
11    value: f32,
12    half_life: f32,
13    last_update: f32,
14}
15
16#[allow(dead_code)]
17impl DecayCounter {
18    pub fn new(half_life: f32) -> Self {
19        Self {
20            value: 0.0,
21            half_life: half_life.max(f32::EPSILON),
22            last_update: 0.0,
23        }
24    }
25
26    pub fn with_initial(value: f32, half_life: f32) -> Self {
27        Self {
28            value,
29            half_life: half_life.max(f32::EPSILON),
30            last_update: 0.0,
31        }
32    }
33
34    pub fn increment(&mut self, amount: f32) {
35        self.value += amount;
36    }
37
38    pub fn update(&mut self, current_time: f32) {
39        let dt = current_time - self.last_update;
40        if dt > 0.0 {
41            let decay_rate = (2.0_f32).ln() / self.half_life;
42            self.value *= (-decay_rate * dt).exp();
43            self.last_update = current_time;
44        }
45    }
46
47    pub fn value(&self) -> f32 {
48        self.value
49    }
50
51    pub fn value_at(&self, time: f32) -> f32 {
52        let dt = time - self.last_update;
53        if dt <= 0.0 {
54            return self.value;
55        }
56        let decay_rate = (2.0_f32).ln() / self.half_life;
57        self.value * (-decay_rate * dt).exp()
58    }
59
60    pub fn half_life(&self) -> f32 {
61        self.half_life
62    }
63
64    pub fn set_half_life(&mut self, hl: f32) {
65        self.half_life = hl.max(f32::EPSILON);
66    }
67
68    pub fn reset(&mut self) {
69        self.value = 0.0;
70        self.last_update = 0.0;
71    }
72
73    pub fn is_negligible(&self, threshold: f32) -> bool {
74        self.value.abs() < threshold
75    }
76
77    pub fn time_to_reach(&self, target: f32) -> Option<f32> {
78        if self.value <= 0.0 || target <= 0.0 || target >= self.value {
79            return None;
80        }
81        let decay_rate = (2.0_f32).ln() / self.half_life;
82        Some((self.value / target).ln() / decay_rate)
83    }
84
85    /// Return the decay constant (lambda = ln(2)/half_life).
86    pub fn decay_constant(&self) -> f32 {
87        let _ = E; // reference E from consts
88        (2.0_f32).ln() / self.half_life
89    }
90}
91
92#[cfg(test)]
93mod tests {
94    use super::*;
95
96    #[test]
97    fn test_new() {
98        let dc = DecayCounter::new(1.0);
99        assert!((dc.value() - 0.0).abs() < f32::EPSILON);
100    }
101
102    #[test]
103    fn test_increment() {
104        let mut dc = DecayCounter::new(1.0);
105        dc.increment(10.0);
106        assert!((dc.value() - 10.0).abs() < f32::EPSILON);
107    }
108
109    #[test]
110    fn test_decay_one_half_life() {
111        let mut dc = DecayCounter::with_initial(100.0, 1.0);
112        dc.update(1.0);
113        assert!((dc.value() - 50.0).abs() < 0.5);
114    }
115
116    #[test]
117    fn test_value_at() {
118        let dc = DecayCounter::with_initial(100.0, 1.0);
119        let v = dc.value_at(1.0);
120        assert!((v - 50.0).abs() < 0.5);
121    }
122
123    #[test]
124    fn test_half_life_getter() {
125        let dc = DecayCounter::new(2.5);
126        assert!((dc.half_life() - 2.5).abs() < f32::EPSILON);
127    }
128
129    #[test]
130    fn test_set_half_life() {
131        let mut dc = DecayCounter::new(1.0);
132        dc.set_half_life(5.0);
133        assert!((dc.half_life() - 5.0).abs() < f32::EPSILON);
134    }
135
136    #[test]
137    fn test_reset() {
138        let mut dc = DecayCounter::with_initial(50.0, 1.0);
139        dc.reset();
140        assert!((dc.value() - 0.0).abs() < f32::EPSILON);
141    }
142
143    #[test]
144    fn test_is_negligible() {
145        let dc = DecayCounter::with_initial(0.001, 1.0);
146        assert!(dc.is_negligible(0.01));
147        assert!(!dc.is_negligible(0.0001));
148    }
149
150    #[test]
151    fn test_time_to_reach() {
152        let dc = DecayCounter::with_initial(100.0, 1.0);
153        let t = dc.time_to_reach(50.0).expect("should succeed");
154        assert!((t - 1.0).abs() < 0.1);
155    }
156
157    #[test]
158    fn test_decay_constant() {
159        let dc = DecayCounter::new(1.0);
160        let lambda = dc.decay_constant();
161        assert!((lambda - (2.0_f32).ln()).abs() < 1e-5);
162    }
163}