diff --git a/.github/workflows/auto-tag.yml b/.github/workflows/auto-tag.yml new file mode 100644 index 0000000..05b6962 --- /dev/null +++ b/.github/workflows/auto-tag.yml @@ -0,0 +1,22 @@ +name: Auto-tag + +on: + push: + branches: [main] + +jobs: + tag: + runs-on: ubuntu-latest + permissions: + contents: write + steps: + - uses: actions/checkout@v4 + - name: Create tag if new version + run: | + VERSION=$(cargo metadata --no-deps --format-version 1 | jq -r '.packages[0].version') + if git ls-remote --tags origin "refs/tags/v$VERSION" | grep -q .; then + echo "Tag v$VERSION already exists, skipping" + else + git tag "v$VERSION" + git push origin "v$VERSION" + fi diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 994533a..184ecf1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -32,8 +32,8 @@ jobs: with: toolchain: ${{ matrix.rust }} - uses: Swatinem/rust-cache@v2 - - run: cargo test --all-features - - run: cargo test --doc --all-features + - run: cargo test --features std + - run: cargo test --doc --features std no-std: name: no_std @@ -53,8 +53,8 @@ jobs: components: rustfmt, clippy - uses: Swatinem/rust-cache@v2 - run: cargo fmt --all -- --check - - run: cargo clippy --all-targets --all-features -- -D warnings - - run: cargo doc --no-deps --all-features + - run: cargo clippy --all-targets --features std -- -D warnings + - run: cargo doc --no-deps --features std env: RUSTDOCFLAGS: -Dwarnings @@ -65,7 +65,7 @@ jobs: - uses: actions/checkout@v4 - uses: dtolnay/rust-toolchain@1.88 - uses: Swatinem/rust-cache@v2 - - run: cargo check --all-features + - run: cargo check --features std coverage: name: Coverage @@ -75,7 +75,7 @@ jobs: - uses: dtolnay/rust-toolchain@stable - uses: Swatinem/rust-cache@v2 - uses: taiki-e/install-action@cargo-tarpaulin - - run: cargo tarpaulin --out xml --all-features --exclude-files 'tools/*' + - run: cargo tarpaulin --out xml --features std --exclude-files 'tools/*' - uses: codecov/codecov-action@v4 with: token: ${{ secrets.CODECOV_TOKEN }} @@ -91,6 +91,29 @@ jobs: with: token: ${{ secrets.GITHUB_TOKEN }} + no-panic: + name: Verify No Panics + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@stable + - uses: Swatinem/rust-cache@v2 + + - name: Check all pub fns are annotated + run: | + pub_fns=$(grep -r '^pub fn ' src/ops/*.rs | wc -l) + annotations=$(grep -r 'no_panic::no_panic' src/ops/*.rs | wc -l) + echo "pub fn count: $pub_fns" + echo "no_panic annotations: $annotations" + if [ "$pub_fns" -ne "$annotations" ]; then + echo "::error::Not all public functions in src/ops/ have #[no_panic] annotation ($annotations/$pub_fns)" + grep -n '^pub fn ' src/ops/*.rs + exit 1 + fi + + - name: Build with no-panic verification + run: cargo build --profile no-panic-check --features verify-no-panic --bin verify_no_panic + bench-check: name: Benchmarks Compile runs-on: ubuntu-latest diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index e7eae0b..2d936e0 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -17,9 +17,9 @@ jobs: - uses: actions/checkout@v4 - uses: dtolnay/rust-toolchain@stable - uses: Swatinem/rust-cache@v2 - - run: cargo test --all-features + - run: cargo test --features std - run: cargo test --no-default-features - - run: cargo doc --no-deps --all-features + - run: cargo doc --no-deps --features std env: RUSTDOCFLAGS: -Dwarnings diff --git a/Cargo.toml b/Cargo.toml index 48a95a7..48f38f1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "fixed_analytics" -version = "0.5.1" +version = "1.0.0" edition = "2024" rust-version = "1.88" authors = ["David Gathercole"] @@ -13,28 +13,40 @@ keywords = ["cordic", "fixed-point", "trigonometry", "math", "no_std"] categories = ["mathematics", "no-std", "algorithms", "embedded"] [package.metadata.docs.rs] -all-features = true +features = ["std"] rustdoc-args = ["--cfg", "docsrs"] [features] default = ["std"] std = [] +verify-no-panic = ["dep:no-panic"] [dependencies] -fixed = "1" +fixed = "1.30" +no-panic = { version = "0.1", optional = true } [dev-dependencies] -criterion = { version = "0.8.1", features = ["html_reports"] } +criterion = { version = "0.8", features = ["html_reports"] } [[bench]] name = "benchmarks" harness = false +[[bin]] +name = "verify_no_panic" +required-features = ["verify-no-panic"] + [profile.release] lto = true codegen-units = 1 panic = "abort" +[profile.no-panic-check] +inherits = "release" +lto = "fat" +codegen-units = 1 +panic = "unwind" + [profile.bench] lto = true codegen-units = 1 diff --git a/README.md b/README.md index 0ff024a..2586f9f 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # fixed_analytics -Fixed-point mathematical functions which are accurate, fast, safe, and machine independent. +Fixed-point mathematical functions which are accurate, deterministic, and guaranteed not to panic. [![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.5.1" +fixed_analytics = "1.0.0" ``` For `no_std` environments: ```toml [dependencies] -fixed_analytics = { version = "0.5.1", default-features = false } +fixed_analytics = { version = "1.0.0", default-features = false } ``` ## Available Functions @@ -54,7 +54,7 @@ fixed_analytics = { version = "0.5.1", default-features = false } | Exponential | `exp`, `pow2` | `ln`, `log2`, `log10` | | Algebraic | — | `sqrt` | -Functions are calculated via CORDIC, Newton-Raphson, and Taylor series techniques. +Functions are calculated via CORDIC, Newton-Raphson, and Taylor series techniques. Complete absence of panic is verified at the linker level via the [`no-panic`](https://github.com/dtolnay/no-panic) crate. ### Saturation Behavior @@ -75,7 +75,7 @@ Where for `tan`, "pole" refers to ±π/2, ±3π/2, ±5π/2, ... ### Accuracy -Relative error statistics measured against MPFR reference implementations. The file tools/accuracy-bench/baseline.json contains further measurements. +Relative error statistics measured against MPFR reference implementations. Accuracy regressions are not permitted; every change is benchmarked against the baseline before merging. The file tools/accuracy-bench/baseline.json contains further measurements. | Function | I16F16 Mean | I16F16 Median | I16F16 P95 | I32F32 Mean | I32F32 Median | I32F32 P95 | |----------|-------------|---------------|------------|-------------|---------------|------------| diff --git a/src/bin/verify_no_panic.rs b/src/bin/verify_no_panic.rs new file mode 100644 index 0000000..6d1c735 --- /dev/null +++ b/src/bin/verify_no_panic.rs @@ -0,0 +1,56 @@ +//! Binary that instantiates every public function with a concrete type. +//! +//! This exists solely to trigger monomorphization so that `no_panic`'s +//! linker-level check can verify that no panic paths survive optimization. +//! It is only compiled under the `verify-no-panic` feature. + +#[cfg(not(feature = "verify-no-panic"))] +compile_error!("this binary should only be built with --features verify-no-panic"); + +use fixed::types::I16F16; +use fixed_analytics::bounded::{NonNegative, OpenUnitInterval}; +use fixed_analytics::ops::algebraic::sqrt_nonneg; +use fixed_analytics::ops::hyperbolic::atanh_open; +use fixed_analytics::{ + acos, acosh, acoth, asin, asinh, atan, atan2, atanh, cos, cosh, coth, exp, ln, log2, log10, + pow2, sin, sin_cos, sinh, sinh_cosh, sqrt, tan, tanh, +}; + +fn main() { + // Use black_box to prevent the optimizer from eliminating calls entirely. + let x = std::hint::black_box(I16F16::from_num(0.5)); + let y = std::hint::black_box(I16F16::from_num(0.25)); + + // Total functions (return T) + let _ = std::hint::black_box(sin(x)); + let _ = std::hint::black_box(cos(x)); + let _ = std::hint::black_box(tan(x)); + let _ = std::hint::black_box(sin_cos(x)); + let _ = std::hint::black_box(atan(x)); + let _ = std::hint::black_box(atan2(y, x)); + let _ = std::hint::black_box(exp(x)); + let _ = std::hint::black_box(pow2(x)); + let _ = std::hint::black_box(sinh(x)); + let _ = std::hint::black_box(cosh(x)); + let _ = std::hint::black_box(tanh(x)); + let _ = std::hint::black_box(sinh_cosh(x)); + let _ = std::hint::black_box(asinh(x)); + + // Fallible functions (return Result) + let _ = std::hint::black_box(asin(x)); + let _ = std::hint::black_box(acos(x)); + let _ = std::hint::black_box(sqrt(x)); + let _ = std::hint::black_box(ln(x)); + let _ = std::hint::black_box(log2(x)); + let _ = std::hint::black_box(log10(x)); + let _ = std::hint::black_box(acosh(I16F16::from_num(2))); + let _ = std::hint::black_box(atanh(x)); + let _ = std::hint::black_box(coth(x)); + let _ = std::hint::black_box(acoth(I16F16::from_num(2))); + + // Type-safe wrapper functions + let nn = NonNegative::new(x).unwrap(); + let _ = std::hint::black_box(sqrt_nonneg(nn)); + let ou = OpenUnitInterval::new(x).unwrap(); + let _ = std::hint::black_box(atanh_open(ou)); +} diff --git a/src/ops/algebraic.rs b/src/ops/algebraic.rs index 51c5534..2647aeb 100644 --- a/src/ops/algebraic.rs +++ b/src/ops/algebraic.rs @@ -9,6 +9,7 @@ use crate::traits::CordicNumber; /// # Errors /// Returns `DomainError` if `x < 0`. #[must_use = "returns the square root result which should be handled"] +#[cfg_attr(feature = "verify-no-panic", no_panic::no_panic)] pub fn sqrt(x: T) -> Result { NonNegative::new(x) .map(sqrt_nonneg) @@ -23,6 +24,7 @@ pub fn sqrt(x: T) -> Result { /// 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] +#[cfg_attr(feature = "verify-no-panic", no_panic::no_panic)] pub fn sqrt_nonneg(x: NonNegative) -> T { let x = x.get(); let zero = T::zero(); @@ -85,9 +87,9 @@ pub fn sqrt_nonneg(x: NonNegative) -> T { let new_guess = sum.saturating_mul(half); let diff = if new_guess > guess { - new_guess - guess + new_guess.saturating_sub(guess) } else { - guess - new_guess + guess.saturating_sub(new_guess) }; if diff <= epsilon { diff --git a/src/ops/circular.rs b/src/ops/circular.rs index 87d5e46..a8c5b76 100644 --- a/src/ops/circular.rs +++ b/src/ops/circular.rs @@ -8,6 +8,7 @@ use crate::traits::CordicNumber; /// Sine and cosine. More efficient than separate calls. Accepts any angle. #[must_use] +#[cfg_attr(feature = "verify-no-panic", no_panic::no_panic)] pub fn sin_cos(angle: T) -> (T, T) { let pi = T::pi(); let frac_pi_2 = T::frac_pi_2(); @@ -58,6 +59,7 @@ pub fn sin_cos(angle: T) -> (T, T) { /// Sine. Accepts any angle (reduced internally). #[inline] #[must_use] +#[cfg_attr(feature = "verify-no-panic", no_panic::no_panic)] pub fn sin(angle: T) -> T { sin_cos(angle).0 } @@ -65,6 +67,7 @@ pub fn sin(angle: T) -> T { /// Cosine. Accepts any angle (reduced internally). #[inline] #[must_use] +#[cfg_attr(feature = "verify-no-panic", no_panic::no_panic)] pub fn cos(angle: T) -> T { sin_cos(angle).1 } @@ -108,6 +111,7 @@ pub fn cos(angle: T) -> T { /// } /// ``` #[must_use] +#[cfg_attr(feature = "verify-no-panic", no_panic::no_panic)] pub fn tan(angle: T) -> T { let (s, c) = sin_cos(angle); s.div(c) @@ -118,6 +122,7 @@ pub fn tan(angle: T) -> T { /// # Errors /// Returns `DomainError` if `|x| > 1`. #[must_use = "returns the arcsine result which should be handled"] +#[cfg_attr(feature = "verify-no-panic", no_panic::no_panic)] pub fn asin(x: T) -> Result { let Some(unit_x) = UnitInterval::new(x) else { return Err(Error::domain("asin", "value in range [-1, 1]")); @@ -156,13 +161,15 @@ pub fn asin(x: T) -> Result { /// # Errors /// Returns `DomainError` if `|x| > 1`. #[must_use = "returns the arccosine result which should be handled"] +#[cfg_attr(feature = "verify-no-panic", no_panic::no_panic)] pub fn acos(x: T) -> Result { // acos(x) = π/2 - asin(x) - asin(x).map(|a| T::frac_pi_2() - a) + asin(x).map(|a| T::frac_pi_2().saturating_sub(a)) } /// Arctangent. Accepts any value. Returns angle in `(-π/2, π/2)`. #[must_use] +#[cfg_attr(feature = "verify-no-panic", no_panic::no_panic)] pub fn atan(x: T) -> T { let zero = T::zero(); let one = T::one(); @@ -192,6 +199,7 @@ pub fn atan(x: T) -> T { /// Four-quadrant arctangent. Returns angle in `[-π, π]`. Returns 0 for (0, 0). #[must_use] +#[cfg_attr(feature = "verify-no-panic", no_panic::no_panic)] pub fn atan2(y: T, x: T) -> T { let zero = T::zero(); let pi = T::pi(); diff --git a/src/ops/exponential.rs b/src/ops/exponential.rs index e2330c7..47655fe 100644 --- a/src/ops/exponential.rs +++ b/src/ops/exponential.rs @@ -37,6 +37,7 @@ use crate::traits::CordicNumber; /// } /// ``` #[must_use] +#[cfg_attr(feature = "verify-no-panic", no_panic::no_panic)] pub fn exp(x: T) -> T { let zero = T::zero(); let one = T::one(); @@ -55,7 +56,7 @@ pub fn exp(x: T) -> T { // Reduce positive values let mut i = 0; while reduced > ln2 && i < 64 { - reduced -= ln2; + reduced = reduced.saturating_sub(ln2); scale += 1; i += 1; } @@ -63,7 +64,7 @@ pub fn exp(x: T) -> T { // Reduce negative values i = 0; while reduced < -ln2 && i < 64 { - reduced += ln2; + reduced = reduced.saturating_add(ln2); scale -= 1; i += 1; } @@ -100,6 +101,7 @@ pub fn exp(x: T) -> T { /// # Errors /// Returns `DomainError` if `x ≤ 0`. #[must_use = "returns the natural logarithm result which should be handled"] +#[cfg_attr(feature = "verify-no-panic", no_panic::no_panic)] pub fn ln(x: T) -> Result { let zero = T::zero(); let one = T::one(); @@ -128,15 +130,15 @@ pub fn ln(x: T) -> Result { let mut i = 0; while normalized > two && i < 128 { normalized = normalized >> 1; - k_ln2 += ln2; + k_ln2 = k_ln2.saturating_add(ln2); i += 1; } // For small x (< 0.5), multiply by 2 repeatedly i = 0; while normalized < half && i < 128 { - normalized = normalized + normalized; - k_ln2 -= ln2; + normalized = normalized.saturating_add(normalized); + k_ln2 = k_ln2.saturating_sub(ln2); i += 1; } @@ -150,9 +152,9 @@ pub fn ln(x: T) -> Result { let arg = OpenUnitInterval::from_normalized_ln_arg(norm); let atanh_val = atanh_open(arg); - let ln_normalized = atanh_val + atanh_val; // 2 * atanh + let ln_normalized = atanh_val.saturating_add(atanh_val); // 2 * atanh - Ok(ln_normalized + k_ln2) + Ok(ln_normalized.saturating_add(k_ln2)) } /// Base-2 logarithm. Domain: `x > 0`. @@ -160,6 +162,7 @@ pub fn ln(x: T) -> Result { /// # Errors /// Returns `DomainError` if `x ≤ 0`. #[must_use = "returns the base-2 logarithm result which should be handled"] +#[cfg_attr(feature = "verify-no-panic", no_panic::no_panic)] pub fn log2(x: T) -> Result { let ln_x = ln(x)?; let ln_2 = T::ln_2(); @@ -171,6 +174,7 @@ pub fn log2(x: T) -> Result { /// # Errors /// Returns `DomainError` if `x ≤ 0`. #[must_use = "returns the base-10 logarithm result which should be handled"] +#[cfg_attr(feature = "verify-no-panic", no_panic::no_panic)] pub fn log10(x: T) -> Result { let ln_x = ln(x)?; let ln_10 = T::ln_10(); @@ -192,6 +196,7 @@ pub fn log10(x: T) -> Result { /// - **I16F16:** Saturates for x > ~15 or x < ~-16 /// - **I32F32:** Saturates for x > ~31 or x < ~-32 #[must_use] +#[cfg_attr(feature = "verify-no-panic", no_panic::no_panic)] pub fn pow2(x: T) -> T { let ln_2 = T::ln_2(); exp(x.saturating_mul(ln_2)) diff --git a/src/ops/hyperbolic.rs b/src/ops/hyperbolic.rs index 9155e51..788e47e 100644 --- a/src/ops/hyperbolic.rs +++ b/src/ops/hyperbolic.rs @@ -58,27 +58,25 @@ const ATANH_REDUCTION_THRESHOLD_I1F63: i64 = 0x6000_0000_0000_0000; /// at the same rate), so the relationship cosh²(x) - sinh²(x) = 1 /// will not hold for saturated outputs. #[must_use] +#[cfg_attr(feature = "verify-no-panic", no_panic::no_panic)] pub fn sinh_cosh(x: T) -> (T, T) { let zero = T::zero(); let one = T::one(); // Compute limit as 1 + fractional_part (~1.1182) let limit = one.saturating_add(T::from_i1f63(HYPERBOLIC_CONVERGENCE_LIMIT_FRAC_I1F63)); - // Handle argument reduction for large values - if x.abs() > limit { - // Use the identities: - // sinh(2x) = 2 * sinh(x) * cosh(x) - // cosh(2x) = cosh²(x) + sinh²(x) - let half_x = x >> 1; - let (sh, ch) = sinh_cosh(half_x); - - let sinh_result = sh.saturating_mul(ch).saturating_mul(T::two()); - let cosh_result = ch.saturating_mul(ch).saturating_add(sh.saturating_mul(sh)); - - return (sinh_result, cosh_result); + // Iterative argument reduction for large values. + // Count how many halvings are needed, then rebuild on the way back. + let mut reduced = x; + let mut depth: u32 = 0; + // Max depth bounded by bit width: each halving shifts right by 1, + // so after total_bits iterations the value is zero and below the limit. + while reduced.abs() > limit && depth < T::total_bits() { + reduced = reduced >> 1; + depth += 1; } - // For very small x, use Taylor series approximation to avoid CORDIC + // For very small reduced, use Taylor series approximation to avoid CORDIC // overshoot on the first iteration (where atanh(0.5) ≈ 0.549 is larger than x). // Use precision-dependent threshold and order. let threshold_bits = if T::frac_bits() >= 24 { @@ -87,41 +85,47 @@ pub fn sinh_cosh(x: T) -> (T, T) { TAYLOR_THRESHOLD_LOW_PREC_I1F63 }; let small_threshold = T::from_i1f63(threshold_bits); - if x.abs() < small_threshold { - let x_sq = x.saturating_mul(x); - let x_cu = x_sq.saturating_mul(x); + let (mut sh, mut ch) = if reduced.abs() < small_threshold { + let x_sq = reduced.saturating_mul(reduced); + let x_cu = x_sq.saturating_mul(reduced); let x_qu = x_sq.saturating_mul(x_sq); // Higher precision benefits from higher-order Taylor terms if T::frac_bits() >= 24 { // 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_5 = x_qu.saturating_mul(reduced); let x_6 = x_qu.saturating_mul(x_sq); - // 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 = 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_base = reduced.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_approx, cosh_approx) + } else { + // sinh(x) ≈ x + x³/6, cosh(x) ≈ 1 + x²/2 + x⁴/24 + let c = one.saturating_add(x_sq >> 1); + let cosh_approx = c.saturating_add(x_qu.div(T::from_num(24))); + let sinh_approx = reduced.saturating_add(x_cu.div(T::from_num(6))); + (sinh_approx, cosh_approx) } - // sinh(x) ≈ x + x³/6, cosh(x) ≈ 1 + x²/2 + x⁴/24 - let c = one.saturating_add(x_sq >> 1); - let cosh_approx = c.saturating_add(x_qu.div(T::from_num(24))); - let sinh_approx = x.saturating_add(x_cu.div(T::from_num(6))); - return (sinh_approx, cosh_approx); - } - - // For moderate x, use CORDIC directly. - // Hyperbolic CORDIC scales results by 1/K_h ≈ 1.2075. - // To compensate, we pre-multiply by 1/K_h (using precomputed constant). - let inv_gain = hyperbolic_gain_inv(); // 1/K_h ≈ 1.2075 + } else { + // For moderate x, use CORDIC directly. + let inv_gain = hyperbolic_gain_inv(); + let (cosh_val, sinh_val, _) = hyperbolic_rotation(inv_gain, zero, reduced); + (sinh_val, cosh_val) + }; - let (cosh_val, sinh_val, _) = hyperbolic_rotation(inv_gain, zero, x); + // Reconstruct via doubling: sinh(2x) = 2·sinh(x)·cosh(x), + // cosh(2x) = cosh²(x) + sinh²(x) + for _ in 0..depth { + let new_sh = sh.saturating_mul(ch).saturating_mul(T::two()); + let new_ch = ch.saturating_mul(ch).saturating_add(sh.saturating_mul(sh)); + sh = new_sh; + ch = new_ch; + } - (sinh_val, cosh_val) + (sh, ch) } /// Hyperbolic sine. @@ -144,6 +148,7 @@ pub fn sinh_cosh(x: T) -> (T, T) { /// with argument reduction for |x| > 1.118. #[inline] #[must_use] +#[cfg_attr(feature = "verify-no-panic", no_panic::no_panic)] pub fn sinh(x: T) -> T { sinh_cosh(x).0 } @@ -166,12 +171,14 @@ pub fn sinh(x: T) -> T { /// - **I32F32:** Saturates for |x| > ~21.5 #[inline] #[must_use] +#[cfg_attr(feature = "verify-no-panic", no_panic::no_panic)] pub fn cosh(x: T) -> T { sinh_cosh(x).1 } /// Hyperbolic tangent. Result in `(-1, 1)`. #[must_use] +#[cfg_attr(feature = "verify-no-panic", no_panic::no_panic)] pub fn tanh(x: T) -> T { let (s, c) = sinh_cosh(x); s.div(c) @@ -182,6 +189,7 @@ pub fn tanh(x: T) -> T { /// # Errors /// Returns `DomainError` if `x = 0`. #[must_use = "returns the hyperbolic cotangent result which should be handled"] +#[cfg_attr(feature = "verify-no-panic", no_panic::no_panic)] pub fn coth(x: T) -> Result { if x == T::zero() { return Err(Error::domain("coth", "non-zero value")); @@ -192,6 +200,7 @@ pub fn coth(x: T) -> Result { /// Inverse hyperbolic sine. Accepts any value. #[must_use] +#[cfg_attr(feature = "verify-no-panic", no_panic::no_panic)] pub fn asinh(x: T) -> T { if x == T::zero() { return T::zero(); @@ -212,6 +221,7 @@ pub fn asinh(x: T) -> T { /// # Errors /// Returns `DomainError` if `x < 1`. #[must_use = "returns the inverse hyperbolic cosine result which should be handled"] +#[cfg_attr(feature = "verify-no-panic", no_panic::no_panic)] pub fn acosh(x: T) -> Result { let at_least_one = AtLeastOne::new(x).ok_or_else(|| Error::domain("acosh", "value >= 1"))?; @@ -234,6 +244,7 @@ pub fn acosh(x: T) -> Result { /// # Errors /// Returns `DomainError` if `|x| ≥ 1`. #[must_use = "returns the inverse hyperbolic tangent result which should be handled"] +#[cfg_attr(feature = "verify-no-panic", no_panic::no_panic)] pub fn atanh(x: T) -> Result { OpenUnitInterval::new(x) .map(atanh_open) @@ -248,6 +259,7 @@ pub fn atanh(x: T) -> Result { /// Use this when the input is known to be in (-1, 1) through mathematical /// invariants (e.g., `x / sqrt(1 + x²)`). #[must_use] +#[cfg_attr(feature = "verify-no-panic", no_panic::no_panic)] pub fn atanh_open(x: OpenUnitInterval) -> T { atanh_core(x.get()) } @@ -263,31 +275,36 @@ fn atanh_core(x: T) -> T { let threshold = T::from_i1f63(ATANH_REDUCTION_THRESHOLD_I1F63); + // Fast path: no argument reduction needed, use CORDIC directly. if x.abs() <= threshold { - // Direct CORDIC computation let (_, _, z) = hyperbolic_vectoring(one, x, zero); return z; } - // Argument reduction using the identity: - // atanh(x) = atanh(a) + atanh((x - a) / (1 - a*x)) - // We use a = 0.5, for which atanh(0.5) is a precomputed constant. + // Argument reduction needed. Work with |x| and track sign. let half = T::half(); let atanh_half = T::from_i1f63(crate::tables::hyperbolic::ATANH_HALF); - let sign = if x.is_negative() { -one } else { one }; - let abs_x = x.abs(); - - // Compute reduced argument: (|x| - 0.5) / (1 - 0.5*|x|) - let numerator = abs_x - half; - let denominator = one - half.saturating_mul(abs_x); - let reduced = numerator.div(denominator); + let mut abs_x = x.abs(); + + // Iterative argument reduction: each step reduces |x| and accumulates + // atanh(0.5) into the result. Max iterations bounded by frac_bits since + // each reduction roughly halves the distance from the threshold. + let mut accumulated = zero; + let mut i: u32 = 0; + while abs_x > threshold && i < T::frac_bits() { + // atanh(x) = atanh(0.5) + atanh((x - 0.5) / (1 - 0.5*x)) + let numerator = abs_x.saturating_sub(half); + let denominator = one.saturating_sub(half.saturating_mul(abs_x)); + abs_x = numerator.div(denominator); + accumulated = accumulated.saturating_add(atanh_half); + i += 1; + } - // Recursively compute atanh of reduced argument - let atanh_reduced = atanh_core(reduced); + // Direct CORDIC computation on the reduced argument + let (_, _, z) = hyperbolic_vectoring(one, abs_x, zero); - // atanh(x) = sign * (atanh(0.5) + atanh(reduced)) - sign.saturating_mul(atanh_half.saturating_add(atanh_reduced)) + sign.saturating_mul(accumulated.saturating_add(z)) } /// Inverse hyperbolic cotangent. Domain: `|x| > 1`. @@ -295,6 +312,7 @@ fn atanh_core(x: T) -> T { /// # Errors /// Returns `DomainError` if `|x| ≤ 1`. #[must_use = "returns the inverse hyperbolic cotangent result which should be handled"] +#[cfg_attr(feature = "verify-no-panic", no_panic::no_panic)] pub fn acoth(x: T) -> Result { let one = T::one(); diff --git a/src/traits.rs b/src/traits.rs index 48dd169..0bbe64f 100644 --- a/src/traits.rs +++ b/src/traits.rs @@ -1,7 +1,7 @@ //! Trait definitions for types compatible with CORDIC algorithms. use core::ops::{Add, AddAssign, Mul, Neg, Shl, Shr, Sub, SubAssign}; -use fixed::traits::Fixed; +use fixed::traits::{Fixed, FixedSigned}; use fixed::types::extra::{IsLessOrEqual, LeEqU128, True, Unsigned}; use fixed::{FixedI8, FixedI16, FixedI32, FixedI64, FixedI128}; @@ -177,7 +177,7 @@ macro_rules! impl_cordic_generic { #[inline] fn abs(self) -> Self { - if self.is_negative() { -self } else { self } + FixedSigned::saturating_abs(self) } #[inline] @@ -276,7 +276,17 @@ macro_rules! impl_cordic_generic { #[inline] fn div(self, rhs: Self) -> Self { - self / rhs + match Fixed::checked_div(self, rhs) { + Some(v) => v, + // Division by zero or overflow: saturate based on sign agreement. + None => { + if self.is_negative() != rhs.is_negative() { + Self::MIN + } else { + Self::MAX + } + } + } } #[inline] diff --git a/tools/accuracy-bench/Cargo.toml b/tools/accuracy-bench/Cargo.toml index d31456a..48f753e 100644 --- a/tools/accuracy-bench/Cargo.toml +++ b/tools/accuracy-bench/Cargo.toml @@ -6,9 +6,9 @@ publish = false [dependencies] fixed_analytics = { path = "../.." } -fixed = "1" -rug = "1.27" -serde = { version = "1", features = ["derive"] } -serde_json = "1" -comfy-table = "7" +fixed = "1.30" +rug = "1.28" +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" +comfy-table = "7.2" rayon = "1.10" diff --git a/tools/accuracy-bench/baseline.json b/tools/accuracy-bench/baseline.json index a6ee989..62e77a7 100644 --- a/tools/accuracy-bench/baseline.json +++ b/tools/accuracy-bench/baseline.json @@ -1,5 +1,5 @@ { - "timestamp": 1767828080, + "timestamp": 1773659600, "results": [ { "name": "sin", @@ -186,28 +186,28 @@ "i16f16": { "count": 59007, "abs_max": 1.0772810843461684, - "abs_mean": 0.06303729385570156, + "abs_mean": 0.06303677330481015, "abs_p50": 0.004098231957545551, "abs_p95": 0.34691838864932834, "abs_p99": 0.6188876148160034, "rel_max": 0.0049975358676744045, - "rel_mean": 0.00018543208668244187, - "rel_p50": 0.00013881257641081612, - "rel_p95": 0.0005054464161457844, - "rel_p99": 0.000659939413180731 + "rel_mean": 0.00018236789394986054, + "rel_p50": 0.0001344182189823812, + "rel_p95": 0.0005027598796161228, + "rel_p99": 0.0006566622127170074 }, "i32f32": { "count": 59007, "abs_max": 0.008872128233271326, - "abs_mean": 1.396564603408181e-6, + "abs_mean": 1.396558004830551e-6, "abs_p50": 7.002165069991406e-8, "abs_p95": 5.8817661283683265e-6, "abs_p99": 0.000011134297892567702, "rel_max": 0.00020256339056303632, - "rel_mean": 1.058698242414027e-8, - "rel_p50": 2.3731117361650507e-9, - "rel_p95": 9.576798998259808e-9, - "rel_p99": 1.3814111163622916e-8 + "rel_mean": 1.0480732995514983e-8, + "rel_p50": 2.3544956150429304e-9, + "rel_p95": 9.303074988477954e-9, + "rel_p99": 1.285273873766771e-8 }, "samples_tested": 59007 }, @@ -216,12 +216,12 @@ "i16f16": { "count": 59007, "abs_max": 1.0772732080154128, - "abs_mean": 0.0630310547325392, + "abs_mean": 0.06303092881225811, "abs_p50": 0.004094172796769158, "abs_p95": 0.3469277889677187, "abs_p99": 0.6188732979264842, "rel_max": 0.0010691901922094571, - "rel_mean": 0.00017297585484030712, + "rel_mean": 0.00017285180819279924, "rel_p50": 0.00012268943900841178, "rel_p95": 0.0005004633648159678, "rel_p99": 0.0006510488630542412 @@ -229,12 +229,12 @@ "i32f32": { "count": 59007, "abs_max": 0.008869816353453075, - "abs_mean": 1.39642538721307e-6, + "abs_mean": 1.3964231030204069e-6, "abs_p50": 7.00661288988158e-8, "abs_p95": 5.882438131266099e-6, "abs_p99": 0.000011134392480016686, "rel_max": 0.00020245784587786796, - "rel_mean": 1.0250328326619625e-8, + "rel_mean": 1.0248053709258805e-8, "rel_p50": 2.073955819960864e-9, "rel_p95": 9.159278823861197e-9, "rel_p99": 1.2647926134213153e-8 @@ -246,28 +246,28 @@ "i16f16": { "count": 59007, "abs_max": 0.0001416839219182675, - "abs_mean": 0.000013029573082792987, - "abs_p50": 0.000013432866548424016, - "abs_p95": 0.00003051174541490731, - "abs_p99": 0.00006235363517403947, + "abs_mean": 0.000012729098112476967, + "abs_p50": 0.000013373680597172921, + "abs_p95": 0.000027615223236021613, + "abs_p99": 0.000057425093706209296, "rel_max": 0.004137583155192043, - "rel_mean": 0.000022637207713695504, - "rel_p50": 0.00001383955161030536, - "rel_p95": 0.00006125969525471107, - "rel_p99": 0.0002692388398758907 + "rel_mean": 0.000020784111846600086, + "rel_p50": 0.0000138370779552808, + "rel_p95": 0.000058866070650500954, + "rel_p99": 0.00018729767889252025 }, "i32f32": { "count": 59007, "abs_max": 0.000020695083288946314, - "abs_mean": 1.2095156976429015e-9, - "abs_p50": 1.2857270803579013e-10, - "abs_p95": 6.234735505650235e-10, - "abs_p99": 1.298984919628765e-9, + "abs_mean": 1.2050832301560682e-9, + "abs_p50": 1.2859258102793092e-10, + "abs_p95": 5.812544889849391e-10, + "abs_p99": 1.2296446916248982e-9, "rel_max": 0.00002564595268436019, - "rel_mean": 1.7074502719755845e-9, - "rel_p50": 1.312886102619071e-10, - "rel_p95": 1.220758508492699e-9, - "rel_p99": 6.378228306145412e-9 + "rel_mean": 1.6354159982679383e-9, + "rel_p50": 1.3132154604890873e-10, + "rel_p95": 1.2321919801854915e-9, + "rel_p99": 4.89240139683993e-9 }, "samples_tested": 59007 }, @@ -275,29 +275,29 @@ "name": "coth", "i16f16": { "count": 59003, - "abs_max": 0.009257363297408006, - "abs_mean": 0.00006385935542136119, - "abs_p50": 4.967882137663082e-6, - "abs_p95": 0.0001682281371442329, - "abs_p99": 0.0017492381017776282, - "rel_max": 0.0009808148379961716, - "rel_mean": 0.000017359339848271247, - "rel_p50": 4.861209237654236e-6, - "rel_p95": 0.00007231630780889138, - "rel_p99": 0.00027457495901270164 + "abs_max": 0.00186560782194789, + "abs_mean": 0.000027539381408770688, + "abs_p50": 4.961126731650722e-6, + "abs_p95": 0.00013365012785548913, + "abs_p99": 0.00048479706134862965, + "rel_max": 0.0003172539281558055, + "rel_mean": 0.000011996517339386098, + "rel_p50": 4.82788872128744e-6, + "rel_p95": 0.00005573355105518167, + "rel_p99": 0.00011790663206009863 }, "i32f32": { "count": 59003, "abs_max": 5.297342871246613e-6, - "abs_mean": 1.320947570471908e-9, - "abs_p50": 1.4142131909977707e-10, - "abs_p95": 3.025110828502875e-9, - "abs_p99": 3.047723495797072e-8, + "abs_mean": 1.0266911358088638e-9, + "abs_p50": 1.4142287341201154e-10, + "abs_p95": 3.0405140627465244e-9, + "abs_p99": 2.25219967120438e-8, "rel_max": 5.177780320012974e-6, - "rel_mean": 4.3357289970588107e-10, - "rel_p50": 1.3931982528997815e-10, - "rel_p95": 1.3067644570630913e-9, - "rel_p99": 5.16707300247154e-9 + "rel_mean": 3.9985892305619165e-10, + "rel_p50": 1.3934409121730228e-10, + "rel_p95": 1.298366831659975e-9, + "rel_p99": 4.015827932536157e-9 }, "samples_tested": 59003 }, @@ -305,16 +305,16 @@ "name": "asinh", "i16f16": { "count": 59007, - "abs_max": 0.009368954975967902, - "abs_mean": 0.0021266635260911025, - "abs_p50": 0.0014801997013025314, - "abs_p95": 0.006256595362091666, - "abs_p99": 0.008003853812445172, - "rel_max": 0.026957011464039424, - "rel_mean": 0.0006481167683768791, - "rel_p50": 0.0004840298276696915, - "rel_p95": 0.0017561901213065945, - "rel_p99": 0.002215571312015438 + "abs_max": 0.009323178608780402, + "abs_mean": 0.0021284888817507635, + "abs_p50": 0.0014844659980624009, + "abs_p95": 0.006257894227960303, + "abs_p99": 0.007984492050545633, + "rel_max": 0.016260839471297594, + "rel_mean": 0.0006442542549104709, + "rel_p50": 0.0004833604162310917, + "rel_p95": 0.001753899668466157, + "rel_p99": 0.0022055481830100744 }, "i32f32": { "count": 59007, @@ -335,16 +335,16 @@ "name": "acosh", "i16f16": { "count": 59001, - "abs_max": 0.009733816681792629, - "abs_mean": 0.0022251709451035586, - "abs_p50": 0.0015877818855538628, - "abs_p95": 0.006443120819416226, - "abs_p99": 0.008309283221763941, - "rel_max": 0.002639144908644039, - "rel_mean": 0.0006738966410851339, - "rel_p50": 0.0005199648063320356, - "rel_p95": 0.0017993633344763151, - "rel_p99": 0.002270953061885943 + "abs_max": 0.009688040314605129, + "abs_mean": 0.0022273528018676597, + "abs_p50": 0.0015914165377841627, + "abs_p95": 0.006447086883274444, + "abs_p99": 0.008295719356268272, + "rel_max": 0.002626733490764648, + "rel_mean": 0.0006736969672385671, + "rel_p50": 0.0005214367925851396, + "rel_p95": 0.0018006676087912823, + "rel_p99": 0.0022695168421162757 }, "i32f32": { "count": 59001, @@ -365,16 +365,16 @@ "name": "atanh", "i16f16": { "count": 59001, - "abs_max": 0.001042712906590637, - "abs_mean": 0.00005745413304488152, - "abs_p50": 0.000042381852155770616, - "abs_p95": 0.0001477479263376491, - "abs_p99": 0.0003605051872503928, - "rel_max": 0.6371890219738001, - "rel_mean": 0.0003884945546221465, - "rel_p50": 0.00007479710783610355, - "rel_p95": 0.0008262046645402671, - "rel_p99": 0.004189922942606896 + "abs_max": 0.000996936539403137, + "abs_mean": 0.000048225767174518445, + "abs_p50": 0.00003234791334638665, + "abs_p95": 0.00013384945319838693, + "abs_p99": 0.00036007364172752077, + "rel_max": 0.3650807884541502, + "rel_mean": 0.0003009640696123398, + "rel_p50": 0.0000590390888308655, + "rel_p95": 0.0006253162143217303, + "rel_p99": 0.003043533591188758 }, "i32f32": { "count": 59001, @@ -395,16 +395,16 @@ "name": "acoth", "i16f16": { "count": 59001, - "abs_max": 0.0006742792358886973, - "abs_mean": 0.00004786209394856838, - "abs_p50": 0.00004119574939574766, - "abs_p95": 0.00011414091901112977, - "abs_p99": 0.00014234721267197652, - "rel_max": 0.014055554282461207, - "rel_mean": 0.0025039738676213056, - "rel_p50": 0.001578162208162982, - "rel_p95": 0.008079244902209818, - "rel_p99": 0.010661069956830615 + "abs_max": 0.00063469181628939, + "abs_mean": 0.00003892003359818683, + "abs_p50": 0.00003349354136025079, + "abs_p95": 0.00009199005491542477, + "abs_p99": 0.00011657816953467709, + "rel_max": 0.01284496346263612, + "rel_mean": 0.0020995023707175223, + "rel_p50": 0.0013292638519312421, + "rel_p95": 0.006665326707366214, + "rel_p99": 0.009055939297598636 }, "i32f32": { "count": 59001, @@ -426,26 +426,26 @@ "i16f16": { "count": 59007, "abs_max": 0.46813349528929393, - "abs_mean": 0.008584803135554181, - "abs_p50": 0.000016375478616971473, - "abs_p95": 0.05339109960641508, - "abs_p99": 0.16808381906048453, + "abs_mean": 0.006142015618932389, + "abs_p50": 0.000014615890257533865, + "abs_p95": 0.03260455542874752, + "abs_p99": 0.1284817312330233, "rel_max": 0.3332355454934613, - "rel_mean": 0.011423961708018175, - "rel_p50": 0.00008313063134226579, - "rel_p95": 0.07867272148882169, + "rel_mean": 0.011417573122417932, + "rel_p50": 0.00006673575383040605, + "rel_p95": 0.07871431908682497, "rel_p99": 0.1835814167869461 }, "i32f32": { "count": 59007, "abs_max": 0.000014848937098577153, - "abs_mean": 4.591129582438186e-7, - "abs_p50": 3.602109027722733e-10, - "abs_p95": 3.300957587271114e-6, - "abs_p99": 7.671637376915896e-6, + "abs_mean": 4.428319596643952e-7, + "abs_p50": 2.8667312967911585e-10, + "abs_p95": 3.271608875365928e-6, + "abs_p99": 7.336121598200407e-6, "rel_max": 5.084182472060312e-6, - "rel_mean": 1.9143373009014e-7, - "rel_p50": 2.444515430731482e-9, + "rel_mean": 1.9135427833977108e-7, + "rel_p50": 2.2358414909904965e-9, "rel_p95": 1.2962977984451641e-6, "rel_p99": 3.2213312688224884e-6 }, @@ -455,16 +455,16 @@ "name": "ln", "i16f16": { "count": 59003, - "abs_max": 0.007028960622761815, - "abs_mean": 0.0000838118301668609, - "abs_p50": 0.00007251431725396884, - "abs_p95": 0.00020006211659051587, - "abs_p99": 0.0002573957974867369, - "rel_max": 0.08319029452329336, - "rel_mean": 0.00001986567678225745, - "rel_p50": 0.000012494171239045365, - "rel_p95": 0.00003892350594362073, - "rel_p99": 0.0000731862128693758 + "abs_max": 0.007120513357136815, + "abs_mean": 0.00006108372519995839, + "abs_p50": 0.0000507042797313062, + "abs_p95": 0.0001528340420877683, + "abs_p99": 0.00020458018328906036, + "rel_max": 0.01244179019784243, + "rel_mean": 0.000013511077843052894, + "rel_p50": 8.759334514528923e-6, + "rel_p95": 0.00002967197208423552, + "rel_p99": 0.00005295682216701669 }, "i32f32": { "count": 59003, @@ -485,16 +485,16 @@ "name": "log2", "i16f16": { "count": 59003, - "abs_max": 0.0006140250690256366, - "abs_mean": 0.00011891466211653321, - "abs_p50": 0.00010307461606018364, - "abs_p95": 0.0002832318869696593, - "abs_p99": 0.0003662766333043521, - "rel_max": 0.011873289826984924, - "rel_mean": 0.00001771172892316616, - "rel_p50": 0.000012356254980832067, - "rel_p95": 0.000038052260832786956, - "rel_p99": 0.00007053468077128446 + "abs_max": 0.0006598014362131366, + "abs_mean": 0.00008558332538114927, + "abs_p50": 0.00007083049510292483, + "abs_p95": 0.00021631260077370484, + "abs_p99": 0.00028929750223483097, + "rel_max": 0.014636494762760797, + "rel_mean": 0.000013259536025697493, + "rel_p50": 8.478787777090254e-6, + "rel_p95": 0.000029151110902145505, + "rel_p99": 0.00005287799348650687 }, "i32f32": { "count": 59003, @@ -515,16 +515,16 @@ "name": "log10", "i16f16": { "count": 59003, - "abs_max": 0.00018310546875, - "abs_mean": 0.000037289963196447204, - "abs_p50": 0.00003219926275743745, - "abs_p95": 0.00008958987203810942, - "abs_p99": 0.00011631138507617322, - "rel_max": 0.013356845368307679, - "rel_mean": 0.0000186368528692315, - "rel_p50": 0.000012807206636373906, - "rel_p95": 0.00003986763539042081, - "rel_p99": 0.00007370750128702391 + "abs_max": 0.0001983642578125, + "abs_mean": 0.00002802985157528529, + "abs_p50": 0.000023431607028001622, + "abs_p95": 0.00007010842908883319, + "abs_p99": 0.000092947326506998, + "rel_max": 0.020974729725972013, + "rel_mean": 0.000014423993800471001, + "rel_p50": 9.28207260892599e-6, + "rel_p95": 0.000031374333977432835, + "rel_p99": 0.00005676337528031852 }, "i32f32": { "count": 59003, @@ -546,28 +546,28 @@ "i16f16": { "count": 59007, "abs_max": 0.1240198076766319, - "abs_mean": 0.0034303713742522895, - "abs_p50": 0.000039326024889874134, - "abs_p95": 0.02136241704067743, - "abs_p99": 0.054646100538207065, + "abs_mean": 0.003254876418514731, + "abs_p50": 0.000029241600689067226, + "abs_p95": 0.019762391636334087, + "abs_p99": 0.052000798585595476, "rel_max": 0.015141079259996712, - "rel_mean": 0.0007346566450210694, - "rel_p50": 0.00006021458901555424, - "rel_p95": 0.004702059500442153, + "rel_mean": 0.0007282995676444927, + "rel_p50": 0.00004740369660883063, + "rel_p95": 0.004705167104145861, "rel_p99": 0.01027006938792182 }, "i32f32": { "count": 59007, "abs_max": 2.711145384637348e-6, - "abs_mean": 7.240070011031757e-8, - "abs_p50": 8.620442137896589e-10, - "abs_p95": 4.68344865112158e-7, - "abs_p99": 1.1136039574921597e-6, - "rel_max": 4.44557716494002e-6, - "rel_mean": 1.1722305125266224e-8, - "rel_p50": 1.2452105978093034e-9, - "rel_p95": 7.3844316765736e-8, - "rel_p99": 1.569632910531003e-7 + "abs_mean": 6.906259627837998e-8, + "abs_p50": 6.68742838882963e-10, + "abs_p95": 4.3179491626688105e-7, + "abs_p99": 1.0690903309296118e-6, + "rel_max": 2.384185791015625e-7, + "rel_mean": 1.1514619376993808e-8, + "rel_p50": 1.0561495138250126e-9, + "rel_p95": 7.384199695898233e-8, + "rel_p99": 1.5696090047105027e-7 }, "samples_tested": 59007 },