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}"); + } + } +}