#![allow(clippy::cast_possible_truncation)]
use std::sync::Arc;
use antecedent_core::VariableId;
use antecedent_graph::{Dag, DenseNodeId, NodeRef};
use crate::error::ModelError;
#[derive(Clone, Debug)]
pub struct ParentGatherPlan {
pub child: DenseNodeId,
pub parents: Arc<[DenseNodeId]>,
}
impl ParentGatherPlan {
#[must_use]
pub fn n_parents(&self) -> usize {
self.parents.len()
}
pub fn gather(&self, values: &[f64], n_rows: usize, out: &mut [f64]) {
debug_assert!(out.len() >= self.parents.len().saturating_mul(n_rows));
for (pi, &p) in self.parents.iter().enumerate() {
let src = p.as_usize() * n_rows;
let dst = pi * n_rows;
out[dst..dst + n_rows].copy_from_slice(&values[src..src + n_rows]);
}
}
}
#[derive(Clone, Debug)]
pub struct ModelOutputLayout {
pub node_order: Arc<[DenseNodeId]>,
pub variables: Arc<[VariableId]>,
}
pub trait DynamicMechanism: Send + Sync {
fn sample_noise_column(
&self,
n_rows: usize,
rng: &mut antecedent_core::CausalRng,
output: &mut [f64],
) -> Result<(), ModelError>;
fn evaluate_column(
&self,
parents: crate::batch::ParentBatch<'_>,
noise: &[f64],
output: &mut [f64],
workspace: &mut crate::batch::MechanismWorkspace,
) -> Result<(), ModelError>;
fn infer_noise_column(
&self,
value: &[f64],
parents: crate::batch::ParentBatch<'_>,
output: &mut [f64],
) -> Result<(), ModelError> {
let n = parents.n_rows;
if value.len() < n || output.len() < n {
return Err(ModelError::Shape {
message: "dynamic infer_noise buffers too short".into(),
});
}
let zeros = vec![0.0; n];
let mut mean = vec![0.0; n];
let mut ws = crate::batch::MechanismWorkspace::default();
self.evaluate_column(parents, &zeros, &mut mean, &mut ws)?;
for i in 0..n {
output[i] = value[i] - mean[i];
}
Ok(())
}
fn log_prob_column(
&self,
values: &[f64],
parents: crate::batch::ParentBatch<'_>,
output: &mut [f64],
) -> Result<(), ModelError> {
let n = parents.n_rows;
if values.len() < n || output.len() < n {
return Err(ModelError::Shape { message: "dynamic log_prob buffers too short".into() });
}
let mut resid = vec![0.0; n];
self.infer_noise_column(values, parents, &mut resid)?;
let log_norm = -0.5 * (2.0 * std::f64::consts::PI).ln();
for i in 0..n {
output[i] = log_norm - 0.5 * resid[i] * resid[i];
}
Ok(())
}
}
#[derive(Clone, Default)]
pub enum MechanismSlot {
#[default]
Vacant,
Pending {
family_id: Arc<str>,
},
LinearGaussian {
intercept: f64,
coeffs: Arc<[f64]>,
sigma: f64,
},
Discrete {
support: Arc<[f64]>,
probs: Arc<[f64]>,
logit_coeffs: Option<Arc<[f64]>>,
},
Constant {
value: f64,
},
HierarchicalLinear {
intercept: f64,
coeffs: Arc<[f64]>,
sigma: f64,
shrinkage: f64,
},
Bvar {
intercept: f64,
coeffs: Arc<[f64]>,
sigma: f64,
},
LinearGaussianStateSpace {
a: f64,
process_std: f64,
obs_std: f64,
initial_mean: f64,
},
GaussianProcess {
length_scale: f64,
variance: f64,
noise_std: f64,
x_train: Arc<[f64]>,
n_train: usize,
n_parents: usize,
alpha: Arc<[f64]>,
},
Dynamic {
id: Arc<str>,
mechanism: Arc<dyn DynamicMechanism>,
},
}
impl std::fmt::Debug for MechanismSlot {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Vacant => write!(f, "Vacant"),
Self::Pending { family_id } => {
f.debug_struct("Pending").field("family_id", family_id).finish()
}
Self::LinearGaussian { intercept, coeffs, sigma } => f
.debug_struct("LinearGaussian")
.field("intercept", intercept)
.field("coeffs", coeffs)
.field("sigma", sigma)
.finish(),
Self::Discrete { support, probs, logit_coeffs } => f
.debug_struct("Discrete")
.field("support", support)
.field("probs", probs)
.field("logit_coeffs", logit_coeffs)
.finish(),
Self::Constant { value } => f.debug_struct("Constant").field("value", value).finish(),
Self::HierarchicalLinear { intercept, coeffs, sigma, shrinkage } => f
.debug_struct("HierarchicalLinear")
.field("intercept", intercept)
.field("coeffs", coeffs)
.field("sigma", sigma)
.field("shrinkage", shrinkage)
.finish(),
Self::Bvar { intercept, coeffs, sigma } => f
.debug_struct("Bvar")
.field("intercept", intercept)
.field("coeffs", coeffs)
.field("sigma", sigma)
.finish(),
Self::LinearGaussianStateSpace { a, process_std, obs_std, initial_mean } => f
.debug_struct("LinearGaussianStateSpace")
.field("a", a)
.field("process_std", process_std)
.field("obs_std", obs_std)
.field("initial_mean", initial_mean)
.finish(),
Self::GaussianProcess {
length_scale, variance, noise_std, n_train, n_parents, ..
} => f
.debug_struct("GaussianProcess")
.field("length_scale", length_scale)
.field("variance", variance)
.field("noise_std", noise_std)
.field("n_train", n_train)
.field("n_parents", n_parents)
.finish(),
Self::Dynamic { id, .. } => f
.debug_struct("Dynamic")
.field("id", id)
.field("mechanism", &"<dyn DynamicMechanism>")
.finish(),
}
}
}
#[derive(Clone, Debug)]
pub struct CompiledMechanismStore {
pub slots: Arc<[MechanismSlot]>,
}
impl CompiledMechanismStore {
#[must_use]
pub fn vacant(n: usize) -> Self {
Self { slots: Arc::from(vec![MechanismSlot::Vacant; n]) }
}
#[must_use]
pub fn get(&self, id: DenseNodeId) -> &MechanismSlot {
&self.slots[id.as_usize()]
}
pub fn with_replaced(&self, id: DenseNodeId, slot: MechanismSlot) -> Result<Self, ModelError> {
let idx = id.as_usize();
if idx >= self.slots.len() {
return Err(ModelError::Shape { message: "mechanism slot index out of range".into() });
}
let mut slots = self.slots.as_ref().to_vec();
slots[idx] = slot;
Ok(Self { slots: Arc::from(slots) })
}
}
#[derive(Clone, Debug)]
pub struct CompiledCausalModel {
pub node_order: Arc<[DenseNodeId]>,
pub parent_gathers: Arc<[ParentGatherPlan]>,
pub mechanisms: CompiledMechanismStore,
pub output_layout: ModelOutputLayout,
pub graph: Arc<Dag>,
}
impl CompiledCausalModel {
pub fn compile(graph: Dag) -> Result<Self, ModelError> {
let order = graph.topological_order().ok_or_else(|| ModelError::NotDag {
message: "graph has no topological order".into(),
})?;
let n = graph.node_count();
let mut variables = Vec::with_capacity(n);
for i in 0..n {
let id = DenseNodeId::from_raw(i as u32);
match graph.nodes().get(i) {
Some(NodeRef::Static(v)) => variables.push(*v),
Some(other) => {
return Err(ModelError::Unsupported {
message: format!(
"CompiledCausalModel requires Static nodes, got {other:?}"
),
});
}
None => {
return Err(ModelError::Shape { message: "node missing".into() });
}
}
let _ = id;
}
let mut gathers = Vec::with_capacity(order.len());
for &child in &order {
let parents = graph.parents(child).to_vec();
gathers.push(ParentGatherPlan { child, parents: Arc::from(parents) });
}
let node_order = Arc::from(order);
Ok(Self {
output_layout: ModelOutputLayout {
node_order: Arc::clone(&node_order),
variables: Arc::from(variables),
},
node_order,
parent_gathers: Arc::from(gathers),
mechanisms: CompiledMechanismStore::vacant(n),
graph: Arc::new(graph),
})
}
#[must_use]
pub fn n_nodes(&self) -> usize {
self.graph.node_count()
}
#[must_use]
pub fn dense_of(&self, var: VariableId) -> Option<DenseNodeId> {
self.output_layout
.variables
.iter()
.position(|v| *v == var)
.map(|i| DenseNodeId::from_raw(i as u32))
}
#[must_use]
pub fn with_mechanisms(mut self, mechanisms: CompiledMechanismStore) -> Self {
self.mechanisms = mechanisms;
self
}
#[must_use]
pub fn gather_for(&self, child: DenseNodeId) -> Option<&ParentGatherPlan> {
self.parent_gathers.iter().find(|g| g.child == child)
}
}
#[derive(Clone, Debug)]
pub struct ProbabilisticCausalModel {
pub compiled: CompiledCausalModel,
}
impl ProbabilisticCausalModel {
#[must_use]
pub fn new(compiled: CompiledCausalModel) -> Self {
Self { compiled }
}
}
#[derive(Clone, Debug)]
pub struct StructuralCausalModel {
pub compiled: CompiledCausalModel,
}
impl StructuralCausalModel {
#[must_use]
pub fn new(compiled: CompiledCausalModel) -> Self {
Self { compiled }
}
}
#[derive(Clone, Debug)]
pub struct InvertibleStructuralCausalModel {
pub compiled: CompiledCausalModel,
}
impl InvertibleStructuralCausalModel {
#[must_use]
pub fn new(compiled: CompiledCausalModel) -> Self {
Self { compiled }
}
}
#[cfg(test)]
mod tests {
use super::*;
use antecedent_core::VariableId;
use antecedent_graph::Dag;
#[test]
fn compile_chain_topo_order() {
let mut g = Dag::with_variables(3);
let a = DenseNodeId::from_raw(0);
let b = DenseNodeId::from_raw(1);
let c = DenseNodeId::from_raw(2);
g.insert_directed(a, b).unwrap();
g.insert_directed(b, c).unwrap();
let plan = CompiledCausalModel::compile(g).unwrap();
assert_eq!(plan.n_nodes(), 3);
assert_eq!(plan.node_order.as_ref(), &[a, b, c]);
assert_eq!(plan.gather_for(c).unwrap().n_parents(), 1);
assert_eq!(plan.dense_of(VariableId::from_raw(1)), Some(b));
}
}