Skip to main content

fabula_petgraph/
lib.rs

1//! Petgraph adapter for fabula — wraps `petgraph::StableGraph` with temporal edges.
2//!
3//! Petgraph has no native temporal support, so this adapter stores intervals
4//! as part of the edge weight. Queries filter by time at evaluation.
5
6use fabula::datasource::{DataSource, Edge, ValueConstraint};
7use fabula::interval::Interval;
8use petgraph::stable_graph::{NodeIndex, StableGraph};
9use petgraph::Direction;
10use std::collections::HashMap;
11use std::fmt::Debug;
12use std::hash::{Hash, Hasher};
13
14/// A temporal edge weight — bundles a label, value, and validity interval
15/// onto a petgraph edge.
16#[derive(Debug, Clone)]
17pub struct TemporalEdge<L, V, T> {
18    pub label: L,
19    pub value: V,
20    pub interval: Interval<T>,
21}
22
23/// A temporal graph backed by `petgraph::StableGraph`.
24///
25/// Nodes are identified by `N`, edges carry `TemporalEdge<L, V, T>`.
26/// A `HashMap<N, NodeIndex>` provides reverse lookup from user IDs to
27/// petgraph's internal `NodeIndex`.
28pub struct PetTemporalGraph<N, L, V, T> {
29    graph: StableGraph<N, TemporalEdge<L, V, T>>,
30    node_index: HashMap<N, NodeIndex>,
31    current_time: T,
32}
33
34impl<N, L, V, T> PetTemporalGraph<N, L, V, T>
35where
36    N: Eq + Hash + Clone + Debug,
37    T: Clone,
38{
39    /// Create a new empty graph with the given initial time.
40    pub fn new(initial_time: T) -> Self {
41        Self {
42            graph: StableGraph::new(),
43            node_index: HashMap::new(),
44            current_time: initial_time,
45        }
46    }
47
48    /// Set the current time.
49    pub fn set_time(&mut self, t: T) {
50        self.current_time = t;
51    }
52
53    /// Add a node (or return existing index if already present).
54    pub fn add_node(&mut self, id: N) -> NodeIndex {
55        if let Some(&idx) = self.node_index.get(&id) {
56            idx
57        } else {
58            let idx = self.graph.add_node(id.clone());
59            self.node_index.insert(id, idx);
60            idx
61        }
62    }
63
64    /// Add a temporal edge between two nodes.
65    pub fn add_edge(&mut self, from: N, label: L, value: V, interval: Interval<T>) {
66        let from_idx = self.add_node(from);
67        let edge = TemporalEdge {
68            label,
69            value,
70            interval,
71        };
72        // We need a "to" node for petgraph. Use from_idx as a self-loop
73        // since we store the target in the edge value, not as a graph edge target.
74        // This is a design choice: petgraph edges connect NodeIndex pairs,
75        // but fabula's model has edges carrying values (which may or may not be nodes).
76        self.graph.add_edge(from_idx, from_idx, edge);
77    }
78
79    /// Add a temporal edge with a node-valued target (also ensures target node exists).
80    pub fn add_edge_to_node(&mut self, from: N, label: L, to: N, interval: Interval<T>)
81    where
82        V: From<NodeRef<N>>,
83    {
84        self.add_node(to.clone());
85        let value = V::from(NodeRef(to));
86        self.add_edge(from, label, value, interval);
87    }
88
89    /// Add a temporal edge with a bounded interval `[start, end)`.
90    pub fn add_edge_bounded(&mut self, from: N, label: L, value: V, start: T, end: T)
91    where
92        T: Ord,
93    {
94        let from_idx = self.add_node(from);
95        let edge = TemporalEdge {
96            label,
97            value,
98            interval: Interval::new(start, end),
99        };
100        self.graph.add_edge(from_idx, from_idx, edge);
101    }
102
103    /// Number of nodes.
104    pub fn node_count(&self) -> usize {
105        self.graph.node_count()
106    }
107
108    /// Number of edges.
109    pub fn edge_count(&self) -> usize {
110        self.graph.edge_count()
111    }
112}
113
114/// Wrapper to distinguish node references from other values.
115/// Used with `From<NodeRef<N>>` to convert node IDs into values.
116#[derive(Debug, Clone)]
117pub struct NodeRef<N>(pub N);
118
119/// A value type for the petgraph adapter that can hold node references or literals.
120#[derive(Debug, Clone, PartialEq, PartialOrd)]
121pub enum PetValue<N: Debug + Clone + PartialOrd> {
122    /// Reference to another node.
123    Node(N),
124    /// String literal.
125    Str(String),
126    /// Numeric value.
127    Num(f64),
128    /// Boolean value.
129    Bool(bool),
130}
131
132impl<N: Debug + Clone + PartialOrd + Hash> Hash for PetValue<N> {
133    fn hash<H: Hasher>(&self, state: &mut H) {
134        std::mem::discriminant(self).hash(state);
135        match self {
136            PetValue::Node(n) => n.hash(state),
137            PetValue::Str(s) => s.hash(state),
138            PetValue::Num(n) => n.to_bits().hash(state),
139            PetValue::Bool(b) => b.hash(state),
140        }
141    }
142}
143
144impl<N: Debug + Clone + PartialOrd> From<NodeRef<N>> for PetValue<N> {
145    fn from(r: NodeRef<N>) -> Self {
146        PetValue::Node(r.0)
147    }
148}
149
150impl<N, L, T> DataSource for PetTemporalGraph<N, L, PetValue<N>, T>
151where
152    N: Eq + Hash + Clone + Debug + PartialOrd,
153    L: Eq + Hash + Clone + Debug,
154    T: Ord + Clone + Debug + Hash,
155{
156    type N = N;
157    type L = L;
158    type V = PetValue<N>;
159    type T = T;
160
161    fn edges_from(&self, node: &N, label: &L, at: &T) -> Vec<Edge<N, PetValue<N>, T>> {
162        let Some(&idx) = self.node_index.get(node) else {
163            return Vec::new();
164        };
165        self.graph
166            .edges_directed(idx, Direction::Outgoing)
167            .filter(|e| &e.weight().label == label && e.weight().interval.covers(at))
168            .map(|e| Edge {
169                source: node.clone(),
170                target: e.weight().value.clone(),
171                interval: e.weight().interval.clone(),
172            })
173            .collect()
174    }
175
176    fn scan(
177        &self,
178        label: &L,
179        constraint: &ValueConstraint<PetValue<N>>,
180        at: &T,
181    ) -> Vec<Edge<N, PetValue<N>, T>> {
182        let mut results = Vec::new();
183        for idx in self.graph.node_indices() {
184            let node_id = &self.graph[idx];
185            for e in self.graph.edges_directed(idx, Direction::Outgoing) {
186                let w = e.weight();
187                if &w.label == label && constraint.matches(&w.value) && w.interval.covers(at) {
188                    results.push(Edge {
189                        source: node_id.clone(),
190                        target: w.value.clone(),
191                        interval: w.interval.clone(),
192                    });
193                }
194            }
195        }
196        results
197    }
198
199    fn edges_from_any_time(&self, node: &N, label: &L) -> Vec<Edge<N, PetValue<N>, T>> {
200        let Some(&idx) = self.node_index.get(node) else {
201            return Vec::new();
202        };
203        self.graph
204            .edges_directed(idx, Direction::Outgoing)
205            .filter(|e| &e.weight().label == label)
206            .map(|e| Edge {
207                source: node.clone(),
208                target: e.weight().value.clone(),
209                interval: e.weight().interval.clone(),
210            })
211            .collect()
212    }
213
214    fn scan_any_time(
215        &self,
216        label: &L,
217        constraint: &ValueConstraint<PetValue<N>>,
218    ) -> Vec<Edge<N, PetValue<N>, T>> {
219        let mut results = Vec::new();
220        for idx in self.graph.node_indices() {
221            let node_id = &self.graph[idx];
222            for e in self.graph.edges_directed(idx, Direction::Outgoing) {
223                let w = e.weight();
224                if &w.label == label && constraint.matches(&w.value) {
225                    results.push(Edge {
226                        source: node_id.clone(),
227                        target: w.value.clone(),
228                        interval: w.interval.clone(),
229                    });
230                }
231            }
232        }
233        results
234    }
235
236    fn now(&self) -> T {
237        self.current_time.clone()
238    }
239
240    fn value_as_node(&self, value: &PetValue<N>) -> Option<N> {
241        match value {
242            PetValue::Node(n) => Some(n.clone()),
243            _ => None,
244        }
245    }
246}