@mark1russell7/highways 0.4.0 → 0.4.2

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 (52) hide show
  1. package/README.md +67 -1
  2. package/build/index.d.ts +954 -2
  3. package/build/index.d.ts.map +1 -1
  4. package/build/index.js +5914 -1419
  5. package/build/index.js.map +1 -1
  6. package/package.json +2 -1
  7. package/src/bench/measures/bundles.ts +101 -2
  8. package/src/bench/measures/highways.ts +93 -0
  9. package/src/bench/measures/index.ts +2 -0
  10. package/src/bench/measures/order.ts +97 -0
  11. package/src/bench/room.ts +126 -9
  12. package/src/bench/screen/measures.ts +17 -0
  13. package/src/bench/screen/screen.ts +87 -9
  14. package/src/highways/base.ts +126 -0
  15. package/src/highways/flow-field/index.ts +49 -0
  16. package/src/highways/gates/ambiguity/index.ts +110 -0
  17. package/src/highways/gates/breakeven/index.ts +62 -0
  18. package/src/highways/gates/hysteresis/index.ts +48 -0
  19. package/src/highways/gates/ids/index.ts +59 -0
  20. package/src/highways/gates/index.ts +6 -0
  21. package/src/highways/gates/model.ts +129 -0
  22. package/src/highways/gates/stretch/index.ts +39 -0
  23. package/src/highways/grouped.ts +123 -0
  24. package/src/highways/index.ts +15 -0
  25. package/src/highways/level-links/index.ts +114 -0
  26. package/src/highways/meet/index.ts +84 -0
  27. package/src/highways/model.ts +5 -0
  28. package/src/highways/od-cells/index.ts +50 -0
  29. package/src/highways/quickbundles/index.ts +117 -0
  30. package/src/highways/road-classes/index.ts +74 -0
  31. package/src/highways/router.ts +1 -1
  32. package/src/highways/select/index.ts +162 -0
  33. package/src/highways/stretches.ts +141 -0
  34. package/src/highways/stubs/index.ts +117 -0
  35. package/src/highways/weiszfeld/index.ts +105 -0
  36. package/src/highways/wspd/index.ts +130 -0
  37. package/src/highways/zipper/index.ts +168 -0
  38. package/src/index.ts +2 -0
  39. package/src/order/index.ts +65 -0
  40. package/src/plugs/justify.ts +48 -0
  41. package/src/plugs/side.ts +138 -0
  42. package/src/repair/index.ts +377 -0
  43. package/src/repair/walk.ts +220 -0
  44. package/src/room/lanes.ts +109 -10
  45. package/src/routers/corners.ts +73 -6
  46. package/src/routers/fan.ts +145 -0
  47. package/src/routers/index.ts +2 -2
  48. package/src/routers/shared.ts +107 -8
  49. package/src/routers/snell.ts +140 -0
  50. package/src/routers/trunks.ts +43 -2
  51. package/src/strategies/builtins.ts +149 -7
  52. package/src/strategies/schemas.ts +173 -2
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.4.0",
12
+ "version": "0.4.2",
13
13
  "type": "module",
14
14
  "main": "./build/index.js",
15
15
  "types": "./build/index.d.ts",
@@ -38,6 +38,7 @@
38
38
  "build": "tsdown",
39
39
  "test": "vitest run",
40
40
  "bench": "node scripts/bench.ts",
41
+ "ideas": "node scripts/ideas.ts",
41
42
  "bench:join": "node scripts/bench-join.ts",
42
43
  "profile": "node scripts/profile.ts",
43
44
  "levels": "node scripts/levels.ts",
@@ -1,7 +1,8 @@
1
- import { dist, polylineLength } from "../../geom.ts";
1
+ import { dist, polylineLength, type Point } from "../../geom.ts";
2
2
  import { rng } from "../../fixtures.ts";
3
- import type { Graph, Route } from "../../model.ts";
3
+ import type { Graph, Result, Route } from "../../model.ts";
4
4
  import { drawnSegments, segmentKey } from "./routes.ts";
5
+ import { quantile } from "./stats.ts";
5
6
 
6
7
  /**
7
8
  * This function gives the share of the ink that shared segments save (B1 of MEAS0): `inkSaved = 1 - ink(S) / Σ_e L(r_e)`.
@@ -117,3 +118,101 @@ export function ambiguity(graph: Graph, routes: readonly Route[], options: Ambig
117
118
  });
118
119
  return { value: all ? wrong / all : 0, estimated: !exact };
119
120
  }
121
+
122
+ /** The drawn segments of each route, as keys, in the order of its points. A segment shorter than the grain of `segmentKey` has no key. */
123
+ function routeKeys(routes: readonly Route[]): { readonly keys: Map<string, string[]>; readonly users: Map<string, Set<string>> } {
124
+ const keys = new Map<string, string[]>();
125
+ const users = new Map<string, Set<string>>();
126
+ for (const r of routes) {
127
+ const own: string[] = [];
128
+ for (let i = 1; i < r.points.length; i++) {
129
+ const k = segmentKey(r.points[i - 1]!, r.points[i]!);
130
+ if (k === undefined) continue;
131
+ own.push(k);
132
+ let set = users.get(k);
133
+ if (!set) users.set(k, (set = new Set()));
134
+ set.add(r.edge);
135
+ }
136
+ keys.set(r.edge, own);
137
+ }
138
+ return { keys, users };
139
+ }
140
+
141
+ /**
142
+ * This function gives the bundled share of a result (B2 of MEAS0): `count(e : e is in a run or a bundle) / m`.
143
+ * An edge is also in a bundle when its route shares a drawn segment with the route of another edge. Thus a router without `runs` and `bundles` gets a value too.
144
+ * The scale is 0 to 1. A higher value is better only in context. The cost is linear.
145
+ */
146
+ export function bundledShare(result: Result): number {
147
+ const routes = result.routes;
148
+ if (routes.length === 0) return 0;
149
+ const shared = new Set<string>();
150
+ for (const run of result.runs ?? []) for (const e of run.edges) shared.add(e);
151
+ for (const b of result.bundles ?? []) if (b.edges.length > 1) for (const e of b.edges) shared.add(e);
152
+ const { keys, users } = routeKeys(routes);
153
+ for (const [edge, own] of keys) if (own.some((k) => (users.get(k)?.size ?? 0) > 1)) shared.add(edge);
154
+ let count = 0;
155
+ for (const r of routes) if (shared.has(r.edge)) count++;
156
+ return count / routes.length;
157
+ }
158
+
159
+ /** The bundle stretch of the routes against base routes (B3 of MEAS0). */
160
+ export interface BundleStretch {
161
+ /** The median of `st(e) = L(r_e) / L(r*_e)`. A value of 1 is the length of the base route. Lower is better. */
162
+ readonly median: number;
163
+ /** The `0.95` quantile of `st(e)`: the tail of long detours that the bundles add. */
164
+ readonly q95: number;
165
+ /** The number of edges with a route in both lists and a base route longer than 1 unit. */
166
+ readonly edges: number;
167
+ }
168
+
169
+ /**
170
+ * This function gives the bundle stretch (B3 of MEAS0): `st(e) = L(r_e) / L(r*_e)` for each edge, with `r*_e` the base route of the edge.
171
+ * The base routes are the routes of `corners` on the same input. An edge without a base route, or with a base route of 1 unit or less, does not count.
172
+ * The cost is an extra run of the base router, and then linear.
173
+ */
174
+ export function bundleStretch(routes: readonly Route[], base: readonly Route[]): BundleStretch {
175
+ const baseLength = new Map<string, number>();
176
+ for (const r of base) baseLength.set(r.edge, polylineLength(r.points));
177
+ const st: number[] = [];
178
+ for (const r of routes) {
179
+ const b = baseLength.get(r.edge);
180
+ if (b === undefined || b <= 1 || r.points.length < 2) continue;
181
+ st.push(polylineLength(r.points) / b);
182
+ }
183
+ if (st.length === 0) return { median: 1, q95: 1, edges: 0 };
184
+ return { median: quantile(st, 0.5), q95: quantile(st, 0.95), edges: st.length };
185
+ }
186
+
187
+ /**
188
+ * This function gives the merge angle of the routes (B6 of MEAS0): the largest turn of a lane where it joins or leaves a run, in degrees.
189
+ * A run is a drawn segment that the routes of two edges or more share. A route joins or leaves a run at a vertex where the edges on its segment change.
190
+ * One of the two segments at that vertex has two edges or more. The turn is the angle between the two segments.
191
+ * Routes without a shared segment give 0. The scale is 0 to 180 degrees, and lower is better. The cost is linear.
192
+ */
193
+ export function mergeAngle(routes: readonly Route[]): number {
194
+ const { users } = routeKeys(routes);
195
+ const sig = new Map<string, string>();
196
+ for (const [k, set] of users) sig.set(k, [...set].sort().join("\u0000"));
197
+ let max = 0;
198
+ for (const r of routes) {
199
+ // The vertices of the route, with each segment that has no key dropped.
200
+ let prev: { readonly k: string; readonly a: Point; readonly b: Point } | undefined;
201
+ for (let i = 1; i < r.points.length; i++) {
202
+ const a = r.points[i - 1]!;
203
+ const b = r.points[i]!;
204
+ const k = segmentKey(a, b);
205
+ if (k === undefined) continue;
206
+ if (prev && sig.get(prev.k) !== sig.get(k) && ((users.get(prev.k)?.size ?? 0) > 1 || (users.get(k)?.size ?? 0) > 1)) {
207
+ const ux = prev.b.x - prev.a.x;
208
+ const uy = prev.b.y - prev.a.y;
209
+ const vx = b.x - a.x;
210
+ const vy = b.y - a.y;
211
+ const turn = Math.abs(Math.atan2(ux * vy - uy * vx, ux * vx + uy * vy));
212
+ max = Math.max(max, (turn * 180) / Math.PI);
213
+ }
214
+ prev = { k, a, b };
215
+ }
216
+ }
217
+ return max;
218
+ }
@@ -0,0 +1,93 @@
1
+ import { follow } from "../../highways/follow.ts";
2
+ import { commonEdges, matchHighways } from "../../highways/gates/model.ts";
3
+ import type { HighwayPlan } from "../../highways/model.ts";
4
+ import { wayOptionsOf } from "../../highways/router.ts";
5
+ import type { Graph } from "../../model.ts";
6
+ import type { Strategy } from "../../strategies/model.ts";
7
+ import { changeGraph, routeStability } from "./stability.ts";
8
+
9
+ /**
10
+ * This function gives the churn of the highways over one edit (Z10 for highways, from HWY-046). Two edges are a pair when they share a highway.
11
+ * The churn is the share of the pairs that come or go: `|P ∆ P'| / |P ∪ P'|`. Only the edges of `edges` count, for example the edges of both graphs.
12
+ * Two plans without pairs give 0. The scale is 0 to 1, and lower is better.
13
+ */
14
+ export function highwayChurn(before: HighwayPlan, after: HighwayPlan, edges: ReadonlySet<string>): number {
15
+ const pairs = (plan: HighwayPlan): Set<string> => {
16
+ const out = new Set<string>();
17
+ for (const h of plan.highways) {
18
+ const list = [...new Set(h.members.map((m) => m.edge).filter((e) => edges.has(e)))].sort();
19
+ for (let i = 0; i < list.length; i++) for (let j = i + 1; j < list.length; j++) out.add(`${list[i]}\u0000${list[j]}`);
20
+ }
21
+ return out;
22
+ };
23
+ const a = pairs(before);
24
+ const b = pairs(after);
25
+ let common = 0;
26
+ for (const p of a) if (b.has(p)) common++;
27
+ const all = a.size + b.size - common;
28
+ return all === 0 ? 0 : (all - common) / all;
29
+ }
30
+
31
+ /** The match of the highways over one edit. */
32
+ export interface HighwayIdKeep {
33
+ /** The number of new highways with a match in the old plan: a Jaccard index of their edges of 0.5 or more. */
34
+ readonly matched: number;
35
+ /** The number of matched highways that keep the id of their match. */
36
+ readonly kept: number;
37
+ /** `kept / matched`, or `NaN` without a match. The scale is 0 to 1, and higher is better. */
38
+ readonly share: number;
39
+ }
40
+
41
+ /**
42
+ * This function tells how many highways keep their id over one edit (HWY-054). It matches the new highways with the old ones by `matchHighways`, with the index 0.5.
43
+ * A matched highway keeps its id when its id is the id of its match. Only the edges of `edges` count.
44
+ */
45
+ export function highwayIdKeep(before: HighwayPlan, after: HighwayPlan, edges: ReadonlySet<string>): HighwayIdKeep {
46
+ const match = matchHighways(before, after, 0.5, edges);
47
+ let kept = 0;
48
+ for (const [i, i0] of match) if (after.highways[i]!.id === before.highways[i0]!.id) kept++;
49
+ return { matched: match.size, kept, share: match.size === 0 ? Number.NaN : kept / match.size };
50
+ }
51
+
52
+ /** The stability of a highway strategy over a seeded sequence of edits. Each value is the mean over the edits. */
53
+ export interface HighwayEdits {
54
+ /** The mean Fréchet distance of the routes over one edit, in units of the board (Z10). */
55
+ readonly mean: number;
56
+ /** The share of the routes that move over one edit (Z10). */
57
+ readonly moved: number;
58
+ /** The churn of the highways over one edit (`highwayChurn`). */
59
+ readonly churn: number;
60
+ /** The share of the matched highways that keep their id over one edit (`highwayIdKeep`), or `NaN` without a match. */
61
+ readonly ids: number;
62
+ }
63
+
64
+ /**
65
+ * This function measures a highway strategy over a sequence of `edits` seeded edits. Each edit is the small change of `changeGraph` with the seed `seed + i`.
66
+ * The strategy gets the graph and its plan before the edit as `previous`, so a strategy with memory can keep its highways.
67
+ * `follow` routes each plan with the way options of the strategy. The same graph, strategy, options and seed give the same values.
68
+ */
69
+ export function highwayEdits(graph: Graph, strategy: Strategy<"highway", object>, options: object | undefined, edits: number, seed = 1): HighwayEdits {
70
+ const way = wayOptionsOf({ ...strategy.defaults, ...options });
71
+ let g = graph;
72
+ let plan = strategy.run({ graph: g }, options);
73
+ let routes = follow(g, plan, way);
74
+ const mean: number[] = [];
75
+ const moved: number[] = [];
76
+ const churn: number[] = [];
77
+ const ids: number[] = [];
78
+ for (let i = 0; i < edits; i++) {
79
+ const next = changeGraph(g, seed + i).graph;
80
+ const p = strategy.run({ graph: next, previous: { graph: g, plan } }, options);
81
+ const r = follow(next, p, way);
82
+ const s = routeStability(g, routes, r, { seed });
83
+ const edges = commonEdges(g, next);
84
+ mean.push(s.mean);
85
+ moved.push(s.moved);
86
+ churn.push(highwayChurn(plan, p, edges));
87
+ const keep = highwayIdKeep(plan, p, edges);
88
+ if (keep.matched > 0) ids.push(keep.share);
89
+ [g, plan, routes] = [next, p, r];
90
+ }
91
+ const avg = (v: readonly number[]): number => (v.length === 0 ? Number.NaN : v.reduce((a, b) => a + b, 0) / v.length);
92
+ return { mean: avg(mean), moved: avg(moved), churn: avg(churn), ids: avg(ids) };
93
+ }
@@ -7,3 +7,5 @@ export * from "./groups.ts";
7
7
  export * from "./stability.ts";
8
8
  export * from "./room.ts";
9
9
  export * from "./pareto.ts";
10
+ export * from "./order.ts";
11
+ export * from "./highways.ts";
@@ -0,0 +1,97 @@
1
+ import type { Graph, Result, Route } from "../../model.ts";
2
+ import { shuffleEdges } from "../../order/index.ts";
3
+ import { sameData } from "../run.ts";
4
+ import { inkSaved } from "./bundles.ts";
5
+ import { crossingAngles, detourSpread, drawnSegments } from "./routes.ts";
6
+ import { quantile } from "./stats.ts";
7
+
8
+ /** The order fragility of a router on one graph (W3 of MEAS3, from WILD-056). */
9
+ export interface OrderFragility {
10
+ /** The number of random orders. */
11
+ readonly orders: number;
12
+ /** `IQR / median` of the crossings (R1) over the orders. A value of 0 is a router that the order does not move. */
13
+ readonly crossings: number;
14
+ /** `IQR / median` of the median detour (R5) over the orders. */
15
+ readonly detour: number;
16
+ /** `IQR / median` of the ink saved (B1) over the orders. */
17
+ readonly inkSaved: number;
18
+ /** `IQR / median` of the ink over the orders. */
19
+ readonly ink: number;
20
+ /** The share of the orders whose routes differ from the routes of the input order. A value of 0 is a router that is free of the order. */
21
+ readonly changed: number;
22
+ /**
23
+ * The crossings of the input order divided by the fewest crossings over the orders (the price of anarchy of WILD-056).
24
+ * A value of 1 is a default order as good as the best random order. It is `NaN` when the best order has 0 crossings and the input order has more.
25
+ */
26
+ readonly poa: number;
27
+ }
28
+
29
+ /** The options of `orderFragility`. */
30
+ export interface OrderFragilityOptions {
31
+ /** The number of random orders of the edges. The default is 20. */
32
+ readonly orders?: number;
33
+ /** The seed of the first order. The order `k` has the seed `seed + k`. The default is 1. */
34
+ readonly seed?: number;
35
+ /** The result of the input order, when the caller has it. Without it, the function routes the input order one time more. */
36
+ readonly base?: Result;
37
+ }
38
+
39
+ /**
40
+ * This function gives `IQR / median` of the values. Values with a median of 0 give `IQR / mean`, and values that are all 0 give 0.
41
+ * An IQR below `1e-9` of the median is rounding noise of the sums, so it gives 0.
42
+ */
43
+ function spread(values: readonly number[]): number {
44
+ const iqr = quantile(values, 0.75) - quantile(values, 0.25);
45
+ const mid = Math.abs(quantile(values, 0.5));
46
+ if (iqr <= 1e-9 * Math.max(1, mid)) return 0;
47
+ if (mid > 0) return iqr / mid;
48
+ const mean = values.reduce((s, v) => s + Math.abs(v), 0) / values.length;
49
+ return mean > 0 ? iqr / mean : 0;
50
+ }
51
+
52
+ /** This function gives the routes by the id of the edge, in the order of the ids, so two orders of the input compare. */
53
+ function byEdge(routes: readonly Route[]): Route[] {
54
+ return [...routes].sort((a, b) => (a.edge < b.edge ? -1 : a.edge > b.edge ? 1 : 0));
55
+ }
56
+
57
+ /**
58
+ * This function measures the order fragility of a router on one graph (W3, from WILD-056).
59
+ * It routes the graph with `orders` random orders of the edges.
60
+ * It gives the spread `IQR / median` of the crossings, the detour, the ink saved and the ink.
61
+ * It also gives the share of orders that change a route, and the price of anarchy of the crossings.
62
+ * The cost is `orders` extra runs of the router.
63
+ */
64
+ export function orderFragility(graph: Graph, route: (graph: Graph) => Result, options: OrderFragilityOptions = {}): OrderFragility {
65
+ const orders = Math.max(1, Math.floor(options.orders ?? 20));
66
+ const seed = options.seed ?? 1;
67
+ const base = options.base ?? route(graph);
68
+ const baseRoutes = byEdge(base.routes);
69
+ const crossings: number[] = [];
70
+ const detour: number[] = [];
71
+ const saved: number[] = [];
72
+ const ink: number[] = [];
73
+ let changed = 0;
74
+ for (let k = 0; k < orders; k++) {
75
+ // The measures read the routes in the order of the edge ids, so the order of the output does not move them.
76
+ const routes = byEdge(route(shuffleEdges(graph, seed + k)).routes);
77
+ const segs = drawnSegments(routes);
78
+ crossings.push(crossingAngles(segs).crossings);
79
+ detour.push(detourSpread(routes).median);
80
+ saved.push(inkSaved(routes));
81
+ let length = 0;
82
+ for (const [a, b] of segs) length += Math.hypot(b.x - a.x, b.y - a.y);
83
+ ink.push(length);
84
+ if (!sameData(routes, baseRoutes)) changed++;
85
+ }
86
+ const own = crossingAngles(drawnSegments(baseRoutes)).crossings;
87
+ const best = Math.min(...crossings);
88
+ return {
89
+ orders,
90
+ crossings: spread(crossings),
91
+ detour: spread(detour),
92
+ inkSaved: spread(saved),
93
+ ink: spread(ink),
94
+ changed: changed / orders,
95
+ poa: best > 0 ? own / best : own === 0 ? 1 : Number.NaN,
96
+ };
97
+ }
package/src/bench/room.ts CHANGED
@@ -2,7 +2,7 @@ import type { Box } from "../geom.ts";
2
2
  import { levelLinks } from "../levels.ts";
3
3
  import type { Graph, LevelLink, Node, Result, Router } from "../model.ts";
4
4
  import { makeRoom, openGaps, roomLinks, type RoomOptions } from "../room/index.ts";
5
- import { gapLanes, LANE_ORDERS, type LaneOrder } from "../room/lanes.ts";
5
+ import { gapLaneCounts, gapLanes, LANE_ORDERS, LANE_TRACKS, type LaneOrder, type LaneTracks } from "../room/lanes.ts";
6
6
  import { levelPairs, levelRoom, type LevelRoomOptions } from "../room/levels/index.ts";
7
7
  import { buildSections, levelCut, treeDepth, type SectionOptions } from "../sections.ts";
8
8
  import { sectionsRouter } from "../routers/sections.ts";
@@ -70,14 +70,7 @@ export function measureBoard(graph: Graph, spacing: number, order?: LaneOrder):
70
70
  );
71
71
  const drawn = order ? gapLanes(graph.nodes, level, { spacing, order }) : level;
72
72
  const boxes = new Map(graph.nodes.map((n) => [n.id, n.box]));
73
- // `measureQuality` reads routes of edges, so each link of the far view stands for the first edge of its pair.
74
- const edgeOf = new Map<string, string>();
75
- for (const e of graph.edges) {
76
- const key = `${e.source.node}\u0000${e.target.node}`;
77
- if (!edgeOf.has(key)) edgeOf.set(key, e.id);
78
- }
79
- const asRoutes: Result = { routes: drawn.map((l) => ({ edge: edgeOf.get(`${l.source}\u0000${l.target}`)!, points: l.points })) };
80
- const q = measureQuality(graph, asRoutes);
73
+ const q = farQuality(graph, drawn);
81
74
  const routed = sectionsRouter({ name: "sections" }).route(graph);
82
75
  const segments = segmentsOf(routed);
83
76
  const open = new Set(segments.segments.filter((s) => s.count > 1).map((s) => runKey(s.edges)));
@@ -91,6 +84,26 @@ export function measureBoard(graph: Graph, spacing: number, order?: LaneOrder):
91
84
  };
92
85
  }
93
86
 
87
+ /** This function gives the links of the far view of a board: `levelLinks` with each node as one item. */
88
+ function farLinks(graph: Graph): LevelLink[] {
89
+ const items = graph.nodes.map((n) => ({ id: n.id, box: n.box, nodes: [n.id] }));
90
+ return levelLinks(
91
+ items,
92
+ roomLinks(graph).map((l) => ({ source: l.source, target: l.target, count: l.count ?? 1 })),
93
+ );
94
+ }
95
+
96
+ /** This function measures the links of the far view with `measureQuality`. Each link stands for the first edge of its pair, because `measureQuality` reads routes of edges. */
97
+ function farQuality(graph: Graph, drawn: readonly LevelLink[]): ReturnType<typeof measureQuality> {
98
+ const edgeOf = new Map<string, string>();
99
+ for (const e of graph.edges) {
100
+ const key = `${e.source.node}\u0000${e.target.node}`;
101
+ if (!edgeOf.has(key)) edgeOf.set(key, e.id);
102
+ }
103
+ const asRoutes: Result = { routes: drawn.map((l) => ({ edge: edgeOf.get(`${l.source}\u0000${l.target}`)!, points: l.points })) };
104
+ return measureQuality(graph, asRoutes);
105
+ }
106
+
94
107
  /**
95
108
  * This function measures a packed board, the board with each gap open to the gutter only, and the board that each room strategy opens.
96
109
  * The options are the same for each strategy, except `method`. The default strategies are the methods `routes` and `lines` of `roomDemand`.
@@ -395,3 +408,107 @@ export function formatBendRoom(rows: readonly BendRoomRow[]): string {
395
408
  );
396
409
  return [head, line, ...body].join("\n");
397
410
  }
411
+
412
+ /** One row of `benchTracks` (EDA-004): the lanes of the far view of one board with one lane rule of `gapLanes`. */
413
+ export interface TrackBenchRow {
414
+ readonly fixture: string;
415
+ /** The board: `given` (the board as it is) or `open` (the board that `makeRoom` opens at gutter 8 and spacing 4). */
416
+ readonly board: string;
417
+ readonly tracks: LaneTracks;
418
+ /** The number of gaps with at least one piece. */
419
+ readonly gaps: number;
420
+ /** The sum of the lanes of the gaps. */
421
+ readonly lanes: number;
422
+ /** The sum of the densities of the gaps: the most pieces that overlap at one place. No rule gives fewer lanes. */
423
+ readonly density: number;
424
+ /** The number of gaps whose lanes are nearer than `spacing`, because the gap is too narrow for its lanes. */
425
+ readonly squeezed: number;
426
+ /** The crossings of the links of the far view (`measureQuality`). */
427
+ readonly crossings: number;
428
+ /** The ink of the links of the far view. */
429
+ readonly ink: number;
430
+ /** R12: the length of parallel legs of two links nearer than `spacing`, as a share of the ink. */
431
+ readonly separation: number;
432
+ /** The number of links of the far view that go through a box. */
433
+ readonly through: number;
434
+ }
435
+
436
+ /**
437
+ * This function gives R12 (lane separation) of the links. Two legs of two links are near when they are parallel and nearer than `near`.
438
+ * R12 is the length of the near legs, as a share of the ink.
439
+ * It reads only the legs on an axis. The links of the far view have no other legs.
440
+ */
441
+ export function laneSeparation(links: readonly LevelLink[], near: number): number {
442
+ let ink = 0;
443
+ let close = 0;
444
+ for (const across of ["x", "y"] as const) {
445
+ const along = across === "x" ? "y" : "x";
446
+ const legs: { link: number; at: number; lo: number; hi: number }[] = [];
447
+ links.forEach((l, li) => {
448
+ for (let i = 1; i < l.points.length; i++) {
449
+ const a = l.points[i - 1]!;
450
+ const b = l.points[i]!;
451
+ if (Math.abs(a[across] - b[across]) > 1e-6 || Math.abs(a[along] - b[along]) <= 1e-6) continue;
452
+ legs.push({ link: li, at: a[across], lo: Math.min(a[along], b[along]), hi: Math.max(a[along], b[along]) });
453
+ }
454
+ });
455
+ legs.sort((p, q) => p.at - q.at);
456
+ for (let i = 0; i < legs.length; i++) {
457
+ const P = legs[i]!;
458
+ ink += P.hi - P.lo;
459
+ for (let j = i + 1; j < legs.length && legs[j]!.at - P.at < near; j++) {
460
+ const Q = legs[j]!;
461
+ if (Q.link !== P.link) close += Math.max(0, Math.min(P.hi, Q.hi) - Math.max(P.lo, Q.lo));
462
+ }
463
+ }
464
+ }
465
+ return ink > 0 ? close / ink : 0;
466
+ }
467
+
468
+ /**
469
+ * This function measures each lane rule of `gapLanes` (EDA-004) on the board as it is, and on the board that `makeRoom` opens.
470
+ * The lane order is `crossings` for each rule. The lanes are `spacing` apart where the gap has room.
471
+ */
472
+ export function benchTracks(fixture: string, graph: Graph, spacing: number = 4, rules: readonly LaneTracks[] = LANE_TRACKS): TrackBenchRow[] {
473
+ const open = makeRoom(graph.nodes, roomLinks(graph), { gutter: 8, spacing, method: "routes" }).items;
474
+ const rows: TrackBenchRow[] = [];
475
+ for (const [board, nodes] of [
476
+ ["given", graph.nodes],
477
+ ["open", open],
478
+ ] as const) {
479
+ const g: Graph = { nodes, edges: graph.edges };
480
+ const level = farLinks(g);
481
+ const boxes = new Map(nodes.map((n) => [n.id, n.box]));
482
+ for (const tracks of rules) {
483
+ const options = { spacing, order: "crossings" as const, tracks };
484
+ const counts = gapLaneCounts(nodes, level, options);
485
+ const drawn = gapLanes(nodes, level, options);
486
+ const q = farQuality(g, drawn);
487
+ rows.push({
488
+ fixture,
489
+ board,
490
+ tracks,
491
+ gaps: counts.length,
492
+ lanes: counts.reduce((s, c) => s + c.lanes, 0),
493
+ density: counts.reduce((s, c) => s + c.density, 0),
494
+ squeezed: counts.filter((c) => c.width / (c.lanes + 1) < spacing - 1e-9).length,
495
+ crossings: q.crossings,
496
+ ink: q.ink,
497
+ separation: laneSeparation(drawn, spacing),
498
+ through: measureLinks(boxes, drawn).through,
499
+ });
500
+ }
501
+ }
502
+ return rows;
503
+ }
504
+
505
+ /** This function writes the rows of `benchTracks` as a Markdown table. */
506
+ export function formatTracks(rows: readonly TrackBenchRow[]): string {
507
+ const head = "| fixture | board | tracks | gaps | lanes | density | squeezed | crossings | ink | R12 | through |";
508
+ const line = "|---|---|---|---:|---:|---:|---:|---:|---:|---:|---:|";
509
+ const body = rows.map(
510
+ (r) =>
511
+ `| ${r.fixture} | ${r.board} | ${r.tracks} | ${r.gaps} | ${r.lanes} | ${r.density} | ${r.squeezed} | ${r.crossings} | ${Math.round(r.ink)} | ${r.separation.toFixed(4)} | ${r.through} |`,
512
+ );
513
+ return [head, line, ...body].join("\n");
514
+ }
@@ -49,6 +49,18 @@ const ROWS: readonly Row[] = [
49
49
  // The bundles.
50
50
  ["B1.inkSaved", "B1", "route.bundle.ink-saved", "0 to 1", "higher", false],
51
51
  ["B4.ambiguity", "B4", "route.bundle.ambiguity", "0 to 1", "lower", false],
52
+ // B2, B3 and B6 score the shared ways of the routes for a fixed placement, as B1 does. B3 compares with the routes of `corners` on the same input.
53
+ ["B2.share", "B2", "route.bundle.share", "0 to 1", "higher", false],
54
+ ["B3.median", "B3", "route.bundle.stretch.median", "ratio", "lower", false],
55
+ ["B3.q95", "B3", "route.bundle.stretch.q95", "ratio", "lower", false],
56
+ ["B6.max", "B6", "route.bundle.merge-angle.max", "degrees", "lower", false],
57
+ // The order fragility (W3 of MEAS3, from WILD-056): the spread of a measure over random orders of the edges. The placement can move it.
58
+ ["W3.crossings", "W3", "route.order.crossings", "IQR over the median", "lower", true],
59
+ ["W3.detour", "W3", "route.order.detour", "IQR over the median", "lower", true],
60
+ ["W3.inkSaved", "W3", "route.order.ink-saved", "IQR over the median", "lower", true],
61
+ ["W3.ink", "W3", "route.order.ink", "IQR over the median", "lower", true],
62
+ ["W3.changed", "W3", "route.order.changed", "0 to 1", "lower", true],
63
+ ["W3.poa", "W3", "route.order.poa", "ratio", "lower", true],
52
64
  // The stability after the small change of `changeGraph`, and against the run on the base input of a variant.
53
65
  ["Z10.mean", "Z10", "route.stability.mean", "units of the board", "lower", false],
54
66
  ["Z10.max", "Z10", "route.stability.max", "units of the board", "lower", false],
@@ -56,6 +68,11 @@ const ROWS: readonly Row[] = [
56
68
  ["Z10.base.mean", "Z10", "route.stability.base.mean", "units of the board", "lower", false],
57
69
  ["Z10.base.max", "Z10", "route.stability.base.max", "units of the board", "lower", false],
58
70
  ["Z10.base.moved", "Z10", "route.stability.base.moved", "0 to 1", "lower", false],
71
+ // The stability of a highway strategy over a sequence of seeded edits, with its plan before each edit (the screen option `edits`).
72
+ ["Z10.edits.mean", "Z10", "route.stability.edits.mean", "units of the board", "lower", false],
73
+ ["Z10.edits.moved", "Z10", "route.stability.edits.moved", "0 to 1", "lower", false],
74
+ ["Z10.churn", "Z10", "route.stability.highway-churn", "0 to 1", "lower", false],
75
+ ["Z10.ids", "Z10", "route.stability.highway-ids", "0 to 1", "higher", false],
59
76
  // The zoom levels and the groups. The default grouping rule reads the positions, so most of these measures depend on the placement.
60
77
  ["level-nesting", "Z6", "route.level.nesting", "count", "lower", false],
61
78
  ["Z3.through", "Z3", "route.level.through", "count", "lower", true],