hoomd_md/thermostat/martyna_tuckerman_tobias_klein.rs
1// Copyright (c) 2024-2026 The Regents of the University of Michigan.
2// Part of hoomd-rs, released under the BSD 3-Clause License.
3
4//! Implement `MartynaTuckermanTobiasKlein`
5
6use rand::Rng;
7use rand_distr::{Distribution, Normal};
8use serde::{Deserialize, Serialize};
9
10use crate::Thermostat;
11use hoomd_simulation::macrostate::Temperature;
12use hoomd_utility::valid::PositiveReal;
13
14/// Nosé-Hoover thermostat.
15///
16/// [`MartynaTuckermanTobiasKlein`] adds a new degree of freedom ($`\eta`$)
17/// to a molecular dynamics simulation in such a way that the existing
18/// degrees of freedom sample a constant temperature ensemble. Each
19/// [`MartynaTuckermanTobiasKlein`] instance stores $`\eta`$ and its momentum,
20/// $`\xi`$, internally.
21///
22/// The extended Hamiltonian $`H`$ is:
23/// ```math
24/// H = K + U
25/// + N kT \eta
26/// + \frac{1}{2} N kT \tau^2\xi^2
27/// ```
28/// Where $`K`$ is the kinetic energy of the system, $`U`$ is the potential
29/// energy of the system, $`N`$ is the number of degrees of freedom, and $`kT`$
30/// is the temperature.
31///
32/// Following the Trotter decomposition of Liouvillian,
33/// [`MartynaTuckermanTobiasKlein`] integrates $`\eta`$ and $`\xi`$ forward
34/// by half time step $`\frac{\delta t}{2}`$ via the following procedure:
35///
36/// ```math
37/// \begin{align*}
38///
39/// G_\mathrm{old} &= \frac{1}{\tau^2} \left( \frac{2 K}{N kT} - 1 \right) \\
40///
41/// \xi \left\{ t+\frac{\delta t} {4} \right\} &= \xi \{ t \} + G_\mathrm{old}\frac{\delta t}{4} \\
42///
43/// \alpha &= \exp\left[ -\xi \left\{ t+\frac{\delta t} {4} \right\} \frac{dt}{2} \right] \\
44///
45/// K_{new} &= K \alpha^2 \\
46///
47/// \eta \left\{ t+\frac{\delta t} {2} \right\} &= \eta \{ t \} + \xi \left\{ t+\frac{\delta t} {4} \right\} \frac{\delta t}{2} \\
48///
49/// G_\mathrm{new} &= \frac{1}{\tau^2} \left( \frac{2 K_\mathrm{new} }{kT} - 1 \right) \\
50///
51/// \xi \left\{ t+\frac{\delta t} {2} \right\} &= \xi \left\{ t+\frac{\delta t} {4} \right\} + G_\mathrm{new} \frac{\delta t}{4}
52///
53/// \end{align*}
54/// ```
55///
56/// # Warning
57///
58/// [`MartynaTuckermanTobiasKlein`] fails to sample the correct distribution when there are
59/// strong harmonic interactions in the system. In such situations, use
60/// [`Bussi`] or [`NoséHooverChain`] instead.
61///
62/// [`Bussi`]: crate::thermostat::Bussi
63/// [`NoséHooverChain`]: crate::thermostat::NoséHooverChain
64///
65/// # References
66/// * [Tuckerman et al. 2006](https://doi.org/10.1088/0305-4470/39/19/S18)
67/// * [Martyna et al. 1994](https://doi.org/10.1063/1.467468)
68///
69/// # Example
70///
71/// ```
72/// use hoomd_md::thermostat::MartynaTuckermanTobiasKlein;
73///
74/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
75/// let thermostat = MartynaTuckermanTobiasKlein::zero(0.5.try_into()?);
76/// # Ok(())
77/// # }
78/// ```
79#[doc(alias = "mttk")]
80#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
81pub struct MartynaTuckermanTobiasKlein {
82 /// Thermostat time constant.
83 tau: PositiveReal,
84 /// Thermostat velocity.
85 xi: f64,
86 /// Thermostat position.
87 eta: f64,
88 /// Energy the thermostat contributes to the Hamiltonian.
89 energy: f64,
90}
91
92impl MartynaTuckermanTobiasKlein {
93 /// Construct a new `MartynaTuckermanTobiasKlein` thermostat with the given time constant,
94 /// $` \xi = 0 `$, and $` \eta = 0 `$ .
95 ///
96 /// This initial condition is likely to be very far from equilibrium which
97 /// will result in wild kinetic energy oscillations for the first hundred to
98 /// thousand time steps. Use [`thermalized`] to choose the initial position
99 /// and momentum from a thermal distribution.
100 ///
101 /// [`thermalized`]: Self::thermalized
102 ///
103 /// # Example
104 ///
105 /// ```
106 /// use hoomd_md::thermostat::MartynaTuckermanTobiasKlein;
107 ///
108 /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
109 /// let thermostat = MartynaTuckermanTobiasKlein::zero(0.5.try_into()?);
110 /// # Ok(())
111 /// # }
112 /// ```
113 #[inline]
114 pub fn zero(tau: PositiveReal) -> Self {
115 Self {
116 tau,
117 xi: 0.0,
118 eta: 0.0,
119 energy: 0.0,
120 }
121 }
122
123 /// Construct a new `MartynaTuckermanTobiasKlein` thermostat with a random $` \xi `$
124 /// drawn from a thermal distribution.
125 ///
126 /// # Panics
127 ///
128 /// This method will panic when `degrees_of_freedom` is 0.
129 ///
130 /// # Example
131 ///
132 /// ```
133 /// use hoomd_md::{
134 /// TranslationalKineticEnergy, thermostat::MartynaTuckermanTobiasKlein,
135 /// };
136 /// use hoomd_microstate::{
137 /// Body, Microstate,
138 /// property::{DynamicPoint, Point},
139 /// };
140 /// use hoomd_simulation::macrostate::Isothermal;
141 /// use hoomd_vector::Cartesian;
142 ///
143 /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
144 /// let mut microstate = Microstate::builder()
145 /// .bodies([
146 /// Body::single_site(
147 /// DynamicPoint {
148 /// position: Cartesian::from([1.0, 2.0]),
149 /// ..Default::default()
150 /// },
151 /// Point::default(),
152 /// ),
153 /// Body::single_site(
154 /// DynamicPoint {
155 /// position: Cartesian::from([-2.0, 3.0]),
156 /// ..Default::default()
157 /// },
158 /// Point::default(),
159 /// ),
160 /// ])
161 /// .try_build()?;
162 ///
163 /// let macrostate = Isothermal { temperature: 1.5 };
164 /// let mut rng = microstate.counter().make_rng();
165 /// let translational_thermostat = MartynaTuckermanTobiasKlein::thermalized(
166 /// &mut rng,
167 /// 0.5.try_into()?,
168 /// ¯ostate,
169 /// microstate.translational_kinetic_energy().1,
170 /// );
171 /// microstate.increment_substep();
172 /// # Ok(())
173 /// # }
174 /// ```
175 #[inline]
176 pub fn thermalized<M, R: Rng + ?Sized>(
177 rng: &mut R,
178 tau: PositiveReal,
179 macrostate: &M,
180 degrees_of_freedom: usize,
181 ) -> Self
182 where
183 M: Temperature,
184 {
185 let sigma = 1.0 / (degrees_of_freedom as f64 * tau.get().powi(2));
186
187 let xi = Normal::new(0.0, sigma.sqrt())
188 .expect("Normal distribution should be valid")
189 .sample(rng);
190
191 let mut result = Self {
192 tau,
193 xi,
194 eta: 0.0,
195 energy: 0.0,
196 };
197
198 result.energy = result.thermostat_energy(*macrostate.temperature(), degrees_of_freedom);
199
200 result
201 }
202
203 /// Calculate the thermostats energy.
204 #[inline]
205 fn thermostat_energy(&self, temperature_set_point: f64, degrees_of_freedom: usize) -> f64 {
206 (degrees_of_freedom as f64)
207 * temperature_set_point
208 * (self.eta + 0.5 * (self.xi * self.tau.get()).powi(2))
209 }
210
211 /// The total energy of the thermostat.
212 ///
213 /// # Example
214 ///
215 /// ```
216 /// use hoomd_md::thermostat::MartynaTuckermanTobiasKlein;
217 ///
218 /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
219 /// let thermostat = MartynaTuckermanTobiasKlein::zero(0.5.try_into()?);
220 ///
221 /// let energy = thermostat.energy();
222 /// # Ok(())
223 /// # }
224 /// ```
225 #[inline]
226 pub fn energy(&self) -> f64 {
227 self.energy
228 }
229
230 /// The thermostat's position.
231 ///
232 /// # Example
233 ///
234 /// ```
235 /// use hoomd_md::thermostat::MartynaTuckermanTobiasKlein;
236 ///
237 /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
238 /// let thermostat = MartynaTuckermanTobiasKlein::zero(0.5.try_into()?);
239 ///
240 /// let eta = thermostat.eta();
241 /// # Ok(())
242 /// # }
243 /// ```
244 #[inline]
245 pub fn eta(&self) -> f64 {
246 self.eta
247 }
248
249 /// The thermostat's momentum.
250 ///
251 /// # Example
252 ///
253 /// ```
254 /// use hoomd_md::thermostat::MartynaTuckermanTobiasKlein;
255 ///
256 /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
257 /// let thermostat = MartynaTuckermanTobiasKlein::zero(0.5.try_into()?);
258 ///
259 /// let xi = thermostat.xi();
260 /// # Ok(())
261 /// # }
262 /// ```
263 #[inline]
264 pub fn xi(&self) -> f64 {
265 self.xi
266 }
267}
268
269impl<M> Thermostat<M> for MartynaTuckermanTobiasKlein
270where
271 M: Temperature,
272{
273 #[inline]
274 fn integrate_half_step_one<R: Rng + ?Sized>(
275 &mut self,
276 _rng: &mut R,
277 macrostate: &M,
278 delta_t: f64,
279 kinetic_energy: f64,
280 degrees_of_freedom: usize,
281 ) -> f64 {
282 // Integrate extra degrees-of-freedom and return the
283 // velocity rescaling factor, following Tuckerman's work
284 // https://doi.org/10.1088/0305-4470/39/19/S18.
285
286 let kinetic_temperature = 2.0 * kinetic_energy / (degrees_of_freedom as f64);
287 let g = (kinetic_temperature / *macrostate.temperature() - 1.0) / self.tau.get().powi(2);
288 let xi_quarter = self.xi + 0.25 * g * delta_t;
289 let rescaling_factor = (-0.5 * xi_quarter * delta_t).exp();
290
291 let kinetic_temperature_new = kinetic_temperature * (rescaling_factor).powi(2);
292 self.eta += 0.5 * xi_quarter * delta_t;
293 let g_new =
294 (kinetic_temperature_new / *macrostate.temperature() - 1.0) / self.tau.get().powi(2);
295 self.xi = xi_quarter + 0.25 * g_new * delta_t;
296
297 // Cache the thermostat energy so that users do not have the opportunity
298 // to provide incorrect temperature or degree of freedom values when
299 // logging the thermostat's energy.
300 self.energy = self.thermostat_energy(*macrostate.temperature(), degrees_of_freedom);
301 rescaling_factor
302 }
303
304 #[inline]
305 fn integrate_half_step_two<R: Rng + ?Sized>(
306 &mut self,
307 rng: &mut R,
308 macrostate: &M,
309 delta_t: f64,
310 kinetic_energy: f64,
311 degrees_of_freedom: usize,
312 ) -> f64 {
313 self.integrate_half_step_one(rng, macrostate, delta_t, kinetic_energy, degrees_of_freedom)
314 }
315}
316
317#[cfg(test)]
318mod tests {
319 use super::*;
320 use assert2::check;
321
322 use crate::TranslationalKineticEnergy;
323 use hoomd_microstate::{
324 Body, Microstate,
325 property::{DynamicPoint, Point},
326 };
327 use hoomd_simulation::macrostate::Isothermal;
328 use hoomd_vector::Cartesian;
329
330 #[test]
331 fn test_zero() -> anyhow::Result<()> {
332 let thermostat = MartynaTuckermanTobiasKlein::zero(0.5.try_into()?);
333
334 check!(thermostat.tau.get() == 0.5);
335 check!(thermostat.xi() == 0.0);
336 check!(thermostat.eta() == 0.0);
337 check!(thermostat.energy() == 0.0);
338
339 Ok(())
340 }
341
342 #[test]
343 fn test_thermalized() -> anyhow::Result<()> {
344 let microstate = Microstate::builder()
345 .bodies([
346 Body {
347 properties: DynamicPoint {
348 position: Cartesian::from([1.0, 2.0]),
349 ..Default::default()
350 },
351 sites: vec![Point::default()],
352 },
353 Body {
354 properties: DynamicPoint {
355 position: Cartesian::from([-2.0, 3.0]),
356 ..Default::default()
357 },
358 sites: vec![Point::default()],
359 },
360 ])
361 .try_build()?;
362
363 let macrostate = Isothermal { temperature: 1.5 };
364 let mut rng = microstate.counter().make_rng();
365 let thermostat = MartynaTuckermanTobiasKlein::thermalized(
366 &mut rng,
367 0.5.try_into()?,
368 ¯ostate,
369 microstate.translational_kinetic_energy().1,
370 );
371
372 check!(thermostat.tau.get() == 0.5);
373 check!(thermostat.xi() != 0.0);
374 check!(thermostat.eta() == 0.0);
375 check!(thermostat.energy() != 0.0);
376
377 Ok(())
378 }
379}