Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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
{
/// <summary>
/// Reading order for <see cref="RewriteRuleSet.ApplyOnce(Entity)"/>.
/// </summary>
/// <remarks>
/// 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.
/// </remarks>
internal static class RewriteRuleSetExtensions
{
/// <summary>Applies <paramref name="ruleSet"/> once over every node of <paramref name="expression"/>.</summary>
internal static Entity Rewrite(this Entity expression, RewriteRuleSet ruleSet)
=> ruleSet.ApplyOnce(expression);
}
}
315 changes: 296 additions & 19 deletions Sources/AngouriMath/Core/Transformations/RewriteRules.cs

Large diffs are not rendered by default.

18 changes: 14 additions & 4 deletions Sources/AngouriMath/Docs/Contributing/Transformations.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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

Expand All @@ -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:

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<Entity>();
foreach (var linChild in Sumf.LinearChildren(this))
if (TreeAnalyzer.SmartExpandOver(linChild, entity => true) is { } exp)
Expand Down Expand Up @@ -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;
Expand Down
66 changes: 33 additions & 33 deletions Sources/AngouriMath/Functions/Simplification/Simplificator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -83,7 +83,7 @@ void __IterAddHistory(Entity expr)
history[ncompl] = new HashSet<Entity> { n };
}
__IterAddHistory(expr);
__IterAddHistory(expr.Replace(Patterns.InvertNegativePowers));
__IterAddHistory(expr.Rewrite(RewriteRules.InvertNegativePowers));

MultithreadingFunctional.ExitIfCancelled();
}
Expand Down Expand Up @@ -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
Expand All @@ -138,77 +138,77 @@ 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);
}
}


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)
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down
13 changes: 13 additions & 0 deletions Sources/Tests/UnitTests/Common/SimplificationRegressionTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}
}
Loading
Loading