From a78d71288e853ff2e7c7595bee8d43f5ad41cb10 Mon Sep 17 00:00:00 2001 From: Rafael Vuijk Date: Sun, 4 Oct 2026 17:54:51 +0000 Subject: [PATCH] Two tangents of arguments a constant apart are written apart By the addition formulas, tan(A) tan(B) = cot(A - B) (tan(A) - tan(B)) - 1 for a constant A - B, and 1 - cot(A + B) (tan(A) + tan(B)) for a constant A + B; the cotangents, secants and cosecants likewise. A constant times such a product of two functions of one kind, of linear arguments with one slope or opposite ones, is written so and asked as the same question. Rubi's tan(a + b x) tan(c + b x) and the rest of its 4.7.7 with two such arguments were declined. 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 | 70 +++++++++++++++++++ .../Integration/Integration.Definition.cs | 3 + ...FunctionsOfShiftedArgumentsIntegralTest.cs | 52 ++++++++++++++ 4 files changed, 138 insertions(+) create mode 100644 Sources/Tests/UnitTests/Calculus/TwoFunctionsOfShiftedArgumentsIntegralTest.cs diff --git a/BREAKING-CHANGES.md b/BREAKING-CHANGES.md index dc39a1fe6..b4df54c96 100644 --- a/BREAKING-CHANGES.md +++ b/BREAKING-CHANGES.md @@ -688,6 +688,19 @@ read them as nonzero. They are now decided over one bar, expanded, and at pinned | `"e^(3*acoth(a*x))/(c-a*c*x)^3".Integrate("x")`, and over `(c - a c x)^4` | left unevaluated | an antiderivative in `sqrt((a x + 1)/(a x - 1))` | | `"e^(2*acoth(a*x))*sqrt(c-a*c*x)/x".Integrate("x")`, and over `x^2` | left unevaluated | an antiderivative in `sqrt(c - a c x)`, by cases on the sign of `c` | +### Two tangents of arguments a constant apart are written apart + +**Answers where there were none.** `tan(a + b x) tan(c + b x)` was declined, with the secants, +cotangents and cosecants the same way and the products whose arguments add to a constant, Rubi's +4.7.7. By the addition formulas each such product is the functions of the two arguments apart: +`tan(A) tan(B) = cot(A - B) (tan(A) - tan(B)) - 1` and its kin, for `A - B` or `A + B` a constant +that is not a multiple of `pi` ([#718](https://github.com/asc-community/AngouriMath/issues/718)). + +| Input | Was (2.5.0) | Now | +|---|---|---| +| `"tan(a + b*x)*tan(c + b*x)".ToEntity().Integrate("x")` | `integral(...)` | `cot(a - c) (ln(cos(c + b x)) - ln(cos(a + b x)))/b - x` | +| `"sec(c - b*x)*sec(a + b*x)".ToEntity().Integrate("x")` | `integral(...)` | `csc(a + c) (ln(cos(c - b x)) - ln(cos(a + b x)))/b` | + ### A trigonometric function of an imaginary multiple of a logarithm is integrated in exponentials **Answers where there were none.** `tan(a + i ln(x))` and `sin(a + ln(c x^2) sqrt(-1/4))` were diff --git a/Sources/AngouriMath/Functions/Continuous/Integration/IndefiniteIntegralSolver.cs b/Sources/AngouriMath/Functions/Continuous/Integration/IndefiniteIntegralSolver.cs index 875a31f1c..1bf664cfb 100644 --- a/Sources/AngouriMath/Functions/Continuous/Integration/IndefiniteIntegralSolver.cs +++ b/Sources/AngouriMath/Functions/Continuous/Integration/IndefiniteIntegralSolver.cs @@ -8381,6 +8381,76 @@ private static (Entity Coefficient, Entity Degree)? TheMonomial(Entity expr, Ent return null; } + /// + /// A product of two tangents, cotangents, secants or cosecants of linear arguments whose + /// difference or sum is a constant, written as the functions of each apart: with + /// d = A - B, tan(A) tan(B) = cot(d) (tan(A) - tan(B)) - 1. + /// + /// + /// + /// The addition formulas, read for the product: for a constant d = A - B, + /// tan(A) tan(B) = cot(d) (tan(A) - tan(B)) - 1, + /// cot(A) cot(B) = cot(d) (cot(B) - cot(A)) - 1, + /// sec(A) sec(B) = csc(d) (tan(A) - tan(B)) and + /// csc(A) csc(B) = csc(d) (cot(B) - cot(A)); for a constant s = A + B, + /// tan(A) tan(B) = 1 - cot(s) (tan(A) + tan(B)), + /// cot(A) cot(B) = 1 + cot(s) (cot(A) + cot(B)), + /// sec(A) sec(B) = csc(s) (tan(A) + tan(B)) and + /// csc(A) csc(B) = csc(s) (cot(A) + cot(B)). Each holds wherever both sides are + /// defined, for d or s not a multiple of pi, which a symbolic one is + /// taken not to be, as everywhere in this integrator. Rubi's 4.7.7 has + /// tan(a + b x) tan(c + b x) and the rest, and nothing read two arguments. + /// + /// + /// The same question in another spelling, so asked as it. A constant times the product + /// only, and the two functions of one kind. + /// https://github.com/asc-community/AngouriMath/issues/718 + /// + /// + internal static Entity? SolveByWritingTwoFunctionsOfShiftedArgumentsApart(Entity expr, Entity.Variable x, bool integrateByParts) + { + Entity constant = Number.Integer.One; + Entity? first = null, second = null; + foreach (var factor in Mulf.LinearChildren(expr)) + { + if (!factor.ContainsNode(x)) + constant = constant == Number.Integer.One ? factor : constant * factor; + else if (first is null) + first = factor; + else if (second is null) + second = factor; + else + return null; + } + if (first is null || second is null || first.GetType() != second.GetType() + || first is not (Tanf or Cotanf or Secantf or Cosecantf)) + return null; + var a = first.DirectChildren.First(); + var b = second.DirectChildren.First(); + if (a == b || !TreeAnalyzer.TryGetPolyLinear(a, x, out var slopeOfA, out _) || slopeOfA.ContainsNode(x) + || !TreeAnalyzer.TryGetPolyLinear(b, x, out var slopeOfB, out _) || slopeOfB.ContainsNode(x)) + return null; + static bool IsZero(Entity value) => value.Expand().InnerSimplified.Evaled is Number.Complex { IsZero: true }; + var sameSlope = IsZero(slopeOfA - slopeOfB); + if (!sameSlope && !IsZero(slopeOfA + slopeOfB)) + return null; + var shift = (sameSlope ? a - b : a + b).Expand().InnerSimplified; + if (shift.ContainsNode(x) || shift.Evaled is Number.Complex { IsZero: true }) + return null; + Entity apart = first switch + { + Tanf => sameSlope + ? MathS.Cotan(shift) * (MathS.Tan(a) - MathS.Tan(b)) - 1 + : 1 - MathS.Cotan(shift) * (MathS.Tan(a) + MathS.Tan(b)), + Cotanf => sameSlope + ? MathS.Cotan(shift) * (MathS.Cotan(b) - MathS.Cotan(a)) - 1 + : 1 + MathS.Cotan(shift) * (MathS.Cotan(a) + MathS.Cotan(b)), + Secantf => MathS.Cosec(shift) * (sameSlope ? MathS.Tan(a) - MathS.Tan(b) : MathS.Tan(a) + MathS.Tan(b)), + _ => MathS.Cosec(shift) * (sameSlope ? MathS.Cotan(b) - MathS.Cotan(a) : MathS.Cotan(a) + MathS.Cotan(b)), + }; + return Integration.ComputeAsTheSameQuestion(constant == Number.Integer.One ? apart : constant * apart, x, integrateByParts); + } + /// /// A quotient of two homogeneous polynomials in sin(u) and cos(u), /// integrated by t = tan(u) — which turns it into a rational function of t diff --git a/Sources/AngouriMath/Functions/Continuous/Integration/Integration.Definition.cs b/Sources/AngouriMath/Functions/Continuous/Integration/Integration.Definition.cs index 15f973f6b..a4bd076fa 100644 --- a/Sources/AngouriMath/Functions/Continuous/Integration/Integration.Definition.cs +++ b/Sources/AngouriMath/Functions/Continuous/Integration/Integration.Definition.cs @@ -857,6 +857,9 @@ private static Entity Normalized(Entity expr, Entity.Variable x) => // symbolic slope, written in that linear: before the substitution search, which reads // each function on its own. if ((answer = IndefiniteIntegralSolver.SolveByWritingMultiplesOfOneLinearArgument(expr, x, integrateByParts)) is { }) return answer; + // Two tangents, cotangents, secants or cosecants of arguments a constant apart, written + // as functions of each alone by the addition formulas. + if ((answer = IndefiniteIntegralSolver.SolveByWritingTwoFunctionsOfShiftedArgumentsApart(expr, x, integrateByParts)) is { }) return answer; // A constant out of a fractional power of a trigonometric factor: `sqrt(b sec(x))` is // `sqrt(b) sqrt(sec(x))` for a positive `b`, which meets the other powers of the // secant beside it. After the rules that answer the same shapes for any real diff --git a/Sources/Tests/UnitTests/Calculus/TwoFunctionsOfShiftedArgumentsIntegralTest.cs b/Sources/Tests/UnitTests/Calculus/TwoFunctionsOfShiftedArgumentsIntegralTest.cs new file mode 100644 index 000000000..db2668a32 --- /dev/null +++ b/Sources/Tests/UnitTests/Calculus/TwoFunctionsOfShiftedArgumentsIntegralTest.cs @@ -0,0 +1,52 @@ +// +// 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 +{ + /// + /// Two tangents, cotangents, secants or cosecants of arguments whose difference or sum is a + /// constant, written as the functions of each apart by the addition formulas. Rubi's 4.7.7. + /// #718 + /// + /// + /// Checked by differentiating back with a = 0.3, b = 0.9, c = 1.1, on + /// both sides of zero and away from the poles of every function of the three arguments. + /// + [Trait("Area", "Calculus")] + public sealed class TwoFunctionsOfShiftedArgumentsIntegralTest + { + [Theory] + [InlineData("tan(a + b*x)*tan(c + b*x)")] + [InlineData("tan(c - b*x)*tan(a + b*x)")] + [InlineData("cot(a + b*x)*cot(c + b*x)")] + [InlineData("cot(c - b*x)*cot(a + b*x)")] + [InlineData("sec(a + b*x)*sec(c + b*x)")] + [InlineData("sec(c - b*x)*sec(a + b*x)")] + [InlineData("csc(a + b*x)*csc(c + b*x)")] + [InlineData("csc(c - b*x)*csc(a + b*x)")] + public void IsWrittenApartByTheAdditionFormulas(string integrand) + { + var integral = integrand.ToEntity().Integrate("x"); + Assert.DoesNotContain("integral(", integral.Stringize()); + Entity Pinned(Entity e) => e.Substitute("a", 0.3).Substitute("b", 0.9).Substitute("c", 1.1); + var derivative = Pinned(integral.Substitute("C", 0)).Differentiate("x"); + var original = Pinned(integrand.ToEntity()); + foreach (var at in new[] { -1.0, -0.8, 0.1, 0.2, 0.9, 1.0 }) + { + var want = original.Substitute("x", at).EvalNumerical(); + var got = derivative.Substitute("x", at).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) + Math.Abs((double)want.ImaginaryPart)), + $"d/dx of the antiderivative of {integrand} is {got} at x = {at}, where the integrand is {want}"); + } + } + } +}