Skip to main content

tacet_core/statistics/
acquisition.rs

1//! Acquisition stream model for timing measurements.
2//!
3//! This module defines the data structure for storing timing measurements in their
4//! acquisition order, preserving the temporal dependence structure needed for
5//! correct bootstrap resampling.
6//!
7//! See spec Section 2.3.1 (Acquisition Stream Model).
8
9extern crate alloc;
10
11use alloc::vec::Vec;
12use serde::{Deserialize, Serialize};
13
14use crate::types::{Class, TimingSample};
15
16/// Class label for a timing sample.
17#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
18pub enum SampleClass {
19    /// Fixed (baseline) class - typically all-zeros or a specific input.
20    Fixed,
21    /// Random class - randomly sampled inputs.
22    Random,
23}
24
25/// An interleaved acquisition stream of timing measurements.
26///
27/// Measurement produces an interleaved stream indexed by acquisition time:
28/// `{(c_t, y_t)}` where `c_t` is the class label and `y_t` is the timing.
29///
30/// This structure is critical for correct dependence estimation. The underlying
31/// stochastic process operates in continuous time—drift, frequency scaling, and
32/// cache state evolution affect nearby samples regardless of class. Bootstrap
33/// resampling must preserve adjacency in acquisition order, not per-class position.
34///
35/// See spec Section 2.3.1 for the full rationale.
36#[derive(Debug, Clone, Default, Serialize, Deserialize)]
37pub struct AcquisitionStream {
38    /// Interleaved (class, timing) pairs in acquisition order.
39    samples: Vec<(SampleClass, f64)>,
40}
41
42impl AcquisitionStream {
43    /// Create a new empty acquisition stream.
44    pub fn new() -> Self {
45        Self {
46            samples: Vec::new(),
47        }
48    }
49
50    /// Create a new acquisition stream with pre-allocated capacity.
51    pub fn with_capacity(capacity: usize) -> Self {
52        Self {
53            samples: Vec::with_capacity(capacity),
54        }
55    }
56
57    /// Push a new sample onto the stream.
58    #[inline]
59    pub fn push(&mut self, class: SampleClass, timing: f64) {
60        self.samples.push((class, timing));
61    }
62
63    /// Push a batch of samples, interleaving Fixed and Random classes.
64    ///
65    /// Samples are pushed in interleaved order: F, R, F, R, ...
66    /// Both vectors must have the same length.
67    pub fn push_batch_interleaved(&mut self, fixed: &[f64], random: &[f64]) {
68        debug_assert_eq!(
69            fixed.len(),
70            random.len(),
71            "Fixed and random batches must have same length"
72        );
73
74        self.samples.reserve(fixed.len() + random.len());
75        for (f, r) in fixed.iter().zip(random.iter()) {
76            self.samples.push((SampleClass::Fixed, *f));
77            self.samples.push((SampleClass::Random, *r));
78        }
79    }
80
81    /// Get the total number of samples in the stream.
82    #[inline]
83    pub fn len(&self) -> usize {
84        self.samples.len()
85    }
86
87    /// Check if the stream is empty.
88    #[inline]
89    pub fn is_empty(&self) -> bool {
90        self.samples.is_empty()
91    }
92
93    /// Get the number of samples per class (assumes balanced classes).
94    #[inline]
95    pub fn n_per_class(&self) -> usize {
96        self.samples.len() / 2
97    }
98
99    /// Split the stream into per-class vectors.
100    ///
101    /// Returns (fixed_timings, random_timings).
102    pub fn split_by_class(&self) -> (Vec<f64>, Vec<f64>) {
103        let mut fixed = Vec::with_capacity(self.samples.len() / 2);
104        let mut random = Vec::with_capacity(self.samples.len() / 2);
105
106        for &(class, timing) in &self.samples {
107            match class {
108                SampleClass::Fixed => fixed.push(timing),
109                SampleClass::Random => random.push(timing),
110            }
111        }
112
113        (fixed, random)
114    }
115
116    /// Get an iterator over all timings (ignoring class labels).
117    ///
118    /// Used for ACF computation on the pooled stream.
119    pub fn timings(&self) -> impl Iterator<Item = f64> + '_ {
120        self.samples.iter().map(|&(_, t)| t)
121    }
122
123    /// Get a slice of the raw samples.
124    pub fn as_slice(&self) -> &[(SampleClass, f64)] {
125        &self.samples
126    }
127
128    /// Get mutable access to the raw samples.
129    pub fn as_mut_slice(&mut self) -> &mut [(SampleClass, f64)] {
130        &mut self.samples
131    }
132
133    /// Get an iterator over (class, timing) pairs.
134    pub fn iter(&self) -> impl Iterator<Item = &(SampleClass, f64)> {
135        self.samples.iter()
136    }
137
138    /// Clear the stream, removing all samples but keeping capacity.
139    pub fn clear(&mut self) {
140        self.samples.clear();
141    }
142
143    /// Truncate the stream to the given length.
144    pub fn truncate(&mut self, len: usize) {
145        self.samples.truncate(len);
146    }
147
148    /// Get the sample at the given index.
149    #[inline]
150    pub fn get(&self, index: usize) -> Option<&(SampleClass, f64)> {
151        self.samples.get(index)
152    }
153
154    /// Convert raw u64 measurements to nanoseconds and store as stream.
155    ///
156    /// Interleaves the measurements: baseline[0], sample[0], baseline[1], sample[1], ...
157    pub fn from_raw_interleaved(baseline: &[u64], sample: &[u64], ns_per_tick: f64) -> Self {
158        debug_assert_eq!(
159            baseline.len(),
160            sample.len(),
161            "Baseline and sample must have same length"
162        );
163
164        let mut stream = Self::with_capacity(baseline.len() + sample.len());
165        for (b, s) in baseline.iter().zip(sample.iter()) {
166            stream.push(SampleClass::Fixed, *b as f64 * ns_per_tick);
167            stream.push(SampleClass::Random, *s as f64 * ns_per_tick);
168        }
169        stream
170    }
171
172    /// Convert to Vec<TimingSample> for bootstrap functions.
173    ///
174    /// This is an adapter method that converts the acquisition stream to the
175    /// `TimingSample` format used by the bootstrap covariance estimation functions.
176    ///
177    /// Maps `SampleClass::Fixed` → `Class::Baseline`, `SampleClass::Random` → `Class::Sample`.
178    pub fn to_timing_samples(&self) -> Vec<TimingSample> {
179        self.samples
180            .iter()
181            .map(|&(class, time_ns)| TimingSample {
182                time_ns,
183                class: match class {
184                    SampleClass::Fixed => Class::Baseline,
185                    SampleClass::Random => Class::Sample,
186                },
187            })
188            .collect()
189    }
190}
191
192#[cfg(test)]
193mod tests {
194    use super::*;
195
196    #[test]
197    fn test_empty_stream() {
198        let stream = AcquisitionStream::new();
199        assert!(stream.is_empty());
200        assert_eq!(stream.len(), 0);
201        assert_eq!(stream.n_per_class(), 0);
202    }
203
204    #[test]
205    fn test_push_and_split() {
206        let mut stream = AcquisitionStream::new();
207
208        // Push interleaved samples
209        stream.push(SampleClass::Fixed, 100.0);
210        stream.push(SampleClass::Random, 105.0);
211        stream.push(SampleClass::Fixed, 101.0);
212        stream.push(SampleClass::Random, 106.0);
213
214        assert_eq!(stream.len(), 4);
215        assert_eq!(stream.n_per_class(), 2);
216
217        let (fixed, random) = stream.split_by_class();
218        assert_eq!(fixed, vec![100.0, 101.0]);
219        assert_eq!(random, vec![105.0, 106.0]);
220    }
221
222    #[test]
223    fn test_push_batch_interleaved() {
224        let mut stream = AcquisitionStream::new();
225
226        let fixed = vec![100.0, 101.0, 102.0];
227        let random = vec![200.0, 201.0, 202.0];
228        stream.push_batch_interleaved(&fixed, &random);
229
230        assert_eq!(stream.len(), 6);
231
232        // Check interleaving order
233        assert_eq!(stream.samples[0], (SampleClass::Fixed, 100.0));
234        assert_eq!(stream.samples[1], (SampleClass::Random, 200.0));
235        assert_eq!(stream.samples[2], (SampleClass::Fixed, 101.0));
236        assert_eq!(stream.samples[3], (SampleClass::Random, 201.0));
237    }
238
239    #[test]
240    fn test_timings_iterator() {
241        let mut stream = AcquisitionStream::new();
242        stream.push(SampleClass::Fixed, 1.0);
243        stream.push(SampleClass::Random, 2.0);
244        stream.push(SampleClass::Fixed, 3.0);
245
246        let timings: Vec<f64> = stream.timings().collect();
247        assert_eq!(timings, vec![1.0, 2.0, 3.0]);
248    }
249
250    #[test]
251    fn test_from_raw_interleaved() {
252        let baseline = vec![100u64, 110, 120];
253        let sample = vec![200u64, 210, 220];
254        let ns_per_tick = 2.0;
255
256        let stream = AcquisitionStream::from_raw_interleaved(&baseline, &sample, ns_per_tick);
257
258        assert_eq!(stream.len(), 6);
259        assert_eq!(stream.n_per_class(), 3);
260
261        let (fixed, random) = stream.split_by_class();
262        assert_eq!(fixed, vec![200.0, 220.0, 240.0]); // 100*2, 110*2, 120*2
263        assert_eq!(random, vec![400.0, 420.0, 440.0]); // 200*2, 210*2, 220*2
264    }
265
266    #[test]
267    fn test_clear_and_truncate() {
268        let mut stream = AcquisitionStream::new();
269        stream.push(SampleClass::Fixed, 1.0);
270        stream.push(SampleClass::Random, 2.0);
271        stream.push(SampleClass::Fixed, 3.0);
272        stream.push(SampleClass::Random, 4.0);
273
274        stream.truncate(2);
275        assert_eq!(stream.len(), 2);
276
277        stream.clear();
278        assert!(stream.is_empty());
279    }
280}