diff --git a/BREAKING-CHANGES.md b/BREAKING-CHANGES.md index f3690fa0f..a838613d5 100644 --- a/BREAKING-CHANGES.md +++ b/BREAKING-CHANGES.md @@ -200,6 +200,19 @@ for `u` made a matrix of vectors. A lambda written in place is now applied befor | `apply(lambda(u, [[0, -1], [1, 0]] * u), [p, q])` | `[[0, [-p, -q]], [[p, q], 0]]` | `[-q, p]` | | `apply(lambda(u, [1, 1]T * u), [p, q])` | `[[[p, q], [p, q]]]` | `p + q` | +### One and a squared tangent are found anywhere in a sum + +**Silent: an identity reaches its value whichever way the sum is sorted.** `1 + tan(x)^2 = sec(x)^2` and +`1 + cot(x)^2 = csc(x)^2` were rules for two siblings, and secant sorts before tangent, so `1 + tan(x)^2 - sec(x)^2` +became `1 - sec(x)^2 + tan(x)^2` and stayed, while its cotangent twin, sorted the other way, was `0`. Both identities +are now also found wherever the `1` and the square sit in a sum, under the condition the tangent's domain carries +([#2142](https://github.com/asc-community/AngouriMath/issues/2142)). + +| Input | Was (2.6.0) | Now | +|---|---|---| +| `"1 + tan(x)^2 - sec(x)^2".Simplify()` | `1 + tan(x) ^ 2 - sec(x) ^ 2` | `0 provided not cos(x) = 0` | +| `"a + 1 + tan(y)^2 - sec(y)^2".Simplify()` | `a + 1 + tan(y) ^ 2 - sec(y) ^ 2` | `a provided not cos(y) = 0` | + ### A square system is not answered with rows that are no solution **Silent: a row that is no solution is gone, and a system with a row that fails for some values is `null`.** diff --git a/Sources/AngouriMath/Core/Transformations/Matching/MatchedRules.cs b/Sources/AngouriMath/Core/Transformations/Matching/MatchedRules.cs index d3bf1d123..5eaf6b069 100644 --- a/Sources/AngouriMath/Core/Transformations/Matching/MatchedRules.cs +++ b/Sources/AngouriMath/Core/Transformations/Matching/MatchedRules.cs @@ -370,7 +370,46 @@ internal static class MatchedRules MatchPattern.Exact(Integer.Create(2))))), MatchPattern.Node(MatchPattern.Any("k"), MatchPattern.Any("rest")), Soundness.Sound, - description: "k sin(x)^2 + k cos(x)^2 = k")); + description: "k sin(x)^2 + k cos(x)^2 = k"), + + // ... + 1 + ... + tan(x)^2 + ... -> sec(x)^2 + (everything else), and the same with + // the cotangent. The identity divided through by cos^2 and by sin^2 is also in + // TrigonometricRules, but for two siblings only, so the sort decided again: secant + // sorts before tangent, and 1 + tan(x)^2 - sec(x)^2 became 1 - sec(x)^2 + tan(x)^2 + // and stayed, while cotangent sorts before cosecant and its twin was 0. + // SoundUnderAssumptions as there: each side is defined only where cos (sin) is not 0. + // https://github.com/asc-community/AngouriMath/issues/2142 + new MatchedRule( + "one-and-a-squared-tangent-anywhere-in-a-sum-make-a-squared-secant", + MatchPattern.Gathered( + "rest", + MatchPattern.Exact(Integer.Create(1)), + MatchPattern.Node( + MatchPattern.Node(MatchPattern.Any("x")), + MatchPattern.Exact(Integer.Create(2)))), + MatchPattern.Node( + MatchPattern.Node( + MatchPattern.Node(MatchPattern.Any("x")), + MatchPattern.Exact(Integer.Create(2))), + MatchPattern.Any("rest")), + Soundness.SoundUnderAssumptions, + description: "1 + tan(x)^2 = sec(x)^2"), + + new MatchedRule( + "one-and-a-squared-cotangent-anywhere-in-a-sum-make-a-squared-cosecant", + MatchPattern.Gathered( + "rest", + MatchPattern.Exact(Integer.Create(1)), + MatchPattern.Node( + MatchPattern.Node(MatchPattern.Any("x")), + MatchPattern.Exact(Integer.Create(2)))), + MatchPattern.Node( + MatchPattern.Node( + MatchPattern.Node(MatchPattern.Any("x")), + MatchPattern.Exact(Integer.Create(2))), + MatchPattern.Any("rest")), + Soundness.SoundUnderAssumptions, + description: "1 + cotan(x)^2 = cosec(x)^2")); /// /// , as data. diff --git a/Sources/AngouriMath/Core/Transformations/Saturation.cs b/Sources/AngouriMath/Core/Transformations/Saturation.cs index 76b7e0c89..19042aa38 100644 --- a/Sources/AngouriMath/Core/Transformations/Saturation.cs +++ b/Sources/AngouriMath/Core/Transformations/Saturation.cs @@ -43,8 +43,8 @@ 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 338 rules in - /// , 127 collect, 49 rearrange, 36 expand and 126 are + /// nobody measured. But it is where the rules are: of the 340 rules in + /// , 129 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 diff --git a/Sources/AngouriMath/Docs/Contributing/WritingARule.md b/Sources/AngouriMath/Docs/Contributing/WritingARule.md index cef8c2add..87701090d 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 -**338** rules live there today. +**340** 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 309 distinct rule names are, +So the name has to be a clause that survives being read that way. **All 311 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. **335** rules carry one today; a new rule should. +rule happens to be applied in. **337** 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 -**39** of the 338 rules have a pattern on both sides. The rest build their answer in code, and both +**41** of the 340 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 **34** of the 39 +- **a direction.** `MatchedRule.Reversed` is the rule read the other way, and **36** of the 41 two-sided rules have one. The other five forget something: `sin²+cos² = 1` and `k sin² + k cos² = k` forget the angle, `{ x : x in S } = S` forgets the name the set builder bound, and `c ^ log(c, a) = a` and @@ -227,7 +227,7 @@ limit, against nothing for handing the node over. | `SoundUnderAssumptions` | holds given something the rule does not check | | `Heuristic` | usually right | -**189** of the 338 rules are `Sound` and **149** are conditional. Every one of the thirty registered +**189** of the 340 rules are `Sound` and **151** 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/Tests/UnitTests/Common/TangentSecantIdentityTest.cs b/Sources/Tests/UnitTests/Common/TangentSecantIdentityTest.cs new file mode 100644 index 000000000..07a407817 --- /dev/null +++ b/Sources/Tests/UnitTests/Common/TangentSecantIdentityTest.cs @@ -0,0 +1,45 @@ +// +// 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; +using static AngouriMath.Entity.Number; + +namespace AngouriMath.Tests.Common +{ + /// + /// 1 + tan(x)^2 = sec(x)^2 was found for two siblings only, and secant sorts before + /// tangent, so 1 + tan(x)^2 - sec(x)^2 became 1 - sec(x)^2 + tan(x)^2 and stayed, + /// while its cotangent twin, where cotangent sorts first, was 0. + /// #2142 + /// + [Trait("Area", "Common")] + public sealed class TangentSecantIdentityTest + { + [Theory] + [InlineData("1 + tan(x)^2 - sec(x)^2", "0")] + [InlineData("1 + cot(x)^2 - cosec(x)^2", "0")] + [InlineData("a + 1 + tan(y)^2 - sec(y)^2", "a")] + [InlineData("tan(x)^2 - sec(x)^2 + 1", "0")] + public void TheIdentityHolds(string input, string expected) + { + var difference = (input.Simplify() - expected.ToEntity()).Simplify(); + while (difference is Providedf(var inner, _)) + difference = inner; + Assert.Equal(Integer.Zero, difference); + } + + /// + /// Where the tangent is undefined the answer is too, as the input is. + /// + [Fact] + public void TheAnswerKeepsTheTangentsDomain() + => Assert.Contains("cos(x) = 0", "1 + tan(x)^2 - sec(x)^2".Simplify().Stringize()); + } +} diff --git a/Sources/Tests/UnitTests/Core/Transformations/ReversibleRuleTest.cs b/Sources/Tests/UnitTests/Core/Transformations/ReversibleRuleTest.cs index a24828c85..dbe63ddc1 100644 --- a/Sources/Tests/UnitTests/Core/Transformations/ReversibleRuleTest.cs +++ b/Sources/Tests/UnitTests/Core/Transformations/ReversibleRuleTest.cs @@ -437,16 +437,24 @@ public void AReversedRuleSaysWhatAnExpressionCameFrom() [Fact] public void ARuleThatForgetsAHoleHasNoBackwardsReading() { - // Both rules of the set, with a coefficient and without, drop the argument. - foreach (var pythagoras in MatchedRules.PythagoreanIdentity.Rules) + // Both Pythagorean rules of the set, with a coefficient and without, drop the + // argument. The set's other two, 1 + tan^2 = sec^2 and its cotangent twin, keep it, + // and they are the only ones its reversal holds. + var pythagoras = MatchedRules.PythagoreanIdentity.Rules + .Where(rule => rule.Name.StartsWith("squared-sine-and-cosine", StringComparison.Ordinal)) + .ToList(); + Assert.Equal(2, pythagoras.Count); + foreach (var rule in pythagoras) { - Assert.Equal(RuleReversal.ReplacementDropsHoles, pythagoras.Reversal); - Assert.Null(pythagoras.Reversed); + Assert.Equal(RuleReversal.ReplacementDropsHoles, rule.Reversal); + Assert.Null(rule.Reversed); } - // And the reversed set is empty rather than absent, which is the difference being - // reported rather than hidden. - Assert.Empty(MatchedRules.PythagoreanIdentity.Reversed.Rules); + // And what the reversed set holds is the reversals of the others only, rather than + // anything standing for the two that have none. + Assert.Equal( + MatchedRules.PythagoreanIdentity.Rules.Count(rule => rule.Reversed is not null), + MatchedRules.PythagoreanIdentity.Reversed.Rules.Count); Assert.Equal( "1 + a".ToEntity(), MatchedRules.PythagoreanIdentity.Reversed.ApplyHere("1 + a".ToEntity())); diff --git a/Sources/Tests/UnitTests/Core/Transformations/RuleAuthoringGuideTest.cs b/Sources/Tests/UnitTests/Core/Transformations/RuleAuthoringGuideTest.cs index 035b687c1..878affbea 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(338, MatchedRules.All.Sum(set => set.Rules.Count), "the number of rules written as data"); + Stated(340, 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(309, names.Count, "the number of distinct rule names"); + Stated(311, 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(335, + => Stated(337, 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(39, rules.Count(rule => rule.Right is not null), + Stated(41, rules.Count(rule => rule.Right is not null), "how many rules have a pattern on both sides"); - Stated(34, rules.Count(rule => rule.Reversed is not null), + Stated(36, 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"); @@ -97,7 +97,7 @@ public void ReplacementAPatternOrCode() // rearrange, 9 expand and 270 unjudged", measured before the growth declarations // went in. The argument survived; the arithmetic did not. A number a remark reasons // from belongs somewhere that fails when it moves. - Stated(127, rules.Count(rule => rule.Growth is RewriteRuleGrowth.Collects), + Stated(129, rules.Count(rule => rule.Growth is RewriteRuleGrowth.Collects), "how many rules are declared Collects"); Stated(49, rules.Count(rule => rule.Growth is RewriteRuleGrowth.Rearranges), "how many rules are declared Rearranges"); @@ -135,7 +135,7 @@ public void SoundnessIsPerRule() var rules = MatchedRules.All.SelectMany(set => set.Rules).ToList(); Stated(189, rules.Count(rule => rule.Soundness is Soundness.Sound), "how many rules are Sound"); - Stated(149, rules.Count(rule => rule.Soundness is Soundness.SoundUnderAssumptions), + Stated(151, rules.Count(rule => rule.Soundness is Soundness.SoundUnderAssumptions), "how many rules are SoundUnderAssumptions"); Assert.All(RewriteRules.All, set => Assert.Equal(Soundness.SoundUnderAssumptions, set.Soundness)); diff --git a/Sources/Tests/UnitTests/Core/Transformations/RuleMetadataTest.cs b/Sources/Tests/UnitTests/Core/Transformations/RuleMetadataTest.cs index c0681b2bd..e8e7f9267 100644 --- a/Sources/Tests/UnitTests/Core/Transformations/RuleMetadataTest.cs +++ b/Sources/Tests/UnitTests/Core/Transformations/RuleMetadataTest.cs @@ -88,9 +88,9 @@ public void AddressableGrowthIsTheRulesOwnAnswer() public void SoundnessIsFinerPerRuleThanPerSet() { var rules = MatchedRules.All.SelectMany(set => set.Rules).ToList(); - Assert.Equal(338, rules.Count); + Assert.Equal(340, rules.Count); Assert.Equal(189,rules.Count(rule => rule.Soundness is Soundness.Sound)); - Assert.Equal(149, rules.Count(rule => rule.Soundness is Soundness.SoundUnderAssumptions)); + Assert.Equal(151, rules.Count(rule => rule.Soundness is Soundness.SoundUnderAssumptions)); // And every set still reports the weakest of them, which is what makes the set grain // uninformative rather than wrong. diff --git a/Sources/Tests/UnitTests/Core/Transformations/StepAsASentenceTest.cs b/Sources/Tests/UnitTests/Core/Transformations/StepAsASentenceTest.cs index bb4881ea4..e71c6ef15 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(309, names.Count); + Assert.Equal(311, 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"));