From ff524d659d0a9128d751c04508a8457f1b154324 Mon Sep 17 00:00:00 2001 From: Rafael Vuijk Date: Thu, 8 Oct 2026 15:55:33 +0000 Subject: [PATCH] A sine or a cosine of a shifted argument is written in the other Where every trigonometric function is of one of two linear arguments of the same slope, each with an offset, and those of one are sines and cosines only, the addition formula writes them in the other. Part of #718. Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_012sonx8iAspMiwRwokT1Ura --- BREAKING-CHANGES.md | 14 +++++ .../Integration/IndefiniteIntegralSolver.cs | 57 +++++++++++++++++++ .../Integration/Integration.Definition.cs | 3 + .../ASineOfAShiftedArgumentIntegralTest.cs | 50 ++++++++++++++++ 4 files changed, 124 insertions(+) create mode 100644 Sources/Tests/UnitTests/Calculus/ASineOfAShiftedArgumentIntegralTest.cs diff --git a/BREAKING-CHANGES.md b/BREAKING-CHANGES.md index 99f3ca9db..4b4fbb1e8 100644 --- a/BREAKING-CHANGES.md +++ b/BREAKING-CHANGES.md @@ -208,6 +208,20 @@ 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 sine or a cosine of a shifted argument is written in the other + +**Answers where there were none.** `sin(a + b x) sec(c + b x)^3` was declined, with the rest of Rubi's 4.7.1 +that puts a sine or a cosine of one linear argument beside functions of another of the same slope: nothing read +two arguments. The addition formula writes the first in the second, +`sin(a + b x) = sin(a - c) cos(c + b x) + cos(a - c) sin(c + b x)`, and the integrand is then of one argument +([#718](https://github.com/asc-community/AngouriMath/issues/718)). + +| Input | Was (2.5.0) | Now | +|---|---|---| +| `"sin(a + b*x)*sec(c + b*x)^3".ToEntity().Integrate("x")` | `integral(...)` | 82 characters | +| `"cos(a + b*x)*sec(c + b*x)^2".ToEntity().Integrate("x")` | `integral(...)` | 111 characters | +| `"sin(a + b*x)*csc(c + b*x)^4".ToEntity().Integrate("x")` | `integral(...)` | 215 characters | + ### 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 diff --git a/Sources/AngouriMath/Functions/Continuous/Integration/IndefiniteIntegralSolver.cs b/Sources/AngouriMath/Functions/Continuous/Integration/IndefiniteIntegralSolver.cs index 46840f353..62e7c55c5 100644 --- a/Sources/AngouriMath/Functions/Continuous/Integration/IndefiniteIntegralSolver.cs +++ b/Sources/AngouriMath/Functions/Continuous/Integration/IndefiniteIntegralSolver.cs @@ -9259,6 +9259,63 @@ private static (Entity Coefficient, Entity Degree)? TheMonomial(Entity expr, Ent return null; } + /// + /// The sines and cosines of one linear argument written in another of the same slope, where the + /// two differ by a constant d = A - B: sin(A) = sin(d) cos(B) + cos(d) sin(B) and + /// cos(A) = cos(d) cos(B) - sin(d) sin(B), so that every trigonometric function is of + /// B. sin(a + b x) sec(c + b x)^3 is (sin(a - c) + cos(a - c) tan(c + b x)) sec(c + b x)^2. + /// + /// + /// Rubi's 4.7.1 has seventeen of these, each declined in a few milliseconds: nothing read two + /// arguments of which only one is a sine or a cosine. The same question in another spelling. + /// https://github.com/asc-community/AngouriMath/issues/718 + /// + internal static Entity? SolveByWritingASineOfAShiftedArgumentInTheOther(Entity expr, Entity.Variable x, bool integrateByParts) + { + var arguments = new List(); + var onlySineOrCosine = new Dictionary(); + foreach (var node in expr.Nodes) + { + if (node is not (Sinf or Cosf or Tanf or Cotanf or Secantf or Cosecantf) || !node.ContainsNode(x)) + continue; + var argument = node.DirectChildren.First(); + if (!arguments.Contains(argument)) + { + arguments.Add(argument); + onlySineOrCosine[argument] = true; + } + if (node is not (Sinf or Cosf)) + onlySineOrCosine[argument] = false; + } + if (arguments.Count != 2) + return null; + var (first, second) = (arguments[0], arguments[1]); + // Each with an offset: `cos(c) cos(d x) - sin(c) sin(d x)`, the addition formula's own + // expansion of `cos(c + d x)` by another rule, is not to be written back. + if (!TreeAnalyzer.TryGetPolyLinear(first, x, out var slopeOfFirst, out var offsetOfFirst) || slopeOfFirst.ContainsNode(x) + || !TreeAnalyzer.TryGetPolyLinear(second, x, out var slopeOfSecond, out var offsetOfSecond) || slopeOfSecond.ContainsNode(x) + || TreeAnalyzer.IsZero(offsetOfFirst) || TreeAnalyzer.IsZero(offsetOfSecond) + || slopeOfFirst.Expand().InnerSimplified != slopeOfSecond.Expand().InnerSimplified + && (slopeOfFirst - slopeOfSecond).Expand().InnerSimplified.Evaled is not Number.Complex { IsZero: true }) + return null; + // The one of sines and cosines only is written in the other. + var (shifted, kept) = onlySineOrCosine[first] ? (first, second) : onlySineOrCosine[second] ? (second, first) : (null!, null!); + if (shifted is null) + return null; + var d = (shifted - kept).Expand().InnerSimplified; + if (d.ContainsNode(x) || d.Evaled is Number.Complex { IsZero: true }) + return null; + var rewritten = expr.Replace(node => node switch + { + Sinf(var a) when a == shifted => MathS.Sin(d) * MathS.Cos(kept) + MathS.Cos(d) * MathS.Sin(kept), + Cosf(var a) when a == shifted => MathS.Cos(d) * MathS.Cos(kept) - MathS.Sin(d) * MathS.Sin(kept), + _ => node, + }); + if (rewritten.Nodes.Any(node => node is Sinf or Cosf && node.DirectChildren.First() == shifted)) + return null; + return Integration.ComputeAsTheSameQuestion(rewritten, x, integrateByParts); + } + /// /// 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 diff --git a/Sources/AngouriMath/Functions/Continuous/Integration/Integration.Definition.cs b/Sources/AngouriMath/Functions/Continuous/Integration/Integration.Definition.cs index 93817f696..b8a20f845 100644 --- a/Sources/AngouriMath/Functions/Continuous/Integration/Integration.Definition.cs +++ b/Sources/AngouriMath/Functions/Continuous/Integration/Integration.Definition.cs @@ -925,6 +925,9 @@ private static Entity Normalized(Entity expr, Entity.Variable x) => // 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 sine or a cosine of one argument beside functions of another shifted from it: the addition + // formula writes it in the other. + if ((answer = IndefiniteIntegralSolver.SolveByWritingASineOfAShiftedArgumentInTheOther(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/ASineOfAShiftedArgumentIntegralTest.cs b/Sources/Tests/UnitTests/Calculus/ASineOfAShiftedArgumentIntegralTest.cs new file mode 100644 index 000000000..4194b2a4b --- /dev/null +++ b/Sources/Tests/UnitTests/Calculus/ASineOfAShiftedArgumentIntegralTest.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 sine or a cosine of a + b x beside functions of c + b x, written in the latter by the + /// addition formula: sin(a + b x) = sin(a - c) cos(c + b x) + cos(a - c) sin(c + b x). Rubi's 4.7.1. + /// #718 + /// + [Trait("Area", "Calculus")] + public sealed class ASineOfAShiftedArgumentIntegralTest + { + [Theory] + [InlineData("sin(a + b*x)*sec(c + b*x)^3")] + [InlineData("cos(a + b*x)*sec(c + b*x)^2")] + [InlineData("sin(a + b*x)*csc(c + b*x)^4")] + public void InTheOtherArgument(string integrand) + { + var integral = integrand.ToEntity().Integrate("x"); + var text = integral.Stringize(); + Assert.DoesNotContain("integral(", text); + Assert.True(text.Length < 5000, $"{text.Length} characters of answer for {integrand}"); + Entity Pinned(Entity e) => e.Substitute("a", 1.3).Substitute("b", 0.7).Substitute("c", 0.4); + 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}"); + } + } +}