dreid_forge/forge/config/charge.rs
1//! Charge calculation method configurations.
2//!
3//! This module defines the charge assignment strategies available in the
4//! DREIDING parameterization pipeline.
5
6pub use ffcharge::NucleicScheme;
7pub use ffcharge::ProteinScheme;
8pub use ffcharge::WaterScheme;
9
10pub use cheq::{BasisType, DampingStrategy, SolverOptions};
11
12/// Method for calculating partial atomic charges.
13///
14/// Determines how partial charges are assigned to atoms during
15/// parameterization. The choice affects both the accuracy of
16/// electrostatic interactions and hydrogen bond parameters.
17#[derive(Debug, Clone, Default)]
18pub enum ChargeMethod {
19 /// No charge calculation; all charges remain zero.
20 ///
21 /// Use this for gas-phase calculations where electrostatics
22 /// are not critical, or when charges will be assigned externally.
23 #[default]
24 None,
25
26 /// Charge equilibration (QEq) method for all atoms.
27 ///
28 /// Calculates electronegativity-equalized charges based on
29 /// atomic positions and electronegativity parameters. Best suited
30 /// for small molecules or systems without biological metadata.
31 Qeq(QeqConfig),
32
33 /// Hybrid charge assignment for biological systems.
34 ///
35 /// Combines classical force field charges for biomolecules with
36 /// QEq for ligands and hetero groups. Requires biological metadata
37 /// to be present in the input system.
38 Hybrid(HybridConfig),
39}
40
41/// Configuration for QEq charge equilibration.
42///
43/// Controls the behavior of the charge equilibration solver,
44/// including the target total charge and numerical solver options.
45///
46/// # Examples
47///
48/// ```
49/// use dreid_forge::QeqConfig;
50///
51/// // Neutral molecule (default)
52/// let neutral = QeqConfig::default();
53/// assert_eq!(neutral.total_charge, 0.0);
54///
55/// // Negatively charged system
56/// let anion = QeqConfig {
57/// total_charge: -1.0,
58/// ..Default::default()
59/// };
60/// ```
61#[derive(Debug, Clone)]
62pub struct QeqConfig {
63 /// Target total charge of the system in elementary charge units.
64 ///
65 /// The QEq solver will constrain the sum of all partial charges
66 /// to equal this value. Default is `0.0` (neutral).
67 pub total_charge: f64,
68
69 /// QEq solver options.
70 ///
71 /// Controls convergence tolerance, iteration limits, and
72 /// hydrogen SCF treatment.
73 pub solver_options: SolverOptions,
74}
75
76impl Default for QeqConfig {
77 fn default() -> Self {
78 Self {
79 total_charge: 0.0,
80 solver_options: SolverOptions::default(),
81 }
82 }
83}
84
85/// Configuration for hybrid biological/QEq charge assignment.
86///
87/// Specifies how charges are assigned to different molecule types
88/// in a biological system. Standard residues (proteins, nucleic acids,
89/// water, ions) receive classical force field charges, while hetero
90/// groups (ligands) use QEq methods (vacuum or embedded).
91///
92/// # Molecule Classification
93///
94/// The hybrid method classifies atoms based on [`ResidueCategory`](crate::ResidueCategory):
95///
96/// | Category | Charge Source |
97/// |----------|---------------|
98/// | Standard amino acid | [`protein_scheme`](Self::protein_scheme) |
99/// | Standard nucleotide | [`nucleic_scheme`](Self::nucleic_scheme) |
100/// | Water (HOH) | [`water_scheme`](Self::water_scheme) |
101/// | Ion | Formal charges (classical) |
102/// | Hetero | Per-ligand or [`default_ligand_method`](Self::default_ligand_method) |
103#[derive(Debug, Clone)]
104pub struct HybridConfig {
105 /// Force field scheme for protein residue charges.
106 ///
107 /// Default is [`ProteinScheme::AmberFFSB`] (AMBER ff99SB/ff14SB/ff19SB).
108 pub protein_scheme: ProteinScheme,
109
110 /// Force field scheme for nucleic acid residue charges.
111 ///
112 /// Default is [`NucleicScheme::Amber`] (AMBER OL15/OL21/OL24/bsc1/OL3).
113 pub nucleic_scheme: NucleicScheme,
114
115 /// Water model for solvent charges.
116 ///
117 /// Default is [`WaterScheme::Tip3p`].
118 pub water_scheme: WaterScheme,
119
120 /// Per-ligand charge configuration overrides.
121 ///
122 /// Each entry specifies a residue selector and the QEq method to use
123 /// for that specific ligand. Ligands not listed here will use
124 /// [`default_ligand_method`](Self::default_ligand_method).
125 pub ligand_configs: Vec<LigandChargeConfig>,
126
127 /// Default QEq method for ligands not in [`ligand_configs`](Self::ligand_configs).
128 ///
129 /// Default is [`LigandQeqMethod::Embedded`] with 10 Å cutoff and neutral
130 /// total charge.
131 pub default_ligand_method: LigandQeqMethod,
132}
133
134impl Default for HybridConfig {
135 fn default() -> Self {
136 Self {
137 protein_scheme: ProteinScheme::default(),
138 nucleic_scheme: NucleicScheme::default(),
139 water_scheme: WaterScheme::default(),
140 ligand_configs: Vec::new(),
141 default_ligand_method: LigandQeqMethod::Embedded(EmbeddedQeqConfig::default()),
142 }
143 }
144}
145
146/// Residue selector for identifying specific residues.
147///
148/// Used to target specific ligands or hetero groups for custom
149/// charge assignment in hybrid mode.
150///
151/// # Examples
152///
153/// ```
154/// use dreid_forge::ResidueSelector;
155///
156/// // Select residue 500 in chain A
157/// let selector = ResidueSelector::new("A", 500, None);
158/// assert!(selector.matches("A", 500, None));
159/// assert!(!selector.matches("A", 500, Some('A')));
160///
161/// // Select residue 100 with insertion code 'B' in chain L
162/// let with_icode = ResidueSelector::new("L", 100, Some('B'));
163/// assert!(with_icode.matches("L", 100, Some('B')));
164/// assert!(!with_icode.matches("L", 100, None));
165/// ```
166#[derive(Debug, Clone, PartialEq, Eq, Hash)]
167pub struct ResidueSelector {
168 /// Chain identifier.
169 pub chain_id: String,
170 /// Residue sequence number.
171 pub residue_id: i32,
172 /// Optional insertion code for distinguishing residues with same ID.
173 pub insertion_code: Option<char>,
174}
175
176impl ResidueSelector {
177 /// Creates a new residue selector.
178 ///
179 /// # Arguments
180 ///
181 /// * `chain_id` — Chain identifier
182 /// * `residue_id` — Residue sequence number
183 /// * `insertion_code` — Optional insertion code
184 pub fn new(chain_id: impl Into<String>, residue_id: i32, insertion_code: Option<char>) -> Self {
185 Self {
186 chain_id: chain_id.into(),
187 residue_id,
188 insertion_code,
189 }
190 }
191
192 /// Checks if this selector matches an atom's residue info.
193 ///
194 /// # Arguments
195 ///
196 /// * `chain_id` — Chain ID to match
197 /// * `residue_id` — Residue ID to match
198 /// * `insertion_code` — Insertion code to match
199 ///
200 /// # Returns
201 ///
202 /// `true` if all fields match exactly, `false` otherwise.
203 pub fn matches(&self, chain_id: &str, residue_id: i32, insertion_code: Option<char>) -> bool {
204 self.chain_id == chain_id
205 && self.residue_id == residue_id
206 && self.insertion_code == insertion_code
207 }
208}
209
210/// Charge configuration for a specific ligand residue.
211///
212/// Combines a residue selector with the QEq method to use for
213/// that specific ligand.
214#[derive(Debug, Clone)]
215pub struct LigandChargeConfig {
216 /// Selector identifying the target residue.
217 pub selector: ResidueSelector,
218 /// QEq method to use for this ligand.
219 pub method: LigandQeqMethod,
220}
221
222/// QEq method variant for ligand charge assignment.
223///
224/// Ligands can use either vacuum QEq (isolated) or embedded QEq
225/// (polarized by surrounding fixed charges).
226#[derive(Debug, Clone)]
227pub enum LigandQeqMethod {
228 /// Vacuum QEq: ligand treated as isolated molecule.
229 ///
230 /// Best for ligands in solution or far from biomolecular surfaces.
231 Vacuum(QeqConfig),
232
233 /// Embedded QEq: ligand polarized by surrounding fixed charges.
234 ///
235 /// The ligand's charge distribution is influenced by nearby
236 /// protein/nucleic acid atoms within the cutoff radius.
237 Embedded(EmbeddedQeqConfig),
238}
239
240impl Default for LigandQeqMethod {
241 fn default() -> Self {
242 Self::Vacuum(QeqConfig::default())
243 }
244}
245
246/// Configuration for embedded QEq calculations.
247///
248/// In embedded QEq, atoms from surrounding biomolecules (with fixed charges)
249/// contribute an external electrostatic potential that polarizes the
250/// ligand's charge distribution.
251#[derive(Debug, Clone)]
252pub struct EmbeddedQeqConfig {
253 /// Cutoff radius for including environment atoms (Ångströms).
254 ///
255 /// Atoms within this distance from any ligand atom contribute
256 /// to the external electrostatic potential. Default is `10.0` Å.
257 pub cutoff_radius: f64,
258
259 /// QEq solver configuration for the ligand.
260 pub qeq: QeqConfig,
261}
262
263impl Default for EmbeddedQeqConfig {
264 fn default() -> Self {
265 Self {
266 cutoff_radius: 10.0,
267 qeq: QeqConfig::default(),
268 }
269 }
270}
271
272#[cfg(test)]
273mod tests {
274 use super::*;
275
276 #[test]
277 fn charge_method_default_is_none() {
278 assert!(matches!(ChargeMethod::default(), ChargeMethod::None));
279 }
280
281 #[test]
282 fn qeq_config_default_values() {
283 let config = QeqConfig::default();
284 assert_eq!(config.total_charge, 0.0);
285 assert!(config.solver_options.hydrogen_scf);
286 }
287
288 #[test]
289 fn hybrid_config_default_schemes() {
290 let config = HybridConfig::default();
291 assert_eq!(config.protein_scheme, ProteinScheme::AmberFFSB);
292 assert_eq!(config.nucleic_scheme, NucleicScheme::Amber);
293 assert_eq!(config.water_scheme, WaterScheme::Tip3p);
294 assert!(config.ligand_configs.is_empty());
295 assert!(matches!(
296 config.default_ligand_method,
297 LigandQeqMethod::Embedded(_)
298 ));
299 if let LigandQeqMethod::Embedded(embedded) = &config.default_ligand_method {
300 assert_eq!(embedded.cutoff_radius, 10.0);
301 }
302 }
303
304 #[test]
305 fn residue_selector_matches() {
306 let selector = ResidueSelector::new("A", 100, None);
307 assert!(selector.matches("A", 100, None));
308 assert!(!selector.matches("A", 100, Some('B')));
309 assert!(!selector.matches("B", 100, None));
310 assert!(!selector.matches("A", 101, None));
311
312 let with_icode = ResidueSelector::new("A", 100, Some('X'));
313 assert!(with_icode.matches("A", 100, Some('X')));
314 assert!(!with_icode.matches("A", 100, None));
315 assert!(!with_icode.matches("A", 100, Some('Y')));
316 }
317
318 #[test]
319 fn ligand_qeq_method_default_is_vacuum() {
320 assert!(matches!(
321 LigandQeqMethod::default(),
322 LigandQeqMethod::Vacuum(_)
323 ));
324 }
325
326 #[test]
327 fn embedded_qeq_config_default_cutoff() {
328 let config = EmbeddedQeqConfig::default();
329 assert_eq!(config.cutoff_radius, 10.0);
330 }
331
332 #[test]
333 fn hybrid_config_with_custom_ligand() {
334 let config = HybridConfig {
335 ligand_configs: vec![LigandChargeConfig {
336 selector: ResidueSelector::new("A", 500, None),
337 method: LigandQeqMethod::Embedded(EmbeddedQeqConfig {
338 cutoff_radius: 8.0,
339 qeq: QeqConfig {
340 total_charge: -1.0,
341 ..Default::default()
342 },
343 }),
344 }],
345 ..Default::default()
346 };
347
348 assert_eq!(config.ligand_configs.len(), 1);
349 assert!(config.ligand_configs[0].selector.matches("A", 500, None));
350
351 if let LigandQeqMethod::Embedded(embedded) = &config.ligand_configs[0].method {
352 assert_eq!(embedded.cutoff_radius, 8.0);
353 assert_eq!(embedded.qeq.total_charge, -1.0);
354 } else {
355 panic!("Expected Embedded variant");
356 }
357 }
358}