Skip to main content

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    ///     &macrostate,
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            &macrostate,
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}