1use chrono::{DateTime, Duration as ChronoDuration, Utc};
4use serde::{Deserialize, Serialize};
5
6use crate::TemporalId;
7
8#[derive(Debug, Clone, Serialize, Deserialize)]
10pub struct DecayModel {
11 pub id: TemporalId,
13
14 pub label: String,
16
17 pub initial_value: f64,
19
20 pub current_value: f64,
22
23 pub reference_time: DateTime<Utc>,
25
26 pub decay_type: DecayType,
28
29 pub floor: f64,
31
32 pub last_calculated: DateTime<Utc>,
34
35 pub tags: Vec<String>,
37}
38
39#[derive(Debug, Clone, Serialize, Deserialize)]
41pub enum DecayType {
42 Linear {
44 rate: f64,
46 },
47
48 Exponential {
50 lambda: f64,
52 },
53
54 HalfLife {
56 half_life_secs: i64,
58 },
59
60 Step {
62 drop_amount: f64,
64 interval_secs: i64,
66 },
67
68 Custom {
70 points: Vec<(i64, f64)>,
72 },
73}
74
75impl DecayModel {
76 pub fn new(label: impl Into<String>, initial_value: f64, decay_type: DecayType) -> Self {
78 let now = Utc::now();
79 Self {
80 id: TemporalId::new(),
81 label: label.into(),
82 initial_value,
83 current_value: initial_value,
84 reference_time: now,
85 decay_type,
86 floor: 0.0,
87 last_calculated: now,
88 tags: Vec::new(),
89 }
90 }
91
92 pub fn calculate_value(&self, at: DateTime<Utc>) -> f64 {
94 let elapsed_secs = (at - self.reference_time).num_seconds() as f64;
95
96 if elapsed_secs < 0.0 {
97 return self.initial_value;
98 }
99
100 let decayed = match &self.decay_type {
101 DecayType::Linear { rate } => self.initial_value - (rate * elapsed_secs),
102 DecayType::Exponential { lambda } => {
103 self.initial_value * (-lambda * elapsed_secs).exp()
104 }
105 DecayType::HalfLife { half_life_secs } => {
106 let half_life = *half_life_secs as f64;
107 self.initial_value * (0.5_f64).powf(elapsed_secs / half_life)
108 }
109 DecayType::Step {
110 drop_amount,
111 interval_secs,
112 } => {
113 let interval = *interval_secs as f64;
114 let intervals = (elapsed_secs / interval).floor();
115 self.initial_value - (drop_amount * intervals)
116 }
117 DecayType::Custom { points } => self.interpolate_custom(elapsed_secs as i64, points),
118 };
119
120 decayed.max(self.floor)
121 }
122
123 fn interpolate_custom(&self, elapsed: i64, points: &[(i64, f64)]) -> f64 {
124 if points.is_empty() {
125 return self.initial_value;
126 }
127
128 let mut prev = (0i64, self.initial_value);
129 for &(t, v) in points {
130 if t > elapsed {
131 let ratio = (elapsed - prev.0) as f64 / (t - prev.0) as f64;
132 return prev.1 + ratio * (v - prev.1);
133 }
134 prev = (t, v);
135 }
136
137 prev.1
138 }
139
140 pub fn update(&mut self) {
142 let now = Utc::now();
143 self.current_value = self.calculate_value(now);
144 self.last_calculated = now;
145 }
146
147 pub fn time_until(&self, threshold: f64) -> Option<ChronoDuration> {
149 if threshold <= self.floor {
150 return None; }
152
153 if self.current_value <= threshold {
154 return Some(ChronoDuration::zero());
155 }
156
157 let secs = match &self.decay_type {
158 DecayType::Linear { rate } => {
159 if *rate <= 0.0 {
160 return None;
161 }
162 (self.initial_value - threshold) / rate
163 }
164 DecayType::Exponential { lambda } => {
165 if *lambda <= 0.0 {
166 return None;
167 }
168 -(threshold / self.initial_value).ln() / lambda
169 }
170 DecayType::HalfLife { half_life_secs } => {
171 let half_life = *half_life_secs as f64;
172 half_life * (threshold / self.initial_value).log2().abs()
173 }
174 _ => return None, };
176
177 Some(ChronoDuration::seconds(secs as i64))
178 }
179
180 pub fn refresh(&mut self) {
182 self.reference_time = Utc::now();
183 self.current_value = self.initial_value;
184 self.last_calculated = self.reference_time;
185 }
186}
187
188#[cfg(test)]
189mod tests {
190 use super::*;
191
192 #[test]
193 fn test_linear_decay() {
194 let d = DecayModel::new("test", 100.0, DecayType::Linear { rate: 1.0 });
195 let future = d.reference_time + ChronoDuration::seconds(50);
196 assert!((d.calculate_value(future) - 50.0).abs() < 0.01);
197 }
198
199 #[test]
200 fn test_floor() {
201 let mut d = DecayModel::new("test", 100.0, DecayType::Linear { rate: 1.0 });
202 d.floor = 10.0;
203 let far_future = d.reference_time + ChronoDuration::seconds(200);
204 assert!((d.calculate_value(far_future) - 10.0).abs() < 0.01);
205 }
206
207 #[test]
208 fn test_exponential_decay() {
209 let d = DecayModel::new("test", 100.0, DecayType::Exponential { lambda: 0.01 });
210 let at = d.reference_time + ChronoDuration::seconds(100);
211 let val = d.calculate_value(at);
213 assert!((val - 36.79).abs() < 1.0);
214 }
215
216 #[test]
217 fn test_half_life_decay() {
218 let d = DecayModel::new(
219 "test",
220 100.0,
221 DecayType::HalfLife {
222 half_life_secs: 3600,
223 },
224 );
225 let at = d.reference_time + ChronoDuration::seconds(3600);
226 let val = d.calculate_value(at);
227 assert!((val - 50.0).abs() < 0.01);
228 }
229}