Coverage for ufedmm/ufedmm.py: 73%

520 statements  

« prev     ^ index     » next       coverage.py v7.4.1, created at 2024-02-02 04:20 +0000

1""" 

2.. module:: ufedmm 

3 :platform: Unix, Windows 

4 :synopsis: Unified Free Energy Dynamics with OpenMM 

5 

6.. moduleauthor:: Charlles Abreu <abreu@eq.ufrj.br> 

7""" 

8 

9import functools 

10from copy import deepcopy 

11 

12import numpy as np 

13import openmm 

14from openmm import app, unit 

15 

16import ufedmm 

17 

18 

19def _standardized(quantity): 

20 """Returns the numerical value of a quantity in a unit of measurement compatible 

21 with the Molecular Dynamics unit system (mass in Da, distance in nm, time in ps, 

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

23 if unit.is_quantity(quantity): 

24 return quantity.value_in_unit_system(unit.md_unit_system) 

25 else: 

26 return quantity 

27 

28 

29def _add_fake_particles(force, num_particles): 

30 if isinstance(force, openmm.CustomCVForce): 

31 for i in range(force.getNumCollectiveVariables()): 

32 _add_fake_particles(force.getCollectiveVariable(i), num_particles) 

33 elif isinstance(force, openmm.NonbondedForce): 

34 for i in range(num_particles - force.getNumParticles()): 

35 force.addParticle(0.0, 1.0, 0.0) 

36 elif isinstance(force, openmm.CustomNonbondedForce): 

37 for i in range(num_particles - force.getNumParticles()): 

38 force.addParticle([0.0] * force.getNumPerParticleParameters()) 

39 elif isinstance(force, openmm.CustomGBForce): 

40 if force.getNumParticles() < num_particles: 

41 parameter_list = list( 

42 map(force.getParticleParameters, range(force.getNumParticles())) 

43 ) 

44 force.addPerParticleParameter("isreal") 

45 for index in range(force.getNumEnergyTerms()): 

46 expression, type = force.getEnergyTermParameters(index) 

47 energy = ( 

48 "isreal*E_GB" 

49 if type == force.SingleParticle 

50 else "isreal1*isreal2*E_GB" 

51 ) 

52 force.setEnergyTermParameters( 

53 index, f"{energy}; E_GB={expression}", type 

54 ) 

55 for index, parameters in enumerate(parameter_list): 

56 force.setParticleParameters(index, parameters + (1.0,)) 

57 for i in range(num_particles - force.getNumParticles()): 

58 force.addParticle(parameter_list[0] + (0.0,)) 

59 elif isinstance(force, openmm.GBSAOBCForce): 

60 raise RuntimeError("GBSAOBCForce not supported") 

61 elif hasattr(force, "getReferencePositions"): 

62 refpos = force.getReferencePositions()._value 

63 if len(refpos) < num_particles: 

64 for _ in range(num_particles - len(refpos)): 

65 refpos.append(openmm.Vec3(0, 0, 0)) 

66 force.setReferencePositions(refpos) 

67 

68 

69class CollectiveVariable(object): 

70 """A function of the particle coordinates, evaluated by means of an :OpenMM:`Force` 

71 object. 

72 

73 Quoting the manual entry for :OpenMM:`CustomCVForce`: 

74 

75 "Each collective variable is defined by a Force object. The Force's potential 

76 energy is computed, and that becomes the value of the variable. This provides 

77 enormous flexibility in defining collective variables, especially by using 

78 custom forces. Anything that can be computed as a potential function can also 

79 be used as a collective variable." 

80 

81 Parameters 

82 ---------- 

83 id : str 

84 A valid identifier string for this collective variable. 

85 force : openmm.Force 

86 An :OpenMM:`Force` object whose energy function is used to evaluate this 

87 collective variable. 

88 

89 Example 

90 ------- 

91 >>> import openmm 

92 >>> import ufedmm 

93 >>> from openmm import unit 

94 >>> cv = ufedmm.CollectiveVariable('psi', openmm.CustomTorsionForce('theta')) 

95 >>> cv.force.addTorsion(0, 1, 2, 3, []) 

96 0 

97 """ 

98 

99 def __init__(self, id, force): 

100 if not id.isidentifier(): 

101 raise ValueError("Parameter id must be a valid variable identifier") 

102 self.id = id 

103 self.force = force 

104 

105 def _create_context(self, system, positions): 

106 system_copy = deepcopy(system) 

107 for force in system_copy.getForces(): 

108 force.setForceGroup(0) 

109 force_copy = deepcopy(self.force) 

110 force_copy.setForceGroup(1) 

111 _add_fake_particles(force_copy, system.getNumParticles()) 

112 system_copy.addForce(force_copy) 

113 platform = openmm.Platform.getPlatformByName("Reference") 

114 context = openmm.Context(system_copy, openmm.CustomIntegrator(0), platform) 

115 context.setPositions(positions) 

116 return context 

117 

118 def evaluate(self, system, positions, cv_unit=None): 

119 """Computes the value of the collective variable for a given system and a given 

120 set of particle coordinates. 

121 

122 Parameters 

123 ---------- 

124 system : openmm.System 

125 The system for which the collective variable will be evaluated. 

126 positions : list of openmm.Vec3 

127 A list whose size equals the number of particles in the system and which 

128 contains the coordinates of these particles. 

129 

130 Keyword Args 

131 ------------ 

132 cv_unit : unit.Unit, default=None 

133 The unity of measurement of the collective variable. If this is `None`, 

134 then a numerical value is returned based on the OpenMM default units. 

135 

136 Returns 

137 ------- 

138 float or unit.Quantity 

139 

140 Example 

141 ------- 

142 >>> import ufedmm 

143 >>> from openmm import unit 

144 >>> model = ufedmm.AlanineDipeptideModel() 

145 >>> model.phi.evaluate(model.system, model.positions) 

146 3.141592653589793 

147 >>> model.psi.evaluate(model.system, model.positions) 

148 3.141592653589793 

149 """ 

150 context = self._create_context(system, positions) 

151 energy = context.getState(getEnergy=True, groups={1}).getPotentialEnergy() 

152 value = energy.value_in_unit(unit.kilojoules_per_mole) 

153 if cv_unit is not None: 

154 value *= cv_unit / _standardized(1 * cv_unit) 

155 return value 

156 

157 def effective_mass(self, system, positions, cv_unit=None): 

158 """Computes the effective mass of the collective variable for a given system and 

159 a given set of particle coordinates. 

160 

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

162 :cite:`Cuendet_2014`: 

163 

164 .. math:: 

165 m_\\mathrm{eff} = \\left( 

166 \\sum_{j=1}^N \\frac{1}{m_j} \\left\\| 

167 \\frac{dq}{d\\mathbf{r}_j} 

168 \\right\\|^2 

169 \\right)^{-1} 

170 

171 Parameters 

172 ---------- 

173 system : openmm.System 

174 The system for which the collective variable will be evaluated. 

175 positions : list of openmm.Vec3 

176 A list whose size equals the number of particles in the system and which 

177 contains the coordinates of these particles. 

178 

179 Keyword Args 

180 ------------ 

181 cv_unit : unit.Unit, default=None 

182 The unity of measurement of the collective variable. If this is `None`, 

183 then a numerical value is returned based on the OpenMM default units. 

184 

185 Returns 

186 ------- 

187 float or unit.Quantity 

188 

189 Example 

190 ------- 

191 >>> import ufedmm 

192 >>> from openmm import unit 

193 >>> model = ufedmm.AlanineDipeptideModel() 

194 >>> model.phi.effective_mass(model.system, model.positions) 

195 0.04795887... 

196 >>> model.psi.effective_mass(model.system, model.positions) 

197 0.05115582... 

198 """ 

199 context = self._create_context(system, positions) 

200 forces = _standardized( 

201 context.getState(getForces=True, groups={1}).getForces(asNumpy=True) 

202 ) 

203 denom = sum( 

204 f.dot(f) / _standardized(system.getParticleMass(i)) 

205 for i, f in enumerate(forces) 

206 ) 

207 effective_mass = 1.0 / float(denom) 

208 if cv_unit is not None: 

209 factor = _standardized(1 * cv_unit) ** 2 

210 effective_mass *= factor * unit.dalton * (unit.nanometers / cv_unit) ** 2 

211 return effective_mass 

212 

213 

214class DynamicalVariable(object): 

215 """An extended phase-space variable, whose dynamics is coupled to that of one of 

216 more collective variables of a system. 

217 

218 The coupling occurs in the form of a potential energy term involving this dynamical 

219 variable and its associated collective variables. 

220 

221 The default potential is a harmonic driving of the type: 

222 

223 .. math:: 

224 V(s, \\mathbf r) = \\frac{\\kappa}{2} [s - q(\\mathbf r)]^2 

225 

226 where :math:`s` is the new dynamical variable, :math:`q(\\mathbf r)` is its 

227 associated collective variable, and :math:`kappa` is a force constant. 

228 

229 Parameters 

230 ---------- 

231 id : str 

232 A valid identifier string for this dynamical variable. 

233 min_value : float or unit.Quantity 

234 The minimum allowable value for this dynamical variable. 

235 max_value : float or unit.Quantity 

236 The maximum allowable value for this dynamical variable. 

237 mass : float or unit.Quantity 

238 The mass assigned to this dynamical variable, whose unit of measurement must 

239 be compatible with `unit.dalton*(unit.nanometers/X)**2`, where `X` is the 

240 unit of measurement of the dynamical variable itself. 

241 temperature : float or unit.Quantity 

242 The temperature of the heat bath attached to this variable. 

243 colvars : :class:`~ufedmm.ufedmm.CollectiveVariable` or list thereof 

244 Either a single colective variable or a list. 

245 potential : float or unit.Quantity or str 

246 Either the value of the force constant of a harmonic driving potential or an 

247 algebraic expression giving the energy of the system as a function of this 

248 dynamical variable and its associated collective variable. Such expression 

249 can also contain a set of global parameters, whose values must be passed as 

250 keyword arguments (see below). 

251 

252 Keyword Args 

253 ------------ 

254 periodic : bool, default=True 

255 Whether the collective variable is periodic with period 

256 `L=max_value-min_value`. 

257 sigma : float or unit.Quantity, default=None 

258 The standard deviation. If this is `None`, then no bias will be considered. 

259 grid_size : int, default=None 

260 The grid size. If this is `None` and `sigma` is finite, then a convenient 

261 value will be automatically chosen. 

262 **parameters 

263 Names and values of global parameters present in the algebraic expression 

264 defined as `potential` (see above). 

265 

266 Example 

267 ------- 

268 >>> import openmm 

269 >>> import ufedmm 

270 >>> from openmm import unit 

271 >>> cv = ufedmm.CollectiveVariable('psi', openmm.CustomTorsionForce('theta')) 

272 >>> cv.force.addTorsion(0, 1, 2, 3, []) 

273 0 

274 >>> mass = 50*unit.dalton*(unit.nanometer/unit.radians)**2 

275 >>> K = 1000*unit.kilojoules_per_mole/unit.radians**2 

276 >>> Ts = 1500*unit.kelvin 

277 >>> ufedmm.DynamicalVariable( 

278 ... 's_psi', -180*unit.degrees, 180*unit.degrees, mass, Ts, cv, K 

279 ... ) 

280 <s_psi in [-3.141592653589793, 3.141592653589793], periodic, m=50, T=1500> 

281 """ 

282 

283 def __init__( 

284 self, 

285 id, 

286 min_value, 

287 max_value, 

288 mass, 

289 temperature, 

290 colvars, 

291 potential, 

292 periodic=True, 

293 sigma=None, 

294 grid_size=None, 

295 **parameters, 

296 ): 

297 self.id = id 

298 self.min_value = _standardized(min_value) 

299 self.max_value = _standardized(max_value) 

300 self._range = self.max_value - self.min_value 

301 self.mass = _standardized(mass) 

302 self.temperature = _standardized(temperature) 

303 

304 self.colvars = colvars if isinstance(colvars, (list, tuple)) else [colvars] 

305 

306 if isinstance(potential, str): 

307 self.potential = potential 

308 self.parameters = { 

309 key: _standardized(value) for key, value in parameters.items() 

310 } 

311 else: 

312 cv_id = self.colvars[0].id 

313 if periodic: 

314 self.potential = f"0.5*K_{cv_id}*min(d{cv_id},{self._range}-d{cv_id})^2" 

315 self.potential += f"; d{cv_id}=abs({cv_id}-{self.id})" 

316 else: 

317 self.potential = f"0.5*K_{cv_id}*({cv_id}-{self.id})^2" 

318 self.parameters = {f"K_{cv_id}": _standardized(potential)} 

319 

320 self.periodic = periodic 

321 

322 if sigma is None or sigma == 0.0: 

323 self.sigma = self.grid_size = None 

324 else: 

325 self.sigma = _standardized(sigma) 

326 self._scaled_variance = (self.sigma / self._range) ** 2 

327 if grid_size is None: 

328 self.grid_size = int(np.ceil(5 * self._range / self.sigma)) + 1 

329 else: 

330 self.grid_size = grid_size 

331 

332 self.force = openmm.CustomExternalForce(self._get_energy_function()) 

333 self.force.addGlobalParameter("Lx", 0.0) 

334 self.force.addParticle(0, []) 

335 

336 def __repr__(self): 

337 properties = f"m={self.mass}, T={self.temperature}" 

338 status = "periodic" if self.periodic else "non-periodic" 

339 return ( 

340 f"<{self.id} in " 

341 f"[{self.min_value}, {self.max_value}], {status}, {properties}>" 

342 ) 

343 

344 def __getstate__(self): 

345 return dict( 

346 id=self.id, 

347 min_value=self.min_value, 

348 max_value=self.max_value, 

349 mass=self.mass, 

350 temperature=self.temperature, 

351 colvars=self.colvars, 

352 potential=self.potential, 

353 periodic=self.periodic, 

354 sigma=self.sigma, 

355 grid_size=self.grid_size, 

356 **self.parameters, 

357 ) 

358 

359 def __setstate__(self, kw): 

360 self.__init__(**kw) 

361 

362 def _particle_mass(self, Lx): 

363 length = Lx if self.periodic else Lx / 2 

364 return self.mass * (self._range / length) ** 2 

365 

366 def _particle_position(self, value, Lx, y=0): 

367 length = Lx if self.periodic else Lx / 2 

368 return ( 

369 openmm.Vec3(length * (value - self.min_value) / self._range, y, 0) 

370 * unit.nanometer 

371 ) 

372 

373 def _get_energy_function(self, index=""): 

374 """Returns the algebraic expression that transforms the x coordinate of a 

375 particle into this dynamical variables. 

376 

377 Keyword Args 

378 ------------ 

379 index : str or int, default='' 

380 An index for the particle in question, if needed. 

381 

382 Returns 

383 ------- 

384 str 

385 """ 

386 if self.periodic: 

387 energy = f"{self.min_value}+{self._range}*(x{index}/Lx-floor(x{index}/Lx))" 

388 else: 

389 energy = f"{self.min_value}+{2*self._range}*min(pos{index},1-pos{index})" 

390 energy += f";pos{index}=x{index}/Lx-floor(x{index}/Lx)" 

391 return energy 

392 

393 def evaluate(self, x, Lx): 

394 """Computes the value of this dynamical variable for a given x coordinate and a 

395 given length of the simulation box in the x direction. 

396 

397 Parameters 

398 ---------- 

399 x : float or unit.Quantity 

400 The x coordinate. 

401 Lx : float or unit.Quantity 

402 The length of the simulation box in the x direction. 

403 

404 Returns 

405 ------- 

406 float 

407 """ 

408 pos = x / Lx - np.floor(x / Lx) 

409 if self.periodic: 

410 return self.min_value + self._range * pos 

411 else: 

412 return self.min_value + 2 * self._range * min(pos, 1 - pos) 

413 

414 

415def _get_energy_function(variables): 

416 energies = [v.potential.split(";", 1) for v in variables] 

417 energy_terms = [energy[0] for energy in energies] 

418 definitions = [energy[1] for energy in energies if len(energy) == 2] 

419 expression = ";".join(["+".join(energy_terms)] + list(definitions)) 

420 return expression 

421 

422 

423def _get_parameters(variables): 

424 parameters = {} 

425 for v in variables: 

426 parameters.update(v.parameters) 

427 return parameters 

428 

429 

430class PeriodicTask(object): 

431 def __init__(self, frequency): 

432 self.frequency = frequency 

433 

434 def initialize(self, simulation, force_group): 

435 pass 

436 

437 def update(self, simulation, steps): 

438 pass 

439 

440 def describeNextReport(self, simulation): 

441 steps = self.frequency - simulation.context.getStepCount() % self.frequency 

442 return (steps, True, False, False, False, False) 

443 

444 def report(self, simulation, state): 

445 pass 

446 

447 def saveCheckpoint(self, file): 

448 pass 

449 

450 def loadCheckpoint(self, file): 

451 pass 

452 

453 

454class _Metadynamics(PeriodicTask): 

455 """Extended-space Metadynamics. 

456 

457 Parameters 

458 ---------- 

459 variables : list of :class:`DynamicalVariable` 

460 A list of extended-space dynamical variables to which the metadynamics bias 

461 must be applied. In fact, dynamical variables with `sigma = None` will not 

462 be considered. 

463 height : float or unit.Quantity 

464 The height of the Gaussian potential hills to be deposited. If the 

465 `bias_factor` keyword is defined (see below), then this is the unscaled 

466 height. 

467 frequency : int 

468 The frequency of Gaussian hill deposition. 

469 

470 Keyword Args 

471 ------------ 

472 bias_factor : float, default=None 

473 Scales the height of the hills added to the bias potential. If it is `None`, 

474 then the hills will have a constant height over time. For a bias factor to 

475 be applicable, all bias variables must be at the same temperature T. The 

476 extended-space dynamical variables are sampled as if the effective 

477 temperature were T*bias_factor. 

478 buffer_size : int, default=100 

479 The buffer size. 

480 grid_expansion : int, default=20 

481 The number of extra grid points to be used in periodic directions of 

482 multidimensional tabulated functions. This aims at avoiding boundary 

483 discontinuity artifacts. 

484 enforce_gridless : bool, default=False 

485 Enforce gridless metadynamics even for 1D to 3D problems. 

486 """ 

487 

488 def __init__( 

489 self, 

490 variables, 

491 height, 

492 frequency, 

493 bias_factor=None, 

494 buffer_size=100, 

495 grid_expansion=20, 

496 enforce_gridless=False, 

497 ): 

498 super().__init__(frequency) 

499 self.bias_indices = [i for i, v in enumerate(variables) if v.sigma is not None] 

500 self.bias_variables = [variables[i] for i in self.bias_indices] 

501 self.initial_height = self.height = _standardized(height) 

502 self.bias_factor = bias_factor 

503 if bias_factor is not None: 

504 temperature = self.bias_variables[0].temperature 

505 if any(v.temperature != temperature for v in self.bias_variables): 

506 raise ValueError( 

507 "Well-tempered metadynamics requires same temperature " 

508 "for all variables." 

509 ) 

510 self.delta_kT = _standardized( 

511 (bias_factor - 1) * unit.MOLAR_GAS_CONSTANT_R * temperature 

512 ) 

513 self.buffer_size = buffer_size 

514 self.grid_expansion = grid_expansion 

515 self._use_grid = len(self.bias_variables) < 4 and not enforce_gridless 

516 self.force = ( 

517 self._interpolation_grid_force() if self._use_grid else self._hills_force() 

518 ) 

519 

520 def _interpolation_grid_force(self): 

521 self._widths = [] 

522 self._bounds = [] 

523 self._extra_points = [] 

524 full_periodic = self._full_periodic = all( 

525 v.periodic for v in self.bias_variables 

526 ) 

527 for v in self.bias_variables: 

528 extra_points = ( 

529 min(self.grid_expansion, v.grid_size - 1) 

530 if v.periodic and not full_periodic 

531 else 0 

532 ) 

533 extra_range = extra_points * v._range / (v.grid_size - 1) 

534 self._widths.append(v.grid_size + 2 * extra_points) 

535 self._bounds += [v.min_value - extra_range, v.max_value + extra_range] 

536 self._extra_points.append(extra_points) 

537 self._bias = np.zeros(np.prod(self._widths)) 

538 num_bias_variables = len(self.bias_variables) 

539 if num_bias_variables == 1: 

540 self._table = openmm.Continuous1DFunction( 

541 self._bias, *self._bounds, full_periodic 

542 ) 

543 elif num_bias_variables == 2: 

544 self._table = openmm.Continuous2DFunction( 

545 *self._widths, self._bias, *self._bounds, full_periodic 

546 ) 

547 else: 

548 self._table = openmm.Continuous3DFunction( 

549 *self._widths, self._bias, *self._bounds, full_periodic 

550 ) 

551 expression = ( 

552 f'metad_bias_scale*bias({",".join(v.id for v in self.bias_variables)})' 

553 ) 

554 for i, v in enumerate(self.bias_variables): 

555 expression += f";{v.id}={v._get_energy_function(i+1)}" 

556 force = openmm.CustomCVForce(expression) 

557 for i in range(num_bias_variables): 

558 x = openmm.CustomExternalForce("x") 

559 x.addParticle(0, []) 

560 force.addCollectiveVariable(f"x{i+1}", x) 

561 force.addTabulatedFunction("bias", self._table) 

562 force.addGlobalParameter("Lx", 0) 

563 force.addGlobalParameter("metad_bias_scale", 1) 

564 force.addEnergyParameterDerivative("metad_bias_scale") 

565 return force 

566 

567 def _hills_force(self): 

568 num_bias_variables = len(self.bias_variables) 

569 centers = [f"center{i+1}" for i in range(num_bias_variables)] 

570 exponents = [] 

571 for v, center in zip(self.bias_variables, centers): 

572 if v.periodic: # von Mises 

573 factor = 2 * np.pi / v._range 

574 exponents.append( 

575 f"{1.0/(factor*v.sigma)**2}*(cos({factor}*({v.id}-{center}))-1)" 

576 ) 

577 else: # Gauss 

578 exponents.append(f"({-0.5/v.sigma**2})*({v.id}-{center})^2") 

579 expression = f'height*exp({"+".join(exponents)})' 

580 for i, v in enumerate(self.bias_variables): 

581 expression += f";{v.id}={v._get_energy_function(i+1)}" 

582 force = openmm.CustomCompoundBondForce(num_bias_variables, expression) 

583 force.addPerBondParameter("height") 

584 for center in centers: 

585 force.addPerBondParameter(center) 

586 force.addGlobalParameter("Lx", 0) 

587 return force 

588 

589 def _add_buffer(self, simulation): 

590 size = min(self.buffer_size, self._total_hills - self.force.getNumBonds()) 

591 for i in range(size): 

592 self.force.addBond(self.particles, [0] * (len(self.bias_variables) + 1)) 

593 simulation.context.reinitialize(preserveState=True) 

594 

595 def add_bias(self, simulation, bias): 

596 if self._use_grid: 

597 self._bias += bias.ravel() 

598 if len(self.bias_variables) == 1: 

599 self._table.setFunctionParameters(self._bias, *self._bounds) 

600 else: 

601 self._table.setFunctionParameters( 

602 *self._widths, self._bias, *self._bounds 

603 ) 

604 self.force.updateParametersInContext(simulation.context) 

605 

606 def initialize(self, simulation): 

607 context = simulation.context 

608 np = context.getSystem().getNumParticles() - len(context.variables) 

609 self.particles = [np + index for index in self.bias_indices] 

610 self.Lx = context.getParameter("Lx") 

611 if self._use_grid: 

612 for i, particle in enumerate(self.particles): 

613 self.force.getCollectiveVariable(i).setParticleParameters( 

614 0, particle, [] 

615 ) 

616 else: 

617 self._num_hills = 0 

618 self._total_hills = 0 

619 simulation.system.addForce(self.force) 

620 context.reinitialize(preserveState=True) 

621 

622 def update(self, simulation, steps): 

623 if not self._use_grid: 

624 steps_until_next_report = ( 

625 self.frequency - simulation.currentStep % self.frequency 

626 ) 

627 if steps_until_next_report > steps: 

628 required_hills = 0 

629 else: 

630 required_hills = (steps - steps_until_next_report) // self.frequency + 1 

631 self._total_hills = self._num_hills + required_hills 

632 self._add_buffer(simulation) 

633 

634 def report(self, simulation, state): 

635 centers = state.getDynamicalVariables() 

636 if self.bias_factor is None: 

637 self.height = self.initial_height 

638 else: 

639 hills_state = simulation.context.getState(getParameterDerivatives=True) 

640 energy = hills_state.getEnergyParameterDerivatives()["metad_bias_scale"] 

641 self.height = self.initial_height * np.exp(-energy / self.delta_kT) 

642 if self._use_grid: 

643 hills = [] 

644 for i, v in enumerate(self.bias_variables): 

645 x = (centers[i] - v.min_value) / v._range 

646 dist = np.linspace(0, 1, num=v.grid_size) - x 

647 if v.periodic: # von Mises 

648 exponents = (np.cos(2 * np.pi * dist) - 1) / ( 

649 4 * np.pi * np.pi * v._scaled_variance 

650 ) 

651 exponents[0] = exponents[-1] = 0.5 * (exponents[0] + exponents[-1]) 

652 else: # Gauss 

653 exponents = -0.5 * dist * dist / v._scaled_variance 

654 hills.append(self.height * np.exp(exponents)) 

655 ndim = len(self.bias_variables) 

656 bias = ( 

657 hills[0] 

658 if ndim == 1 

659 else functools.reduce(np.multiply.outer, reversed(hills)) 

660 ) 

661 if not self._full_periodic: 

662 for axis, v in enumerate(self.bias_variables): 

663 if v.periodic: 

664 n = self._extra_points[axis] + 1 

665 begin = tuple( 

666 slice(1, n) if i == axis else slice(None) 

667 for i in range(ndim) 

668 ) 

669 end = tuple( 

670 slice(-n, -1) if i == axis else slice(None) 

671 for i in range(ndim) 

672 ) 

673 bias = np.concatenate((bias[end], bias, bias[begin]), axis=axis) 

674 self.add_bias(simulation, bias) 

675 else: 

676 if self._num_hills == self.force.getNumBonds(): 

677 self._add_buffer(simulation) 

678 self.force.setBondParameters( 

679 self._num_hills, self.particles, [self.height] + centers 

680 ) 

681 self._num_hills += 1 

682 self.force.updateParametersInContext(simulation.context) 

683 

684 def saveCheckpoint(self, file): 

685 if self._use_grid: 

686 self._bias.tofile(file) 

687 else: 

688 np.array([self._num_hills], dtype=int).tofile(file) 

689 parameter_list = [] 

690 for index in range(self._num_hills): 

691 _, parameters = self.force.getBondParameters(index) 

692 parameter_list.append(parameters) 

693 np.array(parameter_list).tofile(file) 

694 np.array([self.height]).tofile(file) 

695 

696 def loadCheckpoint(self, file, context): 

697 if self._use_grid: 

698 self._bias = np.fromfile(file, count=len(self._bias)) 

699 if len(self.bias_variables) == 1: 

700 self._table.setFunctionParameters(self._bias, *self._bounds) 

701 else: 

702 self._table.setFunctionParameters( 

703 *self._widths, self._bias, *self._bounds 

704 ) 

705 self.force.updateParametersInContext(context) 

706 else: 

707 npars = len(self.bias_variables) + 1 

708 nhills = self._num_hills = np.fromfile(file, dtype=int, count=1)[0] 

709 parameter_array = np.fromfile(file, count=nhills * npars).reshape( 

710 (nhills, npars) 

711 ) 

712 for index in range(nhills - self.force.getNumBonds()): 

713 self.force.addBond(self.particles, [0] * npars) 

714 self._total_hills = self.force.getNumBonds() 

715 for index, parameters in enumerate(parameter_array): 

716 self.force.setBondParameters(index, self.particles, parameters) 

717 for index in range(self._total_hills - nhills): 

718 self.force.setBondParameters(index, self.particles, [0] * npars) 

719 context.reinitialize(preserveState=True) 

720 self.height = np.fromfile(file, count=1)[0] 

721 

722 

723class ExtendedSpaceState(openmm.State): 

724 """An extension of the :OpenMM:`State` class.""" 

725 

726 def __init__(self, variables, state): 

727 self.__class__ = type( 

728 state.__class__.__name__, (self.__class__, state.__class__), {} 

729 ) 

730 self.__dict__ = state.__dict__ 

731 self._variables = variables 

732 a, _, _ = self.getPeriodicBoxVectors() 

733 self._Lx = a.x 

734 

735 def _split(self, vector, asNumpy=False): 

736 num = (vector.shape[0] if asNumpy else len(vector)) - len(self._variables) 

737 particles_contribution = vector[:num, :] if asNumpy else vector[:num] 

738 variables_contribution = ( 

739 vector[num:, 0] if asNumpy else [vec.x for vec in vector[num:]] 

740 ) 

741 return particles_contribution, variables_contribution 

742 

743 def getPositions(self, asNumpy=False, extended=False, split=True): 

744 """Gets the positions of all physical particles and optionally also gets the 

745 positions of the extra particles from which the extended-space dynamical 

746 variables are computed. 

747 

748 Keyword Args 

749 ------------ 

750 asNumpy : bool, default=False 

751 Whether to return Numpy arrays instead of lists of openmm.Vec3. 

752 extended : bool, default=False 

753 Whether to include the positions of the extra particles from which the 

754 extended-space dynamical variables are computed. 

755 split : bool, default=True 

756 Whether to return the positions of the physical particles and the 

757 positions of the extra particles separately. 

758 

759 Returns 

760 ------- 

761 list(openmm.Vec3) or numpy.ndarray 

762 If `extended=False` or `split=False`. 

763 list(openmm.Vec3) or numpy.ndarray, list(float) or numpy.ndarray 

764 If `extended=True` and `split=True`. 

765 

766 Raises 

767 ------ 

768 Exception 

769 If positions were not requested in the ``context.getState()`` call. 

770 """ 

771 all_positions = super().getPositions(asNumpy) 

772 if extended and not split: 

773 return all_positions 

774 positions, xvars = self._split(all_positions, asNumpy) 

775 return (positions, xvars) if extended else positions 

776 

777 def getVelocities(self, asNumpy=False, extended=False): 

778 """Gets the velocities of all physical particles and optionally also gets the 

779 velocities of the extra particles associated to the extended- space dynamical 

780 variables. 

781 

782 Keyword Args 

783 ------------ 

784 asNumpy : bool, default=False 

785 Whether to return Numpy arrays instead of lists of openmm.Vec3. 

786 extended : bool, default=False 

787 Whether to include the velocities of the extra particles. 

788 

789 Returns 

790 ------- 

791 list(openmm.Vec3) 

792 If `asNumpy=False` and `extended=False`. 

793 numpy.ndarray 

794 If `asNumpy=True` and `extended=False`. 

795 list(openmm.Vec3), list(float) 

796 If `asNumpy=False` and `extended=True`. 

797 numpy.ndarray, numpy.ndarray 

798 If `asNumpy=True` and `extended=True`. 

799 

800 Raises 

801 ------ 

802 Exception 

803 If velocities were not requested in the ``context.getState()`` call. 

804 """ 

805 velocities = super().getVelocities(asNumpy) 

806 if not extended: 

807 velocities, _ = self._split(velocities, asNumpy) 

808 return velocities 

809 

810 def getDynamicalVariables(self): 

811 """Gets the values of the extended-space dynamical variables. 

812 

813 Returns 

814 ------- 

815 list(float) 

816 

817 Raises 

818 ------ 

819 Exception 

820 If positions were not requested in the ``context.getState()`` call. 

821 

822 Example 

823 ------- 

824 >>> import ufedmm 

825 >>> import numpy as np 

826 >>> model = ufedmm.AlanineDipeptideModel() 

827 >>> integrator = ufedmm.CustomIntegrator(300, 0.001) 

828 >>> args = [-np.pi, np.pi, 50, 1500, model.phi, 1000] 

829 >>> s_phi = ufedmm.DynamicalVariable('s_phi', *args) 

830 >>> s_psi = ufedmm.DynamicalVariable('s_psi', *args) 

831 >>> context = ufedmm.ExtendedSpaceContext( 

832 ... [s_phi, s_psi], model.system, integrator 

833 ... ) 

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

835 >>> context.getState(getPositions=True).getDynamicalVariables() 

836 [-3.141592653589793, -3.141592653589793] 

837 """ 

838 _, xvars = self._split(super().getPositions()) 

839 return [v.evaluate(x, self._Lx) for v, x in zip(self._variables, xvars)] 

840 

841 

842class ExtendedSpaceContext(openmm.Context): 

843 """An extension of the :OpenMM:`Context` class. 

844 

845 Parameters 

846 ---------- 

847 variables : list(DynamicalVariable) 

848 The dynamical variables to be added to the system's phase space. 

849 system : openmm.System 

850 The :OpenMM:`System` which will be simulated 

851 integrator : openmm.Integrator 

852 The :OpenMM:`Integrator` which will be used to simulate the 

853 :OpenMM:`System`. 

854 platform : openmm.Platform 

855 The :OpenMM:`Platform` to use for calculations. 

856 properties : dict(str: str) 

857 A set of values for platform-specific properties. Keys are the property 

858 names. 

859 

860 Attributes 

861 ---------- 

862 variables : list(DynamicalVariable) 

863 The dynamical variables added to the system's phase space. 

864 driving_forces : list[openmm.CustomCVForce] 

865 A list of :OpenMM:`CustomCVForce` objects responsible for evaluating the 

866 potential energy terms which couples the extra dynamical variables to their 

867 associated collective variables. 

868 """ 

869 

870 def __init__(self, variables, system, *args, **kwargs): 

871 def get_driving_force(variables): 

872 driving_force = openmm.CustomCVForce(_get_energy_function(variables)) 

873 for name, value in _get_parameters(variables).items(): 

874 driving_force.addGlobalParameter(name, value) 

875 for var in variables: 

876 driving_force.addCollectiveVariable(var.id, var.force) 

877 for colvar in var.colvars: 

878 driving_force.addCollectiveVariable(colvar.id, colvar.force) 

879 return driving_force 

880 

881 driving_force_variables = [[]] 

882 added_colvars = 0 

883 for var in variables: 

884 colvars_to_add = len(var.colvars) + 1 

885 if added_colvars + colvars_to_add > 32: 

886 driving_force_variables.append([]) 

887 added_colvars = 0 

888 driving_force_variables[-1].append(deepcopy(var)) 

889 added_colvars += colvars_to_add 

890 driving_forces = [get_driving_force(vars) for vars in driving_force_variables] 

891 box_length_x = system.getDefaultPeriodicBoxVectors()[0].x 

892 num_particles = system.getNumParticles() 

893 collective_variables = [] 

894 for dfvars in driving_force_variables: 

895 for var in dfvars: 

896 system.addParticle(var._particle_mass(box_length_x)) 

897 var.force.setParticleParameters(0, num_particles, []) 

898 num_particles += 1 

899 collective_variables += var.colvars 

900 for force in system.getForces(): 

901 _add_fake_particles(force, num_particles) 

902 for cv in collective_variables: 

903 _add_fake_particles(cv.force, num_particles) 

904 for driving_force in driving_forces: 

905 system.addForce(driving_force) 

906 super().__init__(system, *args, **kwargs) 

907 self.setParameter("Lx", box_length_x) 

908 self.variables = variables 

909 self.driving_forces = driving_forces 

910 if len(driving_forces) == 1: 

911 self.driving_force = driving_forces[0] # for backwards compatibility 

912 

913 def getState(self, **kwargs): 

914 """Returns a :class:`ExtendedSpaceState` object. 

915 

916 .. _getState: http://docs.openmm.org/latest/api-python/generated/openmm.openmm.Context.html#openmm.openmm.Context.getState # noqa: E501 

917 

918 Keyword Args 

919 ------------ 

920 **kwargs 

921 See getState_. 

922 """ 

923 return ExtendedSpaceState(self.variables, super().getState(**kwargs)) 

924 

925 def setPeriodicBoxVectors(self, a, b, c): 

926 """Set the vectors defining the axes of the periodic box. 

927 

928 .. warning:: 

929 Only orthorhombic boxes are allowed. 

930 

931 Parameters 

932 ---------- 

933 a : openmm.Vec3 

934 The vector defining the first edge of the periodic box. 

935 b : openmm.Vec3 

936 The vector defining the second edge of the periodic box. 

937 c : openmm.Vec3 

938 The vector defining the third edge of the periodic box. 

939 """ 

940 a = openmm.Vec3(*map(_standardized, a)) 

941 b = openmm.Vec3(*map(_standardized, b)) 

942 c = openmm.Vec3(*map(_standardized, c)) 

943 if not (a.y == a.z == b.x == b.z == c.x == c.y == 0.0): 

944 raise ValueError("Only orthorhombic boxes are allowed") 

945 self.setParameter("Lx", a.x) 

946 super().setPeriodicBoxVectors(a, b, c) 

947 system = self.getSystem() 

948 ntotal = system.getNumParticles() 

949 nvars = len(self.variables) 

950 for i, v in enumerate(self.variables): 

951 system.setParticleMass(ntotal - nvars + i, v._particle_mass(a.x)) 

952 self.reinitialize(preserveState=True) 

953 

954 def setPositions(self, positions, extended_positions=None): 

955 """Sets the positions of all particles and extended-space variables in this 

956 context. If the latter are not provided, then suitable values are automatically 

957 determined from the particle positions. 

958 

959 Parameters 

960 ---------- 

961 positions : list of openmm.Vec3 

962 The positions of physical particles. 

963 

964 Keyword Args 

965 ------------ 

966 extended_positions : list of float or unit.Quantity 

967 The positions of extended-space particles. 

968 """ 

969 a, b, _ = self.getState().getPeriodicBoxVectors() 

970 nvars = len(self.variables) 

971 particle_positions = _standardized(positions) 

972 if extended_positions is None: 

973 extra_positions = [ 

974 openmm.Vec3(0, b.y * (i + 1) / (nvars + 2), 0) for i in range(nvars) 

975 ] 

976 minisystem = openmm.System() 

977 expression = _get_energy_function(self.variables) 

978 for i, v in enumerate(self.variables): 

979 expression += f"; {v.id}={v._get_energy_function(index=i+1)}" 

980 force = openmm.CustomCompoundBondForce(nvars, expression) 

981 force.addBond(range(nvars), []) 

982 for name, value in _get_parameters(self.variables).items(): 

983 force.addGlobalParameter(name, value) 

984 force.addGlobalParameter("Lx", a.x) 

985 for v in self.variables: 

986 minisystem.addParticle(v._particle_mass(a.x)) 

987 for cv in v.colvars: 

988 value = cv.evaluate( 

989 self.getSystem(), particle_positions + extra_positions 

990 ) 

991 force.addGlobalParameter(cv.id, value) 

992 minisystem.addForce(force) 

993 minicontext = openmm.Context( 

994 minisystem, 

995 openmm.CustomIntegrator(0), 

996 openmm.Platform.getPlatformByName("Reference"), 

997 ) 

998 minicontext.setPositions(extra_positions) 

999 openmm.LocalEnergyMinimizer.minimize( 

1000 minicontext, 1 * unit.kilojoules_per_mole / unit.nanometers, 0 

1001 ) 

1002 ministate = minicontext.getState(getPositions=True) 

1003 extra_positions = ministate.getPositions().value_in_unit(unit.nanometers) 

1004 else: 

1005 extra_positions = [ 

1006 openmm.Vec3(x, b.y * (i + 1) / (nvars + 2), 0) 

1007 for i, x in enumerate(extended_positions) 

1008 ] 

1009 super().setPositions(particle_positions + extra_positions) 

1010 

1011 def setVelocitiesToTemperature(self, temperature, randomSeed=None): 

1012 """Sets the velocities of all particles in the system to random values chosen 

1013 from a Maxwell-Boltzmann distribution at a given temperature. 

1014 

1015 .. warning :: 

1016 The velocities of the extended-space variables are set to values consistent 

1017 with the distribution at its own specified temperature. 

1018 

1019 Parameters 

1020 ---------- 

1021 temperature : float or unit.Quantity 

1022 The temperature of the system. 

1023 

1024 Keyword Args 

1025 ------------ 

1026 randomSeed : int, default=None 

1027 A seed for the random number generator. 

1028 """ 

1029 system = self.getSystem() 

1030 Ntotal = system.getNumParticles() 

1031 Natoms = Ntotal - len(self.variables) 

1032 m = np.array([system.getParticleMass(i) / unit.dalton for i in range(Ntotal)]) 

1033 T = np.array( 

1034 [_standardized(temperature)] * Natoms 

1035 + [v.temperature for v in self.variables] 

1036 ) 

1037 sigma = np.sqrt(_standardized(unit.MOLAR_GAS_CONSTANT_R) * T / m) 

1038 random_state = np.random.RandomState(randomSeed) 

1039 velocities = sigma[:, np.newaxis] * random_state.normal(0, 1, (Ntotal, 3)) 

1040 super().setVelocities(velocities) 

1041 

1042 

1043class ExtendedSpaceSimulation(app.Simulation): 

1044 """A simulation involving extended phase-space variables. 

1045 

1046 Parameters 

1047 ---------- 

1048 variables : list of DynamicalVariable 

1049 The dynamical variables to be added to the system's phase space. 

1050 topology : openmm.app.Topology 

1051 A Topology describing the system to be simulated. 

1052 system : openmm.System 

1053 The :OpenMM:`System` object to be simulated. 

1054 integrator : openmm.Integrator 

1055 The OpenMM Integrator to use for simulating the system dynamics. 

1056 

1057 Keyword Args 

1058 ------------ 

1059 platform : openmm.Platform, default=None 

1060 If not None, the :OpenMM:`Platform` to use. 

1061 platformProperties : dict, default=None 

1062 If not None, a set of platform-specific properties to pass to the Context's 

1063 constructor. 

1064 """ 

1065 

1066 def __init__( 

1067 self, 

1068 variables, 

1069 topology, 

1070 system, 

1071 integrator, 

1072 platform=None, 

1073 platformProperties=None, 

1074 ): 

1075 self.variables = variables 

1076 self._periodic_tasks = [] 

1077 

1078 for force in system.getForces(): 

1079 cls = force.__class__.__name__ 

1080 if cls == "CMMotionRemover" or cls.endswith("Barostat"): 

1081 raise Exception( 

1082 "UFED: system cannot contain CMMotionRemover nor any Barostat" 

1083 ) 

1084 

1085 box_vectors = topology.getPeriodicBoxVectors() 

1086 if box_vectors is None: 

1087 raise Exception("UFED: system must be confined in a simulation box") 

1088 

1089 self.topology = topology 

1090 self.system = system 

1091 self.integrator = integrator 

1092 if openmm.__version__ < "7.7": 

1093 self.currentStep = 0 

1094 self.reporters = [] 

1095 self._usesPBC = True 

1096 if platform is None: 

1097 self.context = ExtendedSpaceContext(variables, system, integrator) 

1098 elif platformProperties is None: 

1099 self.context = ExtendedSpaceContext(variables, system, integrator, platform) 

1100 else: 

1101 self.context = ExtendedSpaceContext( 

1102 variables, system, integrator, platform, platformProperties 

1103 ) 

1104 

1105 def add_periodic_task(self, task, force_group=0): 

1106 """Adds a task to be executed periodically along this simulation. 

1107 

1108 Parameters 

1109 ---------- 

1110 task : PeriodicTask 

1111 A :class:`~ufedmm.ufedmm.PeriodicTask` object. 

1112 """ 

1113 task.initialize(self) 

1114 self._periodic_tasks.append(task) 

1115 

1116 def step(self, steps): 

1117 """Executed a given number of simulation steps. 

1118 

1119 Parameters 

1120 ---------- 

1121 steps : int 

1122 The number of steps to be executed. 

1123 """ 

1124 if isinstance(self.integrator, ufedmm.AbstractMiddleRespaIntegrator): 

1125 if ( 

1126 self.integrator._num_rattles == 0 

1127 and self.system.getNumConstraints() > 0 

1128 ): 

1129 raise RuntimeError("Integrator cannot handle constraints") 

1130 if self._periodic_tasks: 

1131 for task in self._periodic_tasks: 

1132 task.update(self, steps) 

1133 self.reporters = self._periodic_tasks + self.reporters 

1134 self._simulate(endStep=self.currentStep + steps) 

1135 self.reporters = self.reporters[len(self._periodic_tasks) :] 

1136 else: 

1137 self._simulate(endStep=self.currentStep + steps) 

1138 

1139 def saveCheckpoint(self, file): 

1140 if isinstance(file, str): 

1141 with open(file, "wb") as f: 

1142 self.saveCheckpoint(f) 

1143 else: 

1144 for task in self._periodic_tasks: 

1145 task.saveCheckpoint(file) 

1146 file.write(self.context.createCheckpoint()) 

1147 

1148 def loadCheckpoint(self, file): 

1149 if isinstance(file, str): 

1150 with open(file, "rb") as f: 

1151 self.loadCheckpoint(f) 

1152 else: 

1153 for task in self._periodic_tasks: 

1154 task.loadCheckpoint(file, self.context) 

1155 self.context.loadCheckpoint(file.read()) 

1156 

1157 

1158class UnifiedFreeEnergyDynamics(object): 

1159 """A Unified Free-Energy Dynamics (UFED) setup. 

1160 

1161 Parameters 

1162 ---------- 

1163 variables : list of DynamicalVariable 

1164 The variables. 

1165 temperature : float or unit.Quantity 

1166 The temperature. 

1167 

1168 Keyword Args 

1169 ------------ 

1170 height : float or unit.Quantity, default=None 

1171 The height. 

1172 frequency : int, default=None 

1173 The frequency. 

1174 bias_factor : float, default=None 

1175 Scales the height of the hills added to the metadynamics bias potential. If 

1176 it is `None`, then the hills will have a constant height over time. For a 

1177 bias factor to be applicable, all bias variables must be at the same 

1178 temperature T. The extended-space dynamical variables are sampled as if the 

1179 effective temperature were T*bias_factor. 

1180 grid_expansion : int, default=20 

1181 The grid expansion. 

1182 enforce_gridless : bool, default=False 

1183 If this is `True`, gridless metadynamics is enforced even for 1D to 3D 

1184 problems. 

1185 buffer_size : int, default=100 

1186 The buffer size. 

1187 

1188 Example 

1189 ------- 

1190 >>> import ufedmm 

1191 >>> from openmm import unit 

1192 >>> model = ufedmm.AlanineDipeptideModel(water='tip3p') 

1193 >>> mass = 50*unit.dalton*(unit.nanometer/unit.radians)**2 

1194 >>> Ks = 1000*unit.kilojoules_per_mole/unit.radians**2 

1195 >>> Ts = 1500*unit.kelvin 

1196 >>> limit = 180*unit.degrees 

1197 >>> s_phi = ufedmm.DynamicalVariable( 

1198 ... 's_phi', -limit, limit, mass, Ts, model.phi, Ks 

1199 ... ) 

1200 >>> s_psi = ufedmm.DynamicalVariable( 

1201 ... 's_psi', -limit, limit, mass, Ts, model.psi, Ks 

1202 ... ) 

1203 >>> ufedmm.UnifiedFreeEnergyDynamics([s_phi, s_psi], 300*unit.kelvin) 

1204 <variables=[s_phi, s_psi], temperature=300, height=None, frequency=None> 

1205 """ 

1206 

1207 def __init__( 

1208 self, 

1209 variables, 

1210 temperature, 

1211 height=None, 

1212 frequency=None, 

1213 bias_factor=None, 

1214 grid_expansion=20, 

1215 enforce_gridless=False, 

1216 buffer_size=100, 

1217 ): 

1218 self.variables = variables 

1219 self.temperature = _standardized(temperature) 

1220 self.height = _standardized(height) 

1221 self.frequency = frequency 

1222 self.bias_factor = bias_factor 

1223 self.grid_expansion = grid_expansion 

1224 self.enforce_gridless = enforce_gridless 

1225 self.buffer_size = buffer_size 

1226 

1227 dimensions = sum(v.sigma is not None for v in variables) 

1228 self._metadynamics = not ( 

1229 dimensions == 0 or height is None or frequency is None 

1230 ) 

1231 

1232 def __repr__(self): 

1233 properties = ( 

1234 f"temperature={self.temperature}, " 

1235 f"height={self.height}, " 

1236 f"frequency={self.frequency}" 

1237 ) 

1238 return f'<variables=[{", ".join(v.id for v in self.variables)}], {properties}>' 

1239 

1240 def __getstate__(self): 

1241 return dict( 

1242 variables=self.variables, 

1243 temperature=self.temperature, 

1244 height=self.height, 

1245 frequency=self.frequency, 

1246 bias_factor=self.bias_factor, 

1247 grid_expansion=self.grid_expansion, 

1248 enforce_gridless=self.enforce_gridless, 

1249 buffer_size=self.buffer_size, 

1250 ) 

1251 

1252 def __setstate__(self, kw): 

1253 self.__init__(**kw) 

1254 

1255 def simulation( 

1256 self, topology, system, integrator, platform=None, platformProperties=None 

1257 ): 

1258 """Returns a ExtendedSpaceSimulation object. 

1259 

1260 .. warning:: 

1261 If the temperature of any driving parameter is different from the 

1262 particle-system temperature, then the passed integrator must be a 

1263 :OpenMM:`CustomIntegrator` object containing a per-dof variable `kT` whose 

1264 content is the Boltzmann constant times the temperature associated to each 

1265 degree of freedom. This is true for all integrators available in 

1266 :mod:`ufedmm.integrators`, which are subclasses of 

1267 :class:`ufedmm.integrators.CustomIntegrator`. 

1268 

1269 Parameters 

1270 ---------- 

1271 topology : openmm.app.Topology 

1272 The topology. 

1273 system : openmm.System 

1274 The system. 

1275 integrator : 

1276 The integrator. 

1277 

1278 Keyword Args 

1279 ------------ 

1280 platform : openmm.Platform, default=None 

1281 The platform. 

1282 platformProperties : dict, default=None 

1283 The platform properties. 

1284 

1285 Example 

1286 ------- 

1287 >>> import ufedmm 

1288 >>> from openmm import unit 

1289 >>> model = ufedmm.AlanineDipeptideModel(water='tip3p') 

1290 >>> mass = 50*unit.dalton*(unit.nanometer/unit.radians)**2 

1291 >>> Ks = 1000*unit.kilojoules_per_mole/unit.radians**2 

1292 >>> Ts = 1500*unit.kelvin 

1293 >>> dt = 2*unit.femtoseconds 

1294 >>> gamma = 10/unit.picoseconds 

1295 >>> limit = 180*unit.degrees 

1296 >>> s_phi = ufedmm.DynamicalVariable( 

1297 ... 's_phi', -limit, limit, mass, Ts, model.phi, Ks 

1298 ... ) 

1299 >>> s_psi = ufedmm.DynamicalVariable( 

1300 ... 's_psi', -limit, limit, mass, Ts, model.psi, Ks 

1301 ... ) 

1302 >>> ufed = ufedmm.UnifiedFreeEnergyDynamics([s_phi, s_psi], 300*unit.kelvin) 

1303 >>> integrator = ufedmm.GeodesicLangevinIntegrator( 

1304 ... 300*unit.kelvin, gamma, dt 

1305 ... ) 

1306 >>> simulation = ufed.simulation(model.topology, model.system, integrator) 

1307 """ 

1308 simulation = ExtendedSpaceSimulation( 

1309 self.variables, 

1310 topology, 

1311 system, 

1312 integrator, 

1313 platform, 

1314 platformProperties, 

1315 ) 

1316 

1317 if self._metadynamics: 

1318 simulation.add_periodic_task( 

1319 _Metadynamics( 

1320 self.variables, 

1321 self.height, 

1322 self.frequency, 

1323 bias_factor=self.bias_factor, 

1324 buffer_size=self.buffer_size, 

1325 grid_expansion=self.grid_expansion, 

1326 enforce_gridless=self.enforce_gridless, 

1327 ), 

1328 ) 

1329 

1330 if any(v.temperature != self.temperature for v in self.variables): 

1331 ntotal = system.getNumParticles() 

1332 nvars = len(self.variables) 

1333 simulation.context.setPositions( 

1334 [openmm.Vec3(0, 0, 0)] * (ntotal - nvars), [0] * nvars 

1335 ) 

1336 vartemps = [v.temperature for v in self.variables] 

1337 if isinstance(integrator, ufedmm.integrators.CustomIntegrator): 

1338 integrator.update_temperatures(self.temperature, vartemps) 

1339 else: 

1340 temperatures = [self.temperature] * (ntotal - len(vartemps)) + vartemps 

1341 kT = [ 

1342 unit.MOLAR_GAS_CONSTANT_R * T * openmm.Vec3(1, 1, 1) 

1343 for T in temperatures 

1344 ] 

1345 integrator.setPerDofVariableByName("kT", kT) 

1346 

1347 return simulation