@mark1russell7/highways 0.9.4 → 0.9.6

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.
@@ -0,0 +1,294 @@
1
+ import { checkProperties } from "../../bench/properties.ts";
2
+ import { measureQuality } from "../../bench/quality.ts";
3
+ import { rng } from "../../fixtures.ts";
4
+ import { segmentsCross, type Box, type Point } from "../../geom.ts";
5
+ import { outerGraph, type Graph, type GroupRule, type Result, type Route, type Router } from "../../model.ts";
6
+ import { defineStrategy, type Strategy } from "../../strategies/model.ts";
7
+ import type { OptionSchema } from "../../strategies/options.ts";
8
+ import { sharedRouter } from "../shared.ts";
9
+ import { byCollection } from "../trunks.ts";
10
+
11
+ /** The options of the router `adam` (RULE-020: Adam with a seeded sample of pairs). */
12
+ export interface AdamOptions {
13
+ /** The first step size `eta0`, in units of `clearance`, from 0.01 to 1. The step size falls to a tenth of it at the last step. The default is 0.1. */
14
+ readonly eta: number;
15
+ /** The number of other legs that each leg samples at each step, from 4 to 32. The default is 8. */
16
+ readonly samples: number;
17
+ /** The number of steps: the budget of the search, from 100 to 2000. The default is 200. */
18
+ readonly steps: number;
19
+ /** The seed of the samples, a whole number from 0. The same seed gives the same routes. The default is 1. */
20
+ readonly seed: number;
21
+ /** A leg nearer than this distance to another leg or to a box gets a push, from 4 to 32. The default is 8. */
22
+ readonly clearance: number;
23
+ }
24
+
25
+ /** The default options of the router `adam`. */
26
+ export const ADAM_DEFAULTS: Readonly<AdamOptions> = Object.freeze({ eta: 0.1, samples: 8, steps: 200, seed: 1, clearance: 8 });
27
+
28
+ /** The declarations of the options of the router `adam` (RULE-020). */
29
+ export const ADAM_SCHEMA: OptionSchema<AdamOptions> = {
30
+ eta: { type: "number", min: 0.01, max: 1, step: 0.01, marks: [0.01, 0.1, 1], default: 0.1, describe: "The first step size, in units of the clearance. It falls to a tenth at the last step." },
31
+ samples: { type: "integer", min: 4, max: 32, step: 1, marks: [4, 8, 16, 32], default: 8, describe: "The number of other legs that each leg samples at each step." },
32
+ steps: { type: "integer", min: 100, max: 2000, step: 100, marks: [100, 200, 500, 2000], default: 200, describe: "The number of steps: the budget of the search." },
33
+ seed: { type: "integer", min: 0, max: 1_000_000, step: 1, marks: [1, 2, 3], default: 1, describe: "The seed of the samples. The same seed gives the same routes." },
34
+ clearance: { type: "number", min: 4, max: 32, step: 1, marks: [4, 8, 16, 32], default: 8, describe: "A leg nearer than this distance to another leg or to a box gets a push." },
35
+ };
36
+
37
+ /** The moment factors of Adam (Kingma and Ba 2015), as the record gives them. */
38
+ const BETA1 = 0.9;
39
+ const BETA2 = 0.999;
40
+ const EPS = 1e-8;
41
+
42
+ /** The weight of the push from a box, against the push from a leg. */
43
+ const BOX_WEIGHT = 4;
44
+
45
+ /** The weight of the term that holds a leg near its first place. */
46
+ const HOLD = 0.05;
47
+
48
+ /** A free leg of a route. No end of it is an end of its route or a point of another route. No run or trunk holds it. */
49
+ interface Leg {
50
+ readonly route: number;
51
+ /** The index of the first point of the leg in its route. */
52
+ readonly k: number;
53
+ /** The unit normal of the leg. The leg moves along it. */
54
+ readonly nx: number;
55
+ readonly ny: number;
56
+ /** The boxes near the leg. */
57
+ readonly boxes: readonly Box[];
58
+ }
59
+
60
+ /** This function gives the point of the segment `ab` nearest to `p`. */
61
+ function nearestOn(p: Point, a: Point, b: Point): Point {
62
+ const dx = b.x - a.x;
63
+ const dy = b.y - a.y;
64
+ const l2 = dx * dx + dy * dy;
65
+ const t = l2 > 0 ? Math.max(0, Math.min(1, ((p.x - a.x) * dx + (p.y - a.y) * dy) / l2)) : 0;
66
+ return { x: a.x + t * dx, y: a.y + t * dy };
67
+ }
68
+
69
+ /** This function gives the point of a box nearest to `p`. A point in the box gives the nearest point of its border. */
70
+ function nearestOfBox(p: Point, b: Box): Point {
71
+ const qx = Math.max(b.x, Math.min(b.x + b.w, p.x));
72
+ const qy = Math.max(b.y, Math.min(b.y + b.h, p.y));
73
+ if (qx !== p.x || qy !== p.y) return { x: qx, y: qy };
74
+ const sides = [p.x - b.x, b.x + b.w - p.x, p.y - b.y, b.y + b.h - p.y];
75
+ const m = Math.min(...sides);
76
+ if (m === sides[0]) return { x: b.x, y: p.y };
77
+ if (m === sides[1]) return { x: b.x + b.w, y: p.y };
78
+ if (m === sides[2]) return { x: p.x, y: b.y };
79
+ return { x: p.x, y: b.y + b.h };
80
+ }
81
+
82
+ /** This function gives the crossings of the route `a` with itself and with the routes of `others`. It skips the route with the index `skip`. */
83
+ function crossingsOf(a: readonly Point[], others: readonly (readonly Point[])[], skip: number): number {
84
+ const [ax0, ay0, ax1, ay1] = boundsOf(a);
85
+ let n = 0;
86
+ for (let i = 1; i < a.length; i++) for (let k = i + 2; k < a.length; k++) if (segmentsCross(a[i - 1]!, a[i]!, a[k - 1]!, a[k]!)) n++;
87
+ others.forEach((b, j) => {
88
+ if (j === skip) return;
89
+ const [bx0, by0, bx1, by1] = boundsOf(b);
90
+ if (bx0 > ax1 || bx1 < ax0 || by0 > ay1 || by1 < ay0) return;
91
+ for (let i = 1; i < a.length; i++) for (let k = 1; k < b.length; k++) if (segmentsCross(a[i - 1]!, a[i]!, b[k - 1]!, b[k]!)) n++;
92
+ });
93
+ return n;
94
+ }
95
+
96
+ /** The bounds of each polyline, once for each polyline object. */
97
+ const BOUNDS = new WeakMap<readonly Point[], readonly [number, number, number, number]>();
98
+
99
+ /** This function gives the bounds of a polyline: the least x and y, and the largest x and y. */
100
+ function boundsOf(p: readonly Point[]): readonly [number, number, number, number] {
101
+ let b = BOUNDS.get(p);
102
+ if (b) return b;
103
+ let x0 = Infinity;
104
+ let y0 = Infinity;
105
+ let x1 = -Infinity;
106
+ let y1 = -Infinity;
107
+ for (const q of p) {
108
+ x0 = Math.min(x0, q.x);
109
+ y0 = Math.min(y0, q.y);
110
+ x1 = Math.max(x1, q.x);
111
+ y1 = Math.max(y1, q.y);
112
+ }
113
+ b = [x0, y0, x1, y1];
114
+ BOUNDS.set(p, b);
115
+ return b;
116
+ }
117
+
118
+ /**
119
+ * This function moves the free legs of the routes of `result` with Adam (RULE-020, after Kingma and Ba 2015, and the SGD layouts of Zheng, Pawar and Goodman 2019).
120
+ * A leg is free when its two points are inside its route, and no other route, run or trunk holds them. The variable of a leg is a shift along its normal.
121
+ * The energy of a leg is `Σ (1 - d / c)²` over a seeded sample of `samples` other legs nearer than `c`. The near boxes add the same term times 4.
122
+ * A term `0.05 × (t / c)²` holds each leg near its first place. The step size falls from `eta` to a tenth of it, with only `+`, `*` and `/`.
123
+ * A moved route goes back to its first points when it has more crossings than before, or a break of a hard rule.
124
+ * When the output still has more crossings than the routes of `shared`, it is the routes of `shared`.
125
+ */
126
+ export function adamRoutes(input: Graph, result: Result, options: Partial<AdamOptions> = {}): Result {
127
+ const o = { ...ADAM_DEFAULTS, ...options };
128
+ const c = Math.max(1e-6, o.clearance);
129
+ const graph = outerGraph(input);
130
+ const first = result.routes.map((r) => r.points);
131
+ const pinned = new Set<string>();
132
+ const keyOf = (p: Point): string => `${p.x},${p.y}`;
133
+ for (const run of result.runs ?? []) for (const p of run.points) pinned.add(keyOf(p));
134
+ for (const b of result.bundles ?? []) for (const p of b.trunk) pinned.add(keyOf(p));
135
+ // A point of two routes or more stays, so two routes that share a leg keep it as one drawn leg.
136
+ const owners = new Map<string, number>();
137
+ first.forEach((pts, route) => {
138
+ for (const p of pts) {
139
+ const k = keyOf(p);
140
+ const o = owners.get(k);
141
+ if (o === undefined) owners.set(k, route);
142
+ else if (o !== route) pinned.add(k);
143
+ }
144
+ });
145
+ const boxes = graph.nodes.map((n) => n.box);
146
+ const legs: Leg[] = [];
147
+ first.forEach((pts, route) => {
148
+ for (let k = 1; k + 2 < pts.length; k++) {
149
+ const a = pts[k]!;
150
+ const b = pts[k + 1]!;
151
+ if (pinned.has(keyOf(a)) || pinned.has(keyOf(b))) continue;
152
+ const dx = b.x - a.x;
153
+ const dy = b.y - a.y;
154
+ const len = Math.sqrt(dx * dx + dy * dy);
155
+ if (len <= 1e-9) continue;
156
+ const reach = 3 * c;
157
+ const near = boxes.filter((bx) => Math.min(a.x, b.x) - reach <= bx.x + bx.w && Math.max(a.x, b.x) + reach >= bx.x && Math.min(a.y, b.y) - reach <= bx.y + bx.h && Math.max(a.y, b.y) + reach >= bx.y);
158
+ legs.push({ route, k, nx: -dy / len, ny: dx / len, boxes: near });
159
+ }
160
+ });
161
+ if (legs.length === 0) return result;
162
+ const shift = new Float64Array(legs.length);
163
+ const m1 = new Float64Array(legs.length);
164
+ const m2 = new Float64Array(legs.length);
165
+ // The legs that touch each point of each route, so a point moves with each of its legs.
166
+ const legAt = new Map<string, number[]>();
167
+ legs.forEach((l, i) => {
168
+ for (const k of [l.k, l.k + 1]) {
169
+ const key = `${l.route}:${k}`;
170
+ const list = legAt.get(key);
171
+ if (list) list.push(i);
172
+ else legAt.set(key, [i]);
173
+ }
174
+ });
175
+ const pointOf = (route: number, k: number): Point => {
176
+ const p = first[route]![k]!;
177
+ const list = legAt.get(`${route}:${k}`);
178
+ if (!list) return p;
179
+ let x = p.x;
180
+ let y = p.y;
181
+ for (const i of list) {
182
+ x += shift[i]! * legs[i]!.nx;
183
+ y += shift[i]! * legs[i]!.ny;
184
+ }
185
+ return { x, y };
186
+ };
187
+ // Each leg of each route, as the pairs of the sample: the free legs and the fixed legs.
188
+ const all: { route: number; k: number }[] = [];
189
+ first.forEach((pts, route) => {
190
+ for (let k = 0; k + 1 < pts.length; k++) all.push({ route, k });
191
+ });
192
+ const random = rng(Math.max(0, Math.floor(o.seed)));
193
+ const steps = Math.max(1, Math.floor(o.steps));
194
+ const count = Math.max(1, Math.floor(o.samples));
195
+ let b1 = 1;
196
+ let b2 = 1;
197
+ for (let t = 0; t < steps; t++) {
198
+ b1 *= BETA1;
199
+ b2 *= BETA2;
200
+ const eta = (o.eta * c) / (1 + (9 * t) / Math.max(1, steps - 1));
201
+ const grads = new Float64Array(legs.length);
202
+ legs.forEach((l, i) => {
203
+ const a = pointOf(l.route, l.k);
204
+ const b = pointOf(l.route, l.k + 1);
205
+ const mid = { x: (a.x + b.x) / 2, y: (a.y + b.y) / 2 };
206
+ let g = (2 * HOLD * shift[i]!) / (c * c);
207
+ for (let s = 0; s < count; s++) {
208
+ const other = all[Math.floor(random() * all.length)]!;
209
+ if (other.route === l.route) continue;
210
+ const q = nearestOn(mid, pointOf(other.route, other.k), pointOf(other.route, other.k + 1));
211
+ const dx = mid.x - q.x;
212
+ const dy = mid.y - q.y;
213
+ const d = Math.sqrt(dx * dx + dy * dy);
214
+ if (d >= c || d <= 1e-9) continue;
215
+ // E = (1 - d / c)², so dE/dt = -2 (1 - d / c) / c × (n · (mid - q)) / d.
216
+ g += ((-2 * (1 - d / c)) / c) * ((l.nx * dx + l.ny * dy) / d);
217
+ }
218
+ for (const bx of l.boxes) {
219
+ for (const p of [a, mid, b]) {
220
+ const q = nearestOfBox(p, bx);
221
+ const inside = p.x >= bx.x && p.x <= bx.x + bx.w && p.y >= bx.y && p.y <= bx.y + bx.h;
222
+ const dx = (inside ? q.x - p.x : p.x - q.x) || 0;
223
+ const dy = (inside ? q.y - p.y : p.y - q.y) || 0;
224
+ const d0 = Math.sqrt(dx * dx + dy * dy);
225
+ if (d0 <= 1e-9) continue;
226
+ const d = inside ? -d0 : d0;
227
+ if (d >= c) continue;
228
+ g += ((BOX_WEIGHT * -2 * (1 - d / c)) / c) * ((l.nx * dx + l.ny * dy) / d0);
229
+ }
230
+ }
231
+ grads[i] = g;
232
+ });
233
+ for (let i = 0; i < legs.length; i++) {
234
+ const g = grads[i]!;
235
+ m1[i] = BETA1 * m1[i]! + (1 - BETA1) * g;
236
+ m2[i] = BETA2 * m2[i]! + (1 - BETA2) * g * g;
237
+ const mh = m1[i]! / (1 - b1);
238
+ const vh = m2[i]! / (1 - b2);
239
+ shift[i] = Math.max(-2 * c, Math.min(2 * c, shift[i]! - (eta * mh) / (Math.sqrt(vh) + EPS)));
240
+ }
241
+ }
242
+ const moved = new Set(legs.map((l) => l.route));
243
+ const points = first.map((pts, route) => (moved.has(route) ? pts.map((_, k) => pointOf(route, k)) : pts));
244
+ // A moved route with more crossings than its first points, against the other routes as they are, goes back to its first points.
245
+ for (let round = 0; round < 8; round++) {
246
+ let back = 0;
247
+ for (const route of moved) {
248
+ if (points[route] === first[route] || crossingsOf(points[route]!, points, route) <= crossingsOf(first[route]!, points, route)) continue;
249
+ points[route] = first[route]!;
250
+ back++;
251
+ }
252
+ if (back === 0) break;
253
+ }
254
+ const build = (pts: readonly (readonly Point[])[]): Result => ({ ...result, routes: result.routes.map((r, i): Route => (pts[i] === r.points ? r : { ...r, points: pts[i]! })) });
255
+ let out = build(points);
256
+ for (let round = 0; round < 3; round++) {
257
+ const faulty = new Set(checkProperties(input, out, 1_000_000).map((v) => v.edge));
258
+ const fix = result.routes.map((r, i) => (faulty.has(r.edge) && points[i] !== first[i] ? i : -1)).filter((i) => i >= 0);
259
+ if (fix.length === 0) break;
260
+ for (const i of fix) points[i] = first[i]!;
261
+ out = build(points);
262
+ }
263
+ // The output keeps the routes of `shared` when it has a break of a hard rule, or more crossings, after the fixes.
264
+ if (checkProperties(input, out, 1).length > checkProperties(input, result, 1).length) return result;
265
+ return measureQuality(input, out).crossings > measureQuality(input, result).crossings ? result : out;
266
+ }
267
+
268
+ /**
269
+ * This function makes the router `adam` (RULE-020). It is the router `shared`, then the steps of Adam of `adamRoutes`.
270
+ * The runs and the bundles of `shared` stay, because no point of a run or of a trunk moves.
271
+ */
272
+ export function adamRouter(options: Partial<AdamOptions> & { readonly name: string; readonly rule?: GroupRule; readonly maxNodes?: number }): Router {
273
+ const o = { ...ADAM_DEFAULTS, ...options };
274
+ const base = sharedRouter({ name: o.name, ...(o.rule === undefined ? {} : { rule: o.rule }), ...(o.maxNodes === undefined ? {} : { maxNodes: o.maxNodes }) });
275
+ return {
276
+ name: o.name,
277
+ ...(o.maxNodes === undefined ? {} : { maxNodes: o.maxNodes }),
278
+ route: (graph: Graph): Result => adamRoutes(graph, base.route(graph), o),
279
+ };
280
+ }
281
+
282
+ /**
283
+ * The router strategy `adam` (RULE-020), with the rule `byCollection` of `shared`. It is not a built-in router, so the benchmark and the hashes of `ROUTERS` do not change.
284
+ * A screen module or a consumer adds it to a registry.
285
+ */
286
+ export const adamStrategy: Strategy<"router", AdamOptions> = defineStrategy({
287
+ step: "router",
288
+ name: "adam",
289
+ describe: "Adam with a seeded sample of pairs (RULE-020). The free legs of the routes of `shared` move along their normals, away from near legs and boxes.",
290
+ defaults: { ...ADAM_DEFAULTS },
291
+ schema: ADAM_SCHEMA,
292
+ maxNodes: 10_000,
293
+ run: (graph: Graph, o: Readonly<AdamOptions>): Result => adamRouter({ ...o, name: "adam", rule: byCollection }).route(graph),
294
+ });
@@ -0,0 +1,191 @@
1
+ import type { Graph, GroupRule, Result, Router } from "../../model.ts";
2
+ import { defineStrategy, type Strategy } from "../../strategies/model.ts";
3
+ import type { OptionSchema } from "../../strategies/options.ts";
4
+ import { sharedRouter, type SharedSearch, type Way } from "../shared.ts";
5
+ import { byCollection } from "../trunks.ts";
6
+
7
+ /** The options of the router `canals` (NAT-004: canalization feeder trees to hubs). */
8
+ export interface CanalOptions {
9
+ /** The exponent `mu` of the flux in the growth of a conductance, from 1 to 3 in steps of 0.5. A value above 1 gives a tree. The default is 2. */
10
+ readonly mu: number;
11
+ /** The largest number of updates of the conductances, from 30 to 150. The updates stop when the ways do not change for 3 updates. The default is 60. */
12
+ readonly steps: number;
13
+ /** The decay `r` of a conductance, from 0.5 to 2. The default is 1. */
14
+ readonly decay: number;
15
+ /** A hub has this number of edges of one key or more, from 2 to 8. The default is 3. */
16
+ readonly minMembers: number;
17
+ }
18
+
19
+ /** The default options of the router `canals`. */
20
+ export const CANAL_DEFAULTS: Readonly<CanalOptions> = Object.freeze({ mu: 2, steps: 60, decay: 1, minMembers: 3 });
21
+
22
+ /** The declarations of the options of the router `canals` (NAT-004). */
23
+ export const CANAL_SCHEMA: OptionSchema<CanalOptions> = {
24
+ mu: { type: "number", min: 1, max: 3, step: 0.5, marks: [1, 2, 3], default: 2, describe: "The exponent of the flux in the growth of a conductance. A value above 1 gives a tree." },
25
+ steps: { type: "integer", min: 30, max: 150, step: 10, marks: [30, 60, 150], default: 60, describe: "The largest number of updates of the conductances." },
26
+ decay: { type: "number", min: 0.5, max: 2, step: 0.25, marks: [0.5, 1, 2], default: 1, describe: "The decay of a conductance." },
27
+ minMembers: { type: "integer", min: 2, max: 8, step: 1, marks: [2, 3, 5, 8], default: 3, describe: "A hub has this number of edges of one key or more." },
28
+ };
29
+
30
+ /** The time step of one update of the conductances. */
31
+ const DT = 0.5;
32
+
33
+ /** The gain of a conductance in the cost of a link: a link with the conductance `D` costs `d / (1 + GAIN × D)`. */
34
+ const GAIN = 4;
35
+
36
+ /** The cost share of a link that another way of the key uses, as the default `discount` of `shared`. */
37
+ const SHARE = 0.5;
38
+
39
+ /** The updates with no change of a way, after which the updates stop. */
40
+ const PATIENCE = 3;
41
+
42
+ /**
43
+ * This function gives `x^mu` for `x` from 0 to 1 and `mu` in steps of 0.5, with only `*` and `Math.sqrt`.
44
+ * These operations are exact to the last bit on each platform, so a decision does not depend on the engine.
45
+ */
46
+ export function halfPower(x: number, mu: number): number {
47
+ const twice = Math.max(2, Math.min(6, Math.round(mu * 2)));
48
+ let out = 1;
49
+ for (let i = 0; i < Math.floor(twice / 2); i++) out *= x;
50
+ return twice % 2 === 1 ? out * Math.sqrt(x) : out;
51
+ }
52
+
53
+ /** A hub: a node, the key of its edges, and the ways of those edges. */
54
+ export interface CanalHub {
55
+ readonly node: string;
56
+ readonly key: string;
57
+ readonly ways: readonly Way[];
58
+ }
59
+
60
+ /**
61
+ * This function gives the hubs of the ways. In each key, the node with the most ends is a hub when it has `minMembers` edges or more.
62
+ * Its edges leave the key, and the next hub of the key comes from the edges that stay. A tie goes to the smaller node id.
63
+ * An edge with no key is in no hub.
64
+ */
65
+ export function canalHubs(ways: readonly Way[], minMembers: number): CanalHub[] {
66
+ const byKey = new Map<string, Way[]>();
67
+ for (const w of ways) {
68
+ if (!w.found || w.ids.length < 2 || w.key.startsWith("\u0000")) continue;
69
+ const list = byKey.get(w.key);
70
+ if (list) list.push(w);
71
+ else byKey.set(w.key, [w]);
72
+ }
73
+ const out: CanalHub[] = [];
74
+ for (const key of [...byKey.keys()].sort()) {
75
+ let left = byKey.get(key)!;
76
+ for (;;) {
77
+ const count = new Map<string, number>();
78
+ for (const w of left) for (const n of new Set([w.edge.source.node, w.edge.target.node])) count.set(n, (count.get(n) ?? 0) + 1);
79
+ let node: string | undefined;
80
+ let most = 0;
81
+ for (const [n, c] of count) if (c > most || (c === most && node !== undefined && n < node)) [node, most] = [n, c];
82
+ if (node === undefined || most < Math.max(2, minMembers)) break;
83
+ const hub = node;
84
+ const members = left.filter((w) => w.edge.source.node === hub || w.edge.target.node === hub);
85
+ out.push({ node: hub, key, ways: [...members].sort((a, b) => (a.edge.id < b.edge.id ? -1 : a.edge.id > b.edge.id ? 1 : 0)) });
86
+ left = left.filter((w) => !members.includes(w));
87
+ }
88
+ }
89
+ return out;
90
+ }
91
+
92
+ /**
93
+ * This function grows a feeder tree to each hub (NAT-004, after the canalization of Sachs 1981 and Mitchison 1980).
94
+ * Each member edge is a source of one unit of flux, and the hub is the sink. The flux of a link is the share of the members whose ways use it.
95
+ * At each update, each conductance changes by `DT × (flux^mu - decay × D)`, and each member finds its way again with the cost `d / (1 + GAIN × D)`.
96
+ * A link that another way of the key uses costs half, as in `shared`.
97
+ * With `mu` above 1, a link with more flux grows faster, so the ways gather into one tree. The updates stop after `steps`, or when no way changes for 3 updates.
98
+ */
99
+ export function canalWays(ways: readonly Way[], search: SharedSearch, options: Partial<CanalOptions> = {}): void {
100
+ const o = { ...CANAL_DEFAULTS, ...options };
101
+ const n = search.corners;
102
+ const linksOf = (w: Way): number[] => {
103
+ const out = new Set<number>();
104
+ for (let i = 1; i < w.ids.length; i++) if (w.ids[i - 1]! < n && w.ids[i]! < n) out.add(search.link(w.ids[i - 1]!, w.ids[i]!));
105
+ return [...out];
106
+ };
107
+ // The number of ways of each key on each link, so a link that another way of the key uses keeps the share of `shared`.
108
+ const usage = new Map<string, Map<number, number>>();
109
+ const use = (w: Way, add: number): void => {
110
+ let m = usage.get(w.key);
111
+ if (!m) usage.set(w.key, (m = new Map()));
112
+ for (const k of linksOf(w)) m.set(k, (m.get(k) ?? 0) + add);
113
+ };
114
+ for (const w of ways) if (w.found) use(w, 1);
115
+ for (const hub of canalHubs(ways, Math.floor(o.minMembers))) {
116
+ const members = hub.ways;
117
+ const used = usage.get(hub.key)!;
118
+ const conductance = new Map<number, number>();
119
+ let calm = 0;
120
+ for (let step = 0; step < Math.max(1, Math.floor(o.steps)); step++) {
121
+ const flux = new Map<number, number>();
122
+ for (const w of members) {
123
+ const seen = new Set<number>();
124
+ for (let i = 1; i < w.ids.length; i++) {
125
+ const a = w.ids[i - 1]!;
126
+ const b = w.ids[i]!;
127
+ if (a >= n || b >= n) continue;
128
+ const k = search.link(a, b);
129
+ if (seen.has(k)) continue;
130
+ seen.add(k);
131
+ flux.set(k, (flux.get(k) ?? 0) + 1);
132
+ }
133
+ }
134
+ for (const k of new Set([...conductance.keys(), ...flux.keys()])) {
135
+ const phi = (flux.get(k) ?? 0) / members.length;
136
+ const d = conductance.get(k) ?? 0;
137
+ const next = d + DT * (halfPower(phi, o.mu) - o.decay * d);
138
+ if (next > 1e-6) conductance.set(k, next);
139
+ else conductance.delete(k);
140
+ }
141
+ let top = 0;
142
+ for (const d of conductance.values()) top = Math.max(top, d);
143
+ const floor = SHARE / (1 + GAIN * top);
144
+ let changed = 0;
145
+ for (const w of members) {
146
+ use(w, -1);
147
+ const next = search.find(
148
+ w,
149
+ (i, j, d) => {
150
+ const k = search.link(i, j);
151
+ return ((used.get(k) ?? 0) > 0 ? SHARE * d : d) / (1 + GAIN * (conductance.get(k) ?? 0));
152
+ },
153
+ floor,
154
+ );
155
+ if (next) {
156
+ if (next.ids.length !== w.ids.length || next.ids.some((id, i) => id !== w.ids[i])) changed++;
157
+ w.seq = next.seq;
158
+ w.ids = next.ids;
159
+ }
160
+ use(w, 1);
161
+ }
162
+ calm = changed === 0 ? calm + 1 : 0;
163
+ if (calm >= PATIENCE) break;
164
+ }
165
+ }
166
+ }
167
+
168
+ /**
169
+ * This function makes the router `canals` (NAT-004). It is the router `shared`, then the feeder trees of `canalWays` to the hubs of each key.
170
+ * The runs and the bundles come from the ways after the trees.
171
+ */
172
+ export function canalRouter(options: Partial<CanalOptions> & { readonly name: string; readonly rule?: GroupRule; readonly maxNodes?: number }): Router {
173
+ const o = { ...CANAL_DEFAULTS, ...options };
174
+ return sharedRouter({ name: o.name, ...(o.rule === undefined ? {} : { rule: o.rule }), ...(o.maxNodes === undefined ? {} : { maxNodes: o.maxNodes }) }, undefined, {
175
+ refine: (ways, search) => canalWays(ways, search, o),
176
+ });
177
+ }
178
+
179
+ /**
180
+ * The router strategy `canals` (NAT-004), with the rule `byCollection` of `shared`. It is not a built-in router, so the benchmark and the hashes of `ROUTERS` do not change.
181
+ * A screen module or a consumer adds it to a registry.
182
+ */
183
+ export const canalStrategy: Strategy<"router", CanalOptions> = defineStrategy({
184
+ step: "router",
185
+ name: "canals",
186
+ describe: "Canalization feeder trees (NAT-004). The members of a hub are sources of flux, and the links with more flux get cheaper until the ways make one tree.",
187
+ defaults: { ...CANAL_DEFAULTS },
188
+ schema: CANAL_SCHEMA,
189
+ maxNodes: 10_000,
190
+ run: (graph: Graph, o: Readonly<CanalOptions>): Result => canalRouter({ ...o, name: "canals", rule: byCollection }).route(graph),
191
+ });
@@ -13,6 +13,9 @@ export { frameRoutes, type FrameOptions, type InnerMode, type Outside } from "./
13
13
  export { GUTTER_BEND_COST, gutterRoutes, type GutterEdge, type GutterOptions } from "./gutter.ts";
14
14
  export { sharedRouter, type ShareCost, type SharedHooks, type SharedNegotiation, type SharedOptions, type SharedSearch, type ShareMerge, type ShareOrder } from "./shared.ts";
15
15
  export { ANNEALING_DEFAULTS, ANNEALING_SCHEMA, annealingRouter, annealingStrategy, annealWays, type AnnealingOptions } from "./annealing/index.ts";
16
+ export { CANAL_DEFAULTS, CANAL_SCHEMA, canalHubs, canalRouter, canalStrategy, canalWays, halfPower, type CanalHub, type CanalOptions } from "./canals/index.ts";
17
+ export { ADAM_DEFAULTS, ADAM_SCHEMA, adamRouter, adamRoutes, adamStrategy, type AdamOptions } from "./adam/index.ts";
18
+ export { REROUTE_DEFAULTS, REROUTE_SCHEMA, rerouteRouter, rerouteStrategy, rerouteWays, turnsOf, type RerouteOptions } from "./reroute/index.ts";
16
19
  export { TIGHT_DEFAULTS, TIGHT_FIRSTS, TIGHT_SCHEMA, TIGHT_TIES, tightRouter, tightStrategy, waySlacks, type TightFirst, type TightOptions, type TightTies } from "./tight/index.ts";
17
20
  export { ATTRACTION_DEFAULTS, ATTRACTION_SCHEMA, attractionRouter, attractionStrategy, type AttractionOptions } from "./attraction/index.ts";
18
21
  export { sectionsRouter, type SectionsOptions } from "./sections.ts";
@@ -0,0 +1,164 @@
1
+ import { dist, type Point } from "../../geom.ts";
2
+ import type { Graph, GroupRule, Result, Router } from "../../model.ts";
3
+ import { defineStrategy, type Strategy } from "../../strategies/model.ts";
4
+ import type { OptionSchema } from "../../strategies/options.ts";
5
+ import { sharedRouter, type SharedSearch, type Way } from "../shared.ts";
6
+ import { byCollection } from "../trunks.ts";
7
+
8
+ /** The sine of 1 degree, as a literal. A literal gives the same bits on each platform, so a decision does not depend on `Math.sin`. */
9
+ const SIN_1 = 0.01745240643728351;
10
+
11
+ /** The options of the router `reroute` (RULE-044: a corner graph with the prices of the rules, and rounds of rip-up and reroute). */
12
+ export interface RerouteOptions {
13
+ /** The price of a link in a narrow channel, from 0 to 2. The default is 1. */
14
+ readonly alpha: number;
15
+ /** The price of each edge of another key on a link, from 0 to 2. The default is 0.5. */
16
+ readonly beta: number;
17
+ /** The discount of a link that an edge of the same key uses, from 0 to 2. The default is 0.5. */
18
+ readonly gamma: number;
19
+ /** The price of a turn, in units of `clearance`, from 0 to 2. The default is 0.25. */
20
+ readonly tau: number;
21
+ /** The number of rounds of rip-up and reroute, from 1 to 5. The default is 2. */
22
+ readonly rounds: number;
23
+ /** The share of the edges that each round rips up: the edges with the largest price over their direct length, from 0.05 to 0.3. The default is 0.15. */
24
+ readonly q: number;
25
+ /** A channel narrower than this width gets the price `alpha`, from 4 to 64. The default is 16. */
26
+ readonly clearance: number;
27
+ }
28
+
29
+ /** The default options of the router `reroute`. */
30
+ export const REROUTE_DEFAULTS: Readonly<RerouteOptions> = Object.freeze({ alpha: 1, beta: 0.5, gamma: 0.5, tau: 0.25, rounds: 2, q: 0.15, clearance: 16 });
31
+
32
+ /** The declarations of the options of the router `reroute` (RULE-044). */
33
+ export const REROUTE_SCHEMA: OptionSchema<RerouteOptions> = {
34
+ alpha: { type: "number", min: 0, max: 2, step: 0.05, marks: [0, 1, 2], default: 1, describe: "The price of a link in a narrow channel." },
35
+ beta: { type: "number", min: 0, max: 2, step: 0.05, marks: [0, 0.5, 1], default: 0.5, describe: "The price of each edge of another key on a link." },
36
+ gamma: { type: "number", min: 0, max: 2, step: 0.05, marks: [0, 0.5, 1], default: 0.5, describe: "The discount of a link that an edge of the same key uses." },
37
+ tau: { type: "number", min: 0, max: 2, step: 0.05, marks: [0, 0.25, 1], default: 0.25, describe: "The price of a turn, in units of the clearance." },
38
+ rounds: { type: "integer", min: 1, max: 5, step: 1, marks: [1, 2, 3, 5], default: 2, describe: "The number of rounds of rip-up and reroute." },
39
+ q: { type: "number", min: 0.05, max: 0.3, step: 0.05, marks: [0.05, 0.15, 0.3], default: 0.15, describe: "The share of the edges that each round rips up: the edges with the largest price over their direct length." },
40
+ clearance: { type: "number", min: 4, max: 64, step: 1, marks: [8, 16, 32], default: 16, describe: "A channel narrower than this width gets the price alpha." },
41
+ };
42
+
43
+ /** This function gives the number of turns of a way: the points where the direction changes by more than 1 degree. */
44
+ export function turnsOf(seq: readonly Point[]): number {
45
+ let turns = 0;
46
+ for (let i = 1; i + 1 < seq.length; i++) {
47
+ const a = seq[i - 1]!;
48
+ const b = seq[i]!;
49
+ const c = seq[i + 1]!;
50
+ const ux = b.x - a.x;
51
+ const uy = b.y - a.y;
52
+ const vx = c.x - b.x;
53
+ const vy = c.y - b.y;
54
+ const lu = Math.sqrt(ux * ux + uy * uy);
55
+ const lv = Math.sqrt(vx * vx + vy * vy);
56
+ if (lu <= 1e-9 || lv <= 1e-9) continue;
57
+ if (Math.abs(ux * vy - uy * vx) > SIN_1 * lu * lv || ux * vx + uy * vy < 0) turns++;
58
+ }
59
+ return turns;
60
+ }
61
+
62
+ /** The use of one link of the corner graph: the number of edges of each key on it. */
63
+ type LinkUse = Map<string, number>;
64
+
65
+ /**
66
+ * This function rips up and reroutes the ways of `shared` (RULE-044, after the rip-up and reroute of PathFinder, McMurchie 1995).
67
+ * The price factor of a link is `max(0.05, 1 + alpha × narrow + beta × others - gamma × same)`.
68
+ * `narrow` is `max(0, 1 - width / clearance)`. `others` counts the edges of the other keys on the link.
69
+ * `same` is 1 when an edge of the same key uses the link.
70
+ * The price of a way is the sum of the price factor times the length of each link, plus `tau × clearance` for each turn.
71
+ * Each round takes the share `q` of the ways with the largest price over their direct length. Each one gets a new search with the prices of the links.
72
+ * The new way stays only when its price is lower. The search has no state for the turns, so only the comparison reads `tau`.
73
+ */
74
+ export function rerouteWays(ways: readonly Way[], search: SharedSearch, options: Partial<RerouteOptions> = {}): void {
75
+ const o = { ...REROUTE_DEFAULTS, ...options };
76
+ const n = search.corners;
77
+ const use = new Map<number, LinkUse>();
78
+ const linksOf = (w: Way): number[] => {
79
+ const out: number[] = [];
80
+ for (let i = 1; i < w.ids.length; i++) {
81
+ const a = w.ids[i - 1]!;
82
+ const b = w.ids[i]!;
83
+ if (a < n && b < n) out.push(search.link(a, b));
84
+ }
85
+ return out;
86
+ };
87
+ const change = (w: Way, by: number): void => {
88
+ for (const k of linksOf(w)) {
89
+ let m = use.get(k);
90
+ if (!m) use.set(k, (m = new Map()));
91
+ const next = (m.get(w.key) ?? 0) + by;
92
+ if (next > 0) m.set(w.key, next);
93
+ else m.delete(w.key);
94
+ }
95
+ };
96
+ const found = ways.filter((w) => w.found && w.ids.length >= 2);
97
+ for (const w of found) change(w, 1);
98
+ const factor = (i: number, j: number, key: string): number => {
99
+ const m = use.get(search.link(i, j));
100
+ let others = 0;
101
+ let same = 0;
102
+ for (const [k, c] of m ?? []) {
103
+ if (k === key) same = 1;
104
+ else others += c;
105
+ }
106
+ const narrow = o.alpha > 0 ? Math.max(0, 1 - search.width(i, j) / o.clearance) : 0;
107
+ return Math.max(0.05, 1 + o.alpha * narrow + o.beta * others - o.gamma * same);
108
+ };
109
+ const priceOf = (seq: readonly Point[], ids: readonly number[], key: string): number => {
110
+ let p = 0;
111
+ for (let i = 1; i < seq.length; i++) {
112
+ const a = ids[i - 1]!;
113
+ const b = ids[i]!;
114
+ const d = dist(seq[i - 1]!, seq[i]!);
115
+ p += a < n && b < n ? d * factor(a, b, key) : d;
116
+ }
117
+ return p + o.tau * o.clearance * turnsOf(seq);
118
+ };
119
+ const floor = Math.max(0.05, 1 - o.gamma);
120
+ const count = (share: number): number => Math.max(1, Math.round(share * found.length));
121
+ for (let round = 0; round < Math.max(1, Math.floor(o.rounds)); round++) {
122
+ const ranked = found
123
+ .map((w) => ({ w, score: priceOf(w.seq, w.ids, w.key) / Math.max(1e-9, dist(w.ps.point, w.pt.point)) }))
124
+ .sort((a, b) => b.score - a.score || (a.w.edge.id < b.w.edge.id ? -1 : a.w.edge.id > b.w.edge.id ? 1 : 0));
125
+ let moved = 0;
126
+ for (const { w } of ranked.slice(0, count(Math.min(0.3, Math.max(0.05, o.q))))) {
127
+ change(w, -1);
128
+ const before = priceOf(w.seq, w.ids, w.key);
129
+ const next = search.find(w, (i, j, d) => d * factor(i, j, w.key), floor);
130
+ if (next && priceOf(next.seq, next.ids, w.key) < before - 1e-9) {
131
+ w.seq = next.seq;
132
+ w.ids = next.ids;
133
+ moved++;
134
+ }
135
+ change(w, 1);
136
+ }
137
+ if (moved === 0) break;
138
+ }
139
+ }
140
+
141
+ /**
142
+ * This function makes the router `reroute` (RULE-044). It is the router `shared`, then the rounds of rip-up and reroute of `rerouteWays`.
143
+ * The runs and the bundles come from the ways after the rounds.
144
+ */
145
+ export function rerouteRouter(options: Partial<RerouteOptions> & { readonly name: string; readonly rule?: GroupRule; readonly maxNodes?: number }): Router {
146
+ const o = { ...REROUTE_DEFAULTS, ...options };
147
+ return sharedRouter({ name: o.name, ...(o.rule === undefined ? {} : { rule: o.rule }), ...(o.maxNodes === undefined ? {} : { maxNodes: o.maxNodes }) }, undefined, {
148
+ refine: (ways, search) => rerouteWays(ways, search, o),
149
+ });
150
+ }
151
+
152
+ /**
153
+ * The router strategy `reroute` (RULE-044), with the rule `byCollection` of `shared`. It is not a built-in router, so the benchmark and the hashes of `ROUTERS` do not change.
154
+ * A screen module or a consumer adds it to a registry.
155
+ */
156
+ export const rerouteStrategy: Strategy<"router", RerouteOptions> = defineStrategy({
157
+ step: "router",
158
+ name: "reroute",
159
+ describe: "Rule prices and rip-up (RULE-044). The ways of `shared` with the largest prices of narrow channels, other keys and turns get a new search.",
160
+ defaults: { ...REROUTE_DEFAULTS },
161
+ schema: REROUTE_SCHEMA,
162
+ maxNodes: 10_000,
163
+ run: (graph: Graph, o: Readonly<RerouteOptions>): Result => rerouteRouter({ ...o, name: "reroute", rule: byCollection }).route(graph),
164
+ });