From dd6206393969b499100d9a379f802a3c45bc460b Mon Sep 17 00:00:00 2001 From: Rafael Vuijk Date: Wed, 5 Aug 2026 13:33:31 +0000 Subject: [PATCH] Simplify an opened multiple angle before judging it (#557) The reporter's second expression is 0 and came back as a sum of five terms. Its first was already handled -- opening sin(2t) up is what settles it -- and the second needs the same opening to survive one step further than it did. Opening a multiple angle is offered as a candidate rather than taken, because written out sin(4x) is longer than it started, and the complexity metric decides. That judgement is right; what was wrong is the form it was asked to judge. The opened form was handed to the trigonometric rules only, which settles an expression that is already a single term. This one is a quotient, and it cancels only once its terms are over a common denominator -- and the passes that build one run earlier in the loop, while the angles are still shut. So the metric saw the opened form with none of its payoff collected and rightly rejected it as the longer of the two. It is now simplified in full first, which is what Expand and Factorize already get two lines above, and for the same reason. Expanded as well: the cancellation here only appears once the products are multiplied out, so `sin(t)^6 - (2 sin(t) cos(t))^2 sin(t)^4/4` over `sin(t)^8` has to be opened before it reads as 1. Only one candidate, not two. Offering the unexpanded opening as well made no difference to any expression measured and doubled the added cost. Measured: the reporter's second expression from a sum of five terms to `0 provided not sin(t) = 0`, in 269 ms. Multiple angles that do not cancel keep their compact form -- sin(4x), cos(6x) and sin(3x) are unchanged, and sin(8x)cos(8x) still collapses to sin(16x)/2 -- and cost roughly 1.2 to 2 times what they did, in the tens of milliseconds; only an expression that contains a multiple angle pays anything. Full suite 4858 passed / 0 failed, F# 130/130, corpus 112/117 with 0 wrong, 0 error, 0 timeout, and unchanged in total time. Co-Authored-By: Claude Opus 5 --- .../Functions/Simplification/Simplificator.cs | 20 +++++++++++++ .../PatternsTest/DoubleAngleOverSineTest.cs | 28 +++++++++++++++++++ 2 files changed, 48 insertions(+) diff --git a/Sources/AngouriMath/Functions/Simplification/Simplificator.cs b/Sources/AngouriMath/Functions/Simplification/Simplificator.cs index a8898d766..36cf0b2b1 100644 --- a/Sources/AngouriMath/Functions/Simplification/Simplificator.cs +++ b/Sources/AngouriMath/Functions/Simplification/Simplificator.cs @@ -206,6 +206,26 @@ not solve this issue completely and yet too slow to be accepted. { AddHistory(res.Expand().Simplify(-level)); AddHistory(res.Factorize().Simplify(-level)); + + // A multiple angle written out is worth having only where the pieces then + // cancel, so it has to be simplified in full before the metric can be + // asked -- the same reason Expand and Factorize are re-simplified above, + // and for the same reason it is a candidate rather than a step. The pass + // inside the loop offers the opened form to the trigonometric rules only, + // which settles an expression that is already one term; it cannot settle + // one whose cancellation needs a common denominator, because the passes + // that build one ran earlier in the loop, while the angles were still shut. + // That is https://github.com/asc-community/AngouriMath/issues/557: the + // reporter's second expression is 0, and reaches 0 through + // 2 sin(t) cos(t) and not through sin(2t). + var openedAngles = res + .Replace(Patterns.ExpandMultipleAngleRules) + .Replace(Patterns.NormalTrigonometricForm) + .InnerSimplified; + if (openedAngles != res) + // Expanded, and for the same reason res is expanded above: the + // cancellation only shows up once the products are multiplied out. + AddHistory(openedAngles.Expand().Simplify(-level)); } return history.Values.SelectMany(x => x); diff --git a/Sources/Tests/UnitTests/PatternsTest/DoubleAngleOverSineTest.cs b/Sources/Tests/UnitTests/PatternsTest/DoubleAngleOverSineTest.cs index 14823395e..a2e0079a0 100644 --- a/Sources/Tests/UnitTests/PatternsTest/DoubleAngleOverSineTest.cs +++ b/Sources/Tests/UnitTests/PatternsTest/DoubleAngleOverSineTest.cs @@ -37,6 +37,34 @@ private static Entity Bare(string expression) public void TheDoubleAngleOverTheSingleOne(string expression, string expected) => Assert.Equal(expected.ToEntity().Simplify(), Bare(expression)); + /// + /// The second expression the reporter gives, which is the first one with a + /// quotient of sixth and eighth powers of sin(t) added in front of it. Both are 0. + /// + /// This one needs the opened angle to survive as far as the fractions: the quotient + /// only cancels once its terms are over a common denominator, and the passes that + /// build one run before the angles are opened. So the opened form was offered to the + /// complexity metric in the one shape where its payoff had not happened yet, and was + /// rightly rejected as the longer of the two. It is now simplified in full first -- + /// including expanded, since the cancellation only shows up once the products are + /// multiplied out -- which is what Expand and Factorize already get. + /// + [Fact] + public void TheReportersSecondExpressionIsAlsoZero() => + Assert.Equal(Entity.Number.Integer.Create(0), Bare( + "(cos(2 * t) * sin(t) ^ 6 * (-1) + cos(t) * sin(t) ^ 5 * sin(2 * t)" + + " - sin(2 * t) ^ 2 * sin(t) ^ 4 / 4) / sin(t) ^ 8 - 1" + + " + (sin(2 * t) * cosec(t)) ^ 2 / 4 - cos(2 * t) - sin(t) ^ 2")); + + // The pieces it is built from, each 0 or 1 on its own. Together they say the + // cancellation is the quotient's and not only the tail's. + [Theory] + [InlineData("(cos(2 * t) * sin(t) ^ 6 * (-1) + cos(t) * sin(t) ^ 5 * sin(2 * t)" + + " - sin(2 * t) ^ 2 * sin(t) ^ 4 / 4) / sin(t) ^ 8", "1")] + [InlineData("(sin(t) ^ 6 - sin(2 * t) ^ 2 * sin(t) ^ 4 / 4) / sin(t) ^ 8 - 1", "0")] + public void TheQuotientCancelsOnItsOwn(string expression, string expected) => + Assert.Equal(expected.ToEntity(), 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