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