From c65cc0dc15cc517fe6ccea0d4eb3c39c9d6844a4 Mon Sep 17 00:00:00 2001 From: Rafael Vuijk Date: Tue, 20 Oct 2026 01:22:09 +0000 Subject: [PATCH] Integrate a function of the hyperbolic tangent alone under u = tanh The parser writes tanh(L) as (e^(2 L) - 1)/(e^(2 L) + 1) and coth(L) as its reciprocal, so an integrand built from those alone is R(u) with u = tanh(L), and dx = du/(b (1 - u^2)). tanh(x)/sqrt(a + b tanh(x)^2 + c tanh(x)^4) is u/((1 - u^2) sqrt(a + b u^2 + c u^4)), and sin(tanh(x)) is sin(u)/(1 - u^2), onto the sine and cosine integrals. The trigonometric counterparts are answered under u = tan(x); the hyperbolic functions arrive as exponentials, and nothing read them as a tangent. Only where R is not rational in u: those the exponential substitution answers. Rubi's 6.3 and 6.4: 33 rows go from unevaluated or timed out to solved (689 -> 722 of 728), 0 wrong. Families 0 and 1 and samples of 2-8 otherwise identical; gate and suite pass. Part of #233. Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_012sonx8iAspMiwRwokT1Ura --- BREAKING-CHANGES.md | 14 +++ .../Integration/IndefiniteIntegralSolver.cs | 99 +++++++++++++++++++ .../Integration/Integration.Definition.cs | 2 + ...ctionOfTheHyperbolicTangentIntegralTest.cs | 55 +++++++++++ 4 files changed, 170 insertions(+) create mode 100644 Sources/Tests/UnitTests/Calculus/AFunctionOfTheHyperbolicTangentIntegralTest.cs diff --git a/BREAKING-CHANGES.md b/BREAKING-CHANGES.md index fdd6a2b3b..050721360 100644 --- a/BREAKING-CHANGES.md +++ b/BREAKING-CHANGES.md @@ -282,6 +282,20 @@ becomes a polynomial in `u` times exponentials and trigonometric functions, and | `"(a+b*atan(c*x))^2/(1+i*c*x)^4".ToEntity().Integrate("x")` | `integral(...)` | 731 characters | | `"(a+b*atan(c*x))^3/(d+i*c*d*x)^4".ToEntity().Integrate("x")` | `integral(...)` | 1,202 characters | +### A function of the hyperbolic tangent alone is integrated under `u = tanh` + +**Answers where there were none.** `tanh(x)/sqrt(a + b tanh(x)^2 + c tanh(x)^4)` was declined, and so were 35 more of +Rubi's 6.3 and 6.4: the coth twins, `tanh/(a + b tanh^4)^k`, `sin(tanh(a + b x))^k` and `cos(coth(...))^k`, +and `(b coth(c + d x)^4)^(k/3)`. The parser writes `tanh(L)` as `(e^(2 L) - 1)/(e^(2 L) + 1)`, so nothing read them as +a function of the tangent; under `u = tanh(L)`, `dx = du/(b (1 - u^2))`, and the question in `u` is the one the +trigonometric counterparts are answered through. Only where the integrand is not rational in `u`: rational functions of +tanh keep the answers the exponential substitution gives them ([#233](https://github.com/asc-community/AngouriMath/issues/233)). + +| Input | Was (2.5.0) | Now | +|---|---|---| +| `"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` | + ### A constant or an exponential evaluated at a higher precision than before has all of its digits **More digits where there were a hundred.** `pi`, `e` and an exponential, evaluated once at one precision, went on diff --git a/Sources/AngouriMath/Functions/Continuous/Integration/IndefiniteIntegralSolver.cs b/Sources/AngouriMath/Functions/Continuous/Integration/IndefiniteIntegralSolver.cs index ed165e985..cc998dea0 100644 --- a/Sources/AngouriMath/Functions/Continuous/Integration/IndefiniteIntegralSolver.cs +++ b/Sources/AngouriMath/Functions/Continuous/Integration/IndefiniteIntegralSolver.cs @@ -903,6 +903,105 @@ private static bool IsADerivativeOfAFactorBelowTheBar(Entity expr, Entity.Variab return ((constant * MathS.Pow(determinant, Number.Integer.Create(j + k + EInteger.One))).Simplify() * sum).InnerSimplified; } + /// + /// A function of the hyperbolic tangent of a linear, under u = tanh(a + b x): the parser + /// writes tanh(L) as (W - 1)/(W + 1) and coth(L) as (W + 1)/(W - 1) + /// with W = e^(2 L), so an integrand built from those alone is R(u), and + /// dx = du/(b (1 - u^2)). tanh(x)/sqrt(a + b tanh(x)^2 + c tanh(x)^4) is + /// u/((1 - u^2) sqrt(a + b u^2 + c u^4)), and sin(tanh(x)) is + /// sin(u)/(1 - u^2), onto the sine and cosine integrals. The trigonometric + /// counterparts are answered under u = tan(x), which the substitution reads in the + /// tangent as a node; the hyperbolic functions arrive as exponentials, and nothing read them + /// as a tangent. Rubi's 6.3 and 6.4. + /// https://github.com/asc-community/AngouriMath/issues/233 + /// + /// + /// Only where R is not a rational function: those the exponential substitution answers. + /// Every exponential has to be a whole power of W inside one of the two quotients; + /// one anywhere else, an odd power of e^L from a sinh or cosh among + /// them, and the rule declines. d/dx F(tanh(L)) = b (1 - tanh(L)^2) F'(tanh(L)) + /// wherever F is differentiable, so the answer written back holds where the integrand + /// is defined. + /// + internal static Entity? SolveAFunctionOfTheHyperbolicTangent(Entity expr, Entity.Variable x, bool integrateByParts) + { + // W from the first tangent or cotangent the parser wrote: (e^E -+ 1)/(e^E +- 1). + Entity? exponent = null; + foreach (var node in expr.Nodes) + if (node is Divf(var top, var bottom) && ATangentsExponent(top, bottom) is { } found) + { + exponent = found.Exponent; + break; + } + if (exponent is null || !TreeAnalyzer.TryGetPolyLinear(exponent, x, out var twiceSlope, out var twiceIntercept) || TreeAnalyzer.IsZero(twiceSlope)) + return null; + var u = Variable.CreateUnique(expr, "u_tanh"); + var w = Variable.CreateUnique(expr, "w_tanh"); + // Every exponential of x a whole power of W, or decline. + var declined = false; + var inW = expr.Replace(node => + { + if (node is not Powf(var @base, var e) || @base != MathS.e || !e.ContainsNode(x)) + return node; + if (!TreeAnalyzer.TryGetPolyLinear(e, x, out var slope, out var intercept) + || Functions.PartialFractions.Bare((slope / twiceSlope).Simplify()).Evaled is not Number.Integer k + || !k.EInteger.CanFitInInt32() || !VanishesIdentically(intercept - k * twiceIntercept)) + { + declined = true; + return node; + } + return k == Number.Integer.One ? w : MathS.Pow(w, k); + }); + if (declined) + return null; + // The two quotients as u and 1/u; W anywhere else declines. + var inU = inW.Replace(node => node is Divf(var top, var bottom) && AQuotientInW(top, bottom, w) is { } tangent + ? (tangent ? u : 1 / u) + : node); + if (inU.ContainsNode(w) || inU.ContainsNode(x)) + return null; + // A rational function of u is the exponential substitution's. + if (IsARationalFunction(inU, u)) + return null; + // Free of x and small, simplified: `2 b/2` is otherwise left as written. + var slopeOfL = Functions.PartialFractions.Bare((twiceSlope / 2).Simplify()); + var question = inU / (slopeOfL * (1 - MathS.Sqr(u))); + if (Integration.ComputeAsAQuestionOfItsOwn(question, u, integrateByParts) is not { } answer + || answer.Nodes.Any(node => node is Integralf || node == MathS.NaN)) + return null; + // a + b x from its coefficients: (2 (a + b x))/2 is left as written by the simplification. + var intercept0 = Functions.PartialFractions.Bare((twiceIntercept / 2).Simplify()); + var linear = intercept0.Evaled is Number.Complex { IsZero: true } ? slopeOfL * x : intercept0 + slopeOfL * x; + return answer.Substitute(u, MathS.Hyperbolic.Tanh(linear)); + + // (W - 1)/(W + 1) is the tangent, true, and (W + 1)/(W - 1) the cotangent, false. + static bool? AQuotientInW(Entity top, Entity bottom, Variable w) + { + if (!TreeAnalyzer.TryGetPolyLinear(top, w, out var a1, out var b1) || !TreeAnalyzer.TryGetPolyLinear(bottom, w, out var a2, out var b2) + || a1 != Number.Integer.One || a2 != Number.Integer.One) + return null; + if (b1 == Number.Integer.MinusOne && b2 == Number.Integer.One) + return true; + if (b1 == Number.Integer.One && b2 == Number.Integer.MinusOne) + return false; + return null; + } + + static (Entity Exponent, bool Tangent)? ATangentsExponent(Entity top, Entity bottom) + { + // e^E - 1 over e^E + 1, as the parser writes them, or the other way up. + (Entity, int)? Read(Entity side) => side switch + { + Minusf(Powf(var e0, var power), Number.Integer one) when e0 == MathS.e && one == Number.Integer.One => (power, -1), + Sumf(Powf(var e0, var power), Number.Integer one) when e0 == MathS.e && one == Number.Integer.One => (power, 1), + _ => null + }; + if (Read(top) is not var (p1, s1) || Read(bottom) is not var (p2, s2) || p1 != p2 || s1 == s2) + return null; + return (p1, s1 < 0); + } + } + /// /// A quotient by the square of a base, by parts against the base's reciprocal, where the /// base's derivative makes the rest exact: N/(M v^2) is g v'/v^2 with diff --git a/Sources/AngouriMath/Functions/Continuous/Integration/Integration.Definition.cs b/Sources/AngouriMath/Functions/Continuous/Integration/Integration.Definition.cs index 22dc0dac3..278c548e5 100644 --- a/Sources/AngouriMath/Functions/Continuous/Integration/Integration.Definition.cs +++ b/Sources/AngouriMath/Functions/Continuous/Integration/Integration.Definition.cs @@ -789,6 +789,8 @@ private static Entity WithItsPiecewiseSumsCombined(Entity expr) if ((answer = IndefiniteIntegralSolver.SolveByTheMoebiusSubstitutionOfAQuotientOfLinears(expr, x)) is { }) return answer; // And a power of x times the logarithm of A + B sqrt(Q), by parts, its derivative over the conjugate. if ((answer = IndefiniteIntegralSolver.SolveByPartsAgainstALogarithmOfARadicalSum(expr, x)) is { }) return answer; + // A function of tanh(L) or coth(L) alone that is not rational in it, under u = tanh(L). + if ((answer = IndefiniteIntegralSolver.SolveAFunctionOfTheHyperbolicTangent(expr, x, integrateByParts)) is { }) return answer; // A power of an exponential with a positive base is the exponential of the product, // exactly, and only that spelling is one the exponential rules read. if ((answer = IndefiniteIntegralSolver.SolveByFlatteningAPowerOfAnExponential(expr, x, integrateByParts)) is { }) return answer; diff --git a/Sources/Tests/UnitTests/Calculus/AFunctionOfTheHyperbolicTangentIntegralTest.cs b/Sources/Tests/UnitTests/Calculus/AFunctionOfTheHyperbolicTangentIntegralTest.cs new file mode 100644 index 000000000..0a9af10c9 --- /dev/null +++ b/Sources/Tests/UnitTests/Calculus/AFunctionOfTheHyperbolicTangentIntegralTest.cs @@ -0,0 +1,55 @@ +// +// 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 +{ + /// + /// A function of tanh(a + b x) or coth(a + b x) alone, not rational in it, under + /// u = tanh(a + b x). The trigonometric counterparts were answered under u = tan; + /// the hyperbolic functions arrive as exponentials, and these timed out or were declined. + /// Rubi's 6.3 and 6.4. + /// #233 + /// + [Trait("Area", "Calculus")] + public sealed class AFunctionOfTheHyperbolicTangentIntegralTest + { + [Theory] + [InlineData("tanh(x)/sqrt(a + b*tanh(x)^2 + c*tanh(x)^4)")] + [InlineData("coth(x)/sqrt(a + b*coth(x)^2 + c*coth(x)^4)")] + [InlineData("tanh(x)/(a + b*tanh(x)^4)^(3/2)")] + [InlineData("sin(tanh(a + b*x))")] + [InlineData("cos(tanh(a + b*x))^3")] + [InlineData("sin(coth(a + b*x))")] + [InlineData("(b*coth(c + d*x)^4)^(1/3)")] + public void UnderTheTangent(string integrand) + { + var integral = integrand.ToEntity().Integrate("x"); + var text = integral.Stringize(); + Assert.DoesNotContain("integral(", text); + Entity Pinned(Entity e) => e.Substitute("a", 1.3).Substitute("b", 0.7).Substitute("c", 0.4).Substitute("d", 0.3); + var derivative = Pinned(integral.Substitute("C", 0)).Differentiate("x"); + var original = Pinned(integrand.ToEntity()); + var compared = 0; + foreach (var at in new[] { -2.9, -0.7, 0.3, 0.8, 1.3, 2.1 }) + { + var want = original.Substitute("x", at).EvalNumerical(); + var got = derivative.Substitute("x", at).EvalNumerical(); + if (want.IsNaN) + continue; + compared++; + 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}"); + } + Assert.True(compared >= 5, $"only {compared} points could be compared for {integrand}"); + } + } +}