From 35386d5ca903ef8986b83c5680c4eb439806b54e Mon Sep 17 00:00:00 2001 From: Rafael Vuijk Date: Wed, 21 Oct 2026 23:22:31 +0000 Subject: [PATCH] Read the derivative of a sum of exponentials over another off the integrand over the other's square Rubi's 6.7.1 x (b - a sinh(x))/(a + b sinh(x))^2 ran past the budget: the split took the bracket's terms apart, the substitution in e^x answered each over the square of a quadratic with a piecewise on its discriminant, and parts beside x integrated that piecewise again. For a sum D of exponentials of x, the integrand over D^2 is now read as (N/D)', N a constant plus a constant times each exponential in D. The exponentials' span is closed under the derivative, so N' D - N D' is linear in N's constants, and with each exponential a whole power of the first, w, the identity read at six values of w is a linear system. N and N + c D share a derivative over D; the solution with no constant is tried first, which is -cosh(x)/(a + b sinh(x)). Beside x, by parts, the integral of N/D is written as a constant times ln(D) where N is a constant times D', and asked otherwise. The trigonometric twin is SolveAsTheDerivativeOfASineRaisedFromNothingBesidePowersOfTheCosine's; there are no hyperbolic nodes, since sinh is parsed into exponentials. Part of #233. Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_012sonx8iAspMiwRwokT1Ura --- BREAKING-CHANGES.md | 18 ++ .../Integration/IndefiniteIntegralSolver.cs | 156 ++++++++++++++++++ .../Integration/Integration.Definition.cs | 5 + ...tientOverASumOfExponentialsIntegralTest.cs | 51 ++++++ 4 files changed, 230 insertions(+) create mode 100644 Sources/Tests/UnitTests/Calculus/TheDerivativeOfAQuotientOverASumOfExponentialsIntegralTest.cs diff --git a/BREAKING-CHANGES.md b/BREAKING-CHANGES.md index 76e155998..4ca552def 100644 --- a/BREAKING-CHANGES.md +++ b/BREAKING-CHANGES.md @@ -541,6 +541,24 @@ tanh keep the answers the exponential substitution gives them ([#233](https://gi | `"sin(tanh(a+b*x))".ToEntity().Integrate("x")` | `integral(...)` | `Si` and `Ci` of `1 - tanh(a + b x)` and `1 + tanh(a + b x)` | | `"tanh(x)/sqrt(a+b*tanh(x)^2+c*tanh(x)^4)".ToEntity().Integrate("x")` | `integral(...)` | a logarithm of the root, piecewise on the sign of `a + b + c` | +### The derivative of a sum of exponentials over another is read off the integrand over the other's square + +**Answers where there were none.** Rubi's 6.7.1 `x (b - a sinh(x))/(a + b sinh(x))^2` and `x (b + a cosh(x))/(a + b cosh(x))^2` +were declined. The bracket over the square is the derivative of `-cosh(x)/(a + b sinh(x))`, and of +`sinh(x)/(a + b cosh(x))`. So by parts the integral is `x` times that quotient, plus `ln(a + b sinh(x))/b` or minus +`ln(a + b cosh(x))/b`. The integrals without x were answered with a piecewise on a discriminant, from the +substitution in `e^x`; they are now the quotient itself. For a sum `D` of exponentials, the rule reads the +integrand over `D^2` as `(N/D)'`, with `N` a constant times each exponential in `D` +([#233](https://github.com/asc-community/AngouriMath/issues/233)). + +| Input | Was (2.5.0) | Now | +|---|---|---| +| `"x*(b-a*sinh(x))/(a+b*sinh(x))^2".ToEntity().Integrate("x")` | `integral(...)` | 123 characters | +| `"x*(b+a*cosh(x))/(a+b*cosh(x))^2".ToEntity().Integrate("x")` | `integral(...)` | 119 characters | +| `"(b-a*sinh(x))/(a+b*sinh(x))^2".ToEntity().Integrate("x")` | `integral(...)` | 71 characters, where before this change master gave 7,861 | +| `"(b+a*cosh(x))/(a+b*cosh(x))^2".ToEntity().Integrate("x")` | `integral(...)` | 70 characters, where before this change master gave 7,631 | +| `"sech(x)^2*tanh(x)^2/(2+tanh(x)^3)^2".ToEntity().Integrate("x")` | no answer in thirty seconds | 60 characters, `-1/3/(2 + tanh(x)^3)`, in 55 ms; master gave 117 in 16 s and 9.7 GB | + ### A hyperbolic function of `a + 2 ln(x)` is integrated with `e^a` as a symbol too **Answers where there were none.** `tanh(a + 2 ln(x))^2/x^3` was declined, and so were nineteen more of Rubi's 6.3.2, diff --git a/Sources/AngouriMath/Functions/Continuous/Integration/IndefiniteIntegralSolver.cs b/Sources/AngouriMath/Functions/Continuous/Integration/IndefiniteIntegralSolver.cs index 1ad367eb0..0e3f4d960 100644 --- a/Sources/AngouriMath/Functions/Continuous/Integration/IndefiniteIntegralSolver.cs +++ b/Sources/AngouriMath/Functions/Continuous/Integration/IndefiniteIntegralSolver.cs @@ -42689,6 +42689,162 @@ Entity Pinned(Entity e) } } + /// + /// The derivative of N/D, for D a sum of exponentials of x and constants and + /// N a constant plus a constant times each exponential in D, read off the + /// integrand over D^2: alone, or beside x by parts. Rubi's + /// x (b - a sinh(x))/(a + b sinh(x))^2 is x G' for G = -cosh(x)/(a + b sinh(x)), + /// so x G - integral G, and the integral of G is -ln(a + b sinh(x))/b. + /// + /// + /// + /// The exponentials' span is closed under the derivative, so (N/D)' = (N' D - N D')/D^2 + /// with N' D - N D' linear in N's constants, and matching it to the integrand's + /// numerator is a linear system: each exponential a whole power of the first one, w, + /// and the identity read at six values of w. The answer is differentiated back at sampled + /// points. + /// + /// + /// The split took the numerator's terms apart, and the exponential substitution answered each + /// over the square of a quadratic in e^x with a piecewise on its discriminant; beside x, + /// parts integrated that piecewise again and ran past the budget, where the trigonometric + /// twin is 's. + /// https://github.com/asc-community/AngouriMath/issues/233 + /// + /// + internal static Entity? SolveAsTheDerivativeOfAQuotientOverASumOfExponentials(Entity expr, Entity.Variable x, bool integrateByParts) + { + if (!Integration.AnsweringTheQuestionAskedOrOneBelow || expr is not (Mulf or Divf)) + return null; + // A constant, x at most once, a numerator and the square of one sum below the bar. + Entity constant = Number.Integer.One; + Entity numerator = Number.Integer.One; + Entity? sum = null; + var besideX = false; + foreach (var (factor, underneath) in FactorsOfTheIntegrand(expr)) + { + if (!factor.ContainsNode(x)) + constant = underneath ? constant / factor : constant * factor; + else if (factor == x && !underneath && !besideX) + besideX = true; + else if (underneath && sum is null && factor is Powf(Sumf or Minusf, Number.Integer { EInteger: var two } ) squared && two.Equals(EInteger.FromInt32(2))) + sum = squared.Base; + else if (!underneath) + numerator = numerator == Number.Integer.One ? factor : numerator * factor; + else + return null; + } + bool OnlyThroughTheExponentials(Entity e) + => !e.Replace(node => node is Powf(var @base, var exponent) && @base == MathS.e && exponent.ContainsNode(x) ? Number.Integer.One : node).ContainsNode(x); + if (sum is null || !OnlyThroughTheExponentials(sum) || !OnlyThroughTheExponentials(numerator)) + return null; + // Each exponential a whole power w^n of the first one, w = e^(m + k x): `e^(-(c + d x))` is + // `w^(-1)` beside `e^(c + d x)`, with no `e^c` left to the system. + var exponentials = new List<(Entity Node, Entity Exponent, Entity Rate, int Power)>(); + foreach (var node in expr.Nodes) + if (node is Powf(var @base, var exponent) && @base == MathS.e && exponent.ContainsNode(x) + && !exponentials.Any(known => known.Node == node)) + { + if (!TreeAnalyzer.TryGetPolyLinear(exponent, x, out var rate, out var offset) || rate.ContainsNode(x) || offset.ContainsNode(x) + || Functions.PartialFractions.IsZeroAsAValue(rate)) + return null; + var multiple = exponentials.Count == 0 ? Number.Integer.One : Functions.PartialFractions.Bare((exponent / exponentials[0].Exponent).Simplify()); + if (multiple is not Number.Integer { EInteger: var power } || !power.CanFitInInt32()) + return null; + exponentials.Add((node, exponent, rate, power.ToInt32Unchecked())); + } + var inTheSum = exponentials.Where(exponential => sum.ContainsNode(exponential.Node)).ToList(); + if (inTheSum.Count is 0 or > 4) + return null; + // N = c_0 + sum c_i E_i: N' D - N D' is sum c_i (k_i E_i D - E_i D') - c_0 D'. N and N + c D + // have one derivative over D, so the system has a line of solutions; without c_0 first, + // which is the one of them with no constant where D has one: `-cosh(x)/(a + b sinh(x))`, + // whose integral is a logarithm, and not `-(a/b + e^x)/(a + b sinh(x))`, a constant away. + var derivativeOfTheSum = sum.Differentiate(x); + var columns = new List { -derivativeOfTheSum }; + foreach (var (node, _, rate, _) in inTheSum) + columns.Add(rate * node * sum - node * derivativeOfTheSum); + var target = constant * numerator; + if ((Above(withoutAConstant: true) ?? Above(withoutAConstant: false)) is not { } above) + return null; + Entity? At(Entity e, Number.Rational w) + { + var written = e.Replace(node => node is Powf(var @base, var exponent) && @base == MathS.e && exponent.ContainsNode(x) + && exponentials.FirstOrDefault(known => known.Node == node) is { Node: not null } known + ? MathS.Pow(w, Number.Integer.Create(known.Power)) + : node).InnerSimplified; + return written.ContainsNode(x) ? null : written; + } + var g = above / sum; + if (!Functions.PartialFractions.DerivativeHoldsAtSampledPoints(g, target / MathS.Pow(sum, 2), x)) + return null; + if (!besideX) + return g; + // Where N is a constant times D', the integral of G is that constant times ln(D): the + // constant read off the first exponential's coefficients, D being linear in them, and + // the answer checked. Asked as a question, `-cosh(x)/(a + b sinh(x))` spelled in + // exponentials went through the substitution in e^x and came back a piecewise. + var (firstNode, _, firstRate, _) = inTheSum[0]; + Entity WithTheExponentials(Entity e, Entity? one) + => e.Replace(node => node is Powf(var @base, var exponent) && @base == MathS.e && exponent.ContainsNode(x) + ? (node == one ? Number.Integer.One : Number.Integer.Zero) : node); + var inTheSumOfTheFirst = LowestOverTheSymbols((WithTheExponentials(sum, firstNode) - WithTheExponentials(sum, null)).InnerSimplified); + var inTheNumeratorOfTheFirst = LowestOverTheSymbols((WithTheExponentials(above, firstNode) - WithTheExponentials(above, null)).InnerSimplified); + Entity? answer = null; + if (!Functions.PartialFractions.IsZeroAsAValue(inTheSumOfTheFirst)) + { + var logarithmic = LowestOverTheSymbols(inTheNumeratorOfTheFirst / (firstRate * inTheSumOfTheFirst)); + var closed = (x * g - logarithmic * MathS.Ln(sum)).InnerSimplified; + if (Functions.PartialFractions.DerivativeHoldsAtSampledPoints(closed, expr, x)) + answer = closed; + } + if (answer is null) + { + if (Integration.ComputeAsAQuestionOfItsOwn(g, x, integrateByParts) is not { } integralOfG + || integralOfG.Nodes.Any(node => node == MathS.NaN || node is Integralf)) + return null; + var asked = (x * g - integralOfG).InnerSimplified; + if (Functions.PartialFractions.DerivativeHoldsAtSampledPoints(asked, expr, x)) + answer = asked; + } + return answer; + + // N, the system read at six values of w, or null where it has no solution. + Entity? Above(bool withoutAConstant) + { + var first = withoutAConstant ? 1 : 0; + var samples = new[] { Number.Rational.Create(2), Number.Rational.Create(3), Number.Rational.Create(5), + Number.Rational.Create(1, 2), Number.Rational.Create(1, 3), Number.Rational.Create(7) }; + var matrix = new Entity[samples.Length][]; + var rhs = new Entity[samples.Length]; + for (var j = 0; j < samples.Length; j++) + { + matrix[j] = new Entity[columns.Count - first]; + for (var k = first; k < columns.Count; k++) + if (At(columns[k], samples[j]) is { } entry) + matrix[j][k - first] = entry; + else + return null; + if (At(target, samples[j]) is not { } value) + return null; + rhs[j] = value; + } + if (!Functions.PartialFractions.TrySolveLinearWithSymbols(matrix, rhs, out var coefficients)) + return null; + Entity read = Number.Integer.Zero; + for (var k = 0; k < coefficients.Length; k++) + { + var coefficient = LowestOverTheSymbols(coefficients[k]); + if (Functions.PartialFractions.IsZeroAsAValue(coefficient)) + continue; + var column = k + first; + var term = column == 0 ? coefficient : coefficient == Number.Integer.One ? inTheSum[column - 1].Node : coefficient * inTheSum[column - 1].Node; + read = read == Number.Integer.Zero ? term : read + term; + } + return read == Number.Integer.Zero ? null : read; + } + } + /// /// x^m (a + b atan(c x))^p over a power of d (1 +- i c x), under x = tan(u)/c, where /// 1 + i tan(u) is e^(i u)/cos(u): the integrand in u is diff --git a/Sources/AngouriMath/Functions/Continuous/Integration/Integration.Definition.cs b/Sources/AngouriMath/Functions/Continuous/Integration/Integration.Definition.cs index 1544c7c7b..54eb3cf8e 100644 --- a/Sources/AngouriMath/Functions/Continuous/Integration/Integration.Definition.cs +++ b/Sources/AngouriMath/Functions/Continuous/Integration/Integration.Definition.cs @@ -720,6 +720,11 @@ internal static bool ASumOverRootsWouldAnswer // `(a + a cos(u))^(3/2)/cos(u)^(5/4)`, and parts integrated `x (b + a sin(x))/(a + b sin(x))^2`'s // derivative into a function of tan(x/2) and ran out of time. if ((answer = IndefiniteIntegralSolver.SolveAsTheDerivativeOfASineRaisedFromNothingBesidePowersOfTheCosine(expr, x, integrateByParts)) is { }) return answer; + // And its hyperbolic twin, where the functions are exponentials: the derivative of a sum of + // them over another, read off the integrand over the other's square, alone or beside x by + // parts. The split and the exponential substitution answered `(b - a sinh(x))/(a + b sinh(x))^2` + // with a piecewise on a discriminant, and parts beside x ran past the budget integrating it. + if ((answer = IndefiniteIntegralSolver.SolveAsTheDerivativeOfAQuotientOverASumOfExponentials(expr, x, integrateByParts)) is { }) return answer; // A cosine beside a function of the sine with a power off the rationals in it, in the sine: // the substitution search declined `cos(x)/(sin(x) + sin(x)^sqrt(2))`. if ((answer = IndefiniteIntegralSolver.SolveACosineBesideAFunctionOfTheSineWithAnIrrationalPowerInIt(expr, x, integrateByParts)) is { }) return answer; diff --git a/Sources/Tests/UnitTests/Calculus/TheDerivativeOfAQuotientOverASumOfExponentialsIntegralTest.cs b/Sources/Tests/UnitTests/Calculus/TheDerivativeOfAQuotientOverASumOfExponentialsIntegralTest.cs new file mode 100644 index 000000000..8e8408dbb --- /dev/null +++ b/Sources/Tests/UnitTests/Calculus/TheDerivativeOfAQuotientOverASumOfExponentialsIntegralTest.cs @@ -0,0 +1,51 @@ +// +// 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 +{ + /// + /// The derivative of a sum of exponentials over another, read off the integrand over the other's + /// square: (b - a sinh(x))/(a + b sinh(x))^2 is the derivative of -cosh(x)/(a + b sinh(x)), + /// and beside x, by parts, Rubi's 6.7.1 x (b - a sinh(x))/(a + b sinh(x))^2 is + /// -x cosh(x)/(a + b sinh(x)) + ln(a + b sinh(x))/b. The split and the substitution in + /// e^x answered the first with a piecewise on a discriminant, and the second ran past the budget. + /// #233 + /// + [Trait("Area", "Calculus")] + public sealed class TheDerivativeOfAQuotientOverASumOfExponentialsIntegralTest + { + [Theory] + [InlineData("x*(b - a*sinh(x))/(a + b*sinh(x))^2")] + [InlineData("x*(b + a*cosh(x))/(a + b*cosh(x))^2")] + [InlineData("x*(b - a*sinh(c + d*x))/(a + b*sinh(c + d*x))^2")] + [InlineData("(b - a*sinh(x))/(a + b*sinh(x))^2")] + [InlineData("(b + a*cosh(c + d*x))/(a + b*cosh(c + d*x))^2")] + public void IsAnsweredWithoutCases(string integrand) + { + var integral = integrand.ToEntity().Integrate("x"); + var text = integral.Stringize(); + Assert.DoesNotContain("integral(", text); + Assert.DoesNotContain("piecewise", text); + Entity Pinned(Entity e) => e.Substitute("a", 1.3).Substitute("b", 0.7).Substitute("c", 0.4).Substitute("d", 0.9); + var derivative = Pinned(integral.Substitute("C", 0)).Differentiate("x"); + var original = Pinned(integrand.ToEntity()); + foreach (var at in new[] { -1.7, -0.6, 0.4, 1.3, 2.2 }) + { + var want = original.Substitute("x", at).EvalNumerical(); + var got = derivative.Substitute("x", at).EvalNumerical(); + Assert.False(got.IsNaN, $"d/dx of the antiderivative of {integrand} has no value at x = {at}, where the integrand is {want}"); + Assert.True(Math.Abs((double)(got - want).RealPart) + Math.Abs((double)(got - want).ImaginaryPart) + < 1e-9 * Math.Max(1, Math.Abs((double)want.RealPart) + Math.Abs((double)want.ImaginaryPart)), + $"d/dx of the antiderivative of {integrand} is {got} at x = {at}, where the integrand is {want}"); + } + } + } +}