From 146b7af41da6df8ee4f9a10e877c08f54a62672e Mon Sep 17 00:00:00 2001 From: GeEom Date: Sun, 11 Jan 2026 11:00:49 +0000 Subject: [PATCH] Type-safe domain encoding and stricter lints Introduce bounded types (NonNegative, UnitInterval, etc.) to encode mathematical invariants at the type level, eliminating expect/unwrap in favor of compile-time guarantees. Add comprehensive clippy denies and auto-generated accuracy table in README. Co-Authored-By: Claude Opus 4.5 --- Cargo.toml | 50 ++- README.md | 36 +- benches/benchmarks.rs | 2 +- src/bounded.rs | 342 ++++++++++++++++++ src/kernel/cordic.rs | 31 +- src/lib.rs | 5 +- src/ops/algebraic.rs | 34 +- src/ops/circular.rs | 26 +- src/ops/exponential.rs | 102 +++--- src/ops/hyperbolic.rs | 92 ++--- src/traits.rs | 52 ++- tests/unit/error.rs | 8 +- tests/unit/ops/algebraic.rs | 3 +- tests/unit/ops/circular.rs | 7 +- tests/unit/ops/exponential.rs | 30 +- tests/unit/ops/hyperbolic.rs | 32 +- tests/unit/smoke.rs | 16 +- tests/unit/tables/circular.rs | 6 +- tests/unit/tables/hyperbolic.rs | 6 +- tests/unit/traits.rs | 2 +- tests/unit/verification.rs | 13 +- .../accuracy-bench/src/functions/algebraic.rs | 22 +- .../accuracy-bench/src/functions/circular.rs | 130 +++++-- .../src/functions/exponential.rs | 110 ++++-- .../src/functions/hyperbolic.rs | 172 ++++++--- tools/accuracy-bench/src/lib.rs | 1 + tools/accuracy-bench/src/main.rs | 85 ++++- tools/accuracy-bench/src/metrics.rs | 34 +- tools/accuracy-bench/src/readme.rs | 222 ++++++++++++ tools/accuracy-bench/src/reference.rs | 72 +++- tools/accuracy-bench/src/report.rs | 8 +- tools/accuracy-bench/src/sampling.rs | 8 +- 32 files changed, 1398 insertions(+), 361 deletions(-) create mode 100644 src/bounded.rs create mode 100644 tools/accuracy-bench/src/readme.rs diff --git a/Cargo.toml b/Cargo.toml index 84e87aa..ddb669d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "fixed_analytics" -version = "0.3.0" +version = "0.4.0" edition = "2024" rust-version = "1.88" authors = ["David Gathercole"] @@ -40,27 +40,49 @@ lto = true codegen-units = 1 [lints.rust] -unsafe_code = "forbid" +rust_2024_compatibility = "deny" +future_incompatible = "deny" +nonstandard_style = "deny" +rust_2018_idioms = "deny" missing_docs = "deny" -rust_2024_compatibility = { level = "warn", priority = -1 } -future_incompatible = { level = "deny", priority = -1 } -nonstandard_style = { level = "deny", priority = -1 } -rust_2018_idioms = { level = "deny", priority = -1 } +unsafe_code = "deny" +unused = "deny" [lints.clippy] # Lint groups -all = { level = "warn", priority = -1 } -pedantic = { level = "warn", priority = -1 } -nursery = { level = "warn", priority = -1 } -cargo = { level = "warn", priority = -1 } +correctness = { level = "deny", priority = -1 } +suspicious = { level = "deny", priority = -1 } +complexity = { level = "deny", priority = -1 } +pedantic = { level = "deny", priority = -1 } +nursery = { level = "deny", priority = -1 } +cargo = { level = "deny", priority = -1 } +style = { level = "deny", priority = -1 } +perf = { level = "deny", priority = -1 } -# Safety-critical denies -unwrap_used = "deny" -expect_used = "deny" +allow_attributes_without_reason = "deny" +used_underscore_binding = "deny" +if_then_some_else_none = "deny" +missing_const_for_fn = "deny" +missing_errors_doc = "deny" +missing_panics_doc = "deny" indexing_slicing = "deny" +result_large_err = "deny" +shadow_unrelated = "deny" +wildcard_imports = "deny" +map_err_ignore = "deny" +enum_glob_use = "deny" +infinite_loop = "deny" +unimplemented = "deny" +print_stderr = "deny" +print_stdout = "deny" +expect_used = "deny" +unwrap_used = "deny" +empty_loop = "deny" +mem_forget = "deny" +dbg_macro = "deny" panic = "deny" todo = "deny" -unimplemented = "deny" +exit = "deny" # Allow specific pedantic lints that are too noisy module_name_repetitions = "allow" diff --git a/README.md b/README.md index 97647c8..394e4d4 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # fixed_analytics -Fixed-point trigonometric, hyperbolic, exponential, and algebraic functions via CORDIC. +Fixed-point mathematical functions which are accurate, fast, safe, and machine independent. [![Crates.io](https://img.shields.io/crates/v/fixed_analytics.svg)](https://crates.io/crates/fixed_analytics) [![CI](https://github.com/GeEom/fixed_analytics/actions/workflows/ci.yml/badge.svg)](https://github.com/GeEom/fixed_analytics/actions/workflows/ci.yml) @@ -30,14 +30,14 @@ Requires Rust 1.88 or later. ```toml [dependencies] -fixed_analytics = "0.3" +fixed_analytics = "0.4" ``` For `no_std` environments: ```toml [dependencies] -fixed_analytics = { version = "0.3", default-features = false } +fixed_analytics = { version = "0.4", default-features = false } ``` ## Available Functions @@ -51,3 +51,33 @@ fixed_analytics = { version = "0.3", default-features = false } Fallible functions return `Result` and fail on domain violations. Total functions return `T` directly and handle all inputs. +Functions are calculated via CORDIC, Newton-Raphson, and Taylor series techniques. + + +### Accuracy + +Relative error statistics measured against MPFR reference implementations. + +| Function | I16F16 Mean | I16F16 Median | I16F16 P95 | I32F32 Mean | I32F32 Median | I32F32 P95 | +|----------|-------------|---------------|------------|-------------|---------------|------------| +| sin | 6.19e-4 | 9.34e-5 | 1.30e-3 | 1.22e-8 | 1.79e-9 | 2.52e-8 | +| cos | 6.82e-4 | 1.02e-4 | 1.46e-3 | 1.30e-8 | 1.91e-9 | 2.83e-8 | +| tan | 2.47e-4 | 9.84e-5 | 6.70e-4 | 6.00e-9 | 1.89e-9 | 1.40e-8 | +| asin | 2.87e-4 | 5.93e-5 | 6.46e-4 | 5.34e-9 | 8.82e-10 | 1.03e-8 | +| acos | 3.61e-5 | 2.18e-5 | 1.14e-4 | 5.37e-10 | 3.19e-10 | 1.71e-9 | +| atan | 2.71e-5 | 2.21e-5 | 6.29e-5 | 3.69e-10 | 2.92e-10 | 8.74e-10 | +| sinh | 1.85e-4 | 1.39e-4 | 5.05e-4 | 1.05e-8 | 2.37e-9 | 9.47e-9 | +| cosh | 1.73e-4 | 1.23e-4 | 5.00e-4 | 1.03e-8 | 2.07e-9 | 9.16e-9 | +| tanh | 2.26e-5 | 1.38e-5 | 6.13e-5 | 1.67e-9 | 1.31e-10 | 1.22e-9 | +| coth | 1.74e-5 | 4.86e-6 | 7.23e-5 | 4.34e-10 | 1.39e-10 | 1.31e-9 | +| asinh | 6.44e-4 | 4.83e-4 | 1.75e-3 | 1.03e-8 | 7.59e-9 | 2.85e-8 | +| acosh | 6.74e-4 | 5.21e-4 | 1.80e-3 | 1.05e-8 | 7.96e-9 | 2.88e-8 | +| atanh | 3.01e-4 | 5.90e-5 | 6.25e-4 | 6.68e-9 | 1.32e-9 | 1.44e-8 | +| acoth | 2.10e-3 | 1.33e-3 | 6.67e-3 | 4.26e-8 | 2.62e-8 | 1.39e-7 | +| exp | 1.14e-2 | 7.72e-5 | 7.87e-2 | 1.91e-7 | 2.35e-9 | 1.30e-6 | +| ln | 1.35e-5 | 8.76e-6 | 2.97e-5 | 4.50e-10 | 3.48e-10 | 9.17e-10 | +| log2 | 1.33e-5 | 8.48e-6 | 2.92e-5 | 3.46e-10 | 2.24e-10 | 7.21e-10 | +| log10 | 1.44e-5 | 9.28e-6 | 3.14e-5 | 4.49e-10 | 3.27e-10 | 9.07e-10 | +| pow2 | 7.30e-4 | 5.68e-5 | 4.70e-3 | 1.15e-8 | 1.16e-9 | 7.38e-8 | +| sqrt | 1.77e-7 | 1.16e-7 | 4.74e-7 | 2.70e-12 | 1.78e-12 | 7.16e-12 | + \ No newline at end of file diff --git a/benches/benchmarks.rs b/benches/benchmarks.rs index 43dc5eb..a8786f5 100644 --- a/benches/benchmarks.rs +++ b/benches/benchmarks.rs @@ -1,6 +1,6 @@ //! Benchmarks for CORDIC functions. -#![allow(missing_docs)] +#![allow(missing_docs, reason = "benchmark code does not need documentation")] use std::hint::black_box; diff --git a/src/bounded.rs b/src/bounded.rs new file mode 100644 index 0000000..5bde7e7 --- /dev/null +++ b/src/bounded.rs @@ -0,0 +1,342 @@ +//! Bounded value types that encode mathematical invariants at the type level. +//! +//! These types provide compile-time guarantees about value ranges, eliminating +//! the need for runtime checks in internal computations where the domain is +//! already validated. +//! +//! # Types +//! +//! - [`NonNegative`]: Values >= 0 (for sqrt inputs) +//! - [`UnitInterval`]: Values in [-1, 1] (for asin/acos inputs) +//! - [`OpenUnitInterval`]: Values in (-1, 1) (for atanh inputs) +//! +//! # Design Philosophy +//! +//! Rather than using `unsafe` or `expect`, these types encode the mathematical +//! relationships between operations. For example: +//! +//! - `1 + x^2` is always >= 1, so `NonNegative::one_plus_square(x)` is infallible +//! - If `|x| <= 1`, then `1 - x^2` is in [0, 1], so `NonNegative::one_minus_square(unit_x)` is infallible +//! - `x / sqrt(1 + x^2)` is always in (-1, 1), so `OpenUnitInterval::x_div_sqrt_one_plus_x_sq(x)` is infallible + +use crate::traits::CordicNumber; + +/// A value guaranteed to be non-negative (>= 0). +/// +/// This type enables infallible sqrt operations by encoding the non-negativity +/// constraint at the type level. +/// +/// # Construction +/// +/// - [`NonNegative::new`]: Checked construction, returns `Option` +/// - [`NonNegative::one_plus_square`]: From `1 + x^2`, always valid +/// - [`NonNegative::one_minus_square`]: From `1 - x^2` where `|x| <= 1`, always valid +/// - [`NonNegative::square_minus_one`]: From `x^2 - 1` where `|x| >= 1`, always valid +#[derive(Clone, Copy, Debug)] +pub struct NonNegative(T); + +impl NonNegative { + /// Creates a new `NonNegative` value if the input is >= 0. + /// + /// Returns `None` if the value is negative. + #[inline] + #[must_use] + pub fn new(value: T) -> Option { + (value >= T::zero()).then_some(Self(value)) + } + + /// Constructs from `1 + x^2`, which is always >= 1. + /// + /// This is mathematically infallible: for any real `x`, `1 + x^2 >= 1`. + #[inline] + #[must_use] + pub fn one_plus_square(x: T) -> Self { + let x_sq = x.saturating_mul(x); + Self(T::one().saturating_add(x_sq)) + } + + /// Constructs from `1 - x^2` where `|x| <= 1`. + /// + /// This is mathematically infallible: if `|x| <= 1`, then `x^2 <= 1`, + /// so `1 - x^2 >= 0`. + #[inline] + #[must_use] + pub fn one_minus_square(x: UnitInterval) -> Self { + let x_sq = x.0.saturating_mul(x.0); + Self(T::one().saturating_sub(x_sq)) + } + + /// Constructs from `x^2 - 1` where `|x| >= 1`. + /// + /// This is mathematically infallible: if `|x| >= 1`, then `x^2 >= 1`, + /// so `x^2 - 1 >= 0`. + #[inline] + #[must_use] + pub fn square_minus_one(x: AtLeastOne) -> Self { + let x_sq = x.0.saturating_mul(x.0); + Self(x_sq.saturating_sub(T::one())) + } + + /// Returns the inner value. + #[inline] + #[must_use] + pub const fn get(self) -> T { + self.0 + } +} + +/// A value guaranteed to be in the closed interval [-1, 1]. +/// +/// This type is used for inputs to functions like asin and acos that +/// require their argument to be in this range. +#[derive(Clone, Copy, Debug)] +pub struct UnitInterval(T); + +impl UnitInterval { + /// Creates a new `UnitInterval` value if the input is in [-1, 1]. + /// + /// Returns `None` if the value is outside the interval. + #[inline] + #[must_use] + pub fn new(value: T) -> Option { + let one = T::one(); + (value >= -one && value <= one).then_some(Self(value)) + } + + /// Returns the inner value. + #[inline] + #[must_use] + pub const fn get(self) -> T { + self.0 + } +} + +/// A value guaranteed to be in the open interval (-1, 1). +/// +/// This type is used for inputs to atanh, which requires strict inequality. +#[derive(Clone, Copy, Debug)] +pub struct OpenUnitInterval(T); + +impl OpenUnitInterval { + /// Creates a new `OpenUnitInterval` value if the input is in (-1, 1). + /// + /// Returns `None` if the value is outside the interval or on the boundary. + #[inline] + #[must_use] + pub fn new(value: T) -> Option { + let one = T::one(); + (value > -one && value < one).then_some(Self(value)) + } + + /// Constructs from `x / sqrt(1 + x^2)`, which is always in (-1, 1). + /// + /// This is mathematically infallible: for any real `x`, + /// `|x / sqrt(1 + x^2)| < 1` because `sqrt(1 + x^2) > |x|`. + /// + /// Note: Requires the sqrt to be computed first. + #[inline] + #[must_use] + pub fn from_div_by_sqrt_one_plus_square(x: T, sqrt_one_plus_x_sq: T) -> Self { + Self(x.div(sqrt_one_plus_x_sq)) + } + + /// Constructs from `sqrt(x^2 - 1) / x` where `|x| >= 1`. + /// + /// This is mathematically infallible: for `|x| >= 1`, + /// `|sqrt(x^2 - 1) / x| < 1` because `sqrt(x^2 - 1) < |x|` for `|x| > 1`. + /// + /// Note: Requires the sqrt to be computed first. + #[inline] + #[must_use] + pub fn from_sqrt_square_minus_one_div(sqrt_x_sq_minus_one: T, x: AtLeastOne) -> Self { + Self(sqrt_x_sq_minus_one.div(x.0)) + } + + /// Constructs from `(x - 1) / (x + 1)` where `x` is in [0.5, 2]. + /// + /// This is mathematically infallible: for `x` in [0.5, 2], + /// `(x - 1) / (x + 1)` is in `(-1/3, 1/3)` which is a subset of `(-1, 1)`. + #[inline] + #[must_use] + pub fn from_normalized_ln_arg(x: NormalizedLnArg) -> Self { + let x_minus_1 = x.0 - T::one(); + let x_plus_1 = x.0 + T::one(); + Self(x_minus_1.div(x_plus_1)) + } + + /// Returns the inner value. + #[inline] + #[must_use] + pub const fn get(self) -> T { + self.0 + } +} + +/// A value guaranteed to be >= 1 (or <= -1 for the absolute value). +/// +/// This type is used for inputs to acosh which requires x >= 1. +#[derive(Clone, Copy, Debug)] +pub struct AtLeastOne(T); + +impl AtLeastOne { + /// Creates a new `AtLeastOne` value if the input is >= 1. + /// + /// Returns `None` if the value is less than 1. + #[inline] + #[must_use] + pub fn new(value: T) -> Option { + (value >= T::one()).then_some(Self(value)) + } + + /// Returns the inner value. + #[inline] + #[must_use] + pub const fn get(self) -> T { + self.0 + } +} + +/// A value guaranteed to be in [0.5, 2], used for ln argument normalization. +/// +/// After normalizing the input for ln computation, the value is always +/// in this range, which guarantees that `(x-1)/(x+1)` is in `(-1/3, 1/3)`. +#[derive(Clone, Copy, Debug)] +pub struct NormalizedLnArg(T); + +impl NormalizedLnArg { + /// Creates a new `NormalizedLnArg` from the normalization loop result. + /// + /// The ln function's normalization loop guarantees the result is in [0.5, 2]. + /// This constructor trusts that invariant (used only in ln implementation). + #[inline] + #[must_use] + pub(crate) const fn from_normalized(value: T) -> Self { + Self(value) + } + + /// Returns the inner value. + #[inline] + #[must_use] + pub const fn get(self) -> T { + self.0 + } +} + +#[cfg(test)] +#[allow(clippy::unwrap_used, reason = "test code uses unwrap for conciseness")] +mod tests { + use super::*; + use fixed::types::I16F16; + + #[test] + fn non_negative_new() { + assert!(NonNegative::new(I16F16::from_num(0)).is_some()); + assert!(NonNegative::new(I16F16::from_num(1)).is_some()); + assert!(NonNegative::new(I16F16::from_num(-1)).is_none()); + } + + #[test] + fn non_negative_one_plus_square() { + let x = I16F16::from_num(2); + let nn = NonNegative::one_plus_square(x); + assert_eq!(nn.get(), I16F16::from_num(5)); + } + + #[test] + fn non_negative_one_minus_square() { + let unit = UnitInterval::new(I16F16::from_num(0.5)).unwrap(); + let nn = NonNegative::one_minus_square(unit); + let val: f32 = nn.get().to_num(); + assert!((val - 0.75).abs() < 0.01); + } + + #[test] + fn non_negative_square_minus_one() { + let at_least = AtLeastOne::new(I16F16::from_num(2)).unwrap(); + let nn = NonNegative::square_minus_one(at_least); + assert_eq!(nn.get(), I16F16::from_num(3)); + } + + #[test] + fn unit_interval_new() { + assert!(UnitInterval::new(I16F16::from_num(0)).is_some()); + assert!(UnitInterval::new(I16F16::from_num(1)).is_some()); + assert!(UnitInterval::new(I16F16::from_num(-1)).is_some()); + assert!(UnitInterval::new(I16F16::from_num(1.1)).is_none()); + assert!(UnitInterval::new(I16F16::from_num(-1.1)).is_none()); + } + + #[test] + fn unit_interval_get() { + let unit = UnitInterval::new(I16F16::from_num(0.5)).unwrap(); + assert_eq!(unit.get(), I16F16::from_num(0.5)); + } + + #[test] + fn open_unit_interval_new() { + assert!(OpenUnitInterval::new(I16F16::from_num(0)).is_some()); + assert!(OpenUnitInterval::new(I16F16::from_num(0.5)).is_some()); + assert!(OpenUnitInterval::new(I16F16::from_num(1)).is_none()); + assert!(OpenUnitInterval::new(I16F16::from_num(-1)).is_none()); + } + + #[test] + fn open_unit_interval_get() { + let open = OpenUnitInterval::new(I16F16::from_num(0.5)).unwrap(); + assert_eq!(open.get(), I16F16::from_num(0.5)); + } + + #[test] + fn open_unit_interval_from_div() { + let x = I16F16::from_num(1); + let sqrt_1_plus_x_sq = I16F16::SQRT_2; + let open = OpenUnitInterval::from_div_by_sqrt_one_plus_square(x, sqrt_1_plus_x_sq); + let val: f32 = open.get().to_num(); + assert!((val - core::f32::consts::FRAC_1_SQRT_2).abs() < 0.01); + } + + #[test] + fn open_unit_interval_from_sqrt_div() { + let at_least = AtLeastOne::new(I16F16::from_num(2)).unwrap(); + #[allow( + clippy::approx_constant, + reason = "testing with known approximation of sqrt(3)" + )] + let sqrt_x_sq_minus_one = I16F16::from_num(1.732); + let open = OpenUnitInterval::from_sqrt_square_minus_one_div(sqrt_x_sq_minus_one, at_least); + let val: f32 = open.get().to_num(); + #[allow( + clippy::approx_constant, + reason = "testing with known approximation of sqrt(3)/2" + )] + let expected = 0.866_f32; + assert!((val - expected).abs() < 0.01); + } + + #[test] + fn at_least_one_new() { + assert!(AtLeastOne::new(I16F16::from_num(1)).is_some()); + assert!(AtLeastOne::new(I16F16::from_num(2)).is_some()); + assert!(AtLeastOne::new(I16F16::from_num(0.9)).is_none()); + } + + #[test] + fn at_least_one_get() { + let at_least = AtLeastOne::new(I16F16::from_num(2)).unwrap(); + assert_eq!(at_least.get(), I16F16::from_num(2)); + } + + #[test] + fn normalized_ln_arg_get() { + let norm = NormalizedLnArg::from_normalized(I16F16::from_num(1.5)); + assert_eq!(norm.get(), I16F16::from_num(1.5)); + } + + #[test] + fn open_unit_interval_from_normalized_ln_arg() { + let norm = NormalizedLnArg::from_normalized(I16F16::from_num(1.5)); + let open = OpenUnitInterval::from_normalized_ln_arg(norm); + let val: f32 = open.get().to_num(); + assert!((val - 0.2).abs() < 0.01); + } +} diff --git a/src/kernel/cordic.rs b/src/kernel/cordic.rs index 370df46..682fcfc 100644 --- a/src/kernel/cordic.rs +++ b/src/kernel/cordic.rs @@ -28,19 +28,23 @@ use crate::tables::{ }; use crate::traits::CordicNumber; -/// Table lookup with bounds checking. +/// Table lookup for CORDIC iteration. /// -/// # Panics -/// Panics if index >= 64, which should never happen as CORDIC iterations -/// are bounded by `min(frac_bits, 62)` for circular and `min(frac_bits, 54)` -/// for hyperbolic mode. +/// # Safety Invariant +/// Index is bounded by CORDIC iteration limits: +/// - Circular mode: `min(frac_bits, 62)` → max index 61 +/// - Hyperbolic mode: `min(frac_bits, 54)` with `i.saturating_sub(1)` → max index 53 +/// +/// Since the tables have 64 elements and max index is 61, bounds are always satisfied. +/// The bounds check is optimized away in release builds. #[inline] -#[allow(clippy::expect_used)] // Index bounded by iteration limits, panic indicates bug -fn table_lookup(table: &[i64; 64], index: u32) -> i64 { - // Index is bounded by CORDIC iteration limits (max 62), always < 64. - *table - .get(index as usize) - .expect("CORDIC index exceeds table size") +const fn table_lookup(table: &[i64; 64], index: u32) -> i64 { + // SAFETY: Iteration limits guarantee index < 64 (see doc comment above). + #[allow( + clippy::indexing_slicing, + reason = "index bounded by CORDIC iteration limits" + )] + table[index as usize] } /// Returns the CORDIC scale factor (1/K ≈ 0.6073). @@ -199,7 +203,7 @@ pub fn circular_vectoring(mut x: T, mut y: T, mut z: T) -> (T, #[must_use] pub fn hyperbolic_rotation(mut x: T, mut y: T, mut z: T) -> (T, T, T) { let zero = T::zero(); - // Hyperbolic mode converges more slowly and needs repetitions + // Use frac_bits iterations, capped at 54 for table bounds. let max_iterations = T::frac_bits().min(54); let mut i: u32 = 1; // Hyperbolic starts at i=1 @@ -265,7 +269,8 @@ pub fn hyperbolic_rotation(mut x: T, mut y: T, mut z: T) -> (T, #[must_use] pub fn hyperbolic_vectoring(mut x: T, mut y: T, mut z: T) -> (T, T, T) { let zero = T::zero(); - let max_iterations = T::frac_bits().min(54); + // Use at least 24 iterations for better accuracy, even for lower precision types. + let max_iterations = T::frac_bits().clamp(24, 54); let mut i: u32 = 1; let mut iteration_count: u32 = 0; diff --git a/src/lib.rs b/src/lib.rs index d8a816c..020369f 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -27,11 +27,8 @@ #![no_std] #![cfg_attr(docsrs, feature(doc_cfg))] -#![warn(missing_docs)] -#![warn(clippy::pedantic)] -#![allow(clippy::module_name_repetitions)] -#![allow(clippy::similar_names)] +pub mod bounded; pub mod error; pub mod kernel; pub mod ops; diff --git a/src/ops/algebraic.rs b/src/ops/algebraic.rs index a39e477..51c5534 100644 --- a/src/ops/algebraic.rs +++ b/src/ops/algebraic.rs @@ -1,5 +1,6 @@ //! Algebraic functions (sqrt). +use crate::bounded::NonNegative; use crate::error::{Error, Result}; use crate::traits::CordicNumber; @@ -9,20 +10,31 @@ use crate::traits::CordicNumber; /// Returns `DomainError` if `x < 0`. #[must_use = "returns the square root result which should be handled"] pub fn sqrt(x: T) -> Result { + NonNegative::new(x) + .map(sqrt_nonneg) + .ok_or_else(|| Error::domain("sqrt", "non-negative value")) +} + +/// Infallible square root for non-negative values. +/// +/// This function takes a [`NonNegative`] wrapper, guaranteeing at the type +/// level that the input is valid. No domain check is performed at runtime. +/// +/// Use this when the non-negativity of the input is already established +/// through mathematical invariants (e.g., `1 + x²`, `1 - x²` for `|x| ≤ 1`). +#[must_use] +pub fn sqrt_nonneg(x: NonNegative) -> T { + let x = x.get(); let zero = T::zero(); let one = T::one(); let half = T::half(); - if x < zero { - return Err(Error::domain("sqrt", "non-negative value")); - } - if x == zero { - return Ok(zero); + return zero; } if x == one { - return Ok(one); + return one; } // Initial guess: use bit-level estimation for faster convergence @@ -58,7 +70,11 @@ pub fn sqrt(x: T) -> Result { // Pre-compute epsilon: approximately 2^(-frac_bits/2) for convergence check. // frac_bits ≤ 128 for all supported types, so shift is in range [0, 63]. - #[allow(clippy::cast_possible_wrap, clippy::cast_sign_loss)] + #[allow( + clippy::cast_possible_wrap, + clippy::cast_sign_loss, + reason = "frac_bits bounded by type size" + )] let epsilon_shift = (63i32 - (T::frac_bits() / 2) as i32).max(0) as u32; let epsilon = T::from_i1f63(1i64 << epsilon_shift); @@ -75,7 +91,7 @@ pub fn sqrt(x: T) -> Result { }; if diff <= epsilon { - return Ok(new_guess); + return new_guess; } guess = new_guess; @@ -84,5 +100,5 @@ pub fn sqrt(x: T) -> Result { // Final iteration - always performed, result always returned let quotient = x.div(guess); let sum = guess.saturating_add(quotient); - Ok(sum.saturating_mul(half)) + sum.saturating_mul(half) } diff --git a/src/ops/circular.rs b/src/ops/circular.rs index 55e0120..c944ee8 100644 --- a/src/ops/circular.rs +++ b/src/ops/circular.rs @@ -1,7 +1,9 @@ //! Trigonometric functions via circular CORDIC. +use crate::bounded::{NonNegative, UnitInterval}; use crate::error::{Error, Result}; use crate::kernel::{circular_rotation, circular_vectoring, cordic_scale_factor}; +use crate::ops::algebraic::sqrt_nonneg; use crate::traits::CordicNumber; /// Sine and cosine. More efficient than separate calls. Accepts any angle. @@ -70,25 +72,17 @@ pub fn tan(angle: T) -> T { /// /// # Errors /// Returns `DomainError` if `|x| > 1`. -/// -/// # Panics -/// Panics if the internal sqrt computation fails, which should never happen -/// for valid domain inputs as 1-x² is always non-negative when |x| ≤ 1. #[must_use = "returns the arcsine result which should be handled"] -#[allow(clippy::missing_panics_doc)] // Panic only on internal invariant violation pub fn asin(x: T) -> Result { - let one = T::one(); - let neg_one = -one; - - if x > one || x < neg_one { + let Some(unit_x) = UnitInterval::new(x) else { return Err(Error::domain("asin", "value in range [-1, 1]")); - } + }; // Special cases - if x == one { + if x == T::one() { return Ok(T::frac_pi_2()); } - if x == neg_one { + if x == -T::one() { return Ok(-T::frac_pi_2()); } if x == T::zero() { @@ -96,12 +90,8 @@ pub fn asin(x: T) -> Result { } // Use the identity: asin(x) = atan(x / sqrt(1 - x²)) - // This gives better accuracy than iterative methods - let x_sq = x.saturating_mul(x); - let one_minus_x_sq = one.saturating_sub(x_sq); - // SAFETY: x ∈ [-1,1] is enforced above, so 1-x² ∈ [0,1], sqrt cannot fail. - #[allow(clippy::expect_used)] // Invariant: x in [-1,1] guarantees valid sqrt input - let sqrt_term = crate::ops::algebraic::sqrt(one_minus_x_sq).expect("1-x² ≥ 0"); + // NonNegative::one_minus_square gives 1 - x², which is ≥ 0 since |x| ≤ 1 + let sqrt_term = sqrt_nonneg(NonNegative::one_minus_square(unit_x)); // Handle case where sqrt_term is very small (x close to ±1) if sqrt_term < T::from_i1f63(0x0001_0000_0000_0000) { diff --git a/src/ops/exponential.rs b/src/ops/exponential.rs index 409ed47..ef1db74 100644 --- a/src/ops/exponential.rs +++ b/src/ops/exponential.rs @@ -1,7 +1,8 @@ //! Exponential and logarithmic functions. +use crate::bounded::{NormalizedLnArg, OpenUnitInterval}; use crate::error::{Error, Result}; -use crate::ops::hyperbolic::sinh_cosh; +use crate::ops::hyperbolic::{atanh_open, sinh_cosh}; use crate::traits::CordicNumber; /// Exponential (e^x). May overflow for large positive x. @@ -16,54 +17,59 @@ pub fn exp(x: T) -> T { return one; } - // For large |x|, use argument reduction: exp(x) = exp(x/2)² - // Or better: exp(x) = 2^k * exp(r) where x = k*ln(2) + r - let abs_x = x.abs(); - let threshold = ln2 + ln2; // About 1.386 - - if abs_x > threshold { - // Argument reduction using exp(x) = exp(x - ln2) * 2 - // Find k such that |x - k*ln2| < ln2 - let mut reduced = x; - let mut scale_factor = one; - - let mut i = 0; - if x.is_positive() { - while reduced > ln2 && i < 128 { - reduced -= ln2; - scale_factor = scale_factor + scale_factor; // *= 2 - i += 1; - } - } else { - while reduced < -ln2 && i < 128 { - reduced += ln2; - scale_factor = scale_factor >> 1; // /= 2 - i += 1; - } - } + // For large |x|, use argument reduction: exp(x) = 2^k * exp(r) + // where r is reduced to (-ln2, ln2) range + let mut reduced = x; + let mut scale: i32 = 0; - // Now compute exp(reduced) where |reduced| <= ln2 - let (sinh_r, cosh_r) = sinh_cosh(reduced); - let exp_r = cosh_r.saturating_add(sinh_r); + // Reduce positive values + let mut i = 0; + while reduced > ln2 && i < 64 { + reduced -= ln2; + scale += 1; + i += 1; + } - return scale_factor.saturating_mul(exp_r); + // Reduce negative values + i = 0; + while reduced < -ln2 && i < 64 { + reduced += ln2; + scale -= 1; + i += 1; } - // For small x, use exp(x) = cosh(x) + sinh(x) directly - let (sinh_x, cosh_x) = sinh_cosh(x); - cosh_x.saturating_add(sinh_x) + // Compute exp(reduced) = cosh(reduced) + sinh(reduced) + let (sinh_r, cosh_r) = sinh_cosh(reduced); + let exp_r = cosh_r.saturating_add(sinh_r); + + // Scale by 2^scale using bit shifts + #[allow(clippy::cast_possible_wrap, reason = "total_bits bounded by type size")] + let max_shift = (T::total_bits() - 1) as i32; + + #[allow(clippy::cast_sign_loss, reason = "scale >= 0 checked before cast")] + if scale >= 0 { + if scale > max_shift { + T::max_value() + } else { + let shift = scale as u32; + exp_r << shift + } + } else { + let neg_scale = -scale; + if neg_scale > max_shift { + zero + } else { + let shift = neg_scale as u32; + exp_r >> shift + } + } } /// Natural logarithm. Domain: `x > 0`. /// /// # Errors /// Returns `DomainError` if `x ≤ 0`. -/// -/// # Panics -/// Panics if the internal atanh computation fails, which should never happen -/// as the normalized argument is always in the valid range (-1/3, 1/3). #[must_use = "returns the natural logarithm result which should be handled"] -#[allow(clippy::missing_panics_doc)] // Panic only on internal invariant violation pub fn ln(x: T) -> Result { let zero = T::zero(); let one = T::one(); @@ -77,9 +83,6 @@ pub fn ln(x: T) -> Result { return Ok(zero); } - // For x very close to 1, the direct formula works well - // ln(x) = 2 * atanh((x-1)/(x+1)) - // For x far from 1, use argument reduction: // ln(x) = ln(x * 2^(-k)) + k * ln(2) // where k is chosen so that x * 2^(-k) is close to 1 @@ -109,15 +112,14 @@ pub fn ln(x: T) -> Result { // Now compute ln(normalized) where 0.5 <= normalized <= 2 // Using ln(x) = 2 * atanh((x-1)/(x+1)) - let x_minus_1 = normalized - one; - let x_plus_1 = normalized + one; - let arg = x_minus_1.div(x_plus_1); - - // SAFETY: normalized ∈ [0.5, 2], so arg = (x-1)/(x+1) ∈ (-1/3, 1/3) ⊂ (-1, 1). - // atanh cannot fail for this range. - #[allow(clippy::expect_used)] // Invariant: normalized in [0.5, 2] guarantees valid atanh input - let atanh_val = crate::ops::hyperbolic::atanh(arg) - .expect("normalized in [0.5, 2] guarantees arg in (-1/3, 1/3)"); + // NormalizedLnArg encodes that normalized ∈ [0.5, 2] + let norm = NormalizedLnArg::from_normalized(normalized); + + // OpenUnitInterval::from_normalized_ln_arg computes (x-1)/(x+1), + // which is in (-1/3, 1/3) ⊂ (-1, 1) for x ∈ [0.5, 2] + let arg = OpenUnitInterval::from_normalized_ln_arg(norm); + + let atanh_val = atanh_open(arg); let ln_normalized = atanh_val + atanh_val; // 2 * atanh Ok(ln_normalized + k_ln2) diff --git a/src/ops/hyperbolic.rs b/src/ops/hyperbolic.rs index e828544..a999f3c 100644 --- a/src/ops/hyperbolic.rs +++ b/src/ops/hyperbolic.rs @@ -1,7 +1,9 @@ //! Hyperbolic functions via hyperbolic CORDIC. +use crate::bounded::{AtLeastOne, NonNegative, OpenUnitInterval}; use crate::error::{Error, Result}; use crate::kernel::{hyperbolic_gain_inv, hyperbolic_rotation, hyperbolic_vectoring}; +use crate::ops::algebraic::sqrt_nonneg; use crate::traits::CordicNumber; /// Hyperbolic CORDIC converges for |x| < sum of atanh table ≈ 1.1182. @@ -62,12 +64,14 @@ pub fn sinh_cosh(x: T) -> (T, T) { // sinh(x) ≈ x + x³/6 + x⁵/120, cosh(x) ≈ 1 + x²/2 + x⁴/24 + x⁶/720 let x_5 = x_qu.saturating_mul(x); let x_6 = x_qu.saturating_mul(x_sq); - let c = one + // cosh base: 1 + x²/2 + x⁴/24 + let cosh_base = one .saturating_add(x_sq >> 1) .saturating_add(x_qu.div(T::from_num(24))); - let cosh_approx = c.saturating_add(x_6.div(T::from_num(720))); - let s = x.saturating_add(x_cu.div(T::from_num(6))); - let sinh_approx = s.saturating_add(x_5.div(T::from_num(120))); + let cosh_approx = cosh_base.saturating_add(x_6.div(T::from_num(720))); + // sinh base: x + x³/6 + let sinh_base = x.saturating_add(x_cu.div(T::from_num(6))); + let sinh_approx = sinh_base.saturating_add(x_5.div(T::from_num(120))); return (sinh_approx, cosh_approx); } // sinh(x) ≈ x + x³/6, cosh(x) ≈ 1 + x²/2 + x⁴/24 @@ -122,66 +126,42 @@ pub fn coth(x: T) -> Result { } /// Inverse hyperbolic sine. Accepts any value. -/// -/// # Panics -/// Panics if the internal sqrt computation fails, which should never happen -/// as 1+x² is always ≥ 1 for any x. #[must_use] -#[allow(clippy::missing_panics_doc)] // Panic only on internal invariant violation pub fn asinh(x: T) -> T { - let zero = T::zero(); - let one = T::one(); - - if x == zero { - return zero; + if x == T::zero() { + return T::zero(); } - // asinh(x) = sign(x) * ln(|x| + sqrt(x² + 1)) - // For CORDIC, we use: asinh(x) = atanh(x / sqrt(1 + x²)) - let x_sq = x.saturating_mul(x); - let one_plus_x_sq = one.saturating_add(x_sq); - // SAFETY: 1+x² ≥ 1 for all x, so sqrt cannot fail. - #[allow(clippy::expect_used)] // Invariant: 1+x² is always ≥ 1 - let sqrt_term = crate::ops::algebraic::sqrt(one_plus_x_sq).expect("1+x² is always ≥ 1"); + // asinh(x) = atanh(x / sqrt(1 + x²)) + // NonNegative::one_plus_square(x) returns 1 + x², which is always ≥ 1 + let sqrt_term = sqrt_nonneg(NonNegative::one_plus_square(x)); - // Compute x / sqrt(1 + x²), which is in (-1, 1) - let arg = x.div(sqrt_term); + // x / sqrt(1 + x²) is always in (-1, 1) since sqrt(1 + x²) > |x| + let arg = OpenUnitInterval::from_div_by_sqrt_one_plus_square(x, sqrt_term); - atanh_core(arg) + atanh_open(arg) } /// Inverse hyperbolic cosine. Domain: `x ≥ 1`. /// /// # Errors /// Returns `DomainError` if `x < 1`. -/// -/// # Panics -/// Panics if the internal sqrt computation fails, which should never happen -/// for valid domain inputs as x²-1 is always non-negative when x ≥ 1. #[must_use = "returns the inverse hyperbolic cosine result which should be handled"] -#[allow(clippy::missing_panics_doc)] // Panic only on internal invariant violation pub fn acosh(x: T) -> Result { - let one = T::one(); - - if x < one { - return Err(Error::domain("acosh", "value >= 1")); - } + let at_least_one = AtLeastOne::new(x).ok_or_else(|| Error::domain("acosh", "value >= 1"))?; - if x == one { + if x == T::one() { return Ok(T::zero()); } - // acosh(x) = ln(x + sqrt(x² - 1)) - // Using CORDIC: acosh(x) = atanh(sqrt(x² - 1) / x) for x > 0 - // But this requires |sqrt(x²-1)/x| < 1, which is true for x > 1 - let x_sq = x.saturating_mul(x); - let x_sq_minus_one = x_sq.saturating_sub(one); - // SAFETY: x ≥ 1 is enforced above, so x²-1 ≥ 0, sqrt cannot fail. - #[allow(clippy::expect_used)] // Invariant: x ≥ 1 guarantees valid sqrt input - let sqrt_term = crate::ops::algebraic::sqrt(x_sq_minus_one).expect("x ≥ 1 guarantees x²-1 ≥ 0"); - - let arg = sqrt_term.div(x); - Ok(atanh_core(arg)) + // acosh(x) = atanh(sqrt(x² - 1) / x) for x > 1 + // NonNegative::square_minus_one gives x² - 1, which is ≥ 0 since x ≥ 1 + let sqrt_term = sqrt_nonneg(NonNegative::square_minus_one(at_least_one)); + + // sqrt(x² - 1) / x is in (-1, 1) for x > 1 since sqrt(x² - 1) < x + let arg = OpenUnitInterval::from_sqrt_square_minus_one_div(sqrt_term, at_least_one); + + Ok(atanh_open(arg)) } /// Inverse hyperbolic tangent. Domain: `(-1, 1)`. @@ -190,13 +170,21 @@ pub fn acosh(x: T) -> Result { /// Returns `DomainError` if `|x| ≥ 1`. #[must_use = "returns the inverse hyperbolic tangent result which should be handled"] pub fn atanh(x: T) -> Result { - let one = T::one(); - - if x >= one || x <= -one { - return Err(Error::domain("atanh", "value in range (-1, 1)")); - } + OpenUnitInterval::new(x) + .map(atanh_open) + .ok_or_else(|| Error::domain("atanh", "value in range (-1, 1)")) +} - Ok(atanh_core(x)) +/// Infallible inverse hyperbolic tangent for values in (-1, 1). +/// +/// This function takes an [`OpenUnitInterval`] wrapper, guaranteeing at the +/// type level that the input is valid. No domain check is performed at runtime. +/// +/// Use this when the input is known to be in (-1, 1) through mathematical +/// invariants (e.g., `x / sqrt(1 + x²)`). +#[must_use] +pub fn atanh_open(x: OpenUnitInterval) -> T { + atanh_core(x.get()) } /// Core atanh implementation. Caller must ensure |x| < 1. diff --git a/src/traits.rs b/src/traits.rs index f6877d5..48dd169 100644 --- a/src/traits.rs +++ b/src/traits.rs @@ -102,6 +102,12 @@ pub trait CordicNumber: fn max_value() -> Self; /// Minimum value. fn min_value() -> Self; + /// Round to nearest integer (half away from zero). + #[must_use] + fn round(self) -> Self; + /// Convert to i32 (truncates toward zero). + #[must_use] + fn to_i32(self) -> i32; } // ============================================================================= @@ -186,7 +192,11 @@ macro_rules! impl_cordic_generic { #[inline] // Casts are safe: frac_bits ≤ 128, shift amounts bounded by type size - #[allow(clippy::cast_possible_wrap, clippy::cast_lossless)] + #[allow( + clippy::cast_possible_wrap, + clippy::cast_lossless, + reason = "frac_bits bounded by type size" + )] fn from_i1f63(bits: i64) -> Self { // Convert from I1F63 representation to our type. // I1F63 has 63 fractional bits. @@ -196,12 +206,18 @@ macro_rules! impl_cordic_generic { if shift >= 0 { // We have fewer frac bits than I1F63, shift right - #[allow(clippy::cast_possible_truncation)] + #[allow( + clippy::cast_possible_truncation, + reason = "intentional truncation to target type" + )] Self::from_bits((bits >> shift) as $bits_type) } else { // We have more frac bits than I1F63, shift left // Must cast first to avoid losing sign bit - #[allow(clippy::cast_possible_truncation)] + #[allow( + clippy::cast_possible_truncation, + reason = "intentional truncation to target type" + )] let wide = bits as $bits_type; Self::from_bits(wide << (-shift)) } @@ -209,7 +225,11 @@ macro_rules! impl_cordic_generic { #[inline] // Casts are safe: frac_bits ≤ 128, shift amounts bounded by type size - #[allow(clippy::cast_possible_wrap, clippy::cast_lossless)] + #[allow( + clippy::cast_possible_wrap, + clippy::cast_lossless, + reason = "frac_bits bounded by type size" + )] fn from_i2f62(bits: i64) -> Self { // Convert from I2F62 representation to our type. // I2F62 has 62 fractional bits. @@ -218,11 +238,17 @@ macro_rules! impl_cordic_generic { if shift >= 0 { // We have fewer frac bits than I2F62, shift right - #[allow(clippy::cast_possible_truncation)] + #[allow( + clippy::cast_possible_truncation, + reason = "intentional truncation to target type" + )] Self::from_bits((bits >> shift) as $bits_type) } else { // We have more frac bits than I2F62, shift left - #[allow(clippy::cast_possible_truncation)] + #[allow( + clippy::cast_possible_truncation, + reason = "intentional truncation to target type" + )] let wide = bits as $bits_type; Self::from_bits(wide << (-shift)) } @@ -267,6 +293,20 @@ macro_rules! impl_cordic_generic { fn min_value() -> Self { Self::MIN } + + #[inline] + fn round(self) -> Self { + Fixed::round(self) + } + + #[inline] + #[allow( + clippy::cast_possible_truncation, + reason = "intentional truncation to target type" + )] + fn to_i32(self) -> i32 { + self.to_num::() + } } }; } diff --git a/tests/unit/error.rs b/tests/unit/error.rs index 1ff8c0e..34144ea 100644 --- a/tests/unit/error.rs +++ b/tests/unit/error.rs @@ -24,10 +24,10 @@ mod tests { assert!(msg.contains("[-1, 1]")); // Test overflow error constructor - let err = Error::overflow("exp"); - let msg = format!("{err}"); - assert!(msg.contains("exp")); - assert!(msg.contains("overflow")); + let err_overflow = Error::overflow("exp"); + let msg_overflow = format!("{err_overflow}"); + assert!(msg_overflow.contains("exp")); + assert!(msg_overflow.contains("overflow")); } #[test] diff --git a/tests/unit/ops/algebraic.rs b/tests/unit/ops/algebraic.rs index aee335d..1813d0e 100644 --- a/tests/unit/ops/algebraic.rs +++ b/tests/unit/ops/algebraic.rs @@ -1,8 +1,7 @@ //! Tests for algebraic functions (sqrt) -#![allow(clippy::unwrap_used)] - #[cfg(test)] +#[allow(clippy::unwrap_used, reason = "test code uses unwrap for conciseness")] mod tests { use fixed::types::I16F16; use fixed_analytics::sqrt; diff --git a/tests/unit/ops/circular.rs b/tests/unit/ops/circular.rs index 46898f0..4c2579a 100644 --- a/tests/unit/ops/circular.rs +++ b/tests/unit/ops/circular.rs @@ -1,7 +1,11 @@ //! Tests for circular trigonometric functions #[cfg(test)] -#[allow(clippy::unwrap_used)] +#[allow( + clippy::unwrap_used, + clippy::cast_precision_loss, + reason = "test code uses unwrap and f32 casts for conciseness" +)] mod tests { use fixed::types::{I16F16, I32F32}; use fixed_analytics::{acos, asin, atan, atan2, cos, sin, sin_cos, tan}; @@ -36,7 +40,6 @@ mod tests { // Tests Pythagorean identity: sin²(x) + cos²(x) = 1 #[test] - #[allow(clippy::cast_precision_loss)] fn sin_cos_pythagorean_identity() { for i in -20..=20 { let angle = I16F16::from_num(i) * I16F16::from_num(0.1); diff --git a/tests/unit/ops/exponential.rs b/tests/unit/ops/exponential.rs index 48e9328..eb87f98 100644 --- a/tests/unit/ops/exponential.rs +++ b/tests/unit/ops/exponential.rs @@ -1,7 +1,7 @@ //! Tests for exponential and logarithmic functions #[cfg(test)] -#[allow(clippy::unwrap_used)] +#[allow(clippy::unwrap_used, reason = "test code uses unwrap for conciseness")] mod tests { use fixed::types::I16F16; use fixed_analytics::{exp, ln, log2, log10, pow2}; @@ -231,4 +231,32 @@ mod tests { // ln(0.0001) ≈ -9.2 assert!(val < -8.0, "ln(0.0001) = {val}, expected < -8.0"); } + + #[test] + fn exp_overflow_to_max() { + // exp of very large positive values should return max when scale > max_shift + // For I16F16: total_bits = 32, max_shift = 31 + // Need scale > 31, i.e., x > 31 * ln(2) ≈ 21.5 + // exp(25) triggers scale=36 which exceeds max_shift=31 + let very_large = I16F16::from_num(25.0); + let result: f32 = exp(very_large).to_num(); + // Should return max value + let max: f32 = I16F16::MAX.to_num(); + assert!( + (result - max).abs() < 1.0, + "exp(25) = {result}, expected max {max}" + ); + } + + #[test] + fn exp_underflow_to_zero() { + // exp of very large negative values should return zero when -scale > max_shift + // For I16F16: total_bits = 32, max_shift = 31 + // Need -scale > 31, i.e., x < -31 * ln(2) ≈ -21.5 + // exp(-25) triggers scale=-36 which exceeds -max_shift=-31 + let very_negative = I16F16::from_num(-25.0); + let result: f32 = exp(very_negative).to_num(); + // Should return zero + assert!(result == 0.0, "exp(-25) = {result}, expected 0"); + } } diff --git a/tests/unit/ops/hyperbolic.rs b/tests/unit/ops/hyperbolic.rs index b508662..6e7e686 100644 --- a/tests/unit/ops/hyperbolic.rs +++ b/tests/unit/ops/hyperbolic.rs @@ -1,6 +1,7 @@ //! Tests for hyperbolic functions #[cfg(test)] +#[allow(clippy::unwrap_used, reason = "test code uses unwrap for conciseness")] mod tests { use fixed::types::{I16F16, I32F32}; use fixed_analytics::{acosh, acoth, asinh, atanh, cosh, coth, sinh, sinh_cosh, tanh}; @@ -69,7 +70,6 @@ mod tests { } #[test] - #[allow(clippy::unwrap_used)] fn acoth_values() { // acoth(x) = atanh(1/x) // acoth(2) = atanh(0.5) ≈ 0.5493 @@ -82,12 +82,12 @@ mod tests { ); // acoth(-2) = atanh(-0.5) ≈ -0.5493 - let result = acoth(I16F16::from_num(-2.0)); - assert!(result.is_ok()); - let val: f32 = result.unwrap().to_num(); + let result_neg = acoth(I16F16::from_num(-2.0)); + assert!(result_neg.is_ok()); + let val_neg: f32 = result_neg.unwrap().to_num(); assert!( - (val + 0.5493).abs() < TOLERANCE, - "acoth(-2) expected ~-0.5493, got {val}" + (val_neg + 0.5493).abs() < TOLERANCE, + "acoth(-2) expected ~-0.5493, got {val_neg}" ); } @@ -107,7 +107,6 @@ mod tests { } #[test] - #[allow(clippy::unwrap_used)] fn cosh_acosh_roundtrip() { // cosh(acosh(x)) ≈ x for x >= 1 for i in 1..=10 { @@ -125,7 +124,6 @@ mod tests { } #[test] - #[allow(clippy::unwrap_used)] fn tanh_atanh_roundtrip() { // tanh(atanh(x)) ≈ x for x in (-1, 1) for i in -9..=9 { @@ -141,7 +139,6 @@ mod tests { } #[test] - #[allow(clippy::unwrap_used)] fn atanh_near_boundary() { // atanh approaches infinity as |x| approaches 1 // Test values close to but not at the boundary @@ -160,7 +157,6 @@ mod tests { } #[test] - #[allow(clippy::unwrap_used)] fn acosh_at_boundary() { // acosh(1) should be exactly 0 let result: f32 = acosh(I16F16::ONE).unwrap().to_num(); @@ -216,7 +212,6 @@ mod tests { } #[test] - #[allow(clippy::unwrap_used)] fn coth_nonzero_values() { // coth(x) = cosh(x)/sinh(x) // coth(1) ≈ 1.3130 @@ -268,6 +263,21 @@ mod tests { (c_neg_f32 - 1.00045).abs() < 0.01, "cosh(-0.03) = {c_neg_f32}, expected ~1.00045" ); + + // Additional test with even smaller value to ensure full Taylor path coverage + let tiny = core::hint::black_box(I32F32::from_num(0.01)); + let (s_tiny, c_tiny) = sinh_cosh(tiny); + // Use black_box to prevent optimization + let s_tiny_f32: f32 = core::hint::black_box(s_tiny).to_num(); + let c_tiny_f32: f32 = core::hint::black_box(c_tiny).to_num(); + assert!( + (s_tiny_f32 - 0.01).abs() < 0.001, + "sinh(0.01) = {s_tiny_f32}, expected ~0.01" + ); + assert!( + (c_tiny_f32 - 1.0).abs() < 0.001, + "cosh(0.01) = {c_tiny_f32}, expected ~1.0" + ); } #[test] diff --git a/tests/unit/smoke.rs b/tests/unit/smoke.rs index 00cabb6..8b13594 100644 --- a/tests/unit/smoke.rs +++ b/tests/unit/smoke.rs @@ -1,6 +1,7 @@ //! Smoke tests and multi-type tests for the library API #[cfg(test)] +#[allow(clippy::unwrap_used, reason = "test code uses unwrap for conciseness")] mod tests { use fixed::types::I16F16; use fixed_analytics::{ @@ -42,9 +43,9 @@ mod tests { let _ = asinh(x); let _ = atanh(x); - let x = I16F16::from_num(1.5); - let _ = acosh(x); - let _ = acoth(x); + let x_large = I16F16::from_num(1.5); + let _ = acosh(x_large); + let _ = acoth(x_large); } #[test] @@ -57,7 +58,6 @@ mod tests { } #[test] - #[allow(clippy::unwrap_used)] fn smoke_test_algebraic() { let x = I16F16::from_num(2.0); let _ = sqrt(x).unwrap(); @@ -69,6 +69,7 @@ mod tests { // ========================================================================== #[cfg(test)] +#[allow(clippy::unwrap_used, reason = "test code uses unwrap for conciseness")] mod multi_type { use fixed::types::{I8F24, I32F32}; use fixed_analytics::{acos, asin, atan, exp, ln, sin_cos, sinh_cosh, sqrt}; @@ -103,7 +104,6 @@ mod multi_type { } #[test] - #[allow(clippy::unwrap_used)] fn inverse_trig_i32f32() { let x = I32F32::from_num(0.5); let asin_val = asin(x).unwrap(); @@ -134,7 +134,6 @@ mod multi_type { } #[test] - #[allow(clippy::unwrap_used)] fn exp_ln_i32f32() { let x = I32F32::from_num(2.0); let result = exp(ln(x).unwrap()); @@ -146,7 +145,6 @@ mod multi_type { } #[test] - #[allow(clippy::unwrap_used)] fn sqrt_i32f32() { let x = I32F32::from_num(4.0); let result: f64 = sqrt(x).unwrap().to_num(); @@ -181,7 +179,6 @@ mod multi_type { } #[test] - #[allow(clippy::unwrap_used)] fn sqrt_i8f24() { let x = I8F24::from_num(2.0); let result: f32 = sqrt(x).unwrap().to_num(); @@ -192,7 +189,6 @@ mod multi_type { } #[test] - #[allow(clippy::unwrap_used)] fn ln_i8f24() { let x = I8F24::from_num(2.0); let result: f32 = ln(x).unwrap().to_num(); @@ -205,6 +201,7 @@ mod multi_type { // I8F8 tests - lower precision, 16-bit total #[cfg(test)] +#[allow(clippy::unwrap_used, reason = "test code uses unwrap for conciseness")] mod i8f8 { use fixed::types::I8F8; use fixed_analytics::{sin_cos, sqrt}; @@ -223,7 +220,6 @@ mod i8f8 { } #[test] - #[allow(clippy::unwrap_used)] fn basic_sqrt() { let x = I8F8::from_num(4.0); let result: f32 = sqrt(x).unwrap().to_num(); diff --git a/tests/unit/tables/circular.rs b/tests/unit/tables/circular.rs index 66fcf59..7b9f844 100644 --- a/tests/unit/tables/circular.rs +++ b/tests/unit/tables/circular.rs @@ -1,7 +1,11 @@ //! Tests for circular CORDIC lookup tables #[cfg(test)] -#[allow(clippy::indexing_slicing, clippy::cast_precision_loss)] +#[allow( + clippy::indexing_slicing, + clippy::cast_precision_loss, + reason = "test code uses direct indexing and f64 casts" +)] mod tests { use fixed_analytics::tables::circular::{ATAN_TABLE, CIRCULAR_GAIN_INV}; diff --git a/tests/unit/tables/hyperbolic.rs b/tests/unit/tables/hyperbolic.rs index dbb99af..0bc3d53 100644 --- a/tests/unit/tables/hyperbolic.rs +++ b/tests/unit/tables/hyperbolic.rs @@ -1,7 +1,11 @@ //! Tests for hyperbolic CORDIC lookup tables #[cfg(test)] -#[allow(clippy::indexing_slicing, clippy::cast_precision_loss)] +#[allow( + clippy::indexing_slicing, + clippy::cast_precision_loss, + reason = "test code uses direct indexing and f64 casts" +)] mod tests { use fixed_analytics::tables::hyperbolic::{ ATANH_HALF, ATANH_TABLE, HYPERBOLIC_GAIN, HYPERBOLIC_GAIN_INV, needs_repeat, diff --git a/tests/unit/traits.rs b/tests/unit/traits.rs index 3689703..8074be2 100644 --- a/tests/unit/traits.rs +++ b/tests/unit/traits.rs @@ -7,7 +7,7 @@ mod tests { use fixed_analytics::kernel::hyperbolic_gain_inv; #[test] - #[allow(clippy::approx_constant)] + #[allow(clippy::approx_constant, reason = "testing pi approximation")] fn basic_operations_i16f16() { let x = I16F16::from_num(2.5); assert_eq!(I16F16::zero(), I16F16::ZERO); diff --git a/tests/unit/verification.rs b/tests/unit/verification.rs index c14707c..f41ae9a 100644 --- a/tests/unit/verification.rs +++ b/tests/unit/verification.rs @@ -4,14 +4,14 @@ //! properties: reference values, identities, inverse roundtrips, monotonicity, //! and output bounds. -// Test-specific clippy allows: these tests intentionally use casts and simple arithmetic -// for readability over micro-optimizations. +// Test-specific lints - these are acceptable in test code #![allow( + clippy::unwrap_used, clippy::cast_possible_truncation, - clippy::cast_lossless, - clippy::cast_precision_loss, clippy::suboptimal_flops, - clippy::manual_range_contains + clippy::cast_lossless, + clippy::manual_range_contains, + reason = "test code uses these patterns for conciseness and clarity" )] #[cfg(test)] @@ -205,7 +205,6 @@ mod reference_comparison { } #[test] - #[allow(clippy::unwrap_used)] fn sqrt_vs_f64() { for i in 0..SAMPLES { let bits = sample_bits(SEED, i); @@ -831,6 +830,7 @@ mod roundtrips { } #[cfg(test)] +#[allow(clippy::unwrap_used, reason = "test code uses unwrap for conciseness")] mod monotonicity { //! Verify that monotonic functions are actually monotonic. @@ -838,7 +838,6 @@ mod monotonicity { use fixed_analytics::{asin, atan, exp, ln, sin, sqrt, tanh}; #[test] - #[allow(clippy::unwrap_used)] fn sqrt_is_increasing() { let mut prev = I16F16::ZERO; for i in 0..1000 { diff --git a/tools/accuracy-bench/src/functions/algebraic.rs b/tools/accuracy-bench/src/functions/algebraic.rs index 1729bcb..9e4622b 100644 --- a/tools/accuracy-bench/src/functions/algebraic.rs +++ b/tools/accuracy-bench/src/functions/algebraic.rs @@ -1,4 +1,4 @@ -use crate::{reference, Domain, TestedFunction}; +use crate::{Domain, TestedFunction, reference}; use fixed::types::{I16F16, I32F32}; use rug::Float; @@ -8,9 +8,19 @@ pub fn register() -> Vec> { struct Sqrt; impl TestedFunction for Sqrt { - fn name(&self) -> &'static str { "sqrt" } - fn domain(&self) -> Domain { Domain::Closed(0.0, 10000.0) } - fn reference(&self, x: &Float) -> Float { reference::algebraic::sqrt(x) } - fn compute_i16f16(&self, x: I16F16) -> I16F16 { fixed_analytics::sqrt(x).unwrap_or(I16F16::ZERO) } - fn compute_i32f32(&self, x: I32F32) -> I32F32 { fixed_analytics::sqrt(x).unwrap_or(I32F32::ZERO) } + fn name(&self) -> &'static str { + "sqrt" + } + fn domain(&self) -> Domain { + Domain::Closed(0.0, 10000.0) + } + fn reference(&self, x: &Float) -> Float { + reference::algebraic::sqrt(x) + } + fn compute_i16f16(&self, x: I16F16) -> I16F16 { + fixed_analytics::sqrt(x).unwrap_or(I16F16::ZERO) + } + fn compute_i32f32(&self, x: I32F32) -> I32F32 { + fixed_analytics::sqrt(x).unwrap_or(I32F32::ZERO) + } } diff --git a/tools/accuracy-bench/src/functions/circular.rs b/tools/accuracy-bench/src/functions/circular.rs index 8834d30..e9a8197 100644 --- a/tools/accuracy-bench/src/functions/circular.rs +++ b/tools/accuracy-bench/src/functions/circular.rs @@ -1,64 +1,128 @@ -use crate::{reference, Domain, TestedFunction}; +use crate::{Domain, TestedFunction, reference}; use fixed::types::{I16F16, I32F32}; use rug::Float; pub fn register() -> Vec> { vec![ - Box::new(Sin), Box::new(Cos), Box::new(Tan), - Box::new(Asin), Box::new(Acos), Box::new(Atan), + Box::new(Sin), + Box::new(Cos), + Box::new(Tan), + Box::new(Asin), + Box::new(Acos), + Box::new(Atan), ] } struct Sin; impl TestedFunction for Sin { - fn name(&self) -> &'static str { "sin" } - fn domain(&self) -> Domain { Domain::Full } - fn reference(&self, x: &Float) -> Float { reference::circular::sin(x) } - fn compute_i16f16(&self, x: I16F16) -> I16F16 { fixed_analytics::sin(x) } - fn compute_i32f32(&self, x: I32F32) -> I32F32 { fixed_analytics::sin(x) } + fn name(&self) -> &'static str { + "sin" + } + fn domain(&self) -> Domain { + Domain::Full + } + fn reference(&self, x: &Float) -> Float { + reference::circular::sin(x) + } + fn compute_i16f16(&self, x: I16F16) -> I16F16 { + fixed_analytics::sin(x) + } + fn compute_i32f32(&self, x: I32F32) -> I32F32 { + fixed_analytics::sin(x) + } } struct Cos; impl TestedFunction for Cos { - fn name(&self) -> &'static str { "cos" } - fn domain(&self) -> Domain { Domain::Full } - fn reference(&self, x: &Float) -> Float { reference::circular::cos(x) } - fn compute_i16f16(&self, x: I16F16) -> I16F16 { fixed_analytics::cos(x) } - fn compute_i32f32(&self, x: I32F32) -> I32F32 { fixed_analytics::cos(x) } + fn name(&self) -> &'static str { + "cos" + } + fn domain(&self) -> Domain { + Domain::Full + } + fn reference(&self, x: &Float) -> Float { + reference::circular::cos(x) + } + fn compute_i16f16(&self, x: I16F16) -> I16F16 { + fixed_analytics::cos(x) + } + fn compute_i32f32(&self, x: I32F32) -> I32F32 { + fixed_analytics::cos(x) + } } struct Tan; impl TestedFunction for Tan { - fn name(&self) -> &'static str { "tan" } - fn domain(&self) -> Domain { Domain::Open(-1.5, 1.5) } - fn reference(&self, x: &Float) -> Float { reference::circular::tan(x) } - fn compute_i16f16(&self, x: I16F16) -> I16F16 { fixed_analytics::tan(x) } - fn compute_i32f32(&self, x: I32F32) -> I32F32 { fixed_analytics::tan(x) } + fn name(&self) -> &'static str { + "tan" + } + fn domain(&self) -> Domain { + Domain::Open(-1.5, 1.5) + } + fn reference(&self, x: &Float) -> Float { + reference::circular::tan(x) + } + fn compute_i16f16(&self, x: I16F16) -> I16F16 { + fixed_analytics::tan(x) + } + fn compute_i32f32(&self, x: I32F32) -> I32F32 { + fixed_analytics::tan(x) + } } struct Asin; impl TestedFunction for Asin { - fn name(&self) -> &'static str { "asin" } - fn domain(&self) -> Domain { Domain::Closed(-0.99, 0.99) } - fn reference(&self, x: &Float) -> Float { reference::circular::asin(x) } - fn compute_i16f16(&self, x: I16F16) -> I16F16 { fixed_analytics::asin(x).unwrap_or(I16F16::ZERO) } - fn compute_i32f32(&self, x: I32F32) -> I32F32 { fixed_analytics::asin(x).unwrap_or(I32F32::ZERO) } + fn name(&self) -> &'static str { + "asin" + } + fn domain(&self) -> Domain { + Domain::Closed(-0.99, 0.99) + } + fn reference(&self, x: &Float) -> Float { + reference::circular::asin(x) + } + fn compute_i16f16(&self, x: I16F16) -> I16F16 { + fixed_analytics::asin(x).unwrap_or(I16F16::ZERO) + } + fn compute_i32f32(&self, x: I32F32) -> I32F32 { + fixed_analytics::asin(x).unwrap_or(I32F32::ZERO) + } } struct Acos; impl TestedFunction for Acos { - fn name(&self) -> &'static str { "acos" } - fn domain(&self) -> Domain { Domain::Closed(-0.99, 0.99) } - fn reference(&self, x: &Float) -> Float { reference::circular::acos(x) } - fn compute_i16f16(&self, x: I16F16) -> I16F16 { fixed_analytics::acos(x).unwrap_or(I16F16::ZERO) } - fn compute_i32f32(&self, x: I32F32) -> I32F32 { fixed_analytics::acos(x).unwrap_or(I32F32::ZERO) } + fn name(&self) -> &'static str { + "acos" + } + fn domain(&self) -> Domain { + Domain::Closed(-0.99, 0.99) + } + fn reference(&self, x: &Float) -> Float { + reference::circular::acos(x) + } + fn compute_i16f16(&self, x: I16F16) -> I16F16 { + fixed_analytics::acos(x).unwrap_or(I16F16::ZERO) + } + fn compute_i32f32(&self, x: I32F32) -> I32F32 { + fixed_analytics::acos(x).unwrap_or(I32F32::ZERO) + } } struct Atan; impl TestedFunction for Atan { - fn name(&self) -> &'static str { "atan" } - fn domain(&self) -> Domain { Domain::Closed(-100.0, 100.0) } - fn reference(&self, x: &Float) -> Float { reference::circular::atan(x) } - fn compute_i16f16(&self, x: I16F16) -> I16F16 { fixed_analytics::atan(x) } - fn compute_i32f32(&self, x: I32F32) -> I32F32 { fixed_analytics::atan(x) } + fn name(&self) -> &'static str { + "atan" + } + fn domain(&self) -> Domain { + Domain::Closed(-100.0, 100.0) + } + fn reference(&self, x: &Float) -> Float { + reference::circular::atan(x) + } + fn compute_i16f16(&self, x: I16F16) -> I16F16 { + fixed_analytics::atan(x) + } + fn compute_i32f32(&self, x: I32F32) -> I32F32 { + fixed_analytics::atan(x) + } } diff --git a/tools/accuracy-bench/src/functions/exponential.rs b/tools/accuracy-bench/src/functions/exponential.rs index f4a111b..3c2af3e 100644 --- a/tools/accuracy-bench/src/functions/exponential.rs +++ b/tools/accuracy-bench/src/functions/exponential.rs @@ -1,52 +1,108 @@ -use crate::{reference, Domain, TestedFunction}; +use crate::{Domain, TestedFunction, reference}; use fixed::types::{I16F16, I32F32}; use rug::Float; pub fn register() -> Vec> { - vec![Box::new(Exp), Box::new(Ln), Box::new(Log2), Box::new(Log10), Box::new(Pow2)] + vec![ + Box::new(Exp), + Box::new(Ln), + Box::new(Log2), + Box::new(Log10), + Box::new(Pow2), + ] } struct Exp; impl TestedFunction for Exp { - fn name(&self) -> &'static str { "exp" } - fn domain(&self) -> Domain { Domain::Closed(-10.0, 8.0) } - fn reference(&self, x: &Float) -> Float { reference::exponential::exp(x) } - fn compute_i16f16(&self, x: I16F16) -> I16F16 { fixed_analytics::exp(x) } - fn compute_i32f32(&self, x: I32F32) -> I32F32 { fixed_analytics::exp(x) } + fn name(&self) -> &'static str { + "exp" + } + fn domain(&self) -> Domain { + Domain::Closed(-10.0, 8.0) + } + fn reference(&self, x: &Float) -> Float { + reference::exponential::exp(x) + } + fn compute_i16f16(&self, x: I16F16) -> I16F16 { + fixed_analytics::exp(x) + } + fn compute_i32f32(&self, x: I32F32) -> I32F32 { + fixed_analytics::exp(x) + } } struct Ln; impl TestedFunction for Ln { - fn name(&self) -> &'static str { "ln" } - fn domain(&self) -> Domain { Domain::Closed(0.001, 1000.0) } - fn reference(&self, x: &Float) -> Float { reference::exponential::ln(x) } - fn compute_i16f16(&self, x: I16F16) -> I16F16 { fixed_analytics::ln(x).unwrap_or(I16F16::ZERO) } - fn compute_i32f32(&self, x: I32F32) -> I32F32 { fixed_analytics::ln(x).unwrap_or(I32F32::ZERO) } + fn name(&self) -> &'static str { + "ln" + } + fn domain(&self) -> Domain { + Domain::Closed(0.001, 1000.0) + } + fn reference(&self, x: &Float) -> Float { + reference::exponential::ln(x) + } + fn compute_i16f16(&self, x: I16F16) -> I16F16 { + fixed_analytics::ln(x).unwrap_or(I16F16::ZERO) + } + fn compute_i32f32(&self, x: I32F32) -> I32F32 { + fixed_analytics::ln(x).unwrap_or(I32F32::ZERO) + } } struct Log2; impl TestedFunction for Log2 { - fn name(&self) -> &'static str { "log2" } - fn domain(&self) -> Domain { Domain::Closed(0.01, 1000.0) } - fn reference(&self, x: &Float) -> Float { reference::exponential::log2(x) } - fn compute_i16f16(&self, x: I16F16) -> I16F16 { fixed_analytics::log2(x).unwrap_or(I16F16::ZERO) } - fn compute_i32f32(&self, x: I32F32) -> I32F32 { fixed_analytics::log2(x).unwrap_or(I32F32::ZERO) } + fn name(&self) -> &'static str { + "log2" + } + fn domain(&self) -> Domain { + Domain::Closed(0.01, 1000.0) + } + fn reference(&self, x: &Float) -> Float { + reference::exponential::log2(x) + } + fn compute_i16f16(&self, x: I16F16) -> I16F16 { + fixed_analytics::log2(x).unwrap_or(I16F16::ZERO) + } + fn compute_i32f32(&self, x: I32F32) -> I32F32 { + fixed_analytics::log2(x).unwrap_or(I32F32::ZERO) + } } struct Log10; impl TestedFunction for Log10 { - fn name(&self) -> &'static str { "log10" } - fn domain(&self) -> Domain { Domain::Closed(0.01, 1000.0) } - fn reference(&self, x: &Float) -> Float { reference::exponential::log10(x) } - fn compute_i16f16(&self, x: I16F16) -> I16F16 { fixed_analytics::log10(x).unwrap_or(I16F16::ZERO) } - fn compute_i32f32(&self, x: I32F32) -> I32F32 { fixed_analytics::log10(x).unwrap_or(I32F32::ZERO) } + fn name(&self) -> &'static str { + "log10" + } + fn domain(&self) -> Domain { + Domain::Closed(0.01, 1000.0) + } + fn reference(&self, x: &Float) -> Float { + reference::exponential::log10(x) + } + fn compute_i16f16(&self, x: I16F16) -> I16F16 { + fixed_analytics::log10(x).unwrap_or(I16F16::ZERO) + } + fn compute_i32f32(&self, x: I32F32) -> I32F32 { + fixed_analytics::log10(x).unwrap_or(I32F32::ZERO) + } } struct Pow2; impl TestedFunction for Pow2 { - fn name(&self) -> &'static str { "pow2" } - fn domain(&self) -> Domain { Domain::Closed(-10.0, 10.0) } - fn reference(&self, x: &Float) -> Float { reference::exponential::pow2(x) } - fn compute_i16f16(&self, x: I16F16) -> I16F16 { fixed_analytics::pow2(x) } - fn compute_i32f32(&self, x: I32F32) -> I32F32 { fixed_analytics::pow2(x) } + fn name(&self) -> &'static str { + "pow2" + } + fn domain(&self) -> Domain { + Domain::Closed(-10.0, 10.0) + } + fn reference(&self, x: &Float) -> Float { + reference::exponential::pow2(x) + } + fn compute_i16f16(&self, x: I16F16) -> I16F16 { + fixed_analytics::pow2(x) + } + fn compute_i32f32(&self, x: I32F32) -> I32F32 { + fixed_analytics::pow2(x) + } } diff --git a/tools/accuracy-bench/src/functions/hyperbolic.rs b/tools/accuracy-bench/src/functions/hyperbolic.rs index 7927f5e..e6dc31b 100644 --- a/tools/accuracy-bench/src/functions/hyperbolic.rs +++ b/tools/accuracy-bench/src/functions/hyperbolic.rs @@ -1,82 +1,168 @@ -use crate::{reference, Domain, TestedFunction}; +use crate::{Domain, TestedFunction, reference}; use fixed::types::{I16F16, I32F32}; use rug::Float; pub fn register() -> Vec> { vec![ - Box::new(Sinh), Box::new(Cosh), Box::new(Tanh), Box::new(Coth), - Box::new(Asinh), Box::new(Acosh), Box::new(Atanh), Box::new(Acoth), + Box::new(Sinh), + Box::new(Cosh), + Box::new(Tanh), + Box::new(Coth), + Box::new(Asinh), + Box::new(Acosh), + Box::new(Atanh), + Box::new(Acoth), ] } struct Sinh; impl TestedFunction for Sinh { - fn name(&self) -> &'static str { "sinh" } - fn domain(&self) -> Domain { Domain::Closed(-8.0, 8.0) } - fn reference(&self, x: &Float) -> Float { reference::hyperbolic::sinh(x) } - fn compute_i16f16(&self, x: I16F16) -> I16F16 { fixed_analytics::sinh(x) } - fn compute_i32f32(&self, x: I32F32) -> I32F32 { fixed_analytics::sinh(x) } + fn name(&self) -> &'static str { + "sinh" + } + fn domain(&self) -> Domain { + Domain::Closed(-8.0, 8.0) + } + fn reference(&self, x: &Float) -> Float { + reference::hyperbolic::sinh(x) + } + fn compute_i16f16(&self, x: I16F16) -> I16F16 { + fixed_analytics::sinh(x) + } + fn compute_i32f32(&self, x: I32F32) -> I32F32 { + fixed_analytics::sinh(x) + } } struct Cosh; impl TestedFunction for Cosh { - fn name(&self) -> &'static str { "cosh" } - fn domain(&self) -> Domain { Domain::Closed(-8.0, 8.0) } - fn reference(&self, x: &Float) -> Float { reference::hyperbolic::cosh(x) } - fn compute_i16f16(&self, x: I16F16) -> I16F16 { fixed_analytics::cosh(x) } - fn compute_i32f32(&self, x: I32F32) -> I32F32 { fixed_analytics::cosh(x) } + fn name(&self) -> &'static str { + "cosh" + } + fn domain(&self) -> Domain { + Domain::Closed(-8.0, 8.0) + } + fn reference(&self, x: &Float) -> Float { + reference::hyperbolic::cosh(x) + } + fn compute_i16f16(&self, x: I16F16) -> I16F16 { + fixed_analytics::cosh(x) + } + fn compute_i32f32(&self, x: I32F32) -> I32F32 { + fixed_analytics::cosh(x) + } } struct Tanh; impl TestedFunction for Tanh { - fn name(&self) -> &'static str { "tanh" } - fn domain(&self) -> Domain { Domain::Closed(-10.0, 10.0) } - fn reference(&self, x: &Float) -> Float { reference::hyperbolic::tanh(x) } - fn compute_i16f16(&self, x: I16F16) -> I16F16 { fixed_analytics::tanh(x) } - fn compute_i32f32(&self, x: I32F32) -> I32F32 { fixed_analytics::tanh(x) } + fn name(&self) -> &'static str { + "tanh" + } + fn domain(&self) -> Domain { + Domain::Closed(-10.0, 10.0) + } + fn reference(&self, x: &Float) -> Float { + reference::hyperbolic::tanh(x) + } + fn compute_i16f16(&self, x: I16F16) -> I16F16 { + fixed_analytics::tanh(x) + } + fn compute_i32f32(&self, x: I32F32) -> I32F32 { + fixed_analytics::tanh(x) + } } struct Coth; impl TestedFunction for Coth { - fn name(&self) -> &'static str { "coth" } - fn domain(&self) -> Domain { Domain::Closed(0.1, 10.0) } - fn reference(&self, x: &Float) -> Float { reference::hyperbolic::coth(x) } - fn compute_i16f16(&self, x: I16F16) -> I16F16 { fixed_analytics::coth(x).unwrap_or(I16F16::ZERO) } - fn compute_i32f32(&self, x: I32F32) -> I32F32 { fixed_analytics::coth(x).unwrap_or(I32F32::ZERO) } + fn name(&self) -> &'static str { + "coth" + } + fn domain(&self) -> Domain { + Domain::Closed(0.1, 10.0) + } + fn reference(&self, x: &Float) -> Float { + reference::hyperbolic::coth(x) + } + fn compute_i16f16(&self, x: I16F16) -> I16F16 { + fixed_analytics::coth(x).unwrap_or(I16F16::ZERO) + } + fn compute_i32f32(&self, x: I32F32) -> I32F32 { + fixed_analytics::coth(x).unwrap_or(I32F32::ZERO) + } } struct Asinh; impl TestedFunction for Asinh { - fn name(&self) -> &'static str { "asinh" } - fn domain(&self) -> Domain { Domain::Closed(-20.0, 20.0) } - fn reference(&self, x: &Float) -> Float { reference::hyperbolic::asinh(x) } - fn compute_i16f16(&self, x: I16F16) -> I16F16 { fixed_analytics::asinh(x) } - fn compute_i32f32(&self, x: I32F32) -> I32F32 { fixed_analytics::asinh(x) } + fn name(&self) -> &'static str { + "asinh" + } + fn domain(&self) -> Domain { + Domain::Closed(-20.0, 20.0) + } + fn reference(&self, x: &Float) -> Float { + reference::hyperbolic::asinh(x) + } + fn compute_i16f16(&self, x: I16F16) -> I16F16 { + fixed_analytics::asinh(x) + } + fn compute_i32f32(&self, x: I32F32) -> I32F32 { + fixed_analytics::asinh(x) + } } struct Acosh; impl TestedFunction for Acosh { - fn name(&self) -> &'static str { "acosh" } - fn domain(&self) -> Domain { Domain::Closed(1.01, 20.0) } - fn reference(&self, x: &Float) -> Float { reference::hyperbolic::acosh(x) } - fn compute_i16f16(&self, x: I16F16) -> I16F16 { fixed_analytics::acosh(x).unwrap_or(I16F16::ZERO) } - fn compute_i32f32(&self, x: I32F32) -> I32F32 { fixed_analytics::acosh(x).unwrap_or(I32F32::ZERO) } + fn name(&self) -> &'static str { + "acosh" + } + fn domain(&self) -> Domain { + Domain::Closed(1.01, 20.0) + } + fn reference(&self, x: &Float) -> Float { + reference::hyperbolic::acosh(x) + } + fn compute_i16f16(&self, x: I16F16) -> I16F16 { + fixed_analytics::acosh(x).unwrap_or(I16F16::ZERO) + } + fn compute_i32f32(&self, x: I32F32) -> I32F32 { + fixed_analytics::acosh(x).unwrap_or(I32F32::ZERO) + } } struct Atanh; impl TestedFunction for Atanh { - fn name(&self) -> &'static str { "atanh" } - fn domain(&self) -> Domain { Domain::Open(-0.99, 0.99) } - fn reference(&self, x: &Float) -> Float { reference::hyperbolic::atanh(x) } - fn compute_i16f16(&self, x: I16F16) -> I16F16 { fixed_analytics::atanh(x).unwrap_or(I16F16::ZERO) } - fn compute_i32f32(&self, x: I32F32) -> I32F32 { fixed_analytics::atanh(x).unwrap_or(I32F32::ZERO) } + fn name(&self) -> &'static str { + "atanh" + } + fn domain(&self) -> Domain { + Domain::Open(-0.99, 0.99) + } + fn reference(&self, x: &Float) -> Float { + reference::hyperbolic::atanh(x) + } + fn compute_i16f16(&self, x: I16F16) -> I16F16 { + fixed_analytics::atanh(x).unwrap_or(I16F16::ZERO) + } + fn compute_i32f32(&self, x: I32F32) -> I32F32 { + fixed_analytics::atanh(x).unwrap_or(I32F32::ZERO) + } } struct Acoth; impl TestedFunction for Acoth { - fn name(&self) -> &'static str { "acoth" } - fn domain(&self) -> Domain { Domain::OutsideUnit(1.01) } - fn reference(&self, x: &Float) -> Float { reference::hyperbolic::acoth(x) } - fn compute_i16f16(&self, x: I16F16) -> I16F16 { fixed_analytics::acoth(x).unwrap_or(I16F16::ZERO) } - fn compute_i32f32(&self, x: I32F32) -> I32F32 { fixed_analytics::acoth(x).unwrap_or(I32F32::ZERO) } + fn name(&self) -> &'static str { + "acoth" + } + fn domain(&self) -> Domain { + Domain::OutsideUnit(1.01) + } + fn reference(&self, x: &Float) -> Float { + reference::hyperbolic::acoth(x) + } + fn compute_i16f16(&self, x: I16F16) -> I16F16 { + fixed_analytics::acoth(x).unwrap_or(I16F16::ZERO) + } + fn compute_i32f32(&self, x: I32F32) -> I32F32 { + fixed_analytics::acoth(x).unwrap_or(I32F32::ZERO) + } } diff --git a/tools/accuracy-bench/src/lib.rs b/tools/accuracy-bench/src/lib.rs index bd188df..6f78b48 100644 --- a/tools/accuracy-bench/src/lib.rs +++ b/tools/accuracy-bench/src/lib.rs @@ -2,6 +2,7 @@ pub mod functions; pub mod metrics; +pub mod readme; pub mod reference; pub mod report; pub mod sampling; diff --git a/tools/accuracy-bench/src/main.rs b/tools/accuracy-bench/src/main.rs index e38e5f0..4817dca 100644 --- a/tools/accuracy-bench/src/main.rs +++ b/tools/accuracy-bench/src/main.rs @@ -3,9 +3,13 @@ //! Run with: cargo run --release //! Compare: cargo run --release -- --baseline path/to/baseline.json -use accuracy_bench::{build_registry, report::Report, sampling::SampleStrategy, test_function}; +use accuracy_bench::{ + build_registry, readme, report::Report, sampling::SampleStrategy, test_function, +}; use rayon::prelude::*; -use std::{env, fs, process}; +use std::{env, fs, path::Path, process}; + +const README_PATH: &str = "../../README.md"; fn main() { let args: Vec = env::args().collect(); @@ -26,7 +30,7 @@ fn main() { let registry = build_registry(); eprintln!("Testing {} functions...\n", registry.len()); - let results = registry + let results: Vec<_> = registry .par_iter() .map(|f| { eprintln!(" {}", f.name()); @@ -36,19 +40,76 @@ fn main() { let report = Report::new(results); + // Save JSON report fs::create_dir_all("reports").ok(); let json_path = format!("reports/accuracy-{}.json", report.timestamp); fs::write(&json_path, report.to_json()).expect("Failed to write report"); eprintln!("Report saved: {json_path}"); - if let Some(path) = baseline_path { - let passed = compare_and_report(&report, path); - process::exit(if passed { 0 } else { 1 }); + // Determine README path (handle running from different directories) + let readme_path = find_readme_path(); + + if let Some(baseline_path) = baseline_path { + // CI mode: verify README and compare to baseline + let mut all_passed = true; + + // Verify README is up-to-date + if let Some(ref path) = readme_path { + eprintln!("\nVerifying README accuracy section..."); + match readme::verify_readme(path, &report.results) { + Ok(()) => eprintln!("README: OK"), + Err(e) => { + eprintln!("README: FAILED\n{e}"); + all_passed = false; + } + } + } else { + eprintln!("\nWarning: Could not find README.md to verify"); + } + + // Compare to baseline + let baseline_passed = compare_and_report(&report, baseline_path); + if !baseline_passed { + all_passed = false; + } + + process::exit(if all_passed { 0 } else { 1 }); } else { + // Local mode: update README and print table + if let Some(ref path) = readme_path { + match readme::update_readme(path, &report.results) { + Ok(true) => eprintln!("README.md updated with latest accuracy data"), + Ok(false) => eprintln!("README.md already up-to-date"), + Err(e) => eprintln!("Warning: Could not update README: {e}"), + } + } + report.print_table(); } } +/// Find the README.md file, checking multiple possible locations. +fn find_readme_path() -> Option { + let candidates = [ + README_PATH, + "README.md", + "../README.md", + "../../README.md", + "../../../README.md", + ]; + + for candidate in candidates { + if Path::new(candidate).exists() + && let Ok(content) = fs::read_to_string(candidate) + && content.contains("fixed_analytics") + { + return Some(candidate.to_string()); + } + } + + None +} + fn compare_and_report(current: &Report, baseline_path: &str) -> bool { let baseline_json = match fs::read_to_string(baseline_path) { Ok(s) => s, @@ -94,10 +155,8 @@ fn compare_and_report(current: &Report, baseline_path: &str) -> bool { }; // Check I16F16 - let (passed_16, status_16) = check_regression( - baseline_fn.i16f16.rel_mean, - current_fn.i16f16.rel_mean, - ); + let (passed_16, status_16) = + check_regression(baseline_fn.i16f16.rel_mean, current_fn.i16f16.rel_mean); if !passed_16 { all_passed = false; } @@ -113,10 +172,8 @@ fn compare_and_report(current: &Report, baseline_path: &str) -> bool { ); // Check I32F32 - let (passed_32, status_32) = check_regression( - baseline_fn.i32f32.rel_mean, - current_fn.i32f32.rel_mean, - ); + let (passed_32, status_32) = + check_regression(baseline_fn.i32f32.rel_mean, current_fn.i32f32.rel_mean); if !passed_32 { all_passed = false; } diff --git a/tools/accuracy-bench/src/metrics.rs b/tools/accuracy-bench/src/metrics.rs index f3ee06b..d6cc0dd 100644 --- a/tools/accuracy-bench/src/metrics.rs +++ b/tools/accuracy-bench/src/metrics.rs @@ -68,26 +68,48 @@ impl ErrorStats { Self { count: abs_vals.len(), - abs_max, abs_mean, abs_p50, abs_p95, abs_p99, - rel_max, rel_mean, rel_p50, rel_p95, rel_p99, + abs_max, + abs_mean, + abs_p50, + abs_p95, + abs_p99, + rel_max, + rel_mean, + rel_p50, + rel_p95, + rel_p99, } } pub fn empty() -> Self { Self { count: 0, - abs_max: 0.0, abs_mean: 0.0, abs_p50: 0.0, abs_p95: 0.0, abs_p99: 0.0, - rel_max: 0.0, rel_mean: 0.0, rel_p50: 0.0, rel_p95: 0.0, rel_p99: 0.0, + abs_max: 0.0, + abs_mean: 0.0, + abs_p50: 0.0, + abs_p95: 0.0, + abs_p99: 0.0, + rel_max: 0.0, + rel_mean: 0.0, + rel_p50: 0.0, + rel_p95: 0.0, + rel_p99: 0.0, } } } fn mean(vals: &[f64]) -> f64 { - if vals.is_empty() { 0.0 } else { vals.iter().sum::() / vals.len() as f64 } + if vals.is_empty() { + 0.0 + } else { + vals.iter().sum::() / vals.len() as f64 + } } fn percentile(sorted: &[f64], p: f64) -> f64 { - if sorted.is_empty() { return 0.0; } + if sorted.is_empty() { + return 0.0; + } let idx = ((sorted.len() - 1) as f64 * p).round() as usize; sorted[idx.min(sorted.len() - 1)] } diff --git a/tools/accuracy-bench/src/readme.rs b/tools/accuracy-bench/src/readme.rs new file mode 100644 index 0000000..6401844 --- /dev/null +++ b/tools/accuracy-bench/src/readme.rs @@ -0,0 +1,222 @@ +//! README accuracy table generation and validation. + +use crate::FunctionResult; +use std::fmt::Write; + +const MARKER_START: &str = ""; +const MARKER_END: &str = ""; + +/// Generate the accuracy section content (without markers). +pub fn generate_accuracy_section(results: &[FunctionResult]) -> String { + let mut out = String::new(); + + writeln!(out, "### Accuracy\n").unwrap(); + writeln!( + out, + "Relative error statistics measured against MPFR reference implementations.\n" + ) + .unwrap(); + + // Combined table with both I16F16 and I32F32 + writeln!( + out, + "| Function | I16F16 Mean | I16F16 Median | I16F16 P95 | I32F32 Mean | I32F32 Median | I32F32 P95 |" + ) + .unwrap(); + writeln!( + out, + "|----------|-------------|---------------|------------|-------------|---------------|------------|" + ) + .unwrap(); + for r in results { + writeln!( + out, + "| {} | {:.2e} | {:.2e} | {:.2e} | {:.2e} | {:.2e} | {:.2e} |", + r.name, + r.i16f16.rel_mean, + r.i16f16.rel_p50, + r.i16f16.rel_p95, + r.i32f32.rel_mean, + r.i32f32.rel_p50, + r.i32f32.rel_p95 + ) + .unwrap(); + } + + out +} + +/// Update the README file with new accuracy data. +/// Returns Ok(true) if changes were made, Ok(false) if already up-to-date. +pub fn update_readme(readme_path: &str, results: &[FunctionResult]) -> Result { + let content = std::fs::read_to_string(readme_path) + .map_err(|e| format!("Failed to read {readme_path}: {e}"))?; + + let new_section = generate_accuracy_section(results); + let new_content = replace_section(&content, &new_section)?; + + if new_content == content { + return Ok(false); + } + + std::fs::write(readme_path, &new_content) + .map_err(|e| format!("Failed to write {readme_path}: {e}"))?; + + Ok(true) +} + +/// Verify the README accuracy section matches current results. +/// Returns Ok(()) if valid, Err with details if mismatched. +pub fn verify_readme(readme_path: &str, results: &[FunctionResult]) -> Result<(), String> { + let content = std::fs::read_to_string(readme_path) + .map_err(|e| format!("Failed to read {readme_path}: {e}"))?; + + let current = extract_section(&content)?; + let expected = generate_accuracy_section(results); + + let current_values = parse_table_values(¤t)?; + let expected_values = parse_table_values(&expected)?; + + let mut mismatches = Vec::new(); + + for (key, exp_val) in &expected_values { + match current_values.get(key) { + Some(cur_val) => { + // Allow 1% tolerance for floating-point formatting differences + let rel_diff = if *exp_val != 0.0 { + (cur_val - exp_val).abs() / exp_val.abs() + } else { + cur_val.abs() + }; + if rel_diff > 0.01 { + mismatches.push(format!( + " {}: README has {:.2e}, expected {:.2e}", + key, cur_val, exp_val + )); + } + } + None => { + mismatches.push(format!(" {}: missing from README", key)); + } + } + } + + // Check for extra entries in README + for key in current_values.keys() { + if !expected_values.contains_key(key) { + mismatches.push(format!(" {}: unexpected entry in README", key)); + } + } + + if mismatches.is_empty() { + Ok(()) + } else { + Err(format!( + "README accuracy section is out of date:\n{}\n\nRun `cargo run --release` in tools/accuracy-bench to update.", + mismatches.join("\n") + )) + } +} + +fn replace_section(content: &str, new_section: &str) -> Result { + let start_idx = content.find(MARKER_START) + .ok_or("README missing accuracy section start marker. Add where you want the table.")?; + let end_idx = content.find(MARKER_END) + .ok_or("README missing accuracy section end marker. Add after the start marker.")?; + + if end_idx <= start_idx { + return Err("ACCURACY_END marker must come after ACCURACY_START".to_string()); + } + + let mut result = String::new(); + result.push_str(&content[..start_idx]); + result.push_str(MARKER_START); + result.push('\n'); + result.push_str(new_section); + result.push_str(MARKER_END); + result.push_str(&content[end_idx + MARKER_END.len()..]); + + Ok(result) +} + +fn extract_section(content: &str) -> Result { + let start_idx = content + .find(MARKER_START) + .ok_or("README missing accuracy section start marker")?; + let end_idx = content + .find(MARKER_END) + .ok_or("README missing accuracy section end marker")?; + + let section_start = start_idx + MARKER_START.len(); + Ok(content[section_start..end_idx].to_string()) +} + +/// Parse all numeric values from the accuracy tables into a map. +/// Keys are like "sin/I16F16/mean", "cos/I32F32/p95", etc. +fn parse_table_values(section: &str) -> Result, String> { + let mut values = std::collections::HashMap::new(); + + for line in section.lines() { + let line = line.trim(); + + // Skip non-data lines + if !line.starts_with('|') || line.contains("---") || line.contains("Function") { + continue; + } + + // Parse table row: | func | i16f16_mean | i16f16_median | i16f16_p95 | i32f32_mean | i32f32_median | i32f32_p95 | + let parts: Vec<&str> = line + .split('|') + .map(|s| s.trim()) + .filter(|s| !s.is_empty()) + .collect(); + + if parts.len() >= 7 { + let func = parts[0]; + if let Ok(v) = parts[1].parse::() { + values.insert(format!("{}/I16F16/mean", func), v); + } + if let Ok(v) = parts[2].parse::() { + values.insert(format!("{}/I16F16/median", func), v); + } + if let Ok(v) = parts[3].parse::() { + values.insert(format!("{}/I16F16/p95", func), v); + } + if let Ok(v) = parts[4].parse::() { + values.insert(format!("{}/I32F32/mean", func), v); + } + if let Ok(v) = parts[5].parse::() { + values.insert(format!("{}/I32F32/median", func), v); + } + if let Ok(v) = parts[6].parse::() { + values.insert(format!("{}/I32F32/p95", func), v); + } + } + } + + Ok(values) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_parse_table_values() { + let section = r#" +### Accuracy + +Relative error statistics measured against MPFR reference implementations. + +| Function | I16F16 Mean | I16F16 Median | I16F16 P95 | I32F32 Mean | I32F32 Median | I32F32 P95 | +|----------|-------------|---------------|------------|-------------|---------------|------------| +| sin | 7.30e-5 | 6.05e-5 | 1.80e-4 | 1.41e-9 | 1.16e-9 | 3.49e-9 | +| cos | 7.96e-5 | 6.44e-5 | 2.03e-4 | 1.50e-9 | 1.20e-9 | 3.60e-9 | +"#; + let values = parse_table_values(section).unwrap(); + + assert!((values["sin/I16F16/mean"] - 7.30e-5).abs() < 1e-10); + assert!((values["sin/I32F32/mean"] - 1.41e-9).abs() < 1e-14); + assert!((values["cos/I16F16/p95"] - 2.03e-4).abs() < 1e-10); + } +} diff --git a/tools/accuracy-bench/src/reference.rs b/tools/accuracy-bench/src/reference.rs index abc2f41..837f9ef 100644 --- a/tools/accuracy-bench/src/reference.rs +++ b/tools/accuracy-bench/src/reference.rs @@ -5,22 +5,46 @@ use rug::Float; pub mod circular { use super::*; - pub fn sin(x: &Float) -> Float { x.clone().sin() } - pub fn cos(x: &Float) -> Float { x.clone().cos() } - pub fn tan(x: &Float) -> Float { x.clone().tan() } - pub fn asin(x: &Float) -> Float { x.clone().asin() } - pub fn acos(x: &Float) -> Float { x.clone().acos() } - pub fn atan(x: &Float) -> Float { x.clone().atan() } + pub fn sin(x: &Float) -> Float { + x.clone().sin() + } + pub fn cos(x: &Float) -> Float { + x.clone().cos() + } + pub fn tan(x: &Float) -> Float { + x.clone().tan() + } + pub fn asin(x: &Float) -> Float { + x.clone().asin() + } + pub fn acos(x: &Float) -> Float { + x.clone().acos() + } + pub fn atan(x: &Float) -> Float { + x.clone().atan() + } } pub mod hyperbolic { use super::*; - pub fn sinh(x: &Float) -> Float { x.clone().sinh() } - pub fn cosh(x: &Float) -> Float { x.clone().cosh() } - pub fn tanh(x: &Float) -> Float { x.clone().tanh() } - pub fn asinh(x: &Float) -> Float { x.clone().asinh() } - pub fn acosh(x: &Float) -> Float { x.clone().acosh() } - pub fn atanh(x: &Float) -> Float { x.clone().atanh() } + pub fn sinh(x: &Float) -> Float { + x.clone().sinh() + } + pub fn cosh(x: &Float) -> Float { + x.clone().cosh() + } + pub fn tanh(x: &Float) -> Float { + x.clone().tanh() + } + pub fn asinh(x: &Float) -> Float { + x.clone().asinh() + } + pub fn acosh(x: &Float) -> Float { + x.clone().acosh() + } + pub fn atanh(x: &Float) -> Float { + x.clone().atanh() + } pub fn coth(x: &Float) -> Float { Float::with_val(REFERENCE_PRECISION, 1.0) / x.clone().tanh() } @@ -31,14 +55,26 @@ pub mod hyperbolic { pub mod exponential { use super::*; - pub fn exp(x: &Float) -> Float { x.clone().exp() } - pub fn ln(x: &Float) -> Float { x.clone().ln() } - pub fn log2(x: &Float) -> Float { x.clone().log2() } - pub fn log10(x: &Float) -> Float { x.clone().log10() } - pub fn pow2(x: &Float) -> Float { x.clone().exp2() } + pub fn exp(x: &Float) -> Float { + x.clone().exp() + } + pub fn ln(x: &Float) -> Float { + x.clone().ln() + } + pub fn log2(x: &Float) -> Float { + x.clone().log2() + } + pub fn log10(x: &Float) -> Float { + x.clone().log10() + } + pub fn pow2(x: &Float) -> Float { + x.clone().exp2() + } } pub mod algebraic { use super::*; - pub fn sqrt(x: &Float) -> Float { x.clone().sqrt() } + pub fn sqrt(x: &Float) -> Float { + x.clone().sqrt() + } } diff --git a/tools/accuracy-bench/src/report.rs b/tools/accuracy-bench/src/report.rs index 8c25b39..55a26f4 100644 --- a/tools/accuracy-bench/src/report.rs +++ b/tools/accuracy-bench/src/report.rs @@ -21,9 +21,13 @@ impl Report { } pub fn print_table(&self) { - println!("\n================================================================================"); + println!( + "\n================================================================================" + ); println!(" ACCURACY REPORT"); - println!("================================================================================\n"); + println!( + "================================================================================\n" + ); let mut table = Table::new(); table.set_content_arrangement(ContentArrangement::Dynamic); diff --git a/tools/accuracy-bench/src/sampling.rs b/tools/accuracy-bench/src/sampling.rs index 7c01952..3f428e7 100644 --- a/tools/accuracy-bench/src/sampling.rs +++ b/tools/accuracy-bench/src/sampling.rs @@ -23,7 +23,9 @@ impl SampleStrategy { ); for &v in &[0.0, 1.0, -1.0, 0.5, -0.5, 2.0, -2.0] { - if v >= lo && v <= hi { points.push(v); } + if v >= lo && v <= hi { + points.push(v); + } } for i in 0..self.grid_points { @@ -33,7 +35,9 @@ impl SampleStrategy { let mut rng = self.seed; for _ in 0..self.random_points { - rng = rng.wrapping_mul(6364136223846793005).wrapping_add(1442695040888963407); + rng = rng + .wrapping_mul(6364136223846793005) + .wrapping_add(1442695040888963407); let t = (rng as f64) / (u64::MAX as f64); points.push(lo + t * (hi - lo)); }