use crate::core::sasa;
use crate::core::structure;
use crate::load_pdb;
use pdbtbx::PDB;
use pdbtbx::PDBError;
use serde::Deserialize;
use std::collections::HashSet;
#[derive(Deserialize, Debug, Clone)]
pub struct Interactor {
id: u16,
chain: String,
active: HashSet<i16>,
active_atoms: Option<Vec<String>>,
pub passive: HashSet<i16>,
passive_atoms: Option<Vec<String>>,
target: HashSet<u16>,
target_distance: Option<f64>,
lower_margin: Option<f64>,
upper_margin: Option<f64>,
structure: Option<String>,
pdb: Option<PDB>,
passive_from_active: Option<bool>,
passive_from_active_radius: Option<f64>,
surface_as_passive: Option<bool>,
filter_buried: Option<bool>,
filter_buried_cutoff: Option<f64>,
wildcard: Option<String>,
}
#[allow(clippy::too_many_arguments)]
impl Interactor {
pub fn new(id: u16) -> Self {
Interactor {
id,
chain: String::new(),
active: HashSet::new(),
passive: HashSet::new(),
target: HashSet::new(),
structure: None,
pdb: None,
passive_from_active: None,
passive_from_active_radius: None,
surface_as_passive: None,
filter_buried: None,
filter_buried_cutoff: None,
active_atoms: None,
passive_atoms: None,
wildcard: None,
target_distance: None,
lower_margin: None,
upper_margin: None,
}
}
pub fn is_valid(&self) -> Result<bool, &str> {
if self.target.is_empty() {
return Err("Target residues are empty");
}
if self.active.intersection(&self.passive).next().is_some() {
return Err("Active/Passive selections overlap");
}
Ok(true)
}
pub fn set_passive_from_active(&mut self) {
if let Some(pdb) = &self.pdb {
let residues =
structure::get_residues(pdb, self.active.iter().map(|x| *x as isize).collect());
let search_cutoff = self.passive_from_active_radius.unwrap_or(6.5);
let neighbors = structure::neighbor_search(pdb.clone(), residues, search_cutoff);
neighbors.iter().for_each(|x| {
self.passive.insert(*x as i16);
});
}
}
pub fn set_surface_as_passive(&mut self) {
if let Some(pdb) = &self.pdb {
let sasa = sasa::calculate_sasa(pdb.clone());
sasa.iter().for_each(|r| {
if r.rel_sasa_total > 0.7 && r.chain == self.chain {
self.passive.insert(r.residue.serial_number() as i16);
}
});
}
}
pub fn remove_buried_residues(&mut self) {
if let Some(pdb) = &self.pdb {
let sasa = sasa::calculate_sasa(pdb.clone());
let sasa_cutoff = self.filter_buried_cutoff.unwrap_or(0.7);
sasa.iter().for_each(|r| {
if r.rel_sasa_total < sasa_cutoff && r.chain == self.chain {
self.passive.remove(&(r.residue.serial_number() as i16));
self.active.remove(&(r.residue.serial_number() as i16));
}
});
}
}
pub fn id(&self) -> u16 {
self.id
}
pub fn chain(&self) -> &str {
&self.chain
}
pub fn active(&self) -> &HashSet<i16> {
&self.active
}
pub fn active_atoms(&self) -> &Option<Vec<String>> {
&self.active_atoms
}
pub fn passive(&self) -> &HashSet<i16> {
&self.passive
}
pub fn passive_atoms(&self) -> &Option<Vec<String>> {
&self.passive_atoms
}
pub fn wildcard(&self) -> &str {
match &self.wildcard {
Some(wildcard) => wildcard,
None => "",
}
}
pub fn target(&self) -> &HashSet<u16> {
&self.target
}
pub fn structure(&self) -> &str {
match &self.structure {
Some(structure) => structure,
None => "",
}
}
pub fn set_structure(&mut self, structure: &str) {
self.structure = Some(structure.to_string());
}
pub fn load_structure(&mut self, structure_path: &str) -> Result<(), Vec<PDBError>> {
match load_pdb(structure_path) {
Ok(pdb) => {
self.structure = Some(structure_path.to_string());
self.pdb = Some(pdb);
Ok(())
}
Err(e) => Err(e),
}
}
pub fn pdb(&self) -> &Option<PDB> {
&self.pdb
}
pub fn set_pdb(&mut self, pdb: PDB) {
self.pdb = Some(pdb)
}
pub fn set_chain(&mut self, chain: &str) {
self.chain = chain.to_string();
}
pub fn set_active(&mut self, active: Vec<i16>) {
self.active = active.into_iter().collect();
}
pub fn set_passive(&mut self, passive: Vec<i16>) {
self.passive = passive.into_iter().collect();
}
pub fn set_wildcard(&mut self, wildcard: &str) {
self.wildcard = Some(wildcard.to_string());
}
pub fn set_target_distance(&mut self, distance: f64) {
self.target_distance = Some(distance);
}
pub fn set_lower_margin(&mut self, margin: f64) {
self.lower_margin = Some(margin);
}
pub fn set_upper_margin(&mut self, margin: f64) {
self.upper_margin = Some(margin);
}
pub fn passive_from_active(&self) -> bool {
self.passive_from_active.unwrap_or(false)
}
pub fn surface_as_passive(&self) -> bool {
self.surface_as_passive.unwrap_or(false)
}
pub fn filter_buried(&self) -> bool {
self.filter_buried.unwrap_or(false)
}
pub fn set_filter_buried_cutoff(&mut self, cutoff: f64) {
self.filter_buried_cutoff = Some(cutoff);
}
pub fn add_target(&mut self, target: u16) {
self.target.insert(target);
}
pub fn set_active_atoms(&mut self, atoms: Vec<String>) {
self.active_atoms = Some(atoms);
}
pub fn set_passive_atoms(&mut self, atoms: Vec<String>) {
self.passive_atoms = Some(atoms);
}
pub fn create_block(&self, passive_res: Vec<PassiveResidues>) -> String {
let mut block = String::new();
let mut _active: Vec<i16> = self.active().iter().cloned().collect();
_active.sort();
let mut passive_res: Vec<PassiveResidues> = passive_res.clone();
passive_res.sort_by(|a, b| a.res_number.cmp(&b.res_number));
let multiline = passive_res.len() > 1;
for resnum in _active {
let atom_str = format_atom_string(&self.active_atoms);
let mut assign_str = format!(
"assign ( resid {} and segid {}{} {})",
resnum,
self.chain(),
atom_str,
&self.wildcard()
);
if multiline {
assign_str += "\n (\n";
}
block.push_str(assign_str.as_str());
let res_lines: Vec<String> = passive_res
.iter()
.enumerate()
.map(|(index, res)| {
let atom_str = format_atom_string(res.atom_str);
let mut res_line = String::new();
if multiline {
res_line.push_str(
format!(
" ( {} segid {}{} {})\n",
res.res_number
.map_or(String::new(), |num| format!("resid {} and", num)),
res.chain_id,
atom_str,
res.wildcard
)
.as_str(),
);
} else {
res_line.push_str(
format!(
" ( {} segid {}{} {})",
res.res_number
.map_or(String::new(), |num| format!("resid {} and", num)),
res.chain_id,
atom_str,
res.wildcard
)
.as_str(),
);
}
if index != passive_res.len() - 1 {
res_line.push_str(" or\n");
}
res_line
})
.collect();
block.push_str(&res_lines.join(""));
let distance_string = format_distance_string(
&self.target_distance,
&self.lower_margin,
&self.upper_margin,
);
if multiline {
block.push_str(format!(" ) {}\n\n", distance_string).as_str());
} else {
block.push_str(format!(" {}\n\n", distance_string).as_str())
}
}
block
}
pub fn make_pml_string(&self, passive_res: Vec<PassiveResidues>) -> String {
let mut pml = String::new();
let mut _active: Vec<i16> = self.active().iter().cloned().collect();
_active.sort();
let mut passive_res: Vec<PassiveResidues> = passive_res.clone();
passive_res.sort_by(|a, b| a.res_number.cmp(&b.res_number));
for resnum in _active {
let identifier = format!("{}-{}", resnum, self.chain);
let active_sel = format!("resi {} and name CA and chain {}", resnum, self.chain);
for passive_resnum in &passive_res {
let passive_sel = format!(
"resi {} and name CA and chain {}",
passive_resnum.res_number.unwrap(),
passive_resnum.chain_id
);
pml.push_str(
format!(
"distance {}, ({}), ({})\n",
identifier, active_sel, passive_sel
)
.as_str(),
)
}
}
pml
}
}
#[derive(Debug, Clone)]
pub struct PassiveResidues<'a> {
pub chain_id: &'a str,
pub res_number: Option<i16>,
wildcard: &'a str,
atom_str: &'a Option<Vec<String>>,
}
pub fn collect_residues(interactors: Vec<&Interactor>) -> Vec<PassiveResidues<'_>> {
let mut resnums = Vec::new();
for interactor in interactors {
let active = interactor.active().iter().map(|&x| PassiveResidues {
chain_id: interactor.chain(),
res_number: Some(x),
wildcard: interactor.wildcard(),
atom_str: interactor.active_atoms(),
});
let passive = interactor.passive().iter().map(|&x| PassiveResidues {
chain_id: interactor.chain(),
res_number: Some(x),
wildcard: interactor.wildcard(),
atom_str: interactor.passive_atoms(),
});
resnums.extend(active);
resnums.extend(passive);
if interactor.active().is_empty() && interactor.passive().is_empty() {
resnums.push(PassiveResidues {
chain_id: interactor.chain(),
res_number: None,
wildcard: interactor.wildcard(),
atom_str: &None,
});
}
}
resnums
}
pub fn format_distance_string(
target: &Option<f64>,
lower: &Option<f64>,
upper: &Option<f64>,
) -> String {
let target = match target {
Some(target) => target,
None => &2.0,
};
let lower = match lower {
Some(lower) => lower,
None => &2.0,
};
let upper = match upper {
Some(upper) => upper,
None => &0.0,
};
format!("{:.1} {:.1} {:.1}", target, lower, upper)
}
pub fn format_atom_string(atoms: &Option<Vec<String>>) -> String {
match atoms {
Some(atoms) if atoms.len() > 1 => {
let atoms: String = atoms
.iter()
.map(|x| {
if x.contains("-") || x.contains("+") {
format!(r#"name "{}""#, x)
} else {
format!("name {}", x)
}
})
.collect::<Vec<String>>()
.join(" or ");
format!(" and ({})", atoms)
}
Some(atoms) if atoms.len() == 1 => {
if atoms[0].contains("-") || atoms[0].contains("+") {
format!(r#" and name "{}""#, atoms[0])
} else {
format!(" and name {}", atoms[0])
}
}
_ => "".to_string(),
}
}
#[cfg(test)]
mod tests {
use crate::core::interactor::{Interactor, PassiveResidues, format_atom_string};
#[test]
fn test_format_atom_string() {
let atom_str = format_atom_string(&Some(vec!["O".to_string()]));
let expected_atom_str = " and name O".to_string();
assert_eq!(atom_str, expected_atom_str)
}
#[test]
fn test_format_atom_string_multiple() {
let atom_str = format_atom_string(&Some(vec!["O".to_string(), "CA".to_string()]));
let expected_atom_str = " and (name O or name CA)".to_string();
assert_eq!(atom_str, expected_atom_str)
}
#[test]
fn test_format_atom_string_special_chars() {
let atom_str = format_atom_string(&Some(vec!["ZN+2".to_string()]));
let expected_atom_str = " and name \"ZN+2\"".to_string();
assert_eq!(atom_str, expected_atom_str)
}
#[test]
fn test_format_atom_string_multiple_special_chars() {
let atom_str = format_atom_string(&Some(vec!["ZN+2".to_string(), "FE-3".to_string()]));
let expected_atom_str = " and (name \"ZN+2\" or name \"FE-3\")".to_string();
assert_eq!(atom_str, expected_atom_str)
}
#[test]
fn test_format_atom_string_multiple_hybrid_chars() {
let atom_str = format_atom_string(&Some(vec!["ZN+2".to_string(), "CA".to_string()]));
let expected_atom_str = " and (name \"ZN+2\" or name CA)".to_string();
assert_eq!(atom_str, expected_atom_str)
}
#[test]
fn test_valid_interactor() {
let mut interactor = Interactor::new(1);
interactor.set_active(vec![1]);
interactor.set_passive(vec![2]);
interactor.add_target(2);
assert_eq!(interactor.is_valid(), Ok(true));
}
#[test]
fn test_invalid_interactor_empty() {
let interactor = Interactor::new(1);
assert_eq!(interactor.is_valid(), Err("Target residues are empty"));
}
#[test]
fn test_invalid_interactor_overlap() {
let mut interactor = Interactor::new(1);
interactor.set_active(vec![1]);
interactor.set_passive(vec![1]);
interactor.add_target(2);
assert_eq!(
interactor.is_valid(),
Err("Active/Passive selections overlap")
);
}
#[test]
fn test_set_passive_from_active() {
let mut interactor = Interactor::new(1);
interactor.load_structure("tests/data/complex.pdb").unwrap();
interactor.set_active(vec![1]);
interactor.passive_from_active_radius = Some(5.0);
interactor.set_passive_from_active();
let expected_passive = [16, 15, 18, 3, 19, 61, 56, 17, 2, 62, 63];
assert_eq!(
interactor.passive(),
&expected_passive.iter().cloned().collect()
);
}
#[test]
fn test_set_surface_as_passive() {
let mut interactor = Interactor::new(1);
interactor.load_structure("tests/data/complex.pdb").unwrap();
interactor.set_chain("A");
interactor.set_surface_as_passive();
let expected_passive = [
938, 965, 953, 944, 933, 958, 966, 972, 931, 936, 961, 929, 943, 954, 932, 945, 942,
957, 955, 947, 940, 941, 937, 964, 970, 930, 969, 968, 950, 952, 959, 971, 967, 956,
946, 960, 962, 935, 948, 951, 934, 939,
];
assert_eq!(
interactor.passive(),
&expected_passive.iter().cloned().collect()
);
}
#[test]
fn test_remove_buried_active_residues() {
let mut interactor = Interactor::new(1);
interactor.load_structure("tests/data/complex.pdb").unwrap();
interactor.set_chain("A");
interactor.filter_buried = Some(true);
interactor.filter_buried_cutoff = Some(0.7);
interactor.set_active(vec![949, 931]);
interactor.remove_buried_residues();
let expected_active = [931];
assert_eq!(
interactor.active(),
&expected_active.iter().cloned().collect()
);
}
#[test]
fn test_create_block_multiline() {
let mut interactor = Interactor::new(1);
interactor.set_active(vec![1]);
interactor.set_chain("A");
let observed = interactor.create_block(vec![
PassiveResidues {
chain_id: "B",
res_number: Some(2),
wildcard: "",
atom_str: &None,
},
PassiveResidues {
chain_id: "B",
res_number: Some(3),
wildcard: "",
atom_str: &None,
},
]);
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";
assert_eq!(observed, block);
}
#[test]
fn test_create_block_oneline() {
let mut interactor = Interactor::new(1);
interactor.set_active(vec![1]);
interactor.set_chain("A");
let observed = interactor.create_block(vec![PassiveResidues {
chain_id: "B",
res_number: Some(2),
wildcard: "",
atom_str: &None,
}]);
let block = "assign ( resid 1 and segid A ) ( resid 2 and segid B ) 2.0 2.0 0.0\n\n";
assert_eq!(observed, block);
}
#[test]
fn test_create_block_oneline_atom_subset() {
let mut interactor = Interactor::new(1);
interactor.set_active(vec![1]);
interactor.set_chain("A");
interactor.set_active_atoms(vec!["CA".to_string(), "CB".to_string()]);
let observed = interactor.create_block(vec![PassiveResidues {
chain_id: "B",
res_number: Some(2),
wildcard: "",
atom_str: &None,
}]);
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";
assert_eq!(observed, block);
}
#[test]
fn test_create_block_multiline_atom_subset() {
let mut interactor = Interactor::new(1);
interactor.set_active(vec![1]);
interactor.set_chain("A");
interactor.set_active_atoms(vec!["CA".to_string(), "CB".to_string()]);
interactor.set_passive_atoms(vec!["CA".to_string(), "CB".to_string()]);
let observed = interactor.create_block(vec![
PassiveResidues {
chain_id: "B",
res_number: Some(2),
wildcard: "",
atom_str: &None,
},
PassiveResidues {
chain_id: "B",
res_number: Some(3),
wildcard: "",
atom_str: &None,
},
]);
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";
assert_eq!(observed, block);
}
#[test]
fn test_create_block_multiline_atom_subset_passive() {
let mut interactor = Interactor::new(1);
interactor.set_active(vec![1]);
interactor.set_chain("A");
interactor.set_active_atoms(vec!["CA".to_string(), "CB".to_string()]);
let observed = interactor.create_block(vec![
PassiveResidues {
chain_id: "B",
res_number: Some(2),
wildcard: "",
atom_str: &Some(vec!["N".to_string(), "C".to_string()]),
},
PassiveResidues {
chain_id: "B",
res_number: Some(3),
wildcard: "",
atom_str: &None,
},
]);
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";
assert_eq!(observed, block);
}
#[test]
fn test_create_block_active_atoms() {
let mut interactor = Interactor::new(1);
interactor.set_active(vec![1]);
interactor.set_chain("A");
interactor.set_active_atoms(vec!["CA".to_string()]);
let observed = interactor.create_block(vec![PassiveResidues {
chain_id: "B",
res_number: Some(2),
wildcard: "",
atom_str: &None,
}]);
let block =
"assign ( resid 1 and segid A and name CA ) ( resid 2 and segid B ) 2.0 2.0 0.0\n\n";
assert_eq!(observed, block);
}
#[test]
fn test_create_block_passive_atoms() {
let mut interactor = Interactor::new(1);
interactor.set_active(vec![1]);
interactor.set_chain("A");
let observed = interactor.create_block(vec![PassiveResidues {
chain_id: "B",
res_number: Some(2),
wildcard: "",
atom_str: &Some(vec!["CA".to_string()]),
}]);
let block =
"assign ( resid 1 and segid A ) ( resid 2 and segid B and name CA ) 2.0 2.0 0.0\n\n";
assert_eq!(observed, block);
}
#[test]
fn test_create_block_active_passive_atoms() {
let mut interactor = Interactor::new(1);
interactor.set_active(vec![1]);
interactor.set_chain("A");
interactor.set_active_atoms(vec!["CA".to_string()]);
let observed = interactor.create_block(vec![PassiveResidues {
chain_id: "B",
res_number: Some(2),
wildcard: "",
atom_str: &None,
}]);
let block =
"assign ( resid 1 and segid A and name CA ) ( resid 2 and segid B ) 2.0 2.0 0.0\n\n";
assert_eq!(observed, block);
}
#[test]
fn test_create_multiline_block_active_passive_atoms() {
let mut interactor = Interactor::new(1);
interactor.set_active(vec![1]);
interactor.set_chain("A");
interactor.set_active_atoms(vec!["CA".to_string()]);
let observed = interactor.create_block(vec![
PassiveResidues {
chain_id: "B",
res_number: Some(2),
wildcard: "",
atom_str: &Some(vec!["CB".to_string()]),
},
PassiveResidues {
chain_id: "B",
res_number: Some(3),
wildcard: "",
atom_str: &Some(vec!["N".to_string()]),
},
]);
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";
assert_eq!(observed, block);
}
#[test]
fn test_create_block_with_distance() {
let mut interactor = Interactor::new(1);
interactor.set_active(vec![1]);
interactor.set_chain("A");
interactor.set_target_distance(5.0);
interactor.set_lower_margin(0.0);
let observed = interactor.create_block(vec![PassiveResidues {
chain_id: "B",
res_number: Some(2),
wildcard: "",
atom_str: &None,
}]);
let block = "assign ( resid 1 and segid A ) ( resid 2 and segid B ) 5.0 0.0 0.0\n\n";
assert_eq!(observed, block);
}
#[test]
fn test_create_block_with_wildcard() {
let mut interactor = Interactor::new(1);
interactor.set_active(vec![1]);
interactor.set_chain("A");
interactor.set_wildcard("and attr z gt 42.00 ");
let observed = interactor.create_block(vec![PassiveResidues {
chain_id: "B",
res_number: Some(2),
wildcard: "",
atom_str: &None,
}]);
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";
assert_eq!(observed, block);
}
}