1#![allow(clippy::cast_possible_truncation)]
11
12use std::sync::Arc;
13
14use antecedent_core::{KernelPolicy, Lag, VariableId};
15use antecedent_kernels::{F64VectorView, gather};
16
17use crate::column::{ColumnView, ValidityBitmap};
18use crate::dataset::TimeSeriesData;
19use crate::error::DataError;
20use crate::reference::ReferencePointPolicy;
21use crate::sample::LagMap;
22use crate::sample_policy::{MaskPolicy, MissingPolicy};
23use crate::table::TableView;
24
25#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)]
27pub struct LaggedFrameOptions {
28 pub mask: MaskPolicy,
30 pub missing: MissingPolicy,
32}
33
34impl Default for LaggedFrameOptions {
35 fn default() -> Self {
36 Self { mask: MaskPolicy::Honor, missing: MissingPolicy::CompleteCase }
37 }
38}
39
40#[derive(Clone, Debug)]
46pub struct LaggedFrame {
47 variables: Arc<[VariableId]>,
48 slot_index: std::collections::HashMap<VariableId, usize>,
52 max_lag: u32,
53 n_effective: usize,
54 n_lags: usize,
55 values: Vec<f64>,
57 validity: Vec<ValidityBitmap>,
59}
60
61fn slot_map(variables: &Arc<[VariableId]>) -> std::collections::HashMap<VariableId, usize> {
63 variables.iter().enumerate().map(|(i, &v)| (v, i)).collect()
64}
65
66impl LaggedFrame {
67 pub fn from_series(
76 data: &TimeSeriesData,
77 variables: &[VariableId],
78 max_lag: u32,
79 policy: &KernelPolicy,
80 ) -> Result<Self, DataError> {
81 Self::from_series_with_options(
82 data,
83 variables,
84 max_lag,
85 ReferencePointPolicy::SeriesOrigin,
86 LaggedFrameOptions::default(),
87 policy,
88 )
89 }
90
91 pub fn from_series_with_reference(
98 data: &TimeSeriesData,
99 variables: &[VariableId],
100 max_lag: u32,
101 reference: ReferencePointPolicy,
102 policy: &KernelPolicy,
103 ) -> Result<Self, DataError> {
104 Self::from_series_with_options(
105 data,
106 variables,
107 max_lag,
108 reference,
109 LaggedFrameOptions::default(),
110 policy,
111 )
112 }
113
114 pub fn from_series_with_options(
121 data: &TimeSeriesData,
122 variables: &[VariableId],
123 max_lag: u32,
124 reference: ReferencePointPolicy,
125 options: LaggedFrameOptions,
126 policy: &KernelPolicy,
127 ) -> Result<Self, DataError> {
128 if variables.is_empty() {
129 return Err(DataError::InvalidArgument {
130 message: "lagged frame needs ≥1 variable".into(),
131 });
132 }
133 let lag_map = LagMap::with_reference(data.row_count(), max_lag, reference)?;
134 let n_effective = lag_map.n_effective();
135 let n_lags = max_lag as usize + 1;
136 let n_cols = variables.len().saturating_mul(n_lags);
137 let mut values = vec![0.0; n_cols.saturating_mul(n_effective)];
138 let mut validity = Vec::with_capacity(n_cols);
139
140 let mut lag_rows = vec![vec![0usize; n_effective]; n_lags];
142 for (lag, rows) in lag_rows.iter_mut().enumerate() {
143 lag_map.fill_row_indexes(Lag::from_raw(lag as u32), rows)?;
144 }
145
146 let analysis = data.storage().analysis_mask();
147 for (slot, &var) in variables.iter().enumerate() {
148 let ColumnView::Float64(src) = data.column(var)? else {
149 return Err(DataError::TypeMismatch { id: var, expected: "float64" });
150 };
151 if options.missing == MissingPolicy::ErrorOnMissing && !src.validity.is_all_valid() {
152 return Err(DataError::IncompleteSeries {
153 id: Some(src.id),
154 message: "missing values under ErrorOnMissing policy",
155 });
156 }
157 if options.mask == MaskPolicy::Honor && options.missing == MissingPolicy::ErrorOnMissing
158 {
159 if let Some(mask) = analysis {
160 if !mask.is_all_valid() {
161 return Err(DataError::IncompleteSeries {
162 id: None,
163 message: "analysis mask hides rows under ErrorOnMissing policy",
164 });
165 }
166 }
167 }
168 let src_view = F64VectorView::contiguous(src.values.as_slice());
169 for (lag, rows) in lag_rows.iter().enumerate() {
170 let col = slot * n_lags + lag;
171 let dst = &mut values[col * n_effective..(col + 1) * n_effective];
172 gather(policy, src_view, rows, dst);
173 let col_valid = gather_column_validity(src, analysis, options.mask, rows)?;
174 validity.push(col_valid);
175 }
176 }
177
178 let variables: Arc<[VariableId]> = Arc::from(variables);
179 let slot_index = slot_map(&variables);
180 Ok(Self { variables, slot_index, max_lag, n_effective, n_lags, values, validity })
181 }
182
183 #[must_use]
185 pub fn variables(&self) -> &[VariableId] {
186 &self.variables
187 }
188
189 #[must_use]
191 pub const fn max_lag(&self) -> u32 {
192 self.max_lag
193 }
194
195 #[must_use]
197 pub const fn n_effective(&self) -> usize {
198 self.n_effective
199 }
200
201 #[must_use]
203 pub fn ncols(&self) -> usize {
204 self.variables.len().saturating_mul(self.n_lags)
205 }
206
207 #[must_use]
209 pub fn is_fully_valid(&self) -> bool {
210 self.validity.iter().all(ValidityBitmap::is_all_valid)
211 }
212
213 #[must_use]
215 pub fn values_bytes(&self) -> u64 {
216 (self.values.len() * core::mem::size_of::<f64>()) as u64
217 }
218
219 #[must_use]
221 pub fn column_index(&self, variable: VariableId, lag: Lag) -> Option<usize> {
222 let slot = *self.slot_index.get(&variable)?;
223 let l = lag.raw() as usize;
224 if l >= self.n_lags {
225 return None;
226 }
227 Some(slot * self.n_lags + l)
228 }
229
230 #[must_use]
236 pub fn column(&self, idx: usize) -> &[f64] {
237 let n = self.n_effective;
238 &self.values[idx * n..(idx + 1) * n]
239 }
240
241 #[must_use]
247 pub fn column_valid(&self, idx: usize) -> &ValidityBitmap {
248 &self.validity[idx]
249 }
250
251 pub fn keep_mask_for_columns(&self, cols: &[usize]) -> Result<Vec<bool>, DataError> {
257 if cols.is_empty() {
258 return Err(DataError::InvalidArgument {
259 message: "keep_mask_for_columns needs ≥1 column".into(),
260 });
261 }
262 let mut keep = vec![true; self.n_effective];
263 for &c in cols {
264 if c >= self.ncols() {
265 return Err(DataError::InvalidArgument {
266 message: format!("keep_mask_for_columns: column {c} out of range"),
267 });
268 }
269 let v = &self.validity[c];
270 for (i, slot) in keep.iter_mut().enumerate() {
271 if *slot && !v.is_valid(i) {
272 *slot = false;
273 }
274 }
275 }
276 Ok(keep)
277 }
278
279 pub fn retain_effective(&self, keep: &[bool]) -> Result<Self, DataError> {
288 if keep.len() != self.n_effective {
289 return Err(DataError::InvalidArgument {
290 message: format!(
291 "retain_effective keep length {} != n_effective {}",
292 keep.len(),
293 self.n_effective
294 ),
295 });
296 }
297 let n_new = keep.iter().filter(|&&k| k).count();
298 if n_new == 0 {
299 return Err(DataError::InvalidArgument {
300 message: "retain_effective: no effective rows retained".into(),
301 });
302 }
303 let n_cols = self.ncols();
304 let mut values = vec![0.0; n_cols.saturating_mul(n_new)];
305 let mut validity = Vec::with_capacity(n_cols);
306 for c in 0..n_cols {
307 let src = self.column(c);
308 let dst = &mut values[c * n_new..(c + 1) * n_new];
309 let mut j = 0;
310 for (i, &k) in keep.iter().enumerate() {
311 if k {
312 dst[j] = src[i];
313 j += 1;
314 }
315 }
316 validity.push(self.validity[c].compact(keep)?);
317 }
318 Ok(Self {
319 variables: Arc::clone(&self.variables),
320 slot_index: self.slot_index.clone(),
321 max_lag: self.max_lag,
322 n_effective: n_new,
323 n_lags: self.n_lags,
324 values,
325 validity,
326 })
327 }
328
329 pub fn stack(frames: &[Self]) -> Result<Self, DataError> {
337 let Some(first) = frames.first() else {
338 return Err(DataError::InvalidArgument {
339 message: "LaggedFrame::stack needs ≥1 frame".into(),
340 });
341 };
342 for (i, f) in frames.iter().enumerate().skip(1) {
343 if f.variables.as_ref() != first.variables.as_ref() {
344 return Err(DataError::InvalidArgument {
345 message: format!("LaggedFrame::stack: variables mismatch at frame {i}"),
346 });
347 }
348 if f.max_lag != first.max_lag || f.n_lags != first.n_lags {
349 return Err(DataError::InvalidArgument {
350 message: format!("LaggedFrame::stack: max_lag mismatch at frame {i}"),
351 });
352 }
353 }
354 let n_eff: usize = frames.iter().map(Self::n_effective).sum();
355 if n_eff == 0 {
356 return Err(DataError::InvalidArgument {
357 message: "LaggedFrame::stack: zero effective rows".into(),
358 });
359 }
360 let n_cols = first.ncols();
361 let mut values = vec![0.0; n_cols.saturating_mul(n_eff)];
362 let mut validity = Vec::with_capacity(n_cols);
363 for c in 0..n_cols {
364 let mut offset = 0usize;
365 for f in frames {
366 let src = f.column(c);
367 let dst = &mut values[c * n_eff + offset..c * n_eff + offset + f.n_effective];
368 dst.copy_from_slice(src);
369 offset += f.n_effective;
370 }
371 let parts: Vec<&ValidityBitmap> = frames.iter().map(|f| &f.validity[c]).collect();
372 validity.push(ValidityBitmap::concat(&parts)?);
373 }
374 Ok(Self {
375 variables: Arc::clone(&first.variables),
376 slot_index: first.slot_index.clone(),
377 max_lag: first.max_lag,
378 n_effective: n_eff,
379 n_lags: first.n_lags,
380 values,
381 validity,
382 })
383 }
384
385 pub fn append_constant_lag_columns(
396 &self,
397 columns: &[(VariableId, Vec<f64>)],
398 ) -> Result<Self, DataError> {
399 if columns.is_empty() {
400 return Ok(self.clone());
401 }
402 let mut vars = self.variables.to_vec();
403 for (id, col) in columns {
404 if col.len() != self.n_effective {
405 return Err(DataError::InvalidArgument {
406 message: format!(
407 "append_constant_lag_columns: column len {} != n_effective {}",
408 col.len(),
409 self.n_effective
410 ),
411 });
412 }
413 if vars.contains(id) {
414 return Err(DataError::InvalidArgument {
415 message: format!("append_constant_lag_columns: duplicate variable {id}"),
416 });
417 }
418 vars.push(*id);
419 }
420 let n_eff = self.n_effective;
421 let n_lags = self.n_lags;
422 let old_cols = self.ncols();
423 let new_slots = columns.len();
424 let n_cols = old_cols + new_slots * n_lags;
425 let mut values = vec![0.0; n_cols.saturating_mul(n_eff)];
426 values[..old_cols * n_eff].copy_from_slice(&self.values);
427 let mut validity = self.validity.clone();
428 for (s, (_id, col)) in columns.iter().enumerate() {
429 for lag in 0..n_lags {
430 let c = old_cols + s * n_lags + lag;
431 values[c * n_eff..(c + 1) * n_eff].copy_from_slice(col);
432 validity.push(ValidityBitmap::all_valid(n_eff));
433 }
434 }
435 let variables: Arc<[VariableId]> = Arc::from(vars);
436 Ok(Self {
437 slot_index: slot_map(&variables),
438 variables,
439 max_lag: self.max_lag,
440 n_effective: n_eff,
441 n_lags,
442 values,
443 validity,
444 })
445 }
446}
447
448fn gather_column_validity(
449 src: &crate::column::Float64Column,
450 analysis: Option<&ValidityBitmap>,
451 mask_policy: MaskPolicy,
452 rows: &[usize],
453) -> Result<ValidityBitmap, DataError> {
454 let col_valid = src.validity.gather_rows(rows)?;
455 match (mask_policy, analysis) {
456 (MaskPolicy::Ignore, _) | (MaskPolicy::Honor, None) => Ok(col_valid),
457 (MaskPolicy::Honor, Some(mask)) => {
458 let mask_valid = mask.gather_rows(rows)?;
459 let n = rows.len();
460 let mut bytes = vec![0u8; n.div_ceil(8)];
461 for i in 0..n {
462 if col_valid.is_valid(i) && mask_valid.is_valid(i) {
463 bytes[i / 8] |= 1 << (i % 8);
464 }
465 }
466 ValidityBitmap::from_bytes(bytes, n)
467 }
468 }
469}
470
471#[cfg(test)]
472#[allow(clippy::cast_precision_loss, clippy::many_single_char_names)]
473mod tests {
474 use antecedent_core::{Lag, VariableId};
475
476 use super::*;
477 use crate::sample_policy::{MaskPolicy, MissingPolicy};
478 use crate::testing::{float_series, float_series_with_gap, float_series_with_mask};
479
480 #[test]
481 fn builds_with_missing_values_marking_invalid() {
482 let data = float_series_with_gap(20, 2, 5);
483 let vars = [VariableId::from_raw(0), VariableId::from_raw(1)];
484 let frame = LaggedFrame::from_series(
485 &data,
486 &vars,
487 2,
488 &antecedent_core::KernelPolicy::default_policy(),
489 )
490 .unwrap();
491 assert_eq!(frame.n_effective(), 18);
492 let i0 = frame.column_index(vars[0], Lag::CONTEMPORANEOUS).unwrap();
494 assert!(!frame.column_valid(i0).is_valid(3));
495 assert!(frame.column_valid(i0).is_valid(0));
496 let i1 = frame.column_index(vars[1], Lag::CONTEMPORANEOUS).unwrap();
497 assert!(frame.column_valid(i1).is_all_valid());
498 }
499
500 #[test]
501 fn builds_with_analysis_mask_marking_invalid() {
502 let data = float_series_with_mask(20, 2, 5);
503 let vars = [VariableId::from_raw(0), VariableId::from_raw(1)];
504 let frame = LaggedFrame::from_series(
505 &data,
506 &vars,
507 2,
508 &antecedent_core::KernelPolicy::default_policy(),
509 )
510 .unwrap();
511 let i0 = frame.column_index(vars[0], Lag::CONTEMPORANEOUS).unwrap();
512 assert!(!frame.column_valid(i0).is_valid(3));
513 let keep = frame.keep_mask_for_columns(&[i0]).unwrap();
514 assert!(!keep[3]);
515 assert!(keep[0]);
516 }
517
518 #[test]
519 fn ignore_mask_keeps_analysis_hidden_rows_valid() {
520 let data = float_series_with_mask(20, 2, 5);
521 let vars = [VariableId::from_raw(0), VariableId::from_raw(1)];
522 let frame = LaggedFrame::from_series_with_options(
523 &data,
524 &vars,
525 2,
526 ReferencePointPolicy::SeriesOrigin,
527 LaggedFrameOptions { mask: MaskPolicy::Ignore, missing: MissingPolicy::CompleteCase },
528 &KernelPolicy::default_policy(),
529 )
530 .unwrap();
531 assert!(frame.is_fully_valid());
532 }
533
534 #[test]
535 fn error_on_missing_rejects_gaps() {
536 let data = float_series_with_gap(20, 2, 5);
537 let vars = [VariableId::from_raw(0), VariableId::from_raw(1)];
538 let err = LaggedFrame::from_series_with_options(
539 &data,
540 &vars,
541 2,
542 ReferencePointPolicy::SeriesOrigin,
543 LaggedFrameOptions { mask: MaskPolicy::Honor, missing: MissingPolicy::ErrorOnMissing },
544 &KernelPolicy::default_policy(),
545 )
546 .unwrap_err();
547 assert!(matches!(
548 err,
549 DataError::IncompleteSeries { id: Some(v), .. } if v == VariableId::from_raw(0)
550 ));
551 }
552
553 #[test]
554 fn frame_matches_lag_map_gather() {
555 let data = float_series(20, 2);
556 let vars = [VariableId::from_raw(0), VariableId::from_raw(1)];
557 let frame = LaggedFrame::from_series(
558 &data,
559 &vars,
560 2,
561 &antecedent_core::KernelPolicy::default_policy(),
562 )
563 .unwrap();
564 assert_eq!(frame.n_effective(), 18);
565 assert_eq!(frame.ncols(), 6);
566 assert!(frame.is_fully_valid());
567 let i = frame.column_index(vars[0], Lag::CONTEMPORANEOUS).unwrap();
568 assert!((frame.column(i)[0] - 2.0).abs() < 1e-12);
569 let j = frame.column_index(vars[1], Lag::from_raw(1)).unwrap();
570 assert!((frame.column(j)[0] - 101.0).abs() < 1e-12);
571 }
572
573 #[test]
574 fn retain_effective_compacts_validity() {
575 let data = float_series_with_mask(20, 2, 5);
576 let vars = [VariableId::from_raw(0), VariableId::from_raw(1)];
577 let frame = LaggedFrame::from_series(
578 &data,
579 &vars,
580 2,
581 &antecedent_core::KernelPolicy::default_policy(),
582 )
583 .unwrap();
584 let cols: Vec<usize> = (0..frame.ncols()).collect();
585 let keep = frame.keep_mask_for_columns(&cols).unwrap();
586 let compacted = frame.retain_effective(&keep).unwrap();
587 assert!(compacted.is_fully_valid());
588 assert_eq!(compacted.n_effective(), keep.iter().filter(|&&k| k).count());
589 }
590}