1use crate::error::GraphError;
11use crate::pag::Pag;
12use crate::types::{DenseNodeId, Endpoint};
13use crate::workspace::GraphWorkspace;
14
15#[derive(Clone, Debug)]
17pub struct PagCompletion {
18 pub graph: Pag,
20 pub index: usize,
22}
23
24#[derive(Clone, Debug)]
26pub struct CompletionSampler {
27 base: Pag,
28 circle_sites: Vec<(DenseNodeId, DenseNodeId, bool)>, max_completions: usize,
30 next_index: usize,
31 assign: u64,
33}
34
35impl CompletionSampler {
36 pub fn new(pag: Pag, max_completions: usize) -> Result<Self, GraphError> {
42 let mut sites = Vec::new();
43 let n = pag.node_count();
44 for i in 0..n {
45 let a = DenseNodeId::try_from_usize(i)?;
46 for (b, at_a, at_b) in pag.neighbors(a) {
47 if b.raw() < a.raw() {
48 continue;
49 }
50 if matches!(at_a, Endpoint::Circle) {
51 sites.push((a, b, true));
52 }
53 if matches!(at_b, Endpoint::Circle) {
54 sites.push((a, b, false));
55 }
56 }
57 }
58 if sites.len() > 63 {
59 return Err(GraphError::InvalidEndpoints {
60 message: "too many circle endpoints for CompletionSampler mask",
61 });
62 }
63 Ok(Self { base: pag, circle_sites: sites, max_completions, next_index: 0, assign: 0 })
64 }
65
66 #[must_use]
68 pub fn max_completions(&self) -> usize {
69 self.max_completions
70 }
71
72 #[must_use]
74 pub fn n_circle_sites(&self) -> usize {
75 self.circle_sites.len()
76 }
77
78 fn build_completion(&self, mask: u64) -> Option<Pag> {
79 let mut g = self.base.clone();
80 for (i, &(a, b, at_a_circle)) in self.circle_sites.iter().enumerate() {
81 let choose_arrow = ((mask >> i) & 1) == 1;
82 let new_mark = if choose_arrow { Endpoint::Arrow } else { Endpoint::Tail };
83 let edge = g.edge_between(a, b)?;
84 let (at_a, at_b) =
85 if at_a_circle { (new_mark, edge.at_b) } else { (edge.at_a, new_mark) };
86 if g.set_marks(a, b, at_a, at_b).is_err() {
88 return None;
89 }
90 }
91 if is_mag_completion(&g) { Some(g) } else { None }
92 }
93}
94
95#[must_use]
99pub fn is_mag_completion(g: &Pag) -> bool {
100 let n = g.node_count();
101 let mut ws = GraphWorkspace::default();
102 for i in 0..n {
103 let a = DenseNodeId::try_from_usize(i).expect("node fit");
104 for (b, at_a, at_b) in g.neighbors(a) {
105 if b.raw() < a.raw() {
106 continue;
107 }
108 if matches!(at_a, Endpoint::Circle | Endpoint::Conflict)
109 || matches!(at_b, Endpoint::Circle | Endpoint::Conflict)
110 {
111 return false;
112 }
113 if matches!((at_a, at_b), (Endpoint::Tail, Endpoint::Tail)) {
115 return false;
116 }
117 if matches!((at_a, at_b), (Endpoint::Arrow, Endpoint::Arrow)) {
118 if g.reaches_directed_with(&mut ws, a, b) || g.reaches_directed_with(&mut ws, b, a)
120 {
121 return false;
122 }
123 }
124 }
125 }
126 true
127}
128
129impl Iterator for CompletionSampler {
130 type Item = PagCompletion;
131
132 fn next(&mut self) -> Option<Self::Item> {
133 if self.next_index >= self.max_completions {
134 return None;
135 }
136 let n_sites = self.circle_sites.len();
137 let total = if n_sites == 0 { 1u64 } else { 1u64 << n_sites };
138 while self.assign < total {
139 let mask = self.assign;
140 self.assign += 1;
141 if let Some(graph) = self.build_completion(mask) {
142 let index = self.next_index;
143 self.next_index += 1;
144 return Some(PagCompletion { graph, index });
145 }
146 }
148 None
149 }
150}
151
152#[cfg(test)]
153mod tests {
154 use super::*;
155 use crate::pag::Pag;
156
157 #[test]
158 fn respects_max_completions_bound() {
159 let mut pag = Pag::with_variables(2);
160 pag.insert_circle_circle(DenseNodeId::from_raw(0), DenseNodeId::from_raw(1)).unwrap();
161 let sampler = CompletionSampler::new(pag, 2).unwrap();
162 assert_eq!(sampler.n_circle_sites(), 2);
163 let collected: Vec<_> = sampler.collect();
164 assert!(collected.len() <= 2);
165 assert!(!collected.is_empty());
166 for c in &collected {
167 assert!(is_mag_completion(&c.graph));
168 let e =
169 c.graph.edge_between(DenseNodeId::from_raw(0), DenseNodeId::from_raw(1)).unwrap();
170 assert!(!matches!(e.at_a, Endpoint::Circle));
171 assert!(!matches!(e.at_b, Endpoint::Circle));
172 assert!(!matches!((e.at_a, e.at_b), (Endpoint::Tail, Endpoint::Tail)));
174 }
175 }
176
177 #[test]
178 fn no_circle_yields_single_completion() {
179 let mut pag = Pag::with_variables(2);
180 pag.insert_directed(DenseNodeId::from_raw(0), DenseNodeId::from_raw(1)).unwrap();
181 let collected: Vec<_> = CompletionSampler::new(pag, 10).unwrap().collect();
182 assert_eq!(collected.len(), 1);
183 assert!(is_mag_completion(&collected[0].graph));
184 }
185
186 #[test]
187 fn rejects_almost_directed_cycle() {
188 let mut g = Pag::with_variables(3);
190 let a = DenseNodeId::from_raw(0);
191 let b = DenseNodeId::from_raw(1);
192 let c = DenseNodeId::from_raw(2);
193 g.insert_directed(a, b).unwrap();
194 g.insert_directed(b, c).unwrap();
195 g.insert_bidirected(a, c).unwrap();
196 assert!(!is_mag_completion(&g));
197 }
198
199 #[test]
200 fn accepts_bidirected_without_directed_path() {
201 let mut g = Pag::with_variables(2);
202 g.insert_bidirected(DenseNodeId::from_raw(0), DenseNodeId::from_raw(1)).unwrap();
203 assert!(is_mag_completion(&g));
204 }
205
206 fn random_pag_with_circles(rng: &mut antecedent_core::CausalRng, n: u32) -> Pag {
207 let mut pag = Pag::with_variables(n);
208 let mut order: Vec<u32> = (0..n).collect();
210 for i in (1..usize::try_from(n).unwrap_or(0)).rev() {
211 let bound = u64::try_from(i + 1).unwrap_or(1);
212 let j = usize::try_from(rng.next_u64() % bound).unwrap_or(0);
213 order.swap(i, j);
214 }
215 let n_usize = usize::try_from(n).unwrap_or(0);
216 for i in 0..n_usize {
217 for j in (i + 1)..n_usize {
218 if rng.next_u64() % 3 != 0 {
219 continue;
220 }
221 let a = DenseNodeId::from_raw(order[i]);
222 let b = DenseNodeId::from_raw(order[j]);
223 let kind = rng.next_u64() % 4;
224 let _ = match kind {
225 0 => pag.insert_directed(a, b),
226 1 => pag.insert_circle_arrow(a, b),
227 2 => pag.insert_circle_circle(a, b),
228 _ => pag.insert_bidirected(a, b),
229 };
230 }
231 }
232 pag
233 }
234
235 #[test]
237 fn property_completions_respect_bound_and_no_circles() {
238 use antecedent_core::CausalRng;
239
240 let mut rng = CausalRng::from_seed(23);
241 for _ in 0..40 {
242 let n = 2 + u32::try_from(rng.next_u64() % 3).unwrap_or(0); let pag = random_pag_with_circles(&mut rng, n);
244 let max_c = 1 + usize::try_from(rng.next_u64() % 4).unwrap_or(0); let Ok(sampler) = CompletionSampler::new(pag, max_c) else {
246 continue; };
248 let collected: Vec<_> = sampler.collect();
249 assert!(collected.len() <= max_c, "exceeded max_completions");
250 for (i, c) in collected.iter().enumerate() {
251 assert_eq!(c.index, i);
252 assert!(is_mag_completion(&c.graph));
253 for i in 0..c.graph.node_count() {
254 let a = DenseNodeId::from_raw(u32::try_from(i).unwrap());
255 for (b, at_a, at_b) in c.graph.neighbors(a) {
256 if b.raw() < a.raw() {
257 continue;
258 }
259 assert!(!matches!(at_a, Endpoint::Circle | Endpoint::Conflict));
260 assert!(!matches!(at_b, Endpoint::Circle | Endpoint::Conflict));
261 }
262 }
263 }
264 }
265 }
266
267 #[test]
270 fn property_definite_msep_stable_across_completions() {
271 use antecedent_core::CausalRng;
272
273 let mut rng = CausalRng::from_seed(29);
274 for _ in 0..30 {
275 let n = 3u32;
276 let pag = random_pag_with_circles(&mut rng, n);
277 let Ok(sampler) = CompletionSampler::new(pag.clone(), 8) else {
278 continue;
279 };
280 if sampler.n_circle_sites() > 4 {
281 continue; }
283 let completions: Vec<_> = sampler.collect();
284 if completions.is_empty() {
285 continue;
286 }
287 for x in 0..n {
288 for y in 0..n {
289 if x == y {
290 continue;
291 }
292 let xi = DenseNodeId::from_raw(x);
293 let yi = DenseNodeId::from_raw(y);
294 let Ok(pag_sep) = pag.is_m_separated(xi, yi, &[], 32, 6) else {
296 continue; };
298 if pag_sep {
299 continue; }
301 for c in &completions {
302 let Ok(comp_sep) = c.graph.is_m_separated(xi, yi, &[], 32, 6) else {
303 continue;
304 };
305 assert!(
306 !comp_sep,
307 "PAG m-connected but completion {} separated {}–{}",
308 c.index, x, y
309 );
310 }
311 }
312 }
313 }
314 }
315}