feat(08-03): ALT potentials inside the proven BH skeleton — GREEN
- routeAstar's ONLY change: the h_f/h_b bodies become max(ALT triangle bounds, haversine) — per-search Float64Array(L) constant tables in quanta (0xFFFF written as NaN = search-scoped landmark drop), lazy per-node max over the four triangle families with RAW u16 sentinel compares BEFORE any float conversion, haversine max-fallback on sentinel-flooded nodes (Pattern 3 containment) - the (h_f - h_b)/2 combination, reductions, balanced alternation, meet probes, BH stop rule, and real-seconds reconstruction are byte-identical (research Pattern 1 — the survey's (pf-pr)/2 is what p_f already is) - no Map/Object allocations (A8 green), no new imports, one search path (L >= 1 is the decodeGraph guarantee, D-01) - A1's 625-pair Dijkstra-equivalence sweep re-pinned under ALT: the standing optimality proof; A9 island + A10 skip laws sealed; A11 structural pin green - module header + routeAstar doc gain the ALT paragraph (c-metric exactness, sentinel laws, Ikeda history intact)
This commit is contained in:
+90
-7
@@ -4,9 +4,12 @@
|
|||||||
* ----------------------------------------------------------------------------
|
* ----------------------------------------------------------------------------
|
||||||
* The engine-A router core: routeAstar finds the minimum-seconds path
|
* The engine-A router core: routeAstar finds the minimum-seconds path
|
||||||
* between two graph nodes. The search is BIDIRECTIONAL (BH, Ikeda
|
* between two graph nodes. The search is BIDIRECTIONAL (BH, Ikeda
|
||||||
* potentials — see routeAstar's doc) because the free-plan 10 ms CPU budget
|
* potentials — see routeAstar's doc) with ALT landmark steering (plan
|
||||||
* is the design constraint and the measured unidirectional p95 on the real
|
* 08-03: h_f/h_b = max(ALT triangle bounds, haversine) over the ARG2
|
||||||
* KL graph was 3-5x over it. The hot loops remain allocation-free: binary
|
* landmark tables — see routeAstar's ALT paragraph) because the free-plan
|
||||||
|
* 10 ms CPU budget is the design constraint and the measured
|
||||||
|
* unidirectional p95 on the real KL graph was 3-5x over it. The hot loops
|
||||||
|
* remain allocation-free: binary
|
||||||
* heaps of node indices over preallocated typed arrays, typed-array
|
* heaps of node indices over preallocated typed arrays, typed-array
|
||||||
* distances/closed sets/chains, lazy on-demand potentials, no Map, no
|
* distances/closed sets/chains, lazy on-demand potentials, no Map, no
|
||||||
* objects per node. Optimality is not presumed — astar.test.mjs A1
|
* objects per node. Optimality is not presumed — astar.test.mjs A1
|
||||||
@@ -215,6 +218,23 @@ function reverseFor(graph: RoadGraph): ReverseAdj {
|
|||||||
* not a correctness knob (the stop rule carries optimality); A1's
|
* not a correctness knob (the stop rule carries optimality); A1's
|
||||||
* exhaustive 625-pair Dijkstra-equivalence sweep is the standing proof,
|
* exhaustive 625-pair Dijkstra-equivalence sweep is the standing proof,
|
||||||
* re-run on every pass.
|
* re-run on every pass.
|
||||||
|
*
|
||||||
|
* ALT (plan 08-03): the h_f/h_b bodies are max(ALT triangle bounds,
|
||||||
|
* haversine) over the ARG2 landmark tables (graph.landmarkTo /
|
||||||
|
* graph.landmarkFrom, quantized at graph.landmarkQuantumS). The tables
|
||||||
|
* come from landmark.ts's integer ĉ-metric Dijkstras — floor-quantized
|
||||||
|
* EDGE WEIGHTS make the stored distances an exact metric, so the triangle
|
||||||
|
* bounds derived from them are exactly admissible and exactly consistent
|
||||||
|
* against the real f32 seconds (no admissibility epsilon). Sentinel laws
|
||||||
|
* (research Pattern 3): a landmark whose goal/origin-side cell is the
|
||||||
|
* 0xFFFF sentinel (DATA) drops for the WHOLE search — the search-scoped
|
||||||
|
* constant skip; a per-node sentinel cell skips that ℓ; a node sentinel
|
||||||
|
* for every remaining ℓ falls back to the pure haversine bound (admissible
|
||||||
|
* + consistent everywhere). The survey's (πf−πr)/2 combination is what the
|
||||||
|
* p_f below already implements — ONLY the h_f/h_b bodies changed; the
|
||||||
|
* reductions, meet probes, stop rule, and reconstruction are the proven
|
||||||
|
* BH skeleton, and A1's sweep re-run under ALT (with A9/A10's island +
|
||||||
|
* skip seals) is the standing optimality proof.
|
||||||
*/
|
*/
|
||||||
export function routeAstar(graph: RoadGraph, originNode: number, goalNode: number): AstarResult | null {
|
export function routeAstar(graph: RoadGraph, originNode: number, goalNode: number): AstarResult | null {
|
||||||
const n = graph.nodeCount;
|
const n = graph.nodeCount;
|
||||||
@@ -229,20 +249,83 @@ export function routeAstar(graph: RoadGraph, originNode: number, goalNode: numbe
|
|||||||
const origLat = graph.lat[originNode]!;
|
const origLat = graph.lat[originNode]!;
|
||||||
const origLng = graph.lng[originNode]!;
|
const origLng = graph.lng[originNode]!;
|
||||||
|
|
||||||
|
// ALT per-search constants (plan 08-03): the goal/origin-side table cells
|
||||||
|
// hoisted ONCE per search, kept in QUANTA units with a 0xFFFF sentinel
|
||||||
|
// written as NaN. A sentinel constant drops that landmark for the WHOLE
|
||||||
|
// search — the search-scoped skip; these constant tables are the ONLY
|
||||||
|
// place skip decisions live (never a per-node ℓ-mask mid-max, which would
|
||||||
|
// vary the landmark set inside the max and break the max-of-Lipschitz
|
||||||
|
// consistency argument). L >= 1 is a decodeGraph guarantee (ARG2 refuses
|
||||||
|
// landmarkCount 0 — D-01), so there is no L===0 branch and no dead
|
||||||
|
// fallback path: one search path.
|
||||||
|
const L = graph.landmarkCount;
|
||||||
|
const Q = graph.landmarkQuantumS;
|
||||||
|
const goalToU = new Float64Array(L);
|
||||||
|
const goalFromU = new Float64Array(L);
|
||||||
|
const origToU = new Float64Array(L);
|
||||||
|
const origFromU = new Float64Array(L);
|
||||||
|
for (let l = 0; l < L; l++) {
|
||||||
|
const base = l * n;
|
||||||
|
const gTo = graph.landmarkTo[base + goalNode]!;
|
||||||
|
goalToU[l] = gTo === 0xffff ? Number.NaN : gTo;
|
||||||
|
const gFrom = graph.landmarkFrom[base + goalNode]!;
|
||||||
|
goalFromU[l] = gFrom === 0xffff ? Number.NaN : gFrom;
|
||||||
|
const oTo = graph.landmarkTo[base + originNode]!;
|
||||||
|
origToU[l] = oTo === 0xffff ? Number.NaN : oTo;
|
||||||
|
const oFrom = graph.landmarkFrom[base + originNode]!;
|
||||||
|
origFromU[l] = oFrom === 0xffff ? Number.NaN : oFrom;
|
||||||
|
}
|
||||||
|
|
||||||
// Directional heuristics + opposite potentials, lazy per node — only
|
// Directional heuristics + opposite potentials, lazy per node — only
|
||||||
// explored nodes ever pay, and the hot loops stay allocation-free.
|
// explored nodes ever pay, and the hot loops stay allocation-free.
|
||||||
const hfCache = new Float64Array(n).fill(Number.NaN); // hav(v, goal) * scale
|
const hfCache = new Float64Array(n).fill(Number.NaN); // h_f(v) = max(hav(v, goal) * scale, ALT bounds)
|
||||||
const hbCache = new Float64Array(n).fill(Number.NaN); // hav(v, origin) * scale
|
const hbCache = new Float64Array(n).fill(Number.NaN); // h_b(v) = max(hav(v, origin) * scale, ALT bounds)
|
||||||
const pf = new Float64Array(n); // p_f(v) = (h_f - h_b)/2; p_b = -p_f — valid once both caches are set
|
const pf = new Float64Array(n); // p_f(v) = (h_f - h_b)/2; p_b = -p_f — valid once both caches are set
|
||||||
|
|
||||||
|
// One directional bound: the max-rule (research Pattern 1) — the sharper
|
||||||
|
// of the haversine bound and the best ALT triangle bound toward the target
|
||||||
|
// whose constants are constTo/constFrom. Four families per landmark ℓ
|
||||||
|
// (Bast et al. survey §2.2), evaluated in quanta and converted to seconds
|
||||||
|
// ONCE at the end:
|
||||||
|
// family A: d(v,t) >= d(v,ℓ) − d(t,ℓ) (toTable both terms)
|
||||||
|
// family B: d(v,t) >= d(ℓ,t) − d(ℓ,v) (fromTable both terms)
|
||||||
|
// (mirrored toward the origin by the hb call site). Per-node sentinel
|
||||||
|
// cells skip that ℓ — compared RAW (u16) BEFORE any float conversion
|
||||||
|
// (the sentinel read as a float would be a large-but-finite bogus bound
|
||||||
|
// dominating the max). A node sentinel for every remaining ℓ degrades to
|
||||||
|
// exactly the
|
||||||
|
// pure haversine bound. A family value may sit below zero (v closer to ℓ
|
||||||
|
// than t is) — legal: p_f already goes negative when h_b > h_f, the BH
|
||||||
|
// meet-sum law is algebraic in its sign, and the max with the
|
||||||
|
// non-negative haversine bound keeps h_f/h_b >= 0 anyway.
|
||||||
|
function bound(v: number, lat2: number, lng2: number, constTo: Float64Array, constFrom: Float64Array): number {
|
||||||
|
let bestTo = Number.NEGATIVE_INFINITY;
|
||||||
|
let bestFrom = Number.NEGATIVE_INFINITY;
|
||||||
|
for (let l = 0; l < L; l++) {
|
||||||
|
const base = l * n;
|
||||||
|
const tv = graph.landmarkTo[base + v]!;
|
||||||
|
if (tv === 0xffff) continue; // per-node sentinel skip — RAW u16 compare
|
||||||
|
const cTo = constTo[l]!;
|
||||||
|
if (!Number.isNaN(cTo)) bestTo = Math.max(bestTo, tv - cTo); // family A: d(v,ℓ) − d(t,ℓ)
|
||||||
|
const fv = graph.landmarkFrom[base + v]!;
|
||||||
|
if (fv === 0xffff) continue;
|
||||||
|
const cFrom = constFrom[l]!;
|
||||||
|
if (!Number.isNaN(cFrom)) bestFrom = Math.max(bestFrom, cFrom - fv); // family B: d(ℓ,t) − d(ℓ,v)
|
||||||
|
}
|
||||||
|
const hav = metersTo(graph, v, lat2, lng2) * heuristicScale;
|
||||||
|
const alt = Math.max(bestTo, bestFrom);
|
||||||
|
return alt > Number.NEGATIVE_INFINITY ? Math.max(hav, alt * Q) : hav;
|
||||||
|
}
|
||||||
|
|
||||||
function potential(v: number): number {
|
function potential(v: number): number {
|
||||||
let hf = hfCache[v]!;
|
let hf = hfCache[v]!;
|
||||||
if (Number.isNaN(hf)) {
|
if (Number.isNaN(hf)) {
|
||||||
hf = metersTo(graph, v, goalLat, goalLng) * heuristicScale;
|
hf = bound(v, goalLat, goalLng, goalToU, goalFromU);
|
||||||
hfCache[v] = hf;
|
hfCache[v] = hf;
|
||||||
}
|
}
|
||||||
let hb = hbCache[v]!;
|
let hb = hbCache[v]!;
|
||||||
if (Number.isNaN(hb)) {
|
if (Number.isNaN(hb)) {
|
||||||
hb = metersTo(graph, v, origLat, origLng) * heuristicScale;
|
hb = bound(v, origLat, origLng, origToU, origFromU);
|
||||||
hbCache[v] = hb;
|
hbCache[v] = hb;
|
||||||
}
|
}
|
||||||
const value = (hf - hb) / 2;
|
const value = (hf - hb) / 2;
|
||||||
|
|||||||
Reference in New Issue
Block a user