@mark1russell7/highways 0.8.0 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. package/README.md +74 -8
  2. package/build/index.d.ts +705 -183
  3. package/build/index.d.ts.map +1 -1
  4. package/build/index.js +3458 -1979
  5. package/build/index.js.map +1 -1
  6. package/package.json +3 -2
  7. package/src/bench/compare.ts +174 -5
  8. package/src/bench/exact/index.ts +4 -0
  9. package/src/bench/properties.ts +8 -2
  10. package/src/bench/room.ts +1 -1
  11. package/src/bench/run.ts +38 -3
  12. package/src/bench/screen/generators.ts +11 -41
  13. package/src/bench/screen/screen.ts +16 -10
  14. package/src/bench/screen/set.ts +156 -49
  15. package/src/bench/suite.ts +110 -12
  16. package/src/cases.ts +64 -18
  17. package/src/families/cases.ts +108 -0
  18. package/src/families/fixtures.ts +101 -0
  19. package/src/families/index.ts +36 -0
  20. package/src/families/model.ts +55 -0
  21. package/src/fixtures.ts +8 -2
  22. package/src/highways/soft-keys/index.ts +8 -4
  23. package/src/highways/stretches.ts +28 -7
  24. package/src/index.ts +8 -0
  25. package/src/lane-order.ts +42 -2
  26. package/src/lane-orders/stretch/index.ts +486 -0
  27. package/src/lane-orders/stretch/router.ts +22 -0
  28. package/src/levels/depths/index.ts +57 -0
  29. package/src/plugs/facing/index.ts +22 -0
  30. package/src/portfolio/router.ts +44 -0
  31. package/src/routers/corners.ts +27 -3
  32. package/src/routers/sections.ts +22 -40
  33. package/src/routers/shared.ts +51 -19
  34. package/src/routers/trunks.ts +21 -3
  35. package/src/segments.ts +8 -26
  36. package/src/strategies/builtins.ts +54 -2
  37. package/src/strategies/model.ts +55 -10
  38. package/src/strategies/options.ts +36 -4
  39. package/src/strategies/schemas.ts +68 -5
  40. package/src/whole/pipeline/index.ts +66 -0
@@ -1,7 +1,6 @@
1
- import { hubChainBoard, manyGroupsBoard, packedGridBoard } from "../../cases.ts";
2
- import { FIXTURES, makeFixture, type FixtureSpec } from "../../fixtures.ts";
1
+ import { checkDeclared, corpusHashOf, SPLIT_SCHEMA, splitFileName, type ClaimBreak, type CorpusSplit, type DeclaredClaim, type FamilyBy, type FamilyCell } from "@mark1russell7/layout-contract";
2
+ import { highwaysRegistry, type HighwaysBoard } from "../../families/index.ts";
3
3
  import type { Graph } from "../../model.ts";
4
- import { familySeed } from "../suite.ts";
5
4
  import { EXACT_V1, EXACT_V1_BOUNDS } from "./exact-set.ts";
6
5
  import { graphInput, type LayoutCell, type LayoutGenerator, type LayoutInput } from "./forms.ts";
7
6
 
@@ -20,74 +19,123 @@ export interface ScreenFamily {
20
19
  readonly cell: LayoutCell;
21
20
  }
22
21
 
22
+ /**
23
+ * The key of the family of a screen input (SCREEN-WEIGHT). The map form gives the kind of the cell as the key, with no other dimension.
24
+ * The cells of a screen set have no `kind`, and `dense-groups` and `many-groups` have the same cell. Thus `familyCell` adds the family of the input as the kind.
25
+ */
26
+ export const SCREEN_FAMILY_BY: FamilyBy = Object.freeze({ "*": Object.freeze([]) as readonly string[] });
27
+
28
+ /** This function gives the cell of an input for the key `familyBy`: the cell of the input, with its family as `kind`. */
29
+ export const familyCell = (input: { readonly family: string; readonly cell?: LayoutCell }): FamilyCell => ({ ...input.cell, kind: input.family });
30
+
31
+ /** The claims of a manifest. `declared` gives the count of the inputs of each family (`docs/CORPUS-CHECKS.md` of `layout-contract`). */
32
+ export interface CorpusClaims {
33
+ readonly declared: DeclaredClaim;
34
+ }
35
+
23
36
  /** A screen set: a fixed list of small boards with an id. A new board or a changed board gets a new id. */
24
37
  export interface ScreenSet {
25
38
  /** The id of the set, for example `screen-v1`. The records name it in `set` and `corpus`. */
26
39
  readonly id: string;
27
40
  readonly families: readonly ScreenFamily[];
41
+ /** The claims of the manifest of the set. A set without claims has no field `claims` in its manifest. */
42
+ readonly claims?: CorpusClaims;
43
+ /** The member seeds of each family that the split `default` holds out. A set without them has no split file. */
44
+ readonly heldOut?: readonly number[];
28
45
  }
29
46
 
30
- const fixture = (name: string): FixtureSpec => FIXTURES.find((f) => f.name === name)!;
31
- const DENSE: FixtureSpec = { name: "dense-groups", kind: "clustered", nodes: 150, edgesPerNode: 2, seed: 3 };
32
- const PACKED = { cols: 12, rows: 8, long: 48 } as const;
47
+ /** The registry of the screen sets (CORPUS-GEN). The screens and the site make each board with the same families. */
48
+ const REGISTRY = highwaysRegistry();
33
49
 
34
- /** This function gives a family of `makeFixture`. The member `k` has the seed `familySeed(spec, k)`, as in `runSuite`. */
35
- function fixtureFamily(spec: FixtureSpec, shows: string, seeds: number, cell: LayoutCell): ScreenFamily {
36
- const { name, seed: _seed, ...params } = spec;
37
- return {
38
- name,
39
- shows,
40
- seeds,
41
- cell,
42
- make: (k) => makeFixture({ ...spec, seed: familySeed(spec, k) }),
43
- generator: (k) => ({ name: "fixture", version: 1, params, seed: familySeed(spec, k) }),
50
+ /**
51
+ * This function gives a screen family of a corpus family at its defaults. The member `k` has the input seed `base + 1009 * k`, the rule of `familySeed`.
52
+ * The family keeps the board of its last member, so `make(k)` and `generator(k)` of one member generate one time.
53
+ */
54
+ function registryFamily(name: string, base: number, shows: string, seeds: number, cell: LayoutCell): ScreenFamily {
55
+ let last: { readonly k: number; readonly board: HighwaysBoard } | undefined;
56
+ const board = (k: number): HighwaysBoard => {
57
+ if (last?.k !== k) last = { k, board: REGISTRY.generate(name, caseSeed(base, k)) };
58
+ return last.board;
44
59
  };
60
+ return { name, shows, seeds, cell, make: (k) => board(k).graph, generator: (k) => board(k).generator };
45
61
  }
46
62
 
47
63
  /** The cell dimensions that each family of `screen-v1` shares. */
48
64
  const BASE = { containerSize: "freeAspect", containerShape: "rect", obstacles: "none", mode: "offline", groups: "none" } as const;
49
65
 
66
+ /** This function gives the seed of the member `k` of a case board with the base seed `base`: `base + 1009 * k`, the rule of `familySeed`. */
67
+ export function caseSeed(base: number, k: number): number {
68
+ return base + 1009 * k;
69
+ }
70
+
71
+ /**
72
+ * This function gives the 6 families of the screen. Each fixture family and `packed-grid` has `seeds` members.
73
+ * The case boards `hub-chain` and `many-groups` have `caseSeeds` members. The member `k` of a family has the seed of its rule for `k`.
74
+ * Each family is a family of `HIGHWAYS_FAMILIES` at its defaults. The base seeds are the seeds of the fixtures and the case boards of the benchmark.
75
+ */
76
+ function screenFamilies(seeds: number, caseSeeds: number): ScreenFamily[] {
77
+ return [
78
+ registryFamily("tiny-uniform", 1, "A tiny board: the cost of a strategy at its start.", seeds, { ...BASE, count: "10", edgeDensity: "medium", edgeShape: "random", ports: "free" }),
79
+ registryFamily("small-clustered", 2, "Clusters, and fixed plugs on 20% of the nodes.", seeds, { ...BASE, count: "100", edgeDensity: "medium", edgeShape: "clustered", ports: "fixed" }),
80
+ registryFamily("dense-groups", 3, "Dense groups whose boxes overlap at the far view.", seeds, { ...BASE, count: "100", edgeDensity: "medium", edgeShape: "clustered", ports: "free" }),
81
+ registryFamily("hub-chain", 3, "A chain of hubs: routes climb away from their targets.", caseSeeds, { ...BASE, count: "100", edgeDensity: "medium", edgeShape: "hubChain", ports: "free" }),
82
+ registryFamily("many-groups", 7, "Many groups: level links go around the board.", caseSeeds, { ...BASE, count: "100", edgeDensity: "medium", edgeShape: "clustered", ports: "free" }),
83
+ registryFamily("packed-grid", 11, "A packed grid: boxes touch, with no channel for the routes.", seeds, { ...BASE, count: "100", edgeDensity: "medium", edgeShape: "longRange", ports: "free" }),
84
+ ];
85
+ }
86
+
87
+ const V1_FAMILIES = screenFamilies(3, 1);
88
+
50
89
  /**
51
90
  * The screen set `screen-v1`: 14 boards of made-up data from 10 to 150 nodes. Each family shows a known hard case.
52
91
  * The seed 0 of each family is the fixture or the case board of the benchmark. The seed `k` of a fixture family is `familySeed(spec, k)`, as in `runSuite`.
53
92
  * The generators wrap the fixtures and the case boards without a change to their output.
54
- * This set does not change. The tests compare it with the files in `corpus/screen-v1`.
93
+ *
94
+ * The manifest declares the inputs of each family, with `minCount` 3. The case boards `hub-chain` and `many-groups` have 1 input, so they are in `thin`.
95
+ * The boards of this set do not change. The tests compare it with the files in `corpus/screen-v1`.
55
96
  */
56
97
  export const SCREEN_V1: ScreenSet = {
57
98
  id: "screen-v1",
58
- families: [
59
- fixtureFamily(fixture("tiny-uniform"), "A tiny board: the cost of a strategy at its start.", 3, { ...BASE, count: "10", edgeDensity: "medium", edgeShape: "random", ports: "free" }),
60
- fixtureFamily(fixture("small-clustered"), "Clusters, and fixed plugs on 20% of the nodes.", 3, { ...BASE, count: "100", edgeDensity: "medium", edgeShape: "clustered", ports: "fixed" }),
61
- fixtureFamily(DENSE, "Dense groups whose boxes overlap at the far view.", 3, { ...BASE, count: "100", edgeDensity: "medium", edgeShape: "clustered", ports: "free" }),
62
- {
63
- name: "hub-chain",
64
- shows: "A chain of hubs: routes climb away from their targets.",
65
- seeds: 1,
66
- cell: { ...BASE, count: "100", edgeDensity: "medium", edgeShape: "hubChain", ports: "free" },
67
- make: () => hubChainBoard(),
68
- generator: () => ({ name: "case.hub-chain", version: 1, params: {}, seed: 3 }),
69
- },
70
- {
71
- name: "many-groups",
72
- shows: "Many groups: level links go around the board.",
73
- seeds: 1,
74
- cell: { ...BASE, count: "100", edgeDensity: "medium", edgeShape: "clustered", ports: "free" },
75
- make: () => manyGroupsBoard(),
76
- generator: () => ({ name: "case.many-groups", version: 1, params: {}, seed: 7 }),
77
- },
78
- {
79
- name: "packed-grid",
80
- shows: "A packed grid: boxes touch, with no channel for the routes.",
81
- seeds: 3,
82
- cell: { ...BASE, count: "100", edgeDensity: "medium", edgeShape: "longRange", ports: "free" },
83
- make: (k) => packedGridBoard({ ...PACKED, seed: 11 + 1009 * k }),
84
- generator: (k) => ({ name: "case.packed-grid", version: 1, params: PACKED, seed: 11 + 1009 * k }),
99
+ families: V1_FAMILIES,
100
+ claims: {
101
+ declared: {
102
+ minCount: 3,
103
+ thin: V1_FAMILIES.filter((f) => f.seeds < 3).map((f) => f.name),
104
+ counts: Object.fromEntries(V1_FAMILIES.map((f) => [f.name, f.seeds])),
105
+ familyBy: SCREEN_FAMILY_BY,
85
106
  },
86
- ],
107
+ },
108
+ };
109
+
110
+ /** The member seeds of each family of `screen-v2` that the split `default` holds out. */
111
+ export const SCREEN_V2_HELD_OUT: readonly number[] = [7, 8, 9];
112
+
113
+ const V2_FAMILIES = screenFamilies(10, 10);
114
+
115
+ /**
116
+ * The screen set `screen-v2` (CORPUS-SEEDS): the 6 families of `screen-v1`, each with the 10 members 0 to 9, thus 60 boards.
117
+ * Each family keeps the seed rule of `screen-v1`. Thus the members 0 to 2 are the boards of `screen-v1`, and the member 0 of `hub-chain` and `many-groups` is their board.
118
+ *
119
+ * The manifest declares 10 inputs for each family, and each family key has 10 inputs or more. The split `default` holds out the members 7 to 9 of each family.
120
+ * This set does not change. The tests compare it with the files in `corpus/screen-v2`.
121
+ */
122
+ export const SCREEN_V2: ScreenSet = {
123
+ id: "screen-v2",
124
+ families: V2_FAMILIES,
125
+ claims: { declared: { minCount: 10, thin: [], counts: Object.fromEntries(V2_FAMILIES.map((f) => [f.name, f.seeds])), familyBy: SCREEN_FAMILY_BY, familyMin: 10 } },
126
+ heldOut: SCREEN_V2_HELD_OUT,
87
127
  };
88
128
 
89
- /** The screen sets by id: `screen-v1`, the small boards with a known optimum `exact-v1` (MEAS2), and the larger boards with bounds `exact-v1-bounds` (EXACT-BOUNDS). */
90
- export const SCREEN_SETS: Readonly<Record<string, ScreenSet>> = { [SCREEN_V1.id]: SCREEN_V1, [EXACT_V1.id]: EXACT_V1, [EXACT_V1_BOUNDS.id]: EXACT_V1_BOUNDS };
129
+ /**
130
+ * The screen sets by id. `screen-v1` and its larger version `screen-v2` (CORPUS-SEEDS) have the boards of the screen.
131
+ * `exact-v1` has small boards with a known optimum (MEAS2). `exact-v1-bounds` has larger boards with bounds (EXACT-BOUNDS).
132
+ */
133
+ export const SCREEN_SETS: Readonly<Record<string, ScreenSet>> = {
134
+ [SCREEN_V1.id]: SCREEN_V1,
135
+ [SCREEN_V2.id]: SCREEN_V2,
136
+ [EXACT_V1.id]: EXACT_V1,
137
+ [EXACT_V1_BOUNDS.id]: EXACT_V1_BOUNDS,
138
+ };
91
139
 
92
140
  /** This function gives the id of the member `seed` of a family of a set: `<set>/<family>/s<seed>`. */
93
141
  export function inputId(set: string, family: string, seed: number): string {
@@ -115,10 +163,14 @@ export interface LayoutCorpus {
115
163
  readonly corpus: string;
116
164
  readonly specs: readonly CorpusSpec[];
117
165
  readonly inputs: readonly { readonly id: string; readonly hash: string; readonly cell: LayoutCell }[];
166
+ readonly claims?: CorpusClaims;
118
167
  }
119
168
 
120
- /** This function gives the manifest of a list of inputs. The inputs of one generator, version and parameters share one spec. */
121
- export function corpusManifest(corpus: string, inputs: readonly LayoutInput[]): LayoutCorpus {
169
+ /**
170
+ * This function gives the manifest of a list of inputs. The inputs of one generator, version and parameters share one spec.
171
+ * With `claims`, the manifest has the field `claims`.
172
+ */
173
+ export function corpusManifest(corpus: string, inputs: readonly LayoutInput[], claims?: CorpusClaims): LayoutCorpus {
122
174
  const specs = new Map<string, { generator: string; version: number; params: Readonly<Record<string, unknown>>; seeds: number[] }>();
123
175
  for (const input of inputs) {
124
176
  const g = input.generator;
@@ -133,5 +185,60 @@ export function corpusManifest(corpus: string, inputs: readonly LayoutInput[]):
133
185
  corpus,
134
186
  specs: [...specs.values()],
135
187
  inputs: inputs.map((i) => ({ id: i.id, hash: i.hash, cell: i.cell ?? {} })),
188
+ ...(claims ? { claims } : {}),
189
+ };
190
+ }
191
+
192
+ /** This function gives the manifest of a screen set, with the claims of the set. */
193
+ export function setManifest(set: ScreenSet, inputs: readonly LayoutInput[] = screenInputs(set)): LayoutCorpus {
194
+ return corpusManifest(set.id, inputs, set.claims);
195
+ }
196
+
197
+ /** The hook of the claim `declared` of a screen set: each input declares its family. */
198
+ export const declaresFamily = (input: { readonly family: string }): readonly string[] => [input.family];
199
+
200
+ /**
201
+ * This function checks the claim `declared` of a manifest against its inputs, with the shared check of the contract. A manifest without the claim gives no break.
202
+ * With `familyBy`, the check reads the cell of each input with `familyCell`.
203
+ */
204
+ export function checkFamilies(manifest: LayoutCorpus, inputs: readonly LayoutInput[]): ClaimBreak[] {
205
+ const claim = manifest.claims?.declared;
206
+ return claim ? checkDeclared(inputs, declaresFamily, claim, undefined, familyCell) : [];
207
+ }
208
+
209
+ /** This function gives the hash of the corpus of a manifest with the shared function `corpusHashOf` of the contract. It reads the input ids and the input hashes. */
210
+ export function manifestHash(manifest: LayoutCorpus): string {
211
+ return corpusHashOf(manifest.corpus, manifest.inputs);
212
+ }
213
+
214
+ /**
215
+ * This function gives the split file `corpus-split/1` of a screen set with `heldOut`. A set without `heldOut` gives `undefined`.
216
+ * The seeds of a split file are the seeds of the specs of the manifest. Thus each fold lists the generator seeds of its members.
217
+ * - The split `default` has one fold. It holds out the members `heldOut` of each family and trains on the other members.
218
+ * - The split `by-member` has one fold for each member `k`. The fold holds out the member `k` of each family.
219
+ */
220
+ export function screenSplit(set: ScreenSet, manifest: LayoutCorpus = setManifest(set)): CorpusSplit | undefined {
221
+ const heldOut = set.heldOut;
222
+ if (!heldOut) return undefined;
223
+ const members = Math.max(...set.families.map((f) => f.seeds));
224
+ const seedsOf = (keep: (k: number) => boolean): number[] => {
225
+ const out = new Set<number>();
226
+ for (const f of set.families) for (let k = 0; k < f.seeds; k++) if (keep(k)) out.add(f.generator(k).seed);
227
+ return [...out].sort((a, b) => a - b);
228
+ };
229
+ const fold = (out: (k: number) => boolean) => ({ train: seedsOf((k) => !out(k)), heldOut: seedsOf(out) });
230
+ return {
231
+ schema: SPLIT_SCHEMA,
232
+ corpus: set.id,
233
+ corpusHash: manifestHash(manifest),
234
+ splits: {
235
+ default: { folds: [fold((k) => heldOut.includes(k))] },
236
+ "by-member": { folds: Array.from({ length: members }, (_, m) => fold((k) => k === m)) },
237
+ },
136
238
  };
137
239
  }
240
+
241
+ /** This function gives the path of the split file of a corpus, relative to the folder of the corpora: `splits/<corpus id>.<corpus hash>.json`. */
242
+ export function splitPath(split: CorpusSplit): string {
243
+ return `splits/${splitFileName(split.corpus, split.corpusHash)}`;
244
+ }
@@ -24,7 +24,7 @@ import { boardUnit, crossingAngles, detourSpread, drawnSegments, plugSpacing, to
24
24
  import { changeGraph, routeStability } from "./measures/stability.ts";
25
25
  import { quantile, summarize, type Spread } from "./measures/stats.ts";
26
26
  import { measureBoard } from "./room.ts";
27
- import { benchRun, type BenchRow } from "./run.ts";
27
+ import { benchRun, type BenchRow, type Paired } from "./run.ts";
28
28
 
29
29
  // The core runs in any host, so it reads the clock without the types of Node or the browser.
30
30
  const now = (): number => (globalThis as { performance?: { now(): number } }).performance?.now() ?? Date.now();
@@ -64,6 +64,26 @@ export interface SuiteOptions {
64
64
  readonly steps?: readonly Step[];
65
65
  /** The time limit of one cell of the step `router`, in milliseconds, as `BenchOptions.cellLimit` gives it. The default is no limit. */
66
66
  readonly cellLimit?: number;
67
+ /**
68
+ * One shard of the cells of the steps `router` and `highway` (BENCH-GREEN). The default is all the cells.
69
+ * The strategy at place `s` of its step, on the seed `k`, belongs to the shard `(s + k) % count`, from 0.
70
+ * So the seeds of one strategy go to different shards, and each shard gets a similar share of the time.
71
+ * A shard has only these two steps and the step `curve`, because the cells of the other steps do not divide.
72
+ */
73
+ readonly shard?: { readonly index: number; readonly count: number };
74
+ /**
75
+ * The largest fixture of a strategy of the steps `router` and `highway`, in nodes, by the name of the strategy. The default is no cap.
76
+ * A capped strategy does not route a larger fixture. The report lists each such cell in `capped`, so the row is capped, not missing.
77
+ */
78
+ readonly caps?: Readonly<Record<string, number>>;
79
+ /**
80
+ * The name of the baseline router of the paired arm, for example `shared`. The default is no paired arm.
81
+ * On the first seed of each fixture, the baseline routes the same graph in turn with each strategy of the steps `router` and `highway`.
82
+ * The row and the bench row then get `paired`.
83
+ */
84
+ readonly pair?: string;
85
+ /** The paired arm routes only the fixtures up to this number of nodes. The default is 1000. */
86
+ readonly pairMax?: number;
67
87
  /** This function gets one line of progress for each fixture and seed. */
68
88
  readonly log?: (line: string) => void;
69
89
  }
@@ -108,6 +128,19 @@ export interface SuiteRow {
108
128
  readonly treeMs?: number;
109
129
  /** The measures, by a key that starts with the id of MEAS0, or the name of a proposed rule. */
110
130
  readonly values: Readonly<Record<string, number>>;
131
+ /** The time of the paired baseline arm (`SuiteOptions.pair`). Only the first seed of a fixture has it. */
132
+ readonly paired?: Paired;
133
+ }
134
+
135
+ /** A cell that the suite did not route, because the fixture is larger than the cap of the strategy (`SuiteOptions.caps`). */
136
+ export interface CappedCell {
137
+ readonly step: Step;
138
+ readonly strategy: string;
139
+ readonly fixture: string;
140
+ /** The nodes of the fixture. */
141
+ readonly nodes: number;
142
+ /** The cap of the strategy, in nodes. */
143
+ readonly cap: number;
111
144
  }
112
145
 
113
146
  /** The spread of each measure of one strategy on one fixture family, over the seeds. */
@@ -159,16 +192,27 @@ export interface SuiteReport {
159
192
  readonly summaries: readonly SuiteSummary[];
160
193
  readonly fronts: readonly SuiteFront[];
161
194
  readonly rules: readonly RuleCount[];
195
+ /** The cells over the cap of their strategy, one for each strategy and fixture. A report with no cap has no such field. */
196
+ readonly capped?: readonly CappedCell[];
162
197
  }
163
198
 
164
- /** The axes of the Pareto front of each step. The note MEAS0 asks for two to four scores. */
199
+ /** The axes of the Pareto front of a step that gives routes. */
200
+ const ROUTE_OBJECTIVES: readonly Objective[] = [
201
+ { key: "R1.perEdge", better: "min" },
202
+ { key: "R5.median", better: "min" },
203
+ { key: "B1.inkSaved", better: "max" },
204
+ { key: "R4.turn", better: "min" },
205
+ ];
206
+
207
+ /**
208
+ * The axes of the Pareto front of each step. The note MEAS0 asks for two to four scores.
209
+ * The steps `plugs`, `levels` and `whole` run as routers (`stepRouterStrategy`), so they use the axes of the step `router`.
210
+ */
165
211
  export const FRONT_OBJECTIVES: Readonly<Record<Step, readonly Objective[]>> = {
166
- router: [
167
- { key: "R1.perEdge", better: "min" },
168
- { key: "R5.median", better: "min" },
169
- { key: "B1.inkSaved", better: "max" },
170
- { key: "R4.turn", better: "min" },
171
- ],
212
+ router: ROUTE_OBJECTIVES,
213
+ plugs: ROUTE_OBJECTIVES,
214
+ levels: ROUTE_OBJECTIVES,
215
+ whole: ROUTE_OBJECTIVES,
172
216
  curve: [
173
217
  { key: "C1.share", better: "min" },
174
218
  { key: "C2.median", better: "min" },
@@ -445,6 +489,25 @@ export function runSuite(options: SuiteOptions = {}): { report: SuiteReport; ben
445
489
  const smooth = new Set(options.smoothCurves ?? ["rounded", "arc", "spline", "smooth", "clothoid", "highway"]);
446
490
  const normals = new Set(options.normalCurves ?? ["spline"]);
447
491
  const log = options.log ?? ((): void => {});
492
+ const shard = options.shard ?? { index: 0, count: 1 };
493
+ if (!Number.isInteger(shard.count) || shard.count < 1 || !Number.isInteger(shard.index) || shard.index < 0 || shard.index >= shard.count)
494
+ throw new Error(`The shard ${shard.index} of ${shard.count} is not valid. The index must be from 0 to count - 1.`);
495
+ if (shard.count > 1 && [...steps].some((s) => s !== "router" && s !== "highway" && s !== "curve"))
496
+ throw new Error("A shard runs only the steps router, highway and curve.");
497
+ const mine = (place: number, seed: number): boolean => (place + seed) % shard.count === shard.index;
498
+ const caps = options.caps ?? {};
499
+ const pairMax = options.pairMax ?? 1000;
500
+ const pairStrategy = options.pair === undefined ? undefined : listStrategies(registry, "router").find((s) => s.name === options.pair);
501
+ if (options.pair !== undefined && pairStrategy === undefined) throw new Error(`There is no baseline router "${options.pair}".`);
502
+ const pair = pairStrategy ? asRouter(pairStrategy) : undefined;
503
+ const capped: CappedCell[] = [];
504
+ // This function tells if the cap of a strategy stops a cell. It records the capped cell one time, on the first seed.
505
+ const overCap = (step: Step, strategy: string, fixture: string, nodes: number, seed: number): boolean => {
506
+ const cap = caps[strategy];
507
+ if (cap === undefined || nodes <= cap) return false;
508
+ if (seed === 0) capped.push({ step, strategy, fixture, nodes, cap });
509
+ return true;
510
+ };
448
511
  const fixtures = steps.has("router") || steps.has("group") || steps.has("highway") ? (options.fixtures ?? FIXTURES).filter((f) => f.nodes <= max) : [];
449
512
  const routers = steps.has("router") ? listStrategies(registry, "router").filter((s) => options.routers?.includes(s.name) ?? true) : [];
450
513
  const curves = steps.has("curve") ? listStrategies(registry, "curve") : [];
@@ -463,11 +526,14 @@ export function runSuite(options: SuiteOptions = {}): { report: SuiteReport; ben
463
526
  log(`${spec.name} seed ${k}: ${graph.nodes.length} nodes, ${graph.edges.length} edges`);
464
527
  const first = rows.length;
465
528
  // The step `router`: the hard rules and the measures of today, then the new measures.
466
- for (const strategy of routers) {
529
+ for (const [place, strategy] of routers.entries()) {
467
530
  if (graph.nodes.length > (strategy.maxNodes ?? Infinity)) continue;
531
+ if (!mine(place, k)) continue;
532
+ if (overCap("router", strategy.name, spec.name, graph.nodes.length, k)) continue;
468
533
  const router = asRouter(strategy);
534
+ const paired = k === 0 && graph.nodes.length <= pairMax ? pair : undefined;
469
535
  const t0 = now();
470
- const { row, result } = benchRun(router, spec.name, graph, k === 0 ? repeat : 1, cellLimit);
536
+ const { row, result } = benchRun(router, spec.name, graph, k === 0 ? repeat : 1, cellLimit, paired);
471
537
  const ms = now() - t0;
472
538
  if (k === 0) bench.push(row);
473
539
  const t1 = now();
@@ -494,16 +560,20 @@ export function runSuite(options: SuiteOptions = {}): { report: SuiteReport; ben
494
560
  measureMs,
495
561
  extraMs,
496
562
  values,
563
+ ...(row.paired ? { paired: row.paired } : {}),
497
564
  });
498
565
  if (strategy.name === curveRouter && curves.length > 0 && graph.nodes.length <= curveMax)
499
566
  rows.push(...curveRuns(graph, result, curves, { fixture: spec.name, seed: k, smooth, normals }).map((c) => c.row));
500
567
  }
501
568
  // The step `highway`: the plan of each strategy, routed with `follow`. The measures are the measures of the step `router`.
502
- for (const strategy of highways) {
569
+ for (const [place, strategy] of highways.entries()) {
503
570
  if (graph.nodes.length > (strategy.maxNodes ?? Infinity)) continue;
571
+ if (!mine(place, k)) continue;
572
+ if (overCap("highway", strategy.name, spec.name, graph.nodes.length, k)) continue;
504
573
  const router = asRouter(strategy);
574
+ const paired = k === 0 && graph.nodes.length <= pairMax ? pair : undefined;
505
575
  const t0 = now();
506
- const { row, result } = benchRun(router, spec.name, graph, 2, cellLimit);
576
+ const { row, result } = benchRun(router, spec.name, graph, 2, cellLimit, paired);
507
577
  const ms = now() - t0;
508
578
  const t1 = now();
509
579
  const values: Record<string, number> = routeMeasures(graph, result, row);
@@ -529,6 +599,7 @@ export function runSuite(options: SuiteOptions = {}): { report: SuiteReport; ben
529
599
  measureMs,
530
600
  extraMs,
531
601
  values,
602
+ ...(row.paired ? { paired: row.paired } : {}),
532
603
  });
533
604
  }
534
605
  // The step `group`: the router `sections` with each grouping rule, and the grouping of the changed graph (G9).
@@ -599,6 +670,7 @@ export function runSuite(options: SuiteOptions = {}): { report: SuiteReport; ben
599
670
  summaries,
600
671
  fronts: suiteFronts(summaries),
601
672
  rules: ruleCounts(rows),
673
+ ...(capped.length > 0 ? { capped } : {}),
602
674
  },
603
675
  bench,
604
676
  };
@@ -649,6 +721,7 @@ export function joinReports(reports: readonly SuiteReport[], options: { registry
649
721
  { ...options, curveRouter: first?.curveRouter ?? "corners" },
650
722
  );
651
723
  const summaries = summarizeRows(rows);
724
+ const capped = reports.flatMap((r) => r.capped ?? []);
652
725
  return {
653
726
  options: {
654
727
  max: Math.max(0, ...reports.map((r) => r.options.max)),
@@ -663,6 +736,7 @@ export function joinReports(reports: readonly SuiteReport[], options: { registry
663
736
  summaries,
664
737
  fronts: suiteFronts(summaries),
665
738
  rules: ruleCounts(rows),
739
+ ...(capped.length > 0 ? { capped } : {}),
666
740
  };
667
741
  }
668
742
 
@@ -762,6 +836,9 @@ export const SUITE_COLUMNS: Readonly<Record<Step, readonly string[]>> = {
762
836
  curve: ["C1.share", "C1.max", "C2.median", "C3.median", "C3.q95", "C6.length"],
763
837
  room: ["M1.area", "M4.laneThrough", "crossings", "through", "room-order"],
764
838
  group: ["Z4.farItems", "G1.modularity", "G2.cut", "G6.balance", "G8.treeCost", "G9.ari", "Z7.position", "Z8.order", "Z3.through", "treeMs"],
839
+ plugs: ["R1.perEdge", "R2.angle", "R2.min", "B1.inkSaved", "R5.median", "R5.q95", "R4.turn", "R11.q05", "B4.ambiguity", "Z10.moved"],
840
+ levels: ["R1.perEdge", "R2.angle", "R2.min", "B1.inkSaved", "R5.median", "R5.q95", "R4.turn", "R11.q05", "B4.ambiguity", "Z10.moved"],
841
+ whole: ["R1.perEdge", "R2.angle", "R2.min", "B1.inkSaved", "R5.median", "R5.q95", "R4.turn", "R11.q05", "B4.ambiguity", "Z10.moved"],
765
842
  };
766
843
 
767
844
  /**
@@ -837,3 +914,24 @@ export function formatMeasureTime(rows: readonly SuiteRow[], step: Step = "route
837
914
  });
838
915
  return [head, line, ...body].join("\n");
839
916
  }
917
+
918
+ /** This function writes the capped cells as a Markdown table. Each line is a cell that the cap of its strategy stopped, so the row is capped, not missing. */
919
+ export function formatCapped(capped: readonly CappedCell[]): string {
920
+ const head = "| step | strategy | fixture | nodes | cap | state |";
921
+ const line = "|---|---|---|---:|---:|---|";
922
+ const body = capped.map((c) => `| ${c.step} | ${c.strategy} | ${c.fixture} | ${c.nodes} | ${c.cap} | capped |`);
923
+ return [head, line, ...body].join("\n");
924
+ }
925
+
926
+ /**
927
+ * This function writes the paired arm of the rows as a Markdown table. It gives the medians of the strategy and of the baseline, and their ratio.
928
+ * Only the rows with `paired` count.
929
+ */
930
+ export function formatPaired(rows: readonly SuiteRow[]): string {
931
+ const head = "| step | strategy | fixture | nodes | ms | baseline | baseline ms | ratio |";
932
+ const line = "|---|---|---|---:|---:|---|---:|---:|";
933
+ const body = rows.flatMap((r) =>
934
+ r.paired ? [`| ${r.step} | ${r.strategy} | ${r.fixture} | ${r.nodes} | ${r.paired.ms.toFixed(1)} | ${r.paired.base} | ${r.paired.baseMs.toFixed(1)} | ${r.paired.ratio.toFixed(3)} |`] : [],
935
+ );
936
+ return [head, line, ...body].join("\n");
937
+ }
package/src/cases.ts CHANGED
@@ -37,6 +37,12 @@ class Board {
37
37
  }
38
38
  }
39
39
 
40
+ /** The options of a case board with a seed: `hubChainBoard` and `manyGroupsBoard`. */
41
+ export interface CaseSeedOptions {
42
+ /** The seed of the random links of the board. Each board has its own default. */
43
+ readonly seed?: number;
44
+ }
45
+
40
46
  /**
41
47
  * This function makes a dense board: 150 nodes in tight clusters. At the far view, the boxes of the groups overlap.
42
48
  * Then a level link starts or ends in a group that another group covers.
@@ -45,21 +51,38 @@ export function denseGroupsBoard(): Graph {
45
51
  return makeFixture({ name: "dense-groups", kind: "clustered", nodes: 150, edgesPerNode: 2, seed: 3 });
46
52
  }
47
53
 
54
+ /** The options of `hubChainBoard`. Each option at its default keeps the board of today. */
55
+ export interface HubChainOptions extends CaseSeedOptions {
56
+ /** The number of rows of cards. The default is 3. */
57
+ readonly rows?: number;
58
+ /** The number of cards in each row. The default is 32. */
59
+ readonly cols?: number;
60
+ /** The number of tries for a long link. The default is 40. */
61
+ readonly long?: number;
62
+ /** The free space between two cards, in units. The default is 48. */
63
+ readonly gap?: number;
64
+ }
65
+
48
66
  /**
49
67
  * This function makes a board of three rows of 32 cards. Each card links to the next card of its row.
50
68
  * Forty long links join cards far apart. The hub of a section is near the middle of its cards.
51
69
  * So a long link climbs to hubs behind its source or past its target, and comes back.
70
+ *
71
+ * The seed changes only the long links. The rows of cards, and thus the chain of hubs, do not change.
72
+ * The default seed is 3. Without options, the board does not change.
73
+ * The default source of random numbers is `rng(seed)`. A corpus family gives the same stream from its registry.
52
74
  */
53
- export function hubChainBoard(): Graph {
54
- const R = rng(3);
75
+ export function hubChainBoard(options: HubChainOptions = {}, random: () => number = rng(options.seed ?? 3)): Graph {
76
+ const R = random;
55
77
  const b = new Board();
56
- const rows = 3;
57
- const cols = 32;
58
- const gap = 48;
78
+ const rows = options.rows ?? 3;
79
+ const cols = options.cols ?? 32;
80
+ const gap = options.gap ?? 48;
81
+ const long = options.long ?? 40;
59
82
  const id = (r: number, c: number): string => `c${r}-${c}`;
60
83
  for (let r = 0; r < rows; r++) for (let c = 0; c < cols; c++) b.node(id(r, c), c * (80 + gap), r * (36 + gap), 80, 36);
61
84
  for (let r = 0; r < rows; r++) for (let c = 0; c + 1 < cols; c++) b.link(id(r, c), id(r, c + 1));
62
- for (let n = 0; n < 40; n++) {
85
+ for (let n = 0; n < long; n++) {
63
86
  const r = Math.floor(R() * rows);
64
87
  const c = Math.floor(R() * cols);
65
88
  const span = Math.floor(cols / 8 + (R() * cols) / 2);
@@ -68,21 +91,40 @@ export function hubChainBoard(): Graph {
68
91
  return b.graph();
69
92
  }
70
93
 
94
+ /** The options of `manyGroupsBoard`. Each option at its default keeps the board of today. */
95
+ export interface ManyGroupsOptions extends CaseSeedOptions {
96
+ /** The number of columns of groups. The default is 8. */
97
+ readonly groupCols?: number;
98
+ /** The number of rows of groups. The default is 4. */
99
+ readonly groupRows?: number;
100
+ /** The number of cards in each group. The default is 4. */
101
+ readonly perGroup?: number;
102
+ /** The number of random links between groups, for each group. The default is 3. */
103
+ readonly crossLinksPerGroup?: number;
104
+ /** The free space between two groups, in units. The default is 60. */
105
+ readonly groupGap?: number;
106
+ }
107
+
71
108
  /**
72
109
  * This function makes a board of 32 groups in 8 columns and 4 rows. Each group has 4 cards in a square.
73
110
  * The cards of a group link in a chain, and 96 random links join cards of two groups.
74
111
  * On a level with many groups, some level links go around the outer edge of the board.
112
+ *
113
+ * The seed changes only the 96 links between groups. The 32 groups and their chains do not change.
114
+ * The default seed is 7. Without options, the board does not change.
115
+ * The default source of random numbers is `rng(seed)`. A corpus family gives the same stream from its registry.
75
116
  */
76
- export function manyGroupsBoard(): Graph {
77
- const R = rng(7);
117
+ export function manyGroupsBoard(options: ManyGroupsOptions = {}, random: () => number = rng(options.seed ?? 7)): Graph {
118
+ const R = random;
78
119
  const b = new Board();
79
- const cols = 8;
80
- const rows = 4;
81
- const per = 4;
120
+ const cols = options.groupCols ?? 8;
121
+ const rows = options.groupRows ?? 4;
122
+ const per = options.perGroup ?? 4;
123
+ const cross = options.crossLinksPerGroup ?? 3;
82
124
  const w = 60;
83
125
  const h = 30;
84
126
  const cardGap = 12;
85
- const groupGap = 60;
127
+ const groupGap = options.groupGap ?? 60;
86
128
  const side = Math.ceil(Math.sqrt(per));
87
129
  const gw = side * w + (side - 1) * cardGap;
88
130
  const gh = Math.ceil(per / side) * h + (Math.ceil(per / side) - 1) * cardGap;
@@ -99,7 +141,7 @@ export function manyGroupsBoard(): Graph {
99
141
  }
100
142
  for (const list of groups) for (let i = 1; i < list.length; i++) b.link(list[i - 1]!, list[i]!);
101
143
  const pick = (list: readonly string[]): string => list[Math.floor(R() * list.length)]!;
102
- for (let n = 0; n < groups.length * 3; n++) b.link(pick(groups[Math.floor(R() * groups.length)]!), pick(groups[Math.floor(R() * groups.length)]!));
144
+ for (let n = 0; n < groups.length * cross; n++) b.link(pick(groups[Math.floor(R() * groups.length)]!), pick(groups[Math.floor(R() * groups.length)]!));
103
145
  return b.graph();
104
146
  }
105
147
 
@@ -113,6 +155,8 @@ export interface PackedGridOptions {
113
155
  readonly rows?: number;
114
156
  /** The number of long links between boxes far apart. The default is 48. */
115
157
  readonly long?: number;
158
+ /** The largest step, in rows and in columns, of the link of a box to a box near it. The default is 2. */
159
+ readonly reach?: number;
116
160
  }
117
161
 
118
162
  /**
@@ -123,18 +167,20 @@ export interface PackedGridOptions {
123
167
  *
124
168
  * The board is not in `CASES`, because the digests of the router fix the output on each board of `CASES`.
125
169
  * The options make a family of such boards for the measure suite. Without options, the board does not change.
170
+ * The default source of random numbers is `rng(seed)`. A corpus family gives the same stream from its registry.
126
171
  */
127
- export function packedGridBoard(options: PackedGridOptions = {}): Graph {
128
- const R = rng(options.seed ?? 11);
172
+ export function packedGridBoard(options: PackedGridOptions = {}, random: () => number = rng(options.seed ?? 11)): Graph {
173
+ const R = random;
129
174
  const b = new Board();
130
175
  const cols = options.cols ?? 12;
131
176
  const rows = options.rows ?? 8;
132
177
  const long = options.long ?? 48;
178
+ const reach = options.reach ?? 2;
133
179
  const w = 140;
134
180
  const h = 90;
135
181
  const id = (r: number, c: number): string => `p${r}-${c}`;
136
182
  for (let r = 0; r < rows; r++) for (let c = 0; c < cols; c++) b.node(id(r, c), c * w, r * h, w, h);
137
- const step = (): number => Math.floor(R() * 5) - 2;
183
+ const step = (): number => Math.floor(R() * (2 * reach + 1)) - reach;
138
184
  for (let r = 0; r < rows; r++)
139
185
  for (let c = 0; c < cols; c++) {
140
186
  const rr = Math.max(0, Math.min(rows - 1, r + step()));
@@ -170,6 +216,6 @@ export function nestedGridBoard(): Graph {
170
216
  /** The case boards of the benchmark. Each board has a few hundred nodes at most, so the benchmark races them on each run. */
171
217
  export const CASES: readonly CaseBoard[] = [
172
218
  { name: "case-dense-groups", defect: "Groups overlap at the far view, and level links go through other groups.", make: denseGroupsBoard },
173
- { name: "case-hub-chain", defect: "A route climbs a chain of hubs away from its target and comes back.", make: hubChainBoard },
174
- { name: "case-many-groups", defect: "On a level with many groups, links go around the outer edge of the board.", make: manyGroupsBoard },
219
+ { name: "case-hub-chain", defect: "A route climbs a chain of hubs away from its target and comes back.", make: () => hubChainBoard() },
220
+ { name: "case-many-groups", defect: "On a level with many groups, links go around the outer edge of the board.", make: () => manyGroupsBoard() },
175
221
  ];