1use std::sync::Arc;
6
7use antecedent_core::{
8 Intervention, InterventionSequence, MechanismOverride, StochasticPolicy, TemporalPolicy,
9 VariableId,
10};
11use antecedent_graph::DenseNodeId;
12
13use crate::compile::CompiledCausalModel;
14use crate::error::ModelError;
15
16#[derive(Clone, Debug, Default)]
21pub struct InterventionOverlay {
22 pub hard_set: Vec<Option<f64>>,
24 pub shifts: Vec<f64>,
26 pub stochastic: Vec<Option<StochasticPolicy>>,
28 pub soft: Vec<Option<MechanismOverride>>,
30 pub active: Vec<bool>,
32}
33
34impl InterventionOverlay {
35 #[must_use]
37 pub fn observational(n_nodes: usize) -> Self {
38 Self {
39 hard_set: vec![None; n_nodes],
40 shifts: vec![0.0; n_nodes],
41 stochastic: vec![None; n_nodes],
42 soft: vec![None; n_nodes],
43 active: vec![true; n_nodes],
44 }
45 }
46
47 #[must_use]
49 pub fn is_empty(&self) -> bool {
50 self.hard_set.iter().all(Option::is_none)
51 && self.shifts.iter().all(|s| *s == 0.0)
52 && self.stochastic.iter().all(Option::is_none)
53 && self.soft.iter().all(Option::is_none)
54 && self.active.iter().all(|a| *a)
55 }
56
57 pub fn from_interventions(
63 model: &CompiledCausalModel,
64 interventions: &[Intervention],
65 ) -> Result<Self, ModelError> {
66 let mut overlay = Self::observational(model.n_nodes());
67 for iv in interventions {
68 apply_intervention(model, &mut overlay, iv, true)?;
69 }
70 Ok(overlay)
71 }
72
73 pub fn from_sequence_at(
79 model: &CompiledCausalModel,
80 seq: &InterventionSequence,
81 t: i32,
82 ) -> Result<Self, ModelError> {
83 let mut overlay = Self::observational(model.n_nodes());
84 for step in seq.steps.iter() {
85 if temporal_active(&step.temporal, t)? {
86 apply_intervention(model, &mut overlay, &step.intervention, true)?;
87 }
88 }
89 Ok(overlay)
90 }
91}
92
93fn temporal_active(policy: &TemporalPolicy, t: i32) -> Result<bool, ModelError> {
94 policy.validate().map_err(|e| ModelError::Unsupported { message: e.to_string() })?;
95 Ok(policy.is_active_at(t))
96}
97
98fn apply_intervention(
99 model: &CompiledCausalModel,
100 overlay: &mut InterventionOverlay,
101 iv: &Intervention,
102 allow_nested_sequence: bool,
103) -> Result<(), ModelError> {
104 match iv {
105 Intervention::Set { variable, value } => {
106 let dense = require_dense(model, *variable)?;
107 let v = value.as_f64().ok_or_else(|| ModelError::Unsupported {
108 message: "hard set requires numeric value".into(),
109 })?;
110 overlay.hard_set[dense.as_usize()] = Some(v);
111 Ok(())
112 }
113 Intervention::Shift { variable, delta } => {
114 let dense = require_dense(model, *variable)?;
115 let d = delta.as_f64().ok_or_else(|| ModelError::Unsupported {
116 message: "shift requires numeric delta".into(),
117 })?;
118 overlay.shifts[dense.as_usize()] += d;
119 Ok(())
120 }
121 Intervention::Stochastic { variable, policy } => {
122 policy.validate().map_err(|e| ModelError::Unsupported { message: e.to_string() })?;
123 let dense = require_dense(model, *variable)?;
124 overlay.stochastic[dense.as_usize()] = Some(policy.clone());
125 Ok(())
126 }
127 Intervention::Soft { variable, mechanism } => {
128 let dense = require_dense(model, *variable)?;
129 if mechanism.family_id.as_ref() == "additive_shift" {
132 let d = mechanism.parameters.first().copied().unwrap_or(0.0);
133 overlay.shifts[dense.as_usize()] += d;
134 return Ok(());
135 }
136 overlay.soft[dense.as_usize()] = Some(mechanism.clone());
137 Ok(())
138 }
139 Intervention::Sequence(seq) => {
140 if !allow_nested_sequence {
141 return Err(ModelError::Unsupported {
142 message: "nested intervention sequences are not supported here".into(),
143 });
144 }
145 for step in seq.steps.iter() {
147 if temporal_active(&step.temporal, 0)? {
148 apply_intervention(model, overlay, &step.intervention, false)?;
149 }
150 }
151 Ok(())
152 }
153 _ => Err(ModelError::Unsupported { message: "unknown intervention variant".into() }),
154 }
155}
156
157fn require_dense(model: &CompiledCausalModel, var: VariableId) -> Result<DenseNodeId, ModelError> {
158 model.dense_of(var).ok_or_else(|| ModelError::Shape {
159 message: format!("variable {var} not in compiled model"),
160 })
161}
162
163#[derive(Clone, Debug)]
165pub struct ModelView<'a> {
166 pub model: &'a CompiledCausalModel,
168 pub overlay: Arc<InterventionOverlay>,
170}
171
172impl<'a> ModelView<'a> {
173 #[must_use]
175 pub fn observational(model: &'a CompiledCausalModel) -> Self {
176 Self { model, overlay: Arc::new(InterventionOverlay::observational(model.n_nodes())) }
177 }
178
179 #[must_use]
181 pub fn with_overlay(model: &'a CompiledCausalModel, overlay: InterventionOverlay) -> Self {
182 Self { model, overlay: Arc::new(overlay) }
183 }
184}
185
186#[cfg(test)]
187mod tests {
188 use super::*;
189 use antecedent_core::{
190 DynamicRuleId, Intervention, InterventionSequence, SequencedIntervention, TemporalPolicy,
191 Value, VariableId,
192 };
193 use antecedent_graph::Dag;
194
195 #[test]
196 fn hard_set_overlay() {
197 let g = Dag::with_variables(2);
198 let model = CompiledCausalModel::compile(g).unwrap();
199 let t = VariableId::from_raw(0);
200 let overlay = InterventionOverlay::from_interventions(
201 &model,
202 &[Intervention::set(t, Value::f64(1.0))],
203 )
204 .unwrap();
205 assert_eq!(overlay.hard_set[0], Some(1.0));
206 assert!(overlay.hard_set[1].is_none());
207 }
208
209 #[test]
210 fn dynamic_policy_sequence_activates_on_schedule() {
211 let g = Dag::with_variables(1);
212 let model = CompiledCausalModel::compile(g).unwrap();
213 let t = VariableId::from_raw(0);
214 let seq = InterventionSequence::new(vec![SequencedIntervention::new(
215 Intervention::set(t, Value::f64(1.0)),
216 TemporalPolicy::dynamic(DynamicRuleId::from_raw(0), [0, 2]),
217 )]);
218 let overlay = InterventionOverlay::from_sequence_at(&model, &seq, 0).unwrap();
219 assert_eq!(overlay.hard_set[0], Some(1.0));
220 let idle = InterventionOverlay::from_sequence_at(&model, &seq, 1).unwrap();
221 assert!(idle.hard_set[0].is_none());
222 let again = InterventionOverlay::from_sequence_at(&model, &seq, 2).unwrap();
223 assert_eq!(again.hard_set[0], Some(1.0));
224 }
225}