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());
+ }
+}