From 09c39ecc4f8052c9411bbbf04a43b41c2e6cac56 Mon Sep 17 00:00:00 2001 From: Rafael Date: Tue, 25 Aug 2026 17:27:40 +0000 Subject: [PATCH] Factor a polynomial in any number of variables, not only two Kronecker's substitution was written for two variables because that is the case https://github.com/asc-community/AngouriMath/issues/746 item 43 names, but nothing in it is about two. The exponent pair (i, j) was being packed as i + s*j, which is a two-digit numeral in radix s -- and an exponent vector of any length is a numeral in mixed radix, with radices d_i + 1 and place values s_0 = 1, s_(i+1) = s_i * (d_i + 1). A factor has degree at most d_i in each variable because it divides the polynomial, so the map is injective on every monomial that can appear in the polynomial or in any of its factors, whatever the number of variables. So the class is now KroneckerFactorization rather than BivariateFactorization, the encoding and the decoding are loops over the variables instead of two statements, and the caller no longer refuses a third one. x ^ 2 - (y + z) ^ 2 -> (x + y + z) * (x - y - z) x ^ 2 + 2 * x * y + y ^ 2 - z ^ 2 -> (x + y + z) * (x + y - z) (x + y) * (x + z) * (x + w) -> (x + y) * (w + x) * (x + z) What changes with the number of variables is the ceiling, and it changes sharply: the image has degree the product of the radices less one, not their sum, so three variables of degree 2 fit within the one-variable factoriser's 32 and four do not. That is a refusal and never a wrong answer -- every candidate is still checked by exact division and the assembled factors are still divided back into the input -- and a test now pins three shapes that factor mathematically and are declined. BREAKING-CHANGES had two entries disagreeing about x ^ 2 - y ^ 2, the second having overtaken the first; the earlier one now points at the later instead of contradicting it. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01Bjumi5K7fg8yx6UK1mZTQd --- BREAKING-CHANGES.md | 42 +++-- Sources/.editorconfig | 2 +- .../Convenience/MathS.Polynomials.cs | 26 ++- ...orization.cs => KroneckerFactorization.cs} | 166 +++++++++++------- .../Polynomials/PolynomialSurfaceTest.cs | 57 +++++- 5 files changed, 189 insertions(+), 104 deletions(-) rename Sources/AngouriMath/Functions/Algebra/Polynomials/{BivariateFactorization.cs => KroneckerFactorization.cs} (55%) diff --git a/BREAKING-CHANGES.md b/BREAKING-CHANGES.md index 022dc7d2d..4af73b5e6 100644 --- a/BREAKING-CHANGES.md +++ b/BREAKING-CHANGES.md @@ -305,14 +305,16 @@ bracketing for. The change only ever adds `\left(`/`\right)` groups, which CShar parses, so nothing downstream needs a matching change ([#822](https://github.com/asc-community/AngouriMath/issues/822)). -### `Factor` factors a polynomial in two variables +### `Factor` factors a polynomial in more than one variable -After the content is taken out, what remains may still have polynomial coefficients — and where it -is in two variables it can be factored anyway, by **Kronecker's substitution**. A factor of a -polynomial of degree `d` in `x` has degree at most `d` in `x`, so with `s = d + 1` the map -`x^i y^j → t^(i + s*j)` is injective on every monomial that can appear in the polynomial or in any -of its factors. The one-variable image is factored by the existing factoriser, and each subset of -its irreducible factors names a candidate. +After the content is taken out, what remains may still have polynomial coefficients — and it can be +factored anyway, by **Kronecker's substitution written in mixed radix**. A factor of a polynomial +has degree at most `d_i` in each variable `v_i`, because a factor divides it. So with radices +`d_i + 1` and place values `s_0 = 1`, `s_(i+1) = s_i * (d_i + 1)`, the map sending a monomial to +`t^(Σ e_i · s_i)` writes each exponent as one digit of a numeral, and is therefore injective on +every monomial that can appear in the polynomial or in any of its factors. The one-variable image +is factored by the existing factoriser, and each subset of its irreducible factors names a +candidate. | | 2.3.0 | now | |---|---|---| @@ -322,17 +324,23 @@ its irreducible factors names a candidate. | `Factor("x ^ 4 - y ^ 4", "x")` | `null` | `(x + y) * (x ^ 2 + y ^ 2) * (x - y)` | | `Factor("x ^ 2 * y ^ 2 - 1", "x")` | `null` | `(x * y + 1) * (x * y - 1)` | | `Factor("x ^ 2 - y ^ 2 + 2 * x + 1", "x")` | `null` | `(x + y + 1) * (x - y + 1)` | +| `Factor("x ^ 2 - (y + z) ^ 2", "x")` | `null` | `(x + y + z) * (x - y - z)` | +| `Factor("x ^ 2 + 2 * x * y + y ^ 2 - z ^ 2", "x")` | `null` | `(x + y + z) * (x + y - z)` | +| `Factor("(x + y) * (x + z) * (x + w)", "x")` | `null` | `(x + y) * (w + x) * (x + z)` | | `Factor("x ^ 2 + y ^ 2", "x")` | `null` | `null` — irreducible over ℚ | -| `Factor("x * y + z", "x")` | `null` | `null` — three variables | +| `Factor("x * y + z", "x")` | `null` | `null` — irreducible over ℚ | **It cannot answer wrongly.** The substitution is injective on monomials but not on factorisations, so the image may factor further than the polynomial does and a candidate is a guess. Every one is tested by exact division before it is kept, and the assembled factors are divided back into the input, so the failure mode is a refusal. -**What it refuses.** The image has degree `d + s*e` for degree `e` in the second variable, and the -one-variable factoriser stops at 32 — so this reaches bidegrees like (2, 10), (3, 7) and (5, 4) and -refuses past them. The recombination is over subsets, so the image's factor count is capped too. +**What it refuses.** The image has degree `Π (d_i + 1) - 1`, a **product** and not a sum, and the +one-variable factoriser stops at 32 — so the ceiling closes quickly as variables are added. Two +variables reach bidegrees like (2, 10), (3, 7) and (5, 4); three variables of degree 2 fit (27) and +four do not (81). `Factor("x ^ 12 - y ^ 12", "x")` and +`Factor("(x + y + z + w) * (x - y)", "x")` are both `null` for this reason, though both factor +mathematically. The recombination is over subsets, so the image's factor count is capped too. Lifting that ceiling is Hensel lifting with an evaluation homomorphism, which is a different piece of work. @@ -381,14 +389,14 @@ the other variables — is now taken out first, using the same multivariate mach | `Factor("x ^ 2 * y + x * y", "x")` | `null` | `y * x * (x + 1)` | | `Factor("a * x ^ 2 + a * x", "x")` | `null` | `a * x * (x + 1)` | | `Factor("x ^ 2 * y ^ 2 - y ^ 2", "x")` | `null` | `y ^ 2 * (x + 1) * (x - 1)` | -| `Factor("x ^ 2 - y ^ 2", "x")` | `null` | `null` | | `Factor("x * y + z", "x")` | `null` | `null` | -**Only a refusal becomes an answer.** Nothing that already factorised changes, because the new path -runs only where the old one returned `null`. And it is still a refusal wherever the content is a -constant: `x ^ 2 - y ^ 2` genuinely needs factorisation over ℚ(y), which this is not and does not -claim to be. That remains the open half of -[#746](https://github.com/asc-community/AngouriMath/issues/746) item 43. +**Only a refusal becomes an answer.** Nothing that already factorised changes, because this path +runs only where the old one returned `null`. + +Taking the content out does nothing where the content is a constant, so `x ^ 2 - y ^ 2` is not +answered by this change — it needs factorisation over ℚ(y). That is what Kronecker's substitution +does, in the entry above, and the two paths are tried in that order. The test that pinned the refusal carried a comment saying that handing `x * y + y` back *"would say that `y * (x + 1)` does not exist, which is a wrong answer and not a graceful failure"*. It now diff --git a/Sources/.editorconfig b/Sources/.editorconfig index 10473aee5..bc7424bab 100644 --- a/Sources/.editorconfig +++ b/Sources/.editorconfig @@ -27,7 +27,7 @@ file_header_template=\nCopyright (c) 2019-2026 Angouri.\nAngouriMath is licensed [Tests/UnitTests/Core/Transformations/*.cs] file_header_template=\nCopyright (c) 2019-2026 Angouri.\nAngouriMath is licensed under MIT.\nDetails: https://github.com/asc-community/AngouriMath/blob/master/LICENSE.md.\nWebsite: https://am.angouri.org.\n -[AngouriMath/Functions/Algebra/Polynomials/BivariateFactorization.cs] +[AngouriMath/Functions/Algebra/Polynomials/KroneckerFactorization.cs] file_header_template=\nCopyright (c) 2019-2026 Angouri.\nAngouriMath is licensed under MIT.\nDetails: https://github.com/asc-community/AngouriMath/blob/master/LICENSE.md.\nWebsite: https://am.angouri.org.\n [AngouriMath/Functions/Algebra/Groebner/*.cs] diff --git a/Sources/AngouriMath/Convenience/MathS.Polynomials.cs b/Sources/AngouriMath/Convenience/MathS.Polynomials.cs index daa0af086..4b2c017b6 100644 --- a/Sources/AngouriMath/Convenience/MathS.Polynomials.cs +++ b/Sources/AngouriMath/Convenience/MathS.Polynomials.cs @@ -133,10 +133,10 @@ public static class Polynomials /// So the content in — the greatest common divisor of the /// coefficients, which is a polynomial in the other variables — is taken out first, /// using the same multivariate machinery is built from, and what - /// remains goes down the ordinary path. Where the content is a constant this has - /// nothing to offer and says so, which is the honest answer for - /// x ^ 2 - y ^ 2: that one genuinely needs factorisation over ℚ(y) and is not - /// what this does. + /// remains goes down the ordinary path. Where the content is a constant that path + /// has nothing to offer, and answers instead — + /// x ^ 2 - y ^ 2 is (x + y) * (x - y), which is a factorisation over + /// ℚ(y) reached by substitution rather than by lifting. /// /// private static Entity? FactorAfterTakingOutTheContent(Entity expr, Variable variable) @@ -157,12 +157,12 @@ public static class Polynomials if (poly.DivideExact(content) is not { } primitive) return null; - // What is left may still have polynomial coefficients, and where it is in two - // variables it can be factored anyway -- see BivariateFactorization. + // What is left may still have polynomial coefficients, and it can be factored + // anyway while the substitution's ceiling allows -- see KroneckerFactorization. var rest = Assemble( PolynomialFactorization.FactorComplete(primitive.ToEntity(variables), variable), variable) - ?? Bivariate(primitive, variables, index, variable); + ?? Kronecker(primitive, variables, index, variable); if (rest is null) return null; return content.IsConstant && content.DivideExact(content) is not null @@ -177,21 +177,19 @@ private static bool SameAsOne(MultivariatePolynomial poly) => poly.IsConstant && poly.CoefficientOf(0).CompareTo(ERational.One) == 0; /// - /// The factorisation of a polynomial in exactly two variables, as an expression. + /// The factorisation of a polynomial in more than one variable, as an expression. /// /// - /// Kronecker's substitution: see for what it + /// Kronecker's substitution: see for what it /// does, what it refuses, and why a wrong answer is not among the things it can do. /// - private static Entity? Bivariate( + private static Entity? Kronecker( MultivariatePolynomial poly, IReadOnlyList variables, IReadOnlyDictionary index, Variable variable) { - if (variables.Count != 2) + if (variables.Count < 2) return null; - var main = index[variable]; - var other = main == 0 ? 1 : 0; - if (BivariateFactorization.Factor(poly, main, other) is not { } factors + if (KroneckerFactorization.Factor(poly, index[variable]) is not { } factors || factors.Count < 2) return null; // Repeated factors are collected into a power, as the one-variable path does: diff --git a/Sources/AngouriMath/Functions/Algebra/Polynomials/BivariateFactorization.cs b/Sources/AngouriMath/Functions/Algebra/Polynomials/KroneckerFactorization.cs similarity index 55% rename from Sources/AngouriMath/Functions/Algebra/Polynomials/BivariateFactorization.cs rename to Sources/AngouriMath/Functions/Algebra/Polynomials/KroneckerFactorization.cs index dc8b8bd48..9066bfc31 100644 --- a/Sources/AngouriMath/Functions/Algebra/Polynomials/BivariateFactorization.cs +++ b/Sources/AngouriMath/Functions/Algebra/Polynomials/KroneckerFactorization.cs @@ -11,38 +11,41 @@ namespace AngouriMath.Functions { /// - /// Factorisation of a polynomial in two variables, by reducing it to one variable and - /// putting the answer back. + /// Factorisation of a polynomial in any number of variables, by reducing it to one variable + /// and putting the answer back. /// /// /// - /// Kronecker's substitution. A factor of a polynomial of degree d in x - /// has degree at most d in x, so with s = d + 1 the map - /// x^i y^j -> t^(i + s*j) is injective on every monomial that can appear in the - /// polynomial or in any of its factors: i is the remainder and j the quotient of - /// the exponent by s, and neither can be confused with another pair. So a factorisation - /// of the one-variable image can be read back, and each subset of its irreducible factors - /// names a candidate. + /// Kronecker's substitution, in mixed radix. A factor of a polynomial has degree at + /// most d_i in each variable v_i, because a factor divides it. So with radices + /// d_i + 1 and place values s_0 = 1, s_(i+1) = s_i * (d_i + 1), the map + /// v_0^e_0 · … · v_(k-1)^e_(k-1) -> t^(Σ e_i · s_i) writes each exponent as one digit + /// of a numeral and is therefore injective on every monomial that can appear in the + /// polynomial or in any of its factors. A factorisation of the one-variable image can be read + /// back digit by digit, and each subset of its irreducible factors names a candidate. /// /// - /// A candidate is a guess and is checked by division. The image can factor further than - /// the polynomial does — the substitution is injective on monomials, not on factorisations — - /// so a subset whose product reads back as a polynomial need not divide the original. Every - /// one is tested with exact division before it is kept, which is why this cannot answer - /// wrongly: the worst it does is fail to find a factorisation that exists and say so. + /// A candidate is a guess and is checked by division. The image can factor further + /// than the polynomial does — the substitution is injective on monomials, not on + /// factorisations — so a subset whose product reads back as a polynomial need not divide the + /// original. Every one is tested with exact division before it is kept, which is why this + /// cannot answer wrongly: the worst it does is fail to find a factorisation that exists and + /// say so. /// /// - /// What it will not do. The image has degree d + s * e for a polynomial of - /// degree e in y, and the one-variable factoriser stops at - /// — so this reaches bidegrees like (2, 10), - /// (3, 7) and (5, 4) and refuses beyond them. The recombination is over subsets, so the - /// number of irreducible factors of the image is capped as well. Both limits are refusals, - /// never wrong answers, and neither is the algorithm one would write to lift this ceiling: - /// that is Hensel lifting with an evaluation homomorphism, and it is a different piece of - /// work (#746 item 43). + /// What it will not do. The image has degree Π (d_i + 1) - 1, a *product* and + /// not a sum, and the one-variable factoriser stops at + /// — so the ceiling closes quickly as variables are + /// added. Two variables reach bidegrees like (2, 10), (3, 7) and (5, 4); three variables of + /// degree 2 fit (27 ≤ 32) and four do not (81); and a quadratic in eight variables is far + /// past it. The recombination is over subsets, so the number of irreducible factors of the + /// image is capped as well. Both limits are refusals, never wrong answers, and neither is the + /// algorithm one would write to lift them: that is Hensel lifting with an evaluation + /// homomorphism, and it is a different piece of work + /// (#746 item 43). /// /// - internal static class BivariateFactorization + internal static class KroneckerFactorization { /// /// Beyond this the subset search is refused rather than paid for: the recombination is @@ -52,31 +55,47 @@ internal static class BivariateFactorization private const int MaxImageFactors = 12; /// - /// The factors of in and , - /// each to the first power and each of positive degree in , or - /// where nothing could be settled. A single factor means the - /// polynomial did not factor, which is an answer. + /// The factors of in and whatever other + /// variables it has, each to the first power and each of positive degree in + /// , or where nothing could be settled. A + /// single factor means the polynomial did not factor, which is an answer. /// internal static IReadOnlyList? Factor( - MultivariatePolynomial poly, int x, int y) + MultivariatePolynomial poly, int main) { - var degreeInX = poly.DegreeIn(x); - var degreeInY = poly.DegreeIn(y); - if (poly.IsZero || degreeInX < 1 || degreeInY < 1) + if (poly.IsZero || poly.DegreeIn(main) < 1) return null; - var stride = degreeInX + 1; - var imageDegree = degreeInX + stride * degreeInY; - if (imageDegree > IntegerPolynomial.MaxDegree) + // The main variable is placed first so that its exponent is the lowest digit; the + // rest follow in index order, and a variable the polynomial does not use is left out + // rather than given a radix of one. + var order = new List { main }; + for (var variable = 0; variable < poly.VariableCount; variable++) + if (variable != main && poly.DegreeIn(variable) > 0) + order.Add(variable); + if (order.Count < 2) return null; - if (ToImage(poly, x, y, stride, imageDegree) is not { } image) + var radices = new int[order.Count]; + var places = new int[order.Count]; + long size = 1; + for (var i = 0; i < order.Count; i++) + { + radices[i] = poly.DegreeIn(order[i]) + 1; + places[i] = (int)size; + size *= radices[i]; + if (size > IntegerPolynomial.MaxDegree + 1) + return null; + } + var imageDegree = (int)size - 1; + + if (ToImage(poly, order, places, radices, imageDegree) is not { } image) return null; if (PolynomialFactorization.FactorPrimitive(image.PrimitivePart()) is not { } parts) return null; // The multiplicities are flattened: a square in the image may or may not be a square - // in two variables, and the recombination settles that by division rather than by + // in the original, and the recombination settles that by division rather than by // carrying the exponent across the substitution. var irreducibles = new List(); foreach (var part in parts) @@ -89,7 +108,7 @@ internal static class BivariateFactorization if (irreducibles.Count < 2) return new[] { poly }; - return Recombine(poly, irreducibles, x, y, stride); + return Recombine(poly, irreducibles, main, order, places, radices); } /// @@ -97,23 +116,15 @@ internal static class BivariateFactorization /// not wanted, since a rational multiple of a factor is the same factor. /// private static IntegerPolynomial? ToImage( - MultivariatePolynomial poly, int x, int y, int stride, int imageDegree) + MultivariatePolynomial poly, IReadOnlyList order, + IReadOnlyList places, IReadOnlyList radices, int imageDegree) { var coefficients = new ERational[imageDegree + 1]; for (var i = 0; i < coefficients.Length; i++) coefficients[i] = ERational.Zero; - foreach (var byX in poly.CoefficientsIn(x)) - foreach (var byY in byX.Value.CoefficientsIn(y)) - { - // Anything left is a third variable, and this is the two-variable case. - if (!byY.Value.IsConstant) - return null; - var at = byX.Key + stride * byY.Key; - if (at > imageDegree) - return null; - coefficients[at] = coefficients[at].Add(byY.Value.CoefficientOf(0)); - } + if (!Peel(poly, 0, 0)) + return null; var denominator = EInteger.One; foreach (var coefficient in coefficients) @@ -123,32 +134,57 @@ internal static class BivariateFactorization whole[i] = coefficients[i].Numerator .Multiply(denominator.Divide(coefficients[i].Denominator)); return IntegerPolynomial.Create(whole); + + // One variable at a time, adding that exponent's digit to the place already + // accumulated. What is left when every variable has been peeled has to be a constant. + bool Peel(MultivariatePolynomial rest, int depth, int at) + { + if (depth == order.Count) + { + if (!rest.IsConstant) + return false; + coefficients[at] = coefficients[at].Add(rest.CoefficientOf(0)); + return true; + } + foreach (var pair in rest.CoefficientsIn(order[depth])) + { + if (pair.Key >= radices[depth]) + return false; + if (!Peel(pair.Value, depth + 1, at + places[depth] * pair.Key)) + return false; + } + return true; + } } private static EInteger Lcm(EInteger left, EInteger right) => left.Divide(left.Gcd(right)).Multiply(right); /// - /// Two variables again, reading each exponent as its remainder and quotient by - /// — or where a power is past what a - /// packed monomial holds. + /// Reading each exponent back as one digit of the numeral — or + /// where a power is past what a packed monomial holds. /// private static MultivariatePolynomial? FromImage( - IntegerPolynomial image, int variableCount, int x, int y, int stride) + IntegerPolynomial image, int variableCount, IReadOnlyList order, + IReadOnlyList places, IReadOnlyList radices) { var result = MultivariatePolynomial.Zero(variableCount); for (var power = 0; power <= image.Degree; power++) { if (image[power].IsZero) continue; - var inX = power % stride; - var inY = power / stride; - if (inX > MultivariatePolynomial.MaxDegree || inY > MultivariatePolynomial.MaxDegree) - return null; - if (MultivariatePolynomial.Monomial(variableCount, x).Power(inX) is not { } partX - || MultivariatePolynomial.Monomial(variableCount, y).Power(inY) is not { } partY - || partX.Multiply(partY) is not { } monomial) - return null; + var monomial = MultivariatePolynomial.One(variableCount); + for (var i = 0; i < order.Count; i++) + { + var digit = power / places[i] % radices[i]; + if (digit > MultivariatePolynomial.MaxDegree) + return null; + if (MultivariatePolynomial.Monomial(variableCount, order[i]).Power(digit) + is not { } part + || monomial.Multiply(part) is not { } multiplied) + return null; + monomial = multiplied; + } result = result.Add(monomial.ScaleBy(ERational.Create(image[power], EInteger.One))); } return result.IsZero ? null : result; @@ -164,7 +200,8 @@ private static EInteger Lcm(EInteger left, EInteger right) /// after each success because the remaining polynomial has changed. /// private static IReadOnlyList? Recombine( - MultivariatePolynomial poly, List irreducibles, int x, int y, int stride) + MultivariatePolynomial poly, List irreducibles, int main, + IReadOnlyList order, IReadOnlyList places, IReadOnlyList radices) { var found = new List(); var remaining = poly; @@ -183,8 +220,9 @@ private static EInteger Lcm(EInteger left, EInteger right) product = multiplied; else return null; - if (FromImage(product, poly.VariableCount, x, y, stride) is not { } candidate - || candidate.DegreeIn(x) < 1 + if (FromImage(product, poly.VariableCount, order, places, radices) + is not { } candidate + || candidate.DegreeIn(main) < 1 || remaining.DivideExact(candidate) is not { } quotient) continue; found.Add(candidate.Normalized()); diff --git a/Sources/Tests/UnitTests/Algebra/Polynomials/PolynomialSurfaceTest.cs b/Sources/Tests/UnitTests/Algebra/Polynomials/PolynomialSurfaceTest.cs index ac7d2f853..ae0bb895e 100644 --- a/Sources/Tests/UnitTests/Algebra/Polynomials/PolynomialSurfaceTest.cs +++ b/Sources/Tests/UnitTests/Algebra/Polynomials/PolynomialSurfaceTest.cs @@ -157,15 +157,19 @@ public void ASquareFreePartHasNoRepeatedRoot(string input) } /// - /// A polynomial in two variables is factored by Kronecker's substitution: with - /// s one more than its degree in x, the map x^i y^j -> t^(i + s*j) - /// is injective on every monomial that can appear in it or in any of its factors, so a - /// factorisation of the image reads back and each subset of its irreducible factors - /// names a candidate. Every candidate is checked by exact division, so the failure mode - /// is a refusal and not a wrong answer. + /// A polynomial in several variables is factored by Kronecker's substitution, written in + /// mixed radix: with radices d_i + 1 and place values s_0 = 1, + /// s_(i+1) = s_i * (d_i + 1), the map sending a monomial to + /// t^(sum of e_i * s_i) writes each exponent as one digit of a numeral, so it is + /// injective on every monomial that can appear in the polynomial or in any of its + /// factors. A factorisation of the one-variable image reads back digit by digit, and each + /// subset of its irreducible factors names a candidate. Every candidate is checked by + /// exact division, so the failure mode is a refusal and not a wrong answer. /// /// - /// x ^ 2 - y ^ 2 is the case #746 item 43 names, and the one this test exists for. + /// x ^ 2 - y ^ 2 is the case #746 item 43 names. The three- and four-variable rows + /// are the generalisation: the exponent vector is a numeral whatever its length, and only + /// the image's degree — a product of the radices, not a sum — decides what fits. /// Compared numerically, for the reason the content test above gives. /// [Theory] @@ -175,7 +179,12 @@ public void ASquareFreePartHasNoRepeatedRoot(string input) [InlineData("x ^ 2 * y ^ 2 - 1", 2)] [InlineData("x ^ 4 - y ^ 4", 3)] [InlineData("x ^ 2 - y ^ 2 + 2 * x + 1", 2)] - public void APolynomialInTwoVariablesIsFactored(string input, int distinctFactors) + [InlineData("x ^ 2 - (y + z) ^ 2", 2)] + [InlineData("x ^ 2 + 2 * x * y + y ^ 2 - z ^ 2", 2)] + [InlineData("x * y - x - y + 1", 2)] + [InlineData("(x + y) * (x + z + w)", 2)] + [InlineData("(x + y) * (x + z) * (x + w)", 3)] + public void APolynomialInSeveralVariablesIsFactored(string input, int distinctFactors) { var expr = input.ToEntity(); var factored = MathS.Polynomials.Factor(expr, "x"); @@ -207,6 +216,38 @@ public void APolynomialInTwoVariablesIsFactored(string input, int distinctFactor private static int CountFactors(Entity product) => product is Mulf(var left, var right) ? CountFactors(left) + CountFactors(right) : 1; + /// + /// The image's degree is a product of the radices and not a sum, so the ceiling + /// closes quickly as variables are added — and where it closes the answer is a refusal. + /// + /// + /// Every row here does factor mathematically and is declined anyway, which is the shape + /// of every limit in this path: a refusal is a possible answer and a wrong one is not. + /// The degrees are (2, 2, 1, 1) and (4, 1, 1, 1, 1), giving images of + /// degree 35 and 79 against an IntegerPolynomial.MaxDegree of 32; the third is + /// two variables and past it on its own. + /// + [Theory] + [InlineData("(x + y + z + w) * (x - y)")] + [InlineData("(x + y) * (x + z) * (x + w) * (x + v)")] + [InlineData("x ^ 12 - y ^ 12")] + public void PastTheSubstitutionsCeilingItRefuses(string input) + => Assert.Null(MathS.Polynomials.Factor(input.ToEntity(), "x")); + + /// + /// The main variable is a parameter and not a convention: the same polynomial factored + /// with respect to another variable is the same factorisation. + /// + [Fact] + public void TheMainVariableIsAParameter() + { + var inX = MathS.Polynomials.Factor("x ^ 2 - y ^ 2".ToEntity(), "x"); + var inY = MathS.Polynomials.Factor("x ^ 2 - y ^ 2".ToEntity(), "y"); + Assert.NotNull(inX); + Assert.NotNull(inY); + Assert.Equal(inX!.Expand().Simplify(), inY!.Expand().Simplify()); + } + /// /// The square-free part is p / gcd(p, dp/dx) whatever ring the coefficients live /// in, so it is not univariate for any reason but the representation it used to be