diff --git a/BREAKING-CHANGES.md b/BREAKING-CHANGES.md index 245032122..36814ee25 100644 --- a/BREAKING-CHANGES.md +++ b/BREAKING-CHANGES.md @@ -1329,6 +1329,24 @@ the rules for a root of an even power. to 309 of 422 with five fewer timeouts, and family 6 from 377 to 378; no row is lost in families 1, 4, 5, 6 or 7, and the 1774-problem suite stays at 1707 with no wrong answer. +### A power of `a cos + b sin` with symbolic coefficients is integrated under the tangent + +`sec(x)^2/(a cos(x) + b sin(x))^4` was left as written after ten minutes, where +`sec(x)^2/(2 cos(x) + 3 sin(x))^4` took a tenth of a second. Both are quotients of +homogeneous polynomials in the sine and cosine, which `t = tan(x)` turns into a rational +function, and the rule that does it read the denominator by expanding it first: with numbers +the expanded quartic in `t` is `(2 + 3t)^4` again as soon as the rational integrator factors +it, and with symbols it is `a^4 + 4a^3 b t + 6a^2 b^2 t^2 + 4a b^3 t^3 + b^4 t^4`, which nothing +factors back. A whole power of a homogeneous factor is now read as that power of it, so the +substitution hands on `(1 + t^2)^2/(a + b t)^4`. + +| Input | Was (2.5.0) | Now | +|---|---|---| +| `"sec(x)^2/(a*cos(x)+b*sin(x))^4".Integrate("x")` | left unevaluated | a rational function of `tan(x)` | +| `"cos(x)/(a*cos(x)+b*sin(x))^3".Integrate("x")` | left unevaluated | a rational function of `tan(x)` | + +Rubi's 4.7.2, `trig^m (a trig + b trig)^n`: 199 to 215 of 290, no row lost, 27 timeouts to 20. + ### `binomial(n, k)` is a function **Addition, not silent.** The binomial coefficient is a node, `Entity.Binomialf`, spelled diff --git a/Sources/AngouriMath/Functions/Continuous/Integration/IndefiniteIntegralSolver.cs b/Sources/AngouriMath/Functions/Continuous/Integration/IndefiniteIntegralSolver.cs index d1a5049e0..3bf3b6974 100644 --- a/Sources/AngouriMath/Functions/Continuous/Integration/IndefiniteIntegralSolver.cs +++ b/Sources/AngouriMath/Functions/Continuous/Integration/IndefiniteIntegralSolver.cs @@ -4871,8 +4871,8 @@ node is Logf(var @base, var argument) && !@base.ContainsNode(x) && @base != Math { if (!TryReadAsQuotient(expr, out var numerator, out var denominator)) return null; - if (!TryReadAHomogeneousTrigonometricPolynomial(numerator, x, out var above, out var aboveDegree, out var argument) - || !TryReadAHomogeneousTrigonometricPolynomial(denominator, x, out var below, out var belowDegree, out var otherArgument)) + if (!TryReadAHomogeneousTrigonometricProduct(numerator, x, out var above, out var aboveDegree, out var argument) + || !TryReadAHomogeneousTrigonometricProduct(denominator, x, out var below, out var belowDegree, out var otherArgument)) return null; // A side free of the variable is degree zero and names no argument -- `1/(cos + sin)^2` // is the common case and was declined for it. Only one side may be silent; if both @@ -4903,6 +4903,60 @@ node is Logf(var @base, var argument) && !@base.ContainsNode(x) && @base != Math : null; } + /// + /// over a product, a factor at a + /// time, with a whole power of a factor read as that power of it: the degrees add, and + /// the rewritten factors are multiplied and raised as they were written. + /// + /// + /// Reading the whole product at once expands it first, and an expanded power is a + /// polynomial the rational integrator has to factor again: (a cos(x) + b sin(x))^4 + /// became a^4 + 4 a^3 b t + 6 a^2 b^2 t^2 + 4 a b^3 t^3 + b^4 t^4 under + /// t = tan(x), which with numbers is (2 + 3t)^4 again in a moment and with + /// symbols is a quartic nothing factors -- sec(x)^2/(a cos(x) + b sin(x))^4 ran past + /// ten minutes where (1 + t^2)^2/(a + b t)^4 takes half a second. The whole product is + /// still read at once where a factor on its own is not homogeneous. + /// + private static bool TryReadAHomogeneousTrigonometricProduct( + Entity expr, Entity.Variable x, out Entity rewritten, out int degree, out Entity? argument) + { + rewritten = Number.Integer.One; + degree = 0; + argument = null; + var factors = Mulf.LinearChildren(expr); + if (factors.Count > 1 || expr is Powf(_, Number.Integer)) + { + var product = (Entity)Number.Integer.One; + var total = 0; + Entity? common = null; + var read = true; + foreach (var factor in factors) + { + var (@base, power) = factor is Powf(var b, Number.Integer whole) && whole.EInteger.CanFitInInt32() && !whole.EInteger.IsZero + ? (b, whole.EInteger.ToInt32Checked()) + : (factor, 1); + if (!TryReadAHomogeneousTrigonometricPolynomial(@base, x, out var one, out var oneDegree, out var oneArgument) + || oneArgument is not null && common is not null && oneArgument != common) + { + read = false; + break; + } + common ??= oneArgument; + total += power * oneDegree; + var raised = power == 1 ? one : MathS.Pow(one, Number.Integer.Create(power)); + product = product == Number.Integer.One ? raised : product * raised; + } + if (read) + { + rewritten = product; + degree = total; + argument = common; + return true; + } + } + return TryReadAHomogeneousTrigonometricPolynomial(expr, x, out rewritten, out degree, out argument); + } + /// The two placeholders a homogeneous polynomial is rewritten over. [ConstantField] private static readonly Entity.Variable HomogeneousTangent = Entity.Variable.CreateVariableOrConstant("__homogeneous_tangent"); diff --git a/Sources/Tests/UnitTests/Calculus/HomogeneousTrigonometricTest.cs b/Sources/Tests/UnitTests/Calculus/HomogeneousTrigonometricTest.cs index a5825238d..204d57b77 100644 --- a/Sources/Tests/UnitTests/Calculus/HomogeneousTrigonometricTest.cs +++ b/Sources/Tests/UnitTests/Calculus/HomogeneousTrigonometricTest.cs @@ -1,4 +1,4 @@ -// +// // Copyright (c) 2019-2026 Angouri. // AngouriMath is licensed under MIT. // Details: https://github.com/asc-community/AngouriMath/blob/master/LICENSE.md. @@ -65,6 +65,40 @@ private static void DifferentiatesBack(string integrand, double[] points) + "so this asserts almost nothing"); } + /// + /// The same with symbolic coefficients, where the power has to be read as a power. Read as + /// one expanded polynomial, (a cos(x) + b sin(x))^4 became a quartic in the tangent + /// with a symbol in every coefficient, which the rational integrator cannot factor back + /// into (a + b t)^4 -- with numbers it can, which is why the cases above never + /// showed it: sec(x)^2/(a cos(x) + b sin(x))^4 ran past ten minutes and takes a + /// tenth of a second. Rubi's 4.7.2, sixteen rows. At a = 1.7, b = 0.6 the + /// sum vanishes at x = -1.23, and the points stay clear of it. + /// + [Theory] + [InlineData("sec(x)^2/(a*cos(x) + b*sin(x))^4")] + [InlineData("1/(a*cos(x) + b*sin(x))^4")] + [InlineData("cos(x)/(a*cos(x) + b*sin(x))^3")] + [InlineData("sin(x)^3/(a*cos(x) + b*sin(x))^3")] + [InlineData("csc(x)/(a*cos(x) + b*sin(x))^3")] + public void APowerOfASumWithSymbolicCoefficients(string integrand) + { + var integral = integrand.ToEntity().Integrate("x"); + Assert.DoesNotContain("NaN", integral.Stringize()); + Assert.DoesNotContain("integral(", integral.Stringize()); + var derivative = integral.Substitute("C", 0).Substitute("a", 1.7).Substitute("b", 0.6).Differentiate("x"); + var original = integrand.ToEntity().Substitute("a", 1.7).Substitute("b", 0.6); + foreach (var at in new[] { 0.3, 0.8, 1.2, -0.5 }) + { + var got = derivative.Substitute("x", at).EvalNumerical(); + var want = original.Substitute("x", at).EvalNumerical(); + Assert.False(got.IsNaN, $"the antiderivative of {integrand} differentiates to NaN at x = {at}"); + var difference = Math.Abs((double)(got - want).RealPart) + Math.Abs((double)(got - want).ImaginaryPart); + var scale = Math.Max(1.0, Math.Abs((double)want.RealPart)); + Assert.True(difference / scale < 1e-9, + $"d/dx of the antiderivative of {integrand} is {got} at x = {at}, where the integrand is {want}"); + } + } + /// /// A power of a sum of a sine and a cosine, which is the case that took 239 seconds to be /// declined.