You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
How much can the integrator's code be simplified? #1486
Rubi's rules themselves are condense; one line is one rule. But in our implementation each rule becomes hundreds of lines. This suggests that some of the matching logic should really be abstractable. Does there exist a design that captures the conciseness of Rubi rules such that the agent's context window won't be filled that quickly?
If the best design is necessarily breaking, queue this for v3
Yes, but not the way the question hopes, and the reason matters for the design. Measured first, then a plan in three steps; the first is not breaking and could start now.
Where the lines go.IndefiniteIntegralSolver.cs is 16,857 lines in one file: 88 rules, a third of it comments. The four rules I wrote last night are 27 to 68 lines of code each, and each of them hit a trap about how the integrand is spelled, not about the mathematics:
(d - c^2 d x^2)^3 arrives as (d^0 (-1)^(-1) c^(-2) + x^2)^3, which the polynomial reader refuses, and simplifies to x^2 - c^(-2) provided not d = 0
In Rubi a rule is one line because Mathematica's pattern matcher and its canonical forms carry that weight: a_. + b_.*x_ matches a missing coefficient, commutativity and associativity are free, FreeQ is a primitive, and every integrand is in one spelling before any rule sees it. Ours re-derive all of that inside each rule. So the honest answer is that a pattern DSL moves most of those lines into one place rather than removing them — which is still worth doing, because then they are written once and tested once, instead of rediscovered one trap per rule.
The design, and what it needs. Rules as data, Rubi-shaped: a pattern with typed captures (Free(x), Integer, Linear(x), Polynomial(x)), a condition over the captures, and an answer template or a rewrite into a smaller question. Most of the machinery exists in pieces: simplification rule sets are data already (MatchedRules.cs), ... is the pattern operator, and #746's tier 1 names pattern matching as data. Three counter-arguments that decide the order:
Cost. Converting Simplify's rule sets to data cost +13% on SimplifyEasy, measured against where it started, and it needed an index before it was acceptable. Rubi has about 7,000 rules; tried in order at every node, that is not a design, it is a timeout. It wants a discrimination tree — the e-matcher is a start.
Rules do not replace the searches. Many rows come out of generic machinery — the substitution search, parts, Rothstein–Trager — that Rubi enumerates rule by rule. A table would sit in front of those, not instead of them, and the order between table and search is itself a defect surface.
The plan I would propose:
Now, not breaking: split IndefiniteIntegralSolver.cs into partial-class files by family — trigonometric, hyperbolic, radicals, logarithms and exponentials, rational, inverse functions, parts. An agent working on acosh reads about 2,000 lines instead of 17,000. This answers the context-window half of the question directly, whatever happens to the other half, and it is a move with no behaviour change, so its review is mechanical.
2.8, design tried before v3: a small internal pattern DSL with typed captures over today's normalisation. Port ten rules that are pure shape-and-template — the imaginary tangent and A cos + i A sin below the bar is written as the exponential it is #1485's A cos + i A sin are the obvious first two — and measure code lines per rule, time per call, and the family pass rates, before and after.
v3: the canonical form, and rules as data at scale, if 2.8's measurement says the cost is acceptable. That part is breaking, because answers change spelling.
Triaged as Maintenance on 2.8, since it is the design work 2.8 exists for; the breaking half would move to 3.0 when it is split out. If you agree with the file split, I will open it as its own PR; it touches no rule.
Agreed, it is a tradeoff, so the pilot should measure both sides of it for every rule it ports: code lines and what an agent has to read to change the rule, against time and allocation per call — the gate, plus a benchmark per rule — and the Rubi family pass rates before and after.
The tradeoff is also not uniform, and I think the result will say so. Every integrand passes through a few rules near the top of the dispatch, and those may not be able to afford an interpreted pattern at all. Rules that fire rarely, deep in the list, can. If that is how it comes out, the design is data for the cold rules and hand-written code for the hot ones, and the measurement is what draws the line between them.
On the file split, I read "before the whole-repo refactor" as putting it in the v3 pass with the rest of the restructuring, so I will leave the file as it is unless you say otherwise.
You can also investigate compile-time machinery to optimize declarative rules. Optimization can be itself encoded as code too, and it should have a positive effect on the simplifier too.
The pilot has one rule ported, #1485's A cos(y) + i A sin(y) → A e^(i y), on the existing matcher (MatchPattern, which already does the associative-commutative part through Gathered). One addition was needed: Scaled, Rubi's a_.*u_, a coefficient that is 1 where the factor stands alone.
The rule is now its pattern, its condition and its result, 12 lines:
The hand-written version is 27 lines plus its share of a 47-line reader. The rule type (45 lines) and Scaled (49) are paid once. Behaviour is identical: the rule's 62 tests, seven probes compared answer for answer, and all of Rubi 4.7.2 at 249 of 290 on both builds, every run.
Speed, measured as you asked. The whole of 4.7.2 is 290 trigonometric integrands, and every one of them passes this rule's place in the dispatch. Three runs each:
runs (ms)
median
hand-written
161,553 · 161,825 · 161,520
161,553
as data
163,324 · 163,365 · 161,481
163,324 (+1.1%)
One rule costs up to about 1% where it fires most, and one of the three runs matched the hand-written version. That is affordable for this rule. It is a cost per rule, though, and 88 of them interpreted would add up to exactly what you warned about. So the measurement agrees with you: an interpreter is not the design, and generating the matcher at compile time is.
On compile-time machinery. The repository already has a generator that settled this trade the other way round: RuleRegistryGenerator (#825) keeps the hand-written switch as what runs and derives addressable rule values from its arms, because running the simplifier's rules through the matcher cost about 5% of Simplify per rule set. What you suggest is the reverse: the rule is written declaratively, and the generator emits the switch. The permutations Gathered searches at run time and the coefficient defaults Scaled tries become arms at build time. That keeps both the 12-line source and the hand-written code's cost, and it is Native-AOT-safe, which compiling expression trees at run time is not. The same generator would serve MatchedRules, which is where the simplifier's +13% came from.
Next, if you agree: a generator for exactly this rule's pattern, Gathered over two Scaled parts, emitting its switch. I would check that its output is the same as the interpreted rule's on 4.7.2, and that its time is the hand-written one's, before it takes a second rule. The pilot branch stays unmerged until then. A +1.1% change is not one I would merge on its own merits.
Will do. Three designs, each on the same rule and measured the way the pilot was — all of 4.7.2, three runs each, output compared answer for answer:
Interpreted: the pilot as it stands, +1.1%.
Generated at build time from the MatchPattern expression itself. One source of truth: a generator reads the pattern's syntax and emits the switch, with Gathered's permutations and Scaled's defaults expanded into arms.
Specialised at start-up. The pattern is compiled once into a tree of closures that do no search where the pattern allows none. It is not build time, but it is Native-AOT-safe and needs no generator to maintain, which counts for something if its cost comes close.
By scalable I mean: the cost per rule stays at the hand-written one however many rules there are, a rule is written once and read once, and whatever a design cannot express is found and written down, not worked around. I will report lines, time, allocation and those limits for each.
The first result changes the order I proposed. Interpreted, with one cheap test in front, costs what the hand-written rule costs.
The whole of the pilot's +1.1% was the matcher starting an enumerator at every node below the bar, when the pattern can only ever match a sum. A test on the node's kind before any enumerator is started removes it. Interleaved runs of all of 4.7.2, three each, answers identical every run:
runs (ms)
median
hand-written
163,684 · 162,210 · 161,601
162,210
as data, with the test
161,334 · 163,900 · 162,185
162,185
A difference of 0.02%, inside the ±0.8% spread of either. The test is derived from the pattern, not written per rule: MatchPattern.CouldMatchRoot. Node<T> matches a T, Gathered<Sumf> a sum or a difference, Scaled its part or a product. No rule has to remember to say it, which is what makes it scale: the cost per rule is a type check at each node, the same one the hand-written rules open with.
So, on this evidence:
Design 1, interpreted plus the derived test, is the baseline. It is at hand-written cost for a rule of this kind, with the rule 12 lines instead of 27 plus its reader.
Design 2, generated at build time, is held in reserve rather than built now. It pays where a pattern searches hard — several Gathered parts over long chains, where the permutations are real work — and this rule is not that. The next ports will say whether any rule is.
Design 3, specialised at start-up, is not needed unless design 1 stops being enough.
Two limits on what this shows. It is time, not allocation: the gate does not exercise Integrate, so memory is still unmeasured, and I will add an allocation count per rule before the next port. And it is one rule. Next I would port the imaginary tangent and the reader that #1484's rotation shares, and measure the same way, so that "scales" rests on three rules rather than on one.
The pilot is on the rules-as-data-pilot branch, not a PR.
Second measurement, with memory this time. Two rules are now data, #1485's A cos + i A sin and the imaginary tangent/cotangent (which needed a rule to hold several cases, one per shape). The Rubi harness now records what each Integrate call allocates.
Rubi 4.7.2 + 4.3.10 + 4.4.10, 312 problems where both rules fire, three interleaved runs each, answers identical every run (269/312):
time (ms)
allocation (MB, 297 problems measured)
hand-written
244,246 · 243,000 · 241,328 → 243,000
205,034 · 205,036 · 205,036 → 205,036
as data
241,617 · 247,062 · 241,962 → 241,962
205,053 · 205,034 · 205,043 → 205,043
Time is inside the noise (−0.4%, the run spread is ±1%). Allocation is +7 MB out of 205 GB, 0.003%.
Code: the tangent rule went from 57 lines to 18 and A cos + i A sin from 27 to 12; the solver loses 98 lines. The machinery is paid once: Scaled and CouldMatchRoot in the matcher, and the rule type, about 100 lines together.
A side finding matters more than the rules for your memory question: those files allocate about 690 MB per problem. The integrator's memory goes into its search, not into how a rule is spelled, so if memory is the concern, that is where it would pay to look.
My proposal, given two rules at no measurable cost:
Open the pilot as a PR: the machinery and these two rules. It is cost-neutral, shorter, and gives the pattern form a place in the code base.
Move other rules over when they are next touched, not as a rewrite. A rule that reads a shape is the candidate. Rules whose work is a search or an algorithm — Rothstein–Trager, the substitution search, parts — are not, and stay as code.
Keep the build-time generator in reserve for a pattern whose matching is itself the cost — several Gathered parts over long chains. None has shown up yet.
If you would rather the pilot port more rules before it becomes a PR, say which kind; the hot rules at the top of the dispatch would be the stronger test of the pre-filter.
It can, but not by being moved in place, which is all the pilot did. A rule moved one-for-one into data runs the same search in the same order, and neutral is the most that can come of that. The gains are in what a rule declared as a pattern lets the dispatcher do that it cannot do with a method, and there are four, cheapest first.
Index the rules by what they need. A pattern says which node kinds it can match at all: this one needs a sum with a cosine and a sine in it. The integrand's node kinds are one pass to collect. Today all 85 dispatch entries are called in order at every level of the search, and each does its own check, some of them expensive (SingleQuotient.Combine, a polynomial read) before declining. With patterns, a rule whose kinds are absent is never called. This is Rubi's advantage too: its rules are found by the head of the integrand, not tried in turn.
Order by specificity instead of by hand. Order has been the defect surface all along: a slow search in front of a rule that would answer instantly is a timeout. A power of a constant multiple of the radicand a logarithm holds is written over it #1487's first version shows the same thing from the other side, where a rule firing too early cost 7 s → 31 s. Patterns can be compared by how much they constrain, so the more specific ones go first without anyone deciding it rule by rule.
Search over rewrites instead of committing to one. The same-question rewrites — a constant out, a spelling changed — are exactly e-graph rewrites. The integrator could saturate an integrand's equivalent forms and ask the closed rules of all of them, instead of betting on one order. That is Goal: Math OS — a ten-year vision for AngouriMath as an open mathematical reasoning platform #746's tier 2 and the e-matcher's purpose; it is also the one with a real memory cost, and those files already allocate about 690 MB a problem.
Only the first is cheap to test now, and it rests on a number I don't have: how much of the integrator's time goes into rules that are called and decline. I'll measure it per rule over the same Rubi files. If declines are a large share, the index is the gain you expected; if they're small, the speed isn't in the dispatch, and the case for data is the code and the checks in 3.
Measured. Every rule call in the dispatcher was timed, and each call's own time (nested calls taken out, so nothing counts twice) was sorted by what the call did.
where the integrator's time goes
1774-problem suite
4.7.2, 4.3.10, 4.4.10
family 1, 6 a file
total
72 s
480 s
208 s
declines under 100 µs, which an index could skip
1.7%
0.3%
0.3%
declines over 100 µs with no sub-question
66%
62%
80%
declines after sub-questions
15%
19%
6%
answers
15%
2%
5%
cut off at the 5 s limit
0%
17%
8%
Most rule calls are cheap declines: 409,000 of the suite's 419,000, 2.9 µs each on average. An index can save a percent or two at most.
The time goes to two rules that do real work and then decline without asking anything:
suite
trig files
family 1
SolveBySubstitution
53%, 8 ms a call
28%, 25 ms
46%, 48 ms
SolveByPartialFractions
4%
28%, 25 ms
26%, 31 ms
Neither can be skipped by node kind. The substitution search applies to almost anything, and partial fractions reads any quotient, trigonometric ones included.
That's where data can help the search: by testing, before the work, what a rule needs, instead of skipping the calls that are cheap already. "A quotient of polynomials in x" is a pattern, and partial fractions currently spends 25 ms per call on trigonometric quotients it can't use. Next I'll find where those milliseconds go in the two rules. A stated precondition there is the first measurable test of your question.
Agreed on both. The evaluator I've drafted reads neither setting. What still reads them is the fallback under it: where double intervals can't tell, HoldsAtSampledPoints calls EvalNumerical as before. In the new design the answer there is the same interval evaluation at more bits. The caller asks for an accuracy, evaluation starts at 53 bits and doubles the precision until the interval is narrow enough or reaches a cap, and the answer is still agree, differ or can't tell. No DowncastingEnabled, no DecimalPrecisionContext.
The type, following item 8's tower and item 10:
It lives in AngouriMath.Numerics, internal until v3, beside the BigInteger rational and decimal item 8 plans.
Interval is an approximate real on its face: two bounds, each a binary float (a BigInteger mantissa and an int exponent), with doubles as the fast path. Arithmetic rounds outward at the precision the operation is asked for. ComplexInterval is two of them.
Exact numbers stay exact, and become intervals only where a function has no exact value.
A value becomes a Number node only where it's stored.
The order I'd take: first the type and its evaluation for the nodes the integrator's checks meet, using the #1364/#1366BigInteger series for exp, ln and the trigonometric functions. Then HoldsAtSampledPoints and the screens moved onto it, with nothing falling back to Evaled. I'll post measurements on #1338.
The first precondition is in, #1493: the substitution search now drops a trigonometric candidate the integrand's quotient can't be a function of, found with two pinned evaluations instead of a simplification. On 4.7.2, 4.3.10 and 4.4.10 the problems the integrator answers take 22% less time (two interleaved pairs), with every verdict and every answer text unchanged. Family 4 is about 10% faster. It's a stated property of the rule ("a function of u agrees wherever u does") tested cheaply before the work, which is what a data rule would let us write down once for every rule.
The first piece is up as #1497: the integrator's sampled checks now run on the new internal evaluator, with no EvalNumerical and no DowncastingEnabled in the comparison, measured over every family. The decimal tier still uses PeterO decimals; binary floats replace it next. Details are on #1338.
Moved to 2.7.0 with its parent #233, because both touch the integrator's code. In 2.7.0, shape-reading rules move to data as they are touched. The file split waits for v3.
Activity
Rafael-SOWNet commentedon Sep 24, 2026
Yes, but not the way the question hopes, and the reason matters for the design. Measured first, then a plan in three steps; the first is not breaking and could start now.
Where the lines go.
IndefiniteIntegralSolver.csis 16,857 lines in one file: 88 rules, a third of it comments. The four rules I wrote last night are 27 to 68 lines of code each, and each of them hit a trap about how the integrand is spelled, not about the mathematics:1 * fis notfas a tree, so the rule "took out" nothing and re-asked the same question at every depth — CI hung for 3½ h(u^2)^(-1)below a substitutiona cos + b sinrotated (#1484)cscnode above the bar hides a sine below itacoshbeside a multiple of its radicand (#1487)(d - c^2 d x^2)^3arrives as(d^0 (-1)^(-1) c^(-2) + x^2)^3, which the polynomial reader refuses, and simplifies tox^2 - c^(-2) provided not d = 0In Rubi a rule is one line because Mathematica's pattern matcher and its canonical forms carry that weight:
a_. + b_.*x_matches a missing coefficient, commutativity and associativity are free,FreeQis a primitive, and every integrand is in one spelling before any rule sees it. Ours re-derive all of that inside each rule. So the honest answer is that a pattern DSL moves most of those lines into one place rather than removing them — which is still worth doing, because then they are written once and tested once, instead of rediscovered one trap per rule.The design, and what it needs. Rules as data, Rubi-shaped: a pattern with typed captures (
Free(x),Integer,Linear(x),Polynomial(x)), a condition over the captures, and an answer template or a rewrite into a smaller question. Most of the machinery exists in pieces: simplification rule sets are data already (MatchedRules.cs),...is the pattern operator, and #746's tier 1 names pattern matching as data. Three counter-arguments that decide the order:SimplifyEasy, measured against where it started, and it needed an index before it was acceptable. Rubi has about 7,000 rules; tried in order at every node, that is not a design, it is a timeout. It wants a discrimination tree — the e-matcher is a start.The plan I would propose:
IndefiniteIntegralSolver.csinto partial-class files by family — trigonometric, hyperbolic, radicals, logarithms and exponentials, rational, inverse functions, parts. An agent working onacoshreads about 2,000 lines instead of 17,000. This answers the context-window half of the question directly, whatever happens to the other half, and it is a move with no behaviour change, so its review is mechanical.A cos + i A sinare the obvious first two — and measure code lines per rule, time per call, and the family pass rates, before and after.Triaged as Maintenance on 2.8, since it is the design work 2.8 exists for; the breaking half would move to 3.0 when it is split out. If you agree with the file split, I will open it as its own PR; it touches no rule.
🤖 Generated with Claude Code
Happypig375 commentedon Sep 24, 2026
Yes you can pilot this design before whole-repo refactor
But this might still be a tradeoff between speed/memory and code size / agent context filling
Happypig375 commentedon Sep 24, 2026
If the speed and memory worsen, this might not be worthwhile after all.
Rafael-SOWNet commentedon Sep 24, 2026
Agreed, it is a tradeoff, so the pilot should measure both sides of it for every rule it ports: code lines and what an agent has to read to change the rule, against time and allocation per call — the gate, plus a benchmark per rule — and the Rubi family pass rates before and after.
The tradeoff is also not uniform, and I think the result will say so. Every integrand passes through a few rules near the top of the dispatch, and those may not be able to afford an interpreted pattern at all. Rules that fire rarely, deep in the list, can. If that is how it comes out, the design is data for the cold rules and hand-written code for the hot ones, and the measurement is what draws the line between them.
On the file split, I read "before the whole-repo refactor" as putting it in the v3 pass with the rest of the restructuring, so I will leave the file as it is unless you say otherwise.
🤖 Generated with Claude Code
Happypig375 commentedon Sep 24, 2026
You can also investigate compile-time machinery to optimize declarative rules. Optimization can be itself encoded as code too, and it should have a positive effect on the simplifier too.
Rafael-SOWNet commentedon Sep 24, 2026
The pilot has one rule ported, #1485's
A cos(y) + i A sin(y)→A e^(i y), on the existing matcher (MatchPattern, which already does the associative-commutative part throughGathered). One addition was needed:Scaled, Rubi'sa_.*u_, a coefficient that is1where the factor stands alone.The rule is now its pattern, its condition and its result, 12 lines:
The hand-written version is 27 lines plus its share of a 47-line reader. The rule type (45 lines) and
Scaled(49) are paid once. Behaviour is identical: the rule's 62 tests, seven probes compared answer for answer, and all of Rubi 4.7.2 at 249 of 290 on both builds, every run.Speed, measured as you asked. The whole of 4.7.2 is 290 trigonometric integrands, and every one of them passes this rule's place in the dispatch. Three runs each:
One rule costs up to about 1% where it fires most, and one of the three runs matched the hand-written version. That is affordable for this rule. It is a cost per rule, though, and 88 of them interpreted would add up to exactly what you warned about. So the measurement agrees with you: an interpreter is not the design, and generating the matcher at compile time is.
On compile-time machinery. The repository already has a generator that settled this trade the other way round:
RuleRegistryGenerator(#825) keeps the hand-writtenswitchas what runs and derives addressable rule values from its arms, because running the simplifier's rules through the matcher cost about 5% ofSimplifyper rule set. What you suggest is the reverse: the rule is written declaratively, and the generator emits theswitch. The permutationsGatheredsearches at run time and the coefficient defaultsScaledtries become arms at build time. That keeps both the 12-line source and the hand-written code's cost, and it is Native-AOT-safe, which compiling expression trees at run time is not. The same generator would serveMatchedRules, which is where the simplifier's +13% came from.Next, if you agree: a generator for exactly this rule's pattern,
Gatheredover twoScaledparts, emitting itsswitch. I would check that its output is the same as the interpreted rule's on 4.7.2, and that its time is the hand-written one's, before it takes a second rule. The pilot branch stays unmerged until then. A +1.1% change is not one I would merge on its own merits.🤖 Generated with Claude Code
Happypig375 commentedon Sep 24, 2026
Sure, try some designs and see if each one is scalable
Rafael-SOWNet commentedon Sep 24, 2026
Will do. Three designs, each on the same rule and measured the way the pilot was — all of 4.7.2, three runs each, output compared answer for answer:
MatchPatternexpression itself. One source of truth: a generator reads the pattern's syntax and emits theswitch, withGathered's permutations andScaled's defaults expanded into arms.By scalable I mean: the cost per rule stays at the hand-written one however many rules there are, a rule is written once and read once, and whatever a design cannot express is found and written down, not worked around. I will report lines, time, allocation and those limits for each.
🤖 Generated with Claude Code
Rafael-SOWNet commentedon Sep 24, 2026
The first result changes the order I proposed. Interpreted, with one cheap test in front, costs what the hand-written rule costs.
The whole of the pilot's +1.1% was the matcher starting an enumerator at every node below the bar, when the pattern can only ever match a sum. A test on the node's kind before any enumerator is started removes it. Interleaved runs of all of 4.7.2, three each, answers identical every run:
A difference of 0.02%, inside the ±0.8% spread of either. The test is derived from the pattern, not written per rule:
MatchPattern.CouldMatchRoot.Node<T>matches aT,Gathered<Sumf>a sum or a difference,Scaledits part or a product. No rule has to remember to say it, which is what makes it scale: the cost per rule is a type check at each node, the same one the hand-written rules open with.So, on this evidence:
Gatheredparts over long chains, where the permutations are real work — and this rule is not that. The next ports will say whether any rule is.Two limits on what this shows. It is time, not allocation: the gate does not exercise
Integrate, so memory is still unmeasured, and I will add an allocation count per rule before the next port. And it is one rule. Next I would port the imaginary tangent and the reader that #1484's rotation shares, and measure the same way, so that "scales" rests on three rules rather than on one.The pilot is on the
rules-as-data-pilotbranch, not a PR.🤖 Generated with Claude Code
Rafael-SOWNet commentedon Sep 24, 2026
Second measurement, with memory this time. Two rules are now data, #1485's
A cos + i A sinand the imaginary tangent/cotangent (which needed a rule to hold several cases, one per shape). The Rubi harness now records what eachIntegratecall allocates.Rubi 4.7.2 + 4.3.10 + 4.4.10, 312 problems where both rules fire, three interleaved runs each, answers identical every run (269/312):
Time is inside the noise (−0.4%, the run spread is ±1%). Allocation is +7 MB out of 205 GB, 0.003%.
Code: the tangent rule went from 57 lines to 18 and
A cos + i A sinfrom 27 to 12; the solver loses 98 lines. The machinery is paid once:ScaledandCouldMatchRootin the matcher, and the rule type, about 100 lines together.A side finding matters more than the rules for your memory question: those files allocate about 690 MB per problem. The integrator's memory goes into its search, not into how a rule is spelled, so if memory is the concern, that is where it would pay to look.
My proposal, given two rules at no measurable cost:
Gatheredparts over long chains. None has shown up yet.If you would rather the pilot port more rules before it becomes a PR, say which kind; the hot rules at the top of the dispatch would be the stronger test of the pre-filter.
🤖 Generated with Claude Code
Happypig375 commentedon Sep 24, 2026
Can writing the rules in this way help the search somehow? I thought that there would be more than neutral here.
Rafael-SOWNet commentedon Sep 24, 2026
It can, but not by being moved in place, which is all the pilot did. A rule moved one-for-one into data runs the same search in the same order, and neutral is the most that can come of that. The gains are in what a rule declared as a pattern lets the dispatcher do that it cannot do with a method, and there are four, cheapest first.
SingleQuotient.Combine, a polynomial read) before declining. With patterns, a rule whose kinds are absent is never called. This is Rubi's advantage too: its rules are found by the head of the integrand, not tried in turn.Only the first is cheap to test now, and it rests on a number I don't have: how much of the integrator's time goes into rules that are called and decline. I'll measure it per rule over the same Rubi files. If declines are a large share, the index is the gain you expected; if they're small, the speed isn't in the dispatch, and the case for data is the code and the checks in 3.
🤖 Generated with Claude Code
Rafael-SOWNet commentedon Sep 24, 2026
Measured. Every rule call in the dispatcher was timed, and each call's own time (nested calls taken out, so nothing counts twice) was sorted by what the call did.
Most rule calls are cheap declines: 409,000 of the suite's 419,000, 2.9 µs each on average. An index can save a percent or two at most.
The time goes to two rules that do real work and then decline without asking anything:
SolveBySubstitutionSolveByPartialFractionsNeither can be skipped by node kind. The substitution search applies to almost anything, and partial fractions reads any quotient, trigonometric ones included.
That's where data can help the search: by testing, before the work, what a rule needs, instead of skipping the calls that are cheap already. "A quotient of polynomials in x" is a pattern, and partial fractions currently spends 25 ms per call on trigonometric quotients it can't use. Next I'll find where those milliseconds go in the two rules. A stated precondition there is the first measurable test of your question.
4 remaining items
Rafael-SOWNet commentedon Sep 26, 2026
Agreed on both. The evaluator I've drafted reads neither setting. What still reads them is the fallback under it: where double intervals can't tell,
HoldsAtSampledPointscallsEvalNumericalas before. In the new design the answer there is the same interval evaluation at more bits. The caller asks for an accuracy, evaluation starts at 53 bits and doubles the precision until the interval is narrow enough or reaches a cap, and the answer is still agree, differ or can't tell. NoDowncastingEnabled, noDecimalPrecisionContext.The type, following item 8's tower and item 10:
AngouriMath.Numerics, internal until v3, beside theBigIntegerrational and decimal item 8 plans.Intervalis an approximate real on its face: two bounds, each a binary float (aBigIntegermantissa and anintexponent), with doubles as the fast path. Arithmetic rounds outward at the precision the operation is asked for.ComplexIntervalis two of them.Numbernode only where it's stored.The order I'd take: first the type and its evaluation for the nodes the integrator's checks meet, using the #1364/#1366
BigIntegerseries for exp, ln and the trigonometric functions. ThenHoldsAtSampledPointsand the screens moved onto it, with nothing falling back toEvaled. I'll post measurements on #1338.Rafael-SOWNet commentedon Sep 26, 2026
The first precondition is in, #1493: the substitution search now drops a trigonometric candidate the integrand's quotient can't be a function of, found with two pinned evaluations instead of a simplification. On 4.7.2, 4.3.10 and 4.4.10 the problems the integrator answers take 22% less time (two interleaved pairs), with every verdict and every answer text unchanged. Family 4 is about 10% faster. It's a stated property of the rule ("a function of
uagrees whereverudoes") tested cheaply before the work, which is what a data rule would let us write down once for every rule.Rafael-SOWNet commentedon Sep 27, 2026
The first piece is up as #1497: the integrator's sampled checks now run on the new internal evaluator, with no
EvalNumericaland noDowncastingEnabledin the comparison, measured over every family. The decimal tier still uses PeterO decimals; binary floats replace it next. Details are on #1338.The integrator's sampled-point checks evaluate in intervals, which re…
Rafael-SOWNet commentedon Oct 19, 2026
Moved to 2.7.0 with its parent #233, because both touch the integrator's code. In 2.7.0, shape-reading rules move to data as they are touched. The file split waits for v3.
🤖 Generated with Claude Code
https://claude.ai/code/session_012sonx8iAspMiwRwokT1Ura
Rafael-SOWNet commentedon Oct 19, 2026
Back on 2.6.0 with its parent #233, while #233's milestone is asked about there.
🤖 Generated with Claude Code
https://claude.ai/code/session_012sonx8iAspMiwRwokT1Ura