Coverage for openxps/system.py: 71%
35 statements
« prev ^ index » next coverage.py v7.11.3, created at 2025-11-13 22:08 +0000
« prev ^ index » next coverage.py v7.11.3, created at 2025-11-13 22:08 +0000
1"""
2.. module:: openxps.system
3 :platform: Linux, MacOS, Windows
4 :synopsis: System for extended phase-space simulations with OpenMM.
6.. classauthor:: Charlles Abreu <craabreu@gmail.com>
8"""
10import typing as t
12import numpy as np
13import openmm as mm
14from openmm import unit as mmunit
16from .couplings import Coupling
17from .dynamical_variable import DynamicalVariable
20class ExtendedSpaceSystem(mm.System):
21 """An :OpenMM:`System` object that includes extra dynamical variables (DVs) and
22 allows for extended phase-space (XPS) simulations.
24 Parameters
25 ----------
26 system
27 The :OpenMM:`System` to be used in the XPS simulation.
28 coupling
29 A :class:`Coupling` object, required to couple the physical and extended
30 phase-space systems. The dynamical variables are obtained from this coupling.
32 Keyword Arguments
33 ------------------
34 tether_period
35 The period of oscillation of a harmonic potential that tethers the y and z
36 coordinates of the extension system particles to the origin in the yz-plane.
37 This is not necessary in typical XPS simulations.
39 Example
40 -------
41 >>> import openxps as xps
42 >>> from math import pi
43 >>> import openmm
44 >>> import cvpack
45 >>> from openmm import unit
46 >>> from openmmtools import testsystems
47 >>> model = testsystems.AlanineDipeptideVacuum()
48 >>> mass = 3 * unit.dalton*(unit.nanometer/unit.radian)**2
49 >>> phi0 = xps.DynamicalVariable("phi0", unit.radian, mass, xps.CircularBounds())
50 >>> harmonic_force = xps.HarmonicCoupling(
51 ... cvpack.Torsion(6, 8, 14, 16, name="phi"),
52 ... phi0,
53 ... 1000 * unit.kilojoules_per_mole / unit.radian**2,
54 ... )
55 >>> system = xps.ExtendedSpaceSystem(model.system, harmonic_force)
56 >>> system.getDynamicalVariables()
57 (DynamicalVariable(name='phi0', unit=rad, mass=3 nm**2 Da/(rad**2), bounds=...),)
58 >>> system.getExtensionSystem().getNumParticles()
59 1
60 """
62 def __init__(
63 self,
64 system: mm.System,
65 coupling: Coupling,
66 *,
67 tether_period: t.Optional[mmunit.Quantity] = None,
68 ) -> None:
69 self._coupling = coupling
70 coupling.addToPhysicalSystem(system)
71 self.this = system.this
72 self._extension_system = mm.System()
73 for dv in coupling.getDynamicalVariables():
74 self._extension_system.addParticle(dv.mass / dv.mass.unit)
75 coupling.addToExtensionSystem(self._extension_system)
76 if tether_period is not None:
77 self._tethering_force = self._create_tethering_force(tether_period)
78 self._extension_system.addForce(self._tethering_force)
79 else:
80 self._tethering_force = None
82 def _create_tethering_force(
83 self, tether_period: mmunit.Quantity
84 ) -> mm.CustomExternalForce:
85 """Create a force tethering all particles to the origin in the yz-plane."""
86 tethering_force = mm.CustomExternalForce(
87 "0.5*kappa*(y^2 + z^2); kappa=mass_4_pi_sq/tether_period^2"
88 )
89 tethering_force.setName("Tethering")
90 tethering_force.addGlobalParameter("tether_period", tether_period)
91 tethering_force.addPerParticleParameter("mass_4_pi_sq")
92 for index, dv in enumerate(self._coupling.getDynamicalVariables()):
93 tethering_force.addParticle(index, [4 * np.pi**2 * dv.mass / dv.mass.unit])
94 return tethering_force
96 def getDynamicalVariables(self) -> tuple[DynamicalVariable]:
97 """
98 Get the dynamical variables included in the extended phase-space system.
100 Returns
101 -------
102 t.Tuple[DynamicalVariable]
103 A tuple containing the dynamical variables.
104 """
105 return tuple(self._coupling.getDynamicalVariables())
107 def getCoupling(self) -> Coupling:
108 """
109 Get the coupling included in the extended phase-space system.
111 Returns
112 -------
113 Coupling
114 The coupling.
115 """
116 return self._coupling
118 def getExtensionSystem(self) -> mm.System:
119 """
120 Get the extension system included in the extended phase-space system.
122 Returns
123 -------
124 mm.System
125 The extension system.
126 """
127 return self._extension_system
129 def getTetheringForce(self) -> t.Optional[mm.CustomExternalForce]:
130 """
131 Get the tethering force included in the extended phase-space system.
133 If no tethering force has been added, returns None.
135 Returns
136 -------
137 t.Optional[mm.CustomExternalForce]
138 The tethering force.
139 """
140 return self._tethering_force