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}