Coverage for cvpack/collective_variable.py: 98%

58 statements  

« prev     ^ index     » next       coverage.py v7.5.1, created at 2024-05-09 16:14 +0000

1""" 

2.. class:: CollectiveVariable 

3 :platform: Linux, MacOS, Windows 

4 :synopsis: An abstract class with common attributes and method for all CVs 

5 

6.. moduleauthor:: Charlles Abreu <craabreu@gmail.com> 

7 

8""" 

9 

10import typing as t 

11 

12import openmm 

13import yaml 

14from openmm import _openmm as mmswig 

15from openmm import unit as mmunit 

16 

17from .serialization import Serializable 

18from .units import Quantity, ScalarQuantity, Unit 

19from .utils import compute_effective_mass, get_single_force_state, preprocess_args 

20 

21 

22class CollectiveVariable(Serializable): 

23 r""" 

24 An abstract class with common attributes and method for all CVs. 

25 """ 

26 

27 _unit: Unit = Unit("dimensionless") 

28 _mass_unit: Unit = Unit("dalton") 

29 _args: t.Dict[str, t.Any] = {} 

30 _periodic_bounds: t.Optional[Quantity] = None 

31 

32 def __getstate__(self) -> t.Dict[str, t.Any]: 

33 return self._args 

34 

35 def __setstate__(self, keywords: t.Dict[str, t.Any]) -> None: 

36 self.__init__(**keywords) 

37 

38 def __copy__(self) -> "CollectiveVariable": 

39 return yaml.safe_load(yaml.safe_dump(self)) 

40 

41 def __deepcopy__(self, _) -> "CollectiveVariable": 

42 return yaml.safe_load(yaml.safe_dump(self)) 

43 

44 @preprocess_args 

45 def _registerCV( 

46 self, 

47 name: str, 

48 cvUnit: Unit, 

49 **kwargs: t.Any, 

50 ) -> None: 

51 """ 

52 Register the newly created CollectiveVariable subclass instance. 

53 

54 This method must always be called from Subclass.__init__. 

55 

56 Parameters 

57 ---------- 

58 name 

59 The name of this collective variable. 

60 unit 

61 The unit of measurement of this collective variable. It must be a unit 

62 in the MD unit system (mass in Da, distance in nm, time in ps, 

63 temperature in K, energy in kJ/mol, angle in rad). 

64 kwargs 

65 The keyword arguments needed to construct this collective variable 

66 """ 

67 mmswig.Force_setName(self, name) 

68 self._unit = cvUnit 

69 self._mass_unit = Unit(mmunit.dalton * (mmunit.nanometers / self._unit) ** 2) 

70 self._args = {"name": name} 

71 self._args.update(kwargs) 

72 

73 def _registerPeriodicBounds( 

74 self, lower: ScalarQuantity, upper: ScalarQuantity 

75 ) -> None: 

76 """ 

77 Register the periodic bounds of this collective variable. 

78 

79 This method must called from Subclass.__init__ if the collective variable is 

80 periodic. 

81 

82 Parameters 

83 ---------- 

84 lower 

85 The lower bound of this collective variable 

86 upper 

87 The upper bound of this collective variable 

88 """ 

89 if mmunit.is_quantity(lower): 

90 lower = lower.value_in_unit(self.getUnit()) 

91 if mmunit.is_quantity(upper): 

92 upper = upper.value_in_unit(self.getUnit()) 

93 self._periodic_bounds = Quantity((lower, upper), self.getUnit()) 

94 

95 def _setUnusedForceGroup(self, system: openmm.System) -> None: 

96 """ 

97 Set the force group of this collective variable to the one at a given position 

98 in the ascending ordered list of unused force groups in an :OpenMM:`System`. 

99 

100 .. note:: 

101 

102 Evaluating a collective variable (see :meth:`getValue`) or computing its 

103 effective mass (see :meth:`getEffectiveMass`) is more efficient when the 

104 collective variable is the only force in its own force group. 

105 

106 Parameters 

107 ---------- 

108 system 

109 The system to search for unused force groups 

110 

111 Raises 

112 ------ 

113 RuntimeError 

114 If all force groups are already in use 

115 """ 

116 used_groups = {force.getForceGroup() for force in system.getForces()} 

117 new_group = next(filter(lambda i: i not in used_groups, range(32)), None) 

118 if new_group is None: 

119 raise RuntimeError("All force groups are already in use.") 

120 mmswig.Force_setForceGroup(self, new_group) 

121 

122 def getName(self) -> str: # pylint: disable=useless-parent-delegation 

123 """ 

124 Get the name of this collective variable. 

125 """ 

126 return mmswig.Force_getName(self) 

127 

128 def getUnit(self) -> Unit: 

129 """ 

130 Get the unit of measurement of this collective variable. 

131 """ 

132 return self._unit 

133 

134 def getMassUnit(self) -> Unit: 

135 """ 

136 Get the unit of measurement of the effective mass of this collective variable. 

137 """ 

138 return self._mass_unit 

139 

140 def getPeriodicBounds(self) -> t.Union[Quantity, None]: 

141 """ 

142 Get the periodic bounds of this collective variable. 

143 

144 Returns 

145 ------- 

146 Union[Quantity, None] 

147 The periodic bounds of this collective variable or None if it is not 

148 periodic 

149 """ 

150 return self._periodic_bounds 

151 

152 def addToSystem( 

153 self, system: openmm.System, setUnusedForceGroup: bool = True 

154 ) -> None: 

155 """ 

156 Add this collective variable to an :OpenMM:`System`. 

157 

158 Parameters 

159 ---------- 

160 system 

161 The system to which this collective variable should be added 

162 setUnusedForceGroup 

163 If True, the force group of this collective variable will be set to the 

164 first available force group in the system 

165 """ 

166 if setUnusedForceGroup: 

167 self._setUnusedForceGroup(system) 

168 system.addForce(self) 

169 

170 def getValue( 

171 self, context: openmm.Context, allowReinitialization: bool = False 

172 ) -> Quantity: 

173 """ 

174 Evaluate this collective variable at a given :OpenMM:`Context`. 

175 

176 If this collective variable share the force group with other forces, then its 

177 evaluation requires reinitializing the :OpenMM:`Context` twice at each call. 

178 This is inefficient and should be avoided. To allow this behavior, the 

179 ``allowReinitialization`` parameter must be set to True. 

180 

181 .. note:: 

182 

183 By adding this collective variable to the system using the 

184 :meth:`addToSystem` method, the force group of this collective variable 

185 is set to an available force group in the system by default. 

186 

187 Parameters 

188 ---------- 

189 context 

190 The context at which this collective variable should be evaluated. 

191 allowReinitialization 

192 If True, the context will be reinitialized if necessary. 

193 

194 Returns 

195 ------- 

196 Quantity 

197 The value of this collective variable at the given context. 

198 

199 Example 

200 ------- 

201 In this example, we compute the values of the backbone dihedral angles and 

202 the radius of gyration of an alanine dipeptide molecule in water: 

203 

204 >>> import cvpack 

205 >>> import openmm 

206 >>> from openmmtools import testsystems 

207 >>> model = testsystems.AlanineDipeptideExplicit() 

208 >>> top = model.mdtraj_topology 

209 >>> backbone_atoms = top.select("name N C CA and resid 1 2") 

210 >>> phi = cvpack.Torsion(*backbone_atoms[0:4]) 

211 >>> psi = cvpack.Torsion(*backbone_atoms[1:5]) 

212 >>> radius_of_gyration = cvpack.RadiusOfGyration( 

213 ... top.select("not water") 

214 ... ) 

215 >>> for cv in [phi, psi, radius_of_gyration]: 

216 ... cv.addToSystem(model.system) 

217 >>> context = openmm.Context( 

218 ... model.system, openmm.VerletIntegrator(0) 

219 ... ) 

220 >>> context.setPositions(model.positions) 

221 >>> phi.getValue(context) 

222 3.1415... rad 

223 >>> psi.getValue(context) 

224 3.1415... rad 

225 >>> radius_of_gyration.getValue(context) 

226 0.29514... nm 

227 """ 

228 state = get_single_force_state( 

229 self, 

230 context, 

231 allowReinitialization, 

232 getEnergy=True, 

233 ) 

234 value = state.getPotentialEnergy().value_in_unit(mmunit.kilojoules_per_mole) 

235 return Quantity(value, self.getUnit()) 

236 

237 def getEffectiveMass( 

238 self, context: openmm.Context, allowReinitialization: bool = False 

239 ) -> Quantity: 

240 r""" 

241 Compute the effective mass of this collective variable at a given 

242 :OpenMM:`Context`. 

243 

244 The effective mass of a collective variable :math:`q({\bf r})` is defined as 

245 :cite:`Chipot_2007`: 

246 

247 .. math:: 

248 

249 m_\mathrm{eff}({\bf r}) = \left( 

250 \sum_{i=1}^N \frac{1}{m_i} \left\| \frac{dq}{d{\bf r}_i} \right\|^2 

251 \right)^{-1} 

252 

253 If this collective variable share the force group with other forces, then 

254 evaluating its effective mass requires reinitializing the :OpenMM:`Context` 

255 twice at each call. This is inefficient and should be avoided. To allow this 

256 behavior, the ``allowReinitialization`` parameter must be set to True. 

257 

258 .. note:: 

259 

260 By adding this collective variable to the system using the 

261 :meth:`addToSystem` method, the force group of this collective variable 

262 is set to an available force group in the system by default. 

263 

264 Parameters 

265 ---------- 

266 context 

267 The context at which this collective variable's effective mass should be 

268 evaluated. 

269 allowReinitialization 

270 If True, the context will be reinitialized if necessary. 

271 

272 Returns 

273 ------- 

274 Quantity 

275 The effective mass of this collective variable at the given context. 

276 

277 Example 

278 ------- 

279 In this example, we compute the effective masses of the backbone dihedral 

280 angles and the radius of gyration of an alanine dipeptide molecule in water: 

281 

282 >>> import cvpack 

283 >>> import openmm 

284 >>> from openmmtools import testsystems 

285 >>> model = testsystems.AlanineDipeptideExplicit() 

286 >>> top = model.mdtraj_topology 

287 >>> backbone_atoms = top.select("name N C CA and resid 1 2") 

288 >>> phi = cvpack.Torsion(*backbone_atoms[0:4]) 

289 >>> psi = cvpack.Torsion(*backbone_atoms[1:5]) 

290 >>> radius_of_gyration = cvpack.RadiusOfGyration( 

291 ... top.select("not water") 

292 ... ) 

293 >>> for cv in [phi, psi, radius_of_gyration]: 

294 ... cv.addToSystem(model.system) 

295 >>> context = openmm.Context( 

296 ... model.system, openmm.VerletIntegrator(0) 

297 ... ) 

298 >>> context.setPositions(model.positions) 

299 >>> phi.getEffectiveMass(context) 

300 0.05119... nm**2 Da/(rad**2) 

301 >>> psi.getEffectiveMass(context) 

302 0.05186... nm**2 Da/(rad**2) 

303 >>> radius_of_gyration.getEffectiveMass(context) 

304 30.946... Da 

305 """ 

306 return Quantity( 

307 compute_effective_mass(self, context, allowReinitialization), 

308 self._mass_unit, 

309 )