Skip to main content

smelt_memory/utility/
decay.rs

1//! Exponential decay for time-based utility degradation
2//!
3//! Implements exponential decay to reduce the utility of older episodes,
4//! ensuring more recent experiences are prioritized in retrieval.
5
6use chrono::{DateTime, Utc};
7
8/// Parameters for exponential decay
9#[derive(Debug, Clone)]
10pub struct DecayParams {
11    /// Daily decay rate (0.0 to 1.0)
12    /// A rate of 0.01 gives approximately 69-day half-life
13    pub rate: f64,
14
15    /// Minimum utility floor (prevents complete decay)
16    pub floor: f64,
17}
18
19impl Default for DecayParams {
20    fn default() -> Self {
21        Self {
22            rate: 0.01, // ~69 day half-life
23            floor: 0.1, // 10% minimum utility
24        }
25    }
26}
27
28impl DecayParams {
29    /// Create params with a specific half-life in days
30    pub fn with_half_life(days: f64) -> Self {
31        // decay_factor = 0.5 after `days` days
32        // 0.5 = (1 - rate)^days
33        // rate = 1 - 0.5^(1/days)
34        let rate = 1.0 - 0.5_f64.powf(1.0 / days);
35        Self { rate, floor: 0.1 }
36    }
37
38    /// Set the minimum utility floor
39    pub fn with_floor(mut self, floor: f64) -> Self {
40        self.floor = floor.clamp(0.0, 1.0);
41        self
42    }
43}
44
45/// Apply exponential decay to a utility value
46///
47/// # Arguments
48/// * `utility` - Current utility value
49/// * `created_at` - When the episode was created
50/// * `now` - Current time
51/// * `params` - Decay parameters
52///
53/// # Returns
54/// Decayed utility value (clamped to floor)
55pub fn apply_decay(
56    utility: f64,
57    created_at: DateTime<Utc>,
58    now: DateTime<Utc>,
59    params: &DecayParams,
60) -> f64 {
61    let days_elapsed = (now - created_at).num_seconds() as f64 / 86400.0;
62
63    if days_elapsed <= 0.0 {
64        return utility;
65    }
66
67    // Exponential decay: value * (1 - rate)^days
68    let decay_factor = (1.0 - params.rate).powf(days_elapsed);
69    let decayed = utility * decay_factor;
70
71    // Apply floor
72    decayed.max(params.floor)
73}
74
75/// Calculate the decay factor for a given age
76#[allow(dead_code)]
77pub fn decay_factor(days: f64, params: &DecayParams) -> f64 {
78    if days <= 0.0 {
79        1.0
80    } else {
81        (1.0 - params.rate).powf(days).max(params.floor)
82    }
83}
84
85#[cfg(test)]
86mod tests {
87    use super::*;
88    use chrono::TimeDelta as Duration;
89
90    #[test]
91    fn test_no_decay_for_new() {
92        let params = DecayParams::default();
93        let now = Utc::now();
94        let created = now;
95
96        let decayed = apply_decay(1.0, created, now, &params);
97        assert!((decayed - 1.0).abs() < 0.001);
98    }
99
100    #[test]
101    fn test_decay_over_time() {
102        let params = DecayParams::default();
103        let now = Utc::now();
104        let one_week_ago = now - Duration::days(7);
105        let one_month_ago = now - Duration::days(30);
106
107        let week_decay = apply_decay(1.0, one_week_ago, now, &params);
108        let month_decay = apply_decay(1.0, one_month_ago, now, &params);
109
110        // Older should decay more
111        assert!(week_decay > month_decay);
112        // But both should be less than original
113        assert!(week_decay < 1.0);
114        assert!(month_decay < 1.0);
115    }
116
117    #[test]
118    fn test_floor() {
119        let params = DecayParams {
120            rate: 0.1,
121            floor: 0.2,
122        };
123        let now = Utc::now();
124        let long_ago = now - Duration::days(365);
125
126        let decayed = apply_decay(1.0, long_ago, now, &params);
127        assert!(decayed >= params.floor);
128    }
129
130    #[test]
131    fn test_half_life() {
132        let half_life_days = 30.0;
133        let params = DecayParams::with_half_life(half_life_days).with_floor(0.0);
134        let now = Utc::now();
135        let half_life_ago = now - Duration::days(half_life_days as i64);
136
137        let decayed = apply_decay(1.0, half_life_ago, now, &params);
138        // Should be approximately 0.5 at half-life
139        assert!((decayed - 0.5).abs() < 0.05);
140    }
141
142    #[test]
143    fn test_decay_factor() {
144        let params = DecayParams::default();
145
146        let factor_0 = decay_factor(0.0, &params);
147        let factor_7 = decay_factor(7.0, &params);
148        let factor_30 = decay_factor(30.0, &params);
149
150        assert!((factor_0 - 1.0).abs() < 0.001);
151        assert!(factor_7 < 1.0);
152        assert!(factor_30 < factor_7);
153    }
154
155    #[test]
156    fn test_preserves_relative_utility() {
157        let params = DecayParams::default();
158        let now = Utc::now();
159        let week_ago = now - Duration::days(7);
160
161        let high_utility = apply_decay(1.0, week_ago, now, &params);
162        let low_utility = apply_decay(0.5, week_ago, now, &params);
163
164        // Relative ordering should be preserved
165        assert!(high_utility > low_utility);
166    }
167}