Repository navigation
Apply the first remarkable limit down the product spine (#749) - #759
Merged
Merged
Conversation
The rule that rewrites a vanishing sin(u), tan(u), arcsin(u) or arctan(u) as the u it is equivalent to matched a product or quotient whose own child is one of those functions, and it was applied at the root of the expression alone. A constant factor written on one side pushes that function a level down and out of reach -- 2 * sin(1/x) * x parses as (2 * sin(1/x)) * x, whose children are a product and a variable -- so the descent read it as 0 * (+oo) and was definite about it. The same product answered 2 written one way round and NaN the other, and which side a caller writes a constant on is not a mathematical difference: simplification produces either. Applied down the product-and-quotient spine now, and deliberately not as a plain Replace. The equivalence licenses replacing a factor of the expression as a whole -- if f/g -> 1 then f*h and g*h go to the same place -- and says nothing about a term of a sum, where the difference between f and g is the entire answer. Stopping at anything that is not a product or a quotient is what holds that line: lim x->0 (sin(x)/x - 1)/x^2 stays -1/6, where a rule that descended into sums would answer 0. The cost, which is a value becoming NaN and not a wrong answer: a vanishing factor against a logarithm was being shielded from a rewrite the library already applies to the same limit spelled without the constant, so lim x->0 sin(x) * ln(x) * 2 was 0 while sin(x) * ln(x) was NaN. Both are NaN now. The rewrite is sound -- the two have the same limit -- and what it exposes is that lim x->0 x * ln(x) is NaN by design, ln(x) not being real to the left of 0, the same judgement that pins x^x in LimitTest.TestNoLimit. Under a real codomain both come back unevaluated instead. Recorded in BREAKING-CHANGES.md. Measured over 811 generated products and quotients: 196 NaN -> value, 13 value -> that one NaN, 0 answers changed into different answers. Unit tests 5169 -> 5201 passing with 32 added, F# 130, casbench 112/117 with 0 wrong, propcheck 0 failures, rootcheck 596/596, simpsweep 0 disagreements. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #749 (its remaining half — the first half was withdrawn in a comment there, being #719, which has since landed).
What was wrong
ApplyFirstRemarkablerewrites a vanishingsin(u),tan(u),arcsin(u)orarctan(u)as theuit is equivalent to, and it matches a product or quotient whose own child is one of those functions. It was called on the whole expression, where its sibling one line below is applied at every node.So a constant factor written to the left pushes the sine one level down and out of reach:
2 * sin(1/x) * xparses as(2 * sin(1/x)) * x, whose children are a product and a variable, and neither is a sine. The descent then reads it as0 * (+oo)and is definite about it:Which side of a product a caller writes a constant on is not a mathematical difference, and simplification produces either.
The fix, and why it is not a
ReplaceThe rule is applied down the product-and-quotient spine, stopping at anything that is not a product or a quotient.
That stop is the whole of what keeps it sound. The equivalence licenses replacing a factor of the expression as a whole — if
f/g -> 1thenf*handg*hgo to the same place — and says nothing about a term of a sum, where the difference betweenfandgis the entire answer. Rewriting the sine inside(sin(x)/x - 1)/x^2would answer0where the limit is-1/6. Those cases are pinned inASumIsNotRewrittenandAConstantFactorOnASumDoesNotOpenItEither:What it costs — a value becoming
NaNStated plainly, because it is not purely an improvement:
A vanishing factor against a logarithm was being shielded by its constant from a rewrite the library already applies to the same limit written without the constant. The rewrite is sound (
sin(x) * ln(x)andx * ln(x)have the same limit); what it exposes is thatlim x->0 x * ln(x)isNaNby design —ln(x)is not real to the left of0, the same judgement that pinslim x->0 x^xinLimitTest.TestNoLimit. So master answered two spellings of one limit differently, and they now agree. UnderMathS.Settings.Codomain.Set(Domain.Real)both come back unevaluated, which is the honest answer where there is no real left-hand neighbourhood.Recorded in
BREAKING-CHANGES.mdrather than filed under "fixed".Measured
Generated 811 products and quotients of a vanishing
sin/tan/arcsin/arctanagainstx,1/x,ln(x),x^2,e^xwith constants in every position, at0and+oo, and diffed this branch againstmaster:NaN→ a valueNaNtrig(u) * ln(x)shape above, each with a constant-free twin alreadyNaNon masterSuites and harnesses, all from a clean build of this branch:
casbench112/117 solved, 0 wrong, 0 error, 0 timeout — unchangedpropcheck0 failures of 1337 checksrootcheck596/596 cleansimpsweep0 disagreements of 10463