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
8 changes: 8 additions & 0 deletions Sources/.editorconfig
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,14 @@ file_header_template=\nCopyright (c) 2019-2026 Angouri.\nAngouriMath is licensed
[Tests/UnitTests/Algebra/GroebnerSystemTest.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

# Named per file rather than per directory: AngouriMath/Core holds files from every year of
# the project, so widening the pattern would demand a header rewrite of all of them.
[AngouriMath/Core/CostModel.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/CostModelTest.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/Common/ListArgumentOverloadTest.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

Expand Down
47 changes: 9 additions & 38 deletions Sources/AngouriMath/Convenience/MathS.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5642,44 +5642,15 @@ internal static EDecimal DowncastingTolerance
/// By default criteria it cannot simplify it further, however, the custom one
/// it simplified from 2 to 1.
/// </example>
public static Setting<Func<Entity, double>> ComplexityCriteria { get; } = new Func<Entity, double>(expr =>
{
// Those are of the 2nd power to avoid problems with floating numbers
const double TinyWeight = 0.5;
const double MinorWeight = 1.0;
const double Weight = 2.0;
const double MajorWeight = 4.0;
const double HeavyWeight = 8.0;
const double ExtraHeavyWeight = 12.0;

static double DefaultCriteria(Entity expr) => expr switch {
// Weigh provided predicates much less but nested provideds heavy
Providedf(var inner, var predicate) =>
DefaultCriteria(inner) + 0.1 * DefaultCriteria(predicate) + ExtraHeavyWeight * (inner.Nodes.Count(n => n is Providedf) + predicate.Nodes.Count(n => n is Providedf)),
Piecewise { Cases: var cases } =>
cases.Sum(@case =>
DefaultCriteria(@case.Expression) + 0.1 * DefaultCriteria(@case.Predicate) + ExtraHeavyWeight * (@case.Expression.Nodes.Count(n => n is Providedf) + @case.Predicate.Nodes.Count(n => n is Providedf))),
Variable => Weight, // Number of variables
// A root in a denominator, which the rationalising rule clears out.
// Without a weight here the two forms tie -- 1 / (sqrt(3) + 5) and
// (sqrt(3) - 5) / (-22) are the same rate -- and a tie is settled by
// whichever candidate was generated first, which is not a preference
// so much as an accident. This states the preference instead.
// https://github.com/asc-community/AngouriMath/issues/205
Divf(_, var divisor) when divisor.Nodes.Any(node => node is Powf(_, Rational and not Integer))
=> MinorWeight + Weight + expr.DirectChildren.Sum(DefaultCriteria),
Divf => MinorWeight + expr.DirectChildren.Sum(DefaultCriteria), // Number of divides
Rational(Integer(1 or -1), _) and not Integer => Weight + expr.DirectChildren.Sum(DefaultCriteria), // Number of rationals with unit numerator
Powf(_, Real { IsNegative: true }) => HeavyWeight + expr.DirectChildren.Sum(DefaultCriteria), // Number of negative powers
Logf => TinyWeight + expr.DirectChildren.Sum(DefaultCriteria), // Number of logarithms
Phif => ExtraHeavyWeight + expr.DirectChildren.Sum(DefaultCriteria), // Number of phi functions
Real { IsNegative: true } => MajorWeight + expr.DirectChildren.Sum(DefaultCriteria), // Number of negative reals
ComparisonSign when expr.DirectChildren[0] == 0 => Weight + expr.DirectChildren.Sum(DefaultCriteria), // 0 < x is bad. x > 0 is good.
Notf (Equalsf eq) => -Weight + DefaultCriteria(eq), // (not x = 0) is equally complex as (x = 0)
_ => expr.DirectChildren.Sum(DefaultCriteria)
} + Weight; // Number of nodes
return DefaultCriteria(expr);
});
/// <remarks>
/// The function itself lives on <see cref="CostModel.Default"/>, with the named
/// alternatives beside it — <see cref="CostModel.SmallestTree"/>,
/// <see cref="CostModel.FewestDivisions"/>, <see cref="CostModel.FewestRadicals"/>.
/// It is referenced rather than repeated so the setting's default and the model
/// cannot drift apart.
/// </remarks>
public static Setting<Func<Entity, double>> ComplexityCriteria { get; } =
new Func<Entity, double>(CostModel.DefaultCost);

/// <summary>
/// Settings for the Newton-Raphson's root-search method
Expand Down
152 changes: 152 additions & 0 deletions Sources/AngouriMath/Core/CostModel.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
//
// 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;
using System.Collections.Generic;
using System.Linq;
using static AngouriMath.Entity;
using static AngouriMath.Entity.Number;

namespace AngouriMath.Core
{
/// <summary>
/// What "simpler" means, as a named value rather than as an anonymous function.
/// </summary>
/// <remarks>
/// <para>
/// <see cref="MathS.Settings.ComplexityCriteria"/> has always accepted any
/// <see cref="Func{Entity, Double}"/>, so the cost of an expression was already the caller's
/// to choose. What it could not do is be <i>named</i>: two callers wanting "the smallest tree"
/// each wrote the same lambda, neither could say which one they used, and nothing could list
/// what the alternatives are. <a href="https://github.com/asc-community/AngouriMath/issues/746">#746</a>
/// v2.0 asks for a cost model that is data for exactly that reason, and names the examples —
/// smallest tree, fewest radicals — that <see cref="All"/> now holds.
/// </para>
/// <para>
/// <b>Every model here counts nodes a little, even the ones that are about something else.</b>
/// A criterion that counts only its own feature ties constantly, and a tie is settled by
/// whichever candidate the search happened to generate first — which is an accident rather
/// than a preference. The node term is small enough not to overturn the feature it is added to
/// and large enough to decide between candidates the feature cannot separate.
/// </para>
/// <example>
/// <code>
/// using var _ = MathS.Settings.ComplexityCriteria.Set(CostModel.FewestDivisions.Cost);
/// Console.WriteLine("a / b + b / c".ToEntity().Simplify()); // (a * c + b ^ 2) / (b * c)
/// </code>
/// </example>
/// </remarks>
/// <param name="Name">A short name, so a report can say which model produced an answer.</param>
/// <param name="Description">What this model prefers, in a sentence.</param>
/// <param name="Cost">
/// The cost of an expression; lower is simpler. Pass this to
/// <see cref="MathS.Settings.ComplexityCriteria"/>.
/// </param>
public sealed record CostModel(string Name, string Description, Func<Entity, double> Cost)
{
/// <summary>
/// What <see cref="MathS.Settings.ComplexityCriteria"/> uses when nothing is set: a
/// weighted count that prefers few nodes, few divisions, few negative powers, and a
/// rationalised denominator.
/// </summary>
/// <remarks>
/// This is the same function the setting is initialised with, not a copy of it, so the
/// two cannot drift apart. Setting it explicitly changes nothing.
/// </remarks>
public static CostModel Default { get; } = new(
nameof(Default),
"A weighted count, preferring few nodes, few divisions and no root in a denominator.",
DefaultCost);

/// <summary>Prefers the expression with fewest nodes, counting every node alike.</summary>
/// <remarks>
/// The plainest possible notion of simple, and a useful contrast with
/// <see cref="Default"/>: it has no opinion about which node is worse, so it will accept
/// a division or a negative power that <see cref="Default"/> would pay to remove.
/// </remarks>
public static CostModel SmallestTree { get; } = new(
nameof(SmallestTree),
"Fewest nodes, with no preference between them.",
static expr => expr.Nodes.Count());

/// <summary>Prefers the expression with fewest divisions, then fewest nodes.</summary>
/// <remarks>
/// A negative power is a division written differently, so it counts too — otherwise the
/// model would merely move divisions rather than remove them.
/// </remarks>
public static CostModel FewestDivisions { get; } = new(
nameof(FewestDivisions),
"Fewest divisions, counting a negative power as one, then fewest nodes.",
static expr => Feature(expr, static node =>
node is Divf || node is Powf(_, Real { IsNegative: true })));

/// <summary>Prefers the expression with fewest radicals, then fewest nodes.</summary>
/// <remarks>
/// A radical is a power by a non-integer rational — <c>sqrt(x)</c> is <c>x ^ (1/2)</c>
/// here, and there is no separate root node to count.
/// </remarks>
public static CostModel FewestRadicals { get; } = new(
nameof(FewestRadicals),
"Fewest fractional powers, then fewest nodes.",
static expr => Feature(expr, static node =>
node is Powf(_, Rational and not Integer)));

/// <summary>
/// Every model here, so a caller can offer the choice rather than hard-code one.
/// </summary>
public static IReadOnlyList<CostModel> All { get; } =
new[] { Default, SmallestTree, FewestDivisions, FewestRadicals };

/// <summary>The name, which is what a report wants.</summary>
public override string ToString() => Name;

/// <summary>
/// The count of nodes satisfying <paramref name="predicate"/>, plus a small term in the
/// total size so that expressions the predicate cannot separate are still ordered.
/// </summary>
private static double Feature(Entity expr, Func<Entity, bool> predicate)
=> expr.Nodes.Count(predicate) + 0.001 * expr.Nodes.Count();

// The default criteria, as a method rather than a lambda so that
// MathS.Settings.ComplexityCriteria and CostModel.Default are one function and not two.
// Those weights are of the 2nd power to avoid problems with floating numbers.
private const double TinyWeight = 0.5;
private const double MinorWeight = 1.0;
private const double Weight = 2.0;
private const double MajorWeight = 4.0;
private const double HeavyWeight = 8.0;
private const double ExtraHeavyWeight = 12.0;

internal static double DefaultCost(Entity expr) => expr switch
{
// Weigh provided predicates much less but nested provideds heavy
Providedf(var inner, var predicate) =>
DefaultCost(inner) + 0.1 * DefaultCost(predicate) + ExtraHeavyWeight * (inner.Nodes.Count(n => n is Providedf) + predicate.Nodes.Count(n => n is Providedf)),
Piecewise { Cases: var cases } =>
cases.Sum(@case =>
DefaultCost(@case.Expression) + 0.1 * DefaultCost(@case.Predicate) + ExtraHeavyWeight * (@case.Expression.Nodes.Count(n => n is Providedf) + @case.Predicate.Nodes.Count(n => n is Providedf))),
Variable => Weight, // Number of variables
// A root in a denominator, which the rationalising rule clears out.
// Without a weight here the two forms tie -- 1 / (sqrt(3) + 5) and
// (sqrt(3) - 5) / (-22) are the same rate -- and a tie is settled by
// whichever candidate was generated first, which is not a preference
// so much as an accident. This states the preference instead.
// https://github.com/asc-community/AngouriMath/issues/205
Divf(_, var divisor) when divisor.Nodes.Any(node => node is Powf(_, Rational and not Integer))
=> MinorWeight + Weight + expr.DirectChildren.Sum(DefaultCost),
Divf => MinorWeight + expr.DirectChildren.Sum(DefaultCost), // Number of divides
Rational(Integer(1 or -1), _) and not Integer => Weight + expr.DirectChildren.Sum(DefaultCost), // Number of rationals with unit numerator
Powf(_, Real { IsNegative: true }) => HeavyWeight + expr.DirectChildren.Sum(DefaultCost), // Number of negative powers
Logf => TinyWeight + expr.DirectChildren.Sum(DefaultCost), // Number of logarithms
Phif => ExtraHeavyWeight + expr.DirectChildren.Sum(DefaultCost), // Number of phi functions
Real { IsNegative: true } => MajorWeight + expr.DirectChildren.Sum(DefaultCost), // Number of negative reals
ComparisonSign when expr.DirectChildren[0] == 0 => Weight + expr.DirectChildren.Sum(DefaultCost), // 0 < x is bad. x > 0 is good.
Notf(Equalsf eq) => -Weight + DefaultCost(eq), // (not x = 0) is equally complex as (x = 0)
_ => expr.DirectChildren.Sum(DefaultCost)
} + Weight; // Number of nodes
}
}
18 changes: 18 additions & 0 deletions Sources/Tests/UnitTests/Common/PublicApi.txt
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,23 @@ AngouriMath.Core.Compilation.IntoLinq.CompilationProtocol.ctor()
AngouriMath.Core.Compilation.IntoLinq.CompilationProtocol.ctor(AngouriMath.Core.Compilation.IntoLinq.CompilationProtocol)
AngouriMath.Core.Compilation.IntoLinq.CompilationProtocol.op_Equality(AngouriMath.Core.Compilation.IntoLinq.CompilationProtocol, AngouriMath.Core.Compilation.IntoLinq.CompilationProtocol) : System.Boolean
AngouriMath.Core.Compilation.IntoLinq.CompilationProtocol.op_Inequality(AngouriMath.Core.Compilation.IntoLinq.CompilationProtocol, AngouriMath.Core.Compilation.IntoLinq.CompilationProtocol) : System.Boolean
AngouriMath.Core.CostModel.<Clone>$() : AngouriMath.Core.CostModel
AngouriMath.Core.CostModel.All { } : System.Collections.Generic.IReadOnlyList<AngouriMath.Core.CostModel>
AngouriMath.Core.CostModel.Cost { } : System.Func<AngouriMath.Entity,System.Double>
AngouriMath.Core.CostModel.Deconstruct(System.String&, System.String&, System.Func<AngouriMath.Entity,System.Double>&) : System.Void
AngouriMath.Core.CostModel.Default { } : AngouriMath.Core.CostModel
AngouriMath.Core.CostModel.Description { } : System.String
AngouriMath.Core.CostModel.Equals(AngouriMath.Core.CostModel) : System.Boolean
AngouriMath.Core.CostModel.Equals(System.Object) : System.Boolean
AngouriMath.Core.CostModel.FewestDivisions { } : AngouriMath.Core.CostModel
AngouriMath.Core.CostModel.FewestRadicals { } : AngouriMath.Core.CostModel
AngouriMath.Core.CostModel.GetHashCode() : System.Int32
AngouriMath.Core.CostModel.Name { } : System.String
AngouriMath.Core.CostModel.SmallestTree { } : AngouriMath.Core.CostModel
AngouriMath.Core.CostModel.ToString() : System.String
AngouriMath.Core.CostModel.ctor(System.String, System.String, System.Func<AngouriMath.Entity,System.Double>)
AngouriMath.Core.CostModel.op_Equality(AngouriMath.Core.CostModel, AngouriMath.Core.CostModel) : System.Boolean
AngouriMath.Core.CostModel.op_Inequality(AngouriMath.Core.CostModel, AngouriMath.Core.CostModel) : System.Boolean
AngouriMath.Core.Domain.Any : AngouriMath.Core.Domain
AngouriMath.Core.Domain.Boolean : AngouriMath.Core.Domain
AngouriMath.Core.Domain.Complex : AngouriMath.Core.Domain
Expand Down Expand Up @@ -2452,6 +2469,7 @@ AngouriMath.MathS.oo : AngouriMath.Entity+Number+Real
AngouriMath.MathS.pi : AngouriMath.Entity+Variable
class AngouriMath.Convenience.Setting<T>
class AngouriMath.Core.Compilation.IntoLinq.CompilationProtocol
class AngouriMath.Core.CostModel
class AngouriMath.Core.EquationSystem
class AngouriMath.Core.Exceptions.AngouriBugException
class AngouriMath.Core.Exceptions.AngouriMathBaseException
Expand Down
Loading
Loading