@mark1russell7/rigor-bench 0.2.24 → 0.2.26
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 +76 -0
- package/build/index.d.ts +154 -1
- package/build/index.d.ts.map +1 -1
- package/build/index.js +433 -187
- package/build/index.js.map +1 -1
- package/package.json +2 -2
package/build/index.js
CHANGED
|
@@ -28,7 +28,7 @@ function caseBudget(name, values, aaValues, options, level) {
|
|
|
28
28
|
const aaMedian = aaValues.length > 0 ? median(aaValues) : void 0;
|
|
29
29
|
const difference = aaMedian === void 0 ? void 0 : Math.abs(middle - aaMedian);
|
|
30
30
|
const aaCoverage = aaValues.length > 0 ? medianInterval(aaValues, level).coverage : 0;
|
|
31
|
-
const facts = `median ${round$
|
|
31
|
+
const facts = `median ${round$4(middle)} of ${values.length} rounds, interval ${round$4(interval.low)} to ${round$4(interval.high)}, limit ${limit}, A/A median ${aaMedian === void 0 ? "none" : round$4(aaMedian)} of ${aaValues.length} rounds, difference ${difference === void 0 ? "none" : round$4(difference)}`;
|
|
32
32
|
const decide = (outcome, why) => ({
|
|
33
33
|
case: name,
|
|
34
34
|
rounds: values.length,
|
|
@@ -180,7 +180,7 @@ function budgetLooksVerdict(rounds, aa, options) {
|
|
|
180
180
|
const looks = options.looks ?? BUDGET_LOOKS;
|
|
181
181
|
const level = options.level ?? .95;
|
|
182
182
|
const spent = budgetLookSpend(looks);
|
|
183
|
-
if (spent > 1 - level) throw new RangeError(`budgetLooksVerdict: the looks spend ${round$
|
|
183
|
+
if (spent > 1 - level) throw new RangeError(`budgetLooksVerdict: the looks spend ${round$4(spent)}, over the ${round$4(1 - level)} of the level ${level}.`);
|
|
184
184
|
if (!(limit >= 0 && Number.isFinite(limit))) throw new RangeError(`budgetLooksVerdict: the limit is finite and 0 or more, not ${limit}.`);
|
|
185
185
|
if (!(maxDifference >= 0 && Number.isFinite(maxDifference))) throw new RangeError(`budgetLooksVerdict: the largest difference is finite and 0 or more, not ${maxDifference}.`);
|
|
186
186
|
const byRound = (list) => [...list].sort((a, b) => a.round - b.round);
|
|
@@ -209,9 +209,73 @@ function budgetLooksVerdict(rounds, aa, options) {
|
|
|
209
209
|
fails: verdict.cases.some((c) => c.outcome === "beyond")
|
|
210
210
|
};
|
|
211
211
|
}
|
|
212
|
-
function round$
|
|
212
|
+
function round$4(x) {
|
|
213
213
|
return Math.round(x * 1e4) / 1e4;
|
|
214
214
|
}
|
|
215
|
+
// src/growth.ts
|
|
216
|
+
/**
|
|
217
|
+
* The growth verdict of a cost over sizes: does the cost grow slower than a limit, for each step of size? An example is
|
|
218
|
+
* the claim "the time of one publish grows less than 15 times for each 10 times more subscribers". A cost that grows as
|
|
219
|
+
* n x n grows 100 times for each 10 times, and a linear cost grows 10 times.
|
|
220
|
+
*
|
|
221
|
+
* The verdict reads a ratio inside each job: the cost at the larger size over the cost at the smaller size, on the same
|
|
222
|
+
* runner. So a slow runner changes both sizes, and the ratio keeps its value. The verdict needs no A/A arm.
|
|
223
|
+
*/
|
|
224
|
+
/** This function gives the growth verdict of a cost over its sizes, from the values of one or more jobs. */
|
|
225
|
+
function growthVerdict(rounds, options) {
|
|
226
|
+
const per = options.per ?? 10;
|
|
227
|
+
const level = options.level ?? .95;
|
|
228
|
+
if (!(Number.isFinite(options.maxGrowth) && options.maxGrowth > 1)) throw new RangeError("growthVerdict: maxGrowth is not a finite number more than 1.");
|
|
229
|
+
if (!(Number.isFinite(per) && per > 1)) throw new RangeError("growthVerdict: per is not a finite number more than 1.");
|
|
230
|
+
const byJob = /* @__PURE__ */ new Map();
|
|
231
|
+
for (const round of rounds) {
|
|
232
|
+
if (!(Number.isFinite(round.size) && round.size > 0)) throw new RangeError(`growthVerdict: job ${round.job} has the size ${round.size}, which is not finite and more than 0.`);
|
|
233
|
+
if (!(Number.isFinite(round.value) && round.value > 0)) throw new RangeError(`growthVerdict: job ${round.job} has the value ${round.value} at the size ${round.size}, which is not finite and more than 0.`);
|
|
234
|
+
const sizes = byJob.get(round.job) ?? /* @__PURE__ */ new Map();
|
|
235
|
+
byJob.set(round.job, sizes);
|
|
236
|
+
const values = sizes.get(round.size) ?? [];
|
|
237
|
+
sizes.set(round.size, values);
|
|
238
|
+
values.push(round.value);
|
|
239
|
+
}
|
|
240
|
+
const sizes = [...new Set(rounds.map((round) => round.size))].sort((a, b) => a - b);
|
|
241
|
+
if (sizes.length < 2) throw new RangeError("growthVerdict: the rounds give fewer than 2 sizes.");
|
|
242
|
+
const jobs = [...byJob.keys()].sort();
|
|
243
|
+
const steps = [];
|
|
244
|
+
for (let i = 0; i + 1 < sizes.length; i++) {
|
|
245
|
+
const from = sizes[i];
|
|
246
|
+
const to = sizes[i + 1];
|
|
247
|
+
const growths = [];
|
|
248
|
+
for (const job of jobs) {
|
|
249
|
+
const values = byJob.get(job);
|
|
250
|
+
const small = values.get(from);
|
|
251
|
+
const large = values.get(to);
|
|
252
|
+
if (small === void 0 || large === void 0) continue;
|
|
253
|
+
const exponent = Math.log(median(large) / median(small)) / Math.log(to / from);
|
|
254
|
+
growths.push(Math.pow(per, exponent));
|
|
255
|
+
}
|
|
256
|
+
if (growths.length === 0) throw new RangeError(`growthVerdict: no job gives both the size ${from} and the size ${to}.`);
|
|
257
|
+
const interval = medianInterval(growths, level);
|
|
258
|
+
const middle = median(growths);
|
|
259
|
+
const outcome = interval.high < options.maxGrowth ? "within" : interval.low > options.maxGrowth ? "beyond" : "cannot decide";
|
|
260
|
+
steps.push({
|
|
261
|
+
from,
|
|
262
|
+
to,
|
|
263
|
+
growths,
|
|
264
|
+
median: middle,
|
|
265
|
+
low: interval.low,
|
|
266
|
+
high: interval.high,
|
|
267
|
+
coverage: interval.coverage,
|
|
268
|
+
exponent: Math.log(middle) / Math.log(per),
|
|
269
|
+
outcome
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
return {
|
|
273
|
+
outcome: steps.some((step) => step.outcome === "beyond") ? "beyond" : steps.every((step) => step.outcome === "within") ? "within" : "cannot decide",
|
|
274
|
+
maxGrowth: options.maxGrowth,
|
|
275
|
+
per,
|
|
276
|
+
steps
|
|
277
|
+
};
|
|
278
|
+
}
|
|
215
279
|
// src/gate.ts
|
|
216
280
|
/**
|
|
217
281
|
* The gate of a bench: the result that a check of a merge reads. A verdict gives one of the words of `LimitOutcome` for each
|
|
@@ -871,14 +935,14 @@ function timeVerdict(candidate, aa, options = {}) {
|
|
|
871
935
|
noise
|
|
872
936
|
};
|
|
873
937
|
if (noise.noisy && verdict.band !== void 0) {
|
|
874
|
-
const reason = `${spread === void 0 ? "The A/A arm of the run has no cases, so the noise cap has no spread." : `The A/A spread of the run is ${round$
|
|
938
|
+
const reason = `${spread === void 0 ? "The A/A arm of the run has no cases, so the noise cap has no spread." : `The A/A spread of the run is ${round$3(spread)}, above the noise cap ${cap}.`} Without the cap, the outcome is "${verdict.outcome}".`;
|
|
875
939
|
verdict = {
|
|
876
940
|
...verdict,
|
|
877
941
|
outcome: "cannot decide",
|
|
878
942
|
reason
|
|
879
943
|
};
|
|
880
944
|
} else if (noise.biased && verdict.band !== void 0) {
|
|
881
|
-
const reason = `The A/A median ratio of the run is ${round$
|
|
945
|
+
const reason = `The A/A median ratio of the run is ${round$3(aaMedian)}, out of [${round$3(Math.exp(-cap))}, ${round$3(Math.exp(cap))}] of the noise cap ${cap}. Without the cap, the outcome is "${verdict.outcome}".`;
|
|
882
946
|
verdict = {
|
|
883
947
|
...verdict,
|
|
884
948
|
outcome: "cannot decide",
|
|
@@ -946,44 +1010,59 @@ function ruleVerdict(candidate, aa, options, pooled) {
|
|
|
946
1010
|
};
|
|
947
1011
|
if (rule === "difference") {
|
|
948
1012
|
const margin = marginOf(options, pooled);
|
|
949
|
-
const shown = pooled === void 0 ? margin : round$
|
|
1013
|
+
const shown = pooled === void 0 ? margin : round$3(margin);
|
|
950
1014
|
const logs = cases.map((c) => Math.log(c.ratio));
|
|
951
1015
|
const aaLogs = aaRatios.map((c) => Math.log(c.ratio));
|
|
952
|
-
const d = bootstrapTwo(logs, aaLogs, (x, y) => median(x) - median(y), options.
|
|
1016
|
+
const d = bootstrapTwo(logs, aaLogs, (x, y) => median(x) - median(y), options.pValues === true ? {
|
|
1017
|
+
...options.bootstrap,
|
|
1018
|
+
against: 0
|
|
1019
|
+
} : options.bootstrap);
|
|
1020
|
+
const { level, runs, finite, seed } = d;
|
|
953
1021
|
const interval = {
|
|
954
|
-
...d,
|
|
955
1022
|
estimate: Math.exp(d.estimate),
|
|
956
1023
|
low: Math.exp(d.low),
|
|
957
|
-
high: Math.exp(d.high)
|
|
1024
|
+
high: Math.exp(d.high),
|
|
1025
|
+
level,
|
|
1026
|
+
runs,
|
|
1027
|
+
finite,
|
|
1028
|
+
seed
|
|
958
1029
|
};
|
|
959
|
-
const
|
|
1030
|
+
const sides = options.pValues === true ? {
|
|
1031
|
+
pSlower: d.pAbove ?? 1,
|
|
1032
|
+
pFaster: d.pBelow ?? 1
|
|
1033
|
+
} : {};
|
|
1034
|
+
const span = `The interval of the candidate median over the A/A median is [${round$3(interval.low)}, ${round$3(interval.high)}].`;
|
|
960
1035
|
if (d.low >= -margin && d.high <= margin) return {
|
|
961
1036
|
...facts,
|
|
1037
|
+
...sides,
|
|
962
1038
|
interval,
|
|
963
1039
|
outcome: "same",
|
|
964
1040
|
reason: `${span} The interval is in the margin of ${shown}.`
|
|
965
1041
|
};
|
|
966
1042
|
if (interval.high < 1) return {
|
|
967
1043
|
...facts,
|
|
1044
|
+
...sides,
|
|
968
1045
|
interval,
|
|
969
1046
|
outcome: "faster",
|
|
970
1047
|
reason: `${span} The interval is below 1.`
|
|
971
1048
|
};
|
|
972
1049
|
if (interval.low > 1) return {
|
|
973
1050
|
...facts,
|
|
1051
|
+
...sides,
|
|
974
1052
|
interval,
|
|
975
1053
|
outcome: "slower",
|
|
976
1054
|
reason: `${span} The interval is above 1.`
|
|
977
1055
|
};
|
|
978
1056
|
return {
|
|
979
1057
|
...facts,
|
|
1058
|
+
...sides,
|
|
980
1059
|
interval,
|
|
981
1060
|
outcome: "cannot decide",
|
|
982
1061
|
reason: `${span} The interval holds 1 and crosses the margin of ${shown}.`
|
|
983
1062
|
};
|
|
984
1063
|
}
|
|
985
1064
|
const interval = bootstrapMedian(cases.map((c) => c.ratio), options.bootstrap);
|
|
986
|
-
const span = `The interval of the median ratio is [${round$
|
|
1065
|
+
const span = `The interval of the median ratio is [${round$3(interval.low)}, ${round$3(interval.high)}], and the A/A band is [${round$3(band.low)}, ${round$3(band.high)}].`;
|
|
987
1066
|
if (interval.high < band.low) return {
|
|
988
1067
|
...facts,
|
|
989
1068
|
interval,
|
|
@@ -1009,7 +1088,7 @@ function ruleVerdict(candidate, aa, options, pooled) {
|
|
|
1009
1088
|
reason: `${span} The interval crosses an end of the band.`
|
|
1010
1089
|
};
|
|
1011
1090
|
}
|
|
1012
|
-
function round$
|
|
1091
|
+
function round$3(x) {
|
|
1013
1092
|
return Math.round(x * 1e4) / 1e4;
|
|
1014
1093
|
}
|
|
1015
1094
|
// src/limit.ts
|
|
@@ -1114,20 +1193,358 @@ function limitVerdict(candidate, aa, options) {
|
|
|
1114
1193
|
const rest = others.length === 0 ? "" : ` ${others.length} of ${cases.length} cases are not within${permit}.`;
|
|
1115
1194
|
return {
|
|
1116
1195
|
outcome: "within",
|
|
1117
|
-
reason: `The largest bound of a case within is ${round$
|
|
1196
|
+
reason: `The largest bound of a case within is ${round$2(top.bound)} in the case ${JSON.stringify(top.case)}, and it is at most the limit ${limit}.${rest}`,
|
|
1118
1197
|
...facts
|
|
1119
1198
|
};
|
|
1120
1199
|
}
|
|
1121
1200
|
const top = cases.reduce((a, b) => b.bound > a.bound ? b : a);
|
|
1122
1201
|
return {
|
|
1123
1202
|
outcome: "cannot decide",
|
|
1124
|
-
reason: `${others.length} of ${cases.length} cases are not within${permit}. The bound of the case ${JSON.stringify(top.case)} is ${round$
|
|
1203
|
+
reason: `${others.length} of ${cases.length} cases are not within${permit}. The bound of the case ${JSON.stringify(top.case)} is ${round$2(top.bound)}, above the limit ${limit}, and too few floors are above it.`,
|
|
1125
1204
|
...facts
|
|
1126
1205
|
};
|
|
1127
1206
|
}
|
|
1128
|
-
function round$
|
|
1207
|
+
function round$2(x) {
|
|
1129
1208
|
return Math.round(x * 1e4) / 1e4;
|
|
1130
1209
|
}
|
|
1210
|
+
// src/merge.ts
|
|
1211
|
+
/**
|
|
1212
|
+
* The merge of the records of many jobs, and the pooled verdicts over them. Each job of a bench gives its own records.
|
|
1213
|
+
* A pooled verdict uses all jobs together, and the verdict of each job alone guards it.
|
|
1214
|
+
*/
|
|
1215
|
+
/**
|
|
1216
|
+
* This function removes records until the two orders of each case differ in count by 1 or less.
|
|
1217
|
+
* It removes the records of the larger order with the highest rounds first. The other records keep their order.
|
|
1218
|
+
* A job that stopped early, or a missing job, can leave a case with 2 more rounds of one order. `caseRatios` refuses such a case.
|
|
1219
|
+
*/
|
|
1220
|
+
function balanceOrders(records) {
|
|
1221
|
+
const byCase = /* @__PURE__ */ new Map();
|
|
1222
|
+
records.forEach((record, index) => {
|
|
1223
|
+
const orders = byCase.get(record.case) ?? {
|
|
1224
|
+
a: [],
|
|
1225
|
+
b: []
|
|
1226
|
+
};
|
|
1227
|
+
orders[record.first === "b" ? "b" : "a"].push(index);
|
|
1228
|
+
byCase.set(record.case, orders);
|
|
1229
|
+
});
|
|
1230
|
+
const drop = /* @__PURE__ */ new Set();
|
|
1231
|
+
const highFirst = (x, y) => records[y].round - records[x].round;
|
|
1232
|
+
for (const { a, b } of byCase.values()) {
|
|
1233
|
+
const [more, fewer] = a.length > b.length ? [a, b] : [b, a];
|
|
1234
|
+
const extra = more.length - fewer.length - 1;
|
|
1235
|
+
if (extra > 0) for (const index of more.toSorted(highFirst).slice(0, extra)) drop.add(index);
|
|
1236
|
+
}
|
|
1237
|
+
return {
|
|
1238
|
+
records: records.filter((_, index) => !drop.has(index)),
|
|
1239
|
+
trimmed: drop.size
|
|
1240
|
+
};
|
|
1241
|
+
}
|
|
1242
|
+
/**
|
|
1243
|
+
* This function gives the pool of the jobs. `pooledVerdict` and `pooledLimitVerdict` use it.
|
|
1244
|
+
* - It refuses two jobs with one label, and a record with a different job label. It also refuses two jobs with a record of one case and one round.
|
|
1245
|
+
* - It removes each case that a job skipped from all jobs, so all jobs have the same cases.
|
|
1246
|
+
* - It merges the records of the jobs, and it balances the merged records and the records of each job with `balanceOrders`.
|
|
1247
|
+
*/
|
|
1248
|
+
function poolRecords(jobs) {
|
|
1249
|
+
return pool(jobs, "poolRecords");
|
|
1250
|
+
}
|
|
1251
|
+
/** The pool of `poolRecords`. Each error starts with the name `caller`. */
|
|
1252
|
+
function pool(jobs, caller) {
|
|
1253
|
+
const labels = /* @__PURE__ */ new Set();
|
|
1254
|
+
for (const { job } of jobs) {
|
|
1255
|
+
if (labels.has(job)) throw new RangeError(`${caller}: two jobs have the label ${JSON.stringify(job)}.`);
|
|
1256
|
+
labels.add(job);
|
|
1257
|
+
}
|
|
1258
|
+
for (const { job, candidate: c, aa: a } of jobs) for (const r of [...c, ...a]) if (r.job !== void 0 && r.job !== job) throw new RangeError(`${caller}: a record of the job ${JSON.stringify(job)} has the job ${JSON.stringify(r.job)}.`);
|
|
1259
|
+
const dropped = [...new Set(jobs.flatMap((j) => (j.skipped ?? []).map((s) => s.case)))].sort();
|
|
1260
|
+
const gone = new Set(dropped);
|
|
1261
|
+
const keep = (records) => records.filter((r) => !gone.has(r.case));
|
|
1262
|
+
const kept = jobs.map((j) => ({
|
|
1263
|
+
job: j.job,
|
|
1264
|
+
candidate: keep(j.candidate),
|
|
1265
|
+
aa: keep(j.aa)
|
|
1266
|
+
}));
|
|
1267
|
+
uniqueRounds(kept, "candidate", caller);
|
|
1268
|
+
uniqueRounds(kept, "aa", caller);
|
|
1269
|
+
const candidate = balanceOrders(kept.flatMap((j) => j.candidate));
|
|
1270
|
+
const aa = balanceOrders(kept.flatMap((j) => j.aa));
|
|
1271
|
+
const each = kept.map(({ job, candidate: c, aa: a }) => ({
|
|
1272
|
+
job,
|
|
1273
|
+
candidate: balanceOrders(c).records,
|
|
1274
|
+
aa: balanceOrders(a).records
|
|
1275
|
+
}));
|
|
1276
|
+
return {
|
|
1277
|
+
candidate: candidate.records,
|
|
1278
|
+
aa: aa.records,
|
|
1279
|
+
jobs: each,
|
|
1280
|
+
trimmed: candidate.trimmed + aa.trimmed,
|
|
1281
|
+
dropped
|
|
1282
|
+
};
|
|
1283
|
+
}
|
|
1284
|
+
/** This function refuses two jobs with a record of one case and one round in one pair. */
|
|
1285
|
+
function uniqueRounds(jobs, pair, caller) {
|
|
1286
|
+
const seen = /* @__PURE__ */ new Map();
|
|
1287
|
+
for (const { job, [pair]: records } of jobs) for (const r of records) {
|
|
1288
|
+
const key = `${r.case}\u0000${r.round}`;
|
|
1289
|
+
const other = seen.get(key);
|
|
1290
|
+
if (other !== void 0 && other !== job) throw new RangeError(`${caller}: the jobs ${JSON.stringify(other)} and ${JSON.stringify(job)} both have the round ${r.round} of the case ${JSON.stringify(r.case)} in the ${pair} pair. Set roundOffset of each job to job * rounds.`);
|
|
1291
|
+
seen.set(key, job);
|
|
1292
|
+
}
|
|
1293
|
+
}
|
|
1294
|
+
/**
|
|
1295
|
+
* This function applies the guard of the jobs to a pooled verdict. A job whose outcome is `opposite` of the pooled outcome
|
|
1296
|
+
* makes the outcome `cannot decide`. Each other job does not change the outcome.
|
|
1297
|
+
*/
|
|
1298
|
+
function guard(pooled, jobs, opposite) {
|
|
1299
|
+
const against = opposite[pooled.outcome];
|
|
1300
|
+
const opposed = against === void 0 ? [] : jobs.filter((j) => j.verdict.outcome === against).map((j) => j.job);
|
|
1301
|
+
if (opposed.length === 0) return {
|
|
1302
|
+
outcome: pooled.outcome,
|
|
1303
|
+
opposed,
|
|
1304
|
+
reason: pooled.reason
|
|
1305
|
+
};
|
|
1306
|
+
const names = opposed.map((j) => JSON.stringify(j)).join(", ");
|
|
1307
|
+
return {
|
|
1308
|
+
outcome: "cannot decide",
|
|
1309
|
+
opposed,
|
|
1310
|
+
reason: `${pooled.reason} The pooled outcome is "${pooled.outcome}", but the jobs ${names} give "${against}". Thus the outcome is "cannot decide".`
|
|
1311
|
+
};
|
|
1312
|
+
}
|
|
1313
|
+
const opposite = {
|
|
1314
|
+
faster: "slower",
|
|
1315
|
+
slower: "faster"
|
|
1316
|
+
};
|
|
1317
|
+
/**
|
|
1318
|
+
* This function gives the time verdict over the records of all jobs.
|
|
1319
|
+
* - It makes the pool of the jobs with `poolRecords`, and it gives the time verdict of the merged records.
|
|
1320
|
+
* - The verdict of each job alone uses the same options. A job with `cannot decide` or `same` does not change the outcome.
|
|
1321
|
+
* - A job that decides the opposite direction of the pooled outcome makes the outcome `cannot decide`.
|
|
1322
|
+
*/
|
|
1323
|
+
function pooledVerdict(jobs, options = {}) {
|
|
1324
|
+
const { candidate, aa, jobs: each, trimmed, dropped } = pool(jobs, "pooledVerdict");
|
|
1325
|
+
const pooled = timeVerdict(candidate, aa, options);
|
|
1326
|
+
const verdicts = each.map(({ job, candidate: c, aa: a }) => ({
|
|
1327
|
+
job,
|
|
1328
|
+
verdict: timeVerdict(c, a, options)
|
|
1329
|
+
}));
|
|
1330
|
+
const { outcome, opposed, reason } = guard(pooled, verdicts, opposite);
|
|
1331
|
+
return {
|
|
1332
|
+
outcome,
|
|
1333
|
+
pooled,
|
|
1334
|
+
jobs: verdicts,
|
|
1335
|
+
opposed,
|
|
1336
|
+
trimmed,
|
|
1337
|
+
dropped,
|
|
1338
|
+
reason
|
|
1339
|
+
};
|
|
1340
|
+
}
|
|
1341
|
+
const limitOpposite = {
|
|
1342
|
+
within: "beyond",
|
|
1343
|
+
beyond: "within"
|
|
1344
|
+
};
|
|
1345
|
+
/**
|
|
1346
|
+
* This function gives the limit verdict over the records of all jobs, for example for a speed gate on limits over a CI matrix.
|
|
1347
|
+
* - It makes the pool of the jobs with `poolRecords`, and it gives the limit verdict of the merged records.
|
|
1348
|
+
* - The verdict of each job alone uses the same options. A job with `cannot decide` does not change the outcome.
|
|
1349
|
+
* - A job with `beyond` against a pooled `within`, or the reverse, makes the outcome `cannot decide`.
|
|
1350
|
+
*/
|
|
1351
|
+
function pooledLimitVerdict(jobs, options) {
|
|
1352
|
+
const { candidate, aa, jobs: each, trimmed, dropped } = pool(jobs, "pooledLimitVerdict");
|
|
1353
|
+
const pooled = limitVerdict(candidate, aa, options);
|
|
1354
|
+
const verdicts = each.map(({ job, candidate: c, aa: a }) => ({
|
|
1355
|
+
job,
|
|
1356
|
+
verdict: limitVerdict(c, a, options)
|
|
1357
|
+
}));
|
|
1358
|
+
const { outcome, opposed, reason } = guard(pooled, verdicts, limitOpposite);
|
|
1359
|
+
return {
|
|
1360
|
+
outcome,
|
|
1361
|
+
pooled,
|
|
1362
|
+
jobs: verdicts,
|
|
1363
|
+
opposed,
|
|
1364
|
+
trimmed,
|
|
1365
|
+
dropped,
|
|
1366
|
+
reason
|
|
1367
|
+
};
|
|
1368
|
+
}
|
|
1369
|
+
/**
|
|
1370
|
+
* This function gives the verdict of each candidate pair of a run against its A/A pair `aaPair`.
|
|
1371
|
+
* It balances the records of each pair with `balanceOrders` first. It refuses an `aaPair` that the run does not have.
|
|
1372
|
+
*/
|
|
1373
|
+
function armVerdicts(run, aaPair, options = {}) {
|
|
1374
|
+
const aaRecords = run.pairs[aaPair];
|
|
1375
|
+
if (aaRecords === void 0) throw new RangeError(`armVerdicts: the run has no pair ${JSON.stringify(aaPair)}.`);
|
|
1376
|
+
const aa = balanceOrders(aaRecords).records;
|
|
1377
|
+
const out = {};
|
|
1378
|
+
for (const [pair, records] of Object.entries(run.pairs)) if (pair !== aaPair) out[pair] = timeVerdict(balanceOrders(records).records, aa, options);
|
|
1379
|
+
return out;
|
|
1380
|
+
}
|
|
1381
|
+
// src/family.ts
|
|
1382
|
+
/**
|
|
1383
|
+
* The read of a family of cases. A run reads many cases, and each case has its own interval. Without a correction, the
|
|
1384
|
+
* chance of one false `slower` case or more grows with the count of cases. At 16 cases and 0.025 for each side, this
|
|
1385
|
+
* chance can be 1 - 0.975^16, approximately 0.33. These reads are reports, and they do not change a verdict.
|
|
1386
|
+
*
|
|
1387
|
+
* - The Holm read (Holm, 1979) keeps the chance of one false claim or more on one side at most `level`.
|
|
1388
|
+
* - The read of each job keeps a `slower` or `faster` case only when each job gives that outcome alone.
|
|
1389
|
+
* - The fourth read keeps a case only when the Holm read and the read of each job both keep it.
|
|
1390
|
+
*/
|
|
1391
|
+
const pooledOf = (verdict) => "pooled" in verdict ? verdict : void 0;
|
|
1392
|
+
/** This function changes `slower` and `faster` to `cannot decide` when `keep` refuses the side. */
|
|
1393
|
+
function kept(outcome, keep) {
|
|
1394
|
+
return (outcome === "slower" || outcome === "faster") && !keep(outcome) ? "cannot decide" : outcome;
|
|
1395
|
+
}
|
|
1396
|
+
/** This function gives the Holm read of one side, by the index of the case. A case with no p gets undefined. */
|
|
1397
|
+
function holmSide(ps, level) {
|
|
1398
|
+
const index = ps.flatMap((p, i) => p === void 0 ? [] : [i]);
|
|
1399
|
+
const corrected = holm(index.map((i) => ps[i]), level);
|
|
1400
|
+
const out = new Array(ps.length).fill(void 0);
|
|
1401
|
+
index.forEach((i, k) => out[i] = corrected[k].adjusted);
|
|
1402
|
+
return out;
|
|
1403
|
+
}
|
|
1404
|
+
const count = (outcomes) => ({
|
|
1405
|
+
slower: outcomes.filter((o) => o === "slower").length,
|
|
1406
|
+
faster: outcomes.filter((o) => o === "faster").length
|
|
1407
|
+
});
|
|
1408
|
+
/**
|
|
1409
|
+
* This function gives the Holm read and the read of each job over a family of cases, as a report. The default `level` is 0.025 for each side.
|
|
1410
|
+
* It reads `pSlower` and `pFaster` of each time verdict, and of the pooled verdict of a pooled case. The option `pValues` of a verdict gives them.
|
|
1411
|
+
* A case with no p keeps its outcome in the Holm read, and a line gives the count of such cases.
|
|
1412
|
+
*/
|
|
1413
|
+
function familyRead(items, level = .025) {
|
|
1414
|
+
if (!(level > 0 && level < 1)) throw new RangeError(`familyRead: the level is more than 0 and less than 1, not ${level}.`);
|
|
1415
|
+
const times = items.map((item) => pooledOf(item.verdict)?.pooled ?? item.verdict);
|
|
1416
|
+
const slower = holmSide(times.map((t) => t.pSlower), level);
|
|
1417
|
+
const faster = holmSide(times.map((t) => t.pFaster), level);
|
|
1418
|
+
const cases = items.map((item, i) => {
|
|
1419
|
+
const outcome = item.verdict.outcome;
|
|
1420
|
+
const adjusted = {
|
|
1421
|
+
slower: slower[i],
|
|
1422
|
+
faster: faster[i]
|
|
1423
|
+
};
|
|
1424
|
+
const pooled = pooledOf(item.verdict);
|
|
1425
|
+
const byHolm = kept(outcome, (side) => adjusted[side] === void 0 || adjusted[side] < level);
|
|
1426
|
+
const byJobs = pooled === void 0 ? outcome : kept(outcome, (side) => pooled.jobs.every((j) => j.verdict.outcome === side));
|
|
1427
|
+
return {
|
|
1428
|
+
case: item.case,
|
|
1429
|
+
outcome,
|
|
1430
|
+
holm: byHolm,
|
|
1431
|
+
eachJob: byJobs,
|
|
1432
|
+
eachJobHolm: byHolm === outcome ? byJobs : byHolm,
|
|
1433
|
+
adjustedSlower: adjusted.slower,
|
|
1434
|
+
adjustedFaster: adjusted.faster
|
|
1435
|
+
};
|
|
1436
|
+
});
|
|
1437
|
+
const finite = times.filter((t) => t.pSlower !== void 0).map((t) => t.interval?.finite ?? 0);
|
|
1438
|
+
const floor = finite.length === 0 ? void 0 : 1 / (Math.min(...finite) + 1);
|
|
1439
|
+
const firstStep = finite.length === 0 ? void 0 : level / finite.length;
|
|
1440
|
+
const plain = count(cases.map((c) => c.outcome));
|
|
1441
|
+
const corrected = count(cases.map((c) => c.holm));
|
|
1442
|
+
const eachJob = count(cases.map((c) => c.eachJob));
|
|
1443
|
+
const eachJobHolm = count(cases.map((c) => c.eachJobHolm));
|
|
1444
|
+
const counts = (side) => `${String(plain[side])}, with Holm ${String(corrected[side])}, with each job ${String(eachJob[side])}, with each job and Holm ${String(eachJobHolm[side])}`;
|
|
1445
|
+
const lines = [`Family read (report only): ${String(cases.length)} cases. Slower: ${counts("slower")}. Faster: ${counts("faster")}.`];
|
|
1446
|
+
const bare = times.filter((t) => t.pSlower === void 0).length;
|
|
1447
|
+
if (bare > 0) lines.push(`${String(bare)} of ${String(times.length)} cases have no p, so the Holm read keeps their outcome. Give the verdicts the option pValues.`);
|
|
1448
|
+
if (floor !== void 0 && firstStep !== void 0) lines.push(floor < firstStep ? `The smallest p of the draws is ${round$1(floor)}, below the first step ${round$1(firstStep)} of Holm at the level ${String(level)}.` : `The smallest p of the draws is ${round$1(floor)}, not below the first step ${round$1(firstStep)} of Holm. No case can read slower with Holm, so give the bootstrap more runs.`);
|
|
1449
|
+
for (const c of cases) {
|
|
1450
|
+
if (c.holm !== c.outcome) lines.push(`The case ${JSON.stringify(c.case)} reads ${c.outcome}, and with Holm it reads ${c.holm}: the adjusted p is ${round$1(c.outcome === "slower" ? c.adjustedSlower : c.adjustedFaster)}.`);
|
|
1451
|
+
if (c.eachJob !== c.outcome) lines.push(`The case ${JSON.stringify(c.case)} reads ${c.outcome}, and a job alone does not read ${c.outcome}.`);
|
|
1452
|
+
}
|
|
1453
|
+
return {
|
|
1454
|
+
level,
|
|
1455
|
+
cases,
|
|
1456
|
+
plain,
|
|
1457
|
+
holm: corrected,
|
|
1458
|
+
eachJob,
|
|
1459
|
+
eachJobHolm,
|
|
1460
|
+
floor,
|
|
1461
|
+
firstStep,
|
|
1462
|
+
lines
|
|
1463
|
+
};
|
|
1464
|
+
}
|
|
1465
|
+
/**
|
|
1466
|
+
* This function draws `trials` families of pooled verdicts and gives the share of each read. Each arm of each round adds a
|
|
1467
|
+
* normal noise of `sigma` to the log time. Without `jobShift`, the jobs have no shift between them, so the read of each job has its best case.
|
|
1468
|
+
*/
|
|
1469
|
+
function familyPower(options) {
|
|
1470
|
+
const { family } = options;
|
|
1471
|
+
const ratio = options.ratio ?? 1;
|
|
1472
|
+
const rounds = options.rounds ?? 5;
|
|
1473
|
+
const jobs = options.jobs ?? 2;
|
|
1474
|
+
const sigma = options.sigma ?? .1;
|
|
1475
|
+
const trials = options.trials ?? 200;
|
|
1476
|
+
const jobShift = options.jobShift ?? 0;
|
|
1477
|
+
for (const [name, n] of [
|
|
1478
|
+
["family", family],
|
|
1479
|
+
["rounds", rounds],
|
|
1480
|
+
["jobs", jobs],
|
|
1481
|
+
["trials", trials]
|
|
1482
|
+
]) if (!Number.isSafeInteger(n) || n < 1) throw new RangeError(`familyPower: ${name} is a whole number of 1 or more, not ${n}.`);
|
|
1483
|
+
if (!(ratio > 0 && Number.isFinite(ratio))) throw new RangeError(`familyPower: the ratio is a finite number above 0, not ${ratio}.`);
|
|
1484
|
+
if (!(jobShift >= 0 && Number.isFinite(jobShift))) throw new RangeError(`familyPower: jobShift is a finite number of 0 or more, not ${jobShift}.`);
|
|
1485
|
+
const r = random(options.seed ?? 1);
|
|
1486
|
+
const draw = (truth, job, c) => Array.from({ length: rounds }, (_, round) => ({
|
|
1487
|
+
case: `${job}:${String(round)}`,
|
|
1488
|
+
round: 0,
|
|
1489
|
+
first: firstArm(round, c),
|
|
1490
|
+
a: 10 * Math.exp(r.normal(0, sigma)),
|
|
1491
|
+
b: 10 * truth * Math.exp(r.normal(0, sigma))
|
|
1492
|
+
}));
|
|
1493
|
+
const hits = {
|
|
1494
|
+
false: {
|
|
1495
|
+
plain: 0,
|
|
1496
|
+
holm: 0,
|
|
1497
|
+
eachJob: 0,
|
|
1498
|
+
eachJobHolm: 0
|
|
1499
|
+
},
|
|
1500
|
+
first: {
|
|
1501
|
+
plain: 0,
|
|
1502
|
+
holm: 0,
|
|
1503
|
+
eachJob: 0,
|
|
1504
|
+
eachJobHolm: 0
|
|
1505
|
+
}
|
|
1506
|
+
};
|
|
1507
|
+
for (let t = 0; t < trials; t++) {
|
|
1508
|
+
const read = familyRead(Array.from({ length: family }, (_, c) => {
|
|
1509
|
+
const records = Array.from({ length: jobs }, (_, j) => ({
|
|
1510
|
+
job: `job ${String(j)}`,
|
|
1511
|
+
candidate: draw((c === 0 ? ratio : 1) * (jobShift > 0 ? Math.exp(r.normal(0, jobShift)) : 1), String(j), c),
|
|
1512
|
+
aa: draw(1, String(j), c)
|
|
1513
|
+
}));
|
|
1514
|
+
return {
|
|
1515
|
+
case: `case ${String(c)}`,
|
|
1516
|
+
verdict: pooledVerdict(records, {
|
|
1517
|
+
...options.verdict,
|
|
1518
|
+
pValues: true
|
|
1519
|
+
})
|
|
1520
|
+
};
|
|
1521
|
+
}), options.level);
|
|
1522
|
+
for (const key of [
|
|
1523
|
+
"plain",
|
|
1524
|
+
"holm",
|
|
1525
|
+
"eachJob",
|
|
1526
|
+
"eachJobHolm"
|
|
1527
|
+
]) {
|
|
1528
|
+
const outcome = (c) => key === "plain" ? c.outcome : c[key];
|
|
1529
|
+
if (read.cases.some((c, i) => (i > 0 || ratio === 1) && outcome(c) === "slower")) hits.false[key]++;
|
|
1530
|
+
if (outcome(read.cases[0]) === "slower") hits.first[key]++;
|
|
1531
|
+
}
|
|
1532
|
+
}
|
|
1533
|
+
const share = (h) => ({
|
|
1534
|
+
plain: h.plain / trials,
|
|
1535
|
+
holm: h.holm / trials,
|
|
1536
|
+
eachJob: h.eachJob / trials,
|
|
1537
|
+
eachJobHolm: h.eachJobHolm / trials
|
|
1538
|
+
});
|
|
1539
|
+
return {
|
|
1540
|
+
trials,
|
|
1541
|
+
falseSlower: share(hits.false),
|
|
1542
|
+
firstSlower: share(hits.first)
|
|
1543
|
+
};
|
|
1544
|
+
}
|
|
1545
|
+
function round$1(x) {
|
|
1546
|
+
return Math.round(x * 1e5) / 1e5;
|
|
1547
|
+
}
|
|
1131
1548
|
// src/model.ts
|
|
1132
1549
|
/**
|
|
1133
1550
|
* The CPU model test. A limit from reads of many runners can hide a model that reads apart. A check of the pool
|
|
@@ -1525,177 +1942,6 @@ function tailFalseFails(aa, options) {
|
|
|
1525
1942
|
function round(x) {
|
|
1526
1943
|
return Math.round(x * 1e4) / 1e4;
|
|
1527
1944
|
}
|
|
1528
|
-
// src/merge.ts
|
|
1529
|
-
/**
|
|
1530
|
-
* The merge of the records of many jobs, and the pooled verdicts over them. Each job of a bench gives its own records.
|
|
1531
|
-
* A pooled verdict uses all jobs together, and the verdict of each job alone guards it.
|
|
1532
|
-
*/
|
|
1533
|
-
/**
|
|
1534
|
-
* This function removes records until the two orders of each case differ in count by 1 or less.
|
|
1535
|
-
* It removes the records of the larger order with the highest rounds first. The other records keep their order.
|
|
1536
|
-
* A job that stopped early, or a missing job, can leave a case with 2 more rounds of one order. `caseRatios` refuses such a case.
|
|
1537
|
-
*/
|
|
1538
|
-
function balanceOrders(records) {
|
|
1539
|
-
const byCase = /* @__PURE__ */ new Map();
|
|
1540
|
-
records.forEach((record, index) => {
|
|
1541
|
-
const orders = byCase.get(record.case) ?? {
|
|
1542
|
-
a: [],
|
|
1543
|
-
b: []
|
|
1544
|
-
};
|
|
1545
|
-
orders[record.first === "b" ? "b" : "a"].push(index);
|
|
1546
|
-
byCase.set(record.case, orders);
|
|
1547
|
-
});
|
|
1548
|
-
const drop = /* @__PURE__ */ new Set();
|
|
1549
|
-
const highFirst = (x, y) => records[y].round - records[x].round;
|
|
1550
|
-
for (const { a, b } of byCase.values()) {
|
|
1551
|
-
const [more, fewer] = a.length > b.length ? [a, b] : [b, a];
|
|
1552
|
-
const extra = more.length - fewer.length - 1;
|
|
1553
|
-
if (extra > 0) for (const index of more.toSorted(highFirst).slice(0, extra)) drop.add(index);
|
|
1554
|
-
}
|
|
1555
|
-
return {
|
|
1556
|
-
records: records.filter((_, index) => !drop.has(index)),
|
|
1557
|
-
trimmed: drop.size
|
|
1558
|
-
};
|
|
1559
|
-
}
|
|
1560
|
-
/**
|
|
1561
|
-
* This function gives the pool of the jobs. `pooledVerdict` and `pooledLimitVerdict` use it.
|
|
1562
|
-
* - It refuses two jobs with one label, and a record with a different job label. It also refuses two jobs with a record of one case and one round.
|
|
1563
|
-
* - It removes each case that a job skipped from all jobs, so all jobs have the same cases.
|
|
1564
|
-
* - It merges the records of the jobs, and it balances the merged records and the records of each job with `balanceOrders`.
|
|
1565
|
-
*/
|
|
1566
|
-
function poolRecords(jobs) {
|
|
1567
|
-
return pool(jobs, "poolRecords");
|
|
1568
|
-
}
|
|
1569
|
-
/** The pool of `poolRecords`. Each error starts with the name `caller`. */
|
|
1570
|
-
function pool(jobs, caller) {
|
|
1571
|
-
const labels = /* @__PURE__ */ new Set();
|
|
1572
|
-
for (const { job } of jobs) {
|
|
1573
|
-
if (labels.has(job)) throw new RangeError(`${caller}: two jobs have the label ${JSON.stringify(job)}.`);
|
|
1574
|
-
labels.add(job);
|
|
1575
|
-
}
|
|
1576
|
-
for (const { job, candidate: c, aa: a } of jobs) for (const r of [...c, ...a]) if (r.job !== void 0 && r.job !== job) throw new RangeError(`${caller}: a record of the job ${JSON.stringify(job)} has the job ${JSON.stringify(r.job)}.`);
|
|
1577
|
-
const dropped = [...new Set(jobs.flatMap((j) => (j.skipped ?? []).map((s) => s.case)))].sort();
|
|
1578
|
-
const gone = new Set(dropped);
|
|
1579
|
-
const keep = (records) => records.filter((r) => !gone.has(r.case));
|
|
1580
|
-
const kept = jobs.map((j) => ({
|
|
1581
|
-
job: j.job,
|
|
1582
|
-
candidate: keep(j.candidate),
|
|
1583
|
-
aa: keep(j.aa)
|
|
1584
|
-
}));
|
|
1585
|
-
uniqueRounds(kept, "candidate", caller);
|
|
1586
|
-
uniqueRounds(kept, "aa", caller);
|
|
1587
|
-
const candidate = balanceOrders(kept.flatMap((j) => j.candidate));
|
|
1588
|
-
const aa = balanceOrders(kept.flatMap((j) => j.aa));
|
|
1589
|
-
const each = kept.map(({ job, candidate: c, aa: a }) => ({
|
|
1590
|
-
job,
|
|
1591
|
-
candidate: balanceOrders(c).records,
|
|
1592
|
-
aa: balanceOrders(a).records
|
|
1593
|
-
}));
|
|
1594
|
-
return {
|
|
1595
|
-
candidate: candidate.records,
|
|
1596
|
-
aa: aa.records,
|
|
1597
|
-
jobs: each,
|
|
1598
|
-
trimmed: candidate.trimmed + aa.trimmed,
|
|
1599
|
-
dropped
|
|
1600
|
-
};
|
|
1601
|
-
}
|
|
1602
|
-
/** This function refuses two jobs with a record of one case and one round in one pair. */
|
|
1603
|
-
function uniqueRounds(jobs, pair, caller) {
|
|
1604
|
-
const seen = /* @__PURE__ */ new Map();
|
|
1605
|
-
for (const { job, [pair]: records } of jobs) for (const r of records) {
|
|
1606
|
-
const key = `${r.case}\u0000${r.round}`;
|
|
1607
|
-
const other = seen.get(key);
|
|
1608
|
-
if (other !== void 0 && other !== job) throw new RangeError(`${caller}: the jobs ${JSON.stringify(other)} and ${JSON.stringify(job)} both have the round ${r.round} of the case ${JSON.stringify(r.case)} in the ${pair} pair. Set roundOffset of each job to job * rounds.`);
|
|
1609
|
-
seen.set(key, job);
|
|
1610
|
-
}
|
|
1611
|
-
}
|
|
1612
|
-
/**
|
|
1613
|
-
* This function applies the guard of the jobs to a pooled verdict. A job whose outcome is `opposite` of the pooled outcome
|
|
1614
|
-
* makes the outcome `cannot decide`. Each other job does not change the outcome.
|
|
1615
|
-
*/
|
|
1616
|
-
function guard(pooled, jobs, opposite) {
|
|
1617
|
-
const against = opposite[pooled.outcome];
|
|
1618
|
-
const opposed = against === void 0 ? [] : jobs.filter((j) => j.verdict.outcome === against).map((j) => j.job);
|
|
1619
|
-
if (opposed.length === 0) return {
|
|
1620
|
-
outcome: pooled.outcome,
|
|
1621
|
-
opposed,
|
|
1622
|
-
reason: pooled.reason
|
|
1623
|
-
};
|
|
1624
|
-
const names = opposed.map((j) => JSON.stringify(j)).join(", ");
|
|
1625
|
-
return {
|
|
1626
|
-
outcome: "cannot decide",
|
|
1627
|
-
opposed,
|
|
1628
|
-
reason: `${pooled.reason} The pooled outcome is "${pooled.outcome}", but the jobs ${names} give "${against}". Thus the outcome is "cannot decide".`
|
|
1629
|
-
};
|
|
1630
|
-
}
|
|
1631
|
-
const opposite = {
|
|
1632
|
-
faster: "slower",
|
|
1633
|
-
slower: "faster"
|
|
1634
|
-
};
|
|
1635
|
-
/**
|
|
1636
|
-
* This function gives the time verdict over the records of all jobs.
|
|
1637
|
-
* - It makes the pool of the jobs with `poolRecords`, and it gives the time verdict of the merged records.
|
|
1638
|
-
* - The verdict of each job alone uses the same options. A job with `cannot decide` or `same` does not change the outcome.
|
|
1639
|
-
* - A job that decides the opposite direction of the pooled outcome makes the outcome `cannot decide`.
|
|
1640
|
-
*/
|
|
1641
|
-
function pooledVerdict(jobs, options = {}) {
|
|
1642
|
-
const { candidate, aa, jobs: each, trimmed, dropped } = pool(jobs, "pooledVerdict");
|
|
1643
|
-
const pooled = timeVerdict(candidate, aa, options);
|
|
1644
|
-
const verdicts = each.map(({ job, candidate: c, aa: a }) => ({
|
|
1645
|
-
job,
|
|
1646
|
-
verdict: timeVerdict(c, a, options)
|
|
1647
|
-
}));
|
|
1648
|
-
const { outcome, opposed, reason } = guard(pooled, verdicts, opposite);
|
|
1649
|
-
return {
|
|
1650
|
-
outcome,
|
|
1651
|
-
pooled,
|
|
1652
|
-
jobs: verdicts,
|
|
1653
|
-
opposed,
|
|
1654
|
-
trimmed,
|
|
1655
|
-
dropped,
|
|
1656
|
-
reason
|
|
1657
|
-
};
|
|
1658
|
-
}
|
|
1659
|
-
const limitOpposite = {
|
|
1660
|
-
within: "beyond",
|
|
1661
|
-
beyond: "within"
|
|
1662
|
-
};
|
|
1663
|
-
/**
|
|
1664
|
-
* This function gives the limit verdict over the records of all jobs, for example for a speed gate on limits over a CI matrix.
|
|
1665
|
-
* - It makes the pool of the jobs with `poolRecords`, and it gives the limit verdict of the merged records.
|
|
1666
|
-
* - The verdict of each job alone uses the same options. A job with `cannot decide` does not change the outcome.
|
|
1667
|
-
* - A job with `beyond` against a pooled `within`, or the reverse, makes the outcome `cannot decide`.
|
|
1668
|
-
*/
|
|
1669
|
-
function pooledLimitVerdict(jobs, options) {
|
|
1670
|
-
const { candidate, aa, jobs: each, trimmed, dropped } = pool(jobs, "pooledLimitVerdict");
|
|
1671
|
-
const pooled = limitVerdict(candidate, aa, options);
|
|
1672
|
-
const verdicts = each.map(({ job, candidate: c, aa: a }) => ({
|
|
1673
|
-
job,
|
|
1674
|
-
verdict: limitVerdict(c, a, options)
|
|
1675
|
-
}));
|
|
1676
|
-
const { outcome, opposed, reason } = guard(pooled, verdicts, limitOpposite);
|
|
1677
|
-
return {
|
|
1678
|
-
outcome,
|
|
1679
|
-
pooled,
|
|
1680
|
-
jobs: verdicts,
|
|
1681
|
-
opposed,
|
|
1682
|
-
trimmed,
|
|
1683
|
-
dropped,
|
|
1684
|
-
reason
|
|
1685
|
-
};
|
|
1686
|
-
}
|
|
1687
|
-
/**
|
|
1688
|
-
* This function gives the verdict of each candidate pair of a run against its A/A pair `aaPair`.
|
|
1689
|
-
* It balances the records of each pair with `balanceOrders` first. It refuses an `aaPair` that the run does not have.
|
|
1690
|
-
*/
|
|
1691
|
-
function armVerdicts(run, aaPair, options = {}) {
|
|
1692
|
-
const aaRecords = run.pairs[aaPair];
|
|
1693
|
-
if (aaRecords === void 0) throw new RangeError(`armVerdicts: the run has no pair ${JSON.stringify(aaPair)}.`);
|
|
1694
|
-
const aa = balanceOrders(aaRecords).records;
|
|
1695
|
-
const out = {};
|
|
1696
|
-
for (const [pair, records] of Object.entries(run.pairs)) if (pair !== aaPair) out[pair] = timeVerdict(balanceOrders(records).records, aa, options);
|
|
1697
|
-
return out;
|
|
1698
|
-
}
|
|
1699
1945
|
// src/measures.ts
|
|
1700
1946
|
/**
|
|
1701
1947
|
* The check that a trick does not buy a measure. A control is a strategy made to win one measure and nothing else.
|
|
@@ -2478,6 +2724,6 @@ function timeRounds(arms, options) {
|
|
|
2478
2724
|
sink
|
|
2479
2725
|
};
|
|
2480
2726
|
}
|
|
2481
|
-
export { BUDGET_LOOKS, CEILING_ONLY, GATE_LOOKS, LADDER_FORMAT, NARROW_SPREAD_SHARE, UNKNOWN_MODEL, aaBand, aaSpread, armModels, armOrder, armVerdicts, balanceOrders, budgetLookSpend, budgetLooksVerdict, budgetVerdict, caseIntervals, caseRatios, caseTails, checkLadder, checkMeasures, checkPairedTime, controlFactor, firstArm, gateOf, jointClaim, ladderVerdict, limitOnModel, limitVerdict, modelRowText, modelTest, notSlowerVerdict, orderSize, poolRecords, pooledBand, pooledLimitVerdict, pooledVerdict, resultRows, resultVerdict, rungVerdict, tailFalseFails, tailLimitVerdict, timeRounds, timeVerdict, verdictMargin, verdictPower, visibleShift, winClaim };
|
|
2727
|
+
export { BUDGET_LOOKS, CEILING_ONLY, GATE_LOOKS, LADDER_FORMAT, NARROW_SPREAD_SHARE, UNKNOWN_MODEL, aaBand, aaSpread, armModels, armOrder, armVerdicts, balanceOrders, budgetLookSpend, budgetLooksVerdict, budgetVerdict, caseIntervals, caseRatios, caseTails, checkLadder, checkMeasures, checkPairedTime, controlFactor, familyPower, familyRead, firstArm, gateOf, growthVerdict, jointClaim, ladderVerdict, limitOnModel, limitVerdict, modelRowText, modelTest, notSlowerVerdict, orderSize, poolRecords, pooledBand, pooledLimitVerdict, pooledVerdict, resultRows, resultVerdict, rungVerdict, tailFalseFails, tailLimitVerdict, timeRounds, timeVerdict, verdictMargin, verdictPower, visibleShift, winClaim };
|
|
2482
2728
|
|
|
2483
2729
|
//# sourceMappingURL=index.js.map
|