Skip to content

Put a difference of two divergent fractions over one denominator - #699

Merged
Rafael-SOWNet merged 3 commits into
ASC-Community:masterfrom
Rafael-SOWNet:fix/difference-of-fractions
Aug 4, 2026
Merged

Rafael-SOWNet merged 3 commits into
ASC-Community:masterfrom
Rafael-SOWNet:fix/difference-of-fractions

Conversation

@Rafael-SOWNet

Copy link
Copy Markdown
Member

lim x -> 0 (1/x - 1/sin(x)) answers NaN. It is 0.

Nothing in the descent takes a difference apart any further than asking what each half tends to, and each half here is an infinity: the difference is +oo - +oo on the right and -oo - -oo on the left. NaN is not "I could not tell" — it is the claim that the limit does not exist.

Written over the common denominator the same expression is (sin(x) - x) / (x * sin(x)), an ordinary 0/0 that l'Hopital's rule settles in three steps.

Where the change goes

AsQuotient already existed for exactly this: it hands the rule the quotients that are not written as quotients, which is why lim x -> +oo x^4 * e^(-x) works (#596). A sum of fractions is the second kind of those, so it goes in the same place — AsQuotient is now the one answer to "what are the numerator and the denominator of this", for a product and for a sum of them alike.

Two guards, both to keep the rule from being handed work it cannot use:

  • Only where a denominator contains the variable. It is the denominators that vanish or diverge that make the difference indeterminate. Combining over a constant gains nothing and still costs the rule an expression to differentiate.
  • Only up to three terms. Each term multiplies the numerator by every other term's denominator, so the expression grows with the square of the count. Two and three terms is what the forms that need this are written with.

What it answers

before after
1/x - 1/sin(x) at 0 NaN 0
1/sin(x) - 1/x at 0 NaN 0
csc(x) - cotan(x) at 0 NaN 0
1/ln(x) - 1/(x - 1) at 1 NaN 1/2
1/(x - 1) - 1/ln(x) at 1 NaN -1/2

Five of the eleven new tests fail without the change.

Not everything in the family lands. 1/x - 1/tan(x) and 1/x^2 - 1/sin(x)^2 are still NaN, but the rewrite is not what is stopping them: (tan(x) - x) / (x * tan(x)) written out by hand is NaN too, so the limitation is further down in what the rule can push a quotient through. Worth its own look, not this PR's.

Measurements

  • Suite: Failed: 0, Passed: 4020, Skipped: 14, Total: 4034.
  • A 117-problem corpus of simplifications, solves, integrals and limits: unchanged at 101/117 solved, with 0 wrong / 0 error / 0 timeout, measured with and without the change on this branch. The corpus has no difference-at-a-point in it, so no movement was expected; what it is here for is to show nothing else moved.

Independent of my other open PRs. It touches AsQuotient in Transformations.cs, which #697 also touches, but in a different part of the file — git merge-tree reports no conflict between them.

Fork CI to follow in a comment.

Nothing in the descent takes a difference apart any further than asking what
each half tends to, so 1/x - 1/sin(x) at 0 came out as +oo - +oo on the right
and -oo - -oo on the left, and NaN either way -- the claim that the limit does
not exist, where it is 0. Written over the common denominator the same
expression is (sin(x) - x) / (x * sin(x)), an ordinary 0/0 that l'Hopital's rule
settles in three steps.

The rewrite goes where the products already went. AsQuotient existed to hand the
rule the quotients that are not written as one, and a sum of fractions is the
second kind of those; it is now the one place that answers what the numerator
and the denominator of an expression are, for a product and for a sum of them
alike.

Only where a denominator contains the variable, since it is the denominators
that vanish or diverge that make the difference indeterminate, and combining
over a constant gains nothing while still costing the rule an expression to
differentiate. Only up to three terms: each one multiplies the numerator by
every other term's denominator, so the expression grows with the square of the
count, and two or three is what the forms that need this are written with.

Answers 1/x - 1/sin(x) and 1/sin(x) - 1/x at 0, csc(x) - cotan(x) at 0, and
1/ln(x) - 1/(x - 1) at 1 either way round. Five of the eleven new tests fail
without it. Corpus unchanged at 101/117 with 0 wrong; suite 4020 passed,
0 failed.
@Rafael-SOWNet

Copy link
Copy Markdown
Member Author

Fork CI, matrix run 30934551606 — success on windows-latest, ubuntu-latest and macos-latest, Failed: 0, Passed: 4020, Skipped: 14, Total: 4034 on each.

The branch ci-check/difference-of-fractions is this PR's head b4fc33e0 plus one commit that only adds a trigger to the workflow file, so Sources/ is byte-identical to what is under review.

One note on the interaction with #697: this rewrite changes how the expression is written, not what is being asked of it, so it is as true one side at a time as it is of both. Whether a one-sided limit reaches l'Hopital's rule at all is #697's business, though, so on this branch alone limitright(1/x - 1/sin(x), x, 0) is still NaN and only the two-sided form answers. With both branches merged together it answers 0 from either side, and 1/ln(x) - 1/(x - 1) at 1 answers 1/2 from either side. I have kept that assertion out of both PRs, since neither is enough on its own — whichever of the two lands second is the place for it, and I will follow up with it then.

git merge-tree reports no conflict between this branch and any of my other open ones.

@codecov-commenter

codecov-commenter commented Aug 4, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 93.54839% with 2 lines in your changes missing coverage. Please review.
✅ Project coverage is 81.60%. Comparing base (90c00a8) to head (c949026).
⚠️ Report is 73 commits behind head on master.

Files with missing lines Patch % Lines
...ath/Functions/Continuous/Limits/Transformations.cs 93.33% 1 Missing and 1 partial ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##           master     #699      +/-   ##
==========================================
+ Coverage   80.99%   81.60%   +0.60%     
==========================================
  Files         155      157       +2     
  Lines       13687    13370     -317     
  Branches     1957     2207     +250     
==========================================
- Hits        11086    10910     -176     
+ Misses       1990     1853     -137     
+ Partials      611      607       -4     

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

tan(x) is opaque to everything that reads quotients: it is neither a quotient
nor a negative power, so the split puts it into the numerator whole and
1/x - 1/tan(x) comes out over x * tan(x). That is the right answer written in
the wrong form. The rules do reach 0 through it, but only after rewriting the
whole expression dozens of ways, and lim x->0+ (1/sin(x) - 1/tan(x)) took forty
seconds against under one for the same limit over sin(x) * sin(x).

Not with the other trigonometric rewrite, which runs in front of the first
remarkable limit. That one matches a quotient, and rewriting tan(b*x - x) as a
quotient of its own turns the quotient it sits under into a product:
lim x->0 sin(x - a*x) / tan(b*x - x), which the suite pins at (a-1)/(1-b),
stops being read at all. Measured, not reasoned about -- it is what the test
caught when the rewrite was put there first.

Suite 4020 passed, 0 failed, unchanged.
@Rafael-SOWNet

Copy link
Copy Markdown
Member Author

Follow-up commit 0107c8a0, and a cost finding worth stating plainly.

The finding. Each of #697, #699 and #700 is fast on its own. Combined, three limits of this family became very slow — lim x->0+ (1/sin(x) - 1/tan(x)) took forty seconds where master answered NaN in twenty-four milliseconds. It terminates and the answer is right where master's was wrong, but forty seconds is not an acceptable trade.

The cause, measured rather than guessed. I instrumented all three mechanisms before changing anything, and my first guess was wrong: #700's derivative-taking accounts for 4 ms of the thirty seconds, and Gruntz for 5. What costs is that Simplify() asks the limit some sixty times, of sixty rewritten forms, and each ask went from about two milliseconds to about half a second. The reason is this PR's own rewrite: tan(x) is neither a quotient nor a negative power, so the split takes it into the numerator whole and 1/x - 1/tan(x) comes out over x * tan(x). That is the right answer in the wrong form — the rules do reach 0 through it, but only the long way round. Over x * sin(x), with sin(x) - x * cos(x) above it, the same limit is answered immediately.

So the follow-up writes a tangent and a cotangent as the quotients they are, once, in the limit pipeline.

Where it goes matters, and a test caught me putting it in the wrong place. My first attempt added it to TrivialTrigonometricReplacement, next to the existing sec and csc rewrites. That runs in front of the first remarkable limit, which matches a quotient — and rewriting tan(b*x - x) as a quotient of its own turns the quotient it sits under into a product, so lim x->0 sin(x - a*x) / tan(b*x - x) stopped being read at all. TestEquivalenceTableTo0 failed, which is exactly what it is there for. It now runs after the first remarkable limit instead.

before after
lim x->0+ (1/sin(x) - 1/tan(x)) 43 s 0.84 s
lim x->0+ (1/x - 1/tan(x)) 30 s 26 s
lim x->0+ (1/x^2 - 1/sin(x)^2) 31 s 31 s

What I have not fixed. The last two are still slow with all three branches applied. Both terminate; 1/x - 1/tan(x) gives the right answer, 1/x^2 - 1/sin(x)^2 gives NaN as master does. Neither is in the 117-problem corpus, which stays at 111/117 with 0 wrong / 0 error / 0 timeout, and neither is slow on any single branch. I tried a work budget across one limit computation and it did not bind, because each of the sixty asks stays well inside any budget that keeps the answers — the fan-out is across asks, not within one. Bounding it properly means memoising limits across one Simplify(), which is a larger change than any of these fixes and not one I would smuggle into this PR. Recorded rather than hidden.

Fork CI on the new head, matrix run 30942267526 — success on windows-latest, ubuntu-latest and macos-latest, Failed: 0, Passed: 4020, Skipped: 14, Total: 4034 on each. ci-check/difference-of-fractions is this PR's head 0107c8a0 plus the workflow-trigger commit; Sources/ is byte-identical.

One correction to my earlier comment: this branch now touches Solvers.Definition.cs as well, and so conflicts textually with #697, which hoists the same block of pre-passes. The conflict is two adjacent lines and the resolution is to keep both; whichever lands second is where I will do it.

The tangent rewrite moves into the block of pre-passes #697 hoisted above the
side dispatch, which is where it belonged once that landed. The per-term copy of
the same rewrite inside AsQuotient goes: the pass in front of the descent
already reaches every term, so keeping both said the same thing twice.
@Rafael-SOWNet
Rafael-SOWNet merged commit 1c5fb82 into ASC-Community:master Aug 4, 2026
26 checks passed
@Rafael-SOWNet
Rafael-SOWNet deleted the fix/difference-of-fractions 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.

2 participants