@mark1russell7/highways 0.8.1 → 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.
- package/README.md +74 -8
- package/build/index.d.ts +705 -183
- package/build/index.d.ts.map +1 -1
- package/build/index.js +4079 -2639
- package/build/index.js.map +1 -1
- package/package.json +3 -2
- package/src/bench/compare.ts +174 -5
- package/src/bench/room.ts +1 -1
- package/src/bench/run.ts +38 -3
- package/src/bench/screen/generators.ts +11 -41
- package/src/bench/screen/screen.ts +16 -10
- package/src/bench/screen/set.ts +156 -49
- package/src/bench/suite.ts +110 -12
- package/src/cases.ts +64 -18
- package/src/families/cases.ts +108 -0
- package/src/families/fixtures.ts +101 -0
- package/src/families/index.ts +36 -0
- package/src/families/model.ts +55 -0
- package/src/fixtures.ts +8 -2
- package/src/highways/soft-keys/index.ts +8 -4
- package/src/index.ts +8 -0
- package/src/lane-order.ts +37 -0
- package/src/lane-orders/stretch/index.ts +486 -0
- package/src/lane-orders/stretch/router.ts +22 -0
- package/src/levels/depths/index.ts +57 -0
- package/src/plugs/facing/index.ts +22 -0
- package/src/portfolio/router.ts +44 -0
- package/src/routers/corners.ts +27 -3
- package/src/routers/sections.ts +22 -40
- package/src/routers/shared.ts +26 -7
- package/src/routers/trunks.ts +21 -3
- package/src/segments.ts +4 -25
- package/src/strategies/builtins.ts +54 -2
- package/src/strategies/model.ts +55 -10
- package/src/strategies/options.ts +36 -4
- package/src/strategies/schemas.ts +68 -5
- package/src/whole/pipeline/index.ts +66 -0
package/src/bench/screen/set.ts
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
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
|
-
|
|
31
|
-
const
|
|
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
|
-
/**
|
|
35
|
-
function
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
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
|
-
*
|
|
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
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
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
|
-
/**
|
|
90
|
-
|
|
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
|
-
/**
|
|
121
|
-
|
|
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
|
+
}
|
package/src/bench/suite.ts
CHANGED
|
@@ -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
|
|
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
|
-
|
|
168
|
-
|
|
169
|
-
|
|
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 =
|
|
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 <
|
|
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 =
|
|
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 *
|
|
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 =
|
|
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() *
|
|
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
|
];
|