use crate::ids::VariableId;
use crate::intervention::{Intervention, TemporalPolicy};
use crate::value::Value;
use super::TargetPopulation;
use super::error::QueryError;
#[derive(Clone, Debug, PartialEq)]
pub struct TemporalEffectQuery {
pub treatment: VariableId,
pub outcome: VariableId,
pub policy: TemporalPolicy,
pub control: Intervention,
pub active: Intervention,
pub horizon_steps: u32,
pub max_history_lag: Option<u32>,
pub target_population: TargetPopulation,
}
impl TemporalEffectQuery {
#[must_use]
pub fn pulse(treatment: VariableId, outcome: VariableId, active_level: f64) -> Self {
Self {
treatment,
outcome,
policy: TemporalPolicy::pulse(0),
control: Intervention::set(treatment, Value::f64(0.0)),
active: Intervention::set(treatment, Value::f64(active_level)),
horizon_steps: 1,
max_history_lag: None,
target_population: TargetPopulation::AllObserved,
}
}
#[must_use]
pub fn sustained(
treatment: VariableId,
outcome: VariableId,
until: i32,
active_level: f64,
) -> Self {
Self {
treatment,
outcome,
policy: TemporalPolicy::sustained(0, until),
control: Intervention::set(treatment, Value::f64(0.0)),
active: Intervention::set(treatment, Value::f64(active_level)),
horizon_steps: 1,
max_history_lag: None,
target_population: TargetPopulation::AllObserved,
}
}
#[must_use]
pub const fn with_horizon_steps(mut self, horizon_steps: u32) -> Self {
self.horizon_steps = horizon_steps;
self
}
#[must_use]
pub const fn with_max_history_lag(mut self, max_history_lag: Option<u32>) -> Self {
self.max_history_lag = max_history_lag;
self
}
#[must_use]
pub fn with_policy(mut self, policy: TemporalPolicy) -> Self {
self.policy = policy;
self
}
#[must_use]
pub fn with_target_population(mut self, population: TargetPopulation) -> Self {
self.target_population = population;
self
}
pub fn validate(&self) -> Result<(), QueryError> {
if self.treatment == self.outcome {
return Err(QueryError::TreatmentEqualsOutcome { id: self.treatment });
}
if self.horizon_steps == 0 {
return Err(QueryError::NonPositiveHorizon);
}
self.policy.validate().map_err(|e| match e {
crate::intervention::InterventionError::InvalidTemporalWindow { from, until } => {
QueryError::InvalidTemporalWindow { from, until }
}
other => QueryError::InvalidIntervention(other.to_string()),
})?;
self.target_population.validate()?;
let control_var =
self.control.primary_variable().ok_or(QueryError::AmbiguousInterventionTarget)?;
if control_var != self.treatment {
return Err(QueryError::InterventionVariableMismatch {
expected: self.treatment,
got: control_var,
});
}
let active_var =
self.active.primary_variable().ok_or(QueryError::AmbiguousInterventionTarget)?;
if active_var != self.treatment {
return Err(QueryError::InterventionVariableMismatch {
expected: self.treatment,
got: active_var,
});
}
Ok(())
}
#[must_use]
pub fn treatment_offset(&self) -> i32 {
self.try_treatment_offset().unwrap_or_default()
}
pub fn try_treatment_offset(&self) -> Result<i32, QueryError> {
match &self.policy {
TemporalPolicy::Pulse { at } => Ok(*at),
TemporalPolicy::Sustained { from, .. } => Ok(*from),
TemporalPolicy::Dynamic { active_at, .. } => {
active_at.first().copied().ok_or(QueryError::DynamicPolicyHasNoTreatmentOffset)
}
}
}
#[must_use]
pub fn outcome_offset(&self) -> i32 {
i32::try_from(self.horizon_steps.saturating_sub(1)).unwrap_or(i32::MAX)
}
}