@mark1russell7/rigor-bench 0.2.25 → 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 +49 -0
- package/build/index.d.ts +108 -1
- package/build/index.d.ts.map +1 -1
- package/build/index.js +369 -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,7 +209,7 @@ 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
215
|
// src/growth.ts
|
|
@@ -935,14 +935,14 @@ function timeVerdict(candidate, aa, options = {}) {
|
|
|
935
935
|
noise
|
|
936
936
|
};
|
|
937
937
|
if (noise.noisy && verdict.band !== void 0) {
|
|
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$
|
|
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}".`;
|
|
939
939
|
verdict = {
|
|
940
940
|
...verdict,
|
|
941
941
|
outcome: "cannot decide",
|
|
942
942
|
reason
|
|
943
943
|
};
|
|
944
944
|
} else if (noise.biased && verdict.band !== void 0) {
|
|
945
|
-
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}".`;
|
|
946
946
|
verdict = {
|
|
947
947
|
...verdict,
|
|
948
948
|
outcome: "cannot decide",
|
|
@@ -1010,44 +1010,59 @@ function ruleVerdict(candidate, aa, options, pooled) {
|
|
|
1010
1010
|
};
|
|
1011
1011
|
if (rule === "difference") {
|
|
1012
1012
|
const margin = marginOf(options, pooled);
|
|
1013
|
-
const shown = pooled === void 0 ? margin : round$
|
|
1013
|
+
const shown = pooled === void 0 ? margin : round$3(margin);
|
|
1014
1014
|
const logs = cases.map((c) => Math.log(c.ratio));
|
|
1015
1015
|
const aaLogs = aaRatios.map((c) => Math.log(c.ratio));
|
|
1016
|
-
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;
|
|
1017
1021
|
const interval = {
|
|
1018
|
-
...d,
|
|
1019
1022
|
estimate: Math.exp(d.estimate),
|
|
1020
1023
|
low: Math.exp(d.low),
|
|
1021
|
-
high: Math.exp(d.high)
|
|
1024
|
+
high: Math.exp(d.high),
|
|
1025
|
+
level,
|
|
1026
|
+
runs,
|
|
1027
|
+
finite,
|
|
1028
|
+
seed
|
|
1022
1029
|
};
|
|
1023
|
-
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)}].`;
|
|
1024
1035
|
if (d.low >= -margin && d.high <= margin) return {
|
|
1025
1036
|
...facts,
|
|
1037
|
+
...sides,
|
|
1026
1038
|
interval,
|
|
1027
1039
|
outcome: "same",
|
|
1028
1040
|
reason: `${span} The interval is in the margin of ${shown}.`
|
|
1029
1041
|
};
|
|
1030
1042
|
if (interval.high < 1) return {
|
|
1031
1043
|
...facts,
|
|
1044
|
+
...sides,
|
|
1032
1045
|
interval,
|
|
1033
1046
|
outcome: "faster",
|
|
1034
1047
|
reason: `${span} The interval is below 1.`
|
|
1035
1048
|
};
|
|
1036
1049
|
if (interval.low > 1) return {
|
|
1037
1050
|
...facts,
|
|
1051
|
+
...sides,
|
|
1038
1052
|
interval,
|
|
1039
1053
|
outcome: "slower",
|
|
1040
1054
|
reason: `${span} The interval is above 1.`
|
|
1041
1055
|
};
|
|
1042
1056
|
return {
|
|
1043
1057
|
...facts,
|
|
1058
|
+
...sides,
|
|
1044
1059
|
interval,
|
|
1045
1060
|
outcome: "cannot decide",
|
|
1046
1061
|
reason: `${span} The interval holds 1 and crosses the margin of ${shown}.`
|
|
1047
1062
|
};
|
|
1048
1063
|
}
|
|
1049
1064
|
const interval = bootstrapMedian(cases.map((c) => c.ratio), options.bootstrap);
|
|
1050
|
-
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)}].`;
|
|
1051
1066
|
if (interval.high < band.low) return {
|
|
1052
1067
|
...facts,
|
|
1053
1068
|
interval,
|
|
@@ -1073,7 +1088,7 @@ function ruleVerdict(candidate, aa, options, pooled) {
|
|
|
1073
1088
|
reason: `${span} The interval crosses an end of the band.`
|
|
1074
1089
|
};
|
|
1075
1090
|
}
|
|
1076
|
-
function round$
|
|
1091
|
+
function round$3(x) {
|
|
1077
1092
|
return Math.round(x * 1e4) / 1e4;
|
|
1078
1093
|
}
|
|
1079
1094
|
// src/limit.ts
|
|
@@ -1178,20 +1193,358 @@ function limitVerdict(candidate, aa, options) {
|
|
|
1178
1193
|
const rest = others.length === 0 ? "" : ` ${others.length} of ${cases.length} cases are not within${permit}.`;
|
|
1179
1194
|
return {
|
|
1180
1195
|
outcome: "within",
|
|
1181
|
-
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}`,
|
|
1182
1197
|
...facts
|
|
1183
1198
|
};
|
|
1184
1199
|
}
|
|
1185
1200
|
const top = cases.reduce((a, b) => b.bound > a.bound ? b : a);
|
|
1186
1201
|
return {
|
|
1187
1202
|
outcome: "cannot decide",
|
|
1188
|
-
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.`,
|
|
1189
1204
|
...facts
|
|
1190
1205
|
};
|
|
1191
1206
|
}
|
|
1192
|
-
function round$
|
|
1207
|
+
function round$2(x) {
|
|
1193
1208
|
return Math.round(x * 1e4) / 1e4;
|
|
1194
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
|
+
}
|
|
1195
1548
|
// src/model.ts
|
|
1196
1549
|
/**
|
|
1197
1550
|
* The CPU model test. A limit from reads of many runners can hide a model that reads apart. A check of the pool
|
|
@@ -1589,177 +1942,6 @@ function tailFalseFails(aa, options) {
|
|
|
1589
1942
|
function round(x) {
|
|
1590
1943
|
return Math.round(x * 1e4) / 1e4;
|
|
1591
1944
|
}
|
|
1592
|
-
// src/merge.ts
|
|
1593
|
-
/**
|
|
1594
|
-
* The merge of the records of many jobs, and the pooled verdicts over them. Each job of a bench gives its own records.
|
|
1595
|
-
* A pooled verdict uses all jobs together, and the verdict of each job alone guards it.
|
|
1596
|
-
*/
|
|
1597
|
-
/**
|
|
1598
|
-
* This function removes records until the two orders of each case differ in count by 1 or less.
|
|
1599
|
-
* It removes the records of the larger order with the highest rounds first. The other records keep their order.
|
|
1600
|
-
* 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.
|
|
1601
|
-
*/
|
|
1602
|
-
function balanceOrders(records) {
|
|
1603
|
-
const byCase = /* @__PURE__ */ new Map();
|
|
1604
|
-
records.forEach((record, index) => {
|
|
1605
|
-
const orders = byCase.get(record.case) ?? {
|
|
1606
|
-
a: [],
|
|
1607
|
-
b: []
|
|
1608
|
-
};
|
|
1609
|
-
orders[record.first === "b" ? "b" : "a"].push(index);
|
|
1610
|
-
byCase.set(record.case, orders);
|
|
1611
|
-
});
|
|
1612
|
-
const drop = /* @__PURE__ */ new Set();
|
|
1613
|
-
const highFirst = (x, y) => records[y].round - records[x].round;
|
|
1614
|
-
for (const { a, b } of byCase.values()) {
|
|
1615
|
-
const [more, fewer] = a.length > b.length ? [a, b] : [b, a];
|
|
1616
|
-
const extra = more.length - fewer.length - 1;
|
|
1617
|
-
if (extra > 0) for (const index of more.toSorted(highFirst).slice(0, extra)) drop.add(index);
|
|
1618
|
-
}
|
|
1619
|
-
return {
|
|
1620
|
-
records: records.filter((_, index) => !drop.has(index)),
|
|
1621
|
-
trimmed: drop.size
|
|
1622
|
-
};
|
|
1623
|
-
}
|
|
1624
|
-
/**
|
|
1625
|
-
* This function gives the pool of the jobs. `pooledVerdict` and `pooledLimitVerdict` use it.
|
|
1626
|
-
* - 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.
|
|
1627
|
-
* - It removes each case that a job skipped from all jobs, so all jobs have the same cases.
|
|
1628
|
-
* - It merges the records of the jobs, and it balances the merged records and the records of each job with `balanceOrders`.
|
|
1629
|
-
*/
|
|
1630
|
-
function poolRecords(jobs) {
|
|
1631
|
-
return pool(jobs, "poolRecords");
|
|
1632
|
-
}
|
|
1633
|
-
/** The pool of `poolRecords`. Each error starts with the name `caller`. */
|
|
1634
|
-
function pool(jobs, caller) {
|
|
1635
|
-
const labels = /* @__PURE__ */ new Set();
|
|
1636
|
-
for (const { job } of jobs) {
|
|
1637
|
-
if (labels.has(job)) throw new RangeError(`${caller}: two jobs have the label ${JSON.stringify(job)}.`);
|
|
1638
|
-
labels.add(job);
|
|
1639
|
-
}
|
|
1640
|
-
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)}.`);
|
|
1641
|
-
const dropped = [...new Set(jobs.flatMap((j) => (j.skipped ?? []).map((s) => s.case)))].sort();
|
|
1642
|
-
const gone = new Set(dropped);
|
|
1643
|
-
const keep = (records) => records.filter((r) => !gone.has(r.case));
|
|
1644
|
-
const kept = jobs.map((j) => ({
|
|
1645
|
-
job: j.job,
|
|
1646
|
-
candidate: keep(j.candidate),
|
|
1647
|
-
aa: keep(j.aa)
|
|
1648
|
-
}));
|
|
1649
|
-
uniqueRounds(kept, "candidate", caller);
|
|
1650
|
-
uniqueRounds(kept, "aa", caller);
|
|
1651
|
-
const candidate = balanceOrders(kept.flatMap((j) => j.candidate));
|
|
1652
|
-
const aa = balanceOrders(kept.flatMap((j) => j.aa));
|
|
1653
|
-
const each = kept.map(({ job, candidate: c, aa: a }) => ({
|
|
1654
|
-
job,
|
|
1655
|
-
candidate: balanceOrders(c).records,
|
|
1656
|
-
aa: balanceOrders(a).records
|
|
1657
|
-
}));
|
|
1658
|
-
return {
|
|
1659
|
-
candidate: candidate.records,
|
|
1660
|
-
aa: aa.records,
|
|
1661
|
-
jobs: each,
|
|
1662
|
-
trimmed: candidate.trimmed + aa.trimmed,
|
|
1663
|
-
dropped
|
|
1664
|
-
};
|
|
1665
|
-
}
|
|
1666
|
-
/** This function refuses two jobs with a record of one case and one round in one pair. */
|
|
1667
|
-
function uniqueRounds(jobs, pair, caller) {
|
|
1668
|
-
const seen = /* @__PURE__ */ new Map();
|
|
1669
|
-
for (const { job, [pair]: records } of jobs) for (const r of records) {
|
|
1670
|
-
const key = `${r.case}\u0000${r.round}`;
|
|
1671
|
-
const other = seen.get(key);
|
|
1672
|
-
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.`);
|
|
1673
|
-
seen.set(key, job);
|
|
1674
|
-
}
|
|
1675
|
-
}
|
|
1676
|
-
/**
|
|
1677
|
-
* This function applies the guard of the jobs to a pooled verdict. A job whose outcome is `opposite` of the pooled outcome
|
|
1678
|
-
* makes the outcome `cannot decide`. Each other job does not change the outcome.
|
|
1679
|
-
*/
|
|
1680
|
-
function guard(pooled, jobs, opposite) {
|
|
1681
|
-
const against = opposite[pooled.outcome];
|
|
1682
|
-
const opposed = against === void 0 ? [] : jobs.filter((j) => j.verdict.outcome === against).map((j) => j.job);
|
|
1683
|
-
if (opposed.length === 0) return {
|
|
1684
|
-
outcome: pooled.outcome,
|
|
1685
|
-
opposed,
|
|
1686
|
-
reason: pooled.reason
|
|
1687
|
-
};
|
|
1688
|
-
const names = opposed.map((j) => JSON.stringify(j)).join(", ");
|
|
1689
|
-
return {
|
|
1690
|
-
outcome: "cannot decide",
|
|
1691
|
-
opposed,
|
|
1692
|
-
reason: `${pooled.reason} The pooled outcome is "${pooled.outcome}", but the jobs ${names} give "${against}". Thus the outcome is "cannot decide".`
|
|
1693
|
-
};
|
|
1694
|
-
}
|
|
1695
|
-
const opposite = {
|
|
1696
|
-
faster: "slower",
|
|
1697
|
-
slower: "faster"
|
|
1698
|
-
};
|
|
1699
|
-
/**
|
|
1700
|
-
* This function gives the time verdict over the records of all jobs.
|
|
1701
|
-
* - It makes the pool of the jobs with `poolRecords`, and it gives the time verdict of the merged records.
|
|
1702
|
-
* - The verdict of each job alone uses the same options. A job with `cannot decide` or `same` does not change the outcome.
|
|
1703
|
-
* - A job that decides the opposite direction of the pooled outcome makes the outcome `cannot decide`.
|
|
1704
|
-
*/
|
|
1705
|
-
function pooledVerdict(jobs, options = {}) {
|
|
1706
|
-
const { candidate, aa, jobs: each, trimmed, dropped } = pool(jobs, "pooledVerdict");
|
|
1707
|
-
const pooled = timeVerdict(candidate, aa, options);
|
|
1708
|
-
const verdicts = each.map(({ job, candidate: c, aa: a }) => ({
|
|
1709
|
-
job,
|
|
1710
|
-
verdict: timeVerdict(c, a, options)
|
|
1711
|
-
}));
|
|
1712
|
-
const { outcome, opposed, reason } = guard(pooled, verdicts, opposite);
|
|
1713
|
-
return {
|
|
1714
|
-
outcome,
|
|
1715
|
-
pooled,
|
|
1716
|
-
jobs: verdicts,
|
|
1717
|
-
opposed,
|
|
1718
|
-
trimmed,
|
|
1719
|
-
dropped,
|
|
1720
|
-
reason
|
|
1721
|
-
};
|
|
1722
|
-
}
|
|
1723
|
-
const limitOpposite = {
|
|
1724
|
-
within: "beyond",
|
|
1725
|
-
beyond: "within"
|
|
1726
|
-
};
|
|
1727
|
-
/**
|
|
1728
|
-
* This function gives the limit verdict over the records of all jobs, for example for a speed gate on limits over a CI matrix.
|
|
1729
|
-
* - It makes the pool of the jobs with `poolRecords`, and it gives the limit verdict of the merged records.
|
|
1730
|
-
* - The verdict of each job alone uses the same options. A job with `cannot decide` does not change the outcome.
|
|
1731
|
-
* - A job with `beyond` against a pooled `within`, or the reverse, makes the outcome `cannot decide`.
|
|
1732
|
-
*/
|
|
1733
|
-
function pooledLimitVerdict(jobs, options) {
|
|
1734
|
-
const { candidate, aa, jobs: each, trimmed, dropped } = pool(jobs, "pooledLimitVerdict");
|
|
1735
|
-
const pooled = limitVerdict(candidate, aa, options);
|
|
1736
|
-
const verdicts = each.map(({ job, candidate: c, aa: a }) => ({
|
|
1737
|
-
job,
|
|
1738
|
-
verdict: limitVerdict(c, a, options)
|
|
1739
|
-
}));
|
|
1740
|
-
const { outcome, opposed, reason } = guard(pooled, verdicts, limitOpposite);
|
|
1741
|
-
return {
|
|
1742
|
-
outcome,
|
|
1743
|
-
pooled,
|
|
1744
|
-
jobs: verdicts,
|
|
1745
|
-
opposed,
|
|
1746
|
-
trimmed,
|
|
1747
|
-
dropped,
|
|
1748
|
-
reason
|
|
1749
|
-
};
|
|
1750
|
-
}
|
|
1751
|
-
/**
|
|
1752
|
-
* This function gives the verdict of each candidate pair of a run against its A/A pair `aaPair`.
|
|
1753
|
-
* It balances the records of each pair with `balanceOrders` first. It refuses an `aaPair` that the run does not have.
|
|
1754
|
-
*/
|
|
1755
|
-
function armVerdicts(run, aaPair, options = {}) {
|
|
1756
|
-
const aaRecords = run.pairs[aaPair];
|
|
1757
|
-
if (aaRecords === void 0) throw new RangeError(`armVerdicts: the run has no pair ${JSON.stringify(aaPair)}.`);
|
|
1758
|
-
const aa = balanceOrders(aaRecords).records;
|
|
1759
|
-
const out = {};
|
|
1760
|
-
for (const [pair, records] of Object.entries(run.pairs)) if (pair !== aaPair) out[pair] = timeVerdict(balanceOrders(records).records, aa, options);
|
|
1761
|
-
return out;
|
|
1762
|
-
}
|
|
1763
1945
|
// src/measures.ts
|
|
1764
1946
|
/**
|
|
1765
1947
|
* The check that a trick does not buy a measure. A control is a strategy made to win one measure and nothing else.
|
|
@@ -2542,6 +2724,6 @@ function timeRounds(arms, options) {
|
|
|
2542
2724
|
sink
|
|
2543
2725
|
};
|
|
2544
2726
|
}
|
|
2545
|
-
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, growthVerdict, 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 };
|
|
2546
2728
|
|
|
2547
2729
|
//# sourceMappingURL=index.js.map
|