Solving a system is bounded whichever internal path takes it (#896) - #1130
Conversation
The triangularising path bounded itself and handed what it declined to an elimination in radicals with no budget at all, so the same `Solve` was bounded or unbounded depending on which internal path accepted it. The whole call draws on one budget now, and where it runs out the caller is told, with both paths named and both ways to ask for more. **The measurement the issue said nobody had taken.** Its worry was that bounding the fall-through would cost the systems the fast path declines for reasons that have nothing to do with difficulty -- five uncoupled quartics with 1024 solutions, above the quotient-dimension cap, which the elimination answers cheaply. Measured: the Gröbner path declines that one in **8 ms**. So the whole-call bound leaves the elimination essentially all of it, and the case survives. It still answers 1024 solutions. **What is shared between the stages is the clock, not the ledger.** Sharing the ledger was tried first and is wrong: the Gröbner path uses one mechanism for a genuine resource ceiling and for a structural refusal like "not polynomial", and a ledger that has recorded any ceiling refuses every later spend. So it read "declined in 8 ms because the system is uncoupled" as "the budget is gone", and the 1024-solution case raised instead of answering. Measured, not reasoned about. **Two ceilings, because neither bounds this alone.** A step is one candidate solution explored, which is what compounds -- each elimination turns the next level's coefficients into nested radicals. The systems that answer explore 2, 8 and at most 341; cyclic-5 passes 100 000 without finishing, so 10 000 has thirty-fold headroom over anything known to work and still refuses the runaways. The clock is a backstop and is deliberately loose, because a step can be arbitrarily expensive -- cyclic-4 spends five seconds in eight of them. A tight clock was tried at five seconds and made the same system answer or decline depending on machine load: it passed alone and failed inside the suite. A flaky answer is worse than a slow one. **The bound is cooperative and checked once per branch**, so a call can overshoot by the cost of the branch that was running. Cyclic-6 returns in about two minutes where it previously did not return. That is `BudgetLedger`'s stated design and it is a bound where there was none rather than a tight one. Cyclic-6 is deliberately not in the suite: two minutes is two minutes on every run, and even a ceiling of twenty takes half an hour on it because its branches are individually enormous. The test that matters is that the elimination consults a budget at all, which a system it answers in two candidates demonstrates in milliseconds. `BudgetTest.ExhaustionDoesNotChangeTheAnswer` asserted the old promise -- that exhausting the bounded path left the answer alone, because the caller carried on regardless. That promise is what this changes, so it is split into the two that are now true: a budget not reached changes nothing, and a budget reached bounds the whole call. A recording also sees two outcomes per solve rather than one, named rather than counted. Part of #896.
…d-to-end # Conflicts: # BREAKING-CHANGES.md # Sources/.editorconfig
…s cost `ADifferenceOfReciprocalLogarithms` wraps its limit in a task and fails it after sixty seconds of wall clock. The limit costs about seven, so the guard was never near the work -- but a wall clock in a parallel suite measures what else the machine is doing, and once this branch added nine tests it started failing about two runs in three while passing alone every time. Measured before blaming it, because the obvious suspect was this branch's own budget: the limit takes 6906, 6908 and 6924 ms with the new budget and 6970, 6896 and 6917 ms without it. Identical. The budget never fires here; the suite simply got heavier. Three minutes still catches a hang in a suite that takes seven.
|
Merged One extra commit, and it is worth reading before the rest.
Identical. The budget never fires there. The test wraps its limit in Full suite green twice on the merged tree: |
Closes #896.
The issue left the choice open between three options, and said option 3 "rests on an assumption that ... is not measured". I took that measurement first, and it turned out to settle the whole thing.
The measurement
The worry about bounding the fall-through was that it would cost the systems the fast path declines for reasons unrelated to difficulty — five uncoupled quartics with 1024 solutions, above the quotient-dimension cap, which the elimination answers cheaply.
a^4-1 … e^4-1uncoupled — must not regress8 milliseconds. So a whole-call bound leaves the elimination essentially all of it, and that case survives untouched — it still answers 1024 solutions. Option 1 was never the threat it looked like, and option 3's exit-reason discrimination isn't needed: a shared budget discriminates by construction, since a fast decline leaves the budget nearly full and a slow one does not.
Two things I got wrong on the way, both caught by measuring
1. Sharing the
BudgetLedgeritself is wrong. The Gröbner path uses one mechanism for a genuine resource ceiling and for a structural refusal like "not polynomial", and a ledger that has recorded any ceiling refuses every later spend. So sharing it read "declined in 8 ms because the system is uncoupled" as "the budget is gone" — and the 1024-solution case raised instead of answering. What crosses the boundary is the clock, not the ledger.2. A wall-clock bound is flaky, and a step bound is incomplete. The clock version passed in isolation and failed inside the full suite: under parallel load the good case exceeded five seconds and was cut off. A bound that makes the same system answer or decline depending on what else the machine is doing is a worse failure than a slow answer.
But steps alone don't bound cost either — cyclic-4 spends 4946 ms in eight branches.
So the default carries both, sized from measurement
A step is one candidate solution explored, which is what compounds: each elimination turns the next level's coefficients into nested radicals.
a^4-1 ×5, the largest that answersSteps = 10_000— thirtyfold headroom over anything known to work, and far below the runaways.Time = 60sas a loose backstop for the arbitrarily-expensive single step.The honest limitation
The bound is cooperative and checked once per branch, so a call can overshoot by the cost of the branch that was running when the budget ran out. Cyclic-6 returns in about two minutes, where before it did not return at all. That is
BudgetLedger's stated design — an algorithm that does not ask cannot be bounded, and a bound enforced from outside is a thread abort in the middle of a rewrite. It is a bound where there was none, not a tight one.Cyclic-6 is deliberately not in the test suite: two minutes is two minutes on every run, and even a ceiling of twenty takes half an hour on it, because its branches are individually enormous. The mechanism is demonstrated instead by a system the elimination answers in two candidates, which declines under a ceiling of one in milliseconds. Its own measurement is recorded in
BREAKING-CHANGES.md.Behaviour
a^4-1 ×5(1024 solutions)Budgetset below what the solve needsBudgetTest.ExhaustionDoesNotChangeTheAnswerasserted the old promise — that exhausting the bounded path left the answer alone because the caller carried on regardless. That promise is exactly what this changes, so it is split into the two that are now true: a budget not reached changes nothing, and a budget reached bounds the whole call.A recording now also sees two outcomes per solve rather than one, named
GröbnerandSolveSystem, so what stopped each is separately visible — which the issue notes a caller previously could not find out.Failed: 0, Passed: 9136, Skipped: 14locally on net10.0, with suite time unchanged.🤖 Generated with Claude Code
https://claude.ai/code/session_012sonx8iAspMiwRwokT1Ura