Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 25 additions & 17 deletions BREAKING-CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 |
|---|---|---|
Expand All @@ -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.

Expand Down Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion Sources/.editorconfig
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down
26 changes: 12 additions & 14 deletions Sources/AngouriMath/Convenience/MathS.Polynomials.cs
Original file line number Diff line number Diff line change
Expand Up @@ -133,10 +133,10 @@ public static class Polynomials
/// So the content in <paramref name="variable"/> — the greatest common divisor of the
/// coefficients, which is a polynomial in the other variables — is taken out first,
/// using the same multivariate machinery <see cref="Gcd"/> 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
/// <c>x ^ 2 - y ^ 2</c>: 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 <see cref="KroneckerFactorization"/> answers instead —
/// <c>x ^ 2 - y ^ 2</c> is <c>(x + y) * (x - y)</c>, which is a factorisation over
/// ℚ(y) reached by substitution rather than by lifting.
/// </para>
/// </remarks>
private static Entity? FactorAfterTakingOutTheContent(Entity expr, Variable variable)
Expand All @@ -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
Expand All @@ -177,21 +177,19 @@ private static bool SameAsOne(MultivariatePolynomial poly)
=> poly.IsConstant && poly.CoefficientOf(0).CompareTo(ERational.One) == 0;

/// <summary>
/// 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.
/// </summary>
/// <remarks>
/// Kronecker's substitution: see <see cref="BivariateFactorization"/> for what it
/// Kronecker's substitution: see <see cref="KroneckerFactorization"/> for what it
/// does, what it refuses, and why a wrong answer is not among the things it can do.
/// </remarks>
private static Entity? Bivariate(
private static Entity? Kronecker(
MultivariatePolynomial poly, IReadOnlyList<Variable> variables,
IReadOnlyDictionary<Variable, int> 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:
Expand Down
Loading
Loading