Read arccotan's range off arccotan rather than off a textbook (#887) - #888
Merged
Conversation
This library's arccotan is arctan(1/x) extended by arccotan(0) = pi/2, so its range is (-pi/2, pi/2] and not the (0, pi) many texts use: arccotan(-1) is -pi/4. Two rewrites assumed otherwise. arctan(x) + arccotan(x) was pi/2 unconditionally, which is a wrong answer at every negative real -- the sum is -pi/2 there. pi/2 * sgn(x) is the closed form and is wrong at exactly one point, x = 0, where the sum is pi/2 and sgn is 0, so the sign is decided where it can be read and the sum left alone otherwise. A Piecewise would be total, but Compile throws on one, so answering that way would stop expressions compiling that compile today. The neighbouring arcsin(x) + arccos(x) is unconditional and correct, because arccos(x) is pi/2 - arcsin(x) by definition over the whole plane; the block had a sound half and an unsound half again. arccotan(cotan(x)) was guarded with [0, pi] in #885, by me, on the same wrong assumption about the range. That admitted (pi/2, pi), where the rewrite is false -- arccotan(cotan(2)) simplified to 2 and is 2 - pi -- and refused (-pi/2, 0), where it is true. The interval is now (-pi/2, pi/2] without zero, zero excluded because cotan has no value there, so the composition has none either and rewriting to x would invent one. The other three intervals from #885 check out against the same measurements. Three tests moved, each pinning the old wrong answer for a symbolic argument, and each now covering both signs with numbers. SortSimplifyTest's case sorted its whole sum only because the collapse shortened it; with the collapse gone the sum stays, and the sibling arcsin case still collapses and still sorts. Found by work/boundcheck on its first run, which is the point of it: the shapes come from the nodes by reflection and the points are chosen where an assumption fails, so it found an error in my own merged fix within minutes of existing. Measured on this branch: 6296 C# tests pass, 130 F# tests pass, casbench 117/119 with 0 wrong, propcheck 1340 checks with 0 failures, rootcheck 596/596, simpsweep 10463/10463 agreeing, boundcheck 5 disagreements down to 4 -- the rest being #884's logarithm half and two findings filed on #887.
Rafael-SOWNet
added a commit
that referenced
this pull request
Aug 11, 2026
Both changelog sections were inserted at the same anchor, so both are kept: the arccotan correction from #888 continues the inverse-trigonometric entry above it, and the Compile entry follows it.
Rafael-SOWNet
added a commit
that referenced
this pull request
Aug 11, 2026
Two conflicts, both additive on each side, so both sides are kept. BREAKING-CHANGES.md: #888's arccotan row and section went in at the same anchors as the logarithm ones. The arccotan entries come first, continuing the inverse-trigonometric entry above them. SimplificationRegressionTest.cs: #888's arccotan cases and this branch's logarithm cases were appended at the same point.
Member
|
Should we fix the range to be |
Rafael-SOWNet
added a commit
that referenced
this pull request
Aug 12, 2026
* Read abs and signum at zero, where both compositions are 0 (#892) |sgn(z)| and sgn(|z|) are 1 for every z except 0, where both are 0 -- sgn(0) is 0, which the comment directly above one of these rules already said. Both answered 1 for any argument, so a symbolic one gave a value that is wrong at one point: abs(sgn(x)) and sgn(abs(x)) simplified to 1. The rules read the argument's DomainCondition, which is a different question from the one they needed. A bare x is defined everywhere and can itself be zero; x/x is defined only away from zero and is nonzero throughout. Reading the domain conflates the two. They now decide where the argument's value can be read and leave the node alone otherwise. A condition would have been the wrong fix, as in #884 and #887: the expression is defined at zero and equal to 0 there, so `1 provided not z = 0` would trade a wrong value for a wrong domain, and there is no closed form for "1 away from zero, 0 at it" other than the expressions themselves. The four x/x cases go with them, and that is a coverage loss rather than a correction: 1 provided not x = 0 was right for those. Telling them apart needs "nonzero throughout its domain", and the two tests available here cannot -- Evaled leaves x/x as x/x, and DomainCondition is what conflated them. Simplify would answer it and must not be called from inside InnerSimplify (#403). Written down in the test file rather than left as a silent regression. This is the third rule block in two days found to have a sound half and an unsound half written as though they were symmetric, after #884 and #887. Measured: 6333 C# tests pass, 130 F# tests pass, casbench 117/119 with 0 wrong, propcheck 0 failures, rootcheck 596/596, simpsweep 10463/10463 agreeing, and boundcheck 7 disagreements down to 4 -- the two this fixes, plus the two the merged #888 and #893 fixed. * Point six .gitignore entries at where the files actually are The previous commit swept in a 1281-line BenchmarkDotNet log, because /Tests/DotnetBenchmark/benchmark_results.csv is anchored at the repository root and the directory is Sources/Tests/DotnetBenchmark/benchmark_results.csv. The entry has been inert since f0db3ee moved everything under Sources/, so the benchmark's output has been showing up in git status -- and in git add -A -- ever since. Five neighbours had the same problem and are corrected with it. The Sources/Samples entries a few lines below were already updated, which is why the file looked fine at a glance. Third path in this repository found pointing at the pre-f0db3eef layout, after amsite.fsx's publish path and NaiveStaticGenerator's read of AngouriMath.xml. Worth grepping for others. * Measure what 2.0.0 shipped without, and correct the ignore fix 2.0.0 has no column in this file: it is commit 1691 and the table stopped at the 1671st. The Math OS plan (#746) lists speed on popular use cases as a standing condition on every tier rather than a roadmap item, and the release met the correctness half of that exhaustively and the performance half not at all. Filling the gap with one new column would have been worse than leaving it. Measured here, every one of the sixteen rows came out at 0.5-0.6x of the 1671st -- a uniform factor, which is a faster machine and not faster code, and a reader comparing the columns would have seen a 1.75x improvement that does not exist. That is the mistake this file's own header warns about, so the 1671st was re-measured on the same machine minutes later and both are recorded as a pair that may be read as a ratio. The answer: no regression. Most rows move 1-5%, which by this file's standard says nothing. SimplifyEasy is +8.7%, which is above it, and the cause is likely ours rather than mysterious -- the corrections merged since the 1671st added guards that call Evaled inside a pattern's when clause on a path that used to match structurally. Recorded as a row to watch, with a note not to write more guards that way without measuring. Also corrects the previous commit here. Of the six .gitignore entries it repointed at Sources/, four name directories that no longer exist anywhere in the tree, so it made dead configuration look current. Those are deleted along with three more of the same kind; what remains are the two that match something real -- the benchmark's output directory, which is what started this, and the local ANTLR download.
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.
Fixes two wrong answers, one of which is a correction to 2.0.0 — the guard I added in #885 used
the wrong interval.
Closes #887's first two sections.
One fact, two consequences
This library's
arccotanisarctan(1/x)extended byarccotan(0) = pi/2, so its range is(-pi/2, pi/2]and not the(0, pi)that many textbooks use. Measured:arccotan(1)ispi/4,arccotan(-1)is-pi/4,arccotan(0)ispi/2.arctan(-3) + arccotan(-3)pi/2-1/2 * pi— the valuearctan(3) + arccotan(3)pi/2arctan(x) + arccotan(x), symbolicpi/2arccotan(cotan(2))2-1.1416...— the value,2 - piarccotan(cotan(-1/2))-1/2The sum.
pi/2for a non-negative real argument,-pi/2for a negative one.pi/2 * sgn(x)isthe closed form and is wrong at exactly one point — at
x = 0the sum ispi/2whilesgn(0)is0— so the sign is decided where it can be read and the sum is left alone otherwise. APiecewisewould be total, and is not used because
CompilethrowsUncompilableNodeExceptionon one, soanswering that way would stop expressions compiling that compile today.
The neighbouring
arcsin(x) + arccos(x) -> pi/2is untouched and needs no assumption, sincearccos(x)ispi/2 - arcsin(x)by definition over the whole plane. The block had a sound halfand an unsound half written as though they were symmetric — the same shape as #884, four lines
apart, which is worth noting as a thing to look for.
The composition. #885 guarded
arccotan(cotan(x))with[0, pi], which admitted(pi/2, pi)where the rewrite is false and refused
(-pi/2, 0)where it is true. Now(-pi/2, pi/2]withoutzero — zero excluded because
cotanhas no value there, so the composition has none either andrewriting to
xwould invent one. The other three intervals from #885 check out against the samemeasurements:
arcsin[-pi/2, pi/2],arccos[0, pi](arccos(-1)ispi),arctan(-pi/2, pi/2).How it was found, which is the part worth reading
By
work/boundcheck, written for exactly this class after #884 showed that four rules had been wrongsince 2020 with every harness green. It composes every unary function node with every other — found
by reflection, so a node added later is covered without anyone remembering — and compares each
simplification against the original at points chosen to sit where an assumption fails, rather than
at sampled points. Both undefined counts as agreement.
It found an error in my own merged fix within minutes of existing. That is the argument for building
it:
simpsweepsamples real points and cannot see a rule that is wrong off the real line, and itsgrammar never nests a function inside its own inverse, so this shape was outside its space entirely.
First run: 366 shapes, 936 comparisons, 5 disagreements. Now 4, and every one of those is filed —
two are #884's logarithm half,
log(x, x) -> 1 provided x > 0has a condition too strong (atx = -3the value is1and the simplification is undefined) and is recorded on #887, andln(x) + ln(x+1)is the known domain-widening gathering.Tests that moved
Three asserted
pi/2for a symbolic argument, so all three were pinning the wrong answer. Eachnow uses numbers and covers both signs.
SortSimplifyTest'sarctan(x2) + arccot(x*x)case is the interesting one: it sorted and collectedits whole nine-term sum only because the collapse to
pi/2shortened it and made the sortedcandidate win on complexity. With the collapse gone the sum stays as written.
x^2is not a knownnon-negative for a symbolic
x— atx = iit is-1— so declining is correct, and the siblingarcsin/arccoscase still collapses and still sorts. The comment says so, so nobody reads it assorting having broken.
New regression cases assert the value and take the argument as a number before
Simplifyruns, because that is what the guard reads: my first attempt simplified the symbolic form and
substituted afterwards, which passes whatever the interval says.
Measured on this branch, .NET 10
casbenchpropcheckrootchecksimpsweepboundcheckOne thing found while measuring, not fixed here
dotnet test -c release(lowercase) cannot buildAngouriMath.FSharpfrom clean on acase-sensitive filesystem. The fsproj hardcodes
<DocumentationFile>bin\Release\netstandard2.0\AngouriMath.FSharp.XML</DocumentationFile>under acondition that MSBuild compares case-insensitively, so with
-c releasethe condition matches while$(OutputPath)becomesbin/release/— and the compiler fails withFS0193: Could not find a part of the path ... bin/Release/....-c Releaseworks, and so does-c releaseonce a capitalised build has created the directory, which is why it has not beennoticed. Filed separately; the fix is
<GenerateDocumentationFile>true</GenerateDocumentationFile>instead of a hardcoded path.