@mark1russell7/highways 0.4.0 → 0.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +40 -0
- package/build/index.d.ts +313 -2
- package/build/index.d.ts.map +1 -1
- package/build/index.js +2256 -341
- package/build/index.js.map +1 -1
- package/package.json +2 -1
- package/src/bench/measures/bundles.ts +101 -2
- package/src/bench/measures/index.ts +1 -0
- package/src/bench/measures/order.ts +97 -0
- package/src/bench/room.ts +126 -9
- package/src/bench/screen/measures.ts +12 -0
- package/src/bench/screen/screen.ts +73 -9
- package/src/index.ts +2 -0
- package/src/order/index.ts +65 -0
- package/src/plugs/justify.ts +48 -0
- package/src/plugs/side.ts +138 -0
- package/src/repair/index.ts +377 -0
- package/src/repair/walk.ts +220 -0
- package/src/room/lanes.ts +109 -10
- package/src/routers/corners.ts +73 -6
- package/src/routers/fan.ts +145 -0
- package/src/routers/index.ts +2 -2
- package/src/routers/shared.ts +107 -8
- package/src/routers/snell.ts +140 -0
- package/src/routers/trunks.ts +43 -2
- package/src/strategies/schemas.ts +53 -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.
|
|
12
|
+
"version": "0.4.1",
|
|
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,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
|
-
|
|
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],
|
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
import { highwayRouterStrategy } from "../../highways/router.ts";
|
|
2
|
-
import type { Graph, Result } from "../../model.ts";
|
|
2
|
+
import type { Graph, Result, Route } from "../../model.ts";
|
|
3
|
+
import { stableOrderStrategy } from "../../order/index.ts";
|
|
4
|
+
import { repairStrategy } from "../../repair/index.ts";
|
|
3
5
|
import { STRATEGIES } from "../../strategies/builtins.ts";
|
|
4
|
-
import { createRegistry, getStrategy, listStrategies, STEPS, type AnyStrategy, type Registry, type Strategy } from "../../strategies/model.ts";
|
|
6
|
+
import { asRouter, createRegistry, getStrategy, listStrategies, register, STEPS, type AnyStrategy, type Registry, type Strategy } from "../../strategies/model.ts";
|
|
5
7
|
import { compareStep, type CompareRow, type CompareStep, type StepComparison } from "../compare.ts";
|
|
8
|
+
import { bundledShare, bundleStretch, mergeAngle } from "../measures/bundles.ts";
|
|
9
|
+
import { orderFragility } from "../measures/order.ts";
|
|
6
10
|
import { paretoFront, type Objective } from "../measures/pareto.ts";
|
|
7
11
|
import { routeStability } from "../measures/stability.ts";
|
|
8
12
|
import { quantile } from "../measures/stats.ts";
|
|
@@ -26,6 +30,15 @@ export const SCREEN_STEPS: readonly ScreenStep[] = ["group", "router", "highway"
|
|
|
26
30
|
/** The routers that the screen of the step `highway` also runs by default. They are the baselines of the note on highways. */
|
|
27
31
|
export const HIGHWAY_BASE_ROUTERS: readonly string[] = ["corners", "shared", "trunks"];
|
|
28
32
|
|
|
33
|
+
/**
|
|
34
|
+
* A wrapper of each router of a screen. `stable` routes the edges in the stable order of WILD-053 (`stableOrderStrategy`).
|
|
35
|
+
* `repair` runs the repair step of RULE-046 after the router (`repairStrategy`).
|
|
36
|
+
*/
|
|
37
|
+
export type ScreenWrap = "stable" | "repair";
|
|
38
|
+
|
|
39
|
+
/** The wrappers of `ScreenWrap`. */
|
|
40
|
+
export const SCREEN_WRAPS: readonly ScreenWrap[] = ["stable", "repair"];
|
|
41
|
+
|
|
29
42
|
/** The hard rules of the routes of a router, or of a plan after `follow`. */
|
|
30
43
|
const ROUTE_RULES: readonly string[] = ["complete", "ends-at-plugs", "fixed-plugs", "no-pass-through", "counts-sum", "link-ends", "bundle-trunk", "run-shared", "same-twice"];
|
|
31
44
|
|
|
@@ -59,8 +72,18 @@ export interface ScreenOptions {
|
|
|
59
72
|
* The default is `HIGHWAY_BASE_ROUTERS`. The other steps do not read it.
|
|
60
73
|
*/
|
|
61
74
|
readonly baseRouters?: readonly string[];
|
|
62
|
-
/** The seed of the small change of the stability measures
|
|
75
|
+
/** The seed of the small change of the stability measures, and of the first random order of `shuffles`. The default is 1. */
|
|
63
76
|
readonly seed?: number;
|
|
77
|
+
/**
|
|
78
|
+
* The wrappers of each router of the steps `router` and `highway`. Each wrapper `w` adds the router `<name>+<w>` as a new strategy, beside the router `<name>`.
|
|
79
|
+
* The default is none. The steps `group` and `curve` do not read it.
|
|
80
|
+
*/
|
|
81
|
+
readonly wrap?: readonly ScreenWrap[];
|
|
82
|
+
/**
|
|
83
|
+
* The number of random orders of the edges for the order fragility (W3, from WILD-056). Each router routes each input this number of times more.
|
|
84
|
+
* The default is 0: no measure of the order fragility. The steps `group` and `curve` do not read it.
|
|
85
|
+
*/
|
|
86
|
+
readonly shuffles?: number;
|
|
64
87
|
/** The commit of the code, for the records. */
|
|
65
88
|
readonly commit?: string;
|
|
66
89
|
/** The predictions of the run. Each record names the first prediction with its strategy, its set and its input. */
|
|
@@ -175,8 +198,11 @@ function screenRegistry(step: ScreenStep, strategies: readonly (Strategy<ScreenS
|
|
|
175
198
|
/** This function tells if a step of the screen gives routes. The step `router` gives routes, and the step `highway` gives routes after `follow`. */
|
|
176
199
|
const givesRoutes = (step: ScreenStep): step is "router" | "highway" => step === "router" || step === "highway";
|
|
177
200
|
|
|
178
|
-
/**
|
|
179
|
-
function
|
|
201
|
+
/**
|
|
202
|
+
* This function gives the values of a row of `compareStep`, with the measures of `measureQuality` for a router.
|
|
203
|
+
* A router also gets the bundled share (B2), the merge angle (B6) and the bundle stretch (B3) against the routes `base` of `corners`.
|
|
204
|
+
*/
|
|
205
|
+
function rowValues(step: ScreenStep, graph: Graph, row: CompareRow, base: () => readonly Route[]): Record<string, number> {
|
|
180
206
|
const values: Record<string, number> = { ...row.values };
|
|
181
207
|
delete values["ms"];
|
|
182
208
|
if (givesRoutes(step) && row.result) {
|
|
@@ -186,6 +212,11 @@ function rowValues(step: ScreenStep, graph: Graph, row: CompareRow): Record<stri
|
|
|
186
212
|
values["R8.tight"] = q.tight;
|
|
187
213
|
values["R10.plugAngle"] = q.plugAngle;
|
|
188
214
|
values["R15.inflections"] = q.inflections;
|
|
215
|
+
values["B2.share"] = bundledShare(row.result);
|
|
216
|
+
const st = bundleStretch(row.result.routes, base());
|
|
217
|
+
values["B3.median"] = st.median;
|
|
218
|
+
values["B3.q95"] = st.q95;
|
|
219
|
+
values["B6.max"] = mergeAngle(row.result.routes);
|
|
189
220
|
}
|
|
190
221
|
return values;
|
|
191
222
|
}
|
|
@@ -208,6 +239,20 @@ function rowRules(step: ScreenStep, graph: Graph, row: CompareRow, again: Compar
|
|
|
208
239
|
return { hard: { ...countRules(rules, []), "same-twice": deterministic ? 0 : 1 }, deterministic };
|
|
209
240
|
}
|
|
210
241
|
|
|
242
|
+
/** This function adds the router `<name>+<w>` for each router of the registry and each wrapper `w`. It gives the new registry and the new names. */
|
|
243
|
+
function wrapRouters(registry: Registry, wraps: readonly ScreenWrap[]): { registry: Registry; added: string[] } {
|
|
244
|
+
const routers = listStrategies(registry, "router");
|
|
245
|
+
const added: AnyStrategy[] = [];
|
|
246
|
+
for (const w of wraps) for (const s of routers) added.push((w === "stable" ? stableOrderStrategy(s) : repairStrategy(s)) as AnyStrategy);
|
|
247
|
+
return { registry: added.length === 0 ? registry : register(registry, ...added), added: added.map((s) => s.name) };
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/** This function gives the name of a router without the suffix of a wrapper, for example `shared` for `shared+repair`. */
|
|
251
|
+
export function unwrappedName(name: string): string {
|
|
252
|
+
for (const w of SCREEN_WRAPS) if (name.endsWith(`+${w}`)) return name.slice(0, -(w.length + 1));
|
|
253
|
+
return name;
|
|
254
|
+
}
|
|
255
|
+
|
|
211
256
|
/** This function keeps the finite values and gives them the names of a record. */
|
|
212
257
|
function named(values: Readonly<Record<string, number>>): Record<string, number> {
|
|
213
258
|
const out: Record<string, number> = {};
|
|
@@ -232,14 +277,23 @@ export function screen(step: ScreenStep, strategies: readonly (Strategy<ScreenSt
|
|
|
232
277
|
const candidates = screened.candidates;
|
|
233
278
|
// The step `highway` runs as the step `router`. Each highway strategy with `follow` is a router, after the base routers.
|
|
234
279
|
const base = step === "highway" ? (options.baseRouters ?? HIGHWAY_BASE_ROUTERS).map((name) => getStrategy(STRATEGIES, "router", name)) : [];
|
|
235
|
-
const
|
|
280
|
+
const unwrapped =
|
|
236
281
|
step === "highway"
|
|
237
282
|
? createRegistry([...base, ...listStrategies(screened.registry, "highway").map((s) => highwayRouterStrategy(s))] as readonly AnyStrategy[])
|
|
238
283
|
: screened.registry;
|
|
284
|
+
const wrapped = givesRoutes(step) ? wrapRouters(unwrapped, options.wrap ?? []) : { registry: unwrapped, added: [] };
|
|
285
|
+
const registry = wrapped.registry;
|
|
286
|
+
for (const name of wrapped.added) candidates.add(name);
|
|
239
287
|
const compareAs: CompareStep = step === "highway" ? "router" : step;
|
|
240
288
|
const baseNames = new Set(base.map((s) => s.name));
|
|
241
|
-
const stepOf = (name: string): ScreenStep => (baseNames.has(name) ? "router" : step);
|
|
242
|
-
|
|
289
|
+
const stepOf = (name: string): ScreenStep => (baseNames.has(unwrappedName(name)) ? "router" : step);
|
|
290
|
+
// A wrapped router gets the options of its router, unless the call gives its own.
|
|
291
|
+
const own: Record<string, object> = { ...options.options };
|
|
292
|
+
for (const name of wrapped.added) {
|
|
293
|
+
const from = options.options?.[unwrappedName(name)];
|
|
294
|
+
if (own[name] === undefined && from !== undefined) own[name] = from;
|
|
295
|
+
}
|
|
296
|
+
const shuffles = givesRoutes(step) ? Math.max(0, Math.floor(options.shuffles ?? 0)) : 0;
|
|
243
297
|
const seed = options.seed ?? 1;
|
|
244
298
|
const log = options.log ?? ((): void => {});
|
|
245
299
|
const families = options.families;
|
|
@@ -259,6 +313,10 @@ export function screen(step: ScreenStep, strategies: readonly (Strategy<ScreenSt
|
|
|
259
313
|
comparisons.push(cmp);
|
|
260
314
|
log(`${input.id}: ${graph.nodes.length} nodes, ${graph.edges.length} edges, ${cmp.rows.map((r) => `${r.strategy} ${r.ms.toFixed(0)}`).join(", ")}`);
|
|
261
315
|
if (bases.has(input.id)) kept.set(input.id, { graph, results: new Map(cmp.rows.flatMap((r) => (r.result ? [[r.strategy, r.result] as const] : []))) });
|
|
316
|
+
// The routes of `corners` on the input: the base of the bundle stretch (B3). The row of `corners` gives them, or one more run.
|
|
317
|
+
let cornersRoutes: readonly Route[] | undefined;
|
|
318
|
+
const cornersBase = (): readonly Route[] =>
|
|
319
|
+
(cornersRoutes ??= (cmp.rows.find((r) => r.strategy === "corners" && r.result)?.result ?? asRouter(getStrategy(STRATEGIES, "router", "corners")).route(graph)).routes);
|
|
262
320
|
cmp.rows.forEach((row, i) => {
|
|
263
321
|
const strategy = byName.get(row.strategy)!;
|
|
264
322
|
const head = {
|
|
@@ -282,7 +340,13 @@ export function screen(step: ScreenStep, strategies: readonly (Strategy<ScreenSt
|
|
|
282
340
|
return;
|
|
283
341
|
}
|
|
284
342
|
const { hard, deterministic } = rowRules(step, graph, row, again?.rows[i]);
|
|
285
|
-
const
|
|
343
|
+
const values = rowValues(step, graph, row, cornersBase);
|
|
344
|
+
if (shuffles > 0 && row.result) {
|
|
345
|
+
const router = asRouter(strategy as Strategy<"router">, own[row.strategy]);
|
|
346
|
+
const f = orderFragility(graph, (g) => router.route(g), { orders: shuffles, seed, base: row.result });
|
|
347
|
+
Object.assign(values, { "W3.crossings": f.crossings, "W3.detour": f.detour, "W3.inkSaved": f.inkSaved, "W3.ink": f.ink, "W3.changed": f.changed, "W3.poa": f.poa });
|
|
348
|
+
}
|
|
349
|
+
const record = { ...head, measures: named(values), hard, tolerance: { through: TOLERANCE }, deterministic, ms: row.ms, ...tail };
|
|
286
350
|
records.push(record);
|
|
287
351
|
if (givesRoutes(step) && input.base !== undefined && row.result) variants.push({ record, base: input.base, result: row.result });
|
|
288
352
|
});
|
package/src/index.ts
CHANGED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { fnv1a32 } from "../bench/screen/hash.ts";
|
|
2
|
+
import { rng } from "../fixtures.ts";
|
|
3
|
+
import type { Edge, Graph, Result, Router } from "../model.ts";
|
|
4
|
+
import { defineStrategy, type Strategy } from "../strategies/model.ts";
|
|
5
|
+
|
|
6
|
+
/** This function compares two edges by the hash of their ids, and then by their ids. Thus the order of two edges does not depend on the input. */
|
|
7
|
+
function byHash(a: { readonly h: number; readonly e: Edge }, b: { readonly h: number; readonly e: Edge }): number {
|
|
8
|
+
if (a.h !== b.h) return a.h - b.h;
|
|
9
|
+
return a.e.id < b.e.id ? -1 : a.e.id > b.e.id ? 1 : 0;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* This function gives the graph with its edges in the stable order of WILD-053: by the FNV-1a hash of the id, then by the id.
|
|
14
|
+
* The nodes and the edges do not change. Two orders of the same edges give the same graph.
|
|
15
|
+
* The hash spreads the edges, so the stable order does not favor the edges of one node or one collection.
|
|
16
|
+
*/
|
|
17
|
+
export function stableOrder(graph: Graph): Graph {
|
|
18
|
+
const edges = graph.edges.map((e) => ({ h: fnv1a32(e.id), e })).sort(byHash).map((x) => x.e);
|
|
19
|
+
return { ...graph, edges };
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** This function gives the graph with its edges in a random order of the seed. The same graph and seed always give the same order. */
|
|
23
|
+
export function shuffleEdges(graph: Graph, seed: number): Graph {
|
|
24
|
+
const edges = [...graph.edges];
|
|
25
|
+
const r = rng(seed);
|
|
26
|
+
for (let i = edges.length - 1; i > 0; i--) {
|
|
27
|
+
const j = Math.floor(r() * (i + 1));
|
|
28
|
+
[edges[i], edges[j]] = [edges[j]!, edges[i]!];
|
|
29
|
+
}
|
|
30
|
+
return { ...graph, edges };
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* This function routes the graph in the stable order of `stableOrder`, with the function `route`.
|
|
35
|
+
* The routes come back in the order of the input edges, so the caller sees no change of order.
|
|
36
|
+
* The bundles, the runs and the levels come from the stable order. Thus each order of the input gives the same data.
|
|
37
|
+
*/
|
|
38
|
+
export function routeInStableOrder(graph: Graph, route: (graph: Graph) => Result): Result {
|
|
39
|
+
const result = route(stableOrder(graph));
|
|
40
|
+
const index = new Map(graph.edges.map((e, i) => [e.id, i] as const));
|
|
41
|
+
const at = (edge: string): number => index.get(edge) ?? Number.MAX_SAFE_INTEGER;
|
|
42
|
+
const routes = [...result.routes].sort((a, b) => at(a.edge) - at(b.edge));
|
|
43
|
+
return { ...result, routes };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** This function gives a router that routes each graph in the stable order of WILD-053. It keeps the name and the `maxNodes` of the router. */
|
|
47
|
+
export function withStableOrder(router: Router): Router {
|
|
48
|
+
return { ...router, route: (graph: Graph): Result => routeInStableOrder(graph, (g) => router.route(g)) };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* This function gives a router strategy that runs `strategy` in the stable order of WILD-053. The output does not depend on the order of the input edges.
|
|
53
|
+
* The new strategy has the name `<name>+stable`, and the options and the schema of `strategy`. The strategy `strategy` does not change.
|
|
54
|
+
*/
|
|
55
|
+
export function stableOrderStrategy<O extends object>(strategy: Strategy<"router", O>): Strategy<"router", O> {
|
|
56
|
+
return defineStrategy<"router", O>({
|
|
57
|
+
step: "router",
|
|
58
|
+
name: `${strategy.name}+stable`,
|
|
59
|
+
describe: `${strategy.describe} The edges go in a stable order: by a hash of their ids.`,
|
|
60
|
+
defaults: { ...strategy.defaults } as O,
|
|
61
|
+
...(strategy.schema === undefined ? {} : { schema: strategy.schema }),
|
|
62
|
+
...(strategy.maxNodes === undefined ? {} : { maxNodes: strategy.maxNodes }),
|
|
63
|
+
run: (graph: Graph, options: Readonly<O>): Result => routeInStableOrder(graph, (g) => strategy.run(g, options)),
|
|
64
|
+
});
|
|
65
|
+
}
|