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 marked_storage::insert_marked_finish(&mut self.adj, edge)
125 }
126
127 pub fn insert_directed(
133 &mut self,
134 from: DenseNodeId,
135 to: DenseNodeId,
136 ) -> Result<(), GraphError> {
137 self.insert_marked(MarkedEdge::directed(from, to))
138 }
139
140 pub fn insert_circle_arrow(
146 &mut self,
147 from: DenseNodeId,
148 to: DenseNodeId,
149 ) -> Result<(), GraphError> {
150 self.insert_marked(MarkedEdge {
151 a: from,
152 b: to,
153 at_a: Endpoint::Circle,
154 at_b: Endpoint::Arrow,
155 middle: MiddleMark::Empty,
156 })
157 }
158
159 pub fn insert_circle_circle(
165 &mut self,
166 a: DenseNodeId,
167 b: DenseNodeId,
168 ) -> Result<(), GraphError> {
169 let (a, b) = if a.raw() <= b.raw() { (a, b) } else { (b, a) };
170 self.insert_marked(MarkedEdge {
171 a,
172 b,
173 at_a: Endpoint::Circle,
174 at_b: Endpoint::Circle,
175 middle: MiddleMark::Empty,
176 })
177 }
178
179 pub fn insert_bidirected(&mut self, a: DenseNodeId, b: DenseNodeId) -> Result<(), GraphError> {
185 self.insert_marked(MarkedEdge::bidirected(a, b))
186 }
187
188 #[must_use]
190 pub fn has_edge(&self, a: DenseNodeId, b: DenseNodeId) -> bool {
191 self.edge_between(a, b).is_some()
192 }
193
194 #[must_use]
196 pub fn edge_between(&self, a: DenseNodeId, b: DenseNodeId) -> Option<MarkedEdge> {
197 marked_storage::edge_between(&self.adj, a, b)
198 }
199
200 pub fn neighbors(
202 &self,
203 id: DenseNodeId,
204 ) -> impl Iterator<Item = (DenseNodeId, Endpoint, Endpoint)> + '_ {
205 marked_storage::neighbors(&self.adj, id)
206 }
207
208 pub fn set_marks(
214 &mut self,
215 a: DenseNodeId,
216 b: DenseNodeId,
217 at_a: Endpoint,
218 at_b: Endpoint,
219 ) -> Result<(), GraphError> {
220 self.validate_node(a)?;
221 self.validate_node(b)?;
222 if !self.has_edge(a, b) {
223 return Err(GraphError::UnknownNode { id: a.raw() });
224 }
225 let previous =
226 marked_storage::edge_between(&self.adj, a, b).expect("edge present after has_edge");
227 marked_storage::set_marks_finish(&mut self.adj, a, b, at_a, at_b, previous)
228 }
229
230 pub fn mark_conflict(&mut self, a: DenseNodeId, b: DenseNodeId) -> Result<(), GraphError> {
236 self.set_marks(a, b, Endpoint::Conflict, Endpoint::Conflict)
237 }
238
239 pub fn remove_edge(&mut self, a: DenseNodeId, b: DenseNodeId) -> Result<(), GraphError> {
245 self.validate_node(a)?;
246 self.validate_node(b)?;
247 if self.edge_between(a, b).is_none() {
248 return Err(GraphError::UnknownNode { id: a.raw() });
249 }
250 marked_storage::remove_edge(&mut self.adj, a, b);
251 Ok(())
252 }
253
254 #[must_use]
256 pub fn directed_children(&self, id: DenseNodeId) -> Vec<DenseNodeId> {
257 marked_storage::directed_children(&self.adj, id).collect()
258 }
259
260 pub fn directed_children_iter(
262 &self,
263 id: DenseNodeId,
264 ) -> impl Iterator<Item = DenseNodeId> + '_ {
265 marked_storage::directed_children(&self.adj, id)
266 }
267
268 #[must_use]
270 pub fn reaches_directed(&self, from: DenseNodeId, to: DenseNodeId) -> bool {
271 let mut ws = GraphWorkspace::default();
272 self.reaches_directed_with(&mut ws, from, to)
273 }
274
275 #[must_use]
277 pub fn reaches_directed_with(
278 &self,
279 ws: &mut GraphWorkspace,
280 from: DenseNodeId,
281 to: DenseNodeId,
282 ) -> bool {
283 marked_storage::reaches_directed(&self.adj, ws, from, to)
284 }
285}
286
287#[derive(Clone, Debug, Eq, PartialEq)]
289pub struct DefiniteStatusPath {
290 pub nodes: Vec<DenseNodeId>,
292}
293
294#[derive(Clone, Debug, Eq, PartialEq)]
296pub struct DefiniteStatusPathSearch {
297 pub paths: Vec<DefiniteStatusPath>,
299 pub truncated: bool,
301}
302
303impl Pag {
304 pub fn definite_status_paths(
310 &self,
311 x: DenseNodeId,
312 y: DenseNodeId,
313 max_paths: usize,
314 max_len: usize,
315 ) -> Result<DefiniteStatusPathSearch, GraphError> {
316 self.validate_node(x)?;
317 self.validate_node(y)?;
318 let mut out = Vec::new();
319 if max_paths == 0 || max_len == 0 {
320 return Ok(DefiniteStatusPathSearch { paths: out, truncated: true });
321 }
322 let mut truncated = false;
323 let mut stack = vec![vec![x]];
324 while let Some(path) = stack.pop() {
325 if out.len() >= max_paths {
326 truncated = true;
327 break;
328 }
329 let last = *path.last().expect("nonempty");
330 if path.len() > 1 && last == y {
331 if self.path_is_definite_status(&path) {
332 out.push(DefiniteStatusPath { nodes: path });
333 }
334 continue;
335 }
336 if path.len() >= max_len {
337 for (nbr, _, _) in self.neighbors(last) {
339 if path.len() >= 2 && path[path.len() - 2] == nbr {
340 continue;
341 }
342 if path.contains(&nbr) {
343 continue;
344 }
345 truncated = true;
346 break;
347 }
348 continue;
349 }
350 for (nbr, _, _) in self.neighbors(last) {
351 if path.len() >= 2 && path[path.len() - 2] == nbr {
352 continue; }
354 if path.contains(&nbr) {
355 continue;
356 }
357 let mut next = path.clone();
358 next.push(nbr);
359 stack.push(next);
360 }
361 }
362 Ok(DefiniteStatusPathSearch { paths: out, truncated })
363 }
364
365 fn path_is_definite_status(&self, path: &[DenseNodeId]) -> bool {
366 if path.len() < 2 {
367 return true;
368 }
369 for i in 1..path.len() - 1 {
370 let pred = path[i - 1];
371 let v = path[i];
372 let succ = path[i + 1];
373 let Some(e1) = self.edge_between(pred, v) else {
374 return false;
375 };
376 let Some(e2) = self.edge_between(v, succ) else {
377 return false;
378 };
379 let mark_from_pred = if e1.a == v { e1.at_a } else { e1.at_b };
380 let mark_from_succ = if e2.a == v { e2.at_a } else { e2.at_b };
381 let definite_collider = matches!(mark_from_pred, Endpoint::Arrow)
382 && matches!(mark_from_succ, Endpoint::Arrow);
383 let definite_noncollider = matches!(mark_from_pred, Endpoint::Tail)
384 || matches!(mark_from_succ, Endpoint::Tail);
385 if !(definite_collider || definite_noncollider) {
386 return false;
387 }
388 }
389 true
390 }
391
392 #[must_use]
396 pub fn path_active_given(&self, path: &[DenseNodeId], z: &[DenseNodeId]) -> bool {
397 if path.len() < 2 {
398 return false;
399 }
400 let in_z = |n: DenseNodeId| z.iter().any(|&v| v == n);
401 if in_z(path[0]) || in_z(path[path.len() - 1]) {
402 return true;
403 }
404 for i in 1..path.len() - 1 {
405 let pred = path[i - 1];
406 let v = path[i];
407 let succ = path[i + 1];
408 let e1 = self.edge_between(pred, v).expect("path edge");
409 let e2 = self.edge_between(v, succ).expect("path edge");
410 let mark_from_pred = if e1.a == v { e1.at_a } else { e1.at_b };
411 let mark_from_succ = if e2.a == v { e2.at_a } else { e2.at_b };
412 let collider = matches!(mark_from_pred, Endpoint::Arrow)
413 && matches!(mark_from_succ, Endpoint::Arrow);
414 if collider {
415 if !in_z(v) && !self.collider_descendant_in_z(v, z) {
416 return false;
417 }
418 } else if in_z(v) {
419 return false;
420 }
421 }
422 true
423 }
424
425 fn collider_descendant_in_z(&self, v: DenseNodeId, z: &[DenseNodeId]) -> bool {
427 z.iter().any(|&d| d != v && self.reaches_directed(v, d))
428 }
429}
430
431#[cfg(test)]
432mod tests {
433 use super::*;
434
435 #[test]
436 fn accepts_circle_marks() {
437 let mut g = Pag::with_variables(2);
438 g.insert_circle_arrow(DenseNodeId::from_raw(0), DenseNodeId::from_raw(1)).unwrap();
439 assert!(g.has_edge(DenseNodeId::from_raw(0), DenseNodeId::from_raw(1)));
440 }
441
442 #[test]
443 fn remove_edge_clears_both_halves() {
444 let mut g = Pag::with_variables(2);
445 let a = DenseNodeId::from_raw(0);
446 let b = DenseNodeId::from_raw(1);
447 g.insert_directed(a, b).unwrap();
448 g.remove_edge(a, b).unwrap();
449 assert!(!g.has_edge(a, b));
450 assert!(g.remove_edge(a, b).is_err());
451 }
452
453 #[test]
454 fn definite_status_chain() {
455 let mut g = Pag::with_variables(3);
456 let a = DenseNodeId::from_raw(0);
457 let b = DenseNodeId::from_raw(1);
458 let c = DenseNodeId::from_raw(2);
459 g.insert_directed(a, b).unwrap();
460 g.insert_directed(b, c).unwrap();
461 let paths = g.definite_status_paths(a, c, 10, 8).unwrap();
462 assert!(!paths.paths.is_empty());
463 assert!(g.path_active_given(&paths.paths[0].nodes, &[]));
464 assert!(!g.path_active_given(&paths.paths[0].nodes, &[b]));
465 }
466}
467
468#[derive(Clone, Debug)]
470pub struct PagReview {
471 pub graph: Pag,
473 pub pending_circles: Arc<[(DenseNodeId, DenseNodeId)]>,
475 pub algorithm: Arc<str>,
477}
478
479impl PagReview {
480 #[must_use]
482 pub fn from_pag(graph: Pag, algorithm: impl Into<Arc<str>>) -> Self {
483 let mut pending = Vec::new();
484 for i in 0..graph.node_count() {
485 let a = DenseNodeId::from_raw(u32::try_from(i).expect("node fit"));
486 for (b, at_a, at_b) in graph.neighbors(a) {
487 if b.raw() < a.raw() {
488 continue;
489 }
490 if matches!(at_a, Endpoint::Circle) || matches!(at_b, Endpoint::Circle) {
491 pending.push((a, b));
492 }
493 }
494 }
495 Self { graph, pending_circles: Arc::from(pending), algorithm: algorithm.into() }
496 }
497
498 #[must_use]
500 pub fn is_complete(&self) -> bool {
501 self.pending_circles.is_empty()
502 }
503}
504
505#[cfg(test)]
506mod review_tests {
507 use super::*;
508
509 #[test]
510 fn review_lists_circle_edges() {
511 let mut g = Pag::with_variables(2);
512 g.insert_circle_circle(DenseNodeId::from_raw(0), DenseNodeId::from_raw(1)).unwrap();
513 let review = PagReview::from_pag(g, "fci");
514 assert_eq!(review.pending_circles.len(), 1);
515 assert!(!review.is_complete());
516 }
517
518 #[test]
519 fn directed_only_is_complete() {
520 let mut g = Pag::with_variables(2);
521 g.insert_directed(DenseNodeId::from_raw(0), DenseNodeId::from_raw(1)).unwrap();
522 let review = PagReview::from_pag(g, "fci");
523 assert!(review.is_complete());
524 }
525}