code wiki / (root) / nx_abstat_lib.nx

nx_abstat_lib.nx

buildroot/runtime/nx_abstat_lib.nx

38479 B688 linesdepth 6pulls 9 transitivereach 4 importersview sourcekind library
docsdependenciesstructsconstsfunctions

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

nx_multivariate.nx nx_intlog.nx nx_abstat_lib.nx nx_abstat_aa_gate.nx nx_abstat_gate.nx nx_frameab.nx

imports: nx_multivariate.nxnx_intlog.nx

imported by: nx_abstat_aa_gate.nxnx_abstat_gate.nxnx_frameab.nx

structs

none

consts

45const AB_A_N: i64 = 0 // samples pushed into this arm
46const AB_A_MEAN: i64 = 1 // Welford running mean, Q10
47const AB_A_M2: i64 = 2 // Welford sum of squared deviations, Q10 squared
48const AB_A_MIN: i64 = 3 // Q10
49const AB_A_MAX: i64 = 4 // Q10
50const AB_A_WORDS: i64 = 5
53const AB_ARM_B: i64 = 0
54const AB_ARM_A: i64 = 1
55const AB_ARMS: i64 = 2
56const AB_MAX_AXES: i64 = 16 // _mv_bonferroni_critical_t_q10 caps its table at 16; past that it repeats
57const AB_WORDS: i64 = 160 // AB_MAX_AXES * AB_ARMS * AB_A_WORDS
59const AB_Q10: i64 = 1024 // NX_MC_Q10, restated as a local name only for arithmetic readability
60const AB_UNOBSERVED: i64 = 0 - 1
61const AB_HUGE: i64 = 3037000499 // floor(sqrt(i64max)): the squaring fence, so a product never wraps
64const AB_AX_UP: i64 = 0 // significantly better AND at or above the pre-declared effect floor
65const AB_AX_DOWN: i64 = 1 // significantly worse
66const AB_AX_FLAT: i64 = 2 // the contrast does not clear the corrected critical value
67const AB_AX_BELOW_MDE: i64 = 3 // significant, but the effect is smaller than the caller declared to matter
68const AB_AX_INSUFF: i64 = 4 // cannot be tested at all: too few samples, or no within-arm variance
69const AB_AX_N: i64 = 5
72const AB_V_IMPROVED: i64 = 0
73const AB_V_REGRESSED: i64 = 1
74const AB_V_NO_CHANGE: i64 = 2
75const AB_V_INSUFFICIENT: i64 = 3
76const AB_V_N: i64 = 4
79const AB_R_OK: i64 = 0
80const AB_R_FEW_B: i64 = 1 // baseline arm under the sample floor
81const AB_R_FEW_A: i64 = 2 // candidate arm under the sample floor
82const AB_R_ZEROVAR: i64 = 3 // BOTH arms have zero within-arm variance: the fixture cannot fail
83const AB_R_OVERFLOW: i64 = 4 // the squared comparison would exceed i64: refuse, never wrap
84const AB_R_NOTBINARY: i64 = 5 // a proportion test was asked about an axis holding non-0/1 samples
85const AB_R_N: i64 = 6
400const AB_SS_COEF: i64 = 4 // the paper's coefficient for 95 percent confidence and 90 percent power
441const AB_LN2_Q10: i64 = 710 // ln 2 = 0.693147 x 1024 = 709.78, rounded: the ONE ln 2 in this lib
442const AB_LOG2_ONE_Q10: i64 = 10240 // ilog2_1024(1024): log2 of a Q10 one, in Q10
443const AB_I64_MAX: i64 = 9223372036854775807 // the exact product fence: a x b is safe iff a <= MAX / b
444const AB_PERMIL: i64 = 1000
537const AB_CV_N: i64 = 0
538const AB_CV_MX: i64 = 1 // running mean of the covariate X, Q10
539const AB_CV_MY: i64 = 2 // running mean of the metric Y, Q10
540const AB_CV_M2X: i64 = 3 // sum of squared deviations of X, Q10 squared
541const AB_CV_M2Y: i64 = 4 // sum of squared deviations of Y, Q10 squared
542const AB_CV_CXY: i64 = 5 // co-moment sum (X - mx)(Y - my), Q10 squared
543const AB_CV_WORDS: i64 = 6
544const AB_CV_POOL: i64 = 2 // the third accumulator: both arms together, the paper's pooled theta
545const AB_CV_ACCS: i64 = 3 // B, A, POOL
546const AB_CV_TOTAL: i64 = 18 // AB_CV_ACCS x AB_CV_WORDS

functions

87func ab_words() -> i64 { return AB_WORDS }
called by 4: mainmainmainmain
88func ab_max_axes() -> i64 { return AB_MAX_AXES }
called by 1: main
89func ab_min_n() -> i64 { return NX_MC_MIN_N } // the sample floor is the incumbent, not a fresh number
92func ab_crit_t_q10(n_axes: i64) -> i64 { return _mv_bonferroni_critical_t_q10(n_axes) }
94func ab_init(r: *i64) -> i64
101func ab_base(axis: i64, arm: i64) -> i64
105func ab_valid(axis: i64, arm: i64) -> i64
118func ab_push(r: *i64, axis: i64, arm: i64, raw: i64) -> i64
140func ab_welford_matches_incumbent(s1: i64, s2: i64) -> i64
160func ab_n(r: *i64, axis: i64, arm: i64) -> i64
165func ab_mean_q10(r: *i64, axis: i64, arm: i64) -> i64
174func ab_var_q10(r: *i64, axis: i64, arm: i64) -> i64
183func ab_delta_q10(r: *i64, axis: i64) -> i64
192func ab_axis_reason(r: *i64, axis: i64) -> i64
206func ab_se2_q10(r: *i64, axis: i64) -> i64
223func ab_axis_verdict(r: *i64, axis: i64, n_axes: i64, higher_is_better: i64, mde_q10: i64) -> i64
235func ab_decide(d_raw: i64, se2: i64, n_axes: i64, higher_is_better: i64, mde_q10: i64) -> i64
269func ab_is_binary(r: *i64, axis: i64, arm: i64) -> i64
281func ab_pooled_p_q10(r: *i64, axis: i64) -> i64
called by 2: ab_prop_se2_q10main calls 2: ab_nab_mean_q10
294func ab_prop_se2_q10(r: *i64, axis: i64) -> i64
312func ab_prop_reason(r: *i64, axis: i64) -> i64
328func ab_prop_verdict(r: *i64, axis: i64, n_axes: i64, higher_is_better: i64, mde_q10: i64) -> i64
362func ab_family_verdict(axv: *i64, n_axes: i64) -> i64
called by 3: mainmainmain
383func ab_tested_axes(axv: *i64, n_axes: i64) -> i64
called by 3: mainmainmain
401func ab_sample_size_aa(r_variants: i64, sigma_units: i64, mde_units: i64) -> i64
called by 1: main
448func ab_ratio_q10(num: i64, den: i64) -> i64
461func ab_mul_q10(a: i64, b: i64) -> i64
472func ab_ln_q10(x_q10: i64) -> i64
478func ab_av_tau2_from_mde_q10(mde_q10: i64) -> i64
called by 1: main
485func ab_av_bar_q10(alpha_permil: i64) -> i64
called by 1: main calls 1: ab_ln_q10
495func ab_always_valid(delta_q10: i64, se2_q10: i64, tau2_q10: i64) -> i64
548func ab_cuped_words() -> i64 { return AB_CV_TOTAL }
called by 1: main
549func ab_cuped_init(cv: *i64) -> i64
called by 2: aa_cv_trialmain
554func ab_cv_base(acc: i64) -> i64 { return acc * AB_CV_WORDS }
558func ab_cv_step(cv: *i64, acc: i64, x: i64, y: i64) -> i64
called by 1: ab_cuped calls 1: ab_cv_base
579func ab_cuped(cv: *i64, arm: i64, y_raw: i64, x_raw: i64) -> i64
called by 2: aa_cv_trialmain calls 1: ab_cv_step
587func ab_cuped_n(cv: *i64, acc: i64) -> i64
595func ab_cuped_theta_q10(cv: *i64) -> i64
605func ab_cuped_rho2_q10(cv: *i64) -> i64
623func ab_cuped_mean_q10(cv: *i64, arm: i64, theta_q10: i64) -> i64
635func ab_cuped_delta_q10(cv: *i64, theta_q10: i64) -> i64
646func ab_cuped_var_q10(cv: *i64, arm: i64, theta_q10: i64) -> i64
664func ab_cuped_se2_q10(cv: *i64, theta_q10: i64) -> i64
679func ab_cuped_verdict(cv: *i64, theta_q10: i64, n_axes: i64, higher_is_better: i64, mde_q10: i64) -> i64