haddock_restraints/core/interactor.rs
1use crate::core::sasa;
2use crate::core::structure;
3use crate::load_pdb;
4use pdbtbx::PDB;
5use pdbtbx::PDBError;
6use serde::Deserialize;
7use std::collections::HashSet;
8
9/// Represents an interactor in a molecular system.
10///
11/// This struct contains information about a specific interactor, including
12/// its identification, residues, atoms, target interactions, and various
13/// parameters for interaction calculations.
14#[derive(Deserialize, Debug, Clone)]
15pub struct Interactor {
16 /// Unique identifier for the interactor.
17 id: u16,
18
19 /// Chain identifier for the interactor.
20 chain: String,
21
22 /// Set of active residue numbers.
23 active: HashSet<i16>,
24
25 /// Optional list of active atom names.
26 active_atoms: Option<Vec<String>>,
27
28 /// Set of passive residue numbers.
29 pub passive: HashSet<i16>,
30
31 /// Optional list of passive atom names.
32 passive_atoms: Option<Vec<String>>,
33
34 /// Set of target interactor IDs.
35 target: HashSet<u16>,
36
37 /// Optional target distance for interactions.
38 target_distance: Option<f64>,
39
40 /// Optional lower margin for distance calculations.
41 lower_margin: Option<f64>,
42
43 /// Optional upper margin for distance calculations.
44 upper_margin: Option<f64>,
45
46 /// Optional path to the structure file.
47 structure: Option<String>,
48
49 /// Optional PDB object.
50 pdb: Option<PDB>,
51
52 /// Optional flag to determine if passive residues should be derived from active ones.
53 passive_from_active: Option<bool>,
54
55 /// Optional radius to define the neighbor search radius
56 passive_from_active_radius: Option<f64>,
57
58 /// Optional flag to treat surface residues as passive.
59 surface_as_passive: Option<bool>,
60
61 /// Optional flag to filter buried residues.
62 filter_buried: Option<bool>,
63
64 /// Optional cutoff value for buried residue filtering.
65 filter_buried_cutoff: Option<f64>,
66
67 /// Optional wildcard value for the interactor.
68 wildcard: Option<String>,
69}
70
71#[allow(clippy::too_many_arguments)]
72impl Interactor {
73 /// Creates a new `Interactor` instance with default values.
74 ///
75 /// This method initializes a new `Interactor` with the given ID and default values for all other fields.
76 /// It's marked with `#[allow(clippy::too_many_arguments)]` to suppress warnings about the number of fields,
77 /// even though this constructor doesn't actually take multiple arguments.
78 ///
79 /// # Arguments
80 ///
81 /// * `id` - A `u16` that specifies the unique identifier for the new `Interactor`.
82 ///
83 /// # Returns
84 ///
85 /// A new `Interactor` instance with the specified ID and default values for all other fields.
86 ///
87 pub fn new(id: u16) -> Self {
88 Interactor {
89 id,
90 chain: String::new(),
91 active: HashSet::new(),
92 passive: HashSet::new(),
93 target: HashSet::new(),
94 structure: None,
95 pdb: None,
96 passive_from_active: None,
97 passive_from_active_radius: None,
98 surface_as_passive: None,
99 filter_buried: None,
100 filter_buried_cutoff: None,
101 active_atoms: None,
102 passive_atoms: None,
103 wildcard: None,
104 target_distance: None,
105 lower_margin: None,
106 upper_margin: None,
107 }
108 }
109
110 /// Checks if the Interactor is in a valid state.
111 ///
112 /// This method performs two validity checks:
113 /// 1. Ensures that the target set is not empty.
114 /// 2. Verifies that there's no overlap between active and passive residues.
115 ///
116 /// # Returns
117 ///
118 /// - `Ok(true)` if the Interactor is valid.
119 /// - `Err(&str)` with an error message if any validity check fails.
120 ///
121 pub fn is_valid(&self) -> Result<bool, &str> {
122 if self.target.is_empty() {
123 return Err("Target residues are empty");
124 }
125 if self.active.intersection(&self.passive).next().is_some() {
126 return Err("Active/Passive selections overlap");
127 }
128 Ok(true)
129 }
130
131 /// Sets passive residues based on the active residues and their neighboring residues.
132 ///
133 /// This method performs the following steps:
134 /// 1. Opens the PDB file specified in the `structure` field.
135 /// 2. Retrieves the residues corresponding to the active residues.
136 /// 3. Performs a neighbor search to find residues within 5.0 Å of the active residues.
137 /// 4. Adds these neighboring residues to the passive set.
138 ///
139 /// # Panics
140 ///
141 /// This method will panic if:
142 /// - The `structure` field is `None`.
143 /// - There's an error opening or parsing the PDB file.
144 ///
145 /// # Side Effects
146 ///
147 /// This method modifies the `passive` set of the `Interactor`, adding new residues based on
148 /// the neighbor search results.
149 ///
150 /// # Dependencies
151 ///
152 /// This method relies on external functions from the `structure` module:
153 /// - `structure::get_residues`
154 /// - `structure::neighbor_search`
155 /// - `structure::load_pdb`
156 ///
157 pub fn set_passive_from_active(&mut self) {
158 if let Some(pdb) = &self.pdb {
159 let residues =
160 structure::get_residues(pdb, self.active.iter().map(|x| *x as isize).collect());
161
162 let search_cutoff = self.passive_from_active_radius.unwrap_or(6.5);
163 let neighbors = structure::neighbor_search(pdb.clone(), residues, search_cutoff);
164
165 // Add these neighbors to the passive set
166 neighbors.iter().for_each(|x| {
167 self.passive.insert(*x as i16);
168 });
169 }
170 }
171
172 /// Sets surface residues as passive based on their solvent accessible surface area (SASA).
173 ///
174 /// This method performs the following steps:
175 /// 1. Opens the PDB file specified in the `structure` field.
176 /// 2. Calculates the SASA for all residues in the structure.
177 /// 3. Identifies surface residues (those with relative SASA > 0.7) on the same chain as the interactor.
178 /// 4. Adds these surface residues to the passive set.
179 ///
180 /// # Panics
181 ///
182 /// This method will panic if:
183 /// - The `structure` field is `None`.
184 /// - There's an error opening or parsing the PDB file.
185 ///
186 /// # Side Effects
187 ///
188 /// This method modifies the `passive` set of the `Interactor`, adding new residues based on
189 /// the SASA calculation results.
190 ///
191 /// # Dependencies
192 ///
193 /// This method relies on external functions from the `sasa` and `structure` modules:
194 /// - `sasa::calculate_sasa`
195 /// - `structure::load_pdb`
196 ///
197 /// # Note
198 ///
199 /// The threshold for considering a residue as "surface" is set to 0.7 relative SASA.
200 /// This value may need to be adjusted based on specific requirements.
201 pub fn set_surface_as_passive(&mut self) {
202 if let Some(pdb) = &self.pdb {
203 let sasa = sasa::calculate_sasa(pdb.clone());
204
205 // Add these neighbors to the passive set
206 sasa.iter().for_each(|r| {
207 // If the `rel_sasa_total` is more than 0.7 then add it to the passive set
208 if r.rel_sasa_total > 0.7 && r.chain == self.chain {
209 self.passive.insert(r.residue.serial_number() as i16);
210 }
211 });
212 }
213 }
214
215 /// Removes buried residues from both active and passive sets based on solvent accessible surface area (SASA).
216 ///
217 /// This method performs the following steps:
218 /// 1. Opens the PDB file specified in the `structure` field.
219 /// 2. Calculates the SASA for all residues in the structure.
220 /// 3. Identifies buried residues (those with relative SASA below a certain cutoff) on the same chain as the interactor.
221 /// 4. Removes these buried residues from both the active and passive sets.
222 ///
223 /// # Panics
224 ///
225 /// This method will panic if:
226 /// - The `structure` field is `None`.
227 /// - There's an error opening or parsing the PDB file.
228 ///
229 /// # Side Effects
230 ///
231 /// This method modifies both the `active` and `passive` sets of the `Interactor`,
232 /// removing residues based on the SASA calculation results.
233 ///
234 /// # Dependencies
235 ///
236 /// This method relies on external functions from the `sasa` and `structure` module:
237 /// - `sasa::calculate_sasa`
238 /// - `structure::load_pdb`
239 ///
240 /// # Note
241 ///
242 /// The default threshold for considering a residue as "buried" is set to 0.7 relative SASA.
243 /// This can be customized by setting the `filter_buried_cutoff` field of the `Interactor`.
244 pub fn remove_buried_residues(&mut self) {
245 if let Some(pdb) = &self.pdb {
246 let sasa = sasa::calculate_sasa(pdb.clone());
247
248 let sasa_cutoff = self.filter_buried_cutoff.unwrap_or(0.7);
249
250 sasa.iter().for_each(|r| {
251 // If the `rel_sasa_total` is more than 0.7 then add it to the passive set
252 if r.rel_sasa_total < sasa_cutoff && r.chain == self.chain {
253 // This residue is not accessible, remove it from the passive and active sets
254 self.passive.remove(&(r.residue.serial_number() as i16));
255 self.active.remove(&(r.residue.serial_number() as i16));
256 }
257 });
258 }
259 }
260
261 /// Returns the unique identifier of the Interactor.
262 ///
263 /// # Returns
264 ///
265 /// A `u16` representing the ID of the Interactor.
266 pub fn id(&self) -> u16 {
267 self.id
268 }
269
270 /// Returns the chain identifier of the Interactor.
271 ///
272 /// # Returns
273 ///
274 /// A string slice (`&str`) representing the chain of the Interactor.
275 pub fn chain(&self) -> &str {
276 &self.chain
277 }
278
279 /// Returns a reference to the set of active residues.
280 ///
281 /// # Returns
282 ///
283 /// A reference to a `HashSet<i16>` containing the active residue numbers.
284 pub fn active(&self) -> &HashSet<i16> {
285 &self.active
286 }
287
288 /// Returns a reference to a set of active atoms strings.
289 ///
290 /// # Returns
291 ///
292 /// A reference to a `Option<Vec<String>>` containing active atom names.
293 pub fn active_atoms(&self) -> &Option<Vec<String>> {
294 &self.active_atoms
295 }
296
297 /// Returns a reference to the set of passive residues.
298 ///
299 /// # Returns
300 ///
301 /// A reference to a `HashSet<i16>` containing the passive residue numbers.
302 pub fn passive(&self) -> &HashSet<i16> {
303 &self.passive
304 }
305
306 /// Returns a reference to a set of passive atoms strings.
307 ///
308 /// # Returns
309 ///
310 /// A reference to a `Option<Vec<String>>` containing passive atom names.
311 pub fn passive_atoms(&self) -> &Option<Vec<String>> {
312 &self.passive_atoms
313 }
314
315 /// Returns the wildcard string associated with this Interactor.
316 ///
317 /// # Returns
318 ///
319 /// - If a wildcard is set, returns a string slice (`&str`) containing the wildcard value.
320 /// - If no wildcard is set, returns an empty string slice.
321 ///
322 /// # Notes
323 ///
324 /// - This method provides read-only access to the wildcard value.
325 /// - The wildcard is typically used to represent any residue or atom in certain contexts.
326 pub fn wildcard(&self) -> &str {
327 match &self.wildcard {
328 Some(wildcard) => wildcard,
329 None => "",
330 }
331 }
332
333 /// Returns a reference to the set of target interactor IDs.
334 ///
335 /// # Returns
336 ///
337 /// A reference to a `HashSet<u16>` containing the IDs of target interactors.
338 pub fn target(&self) -> &HashSet<u16> {
339 &self.target
340 }
341
342 /// Returns the structure file path of the Interactor.
343 ///
344 /// # Returns
345 ///
346 /// A string slice (`&str`) representing the structure file path, or an empty string if not set.
347 pub fn structure(&self) -> &str {
348 match &self.structure {
349 Some(structure) => structure,
350 None => "",
351 }
352 }
353
354 /// Sets the structure file path for the Interactor.
355 ///
356 /// # Arguments
357 ///
358 /// * `structure` - A string slice containing the path to the structure file.
359 pub fn set_structure(&mut self, structure: &str) {
360 self.structure = Some(structure.to_string());
361 }
362
363 /// Loads a PDB structure from the given path and stores it.
364 ///
365 /// # Returns
366 ///
367 /// `Ok(())` on success, or `Vec<PDBError>` if loading failed.
368 pub fn load_structure(&mut self, structure_path: &str) -> Result<(), Vec<PDBError>> {
369 match load_pdb(structure_path) {
370 Ok(pdb) => {
371 self.structure = Some(structure_path.to_string());
372 self.pdb = Some(pdb);
373 Ok(())
374 }
375 Err(e) => Err(e),
376 }
377 }
378
379 /// Returns a reference to the stored PDB structure, if any.
380 ///
381 /// # Returns
382 ///
383 /// Reference to an `Option<PDB>` containing the structure.
384 pub fn pdb(&self) -> &Option<PDB> {
385 &self.pdb
386 }
387
388 /// Stores a PDB structure in the Interactor.
389 ///
390 /// # Arguments
391 ///
392 /// * `pdb` - The PDB structure to store
393 pub fn set_pdb(&mut self, pdb: PDB) {
394 self.pdb = Some(pdb)
395 }
396
397 /// Sets the chain identifier for the Interactor.
398 ///
399 /// # Arguments
400 ///
401 /// * `chain` - A string slice containing the chain identifier.
402 pub fn set_chain(&mut self, chain: &str) {
403 self.chain = chain.to_string();
404 }
405
406 /// Sets the active residues for the Interactor.
407 ///
408 /// # Arguments
409 ///
410 /// * `active` - A vector of `i16` values representing the active residue numbers.
411 pub fn set_active(&mut self, active: Vec<i16>) {
412 self.active = active.into_iter().collect();
413 }
414
415 /// Sets the passive residues for the Interactor.
416 ///
417 /// # Arguments
418 ///
419 /// * `passive` - A vector of `i16` values representing the passive residue numbers.
420 pub fn set_passive(&mut self, passive: Vec<i16>) {
421 self.passive = passive.into_iter().collect();
422 }
423
424 /// Sets the wildcard string for this Interactor.
425 ///
426 /// This method allows you to set or update the wildcard value associated with the Interactor.
427 ///
428 /// # Arguments
429 ///
430 /// * `wildcard` - A string slice (`&str`) that specifies the new wildcard value.
431 ///
432 /// # Notes
433 ///
434 /// - This method will overwrite any previously set wildcard value.
435 /// - The wildcard is stored as an owned `String`, so the input `&str` is cloned.
436 /// - An empty string is a valid wildcard value, though its interpretation may depend on the context.
437 /// - The wildcard is typically used to represent any residue or atom in certain contexts.
438 pub fn set_wildcard(&mut self, wildcard: &str) {
439 self.wildcard = Some(wildcard.to_string());
440 }
441
442 /// Sets the target distance for the Interactor.
443 ///
444 /// # Arguments
445 ///
446 /// * `distance` - A `f64` value representing the target distance.
447 pub fn set_target_distance(&mut self, distance: f64) {
448 self.target_distance = Some(distance);
449 }
450
451 /// Sets the lower margin for the Interactor.
452 ///
453 /// # Arguments
454 ///
455 /// * `margin` - A `f64` value representing the lower margin.
456 pub fn set_lower_margin(&mut self, margin: f64) {
457 self.lower_margin = Some(margin);
458 }
459
460 /// Sets the upper margin for the Interactor.
461 ///
462 /// # Arguments
463 ///
464 /// * `margin` - A `f64` value representing the upper margin.
465 pub fn set_upper_margin(&mut self, margin: f64) {
466 self.upper_margin = Some(margin);
467 }
468
469 /// Returns whether passive residues should be derived from active residues.
470 ///
471 /// # Returns
472 ///
473 /// A `bool` indicating if passive residues should be derived from active ones.
474 pub fn passive_from_active(&self) -> bool {
475 self.passive_from_active.unwrap_or(false)
476 }
477
478 /// Returns whether surface residues should be treated as passive.
479 ///
480 /// # Returns
481 ///
482 /// A `bool` indicating if surface residues should be treated as passive.
483 pub fn surface_as_passive(&self) -> bool {
484 self.surface_as_passive.unwrap_or(false)
485 }
486
487 /// Returns whether buried residues should be filtered.
488 ///
489 /// # Returns
490 ///
491 /// A `bool` indicating if buried residues should be filtered.
492 pub fn filter_buried(&self) -> bool {
493 self.filter_buried.unwrap_or(false)
494 }
495
496 /// Sets whether passive residues should be derived from active residues.
497 ///
498 /// # Arguments
499 ///
500 /// * `cutoff` - A `f64` value representing the cutoff to filter buried residues.
501 pub fn set_filter_buried_cutoff(&mut self, cutoff: f64) {
502 self.filter_buried_cutoff = Some(cutoff);
503 }
504
505 /// Adds a target interactor ID.
506 ///
507 /// # Arguments
508 ///
509 /// * `target` - A `u16` value representing the ID of the target interactor to add.
510 pub fn add_target(&mut self, target: u16) {
511 self.target.insert(target);
512 }
513
514 /// Sets the active atoms for the Interactor.
515 ///
516 /// # Arguments
517 ///
518 /// * `atoms` - A vector of `String`s representing the active atom names.
519 pub fn set_active_atoms(&mut self, atoms: Vec<String>) {
520 self.active_atoms = Some(atoms);
521 }
522
523 /// Sets the passive atoms for the Interactor.
524 ///
525 /// # Arguments
526 ///
527 /// * `atoms` - A vector of `String`s representing the passive atom names.
528 pub fn set_passive_atoms(&mut self, atoms: Vec<String>) {
529 self.passive_atoms = Some(atoms);
530 }
531
532 /// Creates a block of restraints for the Interactor.
533 ///
534 /// This method generates a string representation of restraints for the Interactor,
535 /// based on its active residues and the provided target residues.
536 ///
537 /// # Arguments
538 ///
539 /// * `target_res` - A vector of tuples, each containing a chain identifier (&str)
540 /// and a residue number (&i16) for the target residues.
541 ///
542 /// # Returns
543 ///
544 /// A `String` containing the formatted block of restraints.
545 ///
546 pub fn create_block(&self, passive_res: Vec<PassiveResidues>) -> String {
547 let mut block = String::new();
548 let mut _active: Vec<i16> = self.active().iter().cloned().collect();
549 _active.sort();
550
551 // Sort the target residues by residue number
552 let mut passive_res: Vec<PassiveResidues> = passive_res.clone();
553 passive_res.sort_by(|a, b| a.res_number.cmp(&b.res_number));
554
555 // Check if need to use multiline separation
556 let multiline = passive_res.len() > 1;
557
558 for resnum in _active {
559 // Create the `assign` statement
560 let atom_str = format_atom_string(&self.active_atoms);
561
562 let mut assign_str = format!(
563 "assign ( resid {} and segid {}{} {})",
564 resnum,
565 self.chain(),
566 atom_str,
567 &self.wildcard()
568 );
569
570 if multiline {
571 assign_str += "\n (\n";
572 }
573
574 block.push_str(assign_str.as_str());
575
576 // Loop over the passive residues
577 let res_lines: Vec<String> = passive_res
578 .iter()
579 .enumerate()
580 .map(|(index, res)| {
581 let atom_str = format_atom_string(res.atom_str);
582
583 let mut res_line = String::new();
584 if multiline {
585 res_line.push_str(
586 format!(
587 " ( {} segid {}{} {})\n",
588 res.res_number
589 .map_or(String::new(), |num| format!("resid {} and", num)),
590 res.chain_id,
591 atom_str,
592 res.wildcard
593 )
594 .as_str(),
595 );
596 } else {
597 res_line.push_str(
598 format!(
599 " ( {} segid {}{} {})",
600 res.res_number
601 .map_or(String::new(), |num| format!("resid {} and", num)),
602 res.chain_id,
603 atom_str,
604 res.wildcard
605 )
606 .as_str(),
607 );
608 }
609
610 if index != passive_res.len() - 1 {
611 res_line.push_str(" or\n");
612 }
613 res_line
614 })
615 .collect();
616
617 block.push_str(&res_lines.join(""));
618
619 let distance_string = format_distance_string(
620 &self.target_distance,
621 &self.lower_margin,
622 &self.upper_margin,
623 );
624 if multiline {
625 block.push_str(format!(" ) {}\n\n", distance_string).as_str());
626 } else {
627 block.push_str(format!(" {}\n\n", distance_string).as_str())
628 }
629 }
630 block
631 }
632
633 pub fn make_pml_string(&self, passive_res: Vec<PassiveResidues>) -> String {
634 let mut pml = String::new();
635 let mut _active: Vec<i16> = self.active().iter().cloned().collect();
636 _active.sort();
637
638 let mut passive_res: Vec<PassiveResidues> = passive_res.clone();
639 passive_res.sort_by(|a, b| a.res_number.cmp(&b.res_number));
640
641 for resnum in _active {
642 let identifier = format!("{}-{}", resnum, self.chain);
643 let active_sel = format!("resi {} and name CA and chain {}", resnum, self.chain);
644
645 for passive_resnum in &passive_res {
646 let passive_sel = format!(
647 "resi {} and name CA and chain {}",
648 passive_resnum.res_number.unwrap(),
649 passive_resnum.chain_id
650 );
651
652 pml.push_str(
653 format!(
654 "distance {}, ({}), ({})\n",
655 identifier, active_sel, passive_sel
656 )
657 .as_str(),
658 )
659 }
660 }
661
662 pml
663 }
664}
665
666#[derive(Debug, Clone)]
667pub struct PassiveResidues<'a> {
668 pub chain_id: &'a str,
669 pub res_number: Option<i16>,
670 wildcard: &'a str,
671 // TODO: ADD THE ATOM ATOM NAMES HERE, THEY SHOULD BE USED WHEN GENERATING THE BLOCK
672 atom_str: &'a Option<Vec<String>>,
673}
674
675/// Collects residue numbers from a vector of Interactors.
676///
677/// This function gathers both active and passive residue numbers from each Interactor,
678/// along with their corresponding chain identifiers.
679///
680/// # Arguments
681///
682/// * `interactors` - A vector of references to Interactor objects.
683///
684/// # Returns
685///
686/// A vector of tuples, where each tuple contains:
687/// - A string slice representing the chain identifier
688/// - A reference to an i16 representing the residue number
689///
690pub fn collect_residues(interactors: Vec<&Interactor>) -> Vec<PassiveResidues> {
691 let mut resnums = Vec::new();
692 for interactor in interactors {
693 let active = interactor.active().iter().map(|&x| PassiveResidues {
694 chain_id: interactor.chain(),
695 res_number: Some(x),
696 wildcard: interactor.wildcard(),
697 atom_str: interactor.active_atoms(),
698 });
699
700 let passive = interactor.passive().iter().map(|&x| PassiveResidues {
701 chain_id: interactor.chain(),
702 res_number: Some(x),
703 wildcard: interactor.wildcard(),
704 atom_str: interactor.passive_atoms(),
705 });
706
707 resnums.extend(active);
708 resnums.extend(passive);
709
710 // If both active and passive are empty, add a single ResidueIdentifier with None as res_number
711 if interactor.active().is_empty() && interactor.passive().is_empty() {
712 resnums.push(PassiveResidues {
713 chain_id: interactor.chain(),
714 res_number: None,
715 wildcard: interactor.wildcard(),
716 atom_str: &None,
717 });
718 }
719 }
720 resnums
721}
722
723/// Formats a distance string based on target, lower, and upper bounds.
724///
725/// This function creates a formatted string representing distance constraints.
726/// If any of the input values are None, default values are used.
727///
728/// # Arguments
729///
730/// * `target` - An Option<f64> representing the target distance.
731/// * `lower` - An Option<f64> representing the lower bound of the distance.
732/// * `upper` - An Option<f64> representing the upper bound of the distance.
733///
734/// # Returns
735///
736/// A String containing the formatted distance values, with one decimal place precision.
737///
738pub fn format_distance_string(
739 target: &Option<f64>,
740 lower: &Option<f64>,
741 upper: &Option<f64>,
742) -> String {
743 let target = match target {
744 Some(target) => target,
745 None => &2.0,
746 };
747
748 let lower = match lower {
749 Some(lower) => lower,
750 None => &2.0,
751 };
752
753 let upper = match upper {
754 Some(upper) => upper,
755 None => &0.0,
756 };
757
758 format!("{:.1} {:.1} {:.1}", target, lower, upper)
759}
760
761/// Formats a string representing atom names for use in constraints.
762///
763/// This function takes an optional vector of atom names and formats them
764/// into a string suitable for use in constraint definitions.
765///
766/// # Arguments
767///
768/// * `atoms` - An Option<Vec<String>> containing atom names.
769///
770/// # Returns
771///
772/// A String containing the formatted atom names, or an empty string if no atoms are provided.
773///
774pub fn format_atom_string(atoms: &Option<Vec<String>>) -> String {
775 match atoms {
776 Some(atoms) if atoms.len() > 1 => {
777 let atoms: String = atoms
778 .iter()
779 .map(|x| format!("name {}", x))
780 .collect::<Vec<String>>()
781 .join(" or ");
782
783 format!(" and ({})", atoms)
784 }
785 Some(atoms) if atoms.len() == 1 => format!(" and name {}", atoms[0]),
786 _ => "".to_string(),
787 }
788}
789
790#[cfg(test)]
791mod tests {
792
793 use crate::core::interactor::{Interactor, PassiveResidues};
794
795 #[test]
796 fn test_valid_interactor() {
797 let mut interactor = Interactor::new(1);
798 interactor.set_active(vec![1]);
799 interactor.set_passive(vec![2]);
800 interactor.add_target(2);
801
802 assert_eq!(interactor.is_valid(), Ok(true));
803 }
804
805 #[test]
806 fn test_invalid_interactor_empty() {
807 let interactor = Interactor::new(1);
808
809 assert_eq!(interactor.is_valid(), Err("Target residues are empty"));
810 }
811
812 #[test]
813 fn test_invalid_interactor_overlap() {
814 let mut interactor = Interactor::new(1);
815 interactor.set_active(vec![1]);
816 interactor.set_passive(vec![1]);
817 interactor.add_target(2);
818
819 assert_eq!(
820 interactor.is_valid(),
821 Err("Active/Passive selections overlap")
822 );
823 }
824
825 #[test]
826 fn test_set_passive_from_active() {
827 let mut interactor = Interactor::new(1);
828 interactor.load_structure("tests/data/complex.pdb").unwrap();
829 interactor.set_active(vec![1]);
830 interactor.passive_from_active_radius = Some(5.0);
831 interactor.set_passive_from_active();
832
833 let expected_passive = [16, 15, 18, 3, 19, 61, 56, 17, 2, 62, 63];
834
835 assert_eq!(
836 interactor.passive(),
837 &expected_passive.iter().cloned().collect()
838 );
839 }
840
841 #[test]
842 fn test_set_surface_as_passive() {
843 let mut interactor = Interactor::new(1);
844 interactor.load_structure("tests/data/complex.pdb").unwrap();
845 interactor.set_chain("A");
846 interactor.set_surface_as_passive();
847
848 let expected_passive = [
849 938, 965, 953, 944, 933, 958, 966, 972, 931, 936, 961, 929, 943, 954, 932, 945, 942,
850 957, 955, 947, 940, 941, 937, 964, 970, 930, 969, 968, 950, 952, 959, 971, 967, 956,
851 946, 960, 962, 935, 948, 951, 934, 939,
852 ];
853
854 assert_eq!(
855 interactor.passive(),
856 &expected_passive.iter().cloned().collect()
857 );
858 }
859
860 #[test]
861 fn test_remove_buried_active_residues() {
862 let mut interactor = Interactor::new(1);
863
864 interactor.load_structure("tests/data/complex.pdb").unwrap();
865 interactor.set_chain("A");
866 interactor.filter_buried = Some(true);
867 interactor.filter_buried_cutoff = Some(0.7);
868 interactor.set_active(vec![949, 931]);
869 interactor.remove_buried_residues();
870
871 let expected_active = [931];
872
873 assert_eq!(
874 interactor.active(),
875 &expected_active.iter().cloned().collect()
876 );
877 }
878
879 #[test]
880 fn test_create_block_multiline() {
881 let mut interactor = Interactor::new(1);
882 interactor.set_active(vec![1]);
883 interactor.set_chain("A");
884
885 let observed = interactor.create_block(vec![
886 PassiveResidues {
887 chain_id: "B",
888 res_number: Some(2),
889 wildcard: "",
890 atom_str: &None,
891 },
892 PassiveResidues {
893 chain_id: "B",
894 res_number: Some(3),
895 wildcard: "",
896 atom_str: &None,
897 },
898 ]);
899
900 let block = "assign ( resid 1 and segid A )\n (\n ( resid 2 and segid B )\n or\n ( resid 3 and segid B )\n ) 2.0 2.0 0.0\n\n";
901
902 assert_eq!(observed, block);
903 }
904
905 #[test]
906 fn test_create_block_oneline() {
907 let mut interactor = Interactor::new(1);
908 interactor.set_active(vec![1]);
909 interactor.set_chain("A");
910
911 let observed = interactor.create_block(vec![PassiveResidues {
912 chain_id: "B",
913 res_number: Some(2),
914 wildcard: "",
915 atom_str: &None,
916 }]);
917
918 let block = "assign ( resid 1 and segid A ) ( resid 2 and segid B ) 2.0 2.0 0.0\n\n";
919
920 assert_eq!(observed, block);
921 }
922
923 #[test]
924 fn test_create_block_oneline_atom_subset() {
925 let mut interactor = Interactor::new(1);
926 interactor.set_active(vec![1]);
927 interactor.set_chain("A");
928 interactor.set_active_atoms(vec!["CA".to_string(), "CB".to_string()]);
929
930 let observed = interactor.create_block(vec![PassiveResidues {
931 chain_id: "B",
932 res_number: Some(2),
933 wildcard: "",
934 atom_str: &None,
935 }]);
936
937 let block = "assign ( resid 1 and segid A and (name CA or name CB) ) ( resid 2 and segid B ) 2.0 2.0 0.0\n\n";
938
939 assert_eq!(observed, block);
940 }
941
942 #[test]
943 fn test_create_block_multiline_atom_subset() {
944 let mut interactor = Interactor::new(1);
945 interactor.set_active(vec![1]);
946 interactor.set_chain("A");
947 interactor.set_active_atoms(vec!["CA".to_string(), "CB".to_string()]);
948 interactor.set_passive_atoms(vec!["CA".to_string(), "CB".to_string()]);
949 let observed = interactor.create_block(vec![
950 PassiveResidues {
951 chain_id: "B",
952 res_number: Some(2),
953 wildcard: "",
954 atom_str: &None,
955 },
956 PassiveResidues {
957 chain_id: "B",
958 res_number: Some(3),
959 wildcard: "",
960 atom_str: &None,
961 },
962 ]);
963
964 let block = "assign ( resid 1 and segid A and (name CA or name CB) )\n (\n ( resid 2 and segid B )\n or\n ( resid 3 and segid B )\n ) 2.0 2.0 0.0\n\n";
965
966 assert_eq!(observed, block);
967 }
968
969 #[test]
970 fn test_create_block_multiline_atom_subset_passive() {
971 let mut interactor = Interactor::new(1);
972 interactor.set_active(vec![1]);
973 interactor.set_chain("A");
974 interactor.set_active_atoms(vec!["CA".to_string(), "CB".to_string()]);
975 let observed = interactor.create_block(vec![
976 PassiveResidues {
977 chain_id: "B",
978 res_number: Some(2),
979 wildcard: "",
980 atom_str: &Some(vec!["N".to_string(), "C".to_string()]),
981 },
982 PassiveResidues {
983 chain_id: "B",
984 res_number: Some(3),
985 wildcard: "",
986 atom_str: &None,
987 },
988 ]);
989
990 let block = "assign ( resid 1 and segid A and (name CA or name CB) )\n (\n ( resid 2 and segid B and (name N or name C) )\n or\n ( resid 3 and segid B )\n ) 2.0 2.0 0.0\n\n";
991
992 assert_eq!(observed, block);
993 }
994
995 #[test]
996 fn test_create_block_active_atoms() {
997 let mut interactor = Interactor::new(1);
998 interactor.set_active(vec![1]);
999 interactor.set_chain("A");
1000 interactor.set_active_atoms(vec!["CA".to_string()]);
1001
1002 let observed = interactor.create_block(vec![PassiveResidues {
1003 chain_id: "B",
1004 res_number: Some(2),
1005 wildcard: "",
1006 atom_str: &None,
1007 }]);
1008
1009 let block =
1010 "assign ( resid 1 and segid A and name CA ) ( resid 2 and segid B ) 2.0 2.0 0.0\n\n";
1011
1012 assert_eq!(observed, block);
1013 }
1014
1015 #[test]
1016 fn test_create_block_passive_atoms() {
1017 let mut interactor = Interactor::new(1);
1018 interactor.set_active(vec![1]);
1019 interactor.set_chain("A");
1020
1021 let observed = interactor.create_block(vec![PassiveResidues {
1022 chain_id: "B",
1023 res_number: Some(2),
1024 wildcard: "",
1025 atom_str: &Some(vec!["CA".to_string()]),
1026 }]);
1027
1028 let block =
1029 "assign ( resid 1 and segid A ) ( resid 2 and segid B and name CA ) 2.0 2.0 0.0\n\n";
1030
1031 assert_eq!(observed, block);
1032 }
1033
1034 #[test]
1035 fn test_create_block_active_passive_atoms() {
1036 let mut interactor = Interactor::new(1);
1037 interactor.set_active(vec![1]);
1038 interactor.set_chain("A");
1039 interactor.set_active_atoms(vec!["CA".to_string()]);
1040
1041 let observed = interactor.create_block(vec![PassiveResidues {
1042 chain_id: "B",
1043 res_number: Some(2),
1044 wildcard: "",
1045 atom_str: &None,
1046 }]);
1047
1048 let block =
1049 "assign ( resid 1 and segid A and name CA ) ( resid 2 and segid B ) 2.0 2.0 0.0\n\n";
1050
1051 assert_eq!(observed, block);
1052 }
1053
1054 #[test]
1055 fn test_create_multiline_block_active_passive_atoms() {
1056 let mut interactor = Interactor::new(1);
1057 interactor.set_active(vec![1]);
1058 interactor.set_chain("A");
1059 interactor.set_active_atoms(vec!["CA".to_string()]);
1060
1061 let observed = interactor.create_block(vec![
1062 PassiveResidues {
1063 chain_id: "B",
1064 res_number: Some(2),
1065 wildcard: "",
1066 atom_str: &Some(vec!["CB".to_string()]),
1067 },
1068 PassiveResidues {
1069 chain_id: "B",
1070 res_number: Some(3),
1071 wildcard: "",
1072 atom_str: &Some(vec!["N".to_string()]),
1073 },
1074 ]);
1075
1076 let block = "assign ( resid 1 and segid A and name CA )\n (\n ( resid 2 and segid B and name CB )\n or\n ( resid 3 and segid B and name N )\n ) 2.0 2.0 0.0\n\n";
1077
1078 assert_eq!(observed, block);
1079 }
1080
1081 #[test]
1082 fn test_create_block_with_distance() {
1083 let mut interactor = Interactor::new(1);
1084 interactor.set_active(vec![1]);
1085 interactor.set_chain("A");
1086 interactor.set_target_distance(5.0);
1087 interactor.set_lower_margin(0.0);
1088
1089 let observed = interactor.create_block(vec![PassiveResidues {
1090 chain_id: "B",
1091 res_number: Some(2),
1092 wildcard: "",
1093 atom_str: &None,
1094 }]);
1095
1096 let block = "assign ( resid 1 and segid A ) ( resid 2 and segid B ) 5.0 0.0 0.0\n\n";
1097
1098 assert_eq!(observed, block);
1099 }
1100
1101 #[test]
1102 fn test_create_block_with_wildcard() {
1103 let mut interactor = Interactor::new(1);
1104 interactor.set_active(vec![1]);
1105 interactor.set_chain("A");
1106 interactor.set_wildcard("and attr z gt 42.00 ");
1107
1108 let observed = interactor.create_block(vec![PassiveResidues {
1109 chain_id: "B",
1110 res_number: Some(2),
1111 wildcard: "",
1112 atom_str: &None,
1113 }]);
1114
1115 let block = "assign ( resid 1 and segid A and attr z gt 42.00 ) ( resid 2 and segid B ) 2.0 2.0 0.0\n\n";
1116
1117 assert_eq!(observed, block);
1118 }
1119}