diff --git a/BREAKING-CHANGES.md b/BREAKING-CHANGES.md index c9ce36ff0..02dedd57f 100644 --- a/BREAKING-CHANGES.md +++ b/BREAKING-CHANGES.md @@ -37,6 +37,7 @@ read first. | | `new Entity[0].MultiplyAll()`, and `Mulf.Multiply` on an empty list | `AngouriBugException` | `1` | | | `MathS.Vector()` and `new Entity[0].ToVector()` | `IndexOutOfRangeException` — outside the documented hierarchy | `InvalidMatrixOperationException` | | | `"x3 - 1".ToEntity().Factorize()`, and every polynomial no rewrite rule has a rule for | `x ^ 3 - 1` — handed back whole | `(x - 1) * (x ^ 2 + x + 1)` | +| | `"x^2/(x^4 + 1)".Integrate("x")`, and every quotient whose denominator is a biquadratic irreducible over `Q` | `integral(x ^ 2 / (x ^ 4 + 1), x)` — left unevaluated | the antiderivative, over the real quadratic factors | | | `Entity.DomainConditionIn(Domain)` | did not exist | the domain of definition for a **stated** reading, through the whole tree | | | `MathS.Polynomials.Factor("(y * x3 + 1) * (x4 - y3)", "x")`, and bivariate polynomials whose leading coefficient in the main variable is a polynomial | `null` — a refusal | `(x ^ 4 - y ^ 3) * (x ^ 3 * y + 1)` | | | `MathS.Polynomials.Factor("x7 - y7", "x")`, and bivariate polynomials whose substituted image over-factors | `null` — a refusal | `(x - y) * (x ^ 6 + x ^ 5 * y + … + y ^ 6)` | @@ -2344,6 +2345,11 @@ no integration rule reads: an irreducible of degree three or more, or a quadrati `(x^2 + 1)^2` is declined, and the ladder that would decompose it is deliberately not built, because every term it produces is over `(x^2 + c)^k` and would come back unevaluated in turn. +*The `x^4 + 1` half of that is no longer true: allowing those real coefficients is exactly what the +entry below does, and `x^2/(x^4 + 1)` is now answered. The rest of the paragraph stands — an +irreducible of degree three or more, and a repeated quadratic, are still declined, and `(x^2 + 1)^2` +still is.* + Deciding that from the factorisation rather than by trying is what keeps the cost of declining to the one factorisation. Splitting regardless and recursing made `(1 - x^4)/(1 + x^4 + x^8)`, whose factorisation holds the irreducible quartic `x^4 - x^2 + 1`, take 18s to return the same unevaluated @@ -2351,6 +2357,54 @@ integral it returns in 203ms — measured, and the reason the guard is there rat [#919](https://github.com/asc-community/AngouriMath/issues/919). +### A biquadratic denominator is decomposed over the reals + +The same blindness one level further along, and the last place it reaches. The step above factors +over `Q` and stops where `Q` does, so `x^4 + 1` — irreducible over the rationals — was left whole +and `x^2/(x^4 + 1)` came back unevaluated. Over the reals it is +`(x^2 - sqrt(2)x + 1)(x^2 + sqrt(2)x + 1)`, and both halves are read by the rule for a linear +numerator over a quadratic. Nothing was missing but a factorisation the rational step is right to +refuse. + +``` +"x^2/(x^4 + 1)".Integrate("x") + +was integral(x ^ 2 / (x ^ 4 + 1), x) +is -1/2 * sqrt(2) * 1/2 / 2 * ln(x ^ 2 + sqrt(2) * x + 1) + + 1/2 * arctan((2 * x + sqrt(2)) * 1/2 * sqrt(2)) * 1/2 * sqrt(2) + + 1/2 * sqrt(2) * 1/2 / 2 * ln(x ^ 2 - sqrt(2) * x + 1) + + 1/2 * arctan((2 * x + -sqrt(2)) * 1/2 * sqrt(2)) * 1/2 * sqrt(2) + C +``` + +This is the integral [#233](https://github.com/asc-community/AngouriMath/issues/233) names as +wanting "partial fractioning", and it is the first of that issue's list to need a factorisation +rather than a rule. `1/(x^4 + 1)`, `1/(x^4 - 2)` and `1/(x^4 + 3x^2 + 1)` come with it. + +**Biquadratic only, and that is a boundary rather than a first cut.** A general quartic factors into +real quadratics through its resolvent cubic, whose roots carry Cardano's nested radicals; a +biquadratic `x^4 + px^2 + q` is the case where the resolvent is solvable by inspection and the two +factors stay inside one square root. Two shapes come out of it, by the sign of `p^2 - 4q`: negative +gives `(x^2 + ax + b)(x^2 - ax + b)` with `b = sqrt(q)` and `a = sqrt(2b - p)`, and positive gives +the even `(x^2 + u)(x^2 + v)` with `u, v = (p -+ sqrt(p^2 - 4q))/2`. Zero is `(x^2 + p/2)^2`, a +repeated quadratic, declined for the reason the step above declines one. **A quartic with an odd +power in it — `x^4 + x^3 + 1`, `x^4 + x + 1` — is still declined**, and so is everything of degree +five and up that does not factor over `Q`. + +**No condition is attached**, on the same argument as the step above: the two factors are distinct, +so their product is zero exactly where the original denominator is, and nothing is cancelled. + +It is tried **after** both rational steps, which is what keeps a denominator that factors over `Q` +in exact arithmetic: `x^4 + 3x^2 + 2` is decomposed by the step above and never arrives here to be +given a square root it does not need. Declining stays as cheap as it was — +`(1 - x^4)/(1 + x^4 + x^8)` returns the same unevaluated integral in the same fraction of a second, +because every guard here is rational arithmetic on coefficients already read. + +`sqrt(tan(x))`, the one remaining entry on #233's list, is **not** answered by this. It reduces +under `u = sqrt(tan x)` to `2 * integral(u^2/(u^4 + 1), u)`, which is now integrable — but the +substitution that gets there is a separate capability and is not built here. + +[#233](https://github.com/asc-community/AngouriMath/issues/233). + ### A polynomial equation that factors is solved through its factors A polynomial of degree four or more may factor over the rationals with no rational root anywhere in diff --git a/Sources/AngouriMath/Functions/Algebra/Polynomials/PartialFractions.cs b/Sources/AngouriMath/Functions/Algebra/Polynomials/PartialFractions.cs index c3b3ad652..2fb9fef8a 100644 --- a/Sources/AngouriMath/Functions/Algebra/Polynomials/PartialFractions.cs +++ b/Sources/AngouriMath/Functions/Algebra/Polynomials/PartialFractions.cs @@ -5,6 +5,7 @@ // Website: https://am.angouri.org. // +using PeterO.Numbers; using System.Diagnostics.CodeAnalysis; using static AngouriMath.Entity; using static AngouriMath.Entity.Number; @@ -133,6 +134,181 @@ internal static bool TrySplitIntoCoprimeParts( return true; } + /// + /// N/D written as two fractions over the quadratic factors of a biquadratic + /// — one with no odd power in it — or + /// where it is not one, or does not split into two distinct + /// quadratics. + /// + /// + /// + /// Why this exists next to the step above. That one factors over the rationals, + /// and stops where the rationals do: x^4 + 1 is irreducible over Q, so it + /// is left whole and x^2/(x^4 + 1) has no antiderivative — the case + /// #233 names as + /// wanting "partial fractioning". Over the reals it is + /// (x^2 - sqrt(2)x + 1)(x^2 + sqrt(2)x + 1), and both halves are read by the rule + /// for a linear numerator over a quadratic. Nothing was missing but a factorisation the + /// rational one is right to refuse. + /// + /// + /// Biquadratic only, and that is a real boundary rather than a first cut. A + /// general quartic factors into real quadratics through its resolvent cubic, whose roots + /// carry Cardano's nested radicals; a biquadratic x^4 + px^2 + q is the case where + /// the resolvent is solvable by inspection, and the two factors stay in one square root. + /// Two shapes come out of it, by the sign of p^2 - 4q: + /// + /// + /// + /// Negative — no real root in x^2. Matching + /// (x^2 + ax + b)(x^2 - ax + b) = x^4 + (2b - a^2)x^2 + b^2 gives + /// b = sqrt(q) and a = sqrt(2b - p), both real because q > 0 and + /// p^2 < 4q forces p < 2sqrt(q). This is x^4 + 1, at + /// a = sqrt(2), b = 1. + /// + /// + /// Positive — two distinct real roots in x^2, so + /// (x^2 + M)(x^2 + N) with M, N = (p +- sqrt(p^2 - 4q))/2. Both factors are + /// even, and the split is two independent pairs of equations rather than four. + /// + /// + /// Zero — (x^2 + p/2)^2, a repeated quadratic, declined for the same reason + /// the guard above declines one: there is no rule for a numerator over + /// (x^2 + c)^k, so decomposing it ends in the integral it started from. + /// + /// + /// + /// No condition is owed, on the same argument as the step above: the two factors + /// are distinct and coprime, so their product vanishes exactly where the original + /// denominator does. q > 0 is required rather than assumed, which is what keeps + /// b real; a negative q puts a real root in x^2 of either sign and + /// is left to the rational step, which reaches it whenever the root is rational. + /// + /// + /// Reached only after the rational split has declined, so a biquadratic that factors over + /// Q — x^4 + 3x^2 + 2 — is decomposed there, in exact arithmetic, and never + /// arrives here to be given a square root it does not need. + /// + /// + internal static bool TrySplitBiquadraticOverTheReals( + Entity numerator, Entity denominator, Variable x, + [NotNullWhen(true)] out Entity? left, + [NotNullWhen(true)] out Entity? right) + { + left = right = null; + + // Every guard below is rational arithmetic on coefficients already in hand, so a + // denominator this does not apply to costs one polynomial read to decline. + if (!PolynomialFactoring.TryGetRationalCoefficients( + denominator, x, leastTerms: 2, leastDegree: 4, maxDegree: 4, out var d) + || d.Length != 5 + || !d[1].IsZero || !d[3].IsZero) + return false; + + // A proper fraction only, as above: an improper one is a polynomial plus a proper + // fraction and has to be divided out first, which is not done here. The degree + // ceiling of three is what says so, the denominator's being four. + if (!PolynomialFactoring.TryGetRationalCoefficients( + numerator, x, leastTerms: 1, leastDegree: 0, maxDegree: 3, out var c)) + return false; + + var lead = d[4]; + var p = d[2].Divide(lead); + var q = d[0].Divide(lead); + + // At q = 0 the quartic is x^2(x^2 + p), whose rational root zero the step above has + // already had. Nothing else about the sign of q is required here: the branch that + // needs sqrt(q) real is the one below with a negative discriminant, and p^2 < 4q + // makes q positive on its own. + if (q.IsZero) + return false; + + var discriminant = p.Multiply(p).Subtract(q.Multiply(ERational.FromInt32(4))); + if (discriminant.IsZero) + return false; + + // The numerator, padded to four coefficients so the two branches can index it + // without asking how many terms it happened to have. + var n = new Entity[4]; + for (var i = 0; i < n.Length; i++) + n[i] = Rational.Create(i < c.Length ? c[i] : ERational.Zero); + + Entity leftNumerator, leftFactor, rightNumerator, rightFactor; + if (discriminant.Sign < 0) + { + // (x^2 + ax + b)(x^2 - ax + b). Writing the split as (alpha x + beta)/A + + // (gamma x + delta)/B and equating the four coefficients of + // (alpha x + beta)B + (gamma x + delta)A against the numerator gives, using + // B's -a where A has +a: + // + // x^3: alpha + gamma = n3 + // x^2: a(gamma - alpha) + beta + delta = n2 + // x^1: b(alpha + gamma) + a(delta - beta) = n1 + // x^0: b(beta + delta) = n0 + // + // which is two sums and two differences rather than a linear solve. + var b = MathS.Sqrt(Rational.Create(q)).InnerSimplified; + var a = MathS.Sqrt(2 * b - Rational.Create(p)).InnerSimplified; + + var sum = (n[0] / b).InnerSimplified; // beta + delta + var difference = ((n[1] - b * n[3]) / a).InnerSimplified; // delta - beta + var spread = ((n[2] - sum) / a).InnerSimplified; // gamma - alpha + + leftFactor = MathS.Sqr(x) + a * x + b; + rightFactor = MathS.Sqr(x) - a * x + b; + leftNumerator = ((n[3] - spread) / 2 * x + (sum - difference) / 2).InnerSimplified; + rightNumerator = ((n[3] + spread) / 2 * x + (sum + difference) / 2).InnerSimplified; + } + else + { + // (x^2 + u)(x^2 + v), both even, so the odd and even halves of the numerator + // separate and each gives its own pair rather than one system of four. Writing + // the split as (alpha x + beta)/(x^2 + u) + (gamma x + delta)/(x^2 + v), the + // coefficients of (alpha x + beta)(x^2 + v) + (gamma x + delta)(x^2 + u) are + // + // x^3: alpha + gamma = n3 x^1: v*alpha + u*gamma = n1 + // x^2: beta + delta = n2 x^0: v*beta + u*delta = n0 + // + // so alpha = (n1 - u*n3)/(v - u) and beta = (n0 - u*n2)/(v - u), with v - u the + // square root of the discriminant. Note which of the two the numerators divide + // by: pairing a numerator with the wrong factor flips the sign of the answer + // and still satisfies the x^3 and x^2 rows, so it is not something the identity + // check further down would catch on every numerator. + var root = MathS.Sqrt(Rational.Create(discriminant)).InnerSimplified; + var v = ((Rational.Create(p) + root) / 2).InnerSimplified; + var u = ((Rational.Create(p) - root) / 2).InnerSimplified; + + var alpha = ((n[1] - u * n[3]) / root).InnerSimplified; + var beta = ((n[0] - u * n[2]) / root).InnerSimplified; + + leftFactor = MathS.Sqr(x) + u; + rightFactor = MathS.Sqr(x) + v; + leftNumerator = (alpha * x + beta).InnerSimplified; + rightNumerator = ((n[3] - alpha) * x + (n[2] - beta)).InnerSimplified; + } + + // Two identities, checked rather than assumed -- the numerators against the numerator + // they decompose, and the factors against the denominator they came from. The step + // above checks one because its factorisation is exact by construction; here the + // factors were built by matching coefficients through a square root, so the + // factorisation is a claim of its own. + // + // Neither implies the other, so both are made. A wrong term common to the two factors + // -- MathS.Sqr(x) misread as C#'s x ^ 2, which on an Entity is exclusive or and not a + // power -- cancels between the two halves of the first identity and passes it, while + // the second sees it immediately. + if ((leftNumerator * rightFactor + rightNumerator * leftFactor + - numerator).Simplify() != Integer.Create(0)) + return false; + if ((Rational.Create(lead) * leftFactor * rightFactor - denominator).Simplify() + != Integer.Create(0)) + return false; + + left = (leftNumerator / (Rational.Create(lead) * leftFactor)).InnerSimplified; + right = (rightNumerator / (Rational.Create(lead) * rightFactor)).InnerSimplified; + return true; + } + /// /// A vanishing numerator is answered as zero rather than as a quotient, so that a /// numerator sharing a factor with the denominator does not leave the integrator diff --git a/Sources/AngouriMath/Functions/Continuous/Integration/IndefiniteIntegralSolver.cs b/Sources/AngouriMath/Functions/Continuous/Integration/IndefiniteIntegralSolver.cs index 1776531ca..594b32a4a 100644 --- a/Sources/AngouriMath/Functions/Continuous/Integration/IndefiniteIntegralSolver.cs +++ b/Sources/AngouriMath/Functions/Continuous/Integration/IndefiniteIntegralSolver.cs @@ -45,6 +45,15 @@ internal static class IndefiniteIntegralSolver /// applies and leaves something that will not integrate — it takes the denominator /// apart differently, so a failure of the one is no evidence about the other. /// + /// + /// Both of those factor over the rationals and stop where the rationals do, which left + /// x^2/(x^4 + 1) unevaluated: x^4 + 1 is irreducible over Q, and over the reals it is + /// (x^2 - sqrt(2)x + 1)(x^2 + sqrt(2)x + 1). The third step + /// () reads a + /// biquadratic denominator that way. It is last because it is the only one that + /// introduces a radical, and a denominator that factors over Q should be taken apart in + /// exact arithmetic by one of the two above. + /// /// internal static Entity? SolveByPartialFractions(Entity expr, Entity.Variable x, bool integrateByParts) { @@ -63,6 +72,12 @@ internal static class IndefiniteIntegralSolver && Integration.ComputeIndefiniteIntegral(right, x, integrateByParts) is { } overOther) return overOne + overOther; + if (Functions.PartialFractions.TrySplitBiquadraticOverTheReals( + numerator, denominator, x, out var overOneReal, out var overOtherReal) + && Integration.ComputeIndefiniteIntegral(overOneReal, x, integrateByParts) is { } realFirst + && Integration.ComputeIndefiniteIntegral(overOtherReal, x, integrateByParts) is { } realRest) + return realFirst + realRest; + return null; } diff --git a/Sources/Tests/UnitTests/Calculus/PartialFractionsTest.cs b/Sources/Tests/UnitTests/Calculus/PartialFractionsTest.cs index 57c96cffd..53fd16b69 100644 --- a/Sources/Tests/UnitTests/Calculus/PartialFractionsTest.cs +++ b/Sources/Tests/UnitTests/Calculus/PartialFractionsTest.cs @@ -103,18 +103,73 @@ public void ANumeratorThatCancels(string integrand, double[] points) => public void ARootAndAnIrreducibleFactorTogether(string integrand, double[] points) => AssertIsAntiderivative(integrand, points); + /// + /// A biquadratic denominator that is irreducible over the rationals but factors over + /// the reals, which is the remaining case + /// #233 names: + /// x^4 + 1 is (x^2 - sqrt(2)x + 1)(x^2 + sqrt(2)x + 1), and both halves + /// are read by the rule for a linear numerator over a quadratic. + /// + [Theory] + [InlineData("x ^ 2 / (x ^ 4 + 1)", new[] { 0.3, 1.7, 3.2, -2.4 })] + [InlineData("1 / (x ^ 4 + 1)", new[] { 0.3, 1.7, 3.2, -2.4 })] + [InlineData("x ^ 3 / (x ^ 4 + 1)", new[] { 0.3, 1.7, 3.2, -2.4 })] + [InlineData("(x ^ 3 + 1) / (x ^ 4 + 1)", new[] { 0.3, 1.7, 3.2, -2.4 })] + [InlineData("(x ^ 2 + x) / (x ^ 4 + 1)", new[] { 0.3, 1.7, 3.2, -2.4 })] + // A leading coefficient other than one is divided out rather than refused. + [InlineData("1 / (2 * x ^ 4 + 2)", new[] { 0.3, 1.7, 3.2, -2.4 })] + [InlineData("1 / (3 * x ^ 4 + 12)", new[] { 0.3, 1.7, 3.2, -2.4 })] + public void ABiquadraticThatFactorsOnlyOverTheReals(string integrand, double[] points) => + AssertIsAntiderivative(integrand, points); + + /// + /// The other shape a biquadratic takes, where p^2 - 4q is positive so there are + /// two real roots in x^2 and the factors are the even (x^2 + u)(x^2 + v). + /// A negative q puts one factor either side of zero -- x^4 - 2 is + /// (x^2 - sqrt(2))(x^2 + sqrt(2)) -- so one half integrates to a logarithm and + /// the other to an arctangent, which is what makes it worth testing next to the pair + /// above rather than folded into them. + /// + [Theory] + [InlineData("1 / (x ^ 4 + 3 * x ^ 2 + 1)", new[] { 0.3, 1.7, 3.2, -2.4 })] + [InlineData("x ^ 2 / (x ^ 4 + 3 * x ^ 2 + 1)", new[] { 0.3, 1.7, 3.2, -2.4 })] + [InlineData("1 / (x ^ 4 - 2)", new[] { 0.3, 1.7, 3.2, -2.4 })] + [InlineData("x ^ 2 / (x ^ 4 - 2)", new[] { 0.3, 1.7, 3.2, -2.4 })] + [InlineData("1 / (x ^ 4 - 5 * x ^ 2 + 5)", new[] { 0.3, 3.2, -2.4 })] + public void ABiquadraticWithTwoRealRootsInTheSquare(string integrand, double[] points) => + AssertIsAntiderivative(integrand, points); + /// /// What is still left unevaluated rather than answered wrongly: a denominator that is - /// irreducible, and one that is a power of a single irreducible. The second is not a - /// splitting problem -- there is no coprime pair to split it into, and the ladder - /// over f^k that would decompose it produces terms over (x^2 + 1)^2 - /// that no integration rule reads, so decomposing it would answer nothing. + /// irreducible and not biquadratic, and one that is a power of a single irreducible. + /// The second is not a splitting problem -- there is no coprime pair to split it into, + /// and the ladder over f^k that would decompose it produces terms over + /// (x^2 + 1)^2 that no integration rule reads, so decomposing it would answer + /// nothing. x^2/(x^4 + 1) used to be on this list and is now answered above; the + /// step over the reals reaches a biquadratic only, so a quartic with an odd power in it + /// stays here. /// [Theory] - [InlineData("x ^ 2 / (x ^ 4 + 1)")] [InlineData("1 / (x ^ 3 + x ^ 2 + x + 2)")] [InlineData("1 / (x ^ 4 + 2 * x ^ 2 + 1)")] + [InlineData("1 / (x ^ 4 + x ^ 3 + 1)")] + [InlineData("1 / (x ^ 4 + x + 1)")] public void WhatCannotBeSplitIsLeftAlone(string integrand) => Assert.Contains("integral(", integrand.ToEntity().Integrate("x").Stringize()); + + /// + /// The guard that keeps declining cheap, which the step over the reals must not undo: + /// this factorises into an irreducible quartic that nothing reads, and the whole point + /// of reading the factorisation is that finding that out costs one factorisation rather + /// than a search of every half of every split. + /// + [Fact] + public void DecliningStaysCheap() + { + var clock = System.Diagnostics.Stopwatch.StartNew(); + var answer = "(1 - x ^ 4) / (1 + x ^ 4 + x ^ 8)".ToEntity().Integrate("x"); + Assert.Contains("integral(", answer.Stringize()); + Assert.True(clock.Elapsed < System.TimeSpan.FromSeconds(10), $"took {clock.Elapsed}"); + } } } diff --git a/Sources/Tests/UnitTests/Calculus/PowerSubstitutionIntegralTest.cs b/Sources/Tests/UnitTests/Calculus/PowerSubstitutionIntegralTest.cs index 0081ec396..965c7e8a0 100644 --- a/Sources/Tests/UnitTests/Calculus/PowerSubstitutionIntegralTest.cs +++ b/Sources/Tests/UnitTests/Calculus/PowerSubstitutionIntegralTest.cs @@ -119,13 +119,23 @@ public void WhatIntegratedBeforeStillDoes(string integrand) => DifferentiatesBack(integrand); /// - /// And the rewrite does not make a candidate succeed that should not. - /// x^2 / (x^4 + 1) under u = x^2 leaves a bare x behind, so it is - /// still rejected — that integral needs the denominator factored over the reals, which - /// this does not do, and it remains open on the issue. + /// And the rewrite does not make a candidate succeed that should not: under + /// u = x^2 each of these leaves a bare x behind, so the substitution + /// rejects them. /// - [Fact] - public void AnIntegralThisDoesNotReachIsStillDeclined() - => Assert.Contains("integral(", "x^2/(x^4 + 1)".ToEntity().Integrate("x").Stringize()); + /// + /// The witness used to be x^2/(x^4 + 1), which is now answered — not by this + /// substitution, which still rejects it for the same reason, but by the partial fraction + /// step learning to factor a biquadratic denominator over the reals. A test that the + /// substitution declines something needs an integrand nothing else answers either, or it + /// stops testing the substitution the moment a neighbouring capability arrives. These + /// carry an odd power, which puts them out of reach of that step as well. + /// . + /// + [Theory] + [InlineData("x^2/(x^4 + x + 1)")] + [InlineData("x^2/(x^4 + x^3 + 1)")] + public void AnIntegralThisDoesNotReachIsStillDeclined(string integrand) + => Assert.Contains("integral(", integrand.ToEntity().Integrate("x").Stringize()); } } diff --git a/Sources/Tests/UnitTests/Calculus/RationalIntegralsTest.cs b/Sources/Tests/UnitTests/Calculus/RationalIntegralsTest.cs index d17e97f4e..35137dad7 100644 --- a/Sources/Tests/UnitTests/Calculus/RationalIntegralsTest.cs +++ b/Sources/Tests/UnitTests/Calculus/RationalIntegralsTest.cs @@ -105,12 +105,17 @@ public void ARepeatedRationalRootDecomposesToo(string integrand, double[] points public void ADenominatorThatFactorsWithNoRationalRoot(string integrand, double[] points) => AssertIsAntiderivative(integrand, points); - // What is out of reach is a denominator that does not factor over Q at all -- x^4 + 1 - // is irreducible and only factors once real coefficients are allowed -- and one that - // is a power of a single irreducible, which has no coprime pair to split into. - // Recorded so the boundary is visible rather than inferred from an absence. + // What is out of reach is a denominator that does not factor over Q and is not a + // biquadratic either -- an odd power puts it past the step that factors over the reals -- + // and one that is a power of a single irreducible, which has no coprime pair to split + // into. Recorded so the boundary is visible rather than inferred from an absence. + // + // x^2/(x^4 + 1) was the first entry here, on the grounds that x^4 + 1 is irreducible + // over Q and only factors once real coefficients are allowed. Allowing them is what the + // real-quadratic step now does, so it moved to PartialFractionsTest as an answer. [Theory] - [InlineData("x ^ 2 / (x ^ 4 + 1)")] + [InlineData("1 / (x ^ 4 + x + 1)")] + [InlineData("1 / (x ^ 4 + x ^ 3 + 1)")] [InlineData("1 / (x ^ 4 + 2 * x ^ 2 + 1)")] public void ADenominatorThatDoesNotFactorIsStillDeclined(string integrand) => Assert.Contains("integral(", integrand.ToEntity().Integrate("x").Stringize());