Repository navigation
Compile arcsine to the arcsine, not to its conjugate - #696
Conversation
The FE compiler conjugated arcsine unconditionally:
stack.Push(Complex.Conjugate(Complex.Asin(stack.Pop())));
That is right on the two branch cuts -- the real arguments outside [-1, 1],
where the library does take the lower side and where the only test of it
looked -- and wrong everywhere else. Compiled arcsine was the conjugate of
the arcsine over the whole rest of the plane, so sin(arcsin(0.5 + 0.1i))
came back as 0.5 - 0.1i, and arcsin(z) + arccos(z) was not pi/2 off the
real axis.
Measured against Evaled over a grid of 63 points spanning both cuts, both
sides of each and the interval between them, the FE compiler agreed at 15.
The Linq compiler, which does not conjugate, agreed at 59: it misses the
four points on the cut, taking the other side of it from the rest of the
library. Neither was right; they were wrong in complementary places.
Both now go through one helper that conjugates on the cut and nowhere else,
which agrees with Evaled at every one of the 63 -- and at 62 of 62 for
arccosecant, which is arcsine of the reciprocal and so lands on the same
cuts. Nothing changes on the real axis, where every existing test looks.
Newton's method was the visible casualty. Started at a root of
arcsin(x) - x*pi/3 it walked away from it, because the derivative the
conjugated arcsine implies has the wrong sign in the imaginary direction,
which turns the root into a repeller: the imaginary part grew by a factor
of about 21 per step until the iteration left for elsewhere. -1/2 is now
found. This is part of #115
rather than the whole of it -- at the default step count the grid steps
over both of the outlying roots regardless.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## master #696 +/- ##
==========================================
- Coverage 80.99% 80.45% -0.54%
==========================================
Files 155 157 +2
Lines 13687 12921 -766
Branches 1957 2123 +166
==========================================
- Hits 11086 10396 -690
+ Misses 1990 1917 -73
+ Partials 611 608 -3 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
|
CI, run on my fork across the full matrix, since the run here is held at https://github.com/Rafael-SOWNet/AngouriMath/actions/runs/30921443094
That matters more than usual here, because the branch-cut convention this PR preserves depends on how The branch it ran on, |
The defect
The FE compiler conjugated arcsine unconditionally (
Functions/Compilation/IntoFE/FastExpression.cs, since October 2020):That is correct on the two branch cuts — the real arguments outside
[-1, 1], where the library does take the lower side, and where the only test of it looks (CompilationFETestevaluates everything atx = 3). It is wrong everywhere else. Compiled arcsine was the conjugate of the arcsine over the whole rest of the plane:sin(arcsin(0.5 + 0.1i))arcsin(z) + arccos(z)at0.5 + 0.1i0.5 − 0.1iπ/2 − 0.2299i0.5 + 0.1iπ/2Measuring it rather than arguing about branches
Against
Evaled— the library's own answer, and the thing a compiled expression is supposed to reproduce — over a grid of 63 points spanning both cuts, both sides of each (im = ±1e-9), and the interval between them:arcsinarcsinarccscNeither compiler was right. The FE one conjugated everywhere; the Linq one conjugated nowhere, so it missed exactly the four points on the cut, where it takes the opposite side from the rest of the library. They were wrong in complementary places, and had been for five years.
arccos,arctan,arcsecandarccotalready agreed at every point and are untouched.The fix
One helper,
Core/Compilation/ComplexBranches.cs, that conjugates on the cut and nowhere else, used by both compilers and byarccsc(which is arcsine of the reciprocal, so it lands on the same cuts — for it, at the real arguments strictly inside[-1, 1]).MathAllMethods.csis generated, soMathAllMethods.ttcarries the same change and the two are consistent.The library's side of the cut is deliberately kept.
arcsin(3)staysπ/2 − 1.7627ihere, whereSystem.Numerics.Complex.Asingivesπ/2 + 1.7627i. Which side of a cut to take is a convention, not a bug, and this PR does not touch it — pinned byTheLibrarysSideOfTheCutIsUnchanged. That is why nothing on the real axis changes and no existing test needed editing.Why it mattered beyond compilation
Newton's method was the visible casualty. Started at a root of
arcsin(x) - x*pi/3, it walked away from it: the derivative a conjugated arcsine implies has the wrong sign in the imaginary direction, which turns the root into a repeller. Traced, the imaginary part grew by a factor of about 21 per step from4.5e-31until the iteration left for a non-root near−0.0014 + 3.7998i.−1/2is now found.This is part of #115, not the whole of it. At the default
StepCountof(10, 10)the grid has spacing 2 over[-10, 10], so it steps over both outlying roots regardless of this fix, andSolvestill answers{0}. Raising the step count reaches them — which needs #695 to work at all.Tests
Sources/Tests/UnitTests/Common/CompiledArcsinBranchTest.cs, 138 cases. 61 of them fail without the source change (verified by reverting it in place and re-running).Evaledacross the grid, forarcsinandarccscsin(arcsin(z)) == z— the defining property, which the old code did not havearcsin(z) + arccos(z) == π/2off the real axis, which the existingCompilationFETestcase for that pair never checkedFull suite:
Failed: 0, Passed: 4147, Skipped: 14, Total: 4161.Noted, not fixed
arccot(0)compiles toNaN(Atan(1/0)) whereEvaledgivesπ/2. Same family, different cause, and out of scope here.🤖 Generated with Claude Code