diff --git a/BREAKING-CHANGES.md b/BREAKING-CHANGES.md index 12aa10633..1a1ab7b56 100644 --- a/BREAKING-CHANGES.md +++ b/BREAKING-CHANGES.md @@ -217,6 +217,23 @@ its arguments and answers may change in a minor release. `amcli mcp` no longer l | `am_classify` on `G*M*m/r^2`, to `amcli mcp` | `solved`, `Newtonian gravitation` first | `failed`: `'am_classify' is preliminary, and is served only when the server is started as 'amcli mcp --preliminary'` | | the same, to `amcli mcp --preliminary` | `solved` (the flag is ignored) | `solved`, and `"preliminary": true` | +### A cosine of a sum is expanded, as a sine of one is + +**Silent: identities in a shifted cosine reduce, and `Expand` opens a cosine of a sum.** The expansion rules had the +sine's addition theorem and not the cosine's, so no shift of a cosine was reduced: `sin(pi - x) - sin(x)` was `0` and +`cos(pi - x) + cos(x)` stayed as written. `cos(a + b)` and `cos(a - b)` are now expanded too. A shift whose reduced +form is shorter comes back reduced. `cos(pi - x)` on its own stays, as `sin(pi + x)` always has, because `-cos(x)` +is rated no shorter ([#2135](https://github.com/asc-community/AngouriMath/issues/2135)). + +| Input | Was (2.6.0) | Now | +|---|---|---| +| `"cos(pi/2 - x)".Simplify()` | `cos(pi / 2 - x)` | `sin(x)` | +| `"cos(2*pi - x)".Simplify()` | `cos(2 * pi - x)` | `cos(x)` | +| `"cos(pi - x) + cos(x)".Simplify()` | `cos(pi - x) + cos(x)` | `0` | +| `"cos(s + t) - cos(s)*cos(t) + sin(s)*sin(t)".Simplify()` | unchanged | `0` | +| `"cos(a + b)".Expand()` | `cos(a + b)` | `cos(a) * cos(b) + -sin(a) * sin(b)` | +| `"cos(a - b)".Expand()` | `cos(a - b)` | `cos(a) * cos(b) + sin(a) * sin(b)` | + ### A subscript may have a subscript **Not silent: what was a parse error is a name.** A name took at most one `_`, so `x_1_2` and `X_a_beta` were lexer diff --git a/Sources/.editorconfig b/Sources/.editorconfig index 090d52ebb..a834a9318 100644 --- a/Sources/.editorconfig +++ b/Sources/.editorconfig @@ -279,6 +279,9 @@ file_header_template=\nCopyright (c) 2019-2026 Angouri.\nAngouriMath is licensed [Tests/UnitTests/Algebra/SolveTest/DuplicateSystemRowTest.cs] file_header_template=\nCopyright (c) 2019-2026 Angouri.\nAngouriMath is licensed under MIT.\nDetails: https://github.com/asc-community/AngouriMath/blob/master/LICENSE.md.\nWebsite: https://am.angouri.org.\n +[Tests/UnitTests/Common/CosineOfASumTest.cs] +file_header_template=\nCopyright (c) 2019-2026 Angouri.\nAngouriMath is licensed under MIT.\nDetails: https://github.com/asc-community/AngouriMath/blob/master/LICENSE.md.\nWebsite: https://am.angouri.org.\n + [Tests/UnitTests/Common/LambdaArrowSyntaxTest.cs] file_header_template=\nCopyright (c) 2019-2026 Angouri.\nAngouriMath is licensed under MIT.\nDetails: https://github.com/asc-community/AngouriMath/blob/master/LICENSE.md.\nWebsite: https://am.angouri.org.\n diff --git a/Sources/AngouriMath/Core/Transformations/Matching/MatchedRules.cs b/Sources/AngouriMath/Core/Transformations/Matching/MatchedRules.cs index 2bef66f90..8c862ab46 100644 --- a/Sources/AngouriMath/Core/Transformations/Matching/MatchedRules.cs +++ b/Sources/AngouriMath/Core/Transformations/Matching/MatchedRules.cs @@ -612,7 +612,39 @@ internal static class MatchedRules MatchPattern.Node(MatchPattern.Any("b")), MatchPattern.Node(MatchPattern.Any("a")))), Soundness.Sound, - description: "sin(a - b) = sin(a)cos(b) - sin(b)cos(a)")); + description: "sin(a - b) = sin(a)cos(b) - sin(b)cos(a)"), + + // cos(a + b) -> cos(a)cos(b) - sin(a)sin(b) + // https://github.com/asc-community/AngouriMath/issues/2135 + new MatchedRule( + "cosine-of-a-sum", + MatchPattern.Node( + MatchPattern.Node(MatchPattern.Any("a"), MatchPattern.Any("b"))), + MatchPattern.Node( + MatchPattern.Node( + MatchPattern.Node(MatchPattern.Any("a")), + MatchPattern.Node(MatchPattern.Any("b"))), + MatchPattern.Node( + MatchPattern.Node(MatchPattern.Any("a")), + MatchPattern.Node(MatchPattern.Any("b")))), + // The addition theorem again, for every complex a and b. + Soundness.Sound, + description: "cos(a + b) = cos(a)cos(b) - sin(a)sin(b)"), + + // cos(a - b) -> cos(a)cos(b) + sin(a)sin(b) + new MatchedRule( + "cosine-of-a-difference", + MatchPattern.Node( + MatchPattern.Node(MatchPattern.Any("a"), MatchPattern.Any("b"))), + MatchPattern.Node( + MatchPattern.Node( + MatchPattern.Node(MatchPattern.Any("a")), + MatchPattern.Node(MatchPattern.Any("b"))), + MatchPattern.Node( + MatchPattern.Node(MatchPattern.Any("a")), + MatchPattern.Node(MatchPattern.Any("b")))), + Soundness.Sound, + description: "cos(a - b) = cos(a)cos(b) + sin(a)sin(b)")); /// /// , as data. diff --git a/Sources/AngouriMath/Core/Transformations/Saturation.cs b/Sources/AngouriMath/Core/Transformations/Saturation.cs index 7bc6bddbf..5861b2495 100644 --- a/Sources/AngouriMath/Core/Transformations/Saturation.cs +++ b/Sources/AngouriMath/Core/Transformations/Saturation.cs @@ -43,9 +43,9 @@ internal static class Saturation /// one, and that is a measurement rather than a preference. It reads like the value a /// ceiling should refuse — it means the rule's growth was not judged, because its /// replacement is code rather than a written pattern, so admitting it accepts a rewrite - /// nobody measured. But it is where the rules are: of the 324 rules in - /// , 124 collect, 49 rearrange, 32 expand and 123 are - /// unjudged. A ceiling that refuses the fourth value still refuses 38% of the library, + /// nobody measured. But it is where the rules are: of the 337 rules in + /// , 126 collect, 49 rearrange, 36 expand and 126 are + /// unjudged. A ceiling that refuses the fourth value still refuses 37% of the library, /// and what is left could not do much when this was written — over six expression pairs /// equal only through a larger intermediate form, the /// ceiling proved two, and diff --git a/Sources/AngouriMath/Docs/Contributing/WritingARule.md b/Sources/AngouriMath/Docs/Contributing/WritingARule.md index 1af3fdf87..0bb1be776 100644 --- a/Sources/AngouriMath/Docs/Contributing/WritingARule.md +++ b/Sources/AngouriMath/Docs/Contributing/WritingARule.md @@ -18,7 +18,7 @@ effect". This is how to supply those seven things. ## Where a rule goes `Core/Transformations/Matching/MatchedRules.cs`, as a value in a `MatchedRuleSet`. **33** sets and -**335** rules live there today. +**337** rules live there today. The `switch` statements in `Functions/Simplification/Patterns` are the older form. **All thirty registered sets now run as data and describe what they run**; none executes its `switch` any more. @@ -64,7 +64,7 @@ in a name and putting the identity in brackets after it: 2. Tangent is sine over cosine (tan(a) = sin(a) / cos(a)), so tan(x) becomes sin(x) / cos(x). ``` -So the name has to be a clause that survives being read that way. **All 306 distinct rule names are, +So the name has to be a clause that survives being read that way. **All 308 distinct rule names are, and `StepAsASentenceTest` holds them to it** — a name with a capital, a bracket or an underscore fails that test rather than degrading the prose quietly. @@ -91,7 +91,7 @@ debugged for an afternoon. | `Left.ToString()` | `Divf(var a, Divf(var b, var c))` — how the matcher spells it | Write the identity with `=`, not `->`: it is an equality, and the arrow belongs to the direction the -rule happens to be applied in. **332** rules carry one today; a new rule should. +rule happens to be applied in. **334** rules carry one today; a new rule should. ## The pattern language @@ -123,12 +123,12 @@ matches and silently builds nothing. `BuildableNodeTypesTest` is the list. ## Replacement: a pattern, or code -**35** of the 328 rules have a pattern on both sides. The rest build their answer in code, and both +**38** of the 337 rules have a pattern on both sides. The rest build their answer in code, and both are legitimate — but the choice costs two things, so make it deliberately. A pattern replacement gets: -- **a direction.** `MatchedRule.Reversed` is the rule read the other way, and **32** of the 36 +- **a direction.** `MatchedRule.Reversed` is the rule read the other way, and **34** of the 38 two-sided rules have one. The other four forget something: `sin²+cos² = 1` forgets the angle, `{ x : x in S } = S` forgets the name the set builder bound, and `c ^ log(c, a) = a` and `c ^ (k * log(c, a)) = a ^ k` for a constant `c` forget which constant, so none has anything to @@ -226,7 +226,7 @@ limit, against nothing for handing the node over. | `SoundUnderAssumptions` | holds given something the rule does not check | | `Heuristic` | usually right | -**186** of the 335 rules are `Sound` and **149** are conditional. Every one of the thirty registered +**188** of the 337 rules are `Sound` and **149** are conditional. Every one of the thirty registered *sets* declares `SoundUnderAssumptions`, because a set's tier is the **minimum** over its rules — so the set grain says nothing and the rule grain says everything. A derivation reports the rule's tier (`RewriteStep.Soundness`), which is why getting it right matters beyond the label. diff --git a/Sources/AngouriMath/Functions/Simplification/Patterns/Patterns.ExpandFactorize.cs b/Sources/AngouriMath/Functions/Simplification/Patterns/Patterns.ExpandFactorize.cs index 47a9c7b8e..0659be336 100644 --- a/Sources/AngouriMath/Functions/Simplification/Patterns/Patterns.ExpandFactorize.cs +++ b/Sources/AngouriMath/Functions/Simplification/Patterns/Patterns.ExpandFactorize.cs @@ -21,6 +21,10 @@ internal static partial class Patterns { Sinf(Sumf(var any1, var any2)) => new Sinf(any1) * new Cosf(any2) + new Sinf(any2) * new Cosf(any1), Sinf(Minusf(var any1, var any2)) => new Sinf(any1) * new Cosf(any2) - new Sinf(any2) * new Cosf(any1), + // The cosine's, which were missing: cos(pi - x) + cos(x) did not reduce to 0 where + // sin(pi - x) - sin(x) did. https://github.com/asc-community/AngouriMath/issues/2135 + Cosf(Sumf(var any1, var any2)) => new Cosf(any1) * new Cosf(any2) - new Sinf(any1) * new Sinf(any2), + Cosf(Minusf(var any1, var any2)) => new Cosf(any1) * new Cosf(any2) + new Sinf(any1) * new Sinf(any2), _ => x }; diff --git a/Sources/Tests/UnitTests/Common/CosineOfASumTest.cs b/Sources/Tests/UnitTests/Common/CosineOfASumTest.cs new file mode 100644 index 000000000..74fcfb72c --- /dev/null +++ b/Sources/Tests/UnitTests/Common/CosineOfASumTest.cs @@ -0,0 +1,51 @@ +// +// 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 AngouriMath; +using AngouriMath.Extensions; +using Xunit; +using static AngouriMath.Entity.Number; + +namespace AngouriMath.Tests.Common +{ + /// + /// The expansion rules opened a sine of a sum and not a cosine, so no shift of a cosine was + /// reduced: sin(pi - x) - sin(x) was 0 and cos(pi - x) + cos(x) stayed as it + /// was, and so did cos(s + t) - cos(s) cos(t) + sin(s) sin(t). + /// #2135 + /// + [Trait("Area", "Common")] + public sealed class CosineOfASumTest + { + [Theory] + [InlineData("cos(pi - x) + cos(x)")] + [InlineData("cos(x - pi) + cos(x)")] + [InlineData("cos(pi/2 + x) + sin(x)")] + [InlineData("cos(s + t) - cos(s) * cos(t) + sin(s) * sin(t)")] + [InlineData("cos(s - t) - cos(s) * cos(t) - sin(s) * sin(t)")] + [InlineData("cos(pi/3 - x) - cos(x) / 2 - sqrt(3) * sin(x) / 2")] + public void TheIdentityIsZero(string identity) + => Assert.Equal(Integer.Zero, identity.Simplify()); + + /// + /// Where the shifted form is longer than what it reduces to, the reduced one is the + /// answer. cos(pi - x) on its own stays, as sin(pi + x) always has: + /// -cos(x) is rated no shorter. + /// + [Theory] + [InlineData("cos(pi/2 - x)", "sin(x)")] + [InlineData("cos(2 * pi - x)", "cos(x)")] + public void AShiftIsReducedWhereThatIsShorter(string shifted, string reduced) + => Assert.Equal(reduced.ToEntity(), shifted.Simplify()); + + [Theory] + [InlineData("cos(a + b)", "cos(a) * cos(b) - sin(a) * sin(b)")] + [InlineData("cos(a - b)", "cos(a) * cos(b) + sin(a) * sin(b)")] + public void ExpandOpensACosineOfASum(string cosine, string expanded) + => Assert.Equal(Integer.Zero, (cosine.ToEntity().Expand() - expanded).Simplify()); + } +} diff --git a/Sources/Tests/UnitTests/Core/Transformations/AddressableRulesTest.cs b/Sources/Tests/UnitTests/Core/Transformations/AddressableRulesTest.cs index 7b241bbb3..17ed1d916 100644 --- a/Sources/Tests/UnitTests/Core/Transformations/AddressableRulesTest.cs +++ b/Sources/Tests/UnitTests/Core/Transformations/AddressableRulesTest.cs @@ -299,7 +299,7 @@ public void TheRegistryIsAddressableAsFarAsItSaysItIs() // are thirty-three; NumericNeat's sixteen are eleven, six of them being three rules // written once per side a negative factor can sit on; and the two factorial sets are // eight arms each written as three. Every other repointed set is one arm for one rule. - Assert.Equal(332, withRules.Sum(set => set.Rules.Count)); + Assert.Equal(334, withRules.Sum(set => set.Rules.Count)); } /// diff --git a/Sources/Tests/UnitTests/Core/Transformations/RuleAuthoringGuideTest.cs b/Sources/Tests/UnitTests/Core/Transformations/RuleAuthoringGuideTest.cs index ae4db26ef..496a1603b 100644 --- a/Sources/Tests/UnitTests/Core/Transformations/RuleAuthoringGuideTest.cs +++ b/Sources/Tests/UnitTests/Core/Transformations/RuleAuthoringGuideTest.cs @@ -43,7 +43,7 @@ private static void Stated(int stated, int measured, string claim) public void WhereARuleGoes() { Stated(33, MatchedRules.All.Count, "the number of rule sets written as data"); - Stated(335, MatchedRules.All.Sum(set => set.Rules.Count), "the number of rules written as data"); + Stated(337, MatchedRules.All.Sum(set => set.Rules.Count), "the number of rules written as data"); Stated(30, RewriteRules.All.Count, "the number of registered sets"); // Two families register under one name and run a data set under another -- // CommonDenominator over MatchedRules.CommonDenominator(level), CanonicalOrder over @@ -64,7 +64,7 @@ public void TheNameIsASentence() { var names = MatchedRules.All.SelectMany(set => set.Rules) .Select(rule => rule.Name).Distinct(StringComparer.Ordinal).ToList(); - Stated(306, names.Count, "the number of distinct rule names"); + Stated(308, names.Count, "the number of distinct rule names"); var words = names.Select(name => name.Split('-').Length).ToList(); Stated(4, words.Min(), "the shortest rule name in words"); @@ -73,7 +73,7 @@ public void TheNameIsASentence() [Fact] public void TheIdentityIsNotTheName() - => Stated(332, + => Stated(334, MatchedRules.All.SelectMany(set => set.Rules).Count(rule => rule.Description is not null), "how many rules carry an identity"); @@ -85,9 +85,9 @@ public void ThePatternLanguage() public void ReplacementAPatternOrCode() { var rules = MatchedRules.All.SelectMany(set => set.Rules).ToList(); - Stated(36, rules.Count(rule => rule.Right is not null), + Stated(38, rules.Count(rule => rule.Right is not null), "how many rules have a pattern on both sides"); - Stated(32, rules.Count(rule => rule.Reversed is not null), + Stated(34, rules.Count(rule => rule.Reversed is not null), "how many two-sided rules have a direction"); Stated(126, rules.Count(rule => rule.Growth is RewriteRuleGrowth.Unknown), "how many rules sit at Unknown growth"); @@ -101,7 +101,7 @@ public void ReplacementAPatternOrCode() "how many rules are declared Collects"); Stated(49, rules.Count(rule => rule.Growth is RewriteRuleGrowth.Rearranges), "how many rules are declared Rearranges"); - Stated(34, rules.Count(rule => rule.Growth is RewriteRuleGrowth.Expands), + Stated(36, rules.Count(rule => rule.Growth is RewriteRuleGrowth.Expands), "how many rules are declared Expands"); // And the three the document names. By their own names rather than by what the prose @@ -132,7 +132,7 @@ public void ReplacementAPatternOrCode() public void SoundnessIsPerRule() { var rules = MatchedRules.All.SelectMany(set => set.Rules).ToList(); - Stated(186, rules.Count(rule => rule.Soundness is Soundness.Sound), + Stated(188, rules.Count(rule => rule.Soundness is Soundness.Sound), "how many rules are Sound"); Stated(149, rules.Count(rule => rule.Soundness is Soundness.SoundUnderAssumptions), "how many rules are SoundUnderAssumptions"); diff --git a/Sources/Tests/UnitTests/Core/Transformations/RuleMetadataTest.cs b/Sources/Tests/UnitTests/Core/Transformations/RuleMetadataTest.cs index 6f8c9c8a5..406f0dbf5 100644 --- a/Sources/Tests/UnitTests/Core/Transformations/RuleMetadataTest.cs +++ b/Sources/Tests/UnitTests/Core/Transformations/RuleMetadataTest.cs @@ -88,8 +88,8 @@ public void AddressableGrowthIsTheRulesOwnAnswer() public void SoundnessIsFinerPerRuleThanPerSet() { var rules = MatchedRules.All.SelectMany(set => set.Rules).ToList(); - Assert.Equal(335, rules.Count); - Assert.Equal(186,rules.Count(rule => rule.Soundness is Soundness.Sound)); + Assert.Equal(337, rules.Count); + Assert.Equal(188,rules.Count(rule => rule.Soundness is Soundness.Sound)); Assert.Equal(149, rules.Count(rule => rule.Soundness is Soundness.SoundUnderAssumptions)); // And every set still reports the weakest of them, which is what makes the set grain @@ -174,7 +174,7 @@ public void EveryRuleOfARepointedSetCarriesItsIdentity() var repointed = RewriteRules.All .Where(set => set.Rules.Count > 0 && set.Rules.All(rule => rule.Soundness is not null)) .ToList(); - Assert.Equal(332, repointed.Sum(set => set.Rules.Count)); + Assert.Equal(334, repointed.Sum(set => set.Rules.Count)); Assert.All(repointed, set => Assert.All(set.Rules, rule => Assert.NotNull(rule.Description))); } diff --git a/Sources/Tests/UnitTests/Core/Transformations/StepAsASentenceTest.cs b/Sources/Tests/UnitTests/Core/Transformations/StepAsASentenceTest.cs index 777ee381e..aed9ab502 100644 --- a/Sources/Tests/UnitTests/Core/Transformations/StepAsASentenceTest.cs +++ b/Sources/Tests/UnitTests/Core/Transformations/StepAsASentenceTest.cs @@ -59,7 +59,7 @@ public void EveryRuleWrittenAsDataIsNamedInProse() { var names = MatchedRules.All.SelectMany(set => set.Rules).Select(rule => rule.Name) .Distinct(StringComparer.Ordinal).ToList(); - Assert.Equal(306, names.Count); + Assert.Equal(308, names.Count); Assert.All(names, name => Assert.True(Explanation.IsProse(name), $"'{name}' is a rule name that does not read as a phrase in English"));