1use 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
18pub type ChunkData<'a> = (usize, &'a Array2<Float>, &'a Array1<Float>);
20pub type ChunkResult<'a> = Result<ChunkData<'a>>;
22
23#[derive(Debug, Clone)]
25pub struct ChunkedProcessingConfig {
26 pub max_chunk_size: usize,
28 pub max_memory_mb: usize,
30 pub cache_chunks: usize,
32 pub temp_dir: Option<PathBuf>,
34 pub chunk_overlap: usize,
36 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, cache_chunks: 3,
46 temp_dir: None,
47 chunk_overlap: 100,
48 compression_level: 6,
49 }
50 }
51}
52
53pub 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#[derive(Debug, Clone)]
65struct DataChunk {
66 id: usize,
68 start_idx: usize,
70 end_idx: usize,
72 #[allow(dead_code)] n_samples: usize,
75 file_path: Option<PathBuf>,
77 #[allow(dead_code)] in_memory: bool,
80}
81
82#[derive(Debug, Clone)]
84struct CachedChunk {
85 x: Array2<Float>,
86 y: Array1<Float>,
87 last_accessed: std::time::Instant,
88}
89
90impl ChunkedDataset {
91 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 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 dataset.store_chunks_to_disk(x, y)?;
139
140 Ok(dataset)
141 }
142
143 pub fn from_files(_data_files: Vec<PathBuf>, _config: ChunkedProcessingConfig) -> Result<Self> {
145 Err(SklearsError::InvalidInput(
148 "File-based chunked loading not yet implemented".to_string(),
149 ))
150 }
151
152 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 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 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 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 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 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 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 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 let mut x = Array2::zeros((n_rows, n_cols));
243 let mut value_bytes = [0u8; 8]; 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 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 pub fn get_chunk(&mut self, chunk_id: usize) -> Result<(&Array2<Float>, &Array1<Float>)> {
267 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 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 self.add_to_cache(chunk_id, x, y);
296
297 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 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 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 pub fn chunk_iter(&mut self) -> ChunkIterator<'_> {
323 ChunkIterator {
324 dataset: self,
325 current_chunk: 0,
326 }
327 }
328
329 pub fn n_chunks(&self) -> usize {
331 self.chunks.len()
332 }
333
334 pub fn n_samples(&self) -> usize {
336 self.total_samples
337 }
338
339 pub fn n_features(&self) -> usize {
341 self.n_features
342 }
343
344 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 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 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 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
406pub 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 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#[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
448pub struct ChunkedSvmTrainer<K: Kernel> {
450 kernel: K,
451 #[allow(dead_code)] config: ChunkedProcessingConfig,
453 dataset: Option<ChunkedDataset>,
454}
455
456impl<K: Kernel> ChunkedSvmTrainer<K> {
457 pub fn new(kernel: K, config: ChunkedProcessingConfig) -> Self {
459 Self {
460 kernel,
461 config,
462 dataset: None,
463 }
464 }
465
466 pub fn set_dataset(&mut self, dataset: ChunkedDataset) {
468 self.dataset = Some(dataset);
469 }
470
471 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 for chunk_id in 0..dataset.n_chunks() {
491 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 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 #[allow(dead_code)] 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 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 for i in 0..n_samples {
573 let old_alpha = chunk_alpha[i];
574 let gradient_i = chunk_gradient[i];
575
576 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 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 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 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 #[allow(dead_code)] 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 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#[derive(Debug)]
639struct ChunkUpdateResult {
640 n_updates: usize,
641 max_violation: Float,
642}
643
644#[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 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 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}