@mark1russell7/highways 0.5.0 → 0.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +43 -2
- package/build/index.d.ts +1472 -63
- package/build/index.d.ts.map +1 -1
- package/build/index.js +10007 -3406
- package/build/index.js.map +1 -1
- package/package.json +11 -2
- package/src/bench/exact/index.ts +72 -11
- package/src/bench/exact/optima-exact-v1-bounds.json +14409 -0
- package/src/bench/exact/optima-exact-v1.json +4809 -0
- package/src/bench/exact/solve.ts +129 -0
- package/src/bench/screen/activation.ts +3 -0
- package/src/bench/screen/bounds.ts +140 -0
- package/src/bench/screen/compare.ts +26 -0
- package/src/bench/screen/exact-set.ts +100 -0
- package/src/bench/screen/exercise.ts +290 -0
- package/src/bench/screen/forms.ts +14 -0
- package/src/bench/screen/gap.ts +316 -0
- package/src/bench/screen/hash.ts +7 -76
- package/src/bench/screen/index.ts +6 -0
- package/src/bench/screen/measures.ts +71 -1
- package/src/bench/screen/portfolio-pick.ts +20 -0
- package/src/bench/screen/screen.ts +50 -2
- package/src/bench/screen/set.ts +3 -2
- package/src/highways/areas/index.ts +160 -0
- package/src/highways/confluent/index.ts +154 -0
- package/src/highways/confluent/k23.test-util.ts +8 -0
- package/src/highways/consensus/index.ts +129 -0
- package/src/highways/crossbars/index.ts +184 -0
- package/src/highways/cubu/index.ts +26 -0
- package/src/highways/divided/index.ts +111 -0
- package/src/highways/fdeb/index.ts +193 -0
- package/src/highways/frechet/index.ts +232 -0
- package/src/highways/hierarchy/index.ts +140 -0
- package/src/highways/index.ts +64 -0
- package/src/highways/kdeeb/index.ts +355 -0
- package/src/highways/link-communities/index.ts +189 -0
- package/src/highways/mesh/index.ts +209 -0
- package/src/highways/mlseb/index.ts +204 -0
- package/src/highways/p-hub/index.ts +248 -0
- package/src/highways/power/index.ts +117 -0
- package/src/highways/resistance/index.ts +159 -0
- package/src/highways/sbeb/index.ts +303 -0
- package/src/highways/scale-space/index.ts +135 -0
- package/src/highways/spectral/index.ts +391 -0
- package/src/highways/territories/index.ts +150 -0
- package/src/highways/trace.ts +153 -0
- package/src/index.ts +1 -0
- package/src/portfolio/bands.ts +33 -0
- package/src/portfolio/fit.ts +29 -0
- package/src/portfolio/index.ts +6 -0
- package/src/portfolio/key.ts +98 -0
- package/src/portfolio/members.ts +171 -0
- package/src/portfolio/router.ts +133 -0
- package/src/portfolio/sets.ts +28 -0
- package/src/strategies/builtins.ts +222 -1
- package/src/strategies/schemas.ts +280 -1
package/src/bench/screen/hash.ts
CHANGED
|
@@ -1,14 +1,10 @@
|
|
|
1
|
-
// The
|
|
1
|
+
// The hash, the canonical JSON and the seeded random numbers of the screen. `sha256Hex`, `canonicalJson` and `RECORD_EPS` come from
|
|
2
|
+
// `@mark1russell7/layout-contract` (PORTFOLIO-FIT). The other functions have no equal in the contract, so they stay here.
|
|
3
|
+
import { canonicalJson, sha256Hex } from "@mark1russell7/layout-contract";
|
|
2
4
|
|
|
3
|
-
|
|
4
|
-
0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5, 0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, 0x72be5d74, 0x80deb1fe,
|
|
5
|
-
0x9bdc06a7, 0xc19bf174, 0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da, 0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7,
|
|
6
|
-
0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967, 0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13, 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85, 0xa2bfe8a1, 0xa81a664b,
|
|
7
|
-
0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070, 0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3,
|
|
8
|
-
0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, 0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2,
|
|
9
|
-
];
|
|
5
|
+
export { canonicalJson, RECORD_EPS, sha256Hex } from "@mark1russell7/layout-contract";
|
|
10
6
|
|
|
11
|
-
/** This function gives the bytes of a string in UTF-8. */
|
|
7
|
+
/** This function gives the bytes of a string in UTF-8. The contract does not export its own `utf8`, so the core keeps this function. */
|
|
12
8
|
export function utf8(text: string): Uint8Array {
|
|
13
9
|
const out: number[] = [];
|
|
14
10
|
for (const ch of text) {
|
|
@@ -21,75 +17,10 @@ export function utf8(text: string): Uint8Array {
|
|
|
21
17
|
return Uint8Array.from(out);
|
|
22
18
|
}
|
|
23
19
|
|
|
24
|
-
/** This function gives the SHA-256 digest of a string, as 64 lowercase hex characters. It encodes the string in UTF-8. */
|
|
25
|
-
export function sha256Hex(text: string): string {
|
|
26
|
-
const bytes = utf8(text);
|
|
27
|
-
const bits = bytes.length * 8;
|
|
28
|
-
const total = Math.ceil((bytes.length + 9) / 64) * 64;
|
|
29
|
-
const data = new Uint8Array(total);
|
|
30
|
-
data.set(bytes);
|
|
31
|
-
data[bytes.length] = 0x80;
|
|
32
|
-
const view = new DataView(data.buffer);
|
|
33
|
-
view.setUint32(total - 8, Math.floor(bits / 0x100000000));
|
|
34
|
-
view.setUint32(total - 4, bits >>> 0);
|
|
35
|
-
const h = [0x6a09e667, 0xbb67ae85, 0x3c6ef372, 0xa54ff53a, 0x510e527f, 0x9b05688c, 0x1f83d9ab, 0x5be0cd19];
|
|
36
|
-
const w = new Uint32Array(64);
|
|
37
|
-
const rotr = (x: number, n: number): number => (x >>> n) | (x << (32 - n));
|
|
38
|
-
for (let off = 0; off < total; off += 64) {
|
|
39
|
-
for (let i = 0; i < 16; i++) w[i] = view.getUint32(off + 4 * i);
|
|
40
|
-
for (let i = 16; i < 64; i++) {
|
|
41
|
-
const a = w[i - 15]!;
|
|
42
|
-
const b = w[i - 2]!;
|
|
43
|
-
const s0 = rotr(a, 7) ^ rotr(a, 18) ^ (a >>> 3);
|
|
44
|
-
const s1 = rotr(b, 17) ^ rotr(b, 19) ^ (b >>> 10);
|
|
45
|
-
w[i] = (w[i - 16]! + s0 + w[i - 7]! + s1) >>> 0;
|
|
46
|
-
}
|
|
47
|
-
let [a, b, c, d, e, f, g, k] = h as [number, number, number, number, number, number, number, number];
|
|
48
|
-
for (let i = 0; i < 64; i++) {
|
|
49
|
-
const s1 = rotr(e, 6) ^ rotr(e, 11) ^ rotr(e, 25);
|
|
50
|
-
const ch = (e & f) ^ (~e & g);
|
|
51
|
-
const t1 = (k + s1 + ch + K[i]! + w[i]!) >>> 0;
|
|
52
|
-
const s0 = rotr(a, 2) ^ rotr(a, 13) ^ rotr(a, 22);
|
|
53
|
-
const maj = (a & b) ^ (a & c) ^ (b & c);
|
|
54
|
-
const t2 = (s0 + maj) >>> 0;
|
|
55
|
-
k = g;
|
|
56
|
-
g = f;
|
|
57
|
-
f = e;
|
|
58
|
-
e = (d + t1) >>> 0;
|
|
59
|
-
d = c;
|
|
60
|
-
c = b;
|
|
61
|
-
b = a;
|
|
62
|
-
a = (t1 + t2) >>> 0;
|
|
63
|
-
}
|
|
64
|
-
h[0] = (h[0]! + a) >>> 0;
|
|
65
|
-
h[1] = (h[1]! + b) >>> 0;
|
|
66
|
-
h[2] = (h[2]! + c) >>> 0;
|
|
67
|
-
h[3] = (h[3]! + d) >>> 0;
|
|
68
|
-
h[4] = (h[4]! + e) >>> 0;
|
|
69
|
-
h[5] = (h[5]! + f) >>> 0;
|
|
70
|
-
h[6] = (h[6]! + g) >>> 0;
|
|
71
|
-
h[7] = (h[7]! + k) >>> 0;
|
|
72
|
-
}
|
|
73
|
-
return h.map((x) => x.toString(16).padStart(8, "0")).join("");
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
/**
|
|
77
|
-
* This function writes a value as canonical JSON: the keys of each object in sorted order, and no whitespace.
|
|
78
|
-
* It drops a key whose value is `undefined`, as `JSON.stringify` does.
|
|
79
|
-
*/
|
|
80
|
-
export function canonicalJson(value: unknown): string {
|
|
81
|
-
if (value === null || typeof value !== "object") return JSON.stringify(value) ?? "null";
|
|
82
|
-
if (Array.isArray(value)) return `[${value.map((v) => (v === undefined ? "null" : canonicalJson(v))).join(",")}]`;
|
|
83
|
-
const o = value as Record<string, unknown>;
|
|
84
|
-
const keys = Object.keys(o)
|
|
85
|
-
.filter((k) => o[k] !== undefined)
|
|
86
|
-
.sort();
|
|
87
|
-
return `{${keys.map((k) => `${JSON.stringify(k)}:${canonicalJson(o[k])}`).join(",")}}`;
|
|
88
|
-
}
|
|
89
|
-
|
|
90
20
|
/**
|
|
91
21
|
* This function gives the hash of a JSON document: the first 16 hex characters of the SHA-256 of its canonical JSON.
|
|
92
22
|
* The document does not include its own field `hash`. The function removes that field before it calculates the hash.
|
|
23
|
+
* The `documentHash` of the contract puts each number on a grid first, so it is not equal to this function. The hashes of the corpus files use this function.
|
|
93
24
|
*/
|
|
94
25
|
export function documentHash(value: object): string {
|
|
95
26
|
const { hash: _hash, ...rest } = value as { hash?: unknown };
|
|
@@ -122,7 +53,7 @@ export function mulberry32(seed: number): () => number {
|
|
|
122
53
|
};
|
|
123
54
|
}
|
|
124
55
|
|
|
125
|
-
/** This function rounds a number to 3 decimals, as the files of a corpus keep it. The value `-0` becomes `0`. */
|
|
56
|
+
/** This function rounds a number to 3 decimals (a step of `RECORD_EPS`), as the files of a corpus keep it. The value `-0` becomes `0`. */
|
|
126
57
|
export function round3(value: number): number {
|
|
127
58
|
const r = Math.round(value * 1000) / 1000;
|
|
128
59
|
return r === 0 ? 0 : r;
|
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
export * from "./hash.ts";
|
|
2
2
|
export * from "./forms.ts";
|
|
3
3
|
export * from "./measures.ts";
|
|
4
|
+
export * from "./compare.ts";
|
|
4
5
|
export * from "./set.ts";
|
|
5
6
|
export * from "./screen.ts";
|
|
6
7
|
export * from "./generators.ts";
|
|
8
|
+
export * from "./exact-set.ts";
|
|
9
|
+
export * from "./gap.ts";
|
|
10
|
+
export * from "./activation.ts";
|
|
11
|
+
export * from "./exercise.ts";
|
|
12
|
+
export * from "./portfolio-pick.ts";
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { Objective } from "../measures/pareto.ts";
|
|
2
|
+
import type { CompareSpec, MinEffect } from "./compare.ts";
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
5
|
* One measure of a record. `key` is the key of the measure in the values of `runSuite` and `compareStep`.
|
|
@@ -18,8 +19,55 @@ export interface MeasureInfo {
|
|
|
18
19
|
* Then two values compare only on the same input. The value is `false` when the measure scores the routes or the curves for a fixed placement.
|
|
19
20
|
*/
|
|
20
21
|
readonly placementDependent: boolean;
|
|
22
|
+
/**
|
|
23
|
+
* The smallest change that counts as better or worse in `compareMeasure` (WIN-RES). `MIN_EFFECTS` gives it by the unit, with its reason.
|
|
24
|
+
* A measure without this field has the threshold `RECORD_EPS`.
|
|
25
|
+
*/
|
|
26
|
+
readonly minEffect?: MinEffect;
|
|
21
27
|
}
|
|
22
28
|
|
|
29
|
+
/** One rule of `MIN_EFFECTS`: the units, the minimum effect of a measure with one of these units, and the reason. */
|
|
30
|
+
export interface MinEffectRule {
|
|
31
|
+
readonly units: readonly string[];
|
|
32
|
+
/** The minimum effect. The value `undefined` gives the threshold `RECORD_EPS`. */
|
|
33
|
+
readonly minEffect: MinEffect | undefined;
|
|
34
|
+
readonly reason: string;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* The minimum effect of each measure of `MEASURES`, by the unit of the measure (WIN-RES).
|
|
39
|
+
* Each unit of `MEASURES` is in one rule. The test of the module checks it.
|
|
40
|
+
*/
|
|
41
|
+
export const MIN_EFFECTS: readonly MinEffectRule[] = Object.freeze([
|
|
42
|
+
{
|
|
43
|
+
units: ["0 to 1", "-1 to 1", "-0.5 to 1", "share of the gap", "0 or 1"],
|
|
44
|
+
minEffect: undefined,
|
|
45
|
+
reason: "A share or a flag of 0 or 1 has the precision of a written file. Thus the threshold is `RECORD_EPS`.",
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
units: ["ratio", "IQR over the median", "no unit", "half turns for each route", "curvature times the unit of the board"],
|
|
49
|
+
minEffect: undefined,
|
|
50
|
+
reason: "A stretch or another value without a unit has the precision of a written file. Thus the threshold is `RECORD_EPS`.",
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
units: ["count", "count for each edge", "count for each route"],
|
|
54
|
+
minEffect: 0,
|
|
55
|
+
reason: "A count or a mean of counts changes in steps. The threshold is `RECORD_EPS`, so a change of 1 counts.",
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
units: ["units", "units of the board"],
|
|
59
|
+
minEffect: { rel: 0.01 },
|
|
60
|
+
reason: "A length in pixels moves with each small move of a point. A change of 1% or less is not visible.",
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
units: ["degrees"],
|
|
64
|
+
minEffect: { rel: 0.01 },
|
|
65
|
+
reason: "An angle is a continuous value of the geometry, as a length. A change of 1% or less is not visible.",
|
|
66
|
+
},
|
|
67
|
+
]);
|
|
68
|
+
|
|
69
|
+
const MIN_EFFECT_BY_UNIT: ReadonlyMap<string, MinEffectRule> = new Map(MIN_EFFECTS.flatMap((r) => r.units.map((u) => [u, r] as const)));
|
|
70
|
+
|
|
23
71
|
/** The text of a row of `MEASURES`: the key, the id, the name, the unit, the better side and the placement dependence. */
|
|
24
72
|
type Row = readonly [key: string, id: string, name: string, unit: string, better: "lower" | "higher", placement: boolean];
|
|
25
73
|
|
|
@@ -57,6 +105,16 @@ const ROWS: readonly Row[] = [
|
|
|
57
105
|
["B3.median", "B3", "route.bundle.stretch.median", "ratio", "lower", false],
|
|
58
106
|
["B3.q95", "B3", "route.bundle.stretch.q95", "ratio", "lower", false],
|
|
59
107
|
["B6.max", "B6", "route.bundle.merge-angle.max", "degrees", "lower", false],
|
|
108
|
+
// MEAS2, on the inputs of `exact-v1`: the largest B3, the largest stretch of the exact model, and the gap closed (B10) to the exact optimum at the smallest cap at or above that stretch.
|
|
109
|
+
["B3.max", "B3", "route.bundle.stretch.max", "ratio", "lower", false],
|
|
110
|
+
["gap.stretch", "B3", "route.bundle.stretch.model.max", "ratio", "lower", false],
|
|
111
|
+
["gap.mixed", "B10", "route.bundle.gap-closed.mixed", "0 or 1", "lower", false],
|
|
112
|
+
["gap", "B10", "route.bundle.gap-closed", "share of the gap", "higher", false],
|
|
113
|
+
["gap.cap", "B10", "route.bundle.gap-closed.cap", "ratio", "lower", false],
|
|
114
|
+
["gap.collection", "B10", "route.bundle.gap-closed.collection", "share of the gap", "higher", false],
|
|
115
|
+
["gap.cap.collection", "B10", "route.bundle.gap-closed.cap.collection", "ratio", "lower", false],
|
|
116
|
+
["gap.all", "B10", "route.bundle.gap-closed.all", "share of the gap", "higher", false],
|
|
117
|
+
["gap.cap.all", "B10", "route.bundle.gap-closed.cap.all", "ratio", "lower", false],
|
|
60
118
|
// MEAS3A, for the steps `router` and `highway`. B5, B8 and B9 read the runs and the trunks of the result. B8 is good or bad only in context, as B2.
|
|
61
119
|
["B5.crossings", "B5", "route.bundle.lane-crossings", "count", "lower", false],
|
|
62
120
|
["B7.ratio", "B7", "route.bundle.occupied-area", "ratio", "lower", false],
|
|
@@ -123,7 +181,19 @@ const ROWS: readonly Row[] = [
|
|
|
123
181
|
* A joint harness that moves the boxes and routes the edges reads `placementDependent`.
|
|
124
182
|
* It compares a measure with the value `true` only between runs on the same input.
|
|
125
183
|
*/
|
|
126
|
-
export const MEASURES: readonly MeasureInfo[] = Object.freeze(
|
|
184
|
+
export const MEASURES: readonly MeasureInfo[] = Object.freeze(
|
|
185
|
+
ROWS.map(([key, id, name, unit, better, placementDependent]) => {
|
|
186
|
+
const rule = MIN_EFFECT_BY_UNIT.get(unit);
|
|
187
|
+
if (!rule) throw new Error(`MEASURES: the unit "${unit}" of ${key} has no rule in MIN_EFFECTS`);
|
|
188
|
+
const info: MeasureInfo = { key, id, name, unit, better, placementDependent };
|
|
189
|
+
return Object.freeze(rule.minEffect === undefined ? info : { ...info, minEffect: rule.minEffect });
|
|
190
|
+
}),
|
|
191
|
+
);
|
|
192
|
+
|
|
193
|
+
/** This function gives the spec of a compare of a measure: its better side and its minimum effect. */
|
|
194
|
+
export function compareSpec(info: MeasureInfo): CompareSpec {
|
|
195
|
+
return info.minEffect === undefined ? { direction: info.better } : { direction: info.better, minEffect: info.minEffect };
|
|
196
|
+
}
|
|
127
197
|
|
|
128
198
|
const BY_KEY: ReadonlyMap<string, MeasureInfo> = new Map(MEASURES.map((m) => [m.key, m]));
|
|
129
199
|
const BY_NAME: ReadonlyMap<string, MeasureInfo> = new Map(MEASURES.map((m) => [m.name, m]));
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
// The portfolio combinator (PORTFOLIO-API): `portfolio`, `selectBest` and `pickMoves` come from `@mark1russell7/layout-contract` 0.2.6 (PORTFOLIO-FIT).
|
|
2
|
+
// The rules of the pick, the time limit and the trace are in `docs/PORTFOLIO.md` of the contract.
|
|
3
|
+
export {
|
|
4
|
+
pickMoves,
|
|
5
|
+
portfolio,
|
|
6
|
+
selectBest,
|
|
7
|
+
type Member,
|
|
8
|
+
type MemberStatus,
|
|
9
|
+
type MemberTrace,
|
|
10
|
+
type PickSpec,
|
|
11
|
+
type Portfolio,
|
|
12
|
+
type PortfolioKey,
|
|
13
|
+
type PortfolioLevel,
|
|
14
|
+
type PortfolioOptions,
|
|
15
|
+
type PortfolioPick,
|
|
16
|
+
type PortfolioTrace,
|
|
17
|
+
type SelectCandidate,
|
|
18
|
+
type SelectLevel,
|
|
19
|
+
type Selection,
|
|
20
|
+
} from "@mark1russell7/layout-contract";
|
|
@@ -19,6 +19,10 @@ import { measureQuality } from "../quality.ts";
|
|
|
19
19
|
import { sameData } from "../run.ts";
|
|
20
20
|
import { FRONT_OBJECTIVES } from "../suite.ts";
|
|
21
21
|
import { inputGraph, jsonSafe, type LayoutInput, type LayoutPrediction, type LayoutRecord, type PredictionOp } from "./forms.ts";
|
|
22
|
+
import { curvesHash, resultHash, type MoveCounter } from "./exercise.ts";
|
|
23
|
+
import { outputHash } from "./activation.ts";
|
|
24
|
+
import { RECORD_EPS } from "./hash.ts";
|
|
25
|
+
import { formatGap, gapBaselines, gapValues, optimumKey, summarizeGap, type ExactOptimum, type GapRule, type GapSummary } from "./gap.ts";
|
|
22
26
|
import { measureByName, measureName, namedObjectives } from "./measures.ts";
|
|
23
27
|
import { SCREEN_V1, screenInputs, type ScreenSet } from "./set.ts";
|
|
24
28
|
|
|
@@ -98,6 +102,16 @@ export interface ScreenOptions {
|
|
|
98
102
|
readonly commit?: string;
|
|
99
103
|
/** The predictions of the run. Each record names the first prediction with its strategy, its set and its input. */
|
|
100
104
|
readonly predictions?: readonly LayoutPrediction[];
|
|
105
|
+
/**
|
|
106
|
+
* The exact optima of the inputs, from the file of the optima of a set (MEAS2). The steps `router` and `highway` read them.
|
|
107
|
+
* Each run on an input with optima gets the largest B3 and the gap closed of each rule (B10). The summary then gets the gap table.
|
|
108
|
+
*/
|
|
109
|
+
readonly optima?: readonly ExactOptimum[];
|
|
110
|
+
/**
|
|
111
|
+
* This function gives the own move counts of a strategy on an input (ACTIVATION), for example `{ "move.powerEdge": 3 }`.
|
|
112
|
+
* Each record of a strategy with counts gets them as the trace measures of its step. The default is no counts.
|
|
113
|
+
*/
|
|
114
|
+
readonly moves?: MoveCounter;
|
|
101
115
|
/** This function gets one line of progress for each input. */
|
|
102
116
|
readonly log?: (line: string) => void;
|
|
103
117
|
}
|
|
@@ -154,6 +168,8 @@ export interface ScreenSummary {
|
|
|
154
168
|
/** The strategies with a break over the floor, without a run or with two different outputs of one input. They do not enter the front. */
|
|
155
169
|
readonly excluded: readonly string[];
|
|
156
170
|
readonly predictions: readonly PredictionCheck[];
|
|
171
|
+
/** The gap closed of each strategy and rule, when the screen has optima. */
|
|
172
|
+
readonly gap?: readonly GapSummary[];
|
|
157
173
|
}
|
|
158
174
|
|
|
159
175
|
/** The result of `screen`: one record for each strategy and input, and the summary. */
|
|
@@ -272,6 +288,27 @@ function rowRules(step: ScreenStep, graph: Graph, row: CompareRow, again: Compar
|
|
|
272
288
|
return { hard: { ...countRules(rules, []), "same-twice": deterministic ? 0 : 1 }, deterministic };
|
|
273
289
|
}
|
|
274
290
|
|
|
291
|
+
/**
|
|
292
|
+
* This function gives the output hash of a row of `compareStep` (ACTIVATION), or `undefined` for a row without output.
|
|
293
|
+
* A router hashes its result with `resultHash`. The step `group` hashes its tree and its result, and the step `curve` hashes its curves.
|
|
294
|
+
*/
|
|
295
|
+
function rowHash(row: CompareRow): string | undefined {
|
|
296
|
+
try {
|
|
297
|
+
return rowHashOf(row);
|
|
298
|
+
} catch (e) {
|
|
299
|
+
// An output with NaN or an infinite number has no hash. The record then has no `outputHash`, and the script `screen` names it.
|
|
300
|
+
if (!(e instanceof RangeError)) throw e;
|
|
301
|
+
return undefined;
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/** This function gives the output hash of a row. It throws a `RangeError` for an output with a number that is not finite. */
|
|
306
|
+
function rowHashOf(row: CompareRow): string | undefined {
|
|
307
|
+
if (row.step === "curve") return row.curves && row.edges ? curvesHash(row.edges, row.curves) : undefined;
|
|
308
|
+
if (row.step === "group") return row.result || row.tree ? outputHash({ tree: row.tree, result: row.result }, { grid: RECORD_EPS }) : undefined;
|
|
309
|
+
return row.result ? resultHash(row.result) : undefined;
|
|
310
|
+
}
|
|
311
|
+
|
|
275
312
|
/** This function adds the router `<name>+<w>` for each router of the registry and each wrapper `w`. It gives the new registry and the new names. */
|
|
276
313
|
function wrapRouters(registry: Registry, wraps: readonly ScreenWrap[]): { registry: Registry; added: string[] } {
|
|
277
314
|
const routers = listStrategies(registry, "router");
|
|
@@ -339,6 +376,8 @@ export function screen(step: ScreenStep, strategies: readonly (Strategy<ScreenSt
|
|
|
339
376
|
const comparisons: StepComparison[] = [];
|
|
340
377
|
const kept = new Map<string, { graph: Graph; results: Map<string, Result> }>();
|
|
341
378
|
const variants: { record: (typeof records)[number]; base: string; result: Result }[] = [];
|
|
379
|
+
const optima = givesRoutes(step) && options.optima ? new Map(options.optima.map((o) => [optimumKey(o.input, o.rule, o.cap), o])) : undefined;
|
|
380
|
+
const withOptima = new Set(options.optima?.map((o) => o.input));
|
|
342
381
|
|
|
343
382
|
for (const input of inputs) {
|
|
344
383
|
const graph = inputGraph(input);
|
|
@@ -352,6 +391,9 @@ export function screen(step: ScreenStep, strategies: readonly (Strategy<ScreenSt
|
|
|
352
391
|
let cornersRoutes: readonly Route[] | undefined;
|
|
353
392
|
const cornersBase = (): readonly Route[] =>
|
|
354
393
|
(cornersRoutes ??= (cmp.rows.find((r) => r.strategy === "corners" && r.result)?.result ?? asRouter(getStrategy(STRATEGIES, "router", "corners")).route(graph)).routes);
|
|
394
|
+
// The B1 of the baseline of each rule of the gap closed, for an input with optima.
|
|
395
|
+
let baselines: Record<GapRule, number> | undefined;
|
|
396
|
+
const gapBase = (): Record<GapRule, number> => (baselines ??= gapBaselines(graph));
|
|
355
397
|
cmp.rows.forEach((row, i) => {
|
|
356
398
|
const strategy = byName.get(row.strategy)!;
|
|
357
399
|
const head = {
|
|
@@ -378,6 +420,7 @@ export function screen(step: ScreenStep, strategies: readonly (Strategy<ScreenSt
|
|
|
378
420
|
const values = rowValues(step, graph, row, cornersBase);
|
|
379
421
|
// Z2 is the count of the hard rule `link-ends`, as a measure. The steps `router`, `highway` and `group` check the rule.
|
|
380
422
|
if (hard["link-ends"] !== undefined) values["Z2.linkEnds"] = hard["link-ends"];
|
|
423
|
+
if (optima && withOptima.has(input.id) && row.result) Object.assign(values, gapValues(input, graph, row.result, cornersBase(), gapBase(), optima));
|
|
381
424
|
if (shuffles > 0 && row.result) {
|
|
382
425
|
const router = asRouter(strategy as Strategy<"router">, own[row.strategy]);
|
|
383
426
|
const f = orderFragility(graph, (g) => router.route(g), { orders: shuffles, seed, base: row.result });
|
|
@@ -388,7 +431,10 @@ export function screen(step: ScreenStep, strategies: readonly (Strategy<ScreenSt
|
|
|
388
431
|
const z = highwayEdits(graph, plan, own[row.strategy], edits, seed);
|
|
389
432
|
Object.assign(values, { "Z10.edits.mean": z.mean, "Z10.edits.moved": z.moved, "Z10.churn": z.churn, "Z10.ids": z.ids });
|
|
390
433
|
}
|
|
391
|
-
const
|
|
434
|
+
const output = rowHash(row);
|
|
435
|
+
const counts = row.result ? options.moves?.(row.strategy, graph) : undefined;
|
|
436
|
+
const facts = { ...(output === undefined ? {} : { outputHash: output }), ...(counts === undefined ? {} : { trace: [{ id: head.step, measures: { ...counts } }] }) };
|
|
437
|
+
const record = { ...head, measures: named(values), hard, tolerance: { through: TOLERANCE }, deterministic, ms: row.ms, ...tail, ...facts };
|
|
392
438
|
records.push(record);
|
|
393
439
|
if (givesRoutes(step) && input.base !== undefined && row.result) variants.push({ record, base: input.base, result: row.result });
|
|
394
440
|
});
|
|
@@ -403,7 +449,8 @@ export function screen(step: ScreenStep, strategies: readonly (Strategy<ScreenSt
|
|
|
403
449
|
Object.assign(v.record.measures, named({ "Z10.base.mean": s.mean, "Z10.base.max": s.max, "Z10.base.moved": s.moved }));
|
|
404
450
|
}
|
|
405
451
|
|
|
406
|
-
|
|
452
|
+
const summary = summarizeScreen(records, comparisons, { set: set.id, step, candidates, ...(options.predictions ? { predictions: options.predictions } : {}) });
|
|
453
|
+
return { records, summary: optima ? { ...summary, gap: summarizeGap(records) } : summary };
|
|
407
454
|
}
|
|
408
455
|
|
|
409
456
|
/** This function gives the summary of the records of a screen. */
|
|
@@ -492,5 +539,6 @@ export function formatScreen(summary: ScreenSummary): string {
|
|
|
492
539
|
for (const p of summary.predictions)
|
|
493
540
|
lines.push(`| ${p.id} | ${p.strategy} | ${p.input ?? "median"} | ${p.measure} | ${p.op} ${fmt(p.expected)} | ${fmt(p.measured)} | ${p.met === undefined ? "-" : p.met ? "yes" : "no"} | ${p.reason} |`);
|
|
494
541
|
}
|
|
542
|
+
if (summary.gap) lines.push(formatGap(summary.gap).trimEnd());
|
|
495
543
|
return lines.join("\n") + "\n";
|
|
496
544
|
}
|
package/src/bench/screen/set.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { hubChainBoard, manyGroupsBoard, packedGridBoard } from "../../cases.ts"
|
|
|
2
2
|
import { FIXTURES, makeFixture, type FixtureSpec } from "../../fixtures.ts";
|
|
3
3
|
import type { Graph } from "../../model.ts";
|
|
4
4
|
import { familySeed } from "../suite.ts";
|
|
5
|
+
import { EXACT_V1, EXACT_V1_BOUNDS } from "./exact-set.ts";
|
|
5
6
|
import { graphInput, type LayoutCell, type LayoutGenerator, type LayoutInput } from "./forms.ts";
|
|
6
7
|
|
|
7
8
|
/** A family of boards of a screen set. The member `k` of the family has the seed `k`. */
|
|
@@ -85,8 +86,8 @@ export const SCREEN_V1: ScreenSet = {
|
|
|
85
86
|
],
|
|
86
87
|
};
|
|
87
88
|
|
|
88
|
-
/** The screen sets by id. */
|
|
89
|
-
export const SCREEN_SETS: Readonly<Record<string, ScreenSet>> = { [SCREEN_V1.id]: SCREEN_V1 };
|
|
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 };
|
|
90
91
|
|
|
91
92
|
/** This function gives the id of the member `seed` of a family of a set: `<set>/<family>/s<seed>`. */
|
|
92
93
|
export function inputId(set: string, family: string, seed: number): string {
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import { boardUnit } from "../../bench/measures/routes.ts";
|
|
2
|
+
import { dist, type Point } from "../../geom.ts";
|
|
3
|
+
import { WayFinder, type WayOptions } from "../../ways.ts";
|
|
4
|
+
import type { DensitySource } from "../kdeeb/index.ts";
|
|
5
|
+
import type { HighwayInput, HighwayPlan } from "../model.ts";
|
|
6
|
+
import { bigGroups, firstLines, nearestOnLine, shapeSkeleton, skeletonGroups, type ShapeSkeleton, type SkeletonGroups } from "../sbeb/index.ts";
|
|
7
|
+
import { snapPlan, type SnapLine, type SnapMember } from "../snap.ts";
|
|
8
|
+
|
|
9
|
+
/** The rule of the half width of an area: the spread of the edges of the group, or the number of its lanes. */
|
|
10
|
+
export type AreaWidth = "spread" | "count";
|
|
11
|
+
|
|
12
|
+
/** The values of `AreaWidth`. */
|
|
13
|
+
export const AREA_WIDTHS: readonly AreaWidth[] = Object.freeze(["spread", "count"]);
|
|
14
|
+
|
|
15
|
+
/** The options of the highway strategy `areas` (HWY-010 of the notes). */
|
|
16
|
+
export interface AreaOptions extends Pick<WayOptions, "margin" | "k"> {
|
|
17
|
+
/** The clusterer of the groups. The default is `quickbundles`. */
|
|
18
|
+
readonly group: SkeletonGroups;
|
|
19
|
+
/** The lines of the edges that make the shape: their straight lines, or their first routes. The default is `lines`. */
|
|
20
|
+
readonly source: DensitySource;
|
|
21
|
+
/** The radius of the shape of a group, as a multiple of `boardUnit`. The default is 1.5. */
|
|
22
|
+
readonly radius: number;
|
|
23
|
+
/** The rule of the half width of the area. The default is `spread`. */
|
|
24
|
+
readonly width: AreaWidth;
|
|
25
|
+
/** The width of one lane for the rule `count`, in units of the board. The default is 4. */
|
|
26
|
+
readonly laneWidth: number;
|
|
27
|
+
/** The side of a cell of the image of the shape, in units of the board. The default is 4. */
|
|
28
|
+
readonly cell: number;
|
|
29
|
+
/** A highway has this number of edges or more. The default is 2. */
|
|
30
|
+
readonly minSize: number;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** The default options of the strategy `areas`. */
|
|
34
|
+
export const AREA_DEFAULTS: Readonly<Required<Omit<AreaOptions, "margin" | "k">>> = Object.freeze({
|
|
35
|
+
group: "quickbundles",
|
|
36
|
+
source: "lines",
|
|
37
|
+
radius: 1.5,
|
|
38
|
+
width: "spread",
|
|
39
|
+
laneWidth: 4,
|
|
40
|
+
cell: 4,
|
|
41
|
+
minSize: 2,
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* This function gives the longest path of the largest part of a skeleton, as the middles of its cells. The neighbours of a cell are its 8 neighbours.
|
|
46
|
+
* The function finds the cell farthest from a first cell, then the cell farthest from that cell. A skeleton without cells gives an empty list.
|
|
47
|
+
*/
|
|
48
|
+
export function skeletonPath(s: ShapeSkeleton): Point[] {
|
|
49
|
+
const { grid: g, skeleton } = s;
|
|
50
|
+
const n = g.w * g.h;
|
|
51
|
+
const part = new Int32Array(n).fill(-1);
|
|
52
|
+
const sizes: number[] = [];
|
|
53
|
+
const neighbours = (i: number): number[] => {
|
|
54
|
+
const x = i % g.w;
|
|
55
|
+
const y = (i - x) / g.w;
|
|
56
|
+
const out: number[] = [];
|
|
57
|
+
for (let dy = -1; dy <= 1; dy++)
|
|
58
|
+
for (let dx = -1; dx <= 1; dx++) {
|
|
59
|
+
if (dx === 0 && dy === 0) continue;
|
|
60
|
+
const u = x + dx;
|
|
61
|
+
const v = y + dy;
|
|
62
|
+
if (u >= 0 && v >= 0 && u < g.w && v < g.h && skeleton[v * g.w + u]) out.push(v * g.w + u);
|
|
63
|
+
}
|
|
64
|
+
return out;
|
|
65
|
+
};
|
|
66
|
+
for (let i = 0; i < n; i++) {
|
|
67
|
+
if (!skeleton[i] || part[i]! >= 0) continue;
|
|
68
|
+
const id = sizes.length;
|
|
69
|
+
let size = 0;
|
|
70
|
+
const stack = [i];
|
|
71
|
+
part[i] = id;
|
|
72
|
+
while (stack.length > 0) {
|
|
73
|
+
const c = stack.pop()!;
|
|
74
|
+
size++;
|
|
75
|
+
for (const j of neighbours(c))
|
|
76
|
+
if (part[j]! < 0) {
|
|
77
|
+
part[j] = id;
|
|
78
|
+
stack.push(j);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
sizes.push(size);
|
|
82
|
+
}
|
|
83
|
+
if (sizes.length === 0) return [];
|
|
84
|
+
const big = sizes.indexOf(Math.max(...sizes));
|
|
85
|
+
const bfs = (from: number): { far: number; prev: Int32Array } => {
|
|
86
|
+
const prev = new Int32Array(n).fill(-2);
|
|
87
|
+
prev[from] = -1;
|
|
88
|
+
const queue = [from];
|
|
89
|
+
let far = from;
|
|
90
|
+
for (let q = 0; q < queue.length; q++) {
|
|
91
|
+
const c = queue[q]!;
|
|
92
|
+
far = c;
|
|
93
|
+
for (const j of neighbours(c))
|
|
94
|
+
if (prev[j] === -2) {
|
|
95
|
+
prev[j] = c;
|
|
96
|
+
queue.push(j);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
return { far, prev };
|
|
100
|
+
};
|
|
101
|
+
let first = -1;
|
|
102
|
+
for (let i = 0; i < n && first < 0; i++) if (part[i] === big) first = i;
|
|
103
|
+
const a = bfs(first).far;
|
|
104
|
+
const { far: b, prev } = bfs(a);
|
|
105
|
+
const path: Point[] = [];
|
|
106
|
+
for (let c = b; c >= 0; c = prev[c]!) {
|
|
107
|
+
const x = c % g.w;
|
|
108
|
+
const y = (c - x) / g.w;
|
|
109
|
+
path.push({ x: g.x0 + (x + 0.5) * g.cell, y: g.y0 + (y + 0.5) * g.cell });
|
|
110
|
+
}
|
|
111
|
+
return path;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** This function gives the value at the share `q` of the sorted list, with a linear mix between two values. An empty list gives 0. */
|
|
115
|
+
function quantileOf(values: readonly number[], q: number): number {
|
|
116
|
+
if (values.length === 0) return 0;
|
|
117
|
+
const s = [...values].sort((a, b) => a - b);
|
|
118
|
+
const at = Math.max(0, Math.min(s.length - 1, q * (s.length - 1)));
|
|
119
|
+
const i = Math.floor(at);
|
|
120
|
+
const u = at - i;
|
|
121
|
+
return i + 1 < s.length ? s[i]! * (1 - u) + s[i + 1]! * u : s[i]!;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* This function gives the highways of the image-based edge bundles (HWY-010, Telea and Ersoy 2010). Each group of edges is one area with one spine.
|
|
126
|
+
* The spine goes through the corner graph between the two ends of the longest path of the skeleton of the shape of the group.
|
|
127
|
+
* The area is the skeleton path with a half width. With `spread`, the half width is the 90th percentile of the distances of the points to the path.
|
|
128
|
+
* With `count`, it is the number of lanes times half of `laneWidth`. The half width is at least the margin.
|
|
129
|
+
* An edge joins the spine where its line first goes into the area. It leaves the spine where its line last comes out of the area.
|
|
130
|
+
*/
|
|
131
|
+
export function areaPlan(input: HighwayInput, o: Readonly<AreaOptions>): HighwayPlan {
|
|
132
|
+
const way: WayOptions = { ...(o.margin === undefined ? {} : { margin: o.margin }), ...(o.k === undefined ? {} : { k: o.k }) };
|
|
133
|
+
const finder = new WayFinder(input.graph.nodes, way);
|
|
134
|
+
const radius = Math.max(1e-3, o.radius * boardUnit(input.graph));
|
|
135
|
+
const lines = firstLines(input, finder, o.source, Math.max(4, 2 * o.cell));
|
|
136
|
+
const snapLines: SnapLine[] = [];
|
|
137
|
+
for (const g of bigGroups(skeletonGroups(input, o.group), o.minSize)) {
|
|
138
|
+
const own = g.edges.filter((m) => lines.has(m.edge));
|
|
139
|
+
if (own.length < Math.max(2, o.minSize)) continue;
|
|
140
|
+
const path = skeletonPath(shapeSkeleton(own.map((m) => lines.get(m.edge)!), finder, radius, o.cell));
|
|
141
|
+
if (path.length < 2 || dist(path[0]!, path[path.length - 1]!) <= finder.margin) continue;
|
|
142
|
+
const spread = own.flatMap((m) => lines.get(m.edge)!.map((p) => dist(p, nearestOnLine(path, p))));
|
|
143
|
+
const half = Math.max(finder.margin, o.width === "count" ? (own.length * o.laneWidth) / 2 : quantileOf(spread, 0.9));
|
|
144
|
+
const members: SnapMember[] = [];
|
|
145
|
+
for (const m of own) {
|
|
146
|
+
const l = lines.get(m.edge)!;
|
|
147
|
+
let first = -1;
|
|
148
|
+
let last = -1;
|
|
149
|
+
l.forEach((p, i) => {
|
|
150
|
+
if (dist(p, nearestOnLine(path, p)) > half) return;
|
|
151
|
+
if (first < 0) first = i;
|
|
152
|
+
last = i;
|
|
153
|
+
});
|
|
154
|
+
if (first < 0 || last <= first) continue;
|
|
155
|
+
members.push({ edge: m.edge, from: l[first]!, to: l[last]!, at: first / Math.max(1, l.length - 1) });
|
|
156
|
+
}
|
|
157
|
+
snapLines.push({ key: g.key, a: path[0]!, b: path[path.length - 1]!, members });
|
|
158
|
+
}
|
|
159
|
+
return snapPlan(input, snapLines, { ...way, minSize: o.minSize });
|
|
160
|
+
}
|