feat(08-02): implement the pure landmark compute core — L-suite GREEN

farthestPointAnchors + landmarkTables + landmarkCoverage over the
ĉ metric: edge weights floor-quantized once (Math.floor(seconds/q)),
exact integer Dijkstras (Int32 labels, binary heap, lazy deletion),
0xFFFF sentinel, 65000-quantum loud overflow. Traversal law mirrors
astar.ts verbatim: bit0 edges never relaxed in EITHER direction.

Test-vector corrections caught at GREEN (expectations were wrong,
implementation was right):
- L4: rows index by anchor POSITION (row-major [l*N+v]), not node id
- L6: the c-metric farthest node from corner 0 is interior node 18
  (297q: interior hops cost 2x perimeter), not corner 24 (264q) —
  verified against an independent brute-force Dijkstra
This commit is contained in:
araharah-executor
2026-10-25 18:40:17 +08:00
parent 1bf413b3ac
commit 5b7a1300c2
2 changed files with 405 additions and 14 deletions
+387
View File
@@ -0,0 +1,387 @@
/*
* landmark.ts — the ALT landmark preprocessing core (plan 08-02, D-02).
* ----------------------------------------------------------------------------
* One pure, typed-array module for everything integer about landmarks:
* deterministic farthest-point anchor selection + the per-anchor distance
* tables, both over the ĉ metric. The fixture builder
* (test/fixtures/build-mini-graph.mjs), the real build script
* (scripts/build-road-graph.mjs) and the bot suites ALL compute their
* landmark blocks through these exports — there is no second
* integer-Dijkstra implementation anywhere (single-authority law).
*
* THE ĉ METRIC (research Pattern 2 — quantization WITHOUT an epsilon):
* edge weights are floor-quantized ONCE, ĉ(u,v) = floor(seconds/quantumS)
* in fixed 0.1 s units, and the landmark Dijkstras run over that integer
* metric in EXACT arithmetic. Stored distances are never rounded; because
* ĉ <= c edge-wise and Dijkstra distances are 1-Lipschitz in their own
* edges, the triangle bounds Plan 08-03 derives from these tables are
* exactly admissible and exactly consistent against the real f32 seconds.
*
* TRAVERSAL LAW (mirrors astar.ts verbatim — the A1/625-pair precedent):
* an edge with bit0 set ("oneway-reverse", graph.ts) is NEVER relaxed —
* not in the forward pass, and not in the reversed pass either (astar.ts's
* backward search skips bit0 in-edges at line 321). The reversed pass
* relaxes a stored edge u->v FROM v (toward u) exactly when its bit0 is
* clear, which is what makes to-row / from-row asymmetry representable.
*
* RANGE LAW: 0xFFFF is DATA (the unreachable sentinel), so any label
* beyond LANDMARK_MAX_QUANTA (65 000 — headroom below the sentinel)
* throws at build time, naming the distance and the fix
* (raise landmarkQuantumS). Nothing is clamped, ever.
*
* DETERMINISM: no randomness of any kind — no PRNG, no Date, no
* map-iteration order. Anchor greed starts at the first degree>=2
* candidate by node id, ties break to the lower node id, and identical
* input bytes give identical anchors.
*
* Pure module: no I/O, no logging, no storage, zero imports (T-06-05).
*/
/** The ĉ unit in seconds — the header field default, a documented fork knob. */
export const LANDMARK_QUANTUM_S = 0.1;
/** u16 headroom below the 0xFFFF sentinel; any label past it fails loud. */
export const LANDMARK_MAX_QUANTA = 65000;
/** Stored sentinel for "unreachable" — DATA, never validated away. The
* all-ones u16 pattern 0xffff is every table cell's out-of-domain value;
* consumers check against this constant, never a per-callsite literal. */
export const LANDMARK_SENTINEL = 0xffff;
/** The landmark compute input — the shape build scripts assemble. */
export interface LandmarkInput {
nodeCount: number;
/**
* Directed edges in any order. flags bit0 = oneway-reverse: relaxed by
* NO direction (mirrors the CSR's edgeFlags; see the traversal law).
*/
edges: Array<{ from: number; to: number; seconds: number; flags: number }>;
/** The ĉ quantum in seconds; named callers pass LANDMARK_QUANTUM_S. */
quantumS: number;
}
/** Internal "unvisited" distance — outside the u16 domain on purpose. */
const UNVISITED = 0x7fffffff;
function fail(context: string, message: string): never {
throw new Error(`${context}: ${message}`);
}
/* ————— integer CSR (both directions) over the quantized weights ————— */
interface Prepared {
n: number;
/** Per-input-edge quantized weight ĉ (integer quanta). */
qWeight: Int32Array;
/** Per-input-edge traversal-relevant flags (bit0 only). */
flags: Uint8Array;
/** Per-input-edge endpoints (the reverse pass relaxes toward source). */
target: Uint32Array;
source: Uint32Array;
/** Forward CSR: row u lists u's out-edges (input-edge indices). */
fwdRow: Uint32Array;
fwdEdge: Uint32Array;
/** Reverse CSR: row v lists v's in-edges (input-edge indices). */
revRow: Uint32Array;
revEdge: Uint32Array;
}
function prepare(input: LandmarkInput): Prepared {
const context = "landmark";
const n = input.nodeCount;
if (!Number.isInteger(n) || n < 1) fail(context, "nodeCount must be a positive integer");
if (!Number.isFinite(input.quantumS) || input.quantumS <= 0) {
fail(context, "quantumS must be a positive finite number");
}
const edgeCount = input.edges.length;
const qWeight = new Int32Array(edgeCount);
const flags = new Uint8Array(edgeCount);
const target = new Uint32Array(edgeCount);
const source = new Uint32Array(edgeCount);
const fwdRow = new Uint32Array(n + 1);
const revRow = new Uint32Array(n + 1);
for (let e = 0; e < edgeCount; e++) {
const edge = input.edges[e]!;
if (!Number.isInteger(edge.from) || edge.from < 0 || edge.from >= n) {
fail(context, `edge ${e} from-node ${edge.from} out of range`);
}
if (!Number.isInteger(edge.to) || edge.to < 0 || edge.to >= n) {
fail(context, `edge ${e} to-node ${edge.to} out of range`);
}
if (!Number.isFinite(edge.seconds) || edge.seconds < 0) {
fail(context, `edge ${edge.from}->${edge.to} has a non-finite/negative weight`);
}
// THE ĉ RULE (Pattern 2): floor keeps the surrogate at or below the
// live edge — the exactness hinge for admissibility downstream.
qWeight[e] = Math.floor(edge.seconds / input.quantumS);
flags[e] = edge.flags & 1;
target[e] = edge.to;
source[e] = edge.from;
fwdRow[edge.from + 1] = fwdRow[edge.from + 1]! + 1;
revRow[edge.to + 1] = revRow[edge.to + 1]! + 1;
}
for (let i = 0; i < n; i++) {
fwdRow[i + 1] = fwdRow[i + 1]! + fwdRow[i]!;
revRow[i + 1] = revRow[i + 1]! + revRow[i]!;
}
const fwdEdge = new Uint32Array(edgeCount);
const revEdge = new Uint32Array(edgeCount);
const fwdCursor = fwdRow.slice(0, n);
const revCursor = revRow.slice(0, n);
for (let e = 0; e < edgeCount; e++) {
const fu = source[e]!;
const fp = fwdCursor[fu]!;
fwdEdge[fp] = e;
fwdCursor[fu] = fp + 1;
const rv = target[e]!;
const rp = revCursor[rv]!;
revEdge[rp] = e;
revCursor[rv] = rp + 1;
}
return { n, qWeight, flags, target, source, fwdRow, fwdEdge, revRow, revEdge };
}
/* ————— binary min-heap of (integer key, node) over preallocated arrays ————— */
class IntHeap {
private keys = new Int32Array(1024);
private nodes = new Uint32Array(1024);
size = 0;
push(key: number, node: number): void {
if (this.size === this.keys.length) this.grow();
let i = this.size;
this.size += 1;
while (i > 0) {
const parent = (i - 1) >> 1;
if (this.keys[parent]! <= key) break;
this.keys[i] = this.keys[parent]!;
this.nodes[i] = this.nodes[parent]!;
i = parent;
}
this.keys[i] = key;
this.nodes[i] = node;
}
peekKey(): number {
return this.size === 0 ? UNVISITED : this.keys[0]!;
}
/** Min-key node, or -1 when empty. Lazy deletion: stale entries are
* skipped by the dist check in dijkstraPass (the astar.ts shape). */
pop(): number {
if (this.size === 0) return -1;
const top = this.nodes[0]!;
this.size -= 1;
const lastKey = this.keys[this.size]!;
const lastNode = this.nodes[this.size]!;
let i = 0;
for (;;) {
const left = 2 * i + 1;
if (left >= this.size) break;
const right = left + 1;
const child = right < this.size && this.keys[right]! < this.keys[left]! ? right : left;
if (this.keys[child]! >= lastKey) break;
this.keys[i] = this.keys[child]!;
this.nodes[i] = this.nodes[child]!;
i = child;
}
if (this.size > 0) {
this.keys[i] = lastKey;
this.nodes[i] = lastNode;
}
return top;
}
private grow(): void {
const keys = new Int32Array(this.keys.length * 2);
keys.set(this.keys);
this.keys = keys;
const nodes = new Uint32Array(this.nodes.length * 2);
nodes.set(this.nodes);
this.nodes = nodes;
}
}
/* ————— the ONE integer Dijkstra (both directions share it) ————— */
/**
* Exact integer Dijkstra from `anchor` over the ĉ metric. `reverse=false`
* relaxes stored out-edges (d̂(anchor, v)); `reverse=true` relaxes stored
* in-edges from their head (d̂(v, anchor)) — the astar.ts backward-search
* law: an in-edge with bit0 set is skipped, never traversed.
* Returns per-node labels; UNVISITED means unreachable. Any improving
* label beyond LANDMARK_MAX_QUANTA throws (context names the entry point).
*/
function dijkstraPass(p: Prepared, reverse: boolean, anchor: number, context: string): Int32Array {
const n = p.n;
const dist = new Int32Array(n).fill(UNVISITED);
const heap = new IntHeap();
dist[anchor] = 0;
heap.push(0, anchor);
const row = reverse ? p.revRow : p.fwdRow;
const list = reverse ? p.revEdge : p.fwdEdge;
while (heap.size > 0) {
const d = heap.peekKey();
const u = heap.pop();
if (d > dist[u]!) continue; // stale heap entry (lazy deletion)
const end = row[u + 1]!;
for (let r = row[u]!; r < end; r++) {
const e = list[r]!;
if ((p.flags[e]! & 1) !== 0) continue; // oneway-reverse: never relaxed, either direction
const v = reverse ? p.source[e]! : p.target[e]!;
const nd = d + p.qWeight[e]!;
if (nd < dist[v]!) {
if (nd > LANDMARK_MAX_QUANTA) {
fail(
context,
`quantized distance ${nd} exceeds ${LANDMARK_MAX_QUANTA} (raise landmarkQuantumS)`,
);
}
dist[v] = nd;
heap.push(nd, v);
}
}
}
return dist;
}
/** u16 finish: unreachable -> the 0xFFFF sentinel; finite labels are
* already <= LANDMARK_MAX_QUANTA (the pass throws beyond it). */
function finish(d: number): number {
return d === UNVISITED ? LANDMARK_SENTINEL : d;
}
/* ————— public surface ————— */
/**
* Deterministic farthest-point anchor selection over the ĉ metric
* (D-02's locked approach family). Candidates are nodes with stored
* out-degree >= 2 (blocked mirrors included — the node still sits on the
* road). Greed: the first anchor is the first candidate by node id; each
* next anchor maximizes the per-node min-distance to the already-picked
* set (ties -> lower node id). Each pick's FORWARD pass is reused to
* maintain that min-distance field (one pass per pick, not per candidate).
*
* `opts.coverageFraction` (0..1): stop early once the fraction of nodes
* within finite ĉ-distance of the picked set reaches it — a documented
* knob that may return FEWER than `count` anchors. Without it, exactly
* `min(count, candidates)` anchors return (early-stop when candidates run
* out). Returns an empty array when no degree>=2 candidate exists.
*/
export function farthestPointAnchors(
input: LandmarkInput,
count: number,
opts?: { coverageFraction?: number },
): Uint32Array {
const context = "farthestPointAnchors";
if (!Number.isInteger(count) || count < 0) fail(context, "count must be a non-negative integer");
const coverageFraction = opts?.coverageFraction;
if (
coverageFraction !== undefined &&
(!Number.isFinite(coverageFraction) || coverageFraction < 0 || coverageFraction > 1)
) {
fail(context, "coverageFraction must be a number in [0, 1]");
}
const p = prepare(input);
const n = p.n;
const capacity = Math.min(count, n);
const anchors = new Uint32Array(capacity);
if (capacity === 0) return anchors;
const degree = (u: number): number => p.fwdRow[u + 1]! - p.fwdRow[u]!;
let first = -1;
for (let u = 0; u < n; u++) {
if (degree(u) >= 2) {
first = u;
break;
}
}
if (first < 0) return anchors.slice(0, 0); // no junction-ish candidate exists
const minDist = new Int32Array(n).fill(UNVISITED);
const isPicked = new Uint8Array(n);
let picked = 0;
let current = first;
// Each loop turn folds ONE pass (the just-picked anchor's forward pass)
// into the min-distance field, then greedily picks the next anchor.
let dist = dijkstraPass(p, false, current, context);
for (;;) {
let covered = 0;
for (let v = 0; v < n; v++) {
const d = dist[v]!;
if (d < minDist[v]!) minDist[v] = d;
if (minDist[v]! !== UNVISITED) covered++;
}
anchors[picked] = current;
isPicked[current] = 1;
picked += 1;
if (picked === capacity) break;
if (coverageFraction !== undefined && covered / n >= coverageFraction) break;
let next = -1;
let best = -1;
for (let v = 0; v < n; v++) {
const d = minDist[v]!;
if (d === UNVISITED || isPicked[v] !== 0 || degree(v) < 2) continue;
if (d > best) {
best = d;
next = v; // strict > keeps the FIRST (lowest-id) node on ties
}
}
if (next < 0) break; // candidates exhausted
current = next;
dist = dijkstraPass(p, false, current, context);
}
return anchors.slice(0, picked);
}
/**
* Integer ĉ-metric Dijkstras per anchor. Returns row-major tables of
* L·N u16 cells ([ℓ*N + v]):
* to[ℓ*N+v] = d̂(v, ℓ) — the REVERSED-graph pass from ℓ
* from[ℓ*N+v] = d̂(ℓ, v) — the forward pass from ℓ
* with 0xFFFF wherever v cannot reach (resp. be reached from) ℓ. Throws
* on an out-of-range anchor or any label beyond LANDMARK_MAX_QUANTA.
*/
export function landmarkTables(
input: LandmarkInput,
anchors: Uint32Array,
): { to: Uint16Array; from: Uint16Array } {
const context = "landmarkTables";
const p = prepare(input);
const n = p.n;
const l = anchors.length;
for (let i = 0; i < l; i++) {
const a = anchors[i]!;
if (!Number.isInteger(a) || a < 0 || a >= n) fail(context, `anchor ${a} out of range`);
}
const to = new Uint16Array(l * n);
const from = new Uint16Array(l * n);
for (let i = 0; i < l; i++) {
const anchor = anchors[i]!;
const distTo = dijkstraPass(p, true, anchor, context);
const distFrom = dijkstraPass(p, false, anchor, context);
const base = i * n;
for (let v = 0; v < n; v++) {
to[base + v] = finish(distTo[v]!);
from[base + v] = finish(distFrom[v]!);
}
}
return { to, from };
}
/**
* Per-anchor coverage audit: the fraction of nodes whose to-row cell is
* NOT the sentinel — the build script's per-anchor assert feed
* (research Pattern 3, layer 1).
*/
export function landmarkCoverage(to: Uint16Array, anchors: Uint32Array, nodeCount: number): number[] {
const coverage: number[] = [];
for (let i = 0; i < anchors.length; i++) {
let reached = 0;
const base = i * nodeCount;
for (let v = 0; v < nodeCount; v++) {
if (to[base + v] !== LANDMARK_SENTINEL) reached++;
}
coverage.push(reached / nodeCount);
}
return coverage;
}