1#![allow(clippy::cast_possible_truncation)]
10
11use std::sync::Arc;
12
13use antecedent_core::{KernelPolicy, Lag, VariableId};
14use antecedent_kernels::{F64VectorView, gather};
15
16use crate::column::{ColumnView, Float64Column};
17use crate::dataset::TimeSeriesData;
18use crate::error::DataError;
19use crate::reference::ReferencePointPolicy;
20use crate::table::TableView;
21
22#[derive(Clone, Debug, Eq, PartialEq)]
28pub struct LagMap {
29 series_len: usize,
30 max_lag: u32,
31 n_effective: usize,
32 base_t: usize,
33 reference: ReferencePointPolicy,
34}
35
36impl LagMap {
37 pub fn new(series_len: usize, max_lag: u32) -> Result<Self, DataError> {
43 Self::with_reference(series_len, max_lag, ReferencePointPolicy::SeriesOrigin)
44 }
45
46 pub fn with_reference(
52 series_len: usize,
53 max_lag: u32,
54 reference: ReferencePointPolicy,
55 ) -> Result<Self, DataError> {
56 let (base_t, n_effective) = reference.base_and_n(series_len, max_lag)?;
57 Ok(Self { series_len, max_lag, n_effective, base_t, reference })
58 }
59
60 #[must_use]
62 pub const fn series_len(&self) -> usize {
63 self.series_len
64 }
65
66 #[must_use]
68 pub const fn max_lag(&self) -> u32 {
69 self.max_lag
70 }
71
72 #[must_use]
74 pub const fn n_effective(&self) -> usize {
75 self.n_effective
76 }
77
78 #[must_use]
80 pub const fn reference(&self) -> ReferencePointPolicy {
81 self.reference
82 }
83
84 #[must_use]
90 pub fn row_index(&self, lag: Lag, sample_i: usize) -> usize {
91 debug_assert!(sample_i < self.n_effective);
92 debug_assert!(lag.raw() <= self.max_lag);
93 self.base_t + sample_i - lag.raw() as usize
94 }
95
96 pub fn fill_row_indexes(&self, lag: Lag, out: &mut [usize]) -> Result<(), DataError> {
102 if out.len() != self.n_effective {
103 return Err(DataError::LengthMismatch {
104 expected: self.n_effective,
105 actual: out.len(),
106 context: "lag-map row index buffer",
107 });
108 }
109 if lag.raw() > self.max_lag {
110 return Err(DataError::InvalidArgument { message: "lag exceeds max_lag".into() });
111 }
112 for (i, slot) in out.iter_mut().enumerate() {
113 *slot = self.row_index(lag, i);
114 }
115 Ok(())
116 }
117}
118
119pub(crate) fn ensure_unmasked(data: &TimeSeriesData) -> Result<(), DataError> {
122 if let Some(mask) = data.storage().analysis_mask() {
123 if !mask.is_all_valid() {
124 return Err(DataError::IncompleteSeries {
125 id: None,
126 message: "analysis mask hides rows; temporal discovery requires complete series",
127 });
128 }
129 }
130 Ok(())
131}
132
133pub(crate) fn ensure_complete_float(src: &Float64Column) -> Result<(), DataError> {
136 if !src.validity.is_all_valid() {
137 return Err(DataError::IncompleteSeries {
138 id: Some(src.id),
139 message: "missing values in series; temporal discovery requires complete series",
140 });
141 }
142 Ok(())
143}
144
145#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)]
147pub struct LaggedColumn {
148 pub variable: VariableId,
150 pub lag: Lag,
152}
153
154#[derive(Clone, Debug)]
156pub struct LaggedSamplePlan {
157 columns: Arc<[LaggedColumn]>,
158 lag_map: Arc<LagMap>,
159}
160
161impl LaggedSamplePlan {
162 pub fn new(
168 series_len: usize,
169 max_lag: u32,
170 columns: impl Into<Arc<[LaggedColumn]>>,
171 ) -> Result<Self, DataError> {
172 Self::with_reference(series_len, max_lag, ReferencePointPolicy::SeriesOrigin, columns)
173 }
174
175 pub fn with_reference(
181 series_len: usize,
182 max_lag: u32,
183 reference: ReferencePointPolicy,
184 columns: impl Into<Arc<[LaggedColumn]>>,
185 ) -> Result<Self, DataError> {
186 let columns = columns.into();
187 if columns.is_empty() {
188 return Err(DataError::InvalidArgument {
189 message: "sample plan needs ≥1 column".into(),
190 });
191 }
192 let lag_map = Arc::new(LagMap::with_reference(series_len, max_lag, reference)?);
193 Self::validate_columns(lag_map.max_lag(), &columns)?;
194 Ok(Self { columns, lag_map })
195 }
196
197 pub fn with_shared(
203 lag_map: Arc<LagMap>,
204 columns: Arc<[LaggedColumn]>,
205 ) -> Result<Self, DataError> {
206 if columns.is_empty() {
207 return Err(DataError::InvalidArgument {
208 message: "sample plan needs ≥1 column".into(),
209 });
210 }
211 Self::validate_columns(lag_map.max_lag(), &columns)?;
212 Ok(Self { columns, lag_map })
213 }
214
215 fn validate_columns(max_lag: u32, columns: &[LaggedColumn]) -> Result<(), DataError> {
216 for c in columns {
217 if c.lag.raw() > max_lag {
218 return Err(DataError::InvalidArgument {
219 message: "planned column lag exceeds max_lag".into(),
220 });
221 }
222 }
223 Ok(())
224 }
225
226 #[must_use]
228 pub fn columns(&self) -> &[LaggedColumn] {
229 &self.columns
230 }
231
232 #[must_use]
234 pub fn columns_arc(&self) -> &Arc<[LaggedColumn]> {
235 &self.columns
236 }
237
238 #[must_use]
240 pub fn lag_map(&self) -> &LagMap {
241 &self.lag_map
242 }
243
244 #[must_use]
246 pub fn lag_map_arc(&self) -> &Arc<LagMap> {
247 &self.lag_map
248 }
249
250 #[must_use]
252 pub fn n_effective(&self) -> usize {
253 self.lag_map.n_effective
254 }
255
256 pub fn prepare<'a>(
263 &'a self,
264 data: &TimeSeriesData,
265 workspace: &'a mut LaggedSampleWorkspace,
266 policy: &KernelPolicy,
267 ) -> Result<LaggedPreparedSample<'a>, DataError> {
268 if data.row_count() != self.lag_map.series_len {
269 return Err(DataError::LengthMismatch {
270 expected: self.lag_map.series_len,
271 actual: data.row_count(),
272 context: "time series length vs sample plan",
273 });
274 }
275 ensure_unmasked(data)?;
276 let n = self.lag_map.n_effective;
277 let ncols = self.columns.len();
278 workspace.prepare(n, ncols);
279
280 for (c, col) in self.columns.iter().enumerate() {
281 let ColumnView::Float64(src) = data.column(col.variable)? else {
282 return Err(DataError::TypeMismatch { id: col.variable, expected: "float64" });
283 };
284 ensure_complete_float(src)?;
285 self.lag_map.fill_row_indexes(col.lag, &mut workspace.row_indexes[..n])?;
286 let dst = &mut workspace.values[c * n..(c + 1) * n];
287 gather(
288 policy,
289 F64VectorView::contiguous(src.values.as_slice()),
290 &workspace.row_indexes[..n],
291 dst,
292 );
293 }
294
295 Ok(LaggedPreparedSample {
296 n,
297 ncols,
298 values: &workspace.values[..n * ncols],
299 columns: &self.columns,
300 dropped: DropSummary { requested: self.lag_map.series_len, retained: n },
301 })
302 }
303}
304
305#[derive(Clone, Debug, Default)]
307pub struct LaggedSampleWorkspace {
308 pub row_indexes: Vec<usize>,
310 pub values: Vec<f64>,
312 capacity_n: usize,
313 capacity_cols: usize,
314}
315
316impl LaggedSampleWorkspace {
317 pub fn prepare(&mut self, n: usize, ncols: usize) {
319 if self.row_indexes.len() < n {
320 self.row_indexes.resize(n, 0);
321 }
322 let need = n.saturating_mul(ncols);
323 if self.values.len() < need {
324 self.values.resize(need, 0.0);
325 }
326 self.capacity_n = self.capacity_n.max(n);
327 self.capacity_cols = self.capacity_cols.max(ncols);
328 }
329
330 #[must_use]
332 pub const fn capacity_n(&self) -> usize {
333 self.capacity_n
334 }
335
336 #[must_use]
338 pub const fn capacity_cols(&self) -> usize {
339 self.capacity_cols
340 }
341}
342
343#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
345pub struct DropSummary {
346 pub requested: usize,
348 pub retained: usize,
350}
351
352#[derive(Clone, Copy, Debug)]
354pub struct LaggedPreparedSample<'a> {
355 pub n: usize,
357 pub ncols: usize,
359 pub values: &'a [f64],
361 pub columns: &'a [LaggedColumn],
363 pub dropped: DropSummary,
365}
366
367impl LaggedPreparedSample<'_> {
368 #[must_use]
370 pub fn column(&self, c: usize) -> &[f64] {
371 &self.values[c * self.n..(c + 1) * self.n]
372 }
373}
374
375impl TimeSeriesData {
376 pub fn plan_lagged_sample(
382 &self,
383 max_lag: u32,
384 columns: impl Into<Arc<[LaggedColumn]>>,
385 ) -> Result<LaggedSamplePlan, DataError> {
386 LaggedSamplePlan::new(self.row_count(), max_lag, columns)
387 }
388}
389
390#[cfg(test)]
391#[allow(clippy::cast_precision_loss, clippy::many_single_char_names)]
392mod tests {
393 use std::sync::Arc;
394
395 use antecedent_core::{Lag, VariableId};
396
397 use super::*;
398 use crate::testing::{float_series, float_series_with_gap, float_series_with_mask};
399
400 #[test]
401 fn prepare_rejects_missing_values() {
402 let data = float_series_with_gap(20, 1, 3);
403 let cols = Arc::from([LaggedColumn {
404 variable: VariableId::from_raw(0),
405 lag: Lag::CONTEMPORANEOUS,
406 }]);
407 let plan = data.plan_lagged_sample(2, cols).unwrap();
408 let mut ws = LaggedSampleWorkspace::default();
409 let err = plan.prepare(&data, &mut ws, &KernelPolicy::default_policy()).unwrap_err();
410 assert!(matches!(
411 err,
412 DataError::IncompleteSeries { id: Some(v), .. } if v == VariableId::from_raw(0)
413 ));
414 }
415
416 #[test]
417 fn prepare_rejects_row_hiding_analysis_mask() {
418 let data = float_series_with_mask(20, 1, 3);
419 let cols = Arc::from([LaggedColumn {
420 variable: VariableId::from_raw(0),
421 lag: Lag::CONTEMPORANEOUS,
422 }]);
423 let plan = data.plan_lagged_sample(2, cols).unwrap();
424 let mut ws = LaggedSampleWorkspace::default();
425 let err = plan.prepare(&data, &mut ws, &KernelPolicy::default_policy()).unwrap_err();
426 assert!(matches!(err, DataError::IncompleteSeries { id: None, .. }));
427 }
428
429 #[test]
430 fn lag_map_row_indexes() {
431 let map = LagMap::new(10, 2).unwrap();
432 assert_eq!(map.n_effective(), 8);
433 assert_eq!(map.row_index(Lag::CONTEMPORANEOUS, 0), 2);
434 assert_eq!(map.row_index(Lag::from_raw(2), 0), 0);
435 assert_eq!(map.row_index(Lag::from_raw(1), 3), 2 + 3 - 1);
436 }
437
438 #[test]
439 fn prepare_gathers_lagged_values() {
440 let data = float_series(20, 2);
441 let cols = Arc::from([
442 LaggedColumn { variable: VariableId::from_raw(0), lag: Lag::CONTEMPORANEOUS },
443 LaggedColumn { variable: VariableId::from_raw(0), lag: Lag::from_raw(2) },
444 LaggedColumn { variable: VariableId::from_raw(1), lag: Lag::from_raw(1) },
445 ]);
446 let plan = data.plan_lagged_sample(2, cols).unwrap();
447 let mut ws = LaggedSampleWorkspace::default();
448 let prep = plan.prepare(&data, &mut ws, &KernelPolicy::default_policy()).unwrap();
449 assert_eq!(prep.n, 18);
450 assert!((prep.column(0)[0] - 2.0).abs() < 1e-12);
451 assert!((prep.column(1)[0] - 0.0).abs() < 1e-12);
452 assert!((prep.column(2)[0] - 101.0).abs() < 1e-12);
453 }
454
455 #[test]
456 fn repeated_prepare_reuses_workspace_capacity() {
457 let data = float_series(100, 3);
458 let cols = Arc::from([
459 LaggedColumn { variable: VariableId::from_raw(0), lag: Lag::CONTEMPORANEOUS },
460 LaggedColumn { variable: VariableId::from_raw(1), lag: Lag::from_raw(3) },
461 LaggedColumn { variable: VariableId::from_raw(2), lag: Lag::from_raw(1) },
462 ]);
463 let plan = data.plan_lagged_sample(3, cols).unwrap();
464 let mut ws = LaggedSampleWorkspace::default();
465 let _ = plan.prepare(&data, &mut ws, &KernelPolicy::default_policy()).unwrap();
466 let cap_n = ws.capacity_n();
467 let cap_c = ws.capacity_cols();
468 let values_cap = ws.values.capacity();
469 let idx_cap = ws.row_indexes.capacity();
470 for _ in 0..50 {
471 let _ = plan.prepare(&data, &mut ws, &KernelPolicy::default_policy()).unwrap();
472 assert_eq!(ws.capacity_n(), cap_n);
473 assert_eq!(ws.capacity_cols(), cap_c);
474 assert_eq!(ws.values.capacity(), values_cap);
475 assert_eq!(ws.row_indexes.capacity(), idx_cap);
476 }
477 }
478}