Skip to main content

antecedent_data/
sim.rs

1//! Structural process generators for conformance / toys .
2//!
3//! Full SCM mechanism registry is ; this module covers lagged linear
4//! processes with known parents for fixtures and notebooks.
5//!
6//! SPDX-License-Identifier: MIT OR Apache-2.0
7
8#![allow(clippy::cast_precision_loss, clippy::cast_possible_truncation)]
9
10use std::sync::Arc;
11
12use antecedent_core::{
13    CausalSchemaBuilder, Lag, MeasurementSpec, RoleHint, SmallRoleSet, ValueType, VariableId,
14};
15
16use crate::column::{Float64Column, OwnedColumn, ValidityBitmap};
17use crate::dataset::TimeSeriesData;
18use crate::error::DataError;
19use crate::storage::OwnedColumnarStorage;
20use crate::temporal::{SamplingRegularity, TimeIndex};
21
22/// Known lagged parent used as ground truth in fixtures.
23#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)]
24pub struct KnownLaggedParent {
25    /// Source variable index into the generator’s variable list.
26    pub source: VariableId,
27    /// Source lag.
28    pub source_lag: Lag,
29    /// Target variable.
30    pub target: VariableId,
31}
32
33/// Two-variable lagged linear SCM: `Y_t = coef * X_{t-lag} + noise`.
34#[derive(Clone, Debug)]
35pub struct LaggedLinearPair {
36    /// Series length.
37    pub n: usize,
38    /// Coupling coefficient.
39    pub coef: f64,
40    /// Lag of X → Y.
41    pub lag: u32,
42    /// Master seed for deterministic noise streams.
43    pub seed: u64,
44}
45
46impl Default for LaggedLinearPair {
47    fn default() -> Self {
48        Self { n: 500, coef: 0.8, lag: 1, seed: 1 }
49    }
50}
51
52impl LaggedLinearPair {
53    /// Simulate `(X, Y)` and return data plus the true parent edge.
54    ///
55    /// # Errors
56    ///
57    /// Invalid lengths / lag.
58    pub fn simulate(&self) -> Result<(TimeSeriesData, KnownLaggedParent), DataError> {
59        if self.n < self.lag as usize + 2 {
60            return Err(DataError::InvalidArgument {
61                message: "series too short for configured lag".into(),
62            });
63        }
64        let mut b = CausalSchemaBuilder::new();
65        b.add_variable(
66            "x",
67            ValueType::Continuous,
68            SmallRoleSet::from_hint(RoleHint::Context),
69            None,
70            None,
71            MeasurementSpec::default(),
72        )
73        .map_err(|e| DataError::Schema(e.to_string()))?;
74        b.add_variable(
75            "y",
76            ValueType::Continuous,
77            SmallRoleSet::from_hint(RoleHint::Context),
78            None,
79            None,
80            MeasurementSpec::default(),
81        )
82        .map_err(|e| DataError::Schema(e.to_string()))?;
83        let schema = b.build().map_err(|e| DataError::Schema(e.to_string()))?;
84
85        let mut x = vec![0.0; self.n];
86        let mut y = vec![0.0; self.n];
87        for t in 0..self.n {
88            x[t] = det_noise(self.seed, t as u64, 1);
89            let x_lag = if t >= self.lag as usize { x[t - self.lag as usize] } else { 0.0 };
90            y[t] = self.coef * x_lag + 0.2 * det_noise(self.seed, t as u64, 2);
91        }
92
93        let cols = vec![
94            OwnedColumn::Float64(Float64Column::new(
95                VariableId::from_raw(0),
96                Arc::from(x),
97                ValidityBitmap::all_valid(self.n),
98            )?),
99            OwnedColumn::Float64(Float64Column::new(
100                VariableId::from_raw(1),
101                Arc::from(y),
102                ValidityBitmap::all_valid(self.n),
103            )?),
104        ];
105        let storage = OwnedColumnarStorage::try_new(schema, cols, None, None)?;
106        let data = TimeSeriesData::try_new(
107            storage,
108            TimeIndex {
109                regularity: SamplingRegularity::Regular { interval_ns: 1 },
110                length: self.n,
111            },
112        )?;
113        let parent = KnownLaggedParent {
114            source: VariableId::from_raw(0),
115            source_lag: Lag::from_raw(self.lag),
116            target: VariableId::from_raw(1),
117        };
118        Ok((data, parent))
119    }
120}
121
122fn det_noise(seed: u64, t: u64, stream: u64) -> f64 {
123    // SplitMix-style deterministic noise in (-1, 1).
124    let mut z = seed
125        .wrapping_add(t.wrapping_mul(0x9E37_79B9_7F4A_7C15))
126        .wrapping_add(stream.wrapping_mul(0xD6E8_FEB8_6659_FD93));
127    z = (z ^ (z >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9);
128    z = (z ^ (z >> 27)).wrapping_mul(0x94D0_49BB_1331_11EB);
129    z ^= z >> 31;
130    let u = (z >> 11) as f64 / ((1u64 << 53) as f64);
131    u * 2.0 - 1.0
132}
133
134#[cfg(test)]
135mod tests {
136    use super::*;
137    use crate::table::TableView;
138
139    #[test]
140    fn lag1_pair_recovers_length_and_parent() {
141        let (data, parent) = LaggedLinearPair::default().simulate().unwrap();
142        assert_eq!(data.row_count(), 500);
143        assert_eq!(parent.source_lag.raw(), 1);
144        assert_eq!(parent.source.raw(), 0);
145        assert_eq!(parent.target.raw(), 1);
146    }
147}