sparse_ir/
numeric.rs

1//! Custom numeric traits for high-precision computation
2//!
3//! This module provides custom numeric traits that work with both f64 and the xprec Df64 backend
4//! for high-precision numerical computation in gauss quadrature and matrix operations.
5
6use crate::Df64;
7use simba::scalar::ComplexField;
8use std::fmt::Debug;
9
10/// Custom numeric trait for high-precision numerical computation
11///
12/// This trait provides the essential numeric operations needed for gauss module
13/// and matrix_from_gauss functions. Supports both f64 and Df64 types.
14///
15/// Uses standard traits for common operations:
16/// - num_traits::Zero for zero()
17/// - simba::scalar::ComplexField for mathematical functions (abs, sqrt, cos, sin, exp, etc.)
18/// - ComplexField::is_finite() for is_finite()
19pub trait CustomNumeric:
20    Copy
21    + Debug
22    + PartialOrd
23    + std::fmt::Display
24    + std::ops::Add<Output = Self>
25    + std::ops::Sub<Output = Self>
26    + std::ops::Mul<Output = Self>
27    + std::ops::Div<Output = Self>
28    + std::ops::Neg<Output = Self>
29    + num_traits::Zero                    // zero()
30    + simba::scalar::ComplexField         // abs, cos, sin, exp, sqrt, etc.
31{
32    /// Convert from f64 to Self (direct conversion, no Option)
33    ///
34    /// This is a static method that can be called as T::from_f64_unchecked(x)
35    /// Renamed to avoid conflict with num_traits::FromPrimitive::from_f64
36    fn from_f64_unchecked(x: f64) -> Self;
37
38    /// Convert from any CustomNumeric type to Self (generic conversion)
39    ///
40    /// This allows conversion between different numeric types while preserving precision.
41    /// Can be called as T::convert_from(other_numeric_value)
42    fn convert_from<U: CustomNumeric + 'static>(value: U) -> Self;
43
44
45    /// Convert to f64
46    fn to_f64(self) -> f64;
47
48    /// Get machine epsilon
49    fn epsilon() -> Self;
50
51    /// Get high-precision PI constant
52    fn pi() -> Self;
53
54    /// Maximum of two values (not provided by ComplexField)
55    fn max(self, other: Self) -> Self;
56
57    /// Minimum of two values (not provided by ComplexField)
58    fn min(self, other: Self) -> Self;
59
60    /// Check if the value is valid (not NaN/infinite)
61    fn is_valid(&self) -> bool;
62
63    /// Check if the value is finite (not NaN or infinite)
64
65    /// Get absolute value as the same type (convenience method)
66    ///
67    /// This method wraps ComplexField::abs() and converts the result back to Self
68    /// to avoid type conversion issues in generic code.
69    /// Note: Only abs() has this problem - other math functions (exp, cos, sin, sqrt) 
70    /// already return Self directly from ComplexField.
71    fn abs_as_same_type(self) -> Self;
72}
73
74/// f64 implementation of CustomNumeric
75impl CustomNumeric for f64 {
76    fn from_f64_unchecked(x: f64) -> Self {
77        x
78    }
79
80    fn convert_from<U: CustomNumeric + 'static>(value: U) -> Self {
81        // Use match to optimize conversion based on the source type
82        // Note: Using TypeId for compile-time optimization, but falling back to safe conversion
83        match std::any::TypeId::of::<U>() {
84            // For f64 to f64, this is just a copy (no conversion needed)
85            id if id == std::any::TypeId::of::<f64>() => {
86                // Safe: f64 to f64 conversion
87                // This is a no-op for f64
88                value.to_f64()
89            }
90            // For Df64 to f64, use the conversion method
91            id if id == std::any::TypeId::of::<Df64>() => {
92                // Safe: Df64 to f64 conversion
93                value.to_f64()
94            }
95            // Fallback: convert via f64 for unknown types
96            _ => value.to_f64(),
97        }
98    }
99
100    fn to_f64(self) -> f64 {
101        self
102    }
103
104    fn epsilon() -> Self {
105        f64::EPSILON
106    }
107
108    fn pi() -> Self {
109        std::f64::consts::PI
110    }
111
112    fn max(self, other: Self) -> Self {
113        self.max(other)
114    }
115
116    fn min(self, other: Self) -> Self {
117        self.min(other)
118    }
119
120    fn is_valid(&self) -> bool {
121        num_traits::Float::is_finite(*self)
122    }
123
124    fn abs_as_same_type(self) -> Self {
125        Self::convert_from(self.abs())
126    }
127}
128
129/// Df64 implementation of CustomNumeric
130impl CustomNumeric for Df64 {
131    fn from_f64_unchecked(x: f64) -> Self {
132        Df64::from(x)
133    }
134
135    fn convert_from<U: CustomNumeric + 'static>(value: U) -> Self {
136        // Use match to optimize conversion based on the source type
137        // Note: Using TypeId for compile-time optimization, but falling back to safe conversion
138        match std::any::TypeId::of::<U>() {
139            // For f64 to Df64, use the conversion method
140            id if id == std::any::TypeId::of::<f64>() => {
141                // Safe: f64 to Df64 conversion
142                let f64_value = value.to_f64();
143                Self::from_f64_unchecked(f64_value)
144            }
145            // For Df64 to Df64, this is just a copy (no conversion needed)
146            id if id == std::any::TypeId::of::<Df64>() => {
147                // Safe: Df64 to Df64 conversion (copy)
148                // We know U is Df64 from TypeId check, and Df64 implements Copy
149                // so we can safely copy the value without losing precision
150                // Note: We can't just return `value` directly because Rust's type system
151                // doesn't know that U == Df64 at compile time, even though we've verified
152                // it at runtime with TypeId. So we need unsafe transmute_copy.
153                unsafe {
154                    // Safety: We've verified via TypeId that U == Df64, and Df64 is Copy
155                    // so this is a no-op copy that preserves all precision (hi and lo parts)
156                    std::mem::transmute_copy(&value)
157                }
158            }
159            // Fallback: convert via f64 for unknown types
160            _ => Self::from_f64_unchecked(value.to_f64()),
161        }
162    }
163
164    fn to_f64(self) -> f64 {
165        // Use hi() and lo() methods for better precision
166        let hi = self.hi();
167        let lo = self.lo();
168        hi + lo
169    }
170
171    fn epsilon() -> Self {
172        // Df64::EPSILON = f64::EPSILON * f64::EPSILON / 2.0
173        let epsilon_value = f64::EPSILON * f64::EPSILON / 2.0;
174        Df64::from(epsilon_value)
175    }
176
177    fn pi() -> Self {
178        Df64::from(std::f64::consts::PI)
179    }
180
181    fn max(self, other: Self) -> Self {
182        if self > other { self } else { other }
183    }
184
185    fn min(self, other: Self) -> Self {
186        if self < other { self } else { other }
187    }
188
189    fn is_valid(&self) -> bool {
190        let value = *self;
191        f64::from(value).is_finite() && !f64::from(value).is_nan()
192    }
193
194    fn abs_as_same_type(self) -> Self {
195        Self::convert_from(self.abs())
196    }
197}
198
199// Note: ScalarOperand implementations for f64 and Df64 are provided by ndarray
200// We cannot implement them here due to Orphan Rules, but they are already implemented
201// in the ndarray crate for standard numeric types.
202
203// Note: Df64ArrayOps trait and impl removed after ndarray migration
204// Array operations should now be done using mdarray Tensor methods directly
205
206#[cfg(test)]
207mod tests {
208    use super::*;
209
210    #[test]
211    fn test_f64_custom_numeric() {
212        let x = 1.5_f64;
213        let y = -2.0_f64;
214
215        // Test basic operations
216        assert_eq!(x.abs(), 1.5);
217        assert_eq!(y.abs(), 2.0);
218
219        // Test mathematical functions
220        let cos_x = x.cos();
221        assert!(f64::from(cos_x).is_finite());
222
223        let sqrt_x = x.sqrt();
224        assert!(f64::from(sqrt_x).is_finite());
225
226        // Test conversion
227        assert_eq!(x.to_f64(), 1.5);
228        assert_eq!(<f64 as CustomNumeric>::epsilon(), f64::EPSILON);
229    }
230
231    #[test]
232    fn test_twofloat_custom_numeric() {
233        let x = Df64::from_f64_unchecked(1.5);
234        let y = Df64::from_f64_unchecked(-2.0);
235
236        // Test basic operations
237        assert_eq!(x.abs(), Df64::from_f64_unchecked(1.5));
238        assert_eq!(y.abs(), Df64::from_f64_unchecked(2.0));
239
240        // Test mathematical functions
241        let cos_x = x.cos();
242        assert!(f64::from(cos_x).is_finite());
243
244        let sqrt_x = x.sqrt();
245        assert!(f64::from(sqrt_x).is_finite());
246
247        // Test conversion
248        let x_f64 = x.to_f64();
249        assert!((x_f64 - 1.5).abs() < 1e-15);
250
251        // Test epsilon
252        let eps = Df64::epsilon();
253        assert!(eps > Df64::from_f64_unchecked(0.0));
254        assert!(eps < Df64::from_f64_unchecked(1.0));
255    }
256
257    // Note: Df64ArrayOps tests removed after ndarray migration
258
259    #[test]
260    fn test_precision_comparison() {
261        // Test that Df64 provides higher precision than f64
262        let pi_f64 = std::f64::consts::PI;
263        let pi_tf = Df64::from_f64_unchecked(pi_f64);
264
265        // Both should be finite
266        assert!(pi_f64.is_finite());
267        assert!(f64::from(pi_tf).is_finite());
268
269        // Df64 should convert back to f64 with minimal loss
270        let pi_back = pi_tf.to_f64();
271        assert!((pi_back - pi_f64).abs() < f64::EPSILON);
272    }
273}