Skip to main content

haddock_restraints/core/
interactor.rs

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