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());