Skip to content

Read arccotan's range off arccotan rather than off a textbook (#887) - #888

Merged
Rafael-SOWNet merged 1 commit into
masterfrom
fix/arccotan-range
Aug 11, 2026
Merged

Read arccotan's range off arccotan rather than off a textbook (#887)#888
Rafael-SOWNet merged 1 commit into
masterfrom
fix/arccotan-range

Conversation

@Rafael-SOWNet

Copy link
Copy Markdown
Collaborator

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 arccotan is arctan(1/x) extended by arccotan(0) = pi/2, so its range is
(-pi/2, pi/2]
and not the (0, pi) that many textbooks use. Measured: arccotan(1) is pi/4,
arccotan(-1) is -pi/4, arccotan(0) is pi/2.

was is
arctan(-3) + arccotan(-3) pi/2 -1/2 * pi — the value
arctan(3) + arccotan(3) pi/2 unchanged
arctan(x) + arccotan(x), symbolic pi/2 left as written
arccotan(cotan(2)) 2 -1.1416... — the value, 2 - pi
arccotan(cotan(-1/2)) left as written -1/2

The sum. pi/2 for a non-negative real argument, -pi/2 for a negative one. pi/2 * sgn(x) is
the closed form and is wrong at exactly one point — at x = 0 the sum is pi/2 while sgn(0) is
0 — so the sign is decided where it can be read and the sum is left alone otherwise. A Piecewise
would be total, and is not used because Compile throws UncompilableNodeException on one, so
answering that way would stop expressions compiling that compile today.

The neighbouring arcsin(x) + arccos(x) -> pi/2 is untouched and needs no assumption, since
arccos(x) is pi/2 - arcsin(x) by definition over the whole plane. The block had a sound half
and 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] 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: arcsin [-pi/2, pi/2], arccos [0, pi] (arccos(-1) is pi), 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 wrong
since 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: simpsweep samples real points and cannot see a rule that is wrong off the real line, and its
grammar 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 > 0 has a condition too strong (at
x = -3 the value is 1 and the simplification is undefined) and is recorded on #887, and
ln(x) + ln(x+1) is the known domain-widening gathering.

Tests that moved

Three asserted pi/2 for a symbolic argument, so all three were pinning the wrong answer. Each
now uses numbers and covers both signs.

SortSimplifyTest's arctan(x2) + arccot(x*x) case is the interesting one: it sorted and collected
its whole nine-term sum only because the collapse to pi/2 shortened it and made the sorted
candidate win on complexity. With the collapse gone the sum stays as written. x^2 is not a known
non-negative for a symbolic x — at x = i it is -1 — so declining is correct, and the sibling
arcsin/arccos case still collapses and still sorts. The comment says so, so nobody reads it as
sorting having broken.

New regression cases assert the value and take the argument as a number before Simplify
runs, 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

C# tests 6296 passed, 0 failed, 14 skipped
F# tests 130 passed, 0 failed
casbench 117/119, 0 wrong, 0 error, 0 timeout
propcheck 1340 checks, 0 failures
rootcheck 596/596 clean
simpsweep 10463/10463 agree, 0 disagree
boundcheck 5 disagreements → 4, all four filed

One thing found while measuring, not fixed here

dotnet test -c release (lowercase) cannot build AngouriMath.FSharp from clean on a
case-sensitive filesystem.
The fsproj hardcodes
<DocumentationFile>bin\Release\netstandard2.0\AngouriMath.FSharp.XML</DocumentationFile> under a
condition that MSBuild compares case-insensitively, so with -c release the condition matches while
$(OutputPath) becomes bin/release/ — and the compiler fails with
FS0193: Could not find a part of the path ... bin/Release/.... -c Release works, and so does
-c release once a capitalised build has created the directory, which is why it has not been
noticed. Filed separately; the fix is <GenerateDocumentationFile>true</GenerateDocumentationFile>
instead of a hardcoded path.

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
Rafael-SOWNet merged commit 7e1a8ea into master Aug 11, 2026
25 checks passed
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.
@Happypig375

Copy link
Copy Markdown
Member

Should we fix the range to be (0, pi) instead?

Rafael-SOWNet added a commit that referenced this pull request Aug 11, 2026
#893's logarithm section went into BREAKING-CHANGES.md at the same anchor this branch
uses, so both are kept -- as with #888 before it. Three of the entries in this file now
sit next to each other because three changes to the same table landed in the same
afternoon, which is a good problem to have.
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.
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.

arccotan's range is (-pi/2, pi/2], and two rewrites assume (0, pi)

2 participants