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
17 changes: 17 additions & 0 deletions BREAKING-CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -3145,6 +3145,23 @@ Both columns measured on a build, `v2.5.0` against this change.
| `"x*sin(ln(x))/ln(x)".Integrate("x")` | `integral(x * sin(ln(x)) / ln(x), x)` | `(-1/2 * i) * Ei((2 + i) * ln(x)) + 1/2 * i * Ei((2 - i) * ln(x)) + C` |
| `"cos(a+b*ln(c*x^n))^2".Integrate("x")` | `integral(cos(a + b * ln(c * x ^ n)) ^ 2, x)` | an antiderivative in `sin` and `cos` of `2 (a + b ln(c x^n))`, beside `x (c x^n)^(-1/n)` |

### Exponentials below the bar beside a power of a linear are integrated to the exponential integral

**Answers where there were none.** `1/(x e^(2x))` was declined where `e^(-2x)/x` was answered, and so
were `1/((c + d x)(a + a tanh(e + f x)))` and its powers, Rubi's 6.3.1, and the same with `coth`, 6.4.1:
the rules for an exponential over a linear read it above the bar. Written in the exponential every
other one is a whole power of, exponentials whose denominator is then a power of that one alone are a
sum of its powers -- `1/(1 + tanh(z))` is `(1 + e^(-2z))/2` -- and each term over the linear is an
exponential integral. Where anything else stays below the bar, `1/(x (1 + e^x))`, the integral is not
of this kind and is declined as before ([#718](https://github.com/asc-community/AngouriMath/issues/718)).

| Input | Was (2.5.0) | Now |
|---|---|---|
| `"1/(x*exp(2*x))".ToEntity().Integrate("x")` | `integral(...)` | `Ei(-2 x)` |
| `"1/(x*(1 + tanh(x)))".ToEntity().Integrate("x")` | `integral(...)` | `ln(x)/2 + Ei(-2 x)/2` |
| `"1/((c + d*x)*(a + a*tanh(e + f*x)))".ToEntity().Integrate("x")` | `integral(...)` | a logarithm and an exponential integral |
| `"1/((c + d*x)^2*(a + a*coth(e + f*x))^3)".ToEntity().Integrate("x")` | `integral(...)` | powers of `c + d x` and exponential integrals |

### An exponential or a hyperbolic function over several linears is split into partial fractions over them

`e^x/(x (x + 1))` was left unevaluated, where `e^x/x` and `e^x/(x + 1)` were each answered with
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5309,6 +5309,141 @@ void AddALinear(Entity linear, int power)
return Functions.PartialFractions.Bare((constant * sum).InnerSimplified);
}

/// <summary>
/// Exponentials of one linear below the bar, beside polynomials of <c>x</c> with one below
/// the bar: in <c>w</c>, the exponential every other is a whole power of, the exponentials
/// are a rational function of <c>w</c>, and where that is a sum of whole powers of <c>w</c>
/// -- its denominator in lowest terms a power of <c>w</c> alone -- each term is an
/// exponential of a linear over the polynomials, which
/// <see cref="SolveAnExponentialOfALinearOverAPowerOfALinear"/> answers onto the exponential
/// integral. <c>1 + tanh(z)</c> is <c>2 e^(2z)/(e^(2z) + 1)</c>, so
/// <c>1/((c + d x)(a + a tanh(e + f x)))</c> is <c>(1 + e^(-2(e + f x)))/(2 a (c + d x))</c>.
/// Rubi's 6.3.1 and 6.4.1, <c>(c + d x)^m (a + a tanh(e + f x))^n</c> and the same with
/// <c>coth</c>, for a negative <c>m</c>.
/// https://github.com/asc-community/AngouriMath/issues/718
/// </summary>
/// <remarks>
/// The rules for an exponential over a linear read it above the bar: <c>e^(-2x)/x</c> was
/// answered and <c>1/(x e^(2x))</c> declined. Where the denominator keeps a factor other
/// than <c>w</c> after cancelling, <c>1/(x (1 + e^x))</c>, the integral is neither elementary
/// nor an exponential integral, and this declines.
/// </remarks>
internal static Entity? SolveAnExponentialBelowTheBarBesideAPowerOfALinear(Entity expr, Entity.Variable x)
{
bool IsAnExponential(Entity node) => node is Powf(var b, var p) && !b.ContainsNode(x) && p.ContainsNode(x);
if (!expr.Nodes.Any(node => node is Divf(_, var below) && below.Nodes.Any(IsAnExponential)
|| node is Powf(var raised, Number.Integer { IsNegative: true }) && raised.Nodes.Any(IsAnExponential)))
return null;
Entity constant = Number.Integer.One;
Entity aboveX = Number.Integer.One;
Entity belowX = Number.Integer.One;
Entity exponentials = Number.Integer.One;
foreach (var (factor, underneath) in FactorsOfTheIntegrand(expr))
{
if (!factor.ContainsNode(x))
{
constant = underneath ? constant / factor : constant * factor;
continue;
}
if (factor.Nodes.Any(IsAnExponential))
{
exponentials = underneath ? exponentials / factor : exponentials * factor;
continue;
}
// A polynomial, or a whole power of one, above or below the bar.
var (@base, power) = factor is Powf(var raised, Number.Integer whole) && whole.EInteger.CanFitInInt32() && !whole.EInteger.IsZero
? (raised, whole.EInteger.ToInt32Checked()) : (factor, 1);
if (underneath)
power = -power;
if (!TreeAnalyzer.TryGetPolynomial(@base, x, out var read) || read.Keys.Any(degree => degree.Sign < 0) || read.Values.Any(coefficient => coefficient.ContainsNode(x)))
return null;
if (power > 0)
aboveX = aboveX == Number.Integer.One ? MathS.Pow(@base, power) : aboveX * MathS.Pow(@base, power);
else
belowX = belowX == Number.Integer.One ? MathS.Pow(@base, -power) : belowX * MathS.Pow(@base, -power);
}
// Nothing of x below the bar: a polynomial beside the exponentials is the rules' by parts.
if (belowX == Number.Integer.One || exponentials.Complexity > 160)
return null;

// One base, and every exponent a rational multiple of the first, its offset included,
// so that w is a power of the base at that exponent and no constant is left beside it.
Entity? commonBase = null;
Entity? unit = null, unitSlope = null, unitOffset = null;
var multiples = new Dictionary<Entity, ERational>();
foreach (var node in exponentials.Nodes)
{
if (!IsAnExponential(node) || multiples.ContainsKey(node) || node is not Powf(var @base, var exponent))
continue;
if (!TreeAnalyzer.TryGetPolyLinear(exponent, x, out var slope, out var offset) || TreeAnalyzer.IsZero(slope))
return null;
if (commonBase is null)
{
(commonBase, unit, unitSlope, unitOffset) = (@base, exponent, slope, offset);
multiples[node] = ERational.One;
continue;
}
if (@base != commonBase
|| Functions.PartialFractions.Bare((slope / unitSlope!).Simplify()).Evaled is not Number.Rational ratio || ratio.ERational.IsZero
|| !IsTheZeroPolynomial((offset - ratio * unitOffset!).InnerSimplified))
return null;
multiples[node] = ratio.ERational;
}
if (commonBase is null || unit is null)
return null;
if (commonBase != MathS.e && (commonBase.Evaled is Number.Complex and not Number.Real || commonBase.Evaled is Number.Real { IsNegative: true } || TreeAnalyzer.IsZero(commonBase)))
return null;
var numerators = EInteger.Zero;
var denominators = EInteger.One;
foreach (var multiple in multiples.Values)
{
numerators = numerators.Gcd(multiple.Numerator.Abs());
denominators = denominators.Multiply(multiple.Denominator).Divide(denominators.Gcd(multiple.Denominator));
}
var step = ERational.Create(numerators, denominators);
if (multiples.Values.Any(multiple => multiple.Divide(step).ToLowestTerms().Numerator.Abs().CompareTo(EInteger.FromInt32(12)) > 0))
return null;
var w = Variable.CreateUnique(expr, "w_exp");
var inW = exponentials.Replace(node =>
multiples.TryGetValue(node, out var multiple) ? MathS.Pow(w, Number.Integer.Create(multiple.Divide(step).ToLowestTerms().Numerator)) : node);
if (inW.ContainsNode(x))
return null;

// A sum of whole powers of w: its denominator in lowest terms a power of w alone.
var (top, bottom) = Functions.SingleQuotient.Of(inW);
if (!TreeAnalyzer.TryGetPolynomial(bottom, w, out var below) || below.Count != 1)
{
if (!Functions.PolynomialGcd.TryCancel(top, bottom, out var cancelled, maxComplexity: 1024))
return null;
(top, bottom) = Functions.SingleQuotient.Of(cancelled is Providedf(var inner, _) ? inner : cancelled);
if (!TreeAnalyzer.TryGetPolynomial(bottom, w, out below) || below.Count != 1)
return null;
}
var lowest = below.Keys.Single();
var leading = below[lowest];
if (!TreeAnalyzer.TryGetPolynomial(top, w, out var above) || above.Count > 16 || TreeAnalyzer.IsZero(leading))
return null;

Entity sum = Number.Integer.Zero;
foreach (var term in above)
{
if (TreeAnalyzer.IsZero(term.Value))
continue;
var power = term.Key.Subtract(lowest);
var exponential = power.IsZero ? Number.Integer.One
: MathS.Pow(commonBase, (Number.Rational.Create(step.Multiply(ERational.FromEInteger(power))) * unit).InnerSimplified);
var question = constant * term.Value / leading * aboveX * exponential / belowX;
var integral = power.IsZero
? Integration.ComputeIndefiniteIntegral(question, x, false)
: SolveAnExponentialOfALinearOverAPowerOfALinear(question, x) ?? SolveAnExponentialOverSeveralLinears(question, x)
?? Integration.ComputeIndefiniteIntegral(question, x, false);
if (integral is null || integral.Nodes.Any(node => node is Integralf))
return null;
sum += integral;
}
return sum == Number.Integer.Zero ? null : Functions.PartialFractions.Bare(sum.InnerSimplified);
}

/// <summary>
/// An exponential of a linear times sines and cosines of linears, and a polynomial, over
/// linears: each sine and cosine is written as exponentials, <c>sin(c x) = (e^(i c x) - e^(-i c x))/(2i)</c>,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -656,6 +656,9 @@ private static Entity Normalized(Entity expr, Entity.Variable x) =>
// And either over several linears, split into partial fractions over them first, as the
// trigonometric rule below splits.
if ((answer = IndefiniteIntegralSolver.SolveAnExponentialOverSeveralLinears(expr, x)) is { }) return answer;
// And exponentials below the bar, where they are whole powers of one exponential and
// its powers alone are left below: `1/((c + d x)(a + a tanh(e + f x)))`.
if ((answer = IndefiniteIntegralSolver.SolveAnExponentialBelowTheBarBesideAPowerOfALinear(expr, x)) is { }) return answer;
// And with sines and cosines beside the exponential, written as exponentials: each term
// is then the exponential's, with a complex rate.
if ((answer = IndefiniteIntegralSolver.SolveAnExponentialTimesATrigonometricOverLinears(expr, x)) is { }) return answer;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
//
// 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>
/// Exponentials of one linear below the bar, beside a power of a linear: written in the
/// exponential the others are whole powers of, they are a sum of whole powers of it, and
/// each term over the linear is an exponential integral. <c>1/(x e^(2x))</c> was declined
/// where <c>e^(-2x)/x</c> was answered, and so was <c>1/((c + d x)(a + a tanh(k + f x)))</c>,
/// Rubi's 6.3.1, which is <c>(1 + e^(-2(k + f x)))/(2 a (c + d x))</c>.
/// <a href="https://github.com/asc-community/AngouriMath/issues/718">#718</a>
/// </summary>
[Trait("Area", "Calculus")]
public sealed class ExponentialBelowTheBarIntegralTest
{
[Theory]
[InlineData("1/(x*exp(2*x))")]
[InlineData("(exp(2*x) + 1)/(2*x*exp(2*x))")]
[InlineData("1/(x*(1 + tanh(x)))")]
[InlineData("1/((c + d*x)*(a + a*tanh(k + f*x)))")]
[InlineData("1/((c + d*x)^2*(a + a*tanh(k + f*x))^3)")]
[InlineData("1/((c + d*x)*(a + a*coth(k + f*x))^2)")]
[InlineData("1/((c + d*x)*(a - a*tanh(e + f*x)))")]
[InlineData("1/(x*(x + 1)*exp(x))")]
public void ExpandedOverTheLinear(string integrand)
{
var integral = integrand.ToEntity().Integrate("x");
Assert.DoesNotContain("integral(", integral.Stringize());
Assert.DoesNotContain("NaN", integral.Stringize());
Entity Pinned(Entity e) => e.Substitute("a", 2.3).Substitute("c", 1.3).Substitute("d", 1.7).Substitute("f", 0.9).Substitute("k", 0.4);
var derivative = Pinned(integral.Substitute("C", 0)).Differentiate("x");
var original = Pinned(integrand.ToEntity());
var compared = 0;
foreach (var at in new[] { -1.7, -0.9, 0.3, 0.8, 1.6, 2.9 })
{
var want = original.Substitute("x", at).EvalNumerical();
if (want.IsNaN || Math.Abs((double)want.ImaginaryPart) > 1e-12)
continue;
compared++;
var got = derivative.Substitute("x", at).EvalNumerical();
Assert.True(Math.Abs((double)(got - want).RealPart) + Math.Abs((double)(got - want).ImaginaryPart)
< 1e-9 * Math.Max(1, Math.Abs((double)want.RealPart)),
$"d/dx of the antiderivative of {integrand} is {got} at x = {at}, where the integrand is {want}");
}
Assert.True(compared >= 5, $"only {compared} points could be compared for {integrand}");
}
}
}
Loading