1#![allow(clippy::many_single_char_names)] use crate::dag::Dag;
14use crate::error::GraphError;
15use crate::overlay::GraphOverlay;
16use crate::types::DenseNodeId;
17use crate::workspace::{BitSet, GraphWorkspace};
18
19#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)]
21pub struct PathStep {
22 pub node: DenseNodeId,
24}
25
26#[derive(Clone, Debug, Eq, PartialEq)]
28pub struct SeparationCertificate {
29 pub conditioning: Vec<DenseNodeId>,
31}
32
33#[derive(Clone, Debug, Eq, PartialEq)]
35pub enum SeparationResult {
36 Separated {
38 conditioning: Vec<DenseNodeId>,
40 certificate: SeparationCertificate,
42 },
43 Connected {
46 active_path: Vec<PathStep>,
48 },
49}
50
51#[derive(Clone, Debug, Default)]
53pub struct DSeparationWorkspace {
54 pub ancestral: BitSet,
56 pub conditioning: BitSet,
58 pub undirected: Vec<Vec<DenseNodeId>>,
60 pub visited: BitSet,
62 pub frontier: Vec<DenseNodeId>,
64 pub pred: Vec<Option<DenseNodeId>>,
66 pub graph_ws: GraphWorkspace,
68}
69
70impl DSeparationWorkspace {
71 pub fn prepare(&mut self, n: usize) {
73 self.ancestral.resize(n);
74 self.conditioning.resize(n);
75 self.visited.resize(n);
76 self.undirected.resize(n, Vec::new());
77 for adj in &mut self.undirected {
78 adj.clear();
79 }
80 self.frontier.clear();
81 self.pred.clear();
82 self.pred.resize(n, None);
83 self.graph_ws.prepare(n);
84 }
85}
86
87impl Dag {
88 pub fn is_d_separated(
94 &self,
95 x: DenseNodeId,
96 y: DenseNodeId,
97 z: &[DenseNodeId],
98 ws: &mut DSeparationWorkspace,
99 ) -> Result<bool, GraphError> {
100 self.is_d_separated_with(x, y, z, ws, None)
101 }
102
103 pub(crate) fn is_d_separated_with(
105 &self,
106 x: DenseNodeId,
107 y: DenseNodeId,
108 z: &[DenseNodeId],
109 ws: &mut DSeparationWorkspace,
110 overlay: Option<&GraphOverlay>,
111 ) -> Result<bool, GraphError> {
112 self.validate_node_pub(x)?;
113 self.validate_node_pub(y)?;
114 for &v in z {
115 self.validate_node_pub(v)?;
116 }
117 if x == y {
118 return Ok(false);
119 }
120 Ok(self.d_sep_active_path(x, y, z, ws, overlay).is_none())
121 }
122
123 pub fn is_d_separated_batch(
129 &self,
130 queries: &[(DenseNodeId, DenseNodeId, &[DenseNodeId])],
131 out: &mut [bool],
132 ws: &mut DSeparationWorkspace,
133 ) -> Result<(), GraphError> {
134 if out.len() != queries.len() {
135 return Err(GraphError::InvalidEndpoints { message: "batch output length mismatch" });
136 }
137 for (i, &(x, y, z)) in queries.iter().enumerate() {
138 out[i] = self.is_d_separated(x, y, z, ws)?;
139 }
140 Ok(())
141 }
142
143 pub fn d_separation(
149 &self,
150 x: DenseNodeId,
151 y: DenseNodeId,
152 z: &[DenseNodeId],
153 ws: &mut DSeparationWorkspace,
154 ) -> Result<SeparationResult, GraphError> {
155 self.d_separation_with(x, y, z, ws, None)
156 }
157
158 pub(crate) fn d_separation_with(
160 &self,
161 x: DenseNodeId,
162 y: DenseNodeId,
163 z: &[DenseNodeId],
164 ws: &mut DSeparationWorkspace,
165 overlay: Option<&GraphOverlay>,
166 ) -> Result<SeparationResult, GraphError> {
167 self.validate_node_pub(x)?;
168 self.validate_node_pub(y)?;
169 for &v in z {
170 self.validate_node_pub(v)?;
171 }
172 if x == y {
173 return Ok(SeparationResult::Connected { active_path: vec![PathStep { node: x }] });
174 }
175 if let Some(path) = self.d_sep_active_path(x, y, z, ws, overlay) {
176 Ok(SeparationResult::Connected {
177 active_path: path.into_iter().map(|node| PathStep { node }).collect(),
178 })
179 } else {
180 Ok(SeparationResult::Separated {
181 conditioning: z.to_vec(),
182 certificate: SeparationCertificate { conditioning: z.to_vec() },
183 })
184 }
185 }
186
187 fn d_sep_active_path(
189 &self,
190 x: DenseNodeId,
191 y: DenseNodeId,
192 z: &[DenseNodeId],
193 ws: &mut DSeparationWorkspace,
194 overlay: Option<&GraphOverlay>,
195 ) -> Option<Vec<DenseNodeId>> {
196 let n = self.node_count();
197 ws.prepare(n);
198
199 let mut seeds = Vec::with_capacity(2 + z.len());
201 seeds.push(x);
202 seeds.push(y);
203 seeds.extend_from_slice(z);
204 self.ancestors_of_with(&seeds, &mut ws.ancestral, &mut ws.graph_ws, overlay);
205
206 ws.conditioning.clear();
207 for &v in z {
208 ws.conditioning.insert(v);
209 }
210
211 for i in 0..n {
213 let u = DenseNodeId::from_raw(u32::try_from(i).expect("fit"));
214 if !ws.ancestral.contains(u) {
215 continue;
216 }
217 for &c in self.children(u) {
219 if overlay.is_some_and(|ov| !ov.edge_visible(u, c)) {
220 continue;
221 }
222 if ws.ancestral.contains(c) {
223 add_undirected(&mut ws.undirected, u, c);
224 }
225 }
226 let parents = self.parents(u);
228 for (a_idx, &a) in parents.iter().enumerate() {
229 if overlay.is_some_and(|ov| !ov.edge_visible(a, u)) {
230 continue;
231 }
232 if !ws.ancestral.contains(a) {
233 continue;
234 }
235 for &b in &parents[a_idx + 1..] {
236 if overlay.is_some_and(|ov| !ov.edge_visible(b, u)) {
237 continue;
238 }
239 if ws.ancestral.contains(b) {
240 add_undirected(&mut ws.undirected, a, b);
241 }
242 }
243 }
244 }
245
246 ws.visited.clear();
248 for p in &mut ws.pred {
249 *p = None;
250 }
251 if ws.conditioning.contains(x) || ws.conditioning.contains(y) {
252 return None;
256 }
257 ws.frontier.clear();
258 ws.frontier.push(x);
259 ws.visited.insert(x);
260 while let Some(u) = ws.frontier.pop() {
261 if u == y {
262 return Some(reconstruct_path(&ws.pred, x, y));
263 }
264 for &v in &ws.undirected[u.as_usize()] {
265 if ws.conditioning.contains(v) || ws.visited.contains(v) {
266 continue;
267 }
268 if !ws.ancestral.contains(v) {
269 continue;
270 }
271 ws.visited.insert(v);
272 ws.pred[v.as_usize()] = Some(u);
273 ws.frontier.push(v);
274 }
275 }
276 None
277 }
278}
279
280fn add_undirected(adj: &mut [Vec<DenseNodeId>], a: DenseNodeId, b: DenseNodeId) {
281 if a == b {
282 return;
283 }
284 let ai = a.as_usize();
285 let bi = b.as_usize();
286 if !adj[ai].contains(&b) {
287 adj[ai].push(b);
288 }
289 if !adj[bi].contains(&a) {
290 adj[bi].push(a);
291 }
292}
293
294fn reconstruct_path(
295 pred: &[Option<DenseNodeId>],
296 start: DenseNodeId,
297 end: DenseNodeId,
298) -> Vec<DenseNodeId> {
299 let mut path = vec![end];
300 let mut cur = end;
301 while cur != start {
302 cur = pred[cur.as_usize()].expect("path predecessor");
303 path.push(cur);
304 }
305 path.reverse();
306 path
307}
308
309#[cfg(test)]
310#[path = "dsep_tests.rs"]
311mod tests;