diff --git a/BREAKING-CHANGES.md b/BREAKING-CHANGES.md
index 33e0d7dfa..e69635cad 100644
--- a/BREAKING-CHANGES.md
+++ b/BREAKING-CHANGES.md
@@ -347,6 +347,25 @@ in Rubi's 1.1.3.4 come out shorter as well, and those that took two seconds take
| `"1/(x^8*(a + b*x^3)*(c + d*x^3))".ToEntity().Integrate("x")` | `integral(...)` | 1,217 characters |
| `"1/(x^6*(a + b*x^4)*(c + d*x^4))".ToEntity().Integrate("x")` | `integral(...)` | 795 characters |
+### A power of the cosine beside a secant binomial is integrated in the cosine
+
+**Answers where there were none.** `cos(c + d x)^2/(a + b sec(c + d x))` was declined, and so were the cosine's powers
+beside `A + B sec + C sec^2` over the same binomial (Rubi's 4.5.1.2, 4.5.3.1 and 4.5.4.2). Wherever the secant is
+defined, the integrand is one quotient in the cosine, `cos(c + d x)^3/(a cos(c + d x) + b)`, and it is integrated
+in that spelling now. The same holds for a power of the sine beside a cosecant binomial. Only for the binomial to
+the first power: for a higher power the secant's own rules answer
+([#718](https://github.com/asc-community/AngouriMath/issues/718)).
+
+| Input | Was (2.5.0) | Now |
+|---|---|---|
+| `"cos(c + d*x)^2/(a + b*sec(c + d*x))".ToEntity().Integrate("x")` | `integral(...)` | 1,091 characters |
+| `"cos(c + d*x)^2*(A + B*sec(c + d*x))/(a + b*sec(c + d*x))".ToEntity().Integrate("x")` | `integral(...)` | 4,347 characters |
+| `"cos(c + d*x)^2*(A + C*sec(c + d*x)^2)/(a + b*sec(c + d*x))".ToEntity().Integrate("x")` | `integral(...)` | 3,999 characters |
+| `"cos(c + d*x)^3*(B*sec(c + d*x) + C*sec(c + d*x)^2)/(a + b*sec(c + d*x))".ToEntity().Integrate("x")` | `integral(...)` | 4,349 characters |
+| `"cos(c + d*x)^4*(B*sec(c + d*x) + C*sec(c + d*x)^2)/(a + b*sec(c + d*x))".ToEntity().Integrate("x")` | `integral(...)` | 5,579 characters |
+| `"cos(c + d*x)^2*(A + B*sec(c + d*x) + C*sec(c + d*x)^2)/(a + b*sec(c + d*x))".ToEntity().Integrate("x")` | `integral(...)` | 12,065 characters |
+| `"sin(x)^2/(a + b*csc(x))".ToEntity().Integrate("x")` | `integral(...)` | 898 characters |
+
### One linear twice is not two linear powers
**Answers that had no value.** `(a c + b c x)^(-3 - 2 p) (f + g x) (a^2 + 2 a b x + b^2 x^2)^p` was answered
diff --git a/Sources/AngouriMath/Functions/Continuous/Integration/IndefiniteIntegralSolver.cs b/Sources/AngouriMath/Functions/Continuous/Integration/IndefiniteIntegralSolver.cs
index 5df2d1209..3fcac5325 100644
--- a/Sources/AngouriMath/Functions/Continuous/Integration/IndefiniteIntegralSolver.cs
+++ b/Sources/AngouriMath/Functions/Continuous/Integration/IndefiniteIntegralSolver.cs
@@ -28951,12 +28951,14 @@ bool IsAPolynomialInBoth(Entity polynomial)
///
/// The half-angle substitution read the secant's spelling into a rational function of the
/// half-angle tangent that a substitution search expanded past thirty seconds, where the
- /// cosine's is answered in half of one. Only for two sums or more, all below the bar. Rubi's 4.5.2.3.
+ /// cosine's is answered in half of one. Only for two sums or more, all below the bar, or beside the
+ /// cosine itself. Rubi's 4.5.2.3 and 4.5.4.2.
/// https://github.com/asc-community/AngouriMath/issues/718
///
internal static Entity? SolveByWritingARationalFunctionOfTheSecantInTheCosine(Entity expr, Entity.Variable x, bool integrateByParts)
{
Entity? reciprocal = null;
+ var partners = new List();
foreach (var node in expr.Nodes)
{
if (!node.ContainsNode(x) || node is Entity.Variable)
@@ -28969,6 +28971,9 @@ bool IsAPolynomialInBoth(Entity polynomial)
else if (reciprocal != node)
return null;
break;
+ case Cosf or Sinf:
+ partners.Add(node);
+ break;
case Sumf or Minusf or Mulf or Divf or Powf(_, Number.Integer):
break;
default:
@@ -28982,15 +28987,29 @@ bool IsAPolynomialInBoth(Entity polynomial)
}
if (reciprocal is null || !TreeAnalyzer.TryGetPolyLinear(reciprocal.DirectChildren.First(), x, out _, out _))
return null;
+ var argument = reciprocal.DirectChildren.First();
+ var function = reciprocal is Secantf ? MathS.Cos(argument) : MathS.Sin(argument);
+ // The cosine of the same argument beside the secant, or the sine beside the cosecant, is
+ // what the rewritten integrand is in already; any other is a second function.
+ if (partners.Any(partner => partner != function))
+ return null;
// Only two sums in the secant or more, all below the bar: one, or one above, the rules for
// the secant answer as written, and in the cosine `sec(x)^5/(a + b sec(x))^3` ran past five
- // seconds where it is half of one.
+ // seconds where it is half of one. Beside the cosine itself, though, the secant's rules have
+ // two functions to read, and in the cosine alone it is one quotient: the half-angle
+ // substitution wrote `cos(x)^2/(a + b sec(x))` in 117,409 characters where
+ // `cos(x)^3/(a cos(x) + b)` is 873. Rubi's 4.5.4.2.
var (above, below) = Functions.SingleQuotient.Of(expr);
bool ASum(Entity node) => node is Sumf or Minusf && node.ContainsNode(reciprocal);
- if (above.Nodes.Any(ASum) || below.Nodes.Where(ASum).Distinct().Count() < 2)
+ if (partners.Count == 0 && (above.Nodes.Any(ASum) || below.Nodes.Where(ASum).Distinct().Count() < 2))
+ return null;
+ // Beside the cosine, a sum in the secant below, each to the first power only:
+ // `cos(x)/(a + b sec(x))^4` in the cosine is a partial fraction search past five seconds,
+ // which the secant's rules answer in a third of one, and with the sum in the sine instead,
+ // `csc(x)^3/(a + b sin(x)^3)` is a question for the rules of the sine's binomial.
+ if (partners.Count > 0
+ && (!below.Nodes.Any(ASum) || Mulf.LinearChildren(below).Any(factor => factor is Powf(var @base, _) && ASum(@base))))
return null;
- var argument = reciprocal.DirectChildren.First();
- var function = reciprocal is Secantf ? MathS.Cos(argument) : MathS.Sin(argument);
var rewritten = Functions.SingleQuotient.Combine(expr.Replace(node => node == reciprocal ? 1 / function : node));
return Integration.ComputeAsTheSameQuestion(rewritten, x, integrateByParts);
}
diff --git a/Sources/Tests/UnitTests/Calculus/ACosineBesideASecantBinomialIntegralTest.cs b/Sources/Tests/UnitTests/Calculus/ACosineBesideASecantBinomialIntegralTest.cs
new file mode 100644
index 000000000..e6aec0b6b
--- /dev/null
+++ b/Sources/Tests/UnitTests/Calculus/ACosineBesideASecantBinomialIntegralTest.cs
@@ -0,0 +1,47 @@
+//
+// 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 power of the cosine beside a rational function of the secant of the same argument, written
+ /// in the cosine alone, where it is one quotient. Rubi's 4.5.4.2.
+ /// #718
+ ///
+ [Trait("Area", "Calculus")]
+ public sealed class ACosineBesideASecantBinomialIntegralTest
+ {
+ [Theory]
+ [InlineData("cos(c + d*x)^2*(A + K*sec(c + d*x)^2)/(a + b*sec(c + d*x))", 6000)]
+ [InlineData("cos(c + d*x)^3*(B*sec(c + d*x) + K*sec(c + d*x)^2)/(a + b*sec(c + d*x))", 6000)]
+ [InlineData("cos(c + d*x)^2*(A + B*sec(c + d*x) + K*sec(c + d*x)^2)/(a + b*sec(c + d*x))", 15000)]
+ [InlineData("cos(x)^2/(a + b*sec(x))", 1500)]
+ [InlineData("sin(x)^2/(a + b*csc(x))", 1500)]
+ public void DifferentiatesBack(string integrand, int atMostCharacters)
+ {
+ var integral = integrand.ToEntity().Integrate("x");
+ Assert.DoesNotContain("integral(", integral.Stringize());
+ Assert.True(integral.Stringize().Length <= atMostCharacters, $"{integral.Stringize().Length} characters");
+ Entity Pinned(Entity e) => e.Substitute("a", 2).Substitute("b", 3).Substitute("c", "1/4".ToEntity()).Substitute("d", "3/2".ToEntity())
+ .Substitute("A", 5).Substitute("B", 7).Substitute("K", 11);
+ var derivative = Pinned(integral.Substitute("C", 0)).Differentiate("x");
+ var original = Pinned(integrand.ToEntity());
+ foreach (var at in new[] { "0.35", "0.6", "1.1", "1.4" })
+ {
+ var want = original.Substitute("x", at.ToEntity()).EvalNumerical();
+ var got = derivative.Substitute("x", at.ToEntity()).EvalNumerical();
+ Assert.True(Math.Abs((double)(got - want).RealPart) + Math.Abs((double)(got - want).ImaginaryPart)
+ < 1e-9 * Math.Max(1, Math.Abs((double)want.RealPart)),
+ $"d/dx of the antiderivative of {integrand} is {got} at x = {at}, where the integrand is {want}");
+ }
+ }
+ }
+}