From 640b9ead65ea8008c258f4dd5e9e062328a66e9d Mon Sep 17 00:00:00 2001 From: Rafael Vuijk Date: Thu, 8 Oct 2026 12:00:23 +0000 Subject: [PATCH] A rational function of the secant with two sums below the bar is written in the cosine 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; as one quotient in the cosine it is answered in a second. Only where two sums in the secant stand below the bar and none above. Part of #718. Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_012sonx8iAspMiwRwokT1Ura --- BREAKING-CHANGES.md | 13 +++++ .../Integration/IndefiniteIntegralSolver.cs | 52 +++++++++++++++++++ .../Integration/Integration.Definition.cs | 3 ++ ...ctionOfTheSecantInTheCosineIntegralTest.cs | 50 ++++++++++++++++++ 4 files changed, 118 insertions(+) create mode 100644 Sources/Tests/UnitTests/Calculus/ARationalFunctionOfTheSecantInTheCosineIntegralTest.cs diff --git a/BREAKING-CHANGES.md b/BREAKING-CHANGES.md index 5e09bede5..99f3ca9db 100644 --- a/BREAKING-CHANGES.md +++ b/BREAKING-CHANGES.md @@ -208,6 +208,19 @@ after it |---|---|---| | `"(a*c + b*c*x)^(-3-2*p)*(f + g*x)*(a^2 + 2*a*b*x + b^2*x^2)^p".ToEntity().Integrate("x")` | `integral(...)`; an answer with no value on the unreleased master | the antiderivative | +### A rational function of the secant with two sums below the bar is written in the cosine + +**Answers where there were none.** `sec(x)/((a + b sec(x)) (c + d sec(x))^2)` ran past thirty seconds: the +half-angle substitution read the secant's spelling into a rational function of the half-angle tangent that a +substitution search expanded without end. As one quotient in the cosine it is +`cos(x)^2/((a cos(x) + b) (c cos(x) + d)^2)`, answered in a second, and it is written so where two sums in the +secant stand below the bar and none above ([#718](https://github.com/asc-community/AngouriMath/issues/718)). + +| Input | Was (2.5.0) | Now | +|---|---|---| +| `"sec(x)/((a + b*sec(x))*(c + d*sec(x))^2)".ToEntity().Integrate("x")` | `integral(...)`; past thirty seconds on the unreleased master | 9,439 characters, a case for each sign of two discriminants | +| `"sec(x)/((a + b*sec(x))^2*(c + d*sec(x)))".ToEntity().Integrate("x")` | `integral(...)`; past thirty seconds on the unreleased master | 9,430 characters | + ### `a + i a tan` of a shifted linear below the bar is integrated in the linear **Answers where there were none.** `sqrt(a + i a tan(g + f x)) (A + B tan(g + f x))/sqrt(c - i c tan(g + f x))` diff --git a/Sources/AngouriMath/Functions/Continuous/Integration/IndefiniteIntegralSolver.cs b/Sources/AngouriMath/Functions/Continuous/Integration/IndefiniteIntegralSolver.cs index 800082a70..46840f353 100644 --- a/Sources/AngouriMath/Functions/Continuous/Integration/IndefiniteIntegralSolver.cs +++ b/Sources/AngouriMath/Functions/Continuous/Integration/IndefiniteIntegralSolver.cs @@ -27741,6 +27741,58 @@ bool IsAPolynomialInBoth(Entity polynomial) return null; } + /// + /// A rational function of the secant of one argument, or of the cosecant, written in the + /// cosine, or the sine, as one quotient: sec(z)/((a + b sec(z)) (c + d sec(z))^2) is + /// cos(z)^2/((a cos(z) + b) (c cos(z) + d)^2) wherever the secant is defined, exactly. + /// + /// + /// 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. + /// https://github.com/asc-community/AngouriMath/issues/718 + /// + internal static Entity? SolveByWritingARationalFunctionOfTheSecantInTheCosine(Entity expr, Entity.Variable x, bool integrateByParts) + { + Entity? reciprocal = null; + foreach (var node in expr.Nodes) + { + if (!node.ContainsNode(x) || node is Entity.Variable) + continue; + switch (node) + { + case Secantf or Cosecantf: + if (reciprocal is null) + reciprocal = node; + else if (reciprocal != node) + return null; + break; + case Sumf or Minusf or Mulf or Divf or Powf(_, Number.Integer): + break; + default: + // Inside the reciprocal's own argument, a linear in x. + if (reciprocal is not null && reciprocal.DirectChildren.First().Nodes.Contains(node)) + break; + if (expr.Nodes.Any(other => other is Secantf or Cosecantf && other.DirectChildren.First().Nodes.Contains(node))) + break; + return null; + } + } + if (reciprocal is null || !TreeAnalyzer.TryGetPolyLinear(reciprocal.DirectChildren.First(), x, out _, out _)) + 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. + 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) + 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); + } + /// /// A half-odd power of the secant or the cosecant, in an integrand with the cosine or the /// sine of the same argument in it, written as a power of the cosine or the sine: diff --git a/Sources/AngouriMath/Functions/Continuous/Integration/Integration.Definition.cs b/Sources/AngouriMath/Functions/Continuous/Integration/Integration.Definition.cs index 8192a0c89..93817f696 100644 --- a/Sources/AngouriMath/Functions/Continuous/Integration/Integration.Definition.cs +++ b/Sources/AngouriMath/Functions/Continuous/Integration/Integration.Definition.cs @@ -1101,6 +1101,9 @@ private static Entity Normalized(Entity expr, Entity.Variable x) => // Bioche's first two rules before the third: a rational function of the two that // is odd in one of them is a rational function of the other, smaller than the // half-angle tangent's and answered more shortly. + // A rational function of the secant alone in the cosine first, as one quotient: the + // half-angle tangent of the secant's spelling was a search past thirty seconds. + if ((answer = IndefiniteIntegralSolver.SolveByWritingARationalFunctionOfTheSecantInTheCosine(expr, x, integrateByParts)) is { }) return answer; if ((answer = IndefiniteIntegralSolver.SolveByBiochesOddSubstitution(expr, x, integrateByParts)) is { }) return answer; if ((answer = IndefiniteIntegralSolver.SolveByHalfAngleSubstitution(expr, x, integrateByParts)) is { }) return answer; // The substitution by a sine or a cosine where an odd power of the complement is diff --git a/Sources/Tests/UnitTests/Calculus/ARationalFunctionOfTheSecantInTheCosineIntegralTest.cs b/Sources/Tests/UnitTests/Calculus/ARationalFunctionOfTheSecantInTheCosineIntegralTest.cs new file mode 100644 index 000000000..68ff1a5ea --- /dev/null +++ b/Sources/Tests/UnitTests/Calculus/ARationalFunctionOfTheSecantInTheCosineIntegralTest.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 +{ + /// + /// A rational function of the secant with two sums in it below the bar, written in the cosine as + /// one quotient: sec(x)/((a + b sec(x)) (c + d sec(x))^2) is + /// cos(x)^2/((a cos(x) + b) (c cos(x) + d)^2). Rubi's 4.5.2.3. + /// #718 + /// + [Trait("Area", "Calculus")] + public sealed class ARationalFunctionOfTheSecantInTheCosineIntegralTest + { + [Theory] + [InlineData("sec(x)/((a + b*sec(x))*(c + d*sec(x))^2)")] + [InlineData("sec(x)/((a + b*sec(x))^2*(c + d*sec(x)))")] + public void AsOneQuotient(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", 2.1).Substitute("d", 0.6); + var derivative = Pinned(integral.Substitute("C", 0)).Differentiate("x"); + var original = Pinned(integrand.ToEntity()); + var compared = 0; + foreach (var at in new[] { -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}"); + } + } +}