antecedent_core/
temporal.rs1use crate::ids::{Lag, VariableId};
8
9#[derive(Clone, Debug, Eq, PartialEq, thiserror::Error)]
11#[non_exhaustive]
12pub enum TemporalIndexError {
13 #[error("{message}")]
15 Invalid {
16 message: &'static str,
18 },
19 #[error("unknown variable id {}", id.raw())]
21 UnknownVariable {
22 id: VariableId,
24 },
25}
26
27#[derive(Clone, Copy, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)]
29pub struct TemporalNodeKey {
30 pub variable: VariableId,
32 pub offset: i32,
34}
35
36impl TemporalNodeKey {
37 #[must_use]
39 pub const fn contemporaneous(variable: VariableId) -> Self {
40 Self { variable, offset: 0 }
41 }
42
43 #[must_use]
45 pub fn lagged(variable: VariableId, lag: Lag) -> Option<Self> {
46 let lag_i = i32::try_from(lag.raw()).ok()?;
47 Some(Self { variable, offset: -lag_i })
48 }
49}
50
51#[derive(Clone, Debug, Eq, PartialEq)]
56pub struct TemporalIndexer {
57 variable_count: u32,
58 history: u32,
60 horizon: u32,
62 dense_len: usize,
64}
65
66impl TemporalIndexer {
67 pub fn new(
73 variable_count: u32,
74 history: u32,
75 horizon: u32,
76 ) -> Result<Self, TemporalIndexError> {
77 if variable_count == 0 || horizon == 0 {
78 return Err(TemporalIndexError::Invalid {
79 message: "variable_count and horizon must be non-zero",
80 });
81 }
82 let slices = history
83 .checked_add(horizon)
84 .ok_or(TemporalIndexError::Invalid { message: "history+horizon overflow" })?;
85 let dense_u32 = variable_count
86 .checked_mul(slices)
87 .ok_or(TemporalIndexError::Invalid { message: "dense index space overflow" })?;
88 let dense_len = usize::try_from(dense_u32)
89 .map_err(|_| TemporalIndexError::Invalid { message: "dense index space overflow" })?;
90 Ok(Self { variable_count, history, horizon, dense_len })
91 }
92
93 #[must_use]
95 pub const fn variable_count(&self) -> u32 {
96 self.variable_count
97 }
98
99 #[must_use]
101 pub const fn history(&self) -> u32 {
102 self.history
103 }
104
105 #[must_use]
107 pub const fn horizon(&self) -> u32 {
108 self.horizon
109 }
110
111 #[must_use]
113 pub const fn dense_len(&self) -> usize {
114 self.dense_len
115 }
116
117 pub fn dense_id(&self, key: TemporalNodeKey) -> Result<u32, TemporalIndexError> {
123 let v = key.variable.raw();
124 if v >= self.variable_count {
125 return Err(TemporalIndexError::UnknownVariable { id: key.variable });
126 }
127 let slice = i64::from(key.offset) + i64::from(self.history);
128 if slice < 0 || slice >= i64::from(self.history) + i64::from(self.horizon) {
129 return Err(TemporalIndexError::Invalid {
130 message: "temporal offset outside unfolding window",
131 });
132 }
133 let slice_u = u64::try_from(slice).map_err(|_| TemporalIndexError::Invalid {
134 message: "temporal offset outside unfolding window",
135 })?;
136 let dense = slice_u * u64::from(self.variable_count) + u64::from(v);
137 u32::try_from(dense)
138 .map_err(|_| TemporalIndexError::Invalid { message: "dense id exceeds u32" })
139 }
140
141 pub fn key_of(&self, dense: u32) -> Result<TemporalNodeKey, TemporalIndexError> {
147 let dense_usize = usize::try_from(dense)
148 .map_err(|_| TemporalIndexError::Invalid { message: "dense id out of range" })?;
149 if dense_usize >= self.dense_len() {
150 return Err(TemporalIndexError::Invalid { message: "dense id out of range" });
151 }
152 let vc = self.variable_count;
153 let slice = dense / vc;
154 let var = dense % vc;
155 let offset = i32::try_from(i64::from(slice) - i64::from(self.history))
156 .map_err(|_| TemporalIndexError::Invalid { message: "offset overflow" })?;
157 Ok(TemporalNodeKey { variable: VariableId::from_raw(var), offset })
158 }
159}
160
161#[cfg(test)]
162mod tests {
163 use super::*;
164
165 #[test]
166 fn time_major_dense_round_trip() {
167 let idx = TemporalIndexer::new(3, 2, 4).unwrap();
168 assert_eq!(idx.dense_len(), 18);
169 let key = TemporalNodeKey { variable: VariableId::from_raw(1), offset: -1 };
170 let dense = idx.dense_id(key).unwrap();
171 assert_eq!(dense, 4);
172 assert_eq!(idx.key_of(dense).unwrap(), key);
173 }
174}