Skip to main content

antecedent_core/query/
counterfactual.rs

1//! Query submodule.
2//!
3//! SPDX-License-Identifier: MIT OR Apache-2.0
4
5use std::sync::Arc;
6
7use crate::ids::VariableId;
8use crate::intervention::Intervention;
9
10use super::error::QueryError;
11
12#[derive(Clone, Debug, PartialEq)]
13/// Counterfactual query over factual observations and interventions.
14pub struct CounterfactualQuery {
15    /// Outcome variable(s) to predict under the counterfactual world.
16    pub outcomes: Arc<[VariableId]>,
17    /// Interventions defining the counterfactual world (applied after abduction).
18    pub interventions: Arc<[Intervention]>,
19    /// When true, allow nested counterfactual interventions under invertible SCMs.
20    pub allow_nested: bool,
21}
22
23impl CounterfactualQuery {
24    /// Construct a single-outcome counterfactual query.
25    #[must_use]
26    pub fn new(outcome: VariableId, interventions: impl Into<Arc<[Intervention]>>) -> Self {
27        Self {
28            outcomes: Arc::from([outcome]),
29            interventions: interventions.into(),
30            allow_nested: false,
31        }
32    }
33
34    /// Enable nested interventions where the model supports them.
35    #[must_use]
36    pub const fn with_nested(mut self, allow_nested: bool) -> Self {
37        self.allow_nested = allow_nested;
38        self
39    }
40
41    /// Validate interventions.
42    ///
43    /// # Errors
44    ///
45    /// Empty outcomes or invalid interventions.
46    pub fn validate(&self) -> Result<(), QueryError> {
47        if self.outcomes.is_empty() {
48            return Err(QueryError::EmptyCounterfactualOutcomes);
49        }
50        for iv in self.interventions.iter() {
51            iv.validate().map_err(|e| QueryError::InvalidIntervention(e.to_string()))?;
52        }
53        Ok(())
54    }
55}