@mark1russell7/highways 0.4.2 → 0.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +59 -2
- package/build/index.d.ts +2333 -193
- package/build/index.d.ts.map +1 -1
- package/build/index.js +13042 -2334
- package/build/index.js.map +1 -1
- package/package.json +12 -1
- package/src/bench/exact/index.ts +258 -0
- 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/measures/index.ts +1 -0
- package/src/bench/measures/lanes.ts +304 -0
- package/src/bench/measures/levels.ts +86 -2
- 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 +25 -5
- 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 +89 -1
- package/src/bench/screen/portfolio-pick.ts +20 -0
- package/src/bench/screen/screen.ts +78 -2
- package/src/bench/screen/set.ts +3 -2
- package/src/channels.ts +56 -0
- 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/diffusion/index.ts +103 -0
- package/src/highways/divided/index.ts +111 -0
- package/src/highways/edge-paths/index.ts +155 -0
- package/src/highways/fdeb/index.ts +193 -0
- package/src/highways/fixtures.test-util.ts +71 -0
- package/src/highways/flow-clusters/index.ts +143 -0
- package/src/highways/frechet/index.ts +232 -0
- package/src/highways/growth/index.ts +148 -0
- package/src/highways/hdbscan/index.ts +250 -0
- package/src/highways/hierarchy/index.ts +140 -0
- package/src/highways/hough/index.ts +182 -0
- package/src/highways/hub-arcs/index.ts +181 -0
- package/src/highways/index.ts +79 -0
- package/src/highways/kdeeb/index.ts +355 -0
- package/src/highways/lanes/index.ts +189 -0
- package/src/highways/link-communities/index.ts +189 -0
- package/src/highways/mesh/index.ts +209 -0
- package/src/highways/mingle/index.ts +171 -0
- package/src/highways/mlseb/index.ts +204 -0
- package/src/highways/nested/index.ts +143 -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/ridges/index.ts +328 -0
- package/src/highways/sbeb/index.ts +303 -0
- package/src/highways/scale-space/index.ts +135 -0
- package/src/highways/snap.ts +110 -0
- package/src/highways/soft-keys/index.ts +114 -0
- package/src/highways/spectral/index.ts +391 -0
- package/src/highways/spiral/index.ts +205 -0
- package/src/highways/territories/index.ts +150 -0
- package/src/highways/trace.ts +153 -0
- package/src/highways/traclus/index.ts +296 -0
- package/src/index.ts +2 -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/routers/shared.ts +56 -3
- package/src/strategies/builtins.ts +376 -1
- package/src/strategies/schemas.ts +460 -1
- package/src/visibility.ts +5 -0
|
@@ -0,0 +1,316 @@
|
|
|
1
|
+
import { polylineLength } from "../../geom.ts";
|
|
2
|
+
import { follow } from "../../highways/follow.ts";
|
|
3
|
+
import type { HighwayInput, HighwayPlan } from "../../highways/model.ts";
|
|
4
|
+
import { sharedWaysPlan } from "../../highways/shared-ways/index.ts";
|
|
5
|
+
import type { Graph, Result, Route } from "../../model.ts";
|
|
6
|
+
import { byCollection } from "../../routers/trunks.ts";
|
|
7
|
+
import { STRATEGIES } from "../../strategies/builtins.ts";
|
|
8
|
+
import { asRouter, defineStrategy, getStrategy, type Strategy } from "../../strategies/model.ts";
|
|
9
|
+
import { inkSaved } from "../measures/bundles.ts";
|
|
10
|
+
import { quantile } from "../measures/stats.ts";
|
|
11
|
+
import { exactStretch } from "../exact/index.ts";
|
|
12
|
+
import { mulberry32 } from "./hash.ts";
|
|
13
|
+
|
|
14
|
+
/** The rule of the exact model: `collection` lets only the edges of one collection share a link, and `all` lets each edge share a link. */
|
|
15
|
+
export type GapRule = "collection" | "all";
|
|
16
|
+
|
|
17
|
+
/** The rules of the gap closed, in the order of the records. */
|
|
18
|
+
export const GAP_RULES: readonly GapRule[] = ["collection", "all"];
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* A view of the gap closed: one rule, or `chosen`. The view `chosen` takes a rule for each run.
|
|
22
|
+
* It takes `all` when a shared segment of the run holds edges of two collections. Else it takes `collection`.
|
|
23
|
+
*/
|
|
24
|
+
export type GapView = GapRule | "chosen";
|
|
25
|
+
|
|
26
|
+
/** The views of the gap closed, in the order of the tables. */
|
|
27
|
+
export const GAP_VIEWS: readonly GapView[] = ["chosen", "collection", "all"];
|
|
28
|
+
|
|
29
|
+
/** The stretch caps of the exact optima, from small to large. */
|
|
30
|
+
export const GAP_CAPS: readonly number[] = [1.1, 1.25, 1.5];
|
|
31
|
+
|
|
32
|
+
/** The baseline of each rule: the router `shared` for `collection`, and the plan `shared-ways-all` after `follow` for `all`. */
|
|
33
|
+
export const GAP_BASELINES: Readonly<Record<GapRule, string>> = { collection: "shared", all: "shared-ways-all" };
|
|
34
|
+
|
|
35
|
+
/** The parameters of the paired bootstrap of the gap closed, from `docs/PROMOTION.md`. */
|
|
36
|
+
export const GAP_BOOTSTRAP: { readonly runs: number; readonly level: number; readonly seed: number } = { runs: 10_000, level: 0.95, seed: 1 };
|
|
37
|
+
|
|
38
|
+
/** The optimum of the exact model on one input, for one rule and one stretch cap. The file of the optima of a set holds one for each. */
|
|
39
|
+
export interface ExactOptimum {
|
|
40
|
+
/** The id of the input. */
|
|
41
|
+
readonly input: string;
|
|
42
|
+
/** The hash of the input file. The screen uses the optimum only when the hash matches. */
|
|
43
|
+
readonly hash: string;
|
|
44
|
+
/** The class of the input: its family in the set. */
|
|
45
|
+
readonly class: string;
|
|
46
|
+
readonly nodes: number;
|
|
47
|
+
readonly edges: number;
|
|
48
|
+
readonly rule: GapRule;
|
|
49
|
+
readonly cap: number;
|
|
50
|
+
/** The status of HiGHS: `optimal`, `time-limit` or another status. */
|
|
51
|
+
readonly status: string;
|
|
52
|
+
/** This value is `true` when HiGHS proved the optimum within the time limit. */
|
|
53
|
+
readonly proven: boolean;
|
|
54
|
+
/** The objective of the best solution, the dual bound and the relative MIP gap. Each one is `null` when HiGHS gives none. */
|
|
55
|
+
readonly objective: number | null;
|
|
56
|
+
readonly bound: number | null;
|
|
57
|
+
readonly mipGap: number | null;
|
|
58
|
+
/** The time of the solve, in seconds. */
|
|
59
|
+
readonly seconds: number;
|
|
60
|
+
/** The number of binary variables of the model. */
|
|
61
|
+
readonly binaries: number;
|
|
62
|
+
/** The ink saved (B1) of the routes of the best plan after `follow`. The gap closed uses this value. */
|
|
63
|
+
readonly b1: number | null;
|
|
64
|
+
/** The ink saved of the model itself, without `follow`. */
|
|
65
|
+
readonly modelB1: number | null;
|
|
66
|
+
/** The largest bundle stretch (B3) of the routes of the best plan, against the routes of `corners`. */
|
|
67
|
+
readonly b3max: number | null;
|
|
68
|
+
/** The number of highways of the best plan. */
|
|
69
|
+
readonly highways: number;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** The file of the optima of a set (`exact-optima/1`). Later screens read it and do not solve again. */
|
|
73
|
+
export interface ExactOptimaFile {
|
|
74
|
+
readonly schema: "exact-optima/1";
|
|
75
|
+
readonly set: string;
|
|
76
|
+
/** The time limit of each solve, in seconds. */
|
|
77
|
+
readonly seconds: number;
|
|
78
|
+
/** The commit of the code and the CI run of the solves. */
|
|
79
|
+
readonly commit?: string;
|
|
80
|
+
readonly run?: string;
|
|
81
|
+
readonly optima: readonly ExactOptimum[];
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** This function gives the file of the optima of a set, with the rows in the order of the input, the rule and the cap. */
|
|
85
|
+
export function optimaFile(set: string, rows: readonly ExactOptimum[], at: { readonly seconds: number; readonly commit?: string; readonly run?: string }): ExactOptimaFile {
|
|
86
|
+
const order = (r: ExactOptimum): string => `${r.input}\u0000${GAP_RULES.indexOf(r.rule)}\u0000${r.cap.toFixed(3)}`;
|
|
87
|
+
const optima = [...rows].sort((a, b) => (order(a) < order(b) ? -1 : order(a) > order(b) ? 1 : 0));
|
|
88
|
+
return { schema: "exact-optima/1", set, seconds: at.seconds, ...(at.commit ? { commit: at.commit } : {}), ...(at.run ? { run: at.run } : {}), optima };
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** This function gives the key of an optimum in a map: the input, the rule and the cap. */
|
|
92
|
+
export function optimumKey(input: string, rule: GapRule, cap: number): string {
|
|
93
|
+
return `${input}|${rule}|${cap.toFixed(3)}`;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** This function gives the smallest cap that is at least the stretch, or `undefined` when the stretch is above the largest cap. */
|
|
97
|
+
export function gapCap(stretch: number, caps: readonly number[] = GAP_CAPS): number | undefined {
|
|
98
|
+
return caps.find((c) => stretch <= c + 1e-9);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* This function gives the gap closed: `(b1 - base) / (optimum - base)`.
|
|
103
|
+
* It gives `undefined` when the denominator is 0 or less, because then the baseline is at the optimum or above it.
|
|
104
|
+
*/
|
|
105
|
+
export function gapClosed(b1: number, base: number, optimum: number): number | undefined {
|
|
106
|
+
const d = optimum - base;
|
|
107
|
+
return d > 1e-9 ? (b1 - base) / d : undefined;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* This function gives the largest bundle stretch of routes against base routes: the max of `L(r_e) / L(r*_e)` over the edges, as B3 counts them.
|
|
112
|
+
* An edge without a base route, or with a base route of 1 unit or less, does not count. Without such an edge, the value is 1.
|
|
113
|
+
*/
|
|
114
|
+
export function maxStretch(routes: readonly Route[], base: readonly Route[]): number {
|
|
115
|
+
const baseLength = new Map<string, number>();
|
|
116
|
+
for (const r of base) baseLength.set(r.edge, polylineLength(r.points));
|
|
117
|
+
let max = 1;
|
|
118
|
+
for (const r of routes) {
|
|
119
|
+
const b = baseLength.get(r.edge);
|
|
120
|
+
if (b === undefined || b <= 1 || r.points.length < 2) continue;
|
|
121
|
+
max = Math.max(max, polylineLength(r.points) / b);
|
|
122
|
+
}
|
|
123
|
+
return max;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/** The highway strategy `shared-ways-all`: the plan of the router `shared` without a rule. It is the baseline of the rule `all`. */
|
|
127
|
+
export const sharedWaysAll: Strategy<"highway", object> = defineStrategy({
|
|
128
|
+
step: "highway",
|
|
129
|
+
name: "shared-ways-all",
|
|
130
|
+
describe: "The plan of the router `shared` without a rule.",
|
|
131
|
+
defaults: {},
|
|
132
|
+
schema: {},
|
|
133
|
+
maxNodes: 10_000,
|
|
134
|
+
run: (input: HighwayInput): HighwayPlan => sharedWaysPlan(input, {}),
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
/** This function gives the B1 of the baseline of each rule on a graph. */
|
|
138
|
+
export function gapBaselines(graph: Graph): Record<GapRule, number> {
|
|
139
|
+
const shared = asRouter(getStrategy(STRATEGIES, "router", "shared")).route(graph).routes;
|
|
140
|
+
const all = follow(graph, sharedWaysAll.run({ graph }, {})).routes;
|
|
141
|
+
return { collection: inkSaved(shared), all: inkSaved(all) };
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/** A median with its paired bootstrap interval. */
|
|
145
|
+
export interface BootstrapMedian {
|
|
146
|
+
readonly median: number;
|
|
147
|
+
readonly lo: number;
|
|
148
|
+
readonly hi: number;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* This function gives the median of the values and its bootstrap interval: `runs` samples of the values with replacement, from the seed `seed`.
|
|
153
|
+
* The interval is the central `level` share of the medians of the samples. Each value is the paired value of one input, so the bootstrap is paired.
|
|
154
|
+
*/
|
|
155
|
+
export function bootstrapMedian(values: readonly number[], o: { readonly runs?: number; readonly level?: number; readonly seed?: number } = {}): BootstrapMedian {
|
|
156
|
+
if (values.length === 0) return { median: Number.NaN, lo: Number.NaN, hi: Number.NaN };
|
|
157
|
+
const runs = o.runs ?? GAP_BOOTSTRAP.runs;
|
|
158
|
+
const level = o.level ?? GAP_BOOTSTRAP.level;
|
|
159
|
+
const r = mulberry32(o.seed ?? GAP_BOOTSTRAP.seed);
|
|
160
|
+
const medians: number[] = [];
|
|
161
|
+
const sample = new Array<number>(values.length);
|
|
162
|
+
for (let i = 0; i < runs; i++) {
|
|
163
|
+
for (let j = 0; j < values.length; j++) sample[j] = values[Math.floor(r() * values.length)]!;
|
|
164
|
+
medians.push(quantile(sample, 0.5));
|
|
165
|
+
}
|
|
166
|
+
const tail = (1 - level) / 2;
|
|
167
|
+
return { median: quantile(values, 0.5), lo: quantile(medians, tail), hi: quantile(medians, 1 - tail) };
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/** The gap closed of one strategy and one view over the inputs of a screen. */
|
|
171
|
+
export interface GapSummary {
|
|
172
|
+
readonly strategy: string;
|
|
173
|
+
readonly rule: GapView;
|
|
174
|
+
/** The number of runs, and the number of runs with a value. A run has no value above the largest cap, or without a proven optimum. */
|
|
175
|
+
readonly runs: number;
|
|
176
|
+
readonly values: number;
|
|
177
|
+
/** The median over the inputs with a value, and its paired bootstrap interval. */
|
|
178
|
+
readonly median: number;
|
|
179
|
+
readonly lo: number;
|
|
180
|
+
readonly hi: number;
|
|
181
|
+
/** The median of each class, by the family of the input. */
|
|
182
|
+
readonly byClass: Readonly<Record<string, number>>;
|
|
183
|
+
/** The number of values at each cap, by the cap. */
|
|
184
|
+
readonly caps: Readonly<Record<string, number>>;
|
|
185
|
+
/** The number of runs with some ink saved (B1 above 0): the runs where the strategy builds a shared segment. */
|
|
186
|
+
readonly builds: number;
|
|
187
|
+
/** The number of runs where a shared segment holds edges of two collections. */
|
|
188
|
+
readonly mixed: number;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/** The names of the measures of the gap closed in a record, for a view. */
|
|
192
|
+
export function gapNames(view: GapView): { readonly gap: string; readonly cap: string } {
|
|
193
|
+
return view === "chosen" ? { gap: "route.bundle.gap-closed", cap: "route.bundle.gap-closed.cap" } : { gap: `route.bundle.gap-closed.${view}`, cap: `route.bundle.gap-closed.cap.${view}` };
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/** This function gives the family of an input id `<set>/<family>/s<seed>`. */
|
|
197
|
+
const familyOf = (input: string): string => input.split("/")[1] ?? input;
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* This function gives the gap closed of each strategy and view over records.
|
|
201
|
+
* It sorts them by the view and then by the median, from high to low.
|
|
202
|
+
* A strategy with a value on fewer than half of its runs comes after the others, because its median shows only a few inputs.
|
|
203
|
+
*/
|
|
204
|
+
export function summarizeGap(
|
|
205
|
+
records: readonly { readonly strategy: string; readonly input: string; readonly skipped?: string; readonly measures: Readonly<Record<string, number>> }[],
|
|
206
|
+
): GapSummary[] {
|
|
207
|
+
const out: GapSummary[] = [];
|
|
208
|
+
const names = [...new Set(records.map((r) => r.strategy))];
|
|
209
|
+
for (const rule of GAP_VIEWS) {
|
|
210
|
+
const key = gapNames(rule);
|
|
211
|
+
const rows: GapSummary[] = [];
|
|
212
|
+
for (const strategy of names) {
|
|
213
|
+
const ran = records.filter((r) => r.strategy === strategy && r.skipped === undefined);
|
|
214
|
+
const withValue = ran.filter((r) => r.measures[key.gap] !== undefined);
|
|
215
|
+
const values = withValue.map((r) => r.measures[key.gap]!);
|
|
216
|
+
const boot = bootstrapMedian(values);
|
|
217
|
+
const classes = [...new Set(withValue.map((r) => familyOf(r.input)))].sort();
|
|
218
|
+
const byClass = Object.fromEntries(classes.map((c) => [c, quantile(withValue.filter((r) => familyOf(r.input) === c).map((r) => r.measures[key.gap]!), 0.5)]));
|
|
219
|
+
const caps: Record<string, number> = {};
|
|
220
|
+
for (const r of withValue) {
|
|
221
|
+
const c = String(r.measures[key.cap]);
|
|
222
|
+
caps[c] = (caps[c] ?? 0) + 1;
|
|
223
|
+
}
|
|
224
|
+
const builds = ran.filter((r) => (r.measures["route.bundle.ink-saved"] ?? 0) > 1e-9).length;
|
|
225
|
+
const mixed = ran.filter((r) => r.measures["route.bundle.gap-closed.mixed"] === 1).length;
|
|
226
|
+
rows.push({ strategy, rule, runs: ran.length, values: values.length, median: boot.median, lo: boot.lo, hi: boot.hi, byClass, caps, builds, mixed });
|
|
227
|
+
}
|
|
228
|
+
const few = (r: GapSummary): number => (2 * r.values < r.runs ? 1 : 0);
|
|
229
|
+
rows.sort((a, b) => few(a) - few(b) || (Number.isNaN(b.median) ? -Infinity : b.median) - (Number.isNaN(a.median) ? -Infinity : a.median) || (a.strategy < b.strategy ? -1 : 1));
|
|
230
|
+
out.push(...rows);
|
|
231
|
+
}
|
|
232
|
+
return out;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/** This function writes the gap closed as Markdown: one table for each view, sorted by the median. */
|
|
236
|
+
export function formatGap(rows: readonly GapSummary[]): string {
|
|
237
|
+
const f = (v: number): string => (Number.isNaN(v) ? "-" : v.toFixed(3));
|
|
238
|
+
const lines: string[] = [];
|
|
239
|
+
for (const rule of GAP_VIEWS) {
|
|
240
|
+
const mine = rows.filter((r) => r.rule === rule);
|
|
241
|
+
if (mine.length === 0) continue;
|
|
242
|
+
const classes = [...new Set(mine.flatMap((r) => Object.keys(r.byClass)))].sort();
|
|
243
|
+
lines.push(
|
|
244
|
+
"",
|
|
245
|
+
rule === "chosen"
|
|
246
|
+
? "### The gap closed with the rule of each run (`all` when a shared segment mixes collections, else `collection`)"
|
|
247
|
+
: `### The gap closed with the rule \`${rule}\` (baseline \`${GAP_BASELINES[rule]}\`)`,
|
|
248
|
+
"",
|
|
249
|
+
"The value of an input is `(B1 - B1 of the baseline) / (the optimum B1 - B1 of the baseline)`, at the smallest cap at or above the largest stretch of the routes.",
|
|
250
|
+
"The stretch is the stretch of the exact model: the length of a route without its plug ends, divided by the shortest way in the network of the model.",
|
|
251
|
+
"The interval is the 95% paired bootstrap interval of the median, with 10000 samples.",
|
|
252
|
+
"A strategy with a value on fewer than half of its runs comes after the others. Above the largest cap, a run has no value.",
|
|
253
|
+
"",
|
|
254
|
+
`| strategy | runs | builds | mixed | values | median | interval | ${classes.join(" | ")} | caps 1.1 / 1.25 / 1.5 |`,
|
|
255
|
+
`|---|---:|---:|---:|---:|---:|---|${classes.map(() => "---:").join("|")}|---|`,
|
|
256
|
+
...mine.map(
|
|
257
|
+
(r) =>
|
|
258
|
+
`| ${r.strategy} | ${r.runs} | ${r.builds} | ${r.mixed} | ${r.values} | ${f(r.median)} | ${f(r.lo)} to ${f(r.hi)} | ${classes.map((c) => f(r.byClass[c] ?? Number.NaN)).join(" | ")} | ${GAP_CAPS.map((c) => r.caps[String(c)] ?? 0).join(" / ")} |`,
|
|
259
|
+
),
|
|
260
|
+
);
|
|
261
|
+
}
|
|
262
|
+
return lines.join("\n") + "\n";
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/** This function gives the count of the shared segments of a result (the runs and the bundles) that hold edges of two collections or more. */
|
|
266
|
+
export function mixedSegments(graph: Graph, result: Result): number {
|
|
267
|
+
const key = new Map(graph.edges.map((e) => [e.id, byCollection(e) ?? `\u0000${e.id}`]));
|
|
268
|
+
const shared = [...(result.runs ?? []).map((r) => r.edges), ...(result.bundles ?? []).map((b) => b.edges)];
|
|
269
|
+
return shared.filter((edges) => new Set(edges.map((e) => key.get(e))).size > 1).length;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/** This function tells if a shared segment of a result (a run or a bundle) holds edges of two collections. */
|
|
273
|
+
export function mixesCollections(graph: Graph, result: Result): boolean {
|
|
274
|
+
return mixedSegments(graph, result) > 0;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* This function gives the gap measures of the result of one run on an input with optima, by the keys of `MEASURES`.
|
|
279
|
+
* `B3.max` is the largest stretch against the routes of `corners`. `gap.stretch` is the largest stretch as the exact model counts it (`exactStretch`).
|
|
280
|
+
* The cap is the smallest cap at or above `gap.stretch`, so both sides of the gap have the same stretch limit.
|
|
281
|
+
*
|
|
282
|
+
* `gap.<rule>` is the gap closed at the cap `gap.cap.<rule>`. `gap` is the gap closed of the rule of the run.
|
|
283
|
+
* That rule is `all` when `gap.mixed` is 1, else `collection`.
|
|
284
|
+
* A rule has no value above the largest cap or without a proven optimum at the cap.
|
|
285
|
+
* It also has no value when the hash of the optimum is not the hash of the input.
|
|
286
|
+
*/
|
|
287
|
+
export function gapValues(
|
|
288
|
+
input: { readonly id: string; readonly hash: string },
|
|
289
|
+
graph: Graph,
|
|
290
|
+
result: Result,
|
|
291
|
+
corners: readonly Route[],
|
|
292
|
+
baselines: Readonly<Record<GapRule, number>>,
|
|
293
|
+
optima: ReadonlyMap<string, ExactOptimum>,
|
|
294
|
+
): Record<string, number> {
|
|
295
|
+
const routes = result.routes;
|
|
296
|
+
const stretch = exactStretch(graph, routes);
|
|
297
|
+
const mixed = mixesCollections(graph, result);
|
|
298
|
+
const out: Record<string, number> = { "B3.max": maxStretch(routes, corners), "gap.stretch": stretch, "gap.mixed": mixed ? 1 : 0 };
|
|
299
|
+
const cap = gapCap(stretch);
|
|
300
|
+
if (cap === undefined) return out;
|
|
301
|
+
const b1 = inkSaved(routes);
|
|
302
|
+
for (const rule of GAP_RULES) {
|
|
303
|
+
const o = optima.get(optimumKey(input.id, rule, cap));
|
|
304
|
+
if (!o || !o.proven || o.b1 === null || o.hash !== input.hash) continue;
|
|
305
|
+
const g = gapClosed(b1, baselines[rule], o.b1);
|
|
306
|
+
if (g === undefined) continue;
|
|
307
|
+
out[`gap.${rule}`] = g;
|
|
308
|
+
out[`gap.cap.${rule}`] = cap;
|
|
309
|
+
}
|
|
310
|
+
const chosen: GapRule = mixed ? "all" : "collection";
|
|
311
|
+
if (out[`gap.${chosen}`] !== undefined) {
|
|
312
|
+
out["gap"] = out[`gap.${chosen}`]!;
|
|
313
|
+
out["gap.cap"] = cap;
|
|
314
|
+
}
|
|
315
|
+
return out;
|
|
316
|
+
}
|
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
|
|
|
@@ -45,6 +93,9 @@ const ROWS: readonly Row[] = [
|
|
|
45
93
|
["R11.q05", "R11", "route.plug-spacing.q05", "0 to 1", "higher", true],
|
|
46
94
|
["R13.outside", "R13", "route.outside", "count", "lower", false],
|
|
47
95
|
["R15.inflections", "R15", "route.inflections", "count for each route", "lower", false],
|
|
96
|
+
// MEAS3A, for the steps `router` and `highway`. R12 is the share of the ink near a parallel leg of other edges. A narrow channel between boxes moves it.
|
|
97
|
+
["R12.share", "R12", "route.lane.separation", "0 to 1", "lower", true],
|
|
98
|
+
["R14.share", "R14", "route.orthogonality", "0 to 1", "higher", false],
|
|
48
99
|
["ink", "R1", "route.ink", "units", "lower", true],
|
|
49
100
|
// The bundles.
|
|
50
101
|
["B1.inkSaved", "B1", "route.bundle.ink-saved", "0 to 1", "higher", false],
|
|
@@ -54,6 +105,28 @@ const ROWS: readonly Row[] = [
|
|
|
54
105
|
["B3.median", "B3", "route.bundle.stretch.median", "ratio", "lower", false],
|
|
55
106
|
["B3.q95", "B3", "route.bundle.stretch.q95", "ratio", "lower", false],
|
|
56
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],
|
|
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.
|
|
119
|
+
["B5.crossings", "B5", "route.bundle.lane-crossings", "count", "lower", false],
|
|
120
|
+
["B7.ratio", "B7", "route.bundle.occupied-area", "ratio", "lower", false],
|
|
121
|
+
["B8.runs", "B8", "route.bundle.runs", "count", "lower", false],
|
|
122
|
+
["B8.median", "B8", "route.bundle.run-edges.median", "count", "higher", false],
|
|
123
|
+
["B8.max", "B8", "route.bundle.run-edges.max", "count", "higher", false],
|
|
124
|
+
["B9.median", "B9", "route.bundle.lane-fidelity.median", "ratio", "lower", false],
|
|
125
|
+
["B9.max", "B9", "route.bundle.lane-fidelity.max", "ratio", "lower", false],
|
|
126
|
+
// M1 and M4 of the room, for the steps `router` and `highway`: the lanes of each shared segment open at `MEASURE_LANE_WIDTH`.
|
|
127
|
+
// The step `room` of `runSuite` gives the same keys for the boards that a room strategy opens.
|
|
128
|
+
["M1.area", "M1", "route.lane.area", "ratio", "lower", false],
|
|
129
|
+
["M4.laneThrough", "M4", "route.lane.through", "count", "lower", true],
|
|
57
130
|
// The order fragility (W3 of MEAS3, from WILD-056): the spread of a measure over random orders of the edges. The placement can move it.
|
|
58
131
|
["W3.crossings", "W3", "route.order.crossings", "IQR over the median", "lower", true],
|
|
59
132
|
["W3.detour", "W3", "route.order.detour", "IQR over the median", "lower", true],
|
|
@@ -75,6 +148,9 @@ const ROWS: readonly Row[] = [
|
|
|
75
148
|
["Z10.ids", "Z10", "route.stability.highway-ids", "0 to 1", "higher", false],
|
|
76
149
|
// The zoom levels and the groups. The default grouping rule reads the positions, so most of these measures depend on the placement.
|
|
77
150
|
["level-nesting", "Z6", "route.level.nesting", "count", "lower", false],
|
|
151
|
+
// MEAS3A, for the steps `router`, `highway` and `group`. Z2 is the count of the hard rule `link-ends`. Z9 needs a result with zoom levels.
|
|
152
|
+
["Z2.linkEnds", "Z2", "route.level.link-ends", "count", "lower", false],
|
|
153
|
+
["Z9.mean", "Z9", "route.level.link-agreement", "no unit", "lower", true],
|
|
78
154
|
["Z3.through", "Z3", "route.level.through", "count", "lower", true],
|
|
79
155
|
["Z3.length", "Z3", "route.level.link-length", "units", "lower", true],
|
|
80
156
|
["Z4.farItems", "Z4", "route.level.far-items", "count", "lower", true],
|
|
@@ -105,7 +181,19 @@ const ROWS: readonly Row[] = [
|
|
|
105
181
|
* A joint harness that moves the boxes and routes the edges reads `placementDependent`.
|
|
106
182
|
* It compares a measure with the value `true` only between runs on the same input.
|
|
107
183
|
*/
|
|
108
|
-
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
|
+
}
|
|
109
197
|
|
|
110
198
|
const BY_KEY: ReadonlyMap<string, MeasureInfo> = new Map(MEASURES.map((m) => [m.key, m]));
|
|
111
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";
|