1#![allow(clippy::excessive_precision)]
2
3use std::{fs, io, path::Path, time::Instant};
11
12use bincode::{Decode, Encode};
13use bio_files::{gromacs, gromacs::gro::Gro};
14use lin_alg::f32::{Quaternion, Vec3};
15
16use crate::{
17 AtomDynamics, ComputationDevice, MdState, Solvent,
18 barostat::SimBox,
19 partial_charge_inference::{files::load_from_bytes_bincode, save},
20 sa_surface,
21 solvent::WaterMolOpc,
22};
23const WATER_DENSITY: f32 = 0.997;
26
27const MASS_WATER: f32 = 18.015_28;
31
32const N_A: f32 = 6.022_140_76e23;
34
35const WATER_MOLS_PER_VOL: f32 = WATER_DENSITY * N_A / (MASS_WATER * 1.0e24);
38
39const MIN_NONWATER_DIST: f32 = 1.7;
42const MIN_NONWATER_DIST_SQ: f32 = MIN_NONWATER_DIST * MIN_NONWATER_DIST;
43
44const MIN_WATER_O_O_DIST: f32 = 1.7;
46pub(in crate::solvent) const MIN_WATER_O_O_DIST_SQ: f32 = MIN_WATER_O_O_DIST * MIN_WATER_O_O_DIST;
47
48const PBC_MIN_WATER_O_O_DIST: f32 = 2.8;
59const PBC_MIN_WATER_O_O_DIST_SQ: f32 = PBC_MIN_WATER_O_O_DIST * PBC_MIN_WATER_O_O_DIST;
60
61const NUM_EQUILIBRATION_STEPS_WATER: usize = 200;
65const NUM_EQUILIBRATION_STEPS_OTHER_SOLVENT: usize = 600;
66
67const DT_EQUILIBRATION: f32 = 0.0005;
70
71pub const WATER_TEMPLATE_60A: &[u8] =
73 include_bytes!("../../param_data/water_60A.water_init_template");
74
75pub const WATER_TEMPLATE_TIP4: &str = include_str!("../../param_data/tip4p.gro");
77pub const OCTANOL_WATER_TEMPLATE: &str =
79 include_str!("../../param_data/octanol_water_saturated.gro");
80
81#[derive(Clone, Debug, PartialEq, Default, Decode, Encode)]
84pub enum SolventTemplateType {
85 Water60A,
86 #[default]
89 Tip4Gromacs,
90 OctanolWithWater,
93 Custom(WaterInitTemplate),
94}
95
96impl SolventTemplateType {
97 pub fn get_template(&self) -> io::Result<WaterInitTemplate> {
98 match self {
99 Self::Water60A => load_from_bytes_bincode(WATER_TEMPLATE_60A),
100 Self::Tip4Gromacs => WaterInitTemplate::from_gro(WATER_TEMPLATE_TIP4),
101 Self::OctanolWithWater => Ok(Default::default()),
103 Self::Custom(t) => Ok(t.clone()),
104 }
105 }
106}
107
108#[derive(Clone, Debug, PartialEq, Default, Encode, Decode)]
122pub struct WaterInitTemplate {
123 o_posits: Vec<Vec3>,
125 h0_posits: Vec<Vec3>,
126 h1_posits: Vec<Vec3>,
127 o_velocities: Vec<Vec3>,
128 h0_velocities: Vec<Vec3>,
129 h1_velocities: Vec<Vec3>,
130 cell: SimBox,
132}
133
134impl WaterInitTemplate {
135 pub fn load(path: &Path) -> io::Result<Self> {
138 let bytes = fs::read(path)?;
139 load_from_bytes_bincode(&bytes)
140 }
141
142 pub fn from_bytes(bytes: &[u8]) -> io::Result<Self> {
143 load_from_bytes_bincode(bytes)
144 }
145
146 pub fn from_parts(
147 o_posits: Vec<Vec3>,
148 h0_posits: Vec<Vec3>,
149 h1_posits: Vec<Vec3>,
150 o_velocities: Vec<Vec3>,
151 h0_velocities: Vec<Vec3>,
152 h1_velocities: Vec<Vec3>,
153 cell: SimBox,
154 ) -> io::Result<Self> {
155 let n = o_posits.len();
156 let lengths = [
157 h0_posits.len(),
158 h1_posits.len(),
159 o_velocities.len(),
160 h0_velocities.len(),
161 h1_velocities.len(),
162 ];
163
164 if lengths.iter().any(|len| *len != n) {
165 return Err(io::Error::new(
166 io::ErrorKind::InvalidInput,
167 "WaterInitTemplate component lengths must match.",
168 ));
169 }
170
171 Ok(Self {
172 o_posits,
173 h0_posits,
174 h1_posits,
175 o_velocities,
176 h0_velocities,
177 h1_velocities,
178 cell,
179 })
180 }
181
182 pub fn len(&self) -> usize {
183 self.o_posits.len()
184 }
185
186 pub fn is_empty(&self) -> bool {
187 self.o_posits.is_empty()
188 }
189
190 pub fn from_gro(gro_text: &str) -> io::Result<Self> {
191 const NM_TO_ANGSTROM: f32 = 10.;
192
193 let gro = Gro::new(gro_text)?;
194
195 let mut o_posits = Vec::new();
196 let mut h0_posits = Vec::new();
197 let mut h1_posits = Vec::new();
198
199 let mut o_velocities = Vec::new();
200 let mut h0_velocities = Vec::new();
201 let mut h1_velocities = Vec::new();
202
203 for atom in gro.atoms {
204 match atom.atom_type.as_ref() {
205 "OW" => {
206 let p: Vec3 = atom.posit.into();
207 o_posits.push(p * NM_TO_ANGSTROM);
208 let Some(vel) = &atom.velocity else {
209 return Err(io::Error::other("Missing velocity on tip4 water template"));
210 };
211 let v: Vec3 = (*vel).into();
212 o_velocities.push(v * NM_TO_ANGSTROM.into());
213 }
214 "HW1" => {
215 let p: Vec3 = atom.posit.into();
216 h0_posits.push(p * NM_TO_ANGSTROM);
217 let Some(vel) = &atom.velocity else {
218 return Err(io::Error::other("Missing velocity on tip4 water template"));
219 };
220 let v: Vec3 = (*vel).into();
221 h0_velocities.push(v * NM_TO_ANGSTROM);
222 }
223 "HW2" => {
224 let p: Vec3 = atom.posit.into();
225 h1_posits.push(p * NM_TO_ANGSTROM);
226 let Some(vel) = &atom.velocity else {
227 return Err(io::Error::other("Missing velocity on tip4 water template"));
228 };
229 let v: Vec3 = (*vel).into();
230 h1_velocities.push(v * NM_TO_ANGSTROM);
231 }
232 _ => (),
233 }
234 }
235
236 Ok(Self {
237 o_posits,
238 h0_posits,
239 h1_posits,
240 o_velocities,
241 h0_velocities,
242 h1_velocities,
243 cell: SimBox::new(
244 (-gro.box_vec * NM_TO_ANGSTROM as f64 / 2.).into(),
245 (gro.box_vec * NM_TO_ANGSTROM as f64 / 2.).into(),
246 ), })
248 }
249
250 pub fn create_and_save(water: &[WaterMolOpc], cell: SimBox, path: &Path) -> io::Result<()> {
253 let n = water.len();
254
255 let mut o_posits = Vec::with_capacity(n);
256 let mut h0_posits = Vec::with_capacity(n);
257 let mut h1_posits = Vec::with_capacity(n);
258
259 let mut o_velocities = Vec::with_capacity(n);
260 let mut h0_velocities = Vec::with_capacity(n);
261 let mut h1_velocities = Vec::with_capacity(n);
262
263 let water = {
268 let ctr = cell.center();
269
270 let mut w = water.to_vec();
271 w.sort_by(|a, b| {
272 let da = (a.o.posit - ctr).magnitude_squared();
273 let db = (b.o.posit - ctr).magnitude_squared();
274 da.total_cmp(&db)
275 });
276
277 w
278 };
279
280 for mol in water {
281 o_posits.push(mol.o.posit);
282 h0_posits.push(mol.h0.posit);
283 h1_posits.push(mol.h1.posit);
284
285 o_velocities.push(mol.o.vel);
286 h0_velocities.push(mol.h0.vel);
287 h1_velocities.push(mol.h1.vel);
288
289 }
292
293 let result = Self {
294 o_posits,
295 h0_posits,
296 h1_posits,
297 o_velocities,
298 h0_velocities,
299 h1_velocities,
300 cell,
301 };
302
303 save(path, &result)
304 }
305
306 pub fn from_water_mols(water: &[WaterMolOpc], cell: SimBox) -> io::Result<Self> {
307 let n = water.len();
308
309 let mut o_posits = Vec::with_capacity(n);
310 let mut h0_posits = Vec::with_capacity(n);
311 let mut h1_posits = Vec::with_capacity(n);
312
313 let mut o_velocities = Vec::with_capacity(n);
314 let mut h0_velocities = Vec::with_capacity(n);
315 let mut h1_velocities = Vec::with_capacity(n);
316
317 for mol in water {
318 o_posits.push(mol.o.posit);
319 h0_posits.push(mol.h0.posit);
320 h1_posits.push(mol.h1.posit);
321
322 o_velocities.push(mol.o.vel);
323 h0_velocities.push(mol.h0.vel);
324 h1_velocities.push(mol.h1.vel);
325 }
326
327 Self::from_parts(
328 o_posits,
329 h0_posits,
330 h1_posits,
331 o_velocities,
332 h0_velocities,
333 h1_velocities,
334 cell,
335 )
336 }
337
338 pub fn to_gromacs(&self) -> gromacs::solvate::WaterInitTemplate {
342 gromacs::solvate::WaterInitTemplate {
343 o_posits: self.o_posits.clone(),
344 h0_posits: self.h0_posits.clone(),
345 h1_posits: self.h1_posits.clone(),
346 o_velocities: self.o_velocities.clone(),
347 h0_velocities: self.h0_velocities.clone(),
348 h1_velocities: self.h1_velocities.clone(),
349 bounds: (self.cell.bounds_low, self.cell.bounds_high),
350 }
351 }
352}
353
354pub(in crate::solvent) fn n_water_mols(cell: &SimBox, solute_atoms: &[AtomDynamics]) -> usize {
356 let cell_volume = cell.volume();
357 let mol_volume = sa_surface::vol_take_up_by_atoms(solute_atoms);
358 let free_vol = cell_volume - mol_volume;
359
360 let dims = format!(
361 "{}:.2 x {:.2} x {:.2}",
362 (cell.bounds_high.x - cell.bounds_low.x).abs(),
363 (cell.bounds_high.y - cell.bounds_low.y).abs(),
364 (cell.bounds_high.z - cell.bounds_low.z).abs()
365 );
366
367 println!(
368 "Solvent-free vol: {:.2} Cell vol: {:.2} (ų / 1,000). Dims: {dims} Å",
369 free_vol / 1_000.,
370 cell_volume / 1_000.
371 );
372
373 (WATER_MOLS_PER_VOL * free_vol).round() as usize
375}
376
377pub fn water_mols_from_template(
389 cell: &SimBox,
390 solute: &[AtomDynamics],
392 specify_num_water: Option<usize>,
393 template_type: &SolventTemplateType,
394 skip_pbc_filter: bool,
400) -> Vec<WaterMolOpc> {
401 match water_mols_from_template_in_region(
402 cell,
403 cell,
404 solute,
405 specify_num_water,
406 template_type,
407 skip_pbc_filter,
408 ) {
409 Ok(water) => water,
410 Err(e) => {
411 eprintln!("\nError initializing water: {e}");
412 Vec::new()
413 }
414 }
415}
416
417fn validate_positive_cell(label: &str, cell: &SimBox) -> io::Result<()> {
418 if cell.extent.x.is_finite()
419 && cell.extent.y.is_finite()
420 && cell.extent.z.is_finite()
421 && cell.extent.x > 0.0
422 && cell.extent.y > 0.0
423 && cell.extent.z > 0.0
424 {
425 Ok(())
426 } else {
427 Err(io::Error::new(
428 io::ErrorKind::InvalidInput,
429 format!(
430 "{label} must have positive finite dimensions; got low={:?}, high={:?}.",
431 cell.bounds_low, cell.bounds_high
432 ),
433 ))
434 }
435}
436
437pub fn water_mols_from_template_in_region(
446 cell: &SimBox,
447 region: &SimBox,
448 solute: &[AtomDynamics],
449 specify_num_water: Option<usize>,
450 template_type: &SolventTemplateType,
451 skip_pbc_filter: bool,
452) -> io::Result<Vec<WaterMolOpc>> {
453 water_mols_from_template_in_region_avoiding(
454 cell,
455 region,
456 solute,
457 &[],
458 specify_num_water,
459 template_type,
460 skip_pbc_filter,
461 )
462}
463
464#[derive(Clone, Copy)]
465struct WaterTemplateCandidate {
466 tile: (i32, i32, i32),
467 o_posit: Vec3,
468 h0_posit: Vec3,
469 h1_posit: Vec3,
470 o_velocity: Vec3,
471 h0_velocity: Vec3,
472 h1_velocity: Vec3,
473}
474
475fn candidate_fits_region(
476 candidate: &WaterTemplateCandidate,
477 cell: &SimBox,
478 region: &SimBox,
479) -> bool {
480 [candidate.o_posit, candidate.h0_posit, candidate.h1_posit]
481 .into_iter()
482 .all(|posit| region.contains(posit) && cell.contains(posit))
483}
484
485fn spatially_interleave_candidates(
486 candidates: Vec<WaterTemplateCandidate>,
487 region: &SimBox,
488 desired_count: usize,
489) -> Vec<WaterTemplateCandidate> {
490 let bins_per_axis = (desired_count as f32).cbrt().ceil() as usize;
491 let mut bins = vec![Vec::new(); bins_per_axis.pow(3)];
492 let bin_coord = |value: f32, low: f32, extent: f32| {
493 (((value - low) / extent * bins_per_axis as f32).floor() as usize).min(bins_per_axis - 1)
494 };
495
496 for candidate in candidates {
497 let x = bin_coord(candidate.o_posit.x, region.bounds_low.x, region.extent.x);
498 let y = bin_coord(candidate.o_posit.y, region.bounds_low.y, region.extent.y);
499 let z = bin_coord(candidate.o_posit.z, region.bounds_low.z, region.extent.z);
500 bins[x + bins_per_axis * (y + bins_per_axis * z)].push(candidate);
501 }
502
503 let max_bin_len = bins.iter().map(Vec::len).max().unwrap_or(0);
504 let mut bin_order: Vec<_> = (0..bins.len()).collect();
505 bin_order.sort_unstable_by_key(|index| (*index as u32).wrapping_mul(0x9E37_79B9));
506 let mut result = Vec::new();
507
508 for round in 0..max_bin_len {
509 for &bin_i in &bin_order {
510 let bin = &bins[bin_i];
511 if let Some(candidate) = bin.get(round) {
512 result.push(*candidate);
513 }
514 }
515 }
516
517 result
518}
519
520pub(crate) fn water_mols_from_template_in_region_avoiding(
521 cell: &SimBox,
522 region: &SimBox,
523 solute: &[AtomDynamics],
524 prior_water: &[WaterMolOpc],
525 specify_num_water: Option<usize>,
526 template_type: &SolventTemplateType,
527 skip_pbc_filter: bool,
528) -> io::Result<Vec<WaterMolOpc>> {
529 validate_positive_cell("Simulation cell", cell)?;
530 validate_positive_cell("Water placement region", region)?;
531
532 if !cell.contains(region.bounds_low) || !cell.contains(region.bounds_high) {
533 return Err(io::Error::new(
534 io::ErrorKind::InvalidInput,
535 "Water placement region must be fully inside the simulation cell.",
536 ));
537 }
538
539 println!("Initializing solvent molecules...");
540 let start = Instant::now();
541
542 let template = template_type.get_template()?;
543 validate_positive_cell("Water template cell", &template.cell)?;
544
545 let solute_for_count: Vec<_> = if cell == region {
546 Vec::new()
547 } else {
548 solute
549 .iter()
550 .filter(|atom| region.contains(atom.posit))
551 .cloned()
552 .collect()
553 };
554 let n_mols = specify_num_water.unwrap_or_else(|| {
555 if cell == region {
556 n_water_mols(region, solute)
557 } else {
558 n_water_mols(region, &solute_for_count)
559 }
560 });
561 let mut result = Vec::with_capacity(n_mols);
562
563 if n_mols == 0 {
564 println!("Complete in {} ms.", start.elapsed().as_millis());
565 return Ok(result);
566 }
567
568 let solute_posits: Vec<_> = solute.iter().map(|a| a.posit).collect();
569
570 let template_size = template.cell.extent;
571 let template_ctr = template.cell.center();
572
573 let region_ctr = region.center();
574
575 let base_offset = region_ctr - template_ctr;
577
578 let region_size = region.extent;
580 let half_x = (region_size.x / (2.0 * template_size.x)).ceil() as i32 + 1;
581 let half_y = (region_size.y / (2.0 * template_size.y)).ceil() as i32 + 1;
582 let half_z = (region_size.z / (2.0 * template_size.z)).ceil() as i32 + 1;
583
584 let mut loops_used = 0;
585
586 let mut placed_tiles: Vec<(i32, i32, i32)> = Vec::with_capacity(n_mols);
589
590 let make_candidate =
591 |tile: (i32, i32, i32), tile_offset: Vec3, i: usize| WaterTemplateCandidate {
592 tile,
593 o_posit: template.o_posits[i] + tile_offset,
594 h0_posit: template.h0_posits[i] + tile_offset,
595 h1_posit: template.h1_posits[i] + tile_offset,
596 o_velocity: template.o_velocities[i],
597 h0_velocity: template.h0_velocities[i],
598 h1_velocity: template.h1_velocities[i],
599 };
600
601 let mut place_candidate = |candidate: WaterTemplateCandidate| {
602 if !candidate_fits_region(&candidate, cell, region) {
603 return false;
604 }
605
606 for &atom_p in &solute_posits {
608 if cell
609 .min_image(atom_p - candidate.o_posit)
610 .magnitude_squared()
611 < MIN_NONWATER_DIST_SQ
612 {
613 return false;
614 }
615 }
616
617 for w in prior_water {
621 let diff = cell.min_image(w.o.posit - candidate.o_posit);
622 if diff.magnitude_squared() < PBC_MIN_WATER_O_O_DIST_SQ {
623 return false;
624 }
625 }
626
627 for (j, w) in result.iter().enumerate() {
641 let diff = w.o.posit - candidate.o_posit;
642 let direct_sq = diff.magnitude_squared();
643 if direct_sq < MIN_WATER_O_O_DIST_SQ {
644 return false;
645 }
646 let min_image_sq = cell.min_image(diff).magnitude_squared();
647 if min_image_sq < MIN_WATER_O_O_DIST_SQ {
650 return false;
651 }
652 if !skip_pbc_filter && placed_tiles[j] == candidate.tile {
653 if min_image_sq < PBC_MIN_WATER_O_O_DIST_SQ && min_image_sq < direct_sq {
654 return false;
655 }
656 }
657 }
658
659 let mut mol = WaterMolOpc::new(
660 Vec3::new_zero(),
661 Vec3::new_zero(),
662 Quaternion::new_identity(),
663 );
664
665 mol.o.posit = candidate.o_posit;
669 mol.h0.posit = candidate.h0_posit;
670 mol.h1.posit = candidate.h1_posit;
671
672 mol.o.vel = candidate.o_velocity;
673 mol.h0.vel = candidate.h0_velocity;
674 mol.h1.vel = candidate.h1_velocity;
675 mol.update_virtual_site();
676
677 result.push(mol);
678 placed_tiles.push(candidate.tile);
679 result.len() == n_mols
680 };
681
682 if specify_num_water.is_some() {
683 let mut candidates = Vec::new();
684
685 for ix in -half_x..=half_x {
686 for iy in -half_y..=half_y {
687 for iz in -half_z..=half_z {
688 let tile = (ix, iy, iz);
689 let tile_offset = base_offset
690 + Vec3::new(
691 ix as f32 * template_size.x,
692 iy as f32 * template_size.y,
693 iz as f32 * template_size.z,
694 );
695
696 for i in 0..template.o_posits.len() {
697 loops_used += 1;
698 let candidate = make_candidate(tile, tile_offset, i);
699 if candidate_fits_region(&candidate, cell, region) {
700 candidates.push(candidate);
701 }
702 }
703 }
704 }
705 }
706
707 for candidate in spatially_interleave_candidates(candidates, region, n_mols) {
708 if place_candidate(candidate) {
709 break;
710 }
711 }
712 } else {
713 'tiles: for ix in -half_x..=half_x {
714 for iy in -half_y..=half_y {
715 for iz in -half_z..=half_z {
716 let tile = (ix, iy, iz);
717 let tile_offset = base_offset
718 + Vec3::new(
719 ix as f32 * template_size.x,
720 iy as f32 * template_size.y,
721 iz as f32 * template_size.z,
722 );
723
724 for i in 0..template.o_posits.len() {
725 loops_used += 1;
726 if place_candidate(make_candidate(tile, tile_offset, i)) {
727 break 'tiles;
728 }
729 }
730 }
731 }
732 }
733 }
734
735 let elapsed = start.elapsed().as_millis();
736 println!(
737 "Added {} / {n_mols} solvent mols in {elapsed} ms. Used {loops_used} loops",
738 result.len()
739 );
740
741 Ok(result)
742}
743
744impl MdState {
745 fn mark_solute_static_for_init_relaxation(&mut self) -> Vec<bool> {
746 let mut static_state = Vec::with_capacity(self.atoms.len());
747
748 for (i, atom) in self.atoms.iter_mut().enumerate() {
749 static_state.push(atom.static_);
750 if i < self.solute_atom_count {
751 atom.static_ = true;
752 }
753 }
754
755 static_state
756 }
757
758 fn restore_static_state(&mut self, static_state: &[bool]) {
759 for (atom, &was_static) in self.atoms.iter_mut().zip(static_state.iter()) {
760 atom.static_ = was_static;
761 }
762 }
763
764 pub fn md_on_solute_only(&mut self, dev: &ComputationDevice) {
774 println!("Initializing solvent structure prior to production MD...");
775 let start = Instant::now();
776
777 self.solvent_only_sim_at_init = true;
779 let thermo_dof_prev = self.thermo_dof;
780
781 let static_state = self.mark_solute_static_for_init_relaxation();
784 self.thermo_dof = self.dof_for_thermo();
785
786 let steps = match self.cfg.solvent {
787 Solvent::None => 0,
788 Solvent::WaterOpc
789 | Solvent::WaterOpcSpecifyMolCount(_)
790 | Solvent::WaterOpcCustomRegions(_) => NUM_EQUILIBRATION_STEPS_WATER,
791 Solvent::OctanolWithWater | Solvent::Custom(_) => NUM_EQUILIBRATION_STEPS_OTHER_SOLVENT,
792 };
793
794 for _ in 0..steps {
795 self.step(dev, DT_EQUILIBRATION, None);
796 }
797
798 self.restore_static_state(&static_state);
799 self.solvent_only_sim_at_init = false;
800 self.thermo_dof = thermo_dof_prev;
801 self.step_count = 0; let elapsed = start.elapsed().as_millis();
804 println!("Solvent initialization MD complete in {elapsed} ms");
805 }
806}