mockforge_core/reality_continuum/
schedule.rs

1//! Time-based scheduling for Reality Continuum
2//!
3//! Provides time-based progression of blend ratios, allowing gradual transition
4//! from mock to real data over a simulated timeline.
5
6use chrono::{DateTime, Duration, Utc};
7use serde::{Deserialize, Serialize};
8
9/// Transition curve type for blend ratio progression
10#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
11#[serde(rename_all = "snake_case")]
12pub enum TransitionCurve {
13    /// Linear progression (constant rate)
14    Linear,
15    /// Exponential progression (slow start, fast end)
16    Exponential,
17    /// Sigmoid progression (slow start and end, fast middle)
18    Sigmoid,
19}
20
21impl Default for TransitionCurve {
22    fn default() -> Self {
23        TransitionCurve::Linear
24    }
25}
26
27/// Time schedule for blend ratio progression
28#[derive(Debug, Clone, Serialize, Deserialize)]
29pub struct TimeSchedule {
30    /// Start time for the transition
31    pub start_time: DateTime<Utc>,
32    /// End time for the transition
33    pub end_time: DateTime<Utc>,
34    /// Initial blend ratio at start time
35    pub start_ratio: f64,
36    /// Final blend ratio at end time
37    pub end_ratio: f64,
38    /// Transition curve type
39    #[serde(default)]
40    pub curve: TransitionCurve,
41}
42
43impl TimeSchedule {
44    /// Create a new time schedule
45    pub fn new(
46        start_time: DateTime<Utc>,
47        end_time: DateTime<Utc>,
48        start_ratio: f64,
49        end_ratio: f64,
50    ) -> Self {
51        Self {
52            start_time,
53            end_time,
54            start_ratio: start_ratio.clamp(0.0, 1.0),
55            end_ratio: end_ratio.clamp(0.0, 1.0),
56            curve: TransitionCurve::Linear,
57        }
58    }
59
60    /// Create a new time schedule with a specific curve
61    pub fn with_curve(
62        start_time: DateTime<Utc>,
63        end_time: DateTime<Utc>,
64        start_ratio: f64,
65        end_ratio: f64,
66        curve: TransitionCurve,
67    ) -> Self {
68        Self {
69            start_time,
70            end_time,
71            start_ratio: start_ratio.clamp(0.0, 1.0),
72            end_ratio: end_ratio.clamp(0.0, 1.0),
73            curve,
74        }
75    }
76
77    /// Calculate the blend ratio at a specific time
78    ///
79    /// Returns the blend ratio based on the current time relative to the schedule.
80    /// If the time is before start_time, returns start_ratio.
81    /// If the time is after end_time, returns end_ratio.
82    /// Otherwise, calculates based on the transition curve.
83    pub fn calculate_ratio(&self, current_time: DateTime<Utc>) -> f64 {
84        // Before start time, return start ratio
85        if current_time < self.start_time {
86            return self.start_ratio;
87        }
88
89        // After end time, return end ratio
90        if current_time > self.end_time {
91            return self.end_ratio;
92        }
93
94        // Calculate progress (0.0 to 1.0)
95        let total_duration = self.end_time - self.start_time;
96        let elapsed = current_time - self.start_time;
97
98        let progress = if total_duration.num_seconds() == 0 {
99            1.0
100        } else {
101            elapsed.num_seconds() as f64 / total_duration.num_seconds() as f64
102        };
103
104        // Apply transition curve
105        let curved_progress = match self.curve {
106            TransitionCurve::Linear => progress,
107            TransitionCurve::Exponential => {
108                // Exponential: e^(k * progress) - 1 / (e^k - 1)
109                // Using k=2 for moderate exponential curve
110                let k = 2.0;
111                (progress * k).exp() - 1.0 / (k.exp() - 1.0)
112            }
113            TransitionCurve::Sigmoid => {
114                // Sigmoid: 1 / (1 + e^(-k * (progress - 0.5)))
115                // Using k=10 for smooth sigmoid curve
116                let k = 10.0;
117                1.0 / (1.0 + (-k * (progress - 0.5)).exp())
118            }
119        };
120
121        // Interpolate between start and end ratio
122        self.start_ratio + (self.end_ratio - self.start_ratio) * curved_progress
123    }
124
125    /// Check if the schedule is active at the given time
126    pub fn is_active(&self, current_time: DateTime<Utc>) -> bool {
127        current_time >= self.start_time && current_time <= self.end_time
128    }
129
130    /// Get the duration of the transition
131    pub fn duration(&self) -> Duration {
132        self.end_time - self.start_time
133    }
134}
135
136#[cfg(test)]
137mod tests {
138    use super::*;
139
140    #[test]
141    fn test_time_schedule_before_start() {
142        let start = Utc::now();
143        let end = start + Duration::days(30);
144        let schedule = TimeSchedule::new(start, end, 0.0, 1.0);
145
146        let before_start = start - Duration::days(1);
147        assert_eq!(schedule.calculate_ratio(before_start), 0.0);
148    }
149
150    #[test]
151    fn test_time_schedule_after_end() {
152        let start = Utc::now();
153        let end = start + Duration::days(30);
154        let schedule = TimeSchedule::new(start, end, 0.0, 1.0);
155
156        let after_end = end + Duration::days(1);
157        assert_eq!(schedule.calculate_ratio(after_end), 1.0);
158    }
159
160    #[test]
161    fn test_time_schedule_linear_midpoint() {
162        let start = Utc::now();
163        let end = start + Duration::days(30);
164        let schedule = TimeSchedule::with_curve(start, end, 0.0, 1.0, TransitionCurve::Linear);
165
166        let midpoint = start + Duration::days(15);
167        let ratio = schedule.calculate_ratio(midpoint);
168        // Should be approximately 0.5 for linear curve at midpoint
169        assert!((ratio - 0.5).abs() < 0.01);
170    }
171
172    #[test]
173    fn test_time_schedule_is_active() {
174        let start = Utc::now();
175        let end = start + Duration::days(30);
176        let schedule = TimeSchedule::new(start, end, 0.0, 1.0);
177
178        assert!(!schedule.is_active(start - Duration::days(1)));
179        assert!(schedule.is_active(start + Duration::days(15)));
180        assert!(!schedule.is_active(end + Duration::days(1)));
181    }
182
183    #[test]
184    fn test_time_schedule_duration() {
185        let start = Utc::now();
186        let end = start + Duration::days(30);
187        let schedule = TimeSchedule::new(start, end, 0.0, 1.0);
188
189        assert_eq!(schedule.duration().num_days(), 30);
190    }
191
192    #[test]
193    fn test_exponential_curve() {
194        let start = Utc::now();
195        let end = start + Duration::days(30);
196        let schedule = TimeSchedule::with_curve(start, end, 0.0, 1.0, TransitionCurve::Exponential);
197
198        let midpoint = start + Duration::days(15);
199        let ratio = schedule.calculate_ratio(midpoint);
200        // Exponential should be less than linear at midpoint (slow start)
201        assert!(ratio < 0.5);
202    }
203
204    #[test]
205    fn test_sigmoid_curve() {
206        let start = Utc::now();
207        let end = start + Duration::days(30);
208        let schedule = TimeSchedule::with_curve(start, end, 0.0, 1.0, TransitionCurve::Sigmoid);
209
210        let midpoint = start + Duration::days(15);
211        let ratio = schedule.calculate_ratio(midpoint);
212        // Sigmoid should be close to 0.5 at midpoint
213        assert!((ratio - 0.5).abs() < 0.1);
214    }
215
216    #[test]
217    fn test_ratio_clamping() {
218        let start = Utc::now();
219        let end = start + Duration::days(30);
220        let schedule = TimeSchedule::new(start, end, -0.5, 1.5);
221
222        // Should be clamped to [0.0, 1.0]
223        assert_eq!(schedule.start_ratio, 0.0);
224        assert_eq!(schedule.end_ratio, 1.0);
225    }
226}