nx_abstat_lib.nx
buildroot/runtime/nx_abstat_lib.nx
about
nx_abstat_lib.nx -- THE A/B CONTRAST CORE: two arms, replicated, per-axis, family-wise corrected.
WHY THIS EXISTS (operator 2026-09-03: "validating if we are improving in ab and multivariate methodology").
The three-party acceptance ledger (nx_accept) answers "has everyone signed?". It CANNOT answer "are we
improving?", because a referee row is a SINGLE-ARM, SINGLE-REPLICATE point grade: one capture, one number,
no control, no variance. percept=214 floor=151 says the frame cleared the bar. It says NOTHING about
whether the frame is better than the one before it, and a bar-clearing frame can be a REGRESSION from a
much better predecessor.
MEASURED, NOT ASSERTED -- the near-miss that motivated this lib, 2026-09-03: four point measurements of the
SAME live world (/world/beach, unchanged build) returned q=5,1,5,5 and worst_ever=13,250,13,12. The spread
was the measuring harness own concurrent load, not the subject. A single-arm grade would have published
whichever sample the seat happened to take. Only replication plus a control arm separates the two.
TWO QUESTIONS, NEVER CONFLATED. This lib answers exactly one of them and refuses to answer the other:
"is it above the bar?" -> referee percept vs floor. nx_accept owns it. NOT computed here.
"is it better than before?" -> this lib.
A subject can be improving and below floor, or above floor and regressing. Neither verdict is derivable
from the other, so a consumer must print both or say which one it is showing.
WHY BONFERRONI. Acceptance is judged on MANY axes at once (face, skin, hair, garments, locomotion; frame
p50/p95/p99; draws; memory). Five axes each tested at alpha=0.05 carry a ~23% chance that at least one
reads "improved" by chance alone. ab_crit_t_q10 composes nx_multivariate _mv_bonferroni_critical_t_q10
so a multi-axis "we improved" is a claim rather than a lottery ticket. The table is NOT copied here --
one Bonferroni ruler in the estate, and this lib is a caller of it.
NO SQUARE ROOT, AND THAT IS EXACT, NOT A SHORTCUT. The two-sample test is t = delta / sqrt(se2). Comparing
t against a critical value is algebraically identical to comparing delta^2 against crit^2 * se2 whenever
delta carries the sign we are testing, so this lib squares instead of rooting: no isqrt primitive, no
truncation of an irrational, and the comparison is exact integer arithmetic.
Q10 IS APPLIED BY THIS LIB, AND HERE IS WHY. It composes the Welford recurrence, whose running mean uses
INTEGER division (mean + delta/n). On raw small integers that is lossy: pushing 4 then 5 yields mean 4,
not 4.5. Pushing the SAME samples scaled by Q10 (4096, 5120) yields 4608 = 4.5 exactly. So ab_push takes
RAW samples and scales them itself -- the caller cannot forget. The residual imprecision is one part in
1024 of a sample unit, named here rather than left for a reader to discover.
WASM-COMPATIBLE BY CONSTRUCTION: no allocation, no syscalls, no floating point in the arithmetic path. The
caller hands in a region of AB_WORDS i64 (an engine arena offset, or a native mmap), as nx_perf_lib does.
license_tier: ORIGINAL No hw writes (Rule 26).
dependencies 2 imports · 3 importers
imports: nx_multivariate.nxnx_intlog.nx
imported by: nx_abstat_aa_gate.nxnx_abstat_gate.nxnx_frameab.nx
structs
| none |
consts
| 45 | const AB_A_N: i64 = 0 // samples pushed into this arm |
| 46 | const AB_A_MEAN: i64 = 1 // Welford running mean, Q10 |
| 47 | const AB_A_M2: i64 = 2 // Welford sum of squared deviations, Q10 squared |
| 48 | const AB_A_MIN: i64 = 3 // Q10 |
| 49 | const AB_A_MAX: i64 = 4 // Q10 |
| 50 | const AB_A_WORDS: i64 = 5 |
| 53 | const AB_ARM_B: i64 = 0 |
| 54 | const AB_ARM_A: i64 = 1 |
| 55 | const AB_ARMS: i64 = 2 |
| 56 | const AB_MAX_AXES: i64 = 16 // _mv_bonferroni_critical_t_q10 caps its table at 16; past that it repeats |
| 57 | const AB_WORDS: i64 = 160 // AB_MAX_AXES * AB_ARMS * AB_A_WORDS |
| 59 | const AB_Q10: i64 = 1024 // NX_MC_Q10, restated as a local name only for arithmetic readability |
| 60 | const AB_UNOBSERVED: i64 = 0 - 1 |
| 61 | const AB_HUGE: i64 = 3037000499 // floor(sqrt(i64max)): the squaring fence, so a product never wraps |
| 64 | const AB_AX_UP: i64 = 0 // significantly better AND at or above the pre-declared effect floor |
| 65 | const AB_AX_DOWN: i64 = 1 // significantly worse |
| 66 | const AB_AX_FLAT: i64 = 2 // the contrast does not clear the corrected critical value |
| 67 | const AB_AX_BELOW_MDE: i64 = 3 // significant, but the effect is smaller than the caller declared to matter |
| 68 | const AB_AX_INSUFF: i64 = 4 // cannot be tested at all: too few samples, or no within-arm variance |
| 69 | const AB_AX_N: i64 = 5 |
| 72 | const AB_V_IMPROVED: i64 = 0 |
| 73 | const AB_V_REGRESSED: i64 = 1 |
| 74 | const AB_V_NO_CHANGE: i64 = 2 |
| 75 | const AB_V_INSUFFICIENT: i64 = 3 |
| 76 | const AB_V_N: i64 = 4 |
| 79 | const AB_R_OK: i64 = 0 |
| 80 | const AB_R_FEW_B: i64 = 1 // baseline arm under the sample floor |
| 81 | const AB_R_FEW_A: i64 = 2 // candidate arm under the sample floor |
| 82 | const AB_R_ZEROVAR: i64 = 3 // BOTH arms have zero within-arm variance: the fixture cannot fail |
| 83 | const AB_R_OVERFLOW: i64 = 4 // the squared comparison would exceed i64: refuse, never wrap |
| 84 | const AB_R_NOTBINARY: i64 = 5 // a proportion test was asked about an axis holding non-0/1 samples |
| 85 | const AB_R_N: i64 = 6 |
| 400 | const AB_SS_COEF: i64 = 4 // the paper's coefficient for 95 percent confidence and 90 percent power |
| 441 | const AB_LN2_Q10: i64 = 710 // ln 2 = 0.693147 x 1024 = 709.78, rounded: the ONE ln 2 in this lib |
| 442 | const AB_LOG2_ONE_Q10: i64 = 10240 // ilog2_1024(1024): log2 of a Q10 one, in Q10 |
| 443 | const AB_I64_MAX: i64 = 9223372036854775807 // the exact product fence: a x b is safe iff a <= MAX / b |
| 444 | const AB_PERMIL: i64 = 1000 |
| 537 | const AB_CV_N: i64 = 0 |
| 538 | const AB_CV_MX: i64 = 1 // running mean of the covariate X, Q10 |
| 539 | const AB_CV_MY: i64 = 2 // running mean of the metric Y, Q10 |
| 540 | const AB_CV_M2X: i64 = 3 // sum of squared deviations of X, Q10 squared |
| 541 | const AB_CV_M2Y: i64 = 4 // sum of squared deviations of Y, Q10 squared |
| 542 | const AB_CV_CXY: i64 = 5 // co-moment sum (X - mx)(Y - my), Q10 squared |
| 543 | const AB_CV_WORDS: i64 = 6 |
| 544 | const AB_CV_POOL: i64 = 2 // the third accumulator: both arms together, the paper's pooled theta |
| 545 | const AB_CV_ACCS: i64 = 3 // B, A, POOL |
| 546 | const AB_CV_TOTAL: i64 = 18 // AB_CV_ACCS x AB_CV_WORDS |
functions
| 87 | func ab_words() -> i64 { return AB_WORDS } |
| 88 | func ab_max_axes() -> i64 { return AB_MAX_AXES } called by 1: main |
| 89 | func ab_min_n() -> i64 { return NX_MC_MIN_N } // the sample floor is the incumbent, not a fresh number |
| 92 | func ab_crit_t_q10(n_axes: i64) -> i64 { return _mv_bonferroni_critical_t_q10(n_axes) } |
| 94 | func ab_init(r: *i64) -> i64 |
| 101 | func ab_base(axis: i64, arm: i64) -> i64 |
| 105 | func ab_valid(axis: i64, arm: i64) -> i64 |
| 118 | func ab_push(r: *i64, axis: i64, arm: i64, raw: i64) -> i64 called by 8: aa_trialaa_seq_trialaa_cv_trialabg_fillmainab_welford_matches_incumbent+2 calls 2: ab_validab_base |
| 140 | func ab_welford_matches_incumbent(s1: i64, s2: i64) -> i64 |
| 160 | func ab_n(r: *i64, axis: i64, arm: i64) -> i64 called by 7: ab_axis_reasonab_se2_q10ab_pooled_p_q10ab_prop_se2_q10ab_prop_reasonab_prop_verdict+1 calls 2: ab_validab_base |
| 165 | func ab_mean_q10(r: *i64, axis: i64, arm: i64) -> i64 |
| 174 | func ab_var_q10(r: *i64, axis: i64, arm: i64) -> i64 |
| 183 | func ab_delta_q10(r: *i64, axis: i64) -> i64 called by 8: aa_seq_trialmainmainab_axis_verdictab_prop_reasonab_prop_verdict+2 calls 1: ab_mean_q10 |
| 192 | func ab_axis_reason(r: *i64, axis: i64) -> i64 |
| 206 | func ab_se2_q10(r: *i64, axis: i64) -> i64 |
| 223 | func ab_axis_verdict(r: *i64, axis: i64, n_axes: i64, higher_is_better: i64, mde_q10: i64) -> i64 called by 6: aa_trialaa_seq_trialaa_cv_trialmainmainmain calls 4: ab_axis_reasonab_delta_q10ab_se2_q10ab_decide |
| 235 | func ab_decide(d_raw: i64, se2: i64, n_axes: i64, higher_is_better: i64, mde_q10: i64) -> i64 |
| 269 | func ab_is_binary(r: *i64, axis: i64, arm: i64) -> i64 |
| 281 | func ab_pooled_p_q10(r: *i64, axis: i64) -> i64 |
| 294 | func ab_prop_se2_q10(r: *i64, axis: i64) -> i64 |
| 312 | func ab_prop_reason(r: *i64, axis: i64) -> i64 |
| 328 | func ab_prop_verdict(r: *i64, axis: i64, n_axes: i64, higher_is_better: i64, mde_q10: i64) -> i64 |
| 362 | func ab_family_verdict(axv: *i64, n_axes: i64) -> i64 |
| 383 | func ab_tested_axes(axv: *i64, n_axes: i64) -> i64 |
| 401 | func ab_sample_size_aa(r_variants: i64, sigma_units: i64, mde_units: i64) -> i64 called by 1: main |
| 448 | func ab_ratio_q10(num: i64, den: i64) -> i64 |
| 461 | func ab_mul_q10(a: i64, b: i64) -> i64 |
| 472 | func ab_ln_q10(x_q10: i64) -> i64 |
| 478 | func ab_av_tau2_from_mde_q10(mde_q10: i64) -> i64 called by 1: main |
| 485 | func ab_av_bar_q10(alpha_permil: i64) -> i64 |
| 495 | func ab_always_valid(delta_q10: i64, se2_q10: i64, tau2_q10: i64) -> i64 |
| 548 | func ab_cuped_words() -> i64 { return AB_CV_TOTAL } called by 1: main |
| 549 | func ab_cuped_init(cv: *i64) -> i64 |
| 554 | func ab_cv_base(acc: i64) -> i64 { return acc * AB_CV_WORDS } |
| 558 | func ab_cv_step(cv: *i64, acc: i64, x: i64, y: i64) -> i64 |
| 579 | func ab_cuped(cv: *i64, arm: i64, y_raw: i64, x_raw: i64) -> i64 |
| 587 | func ab_cuped_n(cv: *i64, acc: i64) -> i64 |
| 595 | func ab_cuped_theta_q10(cv: *i64) -> i64 |
| 605 | func ab_cuped_rho2_q10(cv: *i64) -> i64 |
| 623 | func ab_cuped_mean_q10(cv: *i64, arm: i64, theta_q10: i64) -> i64 |
| 635 | func ab_cuped_delta_q10(cv: *i64, theta_q10: i64) -> i64 |
| 646 | func ab_cuped_var_q10(cv: *i64, arm: i64, theta_q10: i64) -> i64 |
| 664 | func ab_cuped_se2_q10(cv: *i64, theta_q10: i64) -> i64 |
| 679 | func ab_cuped_verdict(cv: *i64, theta_q10: i64, n_axes: i64, higher_is_better: i64, mde_q10: i64) -> i64 |