Skip to main content

ipfrs_tensorlogic/
temporal_reasoning.rs

1//! Temporal Reasoning Engine — temporal logic for time-based constraints, intervals, and event ordering.
2//!
3//! Implements Allen's interval algebra (13 relations) for expressive temporal reasoning,
4//! providing event storage, constraint checking, and graph-based chain analysis.
5
6use std::collections::HashMap;
7use thiserror::Error;
8
9// ---------------------------------------------------------------------------
10// Core temporal types
11// ---------------------------------------------------------------------------
12
13/// Unix timestamp in milliseconds.
14#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
15pub struct TimePoint {
16    pub t: i64,
17}
18
19impl TimePoint {
20    /// Create a new `TimePoint` from a millisecond timestamp.
21    pub fn new(t: i64) -> Self {
22        Self { t }
23    }
24}
25
26impl From<i64> for TimePoint {
27    fn from(t: i64) -> Self {
28        Self { t }
29    }
30}
31
32// ---------------------------------------------------------------------------
33// TimeInterval
34// ---------------------------------------------------------------------------
35
36/// A closed time interval `[start, end]` with the invariant `start ≤ end`.
37#[derive(Debug, Clone, PartialEq, Eq)]
38pub struct TimeInterval {
39    pub start: TimePoint,
40    pub end: TimePoint,
41}
42
43impl TimeInterval {
44    /// Construct a new `TimeInterval`, returning an error when `start > end`.
45    pub fn new(start: TimePoint, end: TimePoint) -> Result<Self, TemporalError> {
46        if start > end {
47            return Err(TemporalError::InvalidInterval {
48                start: start.t,
49                end: end.t,
50            });
51        }
52        Ok(Self { start, end })
53    }
54
55    /// Duration in milliseconds.  Always `≥ 0`.
56    pub fn duration_ms(&self) -> u64 {
57        (self.end.t - self.start.t) as u64
58    }
59
60    /// Returns `true` if the two intervals share at least one point (Allen's
61    /// *overlaps*, *starts*, *during*, *finishes*, *equals*, and their inverses
62    /// all count as overlapping in the common sense).  Specifically we test
63    /// `self.start ≤ other.end && other.start ≤ self.end`.
64    pub fn overlaps(&self, other: &TimeInterval) -> bool {
65        self.start <= other.end && other.start <= self.end
66    }
67
68    /// Returns `true` if `t` lies within `[start, end]`.
69    pub fn contains_point(&self, t: &TimePoint) -> bool {
70        self.start <= *t && *t <= self.end
71    }
72
73    /// Allen's *precedes*: `self.end < other.start`.
74    pub fn before(&self, other: &TimeInterval) -> bool {
75        self.end < other.start
76    }
77
78    /// Allen's *preceded-by*: `other.end < self.start`.
79    pub fn after(&self, other: &TimeInterval) -> bool {
80        other.end < self.start
81    }
82
83    /// Allen's *meets*: `self.end == other.start`.
84    pub fn meets(&self, other: &TimeInterval) -> bool {
85        self.end == other.start
86    }
87
88    /// Allen's *during* (proper): `self.start >= other.start && self.end <= other.end`.
89    pub fn during(&self, other: &TimeInterval) -> bool {
90        self.start >= other.start && self.end <= other.end
91    }
92}
93
94// ---------------------------------------------------------------------------
95// Allen's interval algebra — full 13-relation classification
96// ---------------------------------------------------------------------------
97
98/// The 13 relations of Allen's interval algebra.
99#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
100pub enum AllenRelation {
101    /// `a` ends strictly before `b` starts.
102    Precedes,
103    /// `a` ends exactly when `b` starts.
104    Meets,
105    /// `a` starts before `b` and ends inside `b`.
106    Overlaps,
107    /// `a` starts before `b` and ends exactly when `b` ends.
108    FinishedBy,
109    /// `a` starts before `b` starts and ends after `b` ends.
110    Contains,
111    /// `a` and `b` start at the same point; `a` ends before `b`.
112    Starts,
113    /// `a` and `b` are identical.
114    Equals,
115    /// `a` and `b` start at the same point; `b` ends before `a`.
116    StartedBy,
117    /// `a` starts after `b` starts and ends before `b` ends (proper subset).
118    During,
119    /// `a` starts after `b` starts and ends exactly when `b` ends.
120    Finishes,
121    /// `b` starts before `a` and ends inside `a`.
122    OverlappedBy,
123    /// `a` starts exactly when `b` ends.
124    MetBy,
125    /// `a` starts strictly after `b` ends.
126    PrecededBy,
127}
128
129impl AllenRelation {
130    /// Classify two intervals into exactly one Allen relation.
131    pub fn classify(a: &TimeInterval, b: &TimeInterval) -> AllenRelation {
132        let as_ = a.start.t;
133        let ae = a.end.t;
134        let bs = b.start.t;
135        let be = b.end.t;
136
137        if ae < bs {
138            AllenRelation::Precedes
139        } else if ae == bs {
140            AllenRelation::Meets
141        } else if as_ < bs && ae < be {
142            AllenRelation::Overlaps
143        } else if as_ < bs && ae == be {
144            AllenRelation::FinishedBy
145        } else if as_ < bs && ae > be {
146            AllenRelation::Contains
147        } else if as_ == bs && ae < be {
148            AllenRelation::Starts
149        } else if as_ == bs && ae == be {
150            AllenRelation::Equals
151        } else if as_ == bs && ae > be {
152            AllenRelation::StartedBy
153        } else if as_ > bs && ae < be {
154            AllenRelation::During
155        } else if as_ > bs && ae == be {
156            AllenRelation::Finishes
157        } else if as_ > bs && ae > be && as_ < be {
158            AllenRelation::OverlappedBy
159        } else if as_ == be {
160            AllenRelation::MetBy
161        } else {
162            // as_ > be
163            AllenRelation::PrecededBy
164        }
165    }
166
167    /// Returns `true` when the two intervals share any common point under this
168    /// relation (i.e. the relation is *not* a strict precede or strict follow).
169    pub fn is_overlapping_kind(self) -> bool {
170        !matches!(
171            self,
172            AllenRelation::Precedes
173                | AllenRelation::PrecededBy
174                | AllenRelation::Meets
175                | AllenRelation::MetBy
176        )
177    }
178}
179
180// ---------------------------------------------------------------------------
181// TemporalEvent
182// ---------------------------------------------------------------------------
183
184/// A named event occupying a time interval with optional tags and payload.
185#[derive(Debug, Clone, PartialEq, Eq)]
186pub struct TemporalEvent {
187    pub id: String,
188    pub interval: TimeInterval,
189    pub tags: Vec<String>,
190    pub payload: String,
191}
192
193impl TemporalEvent {
194    /// Convenience constructor.
195    pub fn new(
196        id: impl Into<String>,
197        interval: TimeInterval,
198        tags: Vec<String>,
199        payload: impl Into<String>,
200    ) -> Self {
201        Self {
202            id: id.into(),
203            interval,
204            tags,
205            payload: payload.into(),
206        }
207    }
208}
209
210// ---------------------------------------------------------------------------
211// TemporalConstraint
212// ---------------------------------------------------------------------------
213
214/// Constraints that can be enforced across events.
215#[derive(Debug, Clone, PartialEq, Eq)]
216pub enum TemporalConstraint {
217    /// Event `a` must end before event `b` starts.
218    Before { a: String, b: String },
219    /// Event `a` must start after event `b` ends.
220    After { a: String, b: String },
221    /// Events `a` and `b` must share at least one point in time.
222    Overlapping { a: String, b: String },
223    /// Event `inner` must be entirely contained within event `outer`.
224    During { inner: String, outer: String },
225    /// Event `event` must overlap with the given fixed `window`.
226    Within { event: String, window: TimeInterval },
227}
228
229// ---------------------------------------------------------------------------
230// ConstraintViolation
231// ---------------------------------------------------------------------------
232
233/// Describes a single constraint violation.
234#[derive(Debug, Clone)]
235pub struct ConstraintViolation {
236    /// Human-readable description of the violated constraint.
237    pub constraint: String,
238    /// First event involved in the violation.
239    pub event_a: String,
240    /// Second event (may equal `event_a` for unary constraints).
241    pub event_b: String,
242    /// The Allen relation that was actually observed.
243    pub relation: AllenRelation,
244}
245
246// ---------------------------------------------------------------------------
247// TemporalError
248// ---------------------------------------------------------------------------
249
250/// Errors produced by the temporal reasoning engine.
251#[derive(Debug, Error, Clone, PartialEq, Eq)]
252pub enum TemporalError {
253    #[error("event not found: {0}")]
254    EventNotFound(String),
255
256    #[error("duplicate event id: {0}")]
257    DuplicateEventId(String),
258
259    #[error("maximum event capacity reached")]
260    MaxEventsReached,
261
262    #[error("invalid interval: start ({start}) > end ({end})")]
263    InvalidInterval { start: i64, end: i64 },
264}
265
266// ---------------------------------------------------------------------------
267// TemporalStats
268// ---------------------------------------------------------------------------
269
270/// Aggregate statistics about the engine's current state.
271#[derive(Debug, Clone)]
272pub struct TemporalStats {
273    pub event_count: usize,
274    pub constraint_count: usize,
275    /// Span in ms from the earliest event start to the latest event end.
276    /// Zero if there are no events.
277    pub timeline_span_ms: u64,
278    /// Mean event duration in ms.  Zero if there are no events.
279    pub avg_event_duration_ms: f64,
280}
281
282// ---------------------------------------------------------------------------
283// TemporalReasoningEngine
284// ---------------------------------------------------------------------------
285
286/// A production-grade temporal logic reasoning system.
287///
288/// Stores a collection of [`TemporalEvent`]s and a set of [`TemporalConstraint`]s,
289/// and provides rich queries over them including Allen's interval algebra
290/// classification, windowed search, tag-based lookup, chain analysis, and
291/// full constraint checking.
292#[derive(Debug)]
293pub struct TemporalReasoningEngine {
294    events: HashMap<String, TemporalEvent>,
295    constraints: Vec<TemporalConstraint>,
296    max_events: usize,
297}
298
299impl TemporalReasoningEngine {
300    /// Create a new engine with the given event capacity.
301    pub fn new(max_events: usize) -> Self {
302        Self {
303            events: HashMap::new(),
304            constraints: Vec::new(),
305            max_events,
306        }
307    }
308
309    // -----------------------------------------------------------------------
310    // Event management
311    // -----------------------------------------------------------------------
312
313    /// Add an event.  Returns `Err` if the id already exists or if the engine
314    /// is at capacity.
315    pub fn add_event(&mut self, event: TemporalEvent) -> Result<(), TemporalError> {
316        if self.events.contains_key(&event.id) {
317            return Err(TemporalError::DuplicateEventId(event.id.clone()));
318        }
319        if self.events.len() >= self.max_events {
320            return Err(TemporalError::MaxEventsReached);
321        }
322        self.events.insert(event.id.clone(), event);
323        Ok(())
324    }
325
326    /// Remove an event by id.  Returns `true` if it existed.
327    pub fn remove_event(&mut self, id: &str) -> bool {
328        self.events.remove(id).is_some()
329    }
330
331    /// Retrieve a reference to an event by id.
332    pub fn get_event(&self, id: &str) -> Option<&TemporalEvent> {
333        self.events.get(id)
334    }
335
336    // -----------------------------------------------------------------------
337    // Temporal queries
338    // -----------------------------------------------------------------------
339
340    /// Return all events whose interval overlaps `window`, sorted by start time
341    /// (then end time as tiebreaker).
342    pub fn events_in_window(&self, window: &TimeInterval) -> Vec<&TemporalEvent> {
343        let mut result: Vec<&TemporalEvent> = self
344            .events
345            .values()
346            .filter(|e| e.interval.overlaps(window))
347            .collect();
348        result.sort_by(|a, b| {
349            a.interval
350                .start
351                .cmp(&b.interval.start)
352                .then_with(|| a.interval.end.cmp(&b.interval.end))
353        });
354        result
355    }
356
357    /// Return all events that carry `tag`, sorted by start time.
358    pub fn events_with_tag(&self, tag: &str) -> Vec<&TemporalEvent> {
359        let mut result: Vec<&TemporalEvent> = self
360            .events
361            .values()
362            .filter(|e| e.tags.iter().any(|t| t == tag))
363            .collect();
364        result.sort_by(|a, b| {
365            a.interval
366                .start
367                .cmp(&b.interval.start)
368                .then_with(|| a.interval.end.cmp(&b.interval.end))
369        });
370        result
371    }
372
373    /// Return all events sorted chronologically (by start, then end).
374    pub fn timeline(&self) -> Vec<&TemporalEvent> {
375        let mut events: Vec<&TemporalEvent> = self.events.values().collect();
376        events.sort_by(|a, b| {
377            a.interval
378                .start
379                .cmp(&b.interval.start)
380                .then_with(|| a.interval.end.cmp(&b.interval.end))
381        });
382        events
383    }
384
385    /// Return all events that overlap with the event identified by `id`,
386    /// excluding the event itself.  Returns an empty `Vec` if `id` not found.
387    pub fn concurrent_events(&self, id: &str) -> Vec<&TemporalEvent> {
388        let target = match self.events.get(id) {
389            Some(e) => e,
390            None => return Vec::new(),
391        };
392        let mut result: Vec<&TemporalEvent> = self
393            .events
394            .values()
395            .filter(|e| e.id != id && e.interval.overlaps(&target.interval))
396            .collect();
397        result.sort_by(|a, b| {
398            a.interval
399                .start
400                .cmp(&b.interval.start)
401                .then_with(|| a.interval.end.cmp(&b.interval.end))
402        });
403        result
404    }
405
406    // -----------------------------------------------------------------------
407    // Allen's relation
408    // -----------------------------------------------------------------------
409
410    /// Classify the Allen relation between two events.  Returns `None` if
411    /// either event is not found.
412    pub fn allen_relation(&self, a: &str, b: &str) -> Option<AllenRelation> {
413        let ea = self.events.get(a)?;
414        let eb = self.events.get(b)?;
415        Some(AllenRelation::classify(&ea.interval, &eb.interval))
416    }
417
418    // -----------------------------------------------------------------------
419    // Constraints
420    // -----------------------------------------------------------------------
421
422    /// Register a constraint.  Constraints are not validated on insertion;
423    /// call [`check_constraints`](TemporalReasoningEngine::check_constraints)
424    /// to evaluate them.
425    pub fn add_constraint(&mut self, c: TemporalConstraint) {
426        self.constraints.push(c);
427    }
428
429    /// Evaluate all registered constraints and return a list of violations.
430    pub fn check_constraints(&self) -> Vec<ConstraintViolation> {
431        let mut violations = Vec::new();
432        for constraint in &self.constraints {
433            match constraint {
434                TemporalConstraint::Before { a, b } => {
435                    self.check_before(a, b, &mut violations);
436                }
437                TemporalConstraint::After { a, b } => {
438                    self.check_after(a, b, &mut violations);
439                }
440                TemporalConstraint::Overlapping { a, b } => {
441                    self.check_overlapping(a, b, &mut violations);
442                }
443                TemporalConstraint::During { inner, outer } => {
444                    self.check_during(inner, outer, &mut violations);
445                }
446                TemporalConstraint::Within { event, window } => {
447                    self.check_within(event, window, &mut violations);
448                }
449            }
450        }
451        violations
452    }
453
454    // -----------------------------------------------------------------------
455    // Event chains (connected components via overlap)
456    // -----------------------------------------------------------------------
457
458    /// Compute connected components of the overlap graph.
459    ///
460    /// Two events are in the same component if they overlap (directly or
461    /// transitively).  Returns chains sorted by the start time of their
462    /// first event; within each chain events are also sorted by start time.
463    pub fn event_chains(&self) -> Vec<Vec<&TemporalEvent>> {
464        // Collect ids in a stable order for BFS.
465        let ids: Vec<&str> = self.events.keys().map(String::as_str).collect();
466        let n = ids.len();
467        if n == 0 {
468            return Vec::new();
469        }
470
471        // Build an id → index map.
472        let idx: HashMap<&str, usize> = ids.iter().enumerate().map(|(i, &id)| (id, i)).collect();
473
474        // Adjacency list — symmetric overlap.
475        let mut adj: Vec<Vec<usize>> = vec![Vec::new(); n];
476        for i in 0..n {
477            let ei = self.events.get(ids[i]).expect("known key");
478            for j in (i + 1)..n {
479                let ej = self.events.get(ids[j]).expect("known key");
480                if ei.interval.overlaps(&ej.interval) {
481                    adj[i].push(j);
482                    adj[j].push(i);
483                }
484            }
485        }
486
487        // BFS to find components.
488        let mut visited = vec![false; n];
489        let mut components: Vec<Vec<usize>> = Vec::new();
490
491        for start in 0..n {
492            if visited[start] {
493                continue;
494            }
495            let mut component = Vec::new();
496            let mut queue = std::collections::VecDeque::new();
497            queue.push_back(start);
498            visited[start] = true;
499            while let Some(cur) = queue.pop_front() {
500                component.push(cur);
501                for &nb in &adj[cur] {
502                    if !visited[nb] {
503                        visited[nb] = true;
504                        queue.push_back(nb);
505                    }
506                }
507            }
508            components.push(component);
509        }
510
511        // Build result: sort within each component by start time, then sort
512        // components by the start time of their first event.
513        let sort_key = |idx_val: usize| -> (i64, i64) {
514            let e = self.events.get(ids[idx_val]).expect("known key");
515            (e.interval.start.t, e.interval.end.t)
516        };
517
518        let mut chains: Vec<Vec<&TemporalEvent>> = components
519            .into_iter()
520            .map(|mut comp| {
521                comp.sort_by_key(|&i| sort_key(i));
522                comp.iter()
523                    .map(|&i| self.events.get(ids[i]).expect("known key"))
524                    .collect()
525            })
526            .collect();
527
528        chains.sort_by(|a, b| {
529            let ka = a
530                .first()
531                .map(|e| (e.interval.start.t, e.interval.end.t))
532                .unwrap_or((i64::MAX, i64::MAX));
533            let kb = b
534                .first()
535                .map(|e| (e.interval.start.t, e.interval.end.t))
536                .unwrap_or((i64::MAX, i64::MAX));
537            ka.cmp(&kb)
538        });
539
540        // Drop the idx map — it was only used inside idx for the adj build.
541        let _ = idx;
542
543        chains
544    }
545
546    // -----------------------------------------------------------------------
547    // Statistics
548    // -----------------------------------------------------------------------
549
550    /// Compute aggregate statistics about the current engine state.
551    pub fn stats(&self) -> TemporalStats {
552        let event_count = self.events.len();
553        let constraint_count = self.constraints.len();
554
555        if event_count == 0 {
556            return TemporalStats {
557                event_count: 0,
558                constraint_count,
559                timeline_span_ms: 0,
560                avg_event_duration_ms: 0.0,
561            };
562        }
563
564        let mut earliest = i64::MAX;
565        let mut latest = i64::MIN;
566        let mut total_duration: u64 = 0;
567
568        for e in self.events.values() {
569            if e.interval.start.t < earliest {
570                earliest = e.interval.start.t;
571            }
572            if e.interval.end.t > latest {
573                latest = e.interval.end.t;
574            }
575            total_duration += e.interval.duration_ms();
576        }
577
578        let timeline_span_ms = if latest >= earliest {
579            (latest - earliest) as u64
580        } else {
581            0
582        };
583
584        let avg_event_duration_ms = total_duration as f64 / event_count as f64;
585
586        TemporalStats {
587            event_count,
588            constraint_count,
589            timeline_span_ms,
590            avg_event_duration_ms,
591        }
592    }
593
594    // -----------------------------------------------------------------------
595    // Private helpers for constraint checking
596    // -----------------------------------------------------------------------
597
598    fn check_before(&self, a: &str, b: &str, violations: &mut Vec<ConstraintViolation>) {
599        let (ea, eb) = match (self.events.get(a), self.events.get(b)) {
600            (Some(x), Some(y)) => (x, y),
601            _ => return,
602        };
603        let rel = AllenRelation::classify(&ea.interval, &eb.interval);
604        // Satisfied only by Precedes (strict before).
605        if rel != AllenRelation::Precedes {
606            violations.push(ConstraintViolation {
607                constraint: format!("Before({a}, {b})"),
608                event_a: a.to_owned(),
609                event_b: b.to_owned(),
610                relation: rel,
611            });
612        }
613    }
614
615    fn check_after(&self, a: &str, b: &str, violations: &mut Vec<ConstraintViolation>) {
616        let (ea, eb) = match (self.events.get(a), self.events.get(b)) {
617            (Some(x), Some(y)) => (x, y),
618            _ => return,
619        };
620        let rel = AllenRelation::classify(&ea.interval, &eb.interval);
621        // Satisfied only by PrecededBy.
622        if rel != AllenRelation::PrecededBy {
623            violations.push(ConstraintViolation {
624                constraint: format!("After({a}, {b})"),
625                event_a: a.to_owned(),
626                event_b: b.to_owned(),
627                relation: rel,
628            });
629        }
630    }
631
632    fn check_overlapping(&self, a: &str, b: &str, violations: &mut Vec<ConstraintViolation>) {
633        let (ea, eb) = match (self.events.get(a), self.events.get(b)) {
634            (Some(x), Some(y)) => (x, y),
635            _ => return,
636        };
637        let rel = AllenRelation::classify(&ea.interval, &eb.interval);
638        if !ea.interval.overlaps(&eb.interval) {
639            violations.push(ConstraintViolation {
640                constraint: format!("Overlapping({a}, {b})"),
641                event_a: a.to_owned(),
642                event_b: b.to_owned(),
643                relation: rel,
644            });
645        }
646    }
647
648    fn check_during(&self, inner: &str, outer: &str, violations: &mut Vec<ConstraintViolation>) {
649        let (ei, eo) = match (self.events.get(inner), self.events.get(outer)) {
650            (Some(x), Some(y)) => (x, y),
651            _ => return,
652        };
653        let rel = AllenRelation::classify(&ei.interval, &eo.interval);
654        // Satisfied by During, Starts, Finishes, Equals (inner fully inside outer).
655        let satisfied = matches!(
656            rel,
657            AllenRelation::During
658                | AllenRelation::Starts
659                | AllenRelation::Finishes
660                | AllenRelation::Equals
661        );
662        if !satisfied {
663            violations.push(ConstraintViolation {
664                constraint: format!("During(inner={inner}, outer={outer})"),
665                event_a: inner.to_owned(),
666                event_b: outer.to_owned(),
667                relation: rel,
668            });
669        }
670    }
671
672    fn check_within(
673        &self,
674        event_id: &str,
675        window: &TimeInterval,
676        violations: &mut Vec<ConstraintViolation>,
677    ) {
678        let ev = match self.events.get(event_id) {
679            Some(e) => e,
680            None => return,
681        };
682        // Synthesise a virtual "window" event for Allen classification.
683        let window_event = TemporalEvent::new("__window__", window.clone(), Vec::new(), "");
684        let rel = AllenRelation::classify(&ev.interval, &window_event.interval);
685        if !ev.interval.overlaps(window) {
686            violations.push(ConstraintViolation {
687                constraint: format!("Within({event_id}, window)"),
688                event_a: event_id.to_owned(),
689                event_b: "__window__".to_owned(),
690                relation: rel,
691            });
692        }
693    }
694}
695
696// ---------------------------------------------------------------------------
697// Tests
698// ---------------------------------------------------------------------------
699
700#[cfg(test)]
701mod tests {
702    use crate::temporal_reasoning::{
703        AllenRelation, ConstraintViolation, TemporalConstraint, TemporalError, TemporalEvent,
704        TemporalReasoningEngine, TimeInterval, TimePoint,
705    };
706
707    // ------------------------------------------------------------------
708    // Helpers
709    // ------------------------------------------------------------------
710
711    fn tp(t: i64) -> TimePoint {
712        TimePoint::new(t)
713    }
714
715    fn iv(s: i64, e: i64) -> TimeInterval {
716        TimeInterval::new(tp(s), tp(e)).expect("valid interval in test helper")
717    }
718
719    fn ev(id: &str, s: i64, e: i64) -> TemporalEvent {
720        TemporalEvent::new(id, iv(s, e), Vec::new(), "")
721    }
722
723    fn ev_tagged(id: &str, s: i64, e: i64, tags: &[&str]) -> TemporalEvent {
724        TemporalEvent::new(
725            id,
726            iv(s, e),
727            tags.iter().map(|t| t.to_string()).collect(),
728            "",
729        )
730    }
731
732    // ------------------------------------------------------------------
733    // TimePoint
734    // ------------------------------------------------------------------
735
736    #[test]
737    fn test_time_point_ordering() {
738        assert!(tp(10) < tp(20));
739        assert!(tp(20) > tp(10));
740        assert_eq!(tp(5), tp(5));
741    }
742
743    #[test]
744    fn test_time_point_from_i64() {
745        let p: TimePoint = 42_i64.into();
746        assert_eq!(p.t, 42);
747    }
748
749    // ------------------------------------------------------------------
750    // TimeInterval construction & invariant
751    // ------------------------------------------------------------------
752
753    #[test]
754    fn test_interval_valid() {
755        let i = iv(0, 100);
756        assert_eq!(i.duration_ms(), 100);
757    }
758
759    #[test]
760    fn test_interval_invalid_returns_error() {
761        let result = TimeInterval::new(tp(100), tp(0));
762        assert_eq!(
763            result,
764            Err(TemporalError::InvalidInterval { start: 100, end: 0 })
765        );
766    }
767
768    #[test]
769    fn test_interval_zero_duration() {
770        let i = iv(50, 50);
771        assert_eq!(i.duration_ms(), 0);
772    }
773
774    // ------------------------------------------------------------------
775    // TimeInterval predicates
776    // ------------------------------------------------------------------
777
778    #[test]
779    fn test_contains_point() {
780        let i = iv(10, 20);
781        assert!(i.contains_point(&tp(10)));
782        assert!(i.contains_point(&tp(15)));
783        assert!(i.contains_point(&tp(20)));
784        assert!(!i.contains_point(&tp(9)));
785        assert!(!i.contains_point(&tp(21)));
786    }
787
788    #[test]
789    fn test_before_after() {
790        let a = iv(0, 10);
791        let b = iv(20, 30);
792        assert!(a.before(&b));
793        assert!(b.after(&a));
794        assert!(!b.before(&a));
795        assert!(!a.after(&b));
796    }
797
798    #[test]
799    fn test_meets() {
800        let a = iv(0, 10);
801        let b = iv(10, 20);
802        assert!(a.meets(&b));
803        assert!(!b.meets(&a));
804    }
805
806    #[test]
807    fn test_during() {
808        let outer = iv(0, 100);
809        let inner = iv(20, 80);
810        assert!(inner.during(&outer));
811        assert!(!outer.during(&inner));
812    }
813
814    #[test]
815    fn test_overlaps_various() {
816        assert!(iv(0, 10).overlaps(&iv(5, 15)));
817        assert!(iv(5, 15).overlaps(&iv(0, 10)));
818        assert!(iv(0, 10).overlaps(&iv(10, 20))); // touching = overlapping in common sense
819        assert!(!iv(0, 10).overlaps(&iv(11, 20)));
820    }
821
822    // ------------------------------------------------------------------
823    // Allen's interval algebra
824    // ------------------------------------------------------------------
825
826    #[test]
827    fn test_allen_precedes() {
828        let rel = AllenRelation::classify(&iv(0, 5), &iv(10, 20));
829        assert_eq!(rel, AllenRelation::Precedes);
830    }
831
832    #[test]
833    fn test_allen_preceded_by() {
834        let rel = AllenRelation::classify(&iv(10, 20), &iv(0, 5));
835        assert_eq!(rel, AllenRelation::PrecededBy);
836    }
837
838    #[test]
839    fn test_allen_meets() {
840        let rel = AllenRelation::classify(&iv(0, 10), &iv(10, 20));
841        assert_eq!(rel, AllenRelation::Meets);
842    }
843
844    #[test]
845    fn test_allen_met_by() {
846        let rel = AllenRelation::classify(&iv(10, 20), &iv(0, 10));
847        assert_eq!(rel, AllenRelation::MetBy);
848    }
849
850    #[test]
851    fn test_allen_overlaps() {
852        let rel = AllenRelation::classify(&iv(0, 15), &iv(10, 25));
853        assert_eq!(rel, AllenRelation::Overlaps);
854    }
855
856    #[test]
857    fn test_allen_overlapped_by() {
858        let rel = AllenRelation::classify(&iv(10, 25), &iv(0, 15));
859        assert_eq!(rel, AllenRelation::OverlappedBy);
860    }
861
862    #[test]
863    fn test_allen_starts() {
864        let rel = AllenRelation::classify(&iv(0, 10), &iv(0, 20));
865        assert_eq!(rel, AllenRelation::Starts);
866    }
867
868    #[test]
869    fn test_allen_started_by() {
870        let rel = AllenRelation::classify(&iv(0, 20), &iv(0, 10));
871        assert_eq!(rel, AllenRelation::StartedBy);
872    }
873
874    #[test]
875    fn test_allen_finishes() {
876        let rel = AllenRelation::classify(&iv(10, 20), &iv(0, 20));
877        assert_eq!(rel, AllenRelation::Finishes);
878    }
879
880    #[test]
881    fn test_allen_finished_by() {
882        let rel = AllenRelation::classify(&iv(0, 20), &iv(10, 20));
883        assert_eq!(rel, AllenRelation::FinishedBy);
884    }
885
886    #[test]
887    fn test_allen_during() {
888        let rel = AllenRelation::classify(&iv(10, 15), &iv(0, 20));
889        assert_eq!(rel, AllenRelation::During);
890    }
891
892    #[test]
893    fn test_allen_contains() {
894        let rel = AllenRelation::classify(&iv(0, 20), &iv(10, 15));
895        assert_eq!(rel, AllenRelation::Contains);
896    }
897
898    #[test]
899    fn test_allen_equals() {
900        let rel = AllenRelation::classify(&iv(5, 15), &iv(5, 15));
901        assert_eq!(rel, AllenRelation::Equals);
902    }
903
904    #[test]
905    fn test_allen_overlapping_kind_filter() {
906        // Overlapping relations
907        for rel in [
908            AllenRelation::Overlaps,
909            AllenRelation::FinishedBy,
910            AllenRelation::Contains,
911            AllenRelation::Starts,
912            AllenRelation::Equals,
913            AllenRelation::StartedBy,
914            AllenRelation::During,
915            AllenRelation::Finishes,
916            AllenRelation::OverlappedBy,
917        ] {
918            assert!(rel.is_overlapping_kind(), "{rel:?} should be overlapping");
919        }
920        // Non-overlapping relations
921        for rel in [
922            AllenRelation::Precedes,
923            AllenRelation::PrecededBy,
924            AllenRelation::Meets,
925            AllenRelation::MetBy,
926        ] {
927            assert!(
928                !rel.is_overlapping_kind(),
929                "{rel:?} should not be overlapping"
930            );
931        }
932    }
933
934    // ------------------------------------------------------------------
935    // Engine: basic event CRUD
936    // ------------------------------------------------------------------
937
938    #[test]
939    fn test_add_and_get_event() {
940        let mut engine = TemporalReasoningEngine::new(100);
941        engine
942            .add_event(ev("e1", 0, 100))
943            .expect("test: should succeed");
944        let e = engine.get_event("e1").expect("test: should succeed");
945        assert_eq!(e.id, "e1");
946    }
947
948    #[test]
949    fn test_duplicate_event_error() {
950        let mut engine = TemporalReasoningEngine::new(100);
951        engine
952            .add_event(ev("e1", 0, 100))
953            .expect("test: should succeed");
954        let res = engine.add_event(ev("e1", 50, 150));
955        assert_eq!(res, Err(TemporalError::DuplicateEventId("e1".to_owned())));
956    }
957
958    #[test]
959    fn test_max_events_error() {
960        let mut engine = TemporalReasoningEngine::new(2);
961        engine
962            .add_event(ev("e1", 0, 10))
963            .expect("test: should succeed");
964        engine
965            .add_event(ev("e2", 20, 30))
966            .expect("test: should succeed");
967        let res = engine.add_event(ev("e3", 40, 50));
968        assert_eq!(res, Err(TemporalError::MaxEventsReached));
969    }
970
971    #[test]
972    fn test_remove_event() {
973        let mut engine = TemporalReasoningEngine::new(100);
974        engine
975            .add_event(ev("e1", 0, 100))
976            .expect("test: should succeed");
977        assert!(engine.remove_event("e1"));
978        assert!(!engine.remove_event("e1"));
979        assert!(engine.get_event("e1").is_none());
980    }
981
982    // ------------------------------------------------------------------
983    // Engine: queries
984    // ------------------------------------------------------------------
985
986    #[test]
987    fn test_events_in_window() {
988        let mut engine = TemporalReasoningEngine::new(100);
989        engine
990            .add_event(ev("a", 0, 10))
991            .expect("test: should succeed"); // before window
992        engine
993            .add_event(ev("b", 5, 15))
994            .expect("test: should succeed"); // overlaps
995        engine
996            .add_event(ev("c", 12, 20))
997            .expect("test: should succeed"); // inside window
998        engine
999            .add_event(ev("d", 19, 30))
1000            .expect("test: should succeed"); // overlaps end
1001        engine
1002            .add_event(ev("e", 25, 35))
1003            .expect("test: should succeed"); // after window
1004        let window = iv(11, 22);
1005        let result = engine.events_in_window(&window);
1006        let ids: Vec<&str> = result.iter().map(|e| e.id.as_str()).collect();
1007        assert!(ids.contains(&"b"), "b should overlap window [11,22]");
1008        assert!(ids.contains(&"c"), "c should be inside window");
1009        assert!(ids.contains(&"d"), "d should overlap window end");
1010        assert!(!ids.contains(&"a"), "a should not overlap window");
1011        assert!(!ids.contains(&"e"), "e should not overlap window");
1012        // Sorted by start
1013        for i in 1..result.len() {
1014            assert!(result[i - 1].interval.start <= result[i].interval.start);
1015        }
1016    }
1017
1018    #[test]
1019    fn test_events_with_tag() {
1020        let mut engine = TemporalReasoningEngine::new(100);
1021        engine
1022            .add_event(ev_tagged("a", 0, 10, &["foo", "bar"]))
1023            .expect("test: should succeed");
1024        engine
1025            .add_event(ev_tagged("b", 5, 15, &["bar"]))
1026            .expect("test: should succeed");
1027        engine
1028            .add_event(ev("c", 20, 30))
1029            .expect("test: should succeed");
1030
1031        let foo = engine.events_with_tag("foo");
1032        assert_eq!(foo.len(), 1);
1033        assert_eq!(foo[0].id, "a");
1034
1035        let bar = engine.events_with_tag("bar");
1036        assert_eq!(bar.len(), 2);
1037        assert_eq!(bar[0].id, "a");
1038        assert_eq!(bar[1].id, "b");
1039    }
1040
1041    #[test]
1042    fn test_timeline_sorted() {
1043        let mut engine = TemporalReasoningEngine::new(100);
1044        engine
1045            .add_event(ev("c", 50, 70))
1046            .expect("test: should succeed");
1047        engine
1048            .add_event(ev("a", 0, 20))
1049            .expect("test: should succeed");
1050        engine
1051            .add_event(ev("b", 10, 40))
1052            .expect("test: should succeed");
1053        let tl = engine.timeline();
1054        assert_eq!(tl[0].id, "a");
1055        assert_eq!(tl[1].id, "b");
1056        assert_eq!(tl[2].id, "c");
1057    }
1058
1059    #[test]
1060    fn test_concurrent_events() {
1061        let mut engine = TemporalReasoningEngine::new(100);
1062        engine
1063            .add_event(ev("a", 0, 50))
1064            .expect("test: should succeed");
1065        engine
1066            .add_event(ev("b", 30, 80))
1067            .expect("test: should succeed");
1068        engine
1069            .add_event(ev("c", 60, 100))
1070            .expect("test: should succeed");
1071        engine
1072            .add_event(ev("d", 200, 300))
1073            .expect("test: should succeed");
1074
1075        let conc = engine.concurrent_events("a");
1076        let ids: Vec<&str> = conc.iter().map(|e| e.id.as_str()).collect();
1077        assert!(ids.contains(&"b"));
1078        assert!(!ids.contains(&"c"));
1079        assert!(!ids.contains(&"d"));
1080        assert!(!ids.contains(&"a")); // self excluded
1081    }
1082
1083    #[test]
1084    fn test_concurrent_events_unknown_id() {
1085        let engine = TemporalReasoningEngine::new(100);
1086        assert!(engine.concurrent_events("nonexistent").is_empty());
1087    }
1088
1089    #[test]
1090    fn test_allen_relation_via_engine() {
1091        let mut engine = TemporalReasoningEngine::new(100);
1092        engine
1093            .add_event(ev("a", 0, 10))
1094            .expect("test: should succeed");
1095        engine
1096            .add_event(ev("b", 20, 30))
1097            .expect("test: should succeed");
1098        assert_eq!(
1099            engine.allen_relation("a", "b"),
1100            Some(AllenRelation::Precedes)
1101        );
1102        assert_eq!(engine.allen_relation("a", "missing"), None);
1103    }
1104
1105    // ------------------------------------------------------------------
1106    // Constraints
1107    // ------------------------------------------------------------------
1108
1109    #[test]
1110    fn test_constraint_before_satisfied() {
1111        let mut engine = TemporalReasoningEngine::new(100);
1112        engine
1113            .add_event(ev("a", 0, 10))
1114            .expect("test: should succeed");
1115        engine
1116            .add_event(ev("b", 20, 30))
1117            .expect("test: should succeed");
1118        engine.add_constraint(TemporalConstraint::Before {
1119            a: "a".into(),
1120            b: "b".into(),
1121        });
1122        assert!(engine.check_constraints().is_empty());
1123    }
1124
1125    #[test]
1126    fn test_constraint_before_violated() {
1127        let mut engine = TemporalReasoningEngine::new(100);
1128        engine
1129            .add_event(ev("a", 0, 20))
1130            .expect("test: should succeed"); // overlaps b
1131        engine
1132            .add_event(ev("b", 10, 30))
1133            .expect("test: should succeed");
1134        engine.add_constraint(TemporalConstraint::Before {
1135            a: "a".into(),
1136            b: "b".into(),
1137        });
1138        let violations = engine.check_constraints();
1139        assert!(!violations.is_empty());
1140        assert_eq!(violations[0].event_a, "a");
1141    }
1142
1143    #[test]
1144    fn test_constraint_after_satisfied() {
1145        let mut engine = TemporalReasoningEngine::new(100);
1146        engine
1147            .add_event(ev("a", 50, 100))
1148            .expect("test: should succeed");
1149        engine
1150            .add_event(ev("b", 0, 30))
1151            .expect("test: should succeed");
1152        engine.add_constraint(TemporalConstraint::After {
1153            a: "a".into(),
1154            b: "b".into(),
1155        });
1156        assert!(engine.check_constraints().is_empty());
1157    }
1158
1159    #[test]
1160    fn test_constraint_after_violated() {
1161        let mut engine = TemporalReasoningEngine::new(100);
1162        engine
1163            .add_event(ev("a", 0, 20))
1164            .expect("test: should succeed");
1165        engine
1166            .add_event(ev("b", 0, 30))
1167            .expect("test: should succeed");
1168        engine.add_constraint(TemporalConstraint::After {
1169            a: "a".into(),
1170            b: "b".into(),
1171        });
1172        let violations = engine.check_constraints();
1173        assert!(!violations.is_empty());
1174    }
1175
1176    #[test]
1177    fn test_constraint_overlapping_satisfied() {
1178        let mut engine = TemporalReasoningEngine::new(100);
1179        engine
1180            .add_event(ev("a", 0, 20))
1181            .expect("test: should succeed");
1182        engine
1183            .add_event(ev("b", 10, 30))
1184            .expect("test: should succeed");
1185        engine.add_constraint(TemporalConstraint::Overlapping {
1186            a: "a".into(),
1187            b: "b".into(),
1188        });
1189        assert!(engine.check_constraints().is_empty());
1190    }
1191
1192    #[test]
1193    fn test_constraint_overlapping_violated() {
1194        let mut engine = TemporalReasoningEngine::new(100);
1195        engine
1196            .add_event(ev("a", 0, 5))
1197            .expect("test: should succeed");
1198        engine
1199            .add_event(ev("b", 10, 20))
1200            .expect("test: should succeed");
1201        engine.add_constraint(TemporalConstraint::Overlapping {
1202            a: "a".into(),
1203            b: "b".into(),
1204        });
1205        let violations = engine.check_constraints();
1206        assert!(!violations.is_empty());
1207    }
1208
1209    #[test]
1210    fn test_constraint_during_satisfied() {
1211        let mut engine = TemporalReasoningEngine::new(100);
1212        engine
1213            .add_event(ev("inner", 10, 20))
1214            .expect("test: should succeed");
1215        engine
1216            .add_event(ev("outer", 0, 50))
1217            .expect("test: should succeed");
1218        engine.add_constraint(TemporalConstraint::During {
1219            inner: "inner".into(),
1220            outer: "outer".into(),
1221        });
1222        assert!(engine.check_constraints().is_empty());
1223    }
1224
1225    #[test]
1226    fn test_constraint_during_violated() {
1227        let mut engine = TemporalReasoningEngine::new(100);
1228        engine
1229            .add_event(ev("inner", 0, 100))
1230            .expect("test: should succeed");
1231        engine
1232            .add_event(ev("outer", 10, 50))
1233            .expect("test: should succeed");
1234        engine.add_constraint(TemporalConstraint::During {
1235            inner: "inner".into(),
1236            outer: "outer".into(),
1237        });
1238        let violations = engine.check_constraints();
1239        assert!(!violations.is_empty());
1240    }
1241
1242    #[test]
1243    fn test_constraint_within_satisfied() {
1244        let mut engine = TemporalReasoningEngine::new(100);
1245        engine
1246            .add_event(ev("e", 10, 20))
1247            .expect("test: should succeed");
1248        let window = iv(0, 50);
1249        engine.add_constraint(TemporalConstraint::Within {
1250            event: "e".into(),
1251            window,
1252        });
1253        assert!(engine.check_constraints().is_empty());
1254    }
1255
1256    #[test]
1257    fn test_constraint_within_violated() {
1258        let mut engine = TemporalReasoningEngine::new(100);
1259        engine
1260            .add_event(ev("e", 100, 200))
1261            .expect("test: should succeed");
1262        let window = iv(0, 50);
1263        engine.add_constraint(TemporalConstraint::Within {
1264            event: "e".into(),
1265            window,
1266        });
1267        let violations = engine.check_constraints();
1268        assert!(!violations.is_empty());
1269        assert_eq!(violations[0].event_a, "e");
1270    }
1271
1272    #[test]
1273    fn test_multiple_constraints_mixed() {
1274        let mut engine = TemporalReasoningEngine::new(100);
1275        engine
1276            .add_event(ev("a", 0, 10))
1277            .expect("test: should succeed");
1278        engine
1279            .add_event(ev("b", 20, 30))
1280            .expect("test: should succeed");
1281        engine
1282            .add_event(ev("c", 5, 15))
1283            .expect("test: should succeed");
1284        // a before b — satisfied
1285        engine.add_constraint(TemporalConstraint::Before {
1286            a: "a".into(),
1287            b: "b".into(),
1288        });
1289        // a before c — violated (a overlaps c)
1290        engine.add_constraint(TemporalConstraint::Before {
1291            a: "a".into(),
1292            b: "c".into(),
1293        });
1294        let violations = engine.check_constraints();
1295        assert_eq!(violations.len(), 1);
1296        assert_eq!(violations[0].event_b, "c");
1297    }
1298
1299    // ------------------------------------------------------------------
1300    // Event chains
1301    // ------------------------------------------------------------------
1302
1303    #[test]
1304    fn test_event_chains_empty() {
1305        let engine = TemporalReasoningEngine::new(100);
1306        assert!(engine.event_chains().is_empty());
1307    }
1308
1309    #[test]
1310    fn test_event_chains_single() {
1311        let mut engine = TemporalReasoningEngine::new(100);
1312        engine
1313            .add_event(ev("a", 0, 10))
1314            .expect("test: should succeed");
1315        let chains = engine.event_chains();
1316        assert_eq!(chains.len(), 1);
1317        assert_eq!(chains[0].len(), 1);
1318    }
1319
1320    #[test]
1321    fn test_event_chains_two_isolated() {
1322        let mut engine = TemporalReasoningEngine::new(100);
1323        engine
1324            .add_event(ev("a", 0, 10))
1325            .expect("test: should succeed");
1326        engine
1327            .add_event(ev("b", 100, 200))
1328            .expect("test: should succeed");
1329        let chains = engine.event_chains();
1330        assert_eq!(chains.len(), 2);
1331    }
1332
1333    #[test]
1334    fn test_event_chains_transitive() {
1335        let mut engine = TemporalReasoningEngine::new(100);
1336        // a overlaps b; b overlaps c; a does NOT directly overlap c
1337        engine
1338            .add_event(ev("a", 0, 20))
1339            .expect("test: should succeed");
1340        engine
1341            .add_event(ev("b", 15, 35))
1342            .expect("test: should succeed");
1343        engine
1344            .add_event(ev("c", 30, 50))
1345            .expect("test: should succeed");
1346        engine
1347            .add_event(ev("d", 200, 300))
1348            .expect("test: should succeed"); // isolated
1349        let chains = engine.event_chains();
1350        // {a,b,c} is one component; {d} is another
1351        assert_eq!(chains.len(), 2);
1352        let big = chains
1353            .iter()
1354            .find(|ch| ch.len() == 3)
1355            .expect("test: should succeed");
1356        let ids: Vec<&str> = big.iter().map(|e| e.id.as_str()).collect();
1357        assert!(ids.contains(&"a") && ids.contains(&"b") && ids.contains(&"c"));
1358    }
1359
1360    // ------------------------------------------------------------------
1361    // Statistics
1362    // ------------------------------------------------------------------
1363
1364    #[test]
1365    fn test_stats_empty() {
1366        let engine = TemporalReasoningEngine::new(100);
1367        let s = engine.stats();
1368        assert_eq!(s.event_count, 0);
1369        assert_eq!(s.constraint_count, 0);
1370        assert_eq!(s.timeline_span_ms, 0);
1371        assert_eq!(s.avg_event_duration_ms, 0.0);
1372    }
1373
1374    #[test]
1375    fn test_stats_with_events() {
1376        let mut engine = TemporalReasoningEngine::new(100);
1377        engine
1378            .add_event(ev("a", 0, 100))
1379            .expect("test: should succeed");
1380        engine
1381            .add_event(ev("b", 50, 200))
1382            .expect("test: should succeed");
1383        engine.add_constraint(TemporalConstraint::Overlapping {
1384            a: "a".into(),
1385            b: "b".into(),
1386        });
1387        let s = engine.stats();
1388        assert_eq!(s.event_count, 2);
1389        assert_eq!(s.constraint_count, 1);
1390        assert_eq!(s.timeline_span_ms, 200); // 0..200
1391                                             // avg = (100 + 150) / 2 = 125
1392        assert!((s.avg_event_duration_ms - 125.0).abs() < f64::EPSILON);
1393    }
1394
1395    // ------------------------------------------------------------------
1396    // ConstraintViolation fields
1397    // ------------------------------------------------------------------
1398
1399    #[test]
1400    fn test_violation_contains_relation() {
1401        let mut engine = TemporalReasoningEngine::new(100);
1402        engine
1403            .add_event(ev("x", 0, 50))
1404            .expect("test: should succeed");
1405        engine
1406            .add_event(ev("y", 40, 80))
1407            .expect("test: should succeed");
1408        engine.add_constraint(TemporalConstraint::Before {
1409            a: "x".into(),
1410            b: "y".into(),
1411        });
1412        let violations = engine.check_constraints();
1413        assert_eq!(violations.len(), 1);
1414        let v: &ConstraintViolation = &violations[0];
1415        assert_eq!(v.relation, AllenRelation::Overlaps);
1416        assert!(v.constraint.contains("Before"));
1417    }
1418}