diff --git a/BREAKING-CHANGES.md b/BREAKING-CHANGES.md index 531381a24..88eeae61c 100644 --- a/BREAKING-CHANGES.md +++ b/BREAKING-CHANGES.md @@ -678,6 +678,21 @@ was declined. Each of its terms alone is not elementary, and the sum is the deri |---|---|---| | `"e^(a+b*x)*sinh(c+d*x)^(3/2)-3/4*e^(a+b*x)*d^2/((b^2-9/4*d^2)*sqrt(sinh(c+d*x)))".ToEntity().Integrate("x")` | `integral(...)` | 249 characters | +### Under the half tangent, a factor the parts share is written out + +**Answers where there were none, shorter than master's.** Rubi's 6.7.1 `sech(x)^2/(a + c sech(x) + b tanh(x))` ran past +the corpus's budget. Master answered it in 1,383,216 characters after twenty-two seconds. Under `u = tanh(x/2)` the parts +of the integrand are homogenised apart, and a factor they share stayed hidden inside the expanded quartic of one of them: +`(1 + u^2)^2` over `(1 + u^2)^2` times a quartic that holds `1 + u^2` once more. The factors `1 + u^2`, `1 - u^2`, `1 - u`, `1 + u` +and `u`, which the homogenising makes, are now divided out of the written factors of the denominator and out of the +numerator, and the integrand is `1/((1 + u^2) Q(u))` for a quadratic Q +([#718](https://github.com/asc-community/AngouriMath/issues/718)). + +| Input | Was (2.5.0) | Now | +|---|---|---| +| `"sech(x)^2/(a+c*sech(x)+b*tanh(x))".ToEntity().Integrate("x")` | `integral(...)` | 2,931 characters, where before this change master gave 1,383,216 | +| `"csch(x)^2/(a+c*csch(x)+b*coth(x))".ToEntity().Integrate("x")` | `integral(...)` | 1,766 characters, where before this change master gave 1,690 | + ### 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/Algebra/Polynomials/PartialFractions.cs b/Sources/AngouriMath/Functions/Algebra/Polynomials/PartialFractions.cs index a920387b4..6243fcad4 100644 --- a/Sources/AngouriMath/Functions/Algebra/Polynomials/PartialFractions.cs +++ b/Sources/AngouriMath/Functions/Algebra/Polynomials/PartialFractions.cs @@ -882,6 +882,87 @@ private static Entity WithThePrimitiveDenominator(Entity quotient) return primitive.IsConstant ? scaledTop : scaledTop / primitive.ToEntity(variables); } + /// + /// over , a product of powers as + /// written, with each of divided out of every written factor of + /// the denominator as often as it goes exactly and written as a power of its own, and then + /// out of the numerator as often as it goes there too. Null where no candidate divides a + /// factor it is not already, where none then divides the numerator, or where a side does not + /// read as a polynomial over the rationals. + /// + /// + /// A substitution homogenises the parts of an integrand apart, and a factor they share is + /// then hidden inside the expanded quartic of one of them: under u = tanh(x/2), + /// sech(x)^2/(a + c sech(x) + b tanh(x)) is (1 + u^2)^2 over (1 + u^2)^2 + /// times a quartic that holds 1 + u^2 once more. Written out, it is 1/((1 + u^2) Q(u)) + /// with Q a quadratic, which the rational rules read. + /// https://github.com/asc-community/AngouriMath/issues/718 + /// + internal static Entity? WithTheCandidateFactorsWrittenOut(Entity numerator, Entity denominator, IReadOnlyList candidates) + { + var variables = numerator.Vars.Concat(denominator.Vars).Concat(candidates.SelectMany(c => c.Vars)).Distinct() + .OrderBy(v => v.Name, System.StringComparer.Ordinal).ToArray(); + if (variables.Length == 0 || variables.Length > MultivariatePolynomial.MaxVariables) + return null; + var indices = new Dictionary(variables.Length); + for (var i = 0; i < variables.Length; i++) + indices[variables[i]] = i; + var parsed = new List(); + foreach (var candidate in candidates) + if (MultivariatePolynomial.TryParse(candidate, indices) is { IsConstant: false } read) + parsed.Add(read); + else + return null; + var times = new int[parsed.Count]; + var changed = false; + Entity rest = Integer.One; + foreach (var written in Mulf.LinearChildren(denominator)) + { + var (@base, power) = written is Powf(var repeated, Integer { EInteger.Sign: > 0 } raised) && raised.EInteger.CanFitInInt32() + ? (repeated, raised.EInteger.ToInt32Unchecked()) : (written, 1); + if (!@base.Vars.Any() || MultivariatePolynomial.TryParse(@base, indices) is not { IsConstant: false } factor) + { + rest *= written; + continue; + } + var divided = false; + for (var k = 0; k < parsed.Count; k++) + while (factor.DivideExact(parsed[k]) is { } quotient) + { + // A factor that is the candidate itself, to a power, is already written out. + divided |= !quotient.IsConstant; + factor = quotient; + times[k] += power; + } + changed |= divided; + var remaining = factor.ToEntity(variables); + if (remaining != Integer.One) + rest *= power == 1 ? remaining : MathS.Pow(remaining, power); + } + if (!changed) + return null; + if (MultivariatePolynomial.TryParse(numerator, indices) is not { } top) + return null; + // Only where the numerator shares one of them: written out with nothing cancelled, the + // tangent's `(c + d x)^3/(a + a tanh(e + f x))^3` ran past the budget it was answered in. + var cancelled = false; + for (var k = 0; k < parsed.Count; k++) + while (times[k] > 0 && top.DivideExact(parsed[k]) is { } quotient) + { + top = quotient; + times[k]--; + cancelled = true; + } + if (!cancelled) + return null; + Entity below = rest; + for (var k = 0; k < parsed.Count; k++) + if (times[k] > 0) + below *= times[k] == 1 ? candidates[k] : MathS.Pow(candidates[k], times[k]); + var above = top.ToEntity(variables); + return below == Integer.One ? above : above / below; + } + /// /// over , a product of /// powers as written, with every written factor that divides the numerator exactly diff --git a/Sources/AngouriMath/Functions/Continuous/Integration/IndefiniteIntegralSolver.cs b/Sources/AngouriMath/Functions/Continuous/Integration/IndefiniteIntegralSolver.cs index 91b4b1ad6..817c6afcb 100644 --- a/Sources/AngouriMath/Functions/Continuous/Integration/IndefiniteIntegralSolver.cs +++ b/Sources/AngouriMath/Functions/Continuous/Integration/IndefiniteIntegralSolver.cs @@ -34467,6 +34467,15 @@ private static bool IsTheZeroPolynomial(Entity expr) && Mulf.LinearChildren(below).All(factor => !factor.ContainsNode(t) || factor == t || factor is Powf(var b, Number.Integer) && b == t))) continue; var rationalInT = WithThePowersOfOnePlusMinusTCollected(inT / (1 - MathS.Sqr(t))); + // With the factors the parts were homogenised over written out of the rest: `sech(x)^2` + // and `a + c sech(x) + b tanh(x)` are each over `1 + t^2`, and the one in the quartic + // below was hidden, where the rational integrator wrote 1.38 million characters. + // `1 - t^2` before its two linears, so that an integrand even in t is written so: split + // into them, `sinh(u)^8/(a - b sinh(u)^4)^2` took ten times the memory. + if (Functions.SingleQuotient.Of(rationalInT) is var (aboveT, belowT) && belowT.ContainsNode(t) + && Functions.PartialFractions.WithTheCandidateFactorsWrittenOut(aboveT, belowT, + new Entity[] { 1 + MathS.Sqr(t), 1 - MathS.Sqr(t), 1 - t, 1 + t, t }) is { } writtenOut) + rationalInT = writtenOut; if (rationalInT.ContainsNode(square) || rationalInT.ContainsNode(vt) || rationalInT.Nodes.Any(node => node == MathS.NaN) || Integration.ComputeAsAQuestionOfItsOwn(rationalInT, t, integrateByParts) is not { } evenResult) return null; diff --git a/Sources/Tests/UnitTests/Calculus/TheHalfTangentsFactorsWrittenOutIntegralTest.cs b/Sources/Tests/UnitTests/Calculus/TheHalfTangentsFactorsWrittenOutIntegralTest.cs new file mode 100644 index 000000000..b0463c78d --- /dev/null +++ b/Sources/Tests/UnitTests/Calculus/TheHalfTangentsFactorsWrittenOutIntegralTest.cs @@ -0,0 +1,50 @@ +// +// 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 +{ + /// + /// Under u = tanh(x/2) the parts of an integrand are homogenised apart, and a factor they + /// share was left hidden inside the expanded quartic of one of them: Rubi's 6.7.1 + /// sech(x)^2/(a + c sech(x) + b tanh(x)) is (1 + u^2)^2 over (1 + u^2)^2 times a + /// quartic holding 1 + u^2 once more, and the rational integrator wrote 1.38 million + /// characters in twenty-two seconds, past the corpus's budget. Written out, it is + /// 1/((1 + u^2) Q(u)) for a quadratic Q. + /// #718 + /// + [Trait("Area", "Calculus")] + public sealed class TheHalfTangentsFactorsWrittenOutIntegralTest + { + [Theory] + // Rubi 6.7.1:910. + [InlineData("sech(x)^2/(a + c*sech(x) + b*tanh(x))")] + [InlineData("csch(x)^2/(a + c*csch(x) + b*coth(x))")] + public void IsAnsweredShortly(string integrand) + { + var integral = integrand.ToEntity().Integrate("x"); + var text = integral.Stringize(); + Assert.DoesNotContain("integral(", text); + Assert.True(text.Length < 5000, $"{integrand} came back in {text.Length} characters"); + Entity Pinned(Entity e) => e.Substitute("a", 1.3).Substitute("b", 0.7).Substitute("c", 0.4); + var derivative = Pinned(integral.Substitute("C", 0)).Differentiate("x"); + var original = Pinned(integrand.ToEntity()); + foreach (var at in new[] { -2.2, -0.9, 0.3, 1.4, 2.1 }) + { + var want = original.Substitute("x", at).EvalNumerical(); + var got = derivative.Substitute("x", at).EvalNumerical(); + Assert.False(got.IsNaN, $"d/dx of the antiderivative of {integrand} has no value at x = {at}, where the integrand is {want}"); + 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}"); + } + } + } +}