code wiki / _hdl_build / nx_ltr_lib_gate.nx
nx_ltr_lib_gate.nx source
↩ module page · 116 lines · 5683 B
1// nx_ltr_lib_gate.nx -- teeth for the learning-to-rank reranker (nx_ltr_lib.ltr_cv) on a synthetic
2// fixture it builds itself (search rung R0, 2026-09-14). A SIGNAL fixture where one feature predicts
3// the sparse relevant candidates -- the trained arm's 5-fold held-out nDCG must be high; a SHUFFLE
4// neg-control where gold is decorrelated from every feature -- with 20 candidates and only 3 relevant
5// a random ordering pushes them out of the top-10, so it must score LOW (the model may not hallucinate
6// signal); determinism (integer, so the same fixture twice is the same number). No corpus, no fork.
7// Declared in organ_gate.conf.
8
9import "nx_ltr_lib.nx"
10import "nx_gate_verdict.nx"
11
12const GL_NQ: i64 = 20
13const GL_NC: i64 = 20 // 20 candidates, only 3 relevant -> a bad ordering loses them past nDCG@10
14const GL_BOX: i64 = 8
15
16// deterministic small pseudo-noise in 0..1023, uncorrelated with the signal below
17func gl_noise(q: i64, c: i64, k: i64) -> i64 {
18 var h: i64 = (q * 131 + c * 17 + k * 1315423911) & 0x7fffffff
19 h = (h * 2654435761) & 0x3ff
20 return h
21}
22
23// idcg@10 for a query's gold row (graded), from a scratch descending sort
24func gl_idcg(gold: *i64, base: i64, nc: i64) -> i64 {
25 let s: *i64 = sys_mmap(nc * 8) as *i64
26 var i: i64 = 0
27 while i < nc { s[i] = gold[base + i]; i = i + 1 }
28 i = 1
29 while i < nc {
30 let cur: i64 = s[i]
31 var j: i64 = i - 1
32 var go: i64 = 1
33 while go == 1 { if j >= 0 { if s[j] < cur { s[j + 1] = s[j]; j = j - 1 } else { go = 0 } } else { go = 0 } }
34 s[j + 1] = cur
35 i = i + 1
36 }
37 var idcg: i64 = 0
38 var r: i64 = 0
39 while r < LTR_TOPK { if r < nc { idcg = idcg + s[r] * ltr_disc(r + 1) } r = r + 1 }
40 sys_munmap(s as *u8, nc * 8)
41 return idcg
42}
43
44func gl_mod(a: i64, m: i64) -> i64 { return a - (a / m) * m }
45
46// build a fixture. mode 0 = SIGNAL (feature 2 high exactly on the relevant candidates), mode 1 =
47// SHUFFLE (feature 2 high on three positions, but the relevant candidates are elsewhere, so no
48// feature tracks relevance). Three relevant per query, gold 3/2/1; every other candidate gold 0.
49func gl_build(feat: *i64, gold: *i64, ncand: *i64, idcg: *i64, fold: *i64, mode: i64) -> i64 {
50 var q: i64 = 0
51 while q < GL_NQ {
52 ncand[q] = GL_NC
53 fold[q] = gl_mod(q, LTR_FOLDS)
54 var c: i64 = 0
55 while c < GL_NC {
56 let base: i64 = (q * GL_NC + c) * LTR_NFEAT
57 var k: i64 = 0
58 while k < LTR_NFEAT { feat[base + k] = gl_noise(q, c, k); k = k + 1 }
59 gold[q * GL_NC + c] = 0
60 c = c + 1
61 }
62 // feature-2 is high on p1,p2,p3 in BOTH modes (so the feature carries the same information)
63 let p1: i64 = gl_mod(q, GL_NC)
64 let p2: i64 = gl_mod(q + 7, GL_NC)
65 let p3: i64 = gl_mod(q + 13, GL_NC)
66 feat[(q * GL_NC + p1) * LTR_NFEAT + 2] = 5000
67 feat[(q * GL_NC + p2) * LTR_NFEAT + 2] = 4000
68 feat[(q * GL_NC + p3) * LTR_NFEAT + 2] = 3000
69 if mode == 0 {
70 // SIGNAL: relevance sits exactly where feature 2 is high
71 gold[q * GL_NC + p1] = 3
72 gold[q * GL_NC + p2] = 2
73 gold[q * GL_NC + p3] = 1
74 } else {
75 // SHUFFLE: relevance sits elsewhere; the high-feature-2 positions are gold 0
76 gold[q * GL_NC + gl_mod(q + 3, GL_NC)] = 3
77 gold[q * GL_NC + gl_mod(q + 11, GL_NC)] = 2
78 gold[q * GL_NC + gl_mod(q + 17, GL_NC)] = 1
79 }
80 idcg[q] = gl_idcg(gold, q * GL_NC, GL_NC)
81 q = q + 1
82 }
83 return 0
84}
85
86func main() -> i64 {
87 gv_puts("=== nx_ltr_lib gate (coordinate-ascent LTR, 5-fold CV, sparse signal/shuffle over 20 candidates) ===\n" as *u8)
88 let ctr: *i64 = gv_ctr()
89 let feat: *i64 = sys_mmap(GL_NQ * GL_NC * LTR_NFEAT * 8) as *i64
90 let gold: *i64 = sys_mmap(GL_NQ * GL_NC * 8) as *i64
91 let ncand: *i64 = sys_mmap(GL_NQ * 8) as *i64
92 let idcg: *i64 = sys_mmap(GL_NQ * 8) as *i64
93 let fold: *i64 = sys_mmap(GL_NQ * 8) as *i64
94 let cnt: *i64 = sys_mmap(GL_BOX) as *i64
95
96 gl_build(feat, gold, ncand, idcg, fold, 0)
97 let cv_signal: i64 = ltr_cv(feat, gold, fold, GL_NQ, ncand, GL_NC, idcg, cnt)
98 let scored: i64 = cnt[0]
99 gv_check("T1 fixture-reached: every query is scored once across the folds" as *u8, (scored == GL_NQ) as i64, ctr)
100 gv_check("T2 the trained arm learns the signal: 5-fold held-out nDCG is high (>800 permil)" as *u8, (cv_signal > 800) as i64, ctr)
101
102 let cv_signal2: i64 = ltr_cv(feat, gold, fold, GL_NQ, ncand, GL_NC, idcg, cnt)
103 gv_check("T3 determinism: the same fixture yields the identical CV number" as *u8, (cv_signal2 == cv_signal) as i64, ctr)
104
105 gl_build(feat, gold, ncand, idcg, fold, 1)
106 let cv_shuffle: i64 = ltr_cv(feat, gold, fold, GL_NQ, ncand, GL_NC, idcg, cnt)
107 gv_check("T4 neg-control-shuffled-labels: gold decorrelated from every feature scores LOW (<450 permil)" as *u8, (cv_shuffle < 450) as i64, ctr)
108 gv_check("T5 neg-control-margin: the signal fixture beats the shuffle by a wide margin (>400 permil)" as *u8, (cv_signal - cv_shuffle > 400) as i64, ctr)
109
110 gv_values_head()
111 gv_kv("cv_signal_permil" as *u8, cv_signal)
112 gv_kv("cv_signal_permil_run2" as *u8, cv_signal2)
113 gv_kv("cv_shuffle_permil" as *u8, cv_shuffle)
114 gv_kv("queries_scored" as *u8, scored)
115 return gv_verdict("NX-LTR-LIB-GATE" as *u8, ctr, "a coordinate-ascent reranker (Metzler-Croft, initialised at bm25-only) evaluated by 5-fold cross-validation learns a sparse signal feature to a high held-out nDCG, refuses to score shuffled labels above the random-ordering floor, and is bit-deterministic" as *u8)
116}