rust-simulation-tools 0.2.2__tar.gz → 0.2.4__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {rust_simulation_tools-0.2.2 → rust_simulation_tools-0.2.4}/PKG-INFO +1 -1
- {rust_simulation_tools-0.2.2 → rust_simulation_tools-0.2.4}/crates/bindings/src/lib.rs +9 -1
- {rust_simulation_tools-0.2.2 → rust_simulation_tools-0.2.4}/crates/mmpbsa/src/gb_energy.rs +29 -13
- {rust_simulation_tools-0.2.2 → rust_simulation_tools-0.2.4}/crates/mmpbsa/src/mm_energy.rs +6 -4
- {rust_simulation_tools-0.2.2 → rust_simulation_tools-0.2.4}/crates/mmpbsa/src/pb_energy.rs +236 -62
- {rust_simulation_tools-0.2.2 → rust_simulation_tools-0.2.4}/crates/mmpbsa/src/pb_grid.rs +333 -21
- {rust_simulation_tools-0.2.2 → rust_simulation_tools-0.2.4}/crates/mmpbsa/src/pb_solver.rs +65 -3
- {rust_simulation_tools-0.2.2 → rust_simulation_tools-0.2.4}/pyproject.toml +1 -1
- {rust_simulation_tools-0.2.2 → rust_simulation_tools-0.2.4}/Cargo.lock +0 -0
- {rust_simulation_tools-0.2.2 → rust_simulation_tools-0.2.4}/Cargo.toml +0 -0
- {rust_simulation_tools-0.2.2 → rust_simulation_tools-0.2.4}/README.md +0 -0
- {rust_simulation_tools-0.2.2 → rust_simulation_tools-0.2.4}/crates/bindings/Cargo.toml +0 -0
- {rust_simulation_tools-0.2.2 → rust_simulation_tools-0.2.4}/crates/core/Cargo.toml +0 -0
- {rust_simulation_tools-0.2.2 → rust_simulation_tools-0.2.4}/crates/core/benches/core_bench.rs +0 -0
- {rust_simulation_tools-0.2.2 → rust_simulation_tools-0.2.4}/crates/core/src/amber/inpcrd.rs +0 -0
- {rust_simulation_tools-0.2.2 → rust_simulation_tools-0.2.4}/crates/core/src/amber/mod.rs +0 -0
- {rust_simulation_tools-0.2.2 → rust_simulation_tools-0.2.4}/crates/core/src/amber/prmtop.rs +0 -0
- {rust_simulation_tools-0.2.2 → rust_simulation_tools-0.2.4}/crates/core/src/fingerprint.rs +0 -0
- {rust_simulation_tools-0.2.2 → rust_simulation_tools-0.2.4}/crates/core/src/kabsch.rs +0 -0
- {rust_simulation_tools-0.2.2 → rust_simulation_tools-0.2.4}/crates/core/src/lib.rs +0 -0
- {rust_simulation_tools-0.2.2 → rust_simulation_tools-0.2.4}/crates/core/src/sasa.rs +0 -0
- {rust_simulation_tools-0.2.2 → rust_simulation_tools-0.2.4}/crates/core/src/selection/ast.rs +0 -0
- {rust_simulation_tools-0.2.2 → rust_simulation_tools-0.2.4}/crates/core/src/selection/error.rs +0 -0
- {rust_simulation_tools-0.2.2 → rust_simulation_tools-0.2.4}/crates/core/src/selection/eval.rs +0 -0
- {rust_simulation_tools-0.2.2 → rust_simulation_tools-0.2.4}/crates/core/src/selection/keywords.rs +0 -0
- {rust_simulation_tools-0.2.2 → rust_simulation_tools-0.2.4}/crates/core/src/selection/mod.rs +0 -0
- {rust_simulation_tools-0.2.2 → rust_simulation_tools-0.2.4}/crates/core/src/selection/parser.rs +0 -0
- {rust_simulation_tools-0.2.2 → rust_simulation_tools-0.2.4}/crates/core/src/selection/selection.rs +0 -0
- {rust_simulation_tools-0.2.2 → rust_simulation_tools-0.2.4}/crates/core/src/selection/token.rs +0 -0
- {rust_simulation_tools-0.2.2 → rust_simulation_tools-0.2.4}/crates/core/src/trajectory/dcd.rs +0 -0
- {rust_simulation_tools-0.2.2 → rust_simulation_tools-0.2.4}/crates/core/src/trajectory/mod.rs +0 -0
- {rust_simulation_tools-0.2.2 → rust_simulation_tools-0.2.4}/crates/core/src/util.rs +0 -0
- {rust_simulation_tools-0.2.2 → rust_simulation_tools-0.2.4}/crates/core/src/wrapping.rs +0 -0
- {rust_simulation_tools-0.2.2 → rust_simulation_tools-0.2.4}/crates/mmpbsa/Cargo.toml +0 -0
- {rust_simulation_tools-0.2.2 → rust_simulation_tools-0.2.4}/crates/mmpbsa/benches/mmpbsa_bench.rs +0 -0
- {rust_simulation_tools-0.2.2 → rust_simulation_tools-0.2.4}/crates/mmpbsa/examples/bench_pb_real.rs +0 -0
- {rust_simulation_tools-0.2.2 → rust_simulation_tools-0.2.4}/crates/mmpbsa/examples/mmpbsa_binding.rs +0 -0
- {rust_simulation_tools-0.2.2 → rust_simulation_tools-0.2.4}/crates/mmpbsa/src/binding.rs +0 -0
- {rust_simulation_tools-0.2.2 → rust_simulation_tools-0.2.4}/crates/mmpbsa/src/decomposition.rs +0 -0
- {rust_simulation_tools-0.2.2 → rust_simulation_tools-0.2.4}/crates/mmpbsa/src/entropy.rs +0 -0
- {rust_simulation_tools-0.2.2 → rust_simulation_tools-0.2.4}/crates/mmpbsa/src/lib.rs +0 -0
- {rust_simulation_tools-0.2.2 → rust_simulation_tools-0.2.4}/crates/mmpbsa/src/mdcrd.rs +0 -0
- {rust_simulation_tools-0.2.2 → rust_simulation_tools-0.2.4}/crates/mmpbsa/src/sa_energy.rs +0 -0
- {rust_simulation_tools-0.2.2 → rust_simulation_tools-0.2.4}/crates/mmpbsa/src/subsystem.rs +0 -0
|
@@ -1539,7 +1539,7 @@ struct PyPbParams {
|
|
|
1539
1539
|
#[pymethods]
|
|
1540
1540
|
impl PyPbParams {
|
|
1541
1541
|
#[new]
|
|
1542
|
-
#[pyo3(signature = (grid_spacing=None, grid_buffer=None, solute_dielectric=None, solvent_dielectric=None, salt_concentration=None, temperature=None, probe_radius=None, ion_radius=None, tolerance=None, max_iterations=None))]
|
|
1542
|
+
#[pyo3(signature = (grid_spacing=None, grid_buffer=None, solute_dielectric=None, solvent_dielectric=None, salt_concentration=None, temperature=None, probe_radius=None, ion_radius=None, tolerance=None, max_iterations=None, fillratio=None, fscale=None))]
|
|
1543
1543
|
fn new(
|
|
1544
1544
|
grid_spacing: Option<f64>,
|
|
1545
1545
|
grid_buffer: Option<f64>,
|
|
@@ -1551,6 +1551,8 @@ impl PyPbParams {
|
|
|
1551
1551
|
ion_radius: Option<f64>,
|
|
1552
1552
|
tolerance: Option<f64>,
|
|
1553
1553
|
max_iterations: Option<usize>,
|
|
1554
|
+
fillratio: Option<f64>,
|
|
1555
|
+
fscale: Option<usize>,
|
|
1554
1556
|
) -> Self {
|
|
1555
1557
|
let mut params = PbParams::default();
|
|
1556
1558
|
if let Some(v) = grid_spacing {
|
|
@@ -1583,6 +1585,12 @@ impl PyPbParams {
|
|
|
1583
1585
|
if let Some(v) = max_iterations {
|
|
1584
1586
|
params.max_iterations = v;
|
|
1585
1587
|
}
|
|
1588
|
+
if let Some(v) = fillratio {
|
|
1589
|
+
params.fillratio = v;
|
|
1590
|
+
}
|
|
1591
|
+
if let Some(v) = fscale {
|
|
1592
|
+
params.fscale = v;
|
|
1593
|
+
}
|
|
1586
1594
|
PyPbParams { inner: params }
|
|
1587
1595
|
}
|
|
1588
1596
|
|
|
@@ -241,7 +241,7 @@ fn compute_born_radii(
|
|
|
241
241
|
let psi2 = psi_rho * psi_rho;
|
|
242
242
|
let psi3 = psi2 * psi_rho;
|
|
243
243
|
let tanh_val = (alpha * psi_rho - beta * psi2 + gamma * psi3).tanh();
|
|
244
|
-
let inv_r = 1.0 / rho_i - tanh_val /
|
|
244
|
+
let inv_r = 1.0 / rho_i - tanh_val / topology.radii[i];
|
|
245
245
|
born_radii[i] = if inv_r > 0.0 {
|
|
246
246
|
1.0 / inv_r
|
|
247
247
|
} else {
|
|
@@ -267,10 +267,14 @@ fn compute_born_radii(
|
|
|
267
267
|
/// # Returns
|
|
268
268
|
/// `GbEnergy` containing the total energy in kcal/mol and per-atom Born radii.
|
|
269
269
|
///
|
|
270
|
-
/// The GB energy is computed
|
|
271
|
-
/// E_GB = -½
|
|
270
|
+
/// The GB energy is computed using the AMBER convention:
|
|
271
|
+
/// E_GB = -½ Σ_{i,j} q_i·q_j · (1/ε_in - exp(-KSCALE·κ·f_GB)/ε_out) / f_GB
|
|
272
272
|
///
|
|
273
273
|
/// where f_GB = sqrt(r²_ij + R_i·R_j·exp(-r²_ij/(4·R_i·R_j)))
|
|
274
|
+
/// and KSCALE = 0.73 (Srinivasan et al.)
|
|
275
|
+
///
|
|
276
|
+
/// When salt_concentration = 0 (κ = 0), this reduces to:
|
|
277
|
+
/// E_GB = -½(1/ε_in - 1/ε_out) Σ_{i,j} q_i·q_j / f_GB
|
|
274
278
|
///
|
|
275
279
|
/// Charges are taken from `charges_amber` (AMBER internal units where
|
|
276
280
|
/// q_amber = q_real × 18.2223, so q_i·q_j already yields kcal·Å/mol when
|
|
@@ -283,23 +287,34 @@ fn compute_gb_energy_from_radii(
|
|
|
283
287
|
born_radii: &[f64],
|
|
284
288
|
) -> f64 {
|
|
285
289
|
let n = topology.n_atoms;
|
|
286
|
-
let dielectric_factor =
|
|
287
|
-
-0.5 * (1.0 / params.solute_dielectric - 1.0 / params.solvent_dielectric);
|
|
288
290
|
let kappa = compute_kappa(
|
|
289
291
|
params.salt_concentration,
|
|
290
292
|
params.solvent_dielectric,
|
|
291
293
|
params.temperature,
|
|
292
294
|
);
|
|
293
295
|
let cutoff_sq = params.cutoff * params.cutoff;
|
|
296
|
+
let inv_ein = 1.0 / params.solute_dielectric;
|
|
297
|
+
let inv_eout = 1.0 / params.solvent_dielectric;
|
|
298
|
+
|
|
299
|
+
// AMBER Debye-Hückel scaling factor (Srinivasan et al.)
|
|
300
|
+
const KSCALE: f64 = 0.73;
|
|
301
|
+
|
|
302
|
+
// Dielectric factor depends on whether salt screening is active:
|
|
303
|
+
// With salt: E = -0.5 * q_i*q_j * (1/ε_in - exp(-KSCALE*κ*f)/ε_out) / f
|
|
304
|
+
// Without salt: E = -0.5 * q_i*q_j * (1/ε_in - 1/ε_out) / f
|
|
305
|
+
let no_salt = kappa <= 0.0;
|
|
306
|
+
let dielectric_no_salt = -0.5 * (inv_ein - inv_eout);
|
|
294
307
|
|
|
295
308
|
// Self-energy terms (O(N), kept serial)
|
|
296
309
|
let mut energy = 0.0;
|
|
297
310
|
for i in 0..n {
|
|
298
311
|
let qi = topology.charges_amber[i];
|
|
299
|
-
let
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
312
|
+
let self_energy = if no_salt {
|
|
313
|
+
dielectric_no_salt * qi * qi / born_radii[i]
|
|
314
|
+
} else {
|
|
315
|
+
-0.5 * qi * qi * (inv_ein - (-KSCALE * kappa * born_radii[i]).exp() * inv_eout)
|
|
316
|
+
/ born_radii[i]
|
|
317
|
+
};
|
|
303
318
|
energy += self_energy;
|
|
304
319
|
}
|
|
305
320
|
|
|
@@ -324,10 +339,11 @@ fn compute_gb_energy_from_radii(
|
|
|
324
339
|
let ri_rj = born_radii[i] * born_radii[j];
|
|
325
340
|
let f_gb = (r2 + ri_rj * (-r2 / (4.0 * ri_rj)).exp()).sqrt();
|
|
326
341
|
|
|
327
|
-
let
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
342
|
+
let pair_energy = if no_salt {
|
|
343
|
+
dielectric_no_salt * qi * qj / f_gb
|
|
344
|
+
} else {
|
|
345
|
+
-0.5 * qi * qj * (inv_ein - (-KSCALE * kappa * f_gb).exp() * inv_eout) / f_gb
|
|
346
|
+
};
|
|
331
347
|
|
|
332
348
|
sum += 2.0 * pair_energy;
|
|
333
349
|
}
|
|
@@ -201,13 +201,14 @@ pub fn compute_mm_energy_with_nb(
|
|
|
201
201
|
energy.angle += ka * (theta - theta_eq) * (theta - theta_eq);
|
|
202
202
|
}
|
|
203
203
|
|
|
204
|
-
// Dihedral energy: E =
|
|
204
|
+
// Dihedral energy: E = pk * [1 + cos(n*phi - gamma)]
|
|
205
|
+
// AMBER prmtop DIHEDRAL_FORCE_CONSTANT stores pk (already Vn/2, the half-barrier height)
|
|
205
206
|
for &(i, j, k, l, type_idx, _ignore_14) in &topology.dihedrals {
|
|
206
|
-
let
|
|
207
|
+
let pk = topology.dihedral_force_constants[type_idx];
|
|
207
208
|
let n = topology.dihedral_periodicities[type_idx];
|
|
208
209
|
let gamma = topology.dihedral_phases[type_idx];
|
|
209
210
|
let phi = compute_dihedral(&coords[i], &coords[j], &coords[k], &coords[l]);
|
|
210
|
-
energy.dihedral +=
|
|
211
|
+
energy.dihedral += pk * (1.0 + (n * phi - gamma).cos());
|
|
211
212
|
}
|
|
212
213
|
|
|
213
214
|
// Non-bonded interactions (parallelized over outer atom index)
|
|
@@ -380,7 +381,8 @@ mod tests {
|
|
|
380
381
|
top.lj_acoef = Arc::new(vec![0.0]);
|
|
381
382
|
top.lj_bcoef = Arc::new(vec![0.0]);
|
|
382
383
|
|
|
383
|
-
// Trans dihedral (phi=PI): E =
|
|
384
|
+
// Trans dihedral (phi=PI): E = 2 * [1 + cos(2*PI - PI)] = 2 * [1 + cos(PI)] = 0
|
|
385
|
+
// (pk=2.0 is already the half-barrier height in AMBER prmtop convention)
|
|
384
386
|
let coords = [
|
|
385
387
|
[1.0, 1.0, 0.0],
|
|
386
388
|
[0.0, 0.0, 0.0],
|
|
@@ -7,8 +7,12 @@ use rayon;
|
|
|
7
7
|
|
|
8
8
|
use crate::pb_grid::{
|
|
9
9
|
assign_dielectrics, assign_kappa, auto_grid, map_charges, topology_charges, topology_radii,
|
|
10
|
+
PbGrid,
|
|
11
|
+
};
|
|
12
|
+
use crate::pb_solver::{
|
|
13
|
+
compute_elec_energy, interpolated_boundary, solve_lpbe_multigrid, BoundaryCondition,
|
|
14
|
+
PbSolveResult,
|
|
10
15
|
};
|
|
11
|
-
use crate::pb_solver::{compute_elec_energy, solve_lpbe_multigrid, BoundaryCondition};
|
|
12
16
|
use crate::subsystem::{extract_coords, extract_subtopology};
|
|
13
17
|
use rst_core::amber::prmtop::AmberTopology;
|
|
14
18
|
|
|
@@ -35,6 +39,11 @@ pub struct PbParams {
|
|
|
35
39
|
pub tolerance: f64,
|
|
36
40
|
/// Maximum SOR iterations. Default: 10000.
|
|
37
41
|
pub max_iterations: usize,
|
|
42
|
+
/// Ratio of coarse grid extent to molecule extent for focusing. Default: 4.0.
|
|
43
|
+
/// Set to 0.0 to disable focusing (not recommended for large molecules).
|
|
44
|
+
pub fillratio: f64,
|
|
45
|
+
/// Ratio of coarse grid spacing to fine grid spacing. Default: 8.
|
|
46
|
+
pub fscale: usize,
|
|
38
47
|
}
|
|
39
48
|
|
|
40
49
|
impl Default for PbParams {
|
|
@@ -50,6 +59,8 @@ impl Default for PbParams {
|
|
|
50
59
|
ion_radius: 2.0,
|
|
51
60
|
tolerance: 1e-6,
|
|
52
61
|
max_iterations: 10000,
|
|
62
|
+
fillratio: 4.0,
|
|
63
|
+
fscale: 8,
|
|
53
64
|
}
|
|
54
65
|
}
|
|
55
66
|
}
|
|
@@ -74,13 +85,191 @@ pub(crate) fn compute_kappa(salt_conc: f64, solvent_dielectric: f64, temperature
|
|
|
74
85
|
(factor * salt_conc / (solvent_dielectric * kb * temperature)).sqrt()
|
|
75
86
|
}
|
|
76
87
|
|
|
88
|
+
/// Build a coarse grid for focusing.
|
|
89
|
+
///
|
|
90
|
+
/// The coarse grid covers `fillratio` × the molecule extent, with spacing
|
|
91
|
+
/// equal to `fscale` × the fine grid spacing.
|
|
92
|
+
fn build_coarse_grid(
|
|
93
|
+
coords: &[[f64; 3]],
|
|
94
|
+
fine_spacing: f64,
|
|
95
|
+
fillratio: f64,
|
|
96
|
+
fscale: usize,
|
|
97
|
+
) -> PbGrid {
|
|
98
|
+
let mut min = [f64::MAX; 3];
|
|
99
|
+
let mut max = [f64::MIN; 3];
|
|
100
|
+
for c in coords {
|
|
101
|
+
for d in 0..3 {
|
|
102
|
+
if c[d] < min[d] {
|
|
103
|
+
min[d] = c[d];
|
|
104
|
+
}
|
|
105
|
+
if c[d] > max[d] {
|
|
106
|
+
max[d] = c[d];
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
let coarse_spacing = fine_spacing * fscale as f64;
|
|
112
|
+
let mut dims = [0usize; 3];
|
|
113
|
+
let mut origin = [0.0f64; 3];
|
|
114
|
+
for d in 0..3 {
|
|
115
|
+
let mol_extent = max[d] - min[d];
|
|
116
|
+
let grid_extent = mol_extent * fillratio;
|
|
117
|
+
let n = (grid_extent / coarse_spacing).ceil() as usize + 1;
|
|
118
|
+
dims[d] = if n.is_multiple_of(2) { n + 1 } else { n };
|
|
119
|
+
let actual_extent = (dims[d] - 1) as f64 * coarse_spacing;
|
|
120
|
+
let center = 0.5 * (min[d] + max[d]);
|
|
121
|
+
origin[d] = center - 0.5 * actual_extent;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
PbGrid::new(dims, [coarse_spacing; 3], origin)
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/// Perform a single PB solve (solvated or reference) with optional focusing.
|
|
128
|
+
///
|
|
129
|
+
/// If `coarse_grid` is provided, first solves on the coarse grid, then
|
|
130
|
+
/// interpolates boundary conditions for the fine grid.
|
|
131
|
+
fn pb_solve_with_focusing(
|
|
132
|
+
fine_grid: &PbGrid,
|
|
133
|
+
coarse_grid: Option<&PbGrid>,
|
|
134
|
+
coords: &[[f64; 3]],
|
|
135
|
+
charges: &[f64],
|
|
136
|
+
radii: &[f64],
|
|
137
|
+
params: &PbParams,
|
|
138
|
+
eps_in: f64,
|
|
139
|
+
eps_out: f64,
|
|
140
|
+
kappa: f64,
|
|
141
|
+
use_ionic: bool,
|
|
142
|
+
) -> (PbSolveResult, f64) {
|
|
143
|
+
// Determine DH BC parameters for the initial (or only) solve
|
|
144
|
+
let bc_kappa = if use_ionic { kappa } else { 0.0 };
|
|
145
|
+
|
|
146
|
+
match coarse_grid {
|
|
147
|
+
Some(cgrid) => {
|
|
148
|
+
// === Two-level focusing ===
|
|
149
|
+
// 1. Solve on coarse grid with DH/Coulomb BCs
|
|
150
|
+
let coarse_charges = map_charges(cgrid, coords, charges);
|
|
151
|
+
let coarse_diel =
|
|
152
|
+
assign_dielectrics(cgrid, coords, radii, params.probe_radius, eps_in, eps_out);
|
|
153
|
+
let coarse_kappa = if use_ionic {
|
|
154
|
+
assign_kappa(
|
|
155
|
+
cgrid,
|
|
156
|
+
coords,
|
|
157
|
+
radii,
|
|
158
|
+
params.ion_radius,
|
|
159
|
+
kappa,
|
|
160
|
+
params.solvent_dielectric,
|
|
161
|
+
)
|
|
162
|
+
} else {
|
|
163
|
+
vec![0.0; cgrid.len()]
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
let coarse_result = solve_lpbe_multigrid(
|
|
167
|
+
cgrid,
|
|
168
|
+
&coarse_charges,
|
|
169
|
+
&coarse_diel,
|
|
170
|
+
&coarse_kappa,
|
|
171
|
+
BoundaryCondition::DebyeHuckel,
|
|
172
|
+
coords,
|
|
173
|
+
charges,
|
|
174
|
+
bc_kappa,
|
|
175
|
+
eps_out,
|
|
176
|
+
params.tolerance * 10.0, // coarser tolerance for speed
|
|
177
|
+
params.max_iterations,
|
|
178
|
+
);
|
|
179
|
+
|
|
180
|
+
// 2. Interpolate coarse solution as BCs for fine grid
|
|
181
|
+
let fine_bc = interpolated_boundary(fine_grid, cgrid, &coarse_result.potential);
|
|
182
|
+
|
|
183
|
+
// 3. Solve on fine grid with interpolated BCs
|
|
184
|
+
let fine_charges = map_charges(fine_grid, coords, charges);
|
|
185
|
+
let fine_diel = assign_dielectrics(
|
|
186
|
+
fine_grid,
|
|
187
|
+
coords,
|
|
188
|
+
radii,
|
|
189
|
+
params.probe_radius,
|
|
190
|
+
eps_in,
|
|
191
|
+
eps_out,
|
|
192
|
+
);
|
|
193
|
+
let fine_kappa = if use_ionic {
|
|
194
|
+
assign_kappa(
|
|
195
|
+
fine_grid,
|
|
196
|
+
coords,
|
|
197
|
+
radii,
|
|
198
|
+
params.ion_radius,
|
|
199
|
+
kappa,
|
|
200
|
+
params.solvent_dielectric,
|
|
201
|
+
)
|
|
202
|
+
} else {
|
|
203
|
+
vec![0.0; fine_grid.len()]
|
|
204
|
+
};
|
|
205
|
+
|
|
206
|
+
let result = solve_lpbe_multigrid(
|
|
207
|
+
fine_grid,
|
|
208
|
+
&fine_charges,
|
|
209
|
+
&fine_diel,
|
|
210
|
+
&fine_kappa,
|
|
211
|
+
BoundaryCondition::Interpolated(fine_bc),
|
|
212
|
+
coords,
|
|
213
|
+
charges,
|
|
214
|
+
bc_kappa,
|
|
215
|
+
eps_out,
|
|
216
|
+
params.tolerance,
|
|
217
|
+
params.max_iterations,
|
|
218
|
+
);
|
|
219
|
+
let energy = compute_elec_energy(fine_grid, &result.potential, coords, charges);
|
|
220
|
+
(result, energy)
|
|
221
|
+
}
|
|
222
|
+
None => {
|
|
223
|
+
// === Single-level solve ===
|
|
224
|
+
let charge_map = map_charges(fine_grid, coords, charges);
|
|
225
|
+
let diel = assign_dielectrics(
|
|
226
|
+
fine_grid,
|
|
227
|
+
coords,
|
|
228
|
+
radii,
|
|
229
|
+
params.probe_radius,
|
|
230
|
+
eps_in,
|
|
231
|
+
eps_out,
|
|
232
|
+
);
|
|
233
|
+
let kappa_map = if use_ionic {
|
|
234
|
+
assign_kappa(
|
|
235
|
+
fine_grid,
|
|
236
|
+
coords,
|
|
237
|
+
radii,
|
|
238
|
+
params.ion_radius,
|
|
239
|
+
kappa,
|
|
240
|
+
params.solvent_dielectric,
|
|
241
|
+
)
|
|
242
|
+
} else {
|
|
243
|
+
vec![0.0; fine_grid.len()]
|
|
244
|
+
};
|
|
245
|
+
|
|
246
|
+
let result = solve_lpbe_multigrid(
|
|
247
|
+
fine_grid,
|
|
248
|
+
&charge_map,
|
|
249
|
+
&diel,
|
|
250
|
+
&kappa_map,
|
|
251
|
+
BoundaryCondition::DebyeHuckel,
|
|
252
|
+
coords,
|
|
253
|
+
charges,
|
|
254
|
+
bc_kappa,
|
|
255
|
+
eps_out,
|
|
256
|
+
params.tolerance,
|
|
257
|
+
params.max_iterations,
|
|
258
|
+
);
|
|
259
|
+
let energy = compute_elec_energy(fine_grid, &result.potential, coords, charges);
|
|
260
|
+
(result, energy)
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
77
265
|
/// Compute PB polar solvation energy for a molecular system.
|
|
78
266
|
///
|
|
79
|
-
///
|
|
80
|
-
/// 1.
|
|
81
|
-
/// 2.
|
|
267
|
+
/// Uses two-level focusing (when `fillratio > 0`):
|
|
268
|
+
/// 1. Coarse solve on a large grid with DH/Coulomb boundary conditions
|
|
269
|
+
/// 2. Fine solve with boundary conditions interpolated from the coarse solution
|
|
82
270
|
///
|
|
83
|
-
///
|
|
271
|
+
/// Performs paired solves (solvated + reference) and returns
|
|
272
|
+
/// ΔG_PB = E_solvated - E_reference (reaction field energy).
|
|
84
273
|
pub fn compute_pb_energy(
|
|
85
274
|
topology: &AmberTopology,
|
|
86
275
|
coords: &[[f64; 3]],
|
|
@@ -95,77 +284,60 @@ pub fn compute_pb_energy(
|
|
|
95
284
|
params.temperature,
|
|
96
285
|
);
|
|
97
286
|
|
|
98
|
-
// Build grid
|
|
99
|
-
let
|
|
100
|
-
log::info!(
|
|
101
|
-
"PB grid: {}x{}x{} ({} points) for {} atoms",
|
|
102
|
-
grid.dims[0],
|
|
103
|
-
grid.dims[1],
|
|
104
|
-
grid.dims[2],
|
|
105
|
-
grid.len(),
|
|
106
|
-
coords.len()
|
|
107
|
-
);
|
|
287
|
+
// Build fine grid (centered on molecule with buffer)
|
|
288
|
+
let fine_grid = auto_grid(coords, params.grid_spacing, params.grid_buffer);
|
|
108
289
|
|
|
109
|
-
//
|
|
110
|
-
let
|
|
290
|
+
// Build coarse grid for focusing (if enabled)
|
|
291
|
+
let coarse_grid = if params.fillratio > 0.0 && params.fscale > 1 {
|
|
292
|
+
let cg = build_coarse_grid(coords, params.grid_spacing, params.fillratio, params.fscale);
|
|
293
|
+
log::info!(
|
|
294
|
+
"PB focusing: coarse {}x{}x{} (spacing {:.1}), fine {}x{}x{} (spacing {:.1}) for {} atoms",
|
|
295
|
+
cg.dims[0], cg.dims[1], cg.dims[2], cg.spacing[0],
|
|
296
|
+
fine_grid.dims[0], fine_grid.dims[1], fine_grid.dims[2], fine_grid.spacing[0],
|
|
297
|
+
coords.len()
|
|
298
|
+
);
|
|
299
|
+
Some(cg)
|
|
300
|
+
} else {
|
|
301
|
+
log::info!(
|
|
302
|
+
"PB grid: {}x{}x{} ({} points) for {} atoms",
|
|
303
|
+
fine_grid.dims[0],
|
|
304
|
+
fine_grid.dims[1],
|
|
305
|
+
fine_grid.dims[2],
|
|
306
|
+
fine_grid.len(),
|
|
307
|
+
coords.len()
|
|
308
|
+
);
|
|
309
|
+
None
|
|
310
|
+
};
|
|
111
311
|
|
|
112
312
|
// Run solvated and reference solves in parallel
|
|
113
313
|
let ((result_solv, e_solv), (result_ref, e_ref)) = rayon::join(
|
|
114
314
|
|| {
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
315
|
+
pb_solve_with_focusing(
|
|
316
|
+
&fine_grid,
|
|
317
|
+
coarse_grid.as_ref(),
|
|
118
318
|
coords,
|
|
319
|
+
&charges,
|
|
119
320
|
&radii,
|
|
120
|
-
params
|
|
321
|
+
params,
|
|
121
322
|
params.solute_dielectric,
|
|
122
323
|
params.solvent_dielectric,
|
|
123
|
-
);
|
|
124
|
-
let kappa_sq_solv = assign_kappa(&grid, coords, &radii, params.ion_radius, kappa);
|
|
125
|
-
|
|
126
|
-
let result = solve_lpbe_multigrid(
|
|
127
|
-
&grid,
|
|
128
|
-
&charge_map,
|
|
129
|
-
&diel_solv,
|
|
130
|
-
&kappa_sq_solv,
|
|
131
|
-
BoundaryCondition::DebyeHuckel,
|
|
132
|
-
coords,
|
|
133
|
-
&charges,
|
|
134
324
|
kappa,
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
params.max_iterations,
|
|
138
|
-
);
|
|
139
|
-
let energy = compute_elec_energy(&grid, &result.potential, coords, &charges);
|
|
140
|
-
(result, energy)
|
|
325
|
+
true, // use ionic strength
|
|
326
|
+
)
|
|
141
327
|
},
|
|
142
328
|
|| {
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
coords,
|
|
147
|
-
&radii,
|
|
148
|
-
params.probe_radius,
|
|
149
|
-
params.solute_dielectric,
|
|
150
|
-
params.solute_dielectric,
|
|
151
|
-
);
|
|
152
|
-
let kappa_sq_ref = vec![0.0; grid.len()];
|
|
153
|
-
|
|
154
|
-
let result = solve_lpbe_multigrid(
|
|
155
|
-
&grid,
|
|
156
|
-
&charge_map,
|
|
157
|
-
&diel_ref,
|
|
158
|
-
&kappa_sq_ref,
|
|
159
|
-
BoundaryCondition::Zero,
|
|
329
|
+
pb_solve_with_focusing(
|
|
330
|
+
&fine_grid,
|
|
331
|
+
coarse_grid.as_ref(),
|
|
160
332
|
coords,
|
|
161
333
|
&charges,
|
|
162
|
-
|
|
334
|
+
&radii,
|
|
335
|
+
params,
|
|
163
336
|
params.solute_dielectric,
|
|
164
|
-
params.
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
(result, energy)
|
|
337
|
+
params.solute_dielectric, // uniform ε_in for reference
|
|
338
|
+
kappa,
|
|
339
|
+
false, // no ionic strength
|
|
340
|
+
)
|
|
169
341
|
},
|
|
170
342
|
);
|
|
171
343
|
|
|
@@ -186,11 +358,13 @@ pub fn compute_pb_energy(
|
|
|
186
358
|
}
|
|
187
359
|
|
|
188
360
|
log::debug!(
|
|
189
|
-
"PB energies: E_solv = {:.
|
|
361
|
+
"PB energies: E_solv = {:.4} (iters={}, res={:.2e}), E_ref = {:.4} (iters={}, res={:.2e}), delta = {:.4}",
|
|
190
362
|
e_solv,
|
|
191
363
|
result_solv.iterations,
|
|
364
|
+
result_solv.final_residual,
|
|
192
365
|
e_ref,
|
|
193
366
|
result_ref.iterations,
|
|
367
|
+
result_ref.final_residual,
|
|
194
368
|
e_solv - e_ref
|
|
195
369
|
);
|
|
196
370
|
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
use rayon::prelude::*;
|
|
8
8
|
use rst_core::amber::prmtop::AmberTopology;
|
|
9
|
+
use std::collections::VecDeque;
|
|
9
10
|
|
|
10
11
|
/// A regular 3D grid for finite-difference PB calculations.
|
|
11
12
|
#[derive(Debug, Clone)]
|
|
@@ -317,26 +318,48 @@ fn point_inside_molecule(
|
|
|
317
318
|
false
|
|
318
319
|
}
|
|
319
320
|
|
|
320
|
-
///
|
|
321
|
+
/// Compute Solvent-Excluded Surface (SES) inside/outside classification.
|
|
321
322
|
///
|
|
322
|
-
///
|
|
323
|
-
///
|
|
324
|
-
///
|
|
325
|
-
|
|
323
|
+
/// The SES (molecular surface) is the boundary between regions accessible
|
|
324
|
+
/// to a probe sphere of the given radius and regions that are not. This is
|
|
325
|
+
/// tighter than the SAS (Solvent-Accessible Surface) because crevices and
|
|
326
|
+
/// grooves between atoms are classified as interior even though they are
|
|
327
|
+
/// outside the union of inflated spheres.
|
|
328
|
+
///
|
|
329
|
+
/// Algorithm:
|
|
330
|
+
/// 1. Identify valid probe-center positions: grid points outside the SAS
|
|
331
|
+
/// (where a probe sphere wouldn't overlap any atom) connected to the
|
|
332
|
+
/// bulk (grid boundary) via a path of such positions.
|
|
333
|
+
/// 2. A grid point is outside the SES if any valid probe center within
|
|
334
|
+
/// `probe_radius` can reach it; otherwise it is inside.
|
|
335
|
+
fn compute_inside_ses(
|
|
326
336
|
grid: &PbGrid,
|
|
327
337
|
coords: &[[f64; 3]],
|
|
328
338
|
radii: &[f64],
|
|
329
339
|
probe_radius: f64,
|
|
330
|
-
|
|
331
|
-
eps_out: f64,
|
|
332
|
-
) -> DielectricMaps {
|
|
340
|
+
) -> Vec<bool> {
|
|
333
341
|
let nx = grid.dims[0];
|
|
334
342
|
let ny = grid.dims[1];
|
|
335
343
|
let nz = grid.dims[2];
|
|
336
344
|
let n = nx * ny * nz;
|
|
337
345
|
|
|
338
|
-
|
|
339
|
-
|
|
346
|
+
if probe_radius <= 0.0 {
|
|
347
|
+
// No probe: SES = VDW surface
|
|
348
|
+
return (0..n)
|
|
349
|
+
.into_par_iter()
|
|
350
|
+
.map(|idx| {
|
|
351
|
+
let iz = idx / (nx * ny);
|
|
352
|
+
let iy = (idx % (nx * ny)) / nx;
|
|
353
|
+
let ix = idx % nx;
|
|
354
|
+
let pt = grid.point(ix, iy, iz);
|
|
355
|
+
point_inside_molecule(&pt, coords, radii, 0.0)
|
|
356
|
+
})
|
|
357
|
+
.collect();
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
// Step 1: Compute inside_sas (SAS = union of spheres with r + probe).
|
|
361
|
+
// Outside SAS = valid probe-center positions (probe fits without overlap).
|
|
362
|
+
let inside_sas: Vec<bool> = (0..n)
|
|
340
363
|
.into_par_iter()
|
|
341
364
|
.map(|idx| {
|
|
342
365
|
let iz = idx / (nx * ny);
|
|
@@ -347,14 +370,239 @@ pub fn assign_dielectrics(
|
|
|
347
370
|
})
|
|
348
371
|
.collect();
|
|
349
372
|
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
373
|
+
// Step 2: Flood-fill from boundary through outside_sas to find the
|
|
374
|
+
// connected exterior (probe centers reachable from the bulk).
|
|
375
|
+
let mut connected_exterior = vec![false; n];
|
|
376
|
+
let mut queue = VecDeque::new();
|
|
377
|
+
|
|
378
|
+
// Seed: boundary points that are outside SAS
|
|
379
|
+
for iz in 0..nz {
|
|
380
|
+
for iy in 0..ny {
|
|
381
|
+
for ix in 0..nx {
|
|
382
|
+
let is_boundary =
|
|
383
|
+
ix == 0 || ix == nx - 1 || iy == 0 || iy == ny - 1 || iz == 0 || iz == nz - 1;
|
|
384
|
+
if is_boundary {
|
|
385
|
+
let idx = grid.index(ix, iy, iz);
|
|
386
|
+
if !inside_sas[idx] {
|
|
387
|
+
connected_exterior[idx] = true;
|
|
388
|
+
queue.push_back(idx);
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
// BFS through outside_sas
|
|
396
|
+
while let Some(idx) = queue.pop_front() {
|
|
397
|
+
let iz = idx / (nx * ny);
|
|
398
|
+
let iy = (idx % (nx * ny)) / nx;
|
|
399
|
+
let ix = idx % nx;
|
|
400
|
+
|
|
401
|
+
let neighbors: [Option<usize>; 6] = [
|
|
402
|
+
if ix > 0 {
|
|
403
|
+
Some(grid.index(ix - 1, iy, iz))
|
|
404
|
+
} else {
|
|
405
|
+
None
|
|
406
|
+
},
|
|
407
|
+
if ix + 1 < nx {
|
|
408
|
+
Some(grid.index(ix + 1, iy, iz))
|
|
409
|
+
} else {
|
|
410
|
+
None
|
|
411
|
+
},
|
|
412
|
+
if iy > 0 {
|
|
413
|
+
Some(grid.index(ix, iy - 1, iz))
|
|
414
|
+
} else {
|
|
415
|
+
None
|
|
416
|
+
},
|
|
417
|
+
if iy + 1 < ny {
|
|
418
|
+
Some(grid.index(ix, iy + 1, iz))
|
|
419
|
+
} else {
|
|
420
|
+
None
|
|
421
|
+
},
|
|
422
|
+
if iz > 0 {
|
|
423
|
+
Some(grid.index(ix, iy, iz - 1))
|
|
424
|
+
} else {
|
|
425
|
+
None
|
|
426
|
+
},
|
|
427
|
+
if iz + 1 < nz {
|
|
428
|
+
Some(grid.index(ix, iy, iz + 1))
|
|
429
|
+
} else {
|
|
430
|
+
None
|
|
431
|
+
},
|
|
432
|
+
];
|
|
433
|
+
|
|
434
|
+
for ni in neighbors.into_iter().flatten() {
|
|
435
|
+
if !connected_exterior[ni] && !inside_sas[ni] {
|
|
436
|
+
connected_exterior[ni] = true;
|
|
437
|
+
queue.push_back(ni);
|
|
438
|
+
}
|
|
355
439
|
}
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
// Step 3: Dilate connected exterior by probe_radius.
|
|
443
|
+
// A point is outside SES if any connected_exterior point is within probe_radius.
|
|
444
|
+
let probe_sq = probe_radius * probe_radius;
|
|
445
|
+
let rx = (probe_radius / grid.spacing[0]).ceil() as isize;
|
|
446
|
+
let ry = (probe_radius / grid.spacing[1]).ceil() as isize;
|
|
447
|
+
let rz = (probe_radius / grid.spacing[2]).ceil() as isize;
|
|
448
|
+
|
|
449
|
+
(0..n)
|
|
450
|
+
.into_par_iter()
|
|
451
|
+
.map(|idx| {
|
|
452
|
+
let iz = (idx / (nx * ny)) as isize;
|
|
453
|
+
let iy = ((idx % (nx * ny)) / nx) as isize;
|
|
454
|
+
let ix = (idx % nx) as isize;
|
|
455
|
+
let pt = grid.point(ix as usize, iy as usize, iz as usize);
|
|
456
|
+
|
|
457
|
+
for dz in -rz..=rz {
|
|
458
|
+
let jz = iz + dz;
|
|
459
|
+
if jz < 0 || jz >= nz as isize {
|
|
460
|
+
continue;
|
|
461
|
+
}
|
|
462
|
+
for dy in -ry..=ry {
|
|
463
|
+
let jy = iy + dy;
|
|
464
|
+
if jy < 0 || jy >= ny as isize {
|
|
465
|
+
continue;
|
|
466
|
+
}
|
|
467
|
+
for dx in -rx..=rx {
|
|
468
|
+
let jx = ix + dx;
|
|
469
|
+
if jx < 0 || jx >= nx as isize {
|
|
470
|
+
continue;
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
let jidx = grid.index(jx as usize, jy as usize, jz as usize);
|
|
474
|
+
if connected_exterior[jidx] {
|
|
475
|
+
let neighbor_pt = grid.point(jx as usize, jy as usize, jz as usize);
|
|
476
|
+
let ddx = pt[0] - neighbor_pt[0];
|
|
477
|
+
let ddy = pt[1] - neighbor_pt[1];
|
|
478
|
+
let ddz = pt[2] - neighbor_pt[2];
|
|
479
|
+
if ddx * ddx + ddy * ddy + ddz * ddz <= probe_sq {
|
|
480
|
+
return false; // Probe can reach -> outside SES
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
true // No probe can reach -> inside SES
|
|
487
|
+
})
|
|
488
|
+
.collect()
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
/// Compute the fraction of an axis-aligned edge segment that lies inside the
|
|
492
|
+
/// molecular surface (union of atom spheres inflated by probe radius).
|
|
493
|
+
///
|
|
494
|
+
/// The edge runs from `lo` to `hi` along `axis` (0=x, 1=y, 2=z), at fixed
|
|
495
|
+
/// perpendicular coordinates `perp1` and `perp2`.
|
|
496
|
+
///
|
|
497
|
+
/// Returns a value in [0, 1].
|
|
498
|
+
fn fraction_inside_along_axis(
|
|
499
|
+
lo: f64,
|
|
500
|
+
hi: f64,
|
|
501
|
+
perp1: f64,
|
|
502
|
+
perp2: f64,
|
|
503
|
+
axis: usize,
|
|
504
|
+
coords: &[[f64; 3]],
|
|
505
|
+
radii: &[f64],
|
|
506
|
+
probe_radius: f64,
|
|
507
|
+
) -> f64 {
|
|
508
|
+
let edge_len = hi - lo;
|
|
509
|
+
if edge_len <= 0.0 {
|
|
510
|
+
return 0.0;
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
// Perpendicular axis indices
|
|
514
|
+
let (a1, a2) = match axis {
|
|
515
|
+
0 => (1, 2),
|
|
516
|
+
1 => (0, 2),
|
|
517
|
+
_ => (0, 1),
|
|
356
518
|
};
|
|
357
519
|
|
|
520
|
+
// Collect intervals along the edge that lie inside each atom's sphere
|
|
521
|
+
let mut intervals: Vec<(f64, f64)> = Vec::new();
|
|
522
|
+
|
|
523
|
+
for (c, &r) in coords.iter().zip(radii.iter()) {
|
|
524
|
+
let big_r = r + probe_radius;
|
|
525
|
+
let d1 = perp1 - c[a1];
|
|
526
|
+
let d2 = perp2 - c[a2];
|
|
527
|
+
let rho_sq = big_r * big_r - d1 * d1 - d2 * d2;
|
|
528
|
+
if rho_sq <= 0.0 {
|
|
529
|
+
continue;
|
|
530
|
+
}
|
|
531
|
+
let rho = rho_sq.sqrt();
|
|
532
|
+
let ilo = (c[axis] - rho).max(lo);
|
|
533
|
+
let ihi = (c[axis] + rho).min(hi);
|
|
534
|
+
if ilo < ihi {
|
|
535
|
+
intervals.push((ilo, ihi));
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
if intervals.is_empty() {
|
|
540
|
+
return 0.0;
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
// Sort by interval start
|
|
544
|
+
intervals.sort_unstable_by(|a, b| a.0.partial_cmp(&b.0).unwrap());
|
|
545
|
+
|
|
546
|
+
// Merge overlapping intervals and sum total inside length
|
|
547
|
+
let mut total = 0.0;
|
|
548
|
+
let mut cur_lo = intervals[0].0;
|
|
549
|
+
let mut cur_hi = intervals[0].1;
|
|
550
|
+
for &(ilo, ihi) in &intervals[1..] {
|
|
551
|
+
if ilo <= cur_hi {
|
|
552
|
+
cur_hi = cur_hi.max(ihi);
|
|
553
|
+
} else {
|
|
554
|
+
total += cur_hi - cur_lo;
|
|
555
|
+
cur_lo = ilo;
|
|
556
|
+
cur_hi = ihi;
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
total += cur_hi - cur_lo;
|
|
560
|
+
|
|
561
|
+
(total / edge_len).clamp(0.0, 1.0)
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
/// Weighted harmonic mean of dielectrics.
|
|
565
|
+
///
|
|
566
|
+
/// `1/ε = f/ε_in + (1-f)/ε_out` where `f` is the fraction inside.
|
|
567
|
+
/// This is the standard approach used by AMBER and other PB solvers
|
|
568
|
+
/// for face-centered dielectrics at boundary edges.
|
|
569
|
+
#[inline]
|
|
570
|
+
fn weighted_harmonic_eps(f: f64, eps_in: f64, eps_out: f64) -> f64 {
|
|
571
|
+
if f <= 0.0 {
|
|
572
|
+
return eps_out;
|
|
573
|
+
}
|
|
574
|
+
if f >= 1.0 {
|
|
575
|
+
return eps_in;
|
|
576
|
+
}
|
|
577
|
+
1.0 / (f / eps_in + (1.0 - f) / eps_out)
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
/// Assign face-centered dielectric values based on the Solvent-Excluded Surface.
|
|
581
|
+
///
|
|
582
|
+
/// Uses the SES (molecular surface) to classify grid points as inside or
|
|
583
|
+
/// outside, then assigns edge dielectrics:
|
|
584
|
+
/// - Both endpoints inside → ε_in
|
|
585
|
+
/// - Both endpoints outside → ε_out
|
|
586
|
+
/// - Mixed → weighted harmonic mean using the fraction of the edge inside
|
|
587
|
+
/// the molecular surface (computed analytically from sphere geometry).
|
|
588
|
+
pub fn assign_dielectrics(
|
|
589
|
+
grid: &PbGrid,
|
|
590
|
+
coords: &[[f64; 3]],
|
|
591
|
+
radii: &[f64],
|
|
592
|
+
probe_radius: f64,
|
|
593
|
+
eps_in: f64,
|
|
594
|
+
eps_out: f64,
|
|
595
|
+
) -> DielectricMaps {
|
|
596
|
+
let nx = grid.dims[0];
|
|
597
|
+
let ny = grid.dims[1];
|
|
598
|
+
let nz = grid.dims[2];
|
|
599
|
+
let n = nx * ny * nz;
|
|
600
|
+
|
|
601
|
+
// Use the Solvent-Excluded Surface for inside/outside classification.
|
|
602
|
+
// This handles re-entrant regions correctly, unlike the simpler SAS
|
|
603
|
+
// (union of inflated spheres) which over-estimates the cavity size.
|
|
604
|
+
let inside = compute_inside_ses(grid, coords, radii, probe_radius);
|
|
605
|
+
|
|
358
606
|
let mut eps_x = vec![eps_out; n];
|
|
359
607
|
let mut eps_y = vec![eps_out; n];
|
|
360
608
|
let mut eps_z = vec![eps_out; n];
|
|
@@ -364,14 +612,70 @@ pub fn assign_dielectrics(
|
|
|
364
612
|
for ix in 0..nx {
|
|
365
613
|
let idx = grid.index(ix, iy, iz);
|
|
366
614
|
let a = inside[idx];
|
|
615
|
+
|
|
616
|
+
// X-edge: (ix,iy,iz) → (ix+1,iy,iz)
|
|
367
617
|
if ix + 1 < nx {
|
|
368
|
-
|
|
618
|
+
let b = inside[grid.index(ix + 1, iy, iz)];
|
|
619
|
+
if a == b {
|
|
620
|
+
eps_x[idx] = if a { eps_in } else { eps_out };
|
|
621
|
+
} else {
|
|
622
|
+
// Use VDW sphere geometry (probe=0) for the fraction
|
|
623
|
+
// since the SES contact surface is at the VDW boundary.
|
|
624
|
+
let pt = grid.point(ix, iy, iz);
|
|
625
|
+
let f = fraction_inside_along_axis(
|
|
626
|
+
pt[0],
|
|
627
|
+
pt[0] + grid.spacing[0],
|
|
628
|
+
pt[1],
|
|
629
|
+
pt[2],
|
|
630
|
+
0,
|
|
631
|
+
coords,
|
|
632
|
+
radii,
|
|
633
|
+
0.0, // VDW radii, no probe inflation
|
|
634
|
+
);
|
|
635
|
+
eps_x[idx] = weighted_harmonic_eps(f, eps_in, eps_out);
|
|
636
|
+
}
|
|
369
637
|
}
|
|
638
|
+
|
|
639
|
+
// Y-edge: (ix,iy,iz) → (ix,iy+1,iz)
|
|
370
640
|
if iy + 1 < ny {
|
|
371
|
-
|
|
641
|
+
let b = inside[grid.index(ix, iy + 1, iz)];
|
|
642
|
+
if a == b {
|
|
643
|
+
eps_y[idx] = if a { eps_in } else { eps_out };
|
|
644
|
+
} else {
|
|
645
|
+
let pt = grid.point(ix, iy, iz);
|
|
646
|
+
let f = fraction_inside_along_axis(
|
|
647
|
+
pt[1],
|
|
648
|
+
pt[1] + grid.spacing[1],
|
|
649
|
+
pt[0],
|
|
650
|
+
pt[2],
|
|
651
|
+
1,
|
|
652
|
+
coords,
|
|
653
|
+
radii,
|
|
654
|
+
0.0,
|
|
655
|
+
);
|
|
656
|
+
eps_y[idx] = weighted_harmonic_eps(f, eps_in, eps_out);
|
|
657
|
+
}
|
|
372
658
|
}
|
|
659
|
+
|
|
660
|
+
// Z-edge: (ix,iy,iz) → (ix,iy,iz+1)
|
|
373
661
|
if iz + 1 < nz {
|
|
374
|
-
|
|
662
|
+
let b = inside[grid.index(ix, iy, iz + 1)];
|
|
663
|
+
if a == b {
|
|
664
|
+
eps_z[idx] = if a { eps_in } else { eps_out };
|
|
665
|
+
} else {
|
|
666
|
+
let pt = grid.point(ix, iy, iz);
|
|
667
|
+
let f = fraction_inside_along_axis(
|
|
668
|
+
pt[2],
|
|
669
|
+
pt[2] + grid.spacing[2],
|
|
670
|
+
pt[0],
|
|
671
|
+
pt[1],
|
|
672
|
+
2,
|
|
673
|
+
coords,
|
|
674
|
+
radii,
|
|
675
|
+
0.0,
|
|
676
|
+
);
|
|
677
|
+
eps_z[idx] = weighted_harmonic_eps(f, eps_in, eps_out);
|
|
678
|
+
}
|
|
375
679
|
}
|
|
376
680
|
}
|
|
377
681
|
}
|
|
@@ -385,7 +689,13 @@ pub fn assign_dielectrics(
|
|
|
385
689
|
}
|
|
386
690
|
}
|
|
387
691
|
|
|
388
|
-
/// Assign
|
|
692
|
+
/// Assign κ̄² map: zero inside ion-exclusion surface, ε_s·κ² outside.
|
|
693
|
+
///
|
|
694
|
+
/// The linearized PBE is: ∇·[ε∇φ] - κ̄²φ = -4π·ec·ρ
|
|
695
|
+
/// where κ̄² = ε_s · κ² (the Debye-Hückel κ² multiplied by the solvent
|
|
696
|
+
/// dielectric). The factor of ε_s arises because the mobile ion charge
|
|
697
|
+
/// density is -2c₀·φ/(kBT) (no ε_s), while κ² = 8π·ec·c₀·N_A/(ε_s·kBT)
|
|
698
|
+
/// already has 1/ε_s from the Debye-Hückel derivation.
|
|
389
699
|
///
|
|
390
700
|
/// The ion-exclusion surface is the molecular surface inflated by the ion radius.
|
|
391
701
|
pub fn assign_kappa(
|
|
@@ -394,12 +704,14 @@ pub fn assign_kappa(
|
|
|
394
704
|
radii: &[f64],
|
|
395
705
|
ion_radius: f64,
|
|
396
706
|
kappa_bulk: f64,
|
|
707
|
+
solvent_dielectric: f64,
|
|
397
708
|
) -> Vec<f64> {
|
|
398
709
|
let nx = grid.dims[0];
|
|
399
710
|
let ny = grid.dims[1];
|
|
400
711
|
let nz = grid.dims[2];
|
|
401
712
|
let n = nx * ny * nz;
|
|
402
|
-
|
|
713
|
+
// κ̄² = ε_s · κ² is the coefficient in the linearized PBE
|
|
714
|
+
let kappa_bar_sq = solvent_dielectric * kappa_bulk * kappa_bulk;
|
|
403
715
|
|
|
404
716
|
(0..n)
|
|
405
717
|
.into_par_iter()
|
|
@@ -412,7 +724,7 @@ pub fn assign_kappa(
|
|
|
412
724
|
if point_inside_molecule(&pt, coords, radii, ion_radius) {
|
|
413
725
|
0.0
|
|
414
726
|
} else {
|
|
415
|
-
|
|
727
|
+
kappa_bar_sq
|
|
416
728
|
}
|
|
417
729
|
})
|
|
418
730
|
.collect()
|
|
@@ -10,12 +10,15 @@ use crate::pb_grid::{DielectricMaps, PbGrid};
|
|
|
10
10
|
use rayon::prelude::*;
|
|
11
11
|
|
|
12
12
|
/// Boundary condition for the PB solve.
|
|
13
|
-
#[derive(Debug, Clone
|
|
13
|
+
#[derive(Debug, Clone)]
|
|
14
14
|
pub enum BoundaryCondition {
|
|
15
15
|
/// phi = 0 on all boundaries.
|
|
16
16
|
Zero,
|
|
17
17
|
/// Debye-Huckel single-sphere approximation on boundaries.
|
|
18
18
|
DebyeHuckel,
|
|
19
|
+
/// Interpolated from a coarse grid solution (for focusing).
|
|
20
|
+
/// Contains the precomputed boundary values for each boundary grid point.
|
|
21
|
+
Interpolated(Vec<f64>),
|
|
19
22
|
}
|
|
20
23
|
|
|
21
24
|
/// Set Debye-Huckel boundary conditions on the grid.
|
|
@@ -65,6 +68,59 @@ fn set_dh_boundary(
|
|
|
65
68
|
}
|
|
66
69
|
}
|
|
67
70
|
|
|
71
|
+
/// Compute interpolated boundary conditions from a coarse grid solution.
|
|
72
|
+
///
|
|
73
|
+
/// For each boundary point of `fine_grid`, looks up the corresponding
|
|
74
|
+
/// position in the coarse solution via trilinear interpolation.
|
|
75
|
+
pub fn interpolated_boundary(
|
|
76
|
+
fine_grid: &PbGrid,
|
|
77
|
+
coarse_grid: &PbGrid,
|
|
78
|
+
coarse_potential: &[f64],
|
|
79
|
+
) -> Vec<f64> {
|
|
80
|
+
let nx = fine_grid.dims[0];
|
|
81
|
+
let ny = fine_grid.dims[1];
|
|
82
|
+
let nz = fine_grid.dims[2];
|
|
83
|
+
let n = nx * ny * nz;
|
|
84
|
+
let mut boundary = vec![0.0f64; n];
|
|
85
|
+
|
|
86
|
+
for iz in 0..nz {
|
|
87
|
+
for iy in 0..ny {
|
|
88
|
+
for ix in 0..nx {
|
|
89
|
+
let is_boundary =
|
|
90
|
+
ix == 0 || ix == nx - 1 || iy == 0 || iy == ny - 1 || iz == 0 || iz == nz - 1;
|
|
91
|
+
if !is_boundary {
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
let pt = fine_grid.point(ix, iy, iz);
|
|
95
|
+
let idx = fine_grid.index(ix, iy, iz);
|
|
96
|
+
boundary[idx] = coarse_grid.interpolate_with_data(&pt, coarse_potential);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
boundary
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/// Set boundary conditions from precomputed interpolated values.
|
|
104
|
+
fn set_interpolated_boundary(potential: &mut [f64], grid: &PbGrid, boundary: &[f64]) {
|
|
105
|
+
let nx = grid.dims[0];
|
|
106
|
+
let ny = grid.dims[1];
|
|
107
|
+
let nz = grid.dims[2];
|
|
108
|
+
|
|
109
|
+
for iz in 0..nz {
|
|
110
|
+
for iy in 0..ny {
|
|
111
|
+
for ix in 0..nx {
|
|
112
|
+
let is_boundary =
|
|
113
|
+
ix == 0 || ix == nx - 1 || iy == 0 || iy == ny - 1 || iz == 0 || iz == nz - 1;
|
|
114
|
+
if !is_boundary {
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
let idx = grid.index(ix, iy, iz);
|
|
118
|
+
potential[idx] = boundary[idx];
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
68
124
|
/// Estimate the optimal SOR relaxation parameter from grid dimensions.
|
|
69
125
|
fn estimate_omega(dims: &[usize; 3]) -> f64 {
|
|
70
126
|
// Spectral radius of Jacobi iteration for 3D Laplacian
|
|
@@ -264,11 +320,14 @@ pub fn solve_lpbe(
|
|
|
264
320
|
let mut potential = vec![0.0f64; n];
|
|
265
321
|
|
|
266
322
|
// Set boundary conditions
|
|
267
|
-
match boundary {
|
|
323
|
+
match &boundary {
|
|
268
324
|
BoundaryCondition::Zero => {}
|
|
269
325
|
BoundaryCondition::DebyeHuckel => {
|
|
270
326
|
set_dh_boundary(&mut potential, grid, coords, charges, kappa_bulk, eps_out);
|
|
271
327
|
}
|
|
328
|
+
BoundaryCondition::Interpolated(bvals) => {
|
|
329
|
+
set_interpolated_boundary(&mut potential, grid, bvals);
|
|
330
|
+
}
|
|
272
331
|
}
|
|
273
332
|
|
|
274
333
|
let omega = estimate_omega(&grid.dims);
|
|
@@ -703,11 +762,14 @@ pub fn solve_lpbe_multigrid(
|
|
|
703
762
|
let mut potential = vec![0.0f64; n];
|
|
704
763
|
|
|
705
764
|
// Set boundary conditions
|
|
706
|
-
match boundary {
|
|
765
|
+
match &boundary {
|
|
707
766
|
BoundaryCondition::Zero => {}
|
|
708
767
|
BoundaryCondition::DebyeHuckel => {
|
|
709
768
|
set_dh_boundary(&mut potential, grid, coords, charges, kappa_bulk, eps_out);
|
|
710
769
|
}
|
|
770
|
+
BoundaryCondition::Interpolated(bvals) => {
|
|
771
|
+
set_interpolated_boundary(&mut potential, grid, bvals);
|
|
772
|
+
}
|
|
711
773
|
}
|
|
712
774
|
|
|
713
775
|
let min_dim = grid.dims.iter().copied().min().unwrap_or(0);
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{rust_simulation_tools-0.2.2 → rust_simulation_tools-0.2.4}/crates/core/benches/core_bench.rs
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{rust_simulation_tools-0.2.2 → rust_simulation_tools-0.2.4}/crates/core/src/selection/ast.rs
RENAMED
|
File without changes
|
{rust_simulation_tools-0.2.2 → rust_simulation_tools-0.2.4}/crates/core/src/selection/error.rs
RENAMED
|
File without changes
|
{rust_simulation_tools-0.2.2 → rust_simulation_tools-0.2.4}/crates/core/src/selection/eval.rs
RENAMED
|
File without changes
|
{rust_simulation_tools-0.2.2 → rust_simulation_tools-0.2.4}/crates/core/src/selection/keywords.rs
RENAMED
|
File without changes
|
{rust_simulation_tools-0.2.2 → rust_simulation_tools-0.2.4}/crates/core/src/selection/mod.rs
RENAMED
|
File without changes
|
{rust_simulation_tools-0.2.2 → rust_simulation_tools-0.2.4}/crates/core/src/selection/parser.rs
RENAMED
|
File without changes
|
{rust_simulation_tools-0.2.2 → rust_simulation_tools-0.2.4}/crates/core/src/selection/selection.rs
RENAMED
|
File without changes
|
{rust_simulation_tools-0.2.2 → rust_simulation_tools-0.2.4}/crates/core/src/selection/token.rs
RENAMED
|
File without changes
|
{rust_simulation_tools-0.2.2 → rust_simulation_tools-0.2.4}/crates/core/src/trajectory/dcd.rs
RENAMED
|
File without changes
|
{rust_simulation_tools-0.2.2 → rust_simulation_tools-0.2.4}/crates/core/src/trajectory/mod.rs
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{rust_simulation_tools-0.2.2 → rust_simulation_tools-0.2.4}/crates/mmpbsa/benches/mmpbsa_bench.rs
RENAMED
|
File without changes
|
{rust_simulation_tools-0.2.2 → rust_simulation_tools-0.2.4}/crates/mmpbsa/examples/bench_pb_real.rs
RENAMED
|
File without changes
|
{rust_simulation_tools-0.2.2 → rust_simulation_tools-0.2.4}/crates/mmpbsa/examples/mmpbsa_binding.rs
RENAMED
|
File without changes
|
|
File without changes
|
{rust_simulation_tools-0.2.2 → rust_simulation_tools-0.2.4}/crates/mmpbsa/src/decomposition.rs
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|