use crate::{CoreError, SecondaryStructure};
use molgfx_math::Vec3;
use std::str::FromStr;
#[path = "parser.rs"]
mod parser;
#[cfg(test)]
#[path = "select_tests.rs"]
mod tests;
#[derive(Clone, PartialEq, Debug)]
pub struct Select(pub(crate) SelectExpr);
#[derive(Clone, PartialEq, Debug)]
pub(crate) enum SelectExpr {
Class(EntityClass),
Predicate(AtomPredicate),
InSphere {
center: Vec3,
radius: f32,
},
InBox {
min: Vec3,
max: Vec3,
},
Within {
distance: f32,
reference: Box<SelectExpr>,
residues: bool,
},
And(Box<SelectExpr>, Box<SelectExpr>),
Or(Box<SelectExpr>, Box<SelectExpr>),
Not(Box<SelectExpr>),
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub(crate) enum EntityClass {
All,
None,
Polymer,
Protein,
Nucleic,
NonPolymer,
Water,
Branched,
}
#[derive(Clone, PartialEq, Debug)]
pub(crate) enum AtomPredicate {
Chain(Box<str>),
ResidueName(Box<str>),
AtomName(Box<str>),
ResidueNumber(i32),
Element(u8),
Secondary(SecondaryStructure),
Scalar {
property: ScalarProperty,
comparison: PropertyComparison,
threshold: f32,
},
Hydrogen,
Heavy,
Backbone,
Terminus,
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub(crate) enum ScalarProperty {
BFactor,
Occupancy,
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum PropertyComparison {
Less,
LessOrEqual,
Equal,
GreaterOrEqual,
Greater,
}
impl PropertyComparison {
pub(crate) const fn matches(self, value: f32, threshold: f32) -> bool {
match self {
Self::Less => value < threshold,
Self::LessOrEqual => value <= threshold,
Self::Equal => value.to_bits() == threshold.to_bits(),
Self::GreaterOrEqual => value >= threshold,
Self::Greater => value > threshold,
}
}
}
impl Select {
#[must_use]
pub const fn all() -> Self {
Self(SelectExpr::Class(EntityClass::All))
}
#[must_use]
pub const fn none() -> Self {
Self(SelectExpr::Class(EntityClass::None))
}
#[must_use]
pub const fn polymer() -> Self {
Self(SelectExpr::Class(EntityClass::Polymer))
}
#[must_use]
pub const fn protein() -> Self {
Self(SelectExpr::Class(EntityClass::Protein))
}
#[must_use]
pub const fn nucleic() -> Self {
Self(SelectExpr::Class(EntityClass::Nucleic))
}
#[must_use]
pub const fn ligands() -> Self {
Self(SelectExpr::Class(EntityClass::NonPolymer))
}
#[must_use]
pub const fn water() -> Self {
Self(SelectExpr::Class(EntityClass::Water))
}
#[must_use]
pub const fn branched() -> Self {
Self(SelectExpr::Class(EntityClass::Branched))
}
pub fn chain(label: impl AsRef<str>) -> Result<Self, CoreError> {
text_predicate(label, AtomPredicate::Chain, "chain label must not be empty")
}
pub fn residue_name(name: impl AsRef<str>) -> Result<Self, CoreError> {
text_predicate(
name,
AtomPredicate::ResidueName,
"residue name must not be empty",
)
}
pub fn atom_name(name: impl AsRef<str>) -> Result<Self, CoreError> {
text_predicate(name, AtomPredicate::AtomName, "atom name must not be empty")
}
#[must_use]
pub const fn residue(number: i32) -> Self {
Self(SelectExpr::Predicate(AtomPredicate::ResidueNumber(number)))
}
pub fn element(symbol: &str) -> Result<Self, CoreError> {
let Some(element) = molframe::Element::from_symbol(symbol) else {
return Err(invalid("element symbol is unknown"));
};
Ok(Self(SelectExpr::Predicate(AtomPredicate::Element(
element.atomic_number(),
))))
}
#[must_use]
pub const fn secondary(value: SecondaryStructure) -> Self {
Self(SelectExpr::Predicate(AtomPredicate::Secondary(value)))
}
#[must_use]
pub const fn helix() -> Self {
Self::secondary(SecondaryStructure::Helix)
}
#[must_use]
pub const fn sheet() -> Self {
Self::secondary(SecondaryStructure::Strand)
}
#[must_use]
pub const fn coil() -> Self {
Self::secondary(SecondaryStructure::Coil)
}
#[must_use]
pub const fn hydrogen() -> Self {
Self(SelectExpr::Predicate(AtomPredicate::Hydrogen))
}
#[must_use]
pub const fn heavy() -> Self {
Self(SelectExpr::Predicate(AtomPredicate::Heavy))
}
#[must_use]
pub const fn backbone() -> Self {
Self(SelectExpr::Predicate(AtomPredicate::Backbone))
}
#[must_use]
pub const fn terminus() -> Self {
Self(SelectExpr::Predicate(AtomPredicate::Terminus))
}
pub fn b_factor(comparison: PropertyComparison, threshold: f32) -> Result<Self, CoreError> {
scalar(ScalarProperty::BFactor, comparison, threshold)
}
pub fn occupancy(comparison: PropertyComparison, threshold: f32) -> Result<Self, CoreError> {
scalar(ScalarProperty::Occupancy, comparison, threshold)
}
pub fn within(distance: f32, reference: Self) -> Result<Self, CoreError> {
spatial(distance, reference, false)
}
pub fn residues_within(distance: f32, reference: Self) -> Result<Self, CoreError> {
spatial(distance, reference, true)
}
pub fn beyond(distance: f32, reference: Self) -> Result<Self, CoreError> {
Ok(spatial(distance, reference, false)?.negate())
}
pub fn in_sphere(center: Vec3, radius: f32) -> Result<Self, CoreError> {
if !center.is_finite() || !radius.is_finite() || radius < 0.0 {
return Err(invalid("sphere center and radius must be finite and valid"));
}
Ok(Self(SelectExpr::InSphere { center, radius }))
}
pub fn in_box(min: Vec3, max: Vec3) -> Result<Self, CoreError> {
if !min.is_finite() || !max.is_finite() || min.x > max.x || min.y > max.y || min.z > max.z {
return Err(invalid("box bounds must be finite and ordered"));
}
Ok(Self(SelectExpr::InBox { min, max }))
}
#[must_use]
pub fn and(self, other: Self) -> Self {
Self(SelectExpr::And(Box::new(self.0), Box::new(other.0)))
}
#[must_use]
pub fn or(self, other: Self) -> Self {
Self(SelectExpr::Or(Box::new(self.0), Box::new(other.0)))
}
#[must_use]
pub fn negate(self) -> Self {
Self(SelectExpr::Not(Box::new(self.0)))
}
}
impl FromStr for Select {
type Err = CoreError;
fn from_str(source: &str) -> Result<Self, Self::Err> {
parser::parse(source)
}
}
fn spatial(distance: f32, reference: Select, residues: bool) -> Result<Select, CoreError> {
if !distance.is_finite() || distance < 0.0 {
return Err(invalid("spatial distance must be finite and non-negative"));
}
Ok(Select(SelectExpr::Within {
distance,
reference: Box::new(reference.0),
residues,
}))
}
fn text_predicate(
text: impl AsRef<str>,
make: fn(Box<str>) -> AtomPredicate,
reason: &'static str,
) -> Result<Select, CoreError> {
let text = text.as_ref().trim();
if text.is_empty() {
return Err(invalid(reason));
}
Ok(Select(SelectExpr::Predicate(make(text.into()))))
}
fn scalar(
property: ScalarProperty,
comparison: PropertyComparison,
threshold: f32,
) -> Result<Select, CoreError> {
if !threshold.is_finite() {
return Err(invalid("property threshold must be finite"));
}
Ok(Select(SelectExpr::Predicate(AtomPredicate::Scalar {
property,
comparison,
threshold,
})))
}
const fn invalid(reason: &'static str) -> CoreError {
CoreError::InvalidSelection { reason }
}