Skip to main content

dynamics/
com_zero.rs

1//! Zero center of mass linear drift and rotation for all atoms in the system.
2
3use lin_alg::{f32::Vec3, f64::Vec3 as Vec3F64};
4
5use crate::{
6    MdState,
7    solvent::{H_MASS, MASS_WATER_MOL, O_MASS},
8};
9
10const EPS: f64 = 1e-6;
11
12impl MdState {
13    fn dynamic_mass_and_momentum(&self) -> (f64, Vec3F64) {
14        let mut mass_sum = 0.0;
15        let mut p_sum = Vec3F64::new_zero();
16
17        for a in &self.atoms {
18            if a.static_ {
19                continue;
20            }
21            mass_sum += a.mass as f64;
22            let p: Vec3F64 = (a.vel * a.mass).into();
23            p_sum += p;
24        }
25
26        for w in &self.water {
27            mass_sum += MASS_WATER_MOL as f64;
28
29            let p_o: Vec3F64 = (w.o.vel * O_MASS).into();
30            let p_h0: Vec3F64 = (w.h0.vel * H_MASS).into();
31            let p_h1: Vec3F64 = (w.h1.vel * H_MASS).into();
32
33            p_sum += p_o + p_h0 + p_h1;
34        }
35
36        (mass_sum, p_sum)
37    }
38
39    fn shift_dynamic_positions(&mut self, displacement: Vec3) {
40        for a in &mut self.atoms {
41            if a.static_ {
42                continue;
43            }
44            a.posit += displacement;
45        }
46
47        for w in &mut self.water {
48            w.o.posit += displacement;
49            w.h0.posit += displacement;
50            w.h1.posit += displacement;
51            w.m.posit += displacement;
52        }
53    }
54
55    /// Remove center-of-mass drift. This can help stabilize system energy.
56    /// We perform the sums here as f64.
57    pub fn zero_linear_momentum(&mut self) {
58        let (mass_sum, p_sum) = self.dynamic_mass_and_momentum();
59
60        if mass_sum <= EPS {
61            return;
62        }
63
64        let vel_com: Vec3 = (p_sum / mass_sum).into();
65
66        // Subtract uniformly so Σ m v' = 0
67        for a in &mut self.atoms {
68            if a.static_ {
69                continue;
70            }
71            a.vel -= vel_com;
72        }
73
74        // I don't think we need to use SHAKE/RATTLE here, as the velocity
75        // change is uniform for the solvent atoms in a given mol.
76        for w in &mut self.water {
77            w.o.vel -= vel_com;
78            w.h0.vel -= vel_com;
79            w.h1.vel -= vel_com;
80        }
81    }
82
83    /// GROMACS-style linear acceleration correction: use the current COM velocity
84    /// to estimate the displacement accumulated over the removal interval under
85    /// nearly constant COM acceleration, shift positions back uniformly, then
86    /// remove the translational COM velocity.
87    pub(crate) fn zero_linear_momentum_acceleration_corrected(&mut self, interval_dt: f32) {
88        if interval_dt <= 0.0 {
89            self.zero_linear_momentum();
90            return;
91        }
92
93        let (mass_sum, p_sum) = self.dynamic_mass_and_momentum();
94        if mass_sum <= EPS {
95            return;
96        }
97
98        let vel_com: Vec3 = (p_sum / mass_sum).into();
99
100        // If the COM acceleration has been nearly constant since the previous
101        // removal event, the displacement is 0.5 * a * T^2 = 0.5 * v_end * T.
102        self.shift_dynamic_positions(-(vel_com * (0.5 * interval_dt)));
103        self.zero_linear_momentum();
104    }
105
106    // todo: Assess if you want this for multi-molecule systems.
107    /// Remove rigid-body rotation.
108    /// Computes ω from I ω = L about the atoms' COM, then sets v' = v - ω × (r - r_cm).
109    pub fn zero_angular_momentum(&mut self) {
110        let mut mass_sum = 0.0;
111        let mut m_r_sum = Vec3F64::new_zero();
112
113        for a in &self.atoms {
114            if a.static_ {
115                continue;
116            }
117            mass_sum += a.mass as f64;
118
119            let m_r: Vec3F64 = (a.posit * a.mass).into();
120            m_r_sum += m_r;
121        }
122
123        for w in &self.water {
124            mass_sum += MASS_WATER_MOL as f64;
125
126            let mr_o: Vec3F64 = (w.o.posit * O_MASS).into();
127            let mr_h0: Vec3F64 = (w.h0.posit * H_MASS).into();
128            let mr_h1: Vec3F64 = (w.h1.posit * H_MASS).into();
129
130            m_r_sum += mr_o + mr_h0 + mr_h1;
131        }
132
133        if mass_sum <= EPS {
134            return;
135        }
136        let rot_com: Vec3 = (m_r_sum / mass_sum).into();
137
138        // Build inertia tensor I and angular momentum L about r_cm
139        let mut i_xx = 0.0;
140        let mut i_xy = 0.0;
141        let mut i_xz = 0.0;
142        let mut i_yy = 0.0;
143        let mut i_yz = 0.0;
144        let mut i_zz = 0.0;
145
146        let mut L = Vec3::new_zero();
147
148        for a in &self.atoms {
149            if a.static_ {
150                continue;
151            }
152            let m = a.mass;
153
154            let r = a.posit - rot_com;
155            let vxr = r.cross(a.vel);
156            L += vxr * m;
157
158            let rx = r.x;
159            let ry = r.y;
160            let rz = r.z;
161            let r2 = rx * rx + ry * ry + rz * rz;
162
163            i_xx += m * (r2 - rx * rx);
164            i_yy += m * (r2 - ry * ry);
165            i_zz += m * (r2 - rz * rz);
166            i_xy -= m * (rx * ry);
167            i_xz -= m * (rx * rz);
168            i_yz -= m * (ry * rz);
169        }
170
171        for w in &self.water {
172            for a in [&w.o, &w.h0, &w.h1] {
173                let m = a.mass;
174
175                let r = a.posit - rot_com;
176                let vxr = r.cross(a.vel);
177                L += vxr * m;
178
179                let rx = r.x;
180                let ry = r.y;
181                let rz = r.z;
182                let r2 = rx * rx + ry * ry + rz * rz;
183
184                i_xx += m * (r2 - rx * rx);
185                i_yy += m * (r2 - ry * ry);
186                i_zz += m * (r2 - rz * rz);
187                i_xy -= m * (rx * ry);
188                i_xz -= m * (rx * rz);
189                i_yz -= m * (ry * rz);
190            }
191        }
192
193        // Inertia tensor (symmetric)
194        let I = [[i_xx, i_xy, i_xz], [i_xy, i_yy, i_yz], [i_xz, i_yz, i_zz]];
195
196        // Solve I * ω = L  (3x3). Add tiny Tikhonov for degeneracy.
197        let eps = 1.0e-6f32;
198        let Ireg = [
199            [I[0][0] + eps, I[0][1], I[0][2]],
200            [I[1][0], I[1][1] + eps, I[1][2]],
201            [I[2][0], I[2][1], I[2][2] + eps],
202        ];
203
204        // Inverse of 3x3 (cofactor / det)
205        let det = Ireg[0][0] * (Ireg[1][1] * Ireg[2][2] - Ireg[1][2] * Ireg[2][1])
206            - Ireg[0][1] * (Ireg[1][0] * Ireg[2][2] - Ireg[1][2] * Ireg[2][0])
207            + Ireg[0][2] * (Ireg[1][0] * Ireg[2][1] - Ireg[1][1] * Ireg[2][0]);
208
209        if det.abs() < 1e-12 {
210            return;
211        }
212
213        let inv_det = 1.0 / det;
214        let inv = [
215            [
216                (Ireg[1][1] * Ireg[2][2] - Ireg[1][2] * Ireg[2][1]) * inv_det,
217                (Ireg[0][2] * Ireg[2][1] - Ireg[0][1] * Ireg[2][2]) * inv_det,
218                (Ireg[0][1] * Ireg[1][2] - Ireg[0][2] * Ireg[1][1]) * inv_det,
219            ],
220            [
221                (Ireg[1][2] * Ireg[2][0] - Ireg[1][0] * Ireg[2][2]) * inv_det,
222                (Ireg[0][0] * Ireg[2][2] - Ireg[0][2] * Ireg[2][0]) * inv_det,
223                (Ireg[0][2] * Ireg[1][0] - Ireg[0][0] * Ireg[1][2]) * inv_det,
224            ],
225            [
226                (Ireg[1][0] * Ireg[2][1] - Ireg[1][1] * Ireg[2][0]) * inv_det,
227                (Ireg[0][1] * Ireg[2][0] - Ireg[0][0] * Ireg[2][1]) * inv_det,
228                (Ireg[0][0] * Ireg[1][1] - Ireg[0][1] * Ireg[1][0]) * inv_det,
229            ],
230        ];
231
232        let omega = Vec3 {
233            x: inv[0][0] * L.x + inv[0][1] * L.y + inv[0][2] * L.z,
234            y: inv[1][0] * L.x + inv[1][1] * L.y + inv[1][2] * L.z,
235            z: inv[2][0] * L.x + inv[2][1] * L.y + inv[2][2] * L.z,
236        };
237
238        // If ω is tiny, nothing to do
239        if !omega.x.is_finite() || !omega.y.is_finite() || !omega.z.is_finite() {
240            return;
241        }
242        if omega.magnitude_squared() < 1e-16 {
243            return;
244        }
245
246        // v' = v - ω × (r - r_cm)
247        for a in &mut self.atoms {
248            if a.static_ {
249                continue;
250            }
251            let r = a.posit - rot_com;
252            a.vel -= omega.cross(r);
253        }
254
255        // todo: Do we need to shake/rattle here? likely.
256        for w in &mut self.water {
257            let r_o = w.o.posit - rot_com;
258            let r_h0 = w.h0.posit - rot_com;
259            let r_h1 = w.h1.posit - rot_com;
260
261            w.o.vel -= omega.cross(r_o);
262            w.h0.vel -= omega.cross(r_h0);
263            w.h1.vel -= omega.cross(r_h1);
264        }
265
266        // Clean up any translation introduced by roundoff
267        self.zero_linear_momentum();
268    }
269}