nx_collocation_test.nx source
↩ module page · 115 lines · 5800 B
1// nx_collocation_test.nx -- smoke for collocation association metrics.
2
3import "nx_syscalls.nx"
4import "nx_tier.nx"
5import "nx_essentials.nx"
6import "nx_collocation.nx"
7
8func main() -> nx_int {
9 // === Test 1: independence (PMI ≈ 0) =============================
10 // If p(a,b) == p(a) * p(b) then PMI = log2(1) = 0.
11 // n_a = 100, n_b = 100, n_total = 10000 -> p(a) = p(b) = 0.01
12 // p(a,b) under independence = 0.0001 -> n_ab = 1
13 let pmi_indep: nx_int = nx_col_pmi_q10(100, 100, 1, 10000)
14 // Expect ≈ 0 (Q10). Allow ±256 (±0.25) slack for log2 refinement.
15 if pmi_indep > 256 { return 1 }
16 if pmi_indep < 0 - 256 { return 2 }
17 if nx_col_pmi_band(pmi_indep) != NX_COL_BAND_WEAK { return 3 }
18
19 // === Test 2: strong positive association ========================
20 // Same n_a, n_b, n_total but n_ab = 50 (50x more co-occurrence
21 // than independence predicted).
22 let pmi_strong: nx_int = nx_col_pmi_q10(100, 100, 50, 10000)
23 // log2(50) - log2(1) = ~5.64 -> Q10 ~ 5775; should land in STRONG
24 if pmi_strong < NX_COL_PMI_STRONG_Q10 { return 10 }
25 if nx_col_pmi_band(pmi_strong) != NX_COL_BAND_STRONG { return 11 }
26
27 // === Test 3: very strong (idiomatic) ============================
28 // n_ab equals n_a entirely (every occurrence of a co-occurs with b).
29 let pmi_very: nx_int = nx_col_pmi_q10(100, 100, 100, 10000)
30 // log2(10000 * 100 / (100 * 100)) = log2(100) ≈ 6.64 -> Q10 ~ 6800
31 if nx_col_pmi_band(pmi_very) != NX_COL_BAND_VERY_STRONG { return 20 }
32
33 // === Test 4: zero co-occurrence -> PMI_FLOOR ====================
34 let pmi_zero: nx_int = nx_col_pmi_q10(100, 100, 0, 10000)
35 if pmi_zero != nx_col_pmi_floor() { return 30 }
36 if nx_col_pmi_band(pmi_zero) != NX_COL_BAND_NONE { return 31 }
37
38 // === Test 5: degenerate inputs ===================================
39 if nx_col_pmi_q10(0, 100, 5, 10000) != 0 { return 40 }
40 if nx_col_pmi_q10(100, 0, 5, 10000) != 0 { return 41 }
41 if nx_col_pmi_q10(100, 100, 5, 0) != 0 { return 42 }
42
43 // === Test 6: log-likelihood smoke ===============================
44 //
45 // Strong association case: same n_a=100, n_b=100, n_ab=50, N=10000.
46 // Expected E11 = 100*100/10000 = 1. O11 = 50, way above expected.
47 // LL should be substantial (Dunning's stat is non-negative; large
48 // when observed and expected diverge sharply).
49 let ll_strong: nx_int = nx_col_log_likelihood_q10(100, 100, 50, 10000)
50 if ll_strong < NX_COL_LL_MODERATE_Q10 { return 50 }
51 let ll_band_strong: nx_int = nx_col_log_likelihood_band(ll_strong)
52 if nx_col_band_is_valid(ll_band_strong) != 1 { return 51 }
53
54 // Independence: LL should be near zero.
55 let ll_indep: nx_int = nx_col_log_likelihood_q10(100, 100, 1, 10000)
56 if ll_indep > NX_COL_LL_WEAK_Q10 { return 52 }
57 if nx_col_log_likelihood_band(ll_indep) != NX_COL_BAND_NONE { return 53 }
58
59 // === Test 7: T-score ============================================
60 //
61 // T = (O - E) / sqrt(O); strong case n_ab=50, E11=1
62 // -> T ≈ (50-1)/sqrt(50) = 49/7.07 ≈ 6.93 -> Q10 ~ 7100
63 let t_strong: nx_int = nx_col_t_score_q10(100, 100, 50, 10000)
64 if nx_col_t_score_band(t_strong) != NX_COL_BAND_VERY_STRONG { return 60 }
65
66 // Independence: T near 0
67 let t_indep: nx_int = nx_col_t_score_q10(100, 100, 1, 10000)
68 if nx_col_t_score_band(t_indep) != NX_COL_BAND_NONE { return 61 }
69
70 // Zero co-occurrence
71 if nx_col_t_score_q10(100, 100, 0, 10000) != 0 { return 62 }
72
73 // === Test 8: log-Dice ============================================
74 //
75 // logDice = 14 + log2(2 * n_ab / (n_a + n_b))
76 // strong case: 14 + log2(100/200) = 14 + log2(0.5) = 14 - 1 = 13
77 let dice_strong: nx_int = nx_col_log_dice_q10(100, 100, 50)
78 // Expect ~13 Q10 = 13312
79 if dice_strong < 12288 { return 70 } // 12.0
80 if dice_strong > 14336 { return 71 } // 14.0
81 if nx_col_log_dice_band(dice_strong) != NX_COL_BAND_VERY_STRONG { return 72 }
82
83 // Independence: n_ab=1 -> 14 + log2(2/200) = 14 - 6.64 = 7.36
84 let dice_indep: nx_int = nx_col_log_dice_q10(100, 100, 1)
85 // STRONG band (7.36 falls in [7, 10))
86 if nx_col_log_dice_band(dice_indep) != NX_COL_BAND_STRONG { return 73 }
87
88 // Zero co-occurrence
89 if nx_col_log_dice_q10(100, 100, 0) != 0 { return 74 }
90
91 // === Test 9: emit_all composite =================================
92 let out: *i64 = (sys_mmap(NX_COL_OUT_FIELDS * NX_SIZEOF_NX_INT)) as *i64
93 nx_col_emit_all(100, 100, 50, 10000, out)
94 // All four metric Q10s should match the standalone calls
95 if out[NX_COL_OUT_PMI_Q10] != pmi_strong { return 80 }
96 if out[NX_COL_OUT_LL_Q10] != ll_strong { return 81 }
97 if out[NX_COL_OUT_T_Q10] != t_strong { return 82 }
98 if out[NX_COL_OUT_DICE_Q10] != dice_strong { return 83 }
99 // All four bands valid sealed-enum
100 if nx_col_band_is_valid(out[NX_COL_OUT_PMI_BAND]) != 1 { return 84 }
101 if nx_col_band_is_valid(out[NX_COL_OUT_LL_BAND]) != 1 { return 85 }
102 if nx_col_band_is_valid(out[NX_COL_OUT_T_BAND]) != 1 { return 86 }
103 if nx_col_band_is_valid(out[NX_COL_OUT_DICE_BAND]) != 1 { return 87 }
104
105 // === Test 10: sealed-enum validators ===========================
106 if nx_col_band_is_valid(NX_COL_BAND_NONE) != 1 { return 90 }
107 if nx_col_band_is_valid(NX_COL_BAND_VERY_STRONG) != 1 { return 91 }
108 if nx_col_band_is_valid(NX_COL_N_BANDS) != 0 { return 92 }
109 if nx_col_band_is_valid(99) != 0 { return 93 }
110 if nx_col_metric_is_valid(NX_COL_METRIC_PMI) != 1 { return 94 }
111 if nx_col_metric_is_valid(NX_COL_METRIC_LOG_DICE) != 1 { return 95 }
112 if nx_col_metric_is_valid(NX_COL_N_METRICS) != 0 { return 96 }
113
114 return 0
115}