@mark1russell7/highways 0.5.0 → 0.5.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.
Files changed (56) hide show
  1. package/README.md +43 -2
  2. package/build/index.d.ts +1472 -63
  3. package/build/index.d.ts.map +1 -1
  4. package/build/index.js +10007 -3406
  5. package/build/index.js.map +1 -1
  6. package/package.json +11 -2
  7. package/src/bench/exact/index.ts +72 -11
  8. package/src/bench/exact/optima-exact-v1-bounds.json +14409 -0
  9. package/src/bench/exact/optima-exact-v1.json +4809 -0
  10. package/src/bench/exact/solve.ts +129 -0
  11. package/src/bench/screen/activation.ts +3 -0
  12. package/src/bench/screen/bounds.ts +140 -0
  13. package/src/bench/screen/compare.ts +26 -0
  14. package/src/bench/screen/exact-set.ts +100 -0
  15. package/src/bench/screen/exercise.ts +290 -0
  16. package/src/bench/screen/forms.ts +14 -0
  17. package/src/bench/screen/gap.ts +316 -0
  18. package/src/bench/screen/hash.ts +7 -76
  19. package/src/bench/screen/index.ts +6 -0
  20. package/src/bench/screen/measures.ts +71 -1
  21. package/src/bench/screen/portfolio-pick.ts +20 -0
  22. package/src/bench/screen/screen.ts +50 -2
  23. package/src/bench/screen/set.ts +3 -2
  24. package/src/highways/areas/index.ts +160 -0
  25. package/src/highways/confluent/index.ts +154 -0
  26. package/src/highways/confluent/k23.test-util.ts +8 -0
  27. package/src/highways/consensus/index.ts +129 -0
  28. package/src/highways/crossbars/index.ts +184 -0
  29. package/src/highways/cubu/index.ts +26 -0
  30. package/src/highways/divided/index.ts +111 -0
  31. package/src/highways/fdeb/index.ts +193 -0
  32. package/src/highways/frechet/index.ts +232 -0
  33. package/src/highways/hierarchy/index.ts +140 -0
  34. package/src/highways/index.ts +64 -0
  35. package/src/highways/kdeeb/index.ts +355 -0
  36. package/src/highways/link-communities/index.ts +189 -0
  37. package/src/highways/mesh/index.ts +209 -0
  38. package/src/highways/mlseb/index.ts +204 -0
  39. package/src/highways/p-hub/index.ts +248 -0
  40. package/src/highways/power/index.ts +117 -0
  41. package/src/highways/resistance/index.ts +159 -0
  42. package/src/highways/sbeb/index.ts +303 -0
  43. package/src/highways/scale-space/index.ts +135 -0
  44. package/src/highways/spectral/index.ts +391 -0
  45. package/src/highways/territories/index.ts +150 -0
  46. package/src/highways/trace.ts +153 -0
  47. package/src/index.ts +1 -0
  48. package/src/portfolio/bands.ts +33 -0
  49. package/src/portfolio/fit.ts +29 -0
  50. package/src/portfolio/index.ts +6 -0
  51. package/src/portfolio/key.ts +98 -0
  52. package/src/portfolio/members.ts +171 -0
  53. package/src/portfolio/router.ts +133 -0
  54. package/src/portfolio/sets.ts +28 -0
  55. package/src/strategies/builtins.ts +222 -1
  56. 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
@@ -32,3 +32,4 @@ export * from "./bench/screen/index.ts";
32
32
  export * from "./highways/index.ts";
33
33
  export * from "./order/index.ts";
34
34
  export * from "./repair/index.ts";
35
+ export * from "./portfolio/index.ts";
@@ -0,0 +1,33 @@
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
+ /** The size bands: up to 12 nodes, 13 to 24 nodes, and 25 to 40 nodes. An input above 40 nodes uses the band `n25-40`. */
24
+ export const PORTFOLIO_BANDS: readonly PortfolioBand[] = [
25
+ { id: "n1-12", maxNodes: 12 },
26
+ { id: "n13-24", maxNodes: 24 },
27
+ { id: "n25-40", maxNodes: 40 },
28
+ ];
29
+
30
+ /** This function gives the band of a node count: the first band whose `maxNodes` is the count or more, else the last band. */
31
+ export function portfolioBand(nodes: number, bands: readonly PortfolioBand[] = PORTFOLIO_BANDS): PortfolioBand {
32
+ return bands.find((b) => nodes <= b.maxNodes) ?? bands[bands.length - 1]!;
33
+ }
@@ -0,0 +1,29 @@
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 40658558076 (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 40 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], ["corners-side", 24], ["shared-p2", 24], ["w2c:zipper+stretch-b1.1", 18], ["trunks-tree-bf08", 18], ["hw:weiszfeld", 18]],
13
+ "collection|1.1|n25-40": [["w2a:shared-ways+breakeven", 32], ["w2b:weiszfeld-a1", 32]],
14
+ "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]],
15
+ "collection|1.25|n13-24": [["w2a:shared-ways+stretch", 24], ["shared-p3", 24], ["trunks-gilbert-a07", 24], ["w3e:crossbars+stretch", 24], ["trunks-tree-bf06", 18], ["w3c:hdbscan+stretch", 18], ["shared-wind-p2-r1-e2", 18], ["hw:stubs", 18]],
16
+ "collection|1.25|n25-40": [["w2a:shared-ways+stretch", 32], ["shared-p3", 32], ["shared-order-shortest", 32]],
17
+ "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]],
18
+ "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", 18], ["shared-wind-p3-r1", 18], ["trunks-gilbert-a07", 18]],
19
+ "collection|1.5|n25-40": [["shared-p3", 40], ["hw:lanes", 32], ["shared-wind-p2-r1-e2", 32]],
20
+ "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", 12], ["hw:spiral", 12], ["w3b:growth-s0.7", 12]],
21
+ "all|1.1|n13-24": [["w3e:frechet-e0.5", 24], ["w3a:diffusion-wide", 24], ["w2c:zipper+stretch-b1.1", 24], ["w3d:cubu+stretch", 18]],
22
+ "all|1.1|n25-40": [["w2c:zipper+stretch-b1.1", 32], ["w3a:spiral-a15", 32]],
23
+ "all|1.25|n1-12": [["w3d:kdeeb-routes", 12], ["w3e:mlseb+stretch", 12], ["hw:growth", 12], ["hw:level-links", 12], ["w3b:growth-input", 12], ["w3e:confluent+stretch", 12], ["w3d:kdeeb-nomask", 10], ["w3a:diffusion-g2", 10]],
24
+ "all|1.25|n13-24": [["w3d:kdeeb-routes", 24], ["w3e:mlseb+stretch", 18]],
25
+ "all|1.25|n25-40": [["w3d:kdeeb-routes", 32]],
26
+ "all|1.5|n1-12": [["w3b:growth-s0.3", 12], ["w3e:mlseb-a0", 12], ["w3e:mlseb-routes", 10], ["w3c:edge-paths-k1.5", 10]],
27
+ "all|1.5|n13-24": [["hw:growth", 24], ["w3b:growth-input", 24]],
28
+ "all|1.5|n25-40": [["hw:growth", 40], ["w3b:growth-s0.7", 32], ["w3b:growth-s0.3", 32]],
29
+ };
@@ -0,0 +1,6 @@
1
+ export * from "./bands.ts";
2
+ export * from "./fit.ts";
3
+ export * from "./key.ts";
4
+ export * from "./members.ts";
5
+ export * from "./router.ts";
6
+ export * from "./sets.ts";
@@ -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
+ }
@@ -0,0 +1,133 @@
1
+ // The portfolio router (PORTFOLIO-API, PORTFOLIO-FIT): it runs the base `shared` and a small greedy set of members on an input, and gives the output with the most
2
+ // ink saved inside the rule and the stretch cap of the caller. The combinator is `portfolio` of `@mark1russell7/layout-contract`.
3
+ // The router is opt-in: it is not in the registry `STRATEGIES`, so no default output changes.
4
+ import { pickMoves, portfolio, type Member, type PortfolioTrace } from "../bench/screen/portfolio-pick.ts";
5
+ import type { Graph, Result, Router } from "../model.ts";
6
+ import { asRouter, defineStrategy, type Strategy } from "../strategies/model.ts";
7
+ import { PORTFOLIO_CAPS, PORTFOLIO_KS_API, PORTFOLIO_RULES, portfolioBand, type PortfolioCap, type PortfolioRule } from "./bands.ts";
8
+ import { PORTFOLIO_FIT } from "./fit.ts";
9
+ import { portfolioKey } from "./key.ts";
10
+ import { portfolioMember } from "./members.ts";
11
+ import { PORTFOLIO_SETS } from "./sets.ts";
12
+
13
+ /** The options of the portfolio router. */
14
+ export interface PortfolioRouterOptions {
15
+ /** The rule of the caller. Under `collection` (the default), a shared segment must hold edges of one collection only. Under `all`, collections can mix. */
16
+ readonly rule?: PortfolioRule;
17
+ /** The stretch cap of the caller: 1.1, 1.25 or 1.5 (the default). An output with an edge above the cap does not win. The base is the fallback output. */
18
+ readonly cap?: PortfolioCap;
19
+ /**
20
+ * The count of members after the base: 3, 5 or 8. Without `k` (the default), the router runs the fit of `fit.ts`.
21
+ * The fit gives a member count for each rule, cap and band, and a node limit for each member. Thus the default is deterministic.
22
+ */
23
+ readonly k?: 3 | 5 | 8;
24
+ /**
25
+ * A time limit of the pick in milliseconds, as a ceiling that the caller sets. The base always runs. The combinator cuts the members that start after the limit.
26
+ * A cut by the budget makes the output depend on the speed of the machine. Then the trace has `deterministic: false`.
27
+ */
28
+ readonly budgetMs?: number;
29
+ /** The largest count of members that run, with the base. */
30
+ readonly maxMembers?: number;
31
+ /** The clock in milliseconds. The default is `performance.now`. */
32
+ readonly now?: () => number;
33
+ }
34
+
35
+ /** The base member of each portfolio: the router `shared` with the rule `byCollection`. It is the fallback output above the cap. */
36
+ export const PORTFOLIO_BASE_MEMBER = "shared";
37
+
38
+ /** The result of the portfolio router: the output of the winner, the winner, the trace of the pick, the move counts and the band. */
39
+ export interface PortfolioRoute {
40
+ readonly result: Result;
41
+ readonly winner: string;
42
+ readonly trace: PortfolioTrace;
43
+ /** The move count of the pick (ACTIVATION): `{ "move.pick": 1 }` when the winner is not the base and has another output. */
44
+ readonly moves: Readonly<Record<string, number>>;
45
+ /** The size band of the input, from its node count. */
46
+ readonly band: string;
47
+ /** The names of the members, in their order. */
48
+ readonly members: readonly string[];
49
+ }
50
+
51
+ /** This function checks the options of the portfolio router, and gives them with their defaults. It throws a `RangeError` for a bad value. */
52
+ function checked(o: PortfolioRouterOptions): { rule: PortfolioRule; cap: PortfolioCap; k: 3 | 5 | 8 | undefined } {
53
+ const rule = o.rule ?? "collection";
54
+ const cap = o.cap ?? 1.5;
55
+ const k = o.k;
56
+ if (!PORTFOLIO_RULES.includes(rule)) throw new RangeError(`The portfolio router has the rule "${String(rule)}". It must be ${PORTFOLIO_RULES.join(" or ")}.`);
57
+ if (!PORTFOLIO_CAPS.includes(cap)) throw new RangeError(`The portfolio router has the cap ${String(cap)}. It must be ${PORTFOLIO_CAPS.join(", ")}.`);
58
+ if (k !== undefined && !PORTFOLIO_KS_API.includes(k)) throw new RangeError(`The portfolio router has the k ${String(k)}. It must be ${PORTFOLIO_KS_API.join(", ")}.`);
59
+ return { rule, cap, k };
60
+ }
61
+
62
+ /** This function gives the name of the portfolio router with the options `options`: `portfolio-<rule>-<cap>-k<k>`, or `portfolio-<rule>-<cap>-fit` without `k`. */
63
+ export function portfolioName(options: PortfolioRouterOptions = {}): string {
64
+ const { rule, cap, k } = checked(options);
65
+ return `portfolio-${rule}-${cap}-${k === undefined ? "fit" : `k${k}`}`;
66
+ }
67
+
68
+ /**
69
+ * This function gives the names of the members for an input with `nodes` nodes. The base comes first.
70
+ * - With `k`, the first `k` names of the set of the band follow.
71
+ * - Without `k`, the members of the fit of the band follow, each only when `nodes` is at or below its node limit.
72
+ */
73
+ export function portfolioMembers(nodes: number, options: PortfolioRouterOptions = {}): string[] {
74
+ const { rule, cap, k } = checked(options);
75
+ const key: `${PortfolioRule}|${PortfolioCap}|${string}` = `${rule}|${cap}|${portfolioBand(nodes).id}`;
76
+ if (k === undefined) return [PORTFOLIO_BASE_MEMBER, ...(PORTFOLIO_FIT[key] ?? []).filter(([s, limit]) => s !== PORTFOLIO_BASE_MEMBER && nodes <= limit).map(([s]) => s)];
77
+ const set = PORTFOLIO_SETS[key] ?? [];
78
+ return [PORTFOLIO_BASE_MEMBER, ...set.filter((s) => s !== PORTFOLIO_BASE_MEMBER).slice(0, k)];
79
+ }
80
+
81
+ /**
82
+ * This function routes the graph `graph` with the portfolio of its size band, rule and cap. The base runs first, then the members in their order.
83
+ * The key is `portfolioKey`: a member wins only when it passes the guard against the base. Of those members, the winner has the most ink saved.
84
+ * A tie within the `minEffect` of B1 goes to the first member (`selectBest`). A member that throws does not win.
85
+ * The result carries the trace of the pick and the move count `move.pick`.
86
+ */
87
+ export function routePortfolio(graph: Graph, options: PortfolioRouterOptions = {}): PortfolioRoute {
88
+ // A member with a node limit below the size of the graph does not run. The base `shared` has no such limit below the limit of the router.
89
+ const names = portfolioMembers(graph.nodes.length, options).filter((name) => name === PORTFOLIO_BASE_MEMBER || (portfolioMember(name)?.maxNodes ?? Infinity) >= graph.nodes.length);
90
+ const { rule, cap } = checked(options);
91
+ // The key compares each member with the output of the base, so the run of the base keeps its output.
92
+ let base: Result | undefined;
93
+ const members: Member<Graph, Result>[] = names.map((name) => {
94
+ const s = portfolioMember(name);
95
+ if (s === undefined) throw new Error(`The portfolio router has no member "${name}".`);
96
+ const router = asRouter(s);
97
+ if (name !== PORTFOLIO_BASE_MEMBER) return { id: name, run: (g: Graph) => router.route(g) };
98
+ return {
99
+ id: name,
100
+ run: (g: Graph) => {
101
+ base = router.route(g);
102
+ return base;
103
+ },
104
+ };
105
+ });
106
+ const p = portfolio(portfolioName(options), members, portfolioKey(graph, rule, cap, () => base), {
107
+ ...(options.budgetMs === undefined ? {} : { budgetMs: options.budgetMs }),
108
+ ...(options.maxMembers === undefined ? {} : { maxMembers: options.maxMembers }),
109
+ ...(options.now === undefined ? {} : { now: options.now }),
110
+ });
111
+ const pick = p.pick(graph);
112
+ const moves = Object.fromEntries(Object.entries(pickMoves(pick.trace)).map(([k, v]) => [`move.${k}`, v]));
113
+ return { result: pick.output, winner: pick.winner, trace: pick.trace, moves, band: portfolioBand(graph.nodes.length).id, members: names };
114
+ }
115
+
116
+ /** This function gives a router that routes each graph with `routePortfolio` and the options `options`. Its name is `portfolioName(options)`. */
117
+ export function portfolioRouter(options: PortfolioRouterOptions = {}): Router {
118
+ checked(options);
119
+ return { name: portfolioName(options), maxNodes: 10_000, route: (graph) => routePortfolio(graph, options).result };
120
+ }
121
+
122
+ /**
123
+ * 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.
124
+ * A caller adds it to a registry of its own, or calls `routePortfolio` for the trace.
125
+ */
126
+ export const portfolioStrategy: Strategy<"router", PortfolioRouterOptions> = defineStrategy<"router", PortfolioRouterOptions>({
127
+ step: "router",
128
+ name: "portfolio",
129
+ 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.",
130
+ defaults: { rule: "collection", cap: 1.5 },
131
+ maxNodes: 10_000,
132
+ run: (graph, options) => routePortfolio(graph, options).result,
133
+ });