1#![allow(clippy::excessive_precision)]
2
3use std::{f32::consts::TAU, fs, io, path::Path, time::Instant};
11
12use bincode::{Decode, Encode};
13use bio_files::gromacs;
14use lin_alg::{
15 f32::{Mat3 as Mat3F32, Quaternion, Vec3},
16 f64::{Quaternion as QuaternionF64, Vec3 as Vec3F64},
17};
18use rand::{Rng, distr::Uniform, rngs::ThreadRng};
19use rand_distr::{Distribution, Normal};
20
21use crate::{
22 AtomDynamics, ComputationDevice, MdState, MolDynamics, NATIVE_TO_KCAL,
23 barostat::SimBox,
24 partial_charge_inference::{files::load_from_bytes_bincode, save},
25 sa_surface,
26 solvent::WaterMol,
27 thermostat::{GAS_CONST_R, KB_A2_PS2_PER_K_PER_AMU},
28};
29
30const WATER_DENSITY: f32 = 0.997;
33
34const MASS_WATER: f32 = 18.015_28;
38
39const N_A: f32 = 6.022_140_76e23;
41
42const WATER_MOLS_PER_VOL: f32 = WATER_DENSITY * N_A / (MASS_WATER * 1.0e24);
45
46const MIN_NONWATER_DIST: f32 = 1.7;
51const MIN_NONWATER_DIST_SQ: f32 = MIN_NONWATER_DIST * MIN_NONWATER_DIST;
52
53const MIN_WATER_O_O_DIST: f32 = 1.7;
55const MIN_WATER_O_O_DIST_SQ: f32 = MIN_WATER_O_O_DIST * MIN_WATER_O_O_DIST;
56
57const PBC_MIN_WATER_O_O_DIST: f32 = 2.8;
68const PBC_MIN_WATER_O_O_DIST_SQ: f32 = PBC_MIN_WATER_O_O_DIST * PBC_MIN_WATER_O_O_DIST;
69
70const NUM_EQUILIBRATION_STEPS: usize = 200;
74const DT_EQUILIBRATION: f32 = 0.0005;
77
78pub const WATER_TEMPLATE_60A: &[u8] =
80 include_bytes!("../../param_data/water_60A.water_init_template");
81
82#[derive(Encode, Decode)]
92pub struct WaterInitTemplate {
93 o_posits: Vec<Vec3>,
95 h0_posits: Vec<Vec3>,
96 h1_posits: Vec<Vec3>,
97 o_velocities: Vec<Vec3>,
98 h0_velocities: Vec<Vec3>,
99 h1_velocities: Vec<Vec3>,
100 bounds: (Vec3, Vec3),
103}
104
105impl WaterInitTemplate {
106 pub fn load(path: &Path) -> io::Result<Self> {
108 let bytes = fs::read(path)?;
109 load_from_bytes_bincode(&bytes)
110 }
111
112 pub fn from_bytes(bytes: &[u8]) -> io::Result<Self> {
113 load_from_bytes_bincode(bytes)
114 }
115
116 pub fn create_and_save(
119 water: &[WaterMol],
120 bounds: (Vec3, Vec3),
121 path: &Path,
122 ) -> io::Result<()> {
123 let n = water.len();
124
125 let mut o_posits = Vec::with_capacity(n);
126 let mut h0_posits = Vec::with_capacity(n);
127 let mut h1_posits = Vec::with_capacity(n);
128
129 let mut o_velocities = Vec::with_capacity(n);
130 let mut h0_velocities = Vec::with_capacity(n);
131 let mut h1_velocities = Vec::with_capacity(n);
132
133 let water = {
138 let ctr = (bounds.1 + bounds.0) / 2.;
139
140 let mut w = water.to_vec();
141 w.sort_by(|a, b| {
142 let da = (a.o.posit - ctr).magnitude_squared();
143 let db = (b.o.posit - ctr).magnitude_squared();
144 da.total_cmp(&db)
145 });
146
147 w
148 };
149
150 for mol in water {
151 o_posits.push(mol.o.posit);
152 h0_posits.push(mol.h0.posit);
153 h1_posits.push(mol.h1.posit);
154
155 o_velocities.push(mol.o.vel);
156 h0_velocities.push(mol.h0.vel);
157 h1_velocities.push(mol.h1.vel);
158
159 }
162
163 let result = Self {
164 o_posits,
165 h0_posits,
166 h1_posits,
167 o_velocities,
168 h0_velocities,
169 h1_velocities,
170 bounds,
171 };
172
173 save(path, &result)
174 }
175
176 pub fn to_gromacs(&self) -> gromacs::solvate::WaterInitTemplate {
180 gromacs::solvate::WaterInitTemplate {
181 o_posits: self.o_posits.clone(),
182 h0_posits: self.h0_posits.clone(),
183 h1_posits: self.h1_posits.clone(),
184 o_velocities: self.o_velocities.clone(),
185 h0_velocities: self.h0_velocities.clone(),
186 h1_velocities: self.h1_velocities.clone(),
187 bounds: self.bounds,
188 }
189 }
190}
191
192fn n_water_mols(cell: &SimBox, solute_atoms: &[AtomDynamics]) -> usize {
194 let cell_volume = cell.volume();
195 let mol_volume = sa_surface::vol_take_up_by_atoms(solute_atoms);
196 let free_vol = cell_volume - mol_volume;
197
198 let dims = format!(
199 "{}:.2 x {:.2} x {:.2}",
200 (cell.bounds_high.x - cell.bounds_low.x).abs(),
201 (cell.bounds_high.y - cell.bounds_low.y).abs(),
202 (cell.bounds_high.z - cell.bounds_low.z).abs()
203 );
204
205 println!(
206 "Solvent-free vol: {:.2} Cell vol: {:.2} (ų / 1,000). Dims: {dims} Å",
207 free_vol / 1_000.,
208 cell_volume / 1_000.
209 );
210
211 (WATER_MOLS_PER_VOL * free_vol).round() as usize
213}
214
215pub fn make_water_mols(
225 cell: &SimBox,
226 atoms: &[AtomDynamics],
227 specify_num_water: Option<usize>,
228 template_override: Option<&WaterInitTemplate>,
229 skip_pbc_filter: bool,
235) -> Vec<WaterMol> {
236 println!("Initializing solvent molecules...");
237 let start = Instant::now();
238
239 let default_template;
240
241 let template: &WaterInitTemplate = match template_override {
242 Some(t) => t,
243 None => {
244 default_template = load_from_bytes_bincode(WATER_TEMPLATE_60A).unwrap();
245 &default_template
246 }
247 };
248
249 let n_mols = specify_num_water.unwrap_or_else(|| n_water_mols(cell, atoms));
250 let mut result = Vec::with_capacity(n_mols);
251
252 if n_mols == 0 {
253 println!("Complete in {} ms.", start.elapsed().as_millis());
254 return result;
255 }
256
257 let atom_posits: Vec<_> = atoms.iter().map(|a| a.posit).collect();
258
259 let template_size = template.bounds.1 - template.bounds.0;
260 let template_ctr = (template.bounds.0 + template.bounds.1) / 2.;
261 let cell_ctr = (cell.bounds_low + cell.bounds_high) / 2.;
262
263 let base_offset = cell_ctr - template_ctr;
265
266 let cell_size = cell.bounds_high - cell.bounds_low;
268 let half_x = (cell_size.x / (2.0 * template_size.x)).ceil() as i32 + 1;
269 let half_y = (cell_size.y / (2.0 * template_size.y)).ceil() as i32 + 1;
270 let half_z = (cell_size.z / (2.0 * template_size.z)).ceil() as i32 + 1;
271
272 let mut loops_used = 0;
273
274 'tiles: for ix in -half_x..=half_x {
275 for iy in -half_y..=half_y {
276 for iz in -half_z..=half_z {
277 let tile_offset = base_offset
278 + Vec3::new(
279 ix as f32 * template_size.x,
280 iy as f32 * template_size.y,
281 iz as f32 * template_size.z,
282 );
283
284 'mol: for i in 0..template.o_posits.len() {
285 let o_posit = template.o_posits[i] + tile_offset;
286 let h0_posit = template.h0_posits[i] + tile_offset;
287 let h1_posit = template.h1_posits[i] + tile_offset;
288
289 loops_used += 1;
290
291 if !cell.contains(o_posit) {
292 continue;
293 }
294
295 for &atom_p in &atom_posits {
297 if (atom_p - o_posit).magnitude_squared() < MIN_NONWATER_DIST_SQ {
298 continue 'mol;
299 }
300 }
301
302 for w in &result {
311 let diff = w.o.posit - o_posit;
312 let direct_sq = diff.magnitude_squared();
313 if direct_sq < MIN_WATER_O_O_DIST_SQ {
314 continue 'mol;
315 }
316 let min_image_sq = cell.min_image(diff).magnitude_squared();
317 if min_image_sq < MIN_WATER_O_O_DIST_SQ {
320 continue 'mol;
321 }
322 if !skip_pbc_filter {
323 if min_image_sq < PBC_MIN_WATER_O_O_DIST_SQ && min_image_sq < direct_sq
324 {
325 continue 'mol;
326 }
327 }
328 }
329
330 let mut mol = WaterMol::new(
331 Vec3::new_zero(),
332 Vec3::new_zero(),
333 Quaternion::new_identity(),
334 );
335 mol.o.posit = o_posit;
336 mol.h0.posit = h0_posit;
337 mol.h1.posit = h1_posit;
338 mol.o.vel = template.o_velocities[i];
339 mol.h0.vel = template.h0_velocities[i];
340 mol.h1.vel = template.h1_velocities[i];
341
342 result.push(mol);
343
344 if result.len() == n_mols {
345 break 'tiles;
346 }
347 }
348 }
349 }
350 }
351
352 let elapsed = start.elapsed().as_millis();
353 println!(
354 "Added {} / {n_mols} solvent mols in {elapsed} ms. Used {loops_used} loops",
355 result.len()
356 );
357
358 result
359}
360
361pub(crate) fn pack_custom_solvent(
371 bounds_low: Vec3,
372 bounds_high: Vec3,
373 existing_posits: &[Vec3F64], mols_solvent: &[(MolDynamics, usize)],
375) -> Vec<MolDynamics> {
376 const MIN_ATOM_DIST_SQ: f64 = 1.4 * 1.4; const WALL_MARGIN: f64 = 0.6; const MAX_ROT_ATTEMPTS: usize = 200;
382
383 let mut rng = rand::rng();
384
385 let lo = Vec3F64::new(
386 bounds_low.x as f64,
387 bounds_low.y as f64,
388 bounds_low.z as f64,
389 );
390 let hi = Vec3F64::new(
391 bounds_high.x as f64,
392 bounds_high.y as f64,
393 bounds_high.z as f64,
394 );
395 let box_size = hi - lo;
396 let box_ctr = (lo + hi) * 0.5;
397
398 let mut placed_posits: Vec<Vec3F64> = existing_posits.to_vec();
400
401 let mut result: Vec<MolDynamics> = Vec::new();
402
403 for (mol, count) in mols_solvent {
404 let count = *count;
405 if count == 0 {
406 continue;
407 }
408
409 let template_world: Vec<Vec3F64> = if let Some(ap) = &mol.atom_posits {
411 ap.clone()
412 } else {
413 mol.atoms.iter().map(|a| a.posit).collect()
414 };
415
416 let n_atoms = template_world.len();
417 if n_atoms == 0 {
418 continue;
419 }
420
421 let centroid = template_world
423 .iter()
424 .fold(Vec3F64::new(0., 0., 0.), |s, &p| s + p)
425 * (1.0 / n_atoms as f64);
426 let local: Vec<Vec3F64> = template_world.iter().map(|&p| p - centroid).collect();
427 let bounding_r: f64 = local.iter().map(|p| p.magnitude()).fold(0.0_f64, f64::max);
428
429 let search_sq = (bounding_r * 2.0 + 2.0).powi(2);
431
432 let safe_margin = bounding_r + WALL_MARGIN;
435 let inner_lo = lo + Vec3F64::new(safe_margin, safe_margin, safe_margin);
436 let inner_hi = hi - Vec3F64::new(safe_margin, safe_margin, safe_margin);
437
438 if inner_lo.x >= inner_hi.x || inner_lo.y >= inner_hi.y || inner_lo.z >= inner_hi.z {
439 eprintln!(
440 "pack_custom_solvent: box too small for molecule \
441 (bounding_r={:.1} Å, need >{:.1} Å per side); skipping {} copies.",
442 bounding_r,
443 2.0 * safe_margin,
444 count
445 );
446 continue;
447 }
448
449 let inner_size = inner_hi - inner_lo;
450 let naive_n = (count as f64).cbrt().ceil() as usize;
453 let scale = (box_size.x / inner_size.x)
454 .max(box_size.y / inner_size.y)
455 .max(box_size.z / inner_size.z);
456 let n = ((naive_n as f64 * scale).ceil() as usize).max(3);
457 let (sx, sy, sz) = (
458 box_size.x / n as f64,
459 box_size.y / n as f64,
460 box_size.z / n as f64,
461 );
462 let (hx, hy, hz) = (
464 box_size.x * 0.5 - WALL_MARGIN,
465 box_size.y * 0.5 - WALL_MARGIN,
466 box_size.z * 0.5 - WALL_MARGIN,
467 );
468
469 let mut grid: Vec<Vec3F64> = (0..n)
471 .flat_map(|ix| {
472 (0..n).flat_map(move |iy| {
473 (0..n).map(move |iz| {
474 Vec3F64::new(
475 lo.x + (ix as f64 + 0.5) * sx,
476 lo.y + (iy as f64 + 0.5) * sy,
477 lo.z + (iz as f64 + 0.5) * sz,
478 )
479 })
480 })
481 })
482 .filter(|c| {
483 c.x >= inner_lo.x
484 && c.x <= inner_hi.x
485 && c.y >= inner_lo.y
486 && c.y <= inner_hi.y
487 && c.z >= inner_lo.z
488 && c.z <= inner_hi.z
489 })
490 .collect();
491
492 for copy_i in 0..count {
493 let best_cell_idx = if placed_posits.is_empty() {
495 0
496 } else {
497 grid.iter()
498 .enumerate()
499 .map(|(i, &cell_ctr)| {
500 let min_dsq = placed_posits
501 .iter()
502 .map(|&p| (cell_ctr - p).magnitude_squared())
503 .fold(f64::MAX, f64::min);
504 (i, min_dsq)
505 })
506 .max_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(std::cmp::Ordering::Equal))
507 .map(|(i, _)| i)
508 .unwrap_or(0)
509 };
510
511 let world_ctr = grid.remove(best_cell_idx);
512
513 let mut best_min_sq = f64::NEG_INFINITY;
514 let mut best_posits: Vec<Vec3F64> = Vec::new();
515
516 for _ in 0..MAX_ROT_ATTEMPTS {
517 let (w, x, y, z): (f64, f64, f64, f64) =
518 (rng.random(), rng.random(), rng.random(), rng.random());
519 let rot = QuaternionF64::new(w, x, y, z).to_normalized();
520
521 let new_posits: Vec<Vec3F64> = local
522 .iter()
523 .map(|&l| rot.rotate_vec(l) + world_ctr)
524 .collect();
525
526 if !new_posits.iter().all(|p| {
528 let dp = *p - box_ctr;
529 dp.x.abs() <= hx && dp.y.abs() <= hy && dp.z.abs() <= hz
530 }) {
531 continue;
532 }
533
534 let mut min_sq = f64::MAX;
536 'check: for &np in &new_posits {
537 for &pp in &placed_posits {
538 if (pp - world_ctr).magnitude_squared() > search_sq {
539 continue;
540 }
541 let dsq = (np - pp).magnitude_squared();
542 if dsq < min_sq {
543 min_sq = dsq;
544 if min_sq < MIN_ATOM_DIST_SQ {
545 break 'check;
546 }
547 }
548 }
549 }
550
551 if min_sq > best_min_sq {
552 best_min_sq = min_sq;
553 best_posits = new_posits;
554 }
555 if best_min_sq >= MIN_ATOM_DIST_SQ {
556 break; }
558 }
559
560 if best_posits.is_empty() {
562 best_posits = local.iter().map(|&l| l + world_ctr).collect();
563 }
564
565 if best_min_sq < MIN_ATOM_DIST_SQ {
566 eprintln!(
567 "pack_custom_solvent: copy {copy_i}: best min atom dist {:.2} Å — \
568 placing with soft overlap (energy minimiser will resolve).",
569 best_min_sq.max(0.0).sqrt()
570 );
571 }
572
573 placed_posits.extend_from_slice(&best_posits);
574
575 let mut mol_copy = mol.clone();
576 mol_copy.atom_posits = Some(best_posits);
577 result.push(mol_copy);
578
579 if grid.is_empty() && copy_i + 1 < count {
580 eprintln!(
581 "pack_custom_solvent: grid cells exhausted after {} / {} copies; \
582 box may be too small for this many solvent molecules.",
583 copy_i + 1,
584 count
585 );
586 break;
587 }
588 }
589 }
590
591 result
592}
593
594#[allow(unused)]
595pub fn make_water_mols_grid(
612 cell: &SimBox,
613 temperature_tgt: f32,
614 zero_com_drift: bool,
615) -> Vec<WaterMol> {
616 println!("Initializing a solvent grid, as part of template preparation...");
617 let mut rng = rand::rng();
619 let distro = Uniform::<f32>::new(0.0, 1.0).unwrap();
620
621 let n_mols = n_water_mols(cell, &[]);
622
623 let mut result: Vec<WaterMol> = Vec::with_capacity(n_mols);
624
625 let lx = cell.bounds_high.x - cell.bounds_low.x;
627 let ly = cell.bounds_high.y - cell.bounds_low.y;
628 let lz = cell.bounds_high.z - cell.bounds_low.z;
629
630 let base = (n_mols as f32).cbrt().round().max(1.0) as usize;
631 let n_x = base;
632 let n_y = base;
633 let n_z = n_mols.div_ceil(n_x * n_y);
634
635 let spacing_x = lx / n_x as f32;
636 let spacing_y = ly / n_y as f32;
637 let spacing_z = lz / n_z as f32;
638
639 let fault_ratio = 3;
642
643 let mut num_added = 0;
644 let mut loops_used = 0;
645
646 'outer: for i in 0..n_mols * fault_ratio {
647 let a = i % n_x;
648 let b = (i / n_x) % n_y;
649 let c = (i / (n_x * n_y)) % n_z;
650
651 let posit = Vec3::new(
652 cell.bounds_low.x + (a as f32 + 0.5) * spacing_x,
653 cell.bounds_low.y + (b as f32 + 0.5) * spacing_y,
654 cell.bounds_low.z + (c as f32 + 0.5) * spacing_z,
655 );
656
657 for w in &result {
659 let dist_sq = (w.o.posit - posit).magnitude_squared();
660 if dist_sq < MIN_WATER_O_O_DIST_SQ {
661 loops_used += 1;
662 continue 'outer;
663 }
664 }
665
666 result.push(WaterMol::new(
667 posit,
668 Vec3::new_zero(),
669 random_quaternion(&mut rng, distro),
670 ));
671 num_added += 1;
672
673 if num_added == n_mols {
674 break;
675 }
676 loops_used += 1;
677 }
678
679 init_velocities(&mut result, temperature_tgt, zero_com_drift, &mut rng);
681
682 println!(
683 "Added {} / {n_mols} solvent mols. Used {loops_used} loops",
684 result.len()
685 );
686 result
687}
688
689fn init_velocities(
695 mols: &mut [WaterMol],
696 t_target: f32,
697 zero_com_drift: bool,
698 rng: &mut ThreadRng,
699) {
700 let kT = KB_A2_PS2_PER_K_PER_AMU * t_target;
701
702 for m in mols.iter_mut() {
703 let (r_com, m_tot) = {
705 let mut r = Vec3::new_zero();
706 let mut m_tot = 0.0;
707 for a in [&m.o, &m.h0, &m.h1] {
708 r += a.posit * a.mass;
709 m_tot += a.mass;
710 }
711 (r / m_tot, m_tot)
712 };
713
714 let r_0 = m.o.posit - r_com;
715 let r_h0 = m.h0.posit - r_com;
716 let r_h1 = m.h1.posit - r_com;
717
718 let sigma_v = (kT / m_tot).sqrt();
720 let n = Normal::new(0.0, sigma_v).unwrap();
721 let v_com = Vec3::new(n.sample(rng), n.sample(rng), n.sample(rng));
722
723 let inertia = |r: Vec3, mass: f32| {
726 let r2 = r.dot(r);
727 [
728 [
729 mass * (r2 - r.x * r.x),
730 -mass * r.x * r.y,
731 -mass * r.x * r.z,
732 ],
733 [
734 -mass * r.y * r.x,
735 mass * (r2 - r.y * r.y),
736 -mass * r.y * r.z,
737 ],
738 [
739 -mass * r.z * r.x,
740 -mass * r.z * r.y,
741 mass * (r2 - r.z * r.z),
742 ],
743 ]
744 };
745 let mut I_arr = inertia(r_0, m.o.mass);
746 let add_I = |I: &mut [[f32; 3]; 3], J: [[f32; 3]; 3]| {
747 for i in 0..3 {
748 for j in 0..3 {
749 I[i][j] += J[i][j];
750 }
751 }
752 };
753 add_I(&mut I_arr, inertia(r_h0, m.h0.mass));
754 add_I(&mut I_arr, inertia(r_h1, m.h1.mass));
755
756 let I = Mat3F32::from_arr(I_arr);
757
758 let (eigvecs, eigvals) = I.eigen_vecs_vals();
760 let L_principal = Vec3::new(
761 Normal::new(0.0, (kT * eigvals.x.max(0.0)).sqrt())
762 .unwrap()
763 .sample(rng),
764 Normal::new(0.0, (kT * eigvals.y.max(0.0)).sqrt())
765 .unwrap()
766 .sample(rng),
767 Normal::new(0.0, (kT * eigvals.z.max(0.0)).sqrt())
768 .unwrap()
769 .sample(rng),
770 );
771 let L_world = eigvecs * L_principal; let omega = I.solve_system(L_world); m.o.vel = v_com + omega.cross(r_0);
776 m.h0.vel = v_com + omega.cross(r_h0);
777 m.h1.vel = v_com + omega.cross(r_h1);
778 }
779
780 if zero_com_drift {
781 remove_com_velocity(mols);
783 }
784
785 let (ke_raw, dof) = _kinetic_energy_and_dof(mols, zero_com_drift);
786
787 let temperature_meas = (2.0 * ke_raw) / (dof as f32 * GAS_CONST_R as f32);
789 let lambda = (t_target / temperature_meas).sqrt();
790
791 for a in atoms_mut(mols) {
792 if a.mass > 0.0 {
793 a.vel *= lambda;
794 }
795 }
796}
797
798fn _kinetic_energy_and_dof(mols: &[WaterMol], zero_com_drift: bool) -> (f32, usize) {
801 let mut ke = 0.;
802 for w in mols {
803 ke += (w.o.mass * w.o.vel.magnitude_squared()) as f64;
804 ke += (w.h0.mass * w.h0.vel.magnitude_squared()) as f64;
805 ke += (w.h1.mass * w.h1.vel.magnitude_squared()) as f64;
806 }
807
808 let mut dof = mols.len() * 3;
809
810 if zero_com_drift {
811 dof = dof.saturating_sub(3);
812 }
813
814 (ke as f32 * 0.5 * NATIVE_TO_KCAL, dof)
816}
817
818fn atoms_mut(mols: &mut [WaterMol]) -> impl Iterator<Item = &mut AtomDynamics> {
819 mols.iter_mut()
820 .flat_map(|m| [&mut m.o, &mut m.h0, &mut m.h1].into_iter())
821}
822
823#[allow(unused)]
824fn remove_com_velocity(mols: &mut [WaterMol]) {
826 let mut p = Vec3::new_zero();
827 let mut m_tot = 0.0;
828 for a in atoms_mut(mols) {
829 p += a.vel * a.mass;
830 m_tot += a.mass;
831 }
832
833 let v_com = p / m_tot;
834 for a in atoms_mut(mols) {
835 a.vel -= v_com;
836 }
837}
838
839#[allow(unused)]
840fn random_quaternion(rng: &mut ThreadRng, distro: Uniform<f32>) -> Quaternion {
844 let (u1, u2, u3) = (rng.sample(distro), rng.sample(distro), rng.sample(distro));
845 let sqrt1_minus_u1 = (1.0 - u1).sqrt();
846 let sqrt_u1 = u1.sqrt();
847 let (theta1, theta2) = (TAU * u2, TAU * u3);
848
849 Quaternion::new(
850 sqrt1_minus_u1 * theta1.sin(),
851 sqrt1_minus_u1 * theta1.cos(),
852 sqrt_u1 * theta2.sin(),
853 sqrt_u1 * theta2.cos(),
854 )
855 .to_normalized()
856}
857
858impl MdState {
859 pub fn md_on_water_only(&mut self, dev: &ComputationDevice) {
869 println!("Initializing solvent H bond networks...");
870 let start = Instant::now();
871
872 self.solvent_only_sim_at_init = true;
874
875 let mut static_state = Vec::with_capacity(self.atoms.len());
877 for a in &mut self.atoms {
878 static_state.push(a.static_);
879 a.static_ = true;
880 }
881
882 for _ in 0..NUM_EQUILIBRATION_STEPS {
883 self.step(dev, DT_EQUILIBRATION, None);
884 }
885
886 for (i, a) in self.atoms.iter_mut().enumerate() {
888 a.static_ = static_state[i];
889 }
890
891 self.solvent_only_sim_at_init = false;
892 self.step_count = 0; let elapsed = start.elapsed().as_millis();
895 println!("Water H bond networks complete in {elapsed} ms");
896 }
897}