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()
{