use std::sync::Arc;
use crate::ids::{DynamicRuleId, VariableId};
use crate::value::Value;
#[derive(Clone, Debug, Eq, PartialEq, Hash)]
#[non_exhaustive]
pub enum TemporalPolicy {
Pulse {
at: i32,
},
Sustained {
from: i32,
until: i32,
},
Dynamic {
rule: DynamicRuleId,
active_at: Arc<[i32]>,
},
}
impl TemporalPolicy {
#[must_use]
pub const fn pulse(at: i32) -> Self {
Self::Pulse { at }
}
#[must_use]
pub const fn sustained(from: i32, until: i32) -> Self {
Self::Sustained { from, until }
}
#[must_use]
pub fn dynamic(rule: DynamicRuleId, active_at: impl Into<Arc<[i32]>>) -> Self {
let mut steps: Vec<i32> = active_at.into().as_ref().to_vec();
steps.sort_unstable();
steps.dedup();
Self::Dynamic { rule, active_at: Arc::from(steps) }
}
#[must_use]
pub fn is_active_at(&self, t: i32) -> bool {
match self {
Self::Pulse { at } => t == *at,
Self::Sustained { from, until } => t >= *from && t <= *until,
Self::Dynamic { active_at, .. } => active_at.binary_search(&t).is_ok(),
}
}
pub fn active_offsets(&self) -> Result<Arc<[i32]>, InterventionError> {
match self {
Self::Pulse { at } => Ok(Arc::from([*at])),
Self::Sustained { from, until } => {
if *until < *from {
return Err(InterventionError::InvalidTemporalWindow {
from: *from,
until: *until,
});
}
let steps: Vec<i32> = (*from..=*until).collect();
Ok(Arc::from(steps))
}
Self::Dynamic { active_at, .. } => {
if active_at.is_empty() {
return Err(InterventionError::EmptyDynamicSchedule);
}
Ok(Arc::clone(active_at))
}
}
}
pub fn validate(&self) -> Result<(), InterventionError> {
match self {
Self::Pulse { .. } => Ok(()),
Self::Sustained { from, until } => {
if *until < *from {
return Err(InterventionError::InvalidTemporalWindow {
from: *from,
until: *until,
});
}
Ok(())
}
Self::Dynamic { active_at, .. } => {
if active_at.is_empty() {
return Err(InterventionError::EmptyDynamicSchedule);
}
Ok(())
}
}
}
}
#[derive(Clone, Debug, PartialEq)]
pub struct MechanismOverride {
pub family_id: Arc<str>,
pub parameters: Arc<[f64]>,
}
impl MechanismOverride {
#[must_use]
pub fn named(family_id: impl Into<Arc<str>>, parameters: impl Into<Arc<[f64]>>) -> Self {
Self { family_id: family_id.into(), parameters: parameters.into() }
}
#[must_use]
pub fn constant(value: f64) -> Self {
Self::named("constant", Arc::<[f64]>::from(vec![value]))
}
#[must_use]
pub fn additive_shift(delta: f64) -> Self {
Self::named("additive_shift", Arc::<[f64]>::from(vec![delta]))
}
}
impl Eq for MechanismOverride {}
impl core::hash::Hash for MechanismOverride {
fn hash<H: core::hash::Hasher>(&self, state: &mut H) {
self.family_id.hash(state);
for p in self.parameters.iter() {
p.to_bits().hash(state);
}
}
}
#[derive(Clone, Debug, PartialEq)]
#[non_exhaustive]
pub enum StochasticPolicy {
Bernoulli {
p: f64,
},
Gaussian {
mean: f64,
variance: f64,
},
Categorical {
probs: Arc<[f64]>,
},
}
impl StochasticPolicy {
#[must_use]
pub const fn bernoulli(p: f64) -> Self {
Self::Bernoulli { p }
}
#[must_use]
pub const fn gaussian(mean: f64, variance: f64) -> Self {
Self::Gaussian { mean, variance }
}
#[must_use]
pub fn categorical(probs: impl Into<Arc<[f64]>>) -> Self {
Self::Categorical { probs: probs.into() }
}
pub fn validate(&self) -> Result<(), InterventionError> {
match self {
Self::Bernoulli { p } => {
if !(0.0..=1.0).contains(p) || !p.is_finite() {
return Err(InterventionError::InvalidStochasticPolicy {
message: "Bernoulli p must be finite and in [0, 1]",
});
}
}
Self::Gaussian { variance, .. } => {
if !(variance.is_finite() && *variance > 0.0) {
return Err(InterventionError::InvalidStochasticPolicy {
message: "Gaussian variance must be finite and > 0",
});
}
}
Self::Categorical { probs } => {
if probs.is_empty() {
return Err(InterventionError::InvalidStochasticPolicy {
message: "categorical probs must be non-empty",
});
}
if probs.iter().any(|p| !(p.is_finite() && *p >= 0.0)) {
return Err(InterventionError::InvalidStochasticPolicy {
message: "categorical probs must be finite and >= 0",
});
}
let sum: f64 = probs.iter().sum();
if sum.partial_cmp(&0.0) != Some(std::cmp::Ordering::Greater) {
return Err(InterventionError::InvalidStochasticPolicy {
message: "categorical probs must sum to a positive value",
});
}
}
}
Ok(())
}
}
impl Eq for StochasticPolicy {}
impl core::hash::Hash for StochasticPolicy {
fn hash<H: core::hash::Hasher>(&self, state: &mut H) {
core::mem::discriminant(self).hash(state);
match self {
Self::Bernoulli { p } => p.to_bits().hash(state),
Self::Gaussian { mean, variance } => {
mean.to_bits().hash(state);
variance.to_bits().hash(state);
}
Self::Categorical { probs } => {
for p in probs.iter() {
p.to_bits().hash(state);
}
}
}
}
}
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub struct SequencedIntervention {
pub intervention: Intervention,
pub temporal: TemporalPolicy,
}
impl SequencedIntervention {
#[must_use]
pub fn new(intervention: Intervention, temporal: TemporalPolicy) -> Self {
Self { intervention, temporal }
}
}
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub struct InterventionSequence {
pub steps: Arc<[SequencedIntervention]>,
}
impl InterventionSequence {
#[must_use]
pub fn new(steps: impl Into<Arc<[SequencedIntervention]>>) -> Self {
Self { steps: steps.into() }
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.steps.is_empty()
}
#[must_use]
pub fn len(&self) -> usize {
self.steps.len()
}
}
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum Intervention {
Set {
variable: VariableId,
value: Value,
},
Shift {
variable: VariableId,
delta: Value,
},
Stochastic {
variable: VariableId,
policy: StochasticPolicy,
},
Soft {
variable: VariableId,
mechanism: MechanismOverride,
},
Sequence(InterventionSequence),
}
impl Intervention {
#[must_use]
pub const fn set(variable: VariableId, value: Value) -> Self {
Self::Set { variable, value }
}
#[must_use]
pub const fn shift(variable: VariableId, delta: Value) -> Self {
Self::Shift { variable, delta }
}
#[must_use]
pub const fn stochastic(variable: VariableId, policy: StochasticPolicy) -> Self {
Self::Stochastic { variable, policy }
}
#[must_use]
pub fn soft(variable: VariableId, mechanism: MechanismOverride) -> Self {
Self::Soft { variable, mechanism }
}
#[must_use]
pub fn sequence(seq: InterventionSequence) -> Self {
Self::Sequence(seq)
}
#[must_use]
pub fn primary_variable(&self) -> Option<VariableId> {
match self {
Self::Set { variable, .. }
| Self::Shift { variable, .. }
| Self::Stochastic { variable, .. }
| Self::Soft { variable, .. } => Some(*variable),
Self::Sequence(seq) => {
if seq.steps.is_empty() {
return None;
}
let first = seq.steps[0].intervention.primary_variable()?;
if seq.steps.iter().all(|s| s.intervention.primary_variable() == Some(first)) {
Some(first)
} else {
None
}
}
}
}
pub fn validate(&self) -> Result<(), InterventionError> {
match self {
Self::Set { .. } | Self::Shift { .. } | Self::Soft { .. } => Ok(()),
Self::Stochastic { policy, .. } => policy.validate(),
Self::Sequence(seq) => {
if seq.is_empty() {
return Err(InterventionError::EmptySequence);
}
for step in seq.steps.iter() {
step.temporal.validate()?;
step.intervention.validate()?;
}
Ok(())
}
}
}
#[must_use]
pub const fn is_hard_set(&self) -> bool {
matches!(self, Self::Set { .. })
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum InterventionError {
InvalidStochasticPolicy {
message: &'static str,
},
EmptySequence,
InvalidTemporalWindow {
from: i32,
until: i32,
},
EmptyDynamicSchedule,
}
impl core::fmt::Display for InterventionError {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
Self::InvalidStochasticPolicy { message } => {
write!(f, "invalid stochastic policy: {message}")
}
Self::EmptySequence => write!(f, "intervention sequence is empty"),
Self::InvalidTemporalWindow { from, until } => {
write!(f, "invalid temporal window [{from}, {until}]")
}
Self::EmptyDynamicSchedule => {
write!(f, "TemporalPolicy::Dynamic requires a non-empty active_at schedule")
}
}
}
}
impl std::error::Error for InterventionError {}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn hard_set_primary_variable() {
let v = VariableId::from_raw(3);
let i = Intervention::set(v, Value::f64(1.0));
assert_eq!(i.primary_variable(), Some(v));
assert!(i.is_hard_set());
i.validate().unwrap();
}
#[test]
fn stochastic_bernoulli_validates() {
let v = VariableId::from_raw(0);
let ok = Intervention::stochastic(v, StochasticPolicy::bernoulli(0.4));
ok.validate().unwrap();
let bad = Intervention::stochastic(v, StochasticPolicy::bernoulli(1.5));
assert!(bad.validate().is_err());
}
#[test]
fn sequence_rejects_empty() {
let seq = Intervention::sequence(InterventionSequence::new(Vec::new()));
assert!(matches!(seq.validate(), Err(InterventionError::EmptySequence)));
}
#[test]
fn sequence_uniform_primary() {
let v = VariableId::from_raw(1);
let seq = Intervention::sequence(InterventionSequence::new(vec![
SequencedIntervention::new(
Intervention::set(v, Value::f64(1.0)),
TemporalPolicy::pulse(0),
),
SequencedIntervention::new(
Intervention::shift(v, Value::f64(0.1)),
TemporalPolicy::sustained(1, 3),
),
]));
seq.validate().unwrap();
assert_eq!(seq.primary_variable(), Some(v));
}
#[test]
fn soft_override_helpers() {
let m = MechanismOverride::constant(2.0);
assert_eq!(&*m.family_id, "constant");
assert_eq!(m.parameters.as_ref(), &[2.0]);
}
#[test]
fn dynamic_policy_validates() {
use crate::ids::DynamicRuleId;
let p = TemporalPolicy::dynamic(DynamicRuleId::from_raw(1), [0, 2, 5]);
p.validate().unwrap();
assert!(p.is_active_at(2));
assert!(!p.is_active_at(1));
assert_eq!(p.active_offsets().unwrap().as_ref(), &[0, 2, 5]);
let empty = TemporalPolicy::dynamic(DynamicRuleId::from_raw(1), []);
assert!(matches!(empty.validate(), Err(InterventionError::EmptyDynamicSchedule)));
}
}