1use crate::error::{StatsError, StatsResult};
7use scirs2_core::ndarray::{s, Array1, Array2, ArrayBase, ArrayView1, ArrayView2, Data, Ix1, Ix2};
8use scirs2_core::numeric::{Float, NumCast};
9use std::collections::VecDeque;
10use std::sync::Arc;
11
12#[derive(Debug, Clone)]
14pub struct MemoryProfile {
15 pub peak_memory: usize,
17 pub avg_memory: usize,
19 pub allocations: usize,
21 pub deallocations: usize,
23 pub efficiency_score: f64,
25}
26
27pub struct MemoryAdaptiveAlgorithm {
29 available_memory: usize,
31 preferred_chunksize: usize,
33 #[allow(dead_code)]
35 prefer_inplace: bool,
36}
37
38impl Default for MemoryAdaptiveAlgorithm {
39 fn default() -> Self {
40 Self::new()
41 }
42}
43
44impl MemoryAdaptiveAlgorithm {
45 pub fn new() -> Self {
47 let available_memory = Self::estimate_available_memory();
49 let preferred_chunksize = Self::calculate_optimal_chunksize(available_memory);
50
51 Self {
52 available_memory,
53 preferred_chunksize,
54 prefer_inplace: available_memory < 1_000_000_000, }
56 }
57
58 fn estimate_available_memory() -> usize {
60 #[cfg(target_os = "linux")]
61 {
62 Self::get_available_memory_linux()
63 }
64 #[cfg(target_os = "windows")]
65 {
66 Self::get_available_memory_windows()
67 }
68 #[cfg(target_os = "macos")]
69 {
70 Self::get_available_memory_macos()
71 }
72 #[cfg(not(any(target_os = "linux", target_os = "windows", target_os = "macos")))]
73 {
74 Self::get_available_memory_fallback()
76 }
77 }
78
79 #[cfg(target_os = "linux")]
80 fn get_available_memory_linux() -> usize {
81 use std::fs;
82
83 if let Ok(meminfo) = fs::read_to_string("/proc/meminfo") {
85 let mut mem_available = None;
86 let mut mem_free = None;
87 let mut mem_total = None;
88
89 for line in meminfo.lines() {
90 if line.starts_with("MemAvailable:") {
91 if let Some(value) = line.split_whitespace().nth(1) {
92 if let Ok(kb) = value.parse::<usize>() {
93 mem_available = Some(kb * 1024); }
95 }
96 } else if line.starts_with("MemFree:") {
97 if let Some(value) = line.split_whitespace().nth(1) {
98 if let Ok(kb) = value.parse::<usize>() {
99 mem_free = Some(kb * 1024);
100 }
101 }
102 } else if line.starts_with("MemTotal:") {
103 if let Some(value) = line.split_whitespace().nth(1) {
104 if let Ok(kb) = value.parse::<usize>() {
105 mem_total = Some(kb * 1024);
106 }
107 }
108 }
109 }
110
111 if let Some(available) = mem_available {
113 return available;
114 } else if let Some(free) = mem_free {
115 return free;
116 } else if let Some(total) = mem_total {
117 return total / 2;
119 }
120 }
121
122 Self::get_available_memory_fallback()
124 }
125
126 #[cfg(target_os = "windows")]
127 fn get_available_memory_windows() -> usize {
128 let conservative_total = 4_000_000_000; conservative_total / 4
139 }
140
141 #[cfg(target_os = "macos")]
142 fn get_available_memory_macos() -> usize {
143 use std::process::Command;
144
145 if let Ok(output) = Command::new("vm_stat").output() {
147 if let Ok(stdout) = String::from_utf8(output.stdout) {
148 let mut pagesize = 4096; let mut free_pages = 0;
150 let mut inactive_pages = 0;
151
152 for line in stdout.lines() {
153 if line.starts_with("Mach Virtual Memory Statistics:") {
154 if line.contains("page size of") {
156 if let Some(size_str) = line.split("page size of ").nth(1) {
157 if let Some(size_str) = size_str.split(" bytes").next() {
158 if let Ok(size) = size_str.parse::<usize>() {
159 pagesize = size;
160 }
161 }
162 }
163 }
164 } else if line.starts_with("Pages free:") {
165 if let Some(count_str) = line.split(':').nth(1) {
166 if let Some(count_str) = count_str.trim().split('.').next() {
167 if let Ok(count) = count_str.parse::<usize>() {
168 free_pages = count;
169 }
170 }
171 }
172 } else if line.starts_with("Pages inactive:") {
173 if let Some(count_str) = line.split(':').nth(1) {
174 if let Some(count_str) = count_str.trim().split('.').next() {
175 if let Ok(count) = count_str.parse::<usize>() {
176 inactive_pages = count;
177 }
178 }
179 }
180 }
181 }
182
183 return (free_pages + inactive_pages) * pagesize;
185 }
186 }
187
188 Self::get_available_memory_fallback()
190 }
191
192 fn get_available_memory_fallback() -> usize {
193 let conservative_total = 2_000_000_000; conservative_total / 4 }
198
199 fn calculate_optimal_chunksize(_availablememory: usize) -> usize {
201 let l3_cache_estimate = 8_000_000; let max_chunk = _availablememory / 10; l3_cache_estimate.min(max_chunk).max(4096)
206 }
207
208 pub fn can_allocate(&self, bytes: usize) -> bool {
210 bytes <= self.available_memory / 2 }
212
213 pub fn recommend_algorithm<F: Float>(&self, datasize: usize) -> AlgorithmChoice {
215 let elementsize = std::mem::size_of::<F>();
216 let total_bytes = datasize * elementsize;
217
218 if total_bytes < 1_000_000 {
219 AlgorithmChoice::Direct
221 } else if self.can_allocate(total_bytes) {
222 AlgorithmChoice::Optimized
223 } else {
224 AlgorithmChoice::Streaming(self.preferred_chunksize / elementsize)
225 }
226 }
227}
228
229#[derive(Debug, Clone)]
230pub enum AlgorithmChoice {
231 Direct,
233 Optimized,
235 Streaming(usize),
237}
238
239pub mod zero_copy {
243 use super::*;
244
245 pub fn rolling_stats_zerocopy<F, D, S>(
247 data: &ArrayBase<D, Ix1>,
248 windowsize: usize,
249 stat_fn: S,
250 ) -> StatsResult<Array1<F>>
251 where
252 F: Float + NumCast,
253 D: Data<Elem = F>,
254 S: Fn(ArrayView1<F>) -> StatsResult<F>,
255 {
256 let n = data.len();
257 if windowsize == 0 || windowsize > n {
258 return Err(StatsError::invalid_argument("Invalid window size"));
259 }
260
261 let output_len = n - windowsize + 1;
262 let mut results = Array1::zeros(output_len);
263
264 for i in 0..output_len {
266 let window = data.slice(s![i..i + windowsize]);
267 results[i] = stat_fn(window)?;
268 }
269
270 Ok(results)
271 }
272
273 pub fn pairwise_operation_zerocopy<F, D, Op>(
275 data: &ArrayBase<D, Ix2>,
276 operation: Op,
277 ) -> StatsResult<Array2<F>>
278 where
279 F: Float + NumCast,
280 D: Data<Elem = F>,
281 Op: Fn(ArrayView1<F>, ArrayView1<F>) -> StatsResult<F>,
282 {
283 let n = data.nrows();
284 let mut result = Array2::zeros((n, n));
285
286 for i in 0..n {
287 result[(i, i)] = F::one(); for j in (i + 1)..n {
289 let row_i = data.row(i);
290 let row_j = data.row(j);
291 let value = operation(row_i, row_j)?;
292 result[(i, j)] = value;
293 result[(j, i)] = value; }
295 }
296
297 Ok(result)
298 }
299}
300
301pub mod memory_mapped {
303 use super::*;
304
305 pub fn mmap_mean<'a, F: Float + NumCast + std::fmt::Display + std::iter::Sum<F> + 'a>(
307 data_chunks: impl Iterator<Item = ArrayView1<'a, F>>,
308 total_count: usize,
309 ) -> StatsResult<F> {
310 if total_count == 0 {
311 return Err(StatsError::invalid_argument("Empty dataset"));
312 }
313
314 let mut total_sum = F::zero();
315 let mut count_processed = 0;
316
317 for chunk in data_chunks {
318 let chunk_sum = chunk.sum();
319 total_sum = total_sum + chunk_sum;
320 count_processed += chunk.len();
321 }
322
323 if count_processed != total_count {
324 return Err(StatsError::invalid_argument("Chunk _count mismatch"));
325 }
326
327 Ok(total_sum / F::from(total_count).expect("Failed to convert to float"))
328 }
329
330 pub fn mmap_variance<'a, F: Float + NumCast + std::fmt::Display + 'a>(
332 data_chunks: impl Iterator<Item = ArrayView1<'a, F>>,
333 total_count: usize,
334 ddof: usize,
335 ) -> StatsResult<(F, F)> {
336 if total_count <= ddof {
337 return Err(StatsError::invalid_argument("Insufficient data for ddof"));
338 }
339
340 let mut mean = F::zero();
341 let mut m2 = F::zero();
342 let mut _count = 0;
343
344 for chunk in data_chunks {
345 for &value in chunk.iter() {
346 _count += 1;
347 let delta = value - mean;
348 mean = mean + delta / F::from(_count).expect("Failed to convert to float");
349 let delta2 = value - mean;
350 m2 = m2 + delta * delta2;
351 }
352 }
353
354 let variance = m2 / F::from(_count - ddof).expect("Failed to convert to float");
355 Ok((mean, variance))
356 }
357}
358
359pub struct RingBufferStats<F: Float> {
361 buffer: VecDeque<F>,
362 capacity: usize,
363 sum: F,
364 sum_squares: F,
365}
366
367impl<F: Float + NumCast + std::fmt::Display> RingBufferStats<F> {
368 pub fn new(capacity: usize) -> Self {
370 Self {
371 buffer: VecDeque::with_capacity(capacity),
372 capacity,
373 sum: F::zero(),
374 sum_squares: F::zero(),
375 }
376 }
377
378 pub fn push(&mut self, value: F) {
380 if self.buffer.len() >= self.capacity {
381 if let Some(old_value) = self.buffer.pop_front() {
382 self.sum = self.sum - old_value;
383 self.sum_squares = self.sum_squares - old_value * old_value;
384 }
385 }
386
387 self.buffer.push_back(value);
388 self.sum = self.sum + value;
389 self.sum_squares = self.sum_squares + value * value;
390 }
391
392 pub fn mean(&self) -> F {
394 if self.buffer.is_empty() {
395 F::zero()
396 } else {
397 self.sum / F::from(self.buffer.len()).expect("Operation failed")
398 }
399 }
400
401 pub fn variance(&self, ddof: usize) -> Option<F> {
403 let n = self.buffer.len();
404 if n <= ddof {
405 return None;
406 }
407
408 let mean = self.mean();
409 let var = self.sum_squares / F::from(n).expect("Failed to convert to float") - mean * mean;
410 Some(
411 var * F::from(n).expect("Failed to convert to float")
412 / F::from(n - ddof).expect("Failed to convert to float"),
413 )
414 }
415
416 pub fn std(&self, ddof: usize) -> Option<F> {
418 self.variance(ddof).map(|v| v.sqrt())
419 }
420}
421
422pub struct LazyStatComputation<F: Float> {
424 data_ref: Arc<Vec<F>>,
425 operations: Vec<StatOperation>,
426}
427
428#[derive(Clone)]
429enum StatOperation {
430 Mean,
431 Variance(usize), Quantile(f64),
433 StandardScaling,
434}
435
436impl<F: Float + NumCast + std::iter::Sum + std::fmt::Display> LazyStatComputation<F> {
437 pub fn new(data: Vec<F>) -> Self {
439 Self {
440 data_ref: Arc::new(data),
441 operations: Vec::new(),
442 }
443 }
444
445 pub fn mean(mut self) -> Self {
447 self.operations.push(StatOperation::Mean);
448 self
449 }
450
451 pub fn variance(mut self, ddof: usize) -> Self {
453 self.operations.push(StatOperation::Variance(ddof));
454 self
455 }
456
457 pub fn quantile(mut self, q: f64) -> Self {
459 self.operations.push(StatOperation::Quantile(q));
460 self
461 }
462
463 pub fn standard_scaling(mut self) -> Self {
471 self.operations.push(StatOperation::StandardScaling);
472 self
473 }
474
475 pub fn compute(&self) -> StatsResult<Vec<F>> {
477 let mut results = Vec::new();
478 let data = &*self.data_ref;
479
480 let need_mean = self.operations.iter().any(|op| {
482 matches!(
483 op,
484 StatOperation::Mean | StatOperation::Variance(_) | StatOperation::StandardScaling
485 )
486 });
487 let need_sorted = self
488 .operations
489 .iter()
490 .any(|op| matches!(op, StatOperation::Quantile(_)));
491
492 let mean = if need_mean {
494 Some(
495 data.iter().fold(F::zero(), |acc, &x| acc + x)
496 / F::from(data.len()).expect("Operation failed"),
497 )
498 } else {
499 None
500 };
501
502 let sorteddata = if need_sorted {
503 let mut sorted = data.clone();
504 sorted.sort_by(|a, b| a.partial_cmp(b).expect("Operation failed"));
505 Some(sorted)
506 } else {
507 None
508 };
509
510 for op in &self.operations {
512 match op {
513 StatOperation::Mean => {
514 results.push(mean.expect("Operation failed"));
515 }
516 StatOperation::Variance(ddof) => {
517 let m = mean.expect("Operation failed");
518 let var = data
519 .iter()
520 .map(|&x| {
521 let diff = x - m;
522 diff * diff
523 })
524 .sum::<F>()
525 / F::from(data.len() - ddof).expect("Operation failed");
526 results.push(var);
527 }
528 StatOperation::Quantile(q) => {
529 let sorted = sorteddata.as_ref().expect("Operation failed");
530 let pos = *q * (sorted.len() - 1) as f64;
531 let idx = pos.floor() as usize;
532 let frac = pos - pos.floor();
533
534 let result = if frac == 0.0 {
535 sorted[idx]
536 } else {
537 let lower = sorted[idx];
538 let upper = sorted[idx + 1];
539 lower + F::from(frac).expect("Failed to convert to float") * (upper - lower)
540 };
541 results.push(result);
542 }
543 StatOperation::StandardScaling => {
544 if data.len() < 2 {
548 return Err(StatsError::invalid_argument(
549 "standard scaling requires at least 2 data points",
550 ));
551 }
552 let m = mean.expect("mean is computed when standard scaling is requested");
553 let var = data
554 .iter()
555 .map(|&x| {
556 let diff = x - m;
557 diff * diff
558 })
559 .sum::<F>()
560 / F::from(data.len() - 1).expect("Operation failed");
561 let std = var.sqrt();
562 if std <= F::zero() {
563 return Err(StatsError::invalid_argument(
564 "standard scaling is undefined for data with zero variance",
565 ));
566 }
567 for &x in data.iter() {
568 results.push((x - m) / std);
569 }
570 }
571 }
572 }
573
574 Ok(results)
575 }
576}
577
578pub struct MemoryTracker {
580 current_usage: usize,
581 peak_usage: usize,
582 allocations: usize,
583 deallocations: usize,
584}
585
586impl Default for MemoryTracker {
587 fn default() -> Self {
588 Self::new()
589 }
590}
591
592impl MemoryTracker {
593 pub fn new() -> Self {
595 Self {
596 current_usage: 0,
597 peak_usage: 0,
598 allocations: 0,
599 deallocations: 0,
600 }
601 }
602
603 pub fn record_allocation(&mut self, bytes: usize) {
605 self.current_usage += bytes;
606 self.peak_usage = self.peak_usage.max(self.current_usage);
607 self.allocations += 1;
608 }
609
610 pub fn record_deallocation(&mut self, bytes: usize) {
612 self.current_usage = self.current_usage.saturating_sub(bytes);
613 self.deallocations += 1;
614 }
615
616 pub fn get_profile(&self) -> MemoryProfile {
618 let efficiency_score = if self.peak_usage > 0 {
619 1.0 - (self.current_usage as f64 / self.peak_usage as f64)
620 } else {
621 1.0
622 };
623
624 MemoryProfile {
625 peak_memory: self.peak_usage,
626 avg_memory: (self.peak_usage + self.current_usage) / 2,
627 allocations: self.allocations,
628 deallocations: self.deallocations,
629 efficiency_score,
630 }
631 }
632}
633
634pub mod cache_friendly {
636 use super::*;
637
638 pub fn tiled_matrix_operation<F, D1, D2, Op>(
640 a: &ArrayBase<D1, Ix2>,
641 b: &ArrayBase<D2, Ix2>,
642 tilesize: usize,
643 operation: Op,
644 ) -> StatsResult<Array2<F>>
645 where
646 F: Float + NumCast,
647 D1: Data<Elem = F>,
648 D2: Data<Elem = F>,
649 Op: Fn(ArrayView2<F>, ArrayView2<F>) -> StatsResult<Array2<F>>,
650 {
651 let (m, k1) = a.dim();
652 let (k2, n) = b.dim();
653
654 if k1 != k2 {
655 return Err(StatsError::dimension_mismatch(
656 "Matrix dimensions incompatible",
657 ));
658 }
659
660 let mut result = Array2::zeros((m, n));
661
662 for i in (0..m).step_by(tilesize) {
664 for j in (0..n).step_by(tilesize) {
665 for k in (0..k1).step_by(tilesize) {
666 let i_end = (i + tilesize).min(m);
667 let j_end = (j + tilesize).min(n);
668 let k_end = (k + tilesize).min(k1);
669
670 let a_tile = a.slice(s![i..i_end, k..k_end]);
671 let b_tile = b.slice(s![k..k_end, j..j_end]);
672
673 let tile_result = operation(a_tile, b_tile)?;
674
675 let mut result_tile = result.slice_mut(s![i..i_end, j..j_end]);
677 result_tile.zip_mut_with(&tile_result, |r, &t| *r = *r + t);
678 }
679 }
680 }
681
682 Ok(result)
683 }
684}
685
686#[cfg(test)]
687mod tests {
688 use super::*;
689 use approx::assert_relative_eq;
690 use scirs2_core::ndarray::array;
691
692 #[test]
693 fn test_memory_adaptive_algorithm() {
694 let adapter = MemoryAdaptiveAlgorithm::new();
695
696 match adapter.recommend_algorithm::<f64>(100) {
698 AlgorithmChoice::Direct => (), _ => panic!("Expected Direct algorithm for small data"),
700 }
701
702 let hugedatasize = adapter.available_memory / 4; match adapter.recommend_algorithm::<f64>(hugedatasize) {
706 AlgorithmChoice::Streaming(_) => (), other => panic!(
708 "Expected Streaming algorithm for large data, got {:?}",
709 other
710 ),
711 }
712 }
713
714 #[test]
715 fn test_ring_buffer_stats() {
716 let mut buffer = RingBufferStats::<f64>::new(5);
717
718 for i in 1..=5 {
720 buffer.push(i as f64);
721 }
722
723 assert_relative_eq!(buffer.mean(), 3.0, epsilon = 1e-10);
724
725 buffer.push(6.0);
727 assert_relative_eq!(buffer.mean(), 4.0, epsilon = 1e-10); }
729
730 #[test]
731 fn test_lazy_computation() {
732 let data = vec![1.0, 2.0, 3.0, 4.0, 5.0];
733 let lazy = LazyStatComputation::new(data)
734 .mean()
735 .variance(1)
736 .quantile(0.5);
737
738 let results = lazy.compute().expect("Operation failed");
739 assert_eq!(results.len(), 3);
740 assert_relative_eq!(results[0], 3.0, epsilon = 1e-10); assert_relative_eq!(results[1], 2.5, epsilon = 1e-10); assert_relative_eq!(results[2], 3.0, epsilon = 1e-10); }
744
745 #[test]
746 fn test_lazy_standard_scaling() {
747 let data = vec![1.0_f64, 2.0, 3.0, 4.0, 5.0];
749 let results = LazyStatComputation::new(data)
750 .standard_scaling()
751 .compute()
752 .expect("standard scaling should succeed");
753
754 assert_eq!(results.len(), 5);
756
757 let std = 2.5_f64.sqrt();
758 let expected = [-2.0 / std, -1.0 / std, 0.0, 1.0 / std, 2.0 / std];
759 for (got, want) in results.iter().zip(expected.iter()) {
760 assert_relative_eq!(*got, *want, epsilon = 1e-10);
761 }
762
763 let m = results.iter().sum::<f64>() / results.len() as f64;
766 assert_relative_eq!(m, 0.0, epsilon = 1e-10);
767 let var =
768 results.iter().map(|x| (x - m) * (x - m)).sum::<f64>() / (results.len() - 1) as f64;
769 assert_relative_eq!(var, 1.0, epsilon = 1e-10);
770 }
771
772 #[test]
773 fn test_lazy_standard_scaling_zero_variance_errors() {
774 let data = vec![7.0_f64, 7.0, 7.0, 7.0];
777 assert!(LazyStatComputation::new(data)
778 .standard_scaling()
779 .compute()
780 .is_err());
781 }
782
783 #[test]
784 fn test_zero_copy_rolling() {
785 let data = array![1.0, 2.0, 3.0, 4.0, 5.0];
786 let results = zero_copy::rolling_stats_zerocopy(&data.view(), 3, |window| {
787 Ok(window.mean().expect("Operation failed"))
788 })
789 .expect("Operation failed");
790
791 assert_eq!(results.len(), 3);
792 assert_relative_eq!(results[0], 2.0, epsilon = 1e-10);
793 assert_relative_eq!(results[1], 3.0, epsilon = 1e-10);
794 assert_relative_eq!(results[2], 4.0, epsilon = 1e-10);
795 }
796}