Files
arah2/routing/scripts/experiment-alt-direction-split.mjs
T

210 lines
13 KiB
JavaScript

#!/usr/bin/env node
/* Branch-only directional ALT experiment. Neither production nor integrations import this. */
import { readFileSync, existsSync, writeFileSync, unlinkSync } from 'node:fs';
import { createHash } from 'node:crypto';
import { hostname } from 'node:os';
import { fileURLToPath } from 'node:url';
import { decodeGraph } from '../src/graph.ts';
import { haversineM } from '../src/geo.ts';
import { snapToGraph } from '../src/snap.ts';
import { routeAstar } from '../src/astar.ts';
import { generateCrossMetroPairs } from './measure-optimality-baseline.mjs';
import { sweepOracle } from './measure-astar-oracle-sweep.mjs';
// Source-identical isolated instrumented clone: only two settled counters are injected.
// The production file is never opened for writing; remove the ephemeral copy in finally.
async function instrumentedShipped() {
const source = new URL('../src/astar.ts', import.meta.url);
const clone = new URL('../src/astar-alt-measure.tmp.ts', import.meta.url);
let code = readFileSync(source, 'utf8');
const forward = 'if (expandForward) fClosed[u] = 1;', backward = 'else bClosed[u] = 1;';
if (!code.includes(forward) || !code.includes(backward)) throw Error('production settled markers changed; instrumentation not valid');
code = code.replace(forward, 'if (expandForward) { fClosed[u] = 1; globalThis.__altControlSettled.forward++; }')
.replace(backward, 'else { bClosed[u] = 1; globalThis.__altControlSettled.backward++; }');
if (existsSync(clone)) throw Error('temporary instrumentation clone already exists');
try {
writeFileSync(clone, code, { flag: 'wx' });
return await import(clone.href);
} finally { if (existsSync(clone)) unlinkSync(clone); }
}
const INF = Infinity, SENTINEL = 0xffff;
class Heap {
a = [];
push(k, g, v) {
const a = this.a; let i = a.length; a.push({ k, g, v }); const x = a[i];
while (i) { const p = (i - 1) >> 1; if (a[p].k <= k) break; a[i] = a[p]; i = p; } a[i] = x;
}
pop() {
const a = this.a, top = a[0], x = a.pop();
if (a.length) { let i = 0; for (;;) { let j = i * 2 + 1; if (j >= a.length) break; if (j + 1 < a.length && a[j + 1].k < a[j].k) j++; if (a[j].k >= x.k) break; a[i] = a[j]; i = j; } a[i] = x; }
return top;
}
peek(dist, closed) { while (this.a.length && (closed[this.a[0].v] || dist[this.a[0].v] !== this.a[0].g)) this.pop(); return this.a[0]?.k ?? INF; }
}
const reverseCache = new WeakMap();
function reverse(graph) {
if (reverseCache.has(graph)) return reverseCache.get(graph);
const rows = new Uint32Array(graph.nodeCount + 1);
for (let e = 0; e < graph.edgeCount; e++) rows[graph.edgeTarget[e] + 1]++;
for (let v = 0; v < graph.nodeCount; v++) rows[v + 1] += rows[v];
const source = new Uint32Array(graph.edgeCount), ids = new Uint32Array(graph.edgeCount), cursor = rows.slice(0, -1);
for (let u = 0; u < graph.nodeCount; u++) for (let e = graph.edgeStart[u]; e < graph.edgeStart[u + 1]; e++) {
const pos = cursor[graph.edgeTarget[e]]++; source[pos] = u; ids[pos] = e;
}
const result = { rows, source, ids }; reverseCache.set(graph, result); return result;
}
/** Returns null on an unreachable pair; throws on a violated proof assumption. */
export function routeDirectionSplit(graph, o, g) {
const n = graph.nodeCount;
if (!Number.isInteger(o) || !Number.isInteger(g) || o < 0 || g < 0 || o >= n || g >= n) return null;
if (o === g) return { nodes: [o], seconds: 0, expansionsForward: 0, expansionsBackward: 0, activeTo: 0, activeFrom: 0 };
const T = [], F = [], Q = graph.landmarkQuantumS;
for (let l = 0; l < graph.landmarkCount; l++) {
const base = l * n;
if (graph.landmarkTo[base + g] !== SENTINEL) {
if (graph.landmarkTo[base + o] === SENTINEL) return null;
T.push([base, graph.landmarkTo[base + g]]);
}
if (graph.landmarkFrom[base + o] !== SENTINEL) {
if (graph.landmarkFrom[base + g] === SENTINEL) return null;
F.push([base, graph.landmarkFrom[base + o]]);
}
}
const domain = new Int8Array(n);
function inside(v) {
if (domain[v]) return domain[v] === 1;
for (const [base] of T) if (graph.landmarkTo[base + v] === SENTINEL) { domain[v] = -1; return false; }
for (const [base] of F) if (graph.landmarkFrom[base + v] === SENTINEL) { domain[v] = -1; return false; }
domain[v] = 1; return true;
}
const scale = 3.6 / graph.maxSpeedKmh;
if (!(scale > 0 && Number.isFinite(scale))) throw Error('nonpositive graph maxSpeedKmh');
const hf = new Float64Array(n).fill(NaN), hb = new Float64Array(n).fill(NaN);
function heuristic(v, forward) {
const cache = forward ? hf : hb;
if (!Number.isNaN(cache[v])) return cache[v];
const target = forward ? g : o;
let h = haversineM({ lat: graph.lat[v], lng: graph.lng[v] }, { lat: graph.lat[target], lng: graph.lng[target] }) * scale;
for (const [base, constant] of (forward ? T : F)) {
const cell = (forward ? graph.landmarkTo : graph.landmarkFrom)[base + v];
if (cell === SENTINEL) throw Error('sentinel in included domain');
h = Math.max(h, (cell - constant) * Q);
}
cache[v] = h; return h;
}
const df = new Float64Array(n).fill(INF), db = new Float64Array(n).fill(INF);
const prev = new Int32Array(n).fill(-1), next = new Int32Array(n).fill(-1);
const cf = new Uint8Array(n), cb = new Uint8Array(n), a = new Heap(), b = new Heap(), rev = reverse(graph);
df[o] = 0; db[g] = 0; a.push(heuristic(o, true), 0, o); b.push(heuristic(g, false), 0, g);
let U = INF, meeting = -1, expansionsForward = 0, expansionsBackward = 0;
while (true) {
const fa = a.peek(df, cf), bb = b.peek(db, cb);
if (fa === INF || bb === INF || fa >= U || bb >= U) break;
const forward = a.a.length <= b.a.length;
const heap = forward ? a : b, distances = forward ? df : db, other = forward ? db : df;
const closed = forward ? cf : cb, chain = forward ? prev : next;
const u = heap.pop().v, du = distances[u]; closed[u] = 1;
if (forward) expansionsForward++; else expansionsBackward++;
if (du + other[u] < U) { U = du + other[u]; meeting = u; }
const start = forward ? graph.edgeStart[u] : rev.rows[u], end = forward ? graph.edgeStart[u + 1] : rev.rows[u + 1];
for (let r = start; r < end; r++) {
const e = forward ? r : rev.ids[r];
if (graph.edgeFlags[e] & 1) continue;
const v = forward ? graph.edgeTarget[e] : rev.source[r];
if (!inside(v)) continue;
const seconds = graph.edgeSeconds[e];
// Enforce both directed inequalities on each inspected edge. Tiny f64 rounding tolerance only in assertion.
const delta = heuristic(u, forward) - heuristic(v, forward);
if (delta > seconds + 1e-7) throw Error(`inconsistent ${forward ? 'forward' : 'backward'} potential ${u}->${v}: delta=${delta}, seconds=${seconds}, Q=${Q}`);
const tentative = du + seconds;
if (tentative < distances[v]) {
if (closed[v]) throw Error('improved closed label');
distances[v] = tentative; chain[v] = u; heap.push(tentative + heuristic(v, forward), tentative, v);
}
if (distances[v] + other[v] < U) { U = distances[v] + other[v]; meeting = v; }
}
}
if (meeting < 0) return null;
const left = []; for (let v = meeting; v !== -1; v = prev[v]) left.push(v); left.reverse();
const right = []; for (let v = next[meeting]; v !== -1; v = next[v]) right.push(v);
const nodes = left.concat(right);
let seconds = 0;
for (let i = 1; i < nodes.length; i++) {
let e = graph.edgeStart[nodes[i - 1]], end = graph.edgeStart[nodes[i - 1] + 1];
while (e < end && (graph.edgeTarget[e] !== nodes[i] || (graph.edgeFlags[e] & 1))) e++;
if (e === end) throw Error('invalid reconstructed edge');
seconds += graph.edgeSeconds[e];
}
if (Math.abs(seconds - U) > 1e-6) throw Error(`reconstructed seconds mismatch: ${seconds} vs ${U}`);
return { nodes, seconds, expansionsForward, expansionsBackward, activeTo: T.length, activeFrom: F.length };
}
export function dijkstra(graph, o, g) {
const dist = new Float64Array(graph.nodeCount).fill(INF), closed = new Uint8Array(graph.nodeCount), heap = new Heap();
dist[o] = 0; heap.push(0, 0, o);
while (heap.peek(dist, closed) !== INF) {
const u = heap.pop().v; if (u === g) return dist[u]; closed[u] = 1;
for (let e = graph.edgeStart[u]; e < graph.edgeStart[u + 1]; e++) {
if (graph.edgeFlags[e] & 1) continue;
const v = graph.edgeTarget[e], nd = dist[u] + graph.edgeSeconds[e];
if (nd < dist[v]) { dist[v] = nd; heap.push(nd, nd, v); }
}
}
return INF;
}
if (process.argv[1] && fileURLToPath(import.meta.url) === process.argv[1]) {
const path = new URL('../data/kl-roadgraph.v1.bin', import.meta.url);
if (!existsSync(path)) { console.error('NOT MEASURED: missing ARG2 binary'); process.exitCode = 2; }
else {
const bytes = readFileSync(path), graph = decodeGraph(bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength));
if (!graph) { console.error('NOT MEASURED: undecodable ARG2 binary'); process.exitCode = 2; }
else {
console.log(`host=${hostname()} node=${process.version} sha256=${createHash('sha256').update(bytes).digest('hex')} graph=${graph.nodeCount}/${graph.edgeCount}`);
const raw = generateCrossMetroPairs(graph.bbox, { count: 600, seed: 0x08f1, minPairKm: 12, haversineM });
const pairs = [], missing = { snap: 0 };
for (const pair of raw) { if (pairs.length === 200) break; if (!snapToGraph(graph, pair.origin) || !snapToGraph(graph, pair.destination)) missing.snap++; else pairs.push(pair); }
if (pairs.length < 200) throw Error(`only ${pairs.length}/200 snapped pairs`);
const verdict = sweepOracle(graph, pairs, { snap: snapToGraph, astar: routeDirectionSplit, oracle: dijkstra });
console.log(`oracle: compared=${pairs.length} snap-miss=${missing.snap} routed=${verdict.routed} unreachable=${verdict.unreachable} suboptimal=${verdict.suboptimal} oracle-mismatch=${verdict.oracleMismatch} class-break=${verdict.classBreaks} pass=${verdict.pass}`);
if (!verdict.pass) { console.error(verdict.rows.filter(r => !['pass', 'unreachable'].includes(r.verdict))); process.exitCode = 1; }
globalThis.__altControlSettled = { forward: 0, backward: 0 };
const control = (await instrumentedShipped()).routeAstar;
const extended = generateCrossMetroPairs(graph.bbox, { count: 100, seed: 0x08a1, minPairKm: 12, haversineM });
const warm = (fn) => { const a = process.hrtime.bigint(); fn(); return Number(process.hrtime.bigint() - a) / 1e6; };
const warmExp = warm(() => routeDirectionSplit(graph, 0, 1));
const warmShipped = warm(() => routeAstar(graph, 0, 1));
const warmClone = warm(() => control(graph, 0, 1));
console.log(`warm-up ms (unmeasured): experimental=${warmExp.toFixed(3)} original-shipped=${warmShipped.toFixed(3)} instrumented-clone=${warmClone.toFixed(3)}`);
globalThis.__altControlSettled = { forward: 0, backward: 0 };
const times = [[], [], []], counts = [0, 0], misses = { snap: 0, unreachable: 0 }, active = [0, 0];
for (let round = 0; round < 2; round++) for (let i = 0; i < extended.length; i++) {
const pair = extended[i], s = snapToGraph(graph, pair.origin), t = snapToGraph(graph, pair.destination);
if (!s || !t) { if (round === 0) misses.snap++; continue; }
const results = [];
for (const k of ((i + round) % 2 ? [2, 1, 0] : [0, 1, 2])) {
const st = process.hrtime.bigint(), result = k === 0 ? routeDirectionSplit(graph, s.node, t.node) : k === 1 ? control(graph, s.node, t.node) : routeAstar(graph, s.node, t.node);
const ms = Number(process.hrtime.bigint() - st) / 1e6; results[k] = { result, ms };
}
if (results[0].result === null || results[1].result === null || results[2].result === null) {
if (results[0].result !== results[1].result || results[0].result !== results[2].result) throw Error(`paired reachability mismatch ${pair.pairId}`);
if (round === 0) misses.unreachable++; continue;
}
if (Math.abs(results[0].result.seconds - results[1].result.seconds) > 1e-6 || Math.abs(results[1].result.seconds - results[2].result.seconds) > 1e-6) throw Error(`paired cost mismatch ${pair.pairId}`);
for (let k = 0; k < 3; k++) times[k].push(results[k].ms);
counts[0] += results[0].result.expansionsForward + results[0].result.expansionsBackward;
active[0] += results[0].result.activeTo; active[1] += results[0].result.activeFrom;
}
const stats = a => { a.sort((x, y) => x - y); return `n=${a.length} median=${a[Math.floor(a.length / 2)].toFixed(3)} mean=${(a.reduce((x,y) => x + y,0)/a.length).toFixed(3)} p95=${a[Math.ceil(a.length * .95)-1].toFixed(3)} ms`; };
console.log(`extended snap-miss=${misses.snap} unreachable=${misses.unreachable} (outside routed p95), 2 alternating rounds`);
console.log(`experimental: ${stats(times[0])} expansions=${counts[0]} active-to/from=${active.join('/')}`);
counts[1] = globalThis.__altControlSettled.forward + globalThis.__altControlSettled.backward;
console.log(`shipped isolated instrumented clone: ${stats(times[1])} expansions=${counts[1]} forward/backward=${globalThis.__altControlSettled.forward}/${globalThis.__altControlSettled.backward}; experimental/control expansion ratio=${(counts[0]/counts[1]).toFixed(3)}`);
console.log(`original shipped routeAstar (paired unmodified haversine-only control): ${stats(times[2])}`);
}
}
}