@mark1russell7/highways 0.4.2 → 0.5.1
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 +59 -2
- package/build/index.d.ts +2333 -193
- package/build/index.d.ts.map +1 -1
- package/build/index.js +13042 -2334
- package/build/index.js.map +1 -1
- package/package.json +12 -1
- package/src/bench/exact/index.ts +258 -0
- 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/measures/index.ts +1 -0
- package/src/bench/measures/lanes.ts +304 -0
- package/src/bench/measures/levels.ts +86 -2
- 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 +25 -5
- 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 +89 -1
- package/src/bench/screen/portfolio-pick.ts +20 -0
- package/src/bench/screen/screen.ts +78 -2
- package/src/bench/screen/set.ts +3 -2
- package/src/channels.ts +56 -0
- 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/diffusion/index.ts +103 -0
- package/src/highways/divided/index.ts +111 -0
- package/src/highways/edge-paths/index.ts +155 -0
- package/src/highways/fdeb/index.ts +193 -0
- package/src/highways/fixtures.test-util.ts +71 -0
- package/src/highways/flow-clusters/index.ts +143 -0
- package/src/highways/frechet/index.ts +232 -0
- package/src/highways/growth/index.ts +148 -0
- package/src/highways/hdbscan/index.ts +250 -0
- package/src/highways/hierarchy/index.ts +140 -0
- package/src/highways/hough/index.ts +182 -0
- package/src/highways/hub-arcs/index.ts +181 -0
- package/src/highways/index.ts +79 -0
- package/src/highways/kdeeb/index.ts +355 -0
- package/src/highways/lanes/index.ts +189 -0
- package/src/highways/link-communities/index.ts +189 -0
- package/src/highways/mesh/index.ts +209 -0
- package/src/highways/mingle/index.ts +171 -0
- package/src/highways/mlseb/index.ts +204 -0
- package/src/highways/nested/index.ts +143 -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/ridges/index.ts +328 -0
- package/src/highways/sbeb/index.ts +303 -0
- package/src/highways/scale-space/index.ts +135 -0
- package/src/highways/snap.ts +110 -0
- package/src/highways/soft-keys/index.ts +114 -0
- package/src/highways/spectral/index.ts +391 -0
- package/src/highways/spiral/index.ts +205 -0
- package/src/highways/territories/index.ts +150 -0
- package/src/highways/trace.ts +153 -0
- package/src/highways/traclus/index.ts +296 -0
- package/src/index.ts +2 -0
- package/src/portfolio/bands.ts +33 -0
- package/src/portfolio/fit.ts +29 -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 +28 -0
- package/src/routers/shared.ts +56 -3
- package/src/strategies/builtins.ts +376 -1
- package/src/strategies/schemas.ts +460 -1
- package/src/visibility.ts +5 -0
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.5.1",
|
|
13
13
|
"type": "module",
|
|
14
14
|
"main": "./build/index.js",
|
|
15
15
|
"types": "./build/index.d.ts",
|
|
@@ -28,9 +28,13 @@
|
|
|
28
28
|
"publishConfig": {
|
|
29
29
|
"access": "public"
|
|
30
30
|
},
|
|
31
|
+
"dependencies": {
|
|
32
|
+
"@mark1russell7/layout-contract": "^0.2.6"
|
|
33
|
+
},
|
|
31
34
|
"devDependencies": {
|
|
32
35
|
"@mark1russell7/cue": "github:mark1russell7/cue",
|
|
33
36
|
"@types/node": "^26.6.4",
|
|
37
|
+
"highs": "^1.15.4",
|
|
34
38
|
"typescript": "^7.0.2",
|
|
35
39
|
"vitest": "^5.0.3"
|
|
36
40
|
},
|
|
@@ -43,6 +47,13 @@
|
|
|
43
47
|
"profile": "node scripts/profile.ts",
|
|
44
48
|
"levels": "node scripts/levels.ts",
|
|
45
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",
|
|
46
57
|
"screen:inputs": "node scripts/screen-inputs.ts",
|
|
47
58
|
"test:watch": "vitest",
|
|
48
59
|
"typecheck": "tsc -p tsconfig.json && tsc -p tsconfig.test.json"
|
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
import { dist, polylineLength, type Point } from "../../geom.ts";
|
|
2
|
+
import { edgePlugs, sharedStretches, type KeyedWay } from "../../highways/stretches.ts";
|
|
3
|
+
import type { HighwayPlan } from "../../highways/model.ts";
|
|
4
|
+
import type { Edge, Graph, Route } from "../../model.ts";
|
|
5
|
+
import { byCollection } from "../../routers/trunks.ts";
|
|
6
|
+
import { WayFinder, type WayOptions } from "../../ways.ts";
|
|
7
|
+
|
|
8
|
+
/** The edges that can share a link: all the edges, or only the edges of one collection, as with the rule `byCollection`. */
|
|
9
|
+
export type ExactRule = "all" | "collection";
|
|
10
|
+
|
|
11
|
+
/** The options of the exact model of fixed-charge network design (HWY-040). */
|
|
12
|
+
export interface ExactOptions extends WayOptions {
|
|
13
|
+
/** The way of each edge is this value times its shortest way or shorter. */
|
|
14
|
+
readonly stretch: number;
|
|
15
|
+
/** The edges that can share a link. */
|
|
16
|
+
readonly rule: ExactRule;
|
|
17
|
+
/** The cost of each unit of length of each way, as a share of the cost of the ink. A small value removes needless loops. The default is 0.001. */
|
|
18
|
+
readonly epsilon?: number;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** A model of fixed-charge network design as the text of a mixed integer program (CPLEX LP format), and the function that reads its solution. */
|
|
22
|
+
export interface ExactModel {
|
|
23
|
+
/** The text of the program in the CPLEX LP format. */
|
|
24
|
+
readonly lp: string;
|
|
25
|
+
/** The number of binary variables. */
|
|
26
|
+
readonly columns: number;
|
|
27
|
+
/** The number of constraints. */
|
|
28
|
+
readonly rows: number;
|
|
29
|
+
/** The number of edges in the model. An edge without a way in the stretch limit is not in the model. */
|
|
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>;
|
|
33
|
+
/** This function gives the plan of a solution. `value` gives the value of a variable by its name. */
|
|
34
|
+
decode(value: (name: string) => number): HighwayPlan;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
interface Arc {
|
|
38
|
+
readonly from: number;
|
|
39
|
+
readonly to: number;
|
|
40
|
+
readonly link: number;
|
|
41
|
+
readonly len: number;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** This function gives the shortest distance from `start` to each node, with Dijkstra on an adjacency list. */
|
|
45
|
+
function distances(adj: readonly (readonly { to: number; len: number }[])[], start: number): Float64Array {
|
|
46
|
+
const d = new Float64Array(adj.length).fill(Infinity);
|
|
47
|
+
d[start] = 0;
|
|
48
|
+
const done = new Uint8Array(adj.length);
|
|
49
|
+
// The boards of the model are small, so a scan for the smallest open node is fast enough.
|
|
50
|
+
for (;;) {
|
|
51
|
+
let u = -1;
|
|
52
|
+
for (let i = 0; i < adj.length; i++) if (!done[i] && d[i]! < Infinity && (u < 0 || d[i]! < d[u]!)) u = i;
|
|
53
|
+
if (u < 0) return d;
|
|
54
|
+
done[u] = 1;
|
|
55
|
+
for (const { to, len } of adj[u]!) if (d[u]! + len < d[to]!) d[to] = d[u]! + len;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
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 {
|
|
72
|
+
const finder = new WayFinder(graph.nodes, { ...(o.margin === undefined ? {} : { margin: o.margin }), ...(o.k === undefined ? {} : { k: o.k }) });
|
|
73
|
+
const corners = finder.corners;
|
|
74
|
+
const k = o.k ?? 8;
|
|
75
|
+
const pad = finder.margin / 2;
|
|
76
|
+
const points: Point[] = [...corners.points];
|
|
77
|
+
const ids = new Map<string, number>();
|
|
78
|
+
const nodeOf = (p: Point): number => {
|
|
79
|
+
const key = `${p.x},${p.y}`;
|
|
80
|
+
let id = ids.get(key);
|
|
81
|
+
if (id === undefined) {
|
|
82
|
+
id = points.length;
|
|
83
|
+
points.push(p);
|
|
84
|
+
ids.set(key, id);
|
|
85
|
+
}
|
|
86
|
+
return id;
|
|
87
|
+
};
|
|
88
|
+
corners.points.forEach((p, i) => ids.set(`${p.x},${p.y}`, i));
|
|
89
|
+
const plugs = edgePlugs(graph);
|
|
90
|
+
const ends = plugs.map(({ edge, ps, pt }) => ({ edge, a: nodeOf(finder.out(ps)), b: nodeOf(finder.out(pt)) }));
|
|
91
|
+
|
|
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.
|
|
93
|
+
const linkIds = new Map<string, number>();
|
|
94
|
+
const links: { u: number; v: number; len: number }[] = [];
|
|
95
|
+
const addLink = (u: number, v: number): void => {
|
|
96
|
+
if (u === v) return;
|
|
97
|
+
const key = u < v ? `${u},${v}` : `${v},${u}`;
|
|
98
|
+
if (linkIds.has(key)) return;
|
|
99
|
+
linkIds.set(key, links.length);
|
|
100
|
+
links.push({ u: Math.min(u, v), v: Math.max(u, v), len: dist(points[u]!, points[v]!) });
|
|
101
|
+
};
|
|
102
|
+
for (let i = 0; i < corners.points.length; i++) for (const j of corners.nearestVisible(corners.points[i]!, k, i)) addLink(i, j);
|
|
103
|
+
for (let t = corners.points.length; t < points.length; t++) for (const j of corners.nearestVisible(points[t]!, 2 * k)) addLink(t, j);
|
|
104
|
+
for (const e of ends) if (finder.obstacles.clear(points[e.a]!, points[e.b]!, pad)) addLink(e.a, e.b);
|
|
105
|
+
const adj: { to: number; len: number }[][] = points.map(() => []);
|
|
106
|
+
for (const l of links) {
|
|
107
|
+
adj[l.u]!.push({ to: l.v, len: l.len });
|
|
108
|
+
adj[l.v]!.push({ to: l.u, len: l.len });
|
|
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 }));
|
|
122
|
+
|
|
123
|
+
// The arcs of each edge: the arcs on some way within the stretch limit.
|
|
124
|
+
const eps = o.epsilon ?? 0.001;
|
|
125
|
+
const objective: string[] = [];
|
|
126
|
+
const rows: string[] = [];
|
|
127
|
+
const binaries: string[] = [];
|
|
128
|
+
const lengths = new Map<string, number>();
|
|
129
|
+
const keys = [...new Set(ends.map((e) => e.key))];
|
|
130
|
+
const used = new Map<string, Set<number>>();
|
|
131
|
+
const arcsOf: { e: number; arcs: Arc[] }[] = [];
|
|
132
|
+
ends.forEach((e, ei) => {
|
|
133
|
+
const da = distances(adj, e.a);
|
|
134
|
+
const db = distances(adj, e.b);
|
|
135
|
+
const best = da[e.b]!;
|
|
136
|
+
if (!Number.isFinite(best) || e.a === e.b) return;
|
|
137
|
+
const limit = o.stretch * best + 1e-9;
|
|
138
|
+
const arcs: Arc[] = [];
|
|
139
|
+
links.forEach((l, li) => {
|
|
140
|
+
if (da[l.u]! + l.len + db[l.v]! <= limit) arcs.push({ from: l.u, to: l.v, link: li, len: l.len });
|
|
141
|
+
if (da[l.v]! + l.len + db[l.u]! <= limit) arcs.push({ from: l.v, to: l.u, link: li, len: l.len });
|
|
142
|
+
});
|
|
143
|
+
arcsOf.push({ e: ei, arcs });
|
|
144
|
+
let set = used.get(e.key);
|
|
145
|
+
if (!set) used.set(e.key, (set = new Set()));
|
|
146
|
+
for (const a of arcs) set.add(a.link);
|
|
147
|
+
// Flow: one unit leaves `a` and arrives at `b`.
|
|
148
|
+
const flow = new Map<number, string[]>();
|
|
149
|
+
const term = (n: number, s: string): void => {
|
|
150
|
+
const list = flow.get(n);
|
|
151
|
+
if (list) list.push(s);
|
|
152
|
+
else flow.set(n, [s]);
|
|
153
|
+
};
|
|
154
|
+
const stretch: string[] = [];
|
|
155
|
+
for (const a of arcs) {
|
|
156
|
+
const x = `x${ei}_${a.link}_${a.from < a.to ? 0 : 1}`;
|
|
157
|
+
binaries.push(x);
|
|
158
|
+
lengths.set(x, a.len);
|
|
159
|
+
objective.push(`${(eps * a.len).toFixed(6)} ${x}`);
|
|
160
|
+
term(a.from, `+ ${x}`);
|
|
161
|
+
term(a.to, `- ${x}`);
|
|
162
|
+
stretch.push(`${a.len.toFixed(6)} ${x}`);
|
|
163
|
+
}
|
|
164
|
+
for (const [n, list] of flow) rows.push(`f${ei}_${n}: ${list.join(" ")} = ${n === e.a ? 1 : n === e.b ? -1 : 0}`);
|
|
165
|
+
rows.push(`s${ei}: ${stretch.join(" + ")} <= ${limit.toFixed(6)}`);
|
|
166
|
+
// An arc needs its link open for the key of the edge.
|
|
167
|
+
const byLink = new Map<number, string[]>();
|
|
168
|
+
for (const a of arcs) {
|
|
169
|
+
const list = byLink.get(a.link);
|
|
170
|
+
const x = `x${ei}_${a.link}_${a.from < a.to ? 0 : 1}`;
|
|
171
|
+
if (list) list.push(x);
|
|
172
|
+
else byLink.set(a.link, [x]);
|
|
173
|
+
}
|
|
174
|
+
const ki = keys.indexOf(e.key);
|
|
175
|
+
for (const [li, xs] of byLink) rows.push(`o${ei}_${li}: ${xs.join(" + ")} - y${ki}_${li} <= 0`);
|
|
176
|
+
});
|
|
177
|
+
keys.forEach((key, ki) => {
|
|
178
|
+
for (const li of used.get(key) ?? []) {
|
|
179
|
+
const y = `y${ki}_${li}`;
|
|
180
|
+
binaries.push(y);
|
|
181
|
+
lengths.set(y, links[li]!.len);
|
|
182
|
+
objective.push(`${links[li]!.len.toFixed(6)} ${y}`);
|
|
183
|
+
}
|
|
184
|
+
});
|
|
185
|
+
const wrap = (terms: readonly string[]): string => {
|
|
186
|
+
const out: string[] = [];
|
|
187
|
+
for (let i = 0; i < terms.length; i += 8) out.push(terms.slice(i, i + 8).join(" + "));
|
|
188
|
+
return out.join("\n + ");
|
|
189
|
+
};
|
|
190
|
+
const lp = [
|
|
191
|
+
"Minimize",
|
|
192
|
+
` obj: ${objective.length > 0 ? wrap(objective) : "0 dummy"}`,
|
|
193
|
+
"Subject To",
|
|
194
|
+
...rows.map((r) => ` ${r.replace(/((?:\S+\s+){24})/g, "$1\n ")}`),
|
|
195
|
+
"Binary",
|
|
196
|
+
...Array.from({ length: Math.ceil(binaries.length / 8) }, (_, i) => ` ${binaries.slice(i * 8, i * 8 + 8).join(" ")}`),
|
|
197
|
+
"End",
|
|
198
|
+
"",
|
|
199
|
+
].join("\n");
|
|
200
|
+
|
|
201
|
+
const decode = (value: (name: string) => number): HighwayPlan => {
|
|
202
|
+
const ways: KeyedWay[] = [];
|
|
203
|
+
for (const { e: ei, arcs } of arcsOf) {
|
|
204
|
+
const e = ends[ei]!;
|
|
205
|
+
const next = new Map<number, number>();
|
|
206
|
+
for (const a of arcs) if (value(`x${ei}_${a.link}_${a.from < a.to ? 0 : 1}`) > 0.5) next.set(a.from, a.to);
|
|
207
|
+
const seq: Point[] = [points[e.a]!];
|
|
208
|
+
let at = e.a;
|
|
209
|
+
for (let guard = 0; at !== e.b && guard <= points.length; guard++) {
|
|
210
|
+
const to = next.get(at);
|
|
211
|
+
if (to === undefined) break;
|
|
212
|
+
seq.push(points[to]!);
|
|
213
|
+
at = to;
|
|
214
|
+
}
|
|
215
|
+
if (at === e.b) ways.push({ edge: e.edge, key: e.key, points: seq });
|
|
216
|
+
}
|
|
217
|
+
return sharedStretches(ways, { minSize: 2 });
|
|
218
|
+
};
|
|
219
|
+
return { lp, columns: binaries.length, rows: rows.length, edges: arcsOf.length, lengths, decode };
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* This function gives the stretch of each route as the exact model counts it, by the edge id.
|
|
224
|
+
* The stretch of a route is its length without its two plug ends, divided by the shortest way in the network of the model.
|
|
225
|
+
* 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.
|
|
226
|
+
* An edge without a route or without a way in the network has no value.
|
|
227
|
+
*/
|
|
228
|
+
export function exactStretches(graph: Graph, o: Readonly<WayOptions> = {}): (routes: readonly Route[]) => Map<string, number> {
|
|
229
|
+
const { adj, ends, margin } = exactNetwork(graph, o);
|
|
230
|
+
const best = new Map<number, Float64Array>();
|
|
231
|
+
const from = (a: number): Float64Array => {
|
|
232
|
+
if (!best.has(a)) best.set(a, distances(adj, a));
|
|
233
|
+
return best.get(a)!;
|
|
234
|
+
};
|
|
235
|
+
return (routes) => {
|
|
236
|
+
const byEdge = new Map(routes.map((r) => [r.edge, r]));
|
|
237
|
+
const out = new Map<string, number>();
|
|
238
|
+
for (const e of ends) {
|
|
239
|
+
const r = byEdge.get(e.edge.id);
|
|
240
|
+
if (!r || r.points.length < 2 || e.a === e.b) continue;
|
|
241
|
+
const b = from(e.a)[e.b]!;
|
|
242
|
+
if (!Number.isFinite(b) || b <= 1) continue;
|
|
243
|
+
out.set(e.edge.id, (polylineLength(r.points) - 2 * margin) / b);
|
|
244
|
+
}
|
|
245
|
+
return out;
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* This function gives the largest stretch of routes as the exact model counts it (`exactStretches`).
|
|
251
|
+
* The cap of the model is a limit on this value.
|
|
252
|
+
* An edge without a route or without a way in the network does not count. Without such an edge, the value is 1.
|
|
253
|
+
*/
|
|
254
|
+
export function exactStretch(graph: Graph, routes: readonly Route[], o: Readonly<WayOptions> = {}): number {
|
|
255
|
+
let max = 1;
|
|
256
|
+
for (const v of exactStretches(graph, o)(routes).values()) max = Math.max(max, v);
|
|
257
|
+
return max;
|
|
258
|
+
}
|