@mark1russell7/highways 0.8.1 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +74 -8
- package/build/index.d.ts +705 -183
- package/build/index.d.ts.map +1 -1
- package/build/index.js +4079 -2639
- package/build/index.js.map +1 -1
- package/package.json +3 -2
- package/src/bench/compare.ts +174 -5
- package/src/bench/room.ts +1 -1
- package/src/bench/run.ts +38 -3
- package/src/bench/screen/generators.ts +11 -41
- package/src/bench/screen/screen.ts +16 -10
- package/src/bench/screen/set.ts +156 -49
- package/src/bench/suite.ts +110 -12
- package/src/cases.ts +64 -18
- package/src/families/cases.ts +108 -0
- package/src/families/fixtures.ts +101 -0
- package/src/families/index.ts +36 -0
- package/src/families/model.ts +55 -0
- package/src/fixtures.ts +8 -2
- package/src/highways/soft-keys/index.ts +8 -4
- package/src/index.ts +8 -0
- package/src/lane-order.ts +37 -0
- package/src/lane-orders/stretch/index.ts +486 -0
- package/src/lane-orders/stretch/router.ts +22 -0
- package/src/levels/depths/index.ts +57 -0
- package/src/plugs/facing/index.ts +22 -0
- package/src/portfolio/router.ts +44 -0
- package/src/routers/corners.ts +27 -3
- package/src/routers/sections.ts +22 -40
- package/src/routers/shared.ts +26 -7
- package/src/routers/trunks.ts +21 -3
- package/src/segments.ts +4 -25
- package/src/strategies/builtins.ts +54 -2
- package/src/strategies/model.ts +55 -10
- package/src/strategies/options.ts +36 -4
- package/src/strategies/schemas.ts +68 -5
- package/src/whole/pipeline/index.ts +66 -0
|
@@ -0,0 +1,486 @@
|
|
|
1
|
+
// One lane order for each stretch (LANE-STRETCH). The runs of the router `shared` that follow each other on the routes make a stretch.
|
|
2
|
+
// The function `stretchLaneOrders` gives each run of a stretch the lane order of one total order of the edges of the stretch.
|
|
3
|
+
// Thus two neighbour runs cannot break a cycle of the lane relation in two ways, and their lanes do not cross between them.
|
|
4
|
+
import { byText } from "../../compare.ts";
|
|
5
|
+
import type { Point } from "../../geom.ts";
|
|
6
|
+
import { byBundle, byEdge, mergeSort, placeRoutes, type PlacedRoute } from "../../lane-order.ts";
|
|
7
|
+
import type { Route } from "../../model.ts";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* The lane order of the stretches (LANE-STRETCH).
|
|
11
|
+
*
|
|
12
|
+
* - `none`: each run gets its own order, as the order `bundle` of `orderLanes` gives it.
|
|
13
|
+
* - `consensus`: one order of the pair relation of the stretch, from the heuristic of Eades, Lin and Smyth for the feedback arc set.
|
|
14
|
+
* - `periphery`: the largest run gives its order. Each next run keeps that order and puts each new edge at the place with the fewest broken pairs.
|
|
15
|
+
* - `sifting`: the order of `consensus`. Then each edge moves to the place with the fewest broken pairs, for `passes` passes.
|
|
16
|
+
*/
|
|
17
|
+
export type LaneStretchOrder = "none" | "consensus" | "periphery" | "sifting";
|
|
18
|
+
|
|
19
|
+
/** The values of `LaneStretchOrder`, in the order of a control. */
|
|
20
|
+
export const LANE_STRETCH_ORDERS: readonly LaneStretchOrder[] = Object.freeze(["none", "consensus", "periphery", "sifting"]);
|
|
21
|
+
|
|
22
|
+
/** The options of `stretchLaneOrders`. */
|
|
23
|
+
export interface LaneStretchOptions {
|
|
24
|
+
/** The lane order of the stretches. The default is `sifting`. */
|
|
25
|
+
readonly method: LaneStretchOrder;
|
|
26
|
+
/** The number of passes of the method `sifting`, from 1 to 4. The default is 2. The other methods do not read it. */
|
|
27
|
+
readonly passes: number;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** The default options of `stretchLaneOrders`. */
|
|
31
|
+
export const LANE_STRETCH_DEFAULTS: Readonly<LaneStretchOptions> = Object.freeze({ method: "sifting", passes: 2 });
|
|
32
|
+
|
|
33
|
+
/** One run: its points and the routes on it. Each route holds the points of the run in a row, in either direction. */
|
|
34
|
+
export interface LaneRun {
|
|
35
|
+
readonly points: readonly Point[];
|
|
36
|
+
readonly routes: readonly Route[];
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** One stretch: the runs that follow each other on the routes, and the order of the edges of the stretch. */
|
|
40
|
+
export interface LaneStretch {
|
|
41
|
+
/** The indexes of the runs in the list of the input. The first run is the root: the run with the most lanes. The others follow in the order of a breadth-first walk. */
|
|
42
|
+
readonly runs: readonly number[];
|
|
43
|
+
/** The way of each run of `runs` against the root. The value -1 tells that the left of the root is the right of the run. */
|
|
44
|
+
readonly turns: readonly (1 | -1)[];
|
|
45
|
+
/** The edges of the stretch from left to right, in the way of the root. A stretch of one run has the order `bundle` of the run. */
|
|
46
|
+
readonly order: readonly string[];
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** A run with its placed routes. */
|
|
50
|
+
interface Prepared {
|
|
51
|
+
readonly placed: readonly PlacedRoute[];
|
|
52
|
+
readonly lost: readonly string[];
|
|
53
|
+
/** The placed route of each edge. */
|
|
54
|
+
readonly byEdge: ReadonlyMap<string, PlacedRoute>;
|
|
55
|
+
/** The key of the run: its sorted edges, then its points. Two runs with one key give the same order. */
|
|
56
|
+
readonly key: string;
|
|
57
|
+
/** This value is true when an edge goes two times through the run. Then the run is a stretch alone, with the order `bundle`. */
|
|
58
|
+
readonly twice: boolean;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** A link between two runs that follow each other on a route. `turn` is -1 when the route goes through one of them in reverse. */
|
|
62
|
+
interface Link {
|
|
63
|
+
readonly to: number;
|
|
64
|
+
readonly turn: 1 | -1;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** This function places the routes of each run, and gives the key of each run. */
|
|
68
|
+
function prepare(runs: readonly LaneRun[]): Prepared[] {
|
|
69
|
+
return runs.map((run) => {
|
|
70
|
+
const { placed, lost } = placeRoutes(run.points, run.routes);
|
|
71
|
+
const byEdgeMap = new Map(placed.map((p) => [p.edge, p] as const));
|
|
72
|
+
const edges = placed.map((p) => p.edge).sort(byText);
|
|
73
|
+
const key = `${edges.join("\u0001")}\u0002${run.points.map((q) => `${q.x},${q.y}`).join(";")}`;
|
|
74
|
+
return { placed, lost: [...lost].sort(byText), byEdge: byEdgeMap, key, twice: byEdgeMap.size !== placed.length };
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** This function gives the number of edges that two runs share, up to 2. */
|
|
79
|
+
function shared(a: Prepared, b: Prepared): number {
|
|
80
|
+
const [small, large] = a.placed.length <= b.placed.length ? [a, b] : [b, a];
|
|
81
|
+
let n = 0;
|
|
82
|
+
for (const p of small.placed) if (large.byEdge.has(p.edge) && ++n >= 2) return n;
|
|
83
|
+
return n;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* This function gives the links between the runs. A link can join two runs when a route goes through one and then the other.
|
|
88
|
+
* No shared run is between them on that route, and the two runs share two edges or more. Each such route gives a vote for the way of one run against the other.
|
|
89
|
+
* A link joins two runs only when all the votes agree. The links are a forest. The edges and the runs are taken in a fixed order, so the links do not depend on the order of the input.
|
|
90
|
+
*/
|
|
91
|
+
function linksOf(prepared: readonly Prepared[], keyOrder: readonly number[]): Link[][] {
|
|
92
|
+
const visits = new Map<string, { run: number; at: number; dir: 1 | -1 }[]>();
|
|
93
|
+
for (const i of keyOrder) {
|
|
94
|
+
for (const p of prepared[i]!.placed) {
|
|
95
|
+
// The index of the first point of the run in the route, in the way of the route.
|
|
96
|
+
const at = p.reversed ? p.p.length - 1 - p.e : p.s;
|
|
97
|
+
const list = visits.get(p.edge);
|
|
98
|
+
const v = { run: i, at, dir: p.reversed ? (-1 as const) : (1 as const) };
|
|
99
|
+
if (list) list.push(v);
|
|
100
|
+
else visits.set(p.edge, [v]);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
const rank = new Map(keyOrder.map((i, r) => [i, r] as const));
|
|
104
|
+
// The votes of each pair of runs, with the run of the smaller rank first.
|
|
105
|
+
const votes = new Map<string, { a: number; b: number; plus: number; minus: number }>();
|
|
106
|
+
for (const edge of [...visits.keys()].sort(byText)) {
|
|
107
|
+
const list = visits.get(edge)!;
|
|
108
|
+
list.sort((u, v) => u.at - v.at || rank.get(u.run)! - rank.get(v.run)!);
|
|
109
|
+
for (let k = 1; k < list.length; k++) {
|
|
110
|
+
const u = list[k - 1]!;
|
|
111
|
+
const v = list[k]!;
|
|
112
|
+
if (u.run === v.run || prepared[u.run]!.twice || prepared[v.run]!.twice) continue;
|
|
113
|
+
const [a, b] = rank.get(u.run)! < rank.get(v.run)! ? [u.run, v.run] : [v.run, u.run];
|
|
114
|
+
const key = `${a},${b}`;
|
|
115
|
+
let vote = votes.get(key);
|
|
116
|
+
if (!vote) {
|
|
117
|
+
if (shared(prepared[a]!, prepared[b]!) < 2) continue;
|
|
118
|
+
vote = { a, b, plus: 0, minus: 0 };
|
|
119
|
+
votes.set(key, vote);
|
|
120
|
+
}
|
|
121
|
+
if (u.dir === v.dir) vote.plus++;
|
|
122
|
+
else vote.minus++;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
// The links with the most votes go first. A link that closes a cycle of links does not join: the links are a forest, so the ways of the runs of a stretch agree.
|
|
126
|
+
const list = [...votes.values()]
|
|
127
|
+
.filter((v) => v.plus === 0 || v.minus === 0)
|
|
128
|
+
.sort((x, y) => y.plus + y.minus - (x.plus + x.minus) || rank.get(x.a)! - rank.get(y.a)! || rank.get(x.b)! - rank.get(y.b)!);
|
|
129
|
+
const parent = prepared.map((_, i) => i);
|
|
130
|
+
const find = (i: number): number => {
|
|
131
|
+
let r = i;
|
|
132
|
+
while (parent[r] !== r) r = parent[r]!;
|
|
133
|
+
return r;
|
|
134
|
+
};
|
|
135
|
+
const links: Link[][] = prepared.map(() => []);
|
|
136
|
+
for (const v of list) {
|
|
137
|
+
const ra = find(v.a);
|
|
138
|
+
const rb = find(v.b);
|
|
139
|
+
if (ra === rb) continue;
|
|
140
|
+
parent[rb] = ra;
|
|
141
|
+
const turn = v.minus === 0 ? (1 as const) : (-1 as const);
|
|
142
|
+
links[v.a]!.push({ to: v.b, turn });
|
|
143
|
+
links[v.b]!.push({ to: v.a, turn });
|
|
144
|
+
}
|
|
145
|
+
for (const l of links) l.sort((x, y) => rank.get(x.to)! - rank.get(y.to)!);
|
|
146
|
+
return links;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/** The pair relation of the edges of one stretch, in the way of the root. */
|
|
150
|
+
class Relation {
|
|
151
|
+
/** The index of each edge of the stretch. The indexes follow the order of the edge ids. */
|
|
152
|
+
readonly index: ReadonlyMap<string, number>;
|
|
153
|
+
readonly edges: readonly string[];
|
|
154
|
+
/** The pairs of each edge: the other edge and the side of the edge against it (-1: on the left). */
|
|
155
|
+
readonly pairs: Map<number, number>[];
|
|
156
|
+
|
|
157
|
+
constructor(edges: readonly string[]) {
|
|
158
|
+
this.edges = edges;
|
|
159
|
+
this.index = new Map(edges.map((e, i) => [e, i] as const));
|
|
160
|
+
this.pairs = edges.map(() => new Map<number, number>());
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/** This method sets the side of the edge `a` against the edge `b`. The value -1 puts `a` on the left of `b`, and 1 puts it on the right. */
|
|
164
|
+
set(a: string, b: string, side: number): void {
|
|
165
|
+
const i = this.index.get(a)!;
|
|
166
|
+
const j = this.index.get(b)!;
|
|
167
|
+
this.pairs[i]!.set(j, side);
|
|
168
|
+
this.pairs[j]!.set(i, -side);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/** This method gives -1 when the edge `a` is on the left of the edge `b`, and 1 when it is on the right. It gives 0 when the two edges share no run. */
|
|
172
|
+
side(a: number, b: number): number {
|
|
173
|
+
return this.pairs[a]!.get(b) ?? 0;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* This function gives an order of the edges of a relation with few broken pairs. It is the heuristic of Eades, Lin and Smyth (1993) for the feedback arc set.
|
|
179
|
+
* The sinks go to the end, and the sources go to the start. Else, the edge with the largest count of right pairs minus left pairs goes to the start.
|
|
180
|
+
* Each tie goes to the smallest index. A relation with no cycle gives an order with no broken pair.
|
|
181
|
+
*/
|
|
182
|
+
function eadesOrder(rel: Relation): number[] {
|
|
183
|
+
const n = rel.edges.length;
|
|
184
|
+
const left = new Int32Array(n);
|
|
185
|
+
const right = new Int32Array(n);
|
|
186
|
+
for (let a = 0; a < n; a++) for (const s of rel.pairs[a]!.values()) (s < 0 ? right : left)[a]!++;
|
|
187
|
+
const alive = new Uint8Array(n).fill(1);
|
|
188
|
+
const head: number[] = [];
|
|
189
|
+
const tail: number[] = [];
|
|
190
|
+
const remove = (a: number): void => {
|
|
191
|
+
alive[a] = 0;
|
|
192
|
+
for (const [b, s] of rel.pairs[a]!) {
|
|
193
|
+
if (!alive[b]) continue;
|
|
194
|
+
// The edge `a` is on the left of `b` (s < 0): `b` loses one edge on its left.
|
|
195
|
+
if (s < 0) left[b]!--;
|
|
196
|
+
else right[b]!--;
|
|
197
|
+
}
|
|
198
|
+
};
|
|
199
|
+
let count = n;
|
|
200
|
+
while (count > 0) {
|
|
201
|
+
let pick = -1;
|
|
202
|
+
let sink = false;
|
|
203
|
+
for (let a = n - 1; a >= 0; a--) {
|
|
204
|
+
if (alive[a] && right[a] === 0) {
|
|
205
|
+
pick = a;
|
|
206
|
+
sink = true;
|
|
207
|
+
break;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
if (pick < 0) {
|
|
211
|
+
let best = -Infinity;
|
|
212
|
+
for (let a = 0; a < n; a++) {
|
|
213
|
+
if (!alive[a]) continue;
|
|
214
|
+
const d = left[a] === 0 ? Infinity : right[a]! - left[a]!;
|
|
215
|
+
if (d > best) {
|
|
216
|
+
best = d;
|
|
217
|
+
pick = a;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
remove(pick);
|
|
222
|
+
count--;
|
|
223
|
+
if (sink) tail.push(pick);
|
|
224
|
+
else head.push(pick);
|
|
225
|
+
}
|
|
226
|
+
return [...head, ...tail.reverse()];
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/** This function counts the broken pairs of the edge `a` at the place `at` of an order. A broken pair has the other edge on the wrong side. */
|
|
230
|
+
function cost(rel: Relation, order: readonly number[], a: number, at: number): number {
|
|
231
|
+
let c = 0;
|
|
232
|
+
for (let k = 0; k < order.length; k++) {
|
|
233
|
+
const b = order[k]!;
|
|
234
|
+
if (b === a) continue;
|
|
235
|
+
const s = rel.side(a, b);
|
|
236
|
+
if (s === 0) continue;
|
|
237
|
+
if (k < at ? s < 0 : s > 0) c++;
|
|
238
|
+
}
|
|
239
|
+
return c;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* This function moves each edge to the place with the fewest broken pairs (sifting). It moves one edge at a time, in the order of the start of the pass.
|
|
244
|
+
* An edge stays at its place when no other place is better. Each tie goes to the place more to the left.
|
|
245
|
+
*/
|
|
246
|
+
function sift(rel: Relation, start: readonly number[], passes: number): number[] {
|
|
247
|
+
let order = [...start];
|
|
248
|
+
for (let pass = 0; pass < passes; pass++) {
|
|
249
|
+
let moved = false;
|
|
250
|
+
for (const a of [...order]) {
|
|
251
|
+
const from = order.indexOf(a);
|
|
252
|
+
const rest = order.filter((b) => b !== a);
|
|
253
|
+
// The cost of each place from 0 to `rest.length`: the place 0 breaks each pair where `a` must be on the right.
|
|
254
|
+
let c = 0;
|
|
255
|
+
for (const b of rest) if (rel.side(a, b) > 0) c++;
|
|
256
|
+
let best = c;
|
|
257
|
+
let at = 0;
|
|
258
|
+
let here = from === 0 ? c : Infinity;
|
|
259
|
+
for (let k = 0; k < rest.length; k++) {
|
|
260
|
+
const s = rel.side(a, rest[k]!);
|
|
261
|
+
if (s > 0) c--;
|
|
262
|
+
else if (s < 0) c++;
|
|
263
|
+
if (k + 1 === from) here = c;
|
|
264
|
+
if (c < best) {
|
|
265
|
+
best = c;
|
|
266
|
+
at = k + 1;
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
if (best < here) {
|
|
270
|
+
rest.splice(at, 0, a);
|
|
271
|
+
order = rest;
|
|
272
|
+
moved = true;
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
if (!moved) break;
|
|
276
|
+
}
|
|
277
|
+
return order;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/** This function gives the order `bundle` of the placed routes of one run, from left to right in the way of the run. */
|
|
281
|
+
function bundleOrder(run: Prepared): string[] {
|
|
282
|
+
return mergeSort([...run.placed].sort(byEdge), byBundle).map((p) => p.edge);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* This function gives the order of the root, then puts the edges of each next run in it (periphery).
|
|
287
|
+
* The edges that the order holds keep their order. Each new edge of a run goes to the place in the run with the fewest broken pairs, and each tie goes to the left.
|
|
288
|
+
*/
|
|
289
|
+
function peripheryOrder(rel: Relation, prepared: readonly Prepared[], runs: readonly number[], turns: readonly (1 | -1)[]): number[] {
|
|
290
|
+
const toIndex = (edges: readonly string[]): number[] => edges.map((e) => rel.index.get(e)!);
|
|
291
|
+
const root = toIndex(bundleOrder(prepared[runs[0]!]!));
|
|
292
|
+
const order = [...root];
|
|
293
|
+
const placed = new Set(order);
|
|
294
|
+
for (let r = 1; r < runs.length; r++) {
|
|
295
|
+
const run = prepared[runs[r]!]!;
|
|
296
|
+
const own = new Set(run.placed.map((p) => rel.index.get(p.edge)!));
|
|
297
|
+
const seq = order.filter((a) => own.has(a));
|
|
298
|
+
let fresh = toIndex(bundleOrder(run)).filter((a) => !placed.has(a));
|
|
299
|
+
if (turns[r] === -1) fresh = fresh.reverse();
|
|
300
|
+
for (const a of fresh) {
|
|
301
|
+
let best = Infinity;
|
|
302
|
+
let at = 0;
|
|
303
|
+
for (let k = 0; k <= seq.length; k++) {
|
|
304
|
+
const c = cost(rel, seq, a, k);
|
|
305
|
+
if (c < best) {
|
|
306
|
+
best = c;
|
|
307
|
+
at = k;
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
seq.splice(at, 0, a);
|
|
311
|
+
// The new edge goes into the order of the stretch next to its left neighbour in the run, or before its right neighbour.
|
|
312
|
+
if (at > 0) order.splice(order.indexOf(seq[at - 1]!) + 1, 0, a);
|
|
313
|
+
else if (seq.length > 1) order.splice(order.indexOf(seq[1]!), 0, a);
|
|
314
|
+
else order.push(a);
|
|
315
|
+
placed.add(a);
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
return order;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/**
|
|
322
|
+
* This function gives the stretches of the runs (LANE-STRETCH). Two runs are in one stretch when a chain of links joins them.
|
|
323
|
+
* A link joins two runs when a route goes through one and then the other, and they share two edges or more.
|
|
324
|
+
* The result does not depend on the order of the runs or of the routes of each run.
|
|
325
|
+
*/
|
|
326
|
+
export function laneStretches(runs: readonly LaneRun[], options: Partial<LaneStretchOptions> = {}): LaneStretch[] {
|
|
327
|
+
return stretchesOf(prepare(runs), { ...LANE_STRETCH_DEFAULTS, ...options });
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
/**
|
|
331
|
+
* The side of each pair of edges of a stretch, in the way of the root. The value -1 puts the first edge of the pair on the left.
|
|
332
|
+
* The key of a pair is `a * count + b`, with `a < b` the ids of the two edges and `count` the number of the ids.
|
|
333
|
+
*/
|
|
334
|
+
type Sides = Map<number, number>;
|
|
335
|
+
|
|
336
|
+
/** The sides of the pairs of one run, in its own way. The pair of the lanes `x < y` has the index `x * n - x * (x + 1) / 2 + y - x - 1`. */
|
|
337
|
+
interface RunSides {
|
|
338
|
+
/** The ids of the edges of the run, in increasing order. */
|
|
339
|
+
readonly ids: Int32Array;
|
|
340
|
+
/** The side of each pair: 1 or -1. */
|
|
341
|
+
readonly sides: Int8Array;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
/** This function gives the sides of the pairs of a run in its own way. A typed array keeps them small, because a dense board has many large runs. */
|
|
345
|
+
function sidesOf(run: Prepared, idOf: ReadonlyMap<string, number>): RunSides {
|
|
346
|
+
const lanes = [...run.placed].sort(byEdge);
|
|
347
|
+
const n = lanes.length;
|
|
348
|
+
const ids = Int32Array.from(lanes, (l) => idOf.get(l.edge)!);
|
|
349
|
+
const sides = new Int8Array((n * (n - 1)) / 2);
|
|
350
|
+
let k = 0;
|
|
351
|
+
for (let x = 0; x < n; x++) for (let y = x + 1; y < n; y++) sides[k++] = byBundle(lanes[x]!, lanes[y]!) > 0 ? 1 : -1;
|
|
352
|
+
return { ids, sides };
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
/**
|
|
356
|
+
* This function gives the stretches of the prepared runs, with the order of each stretch.
|
|
357
|
+
* A run joins a stretch only when the relation of each pair of its edges agrees with the runs of the stretch.
|
|
358
|
+
* Thus one order of the stretch can keep the side of each pair on each run, apart from the cycles of the relation.
|
|
359
|
+
*/
|
|
360
|
+
function stretchesOf(prepared: readonly Prepared[], options: Readonly<LaneStretchOptions>): LaneStretch[] {
|
|
361
|
+
const keyOrder = prepared.map((_, i) => i).sort((a, b) => byText(prepared[a]!.key, prepared[b]!.key));
|
|
362
|
+
const links = linksOf(prepared, keyOrder);
|
|
363
|
+
const rank = new Map(keyOrder.map((i, r) => [i, r] as const));
|
|
364
|
+
const done = new Uint8Array(prepared.length);
|
|
365
|
+
const out: LaneStretch[] = [];
|
|
366
|
+
// The id of each edge, in the order of `byText`. Thus the lanes of a run in the order of `byEdge` have increasing ids.
|
|
367
|
+
const names = [...new Set(prepared.flatMap((r) => r.placed.map((p) => p.edge)))].sort(byText);
|
|
368
|
+
const idOf = new Map(names.map((e, i) => [e, i] as const));
|
|
369
|
+
const count = names.length;
|
|
370
|
+
// The sides of each run, in its own way. Each run gets them one time, because the walks test a run again in each later stretch.
|
|
371
|
+
const own = new Map<number, RunSides>();
|
|
372
|
+
const sidesOfRun = (i: number): RunSides => {
|
|
373
|
+
let s = own.get(i);
|
|
374
|
+
if (!s) own.set(i, (s = sidesOf(prepared[i]!, idOf)));
|
|
375
|
+
return s;
|
|
376
|
+
};
|
|
377
|
+
/** This function calls `f` for each pair of a run with its key and its side in the way `t`. A return of false stops the loop. */
|
|
378
|
+
const eachPair = (r: RunSides, t: 1 | -1, f: (key: number, side: number) => boolean): boolean => {
|
|
379
|
+
const n = r.ids.length;
|
|
380
|
+
let k = 0;
|
|
381
|
+
for (let x = 0; x < n; x++) {
|
|
382
|
+
const base = r.ids[x]! * count;
|
|
383
|
+
for (let y = x + 1; y < n; y++) if (!f(base + r.ids[y]!, r.sides[k++]! * t)) return false;
|
|
384
|
+
}
|
|
385
|
+
return true;
|
|
386
|
+
};
|
|
387
|
+
// Each loop makes one stretch and marks its runs. A run that the walk does not take stays for a later stretch.
|
|
388
|
+
for (let p = 0; p < keyOrder.length; ) {
|
|
389
|
+
const start = keyOrder[p]!;
|
|
390
|
+
if (done[start]) {
|
|
391
|
+
p++;
|
|
392
|
+
continue;
|
|
393
|
+
}
|
|
394
|
+
// The runs that the links join to `start`, and the root: the run with the most lanes, then the first key.
|
|
395
|
+
const members = [start];
|
|
396
|
+
const seen = new Set(members);
|
|
397
|
+
for (let q = 0; q < members.length; q++) {
|
|
398
|
+
const i = members[q]!;
|
|
399
|
+
if (prepared[i]!.twice) continue;
|
|
400
|
+
for (const l of links[i]!) {
|
|
401
|
+
if (done[l.to] || seen.has(l.to) || prepared[l.to]!.twice) continue;
|
|
402
|
+
seen.add(l.to);
|
|
403
|
+
members.push(l.to);
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
const root = members.reduce((best, i) => {
|
|
407
|
+
const d = prepared[i]!.placed.length - prepared[best]!.placed.length;
|
|
408
|
+
return d > 0 || (d === 0 && rank.get(i)! < rank.get(best)!) ? i : best;
|
|
409
|
+
}, members[0]!);
|
|
410
|
+
// The walk from the root gives the order of the runs and the way of each run against the root.
|
|
411
|
+
const runsOf: number[] = [root];
|
|
412
|
+
const turns: (1 | -1)[] = [1];
|
|
413
|
+
const turnOf = new Map<number, 1 | -1>([[root, 1]]);
|
|
414
|
+
const sides: Sides = new Map();
|
|
415
|
+
eachPair(sidesOfRun(root), 1, (key, side) => (sides.set(key, side), true));
|
|
416
|
+
done[root] = 1;
|
|
417
|
+
for (let k = 0; k < runsOf.length; k++) {
|
|
418
|
+
const i = runsOf[k]!;
|
|
419
|
+
if (prepared[i]!.twice) continue;
|
|
420
|
+
for (const l of links[i]!) {
|
|
421
|
+
if (done[l.to] || prepared[l.to]!.twice) continue;
|
|
422
|
+
const t = (turnOf.get(i)! * l.turn) as 1 | -1;
|
|
423
|
+
const run = sidesOfRun(l.to);
|
|
424
|
+
const agrees = eachPair(run, t, (key, side) => {
|
|
425
|
+
const s = sides.get(key);
|
|
426
|
+
return s === undefined || s === side;
|
|
427
|
+
});
|
|
428
|
+
if (!agrees) continue;
|
|
429
|
+
eachPair(run, t, (key, side) => (sides.set(key, side), true));
|
|
430
|
+
done[l.to] = 1;
|
|
431
|
+
turnOf.set(l.to, t);
|
|
432
|
+
runsOf.push(l.to);
|
|
433
|
+
turns.push(t);
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
out.push({ runs: runsOf, turns, order: orderOf(prepared, runsOf, turns, sides, names, options) });
|
|
437
|
+
}
|
|
438
|
+
return out;
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
/** This function gives the order of the edges of one stretch, from left to right in the way of its root. */
|
|
442
|
+
function orderOf(
|
|
443
|
+
prepared: readonly Prepared[],
|
|
444
|
+
runs: readonly number[],
|
|
445
|
+
turns: readonly (1 | -1)[],
|
|
446
|
+
sides: Sides,
|
|
447
|
+
names: readonly string[],
|
|
448
|
+
options: Readonly<LaneStretchOptions>,
|
|
449
|
+
): string[] {
|
|
450
|
+
// A stretch of one run keeps the order `bundle` of the run. The method `none` keeps the order of each run.
|
|
451
|
+
if (runs.length === 1 || options.method === "none") return bundleOrder(prepared[runs[0]!]!);
|
|
452
|
+
const edges = [...new Set(runs.flatMap((i) => prepared[i]!.placed.map((p) => p.edge)))].sort(byText);
|
|
453
|
+
const rel = new Relation(edges);
|
|
454
|
+
const count = names.length;
|
|
455
|
+
for (const [key, side] of sides) rel.set(names[Math.floor(key / count)]!, names[key % count]!, side);
|
|
456
|
+
let order: number[];
|
|
457
|
+
if (options.method === "periphery") order = peripheryOrder(rel, prepared, runs, turns);
|
|
458
|
+
else {
|
|
459
|
+
order = eadesOrder(rel);
|
|
460
|
+
if (options.method === "sifting") order = sift(rel, order, Math.max(1, Math.min(4, Math.floor(options.passes))));
|
|
461
|
+
}
|
|
462
|
+
return order.map((i) => edges[i]!);
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
/**
|
|
466
|
+
* This function gives the lane order of each run (LANE-STRETCH). The orders are in the order of `runs`.
|
|
467
|
+
* Each order holds the edges of the run from left to right. The way goes from the first point of the run to its last point, with the y axis down.
|
|
468
|
+
* A route that does not hold its run goes last, as in `orderLanes`.
|
|
469
|
+
*
|
|
470
|
+
* With the method `none`, each run gets the order `bundle` of `orderLanes`. With the other methods, each run of a stretch gets the order of the stretch.
|
|
471
|
+
* Thus two runs of one stretch give two edges the same side. The result does not depend on the order of the runs or of the routes of each run.
|
|
472
|
+
*/
|
|
473
|
+
export function stretchLaneOrders(runs: readonly LaneRun[], options: Partial<LaneStretchOptions> = {}): string[][] {
|
|
474
|
+
const o: LaneStretchOptions = { ...LANE_STRETCH_DEFAULTS, ...options };
|
|
475
|
+
const prepared = prepare(runs);
|
|
476
|
+
if (o.method === "none") return prepared.map((run) => [...bundleOrder(run), ...run.lost]);
|
|
477
|
+
const out: string[][] = prepared.map(() => []);
|
|
478
|
+
for (const s of stretchesOf(prepared, o)) {
|
|
479
|
+
s.runs.forEach((i, r) => {
|
|
480
|
+
const run = prepared[i]!;
|
|
481
|
+
const own = s.order.filter((e) => run.byEdge.has(e));
|
|
482
|
+
out[i] = [...(s.turns[r] === -1 ? own.reverse() : own), ...run.lost];
|
|
483
|
+
});
|
|
484
|
+
}
|
|
485
|
+
return out;
|
|
486
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
// The router `shared-stretch` (LANE-STRETCH): the router `shared` with one lane order for each stretch of runs.
|
|
2
|
+
// The router is opt-in: it is not in the registry `STRATEGIES`, so no default output changes. The benchmark adds it with `--opt-in shared-stretch`.
|
|
3
|
+
import type { RouterPreset } from "../../strategies/builtins.ts";
|
|
4
|
+
import { byCollection } from "../../routers/trunks.ts";
|
|
5
|
+
import type { Graph, Result } from "../../model.ts";
|
|
6
|
+
import { sharedRouter, type SharedOptions } from "../../routers/shared.ts";
|
|
7
|
+
import { defineStrategy, type Strategy } from "../../strategies/model.ts";
|
|
8
|
+
import { SHARED_SCHEMA } from "../../strategies/schemas.ts";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* The router strategy `shared-stretch` (LANE-STRETCH). It routes as `shared`, and the runs of one stretch share one lane order.
|
|
12
|
+
* The default stretch order is `sifting`, the best order of the screen. The option `stretchOrder` selects `consensus` or `periphery`.
|
|
13
|
+
*/
|
|
14
|
+
export const sharedStretchStrategy: Strategy<"router", RouterPreset<SharedOptions>> = defineStrategy<"router", RouterPreset<SharedOptions>>({
|
|
15
|
+
step: "router",
|
|
16
|
+
name: "shared-stretch",
|
|
17
|
+
describe: "The same ways as `shared`. The runs that follow each other on the routes share one lane order, so fewer lanes cross between them (LANE-STRETCH).",
|
|
18
|
+
defaults: { rule: byCollection, stretchOrder: "sifting" },
|
|
19
|
+
schema: { ...SHARED_SCHEMA, stretchOrder: { ...SHARED_SCHEMA.stretchOrder!, default: "sifting" } },
|
|
20
|
+
maxNodes: 10_000,
|
|
21
|
+
run: (graph: Graph, options: Readonly<RouterPreset<SharedOptions>>): Result => sharedRouter({ ...options, name: "shared-stretch" }).route(graph),
|
|
22
|
+
});
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { levelLinks, type ItemPair, type LevelLinkOptions } from "../../levels.ts";
|
|
2
|
+
import type { Level, Route } from "../../model.ts";
|
|
3
|
+
import { levelCut, treeDepth } from "../../sections.ts";
|
|
4
|
+
import type { LevelInput } from "../../strategies/model.ts";
|
|
5
|
+
|
|
6
|
+
/** The options of the level strategy `depths`: the limits of the level links, and the depths that get a level with groups. */
|
|
7
|
+
export interface DepthLevelOptions extends Omit<LevelLinkOptions, "smooth"> {
|
|
8
|
+
/**
|
|
9
|
+
* The depths of the tree that get a zoom level with groups. The last level, with each node alone, always comes after them.
|
|
10
|
+
* The default is each depth, from 0 to the depth of the deepest section.
|
|
11
|
+
*/
|
|
12
|
+
readonly levels?: readonly number[];
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* This function gives the zoom levels of the level strategy `depths`.
|
|
17
|
+
* It gives one level for each depth of the tree, and a last level with each node alone.
|
|
18
|
+
* It is the default of the router `sections`. With the option `levels`, only those depths get a level with groups.
|
|
19
|
+
* At depth d, a section of depth d is one item. A link joins two items and counts the edges between them.
|
|
20
|
+
* In a level with groups, a link goes around the boxes of the other items (`levelLinks`).
|
|
21
|
+
* In the last level, the points of a link are the route of its first edge.
|
|
22
|
+
*/
|
|
23
|
+
export function depthLevels(input: LevelInput, options: DepthLevelOptions = {}): Level[] {
|
|
24
|
+
const { graph, root } = input;
|
|
25
|
+
const routeOf = new Map<string, Route>(input.routes.map((r) => [r.edge, r]));
|
|
26
|
+
const indexOfNode = new Map(graph.nodes.map((n, i) => [n.id, i]));
|
|
27
|
+
const levels: Level[] = [];
|
|
28
|
+
const deepest = treeDepth(root);
|
|
29
|
+
const shown = options.levels && new Set(options.levels);
|
|
30
|
+
for (let d = 0; d <= deepest + 1; d++) {
|
|
31
|
+
if (shown && d <= deepest && !shown.has(d)) continue;
|
|
32
|
+
const { items, owner } = levelCut(graph.nodes, root, d);
|
|
33
|
+
const links = new Map<string, { source: string; target: string; count: number; edge: string }>();
|
|
34
|
+
for (const e of graph.edges) {
|
|
35
|
+
const iu = indexOfNode.get(e.source.node);
|
|
36
|
+
const iv = indexOfNode.get(e.target.node);
|
|
37
|
+
if (iu === undefined || iv === undefined) continue;
|
|
38
|
+
const a = owner[iu]!;
|
|
39
|
+
const b = owner[iv]!;
|
|
40
|
+
if (a === b) continue;
|
|
41
|
+
const ida = items[a]!.id;
|
|
42
|
+
const idb = items[b]!.id;
|
|
43
|
+
const key = `${ida}\u0000${idb}`;
|
|
44
|
+
const link = links.get(key);
|
|
45
|
+
if (link) {
|
|
46
|
+
link.count++;
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
links.set(key, { source: ida, target: idb, count: 1, edge: e.id });
|
|
50
|
+
}
|
|
51
|
+
const pairs: ItemPair[] = [...links.values()].map(({ source, target, count }) => ({ source, target, count }));
|
|
52
|
+
if (d > deepest)
|
|
53
|
+
levels.push({ depth: d, items, links: [...links.values()].map(({ edge, ...l }) => ({ ...l, points: routeOf.get(edge)?.points ?? [] })) });
|
|
54
|
+
else levels.push({ depth: d, items, links: levelLinks(items, pairs, options) });
|
|
55
|
+
}
|
|
56
|
+
return levels;
|
|
57
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { center } from "../../geom.ts";
|
|
2
|
+
import { nodeIndex } from "../../model.ts";
|
|
3
|
+
import { placePlug } from "../../plugs.ts";
|
|
4
|
+
import type { PlugPair, PlugInput } from "../../strategies/model.ts";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* This function gives the plugs of the plug strategy `facing`: each free plug faces the other end of its edge (`placePlug`).
|
|
8
|
+
* A fixed plug stays at its point, and a plug with a side stays on that side. It is the default of the router `corners`.
|
|
9
|
+
* The output has one entry for each edge with two known nodes, in the order of the edges.
|
|
10
|
+
*/
|
|
11
|
+
export function facingEnds(input: PlugInput): PlugPair[] {
|
|
12
|
+
const { graph } = input;
|
|
13
|
+
const byId = nodeIndex(graph);
|
|
14
|
+
const out: PlugPair[] = [];
|
|
15
|
+
for (const e of graph.edges) {
|
|
16
|
+
const s = byId.get(e.source.node);
|
|
17
|
+
const t = byId.get(e.target.node);
|
|
18
|
+
if (!s || !t) continue;
|
|
19
|
+
out.push({ edge: e.id, source: placePlug(s, e.source, center(t.box)), target: placePlug(t, e.target, center(s.box)) });
|
|
20
|
+
}
|
|
21
|
+
return out;
|
|
22
|
+
}
|
package/src/portfolio/router.ts
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
import { pickMoves, portfolio, type Member, type PortfolioTrace } from "../bench/screen/portfolio-pick.ts";
|
|
5
5
|
import type { Graph, Result, Router } from "../model.ts";
|
|
6
6
|
import { asRouter, defineStrategy, type Strategy } from "../strategies/model.ts";
|
|
7
|
+
import type { OptionSchema } from "../strategies/options.ts";
|
|
7
8
|
import { PORTFOLIO_CAPS, PORTFOLIO_KS_API, PORTFOLIO_RULES, portfolioBand, type PortfolioCap, type PortfolioRule } from "./bands.ts";
|
|
8
9
|
import { PORTFOLIO_FIT } from "./fit.ts";
|
|
9
10
|
import { portfolioKey } from "./key.ts";
|
|
@@ -135,6 +136,47 @@ export function portfolioRouter(options: PortfolioRouterOptions = {}): Router {
|
|
|
135
136
|
return { name: portfolioName(options), maxNodes: 10_000, route: (graph) => routePortfolio(graph, options).result };
|
|
136
137
|
}
|
|
137
138
|
|
|
139
|
+
/** The largest count of members after the base: the largest list of the fit and of the sets. */
|
|
140
|
+
const MAX_AFTER_BASE = Math.max(...PORTFOLIO_KS_API, ...Object.values(PORTFOLIO_FIT).map((l) => l.length), ...Object.values(PORTFOLIO_SETS).map((l) => l.length));
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* The declarations of the options of the portfolio router (SCHEMA-GAPS). The lists of values come from the constants, so a new rule or cap needs no change here.
|
|
144
|
+
* The option `now` has no declaration. It is a clock for the tests, not a choice of the layout, and no control or sweep can set a function.
|
|
145
|
+
*/
|
|
146
|
+
const PORTFOLIO_SCHEMA: OptionSchema<Omit<PortfolioRouterOptions, "now">> = {
|
|
147
|
+
rule: { type: "enum", values: PORTFOLIO_RULES, default: "collection", describe: "With `collection`, a shared segment holds edges of one collection only. With `all`, collections can mix." },
|
|
148
|
+
cap: {
|
|
149
|
+
type: "number",
|
|
150
|
+
min: Math.min(...PORTFOLIO_CAPS),
|
|
151
|
+
max: Math.max(...PORTFOLIO_CAPS),
|
|
152
|
+
values: PORTFOLIO_CAPS,
|
|
153
|
+
default: 1.5,
|
|
154
|
+
describe: "The stretch cap. An output with an edge above the cap does not win.",
|
|
155
|
+
},
|
|
156
|
+
k: {
|
|
157
|
+
type: "integer",
|
|
158
|
+
min: Math.min(...PORTFOLIO_KS_API),
|
|
159
|
+
max: Math.max(...PORTFOLIO_KS_API),
|
|
160
|
+
values: PORTFOLIO_KS_API,
|
|
161
|
+
describe: "The count of members after the base. Without it, the router uses the fit of the size band.",
|
|
162
|
+
},
|
|
163
|
+
budgetMs: {
|
|
164
|
+
type: "integer",
|
|
165
|
+
min: 0,
|
|
166
|
+
max: 10_000,
|
|
167
|
+
step: 10,
|
|
168
|
+
describe: "The time limit of the pick in milliseconds. Without it, there is no limit. A cut by the limit makes the output depend on the machine.",
|
|
169
|
+
},
|
|
170
|
+
maxMembers: {
|
|
171
|
+
type: "integer",
|
|
172
|
+
min: 1,
|
|
173
|
+
max: 1 + MAX_AFTER_BASE,
|
|
174
|
+
step: 1,
|
|
175
|
+
describe: "The largest count of members that run, with the base. Without it, each member of the pick runs.",
|
|
176
|
+
},
|
|
177
|
+
hashes: { type: "enum", values: PORTFOLIO_HASHES, default: "pick", describe: "With `pick`, only the base and the winner get an output hash in the trace. With `all`, each member gets one." },
|
|
178
|
+
};
|
|
179
|
+
|
|
138
180
|
/**
|
|
139
181
|
* The portfolio router as a router strategy. It is not in the registry `STRATEGIES`, so the benchmark and the site do not race it by default.
|
|
140
182
|
* A caller adds it to a registry of its own, or calls `routePortfolio` for the trace.
|
|
@@ -144,6 +186,8 @@ export const portfolioStrategy: Strategy<"router", PortfolioRouterOptions> = def
|
|
|
144
186
|
name: "portfolio",
|
|
145
187
|
describe: "The portfolio router (PORTFOLIO-API). It runs `shared` and the fit of the size band, and gives the output with the most ink saved inside the rule and the cap.",
|
|
146
188
|
defaults: { rule: "collection", cap: 1.5 },
|
|
189
|
+
// The schema leaves out the clock `now`, so its type is the options without `now`.
|
|
190
|
+
schema: PORTFOLIO_SCHEMA as OptionSchema<PortfolioRouterOptions>,
|
|
147
191
|
maxNodes: 10_000,
|
|
148
192
|
run: (graph, options) => routePortfolio(graph, options).result,
|
|
149
193
|
});
|