@mark1russell7/highways 0.5.0 → 0.6.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 +47 -2
- package/build/index.d.ts +1489 -63
- package/build/index.d.ts.map +1 -1
- package/build/index.js +10139 -3356
- package/build/index.js.map +1 -1
- package/package.json +11 -2
- package/src/bench/exact/index.ts +218 -11
- package/src/bench/exact/optima-exact-v1-bounds.json +14409 -0
- package/src/bench/exact/optima-exact-v1.json +4809 -0
- package/src/bench/exact/solve.ts +129 -0
- package/src/bench/screen/activation.ts +3 -0
- package/src/bench/screen/bounds.ts +140 -0
- package/src/bench/screen/compare.ts +26 -0
- package/src/bench/screen/exact-set.ts +100 -0
- package/src/bench/screen/exercise.ts +290 -0
- package/src/bench/screen/forms.ts +14 -0
- package/src/bench/screen/gap.ts +316 -0
- package/src/bench/screen/hash.ts +7 -76
- package/src/bench/screen/index.ts +6 -0
- package/src/bench/screen/measures.ts +71 -1
- package/src/bench/screen/portfolio-pick.ts +20 -0
- package/src/bench/screen/screen.ts +50 -2
- package/src/bench/screen/set.ts +3 -2
- package/src/highways/areas/index.ts +160 -0
- package/src/highways/confluent/index.ts +154 -0
- package/src/highways/confluent/k23.test-util.ts +8 -0
- package/src/highways/consensus/index.ts +129 -0
- package/src/highways/crossbars/index.ts +184 -0
- package/src/highways/cubu/index.ts +26 -0
- package/src/highways/divided/index.ts +111 -0
- package/src/highways/fdeb/index.ts +193 -0
- package/src/highways/frechet/index.ts +232 -0
- package/src/highways/hierarchy/index.ts +140 -0
- package/src/highways/index.ts +64 -0
- package/src/highways/kdeeb/index.ts +355 -0
- package/src/highways/link-communities/index.ts +189 -0
- package/src/highways/mesh/index.ts +209 -0
- package/src/highways/mlseb/index.ts +204 -0
- package/src/highways/p-hub/index.ts +248 -0
- package/src/highways/power/index.ts +117 -0
- package/src/highways/resistance/index.ts +159 -0
- package/src/highways/sbeb/index.ts +303 -0
- package/src/highways/scale-space/index.ts +135 -0
- package/src/highways/spectral/index.ts +391 -0
- package/src/highways/territories/index.ts +150 -0
- package/src/highways/trace.ts +153 -0
- package/src/index.ts +3 -0
- package/src/portfolio/bands.ts +38 -0
- package/src/portfolio/fit.ts +41 -0
- package/src/portfolio/index.ts +6 -0
- package/src/portfolio/key.ts +98 -0
- package/src/portfolio/members.ts +171 -0
- package/src/portfolio/router.ts +133 -0
- package/src/portfolio/sets.ts +40 -0
- package/src/strategies/builtins.ts +222 -1
- package/src/strategies/schemas.ts +280 -1
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import { dist, type Point } from "../geom.ts";
|
|
2
|
+
import type { Obstacles } from "../visibility.ts";
|
|
3
|
+
import type { WayFinder } from "../ways.ts";
|
|
4
|
+
import type { HighwayInput, HighwayPlan } from "./model.ts";
|
|
5
|
+
import { edgePlugs, sharedStretches, type KeyedWay } from "./stretches.ts";
|
|
6
|
+
|
|
7
|
+
/** The options of `tracePlan`. */
|
|
8
|
+
export interface TraceOptions {
|
|
9
|
+
/** The side of the grid that the waypoints snap to, in units of the board. The value 0 keeps each waypoint where it is. */
|
|
10
|
+
readonly snap: number;
|
|
11
|
+
/** A highway has this number of edges or more. The value is 2 or more. */
|
|
12
|
+
readonly minSize: number;
|
|
13
|
+
/** The key of the ways. Only the ways with one key share a highway. */
|
|
14
|
+
readonly key: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* This function moves a point out of each box, made larger by `pad`. A point in a box moves to the nearest side of that box.
|
|
19
|
+
* The function tries four times, so a point between two near boxes can stay in a box.
|
|
20
|
+
*/
|
|
21
|
+
export function outOfBoxes(obstacles: Obstacles, p: Point, pad: number): Point {
|
|
22
|
+
let q = p;
|
|
23
|
+
for (let k = 0; k < 4; k++) {
|
|
24
|
+
let hit: { x: number; y: number; w: number; h: number } | undefined;
|
|
25
|
+
obstacles.query({ x: q.x - pad, y: q.y - pad, w: 2 * pad, h: 2 * pad }, (n) => {
|
|
26
|
+
const b = n.box;
|
|
27
|
+
if (!hit && q.x > b.x - pad && q.x < b.x + b.w + pad && q.y > b.y - pad && q.y < b.y + b.h + pad) hit = b;
|
|
28
|
+
});
|
|
29
|
+
if (!hit) return q;
|
|
30
|
+
const b = hit;
|
|
31
|
+
const eps = 1e-3;
|
|
32
|
+
const moves = [
|
|
33
|
+
{ d: q.x - (b.x - pad), p: { x: b.x - pad - eps, y: q.y } },
|
|
34
|
+
{ d: b.x + b.w + pad - q.x, p: { x: b.x + b.w + pad + eps, y: q.y } },
|
|
35
|
+
{ d: q.y - (b.y - pad), p: { x: q.x, y: b.y - pad - eps } },
|
|
36
|
+
{ d: b.y + b.h + pad - q.y, p: { x: q.x, y: b.y + b.h + pad + eps } },
|
|
37
|
+
];
|
|
38
|
+
moves.sort((u, v) => u.d - v.d);
|
|
39
|
+
q = moves[0]!.p;
|
|
40
|
+
}
|
|
41
|
+
return q;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* This function gives the push of the boxes on a point. Each box nearer than `reach` adds a unit vector away from it, times `(reach - d) / reach`.
|
|
46
|
+
* The value `d` is the distance from the point to the box. It is the gradient of a field that is 0 at the distance `reach` from each box.
|
|
47
|
+
*/
|
|
48
|
+
export function boxPush(obstacles: Obstacles, p: Point, reach: number): Point {
|
|
49
|
+
let x = 0;
|
|
50
|
+
let y = 0;
|
|
51
|
+
if (reach <= 0) return { x, y };
|
|
52
|
+
obstacles.query({ x: p.x - reach, y: p.y - reach, w: 2 * reach, h: 2 * reach }, (n) => {
|
|
53
|
+
const b = n.box;
|
|
54
|
+
const qx = Math.max(b.x, Math.min(b.x + b.w, p.x));
|
|
55
|
+
const qy = Math.max(b.y, Math.min(b.y + b.h, p.y));
|
|
56
|
+
let dx = p.x - qx;
|
|
57
|
+
let dy = p.y - qy;
|
|
58
|
+
let d = Math.hypot(dx, dy);
|
|
59
|
+
if (d >= reach) return;
|
|
60
|
+
if (d < 1e-9) {
|
|
61
|
+
// The point is in the box: it goes out through the nearest side.
|
|
62
|
+
const sides = [
|
|
63
|
+
{ d: p.x - b.x, v: [-1, 0] },
|
|
64
|
+
{ d: b.x + b.w - p.x, v: [1, 0] },
|
|
65
|
+
{ d: p.y - b.y, v: [0, -1] },
|
|
66
|
+
{ d: b.y + b.h - p.y, v: [0, 1] },
|
|
67
|
+
].sort((u, v) => u.d - v.d);
|
|
68
|
+
[dx, dy] = sides[0]!.v as [number, number];
|
|
69
|
+
d = 0;
|
|
70
|
+
} else {
|
|
71
|
+
dx /= d;
|
|
72
|
+
dy /= d;
|
|
73
|
+
}
|
|
74
|
+
const f = (reach - d) / reach;
|
|
75
|
+
x += dx * f;
|
|
76
|
+
y += dy * f;
|
|
77
|
+
});
|
|
78
|
+
return { x, y };
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** This function gives points at equal steps of `step` or a little less along a polyline, without its two ends. A polyline without length gives no point. */
|
|
82
|
+
export function innerPoints(line: readonly Point[], step: number): Point[] {
|
|
83
|
+
const at: number[] = [0];
|
|
84
|
+
for (let i = 1; i < line.length; i++) at.push(at[i - 1]! + dist(line[i - 1]!, line[i]!));
|
|
85
|
+
const total = at[at.length - 1] ?? 0;
|
|
86
|
+
if (total <= 1e-9 || step <= 0) return [];
|
|
87
|
+
const count = Math.floor(total / step);
|
|
88
|
+
const out: Point[] = [];
|
|
89
|
+
let j = 1;
|
|
90
|
+
for (let k = 1; k <= count; k++) {
|
|
91
|
+
const s = (total * k) / (count + 1);
|
|
92
|
+
while (j < line.length - 1 && at[j]! < s) j++;
|
|
93
|
+
const a = line[j - 1]!;
|
|
94
|
+
const b = line[j]!;
|
|
95
|
+
const len = at[j]! - at[j - 1]!;
|
|
96
|
+
const u = len > 0 ? Math.max(0, Math.min(1, (s - at[j - 1]!) / len)) : 0;
|
|
97
|
+
out.push({ x: a.x + (b.x - a.x) * u, y: a.y + (b.y - a.y) * u });
|
|
98
|
+
}
|
|
99
|
+
return out;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* This function gives the way of one edge through its waypoints. The way starts at `a` and ends at `b`.
|
|
104
|
+
* Each waypoint snaps to the grid of side `snap`. A waypoint in a box moves to the nearest corner of the corner graph.
|
|
105
|
+
* From each waypoint to the next one, the way is a straight line when that line is clear. Otherwise it goes through the corner graph.
|
|
106
|
+
* A waypoint that the graph does not reach is not on the way. When the way cannot reach `b`, the function gives the way of the corner graph from `a` to `b`.
|
|
107
|
+
*/
|
|
108
|
+
export function waypointWay(finder: WayFinder, a: Point, b: Point, waypoints: readonly Point[], snap: number): Point[] | undefined {
|
|
109
|
+
const corners = finder.corners;
|
|
110
|
+
const pad = finder.margin / 2;
|
|
111
|
+
const fixed: Point[] = [];
|
|
112
|
+
for (const w of waypoints) {
|
|
113
|
+
let q = snap > 0 ? { x: Math.round(w.x / snap) * snap, y: Math.round(w.y / snap) * snap } : w;
|
|
114
|
+
if (finder.obstacles.inside(q, pad)) {
|
|
115
|
+
const i = corners.nearest(q);
|
|
116
|
+
if (i < 0) continue;
|
|
117
|
+
q = corners.points[i]!;
|
|
118
|
+
}
|
|
119
|
+
const last = fixed[fixed.length - 1];
|
|
120
|
+
if (last && dist(last, q) <= 1e-9) continue;
|
|
121
|
+
fixed.push(q);
|
|
122
|
+
}
|
|
123
|
+
const out: Point[] = [a];
|
|
124
|
+
let at = a;
|
|
125
|
+
for (const q of fixed) {
|
|
126
|
+
if (dist(at, q) <= 1e-9) continue;
|
|
127
|
+
const piece = corners.path(at, q);
|
|
128
|
+
if (!piece) continue;
|
|
129
|
+
out.push(...piece.slice(1));
|
|
130
|
+
at = q;
|
|
131
|
+
}
|
|
132
|
+
const tail = dist(at, b) <= 1e-9 ? [b] : corners.path(at, b);
|
|
133
|
+
if (!tail) return corners.path(a, b);
|
|
134
|
+
out.push(...tail.slice(1));
|
|
135
|
+
return out;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* This function gives the highways of the waypoints of each edge. The ways go through the corner graph, so they stay out of the boxes.
|
|
140
|
+
* Each edge goes from the point at `margin` from its source plug, through its waypoints, to the point at `margin` from its target plug.
|
|
141
|
+
* Each stretch that `minSize` ways or more share is one highway, with the needless corners removed.
|
|
142
|
+
* An edge without waypoints takes its way of the corner graph.
|
|
143
|
+
*/
|
|
144
|
+
export function tracePlan(input: HighwayInput, finder: WayFinder, waypoints: ReadonlyMap<string, readonly Point[]>, o: Readonly<TraceOptions>): HighwayPlan {
|
|
145
|
+
const ways: KeyedWay[] = [];
|
|
146
|
+
for (const { edge, ps, pt } of edgePlugs(input.graph)) {
|
|
147
|
+
const a = finder.out(ps);
|
|
148
|
+
const b = finder.out(pt);
|
|
149
|
+
const way = waypointWay(finder, a, b, waypoints.get(edge.id) ?? [], o.snap);
|
|
150
|
+
if (way) ways.push({ edge: edge.id, key: o.key, points: way });
|
|
151
|
+
}
|
|
152
|
+
return sharedStretches(ways, { minSize: o.minSize, pull: (p) => finder.corners.pull(p) });
|
|
153
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -29,6 +29,9 @@ export * from "./bench/suite.ts";
|
|
|
29
29
|
export * from "./strategies/index.ts";
|
|
30
30
|
export * from "./bench/compare.ts";
|
|
31
31
|
export * from "./bench/screen/index.ts";
|
|
32
|
+
// The stretch of the exact model, for a benchmark that compares routers with the measures of the portfolio (PORTFOLIO-SPEED).
|
|
33
|
+
export { exactStretch, exactStretches } from "./bench/exact/index.ts";
|
|
32
34
|
export * from "./highways/index.ts";
|
|
33
35
|
export * from "./order/index.ts";
|
|
34
36
|
export * from "./repair/index.ts";
|
|
37
|
+
export * from "./portfolio/index.ts";
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/** The rule of the caller. Under `collection`, a shared segment holds edges of one collection only. Under `all`, collections can mix. */
|
|
2
|
+
export type PortfolioRule = "collection" | "all";
|
|
3
|
+
|
|
4
|
+
/** The stretch caps of the portfolio router: the caps of the gap closed (B10). */
|
|
5
|
+
export type PortfolioCap = 1.1 | 1.25 | 1.5;
|
|
6
|
+
|
|
7
|
+
/** The rules of the portfolio router. */
|
|
8
|
+
export const PORTFOLIO_RULES: readonly PortfolioRule[] = ["collection", "all"];
|
|
9
|
+
|
|
10
|
+
/** The caps of the portfolio router. */
|
|
11
|
+
export const PORTFOLIO_CAPS: readonly PortfolioCap[] = [1.1, 1.25, 1.5];
|
|
12
|
+
|
|
13
|
+
/** The sizes of the sets of the portfolio router, without the base. */
|
|
14
|
+
export const PORTFOLIO_KS_API: readonly (3 | 5 | 8)[] = [3, 5, 8];
|
|
15
|
+
|
|
16
|
+
/** One size band of the inputs. Each band has its own greedy sets. */
|
|
17
|
+
export interface PortfolioBand {
|
|
18
|
+
readonly id: string;
|
|
19
|
+
/** The largest node count of the band. An input with more nodes than the last band uses the last band. */
|
|
20
|
+
readonly maxNodes: number;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* The size bands: up to 12 nodes, 13 to 24 nodes, 25 to 40 nodes, 41 to 64 nodes and 65 to 128 nodes. An input above 128 nodes uses the band `n65-128`.
|
|
25
|
+
* The two large bands come from PORTFOLIO-SPEED. Their boards have no optima, so their sets come from the B1 under the guard.
|
|
26
|
+
*/
|
|
27
|
+
export const PORTFOLIO_BANDS: readonly PortfolioBand[] = [
|
|
28
|
+
{ id: "n1-12", maxNodes: 12 },
|
|
29
|
+
{ id: "n13-24", maxNodes: 24 },
|
|
30
|
+
{ id: "n25-40", maxNodes: 40 },
|
|
31
|
+
{ id: "n41-64", maxNodes: 64 },
|
|
32
|
+
{ id: "n65-128", maxNodes: 128 },
|
|
33
|
+
];
|
|
34
|
+
|
|
35
|
+
/** This function gives the band of a node count: the first band whose `maxNodes` is the count or more, else the last band. */
|
|
36
|
+
export function portfolioBand(nodes: number, bands: readonly PortfolioBand[] = PORTFOLIO_BANDS): PortfolioBand {
|
|
37
|
+
return bands.find((b) => nodes <= b.maxNodes) ?? bands[bands.length - 1]!;
|
|
38
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
// This file is generated by `scripts/portfolio-sets.ts` (`pnpm portfolio:sets`). Do not edit it. The test `sets.test.ts` checks that it is current.
|
|
2
|
+
// The times come from the CI run 40670252502 (the workflow `Portfolio`, `portfolio:time --members`).
|
|
3
|
+
import type { PortfolioCap, PortfolioRule } from "./bands.ts";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* The fit of each rule, cap and size band, by the key `<rule>|<cap>|<band>`: the members of the default of the portfolio router, in their order.
|
|
7
|
+
* Each member has a node limit: it runs only on an input with that many nodes or fewer. The limits keep the model time at or below 42 ms.
|
|
8
|
+
* The base `shared` is not in a fit: the portfolio always runs it first.
|
|
9
|
+
*/
|
|
10
|
+
export const PORTFOLIO_FIT: Readonly<Record<`${PortfolioRule}|${PortfolioCap}|${string}`, readonly (readonly [string, number])[]>> = {
|
|
11
|
+
"collection|1.1|n1-12": [["corners-side", 12], ["w2c:select+stretch", 12], ["trunks-gilbert-a05", 12], ["w3a:spiral-a15", 12], ["trunks-tree-bf08", 12], ["hw:zipper", 12], ["w2c:level-links+stretch-b1.1", 12], ["hw:stubs", 12]],
|
|
12
|
+
"collection|1.1|n13-24": [["w3e:crossbars+stretch", 24], ["trunks-tree-bf08", 24], ["corners-side", 24], ["shared-p2", 24], ["w2c:zipper+stretch-b1.1", 18]],
|
|
13
|
+
"collection|1.1|n25-40": [["w2a:shared-ways+breakeven", 40], ["w2b:weiszfeld-a1", 32]],
|
|
14
|
+
"collection|1.1|n41-64": [],
|
|
15
|
+
"collection|1.1|n65-128": [],
|
|
16
|
+
"collection|1.25|n1-12": [["corners-side", 12], ["trunks-tree-bf06", 12], ["w3f:p-hub-a0.2", 12], ["hw:stubs", 12], ["hw:p-hub", 12], ["hw:spiral", 12], ["w2c:level-links+stretch", 12], ["w3f:spectral-c0.3", 12]],
|
|
17
|
+
"collection|1.25|n13-24": [["w2a:shared-ways+stretch", 24], ["shared-p3", 24], ["trunks-gilbert-a07", 24], ["w3e:crossbars+stretch", 24], ["trunks-tree-bf06", 24], ["w3c:hdbscan+stretch", 18], ["shared-wind-p2-r1-e2", 18]],
|
|
18
|
+
"collection|1.25|n25-40": [["w2a:shared-ways+stretch", 40], ["shared-p3", 40], ["shared-order-shortest", 32]],
|
|
19
|
+
"collection|1.25|n41-64": [],
|
|
20
|
+
"collection|1.25|n65-128": [],
|
|
21
|
+
"collection|1.5|n1-12": [["trunks-gilbert-a03", 12], ["w3e:areas-keys", 12], ["trunks-tree-bf02", 12], ["w3f:p-hub-a0.2", 12], ["w2b:zipper-a1", 12], ["w3a:spiral-a35", 12], ["w3f:spectral-c0.3", 12], ["w2c:level-links+stretch", 12]],
|
|
22
|
+
"collection|1.5|n13-24": [["shared-wind-p2-r0.5", 24], ["trunks-gilbert-a03", 24], ["w3e:crossbars+stretch", 24], ["shared-wind-p2-r1-e2", 24], ["trunks-tree-bf06", 24], ["shared-flow-a06-p3", 24], ["shared-wind-p3-r1", 18], ["trunks-gilbert-a07", 18]],
|
|
23
|
+
"collection|1.5|n25-40": [["shared-p3", 40], ["hw:lanes", 40], ["shared-wind-p2-r1-e2", 32]],
|
|
24
|
+
"collection|1.5|n41-64": [["shared-flow-a06-p3", 64]],
|
|
25
|
+
"collection|1.5|n65-128": [],
|
|
26
|
+
"all|1.1|n1-12": [["w2b:zipper-p2", 12], ["hw:cubu", 12], ["w3d:kdeeb+stretch", 12], ["w3c:hough-wide", 12], ["w3d:kdeeb-routes", 12], ["hw:stubs", 10], ["hw:spiral", 10], ["w3b:growth-s0.7", 10]],
|
|
27
|
+
"all|1.1|n13-24": [["w3e:frechet-e0.5", 24], ["w3a:diffusion-wide", 24], ["w2c:zipper+stretch-b1.1", 24], ["w3d:cubu+stretch", 18]],
|
|
28
|
+
"all|1.1|n25-40": [["w2c:zipper+stretch-b1.1", 40], ["w3a:spiral-a15", 32]],
|
|
29
|
+
"all|1.1|n41-64": [],
|
|
30
|
+
"all|1.1|n65-128": [],
|
|
31
|
+
"all|1.25|n1-12": [["w3d:kdeeb-routes", 12], ["w3e:mlseb+stretch", 12], ["hw:growth", 12], ["hw:level-links", 10], ["w3b:growth-input", 10], ["w3e:confluent+stretch", 10]],
|
|
32
|
+
"all|1.25|n13-24": [["w3d:kdeeb-routes", 24], ["w3f:p-hub-a0.2", 18], ["w3d:kdeeb-nomask", 18]],
|
|
33
|
+
"all|1.25|n25-40": [],
|
|
34
|
+
"all|1.25|n41-64": [],
|
|
35
|
+
"all|1.25|n65-128": [],
|
|
36
|
+
"all|1.5|n1-12": [["w3b:growth-s0.3", 12], ["w3e:mlseb-a0", 12], ["w3e:mlseb-routes", 10], ["w3c:edge-paths-k1.5", 10], ["w3f:territories-r2", 10]],
|
|
37
|
+
"all|1.5|n13-24": [["hw:growth", 24], ["w3b:growth-input", 24]],
|
|
38
|
+
"all|1.5|n25-40": [["hw:growth", 40], ["w3b:growth-s0.7", 40]],
|
|
39
|
+
"all|1.5|n41-64": [],
|
|
40
|
+
"all|1.5|n65-128": [],
|
|
41
|
+
};
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
// The key of the portfolio router (PORTFOLIO-FIT): one guard for the router, the records and the greedy choice of the sets.
|
|
2
|
+
// A member passes the guard when it has no more breaks of each hard rule than the base, a stretch at or below the cap, and, under `collection`, no mixed segment.
|
|
3
|
+
// The base `shared` is the fallback output, so it passes the guard at each cap. The script `portfolio.ts` applies the same guard to the records.
|
|
4
|
+
import { exactStretches } from "../bench/exact/index.ts";
|
|
5
|
+
import { inkSaved } from "../bench/measures/bundles.ts";
|
|
6
|
+
import { checkProperties } from "../bench/properties.ts";
|
|
7
|
+
import { resultHash } from "../bench/screen/exercise.ts";
|
|
8
|
+
import { mixedSegments } from "../bench/screen/gap.ts";
|
|
9
|
+
import type { CompareSpec } from "../bench/screen/compare.ts";
|
|
10
|
+
import { compareSpec, measureByKey } from "../bench/screen/measures.ts";
|
|
11
|
+
import type { PortfolioKey } from "../bench/screen/portfolio-pick.ts";
|
|
12
|
+
import type { Graph, Result } from "../model.ts";
|
|
13
|
+
import type { PortfolioCap, PortfolioRule } from "./bands.ts";
|
|
14
|
+
|
|
15
|
+
/** The id of the B1 level of the key of the portfolio router. */
|
|
16
|
+
export const PORTFOLIO_B1 = "B1";
|
|
17
|
+
|
|
18
|
+
/** This function gives the spec of the B1 level: higher is better, with the `minEffect` of the row `B1.inkSaved` of the table `MEASURES`. */
|
|
19
|
+
export function portfolioB1Spec(): CompareSpec {
|
|
20
|
+
const info = measureByKey("B1.inkSaved");
|
|
21
|
+
if (info === undefined) throw new Error("The table MEASURES has no row B1.inkSaved.");
|
|
22
|
+
return compareSpec(info);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** The facts of one output that the guard reads. The router takes them from the output, and the script `portfolio.ts` from a record. */
|
|
26
|
+
export interface PortfolioFacts {
|
|
27
|
+
/** The count of the breaks of each hard rule. */
|
|
28
|
+
readonly hard: Readonly<Record<string, number>>;
|
|
29
|
+
/** The count of the edges with a stretch above each cap, by the cap. The largest stretch of a record gives 1 or 0. */
|
|
30
|
+
readonly over: (cap: number) => number;
|
|
31
|
+
/** The count of the shared segments that hold edges of two collections. */
|
|
32
|
+
readonly mixed: number;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* This function gives the count of the breaks of the guard of an output against the base, with the rule `rule` and the cap `cap`.
|
|
37
|
+
* The count is the sum of three parts:
|
|
38
|
+
* - for each hard rule, the breaks above the breaks of the base
|
|
39
|
+
* - the edges with a stretch above the cap
|
|
40
|
+
* - under the rule `collection`, the mixed segments.
|
|
41
|
+
* An output passes the guard when the count is 0.
|
|
42
|
+
*/
|
|
43
|
+
export function guardBreaks(facts: PortfolioFacts, base: PortfolioFacts | undefined, rule: PortfolioRule, cap: number): number {
|
|
44
|
+
let n = 0;
|
|
45
|
+
for (const [k, v] of Object.entries(facts.hard)) n += Math.max(0, v - (base?.hard[k] ?? 0));
|
|
46
|
+
n += facts.over(cap);
|
|
47
|
+
if (rule === "collection") n += facts.mixed;
|
|
48
|
+
return n;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** This function counts the breaks of each hard rule in a list of breaks. */
|
|
52
|
+
function countRules(breaks: readonly { readonly rule: string }[]): Record<string, number> {
|
|
53
|
+
const out: Record<string, number> = {};
|
|
54
|
+
for (const b of breaks) out[b.rule] = (out[b.rule] ?? 0) + 1;
|
|
55
|
+
return out;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* This function gives the function of the facts of the outputs on the graph `graph`. It builds the network of the stretch one time for the graph.
|
|
60
|
+
* The hard rules are the rules of `checkProperties`. The stretch is the stretch of the exact model, as the records of the gap screen give it.
|
|
61
|
+
*/
|
|
62
|
+
export function portfolioFacts(graph: Graph): (result: Result) => PortfolioFacts {
|
|
63
|
+
// The network of the stretch is built at the first call. A portfolio of the base alone does not build it.
|
|
64
|
+
let stretches: ReturnType<typeof exactStretches> | undefined;
|
|
65
|
+
return (result) => {
|
|
66
|
+
stretches ??= exactStretches(graph);
|
|
67
|
+
const values = [...stretches(result.routes).values()];
|
|
68
|
+
return {
|
|
69
|
+
hard: countRules(checkProperties(graph, result, 1_000_000)),
|
|
70
|
+
over: (cap) => values.filter((v) => v > cap + 1e-9).length,
|
|
71
|
+
mixed: mixedSegments(graph, result),
|
|
72
|
+
};
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* This function gives the key of the portfolio router on the graph `graph`, with the rule `rule` and the cap `cap`.
|
|
78
|
+
* - `hard` is `guardBreaks` against the output of the base. `base` gives that output after the base runs. The output of the base has 0 breaks.
|
|
79
|
+
* Before the base runs, or when the base throws, a member has its own breaks of the hard rules.
|
|
80
|
+
* - The one level `B1` is the ink saved (higher is better), with the `minEffect` of the table `MEASURES`.
|
|
81
|
+
* - The hash is `resultHash`.
|
|
82
|
+
* The winner is the first member with the most B1 of the members with 0 breaks (`selectBest`). The base has 0 breaks, so a member that fails the guard does not win.
|
|
83
|
+
*/
|
|
84
|
+
export function portfolioKey(graph: Graph, rule: PortfolioRule, cap: PortfolioCap, base: () => Result | undefined = () => undefined): PortfolioKey<Graph, Result> {
|
|
85
|
+
const facts = portfolioFacts(graph);
|
|
86
|
+
const spec = portfolioB1Spec();
|
|
87
|
+
let baseFacts: { readonly of: Result; readonly facts: PortfolioFacts } | undefined;
|
|
88
|
+
return {
|
|
89
|
+
hard: (_g, result) => {
|
|
90
|
+
const b = base();
|
|
91
|
+
if (b !== undefined && result === b) return 0;
|
|
92
|
+
if (b !== undefined && baseFacts?.of !== b) baseFacts = { of: b, facts: facts(b) };
|
|
93
|
+
return guardBreaks(facts(result), b === undefined ? undefined : baseFacts!.facts, rule, cap);
|
|
94
|
+
},
|
|
95
|
+
levels: [{ id: PORTFOLIO_B1, spec, value: (_g, result) => inkSaved(result.routes) }],
|
|
96
|
+
hash: resultHash,
|
|
97
|
+
};
|
|
98
|
+
}
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
// The members of the portfolio router (PORTFOLIO-API). Each member is a router strategy with the name of its records in the gap screen.
|
|
2
|
+
// A built-in highway strategy has the name `hw:<name>`, and an option of an idea wave has the name `<wave>:<name>`. A router option has its own name.
|
|
3
|
+
// The test `members.test.ts` checks that each member gives the same output hash as the strategy of the same name in the screen module of MEAS2.
|
|
4
|
+
import { ambiguityGate } from "../highways/gates/ambiguity/index.ts";
|
|
5
|
+
import { breakEvenGate } from "../highways/gates/breakeven/index.ts";
|
|
6
|
+
import { gateStrategy } from "../highways/gates/model.ts";
|
|
7
|
+
import { stretchGate } from "../highways/gates/stretch/index.ts";
|
|
8
|
+
import type { HighwayInput, HighwayPlan } from "../highways/model.ts";
|
|
9
|
+
import { highwayRouterStrategy } from "../highways/router.ts";
|
|
10
|
+
import type { Graph, Result } from "../model.ts";
|
|
11
|
+
import { cornersRouter, type CornersOptions } from "../routers/corners.ts";
|
|
12
|
+
import { sharedRouter, type SharedOptions } from "../routers/shared.ts";
|
|
13
|
+
import { byCollection, trunksRouter, type TrunksOptions } from "../routers/trunks.ts";
|
|
14
|
+
import { highwayStrategies } from "../strategies/builtins.ts";
|
|
15
|
+
import { defineStrategy, type Strategy } from "../strategies/model.ts";
|
|
16
|
+
import { sharedWaysAll } from "../bench/screen/gap.ts";
|
|
17
|
+
|
|
18
|
+
/** The name of a built-in highway strategy. */
|
|
19
|
+
type HighwayName = keyof typeof highwayStrategies;
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* The form of one member:
|
|
23
|
+
* - `["shared", options]`, `["trunks", options]` or `["corners", options]`: the router with the options. `shared` and `trunks` use the rule `byCollection`.
|
|
24
|
+
* - `["highway", name]`: the built-in highway strategy `name`, or `shared-ways-all`, with `follow`.
|
|
25
|
+
* - `["gate", name, gate, budget?]`: the built-in highway strategy with the gate `stretch`, `breakeven` or `ambiguity`. `budget` sets the budget of the gate `stretch`.
|
|
26
|
+
* - `["plan", name, options]`: the plan of the built-in highway strategy `name` with other options.
|
|
27
|
+
*/
|
|
28
|
+
export type MemberForm =
|
|
29
|
+
| readonly ["shared", Omit<SharedOptions, "name" | "rule">]
|
|
30
|
+
| readonly ["trunks", Omit<TrunksOptions, "name" | "rule">]
|
|
31
|
+
| readonly ["corners", Omit<CornersOptions, "name">]
|
|
32
|
+
| readonly ["highway", HighwayName | "shared-ways-all"]
|
|
33
|
+
| readonly ["gate", HighwayName, "stretch" | "breakeven" | "ambiguity", number?]
|
|
34
|
+
| readonly ["plan", HighwayName, Readonly<Record<string, unknown>>];
|
|
35
|
+
|
|
36
|
+
/** The forms of the members, by the names of the records. `shared` is the base. The greedy sets of `sets.ts` name only these members. */
|
|
37
|
+
export const MEMBER_FORMS: Readonly<Record<string, MemberForm>> = {
|
|
38
|
+
shared: ["shared", {}],
|
|
39
|
+
"corners-side": ["corners", { plugs: "side" }],
|
|
40
|
+
"shared-flow-a06-p3": ["shared", { cost: "flow", alpha: 0.6, passes: 3 }],
|
|
41
|
+
"shared-order-shortest": ["shared", { order: "shortest" }],
|
|
42
|
+
"shared-p2": ["shared", { passes: 2 }],
|
|
43
|
+
"shared-p3": ["shared", { passes: 3 }],
|
|
44
|
+
"shared-wind-p2-r0.5": ["shared", { passes: 2, reuse: 0.5 }],
|
|
45
|
+
"shared-wind-p2-r1": ["shared", { passes: 2, reuse: 1 }],
|
|
46
|
+
"shared-wind-p2-r1-e2": ["shared", { passes: 2, reuse: 1, reuseExponent: 2 }],
|
|
47
|
+
"shared-wind-p2-r2": ["shared", { passes: 2, reuse: 2 }],
|
|
48
|
+
"shared-wind-p3-r1": ["shared", { passes: 3, reuse: 1 }],
|
|
49
|
+
"trunks-gilbert-a03": ["trunks", { splitAt: "gilbert", alpha: 0.3 }],
|
|
50
|
+
"trunks-gilbert-a05": ["trunks", { splitAt: "gilbert", alpha: 0.5 }],
|
|
51
|
+
"trunks-gilbert-a07": ["trunks", { splitAt: "gilbert", alpha: 0.7 }],
|
|
52
|
+
"trunks-tree-bf02": ["trunks", { fan: "tree", bf: 0.2 }],
|
|
53
|
+
"trunks-tree-bf06": ["trunks", { fan: "tree", bf: 0.6 }],
|
|
54
|
+
"trunks-tree-bf08": ["trunks", { fan: "tree", bf: 0.8 }],
|
|
55
|
+
"hw:crossbars": ["highway", "crossbars"],
|
|
56
|
+
"hw:cubu": ["highway", "cubu"],
|
|
57
|
+
"hw:diffusion": ["highway", "diffusion"],
|
|
58
|
+
"hw:growth": ["highway", "growth"],
|
|
59
|
+
"hw:lanes": ["highway", "lanes"],
|
|
60
|
+
"hw:level-links": ["highway", "level-links"],
|
|
61
|
+
"hw:mesh": ["highway", "mesh"],
|
|
62
|
+
"hw:mlseb": ["highway", "mlseb"],
|
|
63
|
+
"hw:p-hub": ["highway", "p-hub"],
|
|
64
|
+
"hw:shared-ways-all": ["highway", "shared-ways-all"],
|
|
65
|
+
"hw:spiral": ["highway", "spiral"],
|
|
66
|
+
"hw:stubs": ["highway", "stubs"],
|
|
67
|
+
"hw:weiszfeld": ["highway", "weiszfeld"],
|
|
68
|
+
"hw:zipper": ["highway", "zipper"],
|
|
69
|
+
"hw:areas": ["highway", "areas"],
|
|
70
|
+
"w2a:shared-ways+ambiguity": ["gate", "shared-ways", "ambiguity"],
|
|
71
|
+
"w2a:shared-ways+breakeven": ["gate", "shared-ways", "breakeven"],
|
|
72
|
+
"w2a:shared-ways+stretch": ["gate", "shared-ways", "stretch"],
|
|
73
|
+
"w2b:weiszfeld-a0": ["plan", "weiszfeld", { alpha: 0 }],
|
|
74
|
+
"w2b:weiszfeld-a1": ["plan", "weiszfeld", { alpha: 1 }],
|
|
75
|
+
"w2b:zipper-a1": ["plan", "zipper", { adhesion: 1 }],
|
|
76
|
+
"w2b:zipper-p2": ["plan", "zipper", { passes: 2 }],
|
|
77
|
+
"w2c:level-links+stretch": ["gate", "level-links", "stretch"],
|
|
78
|
+
"w2c:level-links+stretch-b1.1": ["gate", "level-links", "stretch", 1.1],
|
|
79
|
+
"w2c:road-classes-d0.3": ["plan", "road-classes", { discount: 0.3 }],
|
|
80
|
+
"w2c:select+stretch": ["gate", "select", "stretch"],
|
|
81
|
+
"w2c:zipper+stretch-b1.1": ["gate", "zipper", "stretch", 1.1],
|
|
82
|
+
"w3a:diffusion+stretch": ["gate", "diffusion", "stretch"],
|
|
83
|
+
"w3a:diffusion-g2": ["plan", "diffusion", { gamma: 2 }],
|
|
84
|
+
"w3a:diffusion-l3": ["plan", "diffusion", { lambda: 3 }],
|
|
85
|
+
"w3a:diffusion-wide": ["plan", "diffusion", { a: 0.5, steps: 6 }],
|
|
86
|
+
"w3a:spiral-a15": ["plan", "spiral", { alpha: 15 }],
|
|
87
|
+
"w3a:spiral-a35": ["plan", "spiral", { alpha: 35 }],
|
|
88
|
+
"w3a:spiral-n3": ["plan", "spiral", { minTargets: 3 }],
|
|
89
|
+
"w3b:growth-input": ["plan", "growth", { order: "input" }],
|
|
90
|
+
"w3b:growth-s0.3": ["plan", "growth", { share: 0.3 }],
|
|
91
|
+
"w3b:growth-s0.7": ["plan", "growth", { share: 0.7 }],
|
|
92
|
+
"w3b:growth-t1.2": ["plan", "growth", { stretch: 1.2 }],
|
|
93
|
+
"w3c:edge-paths-k1.5": ["plan", "edge-paths", { distortion: 1.5 }],
|
|
94
|
+
"w3c:hdbscan+stretch": ["gate", "hdbscan", "stretch"],
|
|
95
|
+
"w3c:hough-wide": ["plan", "hough", { dTheta: 12, dRho: 24 }],
|
|
96
|
+
"w3d:cubu+stretch": ["gate", "cubu", "stretch"],
|
|
97
|
+
"w3d:kdeeb+stretch": ["gate", "kdeeb", "stretch"],
|
|
98
|
+
"w3d:kdeeb-nomask": ["plan", "kdeeb", { mask: false }],
|
|
99
|
+
"w3d:kdeeb-routes": ["plan", "kdeeb", { source: "routes" }],
|
|
100
|
+
"w3d:mesh+stretch": ["gate", "mesh", "stretch"],
|
|
101
|
+
"w3e:areas-keys": ["plan", "areas", { group: "keys" }],
|
|
102
|
+
"w3e:confluent+stretch": ["gate", "confluent", "stretch"],
|
|
103
|
+
"w3e:crossbars+stretch": ["gate", "crossbars", "stretch"],
|
|
104
|
+
"w3e:frechet+stretch": ["gate", "frechet", "stretch"],
|
|
105
|
+
"w3e:frechet-e0.5": ["plan", "frechet", { eps: 0.5 }],
|
|
106
|
+
"w3e:mlseb+stretch": ["gate", "mlseb", "stretch"],
|
|
107
|
+
"w3e:mlseb-a0": ["plan", "mlseb", { angleWeight: 0 }],
|
|
108
|
+
"w3e:mlseb-parabola": ["plan", "mlseb", { fit: "parabola" }],
|
|
109
|
+
"w3e:mlseb-routes": ["plan", "mlseb", { source: "routes" }],
|
|
110
|
+
"w3f:p-hub-a0.2": ["plan", "p-hub", { alpha: 0.2 }],
|
|
111
|
+
"w3f:spectral-c0.3": ["plan", "spectral", { c0: 0.3 }],
|
|
112
|
+
"w3f:territories-r2": ["plan", "territories", { radius: 2 }],
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
const GATES = { stretch: stretchGate, breakeven: breakEvenGate, ambiguity: ambiguityGate } as const;
|
|
116
|
+
|
|
117
|
+
/** This function gives a highway strategy with no options: it runs `run`, with the node limit `maxNodes`. As in the screen, `follow` then gets no way options. */
|
|
118
|
+
function fixedHighway(name: string, run: (input: HighwayInput) => HighwayPlan, maxNodes: number | undefined): Strategy<"highway", object> {
|
|
119
|
+
return defineStrategy({ step: "highway", name, describe: `The member ${name} of the portfolio router.`, defaults: {}, schema: {}, maxNodes: maxNodes ?? 10_000, run });
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** This function gives a router strategy with no options: it runs `route`. */
|
|
123
|
+
function fixedRouter(name: string, route: (graph: Graph) => Result): Strategy<"router", object> {
|
|
124
|
+
return defineStrategy({ step: "router", name, describe: `The member ${name} of the portfolio router.`, defaults: {}, schema: {}, maxNodes: 10_000, run: (graph: Graph) => route(graph) });
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/** This function gives the router strategy of a member form, with the name `name`. */
|
|
128
|
+
export function memberStrategy(name: string, form: MemberForm): Strategy<"router", object> {
|
|
129
|
+
const highway = (h: Strategy<"highway", object>): Strategy<"router", object> => {
|
|
130
|
+
const r = highwayRouterStrategy(h);
|
|
131
|
+
return defineStrategy({ step: "router", name, describe: r.describe, defaults: r.defaults, ...(r.schema === undefined ? {} : { schema: r.schema }), maxNodes: r.maxNodes ?? 10_000, run: r.run });
|
|
132
|
+
};
|
|
133
|
+
switch (form[0]) {
|
|
134
|
+
case "shared": {
|
|
135
|
+
const o = form[1];
|
|
136
|
+
return fixedRouter(name, (g) => sharedRouter({ ...o, rule: byCollection, name }).route(g));
|
|
137
|
+
}
|
|
138
|
+
case "trunks": {
|
|
139
|
+
const o = form[1];
|
|
140
|
+
return fixedRouter(name, (g) => trunksRouter({ ...o, rule: byCollection, name }).route(g));
|
|
141
|
+
}
|
|
142
|
+
case "corners": {
|
|
143
|
+
const o = form[1];
|
|
144
|
+
return fixedRouter(name, (g) => cornersRouter({ ...o, name }).route(g));
|
|
145
|
+
}
|
|
146
|
+
case "highway":
|
|
147
|
+
return highway(form[1] === "shared-ways-all" ? sharedWaysAll : (highwayStrategies[form[1]] as Strategy<"highway", object>));
|
|
148
|
+
case "gate": {
|
|
149
|
+
const gated = gateStrategy(highwayStrategies[form[1]] as Strategy<"highway", object>, GATES[form[2]] as Parameters<typeof gateStrategy>[1]);
|
|
150
|
+
const budget = form[3];
|
|
151
|
+
if (budget === undefined) return highway(gated);
|
|
152
|
+
const options = { ...gated.defaults, budget };
|
|
153
|
+
return highway(fixedHighway(name, (input) => gated.run(input, options), gated.maxNodes));
|
|
154
|
+
}
|
|
155
|
+
case "plan": {
|
|
156
|
+
const base = highwayStrategies[form[1]] as Strategy<"highway", object>;
|
|
157
|
+
const options = { ...base.defaults, ...form[2] };
|
|
158
|
+
return highway(fixedHighway(name, (input) => base.run(input, options), base.maxNodes));
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const cache = new Map<string, Strategy<"router", object>>();
|
|
164
|
+
|
|
165
|
+
/** This function gives the member with the name `name`, or `undefined` for a name without a form in `MEMBER_FORMS`. */
|
|
166
|
+
export function portfolioMember(name: string): Strategy<"router", object> | undefined {
|
|
167
|
+
const form = MEMBER_FORMS[name];
|
|
168
|
+
if (form === undefined) return undefined;
|
|
169
|
+
if (!cache.has(name)) cache.set(name, memberStrategy(name, form));
|
|
170
|
+
return cache.get(name);
|
|
171
|
+
}
|