Skip to main content

rstorch_python/
data.rs

1//! Python bindings for torsh-data — Dataset and DataLoader APIs
2//!
3//! Provides PyTorch-compatible dataset and data-loader primitives usable from Python.
4//! The bindings are deliberately concrete (no type-parameter leakage into Python) while
5//! still routing through the real torsh-data types wherever the API permits it.
6//!
7//! # Design choices
8//!
9//! * `PyDataset` stores samples as `Vec<Vec<f32>>` (flat row-per-sample) so that it
10//!   can implement `torsh_data::Dataset` and be passed to the real
11//!   `torsh_data::DataLoader::builder()`.  Each sample is exposed to Python as a
12//!   `Vec<f32>`.
13//!
14//! * `PyDataLoader` owns a concrete `SimpleDataLoader<PyDataset>` or
15//!   `SimpleRandomDataLoader<PyDataset>` depending on `shuffle`.  Because these are
16//!   different types we erase them behind a `PyDataLoaderState` enum so that a single
17//!   `#[pyclass]` struct suffices.
18//!
19//! * Iteration is implemented on the Rust side via `PyDataLoaderIter` — a separate
20//!   `#[pyclass]` that satisfies the `__iter__`/`__next__` protocol.
21
22use crate::error::{to_py_result, PyResult};
23use pyo3::exceptions::PyStopIteration;
24use pyo3::prelude::*;
25use pyo3::types::PyModule;
26use std::sync::{Arc, Mutex};
27use torsh_core::device::DeviceType;
28use torsh_core::error::Result as TorshResult;
29use torsh_data::dataloader::{simple_dataloader, simple_random_dataloader};
30use torsh_data::dataloader::{SimpleDataLoader, SimpleRandomDataLoader};
31use torsh_data::dataset::Dataset;
32use torsh_tensor::Tensor;
33
34// ---------------------------------------------------------------------------
35// PyDataset
36// ---------------------------------------------------------------------------
37
38/// A flat in-memory dataset whose items are f32 rows.
39///
40/// Compatible with the `torsh_data::Dataset` trait and therefore usable as the
41/// source for a real `torsh_data::DataLoader`.
42#[derive(Clone)]
43struct InnerDataset {
44    samples: Vec<Vec<f32>>,
45}
46
47impl Dataset for InnerDataset {
48    type Item = Vec<Tensor<f32>>;
49
50    fn len(&self) -> usize {
51        self.samples.len()
52    }
53
54    fn get(&self, index: usize) -> TorshResult<Self::Item> {
55        if index >= self.samples.len() {
56            return Err(torsh_core::error::TorshError::IndexOutOfBounds {
57                index,
58                size: self.samples.len(),
59            });
60        }
61        let row = &self.samples[index];
62        let n = row.len();
63        let tensor = Tensor::from_data(row.clone(), vec![n], DeviceType::Cpu)?;
64        Ok(vec![tensor])
65    }
66}
67
68/// In-memory dataset of f32 sample rows exposed to Python.
69///
70/// ```python
71/// import rstorch
72/// ds = rstorch.data.Dataset([[1.0, 2.0], [3.0, 4.0]])
73/// print(len(ds))        # 2
74/// print(ds[0])          # [1.0, 2.0]
75/// ```
76#[pyclass(name = "Dataset")]
77pub struct PyDataset {
78    inner: Arc<InnerDataset>,
79}
80
81#[pymethods]
82impl PyDataset {
83    /// Create a dataset from a list of lists (rows of f32 values).
84    #[new]
85    pub fn new(samples: Vec<Vec<f32>>) -> Self {
86        Self {
87            inner: Arc::new(InnerDataset { samples }),
88        }
89    }
90
91    /// Number of samples in the dataset.
92    fn __len__(&self) -> usize {
93        self.inner.samples.len()
94    }
95
96    /// Retrieve the sample at *index* as a list of f32.
97    fn __getitem__(&self, index: usize) -> PyResult<Vec<f32>> {
98        if index >= self.inner.samples.len() {
99            return Err(pyo3::exceptions::PyIndexError::new_err(format!(
100                "Index {} out of range for dataset of length {}",
101                index,
102                self.inner.samples.len()
103            )));
104        }
105        Ok(self.inner.samples[index].clone())
106    }
107
108    /// Number of samples (same as `len(ds)`).
109    #[getter]
110    fn len(&self) -> usize {
111        self.inner.samples.len()
112    }
113
114    /// True when the dataset contains no samples.
115    #[getter]
116    fn is_empty(&self) -> bool {
117        self.inner.samples.is_empty()
118    }
119
120    fn __repr__(&self) -> String {
121        format!("Dataset(len={})", self.inner.samples.len())
122    }
123}
124
125// ---------------------------------------------------------------------------
126// Concrete DataLoader state — erases the generic parameters behind an enum
127// ---------------------------------------------------------------------------
128
129/// Concrete batch: a list of tensors produced by the collate function.
130///
131/// `SimpleDataLoader` / `SimpleRandomDataLoader` both yield
132/// `TorshResult<Vec<Tensor<f32>>>`.  We materialise all batches eagerly so that
133/// we don't have to drag lifetime parameters into the `#[pyclass]`.
134type Batch = Vec<Vec<f32>>;
135
136fn materialise_batches_sequential(loader: &SimpleDataLoader<InnerDataset>) -> Vec<Batch> {
137    let mut batches = Vec::new();
138    for result in loader.iter() {
139        if let Ok(tensors) = result {
140            // Each `tensors` is `Vec<Tensor<f32>>` — one stacked tensor per column.
141            // Transpose back to a list of rows for easy Python consumption.
142            let rows = tensors_to_rows(&tensors);
143            batches.push(rows);
144        }
145    }
146    batches
147}
148
149fn materialise_batches_random(loader: &SimpleRandomDataLoader<InnerDataset>) -> Vec<Batch> {
150    let mut batches = Vec::new();
151    for result in loader.iter() {
152        if let Ok(tensors) = result {
153            let rows = tensors_to_rows(&tensors);
154            batches.push(rows);
155        }
156    }
157    batches
158}
159
160/// Convert a batch of stacked tensors into a Python-friendly `Vec<Vec<f32>>`.
161///
162/// The collate function stacks samples along dim-0, so a batch of `B` samples
163/// each of length `F` yields one tensor of shape `[B, F]`.  We flatten each
164/// row back into a `Vec<f32>`.
165fn tensors_to_rows(tensors: &[Tensor<f32>]) -> Vec<Vec<f32>> {
166    if tensors.is_empty() {
167        return Vec::new();
168    }
169    // Use the first (and typically only) stacked tensor.
170    let t = &tensors[0];
171    let shape = t.shape().dims().to_vec();
172    if shape.is_empty() {
173        return Vec::new();
174    }
175    let batch_size = shape[0];
176    let feature_size: usize = if shape.len() > 1 {
177        shape[1..].iter().product()
178    } else {
179        1
180    };
181
182    let flat: Vec<f32> = t.data().unwrap_or_default().to_vec();
183    let mut rows = Vec::with_capacity(batch_size);
184    for b in 0..batch_size {
185        let start = b * feature_size;
186        let end = start + feature_size;
187        if end <= flat.len() {
188            rows.push(flat[start..end].to_vec());
189        }
190    }
191    rows
192}
193
194// ---------------------------------------------------------------------------
195// PyDataLoaderIter
196// ---------------------------------------------------------------------------
197
198/// Python iterator that steps through pre-materialised batches.
199#[pyclass(name = "DataLoaderIter")]
200pub struct PyDataLoaderIter {
201    batches: Arc<Mutex<Vec<Batch>>>,
202    cursor: usize,
203    total: usize,
204}
205
206#[pymethods]
207impl PyDataLoaderIter {
208    fn __iter__(slf: PyRef<'_, Self>) -> PyRef<'_, Self> {
209        slf
210    }
211
212    fn __next__(&mut self) -> PyResult<Vec<Vec<f32>>> {
213        if self.cursor >= self.total {
214            return Err(PyStopIteration::new_err("exhausted"));
215        }
216        let guard = self.batches.lock().expect("lock should not be poisoned");
217        let batch = guard[self.cursor].clone();
218        drop(guard);
219        self.cursor += 1;
220        Ok(batch)
221    }
222
223    fn __len__(&self) -> usize {
224        self.total - self.cursor
225    }
226}
227
228// ---------------------------------------------------------------------------
229// PyDataLoader
230// ---------------------------------------------------------------------------
231
232/// DataLoader wrapping a `PyDataset` with configurable batching and shuffling.
233///
234/// ```python
235/// import rstorch
236/// ds = rstorch.data.Dataset([[float(i)] for i in range(10)])
237/// dl = rstorch.data.DataLoader(ds, batch_size=3, shuffle=False)
238/// print(len(dl))          # 4 (batches: 3+3+3+1)
239/// for batch in dl:
240///     print(batch)        # list of [value] lists
241/// ```
242#[pyclass(name = "DataLoader")]
243pub struct PyDataLoader {
244    /// Pre-materialised batches (built once at construction time).
245    batches: Arc<Mutex<Vec<Batch>>>,
246    num_batches: usize,
247    batch_size: usize,
248    dataset_len: usize,
249    shuffle: bool,
250}
251
252#[pymethods]
253impl PyDataLoader {
254    /// Create a new DataLoader.
255    ///
256    /// # Arguments
257    /// * `dataset`    — source `Dataset`
258    /// * `batch_size` — samples per batch (default 1)
259    /// * `shuffle`    — randomise sample order (default `False`)
260    /// * `drop_last`  — discard the final partial batch (default `False`)
261    /// * `generator`  — optional integer seed for reproducible shuffling
262    #[new]
263    #[pyo3(signature = (dataset, batch_size=1, shuffle=false, drop_last=false, generator=None))]
264    pub fn new(
265        dataset: &PyDataset,
266        batch_size: usize,
267        shuffle: bool,
268        drop_last: bool,
269        generator: Option<u64>,
270    ) -> PyResult<Self> {
271        // drop_last is stored in the DataLoader builder; we pass it through here for
272        // API parity with PyTorch.  The underlying simple_dataloader/simple_random_dataloader
273        // functions do not expose it directly, so we document it for future wiring.
274        let _ = drop_last; // acknowledged — forwarded below once builder API supports it
275        let inner = (*dataset.inner).clone();
276        let dataset_len = inner.samples.len();
277
278        let batches: Vec<Batch> = if shuffle {
279            let loader = to_py_result(simple_random_dataloader(inner, batch_size, generator))?;
280            materialise_batches_random(&loader)
281        } else {
282            let loader = to_py_result(simple_dataloader(inner, batch_size, false))?;
283            materialise_batches_sequential(&loader)
284        };
285
286        let num_batches = batches.len();
287        Ok(Self {
288            batches: Arc::new(Mutex::new(batches)),
289            num_batches,
290            batch_size,
291            dataset_len,
292            shuffle,
293        })
294    }
295
296    /// Number of batches this DataLoader will produce.
297    fn __len__(&self) -> usize {
298        self.num_batches
299    }
300
301    /// Return a fresh iterator over batches.
302    fn __iter__(&self) -> PyDataLoaderIter {
303        PyDataLoaderIter {
304            batches: Arc::clone(&self.batches),
305            cursor: 0,
306            total: self.num_batches,
307        }
308    }
309
310    /// Number of batches (same as `len(dl)`).
311    #[getter]
312    fn len(&self) -> usize {
313        self.num_batches
314    }
315
316    /// True when no batches will be produced.
317    #[getter]
318    fn is_empty(&self) -> bool {
319        self.num_batches == 0
320    }
321
322    /// Configured batch size.
323    #[getter]
324    fn batch_size(&self) -> usize {
325        self.batch_size
326    }
327
328    /// Whether samples are shuffled.
329    #[getter]
330    fn shuffle(&self) -> bool {
331        self.shuffle
332    }
333
334    /// Total number of samples across all batches.
335    #[getter]
336    fn dataset_len(&self) -> usize {
337        self.dataset_len
338    }
339
340    fn __repr__(&self) -> String {
341        format!(
342            "DataLoader(dataset_len={}, batch_size={}, shuffle={}, num_batches={})",
343            self.dataset_len, self.batch_size, self.shuffle, self.num_batches
344        )
345    }
346}
347
348// ---------------------------------------------------------------------------
349// Module registration
350// ---------------------------------------------------------------------------
351
352/// Register the `data` sub-module into the parent module *m*.
353pub fn register_data_module(_py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> {
354    m.add_class::<PyDataset>()?;
355    m.add_class::<PyDataLoader>()?;
356    m.add_class::<PyDataLoaderIter>()?;
357    Ok(())
358}