use std::{collections::HashMap, fmt::Display, io};
use bincode::{Decode, Encode, config};
use bio_files::PharmacophoreTypeGeneric;
use lin_alg::f64::Vec3;
use crate::{
Color,
molecules::{pocket::Pocket, small::MoleculeSmall},
properties::mol_characterization::{MolCharacterization, RingType},
};
#[derive(Clone, Debug, Default)]
pub struct PharmacophoreState {
pub screening_results: Vec<PhScreeningScore>,
pub screening_in_progress: bool,
pub ph_for_screening: Option<usize>,
}
pub const PHARMACOPHORE_SCREENING_THRESH_DEFAULT: f32 = 0.6;
#[derive(Clone, Debug)]
pub struct PhScreeningScore {
pub index: usize,
pub smiles_or_ident: String, pub score: f32,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Hash, PartialOrd, Ord)] #[repr(u8)]
pub enum PharmacophoreFeatType {
Hydrophobic = 0,
Hydrophilic = 1,
Aromatic = 3,
#[default]
Acceptor = 4,
AcceptorProjected = 5,
Donor = 6,
Cation = 7,
Anion = 8,
DonorProjected = 9,
}
impl PharmacophoreFeatType {
pub fn all() -> Vec<Self> {
use PharmacophoreFeatType::*;
vec![
Hydrophobic,
Hydrophilic,
Aromatic,
Acceptor,
AcceptorProjected, Donor,
Cation,
Anion,
DonorProjected, ]
}
pub fn from_u8(v: u8) -> Option<Self> {
use PharmacophoreFeatType::*;
Some(match v {
0 => Hydrophobic,
1 => Hydrophilic,
3 => Aromatic,
4 => Acceptor,
5 => AcceptorProjected,
6 => Donor,
7 => Cation,
8 => Anion,
9 => DonorProjected,
_ => return None,
})
}
pub fn hint_sites(self, char: &MolCharacterization, atom_posits: &[Vec3]) -> Vec<Vec3> {
use PharmacophoreFeatType::*;
match self {
Aromatic => {
let mut sites = Vec::new();
for ring in char
.rings
.iter()
.filter(|r| r.ring_type == RingType::Aromatic)
{
sites.push(ring.center(atom_posits));
}
sites
}
Donor => {
let mut sites = Vec::new();
for v in &char.h_bond_donor {
sites.push(atom_posits[*v]);
}
sites
}
Acceptor => {
let mut sites = Vec::new();
for v in &char.h_bond_acceptor {
sites.push(atom_posits[*v]);
}
sites
}
Hydrophobic => {
let mut sites = Vec::new();
for v in &char.hydrophobic_carbon {
sites.push(atom_posits[*v]);
}
sites
}
_ => Vec::new(),
}
}
pub fn disp_radius(self) -> f32 {
use PharmacophoreFeatType::*;
match self {
Aromatic => 1.05,
Hydrophobic => 1.0, _ => 0.6,
}
}
pub fn color(self) -> Color {
use PharmacophoreFeatType::*;
match self {
Hydrophobic => (0., 0.8, 0.),
Hydrophilic => (1., 1., 1.),
Aromatic => (0.4, 0.1, 0.8), Acceptor => (1., 0.5, 0.2),
Donor => (1., 1., 1.), _ => (1., 0., 0.), }
}
pub fn to_generic(self) -> PharmacophoreTypeGeneric {
use PharmacophoreFeatType::*;
match self {
Hydrophobic => PharmacophoreTypeGeneric::Acceptor,
Hydrophilic => PharmacophoreTypeGeneric::Hydrophobic,
Aromatic => PharmacophoreTypeGeneric::Aromatic,
Acceptor | AcceptorProjected => PharmacophoreTypeGeneric::Acceptor,
Donor | DonorProjected => PharmacophoreTypeGeneric::Donor,
Cation => PharmacophoreTypeGeneric::Cation,
Anion => PharmacophoreTypeGeneric::Anion,
}
}
}
impl Display for PharmacophoreFeatType {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{:?}", self)
}
}
impl From<PharmacophoreTypeGeneric> for PharmacophoreFeatType {
fn from(value: PharmacophoreTypeGeneric) -> Self {
use PharmacophoreFeatType::*;
match &value {
PharmacophoreTypeGeneric::Acceptor => Acceptor,
PharmacophoreTypeGeneric::Donor => Donor,
PharmacophoreTypeGeneric::Cation => Cation,
PharmacophoreTypeGeneric::Rings => Aromatic, PharmacophoreTypeGeneric::Hydrophobic => Hydrophobic,
PharmacophoreTypeGeneric::Hydrophilic => Hydrophilic,
PharmacophoreTypeGeneric::Anion => Anion,
PharmacophoreTypeGeneric::Aromatic => Aromatic,
PharmacophoreTypeGeneric::Other(v) => {
eprintln!("Unknown generic Pharmacophore type: {v}");
Acceptor
}
}
}
}
#[derive(Clone, Debug, Encode, Decode)]
pub struct Oscillator {
pub k_b: f32,
pub max_displacement: f32,
pub orientation: Vec3,
}
#[derive(Clone, Debug, Encode, Decode)]
pub enum Motion {
Oscillator(Oscillator),
Gaussian(Vec<(f32, f32)>),
}
#[derive(Clone, Copy, PartialEq, Debug)]
pub enum FeatureRelation {
And((usize, usize)),
Or((usize, usize)),
}
impl FeatureRelation {
pub fn to_bytes(&self) -> Vec<u8> {
let mut res = vec![0; 9];
match self {
Self::And((v0, v1)) => {
res[0] = 0;
copy_le!(res, (*v0 as u32), 1..5);
copy_le!(res, (*v1 as u32), 5..9);
}
Self::Or((v0, v1)) => {
res[0] = 1;
copy_le!(res, (*v0 as u32), 1..5);
copy_le!(res, (*v1 as u32), 5..9);
}
}
res
}
pub fn from_bytes(bytes: &[u8]) -> Self {
let v0 = parse_le!(bytes, u32, 1..5) as usize;
let v1 = parse_le!(bytes, u32, 5..9) as usize;
match bytes[0] {
0 => Self::And((v0, v1)),
1 => Self::Or((v0, v1)),
_ => {
eprintln!("Error parsing feat relation");
Self::Or((v0, v1))
}
}
}
}
#[derive(Clone, Debug)]
pub struct PharmacophoreFeature {
pub feature_type: PharmacophoreFeatType,
pub posit: Vec3,
pub posit_projected: Option<Vec3>,
pub atom_i: Vec<usize>,
pub atom_i_projected: Option<usize>,
pub strength: f32,
pub tolerance: f32,
pub oscillation: Option<Motion>,
pub ui_selected: bool,
}
impl Default for PharmacophoreFeature {
fn default() -> Self {
Self {
feature_type: PharmacophoreFeatType::default(),
posit: Vec3::new_zero(),
posit_projected: None,
atom_i: Vec::new(),
atom_i_projected: None,
strength: 1.0, tolerance: 1.0,
oscillation: None,
ui_selected: false,
}
}
}
impl Display for PharmacophoreFeature {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"{}: Str: {:.2} Tol: {:.2}",
self.feature_type, self.strength, self.tolerance,
)
}
}
impl PharmacophoreFeature {
pub fn to_bytes(&self) -> Vec<u8> {
let atom_len = self.atom_i.len();
assert!(
atom_len <= u8::MAX as usize,
"atom_i too long to serialize as u8"
);
let total_size = 34 + 4 * atom_len;
let mut result = vec![0; total_size];
let mut i = 0;
result[i] = self.feature_type as u8;
i += 1;
copy_le!(result, self.posit, i..i + 24);
i += 24;
result[i] = atom_len as u8;
i += 1;
for atom_i in &self.atom_i {
copy_le!(result, *atom_i as u32, i..i + 4);
i += 4;
}
copy_le!(result, self.strength, i..i + 4);
i += 4;
copy_le!(result, self.tolerance, i..i + 4);
result
}
pub fn from_bytes(bytes: &[u8]) -> Self {
let mut i = 0usize;
assert!(bytes.len() >= 1 + 24 + 1 + 4 + 4, "bytes too short");
let feature_type = PharmacophoreFeatType::from_u8(bytes[i]).unwrap_or_default();
i += 1;
let posit_bytes: [u8; 24] = bytes[i..i + 24].try_into().unwrap();
let posit = Vec3::from_le_bytes(&posit_bytes);
i += 24;
let atom_len = bytes[i] as usize;
i += 1;
let needed = 1 + 24 + 1 + 4 * atom_len + 4 + 4;
assert!(
bytes.len() >= needed,
"bytes too short for atom_i_len={atom_len}"
);
let mut atom_i = Vec::with_capacity(atom_len);
for _ in 0..atom_len {
let v = parse_le!(bytes, u32, i..i + 4);
atom_i.push(v as usize);
i += 4;
}
let strength = parse_le!(bytes, f32, i..i + 4);
i += 4;
let tolerance = parse_le!(bytes, f32, i..i + 4);
Self {
feature_type,
posit,
posit_projected: None,
atom_i,
atom_i_projected: None,
strength,
tolerance,
oscillation: None,
ui_selected: false,
}
}
pub fn posit_from_atoms(&self, atom_posits: &[Vec3]) -> Option<Vec3> {
if self.atom_i.is_empty() {
return None;
};
let mut result = Vec3::new_zero();
for i in &self.atom_i {
if *i >= atom_posits.len() {
eprintln!("Error: Atom index out of bounds when getting pharmacophore posit");
return None;
}
result += atom_posits[*i];
}
Some(result / self.atom_i.len() as f64)
}
}
#[derive(Clone, Debug, Default)]
pub struct Pharmacophore {
pub name: String,
pub mol_ident: String,
pub features: Vec<PharmacophoreFeature>,
pub feature_relations: Vec<FeatureRelation>,
pub pocket: Option<Pocket>,
}
impl Pharmacophore {
pub fn to_bytes(&self) -> Vec<u8> {
let mut res = Vec::new();
let name_bytes = self.name.as_bytes();
res.extend_from_slice(&(name_bytes.len() as u32).to_le_bytes());
res.extend_from_slice(name_bytes);
let mol_ident_bytes = self.mol_ident.as_bytes();
res.extend_from_slice(&(mol_ident_bytes.len() as u32).to_le_bytes());
res.extend_from_slice(mol_ident_bytes);
res.extend_from_slice(&(self.features.len() as u32).to_le_bytes());
for feat in &self.features {
let feat_bytes = feat.to_bytes();
res.extend_from_slice(&(feat_bytes.len() as u32).to_le_bytes());
res.extend_from_slice(&feat_bytes);
}
res.extend_from_slice(&(self.feature_relations.len() as u32).to_le_bytes());
for rel in &self.feature_relations {
res.extend_from_slice(&rel.to_bytes());
}
match &self.pocket {
None => res.push(0),
Some(pocket) => {
res.push(1);
let pocket_bytes =
bincode::encode_to_vec(pocket, config::standard()).unwrap_or_default();
res.extend_from_slice(&(pocket_bytes.len() as u32).to_le_bytes());
res.extend_from_slice(&pocket_bytes);
}
}
res
}
pub fn from_bytes(bytes: &[u8]) -> Self {
let mut i = 0usize;
let name_len = parse_le!(bytes, u32, i..i + 4) as usize;
i += 4;
let name = String::from_utf8(bytes[i..i + name_len].to_vec()).unwrap_or_default();
i += name_len;
let mol_ident_len = parse_le!(bytes, u32, i..i + 4) as usize;
i += 4;
let mol_ident = String::from_utf8(bytes[i..i + mol_ident_len].to_vec()).unwrap_or_default();
i += mol_ident_len;
let feat_count = parse_le!(bytes, u32, i..i + 4) as usize;
i += 4;
let mut features = Vec::with_capacity(feat_count);
for _ in 0..feat_count {
let feat_len = parse_le!(bytes, u32, i..i + 4) as usize;
i += 4;
features.push(PharmacophoreFeature::from_bytes(&bytes[i..i + feat_len]));
i += feat_len;
}
let rel_count = parse_le!(bytes, u32, i..i + 4) as usize;
i += 4;
let mut feature_relations = Vec::with_capacity(rel_count);
for _ in 0..rel_count {
feature_relations.push(FeatureRelation::from_bytes(&bytes[i..i + 9]));
i += 9;
}
let pocket = if bytes[i] == 0 {
None
} else {
i += 1;
let pocket_len = parse_le!(bytes, u32, i..i + 4) as usize;
bincode::decode_from_slice::<Pocket, _>(&bytes[i..i + pocket_len], config::standard())
.ok()
.map(|(pocket, _)| pocket)
};
Self {
name,
mol_ident,
features,
feature_relations,
pocket,
}
}
pub fn new_all_candidates(mol: &MoleculeSmall) -> Self {
use PharmacophoreFeatType::*;
let Some(char) = mol.characterization.as_ref() else {
return Self::default();
};
let atom_posits = &mol.common.atom_posits;
let mut features = Vec::new();
for &i in &char.h_bond_donor {
if i >= atom_posits.len() {
continue;
}
features.push(PharmacophoreFeature {
feature_type: Donor,
posit: atom_posits[i],
atom_i: vec![i],
tolerance: 1.0,
strength: 1.0,
..Default::default()
});
}
for &i in &char.h_bond_acceptor {
if i >= atom_posits.len() {
continue;
}
features.push(PharmacophoreFeature {
feature_type: Acceptor,
posit: atom_posits[i],
atom_i: vec![i],
tolerance: 1.0,
strength: 1.0,
..Default::default()
});
}
for &i in &char.amines {
if i >= atom_posits.len() {
continue;
}
features.push(PharmacophoreFeature {
feature_type: Cation,
posit: atom_posits[i],
atom_i: vec![i],
tolerance: 1.5,
strength: 1.0,
..Default::default()
});
}
for &i in &char.carboxylate {
if i >= atom_posits.len() {
continue;
}
features.push(PharmacophoreFeature {
feature_type: Anion,
posit: atom_posits[i],
atom_i: vec![i],
tolerance: 1.5,
strength: 1.0,
..Default::default()
});
}
for ring in char
.rings
.iter()
.filter(|r| r.ring_type == RingType::Aromatic)
{
if ring.atoms.iter().any(|&a| a >= atom_posits.len()) {
eprintln!("Warning: aromatic ring has out-of-bounds atom index; skipping feature.");
continue;
}
features.push(PharmacophoreFeature {
feature_type: Aromatic,
posit: ring.center(atom_posits),
atom_i: ring.atoms.clone(),
tolerance: 1.5,
strength: 1.0,
oscillation: Some(Motion::Oscillator(Oscillator {
k_b: 0.0,
max_displacement: 0.0,
orientation: ring.plane_norm,
})),
..Default::default()
});
}
for &i in &char.hydrophobic_carbon {
if i >= atom_posits.len() {
continue;
}
features.push(PharmacophoreFeature {
feature_type: Hydrophobic,
posit: atom_posits[i],
atom_i: vec![i],
tolerance: 1.5,
strength: 0.8, ..Default::default()
});
}
Self {
name: "All sites".to_string(),
mol_ident: mol.common.ident.clone(),
features,
feature_relations: Vec::new(),
pocket: None,
}
}
pub fn score(&self, mol: &MoleculeSmall) -> f32 {
let char = match mol.characterization.as_ref() {
Some(c) => c,
None => return 0.0,
};
if self.features.is_empty() {
return 0.0;
}
let atoms = &mol.common.atoms;
let atom_posits = &mol.common.atom_posits;
let adj = &mol.common.adjacency_list;
if atom_posits.is_empty() {
return 0.0;
}
let donor_dir = |i: usize| -> Option<Vec3> {
if i >= adj.len() {
return None;
}
for &j in &adj[i] {
if j < atoms.len() && atoms[j].element == na_seq::Element::Hydrogen {
let d = atom_posits[j] - atom_posits[i];
let mag = d.magnitude();
if mag > 1e-8 {
return Some(d / mag);
}
}
}
None
};
let acceptor_dir = |i: usize| -> Option<Vec3> {
if i >= adj.len() {
return None;
}
let mut centroid = Vec3::new_zero();
let mut count = 0usize;
for &j in &adj[i] {
if j < atoms.len() && atoms[j].element != na_seq::Element::Hydrogen {
centroid += atom_posits[j];
count += 1;
}
}
if count == 0 {
return None;
}
let c = centroid / count as f64;
let d = atom_posits[i] - c;
let mag = d.magnitude();
if mag > 1e-8 { Some(d / mag) } else { None }
};
#[allow(clippy::type_complexity)]
let ligand_sites =
|ft: PharmacophoreFeatType| -> Vec<(Vec3, Vec<usize>, Option<usize>, Option<Vec3>)> {
use PharmacophoreFeatType::*;
match ft {
Hydrophobic => char
.hydrophobic_carbon
.iter()
.map(|&i| (atom_posits[i], vec![i], None, None))
.collect(),
Hydrophilic => {
let mut sites = Vec::new();
let mut seen = Vec::new();
for &i in &char.h_bond_donor {
sites.push((atom_posits[i], vec![i], None, None));
seen.push(i);
}
for &i in &char.h_bond_acceptor {
if !seen.contains(&i) {
sites.push((atom_posits[i], vec![i], None, None));
}
}
sites
}
Aromatic => char
.rings
.iter()
.enumerate()
.filter(|(_, r)| r.ring_type == RingType::Aromatic)
.map(|(ri, ring)| {
(
ring.center(atom_posits),
Vec::new(),
Some(ri),
Some(ring.plane_norm),
)
})
.collect(),
Acceptor | AcceptorProjected => char
.h_bond_acceptor
.iter()
.map(|&i| (atom_posits[i], vec![i], None, acceptor_dir(i)))
.collect(),
Donor | DonorProjected => char
.h_bond_donor
.iter()
.map(|&i| (atom_posits[i], vec![i], None, donor_dir(i)))
.collect(),
Cation => char
.amines
.iter()
.map(|&i| (atom_posits[i], vec![i], None, None))
.collect(),
Anion => char
.carboxylate
.iter()
.map(|&i| (atom_posits[i], vec![i], None, None))
.collect(),
}
};
let mut feat_order: Vec<usize> = (0..self.features.len()).collect();
feat_order.sort_by(|&a, &b| {
self.features[b]
.strength
.partial_cmp(&self.features[a].strength)
.unwrap_or(std::cmp::Ordering::Equal)
});
let mut claimed_atoms = vec![false; atom_posits.len()];
let mut claimed_rings = vec![false; char.rings.len()];
let mut feat_scores = vec![0.0f32; self.features.len()];
let mut feat_matched = vec![false; self.features.len()];
for &fi in &feat_order {
let feat = &self.features[fi];
let qpos = feat.posit;
let sigma = feat.tolerance.max(1e-6) as f64;
let sites = ligand_sites(feat.feature_type);
if sites.is_empty() {
continue;
}
let feat_dir: Option<Vec3> = if matches!(
feat.feature_type,
PharmacophoreFeatType::AcceptorProjected | PharmacophoreFeatType::DonorProjected
) {
feat.posit_projected
.map(|proj| (proj - qpos).to_normalized())
} else if feat.feature_type == PharmacophoreFeatType::Aromatic {
feat.oscillation.as_ref().and_then(|m| match m {
Motion::Oscillator(o) => Some(o.orientation.to_normalized()),
_ => None,
})
} else {
None
};
let mut best_score = 0.0f32;
let mut best_idx: Option<usize> = None;
for (si, (spos, claim_atoms, claim_ring, site_dir)) in sites.iter().enumerate() {
let already = if let Some(ri) = claim_ring {
*ri < claimed_rings.len() && claimed_rings[*ri]
} else {
claim_atoms
.iter()
.any(|&a| a < claimed_atoms.len() && claimed_atoms[a])
};
if already {
continue;
}
let dist_sq = (qpos - *spos).magnitude_squared();
let mut s = gaussian(dist_sq, sigma);
if let (Some(fd), Some(sd)) = (&feat_dir, site_dir) {
let cos_a = if feat.feature_type == PharmacophoreFeatType::Aromatic {
fd.dot(*sd).abs()
} else {
fd.dot(*sd).max(0.0)
} as f32;
s *= 0.7 + 0.3 * cos_a;
}
if s > best_score {
best_score = s;
best_idx = Some(si);
}
}
if let Some(si) = best_idx {
feat_scores[fi] = best_score;
feat_matched[fi] = best_score > 0.2;
let (_, ref claim_atoms, claim_ring, _) = sites[si];
if let Some(ri) = claim_ring
&& ri < claimed_rings.len()
{
claimed_rings[ri] = true;
}
for &a in claim_atoms {
if a < claimed_atoms.len() {
claimed_atoms[a] = true;
}
}
}
}
let mut or_suppressed = vec![false; self.features.len()];
for rel in &self.feature_relations {
match rel {
FeatureRelation::Or((a, b)) => {
let (a, b) = (*a, *b);
if a < self.features.len() && b < self.features.len() {
if feat_scores[a] >= feat_scores[b] {
or_suppressed[b] = true;
} else {
or_suppressed[a] = true;
}
}
}
FeatureRelation::And((a, b)) => {
let (a, b) = (*a, *b);
if a < self.features.len() && b < self.features.len() {
if !feat_matched[a] || !feat_matched[b] {
feat_scores[a] *= 0.5;
feat_scores[b] *= 0.5;
}
}
}
}
}
let mut total_weight = 0.0f32;
let mut weighted_sum = 0.0f32;
let mut matched_count = 0usize;
let mut considered = 0usize;
for (fi, feat) in self.features.iter().enumerate() {
if or_suppressed[fi] {
continue;
}
let w = feat.strength.max(0.0);
considered += 1;
total_weight += w;
weighted_sum += w * feat_scores[fi];
if feat_matched[fi] {
matched_count += 1;
}
}
if total_weight <= 0.0 || considered == 0 {
return 0.0;
}
let mut score = weighted_sum / total_weight;
let match_frac = matched_count as f32 / considered as f32;
if match_frac < 0.5 {
score *= match_frac / 0.5;
}
if let Some(pocket) = &self.pocket {
let mut clash_count = 0usize;
for &p in atom_posits {
if pocket.volume.inside(p) {
clash_count += 1;
}
}
if clash_count > 0 {
let clash_frac = clash_count as f32 / atom_posits.len().max(1) as f32;
score *= (1.0 - 2.0 * clash_frac).clamp(0.0, 1.0);
}
}
score.clamp(0.0, 1.0)
}
pub fn summary(&self) -> String {
let mut feat_counts = HashMap::new();
for feat in &self.features {
*feat_counts.entry(feat.feature_type).or_insert(0) += 1;
}
let mut items: Vec<_> = feat_counts.into_iter().collect();
items.sort_by(|(a_ft, _), (b_ft, _)| a_ft.cmp(b_ft));
let mut res = String::new();
for (ft, count) in items {
res += &format!("{ft}: {count} ");
}
res
}
}
pub fn add_pharmacophore_feat(
mol: &mut MoleculeSmall,
feat_type: PharmacophoreFeatType,
atom_i: usize,
) -> io::Result<()> {
let mut indices = vec![atom_i];
let posit = if feat_type == PharmacophoreFeatType::Aromatic {
let mut val = None;
for ring in &mol.characterization.as_ref().unwrap().rings {
if ring.atoms.contains(&atom_i) {
val = Some(ring.center(&mol.common.atom_posits));
indices = ring.atoms.clone();
break;
}
}
match val {
Some(v) => v,
None => return Err(io::Error::other("No ring found for atom.")),
}
} else {
if atom_i >= mol.common.atom_posits.len() {
return Err(io::Error::other("Atom index out of bounds."));
}
mol.common.atom_posits[atom_i]
};
mol.pharmacophore.features.push(PharmacophoreFeature {
feature_type: feat_type,
posit,
atom_i: indices,
..Default::default()
});
Ok(())
}
fn gaussian(dist_sq: f64, sigma: f64) -> f32 {
if sigma <= 0.0 {
return 0.0;
}
let denom = 2.0 * sigma * sigma;
(-(dist_sq / denom)).exp() as f32
}