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