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| format!("name {}", x))
780                .collect::<Vec<String>>()
781                .join(" or ");
782
783            format!(" and ({})", atoms)
784        }
785        Some(atoms) if atoms.len() == 1 => format!(" and name {}", atoms[0]),
786        _ => "".to_string(),
787    }
788}
789
790#[cfg(test)]
791mod tests {
792
793    use crate::core::interactor::{Interactor, PassiveResidues};
794
795    #[test]
796    fn test_valid_interactor() {
797        let mut interactor = Interactor::new(1);
798        interactor.set_active(vec![1]);
799        interactor.set_passive(vec![2]);
800        interactor.add_target(2);
801
802        assert_eq!(interactor.is_valid(), Ok(true));
803    }
804
805    #[test]
806    fn test_invalid_interactor_empty() {
807        let interactor = Interactor::new(1);
808
809        assert_eq!(interactor.is_valid(), Err("Target residues are empty"));
810    }
811
812    #[test]
813    fn test_invalid_interactor_overlap() {
814        let mut interactor = Interactor::new(1);
815        interactor.set_active(vec![1]);
816        interactor.set_passive(vec![1]);
817        interactor.add_target(2);
818
819        assert_eq!(
820            interactor.is_valid(),
821            Err("Active/Passive selections overlap")
822        );
823    }
824
825    #[test]
826    fn test_set_passive_from_active() {
827        let mut interactor = Interactor::new(1);
828        interactor.load_structure("tests/data/complex.pdb").unwrap();
829        interactor.set_active(vec![1]);
830        interactor.passive_from_active_radius = Some(5.0);
831        interactor.set_passive_from_active();
832
833        let expected_passive = [16, 15, 18, 3, 19, 61, 56, 17, 2, 62, 63];
834
835        assert_eq!(
836            interactor.passive(),
837            &expected_passive.iter().cloned().collect()
838        );
839    }
840
841    #[test]
842    fn test_set_surface_as_passive() {
843        let mut interactor = Interactor::new(1);
844        interactor.load_structure("tests/data/complex.pdb").unwrap();
845        interactor.set_chain("A");
846        interactor.set_surface_as_passive();
847
848        let expected_passive = [
849            938, 965, 953, 944, 933, 958, 966, 972, 931, 936, 961, 929, 943, 954, 932, 945, 942,
850            957, 955, 947, 940, 941, 937, 964, 970, 930, 969, 968, 950, 952, 959, 971, 967, 956,
851            946, 960, 962, 935, 948, 951, 934, 939,
852        ];
853
854        assert_eq!(
855            interactor.passive(),
856            &expected_passive.iter().cloned().collect()
857        );
858    }
859
860    #[test]
861    fn test_remove_buried_active_residues() {
862        let mut interactor = Interactor::new(1);
863
864        interactor.load_structure("tests/data/complex.pdb").unwrap();
865        interactor.set_chain("A");
866        interactor.filter_buried = Some(true);
867        interactor.filter_buried_cutoff = Some(0.7);
868        interactor.set_active(vec![949, 931]);
869        interactor.remove_buried_residues();
870
871        let expected_active = [931];
872
873        assert_eq!(
874            interactor.active(),
875            &expected_active.iter().cloned().collect()
876        );
877    }
878
879    #[test]
880    fn test_create_block_multiline() {
881        let mut interactor = Interactor::new(1);
882        interactor.set_active(vec![1]);
883        interactor.set_chain("A");
884
885        let observed = interactor.create_block(vec![
886            PassiveResidues {
887                chain_id: "B",
888                res_number: Some(2),
889                wildcard: "",
890                atom_str: &None,
891            },
892            PassiveResidues {
893                chain_id: "B",
894                res_number: Some(3),
895                wildcard: "",
896                atom_str: &None,
897            },
898        ]);
899
900        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";
901
902        assert_eq!(observed, block);
903    }
904
905    #[test]
906    fn test_create_block_oneline() {
907        let mut interactor = Interactor::new(1);
908        interactor.set_active(vec![1]);
909        interactor.set_chain("A");
910
911        let observed = interactor.create_block(vec![PassiveResidues {
912            chain_id: "B",
913            res_number: Some(2),
914            wildcard: "",
915            atom_str: &None,
916        }]);
917
918        let block = "assign ( resid 1 and segid A ) ( resid 2 and segid B ) 2.0 2.0 0.0\n\n";
919
920        assert_eq!(observed, block);
921    }
922
923    #[test]
924    fn test_create_block_oneline_atom_subset() {
925        let mut interactor = Interactor::new(1);
926        interactor.set_active(vec![1]);
927        interactor.set_chain("A");
928        interactor.set_active_atoms(vec!["CA".to_string(), "CB".to_string()]);
929
930        let observed = interactor.create_block(vec![PassiveResidues {
931            chain_id: "B",
932            res_number: Some(2),
933            wildcard: "",
934            atom_str: &None,
935        }]);
936
937        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";
938
939        assert_eq!(observed, block);
940    }
941
942    #[test]
943    fn test_create_block_multiline_atom_subset() {
944        let mut interactor = Interactor::new(1);
945        interactor.set_active(vec![1]);
946        interactor.set_chain("A");
947        interactor.set_active_atoms(vec!["CA".to_string(), "CB".to_string()]);
948        interactor.set_passive_atoms(vec!["CA".to_string(), "CB".to_string()]);
949        let observed = interactor.create_block(vec![
950            PassiveResidues {
951                chain_id: "B",
952                res_number: Some(2),
953                wildcard: "",
954                atom_str: &None,
955            },
956            PassiveResidues {
957                chain_id: "B",
958                res_number: Some(3),
959                wildcard: "",
960                atom_str: &None,
961            },
962        ]);
963
964        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";
965
966        assert_eq!(observed, block);
967    }
968
969    #[test]
970    fn test_create_block_multiline_atom_subset_passive() {
971        let mut interactor = Interactor::new(1);
972        interactor.set_active(vec![1]);
973        interactor.set_chain("A");
974        interactor.set_active_atoms(vec!["CA".to_string(), "CB".to_string()]);
975        let observed = interactor.create_block(vec![
976            PassiveResidues {
977                chain_id: "B",
978                res_number: Some(2),
979                wildcard: "",
980                atom_str: &Some(vec!["N".to_string(), "C".to_string()]),
981            },
982            PassiveResidues {
983                chain_id: "B",
984                res_number: Some(3),
985                wildcard: "",
986                atom_str: &None,
987            },
988        ]);
989
990        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";
991
992        assert_eq!(observed, block);
993    }
994
995    #[test]
996    fn test_create_block_active_atoms() {
997        let mut interactor = Interactor::new(1);
998        interactor.set_active(vec![1]);
999        interactor.set_chain("A");
1000        interactor.set_active_atoms(vec!["CA".to_string()]);
1001
1002        let observed = interactor.create_block(vec![PassiveResidues {
1003            chain_id: "B",
1004            res_number: Some(2),
1005            wildcard: "",
1006            atom_str: &None,
1007        }]);
1008
1009        let block =
1010            "assign ( resid 1 and segid A and name CA ) ( resid 2 and segid B ) 2.0 2.0 0.0\n\n";
1011
1012        assert_eq!(observed, block);
1013    }
1014
1015    #[test]
1016    fn test_create_block_passive_atoms() {
1017        let mut interactor = Interactor::new(1);
1018        interactor.set_active(vec![1]);
1019        interactor.set_chain("A");
1020
1021        let observed = interactor.create_block(vec![PassiveResidues {
1022            chain_id: "B",
1023            res_number: Some(2),
1024            wildcard: "",
1025            atom_str: &Some(vec!["CA".to_string()]),
1026        }]);
1027
1028        let block =
1029            "assign ( resid 1 and segid A ) ( resid 2 and segid B and name CA ) 2.0 2.0 0.0\n\n";
1030
1031        assert_eq!(observed, block);
1032    }
1033
1034    #[test]
1035    fn test_create_block_active_passive_atoms() {
1036        let mut interactor = Interactor::new(1);
1037        interactor.set_active(vec![1]);
1038        interactor.set_chain("A");
1039        interactor.set_active_atoms(vec!["CA".to_string()]);
1040
1041        let observed = interactor.create_block(vec![PassiveResidues {
1042            chain_id: "B",
1043            res_number: Some(2),
1044            wildcard: "",
1045            atom_str: &None,
1046        }]);
1047
1048        let block =
1049            "assign ( resid 1 and segid A and name CA ) ( resid 2 and segid B ) 2.0 2.0 0.0\n\n";
1050
1051        assert_eq!(observed, block);
1052    }
1053
1054    #[test]
1055    fn test_create_multiline_block_active_passive_atoms() {
1056        let mut interactor = Interactor::new(1);
1057        interactor.set_active(vec![1]);
1058        interactor.set_chain("A");
1059        interactor.set_active_atoms(vec!["CA".to_string()]);
1060
1061        let observed = interactor.create_block(vec![
1062            PassiveResidues {
1063                chain_id: "B",
1064                res_number: Some(2),
1065                wildcard: "",
1066                atom_str: &Some(vec!["CB".to_string()]),
1067            },
1068            PassiveResidues {
1069                chain_id: "B",
1070                res_number: Some(3),
1071                wildcard: "",
1072                atom_str: &Some(vec!["N".to_string()]),
1073            },
1074        ]);
1075
1076        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";
1077
1078        assert_eq!(observed, block);
1079    }
1080
1081    #[test]
1082    fn test_create_block_with_distance() {
1083        let mut interactor = Interactor::new(1);
1084        interactor.set_active(vec![1]);
1085        interactor.set_chain("A");
1086        interactor.set_target_distance(5.0);
1087        interactor.set_lower_margin(0.0);
1088
1089        let observed = interactor.create_block(vec![PassiveResidues {
1090            chain_id: "B",
1091            res_number: Some(2),
1092            wildcard: "",
1093            atom_str: &None,
1094        }]);
1095
1096        let block = "assign ( resid 1 and segid A ) ( resid 2 and segid B ) 5.0 0.0 0.0\n\n";
1097
1098        assert_eq!(observed, block);
1099    }
1100
1101    #[test]
1102    fn test_create_block_with_wildcard() {
1103        let mut interactor = Interactor::new(1);
1104        interactor.set_active(vec![1]);
1105        interactor.set_chain("A");
1106        interactor.set_wildcard("and attr z gt 42.00 ");
1107
1108        let observed = interactor.create_block(vec![PassiveResidues {
1109            chain_id: "B",
1110            res_number: Some(2),
1111            wildcard: "",
1112            atom_str: &None,
1113        }]);
1114
1115        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";
1116
1117        assert_eq!(observed, block);
1118    }
1119}