Skip to main content

eventcv_core/transform/
temporal.rs

1//! Temporal transforms — operate on timestamps (and event selection in time); coordinates and
2//! sensor size are unchanged.
3
4use crate::{EventStream, EventStreamBuilder};
5
6impl EventStream {
7    /// Keeps events whose timestamp lies in the half-open window `[t0, t1)`.
8    ///
9    /// In a time-ordered stream — what every reader and the simulator produce — the window is a
10    /// contiguous range, so this is two binary searches and a slice rather than a predicate per
11    /// event; a window covering the whole stream copies nothing at all. The scan that establishes
12    /// the ordering is one pass over the timestamps and vectorises, and an unordered stream falls
13    /// back to the general path rather than being silently mis-windowed.
14    pub fn time_window(&self, t0: i64, t1: i64) -> EventStream {
15        let (width, height) = self.sensor_size();
16        let ts = self.ts();
17        if !ts.is_sorted() {
18            return self.remap(width, height, |x, y, t, p| {
19                (t >= t0 && t < t1).then_some((x, y, t, p))
20            });
21        }
22        let (lo, hi) = (
23            ts.partition_point(|&t| t < t0),
24            ts.partition_point(|&t| t < t1),
25        );
26        if (lo, hi) == (0, self.len()) {
27            return self.clone();
28        }
29        let mut builder =
30            EventStreamBuilder::with_capacity(width, height, self.timestamp_scale_ms(), hi - lo);
31        builder.extend_from_columns(
32            &self.xs()[lo..hi],
33            &self.ys()[lo..hi],
34            &ts[lo..hi],
35            &self.ps()[lo..hi],
36        );
37        builder.build()
38    }
39
40    /// Shifts every timestamp by `dt` (same units as the stored timestamps).
41    pub fn time_shift(&self, dt: i64) -> EventStream {
42        let (width, height) = self.sensor_size();
43        self.map_columns(width, height, |out| {
44            for t in &mut out.ts {
45                *t += dt;
46            }
47        })
48    }
49
50    /// Scales every timestamp by `factor` (rounded), e.g. to change playback speed.
51    pub fn time_scale(&self, factor: f64) -> EventStream {
52        let (width, height) = self.sensor_size();
53        self.map_columns(width, height, |out| {
54            for t in &mut out.ts {
55                *t = (*t as f64 * factor).round() as i64;
56            }
57        })
58    }
59
60    /// Shifts timestamps so the earliest event starts at zero. A no-op on an empty stream.
61    pub fn normalize_time(&self) -> EventStream {
62        match self.ts().iter().min() {
63            Some(&t_min) => self.time_shift(-t_min),
64            None => self.clone(),
65        }
66    }
67
68    /// Keeps every `k`-th event by index (`k = 1` is the identity); `k = 0` is treated as 1.
69    pub fn decimate(&self, k: usize) -> EventStream {
70        let k = k.max(1);
71        let (width, height) = self.sensor_size();
72        let mut builder = EventStreamBuilder::with_capacity(
73            width,
74            height,
75            self.timestamp_scale_ms(),
76            self.len() / k + 1,
77        );
78        let (xs, ys, ts, ps) = (self.xs(), self.ys(), self.ts(), self.ps());
79        for index in (0..self.len()).step_by(k) {
80            builder.push(xs[index], ys[index], ts[index], ps[index]);
81        }
82        builder.build()
83    }
84}
85
86#[cfg(test)]
87mod tests {
88    use crate::{EventStream, EventStreamBuilder};
89
90    fn sample() -> EventStream {
91        let mut builder = EventStreamBuilder::new(4, 3, 0.001);
92        for i in 0..6u16 {
93            builder.push(i % 4, i % 3, 100 + i64::from(i) * 10, i % 2 == 0);
94        }
95        builder.build()
96    }
97
98    #[test]
99    fn time_window_is_half_open() {
100        let windowed = sample().time_window(110, 140); // keeps t = 110, 120, 130
101        assert_eq!(windowed.ts(), &[110, 120, 130]);
102    }
103
104    /// The sorted fast path is a different code path from the general one, so both have to agree
105    /// — including on a stream whose timestamps are out of order, where only the general one is
106    /// correct.
107    #[test]
108    fn time_window_agrees_on_unordered_streams() {
109        let mut builder = EventStreamBuilder::new(4, 3, 0.001);
110        for (index, t) in [130_i64, 100, 150, 110, 140, 120].into_iter().enumerate() {
111            let index = index as u16;
112            builder.push(index % 4, index % 3, t, index.is_multiple_of(2));
113        }
114        let unordered = builder.build();
115
116        let windowed = unordered.time_window(110, 140);
117        assert_eq!(windowed.ts(), &[130, 110, 120]); // input order kept, window applied per event
118
119        // Sorting first must select the same events, only ordered.
120        let sorted = unordered.sort_by_time().time_window(110, 140);
121        assert_eq!(sorted.ts(), &[110, 120, 130]);
122
123        // A window covering everything is the stream itself, on either path.
124        assert_eq!(unordered.time_window(0, 1_000).ts(), unordered.ts());
125        assert_eq!(sample().time_window(0, 1_000).ts(), sample().ts());
126    }
127
128    #[test]
129    fn time_shift_inverts_and_preserves_count() {
130        let s = sample();
131        let back = s.time_shift(1000).time_shift(-1000);
132        assert_eq!(back.ts(), s.ts());
133        assert_eq!(back.len(), s.len());
134    }
135
136    #[test]
137    fn normalize_time_starts_at_zero() {
138        let n = sample().normalize_time();
139        assert_eq!(n.ts()[0], 0);
140        assert_eq!(n.ts(), &[0, 10, 20, 30, 40, 50]);
141    }
142
143    #[test]
144    fn time_scale_rounds() {
145        let scaled = sample().time_scale(0.5);
146        assert_eq!(scaled.ts(), &[50, 55, 60, 65, 70, 75]);
147    }
148
149    #[test]
150    fn decimate_keeps_every_kth_event() {
151        let d = sample().decimate(2);
152        assert_eq!(d.len(), 3);
153        assert_eq!(d.ts(), &[100, 120, 140]);
154        assert_eq!(sample().decimate(0).len(), 6); // k=0 treated as identity
155    }
156
157    #[test]
158    fn temporal_ops_handle_empty() {
159        let empty = EventStreamBuilder::new(4, 3, 0.001).build();
160        assert!(empty.normalize_time().is_empty());
161        assert!(empty.time_shift(5).is_empty());
162        assert!(empty.decimate(3).is_empty());
163    }
164}