nx_frameab_gate.nx source
↩ module page · 161 lines · 10372 B
1// nx_frameab_gate.nx -- THE REFEREE FOR THE FRAMETRACE A/B WIRING (nx_frameab).
2//
3// nx_abstat_gate already proves the STATISTICS. This gate proves the three things nx_frameab adds on top of
4// them, each of which can be wrong while the arithmetic stays perfect:
5// 1. the CONFOUND GUARD, in BOTH directions -- a guard that refuses everything passes every deny test, so
6// the positive control (an identical-budget pair it must ALLOW) is the load-bearing tooth here;
7// 2. the two per-frame INDICATOR DERIVATIONS at their boundaries, where an off-by-one hides;
8// 3. the end-to-end push, so a real window really does produce the axis verdicts claimed.
9// license_tier: ORIGINAL No hw writes (Rule 26).
10import "nx_syscalls.nx"
11import "nx_gate_verdict.nx"
12import "nx_frameab.nx"
13
14const FBG_N: i64 = 40
15
16func fbg_perf(centre: i64, spread: i64, budget: i64) -> *i64 {
17 let r: *i64 = sys_mmap(pf_words() * 8)
18 pf_init(r)
19 var i: i64 = 0
20 while i < FBG_N {
21 let ph: i64 = i % 5
22 var v: i64 = centre
23 if ph == 0 { v = centre - spread }
24 if ph == 1 { v = centre + spread }
25 if ph == 3 { v = centre - spread }
26 if ph == 4 { v = centre + spread }
27 pf_push(r, v, budget)
28 i = i + 1
29 }
30 return r
31}
32
33func main() -> i64 {
34 let ctr: *i64 = gv_ctr()
35 gv_head("nx_frameab_gate -- the frametrace A/B wiring" as *u8)
36
37 // ---- 1. THE CONFOUND GUARD, BOTH DIRECTIONS ------------------------------------------------------
38 gv_check_eq("confound-guard-refuses-differing-budgets" as *u8, fab_confounded(16, 94), 1, ctr)
39 gv_check_eq("confound-guard-refuses-either-order" as *u8, fab_confounded(94, 16), 1, ctr)
40 // POSITIVE CONTROL, and it is the load-bearing one: a guard that refused every pair would satisfy both
41 // teeth above and be useless. This is the input it MUST let through.
42 gv_check_eq("pos-control-confound-guard-ALLOWS-equal-budgets" as *u8, fab_confounded(16, 16), 0, ctr)
43 gv_check_eq("pos-control-confound-guard-allows-equal-at-6ms" as *u8, fab_confounded(6, 6), 0, ctr)
44
45 // ---- 2. INDICATOR DERIVATIONS AT THEIR BOUNDARIES -------------------------------------------------
46 gv_check_eq("over-budget-strictly-greater-fires" as *u8, fab_over_of(17, 16), 1, ctr)
47 gv_check_eq("over-budget-EQUAL-is-not-over" as *u8, fab_over_of(16, 16), 0, ctr)
48 gv_check_eq("over-budget-under-is-not-over" as *u8, fab_over_of(15, 16), 0, ctr)
49 gv_check_eq("jank-strictly-above-2x-median-fires" as *u8, fab_jank_of(41, 20), 1, ctr)
50 gv_check_eq("jank-EXACTLY-2x-median-is-not-jank" as *u8, fab_jank_of(40, 20), 0, ctr)
51 gv_check_eq("jank-below-2x-median-is-not-jank" as *u8, fab_jank_of(39, 20), 0, ctr)
52 // an unobservable median must ABSTAIN to 0 rather than divide or fire
53 gv_check_eq("jank-with-no-median-abstains-to-zero" as *u8, fab_jank_of(999, 0), 0, ctr)
54 gv_check_eq("jank-with-negative-median-abstains-to-zero" as *u8, fab_jank_of(999, 0 - 1), 0, ctr)
55
56 // ---- 3. END TO END: two real windows through the real push --------------------------------------
57 let pb: *i64 = fbg_perf(20, 2, 16)
58 let pa: *i64 = fbg_perf(14, 2, 16)
59 gv_check_eq("baseline-window-filled" as *u8, pf_n(pb), FBG_N, ctr)
60 gv_check_eq("candidate-window-filled" as *u8, pf_n(pa), FBG_N, ctr)
61
62 let reg: *i64 = sys_mmap(ab_words() * 8)
63 ab_init(reg)
64 gv_check_eq("push-arm-returns-the-window-count" as *u8, fab_push_arm(reg, AB_ARM_B, pb, 16), FBG_N, ctr)
65 fab_push_arm(reg, AB_ARM_A, pa, 16)
66
67 // the frame-time axis is LOWER-IS-BETTER, so a faster candidate must read UP
68 let vframe: i64 = ab_axis_verdict(reg, FAB_AX_FRAME, FAB_AXES, 0, 1024)
69 gv_kv("frame_delta_q10" as *u8, ab_delta_q10(reg, FAB_AX_FRAME))
70 gv_check_eq("faster-candidate-reads-UP-on-frame-ms" as *u8, vframe, AB_AX_UP, ctr)
71
72 // THE INSTRUMENT CHOICE IS LOAD-BEARING, AND THIS PAIR OF TEETH IS THE PROOF. Every baseline frame is
73 // over a 16 ms budget and no candidate frame is: a true 100-percent-to-0 shift with BOTH arms constant.
74 // Under the CONTINUOUS test the denominator is undefined and the honest answer is INSUFFICIENT. Under
75 // the PROPORTION test -- where spread is a function of the mean rather than estimated apart from it --
76 // the same data is decidable. Running both on ONE fixture is what shows the choice is not cosmetic.
77 gv_kv("over_delta_q10" as *u8, ab_delta_q10(reg, FAB_AX_OVER))
78 gv_check_eq("degenerate-rate-under-CONTINUOUS-test-abstains" as *u8,
79 ab_axis_verdict(reg, FAB_AX_OVER, FAB_AXES, 0, 0), AB_AX_INSUFF, ctr)
80 gv_check_eq("degenerate-rate-continuous-reason-is-ZEROVAR" as *u8,
81 ab_axis_reason(reg, FAB_AX_OVER), AB_R_ZEROVAR, ctr)
82 let vover: i64 = ab_prop_verdict(reg, FAB_AX_OVER, FAB_AXES, 0, 0)
83 gv_check_eq("SAME-DATA-under-PROPORTION-test-is-decided-UP" as *u8, vover, AB_AX_UP, ctr)
84 gv_check_eq("proportion-axis-is-recognised-as-binary" as *u8, ab_is_binary(reg, FAB_AX_OVER, AB_ARM_B), 1, ctr)
85 // THE TOOTH THAT CATCHES A SELF-CONTRADICTING LINE: this axis is DECIDED, so its reason must say OK.
86 // The live organ printed "verdict=UP reason=ZERO-VARIANCE-NOT-A-TEST" until the reason was taken from
87 // the same test as the verdict; a reader trusts the reason field, so a stale one is worse than none.
88 gv_check_eq("decided-proportion-axis-reason-is-OK-not-an-abstention" as *u8,
89 ab_prop_reason(reg, FAB_AX_OVER), AB_R_OK, ctr)
90 gv_check_eq("proportion-reason-refuses-a-continuous-axis-BY-NAME" as *u8,
91 ab_prop_reason(reg, FAB_AX_FRAME), AB_R_NOTBINARY, ctr)
92 // NEG-CONTROL FOR THE NEW TEST: a CONTINUOUS axis is not a proportion, and asking the proportion test
93 // about it must REFUSE rather than return a confident number from invalid arithmetic.
94 gv_check_eq("neg-control-continuous-axis-is-not-binary" as *u8, ab_is_binary(reg, FAB_AX_FRAME, AB_ARM_B), 0, ctr)
95 gv_check_eq("neg-control-proportion-test-REFUSES-a-continuous-axis" as *u8,
96 ab_prop_verdict(reg, FAB_AX_FRAME, FAB_AXES, 0, 0), AB_AX_INSUFF, ctr)
97
98 let axv: *i64 = sys_mmap(FAB_AXES * 8)
99 axv[FAB_AX_FRAME] = vframe
100 axv[FAB_AX_OVER] = vover
101 axv[FAB_AX_JANK] = ab_prop_verdict(reg, FAB_AX_JANK, FAB_AXES, 0, 0)
102 gv_check_eq("family-is-IMPROVED" as *u8, ab_family_verdict(axv, FAB_AXES), AB_V_IMPROVED, ctr)
103 // THE DENOMINATOR IS THE POINT: an IMPROVED computed over 2 of 3 axes is not the claim an IMPROVED over
104 // 3 of 3 would be, and a consumer that prints only the word publishes the stronger one. Picking the
105 // right instrument per axis moved this from 1 to 2 -- the third is jank, genuinely zero in both arms.
106 gv_check_eq("tested-axis-denominator-is-two-of-three" as *u8, ab_tested_axes(axv, FAB_AXES), 2, ctr)
107 gv_check_eq("jank-identical-and-zero-in-both-arms-truly-abstains" as *u8,
108 axv[FAB_AX_JANK], AB_AX_INSUFF, ctr)
109
110 // ---- THE PRODUCTION ESTIMATOR IS fab_push_window, NOT fab_push_arm, AND IT NEEDS ITS OWN TEETH ----
111 // fab_push_arm builds a synthetic arm with a known distribution and is how the STATISTICS above are
112 // tested. Production pushes ONE observation per WINDOW, because frames inside a run are not independent
113 // draws. A gate that only exercised the fixture builder would prove the arithmetic and say nothing
114 // about the estimator that actually ships -- the vacuous-test defect one layer up.
115 let wreg: *i64 = sys_mmap(ab_words() * 8)
116 ab_init(wreg)
117 gv_check_eq("window-push-contributes-exactly-ONE-observation" as *u8,
118 fab_push_window(wreg, AB_ARM_B, pb, 16), 1, ctr)
119 gv_check_eq("one-window-gives-n-of-one-not-the-frame-count" as *u8, ab_n(wreg, FAB_AX_FRAME, AB_ARM_B), 1, ctr)
120 // the window MEAN is carried in MICROSECONDS so it survives a 1 ms frame timer: the baseline window is
121 // a sawtooth about 20 ms, so its mean must land at 20000 us, not 20
122 gv_kv("window_mean_us_q10" as *u8, ab_mean_q10(wreg, FAB_AX_FRAME, AB_ARM_B))
123 gv_check_eq("window-mean-is-microseconds-not-milliseconds" as *u8,
124 ab_mean_q10(wreg, FAB_AX_FRAME, AB_ARM_B), 20000 * 1024, ctr)
125 // pushing more windows raises n by one each: the sample size is the RUN COUNT
126 fab_push_window(wreg, AB_ARM_B, pa, 16)
127 gv_check_eq("a-second-window-makes-n-two" as *u8, ab_n(wreg, FAB_AX_FRAME, AB_ARM_B), 2, ctr)
128 // ...and a single window per arm must ABSTAIN, because one run cannot estimate its own variance
129 let sreg: *i64 = sys_mmap(ab_words() * 8)
130 ab_init(sreg)
131 fab_push_window(sreg, AB_ARM_B, pb, 16)
132 fab_push_window(sreg, AB_ARM_A, pa, 16)
133 gv_check_eq("one-window-per-arm-abstains-TOO-FEW-not-ZEROVAR" as *u8,
134 ab_axis_reason(sreg, FAB_AX_FRAME), AB_R_FEW_B, ctr)
135 gv_check_eq("one-window-per-arm-verdict-is-INSUFF" as *u8,
136 ab_axis_verdict(sreg, FAB_AX_FRAME, FAB_AXES, 0, 1024), AB_AX_INSUFF, ctr)
137
138 // a REGRESSION must survive the same path: swap the arms and the frame axis must invert
139 let reg2: *i64 = sys_mmap(ab_words() * 8)
140 ab_init(reg2)
141 fab_push_arm(reg2, AB_ARM_B, pa, 16)
142 fab_push_arm(reg2, AB_ARM_A, pb, 16)
143 gv_check_eq("swapped-arms-read-DOWN-on-frame-ms" as *u8,
144 ab_axis_verdict(reg2, FAB_AX_FRAME, FAB_AXES, 0, 1024), AB_AX_DOWN, ctr)
145
146 gv_values_head()
147 gv_kv("axes_declared" as *u8, FAB_AXES)
148 gv_kv("bonferroni_crit_t_q10_at_3_axes" as *u8, ab_crit_t_q10(FAB_AXES))
149 gv_kv("crit_t_q10_at_1_axis_for_contrast" as *u8, ab_crit_t_q10(1))
150 gv_kv("mde_ms_default_is_the_sample_quantum" as *u8, FAB_MDE_MS_DEFAULT)
151 gv_kv("jank_multiplier" as *u8, FAB_JANK_MULT)
152 gv_kv("frame_mean_b_q10" as *u8, ab_mean_q10(reg, FAB_AX_FRAME, AB_ARM_B))
153 gv_kv("frame_mean_a_q10" as *u8, ab_mean_q10(reg, FAB_AX_FRAME, AB_ARM_A))
154 gv_kv("frame_var_b_q10" as *u8, ab_var_q10(reg, FAB_AX_FRAME, AB_ARM_B))
155 gv_kv("frame_se2_q10" as *u8, ab_se2_q10(reg, FAB_AX_FRAME))
156 gv_kv("over_mean_b_q10_all_frames_over" as *u8, ab_mean_q10(reg, FAB_AX_OVER, AB_ARM_B))
157 gv_kv("over_mean_a_q10_no_frames_over" as *u8, ab_mean_q10(reg, FAB_AX_OVER, AB_ARM_A))
158
159 return gv_verdict("nx_frameab_gate" as *u8, ctr,
160 "confound guard proven in both directions; indicator boundaries pinned; degenerate rate axis abstains" as *u8)
161}