chematic-fp 0.4.29

ECFP4/6, MACCS 166-bit and topological path fingerprints with Tanimoto/Dice similarity for chematic
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
//! MACCS 166-bit structural keys fingerprint.
//!
//! Bit i (0-indexed) corresponds to MACCS key (i+1).
//! SMARTS patterns match the official RDKit MACCSkeys.py definitions.
//!
//! Three keys use code-only logic (no SMARTS pattern):
//! - Key 1  (index 0):   any isotopically-labelled atom
//! - Key 125 (index 124): aromatic ring count > 1
//! - Key 166 (index 165): fragment count > 1 (disconnected molecule)
//!
//! For count-based keys the second tuple element is the minimum count:
//! the bit is set when `matches.len() > min_count`.

use chematic_core::Molecule;
use chematic_perception::find_sssr;
use chematic_smarts::{find_matches, parse_smarts};

use crate::bitvec::BitVec2048;

/// `(smarts, min_count)` pairs for all 166 MACCS keys (index = key - 1).
/// Empty string or "?" → special-case key handled in `maccs()`.
static MACCS_SMARTS: &[(&str, usize)] = &[
    ("?", 0),                                                   // key 1  - ISOTOPE (special)
    ("[#104]", 0),                                              // key 2  - Atomic num >103
    ("[#32,#33,#34,#50,#51,#52,#82,#83,#84]", 0),               // key 3  - Group IVa,Va,VIa
    ("[Ac,Th,Pa,U,Np,Pu,Am,Cm,Bk,Cf,Es,Fm,Md,No,Lr]", 0),       // key 4  - Actinide
    ("[Sc,Ti,Y,Zr,Hf]", 0),                                     // key 5  - Group IIIB,IVB
    ("[La,Ce,Pr,Nd,Pm,Sm,Eu,Gd,Tb,Dy,Ho,Er,Tm,Yb,Lu]", 0),      // key 6  - Lanthanide
    ("[V,Cr,Mn,Nb,Mo,Tc,Ta,W,Re]", 0),                          // key 7  - Group VB,VIB,VIIB
    ("[!#6;!#1]1~*~*~*~1", 0),                                  // key 8  - 4M heterocycle
    ("[Fe,Co,Ni,Ru,Rh,Pd,Os,Ir,Pt]", 0),                        // key 9  - Group VIII
    ("[Be,Mg,Ca,Sr,Ba,Ra]", 0),                                 // key 10 - Group IIa
    ("*1~*~*~*~1", 0),                                          // key 11 - 4M ring
    ("[Cu,Zn,Ag,Cd,Au,Hg]", 0),                                 // key 12 - Group IB,IIB
    ("[#8]~[#7](~[#6])~[#6]", 0),                               // key 13 - ON(C)C
    ("[#16]-[#16]", 0),                                         // key 14 - S-S
    ("[#8]~[#6](~[#8])~[#8]", 0),                               // key 15 - OC(O)O
    ("[!#6;!#1]1~*~*~1", 0),                                    // key 16 - 3M heterocycle
    ("[#6]#[#6]", 0),                                           // key 17 - alkyne
    ("[#5,#13,#31,#49,#81]", 0),                                // key 18 - Group IIIA
    ("*1~*~*~*~*~*~*~1", 0),                                    // key 19 - 7M ring
    ("[#14]", 0),                                               // key 20 - Si
    ("[#6]=[#6](~[!#6;!#1])~[!#6;!#1]", 0),                     // key 21 - C=C(Q)Q
    ("*1~*~*~1", 0),                                            // key 22 - 3M ring
    ("[#7]~[#6](~[#8])~[#8]", 0),                               // key 23 - NC(O)O
    ("[#7]-[#8]", 0),                                           // key 24 - N-O
    ("[#7]~[#6](~[#7])~[#7]", 0),                               // key 25 - NC(N)N
    ("[#6;R]=[#6;R]", 0), // key 26 - ring C=C (was =;@ which fails to parse)
    ("[I]", 0),           // key 27 - I
    ("[!#6;!#1]~[CH2]~[!#6;!#1]", 0), // key 28 - QCH2Q
    ("[#15]", 0),         // key 29 - P
    ("[#6]~[!#6;!#1](~[#6])(~[#6])~*", 0), // key 30 - CQ(C)(C)A
    ("[!#6;!#1]~[F,Cl,Br,I]", 0), // key 31 - QX
    ("[#6]~[#16]~[#7]", 0), // key 32 - CSN
    ("[#7]~[#16]", 0),    // key 33 - NS
    ("[CH2]=*", 0),       // key 34 - CH2=A
    ("[Li,Na,K,Rb,Cs,Fr]", 0), // key 35 - Alkali metal
    ("[#16R]", 0),        // key 36 - S heterocycle
    ("[#7]~[#6](~[#8])~[#7]", 0), // key 37 - NC(O)N
    ("[#7]~[#6](~[#6])~[#7]", 0), // key 38 - NC(C)N
    ("[#8]~[#16](~[#8])~[#8]", 0), // key 39 - OS(O)O
    ("[#16]-[#8]", 0),    // key 40 - S-O
    ("[#6]#[#7]", 0),     // key 41 - nitrile
    ("F", 0),             // key 42 - F
    ("[!#6;!#1;!H0]~*~[!#6;!#1;!H0]", 0), // key 43 - QHAQH
    ("[!#1;!#6;!#7;!#8;!#9;!#14;!#15;!#16;!#17;!#35;!#53]", 0), // key 44 - OTHER
    ("[#6]=[#6]~[#7]", 0), // key 45 - C=CN
    ("Br", 0),            // key 46 - Br
    ("[#16]~*~[#7]", 0),  // key 47 - SAN
    ("[#8]~[!#6;!#1](~[#8])(~[#8])", 0), // key 48 - OQ(O)O
    ("[!+0]", 0),         // key 49 - charged atom
    ("[#6]=[#6](~[#6])~[#6]", 0), // key 50 - C=C(C)C
    ("[#6]~[#16]~[#8]", 0), // key 51 - CSO
    ("[#7]~[#7]", 0),     // key 52 - N-N
    ("[!#6;!#1;!H0]~*~*~*~[!#6;!#1;!H0]", 0), // key 53 - QHAAAQH
    ("[!#6;!#1;!H0]~*~*~[!#6;!#1;!H0]", 0), // key 54 - QHAAQH
    ("[#8]~[#16]~[#8]", 0), // key 55 - OSO
    ("[#8]~[#7](~[#8])~[#6]", 0), // key 56 - ON(O)C
    ("[#8R]", 0),         // key 57 - O heterocycle
    ("[!#6;!#1]~[#16]~[!#6;!#1]", 0), // key 58 - QSQ
    ("[#16]!:*:*", 0),    // key 59 - S-nonarom-arom chain
    ("[#16]=[#8]", 0),    // key 60 - S=O
    ("*~[#16](~*)~*", 0), // key 61 - AS(A)A
    ("*@*!@*@*", 0),      // key 62 - ring-open-ring
    ("[#7]=[#8]", 0),     // key 63 - N=O
    ("*@*!@[#16]", 0),    // key 64 - ring-open-S
    ("c:n", 0),           // key 65 - aromatic C-N
    ("[#6]~[#6](~[#6])(~[#6])~*", 0), // key 66 - quaternary C
    ("[!#6;!#1]~[#16]", 0), // key 67 - QS
    ("[!#6;!#1;!H0]~[!#6;!#1;!H0]", 0), // key 68 - QHQH
    ("[!#6;!#1]~[!#6;!#1;!H0]", 0), // key 69 - QQH
    ("[!#6;!#1]~[#7]~[!#6;!#1]", 0), // key 70 - QNQ
    ("[#7]~[#8]", 0),     // key 71 - N-O (any bond)
    ("[#8]~*~*~[#8]", 0), // key 72 - O..O (2 bonds)
    ("[#16]=*", 0),       // key 73 - S=A
    ("[CH3]~*~[CH3]", 0), // key 74 - CH3-A-CH3
    ("*!@[#7]@*", 0),     // key 75 - open-N-ring
    ("[#6]=[#6](~*)~*", 0), // key 76 - C=C(A)A
    ("[#7]~*~[#7]", 0),   // key 77 - N-A-N
    ("[#6]=[#7]", 0),     // key 78 - C=N
    ("[#7]~*~*~[#7]", 0), // key 79 - N-A-A-N
    ("[#7]~*~*~*~[#7]", 0), // key 80 - N-A-A-A-N
    ("[#16]~*(~*)~*", 0), // key 81 - S(A)A
    ("*~[CH2]~[!#6;!#1;!H0]", 0), // key 82 - A-CH2-QH
    ("[!#6;!#1]1~*~*~*~*~1", 0), // key 83 - 5M heterocycle
    ("[NH2]", 0),         // key 84 - NH2
    ("[#6]~[#7](~[#6])~[#6]", 0), // key 85 - tertiary N
    ("[C;H2,H3][!#6;!#1][C;H2,H3]", 0), // key 86 - CH2/CH3 Q CH2/CH3
    ("[F,Cl,Br,I]!@*@*", 0), // key 87 - X-open-ring
    ("[#16]", 0),         // key 88 - any S
    ("[#8]~*~*~*~[#8]", 0), // key 89 - O..O (3 bonds)
    (
        "[$([!#6;!#1;!H0]~*~*~[CH2]~*),$([!#6;!#1;!H0;R]1@[R]@[R]@[CH2;R]1),$([!#6;!#1;!H0]~[R]1@[R]@[CH2;R]1)]",
        0,
    ), // key 90
    (
        "[$([!#6;!#1;!H0]~*~*~*~[CH2]~*),$([!#6;!#1;!H0;R]1@[R]@[R]@[R]@[CH2;R]1),$([!#6;!#1;!H0]~[R]1@[R]@[R]@[CH2;R]1),$([!#6;!#1;!H0]~*~[R]1@[R]@[CH2;R]1)]",
        0,
    ), // key 91
    ("[#8]~[#6](~[#7])~[#6]", 0), // key 92 - OC(N)C
    ("[!#6;!#1]~[CH3]", 0), // key 93 - Q-CH3
    ("[!#6;!#1]~[#7]", 0), // key 94 - Q-N
    ("[#7]~*~*~[#8]", 0), // key 95 - N-A-A-O
    ("*1~*~*~*~*~1", 0),  // key 96 - 5M ring
    ("[#7]~*~*~*~[#8]", 0), // key 97 - N-A-A-A-O
    ("[!#6;!#1]1~*~*~*~*~*~1", 0), // key 98 - 6M heterocycle
    ("[#6]=[#6]", 0),     // key 99 - C=C
    ("*~[CH2]~[#7]", 0),  // key 100 - A-CH2-N
    (
        "[$([R]@1@[R]@[R]@[R]@[R]@[R]@[R]@[R]1),$([R]@1@[R]@[R]@[R]@[R]@[R]@[R]@[R]@[R]1),$([R]@1@[R]@[R]@[R]@[R]@[R]@[R]@[R]@[R]@[R]1),$([R]@1@[R]@[R]@[R]@[R]@[R]@[R]@[R]@[R]@[R]@[R]1),$([R]@1@[R]@[R]@[R]@[R]@[R]@[R]@[R]@[R]@[R]@[R]@[R]1),$([R]@1@[R]@[R]@[R]@[R]@[R]@[R]@[R]@[R]@[R]@[R]@[R]@[R]1)]",
        0,
    ), // key 101 - 8-14M ring
    ("[!#6;!#1]~[#8]", 0), // key 102 - Q-O
    ("Cl", 0),            // key 103 - Cl
    ("[!#6;!#1;!H0]~*~[CH2]~*", 0), // key 104 - QH-A-CH2-A
    ("*@*(@*)@*", 0),     // key 105 - ring branch
    ("[!#6;!#1]~*(~[!#6;!#1])~[!#6;!#1]", 0), // key 106 - QA(Q)Q
    ("[F,Cl,Br,I]~*(~*)~*", 0), // key 107 - X-A(A)A
    ("[CH3]~*~*~*~[CH2]~*", 0), // key 108 - CH3-A-A-A-CH2-A
    ("*~[CH2]~[#8]", 0),  // key 109 - A-CH2-O
    ("[#7]~[#6]~[#8]", 0), // key 110 - NCO
    ("[#7]~*~[CH2]~*", 0), // key 111 - N-A-CH2-A
    ("*~*(~*)(~*)~*", 0), // key 112 - branched atom
    ("[#8]!:*:*", 0),     // key 113 - O-nonarom-arom
    ("[CH3]~[CH2]~*", 0), // key 114 - CH3-CH2-A
    ("[CH3]~*~[CH2]~*", 0), // key 115 - CH3-A-CH2-A
    ("[$([CH3]~*~*~[CH2]~*),$([CH3]~*1~*~[CH2]1)]", 0), // key 116 - CH3-A-A-CH2
    ("[#7]~*~[#8]", 0),   // key 117 - N-A-O
    ("[$(*~[CH2]~[CH2]~*),$(*1~[CH2]~[CH2]1)]", 1), // key 118 - ACH2CH2A > 1
    ("[#7]=*", 0),        // key 119 - N=A
    ("[!#6;R]", 1),       // key 120 - het in ring > 1
    ("[#7;R]", 0),        // key 121 - N heterocycle
    ("*~[#7](~*)~*", 0),  // key 122 - AN(A)A
    ("[#8]~[#6]~[#8]", 0), // key 123 - OCO
    ("[!#6;!#1]~[!#6;!#1]", 0), // key 124 - QQ
    ("?", 0),             // key 125 - aromatic ring > 1 (special)
    ("*!@[#8]!@*", 0),    // key 126 - A-open-O-open-A
    ("*@*!@[#8]", 1),     // key 127 - ring-open-O > 1
    (
        "[$(*~[CH2]~*~*~*~[CH2]~*),$([R]1@[CH2;R]@[R]@[R]@[R]@[CH2;R]1),$(*~[CH2]~[R]1@[R]@[R]@[CH2;R]1),$(*~[CH2]~*~[R]1@[R]@[CH2;R]1)]",
        0,
    ), // key 128
    (
        "[$(*~[CH2]~*~*~[CH2]~*),$([R]1@[CH2;R]@[R]@[R]@[CH2;R]1),$(*~[CH2]~[R]1@[R]@[CH2;R]1),$(*~[CH2]~*1~*~[CH2]1)]",
        0,
    ), // key 129
    ("[!#6;!#1]~[!#6;!#1]", 1), // key 130 - QQ > 1
    ("[!#6;!#1;!H0]", 1), // key 131 - QH > 1
    ("[#8]~*~[CH2]~*", 0), // key 132 - O-A-CH2-A
    ("*@*!@[#7]", 0),     // key 133 - ring-open-N
    ("[F,Cl,Br,I]", 0),   // key 134 - halogen
    ("[#7]!:*:*", 0),     // key 135 - N-nonarom-arom
    ("[#8]=*", 1),        // key 136 - O=A > 1
    ("[!C;!c;R]", 0),     // key 137 - non-C in ring
    ("[!#6;!#1]~[CH2]~*", 1), // key 138 - QCH2A > 1
    ("[O;!H0]", 0),       // key 139 - OH
    ("[#8]", 3),          // key 140 - O > 3
    ("[CH3]", 2),         // key 141 - CH3 > 2
    ("[#7]", 1),          // key 142 - N > 1
    ("*@*!@[#8]", 0),     // key 143 - ring-open-O
    ("*!:*:*!:*", 0),     // key 144 - nonarom-arom-nonarom
    ("*1~*~*~*~*~*~1", 1), // key 145 - 6M ring > 1
    ("[#8]", 2),          // key 146 - O > 2
    ("[$(*~[CH2]~[CH2]~*),$([R]1@[CH2;R]@[CH2;R]1)]", 0), // key 147 - ACH2CH2A
    ("*~[!#6;!#1](~*)~*", 0), // key 148 - AQ(A)A
    ("[C;H3,H4]", 1),     // key 149 - CH3 > 1
    ("*!@*@*!@*", 0),     // key 150 - open-ring-open
    ("[#7;!H0]", 0),      // key 151 - NH
    ("[#8]~[#6](~[#6])~[#6]", 0), // key 152 - OC(C)C
    ("[!#6;!#1]~[CH2]~*", 0), // key 153 - QCH2A
    ("[#6]=[#8]", 0),     // key 154 - C=O
    ("*!@[CH2]!@*", 0),   // key 155 - open-CH2-open
    ("[#7]~*(~*)~*", 0),  // key 156 - NA(A)A
    ("[#6]-[#8]", 0),     // key 157 - C-O
    ("[#6]-[#7]", 0),     // key 158 - C-N
    ("[#8]", 1),          // key 159 - O > 1
    ("[C;H3,H4]", 0),     // key 160 - CH3
    ("[#7]", 0),          // key 161 - N
    ("[a]", 0),           // key 162 - aromatic atom
    ("*1~*~*~*~*~*~1", 0), // key 163 - 6M ring
    ("[#8]", 0),          // key 164 - O
    ("[R]", 0),           // key 165 - ring atom
    ("?", 0),             // key 166 - fragments > 1 (special)
];

fn count_aromatic_rings(mol: &Molecule) -> usize {
    find_sssr(mol)
        .rings()
        .iter()
        .filter(|ring| ring.iter().all(|&idx| mol.atom(idx).aromatic))
        .count()
}

/// Compute the MACCS 166-bit structural keys fingerprint for `mol`.
///
/// Bit `i` (0-indexed) corresponds to MACCS key `i+1`. Keys 1, 125, and 166
/// use code-only logic; all other keys use SMARTS matching.
/// For count-based keys (14 total), the bit is set only when the match count
/// exceeds the threshold stored in `MACCS_SMARTS`.
pub fn maccs(mol: &Molecule) -> BitVec2048 {
    let mut fp = BitVec2048::new();

    for (i, &(pattern, min_count)) in MACCS_SMARTS.iter().enumerate() {
        if pattern == "?" || pattern.is_empty() {
            continue;
        }
        if let Ok(query) = parse_smarts(pattern)
            && find_matches(&query, mol).len() > min_count
        {
            fp.set(i);
        }
        // Patterns that fail to parse silently leave the bit unset.
    }

    // Key 1 (index 0): isotope — any atom carrying an explicit mass label
    if mol.atoms().any(|(_, a)| a.isotope.is_some()) {
        fp.set(0);
    }

    // Key 125 (index 124): aromatic ring count > 1
    if count_aromatic_rings(mol) > 1 {
        fp.set(124);
    }

    // Key 166 (index 165): fragment count > 1 (disconnected molecule)
    if !mol.is_connected() {
        fp.set(165);
    }

    fp
}

#[cfg(test)]
mod tests {
    use super::*;
    use chematic_smiles::parse;

    #[test]
    fn maccs_benzene_has_aromatic_bits() {
        let mol = parse("c1ccccc1").unwrap();
        let fp = maccs(&mol);
        assert!(
            fp.get(161),
            "key 162 (any aromatic atom) should be set for benzene"
        );
        assert!(fp.get(162), "key 163 (6M ring) should be set for benzene");
        assert!(fp.get(164), "key 165 (ring atom) should be set for benzene");
    }

    #[test]
    fn maccs_benzene_aromatic_ring_gt1_not_set() {
        // Benzene has exactly 1 aromatic ring → key 125 should NOT be set
        let mol = parse("c1ccccc1").unwrap();
        let fp = maccs(&mol);
        assert!(
            !fp.get(124),
            "benzene has only 1 aromatic ring; key 125 must be 0"
        );
    }

    #[test]
    fn maccs_naphthalene_aromatic_ring_gt1_set() {
        // Naphthalene has 2 aromatic rings → key 125 should be set
        let mol = parse("c1ccc2ccccc2c1").unwrap();
        let fp = maccs(&mol);
        assert!(
            fp.get(124),
            "naphthalene has 2 aromatic rings; key 125 must be 1"
        );
    }

    #[test]
    fn maccs_ethanol_has_oxygen_bits() {
        let mol = parse("CCO").unwrap();
        let fp = maccs(&mol);
        assert!(fp.get(163), "key 164 (O) should be set for ethanol");
        assert!(fp.get(156), "key 157 (C-O) should be set for ethanol");
        assert!(fp.get(138), "key 139 (OH) should be set for ethanol");
    }

    #[test]
    fn maccs_isotope_key() {
        // [13C] has an isotope label → key 1 should be set
        let labeled = parse("[13CH4]").unwrap();
        let unlabeled = parse("C").unwrap();
        assert!(maccs(&labeled).get(0), "key 1 should be set for [13C]");
        assert!(!maccs(&unlabeled).get(0), "key 1 should not be set for CH4");
    }

    #[test]
    fn maccs_fragment_key() {
        // NaCl ([Na+].[Cl-]) is disconnected → key 166 should be set
        let salt = parse("[Na+].[Cl-]").unwrap();
        let ethanol = parse("CCO").unwrap();
        assert!(
            maccs(&salt).get(165),
            "key 166 should be set for [Na+].[Cl-]"
        );
        assert!(
            !maccs(&ethanol).get(165),
            "key 166 should not be set for CCO"
        );
    }

    #[test]
    fn maccs_oxygen_count_keys() {
        // Citric acid has 6 O → keys 159/146/140 (O>1, O>2, O>3) should be set
        let citric = parse("OC(CC(O)(C(=O)O)C(=O)O)(C(=O)O)").unwrap();
        let fp = maccs(&citric);
        assert!(fp.get(158), "key 159 (O>1) should be set for citric acid");
        assert!(fp.get(145), "key 146 (O>2) should be set for citric acid");
        assert!(fp.get(139), "key 140 (O>3) should be set for citric acid");
    }

    #[test]
    fn maccs_nitrile_key() {
        let mol = parse("CC#N").unwrap();
        let fp = maccs(&mol);
        assert!(
            fp.get(40),
            "key 41 (C#N nitrile) should be set for acetonitrile"
        );
    }

    #[test]
    fn maccs_bromine_key() {
        let mol = parse("c1ccccc1Br").unwrap();
        let fp = maccs(&mol);
        assert!(fp.get(45), "key 46 (Br) should be set for bromobenzene");
    }

    #[test]
    fn maccs_carbonyl_key() {
        let mol = parse("CC(=O)Oc1ccccc1C(=O)O").unwrap();
        let fp = maccs(&mol);
        assert!(fp.get(153), "key 154 (C=O) should be set for aspirin");
    }

    #[test]
    fn maccs_deterministic() {
        let mol = parse("c1ccccc1").unwrap();
        assert_eq!(maccs(&mol), maccs(&mol));
    }

    #[test]
    fn maccs_n_gt1_key() {
        // Ethylenediamine has 2 N → key 142 (N>1) should be set
        let mol = parse("NCCN").unwrap();
        let fp = maccs(&mol);
        assert!(
            fp.get(141),
            "key 142 (N>1) should be set for ethylenediamine"
        );
    }

    #[test]
    fn maccs_key26_ring_double_bond() {
        // Cyclohexene has a ring C=C → key 26 (index 25) should be set
        let mol = parse("C1=CCCCC1").unwrap();
        assert!(
            maccs(&mol).get(25),
            "cyclohexene should set key 26 (ring C=C)"
        );
        // Ethylene has C=C but not in ring → key 26 should NOT be set
        let mol2 = parse("C=C").unwrap();
        assert!(
            !maccs(&mol2).get(25),
            "ethylene: key 26 (ring C=C) should be 0"
        );
    }

    #[test]
    fn all_maccs_smarts_parse() {
        let mut failed = Vec::new();
        for (i, &(pattern, _)) in MACCS_SMARTS.iter().enumerate() {
            if pattern == "?" || pattern.is_empty() {
                continue;
            }
            if parse_smarts(pattern).is_err() {
                failed.push((i + 1, pattern));
            }
        }
        assert!(
            failed.is_empty(),
            "MACCS SMARTS parse failures: {:?}",
            failed
        );
    }
}