Skip to content

A quadratic whose discriminant is a square is answered without the root - #2170

Merged
Rafael-SOWNet merged 2 commits into
masterfrom
a-square-discriminant-is-taken-out-of-the-root
Oct 27, 2026
Merged

Rafael-SOWNet merged 2 commits into
masterfrom
a-square-discriminant-is-taken-out-of-the-root

Conversation

@Rafael-SOWNet

@Rafael-SOWNet Rafael-SOWNet commented Oct 27, 2026 •

Copy link
Copy Markdown
Member

Fixes #2140.

The defect: SolveQuadratic wrote sqrt(b^2 - 4ac) whatever the discriminant was. With polynomial coefficients and a discriminant that is a square, the square stayed under the root.

Solve 2.6.0 this PR
a*z^2 - (a^2 - b*c)*z - a*b*c = 0 { (a ^ 2 + -b * c - sqrt((-(a ^ 2 + -b * c)) ^ 2 - 4 * a * -a * b * c)) / (2 * a), … } { -b * c / a, a }
x^2 - (p + q)*x + p*q = 0 { (p + q - sqrt((-(p + q)) ^ 2 - 4 * p * q)) / 2, … } { q, p }
x^2 - 2*a*x + a^2 - 2 = 0 { (-(-2) * a - sqrt(((-2) * a) ^ 2 - 4 * (a ^ 2 + -2))) / 2, … } { a - sqrt(2), a + sqrt(2) }
2*x^2 - 2*(p + q)*x + p^2 + q^2 = 0 { (-(-2) * (p + q) - sqrt(((-2) * (p + q)) ^ 2 - 8 * (p ^ 2 + q ^ 2))) / 4, … } { 1/2 * p + 1/2 * q - i * (1/2 * p - 1/2 * q), … }

Why the change is sound: either square root of the discriminant gives the same pair of roots, (-b ∓ r)/(2a), so any polynomial r with r^2 = b^2 - 4ac serves, and no absolute value is needed.

The change: it applies where a, b and c parse as polynomials and the discriminant is a rational number times the square of a polynomial. The square root comes from MultivariatePolynomial.TrySquareRoot, the exact one the integrator already uses.

  • Each numerator -b ∓ r is reduced by its greatest common divisor with 2a, rational factors included.
  • Where the number's own root is not rational, that root (sqrt(k), or i sqrt(k)) is the only factor left outside.
  • Anything else takes the formula as before. 3x^2 - 2(a + b)x + ab, whose discriminant 4(a^2 - ab + b^2) is no square, is unchanged.

Checked:

  • Fourteen equations of this kind had every root substituted back at sampled values of the other symbols, with residual 0.
  • SquareDiscriminantTest: 10 rows. Each checks that the expected roots are there, by value, and that no radical is left over a polynomial.
  • Unit suite: 17,138 passed, 0 failed.
  • RootCheck: 596/596 clean, with 0 incomplete and 0 unsound.
  • CasBench: 120/121 solved, 0 wrong. Its one unsolved row, ln(x) + ln(y) - ln(x * y), is the same as in the committed report.
  • BREAKING-CHANGES entry added, measured on the 2.6.0 package.

Performance:

  • The first CI run failed the performance gate. SolveEasyMedium (x^2 + x + a) allocated 69,690 B against 66,661, +4.5% where the band is 3%. Its discriminant 1 - 4a cannot be a square, but the coefficients were turned into polynomials before that was found.
  • The second commit refuses a discriminant whose degree in some symbol is odd before any polynomial is built. A square has an even degree in every variable.
  • Bytes per SolveEquation, measured on one machine:
master this PR
x2 + x + a 66,976 67,064
x2 + b*x + a 70,488 70,616
x2 + x + 1 450,114 450,282
  • The unit suite after that commit: 17,138 passed, 0 failed.

Not in this PR:

  • With no linear term the solver inverts the power instead: x^2 + (p - q)^2/4 = 0 is still { sqrt(-(p - q) ^ 2 / 4), … }. The same argument holds there, since both signs are in the set, but that code is the n-th root inversion for every exponent, and changing it needs its own measurement.
  • factor::z::… refusing the polynomial from the issue is MathS.Polynomials.Factor declining a bivariate coefficient ring. That is separate too.

🤖 Generated with Claude Code

https://claude.ai/code/session_012sonx8iAspMiwRwokT1Ura

SolveQuadratic wrote sqrt(b^2 - 4ac) whatever the discriminant was. With
polynomial coefficients and a square discriminant that left the square
under the root: a z^2 - (a^2 - bc) z - abc has discriminant (a^2 + bc)^2
and roots a and -bc/a, and came back as
(a^2 - bc -/+ sqrt((a^2 - bc)^2 + 4 a^2 bc)) / (2a). So did the most
ordinary one, z^2 - (p + q) z + pq, whose roots are p and q.

Either square root of the discriminant gives the same pair of roots, so
any polynomial r with r^2 = b^2 - 4ac serves, with no absolute value.
Where a, b and c parse as polynomials and the discriminant is a rational
number times the square of one (MultivariatePolynomial.TrySquareRoot, the
exact square root the integrator already uses), each numerator -b -/+ r
is reduced by its greatest common divisor with 2a. Where the number's root
is not rational, it is the one factor left outside: a +/- sqrt(2),
(p + q)/2 +/- i (p - q)/2. Anything else takes the formula as before.

On 2.6.0 and on this branch, Solve:
  a*z^2 - (a^2 - b*c)*z - a*b*c = 0     sqrt form -> { -b * c / a, a }
  x^2 - (p + q)*x + p*q = 0             sqrt form -> { q, p }
  x^2 - 2*a*x + a^2 - 2 = 0             sqrt form -> { a - sqrt(2), a + sqrt(2) }
Fourteen such equations had every root substituted back at sampled values
of the other symbols, with residual 0. Unit suite 17,138 passed, 0 failed;
RootCheck 596/596 clean; CasBench 120/121, its one unsolved row unchanged.

Fixes #2140

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012sonx8iAspMiwRwokT1Ura
@Rafael-SOWNet Rafael-SOWNet added this to the 2.7.0 milestone Oct 27, 2026
@Rafael-SOWNet Rafael-SOWNet moved this from Todo to In review in AngouriMath Oct 27, 2026
@codecov

codecov Bot commented Oct 27, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 95.89041% with 3 lines in your changes missing coverage. Please review.
✅ Project coverage is 86.42%. Comparing base (689fd01) to head (11b0b53).
⚠️ Report is 1 commits behind head on master.

Files with missing lines Patch % Lines
...tinuous/Solvers/EquationSolver/PolynomialSolver.cs 95.89% 1 Missing and 2 partials ⚠️
Additional details and impacted files

Impacted file tree graph

@@           Coverage Diff           @@
##           master    #2170   +/-   ##
=======================================
  Coverage   86.42%   86.42%           
=======================================
  Files         295      295           
  Lines       64806    64879   +73     
  Branches    19059    19083   +24     
=======================================
+ Hits        56009    56074   +65     
- Misses       4768     4774    +6     
- Partials     4029     4031    +2     
Flag Coverage Δ
cpp 37.77% <ø> (ø)
csharp 87.00% <95.89%> (+<0.01%) ⬆️
fsharp 32.97% <ø> (ø)
interactive 40.72% <ø> (ø)

Flags with carried forward coverage won't be shown. Click here to find out more.

Files with missing lines Coverage Δ
...tinuous/Solvers/EquationSolver/PolynomialSolver.cs 83.53% <95.89%> (+5.30%) ⬆️

... and 5 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

…mial is built

The performance gate failed on #2170: SolveEasyMedium, x^2 + x + a,
allocated 69,690 B against a baseline of 66,661 (+4.5%, band 3%). Its
discriminant 1 - 4a is of degree one in a and cannot be a square, but the
three coefficients were parsed, multiplied and normalized before
TrySquareRoot said so.

A square has an even degree in every variable, and where 2 deg b and
deg a + deg c differ the larger is the discriminant's degree, so a
quadratic for which that is odd is refused first. Degrees as written bound
the true ones; a coefficient that cancels inside itself can be refused
when it need not be, which leaves it to the formula as before.

On one machine, B per SolveEquation:  master    this
  x2 + x + a                           66,976    67,064
  x2 + b*x + a                         70,488    70,616
  x2 + x + 1                          450,114   450,282
The fourteen equations still check by substitution, and the unit suite is
17,138 passed, 0 failed.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012sonx8iAspMiwRwokT1Ura
@Rafael-SOWNet
Rafael-SOWNet merged commit 6d5d9e3 into master Oct 27, 2026
37 of 38 checks passed
@github-project-automation github-project-automation Bot moved this from In review to Done in AngouriMath Oct 27, 2026
Rafael-SOWNet added a commit that referenced this pull request Oct 27, 2026
…2173)

A polynomial whose powers share a factor is solved in x^gcd, and its roots
are the gcd-th roots of what that gives. With the factor 2 that is a square
root, written sqrt(y) whatever y was, so an equation with no term in x kept
the square under the root that #2170 took out of the formula:
x^2 = (a + b)^2 was { sqrt((a + b)^2), -sqrt((a + b)^2) }.

x^2 = y is the quadratic x^2 - y = 0, so with the factor 2 each y goes
through the same exact root as the formula, and keeps sqrt where it is not
a rational number times a square. The parity check in front refuses a y of
odd degree in a symbol, such as x^2 = a, before any polynomial is built.

On 2.6.0 and on this branch, Solve:
  x^2 = (a + b)^2                        sqrt form -> { -a - b, a + b }
  x^4 - (a^2 + b^2)*x^2 + a^2*b^2 = 0    nested sqrt -> { -b, b, -a, a }
  sin(x)^2 = (a + 1)^2                   arcsin(sqrt((a + 1)^2)) -> arcsin(a + 1)
The eigenvalues of [[0, J], [J, 0]] are J and -J instead of +-sqrt(J^2);
Eigenvalues is new since 2.6.0, so its test and doc comment are updated
and no released answer changes there.

Twenty-three equations had every root substituted back at sampled values
of the other symbols, with residual 0. Unit suite 17,144 passed, 0 failed;
RootCheck 596/596 clean; CasBench 120/121, unchanged.

Part of #2140


Claude-Session: https://claude.ai/code/session_012sonx8iAspMiwRwokT1Ura

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@Rafael-SOWNet
Rafael-SOWNet deleted the a-square-discriminant-is-taken-out-of-the-root branch October 28, 2026 10:27
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

A quadratic whose discriminant is a square is answered with the square left under the root

1 participant