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}
348
349impl CompiledCausalModel {
350 pub fn compile(graph: Dag) -> Result<Self, ModelError> {
358 let order = graph.topological_order().ok_or_else(|| ModelError::NotDag {
359 message: "graph has no topological order".into(),
360 })?;
361 let n = graph.node_count();
362 let mut variables = Vec::with_capacity(n);
363 for i in 0..n {
364 let id = DenseNodeId::from_raw(i as u32);
365 match graph.nodes().get(i) {
366 Some(NodeRef::Static(v)) => variables.push(*v),
367 Some(other) => {
368 return Err(ModelError::Unsupported {
369 message: format!(
370 "CompiledCausalModel requires Static nodes, got {other:?}"
371 ),
372 });
373 }
374 None => {
375 return Err(ModelError::Shape { message: "node missing".into() });
376 }
377 }
378 let _ = id;
379 }
380 let mut gathers = Vec::with_capacity(order.len());
381 for &child in &order {
382 let parents = graph.parents(child).to_vec();
383 gathers.push(ParentGatherPlan { child, parents: Arc::from(parents) });
384 }
385 let node_order = Arc::from(order);
386 Ok(Self {
387 output_layout: ModelOutputLayout {
388 node_order: Arc::clone(&node_order),
389 variables: Arc::from(variables),
390 },
391 node_order,
392 parent_gathers: Arc::from(gathers),
393 mechanisms: CompiledMechanismStore::vacant(n),
394 graph: Arc::new(graph),
395 })
396 }
397
398 #[must_use]
400 pub fn n_nodes(&self) -> usize {
401 self.graph.node_count()
402 }
403
404 #[must_use]
406 pub fn dense_of(&self, var: VariableId) -> Option<DenseNodeId> {
407 self.output_layout
408 .variables
409 .iter()
410 .position(|v| *v == var)
411 .map(|i| DenseNodeId::from_raw(i as u32))
412 }
413
414 #[must_use]
416 pub fn with_mechanisms(mut self, mechanisms: CompiledMechanismStore) -> Self {
417 self.mechanisms = mechanisms;
418 self
419 }
420
421 #[must_use]
423 pub fn gather_for(&self, child: DenseNodeId) -> Option<&ParentGatherPlan> {
424 self.parent_gathers.iter().find(|g| g.child == child)
425 }
426}
427
428#[derive(Clone, Debug)]
430pub struct ProbabilisticCausalModel {
431 pub compiled: CompiledCausalModel,
433}
434
435impl ProbabilisticCausalModel {
436 #[must_use]
438 pub fn new(compiled: CompiledCausalModel) -> Self {
439 Self { compiled }
440 }
441}
442
443#[derive(Clone, Debug)]
445pub struct StructuralCausalModel {
446 pub compiled: CompiledCausalModel,
448}
449
450impl StructuralCausalModel {
451 #[must_use]
453 pub fn new(compiled: CompiledCausalModel) -> Self {
454 Self { compiled }
455 }
456}
457
458#[derive(Clone, Debug)]
460pub struct InvertibleStructuralCausalModel {
461 pub compiled: CompiledCausalModel,
463}
464
465impl InvertibleStructuralCausalModel {
466 #[must_use]
468 pub fn new(compiled: CompiledCausalModel) -> Self {
469 Self { compiled }
470 }
471}
472
473#[cfg(test)]
474mod tests {
475 use super::*;
476 use antecedent_core::VariableId;
477 use antecedent_graph::Dag;
478
479 #[test]
480 fn compile_chain_topo_order() {
481 let mut g = Dag::with_variables(3);
482 let a = DenseNodeId::from_raw(0);
483 let b = DenseNodeId::from_raw(1);
484 let c = DenseNodeId::from_raw(2);
485 g.insert_directed(a, b).unwrap();
486 g.insert_directed(b, c).unwrap();
487 let plan = CompiledCausalModel::compile(g).unwrap();
488 assert_eq!(plan.n_nodes(), 3);
489 assert_eq!(plan.node_order.as_ref(), &[a, b, c]);
490 assert_eq!(plan.gather_for(c).unwrap().n_parents(), 1);
491 assert_eq!(plan.dense_of(VariableId::from_raw(1)), Some(b));
492 }
493}