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
33 changes: 33 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -222,6 +222,38 @@ with the measurements against it.
product is contraction, not convolution), and operators or gates — those act *on* states rather than
being states, and they belong to a quantum computing library rather than to a CAS.

## An operation is a value, not only a method

`Simplify`, `Expand`, `Factorize`, `Differentiate`, `Integrate` and `Limit` are adapters over
`AngouriMath.Core.Transformations` — a `Transformation` is the operation itself, carrying a name,
what it claims about its output, and how well justified the claim is. The algorithms underneath are
untouched; what changed is that a step can be named, composed and enumerated rather than only
called. See [`Contributing/Transformations.md`](Sources/AngouriMath/Docs/Contributing/Transformations.md),
and [#746](https://github.com/asc-community/AngouriMath/issues/746) for where it is going.

Three habits it asks for, and each is the honesty rule above in a different place:

**Say which relation you are claiming.** `Equivalence` means the output is another way of writing the
input; `Derivation` means it is a different object. A derivative and an antiderivative are
`Derivation`, and a test that subtracts one from its input and asserts zero is testing nothing.

**Do not label a rewrite `Sound` without an argument.** Every rule set shipped today is
`SoundUnderAssumptions`, and a test over `RewriteRules.All` holds it there. The tier is declared, not
verified — so promoting one means changing that test, and saying in the same change why the rewrite
needs no assumptions. Loosening a tier needs nothing.

**Say "no answer" with `null`, here too.** `ApplyCore` returning `null` is the layer's way of saying
"I could not settle this", and it is the one place where the new layer is *more* honest than the 1.x
method it backs: `Transformation.Integration` has no answer where `Entity.Integrate` returns an
unevaluated `Integralf`. Both are the same claim; neither is `NaN`.

And what not to do with it. `Solve` is not a transformation — it consumes a goal and produces a
solution set, and it belongs in a tactic layer that does not exist yet. `Entity.Set` being an
`Entity` means it would type-check as `Entity -> Entity`, which is the reason to keep it out rather
than a reason to put it in. Nor is there any inverse machinery: `Expand` and `Factor` are not
inverses and `Unsolve` is not well defined, so the API does not invent a symmetry the mathematics
does not have.

## Keep up with the mathematics

Algorithms here are decades of literature deep, and the good ones are written down. Before inventing
Expand Down Expand Up @@ -315,6 +347,7 @@ are short, and a stale one is worse than none — if you change what a file desc
| [`Contributing/General.md`](Sources/AngouriMath/Docs/Contributing/General.md) | the `Entity` hierarchy, in a paragraph |
| [`Contributing/AddingNode.cs`](Sources/AngouriMath/Docs/Contributing/AddingNode.cs) | every place a new node has to be taught about. Read it *before* adding one |
| [`Contributing/ImproveParser.md`](Sources/AngouriMath/Docs/Contributing/ImproveParser.md) | how to change the grammar and regenerate |
| [`Contributing/Transformations.md`](Sources/AngouriMath/Docs/Contributing/Transformations.md) | the transformation layer the 1.x entry points sit on, and how to add the next rule set |
| [`Contributing/coding_rules.md`](Sources/AngouriMath/Docs/Contributing/coding_rules.md) | sealed-or-abstract, and immutability of `Entity` |
| [`WhatsNew/version_performance_control.md`](Sources/AngouriMath/Docs/WhatsNew/version_performance_control.md) | the inter-version performance table, and how to add a column |
| `Sources/Analyzers/` | the custom analyzers, including the static-field one behind `[ConstantField]` |
Expand Down
10 changes: 10 additions & 0 deletions Sources/.editorconfig
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,13 @@ dotnet_diagnostic.IDE0090.severity = none
# dotnet_diagnostic.CA2252.severity = none

file_header_template=\nCopyright (c) 2019-2022 Angouri.\nAngouriMath is licensed under MIT.\nDetails: https://github.com/asc-community/AngouriMath/blob/master/LICENSE.md.\nWebsite: https://am.angouri.org.\n

# Files written after 2022 say so. Bumping the year on the template above would make
# IDE0073 fail on all 367 files that already carry the old one, which is a rewrite of every
# header folded into whatever branch happened to need a new file -- so the year moves per
# directory as directories are added, not all at once.
[AngouriMath/Core/Transformations/*.cs]
file_header_template=\nCopyright (c) 2019-2026 Angouri.\nAngouriMath is licensed under MIT.\nDetails: https://github.com/asc-community/AngouriMath/blob/master/LICENSE.md.\nWebsite: https://am.angouri.org.\n

[Tests/UnitTests/Core/Transformations/*.cs]
file_header_template=\nCopyright (c) 2019-2026 Angouri.\nAngouriMath is licensed under MIT.\nDetails: https://github.com/asc-community/AngouriMath/blob/master/LICENSE.md.\nWebsite: https://am.angouri.org.\n
104 changes: 104 additions & 0 deletions Sources/AngouriMath/Core/Transformations/RewriteRuleSet.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
//
// Copyright (c) 2019-2026 Angouri.
// AngouriMath is licensed under MIT.
// Details: https://github.com/asc-community/AngouriMath/blob/master/LICENSE.md.
// Website: https://am.angouri.org.
//

using System;

namespace AngouriMath.Core.Transformations
{
/// <summary>
/// A named, attributable group of rewrites — the unit this library has always written
/// them in — carrying what it is called, what it claims and how well justified the
/// claim is, so that the set can be enumerated, tested and referred to by name instead
/// of only being called.
/// </summary>
/// <remarks>
/// <para>
/// The set, rather than the single <c>pattern -&gt; replacement</c> line, is the unit
/// here on purpose. Every rewrite in this library is a case of one <c>switch</c>
/// matched against a node, and the C# compiler turns that switch into a type test and a
/// jump. Splitting each case into its own object would replace one dispatch per node
/// with one delegate call per rule per node on the hottest path in the library, and buy
/// nothing that a caller can use today. What a caller can use today is the set:
/// <see cref="RewriteRules.All"/> is enumerable, each entry is applicable on its own,
/// and the tests iterate it.
/// </para>
/// <para>
/// The finer grain is the next step rather than the abandoned one — see
/// <a href="https://github.com/asc-community/AngouriMath/issues/746">#746</a> on rules
/// as data — and nothing here forecloses it: a set whose rewrites become individually
/// addressable keeps the same name and the same entry in the registry.
/// </para>
/// </remarks>
public sealed class RewriteRuleSet
{
private readonly Func<Entity, Entity> rules;

internal RewriteRuleSet(string name, string description, TransformationRelation relation, Soundness soundness, Func<Entity, Entity> rules)
=> (Name, Description, Relation, Soundness, this.rules) = (name, description, relation, soundness, rules);

/// <summary>A stable identity for this set.</summary>
public string Name { get; }

/// <summary>What the set is for, in a sentence.</summary>
public string Description { get; }

/// <summary>What the rewrites in this set claim about the expressions they produce.</summary>
public TransformationRelation Relation { get; }

/// <summary>How well justified that claim is. See <see cref="Soundness"/> on what a tier here is and is not.</summary>
public Soundness Soundness { get; }

/// <summary>
/// Applies the set once, bottom-up over every node, exactly as
/// <see cref="Entity.Replace(Func{Entity, Entity})"/> does. One pass: a rewrite
/// that opens up an opportunity for another rewrite in the same set will not see it
/// taken until the next pass.
/// </summary>
/// <param name="expression">The expression to rewrite.</param>
public Entity ApplyOnce(Entity expression)
=> expression is null
? throw new ArgumentNullException(nameof(expression))
: expression.Replace(rules);

/// <summary>
/// This set as a <see cref="Transformation"/>, so that it composes with the rest of
/// the catalogue.
/// </summary>
/// <remarks>
/// Built on demand, and it has to be. <see cref="RewritingTransformation"/> derives
/// from <see cref="Transformation"/>, so constructing one runs that type's static
/// initialiser -- which reads <see cref="RewriteRules"/>. Doing it in this
/// constructor instead would make the two types depend on each other's
/// initialisation and hand whichever one lost the race a null rule set. Two threads
/// arriving together may each build one; they are equivalent and immutable, so
/// whichever reference lands is the one everyone then uses.
/// </remarks>
public Transformation AsTransformation() => asTransformation ??= new RewritingTransformation(this);
private Transformation? asTransformation;

/// <inheritdoc/>
public override string ToString() => Name;

private sealed class RewritingTransformation : Transformation
{
private readonly RewriteRuleSet ruleSet;

internal RewritingTransformation(RewriteRuleSet ruleSet) => this.ruleSet = ruleSet;

public override string Name => $"rewrite[{ruleSet.Name}]";

public override TransformationRelation Relation => ruleSet.Relation;

public override Soundness Soundness => ruleSet.Soundness;

// A rewrite pass always has an answer: where nothing matched, the answer is the
// expression it was given. That is a fixed point, not a failure, and
// TransformationResult.Changed is what tells the two apart.
protected override Entity? ApplyCore(Entity input) => ruleSet.ApplyOnce(input);
}
}
}
161 changes: 161 additions & 0 deletions Sources/AngouriMath/Core/Transformations/RewriteRules.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
//
// Copyright (c) 2019-2026 Angouri.
// AngouriMath is licensed under MIT.
// Details: https://github.com/asc-community/AngouriMath/blob/master/LICENSE.md.
// Website: https://am.angouri.org.
//

using AngouriMath.Functions;

namespace AngouriMath.Core.Transformations
{
/// <summary>
/// The rewrite rule sets this library ships, as data: named, described, attributed with
/// what they claim, and enumerable through <see cref="All"/>.
/// </summary>
/// <remarks>
/// <para>
/// Registration is explicit and static — there is no assembly scanning and no
/// <c>Activator</c>, so the registry survives trimming and NativeAOT, and
/// <see cref="All"/> is in a fixed order that does not depend on hashing, reflection or
/// which type happened to be loaded first.
/// </para>
/// <para>
/// This is a slice, not the whole pattern table. The sets below are the ones the
/// transformations in <see cref="Transformation"/> are built from; the rest of
/// <c>Functions/Simplification/Patterns</c> is still reached only from
/// <c>Simplificator</c>. Adding one here is five lines and gets it enumeration, a
/// soundness label and the tests over <see cref="All"/> for free.
/// </para>
/// </remarks>
public static class RewriteRules
{
/// <summary>
/// Puts the operands of commutative chains into a canonical order and groups equal
/// ones together, so that <c>x + y</c> and <c>y + x</c> stop being different trees.
/// </summary>
public static RewriteRuleSet CanonicalOrder { get; } = new(
nameof(CanonicalOrder),
"Sorts and groups the operands of sums, products, conjunctions, disjunctions and set operations.",
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));

/// <summary>
/// Turns a negative power into a quotient: <c>a * b ^ (-1)</c> becomes <c>a / b</c>.
/// </summary>
public static RewriteRuleSet InvertNegativePowers { get; } = new(
nameof(InvertNegativePowers),
"Rewrites negative powers as quotients.",
TransformationRelation.Equivalence,
Soundness.SoundUnderAssumptions,
Patterns.InvertNegativePowers);

/// <summary>
/// Brings a negative numeric factor out in front of the term it multiplies.
/// </summary>
public static RewriteRuleSet InvertNegativeMultipliers { get; } = new(
nameof(InvertNegativeMultipliers),
"Moves a negative numeric factor out of a product into the sign of the term.",
TransformationRelation.Equivalence,
Soundness.SoundUnderAssumptions,
Patterns.InvertNegativeMultipliers);

/// <summary>
/// The arithmetic housekeeping rules — collecting like terms, flattening nested
/// quotients, moving numeric coefficients to the front.
/// </summary>
public static RewriteRuleSet Common { get; } = new(
nameof(Common),
"Collects like terms and normalises the arrangement of products and quotients.",
TransformationRelation.Equivalence,
Soundness.SoundUnderAssumptions,
Patterns.CommonRules);

/// <summary>
/// Rules about powers, roots and logarithms.
/// </summary>
public static RewriteRuleSet Power { get; } = new(
nameof(Power),
"Gathers and splits powers, roots and logarithms.",
TransformationRelation.Equivalence,
// (a ^ b) ^ c is a ^ (b c) only on a branch; the rules guard for it, and the
// guard is what the tier is stating.
Soundness.SoundUnderAssumptions,
Patterns.PowerRules);

/// <summary>
/// The trigonometric identities.
/// </summary>
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);

/// <summary>
/// Multiplies products over sums out.
/// </summary>
public static RewriteRuleSet Expansion { get; } = new(
nameof(Expansion),
"Distributes products and powers over sums.",
TransformationRelation.Equivalence,
Soundness.SoundUnderAssumptions,
Patterns.ExpandRules);

/// <summary>
/// Takes common factors back out of a sum.
/// </summary>
public static RewriteRuleSet Factorization { get; } = new(
nameof(Factorization),
"Gathers common factors out of sums.",
TransformationRelation.Equivalence,
Soundness.SoundUnderAssumptions,
Patterns.FactorizeRules);

/// <summary>
/// Recognises a perfect square written out, so that factorisation has something to
/// gather.
/// </summary>
public static RewriteRuleSet PerfectSquare { get; } = new(
nameof(PerfectSquare),
"Collapses a written-out perfect square into a squared binomial.",
TransformationRelation.Equivalence,
Soundness.SoundUnderAssumptions,
Patterns.PerfectSquareRules);

/// <summary>
/// Clears a surd out of a two-term denominator.
/// </summary>
public static RewriteRuleSet RationaliseDenominator { get; } = new(
nameof(RationaliseDenominator),
"Multiplies a quotient by the conjugate of its denominator to clear a surd from it.",
TransformationRelation.Equivalence,
Soundness.SoundUnderAssumptions,
Patterns.RationaliseDenominator);

/// <summary>
/// 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
/// whichever ones somebody remembered.
/// </summary>
public static IReadOnlyList<RewriteRuleSet> All { get; } = new[]
{
CanonicalOrder,
InvertNegativePowers,
InvertNegativeMultipliers,
Common,
Power,
Trigonometric,
Expansion,
Factorization,
PerfectSquare,
RationaliseDenominator,
};
}
}
43 changes: 43 additions & 0 deletions Sources/AngouriMath/Core/Transformations/Soundness.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
//
// 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>
/// How well justified the relation a <see cref="Transformation"/> claims between its
/// input and its output is. The three tiers are never blurred: a heuristic labelled as
/// a proof is a wrong answer with a friendly face.
/// </summary>
/// <remarks>
/// The tier is <b>declared</b> by whoever wrote the transformation, not derived from it.
/// Nothing in the library checks a declaration today, so a tier is a claim to be argued
/// with rather than a guarantee to be relied on, and the registry starts conservative
/// on purpose: tightening a label needs an argument, loosening one does not.
/// </remarks>
public enum Soundness
{
/// <summary>
/// The claimed relation holds for every value of the free variables, with no side
/// conditions and no choice of branch.
/// </summary>
Sound,

/// <summary>
/// The claimed relation holds only where the stated assumptions do: wherever both
/// sides are defined, under the conditions the output carries as
/// <see cref="Entity.Providedf"/>, or under a branch-cut convention. This is the
/// honest tier for most of the rewrite rules in this library.
/// </summary>
SoundUnderAssumptions,

/// <summary>
/// Worth trying; proves nothing. A heuristic result has to be checked by something
/// else before it may be returned as an answer.
/// </summary>
Heuristic
}
}
Loading
Loading