nx_abstat_gate.nx source
↩ module page · 151 lines · 8987 B
1// nx_abstat_gate.nx -- THE REFEREE FOR THE A/B CONTRAST CORE (nx_abstat_lib).
2//
3// A statistics organ is the one kind of organ whose output nobody can eyeball, so every claim it makes has
4// to be bitten here. The teeth below are ordered by what they would cost if they were wrong: a detector that
5// fires on noise is worse than no detector, so the false-positive control comes before the sensitivity one.
6//
7// Every tooth prints the numbers it tested (gv_check_eq / gv_kv), because a PASS-only gate is unfalsifiable
8// from the outside and makes an independent second method structurally impossible.
9// license_tier: ORIGINAL No hw writes (Rule 26).
10import "nx_syscalls.nx"
11import "nx_gate_verdict.nx"
12import "nx_abstat_lib.nx"
13
14const ABG_N: i64 = 40 // comfortably over NX_MC_MIN_N so the sample floor is not what is under test
15const ABG_TIGHT: i64 = 100 // a run of 100 samples for the below-MDE tooth
16
17// deterministic, no PRNG: a fixed sawtooth around a centre, so every run of this gate sees identical inputs
18func abg_saw(i: i64, centre: i64, spread: i64) -> i64 {
19 let phase: i64 = i % 5
20 if phase == 0 { return centre - spread }
21 if phase == 1 { return centre + spread }
22 if phase == 2 { return centre }
23 if phase == 3 { return centre - spread }
24 return centre + spread
25}
26
27func abg_fill(r: *i64, axis: i64, arm: i64, n: i64, centre: i64, spread: i64) -> i64 {
28 var i: i64 = 0
29 while i < n { ab_push(r, axis, arm, abg_saw(i, centre, spread)); i = i + 1 }
30 return 0
31}
32
33func abg_region() -> *i64 {
34 let r: *i64 = sys_mmap(AB_WORDS * 8)
35 ab_init(r)
36 return r
37}
38
39func main() -> i64 {
40 let ctr: *i64 = gv_ctr()
41 gv_head("nx_abstat_gate -- the A/B contrast core" as *u8)
42
43 // ---- the composed rulers are the incumbents, not fresh numbers ------------------------------------
44 gv_check_eq("welford-equivalent-to-incumbent" as *u8, ab_welford_matches_incumbent(4096, 5120), 1, ctr)
45 gv_check_eq("bonferroni-table-is-the-incumbent-5axes" as *u8, ab_crit_t_q10(5), 2641, ctr)
46 gv_check_eq("bonferroni-table-is-the-incumbent-1axis" as *u8, ab_crit_t_q10(1), 2007, ctr)
47 gv_check("bonferroni-actually-tightens-with-more-axes" as *u8,
48 (ab_crit_t_q10(5) > ab_crit_t_q10(1)) as i64, ctr)
49 gv_check_eq("sample-floor-is-the-incumbent-min-n" as *u8, ab_min_n(), 30, ctr)
50
51 // ---- an unmeasured thing must never read as a measured zero --------------------------------------
52 let e: *i64 = abg_region()
53 gv_check_eq("empty-arm-mean-is-UNOBSERVED-not-zero" as *u8, ab_mean_q10(e, 0, AB_ARM_B), 0 - 1, ctr)
54 ab_push(e, 0, AB_ARM_B, 7)
55 gv_check_eq("one-sample-variance-is-UNOBSERVED-not-zero" as *u8, ab_var_q10(e, 0, AB_ARM_B), 0 - 1, ctr)
56
57 // ---- NEG-CONTROL: a fixture the test CANNOT fail must be named, never scored ----------------------
58 // Both arms constant. Delta is zero and variance is zero: reporting NO-CHANGE here would publish a
59 // non-experiment as a measurement. The required answer is INSUFFICIENT with the reason named.
60 let z: *i64 = abg_region()
61 abg_fill(z, 0, AB_ARM_B, ABG_N, 100, 0)
62 abg_fill(z, 0, AB_ARM_A, ABG_N, 100, 0)
63 gv_check_eq("neg-control-zero-variance-reason-is-ZEROVAR" as *u8, ab_axis_reason(z, 0), AB_R_ZEROVAR, ctr)
64 gv_check_eq("neg-control-zero-variance-axis-is-INSUFF" as *u8,
65 ab_axis_verdict(z, 0, 1, 1, 0), AB_AX_INSUFF, ctr)
66
67 // ---- NEG-CONTROL: under the sample floor abstains, and says WHICH arm was short -------------------
68 let f: *i64 = abg_region()
69 abg_fill(f, 0, AB_ARM_B, 29, 100, 3)
70 abg_fill(f, 0, AB_ARM_A, ABG_N, 120, 3)
71 gv_check_eq("neg-control-short-baseline-names-arm-B" as *u8, ab_axis_reason(f, 0), AB_R_FEW_B, ctr)
72 gv_check_eq("neg-control-short-baseline-axis-is-INSUFF" as *u8,
73 ab_axis_verdict(f, 0, 1, 1, 0), AB_AX_INSUFF, ctr)
74
75 // ---- THE FALSE-POSITIVE CONTROL: two arms from the SAME distribution must NOT read as improved ----
76 // Same centre, same spread, different phase. A detector that calls this UP fires on noise, and a
77 // detector that fires on noise is worse than none because it teaches everyone to ignore it.
78 let noise: *i64 = abg_region()
79 var i: i64 = 0
80 while i < ABG_N { ab_push(noise, 0, AB_ARM_B, abg_saw(i, 100, 4)); i = i + 1 }
81 i = 0
82 while i < ABG_N { ab_push(noise, 0, AB_ARM_A, abg_saw(i + 2, 100, 4)); i = i + 1 }
83 let nv: i64 = ab_axis_verdict(noise, 0, 1, 1, 0)
84 gv_kv("noise_control_delta_q10" as *u8, ab_delta_q10(noise, 0))
85 gv_check("false-positive-control-same-distribution-not-UP" as *u8, (nv != AB_AX_UP) as i64, ctr)
86 gv_check("false-positive-control-same-distribution-not-DOWN" as *u8, (nv != AB_AX_DOWN) as i64, ctr)
87
88 // ---- SENSITIVITY: a real separation must be caught ------------------------------------------------
89 let up: *i64 = abg_region()
90 abg_fill(up, 0, AB_ARM_B, ABG_N, 100, 3)
91 abg_fill(up, 0, AB_ARM_A, ABG_N, 130, 3)
92 gv_kv("improve_control_delta_q10" as *u8, ab_delta_q10(up, 0))
93 gv_check_eq("detects-a-real-improvement" as *u8, ab_axis_verdict(up, 0, 1, 1, 0), AB_AX_UP, ctr)
94
95 // ---- THE DIRECTION FLAG IS LOAD-BEARING: identical data, opposite verdict -------------------------
96 // Same region, same samples, higher_is_better flipped. If this tooth passed trivially the flag would be
97 // decorative, and a frame-time axis (smaller is better) would be graded backwards in production.
98 gv_check_eq("direction-flag-inverts-the-same-data" as *u8, ab_axis_verdict(up, 0, 1, 0, 0), AB_AX_DOWN, ctr)
99
100 // ---- a real regression, measured in its own right --------------------------------------------------
101 let dn: *i64 = abg_region()
102 abg_fill(dn, 0, AB_ARM_B, ABG_N, 130, 3)
103 abg_fill(dn, 0, AB_ARM_A, ABG_N, 100, 3)
104 gv_check_eq("detects-a-real-regression" as *u8, ab_axis_verdict(dn, 0, 1, 1, 0), AB_AX_DOWN, ctr)
105
106 // ---- BELOW-MDE IS ITS OWN STATE: significant is not the same as worth shipping --------------------
107 // A large n makes a two-unit shift statistically significant. With a declared MDE of 10 units it must
108 // report BELOW_MDE, not UP -- otherwise the organ launders statistical significance into progress.
109 let sm: *i64 = abg_region()
110 abg_fill(sm, 0, AB_ARM_B, ABG_TIGHT, 100, 1)
111 abg_fill(sm, 0, AB_ARM_A, ABG_TIGHT, 102, 1)
112 let smv: i64 = ab_axis_verdict(sm, 0, 1, 1, 10 * 1024)
113 gv_kv("below_mde_delta_q10" as *u8, ab_delta_q10(sm, 0))
114 gv_check_eq("small-but-significant-is-BELOW-MDE-not-UP" as *u8, smv, AB_AX_BELOW_MDE, ctr)
115 gv_check_eq("same-effect-with-a-low-MDE-is-UP" as *u8, ab_axis_verdict(sm, 0, 1, 1, 1024), AB_AX_UP, ctr)
116
117 // ---- family aggregation ---------------------------------------------------------------------------
118 let axv: *i64 = sys_mmap(8 * 8)
119 axv[0] = AB_AX_UP; axv[1] = AB_AX_UP; axv[2] = AB_AX_UP; axv[3] = AB_AX_DOWN
120 gv_check_eq("regressed-dominates-improved" as *u8, ab_family_verdict(axv, 4), AB_V_REGRESSED, ctr)
121 axv[0] = AB_AX_UP; axv[1] = AB_AX_FLAT; axv[2] = AB_AX_FLAT; axv[3] = AB_AX_FLAT
122 gv_check_eq("one-clean-rise-is-IMPROVED" as *u8, ab_family_verdict(axv, 4), AB_V_IMPROVED, ctr)
123 axv[0] = AB_AX_FLAT; axv[1] = AB_AX_FLAT; axv[2] = AB_AX_FLAT; axv[3] = AB_AX_FLAT
124 gv_check_eq("all-flat-is-NO-CHANGE" as *u8, ab_family_verdict(axv, 4), AB_V_NO_CHANGE, ctr)
125
126 // ---- THE PASS-ON-THE-EMPTY-SET TOOTH ---------------------------------------------------------------
127 axv[0] = AB_AX_INSUFF; axv[1] = AB_AX_INSUFF; axv[2] = AB_AX_INSUFF; axv[3] = AB_AX_INSUFF
128 gv_check_eq("zero-testable-axes-is-INSUFFICIENT-not-NO-CHANGE" as *u8,
129 ab_family_verdict(axv, 4), AB_V_INSUFFICIENT, ctr)
130 gv_check_eq("tested-axis-denominator-is-published" as *u8, ab_tested_axes(axv, 4), 0, ctr)
131 gv_check_eq("no-axes-at-all-is-INSUFFICIENT" as *u8, ab_family_verdict(axv, 0), AB_V_INSUFFICIENT, ctr)
132
133 // ---- the values a second method can recompute from the declared inputs ----------------------------
134 gv_values_head()
135 gv_kv("crit_t_q10_1axis" as *u8, ab_crit_t_q10(1))
136 gv_kv("crit_t_q10_5axes" as *u8, ab_crit_t_q10(5))
137 gv_kv("crit_t_q10_16axes" as *u8, ab_crit_t_q10(16))
138 gv_kv("min_samples_per_arm" as *u8, ab_min_n())
139 gv_kv("region_words" as *u8, ab_words())
140 gv_kv("max_axes" as *u8, ab_max_axes())
141 gv_kv("improve_mean_B_q10" as *u8, ab_mean_q10(up, 0, AB_ARM_B))
142 gv_kv("improve_mean_A_q10" as *u8, ab_mean_q10(up, 0, AB_ARM_A))
143 gv_kv("improve_var_B_q10" as *u8, ab_var_q10(up, 0, AB_ARM_B))
144 gv_kv("improve_var_A_q10" as *u8, ab_var_q10(up, 0, AB_ARM_A))
145 gv_kv("improve_se2_q10" as *u8, ab_se2_q10(up, 0))
146 gv_kv("noise_var_B_q10" as *u8, ab_var_q10(noise, 0, AB_ARM_B))
147 gv_kv("noise_se2_q10" as *u8, ab_se2_q10(noise, 0))
148
149 return gv_verdict("nx_abstat_gate" as *u8, ctr,
150 "two-arm contrast with Bonferroni-corrected per-axis tests; abstains rather than acquits" as *u8)
151}