Skip to content

Commit a2c29b2

Browse files
Split a rational function at a rational root of its denominator (#233) (#690)
* Integrate a quotient whose numerator is not constant (#233) The table matched k/(ax^2 + bx + c) only when k did not mention x, so anything with a variable on top had no antiderivative at all: int x / (x^2 + 2x + 5) int (x + 3) / (x^2 + 3x + 2) int x / (x + 1) A linear numerator is a multiple of the denominator's derivative plus a constant: px + q = (p/2a)(2ax + b) + (q - pb/2a). The first part integrates to a logarithm and the second is the constant-numerator case that was already there, so this only needs the rewrite. The same rewrite one degree down turns (px + q)/(bx + c) into the constant p/b plus a remainder over the divisor. Both are guarded on the leading coefficient being a non-zero number, since the rewrite divides by it. A quadratic denominator with a vanishing quadratic term falls through to the linear arm rather than being divided by zero. Also 1/cos(u)^2 and 1/sin(u)^2, which are written that way at least as often as sec(u)^2 and csc(u)^2, and none of the four shapes was recognised. Corpus 88/117 -> 91/117: int:table and int:rational-quadratic reach 100%, and int:partial-fractions goes from 2 out of 5 to 3. 3901 unit tests and 127 F# tests pass, no wrong answers in the corpus, and the property checker's 1320 checks are clean. x^2/(x^4 + 1) and 1/(x^3 + 1) are still out of reach: both need the denominator factored first, which is a different piece of work. * Split a rational function at a rational root of its denominator A linear or quadratic denominator was answered in one piece, but nothing read anything above that, so 1/(x^3 + 1) had no antiderivative at all. It is on the list of what is missing in #233. The decomposition is done a step at a time rather than in full, because a step is all that is needed: what is left over is a smaller problem of the same kind, and by the time its denominator is a quadratic there is already a rule for it. 1/(x^3 + 1) splits at the root -1 into (1/3)/(x + 1) and (2 - x)/(3(x^2 - x + 1)), and the second is a linear numerator over a quadratic, which the previous commit reads as it stands. Each step takes a degree off the denominator, so it ends. The coefficient is Heaviside's -- at the root every other term of the decomposition is finite, so A = N(r)/Q(r) -- and what is left, N - A*Q, then has that root by construction and divides exactly. All of it on the exact ratios, reusing the rational root search already here for factoring. Rational roots only, and simple ones. A denominator with no rational root, such as x^4 + 1, and one whose root repeats, which would need a term over the square as well, are both left unevaluated rather than answered wrongly. A denominator below degree three is left to the rules that answer it in one piece. Ten integrals gained, each checked by differentiating back: 1/(x^3 + 1), x/(x^3 + 1), 1/(x^3 - x), 1/(x^3 + x), 1/(x^4 - 1), x/(x^4 - 1), (x^2 + 1)/(x^3 - x), 1/((x - 1)(x - 2)(x - 3)) whether written factored or multiplied out, and (x + 1)/(x^3 - x^2 - 2x). 21 tests, 10 of which fail without the change. 4038 in all, 127 F#, corpus 105/117 with the partial fraction category at 80% from 60% and nothing wrong, in error or timing out. 1320 property checks.
1 parent 9444691 commit a2c29b2

4 files changed

Lines changed: 204 additions & 2 deletions

File tree

‎Sources/AngouriMath/Functions/Continuous/Integration/IndefiniteIntegralSolver.cs‎

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,30 @@ internal static class IndefiniteIntegralSolver
2323
});
2424
}
2525

26+
/// <summary>
27+
/// A quotient of polynomials whose denominator has a rational root, split at that
28+
/// root and integrated in two parts.
29+
/// </summary>
30+
/// <remarks>
31+
/// The rules for a linear or a quadratic denominator answer those in one piece, so
32+
/// what is left over are the denominators of degree three and up, which nothing
33+
/// read at all: 1/(x^3 + 1) had no antiderivative. Splitting at the root -1 leaves
34+
/// (1/3)/(x + 1) and (2 - x)/(3(x^2 - x + 1)), and both of those are already
35+
/// integrable, the second by the rule for a linear numerator over a quadratic.
36+
/// Each step takes a degree off the denominator, so this ends.
37+
/// </remarks>
38+
internal static Entity? SolveByPartialFractions(Entity expr, Entity.Variable x, bool integrateByParts)
39+
{
40+
if (expr is not Entity.Divf(var numerator, var denominator)
41+
|| !Functions.PolynomialFactoring.TrySplitOffRationalRoot(
42+
numerator, denominator, x, out var simple, out var restNumerator, out var restDenominator))
43+
return null;
44+
return Integration.ComputeIndefiniteIntegral(simple, x, integrateByParts) is { } first
45+
&& Integration.ComputeIndefiniteIntegral(restNumerator / restDenominator, x, integrateByParts) is { } rest
46+
? first + rest
47+
: null;
48+
}
49+
2650
internal static Entity? SolveAsPolynomialTerm(Entity expr, Entity.Variable x, bool integrateByParts = true) => expr switch
2751
{
2852
Entity.Mulf(var m1, var m2) =>

‎Sources/AngouriMath/Functions/Continuous/Integration/Integration.Definition.cs‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ internal static partial class Integration
5858
if ((answer = IndefiniteIntegralSolver.SolveAsPolynomialTerm(expr, x, integrateByParts)) is { }) return answer;
5959
if ((answer = IndefiniteIntegralSolver.SolveLogarithmic(expr, x, integrateByParts)) is { }) return answer;
6060
if ((answer = IndefiniteIntegralSolver.SolveBySubstitution(expr, x, integrateByParts)) is { }) return answer;
61+
if ((answer = IndefiniteIntegralSolver.SolveByPartialFractions(expr, x, integrateByParts)) is { }) return answer;
6162
if (integrateByParts && (answer = IndefiniteIntegralSolver.SolveIntegratingByParts(expr, x)) is { }) return answer;
6263
// this may expand to too many terms
6364
if ((answer = IndefiniteIntegralSolver.SolveBySplittingSum(expr, x, integrateByParts)) is { }) return answer;

‎Sources/AngouriMath/Functions/Simplification/PolynomialFactoring.cs‎

Lines changed: 93 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -105,21 +105,112 @@ internal static bool TryFactor(Entity expr, Variable x, [NotNullWhen(true)] out
105105
return degree > 1;
106106
}
107107

108+
/// <summary>
109+
/// One step of a partial fraction decomposition, at a rational root of the
110+
/// denominator: <c>N/D</c> becomes <c>A/(x - r) + R/Q</c>, where <c>D = (x - r)Q</c>.
111+
/// </summary>
112+
/// <remarks>
113+
/// <para>
114+
/// One step, and not the whole decomposition, because a step is all that is needed:
115+
/// what is left over is a smaller problem of the same kind, and by the time its
116+
/// denominator is a quadratic there is already a rule for it. So
117+
/// <c>1/(x^3 + 1)</c> splits into <c>(1/3)/(x + 1)</c> and <c>(2 - x)/(3(x^2 - x + 1))</c>,
118+
/// and the second of those is a quotient the integrator can read as it stands.
119+
/// </para>
120+
/// <para>
121+
/// The coefficient is Heaviside's: at the root every other term of the
122+
/// decomposition is finite, so <c>A = N(r)/Q(r)</c>. What is left, <c>N - A*Q</c>,
123+
/// then has <c>r</c> for a root by construction and divides exactly.
124+
/// </para>
125+
/// <para>
126+
/// Rational roots only, and simple ones: a repeated root needs a term over
127+
/// <c>(x - r)^2</c> as well, which this does not produce, so it declines instead.
128+
/// Denominators of degree below three are left alone, since the rules for a linear
129+
/// or quadratic denominator already answer those and answer them in one piece.
130+
/// </para>
131+
/// </remarks>
132+
internal static bool TrySplitOffRationalRoot(
133+
Entity numerator, Entity denominator, Variable x,
134+
[NotNullWhen(true)] out Entity? simplePart,
135+
[NotNullWhen(true)] out Entity? restNumerator,
136+
[NotNullWhen(true)] out Entity? restDenominator)
137+
{
138+
simplePart = restNumerator = restDenominator = null;
139+
if (!TryGetRationalCoefficients(denominator, x, leastTerms: 1, leastDegree: 3, out var d)
140+
|| !TryGetRationalCoefficients(numerator, x, leastTerms: 1, leastDegree: 0, out var n))
141+
return false;
142+
// Only a proper fraction decomposes; an improper one is a polynomial plus a
143+
// proper fraction and has to be divided out first, which is not done here.
144+
if (n.Length >= d.Length)
145+
return false;
146+
147+
foreach (var root in RootCandidates(d))
148+
{
149+
if (!Evaluate(d, root).IsZero)
150+
continue;
151+
var q = DivideByLinear(d, root);
152+
var atRoot = Evaluate(q, root);
153+
if (atRoot.IsZero)
154+
continue; // repeated root, see above
155+
var a = Evaluate(n, root).Divide(atRoot);
156+
var left = Subtract(n, Scale(q, a));
157+
if (!Evaluate(left, root).IsZero)
158+
continue; // cannot happen; checked rather than assumed
159+
simplePart = Rational.Create(a) / LinearFactor(root, x);
160+
restNumerator = BuildPolynomial(DivideByLinear(left, root), x);
161+
restDenominator = BuildPolynomial(q, x);
162+
return true;
163+
}
164+
return false;
165+
}
166+
167+
/// <summary>
168+
/// Zero first, then the candidates of the rational root theorem, which cannot offer
169+
/// it: they are built from divisors of the constant term, and a polynomial with a
170+
/// root at zero has none.
171+
/// </summary>
172+
private static IEnumerable<ERational> RootCandidates(ERational[] coefficients)
173+
=> coefficients[0].IsZero
174+
? new[] { ERational.Zero }.Concat(RationalRootCandidates(coefficients))
175+
: RationalRootCandidates(coefficients);
176+
177+
/// <summary>x - r, or x where r is zero.</summary>
178+
private static Entity LinearFactor(ERational root, Variable x)
179+
=> root.IsZero ? x
180+
: root.Sign < 0 ? x + Rational.Create(root.Negate())
181+
: x - Rational.Create(root);
182+
183+
private static ERational[] Scale(ERational[] coefficients, ERational by)
184+
=> coefficients.Select(c => c.Multiply(by)).ToArray();
185+
186+
private static ERational[] Subtract(ERational[] left, ERational[] right)
187+
{
188+
var result = new ERational[System.Math.Max(left.Length, right.Length)];
189+
for (var i = 0; i < result.Length; i++)
190+
result[i] = (i < left.Length ? left[i] : ERational.Zero)
191+
.Subtract(i < right.Length ? right[i] : ERational.Zero);
192+
return result;
193+
}
194+
108195
/// <summary>
109196
/// The coefficients of <paramref name="expr"/> in <paramref name="x"/>, lowest
110197
/// power first, or <see langword="false"/> if it is not a polynomial in
111198
/// <paramref name="x"/> with rational coefficients alone.
112199
/// </summary>
113200
private static bool TryGetRationalCoefficients(Entity expr, Variable x, out ERational[] coefficients)
201+
=> TryGetRationalCoefficients(expr, x, leastTerms: 2, leastDegree: 2, out coefficients);
202+
203+
private static bool TryGetRationalCoefficients(
204+
Entity expr, Variable x, int leastTerms, int leastDegree, out ERational[] coefficients)
114205
{
115206
coefficients = System.Array.Empty<ERational>();
116207
var monomials = PolynomialSolver.GatherMonomialInformation<EInteger, TreeAnalyzer.PrimitiveInteger>(
117208
Sumf.LinearChildren(expr.Expand()), x);
118-
if (monomials is null || monomials.Count < 2)
209+
if (monomials is null || monomials.Count < leastTerms)
119210
return false;
120211

121212
var degree = monomials.Keys.Max();
122-
if (degree is null || degree > MaxDegree || degree < 2)
213+
if (degree is null || degree > MaxDegree || degree < leastDegree)
123214
return false;
124215

125216
var found = new ERational[degree.ToInt32Checked() + 1];
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
//
2+
// Copyright (c) 2019-2022 Angouri.
3+
// AngouriMath is licensed under MIT.
4+
// Details: https://github.com/asc-community/AngouriMath/blob/master/LICENSE.md.
5+
// Website: https://am.angouri.org.
6+
//
7+
8+
using AngouriMath;
9+
using AngouriMath.Extensions;
10+
using Xunit;
11+
12+
namespace AngouriMath.Tests.Calculus
13+
{
14+
/// <summary>
15+
/// Quotients of polynomials whose denominator is of degree three or more. A linear or
16+
/// quadratic denominator was answered in one piece, but nothing read anything above
17+
/// that, so 1/(x^3 + 1) had no antiderivative at all. Named in the issue's own list of
18+
/// what is missing: https://github.com/asc-community/AngouriMath/issues/233.
19+
/// Each answer is checked by differentiating it back and comparing at points, since
20+
/// what matters is that it is an antiderivative and not what form it is written in.
21+
/// </summary>
22+
public sealed class PartialFractionsTest
23+
{
24+
private static void AssertIsAntiderivative(string integrand, params double[] points)
25+
{
26+
var f = integrand.ToEntity();
27+
var antiderivative = f.Integrate("x");
28+
Assert.DoesNotContain("integral(", antiderivative.Stringize());
29+
var derivative = antiderivative.Substitute("C", 0).Differentiate("x");
30+
foreach (var point in points)
31+
{
32+
var expected = f.Substitute("x", point).EvalNumerical().RealPart.EDecimal.ToDouble();
33+
var actual = derivative.Substitute("x", point).EvalNumerical().RealPart.EDecimal.ToDouble();
34+
Assert.Equal(expected, actual, 7);
35+
}
36+
}
37+
38+
// The denominator is split at one rational root at a time, and what is left over is
39+
// a smaller problem of the same kind until its denominator is a quadratic.
40+
[Theory]
41+
[InlineData("1 / (x ^ 3 + 1)", new[] { 0.3, 1.7, 3.2, -2.4 })]
42+
[InlineData("x / (x ^ 3 + 1)", new[] { 0.3, 1.7, 3.2, -2.4 })]
43+
[InlineData("1 / (x ^ 3 - x)", new[] { 0.3, 1.7, 3.2, -2.4 })]
44+
[InlineData("1 / (x ^ 3 + x)", new[] { 0.3, 1.7, 3.2, -2.4 })]
45+
[InlineData("1 / (x ^ 4 - 1)", new[] { 0.3, 1.7, 3.2, -2.4 })]
46+
[InlineData("x / (x ^ 4 - 1)", new[] { 0.3, 1.7, 3.2 })]
47+
[InlineData("(x ^ 2 + 1) / (x ^ 3 - x)", new[] { 0.3, 1.7, 3.2, -2.4 })]
48+
public void ADenominatorOfDegreeThreeOrMore(string integrand, double[] points) =>
49+
AssertIsAntiderivative(integrand, points);
50+
51+
// Whether the denominator arrives factored or multiplied out makes no difference:
52+
// it is read as a polynomial either way.
53+
[Theory]
54+
[InlineData("1 / ((x - 1) * (x - 2) * (x - 3))", new[] { 0.3, 1.7, 3.6, 5.2 })]
55+
[InlineData("1 / (x ^ 3 - 6 * x ^ 2 + 11 * x - 6)", new[] { 0.3, 1.7, 3.6 })]
56+
[InlineData("(x + 1) / (x ^ 3 - x ^ 2 - 2 * x)", new[] { 0.3, 1.7, 3.2, -2.4 })]
57+
public void FactoredOrMultipliedOutAlike(string integrand, double[] points) =>
58+
AssertIsAntiderivative(integrand, points);
59+
60+
// The shapes this sits next to have to keep working, in particular the linear and
61+
// quadratic denominators that are answered in one piece and must not be split.
62+
[Theory]
63+
[InlineData("1 / (x ^ 2 + 1)", new[] { 0.3, 1.7 })]
64+
[InlineData("1 / (x ^ 2 - 1)", new[] { 0.3, 3.2 })]
65+
[InlineData("1 / (x + 1)", new[] { 0.3, 1.7 })]
66+
[InlineData("x / (x ^ 2 + 1)", new[] { 0.3, 1.7 })]
67+
[InlineData("x / (x ^ 2 + 2 * x + 5)", new[] { 0.3, 1.7 })]
68+
[InlineData("1 / x", new[] { 0.3, 1.7 })]
69+
[InlineData("x ^ 2", new[] { 0.3, 1.7 })]
70+
[InlineData("sin(x)", new[] { 0.3, 1.7 })]
71+
[InlineData("x * e ^ x", new[] { 0.3, 1.7 })]
72+
public void NeighbouringFormsAreUnaffected(string integrand, double[] points) =>
73+
AssertIsAntiderivative(integrand, points);
74+
75+
/// <summary>
76+
/// A denominator with no rational root is not split, and neither is one whose root
77+
/// repeats -- that needs a term over the square as well, which is not produced here.
78+
/// Both are left unevaluated rather than answered wrongly.
79+
/// </summary>
80+
[Theory]
81+
[InlineData("x ^ 2 / (x ^ 4 + 1)")]
82+
[InlineData("1 / (x ^ 3 + x ^ 2 + x + 2)")]
83+
public void WhatCannotBeSplitIsLeftAlone(string integrand) =>
84+
Assert.Contains("integral(", integrand.ToEntity().Integrate("x").Stringize());
85+
}
86+
}

0 commit comments

Comments
 (0)