Skip to content

Keep the domain of a substituted variable in a change of variables - #147

Merged
ChrisRackauckas merged 1 commit into
JuliaSymbolics:mainfrom
s-celles:fix/substitution-domain
Sep 27, 2026
Merged

ChrisRackauckas merged 1 commit into
JuliaSymbolics:mainfrom
s-celles:fix/substitution-domain

Conversation

@s-celles

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

Copy link
Copy Markdown
Contributor

Fixes #145.

∫ sin(sqrt(1+x))/sqrt(1+x) dx comes back unevaluated. It is entry 15 of the difficult
baseline, and it is the single regression that fails the difficult gate on every currently
open pull request.

What happens

With verbose tracing, rule 4_1_12_81 changes variables to u = (1 + x)^(1//2):

┌-------Applied rule 4_1_12_81 (change of variables):
└-------with result: ∫(sin(u)*u) / ((u^2)^(1//2)) du where u = (1 + x)^(1//2)
No rule found for ∫((sin(x)*x) / ((x^2)^(1//2)), x)
                → ∫((sin(x)*x) / abs(x), x)
No rule found for ∫((sin(x)*x) / abs(x), x)

u is a principal square root, so sqrt(u^2) is u and the integrand is just sin(u).
Nothing records that u >= 0: int_and_subst receives to, the expression the new variable
stands for, and discards it. simplify then turns (u^2)^(1//2) into abs(u), which no rule
matches.

The upstream rule producing that abs is correct — it was added in
JuliaSymbolics/SymbolicUtils.jl#1077 to fix the unsound fold of sqrt(x^2) to x
(JuliaSymbolics/SymbolicUtils.jl#1045). The gap is on this side.

The change

int_and_subst now uses to: when the new variable stands for a principal even root, it
rewrites abs(u), sqrt(u^2) and (u^2)^(1//2) to u before integrating.

integrate(sin(sqrt(1 + x)) / sqrt(1 + x), x)
# before: ∫(sin(sqrt(1 + x)) / sqrt(1 + x), x)
# after:  -2cos((1 + x)^(1//2))

nonnegative_substitution is deliberately narrow: sqrt(...), or a power whose exponent is a
positive rational with even denominator. An odd root such as (1 + x)^(1//3) can be negative,
so it is not claimed nonnegative and its abs is kept. A test pins that in both directions,
because getting it wrong would silently drop an absolute value that carries meaning.

Tests

test/methods/rule_based/test_substitution_domain.jl, in the easy group: the two helpers
(including the odd-root and non-root cases that must stay untouched, and that only the named
variable is rewritten), the integral of #145 against its expected antiderivative, and two
neighbouring integrands from the same rule family to catch a rewrite that is too eager.
19 passed.

TEST_GROUP=easy: 221 passed, 1 broken (pre-existing), exit 0. Julia 1.13.0, SymbolicUtils
4.48.0, Symbolics 7.41.1, Nemo 0.56.1, AbstractAlgebra 0.50.2.

TEST_GROUP=difficult could not be completed on the machine I am on, so the CI gate is the
check to read here. Since entry 15 is the only regression that gate currently reports, this
should turn difficult green — on this branch and, once merged, on the other open pull
requests. Note that the same runs also report five Risch improvements ([3], [26], [27],
[49], [55]) which show up on unrelated pull requests too: they come from dependency drift and
would be worth a separate baseline refresh.

Assisted by: AI

How this sits with the other open pull requests

Seven of mine are open at the moment, and every one of them shows the same two red groups for
reasons none of them introduced. Two of the seven remove those causes, and they are
complementary — each one turns green what the other leaves red:

A merge order that avoids re-running red CI for nothing:

  1. Allow SymbolicIntegrationMaxima 0.3 in the qa test environment #146 and Refresh the Risch baseline for five entries the engine now verifies #148 — one line and five digits respectively, no behaviour change.
  2. this PR (Keep the domain of a substituted variable in a change of variables #147) — after it, difficult is green on the four review pull requests below.
  3. Keep algebraic coefficients exact in Risch antiderivatives #136, then Return exact roots from the rule-based backend #141 (which merges Keep algebraic coefficients exact in Risch antiderivatives #136 and needs its compat bound), and Stop calling the deprecated AbstractAlgebra zeros #138 and
    Do not compare nonreal algebraic numbers when taking a sign #140, which are independent of the rest.

I verified on this branch that difficult is green (33 checks pass, the two remaining
failures being the Maxima qa jobs that #146 fixes), and on #146 that its two qa jobs pass
(9 failures instead of 11). Maintainers may of course prefer another order; this is only what
the dependencies suggest.

Fixes JuliaSymbolics#145.

`∫ sin(sqrt(1+x))/sqrt(1+x) dx` came back unevaluated. With verbose tracing,
rule `4_1_12_81` substitutes `u = (1 + x)^(1//2)` and hands
`∫ sin(u)*u / (u^2)^(1//2) du` to the engine; `simplify` turns the denominator
into `abs(u)`, no rule matches it, and the integral is returned as a placeholder.

`u` is a principal square root, so it cannot be negative and `sqrt(u^2)` is just
`u`. Nothing records that: `int_and_subst` gets `to`, the expression the new
variable stands for, and then throws the information away. It now uses it, and
rewrites `abs(u)`, `sqrt(u^2)` and `(u^2)^(1//2)` to `u` when `to` is a principal
even root.

`nonnegative_substitution` is deliberately narrow: `sqrt(...)` and a power with a
positive rational exponent of even denominator. An odd root such as
`(1 + x)^(1//3)` can be negative, so it is not claimed nonnegative and its `abs`
is kept, which a test pins in both directions.

The upstream rule that produces the `abs` is correct; it was added in
SymbolicUtils #1077 to fix the unsound fold of `sqrt(x^2)` to `x`
(SymbolicUtils #1045). The gap was on this side.

    integrate(sin(sqrt(1 + x))/sqrt(1 + x), x)
    # before: ∫(sin(sqrt(1 + x)) / sqrt(1 + x), x)
    # after:  -2cos((1 + x)^(1//2))

`TEST_GROUP=easy`: 221 passed, 1 broken (pre-existing), exit 0. This is entry 15
of the difficult baseline, which every open pull request currently fails, so the
`difficult` gate in CI is the check to watch.

Assisted-by: AI
@codecov-commenter

codecov-commenter commented Sep 27, 2026 •

Copy link
Copy Markdown

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

Codecov Report

❌ Patch coverage is 84.21053% with 3 lines in your changes missing coverage. Please review.
✅ Project coverage is 50.66%. Comparing base (8647f0d) to head (38dbab9).
⚠️ Report is 26 commits behind head on main.

Files with missing lines Patch % Lines
src/methods/rule_based/rules_utility_functions.jl 84.21% 3 Missing ⚠️
❗ Your organization needs to install the Codecov GitHub app to enable full functionality.
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #147      +/-   ##
==========================================
- Coverage   50.98%   50.66%   -0.32%     
==========================================
  Files          23       23              
  Lines        4309     4257      -52     
==========================================
- Hits         2197     2157      -40     
+ Misses       2112     2100      -12     

☔ 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.

@s-celles

Copy link
Copy Markdown
Contributor Author

Some coordination, since I have several pull requests open at once and they all show the same
red checks for reasons none of them introduced.

Two systemic failures were affecting every open pull request:

The two are complementary: each turns green what the other leaves red. Measured, not assumed —
#146 goes from 11 failures to 9 (its two qa jobs pass), and this branch reports 33 checks
passing with only those two qa jobs left.

A merge order that avoids re-running red CI for nothing:

  1. Allow SymbolicIntegrationMaxima 0.3 in the qa test environment #146 and Refresh the Risch baseline for five entries the engine now verifies #148 — one line and five baseline digits, no behaviour change.
  2. Keep the domain of a substituted variable in a change of variables #147 (this one) — after it, difficult is green on the four pull requests below.
  3. Keep algebraic coefficients exact in Risch antiderivatives #136, then Return exact roots from the rule-based backend #141 (which merges Keep algebraic coefficients exact in Risch antiderivatives #136 and needs the SymbolicUtils = "4.46.8" bound it sets),
    plus Stop calling the deprecated AbstractAlgebra zeros #138 and Do not compare nonreal algebraic numbers when taking a sign #140, which are independent.

Happy to reorder, rebase or split any of these if you would rather look at them differently.
One note for whoever reviews #141: the automated review of it pointed at two real bugs, and
fixing them also required rebuilding rubi_sqrt, because #136 no longer introduces the
exact_sqrt helper that branch was written against. Its description has the details.

Assisted by: AI

@ChrisRackauckas
ChrisRackauckas merged commit 4e27faa into JuliaSymbolics:main Sep 27, 2026
33 of 35 checks passed
@s-celles

Copy link
Copy Markdown
Contributor Author

Thanks @ChrisRackauckas for merging

s-celles-bot added a commit to s-celles/SymbolicIntegration.jl that referenced this pull request Oct 2, 2026
SymbolicUtils #1079, the issue this PR's note and its numeric residual check
worked around, was fixed by SymbolicUtils #1108 and closed on 2026-10-01. The
fix is in v4.49.0 and absent from v4.48.0, which the old `"4.46.8"` bound still
allowed; verified on 4.49.0 that `simplify(term(sqrt, 2)^2)` gives `2` rather
than `2.0`.

The documented caveat was therefore wrong and is rewritten rather than dropped,
because a weaker version of it survives for a different reason. Measured on
4.49.0, the symbolic residual `d/dx F - f` now cancels to zero for `1/(x^3 - 1)`,
`1/(x^4 + 1)`, `1/(x^2 + 2)` and `x/(x^4 + 1)`, but not for `1/(x^2 - 2)` and
`1/(x^2 - 3)`, where `simplify` stops at an uncancelled product of conjugate
factors. That residual contains no floating point number and evaluates to about
1e-17 at several points, so it is a completeness limit of `simplify`, not a loss
of exactness.

The numeric residual check stays for that reason, and the test gains the
assertion that 4.49 makes meaningful: the simplified residual must contain no
floating point literal, which is exactly what #1079 used to break.

`TEST_GROUP=easy`: 264 passed, 1 broken (pre-existing), exit 0, with
SymbolicUtils 4.49.0 and Symbolics 7.42.0. The new test file is Runic-clean.
Also merges current main, which brings in JuliaSymbolics#147.

Assisted-by: AI
ChrisRackauckas-Claude pushed a commit to s-celles/SymbolicIntegration.jl that referenced this pull request Oct 7, 2026
Bring in JuliaSymbolics#147 so the difficult CI baseline matches current main; the qa
compat fix itself is unchanged.

Co-Authored-By: Chris Rackauckas <accounts@chrisrackauckas.com>
Co-Authored-By: Cursor Agent <noreply@cursor.com>
Agent-Harness: Cursor Agent CLI
Agent-Model: auto
s-celles-bot added a commit to s-celles/SymbolicIntegration.jl that referenced this pull request Oct 9, 2026
…in-rules

Brings JuliaSymbolics#136 up to date in this branch, and with it current main, which had made
this pull request conflict: JuliaSymbolics#147 added `nonnegative_substitution`,
`drop_abs_of_nonnegative` and their hook in `int_and_subst` to the same
`rules_utility_functions.jl` that this branch rewrites.

Only `test/runtests.jl` conflicted, where both sides add an include; both are
kept. The source file merged cleanly, and all of the functions from both sides
are present: `nonnegative_substitution` and `drop_abs_of_nonnegative` from
JuliaSymbolics#147, `algebraic_constant_value`, `exact_sign`, `constant_sign`,
`compare_constants`, `exact_roots_in_rule` and `rubi_sqrt` from this branch.
The `SymbolicUtils = "4.49"` bound comes in from JuliaSymbolics#136.

`TEST_GROUP=easy`: 301 passed, 1 broken (pre-existing), exit 0.

Assisted-by: AI
s-celles-bot added a commit to s-celles/SymbolicIntegration.jl that referenced this pull request Oct 9, 2026
Brings in JuliaSymbolics#147, which fixes baseline entry 15,
`sin(sqrt(1+x))/sqrt(1+x)`. That entry was the single RuleBased regression
failing the `difficult` gate on this pull request, and it was never caused by
it: it fails the same way on `main` and on every other open pull request based
on the older `main`.

No conflict. `TEST_GROUP=easy`: 250 passed, 1 broken (pre-existing), exit 0,
with zero deprecation warnings in the whole run, which is what this pull request
is about.

Assisted-by: AI
s-celles-bot added a commit to s-celles/SymbolicIntegration.jl that referenced this pull request Oct 9, 2026
…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
s-celles-bot added a commit to s-celles/SymbolicIntegration.jl that referenced this pull request Oct 9, 2026
Brings in JuliaSymbolics#147, which fixes baseline entry 15,
`sin(sqrt(1+x))/sqrt(1+x)`. That was the single RuleBased regression failing the
`difficult` gate on this pull request; it came from `main`, not from refreshing
the baseline.

No conflict. `TEST_GROUP=difficult` on the merged branch: exit 0, **no
regressions**, and the five Risch entries this pull request tightens no longer
appear as improvements — they now match the baseline, which is the whole point.
RuleBased 92 succeeded, Risch 58, 0 errored.

The one improvement the run still reports, entry 132, is left alone on purpose:
its own comment records that RuleBased returns code 1 on most platforms but code
2 on Julia pre and Windows, so the tolerant value is deliberate and tightening
it would break those two.

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.

[RuleBased] change of variables loses the domain of the substituted variable, breaking ∫sin(sqrt(1+x))/sqrt(1+x)dx

4 participants