feat(routing): 08-01 task 3 — --extended D-04 verdict on the CPU gate (100 cross-metro pairs, seed 0x08a1 shared with the baseline); default zero-arg mode byte-compatible, budget-audit 19/19
This commit is contained in:
@@ -40,7 +40,12 @@
|
||||
* landmark graph (build-time preprocessing, still $0 at runtime).
|
||||
*
|
||||
* Usage:
|
||||
* node routing/scripts/measure-route-cpu.mjs
|
||||
* node routing/scripts/measure-route-cpu.mjs # the 25-route CI gate
|
||||
* node routing/scripts/measure-route-cpu.mjs --extended # D-04 extended verdict
|
||||
* # (opt-in, maintainer-time: ~100 coordinate-pinned cross-metro
|
||||
* # pairs — the SAME pair set as the 08-01 optimality baseline —
|
||||
* # with its own p95 + gate verdict; the default mode above is
|
||||
* # byte-compatible and untouched)
|
||||
*
|
||||
* Exit codes: 0 p95 inside budget; 1 p95 over budget or absent binary.
|
||||
*/
|
||||
@@ -51,6 +56,8 @@ import { decodeGraph } from "../src/graph.ts";
|
||||
import { snapToGraph as snap } from "../src/snap.ts";
|
||||
import { routeAstar as astar } from "../src/astar.ts";
|
||||
import { getEntries } from "../../search/src/gazetteer.ts";
|
||||
import { haversineM } from "../src/geo.ts";
|
||||
import { generateCrossMetroPairs } from "./measure-optimality-baseline.mjs";
|
||||
|
||||
const here = dirname(fileURLToPath(import.meta.url)); // routing/scripts
|
||||
const GRAPH_PATH = resolve(here, "..", "data", "kl-roadgraph.v1.bin");
|
||||
@@ -63,6 +70,18 @@ const ROUTE_COUNT = 25;
|
||||
/** The PRNG seed — fixed so re-runs sample identical pairs. */
|
||||
const SAMPLE_SEED = 0x06d4;
|
||||
|
||||
/** D-04 extended verdict: ~100 cross-metro coordinate-pinned pairs
|
||||
* (maintainer-time, opt-in via --extended; the default mode above — the
|
||||
* CI-size snapshot that budget-gates.mjs re-runs with ZERO args — is
|
||||
* byte-compatible and untouched). */
|
||||
const EXTENDED_PAIR_COUNT = 100;
|
||||
/** Shared pair-set seed — identical to the 08-01 baseline recorder's
|
||||
* (the baseline and this verdict measure the SAME pairs; one definition). */
|
||||
const EXTENDED_SEED = 0x08a1;
|
||||
/** Cross-metro floor: straight-line endpoint distance below this is NOT a
|
||||
* worst-case pair. */
|
||||
const EXTENDED_MIN_PAIR_KM = 12;
|
||||
|
||||
function fail(message) {
|
||||
console.error(`measure-route-cpu: ${message}`);
|
||||
process.exit(1);
|
||||
@@ -83,6 +102,67 @@ console.log(
|
||||
`bbox lat ${graph.bbox.minLat.toFixed(3)}..${graph.bbox.maxLat.toFixed(3)} lng ${graph.bbox.minLng.toFixed(3)}..${graph.bbox.maxLng.toFixed(3)}`,
|
||||
);
|
||||
|
||||
/* ————— the D-04 EXTENDED verdict (opt-in; the default mode below is untouched) ————— */
|
||||
|
||||
const extended = process.argv.includes("--extended");
|
||||
if (extended) {
|
||||
console.log(`extended sample (D-04): ~${EXTENDED_PAIR_COUNT} coordinate-pinned cross-metro pairs (seed ${EXTENDED_SEED}, min ${EXTENDED_MIN_PAIR_KM} km straight-line — the SAME pair set as the 08-01 optimality baseline)`);
|
||||
const crossPairs = generateCrossMetroPairs(graph.bbox, {
|
||||
count: EXTENDED_PAIR_COUNT,
|
||||
minPairKm: EXTENDED_MIN_PAIR_KM,
|
||||
seed: EXTENDED_SEED,
|
||||
haversineM,
|
||||
});
|
||||
// One unmeasured warm-up (printed, never hidden) — same warm-isolate law as
|
||||
// the default mode: the reverse-CSR build must not land inside a sample.
|
||||
const extWarm = cpuMs(() => astar(graph, 0, Math.min(1, graph.nodeCount - 1)));
|
||||
console.log(`warm-up (unmeasured): ${extWarm.ms.toFixed(3)} ms`);
|
||||
const extendedSamples = [];
|
||||
let extendedMisses = 0;
|
||||
let extendedMissMs = 0;
|
||||
for (const pair of crossPairs) {
|
||||
const origin = snap(graph, pair.origin);
|
||||
const goal = snap(graph, pair.destination);
|
||||
if (origin === null || goal === null) {
|
||||
// An off-road random coordinate: the honest snap miss, reported
|
||||
// separately — never inside the p95 sample (the same law as the
|
||||
// default mode's unreachable probes).
|
||||
extendedMisses += 1;
|
||||
console.log(` ${pair.pairId}: unsnappable (honest null)`);
|
||||
continue;
|
||||
}
|
||||
const { ms, result } = cpuMs(() => astar(graph, origin.node, goal.node));
|
||||
if (result === null) {
|
||||
extendedMisses += 1;
|
||||
extendedMissMs += ms;
|
||||
console.log(` ${pair.pairId}: ${ms.toFixed(3)} ms (unreachable — honest null, D-03)`);
|
||||
continue;
|
||||
}
|
||||
const km = `${(result.distanceM / 1000).toFixed(1)} km / ${(result.seconds / 60).toFixed(1)} min`;
|
||||
console.log(` ${pair.pairId}: ${ms.toFixed(3)} ms (${km})`);
|
||||
extendedSamples.push(ms);
|
||||
}
|
||||
if (extendedSamples.length === 0) fail("no routable pairs in the extended sample — the graph may be fragmented; rebuild");
|
||||
extendedSamples.sort((a, b) => a - b);
|
||||
const extIdx = Math.min(extendedSamples.length - 1, Math.ceil(0.95 * extendedSamples.length) - 1);
|
||||
const extP95 = extendedSamples[extIdx];
|
||||
const extMax = extendedSamples[extendedSamples.length - 1];
|
||||
const extMean = extendedSamples.reduce((acc, v) => acc + v, 0) / extendedSamples.length;
|
||||
console.log(
|
||||
`routed pairs: ${extendedSamples.length}/${crossPairs.length}` +
|
||||
(extendedMisses > 0
|
||||
? `; snap-miss/unreachable probes: ${extendedMisses} (mean ${(extendedMissMs / extendedMisses).toFixed(3)} ms, reported separately)`
|
||||
: ""),
|
||||
);
|
||||
console.log(`p95: ${extP95.toFixed(3)} ms (max ${extMax.toFixed(3)} ms, mean ${extMean.toFixed(3)} ms, n=${extendedSamples.length})`);
|
||||
if (extP95 > P95_BUDGET_MS) {
|
||||
console.error(`GATE FAIL: p95 ${extP95.toFixed(3)} ms exceeds the ${P95_BUDGET_MS} ms budget — D-04 extended verdict`);
|
||||
process.exit(1);
|
||||
}
|
||||
console.log(`GATE PASS: p95 ${extP95.toFixed(3)} ms <= ${P95_BUDGET_MS} ms budget — D-04 extended verdict`);
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
/** mulberry32 — tiny deterministic PRNG (fixed seed => comparable runs). */
|
||||
function mulberry32(seed) {
|
||||
let a = seed >>> 0;
|
||||
|
||||
Reference in New Issue
Block a user