@mark1russell7/rigor-bench 0.0.0-stage → 0.1.0

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Mark Russell
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,69 @@
1
- # Temporary Holding Version
1
+ # @mark1russell7/rigor-bench
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ The paired-time records of a bench, and the time verdict against an A/A arm. This package is the first part of the shared bench harness of rigor.
4
+
5
+ ```sh
6
+ npm install @mark1russell7/rigor-bench
7
+ ```
8
+
9
+ ## The method
10
+
11
+ - Each round runs the two arms of a pair on one case, one after the other. Arm `a` is the baseline, and arm `b` is the candidate.
12
+ - The order turns each round (`firstArm`). Thus a warm cache or a slow start does not always help the same arm.
13
+ - An A/A pair runs the same code in its two arms. Its ratios show how far apart two equal arms come on the machine.
14
+ - The ratio of a case is `b / a` over its rounds (`caseRatios`).
15
+ - The default rule `difference` gives the bootstrap interval of the candidate median over the A/A median. The verdict compares it with 1.
16
+
17
+ ## Give a verdict
18
+
19
+ ```ts
20
+ import { firstArm, timeVerdict, type PairedTime } from "@mark1russell7/rigor-bench";
21
+
22
+ const first = firstArm(round, caseIndex); // "a" or "b": run this arm first
23
+ const records: PairedTime[] = [{ case: "board-1", round: 0, first: "a", a: 12.1, b: 10.4 }];
24
+ const v = timeVerdict(records, aaRecords);
25
+
26
+ v.outcome; // "faster", "slower", "same" or "cannot decide"
27
+ v.reason; // one sentence with the interval and the band
28
+ v.comparison; // the cases that gain and drop beyond the band
29
+ ```
30
+
31
+ ## The functions
32
+
33
+ | Function | What it gives |
34
+ | --- | --- |
35
+ | `firstArm(round, offset)` | The arm that runs first. The order turns each round, and an odd offset starts with arm `b`. |
36
+ | `checkPairedTime(record)` | The record, or a `RangeError` for a time that is not a finite number above 0. |
37
+ | `caseRatios(records)` | The ratio of each case. It refuses 2 records of one round, and an order that did not turn. |
38
+ | `aaBand(aa, options)` | The band of the A/A arm, symmetric on a log scale. The kind `median` (the default) holds the A/A median, and the kind `case` holds one A/A case. It needs 5 cases or more. |
39
+ | `timeVerdict(candidate, aa, options)` | The outcome of the rule `difference` (the default) or `band`. |
40
+ | `verdictPower(options)` | The share of each outcome over seeded benches with a known ratio: the power test of a rule. |
41
+
42
+ ## The outcomes
43
+
44
+ | Outcome | The rule `difference` (the default) | The rule `band` |
45
+ | --- | --- | --- |
46
+ | `faster` | The whole interval is below 1. | The interval of the median ratio is below the band. |
47
+ | `slower` | The whole interval is above 1. | The interval is above the band. |
48
+ | `same` | The interval is in `[exp(-margin), exp(margin)]`. The default margin is 0.05. | The interval is in the band. |
49
+ | `cannot decide` | The interval holds 1 and crosses the margin. | The interval crosses an end of the band. |
50
+
51
+ Both rules give `cannot decide` when the A/A arm has fewer than 5 cases, or when the candidate has no cases.
52
+
53
+ ## The power test
54
+
55
+ `verdictPower` drew 200 seeded benches for each cell. Each case had 5 rounds. The log time of each arm in each round had a normal noise of 0.1. The bootstrap had 1000 runs.
56
+
57
+ | Rule | Cases | Finds a true speedup of 7% | Calls that speedup `same` | False `faster` or `slower` on equal arms |
58
+ | --- | --- | --- | --- | --- |
59
+ | `band`, kind `case` | 5 | 0.025 | 0.185 | 0 |
60
+ | `band`, kind `case` | 10 | 0 | 0.515 | 0 |
61
+ | `band`, kind `case` | 20 | 0 | 0.755 | 0 |
62
+ | `band`, kind `median` | 5 | 0.025 | 0.22 | 0 |
63
+ | `band`, kind `median` | 10 | 0.045 | 0.1 | 0 |
64
+ | `band`, kind `median` | 20 | 0.22 | 0.035 | 0 |
65
+ | `difference` | 5 | 0.235 | 0 | 0.04 |
66
+ | `difference` | 10 | 0.44 | 0 | 0.045 |
67
+ | `difference` | 20 | 0.71 | 0 | 0.04 |
68
+
69
+ The rule `band` compares an interval with a band of the same noise, so it needs a change of approximately twice that noise. With the kind `case`, it also calls a real speedup `same`. Thus `difference` is the default. Its false rate is near the 5% of a two-sided interval of level 0.95. A change of 7% needs approximately 20 cases for a power of 0.7.
@@ -0,0 +1,148 @@
1
+ import { BootstrapOptions, Comparison, Interval } from "@mark1russell7/rigor-statistics";
2
+ /** The two arms of a pair. Arm `a` is the baseline, and arm `b` is the candidate. */
3
+ type Arm = "a" | "b";
4
+ /** One round of one pair on one case: the times of the two arms in milliseconds, and the arm that ran first. */
5
+ interface PairedTime {
6
+ /** The name of the case: one input of the bench. */
7
+ readonly case: string;
8
+ /** The number of the round, a whole number of 0 or more. */
9
+ readonly round: number;
10
+ readonly first: Arm;
11
+ /** The time of the baseline arm, in milliseconds. */
12
+ readonly a: number;
13
+ /** The time of the candidate arm, in milliseconds. */
14
+ readonly b: number;
15
+ }
16
+ /**
17
+ * This function gives the arm that runs first in a round. The order turns each round. A case with an odd `offset`
18
+ * starts with arm `b`, so half of the cases start with each arm.
19
+ */
20
+ export declare function firstArm(round: number, offset?: number): Arm;
21
+ /** This function examines one record. A time that is not a finite number above 0 is an error. */
22
+ export declare function checkPairedTime(record: PairedTime): PairedTime;
23
+ /** The ratio of one case: the candidate time over the baseline time, over the rounds of the case. */
24
+ interface CaseRatio {
25
+ readonly case: string;
26
+ /** The value `exp(median(log(b / a)))` over the rounds. For an odd count of rounds, it is the median of the ratios. */
27
+ readonly ratio: number;
28
+ readonly rounds: number;
29
+ }
30
+ /**
31
+ * This function gives the ratio of each case, in the order of the first record of each case.
32
+ * It examines each record. It refuses a case with 2 records of one round.
33
+ * It also refuses a case whose two orders differ in count by more than 1, because that order did not turn each round.
34
+ */
35
+ export declare function caseRatios(records: readonly PairedTime[]): CaseRatio[];
36
+ /**
37
+ * The two kinds of an A/A band.
38
+ * - The band `case` holds the ratio of one A/A case. It is wide, so it finds only a large change.
39
+ * - The band `median` holds the median ratio of the A/A cases. It has the scale of the median of the candidate.
40
+ */
41
+ type BandKind = "case" | "median";
42
+ /** The options of the A/A band. */
43
+ interface BandOptions {
44
+ /** The kind of the band. The default is `median`, from the power test of `verdictPower`. */
45
+ readonly kind?: BandKind;
46
+ /** The quantile of the band `case`, or the level of the bootstrap of the band `median`. The default is 0.95. */
47
+ readonly level?: number;
48
+ /** The fewest A/A cases that give a band. The default is 5, the 5-seed rule. */
49
+ readonly minimum?: number;
50
+ /** The bootstrap of the band `median`. The level of the band replaces its level. */
51
+ readonly bootstrap?: BootstrapOptions;
52
+ }
53
+ /** The band of the A/A arm: two equal arms come out in this band of ratios. */
54
+ interface Band {
55
+ /** The count of the A/A cases. */
56
+ readonly cases: number;
57
+ readonly kind: BandKind;
58
+ /**
59
+ * The half width of the band on a log scale.
60
+ * For the band `case`, it is the `level` quantile of `abs(log(ratio))` over the A/A cases.
61
+ * For the band `median`, it is the larger distance of an end of the bootstrap interval of the A/A median from 0.
62
+ */
63
+ readonly noise: number;
64
+ /** The ends of the band: `exp(-noise)` and `exp(noise)`. */
65
+ readonly low: number;
66
+ readonly high: number;
67
+ /** The median ratio of the A/A cases. A value far from 1 shows a bias of the order or of the arm slots. */
68
+ readonly median: number;
69
+ }
70
+ /** This function gives the band of the A/A arm. The band is symmetric on a log scale. It refuses fewer cases than `minimum`. */
71
+ export declare function aaBand(aa: readonly PairedTime[], options?: BandOptions): Band;
72
+ /** The four outcomes of a time verdict. The candidate is the arm `b`, so `faster` is a ratio below 1. */
73
+ type TimeOutcome = "faster" | "slower" | "same" | "cannot decide";
74
+ /** The options of a time verdict. The bootstrap gives the interval of the median ratio of the candidate, and the band `median`. */
75
+ /**
76
+ * The two rules of a time verdict.
77
+ * - The rule `band` compares the interval of the median ratio of the candidate with the A/A band.
78
+ * - The rule `difference` compares the interval of the candidate median over the A/A median with 1 and with `margin`.
79
+ */
80
+ type VerdictRule = "band" | "difference";
81
+ /** The options of a time verdict. The bootstrap gives each interval of the verdict. */
82
+ interface VerdictOptions extends BandOptions {
83
+ /** The rule of the verdict. The default is `difference`, from the power test of `verdictPower`. */
84
+ readonly rule?: VerdictRule;
85
+ /** The half width on a log scale of the ratios that the rule `difference` calls `same`. The default is 0.05. */
86
+ readonly margin?: number;
87
+ }
88
+ /** A time verdict, with the facts that it uses. */
89
+ interface TimeVerdict {
90
+ readonly outcome: TimeOutcome;
91
+ /** The reason of the outcome, in one sentence. */
92
+ readonly reason: string;
93
+ readonly cases: readonly CaseRatio[];
94
+ readonly rule: VerdictRule;
95
+ /**
96
+ * The interval that the rule compares, or undefined when there are no candidate cases.
97
+ * With too few A/A cases, it is the interval of the median ratio of the candidate.
98
+ * For the rule `band`, it is the bootstrap interval of the median ratio of the candidate.
99
+ * For the rule `difference`, it is the interval of the candidate median ratio over the A/A median ratio.
100
+ */
101
+ readonly interval: Interval | undefined;
102
+ /** The band of the A/A arm, or undefined when the A/A arm has too few cases. */
103
+ readonly band: Band | undefined;
104
+ /**
105
+ * The counts of the cases out of the band, or undefined without a band.
106
+ * The comparison uses the value `1 + log(ratio)` of each case and the noise of the band, so its two sides are the ends of the band.
107
+ */
108
+ readonly comparison: Comparison | undefined;
109
+ }
110
+ /**
111
+ * This function gives the time verdict of the candidate arm `b` against the baseline arm `a`.
112
+ * - The rule `difference` gives `faster` when the whole interval is below 1, and `slower` when it is above 1.
113
+ * It gives `same` when the interval is in `[exp(-margin), exp(margin)]`.
114
+ * - The rule `band` gives `faster` below the band, `slower` above the band, and `same` in the band.
115
+ * - Each other case is `cannot decide`, and so is an A/A arm with too few cases.
116
+ */
117
+ export declare function timeVerdict(candidate: readonly PairedTime[], aa: readonly PairedTime[], options?: VerdictOptions): TimeVerdict;
118
+ /** The options of a power test. */
119
+ interface PowerOptions {
120
+ /** The count of the cases of the candidate and of the A/A arm. */
121
+ readonly cases: number;
122
+ /** The count of the rounds of each case. The default is 5. */
123
+ readonly rounds?: number;
124
+ /** The true ratio of the candidate time to the baseline time, for example 0.93 for a speedup of 7%. */
125
+ readonly ratio: number;
126
+ /** The standard deviation of the log time of one arm in one round. The default is 0.1. */
127
+ readonly sigma?: number;
128
+ /** The count of the seeded benches. The default is 200. */
129
+ readonly trials?: number;
130
+ /** The seed of the draws. The default is 1. */
131
+ readonly seed?: number | string;
132
+ /** The options of the verdict of each bench. */
133
+ readonly verdict?: VerdictOptions;
134
+ }
135
+ /** The result of a power test: the share of each outcome over the trials. */
136
+ interface Power {
137
+ readonly trials: number;
138
+ readonly outcomes: Readonly<Record<TimeOutcome, number>>;
139
+ /** The share of the trials whose outcome is the true one: `faster` for a ratio below 1, `slower` above 1, `same` at 1. */
140
+ readonly power: number;
141
+ }
142
+ /**
143
+ * This function draws `trials` benches and gives the share of each verdict. Each case has its own log scale.
144
+ * Each arm of each round adds a normal noise of `sigma` to the log time.
145
+ */
146
+ export declare function verdictPower(options: PowerOptions): Power;
147
+ export type { Arm, Band, BandKind, BandOptions, CaseRatio, PairedTime, Power, PowerOptions, TimeOutcome, TimeVerdict, VerdictOptions, VerdictRule };
148
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","names":[],"sources":["../src/paired.ts","../src/verdict.ts","../src/power.ts"],"mappings":";;KAOY;;UAGK;;WAEN;;WAEA;WACA,OAAO;;WAEP;;WAEA;;;;;;wBAOK,SAAS,eAAe,kBAAa;;wBAOrC,gBAAgB,QAAQ,aAAa;;UAapC;WACN;;WAEA;WACA;;;;;;;wBAQK,WAAW,kBAAkB,eAAe;;;;;;KC7ChD;;UAGK;;WAEN,OAAO;;WAEP;;WAEA;;WAEA,YAAY;;;UAIN;;WAEN;WACA,MAAM;;;;;;WAMN;;WAEA;WACA;;WAEA;;;wBAIK,OAAO,aAAa,cAAc,UAAS,cAAmB;;KAiBlE;;;;;;;KAQA;;UAGK,uBAAuB;;WAE7B,OAAO;;WAEP;;;UAIM;WACN,SAAS;;WAET;WACA,gBAAgB;WAChB,MAAM;;;;;;;WAON,UAAU;;WAEV,MAAM;;;;;WAKN,YAAY;;;;;;;;;wBAUP,YAAY,oBAAoB,cAAc,aAAa,cAAc,UAAS,iBAAsB;;UCtGvG;;WAEN;;WAEA;;WAEA;;WAEA;;WAEA;;WAEA;;WAEA,UAAU;;;UAIJ;WACN;WACA,UAAU,SAAS,OAAO;;WAE1B;;;;;;wBAOK,aAAa,SAAS,eAAe"}
package/build/index.js ADDED
@@ -0,0 +1,251 @@
1
+ import { bootstrapMedian, bootstrapTwo, compareRatios, median, quantile } from "@mark1russell7/rigor-statistics";
2
+ import { random } from "@mark1russell7/rigor-generators";
3
+ // src/paired.ts
4
+ /**
5
+ * The paired-time record of a bench. Each round runs the two arms of a pair on one case, one after the other, in a
6
+ * fresh process. The order turns each round, so a warm cache or a slow start does not always help the same arm.
7
+ */
8
+ /**
9
+ * This function gives the arm that runs first in a round. The order turns each round. A case with an odd `offset`
10
+ * starts with arm `b`, so half of the cases start with each arm.
11
+ */
12
+ function firstArm(round, offset = 0) {
13
+ if (!Number.isSafeInteger(round) || round < 0) throw new RangeError(`firstArm: the round is a whole number of 0 or more, not ${round}.`);
14
+ if (!Number.isSafeInteger(offset)) throw new RangeError(`firstArm: the offset is a whole number, not ${offset}.`);
15
+ return ((round + offset) % 2 + 2) % 2 === 0 ? "a" : "b";
16
+ }
17
+ /** This function examines one record. A time that is not a finite number above 0 is an error. */
18
+ function checkPairedTime(record) {
19
+ const where = `the record of the case ${JSON.stringify(record.case)}, round ${record.round}`;
20
+ if (typeof record.case !== "string" || record.case === "") throw new RangeError("checkPairedTime: the case is a string that is not empty.");
21
+ if (!Number.isSafeInteger(record.round) || record.round < 0) throw new RangeError(`checkPairedTime: in ${where}, the round is a whole number of 0 or more.`);
22
+ if (record.first !== "a" && record.first !== "b") throw new RangeError(`checkPairedTime: in ${where}, the first arm is "a" or "b", not ${String(record.first)}.`);
23
+ for (const arm of ["a", "b"]) {
24
+ const ms = record[arm];
25
+ if (!(typeof ms === "number" && Number.isFinite(ms) && ms > 0)) throw new RangeError(`checkPairedTime: in ${where}, the time of arm ${arm} is a finite number above 0, not ${String(ms)}.`);
26
+ }
27
+ return record;
28
+ }
29
+ /**
30
+ * This function gives the ratio of each case, in the order of the first record of each case.
31
+ * It examines each record. It refuses a case with 2 records of one round.
32
+ * It also refuses a case whose two orders differ in count by more than 1, because that order did not turn each round.
33
+ */
34
+ function caseRatios(records) {
35
+ const cases = /* @__PURE__ */ new Map();
36
+ for (const record of records) {
37
+ checkPairedTime(record);
38
+ const list = cases.get(record.case) ?? [];
39
+ list.push(record);
40
+ cases.set(record.case, list);
41
+ }
42
+ return [...cases].map(([name, list]) => {
43
+ if (new Set(list.map((r) => r.round)).size !== list.length) throw new RangeError(`caseRatios: the case ${JSON.stringify(name)} has 2 records of one round.`);
44
+ const aFirst = list.filter((r) => r.first === "a").length;
45
+ if (Math.abs(2 * aFirst - list.length) > 1) throw new RangeError(`caseRatios: the case ${JSON.stringify(name)} starts with arm a in ${aFirst} of ${list.length} rounds. The order turns each round.`);
46
+ return {
47
+ case: name,
48
+ ratio: Math.exp(median(list.map((r) => Math.log(r.b / r.a)))),
49
+ rounds: list.length
50
+ };
51
+ });
52
+ }
53
+ // src/verdict.ts
54
+ /**
55
+ * The time verdict of a candidate against a baseline, with the noise of an A/A arm. An A/A pair runs the same code in
56
+ * its two arms. Its ratios show how far apart two equal arms come on this machine. A candidate is faster or slower only
57
+ * when its interval is out of that band.
58
+ */
59
+ /** This function gives the band of the A/A arm. The band is symmetric on a log scale. It refuses fewer cases than `minimum`. */
60
+ function aaBand(aa, options = {}) {
61
+ const ratios = caseRatios(aa);
62
+ const minimum = options.minimum ?? 5;
63
+ if (ratios.length < minimum) throw new RangeError(`aaBand: the A/A arm has ${ratios.length} cases, and a band needs ${minimum} or more.`);
64
+ const logs = ratios.map((r) => Math.log(r.ratio));
65
+ const kind = options.kind ?? "median";
66
+ const level = options.level ?? .95;
67
+ let noise;
68
+ if (kind === "case") noise = quantile(logs.map(Math.abs), level);
69
+ else {
70
+ const interval = bootstrapMedian(logs, {
71
+ ...options.bootstrap,
72
+ level
73
+ });
74
+ noise = Math.max(Math.abs(interval.low), Math.abs(interval.high));
75
+ }
76
+ return {
77
+ cases: ratios.length,
78
+ kind,
79
+ noise,
80
+ low: Math.exp(-noise),
81
+ high: Math.exp(noise),
82
+ median: Math.exp(quantile(logs, .5))
83
+ };
84
+ }
85
+ /**
86
+ * This function gives the time verdict of the candidate arm `b` against the baseline arm `a`.
87
+ * - The rule `difference` gives `faster` when the whole interval is below 1, and `slower` when it is above 1.
88
+ * It gives `same` when the interval is in `[exp(-margin), exp(margin)]`.
89
+ * - The rule `band` gives `faster` below the band, `slower` above the band, and `same` in the band.
90
+ * - Each other case is `cannot decide`, and so is an A/A arm with too few cases.
91
+ */
92
+ function timeVerdict(candidate, aa, options = {}) {
93
+ const rule = options.rule ?? "difference";
94
+ const cases = caseRatios(candidate);
95
+ const aaRatios = caseRatios(aa);
96
+ const minimum = options.minimum ?? 5;
97
+ const none = {
98
+ cases,
99
+ rule,
100
+ interval: void 0,
101
+ band: void 0,
102
+ comparison: void 0
103
+ };
104
+ if (cases.length === 0) return {
105
+ ...none,
106
+ outcome: "cannot decide",
107
+ reason: "The candidate has no cases."
108
+ };
109
+ if (aaRatios.length < minimum) {
110
+ const interval = bootstrapMedian(cases.map((c) => c.ratio), options.bootstrap);
111
+ return {
112
+ ...none,
113
+ interval,
114
+ outcome: "cannot decide",
115
+ reason: `The A/A arm has ${aaRatios.length} cases, and a band needs ${minimum} or more.`
116
+ };
117
+ }
118
+ const band = aaBand(aa, options);
119
+ const facts = {
120
+ cases,
121
+ rule,
122
+ band,
123
+ comparison: compareRatios(cases.map((c) => 1 + Math.log(c.ratio)), {
124
+ better: "lower",
125
+ noise: band.noise
126
+ })
127
+ };
128
+ if (rule === "difference") {
129
+ const margin = options.margin ?? .05;
130
+ if (!(margin >= 0)) throw new RangeError(`timeVerdict: the margin is 0 or more, not ${margin}.`);
131
+ const logs = cases.map((c) => Math.log(c.ratio));
132
+ const aaLogs = aaRatios.map((c) => Math.log(c.ratio));
133
+ const d = bootstrapTwo(logs, aaLogs, (x, y) => median(x) - median(y), options.bootstrap);
134
+ const interval = {
135
+ ...d,
136
+ estimate: Math.exp(d.estimate),
137
+ low: Math.exp(d.low),
138
+ high: Math.exp(d.high)
139
+ };
140
+ const span = `The interval of the candidate median over the A/A median is [${round(interval.low)}, ${round(interval.high)}].`;
141
+ if (interval.high < 1) return {
142
+ ...facts,
143
+ interval,
144
+ outcome: "faster",
145
+ reason: `${span} The interval is below 1.`
146
+ };
147
+ if (interval.low > 1) return {
148
+ ...facts,
149
+ interval,
150
+ outcome: "slower",
151
+ reason: `${span} The interval is above 1.`
152
+ };
153
+ if (d.low >= -margin && d.high <= margin) return {
154
+ ...facts,
155
+ interval,
156
+ outcome: "same",
157
+ reason: `${span} The interval is in the margin of ${margin}.`
158
+ };
159
+ return {
160
+ ...facts,
161
+ interval,
162
+ outcome: "cannot decide",
163
+ reason: `${span} The interval holds 1 and crosses the margin of ${margin}.`
164
+ };
165
+ }
166
+ const interval = bootstrapMedian(cases.map((c) => c.ratio), options.bootstrap);
167
+ const span = `The interval of the median ratio is [${round(interval.low)}, ${round(interval.high)}], and the A/A band is [${round(band.low)}, ${round(band.high)}].`;
168
+ if (interval.high < band.low) return {
169
+ ...facts,
170
+ interval,
171
+ outcome: "faster",
172
+ reason: `${span} The interval is below the band.`
173
+ };
174
+ if (interval.low > band.high) return {
175
+ ...facts,
176
+ interval,
177
+ outcome: "slower",
178
+ reason: `${span} The interval is above the band.`
179
+ };
180
+ if (interval.low >= band.low && interval.high <= band.high) return {
181
+ ...facts,
182
+ interval,
183
+ outcome: "same",
184
+ reason: `${span} The interval is in the band.`
185
+ };
186
+ return {
187
+ ...facts,
188
+ interval,
189
+ outcome: "cannot decide",
190
+ reason: `${span} The interval crosses an end of the band.`
191
+ };
192
+ }
193
+ function round(x) {
194
+ return Math.round(x * 1e4) / 1e4;
195
+ }
196
+ // src/power.ts
197
+ /**
198
+ * The power test of a time verdict. It draws seeded benches with a known speedup and counts the verdicts that find it.
199
+ * It also draws benches with equal arms and counts the false verdicts.
200
+ */
201
+ /**
202
+ * This function draws `trials` benches and gives the share of each verdict. Each case has its own log scale.
203
+ * Each arm of each round adds a normal noise of `sigma` to the log time.
204
+ */
205
+ function verdictPower(options) {
206
+ const rounds = options.rounds ?? 5;
207
+ const sigma = options.sigma ?? .1;
208
+ const trials = options.trials ?? 200;
209
+ if (!Number.isSafeInteger(options.cases) || options.cases < 1) throw new RangeError(`verdictPower: cases is a whole number of 1 or more, not ${options.cases}.`);
210
+ if (!(options.ratio > 0 && Number.isFinite(options.ratio))) throw new RangeError(`verdictPower: the ratio is a finite number above 0, not ${options.ratio}.`);
211
+ const r = random(options.seed ?? 1);
212
+ const bench = (ratio, prefix) => {
213
+ const out = [];
214
+ for (let c = 0; c < options.cases; c++) {
215
+ const scale = 10 * Math.exp(r.normal());
216
+ for (let round = 0; round < rounds; round++) {
217
+ const a = scale * Math.exp(r.normal(0, sigma));
218
+ const b = scale * ratio * Math.exp(r.normal(0, sigma));
219
+ out.push({
220
+ case: `${prefix}${c}`,
221
+ round,
222
+ first: firstArm(round, c),
223
+ a,
224
+ b
225
+ });
226
+ }
227
+ }
228
+ return out;
229
+ };
230
+ const truth = options.ratio < 1 ? "faster" : options.ratio > 1 ? "slower" : "same";
231
+ const counts = {
232
+ faster: 0,
233
+ slower: 0,
234
+ same: 0,
235
+ "cannot decide": 0
236
+ };
237
+ for (let t = 0; t < trials; t++) {
238
+ const aa = bench(1, "aa");
239
+ const candidate = bench(options.ratio, "c");
240
+ counts[timeVerdict(candidate, aa, options.verdict).outcome]++;
241
+ }
242
+ const outcomes = Object.fromEntries(Object.entries(counts).map(([k, n]) => [k, n / trials]));
243
+ return {
244
+ trials,
245
+ outcomes,
246
+ power: outcomes[truth]
247
+ };
248
+ }
249
+ export { aaBand, caseRatios, checkPairedTime, firstArm, timeVerdict, verdictPower };
250
+
251
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","names":[],"sources":["../src/paired.ts","../src/verdict.ts","../src/power.ts"],"sourcesContent":["/**\n * The paired-time record of a bench. Each round runs the two arms of a pair on one case, one after the other, in a\n * fresh process. The order turns each round, so a warm cache or a slow start does not always help the same arm.\n */\nimport { median } from \"@mark1russell7/rigor-statistics\";\n\n/** The two arms of a pair. Arm `a` is the baseline, and arm `b` is the candidate. */\nexport type Arm = \"a\" | \"b\";\n\n/** One round of one pair on one case: the times of the two arms in milliseconds, and the arm that ran first. */\nexport interface PairedTime {\n /** The name of the case: one input of the bench. */\n readonly case: string;\n /** The number of the round, a whole number of 0 or more. */\n readonly round: number;\n readonly first: Arm;\n /** The time of the baseline arm, in milliseconds. */\n readonly a: number;\n /** The time of the candidate arm, in milliseconds. */\n readonly b: number;\n}\n\n/**\n * This function gives the arm that runs first in a round. The order turns each round. A case with an odd `offset`\n * starts with arm `b`, so half of the cases start with each arm.\n */\nexport function firstArm(round: number, offset = 0): Arm {\n if (!Number.isSafeInteger(round) || round < 0) throw new RangeError(`firstArm: the round is a whole number of 0 or more, not ${round}.`);\n if (!Number.isSafeInteger(offset)) throw new RangeError(`firstArm: the offset is a whole number, not ${offset}.`);\n return (((round + offset) % 2) + 2) % 2 === 0 ? \"a\" : \"b\";\n}\n\n/** This function examines one record. A time that is not a finite number above 0 is an error. */\nexport function checkPairedTime(record: PairedTime): PairedTime {\n const where = `the record of the case ${JSON.stringify(record.case)}, round ${record.round}`;\n if (typeof record.case !== \"string\" || record.case === \"\") throw new RangeError(\"checkPairedTime: the case is a string that is not empty.\");\n if (!Number.isSafeInteger(record.round) || record.round < 0) throw new RangeError(`checkPairedTime: in ${where}, the round is a whole number of 0 or more.`);\n if (record.first !== \"a\" && record.first !== \"b\") throw new RangeError(`checkPairedTime: in ${where}, the first arm is \"a\" or \"b\", not ${String(record.first)}.`);\n for (const arm of [\"a\", \"b\"] as const) {\n const ms = record[arm];\n if (!(typeof ms === \"number\" && Number.isFinite(ms) && ms > 0)) throw new RangeError(`checkPairedTime: in ${where}, the time of arm ${arm} is a finite number above 0, not ${String(ms)}.`);\n }\n return record;\n}\n\n/** The ratio of one case: the candidate time over the baseline time, over the rounds of the case. */\nexport interface CaseRatio {\n readonly case: string;\n /** The value `exp(median(log(b / a)))` over the rounds. For an odd count of rounds, it is the median of the ratios. */\n readonly ratio: number;\n readonly rounds: number;\n}\n\n/**\n * This function gives the ratio of each case, in the order of the first record of each case.\n * It examines each record. It refuses a case with 2 records of one round.\n * It also refuses a case whose two orders differ in count by more than 1, because that order did not turn each round.\n */\nexport function caseRatios(records: readonly PairedTime[]): CaseRatio[] {\n const cases = new Map<string, PairedTime[]>();\n for (const record of records) {\n checkPairedTime(record);\n const list = cases.get(record.case) ?? [];\n list.push(record);\n cases.set(record.case, list);\n }\n return [...cases].map(([name, list]) => {\n const rounds = new Set(list.map((r) => r.round));\n if (rounds.size !== list.length) throw new RangeError(`caseRatios: the case ${JSON.stringify(name)} has 2 records of one round.`);\n const aFirst = list.filter((r) => r.first === \"a\").length;\n if (Math.abs(2 * aFirst - list.length) > 1) {\n throw new RangeError(`caseRatios: the case ${JSON.stringify(name)} starts with arm a in ${aFirst} of ${list.length} rounds. The order turns each round.`);\n }\n return { case: name, ratio: Math.exp(median(list.map((r) => Math.log(r.b / r.a)))), rounds: list.length };\n });\n}\n","/**\n * The time verdict of a candidate against a baseline, with the noise of an A/A arm. An A/A pair runs the same code in\n * its two arms. Its ratios show how far apart two equal arms come on this machine. A candidate is faster or slower only\n * when its interval is out of that band.\n */\nimport { type BootstrapOptions, type Comparison, type Interval, bootstrapMedian, bootstrapTwo, compareRatios, median, quantile } from \"@mark1russell7/rigor-statistics\";\nimport { type CaseRatio, type PairedTime, caseRatios } from \"./paired.ts\";\n\n/**\n * The two kinds of an A/A band.\n * - The band `case` holds the ratio of one A/A case. It is wide, so it finds only a large change.\n * - The band `median` holds the median ratio of the A/A cases. It has the scale of the median of the candidate.\n */\nexport type BandKind = \"case\" | \"median\";\n\n/** The options of the A/A band. */\nexport interface BandOptions {\n /** The kind of the band. The default is `median`, from the power test of `verdictPower`. */\n readonly kind?: BandKind;\n /** The quantile of the band `case`, or the level of the bootstrap of the band `median`. The default is 0.95. */\n readonly level?: number;\n /** The fewest A/A cases that give a band. The default is 5, the 5-seed rule. */\n readonly minimum?: number;\n /** The bootstrap of the band `median`. The level of the band replaces its level. */\n readonly bootstrap?: BootstrapOptions;\n}\n\n/** The band of the A/A arm: two equal arms come out in this band of ratios. */\nexport interface Band {\n /** The count of the A/A cases. */\n readonly cases: number;\n readonly kind: BandKind;\n /**\n * The half width of the band on a log scale.\n * For the band `case`, it is the `level` quantile of `abs(log(ratio))` over the A/A cases.\n * For the band `median`, it is the larger distance of an end of the bootstrap interval of the A/A median from 0.\n */\n readonly noise: number;\n /** The ends of the band: `exp(-noise)` and `exp(noise)`. */\n readonly low: number;\n readonly high: number;\n /** The median ratio of the A/A cases. A value far from 1 shows a bias of the order or of the arm slots. */\n readonly median: number;\n}\n\n/** This function gives the band of the A/A arm. The band is symmetric on a log scale. It refuses fewer cases than `minimum`. */\nexport function aaBand(aa: readonly PairedTime[], options: BandOptions = {}): Band {\n const ratios = caseRatios(aa);\n const minimum = options.minimum ?? 5;\n if (ratios.length < minimum) throw new RangeError(`aaBand: the A/A arm has ${ratios.length} cases, and a band needs ${minimum} or more.`);\n const logs = ratios.map((r) => Math.log(r.ratio));\n const kind = options.kind ?? \"median\";\n const level = options.level ?? 0.95;\n let noise: number;\n if (kind === \"case\") noise = quantile(logs.map(Math.abs), level);\n else {\n const interval = bootstrapMedian(logs, { ...options.bootstrap, level });\n noise = Math.max(Math.abs(interval.low), Math.abs(interval.high));\n }\n return { cases: ratios.length, kind, noise, low: Math.exp(-noise), high: Math.exp(noise), median: Math.exp(quantile(logs, 0.5)) };\n}\n\n/** The four outcomes of a time verdict. The candidate is the arm `b`, so `faster` is a ratio below 1. */\nexport type TimeOutcome = \"faster\" | \"slower\" | \"same\" | \"cannot decide\";\n\n/** The options of a time verdict. The bootstrap gives the interval of the median ratio of the candidate, and the band `median`. */\n/**\n * The two rules of a time verdict.\n * - The rule `band` compares the interval of the median ratio of the candidate with the A/A band.\n * - The rule `difference` compares the interval of the candidate median over the A/A median with 1 and with `margin`.\n */\nexport type VerdictRule = \"band\" | \"difference\";\n\n/** The options of a time verdict. The bootstrap gives each interval of the verdict. */\nexport interface VerdictOptions extends BandOptions {\n /** The rule of the verdict. The default is `difference`, from the power test of `verdictPower`. */\n readonly rule?: VerdictRule;\n /** The half width on a log scale of the ratios that the rule `difference` calls `same`. The default is 0.05. */\n readonly margin?: number;\n}\n\n/** A time verdict, with the facts that it uses. */\nexport interface TimeVerdict {\n readonly outcome: TimeOutcome;\n /** The reason of the outcome, in one sentence. */\n readonly reason: string;\n readonly cases: readonly CaseRatio[];\n readonly rule: VerdictRule;\n /**\n * The interval that the rule compares, or undefined when there are no candidate cases.\n * With too few A/A cases, it is the interval of the median ratio of the candidate.\n * For the rule `band`, it is the bootstrap interval of the median ratio of the candidate.\n * For the rule `difference`, it is the interval of the candidate median ratio over the A/A median ratio.\n */\n readonly interval: Interval | undefined;\n /** The band of the A/A arm, or undefined when the A/A arm has too few cases. */\n readonly band: Band | undefined;\n /**\n * The counts of the cases out of the band, or undefined without a band.\n * The comparison uses the value `1 + log(ratio)` of each case and the noise of the band, so its two sides are the ends of the band.\n */\n readonly comparison: Comparison | undefined;\n}\n\n/**\n * This function gives the time verdict of the candidate arm `b` against the baseline arm `a`.\n * - The rule `difference` gives `faster` when the whole interval is below 1, and `slower` when it is above 1.\n * It gives `same` when the interval is in `[exp(-margin), exp(margin)]`.\n * - The rule `band` gives `faster` below the band, `slower` above the band, and `same` in the band.\n * - Each other case is `cannot decide`, and so is an A/A arm with too few cases.\n */\nexport function timeVerdict(candidate: readonly PairedTime[], aa: readonly PairedTime[], options: VerdictOptions = {}): TimeVerdict {\n const rule = options.rule ?? \"difference\";\n const cases = caseRatios(candidate);\n const aaRatios = caseRatios(aa);\n const minimum = options.minimum ?? 5;\n const none = { cases, rule, interval: undefined, band: undefined, comparison: undefined };\n if (cases.length === 0) return { ...none, outcome: \"cannot decide\", reason: \"The candidate has no cases.\" };\n if (aaRatios.length < minimum) {\n const interval = bootstrapMedian(cases.map((c) => c.ratio), options.bootstrap);\n return { ...none, interval, outcome: \"cannot decide\", reason: `The A/A arm has ${aaRatios.length} cases, and a band needs ${minimum} or more.` };\n }\n const band = aaBand(aa, options);\n const comparison = compareRatios(cases.map((c) => 1 + Math.log(c.ratio)), { better: \"lower\", noise: band.noise });\n const facts = { cases, rule, band, comparison };\n if (rule === \"difference\") {\n const margin = options.margin ?? 0.05;\n if (!(margin >= 0)) throw new RangeError(`timeVerdict: the margin is 0 or more, not ${margin}.`);\n const logs = cases.map((c) => Math.log(c.ratio));\n const aaLogs = aaRatios.map((c) => Math.log(c.ratio));\n const d = bootstrapTwo(logs, aaLogs, (x, y) => median(x) - median(y), options.bootstrap);\n const interval: Interval = { ...d, estimate: Math.exp(d.estimate), low: Math.exp(d.low), high: Math.exp(d.high) };\n const span = `The interval of the candidate median over the A/A median is [${round(interval.low)}, ${round(interval.high)}].`;\n if (interval.high < 1) return { ...facts, interval, outcome: \"faster\", reason: `${span} The interval is below 1.` };\n if (interval.low > 1) return { ...facts, interval, outcome: \"slower\", reason: `${span} The interval is above 1.` };\n if (d.low >= -margin && d.high <= margin) return { ...facts, interval, outcome: \"same\", reason: `${span} The interval is in the margin of ${margin}.` };\n return { ...facts, interval, outcome: \"cannot decide\", reason: `${span} The interval holds 1 and crosses the margin of ${margin}.` };\n }\n const interval = bootstrapMedian(cases.map((c) => c.ratio), options.bootstrap);\n const span = `The interval of the median ratio is [${round(interval.low)}, ${round(interval.high)}], and the A/A band is [${round(band.low)}, ${round(band.high)}].`;\n if (interval.high < band.low) return { ...facts, interval, outcome: \"faster\", reason: `${span} The interval is below the band.` };\n if (interval.low > band.high) return { ...facts, interval, outcome: \"slower\", reason: `${span} The interval is above the band.` };\n if (interval.low >= band.low && interval.high <= band.high) return { ...facts, interval, outcome: \"same\", reason: `${span} The interval is in the band.` };\n return { ...facts, interval, outcome: \"cannot decide\", reason: `${span} The interval crosses an end of the band.` };\n}\n\nfunction round(x: number): number {\n return Math.round(x * 10_000) / 10_000;\n}\n","/**\n * The power test of a time verdict. It draws seeded benches with a known speedup and counts the verdicts that find it.\n * It also draws benches with equal arms and counts the false verdicts.\n */\nimport { random } from \"@mark1russell7/rigor-generators\";\nimport { firstArm, type PairedTime } from \"./paired.ts\";\nimport { type TimeOutcome, type VerdictOptions, timeVerdict } from \"./verdict.ts\";\n\n/** The options of a power test. */\nexport interface PowerOptions {\n /** The count of the cases of the candidate and of the A/A arm. */\n readonly cases: number;\n /** The count of the rounds of each case. The default is 5. */\n readonly rounds?: number;\n /** The true ratio of the candidate time to the baseline time, for example 0.93 for a speedup of 7%. */\n readonly ratio: number;\n /** The standard deviation of the log time of one arm in one round. The default is 0.1. */\n readonly sigma?: number;\n /** The count of the seeded benches. The default is 200. */\n readonly trials?: number;\n /** The seed of the draws. The default is 1. */\n readonly seed?: number | string;\n /** The options of the verdict of each bench. */\n readonly verdict?: VerdictOptions;\n}\n\n/** The result of a power test: the share of each outcome over the trials. */\nexport interface Power {\n readonly trials: number;\n readonly outcomes: Readonly<Record<TimeOutcome, number>>;\n /** The share of the trials whose outcome is the true one: `faster` for a ratio below 1, `slower` above 1, `same` at 1. */\n readonly power: number;\n}\n\n/**\n * This function draws `trials` benches and gives the share of each verdict. Each case has its own log scale.\n * Each arm of each round adds a normal noise of `sigma` to the log time.\n */\nexport function verdictPower(options: PowerOptions): Power {\n const rounds = options.rounds ?? 5;\n const sigma = options.sigma ?? 0.1;\n const trials = options.trials ?? 200;\n if (!Number.isSafeInteger(options.cases) || options.cases < 1) throw new RangeError(`verdictPower: cases is a whole number of 1 or more, not ${options.cases}.`);\n if (!(options.ratio > 0 && Number.isFinite(options.ratio))) throw new RangeError(`verdictPower: the ratio is a finite number above 0, not ${options.ratio}.`);\n const r = random(options.seed ?? 1);\n const bench = (ratio: number, prefix: string): PairedTime[] => {\n const out: PairedTime[] = [];\n for (let c = 0; c < options.cases; c++) {\n const scale = 10 * Math.exp(r.normal());\n for (let round = 0; round < rounds; round++) {\n const a = scale * Math.exp(r.normal(0, sigma));\n const b = scale * ratio * Math.exp(r.normal(0, sigma));\n out.push({ case: `${prefix}${c}`, round, first: firstArm(round, c), a, b });\n }\n }\n return out;\n };\n const truth: TimeOutcome = options.ratio < 1 ? \"faster\" : options.ratio > 1 ? \"slower\" : \"same\";\n const counts: Record<TimeOutcome, number> = { faster: 0, slower: 0, same: 0, \"cannot decide\": 0 };\n for (let t = 0; t < trials; t++) {\n const aa = bench(1, \"aa\");\n const candidate = bench(options.ratio, \"c\");\n counts[timeVerdict(candidate, aa, options.verdict).outcome]++;\n }\n const outcomes = Object.fromEntries(Object.entries(counts).map(([k, n]) => [k, n / trials])) as Record<TimeOutcome, number>;\n return { trials, outcomes, power: outcomes[truth] };\n}\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,62 @@
1
1
  {
2
+ "$schema": "https://json.schemastore.org/package",
2
3
  "name": "@mark1russell7/rigor-bench",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
4
+ "version": "0.1.0",
5
+ "description": "The paired-time records of a bench: the order that turns each round, the ratio of each case, and the time verdict against the band of an A/A arm.",
6
+ "keywords": [
7
+ "benchmark",
8
+ "paired",
9
+ "a/a",
10
+ "noise",
11
+ "bootstrap",
12
+ "performance",
13
+ "timing"
14
+ ],
15
+ "homepage": "https://github.com/mark1russell7/rigor/tree/main/packages/bench#readme",
16
+ "bugs": {
17
+ "url": "https://github.com/mark1russell7/rigor/issues"
18
+ },
19
+ "repository": {
20
+ "type": "git",
21
+ "url": "git+https://github.com/mark1russell7/rigor.git",
22
+ "directory": "packages/bench"
23
+ },
24
+ "license": "MIT",
25
+ "author": "Mark Russell (https://github.com/mark1russell7)",
26
+ "type": "module",
27
+ "main": "./build/index.js",
28
+ "files": [
29
+ "build"
30
+ ],
31
+ "publishConfig": {
32
+ "access": "public"
33
+ },
34
+ "engines": {
35
+ "node": ">=22"
36
+ },
37
+ "dependencies": {
38
+ "@mark1russell7/rigor-statistics": "^0.1.3"
39
+ },
40
+ "devDependencies": {
41
+ "@mark1russell7/rigor-generators": "^0.1.0",
42
+ "@mark1russell7/cue": "^1.0.2",
43
+ "@types/node": "^26.6.5",
44
+ "tsdown": "^0.23.1",
45
+ "typescript": "^7.0.2",
46
+ "vitest": "^5.0.3"
47
+ },
48
+ "scripts": {
49
+ "build": "tsdown",
50
+ "test": "vitest run",
51
+ "test:watch": "vitest",
52
+ "typecheck": "tsc -p tsconfig.json && tsc -p tsconfig.test.json"
53
+ },
54
+ "types": "./build/index.d.ts",
55
+ "exports": {
56
+ ".": {
57
+ "types": "./build/index.d.ts",
58
+ "import": "./build/index.js"
59
+ },
60
+ "./package.json": "./package.json"
61
+ }
6
62
  }