diff --git a/BREAKING-CHANGES.md b/BREAKING-CHANGES.md index e3901e1c5..b91805b3d 100644 --- a/BREAKING-CHANGES.md +++ b/BREAKING-CHANGES.md @@ -68,24 +68,25 @@ read first. | **Silent** | `RewriteRules.RationalizeDenominator.Rules` | `[]` — the registry could not read the set | its two rules, addressable and named | | **Silent** | `RewriteRules.Power.ApplyOnce("ln(1 / x)")`, and every `log(_, 1/_)` and `log(1/_, _)` whose argument is not decidably a positive real | `-ln(x)`, which is wrong on the negative reals | `ln(1 / x)`, left alone | | **Silent** | `RewriteRules.Boolean.ApplyOnce("a and b or a")`, and two more orientations of absorption | left alone — the arm for that orientation was never written | `a` | -| **Silent** | `RewriteRules.DivisionPreparing.Rules[0].Name`, and every rule of twenty-three sets now described from their data form | `Mulf(var any1, Divf(Integer(1), var any2))` — the `switch` arm's rendered pattern | `reciprocal-factor-becomes-a-quotient` | +| **Silent** | `RewriteRules.DivisionPreparing.Rules[0].Name`, and every rule of twenty-four sets now described from their data form | `Mulf(var any1, Divf(Integer(1), var any2))` — the `switch` arm's rendered pattern | `reciprocal-factor-becomes-a-quotient` | | | `RewriteRules.ExpandFactorialDivisions.Rules.Count`, and `FactorizeFactorialMultiplications` | `8` | `3` — the same rewrites, five of the eight arms being one commutative pattern | | | `RewriteRules.Boolean.Rules.Count` | `36` | `20` — a commutative pattern finds a shared operand wherever it sits | | | `RewriteRules.NumericNeat.Rules.Count`, and `Factorization` 22 -> 11 | `16` | `11` | -| | `RewriteRules.All.Sum(set => set.Rules.Count)` | `407` | `365` | +| | `RewriteRules.Trigonometric.Rules.Count` | `43` | `33` | +| | `RewriteRules.All.Sum(set => set.Rules.Count)` | `407` | `355` | | **Silent** | `RewriteRules.ExpandFactorialDivisions.Rules[0].Growth` | `Collects` — guessed from string length | `Unknown`; whether it collects depends on the offsets | | **Silent** | `RewriteStep.Soundness` on a rewrite whose rule declares a tier — `RewriteRules.SetOperator` on `A /\ A`, and every rewrite of the nineteen sets described from their data form | `SoundUnderAssumptions` — its rule set's tier, which is the minimum over every rule in the set | `Sound` — the rule's own | | **Silent** | `DerivationStep.Soundness` | its rule set's tier | the weakest tier any rewrite that actually fired inside the step holds at | | **Silent** | `"arccotan(-1)".ToEntity().Simplify()`, and every negative argument the inverse-trigonometric table knows | `3/4 * pi` — the textbook range, and **not equal to `arccotan(-1)`**, whose value is `-pi/4` | `-1/4 * pi` | -### Twenty-three rule sets describe the rules they run +### Twenty-four rule sets describe the rules they run `RewriteRuleSet.Rules` is what the registry reports a set is made of, and for most of the library's life it came from `RuleRegistryGenerator` reading the `switch` that defined the set. Twenty-seven of the thirty sets stopped running that `switch` some releases ago — they run `MatchedRuleSet.ApplyHere` — and went on describing it. Thirteen of them now describe what they run. -Which twenty-three. Thirteen had **no described arm at all**, so repointing them could only add +Which twenty-four. Thirteen had **no described arm at all**, so repointing them could only add metadata: `CollapseMultipleFractions`, the three `CommonDenominator` sets, `DivisionPreparing`, `ExpandFactorialDivisions`, `ExpandMultipleAngle`, `ExpandTrigonometric`, `Expansion`, `FactorizeFactorialMultiplications`, `NormalTrigonometricForm`, `PhiFunction` and @@ -98,8 +99,8 @@ where the identities were **written** rather than carried across: its comments n rules' own patterns and replacements. `NumericNeat` and `Factorization` follow it, their comments already being identities and needing only the arrow turned into an equals sign. -The four left on the `switch` are the ones where repointing would cost something today: `Common` -would lose 33 descriptions, `Power` 22, `Trigonometric` 13 and `InequalityEquality` 11. Porting those identities is a later change. Three more — +The three left on the `switch` are the ones where repointing would cost something today: `Common` +would lose 33 descriptions, `Power` 22 and `InequalityEquality` 11. Porting those identities is a later change. Three more — the `CanonicalOrder` family — still *run* their `switch`, so describing it is not a mismatch. Four things move, and only the second changes a count: @@ -117,9 +118,9 @@ commutative pattern finds the shared operand wherever it sits, and absorption's rule twice. `ExpandFactorialDivisions` and `FactorizeFactorialMultiplications` are eight arms each written as three, the other five being one rewrite spelled once for each side a factorial can sit on. `NumericNeat`'s sixteen are eleven, six of them being three rules written once per side a negative -factor can sit on; `Factorization`'s twenty-two are eleven for the same reason. Every other repointed -set is one arm to one rule. Across the registry, 407 becomes **365** while the number of described -rules goes from **95 to 180**. +factor can sit on; `Factorization`'s twenty-two are eleven for the same reason, and +`Trigonometric`'s forty-three are thirty-three. Every other repointed set is one arm to one rule. +Across the registry, 407 becomes **355** while the number of described rules goes from **95 to 200**. `Rules[i].Growth` also stops being a guess. `AsAddressable` used to infer it by comparing the lengths of the two rendered pattern strings — the only thing available to a generator reading source text — diff --git a/Sources/AngouriMath/Core/Transformations/Matching/MatchedRules.cs b/Sources/AngouriMath/Core/Transformations/Matching/MatchedRules.cs index 452020d9a..c9b00a4b9 100644 --- a/Sources/AngouriMath/Core/Transformations/Matching/MatchedRules.cs +++ b/Sources/AngouriMath/Core/Transformations/Matching/MatchedRules.cs @@ -1480,7 +1480,8 @@ is var (divided, remainder) "a-sine-times-a-cosine-of-one-angle-is-half-the-doubled-sine", MatchPattern.Commutative(MatchPattern.Node(MatchPattern.Any("a")), MatchPattern.Node(MatchPattern.Any("a"))), bound => Rational.Create(1, 2) * new Sinf(2 * bound["a"]), - Soundness.Sound), + Soundness.Sound, + description: "sin(a) * cos(a) = (1/2) * sin(2a)"), // arccos(x) is pi/2 - arcsin(x) by definition, over the whole plane, so this needs // no assumption. @@ -1488,7 +1489,8 @@ is var (divided, remainder) "arcsine-plus-arccosine-is-a-right-angle", MatchPattern.Commutative(MatchPattern.Node(MatchPattern.Any("a")), MatchPattern.Node(MatchPattern.Any("a"))), bound => MathS.pi / 2, - Soundness.Sound), + Soundness.Sound, + description: "arcsin(a) + arccos(a) = pi/2"), // This library's arccotan is arctan(1/x) with range (-pi/2, pi/2], so the sum is // pi/2 for non-negative x and -pi/2 for negative x -- not pi/2 unconditionally, @@ -1499,7 +1501,8 @@ is var (divided, remainder) MatchPattern.Commutative(MatchPattern.Node(MatchPattern.Any("a")), MatchPattern.Node(MatchPattern.Any("a"))), bound => Functions.Patterns.ArctanPlusArccotan(bound["a"])!, Soundness.SoundUnderAssumptions, - when: bound => Functions.Patterns.ArctanPlusArccotan(bound["a"]) is not null), + when: bound => Functions.Patterns.ArctanPlusArccotan(bound["a"]) is not null, + description: "arctan(a) + arccotan(a) = pi/2 for a >= 0, and -pi/2 for a < 0"), // Holds as written only while ab < 1: past that the sum leaves the range arctan // answers in and the identity is off by a whole pi. Both arguments have to be @@ -1510,19 +1513,22 @@ is var (divided, remainder) bound => MathS.Arctan((((Real)bound["a"] + (Real)bound["b"]) / (1 - (Real)bound["a"] * (Real)bound["b"])).InnerSimplified), Soundness.SoundUnderAssumptions, - when: bound => ((Real)bound["a"] * (Real)bound["b"]).Evaled is Real product && product < 1), + when: bound => ((Real)bound["a"] * (Real)bound["b"]).Evaled is Real product && product < 1, + description: "arctan(a) + arctan(b) = arctan((a + b) / (1 - a*b)), while a*b < 1"), new MatchedRule( "the-arctangent-of-root-three", MatchPattern.Node(MatchPattern.Node(MatchPattern.Exact(Integer.Create(3)), MatchPattern.Exact(Rational.Create(1, 2)))), bound => MathS.pi / 3, - Soundness.Sound), + Soundness.Sound, + description: "arctan(sqrt(3)) = pi/3"), new MatchedRule( "the-arctangent-of-one-over-root-three", MatchPattern.Node(MatchPattern.Node(MatchPattern.Exact(Integer.Create(1)), MatchPattern.Node(MatchPattern.Exact(Integer.Create(3)), MatchPattern.Exact(Rational.Create(1, 2))))), bound => MathS.pi / 6, - Soundness.Sound), + Soundness.Sound, + description: "arctan(1 / sqrt(3)) = pi/6"), // The cosecant's own condition has to be carried: 2cos(u) is a number where // sin(u) is zero and sin(2u) csc(u) is not. @@ -1531,71 +1537,82 @@ is var (divided, remainder) "a-doubled-sine-times-a-cosecant-is-twice-the-cosine", MatchPattern.Commutative(MatchPattern.Node(MatchPattern.Node(MatchPattern.Exact(Integer.Create(2)), MatchPattern.Any("a"))), MatchPattern.Node(MatchPattern.Any("a"))), bound => (2 * new Cosf(bound["a"])).Provided(new Cosecantf(bound["a"]).DomainCondition), - Soundness.SoundUnderAssumptions), + Soundness.SoundUnderAssumptions, + description: "sin(2a) * cosec(a) = 2 * cos(a)"), new MatchedRule( "a-tangent-times-a-cotangent-of-one-angle-is-one", MatchPattern.Commutative(MatchPattern.Node(MatchPattern.Any("a")), MatchPattern.Node(MatchPattern.Any("a"))), bound => Integer.Create(1), - Soundness.SoundUnderAssumptions), + Soundness.SoundUnderAssumptions, + description: "tan(a) * cotan(a) = 1"), new MatchedRule( "arcsine-of-a-sine-inside-its-own-interval", MatchPattern.Node(MatchPattern.Node(MatchPattern.Any("a"))), bound => bound["a"], Soundness.SoundUnderAssumptions, - when: bound => Functions.Patterns.WithinHalfPi(bound["a"], closed: true)), + when: bound => Functions.Patterns.WithinHalfPi(bound["a"], closed: true), + description: "arcsin(sin(a)) = a, for a in [-pi/2; pi/2]"), new MatchedRule( "arccosine-of-a-cosine-inside-its-own-interval", MatchPattern.Node(MatchPattern.Node(MatchPattern.Any("a"))), bound => bound["a"], Soundness.SoundUnderAssumptions, - when: bound => Functions.Patterns.WithinZeroAndPi(bound["a"], closed: true)), + when: bound => Functions.Patterns.WithinZeroAndPi(bound["a"], closed: true), + description: "arccos(cos(a)) = a, for a in [0; pi]"), new MatchedRule( "arctangent-of-a-tangent-inside-its-own-interval", MatchPattern.Node(MatchPattern.Node(MatchPattern.Any("a"))), bound => bound["a"], Soundness.SoundUnderAssumptions, - when: bound => Functions.Patterns.WithinHalfPi(bound["a"], closed: false)), + when: bound => Functions.Patterns.WithinHalfPi(bound["a"], closed: false), + description: "arctan(tan(a)) = a, for a in (-pi/2; pi/2)"), new MatchedRule( "arccotangent-of-a-cotangent-inside-its-own-range", MatchPattern.Node(MatchPattern.Node(MatchPattern.Any("a"))), bound => bound["a"], Soundness.SoundUnderAssumptions, - when: bound => Functions.Patterns.WithinArccotanRange(bound["a"])), + when: bound => Functions.Patterns.WithinArccotanRange(bound["a"]), + description: "arccotan(cotan(a)) = a, for a in arccotan's own range"), new MatchedRule( "a-sine-of-an-arcsine", MatchPattern.Node(MatchPattern.Node(MatchPattern.Any("a"))), MatchPattern.Any("a"), - Soundness.Sound), + Soundness.Sound, + description: "sin(arcsin(a)) = a"), new MatchedRule( "a-cosine-of-an-arccosine", MatchPattern.Node(MatchPattern.Node(MatchPattern.Any("a"))), MatchPattern.Any("a"), - Soundness.Sound), + Soundness.Sound, + description: "cos(arccos(a)) = a"), new MatchedRule( "a-tangent-of-an-arctangent", MatchPattern.Node(MatchPattern.Node(MatchPattern.Any("a"))), MatchPattern.Any("a"), - Soundness.Sound), + Soundness.Sound, + description: "tan(arctan(a)) = a"), new MatchedRule( "a-cotangent-of-an-arccotangent", MatchPattern.Node(MatchPattern.Node(MatchPattern.Any("a"))), MatchPattern.Any("a"), - Soundness.Sound), + Soundness.Sound, + description: "cotan(arccotan(a)) = a"), new MatchedRule( "a-squared-sine-and-cosine-of-one-angle-sum-to-one", MatchPattern.Commutative(MatchPattern.Node(MatchPattern.Node(MatchPattern.Any("a")), MatchPattern.Exact(Integer.Create(2))), MatchPattern.Node(MatchPattern.Node(MatchPattern.Any("a")), MatchPattern.Exact(Integer.Create(2)))), bound => Integer.Create(1), - Soundness.Sound), + Soundness.Sound, + description: "sin(a)^2 + cos(a)^2 = 1"), // Only this direction: rewriting cos^2 back as 1 - sin^2 would undo it as fast // as it fired. @@ -1603,13 +1620,15 @@ is var (divided, remainder) "one-less-a-squared-sine-is-a-squared-cosine", MatchPattern.Node(MatchPattern.Exact(Integer.Create(1)), MatchPattern.Node(MatchPattern.Node(MatchPattern.Any("a")), MatchPattern.Exact(Integer.Create(2)))), bound => new Powf(new Cosf(bound["a"]), 2), - Soundness.Sound), + Soundness.Sound, + description: "1 - sin(a)^2 = cos(a)^2"), new MatchedRule( "one-less-a-squared-cosine-is-a-squared-sine", MatchPattern.Node(MatchPattern.Exact(Integer.Create(1)), MatchPattern.Node(MatchPattern.Node(MatchPattern.Any("a")), MatchPattern.Exact(Integer.Create(2)))), bound => new Powf(new Sinf(bound["a"]), 2), - Soundness.Sound), + Soundness.Sound, + description: "1 - cos(a)^2 = sin(a)^2"), // The identity divided through by cos^2. Knowing the plain one and not these made // the answer depend on which of the three ways an expression happened to be @@ -1618,89 +1637,103 @@ is var (divided, remainder) "one-and-a-squared-tangent-make-a-squared-secant", MatchPattern.Commutative(MatchPattern.Exact(Integer.Create(1)), MatchPattern.Node(MatchPattern.Node(MatchPattern.Any("a")), MatchPattern.Exact(Integer.Create(2)))), bound => new Powf(new Secantf(bound["a"]), 2), - Soundness.SoundUnderAssumptions), + Soundness.SoundUnderAssumptions, + description: "1 + tan(a)^2 = sec(a)^2"), new MatchedRule( "one-and-a-squared-cotangent-make-a-squared-cosecant", MatchPattern.Commutative(MatchPattern.Exact(Integer.Create(1)), MatchPattern.Node(MatchPattern.Node(MatchPattern.Any("a")), MatchPattern.Exact(Integer.Create(2)))), bound => new Powf(new Cosecantf(bound["a"]), 2), - Soundness.SoundUnderAssumptions), + Soundness.SoundUnderAssumptions, + description: "1 + cotan(a)^2 = cosec(a)^2"), new MatchedRule( "a-squared-secant-less-a-squared-tangent-is-one", MatchPattern.Node(MatchPattern.Node(MatchPattern.Node(MatchPattern.Any("a")), MatchPattern.Exact(Integer.Create(2))), MatchPattern.Node(MatchPattern.Node(MatchPattern.Any("a")), MatchPattern.Exact(Integer.Create(2)))), bound => Integer.Create(1), - Soundness.SoundUnderAssumptions), + Soundness.SoundUnderAssumptions, + description: "sec(a)^2 - tan(a)^2 = 1"), new MatchedRule( "a-squared-cosecant-less-a-squared-cotangent-is-one", MatchPattern.Node(MatchPattern.Node(MatchPattern.Node(MatchPattern.Any("a")), MatchPattern.Exact(Integer.Create(2))), MatchPattern.Node(MatchPattern.Node(MatchPattern.Any("a")), MatchPattern.Exact(Integer.Create(2)))), bound => Integer.Create(1), - Soundness.SoundUnderAssumptions), + Soundness.SoundUnderAssumptions, + description: "cosec(a)^2 - cotan(a)^2 = 1"), new MatchedRule( "a-squared-sine-less-a-squared-cosine-turns-round", MatchPattern.Node(MatchPattern.Node(MatchPattern.Node(MatchPattern.Any("a")), MatchPattern.Exact(Integer.Create(2))), MatchPattern.Node(MatchPattern.Node(MatchPattern.Any("a")), MatchPattern.Exact(Integer.Create(2)))), bound => -1 * (new Powf(new Cosf(bound["a"]), 2) - new Powf(new Sinf(bound["a"]), 2)), - Soundness.Sound), + Soundness.Sound, + description: "sin(a)^2 - cos(a)^2 = -(cos(a)^2 - sin(a)^2)"), new MatchedRule( "a-squared-cosine-less-a-squared-sine-is-the-doubled-cosine", MatchPattern.Node(MatchPattern.Node(MatchPattern.Node(MatchPattern.Any("a")), MatchPattern.Exact(Integer.Create(2))), MatchPattern.Node(MatchPattern.Node(MatchPattern.Any("a")), MatchPattern.Exact(Integer.Create(2)))), bound => new Cosf(2 * bound["a"]), - Soundness.Sound), + Soundness.Sound, + description: "cos(a)^2 - sin(a)^2 = cos(2a)"), new MatchedRule( "a-quotient-by-a-secant-is-a-cosine", MatchPattern.Node(MatchPattern.Any("a"), MatchPattern.Node(MatchPattern.Any("b"))), bound => bound["a"] * bound["b"].Cos(), - Soundness.SoundUnderAssumptions), + Soundness.SoundUnderAssumptions, + description: "x / sec(a) = x * cos(a)"), new MatchedRule( "a-quotient-by-a-cosecant-is-a-sine", MatchPattern.Node(MatchPattern.Any("a"), MatchPattern.Node(MatchPattern.Any("b"))), bound => bound["a"] * bound["b"].Sin(), - Soundness.SoundUnderAssumptions), + Soundness.SoundUnderAssumptions, + description: "x / cosec(a) = x * sin(a)"), new MatchedRule( "a-secant-times-a-cosine-of-one-angle-is-one", MatchPattern.Commutative(MatchPattern.Node(MatchPattern.Any("a")), MatchPattern.Node(MatchPattern.Any("a"))), bound => Integer.Create(1), - Soundness.SoundUnderAssumptions), + Soundness.SoundUnderAssumptions, + description: "sec(a) * cos(a) = 1"), new MatchedRule( "a-cosecant-times-a-sine-of-one-angle-is-one", MatchPattern.Commutative(MatchPattern.Node(MatchPattern.Any("a")), MatchPattern.Node(MatchPattern.Any("a"))), bound => Integer.Create(1), - Soundness.SoundUnderAssumptions), + Soundness.SoundUnderAssumptions, + description: "cosec(a) * sin(a) = 1"), new MatchedRule( "an-arcsine-of-a-numeric-reciprocal-is-an-arccosecant", MatchPattern.Node(MatchPattern.Node(MatchPattern.Any("n"), MatchPattern.Any("d"))), bound => new Arccosecantf(bound["d"] / bound["n"]), Soundness.SoundUnderAssumptions, - when: bound => bound["n"] is Number && bound["d"] is not Number), + when: bound => bound["n"] is Number && bound["d"] is not Number, + description: "arcsin(1 / c) = arccosec(c), for a numeric c"), new MatchedRule( "an-arccosine-of-a-numeric-reciprocal-is-an-arcsecant", MatchPattern.Node(MatchPattern.Node(MatchPattern.Any("n"), MatchPattern.Any("d"))), bound => new Arcsecantf(bound["d"] / bound["n"]), Soundness.SoundUnderAssumptions, - when: bound => bound["n"] is Number && bound["d"] is not Number), + when: bound => bound["n"] is Number && bound["d"] is not Number, + description: "arccos(1 / c) = arcsec(c), for a numeric c"), new MatchedRule( "an-arccosecant-of-a-numeric-reciprocal-is-an-arcsine", MatchPattern.Node(MatchPattern.Node(MatchPattern.Any("n"), MatchPattern.Any("d"))), bound => new Arcsinf(bound["d"] / bound["n"]), Soundness.SoundUnderAssumptions, - when: bound => bound["n"] is Number && bound["d"] is not Number), + when: bound => bound["n"] is Number && bound["d"] is not Number, + description: "arccosec(1 / c) = arcsin(c), for a numeric c"), new MatchedRule( "an-arcsecant-of-a-numeric-reciprocal-is-an-arccosine", MatchPattern.Node(MatchPattern.Node(MatchPattern.Any("n"), MatchPattern.Any("d"))), bound => new Arccosf(bound["d"] / bound["n"]), Soundness.SoundUnderAssumptions, - when: bound => bound["n"] is Number && bound["d"] is not Number)); + when: bound => bound["n"] is Number && bound["d"] is not Number, + description: "arcsec(1 / c) = arccos(c), for a numeric c")); /// /// , as data. diff --git a/Sources/AngouriMath/Core/Transformations/RewriteRules.cs b/Sources/AngouriMath/Core/Transformations/RewriteRules.cs index 7b99f87ba..625a9718c 100644 --- a/Sources/AngouriMath/Core/Transformations/RewriteRules.cs +++ b/Sources/AngouriMath/Core/Transformations/RewriteRules.cs @@ -373,8 +373,7 @@ public static class RewriteRules // tan and cot bring poles with them, so an identity that introduces one holds // away from those points rather than everywhere. Soundness.SoundUnderAssumptions, - Matching.MatchedRules.Trigonometric.ApplyHere, - Patterns.TrigonometricRulesArms); + Matching.MatchedRules.Trigonometric); /// /// Rewrites the derived trigonometric functions in terms of sine and cosine. diff --git a/Sources/AngouriMath/Docs/Contributing/WritingARule.md b/Sources/AngouriMath/Docs/Contributing/WritingARule.md index 33cd789cf..64860eaae 100644 --- a/Sources/AngouriMath/Docs/Contributing/WritingARule.md +++ b/Sources/AngouriMath/Docs/Contributing/WritingARule.md @@ -21,7 +21,7 @@ effect". This is how to supply those seven things. **322** rules live there today. The `switch` statements in `Functions/Simplification/Patterns` are the older form. Twenty-seven of -the thirty registered sets no longer run theirs, and twenty-three no longer describe it either; the +the thirty registered sets no longer run theirs, and twenty-four no longer describe it either; the remainder are being moved set by set ([#825](https://github.com/asc-community/AngouriMath/issues/825)). **Do not add a rule to a `switch`.** A `switch` arm cannot carry a name, a tier, an identity or a direction, and everything below is about those. @@ -88,7 +88,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. **101** rules carry one today; a new rule should. +rule happens to be applied in. **134** rules carry one today; a new rule should. ## The pattern language diff --git a/Sources/Tests/UnitTests/Core/Transformations/AddressableRulesTest.cs b/Sources/Tests/UnitTests/Core/Transformations/AddressableRulesTest.cs index edb20dd9e..558ce2918 100644 --- a/Sources/Tests/UnitTests/Core/Transformations/AddressableRulesTest.cs +++ b/Sources/Tests/UnitTests/Core/Transformations/AddressableRulesTest.cs @@ -290,16 +290,16 @@ public void TheRegistryIsAddressableAsFarAsItSaysItIs() Assert.Equal(30, withRules.Count); - // 365, and it was 407 before the registry started reporting the rules it runs rather - // than the `switch` it no longer runs. The forty-two that went are not rules lost, they + // 355, and it was 407 before the registry started reporting the rules it runs rather + // than the `switch` it no longer runs. The fifty-two that went are not rules lost, they // are arms the data form writes once. Boolean's thirty-six are twenty, because a // commutative pattern finds a shared operand wherever it sits, so eight arms of // distributivity are two rules and absorption's four-arms-each is one rule twice; - // Factorization's twenty-two are eleven for the same reason; 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(365, withRules.Sum(set => set.Rules.Count)); + // Factorization's twenty-two are eleven for the same reason; Trigonometric's forty-three + // 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(355, withRules.Sum(set => set.Rules.Count)); } /// diff --git a/Sources/Tests/UnitTests/Core/Transformations/DerivationPathTest.cs b/Sources/Tests/UnitTests/Core/Transformations/DerivationPathTest.cs index 0e9b23d0e..00b6a10e9 100644 --- a/Sources/Tests/UnitTests/Core/Transformations/DerivationPathTest.cs +++ b/Sources/Tests/UnitTests/Core/Transformations/DerivationPathTest.cs @@ -149,7 +149,16 @@ public void EveryStepNamesWhatDidIt(string raw) Assert.Contains(step.RuleSet, RewriteRules.All); Assert.Equal(step.RuleSet.Name, step.Name); Assert.Equal(step.RuleSet.Relation, step.Relation); - Assert.Equal(step.RuleSet.Soundness, step.Soundness); + // The step's tier is the weakest of the rewrites that actually fired, and the + // set's only where none was recorded. Not the set's outright: a set's tier is the + // minimum over all its rules, so a pass of rules that all hold universally would + // otherwise inherit a caveat from rules it never reached. On + // `sin(x)^2 + cos(x)^2` the step is Sound where Trigonometric is + // SoundUnderAssumptions, which is the whole point of the finer grain. + var weakest = step.Rewrites.Count == 0 + ? step.RuleSet.Soundness + : step.Rewrites.Max(rewrite => rewrite.Soundness); + Assert.Equal(weakest, step.Soundness); } } diff --git a/Sources/Tests/UnitTests/Core/Transformations/RewriteRecordingTest.cs b/Sources/Tests/UnitTests/Core/Transformations/RewriteRecordingTest.cs index 1f8f1d154..f22cff265 100644 --- a/Sources/Tests/UnitTests/Core/Transformations/RewriteRecordingTest.cs +++ b/Sources/Tests/UnitTests/Core/Transformations/RewriteRecordingTest.cs @@ -94,16 +94,27 @@ public void TheDerivationHasNoNormalisation() /// /// A rewrite that takes one step is reported as one step, with the rule that did it. /// + /// + /// The two cases show what repointing a set at its data form buys. Common is + /// still described by RuleRegistryGenerator, so its rule is named by the arm's own + /// rendered pattern; Trigonometric is described from the rules it runs, so its rule + /// is named in words and carries the identity. This asserted the replacement's C# source + /// text until the second of those was repointed, at which point it became + /// (built by code) — the name is the better thing to hold anyway, being what a + /// derivation actually reports. + /// [Theory] - [InlineData("x + x", "2 * any1")] - [InlineData("sin(x)^2 + cos(x)^2", "1")] - public void AOneStepRewriteIsOneStep(string expr, string replacement) + [InlineData("x + x", "Sumf(var any1, var any1a) when any1 == any1a", null)] + [InlineData("sin(x)^2 + cos(x)^2", "a-squared-sine-and-cosine-of-one-angle-sum-to-one", + "sin(a)^2 + cos(a)^2 = 1")] + public void AOneStepRewriteIsOneStep(string expr, string ruleName, string? identity) { using var recording = RewriteRecording.Start(); Parse(expr).Simplify(); var step = Assert.Single(recording.Derivation); - Assert.Equal(replacement, step.Rule?.ReplacementSource); + Assert.Equal(ruleName, step.Rule?.Name); + Assert.Equal(identity, step.Rule?.Description); } [Fact] diff --git a/Sources/Tests/UnitTests/Core/Transformations/RuleAuthoringGuideTest.cs b/Sources/Tests/UnitTests/Core/Transformations/RuleAuthoringGuideTest.cs index b9d082866..7cef5c727 100644 --- a/Sources/Tests/UnitTests/Core/Transformations/RuleAuthoringGuideTest.cs +++ b/Sources/Tests/UnitTests/Core/Transformations/RuleAuthoringGuideTest.cs @@ -49,7 +49,7 @@ public void WhereARuleGoes() MatchedRules.All.Any(data => data.Name == set.Name) || set.Name.StartsWith("CommonDenominator", StringComparison.Ordinal)), "how many registered sets run the matcher"); - Stated(23, RewriteRules.All.Count(set => + Stated(24, RewriteRules.All.Count(set => set.Rules.Count > 0 && set.Rules.All(rule => rule.Soundness is not null)), "how many registered sets describe what they run"); } @@ -68,7 +68,7 @@ public void TheNameIsASentence() [Fact] public void TheIdentityIsNotTheName() - => Stated(101, + => Stated(134, MatchedRules.All.SelectMany(set => set.Rules).Count(rule => rule.Description is not null), "how many rules carry an identity"); diff --git a/Sources/Tests/UnitTests/Core/Transformations/RuleMetadataTest.cs b/Sources/Tests/UnitTests/Core/Transformations/RuleMetadataTest.cs index 8ddf88c83..9e2ba5e55 100644 --- a/Sources/Tests/UnitTests/Core/Transformations/RuleMetadataTest.cs +++ b/Sources/Tests/UnitTests/Core/Transformations/RuleMetadataTest.cs @@ -134,6 +134,7 @@ public void ARuleWrittenAsDataCarriesItsTierIntoTheRegistry() nameof(RewriteRules.PolynomialLongDivision), nameof(RewriteRules.RationalizeDenominator), nameof(RewriteRules.SetOperator), + nameof(RewriteRules.Trigonometric), }; var described = RewriteRules.All @@ -164,7 +165,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(101, repointed.Sum(set => set.Rules.Count)); + Assert.Equal(134, 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 94c0f6762..63fe0df13 100644 --- a/Sources/Tests/UnitTests/Core/Transformations/StepAsASentenceTest.cs +++ b/Sources/Tests/UnitTests/Core/Transformations/StepAsASentenceTest.cs @@ -63,9 +63,17 @@ public void EveryRuleWrittenAsDataIsNamedInProse() Assert.All(names, name => Assert.True(Explanation.IsProse(name), $"'{name}' is a rule name that does not read as a phrase in English")); - // And a generated one is not mistaken for prose. Trigonometric still reads its arms - // from the `switch`, so its names are rendered patterns. - var generated = RewriteRules.Trigonometric.Rules.Select(rule => rule.Name); + // And a generated one is not mistaken for prose. Which sets those are moves as the + // registry is repointed set by set, so they are found rather than named: a set whose + // rules carry no tier of their own is one still described by `RuleRegistryGenerator`, + // and every name it gives is a rendered pattern. Naming one instead cost a failure the + // day Trigonometric was repointed, which is the argument for asking. + var generated = RewriteRules.All + .Where(set => set.Rules.Count > 0 && set.Rules.All(rule => rule.Soundness is null)) + .SelectMany(set => set.Rules) + .Select(rule => rule.Name) + .ToList(); + Assert.NotEmpty(generated); Assert.All(generated, name => Assert.False(Explanation.IsProse(name), $"'{name}' is a rendered pattern and was taken for prose")); }