haddock_restraints/core/
interactor.rs

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