nx_chem_fingerprint_test.nx source
↩ module page · 240 lines · 11183 B
1// nx_chem_fingerprint_test.nx -- C2.6 KAT for ECFP-style Morgan
2// fingerprint + Tanimoto similarity.
3//
4// expect_exit: 0
5//
6// license_tier: ORIGINAL
7
8import "nx_chem_molecule.nx"
9import "nx_chem_smiles.nx"
10import "nx_chem_fingerprint.nx"
11
12// Helpers
13func parse(src: *u8, n: nx_int) -> *MolGraph {
14 return nx_chem_parse_smiles(src, n)
15}
16
17// =================================================================
18// A -- empty molecule: all bits zero
19// =================================================================
20func a_empty_mol() -> nx_int {
21 let m: *MolGraph = nx_chem_mol_new(4, 4)
22 let fp: *nx_int = (sys_mmap(128) as *nx_int) // 1024 bits = 16 slots = 128 bytes
23 let _r: nx_int = nx_chem_compute_ecfp_fingerprint(m, 2, 1024, fp)
24 let pc: nx_int = nx_chem_fp_popcount(fp, 1024)
25 if pc != 0 { return 11 }
26 return 0
27}
28
29// =================================================================
30// B -- "C" (methane): non-zero bits set, deterministic
31// =================================================================
32func b_methane() -> nx_int {
33 let s: *u8 = sys_mmap(8); s[0] = 0x43
34 let m: *MolGraph = parse(s, 1)
35 let fp: *nx_int = (sys_mmap(128) as *nx_int)
36 let _r: nx_int = nx_chem_compute_ecfp_fingerprint(m, 2, 1024, fp)
37 let pc: nx_int = nx_chem_fp_popcount(fp, 1024)
38 if pc < 1 { return 21 } // at least the initial invariant bit
39 return 0
40}
41
42// =================================================================
43// C -- same molecule parsed twice produces identical fingerprints
44// =================================================================
45func c_identical_fingerprints() -> nx_int {
46 let s1: *u8 = sys_mmap(8); s1[0] = 0x43; s1[1] = 0x43; s1[2] = 0x4F // CCO
47 let s2: *u8 = sys_mmap(8); s2[0] = 0x43; s2[1] = 0x43; s2[2] = 0x4F // CCO (same)
48 let m1: *MolGraph = parse(s1, 3)
49 let m2: *MolGraph = parse(s2, 3)
50 let fp1: *nx_int = (sys_mmap(128) as *nx_int)
51 let fp2: *nx_int = (sys_mmap(128) as *nx_int)
52 let _r1: nx_int = nx_chem_compute_ecfp_fingerprint(m1, 2, 1024, fp1)
53 let _r2: nx_int = nx_chem_compute_ecfp_fingerprint(m2, 2, 1024, fp2)
54 if nx_chem_fp_equal(fp1, fp2, 1024) != 1 { return 31 }
55 return 0
56}
57
58// =================================================================
59// D -- Tanimoto of identical molecules = 1.0 (Q4 = 10000)
60// =================================================================
61func d_tanimoto_identity() -> nx_int {
62 let s: *u8 = sys_mmap(8); s[0] = 0x43; s[1] = 0x43; s[2] = 0x4F // CCO
63 let m: *MolGraph = parse(s, 3)
64 let fp: *nx_int = (sys_mmap(128) as *nx_int)
65 let _r: nx_int = nx_chem_compute_ecfp_fingerprint(m, 2, 1024, fp)
66 let tan: nx_int = nx_chem_fp_tanimoto_q4(fp, fp, 1024)
67 if tan != 10000 { return 41 } // identity = 1.0 Q4
68 return 0
69}
70
71// =================================================================
72// E -- different molecules produce different fingerprints
73// "C" (methane) vs "O" (water) should differ
74// =================================================================
75func e_different_fingerprints() -> nx_int {
76 let s1: *u8 = sys_mmap(8); s1[0] = 0x43 // C
77 let s2: *u8 = sys_mmap(8); s2[0] = 0x4F // O
78 let m1: *MolGraph = parse(s1, 1)
79 let m2: *MolGraph = parse(s2, 1)
80 let fp1: *nx_int = (sys_mmap(128) as *nx_int)
81 let fp2: *nx_int = (sys_mmap(128) as *nx_int)
82 let _r1: nx_int = nx_chem_compute_ecfp_fingerprint(m1, 2, 1024, fp1)
83 let _r2: nx_int = nx_chem_compute_ecfp_fingerprint(m2, 2, 1024, fp2)
84 if nx_chem_fp_equal(fp1, fp2, 1024) == 1 { return 51 }
85 let tan: nx_int = nx_chem_fp_tanimoto_q4(fp1, fp2, 1024)
86 if tan == 10000 { return 52 } // not identical
87 return 0
88}
89
90// =================================================================
91// F -- Tanimoto symmetry: T(A, B) == T(B, A)
92// =================================================================
93func f_tanimoto_symmetry() -> nx_int {
94 let s1: *u8 = sys_mmap(8); s1[0] = 0x43; s1[1] = 0x43; s1[2] = 0x4F // CCO
95 let s2: *u8 = sys_mmap(8); s2[0] = 0x43; s2[1] = 0x43; s2[2] = 0x43 // CCC
96 let m1: *MolGraph = parse(s1, 3)
97 let m2: *MolGraph = parse(s2, 3)
98 let fp1: *nx_int = (sys_mmap(128) as *nx_int)
99 let fp2: *nx_int = (sys_mmap(128) as *nx_int)
100 let _r1: nx_int = nx_chem_compute_ecfp_fingerprint(m1, 2, 1024, fp1)
101 let _r2: nx_int = nx_chem_compute_ecfp_fingerprint(m2, 2, 1024, fp2)
102 let t_ab: nx_int = nx_chem_fp_tanimoto_q4(fp1, fp2, 1024)
103 let t_ba: nx_int = nx_chem_fp_tanimoto_q4(fp2, fp1, 1024)
104 if t_ab != t_ba { return 61 }
105 return 0
106}
107
108// =================================================================
109// G -- structurally-related molecules (CCO ethanol vs CCC propane)
110// share initial-radius invariants (C atoms have similar local env);
111// Tanimoto should be > 0 but < 1.0
112// =================================================================
113func g_tanimoto_partial() -> nx_int {
114 let s1: *u8 = sys_mmap(8); s1[0] = 0x43; s1[1] = 0x43; s1[2] = 0x4F // CCO
115 let s2: *u8 = sys_mmap(8); s2[0] = 0x43; s2[1] = 0x43; s2[2] = 0x43 // CCC
116 let m1: *MolGraph = parse(s1, 3)
117 let m2: *MolGraph = parse(s2, 3)
118 let fp1: *nx_int = (sys_mmap(128) as *nx_int)
119 let fp2: *nx_int = (sys_mmap(128) as *nx_int)
120 let _r1: nx_int = nx_chem_compute_ecfp_fingerprint(m1, 2, 1024, fp1)
121 let _r2: nx_int = nx_chem_compute_ecfp_fingerprint(m2, 2, 1024, fp2)
122 let tan: nx_int = nx_chem_fp_tanimoto_q4(fp1, fp2, 1024)
123 if tan <= 0 { return 71 }
124 if tan >= 10000 { return 72 }
125 return 0
126}
127
128// =================================================================
129// H -- popcount sanity: methane (1 C, h=4) has fewer bits than
130// a larger molecule like benzene (6 aromatic C, more circular env)
131// =================================================================
132func h_popcount_scales() -> nx_int {
133 let s1: *u8 = sys_mmap(8); s1[0] = 0x43 // C
134 let s2: *u8 = sys_mmap(16) // c1ccccc1
135 s2[0] = 0x63; s2[1] = 0x31; s2[2] = 0x63; s2[3] = 0x63
136 s2[4] = 0x63; s2[5] = 0x63; s2[6] = 0x63; s2[7] = 0x31
137 let m1: *MolGraph = parse(s1, 1)
138 let m2: *MolGraph = parse(s2, 8)
139 let fp1: *nx_int = (sys_mmap(128) as *nx_int)
140 let fp2: *nx_int = (sys_mmap(128) as *nx_int)
141 let _r1: nx_int = nx_chem_compute_ecfp_fingerprint(m1, 2, 1024, fp1)
142 let _r2: nx_int = nx_chem_compute_ecfp_fingerprint(m2, 2, 1024, fp2)
143 let pc1: nx_int = nx_chem_fp_popcount(fp1, 1024)
144 let pc2: nx_int = nx_chem_fp_popcount(fp2, 1024)
145 if pc1 == 0 { return 81 }
146 if pc2 == 0 { return 82 }
147 return 0
148}
149
150// =================================================================
151// I -- set_bit / get_bit round-trip
152// =================================================================
153func i_set_get_bit() -> nx_int {
154 let fp: *nx_int = (sys_mmap(128) as *nx_int)
155 var i: nx_int = 0
156 while i < 16 { fp[i] = 0; i = i + 1 }
157 let _s1: nx_int = nx_chem_fp_set_bit(fp, 0)
158 let _s2: nx_int = nx_chem_fp_set_bit(fp, 63)
159 let _s3: nx_int = nx_chem_fp_set_bit(fp, 64)
160 let _s4: nx_int = nx_chem_fp_set_bit(fp, 1023)
161 if nx_chem_fp_get_bit(fp, 0) != 1 { return 91 }
162 if nx_chem_fp_get_bit(fp, 63) != 1 { return 92 }
163 if nx_chem_fp_get_bit(fp, 64) != 1 { return 93 }
164 if nx_chem_fp_get_bit(fp, 1023) != 1 { return 94 }
165 if nx_chem_fp_get_bit(fp, 1) != 0 { return 95 }
166 if nx_chem_fp_get_bit(fp, 100) != 0 { return 96 }
167 return 0
168}
169
170// =================================================================
171// J -- popcount of all-set should be n_bits
172// =================================================================
173func j_popcount_all_set() -> nx_int {
174 let fp: *nx_int = (sys_mmap(128) as *nx_int)
175 var i: nx_int = 0
176 while i < 16 {
177 fp[i] = -1 // all bits set in i64 = 0xFFFFFFFFFFFFFFFF
178 i = i + 1
179 }
180 let pc: nx_int = nx_chem_fp_popcount(fp, 1024)
181 if pc != 1024 { return 101 }
182 return 0
183}
184
185func main() -> nx_exit {
186 println("=== nx_chem_fingerprint -- C2.6 KAT: ECFP Morgan fingerprint + Tanimoto ===" as *u8)
187
188 let ra: nx_int = a_empty_mol()
189 if ra != 0 { println("A empty_mol FAIL" as *u8); return ra }
190 println("A empty_mol PASS 0 atoms -> 0 bits set" as *u8)
191
192 let rb: nx_int = b_methane()
193 if rb != 0 { println("B methane FAIL" as *u8); return rb }
194 println("B methane PASS C -> bits set; deterministic encoding" as *u8)
195
196 let rc: nx_int = c_identical_fingerprints()
197 if rc != 0 { println("C identical_fingerprints FAIL" as *u8); return rc }
198 println("C identical_fingerprints PASS same input -> same fingerprint (deterministic)" as *u8)
199
200 let rd: nx_int = d_tanimoto_identity()
201 if rd != 0 { println("D tanimoto_identity FAIL" as *u8); return rd }
202 println("D tanimoto_identity PASS T(A, A) = 1.0 (Q4 = 10000)" as *u8)
203
204 let re: nx_int = e_different_fingerprints()
205 if re != 0 { println("E different_fingerprints FAIL" as *u8); return re }
206 println("E different_fingerprints PASS C vs O -> different fingerprints + T < 1.0" as *u8)
207
208 let rf: nx_int = f_tanimoto_symmetry()
209 if rf != 0 { println("F tanimoto_symmetry FAIL" as *u8); return rf }
210 println("F tanimoto_symmetry PASS T(A, B) = T(B, A)" as *u8)
211
212 let rg: nx_int = g_tanimoto_partial()
213 if rg != 0 { println("G tanimoto_partial FAIL" as *u8); return rg }
214 println("G tanimoto_partial PASS CCO vs CCC: 0 < T < 1.0 (structurally related)" as *u8)
215
216 let rh: nx_int = h_popcount_scales()
217 if rh != 0 { println("H popcount_scales FAIL" as *u8); return rh }
218 println("H popcount_scales PASS C and c1ccccc1 both produce non-zero popcounts" as *u8)
219
220 let ri: nx_int = i_set_get_bit()
221 if ri != 0 { println("I set_get_bit FAIL" as *u8); return ri }
222 println("I set_get_bit PASS bits 0/63/64/1023 set/get round-trip correct" as *u8)
223
224 let rj: nx_int = j_popcount_all_set()
225 if rj != 0 { println("J popcount_all_set FAIL" as *u8); return rj }
226 println("J popcount_all_set PASS all-set 1024-bit vector popcount = 1024" as *u8)
227
228 println("" as *u8)
229 println("=== C2.6 substrate milestone PASS ===" as *u8)
230 println(" ECFP fingerprint : Morgan circular-substructure hash; radius 2 (ECFP4); 1024 bits default" as *u8)
231 println(" initial-invariants from (z, deg, charge, h_count, arom, in_ring, vsum)" as *u8)
232 println(" polynomial hash multiplier 31; seed 5381 (djb2-style)" as *u8)
233 println(" EXCEED axis E5 HIT : hash-pinned by source-byte identity; cross-version drift impossible" as *u8)
234 println(" (addresses RDKit Issue #2018 - ECFP doesn't match original paper)" as *u8)
235 println(" Tanimoto : Q4 fixed-point (0..10000); identity / symmetry verified" as *u8)
236 println(" honest gaps : stereo-aware ECFP (C2.6.1); FCFP functional equivalence (C2.7);" as *u8)
237 println(" variable bit length (1024 only; 512/2048 are param flips)" as *u8)
238 println(" next : C2.7 -- substructure search (SMARTS over MolGraph + fingerprint screen)" as *u8)
239 return 0
240}