@mark1russell7/highways 0.3.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 +66 -2
- package/build/index.d.ts +1009 -12
- package/build/index.d.ts.map +1 -1
- package/build/index.js +4408 -195
- package/build/index.js.map +1 -1
- package/package.json +4 -1
- package/src/bench/compare.ts +41 -6
- 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/forms.ts +372 -0
- package/src/bench/screen/generators.ts +54 -0
- package/src/bench/screen/hash.ts +129 -0
- package/src/bench/screen/index.ts +6 -0
- package/src/bench/screen/measures.ts +126 -0
- package/src/bench/screen/screen.ts +454 -0
- package/src/bench/screen/set.ts +136 -0
- package/src/bench/suite.ts +71 -13
- package/src/highways/corridors/index.ts +305 -0
- package/src/highways/follow.ts +155 -0
- package/src/highways/index.ts +7 -0
- package/src/highways/keys/index.ts +15 -0
- package/src/highways/model.ts +111 -0
- package/src/highways/result.ts +34 -0
- package/src/highways/router.ts +40 -0
- package/src/highways/shared-ways/index.ts +15 -0
- package/src/index.ts +4 -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/builtins.ts +82 -6
- package/src/strategies/model.ts +36 -6
- package/src/strategies/schemas.ts +66 -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.
|
|
12
|
+
"version": "0.4.1",
|
|
13
13
|
"type": "module",
|
|
14
14
|
"main": "./build/index.js",
|
|
15
15
|
"types": "./build/index.d.ts",
|
|
@@ -38,9 +38,12 @@
|
|
|
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",
|
|
45
|
+
"screen": "node scripts/screen.ts",
|
|
46
|
+
"screen:inputs": "node scripts/screen-inputs.ts",
|
|
44
47
|
"test:watch": "vitest",
|
|
45
48
|
"typecheck": "tsc -p tsconfig.json && tsc -p tsconfig.test.json"
|
|
46
49
|
}
|
package/src/bench/compare.ts
CHANGED
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
import type { CurveSegment } from "../curves/model.ts";
|
|
2
|
+
import { polylineLength } from "../geom.ts";
|
|
2
3
|
import type { Graph, Result } from "../model.ts";
|
|
3
4
|
import type { Section } from "../sections.ts";
|
|
5
|
+
import { runKey, segmentsOf } from "../segments.ts";
|
|
4
6
|
import { STRATEGIES } from "../strategies/builtins.ts";
|
|
5
|
-
import { asRouter, getStrategy, listStrategies, type Registry, type Step } from "../strategies/model.ts";
|
|
7
|
+
import { asRouter, getStrategy, listStrategies, type LaneBoard, type Registry, type Step } from "../strategies/model.ts";
|
|
6
8
|
import { peakCurvature } from "./curves.ts";
|
|
9
|
+
import { measureLevelFaults, sumFaults } from "./links.ts";
|
|
7
10
|
import { paretoFront, type Objective } from "./measures/pareto.ts";
|
|
8
11
|
import { boardUnit } from "./measures/routes.ts";
|
|
9
12
|
import { changeGraph, routeStability } from "./measures/stability.ts";
|
|
@@ -20,8 +23,30 @@ function medianOf(values: readonly number[]): number {
|
|
|
20
23
|
return a === b ? a : (a + b) / 2;
|
|
21
24
|
}
|
|
22
25
|
|
|
23
|
-
/**
|
|
24
|
-
|
|
26
|
+
/**
|
|
27
|
+
* This function gives the measures of the level links of a result: the links through a box (`Z3.through`) and the length of all links (`Z3.length`).
|
|
28
|
+
* A result without a level with groups gives no measure.
|
|
29
|
+
*/
|
|
30
|
+
function levelLinkMeasures(result: Result): Record<string, number> {
|
|
31
|
+
const levels = (result.levels ?? []).filter((l) => l.items.some((i) => i.nodes.length > 1));
|
|
32
|
+
if (levels.length === 0) return {};
|
|
33
|
+
const length = levels.reduce((sum, l) => sum + l.links.reduce((s, k) => s + polylineLength(k.points), 0), 0);
|
|
34
|
+
return { "Z3.through": sumFaults(measureLevelFaults(result)).through, "Z3.length": length };
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** This function gives the lanes of a result with each shared segment open. A result without shared segments gives `undefined`. */
|
|
38
|
+
function laneBoard(result: Result, lanes: { readonly width: number; readonly maxWidth?: number }): LaneBoard | undefined {
|
|
39
|
+
const graph = segmentsOf(result);
|
|
40
|
+
const open = new Set(graph.segments.filter((s) => s.count > 1).map((s) => runKey(s.edges)));
|
|
41
|
+
if (open.size === 0) return undefined;
|
|
42
|
+
return { graph, open, width: lanes.width, ...(lanes.maxWidth === undefined ? {} : { maxWidth: lanes.maxWidth }) };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* The steps that `compareStep` compares. The step `room` takes a packed board, not a graph, so it is not in the list.
|
|
47
|
+
* The step `highway` gives a plan, not routes. The screen routes each plan with `follow` and compares it as a router.
|
|
48
|
+
*/
|
|
49
|
+
export type CompareStep = Exclude<Step, "room" | "highway">;
|
|
25
50
|
|
|
26
51
|
/** The steps that `compareStep` compares, in the order of the pipeline. */
|
|
27
52
|
export const COMPARE_STEPS: readonly CompareStep[] = ["group", "router", "curve"];
|
|
@@ -34,6 +59,11 @@ export interface CompareOptions {
|
|
|
34
59
|
readonly options?: Readonly<Record<string, object>>;
|
|
35
60
|
/** The router whose routes the step `curve` draws. The default is `corners`, as in `runSuite`. */
|
|
36
61
|
readonly curveRouter?: string;
|
|
62
|
+
/**
|
|
63
|
+
* With this value, the step `curve` opens each shared segment of the routes of `curveRouter`, and each curve draws the lanes of `laneRoutes`.
|
|
64
|
+
* So the curve `highway` bends the lanes of a bundle as one. Without it, each curve draws the routes. A router without shared segments gives the routes in both cases.
|
|
65
|
+
*/
|
|
66
|
+
readonly lanes?: { readonly width: number; readonly maxWidth?: number };
|
|
37
67
|
/** The seed of the small change of the graph for the stability measures (Z10 and G9). The default is 1. */
|
|
38
68
|
readonly seed?: number;
|
|
39
69
|
/** The route stability (Z10) routes a changed graph again, so the function measures it only up to this number of nodes. The default is 1000. */
|
|
@@ -50,7 +80,10 @@ export interface CompareRow {
|
|
|
50
80
|
readonly violations: number;
|
|
51
81
|
/** The time of the step, in milliseconds. For the step `router`, it is the median of two routes. */
|
|
52
82
|
readonly ms: number;
|
|
53
|
-
/**
|
|
83
|
+
/**
|
|
84
|
+
* The measures, by the keys of `runSuite`, and `ms`. The step `router` adds `ink`, and the step `curve` adds `C5.peak`.
|
|
85
|
+
* A router with zoom levels also gets `Z3.through` and `Z3.length`, the measures of its level links.
|
|
86
|
+
*/
|
|
54
87
|
readonly values: Readonly<Record<string, number>>;
|
|
55
88
|
/** The reason when the strategy did not run, for example a graph over its `maxNodes`. */
|
|
56
89
|
readonly skipped?: string;
|
|
@@ -79,7 +112,7 @@ export interface StepComparison {
|
|
|
79
112
|
|
|
80
113
|
/**
|
|
81
114
|
* This function runs each strategy of one step on one graph and measures it with the measures of `runSuite`. Then it gives the Pareto front.
|
|
82
|
-
* The step `router` routes the graph with each router. The step `curve` draws the routes of `curveRouter` with each curve
|
|
115
|
+
* The step `router` routes the graph with each router. The step `curve` draws the routes of `curveRouter` with each curve, or their lanes with the option `lanes`.
|
|
83
116
|
* The step `group` routes the graph with `sections` on the tree of each grouping rule.
|
|
84
117
|
*
|
|
85
118
|
* The same graph and the same options give the same measures. Only the times change.
|
|
@@ -98,7 +131,7 @@ export function compareStep(step: CompareStep, graph: Graph, options: CompareOpt
|
|
|
98
131
|
if (graph.nodes.length > (strategy.maxNodes ?? Infinity)) return { step, strategy: strategy.name, violations: 0, ms: 0, values: {}, skipped: `over ${strategy.maxNodes} nodes` };
|
|
99
132
|
const router = asRouter(strategy, own[strategy.name]);
|
|
100
133
|
const { row, result } = benchRun(router, name, graph, 2);
|
|
101
|
-
const values: Record<string, number> = { ...routeMeasures(graph, result, row), ink: row.quality.ink, ms: row.ms };
|
|
134
|
+
const values: Record<string, number> = { ...routeMeasures(graph, result, row), ...levelLinkMeasures(result), ink: row.quality.ink, ms: row.ms };
|
|
102
135
|
if (change) {
|
|
103
136
|
const s = routeStability(graph, result, router.route(change.graph), { seed });
|
|
104
137
|
values["Z10.mean"] = s.mean;
|
|
@@ -111,11 +144,13 @@ export function compareStep(step: CompareStep, graph: Graph, options: CompareOpt
|
|
|
111
144
|
const router = asRouter(getStrategy(registry, "router", options.curveRouter ?? "corners"));
|
|
112
145
|
const result = router.route(graph);
|
|
113
146
|
const u = boardUnit(graph);
|
|
147
|
+
const lanes = options.lanes && laneBoard(result, options.lanes);
|
|
114
148
|
const runs = curveRuns(graph, result, listStrategies(registry, "curve"), {
|
|
115
149
|
...at,
|
|
116
150
|
smooth: new Set(["rounded", "arc", "spline", "smooth", "clothoid", "highway"]),
|
|
117
151
|
normals: new Set(["spline"]),
|
|
118
152
|
curveOptions: own,
|
|
153
|
+
...(lanes ? { lanes } : {}),
|
|
119
154
|
});
|
|
120
155
|
rows = runs.map(({ row, drawn, edges }): CompareRow => {
|
|
121
156
|
const peaks = drawn.map((d) => peakCurvature(d) * u);
|
|
@@ -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
|
+
}
|