@mark1russell7/highways 0.9.4 → 0.9.5

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,42 @@
1
+ /**
2
+ * The plug strategy `leaders` (EDA-041): boundary leaders of the labeling of maps, used as the stubs of the plugs.
3
+ * Each wall places its plugs in an order that gives leaders with no crossing, and the least length of the leaders.
4
+ */
5
+ import type { PlugInput, PlugPair } from "../../strategies/model.ts";
6
+ import { spreadWith, SPREAD_DEFAULTS } from "../spread/index.ts";
7
+
8
+ /**
9
+ * The type of leader of a plug. With `s`, the order of a wall is the order of the angles of the far ends, as in `spread`.
10
+ * With `po`, the stub leaves along the normal and turns once, so the order is the order of the far ends along the wall.
11
+ * With `opo`, the plugs of a wall go at even places, in the order of the angles.
12
+ */
13
+ export type LeaderType = "s" | "po" | "opo";
14
+
15
+ /** The values of `LeaderType`. */
16
+ export const LEADER_TYPES: readonly LeaderType[] = Object.freeze(["s", "po", "opo"]);
17
+
18
+ /** The options of the plug strategy `leaders`. */
19
+ export interface LeaderOptions {
20
+ /** The type of leader. The default is `po`. */
21
+ readonly leader: LeaderType;
22
+ /** The least gap between two plugs of one wall, from 2 to 8. The default is 8. */
23
+ readonly slotSpacing: number;
24
+ /** The distance from a plug to a corner of its box, or a quarter of the wall when the wall is short. The default is 6. */
25
+ readonly inset: number;
26
+ /** The number of passes that sort each wall again by the new places of the far ends, from 0 to 4. The default is 2. */
27
+ readonly rounds: number;
28
+ }
29
+
30
+ /** The defaults of the plug strategy `leaders`. */
31
+ export const LEADER_DEFAULTS: Readonly<LeaderOptions> = Object.freeze({ leader: "po", slotSpacing: 8, inset: SPREAD_DEFAULTS.inset, rounds: SPREAD_DEFAULTS.rounds });
32
+
33
+ /**
34
+ * This function gives the plugs of the plug strategy `leaders` (EDA-041, after Bekos 2007). A fixed plug stays at its point, and a plug with a side stays on that side.
35
+ * Each free plug goes on the side that faces the other end. The plugs of each wall keep at least `slotSpacing` between two of them.
36
+ * The type of leader sets their order and their places. The output has one entry for each edge with two known nodes, in the order of the edges.
37
+ */
38
+ export function leaderEnds(input: PlugInput, options: Partial<LeaderOptions> = {}): PlugPair[] {
39
+ const o = { ...LEADER_DEFAULTS, ...options };
40
+ const spread = { plugGap: o.slotSpacing, inset: o.inset, rounds: o.rounds };
41
+ return spreadWith(input, spread, o.leader === "s" ? undefined : o.leader);
42
+ }
@@ -84,6 +84,20 @@ function keyOf(box: Box, side: Side, far: Point): number {
84
84
  return along >= 0 ? 2 - p : -2 - p;
85
85
  }
86
86
 
87
+ /**
88
+ * This function gives the sort key of a far point for a po-leader (EDA-041): its place on the axis along the wall.
89
+ * A far point behind the wall gets the key of `keyOf` with a large shift, so it goes to an end of the wall.
90
+ */
91
+ function poKeyOf(box: Box, side: Side, far: Point): number {
92
+ const c = center(box);
93
+ const n = SIDE_NORMAL[side];
94
+ const out = (far.x - c.x) * n.x + (far.y - c.y) * n.y;
95
+ const along = alongOf(side) === "y" ? far.y : far.x;
96
+ if (out > 0) return along;
97
+ const k = keyOf(box, side, far);
98
+ return k >= 0 ? 1e12 + k : -1e12 + k;
99
+ }
100
+
87
101
  /**
88
102
  * This function places `ideal.length` plugs in their given order on the segment from `lo` to `hi`. Two neighbours are at least `gap` apart.
89
103
  * The gap shrinks evenly when the segment is too short. The sum of the squares of the moves from the ideal places is the least.
@@ -102,7 +116,7 @@ function placeInOrder(ideal: readonly number[], lo: number, hi: number, gap: num
102
116
  * Each slot goes into the part of its ideal place, and the parts keep the order of the slots. In each part, two plugs and a plug and a fixed plug are at least `gap` apart.
103
117
  * The gap of a part shrinks evenly when the part is too short.
104
118
  */
105
- function placeWall(slots: readonly Slot[], fixed: readonly number[], lo: number, hi: number, gap: number): void {
119
+ function placeWall(slots: readonly Slot[], fixed: readonly number[], lo: number, hi: number, gap: number, idealOf: (s: Slot) => number = (s) => s.ideal): void {
106
120
  const cuts = [...new Set(fixed.filter((f) => f > lo - 1e-9 && f < hi + 1e-9))].sort((a, b) => a - b);
107
121
  // The part i is from cuts[i - 1] to cuts[i]. The first part starts at lo, and the last part ends at hi.
108
122
  const ends = [lo, ...cuts, hi];
@@ -110,7 +124,7 @@ function placeWall(slots: readonly Slot[], fixed: readonly number[], lo: number,
110
124
  let part = 0;
111
125
  for (const s of slots) {
112
126
  let p = 0;
113
- while (p < cuts.length && s.ideal > cuts[p]!) p++;
127
+ while (p < cuts.length && idealOf(s) > cuts[p]!) p++;
114
128
  part = Math.max(part, p);
115
129
  parts[part]!.push(s);
116
130
  }
@@ -125,7 +139,7 @@ function placeWall(slots: readonly Slot[], fixed: readonly number[], lo: number,
125
139
  const lower = a + fa * s;
126
140
  const upper = Math.max(lower, b - fb * s);
127
141
  const at = placeInOrder(
128
- list.map((x) => x.ideal),
142
+ list.map((x) => idealOf(x)),
129
143
  lower,
130
144
  upper,
131
145
  s,
@@ -144,6 +158,21 @@ function placeWall(slots: readonly Slot[], fixed: readonly number[], lo: number,
144
158
  * The output has one entry for each edge with two known nodes, in the order of the edges.
145
159
  */
146
160
  export function spreadEnds(input: PlugInput, options: SpreadOptions = {}): PlugPair[] {
161
+ return spreadWith(input, options);
162
+ }
163
+
164
+ /**
165
+ * The order and the places of the plugs of one wall in the strategy `leaders` (EDA-041).
166
+ * With `po`, the sort key of a far end in front of the wall is its place on the axis along the wall.
167
+ * With `opo`, the plugs of a wall go at even places in the order of the keys of `spread`.
168
+ */
169
+ export type WallLeader = "po" | "opo";
170
+
171
+ /**
172
+ * This function does the work of `spreadEnds`, and of the strategy `leaders` with the argument `leader`.
173
+ * Without `leader`, it gives the plugs of `spread`.
174
+ */
175
+ export function spreadWith(input: PlugInput, options: SpreadOptions = {}, leader?: WallLeader): PlugPair[] {
147
176
  const { graph } = input;
148
177
  const gap = options.plugGap ?? SPREAD_DEFAULTS.plugGap;
149
178
  const inset = options.inset ?? SPREAD_DEFAULTS.inset;
@@ -206,11 +235,16 @@ export function spreadEnds(input: PlugInput, options: SpreadOptions = {}): PlugP
206
235
  for (let round = 0; round <= rounds; round++) {
207
236
  for (const wall of walls.values()) {
208
237
  const [lo, hi] = roomOf(wall.node.box, wall.side, inset);
209
- const keys = new Map<Slot, number>(wall.slots.map((s) => [s, keyOf(wall.node.box, wall.side, resolve(s.far!).point)]));
238
+ const key = leader === "po" ? poKeyOf : keyOf;
239
+ const keys = new Map<Slot, number>(wall.slots.map((s) => [s, key(wall.node.box, wall.side, resolve(s.far!).point)]));
210
240
  const tie = (a: Slot, b: Slot): number => (round === 0 ? a.rank[0] - b.rank[0] || a.rank[1] - b.rank[1] : order.get(a)! - order.get(b)!);
211
241
  const sorted = [...wall.slots].sort((a, b) => keys.get(a)! - keys.get(b)! || tie(a, b));
212
242
  sorted.forEach((s, i) => order.set(s, i));
213
- placeWall(sorted, wall.fixed, lo, hi, gap);
243
+ if (leader === "opo") {
244
+ // The even places: the plugs split the room into equal parts, so the plug spacing of the wall is the largest that it can be.
245
+ const even = new Map<Slot, number>(sorted.map((s, i) => [s, lo + ((i + 0.5) * (hi - lo)) / sorted.length]));
246
+ placeWall(sorted, wall.fixed, lo, hi, gap, (s) => even.get(s)!);
247
+ } else placeWall(sorted, wall.fixed, lo, hi, gap);
214
248
  }
215
249
  }
216
250
  return ends.map((x) => ({ edge: x.edge, source: resolve(x.source), target: resolve(x.target) }));
@@ -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";