@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.
- package/README.md +47 -2
- package/build/index.d.ts +1489 -63
- package/build/index.d.ts.map +1 -1
- package/build/index.js +10139 -3356
- package/build/index.js.map +1 -1
- package/package.json +11 -2
- package/src/bench/exact/index.ts +218 -11
- package/src/bench/exact/optima-exact-v1-bounds.json +14409 -0
- package/src/bench/exact/optima-exact-v1.json +4809 -0
- package/src/bench/exact/solve.ts +129 -0
- package/src/bench/screen/activation.ts +3 -0
- package/src/bench/screen/bounds.ts +140 -0
- package/src/bench/screen/compare.ts +26 -0
- package/src/bench/screen/exact-set.ts +100 -0
- package/src/bench/screen/exercise.ts +290 -0
- package/src/bench/screen/forms.ts +14 -0
- package/src/bench/screen/gap.ts +316 -0
- package/src/bench/screen/hash.ts +7 -76
- package/src/bench/screen/index.ts +6 -0
- package/src/bench/screen/measures.ts +71 -1
- package/src/bench/screen/portfolio-pick.ts +20 -0
- package/src/bench/screen/screen.ts +50 -2
- package/src/bench/screen/set.ts +3 -2
- package/src/highways/areas/index.ts +160 -0
- package/src/highways/confluent/index.ts +154 -0
- package/src/highways/confluent/k23.test-util.ts +8 -0
- package/src/highways/consensus/index.ts +129 -0
- package/src/highways/crossbars/index.ts +184 -0
- package/src/highways/cubu/index.ts +26 -0
- package/src/highways/divided/index.ts +111 -0
- package/src/highways/fdeb/index.ts +193 -0
- package/src/highways/frechet/index.ts +232 -0
- package/src/highways/hierarchy/index.ts +140 -0
- package/src/highways/index.ts +64 -0
- package/src/highways/kdeeb/index.ts +355 -0
- package/src/highways/link-communities/index.ts +189 -0
- package/src/highways/mesh/index.ts +209 -0
- package/src/highways/mlseb/index.ts +204 -0
- package/src/highways/p-hub/index.ts +248 -0
- package/src/highways/power/index.ts +117 -0
- package/src/highways/resistance/index.ts +159 -0
- package/src/highways/sbeb/index.ts +303 -0
- package/src/highways/scale-space/index.ts +135 -0
- package/src/highways/spectral/index.ts +391 -0
- package/src/highways/territories/index.ts +150 -0
- package/src/highways/trace.ts +153 -0
- package/src/index.ts +3 -0
- package/src/portfolio/bands.ts +38 -0
- package/src/portfolio/fit.ts +41 -0
- package/src/portfolio/index.ts +6 -0
- package/src/portfolio/key.ts +98 -0
- package/src/portfolio/members.ts +171 -0
- package/src/portfolio/router.ts +133 -0
- package/src/portfolio/sets.ts +40 -0
- package/src/strategies/builtins.ts +222 -1
- 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.
|
|
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"
|
package/src/bench/exact/index.ts
CHANGED
|
@@ -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
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
*/
|
|
63
|
-
|
|
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
|
|
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
|
}
|