num-valid 0.2.5

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

num-valid

Crates.io Docs.rs License: MIT OR Apache-2.0 Pipeline Status Coverage GitLab last commit

Robust numerical computation in Rust with type-level safety guarantees.

Why num-valid?

Floating-point arithmetic is tricky. NaN propagates silently, overflow produces Infinity, and precision loss is common. num-valid solves these problems by enforcing correctness at the type level.

Quick Example

use num_valid::RealNative64StrictFinite;
use try_create::TryNew;

// ✅ Valid value - compiles and runs safely
let x = RealNative64StrictFinite::try_new(4.0)?;
let sqrt_x = x.sqrt(); // Always safe - no NaN surprises!

// ❌ Invalid value - caught immediately
let bad = RealNative64StrictFinite::try_new(f64::NAN); // Returns Err
# Ok::<(), Box<dyn std::error::Error>>(())

Core Benefits

Type Safety - No more NaN surprises
Generic Precision - Switch between f64 and arbitrary-precision seamlessly
Zero Cost - Validation overhead only where you choose
Complete - Trigonometry, logarithms, complex numbers, and more
Production Ready - 96%+ test coverage, comprehensive error handling

Installation

Add to your Cargo.toml:

[dependencies]
num-valid = "0.2"
try_create = "0.1"

Feature Flags

Feature Description
rug Enables arbitrary-precision arithmetic using rug. See LGPL-3.0 notice below.
backtrace Enables backtrace capture in error types for debugging. Disabled by default for performance.

Example with features:

[dependencies]
num-valid = { version = "0.2", features = ["rug", "backtrace"] }

Quick Start

  • Safety by Construction with Validated Types: Instead of using raw primitives like f64 or num::Complex<f64> directly, num-valid encourages the use of validated wrappers like RealValidated and ComplexValidated. These types guarantee that the value they hold is always valid (e.g., finite) according to a specific policy, eliminating entire classes of numerical bugs.

  • Support for Real and Complex Numbers: The library supports both real and complex numbers, with specific validation policies for each type.

  • Layered and Extensible Design: The library has a well-defined, layered, and highly generic architecture. It abstracts the concept of a "numerical kernel" (the underlying number representation and its operations) from the high-level mathematical traits.

    The architecture can be understood in four main layers:

    • Layer 1: Raw Trait Contracts (in the kernels module):
      • The RawScalarTrait, RawRealTrait, and RawComplexTrait define the low-level, "unchecked" contract for any number type.
      • These traits are the foundation, providing a standard set of unchecked_* methods.
      • The contract is that the caller must guarantee the validity of inputs. This is a strong design choice, separating the raw, potentially unsafe operations from the validated, safe API.
      • Why? This design separates the pure, high-performance computational logic from the safety and validation logic. It creates a clear, minimal contract for backend implementors and allows the validated wrappers in Layer 3 to be built on a foundation of trusted, high-speed operations.
    • Layer 2: Validation Policies:
      • The NumKernel trait is the bridge between the raw types and the validated wrappers.
      • It bundles together the raw real/complex types and their corresponding validation policies (e.g., StrictFinitePolicy, DebugValidationPolicy, etc.). This allows the entire behavior of the validated types to be configured with a single generic parameter.
      • Why? It acts as the central policy configuration point. By choosing a NumKernel, a user selects both a numerical backend (e.g., f64 vs. rug) and a set of safety rules (e.g., StrictFinitePolicy vs. DebugValidationPolicy<StrictFinitePolicy>) with a single generic parameter. This dramatically simplifies writing generic code that can be configured for different safety and performance trade-offs.
    • Layer 3: Validated Wrappers:
      • RealValidated<K> and ComplexValidated<K> are the primary user-facing types.
      • These are newtype wrappers that are guaranteed to hold a value that conforms to the NumKernel K (and to the validation policies therein).
      • They use extensive macros to implement high-level traits. The logic is clean: perform a check (if necessary) on the input value, then call the corresponding unchecked_* method from the raw trait, and then perform a check on the output value before returning it. This ensures safety and correctness.
      • Why? These wrappers use the newtype pattern to enforce correctness at the type level. By construction, an instance of RealValidated is guaranteed to contain a value that has passed the validation policy, eliminating entire classes of errors (like NaN propagation) in user code.
    • Layer 4: High-Level Abstraction Traits:
      • The FpScalar trait is the central abstraction, defining a common interface for all scalar types. It uses an associated type sealed type (Kind), to enforce that a scalar is either real or complex, but not both.
      • RealScalar and ComplexScalar are specialized sub-traits of FpScalar that serve as markers for real and complex numbers, respectively.
      • Generic code in a consumer crate is written against these high-level traits.
      • The RealValidated and ComplexValidated structs from Layer 3 are the concrete implementors of these traits.
      • Why? These traits provide the final, safe, and generic public API. Library consumers write their algorithms against these traits, making their code independent of the specific numerical kernel being used.

    This layered approach is powerful, providing both high performance (by using unchecked methods internally) and safety (through the validated wrappers). The use of generics and traits makes it extensible to new numerical backends (as demonstrated by the rug implementation).

  • Multiple Numerical Backends. At the time of writing, 2 numerical backends can be used:

    • the standard (high-performance) numerical backend is the one in which the raw floating point and complex numbers are described by the Rust's native f64 and num::Complex<f64> types, as described by the ANSI/IEEE Std 754-1985;
    • an optional (high-precision) numerical backend is available if the library is compiled with the optional flag --features=rug, and uses the arbitrary precision raw types rug::Float and rug::Complex from the Rust library rug.
  • Comprehensive Mathematical Library. It includes a wide range of mathematical functions for trigonometry, logarithms, exponentials, and more, all implemented as traits (e.g., Sin, Cos, Sqrt) and available on the validated types.

  • Numerically Robust Implementations. The library commits to numerical accuracy. As an example, by using NeumaierSum for its default std::iter::Sum implementation to minimize precision loss.

  • Robust Error Handling: The library defines detailed error types for various numerical operations, ensuring that invalid inputs and outputs are properly handled and reported. Errors are categorized into input and output errors, with specific variants for different types of numerical issues such as division by zero, invalid values, and subnormal numbers.

  • Conditional Backtrace Capture: Backtrace capture in error types is disabled by default for maximum performance. Enable the backtrace feature flag for debugging to get full stack traces in error types.

  • Conditional Copy Implementation: Validated types (RealValidated<K>, ComplexValidated<K>) automatically implement Copy when their underlying raw types support it. This enables zero-cost pass-by-value semantics for native 64-bit types while correctly requiring Clone for non-Copy backends like rug.

  • Comprehensive Documentation: The library includes detailed documentation for each struct, trait, method, and error type, making it easy for users to understand and utilize the provided functionality. Examples are provided for key functions to demonstrate their usage and expected behavior.

Key Features

  • Type-Level Safety: Validated wrappers prevent NaN/Infinity propagation at compile time
  • Dual Backends: Native f64 for speed, arbitrary-precision rug for accuracy
  • Complete Math Library: Trigonometry, logarithms, exponentials, hyperbolic functions, and more
  • Generic API: Write once, run with any precision level
  • Robust Error Handling: Detailed error types with optional backtrace support
  • Numerically Accurate: Uses Neumaier compensated summation to minimize precision loss
  • Zero-Copy Conversions: Efficient bytemuck integration for binary data
  • Conditional Copy: Automatic Copy implementation when underlying types support it
  • Production Ready: 96%+ test coverage, comprehensive documentation

Architecture (For Library Developers)

The library uses a sophisticated 4-layer design:

Layer 1: Raw Trait Contracts

Low-level unchecked_* operations on primitives (f64, Complex<f64>, rug::Float, etc.). These assume caller-validated inputs for maximum performance.

Layer 2: Validation Policies

The NumKernel trait bundles raw types with validation policies like:

  • StrictFinitePolicy - Always validates (debug + release)
  • DebugValidationPolicy<P> - Validates only in debug mode

Layer 3: Validated Wrappers

RealValidated<K> and ComplexValidated<K> enforce correctness:

  • Validate input → call unchecked_* → validate output
  • Type aliases: RealNative64StrictFinite, ComplexRugStrictFinite<100>, etc.

Layer 4: High-Level Traits

Generic interface for all scalars:

  • FpScalar - Universal scalar trait
  • RealScalar - Real number specialization
  • ComplexScalar - Complex number specialization

This design separates performance-critical operations from safety guarantees, enabling both high speed and correctness.

Why This Design?

Layer 1 separates pure computational logic from validation, creating a minimal contract for backend implementors and enabling high-performance operations.

Layer 2 acts as the central policy configuration point. By choosing a NumKernel, you select both a numerical backend and safety rules with a single generic parameter.

Layer 3 uses the newtype pattern to enforce correctness at the type level. By construction, instances are guaranteed valid, eliminating entire error classes.

Layer 4 provides the final safe, generic public API. Library consumers write algorithms against these traits, making code independent of the specific numerical kernel.

This layered approach provides both high performance (unchecked methods internally) and safety (validated wrappers). Generics and traits make it extensible to new numerical backends.

Compiler Requirement

This library requires Rust nightly due to use of unstable features:

  • trait_alias
  • associated_const_equality
  • error_generic_member_access

Set up nightly:

rustup install nightly
rustup override set nightly

The library will support stable Rust once these features are stabilized.

2. Basic Usage

The central idea in num-valid is to use validated types instead of raw primitives like f64. These wrappers guarantee their inner value is always valid (e.g., not NaN or Infinity).

Most common type: RealNative64StrictFinite - wraps f64 with strict finite validation.

use num_valid::{RealNative64StrictFinite, functions::Sqrt};
use try_create::TryNew;

// Create a validated number
let x = RealNative64StrictFinite::try_new(4.0)?;

// Panicking version - for known-valid inputs
let sqrt_x = x.sqrt();
assert_eq!(*sqrt_x.as_ref(), 2.0);

// Fallible version - for runtime validation
let neg = RealNative64StrictFinite::try_new(-4.0)?;
match neg.try_sqrt() {
    Ok(result) => println!("sqrt = {}", result),
    Err(e) => println!("Error: {}", e), // "sqrt of negative number"
}
# Ok::<(), Box<dyn std::error::Error>>(())

4. Generic Functions

Write once, run with any precision:

use num_valid::{RealScalar, RealNative64StrictFinite, functions::{Sin, Cos, Abs}};
use num::One;

// Generic function works with any RealScalar type
fn pythagorean_identity<T: RealScalar>(angle: T) -> T {
    let sin_x = angle.clone().sin();
    let cos_x = angle.cos();
    (sin_x.clone() * sin_x) + (cos_x.clone() * cos_x)
}

// Use with native f64 precision
let angle = RealNative64StrictFinite::try_from_f64(0.5)?;
let result = pythagorean_identity(angle);
let one = RealNative64StrictFinite::one();

// The result should be very close to 1.0 (sin²x + cos²x = 1)
let diff = (result - one).abs();
assert!(*diff.as_ref() < 1e-15);
# Ok::<(), Box<dyn std::error::Error>>(())

5. Arbitrary Precision (Optional)

Enable the rug feature for arbitrary-precision arithmetic:

[dependencies]
num-valid = { version = "0.2", features = ["rug"] }

Then use the same generic function with high precision:

# #[cfg(feature = "rug")] {
use num_valid::RealRugStrictFinite;
use num::One;

// Same generic function from example 4
# use num_valid::RealScalar;
# fn pythagorean_identity<T: RealScalar>(angle: T) -> T {
#     let sin_x = angle.clone().sin();
#     let cos_x = angle.cos();
#     (sin_x.clone() * sin_x) + (cos_x.clone() * cos_x)
# }

// Use with 200-bit precision (≈60 decimal digits)
type HighPrecision = RealRugStrictFinite<200>;

let angle = HighPrecision::try_from_f64(0.5)?;
let result = pythagorean_identity(angle);
let one = HighPrecision::one();

// More accurate than f64!
let diff = (result - one).abs();
let threshold = HighPrecision::try_from_f64(1e-50)?;
assert!(*diff.as_ref() < *threshold.as_ref());
# }
# Ok::<(), Box<dyn std::error::Error>>(())

6. Complex Numbers

Full support for complex arithmetic:

use num_valid::{ComplexNative64StrictFinite, RealNative64StrictFinite};
use num_valid::functions::{Abs, Reciprocal, Exp, Ln, Sqrt, ComplexScalarConstructors, Conjugate};

// Create complex number: 3 + 4i
let re = RealNative64StrictFinite::from_f64(3.0);
let im = RealNative64StrictFinite::from_f64(4.0);
let z = ComplexNative64StrictFinite::new_complex(re, im);

// Complex operations
let magnitude = z.abs(); // |3 + 4i| = 5.0
assert_eq!(*magnitude.as_ref(), 5.0);

let conjugate = z.conjugate(); // 3 - 4i
let reciprocal = z.reciprocal(); // 1/z

// Complex math functions
let exp_z = z.exp();
let log_z = z.ln();
let sqrt_z = z.sqrt();

7. Zero-Copy Conversions

Safe, zero-copy conversions with bytemuck:

use num_valid::RealNative64StrictFinite;
use bytemuck::checked::try_from_bytes;

// From bytes to validated type
let bytes = 42.0_f64.to_ne_bytes();
let validated: &RealNative64StrictFinite = try_from_bytes(&bytes).unwrap();
assert_eq!(*validated.as_ref(), 42.0);

// Invalid values automatically rejected
let nan_bytes = f64::NAN.to_ne_bytes();
assert!(try_from_bytes::<RealNative64StrictFinite>(&nan_bytes).is_err());

Documentation

📚 API Documentation - Complete API reference with examples

🏗️ Architecture Guide - Deep dive into the 4-layer design, implementation patterns, and how to contribute


Contributing

Contributions are welcome! Please read the Architecture Guide to understand the design philosophy before submitting PRs.

Key resources for contributors:


License