@mark1russell7/highways 0.9.3 → 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.
- package/README.md +85 -5
- package/build/index.d.ts +954 -5
- package/build/index.d.ts.map +1 -1
- package/build/index.js +10689 -5294
- package/build/index.js.map +1 -1
- package/package.json +1 -1
- package/src/bench/screen/exercise.ts +4 -1
- package/src/bench/screen/set.ts +58 -1
- package/src/families/cases.ts +17 -9
- package/src/families/coverage.ts +502 -0
- package/src/families/index.ts +4 -1
- package/src/highways/balance/index.ts +93 -0
- package/src/highways/bronchial/index.ts +144 -0
- package/src/highways/index.ts +5 -0
- package/src/highways/knee/index.ts +154 -0
- package/src/highways/knee/root.ts +19 -0
- package/src/highways/manifolds/index.ts +157 -0
- package/src/highways/slots/index.ts +178 -0
- package/src/index.ts +8 -0
- package/src/levels/detail/index.ts +140 -0
- package/src/levels/venation/index.ts +191 -0
- package/src/plugs/leaders/index.ts +42 -0
- package/src/plugs/spread/index.ts +39 -5
- package/src/routers/adam/index.ts +294 -0
- package/src/routers/annealing/index.ts +260 -0
- package/src/routers/canals/index.ts +191 -0
- package/src/routers/frames.ts +80 -16
- package/src/routers/gutter.ts +545 -0
- package/src/routers/index.ts +8 -2
- package/src/routers/reroute/index.ts +164 -0
- package/src/routers/shared.ts +43 -2
- package/src/routers/tight/index.ts +122 -0
- package/src/strategies/builtins.ts +158 -2
- package/src/strategies/schemas.ts +143 -0
- package/src/whole/curriculum/index.ts +243 -0
- package/src/whole/fireflies/index.ts +177 -0
- package/src/whole/portfolio/index.ts +108 -0
- package/src/whole/preferences/index.ts +247 -0
- package/src/whole/tracing/index.ts +144 -0
|
@@ -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,260 @@
|
|
|
1
|
+
import { rng } from "../../fixtures.ts";
|
|
2
|
+
import { dist, type Point } from "../../geom.ts";
|
|
3
|
+
import type { Graph, GroupRule, Result, Router } from "../../model.ts";
|
|
4
|
+
import { defineStrategy, type Strategy } from "../../strategies/model.ts";
|
|
5
|
+
import type { OptionSchema } from "../../strategies/options.ts";
|
|
6
|
+
import { sharedRouter, type SharedSearch, type Way } from "../shared.ts";
|
|
7
|
+
import { byCollection } from "../trunks.ts";
|
|
8
|
+
|
|
9
|
+
/** The options of the router `annealing` (RULE-021: simulated annealing over candidate routes, with its rider WILD-028). */
|
|
10
|
+
export interface AnnealingOptions {
|
|
11
|
+
/** The number of candidate ways of each edge, from 3 to 8. The default is 4. */
|
|
12
|
+
readonly candidates: number;
|
|
13
|
+
/** The cost of one crossing, in units of the median box side, from 0 to 20. The default is 4. */
|
|
14
|
+
readonly crossing: number;
|
|
15
|
+
/** The start temperature, in units of the median box side, from 0.1 to 10. The default is 1. */
|
|
16
|
+
readonly temperature: number;
|
|
17
|
+
/** The factor of the temperature after each sweep of the edges, from 0.9 to 0.99. The default is 0.95. */
|
|
18
|
+
readonly cooling: number;
|
|
19
|
+
/** The budget: the number of changes for each edge, from 20 to 200. The default is 50. */
|
|
20
|
+
readonly budget: number;
|
|
21
|
+
/** The seed of the random choices, a whole number from 0. The same seed gives the same routes. The default is 1. */
|
|
22
|
+
readonly seed: number;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** The default options of the router `annealing`. */
|
|
26
|
+
export const ANNEALING_DEFAULTS: Readonly<AnnealingOptions> = Object.freeze({ candidates: 4, crossing: 4, temperature: 1, cooling: 0.95, budget: 50, seed: 1 });
|
|
27
|
+
|
|
28
|
+
/** The declarations of the options of the router `annealing` (RULE-021). */
|
|
29
|
+
export const ANNEALING_SCHEMA: OptionSchema<AnnealingOptions> = {
|
|
30
|
+
candidates: { type: "integer", min: 3, max: 8, step: 1, marks: [3, 4, 6], default: 4, describe: "The number of candidate ways of each edge." },
|
|
31
|
+
crossing: { type: "number", min: 0, max: 20, step: 0.5, marks: [1, 4, 10], default: 4, describe: "The cost of one crossing, in units of the median box side." },
|
|
32
|
+
temperature: { type: "number", min: 0.1, max: 10, step: 0.1, marks: [0.5, 1, 4], default: 1, describe: "The start temperature, in units of the median box side." },
|
|
33
|
+
cooling: { type: "number", min: 0.9, max: 0.99, step: 0.01, marks: [0.9, 0.95, 0.98], default: 0.95, describe: "The factor of the temperature after each sweep of the edges." },
|
|
34
|
+
budget: { type: "integer", min: 20, max: 200, step: 10, marks: [20, 50, 100], default: 50, describe: "The budget: the number of changes for each edge." },
|
|
35
|
+
seed: { type: "integer", min: 0, max: 1_000_000, step: 1, marks: [1, 2, 3], default: 1, describe: "The seed of the random choices. The same seed gives the same routes." },
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
/** The share of the cost of a link that the edges of one key use already, as in `shared` with its default discount. */
|
|
39
|
+
const DISCOUNT = 0.5;
|
|
40
|
+
/** The cost factor of the links of the earlier candidates in the search of the next candidate. */
|
|
41
|
+
const PENALTY = 3;
|
|
42
|
+
|
|
43
|
+
/** One candidate way of an edge: its points, the indices of its points and the keys of its links. */
|
|
44
|
+
interface Candidate {
|
|
45
|
+
readonly seq: Point[];
|
|
46
|
+
readonly ids: number[];
|
|
47
|
+
readonly links: number[];
|
|
48
|
+
readonly lengths: number[];
|
|
49
|
+
readonly length: number;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** This function tells if the segments `a b` and `c d` cross at one inner point of each. A touch at an end or a common line is not a crossing. */
|
|
53
|
+
function properCross(a: Point, b: Point, c: Point, d: Point): boolean {
|
|
54
|
+
const o = (p: Point, q: Point, r: Point): number => (q.x - p.x) * (r.y - p.y) - (q.y - p.y) * (r.x - p.x);
|
|
55
|
+
const d1 = o(c, d, a);
|
|
56
|
+
const d2 = o(c, d, b);
|
|
57
|
+
const d3 = o(a, b, c);
|
|
58
|
+
const d4 = o(a, b, d);
|
|
59
|
+
return ((d1 > 0 && d2 < 0) || (d1 < 0 && d2 > 0)) && ((d3 > 0 && d4 < 0) || (d3 < 0 && d4 > 0));
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** This function gives the median of the smaller side of the boxes, the unit of the costs. */
|
|
63
|
+
function unitOf(graph: Graph): number {
|
|
64
|
+
const sides = graph.nodes.map((n) => Math.min(n.box.w, n.box.h)).sort((a, b) => a - b);
|
|
65
|
+
const u = sides.length === 0 ? 1 : sides[Math.floor((sides.length - 1) / 2)]!;
|
|
66
|
+
return u > 0 ? u : 1;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* This function anneals the choice of one candidate way for each edge (RULE-021, after Davidson and Harel, 1996).
|
|
71
|
+
* The candidates of an edge are its way in `shared` and its shortest way. Other candidates are the shortest ways when the links of earlier candidates cost 3 times more.
|
|
72
|
+
* The energy is `(1 - 0.5) ink + 0.5 Σ L + crossing u X`: the cost of `shared` plus a cost for each crossing `X` between the ways of two edges.
|
|
73
|
+
* A change takes one edge to another candidate. A worse change stays with the rational chance `1 / (1 + y + y^2 / 2)`, with `y = ΔE / T`.
|
|
74
|
+
* The temperature falls by `cooling` after each sweep. The ways at the end are the ways of the lowest energy of the run.
|
|
75
|
+
*/
|
|
76
|
+
export function annealWays(ways: readonly Way[], search: SharedSearch, o: Readonly<AnnealingOptions>): void {
|
|
77
|
+
const live = ways.filter((w) => w.found);
|
|
78
|
+
const m = live.length;
|
|
79
|
+
if (m < 2) return;
|
|
80
|
+
const unit = unitOf(search.graph);
|
|
81
|
+
const n = search.corners;
|
|
82
|
+
let maxId = 0;
|
|
83
|
+
for (const w of live) for (const i of w.ids) maxId = Math.max(maxId, i);
|
|
84
|
+
const N = maxId + 1;
|
|
85
|
+
const pairOf = (a: number, b: number): number => (a < b ? a * N + b : b * N + a);
|
|
86
|
+
const make = (seq: Point[], ids: number[]): Candidate => {
|
|
87
|
+
const links: number[] = [];
|
|
88
|
+
const lengths: number[] = [];
|
|
89
|
+
let length = 0;
|
|
90
|
+
for (let i = 1; i < ids.length; i++) {
|
|
91
|
+
const d = dist(seq[i - 1]!, seq[i]!);
|
|
92
|
+
links.push(pairOf(ids[i - 1]!, ids[i]!));
|
|
93
|
+
lengths.push(d);
|
|
94
|
+
length += d;
|
|
95
|
+
}
|
|
96
|
+
return { seq, ids, links, lengths, length };
|
|
97
|
+
};
|
|
98
|
+
const k = Math.min(8, Math.max(1, Math.floor(o.candidates)));
|
|
99
|
+
// The candidates of each edge. The first is the way of `shared`, the start state.
|
|
100
|
+
const cands: Candidate[][] = live.map((w) => {
|
|
101
|
+
const list = [make(w.seq, w.ids)];
|
|
102
|
+
const seen = new Set([w.ids.join(",")]);
|
|
103
|
+
const hit = new Set<number>();
|
|
104
|
+
const mark = (c: Candidate): void => {
|
|
105
|
+
for (let i = 1; i < c.ids.length; i++) if (c.ids[i - 1]! < n && c.ids[i]! < n) hit.add(search.link(c.ids[i - 1]!, c.ids[i]!));
|
|
106
|
+
};
|
|
107
|
+
const add = (found: { seq: Point[]; ids: number[] } | undefined): boolean => {
|
|
108
|
+
if (!found) return false;
|
|
109
|
+
const key = found.ids.join(",");
|
|
110
|
+
if (seen.has(key)) return false;
|
|
111
|
+
seen.add(key);
|
|
112
|
+
const c = make(found.seq, found.ids);
|
|
113
|
+
list.push(c);
|
|
114
|
+
mark(c);
|
|
115
|
+
return true;
|
|
116
|
+
};
|
|
117
|
+
mark(list[0]!);
|
|
118
|
+
if (list.length < k) add(search.find(w));
|
|
119
|
+
for (let tries = 0; list.length < k && tries < k; tries++) {
|
|
120
|
+
if (!add(search.find(w, (i, j, d) => (hit.has(search.link(i, j)) ? d * PENALTY : d), 1))) break;
|
|
121
|
+
}
|
|
122
|
+
return list;
|
|
123
|
+
});
|
|
124
|
+
// The crossings of each pair of candidates of two edges, in a table for each pair of edges whose boxes meet.
|
|
125
|
+
type Bounds = { x0: number; y0: number; x1: number; y1: number };
|
|
126
|
+
const boundsOf = (seqs: readonly Point[][]): Bounds => {
|
|
127
|
+
let x0 = Infinity;
|
|
128
|
+
let y0 = Infinity;
|
|
129
|
+
let x1 = -Infinity;
|
|
130
|
+
let y1 = -Infinity;
|
|
131
|
+
for (const seq of seqs)
|
|
132
|
+
for (const p of seq) {
|
|
133
|
+
x0 = Math.min(x0, p.x);
|
|
134
|
+
y0 = Math.min(y0, p.y);
|
|
135
|
+
x1 = Math.max(x1, p.x);
|
|
136
|
+
y1 = Math.max(y1, p.y);
|
|
137
|
+
}
|
|
138
|
+
return { x0, y0, x1, y1 };
|
|
139
|
+
};
|
|
140
|
+
const apart = (A: Bounds, B: Bounds): boolean => A.x1 < B.x0 || B.x1 < A.x0 || A.y1 < B.y0 || B.y1 < A.y0;
|
|
141
|
+
const own = cands.map((list) => list.map((c) => boundsOf([c.seq])));
|
|
142
|
+
const boxes = cands.map((list) => boundsOf(list.map((c) => c.seq)));
|
|
143
|
+
const near: { f: number; table: Int32Array; mine: boolean }[][] = live.map(() => []);
|
|
144
|
+
for (let e = 0; e < m; e++) {
|
|
145
|
+
for (let f = e + 1; f < m; f++) {
|
|
146
|
+
if (apart(boxes[e]!, boxes[f]!)) continue;
|
|
147
|
+
const ke = cands[e]!.length;
|
|
148
|
+
const kf = cands[f]!.length;
|
|
149
|
+
const table = new Int32Array(ke * kf);
|
|
150
|
+
let any = false;
|
|
151
|
+
for (let i = 0; i < ke; i++) {
|
|
152
|
+
const p = cands[e]![i]!.seq;
|
|
153
|
+
for (let j = 0; j < kf; j++) {
|
|
154
|
+
if (apart(own[e]![i]!, own[f]![j]!)) continue;
|
|
155
|
+
const q = cands[f]![j]!.seq;
|
|
156
|
+
let x = 0;
|
|
157
|
+
for (let s = 1; s < p.length; s++) for (let t = 1; t < q.length; t++) if (properCross(p[s - 1]!, p[s]!, q[t - 1]!, q[t]!)) x++;
|
|
158
|
+
table[i * kf + j] = x;
|
|
159
|
+
if (x > 0) any = true;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
if (!any) continue;
|
|
163
|
+
near[e]!.push({ f, table, mine: true });
|
|
164
|
+
near[f]!.push({ f: e, table, mine: false });
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
// The users of each link in each key, for the ink.
|
|
168
|
+
const keys = live.map((w) => w.key);
|
|
169
|
+
const users = new Map<string, Map<number, number>>();
|
|
170
|
+
const use = (e: number, c: Candidate, step: number): number => {
|
|
171
|
+
let mine = users.get(keys[e]!);
|
|
172
|
+
if (!mine) users.set(keys[e]!, (mine = new Map()));
|
|
173
|
+
let ink = 0;
|
|
174
|
+
for (let i = 0; i < c.links.length; i++) {
|
|
175
|
+
const before = mine.get(c.links[i]!) ?? 0;
|
|
176
|
+
const after = before + step;
|
|
177
|
+
if (after === 0) mine.delete(c.links[i]!);
|
|
178
|
+
else mine.set(c.links[i]!, after);
|
|
179
|
+
if (before === 0 && after > 0) ink += c.lengths[i]!;
|
|
180
|
+
if (before > 0 && after === 0) ink -= c.lengths[i]!;
|
|
181
|
+
}
|
|
182
|
+
return ink;
|
|
183
|
+
};
|
|
184
|
+
const choice = new Int32Array(m);
|
|
185
|
+
for (let e = 0; e < m; e++) use(e, cands[e]![0]!, 1);
|
|
186
|
+
const crossCost = Math.max(0, o.crossing) * unit;
|
|
187
|
+
const crossings = (e: number, i: number): number => {
|
|
188
|
+
let x = 0;
|
|
189
|
+
for (const { f, table, mine } of near[e]!) {
|
|
190
|
+
const kf = cands[f]!.length;
|
|
191
|
+
const ke = cands[e]!.length;
|
|
192
|
+
x += mine ? table[i * kf + choice[f]!]! : table[choice[f]! * ke + i]!;
|
|
193
|
+
}
|
|
194
|
+
return x;
|
|
195
|
+
};
|
|
196
|
+
const random = rng(Math.max(0, Math.floor(o.seed)));
|
|
197
|
+
const cooling = Math.min(0.99, Math.max(0.9, o.cooling));
|
|
198
|
+
let temperature = Math.max(1e-6, o.temperature) * unit;
|
|
199
|
+
const moves = Math.max(1, Math.floor(o.budget)) * m;
|
|
200
|
+
let energy = 0;
|
|
201
|
+
let best = 0;
|
|
202
|
+
let bestChoice = choice.slice();
|
|
203
|
+
for (let s = 0; s < moves; s++) {
|
|
204
|
+
if (s > 0 && s % m === 0) temperature *= cooling;
|
|
205
|
+
const e = Math.floor(random() * m);
|
|
206
|
+
const list = cands[e]!;
|
|
207
|
+
if (list.length < 2) continue;
|
|
208
|
+
const from = choice[e]!;
|
|
209
|
+
const to = (from + 1 + Math.floor(random() * (list.length - 1))) % list.length;
|
|
210
|
+
// The change of the energy: the ink and the length of the key, and the crossings with the other edges.
|
|
211
|
+
const dInk = use(e, list[from]!, -1) + use(e, list[to]!, 1);
|
|
212
|
+
const dLength = list[to]!.length - list[from]!.length;
|
|
213
|
+
const dCross = crossings(e, to) - crossings(e, from);
|
|
214
|
+
const delta = (1 - DISCOUNT) * dInk + DISCOUNT * dLength + crossCost * dCross;
|
|
215
|
+
const y = delta / temperature;
|
|
216
|
+
if (delta <= 0 || random() < 1 / (1 + y + (y * y) / 2)) {
|
|
217
|
+
choice[e] = to;
|
|
218
|
+
energy += delta;
|
|
219
|
+
if (energy < best - 1e-9) {
|
|
220
|
+
best = energy;
|
|
221
|
+
bestChoice = choice.slice();
|
|
222
|
+
}
|
|
223
|
+
} else {
|
|
224
|
+
use(e, list[to]!, -1);
|
|
225
|
+
use(e, list[from]!, 1);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
live.forEach((w, e) => {
|
|
229
|
+
const c = cands[e]![bestChoice[e]!]!;
|
|
230
|
+
w.seq = c.seq;
|
|
231
|
+
w.ids = c.ids;
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* This function makes the router `annealing` (RULE-021 with its rider WILD-028). It is the router `shared`, then the annealing of `annealWays`.
|
|
237
|
+
* Each candidate is a way through the corner graph, as in `shared`, so each one keeps the hard rules.
|
|
238
|
+
*/
|
|
239
|
+
export function annealingRouter(options: Partial<AnnealingOptions> & { readonly name: string; readonly rule?: GroupRule; readonly maxNodes?: number }): Router {
|
|
240
|
+
const o = { ...ANNEALING_DEFAULTS, ...options };
|
|
241
|
+
return sharedRouter(
|
|
242
|
+
{ name: o.name, ...(o.rule === undefined ? {} : { rule: o.rule }), ...(o.maxNodes === undefined ? {} : { maxNodes: o.maxNodes }) },
|
|
243
|
+
undefined,
|
|
244
|
+
{ refine: (ways, search) => annealWays(ways, search, o) },
|
|
245
|
+
);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* The router strategy `annealing` (RULE-021), with the rule `byCollection` of `shared`. It is not a built-in router, so the benchmark and the hashes of `ROUTERS` do not change.
|
|
250
|
+
* A screen module or a consumer adds it to a registry.
|
|
251
|
+
*/
|
|
252
|
+
export const annealingStrategy: Strategy<"router", AnnealingOptions> = defineStrategy({
|
|
253
|
+
step: "router",
|
|
254
|
+
name: "annealing",
|
|
255
|
+
describe: "Simulated annealing over candidate routes (RULE-021, with WILD-028). Each edge picks one of a few ways, for less ink and fewer crossings.",
|
|
256
|
+
defaults: { ...ANNEALING_DEFAULTS },
|
|
257
|
+
schema: ANNEALING_SCHEMA,
|
|
258
|
+
maxNodes: 10_000,
|
|
259
|
+
run: (graph: Graph, o: Readonly<AnnealingOptions>): Result => annealingRouter({ ...o, name: "annealing", rule: byCollection }).route(graph),
|
|
260
|
+
});
|