@mark1russell7/highways 0.2.0 → 0.4.0
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 +265 -9
- package/build/index.d.ts +2168 -101
- package/build/index.d.ts.map +1 -1
- package/build/index.js +11624 -3422
- package/build/index.js.map +1 -1
- package/package.json +4 -1
- package/src/bench/compare.ts +180 -0
- package/src/bench/curves.ts +514 -2
- package/src/bench/links.ts +28 -6
- package/src/bench/measures/bundles.ts +119 -0
- package/src/bench/measures/curves.ts +211 -0
- package/src/bench/measures/groups.ts +122 -0
- package/src/bench/measures/index.ts +9 -0
- package/src/bench/measures/levels.ts +103 -0
- package/src/bench/measures/pareto.ts +37 -0
- package/src/bench/measures/room.ts +43 -0
- package/src/bench/measures/routes.ts +226 -0
- package/src/bench/measures/stability.ts +104 -0
- package/src/bench/measures/stats.ts +114 -0
- package/src/bench/properties.ts +18 -2
- package/src/bench/room.ts +209 -2
- package/src/bench/run.ts +86 -11
- package/src/bench/screen/forms.ts +372 -0
- package/src/bench/screen/generators.ts +54 -0
- package/src/bench/screen/hash.ts +129 -0
- package/src/bench/screen/index.ts +6 -0
- package/src/bench/screen/measures.ts +114 -0
- package/src/bench/screen/screen.ts +390 -0
- package/src/bench/screen/set.ts +136 -0
- package/src/bench/suite.ts +839 -0
- package/src/cases.ts +41 -5
- package/src/curves/clothoid/index.ts +150 -0
- package/src/curves/corner.ts +116 -0
- package/src/curves/fit.ts +79 -0
- package/src/curves/highway/index.ts +636 -0
- package/src/curves/index.ts +16 -1
- package/src/curves/model.ts +4 -2
- package/src/curves/smooth/index.ts +50 -0
- package/src/curves/space.ts +76 -0
- package/src/groups/cut/index.ts +292 -0
- package/src/groups/index.ts +2 -0
- package/src/groups/kd/index.ts +207 -0
- package/src/highways/corridors/index.ts +305 -0
- package/src/highways/follow.ts +155 -0
- package/src/highways/index.ts +7 -0
- package/src/highways/keys/index.ts +15 -0
- package/src/highways/model.ts +111 -0
- package/src/highways/result.ts +34 -0
- package/src/highways/router.ts +40 -0
- package/src/highways/shared-ways/index.ts +15 -0
- package/src/index.ts +6 -0
- package/src/levels.ts +163 -5
- package/src/room/bends.ts +203 -0
- package/src/room/index.ts +28 -0
- package/src/room/levels/index.ts +155 -0
- package/src/routers/sections.ts +15 -40
- package/src/routers/shared.ts +7 -2
- package/src/routers/trunks.ts +131 -6
- package/src/sections.ts +47 -1
- package/src/segments.ts +81 -31
- package/src/strategies/builtins.ts +206 -9
- package/src/strategies/index.ts +1 -0
- package/src/strategies/model.ts +55 -7
- package/src/strategies/options.ts +296 -0
- package/src/strategies/schemas.ts +234 -0
- package/src/visibility.ts +190 -1
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
import { dist, polylineLength, turns, type Point } from "../../geom.ts";
|
|
2
|
+
import { rng } from "../../fixtures.ts";
|
|
3
|
+
import { Grid } from "../../grid.ts";
|
|
4
|
+
import { nodeIndex, type Graph, type Route } from "../../model.ts";
|
|
5
|
+
import { nearestSide } from "../../plugs.ts";
|
|
6
|
+
import { TOLERANCE } from "../properties.ts";
|
|
7
|
+
import { quantile } from "./stats.ts";
|
|
8
|
+
|
|
9
|
+
/** A drawn segment: a pair of points. */
|
|
10
|
+
export type DrawnSegment = readonly [Point, Point];
|
|
11
|
+
|
|
12
|
+
const key = (p: Point): string => `${Math.round(p.x * 8)},${Math.round(p.y * 8)}`;
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* This function gives the drawn segments of the routes: one segment for each distinct pair of ends.
|
|
16
|
+
* A segment that several routes share is one drawn segment, as in `measureQuality`.
|
|
17
|
+
*/
|
|
18
|
+
export function drawnSegments(routes: readonly Route[]): DrawnSegment[] {
|
|
19
|
+
const segs: DrawnSegment[] = [];
|
|
20
|
+
const seen = new Set<string>();
|
|
21
|
+
for (const r of routes) {
|
|
22
|
+
const p = r.points;
|
|
23
|
+
for (let i = 1; i < p.length; i++) {
|
|
24
|
+
const ka = key(p[i - 1]!);
|
|
25
|
+
const kb = key(p[i]!);
|
|
26
|
+
if (ka === kb) continue;
|
|
27
|
+
const k = ka < kb ? `${ka}|${kb}` : `${kb}|${ka}`;
|
|
28
|
+
if (seen.has(k)) continue;
|
|
29
|
+
seen.add(k);
|
|
30
|
+
segs.push([p[i - 1]!, p[i]!]);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return segs;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** This function gives the key of the drawn segment between two points. Two routes share a segment when they give the same key. */
|
|
37
|
+
export function segmentKey(a: Point, b: Point): string | undefined {
|
|
38
|
+
const ka = key(a);
|
|
39
|
+
const kb = key(b);
|
|
40
|
+
if (ka === kb) return undefined;
|
|
41
|
+
return ka < kb ? `${ka}|${kb}` : `${kb}|${ka}`;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* This function gives the unit of length of a board: the median of the short sides of the boxes of the nodes.
|
|
46
|
+
* A length divided by the unit has no unit, so boards of different sizes compare. The unit of a board without nodes is 1.
|
|
47
|
+
*/
|
|
48
|
+
export function boardUnit(graph: Graph): number {
|
|
49
|
+
if (graph.nodes.length === 0) return 1;
|
|
50
|
+
const u = quantile(
|
|
51
|
+
graph.nodes.map((n) => Math.min(n.box.w, n.box.h)),
|
|
52
|
+
0.5,
|
|
53
|
+
);
|
|
54
|
+
return u > 0 ? u : 1;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** The crossings of a set of drawn segments and their angles (R1 and R2 of MEAS0). */
|
|
58
|
+
export interface CrossingAngles {
|
|
59
|
+
/** The number of crossings (R1). It is the same value as `crossings` of `measureQuality`. */
|
|
60
|
+
readonly crossings: number;
|
|
61
|
+
/** This value is true when the values come from a sample of the segments. */
|
|
62
|
+
readonly estimated: boolean;
|
|
63
|
+
/** The mean angle of a crossing, divided by 90 degrees (R2). The scale is 0 to 1, and higher is better. A set without crossings gives 1. */
|
|
64
|
+
readonly mean: number;
|
|
65
|
+
/** The smallest angle of a crossing, in degrees from 0 to 90 (R2). Higher is better. A set without crossings gives 90. */
|
|
66
|
+
readonly min: number;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const EXACT_CROSSINGS = 20_000;
|
|
70
|
+
const SAMPLE = 2_000;
|
|
71
|
+
|
|
72
|
+
/** This function gives the angle between two segments, in degrees from 0 to 90. */
|
|
73
|
+
export function crossingAngle(a: Point, b: Point, c: Point, d: Point): number {
|
|
74
|
+
const ux = b.x - a.x;
|
|
75
|
+
const uy = b.y - a.y;
|
|
76
|
+
const vx = d.x - c.x;
|
|
77
|
+
const vy = d.y - c.y;
|
|
78
|
+
const l = Math.hypot(ux, uy) * Math.hypot(vx, vy);
|
|
79
|
+
if (l === 0) return 0;
|
|
80
|
+
return (Math.acos(Math.min(1, Math.abs(ux * vx + uy * vy) / l)) * 180) / Math.PI;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* This function measures the crossings of a set of segments and their angles (R1 and R2 of MEAS0).
|
|
85
|
+
* It uses the grid and the sample of `countCrossings`. Above 20 000 segments, a seeded sample of 2 000 segments gives an estimate.
|
|
86
|
+
* The cost is `n log n`.
|
|
87
|
+
*/
|
|
88
|
+
export function crossingAngles(segs: readonly DrawnSegment[]): CrossingAngles {
|
|
89
|
+
if (segs.length < 2) return { crossings: 0, estimated: false, mean: 1, min: 90 };
|
|
90
|
+
const len = segs.reduce((s, [a, b]) => s + dist(a, b), 0) / segs.length;
|
|
91
|
+
const grid = new Grid<number>(Math.max(20, len));
|
|
92
|
+
segs.forEach(([a, b], i) => grid.addSegment(i, a, b));
|
|
93
|
+
const exact = segs.length <= EXACT_CROSSINGS;
|
|
94
|
+
const R = rng(segs.length);
|
|
95
|
+
const pick = exact ? segs.map((_, i) => i) : Array.from({ length: SAMPLE }, () => Math.floor(R() * segs.length));
|
|
96
|
+
let count = 0;
|
|
97
|
+
let sum = 0;
|
|
98
|
+
let min = 90;
|
|
99
|
+
for (const i of pick) {
|
|
100
|
+
const [a, b] = segs[i]!;
|
|
101
|
+
grid.querySegment(a, b, 0, (j) => {
|
|
102
|
+
if (exact && j <= i) return;
|
|
103
|
+
if (j === i) return;
|
|
104
|
+
const [c, d] = segs[j]!;
|
|
105
|
+
if (!segmentsCrossInside(a, b, c, d)) return;
|
|
106
|
+
count++;
|
|
107
|
+
const t = crossingAngle(a, b, c, d);
|
|
108
|
+
sum += t;
|
|
109
|
+
min = Math.min(min, t);
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
const crossings = exact ? count : Math.round((count * segs.length) / (2 * pick.length));
|
|
113
|
+
return { crossings, estimated: !exact, mean: count ? sum / count / 90 : 1, min: count ? min : 90 };
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const orient = (a: Point, b: Point, c: Point): number => (b.x - a.x) * (c.y - a.y) - (b.y - a.y) * (c.x - a.x);
|
|
117
|
+
|
|
118
|
+
/** This test is the test of `segmentsCross`, so the count of `crossingAngles` is the count of `countCrossings`. */
|
|
119
|
+
function segmentsCrossInside(a: Point, b: Point, c: Point, d: Point): boolean {
|
|
120
|
+
return orient(c, d, a) * orient(c, d, b) < 0 && orient(a, b, c) * orient(a, b, d) < 0;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** The total turn of the routes (R4 of MEAS0). */
|
|
124
|
+
export interface TotalTurn {
|
|
125
|
+
/** The mean over the routes of `T(r) = Σ abs(φ_i) / π`, in half turns. Lower is better. */
|
|
126
|
+
readonly mean: number;
|
|
127
|
+
/** The largest turn at one vertex of a route, in degrees from 0 to 180. Lower is better. */
|
|
128
|
+
readonly max: number;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/** This function measures the total turn of the routes (R4 of MEAS0). A path that goes straight on has 0. The cost is linear. */
|
|
132
|
+
export function totalTurn(routes: readonly Route[]): TotalTurn {
|
|
133
|
+
let sum = 0;
|
|
134
|
+
let max = 0;
|
|
135
|
+
for (const r of routes) {
|
|
136
|
+
let t = 0;
|
|
137
|
+
for (const phi of turns(r.points)) {
|
|
138
|
+
t += Math.abs(phi);
|
|
139
|
+
max = Math.max(max, Math.abs(phi));
|
|
140
|
+
}
|
|
141
|
+
sum += t / Math.PI;
|
|
142
|
+
}
|
|
143
|
+
return { mean: routes.length ? sum / routes.length : 0, max: (max * 180) / Math.PI };
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/** The spread of the detour of the routes (R5 of MEAS0). */
|
|
147
|
+
export interface DetourSpread {
|
|
148
|
+
/** The median of `δ(e) = L(r_e) / span(e)`. */
|
|
149
|
+
readonly median: number;
|
|
150
|
+
/** The `0.95` quantile of `δ(e)`: the tail of bad routes. */
|
|
151
|
+
readonly q95: number;
|
|
152
|
+
readonly mean: number;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* This function measures the spread of the detour `δ(e) = L(r_e) / span(e)` of the routes (R5 of MEAS0).
|
|
157
|
+
* A route with ends nearer than 1 unit does not count. Each value is 1 or more, and lower is better. The cost is linear.
|
|
158
|
+
*/
|
|
159
|
+
export function detourSpread(routes: readonly Route[]): DetourSpread {
|
|
160
|
+
const d: number[] = [];
|
|
161
|
+
for (const r of routes) {
|
|
162
|
+
const p = r.points;
|
|
163
|
+
if (p.length < 2) continue;
|
|
164
|
+
const span = dist(p[0]!, p[p.length - 1]!);
|
|
165
|
+
if (span > 1) d.push(polylineLength(p) / span);
|
|
166
|
+
}
|
|
167
|
+
if (d.length === 0) return { median: 1, q95: 1, mean: 1 };
|
|
168
|
+
return { median: quantile(d, 0.5), q95: quantile(d, 0.95), mean: d.reduce((s, x) => s + x, 0) / d.length };
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/** The spacing of the plugs on the sides of the boxes (R11 of MEAS0). */
|
|
172
|
+
export interface PlugSpacing {
|
|
173
|
+
/** The smallest spacing of a side. The scale is 0 to 1, and higher is better. A value of 1 is even spacing. */
|
|
174
|
+
readonly min: number;
|
|
175
|
+
/** The `0.05` quantile of the spacing of the sides. */
|
|
176
|
+
readonly q05: number;
|
|
177
|
+
/** The number of sides with two plugs or more. */
|
|
178
|
+
readonly sides: number;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* This function measures the spacing of the plugs on each side of each box (R11 of MEAS0), the angular resolution of a box.
|
|
183
|
+
* For a side with `k` distinct plugs, `ps = min gap / (side / (k + 1))`, with a limit of 1.
|
|
184
|
+
* Two ends nearer than `TOLERANCE` are one plug, because a bundle shares its plug. An end farther than 1 unit from the side does not count.
|
|
185
|
+
* Without a side with two plugs, each value is 1. The cost is `n log n`.
|
|
186
|
+
*/
|
|
187
|
+
export function plugSpacing(graph: Graph, routes: readonly Route[]): PlugSpacing {
|
|
188
|
+
const byId = nodeIndex(graph);
|
|
189
|
+
const edges = new Map(graph.edges.map((e) => [e.id, e]));
|
|
190
|
+
const at = new Map<string, number[]>();
|
|
191
|
+
const add = (nodeId: string, p: Point): void => {
|
|
192
|
+
const n = byId.get(nodeId);
|
|
193
|
+
if (!n) return;
|
|
194
|
+
const b = n.box;
|
|
195
|
+
const side = nearestSide(b, p);
|
|
196
|
+
const off = side === "north" ? Math.abs(p.y - b.y) : side === "south" ? Math.abs(p.y - b.y - b.h) : side === "west" ? Math.abs(p.x - b.x) : Math.abs(p.x - b.x - b.w);
|
|
197
|
+
if (off > 1) return;
|
|
198
|
+
const pos = side === "north" || side === "south" ? p.x - b.x : p.y - b.y;
|
|
199
|
+
const k = `${nodeId}\u0000${side}`;
|
|
200
|
+
const list = at.get(k);
|
|
201
|
+
if (list) list.push(pos);
|
|
202
|
+
else at.set(k, [pos]);
|
|
203
|
+
};
|
|
204
|
+
for (const r of routes) {
|
|
205
|
+
const e = edges.get(r.edge);
|
|
206
|
+
const p = r.points;
|
|
207
|
+
if (!e || p.length < 2) continue;
|
|
208
|
+
add(e.source.node, p[0]!);
|
|
209
|
+
add(e.target.node, p[p.length - 1]!);
|
|
210
|
+
}
|
|
211
|
+
const values: number[] = [];
|
|
212
|
+
for (const [k, list] of at) {
|
|
213
|
+
const node = byId.get(k.slice(0, k.indexOf("\u0000")))!;
|
|
214
|
+
const side = k.slice(k.indexOf("\u0000") + 1);
|
|
215
|
+
const length = side === "north" || side === "south" ? node.box.w : node.box.h;
|
|
216
|
+
list.sort((a, b) => a - b);
|
|
217
|
+
const plugs: number[] = [];
|
|
218
|
+
for (const x of list) if (plugs.length === 0 || x - plugs[plugs.length - 1]! > TOLERANCE) plugs.push(x);
|
|
219
|
+
if (plugs.length < 2 || length <= 0) continue;
|
|
220
|
+
let gap = Infinity;
|
|
221
|
+
for (let i = 1; i < plugs.length; i++) gap = Math.min(gap, plugs[i]! - plugs[i - 1]!);
|
|
222
|
+
values.push(Math.min(1, gap / (length / (plugs.length + 1))));
|
|
223
|
+
}
|
|
224
|
+
if (values.length === 0) return { min: 1, q05: 1, sides: 0 };
|
|
225
|
+
return { min: values.reduce((m, x) => Math.min(m, x), 1), q05: quantile(values, 0.05), sides: values.length };
|
|
226
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { inflate, type Box } from "../../geom.ts";
|
|
2
|
+
import { rng } from "../../fixtures.ts";
|
|
3
|
+
import type { Edge, Graph, Node, Result } from "../../model.ts";
|
|
4
|
+
import { measureStability } from "../quality.ts";
|
|
5
|
+
import { boardUnit } from "./routes.ts";
|
|
6
|
+
|
|
7
|
+
/** A small change of a graph, for the stability measures Z10 and G9 of MEAS0. */
|
|
8
|
+
export interface GraphChange {
|
|
9
|
+
/** The graph after the change. It keeps the order of the nodes. */
|
|
10
|
+
readonly graph: Graph;
|
|
11
|
+
/** The node that moved by one unit, if a free place was found. */
|
|
12
|
+
readonly moved?: string;
|
|
13
|
+
/** The new edge, if the graph has two nodes without an edge between them. */
|
|
14
|
+
readonly added?: string;
|
|
15
|
+
/** The edge that the change removed. */
|
|
16
|
+
readonly removed?: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* This function makes a seeded small change of a graph: one node moves by `boardUnit`, one edge goes and one new edge comes.
|
|
21
|
+
* The moved box keeps a gap of 4 units to the other boxes. The same graph and the same seed give the same change.
|
|
22
|
+
*/
|
|
23
|
+
export function changeGraph(graph: Graph, seed: number): GraphChange {
|
|
24
|
+
const R = rng(seed * 2654435761 + 97);
|
|
25
|
+
const n = graph.nodes.length;
|
|
26
|
+
const u = boardUnit(graph);
|
|
27
|
+
let nodes: readonly Node[] = graph.nodes;
|
|
28
|
+
let moved: string | undefined;
|
|
29
|
+
if (n > 0) {
|
|
30
|
+
const i = Math.floor(R() * n);
|
|
31
|
+
const v = graph.nodes[i]!;
|
|
32
|
+
const free = (b: Box): boolean => {
|
|
33
|
+
const g = inflate(b, 4);
|
|
34
|
+
return graph.nodes.every((o, j) => j === i || o.box.x >= g.x + g.w || g.x >= o.box.x + o.box.w || o.box.y >= g.y + g.h || g.y >= o.box.y + o.box.h);
|
|
35
|
+
};
|
|
36
|
+
const start = Math.floor(R() * 4);
|
|
37
|
+
for (let k = 0; k < 4 && moved === undefined; k++) {
|
|
38
|
+
const [dx, dy] = [
|
|
39
|
+
[u, 0],
|
|
40
|
+
[0, u],
|
|
41
|
+
[-u, 0],
|
|
42
|
+
[0, -u],
|
|
43
|
+
][(start + k) % 4]!;
|
|
44
|
+
const box = { ...v.box, x: v.box.x + dx!, y: v.box.y + dy! };
|
|
45
|
+
if (free(box)) {
|
|
46
|
+
nodes = graph.nodes.map((o, j) => (j === i ? { ...o, box } : o));
|
|
47
|
+
moved = v.id;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
let edges: Edge[] = [...graph.edges];
|
|
52
|
+
let removed: string | undefined;
|
|
53
|
+
let added: string | undefined;
|
|
54
|
+
if (edges.length > 1) {
|
|
55
|
+
const j = Math.floor(R() * edges.length);
|
|
56
|
+
const gone = edges[j]!;
|
|
57
|
+
removed = gone.id;
|
|
58
|
+
edges.splice(j, 1);
|
|
59
|
+
const linked = new Set(graph.edges.flatMap((e) => [`${e.source.node}\u0000${e.target.node}`, `${e.target.node}\u0000${e.source.node}`]));
|
|
60
|
+
for (let t = 0; t < 64 && added === undefined && n > 1; t++) {
|
|
61
|
+
const a = graph.nodes[Math.floor(R() * n)]!.id;
|
|
62
|
+
const b = graph.nodes[Math.floor(R() * n)]!.id;
|
|
63
|
+
if (a === b || linked.has(`${a}\u0000${b}`)) continue;
|
|
64
|
+
added = `${gone.id}~`;
|
|
65
|
+
edges = [...edges, { id: added, source: { node: a }, target: { node: b }, ...(gone.data ? { data: gone.data } : {}) }];
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return { graph: { nodes, edges }, ...(moved ? { moved } : {}), ...(added ? { added } : {}), ...(removed ? { removed } : {}) };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** The route stability of a router (Z10 of MEAS0). */
|
|
72
|
+
export interface RouteStability {
|
|
73
|
+
/** The mean Fréchet distance between the two routes of an edge, divided by the unit of the board. */
|
|
74
|
+
readonly mean: number;
|
|
75
|
+
/** The largest Fréchet distance, divided by the unit of the board. */
|
|
76
|
+
readonly max: number;
|
|
77
|
+
/** The share of the routes that moved more than 1 unit of length. */
|
|
78
|
+
readonly moved: number;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** The options of `routeStability`. */
|
|
82
|
+
export interface RouteStabilityOptions {
|
|
83
|
+
/** Above this number of routes, a seeded sample of this size serves. The default is 2000. */
|
|
84
|
+
readonly sample?: number;
|
|
85
|
+
readonly seed?: number;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* This function measures the route stability of a router (Z10 of MEAS0) with `measureStability`.
|
|
90
|
+
* It compares the routes of the edges that both results have. Each distance is divided by the unit of the board.
|
|
91
|
+
* The cost is quadratic for each route, so a seeded sample serves above `sample` routes.
|
|
92
|
+
*/
|
|
93
|
+
export function routeStability(graph: Graph, before: Result, after: Result, options: RouteStabilityOptions = {}): RouteStability {
|
|
94
|
+
const max = options.sample ?? 2000;
|
|
95
|
+
const prev = new Set(before.routes.map((r) => r.edge));
|
|
96
|
+
let routes = after.routes.filter((r) => prev.has(r.edge));
|
|
97
|
+
if (routes.length > max) {
|
|
98
|
+
const R = rng(options.seed ?? 1);
|
|
99
|
+
routes = Array.from({ length: max }, () => routes[Math.floor(R() * routes.length)]!);
|
|
100
|
+
}
|
|
101
|
+
const s = measureStability(before, { routes });
|
|
102
|
+
const u = boardUnit(graph);
|
|
103
|
+
return { mean: s.mean / u, max: s.max / u, moved: s.moved };
|
|
104
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { rng } from "../../fixtures.ts";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* This function gives the `p` quantile of the values, with a linear step between two ranks (type 7 of Hyndman and Fan).
|
|
5
|
+
* The result is `NaN` for an empty list.
|
|
6
|
+
*/
|
|
7
|
+
export function quantile(values: readonly number[], p: number): number {
|
|
8
|
+
if (values.length === 0) return Number.NaN;
|
|
9
|
+
const s = [...values].sort((a, b) => a - b);
|
|
10
|
+
const at = Math.max(0, Math.min(1, p)) * (s.length - 1);
|
|
11
|
+
const lo = Math.floor(at);
|
|
12
|
+
const hi = Math.min(s.length - 1, lo + 1);
|
|
13
|
+
return s[lo]! + (s[hi]! - s[lo]!) * (at - lo);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** The spread of a measure over the seeds of a fixture family. */
|
|
17
|
+
export interface Spread {
|
|
18
|
+
/** The number of values. */
|
|
19
|
+
readonly n: number;
|
|
20
|
+
readonly median: number;
|
|
21
|
+
/** The `0.25` quantile. */
|
|
22
|
+
readonly q1: number;
|
|
23
|
+
/** The `0.75` quantile. */
|
|
24
|
+
readonly q3: number;
|
|
25
|
+
/** The interquartile range: `q3 - q1`. */
|
|
26
|
+
readonly iqr: number;
|
|
27
|
+
readonly min: number;
|
|
28
|
+
readonly max: number;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** This function gives the median, the quartiles, the interquartile range, the smallest and the largest value of a list. */
|
|
32
|
+
export function summarize(values: readonly number[]): Spread {
|
|
33
|
+
const v = values.filter((x) => !Number.isNaN(x));
|
|
34
|
+
const q1 = quantile(v, 0.25);
|
|
35
|
+
const q3 = quantile(v, 0.75);
|
|
36
|
+
return {
|
|
37
|
+
n: v.length,
|
|
38
|
+
median: quantile(v, 0.5),
|
|
39
|
+
q1,
|
|
40
|
+
q3,
|
|
41
|
+
iqr: q3 - q1,
|
|
42
|
+
min: v.length ? v.reduce((m, x) => Math.min(m, x), Infinity) : Number.NaN,
|
|
43
|
+
max: v.length ? v.reduce((m, x) => Math.max(m, x), -Infinity) : Number.NaN,
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** The largest number of values for which `kendallTau` examines each pair. Above it, the function examines a seeded sample of pairs. */
|
|
48
|
+
export const KENDALL_EXACT = 1500;
|
|
49
|
+
const KENDALL_SAMPLE = 200_000;
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* This function gives the Kendall tau-b of two lists of the same length (Kendall, 1938).
|
|
53
|
+
* The value is from -1 to 1. A value of 1 means the same order. A pair with a tie in both lists does not count.
|
|
54
|
+
* When one list has only ties, the value is 1. Above `KENDALL_EXACT` values, a seeded sample of 200 000 pairs gives an estimate.
|
|
55
|
+
*/
|
|
56
|
+
export function kendallTau(a: readonly number[], b: readonly number[], seed = 1): number {
|
|
57
|
+
const n = Math.min(a.length, b.length);
|
|
58
|
+
if (n < 2) return 1;
|
|
59
|
+
let concordant = 0;
|
|
60
|
+
let discordant = 0;
|
|
61
|
+
let tieA = 0;
|
|
62
|
+
let tieB = 0;
|
|
63
|
+
const visit = (i: number, j: number): void => {
|
|
64
|
+
const da = Math.sign(a[i]! - a[j]!);
|
|
65
|
+
const db = Math.sign(b[i]! - b[j]!);
|
|
66
|
+
if (da === 0 && db === 0) return;
|
|
67
|
+
if (da === 0) tieA++;
|
|
68
|
+
else if (db === 0) tieB++;
|
|
69
|
+
else if (da === db) concordant++;
|
|
70
|
+
else discordant++;
|
|
71
|
+
};
|
|
72
|
+
if (n <= KENDALL_EXACT) {
|
|
73
|
+
for (let i = 0; i < n; i++) for (let j = i + 1; j < n; j++) visit(i, j);
|
|
74
|
+
} else {
|
|
75
|
+
const R = rng(seed);
|
|
76
|
+
for (let k = 0; k < KENDALL_SAMPLE; k++) {
|
|
77
|
+
const i = Math.floor(R() * n);
|
|
78
|
+
const j = Math.floor(R() * n);
|
|
79
|
+
if (i !== j) visit(i, j);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
const den = Math.sqrt((concordant + discordant + tieA) * (concordant + discordant + tieB));
|
|
83
|
+
return den === 0 ? 1 : (concordant - discordant) / den;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const pairs = (k: number): number => (k * (k - 1)) / 2;
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* This function gives the adjusted Rand index of two groupings of the same items (Hubert and Arabie, 1985).
|
|
90
|
+
* The item `i` is in the group `a[i]` of the first grouping and in the group `b[i]` of the second grouping.
|
|
91
|
+
* The value is 1 for the same grouping, and near 0 for a random grouping. Two groupings with one group each give 1.
|
|
92
|
+
*/
|
|
93
|
+
export function adjustedRandIndex(a: readonly (string | number)[], b: readonly (string | number)[]): number {
|
|
94
|
+
const n = Math.min(a.length, b.length);
|
|
95
|
+
if (n < 2) return 1;
|
|
96
|
+
const cells = new Map<string, number>();
|
|
97
|
+
const rows = new Map<string | number, number>();
|
|
98
|
+
const cols = new Map<string | number, number>();
|
|
99
|
+
for (let i = 0; i < n; i++) {
|
|
100
|
+
const key = `${String(a[i])}\u0000${String(b[i])}`;
|
|
101
|
+
cells.set(key, (cells.get(key) ?? 0) + 1);
|
|
102
|
+
rows.set(a[i]!, (rows.get(a[i]!) ?? 0) + 1);
|
|
103
|
+
cols.set(b[i]!, (cols.get(b[i]!) ?? 0) + 1);
|
|
104
|
+
}
|
|
105
|
+
let index = 0;
|
|
106
|
+
for (const c of cells.values()) index += pairs(c);
|
|
107
|
+
let sa = 0;
|
|
108
|
+
let sb = 0;
|
|
109
|
+
for (const c of rows.values()) sa += pairs(c);
|
|
110
|
+
for (const c of cols.values()) sb += pairs(c);
|
|
111
|
+
const expected = (sa * sb) / pairs(n);
|
|
112
|
+
const top = (sa + sb) / 2;
|
|
113
|
+
return top === expected ? 1 : (index - expected) / (top - expected);
|
|
114
|
+
}
|
package/src/bench/properties.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { dist, inflate, pointBoxDistance, segmentHitsBox, type Box, type Point } from "../geom.ts";
|
|
2
2
|
import { Grid } from "../grid.ts";
|
|
3
|
-
import { fixedPortPoint, nodeIndex, type Graph, type Node, type Result } from "../model.ts";
|
|
3
|
+
import { fixedPortPoint, nodeIndex, type Graph, type Level, type Node, type Result } from "../model.ts";
|
|
4
4
|
|
|
5
5
|
/** A broken hard rule. A correct router has none. */
|
|
6
6
|
export interface Violation {
|
|
@@ -89,7 +89,23 @@ export function checkProperties(graph: Graph, result: Result, limit = 100): Viol
|
|
|
89
89
|
if (add({ rule: "run-shared", edge: id, detail: `run ${run.id}: the route does not hold the points of the run in a row` })) return out;
|
|
90
90
|
}
|
|
91
91
|
}
|
|
92
|
-
|
|
92
|
+
out.push(...checkLevels(graph, result.levels ?? [], limit - out.length));
|
|
93
|
+
return out;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* This function checks the hard rules of the zoom levels. The counts of each level add up (`counts-sum`).
|
|
98
|
+
* Each link of a level starts on the box of its source and ends on the box of its target (`link-ends`).
|
|
99
|
+
* The function gives at most `limit` violations.
|
|
100
|
+
*/
|
|
101
|
+
export function checkLevels(graph: Graph, levels: readonly Level[], limit = 100): Violation[] {
|
|
102
|
+
const out: Violation[] = [];
|
|
103
|
+
const add = (v: Violation): boolean => {
|
|
104
|
+
out.push(v);
|
|
105
|
+
return out.length >= limit;
|
|
106
|
+
};
|
|
107
|
+
if (limit <= 0) return out;
|
|
108
|
+
for (const level of levels) {
|
|
93
109
|
const itemOf = new Map<string, string>();
|
|
94
110
|
for (const it of level.items) for (const n of it.nodes) itemOf.set(n, it.id);
|
|
95
111
|
if (itemOf.size !== graph.nodes.length)
|