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