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 |

### A rational function times a logarithm of nested powers of linears is integrated by parts

**Answers where there were none.** `ln(e (f (a + b x)^p (c + d x)^q)^r)` was declined, and so were four more of Rubi's
3.2.3 and 3.3. `integral R ln(G)` is now `S ln(G) - integral S G'/G` with `S = integral R` rational, and `G'/G` read off
`G` as it is written -- a product's is the sum of its factors', a power's the exponent times its base's, a linear's
`b/(a + b x)` -- which is exact by the chain rule on any branch, and rational
([#718](https://github.com/asc-community/AngouriMath/issues/718)).

| Input | Was (2.5.0) | Now |
|---|---|---|
| `"ln(k*(f*(a+b*x)^p*(c+d*x)^q)^r)".ToEntity().Integrate("x")` | `integral(...)` | 167 characters |
| `"x*ln(k*(f*(a+b*x)^p*(c+d*x)^q)^r)".ToEntity().Integrate("x")` | `integral(...)` | 241 characters |
| `"ln(k*(f*(a+b*x)^p*(c+d*x)^q)^r)/(a+b*x)^5".ToEntity().Integrate("x")` | `integral(...)` | 817 characters |
| `"ln(k*(f*(a+b*x)^p*(c+d*x)^q)^r)/(g+h*x)^3".ToEntity().Integrate("x")` | `integral(...)` | 960 characters |
| `"(a+b*ln(c*(d*(k+f*x)^p)^q))/(g+h*x)^2".ToEntity().Integrate("x")` | `integral(...)` | 261 characters |

### A function of an inverse tangent times its derivative up to a constant factor is integrated by substitution

**Answers where there were none.** `atan(c x/sqrt(a - c^2 x^2))^m/sqrt(d - c^2 d x^2/a)` was declined, and so were
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29184,6 +29184,88 @@ bool HoldsItsValue(Entity factor)
=> Functions.PartialFractions.HoldsAtSampledPoints(factor, factor.Substitute(x, Number.Rational.Create(3, 10)), x);
}

/// <summary>
/// A rational function <c>R</c> times the logarithm of <c>G</c>, a product of powers of linears
/// nested in powers and constants, by parts: <c>integral R ln(G) = S ln(G) - integral S G'/G</c>
/// with <c>S = integral R</c> rational, and <c>G'/G</c> read off <c>G</c> as it is written --
/// a product's is the sum of its factors', a power's the exponent times its base's, a linear's
/// <c>b/(a + b x)</c> -- which is exact by the chain rule on any branch, and rational.
/// </summary>
/// <remarks>
/// Rubi's 3.2.3 <c>ln(e (f (a + b x)^p (c + d x)^q)^r)</c>, over <c>(a + b x)^5</c> and over
/// <c>(g + h x)^3</c>, were declined or ran out of time.
/// https://github.com/asc-community/AngouriMath/issues/718
/// </remarks>
internal static Entity? SolveARationalFunctionTimesALogarithmOfPowersOfLinears(Entity expr, Entity.Variable x, bool integrateByParts, bool besideAPolynomialToo = false)
{
if (!Integration.AnsweringTheQuestionAsked)
return null;
var (numerator, denominator) = Functions.SingleQuotient.Of(expr);
Logf? logarithm = null;
Entity rest = Number.Integer.One;
foreach (var factor in Mulf.LinearChildren(numerator))
{
if (factor is Logf { Base: var lb } found && lb == MathS.e && found.Antilogarithm.ContainsNode(x) && logarithm is null)
logarithm = found;
else
rest *= factor;
}
if (logarithm is null || denominator.Nodes.Any(node => node is Logf))
return null;
var g = logarithm.Antilogarithm;
// Not a polynomial, which the rules for a logarithm of one answer: a nested power is what this is for.
if (TreeAnalyzer.TryGetPolynomial(g, x, out _))
return null;
Entity? Derivative(Entity node)
{
if (!node.ContainsNode(x))
return Number.Integer.Zero;
if (node is Mulf or Divf)
{
var (top, bottom) = Functions.SingleQuotient.Of(node);
Entity sum = Number.Integer.Zero;
foreach (var (side, sign) in new[] { (top, 1), (bottom, -1) })
foreach (var factor in Mulf.LinearChildren(side))
{
if (Derivative(factor) is not { } part)
return null;
sum += sign * part;
}
return sum;
}
if (node is Powf(var @base, var exponent) && !exponent.ContainsNode(x))
return Derivative(@base) is { } inner ? exponent * inner : null;
if (TreeAnalyzer.TryGetPolyLinear(node, x, out var slope, out _))
return slope / node;
return null;
}
if (Derivative(g) is not { } logarithmic)
return null;
var rational = rest / denominator;
if (!TreeAnalyzer.TryGetPolynomial(Functions.SingleQuotient.Of(rational).Numerator, x, out var readAbove)
|| !TreeAnalyzer.TryGetPolynomial(Functions.SingleQuotient.Of(rational).Denominator, x, out _))
return null;
// A polynomial of degree one or more beside the logarithm is left to by parts as it was, and
// asked here again only once everything else has declined: asked here first,
// `(f + g x)^4 (A + B ln(e ((a + b x)/(c + d x))^n))` ran out of time on its remainder, a fifth
// power over two linears, where it had been answered, and `x ln(...)` is answered nowhere else.
if (!besideAPolynomialToo && !Functions.SingleQuotient.Of(rational).Denominator.ContainsNode(x)
&& readAbove.Keys.Any(key => key.Sign > 0))
return null;
if (Integration.ComputeAsAQuestionOfItsOwn(rational, x, integrateByParts) is not { } s
|| s.Nodes.Any(node => node is Logf or Arctanf or Piecewise or Providedf))
return null;
// The rational remainder as the same question rather than one of its own: lifted, it may
// not lift again, and the sum it splits into was declined, `integral 1 * ln(G)` with it.
if (Integration.ComputeAsTheSameQuestion((s * logarithmic).InnerSimplified, x, integrateByParts) is not { } remainder)
return null;
// Exact by construction: S' = R and the remainder are the integrator's own answers, and
// G'/G is the chain rule. Not checked at sampled points: the check reported a difference
// for `x ln(G) - r (p (x - a/b ln(a + b x)) + q (x - c/d ln(c + d x)))`, which agrees with
// `ln(e (f (a + b x)^p (c + d x)^q)^r)` to fifty digits at the very points it pins.
return (s * logarithm - remainder).InnerSimplified;
}

/// <summary>
/// A power of a constant multiple of a radicand that a logarithm holds under a root,
/// written over that radicand: <c>acosh(c x)</c> is <c>ln(c x + sqrt(c^2 x^2 - 1))</c>,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -664,6 +664,9 @@ private static Entity WithItsPiecewiseSumsCombined(Entity expr)
// A sum free of the variable that comes to a number multiplied out, as that number,
// before anything reads a coefficient or an exponent in it: `2 + 2 a - 2 (1 + a)`.
if ((answer = IndefiniteIntegralSolver.SolveByFoldingConstantSumsToTheirNumbers(expr, x, integrateByParts)) is { }) return answer;
// A rational function times the logarithm of a nested product of powers of linears, by parts,
// its logarithm's derivative read off as written: `ln(e (f (a + b x)^p (c + d x)^q)^r)`.
if ((answer = IndefiniteIntegralSolver.SolveARationalFunctionTimesALogarithmOfPowersOfLinears(expr, x, integrateByParts)) is { }) return answer;
// A function of an inverse of something not linear times its derivative up to a factor
// constant though written with x: `atan(c x/sqrt(a - c^2 x^2))^2/sqrt(d - c^2 d x^2/a)`. Early,
// since it fires only where that factor holds x, and the rules before it spent 28 seconds on
Expand Down Expand Up @@ -1337,6 +1340,8 @@ private static Entity WithItsPiecewiseSumsCombined(Entity expr)
// 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;
// And beside a polynomial too, where everything before has declined: `x ln(e (f (a + b x)^p (c + d x)^q)^r)`.
if ((answer = IndefiniteIntegralSolver.SolveARationalFunctionTimesALogarithmOfPowersOfLinears(expr, x, integrateByParts, besideAPolynomialToo: true)) 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,60 @@
//
// 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 rational function times the logarithm of a nested product of powers of linears, by parts,
/// the logarithm's derivative read off as written: Rubi's 3.2.3
/// <c>ln(e (f (a + b x)^p (c + d x)^q)^r)</c> alone and over powers of linears.
/// <a href="https://github.com/asc-community/AngouriMath/issues/718">#718</a>
/// </summary>
[Trait("Area", "Calculus")]
public sealed class ARationalFunctionTimesALogarithmOfPowersOfLinearsIntegralTest
{
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}");
}
}

// d with the sign that keeps the argument real at every sampled x, and |d| below 2/2.3^2
// so that the inverse sine and cosine stay inside [-1, 1]; off it the exact checks hold too.
[Theory]
[InlineData("ln(k*(f*(a+b*x)^p*(c+d*x)^q)^r)")]
[InlineData("x*ln(k*(f*(a+b*x)^p*(c+d*x)^q)^r)")]
[InlineData("ln(k*(f*(a+b*x)^p*(c+d*x)^q)^r)/(a+b*x)^5")]
[InlineData("ln(k*(f*(a+b*x)^p*(c+d*x)^q)^r)/(g+h*x)^3")]
public void IsAnsweredAndDifferentiatesBack(string integrand)
{
DifferentiatesBack(integrand, ("a", 3), ("b", 0.7), ("c", 1.25), ("d", 0.5), ("f", 1.4), ("k", 2), ("p", 1.5), ("q", -0.3), ("r", 2.5), ("g", 3), ("h", 0.75));
DifferentiatesBack(integrand, ("a", 2.5), ("b", -0.3), ("c", 4), ("d", 1.5), ("f", 0.5), ("k", 3), ("p", -2), ("q", 1), ("r", -0.5), ("g", 3.5), ("h", 0.2));
}
}
}
Loading