antecedent_core/query/
temporal.rs1use crate::ids::VariableId;
6use crate::intervention::{Intervention, TemporalPolicy};
7use crate::value::Value;
8
9use super::TargetPopulation;
10use super::error::QueryError;
11
12#[derive(Clone, Debug, PartialEq)]
14pub struct TemporalEffectQuery {
15 pub treatment: VariableId,
17 pub outcome: VariableId,
19 pub policy: TemporalPolicy,
21 pub control: Intervention,
23 pub active: Intervention,
25 pub horizon_steps: u32,
27 pub max_history_lag: Option<u32>,
29 pub target_population: TargetPopulation,
31}
32
33impl TemporalEffectQuery {
34 #[must_use]
36 pub fn pulse(treatment: VariableId, outcome: VariableId, active_level: f64) -> Self {
37 Self {
38 treatment,
39 outcome,
40 policy: TemporalPolicy::pulse(0),
41 control: Intervention::set(treatment, Value::f64(0.0)),
42 active: Intervention::set(treatment, Value::f64(active_level)),
43 horizon_steps: 1,
44 max_history_lag: None,
45 target_population: TargetPopulation::AllObserved,
46 }
47 }
48
49 #[must_use]
51 pub fn sustained(
52 treatment: VariableId,
53 outcome: VariableId,
54 until: i32,
55 active_level: f64,
56 ) -> Self {
57 Self {
58 treatment,
59 outcome,
60 policy: TemporalPolicy::sustained(0, until),
61 control: Intervention::set(treatment, Value::f64(0.0)),
62 active: Intervention::set(treatment, Value::f64(active_level)),
63 horizon_steps: 1,
64 max_history_lag: None,
65 target_population: TargetPopulation::AllObserved,
66 }
67 }
68
69 #[must_use]
71 pub const fn with_horizon_steps(mut self, horizon_steps: u32) -> Self {
72 self.horizon_steps = horizon_steps;
73 self
74 }
75
76 #[must_use]
78 pub const fn with_max_history_lag(mut self, max_history_lag: Option<u32>) -> Self {
79 self.max_history_lag = max_history_lag;
80 self
81 }
82
83 #[must_use]
85 pub fn with_policy(mut self, policy: TemporalPolicy) -> Self {
86 self.policy = policy;
87 self
88 }
89
90 #[must_use]
92 pub fn with_target_population(mut self, population: TargetPopulation) -> Self {
93 self.target_population = population;
94 self
95 }
96
97 pub fn validate(&self) -> Result<(), QueryError> {
103 if self.treatment == self.outcome {
104 return Err(QueryError::TreatmentEqualsOutcome { id: self.treatment });
105 }
106 if self.horizon_steps == 0 {
107 return Err(QueryError::NonPositiveHorizon);
108 }
109 self.policy.validate().map_err(|e| match e {
110 crate::intervention::InterventionError::InvalidTemporalWindow { from, until } => {
111 QueryError::InvalidTemporalWindow { from, until }
112 }
113 other => QueryError::InvalidIntervention(other.to_string()),
114 })?;
115 self.target_population.validate()?;
116 let control_var =
117 self.control.primary_variable().ok_or(QueryError::AmbiguousInterventionTarget)?;
118 if control_var != self.treatment {
119 return Err(QueryError::InterventionVariableMismatch {
120 expected: self.treatment,
121 got: control_var,
122 });
123 }
124 let active_var =
125 self.active.primary_variable().ok_or(QueryError::AmbiguousInterventionTarget)?;
126 if active_var != self.treatment {
127 return Err(QueryError::InterventionVariableMismatch {
128 expected: self.treatment,
129 got: active_var,
130 });
131 }
132 Ok(())
133 }
134
135 #[must_use]
137 pub fn treatment_offset(&self) -> i32 {
138 self.try_treatment_offset().unwrap_or_default()
139 }
140
141 pub fn try_treatment_offset(&self) -> Result<i32, QueryError> {
147 match &self.policy {
148 TemporalPolicy::Pulse { at } => Ok(*at),
149 TemporalPolicy::Sustained { from, .. } => Ok(*from),
150 TemporalPolicy::Dynamic { active_at, .. } => {
151 active_at.first().copied().ok_or(QueryError::DynamicPolicyHasNoTreatmentOffset)
152 }
153 }
154 }
155
156 #[must_use]
158 pub fn outcome_offset(&self) -> i32 {
159 i32::try_from(self.horizon_steps.saturating_sub(1)).unwrap_or(i32::MAX)
160 }
161}