From 8d6f43cf0d1481ac0ea26bf792dc432057b1c508 Mon Sep 17 00:00:00 2001 From: Rafael Vuijk Date: Fri, 9 Oct 2026 02:37:35 +0000 Subject: [PATCH] A secant beside powers of its conjugate sums, in the sum Part of #718. Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_012sonx8iAspMiwRwokT1Ura --- .../Integration/IndefiniteIntegralSolver.cs | 84 +++++++++++++++++++ .../Integration/Integration.Definition.cs | 3 + .../ASecantBesideConjugateSumsIntegralTest.cs | 51 +++++++++++ 3 files changed, 138 insertions(+) create mode 100644 Sources/Tests/UnitTests/Calculus/ASecantBesideConjugateSumsIntegralTest.cs diff --git a/Sources/AngouriMath/Functions/Continuous/Integration/IndefiniteIntegralSolver.cs b/Sources/AngouriMath/Functions/Continuous/Integration/IndefiniteIntegralSolver.cs index fcbd8c337..014352fda 100644 --- a/Sources/AngouriMath/Functions/Continuous/Integration/IndefiniteIntegralSolver.cs +++ b/Sources/AngouriMath/Functions/Continuous/Integration/IndefiniteIntegralSolver.cs @@ -4370,6 +4370,90 @@ Entity Integral(ERational power) return roots < 2 ? answer : answer.Provided((secantKind ? MathS.Cos(argument) : MathS.Sin(argument)) >= Number.Integer.Zero); } + /// + /// sec(y) (a ± a sec(y))^m (c ∓ c sec(y))^n, with n half-odd and m anything, + /// integrated in u = a ± a sec(y): the two sums multiply to -a c tan(y)^2, so the + /// root of the second is tan(y) over the root of u times a constant, + /// sec(y) tan(y) dy is du over one, and what is left is + /// u^(m - 1/2) (2 - u/a)^(n - 1/2). The cosecant's are the same with the cotangent. + /// + /// + /// Rubi's sec(e + f x) (a + a sec(e + f x))^m sqrt(c - c sec(e + f x)) and two more of + /// 4.5.2.3 were declined. The constants are not written: the answer is the integrand times + /// the antiderivative in u over what that differentiates back to, a quotient whose + /// square is one; and it is kept only where that square is one at the sampled points. + /// https://github.com/asc-community/AngouriMath/issues/718 + /// + internal static Entity? SolveASecantBesidePowersOfItsConjugateSums(Entity expr, Entity.Variable x, bool integrateByParts) + { + if (!Integration.AnsweringTheQuestionAskedOrOneBelow) + return null; + Entity? argument = null; + foreach (var node in expr.Nodes) + { + if (TrigonometricArgument(node) is not { } thisArgument || !thisArgument.ContainsNode(x)) + continue; + if (argument is null) + argument = thisArgument; + else if (argument != thisArgument) + return null; + } + if (argument is null || !TreeAnalyzer.TryGetPolyLinear(argument, x, out var rate, out _) + || rate.ContainsNode(x) || TreeAnalyzer.IsZero(rate)) + return null; + var secant = MathS.Sec(argument); + var cosecant = new Cosecantf(argument); + Entity? function = null; + var sums = new List<(Entity Radicand, Entity Exponent, Entity A, bool Plus)>(); + foreach (var (factor, underneath) in FactorsOfTheIntegrand(expr)) + { + if (!factor.ContainsNode(x)) + continue; + if ((factor == secant || factor == cosecant) && function is null && !underneath) + { + function = factor; + continue; + } + var (radicand, exponent) = factor is Powf(var @base, var power) ? (@base, power) : (factor, (Entity)Number.Integer.One); + if (exponent.ContainsNode(x) || ReadAsOnePlusMinusAFunction(radicand, secant, cosecant) is not var (sumConstant, plus, _)) + return null; + sums.Add((radicand, underneath ? (-exponent).InnerSimplified : exponent, sumConstant, plus)); + } + if (function is null || sums.Count != 2 || sums[0].Plus == sums[1].Plus + || ReadAsOnePlusMinusAFunction(sums[0].Radicand, secant, cosecant) is not var (_, _, firstIsSecant) + || ReadAsOnePlusMinusAFunction(sums[1].Radicand, secant, cosecant) is not var (_, _, secondIsSecant) + || firstIsSecant != secondIsSecant || firstIsSecant != (function == secant)) + return null; + // The half-odd one, which is written through the other; a power of it at least a half + // first, so that what is left of it is a whole power to expand. + static bool IsHalfOdd(Entity exponent) => exponent is Number.Rational rational and not Number.Integer + && rational.ERational.Denominator.Equals(EInteger.FromInt32(2)) && rational.ERational.Numerator.CanFitInInt32(); + var order = sums[0].Exponent is Number.Rational { ERational.Sign: > 0 } && IsHalfOdd(sums[0].Exponent) ? new[] { 0, 1 } : new[] { 1, 0 }; + var (half, other) = (sums[order[0]], sums[order[1]]); + if (!IsHalfOdd(half.Exponent)) + return null; + var n = ((Number.Rational)half.Exponent).ERational; + // With u the other sum, the half-odd one is c (2 - u/a), whatever the signs. + var u = Variable.CreateUnique(expr, "u_conjugate"); + var c = half.A; + var a = other.A; + var leftOver = n.Subtract(ERational.FromInt32(1).Divide(ERational.FromInt32(2))); + var inU = (MathS.Pow(u, other.Exponent - Number.Rational.Create(1, 2)) + * MathS.Pow(2 - u / a, Number.Integer.Create(leftOver.ToEInteger()))).InnerSimplified; + inU = Functions.PartialFractions.Bare(inU); + if (inU.ContainsNode(x) || inU.Nodes.Any(node => node == MathS.NaN)) + return null; + var inX = inU.Substitute(u, other.Radicand) * other.Radicand.Differentiate(x); + // The square of the constant: -c^(2n)/(a rate^2). + var constantSquared = -MathS.Pow(c, Number.Integer.Create(n.Multiply(ERational.FromInt32(2)).ToEInteger())) / (a * MathS.Sqr(rate)); + if (!Functions.PartialFractions.HoldsAtSampledPoints(constantSquared * MathS.Sqr(inX), MathS.Sqr(expr), x)) + return null; + if (Integration.ComputeAsAQuestionOfItsOwn(inU, u, integrateByParts) is not { } inTermsOfU + || inTermsOfU.Nodes.Any(node => node == MathS.NaN)) + return null; + return expr * inTermsOfU.Substitute(u, other.Radicand) / inX; + } + /// /// tan(y) tan(2y) written sec(2y) - 1, and the integrand asked again in the /// one argument 2y. diff --git a/Sources/AngouriMath/Functions/Continuous/Integration/Integration.Definition.cs b/Sources/AngouriMath/Functions/Continuous/Integration/Integration.Definition.cs index f5f23bba3..3e8051366 100644 --- a/Sources/AngouriMath/Functions/Continuous/Integration/Integration.Definition.cs +++ b/Sources/AngouriMath/Functions/Continuous/Integration/Integration.Definition.cs @@ -905,6 +905,9 @@ private static Entity Normalized(Entity expr, Entity.Variable x) => // And `tan(y) tan(2y)` written `sec(2y) - 1` first, so that the rule reads one argument. if ((answer = IndefiniteIntegralSolver.SolveByWritingATangentTimesThatOfItsDoubleThroughTheSecant(expr, x, integrateByParts)) is { }) return answer; if ((answer = IndefiniteIntegralSolver.SolveByTheHalfAngleTangentBesideAHalfOddPowerOfOnePlusASecant(expr, x, integrateByParts)) is { }) return answer; + // And the secant beside powers of a + a sec(y) and c - c sec(y), whose product is a + // square of the tangent: in u = a + a sec(y), a symbolic power of it too. + if ((answer = IndefiniteIntegralSolver.SolveASecantBesidePowersOfItsConjugateSums(expr, x, integrateByParts)) is { }) return answer; // And `a ± a cosh(y)` under a fractional power: `2a cosh(y/2)^2`, `-2a sinh(y/2)^2`. if ((answer = IndefiniteIntegralSolver.SolveByTheHalfAngleWhereOnePlusAHyperbolicCosineIsASquare(expr, x, integrateByParts)) is { }) return answer; // The hyperbolic sine's, off the real line: 1 + i sinh(y) is (cosh(y/2) + i sinh(y/2))^2. diff --git a/Sources/Tests/UnitTests/Calculus/ASecantBesideConjugateSumsIntegralTest.cs b/Sources/Tests/UnitTests/Calculus/ASecantBesideConjugateSumsIntegralTest.cs new file mode 100644 index 000000000..22c1bb9ca --- /dev/null +++ b/Sources/Tests/UnitTests/Calculus/ASecantBesideConjugateSumsIntegralTest.cs @@ -0,0 +1,51 @@ +// +// 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 +{ + /// + /// sec(y) (a ± a sec(y))^m (c ∓ c sec(y))^n, with n half-odd and m a + /// symbol, in u = a ± a sec(y), and the cosecant's. Rubi's 4.5.2.3. + /// #718 + /// + [Trait("Area", "Calculus")] + public sealed class ASecantBesideConjugateSumsIntegralTest + { + [Theory] + [InlineData("sec(g + f*x)*(a + a*sec(g + f*x))^m*(c - c*sec(g + f*x))^(5/2)")] + [InlineData("sec(g + f*x)*(a + a*sec(g + f*x))^m*sqrt(c - c*sec(g + f*x))")] + [InlineData("sec(x)*(a - a*sec(x))^m*sqrt(c + c*sec(x))")] + [InlineData("csc(x)*(a + a*csc(x))^m*sqrt(c - c*csc(x))")] + public void InTheSum(string integrand) + { + var integral = integrand.ToEntity().Integrate("x"); + var text = integral.Stringize(); + Assert.DoesNotContain("integral(", text); + Assert.True(text.Length < 2000, $"{text.Length} characters of answer for {integrand}"); + Entity Pinned(Entity e) => e.Substitute("a", 1.3).Substitute("c", 0.9).Substitute("m", 2.3).Substitute("g", 0.2).Substitute("f", 0.7); + 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}"); + } + } +}