diff --git a/Sources/AngouriMath/Core/Transformations/RewriteRuleSetExtensions.cs b/Sources/AngouriMath/Core/Transformations/RewriteRuleSetExtensions.cs new file mode 100644 index 000000000..fa9d9706a --- /dev/null +++ b/Sources/AngouriMath/Core/Transformations/RewriteRuleSetExtensions.cs @@ -0,0 +1,24 @@ +// +// 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. +// + +namespace AngouriMath.Core.Transformations +{ + /// + /// Reading order for . + /// + /// + /// The simplifier applies a dozen rule sets in sequence, and written as calls the + /// sequence reads inside out. This is the same operation with the expression in front, + /// so that a pipeline still reads in the order it runs. + /// + internal static class RewriteRuleSetExtensions + { + /// Applies once over every node of . + internal static Entity Rewrite(this Entity expression, RewriteRuleSet ruleSet) + => ruleSet.ApplyOnce(expression); + } +} diff --git a/Sources/AngouriMath/Core/Transformations/RewriteRules.cs b/Sources/AngouriMath/Core/Transformations/RewriteRules.cs index 592b11cfa..b8d7e36b3 100644 --- a/Sources/AngouriMath/Core/Transformations/RewriteRules.cs +++ b/Sources/AngouriMath/Core/Transformations/RewriteRules.cs @@ -21,28 +21,59 @@ namespace AngouriMath.Core.Transformations /// which type happened to be loaded first. /// /// - /// This is a slice, not the whole pattern table. The sets below are the ones the - /// transformations in are built from; the rest of - /// Functions/Simplification/Patterns is still reached only from - /// Simplificator. Adding one here is five lines and gets it enumeration, a - /// soundness label and the tests over for free. + /// Every rule set the simplifier applies is registered here, and the simplifier reaches + /// them through this registry rather than through Patterns directly. That is what + /// lets an account of what the simplifier did to an expression be a complete one: a set + /// reachable only by its method has no name to report and nothing to attribute a step to, + /// so a derivation built while some sets were still unregistered would quietly omit + /// whatever they had done. + /// + /// + /// Every entry is declared . That is a claim + /// about what has been argued, not about what is true — nothing here checks a tier, so + /// the registry starts conservative and promoting an entry means making the case for it. /// /// public static class RewriteRules { + #region Arrangement + /// /// Puts the operands of commutative chains into a canonical order and groups equal /// ones together, so that x + y and y + x stop being different trees. + /// Looks at variables and functions only, ignoring constants and operators. /// public static RewriteRuleSet CanonicalOrder { get; } = new( nameof(CanonicalOrder), - "Sorts and groups the operands of sums, products, conjunctions, disjunctions and set operations.", + "Sorts and groups the operands of sums, products, conjunctions, disjunctions and set operations, by variables and functions alone.", TransformationRelation.Equivalence, // Regrouping reads a quotient as a product with a negative power, which is the // same value wherever the divisor is not zero. Soundness.SoundUnderAssumptions, Patterns.SortRules(TreeAnalyzer.SortLevel.HIGH_LEVEL)); + /// + /// , counting constants as well, so that terms differing + /// only by a numeric factor are no longer grouped together. + /// + public static RewriteRuleSet CanonicalOrderCountingConstants { get; } = new( + nameof(CanonicalOrderCountingConstants), + "Sorts and groups commutative operands, distinguishing terms by their constants too.", + TransformationRelation.Equivalence, + Soundness.SoundUnderAssumptions, + Patterns.SortRules(TreeAnalyzer.SortLevel.MIDDLE_LEVEL)); + + /// + /// over the whole subtree, so that only structurally + /// identical operands are grouped. + /// + public static RewriteRuleSet CanonicalOrderExact { get; } = new( + nameof(CanonicalOrderExact), + "Sorts and groups commutative operands by the whole subtree.", + TransformationRelation.Equivalence, + Soundness.SoundUnderAssumptions, + Patterns.SortRules(TreeAnalyzer.SortLevel.LOW_LEVEL)); + /// /// Turns a negative power into a quotient: a * b ^ (-1) becomes a / b. /// @@ -74,6 +105,30 @@ public static class RewriteRules Soundness.SoundUnderAssumptions, Patterns.CommonRules); + /// + /// Gets a quotient into the shape the division rules expect before they run. + /// + public static RewriteRuleSet DivisionPreparing { get; } = new( + nameof(DivisionPreparing), + "Lifts numeric factors out of a quotient so that the division rules can see it.", + TransformationRelation.Equivalence, + Soundness.SoundUnderAssumptions, + Patterns.DivisionPreparingRules); + + /// + /// Cosmetic arrangement of signs, so that adding a negative reads as a difference. + /// + public static RewriteRuleSet NumericNeat { get; } = new( + nameof(NumericNeat), + "Arranges signs so that adding a negative is written as subtracting a positive.", + TransformationRelation.Equivalence, + Soundness.SoundUnderAssumptions, + Patterns.NumericNeatRules); + + #endregion + + #region Powers, products and sums + /// /// Rules about powers, roots and logarithms. /// @@ -86,18 +141,6 @@ public static class RewriteRules Soundness.SoundUnderAssumptions, Patterns.PowerRules); - /// - /// The trigonometric identities. - /// - public static RewriteRuleSet Trigonometric { get; } = new( - nameof(Trigonometric), - "Applies trigonometric identities to sines, cosines and their relatives.", - TransformationRelation.Equivalence, - // tan and cot bring poles with them, so an identity that introduces one holds - // away from those points rather than everywhere. - Soundness.SoundUnderAssumptions, - Patterns.TrigonometricRules); - /// /// Multiplies products over sums out. /// @@ -129,6 +172,10 @@ public static class RewriteRules Soundness.SoundUnderAssumptions, Patterns.PerfectSquareRules); + #endregion + + #region Quotients + /// /// Clears a surd out of a two-term denominator. /// @@ -139,6 +186,193 @@ public static class RewriteRules Soundness.SoundUnderAssumptions, Patterns.RationaliseDenominator); + /// + /// Brings a quotient of quotients down to a single one. + /// + public static RewriteRuleSet CollapseMultipleFractions { get; } = new( + nameof(CollapseMultipleFractions), + "Collapses nested quotients into a single numerator over a single denominator.", + TransformationRelation.Equivalence, + Soundness.SoundUnderAssumptions, + Patterns.CollapseMultipleFractions); + + /// + /// Puts a sum of quotients over one denominator, grouping the terms by variables and + /// functions alone. + /// + public static RewriteRuleSet CommonDenominator { get; } = new( + nameof(CommonDenominator), + "Adds quotients by putting them over a common denominator.", + TransformationRelation.Equivalence, + Soundness.SoundUnderAssumptions, + expr => Patterns.FractionCommonDenominatorRules(expr, TreeAnalyzer.SortLevel.HIGH_LEVEL)); + + /// + /// , counting constants when it groups terms. + /// + public static RewriteRuleSet CommonDenominatorCountingConstants { get; } = new( + nameof(CommonDenominatorCountingConstants), + "Adds quotients over a common denominator, distinguishing terms by their constants too.", + TransformationRelation.Equivalence, + Soundness.SoundUnderAssumptions, + expr => Patterns.FractionCommonDenominatorRules(expr, TreeAnalyzer.SortLevel.MIDDLE_LEVEL)); + + /// + /// , grouping terms by the whole subtree. + /// + public static RewriteRuleSet CommonDenominatorExact { get; } = new( + nameof(CommonDenominatorExact), + "Adds quotients over a common denominator, grouping terms by the whole subtree.", + TransformationRelation.Equivalence, + Soundness.SoundUnderAssumptions, + expr => Patterns.FractionCommonDenominatorRules(expr, TreeAnalyzer.SortLevel.LOW_LEVEL)); + + /// + /// Divides one polynomial by another, leaving a quotient plus a remainder. + /// + public static RewriteRuleSet PolynomialLongDivision { get; } = new( + nameof(PolynomialLongDivision), + "Divides a polynomial by a polynomial, giving the quotient plus the remainder.", + TransformationRelation.Equivalence, + Soundness.SoundUnderAssumptions, + Patterns.PolynomialLongDivision); + + /// + /// Puts a quotient of polynomials into lowest terms. + /// + public static RewriteRuleSet PolynomialGcdCancellation { get; } = new( + nameof(PolynomialGcdCancellation), + "Cancels the greatest common divisor of a polynomial quotient's numerator and denominator.", + TransformationRelation.Equivalence, + Soundness.SoundUnderAssumptions, + Patterns.PolynomialGcdCancellation); + + #endregion + + #region Trigonometry + + /// + /// The trigonometric identities. + /// + public static RewriteRuleSet Trigonometric { get; } = new( + nameof(Trigonometric), + "Applies trigonometric identities to sines, cosines and their relatives.", + TransformationRelation.Equivalence, + // tan and cot bring poles with them, so an identity that introduces one holds + // away from those points rather than everywhere. + Soundness.SoundUnderAssumptions, + Patterns.TrigonometricRules); + + /// + /// Rewrites the derived trigonometric functions in terms of sine and cosine. + /// + public static RewriteRuleSet NormalTrigonometricForm { get; } = new( + nameof(NormalTrigonometricForm), + "Writes tangents, cotangents, secants and cosecants as sines and cosines.", + TransformationRelation.Equivalence, + Soundness.SoundUnderAssumptions, + Patterns.NormalTrigonometricForm); + + /// + /// Gathers sines and cosines back into the derived functions where that is shorter. + /// + public static RewriteRuleSet CollapseTrigonometricFunctions { get; } = new( + nameof(CollapseTrigonometricFunctions), + "Recognises a quotient or reciprocal of sines and cosines as a tangent, cotangent, secant or cosecant.", + TransformationRelation.Equivalence, + Soundness.SoundUnderAssumptions, + Patterns.CollapseTrigonometricFunctions); + + /// + /// Opens a trigonometric function of a sum into functions of its terms. + /// + public static RewriteRuleSet ExpandTrigonometric { get; } = new( + nameof(ExpandTrigonometric), + "Expands a sine or cosine of a sum into products of sines and cosines of its terms.", + TransformationRelation.Equivalence, + Soundness.SoundUnderAssumptions, + Patterns.ExpandTrigonometricRules); + + /// + /// Opens a trigonometric function of a multiplied angle. + /// + /// + /// Written out, sin(4x) is far longer than it started, which is why the + /// simplifier offers the result as a candidate rather than taking it. + /// + public static RewriteRuleSet ExpandMultipleAngle { get; } = new( + nameof(ExpandMultipleAngle), + "Expands a sine or cosine of an integer multiple of an angle.", + TransformationRelation.Equivalence, + Soundness.SoundUnderAssumptions, + Patterns.ExpandMultipleAngleRules); + + #endregion + + #region Statements, sets and number theory + + /// + /// The rules of boolean algebra. + /// + public static RewriteRuleSet Boolean { get; } = new( + nameof(Boolean), + "Applies the identities of boolean algebra to conjunctions, disjunctions and negations.", + TransformationRelation.Equivalence, + Soundness.SoundUnderAssumptions, + Patterns.BooleanRules); + + /// + /// Rules about equalities and inequalities. + /// + public static RewriteRuleSet InequalityEquality { get; } = new( + nameof(InequalityEquality), + "Rearranges equalities and inequalities into their usual form.", + TransformationRelation.Equivalence, + Soundness.SoundUnderAssumptions, + Patterns.InequalityEqualityRules); + + /// + /// Rules about unions, intersections and set differences. + /// + public static RewriteRuleSet SetOperator { get; } = new( + nameof(SetOperator), + "Applies the identities of set algebra to unions, intersections and set differences.", + TransformationRelation.Equivalence, + Soundness.SoundUnderAssumptions, + Patterns.SetOperatorRules); + + /// + /// Cancels a quotient of factorials down to the terms that survive. + /// + public static RewriteRuleSet ExpandFactorialDivisions { get; } = new( + nameof(ExpandFactorialDivisions), + "Cancels a quotient of factorials into the product of the terms that do not cancel.", + TransformationRelation.Equivalence, + Soundness.SoundUnderAssumptions, + Patterns.ExpandFactorialDivisions); + + /// + /// Recognises a product of consecutive terms as a factorial. + /// + public static RewriteRuleSet FactorizeFactorialMultiplications { get; } = new( + nameof(FactorizeFactorialMultiplications), + "Gathers a product of a factorial and its neighbouring terms back into one factorial.", + TransformationRelation.Equivalence, + Soundness.SoundUnderAssumptions, + Patterns.FactorizeFactorialMultiplications); + + /// + /// Rules about Euler's totient function. + /// + public static RewriteRuleSet PhiFunction { get; } = new( + nameof(PhiFunction), + "Applies the multiplicative identities of Euler's totient function.", + TransformationRelation.Equivalence, + Soundness.SoundUnderAssumptions, + Patterns.PhiFunctionRules); + + #endregion + /// /// Every rule set registered above, in a fixed order. Enumerable so that a property /// that should hold of all of them can be tested over all of them rather than over @@ -147,15 +381,58 @@ public static class RewriteRules public static IReadOnlyList All { get; } = new[] { CanonicalOrder, + CanonicalOrderCountingConstants, + CanonicalOrderExact, InvertNegativePowers, InvertNegativeMultipliers, Common, + DivisionPreparing, + NumericNeat, Power, - Trigonometric, Expansion, Factorization, PerfectSquare, RationaliseDenominator, + CollapseMultipleFractions, + CommonDenominator, + CommonDenominatorCountingConstants, + CommonDenominatorExact, + PolynomialLongDivision, + PolynomialGcdCancellation, + Trigonometric, + NormalTrigonometricForm, + CollapseTrigonometricFunctions, + ExpandTrigonometric, + ExpandMultipleAngle, + Boolean, + InequalityEquality, + SetOperator, + ExpandFactorialDivisions, + FactorizeFactorialMultiplications, + PhiFunction, }; + + /// + /// The family, chosen by how finely it distinguishes + /// operands. The simplifier picks the level from which pass it is on. + /// + internal static RewriteRuleSet CanonicalOrderAt(TreeAnalyzer.SortLevel level) + => level switch + { + TreeAnalyzer.SortLevel.MIDDLE_LEVEL => CanonicalOrderCountingConstants, + TreeAnalyzer.SortLevel.LOW_LEVEL => CanonicalOrderExact, + _ => CanonicalOrder + }; + + /// + /// The family, chosen the same way. + /// + internal static RewriteRuleSet CommonDenominatorAt(TreeAnalyzer.SortLevel level) + => level switch + { + TreeAnalyzer.SortLevel.MIDDLE_LEVEL => CommonDenominatorCountingConstants, + TreeAnalyzer.SortLevel.LOW_LEVEL => CommonDenominatorExact, + _ => CommonDenominator + }; } } diff --git a/Sources/AngouriMath/Docs/Contributing/Transformations.md b/Sources/AngouriMath/Docs/Contributing/Transformations.md index e9fec5a64..fba4f399e 100644 --- a/Sources/AngouriMath/Docs/Contributing/Transformations.md +++ b/Sources/AngouriMath/Docs/Contributing/Transformations.md @@ -36,7 +36,7 @@ same claim in the shape its callers expect, by handing back an unevaluated `Inte | `Transformation` | the operation: `Name`, `Relation`, `Soundness`, `Apply`, and the static catalogue | | `TransformationResult` | input, output-or-nothing, and which transformation ran. A struct, so routing an ordinary call through this layer allocates nothing | | `RewriteRuleSet` | a named, attributed group of rewrites — `Name`, `Description`, `Relation`, `Soundness`, `ApplyOnce` | -| `RewriteRules` | the registry: every shipped set, explicitly listed, enumerable through `All` in a fixed order | +| `RewriteRules` | the registry: every rule set the simplifier applies, explicitly listed, enumerable through `All` in a fixed order | Composition is `Then`, `Repeat(n)` and `UntilStable(max)`. All three take their bound from the caller: there is no unbounded rewrite loop anywhere in the layer, and `UntilStable` reports hitting @@ -83,8 +83,12 @@ API does not invent symmetry that the mathematics does not have. **`Heuristic` currently has no instance in the catalogue.** That is a statement about what is registered so far, not a claim that nothing in the library guesses. -**Most of the pattern table is still only reachable from `Simplificator`.** `RewriteRules` registers -the ten sets the catalogue is built from. The rest are unchanged and unregistered. +**Some rewrites still bypass the registry.** Every set the *simplifier* applies is registered, and +`Simplificator` reaches all of them through `RewriteRules` — that is what lets an account of what +`Simplify` did be a complete one, since a set reachable only by its method has no name to attribute a +step to. The equation and set solvers, the integrator and `TreeAnalyzer` still call `Patterns` +directly. `Patterns.TrigonometricToExponentialRules(from, to)` cannot become a registry entry as it +stands: it is parameterised by two variables, so it is a family of sets rather than one. ## Adding the next one @@ -109,7 +113,13 @@ public static RewriteRuleSet Power { get; } = new( ``` Add it to `RewriteRules.All` in the same change — the list is explicit so that its order is a -decision rather than an accident. +decision rather than an accident. Where a set is parameterised by something with a small fixed range, +register one entry per value and add an `internal` chooser beside them (`CanonicalOrderAt`, +`CommonDenominatorAt` are the two): the alternative is an entry that cannot be enumerated, which +defeats the point of the list. + +Inside the library, apply a set with `expression.Rewrite(RewriteRules.Power)` rather than +`RewriteRules.Power.ApplyOnce(expression)` — a sequence of rule sets otherwise reads inside out. Two things to get right: diff --git a/Sources/AngouriMath/Functions/Evaluation/Evaluation.Definition.cs b/Sources/AngouriMath/Functions/Evaluation/Evaluation.Definition.cs index 8c8786c0d..055d6fda2 100644 --- a/Sources/AngouriMath/Functions/Evaluation/Evaluation.Definition.cs +++ b/Sources/AngouriMath/Functions/Evaluation/Evaluation.Definition.cs @@ -216,8 +216,8 @@ internal Entity ExpandOverSum(int level) { static Entity Expand_(Entity e, int level) => level <= 1 - ? e.Replace(Patterns.ExpandRules) - : Expand_(e.Replace(Patterns.ExpandRules), level - 1); + ? e.Rewrite(RewriteRules.Expansion) + : Expand_(e.Rewrite(RewriteRules.Expansion), level - 1); var expChildren = new List(); foreach (var linChild in Sumf.LinearChildren(this)) if (TreeAnalyzer.SmartExpandOver(linChild, entity => true) is { } exp) @@ -265,7 +265,7 @@ private static Entity CollectLikeTerms(Entity expanded) { // PowerRules folds (x^2)^2 into x^4, without which the two would be // counted as different monomials. - var reduced = factor.Replace(Functions.Patterns.PowerRules).InnerSimplified; + var reduced = factor.Rewrite(RewriteRules.Power).InnerSimplified; if (reduced is Number) { coefficient = (coefficient * reduced).InnerSimplified; diff --git a/Sources/AngouriMath/Functions/Simplification/Simplificator.cs b/Sources/AngouriMath/Functions/Simplification/Simplificator.cs index 695e22cb0..4a4a63fa6 100644 --- a/Sources/AngouriMath/Functions/Simplification/Simplificator.cs +++ b/Sources/AngouriMath/Functions/Simplification/Simplificator.cs @@ -72,7 +72,7 @@ void AddHistory(Entity expr) #endif void __IterAddHistory(Entity expr) { - var refexpr = expr.Replace(Patterns.SortRules(TreeAnalyzer.SortLevel.HIGH_LEVEL)).InnerSimplified; + var refexpr = expr.Rewrite(RewriteRules.CanonicalOrder).InnerSimplified; var compl1 = refexpr.SimplifiedRate; var compl2 = expr.SimplifiedRate; var n = compl1 > compl2 ? expr : refexpr; @@ -83,7 +83,7 @@ void __IterAddHistory(Entity expr) history[ncompl] = new HashSet { n }; } __IterAddHistory(expr); - __IterAddHistory(expr.Replace(Patterns.InvertNegativePowers)); + __IterAddHistory(expr.Rewrite(RewriteRules.InvertNegativePowers)); MultithreadingFunctional.ExitIfCancelled(); } @@ -121,15 +121,15 @@ void __IterAddHistory(Entity expr) // is an accident rather than a preference. // https://github.com/asc-community/AngouriMath/issues/205 if (res.Nodes.Any(child => child is Divf)) - AddHistory(res = res.Replace(Patterns.RationaliseDenominator).InnerSimplified); + AddHistory(res = res.Rewrite(RewriteRules.RationaliseDenominator).InnerSimplified); - res = res.Replace(Patterns.SortRules(sortLevel)).InnerSimplified; + res = res.Rewrite(RewriteRules.CanonicalOrderAt(sortLevel)).InnerSimplified; if (res.Nodes.Any(child => child is Powf)) - AddHistory(res = res.Replace(Patterns.PowerRules).InnerSimplified); + AddHistory(res = res.Rewrite(RewriteRules.Power).InnerSimplified); AddHistory(res = SimplifyChildren(res)); - AddHistory(res = res.Replace(Patterns.InvertNegativePowers).Replace(Patterns.DivisionPreparingRules).InnerSimplified); + AddHistory(res = res.Rewrite(RewriteRules.InvertNegativePowers).Rewrite(RewriteRules.DivisionPreparing).InnerSimplified); // Lowest terms, offered as one more candidate rather than taken. Cancelling // means multiplying out, and a quotient that cancels down to a polynomial @@ -138,41 +138,41 @@ void __IterAddHistory(Entity expr) // stands, and u^2 + 4u + 4 only once this has expanded it. The complexity // metric is what should decide between them. if (res.Nodes.Any(child => child is Divf)) - AddHistory(res.Replace(Patterns.PolynomialGcdCancellation).InnerSimplified); + AddHistory(res.Rewrite(RewriteRules.PolynomialGcdCancellation).InnerSimplified); - AddHistory(res = res.Replace(Patterns.PolynomialLongDivision).InnerSimplified); + AddHistory(res = res.Rewrite(RewriteRules.PolynomialLongDivision).InnerSimplified); - AddHistory(res = res.Replace(Patterns.NormalTrigonometricForm).InnerSimplified); - AddHistory(res = res.Replace(Patterns.CollapseMultipleFractions).InnerSimplified); - AddHistory(res = res.Replace(e => Patterns.FractionCommonDenominatorRules(e, sortLevel)).InnerSimplified); - AddHistory(res = res.Replace(Patterns.InvertNegativePowers).Replace(Patterns.DivisionPreparingRules).InnerSimplified); - AddHistory(res = res.Replace(Patterns.PowerRules).InnerSimplified); - AddHistory(res = res.Replace(Patterns.TrigonometricRules).InnerSimplified); - AddHistory(res = res.Replace(Patterns.CollapseTrigonometricFunctions).InnerSimplified); + AddHistory(res = res.Rewrite(RewriteRules.NormalTrigonometricForm).InnerSimplified); + AddHistory(res = res.Rewrite(RewriteRules.CollapseMultipleFractions).InnerSimplified); + AddHistory(res = res.Rewrite(RewriteRules.CommonDenominatorAt(sortLevel)).InnerSimplified); + AddHistory(res = res.Rewrite(RewriteRules.InvertNegativePowers).Rewrite(RewriteRules.DivisionPreparing).InnerSimplified); + AddHistory(res = res.Rewrite(RewriteRules.Power).InnerSimplified); + AddHistory(res = res.Rewrite(RewriteRules.Trigonometric).InnerSimplified); + AddHistory(res = res.Rewrite(RewriteRules.CollapseTrigonometricFunctions).InnerSimplified); if (res.Nodes.Any(child => child is TrigonometricFunction)) { - var res1 = res.Replace(Patterns.ExpandTrigonometricRules).InnerSimplified; - AddHistory(res = res.Replace(Patterns.TrigonometricRules).Replace(Patterns.CommonRules).InnerSimplified); + var res1 = res.Rewrite(RewriteRules.ExpandTrigonometric).InnerSimplified; + AddHistory(res = res.Rewrite(RewriteRules.Trigonometric).Rewrite(RewriteRules.Common).InnerSimplified); AddHistory(res1); res = PickSimplest(res, res1); - AddHistory(res = res.Replace(Patterns.CollapseTrigonometricFunctions).Replace(Patterns.TrigonometricRules)); + AddHistory(res = res.Rewrite(RewriteRules.CollapseTrigonometricFunctions).Rewrite(RewriteRules.Trigonometric)); // Multiple angles opened up, then gathered again by the ordinary rules. // Offered as a candidate rather than taken: written out, sin(4x) is far // longer than it started, and only worth it where the pieces cancel -- // which is what the complexity metric is for. var expandedAngles = res - .Replace(Patterns.ExpandMultipleAngleRules) - .Replace(Patterns.NormalTrigonometricForm) + .Rewrite(RewriteRules.ExpandMultipleAngle) + .Rewrite(RewriteRules.NormalTrigonometricForm) .InnerSimplified; if (expandedAngles != res) { AddHistory(expandedAngles); AddHistory(SimplifyChildren(expandedAngles) - .Replace(Patterns.TrigonometricRules) - .Replace(Patterns.CommonRules) + .Rewrite(RewriteRules.Trigonometric) + .Rewrite(RewriteRules.Common) .InnerSimplified); } } @@ -180,35 +180,35 @@ void __IterAddHistory(Entity expr) if (res.Nodes.Any(child => child is Statement)) { - AddHistory(res = res.Replace(Patterns.BooleanRules).InnerSimplified); + AddHistory(res = res.Rewrite(RewriteRules.Boolean).InnerSimplified); } if (res.Nodes.Any(child => child is ComparisonSign)) { - AddHistory(res = res.Replace(Patterns.InequalityEqualityRules).InnerSimplified); + AddHistory(res = res.Rewrite(RewriteRules.InequalityEquality).InnerSimplified); } if (res.Nodes.Any(child => child is Factorialf)) { - AddHistory(res = res.Replace(Patterns.ExpandFactorialDivisions).InnerSimplified); - AddHistory(res = res.Replace(Patterns.FactorizeFactorialMultiplications).InnerSimplified); + AddHistory(res = res.Rewrite(RewriteRules.ExpandFactorialDivisions).InnerSimplified); + AddHistory(res = res.Rewrite(RewriteRules.FactorizeFactorialMultiplications).InnerSimplified); } if (res.Nodes.Any(child => child is Powf or Logf)) - AddHistory(res = res.Replace(Patterns.PowerRules).InnerSimplified); + AddHistory(res = res.Rewrite(RewriteRules.Power).InnerSimplified); if (res.Nodes.Any(child => child is Set)) { - var replaced = res.Replace(Patterns.SetOperatorRules); + var replaced = res.Rewrite(RewriteRules.SetOperator); AddHistory(res = replaced.InnerSimplified); } if (res.Nodes.Any(child => child is Phif)) - AddHistory(res = res.Replace(Patterns.PhiFunctionRules).InnerSimplified); + AddHistory(res = res.Rewrite(RewriteRules.PhiFunction).InnerSimplified); Entity? possiblePoly = null; foreach (var var in res.Vars) @@ -227,10 +227,10 @@ void __IterAddHistory(Entity expr) AddHistory(factoredPoly); - AddHistory(res = res.Replace(Patterns.CommonRules)); + AddHistory(res = res.Rewrite(RewriteRules.Common)); - AddHistory(res = res.Replace(Patterns.NumericNeatRules)); + AddHistory(res = res.Rewrite(RewriteRules.NumericNeat)); /* This was intended to simplify expressions as polynomials over nodes, some kind of @@ -259,8 +259,8 @@ not solve this issue completely and yet too slow to be accepted. // 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) + .Rewrite(RewriteRules.ExpandMultipleAngle) + .Rewrite(RewriteRules.NormalTrigonometricForm) .InnerSimplified; if (openedAngles != res) // Expanded, and for the same reason res is expanded above: the diff --git a/Sources/Tests/UnitTests/Common/SimplificationRegressionTest.cs b/Sources/Tests/UnitTests/Common/SimplificationRegressionTest.cs index 0663d1f78..4fc3895f2 100644 --- a/Sources/Tests/UnitTests/Common/SimplificationRegressionTest.cs +++ b/Sources/Tests/UnitTests/Common/SimplificationRegressionTest.cs @@ -401,5 +401,18 @@ public void ANegativeFactorUnderTheLineStaysUnderIt(string input, double at, dou Assert.True(System.Math.Abs(((Entity.Number.Complex)value).RealPart.EDecimal.ToDouble() - expected) < 1e-9, $"{input} simplified to {simplified.Stringize()}, which is {value.Stringize()} at x = {at}"); } + + // Expand reaches SmartExpandOver with a sum and that method asserts it never gets + // one, so a public call throws AngouriBugException where the honest answer is the + // expression back unexpanded. Simplify handles the same input and gives 1 + x. + // https://github.com/asc-community/AngouriMath/issues/817 + [Theory(Skip = "https://github.com/asc-community/AngouriMath/issues/817")] + [InlineData("(x + 1)! / x!")] + [InlineData("(x + 2)! / x!")] + public void ExpandDoesNotThrowOnAQuotientOfFactorials(string input) + { + var expanded = input.ToEntity().Expand(); + Assert.NotNull(expanded); + } } } diff --git a/Sources/Tests/UnitTests/Core/Transformations/TransformationTest.cs b/Sources/Tests/UnitTests/Core/Transformations/TransformationTest.cs index b88301f7e..aa93427a4 100644 --- a/Sources/Tests/UnitTests/Core/Transformations/TransformationTest.cs +++ b/Sources/Tests/UnitTests/Core/Transformations/TransformationTest.cs @@ -39,6 +39,16 @@ public sealed class TransformationTest "(x ^ 3 + 3 * x ^ 2 * y + 3 * x * y ^ 2 + y ^ 3) / (x + y)", "2 * x + 3 * x", "sqrt(12) + sqrt(27)", + // Not arithmetic, so that the boolean, comparison, set, factorial and totient + // rule sets are exercised rather than passing over inputs they cannot match. + "a and b or a and not b", + "x > 3 and x < 5", + "{ 1, 2 } unite { 2, 3 }", + // Not (x + 1)! / x!, which crashes Expand: + // https://github.com/asc-community/AngouriMath/issues/817 + "x! * (x + 1)", + "phi(12)", + "tan(x) * cot(x)", }.Select(x => new object[] { x }).ToArray(); private static Entity Parse(string raw) => MathS.FromString(raw); @@ -239,6 +249,9 @@ public void ExpandIsItsTransformation(string raw) public void FactorizeIsItsTransformation(string raw) => Assert.Equal(Parse(raw).Factorize(), Transformation.Factorization.Apply(Parse(raw)).Output); + // All three take a level, so all three are checked at one -- including the levels + // outside the range the catalogue keeps built, and the negative ones Simplify passes + // itself when it re-simplifies a candidate. [Theory] [InlineData(0)] [InlineData(1)] @@ -249,6 +262,40 @@ public void FactorizeRunsThePassAsManyTimesAsItIsAsked(int level) Parse("x * y + y + 1 + x").Factorize(level), Transformation.FactorizationAtLevel(level).Apply(Parse("x * y + y + 1 + x")).Output); + [Theory] + [InlineData(-2)] + [InlineData(-1)] + [InlineData(1)] + [InlineData(2)] + [InlineData(3)] + [InlineData(6)] + public void SimplifyIsItsTransformationAtEveryLevel(int level) + => Assert.Equal( + Parse("sin(x) / tan(x) + a / (b / c)").Simplify(level), + Transformation.SimplificationAtLevel(level).Apply(Parse("sin(x) / tan(x) + a / (b / c)")).Output); + + [Theory] + [InlineData(0)] + [InlineData(1)] + [InlineData(2)] + [InlineData(3)] + [InlineData(6)] + public void ExpandIsItsTransformationAtEveryLevel(int level) + => Assert.Equal( + Parse("(x + y) ^ 3 * (a + b)").Expand(level), + Transformation.ExpansionAtLevel(level).Apply(Parse("(x + y) ^ 3 * (a + b)")).Output); + + [Theory] + [InlineData(6)] + [InlineData(-6)] + public void ALevelOutsideTheCachedRangeStillWorks(int level) + { + // The catalogue keeps -4..4 built and constructs anything else on the spot; a + // level outside that range must behave the same, not merely not throw. + var built = Transformation.FactorizationAtLevel(level); + Assert.Equal(Parse("x * y + y + 1 + x").Factorize(level), built.Apply(Parse("x * y + y + 1 + x")).Output); + } + [Theory] [MemberData(nameof(Corpus))] public void DifferentiateIsItsTransformation(string raw) @@ -355,6 +402,17 @@ public void AnEquivalenceTransformationDoesNotChangeTheValue(string raw) Assert.Equal(TransformationRelation.Equivalence, transformation.Relation); if (transformation.Apply(expr).Output is not { } output) continue; + + // Subtracting a set from a set is elementwise, so the difference of two + // equal sets is the set of pairwise differences and not zero. The property + // is about expressions that denote a value; for the rest, the strongest + // honest statement is that the two simplify to the same thing. + if (expr is Entity.Set || output is Entity.Set) + { + Assert.Equal(expr.Simplify(), output.Simplify()); + continue; + } + // The property, not the printed form: subtract the two sides and simplify. // A domain condition may survive that -- the two sides agree only where both // are defined, which is exactly what SoundUnderAssumptions says -- so the @@ -362,12 +420,89 @@ public void AnEquivalenceTransformationDoesNotChangeTheValue(string raw) var difference = (expr - output).Simplify(); if (difference is Entity.Providedf(var value, _)) difference = value; - Assert.True( - difference == 0, - $"{transformation.Name} changed the value of {raw}: difference simplified to {difference}"); + if (difference == 0) + continue; + + // Simplifying the difference to zero proves the two agree; failing to is not + // proof that they differ, only that this simplifier could not settle it. + // x! * (x + 1) expands to (x + 1)!, which is right, and the difference of the + // two does not reduce. So the fallback looks for an actual counterexample + // instead of reporting the unproven case as a defect. + Assert.False( + DisagreesAtSomePoint(expr, output), + $"{transformation.Name} changed the value of {raw}: it gives {output.Stringize()}, " + + "and the two take different values at a point where both are defined"); + } + } + + /// + /// Whether the two take different values somewhere both are defined — a + /// counterexample to their being the same expression written two ways. + /// + /// + /// Points where either side fails to evaluate, or comes out infinite or NaN, say + /// nothing: a rewrite that is valid away from a pole is exactly what + /// means, so those are passed over + /// rather than counted against it. Small positive integers, since factorials and + /// logarithms are only defined on part of the line and 0 and 1 are degenerate for + /// both. + /// + private static bool DisagreesAtSomePoint(Entity one, Entity another) + { + var variables = one.Vars.Concat(another.Vars).Distinct().ToList(); + foreach (var point in new[] { 2, 3, 5 }) + { + Entity left = one, right = another; + foreach (var variable in variables) + { + left = left.Substitute(variable, point); + right = right.Substitute(variable, point); + } + + Entity.Number difference, scale; + try + { + difference = (left - right).EvalNumerical(); + scale = left.EvalNumerical(); + } + catch (AngouriMath.Core.Exceptions.AngouriMathBaseException) + { + continue; + } + + if (!difference.IsFinite || !scale.IsFinite) + continue; + + // Relative: the values here run from single digits to factorials, and an + // absolute threshold would either pass everything large or fail everything + // evaluated to a hundred digits and rounded. + if (Magnitude(difference) > 1e-9 * (1 + Magnitude(scale))) + return true; + } + return false; + + static double Magnitude(Entity.Number number) + { + var complex = (Entity.Number.Complex)number; + var real = complex.RealPart.EDecimal.ToDouble(); + var imaginary = complex.ImaginaryPart.EDecimal.ToDouble(); + return Math.Sqrt(real * real + imaginary * imaginary); } } + [Theory] + // Genuinely different, and the check must say so -- otherwise the property test + // above is a safety net that catches nothing. + [InlineData("x + 1", "x + 2", true)] + [InlineData("x ^ 2", "x ^ 3", true)] + [InlineData("sqrt(x)", "-sqrt(x)", true)] + // The same value written two ways, including the case Simplify cannot settle. + [InlineData("x * 2", "2 * x", false)] + [InlineData("x! * (x + 1)", "(x + 1)!", false)] + [InlineData("sqrt(12) + sqrt(27)", "5 * sqrt(3)", false)] + public void TheCounterexampleSearchFindsCounterexamplesAndOnlyThose(string one, string another, bool expected) + => Assert.Equal(expected, DisagreesAtSomePoint(Parse(one), Parse(another))); + [Fact] public void SubstitutionIsTheOneThingHereThatNeedsNoAssumptions() {