From e8d37e26e20e3ee02d37442d9776fc2882f3edca Mon Sep 17 00:00:00 2001 From: Rafael Vuijk Date: Thu, 1 Oct 2026 18:32:22 +0000 Subject: [PATCH] What's new: 2.2.0 to 2.5.0, and the quickstart names 2.5.0 The page stopped at 2.1.0, and the library has released four versions since. Each entry is its release's notes, converted from the published Markdown -- links absolute, issue numbers linked -- so the site says what the release said. 2.5.0 stays open and the rest are collapsed, as before. The quickstart named 2.1.0 and called every 2.x a drop-in replacement. The assembly version is pinned at 2.0.0.0, so 2.5.0 binds wherever a 2.x did, but 2.2.0 renamed Transformation.Rationalisation and RewriteRules.RationaliseDenominator, and code calling either throws MissingMethodException until it is rebuilt. Built with dotnet fsi amsite.fsx build: exit 0, and the generated whatsnew and quickstart pages carry the new text. Part of asc-community/AngouriMath#1649. Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_012sonx8iAspMiwRwokT1Ura --- src/content/quickstart/index.html | 8 +- src/content/whatsnew/index.html | 209 +++++++++++++++++++++++++++++- 2 files changed, 213 insertions(+), 4 deletions(-) diff --git a/src/content/quickstart/index.html b/src/content/quickstart/index.html index 20319332f..918083dd1 100644 --- a/src/content/quickstart/index.html +++ b/src/content/quickstart/index.html @@ -39,12 +39,14 @@

Install for F#

dotnet add package AngouriMath.FSharp

- The current release is 2.1.0, and both it and 2.0.0 change answers that earlier versions + The current release is 2.5.0, and every 2.x release changes answers that earlier versions got wrong. Whichever version you are coming from, read BREAKING-CHANGES.md first: it lists every input whose result is now different, with the old value and the new one, - under a heading per release. 2.1.0 is a drop-in replacement for 2.0.0 — the assembly version is - pinned at 2.0.0.0 for the whole of 2.x. + under a heading per release. The assembly version is pinned at 2.0.0.0 for the whole + of 2.x, so 2.5.0 binds wherever a 2.x release did. One rename needs a recompile: 2.2.0 spelled + Transformation.Rationalisation and RewriteRules.RationaliseDenominator + with a z, and code calling either throws MissingMethodException until it is rebuilt.

The library targets netstandard2.0, net8.0 and net10.0. diff --git a/src/content/whatsnew/index.html b/src/content/whatsnew/index.html index 5ef8f61fe..59632be91 100644 --- a/src/content/whatsnew/index.html +++ b/src/content/whatsnew/index.html @@ -34,7 +34,214 @@

What's new

--> -
2.1.0 +
2.5.0 +

+ Tier 2 of the Math OS roadmap — the rewrite graph — is finished in this release. An e-graph now lives in the kernel rather than in a measurement harness, every one of the thirty registered rule sets runs as data through a real e-matcher, and each rule declares its own soundness tier and how much it grows what it matches. The honest result of that work is the part worth reading: Simplify still does not run on the graph, because the tier asked for equality saturation to be evaluated against memory cost on real expressions and the evaluation came back negative. A measurement that says "not by default" is the deliverable, not a substitute for it. +

+ Every changed answer is in BREAKING-CHANGES.md under 2.5.0 — since 2.4.0, with the old value, the new one and why, each measured on a build of both sides. Read that first if you have code depending on an answer. Seventy-six entries at a glance and thirty-seven worked sections; twenty-nine are marked Silent — the call still succeeds and quietly returns something else — and two are Loud, where input that parsed now raises. +

+ AssemblyVersion stays 2.0.0.0. The recorded public surface has 136 additions and no removals, so this is source- and binary-compatible: nothing that compiled against 2.4.0 stops compiling, and nothing bound against it stops binding. +

+ Wrong answers fixed:
+

    +
  • Integrate could recurse without bound and take the process down. A stack overflow is not an exception a caller can handle — the process aborts. Caught by running Rubi's independent test suites against the release candidate, where the run died with SIGABRT; v2.4.0 on the same corpus and the same machine ran past that point, so this was introduced in this release rather than a standing limitation. Two changes: the descent is bounded (#1233), and an integral already being worked out is recognised as a cycle and declined at once (#1234). The memo added in #1157 could not have caught it, and neither could a set of visited shapes — SolveBySubstitution names its variable with Variable.CreateUnique, so every level is alpha-equivalent to the one before rather than equal to it, and the cycle guard has to rename before it compares (#1232).
  • +
  • Matrix operations answered wrongly on more than one thread. MathS.Multithreading documents concurrent use as supported. Forty 4×4 symbolic matrices, computed once sequentially and then rebuilt and recomputed in parallel: 38 of 40 determinants, 18 of 40 inverses and 11 of 40 adjugates came back carrying another computation's values, and the elementwise operators threw on a corrupted dictionary. Two of the three causes were AngouriMath's own caches, grown under a lock and read outside one (#1227, #1229). The third is not ours — GenericTensor 1.0.4 hands every caller the same scratch matrix for a given size and then writes into it, reported as GenericTensor#40 with a fix offered in #41, and guarded here meanwhile (#1230).
  • +
  • A negation was answered with the empty set. The empty set is a positive claim, so saying it for something never settled is a wrong answer rather than a missing one (#1127, #1131).
  • +
  • Domain.Any was treated as the universal set when it is a codomain, so membership questions got an answer that was not asked (#996, #1126).
  • +
  • A negative arccotan was not negative. This library's arccotan has range (-π/2, π/2], not the textbook (0, π), and a guard written from the textbook shipped wrong (#1113).
  • +
  • The limit of z / |z| lost the phase of z, coming back with a real-line answer for something that approaches a different value along every ray (#1208).
  • +
  • A factor of −1 was taken out as though it were a factor rather than read as a sign (#1172).
  • +
  • A saturation check compared two things that have no value, so it passed on inputs it could not have decided (#1166).
  • +
+ The rewrite graph:
+ The e-graph moved out of the harness and into the kernel behind an explicit opt-in (#1101), and what matches against it is a genuine e-matcher — a pattern against an e-class, not against a term (#1103). A cost model is a record taken as a parameter rather than read from an ambient setting (#1102), so SmallestTree, FewestDivisions and FewestRadicals are things you pass rather than things you set. Canonicalisation runs over the graph (#1105), rule priorities are computed from the patterns instead of typed by hand (#1106), and all thirty registered sets run as data (#1192). +

+ The parts that make that safe are tests rather than assertions by an author: a rule's declared growth is checked against a corpus (#1158), a set that rewrites back to where it started is caught (#1165), and so is one that never settles over a whole tree (#1168). The runaway that used to be blamed on a whole soundness tier turned out to be one inverse pair across two sets (#1193), and the growth ceiling turned out to already be the scheduling policy the documents kept deferring (#1194, #1203). +

+ Speed:
+ Five changes, each measured at the entry point rather than in a microbenchmark, and each answer-identical: +

+ + + + + + + +
SolveHard−98.6% allocation — the solver now tries the equation as written before asking for its alternatives (#1209)
SimplifyHard−64% — a candidate already registered is not registered again (#1207)
SimplifyHard−44% — a candidate is registered once and a subtree costed once (#1205)
SimplifyHard−35% — the sort key of a node is spelt once (#1210)
SimplifyHard−25% — a candidate is re-simplified at the default level whatever level it was offered at (#1211)
+ Extraction from the graph became a fixed point computed from the leaves up rather than a top-down walk whose cycle guard defeated its own memo: 1,918 ms to 4 ms on the case that exposed it, and a budget now honoured to the millisecond (#1202). +

+ New notation:
+
    +
  • a divides b is a statement node, printing as a \mid b in LaTeX (#1220). divides is now a keyword, so a variable of that name no longer parses — one of the two Loud entries.
  • +
  • #S is the number of elements of a set, with card(S) still accepted on input (#1221).
  • +
  • The extremum of an expression over a set, as a binder (#1222).
  • +
  • A lambda is written with an arrow as well as a call: x => x + 1 beside lambda(x, x + 1). Nothing that parsed before parses differently — = followed by > was not a token (#1151).
  • +
+ Closed forms:
+ A summation with a closed form now has one, and each carries the condition it holds under rather than asserting it unconditionally: +

+
sum(k, k, 1, n)              →  (n + n^2)/2, with the condition on n
+sum(x^k, k, 0, +oo)          →  1 / (1 - x) provided abs(x) < 1
+sum(2^k, k, 0, +oo)          →  +oo, by the nth-term test, where it used to be left as written
+ Polynomial summands (#1152), monomial products (#1153), a power over a factorial (#1213), binomial sums with a power or trigonometric weight (#1214), geometric series (#1218) and divergent ones (#1224). +

+ Integration reached further too: a biquadratic denominator decomposed over the reals (#1145), fractional powers and the tangent as substitutions (#1146), improper quotients divided out before decomposition (#1147), repeated quadratic denominators (#1155), and an integral across a jump split at the jumps rather than taken through an antiderivative that treats a floor as a constant (#1215). There is also an analytical solver for first-order linear ODEs (#1137). +

+ Integration speed, and a correction to an earlier version of these notes:
+ An earlier version of this page said the integrator was left much slower than 2.4.0 on hard integrands. That was wrong, and it was my measurement error: the figures behind it came from a corpus run that had the full unit suite running beside it on the same machine, which inflated the sections being compared. Measured properly — the seventeen hardest integrands from that part of the corpus, one process, nothing else running, two runs of each arm — 2.5.0 is about 30% faster: 72,017 and 71,390 ms for 2.4.0 against 53,159 and 47,996 ms here. Several individual integrands halve. The details are on #1232, which is closed. +

+ The honest caveat that survives the correction: partial fractions over repeated factors with high powers is slow in both versions — 1/((2+x)^3*(3+x)^4) takes about half a minute either way. That is a standing limitation rather than anything new. +

+ No Rubi coverage figure is claimed for 2.5.0, because that run was never completed on a quiet machine. The last complete one is 2.4.0's, taken the same day: 599 of 1774, 0 wrong. +

+ Documentation that is checked:
+ 254 worked examples in the XML documentation say what they print, and nothing checked any of them; 67 were wrong — 56 printed something else and 11 did not compile or threw. They are compiled and run against what they claim now (#1184), so a changed answer fails a test instead of leaving a wrong promise standing in the docs a user reads. +

+
+ +
2.4.0 +

+ Tier 1 of the Math OS roadmap is finished, and the thing that finished it kept finding wrong answers. Writing a rewrite rule out as data — a pattern and a replacement, rather than an arm of a switch — makes the correspondence between the two something you have to state. Four times this cycle, a rule did not survive stating it. +

+ Every changed answer is in BREAKING-CHANGES.md under 2.4.0 — since 2.3.0, with the old value, the new one and why, each measured on a build of both sides. Read that first if you have code depending on an answer. Twenty-seven entries, and more than half are marked Silent — the call still succeeds and quietly returns something else. +

+ AssemblyVersion stays 2.0.0.0. The recorded public surface shows 68 additions and 130 removals, and none of the removals is one a caller can hit: Stringize() and Latexize() stopped being abstract on Entity and each node's override became a private helper, so the member is still public on Entity and inherited by every node. Source- and binary-compatible. +

+ Wrong answers fixed:
+

    +
  • A factorisation that was not equal to what it factored. Factor("4x² − 4y²", "x") returned (x + y)(x − y) — the 4 simply gone, the difference from the input −3x² + 3y². Every candidate in the polynomial layer is checked by exact division, but that check is on the individual factors; nothing compared the assembled product against the input, so a constant lost during assembly was lost silently. (#1092)
  • +
  • (y < x) or (x = y) simplified to x <= y — its own negation off the diagonal. Four of the eight or-with-equality rules carried their neighbour's comparison. Only reachable with both operands symbolic: with a number on one side, 2 < x is rewritten to x > 2 earlier in the same pass and one of the four correct rules matches. A test written with a numeric operand would have passed on the defect. (#1077)
  • +
  • x! = 0 was answered False everywhere, including at the negative integers where the factorial has a pole and the statement is NaN. The rule read a property pattern on the factorial's argument rather than on the factorial. It took x! / x! → 1 with it, and three recorded test verdicts had been recording that. (#1081)
  • +
  • ln(1/b) = −ln(b) was applied unconditionally, which is false on the negative reals — at b = −0.63 the two differ by the full turn of the argument the principal branch discards. Three rules gained the guard their neighbours ten lines below already had. (#1062)
  • +
+ Factorisation:
+ Factorize was built entirely out of rewrite rules, so it factored what someone had written a rule for and handed everything else back whole — while square-free decomposition, Zassenhaus over ℚ and the multivariate GCD sat in the tree unused by it. +

+
"x3 - 1".Factorize()          x ^ 3 - 1              →  (x - 1) * (x ^ 2 + x + 1)
+"x4 - 5x2 + 4".Factorize()    unchanged              →  (x + 1) * (x + 2) * (x - 2) * (x - 1)
+"x2 + 2x + 1".Factorize()     unchanged              →  (x + 1) ^ 2
+ The layer speaks only where the rules said nothing, so every answer they already gave is unchanged — the order two factors come out in is arbitrary and theirs is the one on record. (#1018) +

+ And the layer itself reaches further. Hensel lifting along an evaluation homomorphism (#1088, #1089) factors bivariate polynomials that Kronecker's substitution refuses — not because its image is too large but because it over-factors: x⁷ − y⁷ maps to t⁷(1 − t⁴⁹), whose factors are cyclotomic. An evaluation image inflates nothing. +

+
Factor("x12 - y12", "x")      null                   →  six factors, the full cyclotomic split
+Factor("x7 - y7",   "x")      null                   →  (x - y)(x⁶ + x⁵y + ⋯ + y⁶)
+ Where the substitution gives up entirely, an evaluation image can still prove a polynomial irreducible — which since #1059 is an answer rather than a refusal, so Factor("x2 + y2 + z2 + w2 + 1", "x") returns the polynomial instead of null. (#1087) +

+ The rule sets are data:
+ Thirty of thirty rule sets now have a form in which each rule is a value — a pattern, a replacement, a soundness tier and a direction — proven to agree with the switch it replaces over thousands of generated expressions. Twenty-seven run it. The three that do not are the canonical orders, and that is a measurement rather than an omission. +

+ It buys: every rule individually addressable (407 entries), per-rule soundness where only the set had a tier before (181 Sound, 141 SoundUnderAssumptions), and 26 rules that can be read backwards. +

+ Performance, honestly:
+ SimplifyEasy is about 13% slower than before the exchange — 82,676 ns to 93,732 ns on one desktop, both arms, standard deviation under half a per cent. Every conversion was measured against the commit in front of it and every one came back free or better; nothing was measured against the start, and the sum of a run of free steps is +13%. +

+ It would have been worse. Indexing each set's rules by node type recovered most of it, and a bounded pattern is now walked by index rather than enumerated — 165.05 MB to 163.40 MB of SolveMediumHard on its own. +

+ The work shape is unchanged: 4,914 rule-set invocations on that input at both ends, so this is per-operation overhead rather than extra work. version_performance_control.md records where it goes, what three attempts to remove it measured, and the roughly 40% still unattributed. +

+ That file also gained a measurement worth more than the column: the Kernel Benchmark run twice on one commit is 30–57% apart on every benchmark, while allocation over the same pair agrees to 0.03%. Timing comparisons between its columns are evidence only above about 50%. +

+
+ +
2.3.0 +

+ Correctness release, and the first one whose claim to that is measured on an outside corpus rather than only on our own tests. Against Rubi's integration suite — 1774 problems that each carry an antiderivative known to exist — this version answers 604 where 2.2.0-era master answered 536, and gets 0 wrong where that answered 7. Six of those seven were NaN: a definite claim that no value exists, made about integrals that have one. +

+ Every changed answer is in BREAKING-CHANGES.md under 2.3.0 — since 2.2.0, with the old value, the new one and why, each measured on a build of both sides. Read that first if you have code depending on an answer. +

+ AssemblyVersion stays 2.0.0.0. This release removes and renames nothing — 103 additions and 0 removals against the 2.2.0 public API baseline — so it is a drop-in replacement in both the binding and the source sense, which 2.2.0 was not. +

+ Wrong answers fixed:
+

    +
  • The symbolic determinant was NaN for ordinary matrices. Gaussian elimination left the pivots as literal divisions, so the expression was undefined wherever a pivot vanishes. Two of four ordinary 3×3 matrices came back NaN, one of them the singular example every linear-algebra course opens with. It is Laplace expansion now, which never divides — and is faster, dramatically so on numeric matrices. (#992)
  • +
  • A matrix was a member of every special set at once — BB, ZZ, QQ, RR, CC and their intersections — because membership was answered with a guard that is deliberately permissive about what it has not ruled out. Asking "might this be a member" and reporting it as "is a member" are different questions. (#995)
  • +
  • Differentiating over pi or e behaved as though they varied. sin(pi) differentiated by MathS.pi was -1, which is cos(pi): the chain rule run over a symbol that cannot change. It is 0. (#993)
  • +
  • Differentiate(x, n) returned raw chain-rule output for n >= 1, and because each pass differentiated the unsimplified result of the last, the expression compounded — x^4 three times was a screenful where differentiating three times by hand is 2 * x * 3 * 4. Same value, and only one of them is an answer. (#1002)
  • +
  • A bound pi or e still carried the constant's value, so a binder could not bind them and derivative(e^2, e) was 0. A name a binder declares is a variable, whatever it is spelled. (#984)
  • +
  • Differentiating or integrating over something that cannot vary — a number in the variable position — renamed it and answered the derivative of a question nobody asked. (#964)
  • +
+ Interoperability:
+
    +
  • ToSympyCode emitted Python that does not run, for every set, every lambda, every piecewise and every non-vector matrix: unqualified FiniteSet, Interval, Union and S against a preamble of only import sympy; a lambda with no body; a set builder that threw out of the exporter; and Interval, Piecewise and Matrix writing their children as this library spells them rather than as SymPy does. Measured by executing the generated program rather than reading it: 24 of 45 ran before, 45 of 45 run now. (#985)
  • +
+ Names and reporting:
+
    +
  • A set builder leaked its internal placeholder. "{ k : k > 0 }".FreeVariables was { %1 } — a name in no expression, not typeable, and different for a different predicate. It also broke Vars' own promise in both directions at once: the name that occurs was missing and the one that does not was there. (#989)
  • +
  • Stringize of a Rational read back as a Divf, so the round trip was not an identity. Fixed by the parser change below rather than by anything aimed at it. (#873)
  • +
  • A quotient of two integer literals now parses as the Rational it denotes rather than as a division.
  • +
+ Under the hood:
+ The rule registry grew to cover every addressable rule set, with confluence and termination checked by tooling rather than asserted — #746 tier 2 asks for exactly that. Core.Binding makes binder resolution happen at construction, which is what let i, pi and e become bindable names. +

+ Performance:
+ The 1769th column of version_performance_control.md, taken on the same runner class as the 1620th and 1671st so the three are comparable. Every row faster or flat. SolveEasy is 21.3 ms to 8.5 ms — a 2.5× speedup with a 2.3× allocation drop beside it, which is how you tell work from machine noise. +

+ What this release is not:
+ It does not complete a #746 tier. It advances tier 1 and tier 2 and finishes neither, which is why it is a minor version. The rewrite graph that v2.0 is reserved for is still ahead. +

+
+ +
2.2.0 +

+ Infrastructure release. 2.1.0 was mostly wrong answers becoming right ones; this one is mostly the layer underneath them — a real polynomial layer, and a written specification of what canonical form means here with both halves implemented. The corrected answers in it are largely consequences of those two rather than separate fixes. +

+ Every changed answer is in BREAKING-CHANGES.md under 2.2.0 — since 2.1.0, with the old value, the new one, and why, measured on a build of each version. Read that first if you have code depending on an answer. +

+ AssemblyVersion stays 2.0.0.0. See the rename note at the bottom before dropping the DLL in without recompiling. +

+ A polynomial layer:
+ The single item that #746 names as unblocking a large fraction of the tracker. Multivariate GCD, resultants, square-free decomposition, and factorisation over ℚ and finite fields. (#918, #920/#923, #921/#927) +

+ It is not shipped for its own sake — three things in this release are consequences of having it: +

+

    +
  • A polynomial equation that factors is solved through its factors. x^5 + 2x^3 - 2x^2 - 4 returned three of its five roots, one of them a float; it returns all five, exact. x^4 + x^2 + 1 loses a nested radical. (#918)
  • +
  • A rational function is decomposed over the factors of its denominator, not only its roots, so 1/(x^4 + 3x^2 + 2) and 1/(x^4 + 4) now integrate instead of coming back unevaluated. (#926)
  • +
  • A resultant is bounded by measured work rather than by a reasoned size limit, which raised the ceiling from a Sylvester matrix of 24 to one of 40 without risking the blow-up the old bound was guessing at. (#921/#927)
  • +
+ Canonical form: specified, measured, and offered:
+ Docs/Contributing/CanonicalForm.md states the position rather than leaving it to be inferred: canonical is about identity, simplest is about presentation, and there is no canonical form for the whole language — zero-equivalence is undecidable once pi, exp, the trigonometric functions and abs are in play (Richardson 1968). So the specification is a canonical form on a decidable sublanguage, a normalisation elsewhere that must not be mistaken for one, and a search that is not required to be canonical at all. (#928) +

+ Both halves are now reachable from Entity, beside Simplify and Factorize: +

+
Entity Canonicalize()                     // the commutative structure: 0 idempotence and
+                                          // 0 order-independence failures over 834 expressions
+Entity? CanonicalizeAsRationalFunction()  // rational functions over Q -- or null, which is the
+                                          // library saying it has no answer rather than guessing
+ x/x canonicalises to 1 provided not x = 0 and is deliberately not equal to the canonical form of 1: the quotient is undefined where the polynomial is not. (#933, #935, #940) +

+ Nothing applies either by default. Simplify and InnerSimplified return exactly what they returned before. Turning canonical ordering on by default would change every commutative operand order in every printed answer, and that is a decision for a release that says so. +

+ Wrong answers fixed:
+
    +
  • sin(-x) + sin(x) was left as written and is 0 — the parity identities were not being applied. cos(-x) and abs(-x) fold too. (#929/#931)
  • +
  • InnerSimplified is idempotent again. An exact trigonometric value reached through a half turn came back as -(-1) where it should have been 1. The value was never wrong, but applying InnerSimplified twice gave a different tree from applying it once, and much of the library treats what it returns as settled. (#930/#932)
  • +
  • A negative pair keeps its sign. (#936/#937)
  • +
  • The logarithm's domain follows the reading, as every other node's already did, so log(-3, -3) is no longer declared undefined while evaluating to 1. (#721/#890, #916)
  • +
  • log(x, x) was 1 provided x > 0 — NaN at every negative x, and 1 at x = 1 where the logarithm is NaN. (#916)
  • +
  • d/dx x^n carried provided x > 0, a condition it never needed, making the derivative undefined at every negative x. (#916)
  • +
  • A sum of logarithms is no longer gathered unless that is exact — ln(a) + ln(b) -> ln(a*b) is wrong by 2*pi*i off the positive reals. Two limits lost to that guard in 2.1.0 are back, via an ambient scope rather than a new pass. (#922, #925)
  • +
+ Under the hood:
+
    +
  • A rewrite rule's left-hand side can be data. MatchPattern matches by enumerating solutions, so commutative operands backtrack properly, and three rule sets are expressed as data and proved equivalent to the switch they mirror. Internal for now — it is a prerequisite for #746's rewrite graph, and for a rule being able to carry its own justification. (#248, #938)
  • +
  • A measured performance pair is published in Docs/WhatsNew/version_performance_control.md, both columns re-measured on one machine. It caught four solver benchmarks 7–21% slower with allocation up 10–20%, isolated to the polynomial layer by bisecting on allocation. That is the price of the factoring solver, recorded rather than quietly absorbed — and the same document notes that none of the ten solver benchmarks factors, so the suite measures that change's cost and none of its benefit.
  • +
+ One rename:
+ Five members were spelled -ise on a surface that is otherwise Factorize, Latexize, Normalization. Three had not shipped; two had: +

+ + + + +
Was (2.1.0)Is
Transformation.RationalisationTransformation.Rationalization
RewriteRules.RationaliseDenominatorRewriteRules.RationalizeDenominator
+ Recompiling turns a stale reference into a compile error. Swapping the DLL without recompiling does not — AssemblyVersion is pinned at 2.0.0.0 so the assembly still binds, and the call throws MissingMethodException when it is reached. Documentation prose keeps British spelling; the convention is about identifiers. (#940) +

+
+ +
2.1.0

A correctness release. Almost everything below is a wrong answer becoming a right one, and most of it was found by harnesses rather than reported — boundary points where a rule's assumption fails,