From c5a1b09fffb6cecf7391095759ac3f9b1276a48a Mon Sep 17 00:00:00 2001 From: Rafael Vuijk Date: Tue, 4 Aug 2026 12:45:16 +0000 Subject: [PATCH] Reduce the double angle over the single one (#557) sin(2u) csc(u) is 2 cos(u), and nothing reduced it. Opening sin(2u) up leaves 2 sin(u) cos(u) csc(u), whose sine and cosecant are no longer adjacent in the product, and the rules that cancel those match a pair of factors. So (sin(2t) csc(t))^2/4 - cos(2t) - sin(t)^2 came down to (sin(2t) csc(t))^2/4 - cos(t)^2 and stopped, one step short of the zero https://github.com/asc-community/AngouriMath/issues/557 asks for. A rule of its own, matching before the double angle is opened up and the two pieces are scattered. The cosecant's condition is carried rather than dropped. 2cos(u) is a number where sin(u) is zero and sin(2u) csc(u) is not, so an unconditional answer would speak for a point the expression does not reach. It is the same condition the cancellation of sin(u) csc(u) already comes back with. 17 tests, 7 of which fail without the change, including that the rewrite is the same number wherever both sides are defined. 4026 in all, 127 F#, corpus 102/117 with simp:trig at 100% from 67%, and 1320 property checks. --- .../Patterns/Patterns.Trigonometry.cs | 15 ++++ .../PatternsTest/DoubleAngleOverSineTest.cs | 86 +++++++++++++++++++ 2 files changed, 101 insertions(+) create mode 100644 Sources/Tests/UnitTests/PatternsTest/DoubleAngleOverSineTest.cs diff --git a/Sources/AngouriMath/Functions/Simplification/Patterns/Patterns.Trigonometry.cs b/Sources/AngouriMath/Functions/Simplification/Patterns/Patterns.Trigonometry.cs index 803222ed5..5c0d6429b 100644 --- a/Sources/AngouriMath/Functions/Simplification/Patterns/Patterns.Trigonometry.cs +++ b/Sources/AngouriMath/Functions/Simplification/Patterns/Patterns.Trigonometry.cs @@ -24,6 +24,21 @@ internal static partial class Patterns Sumf(Arctanf(var any1), Arccotanf(var any1a)) when any1 == any1a => MathS.pi / 2, Sumf(Arccotanf(var any1), Arctanf(var any1a)) when any1 == any1a => MathS.pi / 2, + // sin(2u) csc(u) = 2 cos(u), which is the double angle over the single one. + // A rule of its own because the two do not meet any other way: opening sin(2u) + // up leaves 2 sin(u) cos(u) csc(u), whose sine and cosecant are no longer + // adjacent in the product, and the rules that cancel those are pairwise. So + // (sin(2t) csc(t))^2/4 - cos(2t) - sin(t)^2 stopped one step short of zero -- + // https://github.com/asc-community/AngouriMath/issues/557. + // The condition is the cosecant's own and has to be carried: 2cos(u) is a + // number where sin(u) is zero and sin(2u) csc(u) is not, so dropping it would + // answer for a point the expression does not reach. It is the same condition + // the cancellation of sin(u) csc(u) already comes back with. + Mulf(Sinf(Mulf(Integer(2), var any1)), Cosecantf(var any1a)) when any1 == any1a + => (2 * new Cosf(any1)).Provided(new Cosecantf(any1).DomainCondition), + Mulf(Cosecantf(var any1a), Sinf(Mulf(Integer(2), var any1))) when any1 == any1a + => (2 * new Cosf(any1)).Provided(new Cosecantf(any1).DomainCondition), + // tan * cot = 1 Mulf(Tanf(var any1), Cotanf(var any1a)) when any1 == any1a => 1, Mulf(Cotanf(var any1), Tanf(var any1a)) when any1 == any1a => 1, diff --git a/Sources/Tests/UnitTests/PatternsTest/DoubleAngleOverSineTest.cs b/Sources/Tests/UnitTests/PatternsTest/DoubleAngleOverSineTest.cs new file mode 100644 index 000000000..14823395e --- /dev/null +++ b/Sources/Tests/UnitTests/PatternsTest/DoubleAngleOverSineTest.cs @@ -0,0 +1,86 @@ +// +// Copyright (c) 2019-2022 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; +using AngouriMath.Extensions; +using Xunit; + +namespace AngouriMath.Tests.PatternsTest +{ + /// + /// sin(2u) csc(u) is 2 cos(u), which nothing reduced. Opening sin(2u) up leaves + /// 2 sin(u) cos(u) csc(u), whose sine and cosecant are no longer adjacent in the + /// product, and the rules that cancel those are pairwise -- so + /// (sin(2t) csc(t))^2/4 - cos(2t) - sin(t)^2 stopped one step short of zero, which is + /// what https://github.com/asc-community/AngouriMath/issues/557 asks for. + /// + public sealed class DoubleAngleOverSineTest + { + private static Entity Bare(string expression) + { + var simplified = expression.ToEntity().Simplify(); + while (simplified is Entity.Providedf(var inner, _)) simplified = inner; + return simplified; + } + + [Theory] + [InlineData("sin(2 * x) * cosec(x)", "2 * cos(x)")] + [InlineData("cosec(x) * sin(2 * x)", "2 * cos(x)")] + [InlineData("sin(2 * x) / sin(x)", "2 * cos(x)")] + [InlineData("sin(2 * y) * cosec(y)", "2 * cos(y)")] + [InlineData("(sin(2 * x) * cosec(x)) ^ 2 / 4 - cos(2 * x) - sin(x) ^ 2", "0")] + public void TheDoubleAngleOverTheSingleOne(string expression, string expected) => + Assert.Equal(expected.ToEntity().Simplify(), Bare(expression)); + + /// + /// 2cos(u) is a number where sin(u) is zero and sin(2u) csc(u) is not, so the + /// cosecant's own condition has to be carried rather than dropped. It is the same + /// condition the cancellation of sin(u) csc(u) already comes back with. + /// + [Theory] + [InlineData("sin(2 * x) * cosec(x)")] + [InlineData("sin(2 * x) / sin(x)")] + public void TheCosecantsConditionIsCarried(string expression) => + Assert.Contains("sin(x) = 0", expression.ToEntity().Simplify().Stringize()); + + // Nothing else is claimed: a different argument, and a multiplier the identity + // does not cover. + [Theory] + [InlineData("sin(2 * x) * cosec(y)")] + [InlineData("sin(3 * x) * cosec(x)")] + [InlineData("cos(2 * x) * cosec(x)")] + public void OutsideTheIdentityNothingIsClaimed(string expression) => + Assert.Contains("csc", Bare(expression).Stringize()); + + // The rewrite has to be the same number wherever both sides are defined. + [Fact] + public void TheRewriteIsTheSameNumber() + { + var original = "sin(2 * x) * cosec(x)".ToEntity(); + var simplified = Bare("sin(2 * x) * cosec(x)"); + foreach (var at in new[] { 0.3, 0.9, 1.4, 2.2, -0.7, -1.9 }) + { + var before = original.Substitute("x", at).EvalNumerical().RealPart.EDecimal.ToDouble(); + var after = simplified.Substitute("x", at).EvalNumerical().RealPart.EDecimal.ToDouble(); + Assert.Equal(before, after, 9); + Assert.Equal(2 * Math.Cos(at), after, 9); + } + } + + // The cancellations next to this one have to keep working. + [Theory] + [InlineData("sin(x) * cosec(x)", "1")] + [InlineData("cos(x) * sec(x)", "1")] + [InlineData("tan(x) * cotan(x)", "1")] + [InlineData("sin(x) ^ 2 + cos(x) ^ 2", "1")] + [InlineData("sin(2 * x) - 2 * sin(x) * cos(x)", "0")] + [InlineData("cos(2 * x) - (1 - 2 * sin(x) ^ 2)", "0")] + public void NeighbouringCancellationsAreUnaffected(string expression, string expected) => + Assert.Equal(expected.ToEntity().Simplify(), Bare(expression)); + } +}