num-valid 0.3.3

A robust numerical library providing validated types for real and complex numbers to prevent common floating-point errors like NaN propagation. Features a generic, layered architecture with support for native f64 and optional arbitrary-precision arithmetic.
Documentation
#![deny(rustdoc::broken_intra_doc_links)]

//! Absolute value computation for validated scalar types.
//!
//! This module provides the [`Abs`] trait for computing absolute values of both
//! real and complex numbers.

use crate::{
    core::policies::StrictFinitePolicy,
    functions::FunctionErrors,
    kernels::{RawComplexTrait, RawScalarTrait},
};
use num::Complex;
use thiserror::Error;
use try_create::ValidationPolicy;

//------------------------------------------------------------------------------------------------
/// Errors that can occur during the input validation phase when attempting to compute
/// the absolute value of a number.
///
/// This enum is used as a source for the `Input` variant of composed error types like
/// [`AbsRealErrors`] or [`AbsComplexErrors`]. It is generic over `RawScalar`, which
/// represents the specific scalar type being validated (e.g., [`f64`], [`Complex<f64>`], etc.).
#[derive(Debug, Error)]
pub enum AbsInputErrors<RawScalar: RawScalarTrait> {
    /// The input value failed basic validation checks.
    ///
    /// This error occurs if the input value itself is considered invalid
    /// according to the validation policy (e.g., [`StrictFinitePolicy`]),
    /// such as being NaN or Infinity, before the absolute value calculation
    /// is attempted.
    #[error("the input value is invalid!")]
    ValidationError {
        /// The underlying validation error from the input type.
        ///
        /// This provides more specific details about why the input value
        /// was considered invalid.
        #[source]
        #[backtrace]
        source: <RawScalar as RawScalarTrait>::ValidationErrors,
    },
}

/// A type alias for [`FunctionErrors`], specialized for errors that can occur during
/// the computation of the absolute value of a real number.
///
/// It combines input validation errors specific to the `abs` operation on real numbers
/// with potential validation errors of the resulting real number.
///
/// # Generic Parameters
///
/// - `RawReal`: A type that implements [`RawRealTrait`](crate::kernels::RawRealTrait). This defines:
///   - The error type for the input: `AbsInputErrors<RawReal>`.
///   - The raw error type for the output: `<RawReal as RawScalarTrait>::ValidationErrors`.
///
/// # Variants
///
/// This type alias wraps [`FunctionErrors`], which has the following variants:
/// - `Input { source: AbsInputErrors<RawReal> }`: Indicates that the input real number
///   provided for absolute value computation was invalid. The `source` field contains
///   an [`AbsInputErrors`] detailing the specific input failure.
/// - `Output { source: <RawReal as RawScalarTrait>::ValidationErrors }`: Indicates that the computed absolute value (a real number)
///   itself failed validation. This typically means the result of the `abs` operation yielded
///   a non-finite value (NaN or Infinity). The `source` field contains the raw validation error
///   for the output.
pub type AbsRealErrors<RawReal> =
    FunctionErrors<AbsInputErrors<RawReal>, <RawReal as RawScalarTrait>::ValidationErrors>;

/// A type alias for [`FunctionErrors`], specialized for errors that can occur during
/// the computation of the absolute value (or modulus) of a complex number.
///
/// It combines input validation errors specific to the `abs` operation on complex numbers
/// with potential validation errors of the resulting real number (the modulus).
///
/// # Generic Parameters
///
/// - `RawComplex`: A type that implements [`RawComplexTrait`]. This defines:
///   - The error type for the input complex number: `AbsInputErrors<RawComplex>`.
///   - The raw error type for the output real number: `<<RawComplex as RawComplexTrait>::RawReal as RawScalarTrait>::ValidationErrors`.
///
/// # Variants
///
/// This type alias wraps [`FunctionErrors`], which has the following variants:
/// - `Input { source: AbsInputErrors<RawComplex> }`: Indicates that the input complex number
///   provided for absolute value computation was invalid. The `source` field contains
///   an [`AbsInputErrors`] detailing the specific input failure.
/// - `Output { source: <<RawComplex as RawComplexTrait>::RawReal as RawScalarTrait>::ValidationErrors }`: Indicates that the computed
///   absolute value (a real number) itself failed validation. This typically means the result of the
///   `norm()` operation yielded a non-finite value. The `source` field contains the raw validation
///   error for the output real number.
pub type AbsComplexErrors<RawComplex> = FunctionErrors<
    AbsInputErrors<RawComplex>,
    <<RawComplex as RawComplexTrait>::RawReal as RawScalarTrait>::ValidationErrors,
>;

/// This trait provides the interface for the function used to compute the *absolute value*
/// (also known as modulus or magnitude) of a number, which can be real or complex.
///
/// The absolute value of a real number `x` is `|x|`, which is `x` if `x` is non-negative,
/// and `-x` if `x` is negative.
/// The absolute value (or modulus) of a complex number `z = a + b*i` is `sqrt(a^2 + b^2)`.
/// In both cases, the result is always a non-negative real number.
///
/// This trait provides two methods:
/// - [`try_abs`](Abs::try_abs): A fallible version that performs validation on the input
///   (and potentially the output) and returns a [`Result`]. This is the preferred method
///   when robust error handling is required.
/// - [`abs`](Abs::abs): A convenient infallible version that panics if `try_abs` would return
///   an error in debug builds, or directly computes the value in release builds.
///   Use this when the input is known to be valid or when panicking on error is acceptable.
///
/// # Associated types
///
/// - `Output`: The type of the result of the absolute value operation. This is constrained
///   by [`RealScalar`](crate::RealScalar) to ensure it's a real number type.
/// - `Error` : The error type that can be returned by the [`try_abs`](Abs::try_abs) method.
pub trait Abs: Sized {
    /// The output type of the *absolute value* function.
    ///
    /// This is always a real number type, even when the input `Self` is a complex number.
    /// It must implement the [`RealScalar`](crate::RealScalar) trait.
    ///
    // TODO: the type Output must be bounded to be a RealScalar!
    //    type Output: RealScalar;
    type Output: Sized;

    /// The error type that can be returned by the [`try_abs`](Abs::try_abs) method.
    ///
    /// This type must implement [`std::error::Error`]. Specific implementations will
    /// use types like [`AbsRealErrors`] or [`AbsComplexErrors`] to provide detailed
    /// information about the failure.
    type Error: std::error::Error;

    /// Attempts to compute the absolute value of `self`.
    #[must_use = "this `Result` may contain an error that should be handled"]
    fn try_abs(self) -> Result<Self::Output, Self::Error>;

    /// Returns the *absolute value* of `self`.
    fn abs(self) -> Self::Output;
}

impl Abs for f64 {
    type Output = f64;
    type Error = AbsRealErrors<f64>;

    /// Attempts to compute the absolute value of `self`.
    ///
    /// This method performs validation on the input value according to the
    /// [`StrictFinitePolicy`] (i.e., the input must not be NaN, Infinity, or subnormal).
    /// If the input is valid, it computes the absolute value. The result is also
    /// validated to ensure it is a finite real number.
    ///
    /// # Returns
    ///
    /// - `Ok(Self::Output)`: If the input is valid and the absolute value is successfully computed
    ///   and validated.
    /// - `Err(Self::Error)`: If the input fails validation, or if the computed absolute value
    ///   fails validation. The specific error variant will indicate the cause of the failure.
    #[inline(always)]
    fn try_abs(self) -> Result<f64, Self::Error> {
        StrictFinitePolicy::<f64, 53>::validate(self)
            .map_err(|e| AbsInputErrors::ValidationError { source: e }.into())
            .and_then(|v| {
                StrictFinitePolicy::<f64, 53>::validate(f64::abs(v))
                    .map_err(|e| AbsRealErrors::Output { source: e })
            })
    }

    /// Returns the *absolute value* of `self`.
    ///
    /// This method provides a convenient way to compute the absolute value.
    ///
    /// # Behavior
    ///
    /// - In **debug builds** (`#[cfg(debug_assertions)]`): This method calls
    ///   [`try_abs()`](Abs::try_abs) and unwraps the result. It will panic if `try_abs`
    ///   returns an error (e.g., if the input is NaN or Infinity).
    /// - In **release builds** (`#[cfg(not(debug_assertions))]`): This method
    ///   calls `f64::abs()` directly for performance, bypassing the
    ///   validations performed by `try_abs`.
    ///
    /// # Panics
    ///
    /// This method will panic in debug builds if `try_abs()` would return an `Err`.
    /// In release builds, the behavior for invalid inputs (like NaN or Infinity)
    /// will match `f64::abs()` (e.g., `f64::NAN.abs()` is `NAN`).
    #[inline(always)]
    fn abs(self) -> f64 {
        #[cfg(debug_assertions)]
        {
            self.try_abs()
                .expect("Error in the computation of the absolute value in debug mode")
        }
        #[cfg(not(debug_assertions))]
        {
            f64::abs(self)
        }
    }
}

impl Abs for Complex<f64> {
    type Output = f64;
    type Error = AbsComplexErrors<Complex<f64>>;

    /// Attempts to compute the absolute value (modulus) of `self`.
    ///
    /// This method performs validation on the input value according to the
    /// [`StrictFinitePolicy`] (i.e., both real and imaginary parts must be finite).
    /// If the input is valid, it computes the modulus. The result is also
    /// validated to ensure it is a finite real number.
    #[inline(always)]
    fn try_abs(self) -> Result<f64, AbsComplexErrors<Complex<f64>>> {
        StrictFinitePolicy::<Complex<f64>, 53>::validate(self)
            .map_err(|e| AbsInputErrors::ValidationError { source: e }.into())
            .and_then(|v| {
                let norm = v.norm();
                StrictFinitePolicy::<f64, 53>::validate(norm).map_err(|e| AbsComplexErrors::<
                    Complex<f64>,
                >::Output {
                    source: e,
                })
            })
    }

    /// Returns the *absolute value* of `self`.
    ///
    /// # Behavior
    ///
    /// - In **debug builds** (`#[cfg(debug_assertions)]`): This method calls
    ///   [`try_abs()`](Abs::try_abs) and unwraps the result. It will panic if `try_abs`
    ///   returns an error.
    /// - In **release builds** (`#[cfg(not(debug_assertions))]`): This method calls
    ///   `self.norm()` directly for performance.
    ///
    /// # Panics
    ///
    /// This method will panic in debug builds if `try_abs()` would return an `Err`.
    #[inline(always)]
    fn abs(self) -> f64 {
        #[cfg(debug_assertions)]
        {
            self.try_abs()
                .expect("Error in the computation of the absolute value in debug mode")
        }
        #[cfg(not(debug_assertions))]
        {
            self.norm()
        }
    }
}

//------------------------------------------------------------------------------------------------

//------------------------------------------------------------------------------------------------
#[cfg(test)]
mod tests {
    use super::*;
    use num::Complex;

    mod abs {
        use super::*;

        mod native64 {
            use super::*;

            mod real {
                use super::*;

                #[test]
                fn abs_valid() {
                    let value = 4.0;
                    assert_eq!(value.try_abs().unwrap(), 4.0);
                    assert_eq!(value.abs(), 4.0);
                }

                #[test]
                fn abs_negative() {
                    let value = -4.0;
                    assert_eq!(value.try_abs().unwrap(), 4.0);
                    assert_eq!(value.abs(), 4.0);
                }

                #[test]
                fn abs_zero() {
                    let value = 0.0;
                    assert_eq!(value.try_abs().unwrap(), 0.0);
                    assert_eq!(value.abs(), 0.0);
                }

                #[test]
                fn abs_nan() {
                    let value = f64::NAN;
                    let result = value.try_abs().unwrap_err();
                    assert!(matches!(result, AbsRealErrors::Input { .. }));
                }

                #[test]
                fn abs_infinity() {
                    let value = f64::INFINITY;
                    assert!(matches!(
                        value.try_abs().unwrap_err(),
                        AbsRealErrors::Input { .. }
                    ));
                }

                #[test]
                fn abs_subnormal() {
                    let value = f64::MIN_POSITIVE / 2.0;
                    assert!(matches!(
                        value.try_abs().unwrap_err(),
                        AbsRealErrors::Input { .. }
                    ));
                }
            }

            mod complex {
                use super::*;

                #[test]
                fn abs_valid() {
                    let value = Complex::new(4.0, 0.0);
                    assert_eq!(value.try_abs().unwrap(), 4.0);
                    assert_eq!(value.abs(), 4.0);
                }

                #[test]
                fn abs_negative() {
                    let value = Complex::new(-4.0, 0.0);
                    assert_eq!(value.try_abs().unwrap(), 4.0);
                    assert_eq!(value.abs(), 4.0);
                }

                #[test]
                fn abs_zero() {
                    let value = Complex::new(0.0, 0.0);
                    assert_eq!(value.try_abs().unwrap(), 0.0);
                    assert_eq!(value.abs(), 0.0);
                }

                #[test]
                fn abs_nan() {
                    let value = Complex::new(f64::NAN, 0.0);
                    assert!(matches!(
                        value.try_abs(),
                        Err(AbsComplexErrors::<Complex<f64>>::Input { .. })
                    ));

                    let value = Complex::new(0.0, f64::NAN);
                    assert!(matches!(
                        value.try_abs(),
                        Err(AbsComplexErrors::<Complex<f64>>::Input { .. })
                    ));
                }

                #[test]
                fn abs_infinity() {
                    let value = Complex::new(f64::INFINITY, 0.0);
                    assert!(matches!(
                        value.try_abs(),
                        Err(AbsComplexErrors::<Complex<f64>>::Input { .. })
                    ));

                    let value = Complex::new(0.0, f64::INFINITY);
                    assert!(matches!(
                        value.try_abs(),
                        Err(AbsComplexErrors::<Complex<f64>>::Input { .. })
                    ));
                }

                #[test]
                fn abs_subnormal() {
                    let value = Complex::new(f64::MIN_POSITIVE / 2.0, 0.0);
                    assert!(matches!(
                        value.try_abs(),
                        Err(AbsComplexErrors::<Complex<f64>>::Input { .. })
                    ));

                    let value = Complex::new(0.0, f64::MIN_POSITIVE / 2.0);
                    assert!(matches!(
                        value.try_abs(),
                        Err(AbsComplexErrors::<Complex<f64>>::Input { .. })
                    ));
                }
            }
        }
    }
}
//------------------------------------------------------------------------------------------------