@mark1russell7/highways 0.5.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (56) hide show
  1. package/README.md +47 -2
  2. package/build/index.d.ts +1489 -63
  3. package/build/index.d.ts.map +1 -1
  4. package/build/index.js +10139 -3356
  5. package/build/index.js.map +1 -1
  6. package/package.json +11 -2
  7. package/src/bench/exact/index.ts +218 -11
  8. package/src/bench/exact/optima-exact-v1-bounds.json +14409 -0
  9. package/src/bench/exact/optima-exact-v1.json +4809 -0
  10. package/src/bench/exact/solve.ts +129 -0
  11. package/src/bench/screen/activation.ts +3 -0
  12. package/src/bench/screen/bounds.ts +140 -0
  13. package/src/bench/screen/compare.ts +26 -0
  14. package/src/bench/screen/exact-set.ts +100 -0
  15. package/src/bench/screen/exercise.ts +290 -0
  16. package/src/bench/screen/forms.ts +14 -0
  17. package/src/bench/screen/gap.ts +316 -0
  18. package/src/bench/screen/hash.ts +7 -76
  19. package/src/bench/screen/index.ts +6 -0
  20. package/src/bench/screen/measures.ts +71 -1
  21. package/src/bench/screen/portfolio-pick.ts +20 -0
  22. package/src/bench/screen/screen.ts +50 -2
  23. package/src/bench/screen/set.ts +3 -2
  24. package/src/highways/areas/index.ts +160 -0
  25. package/src/highways/confluent/index.ts +154 -0
  26. package/src/highways/confluent/k23.test-util.ts +8 -0
  27. package/src/highways/consensus/index.ts +129 -0
  28. package/src/highways/crossbars/index.ts +184 -0
  29. package/src/highways/cubu/index.ts +26 -0
  30. package/src/highways/divided/index.ts +111 -0
  31. package/src/highways/fdeb/index.ts +193 -0
  32. package/src/highways/frechet/index.ts +232 -0
  33. package/src/highways/hierarchy/index.ts +140 -0
  34. package/src/highways/index.ts +64 -0
  35. package/src/highways/kdeeb/index.ts +355 -0
  36. package/src/highways/link-communities/index.ts +189 -0
  37. package/src/highways/mesh/index.ts +209 -0
  38. package/src/highways/mlseb/index.ts +204 -0
  39. package/src/highways/p-hub/index.ts +248 -0
  40. package/src/highways/power/index.ts +117 -0
  41. package/src/highways/resistance/index.ts +159 -0
  42. package/src/highways/sbeb/index.ts +303 -0
  43. package/src/highways/scale-space/index.ts +135 -0
  44. package/src/highways/spectral/index.ts +391 -0
  45. package/src/highways/territories/index.ts +150 -0
  46. package/src/highways/trace.ts +153 -0
  47. package/src/index.ts +3 -0
  48. package/src/portfolio/bands.ts +38 -0
  49. package/src/portfolio/fit.ts +41 -0
  50. package/src/portfolio/index.ts +6 -0
  51. package/src/portfolio/key.ts +98 -0
  52. package/src/portfolio/members.ts +171 -0
  53. package/src/portfolio/router.ts +133 -0
  54. package/src/portfolio/sets.ts +40 -0
  55. package/src/strategies/builtins.ts +222 -1
  56. package/src/strategies/schemas.ts +280 -1
package/package.json CHANGED
@@ -9,7 +9,7 @@
9
9
  "directory": "packages/core"
10
10
  },
11
11
  "homepage": "https://mark1russell7.github.io/highways/",
12
- "version": "0.5.0",
12
+ "version": "0.6.0",
13
13
  "type": "module",
14
14
  "main": "./build/index.js",
15
15
  "types": "./build/index.d.ts",
@@ -28,9 +28,11 @@
28
28
  "publishConfig": {
29
29
  "access": "public"
30
30
  },
31
+ "dependencies": {
32
+ "@mark1russell7/layout-contract": "^0.2.7"
33
+ },
31
34
  "devDependencies": {
32
35
  "@mark1russell7/cue": "github:mark1russell7/cue",
33
- "@mark1russell7/layout-contract": "github:mark1russell7/layout-contract#69933a61b0270c5915b82ab563d24908ade708af&path:/packages/layout-contract",
34
36
  "@types/node": "^26.6.4",
35
37
  "highs": "^1.15.4",
36
38
  "typescript": "^7.0.2",
@@ -45,6 +47,13 @@
45
47
  "profile": "node scripts/profile.ts",
46
48
  "levels": "node scripts/levels.ts",
47
49
  "screen": "node scripts/screen.ts",
50
+ "exact": "node scripts/exact.ts",
51
+ "oracle": "node scripts/oracle.ts",
52
+ "portfolio": "node scripts/portfolio.ts",
53
+ "portfolio:time": "node scripts/portfolio-time.ts",
54
+ "portfolio:sets": "node scripts/portfolio-sets.ts",
55
+ "portfolio:api": "node scripts/portfolio-api-report.ts",
56
+ "bounds": "node scripts/bounds.ts",
48
57
  "screen:inputs": "node scripts/screen-inputs.ts",
49
58
  "test:watch": "vitest",
50
59
  "typecheck": "tsc -p tsconfig.json && tsc -p tsconfig.test.json"
@@ -1,7 +1,7 @@
1
- import { dist, type Point } from "../../geom.ts";
1
+ import { dist, polylineLength, type Point } from "../../geom.ts";
2
2
  import { edgePlugs, sharedStretches, type KeyedWay } from "../../highways/stretches.ts";
3
3
  import type { HighwayPlan } from "../../highways/model.ts";
4
- import type { Graph } from "../../model.ts";
4
+ import type { Edge, Graph, Route } from "../../model.ts";
5
5
  import { byCollection } from "../../routers/trunks.ts";
6
6
  import { WayFinder, type WayOptions } from "../../ways.ts";
7
7
 
@@ -28,6 +28,8 @@ export interface ExactModel {
28
28
  readonly rows: number;
29
29
  /** The number of edges in the model. An edge without a way in the stretch limit is not in the model. */
30
30
  readonly edges: number;
31
+ /** The length of the arc or the link of each binary variable, by its name. A name `x…` is an arc of an edge, and a name `y…` is a link of a key. */
32
+ readonly lengths: ReadonlyMap<string, number>;
31
33
  /** This function gives the plan of a solution. `value` gives the value of a variable by its name. */
32
34
  decode(value: (name: string) => number): HighwayPlan;
33
35
  }
@@ -54,13 +56,19 @@ function distances(adj: readonly (readonly { to: number; len: number }[])[], sta
54
56
  }
55
57
  }
56
58
 
57
- /**
58
- * This function gives the model of fixed-charge network design (Magnanti and Wong 1984) for the edges of a graph (HWY-040).
59
- * The network is the corner graph, plus the point out of each plug. Each link costs its length once for each key that uses it.
60
- * Each edge goes one way between the points out of its two plugs. The way is `stretch` times its shortest way or shorter.
61
- * The optimum has the least ink for the stretch limit. The ways of the solution give highways with `sharedStretches`.
62
- */
63
- export function exactModel(graph: Graph, o: Readonly<ExactOptions>): ExactModel {
59
+ /** The network of the exact model: the corner graph, plus the point out of each plug of each edge. */
60
+ interface ExactNetwork {
61
+ readonly points: readonly Point[];
62
+ readonly links: readonly { readonly u: number; readonly v: number; readonly len: number }[];
63
+ readonly adj: readonly (readonly { to: number; len: number }[])[];
64
+ /** The two end points of each edge: the points out of its plugs. */
65
+ readonly ends: readonly { readonly edge: Edge; readonly a: number; readonly b: number }[];
66
+ /** The distance from a plug to the point out of it. */
67
+ readonly margin: number;
68
+ }
69
+
70
+ /** This function makes the network of the exact model for the edges of a graph. */
71
+ function exactNetwork(graph: Graph, o: Readonly<WayOptions>): ExactNetwork {
64
72
  const finder = new WayFinder(graph.nodes, { ...(o.margin === undefined ? {} : { margin: o.margin }), ...(o.k === undefined ? {} : { k: o.k }) });
65
73
  const corners = finder.corners;
66
74
  const k = o.k ?? 8;
@@ -79,7 +87,7 @@ export function exactModel(graph: Graph, o: Readonly<ExactOptions>): ExactModel
79
87
  };
80
88
  corners.points.forEach((p, i) => ids.set(`${p.x},${p.y}`, i));
81
89
  const plugs = edgePlugs(graph);
82
- const ends = plugs.map(({ edge, ps, pt }) => ({ edge: edge.id, key: o.rule === "all" ? "*" : (byCollection(edge) ?? `\u0000${edge.id}`), a: nodeOf(finder.out(ps)), b: nodeOf(finder.out(pt)) }));
90
+ const ends = plugs.map(({ edge, ps, pt }) => ({ edge, a: nodeOf(finder.out(ps)), b: nodeOf(finder.out(pt)) }));
83
91
 
84
92
  // The links: each corner to its k nearest visible corners, as in the corner graph, and each end point to its 2k nearest visible corners.
85
93
  const linkIds = new Map<string, number>();
@@ -99,12 +107,25 @@ export function exactModel(graph: Graph, o: Readonly<ExactOptions>): ExactModel
99
107
  adj[l.u]!.push({ to: l.v, len: l.len });
100
108
  adj[l.v]!.push({ to: l.u, len: l.len });
101
109
  }
110
+ return { points, links, adj, ends, margin: finder.margin };
111
+ }
112
+
113
+ /**
114
+ * This function gives the model of fixed-charge network design (Magnanti and Wong 1984) for the edges of a graph (HWY-040).
115
+ * The network is the corner graph, plus the point out of each plug. Each link costs its length once for each key that uses it.
116
+ * Each edge goes one way between the points out of its two plugs. The way is `stretch` times its shortest way or shorter.
117
+ * The optimum has the least ink for the stretch limit. The ways of the solution give highways with `sharedStretches`.
118
+ */
119
+ export function exactModel(graph: Graph, o: Readonly<ExactOptions>): ExactModel {
120
+ const { points, links, adj, ends: outs } = exactNetwork(graph, o);
121
+ const ends = outs.map((e) => ({ edge: e.edge.id, key: o.rule === "all" ? "*" : (byCollection(e.edge) ?? `\u0000${e.edge.id}`), a: e.a, b: e.b }));
102
122
 
103
123
  // The arcs of each edge: the arcs on some way within the stretch limit.
104
124
  const eps = o.epsilon ?? 0.001;
105
125
  const objective: string[] = [];
106
126
  const rows: string[] = [];
107
127
  const binaries: string[] = [];
128
+ const lengths = new Map<string, number>();
108
129
  const keys = [...new Set(ends.map((e) => e.key))];
109
130
  const used = new Map<string, Set<number>>();
110
131
  const arcsOf: { e: number; arcs: Arc[] }[] = [];
@@ -134,6 +155,7 @@ export function exactModel(graph: Graph, o: Readonly<ExactOptions>): ExactModel
134
155
  for (const a of arcs) {
135
156
  const x = `x${ei}_${a.link}_${a.from < a.to ? 0 : 1}`;
136
157
  binaries.push(x);
158
+ lengths.set(x, a.len);
137
159
  objective.push(`${(eps * a.len).toFixed(6)} ${x}`);
138
160
  term(a.from, `+ ${x}`);
139
161
  term(a.to, `- ${x}`);
@@ -156,6 +178,7 @@ export function exactModel(graph: Graph, o: Readonly<ExactOptions>): ExactModel
156
178
  for (const li of used.get(key) ?? []) {
157
179
  const y = `y${ki}_${li}`;
158
180
  binaries.push(y);
181
+ lengths.set(y, links[li]!.len);
159
182
  objective.push(`${links[li]!.len.toFixed(6)} ${y}`);
160
183
  }
161
184
  });
@@ -193,5 +216,189 @@ export function exactModel(graph: Graph, o: Readonly<ExactOptions>): ExactModel
193
216
  }
194
217
  return sharedStretches(ways, { minSize: 2 });
195
218
  };
196
- return { lp, columns: binaries.length, rows: rows.length, edges: arcsOf.length, decode };
219
+ return { lp, columns: binaries.length, rows: rows.length, edges: arcsOf.length, lengths, decode };
220
+ }
221
+
222
+ /** A binary heap of node indexes by a key, smallest first. A node can be in the heap more than one time. The search skips an old entry. */
223
+ class NodeHeap {
224
+ #key: Float64Array = new Float64Array(64);
225
+ #node: Int32Array = new Int32Array(64);
226
+ size = 0;
227
+
228
+ push(key: number, node: number): void {
229
+ if (this.size === this.#key.length) {
230
+ const k = new Float64Array(2 * this.size);
231
+ k.set(this.#key);
232
+ this.#key = k;
233
+ const n = new Int32Array(2 * this.size);
234
+ n.set(this.#node);
235
+ this.#node = n;
236
+ }
237
+ const keys = this.#key;
238
+ const nodes = this.#node;
239
+ let i = this.size++;
240
+ while (i > 0) {
241
+ const up = (i - 1) >> 1;
242
+ if (keys[up]! <= key) break;
243
+ keys[i] = keys[up]!;
244
+ nodes[i] = nodes[up]!;
245
+ i = up;
246
+ }
247
+ keys[i] = key;
248
+ nodes[i] = node;
249
+ }
250
+
251
+ /** This method removes the node with the smallest key, and gives it. */
252
+ pop(): number {
253
+ const keys = this.#key;
254
+ const nodes = this.#node;
255
+ const top = nodes[0]!;
256
+ const n = --this.size;
257
+ const key = keys[n]!;
258
+ const node = nodes[n]!;
259
+ let i = 0;
260
+ for (;;) {
261
+ let c = 2 * i + 1;
262
+ if (c >= n) break;
263
+ if (c + 1 < n && keys[c + 1]! < keys[c]!) c++;
264
+ if (keys[c]! >= key) break;
265
+ keys[i] = keys[c]!;
266
+ nodes[i] = nodes[c]!;
267
+ i = c;
268
+ }
269
+ keys[i] = key;
270
+ nodes[i] = node;
271
+ return top;
272
+ }
273
+ }
274
+
275
+ /**
276
+ * This function gives the shortest distance from `start` to each node of `targets` (A*). The search stops when it has each target.
277
+ * The guide is the straight distance to the nearest target. Each link is a straight line, so the guide is not above the true distance at any time.
278
+ * Thus the distances are the distances of Dijkstra. Only the order of the search changes.
279
+ * A target that the search cannot reach gets `Infinity`.
280
+ */
281
+ function distancesTo(points: readonly Point[], adj: readonly (readonly { to: number; len: number }[])[], start: number, targets: readonly number[]): Map<number, number> {
282
+ const d = new Float64Array(adj.length).fill(Infinity);
283
+ const done = new Uint8Array(adj.length);
284
+ const goal = new Set(targets);
285
+ const ends = targets.map((t) => points[t]!);
286
+ const guide = (v: number): number => {
287
+ let h = Infinity;
288
+ for (const t of ends) h = Math.min(h, dist(points[v]!, t));
289
+ return h;
290
+ };
291
+ const heap = new NodeHeap();
292
+ d[start] = 0;
293
+ heap.push(guide(start), start);
294
+ let left = goal.size;
295
+ while (heap.size > 0 && left > 0) {
296
+ const u = heap.pop();
297
+ if (done[u]) continue;
298
+ done[u] = 1;
299
+ if (goal.has(u)) left--;
300
+ for (const { to, len } of adj[u]!) {
301
+ const nd = d[u]! + len;
302
+ if (nd < d[to]!) {
303
+ d[to] = nd;
304
+ // A rounding error can make the guide a little too large. Then a node opens again, so its distance stays the least.
305
+ done[to] = 0;
306
+ heap.push(nd + guide(to), to);
307
+ }
308
+ }
309
+ }
310
+ return new Map(targets.map((t) => [t, d[t]!]));
311
+ }
312
+
313
+ /**
314
+ * This function gives the shortest distance in the network between the two end points of each edge, by the pair `a,b` of the end points.
315
+ * The distance does not depend on the direction, so one search serves each edge between the same two points.
316
+ * An end point of many edges starts one search to all its other end points. The function starts the searches from the end points with the most edges first.
317
+ */
318
+ function pairDistances(network: ExactNetwork): Map<string, number> {
319
+ const pairKey = (a: number, b: number): string => (a < b ? `${a},${b}` : `${b},${a}`);
320
+ const partners = new Map<number, Set<number>>();
321
+ const open = new Set<string>();
322
+ for (const e of network.ends) {
323
+ if (e.a === e.b) continue;
324
+ const k = pairKey(e.a, e.b);
325
+ if (open.has(k)) continue;
326
+ open.add(k);
327
+ for (const [u, v] of [
328
+ [e.a, e.b],
329
+ [e.b, e.a],
330
+ ] as const) {
331
+ if (!partners.has(u)) partners.set(u, new Set());
332
+ partners.get(u)!.add(v);
333
+ }
334
+ }
335
+ const out = new Map<string, number>();
336
+ const order = [...partners.keys()].sort((u, v) => partners.get(v)!.size - partners.get(u)!.size || u - v);
337
+ for (const u of order) {
338
+ const targets = [...partners.get(u)!].filter((v) => open.has(pairKey(u, v)));
339
+ if (targets.length === 0) continue;
340
+ for (const [v, len] of distancesTo(network.points, network.adj, u, targets)) {
341
+ const k = pairKey(u, v);
342
+ out.set(k, len);
343
+ open.delete(k);
344
+ }
345
+ }
346
+ return out;
347
+ }
348
+
349
+ /** This function gives the function of the stretch of each route of the graph, from the network and the shortest distance of each edge. */
350
+ function stretchesOf(network: ExactNetwork, best: (e: ExactNetwork["ends"][number]) => number): (routes: readonly Route[]) => Map<string, number> {
351
+ return (routes) => {
352
+ const byEdge = new Map(routes.map((r) => [r.edge, r]));
353
+ const out = new Map<string, number>();
354
+ for (const e of network.ends) {
355
+ const r = byEdge.get(e.edge.id);
356
+ if (!r || r.points.length < 2 || e.a === e.b) continue;
357
+ const b = best(e);
358
+ if (!Number.isFinite(b) || b <= 1) continue;
359
+ out.set(e.edge.id, (polylineLength(r.points) - 2 * network.margin) / b);
360
+ }
361
+ return out;
362
+ };
363
+ }
364
+
365
+ /**
366
+ * This function gives the stretch of each route as the exact model counts it, by the edge id.
367
+ * The stretch of a route is its length without its two plug ends, divided by the shortest way in the network of the model.
368
+ * The function builds the network one time. The result function then measures each list of routes of the graph, so a portfolio pays for the network once.
369
+ * The first call finds the shortest ways with one A* search for each end point with edges (PORTFOLIO-SPEED). The later calls use them again.
370
+ * An edge without a route or without a way in the network has no value.
371
+ */
372
+ export function exactStretches(graph: Graph, o: Readonly<WayOptions> = {}): (routes: readonly Route[]) => Map<string, number> {
373
+ const network = exactNetwork(graph, o);
374
+ let best: Map<string, number> | undefined;
375
+ return stretchesOf(network, (e) => {
376
+ best ??= pairDistances(network);
377
+ return best.get(e.a < e.b ? `${e.a},${e.b}` : `${e.b},${e.a}`) ?? Infinity;
378
+ });
379
+ }
380
+
381
+ /**
382
+ * This function gives the same values as `exactStretches` with the search of the first version. That search is one full Dijkstra with a scan, from the first end point of each edge.
383
+ * It is slow on large boards. The tests and the check `portfolio:time --check-stretch` compare the two functions.
384
+ */
385
+ export function exactStretchesReference(graph: Graph, o: Readonly<WayOptions> = {}): (routes: readonly Route[]) => Map<string, number> {
386
+ const network = exactNetwork(graph, o);
387
+ const best = new Map<number, Float64Array>();
388
+ const from = (a: number): Float64Array => {
389
+ if (!best.has(a)) best.set(a, distances(network.adj, a));
390
+ return best.get(a)!;
391
+ };
392
+ return stretchesOf(network, (e) => from(e.a)[e.b]!);
393
+ }
394
+
395
+ /**
396
+ * This function gives the largest stretch of routes as the exact model counts it (`exactStretches`).
397
+ * The cap of the model is a limit on this value.
398
+ * An edge without a route or without a way in the network does not count. Without such an edge, the value is 1.
399
+ */
400
+ export function exactStretch(graph: Graph, routes: readonly Route[], o: Readonly<WayOptions> = {}): number {
401
+ let max = 1;
402
+ for (const v of exactStretches(graph, o)(routes).values()) max = Math.max(max, v);
403
+ return max;
197
404
  }