Skip to main content

haddock_restraints/core/
interactor.rs

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