1use ferritin_core::AtomCollection;
3use ferritin_molviewspec::molviewspec::nodes::{
4 ComponentExpression, ComponentSelector, ComponentSelectorT,
5};
6
7pub struct AtomMask(pub Vec<bool>);
9
10impl AtomMask {
11 pub fn count_true(&self) -> usize {
12 self.0.iter().filter(|&&b| b).count()
13 }
14
15 pub fn all_false(&self) -> bool {
16 self.0.iter().all(|&b| !b)
17 }
18}
19
20pub fn evaluate_selector(selector: &ComponentSelector, model: &AtomCollection) -> AtomMask {
22 match selector {
23 ComponentSelector::Selector(s) => evaluate_named(s, model),
24 ComponentSelector::Expression(expr) => evaluate_expression(expr, model),
25 ComponentSelector::ExpressionList(exprs) => {
26 let n = model.get_size();
27 let mut mask = vec![false; n];
28 for expr in exprs {
29 let sub = evaluate_expression(expr, model);
30 for (dst, src) in mask.iter_mut().zip(sub.0.iter()) {
31 *dst |= src;
32 }
33 }
34 AtomMask(mask)
35 }
36 }
37}
38
39fn evaluate_named(selector: &ComponentSelectorT, model: &AtomCollection) -> AtomMask {
40 let n = model.get_size();
41 match selector {
42 ComponentSelectorT::All => AtomMask(vec![true; n]),
43 ComponentSelectorT::Polymer => {
44 let mut mask = vec![false; n];
45 for residue in model.iter_residues() {
46 let is_polymer = residue.is_amino_acid() || residue.is_nucleotide();
47 for idx in residue.atom_range() {
48 mask[idx] = is_polymer;
49 }
50 }
51 AtomMask(mask)
52 }
53 ComponentSelectorT::Protein => {
54 let mut mask = vec![false; n];
55 for residue in model.iter_residues() {
56 if residue.is_amino_acid() {
57 for idx in residue.atom_range() {
58 mask[idx] = true;
59 }
60 }
61 }
62 AtomMask(mask)
63 }
64 ComponentSelectorT::Nucleic => {
65 let mut mask = vec![false; n];
66 for residue in model.iter_residues() {
67 if residue.is_nucleotide() {
68 for idx in residue.atom_range() {
69 mask[idx] = true;
70 }
71 }
72 }
73 AtomMask(mask)
74 }
75 ComponentSelectorT::Branched => {
76 let mut mask = vec![false; n];
77 for residue in model.iter_residues() {
78 if residue.is_carbohydrate() {
79 for idx in residue.atom_range() {
80 mask[idx] = true;
81 }
82 }
83 }
84 AtomMask(mask)
85 }
86 ComponentSelectorT::Ligand => {
87 let mut mask = vec![false; n];
88 for residue in model.iter_residues() {
89 if residue.is_hetero() && !residue.is_water() && !residue.is_ion() {
90 for idx in residue.atom_range() {
91 mask[idx] = true;
92 }
93 }
94 }
95 AtomMask(mask)
96 }
97 ComponentSelectorT::Ion => AtomMask(model.select_ion()),
98 ComponentSelectorT::Water => AtomMask(model.select_water()),
99 }
100}
101
102fn evaluate_expression(expr: &ComponentExpression, model: &AtomCollection) -> AtomMask {
103 if is_all_none(expr) {
105 return AtomMask(vec![true; model.get_size()]);
106 }
107
108 let n = model.get_size();
109 let mut mask = vec![false; n];
110
111 for (res_iter_idx, residue) in model.iter_residues().enumerate() {
112 if let Some(ref chain) = expr.label_asym_id
114 && residue.chain_id() != chain
115 {
116 continue;
117 }
118 if let Some(ref chain) = expr.auth_asym_id
119 && residue.chain_id() != chain
120 {
121 continue;
122 }
123
124 if let Some(ri) = expr.residue_index
126 && res_iter_idx as i32 != ri
127 {
128 continue;
129 }
130
131 let res_id = residue.residue_id();
132
133 if let Some(seq) = expr.label_seq_id
135 && res_id != seq
136 {
137 continue;
138 }
139 if let Some(seq) = expr.auth_seq_id
140 && res_id != seq
141 {
142 continue;
143 }
144
145 if let Some(beg) = expr.beg_label_seq_id
147 && res_id < beg
148 {
149 continue;
150 }
151 if let Some(end) = expr.end_label_seq_id
152 && res_id > end
153 {
154 continue;
155 }
156 if let Some(beg) = expr.beg_auth_seq_id
157 && res_id < beg
158 {
159 continue;
160 }
161 if let Some(end) = expr.end_auth_seq_id
162 && res_id > end
163 {
164 continue;
165 }
166
167 for idx in residue.atom_range() {
172 if let Some(ai) = expr.atom_index
174 && idx as i32 != ai
175 {
176 continue;
177 }
178
179 let atom_name = model.get_atom_name(idx);
181 if let Some(ref la) = expr.label_atom_id
182 && atom_name != la
183 {
184 continue;
185 }
186 if let Some(ref aa) = expr.auth_atom_id
187 && atom_name != aa
188 {
189 continue;
190 }
191
192 if let Some(ref sym) = expr.type_symbol
194 && model.get_element(idx).symbol() != sym.as_str()
195 {
196 continue;
197 }
198
199 mask[idx] = true;
200 }
201 }
202
203 AtomMask(mask)
204}
205
206fn is_all_none(expr: &ComponentExpression) -> bool {
207 expr.label_entity_id.is_none()
208 && expr.label_asym_id.is_none()
209 && expr.auth_asym_id.is_none()
210 && expr.label_seq_id.is_none()
211 && expr.auth_seq_id.is_none()
212 && expr.pdbx_pdb_ins_code.is_none()
213 && expr.beg_label_seq_id.is_none()
214 && expr.end_label_seq_id.is_none()
215 && expr.beg_auth_seq_id.is_none()
216 && expr.end_auth_seq_id.is_none()
217 && expr.residue_index.is_none()
218 && expr.label_atom_id.is_none()
219 && expr.auth_atom_id.is_none()
220 && expr.type_symbol.is_none()
221 && expr.atom_id.is_none()
222 && expr.atom_index.is_none()
223}
224
225#[cfg(test)]
226mod tests {
227 use super::*;
228 use ferritin_core::AtomCollection;
229 use ferritin_core::info::elements::Element;
230 use ferritin_molviewspec::molviewspec::nodes::ComponentExpression;
231
232 fn make_test_collection() -> AtomCollection {
235 let coords = vec![[0.0; 3]; 9];
236 let res_ids: Vec<i32> = vec![1, 1, 1, 2, 2, 3, 4, 5, 5];
237 let res_names: Vec<String> =
238 vec!["ALA", "ALA", "ALA", "GLY", "GLY", "HOH", "ZN", "LIG", "LIG"]
239 .into_iter()
240 .map(|s| s.to_string())
241 .collect();
242 let is_hetero = vec![false, false, false, false, false, true, true, true, true];
243 let elements = vec![
244 Element::N,
245 Element::C,
246 Element::C,
247 Element::N,
248 Element::C,
249 Element::O,
250 Element::Zn,
251 Element::C,
252 Element::O,
253 ];
254 let atom_names: Vec<String> = vec!["N", "CA", "C", "N", "CA", "O", "ZN", "C1", "O1"]
255 .into_iter()
256 .map(|s| s.to_string())
257 .collect();
258 let chain_ids: Vec<String> = vec!["A", "A", "A", "A", "A", "B", "B", "B", "B"]
259 .into_iter()
260 .map(|s| s.to_string())
261 .collect();
262 AtomCollection::new(
263 9, coords, res_ids, res_names, is_hetero, elements, atom_names, chain_ids, None,
264 )
265 }
266
267 #[test]
269 fn test_selector_all() {
270 let ac = make_test_collection();
271 let sel = ComponentSelector::Selector(ComponentSelectorT::All);
272 let mask = evaluate_selector(&sel, &ac);
273 assert_eq!(mask.count_true(), 9);
274 }
275
276 #[test]
278 fn test_selector_protein() {
279 let ac = make_test_collection();
280 let sel = ComponentSelector::Selector(ComponentSelectorT::Protein);
281 let mask = evaluate_selector(&sel, &ac);
282 assert_eq!(
283 mask.count_true(),
284 5,
285 "protein: expected 5 atoms (ALA×3 + GLY×2)"
286 );
287 }
288
289 #[test]
291 fn test_selector_water() {
292 let ac = make_test_collection();
293 let sel = ComponentSelector::Selector(ComponentSelectorT::Water);
294 let mask = evaluate_selector(&sel, &ac);
295 assert_eq!(mask.count_true(), 1, "water: expected 1 HOH atom");
296 assert!(mask.0[5], "atom 5 (HOH) must be true");
297 }
298
299 #[test]
301 fn test_selector_ion() {
302 let ac = make_test_collection();
303 let sel = ComponentSelector::Selector(ComponentSelectorT::Ion);
304 let mask = evaluate_selector(&sel, &ac);
305 assert_eq!(mask.count_true(), 1, "ion: expected 1 ZN atom");
306 assert!(mask.0[6], "atom 6 (ZN) must be true");
307 }
308
309 #[test]
311 fn test_selector_ligand() {
312 let ac = make_test_collection();
313 let sel = ComponentSelector::Selector(ComponentSelectorT::Ligand);
314 let mask = evaluate_selector(&sel, &ac);
315 assert_eq!(mask.count_true(), 2, "ligand: expected 2 LIG atoms");
316 assert!(mask.0[7] && mask.0[8], "atoms 7,8 (LIG) must be true");
317 }
318
319 #[test]
321 fn test_selector_polymer() {
322 let ac = make_test_collection();
323 let sel = ComponentSelector::Selector(ComponentSelectorT::Polymer);
324 let mask = evaluate_selector(&sel, &ac);
325 assert_eq!(mask.count_true(), 5);
326 }
327
328 #[test]
330 fn test_expression_chain_a() {
331 let ac = make_test_collection();
332 let expr = ComponentExpression {
333 auth_asym_id: Some("A".to_string()),
334 ..Default::default()
335 };
336 let sel = ComponentSelector::Expression(expr);
337 let mask = evaluate_selector(&sel, &ac);
338 assert_eq!(mask.count_true(), 5);
339 assert!(mask.0[..5].iter().all(|&b| b));
340 assert!(mask.0[5..].iter().all(|&b| !b));
341 }
342
343 #[test]
345 fn test_expression_chain_b() {
346 let ac = make_test_collection();
347 let expr = ComponentExpression {
348 auth_asym_id: Some("B".to_string()),
349 ..Default::default()
350 };
351 let sel = ComponentSelector::Expression(expr);
352 let mask = evaluate_selector(&sel, &ac);
353 assert_eq!(mask.count_true(), 4);
354 assert!(mask.0[5..].iter().all(|&b| b));
355 }
356
357 #[test]
359 fn test_expression_seq_id() {
360 let ac = make_test_collection();
361 let expr = ComponentExpression {
362 auth_seq_id: Some(1),
363 ..Default::default()
364 };
365 let sel = ComponentSelector::Expression(expr);
366 let mask = evaluate_selector(&sel, &ac);
367 assert_eq!(mask.count_true(), 3);
368 }
369
370 #[test]
372 fn test_expression_seq_range() {
373 let ac = make_test_collection();
374 let expr = ComponentExpression {
375 beg_auth_seq_id: Some(1),
376 end_auth_seq_id: Some(2),
377 ..Default::default()
378 };
379 let sel = ComponentSelector::Expression(expr);
380 let mask = evaluate_selector(&sel, &ac);
381 assert_eq!(mask.count_true(), 5);
382 }
383
384 #[test]
386 fn test_expression_atom_name() {
387 let ac = make_test_collection();
388 let expr = ComponentExpression {
389 auth_atom_id: Some("CA".to_string()),
390 ..Default::default()
391 };
392 let sel = ComponentSelector::Expression(expr);
393 let mask = evaluate_selector(&sel, &ac);
394 assert_eq!(mask.count_true(), 2, "CA atoms in ALA and GLY");
395 }
396
397 #[test]
399 fn test_expression_type_symbol() {
400 let ac = make_test_collection();
401 let expr = ComponentExpression {
402 type_symbol: Some("N".to_string()),
403 ..Default::default()
404 };
405 let sel = ComponentSelector::Expression(expr);
406 let mask = evaluate_selector(&sel, &ac);
407 assert_eq!(mask.count_true(), 2, "N atoms in ALA and GLY");
408 }
409
410 #[test]
412 fn test_expression_atom_index() {
413 let ac = make_test_collection();
414 let expr = ComponentExpression {
415 atom_index: Some(0),
416 ..Default::default()
417 };
418 let sel = ComponentSelector::Expression(expr);
419 let mask = evaluate_selector(&sel, &ac);
420 assert_eq!(mask.count_true(), 1);
421 assert!(mask.0[0]);
422 }
423
424 #[test]
426 fn test_expression_list_union() {
427 let ac = make_test_collection();
428 let expr1 = ComponentExpression {
429 auth_asym_id: Some("A".to_string()),
430 auth_seq_id: Some(1),
431 ..Default::default()
432 };
433 let expr2 = ComponentExpression {
434 auth_asym_id: Some("B".to_string()),
435 auth_seq_id: Some(4),
436 ..Default::default()
437 };
438 let sel = ComponentSelector::ExpressionList(vec![expr1, expr2]);
439 let mask = evaluate_selector(&sel, &ac);
440 assert_eq!(mask.count_true(), 4, "union: ALA(3) + ZN(1)");
441 }
442
443 #[test]
445 fn test_expression_all_none_is_all_true() {
446 let ac = make_test_collection();
447 let expr = ComponentExpression::default();
448 let sel = ComponentSelector::Expression(expr);
449 let mask = evaluate_selector(&sel, &ac);
450 assert_eq!(
451 mask.count_true(),
452 9,
453 "all-None expression must match all atoms"
454 );
455 }
456
457 #[test]
459 fn test_atom_mask_helpers() {
460 let full = AtomMask(vec![true, true, false]);
461 assert_eq!(full.count_true(), 2);
462 assert!(!full.all_false());
463
464 let empty = AtomMask(vec![false, false, false]);
465 assert_eq!(empty.count_true(), 0);
466 assert!(empty.all_false());
467 }
468
469 #[test]
471 fn test_label_asym_id_matched_against_auth() {
472 let ac = make_test_collection();
473 let expr = ComponentExpression {
474 label_asym_id: Some("A".to_string()),
475 ..Default::default()
476 };
477 let sel = ComponentSelector::Expression(expr);
478 let mask = evaluate_selector(&sel, &ac);
479 assert_eq!(
480 mask.count_true(),
481 5,
482 "label_asym_id A must match chain A (auth)"
483 );
484 }
485
486 #[test]
488 fn test_label_seq_id_matched_against_auth() {
489 let ac = make_test_collection();
490 let expr = ComponentExpression {
491 label_seq_id: Some(2),
492 ..Default::default()
493 };
494 let sel = ComponentSelector::Expression(expr);
495 let mask = evaluate_selector(&sel, &ac);
496 assert_eq!(
497 mask.count_true(),
498 2,
499 "label_seq_id 2 must match res_id 2 (GLY×2)"
500 );
501 }
502
503 #[test]
505 fn test_expression_residue_index() {
506 let ac = make_test_collection();
507 let expr = ComponentExpression {
509 residue_index: Some(0),
510 ..Default::default()
511 };
512 let sel = ComponentSelector::Expression(expr);
513 let mask = evaluate_selector(&sel, &ac);
514 assert_eq!(
515 mask.count_true(),
516 3,
517 "residue_index 0 must select ALA (3 atoms)"
518 );
519 assert!(
520 mask.0[0] && mask.0[1] && mask.0[2],
521 "atoms 0-2 should be selected"
522 );
523
524 let expr2 = ComponentExpression {
526 residue_index: Some(1),
527 ..Default::default()
528 };
529 let mask2 = evaluate_selector(&ComponentSelector::Expression(expr2), &ac);
530 assert_eq!(
531 mask2.count_true(),
532 2,
533 "residue_index 1 must select GLY (2 atoms)"
534 );
535
536 let expr3 = ComponentExpression {
538 residue_index: Some(3),
539 ..Default::default()
540 };
541 let mask3 = evaluate_selector(&ComponentSelector::Expression(expr3), &ac);
542 assert_eq!(
543 mask3.count_true(),
544 1,
545 "residue_index 3 must select ZN (1 atom)"
546 );
547 }
548
549 #[test]
551 fn test_expression_residue_index_with_atom_name() {
552 let ac = make_test_collection();
553 let expr = ComponentExpression {
555 residue_index: Some(1),
556 auth_atom_id: Some("CA".to_string()),
557 ..Default::default()
558 };
559 let mask = evaluate_selector(&ComponentSelector::Expression(expr), &ac);
560 assert_eq!(
561 mask.count_true(),
562 1,
563 "residue 1 CA should be exactly 1 atom"
564 );
565 assert!(mask.0[4], "atom index 4 is GLY CA");
566 }
567}