@mark1russell7/highways 0.1.5 → 0.3.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 +293 -8
- package/build/index.d.ts +1774 -236
- package/build/index.d.ts.map +1 -1
- package/build/index.js +9307 -3090
- package/build/index.js.map +1 -1
- package/package.json +2 -1
- package/src/bench/compare.ts +145 -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/sections.ts +121 -0
- package/src/bench/suite.ts +781 -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 +4 -0
- package/src/groups/kd/index.ts +207 -0
- package/src/groups/keys/index.ts +81 -0
- package/src/groups/quadtree/index.ts +10 -0
- package/src/index.ts +7 -0
- package/src/levels.ts +178 -9
- package/src/model.ts +2 -0
- package/src/pick.ts +36 -0
- 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 +78 -55
- package/src/routers/shared.ts +7 -2
- package/src/routers/trunks.ts +131 -6
- package/src/sections.ts +93 -4
- package/src/segments.ts +81 -31
- package/src/strategies/builtins.ts +153 -7
- package/src/strategies/index.ts +1 -0
- package/src/strategies/model.ts +29 -7
- package/src/strategies/options.ts +296 -0
- package/src/strategies/schemas.ts +221 -0
- package/src/visibility.ts +190 -1
package/src/bench/room.ts
CHANGED
|
@@ -1,11 +1,15 @@
|
|
|
1
|
+
import type { Box } from "../geom.ts";
|
|
1
2
|
import { levelLinks } from "../levels.ts";
|
|
2
|
-
import type { Graph, Node, Result } from "../model.ts";
|
|
3
|
+
import type { Graph, LevelLink, Node, Result, Router } from "../model.ts";
|
|
3
4
|
import { makeRoom, openGaps, roomLinks, type RoomOptions } from "../room/index.ts";
|
|
4
5
|
import { gapLanes, LANE_ORDERS, type LaneOrder } from "../room/lanes.ts";
|
|
6
|
+
import { levelPairs, levelRoom, type LevelRoomOptions } from "../room/levels/index.ts";
|
|
7
|
+
import { buildSections, levelCut, treeDepth, type SectionOptions } from "../sections.ts";
|
|
5
8
|
import { sectionsRouter } from "../routers/sections.ts";
|
|
6
9
|
import { laneRoutes, runKey, segmentsOf } from "../segments.ts";
|
|
7
10
|
import { roomStrategies } from "../strategies/builtins.ts";
|
|
8
11
|
import type { Strategy } from "../strategies/model.ts";
|
|
12
|
+
import { benchHighways, type HighwayMeasures } from "./curves.ts";
|
|
9
13
|
import { measureLinks, measureRouteFaults, type LinkFaults } from "./links.ts";
|
|
10
14
|
import { measureQuality } from "./quality.ts";
|
|
11
15
|
|
|
@@ -37,7 +41,7 @@ export interface RoomBenchRow {
|
|
|
37
41
|
}
|
|
38
42
|
|
|
39
43
|
/** This function gives the area of the bounding box of the nodes. */
|
|
40
|
-
function areaOf(nodes: readonly
|
|
44
|
+
function areaOf(nodes: readonly { readonly box: Box }[]): number {
|
|
41
45
|
let x0 = Infinity;
|
|
42
46
|
let y0 = Infinity;
|
|
43
47
|
let x1 = -Infinity;
|
|
@@ -188,3 +192,206 @@ export function formatRoom(rows: readonly RoomBenchRow[]): string {
|
|
|
188
192
|
});
|
|
189
193
|
return [head, line, ...body].join("\n");
|
|
190
194
|
}
|
|
195
|
+
|
|
196
|
+
/** One row of the benchmark of the room of the zoom levels: one level with groups of one board. */
|
|
197
|
+
export interface LevelRoomRow {
|
|
198
|
+
readonly fixture: string;
|
|
199
|
+
/** The board: `packed`, `gutter` (each level open to the gutter only), `routes` (the room of the nodes alone), `levels` or `levels each`. */
|
|
200
|
+
readonly board: string;
|
|
201
|
+
readonly depth: number;
|
|
202
|
+
/** The number of items of the level. */
|
|
203
|
+
readonly items: number;
|
|
204
|
+
/** The number of links of the level. */
|
|
205
|
+
readonly links: number;
|
|
206
|
+
/** The area of the bounding box of the items of the level, as a share of the area of the packed board. */
|
|
207
|
+
readonly area: number;
|
|
208
|
+
/** The number of level links that go through a box that is not one of their ends. */
|
|
209
|
+
readonly through: number;
|
|
210
|
+
/** The crossings of the level links, with a lane for each link in each gap (`gapLanes` with the order `crossings`). */
|
|
211
|
+
readonly crossings: number;
|
|
212
|
+
/** The crossings of the level links without lanes. The links of a gap share one line, so this count hides crossings. */
|
|
213
|
+
readonly sharedCrossings: number;
|
|
214
|
+
/** The mean detour of the level links. */
|
|
215
|
+
readonly detour: number;
|
|
216
|
+
/** The worst detour of the level links. */
|
|
217
|
+
readonly worstDetour: number;
|
|
218
|
+
/** The number of pairs of items of the level whose order or alignment on an axis is not the order of the packed board. */
|
|
219
|
+
readonly order: number;
|
|
220
|
+
/** The number of pairs of items of the level before whose order or alignment on an axis changes at this level. */
|
|
221
|
+
readonly levelOrder: number;
|
|
222
|
+
/**
|
|
223
|
+
* The mean shift of an item against its parent from the level before, in gutters. The shift compares the place of the item in its parent
|
|
224
|
+
* at this level with its place in its parent at the level before. The value 0 means that the item moves with its parent.
|
|
225
|
+
*/
|
|
226
|
+
readonly shift: number;
|
|
227
|
+
/** The largest shift of an item against its parent, in gutters. */
|
|
228
|
+
readonly worstShift: number;
|
|
229
|
+
/** The time to open the board of all levels, in milliseconds. It is 0 for the packed board. */
|
|
230
|
+
readonly ms: number;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/** One board of the benchmark of the room of the zoom levels. It has a name and the nodes at each depth, from 0 to the level with each node alone. */
|
|
234
|
+
interface LevelBoards {
|
|
235
|
+
readonly name: string;
|
|
236
|
+
readonly nodes: readonly (readonly Node[])[];
|
|
237
|
+
readonly ms: number;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/** This function gives the sign of `d`, with 0 for a value nearer to 0 than `snap`. */
|
|
241
|
+
const sign = (d: number, snap: number): number => (Math.abs(d) <= snap ? 0 : Math.sign(d));
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* This function counts the pairs of boxes whose order or alignment on an axis is not the same in `a` and in `b`.
|
|
245
|
+
* The order of two boxes is the order of their left (or top) edges. The cost is quadratic in the number of boxes.
|
|
246
|
+
*/
|
|
247
|
+
export function orderChanges(a: readonly Box[], b: readonly Box[], snap: number = 0.5): number {
|
|
248
|
+
let n = 0;
|
|
249
|
+
for (const axis of ["x", "y"] as const)
|
|
250
|
+
for (let i = 0; i < a.length; i++)
|
|
251
|
+
for (let j = i + 1; j < a.length; j++) if (sign(a[i]![axis] - a[j]![axis], snap) !== sign(b[i]![axis] - b[j]![axis], snap)) n++;
|
|
252
|
+
return n;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
const middle = (b: Box): { x: number; y: number } => ({ x: b.x + b.w / 2, y: b.y + b.h / 2 });
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* This function measures the room of the zoom levels of a board. The rows of each board give each level with groups, from depth 1.
|
|
259
|
+
* The boards are the packed board, the fair baseline with each level open to the gutter only, and the room of the nodes alone (`makeRoom`).
|
|
260
|
+
* Then come two boards of `levelRoom` with the options. The board `levels` uses the board of the nearest level for each level.
|
|
261
|
+
* The board `levels each` has a board for each level.
|
|
262
|
+
*
|
|
263
|
+
* The links of each level are `levelLinks` with their defaults. Each link gets a lane of `spacing` in each gap.
|
|
264
|
+
*/
|
|
265
|
+
export function benchLevelRoom(fixture: string, graph: Graph, options: LevelRoomOptions = {}, sections: SectionOptions = {}): LevelRoomRow[] {
|
|
266
|
+
const gutter = options.gutter ?? 8;
|
|
267
|
+
const spacing = options.spacing ?? 4;
|
|
268
|
+
const snap = options.snap ?? 0.5;
|
|
269
|
+
const root = buildSections(graph.nodes, sections);
|
|
270
|
+
const deepest = treeDepth(root);
|
|
271
|
+
const links = roomLinks(graph);
|
|
272
|
+
const all = (nodes: readonly Node[]): (readonly Node[])[] => Array.from({ length: deepest + 2 }, () => nodes);
|
|
273
|
+
const timed = (name: string, make: () => (readonly Node[])[]): LevelBoards => {
|
|
274
|
+
const t0 = now();
|
|
275
|
+
const nodes = make();
|
|
276
|
+
return { name, nodes, ms: now() - t0 };
|
|
277
|
+
};
|
|
278
|
+
const boards: LevelBoards[] = [
|
|
279
|
+
{ name: "packed", nodes: all(graph.nodes), ms: 0 },
|
|
280
|
+
timed("gutter", () => levelRoom(graph.nodes, links, root, { ...options, spacing: 0, maxGrowth: Infinity, passes: 1, board: "each" }).levels.map((l) => l.nodes)),
|
|
281
|
+
timed("routes", () => all(makeRoom(graph.nodes, links, { ...options, method: "routes" }).items)),
|
|
282
|
+
timed("levels", () => levelRoom(graph.nodes, links, root, { ...options, board: "shared" }).levels.map((l) => l.nodes)),
|
|
283
|
+
timed("levels each", () => levelRoom(graph.nodes, links, root, { ...options, board: "each" }).levels.map((l) => l.nodes)),
|
|
284
|
+
];
|
|
285
|
+
const indexOf = new Map(graph.nodes.map((n, i) => [n.id, i]));
|
|
286
|
+
const area0 = areaOf(graph.nodes);
|
|
287
|
+
const rows: LevelRoomRow[] = [];
|
|
288
|
+
for (const board of boards) {
|
|
289
|
+
for (let d = 1; d <= deepest; d++) {
|
|
290
|
+
const here = board.nodes[d]!;
|
|
291
|
+
const before = board.nodes[d - 1]!;
|
|
292
|
+
const cut = levelCut(here, root, d);
|
|
293
|
+
const pairs = levelPairs(cut.items, cut.owner, indexOf, links);
|
|
294
|
+
const boxes = new Map(cut.items.map((i) => [i.id, i.box]));
|
|
295
|
+
const shared = levelLinks(cut.items, pairs);
|
|
296
|
+
const drawn = gapLanes(cut.items, shared, { spacing, order: "crossings", snap });
|
|
297
|
+
const asGraph: Graph = { nodes: cut.items, edges: pairs.map((p, i) => ({ id: `l${i}`, source: { node: p.source }, target: { node: p.target } })) };
|
|
298
|
+
const asRoutes = (list: readonly LevelLink[]): Result => ({ routes: list.map((l, i) => ({ edge: `l${i}`, points: l.points })) });
|
|
299
|
+
const q = measureQuality(asGraph, asRoutes(drawn));
|
|
300
|
+
const faults = measureLinks(boxes, drawn);
|
|
301
|
+
// The level before: its items at its own places, and the same items at the places of this level.
|
|
302
|
+
const parentsThen = levelCut(before, root, d - 1);
|
|
303
|
+
const parentsNow = levelCut(here, root, d - 1);
|
|
304
|
+
const childrenThen = levelCut(before, root, d);
|
|
305
|
+
let shift = 0;
|
|
306
|
+
let worstShift = 0;
|
|
307
|
+
cut.items.forEach((c, k) => {
|
|
308
|
+
const parent = parentsThen.owner[indexOf.get(c.nodes[0]!)!]!;
|
|
309
|
+
const at = middle(c.box);
|
|
310
|
+
const was = middle(childrenThen.items[k]!.box);
|
|
311
|
+
const parentAt = middle(parentsNow.items[parent]!.box);
|
|
312
|
+
const parentWas = middle(parentsThen.items[parent]!.box);
|
|
313
|
+
const s = Math.hypot(at.x - parentAt.x - (was.x - parentWas.x), at.y - parentAt.y - (was.y - parentWas.y)) / gutter;
|
|
314
|
+
shift += s;
|
|
315
|
+
worstShift = Math.max(worstShift, s);
|
|
316
|
+
});
|
|
317
|
+
rows.push({
|
|
318
|
+
fixture,
|
|
319
|
+
board: board.name,
|
|
320
|
+
depth: d,
|
|
321
|
+
items: cut.items.length,
|
|
322
|
+
links: pairs.length,
|
|
323
|
+
area: area0 > 0 ? areaOf(cut.items) / area0 : 1,
|
|
324
|
+
through: faults.through,
|
|
325
|
+
crossings: q.crossings,
|
|
326
|
+
sharedCrossings: measureQuality(asGraph, asRoutes(shared)).crossings,
|
|
327
|
+
detour: q.detour,
|
|
328
|
+
worstDetour: faults.worstDetour,
|
|
329
|
+
order: orderChanges(
|
|
330
|
+
levelCut(graph.nodes, root, d).items.map((i) => i.box),
|
|
331
|
+
cut.items.map((i) => i.box),
|
|
332
|
+
snap,
|
|
333
|
+
),
|
|
334
|
+
levelOrder: orderChanges(
|
|
335
|
+
parentsThen.items.map((i) => i.box),
|
|
336
|
+
parentsNow.items.map((i) => i.box),
|
|
337
|
+
snap,
|
|
338
|
+
),
|
|
339
|
+
shift: cut.items.length ? shift / cut.items.length : 0,
|
|
340
|
+
worstShift,
|
|
341
|
+
ms: board.ms,
|
|
342
|
+
});
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
return rows;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
/** This function writes the rows of `benchLevelRoom` as a Markdown table. */
|
|
349
|
+
export function formatLevelRoom(rows: readonly LevelRoomRow[]): string {
|
|
350
|
+
const head = "| fixture | board | depth | items | links | area | through | crossings | shared crossings | detour | worst detour | order | level order | shift | worst shift | ms |";
|
|
351
|
+
const line = "|---|---|---:|---:|---:|---:|---:|---:|---:|---:|---:|---:|---:|---:|---:|---:|";
|
|
352
|
+
const body = rows.map(
|
|
353
|
+
(r) =>
|
|
354
|
+
`| ${r.fixture} | ${r.board} | ${r.depth} | ${r.items} | ${r.links} | ${r.area.toFixed(3)} | ${r.through} | ${r.crossings} | ${r.sharedCrossings} | ${r.detour.toFixed(3)} | ${r.worstDetour.toFixed(2)} | ${r.order} | ${r.levelOrder} | ${r.shift.toFixed(2)} | ${r.worstShift.toFixed(2)} | ${r.ms.toFixed(1)} |`,
|
|
355
|
+
);
|
|
356
|
+
return [head, line, ...body].join("\n");
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
/** One row of `benchBendRoom`: the lanes of the bundles of one router on one open board, after `highwayCurves`. */
|
|
360
|
+
export interface BendRoomRow extends HighwayMeasures {
|
|
361
|
+
readonly router: string;
|
|
362
|
+
readonly fixture: string;
|
|
363
|
+
/** The board: `room` opens the gaps for the links only, and `room + bends` also opens them for the bends of the bundles. */
|
|
364
|
+
readonly board: "room" | "room + bends";
|
|
365
|
+
/** The area of the bounding box of the boxes, as a share of the area of the board before the room. */
|
|
366
|
+
readonly area: number;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
/**
|
|
370
|
+
* This function measures the room for the bends of the bundles (HWC-D). It opens the board two times with `makeRoom`, without and with the option `bends`.
|
|
371
|
+
* The option `bends` gets the router and the lane options of the curves. Then the router routes each open board, and `benchHighways` curves its bundles with `highway`.
|
|
372
|
+
*/
|
|
373
|
+
export function benchBendRoom(router: Router, fixture: string, graph: Graph, options: RoomOptions = { gutter: 8, spacing: 4 }): BendRoomRow[] {
|
|
374
|
+
const lanes = { spacing: 4, maxWidth: 16, minRadius: 4 };
|
|
375
|
+
const links = roomLinks(graph);
|
|
376
|
+
const area0 = areaOf(graph.nodes);
|
|
377
|
+
const boards = [
|
|
378
|
+
["room", makeRoom(graph.nodes, links, options).items],
|
|
379
|
+
["room + bends", makeRoom(graph.nodes, links, { ...options, bends: { router, edges: graph.edges, ...lanes } }).items],
|
|
380
|
+
] as const;
|
|
381
|
+
return boards.map(([board, nodes]) => {
|
|
382
|
+
const [, highway] = benchHighways(router, fixture, { nodes, edges: graph.edges }, { spacing: lanes.spacing, maxWidth: lanes.maxWidth, minRadius: lanes.minRadius });
|
|
383
|
+
const { router: name, fixture: _f, curve: _c, ms: _ms, ...measures } = highway!;
|
|
384
|
+
return { router: name, fixture, board, area: area0 > 0 ? areaOf(nodes) / area0 : 1, ...measures };
|
|
385
|
+
});
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
/** This function writes the rows of `benchBendRoom` as a Markdown table. The spacing error is a percent of the spacing. */
|
|
389
|
+
export function formatBendRoom(rows: readonly BendRoomRow[]): string {
|
|
390
|
+
const head = "| router | fixture | board | area | bundles | worst spacing | min radius | under min | through |";
|
|
391
|
+
const line = "|---|---|---|---:|---:|---:|---:|---:|---:|";
|
|
392
|
+
const body = rows.map(
|
|
393
|
+
(r) =>
|
|
394
|
+
`| ${r.router} | ${r.fixture} | ${r.board} | ${r.area.toFixed(3)} | ${r.bundles} | ${(r.worstSpacing * 100).toFixed(1)}% | ${r.minRadius === Infinity ? "-" : r.minRadius.toFixed(2)} | ${r.underMin} | ${r.through} |`,
|
|
395
|
+
);
|
|
396
|
+
return [head, line, ...body].join("\n");
|
|
397
|
+
}
|
package/src/bench/run.ts
CHANGED
|
@@ -25,12 +25,28 @@ export interface BenchRow {
|
|
|
25
25
|
readonly faults: LinkFaults;
|
|
26
26
|
/** The faults of each zoom level with groups (`measureLevelFaults`). The list is empty for a router without levels. */
|
|
27
27
|
readonly levelFaults: readonly LevelFaults[];
|
|
28
|
+
/** The routes of this cell took longer than the cell limit. The cell did not start more routes. A row within the limit has no such field. */
|
|
29
|
+
readonly overLimit?: OverLimit;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** The record of a cell over its time limit. */
|
|
33
|
+
export interface OverLimit {
|
|
34
|
+
/** The cell limit, in milliseconds. */
|
|
35
|
+
readonly limit: number;
|
|
36
|
+
/** The number of routes that the cell finished. With one route, the cell did not check "same twice". */
|
|
37
|
+
readonly runs: number;
|
|
28
38
|
}
|
|
29
39
|
|
|
30
40
|
/** The options of a benchmark run. */
|
|
31
41
|
export interface BenchOptions {
|
|
32
42
|
/** The number of timed routes of each fixture by each router. The default is 3. */
|
|
33
43
|
readonly repeat?: number;
|
|
44
|
+
/**
|
|
45
|
+
* The time limit of one cell (one router on one fixture), in milliseconds. The default is no limit.
|
|
46
|
+
* After the routes of a cell pass the limit, the cell starts no more routes, and its row gets `overLimit`.
|
|
47
|
+
* A route that runs does not stop.
|
|
48
|
+
*/
|
|
49
|
+
readonly cellLimit?: number;
|
|
34
50
|
}
|
|
35
51
|
|
|
36
52
|
/** This function routes each fixture with each router, checks the hard rules and measures the quality. */
|
|
@@ -38,7 +54,7 @@ export function runBenchmark(routers: readonly Router[], fixtures: readonly Fixt
|
|
|
38
54
|
const rows: BenchRow[] = [];
|
|
39
55
|
for (const spec of fixtures) {
|
|
40
56
|
const graph = makeFixture(spec);
|
|
41
|
-
for (const router of routers) if (graph.nodes.length <= (router.maxNodes ?? Infinity)) rows.push(benchOne(router, spec.name, graph, options.repeat ?? 3));
|
|
57
|
+
for (const router of routers) if (graph.nodes.length <= (router.maxNodes ?? Infinity)) rows.push(benchOne(router, spec.name, graph, options.repeat ?? 3, options.cellLimit));
|
|
42
58
|
}
|
|
43
59
|
return rows;
|
|
44
60
|
}
|
|
@@ -67,22 +83,41 @@ export function sameData(a: unknown, b: unknown): boolean {
|
|
|
67
83
|
return true;
|
|
68
84
|
}
|
|
69
85
|
|
|
70
|
-
/**
|
|
71
|
-
|
|
86
|
+
/**
|
|
87
|
+
* This function routes one graph with one router and measures the result. It routes at least two times, for the rule "same twice".
|
|
88
|
+
* After the routes pass `cellLimit` milliseconds, it starts no more routes and marks the row with `overLimit`.
|
|
89
|
+
*/
|
|
90
|
+
export function benchOne(router: Router, fixture: string, graph: Graph, repeat = 3, cellLimit: number = Infinity): BenchRow {
|
|
91
|
+
return benchRun(router, fixture, graph, repeat, cellLimit).row;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** One run of `benchRun`: the row of the benchmark and the result that the row measures. */
|
|
95
|
+
export interface BenchRun {
|
|
96
|
+
readonly row: BenchRow;
|
|
97
|
+
readonly result: Result;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** This function does the work of `benchOne` and also gives the result, so more measures can read it without a second route. */
|
|
101
|
+
export function benchRun(router: Router, fixture: string, graph: Graph, repeat = 3, cellLimit: number = Infinity): BenchRun {
|
|
72
102
|
const times: number[] = [];
|
|
73
103
|
let first: Result | undefined;
|
|
74
104
|
let deterministic = true;
|
|
105
|
+
let spent = 0;
|
|
75
106
|
for (let i = 0; i < Math.max(2, repeat); i++) {
|
|
76
107
|
const t0 = now();
|
|
77
108
|
const r = router.route(graph);
|
|
78
|
-
|
|
109
|
+
const t = now() - t0;
|
|
110
|
+
times.push(t);
|
|
111
|
+
spent += t;
|
|
79
112
|
if (!first) first = r;
|
|
80
113
|
else if (i === 1 && !sameData(r, first)) deterministic = false;
|
|
114
|
+
if (spent > cellLimit) break;
|
|
81
115
|
}
|
|
116
|
+
const runs = times.length;
|
|
82
117
|
times.sort((a, b) => a - b);
|
|
83
118
|
const result = first!;
|
|
84
119
|
const all = checkProperties(graph, result, 1_000_000);
|
|
85
|
-
|
|
120
|
+
const row: BenchRow = {
|
|
86
121
|
router: router.name,
|
|
87
122
|
fixture,
|
|
88
123
|
nodes: graph.nodes.length,
|
|
@@ -94,17 +129,38 @@ export function benchOne(router: Router, fixture: string, graph: Graph, repeat =
|
|
|
94
129
|
quality: measureQuality(graph, result),
|
|
95
130
|
faults: measureRouteFaults(graph, result),
|
|
96
131
|
levelFaults: measureLevelFaults(result),
|
|
132
|
+
...(spent > cellLimit ? { overLimit: { limit: cellLimit, runs } } : {}),
|
|
133
|
+
};
|
|
134
|
+
return { row, result };
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* This function puts rows in the order of one benchmark run: by fixture, then by router, each in the order of its list.
|
|
139
|
+
* A name that is not in its list goes after the names of the list. Rows with the same place keep their order.
|
|
140
|
+
* The workflow `Bench` uses it to join the rows of its parts.
|
|
141
|
+
*/
|
|
142
|
+
export function orderRows(rows: readonly BenchRow[], routers: readonly string[], fixtures: readonly string[]): BenchRow[] {
|
|
143
|
+
const place = (list: readonly string[], name: string): number => {
|
|
144
|
+
const i = list.indexOf(name);
|
|
145
|
+
return i < 0 ? list.length : i;
|
|
97
146
|
};
|
|
147
|
+
return rows
|
|
148
|
+
.map((row, i) => ({ row, i, f: place(fixtures, row.fixture), r: place(routers, row.router) }))
|
|
149
|
+
.sort((a, b) => a.f - b.f || a.r - b.r || a.i - b.i)
|
|
150
|
+
.map((x) => x.row);
|
|
98
151
|
}
|
|
99
152
|
|
|
100
|
-
/** This function writes the rows as a Markdown table. */
|
|
153
|
+
/** This function writes the rows as a Markdown table. A cell over its limit shows "over the limit" after its time, and a dash for "same twice" when it did not check it. */
|
|
101
154
|
export function formatTable(rows: readonly BenchRow[]): string {
|
|
102
155
|
const head = "| router | fixture | nodes | edges | ms | same twice | violations | ink | crossings | bends | inflections | clearance | tight | detour | plug angle |";
|
|
103
156
|
const line = "|---|---|---:|---:|---:|---|---:|---:|---:|---:|---:|---:|---:|---:|---:|";
|
|
104
157
|
const f = (v: number, d = 2): string => (Number.isInteger(v) ? String(v) : v.toFixed(d));
|
|
105
158
|
const body = rows.map((r) => {
|
|
106
159
|
const q = r.quality;
|
|
107
|
-
|
|
160
|
+
const over = r.overLimit;
|
|
161
|
+
const ms = over ? `${f(r.ms, 1)} over the limit` : f(r.ms, 1);
|
|
162
|
+
const same = over && over.runs < 2 ? "-" : r.deterministic ? "yes" : "NO";
|
|
163
|
+
return `| ${r.router} | ${r.fixture} | ${r.nodes} | ${r.edges} | ${ms} | ${same} | ${r.violations} | ${Math.round(q.ink)} | ${q.crossingsEstimated ? "≈" : ""}${q.crossings} | ${f(q.bends)} | ${f(q.inflections)} | ${f(q.clearance, 1)} | ${f(q.tight)} | ${f(q.detour)} | ${f(q.plugAngle, 1)} |`;
|
|
108
164
|
});
|
|
109
165
|
return [head, line, ...body].join("\n");
|
|
110
166
|
}
|
|
@@ -115,17 +171,36 @@ export function formatTable(rows: readonly BenchRow[]): string {
|
|
|
115
171
|
*/
|
|
116
172
|
export function formatFaults(rows: readonly BenchRow[]): string {
|
|
117
173
|
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 = "
|
|
174
|
+
"| router | fixture | through | out and back | worst detour | outside | level links | level overlaps | level through | level unavoidable | level avoidable | level out and back | level worst detour | level outside |";
|
|
175
|
+
const line = "|---|---|---:|---:|---:|---:|---:|---:|---:|---:|---:|---:|---:|---:|";
|
|
120
176
|
const body = rows.map((r) => {
|
|
121
177
|
const f = r.faults;
|
|
122
178
|
const cells = [r.router, r.fixture, f.through, f.backtrack, f.worstDetour.toFixed(2), f.outside];
|
|
123
|
-
if (r.levelFaults.length === 0) cells.push("-", "-", "-", "-", "-", "-");
|
|
179
|
+
if (r.levelFaults.length === 0) cells.push("-", "-", "-", "-", "-", "-", "-", "-");
|
|
124
180
|
else {
|
|
125
181
|
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);
|
|
182
|
+
cells.push(l.links, r.levelFaults.reduce((s, x) => s + x.overlaps, 0), l.through, l.unavoidable, l.avoidable, l.backtrack, l.worstDetour.toFixed(2), l.outside);
|
|
127
183
|
}
|
|
128
184
|
return `| ${cells.join(" | ")} |`;
|
|
129
185
|
});
|
|
130
186
|
return [head, line, ...body].join("\n");
|
|
131
187
|
}
|
|
188
|
+
|
|
189
|
+
/** A row with the faults of each zoom level, as `formatLevels` reads it: a router or a setting on one fixture. */
|
|
190
|
+
export interface LevelRow {
|
|
191
|
+
readonly router: string;
|
|
192
|
+
readonly fixture: string;
|
|
193
|
+
readonly levelFaults: readonly LevelFaults[];
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/** This function writes the faults of each zoom level with groups as a Markdown table, one line for each level. */
|
|
197
|
+
export function formatLevels(rows: readonly LevelRow[]): string {
|
|
198
|
+
const head = "| router | fixture | depth | items | groups | overlaps | links | through | unavoidable | avoidable | out and back | worst detour | outside |";
|
|
199
|
+
const line = "|---|---|---:|---:|---:|---:|---:|---:|---:|---:|---:|---:|---:|";
|
|
200
|
+
const body = rows.flatMap((r) =>
|
|
201
|
+
r.levelFaults.map((l) =>
|
|
202
|
+
[r.router, r.fixture, l.depth, l.items, l.groups, l.overlaps, l.links, l.through, l.unavoidable, l.avoidable, l.backtrack, l.worstDetour.toFixed(2), l.outside].join(" | "),
|
|
203
|
+
),
|
|
204
|
+
);
|
|
205
|
+
return [head, line, ...body.map((b) => `| ${b} |`)].join("\n");
|
|
206
|
+
}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import type { Graph, Result } from "../model.ts";
|
|
2
|
+
import { sectionsRouter, type SectionsOptions } from "../routers/sections.ts";
|
|
3
|
+
import { groupStrategies } from "../strategies/builtins.ts";
|
|
4
|
+
import type { Strategy } from "../strategies/model.ts";
|
|
5
|
+
import { sameData } from "./run.ts";
|
|
6
|
+
import { measureLevelFaults, measureRouteFaults, sumFaults, type LinkFaults } from "./links.ts";
|
|
7
|
+
|
|
8
|
+
// The core runs in any host, so it reads the clock without the types of Node or the browser.
|
|
9
|
+
const now = (): number => (globalThis as { performance?: { now(): number } }).performance?.now() ?? Date.now();
|
|
10
|
+
|
|
11
|
+
/** The options of one setting of the router `sections`, without the name. */
|
|
12
|
+
export type SectionsSettingOptions = Omit<SectionsOptions, "name" | "maxNodes">;
|
|
13
|
+
|
|
14
|
+
/** One setting of a sweep of the router `sections`. */
|
|
15
|
+
export interface SectionsSetting {
|
|
16
|
+
/** The name of the setting in the column `setting`. */
|
|
17
|
+
readonly name: string;
|
|
18
|
+
readonly options: SectionsSettingOptions;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** One row of a sweep of the router `sections`: one setting on one board, and the measures of its routes and of its zoom levels. */
|
|
22
|
+
export interface SectionsBenchRow {
|
|
23
|
+
readonly fixture: string;
|
|
24
|
+
readonly setting: string;
|
|
25
|
+
/** The faults of the routes of the edges (`measureRouteFaults`). */
|
|
26
|
+
readonly faults: LinkFaults;
|
|
27
|
+
/** The sum of the faults of the levels with groups (`measureLevelFaults` and `sumFaults`). */
|
|
28
|
+
readonly levelFaults: LinkFaults;
|
|
29
|
+
/** The sum of the overlaps of the levels with groups. */
|
|
30
|
+
readonly overlaps: number;
|
|
31
|
+
/** The number of routes that go through hubs: the routes that are not the same as the direct route of their edge. */
|
|
32
|
+
readonly hubRoutes: number;
|
|
33
|
+
/** The number of shared stretches of way of the result (`runs`). */
|
|
34
|
+
readonly runs: number;
|
|
35
|
+
/** The number of zoom levels, with the last level of single nodes. */
|
|
36
|
+
readonly levels: number;
|
|
37
|
+
/** The number of items of the far view: the first level with more than one item. */
|
|
38
|
+
readonly farItems: number;
|
|
39
|
+
/** The time of one route, in milliseconds. */
|
|
40
|
+
readonly ms: number;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** This function gives the number of items of the first level with more than one item, or 0 for a result without levels. */
|
|
44
|
+
function farItems(result: Result): number {
|
|
45
|
+
return result.levels?.find((l) => l.items.length > 1)?.items.length ?? 0;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* This function measures the router `sections` on one board with each setting. The first row is the fair baseline: the defaults, with no option.
|
|
50
|
+
* The direct routes come from the hub limit 0, so `hubRoutes` counts the routes that use the hubs.
|
|
51
|
+
*/
|
|
52
|
+
export function sweepSections(fixture: string, graph: Graph, settings: readonly SectionsSetting[]): SectionsBenchRow[] {
|
|
53
|
+
const all: SectionsSetting[] = [{ name: "defaults", options: {} }, ...settings];
|
|
54
|
+
return all.map((s) => {
|
|
55
|
+
const direct = sectionsRouter({ ...s.options, name: "direct", stretch: 0, stretchPad: 0 }).route(graph);
|
|
56
|
+
const t0 = now();
|
|
57
|
+
const result = sectionsRouter({ ...s.options, name: s.name }).route(graph);
|
|
58
|
+
const ms = now() - t0;
|
|
59
|
+
const levelFaults = measureLevelFaults(result);
|
|
60
|
+
return {
|
|
61
|
+
fixture,
|
|
62
|
+
setting: s.name,
|
|
63
|
+
faults: measureRouteFaults(graph, result),
|
|
64
|
+
levelFaults: sumFaults(levelFaults),
|
|
65
|
+
overlaps: levelFaults.reduce((n, f) => n + f.overlaps, 0),
|
|
66
|
+
hubRoutes: result.routes.filter((r, i) => !sameData(r.points, direct.routes[i]?.points)).length,
|
|
67
|
+
runs: result.runs?.length ?? 0,
|
|
68
|
+
levels: result.levels?.length ?? 0,
|
|
69
|
+
farItems: farItems(result),
|
|
70
|
+
ms,
|
|
71
|
+
};
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** The values of the hub limit (`stretch`) of the sweep of CUSTOM. The default is 2. */
|
|
76
|
+
export const STRETCH_SWEEP: readonly number[] = [1.25, 1.5, 2, 3, Infinity];
|
|
77
|
+
|
|
78
|
+
/** This function gives the settings of the sweep of the hub limit: one setting for each value of `stretch`. */
|
|
79
|
+
export function stretchSettings(values: readonly number[] = STRETCH_SWEEP): SectionsSetting[] {
|
|
80
|
+
return values.map((stretch) => ({ name: `stretch ${stretch}`, options: { stretch } }));
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** The values of `hubSlack` of the sweep of CUSTOM. The default is 80. */
|
|
84
|
+
export const SLACK_SWEEP: readonly number[] = [0, 40, 80, 160, Infinity];
|
|
85
|
+
|
|
86
|
+
/** This function gives the settings of the sweep of the slack of the hubs: one setting for each value of `hubSlack`. */
|
|
87
|
+
export function slackSettings(values: readonly number[] = SLACK_SWEEP): SectionsSetting[] {
|
|
88
|
+
return values.map((hubSlack) => ({ name: `hubSlack ${hubSlack}`, options: { hubSlack } }));
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** The values of the section limit (`cap`) of the sweep of CUSTOM. The default is 8. */
|
|
92
|
+
export const CAP_SWEEP: readonly number[] = [4, 6, 8, 12, 16];
|
|
93
|
+
|
|
94
|
+
/** This function gives the settings of the sweep of the section limit: one setting for each value of `cap`. */
|
|
95
|
+
export function capSettings(values: readonly number[] = CAP_SWEEP): SectionsSetting[] {
|
|
96
|
+
return values.map((cap) => ({ name: `cap ${cap}`, options: { cap } }));
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* This function gives one setting for each grouping rule: the router `sections` with the strategy as its option `group`.
|
|
101
|
+
* The map `options` gives the options of a strategy by its name.
|
|
102
|
+
*/
|
|
103
|
+
export function groupSettings(
|
|
104
|
+
strategies: readonly Strategy<"group">[] = Object.values(groupStrategies),
|
|
105
|
+
options: Readonly<Record<string, object>> = {},
|
|
106
|
+
): SectionsSetting[] {
|
|
107
|
+
return strategies.map((group) => ({ name: `group ${group.name}`, options: { group, ...(options[group.name] ? { groupOptions: options[group.name] } : {}) } }));
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** This function writes the rows of `sweepSections` as a Markdown table. */
|
|
111
|
+
export function formatSections(rows: readonly SectionsBenchRow[]): string {
|
|
112
|
+
const head =
|
|
113
|
+
"| fixture | setting | through | out and back | worst detour | outside | hub routes | runs | levels | far items | level through | level out and back | level outside | level overlaps | ms |";
|
|
114
|
+
const line = "|---|---|---:|---:|---:|---:|---:|---:|---:|---:|---:|---:|---:|---:|---:|";
|
|
115
|
+
const body = rows.map((r) => {
|
|
116
|
+
const f = r.faults;
|
|
117
|
+
const l = r.levelFaults;
|
|
118
|
+
return `| ${r.fixture} | ${r.setting} | ${f.through} | ${f.backtrack} | ${f.worstDetour.toFixed(2)} | ${f.outside} | ${r.hubRoutes} | ${r.runs} | ${r.levels} | ${r.farItems} | ${l.through} | ${l.backtrack} | ${l.outside} | ${r.overlaps} | ${r.ms.toFixed(1)} |`;
|
|
119
|
+
});
|
|
120
|
+
return [head, line, ...body].join("\n");
|
|
121
|
+
}
|