pub use ffcharge::NucleicScheme;
pub use ffcharge::ProteinScheme;
pub use ffcharge::WaterScheme;
pub use cheq::{BasisType, DampingStrategy, SolverOptions};
#[derive(Debug, Clone, Default)]
pub enum ChargeMethod {
#[default]
None,
Qeq(QeqConfig),
Hybrid(HybridConfig),
}
#[derive(Debug, Clone)]
pub struct QeqConfig {
pub total_charge: f64,
pub solver_options: SolverOptions,
}
impl Default for QeqConfig {
fn default() -> Self {
Self {
total_charge: 0.0,
solver_options: SolverOptions::default(),
}
}
}
#[derive(Debug, Clone)]
pub struct HybridConfig {
pub protein_scheme: ProteinScheme,
pub nucleic_scheme: NucleicScheme,
pub water_scheme: WaterScheme,
pub ligand_configs: Vec<LigandChargeConfig>,
pub default_ligand_method: LigandQeqMethod,
}
impl Default for HybridConfig {
fn default() -> Self {
Self {
protein_scheme: ProteinScheme::default(),
nucleic_scheme: NucleicScheme::default(),
water_scheme: WaterScheme::default(),
ligand_configs: Vec::new(),
default_ligand_method: LigandQeqMethod::Embedded(EmbeddedQeqConfig::default()),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct ResidueSelector {
pub chain_id: String,
pub residue_id: i32,
pub insertion_code: Option<char>,
}
impl ResidueSelector {
pub fn new(chain_id: impl Into<String>, residue_id: i32, insertion_code: Option<char>) -> Self {
Self {
chain_id: chain_id.into(),
residue_id,
insertion_code,
}
}
pub fn matches(&self, chain_id: &str, residue_id: i32, insertion_code: Option<char>) -> bool {
self.chain_id == chain_id
&& self.residue_id == residue_id
&& self.insertion_code == insertion_code
}
}
#[derive(Debug, Clone)]
pub struct LigandChargeConfig {
pub selector: ResidueSelector,
pub method: LigandQeqMethod,
}
#[derive(Debug, Clone)]
pub enum LigandQeqMethod {
Vacuum(QeqConfig),
Embedded(EmbeddedQeqConfig),
}
impl Default for LigandQeqMethod {
fn default() -> Self {
Self::Vacuum(QeqConfig::default())
}
}
#[derive(Debug, Clone)]
pub struct EmbeddedQeqConfig {
pub cutoff_radius: f64,
pub qeq: QeqConfig,
}
impl Default for EmbeddedQeqConfig {
fn default() -> Self {
Self {
cutoff_radius: 10.0,
qeq: QeqConfig::default(),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn charge_method_default_is_none() {
assert!(matches!(ChargeMethod::default(), ChargeMethod::None));
}
#[test]
fn qeq_config_default_values() {
let config = QeqConfig::default();
assert_eq!(config.total_charge, 0.0);
assert!(config.solver_options.hydrogen_scf);
}
#[test]
fn hybrid_config_default_schemes() {
let config = HybridConfig::default();
assert_eq!(config.protein_scheme, ProteinScheme::AmberFFSB);
assert_eq!(config.nucleic_scheme, NucleicScheme::Amber);
assert_eq!(config.water_scheme, WaterScheme::Tip3p);
assert!(config.ligand_configs.is_empty());
assert!(matches!(
config.default_ligand_method,
LigandQeqMethod::Embedded(_)
));
if let LigandQeqMethod::Embedded(embedded) = &config.default_ligand_method {
assert_eq!(embedded.cutoff_radius, 10.0);
}
}
#[test]
fn residue_selector_matches() {
let selector = ResidueSelector::new("A", 100, None);
assert!(selector.matches("A", 100, None));
assert!(!selector.matches("A", 100, Some('B')));
assert!(!selector.matches("B", 100, None));
assert!(!selector.matches("A", 101, None));
let with_icode = ResidueSelector::new("A", 100, Some('X'));
assert!(with_icode.matches("A", 100, Some('X')));
assert!(!with_icode.matches("A", 100, None));
assert!(!with_icode.matches("A", 100, Some('Y')));
}
#[test]
fn ligand_qeq_method_default_is_vacuum() {
assert!(matches!(
LigandQeqMethod::default(),
LigandQeqMethod::Vacuum(_)
));
}
#[test]
fn embedded_qeq_config_default_cutoff() {
let config = EmbeddedQeqConfig::default();
assert_eq!(config.cutoff_radius, 10.0);
}
#[test]
fn hybrid_config_with_custom_ligand() {
let config = HybridConfig {
ligand_configs: vec![LigandChargeConfig {
selector: ResidueSelector::new("A", 500, None),
method: LigandQeqMethod::Embedded(EmbeddedQeqConfig {
cutoff_radius: 8.0,
qeq: QeqConfig {
total_charge: -1.0,
..Default::default()
},
}),
}],
..Default::default()
};
assert_eq!(config.ligand_configs.len(), 1);
assert!(config.ligand_configs[0].selector.matches("A", 500, None));
if let LigandQeqMethod::Embedded(embedded) = &config.ligand_configs[0].method {
assert_eq!(embedded.cutoff_radius, 8.0);
assert_eq!(embedded.qeq.total_charge, -1.0);
} else {
panic!("expected Embedded variant");
}
}
}