1#![allow(clippy::many_single_char_names)]
6
7use std::sync::Arc;
8
9use antecedent_core::VariableId;
10
11use crate::error::GraphError;
12use crate::marked_storage::{self, AdjEntry};
13use crate::types::{DenseNodeId, Endpoint, MarkedEdge, MiddleMark, NodeRef};
14use crate::workspace::GraphWorkspace;
15
16#[derive(Clone, Debug)]
18pub struct Pag {
19 nodes: Vec<NodeRef>,
20 adj: Vec<Vec<AdjEntry>>,
21}
22
23impl Pag {
24 #[must_use]
26 pub fn empty() -> Self {
27 Self { nodes: Vec::new(), adj: Vec::new() }
28 }
29
30 #[must_use]
32 pub fn with_variables(n: u32) -> Self {
33 let mut g = Self::empty();
34 for i in 0..n {
35 let _ = g.add_node(NodeRef::Static(VariableId::from_raw(i)));
36 }
37 g
38 }
39
40 pub fn from_named_edges(
46 schema: &antecedent_core::CausalSchema,
47 edges: &[(&str, &str)],
48 ) -> Result<Self, GraphError> {
49 let n = crate::named::schema_node_count(schema)?;
50 let mut g = Self::with_variables(n);
51 for &(from_name, to_name) in edges {
52 let (from, to) = crate::named::resolve_named_edge(schema, from_name, to_name)?;
53 g.insert_directed(from, to)?;
54 }
55 Ok(g)
56 }
57
58 #[must_use]
60 pub fn node_count(&self) -> usize {
61 self.nodes.len()
62 }
63
64 #[must_use]
66 pub fn is_empty(&self) -> bool {
67 self.nodes.is_empty()
68 }
69
70 #[must_use]
72 pub fn nodes(&self) -> &[NodeRef] {
73 &self.nodes
74 }
75
76 pub fn add_node(&mut self, node: NodeRef) -> Result<DenseNodeId, GraphError> {
82 if !matches!(node, NodeRef::Static(_)) {
83 return Err(GraphError::InvalidEndpoints { message: "Pag accepts only Static nodes" });
84 }
85 let id = u32::try_from(self.nodes.len()).map_err(|_| GraphError::TooManyNodes)?;
86 self.nodes.push(node);
87 self.adj.push(Vec::new());
88 Ok(DenseNodeId::from_raw(id))
89 }
90
91 fn validate_node(&self, id: DenseNodeId) -> Result<(), GraphError> {
92 if id.as_usize() >= self.node_count() {
93 return Err(GraphError::UnknownNode { id: id.raw() });
94 }
95 Ok(())
96 }
97
98 pub(crate) fn validate_node_pub(&self, id: DenseNodeId) -> Result<(), GraphError> {
99 self.validate_node(id)
100 }
101
102 #[must_use]
106 pub const fn is_pag_legal(edge: MarkedEdge) -> bool {
107 edge.a.raw() != edge.b.raw()
108 }
109
110 pub fn insert_marked(&mut self, edge: MarkedEdge) -> Result<(), GraphError> {
116 if !Self::is_pag_legal(edge) {
117 return Err(GraphError::InvalidEndpoints { message: "Pag rejects self-loops" });
118 }
119 self.validate_node(edge.a)?;
120 self.validate_node(edge.b)?;
121 if edge.a == edge.b {
122 return Err(GraphError::InvalidEndpoints { message: "Pag rejects self-loops" });
123 }
124 if self.has_edge(edge.a, edge.b) {
125 return Err(GraphError::DuplicateEdge { from: edge.a.raw(), to: edge.b.raw() });
126 }
127 if let Some((from, to)) = edge.parent_child() {
128 if self.reaches_directed(to, from) {
129 return Err(GraphError::Cycle { from: from.raw(), to: to.raw() });
130 }
131 }
132 marked_storage::push_marked_pair(&mut self.adj, edge);
133 Ok(())
134 }
135
136 pub fn insert_directed(
142 &mut self,
143 from: DenseNodeId,
144 to: DenseNodeId,
145 ) -> Result<(), GraphError> {
146 self.insert_marked(MarkedEdge::directed(from, to))
147 }
148
149 pub fn insert_circle_arrow(
155 &mut self,
156 from: DenseNodeId,
157 to: DenseNodeId,
158 ) -> Result<(), GraphError> {
159 self.insert_marked(MarkedEdge {
160 a: from,
161 b: to,
162 at_a: Endpoint::Circle,
163 at_b: Endpoint::Arrow,
164 middle: MiddleMark::Empty,
165 })
166 }
167
168 pub fn insert_circle_circle(
174 &mut self,
175 a: DenseNodeId,
176 b: DenseNodeId,
177 ) -> Result<(), GraphError> {
178 let (a, b) = if a.raw() <= b.raw() { (a, b) } else { (b, a) };
179 self.insert_marked(MarkedEdge {
180 a,
181 b,
182 at_a: Endpoint::Circle,
183 at_b: Endpoint::Circle,
184 middle: MiddleMark::Empty,
185 })
186 }
187
188 pub fn insert_bidirected(&mut self, a: DenseNodeId, b: DenseNodeId) -> Result<(), GraphError> {
194 self.insert_marked(MarkedEdge::bidirected(a, b))
195 }
196
197 #[must_use]
199 pub fn has_edge(&self, a: DenseNodeId, b: DenseNodeId) -> bool {
200 self.edge_between(a, b).is_some()
201 }
202
203 #[must_use]
205 pub fn edge_between(&self, a: DenseNodeId, b: DenseNodeId) -> Option<MarkedEdge> {
206 marked_storage::edge_between(&self.adj, a, b)
207 }
208
209 pub fn neighbors(
211 &self,
212 id: DenseNodeId,
213 ) -> impl Iterator<Item = (DenseNodeId, Endpoint, Endpoint)> + '_ {
214 self.adj[id.as_usize()].iter().map(|e| (e.neighbor, e.at_self, e.at_neighbor))
215 }
216
217 pub fn set_marks(
223 &mut self,
224 a: DenseNodeId,
225 b: DenseNodeId,
226 at_a: Endpoint,
227 at_b: Endpoint,
228 ) -> Result<(), GraphError> {
229 self.validate_node(a)?;
230 self.validate_node(b)?;
231 if !self.has_edge(a, b) {
232 return Err(GraphError::UnknownNode { id: a.raw() });
233 }
234 let previous =
235 marked_storage::edge_between(&self.adj, a, b).expect("edge present after has_edge");
236 let edge = MarkedEdge { a, b, at_a, at_b, middle: previous.middle };
237 if let Some((from, to)) = edge.parent_child() {
238 marked_storage::remove_edge(&mut self.adj, a, b);
239 let cycle = self.reaches_directed(to, from);
240 if cycle {
241 marked_storage::push_marked_pair(&mut self.adj, previous);
242 return Err(GraphError::Cycle { from: from.raw(), to: to.raw() });
243 }
244 marked_storage::push_marked_pair(&mut self.adj, edge);
245 return Ok(());
246 }
247 marked_storage::set_marks(&mut self.adj, a, b, at_a, at_b)
248 }
249
250 pub fn mark_conflict(&mut self, a: DenseNodeId, b: DenseNodeId) -> Result<(), GraphError> {
256 self.set_marks(a, b, Endpoint::Conflict, Endpoint::Conflict)
257 }
258
259 pub fn remove_edge(&mut self, a: DenseNodeId, b: DenseNodeId) -> Result<(), GraphError> {
265 self.validate_node(a)?;
266 self.validate_node(b)?;
267 if self.edge_between(a, b).is_none() {
268 return Err(GraphError::UnknownNode { id: a.raw() });
269 }
270 marked_storage::remove_edge(&mut self.adj, a, b);
271 Ok(())
272 }
273
274 #[must_use]
276 pub fn directed_children(&self, id: DenseNodeId) -> Vec<DenseNodeId> {
277 marked_storage::directed_children(&self.adj, id).collect()
278 }
279
280 pub fn directed_children_iter(
282 &self,
283 id: DenseNodeId,
284 ) -> impl Iterator<Item = DenseNodeId> + '_ {
285 marked_storage::directed_children(&self.adj, id)
286 }
287
288 #[must_use]
290 pub fn reaches_directed(&self, from: DenseNodeId, to: DenseNodeId) -> bool {
291 let mut ws = GraphWorkspace::default();
292 self.reaches_directed_with(&mut ws, from, to)
293 }
294
295 #[must_use]
297 pub fn reaches_directed_with(
298 &self,
299 ws: &mut GraphWorkspace,
300 from: DenseNodeId,
301 to: DenseNodeId,
302 ) -> bool {
303 marked_storage::reaches_directed(&self.adj, ws, from, to)
304 }
305}
306
307#[derive(Clone, Debug, Eq, PartialEq)]
309pub struct DefiniteStatusPath {
310 pub nodes: Vec<DenseNodeId>,
312}
313
314#[derive(Clone, Debug, Eq, PartialEq)]
316pub struct DefiniteStatusPathSearch {
317 pub paths: Vec<DefiniteStatusPath>,
319 pub truncated: bool,
321}
322
323impl Pag {
324 pub fn definite_status_paths(
330 &self,
331 x: DenseNodeId,
332 y: DenseNodeId,
333 max_paths: usize,
334 max_len: usize,
335 ) -> Result<DefiniteStatusPathSearch, GraphError> {
336 self.validate_node(x)?;
337 self.validate_node(y)?;
338 let mut out = Vec::new();
339 if max_paths == 0 || max_len == 0 {
340 return Ok(DefiniteStatusPathSearch { paths: out, truncated: true });
341 }
342 let mut truncated = false;
343 let mut stack = vec![vec![x]];
344 while let Some(path) = stack.pop() {
345 if out.len() >= max_paths {
346 truncated = true;
347 break;
348 }
349 let last = *path.last().expect("nonempty");
350 if path.len() > 1 && last == y {
351 if self.path_is_definite_status(&path) {
352 out.push(DefiniteStatusPath { nodes: path });
353 }
354 continue;
355 }
356 if path.len() >= max_len {
357 for (nbr, _, _) in self.neighbors(last) {
359 if path.len() >= 2 && path[path.len() - 2] == nbr {
360 continue;
361 }
362 if path.contains(&nbr) {
363 continue;
364 }
365 truncated = true;
366 break;
367 }
368 continue;
369 }
370 for (nbr, _, _) in self.neighbors(last) {
371 if path.len() >= 2 && path[path.len() - 2] == nbr {
372 continue; }
374 if path.contains(&nbr) {
375 continue;
376 }
377 let mut next = path.clone();
378 next.push(nbr);
379 stack.push(next);
380 }
381 }
382 Ok(DefiniteStatusPathSearch { paths: out, truncated })
383 }
384
385 fn path_is_definite_status(&self, path: &[DenseNodeId]) -> bool {
386 if path.len() < 2 {
387 return true;
388 }
389 for i in 1..path.len() - 1 {
390 let pred = path[i - 1];
391 let v = path[i];
392 let succ = path[i + 1];
393 let Some(e1) = self.edge_between(pred, v) else {
394 return false;
395 };
396 let Some(e2) = self.edge_between(v, succ) else {
397 return false;
398 };
399 let mark_from_pred = if e1.a == v { e1.at_a } else { e1.at_b };
400 let mark_from_succ = if e2.a == v { e2.at_a } else { e2.at_b };
401 let definite_collider = matches!(mark_from_pred, Endpoint::Arrow)
402 && matches!(mark_from_succ, Endpoint::Arrow);
403 let definite_noncollider = matches!(mark_from_pred, Endpoint::Tail)
404 || matches!(mark_from_succ, Endpoint::Tail);
405 if !(definite_collider || definite_noncollider) {
406 return false;
407 }
408 }
409 true
410 }
411
412 #[must_use]
416 pub fn path_active_given(&self, path: &[DenseNodeId], z: &[DenseNodeId]) -> bool {
417 if path.len() < 2 {
418 return false;
419 }
420 let in_z = |n: DenseNodeId| z.iter().any(|&v| v == n);
421 for i in 1..path.len() - 1 {
422 let pred = path[i - 1];
423 let v = path[i];
424 let succ = path[i + 1];
425 let e1 = self.edge_between(pred, v).expect("path edge");
426 let e2 = self.edge_between(v, succ).expect("path edge");
427 let mark_from_pred = if e1.a == v { e1.at_a } else { e1.at_b };
428 let mark_from_succ = if e2.a == v { e2.at_a } else { e2.at_b };
429 let collider = matches!(mark_from_pred, Endpoint::Arrow)
430 && matches!(mark_from_succ, Endpoint::Arrow);
431 if collider {
432 if !in_z(v) && !self.collider_descendant_in_z(v, z) {
433 return false;
434 }
435 } else if in_z(v) {
436 return false;
437 }
438 }
439 true
440 }
441
442 fn collider_descendant_in_z(&self, v: DenseNodeId, z: &[DenseNodeId]) -> bool {
444 z.iter().any(|&d| d != v && self.reaches_directed(v, d))
445 }
446}
447
448#[cfg(test)]
449mod tests {
450 use super::*;
451
452 #[test]
453 fn accepts_circle_marks() {
454 let mut g = Pag::with_variables(2);
455 g.insert_circle_arrow(DenseNodeId::from_raw(0), DenseNodeId::from_raw(1)).unwrap();
456 assert!(g.has_edge(DenseNodeId::from_raw(0), DenseNodeId::from_raw(1)));
457 }
458
459 #[test]
460 fn remove_edge_clears_both_halves() {
461 let mut g = Pag::with_variables(2);
462 let a = DenseNodeId::from_raw(0);
463 let b = DenseNodeId::from_raw(1);
464 g.insert_directed(a, b).unwrap();
465 g.remove_edge(a, b).unwrap();
466 assert!(!g.has_edge(a, b));
467 assert!(g.remove_edge(a, b).is_err());
468 }
469
470 #[test]
471 fn definite_status_chain() {
472 let mut g = Pag::with_variables(3);
473 let a = DenseNodeId::from_raw(0);
474 let b = DenseNodeId::from_raw(1);
475 let c = DenseNodeId::from_raw(2);
476 g.insert_directed(a, b).unwrap();
477 g.insert_directed(b, c).unwrap();
478 let paths = g.definite_status_paths(a, c, 10, 8).unwrap();
479 assert!(!paths.paths.is_empty());
480 assert!(g.path_active_given(&paths.paths[0].nodes, &[]));
481 assert!(!g.path_active_given(&paths.paths[0].nodes, &[b]));
482 }
483}
484
485#[derive(Clone, Debug)]
487pub struct PagReview {
488 pub graph: Pag,
490 pub pending_circles: Arc<[(DenseNodeId, DenseNodeId)]>,
492 pub algorithm: Arc<str>,
494}
495
496impl PagReview {
497 #[must_use]
499 pub fn from_pag(graph: Pag, algorithm: impl Into<Arc<str>>) -> Self {
500 let mut pending = Vec::new();
501 for i in 0..graph.node_count() {
502 let a = DenseNodeId::from_raw(u32::try_from(i).expect("node fit"));
503 for (b, at_a, at_b) in graph.neighbors(a) {
504 if b.raw() < a.raw() {
505 continue;
506 }
507 if matches!(at_a, Endpoint::Circle) || matches!(at_b, Endpoint::Circle) {
508 pending.push((a, b));
509 }
510 }
511 }
512 Self { graph, pending_circles: Arc::from(pending), algorithm: algorithm.into() }
513 }
514
515 #[must_use]
517 pub fn is_complete(&self) -> bool {
518 self.pending_circles.is_empty()
519 }
520}
521
522#[cfg(test)]
523mod review_tests {
524 use super::*;
525
526 #[test]
527 fn review_lists_circle_edges() {
528 let mut g = Pag::with_variables(2);
529 g.insert_circle_circle(DenseNodeId::from_raw(0), DenseNodeId::from_raw(1)).unwrap();
530 let review = PagReview::from_pag(g, "fci");
531 assert_eq!(review.pending_circles.len(), 1);
532 assert!(!review.is_complete());
533 }
534
535 #[test]
536 fn directed_only_is_complete() {
537 let mut g = Pag::with_variables(2);
538 g.insert_directed(DenseNodeId::from_raw(0), DenseNodeId::from_raw(1)).unwrap();
539 let review = PagReview::from_pag(g, "fci");
540 assert!(review.is_complete());
541 }
542}