Skip to content

Do on the one-sided path what the two-sided one does - #697

Merged
Happypig375 merged 3 commits into
ASC-Community:masterfrom
Rafael-SOWNet:fix/one-sided-limit-fallback
Aug 4, 2026
Merged

Happypig375 merged 3 commits into
ASC-Community:masterfrom
Rafael-SOWNet:fix/one-sided-limit-fallback

Conversation

@Rafael-SOWNet

@Rafael-SOWNet Rafael-SOWNet commented Aug 4, 2026 •

Copy link
Copy Markdown
Member

The defect

The one-sided path skipped everything the two-sided path has — both in front of the descent and behind it.

In front, four transformations ran only under side is BothSides: the sec/csc rewrite, the substitution of finite sub-limits, and the first and second remarkable limits. Without the second one, the descent reads (1 + x)^(1/x) at 0+ as 1^(+oo) and answers 1 — definitely, not as a NaN:

lim x->0+ (1 + x)^(1/x)   ==   1        lim x->0 (1 + x)^(1/x)   ==   e
lim x->0+ (1 + 2x)^(1/x)  ==   1        lim x->0 (1 + 2x)^(1/x)  ==   e^2

Behind, there was nothing at all:

if (side is ApproachFrom.Left or ApproachFrom.Right)
    return expr.ComputeLimitDivideEtImpera(x, dest, side);

The descent can make an indeterminate form definite on the way down, by putting a part's own limit in place of the part. x * ln(x) at 0+ becomes 0 * ln(x), so 0 * -oo, so NaN — and NaN here is not "not found", it is the claim that the limit does not exist. The clearest case is not exotic:

lim x->0+ sin(x)/x   ==   NaN
lim x->0- sin(x)/x   ==   NaN

The two-sided form gives 1. Only the one-sided ones said it did not exist.

The fix

Hoist the four transformations above the side dispatch. Each is guarded on a two-sided limit, and a two-sided limit that exists is also the limit from either side, so this says nothing one-sided that they did not already say two-sided. The BothSides block is unchanged in behaviour — same passes, same order.

Give the one-sided path the fallback. Where nothing above answers, move x out to infinity and ask there — which is what ComputeLimitImpl already does for a finite destination, so it is the same question asked where l'Hôpital's rule and the rest of the machinery live. Whatever the descent found is kept if this finds nothing better, so it can only add answers.

The substituted expression is simplified first, and not for tidiness. The destination is left behind as a term, and it is exactly that leftover which produces the NaN:

lim x->+oo (0 + 1/x) * ln(0 + 1/x)   ==   NaN
lim x->+oo (1/x) * ln(1/x)           ==   0

Measured

30 one-sided limits, answers worked out by hand:

master + fallback + both
correct 14 19 22
wrong 15 10 7
unevaluated 1 1 1

Fixed: sin(x)/x and x/sin(x) and (1+x)^(1/x) from both sides, tan(x)/x, x*ln(x), (1+2x)^(1/x). Nothing that was right became wrong. Full suite Failed: 0, Passed: 4035, Skipped: 14, Total: 4049, at 3m06s — unchanged, because the fallback is only consulted where the answer was already nothing or NaN.

What this does not fix

Seven remain wrong, and I am not claiming them.

Three want the machinery at infinity to be stronger, not a different bridge to it — x^2*ln(x), sqrt(x)*ln(x), x^x. Their substituted forms are 0·∞ at infinity, which master cannot do either. They answer correctly the moment #682 and #694 are in, with no further change here — on my combined branch this same corpus scores 27 of 30.

The others are their own defects. csc(x)*x shows the shape of it: the rewrite turns it into (1/sin(x))*x, a product, and ApplyFirstRemarkable only matches a Divf. (1 - cos(x))/x^2 and 1/x - 1/sin(x) likewise need something this PR does not add.

This is part of #209 rather than the whole of it: 1/x + ln(x) at 0+ is still unevaluated on master, because the ∞−∞ it becomes needs #682. With that in, it gives +oo.

Tests

Sources/Tests/UnitTests/Calculus/OneSidedLimitTest.cs, 26 cases. 10 fail without the source change (verified by restoring Solvers.Definition.cs from origin/master in place and re-running). The rest pin the one-sided answers that were already right, and that an infinite destination is left alone — the fallback must not fire where substituting for x would mean nothing.

🤖 Generated with Claude Code

@codecov-commenter

codecov-commenter commented Aug 4, 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 85.71429% with 3 lines in your changes missing coverage. Please review.
✅ Project coverage is 80.44%. Comparing base (90c00a8) to head (ee916f2).
⚠️ Report is 58 commits behind head on master.

Files with missing lines Patch % Lines
...ns/Continuous/Limits/Solvers/Solvers.Definition.cs 83.33% 1 Missing and 2 partials ⚠️
❗ Your organization needs to install the Codecov GitHub app to enable full functionality.
Additional details and impacted files
@@            Coverage Diff             @@
##           master     #697      +/-   ##
==========================================
- Coverage   80.99%   80.44%   -0.55%     
==========================================
  Files         155      156       +1     
  Lines       13687    12929     -758     
  Branches     1957     2125     +168     
==========================================
- Hits        11086    10401     -685     
+ Misses       1990     1922      -68     
+ Partials      611      606       -5     

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

The one-sided path skipped everything the two-sided path has, in front of
the descent and behind it.

In front, four transformations ran only under `side is BothSides`: the
sec/csc rewrite, the substitution of finite sub-limits, and the first and
second remarkable limits. Without the second one, (1 + x)^(1/x) at 0+ is
read by the descent as 1^(+oo), and it answers 1 -- definitely, not as a
NaN -- where the same expression approached from both sides gives e. Each
of the four is guarded on a two-sided limit, and a two-sided limit that
exists is also the limit from either side, so hoisting them above the
dispatch says nothing one-sided they did not already say two-sided.

Behind, there was nothing at all:

    if (side is ApproachFrom.Left or ApproachFrom.Right)
        return expr.ComputeLimitDivideEtImpera(x, dest, side);

The descent can make an indeterminate form definite on the way down, by
putting a part's own limit in place of the part. x * ln(x) at 0+ becomes
0 * ln(x), so 0 * -oo, so NaN -- and NaN is the claim that the limit does
not exist, not that it was not found. sin(x) / x from either side came
back the same way: the library said the most familiar limit in the subject
did not exist. Where nothing above answers, this now moves x out to
infinity and asks there, which is what ComputeLimitImpl does for a finite
destination in any case. Whatever the descent found is kept if that finds
nothing better, so it can only add answers.

The substituted expression is simplified before being asked about, and not
for tidiness: the destination is left behind as a term, and it is exactly
that leftover which produces the NaN. (0 + 1/x) * ln(0 + 1/x) at infinity
is NaN where (1/x) * ln(1/x) is 0.

Measured over 30 one-sided limits worked out by hand: 14 correct and 15
wrong before, 22 correct and 7 wrong after. Nothing that was right became
wrong. Suite unchanged at 3m06s.

Seven remain wrong and this does not claim them. Three want the machinery
at infinity to be stronger and answer once the differences-at-infinity and
Gruntz branches are in, with no further change here. The others --
(1 - cos(x))/x^2, 1/x - 1/sin(x) and csc(x)*x -- do not, and csc(x)*x
shows why: the rewrite turns it into (1/sin(x))*x, a product, and the
first remarkable limit only matches a quotient.

Part of #209.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@Rafael-SOWNet
Rafael-SOWNet force-pushed the fix/one-sided-limit-fallback branch from 7ea4508 to 0175d73 Compare August 4, 2026 15:54
@Rafael-SOWNet Rafael-SOWNet changed the title Give a one-sided limit the fallback the other paths have Do on the one-sided path what the two-sided one does Aug 4, 2026
@Rafael-SOWNet

Copy link
Copy Markdown
Member Author

CI, run on my fork across the full matrix, since the run here is held at action_required:

https://github.com/Rafael-SOWNet/AngouriMath/actions/runs/30926523989

job result tests
Test (ubuntu-latest) success Failed: 0, Passed: 4035, Skipped: 14, Total: 4049
Test (windows-latest) success Failed: 0, Passed: 4035, Skipped: 14, Total: 4049
Test (macos-latest) success Failed: 0, Passed: 4035, Skipped: 14, Total: 4049

The branch it ran on, ci-check/one-sided, is this PR's head plus one commit that only touches .github/workflows/CSharpTest.yml, so the run can be triggered at all. Everything under Sources/ is byte-identical to this PR:

$ git diff --stat fix/one-sided-limit-fallback ci-check/one-sided
 .github/workflows/CSharpTest.yml | 1 +
 1 file changed, 1 insertion(+)

The rule was reached from the two-sided path only, so a one-sided limit of a
quotient the descent has no reading of had nothing behind it at all. It is the
same rule either way: l'Hopital's is stated one-sidedly to begin with, and the
two-sided case is the two one-sided ones agreeing.

The side is carried through rather than dropped, because both questions the rule
asks along the way can have a one-sided answer and no two-sided one. The first
step of (1 - cos(x)) / x^3 reaches sin(x) / (3x^2), which is +oo on the right
and -oo on the left; asked about both sides at once it comes back NaN and the
step is wasted. Measured on a corpus of 49 one-sided limits with known answers:
36 correct without carrying the side and calling the rule at all, 40 with the
rule but asked two-sided, 42 with the side carried. No answer changed for the
worse in any of the three.

csc(x) * x is the one worth naming: the csc rewrite leaves the product
(1 / sin(x)) * x, which the descent does not take apart, and the rule reads it
back as the quotient x / sin(x).

One-sided corpus 36/49 -> 42/49. Two-sided corpus unchanged at 101/117, 0 wrong.
Suite 4041 passed, 0 failed.
@Rafael-SOWNet

Copy link
Copy Markdown
Member Author

Follow-up commit ee916f2f: l'Hopital's rule is now reached from the one-sided path too, and asked with the side it was given.

The rule was reached from the two-sided path only, so a one-sided limit of a quotient the descent has no reading of had nothing behind it at all. It is the same rule either way — l'Hopital's is stated one-sidedly to begin with, and the two-sided case is the two one-sided ones agreeing.

Carrying the side rather than dropping it is load-bearing, and I measured that rather than assuming it. Both questions the rule asks along the way can have a one-sided answer and no two-sided one: the first step of (1 - cos(x)) / x^3 reaches sin(x) / (3x^2), which is +oo on the right and -oo on the left, so asked about both sides at once it comes back NaN and the step is wasted.

On a corpus of 49 one-sided limits with known answers:

correct
master 16/49 (3 of them wrong, not merely unanswered)
this PR before the follow-up 36/49
follow-up, rule asked two-sided 40/49
follow-up as pushed 42/49

No answer changed for the worse at any step. csc(x) * x is the one worth naming: the csc rewrite leaves the product (1 / sin(x)) * x, which the descent does not take apart, and the rule reads it back as the quotient x / sin(x).

Two-sided corpus unchanged at 101/117 with 0 wrong / 0 error / 0 timeout, measured with and without the change on this same branch.

CI on the fork, matrix run 30933608421 — success on windows-latest, ubuntu-latest and macos-latest, Failed: 0, Passed: 4041, Skipped: 14, Total: 4055 on each. The branch ci-check/one-sided-limits is this PR's head ee916f2f plus one commit that only adds a trigger to the workflow file; Sources/ is byte-identical.

@Happypig375
Happypig375 merged commit ecd5759 into ASC-Community:master Aug 4, 2026
24 checks passed
@Rafael-SOWNet
Rafael-SOWNet deleted the fix/one-sided-limit-fallback branch August 4, 2026 20:49
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.

3 participants