Skip to content

Decline a substitution of x by itself in the multiplicative exponential solver - #2432

Merged
Rafael-SOWNet merged 1 commit into
masterfrom
feature/a-negative-base-exponential-does-not-recurse
Nov 13, 2026
Merged

Rafael-SOWNet merged 1 commit into
masterfrom
feature/a-negative-base-exponential-does-not-recurse

Conversation

@Rafael-SOWNet

Copy link
Copy Markdown
Member

Closes #2424.

(-2)^x + 4^x = 0 took the process down with a stack overflow. The cycle is Solve → SolveInverting → ExponentialSolver.SolveMultiplicative → Solve:

  1. SolveLinear rewrites (-2)^x + 4^x as t^ln(-2) + t^ln(4) in t = e^x.
  2. SolveMultiplicative chooses a proper power t^unit only when every exponent is real. ln(-2) = ln 2 + iπ is not real, so the substitution stays t itself.
  3. Substituting t by itself only renames the variable, so the solver solved the same equation again, forever.

A negative base was only one way in. Any non-real numeric exponent overflowed the same way on 2.6.0 and on master: x^i + x = 0, x^(1+i) + x = 0, x^i + x^(2*i) = 0.

The fix declines a substitution of x by itself, since it is a renaming and never a step. With the cycle gone, Newton's method answers these equations, and two more changes keep its answers roots:

  • The convergence test is relative near 0. It was absolute (last step < 1e-6). Near the zero at t = 0 that every term shares, the iteration crawled in steps under 1e-6 while still about 18% of |t| from where it settles, and returned points 0.0004 and 0.23 from the nearest root. The step is now measured against |value| below 1 and is unchanged from 1 up, so this can only drop a point, never add one.
  • A point where the equation is undefined at exact arguments is not a root. x^i + x = 0 would otherwise answer 0, where 0^i has no value. A decimal argument never counts as undefined, so a root found in decimals still survives rounding and overflow. Across 2184 existing tests, the new rule fired only in its own test.

With principal powers, the roots of a^x + s·b^x = 0 are x = (ln(−s) + 2πik)/(ln a − ln b). Every root below lies on that family, within 2e-15.

equation = 0 2.6.0 and master this branch
(-2)^x + 4^x stack overflow 9 roots, k = −7..1
(-3)^x + 9^x stack overflow 7 roots, k = −5..1
(-2)^x - 4^x stack overflow 8 roots, k = −6..1, including 0
(-2)^x + 2^x stack overflow { -5, -3, -1, 1, 3 }
x^i + x stack overflow { -e^(π/2) i, e^(-π/2) i }
x^i + x^(2*i) stack overflow { e^π }
x^(1+i) + x stack overflow 0, e^−5π, e^−3π, e^−π, e^π (0^(1+i) is 0, so 0 is a root)

Each takes 290–470 ms. The answers are the roots inside Newton's search region, not the whole infinite family: that is #2319's question, and its rates have no common real unit here. The rows are in BREAKING-CHANGES.md.

Checked on this branch:

  • Tests: UnitTests 19363 passed, plus the MCP, Terminal, InteractiveWrapper and F# wrapper suites, and the AOT smoke test. The new test runs each solve under a termination guard and asserts that every root returned has a relative residual of at most 1e-9 and lies on the family. An unsolved answer is accepted; an empty one is not.
  • Harnesses: BoundCheck, RootCheck, CasBench, PropCheck, SimpSweep, RuleCheck, CanonCheck and Confluence all pass.

🤖 Generated with Claude Code

…al solver (#2424)

Solving (-2)^x + 4^x = 0 overflowed the stack. The linear exponential
solver writes it as t^ln(-2) + t^ln(4) in t = e^x. The multiplicative
solver then picks a power of t to substitute only where every exponent
is real, and ln(-2) is not. So the substitution fell back to t itself,
which renamed the variable and asked the same equation again without
end. x^i + x = 0 and x^(1 + i) + x = 0 overflowed the same way. The
substitution is declined now, and the equation goes on to the other
routes.

Newton's method then answers it, and two of its checks let non-roots
through.

It counted a point as converged when its last step was under 1e-6.
Near t = 0, where both terms vanish, the steps are that small while the
point is still 18% of its own size from where it settles. Points like
that came back as roots, 0.0004 and 0.23 from the nearest one. Below 1
the step is measured against the point now; from 1 up the test is
unchanged, so the change can only drop a point, never add one.

And it compiles 0^i to 0, so x^i + x = 0 had 0 among its roots. The
check against the equation kept it, because a residual that is not
finite counted as no evidence. That protects a root found in decimals,
where rounding or overflow can leave no value at a genuine root. A
function undefined at exact arguments, 0^i or 1/0, has neither excuse,
and is now evidence against the root, in Newton's check and in the
solver's own.

Every root answered for (-2)^x + 4^x, (-3)^x + 9^x, (-2)^x - 4^x and
(-2)^x + 2^x lies on x = (ln(-s) + 2 pi i k) / (ln a - ln b), within
2e-15. The family itself is #2319. Stack overflow -> about 400 ms each.
In the Solving tests and the other users of Newton's method, the new
rule rejected no root. Solving tests 1890 passed, CasBench 120/121
with 0 wrong, RootCheck 596/596 clean, all as on master.

Closes #2424.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@codecov

codecov Bot commented Nov 13, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 94.11765% with 1 line in your changes missing coverage. Please review.
✅ Project coverage is 86.81%. Comparing base (2eea2e9) to head (ce6aa87).
⚠️ Report is 1 commits behind head on master.

Files with missing lines Patch % Lines
...ontinuous/Solvers/EquationSolver/SolveStatement.cs 87.50% 0 Missing and 1 partial ⚠️
Additional details and impacted files

Impacted file tree graph

@@            Coverage Diff             @@
##           master    #2432      +/-   ##
==========================================
- Coverage   86.81%   86.81%   -0.01%     
==========================================
  Files         312      312              
  Lines       69551    69564      +13     
  Branches    20316    20322       +6     
==========================================
+ Hits        60383    60392       +9     
- Misses       4932     4938       +6     
+ Partials     4236     4234       -2     
Flag Coverage Δ
cpp 37.77% <ø> (ø)
csharp 87.35% <94.11%> (-0.01%) ⬇️
fsharp 32.97% <ø> (ø)
interactive 39.81% <ø> (ø)

Flags with carried forward coverage won't be shown. Click here to find out more.

Files with missing lines Coverage Δ
...inuous/Solvers/EquationSolver/ExponentialSolver.cs 86.60% <100.00%> (+0.24%) ⬆️
...ontinuous/Solvers/NumericalSolving/NewtonSolver.cs 96.06% <100.00%> (+0.09%) ⬆️
...ontinuous/Solvers/EquationSolver/SolveStatement.cs 83.85% <87.50%> (+0.13%) ⬆️

... and 7 files with indirect coverage changes

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

@Rafael-SOWNet
Rafael-SOWNet merged commit 376164d into master Nov 13, 2026
35 checks passed
@github-project-automation github-project-automation Bot moved this from Todo to Done in AngouriMath Nov 13, 2026
@Rafael-SOWNet
Rafael-SOWNet deleted the feature/a-negative-base-exponential-does-not-recurse branch November 13, 2026 19:59
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

Solving (-2)^x + 4^x = 0 overflows the stack

1 participant