diff --git a/.claude/worktrees/agent-a42c1d4dbc1f4e25d b/.claude/worktrees/agent-a42c1d4dbc1f4e25d deleted file mode 160000 index d3180f03..00000000 --- a/.claude/worktrees/agent-a42c1d4dbc1f4e25d +++ /dev/null @@ -1 +0,0 @@ -Subproject commit d3180f03444badf329d248ec9bf0e81435dc7070 diff --git a/.claude/worktrees/python b/.claude/worktrees/python new file mode 160000 index 00000000..06ddb498 --- /dev/null +++ b/.claude/worktrees/python @@ -0,0 +1 @@ +Subproject commit 06ddb498122209b370d0cc690916f31c2b6705ef diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 00000000..6029389f --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,121 @@ +on: + push: + branches: + - master + - develop + pull_request: + branches: + - master + - develop + +name: Build + +# Build-only checks — none of these run `cargo test`; they verify the code compiles and documents: +# * `check` — the main workspace `cargo check` across stable / 1.85 / 1.68 (MSRV), +# pinning transitive deps + dropping incompatible ones for 1.68. +# * `rustdoc` — `cargo doc --workspace` with `-D warnings`, guarding against broken +# intra-doc links. The meta-crate `dashu` and `dashu-python` both name their +# lib `dashu`, so they collide on output and are documented separately. +# * `fuzz-check` — the workspace-excluded `fuzz/` crate (its `rug`-linked differentials run +# manually before a release); a compile-guard so it can't silently rot. +# * `build-benchmark`— the `benchmark/` scratchpad (builds with `--features gmp`). +# * `build-aarch64` — an aarch64 cross-build. +# The sibling `Tests` workflow runs the actual `cargo test` matrix. + +jobs: + check: + name: Check + runs-on: ubuntu-latest + strategy: + matrix: + rust: [stable, "1.85", "1.68"] + steps: + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@master + with: + toolchain: ${{ matrix.rust }} + - name: Drop postgres/diesel/dev-deps for Rust 1.68 + if: matrix.rust == '1.68' + run: python3 .github/workflows/drop_incompatible_deps_for_msrv.py + - name: Pin transitive deps for MSRV + if: matrix.rust != 'stable' + run: | + if [ "${{ matrix.rust }}" = "1.68" ]; then + echo "Pinning packages for Rust 1.68:" + echo " parking_lot -> 0.12.3" + echo " lock_api -> 0.4.12" + echo " quote -> 1.0.40" + echo " unicode-ident -> 1.0.13" + echo " zeroize -> 1.8.1" + cargo update -p parking_lot --precise 0.12.3 + cargo update -p lock_api --precise 0.4.12 + cargo update -p quote --precise 1.0.40 + cargo update -p unicode-ident --precise 1.0.13 + cargo update -p zeroize --precise 1.8.1 + elif [ "${{ matrix.rust }}" = "1.85" ]; then + echo "Pinning packages for Rust 1.85:" + echo " diesel -> 2.2.12" + cargo update -p diesel@2 --precise 2.2.12 + fi + - run: cargo check --all-features --tests + if: matrix.rust != '1.68' + - run: cargo check --workspace --exclude dashu-python --features "std,num-order,serde,zeroize,rand,num-traits_v02" + if: matrix.rust == '1.68' + + rustdoc: + name: cargo doc + runs-on: ubuntu-latest + env: + RUSTDOCFLAGS: -D warnings + steps: + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@master + with: + toolchain: stable + - name: cargo doc (workspace, all features) + # `dashu-python`'s lib is also named `dashu`, so it collides with the meta-crate's lib on + # the output path and must be documented in its own invocation. Both are checked. + run: | + cargo doc --workspace --exclude dashu-python --all-features --no-deps + cargo doc -p dashu-python --all-features --no-deps + + fuzz-check: + name: cargo check fuzz + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@master + with: + toolchain: stable + - name: Install GMP/MPFR/MPC build prerequisites + # `rug` pulls in `gmp-mpfr-sys`, which builds GMP/MPFR/MPC (needs `m4`); the -dev packages + # are installed as well in case the build picks up system libraries. + run: sudo apt-get update && sudo apt-get install -y libgmp-dev libmpfr-dev libmpc-dev m4 + - name: cargo check (fuzz crate, all targets) + run: cargo check --manifest-path fuzz/Cargo.toml --all-targets + + build-benchmark: + name: Build benchmark + runs-on: ubuntu-latest + env: + RUSTFLAGS: -D warnings + steps: + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@master + with: + toolchain: stable + - run: cargo build --features gmp + working-directory: benchmark + + build-aarch64: + name: Build aarch64 + runs-on: ubuntu-latest + env: + RUSTFLAGS: -D warnings + steps: + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@master + with: + toolchain: stable + targets: aarch64-unknown-linux-gnu + - run: cargo build --target aarch64-unknown-linux-gnu --all-features --workspace --exclude dashu-python diff --git a/.github/workflows/drop_incompatible_deps_for_msrv.py b/.github/workflows/drop_incompatible_deps_for_msrv.py index b465d2d9..3648602e 100644 --- a/.github/workflows/drop_incompatible_deps_for_msrv.py +++ b/.github/workflows/drop_incompatible_deps_for_msrv.py @@ -52,7 +52,7 @@ # resolver under the 1.68 build. The MSRV build only exercises `rand` # (== rand_v08); rand_v09 and rand_v010 are covered by the stable / 1.85 # `--all-features` jobs. -for manifest in ['Cargo.toml', 'integer/Cargo.toml', 'float/Cargo.toml', 'rational/Cargo.toml']: +for manifest in ['Cargo.toml', 'integer/Cargo.toml', 'float/Cargo.toml', 'rational/Cargo.toml', 'complex/Cargo.toml']: text = open(manifest).read() text = re.sub(r'^rand_v09 = .*\n', '', text, flags=re.MULTILINE) text = re.sub(r'^rand_v010 = .*\n', '', text, flags=re.MULTILINE) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 4713e7cf..a8979b56 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -11,45 +11,6 @@ on: name: Tests jobs: - check: - name: Check - runs-on: ubuntu-latest - strategy: - matrix: - rust: [stable, "1.85", "1.68"] - steps: - - uses: actions/checkout@v4 - - uses: dtolnay/rust-toolchain@master - with: - toolchain: ${{ matrix.rust }} - - name: Drop postgres/diesel/dev-deps for Rust 1.68 - if: matrix.rust == '1.68' - run: python3 .github/workflows/drop_incompatible_deps_for_msrv.py - - name: Pin transitive deps for MSRV - if: matrix.rust != 'stable' - run: | - if [ "${{ matrix.rust }}" = "1.68" ]; then - echo "Pinning packages for Rust 1.68:" - echo " parking_lot -> 0.12.3" - echo " lock_api -> 0.4.12" - echo " quote -> 1.0.40" - echo " unicode-ident -> 1.0.13" - echo " zeroize -> 1.8.1" - cargo update -p parking_lot --precise 0.12.3 - cargo update -p lock_api --precise 0.4.12 - cargo update -p quote --precise 1.0.40 - cargo update -p unicode-ident --precise 1.0.13 - cargo update -p zeroize --precise 1.8.1 - elif [ "${{ matrix.rust }}" = "1.85" ]; then - echo "Pinning packages for Rust 1.85:" - echo " diesel -> 2.2.12" - cargo update -p diesel@2 --precise 2.2.12 - fi - - run: cargo check --all-features --tests - if: matrix.rust != '1.68' - - run: cargo check --workspace --exclude dashu-python --features "std,num-order,serde,zeroize,rand,num-traits_v02" - if: matrix.rust == '1.68' - test: name: Test strategy: @@ -120,32 +81,6 @@ jobs: toolchain: stable - run: cargo test --no-default-features --features rand --workspace --exclude dashu-python - build-benchmark: - name: Build benchmark - runs-on: ubuntu-latest - env: - RUSTFLAGS: -D warnings - steps: - - uses: actions/checkout@v4 - - uses: dtolnay/rust-toolchain@master - with: - toolchain: stable - - run: cargo build --features gmp - working-directory: benchmark - - build-aarch64: - name: Build aarch64 - runs-on: ubuntu-latest - env: - RUSTFLAGS: -D warnings - steps: - - uses: actions/checkout@v4 - - uses: dtolnay/rust-toolchain@master - with: - toolchain: stable - targets: aarch64-unknown-linux-gnu - - run: cargo build --target aarch64-unknown-linux-gnu --all-features --workspace --exclude dashu-python - fmt: name: Rustfmt runs-on: ubuntu-latest diff --git a/Cargo.toml b/Cargo.toml index a87514f0..5d09e4e4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -25,27 +25,29 @@ members = [ "macros", "python", "rational", + "complex", ] exclude = ["benchmark", "fuzz"] -default-members = ["base", "integer", "float", "rational", "macros"] +default-members = ["base", "integer", "float", "rational", "complex", "macros"] [features] default = ["std", "num-order"] -std = ["dashu-base/std", "dashu-int/std", "dashu-float/std", "dashu-ratio/std"] +std = ["dashu-base/std", "dashu-int/std", "dashu-float/std", "dashu-ratio/std", "dashu-cmplx/std"] # stable features -serde = ["dashu-int/serde", "dashu-float/serde", "dashu-ratio/serde"] -num-order = ["dashu-int/num-order", "dashu-float/num-order", "dashu-ratio/num-order"] +serde = ["dashu-int/serde", "dashu-float/serde", "dashu-ratio/serde", "dashu-cmplx/serde"] +num-order = ["dashu-int/num-order", "dashu-float/num-order", "dashu-ratio/num-order", "dashu-cmplx/num-order"] tuning = ["dashu-int/tuning"] -zeroize = ["dashu-int/zeroize", "dashu-float/zeroize", "dashu-ratio/zeroize"] +zeroize = ["dashu-int/zeroize", "dashu-float/zeroize", "dashu-ratio/zeroize", "dashu-cmplx/zeroize"] # unstable features -rand = ["dashu-int/rand", "dashu-float/rand", "dashu-ratio/rand"] -rand_v08 = ["dashu-int/rand_v08", "dashu-float/rand_v08", "dashu-ratio/rand_v08"] -rand_v09 = ["dashu-int/rand_v09", "dashu-float/rand_v09", "dashu-ratio/rand_v09"] -rand_v010 = ["dashu-int/rand_v010", "dashu-float/rand_v010", "dashu-ratio/rand_v010"] -num-traits = ["dashu-int/num-traits", "dashu-float/num-traits", "dashu-ratio/num-traits"] -num-traits_v02 = ["dashu-int/num-traits_v02", "dashu-float/num-traits_v02", "dashu-ratio/num-traits_v02"] +rand = ["dashu-int/rand", "dashu-float/rand", "dashu-ratio/rand", "dashu-cmplx/rand"] +rand_v08 = ["dashu-int/rand_v08", "dashu-float/rand_v08", "dashu-ratio/rand_v08", "dashu-cmplx/rand_v08"] +rand_v09 = ["dashu-int/rand_v09", "dashu-float/rand_v09", "dashu-ratio/rand_v09", "dashu-cmplx/rand_v09"] +rand_v010 = ["dashu-int/rand_v010", "dashu-float/rand_v010", "dashu-ratio/rand_v010", "dashu-cmplx/rand_v010"] +num-traits = ["dashu-int/num-traits", "dashu-float/num-traits", "dashu-ratio/num-traits", "dashu-cmplx/num-traits"] +num-traits_v02 = ["dashu-int/num-traits_v02", "dashu-float/num-traits_v02", "dashu-ratio/num-traits_v02", "dashu-cmplx/num-traits_v02"] +num-complex = ["dashu-cmplx/num-complex"] # this feature enables all related features related to decimal crates. decimal-extras = ["dashu-float/postgres-types", "dashu-float/diesel"] @@ -59,4 +61,5 @@ dashu-base = { version = "0.4.3", default-features = false, path = "./base" } dashu-int = { version = "0.4.3", default-features = false, path = "./integer" } dashu-float = { version = "0.4.5", default-features = false, path = "./float" } dashu-ratio = { version = "0.4.3", default-features = false, path = "./rational", features = ['dashu-float'] } +dashu-cmplx = { version = "0.4.5", default-features = false, path = "./complex" } dashu-macros = { version = "0.4.2", default-features = false, path = "./macros" } diff --git a/README.md b/README.md index 8c26fae2..0b977b25 100644 --- a/README.md +++ b/README.md @@ -19,6 +19,7 @@ A library set of arbitrary precision numbers (aka. big numbers) implemented in R - [`dashu-int`](./integer): Arbitrary precision integers - [`dashu-float`](./float): Arbitrary precision floating point numbers - [`dashu-ratio`](./rational): Arbitrary precision rational numbers +- [`dashu-cmplx`](./complex): Arbitrary precision complex numbers - [`dashu-macros`](./macros): Macros for creating big numbers `dashu` is a meta crate that re-exports all the types from these sub-crates. Please see the README.md in each subdirectory for crate-specific introduction. diff --git a/TODO-v05.md b/TODO-v05.md index 2576036c..3f78b44d 100644 --- a/TODO-v05.md +++ b/TODO-v05.md @@ -1,6 +1,6 @@ # dashu v0.5 Release Plan -Last updated: 2026-06-27 +Last updated: 2026-06-28 This document is the consolidated plan for the **v0.5** release — a **major (breaking)** bump. Because it is a major release, its two organizing goals are: @@ -40,7 +40,7 @@ The phases below are ordered by dependency, not by "importance". The logic is: | 0 | Test / benchmark / fuzz hardening | **GATE for all feature work** | — | | 1 | Breaking changes & deprecation cleanup | must land in 0.5 | 0 (ideally) | | 2 | `dashu-float` shared constant cache | ✅ done (#83, as `CachedFBig`/`ConstCache`) | 0, 1 | -| 3 | `dashu-cmplx` (`CBig`) — new crate | headline feature | 0, 2 | +| 3 | `dashu-cmplx` (`CBig`) — new crate | ✅ done (M1–M6) | 0, 2 | | 4 | The mdBook guide | required deliverable | 1, 2, 3 (content); infra can start now | | 5 | Release prep & version sync | — | 1–4 | @@ -70,11 +70,8 @@ The phases below are ordered by dependency, not by "importance". The logic is: > by extracting the quadrant integer via `to_int`; see `float/CHANGELOG.md` and the > `test_trig_tiny_negative_no_panic` regression test. -One item remains open: - -- [ ] **Record baseline benchmark numbers** so Phase 2/3 perf regressions are detectable (criterion - `--save-baseline`, a committed comparison, or manual capture). The benches exist and compile in - CI; only the baseline-capture workflow is undecided. +> *No committed baseline.* Benchmark numbers are hardware-dependent, so none are checked in — run the +> benches locally before/after a perf-sensitive change to spot regressions. --- @@ -115,7 +112,7 @@ Every item here changes public API and **must** land in 0.5. File:line refs are > *Implemented in #83 and removed from this list:* the infinity/NaN panic policy (infinities are now > terminal values; `FpResult = Result, FpError>`; full IEEE-754 signed zero) — see -> `guide/src/ieee754.md` and `float/CHANGELOG.md`. +> `guide/src/compliance.md` and `float/CHANGELOG.md`. ### 1.4 `dashu-ratio` - [x] **`From for FBig` → `TryFrom`** (`rational/src/third_party/dashu_float.rs:12`): make the @@ -127,9 +124,12 @@ Every item here changes public API and **must** land in 0.5. File:line refs are TODO is the only fast-fmt item left for 0.5.x. Non-breaking internal perf, gated on 1.2. ### 1.5 Doc / internal (non-breaking, fold in opportunistically) -Move verbose type explanations from API docs into the guide (`integer/src/ubig.rs:10` TODO). -Internal algorithm TODOs (right-to-left exponentiation `pow.rs:67`, double-power avoidance -`float/src/div.rs:243`, guard-bit formulation `exp.rs:80`) can land anytime — batch them here. +- [ ] **Move verbose type prose to the guide** — `integer/src/ubig.rs:10` `TODO(v0.5)` (leave a brief + summary + link in the doc-comment). Pairs with Phase 4. +- [ ] **`integer/src/pow.rs:67`** — switch to right-to-left exponentiation (cheaper squaring schedule). +- [ ] **`float/src/div.rs:344`** — avoid the double power in the division kernel; let `q += q0` become + `|=` when `B` is a power of 2. +- [ ] **`float/src/exp.rs:87`** — write down the exact formulation of the required guard bits. --- @@ -174,58 +174,108 @@ the primary `Real`/`Decimal`. ## Phase 3 — `dashu-cmplx` (`CBig`) — Arbitrary-Precision Complex Numbers +> **✅ Implemented** (M1–M6 complete). The `dashu-cmplx` crate (dir `complex/`) provides `CBig` — two +> `Repr` parts over a single shared `Context` — targeting GNU MPC parity for the "common +> functionalities," built on a cached `dashu-float` (Phase 2). Two-layer API mirroring `FBig` +> (`Context::mul → CfpResult>` at the context layer; operators → `CBig` at the +> convenience layer), near-correct rounding via the guard-digit recipe, and the C99 Annex G / Kahan +> no-NaN model (C99 NaN-producing cases → `FpError`). The module layout mirrors `dashu-float` +> (`add`/`mul`/`div`; `exp` hosts the power family; `math/` for transcendentals; `repr.rs` for +> `Context`). Verified with proptest identities + self-oracles, deterministic Annex-G vectors, and a +> manual `rug::Complex`/MPC oracle in `fuzz/`. `NumHash` mirrors `num-complex`'s `Complex` +> algebraic hash (verified against the `num-order` reference). See `complex/CHANGELOG.md` (0.5.0). + **Goal:** a new crate `dashu-cmplx` (dir `complex/`) providing an arbitrary-precision complex type `CBig`, targeting GNU MPC parity for "common functionalities." It composes two parts (`re`, `im`) over a shared precision, with a single rounding mode applied to both components. -### 3.1 Type & context model -- [ ] `CBig { re: Repr, im: Repr, context: Context }` — +### 3.1 Type & context model — ✅ +- [x] `CBig { re: Repr, im: Repr, context: Context }` — two parts over a single shared `Context` (re/im kept at the same precision; MPC allows different precisions but we start uniform — simpler, matches `FBig`'s single-context model). -- [ ] A single `R: Round` applies to both the real and imaginary parts (simpler than MPC's `(R, R)` - pair; per-axis independent rounding is deferred to 0.5.x). Reuse dashu-float's `Round` trait; no - new rounding machinery. -- [ ] Constants: `CBig::ZERO`, `ONE`, `I` (the imaginary unit). No `INFINITY` constant — complex +- [x] A single `R: Round` applies to both the real and imaginary parts (simpler than MPC's `(R, R)` + pair; per-axis independent rounding is deferred to 0.5.x). Reuses `dashu-float`'s `Round` trait; + no new rounding machinery. +- [x] Constants: `CBig::ZERO`, `ONE`, `I` (the imaginary unit). No `INFINITY` constant — complex infinity is the single Riemann point produced by `proj` (`+∞ + i·0`), per the C99 Annex G model `dashu-float` already follows (`Repr` already encodes ±∞). -### 3.2 Core surface for v0.5 ("common functionalities") -- [ ] **Construction & conversion:** `from_parts`, `from_real`, `from_int`, parse/`FromStr`, - conversions to/from primitives (`num_complex::Complex` interop is deferred to 0.5.x). -- [ ] **Field arithmetic:** `add`, `sub`, `mul`, `div`, `neg`, `sqr`, `inv`, `powi` (integer power), - scalar `mul`/`div` by real `FBig` (via mixed-type operators, not named methods), and operator - overloads. **Near-correctly-rounded** `mul`/`div` via Smith's method + guard-digit re-round - (mirroring `FBig`'s own transcendentals; a guaranteed-correct Ziv loop is deferred to 0.5.x). -- [ ] **Comparison:** `PartialEq`/`Eq`, a lexicographic `Ord` (by `re`, then `im`), and +### 3.2 Core surface for v0.5 ("common functionalities") — ✅ +- [x] **Construction & conversion:** `from_parts`, `From`/`From`/`From`, + `TryFrom for FBig`/`for IBig`, `FromStr`, `TryFrom`/`` (`num_complex::Complex` + interop is deferred to 0.5.x — see 3.4). +- [x] **Field arithmetic:** `add`/`sub`/`mul`/`div`/`neg`/`sqr`/`inv`, `powi`, scalar `mul`/`div` by + real `FBig` (mixed-type operators, not named methods), and operator overloads. + **Near-correctly-rounded** `mul`/`div` via Smith's method + guard-digit re-round (mirroring + `FBig`'s own transcendentals; a guaranteed-correct Ziv loop is deferred to 0.5.x). +- [x] **Comparison:** `PartialEq`/`Eq`, a lexicographic `Ord` (by `re`, then `im`), and `AbsOrd`/`NumOrd`/`NumHash` — mirroring `FBig`'s surface, not MPC's "complex has no order" stance. -- [ ] **Decomposition / misc:** `re()`, `imag()`, `conj()`, `abs()` (modulus), `norm()` (squared - modulus), `arg()` (principal argument), `proj()` (Riemann projection), `mul_i`/`-i`. -- [ ] **Powers & elementary transcendentals:** `sqrt` (non-negative real part; ties to non-negative - imaginary), `exp`, `log` (principal, branch cut on negative real axis, `Im ∈ ]-π, π]`), +- [x] **Decomposition / misc:** `re()`/`imag()`/`into_parts()`/`from_parts()`, `conj()`, `abs()` + (modulus via `hypot`), `norm()` (squared modulus), `arg()` (principal argument), `proj()` + (Riemann projection), `mul_i()`. +- [x] **Powers & elementary transcendentals:** `sqrt` (non-negative real part; ties to non-negative + imaginary), `exp`, `log` (principal, branch cut on the negative real axis, `Im ∈ ]-π, π]`), `powf` (complex^complex) and `powi` (complex^integer), `sin`, `cos`, `tan`, `sin_cos`, `asin`, `acos`, `atan`. - *Reuse `FBig`'s real implementations; the complex identities are* + *Reuses `FBig`'s real implementations; the complex identities are* `exp(x+iy)=eˣ(cos y + i sin y)`, `log z = ln|z| + i·arg z`, and `sin/cos` via the real–imaginary form using `FBig`'s `sin`/`cos` + `sinh`/`cosh` (`exp(±iz)` only as a test cross-check). -- [ ] **I/O:** `Display`/`Debug`/`FromStr` in algebraic `a+bi` form (the `num-complex` idiom, not +- [x] **I/O:** `Display`/`Debug`/`FromStr` in algebraic `a+bi` form (the `num-complex` idiom, not MPC's `(re im)` parenthesized pair). -- [ ] **Integration:** add `complex/` to the workspace `members`/`default-members`; re-export as - `dashu::complex` and alias `dashu::Complex = CBig` (alongside `Real`/`Decimal`/…). - -### 3.3 Correctness bar -- [ ] Follow **C99 Annex G / Kahan** branch cuts and principal values exactly (table in the MPC - research notes; key: `sqrt`/`log` cut on `]-∞, 0]`, `atan`/`tanh` on two cuts, etc.). -- [ ] Signed-zero and infinite-operand edge cases (the `powf(0,0) = 1` rule, `proj` on infinities, - C99 NaN-producing cases mapped to `FpError`) — wire into the `FpResult` machinery in - dashu-float (#83). -- [ ] **Fuzz vs MPC/rug oracle**: add property tests (identities: `exp(log z) ≈ z`, - `log z · conj` realness, `sin²+cos²≈1`, de Moivre) and rug/MPC oracle comparisons at random - precisions — same pattern established in Phase 0.2. - -### 3.4 Deferred to post-0.5 *(explicitly out of scope for this release)* -Hyperbolic family (`sinh/cosh/tanh/asinh/acosh/atanh`), `fma`, `rootofunity`, `agm`, -`exp2/exp10/log2/log10`, vector ops (`sum`/`dot`), `serde`/`rkyv` for `CBig`, and the experimental -ball-arithmetic (`mpcb_t`) analogue. (These can be additive point releases under 0.5.x.) +- [x] **Integration:** `complex/` in the workspace `members`/`default-members`; re-exported as + `dashu::complex` with alias `dashu::Complex = CBig`. The `cbig!`/`static_cbig!` literal macros + shipped (M6). `rand` generation: `UniformCBig` (box sampler) + builtin `Standard`/`Open01`/ + `OpenClosed01` (unit square), default `rand_v08` with `rand_v09`/`rand_v010` opt-in. + +### 3.3 Correctness bar — ✅ +- [x] Follows **C99 Annex G / Kahan** branch cuts and principal values exactly (`sqrt`/`log` cut on + `]-∞, 0]`, etc.). +- [x] Signed-zero and infinite-operand edge cases (`powf(0,0) = 1`, `proj` on infinities, C99 + NaN-producing cases mapped to `FpError`), wired into the `FpResult`/`CfpResult` machinery. +- [x] **Fuzz vs MPC/rug oracle**: property tests (identities: `exp(log z) ≈ z`, `log z · conj` + realness, `sin²+cos²≈1`, de Moivre) in `complex/tests/{arith,rounding,transcendental}_prop.rs`, + deterministic Annex-G vectors in `special_values.rs`, and `rug::Complex`/MPC oracle comparisons + in the manual `fuzz/` crate. + +### 3.4 Deferred to v0.5.x *(explicitly out of scope for this release)* + +Consolidated from the original `CBig` design doc (`TODO-cmplx.md`, now folded into this section and +removed). All additive — safe as point releases under 0.5.x. + +- **Guaranteed-correct rounding (Ziv retry loop)** — 0.5 ships near-correct guard-digit rounding + (matching `FBig`); a Ziv loop is expected to land in `FBig` first, then inherited by `CBig`. +- **Complex hyperbolic & inverse-hyperbolic family** (`sinh`/`cosh`/`tanh`/`asinh`/`acosh`/`atanh`). + (Real hyperbolics already exist on `Context` and are *used* by `CBig` trig in 0.5; the + complex-valued functions themselves are deferred.) +- **`fma`** (complex fused multiply-add — hard to round correctly), **`rootofunity`**, complex + **`agm`**, **`exp2`/`exp10`/`log2`/`log10`**. +- **Vector ops** (`sum`/`dot`/mean) — note `Sum`/`Product` for `CBig` (the `iter` analog of `FBig`) + are also not yet implemented. +- **Third-party integration:** `CBig` `serde`/`rkyv`/`zeroize`; `num_complex::Complex` interop + (the `serde`/`num-traits`/`num-complex` feature flags are scaffolded; impls deferred). +- **Independent re/im rounding** (`CRound` trait; MPC `mpc_rnd_t` parity — 0.5 uses one `R` for both + parts). +- **A `ComplexFloat`-style trait** unifying `FBig` and `CBig` (sealed, for generic real/complex code). +- **Ball arithmetic** (the `mpcb_t` analogue — interval/uncertainty complex). +- **`CachedCBig`** — a cache-backed variant mirroring `CachedFBig`. Its structure is settled (so 0.5 + is forward-compatible): it wraps a `CBig` plus a shared `Rc>` + handle, reusing `ConstCache` unchanged from `dashu-float` (there are no complex-specific constants + to cache — `CBig`'s transcendentals are built entirely from real `FBig` ops). `CachedCBig` is + `!Send + !Sync` while `CBig` stays `Send + Sync` (so `static_cbig!` produces `CBig`). **This is why + 0.5 already threads `cache: Option<&mut ConstCache>` through the transcendental `Context` ops:** the + convenience layer passes `None`, `CachedCBig` will pass `Some(&mut cache)`, so adding the cached + variant needs no signature change. +- **Expose ownership-aware kernel functions from `dashu-float`** — `dashu-float`'s `add.rs` already + has `add_val_val` / `add_val_ref` / `add_ref_val` / `add_ref_ref` kernel functions that consume + owned `FBig`/`Repr` when available (avoiding unnecessary clones at the convenience layer). These are + currently `pub(crate)`; they should be made `pub` (or mirrored as `pub` methods on `Context` like + `add_val_val(&self, lhs: Repr, rhs: Repr)`) so that `dashu-cmplx` can call them directly in + its own per-ownership kernel functions instead of immediately borrowing every `CBig` operand through + `Context::add(&CBig, &CBig)` (which takes `&Repr` internally and clones as needed). The same applies + to `sub`/`mul`/`div` and potentially to the transcendental ops. Without this, `CBig`'s by-value + operator impls (e.g. `impl Add for CBig`) take ownership but cannot exploit it — they immediately + borrow their parts through the complex `Context`, which in turn borrows the real `Context`, and the + ownership advantage is lost. --- @@ -282,26 +332,6 @@ and nothing in CI builds or deploys it. --- -## Open Decisions (need maintainer input) - -These shape the plan but don't block starting Phase 0/1. Recommended defaults are marked **(rec)**. - -1. **`CBig` scope for v0.5.** Ship core arithmetic + elementary transcendentals (`sqrt/exp/log/pow/ - sin/cos/tan/asin/acos/atan`) + abs/arg/conj/proj + I/O; defer hyperbolics/fma/agm/vector-ops/ - ball-arith. **(rec)** — matches "common functionalities" and is a defensible 0.5 cut. -2. **Float serde precision padding** (`serde.rs:39`). Apply (pad leading zeros) in 0.5 since - formats are changing anyway. **(rec)**. *Resolved (Phase 1): applied — human-readable FBig serde - now pads to `precision` significant digits.* -3. **Property-testing framework.** Adopt `proptest`. **(rec)**; add `bolero` later if we want - coverage-guided fuzzing (Phase 0.4 P3). -4. **MSRV.** Keep 1.68 unless a concrete 0.5 feature requires newer. **(rec: keep)**. - -> *Resolved in #83:* the cache thread-safety model (`Rc` in `CachedFBig` + `Send + Sync` -> `ConstCache`), `Context` losing `Copy` (kept `Copy` — cache moved into `CachedFBig`), and the float -> infinity/NaN panic policy (infinities are terminal values; no NaN). - ---- - ## Risk Register | Risk | Mitigation | @@ -318,6 +348,16 @@ These shape the plan but don't block starting Phase 0/1. Recommended defaults ar ## Out of Scope for v0.5 - `dashu-python` remains excluded and out of the release critical path (per `AGENTS.md`). -- Complex hyperbolics, `fma`, `rootofunity`, `agm`, vector ops, ball arithmetic — deferred to 0.5.x. -- Guaranteed-correct Ziv rounding loop, `CBig` serde/rkyv, and `num_complex` interop — deferred (additive). +- All `dashu-cmplx` follow-ups (complex hyperbolics, `fma`, `rootofunity`, `agm`, Ziv correct + rounding, `CBig` serde/rkyv/zeroize, `num_complex` interop, `CachedCBig`, ball arithmetic, + `CRound` independent re/im rounding, vector ops) — see §3.4 for the full consolidated list. +- The full **C `` type-generic math surface** — the complete C standard math library for + *both* real and complex (trig & inverse; hyperbolic & inverse; exp/log family including + `exp2`/`exp10`/`expm1`/`log2`/`log10`/`log1p`; power/root `cbrt`/`hypot`/`pow`/`sqrt`; error & gamma + `erf`/`erfc`/`tgamma`/`lgamma`; `fma`; rounding/remainder; fp-classification), unified by a + type-generic `ComplexFloat`-style trait dispatching over `FBig`/`CBig` + ([ref](https://en.cppreference.com/c/header/tgmath)). Desirable as a long-term goal, but explicitly + out of scope for **0.5 and 0.5.x**; the individual pieces already deferred to 0.5.x (complex + hyperbolics, `fma`, `exp2`/`log2`, …, see §3.4) are the first incremental steps toward it. - Any MSRV bump — deferred unless forced. +- SIMD optimized FFT multiplications - it seems that we can leverage the `wide` crate for this, but this won't be considered until v1.0 \ No newline at end of file diff --git a/complex/CHANGELOG.md b/complex/CHANGELOG.md new file mode 100644 index 00000000..25e2f140 --- /dev/null +++ b/complex/CHANGELOG.md @@ -0,0 +1,106 @@ +# Changelog + +## Unreleased + +### Improve +- The complex `sin_cos` kernel now calls `dashu-float`'s combined `sinh_cosh` (new in `dashu-float`) + instead of separate `sinh` + `cosh` calls, sharing the `exp_m1(±y)` sub-computations. +- `CBig::overflow` now returns `+∞ + i·∞` (both parts infinite) instead of `±∞ + i·0`, fixing the + questionably asymmetric infinity representation on overflow. `underflow` still preserves the + signed-zero distinction (`+0` vs `-0`). +- Defined `Uniform01` in `rand.rs` (mirroring `dashu-float`'s `Uniform01`), wrapping two + per-part `Uniform01` samplers for custom-precision unit-square sampling. +- The `Add`/`Sub`/`AddAssign`/`SubAssign` trait impls now use the shared `impl_cbig_binop!` + macro (same as `Mul`/`Div`), replacing the intermediate kernel functions. +- Added `impl_cbig_scalar_binop!` macro in `helper_macros.rs` generating the four ref/val + `CBig op FBig` impls for both `Mul` (componentwise `mul_real`) and `Div` (componentwise + `div_real`), replacing the manually-unrolled impl blocks. +- The `abs_ge` helper in `div.rs` is replaced by inline `FBig::abs_cmp` comparisons at the + two Smith-method branch points. + +### Change +- `complex/src/sub.rs` is merged into `complex/src/add.rs`, mirroring `dashu-float`'s layout where + addition and subtraction share one file and one operator kernel (subtraction is addition of a + negated right operand). The `Add`/`Sub`/`AddAssign`/`SubAssign` impls are now written out + explicitly for all four ref/val combinations — forwarding through `core::mem::take` to the + by-value operator — instead of being generated by the shared binop macro. No behavioral change. +- `complex/src/power.rs` is merged into `complex/src/exp.rs`, mirroring `dashu-float`, which keeps + the power family (`powi`/`powf`) alongside `exp` in a single module. No behavioral change. +- The `CBig · CBig` `Mul`/`MulAssign` impls now use the shared `impl_cbig_binop!` macro (matching + `Div`/`Add`/`Sub`), replacing the manually-written four ref/val combinations. No behavioral change. +- `complex/src/trig.rs` is promoted to a `math` submodule (`complex/src/math/{mod.rs,trig.rs}`), + mirroring `dashu-float`'s `pub mod math` (which groups the transcendentals). `dashu-cmplx` reuses + `dashu-float`'s hyperbolic/constant-cache machinery, so only `trig` is populated for now. No + behavioral change. +- `complex/src/context.rs` is renamed to `complex/src/repr.rs`, mirroring `dashu-float`, where + `Context` lives in `repr.rs`. All `crate::context::` paths are now `crate::repr::`. No behavioral + change. +- `CBig::imag()` is renamed to `CBig::im()` (shorter, matches `num-complex`'s convention). +- `CBig::log()` is renamed to `CBig::ln()` (the primary name; the context-layer method is still + `log`). The `log` convenience alias is removed — consistent with `FBig`'s naming. +- `CBig::from_repr_parts` (a `pub` const constructor) is removed; the existing `CBig::new` is made + `pub` instead (from_repr_parts and new were identical). +- `CBig::inv()` as a standalone method is removed in favor of `impl Inverse for CBig` / `impl + Inverse for &CBig` (the `dashu_base::Inverse` trait, matching `FBig`). +- The `Mul`/`MulAssign` impls now use the shared `impl_cbig_binop!` macro (like `Div`), replacing + the manually-written four ref/val combinations. +- The parameterless `impl_scalar_mul!()` and `impl_scalar_div!()` macros (each invoked exactly + once) are unrolled into inline impl blocks, each calling the context-layer method directly instead + of forwarding through `&self / &rhs`. +- The `cbig_one`/`cbig_real`/`reround`/`ok_exact_zero`/`ok_exact_one` helper functions in + `math/trig.rs` are inlined at their call sites. +- `is_numeric_zero` (a one-line `||` helper) is inlined at its four call sites and removed. +- The `num-order` feature no longer pulls in `num-complex` (it stopped enabling + `num-order/num-complex`), and `num-complex` is no longer an unconditional dependency: it is now an + opt-in normal dependency behind the `num-complex` feature (for the conversions below), plus a + dev-dependency (the `num-order` `NumHash` test compares against `num-complex`'s live `Complex` + reference). Enabling only `num-order` (the default) therefore no longer drags `num-complex` into + the dependency tree. + +### Fix +- Fixed broken intra-doc links surfaced by `cargo doc -D warnings`: the public `powf` docs no longer + link to the `pub(crate)` `Context::guard`, and `dashu_float::Rounded` is corrected to + `dashu_float::round::Rounded`. +- `CBig`'s `NumHash` now mirrors the `num-order` crate's `Complex` hashing (algebraic + combination of the per-part residues `a + ∓PROOT²·b²`, not a sequential tuple hash), so a `CBig` + and a `num-complex` `Complex` of the same value produce the same hash. Verified against + `num-order`'s `f64`/`Complex` reference. The *bterm* computation is extracted into a + shared helper, so the test no longer duplicates the hash formula. +- The inline `Display`/`FromStr` unit tests failed to compile under `no_std` + (`cargo test --no-default-features`): the test modules now import `alloc::format`. +- `is_unit` in `fmt.rs` now uses `IBig::ONE` and `IBig::NEG_ONE` constants rather than + `IBig::from(±1)`. +- Copyright year updated from 2022 to 2026 in `lib.rs`. + +### Add +- New `num-complex` feature providing `TryFrom` conversions between `CBig` and `num-complex`'s + `Complex`/`Complex` (base-2, composing through `FBig`): lifting is exact with NaN → + `OutOfBounds`, infinities and signed zeros preserved; rounding back errors on overflow or + inexactness — mirroring `FBig`'s primitive-float `TryFrom` pair. +- New crate `dashu-cmplx` providing the arbitrary-precision complex number type [`CBig`], built on top of + [`dashu-float`]'s `FBig`. Each `CBig` stores a real and an imaginary part (`Repr`) over a single shared + precision and rounding mode, mirroring `FBig`'s `Repr`+`Context` layout. +- Two-layer API mirroring `FBig`: context-layer operations on [`Context`] return a `CfpResult` + (`Result, FpError>`) carrying per-axis inexactness `(Rounding, Rounding)`, while the + convenience layer (`CBig::add`, operators) unwraps to a plain `CBig` (panicking on `Indeterminate` / + `OutOfDomain` / `InfiniteInput`, saturating `Overflow`/`Underflow` to signed infinity/zero). +- Field arithmetic: `add`/`sub`/`neg`/`sqr`/`mul`/`div`/`inv` plus scalar `mul`/`div` by a real `FBig` + through mixed-type operators. `mul`/`div`/`sqr`/`inv` are near-correctly rounded via the guard-digit + recipe (mirroring `FBig`'s transcendentals; a guaranteed-correct Ziv loop is deferred to 0.5.x). +- Integer power `powi` (repeated squaring) and complex power `powf` (`exp(w·log z)`). +- Decomposition & misc: `re`/`imag`/`into_parts`/`from_parts`, `conj`/`proj`/`mul_i`, `abs` (`hypot`), + `norm` (squared modulus), `arg` (`atan2`). +- Transcendentals: `sqrt`, `exp`, `log`, `sin`/`cos`/`tan`/`sin_cos`, `asin`/`acos`/`atan`. Complex trig + uses the real–imaginary decomposition reusing `FBig`'s `sinh`/`cosh`. +- Comparison surface mirroring `FBig`: lexicographic `Ord`/`PartialOrd` (by `re`, then `im`), `AbsOrd`, + and `NumOrd`/`NumHash` (behind the `num-order` feature). +- Algebraic `"a+bi"` `Display`/`FromStr`, structured `Debug`, and the `I`/`ZERO`/`ONE` constants. +- The `cbig!` / `static_cbig!` literal macros (in `dashu-macros`) for creating `CBig` from a complex + literal (`a+bi` or `re, im`); exposed as `dashu::cbig!` in the meta-crate. +- Random generation via `rand`: the `rand` feature (aliasing `rand_v08`, with `rand_v09`/`rand_v010` + opt-in, matching the other crates). `UniformCBig` samples the box `[low, high)`; the builtin + `Standard`/`StandardUniform`/`Open01`/`OpenClosed01` sample the unit square `[0,1)²` (each part an + independent uniform `FBig`). Reuses `dashu-float`'s `UniformFBig` — no bespoke sampling algorithm. +- No-NaN policy: C99 NaN-producing cases are mapped to `FpError` at the context layer (and panics at the + convenience layer), consistent with `FBig`. Signed zero and the C99 Annex G / Kahan branch-cut model + are first-class (reusing `FBig`'s signed-zero predicates). diff --git a/complex/Cargo.toml b/complex/Cargo.toml new file mode 100644 index 00000000..898d28ba --- /dev/null +++ b/complex/Cargo.toml @@ -0,0 +1,90 @@ +[package] +name = "dashu-cmplx" +version = "0.4.5" +authors = ["Jacob Zhong "] +edition = "2021" +description = "A big arbitrary precision complex number library" +keywords = ["mathematics", "numerics", "complex", "arbitrary-precision", "bignum"] +categories = ["mathematics", "no-std"] +license = "MIT OR Apache-2.0" +repository = "https://github.com/cmpute/dashu" +homepage = "https://github.com/cmpute/dashu" +documentation = "https://docs.rs/dashu-cmplx" +readme = "README.md" +rust-version = "1.68" + +[package.metadata.docs.rs] +all-features = true + +[features] +default = ["std", "num-order"] +std = ["dashu-float/std"] + +# stable dependencies +serde = ["dep:serde", "dashu-float/serde"] +zeroize = ["dep:zeroize", "dashu-float/zeroize"] +num-order = ["dep:num-order", "dep:_num-modular", "dashu-float/num-order"] + +# unstable dependencies +rand = ["rand_v08"] +rand_v08 = ["dep:rand_v08", "dashu-float/rand_v08", "dashu-int/rand_v08"] +rand_v09 = ["dep:rand_v09", "dashu-float/rand_v09", "dashu-int/rand_v09"] +rand_v010 = ["dep:rand_v010", "dashu-float/rand_v010", "dashu-int/rand_v010"] +num-traits = ["num-traits_v02"] +num-traits_v02 = ["dep:num-traits_v02", "dashu-float/num-traits_v02"] +num-complex = ["dep:num-complex_v04"] + +[dependencies] +dashu-base = { version = "0.4.3", default-features = false, path = "../base" } +dashu-int = { version = "0.4.3", default-features = false, path = "../integer" } +dashu-float = { version = "0.4.5", default-features = false, path = "../float" } + +# stable dependencies +rustversion = "1.0.0" +num-order = { optional = true, version = "1.2.0", default-features = false } +_num-modular = { optional = true, version = "0.6.1", package = "num-modular", default-features = false } +serde = { optional = true, version = "1.0.130", default-features = false } +zeroize = { optional = true, version = "1.5.7", default-features = false } + +# unstable dependencies +rand_v08 = { optional = true, version = "0.8.3", package = "rand", default-features = false } +rand_v09 = { optional = true, version = "0.9", package = "rand", default-features = false } +rand_v010 = { optional = true, version = "0.10", package = "rand", default-features = false } +num-traits_v02 = { optional = true, version = "0.2.15", package = "num-traits", default-features = false } +num-complex_v04 = { optional = true, version = "0.4", package = "num-complex", default-features = false } + +[dev-dependencies] +rand_v08 = { version = "0.8.3", package = "rand" } +rand_v09 = { version = "0.9", package = "rand" } +rand_v010 = { version = "0.10", package = "rand" } +proptest = "~1.7" + +# num-complex is needed only by tests: as the live reference for `CBig`'s `NumHash` (the +# `num-order` feature gates the impl, but the `num-order/num-complex` sub-feature that puts +# `NumHash` on `Complex` is test-only), and for the `num-complex` conversion tests. +num-complex_v04 = { version = "0.4", package = "num-complex" } +num-order = { version = "1.2.0", default-features = false, features = ["num-complex"] } + +criterion = { version = "0.5.1", features = ["html_reports"] } + +[lib] +bench = false + +[[test]] +name = "random" +required-features = ["rand"] + +[[bench]] +name = "arith" +harness = false +required-features = ["rand"] + +[[bench]] +name = "transcendental" +harness = false +required-features = ["rand"] + +[[bench]] +name = "io" +harness = false +required-features = ["rand"] diff --git a/complex/README.md b/complex/README.md new file mode 100644 index 00000000..743fabfb --- /dev/null +++ b/complex/README.md @@ -0,0 +1,25 @@ +# dashu-cmplx + +A big arbitrary precision complex number library, implemented in pure Rust. + +`dashu-cmplx` provides the arbitrary-precision complex number type [`CBig`], built on top of +[`dashu-float`](https://docs.rs/dashu-float)'s `FBig`. It is the Rust-native alternative to **GNU MPC**, +targeting MPC parity for the common functionalities (field arithmetic + elementary transcendentals + +abs/arg/conj/proj + I/O). + +Each `CBig` is a pair of real parts (`re`, `im`) sharing one precision and one rounding mode, mirroring +`FBig`'s own `Repr`+`Context` layout. Rounding follows the C99 Annex G / Kahan branch-cut and signed-zero +model that `dashu-float` already implements for reals. + +See the crate-level docs for details. + +## License + +Licensed under either of + + * Apache License, Version 2.0 + ([LICENSE-APACHE](../LICENSE-APACHE) or https://www.apache.org/licenses/LICENSE-2.0) + * MIT license + ([LICENSE-MIT](../LICENSE-MIT) or https://opensource.org/licenses/MIT) + +at your option. diff --git a/complex/benches/arith.rs b/complex/benches/arith.rs new file mode 100644 index 00000000..84bb5e77 --- /dev/null +++ b/complex/benches/arith.rs @@ -0,0 +1,57 @@ +//! Benchmarks for complex field arithmetic (`mul`/`div`/`sqr`). +//! Run: cargo bench -p dashu-cmplx --bench arith --features rand -- --quick + +use criterion::{ + criterion_group, criterion_main, AxisScale, BenchmarkId, Criterion, PlotConfiguration, +}; +use dashu_cmplx::CBig; +use dashu_float::FBig; +use rand_v09::prelude::*; + +type C = CBig; // base-2, Zero rounding (the default) + +const SEED: u64 = 1; + +/// Random base-2 complex number at the given precision, with a random sign and modest magnitude. +fn random_cbig(precision: usize, rng: &mut impl Rng) -> C { + let mut mk = || { + let sig: i64 = rng.random_range(i16::MIN as i64..=i16::MAX as i64); + if sig == 0 { + return FBig::ZERO.with_precision(precision).value(); + } + let exp: isize = rng.random_range(-8i32..=8i32) as isize; + let sig = if rng.random_bool(0.5) { -sig } else { sig }; + FBig::from_parts(sig.into(), exp) + .with_precision(precision) + .value() + }; + CBig::from_parts(mk(), mk()) +} + +fn bench_arith(criterion: &mut Criterion) { + let mut rng = StdRng::seed_from_u64(SEED); + let mut group = criterion.benchmark_group("arith"); + group.plot_config(PlotConfiguration::default().summary_scale(AxisScale::Logarithmic)); + + for precision in [53, 113, 256, 1024] { + let a = random_cbig(precision, &mut rng); + let b = random_cbig(precision, &mut rng); + group.bench_with_input( + BenchmarkId::new("mul", precision), + &(&a, &b), + |bencher, &(a, b)| bencher.iter(|| a * b), + ); + group.bench_with_input( + BenchmarkId::new("div", precision), + &(&a, &b), + |bencher, &(a, b)| bencher.iter(|| a / b), + ); + group.bench_with_input(BenchmarkId::new("sqr", precision), &a, |bencher, a| { + bencher.iter(|| a.sqr()) + }); + } + group.finish(); +} + +criterion_group!(benches, bench_arith); +criterion_main!(benches); diff --git a/complex/benches/io.rs b/complex/benches/io.rs new file mode 100644 index 00000000..4f5e1210 --- /dev/null +++ b/complex/benches/io.rs @@ -0,0 +1,47 @@ +//! Benchmarks for complex I/O (`Display` / `FromStr` in `a+bi` form). +//! Run: cargo bench -p dashu-cmplx --bench io --features rand -- --quick + +use core::str::FromStr; +use criterion::{ + criterion_group, criterion_main, AxisScale, BenchmarkId, Criterion, PlotConfiguration, +}; +use dashu_cmplx::CBig; +use dashu_float::FBig; +use rand_v09::prelude::*; + +type C = CBig; + +const SEED: u64 = 1; + +fn random_cbig(precision: usize, rng: &mut impl Rng) -> C { + let mut mk = || { + let sig: i64 = rng.random_range(1..=i16::MAX as i64); + let exp: isize = rng.random_range(-4i32..=4i32) as isize; + let sig = if rng.random_bool(0.5) { -sig } else { sig }; + FBig::from_parts(sig.into(), exp) + .with_precision(precision) + .value() + }; + CBig::from_parts(mk(), mk()) +} + +fn bench_io(criterion: &mut Criterion) { + let mut rng = StdRng::seed_from_u64(SEED); + let mut group = criterion.benchmark_group("io"); + group.plot_config(PlotConfiguration::default().summary_scale(AxisScale::Logarithmic)); + + for precision in [53, 113, 256] { + let z = random_cbig(precision, &mut rng); + let s = z.to_string(); + group.bench_with_input(BenchmarkId::new("display", precision), &z, |bencher, z| { + bencher.iter(|| z.to_string()) + }); + group.bench_with_input(BenchmarkId::new("parse", precision), &s, |bencher, s| { + bencher.iter(|| C::from_str(s).unwrap()) + }); + } + group.finish(); +} + +criterion_group!(benches, bench_io); +criterion_main!(benches); diff --git a/complex/benches/transcendental.rs b/complex/benches/transcendental.rs new file mode 100644 index 00000000..bcc9c3ac --- /dev/null +++ b/complex/benches/transcendental.rs @@ -0,0 +1,71 @@ +//! Benchmarks for complex transcendentals (`exp`/`log`/`sin`/`cos`/`sqrt`/`abs`/`arg`). +//! Run: cargo bench -p dashu-cmplx --bench transcendental --features rand -- --quick +//! +//! Inputs are drawn from a modest range so the transcendentals stay well-conditioned and away +//! from branch cuts. + +use criterion::{ + criterion_group, criterion_main, AxisScale, BenchmarkId, Criterion, PlotConfiguration, +}; +use dashu_cmplx::CBig; +use dashu_float::FBig; +use rand_v09::prelude::*; + +type C = CBig; + +const SEED: u64 = 1; + +/// Random base-2 complex number with modest magnitude (keeps the transcendentals well-conditioned). +fn random_cbig(precision: usize, rng: &mut impl Rng) -> C { + let mut mk = || { + let sig: i64 = rng.random_range(1..=i16::MAX as i64); + let exp: isize = rng.random_range(-6i32..=-1i32) as isize; + let sig = if rng.random_bool(0.5) { -sig } else { sig }; + FBig::from_parts(sig.into(), exp) + .with_precision(precision) + .value() + }; + CBig::from_parts(mk(), mk()) +} + +macro_rules! unary_bench { + ($name:ident, $method:ident) => { + fn $name(criterion: &mut Criterion) { + let mut rng = StdRng::seed_from_u64(SEED); + let mut group = criterion.benchmark_group(stringify!($name)); + group.plot_config(PlotConfiguration::default().summary_scale(AxisScale::Logarithmic)); + for precision in [53, 113, 256] { + let a = random_cbig(precision, &mut rng); + group.bench_with_input(BenchmarkId::from_parameter(precision), &a, |bencher, a| { + bencher.iter(|| a.$method()) + }); + } + group.finish(); + } + }; +} + +unary_bench!(exp, exp); +unary_bench!(ln, ln); +unary_bench!(sin, sin); +unary_bench!(cos, cos); +unary_bench!(sqrt, sqrt); + +fn abs_arg(criterion: &mut Criterion) { + let mut rng = StdRng::seed_from_u64(SEED); + let mut group = criterion.benchmark_group("abs_arg"); + group.plot_config(PlotConfiguration::default().summary_scale(AxisScale::Logarithmic)); + for precision in [53, 113, 256] { + let a = random_cbig(precision, &mut rng); + group.bench_with_input(BenchmarkId::new("abs", precision), &a, |bencher, a| { + bencher.iter(|| a.abs()) + }); + group.bench_with_input(BenchmarkId::new("arg", precision), &a, |bencher, a| { + bencher.iter(|| a.arg()) + }); + } + group.finish(); +} + +criterion_group!(benches, exp, ln, sin, cos, sqrt, abs_arg); +criterion_main!(benches); diff --git a/complex/src/add.rs b/complex/src/add.rs new file mode 100644 index 00000000..1f548f7c --- /dev/null +++ b/complex/src/add.rs @@ -0,0 +1,122 @@ +//! Complex addition and subtraction. +//! +//! Addition and subtraction are componentwise perfectly-rounded real additions/subtractions on each +//! part, forwarded through the shared [`impl_cbig_binop!`] macro (same pattern as `Mul` / `Div`). + +use crate::cbig::CBig; +use crate::repr::{combine_parts, CfpResult, Context}; +use core::ops::{Add, AddAssign, Sub, SubAssign}; +use dashu_float::round::Round; +use dashu_int::Word; + +impl Context { + /// Add two complex numbers under this context (context layer). + /// + /// Returns a [`CfpResult`] carrying each part's inexactness. Addition is componentwise, so each + /// part is a single correctly-rounded real addition. + pub fn add(&self, z: &CBig, w: &CBig) -> CfpResult { + let re = self.float().add(z.re(), w.re())?; + let im = self.float().add(z.im(), w.im())?; + Ok(combine_parts(re, im)) + } + + /// Subtract two complex numbers under this context (context layer). + /// + /// Returns a [`CfpResult`] carrying each part's inexactness. Subtraction is componentwise, so each + /// part is a single correctly-rounded real subtraction. + pub fn sub(&self, z: &CBig, w: &CBig) -> CfpResult { + let re = self.float().sub(z.re(), w.re())?; + let im = self.float().sub(z.im(), w.im())?; + Ok(combine_parts(re, im)) + } +} + +// --- Add: all four ref/val combinations, plus Assign, via the shared macro --- +crate::helper_macros::impl_cbig_binop!(Add, add, AddAssign, add_assign); + +// --- Sub: all four ref/val combinations, plus Assign, via the shared macro --- +crate::helper_macros::impl_cbig_binop!(Sub, sub, SubAssign, sub_assign); + +#[cfg(test)] +mod tests { + use super::*; + use dashu_float::round::mode; + + type C = CBig; + + #[test] + fn add_componentwise() { + let z = C::from_parts(3.into(), 4.into()); + let w = C::from_parts(1.into(), 2.into()); + let r = &z + &w; + assert_eq!(r.re().significand(), &4.into()); + assert_eq!(r.im().significand(), &6.into()); + } + + #[test] + fn add_all_ref_val_combinations() { + let z = C::from_parts(1.into(), 2.into()); + let w = C::from_parts(3.into(), 4.into()); + // val + val, val + ref, ref + val, ref + ref + assert_eq!((z.clone() + w.clone()).im().significand(), &6.into()); + assert_eq!((z.clone() + &w).im().significand(), &6.into()); + assert_eq!((&z + w.clone()).im().significand(), &6.into()); + assert_eq!((&z + &w).im().significand(), &6.into()); + } + + #[test] + fn add_assign_val_and_ref() { + let z = C::from_parts(1.into(), 2.into()); + let w = C::from_parts(3.into(), 4.into()); + + let mut acc = z.clone(); + acc += w.clone(); + assert_eq!(acc.re().significand(), &4.into()); + assert_eq!(acc.im().significand(), &6.into()); + + let mut acc = z.clone(); + acc += &w; + assert_eq!(acc.re().significand(), &4.into()); + } + + #[test] + fn sub_componentwise() { + let z = C::from_parts(3.into(), 4.into()); + let w = C::from_parts(1.into(), 2.into()); + let r = &z - &w; + assert_eq!(r.re().significand(), &2.into()); + assert_eq!(r.im().significand(), &2.into()); + } + + #[test] + fn sub_all_ref_val_combinations() { + let z = C::from_parts(5.into(), 6.into()); + let w = C::from_parts(2.into(), 1.into()); + assert_eq!((z.clone() - w.clone()).re().significand(), &3.into()); + assert_eq!((z.clone() - &w).re().significand(), &3.into()); + assert_eq!((&z - w.clone()).re().significand(), &3.into()); + assert_eq!((&z - &w).re().significand(), &3.into()); + } + + #[test] + fn z_minus_z_is_zero() { + let z = C::from_parts(7.into(), 9.into()); + let r = &z - &z; + assert!(r.is_zero()); + } + + #[test] + fn sub_assign_val_and_ref() { + let z = C::from_parts(5.into(), 6.into()); + let w = C::from_parts(2.into(), 1.into()); + + let mut acc = z.clone(); + acc -= w.clone(); + assert_eq!(acc.re().significand(), &3.into()); + assert_eq!(acc.im().significand(), &5.into()); + + let mut acc = z.clone(); + acc -= &w; + assert_eq!(acc.re().significand(), &3.into()); + } +} diff --git a/complex/src/cbig.rs b/complex/src/cbig.rs new file mode 100644 index 00000000..004a17de --- /dev/null +++ b/complex/src/cbig.rs @@ -0,0 +1,233 @@ +//! The [`CBig`] type: an arbitrary-precision complex number. +//! +//! A [`CBig`] is a pair of real parts (`re`, `im`) sharing one precision and one rounding mode, +//! mirroring [`dashu_float::FBig`]'s own `Repr`+`Context` layout generalized to two parts over a +//! **single shared** [`Context`](crate::Context). Storing one context — rather than wrapping two +//! `FBig`s (each carrying its own) — makes the uniform-precision invariant *physical*: there is +//! exactly one precision slot, so `re` and `im` structurally cannot disagree. + +use crate::repr::Context; +use dashu_base::Sign; +use dashu_float::round::{mode, Round}; +use dashu_float::{FBig, Repr}; +use dashu_int::Word; + +/// An arbitrary-precision complex number with arbitrary base and rounding mode. +/// +/// The complex number consists of two [`Repr`] parts (the real part `re` and the imaginary part +/// `im`) over a single shared [`Context`](crate::Context). Each part keeps its own significand +/// length; the shared context holds the precision cap and rounding mode applied independently to +/// both components. +/// +/// # Generic parameters +/// +/// The const generic parameters are abbreviated as `BASE` -> `B`, `RoundingMode` -> `R`. The `BASE` +/// must be in range `[2, isize::MAX]`, and the rounding mode `R` is chosen from the +/// [`dashu_float::round::mode`] module. With the defaults the number is base 2 rounded towards zero +/// (matching `FBig`'s default). +/// +/// # Rounding +/// +/// Each component of a result is rounded independently with the single mode `R`, after the +/// operation feeds each part enough guard precision (the same near-correctly-rounded guarantee class +/// `dashu-float`'s transcendentals carry). See the crate-level docs for the no-NaN error policy. +/// +/// # Examples +/// +/// ``` +/// use dashu_cmplx::CBig; +/// use dashu_float::{FBig, round::mode::HalfAway}; +/// +/// // base-10 so each integer keeps its own significand +/// let z = CBig::::from_parts(FBig::from(3), FBig::from(4)); +/// assert_eq!(z.re().significand(), &3.into()); +/// assert_eq!(z.im().significand(), &4.into()); +/// ``` +pub struct CBig { + pub(crate) re: Repr, + pub(crate) im: Repr, + pub(crate) context: Context, +} + +impl CBig { + /// Create a [`CBig`] from raw parts (a `const`-capable constructor, the complex analog of + /// [`dashu_float::FBig::from_repr_const`]). Used by the `static_cbig!` literal macro and + /// internal code; in most cases prefer [`CBig::from_parts`]. + #[inline] + pub const fn new(re: Repr, im: Repr, context: Context) -> Self { + Self { re, im, context } + } + + /// Create a [`CBig`] from its real and imaginary parts. + /// + /// The result context is `max(re.context(), im.context())` (the larger precision wins; an + /// unlimited `0` precision is treated as the minimum, so a limited operand's precision wins), + /// and the smaller-precision part is effectively widened to it — widening is exact, so only the + /// precision cap changes. The rounding mode and base must match and are enforced by the type + /// parameters. + /// + /// # Examples + /// + /// ``` + /// use dashu_cmplx::CBig; + /// use dashu_float::{FBig, round::mode::HalfAway}; + /// + /// type C = CBig; + /// type F = FBig; + /// let z = C::from_parts(F::from(3), F::from(4)); + /// let (re, im) = z.into_parts(); + /// assert_eq!(re, F::from(3)); + /// assert_eq!(im, F::from(4)); + /// ``` + #[inline] + pub fn from_parts(re: FBig, im: FBig) -> Self { + let fctx = dashu_float::Context::max(re.context(), im.context()); + Self { + re: re.into_repr(), + im: im.into_repr(), + context: Context(fctx), + } + } + + /// The complex number zero `0 + 0i` (unlimited precision). + pub const ZERO: Self = Self::new(Repr::zero(), Repr::zero(), Context::new(0)); + + /// The complex number one `1 + 0i` (unlimited precision). + pub const ONE: Self = Self::new(Repr::one(), Repr::zero(), Context::new(0)); + + /// The imaginary unit `0 + 1i` (unlimited precision). + pub const I: Self = Self::new(Repr::zero(), Repr::one(), Context::new(0)); + + /// Get the shared [`Context`](crate::Context) of the complex number. + #[inline] + pub const fn context(&self) -> Context { + self.context + } + + /// Get the precision limit of the complex number (`0` = unlimited). Both parts share it. + #[inline] + pub const fn precision(&self) -> usize { + self.context.precision() + } + + /// Get a reference to the real part's raw representation. + #[inline] + pub const fn re(&self) -> &Repr { + &self.re + } + + /// Get a reference to the imaginary part's raw representation. + #[inline] + pub const fn im(&self) -> &Repr { + &self.im + } + + /// Convert the complex number into its real and imaginary parts as [`FBig`]s, each carrying the + /// (copied) shared context — zero clone of the significands. + #[inline] + pub fn into_parts(self) -> (FBig, FBig) { + let fctx = self.context.float(); + (FBig::from_repr(self.re, fctx), FBig::from_repr(self.im, fctx)) + } + + /// Determine if the complex number is numerically zero (both parts `±0`). + #[inline] + pub fn is_zero(&self) -> bool { + (self.re.is_zero() || self.re.is_neg_zero()) && (self.im.is_zero() || self.im.is_neg_zero()) + } + + /// Determine if either part of the complex number is infinite. + #[inline] + pub fn is_infinite(&self) -> bool { + self.re.is_infinite() || self.im.is_infinite() + } + + /// Determine if the complex number is finite (neither part infinite). + #[inline] + pub fn is_finite(&self) -> bool { + !self.is_infinite() + } + + /// The complex infinity produced on overflow: both parts are `+∞` (the Riemann point; `proj` + /// collapses any infinity to `+∞ + i·0`). + #[inline] + pub(crate) fn overflow(context: &Context, _sign: Sign) -> Self { + Self::new(Repr::infinity(), Repr::infinity(), *context) + } + + /// The complex zero produced on underflow: a signed zero on the real part and `+0` imaginary. + #[inline] + pub(crate) fn underflow(context: &Context, sign: Sign) -> Self { + let re = match sign { + Sign::Positive => Repr::zero(), + Sign::Negative => Repr::neg_zero(), + }; + Self::new(re, Repr::zero(), *context) + } +} + +// Custom Clone (the significands are heap-allocated), mirroring FBig. +impl Clone for CBig { + #[inline] + fn clone(&self) -> Self { + Self { + re: self.re.clone(), + im: self.im.clone(), + context: self.context, + } + } +} + +impl Default for CBig { + /// Default value: `0 + 0i`. + #[inline] + fn default() -> Self { + Self::ZERO + } +} + +#[cfg(test)] +mod tests { + use super::*; + + type C = CBig; + + #[test] + fn constants() { + assert!(C::ZERO.is_zero()); + assert!(!C::ONE.is_zero()); + assert!(!C::I.is_zero()); + let (re, im) = C::I.into_parts(); + assert!(re.repr().is_zero()); + assert!(im.repr().is_one()); + } + + #[test] + fn from_parts_reconciles_precision() { + type F = FBig; + let re = F::from_parts(3.into(), 0); // precision 1 (one decimal digit) + let im = F::from_parts(4.into(), 0); // precision 1 + let z = CBig::from_parts(re, im); + assert_eq!(z.precision(), 1); + assert_eq!(z.re().significand(), &3.into()); + assert_eq!(z.im().significand(), &4.into()); + } + + #[test] + fn predicates() { + let inf = C::new(Repr::infinity(), Repr::zero(), Context::new(0)); + assert!(inf.is_infinite()); + assert!(!inf.is_finite()); + assert!(!inf.is_zero()); + + // a finite, nonzero number + let z = C::from_parts(FBig::from(3), FBig::from(0)); + assert!(!z.is_infinite()); + assert!(z.is_finite()); + assert!(!z.is_zero()); + + // both parts zero (incl. -0) + let neg_zero = C::new(Repr::neg_zero(), Repr::zero(), Context::new(0)); + assert!(neg_zero.is_zero()); + } +} diff --git a/complex/src/cmp.rs b/complex/src/cmp.rs new file mode 100644 index 00000000..a8981667 --- /dev/null +++ b/complex/src/cmp.rs @@ -0,0 +1,110 @@ +//! Comparison traits for [`CBig`]. +//! +//! [`CBig`] mirrors [`dashu_float::FBig`]'s comparison surface rather than MPC's "complex has no +//! order" stance: a lexicographic total [`Ord`] (by real part, then imaginary), an [`AbsOrd`] +//! magnitude comparison via `|z|²`, and (behind `num-order`) `NumOrd`/`NumHash`. + +use crate::cbig::CBig; +use crate::repr::Context; +use core::cmp::Ordering; +use dashu_base::AbsOrd; +use dashu_float::round::Round; +use dashu_float::Repr; +use dashu_int::Word; + +/// Lexicographic comparison by `(re, then im)` using the value-based [`Repr`] order. This is a +/// well-defined total order (usable for `BTreeMap`/sorting), not an algebraic one. +pub(crate) fn lex_cmp( + re1: &Repr, + im1: &Repr, + re2: &Repr, + im2: &Repr, +) -> Ordering { + match re1.cmp(re2) { + Ordering::Equal => im1.cmp(im2), + ord => ord, + } +} + +impl PartialEq> for CBig { + /// Componentwise exact equality. `+0 == -0` per component (matching `FBig`); the context is + /// ignored. + #[inline] + fn eq(&self, other: &CBig) -> bool { + self.re == other.re && self.im == other.im + } +} +impl Eq for CBig {} + +impl PartialOrd> for CBig { + #[inline] + fn partial_cmp(&self, other: &CBig) -> Option { + Some(lex_cmp(&self.re, &self.im, &other.re, &other.im)) + } +} + +impl Ord for CBig { + /// Lexicographic total order by `(re, then im)`. Special values are placed consistently with + /// `FBig` (`-∞ < finite < +∞` per component). + #[inline] + fn cmp(&self, other: &Self) -> Ordering { + lex_cmp(&self.re, &self.im, &other.re, &other.im) + } +} + +impl AbsOrd for CBig { + /// Magnitude comparison by `|z|`. Compared through `|z|²` (the exact squared modulus — both + /// sides are non-negative, so the order is preserved) to avoid the `sqrt`/`hypot` of [`CBig::abs`]. + #[inline] + fn abs_cmp(&self, other: &Self) -> Ordering { + // Exact squared magnitudes at unlimited precision (no rounding, no overflow). + let unlim = Context::::new(0); + let f = unlim.float(); + let n1 = f.unwrap_fp(unlim.norm(self)); + let n2 = f.unwrap_fp(unlim.norm(other)); + n1.cmp(&n2) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use dashu_float::round::mode; + + type C = CBig; + + #[test] + fn eq_componentwise() { + let a = C::from_parts(3.into(), 4.into()); + let b = C::from_parts(3.into(), 4.into()); + assert!(a == b); + let c = C::from_parts(3.into(), 5.into()); + assert!(a != c); + } + + #[test] + fn signed_zero_eq() { + let p = C::from_parts(3.into(), 0.into()); + let n = C::new(Repr::new(3.into(), 0), Repr::neg_zero(), Context::new(0)); + // +0 == -0 on the imaginary part + assert!(p == n); + } + + #[test] + fn ord_lexicographic() { + let a = C::from_parts(1.into(), 9.into()); + let b = C::from_parts(2.into(), 0.into()); + assert!(a < b); // real part dominates + let c = C::from_parts(1.into(), 10.into()); + assert!(a < c); // equal real, larger imag + } + + #[test] + fn absord_by_magnitude() { + let a = C::from_parts(3.into(), 4.into()); // |z| = 5 + let b = C::from_parts(5.into(), 0.into()); // |z| = 5 + assert!(a.abs_cmp(&b).is_eq()); + let c = C::from_parts(1.into(), 1.into()); // |z|² = 2 + assert!(c.abs_cmp(&a).is_lt()); + } +} diff --git a/complex/src/convert.rs b/complex/src/convert.rs new file mode 100644 index 00000000..d7d111e1 --- /dev/null +++ b/complex/src/convert.rs @@ -0,0 +1,118 @@ +//! Conversions between [`CBig`], [`FBig`], and integers. +//! +//! The into-`CBig` direction is lossless through [`From`] (a real [`FBig`], or a `UBig`/`IBig` → +//! the real part, with imaginary `+0`). The out-of-`CBig` direction is lossy through [`TryFrom`], +//! composing `CBig → FBig → IBig` — exactly the [`From`]/[`TryFrom`] split `FBig` uses. + +use crate::cbig::CBig; +use dashu_base::ConversionError; +use dashu_float::round::Round; +use dashu_float::{FBig, Repr}; +use dashu_int::{IBig, UBig, Word}; + +impl From> for CBig { + /// Embed a real [`FBig`] as a complex number with imaginary part `+0`. + #[inline] + fn from(re: FBig) -> Self { + let fctx = re.context(); + Self { + re: re.into_repr(), + im: Repr::zero(), + context: crate::repr::Context(fctx), + } + } +} + +impl From for CBig { + /// Embed an unsigned integer as a complex number (exact, unlimited precision) with imaginary `+0`. + #[inline] + fn from(v: UBig) -> Self { + FBig::from(v).into() + } +} + +impl From for CBig { + /// Embed a signed integer as a complex number (exact, unlimited precision) with imaginary `+0`. + #[inline] + fn from(v: IBig) -> Self { + FBig::from(v).into() + } +} + +impl TryFrom> for FBig { + type Error = ConversionError; + + /// Extract the real part, succeeding only when the imaginary part is zero (purely real; both + /// `±0` count as zero). This is the guarded "is this complex actually real?" check — distinct + /// from [`CBig::re`] / [`CBig::into_parts`], which return the real part unconditionally. + #[inline] + fn try_from(z: CBig) -> Result { + if z.im.is_zero() || z.im.is_neg_zero() { + Ok(FBig::from_repr(z.re, z.context.float())) + } else { + Err(ConversionError::LossOfPrecision) + } + } +} + +impl TryFrom> for IBig { + type Error = ConversionError; + + /// Extract an integer, succeeding only when the number is purely real, finite, and + /// integer-valued. Composes [`CBig`] → [`FBig`] → [`IBig`]; for a rounding-aware path use + /// [`FBig::to_int`] on the real part ([`CBig::re`]). + #[inline] + fn try_from(z: CBig) -> Result { + let re: FBig = FBig::try_from(z)?; + IBig::try_from(re) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use dashu_float::round::mode; + + type C = CBig; + type F = FBig; + + #[test] + fn from_fbig_is_purely_real() { + let z = C::from(F::from(7)); + assert!(z.im().is_zero() || z.im().is_neg_zero()); + assert_eq!(z.re().significand(), &7.into()); + } + + #[test] + fn from_integers() { + let z: C = UBig::from(5u32).into(); + assert_eq!(z.re().significand(), &5.into()); + let z: C = IBig::from(-3).into(); + assert_eq!(z.re().significand(), &(-3i32).into()); + } + + #[test] + fn try_from_fbig_ok_iff_purely_real() { + let z = C::from_parts(7.into(), 0.into()); + let re: F = F::try_from(z).unwrap(); + assert_eq!(re.repr().significand(), &7.into()); + + let z = C::from_parts(3.into(), 4.into()); + assert_eq!(F::try_from(z), Err(ConversionError::LossOfPrecision)); + } + + #[test] + fn try_from_ibig_composes() { + let z: C = IBig::from(9).into(); + let i: IBig = IBig::try_from(z).unwrap(); + assert_eq!(i, 9.into()); + + // fractional real part → LossOfPrecision + let z = C::from(F::from_parts(123.into(), -2)); // 1.23 + assert_eq!(IBig::try_from(z), Err(ConversionError::LossOfPrecision)); + + // nonzero imaginary → LossOfPrecision + let z = C::from_parts(9.into(), 1.into()); + assert_eq!(IBig::try_from(z), Err(ConversionError::LossOfPrecision)); + } +} diff --git a/complex/src/div.rs b/complex/src/div.rs new file mode 100644 index 00000000..1beeb432 --- /dev/null +++ b/complex/src/div.rs @@ -0,0 +1,258 @@ +//! Complex division and reciprocal (near-correctly rounded via Smith's method + guard re-round). + +use crate::cbig::CBig; +use crate::repr::{combine_parts, exact, riemann, CfpResult, Context}; +use core::ops::{Div, DivAssign}; +use dashu_base::{AbsOrd, Inverse}; +use dashu_float::round::Round; +use dashu_float::{FBig, FpError}; +use dashu_int::Word; + +/// Guard digits (base-B) for `div`/`inv`. The naive complex-division error is `~(3+√5)·u`; a fixed +/// guard comfortably absorbs it for well-conditioned denominators. +const DIV_GUARD: usize = 14; + +impl Context { + /// Reciprocal `1/z = conj(z)/|z|²` under this context (context layer). + pub fn inv(&self, z: &CBig) -> CfpResult { + if z.is_infinite() { + return Ok(exact(FBig::ZERO, FBig::ZERO)); // 1/∞ = 0 + } + if z.is_zero() { + return Ok(riemann(*self)); // 1/0 = ∞ + } + let gctx = self.guard(DIV_GUARD); + let p = self.precision(); + let (x, y) = (z.re(), z.im()); + // n = x² + y² + let x2 = gctx.sqr(x)?.value(); + let y2 = gctx.sqr(y)?.value(); + let n = gctx.add(x2.repr(), y2.repr())?.value(); + // 1/z = (x/n) + i(-y/n) + let re = gctx.div(x, n.repr())?.value().with_precision(p); + let neg_y = -y.clone(); + let im = gctx.div(&neg_y, n.repr())?.value().with_precision(p); + Ok(combine_parts(re, im)) + } + + /// Divide two complex numbers under this context (context layer), using Smith's overflow-safe + /// method: the branch `|u| >= |v|` avoids forming `|denominator|²`. + pub fn div(&self, z: &CBig, w: &CBig) -> CfpResult { + if let Some(special) = div_special(z, w) { + return special; + } + let gctx = self.guard(DIV_GUARD); + let p = self.precision(); + let (x, y) = (z.re(), z.im()); + let (u, v) = (w.re(), w.im()); + // Determine |u| >= |v| via abs_cmp on temporary FBig views (Smith's method). + let u_ge_v = { + let fu = FBig::from_repr(u.clone(), gctx); + let fv = FBig::from_repr(v.clone(), gctx); + fu.abs_cmp(&fv).is_ge() + }; + + // r, d depend on which of |u|, |v| is larger + let (r, d) = if u_ge_v { + // r = v/u, d = u + r·v + let r = gctx.div(v, u)?.value(); + let rv = gctx.mul(r.repr(), v)?.value(); + let d = gctx.add(u, rv.repr())?.value(); + (r, d) + } else { + // r = u/v, d = v + r·u + let r = gctx.div(u, v)?.value(); + let ru = gctx.mul(r.repr(), u)?.value(); + let d = gctx.add(v, ru.repr())?.value(); + (r, d) + }; + + let (re, im) = if u_ge_v { + // re = (x + r·y)/d, im = (y - r·x)/d + let ry = gctx.mul(r.repr(), y)?.value(); + let rx = gctx.mul(r.repr(), x)?.value(); + let num_re = gctx.add(x, ry.repr())?.value(); + let num_im = gctx.sub(y, rx.repr())?.value(); + ( + gctx.div(num_re.repr(), d.repr())?.value().with_precision(p), + gctx.div(num_im.repr(), d.repr())?.value().with_precision(p), + ) + } else { + // re = (r·x + y)/d, im = (r·y - x)/d + let rx = gctx.mul(r.repr(), x)?.value(); + let ry = gctx.mul(r.repr(), y)?.value(); + let num_re = gctx.add(rx.repr(), y)?.value(); + let num_im = gctx.sub(ry.repr(), x)?.value(); + ( + gctx.div(num_re.repr(), d.repr())?.value().with_precision(p), + gctx.div(num_im.repr(), d.repr())?.value().with_precision(p), + ) + }; + Ok(combine_parts(re, im)) + } + + /// Divide a complex number by a real scalar (context layer): `(x+iy)/s = (x/s) + i(y/s)`. + pub fn div_real(&self, z: &CBig, s: &FBig) -> CfpResult { + if z.is_infinite() || s.repr().is_infinite() { + if z.is_infinite() && s.repr().is_infinite() { + return Err(FpError::Indeterminate); // ∞/∞ + } + if s.repr().is_infinite() { + return Ok(exact(FBig::ZERO, FBig::ZERO)); // finite/∞ = 0 + } + // z infinite, s finite nonzero → ∞ + return Ok(riemann(*self)); + } + if s.repr().is_zero() { + if z.is_zero() { + return Err(FpError::Indeterminate); // 0/0 + } + return Ok(riemann(*self)); // z/0 (z≠0) = ∞ + } + let gctx = self.guard(DIV_GUARD); + let p = self.precision(); + let re = gctx.div(z.re(), s.repr())?.value().with_precision(p); + let im = gctx.div(z.im(), s.repr())?.value().with_precision(p); + Ok(combine_parts(re, im)) + } +} + +/// Annex-G short-circuit for `z / w`. +fn div_special(z: &CBig, w: &CBig) -> Option> { + let (zi, wi) = (z.is_infinite(), w.is_infinite()); + let (zz, wz) = (z.is_zero(), w.is_zero()); + let ctx = Context::max(z.context(), w.context()); + if (zi && wi) || (zz && wz) { + Some(Err(FpError::Indeterminate)) // ∞/∞ or 0/0 + } else if wi { + Some(Ok(exact(FBig::ZERO, FBig::ZERO))) // (finite or 0) / ∞ = 0 + } else if wz || zi { + Some(Ok(riemann(ctx))) // (nonzero or ∞) / 0, or ∞ / finite = ∞ + } else if zz { + Some(Ok(exact(FBig::ZERO, FBig::ZERO))) // 0 / finite = 0 + } else { + None + } +} + +impl Inverse for CBig { + type Output = CBig; + + #[inline] + fn inv(self) -> Self::Output { + self.context().unwrap_cfp(self.context().inv(&self)) + } +} + +impl Inverse for &CBig { + type Output = CBig; + + #[inline] + fn inv(self) -> Self::Output { + self.context().unwrap_cfp(self.context().inv(self)) + } +} + +// CBig / CBig operators +crate::helper_macros::impl_cbig_binop!(Div, div, DivAssign, div_assign); + +// --- scalar division by a real FBig (mixed-type operators) --- + +// CBig / FBig (componentwise, via the shared scalar macro). +crate::helper_macros::impl_cbig_scalar_binop!(Div, div, div_real); + +// FBig / CBig = (s + 0i) / z, reusing complex division. +impl Div<&CBig> for &FBig { + type Output = CBig; + #[inline] + fn div(self, rhs: &CBig) -> CBig { + let s = CBig::from(self.clone()); + let ctx = Context::max(s.context(), rhs.context()); + ctx.unwrap_cfp(ctx.div(&s, rhs)) + } +} +impl Div> for &FBig { + type Output = CBig; + #[inline] + fn div(self, rhs: CBig) -> CBig { + let this = self.clone(); + let s = CBig::from(this); + let ctx = Context::max(s.context(), rhs.context()); + ctx.unwrap_cfp(ctx.div(&s, &rhs)) + } +} +impl Div<&CBig> for FBig { + type Output = CBig; + #[inline] + fn div(self, rhs: &CBig) -> CBig { + let s = CBig::from(self); + let ctx = Context::max(s.context(), rhs.context()); + ctx.unwrap_cfp(ctx.div(&s, rhs)) + } +} +impl Div> for FBig { + type Output = CBig; + #[inline] + fn div(self, rhs: CBig) -> CBig { + let s = CBig::from(self); + let ctx = Context::max(s.context(), rhs.context()); + ctx.unwrap_cfp(ctx.div(&s, &rhs)) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use dashu_float::round::mode; + + type C = CBig; + type F = FBig; + + fn c(re: i32, im: i32) -> C { + let mk = |v: i32| -> F { F::from(v).with_precision(53).value() }; + C::from_parts(mk(re), mk(im)) + } + + #[test] + fn div_inverse() { + // z / z = 1 for z != 0 + let z = c(3, 4); + let q = &z / &z; + assert_eq!(q.re().significand(), &1.into()); + assert!(q.im().significand().is_zero()); + } + + #[test] + fn div_basic() { + // (6+8i)/(3+4i) = 2 (since 6+8i = 2·(3+4i)) + let z = c(6, 8); + let w = c(3, 4); + let q = &z / &w; + assert_eq!(q.re().significand(), &2.into()); + assert!(q.im().significand().is_zero()); + } + + #[test] + fn inv_basic() { + // 1/(3+4i) = (3-4i)/25 = 0.12 - 0.16i — use precision 53 for exact-ish + type F = FBig; + let mk = |v: i32| -> F { F::from(v).with_precision(53).value() }; + let z = C::from_parts(mk(3), mk(4)); + let r = (&z).inv(); + // (3-4i)/25: re = 3/25, im = -4/25 + assert_eq!(r.context().precision(), 53); + // re ≈ 0.12, im ≈ -0.16; check via multiplying back: z·inv(z) = 1 + let one = &z * &r; + assert_eq!(one.re().significand(), &1.into()); + assert!(one.im().significand().is_zero()); + } + + #[test] + fn scalar_div_by_real() { + let z = c(6, 8); + let s = FBig::::from(2); + let q = &z / &s; + assert_eq!(q.re().significand(), &3.into()); + assert_eq!(q.im().significand(), &4.into()); + } +} diff --git a/complex/src/exp.rs b/complex/src/exp.rs new file mode 100644 index 00000000..242d35c2 --- /dev/null +++ b/complex/src/exp.rs @@ -0,0 +1,236 @@ +//! Complex exponential and powers. +//! +//! * [`Context::exp`] / [`CBig::exp`]: `exp(x+iy) = e^x·(cos y + i sin y)`. +//! * [`Context::powi`] / [`CBig::powi`]: integer exponent via repeated squaring (branch-cut-free, +//! cheaper than `exp(n·log z)`). +//! * [`Context::powf`] / [`CBig::powf`]: `exp(w·log z)` on the principal branch. +//! +//! Mirroring `dashu-float`, the power family lives alongside `exp` in a single module. + +use crate::cbig::CBig; +use crate::repr::{combine_parts, exact, reborrow_cache, riemann, CfpResult, Context}; +use dashu_base::Approximation::*; +use dashu_base::{BitTest, Sign}; +use dashu_float::round::Round; +use dashu_float::{ConstCache, FBig, FpError}; +use dashu_int::{IBig, Word}; + +/// Guard digits (base-B) for `exp`. Composes a real `exp`, a `sin_cos`, and two products. +const EXP_GUARD: usize = 14; + +/// Guard digits (base-B) for `powf`. Composes `log`, a complex product, and `exp` — the +/// cancellation-prone path, so a larger guard than the bare arithmetic ops. +const POWF_GUARD: usize = 22; + +impl Context { + /// Complex exponential under this context (context layer). Reuses `dashu-float`'s `exp` and + /// `sin_cos`; the cache is threaded into both (the convenience layer passes `None`). + /// + /// Special values: `exp(0) = 1`; `exp(+inf + i·finite) = +∞` (Riemann point); + /// `exp(-inf + i·finite) = 0`; an infinite imaginary part makes the trig undefined + /// (`Indeterminate`). + pub fn exp( + &self, + z: &CBig, + mut cache: Option<&mut ConstCache>, + ) -> CfpResult { + if z.is_zero() { + return Ok(exact(FBig::ONE, FBig::ZERO)); + } + if z.is_infinite() { + if z.im().is_infinite() { + return Err(FpError::Indeterminate); // cos/sin(±inf) undefined + } + return if z.re().sign() == Sign::Positive { + Ok(riemann(*self)) + } else { + Ok(exact(FBig::ZERO, FBig::ZERO)) + }; + } + + let gctx = self.guard(EXP_GUARD); + let p = self.precision(); + let ex = gctx.exp(z.re(), reborrow_cache(&mut cache))?.value(); + let (sin_y, cos_y) = gctx.sin_cos(z.im(), reborrow_cache(&mut cache)); + let cos_y = cos_y?.value(); + let sin_y = sin_y?.value(); + let re = gctx.mul(ex.repr(), cos_y.repr())?.value().with_precision(p); + let im = gctx.mul(ex.repr(), sin_y.repr())?.value().with_precision(p); + Ok(combine_parts(re, im)) + } + + /// Raise a complex number to an integer power under this context (context layer), via repeated + /// squaring (branch-cut-free, cheaper than `exp(n·log z)`). No cache. + /// + /// `powi(z, 0) = 1`; a negative exponent computes `powi(z, |n|)` then inverts. + pub fn powi(&self, z: &CBig, exp: IBig) -> CfpResult { + let (sign, n) = exp.into_parts(); + if n.is_zero() { + return Ok(Exact(CBig::ONE)); + } + let negative = sign == Sign::Negative; + let bitlen = n.bit_len(); + // left-to-right binary exponentiation, starting from the leading set bit + let mut acc = z.clone(); + for i in (0..bitlen - 1).rev() { + acc = self.sqr(&acc)?.value(); + if n.bit(i) { + acc = self.mul(&acc, z)?.value(); + } + } + // The intermediate rounding flags are folded away (the value is near-correctly rounded); + // for a negative exponent the final `inv` carries its own flags. + if negative { + self.inv(&acc) + } else { + Ok(Exact(acc)) + } + } + + /// Raise `base` to a complex power under this context (context layer): `exp(w·log base)` on the + /// principal branch, evaluated at `p + POWF_GUARD` and re-rounded. `powf(0, 0) = 1` (matching + /// `FBig::powf`). + /// + /// Unlike `exp`, this drives whole-[`CBig`] operations (`log`/`mul`/`exp`), so it builds a + /// complex working [`Context`] at guard precision directly rather than the float + /// `Context::guard` (which yields a `FloatCtxt` for per-part math). + pub fn powf( + &self, + base: &CBig, + w: &CBig, + mut cache: Option<&mut ConstCache>, + ) -> CfpResult { + if w.is_zero() { + return Ok(Exact(CBig::ONE)); // powf(z, 0) = 1, incl. powf(0, 0) + } + let gctx = Context::new(self.precision() + POWF_GUARD); + let log_z = gctx.log(base, reborrow_cache(&mut cache))?.value(); + let wlogz = gctx.mul(w, &log_z)?.value(); + let hi = gctx.exp(&wlogz, reborrow_cache(&mut cache))?.value(); + let p = self.precision(); + let (re, im) = hi.into_parts(); + Ok(combine_parts(re.with_precision(p), im.with_precision(p))) + } +} + +impl CBig { + /// Complex exponential `e^z` (convenience layer). + /// + /// # Panics + /// + /// Panics if the precision is unlimited or on an indeterminate special value. + #[inline] + pub fn exp(&self) -> Self { + self.context().unwrap_cfp(self.context().exp(self, None)) + } + + /// Integer power (convenience layer). + /// + /// # Panics + /// + /// Panics on an indeterminate / out-of-domain result (e.g. `0⁻¹`). + #[inline] + pub fn powi(&self, exp: IBig) -> Self { + self.context().unwrap_cfp(self.context().powi(self, exp)) + } + + /// Complex power `self^w` (convenience layer). + /// + /// `powf(z, 0) = 1` (including `powf(0, 0) = 1`), matching `FBig::powf` and the real `0⁰ = 1` + /// convention. + #[inline] + pub fn powf(&self, w: &Self) -> Self { + self.context() + .unwrap_cfp(self.context().powf(self, w, None)) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use dashu_float::round::mode; + + type C = CBig; + type F = FBig; + + fn c(re: i32, im: i32) -> C { + let mk = |v: i32| -> F { F::from(v).with_precision(53).value() }; + CBig::from_parts(mk(re), mk(im)) + } + + #[test] + fn exp_zero_is_one() { + assert!(C::ZERO.exp() == C::ONE); + } + + #[test] + fn exp_one_is_e() { + // exp(1+0i) = e ≈ 2.71828…; check 2 < e < 3 via the real part + let e = C::ONE.exp(); + let (re, _im) = e.into_parts(); + assert!(re > F::from(2)); + assert!(re < F::from(3)); + } + + #[test] + fn exp_pi_i_is_neg_one() { + use dashu_base::{Abs, AbsOrd}; + // exp(iπ) = -1 + i·0; use a π literal precise enough that sin(π_approx) ≈ 0 + let pi = F::from_parts(31415926535897932i64.into(), -16) + .with_precision(60) + .value(); + let z = CBig::from_parts(F::ZERO, pi); + let (re, im) = z.exp().into_parts(); + let re_err = (re + F::ONE).abs(); + let tol = F::from_parts(1.into(), -12); + assert!(re_err.abs_cmp(&tol).is_le()); + assert!(im.abs_cmp(&tol).is_le()); + } + + #[test] + fn exp_pos_infinity_is_riemann() { + let inf = CBig::from(F::INFINITY); + let r = inf.exp(); + assert!(r.re().is_infinite()); + assert!(r.im().is_zero()); + } + + #[test] + fn powi_zero_is_one() { + assert!(c(3, 4).powi(0.into()) == C::ONE); + } + + #[test] + fn powi_one_is_self() { + let z = c(3, 4); + assert!(z.powi(1.into()) == z); + } + + #[test] + fn powi_two_is_sqr() { + let z = c(1, 2); + assert!(z.powi(2.into()) == z.sqr()); + } + + #[test] + fn powi_negative_is_inv() { + // z^(-1) = inv(z); z · z^(-1) = 1 + let z = c(3, 4); + let r = z.powi((-1).into()); + let one = &z * &r; + assert!(one == C::ONE); + } + + #[test] + fn powf_zero_exponent_is_one() { + // powf(z, 0) = 1, including powf(0, 0) + assert!(c(3, 4).powf(&C::ZERO) == C::ONE); + assert!(C::ZERO.powf(&C::ZERO) == C::ONE); + } + + #[test] + fn powf_one_exponent_is_self() { + let z = c(2, 1); + assert!(z.powf(&C::ONE) == z); + } +} diff --git a/complex/src/fmt.rs b/complex/src/fmt.rs new file mode 100644 index 00000000..d05a3c78 --- /dev/null +++ b/complex/src/fmt.rs @@ -0,0 +1,122 @@ +//! [`Display`] / [`Debug`] for [`CBig`] in the algebraic `a+bi` notation. +//! +//! This diverges from MPC's parenthesized `"(re im)"` form: `dashu-cmplx` uses the human-readable +//! algebraic notation (the `num-complex` idiom). The parenthesized form is **not** accepted on input. + +use crate::cbig::CBig; +use core::fmt::{self, Debug, Display, Formatter, Write}; +use dashu_float::round::Round; +use dashu_float::{FBig, Repr}; +use dashu_int::{IBig, Word}; + +/// A part is a unit (value exactly `±1`) iff its normalized significand is `±1` at exponent `0`. +fn is_unit(repr: &Repr) -> bool { + repr.exponent() == 0 && { + let s = repr.significand(); + *s == IBig::ONE || *s == IBig::NEG_ONE + } +} + +impl Display for CBig { + /// Format in algebraic `a+bi` form: `"1+2i"`, `"-3-4i"`, `"5"` (pure real), `"-7i"` (pure + /// imaginary), `"i"` (`0+1i`), `"-i"` (`0-1i`). The imaginary term always carries an explicit + /// sign, a unit coefficient is elided, and a zero imaginary is omitted. Each coefficient uses + /// [`FBig`]'s native `Display` (specials render as `inf` / `-inf`). + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + let fctx = self.context.float(); + let re_zero = self.re.is_zero() || self.re.is_neg_zero(); + let im_zero = self.im.is_zero() || self.im.is_neg_zero(); + let im_neg = self.im.sign() == dashu_base::Sign::Negative; + let im_unit = is_unit(&self.im); + + if im_zero { + // pure real (incl. 0+0i → "0") + let re = FBig::from_repr(self.re.clone(), fctx); + return Display::fmt(&re, f); + } + + // imaginary part is nonzero + if !re_zero { + let re = FBig::from_repr(self.re.clone(), fctx); + Display::fmt(&re, f)?; + f.write_char(if im_neg { '-' } else { '+' })?; + } else if im_neg { + f.write_char('-')?; + } + if !im_unit { + let im_abs_repr = if im_neg { + -self.im.clone() + } else { + self.im.clone() + }; + let im_abs = FBig::from_repr(im_abs_repr, fctx); + Display::fmt(&im_abs, f)?; + } + f.write_char('i') + } +} + +impl Debug for CBig { + /// Structured form `"re: im: (prec:

)"` — e.g. `"re:1.5 im:-2.0 (prec: 53)"` — for + /// quick inspection (mirrors `FBig`'s `Debug` style). The alternate `#` form exposes the raw + /// significands and exponent scaling. + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + let fctx = self.context.float(); + let re = FBig::from_repr(self.re.clone(), fctx); + let im = FBig::from_repr(self.im.clone(), fctx); + if f.alternate() { + f.debug_struct("CBig") + .field("re", &re) + .field("im", &im) + .field("precision", &self.context.precision()) + .finish() + } else { + f.write_str("re:")?; + Display::fmt(&re, f)?; + f.write_str(" im:")?; + Display::fmt(&im, f)?; + f.write_fmt(format_args!(" (prec: {})", self.context.precision())) + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use alloc::format; + use dashu_float::round::mode; + + type C = CBig; + + fn c(re: i32, im: i32) -> C { + C::from_parts(re.into(), im.into()) + } + + #[test] + fn display_algebraic() { + assert_eq!(format!("{}", c(0, 0)), "0"); + assert_eq!(format!("{}", c(5, 0)), "5"); + assert_eq!(format!("{}", c(0, 1)), "i"); + assert_eq!(format!("{}", c(0, -1)), "-i"); + assert_eq!(format!("{}", c(0, 4)), "4i"); + assert_eq!(format!("{}", c(0, -4)), "-4i"); + assert_eq!(format!("{}", c(1, 2)), "1+2i"); + assert_eq!(format!("{}", c(-3, -4)), "-3-4i"); + assert_eq!(format!("{}", c(1, 1)), "1+i"); + assert_eq!(format!("{}", c(2, -1)), "2-i"); + } + + #[test] + fn display_constants() { + assert_eq!(format!("{}", C::I), "i"); + assert_eq!(format!("{}", C::ONE), "1"); + assert_eq!(format!("{}", C::ZERO), "0"); + } + + #[test] + fn debug_structured() { + let z = C::from_parts(FBig::from(3), FBig::from(4)); + let s = format!("{:?}", z); + assert!(s.starts_with("re:3 im:4 (prec:")); + } +} diff --git a/complex/src/helper_macros.rs b/complex/src/helper_macros.rs new file mode 100644 index 00000000..910647b6 --- /dev/null +++ b/complex/src/helper_macros.rs @@ -0,0 +1,113 @@ +//! Macros forwarding operator traits to the [`CBig`] context-layer operations. +//! +//! Following `FBig`, binary operators with a trait (`Add`/`Sub`/`Mul`/`Div`) and unary `Neg` have +//! **no** inherent method on [`CBig`] — the operator *is* the convenience API, and it computes the +//! result context (`max(lhs, rhs)`), calls the context-layer op, and unwraps via [`Context::unwrap_cfp`]. +//! The identifiers used inside the macro (`CBig`, `Context`, `Round`, `Word`) resolve at the call +//! site, so call sites must keep them in scope. +//! +//! [`CBig`]: crate::cbig::CBig +//! [`Context::unwrap_cfp`]: crate::repr::Context::unwrap_cfp + +/// Implement a binary operator (`Add`/`Sub`/`Mul`/`Div`) and its `Assign` form for all four +/// ref/val combinations. Each forwards to `Context::$method` at `max(lhs, rhs)` precision. +macro_rules! impl_cbig_binop { + ($trait:ident, $method:ident, $assign_trait:ident, $assign_method:ident) => { + impl $trait for CBig { + type Output = CBig; + #[inline] + fn $method(self, rhs: CBig) -> Self::Output { + let ctx = Context::max(self.context(), rhs.context()); + ctx.unwrap_cfp(ctx.$method(&self, &rhs)) + } + } + + impl $trait<&CBig> for CBig { + type Output = CBig; + #[inline] + fn $method(self, rhs: &CBig) -> Self::Output { + let ctx = Context::max(self.context(), rhs.context()); + ctx.unwrap_cfp(ctx.$method(&self, rhs)) + } + } + + impl $trait> for &CBig { + type Output = CBig; + #[inline] + fn $method(self, rhs: CBig) -> Self::Output { + let ctx = Context::max(self.context(), rhs.context()); + ctx.unwrap_cfp(ctx.$method(self, &rhs)) + } + } + + impl $trait<&CBig> for &CBig { + type Output = CBig; + #[inline] + fn $method(self, rhs: &CBig) -> Self::Output { + let ctx = Context::max(self.context(), rhs.context()); + ctx.unwrap_cfp(ctx.$method(self, rhs)) + } + } + + impl $assign_trait for CBig { + #[inline] + fn $assign_method(&mut self, rhs: CBig) { + let ctx = Context::max(self.context(), rhs.context()); + *self = ctx.unwrap_cfp(ctx.$method(self, &rhs)); + } + } + + impl $assign_trait<&CBig> for CBig { + #[inline] + fn $assign_method(&mut self, rhs: &CBig) { + let ctx = Context::max(self.context(), rhs.context()); + *self = ctx.unwrap_cfp(ctx.$method(self, rhs)); + } + } + }; +} + +/// Implement the four ref/val mixed-type operators `CBig op FBig` — each forwarding directly to +/// `Context::$ctx_method(self, rhs)`. The trait method name is `$trait_method`. Used for +/// `CBig * FBig` (componentwise, trait=multiply/mul, ctx=mul_real) and `CBig / FBig` +/// (componentwise, trait=divide/div, ctx=div_real). The identifiers (`CBig`, `FBig`, `Context`, +/// `Round`, `Word`) must be in scope at the call site. +macro_rules! impl_cbig_scalar_binop { + ($op:ident, $trait_method:ident, $ctx_method:ident) => { + impl $op<&FBig> for &CBig { + type Output = CBig; + #[inline] + fn $trait_method(self, rhs: &FBig) -> CBig { + let ctx = Context::max(self.context(), Context(rhs.context())); + ctx.unwrap_cfp(ctx.$ctx_method(self, rhs)) + } + } + impl $op> for &CBig { + type Output = CBig; + #[inline] + fn $trait_method(self, rhs: FBig) -> CBig { + let ctx = Context::max(self.context(), Context(rhs.context())); + ctx.unwrap_cfp(ctx.$ctx_method(self, &rhs)) + } + } + impl $op<&FBig> for CBig { + type Output = CBig; + #[inline] + fn $trait_method(self, rhs: &FBig) -> CBig { + let ctx = Context::max(self.context(), Context(rhs.context())); + ctx.unwrap_cfp(ctx.$ctx_method(&self, rhs)) + } + } + impl $op> for CBig { + type Output = CBig; + #[inline] + fn $trait_method(self, rhs: FBig) -> CBig { + let ctx = Context::max(self.context(), Context(rhs.context())); + ctx.unwrap_cfp(ctx.$ctx_method(&self, &rhs)) + } + } + }; +} + +pub(crate) use impl_cbig_binop; +pub(crate) use impl_cbig_scalar_binop; diff --git a/complex/src/lib.rs b/complex/src/lib.rs new file mode 100644 index 00000000..14aa6dda --- /dev/null +++ b/complex/src/lib.rs @@ -0,0 +1,96 @@ +// Copyright (c) 2026 Jacob Zhong +// +// Licensed under either of +// +// * Apache License, Version 2.0 +// (LICENSE-APACHE or https://www.apache.org/licenses/LICENSE-2.0) +// * MIT license +// (LICENSE-MIT or https://opensource.org/licenses/MIT) +// +// at your option. +// +// Unless you explicitly state otherwise, any contribution intentionally submitted +// for inclusion in the work by you, as defined in the Apache-2.0 license, shall be +// dual licensed as above, without any additional terms or conditions. + +//! A big arbitrary precision complex number library. +//! +//! The library provides the type [`CBig`]: an arbitrary-precision complex number built on top of +//! [`dashu_float`]'s [`FBig`]. Each [`CBig`] stores a real and an imaginary part ([`Repr`]) over a +//! single shared precision and rounding mode, mirroring [`FBig`]'s `Repr`+`Context` layout. It +//! targets parity with GNU MPC for the common functionalities (field arithmetic + elementary +//! transcendentals + abs/arg/conj/proj + I/O). +//! +//! Rounding follows the C99 Annex G / Kahan branch-cut and signed-zero model that `dashu-float` +//! already implements for reals. There is **no NaN**: C99 NaN-producing cases are mapped to +//! [`FpError`] at the [`Context`] layer (and panics at the convenience layer), exactly mirroring +//! how `FBig` behaves. +//! +//! # Two-layer API +//! +//! Like `FBig`, operations come in two layers: +//! * **Context layer** — [`Context`] methods return a [`CfpResult`] (`Result, FpError>`) +//! carrying per-axis inexactness `(Rounding, Rounding)`. +//! * **Convenience layer** — [`CBig`] methods and operators unwrap to a plain [`CBig`], panicking on +//! `Indeterminate` / `OutOfDomain` / `InfiniteInput` and saturating `Overflow`/`Underflow`. +//! +//! # Examples +//! +//! ``` +//! use dashu_cmplx::CBig; +//! use dashu_float::{FBig, round::mode::HalfAway}; +//! +//! type C = CBig; // base-10 so values render as decimals +//! let z = C::from_parts(FBig::from(3), FBig::from(4)); +//! let w = C::I; +//! let sum = &z + &w; // (3+4i) + i = 3+5i +//! assert_eq!(sum.re().significand(), &3.into()); +//! assert_eq!(sum.im().significand(), &5.into()); +//! +//! // algebraic display +//! assert_eq!(format!("{}", sum), "3+5i"); +//! ``` +//! +//! # Optional dependencies +//! +//! * `std` (*default*): enable `std` for dependencies. +//! * `num-order` (*default*): `NumOrd`/`NumHash` for `CBig`. +//! * `num-complex`: `TryFrom` conversions between `CBig` and `num-complex`'s `Complex`/ +//! `Complex` (base-2, mirroring `FBig`'s primitive-float conversions). + +#![cfg_attr(not(feature = "std"), no_std)] + +extern crate alloc; + +mod add; +mod cbig; +mod cmp; +mod convert; +mod div; +mod exp; +mod fmt; +mod helper_macros; +mod log; +pub mod math; +mod misc; +mod mul; +mod parse; +mod repr; +mod root; +mod third_party; + +// All the public items from third_party will be exposed +#[allow(unused_imports)] +pub use third_party::*; + +pub use cbig::CBig; +pub use repr::{CRounded, CfpResult, Context}; + +// Rounding machinery and the float primitives CBig is built on are reused from dashu-float +// unchanged (they appear in this crate's public signatures). +pub use dashu_float::round; // → dashu_cmplx::round::{mode, Round, Rounding} +pub use dashu_float::round::{Round, Rounding}; +pub use dashu_float::{ConstCache, FBig, FpError, Repr}; + +#[doc(hidden)] +pub use dashu_int::Word; // for the cbig! literal macro (M6) diff --git a/complex/src/log.rs b/complex/src/log.rs new file mode 100644 index 00000000..d79fb9d5 --- /dev/null +++ b/complex/src/log.rs @@ -0,0 +1,105 @@ +//! Complex natural logarithm `log(z) = ln|z| + i·arg(z)` (principal branch; cut on `]−∞, 0]`). + +use crate::cbig::CBig; +use crate::repr::{combine_parts, exact, reborrow_cache, riemann, CfpResult, Context}; +use dashu_float::round::Round; +use dashu_float::{ConstCache, FBig, Repr}; +use dashu_int::Word; + +/// Guard digits (base-B) for `log`. Composes `hypot` (for `|z|`), `ln`, and `atan2`. +const LOG_GUARD: usize = 14; + +impl Context { + /// Complex natural logarithm under this context (context layer). `log z = ln|z| + i·arg(z)`, + /// with the imaginary part in `]−π, π]`. The cache threads into `ln` and `atan2`. + /// + /// Special values: `log(0) = -∞ + i·0`; `log(±∞) = +∞`; the branch cut on `]−∞, 0]` is handled + /// by the signed-zero `atan2` (so `log(-r ± i0) = ln r ± iπ`). + pub fn log( + &self, + z: &CBig, + mut cache: Option<&mut ConstCache>, + ) -> CfpResult { + if z.is_zero() { + // log(±0) = -∞ + i·arg(±0); arg(0,0) is undefined — report the real -∞ via ln(0) + return Ok(exact( + FBig::from_repr(Repr::neg_infinity(), self.float()), + FBig::from_repr(Repr::zero(), self.float()), + )); + } + if z.is_infinite() { + return Ok(riemann(*self)); // log(∞) = +∞ (Riemann point) + } + + let gctx = self.guard(LOG_GUARD); + let p = self.precision(); + // ln|z| + let r = gctx.hypot(z.re(), z.im())?.value(); + let ln_r = gctx.ln(r.repr(), reborrow_cache(&mut cache))?.value(); + // arg(z) = atan2(im, re) + let arg = gctx + .atan2(z.im(), z.re(), reborrow_cache(&mut cache))? + .value(); + let re = ln_r.with_precision(p); + let im = arg.with_precision(p); + Ok(combine_parts(re, im)) + } +} + +impl CBig { + /// Complex natural logarithm (principal branch; convenience layer). + /// + /// # Panics + /// + /// Panics if the precision is unlimited. + #[inline] + pub fn ln(&self) -> Self { + self.context().unwrap_cfp(self.context().log(self, None)) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use dashu_base::{Abs, AbsOrd, Sign}; + use dashu_float::round::mode; + + type C = CBig; + type F = FBig; + + fn c(re: i32, im: i32) -> C { + let mk = |v: i32| -> F { F::from(v).with_precision(53).value() }; + CBig::from_parts(mk(re), mk(im)) + } + + fn within(a: &F, b: &F, k: u32) -> bool { + if a == b { + return true; + } + let diff = (a.clone() - b.clone()).abs(); + diff.abs_cmp(&(a.ulp() * F::from(k))).is_le() + } + + #[test] + fn ln_one_is_zero() { + assert!(C::ONE.ln() == C::ZERO); + } + + #[test] + fn ln_exp_roundtrip() { + // ln(exp z) ≈ z (the imaginary 1 sits inside ]-π, π], so no 2πi wrap) + let z = c(1, 1); + let l = z.exp().ln(); + let (zr, zi) = z.into_parts(); + let (lr, li) = l.into_parts(); + assert!(within(&zr, &lr, 16)); + assert!(within(&zi, &li, 16)); + } + + #[test] + fn ln_zero_is_neg_infinity() { + let l = C::ZERO.ln(); + assert!(l.re().is_infinite()); + assert_eq!(l.re().sign(), Sign::Negative); + } +} diff --git a/complex/src/math/mod.rs b/complex/src/math/mod.rs new file mode 100644 index 00000000..6f92e1e8 --- /dev/null +++ b/complex/src/math/mod.rs @@ -0,0 +1,7 @@ +//! Advanced mathematical functions. +//! +//! Mirroring `dashu-float`'s `math` module, the transcendental functions live under here. Currently +//! only [`trig`] (complex `sin`/`cos`/`tan`/`asin`/`acos`/`atan`); `dashu-cmplx` reuses +//! `dashu-float`'s hyperbolic and constant-cache machinery directly rather than redefining it. + +pub mod trig; diff --git a/complex/src/math/trig.rs b/complex/src/math/trig.rs new file mode 100644 index 00000000..9f3fabfb --- /dev/null +++ b/complex/src/math/trig.rs @@ -0,0 +1,344 @@ +//! Complex trigonometric functions via the real–imaginary decomposition, reusing `dashu-float`'s +//! real `sin`/`cos` and cancellation-free `sinh`/`cosh`. +//! +//! `sin(x+iy) = sin x·cosh y + i·cos x·sinh y`, `cos(x+iy) = cos x·cosh y − i·sin x·sinh y`. This +//! form avoids the `exp(±iz)` identity's exponential blow-up for large `|Im z|`. + +use crate::cbig::CBig; +use crate::repr::{combine_parts, reborrow_cache, CfpResult, Context}; +use dashu_float::round::Round; +use dashu_float::{ConstCache, FBig, FpError, Repr}; +use dashu_int::{IBig, Word}; + +/// Guard digits (base-B) for the forward trig. Composes real `sin_cos` + `sinh_cosh` + two +/// products; the cancellation near the trig zeros is absorbed by the re-round. +const TRIG_GUARD: usize = 16; + +impl Context { + /// Simultaneously compute `sin z` and `cos z` (context layer). Returns `(sin, cos)` each as a + /// [`CfpResult`]. An infinite input maps to [`FpError::Indeterminate`] (the C99 NaN cases). + pub fn sin_cos( + &self, + z: &CBig, + mut cache: Option<&mut ConstCache>, + ) -> (CfpResult, CfpResult) { + if z.is_infinite() { + return (Err(FpError::Indeterminate), Err(FpError::Indeterminate)); + } + if z.is_zero() { + let zero = Ok(crate::repr::exact( + FBig::from_repr(Repr::zero(), self.float()), + FBig::from_repr(Repr::zero(), self.float()), + )); + let one = Ok(crate::repr::exact( + FBig::from_repr(Repr::one(), self.float()), + FBig::from_repr(Repr::zero(), self.float()), + )); + return (zero, one); + } + + let gctx = self.guard(TRIG_GUARD); + let p = self.precision(); + let (sinx, cosx) = gctx.sin_cos(z.re(), reborrow_cache(&mut cache)); + let sinx = match sinx { + Ok(v) => v.value(), + Err(e) => return (Err(e), Err(FpError::Indeterminate)), + }; + let cosx = match cosx { + Ok(v) => v.value(), + Err(e) => return (Err(FpError::Indeterminate), Err(e)), + }; + let (sinhy_res, coshy_res) = gctx.sinh_cosh(z.im(), reborrow_cache(&mut cache)); + let sinhy = match sinhy_res { + Ok(v) => v.value(), + Err(e) => return (Err(e), Err(FpError::Indeterminate)), + }; + let coshy = match coshy_res { + Ok(v) => v.value(), + Err(e) => return (Err(FpError::Indeterminate), Err(e)), + }; + + // sin z = (sinx·coshy) + i·(cosx·sinhy); cos z = (cosx·coshy) − i·(sinx·sinhy). + // `sin_cos` returns a tuple, so the products are matched explicitly (no `?`). + let prod = |a: &FBig, b: &FBig| -> Result<_, FpError> { + Ok(gctx.mul(a.repr(), b.repr())?.value().with_precision(p)) + }; + let sin_re = match prod(&sinx, &coshy) { + Ok(v) => v, + Err(e) => return (Err(e), Err(FpError::Indeterminate)), + }; + let sin_im = match prod(&cosx, &sinhy) { + Ok(v) => v, + Err(e) => return (Err(e), Err(FpError::Indeterminate)), + }; + let cos_re = match prod(&cosx, &coshy) { + Ok(v) => v, + Err(e) => return (Err(FpError::Indeterminate), Err(e)), + }; + let neg_sinx = -sinx; + let cos_im = match prod(&neg_sinx, &sinhy) { + Ok(v) => v, + Err(e) => return (Err(FpError::Indeterminate), Err(e)), + }; + (Ok(combine_parts(sin_re, sin_im)), Ok(combine_parts(cos_re, cos_im))) + } + + /// Complex sine (context layer). + #[inline] + pub fn sin( + &self, + z: &CBig, + cache: Option<&mut ConstCache>, + ) -> CfpResult { + self.sin_cos(z, cache).0 + } + + /// Complex cosine (context layer). + #[inline] + pub fn cos( + &self, + z: &CBig, + cache: Option<&mut ConstCache>, + ) -> CfpResult { + self.sin_cos(z, cache).1 + } + + /// Complex tangent `sin z / cos z` (context layer). + pub fn tan( + &self, + z: &CBig, + cache: Option<&mut ConstCache>, + ) -> CfpResult { + let (sin_z, cos_z) = self.sin_cos(z, cache); + let sin_z = sin_z?; + let cos_z = cos_z?; + self.div(&sin_z.value(), &cos_z.value()) + } + + /// Inverse sine `asin z = -i·log(iz + sqrt(1-z²))` (context layer, Kahan form). The argument of + /// the inner `log` always has positive real part, so the branch cut comes entirely from the + /// `sqrt`; an infinite input maps to [`FpError::Indeterminate`]. + pub fn asin( + &self, + z: &CBig, + mut cache: Option<&mut ConstCache>, + ) -> CfpResult { + if z.is_infinite() { + return Err(FpError::Indeterminate); + } + let gctx = Context::new(self.precision() + ITRIG_GUARD); + let p = self.precision(); + let one = CBig::ONE; + let z2 = gctx.sqr(z)?.value(); + let one_m_z2 = gctx.sub(&one, &z2)?.value(); + let sqrt_term = gctx.sqrt(&one_m_z2)?.value(); + let iz = z.mul_i(false); // exact rotation + let w = gctx.add(&iz, &sqrt_term)?.value(); + let log_w = gctx.log(&w, reborrow_cache(&mut cache))?.value(); + let asin_z = log_w.mul_i(true); // -i·log(w) + let (re, im) = asin_z.into_parts(); + Ok(combine_parts(re.with_precision(p), im.with_precision(p))) + } + + /// Inverse cosine `acos z = -i·log(z + i·sqrt(1-z²))` (context layer, Kahan form). + pub fn acos( + &self, + z: &CBig, + mut cache: Option<&mut ConstCache>, + ) -> CfpResult { + if z.is_infinite() { + return Err(FpError::Indeterminate); + } + let gctx = Context::new(self.precision() + ITRIG_GUARD); + let p = self.precision(); + let one = CBig::ONE; + let z2 = gctx.sqr(z)?.value(); + let one_m_z2 = gctx.sub(&one, &z2)?.value(); + let sqrt_term = gctx.sqrt(&one_m_z2)?.value(); + let i_sqrt = sqrt_term.mul_i(false); // i·sqrt(1-z²) + let w = gctx.add(z, &i_sqrt)?.value(); + let log_w = gctx.log(&w, reborrow_cache(&mut cache))?.value(); + let acos_z = log_w.mul_i(true); // -i·log(w) + let (re, im) = acos_z.into_parts(); + Ok(combine_parts(re.with_precision(p), im.with_precision(p))) + } + + /// Inverse tangent `atan z = (i/2)·(log(1-iz) - log(1+iz))` (context layer). + pub fn atan( + &self, + z: &CBig, + mut cache: Option<&mut ConstCache>, + ) -> CfpResult { + if z.is_infinite() { + // atan(±∞) = ±π/2; defer the exact constant to the formula via the limit, but the + // 1±iz terms become infinite and the log diverges — report Indeterminate for now. + return Err(FpError::Indeterminate); + } + let gctx = Context::new(self.precision() + ITRIG_GUARD); + let p = self.precision(); + let one = CBig::ONE; + let iz = z.mul_i(false); + let a = gctx.sub(&one, &iz)?.value(); // 1 - iz + let b = gctx.add(&one, &iz)?.value(); // 1 + iz + let log_a = gctx.log(&a, reborrow_cache(&mut cache))?.value(); + let log_b = gctx.log(&b, reborrow_cache(&mut cache))?.value(); + let diff = gctx.sub(&log_a, &log_b)?.value(); + let i_half_diff = diff.mul_i(false); // i·diff, then /2 below + let two: CBig = IBig::from(2).into(); + let atan_z = gctx.div(&i_half_diff, &two)?.value(); + let (re, im) = atan_z.into_parts(); + Ok(combine_parts(re.with_precision(p), im.with_precision(p))) + } +} + +/// Guard digits (base-B) for the inverse trig (squares, a sqrt, logs, and a divide). +const ITRIG_GUARD: usize = 18; + +impl CBig { + /// Complex sine (convenience layer). Panics on an indeterminate special value. + #[inline] + pub fn sin(&self) -> Self { + self.context().unwrap_cfp(self.context().sin(self, None)) + } + + /// Complex cosine (convenience layer). Panics on an indeterminate special value. + #[inline] + pub fn cos(&self) -> Self { + self.context().unwrap_cfp(self.context().cos(self, None)) + } + + /// Simultaneously compute `(sin z, cos z)` (convenience layer). + #[inline] + pub fn sin_cos(&self) -> (Self, Self) { + let (s, c) = self.context().sin_cos(self, None); + (self.context().unwrap_cfp(s), self.context().unwrap_cfp(c)) + } + + /// Complex tangent (convenience layer). + #[inline] + pub fn tan(&self) -> Self { + self.context().unwrap_cfp(self.context().tan(self, None)) + } + + /// Inverse sine (convenience layer). + #[inline] + pub fn asin(&self) -> Self { + self.context().unwrap_cfp(self.context().asin(self, None)) + } + + /// Inverse cosine (convenience layer). + #[inline] + pub fn acos(&self) -> Self { + self.context().unwrap_cfp(self.context().acos(self, None)) + } + + /// Inverse tangent (convenience layer). + #[inline] + pub fn atan(&self) -> Self { + self.context().unwrap_cfp(self.context().atan(self, None)) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use dashu_float::round::mode; + + type C = CBig; + type F = FBig; + + fn c(re: i32, im: i32) -> C { + let mk = |v: i32| -> F { F::from(v).with_precision(53).value() }; + CBig::from_parts(mk(re), mk(im)) + } + + #[test] + fn sin_zero_is_zero() { + assert!(C::ZERO.sin() == C::ZERO); + } + + #[test] + fn cos_zero_is_one() { + assert!(C::ZERO.cos() == C::ONE); + } + + #[test] + fn pythagorean_identity() { + // sin²z + cos²z = 1 + let z = c(1, 1); + let s = z.sin(); + let co = z.cos(); + let sum = &s.sqr() + &co.sqr(); + // purely real ≈ 1, imaginary ≈ 0 + let (re, im) = sum.into_parts(); + use dashu_base::{Abs, AbsOrd}; + assert!((re.clone() - F::ONE) + .abs() + .abs_cmp(&F::from_parts(1.into(), -12)) + .is_le()); + assert!(im.abs_cmp(&F::from_parts(1.into(), -12)).is_le()); + } + + #[test] + fn sin_i_is_i_sinh_one() { + // sin(i) = i·sinh(1) = i·1.1752… ; purely imaginary + let s = C::I.sin(); + assert!(s.re().significand().is_zero()); + assert!(!s.im().significand().is_zero()); + } + + #[test] + fn asin_zero_is_zero() { + assert!(C::ZERO.asin() == C::ZERO); + } + + #[test] + fn asin_one_is_half_pi() { + use dashu_base::{Abs, AbsOrd}; + // asin(1) = π/2 + let (re, im) = C::ONE.asin().into_parts(); + let half_pi = F::from_parts(15707963267948966i64.into(), -16) + .with_precision(60) + .value(); + assert!((re.clone() - half_pi) + .abs() + .abs_cmp(&F::from_parts(1.into(), -12)) + .is_le()); + assert!(im.abs_cmp(&F::from_parts(1.into(), -12)).is_le()); + } + + #[test] + fn acos_zero_is_half_pi() { + use dashu_base::{Abs, AbsOrd}; + let (re, _im) = C::ZERO.acos().into_parts(); + let half_pi = F::from_parts(15707963267948966i64.into(), -16) + .with_precision(60) + .value(); + assert!((re - half_pi) + .abs() + .abs_cmp(&F::from_parts(1.into(), -12)) + .is_le()); + } + + #[test] + fn atan_one_is_quarter_pi() { + use dashu_base::{Abs, AbsOrd}; + // atan(1) = π/4 + let (re, _im) = C::ONE.atan().into_parts(); + let quarter_pi = F::from_parts(7853981633974483i64.into(), -16) + .with_precision(60) + .value(); + assert!((re - quarter_pi) + .abs() + .abs_cmp(&F::from_parts(1.into(), -12)) + .is_le()); + } + + #[test] + fn sin_asin_roundtrip() { + // asin(sin z) ≈ z for a small z (within the principal range) + let z = c(1, 1); + let r = z.sin().asin(); + assert!(r == z); + } +} diff --git a/complex/src/misc.rs b/complex/src/misc.rs new file mode 100644 index 00000000..6ebf89ac --- /dev/null +++ b/complex/src/misc.rs @@ -0,0 +1,241 @@ +//! Decomposition and miscellaneous operations: `neg`, `conj`, `proj`, `mul_i`, `norm`, `arg`. + +use crate::cbig::CBig; +use crate::repr::{exact, CfpResult, Context}; +use core::ops::Neg; +use dashu_base::Sign; +use dashu_float::round::Round; +use dashu_float::{FBig, FpResult, Repr}; +use dashu_int::Word; + +/// Guard digits (base-B) used by `norm` — well-conditioned (sum of squares, no cancellation), so a +/// small fixed guard comfortably settles the accumulated rounding of two squarings and an add. +const NORM_GUARD: usize = 8; + +/// Guard digits (base-B) for `abs`. The inner `hypot` already carries its own guard; this extra +/// margin absorbs the final re-round to the CBig precision. +const ABS_GUARD: usize = 8; + +impl CBig { + /// The complex conjugate `x - iy`. Exact (sign flip of the imaginary part, including `-0`/`-inf`). + #[inline] + pub fn conj(&self) -> Self { + self.context.unwrap_cfp(self.context.conj(self)) + } + + /// Project onto the Riemann sphere (`proj`): any part-infinite value maps to `+∞ + i·0` (the + /// imaginary zero carrying the sign of the original imaginary part); finite values are unchanged. + #[inline] + pub fn proj(&self) -> Self { + self.context.unwrap_cfp(self.context.proj(self)) + } + + /// Multiply by `±i` (exact rotation): `×i` maps `(re, im) -> (-im, re)`, `×(-i)` maps + /// `(re, im) -> (im, -re)`. + #[inline] + pub fn mul_i(&self, negative: bool) -> Self { + self.context.unwrap_cfp(self.context.mul_i(self, negative)) + } + + /// The squared modulus `re² + im²` (a real [`FBig`]). Cheap and near-exact — it avoids the + /// `sqrt` of [`CBig::abs`]. Matches num-complex's `norm_sqr`. + #[inline] + pub fn norm(&self) -> FBig { + self.context.float().unwrap_fp(self.context.norm(self)) + } + + /// The modulus `|z| = sqrt(re² + im²)` (a real [`FBig`]). A thin composition over + /// [`dashu_float::Context::hypot`] (the overflow-safe scaled sum-of-squares), evaluated at guard + /// precision and re-rounded. Near-correctly rounded. + /// + /// # Panics + /// + /// Panics if the precision is unlimited. + #[inline] + pub fn abs(&self) -> FBig { + self.context.float().unwrap_fp(self.context.abs(self)) + } + + /// The argument (phase) `atan2(im, re) ∈ ]-π, π]`. The branch cut lies on `]−∞, 0]`; signed zero + /// and infinities are handled per the C99 Annex G `atan2` table (reused from `dashu-float`). + #[inline] + pub fn arg(&self) -> FBig { + self.context.float().unwrap_fp(self.context.arg(self, None)) + } +} + +impl Neg for CBig { + type Output = CBig; + #[inline] + fn neg(self) -> Self::Output { + self.context().unwrap_cfp(self.context().neg(&self)) + } +} + +impl Neg for &CBig { + type Output = CBig; + #[inline] + fn neg(self) -> Self::Output { + self.context().unwrap_cfp(self.context().neg(self)) + } +} + +impl Context { + /// Negate under this context (context layer). Exact. + pub fn neg(&self, z: &CBig) -> CfpResult { + Ok(exact( + FBig::from_repr(-z.re.clone(), self.float()), + FBig::from_repr(-z.im.clone(), self.float()), + )) + } + + /// Complex conjugate under this context (context layer). Exact. + pub fn conj(&self, z: &CBig) -> CfpResult { + Ok(exact( + FBig::from_repr(z.re.clone(), self.float()), + FBig::from_repr(-z.im.clone(), self.float()), + )) + } + + /// Riemann projection under this context (context layer). Exact. + pub fn proj(&self, z: &CBig) -> CfpResult { + if z.is_infinite() { + // +∞ on the real part; the imaginary zero carries the sign of the original imag part. + let im = if z.im().sign() == Sign::Negative { + Repr::neg_zero() + } else { + Repr::zero() + }; + Ok(exact( + FBig::from_repr(Repr::infinity(), self.float()), + FBig::from_repr(im, self.float()), + )) + } else { + Ok(exact( + FBig::from_repr(z.re.clone(), self.float()), + FBig::from_repr(z.im.clone(), self.float()), + )) + } + } + + /// Multiply by `±i` under this context (context layer). Exact rotation. + pub fn mul_i(&self, z: &CBig, negative: bool) -> CfpResult { + let (re, im) = if negative { + // ×(-i): (x, y) -> (y, -x) + (z.im.clone(), -z.re.clone()) + } else { + // ×i: (x, y) -> (-y, x) + (-z.im.clone(), z.re.clone()) + }; + Ok(exact(FBig::from_repr(re, self.float()), FBig::from_repr(im, self.float()))) + } + + /// The squared modulus `re² + im²` (context layer). Near-exact; returns `+∞` for an infinite + /// input and propagates overflow to a signed infinity via the float `unwrap_fp` policy. + pub fn norm(&self, z: &CBig) -> FpResult> { + if z.is_infinite() { + return Ok(dashu_base::Approximation::Exact(FBig::from_repr( + Repr::infinity(), + self.float(), + ))); + } + let gctx = self.guard(NORM_GUARD); + let re2 = gctx.unwrap_fp(gctx.sqr(z.re())); + let im2 = gctx.unwrap_fp(gctx.sqr(z.im())); + let n = gctx.unwrap_fp(gctx.add(re2.repr(), im2.repr())); + Ok(n.with_precision(self.precision())) + } + + /// The argument `atan2(im, re)` (context layer). Delegates to `dashu-float`'s Annex-G `atan2`; + /// the cache threads into it (the convenience layer passes `None`). + pub fn arg( + &self, + z: &CBig, + cache: Option<&mut dashu_float::ConstCache>, + ) -> FpResult> { + self.float().atan2(z.im(), z.re(), cache) + } + + /// The modulus `|z| = hypot(re, im)` (context layer). Near-correctly rounded; returns `+∞` for + /// an infinite input. Thin composition over [`dashu_float::Context::hypot`]. + /// + /// # Panics + /// + /// Panics if the precision is unlimited. + pub fn abs(&self, z: &CBig) -> FpResult> { + let gctx = self.guard(ABS_GUARD); + let h = gctx.hypot(z.re(), z.im())?; + Ok(h.value().with_precision(self.precision())) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use dashu_float::round::mode; + + type C = CBig; + + #[test] + fn neg_and_conj() { + let z = C::from_parts(3.into(), 4.into()); + let n = -&z; // Neg by reference (no inherent neg method) + assert_eq!(n.re().significand(), &(-3i32).into()); + assert_eq!(n.im().significand(), &(-4i32).into()); + let c = z.conj(); + assert_eq!(c.re().significand(), &3.into()); + assert_eq!(c.im().significand(), &(-4i32).into()); + } + + #[test] + fn mul_i_rotation() { + let z = C::from_parts(3.into(), 4.into()); + // ×i: (3,4) -> (-4, 3) + let zi = z.mul_i(false); + assert_eq!(zi.re().significand(), &(-4i32).into()); + assert_eq!(zi.im().significand(), &3.into()); + // ×(-i): (3,4) -> (4, -3) + let zni = z.mul_i(true); + assert_eq!(zni.re().significand(), &4.into()); + assert_eq!(zni.im().significand(), &(-3i32).into()); + // mul_i^4 == identity + let id = z.mul_i(false).mul_i(false).mul_i(false).mul_i(false); + assert!(id == z); + } + + #[test] + fn proj_finite_unchanged() { + let z = C::from_parts(3.into(), 4.into()); + assert!(z.proj() == z); + } + + #[test] + fn proj_infinite_is_riemann_point() { + let inf = C::new(Repr::infinity(), Repr::<10>::new(5.into(), 0), Context::new(53)); + let p = inf.proj(); + assert!(p.re().is_infinite()); + assert_eq!(p.re().sign(), Sign::Positive); + assert!(p.im().is_zero()); + } + + #[test] + fn norm_of_3_4_is_25() { + // build at precision 53 so the 2-digit result 25 is exact + type F = FBig; + let mk = |v: i32| -> F { F::from(v).with_precision(53).value() }; + let z = C::from_parts(mk(3), mk(4)); + let n = z.norm(); + assert_eq!(n.repr().significand(), &25.into()); + } + + #[test] + fn arg_of_1_1_is_pi_quarter() { + type F = FBig; + let mk = |v: i32| -> F { F::from(v).with_precision(53).value() }; + let z = C::from_parts(mk(1), mk(1)); + let a = z.arg(); + // atan(1) = π/4 ≈ 0.7854, strictly between 0 and 1 + assert!(a > F::ZERO); + assert!(a < F::ONE); + } +} diff --git a/complex/src/mul.rs b/complex/src/mul.rs new file mode 100644 index 00000000..926f33cc --- /dev/null +++ b/complex/src/mul.rs @@ -0,0 +1,194 @@ +//! Complex squaring and multiplication (near-correctly rounded via the guard-digit recipe). + +use crate::cbig::CBig; +use crate::repr::{combine_parts, exact, riemann, CfpResult, Context}; +use core::ops::{Mul, MulAssign}; +use dashu_float::round::Round; +use dashu_float::{FBig, FpError}; +use dashu_int::Word; + +/// Guard digits (base-B) for `sqr`/`mul`. The published normwise error bound for complex +/// multiplication is `< √5·u` (Brent–Percival–Zimmermann), so a small fixed guard comfortably +/// settles the accumulated rounding of the 2–4 component products for non-cancelling inputs. +const MUL_GUARD: usize = 10; + +impl Context { + /// Square a complex number under this context: `(x+iy)² = (x²-y²) + i(2xy)`. + pub fn sqr(&self, z: &CBig) -> CfpResult { + if z.is_infinite() { + return Ok(riemann(*self)); // ∞·∞ = Riemann infinity + } + if z.is_zero() { + return Ok(exact(FBig::ZERO, FBig::ZERO)); + } + let gctx = self.guard(MUL_GUARD); + let p = self.precision(); + let (x, y) = (z.re(), z.im()); + // real part: x² - y² + let x2 = gctx.sqr(x)?.value(); + let y2 = gctx.sqr(y)?.value(); + let re = gctx.sub(x2.repr(), y2.repr())?.value().with_precision(p); + // imaginary part: 2·x·y + let xy = gctx.mul(x, y)?.value(); + let im = gctx.add(xy.repr(), xy.repr())?.value().with_precision(p); + Ok(combine_parts(re, im)) + } + + /// Multiply two complex numbers under this context: `(x+iy)(u+iv) = (xu-yv) + i(xv+yu)` + /// (naive 4-mul form; near-correctly rounded via the guard re-round). + pub fn mul(&self, z: &CBig, w: &CBig) -> CfpResult { + if z.is_infinite() || w.is_infinite() { + if z.is_zero() || w.is_zero() { + return Err(FpError::Indeterminate); // 0·∞ + } + return Ok(riemann(Context::max(z.context(), w.context()))); // ∞·finite = Riemann infinity + } + let gctx = self.guard(MUL_GUARD); + let p = self.precision(); + let (x, y) = (z.re(), z.im()); + let (u, v) = (w.re(), w.im()); + // real part: xu - yv + let xu = gctx.mul(x, u)?.value(); + let yv = gctx.mul(y, v)?.value(); + let re = gctx.sub(xu.repr(), yv.repr())?.value().with_precision(p); + // imaginary part: xv + yu + let xv = gctx.mul(x, v)?.value(); + let yu = gctx.mul(y, u)?.value(); + let im = gctx.add(xv.repr(), yu.repr())?.value().with_precision(p); + Ok(combine_parts(re, im)) + } + + /// Multiply a complex number by a real scalar (context layer): `(x+iy)·s = (xs) + i(ys)`. + pub fn mul_real(&self, z: &CBig, s: &FBig) -> CfpResult { + if z.is_infinite() || s.repr().is_infinite() { + if z.is_zero() || s.repr().is_zero() || s.repr().is_neg_zero() { + return Err(FpError::Indeterminate); // 0·∞ + } + return Ok(riemann(*self)); + } + let gctx = self.guard(MUL_GUARD); + let p = self.precision(); + let re = gctx.mul(z.re(), s.repr())?.value().with_precision(p); + let im = gctx.mul(z.im(), s.repr())?.value().with_precision(p); + Ok(combine_parts(re, im)) + } +} + +impl CBig { + /// Square the complex number (convenience layer). + #[inline] + pub fn sqr(&self) -> Self { + self.context().unwrap_cfp(self.context().sqr(self)) + } +} + +// CBig · CBig operators — forwarded through the standard macro (mirroring `dashu-float`'s `mul.rs`). +crate::helper_macros::impl_cbig_binop!(Mul, mul, MulAssign, mul_assign); + +// --- scalar multiplication by a real FBig (mixed-type operators) --- + +// CBig · FBig (componentwise, via the shared scalar macro). +crate::helper_macros::impl_cbig_scalar_binop!(Mul, mul, mul_real); + +// FBig · CBig (commutative: FBig·CBig = CBig·FBig). +impl Mul<&CBig> for &FBig { + type Output = CBig; + #[inline] + fn mul(self, rhs: &CBig) -> CBig { + rhs * self + } +} +impl Mul> for &FBig { + type Output = CBig; + #[inline] + fn mul(self, rhs: CBig) -> CBig { + &rhs * self + } +} +impl Mul<&CBig> for FBig { + type Output = CBig; + #[inline] + fn mul(self, rhs: &CBig) -> CBig { + rhs * &self + } +} +impl Mul> for FBig { + type Output = CBig; + #[inline] + fn mul(self, rhs: CBig) -> CBig { + &rhs * &self + } +} + +#[cfg(test)] +mod tests { + use super::*; + use dashu_float::round::mode; + + type C = CBig; + type F = FBig; + + fn c(re: i32, im: i32) -> C { + let mk = |v: i32| -> F { F::from(v).with_precision(53).value() }; + C::from_parts(mk(re), mk(im)) + } + + #[test] + fn sqr_basic() { + // (3+4i)² = -7+24i + let z = c(3, 4); + let s = z.sqr(); + assert_eq!(s.re().significand(), &(-7i32).into()); + assert_eq!(s.im().significand(), &24.into()); + } + + #[test] + fn mul_basic() { + // (1+2i)(3+4i) = -5+10i (compare full values: 10 normalizes to 1·10¹ in base 10) + let z = c(1, 2); + let w = c(3, 4); + let p = &z * &w; + assert!(p == c(-5, 10)); + } + + #[test] + fn mul_assign_val_and_ref() { + let z = c(1, 2); + let w = c(3, 4); + // (1+2i)(3+4i) = -5+10i + let mut acc = z.clone(); + acc *= w.clone(); + assert!(acc == c(-5, 10)); + let mut acc = z.clone(); + acc *= &w; + assert!(acc == c(-5, 10)); + } + + #[test] + fn mul_by_one_is_identity() { + let z = c(3, 4); + let p = &z * &CBig::ONE; + assert!(p == z); + } + + #[test] + fn mul_by_conj_is_norm() { + // z·conj(z) = norm(z), purely real + let z = c(3, 4); + let p = &z * &z.conj(); + assert!(p.im().is_zero() || p.im().is_neg_zero()); + assert_eq!(p.re().significand(), &25.into()); + } + + #[test] + fn scalar_mul_by_real() { + let z = c(3, 4); + let s = FBig::::from(2); + let p = &z * &s; + assert_eq!(p.re().significand(), &6.into()); + assert_eq!(p.im().significand(), &8.into()); + // commutes: s * z + let p2 = &s * &z; + assert_eq!(p2.re().significand(), &6.into()); + } +} diff --git a/complex/src/parse.rs b/complex/src/parse.rs new file mode 100644 index 00000000..2f62390a --- /dev/null +++ b/complex/src/parse.rs @@ -0,0 +1,122 @@ +//! [`FromStr`] for [`CBig`] — the algebraic `a+bi` grammar that [`Display`](crate::CBig) emits. +//! +//! Accepts an optional real term and an optional `"i"` imaginary term (at least one +//! required): `"5"`, `"-7i"`, `"i"`, `"-i"`, `"1+2i"`, `"-3-4i"`. Each coefficient parses via +//! [`FBig`]'s `FromStr` (so `inf`/`-inf` are accepted). The MPC parenthesized `"(re im)"` form and +//! anything else malformed yield a [`ParseError`]. + +use crate::cbig::CBig; +use core::str::FromStr; +use dashu_base::ParseError; +use dashu_float::round::Round; +use dashu_float::{FBig, Repr}; +use dashu_int::Word; + +impl FromStr for CBig { + type Err = ParseError; + + fn from_str(s: &str) -> Result { + let s = s.trim(); + if s.is_empty() { + return Err(ParseError::NoDigits); + } + // Reject the MPC parenthesized form "(re im)" outright. + if s.contains('(') || s.contains(')') { + return Err(ParseError::InvalidDigit); + } + + // The only valid 'i' is the trailing imaginary-unit marker. + let i_count = s.bytes().filter(|&c| c == b'i').count(); + if i_count > 1 { + return Err(ParseError::InvalidDigit); + } + + if i_count == 0 { + // pure real term + let re = FBig::::from_str(s)?; + return Ok(CBig::from(re)); + } + + // exactly one 'i', and it must be the final character + if !s.ends_with('i') { + return Err(ParseError::InvalidDigit); + } + let prefix = &s[..s.len() - 1]; + + // Split prefix into the real term and the imaginary coefficient. The imaginary coefficient + // starts at the last '+' / '-' that is *not* at the leading position; if there is none, the + // whole prefix (if any) is the imaginary coefficient and the real term is empty. + let split = prefix.rfind(['+', '-']).filter(|&pos| pos > 0); + let (real_str, imag_str) = match split { + Some(pos) => (&prefix[..pos], &prefix[pos..]), + None => ("", prefix), + }; + + let im = match imag_str { + "" | "+" => FBig::::ONE, + "-" => FBig::::NEG_ONE, + other => FBig::::from_str(other)?, + }; + + let re = if real_str.is_empty() { + // implicit real zero carries the imaginary part's precision + FBig::from_repr(Repr::zero(), im.context()) + } else { + FBig::::from_str(real_str)? + }; + + Ok(CBig::from_parts(re, im)) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use alloc::format; + use dashu_float::round::mode; + + type C = CBig; + + fn parse_ok(s: &str) -> C { + s.parse() + .unwrap_or_else(|e| panic!("failed to parse {s:?}: {e:?}")) + } + + #[test] + fn roundtrip_display_fromstr() { + let cases = [ + "0", "5", "-7i", "i", "-i", "1+2i", "-3-4i", "1+i", "2-i", "4i", + ]; + for s in cases { + let z: C = parse_ok(s); + assert_eq!(format!("{}", z), s, "roundtrip failed for {s:?}"); + } + } + + #[test] + fn pure_real() { + let z: C = "5".parse().unwrap(); + assert!(z.im().is_zero()); + assert_eq!(z.re().significand(), &5.into()); + } + + #[test] + fn pure_imaginary_unit() { + let z: C = "i".parse().unwrap(); + assert!(z.re().is_zero()); + assert_eq!(z.im().significand(), &1.into()); + + let z: C = "-i".parse().unwrap(); + assert_eq!(z.im().significand(), &(-1i32).into()); + } + + #[test] + fn malformed_rejected() { + assert!("(1 2)".parse::().is_err()); + assert!("".parse::().is_err()); + assert!("1+2".parse::().is_err()); // 'i' required for an imaginary term + assert!("ii".parse::().is_err()); + assert!("1+2ii".parse::().is_err()); + assert!("i5".parse::().is_err()); // 'i' must be trailing + } +} diff --git a/complex/src/repr.rs b/complex/src/repr.rs new file mode 100644 index 00000000..2b25d707 --- /dev/null +++ b/complex/src/repr.rs @@ -0,0 +1,184 @@ +//! The complex [`Context`] and the result/inexactness types. +//! +//! Mirroring `dashu-float`'s `repr.rs` (which hosts both `Repr` and `Context`), the complex +//! [`Context`] lives here. `dashu-cmplx` reuses `dashu-float`'s [`Repr`] unchanged, so unlike float's +//! module this one holds only the complex-side pieces: the [`Context`] newtype and the +//! [`CfpResult`]/[`CRounded`] result types. +//! +//! [`Context`] is a thin newtype around [`dashu_float::Context`] that hosts the context-layer +//! CBig operations (it can't be added to `FBig`'s own `Context` from this crate — coherence). The +//! wrapped value *is* the shared precision/rounding config, so the config API +//! ([`Context::new`] / [`Context::max`] / [`Context::precision`]) just delegates to the inner float +//! context. +//! +//! The complex analog of `FpResult`/`Rounded` is [`CfpResult`]/[`CRounded`]: a complex result +//! carries **two** inexactness flags (one per axis), modeled as +//! `Approximation`. + +use dashu_base::Approximation; +use dashu_base::Approximation::*; +use dashu_float::round::{Round, Rounding}; +use dashu_float::{ConstCache, Context as FloatCtxt, FBig, FpError, Repr}; +use dashu_int::Word; + +use crate::cbig::CBig; + +/// CBig operation context — a newtype wrapper around [`dashu_float::Context`], and also the type +/// stored on each [`CBig`] as its shared precision/rounding config (so [`CBig::context`] returns it +/// directly, with no wrapping). +/// +/// It is a separate type because inherent methods cannot be added to `FBig`'s `Context` from this +/// crate; it exists to host the context-layer CBig operations ([`Context::mul`], [`Context::exp`], …). +/// The config API just delegates inward to the wrapped float context. +#[derive(Clone, Copy)] +pub struct Context(pub(crate) FloatCtxt); + +/// Correctly-rounded complex result with per-axis inexactness. +/// +/// `Exact(v)` ⟺ both parts are exact; `Inexact(v, (re, im))` carries each part's rounding +/// direction. This is the complex twin of [`dashu_float::round::Rounded`] (`Approximation`), +/// reusing the same [`Rounding`] flag type for each axis. +pub type CRounded = Approximation, (Rounding, Rounding)>; + +/// The result of a context-layer CBig operation: a correctly-rounded [`CBig`] (with per-axis +/// inexactness) or an [`FpError`]. The complex analog of [`dashu_float::FpResult`]. +pub type CfpResult = Result, FpError>; + +impl Context { + /// Create a CBig operation context with the given precision limit (`0` = unlimited). + #[inline] + pub const fn new(precision: usize) -> Self { + Self(FloatCtxt::new(precision)) + } + + /// Create a context with the higher precision from the two inputs (unlimited `0` dominates). + #[inline] + pub const fn max(lhs: Self, rhs: Self) -> Self { + Self(FloatCtxt::max(lhs.0, rhs.0)) + } + + /// The precision limit stored in the context (`0` = unlimited). Both parts of a [`CBig`] always + /// share this single precision. + #[inline] + pub const fn precision(&self) -> usize { + self.0.precision() + } + + /// The inner float context used to drive the real-part math (copied, since it is `Copy`). + #[inline] + pub(crate) const fn float(&self) -> FloatCtxt { + self.0 + } + + /// Build a transient float working context at `p + g` guard digits — the guard-digit recipe + /// (§6.1 of the design doc) evaluates each component at extra precision and re-rounds to `p`. + #[inline] + pub(crate) fn guard(&self, g: usize) -> FloatCtxt { + FloatCtxt::new(self.precision() + g) + } + + /// Unwrap a [`CfpResult`], returning the [`CBig`] value directly. + /// + /// The complex analog of [`dashu_float::Context::unwrap_fp`]. It drops the per-axis + /// `(Rounding, Rounding)` flags, and applies the same error policy: [`FpError::Overflow`] + /// saturates to a signed infinity, [`FpError::Underflow`] to a signed zero, and the remaining + /// variants panic. + #[inline] + pub fn unwrap_cfp(&self, result: CfpResult) -> CBig { + match result { + Ok(rounded) => rounded.value(), + Err(FpError::Overflow(sign)) => CBig::overflow(self, sign), + Err(FpError::Underflow(sign)) => CBig::underflow(self, sign), + Err(FpError::InfiniteInput) => { + panic!("arithmetic operations with the infinity are not allowed!") + } + Err(FpError::OutOfDomain) => panic!("the operation result is out of domain!"), + Err(FpError::Indeterminate) => { + panic!("the result of the operation is an indeterminate form!") + } + } + } +} + +/// Combine two per-part float rounding results into a [`CRounded`] complex result, carrying each +/// part's inexactness flag. `Exact` iff both parts are exact. +pub(crate) fn combine_parts( + re: Approximation, Rounding>, + im: Approximation, Rounding>, +) -> CRounded { + let (re_val, re_rnd) = match re { + Approximation::Exact(v) => (v, Rounding::NoOp), + Approximation::Inexact(v, r) => (v, r), + }; + let (im_val, im_rnd) = match im { + Approximation::Exact(v) => (v, Rounding::NoOp), + Approximation::Inexact(v, r) => (v, r), + }; + let value = CBig::from_parts(re_val, im_val); + if re_rnd == Rounding::NoOp && im_rnd == Rounding::NoOp { + Exact(value) + } else { + Inexact(value, (re_rnd, im_rnd)) + } +} + +/// Build a [`CRounded`] from two already-unwrapped float results, when the per-part rounding flags +/// are known directly (used by the exact/short-circuit special-value paths). +pub(crate) fn exact(re: FBig, im: FBig) -> CRounded { + Exact(CBig::from_parts(re, im)) +} + +/// The Riemann point at infinity `+∞ + i·0` as an exact [`CRounded`] result (dashu's complex +/// infinity — the single point `proj` collapses any infinity to). +pub(crate) fn riemann(context: Context) -> CRounded { + exact( + FBig::from_repr(Repr::infinity(), context.float()), + FBig::from_repr(Repr::zero(), context.float()), + ) +} + +/// Reborrow an `Option<&mut ConstCache>` for a sequential sub-call (mirrors `dashu-float`'s +/// `reborrow_cache`; `as_deref_mut` is the natural reborrow, allowed here centrally). +#[inline] +#[allow(clippy::needless_option_as_deref)] +pub(crate) fn reborrow_cache<'a>( + cache: &'a mut Option<&mut ConstCache>, +) -> Option<&'a mut ConstCache> { + cache.as_deref_mut() +} + +#[cfg(test)] +mod tests { + use super::*; + use dashu_base::Sign; + use dashu_float::round::mode; + use dashu_float::Repr; + + #[test] + fn context_delegates_to_float() { + let ctx: Context = Context::new(53); + assert_eq!(ctx.precision(), 53); + let bigger = Context::max(ctx, Context::new(10)); + assert_eq!(bigger.precision(), 53); + // unlimited (0) is treated as the minimum precision, so a limited operand wins + let limited_wins = Context::max(ctx, Context::new(0)); + assert_eq!(limited_wins.precision(), 53); + let both_unlimited = Context::max(Context::::new(0), Context::new(0)); + assert_eq!(both_unlimited.precision(), 0); + } + + #[test] + fn combine_parts_exact_and_inexact() { + let ctx: Context = Context::new(10); + let f = ctx.float(); + let one = FBig::from_repr(Repr::<2>::one(), f); + // two exact results combine to Exact + let combined = combine_parts(Exact(one.clone()), Exact(one.clone())); + assert!(matches!(combined, Exact(_))); + // an inexact result combines to Inexact + let add_one = f.add(one.repr(), one.repr()).unwrap(); // 1+1, exact at p=10 + let combined2 = combine_parts(add_one, Inexact(one, Rounding::AddOne)); + assert!(matches!(combined2, Inexact(_, _))); + let _ = Sign::Positive; // keep Sign referenced + } +} diff --git a/complex/src/root.rs b/complex/src/root.rs new file mode 100644 index 00000000..4a8d14cc --- /dev/null +++ b/complex/src/root.rs @@ -0,0 +1,185 @@ +//! Complex square root (principal branch; cut on `]−∞, 0]`). + +use crate::cbig::CBig; +use crate::repr::{combine_parts, exact, CfpResult, Context}; +use dashu_base::Sign; +use dashu_float::round::Round; +use dashu_float::{FBig, Repr}; +use dashu_int::Word; + +/// Guard digits (base-B) for `sqrt`. Composes `hypot` + two real `sqrt`s + adds; a modest fixed +/// guard absorbs the accumulated rounding. +const SQRT_GUARD: usize = 12; + +/// A signed-infinity [`Repr`] (the public-API stand-in for the private `infinity_with_sign`). +fn signed_inf(sign: Sign) -> Repr { + match sign { + Sign::Positive => Repr::infinity(), + Sign::Negative => Repr::neg_infinity(), + } +} + +impl Context { + /// Principal square root of a complex number (context layer). + /// + /// The result has non-negative real part; when the real part is zero the imaginary part is + /// non-negative. The branch cut lies on `]−∞, 0]`; `sqrt(conj z) == conj(sqrt z)` holds, which + /// signed zero makes continuous across the cut. + pub fn sqrt(&self, z: &CBig) -> CfpResult { + if let Some(special) = sqrt_special(z, *self) { + return special; + } + + let gctx = self.guard(SQRT_GUARD); + let p = self.precision(); + let two = FBig::from_repr(Repr::new(2.into(), 0), gctx); + let x = z.re(); + let y = z.im(); + + // r = |z| (overflow-safe). Use the cancellation-free form: for x ≥ 0 compute `a` from + // `(r+x)/2` (large) and `b = y/(2a)`; for x < 0 compute `b` from `(r-x)/2` (large) and + // `a = y/(2b)`. This avoids subtracting nearly-equal magnitudes when |y| ≪ |x|. + let r = gctx.hypot(x, y)?.value(); + let (a, b) = if x.sign() != Sign::Negative { + // x ≥ 0 + let rpx = gctx.add(r.repr(), x)?.value(); + let half_rpx = gctx.div(rpx.repr(), two.repr())?.value(); + let a = gctx.sqrt(half_rpx.repr())?.value(); + let two_a = gctx.mul(two.repr(), a.repr())?.value(); + let b = gctx.div(y, two_a.repr())?.value(); + (a, b) + } else { + // x < 0: b carries the sign of y + let rmx = gctx.sub(r.repr(), x)?.value(); // r − x = r + |x| + let half_rmx = gctx.div(rmx.repr(), two.repr())?.value(); + let b_mag = gctx.sqrt(half_rmx.repr())?.value(); + let b = if y.sign() == Sign::Negative { + -b_mag + } else { + b_mag + }; + let two_b = gctx.mul(two.repr(), b.repr())?.value(); + let a = gctx.div(y, two_b.repr())?.value(); + (a, b) + }; + let re = a.with_precision(p); + let im = b.with_precision(p); + Ok(combine_parts(re, im)) + } +} + +impl CBig { + /// Principal square root (convenience layer). + /// + /// # Panics + /// + /// Panics if the precision is unlimited, or on an out-of-domain / indeterminate special value. + #[inline] + pub fn sqrt(&self) -> Self { + self.context().unwrap_cfp(self.context().sqrt(self)) + } +} + +/// Annex G `csqrt` special-value table (the subset expressible without NaN). +fn sqrt_special( + z: &CBig, + ctx: Context, +) -> Option> { + let f = ctx.float(); + // sqrt(±0 + i·0) = ±0 + i·0 (preserve the real sign of zero) + if z.is_zero() { + return Some(Ok(exact( + FBig::from_repr(z.re().clone(), f), + FBig::from_repr(z.im().clone(), f), + ))); + } + if !z.is_infinite() { + return None; + } + + let x_pos_inf = z.re().is_infinite() && z.re().sign() == Sign::Positive; + let x_neg_inf = z.re().is_infinite() && z.re().sign() == Sign::Negative; + let y_sign = z.im().sign(); + + let (re, im) = if x_pos_inf { + // sqrt(+inf + iy) = +inf + i·0 (the zero carries the sign of y) + ( + Repr::infinity(), + if y_sign == Sign::Negative { + Repr::neg_zero() + } else { + Repr::zero() + }, + ) + } else if x_neg_inf { + // sqrt(-inf + iy) = +0 + i·sign(y)·inf + (Repr::zero(), signed_inf::(y_sign)) + } else { + // y infinite, x finite: sqrt(x ± i·inf) = +inf ± i·inf + (Repr::infinity(), signed_inf::(y_sign)) + }; + Some(Ok(exact(FBig::from_repr(re, f), FBig::from_repr(im, f)))) +} + +#[cfg(test)] +mod tests { + use super::*; + use dashu_float::round::mode; + + type C = CBig; + type F = FBig; + + fn c(re: i32, im: i32) -> C { + let mk = |v: i32| -> F { F::from(v).with_precision(53).value() }; + CBig::from_parts(mk(re), mk(im)) + } + + #[test] + fn sqrt_basic() { + // sqrt(3+4i) = 2+i (since (2+i)² = 3+4i) + let z = c(3, 4); + let s = z.sqrt(); + let chk = &s * &s; + assert!(chk == z); + } + + #[test] + fn sqrt_real() { + // sqrt(9+0i) = 3+0i + let z = c(9, 0); + let s = z.sqrt(); + assert!(s == c(3, 0)); + } + + #[test] + fn sqrt_negative_real_is_imaginary() { + // sqrt(-4+0i) = 0+2i + let z = c(-4, 0); + let s = z.sqrt(); + assert!(s.re().significand().is_zero()); + assert_eq!(s.im().significand(), &2.into()); + } + + #[test] + fn sqrt_conj_identity() { + // sqrt(conj z) == conj(sqrt z) + let z = c(3, 4); + let lhs = z.conj().sqrt(); + let rhs = z.sqrt().conj(); + assert!(lhs == rhs); + } + + #[test] + fn sqrt_zero() { + let s = C::ZERO.sqrt(); + assert!(s.is_zero()); + } + + #[test] + fn sqrt_pos_infinity() { + let inf = CBig::from(F::INFINITY); + let s = inf.sqrt(); + assert!(s.re().is_infinite()); + assert!(s.im().is_zero()); + } +} diff --git a/complex/src/third_party/mod.rs b/complex/src/third_party/mod.rs new file mode 100644 index 00000000..7a0b25e1 --- /dev/null +++ b/complex/src/third_party/mod.rs @@ -0,0 +1,21 @@ +//! Third-party trait implementations (feature-gated). + +#[cfg(feature = "num-complex")] +mod num_complex; + +#[cfg(feature = "num-order")] +mod num_order; + +// Version-agnostic `UniformCBig` distribution + per-version `Distribution` glue (the `rand` +// feature aliases `rand_v08`; `rand_v09`/`rand_v010` are opt-in). +#[cfg(any(feature = "rand_v08", feature = "rand_v09", feature = "rand_v010"))] +pub mod rand; + +#[cfg(feature = "rand_v08")] +mod rand_v08; + +#[cfg(feature = "rand_v09")] +mod rand_v09; + +#[cfg(feature = "rand_v010")] +mod rand_v010; diff --git a/complex/src/third_party/num_complex.rs b/complex/src/third_party/num_complex.rs new file mode 100644 index 00000000..881d7abe --- /dev/null +++ b/complex/src/third_party/num_complex.rs @@ -0,0 +1,115 @@ +//! Conversions between [`CBig`] and `num-complex`'s `Complex`/`Complex` (behind the +//! `num-complex` feature). +//! +//! Mirroring `dashu-float`'s primitive-float conversions, both directions are restricted to base 2 +//! and compose through [`FBig`]: a `Complex` splits into two `f64`s, each lifted to an exact +//! base-2 [`FBig`] (NaN → [`ConversionError::OutOfBounds`]; infinities and signed zeros preserved); +//! a base-2 [`CBig`] rounds each part back to `f32`/`f64`, erroring on overflow or inexactness. The +//! pair is therefore the exact-into / rounding-out-of split that [`FBig`] uses for `f64`. + +use crate::cbig::CBig; +use dashu_base::ConversionError; +use dashu_float::round::Round; +use dashu_float::FBig; +use num_complex_v04::{Complex32, Complex64}; + +macro_rules! impl_complex_conversions { + ($cx:ty, $f:ty) => { + impl TryFrom<$cx> for CBig { + type Error = ConversionError; + + /// Lift a primitive-float `Complex` to an exact base-2 [`CBig`]. A `NaN` part is + /// unmappable (`CBig` has no NaN) and yields [`ConversionError::OutOfBounds`]; + /// infinities and signed zeros are preserved, exactly as for `FBig: TryFrom`. + #[inline] + fn try_from(c: $cx) -> Result { + let re = FBig::try_from(c.re)?; + let im = FBig::try_from(c.im)?; + Ok(CBig::from_parts(re, im)) + } + } + + impl TryFrom> for $cx { + type Error = ConversionError; + + /// Round a base-2 [`CBig`] back to a primitive-float `Complex`, composing + /// [`CBig`] → [`FBig`] → `f32`/`f64` per part. Errors on overflow or inexactness, and + /// also when a part is already infinite — mirroring `FBig: TryFrom for f64`. + #[inline] + fn try_from(z: CBig) -> Result { + let fctx = z.context.float(); + let re = <$f>::try_from(FBig::from_repr(z.re, fctx))?; + let im = <$f>::try_from(FBig::from_repr(z.im, fctx))?; + Ok(<$cx>::new(re, im)) + } + } + }; +} + +impl_complex_conversions!(Complex32, f32); +impl_complex_conversions!(Complex64, f64); + +#[cfg(test)] +mod tests { + use super::*; + use dashu_float::round::mode; + + type C2 = CBig; + + #[test] + fn f64_roundtrip() { + for (re, im) in [ + (3.0_f64, 4.0), + (1.0, 0.0), + (0.0, 1.0), + (-2.0, 0.5), + (0.0, 0.0), + (1.5, -2.25), + ] { + let c = Complex64::new(re, im); + let z = C2::try_from(c).unwrap(); + let back: Complex64 = z.try_into().unwrap(); + assert_eq!(back, c, "roundtrip failed for {re}+{im}i"); + } + } + + #[test] + fn f32_roundtrip() { + for (re, im) in [(3.0_f32, 4.0), (-1.5_f32, 0.25)] { + let c = Complex32::new(re, im); + let z = C2::try_from(c).unwrap(); + let back: Complex32 = z.try_into().unwrap(); + assert_eq!(back, c); + } + } + + #[test] + fn nan_is_out_of_bounds() { + assert_eq!(C2::try_from(Complex64::new(f64::NAN, 0.0)), Err(ConversionError::OutOfBounds)); + assert_eq!(C2::try_from(Complex64::new(0.0, f64::NAN)), Err(ConversionError::OutOfBounds)); + } + + #[test] + fn infinities_preserved_on_lift() { + let z = C2::try_from(Complex64::new(f64::INFINITY, f64::NEG_INFINITY)).unwrap(); + assert!(z.re().is_infinite()); + assert!(z.im().is_infinite()); + // an infinite part can't round-trip back to f64 (mirrors FBig) + assert_eq!(Complex64::try_from(z), Err(ConversionError::LossOfPrecision)); + } + + #[test] + fn signed_zero_preserved() { + let z = C2::try_from(Complex64::new(-0.0, 0.0)).unwrap(); + assert!(z.re().is_neg_zero()); + assert!(z.im().is_zero()); + } + + #[test] + fn high_precision_rounds_inexactly() { + // 2^53 + 1 needs 54 mantissa bits, so it can't convert back to f64 exactly + let big = FBig::::from_parts(((1u64 << 53) + 1).into(), 0); + let z = C2::from_parts(big, FBig::from(0)); + assert_eq!(Complex64::try_from(z), Err(ConversionError::LossOfPrecision)); + } +} diff --git a/complex/src/third_party/num_order.rs b/complex/src/third_party/num_order.rs new file mode 100644 index 00000000..bf023b45 --- /dev/null +++ b/complex/src/third_party/num_order.rs @@ -0,0 +1,132 @@ +//! `NumOrd` / `NumHash` for [`CBig`] (behind the `num-order` feature), mirroring `FBig`'s surface. +//! +//! `NumOrd` agrees with the lexicographic [`Ord`](crate::cbig::CBig) (by real, then imaginary). +//! `NumHash` mirrors the `num-order` crate's `Complex`/`Complex` hashing: the number is +//! treated as `a + b·i` and the per-part residues are combined algebraically into a single field +//! element `a + bterm` (where `bterm = ∓PROOT²·b²`, sign of `b`), rather than hashing the parts +//! sequentially. This keeps a `CBig` and a `num-complex` `Complex` of the same value in sync. +//! Consistency relies on `dashu-float`'s `Repr` residue equalling num-order's `f64` `fhash` for the +//! same finite value. + +use crate::cbig::CBig; +use crate::cmp::lex_cmp; +use _num_modular::{FixedMersenneInt, ModularInteger}; +use core::cmp::Ordering; +use core::hash::Hasher; +use dashu_float::round::Round; +use dashu_int::Word; +use num_order::{NumHash, NumOrd}; + +/// The *bterm* in num-order's `Complex` NumHash: `∓PROOT²·b²` (sign of `b`). +#[inline] +fn bterm(b: i128) -> i128 { + type MInt = FixedMersenneInt<127, 1>; + const M127U: u128 = i128::MAX as u128; + const PROOT: u128 = i32::MAX as u128; + + if b >= 0 { + let pb = MInt::new(b as u128, &M127U) * PROOT; + -((pb * pb).residue() as i128) + } else { + let pb = MInt::new((-b) as u128, &M127U) * PROOT; + (pb * pb).residue() as i128 + } +} + +impl NumOrd> for CBig { + #[inline] + fn num_cmp(&self, other: &CBig) -> Ordering { + lex_cmp(&self.re, &self.im, &other.re, &other.im) + } + + #[inline] + fn num_partial_cmp(&self, other: &CBig) -> Option { + Some(self.num_cmp(other)) + } +} + +impl NumHash for CBig { + fn num_hash(&self, state: &mut H) { + // Mirror num-order's `Complex` NumHash: z = a + b·i, hash(a + bterm). + let a = self.re().num_hash_residue(); + let b = self.im().num_hash_residue(); + a.wrapping_add(bterm(b)).num_hash(state) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use dashu_float::round::mode; + + type C = CBig; + + #[test] + fn num_ord_agrees_with_ord() { + let a = C::from_parts(1.into(), 9.into()); + let b = C::from_parts(2.into(), 0.into()); + assert_eq!(a.num_cmp(&b), a.cmp(&b)); + assert!(a.num_lt(&b)); + } + + #[test] + fn num_hash_consistent_with_eq() { + fn hash_of(v: &T) -> u64 { + use std::hash::DefaultHasher; + let mut h = DefaultHasher::new(); + v.num_hash(&mut h); + std::hash::Hasher::finish(&h) + } + let a = C::from_parts(3.into(), 4.into()); + let b = C::from_parts(3.into(), 4.into()); + assert_eq!(hash_of(&a), hash_of(&b)); + } + + /// i128 residue a `NumHash` impl writes (captured via `Hasher::write_i128`). + fn residue(v: &T) -> i128 { + struct Collector(i128); + impl core::hash::Hasher for Collector { + fn write_i128(&mut self, v: i128) { + self.0 = v; + } + fn write(&mut self, _: &[u8]) {} + fn finish(&self) -> u64 { + 0 + } + } + let mut c = Collector(0); + v.num_hash(&mut c); + c.0 + } + + #[test] + fn cbig_num_hash_matches_num_complex() { + // The base-2 CBig residue must equal num-order's `Complex` hash. Uses + // `num_complex::Complex64` as the live reference — it delegates to num-order's actual + // `NumHash` implementation (not a manually-transcribed formula), so if num-order ever + // changes the hash algorithm the test automatically tracks it. + // Relies on dashu-float's Repr residue equalling f64's `fhash` — see float's + // `test_fbig_num_hash_matches_f64`. + use dashu_float::FBig; + use num_complex_v04::Complex64; + type CF = CBig; + + for (re, im) in [ + (3.0_f64, 4.0), + (1.0, 0.0), + (0.0, 1.0), + (-2.0, 0.5), + (0.0, 0.0), + (1.5, -2.25), + (100.0, -0.0625), + ] { + let z = CF::from_parts(FBig::try_from(re).unwrap(), FBig::try_from(im).unwrap()); + let expected = residue(&Complex64::new(re, im)); + assert_eq!( + residue(&z), + expected, + "CBig num_hash disagrees with num-complex hash for {re}+{im}i" + ); + } + } +} diff --git a/complex/src/third_party/rand.rs b/complex/src/third_party/rand.rs new file mode 100644 index 00000000..8cea801d --- /dev/null +++ b/complex/src/third_party/rand.rs @@ -0,0 +1,106 @@ +//! Random complex number generation with the `rand` crate. +//! +//! [`UniformCBig`] samples a complex number with each part uniform in a per-part range +//! `[low, high)` — i.e. uniformly over the **box** `[low.re, high.re) × [low.im, high.im)`. The +//! builtin rand distributions (`Standard`/`StandardUniform`, `Open01`, `OpenClosed01`) generate a +//! complex number with each part uniform in `[0, 1)` — the **unit square** `[0, 1)²` — at inline +//! precision (each part's significand fits in a `DoubleWord`). +//! +//! The distribution is defined here once, generic over [`dashu_int::rand::BitRng`], and reuses +//! [`dashu_float::rand::UniformFBig`] for each part (a random `CBig` is just two independent +//! random `FBig` parts). Each rand version's `Distribution` impls live in the `rand_v08` / +//! `rand_v09` / `rand_v010` modules; enable the matching feature and adapt that version's RNG with +//! `dashu_int::rand::bridge_v08` / `bridge_v09` / `bridge_v010`. + +use crate::cbig::CBig; +use dashu_float::rand::{Uniform01 as FloatUniform01, UniformFBig}; +use dashu_float::round::Round; +use dashu_float::FBig; +use dashu_int::rand::BitRng; +use dashu_int::Word; + +/// Uniform distribution over the box `[low, high)`: the real part is uniform in `[low.re, high.re)` +/// and the imaginary part in `[low.im, high.im)`, each at a chosen precision. +/// +/// There is no single-axis `Uniform`/`SampleUniform` for `CBig` — complex numbers have no interval +/// order. Use this box sampler, or compose two [`UniformFBig`] ranges via [`CBig::from_parts`]. +pub struct UniformCBig { + pub(crate) re: UniformFBig, + pub(crate) im: UniformFBig, +} + +impl UniformCBig { + /// Create a sampler over the box `[low, high)` at `precision` (the two parts are sampled + /// independently; each part's range is `[low.part, high.part)`). + /// + /// # Panics + /// + /// Panics if `low.re > high.re` or `low.im > high.im`. + pub fn new(low: &CBig, high: &CBig, precision: usize) -> Self { + Self { + re: UniformFBig::new(&part_view(low, true), &part_view(high, true), precision), + im: UniformFBig::new(&part_view(low, false), &part_view(high, false), precision), + } + } + + /// Draw a random [`CBig`] from this sampler's box. + pub fn sample_cbig(&self, rng: &mut BR) -> CBig { + let re = self.re.sample_fbig(rng); + let im = self.im.sample_fbig(rng); + CBig::from_parts(re, im) + } +} + +/// Borrow one part of a [`CBig`] as an [`FBig`] view (clones the `Repr`, attaches the shared +/// context). Used only to feed [`UniformFBig::new`], which reads the value and precision. +fn part_view(z: &CBig, re: bool) -> FBig { + let ctx = z.context().float(); + let repr = if re { z.re().clone() } else { z.im().clone() }; + FBig::from_repr(repr, ctx) +} + +/// Uniform distribution over the unit square `(0, 1)²` — each part independent and uniform in +/// `(0, 1)`, at the chosen precision (mirroring [`dashu_float::rand::Uniform01`]). +/// +/// Used by the builtin `rand` distribution impls (`Standard`, `Open01`, `OpenClosed01`) for +/// [`CBig`]; can also be constructed directly for custom-precision sampling. +pub struct Uniform01 { + pub(crate) re: FloatUniform01, + pub(crate) im: FloatUniform01, +} + +impl Uniform01 { + /// Create a uniform distribution in `[0, 1)²` at the given precision. + #[inline] + pub fn new(precision: usize) -> Self { + Self { + re: FloatUniform01::new(precision), + im: FloatUniform01::new(precision), + } + } + + /// Create a uniform distribution in `[0, 1]²` at the given precision. + #[inline] + pub fn new_closed(precision: usize) -> Self { + Self { + re: FloatUniform01::new_closed(precision), + im: FloatUniform01::new_closed(precision), + } + } + + /// Create a uniform distribution in `(0, 1)²` at the given precision. + #[inline] + pub fn new_open(precision: usize) -> Self { + Self { + re: FloatUniform01::new_open(precision), + im: FloatUniform01::new_open(precision), + } + } + + /// Draw a random [`CBig`] with both parts in this sampler's interval. + pub fn sample_cbig(&self, rng: &mut BR) -> CBig { + let re = self.re.sample01(rng); + let im = self.im.sample01(rng); + CBig::from_parts(re, im) + } +} diff --git a/complex/src/third_party/rand_v010.rs b/complex/src/third_party/rand_v010.rs new file mode 100644 index 00000000..4799de4d --- /dev/null +++ b/complex/src/third_party/rand_v010.rs @@ -0,0 +1,48 @@ +//! `rand` 0.10 `Distribution` impls for `CBig` (enable the `rand_v010` feature). + +use crate::cbig::CBig; +use crate::third_party::rand::UniformCBig; +use dashu_float::round::Round; +use dashu_float::FBig; +use dashu_int::Word; +use rand_v010::distr::{Distribution, Open01, OpenClosed01, StandardUniform}; +use rand_v010::Rng; + +fn bridge(rng: &mut R) -> impl dashu_int::rand::BitRng + '_ { + dashu_int::rand::bridge_v010(rng) +} + +impl Distribution> for UniformCBig { + #[inline] + fn sample(&self, rng: &mut RNG) -> CBig { + self.sample_cbig(&mut bridge(rng)) + } +} + +impl Distribution> for StandardUniform { + /// Each part uniform in `[0, 1)` → the unit square `[0, 1)²`, at inline precision. + #[inline] + fn sample(&self, rng: &mut RNG) -> CBig { + let re: FBig = StandardUniform.sample(rng); + let im: FBig = StandardUniform.sample(rng); + CBig::from_parts(re, im) + } +} + +impl Distribution> for Open01 { + #[inline] + fn sample(&self, rng: &mut RNG) -> CBig { + let re: FBig = Open01.sample(rng); + let im: FBig = Open01.sample(rng); + CBig::from_parts(re, im) + } +} + +impl Distribution> for OpenClosed01 { + #[inline] + fn sample(&self, rng: &mut RNG) -> CBig { + let re: FBig = OpenClosed01.sample(rng); + let im: FBig = OpenClosed01.sample(rng); + CBig::from_parts(re, im) + } +} diff --git a/complex/src/third_party/rand_v08.rs b/complex/src/third_party/rand_v08.rs new file mode 100644 index 00000000..fc300f6b --- /dev/null +++ b/complex/src/third_party/rand_v08.rs @@ -0,0 +1,49 @@ +//! `rand` 0.8 `Distribution` impls for `CBig` (the `rand` feature aliases `rand_v08`). + +use crate::cbig::CBig; +use crate::third_party::rand::UniformCBig; +use dashu_float::round::Round; +use dashu_float::FBig; +use dashu_int::Word; +use rand_v08::distributions::{Open01, OpenClosed01, Standard}; +use rand_v08::prelude::Distribution; +use rand_v08::Rng; + +fn bridge(rng: &mut R) -> impl dashu_int::rand::BitRng + '_ { + dashu_int::rand::bridge_v08(rng) +} + +impl Distribution> for UniformCBig { + #[inline] + fn sample(&self, rng: &mut RNG) -> CBig { + self.sample_cbig(&mut bridge(rng)) + } +} + +impl Distribution> for Standard { + /// Each part uniform in `[0, 1)` → the unit square `[0, 1)²`, at inline precision. + #[inline] + fn sample(&self, rng: &mut RNG) -> CBig { + let re: FBig = Standard.sample(rng); + let im: FBig = Standard.sample(rng); + CBig::from_parts(re, im) + } +} + +impl Distribution> for Open01 { + #[inline] + fn sample(&self, rng: &mut RNG) -> CBig { + let re: FBig = Open01.sample(rng); + let im: FBig = Open01.sample(rng); + CBig::from_parts(re, im) + } +} + +impl Distribution> for OpenClosed01 { + #[inline] + fn sample(&self, rng: &mut RNG) -> CBig { + let re: FBig = OpenClosed01.sample(rng); + let im: FBig = OpenClosed01.sample(rng); + CBig::from_parts(re, im) + } +} diff --git a/complex/src/third_party/rand_v09.rs b/complex/src/third_party/rand_v09.rs new file mode 100644 index 00000000..f9a5a47d --- /dev/null +++ b/complex/src/third_party/rand_v09.rs @@ -0,0 +1,48 @@ +//! `rand` 0.9 `Distribution` impls for `CBig` (enable the `rand_v09` feature). + +use crate::cbig::CBig; +use crate::third_party::rand::UniformCBig; +use dashu_float::round::Round; +use dashu_float::FBig; +use dashu_int::Word; +use rand_v09::distr::{Distribution, Open01, OpenClosed01, StandardUniform}; +use rand_v09::Rng; + +fn bridge(rng: &mut R) -> impl dashu_int::rand::BitRng + '_ { + dashu_int::rand::bridge_v09(rng) +} + +impl Distribution> for UniformCBig { + #[inline] + fn sample(&self, rng: &mut RNG) -> CBig { + self.sample_cbig(&mut bridge(rng)) + } +} + +impl Distribution> for StandardUniform { + /// Each part uniform in `[0, 1)` → the unit square `[0, 1)²`, at inline precision. + #[inline] + fn sample(&self, rng: &mut RNG) -> CBig { + let re: FBig = StandardUniform.sample(rng); + let im: FBig = StandardUniform.sample(rng); + CBig::from_parts(re, im) + } +} + +impl Distribution> for Open01 { + #[inline] + fn sample(&self, rng: &mut RNG) -> CBig { + let re: FBig = Open01.sample(rng); + let im: FBig = Open01.sample(rng); + CBig::from_parts(re, im) + } +} + +impl Distribution> for OpenClosed01 { + #[inline] + fn sample(&self, rng: &mut RNG) -> CBig { + let re: FBig = OpenClosed01.sample(rng); + let im: FBig = OpenClosed01.sample(rng); + CBig::from_parts(re, im) + } +} diff --git a/complex/tests/arith_prop.rs b/complex/tests/arith_prop.rs new file mode 100644 index 00000000..f5b1c6d9 --- /dev/null +++ b/complex/tests/arith_prop.rs @@ -0,0 +1,82 @@ +//! Arithmetic identity property tests (exact identities for finite operands). +//! +//! Tolerance-based correctness (the self-oracle) lives in `rounding_prop.rs`. + +use dashu_cmplx::{CBig, FBig}; +use dashu_float::round::mode::HalfEven; +use proptest::prelude::*; + +type C = CBig; +type F = FBig; + +const P: usize = 53; + +fn fbig_strategy() -> impl Strategy { + (-(1i64 << 20)..(1i64 << 20), -10isize..10isize).prop_map(|(sig, exp)| { + if sig == 0 { + F::ZERO.with_precision(P).value() + } else { + F::from_parts(sig.into(), exp).with_precision(P).value() + } + }) +} + +fn cbig_strategy() -> impl Strategy { + (fbig_strategy(), fbig_strategy()).prop_map(|(re, im)| CBig::from_parts(re, im)) +} + +proptest! { + #[test] + fn add_commutes((z, w) in (cbig_strategy(), cbig_strategy())) { + prop_assert!(&z + &w == &w + &z); + } + + #[test] + fn add_zero_identity(z in cbig_strategy()) { + let zero = CBig::from(F::ZERO); + prop_assert!(&z + &zero == z); + } + + #[test] + fn sub_self_is_zero(z in cbig_strategy()) { + prop_assert!((&z - &z).is_zero()); + } + + #[test] + fn mul_commutes((z, w) in (cbig_strategy(), cbig_strategy())) { + // the 4-mul formula is symmetric, so z·w and w·z round identically + prop_assert!(&z * &w == &w * &z); + } + + #[test] + fn mul_one_identity(z in cbig_strategy()) { + prop_assert!(&z * &CBig::ONE == z); + } + + #[test] + fn mul_zero_is_zero(z in cbig_strategy()) { + let zero = CBig::from(F::ZERO); + prop_assert!((&z * &zero).is_zero()); + } + + #[test] + fn mul_i_fourth_is_identity(z in cbig_strategy()) { + let id = z.mul_i(false).mul_i(false).mul_i(false).mul_i(false); + prop_assert!(id == z); + } + + #[test] + fn conj_involution(z in cbig_strategy()) { + prop_assert!(z.conj().conj() == z); + } + + #[test] + fn proj_idempotent_finite(z in cbig_strategy()) { + prop_assert!(z.proj().proj() == z.proj()); + } + + #[test] + fn neg_is_additive_inverse(z in cbig_strategy()) { + prop_assert!((&z + &(-&z)).is_zero()); + } +} diff --git a/complex/tests/random.rs b/complex/tests/random.rs new file mode 100644 index 00000000..86505c12 --- /dev/null +++ b/complex/tests/random.rs @@ -0,0 +1,49 @@ +//! Tests for the `rand` integration: the `Standard` distribution (unit square `[0,1)²`) and the +//! `UniformCBig` box sampler. Uses `rand_v08` (the `rand` feature default). + +use dashu_cmplx::rand::UniformCBig; +use dashu_cmplx::CBig; +use dashu_float::round::mode::HalfEven; +use rand_v08::distributions::Distribution; +use rand_v08::{rngs::StdRng, Rng, SeedableRng}; + +type C = CBig; +type F = dashu_float::FBig; + +#[test] +fn standard_is_in_unit_square() { + let mut rng = StdRng::seed_from_u64(1); + for _ in 0..1024 { + let z: C = rng.gen(); + let (re, im) = z.into_parts(); + // each part uniform in [0, 1) — the unit square + assert!(re >= F::ZERO && re < F::ONE, "real part {re:?} outside [0,1)"); + assert!(im >= F::ZERO && im < F::ONE, "imag part {im:?} outside [0,1)"); + } +} + +#[test] +fn uniform_cbig_box() { + let mut rng = StdRng::seed_from_u64(7); + let low = C::from_parts(F::from(2), F::from(-3)); + let high = C::from_parts(F::from(5), F::from(7)); + let dist = UniformCBig::new(&low, &high, 53); + for _ in 0..1024 { + let z = dist.sample(&mut rng); + let (re, im) = z.into_parts(); + // re ∈ [2, 5), im ∈ [-3, 7) + assert!(re >= F::from(2) && re < F::from(5), "real part {re:?} outside [2,5)"); + assert!(im >= F::from(-3) && im < F::from(7), "imag part {im:?} outside [-3,7)"); + } +} + +#[test] +fn open01_excludes_zero() { + use rand_v08::distributions::Open01; + let mut rng = StdRng::seed_from_u64(3); + for _ in 0..256 { + let z: C = rng.sample(Open01); + let (re, im) = z.into_parts(); + assert!(re > F::ZERO && im > F::ZERO, "Open01 produced a zero part"); + } +} diff --git a/complex/tests/rounding_prop.rs b/complex/tests/rounding_prop.rs new file mode 100644 index 00000000..80763d7a --- /dev/null +++ b/complex/tests/rounding_prop.rs @@ -0,0 +1,94 @@ +//! Correct-rounding self-oracle: each op computed at precision `p` is recomputed at `2p` and +//! re-rounded to `p`; the two must agree to within 1 ulp per component (the near-correctly-rounded +//! guarantee class). Also covers the approximate algebraic identities. + +use dashu_base::{Abs, AbsOrd}; +use dashu_cmplx::{CBig, Context, FBig}; +use dashu_float::round::mode::HalfEven; +use proptest::prelude::*; + +type C = CBig; +type F = FBig; + +const P: usize = 53; + +fn fbig_strategy() -> impl Strategy { + // keep magnitudes modest so div denominators stay well-conditioned + (1i64..(1i64 << 20), -8isize..8isize) + .prop_map(|(sig, exp)| F::from_parts(sig.into(), exp).with_precision(P).value()) +} + +fn cbig_strategy() -> impl Strategy { + (fbig_strategy(), fbig_strategy()).prop_map(|(re, im)| CBig::from_parts(re, im)) +} + +/// True when `a` and `b` agree to within `k` ulps of `a` (both must have limited precision). +fn within_ulps(a: &F, b: &F, k: u32) -> bool { + if a == b { + return true; + } + let diff = (a.clone() - b.clone()).abs(); + let bound = a.ulp() * F::from(k); + diff.abs_cmp(&bound).is_le() +} + +fn within_ulps_cbig(a: &C, b: &C, k: u32) -> bool { + let (ar, ai) = a.clone().into_parts(); + let (br, bi) = b.clone().into_parts(); + within_ulps(&ar, &br, k) && within_ulps(&ai, &bi, k) +} + +/// Recompute a binary/unary op at `2p` and re-round each part back to `p`. +fn reround_hi(hi: C) -> C { + let (re, im) = hi.into_parts(); + CBig::from_parts(re.with_precision(P).value(), im.with_precision(P).value()) +} + +proptest! { + #[test] + fn mul_self_oracle((z, w) in (cbig_strategy(), cbig_strategy())) { + let lo = Context::new(P); + let hi = Context::new(2 * P); + let rp = lo.mul(&z, &w).unwrap().value(); + let r2 = reround_hi(hi.mul(&z, &w).unwrap().value()); + prop_assert!(within_ulps_cbig(&rp, &r2, 1)); + } + + #[test] + fn sqr_self_oracle(z in cbig_strategy()) { + let lo = Context::new(P); + let hi = Context::new(2 * P); + let rp = lo.sqr(&z).unwrap().value(); + let r2 = reround_hi(hi.sqr(&z).unwrap().value()); + prop_assert!(within_ulps_cbig(&rp, &r2, 1)); + } + + #[test] + fn div_self_oracle((z, w) in (cbig_strategy(), cbig_strategy())) { + let lo = Context::new(P); + let hi = Context::new(2 * P); + let rp = lo.div(&z, &w).unwrap().value(); + let r2 = reround_hi(hi.div(&z, &w).unwrap().value()); + prop_assert!(within_ulps_cbig(&rp, &r2, 1)); + } + + #[test] + fn abs_self_oracle(z in cbig_strategy()) { + // abs returns a real FBig; compare at p vs 2p re-rounded + let lo = Context::new(P); + let hi = Context::new(2 * P); + let rp = lo.abs(&z).unwrap().value(); + let r2 = hi.abs(&z).unwrap().value().with_precision(P).value(); + prop_assert!(within_ulps(&rp, &r2, 1)); + } + + #[test] + fn mul_conj_is_norm(z in cbig_strategy()) { + // z·conj(z) is purely real and equals norm(z) = |z|² + let p = &z * &z.conj(); + let (re, im) = p.into_parts(); + let norm = z.norm(); + prop_assert!(within_ulps(&im, &F::ZERO, 4)); + prop_assert!(within_ulps(&re, &norm, 4)); + } +} diff --git a/complex/tests/special_values.rs b/complex/tests/special_values.rs new file mode 100644 index 00000000..8415562f --- /dev/null +++ b/complex/tests/special_values.rs @@ -0,0 +1,215 @@ +//! Exact, deterministic Annex G / Kahan special-value vectors for the arithmetic ops (no proptest). +//! +//! These exercise the context-layer short-circuits: `0·∞` / `0/0` / `∞/∞` map to +//! [`FpError::Indeterminate`], `z/0`/`∞·finite` map to the Riemann point at infinity, and +//! `finite/∞` / `0/finite` map to zero. + +use dashu_base::Sign; +use dashu_cmplx::{CBig, Context, FBig, FpError}; +use dashu_float::round::mode::HalfEven; + +type C = CBig; +type F = FBig; + +fn ctx() -> Context { + Context::new(53) +} + +fn real(v: i64) -> C { + CBig::from(F::from(v)) +} + +fn inf() -> C { + CBig::from(F::INFINITY) +} + +fn is_riemann(r: &C) -> bool { + r.re().is_infinite() && r.re().sign() == Sign::Positive && r.im().is_zero() +} + +#[test] +fn mul_zero_infinity_is_indeterminate() { + assert_eq!(ctx().mul(&real(0), &inf()), Err(FpError::Indeterminate)); + assert_eq!(ctx().mul(&inf(), &real(0)), Err(FpError::Indeterminate)); +} + +#[test] +fn mul_infinity_infinity_is_riemann() { + let r = ctx().mul(&inf(), &inf()).unwrap().value(); + assert!(is_riemann(&r)); +} + +#[test] +fn mul_infinity_finite_is_riemann() { + let r = ctx().mul(&real(3), &inf()).unwrap().value(); + assert!(is_riemann(&r)); +} + +#[test] +fn div_zero_zero_is_indeterminate() { + assert_eq!(ctx().div(&real(0), &real(0)), Err(FpError::Indeterminate)); +} + +#[test] +fn div_inf_inf_is_indeterminate() { + assert_eq!(ctx().div(&inf(), &inf()), Err(FpError::Indeterminate)); +} + +#[test] +fn div_by_zero_is_riemann() { + let r = ctx().div(&real(3), &real(0)).unwrap().value(); + assert!(is_riemann(&r)); +} + +#[test] +fn div_inf_by_finite_is_riemann() { + let r = ctx().div(&inf(), &real(3)).unwrap().value(); + assert!(is_riemann(&r)); +} + +#[test] +fn div_finite_by_inf_is_zero() { + let r = ctx().div(&real(3), &inf()).unwrap().value(); + assert!(r.is_zero()); +} + +#[test] +fn div_zero_by_finite_is_zero() { + let r = ctx().div(&real(0), &real(3)).unwrap().value(); + assert!(r.is_zero()); +} + +#[test] +fn inv_zero_is_riemann() { + let r = ctx().inv(&real(0)).unwrap().value(); + assert!(is_riemann(&r)); +} + +#[test] +fn inv_inf_is_zero() { + let r = ctx().inv(&inf()).unwrap().value(); + assert!(r.is_zero()); +} + +#[test] +fn mul_context_inexactness_flags() { + // 2/3 · 3: exercises the CRounded path and its per-axis (Rounding, Rounding) flags. + use dashu_float::round::Rounding; + let two_thirds = F::from_parts(2.into(), -1).with_precision(53).value(); + let z = CBig::from(two_thirds); + let w = CBig::from(F::from(3)); + let r = ctx().mul(&z, &w).unwrap(); + let _: (Rounding, Rounding) = match r { + dashu_base::Approximation::Inexact(_, flags) => flags, + dashu_base::Approximation::Exact(_) => (Rounding::NoOp, Rounding::NoOp), + }; +} + +// --- sqrt / exp / log special values (M3) --- + +#[test] +fn sqrt_pos_infinity() { + let s = ctx().sqrt(&inf()).unwrap().value(); + assert!(is_riemann(&s)); +} + +#[test] +fn sqrt_zero_is_zero() { + let s = ctx().sqrt(&real(0)).unwrap().value(); + assert!(s.is_zero()); +} + +#[test] +fn exp_pos_infinity_is_riemann() { + let r = ctx().exp(&inf(), None).unwrap().value(); + assert!(is_riemann(&r)); +} + +#[test] +fn exp_neg_infinity_is_zero() { + let neg_inf = CBig::from(F::NEG_INFINITY); + let r = ctx().exp(&neg_inf, None).unwrap().value(); + assert!(r.is_zero()); +} + +#[test] +fn exp_imag_infinity_is_indeterminate() { + let im_inf = CBig::from_parts(F::ZERO, F::INFINITY); + assert_eq!(ctx().exp(&im_inf, None), Err(FpError::Indeterminate)); +} + +#[test] +fn log_zero_is_neg_infinity() { + let r = ctx().log(&real(0), None).unwrap().value(); + assert!(r.re().is_infinite()); + assert_eq!(r.re().sign(), Sign::Negative); +} + +#[test] +fn log_infinity_is_riemann() { + let r = ctx().log(&inf(), None).unwrap().value(); + assert!(is_riemann(&r)); +} + +// --- proj / conj / arg / signed-zero branch-cut specials (M5 hardening) --- + +#[test] +fn proj_infinity_is_riemann() { + // proj collapses any infinity to +∞ + i·0 + assert!(is_riemann(&ctx().proj(&inf()).unwrap().value())); + let im_inf = CBig::from_parts(F::ZERO, F::INFINITY); + assert!(is_riemann(&ctx().proj(&im_inf).unwrap().value())); +} + +#[test] +fn proj_finite_unchanged() { + let z = real(3); + let p = ctx().proj(&z).unwrap().value(); + assert!(p == z); +} + +#[test] +fn conj_infinity_flips_imag_sign() { + // conj(+inf + i·inf) = +inf - i·inf (the real part keeps its sign) + let z = CBig::from_parts(F::INFINITY, F::INFINITY); + let c = ctx().conj(&z).unwrap().value(); + assert!(c.re().is_infinite()); + assert!(c.im().is_infinite()); + assert_eq!(c.im().sign(), Sign::Negative); +} + +#[test] +fn arg_of_imaginary_infinity_is_half_pi() { + // arg(0 + i·inf) = π/2 > 0; arg(0 - i·inf) = -π/2 < 0 + let pos = CBig::from_parts(F::ZERO, F::INFINITY); + let neg = CBig::from_parts(F::ZERO, F::NEG_INFINITY); + assert!(ctx().arg(&pos, None).unwrap().value() > F::ZERO); + assert!(ctx().arg(&neg, None).unwrap().value() < F::ZERO); +} + +#[test] +fn log_negative_real_branch_cut() { + // log(-r ± i·0) = ln r ± i·π: the sign of the imaginary zero selects the side of the cut. + use dashu_float::{Context as FloatCtx, Repr}; + let f = FloatCtx::::new(53); + let neg_r = F::from(-4); + let pos_zero = CBig::from_parts(neg_r.clone(), F::from_repr(Repr::zero(), f)); + let neg_zero = CBig::from_parts(neg_r, F::from_repr(Repr::neg_zero(), f)); + + let (re_p, im_p) = ctx().log(&pos_zero, None).unwrap().value().into_parts(); + let (re_n, im_n) = ctx().log(&neg_zero, None).unwrap().value().into_parts(); + // both real parts = ln 4; imaginary parts are ±π + assert!(re_p == re_n); + assert!(im_p > F::ZERO); // +i·π + assert!(im_n < F::ZERO); // -i·π +} + +#[test] +fn sqrt_neg_infinity_is_imaginary_infinity() { + // sqrt(-inf + i·0) = +0 + i·inf + let neg_inf = CBig::from(F::NEG_INFINITY); + let s = ctx().sqrt(&neg_inf).unwrap().value(); + assert!(s.re().is_zero()); + assert!(s.im().is_infinite()); + assert_eq!(s.im().sign(), Sign::Positive); +} diff --git a/complex/tests/transcendental_prop.rs b/complex/tests/transcendental_prop.rs new file mode 100644 index 00000000..3030466f --- /dev/null +++ b/complex/tests/transcendental_prop.rs @@ -0,0 +1,143 @@ +//! Transcendental identity property tests for sqrt / exp / log (and the sqrt self-oracle). + +use dashu_base::{Abs, AbsOrd}; +use dashu_cmplx::{CBig, Context, FBig}; +use dashu_float::round::mode::HalfEven; +use proptest::prelude::*; + +type C = CBig; +type F = FBig; + +const P: usize = 53; + +fn fbig_strategy() -> impl Strategy { + // keep the real part non-negative so sqrt's principal branch is well away from the cut, and + // magnitudes modest + (1i64..(1i64 << 20), -6isize..6isize) + .prop_map(|(sig, exp)| F::from_parts(sig.into(), exp).with_precision(P).value()) +} + +fn cbig_strategy() -> impl Strategy { + (fbig_strategy(), fbig_strategy()).prop_map(|(re, im)| CBig::from_parts(re, im)) +} + +/// Modest-magnitude parts (≈ [0.25, 1.75], so `|re|,|im| < 2`): for the trig identities and +/// inverse-trig oracles. The pythagorean identity needs small `|im|` (else `cosh²y`/`sinh²y` +/// catastrophically cancel to 1), and `asin`'s `iz + sqrt(1-z²)` cancels for large `|z|`. +fn small_strategy() -> impl Strategy { + (1i64..8, 1i64..8).prop_map(|(re_num, im_num)| { + let re = F::from_parts(re_num.into(), -2).with_precision(P).value(); + let im = F::from_parts(im_num.into(), -2).with_precision(P).value(); + CBig::from_parts(re, im) + }) +} + +fn within_ulps(a: &F, b: &F, k: u32) -> bool { + if a == b { + return true; + } + let diff = (a.clone() - b.clone()).abs(); + diff.abs_cmp(&(a.ulp() * F::from(k))).is_le() +} + +fn within_ulps_cbig(a: &C, b: &C, k: u32) -> bool { + let (ar, ai) = a.clone().into_parts(); + let (br, bi) = b.clone().into_parts(); + within_ulps(&ar, &br, k) && within_ulps(&ai, &bi, k) +} + +fn reround_hi(hi: C) -> C { + let (re, im) = hi.into_parts(); + CBig::from_parts(re.with_precision(P).value(), im.with_precision(P).value()) +} + +proptest! { + #[test] + fn sqrt_conj_identity(z in cbig_strategy()) { + // sqrt(conj z) == conj(sqrt z): the magnitude path is identical, only the im sign differs + prop_assert!(z.conj().sqrt() == z.sqrt().conj()); + } + + #[test] + fn sqrt_self_oracle(z in cbig_strategy()) { + let lo = Context::new(P); + let hi = Context::new(2 * P); + let rp = lo.sqrt(&z).unwrap().value(); + let r2 = reround_hi(hi.sqrt(&z).unwrap().value()); + prop_assert!(within_ulps_cbig(&rp, &r2, 2)); + } + + #[test] + fn exp_self_oracle(z in cbig_strategy()) { + let lo = Context::new(P); + let hi = Context::new(2 * P); + let rp = lo.exp(&z, None).unwrap().value(); + let r2 = reround_hi(hi.exp(&z, None).unwrap().value()); + prop_assert!(within_ulps_cbig(&rp, &r2, 2)); + } + + #[test] + fn log_self_oracle(z in cbig_strategy()) { + let lo = Context::new(P); + let hi = Context::new(2 * P); + let rp = lo.log(&z, None).unwrap().value(); + let r2 = reround_hi(hi.log(&z, None).unwrap().value()); + prop_assert!(within_ulps_cbig(&rp, &r2, 2)); + } + + #[test] + fn log_imag_is_arg(z in cbig_strategy()) { + // the imaginary part of log z equals arg z ∈ ]-π, π]; the real part equals ln|z| + let (lr, li) = z.ln().into_parts(); + let arg = z.arg(); + prop_assert!(within_ulps(&li, &arg, 16)); + let abs = z.abs(); + let ln_abs = abs.ln(); + prop_assert!(within_ulps(&lr, &ln_abs, 16)); + } + + #[test] + fn sin_cos_self_oracle(z in cbig_strategy()) { + let lo = Context::new(P); + let hi = Context::new(2 * P); + let sp = lo.sin(&z, None).unwrap().value(); + let cp = lo.cos(&z, None).unwrap().value(); + let s2 = reround_hi(hi.sin(&z, None).unwrap().value()); + let c2 = reround_hi(hi.cos(&z, None).unwrap().value()); + prop_assert!(within_ulps_cbig(&sp, &s2, 2)); + prop_assert!(within_ulps_cbig(&cp, &c2, 2)); + } + + #[test] + fn pythagorean_identity(z in small_strategy()) { + // sin²z + cos²z = 1: the real part is ~1, the imaginary part is a small residual of O(1) + // terms, so compare it to ulp(1) (not its own tiny ulp). F::ONE has unlimited precision, so + // take the ulp from a precision-P one. + let s = z.sin(); + let c = z.cos(); + let sum = &s.sqr() + &c.sqr(); + let (re, im) = sum.into_parts(); + let one = F::ONE.with_precision(P).value(); + let tol = one.ulp() * F::from(16u32); + prop_assert!(im.abs_cmp(&tol).is_le()); + prop_assert!((re.clone() - F::ONE).abs_cmp(&tol).is_le()); + } + + #[test] + fn asin_self_oracle(z in small_strategy()) { + let lo = Context::new(P); + let hi = Context::new(2 * P); + let rp = lo.asin(&z, None).unwrap().value(); + let r2 = reround_hi(hi.asin(&z, None).unwrap().value()); + prop_assert!(within_ulps_cbig(&rp, &r2, 4)); + } + + #[test] + fn atan_self_oracle(z in small_strategy()) { + let lo = Context::new(P); + let hi = Context::new(2 * P); + let rp = lo.atan(&z, None).unwrap().value(); + let r2 = reround_hi(hi.atan(&z, None).unwrap().value()); + prop_assert!(within_ulps_cbig(&rp, &r2, 4)); + } +} diff --git a/float/CHANGELOG.md b/float/CHANGELOG.md index af0df9b1..ae0793ac 100644 --- a/float/CHANGELOG.md +++ b/float/CHANGELOG.md @@ -2,12 +2,35 @@ ## Unreleased +### Add +- `Repr::num_hash_residue` (behind `num-order`): the numeric-hash field element (mod 2¹²⁷−1) + used by `NumHash`, exposed so composite types (e.g. `CBig`) can combine their parts' residues + algebraically, matching the `num-order` crate's scheme. +- `FBig::hypot` / `Context::hypot`: `sqrt(a² + b²)` computed overflow/underflow-safe via the scaled + sum-of-squares (the larger operand is never squared). `hypot(±inf, ·) = +inf`, `hypot(0,0) = +0`. +- `FBig::sinh_cosh` / `Context::sinh_cosh`: simultaneously compute `sinh(x)` and `cosh(x)` sharing + the `exp_m1(±x)` sub-computations, roughly halving the cost of calling `sinh` + `cosh` separately. +- (test) The `Context::sin` many-digit-significand rounding regression (49 digits at precision 100) + is now CI-guarded as `test_sin_many_digit_rounding_no_panic`, promoted from the excluded `fuzz/` + crate; the `trig_prop` `pythagorean` identity now sweeps precisions {20, 50, 100}. + +### Fix +- Fixed broken intra-doc links surfaced by `cargo doc -D warnings`: `Exact`/`Inexact` now resolve to + `dashu_base::Approximation::{Exact,Inexact}`, `FpError::InfiniteInput` uses the crate path, and the + external `static_fbig!` macro reference (in a crate that isn't a dependency) is plain code. +- `FBig::from_repr`'s debug assertion now accepts the documented single guard digit (`precision + 1` + digits, as an inexact add/sub can produce); previously it rejected exactly-`precision+1` Reprs. + ### Remove - Public `Repr::from_str_native` / `FBig::from_str_native` methods (now crate-private). Use the `core::str::FromStr` impl (`s.parse()` / `FBig::from_str`) instead; its docs now carry the full parsing format specification. ### Change - **(breaking)** `FBig` human-readable serde now pads the serialized string with trailing zeros so its significant-digit count equals the context precision, letting precision round-trip (previously it was lost). The binary format already preserved precision. - (internal) The PostgreSQL `NUMERIC` conversion now extracts base-10000 digits via `UBig::to_digits` instead of a per-digit `div_rem` loop. +- (internal) Trig argument reduction (`reduce_to_quadrant`) now recovers the quadrant integer via `IBig::try_from` instead of `to_int()`, since the rounded value is already an exact integer. + +### Fix +- `IBig::try_from(FBig)` and `UBig::try_from(FBig)` now accept IEEE-754 signed zero (`-0`), returning `Ok(0)` instead of `Err(LossOfPrecision)`. Signed zero carries its sign in a `-1` exponent sentinel rather than the significand, so its integer value is plain `0`. ### Add - Hyperbolic functions `sinh`, `cosh`, `tanh` and their inverses `asinh`, `acosh`, `atanh` on diff --git a/float/src/convert.rs b/float/src/convert.rs index 6aa84bc5..671f573a 100644 --- a/float/src/convert.rs +++ b/float/src/convert.rs @@ -820,6 +820,13 @@ impl TryFrom> for IBig { fn try_from(value: FBig) -> Result { if value.repr.is_infinite() { Err(ConversionError::OutOfBounds) + } else if value.repr.significand.is_zero() { + // A zero significand is integer zero regardless of exponent. This also + // accepts IEEE-754 signed zero, whose sign is carried by a -1 exponent + // sentinel (not the significand); it is treated as plain 0. The zero + // must be handled here rather than in the `else` branch below, which + // shifts by `exponent as usize` and would underflow on the -1 sentinel. + Ok(value.repr.significand) } else if value.repr.exponent < 0 { Err(ConversionError::LossOfPrecision) } else { @@ -967,3 +974,32 @@ macro_rules! impl_from_fbig_for_float { } impl_from_fbig_for_float!(f32, to_f32); impl_from_fbig_for_float!(f64, to_f64); + +#[cfg(test)] +mod tests { + use super::*; + use crate::repr::Repr; + + #[test] + fn ibig_try_from_accepts_signed_zero() { + // IEEE-754 signed zero (sign encoded in a -1 exponent sentinel) is plain 0. + let neg_zero = FBig::::new(Repr::neg_zero(), Context::new(8)); + assert_eq!(IBig::try_from(neg_zero), Ok(IBig::from(0))); + + // positive zero already worked, and still does + let pos_zero = FBig::::new(Repr::zero(), Context::new(8)); + assert_eq!(IBig::try_from(pos_zero), Ok(IBig::from(0))); + + // UBig delegates to the IBig impl, so it accepts signed zero too + let neg_zero = FBig::::new(Repr::neg_zero(), Context::new(8)); + assert_eq!(UBig::try_from(neg_zero), Ok(UBig::from(0u8))); + + // a genuine fractional value must still be rejected + let frac = FBig::::new(Repr::new(IBig::from(1), -1), Context::new(8)); + assert_eq!(IBig::try_from(frac), Err(ConversionError::LossOfPrecision)); + + // a normal integer round-trips exactly + let int_val = FBig::::new(Repr::new(IBig::from(42), 0), Context::new(8)); + assert_eq!(IBig::try_from(int_val), Ok(IBig::from(42))); + } +} diff --git a/float/src/error.rs b/float/src/error.rs index cdf1f429..a98e9cb6 100644 --- a/float/src/error.rs +++ b/float/src/error.rs @@ -11,13 +11,13 @@ use core::fmt::{self, Display, Formatter}; /// # Errors vs. special values /// /// Infinite *outputs* (e.g. `1/0 → +inf`, `ln(0) → -inf`) are **not** errors — they are -/// legitimate [`Exact`] values produced by operations whose mathematical result is genuinely +/// legitimate [`Exact`](dashu_base::Approximation::Exact) values produced by operations whose mathematical result is genuinely /// infinite. Overflow and underflow are distinct: the mathematical result is finite, but its /// magnitude exceeds the representable exponent range. These are reported as /// [`Overflow`](FpError::Overflow) / [`Underflow`](FpError::Underflow), and converted to /// signed infinity / signed zero at the convenience layer via `Context::unwrap_fp` (or the /// `Repr`-level counterpart `Context::unwrap_fp_repr`). Because the true result was finite, -/// the converted value is always [`Inexact`] with `Rounding::NoOp`. +/// the converted value is always [`Inexact`](dashu_base::Approximation::Inexact) with `Rounding::NoOp`. /// /// The remaining variants ([`InfiniteInput`](FpError::InfiniteInput), /// [`OutOfDomain`](FpError::OutOfDomain), [`Indeterminate`](FpError::Indeterminate)) signal @@ -34,14 +34,14 @@ pub enum FpError { /// An indeterminate form, e.g. `0 / 0`. Only a *zero* divided by zero is /// indeterminate — a non-zero value divided by zero yields ±infinity, which is a - /// legitimate [`Exact`] value rather than an error. + /// legitimate [`Exact`](dashu_base::Approximation::Exact) value rather than an error. Indeterminate, /// The result magnitude is too large to represent as a finite number. /// /// At the `FBig` convenience layer this is converted to a signed infinity via /// `Context::unwrap_fp` (or to a signed [`Repr`] via `Context::unwrap_fp_repr`). - /// The converted result is always [`Inexact`]: the true result was a very large + /// The converted result is always [`Inexact`](dashu_base::Approximation::Inexact): the true result was a very large /// finite number, and infinity is an approximation. Overflow(Sign), @@ -49,7 +49,7 @@ pub enum FpError { /// /// At the `FBig` convenience layer this is converted to a signed zero via /// `Context::unwrap_fp` (or to a signed [`Repr`] via `Context::unwrap_fp_repr`). - /// The converted result is always [`Inexact`]: the true result was a very small + /// The converted result is always [`Inexact`](dashu_base::Approximation::Inexact): the true result was a very small /// non-zero number, and zero is an approximation. Underflow(Sign), } diff --git a/float/src/fbig.rs b/float/src/fbig.rs index 8efef36a..af0bd255 100644 --- a/float/src/fbig.rs +++ b/float/src/fbig.rs @@ -97,7 +97,7 @@ use dashu_int::{DoubleWord, IBig}; /// `+0` and `-0` compare equal. /// * Infinities are **terminal values**: they can be produced (e.g. `1 / 0 → +inf`, `ln(0) → -inf`, /// `exp(huge) → +inf`, `tan(π/2) → +inf`), compared, and printed, but feeding an infinity into a -/// further operation is an error at the [`Context`] layer ([`FpError::InfiniteInput`]) and panics +/// further operation is an error at the [`Context`] layer ([`FpError::InfiniteInput`](crate::FpError::InfiniteInput)) and panics /// at the [FBig] layer. This structurally avoids the IEEE indeterminate forms (`inf − inf`, `inf/inf`, /// `0·inf`). The only exceptions are `atan(±inf) = ±π/2` and the `atan2` signed-∞ quadrants, which /// have well-defined finite results. @@ -149,12 +149,12 @@ impl FBig { /// /// # Panics /// - /// Panics if the [Repr] has more digits than the precision limit specified in the context. - /// Note that this condition is not checked in release builds. + /// Panics if the [Repr] has more digits than `precision + 1` (the one allowed guard digit from + /// an inexact add/sub — see [`Repr`]). Note that this condition is not checked in release builds. #[inline] pub fn from_repr(repr: Repr, context: Context) -> Self { debug_assert!( - repr.is_infinite() || !context.is_limited() || repr.digits() <= context.precision + repr.is_infinite() || !context.is_limited() || repr.digits() <= context.precision + 1 ); Self { repr, context } } diff --git a/float/src/fbig_cached.rs b/float/src/fbig_cached.rs index 9c131003..9c009346 100644 --- a/float/src/fbig_cached.rs +++ b/float/src/fbig_cached.rs @@ -22,7 +22,7 @@ use crate::utils::digit_len; /// recomputing constants from scratch on every call. /// /// `Context`/`FBig` themselves stay `Copy` + `Send` + `Sync` + `no_std` (so -/// [`static_fbig!`](dashu_macros::static_fbig!) keeps working); only this cached +/// `static_fbig!` keeps working); only this cached /// wrapper is `!Send + !Sync`, because it shares state through an `Rc>`. /// To share one cache across threads, build an analogous type over /// `Arc>` instead (the [`Context`] methods accept diff --git a/float/src/fbig_cached_ops.rs b/float/src/fbig_cached_ops.rs index 412571e8..948dbf96 100644 --- a/float/src/fbig_cached_ops.rs +++ b/float/src/fbig_cached_ops.rs @@ -420,6 +420,17 @@ impl CachedFBig { ) } + /// Hyperbolic sine and cosine together (see [`FBig::sinh_cosh`]). + pub fn sinh_cosh(&self) -> (Self, Self) { + let mut guard = self.cache.borrow_mut(); + let cache = Some(&mut *guard); + let (s, c) = self.fbig.context.sinh_cosh::(&self.fbig.repr, cache); + ( + Self::from_fbig(self.fbig.context.unwrap_fp(s), &self.cache), + Self::from_fbig(self.fbig.context.unwrap_fp(c), &self.cache), + ) + } + /// `atan2(y, x)` (see [`FBig::atan2`]). pub fn atan2(&self, x: &Self) -> Self { let mut c = self.cache.borrow_mut(); diff --git a/float/src/math/hyper.rs b/float/src/math/hyper.rs index 556d019a..85f3a9c7 100644 --- a/float/src/math/hyper.rs +++ b/float/src/math/hyper.rs @@ -85,6 +85,51 @@ impl Context { } } + /// Simultaneously compute `sinh(x)` and `cosh(x)` (context layer). Returns + /// `(sinh_result, cosh_result)` where each is a [`FpResult`]. + /// + /// This is more efficient than calling [`sinh`](Context::sinh) and [`cosh`](Context::cosh) + /// separately, since the two share the `exp_m1(±x)` sub-computations. + pub fn sinh_cosh( + &self, + x: &Repr, + mut cache: Option<&mut ConstCache>, + ) -> (FpResult>, FpResult>) { + if x.is_infinite() { + return ( + Ok(Exact(FBig::new(Repr::infinity_with_sign(x.sign()), *self))), + Ok(Exact(FBig::new(Repr::infinity(), *self))), + ); + } + assert_limited_precision(self.precision); + if x.significand.is_zero() { + return ( + Ok(Exact(FBig::new(signed_zero_repr(x), *self))), + Ok(Exact(FBig::new(Repr::one(), *self))), + ); + } + + // sinh = (exp_m1(x) - exp_m1(-x)) / 2; cosh = (exp_m1(x) + exp_m1(-x)) / 2 + 1 + let work = Context::::new(self.precision + 50); + let x_f = FBig::::new(work.repr_round_ref(x).value(), work); + let neg_x = -x_f.clone(); + let ep = work.exp_m1(&x_f.repr, reborrow_cache(&mut cache)); + let em = work.exp_m1(&neg_x.repr, reborrow_cache(&mut cache)); + match (ep, em) { + (Ok(ep), Ok(em)) => { + let ep = ep.value(); + let em = em.value(); + let sinh_val = ((ep.clone() - em.clone()) / 2i32).with_precision(self.precision); + let cosh_val = + ((ep + em) / 2i32 + FBig::::ONE).with_precision(self.precision); + (Ok(sinh_val), Ok(cosh_val)) + } + // |x| large enough that exp_m1 overflowed: + // sinh(x) → ±inf (sign of x), cosh(x) → +inf + _ => (Err(FpError::Overflow(x.sign())), Err(FpError::Overflow(Sign::Positive))), + } + } + /// Hyperbolic tangent. pub fn tanh( &self, @@ -277,6 +322,29 @@ impl FBig { self.context.unwrap_fp(self.context.cosh(&self.repr, None)) } + /// Simultaneously calculate the hyperbolic sine and cosine of the number. + /// + /// This is more efficient than calling [`sinh`](FBig::sinh) and [`cosh`](FBig::cosh) + /// separately, since the two share the `exp_m1(±x)` sub-computations. + /// + /// # Examples + /// + /// ``` + /// # use core::str::FromStr; + /// # use dashu_base::ParseError; + /// # use dashu_float::DBig; + /// let a = DBig::from_str("0.5000000")?; + /// let (s, c) = a.sinh_cosh(); + /// assert_eq!(s, DBig::from_str("0.52109531")?); + /// assert_eq!(c, DBig::from_str("1.127626")?); + /// # Ok::<(), ParseError>(()) + /// ``` + #[inline] + pub fn sinh_cosh(&self) -> (Self, Self) { + let (s, c) = self.context.sinh_cosh(&self.repr, None); + (self.context.unwrap_fp(s), self.context.unwrap_fp(c)) + } + /// Calculate the hyperbolic tangent of the floating point number. /// /// # Examples diff --git a/float/src/math/trig.rs b/float/src/math/trig.rs index 6d598c32..deccb85e 100644 --- a/float/src/math/trig.rs +++ b/float/src/math/trig.rs @@ -70,12 +70,9 @@ impl Context { let x_scaled: FBig = &x_f / &half_pi; let k_f = x_scaled.round(); let r = x_f - &k_f * half_pi; - // `k_f` is the integer nearest `x_scaled`, but `round()` of a value in - // (-1, 0) yields signed zero, whose exponent sentinel is negative. Extract - // the integer via `to_int` (truncation) instead of `IBig::try_from`, which - // rejects any negative exponent and would panic here for tiny negative - // inputs. - let k = k_f.to_int().value(); + // `k_f` is the integer nearest `x_scaled`, so it's exact (or a signed zero + // for a tiny argument in (-1, 0), which `IBig::try_from` treats as plain 0). + let k = IBig::try_from(k_f).expect("k_f is an exact integer or signed zero"); let k_mod_4_big = k.rem_euclid(IBig::from(4)); let Ok(k_mod_4_int) = i8::try_from(k_mod_4_big) else { @@ -704,6 +701,8 @@ impl FBig { mod tests { use super::*; use crate::round::mode; + use crate::DBig; + use core::str::FromStr; #[test] fn test_atan_infinity_is_preserved() { @@ -717,8 +716,7 @@ mod tests { /// Regression: a tiny *negative* argument used to panic in `reduce_to_quadrant`. /// `round()` of a value in (-1, 0) yields signed zero (exponent sentinel -1), - /// which `IBig::try_from` rejected, hitting the `unreachable!`. The quadrant - /// integer is now extracted via `to_int`. + /// which `IBig::try_from` now accepts as plain 0. #[test] fn test_trig_tiny_negative_no_panic() { let ctx = Context::::new(30); @@ -737,4 +735,17 @@ mod tests { assert_eq!(cc.sign(), Sign::Positive); } } + + /// Regression: a 49-digit significand at precision 100 used to assertion-fail in `Context::sin`'s + /// rounding logic (found during fuzzing). Promoted here from the excluded `fuzz/` crate so it runs + /// in CI; rewritten to the current `Context::sin` API. + #[test] + fn test_sin_many_digit_rounding_no_panic() { + let x = DBig::from_str("-5.525474318981006776603409487767135633516667011547942409467e-3") + .unwrap(); + let ctx = Context::::new(100); + let s = ctx.sin::<10>(x.repr(), None).unwrap().value(); + // sin(x) ≈ x for a small negative x — completing without panicking is the regression guard. + assert_eq!(s.sign(), Sign::Negative); + } } diff --git a/float/src/root.rs b/float/src/root.rs index 20bef24c..520dbf23 100644 --- a/float/src/root.rs +++ b/float/src/root.rs @@ -254,6 +254,94 @@ impl Context { } } +impl Context { + /// Compute `sqrt(a² + b²)` without spurious overflow/underflow. + /// + /// This is the overflow-safe scaled sum-of-squares: the larger-magnitude operand is never + /// squared. Writing `m = max(|a|, |b|)` and `r = min(|a|,|b|) / m` (so `|r| ≤ 1`), the result is + /// `m · sqrt(1 + r²)`, where `1 + r² ∈ [1, 2]` cannot overflow. The final `m · sqrt(1 + r²)` + /// overflows only when the true result genuinely exceeds the exponent range (reported as + /// [`FpError::Overflow`]). `hypot(±inf, ·) = +inf`, `hypot(0, 0) = +0`. + /// + /// This is a field-arithmetic-class op (no constant cache), like `sqrt`/`atan2`. + /// + /// # Panics + /// + /// Panics if the precision is unlimited. + pub fn hypot(&self, a: &Repr, b: &Repr) -> FpResult> { + if a.is_infinite() || b.is_infinite() { + return Ok(Approximation::Exact(FBig::new(Repr::infinity(), *self))); + } + assert_limited_precision(self.precision); + if a.significand.is_zero() && b.significand.is_zero() { + return Ok(Approximation::Exact(FBig::new(Repr::zero(), *self))); + } + + let guard = crate::utils::ceil_usize(::log2_est( + &self.precision, + )) + 10; + let gctx = Context::::new(self.precision + guard); + + // magnitudes, ordered large >= small (both finite, not both zero here) + let a_mag = if a.sign() == Sign::Negative { + -a.clone() + } else { + a.clone() + }; + let b_mag = if b.sign() == Sign::Negative { + -b.clone() + } else { + b.clone() + }; + let (large, small) = if a_mag.cmp(&b_mag).is_ge() { + (a_mag, b_mag) + } else { + (b_mag, a_mag) + }; + + if small.significand.is_zero() { + // hypot(x, 0) = |x|; `large` is already a magnitude + return Ok(gctx.repr_round_ref(&large).map(|v| FBig::new(v, *self))); + } + + // r = small / large ∈ [0, 1]; 1 + r² ∈ [1, 2] (no overflow); result = large · sqrt(1+r²) + let r = gctx.div(&small, &large)?.value(); + let r2 = gctx.sqr(r.repr())?.value(); + let sum = gctx.add(&Repr::one(), r2.repr())?.value(); + let root = gctx.sqrt(sum.repr())?.value(); + let result = gctx.mul(&large, root.repr())?.value(); + Ok(result.with_precision(self.precision)) + } +} + +impl FBig { + /// Compute `sqrt(self² + other²)` without spurious overflow/underflow. + /// + /// The result precision is `max(self.precision(), other.precision())`. See + /// [`Context::hypot`] for the overflow-safety strategy. + /// + /// # Examples + /// + /// ``` + /// # use core::str::FromStr; + /// # use dashu_base::ParseError; + /// # use dashu_float::DBig; + /// let a = DBig::from_str("3")?; + /// let b = DBig::from_str("4")?; + /// assert_eq!(a.hypot(&b), DBig::from_str("5")?); + /// # Ok::<(), ParseError>(()) + /// ``` + /// + /// # Panics + /// + /// Panics if the precision is unlimited. + #[inline] + pub fn hypot(&self, other: &Self) -> Self { + let context = Context::max(self.context, other.context); + context.unwrap_fp(context.hypot(&self.repr, &other.repr)) + } +} + #[cfg(test)] mod tests { use super::*; @@ -266,4 +354,34 @@ mod tests { let neg_one = FBig::::try_from(-1.0f64).unwrap(); let _ = neg_one.sqrt(); } + + #[test] + fn test_hypot_pythagorean() { + let ctx = Context::::new(53); + let mk = |v: i32| Repr::<2>::new(v.into(), 0); + // hypot(3, 4) = 5 + let r = ctx.hypot(&mk(3), &mk(4)).unwrap().value(); + assert_eq!(r.repr().significand(), &5.into()); + // hypot(5, 0) = 5 + let r = ctx.hypot(&mk(5), &mk(0)).unwrap().value(); + assert_eq!(r.repr().significand(), &5.into()); + // hypot(0, 0) = 0 + let r = ctx.hypot(&mk(0), &mk(0)).unwrap().value(); + assert!(r.repr().is_zero()); + // hypot(inf, x) = +inf + let r = ctx.hypot(&Repr::infinity(), &mk(3)).unwrap().value(); + assert!(r.repr().is_infinite()); + assert_eq!(r.repr().sign(), Sign::Positive); + } + + #[test] + fn test_hypot_no_spurious_overflow() { + // a value whose square would collide with the +inf sentinel exponent, but whose + // hypot is itself representable: hypot(a, 0) = |a| must not overflow via a². + let ctx = Context::::new(53); + // exponent near isize::MAX/2 so that a² would overflow, but |a| is fine + let a = Repr::<2>::new(IBig::from(3), isize::MAX / 2); + let r = ctx.hypot(&a, &Repr::<2>::zero()).unwrap().value(); + assert_eq!(r.repr().exponent(), isize::MAX / 2); + } } diff --git a/float/src/third_party/num_order.rs b/float/src/third_party/num_order.rs index a8580378..4ead391c 100644 --- a/float/src/third_party/num_order.rs +++ b/float/src/third_party/num_order.rs @@ -279,18 +279,28 @@ impl_num_ord_with_float!(f32 f64); forward_num_ord_to_repr!(f32); forward_num_ord_to_repr!(f64); -impl NumHash for Repr { - fn num_hash(&self, state: &mut H) { +impl Repr { + /// The numeric-hash residue (mod 2¹²⁷−1) used by [`NumHash`]: + /// `sgn(significand) · (|significand| mod M127) · (B^exponent mod M127)`. + /// + /// Special values: `+0` → `0`, `-0` → `0`, `+∞` → `HASH_INF` (= `M127`), `-∞` → `HASH_NEGINF` + /// (= `-M127`), matching num-order's `f64::fhash`. The subsequent `i128::num_hash` maps both + /// `HASH_INF` and `HASH_NEGINF` back to `0`, so the *final* hash of ±∞ is `0` — but the + /// *residue* distinguishes them so that composite types (e.g. `CBig`) combine them algebraically + /// the same way num-order's `Complex` does. + pub fn num_hash_residue(&self) -> i128 { // 2^127 - 1 is used in the num-order crate type MInt = FixedMersenneInt<127, 1>; const M127: i128 = i128::MAX; const M127U: u128 = M127 as u128; - // Zero and infinities have a zero significand, so their residue hash is 0. - // Short-circuit to also avoid overflow when negating the isize::MIN sentinel - // exponent that encodes -inf. if self.significand.is_zero() { - return 0i128.num_hash(state); + // Distinguish infinities (sentinel exponents) from signed zero. + return match self.exponent { + isize::MAX => M127, // +∞ → HASH_INF + isize::MIN => i128::MIN + 1, // -∞ → HASH_NEGINF (= -M127) + _ => 0, // ±0 + }; } let signif_residue = &self.significand % M127; @@ -298,7 +308,6 @@ impl NumHash for Repr { let exp_hash = if B == 2 { signif_hash.convert(1 << self.exponent.absm(&127)) } else if self.exponent < 0 { - // since a Word is at most 64 bits right now, B is always less than M127 signif_hash .convert(B as u128) .pow(&(-self.exponent as u128)) @@ -312,8 +321,14 @@ impl NumHash for Repr { if signif_residue < 0 { hash = -hash; } + hash + } +} - hash.num_hash(state) +impl NumHash for Repr { + #[inline] + fn num_hash(&self, state: &mut H) { + self.num_hash_residue().num_hash(state) } } @@ -343,6 +358,50 @@ mod tests { hasher.finish() } + /// Capture the i128 residue a `NumHash` impl writes (the `i128` NumHash writes its value via + /// `Hasher::write_i128`), so the *field element* can be compared directly. + fn residue(value: &T) -> i128 { + struct Collector(i128); + impl core::hash::Hasher for Collector { + fn write_i128(&mut self, v: i128) { + self.0 = v; + } + fn write(&mut self, _: &[u8]) {} + fn finish(&self) -> u64 { + 0 + } + } + let mut c = Collector(0); + value.num_hash(&mut c); + c.0 + } + + // The base-2 Repr residue must equal num-order's f64 `fhash` for the same finite value — this + // is what lets dashu-cmplx's CBig reuse Repr residues and stay in sync with num-order's + // Complex hashing. + #[test] + fn test_fbig_num_hash_matches_f64() { + for v in [ + 1.0_f64, + 2.0, + 3.0, + 0.5, + 0.25, + -0.75, + 100.0, + 1e-10, + 1e20, + 123.456, + 1.0 / 3.0, + f64::INFINITY, + f64::NEG_INFINITY, + -0.0, + ] { + let f: FBin = core::convert::TryFrom::try_from(v).unwrap(); + assert_eq!(residue(&f), residue(&v), "FBig/f64 num_hash disagree for {v}"); + } + } + // -- NumOrd for Repr (same base) -- #[test] diff --git a/float/tests/trig_prop.rs b/float/tests/trig_prop.rs index e282d661..0a419ead 100644 --- a/float/tests/trig_prop.rs +++ b/float/tests/trig_prop.rs @@ -35,12 +35,18 @@ fn one() -> DBig { proptest! { #![proptest_config(ProptestConfig { cases: 64, ..Default::default() })] - /// sin^2(x) + cos^2(x) == 1 + /// sin^2(x) + cos^2(x) == 1 across precisions {20, 50, 100} + /// (consolidated from the former fuzz `test_pythagorean_identity_fuzz`). #[test] fn pythagorean(x in x_in(-200_000, 200_000)) { - let (s, c) = x.sin_cos(); - let resid = (&s * &s + &c * &c - one()).abs(); - prop_assert!(resid < tol(2)); + for p in [20usize, 50, 100] { + let xp = x.clone().with_precision(p).value(); + let (s, c) = xp.sin_cos(); + let one = DBig::ONE.with_precision(p).value(); + let resid = (&s * &s + &c * &c - &one).abs(); + let tol = DBig::from_parts(IBig::from(1), -(p as isize) + 2); + prop_assert!(resid < tol, "sin^2+cos^2 != 1 at prec {p}, x = {xp}"); + } } /// sin(2x) == 2 sin(x) cos(x); cos(2x) == cos^2(x) - sin^2(x) diff --git a/fuzz/Cargo.toml b/fuzz/Cargo.toml index 1b81d601..b1dca268 100644 --- a/fuzz/Cargo.toml +++ b/fuzz/Cargo.toml @@ -5,11 +5,10 @@ edition = "2024" publish = false [dependencies] -dashu-float = { path = "../float" } -dashu-int = { path = "../integer", features = ["rand_v010"] } -dashu-base = { path = "../base" } +dashu = { path = ".." } rand = "0.10.1" rug = "1.24" +proptest = "~1.7" # This package is excluded from the root workspace; declare its own workspace so it builds # standalone (`cargo test --manifest-path fuzz/Cargo.toml`). diff --git a/fuzz/src/lib.rs b/fuzz/src/lib.rs index 8b137891..d0126487 100644 --- a/fuzz/src/lib.rs +++ b/fuzz/src/lib.rs @@ -1 +1,149 @@ +//! Shared strategies and helpers for the `fuzz` differential tests. +//! +//! The test binaries under `fuzz/tests/` are proptest-driven differentials against `rug` (GMP/MPFR/ +//! MPC) or an internal exact-then-round oracle. They live in a workspace-excluded crate and are run +//! manually before a release (`cargo test --manifest-path fuzz/Cargo.toml -- --ignored`); they are +//! **not** part of CI's per-PR test job (CI only `cargo check`s this crate — see the `fuzz-check` +//! workflow). Proptest gives shrinking: a failing differential reduces to a minimal counterexample. +use dashu::float::round::mode::HalfAway; +use dashu::float::{Context, FBig, Repr}; +use dashu::integer::{IBig, UBig, Word}; +use proptest::prelude::*; + +/// Default fuzz strength — more cases than CI's per-crate `PROPTEST_CASES=256`, since these run +/// out-of-band and are meant to be thorough. Overridable via the `PROPTEST_CASES` env var. +pub fn fuzz_config() -> ProptestConfig { + ProptestConfig { + cases: 1024, + ..ProptestConfig::default() + } +} + +/// A random `IBig` of bounded magnitude (up to `max_words · 64` bits) with a random sign. Trailing +/// zero words are trimmed so that proptest shrinking can reduce the magnitude to a minimal failing +/// case rather than getting stuck on a large zero-padded significand. +pub fn ibig_strategy(max_words: usize) -> impl Strategy { + (any::(), prop::collection::vec(any::(), 0..max_words)).prop_map( + |(neg, mut words)| { + while words.last() == Some(&0) { + words.pop(); + } + let mag = if words.is_empty() { + UBig::ZERO + } else { + UBig::from_words(&words) + }; + let v = IBig::from(mag); + if neg && !v.is_zero() { -v } else { v } + }, + ) +} + +/// A random `UBig` of bounded magnitude (no sign) — for unsigned integer oracles (sqrt / root / +/// bit-ops / power-of-two). Trims trailing zero words for better shrinking. +pub fn ubig_strategy(max_words: usize) -> impl Strategy { + prop::collection::vec(any::(), 0..max_words).prop_map(|mut words| { + while words.last() == Some(&0) { + words.pop(); + } + if words.is_empty() { + UBig::ZERO + } else { + UBig::from_words(&words) + } + }) +} + +/// A random base-10 `DBig` (= `FBig`) at unlimited precision, exponent drawn from +/// `exp_range`. Each test re-rounds it to a target precision via its own `Context`. +pub fn dbig_strategy( + exp_range: std::ops::RangeInclusive, +) -> impl Strategy> { + (ibig_strategy(5), exp_range).prop_map(|(sig, exp)| { + FBig::from_repr(Repr::<10>::new(sig, exp), Context::::new(0)) + }) +} + +/// A positive base-10 `DBig` at unlimited precision (significand ≥ 1), for the ln/sqrt/powf domains. +pub fn pos_dbig_strategy( + exp_range: std::ops::RangeInclusive, +) -> impl Strategy> { + (prop::collection::vec(any::(), 1..5), exp_range).prop_map(|(mut words, exp)| { + while words.last() == Some(&0) { + words.pop(); + } + if words.is_empty() { + words.push(1); + } + FBig::from_repr( + Repr::<10>::new(IBig::from(UBig::from_words(&words)), exp), + Context::::new(0), + ) + }) +} + +/// A base-10 `DBig` in `[-1, 1]` (as `n/1000`), for the real `asin`/`acos`/`atanh`/`ln_1p` domains. +/// Shrinks toward 0. +pub fn unit_dbig() -> impl Strategy> { + (-1000i32..=1000) + .prop_map(|n| FBig::from_repr(Repr::<10>::new(n.into(), -3), Context::::new(0))) +} + +/// Shared helpers for the `CBig` vs `rug::Complex` (MPC) differentials at 53-bit precision. +pub mod cmplx { + use core::convert::TryFrom; + use dashu::complex::CBig; + use dashu::float::FBig; + use dashu::float::round::mode::HalfEven; + use proptest::prelude::*; + + pub type C = CBig; + pub type F = FBig; + + /// A modest-magnitude finite `f64` (`±(1..=8) · [1,2) · 2^(-2..=2)`), shrinking toward small values. + pub fn f64_part() -> impl Strategy { + (1u8..=8, any::(), 0u32..1000, -2i32..=2).prop_map(|(sig, neg, frac, exp)| { + let mant = 1.0 + (frac as f64) / 1000.0; + let mag = (sig as f64) * mant * 2f64.powi(exp); + if neg { -mag } else { mag } + }) + } + + pub fn fbig_from(v: f64) -> F { + F::try_from(v).unwrap().with_precision(53).value() + } + + /// Build a dashu `CBig` and a matching `rug::Complex` (53-bit) from `f64` parts. + pub fn pair(re: f64, im: f64) -> (C, rug::Complex) { + let cbig = CBig::from_parts(fbig_from(re), fbig_from(im)); + let rug = rug::Complex::with_val(53, (re, im)); + (cbig, rug) + } + + pub fn cbig_to_f64(z: &C) -> (f64, f64) { + let (re, im) = z.clone().into_parts(); + (re.to_f64().value(), im.to_f64().value()) + } + + pub fn rug_to_f64(z: &rug::Complex) -> (f64, f64) { + (z.real().to_f64(), z.imag().to_f64()) + } + + /// True when both `(re, im)` pairs are finite and agree to within a few ulps (scale-relative). + pub fn close(a: (f64, f64), b: (f64, f64)) -> bool { + let (ar, ai) = a; + let (br, bi) = b; + if !ar.is_finite() || !ai.is_finite() || !br.is_finite() || !bi.is_finite() { + return false; // skip non-finite (overflow / branch-point) results + } + let scale = ar + .abs() + .max(ai.abs()) + .max(br.abs()) + .max(bi.abs()) + .max(1e-300); + let tol = scale * 1e-12; + (ar - br).abs() <= tol && (ai - bi).abs() <= tol + } +} diff --git a/fuzz/tests/add_random.rs b/fuzz/tests/add_random.rs index b5dc7d3d..222680b3 100644 --- a/fuzz/tests/add_random.rs +++ b/fuzz/tests/add_random.rs @@ -5,25 +5,14 @@ //! oracle: the exact sum/difference (computed at unlimited precision) re-rounded with //! `FBig::with_precision`, which uses the simple `repr_round` path rather than //! `repr_round_sum`. The two must agree for every rounding mode, base, precision and operand -//! shape. +//! shape. Proptest-driven so a mismatch shrinks to a minimal `(a, b, precision)` counterexample. //! //! Run with: `cargo test --manifest-path fuzz/Cargo.toml --test add_random -- --ignored --nocapture` -use dashu_float::round::Round; -use dashu_float::round::mode::*; -use dashu_float::{Context, FBig, Repr, Word}; -use dashu_int::{rand::UniformBits, IBig}; -use rand::prelude::*; - -/// Random signed significand drawn directly as a random `IBig` of bounded bit length. -/// -/// The magnitude is at most ~266 bits (≈ 80 decimal digits), matching the coverage of the -/// previous decimal-string generator but without per-iteration allocation + parsing. A -/// base-agnostic integer lets the same significand feed `Repr::` for any base `B`. -fn random_significand(rng: &mut R) -> IBig { - let bits = rng.random_range(1..=266usize); - rng.sample(UniformBits::new(bits)) -} +use dashu::float::round::Round; +use dashu::float::round::mode::*; +use dashu::float::{Context, FBig, Repr, Word}; +use proptest::prelude::*; /// Round the exact result to `precision` and `precision + 1` digits, returning both. /// @@ -59,72 +48,63 @@ fn check_pair( let ctx = Context::::new(precision); let unlimited = Context::::new(0); - let actual_add = ctx.add(a, b).value().repr().clone(); + let actual_add = ctx.add(a, b).unwrap().value().repr().clone(); let (add_p, add_p1) = - rounded_oracle::(unlimited.add(a, b).value().repr().clone(), precision); + rounded_oracle::(unlimited.add(a, b).unwrap().value().repr().clone(), precision); assert!( actual_add == add_p || actual_add == add_p1, "add mismatch (mode={mode_name}, p={precision})\n a={a:?}\n b={b:?}\n actual={actual_add:?}\n oracle(p)={add_p:?}\n oracle(p+1)={add_p1:?}", ); - let actual_sub = ctx.sub(a, b).value().repr().clone(); + let actual_sub = ctx.sub(a, b).unwrap().value().repr().clone(); let (sub_p, sub_p1) = - rounded_oracle::(unlimited.sub(a, b).value().repr().clone(), precision); + rounded_oracle::(unlimited.sub(a, b).unwrap().value().repr().clone(), precision); assert!( actual_sub == sub_p || actual_sub == sub_p1, "sub mismatch (mode={mode_name}, p={precision})\n a={a:?}\n b={b:?}\n actual={actual_sub:?}\n oracle(p)={sub_p:?}\n oracle(p+1)={sub_p1:?}", ); } -fn run_mode(rng: &mut StdRng, iters: usize, mode_name: &str) { - // Deterministic small-precision sweep: precisions 1, 2, 3 are where rounding bugs live, - // but the random `1..200` below hits them only by chance. Always exercise these boundary - // precisions (ported from the removed `add_sub_oracle` example). - for &precision in &[1usize, 2, 3, 5, 10] { - for _ in 0..50 { - let a_exp = rng.random_range(-1500..1500) as isize; - let a = Repr::::new(random_significand(rng), a_exp); - let b_exp = rng.random_range(-1500..1500) as isize; - let b = Repr::::new(random_significand(rng), b_exp); - check_pair::(&a, &b, precision, mode_name); - } - } - - for _ in 0..iters { - // Wide exponent range so that the negligible-small, tight-align, cancellation and borrow - // (result just below a round number) branches are all exercised. - let a_exp = rng.random_range(-1500..1500) as isize; - let a_sig = random_significand(rng); - let a = Repr::::new(a_sig, a_exp); - - let b_exp = rng.random_range(-1500..1500) as isize; - let b_sig = random_significand(rng); - let b = Repr::::new(b_sig, b_exp); - - let precision = rng.random_range(1..200); - check_pair::(&a, &b, precision, mode_name); - } +/// Run `check_pair` under all six rounding modes for one operand pair + precision. +fn check_all_modes(a: &Repr, b: &Repr, precision: usize) { + check_pair::(a, b, precision, "Zero"); + check_pair::(a, b, precision, "Away"); + check_pair::(a, b, precision, "Up"); + check_pair::(a, b, precision, "Down"); + check_pair::(a, b, precision, "HalfEven"); + check_pair::(a, b, precision, "HalfAway"); } -fn run_all_modes(rng: &mut StdRng, iters: usize) { - run_mode::(rng, iters, "Zero"); - run_mode::(rng, iters, "Away"); - run_mode::(rng, iters, "Up"); - run_mode::(rng, iters, "Down"); - run_mode::(rng, iters, "HalfEven"); - run_mode::(rng, iters, "HalfAway"); +/// Precision strategy biased toward the boundary precisions 1/2/3 (where rounding bugs live), +/// mixed with a uniform draw over `1..200`. +fn precision_strategy() -> impl Strategy { + prop_oneof![Just(1usize), Just(2), Just(3), 1usize..200,] } -#[test] -#[ignore] -fn test_add_sub_differential_binary() { - let mut rng = StdRng::seed_from_u64(0x1234_5678_9abc_def0); - run_all_modes::<2>(&mut rng, 10000); -} +proptest! { + #![proptest_config(fuzz::fuzz_config())] -#[test] -#[ignore] -fn test_add_sub_differential_decimal() { - let mut rng = StdRng::seed_from_u64(0x0fed_cba9_8765_4321); - run_all_modes::<10>(&mut rng, 10000); + #[test] + #[ignore] + fn add_sub_differential_binary( + a_sig in fuzz::ibig_strategy(5), a_exp in -1500isize..1500, + b_sig in fuzz::ibig_strategy(5), b_exp in -1500isize..1500, + precision in precision_strategy(), + ) { + let a = Repr::<2>::new(a_sig, a_exp); + let b = Repr::<2>::new(b_sig, b_exp); + check_all_modes::<2>(&a, &b, precision); + } + + #[test] + #[ignore] + fn add_sub_differential_decimal( + a_sig in fuzz::ibig_strategy(5), a_exp in -1500isize..1500, + b_sig in fuzz::ibig_strategy(5), b_exp in -1500isize..1500, + precision in precision_strategy(), + ) { + let a = Repr::<10>::new(a_sig, a_exp); + let b = Repr::<10>::new(b_sig, b_exp); + check_all_modes::<10>(&a, &b, precision); + } } diff --git a/fuzz/tests/cmplx_random.rs b/fuzz/tests/cmplx_random.rs new file mode 100644 index 00000000..8eca87be --- /dev/null +++ b/fuzz/tests/cmplx_random.rs @@ -0,0 +1,36 @@ +//! Differential / fuzz test: `dashu-cmplx::CBig` field arithmetic against `rug::Complex` (GNU MPC) +//! at 53-bit precision. +//! +//! For random finite inputs, `mul`/`div`/`sqr` are computed in both libraries and the `(re, im)` +//! `f64` parts must agree to within a few ulps — both are (near-)correctly rounded at 53 bits, and +//! field arithmetic is MPC's hardest-to-round class (the spec's top risk). Non-finite results are +//! skipped. Proptest-driven so a mismatch shrinks to a minimal counterexample. Shared +//! build/compare helpers live in `fuzz::cmplx`. +//! +//! Run with: `cargo test --manifest-path fuzz/Cargo.toml --test cmplx_random -- --ignored --nocapture` + +use fuzz::cmplx::*; +use proptest::prelude::*; + +proptest! { + #![proptest_config(fuzz::fuzz_config())] + + #[test] + #[ignore] + fn mpc_mul_div_sqr_oracle( + zre in f64_part(), zim in f64_part(), + wre in f64_part(), wim in f64_part(), + ) { + let (z, rz) = pair(zre, zim); + let (w, rw) = pair(wre, wim); + + // mul + prop_assert!(close(cbig_to_f64(&(&z * &w)), rug_to_f64(&(rz.clone() * rw.clone())))); + // sqr + prop_assert!(close(cbig_to_f64(&z.sqr()), rug_to_f64(&(rz.clone() * rz.clone())))); + // div (skip a zero denominator) + if !w.is_zero() { + prop_assert!(close(cbig_to_f64(&(&z / &w)), rug_to_f64(&(rz / rw)))); + } + } +} diff --git a/fuzz/tests/cmplx_transcendental.rs b/fuzz/tests/cmplx_transcendental.rs new file mode 100644 index 00000000..a8e42e8c --- /dev/null +++ b/fuzz/tests/cmplx_transcendental.rs @@ -0,0 +1,123 @@ +//! Differential / fuzz tests for `dashu-cmplx::CBig` transcendentals against `rug::Complex` (GNU MPC) +//! at 53-bit precision. +//! +//! Companion to `cmplx_random.rs` (which covers field arithmetic mul/div/sqr). Here: exp, log, sqrt, +//! sin, cos, tan, asin, acos, atan, powf. rug has direct MPC methods for all of these (no gaps). +//! Reuses the shared `fuzz::cmplx` build/compare helpers (`pair`, `cbig_to_f64`, `rug_to_f64`, +//! `close`). All `#[ignore]`d (manual, release-time). Inputs are modest-magnitude finite `f64` +//! pairs, so results stay finite and `close`'s non-finite guard never trips in practice. +//! +//! Run with: `cargo test --manifest-path fuzz/Cargo.toml --test cmplx_transcendental -- --ignored --nocapture` + +use fuzz::cmplx::*; +use proptest::prelude::*; +use rug::ops::Pow; + +/// Unwrap a `CfpResult` to its `CBig` value, or skip the case on error (e.g. tan at a zero of +/// cos, powf singularities). +macro_rules! cmplx_ok { + ($e:expr) => { + match $e { + Ok(v) => v.value(), + Err(_) => return Ok(()), + } + }; +} + +proptest! { + #![proptest_config(fuzz::fuzz_config())] + + /// exp(z) ≈ MPC exp(z). + #[test] + #[ignore] + fn mpc_exp(zre in f64_part(), zim in f64_part()) { + let (z, rz) = pair(zre, zim); + let d = cmplx_ok!(z.context().exp(&z, None)); + prop_assert!(close(cbig_to_f64(&d), rug_to_f64(&rz.exp()))); + } + + /// log(z) ≈ MPC ln(z). + #[test] + #[ignore] + fn mpc_log(zre in f64_part(), zim in f64_part()) { + let (z, rz) = pair(zre, zim); + let d = cmplx_ok!(z.context().log(&z, None)); + prop_assert!(close(cbig_to_f64(&d), rug_to_f64(&rz.ln()))); + } + + /// sqrt(z) ≈ MPC sqrt(z). + #[test] + #[ignore] + fn mpc_sqrt(zre in f64_part(), zim in f64_part()) { + let (z, rz) = pair(zre, zim); + let d = cmplx_ok!(z.context().sqrt(&z)); + prop_assert!(close(cbig_to_f64(&d), rug_to_f64(&rz.sqrt()))); + } + + /// sin(z) ≈ MPC sin(z). + #[test] + #[ignore] + fn mpc_sin(zre in f64_part(), zim in f64_part()) { + let (z, rz) = pair(zre, zim); + let d = cmplx_ok!(z.context().sin(&z, None)); + prop_assert!(close(cbig_to_f64(&d), rug_to_f64(&rz.sin()))); + } + + /// cos(z) ≈ MPC cos(z). + #[test] + #[ignore] + fn mpc_cos(zre in f64_part(), zim in f64_part()) { + let (z, rz) = pair(zre, zim); + let d = cmplx_ok!(z.context().cos(&z, None)); + prop_assert!(close(cbig_to_f64(&d), rug_to_f64(&rz.cos()))); + } + + /// tan(z) ≈ MPC tan(z) (skips zeros of cos, where tan is singular). + #[test] + #[ignore] + fn mpc_tan(zre in f64_part(), zim in f64_part()) { + let (z, rz) = pair(zre, zim); + let d = cmplx_ok!(z.context().tan(&z, None)); + prop_assert!(close(cbig_to_f64(&d), rug_to_f64(&rz.tan()))); + } + + /// asin(z) ≈ MPC asin(z). + #[test] + #[ignore] + fn mpc_asin(zre in f64_part(), zim in f64_part()) { + let (z, rz) = pair(zre, zim); + let d = cmplx_ok!(z.context().asin(&z, None)); + prop_assert!(close(cbig_to_f64(&d), rug_to_f64(&rz.asin()))); + } + + /// acos(z) ≈ MPC acos(z). + #[test] + #[ignore] + fn mpc_acos(zre in f64_part(), zim in f64_part()) { + let (z, rz) = pair(zre, zim); + let d = cmplx_ok!(z.context().acos(&z, None)); + prop_assert!(close(cbig_to_f64(&d), rug_to_f64(&rz.acos()))); + } + + /// atan(z) ≈ MPC atan(z). + #[test] + #[ignore] + fn mpc_atan(zre in f64_part(), zim in f64_part()) { + let (z, rz) = pair(zre, zim); + let d = cmplx_ok!(z.context().atan(&z, None)); + prop_assert!(close(cbig_to_f64(&d), rug_to_f64(&rz.atan()))); + } + + /// base^w ≈ MPC pow(base, w). + #[test] + #[ignore] + fn mpc_powf( + zre in f64_part(), zim in f64_part(), + wre in f64_part(), wim in f64_part(), + ) { + let (z, rz) = pair(zre, zim); + let (w, rw) = pair(wre, wim); + let d = cmplx_ok!(z.context().powf(&z, &w, None)); + prop_assert!(close(cbig_to_f64(&d), rug_to_f64(&rz.pow(&rw)))); + } +} diff --git a/fuzz/tests/integer.rs b/fuzz/tests/integer.rs new file mode 100644 index 00000000..8f93e1e1 --- /dev/null +++ b/fuzz/tests/integer.rs @@ -0,0 +1,172 @@ +//! Differential / fuzz tests for `dashu-int` (`UBig`/`IBig`) against `rug::Integer` (GMP). +//! +//! Integer ops are EXACT (no rounding), so the comparison is exact equality via a decimal-string +//! round-trip (`dashu.to_string()` → `rug::Integer` parse, compute, rug result → string → +//! `dashu::from_str_radix`). This validates dashu's bignum algorithms against the GMP reference. +//! Proptest-driven; all `#[ignore]`d (manual, release-time). +//! +//! Run with: `cargo test --manifest-path fuzz/Cargo.toml --test integer -- --ignored --nocapture` + +use dashu::base::SquareRoot; +use dashu::base::ring::{DivRem, Gcd}; +use dashu::integer::{IBig, UBig}; +use proptest::prelude::*; +use rug::ops::Pow; + +fn u_to_rug(x: &UBig) -> rug::Integer { + x.to_string().parse::().unwrap() +} +fn i_to_rug(x: &IBig) -> rug::Integer { + x.to_string().parse::().unwrap() +} +fn rug_to_u(i: &rug::Integer) -> UBig { + UBig::from_str_radix(&i.to_string_radix(10), 10).unwrap() +} +fn rug_to_i(i: &rug::Integer) -> IBig { + IBig::from_str_radix(&i.to_string_radix(10), 10).unwrap() +} + +/// Complete a rug `…Incomplete` (or an owned `Integer`) into an `Integer` via `Assign`. +fn rugc(src: S) -> rug::Integer +where + rug::Integer: rug::Assign, +{ + let mut r = rug::Integer::new(); + rug::Assign::assign(&mut r, src); + r +} + +proptest! { + #![proptest_config(fuzz::fuzz_config())] + + // ---- UBig ---- + + + #[test] + #[ignore] + fn ubig_mul(a in fuzz::ubig_strategy(4), b in fuzz::ubig_strategy(4)) { + let d = &a * &b; + let r = rugc(u_to_rug(&a) * u_to_rug(&b)); + prop_assert_eq!(d, rug_to_u(&r)); + } + + #[test] + #[ignore] + fn ubig_sqr(a in fuzz::ubig_strategy(5)) { + let d = a.sqr(); + let ar = u_to_rug(&a); + let r = rugc(&ar * &ar); + prop_assert_eq!(d, rug_to_u(&r)); + } + + #[test] + #[ignore] + fn ubig_gcd((a, b) in (fuzz::ubig_strategy(4), fuzz::ubig_strategy(4)).prop_filter("not both zero (gcd(0,0) is undefined → panic)", |(a, b)| !(a.is_zero() && b.is_zero()))) { + let r = u_to_rug(&a).gcd(&u_to_rug(&b)); + let d = a.gcd(&b); + prop_assert_eq!(d, rug_to_u(&r)); + } + + #[test] + #[ignore] + fn ubig_div_rem(a in fuzz::ubig_strategy(4), b in fuzz::ubig_strategy(2).prop_filter("nonzero", |b| !b.is_zero())) { + let (rq, rr) = u_to_rug(&a).div_rem(u_to_rug(&b)); + let (dq, dr) = a.div_rem(&b); + prop_assert_eq!(dq, rug_to_u(&rq)); + prop_assert_eq!(dr, rug_to_u(&rr)); + } + + #[test] + #[ignore] + fn ubig_pow(a in fuzz::ubig_strategy(3), n in 0u32..=16) { + let d = a.pow(n as usize); + let r = rugc(u_to_rug(&a).pow(n)); + prop_assert_eq!(d, rug_to_u(&r)); + } + + #[test] + #[ignore] + fn ubig_sqrt(a in fuzz::ubig_strategy(6)) { + let d = a.sqrt(); + let r = u_to_rug(&a).sqrt(); + prop_assert_eq!(d, rug_to_u(&r)); + } + + #[test] + #[ignore] + fn ubig_nth_root(a in fuzz::ubig_strategy(6), n in 2u32..=6) { + let d = a.nth_root(n as usize); + let r = u_to_rug(&a).root(n); + prop_assert_eq!(d, rug_to_u(&r)); + } + + #[test] + #[ignore] + fn ubig_bit_ops(a in fuzz::ubig_strategy(4), b in fuzz::ubig_strategy(4)) { + let (ar, br) = (u_to_rug(&a), u_to_rug(&b)); + prop_assert_eq!(&a & &b, rug_to_u(&rugc(&ar & &br))); + prop_assert_eq!(&a | &b, rug_to_u(&rugc(&ar | &br))); + prop_assert_eq!(&a ^ &b, rug_to_u(&rugc(&ar ^ &br))); + } + + #[test] + #[ignore] + fn ubig_shifts(a in fuzz::ubig_strategy(4), n in 0u32..=200) { + let ar = u_to_rug(&a); + prop_assert_eq!(&a << (n as usize), rug_to_u(&rugc(&ar << n))); + prop_assert_eq!(&a >> (n as usize), rug_to_u(&rugc(&ar >> n))); + } + + // ---- IBig ---- + + #[test] + #[ignore] + fn ibig_mul(a in fuzz::ibig_strategy(4), b in fuzz::ibig_strategy(4)) { + let d = &a * &b; + let r = rugc(i_to_rug(&a) * i_to_rug(&b)); + prop_assert_eq!(d, rug_to_i(&r)); + } + + #[test] + #[ignore] + fn ibig_gcd((a, b) in (fuzz::ibig_strategy(4), fuzz::ibig_strategy(4)).prop_filter("not both zero (gcd(0,0) is undefined → panic)", |(a, b)| !(a.is_zero() && b.is_zero()))) { + // gcd is non-negative for both + let r = i_to_rug(&a).gcd(&i_to_rug(&b)); + let d: UBig = a.gcd(&b); + prop_assert_eq!(d, rug_to_u(&r)); + } + + #[test] + #[ignore] + fn ibig_div_rem(a in fuzz::ibig_strategy(4), b in fuzz::ibig_strategy(2).prop_filter("nonzero", |b| !b.is_zero())) { + let (rq, rr) = i_to_rug(&a).div_rem(i_to_rug(&b)); + let (dq, dr) = a.div_rem(&b); + prop_assert_eq!(dq, rug_to_i(&rq)); + prop_assert_eq!(dr, rug_to_i(&rr)); + } + + #[test] + #[ignore] + fn ibig_pow(a in fuzz::ibig_strategy(3), n in 0u32..=12) { + let d = a.pow(n as usize); + let r = rugc(i_to_rug(&a).pow(n)); + prop_assert_eq!(d, rug_to_i(&r)); + } + + #[test] + #[ignore] + fn ibig_bit_ops(a in fuzz::ibig_strategy(4), b in fuzz::ibig_strategy(4)) { + let (ar, br) = (i_to_rug(&a), i_to_rug(&b)); + prop_assert_eq!(&a & &b, rug_to_i(&rugc(&ar & &br))); + prop_assert_eq!(&a | &b, rug_to_i(&rugc(&ar | &br))); + prop_assert_eq!(&a ^ &b, rug_to_i(&rugc(&ar ^ &br))); + } + + #[test] + #[ignore] + fn ibig_shifts(a in fuzz::ibig_strategy(4), n in 0u32..=200) { + let ar = i_to_rug(&a); + prop_assert_eq!(&a << (n as usize), rug_to_i(&rugc(&ar << n))); + prop_assert_eq!(&a >> (n as usize), rug_to_i(&rugc(&ar >> n))); + } +} diff --git a/fuzz/tests/ratio.rs b/fuzz/tests/ratio.rs new file mode 100644 index 00000000..0862c2af --- /dev/null +++ b/fuzz/tests/ratio.rs @@ -0,0 +1,110 @@ +//! Differential / fuzz tests for `dashu-ratio` (`RBig`) against `rug::Rational` (GMP mpq). +//! +//! Rational ops are EXACT (no rounding), so the comparison is exact value equality: build a +//! `rug::Rational` from each side's canonical `(numerator, denominator)`, compute the op, and assert +//! the two `rug::Rational` results are equal (both canonical). Proptest-driven; all `#[ignore]`d +//! (manual, release-time). +//! +//! Run with: `cargo test --manifest-path fuzz/Cargo.toml --test ratio -- --ignored --nocapture` + +use dashu::base::Inverse; +use dashu::rational::RBig; +use proptest::prelude::*; +use rug::ops::Pow; + +/// Mirror a (canonical) dashu `RBig` into a `rug::Rational` via decimal num/den strings. +fn rbig_to_rug(r: &RBig) -> rug::Rational { + let n = r.numerator().to_string().parse::().unwrap(); + let d = r.denominator().to_string().parse::().unwrap(); + rug::Rational::from((n, d)) +} + +/// Complete a rug `…Incomplete` (or owned `Rational`) into a `Rational` via `Assign`. +fn rugc_r(src: S) -> rug::Rational +where + rug::Rational: rug::Assign, +{ + let mut r = rug::Rational::new(); + rug::Assign::assign(&mut r, src); + r +} + +fn rbig_strategy() -> impl Strategy { + ( + fuzz::ibig_strategy(3), + fuzz::ubig_strategy(2).prop_filter("nonzero denominator", |d| !d.is_zero()), + ) + .prop_map(|(n, d)| RBig::from_parts(n, d)) +} + +proptest! { + #![proptest_config(fuzz::fuzz_config())] + + #[test] + #[ignore] + fn ratio_add(a in rbig_strategy(), b in rbig_strategy()) { + let (ra, rb) = (rbig_to_rug(&a), rbig_to_rug(&b)); + let d = &a + &b; + prop_assert!(rbig_to_rug(&d) == rugc_r(&ra + &rb)); + } + + #[test] + #[ignore] + fn ratio_sub(a in rbig_strategy(), b in rbig_strategy()) { + let (ra, rb) = (rbig_to_rug(&a), rbig_to_rug(&b)); + let d = &a - &b; + prop_assert!(rbig_to_rug(&d) == rugc_r(&ra - &rb)); + } + + #[test] + #[ignore] + fn ratio_mul(a in rbig_strategy(), b in rbig_strategy()) { + let (ra, rb) = (rbig_to_rug(&a), rbig_to_rug(&b)); + let d = &a * &b; + prop_assert!(rbig_to_rug(&d) == rugc_r(&ra * &rb)); + } + + #[test] + #[ignore] + fn ratio_div(a in rbig_strategy(), b in rbig_strategy().prop_filter("nonzero value", |b| !b.numerator().is_zero())) { + let (ra, rb) = (rbig_to_rug(&a), rbig_to_rug(&b)); + let d = &a / &b; + prop_assert!(rbig_to_rug(&d) == rugc_r(&ra / &rb)); + } + + #[test] + #[ignore] + fn ratio_sqr(a in rbig_strategy()) { + let ra = rbig_to_rug(&a); + let d = a.sqr(); + prop_assert!(rbig_to_rug(&d) == rugc_r(&ra * &ra)); + } + + #[test] + #[ignore] + fn ratio_pow(a in rbig_strategy(), n in 0u32..=12) { + let ra = rbig_to_rug(&a); + let d = a.pow(n as usize); + prop_assert!(rbig_to_rug(&d) == rugc_r(ra.pow(n))); + } + + #[test] + #[ignore] + fn ratio_inv(a in rbig_strategy().prop_filter("nonzero value", |a| !a.numerator().is_zero())) { + let ra = rbig_to_rug(&a); + let d = a.inv(); + prop_assert!(rbig_to_rug(&d) == rugc_r(ra.recip())); + } + + /// `from_parts` reduces to the same canonical form as GMP. + #[test] + #[ignore] + fn ratio_reduce(num in fuzz::ibig_strategy(3), den in fuzz::ubig_strategy(2).prop_filter("nonzero", |d| !d.is_zero())) { + let r = RBig::from_parts(num.clone(), den.clone()); + // canonical: denominator > 0, gcd(|num|, den) == 1 — check against GMP's reduction + let rug_n = num.to_string().parse::().unwrap(); + let rug_d = den.to_string().parse::().unwrap(); + let rr = rug::Rational::from((rug_n, rug_d)); + prop_assert!(rbig_to_rug(&r) == rr); + } +} diff --git a/fuzz/tests/transcendental.rs b/fuzz/tests/transcendental.rs new file mode 100644 index 00000000..28b6f4d4 --- /dev/null +++ b/fuzz/tests/transcendental.rs @@ -0,0 +1,365 @@ +//! Differential / fuzz tests for dashu-float's non-trig transcendentals against `rug::Float` (MPFR). +//! +//! Companion to `trig_random.rs` (which covers sin/cos/tan/atan2/asin/acos/π). Here: exp, exp_m1, +//! ln, ln_1p, sqrt, cbrt, nth_root, hypot, atan, powf, powi, sinh, cosh, sinh_cosh, tanh, asinh, acosh, atanh. +//! Proptest-driven so a mismatch shrinks to a minimal counterexample; all `#[ignore]`d (manual, +//! release-time — they link `rug` and run long). Tolerance is `within_k_ulps(2)`: dashu is +//! near-correctly-rounded (guard digits), MPFR is Ziv-correct, so a ≤1-ulp divergence is legitimate +//! and `k=2` leaves margin; a >2-ulp divergence is a real bug to investigate. +//! +//! Run with: `cargo test --manifest-path fuzz/Cargo.toml --test transcendental -- --ignored --nocapture` + +use core::str::FromStr; +use dashu::float::ops::Abs; +use dashu::float::round::mode::HalfAway; +use dashu::float::{Context, DBig, Repr}; +use dashu::integer::IBig; +use proptest::prelude::*; +use rug::Float; +use rug::ops::Pow; + +/// MPFR working precision (bits) sufficient to hold `x` and a `prec`-digit result with margin. +fn rug_bits(x: &Repr<10>, prec: usize) -> u32 { + let x_mag = (x.exponent().unsigned_abs() + x.digits()) as f64; + let x_bits = (x_mag * 3.322).ceil() as u32 + 500; + let p_bits = ((prec.max(100) as f64) * 3.322).ceil() as u32; + p_bits + x_bits +} + +/// |dashu - rug| ≤ `k` ulps at dashu's precision. +fn within_k_ulps(d: &DBig, r: &DBig, k: i32) -> bool { + let diff = (d.clone() - r).abs(); + // Exact agreement → no need to inspect ulps (also avoids .ulp() on + // unlimited-precision constants like `FBig::ONE` from powi(x,0)=1). + if diff.repr().significand().is_zero() { + return true; + } + diff <= d.ulp() * k +} + +/// Unwrap a `FpResult` to its `FBig` value, or skip the whole case (`return Ok(())`) on error. +macro_rules! dashu_ok { + ($e:expr) => { + match $e { + Ok(v) => v.value(), + Err(_) => return Ok(()), + } + }; +} + +/// `x ∈ [-100, 100]` as `n/100` — bounded magnitude for exp/exp_m1/sinh/cosh/tanh so the result +/// doesn't overflow to infinity (which would skip the comparison). +fn small_x() -> impl Strategy { + (-10000i32..=10000) + .prop_map(|n| DBig::from_repr(Repr::<10>::new(n.into(), -2), Context::::new(0))) +} + +/// `x ∈ (lo, lo + 100]` — for ln_1p (lo = -1) so the domain `x > -1` holds. +fn small_x_above(lo: i32) -> impl Strategy { + let lo = lo * 100; + (1 + lo..=10000 + lo) + .prop_map(|n| DBig::from_repr(Repr::<10>::new(n.into(), -2), Context::::new(0))) +} + +fn rug_at(x_str: &str, bits: u32) -> Option { + match Float::parse(x_str) { + Ok(p) => Some(Float::with_val(bits, p)), + Err(_) => None, + } +} + +proptest! { + #![proptest_config(fuzz::fuzz_config())] + + /// exp(x) ≈ MPFR exp(x). + #[test] + #[ignore] + fn exp_fuzz(x in small_x()) { + let xs = format!("{x:e}"); + for prec in [20usize, 50, 100] { + let ctx = Context::::new(prec); + let d = dashu_ok!(ctx.exp::<10>(x.repr(), None)); + if d.repr().is_infinite() { continue; } + let xr = rug_at(&xs, rug_bits(x.repr(), prec)).unwrap(); + let r: DBig = DBig::from_str(&xr.exp().to_string_radix(10, Some(prec))).unwrap(); + prop_assert!(within_k_ulps(&d, &r, 2), "exp x={xs} prec={prec}: dashu={d} rug={r}"); + } + } + + + /// exp(x) − 1 ≈ MPFR exp_m1 (cancellation-free near zero). + #[test] + #[ignore] + fn exp_m1_fuzz(x in small_x()) { + let xs = format!("{x:e}"); + for prec in [20usize, 50, 100] { + let ctx = Context::::new(prec); + let d = dashu_ok!(ctx.exp_m1::<10>(x.repr(), None)); + if d.repr().is_infinite() { continue; } + let xr = rug_at(&xs, rug_bits(x.repr(), prec)).unwrap(); + let r: DBig = DBig::from_str(&xr.exp_m1().to_string_radix(10, Some(prec))).unwrap(); + prop_assert!(within_k_ulps(&d, &r, 2), "exp_m1 x={xs} prec={prec}: dashu={d} rug={r}"); + } + } + + + /// ln(x) ≈ MPFR ln(x), x > 0. + #[test] + #[ignore] + fn ln_fuzz(x in fuzz::pos_dbig_strategy(-50..=50)) { + let xs = format!("{x:e}"); + for prec in [20usize, 50, 100] { + let ctx = Context::::new(prec); + let d = dashu_ok!(ctx.ln::<10>(x.repr(), None)); + if d.repr().is_infinite() { continue; } + let xr = rug_at(&xs, rug_bits(x.repr(), prec)).unwrap(); + let r: DBig = DBig::from_str(&xr.ln().to_string_radix(10, Some(prec))).unwrap(); + prop_assert!(within_k_ulps(&d, &r, 2), "ln x={xs} prec={prec}: dashu={d} rug={r}"); + } + } + + + /// ln(1 + x) ≈ MPFR ln_1p, x > −1. + #[test] + #[ignore] + fn ln_1p_fuzz(x in small_x_above(-1)) { + let xs = format!("{x:e}"); + for prec in [20usize, 50, 100] { + let ctx = Context::::new(prec); + let d = dashu_ok!(ctx.ln_1p::<10>(x.repr(), None)); + if d.repr().is_infinite() { continue; } + let xr = rug_at(&xs, rug_bits(x.repr(), prec)).unwrap(); + let r: DBig = DBig::from_str(&xr.ln_1p().to_string_radix(10, Some(prec))).unwrap(); + prop_assert!(within_k_ulps(&d, &r, 2), "ln_1p x={xs} prec={prec}: dashu={d} rug={r}"); + } + } + + + /// sqrt(x) ≈ MPFR sqrt(x), x ≥ 0. + #[test] + #[ignore] + fn sqrt_fuzz(x in fuzz::pos_dbig_strategy(-50..=50)) { + let xs = format!("{x:e}"); + for prec in [20usize, 50, 100] { + let ctx = Context::::new(prec); + let d = dashu_ok!(ctx.sqrt::<10>(x.repr())); + let xr = rug_at(&xs, rug_bits(x.repr(), prec)).unwrap(); + let r: DBig = DBig::from_str(&xr.sqrt().to_string_radix(10, Some(prec))).unwrap(); + prop_assert!(within_k_ulps(&d, &r, 2), "sqrt x={xs} prec={prec}: dashu={d} rug={r}"); + } + } + + + /// cbrt(x) ≈ MPFR cbrt(x), all real. + #[test] + #[ignore] + fn cbrt_fuzz(x in fuzz::dbig_strategy(-50..=50)) { + let xs = format!("{x:e}"); + for prec in [20usize, 50, 100] { + let ctx = Context::::new(prec); + let d = dashu_ok!(ctx.cbrt::<10>(x.repr())); + let xr = rug_at(&xs, rug_bits(x.repr(), prec)).unwrap(); + let r: DBig = DBig::from_str(&xr.cbrt().to_string_radix(10, Some(prec))).unwrap(); + prop_assert!(within_k_ulps(&d, &r, 2), "cbrt x={xs} prec={prec}: dashu={d} rug={r}"); + } + } + + + /// nth_root(n, x) ≈ MPFR root(n), x > 0, n ∈ 2..=6. + #[test] + #[ignore] + fn nth_root_fuzz(x in fuzz::pos_dbig_strategy(-50..=50), n in 2u32..=6) { + let xs = format!("{x:e}"); + for prec in [20usize, 50, 100] { + let ctx = Context::::new(prec); + let d = dashu_ok!(ctx.nth_root::<10>(n as usize, x.repr())); + let xr = rug_at(&xs, rug_bits(x.repr(), prec)).unwrap(); + let r: DBig = DBig::from_str(&xr.root(n).to_string_radix(10, Some(prec))).unwrap(); + prop_assert!(within_k_ulps(&d, &r, 2), "nth_root n={n} x={xs} prec={prec}: dashu={d} rug={r}"); + } + } + + + /// hypot(a, b) = sqrt(a² + b²) ≈ MPFR (computed as such; inputs bounded so no overflow). + #[test] + #[ignore] + fn hypot_fuzz(a in small_x(), b in small_x()) { + let (as_, bs) = (format!("{a:e}"), format!("{b:e}")); + for prec in [20usize, 50, 100] { + let ctx = Context::::new(prec); + let d = dashu_ok!(ctx.hypot::<10>(a.repr(), b.repr())); + let bits = rug_bits(a.repr(), prec).max(rug_bits(b.repr(), prec)); + let ar = rug_at(&as_, bits).unwrap(); + let br = rug_at(&bs, bits).unwrap(); + let hr = (ar.pow(2u32) + br.pow(2u32)).sqrt(); + let r: DBig = DBig::from_str(&hr.to_string_radix(10, Some(prec))).unwrap(); + prop_assert!(within_k_ulps(&d, &r, 2), "hypot a={as_} b={bs} prec={prec}: dashu={d} rug={r}"); + } + } + + + /// atan(x) ≈ MPFR atan(x), all real. + #[test] + #[ignore] + fn atan_fuzz(x in fuzz::dbig_strategy(-50..=50)) { + let xs = format!("{x:e}"); + for prec in [20usize, 50, 100] { + let ctx = Context::::new(prec); + let d = dashu_ok!(ctx.atan::<10>(x.repr(), None)); + let xr = rug_at(&xs, rug_bits(x.repr(), prec)).unwrap(); + let r: DBig = DBig::from_str(&xr.atan().to_string_radix(10, Some(prec))).unwrap(); + prop_assert!(within_k_ulps(&d, &r, 2), "atan x={xs} prec={prec}: dashu={d} rug={r}"); + } + } + + + /// base^exp ≈ MPFR pow, base > 0. + #[test] + #[ignore] + fn powf_fuzz(base in fuzz::pos_dbig_strategy(-5..=5), exp in small_x()) { + let (bs, es) = (format!("{base:e}"), format!("{exp:e}")); + for prec in [20usize, 50, 100] { + let ctx = Context::::new(prec); + let d = dashu_ok!(ctx.powf::<10>(base.repr(), exp.repr(), None)); + if d.repr().is_infinite() { continue; } + let bits = rug_bits(base.repr(), prec).max(rug_bits(exp.repr(), prec)); + let br = rug_at(&bs, bits).unwrap(); + let er = rug_at(&es, bits).unwrap(); + let r: DBig = DBig::from_str(&br.pow(&er).to_string_radix(10, Some(prec))).unwrap(); + prop_assert!(within_k_ulps(&d, &r, 2), "powf base={bs} exp={es} prec={prec}: dashu={d} rug={r}"); + } + } + + + /// base^n ≈ MPFR pow(n), n ∈ 0..=16 (rug takes u32). + #[test] + #[ignore] + fn powi_fuzz(base in fuzz::dbig_strategy(-20..=20), n in 0u32..=16) { + let bs = format!("{base:e}"); + for prec in [20usize, 50, 100] { + let ctx = Context::::new(prec); + let d = dashu_ok!(ctx.powi::<10>(base.repr(), IBig::from(n))); + if d.repr().is_infinite() { continue; } + let br = rug_at(&bs, rug_bits(base.repr(), prec)).unwrap(); + let r: DBig = DBig::from_str(&br.pow(n).to_string_radix(10, Some(prec))).unwrap(); + prop_assert!(within_k_ulps(&d, &r, 2), "powi base={bs} n={n} prec={prec}: dashu={d} rug={r}"); + } + } + + + /// sinh(x) ≈ MPFR sinh(x). + #[test] + #[ignore] + fn sinh_fuzz(x in small_x()) { + let xs = format!("{x:e}"); + for prec in [20usize, 50, 100] { + let ctx = Context::::new(prec); + let d = dashu_ok!(ctx.sinh::<10>(x.repr(), None)); + if d.repr().is_infinite() { continue; } + let xr = rug_at(&xs, rug_bits(x.repr(), prec)).unwrap(); + let r: DBig = DBig::from_str(&xr.sinh().to_string_radix(10, Some(prec))).unwrap(); + prop_assert!(within_k_ulps(&d, &r, 2), "sinh x={xs} prec={prec}: dashu={d} rug={r}"); + } + } + + + /// cosh(x) ≈ MPFR cosh(x). + #[test] + #[ignore] + fn cosh_fuzz(x in small_x()) { + let xs = format!("{x:e}"); + for prec in [20usize, 50, 100] { + let ctx = Context::::new(prec); + let d = dashu_ok!(ctx.cosh::<10>(x.repr(), None)); + if d.repr().is_infinite() { continue; } + let xr = rug_at(&xs, rug_bits(x.repr(), prec)).unwrap(); + let r: DBig = DBig::from_str(&xr.cosh().to_string_radix(10, Some(prec))).unwrap(); + prop_assert!(within_k_ulps(&d, &r, 2), "cosh x={xs} prec={prec}: dashu={d} rug={r}"); + } + } + + + /// tanh(x) ≈ MPFR tanh(x). + #[test] + #[ignore] + fn tanh_fuzz(x in small_x()) { + let xs = format!("{x:e}"); + for prec in [20usize, 50, 100] { + let ctx = Context::::new(prec); + let d = dashu_ok!(ctx.tanh::<10>(x.repr(), None)); + let xr = rug_at(&xs, rug_bits(x.repr(), prec)).unwrap(); + let r: DBig = DBig::from_str(&xr.tanh().to_string_radix(10, Some(prec))).unwrap(); + prop_assert!(within_k_ulps(&d, &r, 2), "tanh x={xs} prec={prec}: dashu={d} rug={r}"); + } + } + + + /// sinh_cosh(x) ≈ (MPFR sinh(x), MPFR cosh(x)). + #[test] + #[ignore] + fn sinh_cosh_fuzz(x in small_x()) { + let xs = format!("{x:e}"); + for prec in [20usize, 50, 100] { + let ctx = Context::::new(prec); + let (ds, dc) = ctx.sinh_cosh::<10>(x.repr(), None); + let d_sinh = dashu_ok!(ds); + let d_cosh = dashu_ok!(dc); + if d_sinh.repr().is_infinite() || d_cosh.repr().is_infinite() { continue; } + let bits = rug_bits(x.repr(), prec); + let r_sinh: DBig = + DBig::from_str(&rug_at(&xs, bits).unwrap().sinh().to_string_radix(10, Some(prec))).unwrap(); + let r_cosh: DBig = + DBig::from_str(&rug_at(&xs, bits).unwrap().cosh().to_string_radix(10, Some(prec))).unwrap(); + prop_assert!(within_k_ulps(&d_sinh, &r_sinh, 2), "sinh_cosh sinh x={xs} prec={prec}: dashu={d_sinh} rug={r_sinh}"); + prop_assert!(within_k_ulps(&d_cosh, &r_cosh, 2), "sinh_cosh cosh x={xs} prec={prec}: dashu={d_cosh} rug={r_cosh}"); + } + } + + + /// asinh(x) ≈ MPFR asinh(x), all real. + #[test] + #[ignore] + fn asinh_fuzz(x in fuzz::dbig_strategy(-50..=50)) { + let xs = format!("{x:e}"); + for prec in [20usize, 50, 100] { + let ctx = Context::::new(prec); + let d = dashu_ok!(ctx.asinh::<10>(x.repr(), None)); + let xr = rug_at(&xs, rug_bits(x.repr(), prec)).unwrap(); + let r: DBig = DBig::from_str(&xr.asinh().to_string_radix(10, Some(prec))).unwrap(); + prop_assert!(within_k_ulps(&d, &r, 2), "asinh x={xs} prec={prec}: dashu={d} rug={r}"); + } + } + + + /// acosh(x) ≈ MPFR acosh(x), x ≥ 1 (pos_dbig_strategy(0..=50) keeps x ≥ 1). + #[test] + #[ignore] + fn acosh_fuzz(x in fuzz::pos_dbig_strategy(0..=50)) { + let xs = format!("{x:e}"); + for prec in [20usize, 50, 100] { + let ctx = Context::::new(prec); + let d = dashu_ok!(ctx.acosh::<10>(x.repr(), None)); + let xr = rug_at(&xs, rug_bits(x.repr(), prec)).unwrap(); + let r: DBig = DBig::from_str(&xr.acosh().to_string_radix(10, Some(prec))).unwrap(); + prop_assert!(within_k_ulps(&d, &r, 2), "acosh x={xs} prec={prec}: dashu={d} rug={r}"); + } + } + + + /// atanh(x) ≈ MPFR atanh(x), |x| < 1 (unit_dbig is [-1,1]; the ±1 endpoints yield ±∞ and are + /// skipped by the `is_infinite` check inside the loop). + #[test] + #[ignore] + fn atanh_fuzz(x in fuzz::unit_dbig()) { + let xs = format!("{x:e}"); + for prec in [20usize, 50, 100] { + let ctx = Context::::new(prec); + let d = dashu_ok!(ctx.atanh::<10>(x.repr(), None)); + if d.repr().is_infinite() { continue; } + let xr = rug_at(&xs, rug_bits(x.repr(), prec)).unwrap(); + let r: DBig = DBig::from_str(&xr.atanh().to_string_radix(10, Some(prec))).unwrap(); + prop_assert!(within_k_ulps(&d, &r, 2), "atanh x={xs} prec={prec}: dashu={d} rug={r}"); + } + } + +} diff --git a/fuzz/tests/trig_random.rs b/fuzz/tests/trig_random.rs index 9637369d..6585251b 100644 --- a/fuzz/tests/trig_random.rs +++ b/fuzz/tests/trig_random.rs @@ -1,356 +1,217 @@ +//! Differential / fuzz tests for dashu-float transcendentals against `rug::Float` (MPFR). +//! +//! The broad random differentials (sin/cos/tan/atan2/asin/acos) are proptest-driven so a mismatch +//! shrinks to a minimal counterexample; the inherently-sweep tests (π over precision, asin near 1, +//! the pinned large-exponent tan regression) stay as deterministic loops. All are `#[ignore]`d and +//! run manually before a release. +//! +//! Run with: `cargo test --manifest-path fuzz/Cargo.toml --test trig_random -- --ignored --nocapture` + use core::str::FromStr; -use dashu_float::math::FpResult; -use dashu_float::ops::Abs; -use dashu_float::round::mode::HalfEven; -use dashu_float::{DBig, FBig}; -use rand::prelude::*; +use dashu::float::ops::Abs; +use dashu::float::round::mode::HalfAway; +use dashu::float::{Context, DBig, Repr}; +use proptest::prelude::*; use rug::Float; -/// Reproduction case for a bug discovered during fuzzing where very small -/// numbers with many digits triggered an assertion failure in the rounding logic. -#[test] -#[ignore] -fn test_reproduce_assertion_failure() { - let x_str = "-5.525474318981006776603409487767135633516667011547942409467e-3"; - let prec = 100; - let x_dashu = DBig::from_str(x_str).unwrap().with_rounding::(); - let dashu_ctx = dashu_float::Context::::new(prec); - let _sin_d = dashu_ctx.sin(x_dashu.repr()).value(&dashu_ctx); +/// MPFR working precision (bits) large enough to hold `x` and the result to `prec` decimal digits +/// with margin: `(|exponent| + significand_digits)·log₂10` for `x`'s magnitude + `prec·log₂10` + slack. +fn rug_bits(x: &Repr<10>, prec: usize) -> u32 { + let x_mag = (x.exponent().unsigned_abs() + x.digits()) as f64; + let x_bits = (x_mag * 3.322).ceil() as u32 + 500; + let p_bits = ((prec.max(100) as f64) * 3.322).ceil() as u32; + p_bits + x_bits } -#[test] -#[ignore] -fn test_pi_fuzz() { - for prec in (10..1000).step_by(53) { - let pi_dashu = DBig::pi(prec).with_rounding::(); - let bits = (prec * 3322).div_ceil(1000) + 32; - let pi_rug = Float::with_val(bits as u32, rug::float::Constant::Pi); - let s_r_val = DBig::from_str(&pi_rug.to_string_radix(10, Some(prec))) - .unwrap() - .with_rounding::(); - assert!( - (pi_dashu.clone() - s_r_val).abs() - <= DBig::from_parts(10.into(), -(isize::try_from(prec).unwrap())), - "Pi mismatch at prec={prec}: dashu={pi_dashu}, rug={pi_rug}" - ); - } +/// Tolerance of `100 · 10^{-prec}` (~100 ulp at `prec` decimal digits) — both libraries are +/// near-/correctly-rounded, so a few-ulp divergence is expected; this catches real bugs. +fn tol(prec: usize) -> DBig { + DBig::from_parts(100.into(), -(prec as isize)) } -/// Generates a truly arbitrary `DBig` value for testing. -fn random_dbig(rng: &mut R, large_exp: bool) -> DBig { - let sign = if rng.random_bool(0.5) { 1 } else { -1 }; - let num_digits = rng.random_range(1..100); - let mut s = String::new(); - if sign == -1 { - s.push('-'); - } - for _ in 0..num_digits { - s.push(char::from_digit(rng.random_range(0..10), 10).unwrap()); +proptest! { + #![proptest_config(fuzz::fuzz_config())] + + /// sin(x) ≈ MPFR sin(x) across precisions {10, 20, 50, 100}. + #[test] + #[ignore] + fn sin_fuzz(x in fuzz::dbig_strategy(-50..=50)) { + let x_str = format!("{x:e}"); + for prec in [10usize, 20, 50, 100] { + let ctx = Context::::new(prec); + let sin_d = ctx.sin::<10>(x.repr(), None).unwrap().value(); + let bits = rug_bits(x.repr(), prec); + let x_rug = match Float::parse(&x_str) { + Ok(p) => Float::with_val(bits, p), + Err(_) => return Ok(()), + }; + let sin_r = x_rug.sin(); + let s_r: DBig = DBig::from_str(&sin_r.to_string_radix(10, Some(prec))).unwrap(); + prop_assert!( + (sin_d.clone() - s_r).abs() <= tol(prec), + "sin mismatch x={x_str} prec={prec}: dashu={sin_d} rug={sin_r}" + ); + } } - let exponent = if large_exp { - rng.random_range(-2000..2000) - } else { - rng.random_range(-10..10) - }; - s.push_str(&format!("e{exponent}")); - DBig::from_str(&s).unwrap_or(DBig::ZERO) -} - -#[test] -#[ignore] -fn test_trig_fuzz_comprehensive() { - let mut rng = StdRng::seed_from_u64(42); - let precisions = [10, 20, 50, 100]; - for i in 0..2000 { - let x_dashu = random_dbig(&mut rng, true).with_rounding::(); - let x_str = format!("{x_dashu:e}"); - - for &prec in &precisions { - let dashu_ctx = dashu_float::Context::::new(prec); - let x_f_repr = x_dashu.repr().clone(); - - // Sin - let sin_d = - match std::panic::catch_unwind(|| dashu_ctx.sin(&x_f_repr).value(&dashu_ctx)) { - Ok(v) => v, - Err(_) => { - panic!("PANIC at iteration {i}, prec {prec}, x = {x_str}"); - } - }; - - // Rug baseline - let x_bits = ((x_dashu.repr().exponent().abs() as f64 * 3.322).ceil() as u32) + 500; - let bits = (((prec as f64).max(100.0) * 3.322).ceil() as u32) + x_bits; + /// cos(x) ≈ MPFR cos(x) across precisions {10, 20, 50, 100}. + #[test] + #[ignore] + fn cos_fuzz(x in fuzz::dbig_strategy(-50..=50)) { + let x_str = format!("{x:e}"); + for prec in [10usize, 20, 50, 100] { + let ctx = Context::::new(prec); + let cos_d = ctx.cos::<10>(x.repr(), None).unwrap().value(); + let bits = rug_bits(x.repr(), prec); let x_rug = match Float::parse(&x_str) { - Ok(parsed) => Float::with_val(bits, parsed), - Err(_) => continue, + Ok(p) => Float::with_val(bits, p), + Err(_) => return Ok(()), }; - - let sin_r = x_rug.clone().sin(); - let s_r_val = DBig::from_str(&sin_r.to_string_radix(10, Some(prec))) - .unwrap() - .with_rounding::(); - assert!( - (sin_d.clone() - s_r_val).abs() - <= DBig::from_parts(100.into(), -(isize::try_from(prec).unwrap())), - "Sin mismatch at iteration {i}, x={x_str}, prec={prec}: dashu={sin_d}, rug={sin_r}" + let cos_r = x_rug.cos(); + let c_r: DBig = DBig::from_str(&cos_r.to_string_radix(10, Some(prec))).unwrap(); + prop_assert!( + (cos_d.clone() - c_r).abs() <= tol(prec), + "cos mismatch x={x_str} prec={prec}: dashu={cos_d} rug={cos_r}" ); } } -} - -#[test] -#[ignore] -fn test_atan2_fuzz_comprehensive() { - let mut rng = StdRng::seed_from_u64(45); - let precisions = [20, 50]; - for i in 0..500 { - let y_dashu = random_dbig(&mut rng, true).with_rounding::(); - let x_dashu = random_dbig(&mut rng, true).with_rounding::(); - let y_str = format!("{y_dashu:e}"); - let x_str = format!("{x_dashu:e}"); - - for &prec in &precisions { - let dashu_ctx = dashu_float::Context::::new(prec); - - let atan2_d = std::panic::catch_unwind(|| { - dashu_ctx - .atan2(y_dashu.repr(), x_dashu.repr()) - .value(&dashu_ctx) - }) - .unwrap_or_else(|_| { - panic!("PANIC at iteration {i}, prec {prec}, y = {y_str}, x = {x_str}"); - }); + /// tan(x) ≈ MPFR tan(x), skipping arguments where |cos(x)| < 1e-5 (too close to a singularity). + #[test] + #[ignore] + fn tan_fuzz(x in fuzz::dbig_strategy(-50..=50)) { + let x_str = format!("{x:e}"); + for prec in [10usize, 20, 50, 100] { + let ctx = Context::::new(prec); + let cos_d = ctx.cos::<10>(x.repr(), None).unwrap().value(); + if cos_d.abs() <= DBig::from_parts(1.into(), -5) { + continue; // near a singularity — tan is ill-conditioned, skip this precision + } + let tan_d = ctx.tan::<10>(x.repr(), None).unwrap().value(); + let bits = rug_bits(x.repr(), prec); + let x_rug = match Float::parse(&x_str) { + Ok(p) => Float::with_val(bits, p), + Err(_) => return Ok(()), + }; + let tan_r = x_rug.tan(); + let t_r: DBig = DBig::from_str(&tan_r.to_string_radix(10, Some(prec))).unwrap(); + prop_assert!( + (tan_d.clone() - t_r).abs() <= tol(prec), + "tan mismatch x={x_str} prec={prec}: dashu={tan_d} rug={tan_r}" + ); + } + } - let bits = (u32::try_from(prec).unwrap() * 4) + 1000; + /// atan2(y, x) ≈ MPFR atan2(y, x) across precisions {20, 50}. + #[test] + #[ignore] + fn atan2_fuzz(y in fuzz::dbig_strategy(-50..=50), x in fuzz::dbig_strategy(-50..=50)) { + let y_str = format!("{y:e}"); + let x_str = format!("{x:e}"); + for prec in [20usize, 50] { + let ctx = Context::::new(prec); + // atan2(0,0) (and other indeterminate forms) report FpError — skip those; nothing to + // compare. Finite in-domain inputs never error here. + let atan2_d = match ctx.atan2::<10>(y.repr(), x.repr(), None) { + Ok(v) => v.value(), + Err(_) => return Ok(()), + }; + let bits = (rug_bits(y.repr(), prec)).max(rug_bits(x.repr(), prec)); let y_rug = Float::with_val(bits, Float::parse(&y_str).unwrap()); let x_rug = Float::with_val(bits, Float::parse(&x_str).unwrap()); let atan2_r = y_rug.atan2(&x_rug); - - let a_r_val = DBig::from_str(&atan2_r.to_string_radix(10, Some(prec))) - .unwrap() - .with_rounding::(); - assert!( - (atan2_d.clone() - a_r_val).abs() - <= DBig::from_parts(100.into(), -(isize::try_from(prec).unwrap())), - "Atan2 mismatch at iteration {i}, y={y_str}, x={x_str}, prec={prec}: dashu={atan2_d}, rug={atan2_r}" + let a_r: DBig = DBig::from_str(&atan2_r.to_string_radix(10, Some(prec))).unwrap(); + prop_assert!( + (atan2_d.clone() - a_r).abs() <= tol(prec), + "atan2 mismatch y={y_str} x={x_str} prec={prec}: dashu={atan2_d} rug={atan2_r}" ); } } -} -/// Generates a random `DBig` within [min, max] range. -fn random_dbig_range(rng: &mut R, min: f64, max: f64) -> DBig { - let val: f64 = rng.random_range(min..max); - DBig::from_str(&format!("{val:.15}")).unwrap() -} - -#[test] -#[ignore] -fn test_inv_trig_fuzz() { - let mut rng = StdRng::seed_from_u64(43); - let precisions = [20, 50]; - - for i in 0..200 { - // Test asin/acos within [-1, 1] - let x_dashu = random_dbig_range(&mut rng, -1.0, 1.0).with_rounding::(); - let x_str = format!("{x_dashu:e}"); - - for &prec in &precisions { - let dashu_ctx = dashu_float::Context::::new(prec); - - // Asin - let asin_d = dashu_ctx.asin(x_dashu.repr()).value(&dashu_ctx); - let bits = (u32::try_from(prec).unwrap() * 4) + 128; + /// asin(x)/acos(x) ≈ MPFR for x in [-1, 1] across precisions {20, 50}. + #[test] + #[ignore] + fn inv_trig_fuzz(x in fuzz::unit_dbig()) { + let x_str = format!("{x:e}"); + for prec in [20usize, 50] { + let ctx = Context::::new(prec); + let bits = (prec as u32) * 4 + 128; let x_rug = Float::with_val(bits, Float::parse(&x_str).unwrap()); + + let asin_d = ctx.asin::<10>(x.repr(), None).unwrap().value(); let asin_r = x_rug.clone().asin(); - let a_r_val = DBig::from_str(&asin_r.to_string_radix(10, Some(prec))) - .unwrap() - .with_rounding::(); - assert!( - (asin_d.clone() - a_r_val).abs() - <= DBig::from_parts(100.into(), -(isize::try_from(prec).unwrap())), - "Asin mismatch at iteration {i}, x={x_str}, prec={prec}: dashu={asin_d}, rug={asin_r}" + let a_r: DBig = DBig::from_str(&asin_r.to_string_radix(10, Some(prec))).unwrap(); + prop_assert!( + (asin_d.clone() - a_r).abs() <= tol(prec), + "asin mismatch x={x_str} prec={prec}: dashu={asin_d} rug={asin_r}" ); - // Acos - let acos_d = dashu_ctx.acos(x_dashu.repr()).value(&dashu_ctx); + let acos_d = ctx.acos::<10>(x.repr(), None).unwrap().value(); let acos_r = x_rug.acos(); - let a_r_val = DBig::from_str(&acos_r.to_string_radix(10, Some(prec))) - .unwrap() - .with_rounding::(); - assert!( - (acos_d.clone() - a_r_val).abs() - <= DBig::from_parts(100.into(), -(isize::try_from(prec).unwrap())), - "Acos mismatch at iteration {i}, x={x_str}, prec={prec}: dashu={acos_d}, rug={acos_r}" + let a_r: DBig = DBig::from_str(&acos_r.to_string_radix(10, Some(prec))).unwrap(); + prop_assert!( + (acos_d.clone() - a_r).abs() <= tol(prec), + "acos mismatch x={x_str} prec={prec}: dashu={acos_d} rug={acos_r}" ); } } } +/// π at every precision matches MPFR's π to within 1 ulp. (Deterministic precision sweep.) #[test] #[ignore] -fn test_edge_cases_fuzz() { - let mut rng = StdRng::seed_from_u64(46); - let precisions = [30, 100]; - - for _ in 0..50 { - for &prec in &precisions { - let dashu_ctx = dashu_float::Context::::new(prec); - - // Numbers very close to 1.0 (test asin/acos precision) - let epsilon = 10.0f64.powi(-(rng.random_range(1..15))); - let x_val = 1.0 - epsilon; - let x_dashu = DBig::from_str(&format!("{x_val:.16}")) - .unwrap() - .with_rounding::(); - let x_str = format!("{x_dashu:e}"); +fn pi_fuzz() { + for prec in (10..1000).step_by(53) { + let pi_dashu = DBig::pi(prec); + let bits = (prec * 3322).div_ceil(1000) + 32; + let pi_rug = Float::with_val(bits as u32, rug::float::Constant::Pi); + let s_r: DBig = DBig::from_str(&pi_rug.to_string_radix(10, Some(prec))).unwrap(); + assert!( + (pi_dashu.clone() - s_r).abs() <= DBig::from_parts(1.into(), -(prec as isize)), + "Pi mismatch at prec={prec}: dashu={pi_dashu}, rug={pi_rug}" + ); + } +} - let asin_d = dashu_ctx.asin(x_dashu.repr()).value(&dashu_ctx); - let bits = (u32::try_from(prec).unwrap() * 4) + 256; +/// asin near 1 (where it → π/2, most sensitive) for x = 1 - 10^-k. (Deterministic k sweep.) +#[test] +#[ignore] +fn asin_near_one_fuzz() { + for k in 1u32..=15 { + let eps = DBig::from_str(&format!("1e-{k}")).unwrap(); + let x = DBig::ONE - eps; + let x_str = format!("{x:e}"); + for &prec in &[30usize, 100] { + let ctx = Context::::new(prec); + let asin_d = ctx.asin::<10>(x.repr(), None).unwrap().value(); + let bits = (prec as u32) * 4 + 256; let x_rug = Float::with_val(bits, Float::parse(&x_str).unwrap()); let asin_r = x_rug.asin(); - let a_r_val = DBig::from_str(&asin_r.to_string_radix(10, Some(prec))) - .unwrap() - .with_rounding::(); + let a_r: DBig = DBig::from_str(&asin_r.to_string_radix(10, Some(prec))).unwrap(); assert!( - (asin_d.clone() - a_r_val).abs() - <= DBig::from_parts(100.into(), -(isize::try_from(prec).unwrap())), - "Edge Asin mismatch: x={x_str}, prec={prec}" + (asin_d.clone() - a_r).abs() <= tol(prec), + "asin-near-1 mismatch k={k} prec={prec}: dashu={asin_d} rug={asin_r}" ); } } } +/// Regression: tan of a pinned very-large-exponent argument must match MPFR. (Deterministic.) #[test] #[ignore] -fn test_tan_large_exponent_regression() { +fn tan_large_exponent_regression() { let x_str = "-3.67225387623341113999117300261402819219640608e511"; for prec in [20usize, 50] { - let x_dashu = DBig::from_str(x_str).unwrap().with_rounding::(); - let dashu_ctx = dashu_float::Context::::new(prec); - let tan_d = dashu_ctx.tan(x_dashu.repr()).value(&dashu_ctx); - - let bits = (u32::try_from(prec).unwrap() * 4) + 512 + 1700; // extra bits for large exponent + let x = DBig::from_str(x_str).unwrap(); + let ctx = Context::::new(prec); + let tan_d = ctx.tan::<10>(x.repr(), None).unwrap().value(); + let bits = (prec as u32) * 4 + 512 + 1700; // extra bits for the large exponent let x_rug = Float::with_val(bits, Float::parse(x_str).unwrap()); let tan_r = x_rug.tan(); - let t_r_val = DBig::from_str(&tan_r.to_string_radix(10, Some(prec))) - .unwrap() - .with_rounding::(); + let t_r: DBig = DBig::from_str(&tan_r.to_string_radix(10, Some(prec))).unwrap(); assert!( - (tan_d.clone() - t_r_val).abs() - <= DBig::from_parts(100.into(), -(isize::try_from(prec).unwrap())), - "Large-exponent tan regression failed at prec={prec}: dashu={tan_d}, rug={tan_r}" + (tan_d.clone() - t_r).abs() <= tol(prec), + "large-exponent tan regression failed at prec={prec}: dashu={tan_d}, rug={tan_r}" ); } } - -#[test] -#[ignore] -fn test_pythagorean_identity_fuzz() { - let mut rng = StdRng::seed_from_u64(99); - let precisions = [20usize, 50, 100]; - - for i in 0..1000 { - let x_dashu = random_dbig(&mut rng, true).with_rounding::(); - - for &prec in &precisions { - let dashu_ctx = dashu_float::Context::::new(prec); - let (s, c) = dashu_ctx.sin_cos(x_dashu.repr()); - if let (FpResult::Normal(s_r), FpResult::Normal(c_r)) = (s, c) { - let s_f = FBig::from_repr(s_r.value(), dashu_ctx); - let c_f = FBig::from_repr(c_r.value(), dashu_ctx); - let sum = s_f.clone() * &s_f + c_f.clone() * &c_f; - let one = DBig::ONE - .with_precision(prec) - .value() - .with_rounding::(); - assert!( - (sum.clone() - one).abs() - <= DBig::from_parts(1000.into(), -(isize::try_from(prec).unwrap())), - "sin²+cos²≠1 at iteration {i}, prec={prec}, x={x_dashu:e}, sum={sum}" - ); - } - } - } -} - -#[test] -#[ignore] -fn test_cos_fuzz_comprehensive() { - let mut rng = StdRng::seed_from_u64(47); - let precisions = [10usize, 20, 50, 100]; - - for i in 0..2000 { - let x_dashu = random_dbig(&mut rng, true).with_rounding::(); - let x_str = format!("{x_dashu:e}"); - - for &prec in &precisions { - let dashu_ctx = dashu_float::Context::::new(prec); - let cos_d = - std::panic::catch_unwind(|| dashu_ctx.cos(x_dashu.repr()).value(&dashu_ctx)) - .unwrap_or_else(|_| panic!("PANIC at iteration {i}, prec {prec}, x = {x_str}")); - - let x_bits = ((x_dashu.repr().exponent().abs() as f64 * 3.322).ceil() as u32) + 500; - let bits = (((prec as f64).max(100.0) * 3.322).ceil() as u32) + x_bits; - let x_rug = match Float::parse(&x_str) { - Ok(parsed) => Float::with_val(bits, parsed), - Err(_) => continue, - }; - let cos_r = x_rug.cos(); - let c_r_val = DBig::from_str(&cos_r.to_string_radix(10, Some(prec))) - .unwrap() - .with_rounding::(); - assert!( - (cos_d.clone() - c_r_val).abs() - <= DBig::from_parts(100.into(), -(isize::try_from(prec).unwrap())), - "Cos mismatch at iteration {i}, x={x_str}, prec={prec}: dashu={cos_d}, rug={cos_r}" - ); - } - } -} - -#[test] -#[ignore] -fn test_tan_fuzz_strict() { - let mut rng = StdRng::seed_from_u64(44); - let precisions = [20usize, 50]; - - for i in 0..500 { - let x_dashu = random_dbig(&mut rng, true).with_rounding::(); - let x_str = format!("{x_dashu:e}"); - - for &prec in &precisions { - let dashu_ctx = dashu_float::Context::::new(prec); - - // Only skip if we can verify it's actually near a singularity (|cos| < 10^-5) - let cos_d = dashu_ctx.cos(x_dashu.repr()).value(&dashu_ctx); - if cos_d.abs() < DBig::from_parts(1.into(), -5).with_rounding::() { - continue; - } - - let tan_d = - std::panic::catch_unwind(|| dashu_ctx.tan(x_dashu.repr()).value(&dashu_ctx)) - .unwrap_or_else(|_| panic!("PANIC at iteration {i}, prec {prec}, x = {x_str}")); - - let x_bits = ((x_dashu.repr().exponent().abs() as f64 * 3.322).ceil() as u32) + 500; - let bits = (((prec as f64).max(100.0) * 3.322).ceil() as u32) + x_bits; - let x_rug = match Float::parse(&x_str) { - Ok(parsed) => Float::with_val(bits, parsed), - Err(_) => continue, - }; - let tan_r = x_rug.tan(); - let t_r_val = DBig::from_str(&tan_r.to_string_radix(10, Some(prec))) - .unwrap() - .with_rounding::(); - - assert!( - (tan_d.clone() - t_r_val).abs() - <= DBig::from_parts(100.into(), -(isize::try_from(prec).unwrap())), - "Tan mismatch at iteration {i}, x={x_str}, prec={prec}: dashu={tan_d}, rug={tan_r}" - ); - } - } -} diff --git a/guide/src/SUMMARY.md b/guide/src/SUMMARY.md index c7f866bd..c68216a7 100644 --- a/guide/src/SUMMARY.md +++ b/guide/src/SUMMARY.md @@ -19,4 +19,5 @@ - [FAQ](./faq.md) - [Performance](./performance.md) - [Cheatsheet](./cheatsheet.md) -- [IEEE 754 Compliance](./ieee754.md) +- [Standards Compliance](./compliance.md) +- [Complex Numbers](./complex.md) diff --git a/guide/src/ieee754.md b/guide/src/compliance.md similarity index 51% rename from guide/src/ieee754.md rename to guide/src/compliance.md index ba17d8cc..fa005f2d 100644 --- a/guide/src/ieee754.md +++ b/guide/src/compliance.md @@ -1,16 +1,24 @@ -# IEEE 754-2008 Compliance of dashu-float +# Standards Compliance -This document describes where `dashu-float`'s `FBig` type is compliant and where it deviates -from IEEE 754-2008. The reference is IEEE Std 754™-2008 (ISO/IEC/IEEE 60559:2011). +This page documents where `dashu`'s numeric types conform to the relevant standards — and where +they intentionally deviate. There are two aspects: -dashu-float is an **arbitrary-precision** floating-point library. Many IEEE 754 concepts -(e.g. fixed-width encoding, subnormals, NaN payloads) have no direct equivalent here. -Where infinite precision makes the standard's rules natural to satisfy, they are satisfied; -where they conflict with the arbitrary-precision model, the deviation is noted. +* **`dashu-float`'s `FBig` vs IEEE 754-2008** — the real floating-point model. +* **`dashu-cmplx`'s `CBig` vs C99 Annex G (IEC 60559 complex)** — the complex model, built on top of + `FBig` and inheriting its signed-zero / signed-infinity behavior. -## Data Model +The common thread: dashu types are **arbitrary-precision**, so fixed-width-encoding concerns +(subnormals, NaN payloads, bit layouts) have no direct equivalent. Where infinite precision makes a +standard's rules natural to satisfy, they are satisfied; where they conflict with the +arbitrary-precision / no-NaN model, the deviation is noted. -### Section 3 — Floating-point formats +## `dashu-float` and IEEE 754-2008 + +The reference here is IEEE Std 754™-2008 (ISO/IEC/IEEE 60559:2011). + +### Data Model + +#### Section 3 — Floating-point formats | IEEE 754 requirement | Compliance | Notes | |---------------------|-----------|-------| @@ -22,9 +30,9 @@ where they conflict with the arbitrary-precision model, the deviation is noted. | Subnormals | N/A | Arbitrary-precision significands eliminate the need for subnormals. Any non-zero number is normalized. | | Fixed-width encoding | N/A | No fixed bit widths; significands are unbounded `IBig` integers. | -## Arithmetic Operations +### Arithmetic Operations -### Section 5 — Operations +#### Section 5 — Operations | IEEE 754 requirement | Compliance | Notes | |---------------------|-----------|-------| @@ -45,7 +53,7 @@ where they conflict with the arbitrary-precision model, the deviation is noted. | Cancellation under roundTowardNegative → `-0` | ✅ | `cancel_zero` in add.rs produces `-0` when `R::IS_ROUND_TOWARD_NEGATIVE`. | | Exact subtraction cancels to `-0` only under directed rounding | ✅ | IEEE 754 §6.3: `(-3) + 3` = `+0` under roundTiesToEven/Up, `-0` under Down. | -### Section 5.3 — Rounding +#### Section 5.3 — Rounding | IEEE 754 requirement | Compliance | Notes | |---------------------|-----------|-------| @@ -53,7 +61,7 @@ where they conflict with the arbitrary-precision model, the deviation is noted. | Correct rounding to within 1 ulp | ✅ | All operations guarantee `|error| < 1 ulp`. The `Rounded` type distinguishes exact from inexact results. | | Round-to-nearest preserves sign of zero | ✅ | `rounded_to_repr` preserves input sign when rounding collapses a non-zero to zero. | -### Section 5.6 — Sign bit operations +#### Section 5.6 — Sign bit operations | IEEE 754 requirement | Compliance | Notes | |---------------------|-----------|-------| @@ -62,7 +70,7 @@ where they conflict with the arbitrary-precision model, the deviation is noted. | `signum(±0)` = `+0` | ✅ | Returns `+0` for both `+0` and `-0` (signum collapses the sign of zero). | | `sign()` distinguishes `+0` from `-0` | ✅ | `Repr::sign()` returns `Negative` for `-0`. | -## Conversions +### Conversions | IEEE 754 requirement | Compliance | Notes | |---------------------|-----------|-------| @@ -73,7 +81,7 @@ where they conflict with the arbitrary-precision model, the deviation is noted. | Int-to-float conversion exact for representable integers | ✅ | | | Float-to-int overflows saturate (per Rust convention) | N/A | Rust's `TryFrom` returns an error on overflow; `ToPrimitive` returns `None`. | -## Exceptional Conditions +### Exceptional Conditions | IEEE 754 requirement | Compliance | Notes | |---------------------|-----------|-------| @@ -83,7 +91,7 @@ where they conflict with the arbitrary-precision model, the deviation is noted. | Underflow → `±0` (no trap) | ✅ | Same. | | Inexact flag | ⚠️ Partial | The `Rounded` type carries `Exact`/`Inexact(T, Rounding)` to signal whether rounding occurred, but there is no sticky flag mechanism. | -## Summary +### Summary (dashu-float) | Category | Status | |----------|--------| @@ -95,3 +103,65 @@ where they conflict with the arbitrary-precision model, the deviation is noted. | Infinite operands in arithmetic | ❌ Error (by design — infinities are terminal) | | Subnormals | N/A (unbounded precision) | | Exception flags | ⚠️ Rounded type signals exact/inexact, no sticky flags | + +## `dashu-cmplx` and C99 Annex G + +`CBig` is a pair of `Repr` parts (real, imaginary) over a single shared precision and rounding mode. +It targets C99 Annex G (IEC 60559-compatible complex arithmetic) for the common functionality, +reusing `dashu-float`'s signed-zero / signed-infinity / branch-cut machinery for each part. As with +`FBig`, there is **no NaN**: C99 cases that would produce a complex NaN are mapped to `FpError` at +the `Context` layer (and panics at the convenience layer). + +### Data Model (§G.2) + +| C99 Annex G requirement | Compliance | Notes | +|---------------------|-----------|-------| +| Complex as an ordered real/imaginary pair | ✅ | `CBig` stores `re` and `im` (`Repr`) over one shared `Context`. | +| Per-part signed zeros (`±0`) | ✅ | Inherited from `dashu-float`; the sign of the imaginary zero selects the side of a branch cut. | +| Per-part signed infinities (`±∞`) | ✅ | Each part may independently be `±∞`. | +| A single complex infinity (Riemann point) | ✅ | `proj` collapses any part-infinite value to `+∞ + i·0`; overflow yields both parts `+∞`. | +| Complex NaN | ❌ Deviates | No NaN. NaN-producing cases map to `FpError` (`Context`) / panic (convenience layer). | + +### Arithmetic (§G.5) + +| C99 Annex G requirement | Compliance | Notes | +|---------------------|-----------|-------| +| `conj(z)` flips the sign of the imaginary part (incl. `-0`, `±∞`) | ✅ | Exact sign flip of the imaginary part. | +| `proj(z)`: any infinity → `+∞ + i·0` | ✅ | The projected imaginary zero carries the sign of the original imaginary part. | +| `∞·∞`, `finite·∞` → `∞` | ✅ | Yields the Riemann point at infinity. | +| `0·∞` → NaN (C) | ⚠️ Partial | Returns `Err(FpError::Indeterminate)` (no NaN). | +| `finite/0`, `∞/finite` → `∞` | ✅ | Riemann point at infinity. | +| `0/0`, `∞/∞` → NaN (C) | ⚠️ Partial | Returns `Err(FpError::Indeterminate)`. | +| `finite/∞`, `0/finite` → `0` | ✅ | | +| `1/0 → ∞`, `1/∞ → 0` (inverse) | ✅ | | + +### Transcendentals and branch cuts (§G.6) + +| C99 Annex G requirement | Compliance | Notes | +|---------------------|-----------|-------| +| Branch cuts follow the Kahan signed-zero model | ✅ | e.g. `log(-r ± i·0) = ln r ± i·π`: the sign of the imaginary zero selects the side of the cut. | +| `sqrt(+∞) = +∞` | ✅ | | +| `sqrt(-∞) = +0 + i·∞` | ✅ | | +| `exp(+∞) = +∞`, `exp(-∞) = +0` | ✅ | | +| `exp(0 + i·∞)` → NaN (C) | ⚠️ Partial | Returns `Err(FpError::Indeterminate)`. | +| `log(0) = -∞`, `log(+∞) = +∞` | ✅ | | +| `arg(0 + i·∞) = +π/2`, `arg(0 - i·∞) = -π/2` | ✅ | `arg = atan2(im, re)`, reusing `dashu-float`'s Annex-G `atan2` table. | +| `abs`/`hypot` overflow-safe modulus | ✅ | Thin composition over `dashu-float`'s `hypot`. | + +### Exceptional Conditions + +| C99 Annex G requirement | Compliance | Notes | +|---------------------|-----------|-------| +| Invalid / indeterminate form → NaN | ❌ Deviates | `Err(FpError::{Indeterminate, InfiniteInput})` at `Context`; panics at the convenience layer. No NaN by design. | +| Domain error (e.g. even root of a negative value, out-of-range inverse trig) | ❌ Deviates | `Err(FpError::OutOfDomain)` / panic, rather than a NaN result. | +| Each component rounded independently to the shared mode | ✅ | Near-correctly rounded per axis (a guaranteed-correct Ziv loop is deferred to 0.5.x). | + +### Summary (dashu-cmplx) + +| Category | Status | +|----------|--------| +| Per-part signed zeros & infinities | ✅ Fully compliant | +| Riemann-point single infinity / `proj` | ✅ Fully compliant | +| Branch cuts (Kahan signed-zero model) | ✅ Fully compliant | +| Arithmetic & transcendental special values | ⚠️ Values that C99 makes NaN are reported as `FpError` / panic | +| Complex NaN | ❌ Absent by design | diff --git a/integer/CHANGELOG.md b/integer/CHANGELOG.md index c4f20ca6..ad16ac94 100644 --- a/integer/CHANGELOG.md +++ b/integer/CHANGELOG.md @@ -5,6 +5,12 @@ ### Add - `UBig::to_digits` / `UBig::from_digits`: convert to and from a sequence of base-`B` digits (base `2..=Word::MAX`, digits stored as `Word`, most-significant first). Complements [`UBig::in_radix`] which is limited to base 2..=36. +### Fix +- Fixed a broken intra-doc link in `MontgomeryRepr`'s docs (a `[...](self)` link that resolved to a + private item is removed), surfaced by `cargo doc -D warnings`. +- `UBig::nth_root` / `IBig::nth_root` (and `cbrt`) of `0` returned `1` instead of `0`: the `bits <= n` + shortcut fired for the zero input (bit length 0). Found by the new `fuzz/` `rug::Integer` oracle. + ### Change - **(breaking)** `IBig`'s serde non-human-readable format switched from the custom byte-length-parity encoding to standard two's complement little-endian bytes (matching [`IBig::to_le_bytes`]), for interop robustness. Previously serialized data is not compatible. - **(breaking)** `UBig::in_radix` and `IBig::in_radix` now take `radix: u8` (was `u32`); the internal `Digit` type alias is now `u8`. `from_str_with_radix_prefix` / `from_str_with_radix_default` now expose the detected/default radix as `u8` (was `u32`). `from_str_radix` keeps its `u32` argument for `std` parity. diff --git a/integer/src/monty/repr.rs b/integer/src/monty/repr.rs index 0a84b290..3712cd50 100644 --- a/integer/src/monty/repr.rs +++ b/integer/src/monty/repr.rs @@ -17,7 +17,7 @@ use num_modular::{Montgomery as NumMontgomery, Reducer}; /// /// This is the Montgomery analogue of [`ConstDivisor`](crate::fast_div::ConstDivisor): /// it stores an odd modulus together with the values needed to perform fast modular -/// arithmetic in [Montgomery form](self). Create it once with [`MontgomeryRepr::new`] +/// arithmetic in Montgomery form. Create it once with [`MontgomeryRepr::new`] /// and use [`MontgomeryRepr::reduce`] to convert values into the ring. /// /// The modulus **must be odd and greater than 1**; this is enforced at construction diff --git a/integer/src/root_ops.rs b/integer/src/root_ops.rs index 1774de82..02432545 100644 --- a/integer/src/root_ops.rs +++ b/integer/src/root_ops.rs @@ -228,6 +228,9 @@ mod repr { } let bits = self.bit_len(); + if bits == 0 { + return Repr::zero(); // the nth root of 0 is 0, not 1 + } if bits <= n { return Repr::one(); } diff --git a/integer/tests/root.rs b/integer/tests/root.rs index a27838a4..3b165bc0 100644 --- a/integer/tests/root.rs +++ b/integer/tests/root.rs @@ -92,6 +92,11 @@ fn test_sqrt_negative_panic() { #[test] fn test_nth_root() { + // the nth root of 0 is 0 (regression: it used to return 1 via the `bits <= n` shortcut) + assert_eq!(ubig!(0).nth_root(1), ubig!(0)); + assert_eq!(ubig!(0).nth_root(3), ubig!(0)); + assert_eq!(ubig!(0).nth_root(5), ubig!(0)); + assert_eq!(ibig!(0).nth_root(3), ibig!(0)); assert_eq!(ubig!(2).nth_root(1), ubig!(2)); assert_eq!(ubig!(2).nth_root(2), ubig!(1)); assert_eq!(ubig!(2).nth_root(3), ubig!(1)); diff --git a/macros/CHANGELOG.md b/macros/CHANGELOG.md index 780696ed..06d082e7 100644 --- a/macros/CHANGELOG.md +++ b/macros/CHANGELOG.md @@ -1,5 +1,14 @@ # Changelog +## Unreleased + +### Add +- `cbig!` / `static_cbig!` (and the `cbig_embedded` / `static_cbig_embedded` building blocks) for + creating [`dashu-cmplx`]'s `CBig` from a complex literal in algebraic `a+bi` form or a `re, im` + pair. Each coefficient reuses the `fbig!` base-2 literal parser; `static_cbig!` builds the value + via the new `CBig::from_repr_parts` const constructor (gated on Rust 1.64+, like the other static + variants). + ## 0.4.2 - Replace `paste` dependency with `pastey` ([#58](https://github.com/cmpute/dashu/pull/58)). diff --git a/macros/Cargo.toml b/macros/Cargo.toml index a908ee10..26c2a8bb 100644 --- a/macros/Cargo.toml +++ b/macros/Cargo.toml @@ -24,6 +24,7 @@ dashu-base = { version = "0.4.2", default-features = false, path = "../base" } dashu-int = { version = "0.4.2", default-features = false, path = "../integer" } dashu-float = { version = "0.4.4", default-features = false, path = "../float" } dashu-ratio = { version = "0.4.2", default-features = false, path = "../rational" } +dashu-cmplx = { version = "0.4.5", default-features = false, path = "../complex" } quote = "1" proc-macro2 = "1" diff --git a/macros/docs/cbig.md b/macros/docs/cbig.md new file mode 100644 index 00000000..50f7e50e --- /dev/null +++ b/macros/docs/cbig.md @@ -0,0 +1,13 @@ +Create an arbitrary precision complex number ([dashu_cmplx::CBig]) with base 2 rounding towards zero. + +Each coefficient is a base-2 `FBig` literal (the same grammar as [fbig!]). The literal uses the +algebraic `a+bi` notation, or a `re, im` pair: + +```rust +# use dashu_macros::cbig; +let z = cbig!(11+100i); // 3 + 4i in base 2 +let r = cbig!(111); // purely real (7) +let im = cbig!(10i); // purely imaginary (2i) +let p = cbig!(11, -100); // pair form: 3 - 4i +assert_eq!(z, p + cbig!(1000i)); // (3-4i) + 8i = 3+4i +``` diff --git a/macros/docs/static_cbig.md b/macros/docs/static_cbig.md new file mode 100644 index 00000000..27f24676 --- /dev/null +++ b/macros/docs/static_cbig.md @@ -0,0 +1,9 @@ +Create a static reference to an arbitrary precision complex number ([dashu_cmplx::CBig]). + +This is the static variant of [cbig!], requiring Rust 1.64+ (it relies on `static` items with +const generics). See [cbig!] for the literal grammar. + +```rust +# use dashu_macros::static_cbig; +let z: &dashu_cmplx::CBig = static_cbig!(11+100i); // 3 + 4i +``` diff --git a/macros/src/lib.rs b/macros/src/lib.rs index 33b60a57..33d872eb 100644 --- a/macros/src/lib.rs +++ b/macros/src/lib.rs @@ -164,3 +164,29 @@ pub fn rbig_embedded(input: TokenStream) -> TokenStream { pub fn static_rbig_embedded(input: TokenStream) -> TokenStream { parse::ratio::parse_static_ratio(true, input.into()).into() } + +#[proc_macro] +#[doc = include_str!("../docs/cbig.md")] +pub fn cbig(input: TokenStream) -> TokenStream { + parse::cmplx::parse_complex(false, false, input.into()).into() +} + +#[proc_macro] +#[rustversion::since(1.64)] +#[doc = include_str!("../docs/static_cbig.md")] +pub fn static_cbig(input: TokenStream) -> TokenStream { + parse::cmplx::parse_complex(true, false, input.into()).into() +} + +#[doc(hidden)] +#[proc_macro] +pub fn cbig_embedded(input: TokenStream) -> TokenStream { + parse::cmplx::parse_complex(false, true, input.into()).into() +} + +#[doc(hidden)] +#[rustversion::since(1.64)] +#[proc_macro] +pub fn static_cbig_embedded(input: TokenStream) -> TokenStream { + parse::cmplx::parse_complex(true, true, input.into()).into() +} diff --git a/macros/src/parse/cmplx.rs b/macros/src/parse/cmplx.rs new file mode 100644 index 00000000..949e4694 --- /dev/null +++ b/macros/src/parse/cmplx.rs @@ -0,0 +1,57 @@ +//! Parser for the `cbig!` literal macro. Accepts the algebraic `a+bi` form (reusing the runtime +//! `CBig::FromStr` grammar) or a `re, im` pair, e.g. `cbig!(11+100i)`, `cbig!(111)`, `cbig!(11, -100)`. + +use super::float::{gen_binary_fbig_value, gen_binary_repr_const}; +use core::str::FromStr; +use dashu_cmplx::CBig; +use dashu_float::FBig; +use proc_macro2::TokenStream; +use quote::quote; + +fn panic_cbig_syntax() -> ! { + panic!("Incorrect syntax, please refer to the docs for acceptable complex literal formats.") +} + +/// Parse a base-2 `FBig` coefficient (the same grammar as `fbig!`). +fn parse_coeff(s: &str) -> FBig { + FBig::from_str(s.trim()).unwrap_or_else(|_| panic_cbig_syntax()) +} + +pub fn parse_complex(static_: bool, embedded: bool, input: TokenStream) -> TokenStream { + let value_str: String = input.into_iter().map(|tt| tt.to_string()).collect(); + let value_str = value_str.trim(); + + // `re, im` pair (im is a plain real coefficient) vs the algebraic `a+bi` form. + let z = if let Some((re_s, im_s)) = value_str.split_once(',') { + CBig::from_parts(parse_coeff(re_s), parse_coeff(im_s)) + } else { + CBig::from_str(value_str).unwrap_or_else(|_| panic_cbig_syntax()) + }; + let (re, im) = z.into_parts(); + + let ns = if embedded { + quote!(::dashu::complex) + } else { + quote!(::dashu_cmplx) + }; + + if static_ { + // const construction: each Repr via from_static_words (or Repr::zero() for a zero coeff), + // then new. + let (re_repr, prec_re) = gen_binary_repr_const(embedded, &re); + let (im_repr, prec_im) = gen_binary_repr_const(embedded, &im); + let prec = prec_re.max(prec_im); + quote! {{ + static VALUE: #ns::CBig = #ns::CBig::new( + #re_repr, + #im_repr, + #ns::Context::new(#prec), + ); + &VALUE + }} + } else { + let re_tt = gen_binary_fbig_value(embedded, &re); + let im_tt = gen_binary_fbig_value(embedded, &im); + quote! { #ns::CBig::from_parts(#re_tt, #im_tt) } + } +} diff --git a/macros/src/parse/float.rs b/macros/src/parse/float.rs index 7e7e8a21..b3bb3936 100644 --- a/macros/src/parse/float.rs +++ b/macros/src/parse/float.rs @@ -135,3 +135,65 @@ pub fn parse_decimal_float(static_: bool, embedded: bool, input: TokenStream) -> }} } } + +/// Generate a `const`-context base-2 [`Repr`](dashu_float::Repr) reconstruction (always via +/// `from_static_words`, which is `const` and handles any significand size). Used by `static_cbig!` +/// to build each coefficient's `Repr` inside a `static` item. +pub fn gen_binary_repr_const(embedded: bool, f: &FBig) -> (TokenStream, usize) { + let prec = f.precision(); + let (signif, exp) = f.clone().into_repr().into_parts(); + let (sign, mag) = signif.into_parts(); + + let ns = if embedded { + quote!(::dashu::float) + } else { + quote!(::dashu_float) + }; + let repr_tt = quote!( #ns::Repr::<2> ); + + // a zero coefficient: `from_static_words` rejects a zero significand, so emit the const zero. + if mag.is_zero() { + let zero = if sign == Sign::Negative { + quote!( #repr_tt::neg_zero() ) + } else { + quote!( #repr_tt::zero() ) + }; + return (zero, prec); + } + + let data_defs = quote_words(&mag.to_le_bytes(), embedded); + let sign = quote_sign(embedded, sign); + let repr = quote! { + unsafe { #repr_tt::from_static_words(#sign, #data_defs, #exp) } + }; + (repr, prec) +} + +/// Generate a non-`static` reconstruction of a base-2 `FBig` (default rounding) from its parsed +/// parts — shared by `fbig!` (via `parse_binary_float`) and `cbig!` (for each coefficient). +pub fn gen_binary_fbig_value(embedded: bool, f: &FBig) -> TokenStream { + let prec = f.precision(); + let (signif, exp) = f.clone().into_repr().into_parts(); + let (sign, mag) = signif.into_parts(); + + let ns = if embedded { + quote!(::dashu::float) + } else { + quote!(::dashu_float) + }; + let repr_tt = quote!( #ns::Repr::<2> ); + let type_tt = quote!( #ns::FBig::<#ns::round::mode::Zero, 2> ); + + if mag.bit_len() <= 32 { + let sign = quote_sign(embedded, sign); + let u: u32 = mag.try_into().unwrap(); + quote!( #type_tt::from_parts_const(#sign, #u as _, #exp, Some(#prec)) ) + } else { + let signif_tt = quote_ibig(embedded, IBig::from_parts(sign, mag)); + quote! {{ + let repr = #repr_tt::new(#signif_tt, #exp); + let context = #ns::Context::<#ns::round::mode::Zero>::new(#prec); + #type_tt::from_repr(repr, context) + }} + } +} diff --git a/macros/src/parse/mod.rs b/macros/src/parse/mod.rs index 518def04..6201d743 100644 --- a/macros/src/parse/mod.rs +++ b/macros/src/parse/mod.rs @@ -1,5 +1,6 @@ //! Parse number from raw literals +pub mod cmplx; mod common; pub mod float; pub mod int; diff --git a/rational/CHANGELOG.md b/rational/CHANGELOG.md index 5f7e262e..08b046f0 100644 --- a/rational/CHANGELOG.md +++ b/rational/CHANGELOG.md @@ -3,6 +3,8 @@ ## Unreleased ### Fix +- Fixed a broken intra-doc link to `Display` in `InRadix`'s docs (`core::fmt::Display`), surfaced by + `cargo doc -D warnings`. - (internal) The `in_expanded` formatting unit tests failed to compile under `no_std` (`cargo test --no-default-features`) because the `format!` macro was not imported; the test module now imports `alloc::format`. ### Change diff --git a/rational/src/fmt/mod.rs b/rational/src/fmt/mod.rs index c9b207e2..ee524f87 100644 --- a/rational/src/fmt/mod.rs +++ b/rational/src/fmt/mod.rs @@ -115,7 +115,7 @@ impl fmt::Display for Relaxed { /// Representation of a rational number in a given radix, returned by /// [`RBig::in_radix`] and [`Relaxed::in_radix`]. /// -/// Implements [`Display`]. The alternate flag (`{:#}`) toggles uppercase +/// Implements [`Display`](core::fmt::Display). The alternate flag (`{:#}`) toggles uppercase /// letters for radices above 10. pub struct InRadix<'a> { numerator: &'a dashu_int::IBig, diff --git a/src/lib.rs b/src/lib.rs index a88de58e..e89628c3 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -22,6 +22,11 @@ pub mod rational { pub use dashu_ratio::*; } +/// Arbitrary precision complex number +pub mod complex { + pub use dashu_cmplx::*; +} + #[doc(hidden)] pub use dashu_macros as __dashu_macros; @@ -110,6 +115,23 @@ macro_rules! static_rbig { } } +#[macro_export] +#[doc = include_str!("macro-docs/cbig.md")] +macro_rules! cbig { + ($($t:tt)+) => { + $crate::__dashu_macros::cbig_embedded!($($t)+) + } +} + +#[macro_export] +#[rustversion::since(1.64)] +#[doc = include_str!("macro-docs/static_cbig.md")] +macro_rules! static_cbig { + ($($t:tt)+) => { + $crate::__dashu_macros::static_cbig_embedded!($($t)+) + } +} + /// A verbose alias for [UBig][dashu_int::UBig] pub type Natural = dashu_int::UBig; @@ -132,3 +154,6 @@ pub type FastDecimal = dashu_float::CachedFBig