@mark1russell7/rigor-bench 0.2.14 → 0.2.15

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
@@ -1,5 +1,5 @@
1
1
  import { n as checkPairedTime, r as firstArm, t as caseRatios } from "./paired.js";
2
- import { bootstrapMedian, bootstrapTwo, compareRatios, holm, median, medianInterval, quantile, quantileInterval, quantileSamples, signTest, signTestPower, wilson } from "@mark1russell7/rigor-statistics";
2
+ import { bootstrapMedian, bootstrapTwo, compareRatios, holm, median, medianInterval, partialConjunction, quantile, quantileInterval, quantileSamples, signTest, signTestPower, wilson } from "@mark1russell7/rigor-statistics";
3
3
  import { random } from "@mark1russell7/rigor-generators";
4
4
  // src/budget.ts
5
5
  /**
@@ -212,6 +212,216 @@ function budgetLooksVerdict(rounds, aa, options) {
212
212
  function round$2(x) {
213
213
  return Math.round(x * 1e4) / 1e4;
214
214
  }
215
+ // src/gate.ts
216
+ /**
217
+ * 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
218
+ * case, and the gate gives "pass", "block" or "no read". A case with no outcome does not pass.
219
+ */
220
+ /**
221
+ * This function gives the gate of a list of case outcomes.
222
+ * - The result is "block" when a case is `beyond`.
223
+ * - The result is "pass" when each case is `within`.
224
+ * - Each other result is "no read", and so is a list with no cases. A caller does not count "no read" as a pass.
225
+ */
226
+ function gateOf(cases) {
227
+ const names = (outcome) => cases.filter((c) => c.outcome === outcome).map((c) => c.case);
228
+ const blocks = names("beyond");
229
+ const noRead = names("cannot decide");
230
+ if (cases.length === 0) return {
231
+ outcome: "no read",
232
+ reason: "The gate has no cases.",
233
+ blocks,
234
+ noRead
235
+ };
236
+ if (blocks.length > 0) return {
237
+ outcome: "block",
238
+ reason: `${blocks.length} of ${cases.length} cases are beyond the claim. The first is ${JSON.stringify(blocks[0])}.`,
239
+ blocks,
240
+ noRead
241
+ };
242
+ if (noRead.length > 0) return {
243
+ outcome: "no read",
244
+ reason: `${noRead.length} of ${cases.length} cases have no outcome. The first is ${JSON.stringify(noRead[0])}.`,
245
+ blocks,
246
+ noRead
247
+ };
248
+ return {
249
+ outcome: "pass",
250
+ reason: `Each of the ${cases.length} cases is within the claim.`,
251
+ blocks,
252
+ noRead
253
+ };
254
+ }
255
+ const READ_OF = {
256
+ within: "not slower",
257
+ beyond: "slower",
258
+ "cannot decide": "no read"
259
+ };
260
+ /** The looks of `notSlowerVerdict`: the rounds in each job. The level of the family has an equal share at each look. */
261
+ const GATE_LOOKS = [
262
+ 12,
263
+ 24,
264
+ 48
265
+ ];
266
+ /** This function examines the options. */
267
+ function checkOptions(level, looks, need) {
268
+ if (!(level > 0 && level < 1)) throw new RangeError(`notSlowerVerdict: the level is above 0 and below 1, not ${level}.`);
269
+ if (looks.length === 0) throw new RangeError("notSlowerVerdict: the list of looks is empty.");
270
+ let last = 0;
271
+ for (const rounds of looks) {
272
+ if (!(Number.isInteger(rounds) && rounds > last && rounds % 2 === 0)) throw new RangeError(`notSlowerVerdict: the looks are even whole numbers in rising order, not ${rounds} after ${last}.`);
273
+ last = rounds;
274
+ }
275
+ if (!(Number.isInteger(need) && need >= 1)) throw new RangeError(`notSlowerVerdict: the count of jobs is a whole number of 1 or more, not ${need}.`);
276
+ }
277
+ /** This function gives the records of one case of one job in the order of the rounds. It refuses a round that comes twice. */
278
+ function casesOf(records) {
279
+ const out = /* @__PURE__ */ new Map();
280
+ for (const record of records) {
281
+ checkPairedTime(record);
282
+ const list = out.get(record.case) ?? [];
283
+ if (list.some((r) => r.round === record.round)) throw new RangeError(`notSlowerVerdict: the round ${record.round} of ${JSON.stringify(record.case)} comes twice.`);
284
+ list.push(record);
285
+ out.set(record.case, list);
286
+ }
287
+ for (const list of out.values()) list.sort((a, b) => a.round - b.round);
288
+ return out;
289
+ }
290
+ /**
291
+ * This function gives the differences of the first `rounds` rounds: log(b / a) of the candidate minus log(b / a) of the A/A arm.
292
+ * A pair has two records with the same first arm, because a difference of two orders holds the effect of the order.
293
+ */
294
+ function differences(candidate, aa, rounds) {
295
+ const out = [];
296
+ const ofOrder = (list, first) => list.slice(0, rounds).filter((r) => r.first === first);
297
+ for (const first of ["a", "b"]) {
298
+ const left = ofOrder(candidate, first);
299
+ const right = ofOrder(aa, first);
300
+ for (let i = 0; i < Math.min(left.length, right.length); i++) {
301
+ const c = left[i];
302
+ const a = right[i];
303
+ out.push(Math.log(c.b / c.a) - Math.log(a.b / a.a));
304
+ }
305
+ }
306
+ return out;
307
+ }
308
+ /** This function gives the read of one job. A job that has not reached the look gives a p of 1 in both directions. */
309
+ function jobRead(job, diffs, margin, rounds) {
310
+ if (diffs.length < rounds) return {
311
+ job,
312
+ rounds: diffs.length,
313
+ below: 0,
314
+ above: 0,
315
+ notSlower: 1,
316
+ slower: 1,
317
+ centre: void 0
318
+ };
319
+ const below = diffs.filter((d) => d < margin).length;
320
+ const above = diffs.filter((d) => d > margin).length;
321
+ return {
322
+ job,
323
+ rounds: diffs.length,
324
+ below,
325
+ above,
326
+ notSlower: signTest(below, diffs.length - below),
327
+ slower: signTest(above, diffs.length - above),
328
+ centre: median(diffs)
329
+ };
330
+ }
331
+ /** This function gives the words of the facts of one job, for a reason. */
332
+ function jobFacts(read) {
333
+ if (read.centre === void 0) return `job ${read.job} has ${read.rounds} pairs`;
334
+ return `job ${read.job} has ${read.below} of ${read.rounds} pairs below the margin, centre ${Math.round(read.centre * 1e5) / 1e3} %`;
335
+ }
336
+ /**
337
+ * This function gives the gate of the claim "the candidate is not slower than the A/A arm by more than the margin" for each
338
+ * case. A one-sided bound answers the claim. Thus a case with its whole interval under the margin reads "not slower", also
339
+ * when its interval holds 1.
340
+ *
341
+ * - In each job, the sign test compares the median of the differences with the margin. It uses no random numbers.
342
+ * - A case reads in `jobs` jobs or more: `partialConjunction` joins the p of each job, with no condition on how they depend.
343
+ * - The level is for the family. Each look has `level / looks` of it, and `holm` corrects each look over all cases.
344
+ * - The first look with an outcome decides a case. Later rounds do not change it. Each look uses the first rounds of a job.
345
+ * - The result is "block" when a case reads "slower", and "pass" when each case reads "not slower".
346
+ * - Each other result is "no read". The field `more` gives the next look of each case. After the last look, the caller adds jobs.
347
+ *
348
+ * The level needs independent rounds. Rounds with a shared cause, for example a slow computer for one job, do not have it.
349
+ * A job that a caller adds after the last look is one more look, and the level does not count it.
350
+ */
351
+ function notSlowerVerdict(jobs, options) {
352
+ const level = options.level ?? .05;
353
+ const looks = options.looks ?? GATE_LOOKS;
354
+ const need = options.jobs ?? 2;
355
+ checkOptions(level, looks, need);
356
+ const marginOf = (name) => {
357
+ const margin = typeof options.margin === "number" ? options.margin : options.margin(name);
358
+ if (!(margin >= 0 && Number.isFinite(margin))) throw new RangeError(`notSlowerVerdict: the margin of ${JSON.stringify(name)} is finite and 0 or more, not ${margin}.`);
359
+ return margin;
360
+ };
361
+ const records = jobs.map((job) => ({
362
+ job: job.job,
363
+ candidate: casesOf(job.candidate),
364
+ aa: casesOf(job.aa)
365
+ }));
366
+ const names = [...new Set(records.flatMap((job) => [...job.candidate.keys()]))].sort();
367
+ const margins = names.map(marginOf);
368
+ const decided = /* @__PURE__ */ new Map();
369
+ const reached = /* @__PURE__ */ new Map();
370
+ const each = level / looks.length;
371
+ for (const rounds of looks) {
372
+ const reads = names.map((name, i) => records.map((job) => jobRead(job.job, differences(job.candidate.get(name) ?? [], job.aa.get(name) ?? [], rounds), margins[i], rounds)));
373
+ const joined = (pick) => reads.map((list) => list.length < need ? 1 : partialConjunction(list.map(pick), need));
374
+ const within = holm(joined((read) => read.notSlower), each);
375
+ const beyond = holm(joined((read) => read.slower), each);
376
+ names.forEach((name, i) => {
377
+ const list = reads[i];
378
+ if (!reached.has(name) || list.some((read) => read.rounds >= rounds)) reached.set(name, list);
379
+ if (decided.has(name)) return;
380
+ if (within[i]?.reject === true) decided.set(name, {
381
+ outcome: "within",
382
+ look: rounds,
383
+ reads: list
384
+ });
385
+ else if (beyond[i]?.reject === true) decided.set(name, {
386
+ outcome: "beyond",
387
+ look: rounds,
388
+ reads: list
389
+ });
390
+ });
391
+ }
392
+ const cases = names.map((name, i) => {
393
+ const decision = decided.get(name);
394
+ const outcome = decision?.outcome ?? "cannot decide";
395
+ const reads = decision?.reads ?? reached.get(name) ?? [];
396
+ const facts = reads.map(jobFacts).join(", ");
397
+ const where = decision === void 0 ? `no look of ${looks.join(", ")} rounds decides it` : `the look of ${decision.look} rounds decides it`;
398
+ return {
399
+ case: name,
400
+ outcome,
401
+ read: READ_OF[outcome],
402
+ margin: margins[i],
403
+ look: decision?.look,
404
+ jobs: reads,
405
+ reason: `The case ${JSON.stringify(name)} reads "${READ_OF[outcome]}": ${where} (${facts}).`
406
+ };
407
+ });
408
+ const more = cases.flatMap((c) => {
409
+ if (c.outcome !== "cannot decide") return [];
410
+ const next = looks.find((rounds) => records.some((job) => differences(job.candidate.get(c.case) ?? [], job.aa.get(c.case) ?? [], rounds).length < rounds));
411
+ return next === void 0 ? [] : [{
412
+ case: c.case,
413
+ rounds: next
414
+ }];
415
+ });
416
+ return {
417
+ ...gateOf(cases),
418
+ level,
419
+ looks,
420
+ need,
421
+ cases,
422
+ more
423
+ };
424
+ }
215
425
  // src/control.ts
216
426
  /**
217
427
  * The load factor of a run, read from a control. Runs on shared machines differ by load. A change does not touch a control.
@@ -1660,6 +1870,6 @@ function timeRounds(arms, options) {
1660
1870
  sink
1661
1871
  };
1662
1872
  }
1663
- export { BUDGET_LOOKS, LADDER_FORMAT, aaBand, aaSpread, armVerdicts, balanceOrders, budgetLookSpend, budgetLooksVerdict, budgetVerdict, caseIntervals, caseRatios, checkLadder, checkMeasures, checkPairedTime, controlFactor, firstArm, ladderVerdict, limitVerdict, poolRecords, pooledBand, pooledLimitVerdict, pooledVerdict, rungVerdict, timeRounds, timeVerdict, verdictMargin, verdictPower, winClaim };
1873
+ export { BUDGET_LOOKS, GATE_LOOKS, LADDER_FORMAT, aaBand, aaSpread, armVerdicts, balanceOrders, budgetLookSpend, budgetLooksVerdict, budgetVerdict, caseIntervals, caseRatios, checkLadder, checkMeasures, checkPairedTime, controlFactor, firstArm, gateOf, ladderVerdict, limitVerdict, notSlowerVerdict, poolRecords, pooledBand, pooledLimitVerdict, pooledVerdict, rungVerdict, timeRounds, timeVerdict, verdictMargin, verdictPower, winClaim };
1664
1874
 
1665
1875
  //# sourceMappingURL=index.js.map