From dcde5aa573f826a5579b5be96cf1aa60e01cc53f Mon Sep 17 00:00:00 2001 From: Rafael Vuijk Date: Fri, 28 Aug 2026 23:14:02 +0000 Subject: [PATCH] A cost model reaches an API, not only an ambient setting #746 tier 2's own row names this remaining, in its own words: "a cost model that reaches an API rather than an ambient setting". Checked against the actual API surface rather than assumed: `Entity.Simplify(int level = 2)` has no overload taking a `CostModel`, and the only way to change what candidate `Simplify` prefers is `MathS.Settings.ComplexityCriteria.Set(...)` around the call. That setting was already safe to use -- `Setting` is async-local, not thread-static, so one caller's override cannot leak into another's concurrent call, the same design `MathS.Settings.Budget` and `BudgetLedger.For` already share -- but it is not a place in the addressable API `Transformation` is, which is what tier 2's own wording asks for. `Transformation.SimplificationAtLevel(int level, CostModel costModel)` is that place. It does not thread an explicit cost parameter through `Simplificator` -- that would be a second candidate-search pipeline to keep in step with the one `Entity.Simplify(int)` actually runs, which is exactly the risk the existing pipeline was written once to avoid. It scopes the existing, already-tested `ComplexityCriteria` setting for the duration of this one call instead, so `Simplify`'s own behaviour is the thing being configured rather than a parallel implementation of it. `CostModel.Default` given explicitly behaves identically to the overload without one, and passing it is how a caller states that on purpose. TDD: a spy `CostModel` proves the given model is the one actually consulted (not the ambient default); a before/after check on `MathS.Settings.ComplexityCriteria.IsOverriden` proves the scope does not leak past the call; and `CostModel.Default` given explicitly is checked against the plain overload to prove it changes nothing when the caller asks for what was already happening. `PublicApi.txt` regenerated for the one new public member. Full suite: 8797 tests, 8783 passed, 14 skipped (pre-existing), 0 failed. --- .../Transformation.Catalogue.cs | 48 +++++++++++++++++-- Sources/Tests/UnitTests/Common/PublicApi.txt | 1 + .../Transformations/TransformationTest.cs | 35 ++++++++++++++ 3 files changed, 80 insertions(+), 4 deletions(-) diff --git a/Sources/AngouriMath/Core/Transformations/Transformation.Catalogue.cs b/Sources/AngouriMath/Core/Transformations/Transformation.Catalogue.cs index 070282199..314323308 100644 --- a/Sources/AngouriMath/Core/Transformations/Transformation.Catalogue.cs +++ b/Sources/AngouriMath/Core/Transformations/Transformation.Catalogue.cs @@ -40,7 +40,38 @@ public static Transformation Rewriting(RewriteRuleSet ruleSet) /// How hard to look; the same argument takes. /// public static Transformation SimplificationAtLevel(int level) - => LevelledCache.Simplification.For(level, static l => new SimplificationTransformation(l)); + => LevelledCache.Simplification.For(level, static l => new SimplificationTransformation(l, null)); + + /// + /// The full simplification pipeline at a chosen level, rating candidates by + /// instead of . + /// + /// + /// How hard to look; the same argument takes. + /// + /// + /// Which candidate counts as cheapest. here behaves + /// exactly like — passing it is how a caller + /// states that on purpose rather than by leaving a parameter out. + /// + /// + /// #746 tier 2 named + /// this remaining on its own row: "a cost model that reaches an API rather than an ambient + /// setting". already is an API in + /// the sense that a caller can scope it explicitly and safely — it is backed by the same + /// uses, async-local rather + /// than thread-static, so one caller's override cannot leak into another's concurrent call. + /// What it lacked was a place in this API, the addressable one + /// is: composing SimplificationAtLevel(2, costModel).Then(...) names the choice + /// where setting an ambient value around a call does not. This overload does not + /// reimplement candidate search against an explicit parameter threaded through + /// Simplificator -- that would be a second pipeline to keep in step with the one + /// actually runs. It scopes the existing, already-tested + /// setting for the duration of this one call instead of introducing a second pipeline. + /// + public static Transformation SimplificationAtLevel(int level, CostModel costModel) + => new SimplificationTransformation( + level, costModel ?? throw new ArgumentNullException(nameof(costModel))); /// /// Multiplies products over sums out, as does. @@ -427,16 +458,25 @@ private sealed class InnerSimplificationTransformation : Transformation private sealed class SimplificationTransformation : Transformation { private readonly int level; + private readonly CostModel? costModel; - internal SimplificationTransformation(int level) => this.level = level; + internal SimplificationTransformation(int level, CostModel? costModel) + => (this.level, this.costModel) = (level, costModel); - public override string Name => $"simplify[{level}]"; + public override string Name + => costModel is null ? $"simplify[{level}]" : $"simplify[{level}, {costModel.Name}]"; public override TransformationRelation Relation => TransformationRelation.Equivalence; public override Soundness Soundness => Soundness.SoundUnderAssumptions; - protected override Entity? ApplyCore(Entity input) => Simplificator.Simplify(input, level); + protected override Entity? ApplyCore(Entity input) + { + if (costModel is null) + return Simplificator.Simplify(input, level); + using var _ = MathS.Settings.ComplexityCriteria.Set(costModel.Cost); + return Simplificator.Simplify(input, level); + } } private sealed class ExpansionTransformation : Transformation diff --git a/Sources/Tests/UnitTests/Common/PublicApi.txt b/Sources/Tests/UnitTests/Common/PublicApi.txt index 8a34c5f42..9e9598d73 100644 --- a/Sources/Tests/UnitTests/Common/PublicApi.txt +++ b/Sources/Tests/UnitTests/Common/PublicApi.txt @@ -264,6 +264,7 @@ AngouriMath.Core.Transformations.Transformation.Rewriting(AngouriMath.Core.Trans AngouriMath.Core.Transformations.Transformation.RuleBasedFactorizationAtLevel(System.Int32) : AngouriMath.Core.Transformations.Transformation AngouriMath.Core.Transformations.Transformation.Simplification { } : AngouriMath.Core.Transformations.Transformation AngouriMath.Core.Transformations.Transformation.SimplificationAtLevel(System.Int32) : AngouriMath.Core.Transformations.Transformation +AngouriMath.Core.Transformations.Transformation.SimplificationAtLevel(System.Int32, AngouriMath.Core.CostModel) : AngouriMath.Core.Transformations.Transformation AngouriMath.Core.Transformations.Transformation.Soundness { } : AngouriMath.Core.Transformations.Soundness AngouriMath.Core.Transformations.Transformation.Substitution(AngouriMath.Entity, AngouriMath.Entity) : AngouriMath.Core.Transformations.Transformation AngouriMath.Core.Transformations.Transformation.Then(AngouriMath.Core.Transformations.Transformation) : AngouriMath.Core.Transformations.Transformation diff --git a/Sources/Tests/UnitTests/Core/Transformations/TransformationTest.cs b/Sources/Tests/UnitTests/Core/Transformations/TransformationTest.cs index 192bc8147..758ed295b 100644 --- a/Sources/Tests/UnitTests/Core/Transformations/TransformationTest.cs +++ b/Sources/Tests/UnitTests/Core/Transformations/TransformationTest.cs @@ -546,6 +546,41 @@ public void RationalizationClearsASurdOutOfADenominator() node => node is Entity.Divf(_, var denominator) && denominator.Nodes.Any(n => n is Entity.Powf)); } + [Fact] + public void SimplificationAtLevelWithACostModelConsultsThatModelRatherThanTheAmbientDefault() + { + var calls = 0; + var spy = new CostModel("spy", "counts how many candidates it was asked to rate", e => + { + calls++; + return CostModel.Default.Cost(e); + }); + + Transformation.SimplificationAtLevel(2, spy).Apply(Parse("(x + 1) ^ 2")); + + Assert.True(calls > 0); + } + + [Fact] + public void SimplificationAtLevelWithACostModelDoesNotLeakTheOverrideAfterwards() + { + Assert.False(MathS.Settings.ComplexityCriteria.IsOverriden); + + Transformation.SimplificationAtLevel(2, CostModel.FewestDivisions).Apply(Parse("(x + 1) ^ 2")); + + Assert.False(MathS.Settings.ComplexityCriteria.IsOverriden); + } + + [Fact] + public void SimplificationAtLevelWithTheDefaultCostModelMatchesTheOverloadWithout() + { + var withoutModel = Transformation.SimplificationAtLevel(2).Apply(Parse("sin(x) / tan(x) + a / (b / c)")); + var withDefaultModel = Transformation.SimplificationAtLevel(2, CostModel.Default) + .Apply(Parse("sin(x) / tan(x) + a / (b / c)")); + + Assert.Equal(withoutModel.Output, withDefaultModel.Output); + } + #endregion } }