| Crates.io | num-valid |
| lib.rs | num-valid |
| version | 0.3.2 |
| created_at | 2025-07-21 16:27:38.323018+00 |
| updated_at | 2026-01-12 17:12:46.187235+00 |
| description | 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. |
| homepage | |
| repository | https://gitlab.com/max.martinelli/num-valid |
| max_upload_size | |
| id | 1762246 |
| size | 1,609,120 |
Robust numerical computation in Rust with type-level safety guarantees.
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.
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>>(())
✅ 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
Add to your Cargo.toml:
[dependencies]
num-valid = "0.3"
try_create = "0.1"
| 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.3", features = ["rug", "backtrace"] }
The easiest way to create validated numbers is using the real!() and complex!() macros:
use num_valid::{real, complex};
use std::f64::consts::PI;
// Create validated real numbers from literals
let x = real!(3.14159);
let angle = real!(PI / 4.0);
let radius = real!(5.0);
// Create validated complex numbers (real, imaginary)
let z1 = complex!(1.0, 2.0); // 1 + 2i
let z2 = complex!(-3.0, 4.0); // -3 + 4i
// Use them in calculations - NaN/Inf propagation impossible!
let area = real!(PI) * radius * radius; // Type-safe area calculation
let product = z1 * z2; // Safe complex arithmetic
Why use macros?
vec![], format!(), etc.real!(f64::NAN)) panic immediately with descriptive messagesFor runtime values or when you need error handling:
use num_valid::{RealNative64StrictFinite, RealScalar};
// Fallible construction - returns Result
let x = RealNative64StrictFinite::try_from_f64(user_input)?;
// Panicking construction - for known-valid values
let pi = RealNative64StrictFinite::from_f64(std::f64::consts::PI);
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:
kernels module):
RawScalarTrait, RawRealTrait, and RawComplexTrait define the low-level, "unchecked" contract for any number type.unchecked_* methods.NumKernel trait is the bridge between the raw types and the validated wrappers.StrictFinitePolicy, DebugValidationPolicy, etc.). This allows the entire behavior of the validated types to be configured with a single generic parameter.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.RealValidated<K> and ComplexValidated<K> are the primary user-facing types.NumKernel K (and to the validation policies therein).unchecked_* method from the raw trait, and then perform a check on the output value before returning it. This ensures safety and correctness.RealValidated is guaranteed to contain a value that has passed the validation policy, eliminating entire classes of errors (like NaN propagation) in user code.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.RealValidated and ComplexValidated structs from Layer 3 are the concrete implementors of these traits.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:
f64 and num::Complex<f64> types, as described by the ANSI/IEEE Std 754-1985;--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.
real!() and complex!() for concise, validated numeric literalsf64 for speed, arbitrary-precision rug for accuracyln, log2, log10, ln_1p), exponentials (exp, exp_m1), hyperbolic functions, and moreAbsoluteTolerance, RelativeTolerance, PositiveRealScalar, NonNegativeRealScalar for domain-specific validationCopy implementation when underlying types support itThe library uses a sophisticated 4-layer design:
Low-level unchecked_* operations on primitives (f64, Complex<f64>, rug::Float, etc.). These assume caller-validated inputs for maximum performance.
The NumKernel trait bundles raw types with validation policies. Choose the right policy for your use case:
| Type Alias | Policy | Debug Build | Release Build | Use Case |
|---|---|---|---|---|
RealNative64StrictFinite |
StrictFinitePolicy |
✅ Validates | ✅ Validates | Safety-critical code, HashMap/BTreeMap keys |
RealNative64StrictFiniteInDebug |
DebugValidationPolicy |
✅ Validates | ❌ No validation | Performance-critical hot paths |
RealRugStrictFinite<P> |
StrictFinitePolicy |
✅ Validates | ✅ Validates | High-precision calculations |
Key Differences:
Eq + Hash + Ord (safe for HashMap/BTreeMap keys, sorted collections)RealValidated<K> and ComplexValidated<K> enforce correctness:
unchecked_* → validate outputRealNative64StrictFinite, ComplexRugStrictFinite<100>, etc.Generic interface for all scalars:
FpScalar - Universal scalar traitRealScalar - Real number specializationComplexScalar - Complex number specializationThis design separates performance-critical operations from safety guarantees, enabling both high speed and correctness.
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.
This library requires Rust nightly due to use of unstable features:
trait_aliaserror_generic_member_accessSet up nightly:
rustup install nightly
rustup override set nightly
The library will support stable Rust once these features are stabilized.
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>>(())
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>>(())
Enable the rug feature for arbitrary-precision arithmetic:
[dependencies]
num-valid = { version = "0.3", 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>>(())
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();
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());
Comprehensive error handling with specific error types:
use num_valid::{RealNative64StrictFinite, RealScalar, functions::Sqrt};
use num_valid::functions::{SqrtRealErrors, SqrtRealInputErrors};
// Pattern 1: Propagate errors with ?
fn compute_radius(area: f64) -> Result<RealNative64StrictFinite, Box<dyn std::error::Error>> {
let validated_area = RealNative64StrictFinite::try_from_f64(area)?;
let pi = RealNative64StrictFinite::try_from_f64(std::f64::consts::PI)?;
let radius_squared = validated_area / pi;
Ok(radius_squared.try_sqrt()?)
}
// Pattern 2: Match on specific error variants
fn sqrt_with_fallback(x: RealNative64StrictFinite) -> RealNative64StrictFinite {
match x.try_sqrt() {
Ok(result) => result,
Err(SqrtRealErrors::Input { source: SqrtRealInputErrors::NegativeValue { .. } }) => {
// Handle negative input: return sqrt(|x|)
(-x).sqrt()
}
Err(e) => panic!("Unexpected error: {e}"),
}
}
# Ok::<(), Box<dyn std::error::Error>>(())
The scalars module provides validated wrapper types for common constrained numeric patterns:
use num_valid::{RealNative64StrictFinite, RealScalar};
use num_valid::scalars::{
AbsoluteTolerance, RelativeTolerance,
PositiveRealScalar, NonNegativeRealScalar
};
use try_create::TryNew;
// AbsoluteTolerance: Non-negative tolerance (≥ 0)
let abs_tol = AbsoluteTolerance::try_new(
RealNative64StrictFinite::from_f64(1e-10)
)?;
// RelativeTolerance: Non-negative with conversion to absolute
let rel_tol = RelativeTolerance::try_new(
RealNative64StrictFinite::from_f64(1e-6)
)?;
let reference = RealNative64StrictFinite::from_f64(1000.0);
let abs_from_rel = rel_tol.absolute_tolerance(reference); // = 1e-3
// PositiveRealScalar: Strictly positive (> 0), rejects zero
let step_size = PositiveRealScalar::try_new(
RealNative64StrictFinite::from_f64(0.001)
)?;
// PositiveRealScalar::try_new(RealNative64StrictFinite::from_f64(0.0)); // Error!
// NonNegativeRealScalar: Non-negative (≥ 0), accepts zero
let threshold = NonNegativeRealScalar::try_new(
RealNative64StrictFinite::from_f64(0.0)
)?; // OK!
# Ok::<(), Box<dyn std::error::Error>>(())
Key differences:
PositiveRealScalar: Rejects zero (useful for divisors, step sizes)NonNegativeRealScalar: Accepts zero (useful for thresholds, counts)RealScalar backend (RealNative64StrictFinite, RealRugStrictFinite<P>, etc.)Here are common mistakes to avoid when using num-valid:
Clone for Reused ValuesValidated types consume self in mathematical operations. Clone before reuse:
# use num_valid::{real, functions::{Sin, Cos}};
let x = real!(1.0);
// ❌ Wrong - x is moved
// let sin_x = x.sin();
// let cos_x = x.cos(); // Error: x already moved!
// ✅ Correct - clone before move
let sin_x = x.clone().sin();
let cos_x = x.cos();
from_f64 with Untrusted Inputfrom_f64 panics on invalid input. Use try_from_f64 for user data:
# use num_valid::{RealNative64StrictFinite, RealScalar};
fn process_user_input(user_input: f64) -> Result<(), Box<dyn std::error::Error>> {
// ❌ Dangerous - panics on NaN/Inf
// let x = RealNative64StrictFinite::from_f64(user_input);
// ✅ Safe - handles errors gracefully
let x = RealNative64StrictFinite::try_from_f64(user_input)?;
println!("Validated: {}", x);
Ok(())
}
# process_user_input(42.0).unwrap();
0.1 cannot be exactly represented in binary floating-point:
# #[cfg(feature = "rug")] {
use num_valid::RealRugStrictFinite;
// ❌ May fail - 0.1 not exactly representable at 53-bit precision
let x = RealRugStrictFinite::<53>::try_from_f64(0.1); // Error!
// ✅ Use higher precision or handle the error appropriately
let x = RealRugStrictFinite::<100>::try_from_f64(0.5)?; // 0.5 is exact
# }
Choose the right policy for your use case:
| Policy | Debug Build | Release Build | Use Case |
|---|---|---|---|
StrictFinite |
✅ Validates | ✅ Validates | Safety-critical code |
StrictFiniteInDebug |
✅ Validates | ❌ No validation | Performance-critical code |
use num_valid::{RealNative64StrictFinite, RealNative64StrictFiniteInDebug};
// For safety-critical code (always validated)
type SafeReal = RealNative64StrictFinite;
// For performance-critical code (validated only in debug builds)
type FastReal = RealNative64StrictFiniteInDebug;
#[must_use] WarningsAll try_* methods return Result. Don't ignore them:
# use num_valid::{RealNative64StrictFinite, functions::Sqrt};
# use try_create::TryNew;
# let x = RealNative64StrictFinite::try_new(4.0).unwrap();
// ❌ Compiler warning: unused Result
// x.try_sqrt();
// ✅ Handle the result
let sqrt_x = x.try_sqrt()?;
# Ok::<(), Box<dyn std::error::Error>>(())
📚 API Documentation - Complete API reference with examples
🏗️ Architecture Guide - Deep dive into the 4-layer design, implementation patterns, and how to contribute
📖 Cookbook - Practical patterns and examples for common use cases
🔄 Migration Guide - Transitioning from raw primitives (f64, Complex<f64>) to validated types
Contributions are welcome! Before submitting PRs, please familiarize yourself with the project's architecture and conventions.
📚 Architecture Guide - Required reading for all contributors
📖 Additional Documentation
src/functions/ for mathematical function examplesunchecked_* for Layer 1, trait methods for Layer 4cargo test --all-features and cargo doc --no-depscargo fmt