Repository navigation
Keep the domain of a substituted variable in a change of variables - #147
Conversation
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 Report❌ Patch coverage is
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. 🚀 New features to boost your workflow:
|
|
Some coordination, since I have several pull requests open at once and they all show the same Two systemic failures were affecting every open pull request:
The two are complementary: each turns green what the other leaves red. Measured, not assumed — A merge order that avoids re-running red CI for nothing:
Happy to reorder, rebase or split any of these if you would rather look at them differently. Assisted by: AI |
|
Thanks @ChrisRackauckas for merging |
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
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
…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
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
…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
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
Fixes #145.
∫ sin(sqrt(1+x))/sqrt(1+x) dxcomes back unevaluated. It is entry 15 of the difficultbaseline, and it is the single regression that fails the
difficultgate on every currentlyopen pull request.
What happens
With verbose tracing, rule
4_1_12_81changes variables tou = (1 + x)^(1//2):uis a principal square root, sosqrt(u^2)isuand the integrand is justsin(u).Nothing records that
u >= 0:int_and_substreceivesto, the expression the new variablestands for, and discards it.
simplifythen turns(u^2)^(1//2)intoabs(u), which no rulematches.
The upstream rule producing that
absis correct — it was added inJuliaSymbolics/SymbolicUtils.jl#1077 to fix the unsound fold of
sqrt(x^2)tox(JuliaSymbolics/SymbolicUtils.jl#1045). The gap is on this side.
The change
int_and_substnow usesto: when the new variable stands for a principal even root, itrewrites
abs(u),sqrt(u^2)and(u^2)^(1//2)toubefore integrating.nonnegative_substitutionis deliberately narrow:sqrt(...), or a power whose exponent is apositive rational with even denominator. An odd root such as
(1 + x)^(1//3)can be negative,so it is not claimed nonnegative and its
absis 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 theeasygroup: 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, SymbolicUtils4.48.0, Symbolics 7.41.1, Nemo 0.56.1, AbstractAlgebra 0.50.2.
TEST_GROUP=difficultcould not be completed on the machine I am on, so the CI gate is thecheck to read here. Since entry 15 is the only regression that gate currently reports, this
should turn
difficultgreen — on this branch and, once merged, on the other open pullrequests. 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:
qajobs of theSymbolicIntegrationMaximaworkflow, whichcannot resolve since the 0.3.0 release of that sub-package. It has failed on
mainsincethen, so every pull request that merges current
maininherits it.difficultregression, entry 15, which is the only thingfailing that gate anywhere.
anything; it just stops the harness asking for it on every run.
A merge order that avoids re-running red CI for nothing:
difficultis green on the four review pull requests below.Do not compare nonreal algebraic numbers when taking a sign #140, which are independent of the rest.
I verified on this branch that
difficultis green (33 checks pass, the two remainingfailures being the Maxima
qajobs that #146 fixes), and on #146 that its twoqajobs pass(9 failures instead of 11). Maintainers may of course prefer another order; this is only what
the dependencies suggest.