1use super::born::{compute_born_radii, gb_kernel};
14use serde::{Deserialize, Serialize};
15
16#[derive(Debug, Clone, Serialize, Deserialize)]
21pub struct AlpbConfig {
22 pub solvent_dielectric: f64,
24 pub probe_radius: f64,
26 pub surface_tension: f64,
28}
29
30impl Default for AlpbConfig {
31 fn default() -> Self {
32 Self {
33 solvent_dielectric: 78.5,
34 probe_radius: 1.4,
35 surface_tension: 0.005,
36 }
37 }
38}
39
40#[derive(Debug, Clone, Serialize, Deserialize)]
44pub struct AlpbResult {
45 pub electrostatic_energy: f64,
47 pub nonpolar_energy: f64,
49 pub total_energy: f64,
51 pub born_radii: Vec<f64>,
53 pub alpb_factor: f64,
55}
56
57pub fn compute_alpb_solvation(
62 elements: &[u8],
63 positions: &[[f64; 3]],
64 charges: &[f64],
65 config: &AlpbConfig,
66) -> AlpbResult {
67 let n = elements.len();
68 let born = compute_born_radii(elements, positions, config.probe_radius);
69
70 let eps = config.solvent_dielectric;
71 let prefactor = -0.5 * (1.0 - 1.0 / eps);
72
73 let mut e_gb = 0.0;
74 let mut e_coulomb = 0.0;
75
76 for i in 0..n {
77 for j in i..n {
78 let dx = positions[i][0] - positions[j][0];
79 let dy = positions[i][1] - positions[j][1];
80 let dz = positions[i][2] - positions[j][2];
81 let r_ij = (dx * dx + dy * dy + dz * dz).sqrt();
82
83 let f = gb_kernel(r_ij, born.radii[i], born.radii[j]);
84 let factor = if i == j { 1.0 } else { 2.0 };
85
86 e_gb += factor * charges[i] * charges[j] / f;
87
88 if i != j && r_ij > 1e-10 {
89 e_coulomb += factor * charges[i] * charges[j] / r_ij;
90 }
91 }
92 }
93
94 e_gb *= prefactor;
95
96 let klamt_a = 0.571412;
98 let (alpb_factor, e_alpb) = if (eps - 1.0).abs() < 1e-12 {
99 (0.0, 0.0)
100 } else {
101 let x = if e_coulomb.abs() > 1e-15 {
102 e_gb / (prefactor * e_coulomb)
103 } else {
104 1.0
105 };
106 let af = (eps - 1.0) / (eps + klamt_a * x.abs().max(0.01));
107 let e = e_gb * af / ((eps - 1.0) / eps);
108 (af, e)
109 };
110
111 let coulomb_kcal_ang = 332.063;
113 let e_electrostatic = e_alpb * coulomb_kcal_ang;
114
115 let sasa_result =
117 crate::surface::sasa::compute_sasa(elements, positions, Some(config.probe_radius), None);
118 let sasa_total = sasa_result.total_sasa;
119
120 let e_nonpolar = config.surface_tension * sasa_total;
121
122 AlpbResult {
123 electrostatic_energy: e_electrostatic,
124 nonpolar_energy: e_nonpolar,
125 total_energy: e_electrostatic + e_nonpolar,
126 born_radii: born.radii,
127 alpb_factor,
128 }
129}
130
131#[cfg(test)]
132mod tests {
133 use super::*;
134
135 #[test]
136 fn test_alpb_water_in_water() {
137 let elements = [8, 1, 1];
138 let positions = [[0.0, 0.0, 0.0], [0.757, 0.586, 0.0], [-0.757, 0.586, 0.0]];
139 let charges = [-0.834, 0.417, 0.417];
140 let config = AlpbConfig::default();
141 let result = compute_alpb_solvation(&elements, &positions, &charges, &config);
142 assert!(result.total_energy.is_finite());
143 assert!(
144 result.electrostatic_energy < 0.0 || result.electrostatic_energy.abs() < 1.0,
145 "Electrostatic = {}",
146 result.electrostatic_energy
147 );
148 }
149
150 #[test]
151 fn test_alpb_vacuum_dielectric() {
152 let elements = [8, 1, 1];
153 let positions = [[0.0, 0.0, 0.0], [0.757, 0.586, 0.0], [-0.757, 0.586, 0.0]];
154 let charges = [-0.834, 0.417, 0.417];
155 let config = AlpbConfig {
156 solvent_dielectric: 1.0,
157 ..Default::default()
158 };
159 let result = compute_alpb_solvation(&elements, &positions, &charges, &config);
160 assert!(
161 result.electrostatic_energy.abs() < 1e-10,
162 "Vacuum solvation should be 0: {}",
163 result.electrostatic_energy
164 );
165 }
166
167 #[test]
168 fn test_alpb_nonpolar_positive() {
169 let elements = [6, 1, 1, 1, 1];
170 let positions = [
171 [0.0, 0.0, 0.0],
172 [0.63, 0.63, 0.63],
173 [-0.63, -0.63, 0.63],
174 [-0.63, 0.63, -0.63],
175 [0.63, -0.63, -0.63],
176 ];
177 let charges = [0.0, 0.0, 0.0, 0.0, 0.0];
178 let config = AlpbConfig::default();
179 let result = compute_alpb_solvation(&elements, &positions, &charges, &config);
180 assert!(result.nonpolar_energy >= 0.0);
181 }
182
183 #[test]
184 fn test_alpb_factor_bounded() {
185 let elements = [8, 1, 1];
186 let positions = [[0.0, 0.0, 0.0], [0.757, 0.586, 0.0], [-0.757, 0.586, 0.0]];
187 let charges = [-0.834, 0.417, 0.417];
188 let config = AlpbConfig::default();
189 let result = compute_alpb_solvation(&elements, &positions, &charges, &config);
190 assert!(
191 result.alpb_factor >= 0.0 && result.alpb_factor <= 1.0,
192 "ALPB factor should be in [0,1]: {}",
193 result.alpb_factor
194 );
195 }
196}