#![allow(clippy::cast_possible_truncation)]
use std::sync::Arc;
use antecedent_core::{KernelPolicy, Lag, VariableId};
use antecedent_kernels::{F64VectorView, gather};
use crate::column::{ColumnView, Float64Column};
use crate::dataset::TimeSeriesData;
use crate::error::DataError;
use crate::reference::ReferencePointPolicy;
use crate::table::TableView;
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct LagMap {
series_len: usize,
max_lag: u32,
n_effective: usize,
base_t: usize,
reference: ReferencePointPolicy,
}
impl LagMap {
pub fn new(series_len: usize, max_lag: u32) -> Result<Self, DataError> {
Self::with_reference(series_len, max_lag, ReferencePointPolicy::SeriesOrigin)
}
pub fn with_reference(
series_len: usize,
max_lag: u32,
reference: ReferencePointPolicy,
) -> Result<Self, DataError> {
let (base_t, n_effective) = reference.base_and_n(series_len, max_lag)?;
Ok(Self { series_len, max_lag, n_effective, base_t, reference })
}
#[must_use]
pub const fn series_len(&self) -> usize {
self.series_len
}
#[must_use]
pub const fn max_lag(&self) -> u32 {
self.max_lag
}
#[must_use]
pub const fn n_effective(&self) -> usize {
self.n_effective
}
#[must_use]
pub const fn reference(&self) -> ReferencePointPolicy {
self.reference
}
#[must_use]
pub fn row_index(&self, lag: Lag, sample_i: usize) -> usize {
debug_assert!(sample_i < self.n_effective);
debug_assert!(lag.raw() <= self.max_lag);
self.base_t + sample_i - lag.raw() as usize
}
pub fn fill_row_indexes(&self, lag: Lag, out: &mut [usize]) -> Result<(), DataError> {
if out.len() != self.n_effective {
return Err(DataError::LengthMismatch {
expected: self.n_effective,
actual: out.len(),
context: "lag-map row index buffer",
});
}
if lag.raw() > self.max_lag {
return Err(DataError::InvalidArgument { message: "lag exceeds max_lag".into() });
}
for (i, slot) in out.iter_mut().enumerate() {
*slot = self.row_index(lag, i);
}
Ok(())
}
}
pub(crate) fn ensure_unmasked(data: &TimeSeriesData) -> Result<(), DataError> {
if let Some(mask) = data.storage().analysis_mask() {
if !mask.is_all_valid() {
return Err(DataError::IncompleteSeries {
id: None,
message: "analysis mask hides rows; temporal discovery requires complete series",
});
}
}
Ok(())
}
pub(crate) fn ensure_complete_float(src: &Float64Column) -> Result<(), DataError> {
if !src.validity.is_all_valid() {
return Err(DataError::IncompleteSeries {
id: Some(src.id),
message: "missing values in series; temporal discovery requires complete series",
});
}
Ok(())
}
#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)]
pub struct LaggedColumn {
pub variable: VariableId,
pub lag: Lag,
}
#[derive(Clone, Debug)]
pub struct LaggedSamplePlan {
columns: Arc<[LaggedColumn]>,
lag_map: Arc<LagMap>,
}
impl LaggedSamplePlan {
pub fn new(
series_len: usize,
max_lag: u32,
columns: impl Into<Arc<[LaggedColumn]>>,
) -> Result<Self, DataError> {
Self::with_reference(series_len, max_lag, ReferencePointPolicy::SeriesOrigin, columns)
}
pub fn with_reference(
series_len: usize,
max_lag: u32,
reference: ReferencePointPolicy,
columns: impl Into<Arc<[LaggedColumn]>>,
) -> Result<Self, DataError> {
let columns = columns.into();
if columns.is_empty() {
return Err(DataError::InvalidArgument {
message: "sample plan needs ≥1 column".into(),
});
}
let lag_map = Arc::new(LagMap::with_reference(series_len, max_lag, reference)?);
Self::validate_columns(lag_map.max_lag(), &columns)?;
Ok(Self { columns, lag_map })
}
pub fn with_shared(
lag_map: Arc<LagMap>,
columns: Arc<[LaggedColumn]>,
) -> Result<Self, DataError> {
if columns.is_empty() {
return Err(DataError::InvalidArgument {
message: "sample plan needs ≥1 column".into(),
});
}
Self::validate_columns(lag_map.max_lag(), &columns)?;
Ok(Self { columns, lag_map })
}
fn validate_columns(max_lag: u32, columns: &[LaggedColumn]) -> Result<(), DataError> {
for c in columns {
if c.lag.raw() > max_lag {
return Err(DataError::InvalidArgument {
message: "planned column lag exceeds max_lag".into(),
});
}
}
Ok(())
}
#[must_use]
pub fn columns(&self) -> &[LaggedColumn] {
&self.columns
}
#[must_use]
pub fn columns_arc(&self) -> &Arc<[LaggedColumn]> {
&self.columns
}
#[must_use]
pub fn lag_map(&self) -> &LagMap {
&self.lag_map
}
#[must_use]
pub fn lag_map_arc(&self) -> &Arc<LagMap> {
&self.lag_map
}
#[must_use]
pub fn n_effective(&self) -> usize {
self.lag_map.n_effective
}
pub fn prepare<'a>(
&'a self,
data: &TimeSeriesData,
workspace: &'a mut LaggedSampleWorkspace,
policy: &KernelPolicy,
) -> Result<LaggedPreparedSample<'a>, DataError> {
if data.row_count() != self.lag_map.series_len {
return Err(DataError::LengthMismatch {
expected: self.lag_map.series_len,
actual: data.row_count(),
context: "time series length vs sample plan",
});
}
ensure_unmasked(data)?;
let n = self.lag_map.n_effective;
let ncols = self.columns.len();
workspace.prepare(n, ncols);
for (c, col) in self.columns.iter().enumerate() {
let ColumnView::Float64(src) = data.column(col.variable)? else {
return Err(DataError::TypeMismatch { id: col.variable, expected: "float64" });
};
ensure_complete_float(src)?;
self.lag_map.fill_row_indexes(col.lag, &mut workspace.row_indexes[..n])?;
let dst = &mut workspace.values[c * n..(c + 1) * n];
gather(
policy,
F64VectorView::contiguous(src.values.as_slice()),
&workspace.row_indexes[..n],
dst,
);
}
Ok(LaggedPreparedSample {
n,
ncols,
values: &workspace.values[..n * ncols],
columns: &self.columns,
dropped: DropSummary { requested: self.lag_map.series_len, retained: n },
})
}
}
#[derive(Clone, Debug, Default)]
pub struct LaggedSampleWorkspace {
pub row_indexes: Vec<usize>,
pub values: Vec<f64>,
capacity_n: usize,
capacity_cols: usize,
}
impl LaggedSampleWorkspace {
pub fn prepare(&mut self, n: usize, ncols: usize) {
if self.row_indexes.len() < n {
self.row_indexes.resize(n, 0);
}
let need = n.saturating_mul(ncols);
if self.values.len() < need {
self.values.resize(need, 0.0);
}
self.capacity_n = self.capacity_n.max(n);
self.capacity_cols = self.capacity_cols.max(ncols);
}
#[must_use]
pub const fn capacity_n(&self) -> usize {
self.capacity_n
}
#[must_use]
pub const fn capacity_cols(&self) -> usize {
self.capacity_cols
}
}
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
pub struct DropSummary {
pub requested: usize,
pub retained: usize,
}
#[derive(Clone, Copy, Debug)]
pub struct LaggedPreparedSample<'a> {
pub n: usize,
pub ncols: usize,
pub values: &'a [f64],
pub columns: &'a [LaggedColumn],
pub dropped: DropSummary,
}
impl LaggedPreparedSample<'_> {
#[must_use]
pub fn column(&self, c: usize) -> &[f64] {
&self.values[c * self.n..(c + 1) * self.n]
}
}
impl TimeSeriesData {
pub fn plan_lagged_sample(
&self,
max_lag: u32,
columns: impl Into<Arc<[LaggedColumn]>>,
) -> Result<LaggedSamplePlan, DataError> {
LaggedSamplePlan::new(self.row_count(), max_lag, columns)
}
}
#[cfg(test)]
#[allow(clippy::cast_precision_loss, clippy::many_single_char_names)]
mod tests {
use std::sync::Arc;
use antecedent_core::{Lag, VariableId};
use super::*;
use crate::testing::{float_series, float_series_with_gap, float_series_with_mask};
#[test]
fn prepare_rejects_missing_values() {
let data = float_series_with_gap(20, 1, 3);
let cols = Arc::from([LaggedColumn {
variable: VariableId::from_raw(0),
lag: Lag::CONTEMPORANEOUS,
}]);
let plan = data.plan_lagged_sample(2, cols).unwrap();
let mut ws = LaggedSampleWorkspace::default();
let err = plan.prepare(&data, &mut ws, &KernelPolicy::default_policy()).unwrap_err();
assert!(matches!(
err,
DataError::IncompleteSeries { id: Some(v), .. } if v == VariableId::from_raw(0)
));
}
#[test]
fn prepare_rejects_row_hiding_analysis_mask() {
let data = float_series_with_mask(20, 1, 3);
let cols = Arc::from([LaggedColumn {
variable: VariableId::from_raw(0),
lag: Lag::CONTEMPORANEOUS,
}]);
let plan = data.plan_lagged_sample(2, cols).unwrap();
let mut ws = LaggedSampleWorkspace::default();
let err = plan.prepare(&data, &mut ws, &KernelPolicy::default_policy()).unwrap_err();
assert!(matches!(err, DataError::IncompleteSeries { id: None, .. }));
}
#[test]
fn lag_map_row_indexes() {
let map = LagMap::new(10, 2).unwrap();
assert_eq!(map.n_effective(), 8);
assert_eq!(map.row_index(Lag::CONTEMPORANEOUS, 0), 2);
assert_eq!(map.row_index(Lag::from_raw(2), 0), 0);
assert_eq!(map.row_index(Lag::from_raw(1), 3), 2 + 3 - 1);
}
#[test]
fn prepare_gathers_lagged_values() {
let data = float_series(20, 2);
let cols = Arc::from([
LaggedColumn { variable: VariableId::from_raw(0), lag: Lag::CONTEMPORANEOUS },
LaggedColumn { variable: VariableId::from_raw(0), lag: Lag::from_raw(2) },
LaggedColumn { variable: VariableId::from_raw(1), lag: Lag::from_raw(1) },
]);
let plan = data.plan_lagged_sample(2, cols).unwrap();
let mut ws = LaggedSampleWorkspace::default();
let prep = plan.prepare(&data, &mut ws, &KernelPolicy::default_policy()).unwrap();
assert_eq!(prep.n, 18);
assert!((prep.column(0)[0] - 2.0).abs() < 1e-12);
assert!((prep.column(1)[0] - 0.0).abs() < 1e-12);
assert!((prep.column(2)[0] - 101.0).abs() < 1e-12);
}
#[test]
fn repeated_prepare_reuses_workspace_capacity() {
let data = float_series(100, 3);
let cols = Arc::from([
LaggedColumn { variable: VariableId::from_raw(0), lag: Lag::CONTEMPORANEOUS },
LaggedColumn { variable: VariableId::from_raw(1), lag: Lag::from_raw(3) },
LaggedColumn { variable: VariableId::from_raw(2), lag: Lag::from_raw(1) },
]);
let plan = data.plan_lagged_sample(3, cols).unwrap();
let mut ws = LaggedSampleWorkspace::default();
let _ = plan.prepare(&data, &mut ws, &KernelPolicy::default_policy()).unwrap();
let cap_n = ws.capacity_n();
let cap_c = ws.capacity_cols();
let values_cap = ws.values.capacity();
let idx_cap = ws.row_indexes.capacity();
for _ in 0..50 {
let _ = plan.prepare(&data, &mut ws, &KernelPolicy::default_policy()).unwrap();
assert_eq!(ws.capacity_n(), cap_n);
assert_eq!(ws.capacity_cols(), cap_c);
assert_eq!(ws.values.capacity(), values_cap);
assert_eq!(ws.row_indexes.capacity(), idx_cap);
}
}
}