Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions BREAKING-CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1105,6 +1105,122 @@ private static bool IsADerivativeOfAFactorBelowTheBar(Entity expr, Entity.Variab
return constant * power * polynomial * MathS.Pow(quadratic, raised);
}

/// <summary>
/// A hyperbolic integrand asked as a trigonometric one, by Osborn's rule: with <c>L = i s</c>,
/// <c>cosh(L) = cos(s)</c>, <c>sinh(L) = i sin(s)</c>, <c>tanh(L) = i tan(s)</c> and
/// <c>coth(L) = -i cot(s)</c>, and <c>dx = i ds/b</c> for <c>L = a + b x</c>. The parser writes
/// the hyperbolic functions as exponentials -- <c>cosh(L)</c> is <c>(e^L + e^(-L))/2</c> -- so
/// the trigonometric rules never read them: <c>(b cosh(x) + c sinh(x) + sqrt(b^2 - c^2))^(1/2)</c>
/// timed out where <c>(b cos(x) + c sin(x) + sqrt(b^2 + c^2))^(1/2)</c> is answered at once, and
/// as <c>(b cos(s) + i c sin(s) + sqrt(b^2 - c^2))^(1/2)</c> it is the same question. The answer
/// in <c>s</c> is written back with <c>s = -i L</c> and checked against the integrand at
/// sampled points: a trigonometric answer derived for a real <c>s</c> 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
/// </summary>
/// <remarks>
/// Only where a sine or cosine is among the quotients, every exponential of x is a whole power
/// of one <c>e^L</c> inside one of them, and x stands nowhere else. With
/// <paramref name="rationalToo"/> false, only where the integrand is not a rational function of
/// <c>e^L</c>, 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 <c>s</c> is not cheap to decline, and asked early it took time from the rules
/// that answer <c>x/cosh(x)^(7/2) + 3/5 x sqrt(cosh(x))</c> and the like.
/// </remarks>
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<ERational>();
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;
}
}

/// <summary>
/// A quotient by the square of a base, by parts against the base's reciprocal, where the
/// base's derivative makes the rest exact: <c>N/(M v^2)</c> is <c>g v'/v^2</c> with
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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;
}
/// <summary>
Expand Down
Original file line number Diff line number Diff line change
@@ -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
{
/// <summary>
/// Hyperbolic integrands asked as trigonometric ones by Osborn's rule, <c>L = i s</c>:
/// <c>(b cosh(x) + c sinh(x) + sqrt(b^2 - c^2))^(1/2)</c> is
/// <c>(b cos(s) + i c sin(s) + sqrt(b^2 - c^2))^(1/2)</c>, which the trigonometric rules answer.
/// The hyperbolic functions arrive as exponentials, and these timed out or were declined.
/// Rubi's 6.7.1.
/// <a href="https://github.com/asc-community/AngouriMath/issues/233">#233</a>
/// </summary>
[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());
}
}
Loading