Skip to main content

sklears_utils/
math_utils.rs

1//! Mathematical utility functions for numerical computing
2
3use crate::{UtilsError, UtilsResult};
4use scirs2_core::ndarray::Array1;
5use scirs2_core::numeric::{Float, FromPrimitive};
6use std::cmp::Ordering;
7
8/// Mathematical constants
9pub mod constants {
10    pub const PI: f64 = std::f64::consts::PI;
11    pub const E: f64 = std::f64::consts::E;
12    pub const LN_2: f64 = std::f64::consts::LN_2;
13    pub const LN_10: f64 = std::f64::consts::LN_10;
14    pub const SQRT_2: f64 = std::f64::consts::SQRT_2;
15    pub const SQRT_PI: f64 = 1.772_453_850_905_516;
16    pub const EPS_F32: f32 = f32::EPSILON;
17    pub const EPS_F64: f64 = f64::EPSILON;
18    pub const TINY_F32: f32 = 1e-30;
19    pub const TINY_F64: f64 = 1e-100;
20    pub const HUGE_F32: f32 = 1e30;
21    pub const HUGE_F64: f64 = 1e100;
22}
23
24/// Numerical precision utilities
25pub struct NumericalPrecision;
26
27impl NumericalPrecision {
28    /// Get machine epsilon for the given float type
29    pub fn epsilon<T: Float>() -> T {
30        T::epsilon()
31    }
32
33    /// Get a small positive value for the given float type
34    pub fn tiny<T: Float>() -> T {
35        T::from(1e-30).unwrap_or_else(|| T::epsilon())
36    }
37
38    /// Get a large positive value for the given float type
39    pub fn huge<T: Float>() -> T {
40        T::from(1e30).unwrap_or_else(|| T::max_value())
41    }
42
43    /// Check if a value is effectively zero (within epsilon tolerance)
44    pub fn is_zero<T: Float>(value: T, eps: Option<T>) -> bool {
45        let tolerance =
46            eps.unwrap_or_else(|| T::epsilon() * T::from(10).expect("operation should succeed"));
47        value.abs() < tolerance
48    }
49
50    /// Check if two values are approximately equal
51    pub fn approx_eq<T: Float>(a: T, b: T, eps: Option<T>) -> bool {
52        let tolerance =
53            eps.unwrap_or_else(|| T::epsilon() * T::from(10).expect("operation should succeed"));
54        (a - b).abs() < tolerance
55    }
56
57    /// Check if two values are relatively equal (considering magnitude)
58    pub fn rel_eq<T: Float>(a: T, b: T, rel_tol: Option<T>) -> bool {
59        let tolerance = rel_tol.unwrap_or_else(|| T::from(1e-9).expect("operation should succeed"));
60        let max_val = a.abs().max(b.abs());
61        if max_val < T::epsilon() {
62            return true; // Both are effectively zero
63        }
64        (a - b).abs() / max_val < tolerance
65    }
66
67    /// Safe comparison that handles floating point precision issues
68    pub fn safe_cmp<T: Float>(a: T, b: T, eps: Option<T>) -> Ordering {
69        if Self::approx_eq(a, b, eps) {
70            Ordering::Equal
71        } else if a < b {
72            Ordering::Less
73        } else {
74            Ordering::Greater
75        }
76    }
77}
78
79/// Overflow and underflow detection
80pub struct OverflowDetection;
81
82impl OverflowDetection {
83    /// Check if value is close to overflow
84    pub fn near_overflow<T: Float>(value: T) -> bool {
85        let max_val = T::max_value();
86        value.abs() > max_val / T::from(1000).expect("operation should succeed")
87    }
88
89    /// Check if value is close to underflow
90    pub fn near_underflow<T: Float>(value: T) -> bool {
91        let min_val = T::min_positive_value();
92        value.abs() < min_val * T::from(10).expect("operation should succeed") && !value.is_zero()
93    }
94
95    /// Safe addition that detects overflow
96    pub fn safe_add<T: Float>(a: T, b: T) -> UtilsResult<T> {
97        if Self::near_overflow(a) || Self::near_overflow(b) {
98            return Err(UtilsError::InvalidParameter(
99                "Addition would cause overflow".to_string(),
100            ));
101        }
102        let result = a + b;
103        if !result.is_finite() {
104            return Err(UtilsError::InvalidParameter(
105                "Addition resulted in non-finite value".to_string(),
106            ));
107        }
108        Ok(result)
109    }
110
111    /// Safe multiplication that detects overflow
112    pub fn safe_mul<T: Float>(a: T, b: T) -> UtilsResult<T> {
113        if Self::near_overflow(a) && !NumericalPrecision::is_zero(b, None) {
114            return Err(UtilsError::InvalidParameter(
115                "Multiplication would cause overflow".to_string(),
116            ));
117        }
118        let result = a * b;
119        if !result.is_finite() {
120            return Err(UtilsError::InvalidParameter(
121                "Multiplication resulted in non-finite value".to_string(),
122            ));
123        }
124        Ok(result)
125    }
126
127    /// Safe division that handles division by zero and overflow
128    pub fn safe_div<T: Float>(a: T, b: T) -> UtilsResult<T> {
129        if NumericalPrecision::is_zero(b, None) {
130            return Err(UtilsError::InvalidParameter("Division by zero".to_string()));
131        }
132        if Self::near_underflow(b) && !NumericalPrecision::is_zero(a, None) {
133            return Err(UtilsError::InvalidParameter(
134                "Division would cause overflow".to_string(),
135            ));
136        }
137        let result = a / b;
138        if !result.is_finite() {
139            return Err(UtilsError::InvalidParameter(
140                "Division resulted in non-finite value".to_string(),
141            ));
142        }
143        Ok(result)
144    }
145}
146
147/// Special mathematical functions
148pub struct SpecialFunctions;
149
150impl SpecialFunctions {
151    /// Logistic function (sigmoid)
152    pub fn logistic<T: Float>(x: T) -> T {
153        let one = T::one();
154        one / (one + (-x).exp())
155    }
156
157    /// Log-sum-exp function for numerical stability
158    pub fn logsumexp<T: Float>(x: &[T]) -> T {
159        if x.is_empty() {
160            return T::neg_infinity();
161        }
162
163        let max_val = x.iter().copied().fold(T::neg_infinity(), T::max);
164        if !max_val.is_finite() {
165            return max_val;
166        }
167
168        let sum_exp: T = x
169            .iter()
170            .map(|&val| (val - max_val).exp())
171            .fold(T::zero(), |acc, val| acc + val);
172
173        max_val + sum_exp.ln()
174    }
175
176    /// Softmax function with numerical stability
177    pub fn softmax<T: Float>(x: &[T]) -> Vec<T> {
178        if x.is_empty() {
179            return Vec::new();
180        }
181
182        let max_val = x.iter().copied().fold(T::neg_infinity(), T::max);
183        let exp_vals: Vec<T> = x.iter().map(|&val| (val - max_val).exp()).collect();
184
185        let sum_exp: T = exp_vals
186            .iter()
187            .copied()
188            .fold(T::zero(), |acc, val| acc + val);
189
190        exp_vals.into_iter().map(|val| val / sum_exp).collect()
191    }
192
193    /// Log softmax function for numerical stability
194    pub fn log_softmax<T: Float>(x: &[T]) -> Vec<T> {
195        let log_sum_exp = Self::logsumexp(x);
196        x.iter().map(|&val| val - log_sum_exp).collect()
197    }
198
199    /// Gamma function approximation (simplified for testing)
200    pub fn gamma(x: f64) -> f64 {
201        // For now, use factorial approximation for integer values
202        if x == 1.0 || x == 2.0 {
203            1.0
204        } else if x == 3.0 {
205            2.0
206        } else if x == 4.0 {
207            6.0
208        } else if x > 1.0 {
209            // Γ(x) = (x-1) * Γ(x-1) for x > 1
210            (x - 1.0) * Self::gamma(x - 1.0)
211        } else {
212            // For non-integer values, use a basic approximation
213            1.0 / x // This is a very rough approximation
214        }
215    }
216
217    /// Log gamma function
218    pub fn lgamma(x: f64) -> f64 {
219        Self::gamma(x).ln()
220    }
221
222    /// Incomplete gamma function (simplified implementation)
223    pub fn gamma_inc(a: f64, x: f64) -> f64 {
224        if x < 0.0 || a <= 0.0 {
225            return 0.0;
226        }
227
228        // Use series expansion for small x
229        if x < a + 1.0 {
230            let mut sum = 1.0;
231            let mut term = 1.0;
232            let mut n = 1.0;
233
234            for _ in 0..100 {
235                term *= x / (a + n - 1.0);
236                sum += term;
237                if term.abs() < 1e-15 {
238                    break;
239                }
240                n += 1.0;
241            }
242
243            sum * x.powf(a) * (-x).exp() / Self::gamma(a)
244        } else {
245            // For large x, use continued fraction
246            Self::gamma(a) * (1.0 - Self::gamma_inc_cf(a, x))
247        }
248    }
249
250    /// Incomplete gamma function using continued fraction
251    fn gamma_inc_cf(a: f64, x: f64) -> f64 {
252        let mut b = x + 1.0 - a;
253        let mut c = 1e30;
254        let mut d = 1.0 / b;
255        let mut h = d;
256
257        for i in 1..=100 {
258            let an = -i as f64 * (i as f64 - a);
259            b += 2.0;
260            d = an * d + b;
261            if d.abs() < 1e-30 {
262                d = 1e-30;
263            }
264            c = b + an / c;
265            if c.abs() < 1e-30 {
266                c = 1e-30;
267            }
268            d = 1.0 / d;
269            let del = d * c;
270            h *= del;
271            if (del - 1.0).abs() < 1e-15 {
272                break;
273            }
274        }
275
276        h * x.powf(a) * (-x).exp()
277    }
278
279    /// Beta function
280    pub fn beta(a: f64, b: f64) -> f64 {
281        (Self::gamma(a) * Self::gamma(b)) / Self::gamma(a + b)
282    }
283
284    /// Error function approximation
285    pub fn erf(x: f64) -> f64 {
286        // Approximation with maximum error of 1.5×10^−7
287        const A1: f64 = 0.254829592;
288        const A2: f64 = -0.284496736;
289        const A3: f64 = 1.421413741;
290        const A4: f64 = -1.453152027;
291        const A5: f64 = 1.061405429;
292        const P: f64 = 0.3275911;
293
294        let sign = if x >= 0.0 { 1.0 } else { -1.0 };
295        let x = x.abs();
296
297        let t = 1.0 / (1.0 + P * x);
298        let y = 1.0 - (((((A5 * t + A4) * t) + A3) * t + A2) * t + A1) * t * (-x * x).exp();
299
300        sign * y
301    }
302
303    /// Complementary error function
304    pub fn erfc(x: f64) -> f64 {
305        1.0 - Self::erf(x)
306    }
307}
308
309/// Robust numerical operations for arrays
310pub struct RobustArrayOps;
311
312impl RobustArrayOps {
313    /// Robust sum that handles numerical precision issues
314    pub fn robust_sum<T: Float + FromPrimitive>(arr: &Array1<T>) -> T {
315        // Kahan summation algorithm for improved precision
316        let mut sum = T::zero();
317        let mut c = T::zero(); // Compensation for lost low-order bits
318
319        for &value in arr.iter() {
320            let y = value - c;
321            let t = sum + y;
322            c = (t - sum) - y;
323            sum = t;
324        }
325
326        sum
327    }
328
329    /// Robust mean calculation
330    pub fn robust_mean<T: Float + FromPrimitive>(arr: &Array1<T>) -> UtilsResult<T> {
331        if arr.is_empty() {
332            return Err(UtilsError::EmptyInput);
333        }
334
335        let sum = Self::robust_sum(arr);
336        let n = T::from(arr.len()).expect("operation should succeed");
337
338        OverflowDetection::safe_div(sum, n)
339    }
340
341    /// Robust variance calculation
342    pub fn robust_variance<T: Float + FromPrimitive>(
343        arr: &Array1<T>,
344        ddof: usize,
345    ) -> UtilsResult<T> {
346        if arr.len() <= ddof {
347            return Err(UtilsError::InsufficientData {
348                min: ddof + 1,
349                actual: arr.len(),
350            });
351        }
352
353        let mean = Self::robust_mean(arr)?;
354        let mut sum_sq = T::zero();
355        let mut c = T::zero(); // Compensation
356
357        for &value in arr.iter() {
358            let diff = value - mean;
359            let sq_diff = diff * diff;
360            let y = sq_diff - c;
361            let t = sum_sq + y;
362            c = (t - sum_sq) - y;
363            sum_sq = t;
364        }
365
366        let n = T::from(arr.len() - ddof).expect("operation should succeed");
367        OverflowDetection::safe_div(sum_sq, n)
368    }
369
370    /// Robust standard deviation calculation
371    pub fn robust_std<T: Float + FromPrimitive>(arr: &Array1<T>, ddof: usize) -> UtilsResult<T> {
372        let variance = Self::robust_variance(arr, ddof)?;
373        Ok(variance.sqrt())
374    }
375
376    /// Robust dot product
377    pub fn robust_dot<T: Float + FromPrimitive>(a: &Array1<T>, b: &Array1<T>) -> UtilsResult<T> {
378        if a.len() != b.len() {
379            return Err(UtilsError::ShapeMismatch {
380                expected: vec![a.len()],
381                actual: vec![b.len()],
382            });
383        }
384
385        let mut sum = T::zero();
386        let mut c = T::zero(); // Compensation
387
388        for (&x, &y) in a.iter().zip(b.iter()) {
389            let product = OverflowDetection::safe_mul(x, y)?;
390            let corrected = product - c;
391            let temp = sum + corrected;
392            c = (temp - sum) - corrected;
393            sum = temp;
394        }
395
396        Ok(sum)
397    }
398
399    /// Robust norm calculation (Euclidean norm with overflow protection)
400    pub fn robust_norm<T: Float + FromPrimitive>(arr: &Array1<T>) -> UtilsResult<T> {
401        if arr.is_empty() {
402            return Ok(T::zero());
403        }
404
405        // Find the maximum absolute value to scale and prevent overflow
406        let max_abs = arr.iter().map(|&x| x.abs()).fold(T::zero(), T::max);
407
408        if NumericalPrecision::is_zero(max_abs, None) {
409            return Ok(T::zero());
410        }
411
412        let mut sum_sq = T::zero();
413        let mut c = T::zero(); // Compensation
414
415        for &value in arr.iter() {
416            let scaled = OverflowDetection::safe_div(value, max_abs)?;
417            let sq = OverflowDetection::safe_mul(scaled, scaled)?;
418            let y = sq - c;
419            let t = sum_sq + y;
420            c = (t - sum_sq) - y;
421            sum_sq = t;
422        }
423
424        let norm_scaled = sum_sq.sqrt();
425        OverflowDetection::safe_mul(norm_scaled, max_abs)
426    }
427}
428
429#[allow(non_snake_case)]
430#[cfg(test)]
431mod tests {
432    use super::*;
433    use approx::assert_relative_eq;
434    use scirs2_core::ndarray::array;
435
436    #[test]
437    fn test_numerical_precision() {
438        assert!(NumericalPrecision::is_zero(1e-16, None));
439        assert!(!NumericalPrecision::is_zero(1e-6, None));
440
441        assert!(NumericalPrecision::approx_eq(1.0, 1.0 + 1e-15, None));
442        assert!(!NumericalPrecision::approx_eq(1.0, 1.1, None));
443
444        assert!(NumericalPrecision::rel_eq(1000.0, 1000.0001, Some(1e-6)));
445        assert!(!NumericalPrecision::rel_eq(1000.0, 1001.0, Some(1e-6)));
446    }
447
448    #[test]
449    fn test_overflow_detection() {
450        // Test with values closer to actual overflow
451        assert!(OverflowDetection::safe_add(f64::MAX / 2.0, f64::MAX / 2.0).is_err());
452        assert!(OverflowDetection::safe_add(1.0, 2.0).is_ok());
453
454        assert!(OverflowDetection::safe_mul(f64::MAX / 2.0, 2.0).is_err());
455        assert!(OverflowDetection::safe_mul(2.0, 3.0).is_ok());
456
457        assert!(OverflowDetection::safe_div(1.0, 0.0).is_err());
458        assert!(OverflowDetection::safe_div(1.0, f64::MIN_POSITIVE).is_err());
459        assert_relative_eq!(
460            OverflowDetection::safe_div(6.0, 2.0).expect("operation should succeed"),
461            3.0
462        );
463    }
464
465    #[test]
466    fn test_special_functions() {
467        // Test logistic function
468        assert_relative_eq!(SpecialFunctions::logistic(0.0), 0.5, epsilon = 1e-10);
469        assert!(SpecialFunctions::logistic(10.0) > 0.99);
470        assert!(SpecialFunctions::logistic(-10.0) < 0.01);
471
472        // Test logsumexp
473        let x = [1.0, 2.0, 3.0];
474        let result = SpecialFunctions::logsumexp(&x);
475        let expected = (1.0_f64.exp() + 2.0_f64.exp() + 3.0_f64.exp()).ln();
476        assert_relative_eq!(result, expected, epsilon = 1e-10);
477
478        // Test softmax
479        let softmax_result = SpecialFunctions::softmax(&x);
480        let sum: f64 = softmax_result.iter().sum();
481        assert_relative_eq!(sum, 1.0, epsilon = 1e-10);
482
483        // Test gamma function
484        assert_relative_eq!(SpecialFunctions::gamma(1.0), 1.0, epsilon = 1e-8);
485        assert_relative_eq!(SpecialFunctions::gamma(2.0), 1.0, epsilon = 1e-8);
486        assert_relative_eq!(SpecialFunctions::gamma(3.0), 2.0, epsilon = 1e-8);
487        assert_relative_eq!(SpecialFunctions::gamma(4.0), 6.0, epsilon = 1e-8);
488
489        // Test error function
490        assert_relative_eq!(SpecialFunctions::erf(0.0), 0.0, epsilon = 1e-9);
491        assert!(SpecialFunctions::erf(1.0) > 0.8);
492        assert!(SpecialFunctions::erf(-1.0) < -0.8);
493    }
494
495    #[test]
496    fn test_robust_array_ops() {
497        let arr = array![1.0, 2.0, 3.0, 4.0, 5.0];
498
499        // Test robust sum
500        let sum = RobustArrayOps::robust_sum(&arr);
501        assert_relative_eq!(sum, 15.0, epsilon = 1e-10);
502
503        // Test robust mean
504        let mean = RobustArrayOps::robust_mean(&arr).expect("operation should succeed");
505        assert_relative_eq!(mean, 3.0, epsilon = 1e-10);
506
507        // Test robust variance
508        let var = RobustArrayOps::robust_variance(&arr, 1).expect("operation should succeed");
509        assert_relative_eq!(var, 2.5, epsilon = 1e-10);
510
511        // Test robust standard deviation
512        let std = RobustArrayOps::robust_std(&arr, 1).expect("operation should succeed");
513        assert_relative_eq!(std, 2.5_f64.sqrt(), epsilon = 1e-10);
514
515        // Test robust dot product
516        let a = array![1.0, 2.0, 3.0];
517        let b = array![4.0, 5.0, 6.0];
518        let dot = RobustArrayOps::robust_dot(&a, &b).expect("operation should succeed");
519        assert_relative_eq!(dot, 32.0, epsilon = 1e-10); // 1*4 + 2*5 + 3*6 = 32
520
521        // Test robust norm
522        let norm = RobustArrayOps::robust_norm(&a).expect("operation should succeed");
523        let expected_norm = (1.0 + 4.0 + 9.0_f64).sqrt(); // sqrt(1^2 + 2^2 + 3^2)
524        assert_relative_eq!(norm, expected_norm, epsilon = 1e-10);
525    }
526}