peeksafe 0.2.0 → 0.4.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/CHANGELOG.md +91 -0
- package/README.md +32 -7
- package/dist/check.d.ts.map +1 -1
- package/dist/check.js +11 -8
- package/dist/check.js.map +1 -1
- package/dist/cluster.d.ts.map +1 -1
- package/dist/cluster.js +11 -4
- package/dist/cluster.js.map +1 -1
- package/dist/frontier.d.ts +5 -4
- package/dist/frontier.d.ts.map +1 -1
- package/dist/frontier.js +30 -31
- package/dist/frontier.js.map +1 -1
- package/dist/gate.d.ts +18 -0
- package/dist/gate.d.ts.map +1 -1
- package/dist/gate.js +32 -3
- package/dist/gate.js.map +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -1
- package/dist/index.js.map +1 -1
- package/dist/plan.d.ts +37 -20
- package/dist/plan.d.ts.map +1 -1
- package/dist/plan.js +167 -73
- package/dist/plan.js.map +1 -1
- package/dist/stats.d.ts +1 -1
- package/dist/stats.d.ts.map +1 -1
- package/dist/stats.js +56 -6
- package/dist/stats.js.map +1 -1
- package/dist/stop.d.ts +0 -1
- package/dist/stop.d.ts.map +1 -1
- package/dist/stop.js +1 -1
- package/dist/stop.js.map +1 -1
- package/package.json +11 -2
- package/src/check.ts +12 -8
- package/src/cluster.ts +12 -4
- package/src/frontier.ts +39 -34
- package/src/gate.ts +50 -3
- package/src/index.ts +5 -0
- package/src/plan.ts +178 -78
- package/src/stats.ts +70 -6
- package/src/stop.ts +1 -1
package/src/stats.ts
CHANGED
|
@@ -115,13 +115,30 @@ export function gammaP(a: number, x: number): number {
|
|
|
115
115
|
* approximation is visible in a p-value and we compare p-values to thresholds.
|
|
116
116
|
*/
|
|
117
117
|
export function erf(x: number): number {
|
|
118
|
+
requireNumber(x, "x", "erf");
|
|
118
119
|
if (x === 0) return 0;
|
|
119
120
|
const p = gammaP(0.5, x * x);
|
|
120
121
|
return x > 0 ? p : -p;
|
|
121
122
|
}
|
|
122
123
|
|
|
123
124
|
/** Φ(x): standard normal CDF. */
|
|
124
|
-
export
|
|
125
|
+
export function normalCdf(x: number): number {
|
|
126
|
+
requireNumber(x, "x", "normalCdf");
|
|
127
|
+
return 0.5 * (1 + erf(x / Math.SQRT2));
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/** Refuses NaN only: erf and normalCdf answer the infinities as their limits. */
|
|
131
|
+
function requireNumber(x: number, name: string, where: string): void {
|
|
132
|
+
if (Number.isNaN(x)) {
|
|
133
|
+
throw new PeeksafeError(
|
|
134
|
+
"PEEKSAFE_E_STAT_DOMAIN",
|
|
135
|
+
`${where}: ${name} must be a number, got NaN`,
|
|
136
|
+
{
|
|
137
|
+
detail: { [name]: x },
|
|
138
|
+
},
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
125
142
|
|
|
126
143
|
/** Φ⁻¹(p): Acklam's inverse normal CDF, refined by one Halley step. */
|
|
127
144
|
export function normalQuantile(p: number): number {
|
|
@@ -330,10 +347,16 @@ export function betaPosterior(
|
|
|
330
347
|
prior: { alpha: number; beta: number } = { alpha: 1, beta: 1 },
|
|
331
348
|
): BetaPosterior {
|
|
332
349
|
requireCounts(successes, successes + failures, "betaPosterior");
|
|
333
|
-
|
|
350
|
+
// An infinite shape passed `> 0` and came back as NaN moments.
|
|
351
|
+
if (
|
|
352
|
+
!(prior.alpha > 0) ||
|
|
353
|
+
!(prior.beta > 0) ||
|
|
354
|
+
!Number.isFinite(prior.alpha) ||
|
|
355
|
+
!Number.isFinite(prior.beta)
|
|
356
|
+
) {
|
|
334
357
|
throw new PeeksafeError(
|
|
335
358
|
"PEEKSAFE_E_STAT_DOMAIN",
|
|
336
|
-
"betaPosterior: prior parameters must be positive",
|
|
359
|
+
"betaPosterior: prior parameters must be finite and positive",
|
|
337
360
|
{
|
|
338
361
|
detail: { prior },
|
|
339
362
|
},
|
|
@@ -424,8 +447,27 @@ export function tQuantile(p: number, df: number): number {
|
|
|
424
447
|
return Math.sqrt((df * (1 - x)) / x);
|
|
425
448
|
}
|
|
426
449
|
|
|
450
|
+
// A posterior built by hand reached betaQuantile or ibeta unchecked and was refused in their name.
|
|
451
|
+
function requirePosteriorShape(post: BetaPosterior, where: string): void {
|
|
452
|
+
if (
|
|
453
|
+
!(post.alpha > 0) ||
|
|
454
|
+
!(post.beta > 0) ||
|
|
455
|
+
!Number.isFinite(post.alpha) ||
|
|
456
|
+
!Number.isFinite(post.beta)
|
|
457
|
+
) {
|
|
458
|
+
throw new PeeksafeError(
|
|
459
|
+
"PEEKSAFE_E_STAT_DOMAIN",
|
|
460
|
+
`${where}: posterior alpha and beta must be finite and positive`,
|
|
461
|
+
{
|
|
462
|
+
detail: { alpha: post.alpha, beta: post.beta },
|
|
463
|
+
},
|
|
464
|
+
);
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
|
|
427
468
|
/** Equal-tailed credible interval of a Beta posterior. */
|
|
428
469
|
export function betaCredibleInterval(post: BetaPosterior, level = 0.95): Interval {
|
|
470
|
+
requirePosteriorShape(post, "betaCredibleInterval");
|
|
429
471
|
// betaQuantile clamps q to [0, 1], so a level of 95 or -1 came back as [0, 1] or [1, 0].
|
|
430
472
|
requireOpenProbability(level, "level", "betaCredibleInterval");
|
|
431
473
|
const tail = (1 - level) / 2;
|
|
@@ -438,6 +480,7 @@ export function betaCredibleInterval(post: BetaPosterior, level = 0.95): Interva
|
|
|
438
480
|
|
|
439
481
|
/** P(a < θ < b) under a Beta posterior. */
|
|
440
482
|
export function betaMassBetween(post: BetaPosterior, a: number, b: number): number {
|
|
483
|
+
requirePosteriorShape(post, "betaMassBetween");
|
|
441
484
|
requireProbability(a, "a", "betaMassBetween");
|
|
442
485
|
requireProbability(b, "b", "betaMassBetween");
|
|
443
486
|
if (a > b) {
|
|
@@ -729,9 +772,8 @@ export function sprtExpectedN(p: number, p0: number, p1: number, alpha = 0.05, b
|
|
|
729
772
|
* outside the two hypotheses, so the two powers are evaluated relative to
|
|
730
773
|
* whichever of them is largest. Written as plain `exp`, a tilt of −662, which is
|
|
731
774
|
* p = 0.0025 against p₀ = 0.999 and p₁ = 0.99, overflows `B^h` to Infinity and
|
|
732
|
-
* the ratio comes back NaN
|
|
733
|
-
*
|
|
734
|
-
* silently prices the case at `maxTrials`.
|
|
775
|
+
* the ratio comes back NaN, which a caller testing the result for `> 0` reads
|
|
776
|
+
* as no estimate at all.
|
|
735
777
|
*/
|
|
736
778
|
function sprtAcceptH1(h: number, A: number, B: number, drift: number): number {
|
|
737
779
|
// No tilt exists at p = 0 or p = 1: one outcome is impossible, so the drift
|
|
@@ -1071,6 +1113,8 @@ export function bernoulliEntropy(p: number): number {
|
|
|
1071
1113
|
/** log of the Beta(a,b) density at p. */
|
|
1072
1114
|
export function logBetaPdf(p: number, a: number, b: number): number {
|
|
1073
1115
|
requireProbability(p, "p", "logBetaPdf");
|
|
1116
|
+
requireConcentration(a, "a", "logBetaPdf");
|
|
1117
|
+
requireConcentration(b, "b", "logBetaPdf");
|
|
1074
1118
|
if (p <= 0 || p >= 1) {
|
|
1075
1119
|
// density is 0 or ∞ at the boundary depending on the shape; the planner
|
|
1076
1120
|
// only ever evaluates strictly inside, so clamp rather than return ±∞.
|
|
@@ -1146,6 +1190,9 @@ export function evidenceCeilingLogE(
|
|
|
1146
1190
|
altConcentration = 8,
|
|
1147
1191
|
): number {
|
|
1148
1192
|
requireProbability(pTrue, "pTrue", "evidenceCeilingLogE");
|
|
1193
|
+
requireCounts(baselineSuccesses, baselineTrials, "evidenceCeilingLogE");
|
|
1194
|
+
requireConcentration(altConcentration, "altConcentration", "evidenceCeilingLogE");
|
|
1195
|
+
requireOpenProbability(mde, "mde", "evidenceCeilingLogE");
|
|
1149
1196
|
const { nullPrior, altPrior } = twoSamplePriors(
|
|
1150
1197
|
baselineSuccesses,
|
|
1151
1198
|
baselineTrials,
|
|
@@ -1213,6 +1260,8 @@ export function evidenceCeilingAsymptotic(
|
|
|
1213
1260
|
): number {
|
|
1214
1261
|
requireCounts(baselineSuccesses, baselineTrials, "evidenceCeilingAsymptotic");
|
|
1215
1262
|
requireProbability(pTrue, "pTrue", "evidenceCeilingAsymptotic");
|
|
1263
|
+
requireConcentration(altConcentration, "altConcentration", "evidenceCeilingAsymptotic");
|
|
1264
|
+
requireOpenProbability(mde, "mde", "evidenceCeilingAsymptotic");
|
|
1216
1265
|
if (baselineTrials === 0) {
|
|
1217
1266
|
throw new PeeksafeError(
|
|
1218
1267
|
"PEEKSAFE_E_STAT_DOMAIN",
|
|
@@ -1274,6 +1323,9 @@ export function expectedLogE(
|
|
|
1274
1323
|
detail: { n },
|
|
1275
1324
|
});
|
|
1276
1325
|
}
|
|
1326
|
+
requireCounts(baselineSuccesses, baselineTrials, "expectedLogE");
|
|
1327
|
+
requireConcentration(altConcentration, "altConcentration", "expectedLogE");
|
|
1328
|
+
requireOpenProbability(mde, "mde", "expectedLogE");
|
|
1277
1329
|
const { nullPrior: n0, altPrior: a1 } = twoSamplePriors(
|
|
1278
1330
|
baselineSuccesses,
|
|
1279
1331
|
baselineTrials,
|
|
@@ -1307,6 +1359,9 @@ export function expectedLogEExact(
|
|
|
1307
1359
|
{ detail: { n } },
|
|
1308
1360
|
);
|
|
1309
1361
|
}
|
|
1362
|
+
requireCounts(baselineSuccesses, baselineTrials, "expectedLogEExact");
|
|
1363
|
+
requireConcentration(altConcentration, "altConcentration", "expectedLogEExact");
|
|
1364
|
+
requireOpenProbability(mde, "mde", "expectedLogEExact");
|
|
1310
1365
|
let acc = 0;
|
|
1311
1366
|
for (let s = 0; s <= n; s++) {
|
|
1312
1367
|
const logW =
|
|
@@ -1339,6 +1394,10 @@ export function samplesForEvidence(
|
|
|
1339
1394
|
): number {
|
|
1340
1395
|
requireSearchCap(maxN, "maxN", "samplesForEvidence");
|
|
1341
1396
|
requireLogThreshold(logThreshold, "samplesForEvidence");
|
|
1397
|
+
requireProbability(pTrue, "pTrue", "samplesForEvidence");
|
|
1398
|
+
requireCounts(baselineSuccesses, baselineTrials, "samplesForEvidence");
|
|
1399
|
+
requireConcentration(altConcentration, "altConcentration", "samplesForEvidence");
|
|
1400
|
+
requireOpenProbability(mde, "mde", "samplesForEvidence");
|
|
1342
1401
|
const ceiling = evidenceCeilingLogE(
|
|
1343
1402
|
pTrue,
|
|
1344
1403
|
baselineSuccesses,
|
|
@@ -1407,6 +1466,8 @@ export function universalTwoSampleLogE(
|
|
|
1407
1466
|
): number {
|
|
1408
1467
|
requireCounts(candidateSuccesses, candidateTrials, "universalTwoSampleLogE(candidate)");
|
|
1409
1468
|
requireCounts(baselineSuccesses, baselineTrials, "universalTwoSampleLogE(baseline)");
|
|
1469
|
+
requireConcentration(altConcentration, "altConcentration", "universalTwoSampleLogE");
|
|
1470
|
+
requireOpenProbability(mde, "mde", "universalTwoSampleLogE");
|
|
1410
1471
|
if (baselineTrials === 0) {
|
|
1411
1472
|
throw new PeeksafeError(
|
|
1412
1473
|
"PEEKSAFE_E_STAT_DOMAIN",
|
|
@@ -1587,6 +1648,9 @@ export function mcnemarSamplesForEvidence(
|
|
|
1587
1648
|
requireConcentration(concentration, "concentration", "mcnemarSamplesForEvidence");
|
|
1588
1649
|
requireSearchCap(maxPairs, "maxPairs", "mcnemarSamplesForEvidence");
|
|
1589
1650
|
requireLogThreshold(logThreshold, "mcnemarSamplesForEvidence");
|
|
1651
|
+
requireProbability(pBaseline, "pBaseline", "mcnemarSamplesForEvidence");
|
|
1652
|
+
requireProbability(rho, "rho", "mcnemarSamplesForEvidence");
|
|
1653
|
+
requireOpenProbability(mde, "mde", "mcnemarSamplesForEvidence");
|
|
1590
1654
|
const { rate, theta } = pairedDiscordance(pBaseline, mde, rho);
|
|
1591
1655
|
if (rate <= 0 || theta <= 0.5) return Infinity;
|
|
1592
1656
|
const a1 = Math.max(0.35, concentration * theta);
|
package/src/stop.ts
CHANGED
|
@@ -449,7 +449,7 @@ export function shouldStopPaired(
|
|
|
449
449
|
}
|
|
450
450
|
|
|
451
451
|
// Mean of prod(1 - lambda (D - mde)) over fixed bets, D = +1 worse, -1 better; a supermartingale whenever E[D] >= mde.
|
|
452
|
-
|
|
452
|
+
function settleLogE(c: PairedCounts, mde: number): number {
|
|
453
453
|
const terms: number[] = [];
|
|
454
454
|
for (let k = 1; k <= 9; k++) {
|
|
455
455
|
const lambda = k / 10 / (1 - mde);
|