hoomd_md/thermostat/
bussi.rs1use rand::Rng;
7use rand_distr::{Distribution, Gamma, Normal};
8use serde::{Deserialize, Serialize};
9
10use crate::Thermostat;
11use hoomd_simulation::macrostate::Temperature;
12
13#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
52pub struct Bussi {
53 tau: f64,
55
56 energy: f64,
58}
59
60impl Bussi {
61 #[inline]
71 pub fn new(tau: f64) -> Self {
72 Self { tau, energy: 0.0 }
73 }
74 #[inline]
76 fn energy_drift(kinetic_energy_old: f64, rescaling_factor: f64) -> f64 {
77 kinetic_energy_old * (1.0 - rescaling_factor.powi(2))
78 }
79
80 #[inline]
90 pub fn energy(&self) -> f64 {
91 self.energy
92 }
93}
94
95impl<M> Thermostat<M> for Bussi
96where
97 M: Temperature,
98{
99 #[inline]
100 fn integrate_half_step_one<R: Rng + ?Sized>(
101 &mut self,
102 rng: &mut R,
103 macrostate: &M,
104 delta_t: f64,
105 kinetic_energy: f64,
106 degrees_of_freedom: usize,
107 ) -> f64 {
108 if degrees_of_freedom == 0 {
111 return 1.0;
112 }
113
114 assert!(
115 kinetic_energy != 0.0,
116 "The Bussi thermostat requires non-zero kinetic energy."
117 );
118
119 let time_decay_factor = if self.tau == 0.0 {
120 0.0
121 } else {
122 (-delta_t / self.tau).exp()
123 };
124
125 let random_normal_one: f64 = Normal::new(0.0, 1.0)
126 .expect("normal distribution should be valid")
127 .sample(rng);
128
129 let random_gamma = if degrees_of_freedom > 0 {
130 2.0 * Gamma::new((degrees_of_freedom as f64 - 1.0) / 2.0, 1.0)
131 .expect("gamma distribution should be valid")
132 .sample(rng)
133 } else {
134 0.0
135 };
136
137 let v = macrostate.temperature() / 2.0 / kinetic_energy;
138 let term1 = v * (1.0 - time_decay_factor) * (random_gamma + random_normal_one.powi(2));
139 let term2 =
140 2.0 * random_normal_one * (v * (1.0 - time_decay_factor) * time_decay_factor).sqrt();
141 let rescaling_factor = (time_decay_factor + term1 + term2).sqrt();
142
143 self.energy += Self::energy_drift(kinetic_energy, rescaling_factor);
144 rescaling_factor
145 }
146
147 #[inline]
148 fn integrate_half_step_two<R: Rng + ?Sized>(
149 &mut self,
150 _rng: &mut R,
151 _macrostate: &M,
152 _delta_t: f64,
153 _kinetic_energy: f64,
154 _degrees_of_freedom: usize,
155 ) -> f64 {
156 1.0
157 }
158}
159
160#[cfg(test)]
161mod tests {
162 use super::*;
163 use assert2::check;
164 use rand::{SeedableRng, rngs::StdRng};
165
166 use hoomd_simulation::macrostate::Isothermal;
167
168 #[test]
169 fn test_init() {
170 let tau = 1.0;
171 let bussi = Bussi::new(tau);
172
173 check!(bussi.tau == tau);
174 check!(bussi.energy() == 0.0);
175 }
176
177 #[test]
178 fn test_scale_down() {
179 let tau = 0.0;
180 let mut bussi = Bussi::new(tau);
181
182 let mut rng = StdRng::seed_from_u64(0);
183 let macrostate = Isothermal { temperature: 0.4 };
184 let alpha = bussi.integrate_half_step_one(&mut rng, ¯ostate, 0.01, 1000.0, 100);
185
186 check!(alpha < 1.0);
187 check!(bussi.energy() > 0.0);
188 }
189
190 #[test]
191 fn test_scale_up() {
192 let tau = 0.0;
193 let mut bussi = Bussi::new(tau);
194
195 let mut rng = StdRng::seed_from_u64(0);
196 let macrostate = Isothermal { temperature: 0.4 };
197 let alpha = bussi.integrate_half_step_one(&mut rng, ¯ostate, 0.01, 1000.0, 10_000);
198
199 check!(alpha > 1.0);
200 check!(bussi.energy() < 0.0);
201 }
202}