Skip to main content

antecedent_validate/
panel_slice.rs

1//! Per-unit panel slice template for refute refits.
2//!
3//! Stacked refute mutations (`with_replaced_float`, RCC append, analysis-mask subset)
4//! Arc-clone unmutated columns. Rebuilding the panel by copying every float column
5//! per unit per replicate is wasted work: unmutated unit buffers can be reused and
6//! only mutated / appended columns need a slice from the stacked table.
7//!
8//! SPDX-License-Identifier: MIT OR Apache-2.0
9
10use std::sync::Arc;
11
12use antecedent_data::{
13    Float64Column, OwnedColumn, OwnedColumnarStorage, PanelData, PanelUnit, TableView, TabularData,
14    TimeSeriesData, ValidityBitmap,
15};
16
17use crate::error::ValidationError;
18
19/// Per-unit offsets into a stacked panel table, plus payload pointers of the
20/// baseline stacked columns so a later mutation can reuse original unit Arcs.
21#[derive(Clone, Debug)]
22pub struct PanelSliceTemplate<'a> {
23    original: &'a PanelData,
24    /// Payload pointer of each baseline stacked column (`None` for empty).
25    baseline_ptrs: Vec<Option<*const u8>>,
26    /// Number of columns on the original panel schema (appended RCC columns sit after this).
27    original_ncols: usize,
28    offsets: Vec<usize>,
29    lengths: Vec<usize>,
30}
31
32impl<'a> PanelSliceTemplate<'a> {
33    /// Compile offsets and baseline stacked-column identity from `original` + `stacked`.
34    ///
35    /// `stacked` must be the pre-mutation concatenation of `original` (same row count).
36    ///
37    /// # Errors
38    ///
39    /// Row-count mismatch, or a unit whose column count disagrees with the panel schema.
40    pub fn from_panel(
41        original: &'a PanelData,
42        stacked: &TabularData,
43    ) -> Result<Self, ValidationError> {
44        let expected = original.total_rows();
45        if stacked.row_count() != expected {
46            return Err(ValidationError::data_msg(format!(
47                "stacked panel refute rows {} != panel total_rows {expected}",
48                stacked.row_count()
49            )));
50        }
51        let original_ncols = original.schema().len();
52        let mut offsets = Vec::with_capacity(original.unit_count());
53        let mut lengths = Vec::with_capacity(original.unit_count());
54        let mut offset = 0usize;
55        for u in original.units() {
56            let n = u.series.row_count();
57            if u.series.storage().columns().len() != original_ncols {
58                return Err(ValidationError::data_msg(
59                    "panel unit column count disagrees with panel schema",
60                ));
61            }
62            offsets.push(offset);
63            lengths.push(n);
64            offset += n;
65        }
66        let baseline_ptrs = stacked.storage().columns().iter().map(payload_ptr).collect();
67        Ok(Self { original, baseline_ptrs, original_ncols, offsets, lengths })
68    }
69
70    /// Rebuild a panel from a (possibly mutated / column-appended) stacked table.
71    ///
72    /// Unmutated columns whose stacked payload pointer still matches the baseline
73    /// reuse the original per-unit `OwnedColumn` Arc. Mutated and appended columns
74    /// are sliced from `stacked`.
75    ///
76    /// # Errors
77    ///
78    /// Row-count mismatch, out-of-range slice, or a non-float mutated column.
79    pub fn apply_stacked(&self, stacked: &TabularData) -> Result<PanelData, ValidationError> {
80        if stacked.row_count() != self.original.total_rows() {
81            return Err(ValidationError::data_msg(format!(
82                "stacked panel refute rows {} != panel total_rows {}",
83                stacked.row_count(),
84                self.original.total_rows()
85            )));
86        }
87        let stacked_storage = stacked.storage();
88        let stacked_cols = stacked_storage.columns();
89        let schema = stacked_storage.schema().clone();
90        let mut units = Vec::with_capacity(self.original.unit_count());
91        for (unit_idx, u) in self.original.units().iter().enumerate() {
92            let start = self.offsets[unit_idx];
93            let len = self.lengths[unit_idx];
94            let mut cols = Vec::with_capacity(stacked_cols.len());
95            for (j, col) in stacked_cols.iter().enumerate() {
96                let reuse = j < self.original_ncols
97                    && j < self.baseline_ptrs.len()
98                    && payload_ptr(col) == self.baseline_ptrs[j];
99                if reuse {
100                    cols.push(u.series.storage().columns()[j].clone());
101                } else {
102                    cols.push(slice_column(col, start, len)?);
103                }
104            }
105            let mask = stacked_storage
106                .analysis_mask()
107                .map(|m| slice_validity(m, start, len))
108                .transpose()
109                .map_err(ValidationError::from)?;
110            let weights = stacked_storage
111                .weights()
112                .map(|w| Arc::<[f64]>::from(w[start..start + len].to_vec()));
113            let storage = OwnedColumnarStorage::try_new(schema.clone(), cols, mask, weights)
114                .map_err(ValidationError::from)?;
115            let series = TimeSeriesData::try_new(storage, u.series.time_index().clone())
116                .map_err(ValidationError::from)?;
117            units.push(PanelUnit { unit_id: u.unit_id, series });
118        }
119        PanelData::try_new(Arc::from(units)).map_err(ValidationError::from)
120    }
121}
122
123fn payload_ptr(col: &OwnedColumn) -> Option<*const u8> {
124    match col {
125        OwnedColumn::Float64(c) => {
126            let s = c.values.as_slice();
127            (!s.is_empty()).then(|| s.as_ptr().cast())
128        }
129        OwnedColumn::Int64(c) => (!c.values.is_empty()).then(|| c.values.as_ptr().cast()),
130        OwnedColumn::Boolean(c) => (!c.values.is_empty()).then(|| c.values.as_ptr().cast()),
131        OwnedColumn::Categorical(c) => (!c.codes.is_empty()).then(|| c.codes.as_ptr().cast()),
132        OwnedColumn::Timestamp(c) => (!c.values_ns.is_empty()).then(|| c.values_ns.as_ptr().cast()),
133        OwnedColumn::FixedVector(c) => (!c.values.is_empty()).then(|| c.values.as_ptr().cast()),
134    }
135}
136
137fn slice_validity(
138    src: &ValidityBitmap,
139    start: usize,
140    len: usize,
141) -> Result<ValidityBitmap, antecedent_data::DataError> {
142    let mut bytes = vec![0u8; len.div_ceil(8)];
143    for i in 0..len {
144        if src.is_valid(start + i) {
145            bytes[i / 8] |= 1 << (i % 8);
146        }
147    }
148    ValidityBitmap::from_bytes(bytes, len)
149}
150
151fn slice_column(
152    col: &OwnedColumn,
153    start: usize,
154    len: usize,
155) -> Result<OwnedColumn, ValidationError> {
156    let end = start
157        .checked_add(len)
158        .ok_or(ValidationError::NotApplicable { message: "panel slice out of range" })?;
159    match col {
160        OwnedColumn::Float64(c) => {
161            if end > c.values.len() {
162                return Err(ValidationError::NotApplicable { message: "panel slice out of range" });
163            }
164            let values: Arc<[f64]> = Arc::from(c.values.as_slice()[start..end].to_vec());
165            let validity =
166                slice_validity(&c.validity, start, len).map_err(ValidationError::from)?;
167            Ok(OwnedColumn::Float64(
168                Float64Column::new(c.id, values, validity).map_err(ValidationError::from)?,
169            ))
170        }
171        _ => Err(ValidationError::NotApplicable {
172            message: "panel refute slice requires float64 columns",
173        }),
174    }
175}
176
177/// Old path: copy every stacked column into per-unit buffers (differential tests).
178#[cfg(test)]
179pub(crate) fn copy_all_panel_from_stacked(
180    original: &PanelData,
181    stacked: &TabularData,
182) -> Result<PanelData, ValidationError> {
183    let expected = original.total_rows();
184    if stacked.row_count() != expected {
185        return Err(ValidationError::data_msg(format!(
186            "stacked panel refute rows {} != panel total_rows {expected}",
187            stacked.row_count()
188        )));
189    }
190    let mut offset = 0usize;
191    let mut units = Vec::with_capacity(original.unit_count());
192    for u in original.units() {
193        let n = u.series.row_count();
194        let mut cols = Vec::with_capacity(stacked.storage().columns().len());
195        for col in stacked.storage().columns() {
196            cols.push(slice_column(col, offset, n)?);
197        }
198        let mask = stacked
199            .storage()
200            .analysis_mask()
201            .map(|m| slice_validity(m, offset, n))
202            .transpose()
203            .map_err(ValidationError::from)?;
204        let weights =
205            stacked.storage().weights().map(|w| Arc::<[f64]>::from(w[offset..offset + n].to_vec()));
206        let storage =
207            OwnedColumnarStorage::try_new(stacked.storage().schema().clone(), cols, mask, weights)
208                .map_err(ValidationError::from)?;
209        let series = TimeSeriesData::try_new(storage, u.series.time_index().clone())
210            .map_err(ValidationError::from)?;
211        units.push(PanelUnit { unit_id: u.unit_id, series });
212        offset += n;
213    }
214    PanelData::try_new(Arc::from(units)).map_err(ValidationError::from)
215}