1#![allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
6
7use std::sync::Arc;
8
9use antecedent_core::{CausalSchema, VariableId};
10
11use crate::column::{
12 BooleanColumn, ColumnView, FixedVectorColumn, Float64Column, Int64Column, OwnedColumn,
13 TimestampColumn, ValidityBitmap,
14};
15use crate::dataset::TimeSeriesData;
16use crate::error::DataError;
17use crate::storage::OwnedColumnarStorage;
18use crate::table::TableView;
19use crate::temporal::{SamplingRegularity, TimeIndex};
20
21#[derive(Clone, Debug)]
26pub struct EventData {
27 storage: OwnedColumnarStorage,
28 event_times_ns: Arc<[i64]>,
30}
31
32impl EventData {
33 pub fn try_new(
39 storage: OwnedColumnarStorage,
40 event_times_ns: impl Into<Arc<[i64]>>,
41 ) -> Result<Self, DataError> {
42 let event_times_ns = event_times_ns.into();
43 let n = storage.row_count();
44 if event_times_ns.len() != n {
45 return Err(DataError::LengthMismatch {
46 expected: n,
47 actual: event_times_ns.len(),
48 context: "event times",
49 });
50 }
51 if n == 0 {
52 return Err(DataError::InvalidArgument {
53 message: "event data requires ≥1 event".into(),
54 });
55 }
56 for w in event_times_ns.windows(2) {
57 if w[1] < w[0] {
58 return Err(DataError::InvalidArgument {
59 message: "event times must be non-decreasing".into(),
60 });
61 }
62 }
63 Ok(Self { storage, event_times_ns })
64 }
65
66 #[must_use]
68 pub fn event_times_ns(&self) -> &[i64] {
69 &self.event_times_ns
70 }
71
72 #[must_use]
74 pub fn storage(&self) -> &OwnedColumnarStorage {
75 &self.storage
76 }
77
78 #[must_use]
80 pub const fn regularity(&self) -> SamplingRegularity {
81 SamplingRegularity::Irregular
82 }
83
84 pub fn reject_integer_lag_planning(&self) -> Result<(), DataError> {
90 Err(DataError::InvalidArgument {
91 message:
92 "integer lags are not valid on EventData; use duration windows or event models"
93 .into(),
94 })
95 }
96
97 pub fn align_to_grid(&self, interval_ns: u64) -> Result<TimeSeriesData, DataError> {
111 if interval_ns == 0 {
112 return Err(DataError::InvalidArgument {
113 message: "align_to_grid requires interval_ns > 0".into(),
114 });
115 }
116 let times = self.event_times_ns.as_ref();
117 let t0 = times[0];
118 let t_last = times[times.len() - 1];
119 let span = (t_last - t0) as u64;
120 let n_bins = (span / interval_ns) as usize + 1;
121
122 let mut last_in_bin: Vec<Option<usize>> = vec![None; n_bins];
124 for (i, &t) in times.iter().enumerate() {
125 let bin = ((t - t0) as u64 / interval_ns) as usize;
126 let bin = bin.min(n_bins - 1);
127 last_in_bin[bin] = Some(i);
128 }
129
130 let mut out_cols = Vec::with_capacity(self.storage.columns().len());
131 for col in self.storage.columns() {
132 out_cols.push(align_column(col, &last_in_bin, n_bins)?);
133 }
134
135 let mut analysis_mask = None;
136 if let Some(mask) = self.storage.analysis_mask() {
137 let mut bytes = vec![0u8; n_bins.div_ceil(8)];
138 for (b, &src) in last_in_bin.iter().enumerate() {
139 if let Some(i) = src {
140 if mask.is_valid(i) {
141 bytes[b / 8] |= 1 << (b % 8);
142 }
143 }
144 }
145 analysis_mask = Some(ValidityBitmap::from_bytes(bytes, n_bins)?);
146 }
147
148 let weights = self.storage.weights().map(|w| {
149 let mut out = vec![0.0_f64; n_bins];
150 for (b, &src) in last_in_bin.iter().enumerate() {
151 if let Some(i) = src {
152 out[b] = w[i];
153 }
154 }
155 Arc::<[f64]>::from(out)
156 });
157
158 let storage = OwnedColumnarStorage::try_new(
159 self.storage.schema().clone(),
160 out_cols,
161 analysis_mask,
162 weights,
163 )?;
164 TimeSeriesData::try_new(
165 storage,
166 TimeIndex { regularity: SamplingRegularity::Regular { interval_ns }, length: n_bins },
167 )
168 }
169}
170
171fn align_column(
172 col: &OwnedColumn,
173 last_in_bin: &[Option<usize>],
174 n_bins: usize,
175) -> Result<OwnedColumn, DataError> {
176 let mut valid_bytes = vec![0u8; n_bins.div_ceil(8)];
177 match col {
178 OwnedColumn::Float64(c) => {
179 let mut values = vec![0.0_f64; n_bins];
180 for (b, &src) in last_in_bin.iter().enumerate() {
181 if let Some(i) = src {
182 if c.validity.is_valid(i) {
183 values[b] = c.values.as_slice()[i];
184 valid_bytes[b / 8] |= 1 << (b % 8);
185 }
186 }
187 }
188 Ok(OwnedColumn::Float64(Float64Column::new(
189 c.id,
190 Arc::<[f64]>::from(values),
191 ValidityBitmap::from_bytes(valid_bytes, n_bins)?,
192 )?))
193 }
194 OwnedColumn::Int64(c) => {
195 let mut values = vec![0_i64; n_bins];
196 for (b, &src) in last_in_bin.iter().enumerate() {
197 if let Some(i) = src {
198 if c.validity.is_valid(i) {
199 values[b] = c.values[i];
200 valid_bytes[b / 8] |= 1 << (b % 8);
201 }
202 }
203 }
204 Ok(OwnedColumn::Int64(Int64Column::new(
205 c.id,
206 Arc::<[i64]>::from(values),
207 ValidityBitmap::from_bytes(valid_bytes, n_bins)?,
208 )?))
209 }
210 OwnedColumn::Boolean(c) => {
211 let mut values = vec![0u8; n_bins];
212 for (b, &src) in last_in_bin.iter().enumerate() {
213 if let Some(i) = src {
214 if c.validity.is_valid(i) {
215 values[b] = c.values[i];
216 valid_bytes[b / 8] |= 1 << (b % 8);
217 }
218 }
219 }
220 Ok(OwnedColumn::Boolean(BooleanColumn::new(
221 c.id,
222 Arc::<[u8]>::from(values),
223 ValidityBitmap::from_bytes(valid_bytes, n_bins)?,
224 )?))
225 }
226 OwnedColumn::Timestamp(c) => {
227 let mut values = vec![0_i64; n_bins];
228 for (b, &src) in last_in_bin.iter().enumerate() {
229 if let Some(i) = src {
230 if c.validity.is_valid(i) {
231 values[b] = c.values_ns[i];
232 valid_bytes[b / 8] |= 1 << (b % 8);
233 }
234 }
235 }
236 Ok(OwnedColumn::Timestamp(TimestampColumn::new(
237 c.id,
238 Arc::<[i64]>::from(values),
239 ValidityBitmap::from_bytes(valid_bytes, n_bins)?,
240 )?))
241 }
242 OwnedColumn::FixedVector(c) => {
243 let dim = c.dim;
244 let mut values = vec![0.0_f64; n_bins * dim];
245 for (b, &src) in last_in_bin.iter().enumerate() {
246 if let Some(i) = src {
247 if c.validity.is_valid(i) {
248 let src_off = i * dim;
249 let dst_off = b * dim;
250 values[dst_off..dst_off + dim]
251 .copy_from_slice(&c.values[src_off..src_off + dim]);
252 valid_bytes[b / 8] |= 1 << (b % 8);
253 }
254 }
255 }
256 Ok(OwnedColumn::FixedVector(FixedVectorColumn::new(
257 c.id,
258 dim,
259 Arc::<[f64]>::from(values),
260 ValidityBitmap::from_bytes(valid_bytes, n_bins)?,
261 )?))
262 }
263 OwnedColumn::Categorical(_) => Err(DataError::InvalidArgument {
264 message: "align_to_grid does not support categorical mark columns".into(),
265 }),
266 }
267}
268
269impl TableView for EventData {
270 fn schema(&self) -> &CausalSchema {
271 self.storage.schema()
272 }
273
274 fn row_count(&self) -> usize {
275 self.storage.row_count()
276 }
277
278 fn column(&self, id: VariableId) -> Result<ColumnView<'_>, DataError> {
279 self.storage.column(id)
280 }
281}
282
283#[cfg(test)]
284mod tests {
285 use std::sync::Arc;
286
287 use antecedent_core::{
288 CausalSchemaBuilder, MeasurementSpec, RoleHint, SmallRoleSet, ValueType, VariableId,
289 };
290
291 use super::*;
292 use crate::column::{Float64Column, OwnedColumn, ValidityBitmap};
293 use crate::table::TableView;
294
295 fn one_col(n: usize) -> OwnedColumnarStorage {
296 let mut b = CausalSchemaBuilder::new();
297 b.add_variable(
298 "mark",
299 ValueType::Continuous,
300 SmallRoleSet::from_hint(RoleHint::Context),
301 None,
302 None,
303 MeasurementSpec::default(),
304 )
305 .unwrap();
306 let schema = b.build().unwrap();
307 let col = Float64Column::new(
308 VariableId::from_raw(0),
309 Arc::<[f64]>::from(vec![1.0; n]),
310 ValidityBitmap::all_valid(n),
311 )
312 .unwrap();
313 OwnedColumnarStorage::try_new(schema, vec![OwnedColumn::Float64(col)], None, None).unwrap()
314 }
315
316 #[test]
317 fn event_data_round_trip() {
318 let times = Arc::<[i64]>::from(vec![0, 10, 10, 25]);
319 let data = EventData::try_new(one_col(4), times).unwrap();
320 assert_eq!(data.row_count(), 4);
321 assert_eq!(data.event_times_ns(), &[0, 10, 10, 25]);
322 assert_eq!(data.regularity(), SamplingRegularity::Irregular);
323 assert!(data.reject_integer_lag_planning().is_err());
324 }
325
326 #[test]
327 fn rejects_decreasing_times() {
328 let err = EventData::try_new(one_col(3), Arc::<[i64]>::from(vec![0, 5, 4]));
329 assert!(err.is_err());
330 }
331
332 #[test]
333 fn align_to_grid_last_event_in_bin() {
334 let mut b = CausalSchemaBuilder::new();
335 b.add_variable(
336 "mark",
337 ValueType::Continuous,
338 SmallRoleSet::from_hint(RoleHint::Context),
339 None,
340 None,
341 MeasurementSpec::default(),
342 )
343 .unwrap();
344 let schema = b.build().unwrap();
345 let col = Float64Column::new(
348 VariableId::from_raw(0),
349 Arc::<[f64]>::from(vec![1.0, 2.0, 3.0]),
350 ValidityBitmap::all_valid(3),
351 )
352 .unwrap();
353 let storage =
354 OwnedColumnarStorage::try_new(schema, vec![OwnedColumn::Float64(col)], None, None)
355 .unwrap();
356 let data = EventData::try_new(storage, Arc::<[i64]>::from(vec![0, 5, 15])).unwrap();
357 let series = data.align_to_grid(10).unwrap();
358 assert_eq!(series.row_count(), 2);
359 assert_eq!(series.time_index().regularity, SamplingRegularity::Regular { interval_ns: 10 });
360 let ColumnView::Float64(c) = series.column(VariableId::from_raw(0)).unwrap() else {
361 panic!("expected float");
362 };
363 assert!((c.values.as_slice()[0] - 2.0).abs() < f64::EPSILON);
364 assert!((c.values.as_slice()[1] - 3.0).abs() < f64::EPSILON);
365 assert!(c.validity.is_valid(0) && c.validity.is_valid(1));
366 }
367
368 #[test]
369 fn align_to_grid_marks_empty_bins_invalid() {
370 let mut b = CausalSchemaBuilder::new();
371 b.add_variable(
372 "mark",
373 ValueType::Continuous,
374 SmallRoleSet::from_hint(RoleHint::Context),
375 None,
376 None,
377 MeasurementSpec::default(),
378 )
379 .unwrap();
380 let schema = b.build().unwrap();
381 let col = Float64Column::new(
383 VariableId::from_raw(0),
384 Arc::<[f64]>::from(vec![1.0, 9.0]),
385 ValidityBitmap::all_valid(2),
386 )
387 .unwrap();
388 let storage =
389 OwnedColumnarStorage::try_new(schema, vec![OwnedColumn::Float64(col)], None, None)
390 .unwrap();
391 let data = EventData::try_new(storage, Arc::<[i64]>::from(vec![0, 20])).unwrap();
392 let series = data.align_to_grid(10).unwrap();
393 assert_eq!(series.row_count(), 3);
394 let ColumnView::Float64(c) = series.column(VariableId::from_raw(0)).unwrap() else {
395 panic!("expected float");
396 };
397 assert!(c.validity.is_valid(0));
398 assert!(!c.validity.is_valid(1));
399 assert!(c.validity.is_valid(2));
400 assert!((c.values.as_slice()[2] - 9.0).abs() < f64::EPSILON);
401 }
402
403 #[test]
404 fn align_rejects_zero_interval() {
405 let data = EventData::try_new(one_col(2), Arc::<[i64]>::from(vec![0, 1])).unwrap();
406 assert!(data.align_to_grid(0).is_err());
407 }
408}