From 5be4755c75dcf842a6f9cc65ae047126b99ebee0 Mon Sep 17 00:00:00 2001 From: Rafael Vuijk Date: Thu, 8 Oct 2026 19:27:25 +0000 Subject: [PATCH] A sine of a double argument beside the argument is written as a product sin(2A) = 2 sin(A) cos(A) is one product whatever its power, so it is written so beside functions of A for any slope, where the rule for multiples of one argument reads a numeric slope and bounds the degree. Part of #718. Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_012sonx8iAspMiwRwokT1Ura --- BREAKING-CHANGES.md | 12 +++++ .../Integration/IndefiniteIntegralSolver.cs | 34 +++++++++++++ .../Integration/Integration.Definition.cs | 2 + .../ASineOfADoubleArgumentIntegralTest.cs | 49 +++++++++++++++++++ 4 files changed, 97 insertions(+) create mode 100644 Sources/Tests/UnitTests/Calculus/ASineOfADoubleArgumentIntegralTest.cs diff --git a/BREAKING-CHANGES.md b/BREAKING-CHANGES.md index 4b4fbb1e8..db2c34985 100644 --- a/BREAKING-CHANGES.md +++ b/BREAKING-CHANGES.md @@ -208,6 +208,18 @@ 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 of a double argument beside the argument is written as a product + +**Answers where there were none.** `csc(a + b x)^3 sin(2a + 2b x)^7` was declined: the rule that writes multiples +of one argument in it reads a numeric slope and bounds the degree it writes. A sine of the double is one product, +`sin(2A) = 2 sin(A) cos(A)`, whatever its power, and is written so beside functions of `A` for any slope +([#718](https://github.com/asc-community/AngouriMath/issues/718)). + +| Input | Was (2.5.0) | Now | +|---|---|---| +| `"csc(a + b*x)^3*sin(2*a + 2*b*x)^7".ToEntity().Integrate("x")` | `integral(...)` | 127 characters | +| `"csc(a + b*x)*sin(2*a + 2*b*x)^8".ToEntity().Integrate("x")` | `integral(...)` | 134 characters | + ### 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 diff --git a/Sources/AngouriMath/Functions/Continuous/Integration/IndefiniteIntegralSolver.cs b/Sources/AngouriMath/Functions/Continuous/Integration/IndefiniteIntegralSolver.cs index 62e7c55c5..aca16ef3c 100644 --- a/Sources/AngouriMath/Functions/Continuous/Integration/IndefiniteIntegralSolver.cs +++ b/Sources/AngouriMath/Functions/Continuous/Integration/IndefiniteIntegralSolver.cs @@ -12197,6 +12197,40 @@ node is Powf(var @base, var power) && @base.ContainsNode(u) return back.Nodes.Any(node => node == MathS.NaN) ? null : back; } + /// + /// A sine of twice an argument beside functions of the argument, written as the product it is: + /// sin(2A) = 2 sin(A) cos(A), so that every trigonometric function is of A. + /// csc(a + b x)^3 sin(2a + 2b x)^7 is 2^7 sin(a + b x)^4 cos(a + b x)^7. + /// + /// + /// The rule that writes multiples of one argument in it reads a numeric slope, and bounds the + /// degree it writes; a sine of the double is one product whatever its power, so it is written + /// here for any slope. Rubi's 4.7.1. + /// https://github.com/asc-community/AngouriMath/issues/718 + /// + internal static Entity? SolveByWritingASineOfADoubleArgumentAsAProduct(Entity expr, Entity.Variable x, bool integrateByParts) + { + var arguments = new List(); + foreach (var node in expr.Nodes) + if (node is Sinf or Cosf or Tanf or Cotanf or Secantf or Cosecantf && node.ContainsNode(x) + && node.DirectChildren.First() is var argument && !arguments.Contains(argument)) + arguments.Add(argument); + if (arguments.Count != 2) + return null; + foreach (var (single, twice) in new[] { (arguments[0], arguments[1]), (arguments[1], arguments[0]) }) + { + if (!TreeAnalyzer.TryGetPolyLinear(single, x, out var slope, out _) || slope.ContainsNode(x) + || (twice - 2 * single).Expand().InnerSimplified.Evaled is not Number.Complex { IsZero: true }) + continue; + // Only sines of the double, so that each is one product. + if (expr.Nodes.Any(node => node is Cosf or Tanf or Cotanf or Secantf or Cosecantf && node.DirectChildren.First() == twice)) + return null; + var rewritten = expr.Replace(node => node is Sinf(var a) && a == twice ? 2 * MathS.Sin(single) * MathS.Cos(single) : node); + return Integration.ComputeAsTheSameQuestion(rewritten, x, integrateByParts); + } + return null; + } + /// /// An integrand whose trigonometric functions have different multiples of one /// argument — sin(x)/cos(2x), cos(x)/(sin(x) tan(x/2)) — rewritten so that diff --git a/Sources/AngouriMath/Functions/Continuous/Integration/Integration.Definition.cs b/Sources/AngouriMath/Functions/Continuous/Integration/Integration.Definition.cs index b8a20f845..f5f23bba3 100644 --- a/Sources/AngouriMath/Functions/Continuous/Integration/Integration.Definition.cs +++ b/Sources/AngouriMath/Functions/Continuous/Integration/Integration.Definition.cs @@ -1196,6 +1196,8 @@ private static Entity Normalized(Entity expr, Entity.Variable x) => // And the other way round: not a product of two arguments but anything else built // from different multiples of one -- `sin(x)/cos(2x)`, `cos(x)/(sin(x) tan(x/2))` -- // rewritten to the one argument every trigonometric rule above reads. + // A sine of the double argument beside the argument: one product, for any slope. + if ((answer = IndefiniteIntegralSolver.SolveByWritingASineOfADoubleArgumentAsAProduct(expr, x, integrateByParts)) is { }) return answer; if ((answer = IndefiniteIntegralSolver.SolveByUnifyingTrigonometricArguments(expr, x, integrateByParts)) is { }) return answer; // The exponential substitution beside the other rewrites. It is also what integrates // the hyperbolic functions, which are not nodes here but quotients of exponentials. diff --git a/Sources/Tests/UnitTests/Calculus/ASineOfADoubleArgumentIntegralTest.cs b/Sources/Tests/UnitTests/Calculus/ASineOfADoubleArgumentIntegralTest.cs new file mode 100644 index 000000000..b25bdf2db --- /dev/null +++ b/Sources/Tests/UnitTests/Calculus/ASineOfADoubleArgumentIntegralTest.cs @@ -0,0 +1,49 @@ +// +// 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 of twice an argument beside functions of the argument, written as the product + /// sin(2A) = 2 sin(A) cos(A), for a symbolic slope too. Rubi's 4.7.1. + /// #718 + /// + [Trait("Area", "Calculus")] + public sealed class ASineOfADoubleArgumentIntegralTest + { + [Theory] + [InlineData("csc(a + b*x)^3*sin(2*a + 2*b*x)^7")] + [InlineData("csc(a + b*x)*sin(2*a + 2*b*x)^8")] + public void AsAProduct(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); + 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}"); + } + } +}