@mark1russell7/rigor-bench 0.2.25 → 0.2.27

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/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$3(middle)} of ${values.length} rounds, interval ${round$3(interval.low)} to ${round$3(interval.high)}, limit ${limit}, A/A median ${aaMedian === void 0 ? "none" : round$3(aaMedian)} of ${aaValues.length} rounds, difference ${difference === void 0 ? "none" : round$3(difference)}`;
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$3(spent)}, over the ${round$3(1 - level)} of the level ${level}.`);
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$3(x) {
212
+ function round$4(x) {
213
213
  return Math.round(x * 1e4) / 1e4;
214
214
  }
215
215
  // src/growth.ts
@@ -658,8 +658,22 @@ function checkLadder(ladder) {
658
658
  });
659
659
  return out;
660
660
  }
661
+ /** This function gives the reason that a list has a NaN, with the index of the first one, or undefined. */
662
+ function nanAt(name, values) {
663
+ const index = values.findIndex((value) => Number.isNaN(value));
664
+ return index < 0 ? void 0 : `${name} ${index} is NaN`;
665
+ }
661
666
  /** This function gives the verdict of a rate: the Wilson interval at 95 % against the limit. */
662
667
  function rateResult(budget, failures, trials) {
668
+ const whole = (value) => Number.isSafeInteger(value) && value >= 0;
669
+ if (!(whole(failures) && whole(trials) && failures <= trials)) {
670
+ const why = `the counts are whole numbers of 0 or more with failures not above trials, not ${failures} failures of ${trials} trials`;
671
+ return {
672
+ measure: budget.measure,
673
+ outcome: "cannot decide",
674
+ reason: `The measure ${JSON.stringify(budget.measure)} is "cannot decide": ${why}.`
675
+ };
676
+ }
663
677
  const interval = wilson(failures, trials);
664
678
  const facts = `${failures} failures of ${trials} trials, interval ${interval.low} to ${interval.high}, limit ${budget.limit}`;
665
679
  const outcome = trials === 0 ? "cannot decide" : interval.high <= budget.limit ? "within" : interval.low > budget.limit ? "beyond" : "cannot decide";
@@ -671,6 +685,12 @@ function rateResult(budget, failures, trials) {
671
685
  }
672
686
  /** This function gives the verdict of a set of drills: the largest value against the limit. */
673
687
  function worstResult(budget, values) {
688
+ const nan = nanAt("the drill", values);
689
+ if (nan !== void 0) return {
690
+ measure: budget.measure,
691
+ outcome: "cannot decide",
692
+ reason: `The measure ${JSON.stringify(budget.measure)} is "cannot decide": ${nan}.`
693
+ };
674
694
  const largest = values.length === 0 ? void 0 : Math.max(...values);
675
695
  const facts = `${values.length} drills, the largest ${largest ?? "none"} ${budget.unit}, limit ${budget.limit}`;
676
696
  const outcome = largest !== void 0 && largest > budget.limit ? "beyond" : values.length < budget.minDrills ? "cannot decide" : "within";
@@ -695,7 +715,8 @@ function quantileResult(budget, values, censored) {
695
715
  const open = censored.length > 0;
696
716
  const bounds = [...values, ...censored];
697
717
  if (bounds.length === 0) return say("cannot decide", `0 values, limit ${budget.limit} ${budget.unit}`);
698
- if (values.some((value) => Number.isNaN(value))) return say("cannot decide", "the values have a NaN");
718
+ const nan = nanAt("the value", values);
719
+ if (nan !== void 0) return say("cannot decide", nan);
699
720
  if (!censored.every((value) => Number.isFinite(value))) return say("cannot decide", "a censored value is not a finite number");
700
721
  const lowest = quantileInterval(bounds, budget.quantile, level);
701
722
  const highest = open ? quantileInterval([...values, ...censored.map(() => Infinity)], budget.quantile, level) : lowest;
@@ -935,14 +956,14 @@ function timeVerdict(candidate, aa, options = {}) {
935
956
  noise
936
957
  };
937
958
  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$2(spread)}, above the noise cap ${cap}.`} Without the cap, the outcome is "${verdict.outcome}".`;
959
+ 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
960
  verdict = {
940
961
  ...verdict,
941
962
  outcome: "cannot decide",
942
963
  reason
943
964
  };
944
965
  } else if (noise.biased && verdict.band !== void 0) {
945
- const reason = `The A/A median ratio of the run is ${round$2(aaMedian)}, out of [${round$2(Math.exp(-cap))}, ${round$2(Math.exp(cap))}] of the noise cap ${cap}. Without the cap, the outcome is "${verdict.outcome}".`;
966
+ 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
967
  verdict = {
947
968
  ...verdict,
948
969
  outcome: "cannot decide",
@@ -1010,44 +1031,59 @@ function ruleVerdict(candidate, aa, options, pooled) {
1010
1031
  };
1011
1032
  if (rule === "difference") {
1012
1033
  const margin = marginOf(options, pooled);
1013
- const shown = pooled === void 0 ? margin : round$2(margin);
1034
+ const shown = pooled === void 0 ? margin : round$3(margin);
1014
1035
  const logs = cases.map((c) => Math.log(c.ratio));
1015
1036
  const aaLogs = aaRatios.map((c) => Math.log(c.ratio));
1016
- const d = bootstrapTwo(logs, aaLogs, (x, y) => median(x) - median(y), options.bootstrap);
1037
+ const d = bootstrapTwo(logs, aaLogs, (x, y) => median(x) - median(y), options.pValues === true ? {
1038
+ ...options.bootstrap,
1039
+ against: 0
1040
+ } : options.bootstrap);
1041
+ const { level, runs, finite, seed } = d;
1017
1042
  const interval = {
1018
- ...d,
1019
1043
  estimate: Math.exp(d.estimate),
1020
1044
  low: Math.exp(d.low),
1021
- high: Math.exp(d.high)
1045
+ high: Math.exp(d.high),
1046
+ level,
1047
+ runs,
1048
+ finite,
1049
+ seed
1022
1050
  };
1023
- const span = `The interval of the candidate median over the A/A median is [${round$2(interval.low)}, ${round$2(interval.high)}].`;
1051
+ const sides = options.pValues === true ? {
1052
+ pSlower: d.pAbove ?? 1,
1053
+ pFaster: d.pBelow ?? 1
1054
+ } : {};
1055
+ const span = `The interval of the candidate median over the A/A median is [${round$3(interval.low)}, ${round$3(interval.high)}].`;
1024
1056
  if (d.low >= -margin && d.high <= margin) return {
1025
1057
  ...facts,
1058
+ ...sides,
1026
1059
  interval,
1027
1060
  outcome: "same",
1028
1061
  reason: `${span} The interval is in the margin of ${shown}.`
1029
1062
  };
1030
1063
  if (interval.high < 1) return {
1031
1064
  ...facts,
1065
+ ...sides,
1032
1066
  interval,
1033
1067
  outcome: "faster",
1034
1068
  reason: `${span} The interval is below 1.`
1035
1069
  };
1036
1070
  if (interval.low > 1) return {
1037
1071
  ...facts,
1072
+ ...sides,
1038
1073
  interval,
1039
1074
  outcome: "slower",
1040
1075
  reason: `${span} The interval is above 1.`
1041
1076
  };
1042
1077
  return {
1043
1078
  ...facts,
1079
+ ...sides,
1044
1080
  interval,
1045
1081
  outcome: "cannot decide",
1046
1082
  reason: `${span} The interval holds 1 and crosses the margin of ${shown}.`
1047
1083
  };
1048
1084
  }
1049
1085
  const interval = bootstrapMedian(cases.map((c) => c.ratio), options.bootstrap);
1050
- const span = `The interval of the median ratio is [${round$2(interval.low)}, ${round$2(interval.high)}], and the A/A band is [${round$2(band.low)}, ${round$2(band.high)}].`;
1086
+ 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
1087
  if (interval.high < band.low) return {
1052
1088
  ...facts,
1053
1089
  interval,
@@ -1073,7 +1109,7 @@ function ruleVerdict(candidate, aa, options, pooled) {
1073
1109
  reason: `${span} The interval crosses an end of the band.`
1074
1110
  };
1075
1111
  }
1076
- function round$2(x) {
1112
+ function round$3(x) {
1077
1113
  return Math.round(x * 1e4) / 1e4;
1078
1114
  }
1079
1115
  // src/limit.ts
@@ -1178,20 +1214,358 @@ function limitVerdict(candidate, aa, options) {
1178
1214
  const rest = others.length === 0 ? "" : ` ${others.length} of ${cases.length} cases are not within${permit}.`;
1179
1215
  return {
1180
1216
  outcome: "within",
1181
- reason: `The largest bound of a case within is ${round$1(top.bound)} in the case ${JSON.stringify(top.case)}, and it is at most the limit ${limit}.${rest}`,
1217
+ 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
1218
  ...facts
1183
1219
  };
1184
1220
  }
1185
1221
  const top = cases.reduce((a, b) => b.bound > a.bound ? b : a);
1186
1222
  return {
1187
1223
  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$1(top.bound)}, above the limit ${limit}, and too few floors are above it.`,
1224
+ 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
1225
  ...facts
1190
1226
  };
1191
1227
  }
1192
- function round$1(x) {
1228
+ function round$2(x) {
1193
1229
  return Math.round(x * 1e4) / 1e4;
1194
1230
  }
1231
+ // src/merge.ts
1232
+ /**
1233
+ * The merge of the records of many jobs, and the pooled verdicts over them. Each job of a bench gives its own records.
1234
+ * A pooled verdict uses all jobs together, and the verdict of each job alone guards it.
1235
+ */
1236
+ /**
1237
+ * This function removes records until the two orders of each case differ in count by 1 or less.
1238
+ * It removes the records of the larger order with the highest rounds first. The other records keep their order.
1239
+ * 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.
1240
+ */
1241
+ function balanceOrders(records) {
1242
+ const byCase = /* @__PURE__ */ new Map();
1243
+ records.forEach((record, index) => {
1244
+ const orders = byCase.get(record.case) ?? {
1245
+ a: [],
1246
+ b: []
1247
+ };
1248
+ orders[record.first === "b" ? "b" : "a"].push(index);
1249
+ byCase.set(record.case, orders);
1250
+ });
1251
+ const drop = /* @__PURE__ */ new Set();
1252
+ const highFirst = (x, y) => records[y].round - records[x].round;
1253
+ for (const { a, b } of byCase.values()) {
1254
+ const [more, fewer] = a.length > b.length ? [a, b] : [b, a];
1255
+ const extra = more.length - fewer.length - 1;
1256
+ if (extra > 0) for (const index of more.toSorted(highFirst).slice(0, extra)) drop.add(index);
1257
+ }
1258
+ return {
1259
+ records: records.filter((_, index) => !drop.has(index)),
1260
+ trimmed: drop.size
1261
+ };
1262
+ }
1263
+ /**
1264
+ * This function gives the pool of the jobs. `pooledVerdict` and `pooledLimitVerdict` use it.
1265
+ * - 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.
1266
+ * - It removes each case that a job skipped from all jobs, so all jobs have the same cases.
1267
+ * - It merges the records of the jobs, and it balances the merged records and the records of each job with `balanceOrders`.
1268
+ */
1269
+ function poolRecords(jobs) {
1270
+ return pool(jobs, "poolRecords");
1271
+ }
1272
+ /** The pool of `poolRecords`. Each error starts with the name `caller`. */
1273
+ function pool(jobs, caller) {
1274
+ const labels = /* @__PURE__ */ new Set();
1275
+ for (const { job } of jobs) {
1276
+ if (labels.has(job)) throw new RangeError(`${caller}: two jobs have the label ${JSON.stringify(job)}.`);
1277
+ labels.add(job);
1278
+ }
1279
+ 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)}.`);
1280
+ const dropped = [...new Set(jobs.flatMap((j) => (j.skipped ?? []).map((s) => s.case)))].sort();
1281
+ const gone = new Set(dropped);
1282
+ const keep = (records) => records.filter((r) => !gone.has(r.case));
1283
+ const kept = jobs.map((j) => ({
1284
+ job: j.job,
1285
+ candidate: keep(j.candidate),
1286
+ aa: keep(j.aa)
1287
+ }));
1288
+ uniqueRounds(kept, "candidate", caller);
1289
+ uniqueRounds(kept, "aa", caller);
1290
+ const candidate = balanceOrders(kept.flatMap((j) => j.candidate));
1291
+ const aa = balanceOrders(kept.flatMap((j) => j.aa));
1292
+ const each = kept.map(({ job, candidate: c, aa: a }) => ({
1293
+ job,
1294
+ candidate: balanceOrders(c).records,
1295
+ aa: balanceOrders(a).records
1296
+ }));
1297
+ return {
1298
+ candidate: candidate.records,
1299
+ aa: aa.records,
1300
+ jobs: each,
1301
+ trimmed: candidate.trimmed + aa.trimmed,
1302
+ dropped
1303
+ };
1304
+ }
1305
+ /** This function refuses two jobs with a record of one case and one round in one pair. */
1306
+ function uniqueRounds(jobs, pair, caller) {
1307
+ const seen = /* @__PURE__ */ new Map();
1308
+ for (const { job, [pair]: records } of jobs) for (const r of records) {
1309
+ const key = `${r.case}\u0000${r.round}`;
1310
+ const other = seen.get(key);
1311
+ 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.`);
1312
+ seen.set(key, job);
1313
+ }
1314
+ }
1315
+ /**
1316
+ * This function applies the guard of the jobs to a pooled verdict. A job whose outcome is `opposite` of the pooled outcome
1317
+ * makes the outcome `cannot decide`. Each other job does not change the outcome.
1318
+ */
1319
+ function guard(pooled, jobs, opposite) {
1320
+ const against = opposite[pooled.outcome];
1321
+ const opposed = against === void 0 ? [] : jobs.filter((j) => j.verdict.outcome === against).map((j) => j.job);
1322
+ if (opposed.length === 0) return {
1323
+ outcome: pooled.outcome,
1324
+ opposed,
1325
+ reason: pooled.reason
1326
+ };
1327
+ const names = opposed.map((j) => JSON.stringify(j)).join(", ");
1328
+ return {
1329
+ outcome: "cannot decide",
1330
+ opposed,
1331
+ reason: `${pooled.reason} The pooled outcome is "${pooled.outcome}", but the jobs ${names} give "${against}". Thus the outcome is "cannot decide".`
1332
+ };
1333
+ }
1334
+ const opposite = {
1335
+ faster: "slower",
1336
+ slower: "faster"
1337
+ };
1338
+ /**
1339
+ * This function gives the time verdict over the records of all jobs.
1340
+ * - It makes the pool of the jobs with `poolRecords`, and it gives the time verdict of the merged records.
1341
+ * - The verdict of each job alone uses the same options. A job with `cannot decide` or `same` does not change the outcome.
1342
+ * - A job that decides the opposite direction of the pooled outcome makes the outcome `cannot decide`.
1343
+ */
1344
+ function pooledVerdict(jobs, options = {}) {
1345
+ const { candidate, aa, jobs: each, trimmed, dropped } = pool(jobs, "pooledVerdict");
1346
+ const pooled = timeVerdict(candidate, aa, options);
1347
+ const verdicts = each.map(({ job, candidate: c, aa: a }) => ({
1348
+ job,
1349
+ verdict: timeVerdict(c, a, options)
1350
+ }));
1351
+ const { outcome, opposed, reason } = guard(pooled, verdicts, opposite);
1352
+ return {
1353
+ outcome,
1354
+ pooled,
1355
+ jobs: verdicts,
1356
+ opposed,
1357
+ trimmed,
1358
+ dropped,
1359
+ reason
1360
+ };
1361
+ }
1362
+ const limitOpposite = {
1363
+ within: "beyond",
1364
+ beyond: "within"
1365
+ };
1366
+ /**
1367
+ * This function gives the limit verdict over the records of all jobs, for example for a speed gate on limits over a CI matrix.
1368
+ * - It makes the pool of the jobs with `poolRecords`, and it gives the limit verdict of the merged records.
1369
+ * - The verdict of each job alone uses the same options. A job with `cannot decide` does not change the outcome.
1370
+ * - A job with `beyond` against a pooled `within`, or the reverse, makes the outcome `cannot decide`.
1371
+ */
1372
+ function pooledLimitVerdict(jobs, options) {
1373
+ const { candidate, aa, jobs: each, trimmed, dropped } = pool(jobs, "pooledLimitVerdict");
1374
+ const pooled = limitVerdict(candidate, aa, options);
1375
+ const verdicts = each.map(({ job, candidate: c, aa: a }) => ({
1376
+ job,
1377
+ verdict: limitVerdict(c, a, options)
1378
+ }));
1379
+ const { outcome, opposed, reason } = guard(pooled, verdicts, limitOpposite);
1380
+ return {
1381
+ outcome,
1382
+ pooled,
1383
+ jobs: verdicts,
1384
+ opposed,
1385
+ trimmed,
1386
+ dropped,
1387
+ reason
1388
+ };
1389
+ }
1390
+ /**
1391
+ * This function gives the verdict of each candidate pair of a run against its A/A pair `aaPair`.
1392
+ * It balances the records of each pair with `balanceOrders` first. It refuses an `aaPair` that the run does not have.
1393
+ */
1394
+ function armVerdicts(run, aaPair, options = {}) {
1395
+ const aaRecords = run.pairs[aaPair];
1396
+ if (aaRecords === void 0) throw new RangeError(`armVerdicts: the run has no pair ${JSON.stringify(aaPair)}.`);
1397
+ const aa = balanceOrders(aaRecords).records;
1398
+ const out = {};
1399
+ for (const [pair, records] of Object.entries(run.pairs)) if (pair !== aaPair) out[pair] = timeVerdict(balanceOrders(records).records, aa, options);
1400
+ return out;
1401
+ }
1402
+ // src/family.ts
1403
+ /**
1404
+ * The read of a family of cases. A run reads many cases, and each case has its own interval. Without a correction, the
1405
+ * chance of one false `slower` case or more grows with the count of cases. At 16 cases and 0.025 for each side, this
1406
+ * chance can be 1 - 0.975^16, approximately 0.33. These reads are reports, and they do not change a verdict.
1407
+ *
1408
+ * - The Holm read (Holm, 1979) keeps the chance of one false claim or more on one side at most `level`.
1409
+ * - The read of each job keeps a `slower` or `faster` case only when each job gives that outcome alone.
1410
+ * - The fourth read keeps a case only when the Holm read and the read of each job both keep it.
1411
+ */
1412
+ const pooledOf = (verdict) => "pooled" in verdict ? verdict : void 0;
1413
+ /** This function changes `slower` and `faster` to `cannot decide` when `keep` refuses the side. */
1414
+ function kept(outcome, keep) {
1415
+ return (outcome === "slower" || outcome === "faster") && !keep(outcome) ? "cannot decide" : outcome;
1416
+ }
1417
+ /** This function gives the Holm read of one side, by the index of the case. A case with no p gets undefined. */
1418
+ function holmSide(ps, level) {
1419
+ const index = ps.flatMap((p, i) => p === void 0 ? [] : [i]);
1420
+ const corrected = holm(index.map((i) => ps[i]), level);
1421
+ const out = new Array(ps.length).fill(void 0);
1422
+ index.forEach((i, k) => out[i] = corrected[k].adjusted);
1423
+ return out;
1424
+ }
1425
+ const count = (outcomes) => ({
1426
+ slower: outcomes.filter((o) => o === "slower").length,
1427
+ faster: outcomes.filter((o) => o === "faster").length
1428
+ });
1429
+ /**
1430
+ * 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.
1431
+ * 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.
1432
+ * A case with no p keeps its outcome in the Holm read, and a line gives the count of such cases.
1433
+ */
1434
+ function familyRead(items, level = .025) {
1435
+ if (!(level > 0 && level < 1)) throw new RangeError(`familyRead: the level is more than 0 and less than 1, not ${level}.`);
1436
+ const times = items.map((item) => pooledOf(item.verdict)?.pooled ?? item.verdict);
1437
+ const slower = holmSide(times.map((t) => t.pSlower), level);
1438
+ const faster = holmSide(times.map((t) => t.pFaster), level);
1439
+ const cases = items.map((item, i) => {
1440
+ const outcome = item.verdict.outcome;
1441
+ const adjusted = {
1442
+ slower: slower[i],
1443
+ faster: faster[i]
1444
+ };
1445
+ const pooled = pooledOf(item.verdict);
1446
+ const byHolm = kept(outcome, (side) => adjusted[side] === void 0 || adjusted[side] < level);
1447
+ const byJobs = pooled === void 0 ? outcome : kept(outcome, (side) => pooled.jobs.every((j) => j.verdict.outcome === side));
1448
+ return {
1449
+ case: item.case,
1450
+ outcome,
1451
+ holm: byHolm,
1452
+ eachJob: byJobs,
1453
+ eachJobHolm: byHolm === outcome ? byJobs : byHolm,
1454
+ adjustedSlower: adjusted.slower,
1455
+ adjustedFaster: adjusted.faster
1456
+ };
1457
+ });
1458
+ const finite = times.filter((t) => t.pSlower !== void 0).map((t) => t.interval?.finite ?? 0);
1459
+ const floor = finite.length === 0 ? void 0 : 1 / (Math.min(...finite) + 1);
1460
+ const firstStep = finite.length === 0 ? void 0 : level / finite.length;
1461
+ const plain = count(cases.map((c) => c.outcome));
1462
+ const corrected = count(cases.map((c) => c.holm));
1463
+ const eachJob = count(cases.map((c) => c.eachJob));
1464
+ const eachJobHolm = count(cases.map((c) => c.eachJobHolm));
1465
+ 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])}`;
1466
+ const lines = [`Family read (report only): ${String(cases.length)} cases. Slower: ${counts("slower")}. Faster: ${counts("faster")}.`];
1467
+ const bare = times.filter((t) => t.pSlower === void 0).length;
1468
+ 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.`);
1469
+ 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.`);
1470
+ for (const c of cases) {
1471
+ 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)}.`);
1472
+ 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}.`);
1473
+ }
1474
+ return {
1475
+ level,
1476
+ cases,
1477
+ plain,
1478
+ holm: corrected,
1479
+ eachJob,
1480
+ eachJobHolm,
1481
+ floor,
1482
+ firstStep,
1483
+ lines
1484
+ };
1485
+ }
1486
+ /**
1487
+ * This function draws `trials` families of pooled verdicts and gives the share of each read. Each arm of each round adds a
1488
+ * 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.
1489
+ */
1490
+ function familyPower(options) {
1491
+ const { family } = options;
1492
+ const ratio = options.ratio ?? 1;
1493
+ const rounds = options.rounds ?? 5;
1494
+ const jobs = options.jobs ?? 2;
1495
+ const sigma = options.sigma ?? .1;
1496
+ const trials = options.trials ?? 200;
1497
+ const jobShift = options.jobShift ?? 0;
1498
+ for (const [name, n] of [
1499
+ ["family", family],
1500
+ ["rounds", rounds],
1501
+ ["jobs", jobs],
1502
+ ["trials", trials]
1503
+ ]) if (!Number.isSafeInteger(n) || n < 1) throw new RangeError(`familyPower: ${name} is a whole number of 1 or more, not ${n}.`);
1504
+ if (!(ratio > 0 && Number.isFinite(ratio))) throw new RangeError(`familyPower: the ratio is a finite number above 0, not ${ratio}.`);
1505
+ if (!(jobShift >= 0 && Number.isFinite(jobShift))) throw new RangeError(`familyPower: jobShift is a finite number of 0 or more, not ${jobShift}.`);
1506
+ const r = random(options.seed ?? 1);
1507
+ const draw = (truth, job, c) => Array.from({ length: rounds }, (_, round) => ({
1508
+ case: `${job}:${String(round)}`,
1509
+ round: 0,
1510
+ first: firstArm(round, c),
1511
+ a: 10 * Math.exp(r.normal(0, sigma)),
1512
+ b: 10 * truth * Math.exp(r.normal(0, sigma))
1513
+ }));
1514
+ const hits = {
1515
+ false: {
1516
+ plain: 0,
1517
+ holm: 0,
1518
+ eachJob: 0,
1519
+ eachJobHolm: 0
1520
+ },
1521
+ first: {
1522
+ plain: 0,
1523
+ holm: 0,
1524
+ eachJob: 0,
1525
+ eachJobHolm: 0
1526
+ }
1527
+ };
1528
+ for (let t = 0; t < trials; t++) {
1529
+ const read = familyRead(Array.from({ length: family }, (_, c) => {
1530
+ const records = Array.from({ length: jobs }, (_, j) => ({
1531
+ job: `job ${String(j)}`,
1532
+ candidate: draw((c === 0 ? ratio : 1) * (jobShift > 0 ? Math.exp(r.normal(0, jobShift)) : 1), String(j), c),
1533
+ aa: draw(1, String(j), c)
1534
+ }));
1535
+ return {
1536
+ case: `case ${String(c)}`,
1537
+ verdict: pooledVerdict(records, {
1538
+ ...options.verdict,
1539
+ pValues: true
1540
+ })
1541
+ };
1542
+ }), options.level);
1543
+ for (const key of [
1544
+ "plain",
1545
+ "holm",
1546
+ "eachJob",
1547
+ "eachJobHolm"
1548
+ ]) {
1549
+ const outcome = (c) => key === "plain" ? c.outcome : c[key];
1550
+ if (read.cases.some((c, i) => (i > 0 || ratio === 1) && outcome(c) === "slower")) hits.false[key]++;
1551
+ if (outcome(read.cases[0]) === "slower") hits.first[key]++;
1552
+ }
1553
+ }
1554
+ const share = (h) => ({
1555
+ plain: h.plain / trials,
1556
+ holm: h.holm / trials,
1557
+ eachJob: h.eachJob / trials,
1558
+ eachJobHolm: h.eachJobHolm / trials
1559
+ });
1560
+ return {
1561
+ trials,
1562
+ falseSlower: share(hits.false),
1563
+ firstSlower: share(hits.first)
1564
+ };
1565
+ }
1566
+ function round$1(x) {
1567
+ return Math.round(x * 1e5) / 1e5;
1568
+ }
1195
1569
  // src/model.ts
1196
1570
  /**
1197
1571
  * 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 +1963,6 @@ function tailFalseFails(aa, options) {
1589
1963
  function round(x) {
1590
1964
  return Math.round(x * 1e4) / 1e4;
1591
1965
  }
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
1966
  // src/measures.ts
1764
1967
  /**
1765
1968
  * 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 +2745,6 @@ function timeRounds(arms, options) {
2542
2745
  sink
2543
2746
  };
2544
2747
  }
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 };
2748
+ 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
2749
 
2547
2750
  //# sourceMappingURL=index.js.map