@mark1russell7/highways 0.9.3 → 0.9.5

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 (39) hide show
  1. package/README.md +85 -5
  2. package/build/index.d.ts +954 -5
  3. package/build/index.d.ts.map +1 -1
  4. package/build/index.js +10689 -5294
  5. package/build/index.js.map +1 -1
  6. package/package.json +1 -1
  7. package/src/bench/screen/exercise.ts +4 -1
  8. package/src/bench/screen/set.ts +58 -1
  9. package/src/families/cases.ts +17 -9
  10. package/src/families/coverage.ts +502 -0
  11. package/src/families/index.ts +4 -1
  12. package/src/highways/balance/index.ts +93 -0
  13. package/src/highways/bronchial/index.ts +144 -0
  14. package/src/highways/index.ts +5 -0
  15. package/src/highways/knee/index.ts +154 -0
  16. package/src/highways/knee/root.ts +19 -0
  17. package/src/highways/manifolds/index.ts +157 -0
  18. package/src/highways/slots/index.ts +178 -0
  19. package/src/index.ts +8 -0
  20. package/src/levels/detail/index.ts +140 -0
  21. package/src/levels/venation/index.ts +191 -0
  22. package/src/plugs/leaders/index.ts +42 -0
  23. package/src/plugs/spread/index.ts +39 -5
  24. package/src/routers/adam/index.ts +294 -0
  25. package/src/routers/annealing/index.ts +260 -0
  26. package/src/routers/canals/index.ts +191 -0
  27. package/src/routers/frames.ts +80 -16
  28. package/src/routers/gutter.ts +545 -0
  29. package/src/routers/index.ts +8 -2
  30. package/src/routers/reroute/index.ts +164 -0
  31. package/src/routers/shared.ts +43 -2
  32. package/src/routers/tight/index.ts +122 -0
  33. package/src/strategies/builtins.ts +158 -2
  34. package/src/strategies/schemas.ts +143 -0
  35. package/src/whole/curriculum/index.ts +243 -0
  36. package/src/whole/fireflies/index.ts +177 -0
  37. package/src/whole/portfolio/index.ts +108 -0
  38. package/src/whole/preferences/index.ts +247 -0
  39. package/src/whole/tracing/index.ts +144 -0
package/package.json CHANGED
@@ -9,7 +9,7 @@
9
9
  "directory": "packages/core"
10
10
  },
11
11
  "homepage": "https://mark1russell7.github.io/highways/",
12
- "version": "0.9.3",
12
+ "version": "0.9.5",
13
13
  "type": "module",
14
14
  "main": "./build/index.js",
15
15
  "types": "./build/index.d.ts",
@@ -82,13 +82,16 @@ export const MOVE_NAMES: Readonly<Record<string, string>> = Object.freeze({
82
82
  streets: "street",
83
83
  search: "searched",
84
84
  interchanges: "networkRoad",
85
+ knee: "tree",
86
+ bronchial: "tree",
87
+ manifolds: "manifold",
85
88
  });
86
89
 
87
90
  /**
88
91
  * 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.
89
92
  * The output against the plan `from` tells if they ran.
90
93
  */
91
- export const PLAN_CHANGERS: readonly string[] = Object.freeze(["crossbars", "divided", "lanes", "level-links", "meet", "stubs", "weiszfeld", "zipper"]);
94
+ export const PLAN_CHANGERS: readonly string[] = Object.freeze(["balance", "crossbars", "divided", "lanes", "level-links", "meet", "slots", "stubs", "weiszfeld", "zipper"]);
92
95
 
93
96
  const FINDERS: readonly string[] = [...new Set([...Object.keys(highwayStrategies).filter((n) => !n.includes("+")), "shared-ways-all"])].sort((a, b) => b.length - a.length);
94
97
 
@@ -126,13 +126,70 @@ export const SCREEN_V2: ScreenSet = {
126
126
  heldOut: SCREEN_V2_HELD_OUT,
127
127
  };
128
128
 
129
+ /** This function gives the 9 families of CORPUS-FAMILIES, each with `seeds` members. Their base seeds are the primes from 13 to 43. */
130
+ function coverageFamilies(seeds: number): ScreenFamily[] {
131
+ return [
132
+ registryFamily("frame-rows", 13, "Cards with full-width rows and fixed plugs on their walls.", seeds, { ...BASE, count: "100", edgeDensity: "sparse", edgeShape: "random", groups: "nested", ports: "fixed" }),
133
+ registryFamily("pipeline", 17, "A long chain of stages in rows that turn.", seeds, { ...BASE, count: "100", edgeDensity: "sparse", edgeShape: "tree", ports: "free" }),
134
+ registryFamily("fan-out", 19, "A wide fan-out from one hub to the rings around it.", seeds, { ...BASE, count: "100", edgeDensity: "medium", edgeShape: "bundled", ports: "free" }),
135
+ registryFamily("two-hubs", 23, "Two hubs with many shared targets.", seeds, { ...BASE, count: "100", edgeDensity: "medium", edgeShape: "bundled", ports: "free" }),
136
+ registryFamily("uneven-sizes", 29, "A few large boxes among many small boxes.", seeds, { ...BASE, sizes: "bimodal", count: "100", edgeDensity: "medium", edgeShape: "random", ports: "free" }),
137
+ registryFamily("thin-bars", 31, "Long thin bars in lanes, as a timeline.", seeds, { ...BASE, sizes: "extremeAspect", count: "100", edgeDensity: "medium", edgeShape: "random", ports: "free" }),
138
+ registryFamily("sparse-long", 37, "Few nodes far apart, with long edges.", seeds, { ...BASE, count: "10", edgeDensity: "sparse", edgeShape: "longRange", ports: "free" }),
139
+ registryFamily("local-mesh", 41, "Many short edges between nodes next to each other.", seeds, { ...BASE, count: "100", edgeDensity: "dense", edgeShape: "clustered", ports: "free" }),
140
+ registryFamily("bipartite", 43, "A complete bipartite part: crossings that no plan removes.", seeds, { ...BASE, count: "10", edgeDensity: "dense", edgeShape: "random", ports: "free" }),
141
+ ];
142
+ }
143
+
144
+ /** The member seeds of each family of `screen-v3` that the split `default` holds out. */
145
+ export const SCREEN_V3_HELD_OUT: readonly number[] = [7, 8, 9];
146
+
147
+ const V3_FAMILIES = [...screenFamilies(10, 10), ...coverageFamilies(10)];
148
+
149
+ /**
150
+ * The screen set `screen-v3` (CORPUS-FAMILIES): the 15 families of `HIGHWAYS_FAMILIES`, each with the 10 members 0 to 9, thus 150 boards.
151
+ * The first 6 families are the families of `screen-v2` with their seeds. Thus their boards are the boards of `screen-v2`, under the ids of `screen-v3`.
152
+ * The 9 new families cover frames with rows, a long chain, a wide fan-out and two hubs.
153
+ * They also cover uneven sizes, thin bars, long edges, short edges and a complete bipartite part.
154
+ *
155
+ * The manifest declares 10 inputs for each family. The split `default` holds out the members 7 to 9 of each family.
156
+ * This set does not change. A new family goes into a new set. The tests compare it with the files in `corpus/screen-v3`.
157
+ */
158
+ export const SCREEN_V3: ScreenSet = {
159
+ id: "screen-v3",
160
+ families: V3_FAMILIES,
161
+ claims: { declared: { minCount: 10, thin: [], counts: Object.fromEntries(V3_FAMILIES.map((f) => [f.name, f.seeds])), familyBy: SCREEN_FAMILY_BY, familyMin: 10 } },
162
+ heldOut: SCREEN_V3_HELD_OUT,
163
+ };
164
+
165
+ /**
166
+ * This function gives the power of the sign test over `families` families at the level `level`.
167
+ * The power is the least count of families on one side that passes.
168
+ * A family set without ties and with that count or more on one side passes the two-sided exact sign test, with the p below `level`.
169
+ * The function gives `undefined` when no count passes, for example for 5 families or fewer at the level 0.05.
170
+ */
171
+ export function signTestPower(families: number, level = 0.05): number | undefined {
172
+ for (let k = Math.ceil(families / 2); k <= families; k++) {
173
+ // The two-sided p of k wins and families - k losses: two times the tail of the smaller count.
174
+ let tail = 0;
175
+ let c = 1;
176
+ for (let i = 0; i <= families - k; i++) {
177
+ tail += c;
178
+ c = (c * (families - i)) / (i + 1);
179
+ }
180
+ if (Math.min(1, (2 * tail) / 2 ** families) < level) return k;
181
+ }
182
+ return undefined;
183
+ }
184
+
129
185
  /**
130
- * The screen sets by id. `screen-v1` and its larger version `screen-v2` (CORPUS-SEEDS) have the boards of the screen.
186
+ * The screen sets by id. `screen-v1`, its larger version `screen-v2` (CORPUS-SEEDS) and `screen-v3` with 15 families (CORPUS-FAMILIES) have the boards of the screen.
131
187
  * `exact-v1` has small boards with a known optimum (MEAS2). `exact-v1-bounds` has larger boards with bounds (EXACT-BOUNDS).
132
188
  */
133
189
  export const SCREEN_SETS: Readonly<Record<string, ScreenSet>> = {
134
190
  [SCREEN_V1.id]: SCREEN_V1,
135
191
  [SCREEN_V2.id]: SCREEN_V2,
192
+ [SCREEN_V3.id]: SCREEN_V3,
136
193
  [EXACT_V1.id]: EXACT_V1,
137
194
  [EXACT_V1_BOUNDS.id]: EXACT_V1_BOUNDS,
138
195
  };
@@ -5,16 +5,19 @@ import type { Graph } from "../model.ts";
5
5
  import { changedDimensions, numberDimension, numberOf, type HighwaysBoard, type HighwaysFamily } from "./model.ts";
6
6
 
7
7
  /** This function gives a whole-number dimension with a linear scale. */
8
- const count = (name: string, min: number, max: number, value: number, meaning: string): CorpusDimension =>
8
+ export const count = (name: string, min: number, max: number, value: number, meaning: string): CorpusDimension =>
9
9
  numberDimension({ name, kind: "int", min, max, default: value, meaning });
10
10
 
11
- /** This function gives the numbers of the full dimension values, by name. */
11
+ /** This function gives the numbers of the full dimension values, by name. A dimension that is not `int` or `real` is not in them. */
12
12
  function numbers(dimensions: readonly CorpusDimension[], dims: DimensionValues): Record<string, number> {
13
- return Object.fromEntries(dimensions.map((d) => [d.name, numberOf(dims, d.name)]));
13
+ return Object.fromEntries(dimensions.filter((d) => d.kind === "int" || d.kind === "real").map((d) => [d.name, numberOf(dims, d.name)]));
14
14
  }
15
15
 
16
+ /** The default rule of the random numbers of a case family. */
17
+ const CASE_SEEDING = "mulberry32 of the input seed. The fixed part of the board takes no number. Then one stream gives the random links.";
18
+
16
19
  /** The fields of a case family of `caseFamily`. */
17
- interface CaseFields {
20
+ export interface CaseFields {
18
21
  readonly name: string;
19
22
  readonly meaning: string;
20
23
  readonly dimensions: readonly CorpusDimension[];
@@ -22,12 +25,17 @@ interface CaseFields {
22
25
  readonly generator: string;
23
26
  /** This function gives the parameters of the generator record from the full values. The default gives the values that differ from their defaults. */
24
27
  readonly params?: (dims: DimensionValues) => Readonly<Record<string, unknown>>;
25
- /** This function makes the board. It gets the values by name, the input seed and the source of random numbers of the registry. */
26
- readonly make: (values: Readonly<Record<string, number>>, seed: number, random: () => number) => Graph;
28
+ /** The text of the rule of the random numbers. The default is the rule of the 3 case families of the screen sets. */
29
+ readonly seeding?: string;
30
+ /**
31
+ * This function makes the board. It gets the numeric values by name, the input seed, the source of random numbers of the registry and the full values.
32
+ * The full values also have the values of the `enum` and `bool` dimensions.
33
+ */
34
+ readonly make: (values: Readonly<Record<string, number>>, seed: number, random: () => number, dims: DimensionValues) => Graph;
27
35
  }
28
36
 
29
37
  /** This function gives a case family. Its stream is mulberry32 of the input seed, and it uses only the stream, `Math.floor` and `Math.ceil`. */
30
- function caseFamily(fields: CaseFields): HighwaysFamily {
38
+ export function caseFamily(fields: CaseFields): HighwaysFamily {
31
39
  const { name, meaning, dimensions } = fields;
32
40
  const params = fields.params ?? ((dims: DimensionValues) => changedDimensions(dimensions, dims));
33
41
  return Object.freeze({
@@ -36,10 +44,10 @@ function caseFamily(fields: CaseFields): HighwaysFamily {
36
44
  owner: "highways",
37
45
  meaning,
38
46
  dimensions,
39
- seeding: "mulberry32 of the input seed. The fixed part of the board takes no number. Then one stream gives the random links.",
47
+ seeding: fields.seeding ?? CASE_SEEDING,
40
48
  crossEngine: "exact",
41
49
  generate(seed: number, dims: DimensionValues, context: GenerateContext): HighwaysBoard {
42
- return { graph: fields.make(numbers(dimensions, dims), seed, context.random), generator: { name: fields.generator, version: 1, params: params(dims), seed } };
50
+ return { graph: fields.make(numbers(dimensions, dims), seed, context.random, dims), generator: { name: fields.generator, version: 1, params: params(dims), seed } };
43
51
  },
44
52
  });
45
53
  }
@@ -0,0 +1,502 @@
1
+ // The 9 families of CORPUS-FAMILIES. They cover regions of the input space that the 6 families of `screen-v2` do not cover.
2
+ // Each family uses only the stream of the registry, whole numbers, `Math.floor` and `Math.min`. Thus each family is `exact`.
3
+ import { enumDimension, numberDimension, type HighwaysFamily } from "./model.ts";
4
+ import { caseFamily, count } from "./cases.ts";
5
+ import type { CorpusDimension } from "@mark1russell7/layout-contract";
6
+ import type { Edge, End, Graph, Node, Port } from "../model.ts";
7
+
8
+ /** A board while it grows. It adds no loop, and no second edge between the same two ends. */
9
+ class Builder {
10
+ readonly nodes: Node[] = [];
11
+ readonly edges: Edge[] = [];
12
+ readonly #keys = new Set<string>();
13
+
14
+ node(id: string, x: number, y: number, w: number, h: number, extra: { readonly parent?: string; readonly ports?: readonly Port[] } = {}): string {
15
+ this.nodes.push({ id, box: { x, y, w, h }, ...(extra.parent !== undefined ? { parent: extra.parent } : {}), ...(extra.ports ? { ports: extra.ports } : {}) });
16
+ return id;
17
+ }
18
+
19
+ /** This method adds an edge. The key of an end is its node and its plug. It adds nothing for a loop or a second edge between the same two ends. */
20
+ link(source: End, target: End): void {
21
+ if (source.node === target.node) return;
22
+ const a = `${source.node}:${source.port ?? ""}`;
23
+ const b = `${target.node}:${target.port ?? ""}`;
24
+ const key = a < b ? `${a}|${b}` : `${b}|${a}`;
25
+ if (this.#keys.has(key)) return;
26
+ this.#keys.add(key);
27
+ this.edges.push({ id: `e${this.edges.length}`, source, target });
28
+ }
29
+
30
+ graph(): Graph {
31
+ return { nodes: this.nodes, edges: this.edges };
32
+ }
33
+ }
34
+
35
+ /** This function gives a whole number from `lo` to `hi`, both included. */
36
+ const int = (random: () => number, lo: number, hi: number): number => lo + Math.floor(random() * (hi - lo + 1));
37
+
38
+ /** This function gives the least whole number `s` with `s × s` equal to `v` or more. It uses no square root, so each engine gives the same value. */
39
+ function ceilSqrt(v: number): number {
40
+ let lo = 0;
41
+ let hi = Math.max(1, v);
42
+ while (lo < hi) {
43
+ const mid = Math.floor((lo + hi) / 2);
44
+ if (mid * mid >= v) hi = mid;
45
+ else lo = mid + 1;
46
+ }
47
+ return lo;
48
+ }
49
+
50
+ /** This function gives `k` of the indices 0 to `n - 1` at random, in increasing order. It uses a part of the shuffle of Fisher and Yates. */
51
+ function choose(random: () => number, n: number, k: number): number[] {
52
+ const list = Array.from({ length: n }, (_, i) => i);
53
+ const take = Math.min(k, n);
54
+ for (let i = 0; i < take; i++) {
55
+ const j = int(random, i, n - 1);
56
+ [list[i], list[j]] = [list[j]!, list[i]!];
57
+ }
58
+ return list.slice(0, take).sort((a, b) => a - b);
59
+ }
60
+
61
+ /** This function gives the indices 0 to `n - 1` in a random order, with the shuffle of Fisher and Yates. */
62
+ function shuffle(random: () => number, n: number): number[] {
63
+ const list = Array.from({ length: n }, (_, i) => i);
64
+ for (let i = n - 1; i > 0; i--) {
65
+ const j = int(random, 0, i);
66
+ [list[i], list[j]] = [list[j]!, list[i]!];
67
+ }
68
+ return list;
69
+ }
70
+
71
+ /** The rule of the random numbers of each family of this file. */
72
+ const SEEDING = "mulberry32 of the input seed. One stream gives the sizes and the places of the boxes, then the edges, in a fixed order.";
73
+
74
+ /** The dimensions of `frame-rows`. */
75
+ const FRAME_ROWS_DIMENSIONS: readonly CorpusDimension[] = Object.freeze([
76
+ count("cards", 1, 24, 9, "The number of cards. Each card is a frame."),
77
+ count("rows", 1, 12, 5, "The number of full-width rows in each card."),
78
+ count("links", 0, 8, 4, "The number of tries for a link from a wall plug of a card to a wall plug of another card, for each card."),
79
+ count("inner", 0, 12, 0, "The number of tries for an inner edge from a row to a wall plug of its card, for each card."),
80
+ count("gap", 24, 240, 96, "The free space between two cards, in units."),
81
+ ]);
82
+
83
+ /**
84
+ * The family `frame-rows`: cards with full-width rows, as INTERIOR-ROUTE takes them. Each row has a fixed plug on the east wall and on the west wall of its card.
85
+ * The links join a plug of a card to a plug of another card. An inner edge joins a row to a plug of its own card. The default has no inner edge.
86
+ */
87
+ export const FRAME_ROWS: HighwaysFamily = caseFamily({
88
+ name: "frame-rows",
89
+ meaning: "Cards with full-width rows and fixed plugs on their walls, as tables of a schema diagram.",
90
+ dimensions: FRAME_ROWS_DIMENSIONS,
91
+ generator: "family.frame-rows",
92
+ seeding: SEEDING,
93
+ make: (v, _seed, random) => {
94
+ const b = new Builder();
95
+ const cards = v["cards"]!;
96
+ const rows = v["rows"]!;
97
+ const gap = v["gap"]!;
98
+ const cols = ceilSqrt(cards);
99
+ const w = 220;
100
+ const header = 28;
101
+ const pitch = 30;
102
+ const h = header + rows * pitch + 4;
103
+ const ports: Port[] = [];
104
+ for (let j = 0; j < rows; j++) {
105
+ const along = (header + j * pitch + 12) / h;
106
+ ports.push({ id: `e${j}`, side: "east", along }, { id: `w${j}`, side: "west", along });
107
+ }
108
+ const colOf = (i: number): number => i % cols;
109
+ for (let i = 0; i < cards; i++) {
110
+ const x = colOf(i) * (w + gap);
111
+ const y = Math.floor(i / cols) * (h + gap);
112
+ const card = b.node(`f${i}`, x, y, w, h, { ports });
113
+ for (let j = 0; j < rows; j++) b.node(`f${i}r${j}`, x + 10, y + header + j * pitch, w - 20, 24, { parent: card });
114
+ }
115
+ for (let n = 0; n < cards * v["links"]!; n++) {
116
+ const a = int(random, 0, cards - 1);
117
+ const c = int(random, 0, cards - 1);
118
+ const ra = int(random, 0, rows - 1);
119
+ const rc = int(random, 0, rows - 1);
120
+ // A link leaves its card on the wall that faces the other card.
121
+ const east = colOf(a) <= colOf(c);
122
+ b.link({ node: `f${a}`, port: `${east ? "e" : "w"}${ra}` }, { node: `f${c}`, port: `${east ? "w" : "e"}${rc}` });
123
+ }
124
+ for (let i = 0; i < cards; i++)
125
+ for (let t = 0; t < v["inner"]!; t++) {
126
+ const row = int(random, 0, rows - 1);
127
+ const side = random() < 0.5 ? "e" : "w";
128
+ b.link({ node: `f${i}r${row}` }, { node: `f${i}`, port: `${side}${int(random, 0, rows - 1)}` });
129
+ }
130
+ return b.graph();
131
+ },
132
+ });
133
+
134
+ /** The dimensions of `pipeline`. */
135
+ const PIPELINE_DIMENSIONS: readonly CorpusDimension[] = Object.freeze([
136
+ count("stages", 4, 240, 72, "The number of stages of the chain."),
137
+ count("perRow", 2, 40, 12, "The number of stages in each row. The chain turns at the end of each row."),
138
+ count("skips", 0, 80, 8, "The number of tries for a link that skips 1 to 3 stages."),
139
+ count("gap", 16, 160, 56, "The free space between two stages, in units."),
140
+ ]);
141
+
142
+ /** The family `pipeline`: a long chain of stages in rows that turn at each end, with some links that skip stages. */
143
+ export const PIPELINE: HighwaysFamily = caseFamily({
144
+ name: "pipeline",
145
+ meaning: "A long chain of stages in rows that turn at each end, with a few links that skip stages.",
146
+ dimensions: PIPELINE_DIMENSIONS,
147
+ generator: "family.pipeline",
148
+ seeding: SEEDING,
149
+ make: (v, _seed, random) => {
150
+ const b = new Builder();
151
+ const stages = v["stages"]!;
152
+ const perRow = v["perRow"]!;
153
+ const gap = v["gap"]!;
154
+ const cell = 120;
155
+ for (let i = 0; i < stages; i++) {
156
+ const r = Math.floor(i / perRow);
157
+ const p = i % perRow;
158
+ const col = r % 2 === 0 ? p : perRow - 1 - p;
159
+ const w = int(random, 72, cell);
160
+ b.node(`s${i}`, col * (cell + gap) + Math.floor((cell - w) / 2), r * (40 + gap), w, 40);
161
+ }
162
+ for (let i = 0; i + 1 < stages; i++) b.link({ node: `s${i}` }, { node: `s${i + 1}` });
163
+ for (let n = 0; n < v["skips"]!; n++) {
164
+ const s = int(random, 0, stages - 1);
165
+ const d = int(random, 2, 4);
166
+ if (s + d < stages) b.link({ node: `s${s}` }, { node: `s${s + d}` });
167
+ }
168
+ return b.graph();
169
+ },
170
+ });
171
+
172
+ /** This function gives the cells of the square ring `r` around the cell (0, 0), in a fixed order. */
173
+ function ring(r: number): [number, number][] {
174
+ const out: [number, number][] = [];
175
+ for (let dy = -r; dy <= r; dy++) for (let dx = -r; dx <= r; dx++) if (Math.max(Math.abs(dx), Math.abs(dy)) === r) out.push([dx, dy]);
176
+ return out;
177
+ }
178
+
179
+ /** The dimensions of `fan-out`. */
180
+ const FAN_OUT_DIMENSIONS: readonly CorpusDimension[] = Object.freeze([
181
+ count("leaves", 4, 240, 96, "The number of leaves. The hub links to each leaf."),
182
+ count("cross", 0, 120, 8, "The number of tries for a link between two leaves."),
183
+ count("gap", 8, 120, 36, "The free space between two leaves, in units."),
184
+ ]);
185
+
186
+ /** The family `fan-out`: one large hub in the middle, with a link to each leaf on the square rings around it. */
187
+ export const FAN_OUT: HighwaysFamily = caseFamily({
188
+ name: "fan-out",
189
+ meaning: "A wide fan-out: one hub in the middle links to each leaf on the rings around it.",
190
+ dimensions: FAN_OUT_DIMENSIONS,
191
+ generator: "family.fan-out",
192
+ seeding: SEEDING,
193
+ make: (v, _seed, random) => {
194
+ const b = new Builder();
195
+ const leaves = v["leaves"]!;
196
+ const gap = v["gap"]!;
197
+ const cw = 80 + gap;
198
+ const ch = 32 + gap;
199
+ // The hub covers the cells from -1 to 1. The leaves take cells of the rings 2 and more, with a quarter of the cells free.
200
+ const cells: [number, number][] = [];
201
+ let r = 2;
202
+ while (cells.length < leaves + Math.floor(leaves / 4)) cells.push(...ring(r++));
203
+ const reach = r - 1;
204
+ const at = (dx: number, dy: number): [number, number] => [(dx + reach) * cw, (dy + reach) * ch];
205
+ const [hx, hy] = at(-1, -1);
206
+ const hub = b.node("hub", hx, hy, 2 * cw + 80, 2 * ch + 32);
207
+ const ids: string[] = [];
208
+ for (const i of choose(random, cells.length, leaves)) {
209
+ const [x, y] = at(...cells[i]!);
210
+ const w = int(random, 48, 80);
211
+ const h = int(random, 20, 32);
212
+ ids.push(b.node(`l${ids.length}`, x, y, w, h));
213
+ }
214
+ for (const id of ids) b.link({ node: hub }, { node: id });
215
+ for (let n = 0; n < v["cross"]!; n++) b.link({ node: ids[int(random, 0, ids.length - 1)]! }, { node: ids[int(random, 0, ids.length - 1)]! });
216
+ return b.graph();
217
+ },
218
+ });
219
+
220
+ /** The dimensions of `two-hubs`. */
221
+ const TWO_HUBS_DIMENSIONS: readonly CorpusDimension[] = Object.freeze([
222
+ count("shared", 0, 160, 48, "The number of nodes between the hubs. Each of them has a link from each hub."),
223
+ count("own", 0, 80, 12, "The number of nodes on the far side of each hub, with a link from that hub only."),
224
+ count("span", 1, 12, 5, "The number of columns of nodes between the two hubs."),
225
+ count("gap", 8, 120, 36, "The free space between two nodes, in units."),
226
+ ]);
227
+
228
+ /** The family `two-hubs`: two tall hubs with many nodes between them. Each such node has a link from each hub. A few nodes of one hub are on its far side. */
229
+ export const TWO_HUBS: HighwaysFamily = caseFamily({
230
+ name: "two-hubs",
231
+ meaning: "Two hubs that share many targets between them: two fans that overlap.",
232
+ dimensions: TWO_HUBS_DIMENSIONS,
233
+ generator: "family.two-hubs",
234
+ seeding: SEEDING,
235
+ make: (v, _seed, random) => {
236
+ const b = new Builder();
237
+ const shared = v["shared"]!;
238
+ const own = v["own"]!;
239
+ const span = v["span"]!;
240
+ const gap = v["gap"]!;
241
+ const cw = 64 + gap;
242
+ const ch = 28 + gap;
243
+ const ceilDiv = (a: number, d: number): number => Math.floor((a + d - 1) / d);
244
+ const rows = Math.max(1, ceilDiv(shared + Math.floor(shared / 4), span), ceilDiv(own + Math.floor(own / 4), 2));
245
+ // The columns 0 and 1 are on the far side of hub A, at the column 2. The shared nodes take the columns 3 to span + 2.
246
+ // Hub B is at the column span + 3, and its far side has the next 2 columns.
247
+ const hubH = rows * ch - gap;
248
+ const a = b.node("A", 2 * cw, 0, 64, hubH);
249
+ const z = b.node("B", (span + 3) * cw, 0, 64, hubH);
250
+ const place = (prefix: string, first: number, cols: number, n: number): string[] =>
251
+ choose(random, cols * rows, n).map((c, i) => b.node(`${prefix}${i}`, (first + (c % cols)) * cw, Math.floor(c / cols) * ch, 64, 28));
252
+ const mid = place("m", 3, span, shared);
253
+ const left = place("a", 0, 2, own);
254
+ const right = place("b", span + 4, 2, own);
255
+ for (const id of mid) {
256
+ b.link({ node: a }, { node: id });
257
+ b.link({ node: z }, { node: id });
258
+ }
259
+ for (const id of left) b.link({ node: a }, { node: id });
260
+ for (const id of right) b.link({ node: z }, { node: id });
261
+ b.link({ node: a }, { node: z });
262
+ return b.graph();
263
+ },
264
+ });
265
+
266
+ /** The dimensions of `uneven-sizes`. */
267
+ const UNEVEN_SIZES_DIMENSIONS: readonly CorpusDimension[] = Object.freeze([
268
+ count("small", 4, 300, 90, "The number of small boxes."),
269
+ count("large", 1, 16, 4, "The number of large boxes."),
270
+ count("ratio", 2, 10, 5, "The size of a large box, divided by the size of a small box."),
271
+ numberDimension({ name: "edgesPerNode", kind: "real", min: 0.5, max: 4, default: 1.5, meaning: "The number of tries for an edge, divided by the number of boxes." }),
272
+ numberDimension({ name: "toLarge", kind: "real", min: 0, max: 1, default: 0.5, meaning: "The share of the edges with a large box at one end." }),
273
+ count("gap", 8, 120, 30, "The free space between two boxes, in units."),
274
+ ]);
275
+
276
+ /** The family `uneven-sizes`: a few large boxes and many small boxes in shelves, in a random order. Many edges end on a large box. */
277
+ export const UNEVEN_SIZES: HighwaysFamily = caseFamily({
278
+ name: "uneven-sizes",
279
+ meaning: "Very uneven box sizes: a few large boxes among many small boxes.",
280
+ dimensions: UNEVEN_SIZES_DIMENSIONS,
281
+ generator: "family.uneven-sizes",
282
+ seeding: SEEDING,
283
+ make: (v, _seed, random) => {
284
+ const b = new Builder();
285
+ const ratio = v["ratio"]!;
286
+ const gap = v["gap"]!;
287
+ const boxes: { id: string; w: number; h: number }[] = [];
288
+ for (let i = 0; i < v["large"]!; i++) boxes.push({ id: `L${i}`, w: ratio * int(random, 48, 72), h: ratio * int(random, 24, 36) });
289
+ for (let i = 0; i < v["small"]!; i++) boxes.push({ id: `s${i}`, w: int(random, 40, 80), h: int(random, 20, 40) });
290
+ const order = shuffle(random, boxes.length);
291
+ // The shelves: each box goes to the right of the last box, or on a new shelf below when the shelf is full.
292
+ const area = boxes.reduce((s, x) => s + (x.w + gap) * (x.h + gap), 0);
293
+ const width = Math.max(...boxes.map((x) => x.w), Math.floor((3 * ceilSqrt(area)) / 2));
294
+ const places = new Map<string, [number, number]>();
295
+ let x = 0;
296
+ let y = 0;
297
+ let shelf = 0;
298
+ for (const i of order) {
299
+ const box = boxes[i]!;
300
+ if (x > 0 && x + box.w > width) {
301
+ x = 0;
302
+ y += shelf + gap;
303
+ shelf = 0;
304
+ }
305
+ places.set(box.id, [x, y]);
306
+ x += box.w + gap + int(random, 0, gap);
307
+ shelf = Math.max(shelf, box.h);
308
+ }
309
+ for (const box of boxes) {
310
+ const [px, py] = places.get(box.id)!;
311
+ b.node(box.id, px, py, box.w, box.h);
312
+ }
313
+ const ids = boxes.map((x) => x.id);
314
+ const large = ids.slice(0, v["large"]!);
315
+ const tries = Math.round(v["edgesPerNode"]! * ids.length);
316
+ for (let n = 0; n < tries; n++) {
317
+ const s = ids[int(random, 0, ids.length - 1)]!;
318
+ const t = random() < v["toLarge"]! ? large[int(random, 0, large.length - 1)]! : ids[int(random, 0, ids.length - 1)]!;
319
+ b.link({ node: s }, { node: t });
320
+ }
321
+ return b.graph();
322
+ },
323
+ });
324
+
325
+ /** The dimensions of `thin-bars`. */
326
+ const THIN_BARS_DIMENSIONS: readonly CorpusDimension[] = Object.freeze([
327
+ count("lanes", 2, 40, 16, "The number of lanes. Each lane is a row of a timeline."),
328
+ count("bars", 1, 12, 5, "The number of bars in each lane."),
329
+ count("links", 0, 240, 64, "The number of tries for a link between two bars of lanes near each other."),
330
+ count("thick", 4, 32, 12, "The height of each bar, in units."),
331
+ count("gap", 4, 80, 16, "The free space between two lanes, in units."),
332
+ ]);
333
+
334
+ /** The family `thin-bars`: long thin bars in lanes, as a timeline or the rows of a table. The links join bars of lanes near each other. */
335
+ export const THIN_BARS: HighwaysFamily = caseFamily({
336
+ name: "thin-bars",
337
+ meaning: "Long thin boxes in lanes, as a timeline or the rows of a table.",
338
+ dimensions: THIN_BARS_DIMENSIONS,
339
+ generator: "family.thin-bars",
340
+ seeding: SEEDING,
341
+ make: (v, _seed, random) => {
342
+ const b = new Builder();
343
+ const lanes = v["lanes"]!;
344
+ const bars = v["bars"]!;
345
+ const thick = v["thick"]!;
346
+ for (let i = 0; i < lanes; i++) {
347
+ let x = int(random, 0, 80);
348
+ for (let j = 0; j < bars; j++) {
349
+ const w = int(random, 120, 420);
350
+ b.node(`t${i}-${j}`, x, i * (thick + v["gap"]!), w, thick);
351
+ x += w + int(random, 24, 120);
352
+ }
353
+ }
354
+ for (let n = 0; n < v["links"]!; n++) {
355
+ const a = int(random, 0, lanes - 1);
356
+ const c = Math.max(0, Math.min(lanes - 1, a + int(random, -3, 3)));
357
+ b.link({ node: `t${a}-${int(random, 0, bars - 1)}` }, { node: `t${c}-${int(random, 0, bars - 1)}` });
358
+ }
359
+ return b.graph();
360
+ },
361
+ });
362
+
363
+ /** The dimensions of `sparse-long`. */
364
+ const SPARSE_LONG_DIMENSIONS: readonly CorpusDimension[] = Object.freeze([
365
+ count("nodes", 4, 120, 36, "The number of nodes."),
366
+ count("edges", 0, 160, 36, "The number of tries for a long edge."),
367
+ count("spacing", 200, 1200, 480, "The size of the square cell of each node, in units."),
368
+ count("far", 1, 8, 4, "The number of random targets of an edge. The edge takes the target farthest from its source."),
369
+ ]);
370
+
371
+ /** The family `sparse-long`: few nodes far apart on a large board, and edges that go to far targets. */
372
+ export const SPARSE_LONG: HighwaysFamily = caseFamily({
373
+ name: "sparse-long",
374
+ meaning: "A sparse board: few nodes far apart, with long edges across the board.",
375
+ dimensions: SPARSE_LONG_DIMENSIONS,
376
+ generator: "family.sparse-long",
377
+ seeding: SEEDING,
378
+ make: (v, _seed, random) => {
379
+ const b = new Builder();
380
+ const n = v["nodes"]!;
381
+ const spacing = v["spacing"]!;
382
+ const cols = ceilSqrt(n);
383
+ for (let i = 0; i < n; i++) {
384
+ const w = int(random, 80, 120);
385
+ const h = int(random, 32, 56);
386
+ b.node(`n${i}`, (i % cols) * spacing + int(random, 0, spacing - w - 40), Math.floor(i / cols) * spacing + int(random, 0, spacing - h - 40), w, h);
387
+ }
388
+ const dist = (i: number, j: number): number => Math.abs((i % cols) - (j % cols)) + Math.abs(Math.floor(i / cols) - Math.floor(j / cols));
389
+ for (let e = 0; e < v["edges"]!; e++) {
390
+ const s = int(random, 0, n - 1);
391
+ let best = -1;
392
+ let far = -1;
393
+ for (let f = 0; f < v["far"]!; f++) {
394
+ const t = int(random, 0, n - 1);
395
+ if (t !== s && dist(s, t) > far) {
396
+ best = t;
397
+ far = dist(s, t);
398
+ }
399
+ }
400
+ if (best >= 0) b.link({ node: `n${s}` }, { node: `n${best}` });
401
+ }
402
+ return b.graph();
403
+ },
404
+ });
405
+
406
+ /** The dimensions of `local-mesh`. */
407
+ const LOCAL_MESH_DIMENSIONS: readonly CorpusDimension[] = Object.freeze([
408
+ count("cols", 2, 40, 12, "The number of columns of nodes."),
409
+ count("rows", 2, 30, 9, "The number of rows of nodes."),
410
+ count("degree", 1, 6, 3, "The number of tries for an edge to one of the 8 nodes around a node, for each node."),
411
+ count("gap", 8, 80, 20, "The free space between two nodes, in units."),
412
+ count("jitter", 0, 40, 8, "The largest random move of a node in its cell, in units. The move stays 8 units or more below the gap."),
413
+ ]);
414
+
415
+ /** The family `local-mesh`: a grid of small nodes with many short edges, each to a node of the 8 nodes around it. */
416
+ export const LOCAL_MESH: HighwaysFamily = caseFamily({
417
+ name: "local-mesh",
418
+ meaning: "Many short local edges: each node links to nodes next to it in a grid.",
419
+ dimensions: LOCAL_MESH_DIMENSIONS,
420
+ generator: "family.local-mesh",
421
+ seeding: SEEDING,
422
+ make: (v, _seed, random) => {
423
+ const b = new Builder();
424
+ const cols = v["cols"]!;
425
+ const rows = v["rows"]!;
426
+ const gap = v["gap"]!;
427
+ const move = Math.min(v["jitter"]!, gap - 8);
428
+ const id = (r: number, c: number): string => `m${r}-${c}`;
429
+ for (let r = 0; r < rows; r++)
430
+ for (let c = 0; c < cols; c++) b.node(id(r, c), c * (48 + gap) + int(random, 0, move), r * (28 + gap) + int(random, 0, move), 48, 28);
431
+ for (let r = 0; r < rows; r++)
432
+ for (let c = 0; c < cols; c++)
433
+ for (let t = 0; t < v["degree"]!; t++) {
434
+ const rr = r + int(random, -1, 1);
435
+ const cc = c + int(random, -1, 1);
436
+ if (rr >= 0 && rr < rows && cc >= 0 && cc < cols) b.link({ node: id(r, c) }, { node: id(rr, cc) });
437
+ }
438
+ return b.graph();
439
+ },
440
+ });
441
+
442
+ /** The values of the dimension `layout` of `bipartite`. */
443
+ export const BIPARTITE_LAYOUTS: readonly string[] = Object.freeze(["rows", "mixed"]);
444
+
445
+ /** The dimensions of `bipartite`. */
446
+ const BIPARTITE_DIMENSIONS: readonly CorpusDimension[] = Object.freeze([
447
+ count("left", 2, 16, 6, "The number of nodes of the first side of the complete bipartite part."),
448
+ count("right", 2, 16, 6, "The number of nodes of the second side. Each node of the first side links to each node of the second side."),
449
+ count("noise", 0, 120, 24, "The number of other nodes, in a band below the bipartite part."),
450
+ count("extra", 0, 160, 24, "The number of tries for a random link between two nodes of the board."),
451
+ enumDimension(
452
+ "layout",
453
+ BIPARTITE_LAYOUTS,
454
+ "rows",
455
+ "The places of the bipartite part. `rows`: each side in one row, one row above the other. `mixed`: all the nodes at random cells of one grid.",
456
+ ),
457
+ count("gap", 16, 160, 64, "The free space between two nodes, in units."),
458
+ ]);
459
+
460
+ /**
461
+ * The family `bipartite`: a complete bipartite part, with dense crossings that no plan can remove, and other nodes with random links.
462
+ * At 3 nodes or more on each side, the part has no drawing without crossings.
463
+ */
464
+ export const BIPARTITE: HighwaysFamily = caseFamily({
465
+ name: "bipartite",
466
+ meaning: "An adversarial board: a complete bipartite part whose crossings no plan can remove.",
467
+ dimensions: BIPARTITE_DIMENSIONS,
468
+ generator: "family.bipartite",
469
+ seeding: SEEDING,
470
+ make: (v, _seed, random, dims) => {
471
+ const b = new Builder();
472
+ const left = v["left"]!;
473
+ const right = v["right"]!;
474
+ const noise = v["noise"]!;
475
+ const gap = v["gap"]!;
476
+ const cw = 80 + gap;
477
+ const ch = 36 + gap;
478
+ const ids = [...Array.from({ length: left }, (_, i) => `a${i}`), ...Array.from({ length: right }, (_, j) => `b${j}`), ...Array.from({ length: noise }, (_, k) => `n${k}`)];
479
+ if (dims["layout"] === "mixed") {
480
+ const cols = ceilSqrt(ids.length + Math.floor(ids.length / 4));
481
+ const cells = choose(random, cols * cols, ids.length);
482
+ const order = shuffle(random, ids.length);
483
+ ids.forEach((id, i) => {
484
+ const c = cells[order[i]!]!;
485
+ b.node(id, (c % cols) * cw, Math.floor(c / cols) * ch, 80, 36);
486
+ });
487
+ } else {
488
+ // The first side is in the row 0, and the second side is in the row 3. The other nodes take random cells of the rows 5 and more.
489
+ const cols = Math.max(left, right, ceilSqrt(noise));
490
+ for (let i = 0; i < left; i++) b.node(`a${i}`, (i + Math.floor((cols - left) / 2)) * cw, 0, 80, 36);
491
+ for (let j = 0; j < right; j++) b.node(`b${j}`, (j + Math.floor((cols - right) / 2)) * cw, 3 * ch, 80, 36);
492
+ const band = Math.max(1, Math.floor((noise + Math.floor(noise / 4) + cols - 1) / cols));
493
+ choose(random, cols * band, noise).forEach((c, k) => b.node(`n${k}`, (c % cols) * cw, (5 + Math.floor(c / cols)) * ch, 80, 36));
494
+ }
495
+ for (let i = 0; i < left; i++) for (let j = 0; j < right; j++) b.link({ node: `a${i}` }, { node: `b${j}` });
496
+ for (let n = 0; n < v["extra"]!; n++) b.link({ node: ids[int(random, 0, ids.length - 1)]! }, { node: ids[int(random, 0, ids.length - 1)]! });
497
+ return b.graph();
498
+ },
499
+ });
500
+
501
+ /** The 9 families of CORPUS-FAMILIES, in the order of `screen-v3`. */
502
+ export const COVERAGE_FAMILIES: readonly HighwaysFamily[] = Object.freeze([FRAME_ROWS, PIPELINE, FAN_OUT, TWO_HUBS, UNEVEN_SIZES, THIN_BARS, SPARSE_LONG, LOCAL_MESH, BIPARTITE]);