Skip to content

Do not compare nonreal algebraic numbers when taking a sign - #140

Open
s-celles wants to merge 5 commits into
JuliaSymbolics:mainfrom
s-celles:fix/algebraic-closure-nonreal-comparisons
Open

s-celles wants to merge 5 commits into
JuliaSymbolics:mainfrom
s-celles:fix/algebraic-closure-nonreal-comparisons

Conversation

@s-celles

@s-celles s-celles commented Sep 13, 2026 •

Copy link
Copy Markdown
Contributor

Partial fix for #13.

Problem

RischMethod(use_algebraic_closure=true) threw as soon as the roots were not real, which is most integrands where the option would be interesting:

julia> integrate(1/(x^2 + 1), x, RischMethod(use_algebraic_closure=true, catch_errors=false))
ERROR: DomainError with {a2: 1.00000*im}: comparing nonreal numbers

Same for 1/(x^2 + 2), 1/(x^3 - 1), 1/(x^2 + x + 1) and (x + 1)/(x^2 + 4). catch_errors=true does not help, since it only intercepts NotImplementedError and AlgorithmFailedError.

Cause

positive_constant_coefficient decided whether to negate a polynomial with constant_coefficient(f) < 0. QQBarFieldElem only orders real numbers and throws DomainError otherwise, so the comparison itself was the failure — not the value being compared.

Fix

Dispatch the sign test rather than comparing unconditionally:

has_negative_sign(c) = c < 0
has_negative_sign(c::QQBarFieldElem) = iszero(imag(c)) && real(c) < 0

Rational coefficients keep the plain comparison; a nonreal algebraic number is reported as not negative, since it has no sign. Note that the naive form of this fix — calling imag on the coefficient directly — breaks the ordinary path, because imag(::QQFieldElem) does not exist; hence the dispatch.

The five integrands listed above now integrate and render. With #136 merged, they come back with exact radicals rather than floats, but the two changes are independent.

Remaining limitation

1/(x^4 + 1) still raises, from a different nonreal comparison. There integrate itself succeeds and only rendering the result throws:

julia> r = integrate(1/(x^4 + 1), x, RischMethod(use_algebraic_closure=true, catch_errors=false));

julia> typeof(r)
Num

julia> string(r)
ERROR: DomainError with {a4: 0.176777 - 0.176777*im}: comparing nonreal numbers

That is reached when the degree-4 Root placeholder in the result is displayed. It is recorded as @test_broken rather than left silent, so #13 should stay open until it is addressed too.

Tests

New test/methods/risch/test_algebraic_closure.jl, registered in the easy group: unit tests for has_negative_sign over rational, real-algebraic and nonreal-algebraic coefficients, and integration tests for the six integrands, each rendered inside the test because show reaches the same comparison. Without the fix, that file reports 11 errors and 0 passes; with it, 21 passes and the 1 documented @test_broken.

TEST_GROUP=easy passes (223 pass, 2 broken — one pre-existing, one added here). TEST_GROUP=difficult reports numbers identical to main: RuleBased 92/50/35/0, Risch 57/77/40/3.


Disclosure: this change was written with AI assistance, and tested locally before opening the PR. TEST_GROUP=easy and TEST_GROUP=difficult were run on this branch and against unmodified main. The new test file was also run with the fix reverted, to confirm it fails without it (11 errors, 0 passes), and the remaining 1/(x^4 + 1) limitation was narrowed down to rendering rather than integration by running the two steps separately.

Changes since the first review

  • Replaced the @test_broken try ... catch ... false end in "Known remaining limitation"
    with @test_throws DomainError string(result), plus an assertion that integrate itself
    succeeds. The old form accepted any exception and would have hidden an unrelated rendering
    regression. Verified that rendering the degree-4 Root placeholder throws exactly a
    DomainError (Julia 1.13.0, SymbolicUtils 4.48.0, Nemo 0.56.1); the error carries no
    message text beyond the offending root, so only the type is asserted. The testset now has
    no broken tests: 23 pass, 0 broken.
  • Formatted the new test file with Runic 1.11.1 (--check now exits 0).
  • Merged current main, so CI runs against it.

TEST_GROUP=easy on this head: 225 passed, 1 broken (pre-existing), exit 0.

Two red checks on this PR are not caused by it:

Assisted by: AI

CI on head a370e6a

33 pass, 2 fail — and the difficult group is green.

Before current main was merged in, this pull request failed nine difficult jobs. All
nine were the same single RuleBased regression, baseline entry 15,
sin(sqrt(1+x))/sqrt(1+x), which fails identically on main and which #147 fixes. Merging
main cleared them without touching a line of this change.

The two remaining failures are the SymbolicIntegrationMaxima qa jobs dying at
resolution on the stale 0.2 bound that #146 fixes; that has failed on main since the
0.3.0 release and nothing here touches it.

`RischMethod(use_algebraic_closure=true)` threw

    DomainError: comparing nonreal numbers

as soon as the roots were not real, which is most integrands of interest:
1/(x^2 + 1), 1/(x^2 + 2), 1/(x^3 - 1) and 1/(x^2 + x + 1) all failed, and
`catch_errors` does not intercept a `DomainError`.

`positive_constant_coefficient` compared a constant coefficient against
zero to decide whether to negate the polynomial. `QQBarFieldElem` only
orders real numbers and throws for the rest, so the comparison itself was
the failure. Dispatch the sign test instead: rational coefficients keep
the plain comparison, and a nonreal algebraic number is reported as not
negative, since it has no sign to speak of.

This is a partial fix for JuliaSymbolics#13. 1/(x^4 + 1) still raises, from a different
nonreal comparison: there `integrate` succeeds and only rendering the
resulting degree-4 `Root` placeholder throws. That case is recorded as
`@test_broken` rather than left silent.

`TEST_GROUP=easy` passes; `TEST_GROUP=difficult` reports numbers identical
to `main` (Risch: 57 succeeded, 77 failed, 40 maybe failed, 3 errored).

Assisted-by: AI
@codecov-commenter

codecov-commenter commented Sep 13, 2026 •

Copy link
Copy Markdown

⚠️ Please install the 'codecov app svg image' to ensure uploads and comments are reliably processed by Codecov.

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 51.24%. Comparing base (8647f0d) to head (a370e6a).
⚠️ Report is 27 commits behind head on main.
❗ Your organization needs to install the Codecov GitHub app to enable full functionality.

Additional details and impacted files
@@            Coverage Diff             @@
##             main     #140      +/-   ##
==========================================
+ Coverage   50.98%   51.24%   +0.25%     
==========================================
  Files          23       23              
  Lines        4309     4215      -94     
==========================================
- Hits         2197     2160      -37     
+ Misses       2112     2055      -57     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@ChrisRackauckas

Copy link
Copy Markdown
Member

🤖 Automated review from an AI agent running as @ChrisRackauckas — not written or reviewed by Chris. It is posted so that you can act on it. Chris or a maintainer may disagree.

Verdict: changes needed before merge (reviewed at f99af10).

Guards the polynomial sign normalization against nonreal algebraic coefficients, allowing six tested Risch antiderivatives to integrate and render. The fix passes local regression and derivative checks, but the new catch-all broken test and unformatted test file need changes.

Risk assessment

  • Risk: low
  • Blast radius: Internal Risch polynomial sign normalization, particularly use_algebraic_closure=true; rational and real-algebraic sign tests pass. No dependency additions, newly public names, or successful public API contract changes were found.
  • Evidence: Local Julia 1.10.12 easy suite: 223 passed, 2 broken, exit 0. New test file on unfixed base ea0d9e6: 11 errors, exit 1; on head: 21 passed, 1 broken, exit 0. All six antiderivatives rendered and produced residual=0 after differentiating and simplifying. Runic 1.11.1 returned exit 1, including formatting changes in the new test file. The nine failing head difficult checks reproduce the historical main dependency failure; all corresponding latest main checks pass (details below).
  • Merge: needs changes (replace the broad suppressed test and format the new test file).

Findings

  1. test/methods/risch/test_algebraic_closure.jl:54–58 — The new @test_broken try … catch … false accepts every exception from rendering as an expected failure, including unrelated regressions. This violates the stated no-new-broken-tests rule and does not verify the documented DomainError. Use a normal, narrowly specified exception assertion for the documented unsupported case, or fix rendering and assert success; do not retain blanket suppression.
  2. test/methods/risch/test_algebraic_closure.jl:38–39 — Runic rejects the newly added loop's multiline vector formatting. Format this new file with Runic. The formatter also reports existing drift in the two modified older files; a whole-file formatting sweep is not requested here.

Verification

Commands ran from the supplied scratch directory with TMPDIR=$HOME/tmp and JULIA_DEPOT_PATH=$PWD/depot:$HOME/.julia:

TEST_GROUP=easy ~/.juliaup/bin/julia +1.10 --project=repo -e 'using Pkg; Pkg.test()'
SymbolicIntegration.jl | 223 passed | 2 broken | 225 total | 3m13.9s

~/.juliaup/bin/julia +1.10 --project=base -e 'include("repo/test/methods/risch/test_algebraic_closure.jl")'
[Risch] Algebraic closure with nonreal roots | 11 errors | 11 total | 21.7s

~/.juliaup/bin/julia +1.10 --project=repo -e 'include("repo/test/methods/risch/test_algebraic_closure.jl")'
[Risch] Algebraic closure with nonreal roots | 21 passed | 1 broken | 22 total | 42.9s

The base test used an unmodified ea0d9e6 checkout with the same resolved dependencies. Nine errors concern the absent helper; the other two are actual DomainError: comparing nonreal numbers failures during integration. Local versions included Nemo 0.56.1, Symbolics 7.41.1, and SymbolicUtils 4.48.0. For the six integrands in the new loop, an additional run printed simplify(expand_derivatives(Differential(x)(result)) - f; expand=true): all six printed 0.

Runic command in a scratch environment containing Runic 1.11.1:

using Runic
exit(Runic.main(["--check", "--diff", "repo/src/methods/risch/rational_functions.jl", "repo/test/methods/risch/test_algebraic_closure.jl", "repo/test/runtests.jl"]))

Head CI easy, QA, documentation, spelling, and SymbolicIntegrationMaxima checks passed. Every failing head check below failed with the same three buffered_operate_to! exceptions as historical main run 34778027656; each corresponding push check passed in latest main CI run 35223647633:

Head check (suffix - pull_request) Head Historical main Latest main
Julia 1.10 - ubuntu-latest - difficult fail fail pass
Julia 1.10 - macos-latest - difficult fail fail pass
Julia 1.10 - windows-latest - difficult fail fail pass
Julia 1 - ubuntu-latest - difficult fail fail pass
Julia 1 - macos-latest - difficult fail fail pass
Julia 1 - windows-latest - difficult fail fail pass
Julia pre - ubuntu-latest - difficult fail fail pass
Julia pre - macos-latest - difficult fail fail pass
Julia pre - windows-latest - difficult fail fail pass

A separate investigation locally reproduced that CI error using a standalone SymbolicUtils expression with historical transitive dependencies: SymbolicUtils 4.46.6 gave 1 error, while changing only SymbolicUtils to 4.46.7 gave 1 pass. The existing upstream bisect identifies introducing commit 072624b; fix df25dd6 shipped in 4.46.7. This is independent of this PR. Full local difficult, QA, and documentation runs were not repeated; their CI results were inspected.

Read the PR description, all comments/reviews, linked issue, and complete diff. No human review or maintainer request is present on this PR; the only PR comment is Codecov. No GitHub writes were made.

Push a fix and the PR is reviewed again automatically at the new head.


🤖 Posted by an AI agent — harness: Claude Code · model: claude-opus-5-5[1m] (fleet master); review by Codex CLI 0.157.1 / gpt-6-astra
Conversation: local Claude Code session 3cd6500a-1f81-46b5-ac0b-c466e15b6a53 on Chris's Mac (session ID, no URL)

…ception

The "Known remaining limitation" testset wrapped `string(result)` in
`@test_broken try ... catch ... false end`, which accepts every exception and
would therefore hide an unrelated regression in rendering.

Rendering the degree-4 `Root` placeholder throws a `DomainError` carrying the
offending nonreal root, verified on Julia 1.13.0 with SymbolicUtils 4.48.0 and
Nemo 0.56.1, so assert exactly that with `@test_throws`, and assert that
`integrate` itself still succeeds. The testset now has no broken tests, and if
rendering is ever fixed the assertion fails and says so.

The error carries no message text beyond the offending root, unlike the
`comparing nonreal numbers` path of issue JuliaSymbolics#13, so only the type is asserted.

Also format the new test file with Runic.

Assisted-by: AI
…risons

Brings in JuliaSymbolics#147, which fixes baseline entry 15,
`sin(sqrt(1+x))/sqrt(1+x)` — the single RuleBased regression that was failing
the `difficult` gate here, and which this pull request never caused: it fails
identically on `main` and on every pull request based on the older `main`.

No conflict. `TEST_GROUP=easy`: 244 passed, 1 broken (pre-existing), exit 0,
and the `[Risch] Algebraic closure with nonreal roots` testset keeps its 23
passing assertions with no broken test.

Assisted-by: AI
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants