1use chrono::{DateTime, Datelike, Timelike, Utc};
12use serde::{Deserialize, Serialize};
13use std::collections::HashMap;
14use uuid::Uuid;
15
16#[derive(Debug, Clone, Serialize, Deserialize)]
21pub struct TimeCrystal {
22 pub id: Uuid,
24
25 pub name: String,
27
28 pub period: TemporalPeriod,
30
31 pub occurrences: Vec<TemporalOccurrence>,
33
34 pub distribution: Vec<f32>,
36
37 pub confidence: f32,
39
40 pub created_at: DateTime<Utc>,
42
43 pub updated_at: DateTime<Utc>,
45}
46
47impl TimeCrystal {
48 pub fn new(name: impl Into<String>, period: TemporalPeriod) -> Self {
50 let bins = period.bin_count();
51 let now = Utc::now();
52
53 Self {
54 id: Uuid::new_v4(),
55 name: name.into(),
56 period,
57 occurrences: Vec::new(),
58 distribution: vec![0.0; bins],
59 confidence: 0.0,
60 created_at: now,
61 updated_at: now,
62 }
63 }
64
65 pub fn record(&mut self, timestamp: DateTime<Utc>, value: f32) {
67 let bin = self.period.time_to_bin(×tamp);
68 self.occurrences.push(TemporalOccurrence {
69 timestamp,
70 bin,
71 value,
72 });
73 self.updated_at = Utc::now();
74 self.recompute_distribution();
75 }
76
77 pub fn record_now(&mut self, value: f32) {
79 self.record(Utc::now(), value);
80 }
81
82 pub fn predict(&self, timestamp: &DateTime<Utc>) -> f32 {
84 let bin = self.period.time_to_bin(timestamp);
85 self.distribution.get(bin).copied().unwrap_or(0.0)
86 }
87
88 pub fn predict_now(&self) -> f32 {
90 self.predict(&Utc::now())
91 }
92
93 pub fn best_time(&self) -> usize {
95 self.distribution
96 .iter()
97 .enumerate()
98 .max_by(|(_, a), (_, b)| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal))
99 .map_or(0, |(i, _)| i)
100 }
101
102 pub fn is_anomalous(&self, timestamp: &DateTime<Utc>, value: f32, threshold: f32) -> bool {
104 let expected = self.predict(timestamp);
105 (value - expected).abs() > threshold
106 }
107
108 fn recompute_distribution(&mut self) {
110 let bins = self.period.bin_count();
111 let mut counts = vec![0.0f32; bins];
112 let mut totals = vec![0.0f32; bins];
113
114 for occ in &self.occurrences {
115 if occ.bin < bins {
116 counts[occ.bin] += 1.0;
117 totals[occ.bin] += occ.value;
118 }
119 }
120
121 for i in 0..bins {
123 self.distribution[i] = if counts[i] > 0.0 {
124 totals[i] / counts[i]
125 } else {
126 0.0
127 };
128 }
129
130 let total_samples: f32 = counts.iter().sum();
132 self.confidence = (total_samples / (bins as f32 * 3.0)).min(1.0);
133 }
134
135 pub fn period_description(&self) -> String {
137 match &self.period {
138 TemporalPeriod::Hourly => "hourly (by minute)".to_string(),
139 TemporalPeriod::Daily => "daily (by hour)".to_string(),
140 TemporalPeriod::Weekly => "weekly (by day)".to_string(),
141 TemporalPeriod::Monthly => "monthly (by day)".to_string(),
142 TemporalPeriod::Custom { name, .. } => format!("custom: {}", name),
143 }
144 }
145}
146
147#[derive(Debug, Clone, Serialize, Deserialize)]
149pub struct TemporalOccurrence {
150 pub timestamp: DateTime<Utc>,
152
153 pub bin: usize,
155
156 pub value: f32,
158}
159
160#[derive(Debug, Clone, Serialize, Deserialize)]
162pub enum TemporalPeriod {
163 Hourly,
165
166 Daily,
168
169 Weekly,
171
172 Monthly,
174
175 Custom {
177 name: String,
179 bins: usize,
181 duration_minutes: u64,
183 },
184}
185
186impl TemporalPeriod {
187 pub fn bin_count(&self) -> usize {
189 match self {
190 TemporalPeriod::Hourly => 60,
191 TemporalPeriod::Daily => 24,
192 TemporalPeriod::Weekly => 7,
193 TemporalPeriod::Monthly => 31,
194 TemporalPeriod::Custom { bins, .. } => *bins,
195 }
196 }
197
198 pub fn time_to_bin(&self, timestamp: &DateTime<Utc>) -> usize {
200 match self {
201 TemporalPeriod::Hourly => timestamp.minute() as usize,
202 TemporalPeriod::Daily => timestamp.hour() as usize,
203 TemporalPeriod::Weekly => timestamp.weekday().num_days_from_monday() as usize,
204 TemporalPeriod::Monthly => (timestamp.day() - 1) as usize,
205 TemporalPeriod::Custom {
206 duration_minutes,
207 bins,
208 ..
209 } => {
210 let minutes_since_epoch = timestamp.timestamp() as u64 / 60;
211 ((minutes_since_epoch % duration_minutes) * (*bins as u64) / duration_minutes)
212 as usize
213 }
214 }
215 }
216
217 pub fn bin_label(&self, bin: usize) -> String {
219 match self {
220 TemporalPeriod::Hourly => format!(":{:02}", bin),
221 TemporalPeriod::Daily => format!("{:02}:00", bin),
222 TemporalPeriod::Weekly => {
223 let day = match bin {
224 0 => "Monday",
225 1 => "Tuesday",
226 2 => "Wednesday",
227 3 => "Thursday",
228 4 => "Friday",
229 5 => "Saturday",
230 6 => "Sunday",
231 _ => "Unknown",
232 };
233 day.to_string()
234 }
235 TemporalPeriod::Monthly => format!("Day {}", bin + 1),
236 TemporalPeriod::Custom { name, .. } => format!("{} bin {}", name, bin),
237 }
238 }
239}
240
241pub struct TemporalMemory {
243 crystals: HashMap<String, TimeCrystal>,
244}
245
246impl TemporalMemory {
247 pub fn new() -> Self {
249 Self {
250 crystals: HashMap::new(),
251 }
252 }
253
254 pub fn get_or_create(&mut self, name: &str, period: TemporalPeriod) -> &mut TimeCrystal {
256 self.crystals
257 .entry(name.to_string())
258 .or_insert_with(|| TimeCrystal::new(name, period))
259 }
260
261 pub fn record(&mut self, pattern_name: &str, period: TemporalPeriod, value: f32) {
263 let crystal = self.get_or_create(pattern_name, period);
264 crystal.record_now(value);
265 }
266
267 pub fn predict(&self, pattern_name: &str) -> Option<f32> {
269 self.crystals
270 .get(pattern_name)
271 .map(TimeCrystal::predict_now)
272 }
273
274 pub fn list_patterns(&self) -> Vec<&str> {
276 self.crystals
277 .keys()
278 .map(std::string::String::as_str)
279 .collect()
280 }
281
282 pub fn get(&self, name: &str) -> Option<&TimeCrystal> {
284 self.crystals.get(name)
285 }
286
287 pub fn len(&self) -> usize {
289 self.crystals.len()
290 }
291
292 pub fn is_empty(&self) -> bool {
294 self.crystals.is_empty()
295 }
296}
297
298impl Default for TemporalMemory {
299 fn default() -> Self {
300 Self::new()
301 }
302}
303
304#[cfg(test)]
305mod tests {
306 use super::*;
307 use chrono::Duration;
308
309 #[test]
317 fn test_daily_pattern() {
318 let mut crystal = TimeCrystal::new("coding_activity", TemporalPeriod::Daily);
319
320 for hour in 0..24 {
322 let value = if (9..17).contains(&hour) {
323 1.0 } else if !(6..22).contains(&hour) {
325 0.0 } else {
327 0.3 };
329
330 let timestamp = Utc::now().with_hour(hour).unwrap().with_minute(0).unwrap();
332 crystal.record(timestamp, value);
333 }
334
335 let work_hour = crystal.predict(&Utc::now().with_hour(10).unwrap().with_minute(0).unwrap());
337 let night_hour = crystal.predict(&Utc::now().with_hour(2).unwrap().with_minute(0).unwrap());
338
339 assert!(work_hour > night_hour);
340 assert!(work_hour >= 0.9); let best = crystal.best_time();
344 assert!((9..17).contains(&best));
345 }
346
347 #[test]
354 fn test_weekly_pattern() {
355 let mut crystal = TimeCrystal::new("deploy_activity", TemporalPeriod::Weekly);
356
357 for day in 0..7 {
359 let value = if day == 1 || day == 3 { 1.0 } else { 0.0 };
360 let days_offset = day as i64 - Utc::now().weekday().num_days_from_monday() as i64;
362 let timestamp = Utc::now() + Duration::days(days_offset);
363 crystal.record(timestamp, value);
364 }
365
366 assert!(crystal.distribution[1] > 0.5); assert!(crystal.distribution[3] > 0.5); assert!(crystal.distribution[0] < 0.1); assert!(crystal.distribution[5] < 0.1); }
372
373 #[test]
380 fn test_anomaly_detection() {
381 let mut crystal = TimeCrystal::new("system_load", TemporalPeriod::Daily);
382
383 for _ in 0..10 {
385 for hour in 0..24 {
386 let normal_value = if (9..17).contains(&hour) { 0.8 } else { 0.1 };
387 let timestamp = Utc::now().with_hour(hour).unwrap().with_minute(0).unwrap();
388 crystal.record(timestamp, normal_value);
389 }
390 }
391
392 let night_time = Utc::now().with_hour(3).unwrap();
394 assert!(crystal.is_anomalous(&night_time, 0.9, 0.3));
395
396 let day_time = Utc::now().with_hour(10).unwrap();
398 assert!(!crystal.is_anomalous(&day_time, 0.8, 0.3));
399 }
400
401 #[test]
408 fn test_temporal_memory() {
409 let mut memory = TemporalMemory::new();
410
411 memory.record("coding", TemporalPeriod::Daily, 1.0);
413 memory.record("meetings", TemporalPeriod::Daily, 0.5);
414 memory.record("deploys", TemporalPeriod::Weekly, 1.0);
415
416 assert_eq!(memory.len(), 3);
417 assert!(memory.predict("coding").is_some());
418 assert!(memory.predict("unknown").is_none());
419
420 let patterns = memory.list_patterns();
421 assert!(patterns.contains(&"coding"));
422 assert!(patterns.contains(&"meetings"));
423 assert!(patterns.contains(&"deploys"));
424 }
425}