diff --git a/src/app/shared/astro/jump-links.spec.ts b/src/app/shared/astro/jump-links.spec.ts index fc3f95d..420da4a 100644 --- a/src/app/shared/astro/jump-links.spec.ts +++ b/src/app/shared/astro/jump-links.spec.ts @@ -94,7 +94,7 @@ describe('routeBetween', () => { 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. // 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. @@ -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 - * the origin, plus the departure — and one at 500 pc that nothing reaches. The dead end is real and - * the search proves it, with the last star it is allowed. + * Exactly a search's budget of stars that reach one another — 40 000 a parsec apart along x, which + * a 1.5 pc range walks end to end — and one 500 pc off that line, which nothing reaches. The dead + * 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; function budgetExactly(): StarNeighbourhood { - let seed = 13; - const random = () => ((seed = (seed * 1103515245 + 12345) % 2147483648) / 2147483648) * 30 - 15; - 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); + const line: StarPoint[] = Array.from({ length: 40_000 }, (_, i) => ({ id: i, x: i, y: 0, z: 0 })); + return new StarNeighbourhood([...line, { id: BUDGET_ISLAND, x: 0, y: 500, z: 0 }], 1.5); } /** diff --git a/src/app/shared/astro/jump-links.ts b/src/app/shared/astro/jump-links.ts index c5fbce1..3aa3bbc 100644 --- a/src/app/shared/astro/jump-links.ts +++ b/src/app/shared/astro/jump-links.ts @@ -65,15 +65,21 @@ const MAX_VISITED = 40000; 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 * 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 * 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. + * + * 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_UNEARNED_GIVE_UPS = 5; /** A binary min-heap of star ids by priority. Duplicates are allowed; stale ones are skipped on the way out. */ 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 * 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, - * and 11 s of them for a star at 236 pc bought two decimal places nobody reads. Bounded, but not - * before the bisection has found a range of its own: until then the only range it could offer is - * the ceiling's, which is the control's maximum, for crossings that work well below it. + * and 11 s of them for a star at 236 pc bought two decimal places nobody reads. Bounded more + * loosely before the bisection has found a range of its own, since until then the only range 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 { const widest = routeBetween(index, fromId, toId, ceilingPc); @@ -256,10 +263,11 @@ export function minimumRangeBetween(index: StarNeighbourhood, fromId: number, to let unreachable = 0; let reachable = ceilingHopPc; let giveUps = 0; - // The cap cannot fire while `reachable` is still the ceiling route's own longest hop: that is - // the question, not an answer the bisection earned, and offering it sends the control to its - // maximum for a crossing that works well below — 8.00 pc for a star that routes at 6. - while (reachable - unreachable > RANGE_RESOLUTION_PC && (giveUps < MAX_RANGE_GIVE_UPS || reachable === ceilingHopPc)) { + // While `reachable` is still the ceiling route's own longest hop the bisection has nothing of its + // own to answer with, and that figure sends the control to its maximum for a crossing that works + // well below — 8.00 pc for a star that routes at 6. So it is allowed more probes there, not + // 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 { route, gaveUp } = routeBetween(index, fromId, toId, range); if (route) {