Skip to content

Reduce radicals and read the trigonometric tables backwards (#281, #569, #179) - #716

Merged
Rafael-SOWNet merged 8 commits into
masterfrom
feat/surds-and-inverse-trig
Aug 5, 2026
Merged

Rafael-SOWNet merged 8 commits into
masterfrom
feat/surds-and-inverse-trig

Conversation

@Rafael-SOWNet

Copy link
Copy Markdown
Member

Closes #281, #569, #179. Takes part of #205 and part of #397; both stay open for the rest, which is named below.

Five commits, each independent of the others.

Perfect powers out from under a radical (#281, part of #205)

sqrt(12) was left as written. The reduced form was generated as a Simplify candidate but lost to the shorter one on node count, so it only appeared where two like radicals had to be collected — sqrt(12) + sqrt(27) came to 5*sqrt(3) while sqrt(12) on its own did not move. The metric is the wrong judge here: a reduced radicand is what makes two surds comparable without simplifying their difference, and it is what sympy, Mathematica and Maxima all print. So the reduction happens where the power is built.

Trial division, with two bounds chosen so the extraction is complete rather than best-effort. Once every factor below 10^4 is out, what is left is a prime, a product of two primes above the bound, or the square of one — three primes above 10^4 would already exceed the 10^12 radicand bound — and only the last has anything to give up, which the perfect-power check takes. So nothing within the bound is missed, and the ordinary case costs a handful of divisions. The size is checked from the bit length before the radicand is raised to the exponent's numerator, so a large rational is refused rather than built and discarded.

A negative exponent is the same question asked of the reciprocal, so it is turned round rather than declined, and a radical below the line is answered where the division is built. Without that, 1/sqrt(2) and sqrt(1/2) printed differently — one number, two forms, only because one of them was refused.

sqrt(12)           ->  2 * sqrt(3)        1 / sqrt(2)   ->  sqrt(2) / 2
sqrt(1/2)          ->  sqrt(2) / 2        1 / sqrt(12)  ->  sqrt(3) / 6
(2^3 * 5^7)^(1/3)  ->  50 * 5^(1/3)       1 / cbrt(2)   ->  4^(1/3) / 2
12^(5/2)           ->  288 * sqrt(3)      8 ^ (-1/3)    ->  1/2

A negative radicand is declined. The principal cube root of -8 is 1 + i*sqrt(3), not -2; which branch a negative base takes is settled elsewhere in the library, and reducing the radicand under an even root would be making that same choice by accident.

Not closed in #205: a surd against a sum, (5 - sqrt(3)) / (5 + sqrt(3)), which needs the conjugate.

Two existing tests moved, both because the answer got better. Issue205_StandaloneSurdsKeepTheShorterForm pinned the behaviour this replaces. Issue662_ExactPathStaysSymbolic expected sqrt(369), and 369 is 9 * 41.

The trigonometric tables read backwards (#569, #179)

sin(pi/4) was sqrt(2)/2 but arcsin(sqrt(2)/2) stayed as written — the tables were only read forwards. arctan was the exception, and only at +-1, which is why arctan(1) already answered pi/4.

The inverse table is written out rather than derived from the forward one at run time, because inverting that one needs the principal branch and it holds every angle around the circle: sin(2pi/3) and sin(2pi/6) are the same number and only the second is an answer arcsin may give. arccos and arccotan take the complement, which is the reading the existing arctan(x) + arccotan(x) rule already takes.

A double comparison picks the candidate and the exact value confirms it, so arcsin(0.4999999) comes back unanswered rather than as pi/6.

arcsin(1/2)          ->  pi / 6      arctan(sqrt(3))                ->  pi / 3
arcsin(sqrt(2) / 2)  ->  pi / 4      arctan(2 - sqrt(3))            ->  pi / 12
arccos(1/2)          ->  pi / 3      arccotan(1)                    ->  pi / 4
arccos(-1/2)         ->  2 * pi / 3  arcsin(sqrt(2 - sqrt(2)) / 2)  ->  pi / 8

Characteristic polynomial (#381)

det(x*I - A). The determinant was already there, so this is the shift and the sign convention.

Monic — det(x*I - A) rather than det(A - x*I). The two are the same polynomial up to a sign for an odd-sized matrix, and the systems disagree: Mathematica prints det(A - x*I), sympy's charpoly is monic. Monic is what most linear algebra texts state and it composes with the polynomial solver without a stray -1 in front, so it is the one documented and tested.

[[1, 2], [3, 4]]  ->  x^2 - 5x - 2        [[a, b], [c, d]]  ->  x^2 - (a + d)x + ad - bc

Coverage from the F# and Interactive suites (part of #397)

Only the C# suite reported to codecov. Both wrappers now collect on the ubuntu leg of their matrix and upload with a flag, keeping the three distinguishable rather than merging them; the other two operating systems keep running the tests without paying for the collector or racing each other's upload — the shape the C# workflow already used.

C++ is deliberately not included and the issue stays open for it: AngouriMath.CPP is a native wrapper over the AOT-exported library, so its coverage would come from gcov or llvm-cov and would measure the wrapper rather than the library. That is a separate decision about whether a coverage build belongs in CI at all.

AGENTS.md

One paragraph next to the existing rule about reproducing before claiming something is broken: the tracker is older than the code. Twelve open issues turned out to be already fixed when re-measured, and re-measuring cost minutes where planning fixes for them would have cost days.

…nator

sqrt(12) was left as written. The reduced form was generated as a Simplify
candidate but lost to the shorter one on node count, so it only ever appeared
where two like radicals had to be collected -- sqrt(12) + sqrt(27) came to
5*sqrt(3), while sqrt(12) on its own did not move. That is the wrong judge for
this: a reduced radicand is what makes two surds comparable without simplifying
their difference, and it is what sympy, Mathematica and Maxima all print. So the
reduction now happens where the power is built.

Trial division, with two bounds chosen so the extraction is complete rather than
best-effort. Once every factor below 10^4 is out, what is left is a prime, a
product of two primes above the bound, or the square of one -- three primes above
10^4 would already exceed the 10^12 radicand bound -- and only the last of those
has anything to give up, which the perfect-power check at the end takes. So
nothing within the bound is missed, and the ordinary case costs a handful of
divisions.

A negative exponent is the same question asked of the reciprocal, so it is turned
round rather than declined, and a radical below the line is answered the same way
where the division is built. Without that, 1/sqrt(2) and sqrt(1/2) printed
differently -- one number, two forms, only because one of them was refused.

A negative radicand is declined. The principal cube root of -8 is 1 + i*sqrt(3),
not -2; which branch a negative base takes is settled elsewhere in the library,
and reducing the radicand under an even root would be making that same choice by
accident.

  sqrt(12)           ->  2 * sqrt(3)
  sqrt(1/2)          ->  sqrt(2) / 2
  1 / sqrt(2)        ->  sqrt(2) / 2
  1 / sqrt(12)       ->  sqrt(3) / 6
  1 / cbrt(2)        ->  4^(1/3) / 2
  (2^3 * 5^7)^(1/3)  ->  50 * 5^(1/3)
  12^(5/2)           ->  288 * sqrt(3)

A surd against a *sum* -- (5 - sqrt(3)) / (5 + sqrt(3)) -- still needs the
conjugate, and is the part of 205 this does not close.

Two existing tests moved with it, both because the answer got better:
Issue205_StandaloneSurdsKeepTheShorterForm pinned the behaviour this replaces,
and Issue662_ExactPathStaysSymbolic expected sqrt(369), where 369 is 9 * 41.

#281
#205
sin(pi/4) was sqrt(2)/2 but arcsin(sqrt(2)/2) stayed as written: the tables were
only ever read forwards. arctan was the exception and only at +-1, which is why
arctan(1) already answered pi/4.

The inverse table is written out rather than derived from the forward one at run
time, because inverting that one needs the principal branch and it holds every
angle around the circle: sin(2pi/3) and sin(2pi/6) are the same number and only
the second is an answer arcsin may give. arccos and arccotan take the complement,
which is the same reading of arccotan the existing arctan(x) + arccotan(x) rule
already takes.

A double comparison picks the candidate and the exact value confirms it, so
arcsin(0.4999999) comes back unanswered rather than as pi/6.

  arcsin(1/2)                    ->  pi / 6
  arcsin(sqrt(2) / 2)            ->  pi / 4
  arccos(1/2)                    ->  pi / 3
  arccos(-1/2)                   ->  2 * pi / 3
  arctan(sqrt(3))                ->  pi / 3
  arctan(2 - sqrt(3))            ->  pi / 12
  arccotan(1)                    ->  pi / 4
  arcsin(sqrt(2 - sqrt(2)) / 2)  ->  pi / 8

#569
#179
det(x*I - A), whose roots are the eigenvalues. The determinant was already there,
so this is the shift and the sign convention.

Monic -- det(x*I - A) rather than det(A - x*I). The two are the same polynomial up
to a sign for an odd-sized matrix, and the systems disagree: Mathematica prints
det(A - x*I), sympy's charpoly is monic. Monic is what most linear algebra texts
state and it is the form that composes with the polynomial solver without a stray
-1 in front, so it is the one documented and tested here.

  [[1, 2], [3, 4]]                ->  x^2 - 5x - 2
  [[a, b], [c, d]]                ->  x^2 - (a + d)x + ad - bc
  [[1, 0, 0], [0, 1, 0], [0, 0, 1]] ->  x^3 - 3x^2 + 3x - 1

#381
Only the C# suite reported to codecov, so the wrappers were invisible in it.
Both now collect on the ubuntu leg of their matrix and upload with a flag, which
keeps the three distinguishable rather than merging into one total; the other two
operating systems keep running the tests without paying for the collector or
racing each other's upload, which is the shape the C# workflow already used.

C++ is deliberately not included. AngouriMath.CPP is a native wrapper over the
AOT-exported library, so its coverage would have to come from gcov or llvm-cov and
would measure the wrapper rather than the library. That is a separate decision
about whether a coverage build is worth having in CI at all, so the issue stays
open for it.

#397
Eleven open issues turned out to be already fixed when re-measured on
2026-08-05, and re-measuring cost minutes where planning fixes for them would
have cost days. Worth saying next to the rule about reproducing before claiming
something is broken, since it is the same measurement taken earlier.
…rse-trig

# Conflicts:
#	Sources/Tests/UnitTests/Common/SimplificationRegressionTest.cs
det(x*I - A) came back NaN at particular values of x rather than the number the
polynomial takes there. The determinant is computed by Gaussian elimination,
which divides, so it carries the conditions under which those divisions were
legal -- and on the shifted matrix those pivots are polynomials in x that vanish
at isolated points. Every one of them is a removable singularity of something
defined everywhere, so the conditions are right of the determinant and wrong of
the characteristic polynomial, and clearing them is not a matter of stripping
them off afterwards.

Getting that right means either a Laplace expansion for this case or cancelling
the conditions against the factors that produced them. Neither is a line of
code, and a polynomial that is NaN at some of its own points is worse than not
having one, so it comes out rather than shipping.

#381
@Rafael-SOWNet Rafael-SOWNet changed the title Reduce radicals, read the trigonometric tables backwards, and give a matrix its characteristic polynomial (#281, #569, #381) Reduce radicals and read the trigonometric tables backwards (#281, #569, #179) Aug 5, 2026
@Rafael-SOWNet

Copy link
Copy Markdown
Member Author

Update: the characteristic polynomial (#381) has been taken back out of this branch, so the title no longer claims it.

det(x*I - A) gave the right polynomial and the right roots but came back NaN at particular values of x. Determinant is computed by Gaussian elimination, which divides, so it carries the conditions under which those divisions were legal — and on the shifted matrix those pivots are polynomials in x that vanish at isolated points. Each is a removable singularity of something defined everywhere, so the conditions are right of the determinant and wrong of the characteristic polynomial, and clearing them is not a matter of stripping them off afterwards. Written up on #381.

Worth recording how it surfaced, because it nearly did not. The first version of the test compared the two polynomials symbolically, with Expand().Simplify() on both sides — which never evaluates at the bad points, so it passed. It was also slow enough to take the suite from three minutes to over forty, and rewriting it to substitute points instead brought that class from two minutes to 135 ms and failed. The expensive test was hiding the defect.

What is left here is measured on the current head: 4807 passed, 0 failed (3m18s, against 3m20s for master unchanged), F# wrapper 130/130, corpus 112/117 with 0 wrong, 0 error, 0 timeout.

@Rafael-SOWNet
Rafael-SOWNet merged commit 7a38a8a into master Aug 5, 2026
24 checks passed
@Rafael-SOWNet
Rafael-SOWNet deleted the feat/surds-and-inverse-trig branch August 5, 2026 03:48
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.

Root of a factorizable integer

1 participant