@takk/bayesoutputgate 1.0.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 +92 -0
- package/LICENSE +190 -0
- package/NOTICE +45 -0
- package/README.md +403 -0
- package/SECURITY.md +98 -0
- package/SPEC.md +467 -0
- package/dist/adapter/index.cjs +411 -0
- package/dist/adapter/index.d.cts +29 -0
- package/dist/adapter/index.d.ts +29 -0
- package/dist/adapter/index.js +404 -0
- package/dist/audit/index.cjs +82 -0
- package/dist/audit/index.d.cts +40 -0
- package/dist/audit/index.d.ts +40 -0
- package/dist/audit/index.js +77 -0
- package/dist/bayesfactor/index.cjs +152 -0
- package/dist/bayesfactor/index.d.cts +15 -0
- package/dist/bayesfactor/index.d.ts +15 -0
- package/dist/bayesfactor/index.js +149 -0
- package/dist/beta/index.cjs +180 -0
- package/dist/beta/index.d.cts +45 -0
- package/dist/beta/index.d.ts +45 -0
- package/dist/beta/index.js +178 -0
- package/dist/calibration/index.cjs +339 -0
- package/dist/calibration/index.d.cts +53 -0
- package/dist/calibration/index.d.ts +53 -0
- package/dist/calibration/index.js +333 -0
- package/dist/cli/index.cjs +968 -0
- package/dist/cli/index.d.cts +1 -0
- package/dist/cli/index.d.ts +1 -0
- package/dist/cli/index.js +966 -0
- package/dist/dimensions/index.cjs +106 -0
- package/dist/dimensions/index.d.cts +33 -0
- package/dist/dimensions/index.d.ts +33 -0
- package/dist/dimensions/index.js +104 -0
- package/dist/edge/index.cjs +1141 -0
- package/dist/edge/index.d.cts +12 -0
- package/dist/edge/index.d.ts +12 -0
- package/dist/edge/index.js +1109 -0
- package/dist/gate/index.cjs +803 -0
- package/dist/gate/index.d.cts +77 -0
- package/dist/gate/index.d.ts +77 -0
- package/dist/gate/index.js +799 -0
- package/dist/hypothesis/index.cjs +268 -0
- package/dist/hypothesis/index.d.cts +38 -0
- package/dist/hypothesis/index.d.ts +38 -0
- package/dist/hypothesis/index.js +266 -0
- package/dist/index.cjs +1141 -0
- package/dist/index.d.cts +29 -0
- package/dist/index.d.ts +29 -0
- package/dist/index.js +1109 -0
- package/dist/likelihood/index.cjs +137 -0
- package/dist/likelihood/index.d.cts +23 -0
- package/dist/likelihood/index.d.ts +23 -0
- package/dist/likelihood/index.js +132 -0
- package/dist/node/index.cjs +1282 -0
- package/dist/node/index.d.cts +24 -0
- package/dist/node/index.d.ts +24 -0
- package/dist/node/index.js +1246 -0
- package/dist/policy/index.cjs +88 -0
- package/dist/policy/index.d.cts +11 -0
- package/dist/policy/index.d.ts +11 -0
- package/dist/policy/index.js +85 -0
- package/dist/types-bMjn1j4e.d.cts +159 -0
- package/dist/types-bMjn1j4e.d.ts +159 -0
- package/package.json +142 -0
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// src/errors.ts
|
|
4
|
+
var BayesOutputGateError = class _BayesOutputGateError extends Error {
|
|
5
|
+
code;
|
|
6
|
+
constructor(code, message) {
|
|
7
|
+
super(message);
|
|
8
|
+
this.name = "BayesOutputGateError";
|
|
9
|
+
this.code = code;
|
|
10
|
+
Object.setPrototypeOf(this, _BayesOutputGateError.prototype);
|
|
11
|
+
}
|
|
12
|
+
};
|
|
13
|
+
function invariant(condition, code, message) {
|
|
14
|
+
if (!condition) {
|
|
15
|
+
throw new BayesOutputGateError(code, message);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// src/mathspecial.ts
|
|
20
|
+
var LN_SQRT_2PI = 0.9189385332046728;
|
|
21
|
+
var LANCZOS_G = 7;
|
|
22
|
+
var LANCZOS_COEFFICIENTS = [
|
|
23
|
+
0.9999999999998099,
|
|
24
|
+
676.5203681218851,
|
|
25
|
+
-1259.1392167224028,
|
|
26
|
+
771.3234287776531,
|
|
27
|
+
-176.6150291621406,
|
|
28
|
+
12.507343278686905,
|
|
29
|
+
-0.13857109526572012,
|
|
30
|
+
9984369578019572e-21,
|
|
31
|
+
15056327351493116e-23
|
|
32
|
+
];
|
|
33
|
+
function lgamma(x) {
|
|
34
|
+
if (!Number.isFinite(x) || x <= 0) {
|
|
35
|
+
throw new BayesOutputGateError(
|
|
36
|
+
"NUMERIC",
|
|
37
|
+
`lgamma requires a positive finite argument, got ${x}`
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
const z = x - 1;
|
|
41
|
+
let acc = LANCZOS_COEFFICIENTS[0];
|
|
42
|
+
for (let i = 1; i < LANCZOS_COEFFICIENTS.length; i++) {
|
|
43
|
+
acc += LANCZOS_COEFFICIENTS[i] / (z + i);
|
|
44
|
+
}
|
|
45
|
+
const t = z + LANCZOS_G + 0.5;
|
|
46
|
+
return LN_SQRT_2PI + (z + 0.5) * Math.log(t) - t + Math.log(acc);
|
|
47
|
+
}
|
|
48
|
+
function lbeta(a, b) {
|
|
49
|
+
return lgamma(a) + lgamma(b) - lgamma(a + b);
|
|
50
|
+
}
|
|
51
|
+
function betaLogDensity(x, a, b) {
|
|
52
|
+
if (a <= 0 || b <= 0) {
|
|
53
|
+
throw new BayesOutputGateError(
|
|
54
|
+
"NUMERIC",
|
|
55
|
+
`Beta shape parameters must be positive, got a=${a}, b=${b}`
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
if (x < 0 || x > 1 || !Number.isFinite(x)) {
|
|
59
|
+
return Number.NEGATIVE_INFINITY;
|
|
60
|
+
}
|
|
61
|
+
if (x === 0) {
|
|
62
|
+
if (a < 1) return Number.POSITIVE_INFINITY;
|
|
63
|
+
if (a > 1) return Number.NEGATIVE_INFINITY;
|
|
64
|
+
return -lbeta(a, b) + (b - 1) * Math.log(1);
|
|
65
|
+
}
|
|
66
|
+
if (x === 1) {
|
|
67
|
+
if (b < 1) return Number.POSITIVE_INFINITY;
|
|
68
|
+
if (b > 1) return Number.NEGATIVE_INFINITY;
|
|
69
|
+
return -lbeta(a, b);
|
|
70
|
+
}
|
|
71
|
+
return (a - 1) * Math.log(x) + (b - 1) * Math.log1p(-x) - lbeta(a, b);
|
|
72
|
+
}
|
|
73
|
+
function clamp(x, lo, hi) {
|
|
74
|
+
if (x < lo) return lo;
|
|
75
|
+
if (x > hi) return hi;
|
|
76
|
+
return x;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// src/beta/index.ts
|
|
80
|
+
var VARIANCE_FLOOR = 1e-9;
|
|
81
|
+
var CONCENTRATION_FLOOR = 1e-3;
|
|
82
|
+
var BetaModel = class _BetaModel {
|
|
83
|
+
priorA;
|
|
84
|
+
priorB;
|
|
85
|
+
count = 0;
|
|
86
|
+
sum = 0;
|
|
87
|
+
sumSquares = 0;
|
|
88
|
+
constructor(options = {}) {
|
|
89
|
+
const prior = options.prior ?? { a: 1, b: 1 };
|
|
90
|
+
invariant(
|
|
91
|
+
prior.a > 0 && prior.b > 0 && Number.isFinite(prior.a) && Number.isFinite(prior.b),
|
|
92
|
+
"INVALID_CONFIG",
|
|
93
|
+
`prior Beta parameters must be positive and finite, got a=${prior.a}, b=${prior.b}`
|
|
94
|
+
);
|
|
95
|
+
this.priorA = prior.a;
|
|
96
|
+
this.priorB = prior.b;
|
|
97
|
+
}
|
|
98
|
+
/** Build a model from labeled scores in one pass. */
|
|
99
|
+
static fromSamples(samples, options = {}) {
|
|
100
|
+
const model = new _BetaModel(options);
|
|
101
|
+
for (const s of samples) {
|
|
102
|
+
model.observe(s);
|
|
103
|
+
}
|
|
104
|
+
return model;
|
|
105
|
+
}
|
|
106
|
+
/** Restore a model from a snapshot. */
|
|
107
|
+
static fromSnapshot(snapshot) {
|
|
108
|
+
const model = new _BetaModel({ prior: snapshot.prior });
|
|
109
|
+
invariant(
|
|
110
|
+
snapshot.count >= 0 && Number.isFinite(snapshot.sum) && Number.isFinite(snapshot.sumSquares),
|
|
111
|
+
"INVALID_SNAPSHOT",
|
|
112
|
+
"snapshot fields must be finite and non-negative"
|
|
113
|
+
);
|
|
114
|
+
model.count = snapshot.count;
|
|
115
|
+
model.sum = snapshot.sum;
|
|
116
|
+
model.sumSquares = snapshot.sumSquares;
|
|
117
|
+
return model;
|
|
118
|
+
}
|
|
119
|
+
/** Number of real observations folded into this model so far. */
|
|
120
|
+
get observations() {
|
|
121
|
+
return this.count;
|
|
122
|
+
}
|
|
123
|
+
/** Fold a single score in [0, 1] into the model, updating its calibration online. */
|
|
124
|
+
observe(score) {
|
|
125
|
+
invariant(
|
|
126
|
+
Number.isFinite(score) && score >= 0 && score <= 1,
|
|
127
|
+
"INVALID_SCORE",
|
|
128
|
+
`score must be a finite number in [0, 1], got ${score}`
|
|
129
|
+
);
|
|
130
|
+
this.count += 1;
|
|
131
|
+
this.sum += score;
|
|
132
|
+
this.sumSquares += score * score;
|
|
133
|
+
return this;
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* The fitted Beta parameters. The prior is mixed in as `priorA + priorB` pseudo-observations with
|
|
137
|
+
* the prior's own mean and variance, then a and b come from method of moments on the blend, so a
|
|
138
|
+
* cold model returns its prior and a warm model is data-driven.
|
|
139
|
+
*/
|
|
140
|
+
params() {
|
|
141
|
+
const priorStrength = this.priorA + this.priorB;
|
|
142
|
+
const priorMean = this.priorA / priorStrength;
|
|
143
|
+
const priorVariance = priorMean * (1 - priorMean) / (priorStrength + 1);
|
|
144
|
+
const totalCount = this.count + priorStrength;
|
|
145
|
+
const totalSum = this.sum + priorStrength * priorMean;
|
|
146
|
+
const totalSumSquares = this.sumSquares + priorStrength * (priorVariance + priorMean * priorMean);
|
|
147
|
+
const mean = clamp(totalSum / totalCount, VARIANCE_FLOOR, 1 - VARIANCE_FLOOR);
|
|
148
|
+
const rawVariance = totalSumSquares / totalCount - mean * mean;
|
|
149
|
+
const maxVariance = mean * (1 - mean);
|
|
150
|
+
const variance = clamp(rawVariance, VARIANCE_FLOOR, maxVariance * (1 - VARIANCE_FLOOR));
|
|
151
|
+
const concentration = Math.max(mean * (1 - mean) / variance - 1, CONCENTRATION_FLOOR);
|
|
152
|
+
const a = Math.max(mean * concentration, CONCENTRATION_FLOOR);
|
|
153
|
+
const b = Math.max((1 - mean) * concentration, CONCENTRATION_FLOOR);
|
|
154
|
+
if (!Number.isFinite(a) || !Number.isFinite(b)) {
|
|
155
|
+
throw new BayesOutputGateError("NUMERIC", "Beta fit produced non-finite parameters");
|
|
156
|
+
}
|
|
157
|
+
return { a, b };
|
|
158
|
+
}
|
|
159
|
+
/** Mean of the fitted Beta. */
|
|
160
|
+
mean() {
|
|
161
|
+
const { a, b } = this.params();
|
|
162
|
+
return a / (a + b);
|
|
163
|
+
}
|
|
164
|
+
/** Log-density of an observed score under the fitted Beta. */
|
|
165
|
+
logDensity(score) {
|
|
166
|
+
const { a, b } = this.params();
|
|
167
|
+
return betaLogDensity(score, a, b);
|
|
168
|
+
}
|
|
169
|
+
/** A serializable snapshot of the current state. */
|
|
170
|
+
snapshot() {
|
|
171
|
+
return {
|
|
172
|
+
count: this.count,
|
|
173
|
+
sum: this.sum,
|
|
174
|
+
sumSquares: this.sumSquares,
|
|
175
|
+
prior: { a: this.priorA, b: this.priorB }
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
// src/hypothesis/index.ts
|
|
181
|
+
var HypothesisManager = class {
|
|
182
|
+
entries = /* @__PURE__ */ new Map();
|
|
183
|
+
defaultWeight;
|
|
184
|
+
defaultPriorHigh;
|
|
185
|
+
defaultPriorLow;
|
|
186
|
+
configured = /* @__PURE__ */ new Map();
|
|
187
|
+
constructor(options = {}) {
|
|
188
|
+
this.defaultWeight = options.defaultWeight ?? 1;
|
|
189
|
+
this.defaultPriorHigh = options.defaultPriorHigh ?? { a: 2, b: 1 };
|
|
190
|
+
this.defaultPriorLow = options.defaultPriorLow ?? { a: 1, b: 2 };
|
|
191
|
+
invariant(
|
|
192
|
+
Number.isFinite(this.defaultWeight) && this.defaultWeight >= 0,
|
|
193
|
+
"INVALID_CONFIG",
|
|
194
|
+
`defaultWeight must be a non-negative finite number, got ${this.defaultWeight}`
|
|
195
|
+
);
|
|
196
|
+
for (const config of options.dimensions ?? []) {
|
|
197
|
+
this.configured.set(config.dimension, config);
|
|
198
|
+
this.ensure(config.dimension);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
ensure(dimension) {
|
|
202
|
+
const existing = this.entries.get(dimension);
|
|
203
|
+
if (existing) {
|
|
204
|
+
return existing;
|
|
205
|
+
}
|
|
206
|
+
const config = this.configured.get(dimension);
|
|
207
|
+
const entry = {
|
|
208
|
+
high: new BetaModel({ prior: config?.priorHigh ?? this.defaultPriorHigh }),
|
|
209
|
+
low: new BetaModel({ prior: config?.priorLow ?? this.defaultPriorLow }),
|
|
210
|
+
weight: config?.weight ?? this.defaultWeight
|
|
211
|
+
};
|
|
212
|
+
invariant(
|
|
213
|
+
Number.isFinite(entry.weight) && entry.weight >= 0,
|
|
214
|
+
"INVALID_CONFIG",
|
|
215
|
+
`weight for "${dimension}" must be a non-negative finite number, got ${entry.weight}`
|
|
216
|
+
);
|
|
217
|
+
this.entries.set(dimension, entry);
|
|
218
|
+
return entry;
|
|
219
|
+
}
|
|
220
|
+
/** Fold one labeled output into the high or low model of each scored dimension. */
|
|
221
|
+
observe(observation) {
|
|
222
|
+
invariant(
|
|
223
|
+
observation.label === "high" || observation.label === "low",
|
|
224
|
+
"INVALID_OBSERVATION",
|
|
225
|
+
`observation label must be "high" or "low", got ${String(observation.label)}`
|
|
226
|
+
);
|
|
227
|
+
for (const score of observation.scores) {
|
|
228
|
+
const entry = this.ensure(score.dimension);
|
|
229
|
+
const model = observation.label === "high" ? entry.high : entry.low;
|
|
230
|
+
model.observe(score.value);
|
|
231
|
+
}
|
|
232
|
+
return this;
|
|
233
|
+
}
|
|
234
|
+
/** Fold a batch of labeled observations. */
|
|
235
|
+
fit(observations) {
|
|
236
|
+
for (const observation of observations) {
|
|
237
|
+
this.observe(observation);
|
|
238
|
+
}
|
|
239
|
+
return this;
|
|
240
|
+
}
|
|
241
|
+
/** The dimension names this manager currently tracks. */
|
|
242
|
+
get dimensions() {
|
|
243
|
+
return [...this.entries.keys()];
|
|
244
|
+
}
|
|
245
|
+
/** Snapshot the current calibrated models as the dimension models the Bayes Factor consumes. */
|
|
246
|
+
models() {
|
|
247
|
+
const out = [];
|
|
248
|
+
for (const [dimension, entry] of this.entries) {
|
|
249
|
+
out.push({
|
|
250
|
+
dimension,
|
|
251
|
+
high: entry.high.params(),
|
|
252
|
+
low: entry.low.params(),
|
|
253
|
+
weight: entry.weight
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
return out;
|
|
257
|
+
}
|
|
258
|
+
/** Number of labeled observations folded into a dimension under one hypothesis. */
|
|
259
|
+
observationCount(dimension, kind) {
|
|
260
|
+
const entry = this.entries.get(dimension);
|
|
261
|
+
if (!entry) {
|
|
262
|
+
return 0;
|
|
263
|
+
}
|
|
264
|
+
return (kind === "high" ? entry.high : entry.low).observations;
|
|
265
|
+
}
|
|
266
|
+
};
|
|
267
|
+
|
|
268
|
+
exports.HypothesisManager = HypothesisManager;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { b as BetaParams, L as LabeledObservation, e as DimensionModel } from '../types-bMjn1j4e.cjs';
|
|
2
|
+
|
|
3
|
+
/** Per-dimension configuration: weight in the combined Bayes Factor and the two regularizing priors. */
|
|
4
|
+
interface DimensionConfig {
|
|
5
|
+
readonly dimension: string;
|
|
6
|
+
readonly weight?: number;
|
|
7
|
+
readonly priorHigh?: BetaParams;
|
|
8
|
+
readonly priorLow?: BetaParams;
|
|
9
|
+
}
|
|
10
|
+
/** Options for a {@link HypothesisManager}. */
|
|
11
|
+
interface HypothesisManagerOptions {
|
|
12
|
+
readonly dimensions?: readonly DimensionConfig[];
|
|
13
|
+
readonly defaultWeight?: number;
|
|
14
|
+
readonly defaultPriorHigh?: BetaParams;
|
|
15
|
+
readonly defaultPriorLow?: BetaParams;
|
|
16
|
+
}
|
|
17
|
+
/** Manages the high-quality and low-quality score models for every dimension. */
|
|
18
|
+
declare class HypothesisManager {
|
|
19
|
+
private readonly entries;
|
|
20
|
+
private readonly defaultWeight;
|
|
21
|
+
private readonly defaultPriorHigh;
|
|
22
|
+
private readonly defaultPriorLow;
|
|
23
|
+
private readonly configured;
|
|
24
|
+
constructor(options?: HypothesisManagerOptions);
|
|
25
|
+
private ensure;
|
|
26
|
+
/** Fold one labeled output into the high or low model of each scored dimension. */
|
|
27
|
+
observe(observation: LabeledObservation): this;
|
|
28
|
+
/** Fold a batch of labeled observations. */
|
|
29
|
+
fit(observations: Iterable<LabeledObservation>): this;
|
|
30
|
+
/** The dimension names this manager currently tracks. */
|
|
31
|
+
get dimensions(): string[];
|
|
32
|
+
/** Snapshot the current calibrated models as the dimension models the Bayes Factor consumes. */
|
|
33
|
+
models(): DimensionModel[];
|
|
34
|
+
/** Number of labeled observations folded into a dimension under one hypothesis. */
|
|
35
|
+
observationCount(dimension: string, kind: "high" | "low"): number;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export { type DimensionConfig, HypothesisManager, type HypothesisManagerOptions };
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { b as BetaParams, L as LabeledObservation, e as DimensionModel } from '../types-bMjn1j4e.js';
|
|
2
|
+
|
|
3
|
+
/** Per-dimension configuration: weight in the combined Bayes Factor and the two regularizing priors. */
|
|
4
|
+
interface DimensionConfig {
|
|
5
|
+
readonly dimension: string;
|
|
6
|
+
readonly weight?: number;
|
|
7
|
+
readonly priorHigh?: BetaParams;
|
|
8
|
+
readonly priorLow?: BetaParams;
|
|
9
|
+
}
|
|
10
|
+
/** Options for a {@link HypothesisManager}. */
|
|
11
|
+
interface HypothesisManagerOptions {
|
|
12
|
+
readonly dimensions?: readonly DimensionConfig[];
|
|
13
|
+
readonly defaultWeight?: number;
|
|
14
|
+
readonly defaultPriorHigh?: BetaParams;
|
|
15
|
+
readonly defaultPriorLow?: BetaParams;
|
|
16
|
+
}
|
|
17
|
+
/** Manages the high-quality and low-quality score models for every dimension. */
|
|
18
|
+
declare class HypothesisManager {
|
|
19
|
+
private readonly entries;
|
|
20
|
+
private readonly defaultWeight;
|
|
21
|
+
private readonly defaultPriorHigh;
|
|
22
|
+
private readonly defaultPriorLow;
|
|
23
|
+
private readonly configured;
|
|
24
|
+
constructor(options?: HypothesisManagerOptions);
|
|
25
|
+
private ensure;
|
|
26
|
+
/** Fold one labeled output into the high or low model of each scored dimension. */
|
|
27
|
+
observe(observation: LabeledObservation): this;
|
|
28
|
+
/** Fold a batch of labeled observations. */
|
|
29
|
+
fit(observations: Iterable<LabeledObservation>): this;
|
|
30
|
+
/** The dimension names this manager currently tracks. */
|
|
31
|
+
get dimensions(): string[];
|
|
32
|
+
/** Snapshot the current calibrated models as the dimension models the Bayes Factor consumes. */
|
|
33
|
+
models(): DimensionModel[];
|
|
34
|
+
/** Number of labeled observations folded into a dimension under one hypothesis. */
|
|
35
|
+
observationCount(dimension: string, kind: "high" | "low"): number;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export { type DimensionConfig, HypothesisManager, type HypothesisManagerOptions };
|
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
// src/errors.ts
|
|
2
|
+
var BayesOutputGateError = class _BayesOutputGateError extends Error {
|
|
3
|
+
code;
|
|
4
|
+
constructor(code, message) {
|
|
5
|
+
super(message);
|
|
6
|
+
this.name = "BayesOutputGateError";
|
|
7
|
+
this.code = code;
|
|
8
|
+
Object.setPrototypeOf(this, _BayesOutputGateError.prototype);
|
|
9
|
+
}
|
|
10
|
+
};
|
|
11
|
+
function invariant(condition, code, message) {
|
|
12
|
+
if (!condition) {
|
|
13
|
+
throw new BayesOutputGateError(code, message);
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// src/mathspecial.ts
|
|
18
|
+
var LN_SQRT_2PI = 0.9189385332046728;
|
|
19
|
+
var LANCZOS_G = 7;
|
|
20
|
+
var LANCZOS_COEFFICIENTS = [
|
|
21
|
+
0.9999999999998099,
|
|
22
|
+
676.5203681218851,
|
|
23
|
+
-1259.1392167224028,
|
|
24
|
+
771.3234287776531,
|
|
25
|
+
-176.6150291621406,
|
|
26
|
+
12.507343278686905,
|
|
27
|
+
-0.13857109526572012,
|
|
28
|
+
9984369578019572e-21,
|
|
29
|
+
15056327351493116e-23
|
|
30
|
+
];
|
|
31
|
+
function lgamma(x) {
|
|
32
|
+
if (!Number.isFinite(x) || x <= 0) {
|
|
33
|
+
throw new BayesOutputGateError(
|
|
34
|
+
"NUMERIC",
|
|
35
|
+
`lgamma requires a positive finite argument, got ${x}`
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
const z = x - 1;
|
|
39
|
+
let acc = LANCZOS_COEFFICIENTS[0];
|
|
40
|
+
for (let i = 1; i < LANCZOS_COEFFICIENTS.length; i++) {
|
|
41
|
+
acc += LANCZOS_COEFFICIENTS[i] / (z + i);
|
|
42
|
+
}
|
|
43
|
+
const t = z + LANCZOS_G + 0.5;
|
|
44
|
+
return LN_SQRT_2PI + (z + 0.5) * Math.log(t) - t + Math.log(acc);
|
|
45
|
+
}
|
|
46
|
+
function lbeta(a, b) {
|
|
47
|
+
return lgamma(a) + lgamma(b) - lgamma(a + b);
|
|
48
|
+
}
|
|
49
|
+
function betaLogDensity(x, a, b) {
|
|
50
|
+
if (a <= 0 || b <= 0) {
|
|
51
|
+
throw new BayesOutputGateError(
|
|
52
|
+
"NUMERIC",
|
|
53
|
+
`Beta shape parameters must be positive, got a=${a}, b=${b}`
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
if (x < 0 || x > 1 || !Number.isFinite(x)) {
|
|
57
|
+
return Number.NEGATIVE_INFINITY;
|
|
58
|
+
}
|
|
59
|
+
if (x === 0) {
|
|
60
|
+
if (a < 1) return Number.POSITIVE_INFINITY;
|
|
61
|
+
if (a > 1) return Number.NEGATIVE_INFINITY;
|
|
62
|
+
return -lbeta(a, b) + (b - 1) * Math.log(1);
|
|
63
|
+
}
|
|
64
|
+
if (x === 1) {
|
|
65
|
+
if (b < 1) return Number.POSITIVE_INFINITY;
|
|
66
|
+
if (b > 1) return Number.NEGATIVE_INFINITY;
|
|
67
|
+
return -lbeta(a, b);
|
|
68
|
+
}
|
|
69
|
+
return (a - 1) * Math.log(x) + (b - 1) * Math.log1p(-x) - lbeta(a, b);
|
|
70
|
+
}
|
|
71
|
+
function clamp(x, lo, hi) {
|
|
72
|
+
if (x < lo) return lo;
|
|
73
|
+
if (x > hi) return hi;
|
|
74
|
+
return x;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// src/beta/index.ts
|
|
78
|
+
var VARIANCE_FLOOR = 1e-9;
|
|
79
|
+
var CONCENTRATION_FLOOR = 1e-3;
|
|
80
|
+
var BetaModel = class _BetaModel {
|
|
81
|
+
priorA;
|
|
82
|
+
priorB;
|
|
83
|
+
count = 0;
|
|
84
|
+
sum = 0;
|
|
85
|
+
sumSquares = 0;
|
|
86
|
+
constructor(options = {}) {
|
|
87
|
+
const prior = options.prior ?? { a: 1, b: 1 };
|
|
88
|
+
invariant(
|
|
89
|
+
prior.a > 0 && prior.b > 0 && Number.isFinite(prior.a) && Number.isFinite(prior.b),
|
|
90
|
+
"INVALID_CONFIG",
|
|
91
|
+
`prior Beta parameters must be positive and finite, got a=${prior.a}, b=${prior.b}`
|
|
92
|
+
);
|
|
93
|
+
this.priorA = prior.a;
|
|
94
|
+
this.priorB = prior.b;
|
|
95
|
+
}
|
|
96
|
+
/** Build a model from labeled scores in one pass. */
|
|
97
|
+
static fromSamples(samples, options = {}) {
|
|
98
|
+
const model = new _BetaModel(options);
|
|
99
|
+
for (const s of samples) {
|
|
100
|
+
model.observe(s);
|
|
101
|
+
}
|
|
102
|
+
return model;
|
|
103
|
+
}
|
|
104
|
+
/** Restore a model from a snapshot. */
|
|
105
|
+
static fromSnapshot(snapshot) {
|
|
106
|
+
const model = new _BetaModel({ prior: snapshot.prior });
|
|
107
|
+
invariant(
|
|
108
|
+
snapshot.count >= 0 && Number.isFinite(snapshot.sum) && Number.isFinite(snapshot.sumSquares),
|
|
109
|
+
"INVALID_SNAPSHOT",
|
|
110
|
+
"snapshot fields must be finite and non-negative"
|
|
111
|
+
);
|
|
112
|
+
model.count = snapshot.count;
|
|
113
|
+
model.sum = snapshot.sum;
|
|
114
|
+
model.sumSquares = snapshot.sumSquares;
|
|
115
|
+
return model;
|
|
116
|
+
}
|
|
117
|
+
/** Number of real observations folded into this model so far. */
|
|
118
|
+
get observations() {
|
|
119
|
+
return this.count;
|
|
120
|
+
}
|
|
121
|
+
/** Fold a single score in [0, 1] into the model, updating its calibration online. */
|
|
122
|
+
observe(score) {
|
|
123
|
+
invariant(
|
|
124
|
+
Number.isFinite(score) && score >= 0 && score <= 1,
|
|
125
|
+
"INVALID_SCORE",
|
|
126
|
+
`score must be a finite number in [0, 1], got ${score}`
|
|
127
|
+
);
|
|
128
|
+
this.count += 1;
|
|
129
|
+
this.sum += score;
|
|
130
|
+
this.sumSquares += score * score;
|
|
131
|
+
return this;
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* The fitted Beta parameters. The prior is mixed in as `priorA + priorB` pseudo-observations with
|
|
135
|
+
* the prior's own mean and variance, then a and b come from method of moments on the blend, so a
|
|
136
|
+
* cold model returns its prior and a warm model is data-driven.
|
|
137
|
+
*/
|
|
138
|
+
params() {
|
|
139
|
+
const priorStrength = this.priorA + this.priorB;
|
|
140
|
+
const priorMean = this.priorA / priorStrength;
|
|
141
|
+
const priorVariance = priorMean * (1 - priorMean) / (priorStrength + 1);
|
|
142
|
+
const totalCount = this.count + priorStrength;
|
|
143
|
+
const totalSum = this.sum + priorStrength * priorMean;
|
|
144
|
+
const totalSumSquares = this.sumSquares + priorStrength * (priorVariance + priorMean * priorMean);
|
|
145
|
+
const mean = clamp(totalSum / totalCount, VARIANCE_FLOOR, 1 - VARIANCE_FLOOR);
|
|
146
|
+
const rawVariance = totalSumSquares / totalCount - mean * mean;
|
|
147
|
+
const maxVariance = mean * (1 - mean);
|
|
148
|
+
const variance = clamp(rawVariance, VARIANCE_FLOOR, maxVariance * (1 - VARIANCE_FLOOR));
|
|
149
|
+
const concentration = Math.max(mean * (1 - mean) / variance - 1, CONCENTRATION_FLOOR);
|
|
150
|
+
const a = Math.max(mean * concentration, CONCENTRATION_FLOOR);
|
|
151
|
+
const b = Math.max((1 - mean) * concentration, CONCENTRATION_FLOOR);
|
|
152
|
+
if (!Number.isFinite(a) || !Number.isFinite(b)) {
|
|
153
|
+
throw new BayesOutputGateError("NUMERIC", "Beta fit produced non-finite parameters");
|
|
154
|
+
}
|
|
155
|
+
return { a, b };
|
|
156
|
+
}
|
|
157
|
+
/** Mean of the fitted Beta. */
|
|
158
|
+
mean() {
|
|
159
|
+
const { a, b } = this.params();
|
|
160
|
+
return a / (a + b);
|
|
161
|
+
}
|
|
162
|
+
/** Log-density of an observed score under the fitted Beta. */
|
|
163
|
+
logDensity(score) {
|
|
164
|
+
const { a, b } = this.params();
|
|
165
|
+
return betaLogDensity(score, a, b);
|
|
166
|
+
}
|
|
167
|
+
/** A serializable snapshot of the current state. */
|
|
168
|
+
snapshot() {
|
|
169
|
+
return {
|
|
170
|
+
count: this.count,
|
|
171
|
+
sum: this.sum,
|
|
172
|
+
sumSquares: this.sumSquares,
|
|
173
|
+
prior: { a: this.priorA, b: this.priorB }
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
// src/hypothesis/index.ts
|
|
179
|
+
var HypothesisManager = class {
|
|
180
|
+
entries = /* @__PURE__ */ new Map();
|
|
181
|
+
defaultWeight;
|
|
182
|
+
defaultPriorHigh;
|
|
183
|
+
defaultPriorLow;
|
|
184
|
+
configured = /* @__PURE__ */ new Map();
|
|
185
|
+
constructor(options = {}) {
|
|
186
|
+
this.defaultWeight = options.defaultWeight ?? 1;
|
|
187
|
+
this.defaultPriorHigh = options.defaultPriorHigh ?? { a: 2, b: 1 };
|
|
188
|
+
this.defaultPriorLow = options.defaultPriorLow ?? { a: 1, b: 2 };
|
|
189
|
+
invariant(
|
|
190
|
+
Number.isFinite(this.defaultWeight) && this.defaultWeight >= 0,
|
|
191
|
+
"INVALID_CONFIG",
|
|
192
|
+
`defaultWeight must be a non-negative finite number, got ${this.defaultWeight}`
|
|
193
|
+
);
|
|
194
|
+
for (const config of options.dimensions ?? []) {
|
|
195
|
+
this.configured.set(config.dimension, config);
|
|
196
|
+
this.ensure(config.dimension);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
ensure(dimension) {
|
|
200
|
+
const existing = this.entries.get(dimension);
|
|
201
|
+
if (existing) {
|
|
202
|
+
return existing;
|
|
203
|
+
}
|
|
204
|
+
const config = this.configured.get(dimension);
|
|
205
|
+
const entry = {
|
|
206
|
+
high: new BetaModel({ prior: config?.priorHigh ?? this.defaultPriorHigh }),
|
|
207
|
+
low: new BetaModel({ prior: config?.priorLow ?? this.defaultPriorLow }),
|
|
208
|
+
weight: config?.weight ?? this.defaultWeight
|
|
209
|
+
};
|
|
210
|
+
invariant(
|
|
211
|
+
Number.isFinite(entry.weight) && entry.weight >= 0,
|
|
212
|
+
"INVALID_CONFIG",
|
|
213
|
+
`weight for "${dimension}" must be a non-negative finite number, got ${entry.weight}`
|
|
214
|
+
);
|
|
215
|
+
this.entries.set(dimension, entry);
|
|
216
|
+
return entry;
|
|
217
|
+
}
|
|
218
|
+
/** Fold one labeled output into the high or low model of each scored dimension. */
|
|
219
|
+
observe(observation) {
|
|
220
|
+
invariant(
|
|
221
|
+
observation.label === "high" || observation.label === "low",
|
|
222
|
+
"INVALID_OBSERVATION",
|
|
223
|
+
`observation label must be "high" or "low", got ${String(observation.label)}`
|
|
224
|
+
);
|
|
225
|
+
for (const score of observation.scores) {
|
|
226
|
+
const entry = this.ensure(score.dimension);
|
|
227
|
+
const model = observation.label === "high" ? entry.high : entry.low;
|
|
228
|
+
model.observe(score.value);
|
|
229
|
+
}
|
|
230
|
+
return this;
|
|
231
|
+
}
|
|
232
|
+
/** Fold a batch of labeled observations. */
|
|
233
|
+
fit(observations) {
|
|
234
|
+
for (const observation of observations) {
|
|
235
|
+
this.observe(observation);
|
|
236
|
+
}
|
|
237
|
+
return this;
|
|
238
|
+
}
|
|
239
|
+
/** The dimension names this manager currently tracks. */
|
|
240
|
+
get dimensions() {
|
|
241
|
+
return [...this.entries.keys()];
|
|
242
|
+
}
|
|
243
|
+
/** Snapshot the current calibrated models as the dimension models the Bayes Factor consumes. */
|
|
244
|
+
models() {
|
|
245
|
+
const out = [];
|
|
246
|
+
for (const [dimension, entry] of this.entries) {
|
|
247
|
+
out.push({
|
|
248
|
+
dimension,
|
|
249
|
+
high: entry.high.params(),
|
|
250
|
+
low: entry.low.params(),
|
|
251
|
+
weight: entry.weight
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
return out;
|
|
255
|
+
}
|
|
256
|
+
/** Number of labeled observations folded into a dimension under one hypothesis. */
|
|
257
|
+
observationCount(dimension, kind) {
|
|
258
|
+
const entry = this.entries.get(dimension);
|
|
259
|
+
if (!entry) {
|
|
260
|
+
return 0;
|
|
261
|
+
}
|
|
262
|
+
return (kind === "high" ? entry.high : entry.low).observations;
|
|
263
|
+
}
|
|
264
|
+
};
|
|
265
|
+
|
|
266
|
+
export { HypothesisManager };
|