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
13 changes: 13 additions & 0 deletions BREAKING-CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -396,6 +396,19 @@ in Rubi's 1.1.3.4 come out shorter as well, and those that took two seconds take
| `"1/(x^8*(a + b*x^3)*(c + d*x^3))".ToEntity().Integrate("x")` | `integral(...)` | 1,217 characters |
| `"1/(x^6*(a + b*x^4)*(c + d*x^4))".ToEntity().Integrate("x")` | `integral(...)` | 795 characters |

### A linear and a quadratic to one power are integrated as one binomial

**Answers where there were none.** `1/((1 + x)^(1/3) (1 - x + x^2)^(1/3))` was declined. A linear and a quadratic to
one power that is not whole, whose product is `A + B x^3`, are now integrated as `(L^p Q^p/(A + B x^3)^p)` times the
integral over `(A + B x^3)^p`. The quotient is constant on each interval between the roots, so it stays in the answer
as written ([#718](https://github.com/asc-community/AngouriMath/issues/718)).

| Input | Was (2.5.0) | Now |
|---|---|---|
| `"1/((1+x)^(1/3)*(1-x+x^2)^(1/3))".ToEntity().Integrate("x")` | `integral(...)` | 263 characters |
| `"1/((1-x)^(1/3)*(1+x+x^2)^(1/3))".ToEntity().Integrate("x")` | `integral(...)` | 269 characters |
| `"1/((b*k-c*k*x)^(1/3)*(b^2+b*c*x+c^2*x^2)^(1/3))".ToEntity().Integrate("x")` | `integral(...)` | 881 characters |

### A sine or a cosine over a symbolic quadratic or binomial is split over its roots with the coefficients named

**Answers where there were none.** A quadratic or a binomial with a compound coefficient below a sine or a cosine is
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19919,6 +19919,75 @@ Piecewise piecewise when piecewise.Cases.Any() => piecewise.Cases.First().Expres
return Functions.PartialFractions.DerivativeHoldsAtSampledPoints(answer, expr, x) ? answer : null;
}

/// <summary>
/// A linear and a quadratic to one power that is not whole, as one binomial:
/// <c>integral g L^p Q^p = (L^p Q^p/P^p) integral g P^p</c> where <c>P = L Q</c> is
/// <c>A + B x^3</c>, as <c>(1 + x) (1 - x + x^2)</c> is <c>1 + x^3</c>.
/// </summary>
/// <remarks>
/// <c>L^p Q^p/P^p</c> is constant wherever it is continuous, for the reason
/// <see cref="SolveTwoLinearsToOnePowerAsOneQuadratic"/> gives, and the rules for a power
/// of a binomial answer the rest. Rubi's 1.2.1.2 <c>1/((1 + x)^(1/3) (1 - x + x^2)^(1/3))</c>
/// and <c>1/((b e - c e x)^(1/3) (b^2 + b c x + c^2 x^2)^(1/3))</c> were declined.
/// https://github.com/asc-community/AngouriMath/issues/718
/// </remarks>
internal static Entity? SolveALinearAndAQuadraticToOnePowerAsOneBinomial(Entity expr, Entity.Variable x, bool integrateByParts)
{
if (!Integration.AnsweringTheQuestionAsked)
return null;
// Without a leading `1 *`, which is another spelling to the rules that read the result.
static Entity Times(Entity a, Entity b) => a == Number.Integer.One ? b : a * b;
static Entity Over(Entity a, Entity b) => a / b;
int? Degree(Entity polynomial)
{
if (!TreeAnalyzer.TryGetPolynomial(polynomial, x, out var read) || read.Keys.Any(key => key.Sign < 0))
return null;
var present = read.Where(term => !VanishesIdentically(term.Value.Expand())).Select(term => term.Key).ToList();
return present.Count == 0 || present.Max() is not { } top || !top.CanFitInInt32() ? null : top.ToInt32Unchecked();
}
var (numerator, denominator) = Functions.SingleQuotient.Of(expr);
var factors = new List<(Entity Base, Number.Rational Power, bool Below, Entity Factor, int Degree)>();
Entity rest = Number.Integer.One;
foreach (var (side, below) in new[] { (numerator, false), (denominator, true) })
foreach (var factor in Mulf.LinearChildren(side))
{
if (factor is Powf(var @base, Number.Rational power) && power is not Number.Integer
&& Degree(@base) is (1 or 2) and var degree)
factors.Add((@base, power, below, factor, degree));
else
rest = below ? Over(rest, factor) : Times(rest, factor);
}
Number.Rational Signed((Entity Base, Number.Rational Power, bool Below, Entity Factor, int Degree) f) => f.Below ? (Number.Rational)(-f.Power) : f.Power;
for (var i = 0; i < factors.Count; i++)
for (var j = 0; j < factors.Count; j++)
{
var (first, second) = (factors[i], factors[j]);
var power = Signed(first);
if (first.Degree != 1 || second.Degree != 2 || power != Signed(second)
|| !TreeAnalyzer.TryGetPolynomial(first.Base * second.Base, x, out var product))
continue;
var terms = product.Where(term => !VanishesIdentically(term.Value.Expand())).ToDictionary(term => term.Key, term => term.Value);
if (terms.Count != 2 || !terms.ContainsKey(EInteger.Zero) || !terms.ContainsKey(EInteger.FromInt32(3)))
continue;
var binomial = terms[EInteger.Zero].Expand().InnerSimplified
+ terms[EInteger.FromInt32(3)].Expand().InnerSimplified * MathS.Pow(x, Number.Integer.Create(3));
Entity others = rest;
for (var k = 0; k < factors.Count; k++)
if (k != i && k != j)
others = factors[k].Below ? Over(others, factors[k].Factor) : Times(others, factors[k].Factor);
// Spelled as a quotient where the power is negative, as the rules for a power of a
// binomial read it.
var asked = power.IsNegative ? Over(others, MathS.Pow(binomial, (Number.Rational)(-power))) : Times(others, MathS.Pow(binomial, power));
if (Integration.ComputeAsAQuestionOfItsOwn(asked, x, integrateByParts) is not { } joined)
return null;
if (!Functions.PartialFractions.DerivativeHoldsAtSampledPoints(joined, asked, x))
return null;
var constant = MathS.Pow(first.Base, power) * MathS.Pow(second.Base, power) / MathS.Pow(binomial, power);
return constant * joined;
}
return null;
}

/// <summary>
/// A power of a linear that is not whole, times a linear, over a whole power of a quadratic,
/// with the power of the quadratic lowered one at a time:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -669,6 +669,9 @@ private static Entity WithItsPiecewiseSumsCombined(Entity expr)
// linear is written once: `(2 + 3x)^4/((1 - 2x)^(5/2) (3 + 5x)^(5/2))` took forty-five
// seconds there, and over `(3 - x - 10x^2)^(5/2)` it takes half of one.
if ((answer = IndefiniteIntegralSolver.SolveTwoLinearsToOnePowerAsOneQuadratic(expr, x, integrateByParts)) is { }) return answer;
// A linear and a quadratic to one power as one binomial, `(1 + x) (1 - x + x^2)` as
// `1 + x^3`, which the rules for a power of a binomial answer.
if ((answer = IndefiniteIntegralSolver.SolveALinearAndAQuadraticToOnePowerAsOneBinomial(expr, x, integrateByParts)) is { }) return answer;
if ((answer = IndefiniteIntegralSolver.SolveByWritingEachLinearOnce(expr, x, integrateByParts)) is { }) return answer;
// A rational function with numbers for coefficients over a denominator past the twelfth
// degree, by Hermite's reduction before anything searches: nothing below answered one.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
//
// Copyright (c) 2019-2026 Angouri.
// AngouriMath is licensed under MIT.
// Details: https://github.com/asc-community/AngouriMath/blob/master/LICENSE.md.
// Website: https://am.angouri.org.
//

using System;
using AngouriMath.Extensions;
using Xunit;

namespace AngouriMath.Tests.Calculus
{
/// <summary>
/// A linear and a quadratic to one power whose product is a binomial in <c>x^3</c>,
/// integrated over the binomial: <c>(1 + x) (1 - x + x^2)</c> is <c>1 + x^3</c>. Rubi's
/// 1.2.1.2 rows, sampled on both sides of the linear's root, where the cube roots are complex.
/// <a href="https://github.com/asc-community/AngouriMath/issues/718">#718</a>
/// </summary>
[Trait("Area", "Calculus")]
public sealed class ALinearAndAQuadraticToOnePowerIntegralTest
{
private static void DifferentiatesBack(string integrand, params (string, double)[] pins)
{
var integral = integrand.ToEntity().Integrate("x");
Assert.DoesNotContain("integral(", integral.Stringize());

var pinned = integral.Substitute("C", 0);
Entity original = integrand.ToEntity();
foreach (var (name, value) in pins)
{
pinned = pinned.Substitute(name, value);
original = original.Substitute(name, value);
}
var derivative = pinned.Differentiate("x");
foreach (var at in new[] { -2.3, -0.7, 0.4, 1.9 })
{
var got = derivative.Substitute("x", at).EvalNumerical();
var want = original.Substitute("x", at).EvalNumerical();
var difference = Math.Abs((double)(got - want).RealPart) + Math.Abs((double)(got - want).ImaginaryPart);
var scale = Math.Max(1.0, Math.Abs((double)want.RealPart) + Math.Abs((double)want.ImaginaryPart));
Assert.True(difference / scale < 1e-8,
$"d/dx of the antiderivative of {integrand} is {got} at x = {at}, where the integrand is {want}");
}
}

[Theory]
[InlineData("1/((1+x)^(1/3)*(1-x+x^2)^(1/3))")]
[InlineData("1/((1-x)^(1/3)*(1+x+x^2)^(1/3))")]
public void WithNumbersIsAnsweredAndDifferentiatesBack(string integrand)
=> DifferentiatesBack(integrand);

[Fact]
public void WithSymbolsIsAnsweredAndDifferentiatesBack()
{
const string integrand = "1/((b*k-c*k*x)^(1/3)*(b^2+b*c*x+c^2*x^2)^(1/3))";
DifferentiatesBack(integrand, ("b", 2), ("c", 3), ("k", 0.7));
DifferentiatesBack(integrand, ("b", -2), ("c", 3), ("k", -1.5));
}
}
}
Loading