diff --git a/BREAKING-CHANGES.md b/BREAKING-CHANGES.md index 10b714cab..24203c315 100644 --- a/BREAKING-CHANGES.md +++ b/BREAKING-CHANGES.md @@ -34,6 +34,28 @@ read first. | **silent** | `"1/(x^4 + 4)".Integrate("x")` | unevaluated | the antiderivative over its two quadratic factors | | **silent** | `"sin(-x) + sin(x)".Simplify()` | `sin(-x) + sin(x)` | `0` | | **silent** | `"cos(-x)".Simplify()` and `"abs(-x)".Simplify()` | unchanged | `cos(x)`, `abs(x)` | +| **silent** | `"cos(0 ^ y)".InnerSimplified` | `-(-1) provided ...` | `1 provided ...` | + +### `InnerSimplified` is idempotent again + +An exact trigonometric value reached through a half turn — `cos(x)` read off the table as +`-cos(pi - x)` — was built as a negation over the value it turned to and handed back unfolded. Where +the answer is then *wrapped* rather than rebuilt, nothing normalises it again: + +``` +"cos(0 ^ y)".InnerSimplified + +was -(-1) provided y / 2 * (1 + 1 / sgn(y) ^ 2) > 0 +is 1 provided y / 2 * (1 + 1 / sgn(y) ^ 2) > 0 +``` + +Both are the same value, so nothing was wrong — but applying `InnerSimplified` twice gave a different +tree from applying it once, and a great deal of the library treats what it hands back as settled. +`Simplify` was unaffected, since it normalises again anyway. + +Found by `canoncheck`, the canonical-form harness: it was the only idempotence failure in 834 +generated expressions, and the count is now zero. +[#930](https://github.com/asc-community/AngouriMath/issues/930). ### The parity identities are applied diff --git a/Sources/AngouriMath/Functions/Simplification/TrigonometricTableValues.cs b/Sources/AngouriMath/Functions/Simplification/TrigonometricTableValues.cs index 38fe31cb6..8b78be55b 100644 --- a/Sources/AngouriMath/Functions/Simplification/TrigonometricTableValues.cs +++ b/Sources/AngouriMath/Functions/Simplification/TrigonometricTableValues.cs @@ -92,7 +92,14 @@ private static bool OrHalfTurn(System.Func } if (lookup(HalfTurn(arg)) is (true, { } turned)) { - res = oddOverHalfTurn ? -turned : turned; + // Folded here rather than left to whoever receives it. The negation is built + // above a value that is already in its final form, so nothing downstream is + // obliged to look at it again -- and where the answer is wrapped rather than + // rebuilt, nothing does: cos(0 ^ y) came back as `-(-1) provided ...` and + // needed a second InnerSimplified to become `1 provided ...`, which is an + // idempotence failure rather than a cosmetic one. + // https://github.com/asc-community/AngouriMath/issues/930 + res = oddOverHalfTurn ? (-turned).InnerSimplified : turned; return true; } res = null; diff --git a/Sources/Tests/UnitTests/Common/InnerSimplifyTest.cs b/Sources/Tests/UnitTests/Common/InnerSimplifyTest.cs index 34a503063..6a2cc846d 100644 --- a/Sources/Tests/UnitTests/Common/InnerSimplifyTest.cs +++ b/Sources/Tests/UnitTests/Common/InnerSimplifyTest.cs @@ -427,6 +427,34 @@ public void InequalityShouldBeNaN(string expr) { expr.ToEntity().Evaled.ShouldBe(MathS.NaN); expr.ToEntity().InnerSimplified.ShouldBe(MathS.NaN); } + + /// + /// Normalising a normalised expression leaves it alone. `cos(0 ^ y)` did not: the + /// exact value of a cosine reached through a half turn was built as a negation over + /// the value it turned to and handed back unfolded, so the answer arrived as + /// `-(-1) provided ...` and became `1 provided ...` only if something normalised it + /// again. https://github.com/asc-community/AngouriMath/issues/930 + /// + /// + /// Compared as entities rather than as strings, deliberately. The general failure here + /// includes two trees that print alike and differ — `(x + y) + a` and `x + (y + a)` do + /// — so a comparison of printed forms would wave those through. See + /// `Docs/Contributing/CanonicalForm.md`. + /// + [Theory] + [InlineData("cos(0 ^ y)")] + [InlineData("sin(0 ^ y)")] + [InlineData("cos(0)")] + [InlineData("cos(pi)")] + [InlineData("sin(pi / 6)")] + [InlineData("cos(pi / 3)")] + [InlineData("tan(pi / 4)")] + [InlineData("x + sin(0 ^ y)")] + public void InnerSimplifiedIsIdempotent(string expr) + { + var once = expr.ToEntity().InnerSimplified; + Assert.Equal(once, once.InnerSimplified); + } } }