@mark1russell7/highways 0.5.0 → 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.
Files changed (56) hide show
  1. package/README.md +43 -2
  2. package/build/index.d.ts +1472 -63
  3. package/build/index.d.ts.map +1 -1
  4. package/build/index.js +10007 -3406
  5. package/build/index.js.map +1 -1
  6. package/package.json +11 -2
  7. package/src/bench/exact/index.ts +72 -11
  8. package/src/bench/exact/optima-exact-v1-bounds.json +14409 -0
  9. package/src/bench/exact/optima-exact-v1.json +4809 -0
  10. package/src/bench/exact/solve.ts +129 -0
  11. package/src/bench/screen/activation.ts +3 -0
  12. package/src/bench/screen/bounds.ts +140 -0
  13. package/src/bench/screen/compare.ts +26 -0
  14. package/src/bench/screen/exact-set.ts +100 -0
  15. package/src/bench/screen/exercise.ts +290 -0
  16. package/src/bench/screen/forms.ts +14 -0
  17. package/src/bench/screen/gap.ts +316 -0
  18. package/src/bench/screen/hash.ts +7 -76
  19. package/src/bench/screen/index.ts +6 -0
  20. package/src/bench/screen/measures.ts +71 -1
  21. package/src/bench/screen/portfolio-pick.ts +20 -0
  22. package/src/bench/screen/screen.ts +50 -2
  23. package/src/bench/screen/set.ts +3 -2
  24. package/src/highways/areas/index.ts +160 -0
  25. package/src/highways/confluent/index.ts +154 -0
  26. package/src/highways/confluent/k23.test-util.ts +8 -0
  27. package/src/highways/consensus/index.ts +129 -0
  28. package/src/highways/crossbars/index.ts +184 -0
  29. package/src/highways/cubu/index.ts +26 -0
  30. package/src/highways/divided/index.ts +111 -0
  31. package/src/highways/fdeb/index.ts +193 -0
  32. package/src/highways/frechet/index.ts +232 -0
  33. package/src/highways/hierarchy/index.ts +140 -0
  34. package/src/highways/index.ts +64 -0
  35. package/src/highways/kdeeb/index.ts +355 -0
  36. package/src/highways/link-communities/index.ts +189 -0
  37. package/src/highways/mesh/index.ts +209 -0
  38. package/src/highways/mlseb/index.ts +204 -0
  39. package/src/highways/p-hub/index.ts +248 -0
  40. package/src/highways/power/index.ts +117 -0
  41. package/src/highways/resistance/index.ts +159 -0
  42. package/src/highways/sbeb/index.ts +303 -0
  43. package/src/highways/scale-space/index.ts +135 -0
  44. package/src/highways/spectral/index.ts +391 -0
  45. package/src/highways/territories/index.ts +150 -0
  46. package/src/highways/trace.ts +153 -0
  47. package/src/index.ts +1 -0
  48. package/src/portfolio/bands.ts +33 -0
  49. package/src/portfolio/fit.ts +29 -0
  50. package/src/portfolio/index.ts +6 -0
  51. package/src/portfolio/key.ts +98 -0
  52. package/src/portfolio/members.ts +171 -0
  53. package/src/portfolio/router.ts +133 -0
  54. package/src/portfolio/sets.ts +28 -0
  55. package/src/strategies/builtins.ts +222 -1
  56. package/src/strategies/schemas.ts +280 -1
@@ -0,0 +1,135 @@
1
+ import { boardUnit } from "../../bench/measures/routes.ts";
2
+ import type { Point } from "../../geom.ts";
3
+ import { firstRoutes } from "../base.ts";
4
+ import { groupedPlan, type EdgeGroup, type GroupSpine } from "../grouped.ts";
5
+ import type { HighwayInput, HighwayPlan } from "../model.ts";
6
+ import { mdf, resample, type BundleLine } from "../quickbundles/index.ts";
7
+ import { edgePlugs } from "../stretches.ts";
8
+
9
+ /** The options of the highway strategy `scale-space` (HWY-053 of the note on highways). */
10
+ export interface ScaleSpaceOptions {
11
+ /** The first scale, in units of the board. The default is 1.5. */
12
+ readonly theta: number;
13
+ /** The scale of each next scale is this value times the scale before it. The default is the root of 2. */
14
+ readonly ratio: number;
15
+ /** The number of scales. The default is 8. */
16
+ readonly scales: number;
17
+ /** The scale of the plan, from 0 (the first scale). With the defaults, the scale 2 is 3 units, as in the level 0 of `nested`. The default is 2. */
18
+ readonly level: number;
19
+ /** A cluster gets highways only when its persistence `ln(σ_death / σ_birth)` is this value or more. The default is 0.6: two steps of the root of 2. */
20
+ readonly p0: number;
21
+ /** The number of points of each line after the resampling. The default is 12. */
22
+ readonly points: number;
23
+ /** The line of each edge. The default is `routes`. */
24
+ readonly line: BundleLine;
25
+ /** A cluster with this number of edges or more gets highways. The default is 2. */
26
+ readonly minSize: number;
27
+ /** The spine of each cluster. The default is `shared`. */
28
+ readonly spine: GroupSpine;
29
+ }
30
+
31
+ /** The default options of the strategy `scale-space`. */
32
+ export const SCALE_SPACE_DEFAULTS: Readonly<ScaleSpaceOptions> = Object.freeze({
33
+ theta: 1.5,
34
+ ratio: Math.SQRT2,
35
+ scales: 8,
36
+ level: 2,
37
+ p0: 0.6,
38
+ points: 12,
39
+ line: "routes",
40
+ minSize: 2,
41
+ spine: "shared",
42
+ });
43
+
44
+ /** One cluster of one scale: its edges (as indices), and the scales of its birth and its death. */
45
+ export interface ScaleCluster {
46
+ readonly members: readonly number[];
47
+ /** The first scale of the cluster. A cluster of the first scale has the birth 0. */
48
+ readonly birth: number;
49
+ /** The first scale where the cluster joins a larger one. A cluster that lives past the last scale has the death `scales`. */
50
+ readonly death: number;
51
+ }
52
+
53
+ /**
54
+ * This function gives the clusters of the single linkage of `n` items at each scale. The distance `d(i, j)` of each pair is in `pairs`.
55
+ * At the scale `s`, two items with a distance of `limits[s]` or less are in one cluster. Thus each cluster of a scale is in one cluster of each larger scale.
56
+ * The result has the clusters of each scale with 2 items or more, with their birth and their death.
57
+ */
58
+ export function scaleClusters(n: number, pairs: readonly { i: number; j: number; d: number }[], limits: readonly number[]): ScaleCluster[][] {
59
+ const parent = new Int32Array(n);
60
+ for (let i = 0; i < n; i++) parent[i] = i;
61
+ const find = (i: number): number => {
62
+ while (parent[i] !== i) i = parent[i] = parent[parent[i]!]!;
63
+ return i;
64
+ };
65
+ const sorted = [...pairs].sort((a, b) => a.d - b.d || a.i - b.i || a.j - b.j);
66
+ // The member sets of each scale, as sorted keys.
67
+ const levels: number[][][] = [];
68
+ let next = 0;
69
+ for (const limit of limits) {
70
+ for (; next < sorted.length && sorted[next]!.d <= limit; next++) {
71
+ const a = find(sorted[next]!.i);
72
+ const b = find(sorted[next]!.j);
73
+ if (a !== b) parent[Math.max(a, b)] = Math.min(a, b);
74
+ }
75
+ const sets = new Map<number, number[]>();
76
+ for (let i = 0; i < n; i++) {
77
+ const r = find(i);
78
+ const list = sets.get(r);
79
+ if (list) list.push(i);
80
+ else sets.set(r, [i]);
81
+ }
82
+ levels.push([...sets.values()].filter((s) => s.length >= 2));
83
+ }
84
+ const keyOf = (s: readonly number[]): string => s.join(",");
85
+ const keys = levels.map((sets) => new Set(sets.map(keyOf)));
86
+ return levels.map((sets, s) =>
87
+ sets.map((members) => {
88
+ const key = keyOf(members);
89
+ let birth = s;
90
+ while (birth > 0 && keys[birth - 1]!.has(key)) birth--;
91
+ let death = s + 1;
92
+ while (death < levels.length && keys[death]!.has(key)) death++;
93
+ return { members, birth, death };
94
+ }),
95
+ );
96
+ }
97
+
98
+ /**
99
+ * This function gives the groups of the edges at the scale `level` that persist (HWY-053). The scale `s` is `theta × ratio^s` units of the board.
100
+ * A cluster of the level gets a group when its persistence `(death - birth) × ln(ratio)` is `p0` or more. Each edge of a group gets `flip` against the first edge of the group.
101
+ */
102
+ export function scaleSpaceGroups(input: HighwayInput, o: Readonly<ScaleSpaceOptions>): Map<string, EdgeGroup> {
103
+ const graph = input.graph;
104
+ const unit = boardUnit(graph);
105
+ const scales = Math.max(1, Math.round(o.scales));
106
+ const ratio = Math.max(1.01, o.ratio);
107
+ const limits = Array.from({ length: scales }, (_, s) => o.theta * ratio ** s * unit);
108
+ const k = Math.max(2, Math.round(o.points));
109
+ const routes = o.line === "routes" ? firstRoutes(input) : undefined;
110
+ const lines: { edge: string; line: Point[] }[] = edgePlugs(graph).map(({ edge, ps, pt }) => ({ edge: edge.id, line: resample(routes?.get(edge.id) ?? [ps.point, pt.point], k) }));
111
+ const top = limits[limits.length - 1]!;
112
+ const pairs: { i: number; j: number; d: number }[] = [];
113
+ for (let i = 0; i < lines.length; i++)
114
+ for (let j = i + 1; j < lines.length; j++) {
115
+ const d = mdf(lines[i]!.line, lines[j]!.line).d;
116
+ if (d <= top) pairs.push({ i, j, d });
117
+ }
118
+ const level = Math.max(0, Math.min(scales - 1, Math.round(o.level)));
119
+ const clusters = scaleClusters(lines.length, pairs, limits)[level]!;
120
+ const groups = new Map<string, EdgeGroup>();
121
+ clusters.forEach((c, ci) => {
122
+ if ((c.death - c.birth) * Math.log(ratio) < o.p0 - 1e-9) return;
123
+ const lead = lines[c.members[0]!]!.line;
124
+ for (const i of c.members) groups.set(lines[i]!.edge, { key: `s${level}.${ci}`, flip: mdf(lines[i]!.line, lead).flip });
125
+ });
126
+ return groups;
127
+ }
128
+
129
+ /**
130
+ * This function gives the highways of one scale of the scale space with persistence (HWY-053). Only the clusters that live over `p0` of the log scale get highways.
131
+ * A short-lived cluster is noise, so a zoom level shows only the stable structure.
132
+ */
133
+ export function scaleSpacePlan(input: HighwayInput, o: Readonly<ScaleSpaceOptions>): HighwayPlan {
134
+ return groupedPlan(input, scaleSpaceGroups(input, o), { spine: o.spine, minSize: Math.max(2, Math.round(o.minSize)) });
135
+ }
@@ -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,150 @@
1
+ import { boardUnit } from "../../bench/measures/routes.ts";
2
+ import { dist, polylineLength, type Point } from "../../geom.ts";
3
+ import { firstRoutes } from "../base.ts";
4
+ import type { HighwayInput, HighwayPlan } from "../model.ts";
5
+ import { snapPlan, type SnapLine, type SnapMember } from "../snap.ts";
6
+ import { edgePlugs } from "../stretches.ts";
7
+
8
+ /** The characteristic points of the territories: the plugs, the bends of the first routes, or both. */
9
+ export type TerritoryPoints = "plugs" | "bends" | "both";
10
+
11
+ /** The values of `TerritoryPoints`. */
12
+ export const TERRITORY_POINTS: readonly TerritoryPoints[] = Object.freeze(["plugs", "bends", "both"]);
13
+
14
+ /** The options of the highway strategy `territories` (HWY-033 of the note on highways). */
15
+ export interface TerritoryOptions {
16
+ /** The radius of a group of points, in units of the board. The default is 4. */
17
+ readonly radius: number;
18
+ /** A flow between two cells becomes a highway when this number of edges or more cross it. The default is 2. */
19
+ readonly minSize: number;
20
+ /** The characteristic points. The default is `both`. */
21
+ readonly points: TerritoryPoints;
22
+ }
23
+
24
+ /** The default options of the strategy `territories`. */
25
+ export const TERRITORY_DEFAULTS: Readonly<TerritoryOptions> = Object.freeze({ radius: 4, minSize: 2, points: "both" });
26
+
27
+ /**
28
+ * This function puts points into groups of the radius `r` (Andrienko and Andrienko 2011). Each point joins the group with the nearest centre within `r`.
29
+ * Else it starts a new group. The centre of a group is the mean of its points. A second pass puts each point into the group of its nearest centre.
30
+ * The result is the centres of the groups that keep points.
31
+ */
32
+ export function pointGroups(points: readonly Point[], r: number): Point[] {
33
+ const sums: { x: number; y: number; n: number }[] = [];
34
+ const centre = (g: { x: number; y: number; n: number }): Point => ({ x: g.x / g.n, y: g.y / g.n });
35
+ for (const p of points) {
36
+ let best = -1;
37
+ let bestD = Infinity;
38
+ sums.forEach((g, i) => {
39
+ const d = dist(p, centre(g));
40
+ if (d < bestD) [best, bestD] = [i, d];
41
+ });
42
+ if (best >= 0 && bestD <= r) {
43
+ const g = sums[best]!;
44
+ g.x += p.x;
45
+ g.y += p.y;
46
+ g.n++;
47
+ } else sums.push({ x: p.x, y: p.y, n: 1 });
48
+ }
49
+ const first = sums.map(centre);
50
+ const again = first.map(() => ({ x: 0, y: 0, n: 0 }));
51
+ for (const p of points) {
52
+ const i = nearestCentre(first, p);
53
+ again[i]!.x += p.x;
54
+ again[i]!.y += p.y;
55
+ again[i]!.n++;
56
+ }
57
+ return again.filter((g) => g.n > 0).map(centre);
58
+ }
59
+
60
+ /** This function gives the index of the centre nearest to `p`. The first of two equal centres wins. */
61
+ export function nearestCentre(centres: readonly Point[], p: Point): number {
62
+ let best = -1;
63
+ let bestD = Infinity;
64
+ centres.forEach((c, i) => {
65
+ const d = dist(p, c);
66
+ if (d < bestD) [best, bestD] = [i, d];
67
+ });
68
+ return best;
69
+ }
70
+
71
+ /** One crossing of a route from the cell `from` into the cell `to`, at the share `at` of the length of the route. */
72
+ export interface CellStep {
73
+ readonly from: number;
74
+ readonly to: number;
75
+ readonly at: number;
76
+ }
77
+
78
+ /**
79
+ * This function gives the crossings of the cells of a polyline. The cells are the Voronoi cells of the centres.
80
+ * It samples the polyline at the spacing `step`, and gives each change of the nearest centre.
81
+ */
82
+ export function cellSteps(line: readonly Point[], centres: readonly Point[], step: number): CellStep[] {
83
+ const total = polylineLength(line);
84
+ if (line.length < 2 || centres.length === 0 || total <= 0) return [];
85
+ const out: CellStep[] = [];
86
+ let cell = nearestCentre(centres, line[0]!);
87
+ let walked = 0;
88
+ for (let i = 1; i < line.length; i++) {
89
+ const a = line[i - 1]!;
90
+ const b = line[i]!;
91
+ const len = dist(a, b);
92
+ const n = Math.max(1, Math.ceil(len / Math.max(1e-6, step)));
93
+ for (let k = 1; k <= n; k++) {
94
+ const t = k / n;
95
+ const c = nearestCentre(centres, { x: a.x + (b.x - a.x) * t, y: a.y + (b.y - a.y) * t });
96
+ if (c !== cell) {
97
+ out.push({ from: cell, to: c, at: (walked + len * t) / total });
98
+ cell = c;
99
+ }
100
+ }
101
+ walked += len;
102
+ }
103
+ return out;
104
+ }
105
+
106
+ /**
107
+ * This function gives the highways of the territories of the flows (HWY-033, Andrienko and Andrienko 2011).
108
+ * The characteristic points form groups of the radius `radius`. The centres of the groups make Voronoi cells.
109
+ * Each first route becomes a list of crossings between neighbour cells.
110
+ *
111
+ * A pair of cells with `minSize` crossings or more is a line from centre to centre. The edges that cross it snap to it. The centres of the cells are the interchanges.
112
+ */
113
+ export function territoryPlan(input: HighwayInput, o: Readonly<TerritoryOptions>): HighwayPlan {
114
+ const graph = input.graph;
115
+ const unit = boardUnit(graph);
116
+ const r = Math.max(0.5, o.radius) * unit;
117
+ const routes = firstRoutes(input);
118
+ const plugs = edgePlugs(graph);
119
+ const points: Point[] = [];
120
+ for (const { edge, ps, pt } of plugs) {
121
+ if (o.points !== "bends") points.push(ps.point, pt.point);
122
+ if (o.points !== "plugs") {
123
+ const route = routes.get(edge.id) ?? [];
124
+ for (let i = 1; i < route.length - 1; i++) points.push(route[i]!);
125
+ }
126
+ }
127
+ if (points.length === 0) return { highways: [] };
128
+ const centres = pointGroups(points, r);
129
+ // The crossings of each pair of cells, in the order of the edges.
130
+ const flows = new Map<string, { a: number; b: number; members: SnapMember[] }>();
131
+ for (const { edge, ps, pt } of plugs) {
132
+ const route = routes.get(edge.id) ?? [ps.point, pt.point];
133
+ const seen = new Set<string>();
134
+ for (const s of cellSteps(route, centres, r / 4)) {
135
+ const [a, b] = s.from < s.to ? [s.from, s.to] : [s.to, s.from];
136
+ const key = `${a}:${b}`;
137
+ if (seen.has(key)) continue;
138
+ seen.add(key);
139
+ const flow = flows.get(key) ?? { a, b, members: [] };
140
+ flow.members.push({ edge: edge.id, from: centres[s.from]!, to: centres[s.to]!, at: s.at });
141
+ flows.set(key, flow);
142
+ }
143
+ }
144
+ const min = Math.max(2, Math.round(o.minSize));
145
+ const lines: SnapLine[] = [...flows.values()]
146
+ .filter((f) => f.members.length >= min)
147
+ .sort((p, q) => p.a - q.a || p.b - q.b)
148
+ .map((f) => ({ key: `t${f.a}-${f.b}`, a: centres[f.a]!, b: centres[f.b]!, members: f.members }));
149
+ return snapPlan(input, lines, { minSize: min });
150
+ }