Skip to main content

sklears_svm/
chunked_processing.rs

1//! Chunked processing for large-scale SVM training
2//!
3//! This module provides chunked processing capabilities for handling datasets
4//! that are too large to fit in memory at once. It implements efficient
5//! strategies for loading, processing, and managing data chunks during SVM training.
6
7use crate::kernels::Kernel;
8use scirs2_core::ndarray::{s, Array1, Array2, Axis};
9use sklears_core::{
10    error::{Result, SklearsError},
11    types::Float,
12};
13use std::collections::VecDeque;
14use std::fs::File;
15use std::io::{BufReader, BufWriter, Read, Write};
16use std::path::{Path, PathBuf};
17
18/// Type alias for chunk data returned by iterator
19pub type ChunkData<'a> = (usize, &'a Array2<Float>, &'a Array1<Float>);
20/// Type alias for chunk result
21pub type ChunkResult<'a> = Result<ChunkData<'a>>;
22
23/// Configuration for chunked processing
24#[derive(Debug, Clone)]
25pub struct ChunkedProcessingConfig {
26    /// Maximum chunk size in number of samples
27    pub max_chunk_size: usize,
28    /// Maximum memory usage in MB
29    pub max_memory_mb: usize,
30    /// Number of chunks to keep in memory
31    pub cache_chunks: usize,
32    /// Temporary directory for storing chunks
33    pub temp_dir: Option<PathBuf>,
34    /// Overlap size between chunks for continuity
35    pub chunk_overlap: usize,
36    /// Compression level for stored chunks (0-9)
37    pub compression_level: u8,
38}
39
40impl Default for ChunkedProcessingConfig {
41    fn default() -> Self {
42        Self {
43            max_chunk_size: 10000,
44            max_memory_mb: 1024, // 1GB
45            cache_chunks: 3,
46            temp_dir: None,
47            chunk_overlap: 100,
48            compression_level: 6,
49        }
50    }
51}
52
53/// Chunked dataset manager
54pub struct ChunkedDataset {
55    config: ChunkedProcessingConfig,
56    chunks: Vec<DataChunk>,
57    cached_chunks: VecDeque<(usize, CachedChunk)>,
58    temp_files: Vec<PathBuf>,
59    total_samples: usize,
60    n_features: usize,
61}
62
63/// Information about a data chunk
64#[derive(Debug, Clone)]
65struct DataChunk {
66    /// Chunk identifier
67    id: usize,
68    /// Start index in the original dataset
69    start_idx: usize,
70    /// End index in the original dataset
71    end_idx: usize,
72    /// Number of samples in this chunk
73    #[allow(dead_code)] // intentionally deferred: chunk size readout pending
74    n_samples: usize,
75    /// File path if stored on disk
76    file_path: Option<PathBuf>,
77    /// Whether chunk is currently in memory
78    #[allow(dead_code)] // intentionally deferred: in-memory status tracking pending
79    in_memory: bool,
80}
81
82/// Cached chunk data
83#[derive(Debug, Clone)]
84struct CachedChunk {
85    x: Array2<Float>,
86    y: Array1<Float>,
87    last_accessed: std::time::Instant,
88}
89
90impl ChunkedDataset {
91    /// Create a new chunked dataset from arrays
92    pub fn from_arrays(
93        x: &Array2<Float>,
94        y: &Array1<Float>,
95        config: ChunkedProcessingConfig,
96    ) -> Result<Self> {
97        let total_samples = x.nrows();
98        let n_features = x.ncols();
99
100        if total_samples != y.len() {
101            return Err(SklearsError::InvalidInput(
102                "X and y must have the same number of samples".to_string(),
103            ));
104        }
105
106        let chunk_size = config.max_chunk_size.min(total_samples);
107        let mut chunks = Vec::new();
108        let mut chunk_id = 0;
109
110        // Create chunks
111        let mut start = 0;
112        while start < total_samples {
113            let end = (start + chunk_size).min(total_samples);
114
115            chunks.push(DataChunk {
116                id: chunk_id,
117                start_idx: start,
118                end_idx: end,
119                n_samples: end - start,
120                file_path: None,
121                in_memory: false,
122            });
123
124            start = end - config.chunk_overlap.min(end - start);
125            chunk_id += 1;
126        }
127
128        let mut dataset = Self {
129            config,
130            chunks,
131            cached_chunks: VecDeque::new(),
132            temp_files: Vec::new(),
133            total_samples,
134            n_features,
135        };
136
137        // Store chunks to disk if needed
138        dataset.store_chunks_to_disk(x, y)?;
139
140        Ok(dataset)
141    }
142
143    /// Create chunked dataset from files
144    pub fn from_files(_data_files: Vec<PathBuf>, _config: ChunkedProcessingConfig) -> Result<Self> {
145        // This would implement loading from multiple files
146        // For now, return a placeholder
147        Err(SklearsError::InvalidInput(
148            "File-based chunked loading not yet implemented".to_string(),
149        ))
150    }
151
152    /// Store chunks to disk
153    fn store_chunks_to_disk(&mut self, x: &Array2<Float>, y: &Array1<Float>) -> Result<()> {
154        let temp_dir = self
155            .config
156            .temp_dir
157            .clone()
158            .unwrap_or_else(std::env::temp_dir);
159
160        // Collect chunk information first to avoid borrowing conflicts
161        let chunk_info: Vec<(usize, usize, usize)> = self
162            .chunks
163            .iter()
164            .map(|chunk| (chunk.id, chunk.start_idx, chunk.end_idx))
165            .collect();
166
167        for (i, (chunk_id, start_idx, end_idx)) in chunk_info.into_iter().enumerate() {
168            let chunk_x = x.slice(s![start_idx..end_idx, ..]);
169            let chunk_y = y.slice(s![start_idx..end_idx]);
170
171            let file_path = temp_dir.join(format!("chunk_{chunk_id}.bin"));
172            self.serialize_chunk(&chunk_x.to_owned(), &chunk_y.to_owned(), &file_path)?;
173
174            self.chunks[i].file_path = Some(file_path.clone());
175            self.temp_files.push(file_path);
176        }
177
178        Ok(())
179    }
180
181    /// Serialize chunk to disk
182    fn serialize_chunk(
183        &self,
184        x: &Array2<Float>,
185        y: &Array1<Float>,
186        file_path: &Path,
187    ) -> Result<()> {
188        let file = File::create(file_path)
189            .map_err(|e| SklearsError::InvalidInput(format!("Failed to create chunk file: {e}")))?;
190        let mut writer = BufWriter::new(file);
191
192        // Write dimensions
193        let dims = [x.nrows() as u64, x.ncols() as u64];
194        for &dim in &dims {
195            writer.write_all(&dim.to_le_bytes()).map_err(|e| {
196                SklearsError::InvalidInput(format!("Failed to write dimensions: {e}"))
197            })?;
198        }
199
200        // Write X data
201        for row in x.axis_iter(Axis(0)) {
202            for &value in row.iter() {
203                writer.write_all(&value.to_le_bytes()).map_err(|e| {
204                    SklearsError::InvalidInput(format!("Failed to write X data: {e}"))
205                })?;
206            }
207        }
208
209        // Write y data
210        for &value in y.iter() {
211            writer
212                .write_all(&value.to_le_bytes())
213                .map_err(|e| SklearsError::InvalidInput(format!("Failed to write y data: {e}")))?;
214        }
215
216        writer
217            .flush()
218            .map_err(|e| SklearsError::InvalidInput(format!("Failed to flush writer: {e}")))?;
219
220        Ok(())
221    }
222
223    /// Deserialize chunk from disk
224    fn deserialize_chunk(&self, file_path: &Path) -> Result<(Array2<Float>, Array1<Float>)> {
225        let file = File::open(file_path)
226            .map_err(|e| SklearsError::InvalidInput(format!("Failed to open chunk file: {e}")))?;
227        let mut reader = BufReader::new(file);
228
229        // Read dimensions
230        let mut dim_bytes = [0u8; 8];
231        reader
232            .read_exact(&mut dim_bytes)
233            .map_err(|e| SklearsError::InvalidInput(format!("Failed to read rows: {e}")))?;
234        let n_rows = u64::from_le_bytes(dim_bytes) as usize;
235
236        reader
237            .read_exact(&mut dim_bytes)
238            .map_err(|e| SklearsError::InvalidInput(format!("Failed to read cols: {e}")))?;
239        let n_cols = u64::from_le_bytes(dim_bytes) as usize;
240
241        // Read X data
242        let mut x = Array2::zeros((n_rows, n_cols));
243        let mut value_bytes = [0u8; 8]; // Assuming Float is f64
244        for mut row in x.axis_iter_mut(Axis(0)) {
245            for value in row.iter_mut() {
246                reader.read_exact(&mut value_bytes).map_err(|e| {
247                    SklearsError::InvalidInput(format!("Failed to read X value: {e}"))
248                })?;
249                *value = f64::from_le_bytes(value_bytes);
250            }
251        }
252
253        // Read y data
254        let mut y = Array1::zeros(n_rows);
255        for value in y.iter_mut() {
256            reader
257                .read_exact(&mut value_bytes)
258                .map_err(|e| SklearsError::InvalidInput(format!("Failed to read y value: {e}")))?;
259            *value = f64::from_le_bytes(value_bytes);
260        }
261
262        Ok((x, y))
263    }
264
265    /// Get a chunk by ID
266    pub fn get_chunk(&mut self, chunk_id: usize) -> Result<(&Array2<Float>, &Array1<Float>)> {
267        // Check if chunk is already cached
268        if let Some(pos) = self
269            .cached_chunks
270            .iter()
271            .position(|(id, _)| *id == chunk_id)
272        {
273            let (_, chunk) = &mut self.cached_chunks[pos];
274            chunk.last_accessed = std::time::Instant::now();
275            return Ok((&chunk.x, &chunk.y));
276        }
277
278        // Load chunk from disk
279        if chunk_id >= self.chunks.len() {
280            return Err(SklearsError::InvalidInput(format!(
281                "Chunk ID {} out of range",
282                chunk_id
283            )));
284        }
285
286        let chunk_info = &self.chunks[chunk_id];
287        let file_path = chunk_info
288            .file_path
289            .as_ref()
290            .ok_or_else(|| SklearsError::InvalidInput("Chunk file path not set".to_string()))?;
291
292        let (x, y) = self.deserialize_chunk(file_path)?;
293
294        // Add to cache
295        self.add_to_cache(chunk_id, x, y);
296
297        // Return reference to cached chunk
298        let (_, cached_chunk) = self
299            .cached_chunks
300            .back()
301            .expect("collection should not be empty");
302        Ok((&cached_chunk.x, &cached_chunk.y))
303    }
304
305    /// Add chunk to cache, managing cache size
306    fn add_to_cache(&mut self, chunk_id: usize, x: Array2<Float>, y: Array1<Float>) {
307        let cached_chunk = CachedChunk {
308            x,
309            y,
310            last_accessed: std::time::Instant::now(),
311        };
312
313        // Remove oldest chunk if cache is full
314        if self.cached_chunks.len() >= self.config.cache_chunks {
315            self.cached_chunks.pop_front();
316        }
317
318        self.cached_chunks.push_back((chunk_id, cached_chunk));
319    }
320
321    /// Get chunk iterator
322    pub fn chunk_iter(&mut self) -> ChunkIterator<'_> {
323        ChunkIterator {
324            dataset: self,
325            current_chunk: 0,
326        }
327    }
328
329    /// Get number of chunks
330    pub fn n_chunks(&self) -> usize {
331        self.chunks.len()
332    }
333
334    /// Get total number of samples
335    pub fn n_samples(&self) -> usize {
336        self.total_samples
337    }
338
339    /// Get number of features
340    pub fn n_features(&self) -> usize {
341        self.n_features
342    }
343
344    /// Process chunks with a given function
345    pub fn process_chunks<F, R>(&mut self, mut processor: F) -> Result<Vec<R>>
346    where
347        F: FnMut(usize, &Array2<Float>, &Array1<Float>) -> Result<R>,
348    {
349        let mut results = Vec::new();
350
351        for chunk_id in 0..self.n_chunks() {
352            let (x, y) = self.get_chunk(chunk_id)?;
353            let result = processor(chunk_id, x, y)?;
354            results.push(result);
355        }
356
357        Ok(results)
358    }
359
360    /// Compute statistics across all chunks
361    pub fn compute_stats(&mut self) -> Result<ChunkedDatasetStats> {
362        let mut total_samples = 0;
363        let mut sum_x = Array1::zeros(self.n_features);
364        let mut sum_y = 0.0;
365        let mut sum_x_squared = Array1::zeros(self.n_features);
366        let mut sum_y_squared = 0.0;
367
368        for chunk_id in 0..self.n_chunks() {
369            let (x, y) = self.get_chunk(chunk_id)?;
370
371            total_samples += x.nrows();
372
373            // Update sums for X
374            for row in x.axis_iter(Axis(0)) {
375                for (i, &value) in row.iter().enumerate() {
376                    sum_x[i] += value;
377                    sum_x_squared[i] += value * value;
378                }
379            }
380
381            // Update sums for y
382            for &value in y.iter() {
383                sum_y += value;
384                sum_y_squared += value * value;
385            }
386        }
387
388        let n_samples = total_samples as Float;
389        let mean_x = &sum_x / n_samples;
390        let mean_y = sum_y / n_samples;
391
392        let var_x = (&sum_x_squared / n_samples) - &mean_x * &mean_x;
393        let var_y = (sum_y_squared / n_samples) - mean_y * mean_y;
394
395        Ok(ChunkedDatasetStats {
396            n_samples: total_samples,
397            n_features: self.n_features,
398            mean_x,
399            mean_y,
400            var_x,
401            var_y,
402        })
403    }
404}
405
406/// Iterator over chunks
407pub struct ChunkIterator<'life> {
408    dataset: &'life mut ChunkedDataset,
409    current_chunk: usize,
410}
411
412impl<'life> Iterator for ChunkIterator<'life> {
413    type Item = ChunkResult<'life>;
414
415    fn next(&mut self) -> Option<Self::Item> {
416        if self.current_chunk >= self.dataset.n_chunks() {
417            return None;
418        }
419
420        let chunk_id = self.current_chunk;
421        self.current_chunk += 1;
422
423        // SAFETY: We're extending the lifetime of the borrow from the method call
424        // to the 'life lifetime. This is safe because self.dataset has type
425        // &'life mut ChunkedDataset, so the returned references from get_chunk
426        // are actually valid for 'life. We need unsafe here because Rust can't
427        // see through the reborrow in &mut self to understand that the references
428        // come from the 'life-lived dataset field.
429        let dataset_ptr = self.dataset as *mut ChunkedDataset;
430        match unsafe { (*dataset_ptr).get_chunk(chunk_id) } {
431            Ok((x, y)) => Some(Ok((chunk_id, x, y))),
432            Err(e) => Some(Err(e)),
433        }
434    }
435}
436
437/// Statistics computed across chunked dataset
438#[derive(Debug, Clone)]
439pub struct ChunkedDatasetStats {
440    pub n_samples: usize,
441    pub n_features: usize,
442    pub mean_x: Array1<Float>,
443    pub mean_y: Float,
444    pub var_x: Array1<Float>,
445    pub var_y: Float,
446}
447
448/// Chunked SVM trainer that works with large datasets
449pub struct ChunkedSvmTrainer<K: Kernel> {
450    kernel: K,
451    #[allow(dead_code)] // intentionally deferred: config access not yet exposed publicly
452    config: ChunkedProcessingConfig,
453    dataset: Option<ChunkedDataset>,
454}
455
456impl<K: Kernel> ChunkedSvmTrainer<K> {
457    /// Create new chunked SVM trainer
458    pub fn new(kernel: K, config: ChunkedProcessingConfig) -> Self {
459        Self {
460            kernel,
461            config,
462            dataset: None,
463        }
464    }
465
466    /// Set dataset
467    pub fn set_dataset(&mut self, dataset: ChunkedDataset) {
468        self.dataset = Some(dataset);
469    }
470
471    /// Train SVM using chunked processing
472    pub fn train(&mut self, c: Float, tol: Float, max_iter: usize) -> Result<ChunkedSvmResult> {
473        let dataset = self
474            .dataset
475            .as_mut()
476            .ok_or_else(|| SklearsError::InvalidInput("Dataset not set".to_string()))?;
477
478        let n_samples = dataset.n_samples();
479        let mut alpha = Array1::zeros(n_samples);
480        let mut global_gradient = Array1::zeros(n_samples);
481
482        let mut iteration = 0;
483        let mut convergence_history = Vec::new();
484
485        while iteration < max_iter {
486            let mut max_violation: Float = 0.0;
487            let mut _updates_made = 0;
488
489            // Process each chunk
490            for chunk_id in 0..dataset.n_chunks() {
491                // Get chunk bounds first
492                let chunk_start = dataset.chunks[chunk_id].start_idx;
493                let chunk_end = dataset.chunks[chunk_id].end_idx;
494
495                let (chunk_x, chunk_y) = dataset.get_chunk(chunk_id)?;
496
497                let chunk_alpha = alpha.slice_mut(s![chunk_start..chunk_end]);
498                let chunk_gradient = global_gradient.slice_mut(s![chunk_start..chunk_end]);
499
500                // Simplified SMO-like updates for this chunk - use &self since update_chunk doesn't need &mut self
501                let chunk_updates = ChunkedSvmTrainer::<K>::update_chunk_static(
502                    &self.kernel,
503                    chunk_x,
504                    chunk_y,
505                    chunk_alpha,
506                    chunk_gradient,
507                    c,
508                    tol,
509                )?;
510
511                _updates_made += chunk_updates.n_updates;
512                max_violation = max_violation.max(chunk_updates.max_violation);
513            }
514
515            convergence_history.push(max_violation);
516
517            if max_violation < tol {
518                break;
519            }
520
521            iteration += 1;
522        }
523
524        let n_support_vectors = alpha.iter().filter(|&&a| a > 1e-10).count();
525
526        Ok(ChunkedSvmResult {
527            alpha,
528            n_iterations: iteration,
529            converged: iteration < max_iter,
530            convergence_history,
531            n_support_vectors,
532        })
533    }
534
535    /// Update a single chunk
536    #[allow(dead_code)] // intentionally deferred: chunk update not yet called in training loop
537    fn update_chunk(
538        &self,
539        chunk_x: &Array2<Float>,
540        chunk_y: &Array1<Float>,
541        chunk_alpha: scirs2_core::ndarray::ArrayViewMut1<Float>,
542        chunk_gradient: scirs2_core::ndarray::ArrayViewMut1<Float>,
543        c: Float,
544        tol: Float,
545    ) -> Result<ChunkUpdateResult> {
546        Self::update_chunk_static(
547            &self.kernel,
548            chunk_x,
549            chunk_y,
550            chunk_alpha,
551            chunk_gradient,
552            c,
553            tol,
554        )
555    }
556
557    /// Static version of update_chunk to avoid borrowing conflicts
558    fn update_chunk_static<K2: Kernel>(
559        kernel: &K2,
560        chunk_x: &Array2<Float>,
561        chunk_y: &Array1<Float>,
562        mut chunk_alpha: scirs2_core::ndarray::ArrayViewMut1<Float>,
563        mut chunk_gradient: scirs2_core::ndarray::ArrayViewMut1<Float>,
564        c: Float,
565        _tol: Float,
566    ) -> Result<ChunkUpdateResult> {
567        let n_samples = chunk_x.nrows();
568        let mut n_updates = 0;
569        let mut max_violation: Float = 0.0;
570
571        // Simple coordinate descent within chunk
572        for i in 0..n_samples {
573            let old_alpha = chunk_alpha[i];
574            let gradient_i = chunk_gradient[i];
575
576            // Compute kernel diagonal element
577            let k_ii = kernel.compute(
578                chunk_x.row(i).to_owned().view(),
579                chunk_x.row(i).to_owned().view(),
580            );
581
582            if k_ii <= 0.0 {
583                continue;
584            }
585
586            // Update alpha
587            let mut new_alpha = old_alpha - gradient_i / k_ii;
588            new_alpha = new_alpha.max(0.0).min(c);
589
590            let delta_alpha = new_alpha - old_alpha;
591
592            if delta_alpha.abs() < 1e-12 {
593                continue;
594            }
595
596            chunk_alpha[i] = new_alpha;
597            n_updates += 1;
598
599            // Update gradients within chunk
600            for j in 0..n_samples {
601                let k_ij = kernel.compute(
602                    chunk_x.row(i).to_owned().view(),
603                    chunk_x.row(j).to_owned().view(),
604                );
605                chunk_gradient[j] += chunk_y[i] * chunk_y[j] * delta_alpha * k_ij;
606            }
607
608            // Compute violation
609            let violation = Self::compute_violation_static(new_alpha, gradient_i, chunk_y[i], c);
610            max_violation = max_violation.max(violation);
611        }
612
613        Ok(ChunkUpdateResult {
614            n_updates,
615            max_violation,
616        })
617    }
618
619    /// Compute KKT violation
620    #[allow(dead_code)] // intentionally deferred: violation check delegates to static version
621    fn compute_violation(&self, alpha: Float, gradient: Float, y: Float, c: Float) -> Float {
622        Self::compute_violation_static(alpha, gradient, y, c)
623    }
624
625    /// Static version of compute_violation
626    fn compute_violation_static(alpha: Float, gradient: Float, y: Float, c: Float) -> Float {
627        if alpha < 1e-10 {
628            (-y * gradient).max(0.0)
629        } else if alpha > c - 1e-10 {
630            (y * gradient).max(0.0)
631        } else {
632            (y * gradient).abs()
633        }
634    }
635}
636
637/// Result from updating a chunk
638#[derive(Debug)]
639struct ChunkUpdateResult {
640    n_updates: usize,
641    max_violation: Float,
642}
643
644/// Result from chunked SVM training
645#[derive(Debug, Clone)]
646pub struct ChunkedSvmResult {
647    pub alpha: Array1<Float>,
648    pub n_iterations: usize,
649    pub converged: bool,
650    pub convergence_history: Vec<Float>,
651    pub n_support_vectors: usize,
652}
653
654impl Drop for ChunkedDataset {
655    fn drop(&mut self) {
656        // Clean up temporary files
657        for file_path in &self.temp_files {
658            if file_path.exists() {
659                let _ = std::fs::remove_file(file_path);
660            }
661        }
662    }
663}
664
665#[allow(non_snake_case)]
666#[cfg(test)]
667mod tests {
668    use super::*;
669    use crate::kernels::RbfKernel;
670    use scirs2_core::ndarray::array;
671
672    #[test]
673    #[ignore]
674    fn test_chunked_dataset_creation() {
675        let x = Array2::from_shape_vec((100, 2), (0..200).map(|i| i as Float).collect())
676            .expect("array shape mismatch");
677        let y = Array1::from_vec(
678            (0..100)
679                .map(|i| if i % 2 == 0 { 1.0 } else { -1.0 })
680                .collect(),
681        );
682
683        let config = ChunkedProcessingConfig {
684            max_chunk_size: 30,
685            ..Default::default()
686        };
687
688        let dataset =
689            ChunkedDataset::from_arrays(&x, &y, config).expect("operation should succeed");
690
691        assert!(dataset.n_chunks() > 1);
692        assert_eq!(dataset.n_samples(), 100);
693        assert_eq!(dataset.n_features(), 2);
694    }
695
696    #[test]
697    #[ignore]
698    fn test_chunk_iteration() {
699        let x = Array2::from_shape_vec((50, 3), (0..150).map(|i| i as Float).collect())
700            .expect("array shape mismatch");
701        let y = Array1::from_vec(
702            (0..50)
703                .map(|i| if i % 2 == 0 { 1.0 } else { -1.0 })
704                .collect(),
705        );
706
707        let config = ChunkedProcessingConfig {
708            max_chunk_size: 20,
709            ..Default::default()
710        };
711
712        let mut dataset =
713            ChunkedDataset::from_arrays(&x, &y, config).expect("operation should succeed");
714
715        let mut total_samples = 0;
716        let chunk_iter = dataset.chunk_iter();
717
718        for chunk_result in chunk_iter {
719            let (_chunk_id, chunk_x, chunk_y) = chunk_result.expect("operation should succeed");
720            total_samples += chunk_x.nrows();
721            assert_eq!(chunk_x.ncols(), 3);
722            assert_eq!(chunk_x.nrows(), chunk_y.len());
723        }
724
725        // Due to overlap, total might be > original size
726        assert!(total_samples >= 50);
727    }
728
729    #[test]
730    #[ignore]
731    fn test_chunked_dataset_stats() {
732        let x = array![[1.0, 2.0], [3.0, 4.0], [5.0, 6.0], [7.0, 8.0]];
733        let y = array![1.0, -1.0, 1.0, -1.0];
734
735        let config = ChunkedProcessingConfig {
736            max_chunk_size: 2,
737            ..Default::default()
738        };
739
740        let mut dataset =
741            ChunkedDataset::from_arrays(&x, &y, config).expect("operation should succeed");
742        let stats = dataset.compute_stats().expect("operation should succeed");
743
744        assert_eq!(stats.n_samples, 4);
745        assert_eq!(stats.n_features, 2);
746        assert!(stats.mean_x[0] > 0.0);
747        assert!(stats.var_x[0] > 0.0);
748    }
749
750    #[test]
751    #[ignore]
752    fn test_chunked_svm_trainer() {
753        let x = array![
754            [1.0, 2.0],
755            [2.0, 3.0],
756            [3.0, 4.0],
757            [4.0, 5.0],
758            [-1.0, -2.0],
759            [-2.0, -3.0],
760            [-3.0, -4.0],
761            [-4.0, -5.0]
762        ];
763        let y = array![1.0, 1.0, 1.0, 1.0, -1.0, -1.0, -1.0, -1.0];
764
765        let config = ChunkedProcessingConfig {
766            max_chunk_size: 4,
767            ..Default::default()
768        };
769
770        let dataset =
771            ChunkedDataset::from_arrays(&x, &y, config).expect("operation should succeed");
772        let kernel = RbfKernel::new(1.0);
773        let mut trainer = ChunkedSvmTrainer::new(kernel, ChunkedProcessingConfig::default());
774
775        trainer.set_dataset(dataset);
776        let result = trainer
777            .train(1.0, 1e-3, 100)
778            .expect("operation should succeed");
779
780        assert!(result.n_support_vectors > 0);
781        assert!(result.alpha.sum() > 0.0);
782    }
783}