Make the budget test spend the budget, and bound the probes that earn nothing
The fixture built for "exactly MAX_VISITED stars reachable" was 338 short: its random cloud leaves clumps the departure never reaches (the LCG gives 12 212 distinct positions for 39 999 stars), so the search settled 39 662 and the pre-fix code answered `gaveUp: false` too. The test could not fail on the code it was written to pin — and the mutant that seemed to prove otherwise was failing to compile, not failing the test. It is now a line of 40 000 a parsec apart with the island off the line: settled 40 000 exactly, 115 ms, and the pre-fix code does report a give-up. Both mutants now compile and are caught. The give-up cap also has to hold while the bisection has earned nothing: the exception added for that case had no bound at all, so a search could spend the resolution's own eight full-budget probes — about 17 s of "Plotting…" — where two used to cost 4 s. Bounded at five. On the repo's crowded-knot fixture: 1.9 s for the unearned ceiling figure with the old cap, 7.2 s for a range the bisection earned, and five probes is where that lands. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016jxMkwA2rbicdGxHosecYi
This commit is contained in:
@@ -94,7 +94,7 @@ describe('routeBetween', () => {
|
|||||||
expect(search.gaveUp).toBe(true);
|
expect(search.gaveUp).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('reports a dead end proved with the last star of the budget as a dead end, not a give-up', { timeout: 30_000 }, () => {
|
it('reports a dead end proved with the last star of the budget as a dead end, not a give-up', () => {
|
||||||
// Exactly the budget's worth of stars reach each other, and the destination is not among them.
|
// Exactly the budget's worth of stars reach each other, and the destination is not among them.
|
||||||
// The search does look everywhere the range reaches, so "no chain" is what it found — but the
|
// The search does look everywhere the range reaches, so "no chain" is what it found — but the
|
||||||
// set is full at the end of it, and a budget read off the settled count says it gave up.
|
// set is full at the end of it, and a budget read off the settled count says it gave up.
|
||||||
@@ -131,16 +131,18 @@ function knotAndChain(cellSizePc?: number): StarNeighbourhood {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Exactly a search's budget of stars that reach one another — 39 999 through the 30 pc cube around
|
* Exactly a search's budget of stars that reach one another — 40 000 a parsec apart along x, which
|
||||||
* the origin, plus the departure — and one at 500 pc that nothing reaches. The dead end is real and
|
* a 1.5 pc range walks end to end — and one 500 pc off that line, which nothing reaches. The dead
|
||||||
* the search proves it, with the last star it is allowed.
|
* end is real and the search proves it, with the last star it is allowed.
|
||||||
|
*
|
||||||
|
* A line rather than a crowd because the count has to be exact: a random cloud dense enough to
|
||||||
|
* connect leaves clumps the departure never reaches, and 39 662 of 40 000 settled is a budget that
|
||||||
|
* was never spent.
|
||||||
*/
|
*/
|
||||||
const BUDGET_ISLAND = 99_999;
|
const BUDGET_ISLAND = 99_999;
|
||||||
function budgetExactly(): StarNeighbourhood {
|
function budgetExactly(): StarNeighbourhood {
|
||||||
let seed = 13;
|
const line: StarPoint[] = Array.from({ length: 40_000 }, (_, i) => ({ id: i, x: i, y: 0, z: 0 }));
|
||||||
const random = () => ((seed = (seed * 1103515245 + 12345) % 2147483648) / 2147483648) * 30 - 15;
|
return new StarNeighbourhood([...line, { id: BUDGET_ISLAND, x: 0, y: 500, z: 0 }], 1.5);
|
||||||
const crowd: StarPoint[] = Array.from({ length: 39_999 }, (_, i) => ({ id: 1000 + i, x: random(), y: random(), z: random() }));
|
|
||||||
return new StarNeighbourhood([{ id: 0, x: 0, y: 0, z: 0 }, ...crowd, { id: BUDGET_ISLAND, x: 500, y: 0, z: 0 }], 1.5);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -65,15 +65,21 @@ const MAX_VISITED = 40000;
|
|||||||
const RANGE_RESOLUTION_PC = 0.05;
|
const RANGE_RESOLUTION_PC = 0.05;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* How many of `minimumRangeBetween`'s probes may give up before it answers with what it has.
|
* How many of `minimumRangeBetween`'s probes may give up, once it has a range of its own, before it
|
||||||
|
* answers with what it has — and how many before it has one.
|
||||||
*
|
*
|
||||||
* A probe that finds a route is quick — it heads straight for the destination — while one that
|
* A probe that finds a route is quick — it heads straight for the destination — while one that
|
||||||
* gives up walks the whole search budget, about two seconds on the real catalogue. Those are also
|
* gives up walks the whole search budget, about two seconds on the real catalogue. Those are also
|
||||||
* the probes that buy the least: they cannot rule anything out. Two of them is the difference
|
* the probes that buy the least: they cannot rule anything out. Two of them is the difference
|
||||||
* between an answer of 7.96 pc in half a second and 5.76 pc in seventeen, for a star at 236 pc; it
|
* between an answer of 7.96 pc in half a second and 5.76 pc in seventeen, for a star at 236 pc; it
|
||||||
* lands on 5.97 pc in five.
|
* lands on 5.97 pc in five.
|
||||||
|
*
|
||||||
|
* Until a probe succeeds there is nothing to answer with but the ceiling route's own longest hop,
|
||||||
|
* which is the control's maximum, so the bound is looser there — but a bound, since the search is
|
||||||
|
* one the panel waits on: five probes, ten seconds, rather than the resolution's own eight.
|
||||||
*/
|
*/
|
||||||
const MAX_RANGE_GIVE_UPS = 2;
|
const MAX_RANGE_GIVE_UPS = 2;
|
||||||
|
const MAX_UNEARNED_GIVE_UPS = 5;
|
||||||
|
|
||||||
/** A binary min-heap of star ids by priority. Duplicates are allowed; stale ones are skipped on the way out. */
|
/** A binary min-heap of star ids by priority. Duplicates are allowed; stale ones are skipped on the way out. */
|
||||||
class Frontier {
|
class Frontier {
|
||||||
@@ -243,9 +249,10 @@ export function routeBetween(index: StarNeighbourhood, fromId: number, toId: num
|
|||||||
* nothing. It is still worth carrying on from — the ranges above it are the ones left to try — but
|
* nothing. It is still worth carrying on from — the ranges above it are the ones left to try — but
|
||||||
* the result is no longer the least range, only a range that works, and `least` says which. The
|
* the result is no longer the least range, only a range that works, and `least` says which. The
|
||||||
* number of steps that may give up is bounded for the same reason: each one walks the whole budget,
|
* number of steps that may give up is bounded for the same reason: each one walks the whole budget,
|
||||||
* and 11 s of them for a star at 236 pc bought two decimal places nobody reads. Bounded, but not
|
* and 11 s of them for a star at 236 pc bought two decimal places nobody reads. Bounded more
|
||||||
* before the bisection has found a range of its own: until then the only range it could offer is
|
* loosely before the bisection has found a range of its own, since until then the only range it
|
||||||
* the ceiling's, which is the control's maximum, for crossings that work well below it.
|
* could offer is the ceiling's, which is the control's maximum, for crossings that work well below
|
||||||
|
* it. See {@link MAX_RANGE_GIVE_UPS}.
|
||||||
*/
|
*/
|
||||||
export function minimumRangeBetween(index: StarNeighbourhood, fromId: number, toId: number, ceilingPc: number): RangeSearch {
|
export function minimumRangeBetween(index: StarNeighbourhood, fromId: number, toId: number, ceilingPc: number): RangeSearch {
|
||||||
const widest = routeBetween(index, fromId, toId, ceilingPc);
|
const widest = routeBetween(index, fromId, toId, ceilingPc);
|
||||||
@@ -256,10 +263,11 @@ export function minimumRangeBetween(index: StarNeighbourhood, fromId: number, to
|
|||||||
let unreachable = 0;
|
let unreachable = 0;
|
||||||
let reachable = ceilingHopPc;
|
let reachable = ceilingHopPc;
|
||||||
let giveUps = 0;
|
let giveUps = 0;
|
||||||
// The cap cannot fire while `reachable` is still the ceiling route's own longest hop: that is
|
// While `reachable` is still the ceiling route's own longest hop the bisection has nothing of its
|
||||||
// the question, not an answer the bisection earned, and offering it sends the control to its
|
// own to answer with, and that figure sends the control to its maximum for a crossing that works
|
||||||
// maximum for a crossing that works well below — 8.00 pc for a star that routes at 6.
|
// well below — 8.00 pc for a star that routes at 6. So it is allowed more probes there, not
|
||||||
while (reachable - unreachable > RANGE_RESOLUTION_PC && (giveUps < MAX_RANGE_GIVE_UPS || reachable === ceilingHopPc)) {
|
// unlimited ones: the panel is waiting on this.
|
||||||
|
while (reachable - unreachable > RANGE_RESOLUTION_PC && giveUps < (reachable === ceilingHopPc ? MAX_UNEARNED_GIVE_UPS : MAX_RANGE_GIVE_UPS)) {
|
||||||
const range = (unreachable + reachable) / 2;
|
const range = (unreachable + reachable) / 2;
|
||||||
const { route, gaveUp } = routeBetween(index, fromId, toId, range);
|
const { route, gaveUp } = routeBetween(index, fromId, toId, range);
|
||||||
if (route) {
|
if (route) {
|
||||||
|
|||||||
Reference in New Issue
Block a user