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 #[allow(dead_code)]
434 StandardScaling,
435}
436
437impl<F: Float + NumCast + std::iter::Sum + std::fmt::Display> LazyStatComputation<F> {
438 pub fn new(data: Vec<F>) -> Self {
440 Self {
441 data_ref: Arc::new(data),
442 operations: Vec::new(),
443 }
444 }
445
446 pub fn mean(mut self) -> Self {
448 self.operations.push(StatOperation::Mean);
449 self
450 }
451
452 pub fn variance(mut self, ddof: usize) -> Self {
454 self.operations.push(StatOperation::Variance(ddof));
455 self
456 }
457
458 pub fn quantile(mut self, q: f64) -> Self {
460 self.operations.push(StatOperation::Quantile(q));
461 self
462 }
463
464 pub fn compute(&self) -> StatsResult<Vec<F>> {
466 let mut results = Vec::new();
467 let data = &*self.data_ref;
468
469 let need_mean = self
471 .operations
472 .iter()
473 .any(|op| matches!(op, StatOperation::Mean | StatOperation::Variance(_)));
474 let need_sorted = self
475 .operations
476 .iter()
477 .any(|op| matches!(op, StatOperation::Quantile(_)));
478
479 let mean = if need_mean {
481 Some(
482 data.iter().fold(F::zero(), |acc, &x| acc + x)
483 / F::from(data.len()).expect("Operation failed"),
484 )
485 } else {
486 None
487 };
488
489 let sorteddata = if need_sorted {
490 let mut sorted = data.clone();
491 sorted.sort_by(|a, b| a.partial_cmp(b).expect("Operation failed"));
492 Some(sorted)
493 } else {
494 None
495 };
496
497 for op in &self.operations {
499 match op {
500 StatOperation::Mean => {
501 results.push(mean.expect("Operation failed"));
502 }
503 StatOperation::Variance(ddof) => {
504 let m = mean.expect("Operation failed");
505 let var = data
506 .iter()
507 .map(|&x| {
508 let diff = x - m;
509 diff * diff
510 })
511 .sum::<F>()
512 / F::from(data.len() - ddof).expect("Operation failed");
513 results.push(var);
514 }
515 StatOperation::Quantile(q) => {
516 let sorted = sorteddata.as_ref().expect("Operation failed");
517 let pos = *q * (sorted.len() - 1) as f64;
518 let idx = pos.floor() as usize;
519 let frac = pos - pos.floor();
520
521 let result = if frac == 0.0 {
522 sorted[idx]
523 } else {
524 let lower = sorted[idx];
525 let upper = sorted[idx + 1];
526 lower + F::from(frac).expect("Failed to convert to float") * (upper - lower)
527 };
528 results.push(result);
529 }
530 StatOperation::StandardScaling => {
531 results.push(F::one());
534 }
535 }
536 }
537
538 Ok(results)
539 }
540}
541
542pub struct MemoryTracker {
544 current_usage: usize,
545 peak_usage: usize,
546 allocations: usize,
547 deallocations: usize,
548}
549
550impl Default for MemoryTracker {
551 fn default() -> Self {
552 Self::new()
553 }
554}
555
556impl MemoryTracker {
557 pub fn new() -> Self {
559 Self {
560 current_usage: 0,
561 peak_usage: 0,
562 allocations: 0,
563 deallocations: 0,
564 }
565 }
566
567 pub fn record_allocation(&mut self, bytes: usize) {
569 self.current_usage += bytes;
570 self.peak_usage = self.peak_usage.max(self.current_usage);
571 self.allocations += 1;
572 }
573
574 pub fn record_deallocation(&mut self, bytes: usize) {
576 self.current_usage = self.current_usage.saturating_sub(bytes);
577 self.deallocations += 1;
578 }
579
580 pub fn get_profile(&self) -> MemoryProfile {
582 let efficiency_score = if self.peak_usage > 0 {
583 1.0 - (self.current_usage as f64 / self.peak_usage as f64)
584 } else {
585 1.0
586 };
587
588 MemoryProfile {
589 peak_memory: self.peak_usage,
590 avg_memory: (self.peak_usage + self.current_usage) / 2,
591 allocations: self.allocations,
592 deallocations: self.deallocations,
593 efficiency_score,
594 }
595 }
596}
597
598pub mod cache_friendly {
600 use super::*;
601
602 pub fn tiled_matrix_operation<F, D1, D2, Op>(
604 a: &ArrayBase<D1, Ix2>,
605 b: &ArrayBase<D2, Ix2>,
606 tilesize: usize,
607 operation: Op,
608 ) -> StatsResult<Array2<F>>
609 where
610 F: Float + NumCast,
611 D1: Data<Elem = F>,
612 D2: Data<Elem = F>,
613 Op: Fn(ArrayView2<F>, ArrayView2<F>) -> StatsResult<Array2<F>>,
614 {
615 let (m, k1) = a.dim();
616 let (k2, n) = b.dim();
617
618 if k1 != k2 {
619 return Err(StatsError::dimension_mismatch(
620 "Matrix dimensions incompatible",
621 ));
622 }
623
624 let mut result = Array2::zeros((m, n));
625
626 for i in (0..m).step_by(tilesize) {
628 for j in (0..n).step_by(tilesize) {
629 for k in (0..k1).step_by(tilesize) {
630 let i_end = (i + tilesize).min(m);
631 let j_end = (j + tilesize).min(n);
632 let k_end = (k + tilesize).min(k1);
633
634 let a_tile = a.slice(s![i..i_end, k..k_end]);
635 let b_tile = b.slice(s![k..k_end, j..j_end]);
636
637 let tile_result = operation(a_tile, b_tile)?;
638
639 let mut result_tile = result.slice_mut(s![i..i_end, j..j_end]);
641 result_tile.zip_mut_with(&tile_result, |r, &t| *r = *r + t);
642 }
643 }
644 }
645
646 Ok(result)
647 }
648}
649
650#[cfg(test)]
651mod tests {
652 use super::*;
653 use approx::assert_relative_eq;
654 use scirs2_core::ndarray::array;
655
656 #[test]
657 fn test_memory_adaptive_algorithm() {
658 let adapter = MemoryAdaptiveAlgorithm::new();
659
660 match adapter.recommend_algorithm::<f64>(100) {
662 AlgorithmChoice::Direct => (), _ => panic!("Expected Direct algorithm for small data"),
664 }
665
666 let hugedatasize = adapter.available_memory / 4; match adapter.recommend_algorithm::<f64>(hugedatasize) {
670 AlgorithmChoice::Streaming(_) => (), other => panic!(
672 "Expected Streaming algorithm for large data, got {:?}",
673 other
674 ),
675 }
676 }
677
678 #[test]
679 fn test_ring_buffer_stats() {
680 let mut buffer = RingBufferStats::<f64>::new(5);
681
682 for i in 1..=5 {
684 buffer.push(i as f64);
685 }
686
687 assert_relative_eq!(buffer.mean(), 3.0, epsilon = 1e-10);
688
689 buffer.push(6.0);
691 assert_relative_eq!(buffer.mean(), 4.0, epsilon = 1e-10); }
693
694 #[test]
695 fn test_lazy_computation() {
696 let data = vec![1.0, 2.0, 3.0, 4.0, 5.0];
697 let lazy = LazyStatComputation::new(data)
698 .mean()
699 .variance(1)
700 .quantile(0.5);
701
702 let results = lazy.compute().expect("Operation failed");
703 assert_eq!(results.len(), 3);
704 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); }
708
709 #[test]
710 fn test_zero_copy_rolling() {
711 let data = array![1.0, 2.0, 3.0, 4.0, 5.0];
712 let results = zero_copy::rolling_stats_zerocopy(&data.view(), 3, |window| {
713 Ok(window.mean().expect("Operation failed"))
714 })
715 .expect("Operation failed");
716
717 assert_eq!(results.len(), 3);
718 assert_relative_eq!(results[0], 2.0, epsilon = 1e-10);
719 assert_relative_eq!(results[1], 3.0, epsilon = 1e-10);
720 assert_relative_eq!(results[2], 4.0, epsilon = 1e-10);
721 }
722}