1#![allow(clippy::cast_possible_truncation)]
6
7use std::sync::Arc;
8
9use antecedent_core::VariableId;
10use antecedent_graph::{Dag, DenseNodeId, NodeRef};
11
12use crate::error::ModelError;
13
14#[derive(Clone, Debug)]
16pub struct ParentGatherPlan {
17 pub child: DenseNodeId,
19 pub parents: Arc<[DenseNodeId]>,
21}
22
23impl ParentGatherPlan {
24 #[must_use]
26 pub fn n_parents(&self) -> usize {
27 self.parents.len()
28 }
29
30 pub fn gather(&self, values: &[f64], n_rows: usize, out: &mut [f64]) {
33 debug_assert!(out.len() >= self.parents.len().saturating_mul(n_rows));
34 for (pi, &p) in self.parents.iter().enumerate() {
35 let src = p.as_usize() * n_rows;
36 let dst = pi * n_rows;
37 out[dst..dst + n_rows].copy_from_slice(&values[src..src + n_rows]);
38 }
39 }
40}
41
42#[derive(Clone, Debug)]
44pub struct ModelOutputLayout {
45 pub node_order: Arc<[DenseNodeId]>,
47 pub variables: Arc<[VariableId]>,
49}
50
51pub trait DynamicMechanism: Send + Sync {
56 fn sample_noise_column(
62 &self,
63 n_rows: usize,
64 rng: &mut antecedent_core::CausalRng,
65 output: &mut [f64],
66 ) -> Result<(), ModelError>;
67
68 fn evaluate_column(
74 &self,
75 parents: crate::batch::ParentBatch<'_>,
76 noise: &[f64],
77 output: &mut [f64],
78 workspace: &mut crate::batch::MechanismWorkspace,
79 ) -> Result<(), ModelError>;
80
81 fn infer_noise_column(
90 &self,
91 value: &[f64],
92 parents: crate::batch::ParentBatch<'_>,
93 output: &mut [f64],
94 ) -> Result<(), ModelError> {
95 let n = parents.n_rows;
96 if value.len() < n || output.len() < n {
97 return Err(ModelError::Shape {
98 message: "dynamic infer_noise buffers too short".into(),
99 });
100 }
101 let zeros = vec![0.0; n];
102 let mut mean = vec![0.0; n];
103 let mut ws = crate::batch::MechanismWorkspace::default();
104 self.evaluate_column(parents, &zeros, &mut mean, &mut ws)?;
105 for i in 0..n {
106 output[i] = value[i] - mean[i];
107 }
108 Ok(())
109 }
110
111 fn log_prob_column(
119 &self,
120 values: &[f64],
121 parents: crate::batch::ParentBatch<'_>,
122 output: &mut [f64],
123 ) -> Result<(), ModelError> {
124 let n = parents.n_rows;
125 if values.len() < n || output.len() < n {
126 return Err(ModelError::Shape { message: "dynamic log_prob buffers too short".into() });
127 }
128 let mut resid = vec![0.0; n];
129 self.infer_noise_column(values, parents, &mut resid)?;
130 let log_norm = -0.5 * (2.0 * std::f64::consts::PI).ln();
131 for i in 0..n {
132 output[i] = log_norm - 0.5 * resid[i] * resid[i];
133 }
134 Ok(())
135 }
136}
137
138#[derive(Clone, Default)]
140pub enum MechanismSlot {
141 #[default]
143 Vacant,
144 Pending {
146 family_id: Arc<str>,
148 },
149 LinearGaussian {
151 intercept: f64,
153 coeffs: Arc<[f64]>,
155 sigma: f64,
157 },
158 Discrete {
166 support: Arc<[f64]>,
168 probs: Arc<[f64]>,
171 logit_coeffs: Option<Arc<[f64]>>,
173 },
174 Constant {
176 value: f64,
178 },
179 HierarchicalLinear {
181 intercept: f64,
183 coeffs: Arc<[f64]>,
185 sigma: f64,
187 shrinkage: f64,
189 },
190 Bvar {
192 intercept: f64,
194 coeffs: Arc<[f64]>,
196 sigma: f64,
198 },
199 LinearGaussianStateSpace {
204 a: f64,
206 process_std: f64,
208 obs_std: f64,
210 initial_mean: f64,
212 },
213 GaussianProcess {
215 length_scale: f64,
217 variance: f64,
219 noise_std: f64,
221 x_train: Arc<[f64]>,
223 n_train: usize,
225 n_parents: usize,
227 alpha: Arc<[f64]>,
229 },
230 Dynamic {
232 id: Arc<str>,
234 mechanism: Arc<dyn DynamicMechanism>,
236 },
237}
238
239impl std::fmt::Debug for MechanismSlot {
240 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
241 match self {
242 Self::Vacant => write!(f, "Vacant"),
243 Self::Pending { family_id } => {
244 f.debug_struct("Pending").field("family_id", family_id).finish()
245 }
246 Self::LinearGaussian { intercept, coeffs, sigma } => f
247 .debug_struct("LinearGaussian")
248 .field("intercept", intercept)
249 .field("coeffs", coeffs)
250 .field("sigma", sigma)
251 .finish(),
252 Self::Discrete { support, probs, logit_coeffs } => f
253 .debug_struct("Discrete")
254 .field("support", support)
255 .field("probs", probs)
256 .field("logit_coeffs", logit_coeffs)
257 .finish(),
258 Self::Constant { value } => f.debug_struct("Constant").field("value", value).finish(),
259 Self::HierarchicalLinear { intercept, coeffs, sigma, shrinkage } => f
260 .debug_struct("HierarchicalLinear")
261 .field("intercept", intercept)
262 .field("coeffs", coeffs)
263 .field("sigma", sigma)
264 .field("shrinkage", shrinkage)
265 .finish(),
266 Self::Bvar { intercept, coeffs, sigma } => f
267 .debug_struct("Bvar")
268 .field("intercept", intercept)
269 .field("coeffs", coeffs)
270 .field("sigma", sigma)
271 .finish(),
272 Self::LinearGaussianStateSpace { a, process_std, obs_std, initial_mean } => f
273 .debug_struct("LinearGaussianStateSpace")
274 .field("a", a)
275 .field("process_std", process_std)
276 .field("obs_std", obs_std)
277 .field("initial_mean", initial_mean)
278 .finish(),
279 Self::GaussianProcess {
280 length_scale, variance, noise_std, n_train, n_parents, ..
281 } => f
282 .debug_struct("GaussianProcess")
283 .field("length_scale", length_scale)
284 .field("variance", variance)
285 .field("noise_std", noise_std)
286 .field("n_train", n_train)
287 .field("n_parents", n_parents)
288 .finish(),
289 Self::Dynamic { id, .. } => f
290 .debug_struct("Dynamic")
291 .field("id", id)
292 .field("mechanism", &"<dyn DynamicMechanism>")
293 .finish(),
294 }
295 }
296}
297
298#[derive(Clone, Debug)]
300pub struct CompiledMechanismStore {
301 pub slots: Arc<[MechanismSlot]>,
303}
304
305impl CompiledMechanismStore {
306 #[must_use]
308 pub fn vacant(n: usize) -> Self {
309 Self { slots: Arc::from(vec![MechanismSlot::Vacant; n]) }
310 }
311
312 #[must_use]
314 pub fn get(&self, id: DenseNodeId) -> &MechanismSlot {
315 &self.slots[id.as_usize()]
316 }
317
318 pub fn with_replaced(&self, id: DenseNodeId, slot: MechanismSlot) -> Result<Self, ModelError> {
324 let idx = id.as_usize();
325 if idx >= self.slots.len() {
326 return Err(ModelError::Shape { message: "mechanism slot index out of range".into() });
327 }
328 let mut slots = self.slots.as_ref().to_vec();
329 slots[idx] = slot;
330 Ok(Self { slots: Arc::from(slots) })
331 }
332}
333
334#[derive(Clone, Debug)]
336pub struct CompiledCausalModel {
337 pub node_order: Arc<[DenseNodeId]>,
339 pub parent_gathers: Arc<[ParentGatherPlan]>,
341 pub mechanisms: CompiledMechanismStore,
343 pub output_layout: ModelOutputLayout,
345 pub graph: Arc<Dag>,
347 var_to_dense: Arc<std::collections::HashMap<VariableId, DenseNodeId>>,
350 gather_slot: Arc<[u32]>,
353}
354
355impl CompiledCausalModel {
356 pub fn compile(graph: Dag) -> Result<Self, ModelError> {
364 let order = graph.topological_order().ok_or_else(|| ModelError::NotDag {
365 message: "graph has no topological order".into(),
366 })?;
367 let n = graph.node_count();
368 let mut variables = Vec::with_capacity(n);
369 for i in 0..n {
370 let id = DenseNodeId::from_raw(i as u32);
371 match graph.nodes().get(i) {
372 Some(NodeRef::Static(v)) => variables.push(*v),
373 Some(other) => {
374 return Err(ModelError::Unsupported {
375 message: format!(
376 "CompiledCausalModel requires Static nodes, got {other:?}"
377 ),
378 });
379 }
380 None => {
381 return Err(ModelError::Shape { message: "node missing".into() });
382 }
383 }
384 let _ = id;
385 }
386 let mut gathers = Vec::with_capacity(order.len());
387 let mut gather_slot = vec![0u32; n];
388 for (gi, &child) in order.iter().enumerate() {
389 let parents = graph.parents(child).to_vec();
390 gather_slot[child.as_usize()] = gi as u32;
391 gathers.push(ParentGatherPlan { child, parents: Arc::from(parents) });
392 }
393 let mut var_to_dense = std::collections::HashMap::with_capacity(n);
394 for (i, &v) in variables.iter().enumerate() {
395 var_to_dense.entry(v).or_insert_with(|| DenseNodeId::from_raw(i as u32));
397 }
398 let node_order = Arc::from(order);
399 Ok(Self {
400 output_layout: ModelOutputLayout {
401 node_order: Arc::clone(&node_order),
402 variables: Arc::from(variables),
403 },
404 node_order,
405 parent_gathers: Arc::from(gathers),
406 mechanisms: CompiledMechanismStore::vacant(n),
407 graph: Arc::new(graph),
408 var_to_dense: Arc::new(var_to_dense),
409 gather_slot: Arc::from(gather_slot),
410 })
411 }
412
413 #[must_use]
415 pub fn n_nodes(&self) -> usize {
416 self.graph.node_count()
417 }
418
419 #[must_use]
421 pub fn dense_of(&self, var: VariableId) -> Option<DenseNodeId> {
422 self.var_to_dense.get(&var).copied()
423 }
424
425 #[must_use]
427 pub fn with_mechanisms(mut self, mechanisms: CompiledMechanismStore) -> Self {
428 self.mechanisms = mechanisms;
429 self
430 }
431
432 #[must_use]
434 pub fn gather_for(&self, child: DenseNodeId) -> Option<&ParentGatherPlan> {
435 self.gather_slot.get(child.as_usize()).map(|&gi| &self.parent_gathers[gi as usize])
436 }
437}
438
439#[derive(Clone, Debug)]
441pub struct ProbabilisticCausalModel {
442 pub compiled: CompiledCausalModel,
444}
445
446impl ProbabilisticCausalModel {
447 #[must_use]
449 pub fn new(compiled: CompiledCausalModel) -> Self {
450 Self { compiled }
451 }
452}
453
454#[derive(Clone, Debug)]
456pub struct StructuralCausalModel {
457 pub compiled: CompiledCausalModel,
459}
460
461impl StructuralCausalModel {
462 #[must_use]
464 pub fn new(compiled: CompiledCausalModel) -> Self {
465 Self { compiled }
466 }
467}
468
469#[derive(Clone, Debug)]
471pub struct InvertibleStructuralCausalModel {
472 pub compiled: CompiledCausalModel,
474}
475
476impl InvertibleStructuralCausalModel {
477 #[must_use]
479 pub fn new(compiled: CompiledCausalModel) -> Self {
480 Self { compiled }
481 }
482}
483
484#[cfg(test)]
485mod tests {
486 use super::*;
487 use antecedent_core::VariableId;
488 use antecedent_graph::Dag;
489
490 #[test]
491 fn compile_chain_topo_order() {
492 let mut g = Dag::with_variables(3);
493 let a = DenseNodeId::from_raw(0);
494 let b = DenseNodeId::from_raw(1);
495 let c = DenseNodeId::from_raw(2);
496 g.insert_directed(a, b).unwrap();
497 g.insert_directed(b, c).unwrap();
498 let plan = CompiledCausalModel::compile(g).unwrap();
499 assert_eq!(plan.n_nodes(), 3);
500 assert_eq!(plan.node_order.as_ref(), &[a, b, c]);
501 assert_eq!(plan.gather_for(c).unwrap().n_parents(), 1);
502 assert_eq!(plan.dense_of(VariableId::from_raw(1)), Some(b));
503 }
504}