Coverage for openxps/couplings/inner_product_coupling.py: 100%

70 statements  

« prev     ^ index     » next       coverage.py v7.11.3, created at 2025-11-13 22:08 +0000

1""" 

2Inner product coupling. 

3 

4.. module:: openxps.couplings.inner_product_coupling 

5 :platform: Linux, MacOS, Windows 

6 :synopsis: The inner product of two separable vector-valued functions 

7 

8.. classauthor:: Charlles Abreu <craabreu@gmail.com> 

9 

10""" 

11 

12import typing as t 

13 

14import cvpack 

15import openmm as mm 

16from openmm import _openmm as mmswig 

17from openmm import unit as mmunit 

18 

19from openxps.utils import Function, preprocess_args 

20 

21from ..dynamical_variable import DynamicalVariable 

22from .base import Coupling 

23 

24 

25class InnerProductCoupling(Coupling): 

26 r"""Coupling defined by the inner product of two separable vector-valued functions. 

27 

28 Use this class if the coupling energy is the inner product of a vector-valued 

29 function :math:`{\boldsymbol \lambda}` of the extended dynamical variables 

30 :math:`{\bf s}` and a vector-valued function :math:`{\bf u}` of the physical 

31 coordinates :math:`{\bf r}`, i.e., 

32 

33 .. math:: 

34 

35 U = {\boldsymbol \lambda}({\bf s}) \cdot {\bf u}({\bf r}) 

36 = \sum_{i=1}^n \lambda_i({\bf s}) u_i({\bf r}) 

37 

38 where :math:`n` is the dimensionality of the vector-valued functions. 

39 

40 Parameters 

41 ---------- 

42 forces 

43 A sequence of :OpenMM:`Force` objects whose sum depends linearly on a vector of 

44 :math:`n` global context parameters. 

45 dynamical_variables 

46 The dynamical variables of the extended phase-space system. 

47 functions 

48 A dictionary defining global context parameters (keys) as mathematical functions 

49 (values) of the dynamical variables. It is not necessary to include identity 

50 functions (i.e., ``key=value``). 

51 **parameters 

52 Named parameters that appear in the mathematical expressions. 

53 

54 Examples 

55 -------- 

56 >>> from copy import copy 

57 >>> from collections import namedtuple 

58 >>> import cvpack 

59 >>> import openxps as xps 

60 >>> from openmm import unit 

61 >>> from math import pi 

62 >>> import openmm as mm 

63 >>> from openmmtools import testsystems 

64 >>> model = testsystems.AlanineDipeptideVacuum() 

65 

66 A function to remove Coulomb interactions: 

67 

68 >>> Parameters = namedtuple( 

69 ... "Parameters", 

70 ... ["index", "charge", "sigma", "epsilon"], 

71 ... ) 

72 >>> def remove_coulomb_interaction( 

73 ... force: mm.NonbondedForce, p1: Parameters, p2: Parameters 

74 ... ) -> None: 

75 ... force.addException( 

76 ... p1.index, 

77 ... p2.index, 

78 ... 0.0, 

79 ... (p1.sigma + p2.sigma)/2, 

80 ... (p1.epsilon * p2.epsilon).sqrt(), 

81 ... ) 

82 

83 Remove carbonyl oxygen <=> amide hydrogen interactions: 

84 

85 >>> nbforce = next( 

86 ... f for f in model.system.getForces() 

87 ... if isinstance(f, mm.NonbondedForce) 

88 ... ) 

89 >>> O1 = Parameters(5, *nbforce.getParticleParameters(5)) 

90 >>> H1 = Parameters(7, *nbforce.getParticleParameters(7)) 

91 >>> O2 = Parameters(15, *nbforce.getParticleParameters(15)) 

92 >>> H2 = Parameters(17, *nbforce.getParticleParameters(17)) 

93 >>> remove_coulomb_interaction(nbforce, O1, H2) 

94 >>> remove_coulomb_interaction(nbforce, O2, H1) 

95 

96 Add scaled Coulomb interactions: 

97 

98 >>> force = mm.CustomBondForce(f"scaling*chargeProd/r") 

99 >>> _ = force.addGlobalParameter("scaling", 1.0) 

100 >>> _ = force.addEnergyParameterDerivative("scaling") 

101 >>> _ = force.addPerBondParameter("chargeProd") 

102 >>> _ = force.addBond(O1.index, H2.index, [O1.charge*H2.charge]) 

103 >>> _ = force.addBond(O2.index, H1.index, [O2.charge*H1.charge]) 

104 

105 Create a coupling between the dynamical variable and the nonbonded force: 

106 

107 >>> lambda_dv = xps.DynamicalVariable( 

108 ... name="lambda", 

109 ... unit=unit.dimensionless, 

110 ... mass=1.0 * unit.dalton * unit.nanometer**2, 

111 ... bounds=xps.ReflectiveBounds(0.0, 1.0, unit.dimensionless), 

112 ... ) 

113 >>> coupling = xps.InnerProductCoupling( 

114 ... [force], 

115 ... [lambda_dv], 

116 ... functions={"scaling": "(1-cos(alpha*lambda))/2"}, 

117 ... alpha=pi*unit.radian, 

118 ... ) 

119 >>> context = xps.ExtendedSpaceContext( 

120 ... xps.ExtendedSpaceSystem(model.system, coupling), 

121 ... xps.LockstepIntegrator(mm.VerletIntegrator(1.0 * mmunit.femtosecond)), 

122 ... mm.Platform.getPlatformByName("Reference"), 

123 ... ) 

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

125 >>> context.setDynamicalVariableValues([1.0]) 

126 """ 

127 

128 @preprocess_args 

129 def __init__( 

130 self, 

131 forces: t.Iterable[mm.Force], 

132 dynamical_variables: t.Iterable[DynamicalVariable], 

133 functions: t.Optional[dict[str, str]] = None, 

134 **parameters: mmunit.Quantity, 

135 ) -> None: 

136 forces = [ 

137 cvpack.OpenMMForceWrapper( 

138 force, 

139 mmunit.kilojoule_per_mole, 

140 name=force.getName(), 

141 ) 

142 if not isinstance(force, cvpack.CollectiveVariable) 

143 else force 

144 for force in forces 

145 ] 

146 self._functions = [ 

147 Function(name, expression, **parameters) 

148 for name, expression in (functions or {}).items() 

149 ] 

150 self._parameters = parameters 

151 super().__init__(forces, dynamical_variables) 

152 self._dynamic_parameters = self._getDynamicParameters() 

153 

154 def __getstate__(self) -> dict[str, t.Any]: 

155 state = super().__getstate__() 

156 state["functions"] = self._functions 

157 state["parameters"] = self._parameters 

158 return state 

159 

160 def __setstate__(self, state: dict[str, t.Any]) -> None: 

161 self._functions = state["functions"] 

162 self._parameters = state["parameters"] 

163 super().__setstate__(state) 

164 self._dynamic_parameters = self._getDynamicParameters() 

165 

166 def _getDynamicParameters(self) -> set[str]: 

167 all_force_parameters = { 

168 force.getGlobalParameterName(index) 

169 for force in self._forces 

170 for index in range(force.getNumGlobalParameters()) 

171 } 

172 function_names = {fn.getName() for fn in self._functions} 

173 if self._functions: 

174 function_variables = set.union( 

175 *(fn.getVariables() for fn in self._functions), 

176 ) 

177 else: 

178 function_variables = set() 

179 

180 if missing := function_names - all_force_parameters: 

181 raise ValueError( 

182 "These functions are not global parameters in the provided forces: " 

183 + ", ".join(missing) 

184 ) 

185 

186 all_dvs = {dv.name for dv in self._dynamical_variables} 

187 

188 if functions_missing_dvs := [ 

189 fn.getName() for fn in self._functions if not (fn.getVariables() & all_dvs) 

190 ]: 

191 raise ValueError( 

192 "These functions do not depend on any dynamical variables: " 

193 + ", ".join(functions_missing_dvs) 

194 ) 

195 

196 dvs_in_force_parameters = all_dvs & all_force_parameters 

197 dvs_in_function_variables = all_dvs & function_variables 

198 

199 if dvs_in_both := dvs_in_function_variables & dvs_in_force_parameters: 

200 raise ValueError( 

201 "These dynamical variables are both function variables and global " 

202 f"context parameters: {', '.join(dvs_in_both)}" 

203 ) 

204 if dvs_in_neither := all_dvs - ( 

205 dvs_in_function_variables | dvs_in_force_parameters 

206 ): 

207 raise ValueError( 

208 "These dynamical variables are neither function variables nor global " 

209 "context parameters: " + ", ".join(dvs_in_neither) 

210 ) 

211 

212 dynamic_parameters = dvs_in_force_parameters | function_names 

213 for force in self._forces: 

214 force_parameters = { 

215 force.getGlobalParameterName(index) 

216 for index in range(force.getNumGlobalParameters()) 

217 } 

218 has_derivatives = { 

219 force.getEnergyParameterDerivativeName(index) 

220 for index in range(force.getNumEnergyParameterDerivatives()) 

221 } 

222 if missing := force_parameters & dynamic_parameters - has_derivatives: 

223 raise ValueError( 

224 "The following parameters require a derivative and are present in " 

225 f"force {force.getName()}, but no derivative was requested: " 

226 + ", ".join(missing) 

227 ) 

228 return dynamic_parameters 

229 

230 @staticmethod 

231 def _derivativeName(parameter: str) -> str: 

232 return "derivative_with_respect_to_" + parameter 

233 

234 def _createFlippedForce(self) -> mm.CustomCVForce: 

235 flipped_force = mm.CustomCVForce( 

236 "+".join(f"{p}*{self._derivativeName(p)}" for p in self._dynamic_parameters) 

237 ) 

238 all_dvs = [dv.name for dv in self._dynamical_variables] 

239 for parameter in self._dynamic_parameters: 

240 flipped_force.addGlobalParameter(self._derivativeName(parameter), 0.0) 

241 for fn in self._functions: 

242 flipped_force.addCollectiveVariable( 

243 fn.getName(), fn.createCollectiveVariable(all_dvs) 

244 ) 

245 for dv in self._dynamical_variables: 

246 if dv.name in self._dynamic_parameters: 

247 flipped_force.addCollectiveVariable( 

248 dv.name, dv.createCollectiveVariable(self._dv_indices[dv.name]) 

249 ) 

250 return flipped_force 

251 

252 def updateExtensionContext( 

253 self, 

254 physical_context: mm.Context, 

255 extension_context: mm.Context, 

256 ) -> None: 

257 state = mmswig.Context_getState(physical_context, mm.State.ParameterDerivatives) 

258 for name, value in mmswig.State_getEnergyParameterDerivatives(state).items(): 

259 if name in self._dynamic_parameters: 

260 mmswig.Context_setParameter( 

261 extension_context, self._derivativeName(name), value 

262 ) 

263 

264 

265InnerProductCoupling.registerTag("!openxps.InnerProductCoupling")