From 8cdcd8b98814373d257880ed4c284807a0c413d5 Mon Sep 17 00:00:00 2001 From: Rafael Vuijk Date: Tue, 27 Oct 2026 16:55:16 +0000 Subject: [PATCH] The square root taken after solving for x^2 is exact where it can be 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 https://github.com/asc-community/AngouriMath/issues/2140 Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_012sonx8iAspMiwRwokT1Ura --- BREAKING-CHANGES.md | 6 +++++- .../Core/Entity/Omni/Entity.Matrix.cs | 4 ++-- .../EquationSolver/PolynomialSolver.cs | 10 ++++++++-- .../UnitTests/Algebra/EigenvaluesTest.cs | 4 +++- .../Algebra/SquareDiscriminantTest.cs | 19 +++++++++++++++++++ 5 files changed, 37 insertions(+), 6 deletions(-) diff --git a/BREAKING-CHANGES.md b/BREAKING-CHANGES.md index 2e1480250..a1851eb11 100644 --- a/BREAKING-CHANGES.md +++ b/BREAKING-CHANGES.md @@ -62,7 +62,8 @@ one inside another was answered with the empty set, a claim that it has no solut **Silent: the same roots, written as what they are.** The quadratic formula wrote `sqrt(b^2 - 4ac)` whatever the discriminant was, so a symbolic quadratic with a square discriminant came back with the square under the root. Either square root gives the same pair of roots, so where the coefficients are polynomials and the discriminant is -a rational number times the square of one, the roots are written with that polynomial and reduced over `2a` +a rational number times the square of one, the roots are written with that polynomial and reduced over `2a`. So is +the square root the solver takes after solving for `x^2`, where an equation has no term in `x` ([#2140](https://github.com/asc-community/AngouriMath/issues/2140)). | Input | Was (2.6.0) | Now | @@ -71,6 +72,9 @@ a rational number times the square of one, the roots are written with that polyn | `"x^2 - (p + q)*x + p*q = 0".Solve("x")` | `{ (p + q - sqrt((-(p + q)) ^ 2 - 4 * p * q)) / 2, … }` | `{ q, p }` | | `"x^2 - 2*a*x + a^2 - 2 = 0".Solve("x")` | `{ (-(-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".Solve("x")` | `{ (-(-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), … }` | +| `"x^2 = (a + b)^2".Solve("x")` | `{ sqrt((a + b) ^ 2), -sqrt((a + b) ^ 2) }` | `{ -a - b, a + b }` | +| `"x^4 - (a^2 + b^2)*x^2 + a^2*b^2 = 0".Solve("x")` | `{ sqrt((a ^ 2 + b ^ 2 - sqrt((-(a ^ 2 + b ^ 2)) ^ 2 - 4 * a ^ 2 * b ^ 2)) / 2), … }` | `{ -b, b, -a, a }` | +| `"sin(x)^2 = (a + 1)^2".Solve("x")` | `{ arcsin(sqrt((a + 1) ^ 2)) + 2 * pi * n_1, … }` | `{ arcsin(-a - 1) + 2 * pi * n_1, … }` | ### A system with a product equation is solved branch by branch diff --git a/Sources/AngouriMath/Core/Entity/Omni/Entity.Matrix.cs b/Sources/AngouriMath/Core/Entity/Omni/Entity.Matrix.cs index 1cbb21ed1..48a52c69c 100644 --- a/Sources/AngouriMath/Core/Entity/Omni/Entity.Matrix.cs +++ b/Sources/AngouriMath/Core/Entity/Omni/Entity.Matrix.cs @@ -383,8 +383,8 @@ public Entity AsScalar() /// /// /// The characteristic polynomial divides by nothing, so its roots carry no condition an - /// elimination's pivots would bring: [[0, J], [J, 0]] has sqrt(J^2) and - /// -sqrt(J^2), which hold at J = 0 too. Up to the fourth degree the roots are + /// elimination's pivots would bring: [[0, J], [J, 0]] has J and -J, + /// which hold at J = 0 too. Up to the fourth degree the roots are /// in radicals, and past it where the polynomial factors; otherwise the set is the one the /// equation solver leaves. /// #1676 diff --git a/Sources/AngouriMath/Functions/Continuous/Solvers/EquationSolver/PolynomialSolver.cs b/Sources/AngouriMath/Functions/Continuous/Solvers/EquationSolver/PolynomialSolver.cs index 021b13b52..9f273c0d6 100644 --- a/Sources/AngouriMath/Functions/Continuous/Solvers/EquationSolver/PolynomialSolver.cs +++ b/Sources/AngouriMath/Functions/Continuous/Solvers/EquationSolver/PolynomialSolver.cs @@ -400,8 +400,14 @@ Entity GetMonomialByPower(EInteger power) => } is { } resConcat ? !gcdPower.Equals(EInteger.One) // if we had x^6 + x^3 + 1, we replace it with x^2 + x + 1 and find all cubic roots of the final answer - ? res.Concat(resConcat).SelectMany(_ => gcdPowerRoots, - (root, coef) => coef * MathS.Pow(root, Rational.Create(1, gcdPower))) + // x^2 = y is a quadratic in x, so a y that is a rational number times a square has + // its two roots written without the root, as the formula's are: x^2 = (a + b)^2 was + // { sqrt((a + b)^2), -sqrt((a + b)^2) } and is { -a - b, a + b }. + // https://github.com/asc-community/AngouriMath/issues/2140 + ? res.Concat(resConcat).SelectMany(root => + gcdPower.Equals(EInteger.FromInt32(2)) && RootsOverASquareDiscriminant(1, 0, -root) is { } exact + ? exact + : gcdPowerRoots.Select(coef => coef * MathS.Pow(root, Rational.Create(1, gcdPower)))) : res.Concat(resConcat) : null; } diff --git a/Sources/Tests/UnitTests/Algebra/EigenvaluesTest.cs b/Sources/Tests/UnitTests/Algebra/EigenvaluesTest.cs index 01be746d6..cdcef52f5 100644 --- a/Sources/Tests/UnitTests/Algebra/EigenvaluesTest.cs +++ b/Sources/Tests/UnitTests/Algebra/EigenvaluesTest.cs @@ -33,7 +33,9 @@ public void ARepeatedRootIsOneMember() public void OfASymbolicMatrixCarryNoPivotCondition() { var eigenvalues = MathS.Matrix(new Entity[,] { { 0, "J" }, { "J", 0 } }).Eigenvalues; - Assert.Equal(new FiniteSet("sqrt(J^2)", "-sqrt(J^2)"), eigenvalues); + // lambda^2 = J^2 is rooted exactly, so these are J and -J rather than the square under + // a root. https://github.com/asc-community/AngouriMath/issues/2140 + Assert.Equal(new FiniteSet("-J", "J"), eigenvalues); Assert.DoesNotContain("provided", eigenvalues!.Stringize()); } diff --git a/Sources/Tests/UnitTests/Algebra/SquareDiscriminantTest.cs b/Sources/Tests/UnitTests/Algebra/SquareDiscriminantTest.cs index 01c190100..8c6e40b98 100644 --- a/Sources/Tests/UnitTests/Algebra/SquareDiscriminantTest.cs +++ b/Sources/Tests/UnitTests/Algebra/SquareDiscriminantTest.cs @@ -70,5 +70,24 @@ public void OnlyTheRootOfTheNumberIsLeft(string equation, string first, string s Assert.Contains(roots, root => Equal(root, second.ToEntity())); Assert.DoesNotContain(roots, root => root.Nodes.Any(node => node is Powf(var radicand, Rational { Denominator: var d }) && d == 2 && radicand is not Number)); } + + /// + /// With no term in x the solver takes x^2 as its unknown and the square + /// root after, and that root was left over the square too: x^2 = (a + b)^2 was + /// { sqrt((a + b)^2), -sqrt((a + b)^2) }. + /// + [Theory] + [InlineData("x^2 = (a + b)^2", new[] { "a + b", "-a - b" })] + [InlineData("x^2 + (p - q)^2/4 = 0", new[] { "i*(p - q)/2", "-i*(p - q)/2" })] + [InlineData("x^2 = 2*a^2", new[] { "sqrt(2)*a", "-sqrt(2)*a" })] + [InlineData("x^4 - (a^2 + b^2)*x^2 + a^2*b^2 = 0", new[] { "a", "-a", "b", "-b" })] + public void APowerOfTwoAgainstASquareIsRootedExactly(string equation, string[] expected) + { + var roots = Roots(equation, "x"); + Assert.Equal(expected.Length, roots.Length); + foreach (var root in expected) + Assert.Contains(roots, found => Equal(found, root.ToEntity())); + Assert.DoesNotContain(roots, root => root.Nodes.Any(node => node is Powf(var radicand, Rational { Denominator: var d }) && d == 2 && radicand is not Number)); + } } }