@mark1russell7/highways 0.4.2 → 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 (78) hide show
  1. package/README.md +59 -2
  2. package/build/index.d.ts +2333 -193
  3. package/build/index.d.ts.map +1 -1
  4. package/build/index.js +13042 -2334
  5. package/build/index.js.map +1 -1
  6. package/package.json +12 -1
  7. package/src/bench/exact/index.ts +258 -0
  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/measures/index.ts +1 -0
  12. package/src/bench/measures/lanes.ts +304 -0
  13. package/src/bench/measures/levels.ts +86 -2
  14. package/src/bench/screen/activation.ts +3 -0
  15. package/src/bench/screen/bounds.ts +140 -0
  16. package/src/bench/screen/compare.ts +26 -0
  17. package/src/bench/screen/exact-set.ts +100 -0
  18. package/src/bench/screen/exercise.ts +290 -0
  19. package/src/bench/screen/forms.ts +25 -5
  20. package/src/bench/screen/gap.ts +316 -0
  21. package/src/bench/screen/hash.ts +7 -76
  22. package/src/bench/screen/index.ts +6 -0
  23. package/src/bench/screen/measures.ts +89 -1
  24. package/src/bench/screen/portfolio-pick.ts +20 -0
  25. package/src/bench/screen/screen.ts +78 -2
  26. package/src/bench/screen/set.ts +3 -2
  27. package/src/channels.ts +56 -0
  28. package/src/highways/areas/index.ts +160 -0
  29. package/src/highways/confluent/index.ts +154 -0
  30. package/src/highways/confluent/k23.test-util.ts +8 -0
  31. package/src/highways/consensus/index.ts +129 -0
  32. package/src/highways/crossbars/index.ts +184 -0
  33. package/src/highways/cubu/index.ts +26 -0
  34. package/src/highways/diffusion/index.ts +103 -0
  35. package/src/highways/divided/index.ts +111 -0
  36. package/src/highways/edge-paths/index.ts +155 -0
  37. package/src/highways/fdeb/index.ts +193 -0
  38. package/src/highways/fixtures.test-util.ts +71 -0
  39. package/src/highways/flow-clusters/index.ts +143 -0
  40. package/src/highways/frechet/index.ts +232 -0
  41. package/src/highways/growth/index.ts +148 -0
  42. package/src/highways/hdbscan/index.ts +250 -0
  43. package/src/highways/hierarchy/index.ts +140 -0
  44. package/src/highways/hough/index.ts +182 -0
  45. package/src/highways/hub-arcs/index.ts +181 -0
  46. package/src/highways/index.ts +79 -0
  47. package/src/highways/kdeeb/index.ts +355 -0
  48. package/src/highways/lanes/index.ts +189 -0
  49. package/src/highways/link-communities/index.ts +189 -0
  50. package/src/highways/mesh/index.ts +209 -0
  51. package/src/highways/mingle/index.ts +171 -0
  52. package/src/highways/mlseb/index.ts +204 -0
  53. package/src/highways/nested/index.ts +143 -0
  54. package/src/highways/p-hub/index.ts +248 -0
  55. package/src/highways/power/index.ts +117 -0
  56. package/src/highways/resistance/index.ts +159 -0
  57. package/src/highways/ridges/index.ts +328 -0
  58. package/src/highways/sbeb/index.ts +303 -0
  59. package/src/highways/scale-space/index.ts +135 -0
  60. package/src/highways/snap.ts +110 -0
  61. package/src/highways/soft-keys/index.ts +114 -0
  62. package/src/highways/spectral/index.ts +391 -0
  63. package/src/highways/spiral/index.ts +205 -0
  64. package/src/highways/territories/index.ts +150 -0
  65. package/src/highways/trace.ts +153 -0
  66. package/src/highways/traclus/index.ts +296 -0
  67. package/src/index.ts +2 -0
  68. package/src/portfolio/bands.ts +33 -0
  69. package/src/portfolio/fit.ts +29 -0
  70. package/src/portfolio/index.ts +6 -0
  71. package/src/portfolio/key.ts +98 -0
  72. package/src/portfolio/members.ts +171 -0
  73. package/src/portfolio/router.ts +133 -0
  74. package/src/portfolio/sets.ts +28 -0
  75. package/src/routers/shared.ts +56 -3
  76. package/src/strategies/builtins.ts +376 -1
  77. package/src/strategies/schemas.ts +460 -1
  78. package/src/visibility.ts +5 -0
@@ -0,0 +1,26 @@
1
+ // The compare of one measure (WIN-RES). `compareMeasure` and `minEffectOf` come from `@mark1russell7/layout-contract` (PORTFOLIO-FIT).
2
+ // `docs/PROMOTION.md` gives the rule. The test `compare.test.ts` checks that the functions agree with the note.
3
+ import type { MeasureSpec, Verdict } from "@mark1russell7/layout-contract";
4
+
5
+ export { compareMeasure, minEffectOf } from "@mark1russell7/layout-contract";
6
+
7
+ /**
8
+ * The smallest change of a measure that counts as better or worse (WIN-RES).
9
+ * A number is an absolute change. `{ rel }` is a share of the larger of the two absolute values.
10
+ * The threshold is not less than `RECORD_EPS` at any time.
11
+ */
12
+ export type MinEffect = NonNullable<MeasureSpec["minEffect"]>;
13
+
14
+ /** The result of a compare of a candidate with a baseline on one measure. A tie is not a win. It is the `Verdict` of the contract. */
15
+ export type MeasureCompare = Verdict;
16
+
17
+ /** The part of a measure that a compare reads: the better side and the minimum effect. */
18
+ export type CompareSpec = Pick<MeasureSpec, "direction" | "minEffect">;
19
+
20
+ /**
21
+ * This function applies the wave rule to the compares of one board, for example of `B1` and of the `B3` q95.
22
+ * The board is a win only when no compare is `worse` and one or more compares are `better`. Thus a board of ties is not a win.
23
+ */
24
+ export function boardWins(compares: readonly MeasureCompare[]): boolean {
25
+ return !compares.includes("worse") && compares.includes("better");
26
+ }
@@ -0,0 +1,100 @@
1
+ import { makeFixture, type FixtureKind } from "../../fixtures.ts";
2
+ import type { Graph } from "../../model.ts";
3
+ import type { LayoutCell, LayoutGenerator } from "./forms.ts";
4
+ import type { ScreenFamily, ScreenSet } from "./set.ts";
5
+
6
+ /** The board classes of the small boards with a known optimum (MEAS2). */
7
+ export type ExactClass = "uniform" | "clustered" | "hubs" | "fanout";
8
+
9
+ /** The board classes of `EXACT_V1`, in the order of the set. */
10
+ export const EXACT_CLASSES: readonly ExactClass[] = ["uniform", "clustered", "hubs", "fanout"];
11
+
12
+ /** The edges of each node of a small board with a known optimum. */
13
+ export const EXACT_EDGES_PER_NODE = 1.5;
14
+
15
+ /** The fixture kind of each class. The class `fanout` is the kind `collections`: each cluster is one collection, with fan-outs. */
16
+ const KINDS: Readonly<Record<ExactClass, FixtureKind>> = { uniform: "uniform", clustered: "clustered", hubs: "hubs", fanout: "collections" };
17
+
18
+ /** The cell of each class in the taxonomy of the corpus. */
19
+ const CELLS: Readonly<Record<ExactClass, LayoutCell>> = {
20
+ uniform: { edgeShape: "random" },
21
+ clustered: { edgeShape: "clustered" },
22
+ hubs: { edgeShape: "hubChain" },
23
+ fanout: { edgeShape: "bundled" },
24
+ };
25
+
26
+ /** This function gives the seed of the fixture of the member `k` of a class: `100 × (the index of the class + 1) + k`. */
27
+ export function exactSeed(cls: ExactClass, k: number): number {
28
+ return 100 * (EXACT_CLASSES.indexOf(cls) + 1) + k;
29
+ }
30
+
31
+ /** This function gives the generator of a small board: the generator `fixture` with the kind of the class, `nodes` and the seed of `exactSeed`. */
32
+ export function exactGenerator(cls: ExactClass, nodes: number, k: number): LayoutGenerator {
33
+ return { name: "fixture", version: 1, params: { kind: KINDS[cls], nodes, edgesPerNode: EXACT_EDGES_PER_NODE }, seed: exactSeed(cls, k) };
34
+ }
35
+
36
+ /** This function makes the small board of a class with `nodes` nodes and the member `k`. The same values always give the same graph. */
37
+ export function exactBoard(cls: ExactClass, nodes: number, k: number): Graph {
38
+ return makeFixture({ name: cls, kind: KINDS[cls], nodes, edgesPerNode: EXACT_EDGES_PER_NODE, seed: exactSeed(cls, k) });
39
+ }
40
+
41
+ /**
42
+ * This function gives a family of small boards of one class. The member `k` has `sizes[k mod sizes.length]` nodes.
43
+ * The count of the cell is `10`, because each board has fewer than 100 nodes.
44
+ */
45
+ export function exactFamily(cls: ExactClass, sizes: readonly number[], seeds: number): ScreenFamily {
46
+ const nodes = (k: number): number => sizes[k % sizes.length]!;
47
+ return {
48
+ name: cls,
49
+ shows: `Small boards of the class ${cls} with ${sizes.join(", ")} nodes and a known optimum.`,
50
+ seeds,
51
+ cell: { containerSize: "freeAspect", containerShape: "rect", obstacles: "none", mode: "offline", groups: "none", count: "10", edgeDensity: "medium", ports: "free", ...CELLS[cls] },
52
+ make: (k) => exactBoard(cls, nodes(k), k),
53
+ generator: (k) => exactGenerator(cls, nodes(k), k),
54
+ };
55
+ }
56
+
57
+ /**
58
+ * The node counts of the boards of `EXACT_V1`. The member `k` of each class has `EXACT_V1_SIZES[k mod length]` nodes.
59
+ * The size ladder of MEAS2 (CI run 40613531631) proved each optimum at 10 nodes within 300 s, for each class, both rules and the cap 1.5.
60
+ * At 14 nodes, 4 of 24 solves stopped at the time limit.
61
+ */
62
+ export const EXACT_V1_SIZES: readonly number[] = [10];
63
+
64
+ /** The number of seeds of each class of `EXACT_V1`. It is `cellMin` of the promotion rule. */
65
+ export const EXACT_V1_SEEDS = 10;
66
+
67
+ /**
68
+ * The screen set `exact-v1` (MEAS2): small boards of made-up data with a known optimum of the exact model.
69
+ * It has 4 classes of 10 seeds. The file `optima-exact-v1.json` of `bench/exact` keeps the optimum of each input, each rule and each cap.
70
+ * This set does not change. The tests compare it with the files in `corpus/exact-v1`.
71
+ */
72
+ export const EXACT_V1: ScreenSet = {
73
+ id: "exact-v1",
74
+ families: EXACT_CLASSES.map((cls) => exactFamily(cls, EXACT_V1_SIZES, EXACT_V1_SEEDS)),
75
+ };
76
+
77
+ /** The node counts of the boards of `EXACT_V1_BOUNDS`. HiGHS proves few optima at these sizes within 300 s, so the set keeps bounds (EXACT-BOUNDS). */
78
+ export const EXACT_V1_BOUNDS_SIZES: readonly number[] = [18, 24, 40];
79
+
80
+ /** This function gives the name of the family of a class at a size in `EXACT_V1_BOUNDS`: `<class>-n<nodes>`. */
81
+ export function boundsFamilyName(cls: ExactClass, nodes: number): string {
82
+ return `${cls}-n${nodes}`;
83
+ }
84
+
85
+ /**
86
+ * The screen set `exact-v1-bounds` (EXACT-BOUNDS): the boards of the classes of `exact-v1` at 18, 24 and 40 nodes, with 10 seeds each.
87
+ * Each family is one class at one size. The member `k` uses the seed `exactSeed(class, k)`, as in `exact-v1`.
88
+ * The file `optima-exact-v1-bounds.json` of `bench/exact` keeps the best bound and the best solution of each input, each rule and each cap.
89
+ * This set does not change. The tests compare it with the files in `corpus/exact-v1-bounds`.
90
+ */
91
+ export const EXACT_V1_BOUNDS: ScreenSet = {
92
+ id: "exact-v1-bounds",
93
+ families: EXACT_V1_BOUNDS_SIZES.flatMap((n) =>
94
+ EXACT_CLASSES.map((cls) => ({
95
+ ...exactFamily(cls, [n], EXACT_V1_SEEDS),
96
+ name: boundsFamilyName(cls, n),
97
+ shows: `Boards of the class ${cls} with ${n} nodes, with a bound of the optimum.`,
98
+ })),
99
+ ),
100
+ };
@@ -0,0 +1,290 @@
1
+ // The activation test of the screen of `highways` (ACTIVATION): the highways form of an output, the own move of each strategy,
2
+ // the base of each strategy, and the report of the candidates that are not exercised.
3
+ // `activation.ts` holds the functions without the types of `highways`. This file holds the parts of `highways`.
4
+ import type { CurveSegment } from "../../curves/model.ts";
5
+ import type { HighwayPlan } from "../../highways/model.ts";
6
+ import type { Graph, Result } from "../../model.ts";
7
+ import { highwayStrategies } from "../../strategies/builtins.ts";
8
+ import type { Strategy } from "../../strategies/model.ts";
9
+ import { exercised, outputGroups, outputHash, type OutputGroup } from "./activation.ts";
10
+ import type { LayoutRecord } from "./forms.ts";
11
+ import { RECORD_EPS } from "./hash.ts";
12
+
13
+ const byEdge = <T extends { readonly edge: string }>(list: readonly T[]): T[] => [...list].sort((a, b) => (a.edge < b.edge ? -1 : a.edge > b.edge ? 1 : 0));
14
+
15
+ /**
16
+ * This function gives the output hash of a result of a router, in the highways form.
17
+ * The form holds the routes in the order of the edge ids, and the levels, the bundles and the runs as the router gives them.
18
+ * Each number goes to the grid `RECORD_EPS`, as `round3` does.
19
+ */
20
+ export function resultHash(result: Result): string {
21
+ return outputHash(resultForm(result), { grid: RECORD_EPS });
22
+ }
23
+
24
+ /** This function gives the highways form of a result of a router: the value that `resultHash` hashes. */
25
+ export function resultForm(result: Result): object {
26
+ return { routes: byEdge(result.routes), levels: result.levels, bundles: result.bundles, runs: result.runs };
27
+ }
28
+
29
+ /** This function gives the output hash of the curves of the step `curve`, in the order of the edge ids. */
30
+ export function curvesHash(edges: readonly string[], curves: readonly (readonly CurveSegment[])[]): string {
31
+ return outputHash(byEdge(edges.map((edge, i) => ({ edge, segments: curves[i] ?? [] }))), { grid: RECORD_EPS });
32
+ }
33
+
34
+ /**
35
+ * The name of the own move of each built-in highway finder. The trace of a record names the count `move.<name>`.
36
+ * The count is the number of highways of the plan with 2 edges or more. A finder without an entry counts `move.highway`.
37
+ */
38
+ export const MOVE_NAMES: Readonly<Record<string, string>> = Object.freeze({
39
+ keys: "trunk",
40
+ "shared-ways": "sharedWay",
41
+ "shared-ways-all": "sharedWay",
42
+ corridors: "corridor",
43
+ quickbundles: "bundle",
44
+ "road-classes": "mainRoad",
45
+ "flow-field": "fieldLine",
46
+ wspd: "pair",
47
+ "od-cells": "cellPair",
48
+ select: "pick",
49
+ mingle: "merge",
50
+ spiral: "spiralTree",
51
+ "flow-clusters": "cluster",
52
+ traclus: "cluster",
53
+ diffusion: "channel",
54
+ nested: "cluster",
55
+ growth: "road",
56
+ "edge-paths": "path",
57
+ "soft-keys": "softKey",
58
+ hough: "line",
59
+ ridges: "ridge",
60
+ hdbscan: "cluster",
61
+ "hub-arcs": "hubArc",
62
+ hierarchy: "bundle",
63
+ fdeb: "bundle",
64
+ mesh: "meshRoad",
65
+ kdeeb: "bundle",
66
+ cubu: "bundle",
67
+ mlseb: "bundle",
68
+ sbeb: "bundle",
69
+ areas: "area",
70
+ confluent: "track",
71
+ power: "powerEdge",
72
+ frechet: "cluster",
73
+ spectral: "cluster",
74
+ resistance: "cluster",
75
+ "link-communities": "community",
76
+ territories: "territory",
77
+ consensus: "cluster",
78
+ "p-hub": "hubLink",
79
+ "scale-space": "cluster",
80
+ });
81
+
82
+ /**
83
+ * The highway strategies that change the plan of another strategy (`from`). Their own move is a change of that plan, so they count no move.
84
+ * The output against the plan `from` tells if they ran.
85
+ */
86
+ export const PLAN_CHANGERS: readonly string[] = Object.freeze(["crossbars", "divided", "lanes", "level-links", "meet", "stubs", "weiszfeld", "zipper"]);
87
+
88
+ const FINDERS: readonly string[] = [...new Set([...Object.keys(highwayStrategies).filter((n) => !n.includes("+")), "shared-ways-all"])].sort((a, b) => b.length - a.length);
89
+
90
+ /** This function gives the name of a strategy without the prefix of a module, for example `spectral-c0.3` for `w3f:spectral-c0.3`. */
91
+ function localName(name: string): string {
92
+ const colon = name.indexOf(":");
93
+ const local = colon >= 0 ? name.slice(colon + 1) : name;
94
+ // A wave can give its own variant a name with the wave, for example `w3f-nested-l1`.
95
+ return local.replace(/^w\d[a-z]+-/, "");
96
+ }
97
+
98
+ /**
99
+ * This function gives the built-in highway strategy of a name: the strategy itself, or the strategy of a variant, for example `power` for `power-near`.
100
+ * It gives `undefined` for a gate (a name with `+`) and for a name of another step.
101
+ */
102
+ export function finderOf(name: string): string | undefined {
103
+ const local = localName(name);
104
+ if (local.includes("+")) return undefined;
105
+ return FINDERS.find((f) => local === f || local.startsWith(`${f}-`));
106
+ }
107
+
108
+ /**
109
+ * This function gives the count of the own move of a highway strategy on its plan, for example `{ "move.powerEdge": 3 }`.
110
+ * It gives `undefined` for a gate and for a strategy of `PLAN_CHANGERS`.
111
+ */
112
+ export function planMoves(name: string, plan: HighwayPlan): Record<string, number> | undefined {
113
+ const finder = finderOf(name);
114
+ if (finder === undefined || PLAN_CHANGERS.includes(finder)) return undefined;
115
+ return { [`move.${MOVE_NAMES[finder] ?? "highway"}`]: plan.highways.filter((h) => h.members.length >= 2).length };
116
+ }
117
+
118
+ /** A function that gives the own move counts of a strategy on a graph, or `undefined` for a strategy without a count. */
119
+ export type MoveCounter = (strategy: string, graph: Graph) => Readonly<Record<string, number>> | undefined;
120
+
121
+ /** This function gives a `MoveCounter` for highway strategies, by the names of the records. It makes the plan of the strategy again. */
122
+ export function highwayMoves(strategies: Readonly<Record<string, Strategy<"highway", object>>>): MoveCounter {
123
+ return (name, graph) => {
124
+ const s = strategies[name];
125
+ return s === undefined ? undefined : planMoves(name, s.run({ graph }, s.defaults));
126
+ };
127
+ }
128
+
129
+ /**
130
+ * This function gives the base of a strategy for the fallback test: the strategy that it falls back to or extends.
131
+ * - A gate `X+G` and a wrapped router `X+W` have the base `X`.
132
+ * - A highway finder of a module (`hw:power`, `w3e:power-near`) has the base `hw:none`, the empty plan.
133
+ * - A plan changer has the base of its plan `from`: `hw:shared-ways`, or `hw:corridors` and `hw:none` for a name that ends with `-corridors` and `-none`.
134
+ * - An option of a router (`shared-cap-3`, `shared-all-p3`, `trunks-tree-bf02`, `corners-side`) has the base `shared`, `shared-all`, `trunks` or `corners`.
135
+ * The function gives `undefined` when the base is not in `known`.
136
+ */
137
+ export function activationBase(name: string, known: ReadonlySet<string>): string | undefined {
138
+ const pick = (...names: string[]): string | undefined => names.find((n) => n !== name && known.has(n));
139
+ const colon = name.indexOf(":");
140
+ if (colon >= 0) {
141
+ const prefix = name.slice(0, colon);
142
+ const local = name.slice(colon + 1);
143
+ if (local.includes("+")) {
144
+ const inner = local.slice(0, local.lastIndexOf("+"));
145
+ return pick(`${prefix}:${inner}`, `hw:${inner}`);
146
+ }
147
+ if (local === "none") return undefined;
148
+ const finder = finderOf(name);
149
+ if (finder !== undefined && PLAN_CHANGERS.includes(finder)) {
150
+ const from = local.endsWith("-corridors") ? "corridors" : local.endsWith("-none") ? "none" : "shared-ways";
151
+ return pick(`hw:${from}`);
152
+ }
153
+ return pick("hw:none");
154
+ }
155
+ if (name.includes("+")) return pick(name.slice(0, name.lastIndexOf("+")));
156
+ for (const router of ["shared-all", "shared", "trunks", "corners"]) if (name.startsWith(`${router}-`)) return pick(router);
157
+ return undefined;
158
+ }
159
+
160
+ /** The activation of one strategy over the decision inputs of a screen. */
161
+ export interface ActivationRow {
162
+ readonly strategy: string;
163
+ /** The base of the fallback test, or `undefined` when the strategy has none. */
164
+ readonly base?: string;
165
+ /** The decision inputs where the strategy ran. */
166
+ readonly inputs: number;
167
+ /** The decision inputs where a move count is above 0. */
168
+ readonly moved: readonly string[];
169
+ /** The decision inputs where the output differs from the output of the base. */
170
+ readonly differs: readonly string[];
171
+ /** The sum of each move count over the decision inputs. */
172
+ readonly moves: Readonly<Record<string, number>>;
173
+ readonly verdict: "exercised" | "not-exercised";
174
+ /** The id of the output group of the strategy on the decision inputs. */
175
+ readonly group: string;
176
+ }
177
+
178
+ /** The groups of the strategies with equal output on each input of one cell (a family of boards). They are diagnostics only. */
179
+ export interface CellGroups {
180
+ readonly cell: string;
181
+ readonly groups: readonly OutputGroup[];
182
+ }
183
+
184
+ /** The activation report of a screen. */
185
+ export interface ActivationReport {
186
+ /** The decision inputs, in sorted order. */
187
+ readonly inputs: readonly string[];
188
+ readonly rows: readonly ActivationRow[];
189
+ /** The output groups with more than one strategy: equal output hashes on each decision input. */
190
+ readonly groups: readonly OutputGroup[];
191
+ /** The cell groups with more than one strategy, for each family of the decision inputs. */
192
+ readonly cells: readonly CellGroups[];
193
+ }
194
+
195
+ /** This function gives the family of an input id, for example `small-clustered` for `screen-v1/small-clustered/s0`. */
196
+ export function familyOf(input: string): string {
197
+ const parts = input.split("/");
198
+ return parts.length >= 3 ? parts[parts.length - 2]! : input;
199
+ }
200
+
201
+ /** This function gives the move counts of a record, from its trace. */
202
+ export function recordMoves(record: LayoutRecord): Record<string, number> {
203
+ const out: Record<string, number> = {};
204
+ for (const t of record.trace ?? []) for (const [k, v] of Object.entries(t.measures ?? {})) if (k.startsWith("move.")) out[k] = (out[k] ?? 0) + v;
205
+ return out;
206
+ }
207
+
208
+ /**
209
+ * This function gives the activation report of the records of a screen.
210
+ * A strategy is exercised when a move count is above 0 on a decision input.
211
+ * It is also exercised when its output differs from the output of its base on a decision input.
212
+ * The decision inputs are the inputs of the families `families`, or each input. `bases` gives the base of each strategy, or `activationBase` gives it.
213
+ */
214
+ export function activationReport(
215
+ records: readonly LayoutRecord[],
216
+ options: { readonly families?: readonly string[]; readonly bases?: Readonly<Record<string, string>> } = {},
217
+ ): ActivationReport {
218
+ const decision = records.filter((r) => options.families?.includes(familyOf(r.input)) ?? true);
219
+ const inputs = [...new Set(decision.map((r) => r.input))].sort();
220
+ const names = [...new Set(decision.map((r) => r.strategy))];
221
+ const known = new Set(names);
222
+ const at = new Map(decision.map((r) => [`${r.strategy}\u0000${r.input}`, r]));
223
+ const hashOf = (strategy: string, input: string): string | undefined => {
224
+ const r = at.get(`${strategy}\u0000${input}`);
225
+ return r === undefined || r.skipped !== undefined ? undefined : r.outputHash;
226
+ };
227
+ const list = (strategy: string, of: readonly string[]): string[] => of.map((i) => hashOf(strategy, i) ?? "-");
228
+ const all = outputGroups(Object.fromEntries(names.map((n) => [n, list(n, inputs)])));
229
+ const groupOf = new Map(all.flatMap((g) => g.members.map((m) => [m, g.id] as const)));
230
+ const rows = names.map((strategy): ActivationRow => {
231
+ const base = options.bases?.[strategy] ?? activationBase(strategy, known);
232
+ const moved: string[] = [];
233
+ const differs: string[] = [];
234
+ const moves: Record<string, number> = {};
235
+ let ran = 0;
236
+ for (const input of inputs) {
237
+ const r = at.get(`${strategy}\u0000${input}`);
238
+ if (r === undefined || r.skipped !== undefined) continue;
239
+ ran++;
240
+ const m = recordMoves(r);
241
+ for (const [k, v] of Object.entries(m)) moves[k] = (moves[k] ?? 0) + v;
242
+ if (exercised({ moves: m })) moved.push(input);
243
+ const own = hashOf(strategy, input);
244
+ const theirs = base === undefined ? undefined : hashOf(base, input);
245
+ // A skipped run of the base has no hash. Then the input gives no evidence of a difference.
246
+ if (own !== undefined && theirs !== undefined && exercised({ hashes: [own], baseHashes: [theirs] })) differs.push(input);
247
+ }
248
+ const verdict = moved.length > 0 || differs.length > 0 ? "exercised" : "not-exercised";
249
+ return { strategy, ...(base === undefined ? {} : { base }), inputs: ran, moved, differs, moves, verdict, group: groupOf.get(strategy) ?? strategy };
250
+ });
251
+ const families = [...new Set(inputs.map(familyOf))].sort();
252
+ const cells = families.map((cell) => {
253
+ const of = inputs.filter((i) => familyOf(i) === cell);
254
+ return { cell, groups: outputGroups(Object.fromEntries(names.map((n) => [n, list(n, of)]))).filter((g) => g.members.length > 1) };
255
+ });
256
+ return { inputs, rows, groups: all.filter((g) => g.members.length > 1), cells };
257
+ }
258
+
259
+ /** This function writes the activation report as Markdown: the strategies that are not exercised, and the output groups. */
260
+ export function formatActivation(report: ActivationReport, candidates?: ReadonlySet<string>): string {
261
+ const rows = report.rows.filter((r) => candidates?.has(r.strategy) ?? true);
262
+ const silent = rows.filter((r) => r.verdict === "not-exercised");
263
+ const lines = [
264
+ "## Activation",
265
+ "",
266
+ `${report.inputs.length} decision inputs, ${rows.length} strategies. A strategy is exercised when its own move count is above 0, or its output differs from the output of its base, on one decision input or more.`,
267
+ "",
268
+ `### Not exercised (${silent.length})`,
269
+ "",
270
+ "| strategy | base | runs | moves |",
271
+ "|---|---|---:|---|",
272
+ ...silent.map((r) => `| ${r.strategy} | ${r.base ?? "-"} | ${r.inputs} | ${Object.entries(r.moves).map(([k, v]) => `${k} ${v}`).join(", ") || "-"} |`),
273
+ "",
274
+ `### Output groups (${report.groups.length})`,
275
+ "",
276
+ "The strategies of a group have equal output on each decision input. A group counts as one candidate.",
277
+ "",
278
+ ...report.groups.map((g) => `- ${g.members.join(", ")}`),
279
+ "",
280
+ "### All strategies",
281
+ "",
282
+ "| strategy | base | runs | moved | differs | moves | verdict | group |",
283
+ "|---|---|---:|---:|---:|---|---|---|",
284
+ ...rows.map(
285
+ (r) =>
286
+ `| ${r.strategy} | ${r.base ?? "-"} | ${r.inputs} | ${r.moved.length} | ${r.differs.length} | ${Object.entries(r.moves).map(([k, v]) => `${k} ${v}`).join(", ") || "-"} | ${r.verdict} | ${r.group} |`,
287
+ ),
288
+ ];
289
+ return lines.join("\n") + "\n";
290
+ }
@@ -47,8 +47,8 @@ export interface LayoutBox {
47
47
  readonly parent?: string;
48
48
  /** The group of the box. */
49
49
  readonly group?: string;
50
- /** This value is `true` when the box is an obstacle: the packer keeps its `x` and `y`. The router ignores it. */
51
- readonly locked?: true;
50
+ /** This value is `true` when the box is an obstacle: the packer keeps its `x` and `y`. `false` is the same as no value. The router ignores it. */
51
+ readonly locked?: boolean;
52
52
  /** The reading order that the caller wants. The router ignores it. */
53
53
  readonly rank?: number;
54
54
  readonly ports?: readonly LayoutPort[];
@@ -87,7 +87,7 @@ export interface LayoutGenerator {
87
87
  * The packer reads the first six dimensions. The router reads the last four.
88
88
  */
89
89
  export interface LayoutCell {
90
- readonly containerSize?: "fixed" | "freeWidth" | "freeAspect";
90
+ readonly containerSize?: "fixed" | "freeHeight" | "freeWidth" | "freeAspect" | "freeArea";
91
91
  readonly containerShape?: "rect" | "convex" | "polygon" | "circle" | "ellipse" | "holes";
92
92
  readonly obstacles?: "none" | "center" | "scattered" | "wall" | "corridors" | "touching" | "edge";
93
93
  readonly sizes?: "identical" | "uniform" | "heavyTail" | "bimodal" | "extremeAspect" | "nearIdentical";
@@ -186,6 +186,20 @@ export interface LayoutRecord {
186
186
  readonly commit?: string;
187
187
  /** The reason when the strategy did not run, for example an input over its `maxNodes`. Then the record has no measures. */
188
188
  readonly skipped?: string;
189
+ /**
190
+ * The first 16 hex characters of the SHA-256 of the output in the highways form (ACTIVATION). `resultHash` gives it for a router.
191
+ * Two records with the same hash have the same output, at the precision `RECORD_EPS`.
192
+ */
193
+ readonly outputHash?: string;
194
+ /** The facts of the stages of the run. The trace measures `move.<name>` count the own move of the strategy (ACTIVATION). */
195
+ readonly trace?: readonly RecordTrace[];
196
+ }
197
+
198
+ /** The facts of one stage of a run, as the `StageTrace` of the contract: its id, its time and its trace measures. */
199
+ export interface RecordTrace {
200
+ readonly id: string;
201
+ readonly ms?: number;
202
+ readonly measures?: Readonly<Record<string, number>>;
189
203
  }
190
204
 
191
205
  /** The comparison of a prediction. Without it, `<=` applies to a measure where lower is better, and `>=` to the other measures. */
@@ -291,19 +305,25 @@ export function regionBounds(region: Region): { x: number; y: number; w: number;
291
305
  return { x, y, w: Math.max(...xs) - x, h: Math.max(...ys) - y };
292
306
  }
293
307
 
308
+ /**
309
+ * The fields of an input that `inputGraph` reads. A contract input, for example the result of `applyPack`, has these fields.
310
+ * Without `edges`, the input has no edges.
311
+ */
312
+ export type RouteInput = Pick<LayoutInput, "id" | "boxes" | "obstacles"> & { readonly edges?: LayoutInput["edges"] };
313
+
294
314
  /**
295
315
  * This function reads the graph of the router from an input file. The router reads the positions, the ports, the edges and the data.
296
316
  * Each obstacle becomes a node without edges, with the bounding box of its region, after the boxes.
297
317
  * The router ignores the other fields of the packer: `container`, `gap`, `locked`, `rank`, the `shape` of a box and the `move` of an obstacle.
298
318
  * A box without `x` and `y` is an error.
299
319
  */
300
- export function inputGraph(input: LayoutInput): Graph {
320
+ export function inputGraph(input: RouteInput): Graph {
301
321
  const nodes = input.boxes.map((b): Node => {
302
322
  if (b.x === undefined || b.y === undefined) throw new Error(`The box "${b.id}" of the input "${input.id}" has no position. The router needs a placed input.`);
303
323
  return { id: b.id, box: { x: b.x, y: b.y, w: b.w, h: b.h }, ...(b.ports ? { ports: b.ports.map(portToModel) } : {}), ...(b.data ? { data: b.data } : {}) };
304
324
  });
305
325
  for (const o of input.obstacles ?? []) nodes.push({ id: o.id, box: regionBounds(o.region) });
306
- const edges = input.edges.map(
326
+ const edges = (input.edges ?? []).map(
307
327
  (e): Edge => ({
308
328
  id: e.id,
309
329
  source: { node: e.source, ...(e.sourcePort !== undefined ? { port: e.sourcePort } : {}) },