Skip to main content

antecedent_graph/
selection.rs

1//! Selection diagrams for structural transportability.
2//!
3//! SPDX-License-Identifier: MIT OR Apache-2.0
4
5use std::collections::BTreeSet;
6use std::sync::Arc;
7
8use antecedent_core::VariableId;
9
10use crate::{Admg, GraphError};
11
12/// A source/target selection diagram.
13///
14/// `selection_targets` are causal variables whose generating mechanisms may differ between the
15/// source and target populations. Selection nodes are represented extensionally by their target;
16/// they are not ordinary observed variables and cannot accidentally enter an adjustment set.
17#[derive(Clone, Debug)]
18pub struct SelectionDiagram {
19    causal_graph: Admg,
20    selection_targets: Arc<[VariableId]>,
21}
22
23impl SelectionDiagram {
24    /// Build and validate a selection diagram over a semi-Markovian causal graph.
25    pub fn try_new(
26        causal_graph: Admg,
27        selection_targets: impl Into<Arc<[VariableId]>>,
28    ) -> Result<Self, GraphError> {
29        let selection_targets = selection_targets.into();
30        let mut seen = BTreeSet::new();
31        for target in selection_targets.iter().copied() {
32            if target.as_usize() >= causal_graph.node_count() {
33                return Err(GraphError::UnknownNode { id: target.raw() });
34            }
35            if !seen.insert(target.raw()) {
36                return Err(GraphError::InvalidSelectionDiagram {
37                    message: "selection targets must be unique".into(),
38                });
39            }
40        }
41        Ok(Self { causal_graph, selection_targets })
42    }
43
44    /// Borrow the causal ADMG shared by source and target populations.
45    #[must_use]
46    pub const fn causal_graph(&self) -> &Admg {
47        &self.causal_graph
48    }
49
50    /// Variables whose mechanisms are allowed to differ between populations.
51    #[must_use]
52    pub fn selection_targets(&self) -> &[VariableId] {
53        &self.selection_targets
54    }
55
56    /// Whether a variable's mechanism is marked as population-specific.
57    #[must_use]
58    pub fn mechanism_may_differ(&self, variable: VariableId) -> bool {
59        self.selection_targets.contains(&variable)
60    }
61}
62
63#[cfg(test)]
64mod tests {
65    use super::*;
66
67    #[test]
68    fn selection_targets_are_not_graph_nodes() {
69        let graph = Admg::with_variables(3);
70        let diagram = SelectionDiagram::try_new(graph, [VariableId::from_raw(1)]).unwrap();
71        assert_eq!(diagram.causal_graph().node_count(), 3);
72        assert!(diagram.mechanism_may_differ(VariableId::from_raw(1)));
73    }
74}