Skip to main content

scirs2_core/numeric/
utilities.rs

1//! Numeric utilities for floating-point comparison, error measurement, and safe clamping.
2//!
3//! Provides generic functions over `Float` types:
4//!
5//! - `approx_eq` -- approximate equality within an absolute tolerance
6//! - `relative_error` -- relative error between computed and exact values
7//! - `ulp_distance` -- ULP (Unit in the Last Place) distance between floats
8//! - `is_finite_and_positive` -- combined finiteness + positivity check
9//! - `clamp_to_range` -- safe clamping that rejects NaN
10//! - `MachineConstants` trait -- machine epsilon, min/max normal, etc.
11//!
12//! All functions are generic over `num_traits::Float` so they work with
13//! both `f32` and `f64`.
14
15use num_traits::Float;
16use std::fmt::Debug;
17
18use crate::error::{CoreError, CoreResult, ErrorContext};
19
20// ---------------------------------------------------------------------------
21// MachineConstants trait
22// ---------------------------------------------------------------------------
23
24/// Machine-specific constants for a floating-point type.
25///
26/// Provides type-level access to epsilon, minimum/maximum normal values,
27/// minimum positive subnormal, and radix information.
28pub trait MachineConstants: Float + Debug {
29    /// Machine epsilon (difference between 1.0 and the next representable value).
30    fn machine_epsilon() -> Self;
31
32    /// Smallest positive normal number.
33    fn min_positive_normal() -> Self;
34
35    /// Largest finite number.
36    fn max_finite() -> Self;
37
38    /// Smallest positive subnormal number (closest to zero without being zero).
39    fn min_positive_subnormal() -> Self;
40
41    /// Number of significand (mantissa) bits.
42    fn mantissa_digits() -> u32;
43
44    /// Radix of the floating-point representation (always 2 for IEEE 754).
45    fn radix() -> u32 {
46        2
47    }
48}
49
50impl MachineConstants for f32 {
51    fn machine_epsilon() -> Self {
52        f32::EPSILON
53    }
54
55    fn min_positive_normal() -> Self {
56        f32::MIN_POSITIVE
57    }
58
59    fn max_finite() -> Self {
60        f32::MAX
61    }
62
63    fn min_positive_subnormal() -> Self {
64        // Smallest positive f32 subnormal: 2^(-149)
65        // f32::MIN_POSITIVE is the smallest *normal* value
66        // The smallest subnormal is: 1.0 * 2^(-126 - 23) = 2^(-149)
67        1.401_298_4e-45_f32
68    }
69
70    fn mantissa_digits() -> u32 {
71        f32::MANTISSA_DIGITS
72    }
73}
74
75impl MachineConstants for f64 {
76    fn machine_epsilon() -> Self {
77        f64::EPSILON
78    }
79
80    fn min_positive_normal() -> Self {
81        f64::MIN_POSITIVE
82    }
83
84    fn max_finite() -> Self {
85        f64::MAX
86    }
87
88    fn min_positive_subnormal() -> Self {
89        // Smallest positive f64 subnormal: 2^(-1074)
90        5e-324_f64
91    }
92
93    fn mantissa_digits() -> u32 {
94        f64::MANTISSA_DIGITS
95    }
96}
97
98// ---------------------------------------------------------------------------
99// approx_eq
100// ---------------------------------------------------------------------------
101
102/// Check whether two floating-point values are approximately equal within an
103/// absolute tolerance.
104///
105/// Returns `true` if `|a - b| <= tol`.
106///
107/// # Arguments
108///
109/// * `a` - First value
110/// * `b` - Second value
111/// * `tol` - Absolute tolerance (must be non-negative)
112///
113/// # Edge Cases
114///
115/// - If either value is NaN the result is `false`.
116/// - If both values are the same infinity the result is `true`.
117pub fn approx_eq<T: Float>(a: T, b: T, tol: T) -> bool {
118    if a.is_nan() || b.is_nan() {
119        return false;
120    }
121    // Exact equality catches +inf == +inf and -inf == -inf
122    if a == b {
123        return true;
124    }
125    (a - b).abs() <= tol
126}
127
128/// Check approximate equality using a relative tolerance.
129///
130/// Returns `true` if `|a - b| <= rel_tol * max(|a|, |b|)`.
131///
132/// Falls back to absolute tolerance `abs_tol` for values near zero.
133pub fn approx_eq_relative<T: Float>(a: T, b: T, rel_tol: T, abs_tol: T) -> bool {
134    if a.is_nan() || b.is_nan() {
135        return false;
136    }
137    if a == b {
138        return true;
139    }
140    let diff = (a - b).abs();
141    let max_abs = a.abs().max(b.abs());
142    diff <= rel_tol * max_abs || diff <= abs_tol
143}
144
145// ---------------------------------------------------------------------------
146// relative_error
147// ---------------------------------------------------------------------------
148
149/// Compute the relative error between a computed value and an exact (reference) value.
150///
151/// `relative_error = |computed - exact| / |exact|`
152///
153/// # Returns
154///
155/// - `Ok(rel_err)` on success.
156/// - `Err(CoreError::DomainError)` if `exact` is zero (division by zero).
157/// - `Err(CoreError::ValueError)` if either input is NaN.
158pub fn relative_error<T: Float + Debug>(computed: T, exact: T) -> CoreResult<T> {
159    if computed.is_nan() || exact.is_nan() {
160        return Err(CoreError::ValueError(ErrorContext::new(
161            "relative_error: NaN input is not allowed",
162        )));
163    }
164    if exact.is_zero() {
165        return Err(CoreError::DomainError(ErrorContext::new(
166            "relative_error: exact value is zero, relative error is undefined",
167        )));
168    }
169    Ok((computed - exact).abs() / exact.abs())
170}
171
172/// Compute relative error with a safe fallback for near-zero exact values.
173///
174/// If `|exact| < floor`, uses `floor` as the denominator instead.
175pub fn relative_error_safe<T: Float + Debug>(computed: T, exact: T, floor: T) -> CoreResult<T> {
176    if computed.is_nan() || exact.is_nan() || floor.is_nan() {
177        return Err(CoreError::ValueError(ErrorContext::new(
178            "relative_error_safe: NaN input is not allowed",
179        )));
180    }
181    let denom = exact.abs().max(floor);
182    if denom.is_zero() {
183        return Err(CoreError::DomainError(ErrorContext::new(
184            "relative_error_safe: denominator is zero even with floor",
185        )));
186    }
187    Ok((computed - exact).abs() / denom)
188}
189
190// ---------------------------------------------------------------------------
191// ulp_distance
192// ---------------------------------------------------------------------------
193
194/// Compute the ULP (Unit in the Last Place) distance between two `f64` values.
195///
196/// ULP distance counts the number of representable floating-point values
197/// between `a` and `b`.
198///
199/// # Returns
200///
201/// - `Ok(distance)` on success.
202/// - `Err` if either input is NaN.
203///
204/// # Notes
205///
206/// For `f32`, convert to `f64` first or use [`ulp_distance_f32`].
207pub fn ulp_distance(a: f64, b: f64) -> CoreResult<u64> {
208    if a.is_nan() || b.is_nan() {
209        return Err(CoreError::ValueError(ErrorContext::new(
210            "ulp_distance: NaN input is not allowed",
211        )));
212    }
213    if a == b {
214        return Ok(0);
215    }
216
217    let a_bits = a.to_bits() as i64;
218    let b_bits = b.to_bits() as i64;
219
220    // For IEEE 754 doubles, the bit representation is ordered except for
221    // the sign bit. We need to map negative numbers to a continuous integer
222    // space.
223    let a_mapped = if a_bits < 0 {
224        i64::MIN - a_bits
225    } else {
226        a_bits
227    };
228    let b_mapped = if b_bits < 0 {
229        i64::MIN - b_bits
230    } else {
231        b_bits
232    };
233
234    Ok((a_mapped - b_mapped).unsigned_abs())
235}
236
237/// Compute ULP distance for `f32` values.
238pub fn ulp_distance_f32(a: f32, b: f32) -> CoreResult<u32> {
239    if a.is_nan() || b.is_nan() {
240        return Err(CoreError::ValueError(ErrorContext::new(
241            "ulp_distance_f32: NaN input is not allowed",
242        )));
243    }
244    if a == b {
245        return Ok(0);
246    }
247
248    let a_bits = a.to_bits() as i32;
249    let b_bits = b.to_bits() as i32;
250
251    let a_mapped = if a_bits < 0 {
252        i32::MIN - a_bits
253    } else {
254        a_bits
255    };
256    let b_mapped = if b_bits < 0 {
257        i32::MIN - b_bits
258    } else {
259        b_bits
260    };
261
262    Ok((a_mapped - b_mapped).unsigned_abs())
263}
264
265// ---------------------------------------------------------------------------
266// is_finite_and_positive
267// ---------------------------------------------------------------------------
268
269/// Check that a value is both finite and strictly positive (`> 0`).
270///
271/// Returns `false` for NaN, infinity, zero, and negative values.
272pub fn is_finite_and_positive<T: Float>(x: T) -> bool {
273    x.is_finite() && x > T::zero()
274}
275
276/// Check that a value is finite and non-negative (`>= 0`).
277pub fn is_finite_and_non_negative<T: Float>(x: T) -> bool {
278    x.is_finite() && x >= T::zero()
279}
280
281// ---------------------------------------------------------------------------
282// clamp_to_range
283// ---------------------------------------------------------------------------
284
285/// Clamp `x` to the range `[min, max]`, returning an error if `x` is NaN or if
286/// `min > max`.
287///
288/// Unlike `f64::clamp()`, this function does not panic on NaN but returns a
289/// proper error.
290///
291/// # Errors
292///
293/// - `CoreError::ValueError` if `x` is NaN.
294/// - `CoreError::DomainError` if `min > max`.
295pub fn clamp_to_range<T: Float + Debug>(x: T, min: T, max: T) -> CoreResult<T> {
296    if x.is_nan() {
297        return Err(CoreError::ValueError(ErrorContext::new(
298            "clamp_to_range: input is NaN",
299        )));
300    }
301    if min > max {
302        return Err(CoreError::DomainError(ErrorContext::new(format!(
303            "clamp_to_range: min ({min:?}) > max ({max:?})"
304        ))));
305    }
306    if x < min {
307        Ok(min)
308    } else if x > max {
309        Ok(max)
310    } else {
311        Ok(x)
312    }
313}
314
315// ---------------------------------------------------------------------------
316// Convenience functions
317// ---------------------------------------------------------------------------
318
319/// Check if two slices of floats are element-wise approximately equal.
320pub fn slices_approx_eq<T: Float>(a: &[T], b: &[T], tol: T) -> bool {
321    if a.len() != b.len() {
322        return false;
323    }
324    a.iter().zip(b.iter()).all(|(&x, &y)| approx_eq(x, y, tol))
325}
326
327/// Compute the maximum absolute difference between two slices.
328///
329/// Returns `Ok(max_diff)` or `Err` if the slices have different lengths.
330pub fn max_abs_difference<T: Float>(a: &[T], b: &[T]) -> CoreResult<T> {
331    if a.len() != b.len() {
332        return Err(CoreError::DimensionError(ErrorContext::new(format!(
333            "max_abs_difference: length mismatch ({} vs {})",
334            a.len(),
335            b.len()
336        ))));
337    }
338    if a.is_empty() {
339        return Ok(T::zero());
340    }
341    let max = a
342        .iter()
343        .zip(b.iter())
344        .map(|(&x, &y)| (x - y).abs())
345        .fold(T::zero(), |acc, d| if d > acc { d } else { acc });
346    Ok(max)
347}
348
349// ---------------------------------------------------------------------------
350// Tests
351// ---------------------------------------------------------------------------
352
353#[cfg(test)]
354mod tests {
355    use super::*;
356
357    // ---- approx_eq tests ----
358
359    #[test]
360    fn test_approx_eq_basic() {
361        assert!(approx_eq(1.0_f64, 1.0 + 1e-11, 1e-10));
362        assert!(!approx_eq(1.0_f64, 1.0 + 1e-9, 1e-10));
363    }
364
365    #[test]
366    fn test_approx_eq_nan() {
367        assert!(!approx_eq(f64::NAN, 1.0, 1e-10));
368        assert!(!approx_eq(1.0, f64::NAN, 1e-10));
369        assert!(!approx_eq(f64::NAN, f64::NAN, 1e-10));
370    }
371
372    #[test]
373    fn test_approx_eq_infinity() {
374        assert!(approx_eq(f64::INFINITY, f64::INFINITY, 1e-10));
375        assert!(approx_eq(f64::NEG_INFINITY, f64::NEG_INFINITY, 1e-10));
376        assert!(!approx_eq(f64::INFINITY, f64::NEG_INFINITY, 1e-10));
377    }
378
379    #[test]
380    fn test_approx_eq_zero() {
381        assert!(approx_eq(0.0_f64, 0.0, 0.0));
382        assert!(approx_eq(0.0_f64, 1e-16, 1e-15));
383    }
384
385    #[test]
386    fn test_approx_eq_f32() {
387        assert!(approx_eq(1.0_f32, 1.0 + 1e-6, 1e-5));
388        assert!(!approx_eq(1.0_f32, 1.0 + 1e-4, 1e-5));
389    }
390
391    #[test]
392    fn test_approx_eq_relative_basic() {
393        assert!(approx_eq_relative(100.0_f64, 100.001, 1e-4, 1e-10));
394        assert!(!approx_eq_relative(100.0_f64, 101.0, 1e-4, 1e-10));
395    }
396
397    // ---- relative_error tests ----
398
399    #[test]
400    fn test_relative_error_basic() {
401        let err = relative_error(1.01_f64, 1.0).expect("should succeed");
402        assert!(approx_eq(err, 0.01, 1e-14));
403    }
404
405    #[test]
406    fn test_relative_error_exact() {
407        let err = relative_error(1.23_f64, 1.23).expect("should succeed");
408        assert_eq!(err, 0.0);
409    }
410
411    #[test]
412    fn test_relative_error_zero_exact() {
413        let result = relative_error(1.0_f64, 0.0);
414        assert!(result.is_err());
415    }
416
417    #[test]
418    fn test_relative_error_nan() {
419        assert!(relative_error(f64::NAN, 1.0).is_err());
420        assert!(relative_error(1.0, f64::NAN).is_err());
421    }
422
423    #[test]
424    fn test_relative_error_safe_near_zero() {
425        let err = relative_error_safe(0.001_f64, 0.0, 1.0).expect("should succeed");
426        assert!(approx_eq(err, 0.001, 1e-14));
427    }
428
429    // ---- ulp_distance tests ----
430
431    #[test]
432    fn test_ulp_distance_same() {
433        assert_eq!(ulp_distance(1.0, 1.0).expect("should succeed"), 0);
434    }
435
436    #[test]
437    fn test_ulp_distance_adjacent() {
438        let a = 1.0_f64;
439        let b = f64::from_bits(a.to_bits() + 1);
440        assert_eq!(ulp_distance(a, b).expect("should succeed"), 1);
441    }
442
443    #[test]
444    fn test_ulp_distance_symmetric() {
445        let a = 1.0_f64;
446        let b = 1.0 + f64::EPSILON;
447        let d1 = ulp_distance(a, b).expect("should succeed");
448        let d2 = ulp_distance(b, a).expect("should succeed");
449        assert_eq!(d1, d2);
450    }
451
452    #[test]
453    fn test_ulp_distance_nan() {
454        assert!(ulp_distance(f64::NAN, 1.0).is_err());
455    }
456
457    #[test]
458    fn test_ulp_distance_across_zero() {
459        // Distance from a small negative to a small positive
460        let d = ulp_distance(-0.0_f64, 0.0).expect("should succeed");
461        // -0.0 and 0.0 should be 0 ULPs apart because they are equal
462        assert_eq!(d, 0);
463    }
464
465    #[test]
466    fn test_ulp_distance_f32_basic() {
467        let a = 1.0_f32;
468        let b = f32::from_bits(a.to_bits() + 1);
469        assert_eq!(ulp_distance_f32(a, b).expect("should succeed"), 1);
470    }
471
472    #[test]
473    fn test_ulp_distance_f32_nan() {
474        assert!(ulp_distance_f32(f32::NAN, 1.0).is_err());
475    }
476
477    // ---- is_finite_and_positive tests ----
478
479    #[test]
480    fn test_is_finite_and_positive_basic() {
481        assert!(is_finite_and_positive(1.0_f64));
482        assert!(is_finite_and_positive(f64::MIN_POSITIVE));
483        assert!(!is_finite_and_positive(0.0_f64));
484        assert!(!is_finite_and_positive(-1.0_f64));
485        assert!(!is_finite_and_positive(f64::INFINITY));
486        assert!(!is_finite_and_positive(f64::NAN));
487    }
488
489    #[test]
490    fn test_is_finite_and_positive_f32() {
491        assert!(is_finite_and_positive(0.001_f32));
492        assert!(!is_finite_and_positive(f32::NEG_INFINITY));
493    }
494
495    #[test]
496    fn test_is_finite_and_non_negative() {
497        assert!(is_finite_and_non_negative(0.0_f64));
498        assert!(is_finite_and_non_negative(1.0_f64));
499        assert!(!is_finite_and_non_negative(-0.001_f64));
500        assert!(!is_finite_and_non_negative(f64::NAN));
501    }
502
503    // ---- clamp_to_range tests ----
504
505    #[test]
506    fn test_clamp_to_range_normal() {
507        assert_eq!(clamp_to_range(5.0_f64, 0.0, 10.0).expect("ok"), 5.0);
508    }
509
510    #[test]
511    fn test_clamp_to_range_below() {
512        assert_eq!(clamp_to_range(-5.0_f64, 0.0, 10.0).expect("ok"), 0.0);
513    }
514
515    #[test]
516    fn test_clamp_to_range_above() {
517        assert_eq!(clamp_to_range(15.0_f64, 0.0, 10.0).expect("ok"), 10.0);
518    }
519
520    #[test]
521    fn test_clamp_to_range_nan() {
522        let result = clamp_to_range(f64::NAN, 0.0, 10.0);
523        assert!(result.is_err());
524    }
525
526    #[test]
527    fn test_clamp_to_range_inverted() {
528        let result = clamp_to_range(5.0_f64, 10.0, 0.0);
529        assert!(result.is_err());
530    }
531
532    #[test]
533    fn test_clamp_to_range_exact_boundary() {
534        assert_eq!(clamp_to_range(0.0_f64, 0.0, 10.0).expect("ok"), 0.0);
535        assert_eq!(clamp_to_range(10.0_f64, 0.0, 10.0).expect("ok"), 10.0);
536    }
537
538    #[test]
539    fn test_clamp_to_range_f32() {
540        assert_eq!(clamp_to_range(100.0_f32, -1.0, 1.0).expect("ok"), 1.0);
541    }
542
543    // ---- MachineConstants tests ----
544
545    #[test]
546    fn test_machine_constants_f64() {
547        assert_eq!(f64::machine_epsilon(), f64::EPSILON);
548        assert_eq!(f64::min_positive_normal(), f64::MIN_POSITIVE);
549        assert_eq!(f64::max_finite(), f64::MAX);
550        assert_eq!(f64::mantissa_digits(), 53);
551        assert_eq!(f64::radix(), 2);
552        assert!(f64::min_positive_subnormal() > 0.0);
553        assert!(f64::min_positive_subnormal() < f64::MIN_POSITIVE);
554    }
555
556    #[test]
557    fn test_machine_constants_f32() {
558        assert_eq!(f32::machine_epsilon(), f32::EPSILON);
559        assert_eq!(f32::min_positive_normal(), f32::MIN_POSITIVE);
560        assert_eq!(f32::max_finite(), f32::MAX);
561        assert_eq!(f32::mantissa_digits(), 24);
562        assert!(f32::min_positive_subnormal() > 0.0);
563        assert!(f32::min_positive_subnormal() < f32::MIN_POSITIVE);
564    }
565
566    // ---- slices_approx_eq tests ----
567
568    #[test]
569    fn test_slices_approx_eq_basic() {
570        let a = [1.0_f64, 2.0, 3.0];
571        let b = [1.0 + 1e-12, 2.0 - 1e-12, 3.0];
572        assert!(slices_approx_eq(&a, &b, 1e-10));
573    }
574
575    #[test]
576    fn test_slices_approx_eq_different_lengths() {
577        let a = [1.0_f64, 2.0];
578        let b = [1.0_f64];
579        assert!(!slices_approx_eq(&a, &b, 1e-10));
580    }
581
582    #[test]
583    fn test_slices_approx_eq_not_equal() {
584        let a = [1.0_f64, 2.0];
585        let b = [1.0_f64, 3.0];
586        assert!(!slices_approx_eq(&a, &b, 0.5));
587    }
588
589    // ---- max_abs_difference tests ----
590
591    #[test]
592    fn test_max_abs_difference_basic() {
593        let a = [1.0_f64, 2.0, 3.0];
594        let b = [1.1, 2.0, 2.5];
595        let d = max_abs_difference(&a, &b).expect("ok");
596        assert!(approx_eq(d, 0.5, 1e-14));
597    }
598
599    #[test]
600    fn test_max_abs_difference_empty() {
601        let a: [f64; 0] = [];
602        let b: [f64; 0] = [];
603        assert_eq!(max_abs_difference(&a, &b).expect("ok"), 0.0);
604    }
605
606    #[test]
607    fn test_max_abs_difference_length_mismatch() {
608        let a = [1.0_f64];
609        let b = [1.0_f64, 2.0];
610        assert!(max_abs_difference(&a, &b).is_err());
611    }
612}