#![allow(clippy::many_single_char_names)]
use crate::dag::Dag;
use crate::error::GraphError;
use crate::overlay::GraphOverlay;
use crate::types::DenseNodeId;
use crate::workspace::{BitSet, GraphWorkspace};
#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)]
pub struct PathStep {
pub node: DenseNodeId,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct SeparationCertificate {
pub conditioning: Vec<DenseNodeId>,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum SeparationResult {
Separated {
conditioning: Vec<DenseNodeId>,
certificate: SeparationCertificate,
},
Connected {
active_path: Vec<PathStep>,
},
}
#[derive(Clone, Debug, Default)]
pub struct DSeparationWorkspace {
pub ancestral: BitSet,
pub conditioning: BitSet,
pub undirected: Vec<Vec<DenseNodeId>>,
pub visited: BitSet,
pub frontier: Vec<DenseNodeId>,
pub pred: Vec<Option<DenseNodeId>>,
pub graph_ws: GraphWorkspace,
}
impl DSeparationWorkspace {
pub fn prepare(&mut self, n: usize) {
self.ancestral.resize(n);
self.conditioning.resize(n);
self.visited.resize(n);
self.undirected.resize(n, Vec::new());
for adj in &mut self.undirected {
adj.clear();
}
self.frontier.clear();
self.pred.clear();
self.pred.resize(n, None);
self.graph_ws.prepare(n);
}
}
impl Dag {
pub fn is_d_separated(
&self,
x: DenseNodeId,
y: DenseNodeId,
z: &[DenseNodeId],
ws: &mut DSeparationWorkspace,
) -> Result<bool, GraphError> {
self.is_d_separated_with(x, y, z, ws, None)
}
pub(crate) fn is_d_separated_with(
&self,
x: DenseNodeId,
y: DenseNodeId,
z: &[DenseNodeId],
ws: &mut DSeparationWorkspace,
overlay: Option<&GraphOverlay>,
) -> Result<bool, GraphError> {
self.validate_node_pub(x)?;
self.validate_node_pub(y)?;
for &v in z {
self.validate_node_pub(v)?;
}
if x == y {
return Ok(false);
}
if z.iter().any(|&v| v == x || v == y) {
return Ok(false);
}
Ok(self.d_sep_active_path(x, y, z, ws, overlay).is_none())
}
pub fn is_d_separated_batch(
&self,
queries: &[(DenseNodeId, DenseNodeId, &[DenseNodeId])],
out: &mut [bool],
ws: &mut DSeparationWorkspace,
) -> Result<(), GraphError> {
if out.len() != queries.len() {
return Err(GraphError::InvalidEndpoints { message: "batch output length mismatch" });
}
for (i, &(x, y, z)) in queries.iter().enumerate() {
out[i] = self.is_d_separated(x, y, z, ws)?;
}
Ok(())
}
pub fn d_separation(
&self,
x: DenseNodeId,
y: DenseNodeId,
z: &[DenseNodeId],
ws: &mut DSeparationWorkspace,
) -> Result<SeparationResult, GraphError> {
self.d_separation_with(x, y, z, ws, None)
}
pub(crate) fn d_separation_with(
&self,
x: DenseNodeId,
y: DenseNodeId,
z: &[DenseNodeId],
ws: &mut DSeparationWorkspace,
overlay: Option<&GraphOverlay>,
) -> Result<SeparationResult, GraphError> {
self.validate_node_pub(x)?;
self.validate_node_pub(y)?;
for &v in z {
self.validate_node_pub(v)?;
}
if x == y {
return Ok(SeparationResult::Connected { active_path: vec![PathStep { node: x }] });
}
if z.iter().any(|&v| v == x || v == y) {
return Ok(SeparationResult::Connected {
active_path: vec![PathStep { node: x }, PathStep { node: y }],
});
}
if let Some(path) = self.d_sep_active_path(x, y, z, ws, overlay) {
Ok(SeparationResult::Connected {
active_path: path.into_iter().map(|node| PathStep { node }).collect(),
})
} else {
Ok(SeparationResult::Separated {
conditioning: z.to_vec(),
certificate: SeparationCertificate { conditioning: z.to_vec() },
})
}
}
fn d_sep_active_path(
&self,
x: DenseNodeId,
y: DenseNodeId,
z: &[DenseNodeId],
ws: &mut DSeparationWorkspace,
overlay: Option<&GraphOverlay>,
) -> Option<Vec<DenseNodeId>> {
let n = self.node_count();
ws.prepare(n);
let mut seeds = Vec::with_capacity(2 + z.len());
seeds.push(x);
seeds.push(y);
seeds.extend_from_slice(z);
self.ancestors_of_with(&seeds, &mut ws.ancestral, &mut ws.graph_ws, overlay);
ws.conditioning.clear();
for &v in z {
ws.conditioning.insert(v);
}
for i in 0..n {
let u = DenseNodeId::from_raw(u32::try_from(i).expect("fit"));
if !ws.ancestral.contains(u) {
continue;
}
for &c in self.children(u) {
if overlay.is_some_and(|ov| !ov.edge_visible(u, c)) {
continue;
}
if ws.ancestral.contains(c) {
add_undirected(&mut ws.undirected, u, c);
}
}
let parents = self.parents(u);
for (a_idx, &a) in parents.iter().enumerate() {
if overlay.is_some_and(|ov| !ov.edge_visible(a, u)) {
continue;
}
if !ws.ancestral.contains(a) {
continue;
}
for &b in &parents[a_idx + 1..] {
if overlay.is_some_and(|ov| !ov.edge_visible(b, u)) {
continue;
}
if ws.ancestral.contains(b) {
add_undirected(&mut ws.undirected, a, b);
}
}
}
}
ws.visited.clear();
for p in &mut ws.pred {
*p = None;
}
ws.frontier.clear();
ws.frontier.push(x);
ws.visited.insert(x);
while let Some(u) = ws.frontier.pop() {
if u == y {
return Some(reconstruct_path(&ws.pred, x, y));
}
for &v in &ws.undirected[u.as_usize()] {
if ws.conditioning.contains(v) || ws.visited.contains(v) {
continue;
}
if !ws.ancestral.contains(v) {
continue;
}
ws.visited.insert(v);
ws.pred[v.as_usize()] = Some(u);
ws.frontier.push(v);
}
}
None
}
}
fn add_undirected(adj: &mut [Vec<DenseNodeId>], a: DenseNodeId, b: DenseNodeId) {
if a == b {
return;
}
let ai = a.as_usize();
let bi = b.as_usize();
if !adj[ai].contains(&b) {
adj[ai].push(b);
}
if !adj[bi].contains(&a) {
adj[bi].push(a);
}
}
fn reconstruct_path(
pred: &[Option<DenseNodeId>],
start: DenseNodeId,
end: DenseNodeId,
) -> Vec<DenseNodeId> {
let mut path = vec![end];
let mut cur = end;
while cur != start {
cur = pred[cur.as_usize()].expect("path predecessor");
path.push(cur);
}
path.reverse();
path
}
#[cfg(test)]
#[path = "dsep_tests.rs"]
mod tests;