Skip to content

feat(elreal): Phase E.2 -- sqrt + hypot with depth-1 EFT refinement on sqrt#894

Merged
Ravenwater merged 2 commits into
mainfrom
feat/elreal-phase-e2
May 21, 2026
Merged

feat(elreal): Phase E.2 -- sqrt + hypot with depth-1 EFT refinement on sqrt#894
Ravenwater merged 2 commits into
mainfrom
feat/elreal-phase-e2

Conversation

@Ravenwater

@Ravenwater Ravenwater commented May 21, 2026

Copy link
Copy Markdown
Contributor

Summary

Phase E.2 of epic #873. Implements #888 (the second math sub-issue under Phase E #878).

Square root via Newton-style EFT residual at depth 1, plus a depth-0 hypot via std::hypot for overflow safety. No dependency on Phase F's lazy division -- the sqrt depth-1 correction divides by 2*c0 (a single double), not by a lazy elreal, so it works under the Phase C / Phase F constraint that lazy / is depth-0 only.

sqrt algorithm

Newton's identity: (c0 + delta)^2 = a => delta = (a - c0^2) / (2*c0 + delta) ~= (a - c0^2) / (2*c0) for |delta| << c0.

c0       = std::sqrt(a.at(0))                  // depth 0
prod_err = two_prod_residual(c0, c0)           // c0^2 = prod_hi + prod_err exactly
num      = (a.at(0) - prod_hi) - prod_err + a.at(1)
c1       = num / (2 * c0)                       // depth 1; double divide, not lazy

So depth-1 captures both the EFT residual of c0^2 and the operand's own depth-1 correction a.at(1). Depth 2+ returns 0.0 (Heron iteration on the lazy stream is a Phase F follow-up).

Negative argument: throws elreal_negative_sqrt_arg under ELREAL_THROW_ARITHMETIC_EXCEPTION, else returns NaN via std::sqrt on the leading double.

hypot algorithm

std::hypot(a.at(0), b.at(0)) at depth 0. The standard library implementation handles overflow (large operands), signed zero, infinities, and NaN per IEEE-754 -- no need to re-derive the max * sqrt(1 + (min/max)^2) form here. Depth-1+ deferred; could be wired up later via sqrt(a*a + b*b) (when neither operand is near overflow) or the algebraic overflow-protected form walked at depth 1.

Test plan

Three new binaries under elastic/elreal/math/:

Binary Coverage
elreal_sqrt exact squares, non-square cross-validation vs std::sqrt, round-trip sqrt(a)^2 ~= a, sqrt(2) matches std::numbers::sqrt2_v, depth-1 propagation on rational input (sqrt(2/3)), negative -> NaN, special values
elreal_sqrt_throw elreal_negative_sqrt_arg raised when ELREAL_THROW_ARITHMETIC_EXCEPTION is set; separate binary because the macro is fixed at compile time
elreal_hypot Pythagorean triples (3-4-5, 5-12-13, 8-15-17), zero / symmetry / negative-arg cases, overflow safety at 1e200, cross-validation vs std::hypot, special-value propagation
  • gcc 13: 16 binaries (3 new + 13 prior) all PASS
  • clang: 16 binaries all PASS

What does not land in this PR

  • Heron iteration / depth-2+ refinement for sqrt (Phase F)
  • Depth-1+ refinement for hypot (small follow-up; the algebraic form needs care to stay overflow-safe at depth 1)

Related

Summary by CodeRabbit

  • New Features

    • Added sqrt() and hypot() for the elreal numeric type, including improved precision for sqrt (additional refinement) and robust handling of zeros, negatives, infinities, and NaNs; configurable behavior for arithmetic exceptions.
  • Tests

    • New comprehensive test suites validating correctness, symmetry, overflow safety, round‑trip identities, leading‑zero/expansion edge cases, special‑value behavior, and throw vs non‑throw modes.

Review Change Stack

…n sqrt

Phase E.2 of epic #873 (resolves #888). The second math sub-issue:
square root via Newton-style EFT residual at depth 1, plus a depth-0
hypot via std::hypot for overflow safety.

sqrt(const elreal&):
  - Depth 0: c0 = std::sqrt(a.at(0)) (IEEE-754 correctly rounded)
  - Depth 1: Newton residual c1 ~= (a - c0^2) / (2*c0). a - c0^2 is
    computed with two_prod (giving c0^2 = prod_hi + prod_err exactly),
    then a single double subtraction picks up the operand's depth-1
    correction a.at(1). The division by 2*c0 is plain double arithmetic
    on the elreal-internal numerator, NOT a lazy elreal divide -- so we
    are not blocked by Phase F's reciprocal-stream work.
  - Depth 2+: 0.0 (documented limitation; full Heron iteration on the
    lazy stream lands in Phase F).
  - Negative argument: throws elreal_negative_sqrt_arg when
    ELREAL_THROW_ARITHMETIC_EXCEPTION is set; otherwise returns NaN
    via std::sqrt(negative double).

hypot(const elreal&, const elreal&):
  - Depth 0: c0 = std::hypot(a.at(0), b.at(0)) -- handles overflow,
    signed zero, infinities, and NaN per IEEE-754 conventions. The
    overflow-protected algebraic form (max(|a|,|b|) * sqrt(1 + (min/max)^2))
    is built into the standard library implementation.
  - Depth 1+: deferred. Either an overflow-protected algebraic form
    walked at depth 1, or sqrt(a*a + b*b) when neither operand is near
    overflow -- a small follow-up.

Tests under elastic/elreal/math/:
  sqrt.cpp        -- perfect squares, non-square cross-validation vs
                     std::sqrt, round-trip sqrt(a)^2 ~= a, sqrt(2)
                     matches std::numbers::sqrt2_v, depth-1 propagation
                     on rational input, negative -> NaN, special values
  sqrt_throw.cpp  -- elreal_negative_sqrt_arg in the throw mode (separate
                     binary because the macro is compile-time fixed)
  hypot.cpp       -- Pythagorean triples, zero/symmetry/negative args,
                     overflow safety at 1e200, cross-validation vs
                     std::hypot, special-value propagation

16 elreal binaries (3 new + 13 prior) all PASS on gcc 13 and clang.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented May 21, 2026

Copy link
Copy Markdown
Contributor

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 6eae10d9-1f16-4bf7-8184-5d717b7ca519

📥 Commits

Reviewing files that changed from the base of the PR and between e7380ea and 40a8289.

📒 Files selected for processing (2)
  • elastic/elreal/math/sqrt.cpp
  • include/sw/universal/number/elreal/elreal_impl.hpp

📝 Walkthrough

Walkthrough

This PR adds Phase E.2 math functions to the elreal lazy-arithmetic type: inline sqrt (with optional depth‑1 EFT refinement) and hypot (depth‑0), plus three test programs exercising default behavior, exception-throwing mode, and hypot correctness and edge cases.

Changes

Phase E.2 Math Functions: sqrt and hypot for elreal

Layer / File(s) Summary
sqrt and hypot core implementations
include/sw/universal/number/elreal/elreal_impl.hpp
Friend declarations and inline implementations added. sqrt computes via std::sqrt, handles negative inputs (throw or NaN based on macro), and installs depth‑1 EFT refinement using two_prod. hypot computes the leading component via std::hypot with no deeper refinement.
sqrt default-mode test suite
elastic/elreal/math/sqrt.cpp
Validates perfect squares, cross-checks non-squares against std::sqrt, verifies round-trip tolerance, confirms sqrt(2) constant, tests depth‑1 residual propagation and a leading-zero regression, checks from_expansion sign/edge cases, asserts negative/inf/NaN/zero behavior, and reports results with exception handling.
sqrt exception-mode test suite
elastic/elreal/math/sqrt_throw.cpp
Enables throw mode and verifies that sqrt(negative) raises elreal_negative_sqrt_arg, while sqrt(0) and sqrt(positive) do not throw and return expected values; includes top-level exception handlers.
hypot test suite
elastic/elreal/math/hypot.cpp
Tests exact Pythagorean triples, zero cases, symmetry, overflow safety for large inputs, magnitude handling of negative arguments, cross-validation against std::hypot, and special values (inf, NaN); reports results and handles exceptions.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related issues

Suggested labels

enhancement

Poem

🐰
I hop through proofs with gentle cheer,
Roots unfold and hypos appear,
Depth‑one whispers tidy the trail,
Tests pass and numbers tell the tale,
Tiny rabbit math — precise and dear.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 25.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately reflects the main changes: it introduces Phase E.2 sqrt and hypot functions with depth-1 EFT refinement specifically for sqrt.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/elreal-phase-e2

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
elastic/elreal/math/sqrt.cpp (1)

86-121: ⚡ Quick win

Add regression coverage for leading-zero expansion inputs.

Please add cases like elreal::from_expansion({0.0, +eps}) and elreal::from_expansion({0.0, -eps}) so the suite validates positive tiny values and negative detection when depth-0 is zero.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@elastic/elreal/math/sqrt.cpp` around lines 86 - 121, Add two regression tests
near the existing sqrt cases: create elreal numbers using
elreal::from_expansion({0.0, +eps}) and elreal::from_expansion({0.0, -eps})
(with eps = std::numeric_limits<double>::epsilon()), then call sqrt(...) on
each. For the positive tiny case assert the sqrt result is finite/non-NaN (and
optionally that r.at(0) >= 0 and r.at(1) != 0 or that (r*r) is close to eps via
check_close). For the negative tiny case assert sqrt returns NaN (use r.isnan())
like the existing negative-argument test. Use the same style/scheme as the
surrounding tests and reference elreal::from_expansion, sqrt, at(), isnan(), and
check_close to locate where to add these checks.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@include/sw/universal/number/elreal/elreal_impl.hpp`:
- Around line 922-937: The sqrt implementation currently only examines the
depth-0 limb (a0 / a.at(0)) which produces incorrect behavior for expansions
whose leading limb is zero (e.g. {0.0, c1, ...}); change the logic in the sqrt
path to locate the first non-zero, finite materialized limb (or call the
existing normalization/materialization routine) instead of using a.at(0)
directly, then base the negative-argument check and the initial c0 =
std::sqrt(...) on that first non-zero limb; ensure you update the resulting
elreal _components and _computed_depth the same way after using that normalized
leading limb (keep elreal, _components, _computed_depth, and the
negative-argument throw/NaN handling intact).

---

Nitpick comments:
In `@elastic/elreal/math/sqrt.cpp`:
- Around line 86-121: Add two regression tests near the existing sqrt cases:
create elreal numbers using elreal::from_expansion({0.0, +eps}) and
elreal::from_expansion({0.0, -eps}) (with eps =
std::numeric_limits<double>::epsilon()), then call sqrt(...) on each. For the
positive tiny case assert the sqrt result is finite/non-NaN (and optionally that
r.at(0) >= 0 and r.at(1) != 0 or that (r*r) is close to eps via check_close).
For the negative tiny case assert sqrt returns NaN (use r.isnan()) like the
existing negative-argument test. Use the same style/scheme as the surrounding
tests and reference elreal::from_expansion, sqrt, at(), isnan(), and check_close
to locate where to add these checks.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 74761c71-3c25-4454-ad6a-bb075477370b

📥 Commits

Reviewing files that changed from the base of the PR and between 238dccd and e7380ea.

📒 Files selected for processing (4)
  • elastic/elreal/math/hypot.cpp
  • elastic/elreal/math/sqrt.cpp
  • elastic/elreal/math/sqrt_throw.cpp
  • include/sw/universal/number/elreal/elreal_impl.hpp

Comment thread include/sw/universal/number/elreal/elreal_impl.hpp Outdated
@Ravenwater Ravenwater self-assigned this May 21, 2026
@Ravenwater Ravenwater moved this to In progress in Universal Number Library May 21, 2026
@Ravenwater Ravenwater added this to the V4 milestone May 21, 2026
Address CodeRabbit's Major finding on PR #894 (real bug).

sqrt was checking only a.at(0) for both the negative-argument guard
and the leading sqrt computation. For expansions whose leading double
cancels in arithmetic -- {0.0, c1, ...} where c1 != 0 -- this misclassifies:

  - {0, +eps}: a small positive value; pre-fix sqrt returned 0 because
    std::sqrt(0.0) == 0, hiding the true sqrt(eps) ~= 3.16e-5.
  - {0, -eps}: a small negative value; pre-fix sqrt returned 0 because
    a.at(0) == 0 bypassed the negative-argument handler, suppressing
    the expected NaN / elreal_negative_sqrt_arg.

The canonical input that triggers the bug is the lazy-real distinctness
test from Phase D: `elreal(1, 3) - elreal(1.0/3.0)` cancels at depth 0
and leaves a positive depth-1 correction equal to the rational residual.
sqrt of that should be ~sqrt(1.85e-17) = ~4.3e-9, not zero.

Fix: walk the materialised components to find the first non-zero one.
That component drives both the sign check and the leading sqrt. The
depth-1 generator captures the index so the EFT residual formula uses
the right "leading" and the right next-correction limb. For the common
case (lead_idx == 0) this collapses to the original formula.

Three new regression tests in elastic/elreal/math/sqrt.cpp cover the
fix:
  - sqrt(elreal(1, 3) - elreal(1.0/3.0)) round-trips to recover the input
  - sqrt({0, -1e-9}) produces NaN
  - sqrt({0, +1e-9}) has leading ~= 3.16e-5

Each of the three would have failed against the pre-fix code.

All 16 elreal binaries PASS on gcc 13 and clang.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@Ravenwater Ravenwater marked this pull request as ready for review May 21, 2026 11:43
@Ravenwater Ravenwater merged commit dd36106 into main May 21, 2026
32 checks passed
@Ravenwater Ravenwater deleted the feat/elreal-phase-e2 branch May 21, 2026 11:58
@github-project-automation github-project-automation Bot moved this from In progress to Done in Universal Number Library May 21, 2026
@coveralls

Copy link
Copy Markdown

Coverage Report for CI Build 26223836575

Warning

Build has drifted: This PR's base is out of sync with its target branch, so coverage data may include unrelated changes.
Quick fix: rebase this PR. Learn more →

Coverage increased (+0.006%) to 84.233%

Details

  • Coverage increased (+0.006%) from the base build.
  • Patch coverage: No coverable lines changed in this PR.
  • 8 coverage regressions across 1 file.

Uncovered Changes

No uncovered changes found.

Coverage Regressions

8 previously-covered lines in 1 file lost coverage.

File Lines Losing Coverage Coverage
include/sw/universal/number/cfloat/cfloat_impl.hpp 8 93.63%

Coverage Stats

Coverage Status
Relevant Lines: 55729
Covered Lines: 46942
Line Coverage: 84.23%
Coverage Strength: 5284872.57 hits per line

💛 - Coveralls

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

2 participants