#!/usr/bin/env python3 """temporal_reasoner.py - GOFAI temporal reasoning engine for the Timmy Foundation fleet. A symbolic temporal constraint network (TCN) for scheduling and ordering events. Models Allen's interval algebra relations (before, after, meets, overlaps, etc.) and propagates temporal constraints via path-consistency to detect conflicts. No ML, no embeddings - just constraint propagation over a temporal graph. Core concepts: TimePoint: A named instant on a symbolic timeline. Interval: A pair of time-points (start, end) with start < end. Constraint: A relation between two time-points or intervals (e.g. A.before(B), A.meets(B)). Usage (Python API): from temporal_reasoner import TemporalNetwork, Interval tn = TemporalNetwork() deploy = tn.add_interval('deploy', duration=(10, 30)) test = tn.add_interval('test', duration=(5, 15)) tn.add_constraint(deploy, 'before', test) consistent = tn.propagate() CLI: python temporal_reasoner.py --demo """ from __future__ import annotations import argparse import sys from dataclasses import dataclass, field from enum import Enum from typing import Dict, List, Optional, Set, Tuple INF = float('inf') # --------------------------------------------------------------------------- # Data model # --------------------------------------------------------------------------- @dataclass(frozen=True) class TimePoint: """A named instant on the timeline.""" name: str id: int = field(default=0) def __str__(self) -> str: return self.name @dataclass class Interval: """A named interval bounded by two time-points.""" name: str start: int # index into the distance matrix end: int # index into the distance matrix def __str__(self) -> str: return self.name class Relation(Enum): """Allen's interval algebra relations (simplified subset).""" BEFORE = 'before' AFTER = 'after' MEETS = 'meets' MET_BY = 'met_by' OVERLAPS = 'overlaps' DURING = 'during' EQUALS = 'equals' # --------------------------------------------------------------------------- # Simple Temporal Network (STN) via distance matrix # --------------------------------------------------------------------------- class TemporalNetwork: """Simple Temporal Network with Floyd-Warshall propagation. Internally maintains a distance matrix D where D[i][j] is the maximum allowed distance from time-point i to time-point j. Negative cycles indicate inconsistency. """ def __init__(self) -> None: self._n = 0 self._names: List[str] = [] self._dist: List[List[float]] = [] self._intervals: Dict[str, Interval] = {} self._origin_idx: int = -1 self._add_point('origin') self._origin_idx = 0 # ------------------------------------------------------------------ # Point management # ------------------------------------------------------------------ def _add_point(self, name: str) -> int: """Add a time-point and return its index.""" idx = self._n self._n += 1 self._names.append(name) # Extend distance matrix for row in self._dist: row.append(INF) self._dist.append([INF] * self._n) self._dist[idx][idx] = 0.0 return idx # ------------------------------------------------------------------ # Interval management # ------------------------------------------------------------------ def add_interval( self, name: str, duration: Optional[Tuple[float, float]] = None, ) -> Interval: """Add a named interval with optional duration bounds [lo, hi]. Returns the Interval object with start/end indices. """ s = self._add_point(f"{name}.start") e = self._add_point(f"{name}.end") # start < end (at least 1 time unit) self._dist[s][e] = min(self._dist[s][e], duration[1] if duration else INF) self._dist[e][s] = min(self._dist[e][s], -(duration[0] if duration else 1)) interval = Interval(name=name, start=s, end=e) self._intervals[name] = interval return interval # ------------------------------------------------------------------ # Constraint management # ------------------------------------------------------------------ def add_distance_constraint( self, i: int, j: int, lo: float, hi: float ) -> None: """Add constraint: lo <= t_j - t_i <= hi.""" self._dist[i][j] = min(self._dist[i][j], hi) self._dist[j][i] = min(self._dist[j][i], -lo) def add_constraint( self, a: Interval, relation: str, b: Interval, gap: Tuple[float, float] = (0, INF) ) -> None: """Add an Allen-style relation between two intervals. Supported relations: before, after, meets, met_by, equals. """ rel = relation.lower() if rel == 'before': # a.end + gap <= b.start self.add_distance_constraint(a.end, b.start, gap[0], gap[1]) elif rel == 'after': self.add_distance_constraint(b.end, a.start, gap[0], gap[1]) elif rel == 'meets': # a.end == b.start self.add_distance_constraint(a.end, b.start, 0, 0) elif rel == 'met_by': self.add_distance_constraint(b.end, a.start, 0, 0) elif rel == 'equals': self.add_distance_constraint(a.start, b.start, 0, 0) self.add_distance_constraint(a.end, b.end, 0, 0) else: raise ValueError(f"Unsupported relation: {relation}") # ------------------------------------------------------------------ # Propagation (Floyd-Warshall) # ------------------------------------------------------------------ def propagate(self) -> bool: """Run Floyd-Warshall to propagate all constraints. Returns True if the network is consistent (no negative cycles). """ n = self._n d = self._dist for k in range(n): for i in range(n): for j in range(n): if d[i][k] + d[k][j] < d[i][j]: d[i][j] = d[i][k] + d[k][j] # Check for negative cycles for i in range(n): if d[i][i] < 0: return False return True def is_consistent(self) -> bool: """Check consistency without mutating (copies matrix first).""" import copy saved = copy.deepcopy(self._dist) result = self.propagate() self._dist = saved return result # ------------------------------------------------------------------ # Query # ------------------------------------------------------------------ def earliest(self, point_idx: int) -> float: """Earliest possible time for a point (relative to origin).""" return -self._dist[point_idx][self._origin_idx] def latest(self, point_idx: int) -> float: """Latest possible time for a point (relative to origin).""" return self._dist[self._origin_idx][point_idx] def interval_bounds(self, interval: Interval) -> Dict[str, Tuple[float, float]]: """Return earliest/latest start and end for an interval.""" return { 'start': (self.earliest(interval.start), self.latest(interval.start)), 'end': (self.earliest(interval.end), self.latest(interval.end)), } # ------------------------------------------------------------------ # Display # ------------------------------------------------------------------ def dump(self) -> None: """Print the current distance matrix and interval bounds.""" print(f"Temporal Network — {self._n} time-points, {len(self._intervals)} intervals") print() for name, interval in self._intervals.items(): bounds = self.interval_bounds(interval) s_lo, s_hi = bounds['start'] e_lo, e_hi = bounds['end'] print(f" {name}:") print(f" start: [{s_lo:.1f}, {s_hi:.1f}]") print(f" end: [{e_lo:.1f}, {e_hi:.1f}]") # --------------------------------------------------------------------------- # Demo: Timmy fleet deployment pipeline # --------------------------------------------------------------------------- def run_demo() -> None: """Run a demo temporal reasoning scenario for the Timmy fleet.""" print("=" * 60) print("Temporal Reasoner Demo - Fleet Deployment Pipeline") print("=" * 60) print() tn = TemporalNetwork() # Define pipeline stages with duration bounds [min, max] build = tn.add_interval('build', duration=(5, 15)) test = tn.add_interval('test', duration=(10, 30)) review = tn.add_interval('review', duration=(2, 10)) deploy = tn.add_interval('deploy', duration=(1, 5)) monitor = tn.add_interval('monitor', duration=(20, 60)) # Temporal constraints tn.add_constraint(build, 'meets', test) # test starts when build ends tn.add_constraint(test, 'before', review, gap=(0, 5)) # review within 5 of test tn.add_constraint(review, 'meets', deploy) # deploy immediately after review tn.add_constraint(deploy, 'before', monitor, gap=(0, 2)) # monitor within 2 of deploy # Global deadline: everything done within 120 time units tn.add_distance_constraint(tn._origin_idx, monitor.end, 0, 120) # Build must start within first 10 units tn.add_distance_constraint(tn._origin_idx, build.start, 0, 10) print("Constraints added. Propagating...") consistent = tn.propagate() print(f"Network consistent: {consistent}") print() if consistent: tn.dump() print() # Now add a conflicting constraint to show inconsistency detection print("--- Adding conflicting constraint: monitor.before(build) ---") tn.add_constraint(monitor, 'before', build) consistent2 = tn.propagate() print(f"Network consistent after conflict: {consistent2}") # --------------------------------------------------------------------------- # CLI # --------------------------------------------------------------------------- def main() -> None: parser = argparse.ArgumentParser( description="GOFAI temporal reasoning engine" ) parser.add_argument( "--demo", action="store_true", help="Run the fleet deployment pipeline demo", ) args = parser.parse_args() if args.demo or not any(vars(args).values()): run_demo() else: parser.print_help() if __name__ == "__main__": main()