From 0fd13e9de5f2dbab03a722ecd0567c6f87a5c4c6 Mon Sep 17 00:00:00 2001 From: Rafael Vuijk Date: Wed, 21 Oct 2026 16:04:55 +0000 Subject: [PATCH] Integrate four inverse-hyperbolic shapes: over a half-odd power of an even quadratic, x^m arsech or arcsch of sqrt(x), a polynomial over a power of the linear of an artanh, and arcoth as artanh plus a constant - An inverse hyperbolic function over a half-odd power of an even quadratic, by parts against it. - A power of x times the arsech or arcsch of sqrt(x), by parts. - A polynomial over a power of the linear of an artanh, written in powers of the linear. - An arcoth of a linear as its artanh plus a constant, last of all, so that every arcoth answered as written keeps that answer. Chain against master f7f2a7a8: pocket of 1,509 rows, 1,498 -> 1,501 solved, 0 wrong; samples unchanged; the 3 rows that moved, alone, 0 -> 3 solved: Rubi's 7.3.4:133 and 639 and 7.4.1:74. Where master answers the same integrands it writes them longer: atanh(a x)/(c + d x^2)^(3/2) is 4,645 characters there and 1,034 here, acosh(a x)/(c + d x^2)^(9/2) 9,652 and 2,409. No row's allocation grew past 1.3x. Part of #233. Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_012sonx8iAspMiwRwokT1Ura --- BREAKING-CHANGES.md | 59 ++++ .../Integration/IndefiniteIntegralSolver.cs | 324 ++++++++++++++++++ .../Integration/Integration.Definition.cs | 17 + ...owerOfALinearOfTheLogarithmIntegralTest.cs | 55 +++ ...cothAsAnArtanhPlusAConstantIntegralTest.cs | 53 +++ ...erAHalfOddPowerOfAQuadraticIntegralTest.cs | 55 +++ ...erbolicSecantOfARootByPartsIntegralTest.cs | 55 +++ 7 files changed, 618 insertions(+) create mode 100644 Sources/Tests/UnitTests/Calculus/APolynomialOverAPowerOfALinearOfTheLogarithmIntegralTest.cs create mode 100644 Sources/Tests/UnitTests/Calculus/AnArcothAsAnArtanhPlusAConstantIntegralTest.cs create mode 100644 Sources/Tests/UnitTests/Calculus/AnInverseHyperbolicOverAHalfOddPowerOfAQuadraticIntegralTest.cs create mode 100644 Sources/Tests/UnitTests/Calculus/AnInverseHyperbolicSecantOfARootByPartsIntegralTest.cs diff --git a/BREAKING-CHANGES.md b/BREAKING-CHANGES.md index 0909ba42e..375222c37 100644 --- a/BREAKING-CHANGES.md +++ b/BREAKING-CHANGES.md @@ -129,6 +129,23 @@ since semantic versioning admits it. Measured on the suite: **7 of 8,525 tests** referenced the old reading — five boolean-solver inputs written `A | B`, and two that existed to record this spelling as at risk. +### An inverse hyperbolic function over a half-odd power of an even quadratic is integrated by parts against it + +**Answers where there were none.** `atanh(a x)/(c + d x^2)^(3/2)` was declined, and so were its powers up to `^(9/2)`, +`acosh(a x)` over the same powers, and `(a + b asinh(c x))` and `(a + b acosh(c x))` over `(d + k x^2)^(3/2)` to +`^(7/2)`: Rubi's 7.1.4, 7.2.4, 7.2.5 and 7.3.4. By parts, the antiderivative of the power is `x` times powers of +`(c + d x^2)^(-1/2)`, and the logarithm's derivative is even in `x`, so the remainder is `x` times a function of +`x^2`; under `w = x^2` each of its terms is a power of `c + d w` over `1 - a^2 w` or over the root of a linear, +answered at once. Not where the quadratic is the function's own, `1 - a^2 x^2` for the artanh, which the +substitution under the hyperbolic function answers ([#233](https://github.com/asc-community/AngouriMath/issues/233)). + +| Input | Was (2.5.0) | Now | +|---|---|---| +| `"atanh(a*x)/(c+d*x^2)^(3/2)".ToEntity().Integrate("x")` | `integral(...)` | 1,034 characters | +| `"atanh(a*x)/(c+d*x^2)^(9/2)".ToEntity().Integrate("x")` | `integral(...)` | 7,136 characters | +| `"acosh(a*x)/(c+d*x^2)^(9/2)".ToEntity().Integrate("x")` | `integral(...)` | 2,409 characters | +| `"(a+b*asinh(c*x))/(d+k*x^2)^(7/2)".ToEntity().Integrate("x")` | `integral(...)` | 1,843 characters | + ### A power of x times `atanh(sqrt(e) x/sqrt(d + e x^2))` is integrated by parts against the root **Answers where there were none.** `x atanh(sqrt(e) x/sqrt(d + e x^2))` was declined, and so were twelve more of Rubi's @@ -142,6 +159,21 @@ quotient of two sums with a root in each, which the search never brought down to | `"atanh(x*sqrt(k)/sqrt(d+k*x^2))".ToEntity().Integrate("x")` | `integral(...)` | `x atanh(...) - sqrt(d + k x^2)/sqrt(k)`, written in the logarithm | | `"atanh(x*sqrt(k)/sqrt(d+k*x^2))/x^5".ToEntity().Integrate("x")` | `integral(...)` | by parts onto `1/(x^4 sqrt(d + k x^2))` | +### A power of x times the arsech or arcsch of `sqrt(x)` is integrated by parts + +**Answers where there were none.** Every power from `x^0` to `x^3` times `asech(sqrt(x))` or `acsch(sqrt(x))`, Rubi's +7.5.2 and 7.6.2, was declined or ran past thirty seconds. By parts the remainder is `K/(2 (m + 1))` times the +integral of `x^m/sqrt(1 - x)`, or of `x^m/sqrt(1 + x)` for the arcsch, where `K = sqrt(1 - x)/(sqrt(x) sqrt(1/x - 1))` is `1` for `x > 0` and `-1` for `x < 0`, and the arcsch's +`K = sqrt(1 + x)/(sqrt(x) sqrt(1/x + 1))` is `-1` between `-1` and `0` and `1` elsewhere. `K` is written in the answer +as it is defined, so the answer holds on the negative half-line too +([#233](https://github.com/asc-community/AngouriMath/issues/233)). + +| Input | Was (2.5.0) | Now | +|---|---|---| +| `"x^3*asech(sqrt(x))".ToEntity().Integrate("x")` | no answer in thirty seconds | 210 characters | +| `"x*acsch(sqrt(x))".ToEntity().Integrate("x")` | `integral(...)` | 146 characters | +| `"x^3*acsch(sqrt(x))".ToEntity().Integrate("x")` | no answer in thirty seconds | 205 characters | + ### A power of x is taken out of a bracket to find the derivative of a product of powers **Answers where there were none.** `x^(2n) (c + d x)^n (2 c x + 3 d x^2)`, Rubi's 1.3.1, was declined. A bracket that @@ -283,6 +315,19 @@ the working digits, or `MathS.Settings.PrecisionErrorZeroRange` where a caller h still that integer, so `e^(-123.456)` is 0 as it was. [#1338](https://github.com/asc-community/AngouriMath/issues/1338). +### A polynomial over a power of the linear of an artanh is written in powers of the linear + +**Answers where there were none.** `x (a + b atanh(c x))^2/(d + c d x)^3`, Rubi's 7.3.4, ran out of time. In powers of +`d + c d x`, `x` is `((d + c d x) - d)/(c d)`, and the integrand is two terms, each a power of one of the logarithm's +linears beside a function of it, which are answered under `t = (1 + c x)/(1 - c x)`. Where the polynomial's degree is +at most the power less two, so that no term is over the linear alone, and the function is not the logarithm's first +power, which by parts answers as before ([#233](https://github.com/asc-community/AngouriMath/issues/233)). + +| Input | Was (2.5.0) | Now | +|---|---|---| +| `"x*(a+b*atanh(c*x))^2/(d+c*d*x)^3".ToEntity().Integrate("x")` | `integral(...)` | 731 characters | +| `"x*(a+b*acoth(c*x))^2/(d+c*d*x)^3".ToEntity().Integrate("x")` | `integral(...)` | 756 characters | + ### An arctangent over a power of `d (1 + i c x)` is integrated under the tangent **Answers where there were none.** `(a + b atan(c x))^2/(d + i c d x)^2` ran out of time, and so did seven more of @@ -297,6 +342,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 | +### An arcoth of a linear is integrated as its artanh plus a constant + +**Answers where there were none.** `acoth(x)^2/(1 - x^2)^2`, Rubi's 7.4.1, was declined, where +`(atanh(x) + k)^2/(1 - x^2)^2` is answered. `acoth(L) - atanh(L)` is `i pi/2` where `|L| < 1` and `-i pi/2` where `|L| > 1`: +constant on each interval, and not the same constant on each. Such an integrand is now asked with a symbol `K` for it, +as one in `atanh(L) + K`, and the answer is written back with `K = acoth(L) - atanh(L)`, so that it holds on every +interval without a case for each. Only where nothing else answers the integrand as written +([#233](https://github.com/asc-community/AngouriMath/issues/233)). + +| Input | Was (2.5.0) | Now | +|---|---|---| +| `"acoth(x)^2/(1-x^2)^2".ToEntity().Integrate("x")` | `integral(...)` | 1,304 characters, with `(ln((x + 1)/(x - 1)) - ln((1 + x)/(1 - x)))/2` for `K` | +| `"acoth(c*x)^2/(1-c^2*x^2)^2".ToEntity().Integrate("x")` | `integral(...)` | 1,681 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 diff --git a/Sources/AngouriMath/Functions/Continuous/Integration/IndefiniteIntegralSolver.cs b/Sources/AngouriMath/Functions/Continuous/Integration/IndefiniteIntegralSolver.cs index 31dc3156e..a01e6cfe6 100644 --- a/Sources/AngouriMath/Functions/Continuous/Integration/IndefiniteIntegralSolver.cs +++ b/Sources/AngouriMath/Functions/Continuous/Integration/IndefiniteIntegralSolver.cs @@ -810,6 +810,104 @@ private static bool IsADerivativeOfAFactorBelowTheBar(Entity expr, Entity.Variab return (constant * (MathS.Pow(x, raised) * logarithm - 2 * slope * integral) / raised).InnerSimplified; } + /// + /// A + B F(a x) for F the inverse hyperbolic sine, cosine or tangent, over a + /// half-odd power Q^(n + 1/2) of Q = d + e x^2, n >= 1, by parts against + /// F. Q^(-n - 1/2) is ((x Q^(1/2 - n))' + (2n - 2) Q^(1/2 - n))/((2n - 1) d), so + /// its antiderivative is S = x sum alpha_k Q^(1/2 - k), k = 1..n; and the logarithm + /// F is written as has the derivative 2a/(1 - a^2 x^2), a/sqrt(a^2 x^2 + 1) or + /// a/sqrt(a^2 x^2 - 1), even in x. So the remainder F' S is x times a + /// function of x^2, and under w = x^2 each of its terms is a power of the linear + /// d + e w over 1 - a^2 w or the root of another linear, closed by rationalising. + /// Rubi's 7.1.4, 7.2.4, 7.2.5 and 7.3.4, atanh(a x)/(c + d x^2)^(9/2) and the like. + /// https://github.com/asc-community/AngouriMath/issues/233 + /// + /// + /// By parts in the search differentiates the logarithm as written, and the remainder's + /// numerator, a (1 - a x) + a (1 + a x) uncollected, was split into terms even in + /// x, each a harder question than the whole: from (c + d x^2)^(7/2) on they were + /// declined or ran out of time. Asked whole the remainder in w ran out of time too, and + /// each term of it is answered in half a second at most. Not where Q is the function's own quadratic, which the + /// substitution under the hyperbolic function answers; and only an argument a x, which + /// keeps the remainder odd. The identities hold for real x on every branch the + /// logarithms take, the cuts included, where each is a constant i pi off its modulus. + /// + internal static Entity? SolveByPartsAgainstAnInverseHyperbolicOverAHalfOddPowerOfAQuadratic(Entity expr, Entity.Variable x, bool integrateByParts) + { + if (!Integration.AnsweringTheQuestionAskedOrOneBelow) + return null; + var logarithms = expr.Nodes.Where(node => node is Logf(var logBase, var of) && logBase == MathS.e && of.ContainsNode(x)).Distinct().ToList(); + if (logarithms.Count != 1 || logarithms[0] is not Logf(_, var antilogarithm) + || ReadAnInverseHyperbolic(antilogarithm, x) is not var (argument, kind) || kind > 3 + || !TreeAnalyzer.TryGetPolyLinear(argument, x, out var a, out var offset) || a is null + || TreeAnalyzer.IsZero(a) || offset is { } && !TreeAnalyzer.IsZero(offset)) + return null; + var logarithm = logarithms[0]; + Entity constant = Number.Integer.One; + Entity? function = null, quadratic = null; + Number.Rational? power = null; + foreach (var (factor, underneath) in FactorsOfTheIntegrand(expr)) + { + if (!factor.ContainsNode(x)) + { + constant = underneath ? constant / factor : constant * factor; + continue; + } + if (!underneath && function is null && factor.ContainsNode(logarithm)) + { + function = factor; + continue; + } + if (quadratic is not null || factor is not Powf(var @base, Number.Rational exponent) || exponent is Number.Integer) + return null; + (quadratic, power) = (@base, underneath ? exponent : (Number.Rational)(-exponent)); + } + // A half-odd power n + 1/2 below the bar, n >= 1. + if (function is null || quadratic is null || power is null + || !power.ERational.Denominator.Equals(EInteger.FromInt32(2)) || power.ERational.Numerator.CompareTo(EInteger.FromInt32(3)) < 0 + || !power.ERational.Numerator.CanFitInInt32()) + return null; + var n = (power.ERational.Numerator.ToInt32Unchecked() - 1) / 2; + if (!TreeAnalyzer.TryGetPolyQuadratic(quadratic, x, out var e, out var linear, out var d) + || e is null || d is null || linear is { } && !TreeAnalyzer.IsZero(linear) || VanishesIdentically(e) || VanishesIdentically(d)) + return null; + // The function linear in its logarithm: A' + B' ln(...). + var symbol = Variable.CreateUnique(expr, "w_inv"); + if (!TreeAnalyzer.TryGetPolyLinear(function.Substitute(logarithm, symbol), symbol, out var slope, out var free) + || slope is null || slope.ContainsNode(x) || free is { } && free.ContainsNode(x) || VanishesIdentically(slope)) + return null; + var w = Variable.CreateUnique(expr + symbol, "w_sq"); + var squared = MathS.Sqr(a); + var (own, derivativeInW) = kind switch + { + 1 => (1 + squared * MathS.Sqr(x), a / MathS.Sqrt(squared * w + 1)), + 2 => (squared * MathS.Sqr(x) - 1, a / MathS.Sqrt(squared * w - 1)), + _ => (1 - squared * MathS.Sqr(x), 2 * a / (1 - squared * w)), + }; + if (AreProportionalAtSampledPoints(quadratic, own, x)) + return null; + // alpha_k of S_n by S_n = x Q^(1/2 - n)/((2n - 1) d) + (2n - 2)/((2n - 1) d) S_(n - 1). + var alphas = new Entity[n + 1]; + for (var m = 1; m <= n; m++) + { + for (var k = 1; k < m; k++) + alphas[k] = (2 * m - 2) * alphas[k] / ((2 * m - 1) * d); + alphas[m] = Number.Integer.One / ((2 * m - 1) * d); + } + Entity antiderivative = Number.Integer.Zero, remainder = Number.Integer.Zero; + for (var k = 1; k <= n; k++) + { + var alpha = alphas[k].InnerSimplified; + antiderivative += alpha * x * MathS.Pow(quadratic, Number.Rational.Create(1 - 2 * k, 2)); + var term = MathS.Pow(d + e * w, Number.Rational.Create(1 - 2 * k, 2)) * derivativeInW; + if (Integration.ComputeAsAQuestionOfItsOwn(term, w, integrateByParts) is not { } integrated + || integrated.Nodes.Any(node => node is Integralf || node == MathS.NaN)) + return null; + remainder += alpha / 2 * integrated.Substitute(w, MathS.Sqr(x)); + } + return constant * (function * antiderivative - slope * remainder); + } + /// /// A power of a quotient of two linears beside whole powers of the two, under the Moebius /// substitution t = (p + q x)/(r + s x): then p + q x = t (r + s x), @@ -11757,6 +11855,70 @@ node is Powf(var q, Number.Rational r) && r is not Number.Integer && q != first return answer; } + /// + /// x^m arsech(sqrt(x)) or x^m arcsch(sqrt(x)), m >= 0, by parts. The + /// logarithm ln(p + s) each is written as, p = 1/sqrt(x) and s = sqrt(1/x -+ 1), + /// has the derivative -p^3/(2 s), since p' = -p^3/2 and s^2 = p^2 -+ 1; so the + /// remainder is integral x^m p/s/(2 (m + 1)). And K = (p/s) sqrt(r), r = 1 -+ x, + /// has K^2 = 1: it is 1 or -1, constant where it is continuous, and the remainder is + /// K integral x^m/sqrt(r), a polynomial times the root, with K written as it is + /// defined. Rubi's 7.5.2 and 7.6.2. + /// https://github.com/asc-community/AngouriMath/issues/233 + /// + /// + /// Under u = sqrt(x) the substitution asked 2 u^7 arsech(u) one level down, where + /// the rules that answer it at the top decline, and x^3 arsech(sqrt(x)) ran out of time; + /// x^2 arsech(sqrt(x)) took thirteen seconds and x^3 arcsch(sqrt(x)) twenty-four. For the + /// secant's K is -1 where x < 0, and for the cosecant's where -1 < x < 0: left + /// out, the answer is wrong there, as the one the substitution gave x arcsch(sqrt(x)) was. + /// + internal static Entity? SolveByPartsAgainstAnInverseHyperbolicSecantOfARoot(Entity expr, Entity.Variable x, bool integrateByParts) + { + Entity constant = Number.Integer.One; + Logf? logarithm = null; + var power = 0; + foreach (var (factor, underneath) in FactorsOfTheIntegrand(expr)) + { + if (!factor.ContainsNode(x)) + { + constant = underneath ? constant / factor : constant * factor; + continue; + } + if (!underneath && logarithm is null && factor is Logf ln && ln.Base == MathS.e) + { + logarithm = ln; + continue; + } + if (APowerOfTheVariable(factor, x) is not { } k) + return null; + power += underneath ? -k : k; + } + if (logarithm is null || power < 0 || power > 24 || logarithm.Antilogarithm is not Sumf(var left, var right)) + return null; + // p = 1/sqrt(x) and s = sqrt(1/x -+ 1), compared by value, in whatever spelling the parser gave them. + var reciprocalRoot = 1 / MathS.Sqrt(x); + foreach (var (p, s) in new[] { (left, right), (right, left) }) + { + if (!TryReadAHalfPower(s, out var radicand, out var numerator) || numerator != 1 + || !Functions.PartialFractions.HoldsAtSampledPoints(p, reciprocalRoot, x)) + continue; + int sign; + if (Functions.PartialFractions.HoldsAtSampledPoints(radicand, 1 / x - 1, x)) sign = -1; + else if (Functions.PartialFractions.HoldsAtSampledPoints(radicand, 1 / x + 1, x)) sign = 1; + else continue; + var r = 1 + sign * x; + var raised = power + 1; + var below = power == 0 ? 1 / MathS.Sqrt(r) : (power == 1 ? x : MathS.Pow(x, power)) / MathS.Sqrt(r); + if (Integration.ComputeAsAQuestionOfItsOwn(below, x, integrateByParts) is not { } integral + || integral.Nodes.Any(node => node is Integralf || node == MathS.NaN)) + return null; + var k = p / s * MathS.Sqrt(r); + var answer = constant * (MathS.Pow(x, raised) * logarithm + k * integral / 2) / raised; + return Functions.PartialFractions.DerivativeHoldsAtSampledPoints(answer, expr, x) ? answer : null; + } + return null; + } + /// /// An integrand holding an inverse hyperbolic function of a linear in the variable, /// integrated by the substitution that undoes it. The inverse hyperbolic functions @@ -11979,6 +12141,82 @@ node is Powf(var @base, Number.Rational power) && power is not Number.Integer && return back; } + /// + /// An inverse hyperbolic cotangent of a linear L as its tangent plus a constant: + /// arcoth(L) - artanh(L) is 1/2 (ln(-q) - ln(q)) for q = (1 + L)/(1 - L), + /// which is i pi/2 where |L| < 1 and -i pi/2 where |L| > 1 -- + /// constant on each interval where both are continuous, and not the same constant on each. + /// With a symbol K for it the integrand is one in artanh(L) + K, which the + /// rules for the inverse hyperbolic tangent read; the answer is written back with + /// K = arcoth(L) - artanh(L), whose derivative is zero wherever it has one, so that + /// it is right on every interval without a case for each. + /// https://github.com/asc-community/AngouriMath/issues/233 + /// + /// + /// The parser writes arcoth(L) as 1/2 ln((L + 1)/(L - 1)), which the + /// substitution under L = tanh(u) does not read as an inverse function: Rubi's 7.4.1 + /// acoth(x)^2/(1 - x^2)^2 was declined in half a second where + /// (atanh(x) + k)^2/(1 - x^2)^2 is answered in a quarter of one. Last, after by parts, + /// so that every arcoth answered as written keeps its answer; at the top only, since it asks + /// the whole question again. + /// + internal static Entity? SolveByWritingAnArcothAsAnArtanhPlusAConstant(Entity expr, Entity.Variable x, bool integrateByParts) + { + if (!Integration.AnsweringTheQuestionAsked || writingAnArcothAsAnArtanh) + return null; + var back = new List<(Entity.Variable Constant, Entity Value)>(); + var rewritten = expr; + foreach (var node in expr.Nodes.Where(node => node is Logf(var @base, _) && @base == MathS.e).Distinct().ToList()) + { + if (node is not Logf(_, Divf(var above, var below)) || ArgumentOfAnArcoth(above, below, x) is not { } argument) + continue; + var constant = Variable.CreateUnique(rewritten, "k_coth"); + var tangent = MathS.Ln((1 + argument) / (1 - argument)); + // ln((L + 1)/(L - 1)) is 2 arcoth(L), and 2 artanh(L) + 2 K. + rewritten = rewritten.Replace(inside => inside == node ? tangent + 2 * constant : inside); + back.Add((constant, (node - tangent) / 2)); + } + if (back.Count == 0) + return null; + Entity? answer; + writingAnArcothAsAnArtanh = true; + try + { + answer = Integration.ComputeAsAQuestionOfItsOwn(rewritten.InnerSimplified, x, integrateByParts); + } + finally + { + writingAnArcothAsAnArtanh = false; + } + if (answer is null || answer.Nodes.Any(node => node is Integralf)) + return null; + foreach (var (constant, value) in back) + answer = answer.Substitute(constant, value); + // The identities the answer in K rests on were checked for a real K, and K is imaginary. + return Functions.PartialFractions.DerivativeHoldsAtSampledPoints(answer, expr, x) ? answer : null; + } + + /// Whether is asking already. + [System.ThreadStatic] private static bool writingAnArcothAsAnArtanh; + + /// + /// The L of ln((L + 1)/(L - 1)), the logarithm the parser writes + /// 2 arcoth(L) as, for a linear L: as written where it is written so, and + /// otherwise read off the two linears, which share their slope and whose offsets are two apart. + /// + private static Entity? ArgumentOfAnArcoth(Entity above, Entity below, Entity.Variable x) + { + if (above is Sumf(var argument, var one) && one == Number.Integer.One + && below is Minusf(var again, var minusOne) && minusOne == Number.Integer.One && again == argument) + return TreeAnalyzer.TryGetPolyLinear(argument, x, out var slope, out _) && slope is { } && !TreeAnalyzer.IsZero(slope) ? argument : null; + if (!TreeAnalyzer.TryGetPolyLinear(above, x, out var slopeAbove, out var offsetAbove) || slopeAbove is null || offsetAbove is null + || !TreeAnalyzer.TryGetPolyLinear(below, x, out var slopeBelow, out var offsetBelow) || slopeBelow is null || offsetBelow is null + || TreeAnalyzer.IsZero(slopeAbove) || !VanishesIdentically(slopeAbove - slopeBelow) || !VanishesIdentically(offsetAbove - offsetBelow - 2)) + return null; + var offset = (offsetAbove - 1).InnerSimplified; + return TreeAnalyzer.IsZero(offset) ? slopeAbove * x : slopeAbove * x + offset; + } + /// /// The m for which is x^m times a function of /// alone, else null: with the inverse written as a symbol, such an @@ -29064,6 +29302,92 @@ static bool AreEqualAsPolynomials(Entity left, Entity right) } } + /// + /// A polynomial P of degree m times a function G of one logarithm of a + /// quotient of linears, over the power L^k of a linear proportional to one of them, + /// with m <= k - 2: P written in powers of L at its root, + /// P = p_0 + p_1 L + ... + p_m L^m, makes the integrand sum p_j G/L^(k - j) + /// exactly, and each term is a power of one of the logarithm's linears beside a function of + /// it, which answers under the + /// quotient. Rubi's 7.3.4 x (a + b atanh(c x))^2/(d + c d x)^3 is + /// ((a + b atanh(c x))^2/(d + c d x)^2 - d (a + b atanh(c x))^2/(d + c d x)^3)/(c d). + /// https://github.com/asc-community/AngouriMath/issues/233 + /// + /// + /// The rule for a rational function times a power of a logarithm took it by parts, and the + /// remainder ran past the budget. Not where a term would be over L alone, a + /// dilogarithm nothing here answers, and not where G is the logarithm's first power, + /// which that rule answers in one step and keeps the answer it gives. + /// + internal static Entity? SolveByDividingAPolynomialByAPowerOfALinearOfTheLogarithm(Entity expr, Entity.Variable x, bool integrateByParts) + { + if (!Integration.AnsweringTheQuestionAskedOrOneBelow) + return null; + Entity polynomial = Number.Integer.One, rest = Number.Integer.One, constant = Number.Integer.One; + Entity? linear = null; + var power = 0; + foreach (var (factor, underneath) in FactorsOfTheIntegrand(expr)) + { + if (!factor.ContainsNode(x)) + { + constant = underneath ? constant / factor : constant * factor; + continue; + } + if (underneath) + { + // One power of one linear below the bar, and nothing else of x. + if (linear is not null || factor is not Powf(var @base, Number.Integer { EInteger: var whole }) + || !whole.CanFitInInt32() || whole.ToInt32Unchecked() < 3 + || !TreeAnalyzer.TryGetPolyLinear(@base, x, out var slope, out _) || slope is null || TreeAnalyzer.IsZero(slope)) + return null; + (linear, power) = (@base, whole.ToInt32Unchecked()); + continue; + } + if (TreeAnalyzer.TryGetPolynomial(factor, x, out var read) && read.Keys.All(degree => degree.Sign >= 0) + && read.Values.All(coefficient => !coefficient.ContainsNode(x))) + polynomial *= factor; + else + rest *= factor; + } + if (linear is null || !rest.ContainsNode(x) + || !TreeAnalyzer.TryGetPolynomial(polynomial, x, out var readPolynomial) || readPolynomial.Count == 0) + return null; + var degree = readPolynomial.Keys.Max()!; + if (degree.Sign <= 0 || !degree.CanFitInInt32() || degree.ToInt32Unchecked() > power - 2) + return null; + // The rest a function of one logarithm of a quotient of linears, one of them L up to a + // constant, and not its first power. + var logarithms = rest.Nodes.Where(node => node is Logf(var logBase, var of) && logBase == MathS.e && of.ContainsNode(x)).Distinct().ToList(); + if (logarithms.Count != 1 || logarithms[0] is not Logf(_, Divf(var above, var below)) + || !TreeAnalyzer.TryGetPolyLinear(above, x, out _, out _) || !TreeAnalyzer.TryGetPolyLinear(below, x, out _, out _) + || !AreProportionalAtSampledPoints(linear, above, x) && !AreProportionalAtSampledPoints(linear, below, x)) + return null; + var w = Variable.CreateUnique(expr, "w_log"); + var inW = rest.Substitute(logarithms[0], w); + if (inW.ContainsNode(x) || TreeAnalyzer.TryGetPolynomial(inW, w, out var inTheLogarithm) + && inTheLogarithm.Keys.All(key => key.CompareTo(EInteger.One) <= 0)) + return null; + if (!TreeAnalyzer.TryGetPolyLinear(linear, x, out var beta, out var alpha) || beta is null) + return null; + var root = Functions.PartialFractions.Bare((-(alpha ?? Number.Integer.Zero) / beta).InnerSimplified); + var count = degree.ToInt32Unchecked() + 1; + if (Functions.PartialFractions.TaylorCoefficientsAtTheRoot(polynomial, root, count, x) is not { } coefficients) + return null; + Entity total = Number.Integer.Zero; + for (var order = 0; order < count; order++) + { + if (Functions.PartialFractions.IsZeroAsAValue(coefficients[order])) + continue; + var coefficient = Functions.PartialFractions.InLowestTermsOverTheSymbols(coefficients[order] / MathS.Pow(beta, order)); + var piece = rest / MathS.Pow(linear, Number.Integer.Create(power - order)); + if (Integration.ComputeAsAQuestionOfItsOwn(piece, x, integrateByParts) is not { } integrated + || integrated.Nodes.Any(node => node is Integralf)) + return null; + total += coefficient * integrated; + } + return constant * total; + } + /// /// A rational function of times a whole power of a factor whose /// derivative is rational -- A + B ln(R) or A + B arctan(R) with R diff --git a/Sources/AngouriMath/Functions/Continuous/Integration/Integration.Definition.cs b/Sources/AngouriMath/Functions/Continuous/Integration/Integration.Definition.cs index b771c2129..0152781f4 100644 --- a/Sources/AngouriMath/Functions/Continuous/Integration/Integration.Definition.cs +++ b/Sources/AngouriMath/Functions/Continuous/Integration/Integration.Definition.cs @@ -1101,6 +1101,10 @@ private static Entity WithItsPiecewiseSumsCombined(Entity expr) // where the rule below went step by step with the coefficients growing to // b^205, and the search after it went past its budget. if ((answer = IndefiniteIntegralSolver.SolveByTheQuotientOfTheLogarithmsLinears(expr, x, integrateByParts)) is { }) return answer; + // And a polynomial over a power of one of those linears, written in powers of it first: + // `x (a + b atanh(c x))^2/(d + c d x)^3` is two terms the rule above answers, where the + // rule after it took the whole by parts and the remainder ran past the budget. + if ((answer = IndefiniteIntegralSolver.SolveByDividingAPolynomialByAPowerOfALinearOfTheLogarithm(expr, x, integrateByParts)) is { }) return answer; if ((answer = IndefiniteIntegralSolver.SolveARationalFunctionTimesAPowerOfALogarithm(expr, x, integrateByParts)) is { }) return answer; // A rational function of the hyperbolic tangent with a symbol among its // coefficients, by u = tanh(y): Rubi's `1/(a + b coth(c + d x)^2)^2` is a symbolic @@ -1190,6 +1194,10 @@ private static Entity WithItsPiecewiseSumsCombined(Entity expr) // before the substitution search, which reads `x^(2n)` as a power of `x^n` only for a // number `n`. if ((answer = IndefiniteIntegralSolver.SolveByAPowerOfXWithASymbolicExponent(expr, x, integrateByParts)) is { }) return answer; + // A power of x times the inverse hyperbolic secant or cosecant of `sqrt(x)`, by parts onto + // `x^m/sqrt(1 -+ x)`: before the substitution search, whose `u = sqrt(x)` asked the + // question in u one level down, where it ran out of time. + if ((answer = IndefiniteIntegralSolver.SolveByPartsAgainstAnInverseHyperbolicSecantOfARoot(expr, x, integrateByParts)) is { }) return answer; if ((answer = IndefiniteIntegralSolver.SolveBySubstitution(expr, x, integrateByParts)) is { }) return answer; // A constant times a whole power of something of x under a power that is not whole, the // whole power taken out with the factor constant where it is not zero: @@ -1542,6 +1550,10 @@ private static Entity WithItsPiecewiseSumsCombined(Entity expr) // where it stands: `cos(x) sgn(sin(x))` is `|sin(x)|` by the substitution, and // `sgn(sin(x)) sin(x)` with the sign taken out first. if ((answer = IndefiniteIntegralSolver.SolveByTakingASignOut(expr, x, integrateByParts)) is { }) return answer; + // An inverse hyperbolic function of `a x` over a half-odd power of an even quadratic, by + // parts with the logarithm's derivative read off whole: the search's own parts split the + // remainder into terms harder than it, and past `(c + d x^2)^(7/2)` ran out of time. + if ((answer = IndefiniteIntegralSolver.SolveByPartsAgainstAnInverseHyperbolicOverAHalfOddPowerOfAQuadratic(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. @@ -1553,6 +1565,11 @@ private static Entity WithItsPiecewiseSumsCombined(Entity expr) // A rational function of x and ln(x) over distinct linears in the logarithm, by partial // fractions in it, where nothing has answered it whole: `(2 L^2 - L - x^2)/(L^3 - x^2 L)`. if ((answer = IndefiniteIntegralSolver.SolveARationalFunctionOfTheLogarithmByPartialFractionsInIt(expr, x, integrateByParts)) is { }) return answer; + // An arcoth of a linear as its artanh plus a constant, last of all, so that every arcoth + // answered as written keeps that answer: `acoth(x)^2/(1 - x^2)^2` is + // `(atanh(x) + K)^2/(1 - x^2)^2` with `K = acoth(x) - atanh(x)`, constant where it is + // continuous, which the substitution under the hyperbolic tangent answers. + if ((answer = IndefiniteIntegralSolver.SolveByWritingAnArcothAsAnArtanhPlusAConstant(expr, x, integrateByParts)) is { }) return answer; return null; } /// diff --git a/Sources/Tests/UnitTests/Calculus/APolynomialOverAPowerOfALinearOfTheLogarithmIntegralTest.cs b/Sources/Tests/UnitTests/Calculus/APolynomialOverAPowerOfALinearOfTheLogarithmIntegralTest.cs new file mode 100644 index 000000000..8e9e3b22b --- /dev/null +++ b/Sources/Tests/UnitTests/Calculus/APolynomialOverAPowerOfALinearOfTheLogarithmIntegralTest.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 polynomial times a function of atanh(c x) or acoth(c x) over a power of + /// d (1 +- c x), written in powers of the linear first: x (a + b atanh(c x))^2/(d + c d x)^3 + /// is two terms answered under t = (1 + c x)/(1 - c x), where by parts on the whole ran out of + /// time. Rubi's 7.3.4. + /// #233 + /// + [Trait("Area", "Calculus")] + public sealed class APolynomialOverAPowerOfALinearOfTheLogarithmIntegralTest + { + [Theory] + [InlineData("x*(a+b*atanh(c*x))^2/(d+c*d*x)^3")] + [InlineData("x*(a+b*atanh(c*x))^3/(1+c*x)^4")] + [InlineData("x^2*(a+b*atanh(c*x))^2/(1+c*x)^4")] + [InlineData("(2+x)*(a+b*atanh(c*x))^2/(1+c*x)^3")] + [InlineData("x*(a+b*atanh(c*x))^2/(d-c*d*x)^3")] + [InlineData("x*(a+b*acoth(c*x))^2/(d+c*d*x)^3")] + public void WrittenInPowersOfTheLinear(string integrand) + { + var integral = integrand.ToEntity().Integrate("x"); + var text = integral.Stringize(); + Assert.DoesNotContain("integral(", text); + Assert.True(text.Length < 20000, $"{text.Length} characters of answer for {integrand}"); + 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, -1.2, -0.7, 0.3, 0.8, 1.3, 2.9 }) + { + 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}"); + } + } +} diff --git a/Sources/Tests/UnitTests/Calculus/AnArcothAsAnArtanhPlusAConstantIntegralTest.cs b/Sources/Tests/UnitTests/Calculus/AnArcothAsAnArtanhPlusAConstantIntegralTest.cs new file mode 100644 index 000000000..156022887 --- /dev/null +++ b/Sources/Tests/UnitTests/Calculus/AnArcothAsAnArtanhPlusAConstantIntegralTest.cs @@ -0,0 +1,53 @@ +// +// 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 +{ + /// + /// An inverse hyperbolic cotangent of a linear as its tangent plus K = acoth(L) - atanh(L), + /// which is i pi/2 inside (-1, 1) and -i pi/2 outside it, constant on each side: + /// acoth(x)^2/(1 - x^2)^2 was declined where (atanh(x) + k)^2/(1 - x^2)^2 is answered. + /// Rubi's 7.4.1. The points are on both sides of c x = 1 and of c x = -1. + /// #233 + /// + [Trait("Area", "Calculus")] + public sealed class AnArcothAsAnArtanhPlusAConstantIntegralTest + { + [Theory] + [InlineData("acoth(x)^2/(1-x^2)^2")] + [InlineData("acoth(c*x)^2/(1-c^2*x^2)^2")] + [InlineData("acoth(x)^3/(1-x^2)^2")] + [InlineData("acoth(x)^2/(1-x^2)^2+acoth(x)/(1-x^2)")] + public void OnBothSidesOfOne(string integrand) + { + var integral = integrand.ToEntity().Integrate("x"); + var text = integral.Stringize(); + Assert.DoesNotContain("integral(", text); + Assert.True(text.Length < 5000, $"{text.Length} characters of answer for {integrand}"); + Entity Pinned(Entity e) => e.Substitute("c", 0.7); + var derivative = Pinned(integral.Substitute("C", 0)).Differentiate("x"); + var original = Pinned(integrand.ToEntity()); + var compared = 0; + foreach (var at in new[] { -2.9, -1.3, -0.7, 0.3, 0.8, 1.3, 2.9 }) + { + 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}"); + } + } +} diff --git a/Sources/Tests/UnitTests/Calculus/AnInverseHyperbolicOverAHalfOddPowerOfAQuadraticIntegralTest.cs b/Sources/Tests/UnitTests/Calculus/AnInverseHyperbolicOverAHalfOddPowerOfAQuadraticIntegralTest.cs new file mode 100644 index 000000000..f4aca1e76 --- /dev/null +++ b/Sources/Tests/UnitTests/Calculus/AnInverseHyperbolicOverAHalfOddPowerOfAQuadraticIntegralTest.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 + B F(a x), F an inverse hyperbolic sine, cosine or tangent, over a half-odd + /// power of c + d x^2, by parts against F, the remainder asked a term at a time under + /// w = x^2: from (c + d x^2)^(7/2) on, by parts in the search was declined or ran out of + /// time. Rubi's 7.1.4, 7.2.4, 7.2.5 and 7.3.4. + /// #233 + /// + [Trait("Area", "Calculus")] + public sealed class AnInverseHyperbolicOverAHalfOddPowerOfAQuadraticIntegralTest + { + [Theory] + [InlineData("atanh(a*x)/(c+d*x^2)^(9/2)")] + [InlineData("acosh(a*x)/(c+d*x^2)^(9/2)")] + [InlineData("acosh(a*x)/(c+d*x^2)^(7/2)")] + [InlineData("(a+b*asinh(c*x))/(d+k*x^2)^(7/2)")] + [InlineData("(a+b*acosh(c*x))/(d+k*x^2)^(7/2)")] + [InlineData("atanh(a*x)/(c+d*x^2)^(3/2)")] + public void ByPartsOntoTermsInTheSquare(string integrand) + { + var integral = integrand.ToEntity().Integrate("x"); + var text = integral.Stringize(); + Assert.DoesNotContain("integral(", text); + Assert.True(text.Length < 20000, $"{text.Length} characters of answer for {integrand}"); + Entity Pinned(Entity e) => e.Substitute("a", 1.3).Substitute("b", 0.7).Substitute("c", 0.4).Substitute("d", 0.3).Substitute("k", 0.9); + var derivative = Pinned(integral.Substitute("C", 0)).Differentiate("x"); + var original = Pinned(integrand.ToEntity()); + var compared = 0; + foreach (var at in new[] { -2.9, -1.2, -0.7, 0.3, 0.8, 1.3, 2.9 }) + { + 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}"); + } + } +} diff --git a/Sources/Tests/UnitTests/Calculus/AnInverseHyperbolicSecantOfARootByPartsIntegralTest.cs b/Sources/Tests/UnitTests/Calculus/AnInverseHyperbolicSecantOfARootByPartsIntegralTest.cs new file mode 100644 index 000000000..404eeb9b2 --- /dev/null +++ b/Sources/Tests/UnitTests/Calculus/AnInverseHyperbolicSecantOfARootByPartsIntegralTest.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 +{ + /// + /// x^m arsech(sqrt(x)) and x^m arcsch(sqrt(x)) by parts onto x^m/sqrt(1 -+ x), + /// with K = sqrt(1 -+ x)/(sqrt(x) sqrt(1/x -+ 1)), which is 1 or -1, beside the remainder. + /// x^3 arsech(sqrt(x)) ran out of time, and the answer the substitution gave x arcsch(sqrt(x)) + /// was wrong for negative x, which the points here include. Rubi's 7.5.2 and 7.6.2. + /// #233 + /// + [Trait("Area", "Calculus")] + public sealed class AnInverseHyperbolicSecantOfARootByPartsIntegralTest + { + [Theory] + [InlineData("x^3*asech(sqrt(x))")] + [InlineData("x^2*asech(sqrt(x))")] + [InlineData("asech(sqrt(x))")] + [InlineData("x^3*acsch(sqrt(x))")] + [InlineData("x*acsch(sqrt(x))")] + [InlineData("acsch(sqrt(x))")] + public void OnEverySideOfZeroAndMinusOne(string integrand) + { + var integral = integrand.ToEntity().Integrate("x"); + var text = integral.Stringize(); + Assert.DoesNotContain("integral(", text); + Assert.True(text.Length < 5000, $"{text.Length} characters of answer for {integrand}"); + Entity Pinned(Entity e) => e; + var derivative = Pinned(integral.Substitute("C", 0)).Differentiate("x"); + var original = Pinned(integrand.ToEntity()); + var compared = 0; + foreach (var at in new[] { -2.9, -1.3, -0.4, 0.3, 0.8, 1.3, 2.9 }) + { + 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}"); + } + } +}