Skip to main content

antecedent_core/
temporal.rs

1//! Temporal node identity and dense unfolding indexes (ADR 0005 / ).
2//!
3//! Dense indexes are process-local and **must not** be serialized.
4//!
5//! SPDX-License-Identifier: MIT OR Apache-2.0
6
7use core::fmt;
8
9use crate::ids::{Lag, VariableId};
10
11/// Errors from temporal indexing.
12#[derive(Clone, Debug, Eq, PartialEq)]
13pub enum TemporalIndexError {
14    /// Invalid constructor arguments.
15    Invalid {
16        /// Message.
17        message: &'static str,
18    },
19    /// Variable id outside the indexer.
20    UnknownVariable {
21        /// Variable.
22        id: VariableId,
23    },
24}
25
26impl fmt::Display for TemporalIndexError {
27    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
28        match self {
29            Self::Invalid { message } => write!(f, "{message}"),
30            Self::UnknownVariable { id } => write!(f, "unknown variable id {}", id.raw()),
31        }
32    }
33}
34
35impl std::error::Error for TemporalIndexError {}
36
37/// Stable unfolded temporal node identity (serializable).
38#[derive(Clone, Copy, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)]
39pub struct TemporalNodeKey {
40    /// Variable.
41    pub variable: VariableId,
42    /// Offset relative to the analysis origin (may be negative for history).
43    pub offset: i32,
44}
45
46impl TemporalNodeKey {
47    /// Contemporaneous node at the origin.
48    #[must_use]
49    pub const fn contemporaneous(variable: VariableId) -> Self {
50        Self { variable, offset: 0 }
51    }
52
53    /// Node at `t - lag` when lag is non-negative and fits in `i32`.
54    #[must_use]
55    pub fn lagged(variable: VariableId, lag: Lag) -> Option<Self> {
56        let lag_i = i32::try_from(lag.raw()).ok()?;
57        Some(Self { variable, offset: -lag_i })
58    }
59}
60
61/// Finite unfolding indexer: time-major dense layout.
62///
63/// `dense_index = time_slice_index * variable_count + variable_index`
64/// where `time_slice_index = offset + history`.
65#[derive(Clone, Debug, Eq, PartialEq)]
66pub struct TemporalIndexer {
67    variable_count: u32,
68    /// Number of historical slices before offset 0.
69    history: u32,
70    /// Number of forward slices including offset 0.
71    horizon: u32,
72    /// Cached `variable_count * (history + horizon)`.
73    dense_len: usize,
74}
75
76impl TemporalIndexer {
77    /// Create an indexer.
78    ///
79    /// # Errors
80    ///
81    /// When counts are zero or products overflow.
82    pub fn new(
83        variable_count: u32,
84        history: u32,
85        horizon: u32,
86    ) -> Result<Self, TemporalIndexError> {
87        if variable_count == 0 || horizon == 0 {
88            return Err(TemporalIndexError::Invalid {
89                message: "variable_count and horizon must be non-zero",
90            });
91        }
92        let slices = history
93            .checked_add(horizon)
94            .ok_or(TemporalIndexError::Invalid { message: "history+horizon overflow" })?;
95        let dense_u32 = variable_count
96            .checked_mul(slices)
97            .ok_or(TemporalIndexError::Invalid { message: "dense index space overflow" })?;
98        let dense_len = usize::try_from(dense_u32)
99            .map_err(|_| TemporalIndexError::Invalid { message: "dense index space overflow" })?;
100        Ok(Self { variable_count, history, horizon, dense_len })
101    }
102
103    /// Variable count.
104    #[must_use]
105    pub const fn variable_count(&self) -> u32 {
106        self.variable_count
107    }
108
109    /// History depth.
110    #[must_use]
111    pub const fn history(&self) -> u32 {
112        self.history
113    }
114
115    /// Horizon (including t=0).
116    #[must_use]
117    pub const fn horizon(&self) -> u32 {
118        self.horizon
119    }
120
121    /// Total dense nodes.
122    #[must_use]
123    pub const fn dense_len(&self) -> usize {
124        self.dense_len
125    }
126
127    /// Convert a stable key to a dense index.
128    ///
129    /// # Errors
130    ///
131    /// Out of range variable or offset.
132    pub fn dense_id(&self, key: TemporalNodeKey) -> Result<u32, TemporalIndexError> {
133        let v = key.variable.raw();
134        if v >= self.variable_count {
135            return Err(TemporalIndexError::UnknownVariable { id: key.variable });
136        }
137        let slice = i64::from(key.offset) + i64::from(self.history);
138        if slice < 0 || slice >= i64::from(self.history) + i64::from(self.horizon) {
139            return Err(TemporalIndexError::Invalid {
140                message: "temporal offset outside unfolding window",
141            });
142        }
143        let slice_u = u64::try_from(slice).map_err(|_| TemporalIndexError::Invalid {
144            message: "temporal offset outside unfolding window",
145        })?;
146        let dense = slice_u * u64::from(self.variable_count) + u64::from(v);
147        u32::try_from(dense)
148            .map_err(|_| TemporalIndexError::Invalid { message: "dense id exceeds u32" })
149    }
150
151    /// Invert a dense index to a stable key.
152    ///
153    /// # Errors
154    ///
155    /// Out of range dense id.
156    pub fn key_of(&self, dense: u32) -> Result<TemporalNodeKey, TemporalIndexError> {
157        let dense_usize = usize::try_from(dense)
158            .map_err(|_| TemporalIndexError::Invalid { message: "dense id out of range" })?;
159        if dense_usize >= self.dense_len() {
160            return Err(TemporalIndexError::Invalid { message: "dense id out of range" });
161        }
162        let vc = self.variable_count;
163        let slice = dense / vc;
164        let var = dense % vc;
165        let offset = i32::try_from(i64::from(slice) - i64::from(self.history))
166            .map_err(|_| TemporalIndexError::Invalid { message: "offset overflow" })?;
167        Ok(TemporalNodeKey { variable: VariableId::from_raw(var), offset })
168    }
169}
170
171#[cfg(test)]
172mod tests {
173    use super::*;
174
175    #[test]
176    fn time_major_dense_round_trip() {
177        let idx = TemporalIndexer::new(3, 2, 4).unwrap();
178        assert_eq!(idx.dense_len(), 18);
179        let key = TemporalNodeKey { variable: VariableId::from_raw(1), offset: -1 };
180        let dense = idx.dense_id(key).unwrap();
181        assert_eq!(dense, 4);
182        assert_eq!(idx.key_of(dense).unwrap(), key);
183    }
184}