num-valid
num-valid is a Rust library designed for robust, generic, and high-performance numerical computation. It provides a safe and extensible framework for working with both real and complex numbers, addressing the challenges of floating-point arithmetic by ensuring correctness and preventing common errors like NaN propagation.
Key Features & Architecture
-
Safety by Construction with Validated Types: Instead of using raw primitives like
f64ornum::Complex<f64>directly,num-validencourages the use of validated wrappers likeRealValidatedandComplexValidated. 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
kernelsmodule):- The
RawScalarTrait,RawRealTrait, andRawComplexTraitdefine 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.
- The
- Layer 2: Validation Policies:
- The
NumKerneltrait 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.,f64vs.rug) and a set of safety rules (e.g.,StrictFinitePolicyvs.DebugValidationPolicy<StrictFinitePolicy>) with a single generic parameter. This dramatically simplifies writing generic code that can be configured for different safety and performance trade-offs.
- The
- Layer 3: Validated Wrappers:
RealValidated<K>andComplexValidated<K>are the primary user-facing types.- These are newtype wrappers that are guaranteed to hold a value that conforms to the
NumKernelK(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
RealValidatedis guaranteed to contain a value that has passed the validation policy, eliminating entire classes of errors (likeNaNpropagation) in user code.
- Layer 4: High-Level Abstraction Traits:
- The
FpScalartrait 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. RealScalarandComplexScalarare specialized sub-traits ofFpScalarthat serve as markers for real and complex numbers, respectively.- Generic code in a consumer crate is written against these high-level traits.
- The
RealValidatedandComplexValidatedstructs 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.
- The
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).
- Layer 1: Raw Trait Contracts (in the
-
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
f64andnum::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 typesrug::Floatandrug::Complexfrom the Rust libraryrug.
- 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
-
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
NeumaierSumfor its defaultstd::iter::Sumimplementation 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.
-
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.
Compiler Requirement: Rust Nightly
This library currently requires the nightly toolchain because it uses some unstable Rust features which, at the time of writing (September 2025), are not yet available in stable or beta releases.
If these features are stabilized in a future Rust release, the library will be updated to support the stable compiler.
To use the nightly toolchain, please run:
This will set your environment to use the nightly compiler, enabling compatibility with the current version of the library.
Getting Started
This guide will walk you through the basics of using num-valid.
1. Add num-valid to your Project
Add the following to your Cargo.toml (change the versions to the latest ones):
[]
= "0.2.0" # Change to the latest version
= "0.1.2" # Needed for the TryNew trait
To enable the arbitrary-precision backend, use the rug feature:
[]
= { = "0.2.0", = ["rug"] } # Change to the latest version
= "0.1.2" # Needed for the TryNew trait
2. Core Concept: Validated Types
The central idea in num-valid is to use validated types instead of raw primitives like f64. These are wrappers that guarantee their inner value is always valid (e.g., not NaN or Infinity) according to a specific policy.
The most common type you'll use is RealNative64StrictFinite, which wraps an f64 and ensures it's always finite, both in Debug and Release mode. For a similar type wrapping an f64 that ensures it's always finite, but with the validity checks executed only in Debug mode (providing a performance equal to the raw f64 type), you can use RealNative64StrictFiniteInDebug.
3. Your First Calculation
Let's perform a square root calculation. You'll need to bring the necessary traits into scope.
// Import traits for the constructor, the sqrt function and the sqrt errors.
use ;
use TryNew;
// 1. Create a validated number. try_new() returns a Result.
let x = try_new.unwrap;
// 2. Use the direct method for operations.
// This will panic if the operation is invalid (e.g., sqrt of a negative).
let sqrt_x = x.sqrt;
assert_eq!;
// 3. Use the `try_*` methods for error handling.
// This is the safe way to handle inputs that might be out of the function's domain.
let neg_x = try_new.unwrap;
let sqrt_neg_x_result = neg_x.try_sqrt;
// The operation fails gracefully, returning an Err.
assert!;
// The error gives information about the problem that occurred
assert!;
4. Writing Generic Functions
The real power of num-valid comes from writing generic functions that work with any supported numerical type. You can do this by using the FpScalar and RealScalar traits as bounds.
use ;
use One;
use TryNew;
// This function works for any type that implements RealScalar,
// including f64, RealNative64StrictFinite, and RealRugStrictFinite.
// Define a type alias for convenience
type MyReal = RealNative64StrictFinite;
// Call it with a validated f64 type.
let angle = try_from_f64.unwrap;
let identity = verify_trig_identity;
// The result should be very close to 1.0.
let one = one;
assert!;
5. Zero-Copy Conversions with Bytemuck
The library provides safe, zero-copy conversions between f64 byte representations and validated types through the bytemuck crate. This is useful for interoperability with binary data, serialization formats, or performance-critical code that works directly with byte arrays.
The validated types RealNative64StrictFinite and RealNative64StrictFiniteInDebug implement bytemuck::CheckedBitPattern and bytemuck::NoUninit, which allow safe conversions that automatically validate the bit pattern:
use RealNative64StrictFinite;
use try_from_bytes;
// Convert from f64 bytes to validated type (succeeds for valid values)
let value = 42.0_f64;
let bytes = value.to_ne_bytes;
let validated: &RealNative64StrictFinite = try_from_bytes.unwrap;
assert_eq!;
// Invalid values (NaN, Infinity, subnormals) are automatically rejected
let nan_bytes = f64NAN.to_ne_bytes;
let result: = try_from_bytes;
assert!; // Validation fails for NaN
// Works with slices too
let values = vec!;
let bytes = ;
// Convert back
let validated_slice: & =
try_cast_slice.unwrap;
assert_eq!;
This integration ensures:
- Safety: Invalid bit patterns (NaN, Infinity, subnormal) are rejected during conversion
- Zero-cost: When the bit pattern is valid, there's no runtime overhead compared to raw
f64 - Convenience: Seamless interoperability with binary formats and external data sources
If the rug feature is enabled, you could call the exact same function with a high-precision number changing only the definition of the alias type MyReal. For example, for real numbers with precision of 100 bits:
// we need to add the proper module
use RealRugStrictFinite;
// ... same modules as above
// ... same verify_trig_identity() function as above
// Define a type alias for convenience
type MyReal = ; // real number with precision of 100 bits
// Initialize it with an f64 value.
let angle = try_from_f64.unwrap;
let identity = verify_trig_identity;
// The result should be very close to 1.0.
let one = one;
assert!;
License
Copyright 2023-2025, C.N.R. - Consiglio Nazionale delle Ricerche
Licensed under either of
at your option.
Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in this project by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions.
License Notice for Optional Feature Dependencies (LGPL-3.0 Compliance)
If you enable the rug feature, this project will depend on the rug library, which is licensed under the LGPL-3.0 license. Activating this feature may introduce LGPL-3.0 requirements to your project. Please review the terms of the LGPL-3.0 license to ensure compliance.