@mark1russell7/highways 0.1.2 → 0.1.4

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/package.json CHANGED
@@ -9,7 +9,7 @@
9
9
  "directory": "packages/core"
10
10
  },
11
11
  "homepage": "https://mark1russell7.github.io/highways/",
12
- "version": "0.1.2",
12
+ "version": "0.1.4",
13
13
  "type": "module",
14
14
  "main": "./build/index.js",
15
15
  "types": "./build/index.d.ts",
@@ -39,6 +39,7 @@
39
39
  "test": "vitest run",
40
40
  "bench": "node scripts/bench.ts",
41
41
  "profile": "node scripts/profile.ts",
42
+ "levels": "node scripts/levels.ts",
42
43
  "test:watch": "vitest",
43
44
  "typecheck": "tsc -p tsconfig.json && tsc -p tsconfig.test.json"
44
45
  }
@@ -0,0 +1,98 @@
1
+ import { curveOf, CURVE_TYPES, curveSpace, sampleCurve, type CurveType } from "../curves/index.ts";
2
+ import { polylineLength } from "../geom.ts";
3
+ import { nodeIndex, type Graph, type Router } from "../model.ts";
4
+ import { nearestSide, SIDE_NORMAL } from "../plugs.ts";
5
+ import { measureLinks, type MeasuredLink } from "./links.ts";
6
+
7
+ // The core runs in any host, so it reads the clock without the types of Node or the browser.
8
+ const now = (): number => (globalThis as { performance?: { now(): number } }).performance?.now() ?? Date.now();
9
+
10
+ /** One row of the benchmark of the curves: one curve type on the routes of one router on one fixture. */
11
+ export interface CurveBenchRow {
12
+ readonly router: string;
13
+ readonly fixture: string;
14
+ readonly type: CurveType;
15
+ /** The number of routes. */
16
+ readonly links: number;
17
+ /** The number of polylines that go through a box that is not one of their ends (`measureLinks`). */
18
+ readonly through: number;
19
+ /** The number of curves without a free space that go through such a box. */
20
+ readonly free: number;
21
+ /** The number of curves with the free space of the nodes (`curveSpace`) that go through such a box. */
22
+ readonly clamped: number;
23
+ /** The ratio of the length of all curves with the free space to the length of all polylines. */
24
+ readonly length: number;
25
+ /** The time to make all curves with the free space, in milliseconds. */
26
+ readonly ms: number;
27
+ }
28
+
29
+ /** The options of `benchCurves`. */
30
+ export interface CurveBenchOptions {
31
+ /** The radius of the types `rounded` and `arc`. The default is 12. */
32
+ readonly radius?: number;
33
+ /** The tolerance of the samples of the curves. The default is 0.25. */
34
+ readonly tolerance?: number;
35
+ }
36
+
37
+ /**
38
+ * This function routes a graph with a router and makes each curve type of each route.
39
+ * It counts the curves that go through a box, with and without the free space, and it compares their length to the polylines.
40
+ * The type `spline` leaves each plug along the normal of the nearest side of its node.
41
+ */
42
+ export function benchCurves(router: Router, fixture: string, graph: Graph, options: CurveBenchOptions = {}): CurveBenchRow[] {
43
+ const tolerance = options.tolerance ?? 0.25;
44
+ const result = router.route(graph);
45
+ const byId = nodeIndex(graph);
46
+ const boxes = new Map(graph.nodes.map((n) => [n.id, n.box]));
47
+ const edges = new Map(graph.edges.map((e) => [e.id, e]));
48
+ const space = curveSpace(
49
+ graph.nodes.map((n) => n.box),
50
+ tolerance,
51
+ );
52
+ const routes = result.routes.flatMap((r) => {
53
+ const e = edges.get(r.edge);
54
+ const s = e && byId.get(e.source.node);
55
+ const t = e && byId.get(e.target.node);
56
+ if (!e || !s || !t || r.points.length < 2) return [];
57
+ const first = r.points[0]!;
58
+ const last = r.points[r.points.length - 1]!;
59
+ return [{ source: s.id, target: t.id, points: r.points, startNormal: SIDE_NORMAL[nearestSide(s.box, first)], endNormal: SIDE_NORMAL[nearestSide(t.box, last)] }];
60
+ });
61
+ const base = measureLinks(boxes, routes);
62
+ const ink = routes.reduce((sum, r) => sum + polylineLength(r.points), 0);
63
+ return CURVE_TYPES.map((type): CurveBenchRow => {
64
+ const make = (withSpace: boolean): MeasuredLink[] =>
65
+ routes.map((r) => ({
66
+ source: r.source,
67
+ target: r.target,
68
+ points: sampleCurve(
69
+ curveOf(r.points, { type, radius: options.radius ?? 12, startNormal: r.startNormal, endNormal: r.endNormal, ...(withSpace ? { space } : {}) }),
70
+ tolerance,
71
+ ),
72
+ }));
73
+ const free = measureLinks(boxes, make(false));
74
+ const t0 = now();
75
+ const clamped = make(true);
76
+ const ms = now() - t0;
77
+ const length = clamped.reduce((sum, l) => sum + polylineLength(l.points), 0);
78
+ return {
79
+ router: router.name,
80
+ fixture,
81
+ type,
82
+ links: routes.length,
83
+ through: base.through,
84
+ free: free.through,
85
+ clamped: measureLinks(boxes, clamped).through,
86
+ length: ink > 0 ? length / ink : 1,
87
+ ms,
88
+ };
89
+ });
90
+ }
91
+
92
+ /** This function writes the rows of `benchCurves` as a Markdown table. */
93
+ export function formatCurves(rows: readonly CurveBenchRow[]): string {
94
+ const head = "| router | fixture | curve | routes | through, polyline | through, free | through, clamped | length | ms |";
95
+ const line = "|---|---|---|---:|---:|---:|---:|---:|---:|";
96
+ const body = rows.map((r) => `| ${r.router} | ${r.fixture} | ${r.type} | ${r.links} | ${r.through} | ${r.free} | ${r.clamped} | ${r.length.toFixed(3)} | ${r.ms.toFixed(1)} |`);
97
+ return [head, line, ...body].join("\n");
98
+ }
@@ -0,0 +1,191 @@
1
+ import { dist, inflate, pointSegmentDistance, polylineLength, segmentHitsBox, type Box, type Point } from "../geom.ts";
2
+ import { Grid } from "../grid.ts";
3
+ import type { Graph, Level, Result } from "../model.ts";
4
+ import { TOLERANCE } from "./properties.ts";
5
+
6
+ /**
7
+ * The faults of a set of links: the routes of the edges, or the links of one zoom level.
8
+ * Each value counts links, except `worstDetour`. Lower is better for each value.
9
+ */
10
+ export interface LinkFaults {
11
+ /** The number of links that the measure examined. */
12
+ readonly links: number;
13
+ /**
14
+ * The number of links that go through the inside of a box that is not one of their two ends.
15
+ * A segment goes through a box when a part of it longer than `TOLERANCE` is inside the box made smaller by `TOLERANCE`.
16
+ */
17
+ readonly through: number;
18
+ /**
19
+ * The number of links that go out and back. The distance from a point of the link to its last point grows after the nearest approach.
20
+ * A link goes out and back when this growth is more than `BACKTRACK_SHARE` of the distance between its ends, and more than `BACKTRACK_MIN` units.
21
+ */
22
+ readonly backtrack: number;
23
+ /** The largest ratio of the length of a link to the distance between its ends. Links with ends nearer than 1 unit do not count. */
24
+ readonly worstDetour: number;
25
+ /**
26
+ * The number of links with a point outside the bounding box of all the boxes, made larger by `slack`.
27
+ * Both ends of a link are always in that bounding box, so such a link goes around the outer edge of the board.
28
+ */
29
+ readonly outside: number;
30
+ }
31
+
32
+ /** The faults of the links of one zoom level. */
33
+ export interface LevelFaults extends LinkFaults {
34
+ readonly depth: number;
35
+ /** The number of items of the level. */
36
+ readonly items: number;
37
+ /** The number of items that hold more than one node. */
38
+ readonly groups: number;
39
+ /** The number of pairs of items whose boxes overlap by more than `TOLERANCE` on each axis. */
40
+ readonly overlaps: number;
41
+ }
42
+
43
+ /** The options of the link measures. */
44
+ export interface LinkFaultOptions {
45
+ /** A link that goes farther than this distance outside the bounding box of all the boxes counts in `outside`. The default is 24. */
46
+ readonly slack?: number;
47
+ }
48
+
49
+ /** A link goes out and back when its distance to its target grows by more than this share of the distance between its ends. */
50
+ export const BACKTRACK_SHARE = 0.25;
51
+ /** A link goes out and back only when its distance to its target grows by more than this number of units. */
52
+ export const BACKTRACK_MIN = 40;
53
+ /** The default `slack` of `LinkFaultOptions`. */
54
+ export const OUTSIDE_SLACK = 24;
55
+
56
+ /** A link from one box to another box, as `measureLinks` sees it. */
57
+ export interface MeasuredLink {
58
+ readonly source: string;
59
+ readonly target: string;
60
+ readonly points: readonly Point[];
61
+ }
62
+
63
+ /**
64
+ * This function gives the largest growth of the distance to the last point of a polyline.
65
+ * The growth at a vertex is its distance to the last point, less the nearest approach of the polyline before that vertex.
66
+ */
67
+ export function backtrackOf(p: readonly Point[]): number {
68
+ if (p.length < 3) return 0;
69
+ const end = p[p.length - 1]!;
70
+ let near = dist(p[0]!, end);
71
+ let worst = 0;
72
+ for (let i = 1; i < p.length; i++) {
73
+ worst = Math.max(worst, dist(p[i]!, end) - near);
74
+ near = Math.min(near, pointSegmentDistance(end, p[i - 1]!, p[i]!));
75
+ }
76
+ return worst;
77
+ }
78
+
79
+ /** This function tells if the polyline `p` goes out and back: `backtrackOf(p)` is more than `BACKTRACK_MIN` and more than `BACKTRACK_SHARE` of the span. */
80
+ export function goesOutAndBack(p: readonly Point[]): boolean {
81
+ if (p.length < 3) return false;
82
+ const span = dist(p[0]!, p[p.length - 1]!);
83
+ return backtrackOf(p) > Math.max(BACKTRACK_MIN, BACKTRACK_SHARE * span);
84
+ }
85
+
86
+ /**
87
+ * This function measures the faults of links between boxes. The map `boxes` gives the box of each end.
88
+ * The bounding box of `outside` is the bounding box of all the boxes of the map.
89
+ */
90
+ export function measureLinks(boxes: ReadonlyMap<string, Box>, links: readonly MeasuredLink[], options: LinkFaultOptions = {}): LinkFaults {
91
+ const slack = options.slack ?? OUTSIDE_SLACK;
92
+ let size = 0;
93
+ let x0 = Infinity;
94
+ let y0 = Infinity;
95
+ let x1 = -Infinity;
96
+ let y1 = -Infinity;
97
+ for (const b of boxes.values()) {
98
+ size += Math.max(b.w, b.h);
99
+ x0 = Math.min(x0, b.x);
100
+ y0 = Math.min(y0, b.y);
101
+ x1 = Math.max(x1, b.x + b.w);
102
+ y1 = Math.max(y1, b.y + b.h);
103
+ }
104
+ // Large items cover many buckets of a small grid, so the buckets grow with the items.
105
+ const grid = new Grid<string>(Math.max(180, (2 * size) / Math.max(1, boxes.size)));
106
+ for (const [id, b] of boxes) grid.add(id, b);
107
+ let through = 0;
108
+ let backtrack = 0;
109
+ let worstDetour = 0;
110
+ let outside = 0;
111
+ for (const l of links) {
112
+ const p = l.points;
113
+ if (p.length === 0) continue;
114
+ let hit = false;
115
+ for (let i = 1; i < p.length && !hit; i++) {
116
+ const a = p[i - 1]!;
117
+ const b = p[i]!;
118
+ grid.querySegment(a, b, 0, (id) => {
119
+ if (!hit && id !== l.source && id !== l.target && segmentHitsBox(a, b, inflate(boxes.get(id)!, -TOLERANCE), TOLERANCE)) hit = true;
120
+ });
121
+ }
122
+ if (hit) through++;
123
+ if (goesOutAndBack(p)) backtrack++;
124
+ const span = dist(p[0]!, p[p.length - 1]!);
125
+ if (span > 1) worstDetour = Math.max(worstDetour, polylineLength(p) / span);
126
+ if (p.some((q) => q.x < x0 - slack || q.x > x1 + slack || q.y < y0 - slack || q.y > y1 + slack)) outside++;
127
+ }
128
+ return { links: links.length, through, backtrack, worstDetour, outside };
129
+ }
130
+
131
+ /** This function measures the faults of the routes of a result. The ends of a route are the nodes of its edge. */
132
+ export function measureRouteFaults(graph: Graph, result: Result, options: LinkFaultOptions = {}): LinkFaults {
133
+ const boxes = new Map(graph.nodes.map((n) => [n.id, n.box]));
134
+ const edges = new Map(graph.edges.map((e) => [e.id, e]));
135
+ const links: MeasuredLink[] = [];
136
+ for (const r of result.routes) {
137
+ const e = edges.get(r.edge);
138
+ if (e) links.push({ source: e.source.node, target: e.target.node, points: r.points });
139
+ }
140
+ return measureLinks(boxes, links, options);
141
+ }
142
+
143
+ /** This function counts the pairs of boxes that overlap by more than `TOLERANCE` on each axis. */
144
+ export function countOverlaps(boxes: readonly Box[]): number {
145
+ let size = 0;
146
+ for (const b of boxes) size += Math.max(b.w, b.h);
147
+ const grid = new Grid<number>(Math.max(180, (2 * size) / Math.max(1, boxes.length)));
148
+ boxes.forEach((b, i) => grid.add(i, b));
149
+ let n = 0;
150
+ boxes.forEach((a, i) => {
151
+ grid.query(a, (j) => {
152
+ const b = boxes[j]!;
153
+ if (j > i && Math.min(a.x + a.w, b.x + b.w) - Math.max(a.x, b.x) > TOLERANCE && Math.min(a.y + a.h, b.y + b.h) - Math.max(a.y, b.y) > TOLERANCE) n++;
154
+ });
155
+ });
156
+ return n;
157
+ }
158
+
159
+ /** This function measures the faults of the links of one zoom level. The items of the level are the boxes. */
160
+ export function measureLevel(level: Level, options: LinkFaultOptions = {}): LevelFaults {
161
+ const boxes = new Map(level.items.map((i) => [i.id, i.box]));
162
+ return {
163
+ depth: level.depth,
164
+ items: level.items.length,
165
+ groups: level.items.filter((i) => i.nodes.length > 1).length,
166
+ overlaps: countOverlaps(level.items.map((i) => i.box)),
167
+ ...measureLinks(boxes, level.links, options),
168
+ };
169
+ }
170
+
171
+ /**
172
+ * This function measures the faults of each zoom level of a result that has groups: the levels of the far view.
173
+ * A level of single nodes only is the near view. Its links are routes, and `measureRouteFaults` measures them.
174
+ */
175
+ export function measureLevelFaults(result: Result, options: LinkFaultOptions = {}): LevelFaults[] {
176
+ return (result.levels ?? []).filter((l) => l.items.some((i) => i.nodes.length > 1)).map((l) => measureLevel(l, options));
177
+ }
178
+
179
+ /** This function adds the faults of several levels: it sums the counts and keeps the largest `worstDetour`. */
180
+ export function sumFaults(list: readonly LinkFaults[]): LinkFaults {
181
+ return list.reduce<LinkFaults>(
182
+ (s, f) => ({
183
+ links: s.links + f.links,
184
+ through: s.through + f.through,
185
+ backtrack: s.backtrack + f.backtrack,
186
+ worstDetour: Math.max(s.worstDetour, f.worstDetour),
187
+ outside: s.outside + f.outside,
188
+ }),
189
+ { links: 0, through: 0, backtrack: 0, worstDetour: 0, outside: 0 },
190
+ );
191
+ }
@@ -0,0 +1,122 @@
1
+ import { levelLinks } from "../levels.ts";
2
+ import type { Graph, Node, Result } from "../model.ts";
3
+ import { makeRoom, roomLinks, type RoomOptions } from "../room/index.ts";
4
+ import { sectionsRouter } from "../routers/sections.ts";
5
+ import { laneRoutes, runKey, segmentsOf } from "../segments.ts";
6
+ import { measureLinks, measureRouteFaults, type LinkFaults } from "./links.ts";
7
+ import { measureQuality } from "./quality.ts";
8
+
9
+ // The core runs in any host, so it reads the clock without the types of Node or the browser.
10
+ const now = (): number => (globalThis as { performance?: { now(): number } }).performance?.now() ?? Date.now();
11
+
12
+ /** One row of the benchmark of the room: one state of a board, packed or open, and the measures of its links. */
13
+ export interface RoomBenchRow {
14
+ readonly fixture: string;
15
+ /** The state of the board: `packed`, `gutter` (each gap open to the gutter only), or a method of `roomDemand`. */
16
+ readonly board: string;
17
+ /** The area of the bounding box of the boxes, as a share of the area of the packed board. */
18
+ readonly area: number;
19
+ /** The faults of the links of the far view (`levelLinks` with each box as one item). */
20
+ readonly faults: LinkFaults;
21
+ /** The crossings of the links of the far view (`measureQuality`). */
22
+ readonly crossings: number;
23
+ /** The ink of the links of the far view. */
24
+ readonly ink: number;
25
+ /** The mean detour of the links of the far view. */
26
+ readonly detour: number;
27
+ /**
28
+ * The number of lane routes of the router `sections` that go through a box. Each shared segment opens to one lane for each edge,
29
+ * at the spacing of the options (`laneRoutes`).
30
+ */
31
+ readonly laneThrough: number;
32
+ /** The time of `makeRoom`, in milliseconds. It is 0 for the packed board. */
33
+ readonly ms: number;
34
+ }
35
+
36
+ /** This function gives the area of the bounding box of the nodes. */
37
+ function areaOf(nodes: readonly Node[]): number {
38
+ let x0 = Infinity;
39
+ let y0 = Infinity;
40
+ let x1 = -Infinity;
41
+ let y1 = -Infinity;
42
+ for (const n of nodes) {
43
+ x0 = Math.min(x0, n.box.x);
44
+ y0 = Math.min(y0, n.box.y);
45
+ x1 = Math.max(x1, n.box.x + n.box.w);
46
+ y1 = Math.max(y1, n.box.y + n.box.h);
47
+ }
48
+ return nodes.length ? (x1 - x0) * (y1 - y0) : 0;
49
+ }
50
+
51
+ /**
52
+ * This function measures the links of one board. It measures the links of the far view, with each node of the graph as one item of a level.
53
+ * It also measures the lane routes of the router `sections` at the lane width `spacing`.
54
+ */
55
+ export function measureBoard(graph: Graph, spacing: number): Omit<RoomBenchRow, "fixture" | "board" | "area" | "ms"> {
56
+ const links = roomLinks(graph);
57
+ const items = graph.nodes.map((n) => ({ id: n.id, box: n.box, nodes: [n.id] }));
58
+ const drawn = levelLinks(
59
+ items,
60
+ links.map((l) => ({ source: l.source, target: l.target, count: l.count ?? 1 })),
61
+ );
62
+ const boxes = new Map(graph.nodes.map((n) => [n.id, n.box]));
63
+ // `measureQuality` reads routes of edges, so each link of the far view stands for the first edge of its pair.
64
+ const edgeOf = new Map<string, string>();
65
+ for (const e of graph.edges) {
66
+ const key = `${e.source.node}\u0000${e.target.node}`;
67
+ if (!edgeOf.has(key)) edgeOf.set(key, e.id);
68
+ }
69
+ const asRoutes: Result = { routes: drawn.map((l) => ({ edge: edgeOf.get(`${l.source}\u0000${l.target}`)!, points: l.points })) };
70
+ const q = measureQuality(graph, asRoutes);
71
+ const routed = sectionsRouter({ name: "sections" }).route(graph);
72
+ const segments = segmentsOf(routed);
73
+ const open = new Set(segments.segments.filter((s) => s.count > 1).map((s) => runKey(s.edges)));
74
+ const lanes = laneRoutes(segments, open, spacing);
75
+ return {
76
+ faults: measureLinks(boxes, drawn),
77
+ crossings: q.crossings,
78
+ ink: q.ink,
79
+ detour: q.detour,
80
+ laneThrough: measureRouteFaults(graph, { routes: lanes }).through,
81
+ };
82
+ }
83
+
84
+ /**
85
+ * This function measures a packed board, the board with each gap open to the gutter only, and the board that `makeRoom` opens with each method.
86
+ * The options of `makeRoom` are the same for each method, except `method`.
87
+ */
88
+ export function benchRoom(fixture: string, graph: Graph, options: RoomOptions = {}): RoomBenchRow[] {
89
+ const spacing = options.spacing ?? 4;
90
+ const area0 = areaOf(graph.nodes);
91
+ const links = roomLinks(graph);
92
+ const row = (board: string, nodes: readonly Node[], ms: number): RoomBenchRow => ({
93
+ fixture,
94
+ board,
95
+ area: area0 > 0 ? areaOf(nodes) / area0 : 1,
96
+ ms,
97
+ ...measureBoard({ nodes, edges: graph.edges }, spacing),
98
+ });
99
+ const rows = [row("packed", graph.nodes, 0)];
100
+ const states: [string, RoomOptions][] = [
101
+ ["gutter", { ...options, spacing: 0, method: "routes" }],
102
+ ["routes", { ...options, method: "routes" }],
103
+ ["lines", { ...options, method: "lines" }],
104
+ ];
105
+ for (const [board, o] of states) {
106
+ const t0 = now();
107
+ const room = makeRoom(graph.nodes, links, o);
108
+ rows.push(row(board, room.items, now() - t0));
109
+ }
110
+ return rows;
111
+ }
112
+
113
+ /** This function writes the rows of `benchRoom` as a Markdown table. */
114
+ export function formatRoom(rows: readonly RoomBenchRow[]): string {
115
+ const head = "| fixture | board | area | links | through | outside | out and back | worst detour | crossings | ink | detour | lane through | ms |";
116
+ const line = "|---|---|---:|---:|---:|---:|---:|---:|---:|---:|---:|---:|---:|";
117
+ const body = rows.map((r) => {
118
+ const f = r.faults;
119
+ return `| ${r.fixture} | ${r.board} | ${r.area.toFixed(3)} | ${f.links} | ${f.through} | ${f.outside} | ${f.backtrack} | ${f.worstDetour.toFixed(2)} | ${r.crossings} | ${Math.round(r.ink)} | ${r.detour.toFixed(3)} | ${r.laneThrough} | ${r.ms.toFixed(1)} |`;
120
+ });
121
+ return [head, line, ...body].join("\n");
122
+ }
package/src/bench/run.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import { makeFixture, type FixtureSpec } from "../fixtures.ts";
2
2
  import type { Graph, Result, Router } from "../model.ts";
3
+ import { measureLevelFaults, measureRouteFaults, sumFaults, type LevelFaults, type LinkFaults } from "./links.ts";
3
4
  import { checkProperties, type Violation } from "./properties.ts";
4
5
  import { measureQuality, type Quality } from "./quality.ts";
5
6
 
@@ -20,6 +21,10 @@ export interface BenchRow {
20
21
  /** The first violations, for the report. */
21
22
  readonly sample: readonly Violation[];
22
23
  readonly quality: Quality;
24
+ /** The faults of the routes of the edges (`measureRouteFaults`). */
25
+ readonly faults: LinkFaults;
26
+ /** The faults of each zoom level with groups (`measureLevelFaults`). The list is empty for a router without levels. */
27
+ readonly levelFaults: readonly LevelFaults[];
23
28
  }
24
29
 
25
30
  /** The options of a benchmark run. */
@@ -87,6 +92,8 @@ export function benchOne(router: Router, fixture: string, graph: Graph, repeat =
87
92
  violations: all.length,
88
93
  sample: all.slice(0, 5),
89
94
  quality: measureQuality(graph, result),
95
+ faults: measureRouteFaults(graph, result),
96
+ levelFaults: measureLevelFaults(result),
90
97
  };
91
98
  }
92
99
 
@@ -101,3 +108,24 @@ export function formatTable(rows: readonly BenchRow[]): string {
101
108
  });
102
109
  return [head, line, ...body].join("\n");
103
110
  }
111
+
112
+ /**
113
+ * This function writes the faults of the rows as a Markdown table: the routes first, then the sum of the levels with groups.
114
+ * A router without such levels gets a dash in the columns of the levels.
115
+ */
116
+ export function formatFaults(rows: readonly BenchRow[]): string {
117
+ const head =
118
+ "| router | fixture | through | out and back | worst detour | outside | level links | level overlaps | level through | level out and back | level worst detour | level outside |";
119
+ const line = "|---|---|---:|---:|---:|---:|---:|---:|---:|---:|---:|---:|";
120
+ const body = rows.map((r) => {
121
+ const f = r.faults;
122
+ const cells = [r.router, r.fixture, f.through, f.backtrack, f.worstDetour.toFixed(2), f.outside];
123
+ if (r.levelFaults.length === 0) cells.push("-", "-", "-", "-", "-", "-");
124
+ else {
125
+ const l = sumFaults(r.levelFaults);
126
+ cells.push(l.links, r.levelFaults.reduce((s, x) => s + x.overlaps, 0), l.through, l.backtrack, l.worstDetour.toFixed(2), l.outside);
127
+ }
128
+ return `| ${cells.join(" | ")} |`;
129
+ });
130
+ return [head, line, ...body].join("\n");
131
+ }
package/src/cases.ts ADDED
@@ -0,0 +1,139 @@
1
+ import { makeFixture, rng } from "./fixtures.ts";
2
+ import type { Edge, Graph, Node } from "./model.ts";
3
+
4
+ /**
5
+ * A board of made-up data that shows one known defect of the routes or of the zoom levels.
6
+ * The benchmark races the routers on each case, and the measures of `measureLinks` give a number for the defect.
7
+ */
8
+ export interface CaseBoard {
9
+ readonly name: string;
10
+ /** The defect that the board shows, in a few words. */
11
+ readonly defect: string;
12
+ /** This function makes the graph. The same function always gives the same graph. */
13
+ readonly make: () => Graph;
14
+ }
15
+
16
+ /** A board while it grows: the functions add nodes and edges in order. */
17
+ class Board {
18
+ readonly nodes: Node[] = [];
19
+ readonly edges: Edge[] = [];
20
+ readonly #pairs = new Set<string>();
21
+
22
+ node(id: string, x: number, y: number, w: number, h: number): string {
23
+ this.nodes.push({ id, box: { x, y, w, h } });
24
+ return id;
25
+ }
26
+
27
+ /** This method adds an edge. It adds nothing for a loop or for a second edge between the same two nodes. */
28
+ link(source: string, target: string): void {
29
+ const key = source < target ? `${source}|${target}` : `${target}|${source}`;
30
+ if (source === target || this.#pairs.has(key)) return;
31
+ this.#pairs.add(key);
32
+ this.edges.push({ id: `e${this.edges.length}`, source: { node: source }, target: { node: target } });
33
+ }
34
+
35
+ graph(): Graph {
36
+ return { nodes: this.nodes, edges: this.edges };
37
+ }
38
+ }
39
+
40
+ /**
41
+ * This function makes a dense board: 150 nodes in tight clusters. At the far view, the boxes of the groups overlap.
42
+ * Then a level link starts or ends in a group that another group covers.
43
+ */
44
+ export function denseGroupsBoard(): Graph {
45
+ return makeFixture({ name: "dense-groups", kind: "clustered", nodes: 150, edgesPerNode: 2, seed: 3 });
46
+ }
47
+
48
+ /**
49
+ * This function makes a board of three rows of 32 cards. Each card links to the next card of its row.
50
+ * Forty long links join cards far apart. The hub of a section is near the middle of its cards.
51
+ * So a long link climbs to hubs behind its source or past its target, and comes back.
52
+ */
53
+ export function hubChainBoard(): Graph {
54
+ const R = rng(3);
55
+ const b = new Board();
56
+ const rows = 3;
57
+ const cols = 32;
58
+ const gap = 48;
59
+ const id = (r: number, c: number): string => `c${r}-${c}`;
60
+ for (let r = 0; r < rows; r++) for (let c = 0; c < cols; c++) b.node(id(r, c), c * (80 + gap), r * (36 + gap), 80, 36);
61
+ for (let r = 0; r < rows; r++) for (let c = 0; c + 1 < cols; c++) b.link(id(r, c), id(r, c + 1));
62
+ for (let n = 0; n < 40; n++) {
63
+ const r = Math.floor(R() * rows);
64
+ const c = Math.floor(R() * cols);
65
+ const span = Math.floor(cols / 8 + (R() * cols) / 2);
66
+ if (c + span < cols) b.link(id(r, c), id((r + 1) % rows, c + span));
67
+ }
68
+ return b.graph();
69
+ }
70
+
71
+ /**
72
+ * This function makes a board of 32 groups in 8 columns and 4 rows. Each group has 4 cards in a square.
73
+ * The cards of a group link in a chain, and 96 random links join cards of two groups.
74
+ * On a level with many groups, some level links go around the outer edge of the board.
75
+ */
76
+ export function manyGroupsBoard(): Graph {
77
+ const R = rng(7);
78
+ const b = new Board();
79
+ const cols = 8;
80
+ const rows = 4;
81
+ const per = 4;
82
+ const w = 60;
83
+ const h = 30;
84
+ const cardGap = 12;
85
+ const groupGap = 60;
86
+ const side = Math.ceil(Math.sqrt(per));
87
+ const gw = side * w + (side - 1) * cardGap;
88
+ const gh = Math.ceil(per / side) * h + (Math.ceil(per / side) - 1) * cardGap;
89
+ const groups: string[][] = [];
90
+ for (let r = 0; r < rows; r++)
91
+ for (let c = 0; c < cols; c++) {
92
+ const list: string[] = [];
93
+ for (let i = 0; i < per; i++) {
94
+ const x = c * (gw + groupGap) + (i % side) * (w + cardGap);
95
+ const y = r * (gh + groupGap) + Math.floor(i / side) * (h + cardGap);
96
+ list.push(b.node(`g${r}-${c}-${i}`, x, y, w, h));
97
+ }
98
+ groups.push(list);
99
+ }
100
+ for (const list of groups) for (let i = 1; i < list.length; i++) b.link(list[i - 1]!, list[i]!);
101
+ const pick = (list: readonly string[]): string => list[Math.floor(R() * list.length)]!;
102
+ for (let n = 0; n < groups.length * 3; n++) b.link(pick(groups[Math.floor(R() * groups.length)]!), pick(groups[Math.floor(R() * groups.length)]!));
103
+ return b.graph();
104
+ }
105
+
106
+ /**
107
+ * This function makes the far view of a board after a host layout packs it.
108
+ * The board has 96 boxes of one size in a grid of 12 columns and 8 rows.
109
+ * The boxes touch, with no gap between them. Each box links to a box near it, and 48 long links join boxes far apart.
110
+ * The links have no channel between the boxes. The roadmap item ROOM measures this board before and after `makeRoom`.
111
+ *
112
+ * The board is not in `CASES`, because the digests of the router fix the output on each board of `CASES`.
113
+ */
114
+ export function packedGridBoard(): Graph {
115
+ const R = rng(11);
116
+ const b = new Board();
117
+ const cols = 12;
118
+ const rows = 8;
119
+ const w = 140;
120
+ const h = 90;
121
+ const id = (r: number, c: number): string => `p${r}-${c}`;
122
+ for (let r = 0; r < rows; r++) for (let c = 0; c < cols; c++) b.node(id(r, c), c * w, r * h, w, h);
123
+ const step = (): number => Math.floor(R() * 5) - 2;
124
+ for (let r = 0; r < rows; r++)
125
+ for (let c = 0; c < cols; c++) {
126
+ const rr = Math.max(0, Math.min(rows - 1, r + step()));
127
+ const cc = Math.max(0, Math.min(cols - 1, c + step()));
128
+ b.link(id(r, c), id(rr, cc));
129
+ }
130
+ for (let n = 0; n < 48; n++) b.link(id(Math.floor(R() * rows), Math.floor(R() * cols)), id(Math.floor(R() * rows), Math.floor(R() * cols)));
131
+ return b.graph();
132
+ }
133
+
134
+ /** The case boards of the benchmark. Each board has a few hundred nodes at most, so the benchmark races them on each run. */
135
+ export const CASES: readonly CaseBoard[] = [
136
+ { name: "case-dense-groups", defect: "Groups overlap at the far view, and level links go through other groups.", make: denseGroupsBoard },
137
+ { name: "case-hub-chain", defect: "A route climbs a chain of hubs away from its target and comes back.", make: hubChainBoard },
138
+ { name: "case-many-groups", defect: "On a level with many groups, links go around the outer edge of the board.", make: manyGroupsBoard },
139
+ ];