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}