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 max_lag: u32,
49 n_effective: usize,
50 n_lags: usize,
51 values: Vec<f64>,
53 validity: Vec<ValidityBitmap>,
55}
56
57impl LaggedFrame {
58 pub fn from_series(
67 data: &TimeSeriesData,
68 variables: &[VariableId],
69 max_lag: u32,
70 policy: &KernelPolicy,
71 ) -> Result<Self, DataError> {
72 Self::from_series_with_options(
73 data,
74 variables,
75 max_lag,
76 ReferencePointPolicy::SeriesOrigin,
77 LaggedFrameOptions::default(),
78 policy,
79 )
80 }
81
82 pub fn from_series_with_reference(
89 data: &TimeSeriesData,
90 variables: &[VariableId],
91 max_lag: u32,
92 reference: ReferencePointPolicy,
93 policy: &KernelPolicy,
94 ) -> Result<Self, DataError> {
95 Self::from_series_with_options(
96 data,
97 variables,
98 max_lag,
99 reference,
100 LaggedFrameOptions::default(),
101 policy,
102 )
103 }
104
105 pub fn from_series_with_options(
112 data: &TimeSeriesData,
113 variables: &[VariableId],
114 max_lag: u32,
115 reference: ReferencePointPolicy,
116 options: LaggedFrameOptions,
117 policy: &KernelPolicy,
118 ) -> Result<Self, DataError> {
119 if variables.is_empty() {
120 return Err(DataError::InvalidArgument {
121 message: "lagged frame needs ≥1 variable".into(),
122 });
123 }
124 let lag_map = LagMap::with_reference(data.row_count(), max_lag, reference)?;
125 let n_effective = lag_map.n_effective();
126 let n_lags = max_lag as usize + 1;
127 let n_cols = variables.len().saturating_mul(n_lags);
128 let mut values = vec![0.0; n_cols.saturating_mul(n_effective)];
129 let mut validity = Vec::with_capacity(n_cols);
130
131 let mut lag_rows = vec![vec![0usize; n_effective]; n_lags];
133 for (lag, rows) in lag_rows.iter_mut().enumerate() {
134 lag_map.fill_row_indexes(Lag::from_raw(lag as u32), rows)?;
135 }
136
137 let analysis = data.storage().analysis_mask();
138 for (slot, &var) in variables.iter().enumerate() {
139 let ColumnView::Float64(src) = data.column(var)? else {
140 return Err(DataError::TypeMismatch { id: var, expected: "float64" });
141 };
142 if options.missing == MissingPolicy::ErrorOnMissing && !src.validity.is_all_valid() {
143 return Err(DataError::IncompleteSeries {
144 id: Some(src.id),
145 message: "missing values under ErrorOnMissing policy",
146 });
147 }
148 if options.mask == MaskPolicy::Honor && options.missing == MissingPolicy::ErrorOnMissing
149 {
150 if let Some(mask) = analysis {
151 if !mask.is_all_valid() {
152 return Err(DataError::IncompleteSeries {
153 id: None,
154 message: "analysis mask hides rows under ErrorOnMissing policy",
155 });
156 }
157 }
158 }
159 let src_view = F64VectorView::contiguous(src.values.as_slice());
160 for (lag, rows) in lag_rows.iter().enumerate() {
161 let col = slot * n_lags + lag;
162 let dst = &mut values[col * n_effective..(col + 1) * n_effective];
163 gather(policy, src_view, rows, dst);
164 let col_valid = gather_column_validity(src, analysis, options.mask, rows)?;
165 validity.push(col_valid);
166 }
167 }
168
169 Ok(Self { variables: Arc::from(variables), max_lag, n_effective, n_lags, values, validity })
170 }
171
172 #[must_use]
174 pub fn variables(&self) -> &[VariableId] {
175 &self.variables
176 }
177
178 #[must_use]
180 pub const fn max_lag(&self) -> u32 {
181 self.max_lag
182 }
183
184 #[must_use]
186 pub const fn n_effective(&self) -> usize {
187 self.n_effective
188 }
189
190 #[must_use]
192 pub fn ncols(&self) -> usize {
193 self.variables.len().saturating_mul(self.n_lags)
194 }
195
196 #[must_use]
198 pub fn is_fully_valid(&self) -> bool {
199 self.validity.iter().all(ValidityBitmap::is_all_valid)
200 }
201
202 #[must_use]
204 pub fn values_bytes(&self) -> u64 {
205 (self.values.len() * core::mem::size_of::<f64>()) as u64
206 }
207
208 #[must_use]
210 pub fn column_index(&self, variable: VariableId, lag: Lag) -> Option<usize> {
211 let slot = self.variables.iter().position(|&v| v == variable)?;
212 let l = lag.raw() as usize;
213 if l >= self.n_lags {
214 return None;
215 }
216 Some(slot * self.n_lags + l)
217 }
218
219 #[must_use]
225 pub fn column(&self, idx: usize) -> &[f64] {
226 let n = self.n_effective;
227 &self.values[idx * n..(idx + 1) * n]
228 }
229
230 #[must_use]
236 pub fn column_valid(&self, idx: usize) -> &ValidityBitmap {
237 &self.validity[idx]
238 }
239
240 pub fn keep_mask_for_columns(&self, cols: &[usize]) -> Result<Vec<bool>, DataError> {
246 if cols.is_empty() {
247 return Err(DataError::InvalidArgument {
248 message: "keep_mask_for_columns needs ≥1 column".into(),
249 });
250 }
251 let mut keep = vec![true; self.n_effective];
252 for &c in cols {
253 if c >= self.ncols() {
254 return Err(DataError::InvalidArgument {
255 message: format!("keep_mask_for_columns: column {c} out of range"),
256 });
257 }
258 let v = &self.validity[c];
259 for (i, slot) in keep.iter_mut().enumerate() {
260 if *slot && !v.is_valid(i) {
261 *slot = false;
262 }
263 }
264 }
265 Ok(keep)
266 }
267
268 pub fn retain_effective(&self, keep: &[bool]) -> Result<Self, DataError> {
277 if keep.len() != self.n_effective {
278 return Err(DataError::InvalidArgument {
279 message: format!(
280 "retain_effective keep length {} != n_effective {}",
281 keep.len(),
282 self.n_effective
283 ),
284 });
285 }
286 let n_new = keep.iter().filter(|&&k| k).count();
287 if n_new == 0 {
288 return Err(DataError::InvalidArgument {
289 message: "retain_effective: no effective rows retained".into(),
290 });
291 }
292 let n_cols = self.ncols();
293 let mut values = vec![0.0; n_cols.saturating_mul(n_new)];
294 let mut validity = Vec::with_capacity(n_cols);
295 for c in 0..n_cols {
296 let src = self.column(c);
297 let dst = &mut values[c * n_new..(c + 1) * n_new];
298 let mut j = 0;
299 for (i, &k) in keep.iter().enumerate() {
300 if k {
301 dst[j] = src[i];
302 j += 1;
303 }
304 }
305 validity.push(self.validity[c].compact(keep)?);
306 }
307 Ok(Self {
308 variables: Arc::clone(&self.variables),
309 max_lag: self.max_lag,
310 n_effective: n_new,
311 n_lags: self.n_lags,
312 values,
313 validity,
314 })
315 }
316
317 pub fn stack(frames: &[Self]) -> Result<Self, DataError> {
325 let Some(first) = frames.first() else {
326 return Err(DataError::InvalidArgument {
327 message: "LaggedFrame::stack needs ≥1 frame".into(),
328 });
329 };
330 for (i, f) in frames.iter().enumerate().skip(1) {
331 if f.variables.as_ref() != first.variables.as_ref() {
332 return Err(DataError::InvalidArgument {
333 message: format!("LaggedFrame::stack: variables mismatch at frame {i}"),
334 });
335 }
336 if f.max_lag != first.max_lag || f.n_lags != first.n_lags {
337 return Err(DataError::InvalidArgument {
338 message: format!("LaggedFrame::stack: max_lag mismatch at frame {i}"),
339 });
340 }
341 }
342 let n_eff: usize = frames.iter().map(Self::n_effective).sum();
343 if n_eff == 0 {
344 return Err(DataError::InvalidArgument {
345 message: "LaggedFrame::stack: zero effective rows".into(),
346 });
347 }
348 let n_cols = first.ncols();
349 let mut values = vec![0.0; n_cols.saturating_mul(n_eff)];
350 let mut validity = Vec::with_capacity(n_cols);
351 for c in 0..n_cols {
352 let mut offset = 0usize;
353 for f in frames {
354 let src = f.column(c);
355 let dst = &mut values[c * n_eff + offset..c * n_eff + offset + f.n_effective];
356 dst.copy_from_slice(src);
357 offset += f.n_effective;
358 }
359 let parts: Vec<&ValidityBitmap> = frames.iter().map(|f| &f.validity[c]).collect();
360 validity.push(ValidityBitmap::concat(&parts)?);
361 }
362 Ok(Self {
363 variables: Arc::clone(&first.variables),
364 max_lag: first.max_lag,
365 n_effective: n_eff,
366 n_lags: first.n_lags,
367 values,
368 validity,
369 })
370 }
371
372 pub fn append_constant_lag_columns(
383 &self,
384 columns: &[(VariableId, Vec<f64>)],
385 ) -> Result<Self, DataError> {
386 if columns.is_empty() {
387 return Ok(self.clone());
388 }
389 let mut vars = self.variables.to_vec();
390 for (id, col) in columns {
391 if col.len() != self.n_effective {
392 return Err(DataError::InvalidArgument {
393 message: format!(
394 "append_constant_lag_columns: column len {} != n_effective {}",
395 col.len(),
396 self.n_effective
397 ),
398 });
399 }
400 if vars.contains(id) {
401 return Err(DataError::InvalidArgument {
402 message: format!("append_constant_lag_columns: duplicate variable {id}"),
403 });
404 }
405 vars.push(*id);
406 }
407 let n_eff = self.n_effective;
408 let n_lags = self.n_lags;
409 let old_cols = self.ncols();
410 let new_slots = columns.len();
411 let n_cols = old_cols + new_slots * n_lags;
412 let mut values = vec![0.0; n_cols.saturating_mul(n_eff)];
413 values[..old_cols * n_eff].copy_from_slice(&self.values);
414 let mut validity = self.validity.clone();
415 for (s, (_id, col)) in columns.iter().enumerate() {
416 for lag in 0..n_lags {
417 let c = old_cols + s * n_lags + lag;
418 values[c * n_eff..(c + 1) * n_eff].copy_from_slice(col);
419 validity.push(ValidityBitmap::all_valid(n_eff));
420 }
421 }
422 Ok(Self {
423 variables: Arc::from(vars),
424 max_lag: self.max_lag,
425 n_effective: n_eff,
426 n_lags,
427 values,
428 validity,
429 })
430 }
431}
432
433fn gather_column_validity(
434 src: &crate::column::Float64Column,
435 analysis: Option<&ValidityBitmap>,
436 mask_policy: MaskPolicy,
437 rows: &[usize],
438) -> Result<ValidityBitmap, DataError> {
439 let col_valid = src.validity.gather_rows(rows)?;
440 match (mask_policy, analysis) {
441 (MaskPolicy::Ignore, _) | (MaskPolicy::Honor, None) => Ok(col_valid),
442 (MaskPolicy::Honor, Some(mask)) => {
443 let mask_valid = mask.gather_rows(rows)?;
444 let n = rows.len();
445 let mut bytes = vec![0u8; n.div_ceil(8)];
446 for i in 0..n {
447 if col_valid.is_valid(i) && mask_valid.is_valid(i) {
448 bytes[i / 8] |= 1 << (i % 8);
449 }
450 }
451 ValidityBitmap::from_bytes(bytes, n)
452 }
453 }
454}
455
456#[cfg(test)]
457#[allow(clippy::cast_precision_loss, clippy::many_single_char_names)]
458mod tests {
459 use antecedent_core::{Lag, VariableId};
460
461 use super::*;
462 use crate::sample_policy::{MaskPolicy, MissingPolicy};
463 use crate::testing::{float_series, float_series_with_gap, float_series_with_mask};
464
465 #[test]
466 fn builds_with_missing_values_marking_invalid() {
467 let data = float_series_with_gap(20, 2, 5);
468 let vars = [VariableId::from_raw(0), VariableId::from_raw(1)];
469 let frame = LaggedFrame::from_series(
470 &data,
471 &vars,
472 2,
473 &antecedent_core::KernelPolicy::default_policy(),
474 )
475 .unwrap();
476 assert_eq!(frame.n_effective(), 18);
477 let i0 = frame.column_index(vars[0], Lag::CONTEMPORANEOUS).unwrap();
479 assert!(!frame.column_valid(i0).is_valid(3));
480 assert!(frame.column_valid(i0).is_valid(0));
481 let i1 = frame.column_index(vars[1], Lag::CONTEMPORANEOUS).unwrap();
482 assert!(frame.column_valid(i1).is_all_valid());
483 }
484
485 #[test]
486 fn builds_with_analysis_mask_marking_invalid() {
487 let data = float_series_with_mask(20, 2, 5);
488 let vars = [VariableId::from_raw(0), VariableId::from_raw(1)];
489 let frame = LaggedFrame::from_series(
490 &data,
491 &vars,
492 2,
493 &antecedent_core::KernelPolicy::default_policy(),
494 )
495 .unwrap();
496 let i0 = frame.column_index(vars[0], Lag::CONTEMPORANEOUS).unwrap();
497 assert!(!frame.column_valid(i0).is_valid(3));
498 let keep = frame.keep_mask_for_columns(&[i0]).unwrap();
499 assert!(!keep[3]);
500 assert!(keep[0]);
501 }
502
503 #[test]
504 fn ignore_mask_keeps_analysis_hidden_rows_valid() {
505 let data = float_series_with_mask(20, 2, 5);
506 let vars = [VariableId::from_raw(0), VariableId::from_raw(1)];
507 let frame = LaggedFrame::from_series_with_options(
508 &data,
509 &vars,
510 2,
511 ReferencePointPolicy::SeriesOrigin,
512 LaggedFrameOptions { mask: MaskPolicy::Ignore, missing: MissingPolicy::CompleteCase },
513 &KernelPolicy::default_policy(),
514 )
515 .unwrap();
516 assert!(frame.is_fully_valid());
517 }
518
519 #[test]
520 fn error_on_missing_rejects_gaps() {
521 let data = float_series_with_gap(20, 2, 5);
522 let vars = [VariableId::from_raw(0), VariableId::from_raw(1)];
523 let err = LaggedFrame::from_series_with_options(
524 &data,
525 &vars,
526 2,
527 ReferencePointPolicy::SeriesOrigin,
528 LaggedFrameOptions { mask: MaskPolicy::Honor, missing: MissingPolicy::ErrorOnMissing },
529 &KernelPolicy::default_policy(),
530 )
531 .unwrap_err();
532 assert!(matches!(
533 err,
534 DataError::IncompleteSeries { id: Some(v), .. } if v == VariableId::from_raw(0)
535 ));
536 }
537
538 #[test]
539 fn frame_matches_lag_map_gather() {
540 let data = float_series(20, 2);
541 let vars = [VariableId::from_raw(0), VariableId::from_raw(1)];
542 let frame = LaggedFrame::from_series(
543 &data,
544 &vars,
545 2,
546 &antecedent_core::KernelPolicy::default_policy(),
547 )
548 .unwrap();
549 assert_eq!(frame.n_effective(), 18);
550 assert_eq!(frame.ncols(), 6);
551 assert!(frame.is_fully_valid());
552 let i = frame.column_index(vars[0], Lag::CONTEMPORANEOUS).unwrap();
553 assert!((frame.column(i)[0] - 2.0).abs() < 1e-12);
554 let j = frame.column_index(vars[1], Lag::from_raw(1)).unwrap();
555 assert!((frame.column(j)[0] - 101.0).abs() < 1e-12);
556 }
557
558 #[test]
559 fn retain_effective_compacts_validity() {
560 let data = float_series_with_mask(20, 2, 5);
561 let vars = [VariableId::from_raw(0), VariableId::from_raw(1)];
562 let frame = LaggedFrame::from_series(
563 &data,
564 &vars,
565 2,
566 &antecedent_core::KernelPolicy::default_policy(),
567 )
568 .unwrap();
569 let cols: Vec<usize> = (0..frame.ncols()).collect();
570 let keep = frame.keep_mask_for_columns(&cols).unwrap();
571 let compacted = frame.retain_effective(&keep).unwrap();
572 assert!(compacted.is_fully_valid());
573 assert_eq!(compacted.n_effective(), keep.iter().filter(|&&k| k).count());
574 }
575}