@mark1russell7/layout-contract 0.2.5 → 0.2.7

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.
@@ -0,0 +1,300 @@
1
+ import { outputHash } from "./hash.ts";
2
+ import { compareMeasure, type MeasureSpec } from "./measures.ts";
3
+
4
+ /** The part of a `MeasureSpec` that a pick uses: the direction and the `minEffect`. */
5
+ export type PickSpec = Pick<MeasureSpec, "direction" | "minEffect">;
6
+
7
+ /** A member of a portfolio: a function with an id. A portfolio is also a member. */
8
+ export interface Member<I, O> {
9
+ readonly id: string;
10
+ /** This function gives the output of the member for an input. */
11
+ run(input: I): O;
12
+ }
13
+
14
+ /** One level of the key of a portfolio: a measure of an output. */
15
+ export interface PortfolioLevel<I, O> {
16
+ /** The id of the level. The id `hard` is reserved. */
17
+ readonly id: string;
18
+ readonly spec: PickSpec;
19
+ /** This function gives the value of the measure for the output of a member. The value must be finite. */
20
+ value(input: I, output: O): number;
21
+ }
22
+
23
+ /** The key of a portfolio: the hard level, then the other levels in order. */
24
+ export interface PortfolioKey<I, O> {
25
+ /** This function gives the count of hard breaks. It is level 0, with the direction `lower` and the `minEffect` 0. */
26
+ hard(input: I, output: O): number;
27
+ /** The levels after the hard level, in the order of the pick. */
28
+ readonly levels: readonly PortfolioLevel<I, O>[];
29
+ /** This function gives the hash of an output. The default is `outputHash` with the default grid. */
30
+ hash?(output: O): string;
31
+ }
32
+
33
+ /** The options of a portfolio. */
34
+ export interface PortfolioOptions {
35
+ /** The time limit in milliseconds, 0 or more. The first member always starts. A member that starts always finishes. */
36
+ readonly budgetMs?: number;
37
+ /** The largest count of members that run, a whole number of 1 or more. The other members are cut. */
38
+ readonly maxMembers?: number;
39
+ /** The clock in milliseconds. The default is `performance.now`, or `Date.now` without `performance`. */
40
+ readonly now?: () => number;
41
+ }
42
+
43
+ /** The status of a member in a pick. */
44
+ export type MemberStatus = "ran" | "cut" | "error";
45
+
46
+ /** The trace of one member in a pick. */
47
+ export interface MemberTrace {
48
+ readonly id: string;
49
+ readonly status: MemberStatus;
50
+ /** The value of each level, with the key `hard` for the hard level. Only a member with the status `ran` has values. */
51
+ readonly values?: Readonly<Record<string, number>>;
52
+ /** The time of the run of the member, in milliseconds. */
53
+ readonly ms?: number;
54
+ /** The hash of the output of the member. */
55
+ readonly outputHash?: string;
56
+ /** The message of the error of a member with the status `error`. */
57
+ readonly error?: string;
58
+ }
59
+
60
+ /** The trace of one pick of a portfolio. */
61
+ export interface PortfolioTrace {
62
+ /** One entry for each member, in the order of the members. A cut member is also in the list. */
63
+ readonly members: readonly MemberTrace[];
64
+ readonly winner: string;
65
+ /** The index of the winner in the list of the members. */
66
+ readonly winnerIndex: number;
67
+ /** The value `true` tells that the winner is not member 0 and has a different output hash. */
68
+ readonly picked: boolean;
69
+ /** The time of the full pick, in milliseconds. */
70
+ readonly totalMs: number;
71
+ readonly budgetMs?: number;
72
+ readonly maxMembers?: number;
73
+ /** The count of the cut members. */
74
+ readonly cut: number;
75
+ /** The value `false` tells that the time limit cut one member or more. Then a run on a different computer can give a different pick. */
76
+ readonly deterministic: boolean;
77
+ /** The ids of the members that stay in after each level of `selectBest`. The first list is for the hard level. */
78
+ readonly kept: readonly (readonly string[])[];
79
+ }
80
+
81
+ /** The result of one pick of a portfolio: the output of the winner, its id and the trace. */
82
+ export interface PortfolioPick<O> {
83
+ readonly output: O;
84
+ readonly winner: string;
85
+ readonly trace: PortfolioTrace;
86
+ }
87
+
88
+ /** A portfolio: a member that runs its members and gives the output of the best member. */
89
+ export interface Portfolio<I, O> extends Member<I, O> {
90
+ /** This function starts the members on an input and gives the output of the winner with the trace. */
91
+ pick(input: I): PortfolioPick<O>;
92
+ }
93
+
94
+ /** A candidate of `selectBest`: an id and the value of each level. */
95
+ export interface SelectCandidate {
96
+ readonly id: string;
97
+ readonly values: Readonly<Record<string, number>>;
98
+ }
99
+
100
+ /** A level of `selectBest`: the id of a value and its spec. */
101
+ export interface SelectLevel {
102
+ readonly id: string;
103
+ readonly spec: PickSpec;
104
+ }
105
+
106
+ /** The result of `selectBest`. */
107
+ export interface Selection {
108
+ readonly winner: string;
109
+ /** The ids of the candidates that stay in after each level, in the given order. */
110
+ readonly kept: readonly (readonly string[])[];
111
+ }
112
+
113
+ /**
114
+ * This function picks the best candidate. For each level, it finds the best value of the candidates that stay in.
115
+ * It keeps each candidate that is not worse than that best value by `compareMeasure`.
116
+ * The winner is the first candidate, in the given order, that stays in after the last level.
117
+ * Without levels, the winner is the first candidate and `kept` is empty.
118
+ *
119
+ * The reason for the rule: a scan that compares each candidate with the current winner is not transitive, because of the `minEffect` ties.
120
+ * Such a scan can drift away from the best value. An example: "a" is lower with the `minEffect` 0.005, and "b" is higher.
121
+ * The candidates A (0, 1), B (0.004, 2) and C (0.008, 3) give the winner B.
122
+ * The scan picks C, but A is better than C on "a".
123
+ *
124
+ * The function throws a `RangeError` for an empty list of candidates, for a missing value, and for a value that is not finite.
125
+ */
126
+ export function selectBest(candidates: readonly SelectCandidate[], levels: readonly SelectLevel[]): Selection {
127
+ if (candidates.length === 0) throw new RangeError("selectBest: the list of candidates is empty.");
128
+ for (const level of levels) {
129
+ for (const c of candidates) {
130
+ const v = c.values[level.id];
131
+ if (typeof v !== "number") throw new RangeError(`selectBest: the candidate "${c.id}" has no value for the level "${level.id}".`);
132
+ if (!Number.isFinite(v)) throw new RangeError(`selectBest: the candidate "${c.id}" has the value ${v} for the level "${level.id}".`);
133
+ }
134
+ }
135
+ let alive: readonly SelectCandidate[] = candidates;
136
+ const kept: (readonly string[])[] = [];
137
+ for (const level of levels) {
138
+ const at = (c: SelectCandidate): number => c.values[level.id]!;
139
+ let best = at(alive[0]!);
140
+ for (const c of alive) best = level.spec.direction === "lower" ? Math.min(best, at(c)) : Math.max(best, at(c));
141
+ alive = alive.filter((c) => compareMeasure(at(c), best, level.spec) !== "worse");
142
+ kept.push(alive.map((c) => c.id));
143
+ }
144
+ return { winner: alive[0]!.id, kept };
145
+ }
146
+
147
+ /** This function gives the move `pick` of a pick: 1 when `picked` is true, else 0. A record keeps it as the trace measure `move.pick`. */
148
+ export function pickMoves(trace: Pick<PortfolioTrace, "picked">): { pick: number } {
149
+ return { pick: trace.picked ? 1 : 0 };
150
+ }
151
+
152
+ /** This function gives the default clock: `performance.now`, or `Date.now` without `performance`. */
153
+ function defaultClock(): () => number {
154
+ const perf = (globalThis as { readonly performance?: { now(): number } }).performance;
155
+ return perf ? () => perf.now() : () => Date.now();
156
+ }
157
+
158
+ /** This function gives the message of an error value. */
159
+ function messageOf(e: unknown): string {
160
+ return e instanceof Error ? e.message : String(e);
161
+ }
162
+
163
+ /** This function throws a `RangeError` for an empty list of members or for two members with the same id. */
164
+ function checkMembers(id: string, members: readonly Member<unknown, unknown>[]): void {
165
+ if (members.length === 0) throw new RangeError(`The portfolio "${id}" has no members.`);
166
+ const seen = new Set<string>();
167
+ for (const m of members) {
168
+ if (seen.has(m.id)) throw new RangeError(`The portfolio "${id}" has two members with the id "${m.id}".`);
169
+ seen.add(m.id);
170
+ }
171
+ }
172
+
173
+ /** The result of the run of one member. */
174
+ type Ran<O> = { readonly ok: true; readonly output: O; readonly values: Record<string, number>; readonly hash: string } | { readonly ok: false; readonly error: unknown };
175
+
176
+ /**
177
+ * This function makes a portfolio. Its `pick` starts the members in their order. `selectBest` picks the winner of the members with the status `ran`.
178
+ * The first level is the hard level, then the levels of the key follow.
179
+ * `members` is a list, or a function of the input that gives the list.
180
+ *
181
+ * A member that throws, or that gives a value that is not finite, gets the status `error`. It does not win.
182
+ * When each member has an error, `pick` throws the error of the first member.
183
+ * `docs/PORTFOLIO.md` tells the rules of the time limit, the trace and `picked`.
184
+ *
185
+ * The function throws a `RangeError` for a level with the id `hard`, for two levels with the same id and for a bad option.
186
+ * It also throws a `RangeError` for an empty list of members, or for two members with the same id.
187
+ */
188
+ export function portfolio<I, O>(
189
+ id: string,
190
+ members: readonly Member<I, O>[] | ((input: I) => readonly Member<I, O>[]),
191
+ key: PortfolioKey<I, O>,
192
+ options: PortfolioOptions = {},
193
+ ): Portfolio<I, O> {
194
+ const levelIds = new Set<string>();
195
+ for (const level of key.levels) {
196
+ if (level.id === "hard") throw new RangeError(`The portfolio "${id}" has a level with the reserved id "hard".`);
197
+ if (levelIds.has(level.id)) throw new RangeError(`The portfolio "${id}" has two levels with the id "${level.id}".`);
198
+ levelIds.add(level.id);
199
+ }
200
+ const { budgetMs, maxMembers } = options;
201
+ if (budgetMs !== undefined && !(budgetMs >= 0)) throw new RangeError(`The portfolio "${id}" has the budgetMs ${budgetMs}. It must be 0 or more.`);
202
+ if (maxMembers !== undefined && !(Number.isInteger(maxMembers) && maxMembers >= 1))
203
+ throw new RangeError(`The portfolio "${id}" has the maxMembers ${maxMembers}. It must be a whole number of 1 or more.`);
204
+ if (typeof members !== "function") checkMembers(id, members);
205
+ const levels: readonly SelectLevel[] = [{ id: "hard", spec: { direction: "lower", minEffect: 0 } }, ...key.levels.map((l) => ({ id: l.id, spec: l.spec }))];
206
+ const now = options.now ?? defaultClock();
207
+
208
+ const evaluate = (m: Member<I, O>, input: I, output: O): Ran<O> => {
209
+ const values: Record<string, number> = {};
210
+ const measure = (level: string, f: () => number): void => {
211
+ let v: number;
212
+ try {
213
+ v = f();
214
+ } catch (e) {
215
+ throw new RangeError(`The level "${level}" of the member "${m.id}" of the portfolio "${id}" throws: ${messageOf(e)}`, { cause: e });
216
+ }
217
+ if (typeof v !== "number" || !Number.isFinite(v))
218
+ throw new RangeError(`The member "${m.id}" of the portfolio "${id}" has the value ${v} for the level "${level}".`);
219
+ values[level] = v;
220
+ };
221
+ try {
222
+ measure("hard", () => key.hard(input, output));
223
+ for (const level of key.levels) measure(level.id, () => level.value(input, output));
224
+ let hash: string;
225
+ try {
226
+ hash = key.hash ? key.hash(output) : outputHash(output);
227
+ } catch (e) {
228
+ throw new RangeError(`The hash of the member "${m.id}" of the portfolio "${id}" throws: ${messageOf(e)}`, { cause: e });
229
+ }
230
+ return { ok: true, output, values, hash };
231
+ } catch (e) {
232
+ return { ok: false, error: e };
233
+ }
234
+ };
235
+
236
+ const pick = (input: I): PortfolioPick<O> => {
237
+ const list = typeof members === "function" ? members(input) : members;
238
+ if (typeof members === "function") checkMembers(id, list);
239
+ const start = now();
240
+ const traces: MemberTrace[] = [];
241
+ const outputs = new Map<string, O>();
242
+ const ran: SelectCandidate[] = [];
243
+ let firstError: { readonly error: unknown } | undefined;
244
+ let cut = 0;
245
+ let outOfTime = false;
246
+ for (let i = 0; i < list.length; i++) {
247
+ const m = list[i]!;
248
+ if (maxMembers !== undefined && i >= maxMembers) {
249
+ traces.push({ id: m.id, status: "cut" });
250
+ cut++;
251
+ continue;
252
+ }
253
+ if (i > 0 && budgetMs !== undefined && (outOfTime || now() - start >= budgetMs)) {
254
+ outOfTime = true;
255
+ traces.push({ id: m.id, status: "cut" });
256
+ cut++;
257
+ continue;
258
+ }
259
+ const t0 = now();
260
+ let result: Ran<O>;
261
+ let ms: number;
262
+ try {
263
+ const output = m.run(input);
264
+ ms = now() - t0;
265
+ result = evaluate(m, input, output);
266
+ } catch (e) {
267
+ ms = now() - t0;
268
+ result = { ok: false, error: e };
269
+ }
270
+ if (result.ok) {
271
+ traces.push({ id: m.id, status: "ran", values: result.values, ms, outputHash: result.hash });
272
+ outputs.set(m.id, result.output);
273
+ ran.push({ id: m.id, values: result.values });
274
+ } else {
275
+ traces.push({ id: m.id, status: "error", ms, error: messageOf(result.error) });
276
+ firstError ??= { error: result.error };
277
+ }
278
+ }
279
+ if (ran.length === 0) throw firstError!.error;
280
+ const { winner, kept } = selectBest(ran, levels);
281
+ const winnerIndex = traces.findIndex((t) => t.id === winner);
282
+ const base = traces[0]!;
283
+ const picked = winnerIndex > 0 && (base.status !== "ran" || traces[winnerIndex]!.outputHash !== base.outputHash);
284
+ const trace: PortfolioTrace = {
285
+ members: traces,
286
+ winner,
287
+ winnerIndex,
288
+ picked,
289
+ totalMs: now() - start,
290
+ ...(budgetMs !== undefined ? { budgetMs } : {}),
291
+ ...(maxMembers !== undefined ? { maxMembers } : {}),
292
+ cut,
293
+ deterministic: !outOfTime,
294
+ kept,
295
+ };
296
+ return { output: outputs.get(winner)!, winner, trace };
297
+ };
298
+
299
+ return { id, pick, run: (input: I): O => pick(input).output };
300
+ }
package/src/tolerance.ts CHANGED
@@ -1,9 +1,15 @@
1
1
  /** The epsilon for the float compares in an engine. */
2
2
  export const EPS = 1e-6;
3
3
 
4
+ /**
5
+ * The count of grid steps in one unit of a written file. A file rounds each number `v` to `Math.round(v * RECORD_SCALE) / RECORD_SCALE`.
6
+ * Use a multiplication by this value, not a division by `RECORD_EPS`. The two differ at some half steps.
7
+ */
8
+ export const RECORD_SCALE = 1000;
9
+
4
10
  /**
5
11
  * The epsilon for the float compares on a written file. A file rounds each number to 3 decimals,
6
- * so a difference of two values can move by up to 0.001.
12
+ * so a difference of two values can move by up to 0.001. It is `1 / RECORD_SCALE`.
7
13
  */
8
14
  export const RECORD_EPS = 1e-3;
9
15