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.