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
16 changes: 16 additions & 0 deletions BREAKING-CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -396,6 +396,22 @@ 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 |

### Powers of multiples of powers of one linear are gathered into one power of it

**Answers where there were none.** `1/(sqrt(-2 - b x) sqrt(2 + b x))` was declined, and so were four more of Rubi's
1.1.1.2, 1.1.1.3, 1.2.2.2 and 1.3.1. A power `B^p` that is not whole, of a polynomial `B` that is a constant times
`L^k` for one linear `L`, is now written `(B^p/L^(k p)) L^(k p)`, and the powers of one linear are integrated as one.
The quotient is constant on each interval where it is continuous, so it stays in the answer as written
([#718](https://github.com/asc-community/AngouriMath/issues/718)).

| Input | Was (2.5.0) | Now |
|---|---|---|
| `"1/(sqrt(-2-b*x)*sqrt(2+b*x))".ToEntity().Integrate("x")` | `integral(...)` | 73 characters |
| `"sqrt(a+b*x)/(x*sqrt(-a-b*x))".ToEntity().Integrate("x")` | `integral(...)` | 56 characters |
| `"(a+b*x)^n/(x*(-a-b*x)^n)".ToEntity().Integrate("x")` | `integral(...)` | 52 characters |
| `"(a^3+3*a^2*b*x+3*a*b^2*x^2+b^3*x^3)^p".ToEntity().Integrate("x")` | `integral(...)` | 187 characters |
| `"1/sqrt(2+2*a-2*(1+a)+c*x^4)".ToEntity().Integrate("x")` | `integral(...)` | 119 characters |

### A linear below the cube root of a quadratic is integrated in closed form

**Answers where there were none.** `1/((d + e x) (d^2 + 3 e^2 x^2)^(1/3))` was declined, and so were six more of Rubi's
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20065,6 +20065,96 @@ Piecewise piecewise when piecewise.Cases.Any() => piecewise.Cases.First().Expres
return Functions.PartialFractions.DerivativeHoldsAtSampledPoints(answer, expr, x) ? answer : null;
}

/// <summary>
/// Powers that are not whole of polynomials that are constant multiples of powers of one
/// linear, gathered into one power of it: <c>B^p = (B^p/L^(k p)) L^(k p)</c> for
/// <c>B = lambda L^k</c>, so <c>sqrt(-2 - b x) sqrt(2 + b x)</c> is a constant times
/// <c>2 + b x</c> and <c>(a^3 + 3 a^2 b x + 3 a b^2 x^2 + b^3 x^3)^p</c> one times
/// <c>(a + b x)^(3 p)</c>.
/// </summary>
/// <remarks>
/// <c>B^p/L^(k p)</c> is constant wherever it is continuous: its logarithmic derivative is
/// <c>p B'/B - k p L'/L</c>, which is zero for <c>B = lambda L^k</c>. It is kept in the
/// answer as written. Rubi's 1.1.1.2 <c>1/(sqrt(-2 - b x) sqrt(2 + b x))</c>, 1.1.1.3
/// <c>(a + b x)^n/(x (-a - b x)^n)</c> and 1.3.1 <c>(a^3 + 3 a^2 b x + 3 a b^2 x^2 + b^3 x^3)^p</c>
/// were declined.
/// https://github.com/asc-community/AngouriMath/issues/718
/// </remarks>
internal static Entity? SolvePowersOfOneLinearAsOnePower(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 bool IsWhole(Entity power) => power is Number.Integer;
// The root r and degree k of a polynomial that is c (x - r)^k, or null.
(Entity Root, int Degree)? AsAPowerOfALinear(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())).ToList();
if (present.Count == 0 || present.Max(term => term.Key) is not { } top || top.Sign <= 0 || !top.CanFitInInt32())
return null;
var k = top.ToInt32Unchecked();
var lead = read[top];
var next = read.TryGetValue(top - EInteger.One, out var n0) ? n0 : Number.Integer.Zero;
var root = (-next / (k * lead)).InnerSimplified;
if (k > 1 && !VanishesIdentically((polynomial - lead * MathS.Pow(x - root, Number.Integer.Create(k))).Expand()))
return null;
return (root, k);
}
var (numerator, denominator) = Functions.SingleQuotient.Of(expr);
var members = new List<(Entity Base, Entity Power, int Degree, Entity Root)>();
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, var power) && !IsWhole(power) && !power.ContainsNode(x)
&& AsAPowerOfALinear(@base) is var (root, degree))
members.Add((@base, below ? -power : power, degree, root));
else
rest = below ? rest / factor : Times(rest, factor);
}
// Gathered by root; worth doing where a group has two members or a power of a linear.
var groups = new List<List<(Entity Base, Entity Power, int Degree, Entity Root)>>();
foreach (var member in members)
{
if (groups.FirstOrDefault(group => VanishesIdentically((group[0].Root - member.Root).Expand().InnerSimplified)) is { } group)
group.Add(member);
else
groups.Add(new() { member });
}
if (!groups.Any(group => group.Count > 1 || group.Any(member => member.Degree > 1)))
return null;
Entity constant = Number.Integer.One;
Entity gathered = rest;
foreach (var group in groups)
{
var linear = group.FirstOrDefault(member => member.Degree == 1).Base ?? x - group[0].Root;
Entity total = Number.Integer.Zero;
foreach (var member in group)
{
var exponent = member.Degree * member.Power;
// The linear itself is its own power, with nothing to carry in front.
if (member.Base != linear)
constant = Times(constant, MathS.Pow(member.Base, member.Power) / MathS.Pow(linear, exponent));
total += exponent;
}
total = total.InnerSimplified;
if (VanishesIdentically(total.Expand()))
continue;
// Spelled as a quotient where the power is negative, as the rules read it.
gathered = total is Number.Real { IsNegative: true } || (total is Mulf(Number.Real { IsNegative: true }, _))
? gathered / MathS.Pow(linear, (-total).InnerSimplified)
: Times(gathered, MathS.Pow(linear, total));
}
if (Integration.ComputeAsAQuestionOfItsOwn(gathered, x, integrateByParts) is not { } joined)
return null;
if (!Functions.PartialFractions.DerivativeHoldsAtSampledPoints(joined, gathered, x))
return null;
return constant * joined;
}

/// <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 @@ -1319,6 +1319,11 @@ private static Entity WithItsPiecewiseSumsCombined(Entity expr)
// function of x and of sin(x) and cos(x) together. `x^2/(x cos(x) - sin(x))^2` is
// `((x sin(x) + cos(x))/(x cos(x) - sin(x)))'`.
if ((answer = IndefiniteIntegralSolver.SolveByTrigonometricTowerAnsatz(expr, x)) is { }) return answer;
// Powers of polynomials that are multiples of powers of one linear, as one power of it,
// the quotient constant in front. Late, so that a rule answering the powers as written
// goes first: asked before the rules for two linears, it wrote `((a + b x)^3)^(1/3)` in
// 111 characters where they write 25.
if ((answer = IndefiniteIntegralSolver.SolvePowersOfOneLinearAsOnePower(expr, x, integrateByParts)) is { }) return answer;
// Last of the rewrites, because it is the only one that fires on an integrand nothing
// is wrong with -- it clears a parameter rather than a shape -- so everything that
// can answer the problem as written gets to try first.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
//
// 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>
/// Powers that are not whole of polynomials that are constant multiples of powers of one
/// linear, gathered into one power of it with the constant quotient in front: Rubi's 1.1.1.2
/// <c>1/(sqrt(-2 - b x) sqrt(2 + b x))</c>, 1.1.1.3 <c>(a + b x)^n/(x (-a - b x)^n)</c> and 1.3.1
/// <c>(a^3 + 3 a^2 b x + 3 a b^2 x^2 + b^3 x^3)^p</c>. Sampled on both sides of the linear's root.
/// <a href="https://github.com/asc-community/AngouriMath/issues/718">#718</a>
/// </summary>
[Trait("Area", "Calculus")]
public sealed class PowersOfOneLinearAsOnePowerIntegralTest
{
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/(sqrt(-2-b*x)*sqrt(2+b*x))")]
[InlineData("sqrt(a+b*x)/(x*sqrt(-a-b*x))")]
[InlineData("(a+b*x)^n/(x*(-a-b*x)^n)")]
[InlineData("(a^3+3*a^2*b*x+3*a*b^2*x^2+b^3*x^3)^n")]
public void WithSymbolsIsAnsweredAndDifferentiatesBack(string integrand)
{
DifferentiatesBack(integrand, ("a", 0.6), ("b", 1.5), ("n", 1.0 / 3));
DifferentiatesBack(integrand, ("a", -0.75), ("b", -0.7), ("n", 2.3));
}

[Theory]
[InlineData("((a+b*x)^3)^(1/3)", 40)]
[InlineData("x*(-a-b*x)^(3/2)/(a+b*x)^(3/2)", 80)]
public void LeavesWhatTheRulesBeforeItAnswer(string integrand, int length)
{
// Asked before them, the rule wrote these in 111 and 106 characters, where they write
// 25 and 61.
var integral = integrand.ToEntity().Integrate("x").Stringize();
Assert.DoesNotContain("integral(", integral);
Assert.True(integral.Length < length, integral);
}
}
}
Loading