Coverage for ufedmm/integrators.py: 76%
319 statements
« prev ^ index » next coverage.py v7.4.1, created at 2024-02-02 04:20 +0000
« prev ^ index » next coverage.py v7.4.1, created at 2024-02-02 04:20 +0000
1"""
2.. module:: integrators
3 :platform: Unix, Windows
4 :synopsis: Unified Free Energy Dynamics Integrators
6.. moduleauthor:: Charlles Abreu <abreu@eq.ufrj.br>
7"""
9import numpy as np
10import openmm
11from openmm import unit
13from ufedmm.ufedmm import _standardized
16def add_inner_nonbonded_force(system, inner_switch, inner_cutoff, force_group_index):
17 """
18 Adds a new force group to an :OpenMM:`System` containing a :OpenMM:`NonbondedForce`
19 object, with the purpose of performing multiple time-scale integration via the
20 RESPA2 splitting scheme of Morrone, Zhou, and Berne :cite:`Morrone_2010`. Also,
21 assigns the provided `force_group_index` to this new group and `force_group_index+1`
22 to the original :OpenMM:`NonbondedForce`. When used in an instance of any subclass
23 of :class:`AbstractMiddleRespaIntegrator`, the new force group must be set as
24 embodied by the :OpenMM:`NonbondedForce` as opposed to being complimentary to it.
26 .. warning:
27 The new force group is not intended to contribute to the system energy. Its sole
28 purpose is to provide a smooth, short-range force calculator for some
29 intermediary time scale in a RESPA-type integration.
31 Parameters
32 ----------
33 system : openmm.System
34 The system the inner force will be added to, which must contain a
35 :OpenMM:`NonbondedForce`.
36 inner_switch : float or unit.Quantity
37 The inner switching distance, where the interaction of an atom pair begins
38 to switch off to zero.
39 inner_cutoff : float or unit.Quantity
40 The inner cutoff distance, where the interaction of an atom pairs completely
41 switches off.
42 force_group_index : int
43 The force group the new interactions will belong to. The old
44 :OpenMM:`NonbondedForce` will be automatically assigned to
45 `force_group_index+1`.
47 Example
48 -------
49 >>> import ufedmm
50 >>> from openmm import unit
51 >>> dt = 2*unit.femtoseconds
52 >>> temp = 300*unit.kelvin
53 >>> tau = 10*unit.femtoseconds
54 >>> gamma = 10/unit.picoseconds
55 >>> model = ufedmm.AlanineDipeptideModel()
56 >>> ufedmm.add_inner_nonbonded_force(
57 ... model.system, 5*unit.angstroms, 8*unit.angstroms, 1
58 ... )
59 """
60 if openmm.__version__ < "7.5":
61 raise Exception("add_inner_nonbonded_force requires OpenMM version >= 7.5")
62 try:
63 nonbonded_force = next(
64 filter(lambda f: isinstance(f, openmm.NonbondedForce), system.getForces())
65 )
66 except StopIteration:
67 raise Exception("add_inner_nonbonded_force requires system with NonbondedForce")
68 if (
69 nonbonded_force.getNumParticleParameterOffsets() > 0
70 or nonbonded_force.getNumExceptionParameterOffsets() > 0
71 ):
72 raise Exception("add_inner_nonbonded_force does not support parameter offsets")
73 periodic = nonbonded_force.usesPeriodicBoundaryConditions()
74 rs = _standardized(inner_switch)
75 rc = _standardized(inner_cutoff)
76 a = rc + rs
77 b = rc * rs
78 c = (30 / (rc - rs) ** 5) * np.array([b**2, -2 * a * b, a**2 + 2 * b, -2 * a, 1])
79 f0s = sum([c[n] * rs ** (n + 1) / (n + 1) for n in range(5)])
81 def coeff(n, m):
82 return c[m - 1] if m == n else c[m - 1] / (m - n)
84 def func(n, m):
85 return "*log(r)" if m == n else (f"*r^{m-n}" if m > n else f"/r^{n-m}")
87 def val(n, m):
88 return f0s if m == 0 else (coeff(n, m) - coeff(0, m) if n != m else coeff(n, m))
90 def sgn(n, m):
91 return "+" if m > 0 and val(n, m) >= 0 else ""
93 def S(n):
94 return "".join(f"{sgn(n, m)}{val(n, m)}{func(n, m)}" for m in range(6))
96 potential = "eps4*((sigma/r)^12-(sigma/r)^6)+Qprod/r"
97 potential += (
98 f"+step(r-{rs})*(eps4*(sigma^12*({S(12)})-sigma^6*({S(6)}))+Qprod*({S(1)}))"
99 )
100 mixing_rules = "; Qprod=Q1*Q2"
101 mixing_rules += "; sigma=halfsig1+halfsig2"
102 mixing_rules += "; eps4=sqrt4eps1*sqrt4eps2"
103 force = openmm.CustomNonbondedForce(potential + mixing_rules)
104 for parameter in ["Q", "halfsig", "sqrt4eps"]:
105 force.addPerParticleParameter(parameter)
106 force.setNonbondedMethod(
107 force.CutoffPeriodic if periodic else force.CutoffNonPeriodic
108 )
109 force.setCutoffDistance(inner_cutoff)
110 force.setUseLongRangeCorrection(False)
111 ONE_4PI_EPS0 = 138.93545764438198
112 for index in range(nonbonded_force.getNumParticles()):
113 charge, sigma, epsilon = map(
114 _standardized, nonbonded_force.getParticleParameters(index)
115 )
116 force.addParticle(
117 [charge * np.sqrt(ONE_4PI_EPS0), sigma / 2, np.sqrt(4 * epsilon)]
118 )
119 non_exclusion_exceptions = []
120 for index in range(nonbonded_force.getNumExceptions()):
121 i, j, q1q2, sigma, epsilon = nonbonded_force.getExceptionParameters(index)
122 q1q2, sigma, epsilon = map(_standardized, [q1q2, sigma, epsilon])
123 force.addExclusion(i, j)
124 if q1q2 != 0.0 or epsilon != 0.0:
125 non_exclusion_exceptions.append(
126 (i, j, q1q2 * ONE_4PI_EPS0, sigma, 4 * epsilon)
127 )
128 force.setForceGroup(force_group_index)
129 system.addForce(force)
130 if non_exclusion_exceptions:
131 exceptions = openmm.CustomBondForce(f"step({rc}-r)*({potential})")
132 for parameter in ["Qprod", "sigma", "eps4"]:
133 exceptions.addPerBondParameter(parameter)
134 for i, j, Qprod, sigma, eps4 in non_exclusion_exceptions:
135 exceptions.addBond(i, j, [Qprod, sigma, eps4])
136 exceptions.setForceGroup(force_group_index)
137 system.addForce(exceptions)
138 nonbonded_force.setForceGroup(force_group_index + 1)
141class CustomIntegrator(openmm.CustomIntegrator):
142 """An extension of :OpenMM:`CustomIntegrator` class with an extra per-dof variable
143 named `temperature`, whose content is the temperature of the heat bath associated to
144 each degree of freedom. A per-dof temperature is necessary if the extended-space
145 variables and the physical system are coupled adiabatically to thermostats at
146 different temperatures. Otherwise, any other OpenMM integrator can be used.
148 Parameters
149 ----------
150 temperature : float or unit.Quantity
151 The temperature.
152 step_size : float or unit.Quantity
153 The step size with which to integrate the equations of motion.
154 """
156 def __init__(self, temperature, step_size):
157 super().__init__(step_size)
158 self.temperature = temperature
159 self.addPerDofVariable("kT", unit.MOLAR_GAS_CONSTANT_R * temperature)
160 self._up_to_date = False
162 def __repr__(self):
163 """A human-readable version of each integrator step (adapted from openmmtools)
165 Returns
166 -------
167 readable_lines : str
168 A list of human-readable versions of each step of the integrator
169 """
170 readable_lines = []
172 self.getNumPerDofVariables() > 0 and readable_lines.append("Per-dof variables:")
173 per_dof = []
174 for index in range(self.getNumPerDofVariables()):
175 per_dof.append(self.getPerDofVariableName(index))
176 readable_lines.append(" " + ", ".join(per_dof))
178 self.getNumGlobalVariables() > 0 and readable_lines.append("Global variables:")
179 for index in range(self.getNumGlobalVariables()):
180 name = self.getGlobalVariableName(index)
181 value = self.getGlobalVariable(index)
182 readable_lines.append(f" {name} = {value}")
184 readable_lines.append("Computation steps:")
186 step_type_str = [
187 "{target} <- {expr}",
188 "{target} <- {expr}",
189 "{target} <- sum({expr})",
190 "constrain positions",
191 "constrain velocities",
192 "allow forces to update the context state",
193 "if ({expr}):",
194 "while ({expr}):",
195 "end",
196 ]
197 indent_level = 0
198 for step in range(self.getNumComputations()):
199 line = ""
200 step_type, target, expr = self.getComputationStep(step)
201 if step_type == 8:
202 indent_level -= 1
203 command = step_type_str[step_type].format(target=target, expr=expr)
204 line += "{:4d}: ".format(step) + " " * indent_level + command
205 if step_type in [6, 7]:
206 indent_level += 1
207 readable_lines.append(line)
208 return "\n".join(readable_lines)
210 def update_temperatures(self, system_temperature, extended_space_temperatures):
211 nparticles = len(self.getPerDofVariableByName("kT")) - len(
212 extended_space_temperatures
213 )
214 temperatures = [system_temperature] * nparticles + extended_space_temperatures
215 kT = [
216 unit.MOLAR_GAS_CONSTANT_R * T * openmm.Vec3(1, 1, 1) for T in temperatures
217 ]
218 self.setPerDofVariableByName("kT", kT)
219 self._up_to_date = True
221 def step(self, steps):
222 if not self._up_to_date:
223 self.update_temperatures(self.temperature, [])
224 super().step(steps)
227class AbstractMiddleRespaIntegrator(CustomIntegrator):
228 """An abstract class for middle-type, multiple time-scale integrators.
230 .. warning::
231 This class is meant for inheritance only and does not actually include
232 thermostatting. Concrete subclasses are available, such as
233 :class:`MiddleMassiveNHCIntegrator` and :class:`GeodesicLangevinIntegrator`,
234 for instance.
236 Child classes will differ by the thermostat algorithm, which must be implemented
237 by overriding the `_bath` method (see the example below).
238 Temperature is treated as a per-dof parameter so as to allow adiabatic simulations.
240 The following :term:`ODE` system is solved for every degree of freedom in the
241 system, with possibly :math:`n_c` holonomic constraints and with forces possibly
242 split into :math:`m` parts according to their characteristic time scales:
244 .. math::
245 & \\dot{r}_i = v_i \\\\
246 & \\dot{v}_i = \\frac{\\sum_{k=1}^m F_i^{[k]}}{m_i}
247 + \\sum_{k=1}^{n_c} \\lambda_k \\nabla_{r_i} \\sigma_k
248 + \\mathrm{bath}(T_i, v_i) \\\\
249 & \\sigma_k(\\mathbf{r}) = 0
251 An approximate solution is obtained by applying the Trotter-Suzuki splitting
252 formula. In the particular case of two time scales, the default splitting scheme
253 goes as follows:
255 .. math::
256 e^{\\Delta t\\mathcal{L}} =
257 e^{\\frac{\\Delta t}{2}\\mathcal{L}^{[1]}_v}
258 \\left[
259 e^{\\frac{\\Delta t}{2 n_0}\\mathcal{L}^{[0]}_v}
260 \\left(
261 e^{\\frac{\\Delta t}{2 n_0 n_b}\\mathcal{L}_r}
262 e^{\\frac{\\Delta t}{n_0 n_b}\\mathcal{L}_\\mathrm{bath}}
263 e^{\\frac{\\Delta t}{2 n_0 n_b}\\mathcal{L}_r}
264 \\right)^{n_b}
265 e^{\\frac{\\Delta t}{2 n_0}\\mathcal{L}^{[0]}_v}
266 \\right]^{n_0}
267 e^{\\frac{\\Delta t}{2}\\mathcal{L}^{[1]}_v}
269 Each exponential operator is the solution of a particular subsystem of equations.
270 If :math:`\\mathrm{bath}(T_i, v_i) = 0`, the scheme above is time-reversible,
271 measure-preserving, and symplectic. It is referred to as the ``VV-Middle`` scheme
272 :cite:`Zhang_2019`, where VV stands for Velocity Verlet. An alternative approach
273 is also available, which is:
275 .. math::
276 e^{\\Delta t\\mathcal{L}} =
277 \\left[
278 \\left(
279 e^{\\frac{\\Delta t}{2 n_0 n_b}\\mathcal{L}_r}
280 e^{\\frac{\\Delta t}{n_0 n_b}\\mathcal{L}_\\mathrm{bath}}
281 e^{\\frac{\\Delta t}{2 n_0 n_b}\\mathcal{L}_r}
282 \\right)^{n_b}
283 e^{\\frac{\\Delta t}{n_0}\\mathcal{L}^{[0]}_v}
284 \\right]^{n_0}
285 e^{\\Delta t \\mathcal{L}^{[1]}_v}
287 This is referred to as the ``LF-Middle`` scheme :cite:`Zhang_2019`, where LF stands
288 for Leap-Frog. In contrast to the previous scheme, it is not time-reversible.
289 However, in single time-scale simulations, the two approaches result in equivalent
290 coordinate trajectories, while the latter provides a velocity trajectory more
291 consistent with the Maxwell-Boltzmann distribution at the specified temperature
292 :cite:`Zhang_2019`.
294 Parameters
295 ----------
296 temperature : unit.Quantity
297 The temperature of the heat bath.
298 step_size : float or unit.Quantity
299 The outer step size with which to integrate the equations of motion.
301 Keyword Args
302 ------------
303 num_rattles : int, default=0
304 The number of RATTLE computations for geodesic integration
305 :cite:`Leimkuhler_2016`. If ``num_rattles=0``, then no constraints are
306 considered at all.
307 scheme : str, default='VV-Middle'
308 Which splitting scheme will be used. Valid options are 'VV-Middle' and
309 'LF-Middle'.
310 respa_loops : list(int), default=[1]
311 A list of `m` integers, where `respa_loops[k]` determines how many substeps
312 with force group `k` are internally executed for every step with force group
313 `k+1`.
314 bath_loops : int, default=1
315 The number of iterations of the bath operator per each step at time scale
316 `0`. This is useful when the bath operator is not exact, but derived from a
317 splitting solution.
318 embodied_force_groups : list(int), default=[]
319 A list of indices of force groups. The presence of an index `i` is this list
320 means that the contribution of force group `i` is embodied in force group
321 `i+1`. Therefore, such contribution must be properly subtracted during the
322 integration at time scale `i+1`. This feature requires OpenMM 7.5 or a newer
323 version.
324 unroll_loops : bool, default=True
325 Whether the integrator loops should be unrolled for improving efficiency.
326 Using ``unroll_loops=False`` can be useful for printing the integrator
327 steps.
329 Example
330 -------
331 >>> from ufedmm import integrators
332 >>> from openmm import unit
333 >>> class MiddleNoseHooverIntegrator(integrators.AbstractMiddleRespaIntegrator):
334 ... def __init__(self, ndof, tau, temperature, step_size, num_rattles=1):
335 ... super().__init__(
336 ... temperature, step_size, num_rattles, 'VV-Middle', [1], 1
337 ... )
338 ... kB = 8.3144626E-3*unit.kilojoules_per_mole/unit.kelvin
339 ... gkT = ndof*unit.MOLAR_GAS_CONSTANT_R*temperature
340 ... self.addGlobalVariable('gkT', gkT)
341 ... self.addGlobalVariable('Q', gkT*tau**2)
342 ... self.addGlobalVariable('v_eta', 0)
343 ... self.addGlobalVariable('twoK', 0)
344 ... self.addGlobalVariable('scaling', 1)
345 ... def _bath(self, fraction):
346 ... self.addComputeSum('twoK', 'm*v*v')
347 ... self.addComputeGlobal(
348 ... 'v_eta', f'v_eta + {0.5*fraction}*dt*(twoK - gkT)/Q'
349 ... )
350 ... self.addComputeGlobal('scaling', f'exp(-{fraction}*dt*v_eta)')
351 ... self.addComputePerDof('v', f'v*scaling')
352 ... self.addComputeGlobal(
353 ... 'v_eta', f'v_eta + {0.5*fraction}*dt*(scaling^2*twoK - gkT)/Q'
354 ... )
355 >>> integrator = MiddleNoseHooverIntegrator(
356 ... 500, 10*unit.femtoseconds, 300*unit.kelvin,
357 ... 1*unit.femtoseconds, num_rattles=0
358 ... )
359 >>> print(integrator)
360 Per-dof variables:
361 kT
362 Global variables:
363 gkT = 1247.169392722986
364 Q = 0.1247169392722986
365 v_eta = 0.0
366 twoK = 0.0
367 scaling = 1.0
368 Computation steps:
369 0: allow forces to update the context state
370 1: v <- v + 0.5*dt*f/m
371 2: x <- x + 0.5*dt*v
372 3: twoK <- sum(m*v*v)
373 4: v_eta <- v_eta + 0.5*dt*(twoK - gkT)/Q
374 5: scaling <- exp(-1.0*dt*v_eta)
375 6: v <- v*scaling
376 7: v_eta <- v_eta + 0.5*dt*(scaling^2*twoK - gkT)/Q
377 8: x <- x + 0.5*dt*v
378 9: v <- v + 0.5*dt*f/m
379 """
381 def __init__(
382 self,
383 temperature,
384 step_size,
385 num_rattles=0,
386 scheme="VV-Middle",
387 respa_loops=[1],
388 bath_loops=1,
389 intertwine=True,
390 embodied_force_groups=[],
391 unroll_loops=True,
392 ):
393 if scheme not in ["LF-Middle", "VV-Middle"]:
394 raise Exception(f"Invalid value {scheme} for keyword scheme")
395 super().__init__(temperature, step_size)
396 self._num_rattles = num_rattles
397 self._scheme = scheme
398 self._respa_loops = respa_loops
399 self._bath_loops = bath_loops
400 self._intertwine = intertwine
401 self._subtractive_groups = embodied_force_groups
402 num_rattles > 0 and self.addPerDofVariable("x0", 0)
403 num_rattles > 1 and self.addGlobalVariable("irattle", 0)
404 if not unroll_loops:
405 for scale, n in enumerate(respa_loops):
406 n > 1 and self.addGlobalVariable(f"irespa{scale}", 0)
407 bath_loops > 1 and self.addGlobalVariable("ibath", 0)
408 if embodied_force_groups:
409 if openmm.__version__ < "7.5":
410 raise Exception(
411 "Use of `embodied_force_groups` option requires OpenMM >= 7.5"
412 )
413 self.addPerDofVariable("f_emb", 0)
414 integration_groups = set(range(len(respa_loops))) - set(
415 embodied_force_groups
416 )
417 self.setIntegrationForceGroups(integration_groups)
419 self.addUpdateContextState()
420 self._step_initialization()
421 if unroll_loops:
422 self._integrate_respa_unrolled(1, len(respa_loops) - 1)
423 else:
424 self._integrate_respa(1, len(respa_loops) - 1)
426 def _step_initialization(self):
427 pass
429 def _integrate_respa(self, fraction, scale):
430 if scale >= 0:
431 n = self._respa_loops[scale]
432 if n > 1:
433 self.addComputeGlobal(f"irespa{scale}", "0")
434 self.beginWhileBlock(f"irespa{scale} < {n-1/2}")
435 self._boost(fraction / (2 * n if self._scheme == "VV-Middle" else n), scale)
436 self._integrate_respa(fraction / n, scale - 1)
437 self._scheme == "VV-Middle" and self._boost(fraction / (2 * n), scale)
438 if n > 1:
439 self.addComputeGlobal(f"irespa{scale}", f"irespa{scale} + 1")
440 self.endBlock()
441 else:
442 self._intertwine or self._translation(0.5 * fraction)
443 n = self._bath_loops
444 if n > 1:
445 self.addComputeGlobal("ibath", "0")
446 self.beginWhileBlock(f"ibath < {n-1/2}")
447 self._intertwine and self._translation(0.5 * fraction / n)
448 self._bath(fraction / n)
449 self._num_rattles > 0 and self.addConstrainVelocities()
450 self._intertwine and self._translation(0.5 * fraction / n)
451 if n > 1:
452 self.addComputeGlobal("ibath", "ibath + 1")
453 self.endBlock()
454 self._intertwine or self._translation(0.5 * fraction)
456 def _integrate_respa_unrolled(self, fraction, scale):
457 if scale >= 0:
458 n = self._respa_loops[scale]
459 for i in range(n):
460 self._boost(
461 fraction / (2 * n if self._scheme == "VV-Middle" and i == 0 else n),
462 scale,
463 )
464 self._integrate_respa_unrolled(fraction / n, scale - 1)
465 self._scheme == "VV-Middle" and i == n - 1 and self._boost(
466 fraction / (2 * n), scale
467 )
468 else:
469 n = self._bath_loops
470 self._intertwine or self._translation(0.5 * fraction)
471 for i in range(n):
472 self._intertwine and self._translation(
473 fraction / (2 * n if i == 0 else n)
474 )
475 self._bath(fraction / n)
476 self._num_rattles > 0 and self.addConstrainVelocities()
477 i == n - 1 and self._intertwine and self._translation(
478 fraction / (2 * n)
479 )
480 self._intertwine or self._translation(0.5 * fraction)
482 def _translation(self, fraction):
483 if self._num_rattles > 1:
484 self.addComputeGlobal("irattle", "0")
485 self.beginWhileBlock(f"irattle < {self._num_rattles-1/2}")
486 self.addComputePerDof("x", f"x + {fraction/max(1, self._num_rattles)}*dt*v")
487 if self._num_rattles > 0:
488 self.addComputePerDof("x0", "x")
489 self.addConstrainPositions()
490 self.addComputePerDof(
491 "v", f"v + (x - x0)/({fraction/self._num_rattles}*dt)"
492 )
493 self.addConstrainVelocities()
494 if self._num_rattles > 1:
495 self.addComputeGlobal("irattle", "irattle + 1")
496 self.endBlock()
498 def _boost(self, fraction, scale):
499 if len(self._respa_loops) > 1:
500 if scale - 1 in self._subtractive_groups:
501 self.addComputePerDof("f_emb", f"f{scale-1}")
502 self.addComputePerDof("v", f"v + {fraction}*dt*(f{scale}-f_emb)/m")
503 else:
504 self.addComputePerDof("v", f"v + {fraction}*dt*f{scale}/m")
505 else:
506 self.addComputePerDof("v", f"v + {fraction}*dt*f/m")
507 self._num_rattles > 0 and self.addConstrainVelocities()
509 def _bath(self, fraction):
510 return
513class GeodesicLangevinIntegrator(AbstractMiddleRespaIntegrator):
514 """A geodesic Langevin integrator :cite:`Leimkuhler_2016`, which can be integrated
515 by using either the LF-Middle or the VV-Middle scheme :cite:`Zhang_2019`.
517 .. note:
518 The VV-Middle scheme is also known as the BAOAB :cite:`Leimkuhler_2016` method.
520 Parameters
521 ----------
522 temperature : float or unit.Quantity
523 The temperature.
524 friction_coefficient : float or unit.Quantity
525 The friction coefficient.
526 step_size : float or unit.Quantity
527 The time-step size.
529 Keyword Args
530 ------------
531 num_rattles : int, default=1
532 The number of RATTLE computations for geodesic integration
533 :cite:`Leimkuhler_2016`. If ``num_rattles=0``, then no constraints are
534 considered at all.
535 scheme : str, default='LF-Middle'
536 Which splitting scheme will be used. Valid options are 'VV-Middle' and
537 'LF-Middle'.
538 **kwargs
539 All other keyword arguments in :class:`AbstractMiddleRespaIntegrator`.
541 Example
542 -------
543 >>> import ufedmm
544 >>> dt = 2*unit.femtoseconds
545 >>> temp = 300*unit.kelvin
546 >>> gamma = 10/unit.picoseconds
547 >>> ufedmm.GeodesicLangevinIntegrator(
548 ... temp, gamma, dt, num_rattles=1, scheme='VV-Middle'
549 ... )
550 Per-dof variables:
551 kT, x0
552 Global variables:
553 friction = 10.0
554 Computation steps:
555 0: allow forces to update the context state
556 1: v <- v + 0.5*dt*f/m
557 2: constrain velocities
558 3: x <- x + 0.5*dt*v
559 4: x0 <- x
560 5: constrain positions
561 6: v <- v + (x - x0)/(0.5*dt)
562 7: constrain velocities
563 8: v <- z*v + sqrt((1 - z*z)*kT/m)*gaussian; z = exp(-friction*1.0*dt)
564 9: constrain velocities
565 10: x <- x + 0.5*dt*v
566 11: x0 <- x
567 12: constrain positions
568 13: v <- v + (x - x0)/(0.5*dt)
569 14: constrain velocities
570 15: v <- v + 0.5*dt*f/m
571 16: constrain velocities
572 """
574 def __init__(
575 self,
576 temperature,
577 friction_coefficient,
578 step_size,
579 num_rattles=1,
580 scheme="LF-Middle",
581 **kwargs,
582 ):
583 super().__init__(
584 temperature, step_size, num_rattles=num_rattles, scheme=scheme, **kwargs
585 )
586 self.addGlobalVariable("friction", friction_coefficient)
588 def _bath(self, fraction):
589 expression = (
590 f"z*v + sqrt((1 - z*z)*kT/m)*gaussian; z = exp(-friction*{fraction}*dt)"
591 )
592 self.addComputePerDof("v", expression)
595class MiddleMassiveNHCIntegrator(AbstractMiddleRespaIntegrator):
596 """A massive, middle-type Nose-Hoover Chain Thermostat solver :cite:`Martyna_1992`
597 with optional multiple time-scale integration via RESPA.
599 To enable RESPA, the forces in OpenMM system must be split into distinct force
600 groups and the keyword ``respa_loop`` (see below) must be a list with multiple
601 entries.
603 Parameters
604 ----------
605 temperature : float or unit.Quantity
606 The temperature.
607 time_constant : float or unit.Quantity
608 The characteristic time constant.
609 step_size : float or unit.Quantity
610 The time-step size.
612 Keyword Args
613 ------------
614 nchain : int, default=2
615 The number of thermostats in each Nose-Hoover chain.
616 track_energy : bool, default=False
617 Whether to track the thermostat energy term.
618 **kwargs
619 All keyword arguments in :class:`AbstractMiddleRespaIntegrator`, except
620 ``num_rattles``.
622 Example
623 -------
624 >>> import ufedmm
625 >>> temp, tau, dt = 300*unit.kelvin, 10*unit.femtoseconds, 2*unit.femtoseconds
626 >>> integrator = ufedmm.MiddleMassiveNHCIntegrator(
627 ... temp, tau, dt, respa_loops=[4, 1], unroll_loops=False
628 ... )
629 >>> print(integrator)
630 Per-dof variables:
631 kT, Q, v1, v2
632 Global variables:
633 irespa0 = 0.0
634 Computation steps:
635 0: allow forces to update the context state
636 1: v <- v + 0.5*dt*f1/m
637 2: irespa0 <- 0
638 3: while (irespa0 < 3.5):
639 4: v <- v + 0.125*dt*f0/m
640 5: x <- x + 0.125*dt*v
641 6: v2 <- v2 + 0.125*dt*(Q*v1^2 - kT)/Q
642 7: v1 <- (v1*z + 0.125*dt*(m*v^2 - kT)/Q)*z; z=exp(-0.0625*dt*v2)
643 8: v <- v*exp(-0.25*dt*v1)
644 9: v1 <- (v1*z + 0.125*dt*(m*v^2 - kT)/Q)*z; z=exp(-0.0625*dt*v2)
645 10: v2 <- v2 + 0.125*dt*(Q*v1^2 - kT)/Q
646 11: x <- x + 0.125*dt*v
647 12: v <- v + 0.125*dt*f0/m
648 13: irespa0 <- irespa0 + 1
649 14: end
650 15: v <- v + 0.5*dt*f1/m
651 """
653 def __init__(
654 self,
655 temperature,
656 time_constant,
657 step_size,
658 nchain=2,
659 track_energy=False,
660 **kwargs,
661 ):
662 if "num_rattles" in kwargs.keys() and kwargs["num_rattles"] != 0:
663 raise ValueError(f"{self.__class__.__name__} cannot handle constraints")
664 self._tau = _standardized(time_constant)
665 self._nchain = nchain
666 self._track_energy = track_energy
667 super().__init__(temperature, step_size, **kwargs)
668 self.addPerDofVariable("Q", 0)
669 for i in range(nchain):
670 self.addPerDofVariable(f"v{i+1}", 0)
671 if track_energy:
672 self.addPerDofVariable(f"eta{i+1}", 0)
674 def update_temperatures(self, system_temperature, extended_space_temperatures):
675 super().update_temperatures(system_temperature, extended_space_temperatures)
676 Q = [self._tau**2 * kT for kT in self.getPerDofVariableByName("kT")]
677 self.setPerDofVariableByName("Q", Q)
679 def _bath(self, fraction):
680 n = self._nchain
682 def a(i):
683 return f"(Q*v{i-1}^2 - kT)/Q" if i > 1 else "(m*v^2 - kT)/Q"
685 def z(i):
686 return f"exp(-{fraction/4}*dt*v{i+1})"
688 self.addComputePerDof(f"v{n}", f"v{n} + {fraction/2}*dt*{a(n)}")
689 for i in reversed(range(1, n)):
690 self.addComputePerDof(
691 f"v{i}", f"(v{i}*z + {fraction/2}*dt*{a(i)})*z; z={z(i)}"
692 )
693 self.addComputePerDof("v", f"v*exp(-{fraction}*dt*v1)")
694 for i in range(1, n):
695 self.addComputePerDof(
696 f"v{i}", f"(v{i}*z + {fraction/2}*dt*{a(i)})*z; z={z(i)}"
697 )
698 self.addComputePerDof(f"v{n}", f"v{n} + {fraction/2}*dt*{a(n)}")
701class MiddleMassiveGGMTIntegrator(AbstractMiddleRespaIntegrator):
702 """A massive, middle-type Generalized Gaussian Moment Thermostat :cite:`Liu_2000`
703 solver with optional multiple time-scale integration via RESPA.
705 To enable RESPA, the forces in OpenMM system must be split into distinct force
706 groups and the keyword ``respa_loop`` (see below) must be a list with multiple
707 entries.
709 Parameters
710 ----------
711 temperature : float or unit.Quantity
712 The temperature.
713 time_constant : float or unit.Quantity
714 The characteristic time constant.
715 step_size : float or unit.Quantity
716 The time-step size.
718 Keyword Args
719 ------------
720 **kwargs
721 All keyword arguments in :class:`AbstractMiddleRespaIntegrator`, except
722 ``num_rattles``.
724 Example
725 -------
726 >>> import ufedmm
727 >>> temp, tau, dt = 300*unit.kelvin, 10*unit.femtoseconds, 2*unit.femtoseconds
728 >>> integrator = ufedmm.MiddleMassiveGGMTIntegrator(temp, tau, dt)
729 >>> print(integrator)
730 Per-dof variables:
731 kT, Q1, Q2, v1, v2
732 Computation steps:
733 0: allow forces to update the context state
734 1: v <- v + 0.5*dt*f/m
735 2: x <- x + 0.5*dt*v
736 3: v1 <- v1 + 0.5*dt*(m*v^2 - kT)/Q1
737 4: v2 <- v2 + 0.5*dt*((m*v^2)^2/3 - kT^2)/Q2
738 5: v <- v*exp(-1.0*dt*(v1 + kT*v2))/sqrt(1 + 2.0*dt*m*v^2*v2/3)
739 6: v1 <- v1 + 0.5*dt*(m*v^2 - kT)/Q1
740 7: v2 <- v2 + 0.5*dt*((m*v^2)^2/3 - kT^2)/Q2
741 8: x <- x + 0.5*dt*v
742 9: v <- v + 0.5*dt*f/m
743 """
745 def __init__(self, temperature, time_constant, step_size, **kwargs):
746 if "num_rattles" in kwargs.keys() and kwargs["num_rattles"] != 0:
747 raise ValueError(f"{self.__class__.__name__} cannot handle constraints")
748 self._tau = _standardized(time_constant)
749 super().__init__(temperature, step_size, **kwargs)
750 self.addPerDofVariable("Q1", 0)
751 self.addPerDofVariable("Q2", 0)
752 self.addPerDofVariable("v1", 0)
753 self.addPerDofVariable("v2", 0)
755 def set_extended_space_time_constants(self, time_constants):
756 self._xs_taus = [_standardized(tau) for tau in time_constants]
758 def update_temperatures(self, system_temperature, extended_space_temperatures):
759 super().update_temperatures(system_temperature, extended_space_temperatures)
760 kT_vectors = self.getPerDofVariableByName("kT")
761 kT3_vectors = [openmm.Vec3(kT.x**3, kT.y**3, kT.z**3) for kT in kT_vectors]
762 if hasattr(self, "_xs_taus"):
763 num_particles = len(kT_vectors) - len(extended_space_temperatures)
764 taus = [self._tau] * num_particles + self._xs_taus
765 Q1 = [kT * tau**2 for kT, tau in zip(kT_vectors, taus)]
766 Q2 = [8 / 3 * kT3 * tau**2 for kT3, tau in zip(kT3_vectors, taus)]
767 else:
768 Q1 = [kT * self._tau**2 for kT in kT_vectors]
769 Q2 = [8 / 3 * kT3 * self._tau**2 for kT3 in kT3_vectors]
770 self.setPerDofVariableByName("Q1", Q1)
771 self.setPerDofVariableByName("Q2", Q2)
773 def _bath(self, fraction):
774 self.addComputePerDof("v1", f"v1 + {fraction/2}*dt*(m*v^2 - kT)/Q1")
775 self.addComputePerDof("v2", f"v2 + {fraction/2}*dt*((m*v^2)^2/3 - kT^2)/Q2")
776 self.addComputePerDof(
777 "v",
778 f"v*exp(-{fraction}*dt*(v1 + kT*v2))/sqrt(1 + {2*fraction}*dt*m*v^2*v2/3)",
779 )
780 self.addComputePerDof("v1", f"v1 + {fraction/2}*dt*(m*v^2 - kT)/Q1")
781 self.addComputePerDof("v2", f"v2 + {fraction/2}*dt*((m*v^2)^2/3 - kT^2)/Q2")
784class RegulatedNHLIntegrator(AbstractMiddleRespaIntegrator):
785 """A regulated version of the massive Nose-Hoover-Langevin
786 :cite:`Samoletov_2007,Leimkuhler_2009` method. Regulation means that the system
787 Hamiltonian is modified so that velocities remain below a temperature-dependent
788 limit. This is closely related to the SIN(R) method :cite:`Leimkuhler_2013` and
789 allows multiple time-scale integration with very large outer time steps, without
790 resonance.
792 .. info:
793 If `regulation_parameter = 1` (default), this method is equivalent to SIN(R)
794 with a single thermostat per degree of freedom (that is, `L=1`).
796 The following :term:`SDE` system is solved for every degree of freedom in the
797 system:
799 .. math::
800 & dr_i = v_i dt \\\\
801 & dp_i = F_i dt - v_{\\eta_i} m_i v_i dt \\\\
802 & dv_{\\eta_i} = \\frac{1}{Q}\\left(\\frac{n+1}{n} m_i v_i^2 - k_B T\\right) dt
803 - \\gamma v_{\\eta_i} dt + \\sqrt{\\frac{2\\gamma k_B T}{Q}} dW_i,
805 where:
807 .. math::
808 v_i = c_i \\tanh\\left(\\frac{p_i}{m_i c_i}\\right).
810 Here, :math:`n` is the regulation parameter and :math:`c_i` is the maximum speed for
811 degree of freedom `i`, given by :math:`c_i = \\sqrt{\\frac{n k T}{m_i}}`. The
812 inertial parameter :math:`Q` is defined as :math:`Q = n k_B T \\tau^2`, with
813 :math:`\\tau` being a relaxation time :cite:`Tuckerman_1992`. An approximate
814 solution is obtained by applying the Trotter-Suzuki splitting formula:
816 .. math::
817 e^{\\Delta t\\mathcal{L}} =
818 e^{\\frac{\\Delta t}{2}\\mathcal{L}^1_p}
819 \\left[e^{\\frac{\\delta t}{2}\\mathcal{L}^0_p}
820 e^{\\frac{\\delta t}{2}\\mathcal{L}_r}
821 e^{\\delta t \\mathcal{L}_\\mathrm{bath}}
822 e^{\\frac{\\delta t}{2}\\mathcal{L}_r}
823 e^{\\frac{\\delta t}{2}\\mathcal{L}^0_p}\\right]^m
824 e^{\\frac{\\Delta t}{2}\\mathcal{L}^1_p}
826 where :math:`\\delta t = \\frac{\\Delta t}{m}`. Each exponential operator above is
827 the solution of a differential equation.
829 The exact solution for the physical-system part is:
831 .. math::
832 r_i(t) = r_i^0 + c_i \\mathrm{tanh}\\left(\\frac{p_i}{m c_i}\\right) t
834 .. math::
835 p_i(t) = p_i^0 + F_i t
837 The bath propagator is further split as:
839 .. math::
840 e^{\\delta t \\mathcal{L}_\\mathrm{bath}} =
841 e^{\\frac{\\delta t}{2m}\\mathcal{L}_B}
842 e^{\\frac{\\delta t}{2m}\\mathcal{L}_S}
843 e^{\\frac{\\delta t}{m}\\mathcal{L}_O}
844 e^{\\frac{\\delta t}{2m}\\mathcal{L}_S}
845 e^{\\frac{\\delta t}{2m}\\mathcal{L}_B}
847 Part 'B' is a boost, whose solution is:
849 .. math::
850 v_{\\eta_i}(t) = v_{\\eta_i}^0 +
851 \\frac{1}{Q}\\left(\\frac{n+1}{n} m_i v_i^2 - k_B T\\right) t
853 Part 'S' is a scaling, whose solution is:
855 .. math::
856 p_i(t) = m_i c_i \\mathrm{arcsinh}\\left[
857 \\sinh\\left(\\frac{p_i^0}{m_i c_i}\\right) e^{- v_{\\eta_i} t}
858 \\right]
860 Part 'O' is an Ornstein–Uhlenbeck process, whose solution is:
862 .. math::
863 v_{\\eta_i}(t) = v_{\\eta_i}^0 e^{-\\gamma t}
864 + \\sqrt{\\frac{k_B T}{Q}(1-e^{-2\\gamma t})} R_N
866 where :math:`R_N` is a normally distributed random number.
868 Parameters
869 ----------
870 step_size : float or unit.Quantity
871 The outer step size with which to integrate the equations of motion.
872 loops : int
873 The number of internal substeps at each time step.
874 temperature : unit.Quantity
875 The temperature of the heat bath.
876 time_scale : unit.Quantity (time)
877 The relaxation time (:math:`\\tau`) of the Nose-Hoover thermostat.
878 friction_coefficient : unit.Quantity (1/time)
879 The friction coefficient (:math:`\\gamma`) of the Langevin thermostat.
880 regulation_parameter : int or float
881 The regulation parameter n.
883 Keyword Args
884 ------------
885 semi_regulated : bool, default=True
886 Whether to use the semi-regulated NHL version of the method instead of its
887 fully-regulated version.
888 split_ornstein_uhlenbeck : bool, default=True
889 Whether to split the drifted Ornstein-Uhlenbeck operator.
890 **kwargs
891 All keyword arguments in :class:`AbstractMiddleRespaIntegrator`, except
892 ``num_rattles``.
893 """
895 def __init__(
896 self,
897 temperature,
898 time_constant,
899 friction_coefficient,
900 step_size,
901 regulation_parameter,
902 semi_regulated=True,
903 split_ornstein_uhlenbeck=True,
904 **kwargs,
905 ):
906 if "num_rattles" in kwargs.keys() and kwargs["num_rattles"] != 0:
907 raise ValueError(f"{self.__class__.__name__} cannot handle constraints")
908 self._tau = np.sqrt(regulation_parameter) * time_constant
909 self._n = regulation_parameter
910 self._split = split_ornstein_uhlenbeck
911 self._semi_regulated = semi_regulated
912 super().__init__(temperature, step_size, **kwargs)
913 self.addPerDofVariable("invQ", 0)
914 self.addPerDofVariable("v_eta", 0)
915 self.addPerDofVariable("c", 0)
916 self.addGlobalVariable("friction", friction_coefficient)
917 self.addGlobalVariable("omega", 1.0 / self._tau)
918 self.addGlobalVariable("aa", 0)
919 self.addGlobalVariable("bb", 0)
921 def update_temperatures(self, system_temperature, extended_space_temperatures):
922 super().update_temperatures(system_temperature, extended_space_temperatures)
923 kT_vectors = self.getPerDofVariableByName("kT")
924 tauSq = _standardized(self._tau) ** 2
925 Q = [tauSq * kT for kT in kT_vectors]
926 invQ = [openmm.Vec3(*map(lambda x: 1 / x if x > 0.0 else 0.0, q)) for q in Q]
927 self.setPerDofVariableByName("invQ", invQ)
929 def _step_initialization(self):
930 self.addComputePerDof("c", f"sqrt({self._n}*kT/m)")
931 n = np.prod(self._respa_loops) * self._bath_loops
932 self.addComputeGlobal("aa", f"exp(-friction*dt/{n})")
933 self.addComputeGlobal("bb", "omega*sqrt(1-aa^2)")
935 def _translation(self, fraction):
936 n = self._n
937 if self._semi_regulated:
938 expression = f"0.5*m*v*c*tanh(v/c); c=sqrt({n}*kT/m)"
939 else:
940 expression = f"{0.5*(n+1)/n}*m*(c*tanh(v/c))^2; c=sqrt({n}*kT/m)"
941 self.setKineticEnergyExpression(expression)
942 self.addComputePerDof("x", f"x + c*tanh(v/c)*{fraction}*dt")
944 def _bath(self, fraction):
945 n = self._n
947 if self._semi_regulated:
948 G = "; G=(m*v*c*tanh(v/c) - kT)*invQ"
949 else:
950 G = f"; G=({(n+1)/n}*m*(c*tanh(v/c))^2 - kT)*invQ"
952 if self._split:
953 boost = f"v_eta + G*{0.5*fraction}*dt" + G
955 if self._semi_regulated:
956 scaling = f"v*exp(-v_eta*{0.5*fraction}*dt)"
957 else:
958 scaling = (
959 "c*asinh_z"
960 "; asinh_z=(2*step(z)-1)*log(select(step(za-1E8),2*za,za+sqrt(1+z*z)))"
961 "; za=abs(z)"
962 f"; z=sinh(v/c)*exp(-v_eta*{0.5*fraction}*dt)"
963 )
965 if self._split:
966 Ornstein_Uhlenbeck = "v_eta*aa + bb*gaussian"
967 else:
968 Ornstein_Uhlenbeck = "v_eta*aa + G*(1-aa)/friction + bb*gaussian" + G
970 self._split and self.addComputePerDof("v_eta", boost)
971 self.addComputePerDof("v", scaling)
972 self.addComputePerDof("v_eta", Ornstein_Uhlenbeck)
973 self.addComputePerDof("v", scaling)
974 self._split and self.addComputePerDof("v_eta", boost)