@mark1russell7/highways 0.4.2 → 0.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +59 -2
- package/build/index.d.ts +2333 -193
- package/build/index.d.ts.map +1 -1
- package/build/index.js +13042 -2334
- package/build/index.js.map +1 -1
- package/package.json +12 -1
- package/src/bench/exact/index.ts +258 -0
- package/src/bench/exact/optima-exact-v1-bounds.json +14409 -0
- package/src/bench/exact/optima-exact-v1.json +4809 -0
- package/src/bench/exact/solve.ts +129 -0
- package/src/bench/measures/index.ts +1 -0
- package/src/bench/measures/lanes.ts +304 -0
- package/src/bench/measures/levels.ts +86 -2
- package/src/bench/screen/activation.ts +3 -0
- package/src/bench/screen/bounds.ts +140 -0
- package/src/bench/screen/compare.ts +26 -0
- package/src/bench/screen/exact-set.ts +100 -0
- package/src/bench/screen/exercise.ts +290 -0
- package/src/bench/screen/forms.ts +25 -5
- package/src/bench/screen/gap.ts +316 -0
- package/src/bench/screen/hash.ts +7 -76
- package/src/bench/screen/index.ts +6 -0
- package/src/bench/screen/measures.ts +89 -1
- package/src/bench/screen/portfolio-pick.ts +20 -0
- package/src/bench/screen/screen.ts +78 -2
- package/src/bench/screen/set.ts +3 -2
- package/src/channels.ts +56 -0
- package/src/highways/areas/index.ts +160 -0
- package/src/highways/confluent/index.ts +154 -0
- package/src/highways/confluent/k23.test-util.ts +8 -0
- package/src/highways/consensus/index.ts +129 -0
- package/src/highways/crossbars/index.ts +184 -0
- package/src/highways/cubu/index.ts +26 -0
- package/src/highways/diffusion/index.ts +103 -0
- package/src/highways/divided/index.ts +111 -0
- package/src/highways/edge-paths/index.ts +155 -0
- package/src/highways/fdeb/index.ts +193 -0
- package/src/highways/fixtures.test-util.ts +71 -0
- package/src/highways/flow-clusters/index.ts +143 -0
- package/src/highways/frechet/index.ts +232 -0
- package/src/highways/growth/index.ts +148 -0
- package/src/highways/hdbscan/index.ts +250 -0
- package/src/highways/hierarchy/index.ts +140 -0
- package/src/highways/hough/index.ts +182 -0
- package/src/highways/hub-arcs/index.ts +181 -0
- package/src/highways/index.ts +79 -0
- package/src/highways/kdeeb/index.ts +355 -0
- package/src/highways/lanes/index.ts +189 -0
- package/src/highways/link-communities/index.ts +189 -0
- package/src/highways/mesh/index.ts +209 -0
- package/src/highways/mingle/index.ts +171 -0
- package/src/highways/mlseb/index.ts +204 -0
- package/src/highways/nested/index.ts +143 -0
- package/src/highways/p-hub/index.ts +248 -0
- package/src/highways/power/index.ts +117 -0
- package/src/highways/resistance/index.ts +159 -0
- package/src/highways/ridges/index.ts +328 -0
- package/src/highways/sbeb/index.ts +303 -0
- package/src/highways/scale-space/index.ts +135 -0
- package/src/highways/snap.ts +110 -0
- package/src/highways/soft-keys/index.ts +114 -0
- package/src/highways/spectral/index.ts +391 -0
- package/src/highways/spiral/index.ts +205 -0
- package/src/highways/territories/index.ts +150 -0
- package/src/highways/trace.ts +153 -0
- package/src/highways/traclus/index.ts +296 -0
- package/src/index.ts +2 -0
- package/src/portfolio/bands.ts +33 -0
- package/src/portfolio/fit.ts +29 -0
- package/src/portfolio/index.ts +6 -0
- package/src/portfolio/key.ts +98 -0
- package/src/portfolio/members.ts +171 -0
- package/src/portfolio/router.ts +133 -0
- package/src/portfolio/sets.ts +28 -0
- package/src/routers/shared.ts +56 -3
- package/src/strategies/builtins.ts +376 -1
- package/src/strategies/schemas.ts +460 -1
- package/src/visibility.ts +5 -0
|
@@ -0,0 +1,391 @@
|
|
|
1
|
+
import type { Point } from "../../geom.ts";
|
|
2
|
+
import { groupedPlan, type EdgeGroup, type GroupSpine } from "../grouped.ts";
|
|
3
|
+
import { compatibility } from "../hdbscan/index.ts";
|
|
4
|
+
import type { HighwayInput, HighwayPlan } from "../model.ts";
|
|
5
|
+
import { edgePlugs } from "../stretches.ts";
|
|
6
|
+
|
|
7
|
+
/** The options of the highway strategy `spectral` (HWY-029 of the note on highways). */
|
|
8
|
+
export interface SpectralOptions {
|
|
9
|
+
/** Two edges are a pair of the graph only when their compatibility is this value or more. The default is 0.5. */
|
|
10
|
+
readonly c0: number;
|
|
11
|
+
/** The number of clusters of each part of the graph. The value 0 lets the largest eigengap choose it. The default is 0. */
|
|
12
|
+
readonly clusters: number;
|
|
13
|
+
/** The eigengap chooses at most this number of clusters in each part of the graph. The default is 16. */
|
|
14
|
+
readonly kMax: number;
|
|
15
|
+
/** A cluster with this number of edges or more gets highways. The default is 2. */
|
|
16
|
+
readonly minSize: number;
|
|
17
|
+
/** The spine of each cluster. The default is `trunk`: one trunk from one area to the other area. */
|
|
18
|
+
readonly spine: GroupSpine;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** The default options of the strategy `spectral`. */
|
|
22
|
+
export const SPECTRAL_DEFAULTS: Readonly<SpectralOptions> = Object.freeze({ c0: 0.5, clusters: 0, kMax: 16, minSize: 2, spine: "trunk" });
|
|
23
|
+
|
|
24
|
+
/** The largest number of edges in one part of the graph. A larger part splits into blocks of edges in the order of their sources. */
|
|
25
|
+
const BLOCK = 400;
|
|
26
|
+
|
|
27
|
+
/** The ends of one edge of the board. */
|
|
28
|
+
export interface EdgeEnds {
|
|
29
|
+
readonly edge: string;
|
|
30
|
+
readonly s: Point;
|
|
31
|
+
readonly t: Point;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** This function gives the ends of each edge of the input: the points of its two plugs. */
|
|
35
|
+
export function edgeEnds(input: HighwayInput): EdgeEnds[] {
|
|
36
|
+
return edgePlugs(input.graph).map(({ edge, ps, pt }) => ({ edge: edge.id, s: ps.point, t: pt.point }));
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* This function gives the eigenvalues and the eigenvectors of a symmetric matrix of `n` rows (the Householder steps and the QL steps of JAMA).
|
|
41
|
+
* The values are in increasing order. The column `j` of `vectors` (the values `vectors[i * n + j]`) is the unit vector of the value `j`.
|
|
42
|
+
*/
|
|
43
|
+
export function symmetricEigen(n: number, a: Float64Array): { values: Float64Array; vectors: Float64Array } {
|
|
44
|
+
const V = Float64Array.from(a);
|
|
45
|
+
const d = new Float64Array(n);
|
|
46
|
+
const e = new Float64Array(n);
|
|
47
|
+
if (n === 0) return { values: d, vectors: V };
|
|
48
|
+
if (n === 1) return { values: Float64Array.of(a[0]!), vectors: Float64Array.of(1) };
|
|
49
|
+
const at = (i: number, j: number): number => V[i * n + j]!;
|
|
50
|
+
const set = (i: number, j: number, v: number): void => {
|
|
51
|
+
V[i * n + j] = v;
|
|
52
|
+
};
|
|
53
|
+
// The reduction to a tridiagonal matrix.
|
|
54
|
+
for (let j = 0; j < n; j++) d[j] = at(n - 1, j);
|
|
55
|
+
for (let i = n - 1; i > 0; i--) {
|
|
56
|
+
let scale = 0;
|
|
57
|
+
let h = 0;
|
|
58
|
+
for (let k = 0; k < i; k++) scale += Math.abs(d[k]!);
|
|
59
|
+
if (scale === 0) {
|
|
60
|
+
e[i] = d[i - 1]!;
|
|
61
|
+
for (let j = 0; j < i; j++) {
|
|
62
|
+
d[j] = at(i - 1, j);
|
|
63
|
+
set(i, j, 0);
|
|
64
|
+
set(j, i, 0);
|
|
65
|
+
}
|
|
66
|
+
} else {
|
|
67
|
+
for (let k = 0; k < i; k++) {
|
|
68
|
+
d[k] = d[k]! / scale;
|
|
69
|
+
h += d[k]! * d[k]!;
|
|
70
|
+
}
|
|
71
|
+
let f = d[i - 1]!;
|
|
72
|
+
let g = Math.sqrt(h);
|
|
73
|
+
if (f > 0) g = -g;
|
|
74
|
+
e[i] = scale * g;
|
|
75
|
+
h -= f * g;
|
|
76
|
+
d[i - 1] = f - g;
|
|
77
|
+
for (let j = 0; j < i; j++) e[j] = 0;
|
|
78
|
+
for (let j = 0; j < i; j++) {
|
|
79
|
+
f = d[j]!;
|
|
80
|
+
set(j, i, f);
|
|
81
|
+
g = e[j]! + at(j, j) * f;
|
|
82
|
+
for (let k = j + 1; k <= i - 1; k++) {
|
|
83
|
+
g += at(k, j) * d[k]!;
|
|
84
|
+
e[k] = e[k]! + at(k, j) * f;
|
|
85
|
+
}
|
|
86
|
+
e[j] = g;
|
|
87
|
+
}
|
|
88
|
+
f = 0;
|
|
89
|
+
for (let j = 0; j < i; j++) {
|
|
90
|
+
e[j] = e[j]! / h;
|
|
91
|
+
f += e[j]! * d[j]!;
|
|
92
|
+
}
|
|
93
|
+
const hh = f / (h + h);
|
|
94
|
+
for (let j = 0; j < i; j++) e[j] = e[j]! - hh * d[j]!;
|
|
95
|
+
for (let j = 0; j < i; j++) {
|
|
96
|
+
f = d[j]!;
|
|
97
|
+
g = e[j]!;
|
|
98
|
+
for (let k = j; k <= i - 1; k++) set(k, j, at(k, j) - (f * e[k]! + g * d[k]!));
|
|
99
|
+
d[j] = at(i - 1, j);
|
|
100
|
+
set(i, j, 0);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
d[i] = h;
|
|
104
|
+
}
|
|
105
|
+
for (let i = 0; i < n - 1; i++) {
|
|
106
|
+
set(n - 1, i, at(i, i));
|
|
107
|
+
set(i, i, 1);
|
|
108
|
+
const h = d[i + 1]!;
|
|
109
|
+
if (h !== 0) {
|
|
110
|
+
for (let k = 0; k <= i; k++) d[k] = at(k, i + 1) / h;
|
|
111
|
+
for (let j = 0; j <= i; j++) {
|
|
112
|
+
let g = 0;
|
|
113
|
+
for (let k = 0; k <= i; k++) g += at(k, i + 1) * at(k, j);
|
|
114
|
+
for (let k = 0; k <= i; k++) set(k, j, at(k, j) - g * d[k]!);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
for (let k = 0; k <= i; k++) set(k, i + 1, 0);
|
|
118
|
+
}
|
|
119
|
+
for (let j = 0; j < n; j++) {
|
|
120
|
+
d[j] = at(n - 1, j);
|
|
121
|
+
set(n - 1, j, 0);
|
|
122
|
+
}
|
|
123
|
+
set(n - 1, n - 1, 1);
|
|
124
|
+
e[0] = 0;
|
|
125
|
+
// The QL steps with implicit shifts.
|
|
126
|
+
for (let i = 1; i < n; i++) e[i - 1] = e[i]!;
|
|
127
|
+
e[n - 1] = 0;
|
|
128
|
+
let f = 0;
|
|
129
|
+
let tst1 = 0;
|
|
130
|
+
const eps = 2 ** -52;
|
|
131
|
+
for (let l = 0; l < n; l++) {
|
|
132
|
+
tst1 = Math.max(tst1, Math.abs(d[l]!) + Math.abs(e[l]!));
|
|
133
|
+
let m = l;
|
|
134
|
+
while (m < n - 1 && Math.abs(e[m]!) > eps * tst1) m++;
|
|
135
|
+
if (m > l) {
|
|
136
|
+
for (let iter = 0; iter < 64; iter++) {
|
|
137
|
+
let g = d[l]!;
|
|
138
|
+
let p = (d[l + 1]! - g) / (2 * e[l]!);
|
|
139
|
+
let r = Math.hypot(p, 1);
|
|
140
|
+
if (p < 0) r = -r;
|
|
141
|
+
d[l] = e[l]! / (p + r);
|
|
142
|
+
d[l + 1] = e[l]! * (p + r);
|
|
143
|
+
const dl1 = d[l + 1]!;
|
|
144
|
+
let h = g - d[l]!;
|
|
145
|
+
for (let i = l + 2; i < n; i++) d[i] = d[i]! - h;
|
|
146
|
+
f += h;
|
|
147
|
+
p = d[m]!;
|
|
148
|
+
let c = 1;
|
|
149
|
+
let c2 = c;
|
|
150
|
+
let c3 = c;
|
|
151
|
+
const el1 = e[l + 1]!;
|
|
152
|
+
let s = 0;
|
|
153
|
+
let s2 = 0;
|
|
154
|
+
for (let i = m - 1; i >= l; i--) {
|
|
155
|
+
c3 = c2;
|
|
156
|
+
c2 = c;
|
|
157
|
+
s2 = s;
|
|
158
|
+
g = c * e[i]!;
|
|
159
|
+
h = c * p;
|
|
160
|
+
r = Math.hypot(p, e[i]!);
|
|
161
|
+
e[i + 1] = s * r;
|
|
162
|
+
s = e[i]! / r;
|
|
163
|
+
c = p / r;
|
|
164
|
+
p = c * d[i]! - s * g;
|
|
165
|
+
d[i + 1] = h + s * (c * g + s * d[i]!);
|
|
166
|
+
for (let k = 0; k < n; k++) {
|
|
167
|
+
const hk = at(k, i + 1);
|
|
168
|
+
set(k, i + 1, s * at(k, i) + c * hk);
|
|
169
|
+
set(k, i, c * at(k, i) - s * hk);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
p = (-s * s2 * c3 * el1 * e[l]!) / dl1;
|
|
173
|
+
e[l] = s * p;
|
|
174
|
+
d[l] = c * p;
|
|
175
|
+
if (Math.abs(e[l]!) <= eps * tst1) break;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
d[l] = d[l]! + f;
|
|
179
|
+
e[l] = 0;
|
|
180
|
+
}
|
|
181
|
+
// The values in increasing order, with their vectors.
|
|
182
|
+
for (let i = 0; i < n - 1; i++) {
|
|
183
|
+
let k = i;
|
|
184
|
+
let p = d[i]!;
|
|
185
|
+
for (let j = i + 1; j < n; j++)
|
|
186
|
+
if (d[j]! < p) {
|
|
187
|
+
k = j;
|
|
188
|
+
p = d[j]!;
|
|
189
|
+
}
|
|
190
|
+
if (k !== i) {
|
|
191
|
+
d[k] = d[i]!;
|
|
192
|
+
d[i] = p;
|
|
193
|
+
for (let j = 0; j < n; j++) {
|
|
194
|
+
const t = at(j, i);
|
|
195
|
+
set(j, i, at(j, k));
|
|
196
|
+
set(j, k, t);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
return { values: d, vectors: V };
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* This function puts the rows of a matrix of `n` rows and `k` columns into `k` clusters (the steps of Lloyd).
|
|
205
|
+
* The first centre is row 0. Each next centre is the row farthest from the centres that exist. The result is the cluster of each row.
|
|
206
|
+
*/
|
|
207
|
+
export function kMeans(n: number, k: number, rows: Float64Array, iterations = 50): Int32Array {
|
|
208
|
+
const label = new Int32Array(n);
|
|
209
|
+
if (k <= 1 || n <= 1) return label;
|
|
210
|
+
const dim = rows.length / n;
|
|
211
|
+
const d2 = (i: number, c: Float64Array, ci: number): number => {
|
|
212
|
+
let s = 0;
|
|
213
|
+
for (let j = 0; j < dim; j++) {
|
|
214
|
+
const v = rows[i * dim + j]! - c[ci * dim + j]!;
|
|
215
|
+
s += v * v;
|
|
216
|
+
}
|
|
217
|
+
return s;
|
|
218
|
+
};
|
|
219
|
+
const centres = new Float64Array(k * dim);
|
|
220
|
+
for (let j = 0; j < dim; j++) centres[j] = rows[j]!;
|
|
221
|
+
const near = new Float64Array(n).fill(Infinity);
|
|
222
|
+
let count = 1;
|
|
223
|
+
for (; count < k; count++) {
|
|
224
|
+
let far = -1;
|
|
225
|
+
let best = -1;
|
|
226
|
+
for (let i = 0; i < n; i++) {
|
|
227
|
+
near[i] = Math.min(near[i]!, d2(i, centres, count - 1));
|
|
228
|
+
if (near[i]! > best) [best, far] = [near[i]!, i];
|
|
229
|
+
}
|
|
230
|
+
if (best <= 1e-12) break;
|
|
231
|
+
for (let j = 0; j < dim; j++) centres[count * dim + j] = rows[far * dim + j]!;
|
|
232
|
+
}
|
|
233
|
+
for (let iter = 0; iter < iterations; iter++) {
|
|
234
|
+
let moved = false;
|
|
235
|
+
for (let i = 0; i < n; i++) {
|
|
236
|
+
let c = 0;
|
|
237
|
+
let best = Infinity;
|
|
238
|
+
for (let ci = 0; ci < count; ci++) {
|
|
239
|
+
const v = d2(i, centres, ci);
|
|
240
|
+
if (v < best - 1e-12) [best, c] = [v, ci];
|
|
241
|
+
}
|
|
242
|
+
if (label[i] !== c) [label[i], moved] = [c, true];
|
|
243
|
+
}
|
|
244
|
+
if (!moved && iter > 0) break;
|
|
245
|
+
const sums = new Float64Array(count * dim);
|
|
246
|
+
const sizes = new Int32Array(count);
|
|
247
|
+
for (let i = 0; i < n; i++) {
|
|
248
|
+
sizes[label[i]!]!++;
|
|
249
|
+
for (let j = 0; j < dim; j++) sums[label[i]! * dim + j] = sums[label[i]! * dim + j]! + rows[i * dim + j]!;
|
|
250
|
+
}
|
|
251
|
+
for (let ci = 0; ci < count; ci++) if (sizes[ci]! > 0) for (let j = 0; j < dim; j++) centres[ci * dim + j] = sums[ci * dim + j]! / sizes[ci]!;
|
|
252
|
+
}
|
|
253
|
+
return label;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* This function gives the spectral clusters of the points of a weight matrix (von Luxburg 2007, with the normalization of Ng, Jordan and Weiss).
|
|
258
|
+
* Each connected part of the graph is one problem. `L_sym = I - D^-1/2 W D^-1/2`.
|
|
259
|
+
* The number of clusters `k` of a part is `clusters`, or the place of the largest gap of the first `kMax + 1` eigenvalues.
|
|
260
|
+
* The first `k` eigenvectors, with each row at the length 1, go into `kMeans`. The result is the cluster of each point, and -1 for a point with no pair.
|
|
261
|
+
*/
|
|
262
|
+
export function spectralLabels(n: number, w: Float64Array, o: Readonly<Pick<SpectralOptions, "clusters" | "kMax">>): Int32Array {
|
|
263
|
+
const labels = new Int32Array(n).fill(-1);
|
|
264
|
+
// The connected parts of the graph.
|
|
265
|
+
const part = new Int32Array(n).fill(-1);
|
|
266
|
+
const parts: number[][] = [];
|
|
267
|
+
for (let i = 0; i < n; i++) {
|
|
268
|
+
if (part[i]! >= 0) continue;
|
|
269
|
+
let alone = true;
|
|
270
|
+
for (let j = 0; j < n && alone; j++) if (j !== i && w[i * n + j]! > 0) alone = false;
|
|
271
|
+
if (alone) continue;
|
|
272
|
+
const list = [i];
|
|
273
|
+
part[i] = parts.length;
|
|
274
|
+
for (let q = 0; q < list.length; q++) {
|
|
275
|
+
const a = list[q]!;
|
|
276
|
+
for (let b = 0; b < n; b++)
|
|
277
|
+
if (part[b]! < 0 && b !== a && w[a * n + b]! > 0) {
|
|
278
|
+
part[b] = parts.length;
|
|
279
|
+
list.push(b);
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
list.sort((x, y) => x - y);
|
|
283
|
+
parts.push(list);
|
|
284
|
+
}
|
|
285
|
+
let next = 0;
|
|
286
|
+
for (const list of parts) {
|
|
287
|
+
const m = list.length;
|
|
288
|
+
if (m <= 2) {
|
|
289
|
+
for (const i of list) labels[i] = next;
|
|
290
|
+
next++;
|
|
291
|
+
continue;
|
|
292
|
+
}
|
|
293
|
+
const deg = list.map((i) => list.reduce((s, j) => s + (i === j ? 0 : w[i * n + j]!), 0));
|
|
294
|
+
const a = new Float64Array(m * m);
|
|
295
|
+
for (let x = 0; x < m; x++)
|
|
296
|
+
for (let y = 0; y < m; y++) {
|
|
297
|
+
const v = x === y ? 0 : w[list[x]! * n + list[y]!]! / Math.sqrt(deg[x]! * deg[y]!);
|
|
298
|
+
a[x * m + y] = (x === y ? 1 : 0) - v;
|
|
299
|
+
}
|
|
300
|
+
const { values, vectors } = symmetricEigen(m, a);
|
|
301
|
+
let k = Math.max(1, Math.min(m, Math.round(o.clusters)));
|
|
302
|
+
if (Math.round(o.clusters) <= 0) {
|
|
303
|
+
k = 1;
|
|
304
|
+
let gap = -Infinity;
|
|
305
|
+
const top = Math.max(1, Math.min(Math.round(o.kMax), m - 1));
|
|
306
|
+
for (let i = 1; i <= top; i++) {
|
|
307
|
+
const g = values[i]! - values[i - 1]!;
|
|
308
|
+
if (g > gap + 1e-12) [gap, k] = [g, i];
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
if (k <= 1) {
|
|
312
|
+
for (const i of list) labels[i] = next;
|
|
313
|
+
next++;
|
|
314
|
+
continue;
|
|
315
|
+
}
|
|
316
|
+
const rows = new Float64Array(m * k);
|
|
317
|
+
for (let x = 0; x < m; x++) {
|
|
318
|
+
let len = 0;
|
|
319
|
+
for (let j = 0; j < k; j++) len += vectors[x * m + j]! ** 2;
|
|
320
|
+
len = Math.sqrt(len) || 1;
|
|
321
|
+
for (let j = 0; j < k; j++) rows[x * k + j] = vectors[x * m + j]! / len;
|
|
322
|
+
}
|
|
323
|
+
const found = kMeans(m, k, rows);
|
|
324
|
+
const ids = new Map<number, number>();
|
|
325
|
+
list.forEach((i, x) => {
|
|
326
|
+
const c = found[x]!;
|
|
327
|
+
if (!ids.has(c)) ids.set(c, next++);
|
|
328
|
+
labels[i] = ids.get(c)!;
|
|
329
|
+
});
|
|
330
|
+
}
|
|
331
|
+
return labels;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
/** This function gives the matrix of the compatibility of the pairs of edges, with 0 for a pair below `c0`. Then a `scale` changes the value of each pair that stays. */
|
|
335
|
+
export function compatibilityMatrix(ends: readonly EdgeEnds[], c0: number, scale?: (i: number, j: number) => number): Float64Array {
|
|
336
|
+
const n = ends.length;
|
|
337
|
+
const w = new Float64Array(n * n);
|
|
338
|
+
for (let i = 0; i < n; i++)
|
|
339
|
+
for (let j = i + 1; j < n; j++) {
|
|
340
|
+
const raw = compatibility(ends[i]!.s, ends[i]!.t, ends[j]!.s, ends[j]!.t);
|
|
341
|
+
if (raw < c0) continue;
|
|
342
|
+
const c = raw * (scale ? scale(i, j) : 1);
|
|
343
|
+
if (c <= 0) continue;
|
|
344
|
+
w[i * n + j] = c;
|
|
345
|
+
w[j * n + i] = c;
|
|
346
|
+
}
|
|
347
|
+
return w;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
/**
|
|
351
|
+
* This function gives the edge groups of the clusters of the edges of one matrix. Each edge of a cluster gets `flip` when it goes opposite to the first edge of the cluster.
|
|
352
|
+
* An edge with the label -1 gets no group.
|
|
353
|
+
*/
|
|
354
|
+
export function labelGroups(ends: readonly EdgeEnds[], labels: Int32Array, prefix: string, groups: Map<string, EdgeGroup>): void {
|
|
355
|
+
const lead = new Map<number, number>();
|
|
356
|
+
ends.forEach((e, i) => {
|
|
357
|
+
const c = labels[i]!;
|
|
358
|
+
if (c < 0) return;
|
|
359
|
+
if (!lead.has(c)) lead.set(c, i);
|
|
360
|
+
const l = ends[lead.get(c)!]!;
|
|
361
|
+
const flip = (e.t.x - e.s.x) * (l.t.x - l.s.x) + (e.t.y - e.s.y) * (l.t.y - l.s.y) < 0;
|
|
362
|
+
groups.set(e.edge, { key: `${prefix}${c}`, flip });
|
|
363
|
+
});
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
/** This function splits the edges into blocks of at most `BLOCK` edges, in the order of their sources. A small graph is one block. */
|
|
367
|
+
export function edgeBlocks<T extends EdgeEnds>(ends: readonly T[]): T[][] {
|
|
368
|
+
if (ends.length <= BLOCK) return [[...ends]];
|
|
369
|
+
const order = [...ends].sort((a, b) => a.s.x - b.s.x || a.s.y - b.s.y || (a.edge < b.edge ? -1 : 1));
|
|
370
|
+
const out: T[][] = [];
|
|
371
|
+
for (let from = 0; from < order.length; from += BLOCK) out.push(order.slice(from, from + BLOCK));
|
|
372
|
+
return out;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
/** This function gives the spectral clusters of the edges (HWY-029). A pair of edges is in the graph when its compatibility of FDEB is `c0` or more. */
|
|
376
|
+
export function spectralClusters(input: HighwayInput, o: Readonly<SpectralOptions>): Map<string, EdgeGroup> {
|
|
377
|
+
const groups = new Map<string, EdgeGroup>();
|
|
378
|
+
edgeBlocks(edgeEnds(input)).forEach((block, b) => {
|
|
379
|
+
const labels = spectralLabels(block.length, compatibilityMatrix(block, o.c0), o);
|
|
380
|
+
labelGroups(block, labels, `p${b}:`, groups);
|
|
381
|
+
});
|
|
382
|
+
return groups;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
/**
|
|
386
|
+
* This function gives the highways of the spectral clusters of the compatibility graph (HWY-029). The eigengap gives the number of highways of each part.
|
|
387
|
+
* The clusters with `minSize` edges or more get highways.
|
|
388
|
+
*/
|
|
389
|
+
export function spectralPlan(input: HighwayInput, o: Readonly<SpectralOptions>): HighwayPlan {
|
|
390
|
+
return groupedPlan(input, spectralClusters(input, o), { spine: o.spine, minSize: Math.max(2, Math.round(o.minSize)) });
|
|
391
|
+
}
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
import { center, dist, type Point } from "../../geom.ts";
|
|
2
|
+
import { nodeIndex, type Node } from "../../model.ts";
|
|
3
|
+
import { orderLanes } from "../../segments.ts";
|
|
4
|
+
import { WayFinder, type WayOptions } from "../../ways.ts";
|
|
5
|
+
import { tidyPlan } from "../base.ts";
|
|
6
|
+
import { memberAt, type Highway, type HighwayInput, type HighwayMember, type HighwayPlan } from "../model.ts";
|
|
7
|
+
import { edgePlugs } from "../stretches.ts";
|
|
8
|
+
|
|
9
|
+
/** The options of the highway strategy `spiral` (HWY-017 with EDA-035 of the notes). */
|
|
10
|
+
export interface SpiralOptions extends Pick<WayOptions, "margin" | "k"> {
|
|
11
|
+
/** The spiral angle, in degrees: the largest angle between a branch and the line to the root. The default is 25. */
|
|
12
|
+
readonly alpha: number;
|
|
13
|
+
/** A node with this number of edges or more gets a spiral tree. The default is 4. */
|
|
14
|
+
readonly minTargets: number;
|
|
15
|
+
/** The number of points of the arc of each branch. The default is 4. */
|
|
16
|
+
readonly arc: number;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** The default options of the strategy `spiral`. */
|
|
20
|
+
export const SPIRAL_DEFAULTS: Readonly<Required<Omit<SpiralOptions, "margin" | "k">>> = Object.freeze({ alpha: 25, minTargets: 4, arc: 4 });
|
|
21
|
+
|
|
22
|
+
/** One node of a spiral tree, in polar coordinates around the root. A leaf has an edge, and a join has two children. */
|
|
23
|
+
export interface SpiralNode {
|
|
24
|
+
readonly r: number;
|
|
25
|
+
readonly theta: number;
|
|
26
|
+
readonly point: Point;
|
|
27
|
+
/** A join with this node is this distance or more nearer to the root than the node. A leaf keeps its box clear so. A join has 0. */
|
|
28
|
+
readonly clear: number;
|
|
29
|
+
readonly edge?: string;
|
|
30
|
+
readonly children: readonly SpiralNode[];
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const TAU = 2 * Math.PI;
|
|
34
|
+
|
|
35
|
+
/** This function gives the angle `t` in the range from 0 to 2 pi. */
|
|
36
|
+
const wrap = (t: number): number => ((t % TAU) + TAU) % TAU;
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* This function gives the join of the nodes `p` and `q` of a spiral tree with the angle `alpha`. It gives `undefined` when no join is possible.
|
|
40
|
+
* Each node can reach the points inside its two spirals toward the root: `|theta - theta_p| <= tan(alpha) ln(r_p / r)`.
|
|
41
|
+
* The join is the point farthest from the root that both nodes reach, and that is `clear` nearer to the root than each node.
|
|
42
|
+
* At that radius, the join is at the middle of the angles that both nodes reach.
|
|
43
|
+
*/
|
|
44
|
+
export function spiralJoin(p: SpiralNode, q: SpiralNode, alpha: number, root: Point): { r: number; theta: number; point: Point } | undefined {
|
|
45
|
+
const t = Math.tan(alpha);
|
|
46
|
+
// The angle from p to q, counterclockwise, in the range from -pi to pi.
|
|
47
|
+
let delta = wrap(q.theta - p.theta);
|
|
48
|
+
if (delta > Math.PI) delta -= TAU;
|
|
49
|
+
const gap = Math.abs(delta);
|
|
50
|
+
if (gap >= Math.PI || t <= 0) return undefined;
|
|
51
|
+
const near = p.r <= q.r ? p : q;
|
|
52
|
+
const far = p.r <= q.r ? q : p;
|
|
53
|
+
const meet = t * Math.log(far.r / near.r) >= gap ? near.r : Math.exp((Math.log(p.r) + Math.log(q.r) - gap / t) / 2);
|
|
54
|
+
const r = Math.min(meet, p.r - p.clear, q.r - q.clear);
|
|
55
|
+
if (!(r > 0)) return undefined;
|
|
56
|
+
// In the frame where p is at 0 and q is at `gap`, p reaches [-rp, rp] and q reaches [gap - rq, gap + rq].
|
|
57
|
+
const rp = t * Math.log(p.r / r);
|
|
58
|
+
const rq = t * Math.log(q.r / r);
|
|
59
|
+
const lo = Math.max(-rp, gap - rq);
|
|
60
|
+
const hi = Math.min(rp, gap + rq);
|
|
61
|
+
if (lo > hi + 1e-9) return undefined;
|
|
62
|
+
const theta = wrap(p.theta + Math.sign(delta) * ((lo + hi) / 2));
|
|
63
|
+
return { r, theta, point: { x: root.x + r * Math.cos(theta), y: root.y + r * Math.sin(theta) } };
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* This function gives the spiral tree of the leaves around the root (Buchin, Speckmann and Verbeek 2011), with the greedy sweep.
|
|
68
|
+
* The sweep goes from the far leaves to the root. A leaf joins the wavefront at its radius. Two nodes that are next to each other in angle join at their join point.
|
|
69
|
+
* The join with the largest radius goes first. No join occurs below the radius `rMin`. The function gives the nodes of the wavefront at the end.
|
|
70
|
+
*/
|
|
71
|
+
export function spiralTree(leaves: readonly SpiralNode[], alpha: number, root: Point, rMin: number): SpiralNode[] {
|
|
72
|
+
const waiting = [...leaves].sort((a, b) => b.r - a.r || (a.edge! < b.edge! ? -1 : 1));
|
|
73
|
+
const front: SpiralNode[] = [];
|
|
74
|
+
const insert = (n: SpiralNode): void => {
|
|
75
|
+
let i = 0;
|
|
76
|
+
while (i < front.length && front[i]!.theta <= n.theta) i++;
|
|
77
|
+
front.splice(i, 0, n);
|
|
78
|
+
};
|
|
79
|
+
for (;;) {
|
|
80
|
+
let best: { i: number; r: number; theta: number; point: Point } | undefined;
|
|
81
|
+
if (front.length >= 2)
|
|
82
|
+
for (let i = 0; i < front.length; i++) {
|
|
83
|
+
if (front.length === 2 && i === 1) break;
|
|
84
|
+
const j = (i + 1) % front.length;
|
|
85
|
+
const join = spiralJoin(front[i]!, front[j]!, alpha, root);
|
|
86
|
+
if (join && join.r >= rMin && (!best || join.r > best.r)) best = { i, ...join };
|
|
87
|
+
}
|
|
88
|
+
const next = waiting[0];
|
|
89
|
+
if (next && (!best || next.r >= best.r)) {
|
|
90
|
+
insert(waiting.shift()!);
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
if (!best) return front;
|
|
94
|
+
const i = best.i;
|
|
95
|
+
const j = (i + 1) % front.length;
|
|
96
|
+
const node: SpiralNode = { r: best.r, theta: best.theta, point: best.point, clear: 0, children: [front[i]!, front[j]!] };
|
|
97
|
+
for (const k of [i, j].sort((a, b) => b - a)) front.splice(k, 1);
|
|
98
|
+
insert(node);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** This function gives the leaves under a node of a spiral tree, in the order of the tree. */
|
|
103
|
+
function leavesOf(n: SpiralNode): string[] {
|
|
104
|
+
return n.edge !== undefined ? [n.edge] : n.children.flatMap(leavesOf);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* This function gives the highways of the spiral trees (HWY-017, EDA-035). Each node with `minTargets` edges or more is the root of a fan.
|
|
109
|
+
* The nodes with more edges go first, and each edge is in one fan at most. The far plug of each edge is a leaf of the tree of its fan.
|
|
110
|
+
* Each branch of a tree with two leaves or more is a highway. Its spine is an arc of a logarithmic spiral, or a way through the corner graph where the arc hits a box.
|
|
111
|
+
* An edge rides the branches from the root to its leaf, or from its leaf to the root when the root is its target.
|
|
112
|
+
*/
|
|
113
|
+
export function spiralPlan(input: HighwayInput, o: Readonly<SpiralOptions>): HighwayPlan {
|
|
114
|
+
const graph = input.graph;
|
|
115
|
+
const finder = new WayFinder(graph.nodes, { ...(o.margin === undefined ? {} : { margin: o.margin }), ...(o.k === undefined ? {} : { k: o.k }) });
|
|
116
|
+
const pad = finder.margin / 2;
|
|
117
|
+
const byId = nodeIndex(graph);
|
|
118
|
+
const plugs = edgePlugs(graph);
|
|
119
|
+
const degree = new Map<string, number>();
|
|
120
|
+
for (const { edge } of plugs) {
|
|
121
|
+
if (edge.source.node === edge.target.node) continue;
|
|
122
|
+
for (const n of [edge.source.node, edge.target.node]) degree.set(n, (degree.get(n) ?? 0) + 1);
|
|
123
|
+
}
|
|
124
|
+
const hubs = [...degree].filter(([, d]) => d >= o.minTargets).sort((a, b) => b[1] - a[1] || (a[0] < b[0] ? -1 : 1));
|
|
125
|
+
const alpha = (Math.max(1, Math.min(80, o.alpha)) * Math.PI) / 180;
|
|
126
|
+
const arcPoints = Math.max(2, Math.round(o.arc));
|
|
127
|
+
const used = new Set<string>();
|
|
128
|
+
const highways: Highway[] = [];
|
|
129
|
+
|
|
130
|
+
/** This function gives the spine from the point `a` to the point `b`. The spine is a spiral arc in free space, or a way through the corner graph. */
|
|
131
|
+
const spineOf = (root: Point, a: { r: number; theta: number; point: Point }, b: { r: number; theta: number; point: Point }): Point[] | undefined => {
|
|
132
|
+
if (finder.obstacles.inside(a.point, pad) || finder.obstacles.inside(b.point, pad)) return undefined;
|
|
133
|
+
let dt = wrap(b.theta - a.theta);
|
|
134
|
+
if (dt > Math.PI) dt -= TAU;
|
|
135
|
+
const arc: Point[] = [a.point];
|
|
136
|
+
for (let s = 1; s < arcPoints - 1; s++) {
|
|
137
|
+
const u = s / (arcPoints - 1);
|
|
138
|
+
const r = a.r * Math.pow(b.r / a.r, u);
|
|
139
|
+
const theta = a.theta + dt * u;
|
|
140
|
+
arc.push({ x: root.x + r * Math.cos(theta), y: root.y + r * Math.sin(theta) });
|
|
141
|
+
}
|
|
142
|
+
arc.push(b.point);
|
|
143
|
+
if (finder.obstacles.clearPath(arc, pad)) return arc;
|
|
144
|
+
const way = finder.corners.path(a.point, b.point);
|
|
145
|
+
return way ? finder.corners.pull(way) : undefined;
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
for (const [hubId] of hubs) {
|
|
149
|
+
const hub: Node | undefined = byId.get(hubId);
|
|
150
|
+
if (!hub) continue;
|
|
151
|
+
const root = center(hub.box);
|
|
152
|
+
const rMin = Math.hypot(hub.box.w, hub.box.h) / 2 + 2 * finder.margin;
|
|
153
|
+
const fan = plugs.filter(({ edge }) => !used.has(edge.id) && edge.source.node !== edge.target.node && (edge.source.node === hubId || edge.target.node === hubId));
|
|
154
|
+
if (fan.length < o.minTargets) continue;
|
|
155
|
+
const out = new Map<string, boolean>();
|
|
156
|
+
const near = new Map<string, Point>();
|
|
157
|
+
const far = new Map<string, Point>();
|
|
158
|
+
const leaves: SpiralNode[] = fan.map(({ edge, ps, pt }) => {
|
|
159
|
+
const outward = edge.source.node === hubId;
|
|
160
|
+
out.set(edge.id, outward);
|
|
161
|
+
near.set(edge.id, outward ? ps.point : pt.point);
|
|
162
|
+
const p = outward ? pt.point : ps.point;
|
|
163
|
+
far.set(edge.id, p);
|
|
164
|
+
const box = byId.get(outward ? edge.target.node : edge.source.node)!.box;
|
|
165
|
+
const clear = Math.hypot(box.w, box.h) / 2 + finder.margin;
|
|
166
|
+
return { r: Math.max(1e-6, dist(root, p)), theta: wrap(Math.atan2(p.y - root.y, p.x - root.x)), point: p, clear, edge: edge.id, children: [] };
|
|
167
|
+
});
|
|
168
|
+
for (const { edge } of fan) used.add(edge.id);
|
|
169
|
+
const tops = spiralTree(leaves, alpha, root, rMin);
|
|
170
|
+
// The branches: each link from a node to a child with two leaves or more, from the root down.
|
|
171
|
+
interface Branch {
|
|
172
|
+
readonly spine: Point[];
|
|
173
|
+
readonly edges: string[];
|
|
174
|
+
readonly depth: number;
|
|
175
|
+
}
|
|
176
|
+
const branches: Branch[] = [];
|
|
177
|
+
const walk = (parent: { r: number; theta: number; point: Point }, child: SpiralNode, depth: number): void => {
|
|
178
|
+
if (child.edge !== undefined) return;
|
|
179
|
+
const spine = dist(parent.point, child.point) > finder.margin ? spineOf(root, parent, child) : undefined;
|
|
180
|
+
if (spine && spine.length >= 2) branches.push({ spine, edges: leavesOf(child), depth });
|
|
181
|
+
for (const c of child.children) if (dist(c.point, child.point) > 1e-9) walk(child, c, depth + 1);
|
|
182
|
+
// A child at the point of the join (the nearer node of a join) goes on from the same point.
|
|
183
|
+
for (const c of child.children) if (dist(c.point, child.point) <= 1e-9) for (const g of c.children) walk(child, g, depth + 1);
|
|
184
|
+
};
|
|
185
|
+
for (const top of tops) {
|
|
186
|
+
if (top.edge !== undefined) continue;
|
|
187
|
+
// The trunk from the hub: it starts at the radius `rMin` on the line to the top node.
|
|
188
|
+
const start = { r: rMin, theta: top.theta, point: { x: root.x + rMin * Math.cos(top.theta), y: root.y + rMin * Math.sin(top.theta) } };
|
|
189
|
+
walk(top.r > rMin + finder.margin ? start : top, top, 0);
|
|
190
|
+
}
|
|
191
|
+
for (const b of branches) {
|
|
192
|
+
if (b.edges.length < 2) continue;
|
|
193
|
+
const last = b.spine.length - 1;
|
|
194
|
+
const tripOf = (edge: string): Point[] => [near.get(edge)!, ...b.spine, far.get(edge)!];
|
|
195
|
+
const lanes = orderLanes(
|
|
196
|
+
b.spine,
|
|
197
|
+
b.edges.map((edge) => ({ edge, points: tripOf(edge) })),
|
|
198
|
+
"bundle",
|
|
199
|
+
);
|
|
200
|
+
const members: HighwayMember[] = lanes.map((edge) => (out.get(edge) ? memberAt(b.spine, edge, 0, last, b.depth) : memberAt(b.spine, edge, last, 0, -b.depth)));
|
|
201
|
+
highways.push({ id: `h${highways.length}`, key: `spiral:${hubId}`, spine: b.spine, lanes, members });
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
return tidyPlan({ highways });
|
|
205
|
+
}
|