From 808c66c1391040b6d89263453af35c478b916e87 Mon Sep 17 00:00:00 2001 From: Rafael Vuijk Date: Tue, 20 Oct 2026 07:27:27 +0000 Subject: [PATCH] Ask a hyperbolic integrand as a trigonometric one The parser writes the hyperbolic functions as exponentials, so the trigonometric rules never read them. `(b cosh(x) + c sinh(x) + sqrt(b^2 - c^2))^(1/2)` was declined where `(b cos(x) + c sin(x) + sqrt(b^2 + c^2))^(1/2)` is answered at once. By Osborn's rule, with L = i s, cosh(L) is cos(s), sinh(L) is i sin(s) and dx = i ds/b, and the question in s is one the trigonometric rules answer. The answer is written back with s = -i L and checked at sampled points, so a branch that holds for a real s but differs on the imaginary axis is declined. The rule only applies where every exponential of x is a whole power of one e^L and x appears nowhere else. It is asked of the question asked only, and after every rule in x. Asked early, the question in s, which is slow to decline, took the budget from rules that answer `x/cosh(x)^(7/2) + 3/5 x sqrt(cosh(x))` and its neighbours at once. Rubi's 6.5.3, 6.6.3 and 6.7.1. 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 | 116 ++++++++++++++++++ .../Integration/Integration.Definition.cs | 5 + ...ntegrandAsATrigonometricOneIntegralTest.cs | 66 ++++++++++ 4 files changed, 205 insertions(+) create mode 100644 Sources/Tests/UnitTests/Calculus/AHyperbolicIntegrandAsATrigonometricOneIntegralTest.cs diff --git a/BREAKING-CHANGES.md b/BREAKING-CHANGES.md index 90cb47a9c..04230258d 100644 --- a/BREAKING-CHANGES.md +++ b/BREAKING-CHANGES.md @@ -627,6 +627,24 @@ roots, `1 - x^2`, is taken as its two ([#718](https://github.com/asc-community/A | `"1/((1 - x^2)*(a + b*x^3))".ToEntity().Integrate("x")` | `integral(...)` | 1,600 characters | | `"1/(x*(1 - x^2)*(a + b*x^3))".ToEntity().Integrate("x")` | `integral(...)` | 1,637 characters | +### A hyperbolic integrand is asked as a trigonometric one + +**Answers where there were none.** The parser writes the hyperbolic functions as exponentials, so the trigonometric +rules never read them: `(b cosh(x) + c sinh(x) + sqrt(b^2 - c^2))^(1/2)` was declined where +`(b cos(x) + c sin(x) + sqrt(b^2 + c^2))^(1/2)` is answered at once. With `L = i s` (Osborn's rule) `cosh(L)` is +`cos(s)`, `sinh(L)` is `i sin(s)` and `dx = i ds/b`, and the question in `s` is one the trigonometric rules answer; the +answer is written back with `s = -i L` and checked against the integrand at sampled points, so that a branch taken for +a real `s` that differs on the imaginary axis is declined. Only where every exponential of x is a whole power of one +`e^L` and x stands nowhere else, asked of the question asked and after every rule in x +([#233](https://github.com/asc-community/AngouriMath/issues/233)). + +| Input | Was (2.5.0) | Now | +|---|---|---| +| `"(b*cosh(x)+c*sinh(x)+sqrt(b^2-c^2))^(1/2)".ToEntity().Integrate("x")` | `integral(...)` | 185 characters | +| `"1/(a+b*sinh(x)+c*sinh(x)^2)".ToEntity().Integrate("x")` | `integral(...)` | 576 characters | +| `"coth(c+d*x)/sqrt(a+b*sech(c+d*x))".ToEntity().Integrate("x")` | `integral(...)` | 725 characters | +| `"(cosh(x)*coth(x))^(3/2)".ToEntity().Integrate("x")` | `integral(...)` | 272 characters | + ### A hyperbolic cosecant beside a power of a binomial in the sine is read through its exponentials **Answers where there were none.** `csch(c + d x) (a + b sinh(c + d x)^4)^2` ran past fifteen minutes. `csch(y)` arrives diff --git a/Sources/AngouriMath/Functions/Continuous/Integration/IndefiniteIntegralSolver.cs b/Sources/AngouriMath/Functions/Continuous/Integration/IndefiniteIntegralSolver.cs index d07e5a3f1..f47f57a9b 100644 --- a/Sources/AngouriMath/Functions/Continuous/Integration/IndefiniteIntegralSolver.cs +++ b/Sources/AngouriMath/Functions/Continuous/Integration/IndefiniteIntegralSolver.cs @@ -1105,6 +1105,122 @@ private static bool IsADerivativeOfAFactorBelowTheBar(Entity expr, Entity.Variab return constant * power * polynomial * MathS.Pow(quadratic, raised); } + /// + /// A hyperbolic integrand asked as a trigonometric one, by Osborn's rule: with L = i s, + /// cosh(L) = cos(s), sinh(L) = i sin(s), tanh(L) = i tan(s) and + /// coth(L) = -i cot(s), and dx = i ds/b for L = a + b x. The parser writes + /// the hyperbolic functions as exponentials -- cosh(L) is (e^L + e^(-L))/2 -- so + /// the trigonometric rules never read them: (b cosh(x) + c sinh(x) + sqrt(b^2 - c^2))^(1/2) + /// timed out where (b cos(x) + c sin(x) + sqrt(b^2 + c^2))^(1/2) is answered at once, and + /// as (b cos(s) + i c sin(s) + sqrt(b^2 - c^2))^(1/2) it is the same question. The answer + /// in s is written back with s = -i L and checked against the integrand at + /// sampled points: a trigonometric answer derived for a real s can take a different + /// branch on the imaginary axis, and then the rule declines. Rubi's 6.7.1. + /// https://github.com/asc-community/AngouriMath/issues/233 + /// + /// + /// Only where a sine or cosine is among the quotients, every exponential of x is a whole power + /// of one e^L inside one of them, and x stands nowhere else. With + /// false, only where the integrand is not a rational function of + /// e^L, which the exponential substitution answers in real terms; with it true, only those, + /// after that substitution has declined them. Asked only of the question asked, and last: the + /// question in s is not cheap to decline, and asked early it took time from the rules + /// that answer x/cosh(x)^(7/2) + 3/5 x sqrt(cosh(x)) and the like. + /// + internal static Entity? SolveAHyperbolicIntegrandAsATrigonometricOne(Entity expr, Entity.Variable x, bool integrateByParts, bool rationalToo) + { + // Only the question asked: a question in s is not cheap to decline, and below the top every + // sub-question a route gives up on would ask it. + if (!Integration.AnsweringTheQuestionAsked) + return null; + // The exponents of x, each linear, as rational multiples of the first one's. + var exponents = expr.Nodes.Where(node => node is Powf(var e0, var p0) && e0 == MathS.e && p0.ContainsNode(x)).Select(node => ((Powf)node).Exponent).Distinct().ToList(); + if (exponents.Count == 0 || !TreeAnalyzer.TryGetPolyLinear(exponents[0], x, out var firstSlope, out var firstIntercept) || TreeAnalyzer.IsZero(firstSlope)) + return null; + var ratios = new List(); + foreach (var exponent in exponents) + { + if (!TreeAnalyzer.TryGetPolyLinear(exponent, x, out var slope, out var intercept) + || Functions.PartialFractions.Bare((slope / firstSlope).Simplify()).Evaled is not Number.Rational ratio + || !VanishesIdentically(intercept - ratio * firstIntercept)) + return null; + ratios.Add(ratio.ERational); + } + // L the largest exponent every one is a whole multiple of: the first times g/d, with d the + // denominators' least common multiple and g the numerators' greatest common divisor over it. + var denominator = ratios.Aggregate(EInteger.One, (lcm, r) => lcm * r.Denominator / lcm.Gcd(r.Denominator)); + var wholes = ratios.Select(r => r.Numerator * (denominator / r.Denominator)).ToList(); + var divisor = wholes.Aggregate(EInteger.Zero, (gcd, n) => gcd.Gcd(n.Abs())); + if (divisor.IsZero) + return null; + var unit = Number.Rational.Create(divisor, denominator); + var slopeOfL = Functions.PartialFractions.Bare((firstSlope * unit).Simplify()); + var interceptOfL = Functions.PartialFractions.Bare((firstIntercept * unit).Simplify()); + var w = Variable.CreateUnique(expr, "w_osb"); + var s = Variable.CreateUnique(expr, "s_osb"); + var multiples = exponents.Zip(wholes, (exponent, n) => (exponent, k: n / divisor)).ToDictionary(pair => pair.exponent, pair => pair.k); + if (multiples.Values.Any(k => !k.CanFitInInt32() || k.Abs().CompareTo(EInteger.FromInt32(16)) > 0)) + return null; + var inW = expr.Replace(node => node is Powf(var e0, var p0) && e0 == MathS.e && multiples.TryGetValue(p0, out var k) + ? (k.Equals(EInteger.One) ? w : MathS.Pow(w, Number.Integer.Create(k))) + : node); + // Only exponentials of x: beside x itself the question in s is a polynomial in s times + // trigonometric functions, slow to decline, where the rules in x answer at once. + if (inW.ContainsNode(x)) + return null; + // A rational function of e^L is the exponential substitution's, and asked here only after it + // has declined one; the rest before. + if (rationalToo != IsARationalFunction(inW, w)) + return null; + var sawASineOrCosine = false; + var inS = inW.Replace(node => + { + if (node is not Divf(var top, var bottom) || !top.ContainsNode(w)) + return node; + // (W +- 1/W)/2: W times the top is W^2 +- 1. + if (bottom == Number.Integer.Create(2) && Squared(top * w, w) is { } sign) + { + sawASineOrCosine = true; + return sign > 0 ? MathS.Cos(s) : MathS.i * MathS.Sin(s); + } + // (W^2 - 1)/(W^2 + 1) and its reciprocal. + if (Squared(top, w) is { } above && Squared(bottom, w) is { } below && above != below) + return above < 0 ? MathS.i * MathS.Tan(s) : -MathS.i * MathS.Cotan(s); + return node; + }); + if (!sawASineOrCosine || inS.ContainsNode(w)) + return null; + var question = MathS.i / slopeOfL * inS; + if (Integration.ComputeAsAQuestionOfItsOwn(question, s, integrateByParts) is not { } inTermsOfS + || inTermsOfS.Nodes.Any(node => node is Integralf || node == MathS.NaN)) + return null; + var linear = interceptOfL.Evaled is Number.Complex { IsZero: true } ? slopeOfL * x : interceptOfL + slopeOfL * x; + // Back in L: cos(-i L) is cosh(L), sin(-i L) is -i sinh(L), tan(-i L) is -i tanh(L) and + // cot(-i L) is i coth(L); s anywhere else is -i L. + var answer = inTermsOfS.Replace(node => node switch + { + Cosf(var arg) when arg == s => MathS.Hyperbolic.Cosh(linear), + Sinf(var arg) when arg == s => -MathS.i * MathS.Hyperbolic.Sinh(linear), + Tanf(var arg) when arg == s => -MathS.i * MathS.Hyperbolic.Tanh(linear), + Cotanf(var arg) when arg == s => MathS.i * MathS.Hyperbolic.Cotanh(linear), + Secantf(var arg) when arg == s => MathS.Hyperbolic.Sech(linear), + Cosecantf(var arg) when arg == s => MathS.i * MathS.Hyperbolic.Cosech(linear), + _ => node + }).Substitute(s, -MathS.i * linear).InnerSimplified; + return Functions.PartialFractions.DerivativeHoldsAtSampledPoints(answer, expr, x) ? answer : null; + + // +1 for W^2 + 1 and -1 for W^2 - 1, as polynomials in W; null for anything else. + static int? Squared(Entity polynomial, Variable w) + { + if (!TreeAnalyzer.TryGetPolynomial(polynomial, w, out var read) || read.Count != 2 + || !read.TryGetValue(EInteger.FromInt32(2), out var leading) || !read.TryGetValue(EInteger.Zero, out var constant) + || leading.InnerSimplified != Number.Integer.One) + return null; + var c = constant.InnerSimplified; + return c == Number.Integer.One ? 1 : c == Number.Integer.MinusOne ? -1 : null; + } + } + /// /// 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 e43b5088c..7d482b645 100644 --- a/Sources/AngouriMath/Functions/Continuous/Integration/Integration.Definition.cs +++ b/Sources/AngouriMath/Functions/Continuous/Integration/Integration.Definition.cs @@ -1373,6 +1373,8 @@ private static Entity WithItsPiecewiseSumsCombined(Entity expr) // The exponential substitution beside the other rewrites. It is also what integrates // the hyperbolic functions, which are not nodes here but quotients of exponentials. if ((answer = IndefiniteIntegralSolver.SolveByExponentialSubstitution(expr, x, integrateByParts)) is { }) return answer; + // And a rational one, where the exponential substitution has declined it. + if ((answer = IndefiniteIntegralSolver.SolveAHyperbolicIntegrandAsATrigonometricOne(expr, x, integrateByParts, rationalToo: true)) is { }) return answer; // A quotient by an exponential is the product with its reciprocal, and only that // spelling reaches integration by parts. After the substitution above rather than // before it, since a quotient that is rational in e^(k x) is that rule's to answer @@ -1438,6 +1440,9 @@ private static Entity WithItsPiecewiseSumsCombined(Entity expr) // `sgn(sin(x)) sin(x)` with the sign taken out first. if ((answer = IndefiniteIntegralSolver.SolveByTakingASignOut(expr, x, integrateByParts)) is { }) return answer; if (integrateByParts && (answer = IndefiniteIntegralSolver.SolveIntegratingByParts(expr, x)) is { }) return answer; + // A hyperbolic integrand that is not rational in its exponential, asked as a trigonometric + // one: last, since the question in s can take long where a rule above answers in x at once. + if ((answer = IndefiniteIntegralSolver.SolveAHyperbolicIntegrandAsATrigonometricOne(expr, x, integrateByParts, rationalToo: false)) is { }) return answer; return null; } /// diff --git a/Sources/Tests/UnitTests/Calculus/AHyperbolicIntegrandAsATrigonometricOneIntegralTest.cs b/Sources/Tests/UnitTests/Calculus/AHyperbolicIntegrandAsATrigonometricOneIntegralTest.cs new file mode 100644 index 000000000..9bfa27355 --- /dev/null +++ b/Sources/Tests/UnitTests/Calculus/AHyperbolicIntegrandAsATrigonometricOneIntegralTest.cs @@ -0,0 +1,66 @@ +// +// 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 +{ + /// + /// Hyperbolic integrands asked as trigonometric ones by Osborn's rule, L = i s: + /// (b cosh(x) + c sinh(x) + sqrt(b^2 - c^2))^(1/2) is + /// (b cos(s) + i c sin(s) + sqrt(b^2 - c^2))^(1/2), which the trigonometric rules answer. + /// The hyperbolic functions arrive as exponentials, and these timed out or were declined. + /// Rubi's 6.7.1. + /// #233 + /// + [Trait("Area", "Calculus")] + public sealed class AHyperbolicIntegrandAsATrigonometricOneIntegralTest + { + [Theory] + [InlineData("(b*cosh(x) + c*sinh(x) + sqrt(b^2 - c^2))^(1/2)")] + [InlineData("1/(b*cosh(x) + c*sinh(x) + sqrt(b^2 - c^2))^(3/2)")] + [InlineData("(b*cosh(x) + c*sinh(x) - sqrt(b^2 - c^2))^(1/2)")] + [InlineData("(cosh(x)*coth(x))^(3/2)")] + [InlineData("1/(a + b*sinh(x) + c*sinh(x)^2)")] + [InlineData("sinh(x)/(a + b*sinh(x) + c*sinh(x)^2)")] + // Rubi's 6.5.3. + [InlineData("coth(a + b*x)/sqrt(c + b*sech(a + b*x))")] + public void AsTheTrigonometricQuestion(string integrand) + { + var integral = integrand.ToEntity().Integrate("x"); + var text = integral.Stringize(); + Assert.DoesNotContain("integral(", text); + // b > c, so that the root is real; n not whole. + Entity Pinned(Entity e) => e.Substitute("a", 1.3).Substitute("b", 2.7).Substitute("c", 0.4).Substitute("n", 0.6); + var derivative = Pinned(integral.Substitute("C", 0)).Differentiate("x"); + var original = Pinned(integrand.ToEntity()); + var compared = 0; + foreach (var at in new[] { -1.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}"); + } + + // Asked before the rules in x, the question in s took the time they answer these in. + [Theory] + [InlineData("x/cosh(x)^(7/2) + 3/5*x*sqrt(cosh(x))")] + [InlineData("cosh(a + b*x)^3/(c + b*sqrt(sinh(a + b*x)))^2")] + [InlineData("(n + c*x)/(c + i*c*sinh(a + b*x))")] + public void LeavesTheRulesInXTheirIntegrands(string integrand) + => Assert.DoesNotContain("integral(", integrand.ToEntity().Integrate("x").Stringize()); + } +}