Skip to content

Factor takes out the content instead of refusing - #1053

Merged
Rafael-SOWNet merged 2 commits into
masterfrom
feat/factor-takes-out-the-content
Aug 25, 2026
Merged

Rafael-SOWNet merged 2 commits into
masterfrom
feat/factor-takes-out-the-content

Conversation

@Rafael-SOWNet

Copy link
Copy Markdown
Member

Part of #746 tier 1, item 43.

What was wrong

MathS.Polynomials.Factor works over ℚ, so a coefficient that is not a rational number stopped it
before it began, and every polynomial in more than one variable was refused. Some of them never
needed a bigger ring at all: x * y + y is y times something univariate, and only the y was in
the way.

The refusal test in PolynomialSurfaceTest carried x * y + y as a case, under a comment that says:

Handing x * y + y back from a factorisation would say that y * (x + 1) does not exist, which is
a wrong answer and not a graceful failure.

It was right about the answer that ought to exist. This makes it exist.

What changed

The content in the named variable — the greatest common divisor of the coefficients, which is a
polynomial in the other variables — is taken out first, via PolynomialGcd.ContentIn, the same
multivariate machinery MathS.Polynomials.Gcd is already built from. What remains goes down the
ordinary path.

2.3.0 now
Factor("x * y + y", "x") null y * (x + 1)
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 ^ 3 * y - x * y", "x") null y * x * (x + 1) * (x - 1)
Factor("x ^ 2 - y ^ 2", "x") null null
Factor("x * y + z", "x") null null

What it does not claim

Only a refusal becomes an answer. The new path runs solely where the old one returned null, so
nothing that already factorised can change.

It is still a refusal wherever the content is a constant, and x ^ 2 - y ^ 2 is the honest case for
that: it needs factorisation over ℚ(y), which this is not. Item 43 asks for multivariate
factorisation over ℚ and 𝔽ₚ and this narrows that gap rather than closing it — the roadmap row
should still say factorisation is univariate over ℚ, with the content taken out first.

Verification

The new tests check the factorisation numerically, at twenty random points per case, rather than
as a string. That is deliberate: Simplify does not prove y * x * (x + 1) equal to
x ^ 2 * y + x * y, and the two are equal — a string or Simplify comparison reports a defect that
is not one, which it did while this was being written.

  • Suite: 8561 passed, 0 failed, 14 skipped.
  • casbench, rootcheck and simpsweep re-run against this branch are byte-identical to
    master's committed reports.

Rafael-SOWNet and others added 2 commits August 24, 2026 23:45
`MathS.Polynomials.Factor` works over Q, so a coefficient that is not a rational
number stopped it before it began and every polynomial in more than one variable
was refused. Some of them never needed a bigger ring: `x * y + y` is `y` times
something univariate, and only the `y` was in the way.

The content in the named variable -- the greatest common divisor of the
coefficients, a polynomial in the other variables -- is taken out first, using
`PolynomialGcd.ContentIn`, the same multivariate machinery `Gcd` is already built
from, and what remains goes down the ordinary path.

    Factor("x * y + y", "x")              null  ->  y * (x + 1)
    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: the new path runs only where the old one
returned null, so nothing that already factorised changes. It is still a refusal
wherever the content is a constant, and `x ^ 2 - y ^ 2` is the honest case for
that -- it needs factorisation over Q(y), which this is not and does not claim to
be. That is the open half of item 43 and this narrows it rather than closing it.

The refusal test carried `x * y + y` with a comment saying that handing it back
"would say that `y * (x + 1)` does not exist, which is a wrong answer and not a
graceful failure". It asserts that answer now. Checked numerically at twenty
random points per case rather than as a string: `Simplify` does not prove
`y * x * (x + 1)` equal to `x ^ 2 * y + x * y`, and the two are equal, so a
string comparison would have reported a defect that is not one.

Suite 8561/0. casbench, rootcheck and simpsweep re-run against this branch are
byte-identical to master's committed reports.

Part of #746 tier 1.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Bjumi5K7fg8yx6UK1mZTQd
@Rafael-SOWNet
Rafael-SOWNet merged commit 3945a8e into master Aug 25, 2026
31 checks passed
Rafael-SOWNet added a commit that referenced this pull request Aug 25, 2026
…1054)

`MathS.Polynomials.SquareFreePart` refused every polynomial in more than one
variable, for the same reason `Factor` did: it was written against a
representation whose coefficients are rational numbers.

`p / gcd(p, dp/dx)` is the square-free part whatever ring the coefficients live
in -- a repeated factor appears in the derivative one time fewer than in the
polynomial, so dividing by the common part leaves each distinct factor exactly
once. Nothing about that is univariate, and the multivariate representation has
all three operations already: `DerivativeIn`, the recursive `PolynomialGcd` that
`Gcd` is built from, and exact division.

    SquareFreePart("(x - y) ^ 2 * (x + y)", "x")   null  ->  x ^ 2 - y ^ 2
    SquareFreePart("(x - y) ^ 3", "x")             null  ->  x - y
    SquareFreePart("(x + a) ^ 2 * (x + b)", "x")   null  ->  (x + a) * (x + b)
    SquareFreePart("x ^ 2 * y ^ 2", "x")           null  ->  x
    SquareFreePart("y", "x")                       null  ->  null

The last two are worth saying out loud. The content is dropped, as it always
was: `SquareFreePart("4 * x ^ 2", "x")` is `x` and not `4 * x`, because the
univariate path takes the primitive part first -- so `x ^ 2 * y ^ 2` is `x` with
`y ^ 2` as the content, which is the existing convention applied to a wider ring
rather than a new one. That was checked against the univariate path rather than
assumed; the expectation written first was the wrong one.

Reached only where the rational path declined, so nothing that already answered
can change. Tests compare numerically at twenty points per case rather than as
strings, for the reason #1053 records.

Suite 8569/0.

Part of #746 tier 1, item 43.


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

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Rafael-SOWNet added a commit that referenced this pull request Aug 25, 2026
…with it (#1059)

Kronecker's substitution does not merely fail to find a factorisation -- it
proves there is none. A splitting of the polynomial into two parts of positive
degree in the main variable maps to a splitting of the one-variable image,
because the substitution is a ring homomorphism on the monomials that can
appear, and every splitting of the image is one of the subsets the
recombination tries. So where nothing recombines, nothing exists.

That proof was being reported as null, and the content went with it:

    x * y + y * z          was null, is y * (x + z)
    a * x + a * y + a * z  was null, is a * (x + y + z)
    x ^ 2 * y - y ^ 3      was null, is y * (x + y) * (x - y)

The content had already been taken out and the factorisation assembled; the
single-factor rest then threw the whole thing away. It also disagreed with the
one-variable path, where Factor("x ^ 2 + 1", "x") has always been x ^ 2 + 1,
so x ^ 2 + y ^ 2, x ^ 2 - a and x * y + z now answer the same way.

The proof has a precondition, and it is now checked rather than assumed.
DivideExact answers null both for "does not divide" and for "ran out of room",
and only the first is evidence -- so it says which, and the irreducibility
claim is withheld where a division was cut short by a term or degree budget.
Where FromImage cannot read a candidate back the recombination now refuses
instead of skipping it silently, since a skipped candidate is a subset that was
never tried and the proof is over all of them.

The XML documentation on Factor still said "Univariate only", with a worked
example asserting that Factor("x * y + y", "x") is null. That has been false
since #1053 and is public API documentation, which nothing in the suite or in
the docsamples harness reads.


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

Co-authored-by: Rafael <darkfader@gmail.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
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.

1 participant