Skip to main content

tenshift_core/
transform.rs

1//! Transform  -  composable operations on samples.
2//!
3//! Transforms are the second community extension point. Built-in transforms
4//! cover the common cases (map, filter, shuffle, batch). Community transforms
5//! can add augmentation, normalization, tokenization, etc.
6//!
7//! # Stateless vs Stateful
8//!
9//! - **[`Transform`]**: Stateless transforms process samples independently.
10//!   They run in parallel across worker threads. Examples: `map`, `filter`, `flat_map`.
11//!
12//! - **[`StatefulTransform`]**: Stateful transforms maintain internal buffers
13//!   and require ordered processing. They run serially in the collector thread.
14//!   Examples: `shuffle`, `batch`.
15
16use crate::error::Error;
17use crate::sample::Sample;
18
19/// Result of applying a transform to a sample.
20#[non_exhaustive]
21pub enum TransformResult {
22    /// The transformed sample.
23    Sample(Sample),
24    /// Yield multiple samples from one input.
25    Samples(Vec<Sample>),
26    /// Skip this sample (e.g., filtered out).
27    Skip,
28    /// An error occurred during transformation.
29    Error(Error),
30}
31
32/// A stateless transform that operates on individual samples.
33///
34/// For stateful operations (shuffle, batch), use [`StatefulTransform`].
35///
36/// # Implementing a Transform
37///
38/// ```rust
39/// use tenshift_core::transform::{Transform, TransformResult};
40/// use tenshift_core::sample::Sample;
41///
42/// struct Normalize {
43///     mean: f32,
44///     std: f32,
45/// }
46///
47/// impl Transform for Normalize {
48///     fn apply(&self, sample: Sample) -> TransformResult {
49///         // Normalize a tensor field in-place
50///         TransformResult::Sample(sample)
51///     }
52///
53///     fn name(&self) -> &str {
54///         "normalize"
55///     }
56/// }
57/// ```
58pub trait Transform: Send + Sync {
59    /// Apply this transform to a sample.
60    fn apply(&self, sample: Sample) -> TransformResult;
61
62    /// Human-readable name for logging.
63    fn name(&self) -> &str;
64}
65
66/// A stateful transform that can buffer, reorder, or group samples.
67///
68/// Unlike [`Transform`], stateful transforms receive samples one at a time
69/// via [`push`](StatefulTransform::push) and yield zero or more samples via
70/// [`finish`](StatefulTransform::finish) or internal buffering.
71pub trait StatefulTransform: Send {
72    /// Push a sample into this transform. Returns any immediately available outputs.
73    fn push(&mut self, sample: Sample) -> Vec<Sample>;
74
75    /// Signal that no more input is coming. Returns any buffered samples.
76    fn finish(&mut self) -> Vec<Sample>;
77
78    /// Human-readable name for logging.
79    fn name(&self) -> &str;
80}
81
82/// Map transform  -  apply a function to each sample.
83pub struct MapTransform<F> {
84    func: F,
85}
86
87impl<F> MapTransform<F>
88where
89    F: Fn(Sample) -> crate::error::Result<Sample> + Send + Sync,
90{
91    /// Create a new map transform.
92    pub fn new(func: F) -> Self {
93        Self { func }
94    }
95}
96
97impl<F> Transform for MapTransform<F>
98where
99    F: Fn(Sample) -> crate::error::Result<Sample> + Send + Sync,
100{
101    fn apply(&self, sample: Sample) -> TransformResult {
102        match (self.func)(sample) {
103            Ok(s) => TransformResult::Sample(s),
104            Err(e) => TransformResult::Error(e),
105        }
106    }
107
108    #[allow(clippy::needless_borrows_for_generic_args)]
109    fn name(&self) -> &str {
110        "map"
111    }
112}
113
114/// `FlatMap` transform  -  apply a function that yields zero or more samples.
115pub struct FlatMapTransform<F> {
116    func: F,
117}
118
119impl<F> FlatMapTransform<F>
120where
121    F: Fn(Sample) -> crate::error::Result<Vec<Sample>> + Send + Sync,
122{
123    /// Create a new `flat_map` transform.
124    pub fn new(func: F) -> Self {
125        Self { func }
126    }
127}
128
129impl<F> Transform for FlatMapTransform<F>
130where
131    F: Fn(Sample) -> crate::error::Result<Vec<Sample>> + Send + Sync,
132{
133    fn apply(&self, sample: Sample) -> TransformResult {
134        match (self.func)(sample) {
135            Ok(samples) => TransformResult::Samples(samples),
136            Err(e) => TransformResult::Error(e),
137        }
138    }
139
140    #[allow(clippy::needless_borrows_for_generic_args)]
141    fn name(&self) -> &str {
142        "flat_map"
143    }
144}
145
146/// Filter transform  -  keep only samples that match a predicate.
147pub struct FilterTransform<F> {
148    predicate: F,
149}
150
151impl<F> FilterTransform<F>
152where
153    F: Fn(&Sample) -> bool + Send + Sync,
154{
155    /// Create a new filter transform.
156    pub fn new(predicate: F) -> Self {
157        Self { predicate }
158    }
159}
160
161impl<F> Transform for FilterTransform<F>
162where
163    F: Fn(&Sample) -> bool + Send + Sync,
164{
165    fn apply(&self, sample: Sample) -> TransformResult {
166        if (self.predicate)(&sample) {
167            TransformResult::Sample(sample)
168        } else {
169            TransformResult::Skip
170        }
171    }
172
173    fn name(&self) -> &str {
174        "filter"
175    }
176}
177
178/// Shuffle transform  -  randomize sample order using a reservoir buffer.
179///
180/// Maintains a buffer of `capacity` samples. When the buffer is full,
181/// a random sample is evicted and yielded. This provides streaming
182/// approximate shuffling with bounded memory.
183pub struct ShuffleBuffer {
184    buffer: Vec<Sample>,
185    capacity: usize,
186    rng_state: u64,
187}
188
189impl ShuffleBuffer {
190    /// Create a new shuffle buffer with the given capacity.
191    pub fn new(capacity: usize, seed: Option<u64>) -> Self {
192        Self {
193            buffer: Vec::with_capacity(capacity),
194            capacity: capacity.max(1),
195            rng_state: seed.unwrap_or(crate::pipeline::DEFAULT_SHUFFLE_SEED),
196        }
197    }
198
199    /// Simple xorshift64 PRNG  -  fast, no dependencies.
200    fn next_rand(&mut self) -> u64 {
201        let mut x = self.rng_state;
202        x ^= x << 13;
203        x ^= x >> 7;
204        x ^= x << 17;
205        self.rng_state = x;
206        x
207    }
208}
209
210impl StatefulTransform for ShuffleBuffer {
211    fn push(&mut self, sample: Sample) -> Vec<Sample> {
212        if self.buffer.len() < self.capacity {
213            self.buffer.push(sample);
214            Vec::new()
215        } else {
216            // Buffer full  -  swap a random element out
217            #[allow(clippy::cast_possible_truncation)]
218            let idx = (self.next_rand() as usize) % self.buffer.len();
219            let evicted = std::mem::replace(&mut self.buffer[idx], sample);
220            vec![evicted]
221        }
222    }
223
224    fn finish(&mut self) -> Vec<Sample> {
225        // Drain remaining buffer in shuffled order
226        let mut remaining = std::mem::take(&mut self.buffer);
227        // Fisher-Yates shuffle on the remaining buffer
228        for i in (1..remaining.len()).rev() {
229            #[allow(clippy::cast_possible_truncation)]
230            let j = (self.next_rand() as usize) % (i + 1);
231            remaining.swap(i, j);
232        }
233        remaining
234    }
235
236    fn name(&self) -> &str {
237        "shuffle"
238    }
239}
240
241/// Batch transform  -  accumulate N samples into a group.
242pub struct BatchAccumulator {
243    batch_size: usize,
244    drop_last: bool,
245    buffer: Vec<Sample>,
246}
247
248impl BatchAccumulator {
249    /// Create a new batch accumulator.
250    pub fn new(batch_size: usize, drop_last: bool) -> Self {
251        Self {
252            batch_size: batch_size.max(1),
253            drop_last,
254            buffer: Vec::new(),
255        }
256    }
257}
258
259impl StatefulTransform for BatchAccumulator {
260    fn push(&mut self, sample: Sample) -> Vec<Sample> {
261        self.buffer.push(sample);
262        if self.buffer.len() >= self.batch_size {
263            std::mem::replace(&mut self.buffer, Vec::with_capacity(self.batch_size))
264        } else {
265            Vec::new()
266        }
267    }
268
269    fn finish(&mut self) -> Vec<Sample> {
270        if self.drop_last && self.buffer.len() < self.batch_size {
271            self.buffer.clear();
272            Vec::new()
273        } else {
274            std::mem::take(&mut self.buffer)
275        }
276    }
277
278    fn name(&self) -> &str {
279        "batch"
280    }
281}