def test_inner_exceptions(): model = ufedmm.AlanineDipeptideModel() nbforce = next( filter(lambda f: isinstance(f, openmm.NonbondedForce), model.system.getForces())) rs = 0.2 rc = 0.4 ufedmm.add_inner_nonbonded_force(model.system, rs, rc, 1) model.system.getForce(model.system.getNumForces() - 1).setForceGroup(3) platform = openmm.Platform.getPlatformByName('Reference') context = openmm.Context(model.system, openmm.CustomIntegrator(0), platform) context.setPositions(model.positions) forces1 = _standardized( context.getState(getForces=True, groups={3}).getForces()) forces2 = [0 * f for f in forces1] ONE_4PI_EPS0 = 138.93545764438198 for index in range(nbforce.getNumExceptions()): i, j, chargeprod, sigma, epsilon = map( _standardized, nbforce.getExceptionParameters(index)) rij = _standardized(model.positions[i] - model.positions[j]) r = np.linalg.norm(rij) z = (r - rs) / (rc - rs) F = S(z) * (24 * epsilon * (2 * (sigma / r)**12 - (sigma / r)**6) / r + ONE_4PI_EPS0 * chargeprod / r**2) * rij / r forces2[i] += F forces2[j] -= F for f1, f2 in zip(forces1, forces2): for i in range(3): assert f1[i] == pytest.approx(f2[i])
def update_temperatures(self, system_temperature, extended_space_temperatures): super().update_temperatures(system_temperature, extended_space_temperatures) kT_vectors = self.getPerDofVariableByName('kT') tauSq = _standardized(self._tau)**2 Q = [tauSq*kT for kT in kT_vectors] invQ = [openmm.Vec3(*map(lambda x: 1/x if x > 0.0 else 0.0, q)) for q in Q] self.setPerDofVariableByName('invQ', invQ)
def free_energy_functions(self, sigma=None, factor=8): """ Returns Python functions for evaluating the potential of mean force and their originating mean forces as a function of the collective variables. Keyword Args ------------ sigma : float or unit.Quantity, default=None The standard deviation of kernels. If this is `None`, then values will be determined from the distances between nodes. factor : float, default=8 If ``sigma`` is not explicitly provided, then it will be computed as ``sigma = factor*range/bins`` for each direction. Returns ------- potential : function A Python function whose arguments are collective variable values and whose result is the potential of mean force at that values. mean_force : function A Python function whose arguments are collective variable values and whose result is the mean force at that values regarding a given direction. Such direction must be defined through a keyword argument `dir`, whose default value is `0` (meaning the direction of the first collective variable). """ self.centers, self.mean_forces = self.centers_and_mean_forces( self._bins, self._min_count, self._adjust_centers, ) variables = self._ufed.variables if sigma is None: sigmas = [ factor * v._range / bin for v, bin in zip(variables, self._bins) ] else: try: sigmas = [_standardized(value) for value in sigma] except TypeError: sigmas = [_standardized(sigma)] * len(variables) return self.mean_force_free_energy(self.centers, self.mean_forces, sigmas)
def __init__(self, temperature, time_constant, step_size, **kwargs): if 'num_rattles' in kwargs.keys() and kwargs['num_rattles'] != 0: raise ValueError(f'{self.__class__.__name__} cannot handle constraints') self._tau = _standardized(time_constant) super().__init__(temperature, step_size, **kwargs) self.addPerDofVariable('Q1', 0) self.addPerDofVariable('Q2', 0) self.addPerDofVariable('v1', 0) self.addPerDofVariable('v2', 0)
def __init__(self, temperature, time_constant, step_size, nchain=2, **kwargs): if 'num_rattles' in kwargs.keys() and kwargs['num_rattles'] != 0: raise ValueError(f'{self.__class__.__name__} cannot handle constraints') self._tau = _standardized(time_constant) self._nchain = nchain super().__init__(temperature, step_size, **kwargs) self.addPerDofVariable('Q', 0) for i in range(nchain): self.addPerDofVariable(f'v{i+1}', 0)
def free_energy_functions(self, sigma=None, factor=8): """ Returns Python functions for evaluating the potential of mean force and their originating mean forces as a function of the collective variables. Keyword Args ------------ sigma : float or unit.Quantity, default=None The standard deviation of kernels. If this is `None`, then values will be determined from the distances between nodes. factor : float, default=8 If ``sigma`` is not explicitly provided, then it will be computed as ``sigma = factor*range/bins`` for each direction. Returns ------- potential : function A Python function whose arguments are collective variable values and whose result is the potential of mean force at that values. mean_force : function A Python function whose arguments are collective variable values and whose result is the mean force at that values regarding a given direction. Such direction must be defined through a keyword argument `dir`, whose default value is `0` (meaning the direction of the first collective variable). """ if sigma is None: variances = [(factor * v._range / self._bins[i])**2 for i, v in enumerate(self._ufed.variables)] else: try: variances = [_standardized(value)**2 for value in sigma] except TypeError: variances = [_standardized(sigma)**2] * len( self._ufed.variables) exponent = [] derivative = [] for v, variance in zip(self._ufed.variables, variances): if v.periodic: # von Mises factor = 2 * np.pi / v._range exponent.append(lambda x: (np.cos(factor * x) - 1.0) / (factor * factor * variance)) derivative.append(lambda x: -np.sin(factor * x) / (factor * variance)) else: # Gauss exponent.append(lambda x: -0.5 * x**2 / variance) derivative.append(lambda x: -x / variance) n = len(self._ufed.variables) def kernel(x): return np.exp(np.sum(exponent[i](x[i]) for i in range(n))) def gradient(x, i): return kernel(x) * derivative[i](x[i]) centers = [np.array(xc) for xc in zip(*self.centers)] coefficients = [] for i in range(n): for x in centers: coefficients.append( np.array([gradient(x - xc, i) for xc in centers])) M = np.vstack(coefficients) F = -np.hstack(self.mean_forces) A, _, _, _ = np.linalg.lstsq(M, F, rcond=None) kernels = np.empty((len(centers), len(centers))) for i, x in enumerate(centers): kernels[i, :] = np.array([kernel(x - xc) for xc in centers]) potentials = kernels.dot(A) minimum = potentials.min() def potential(*x): xa = np.array(x) kernels = np.array([kernel(xa - xc) for xc in centers]) return np.sum(A * kernels) - minimum def mean_force(*x, dir=0): xa = np.array(x) gradients = np.array([gradient(xa - xc, dir) for xc in centers]) return -np.sum(A * gradients) return np.vectorize(potential), np.vectorize(mean_force)
def mean_force_free_energy(self, centers, mean_forces, sigma, platform_name='Reference', properties={}): """ Returns Python functions for evaluating the potential of mean force and their originating mean forces as a function of the collective variables. Parameters ---------- centers : list(numpy.array) The bin centers. mean_forces : list(numpy.array) The mean forces. sigmas : float or unit.Quantity or list The standard deviation of kernels. Keyword Args ------------ platform_name : string, default='Reference' The name of the OpenMM Platform to be used for potential and mean-force evaluations. properties : dict, default={} A set of values for platform-specific properties. Keys are the property names. Returns ------- potential : function A Python function whose arguments are collective variable values and whose result is the potential of mean force at that values. mean_force : function A Python function whose arguments are collective variable values and whose result is the mean force at those values. """ variables = self._ufed.variables n = len(variables) try: variances = [_standardized(value)**2 for value in sigma] except TypeError: variances = [_standardized(sigma)**2] * n exponent = [] derivative = [] for v, variance in zip(variables, variances): if v.periodic: # von Mises factor = 2 * np.pi / v._range exponent.append(lambda x: (np.cos(factor * x) - 1.0) / (factor * factor * variance)) derivative.append(lambda x: -np.sin(factor * x) / (factor * variance)) else: # Gauss exponent.append(lambda x: -0.5 * x**2 / variance) derivative.append(lambda x: -x / variance) def kernel(x): return np.exp(np.sum(exponent[i](x[i]) for i in range(n))) def gradient(x, i): return kernel(x) * derivative[i](x[i]) grid_points = [np.array(xc) for xc in zip(*centers)] coefficients = [] for i in range(n): for x in grid_points: coefficients.append( np.array([gradient(x - xc, i) for xc in grid_points])) M = np.vstack(coefficients) F = -np.hstack(mean_forces) A, _, _, _ = np.linalg.lstsq(M, F, rcond=None) platform = openmm.Platform.getPlatformByName(platform_name) context = _RBFContext(variables, variances, grid_points, A, platform, properties) minimum = 0.0 def potential(*x): for parameter, value in zip(context.parameters, x): context.setParameter(parameter, value) state = context.getState(getEnergy=True) return state.getPotentialEnergy()._value - minimum def mean_force(*x): for parameter, value in zip(context.parameters, x): context.setParameter(parameter, value) state = context.getState(getParameterDerivatives=True) return -state.getEnergyParameterDerivatives()._value minimum = np.min([potential(*x) for x in grid_points]) return np.vectorize(potential), np.vectorize(mean_force)
def add_inner_nonbonded_force(system, inner_switch, inner_cutoff, force_group_index): """ To a given OpenMM System_ containing a NonbondedForce_ object, this function adds a new force group with the purpose of performing multiple time-scale integration according to the RESPA2 splitting scheme of Morrone, Zhou, and Berne :cite:`Morrone_2010`. Besides, it assigns the provided `force_group_index` to this new group and `force_group_index+1` to the original NonbondedForce_. When used in any instance of :class:`AbstractMiddleRespaIntegrator`, the new force group must be identified as being embodied by the NonbondedForce_ as opposed to being complimentary to it. .. warning: The new force group is not intended to contribute to the system energy. Its sole purpose is to provide a smooth, short-range force calculator for some intermediary time scale in a RESPA-type integration. Parameters ---------- system : openmm.System The system the inner force will be added to, which must contain a NonbondedForce_. inner_switch : float or unit.Quantity The inner switching distance, where the interaction of an atom pair begins to switch off to zero. inner_cutoff : float or unit.Quantity The inner cutoff distance, where the interaction of an atom pairs completely switches off. force_group_index : int The force group the new interactions will belong to. The old NonbondedForce_ will be automatically assigned to `force_group_index+1`. Example ------- >>> import ufedmm >>> from simtk import unit >>> dt = 2*unit.femtoseconds >>> temp = 300*unit.kelvin >>> tau = 10*unit.femtoseconds >>> gamma = 10/unit.picoseconds >>> model = ufedmm.AlanineDipeptideModel() >>> ufedmm.add_inner_nonbonded_force(model.system, 5*unit.angstroms, 8*unit.angstroms, 1) >>> for force in model.system.getForces(): ... print(force.__class__.__name__, force.getForceGroup()) HarmonicBondForce 0 HarmonicAngleForce 0 PeriodicTorsionForce 0 NonbondedForce 2 CustomNonbondedForce 1 CustomBondForce 1 """ if openmm.__version__ < '7.5': raise Exception("add_inner_nonbonded_force requires OpenMM version >= 7.5") try: nonbonded_force = next(filter(lambda f: isinstance(f, openmm.NonbondedForce), system.getForces())) except StopIteration: raise Exception("add_inner_nonbonded_force requires system with NonbondedForce") if nonbonded_force.getNumParticleParameterOffsets() > 0 or nonbonded_force.getNumExceptionParameterOffsets() > 0: raise Exception("add_inner_nonbonded_force does not support parameter offsets") periodic = nonbonded_force.usesPeriodicBoundaryConditions() rs = _standardized(inner_switch) rc = _standardized(inner_cutoff) a = rc+rs b = rc*rs c = (30/(rc-rs)**5)*np.array([b**2, -2*a*b, a**2 + 2*b, -2*a, 1]) f0s = sum([c[n]*rs**(n+1)/(n+1) for n in range(5)]) def coeff(n, m): return c[m-1] if m == n else c[m-1]/(m-n) def func(n, m): return '*log(r)' if m == n else (f'*r^{m-n}' if m > n else f'/r^{n-m}') def val(n, m): return f0s if m == 0 else (coeff(n, m) - coeff(0, m) if n != m else coeff(n, m)) def sgn(n, m): return '+' if m > 0 and val(n, m) >= 0 else '' def S(n): return ''.join(f'{sgn(n, m)}{val(n, m)}{func(n, m)}' for m in range(6)) potential = 'eps4*((sigma/r)^12-(sigma/r)^6)+Qprod/r' potential += f'+step(r-{rs})*(eps4*(sigma^12*({S(12)})-sigma^6*({S(6)}))+Qprod*({S(1)}))' mixing_rules = '; Qprod=Q1*Q2' mixing_rules += '; sigma=halfsig1+halfsig2' mixing_rules += '; eps4=sqrt4eps1*sqrt4eps2' force = openmm.CustomNonbondedForce(potential + mixing_rules) for parameter in ['Q', 'halfsig', 'sqrt4eps']: force.addPerParticleParameter(parameter) force.setNonbondedMethod(force.CutoffPeriodic if periodic else force.CutoffNonPeriodic) force.setCutoffDistance(inner_cutoff) force.setUseLongRangeCorrection(False) ONE_4PI_EPS0 = 138.93545764438198 for index in range(nonbonded_force.getNumParticles()): charge, sigma, epsilon = map(_standardized, nonbonded_force.getParticleParameters(index)) force.addParticle([charge*np.sqrt(ONE_4PI_EPS0), sigma/2, np.sqrt(4*epsilon)]) non_exclusion_exceptions = [] for index in range(nonbonded_force.getNumExceptions()): i, j, q1q2, sigma, epsilon = nonbonded_force.getExceptionParameters(index) q1q2, sigma, epsilon = map(_standardized, [q1q2, sigma, epsilon]) force.addExclusion(i, j) if q1q2 != 0.0 or epsilon != 0.0: non_exclusion_exceptions.append((i, j, q1q2*ONE_4PI_EPS0, sigma, 4*epsilon)) force.setForceGroup(force_group_index) system.addForce(force) if non_exclusion_exceptions: exceptions = openmm.CustomBondForce(f'step({rc}-r)*({potential})') for parameter in ['Qprod', 'sigma', 'eps4']: exceptions.addPerBondParameter(parameter) for i, j, Qprod, sigma, eps4 in non_exclusion_exceptions: exceptions.addBond(i, j, [Qprod, sigma, eps4]) exceptions.setForceGroup(force_group_index) system.addForce(exceptions) nonbonded_force.setForceGroup(force_group_index+1)
def mean_force_free_energy(self, centers, mean_forces, sigma): """ Returns Python functions for evaluating the potential of mean force and their originating mean forces as a function of the collective variables. Parameters ---------- centers : list(numpy.array) The bin centers. mean_forces : list(numpy.array) The mean forces. sigmas : float or unit.Quantity or list The standard deviation of kernels. Returns ------- potential : function A Python function whose arguments are collective variable values and whose result is the potential of mean force at that values. mean_force : function A Python function whose arguments are collective variable values and whose result is the mean force at that values regarding a given direction. Such direction must be defined through a keyword argument `dir`, whose default value is `0` (meaning the direction of the first collective variable). """ variables = self._ufed.variables n = len(variables) try: variances = [_standardized(value)**2 for value in sigma] except TypeError: variances = [_standardized(sigma)**2] * n exponent = [] derivative = [] for v, variance in zip(variables, variances): if v.periodic: # von Mises factor = 2 * np.pi / v._range exponent.append(lambda x: (math.cos(factor * x) - 1.0) / (factor * factor * variance)) derivative.append(lambda x: -math.sin(factor * x) / (factor * variance)) else: # Gauss exponent.append(lambda x: -0.5 * x**2 / variance) derivative.append(lambda x: -x / variance) @jit def kernel(x): trace = 0.0 for i in range(n): trace = trace + exponent[i](x[i]) return math.exp(trace) # return math.exp(np.sum(exponent[i](x[i]) for i in range(n))) # return math.exp(np.sum(exponent[i](x[i]) for i in range(n))) def gradient(x, i): return kernel(x) * derivative[i](x[i]) center_points = [np.array(xc) for xc in zip(*centers)] coefficients = [] start = time.time() for i in range(n): for x in center_points: coefficients.append( np.array([gradient(x - xc, i) for xc in center_points])) end = time.time() print("coefficient-appending", end - start) start3 = time.time() M = np.vstack(coefficients) # M_C=cp.asarray(M) # print(M_C.device) F = -np.hstack(mean_forces) # F_C=cp.asarray(F) # B=cp.linalg.lstsq(M_C,F_C,rcond=None) # end3=time.time() # print("linalg-time",end3-start3) # print("B",B) A, _, _, _ = np.linalg.lstsq(M, F, rcond=None) end3 = time.time() print("linalg-time", end3 - start3) # A=cp.asnumpy(B[0]) start = time.time() kernels = np.empty((len(center_points), len(center_points))) for i, x in enumerate(center_points): kernels[i, :] = np.array([kernel(x - xc) for xc in center_points]) potentials = kernels.dot(A) minimum = potentials.min() end = time.time() print("minimum-find", end - start) def potential(*x): xa = np.array(x) kernels = np.array([kernel(xa - xc) for xc in center_points]) # ker=cp.asarray(kernels) # reslt=cp.sum(A*kernels)-minimum return np.sum(A * kernels) - minimum # return reslt - minimum def mean_force(*x, dir=0): xa = np.array(x) gradients = np.array( [gradient(xa - xc, dir) for xc in center_points]) return -np.sum(A * gradients) return np.vectorize(potential), np.vectorize(mean_force)
def __init__(self, group, nbforce, style='conductor-reaction-field', damping_coefficient=0.2 / unit.angstroms, scaling_parameter_name='inOutCoulombScaling', pbc_for_exceptions=False): rc = _standardized(nbforce.getCutoffDistance()) alpha_c = _standardized(damping_coefficient) * rc prefix = f'{138.935485/rc}*charge1*charge2' if style == 'shifted': u_C = '1/x - 1' elif style == 'shifted-force': u_C = '1/x + x - 2' elif style == 'conductor-reaction-field': u_C = '1/x + x^2/2 - 3/2' elif style == 'reaction-field': epsilon = nbforce.getReactionFieldDielectric() krf = (epsilon - 1) / (2 * epsilon + 1) crf = 3 * epsilon / (2 * epsilon + 1) u_C = f'1/x + {krf}*x^2 - {crf}' elif style == 'damped': u_C = f'erfc({alpha_c}*x)/x' elif style == 'damped-shifted-force': A = math.erfc(alpha_c) B = A + 2 * alpha_c * math.exp(-alpha_c**2) / math.sqrt(math.pi) u_C = f'erfc({alpha_c}*x)/x + {A+B}*x - {2*A+B}' else: raise ValueError("Invalid cutoff electrostatic style") super().__init__(f'{prefix}*({u_C}); x=r/{rc}') parameters = self._get_parameters(nbforce) offset_index = {} for index in range(nbforce.getNumParticleParameterOffsets()): variable, i, charge, _, _ = nbforce.getParticleParameterOffset( index) if variable == scaling_parameter_name: offset_index[i] = index parameters[i] = _ParamTuple(charge, parameters[i].sigma, parameters[i].epsilon) self.addPerParticleParameter('charge') for parameter in parameters: self.addParticle([parameter.charge]) self._update_nonbonded_force(group, nbforce, parameters, pbc_for_exceptions) self._import_properties(nbforce) self.addInteractionGroup( set(group), set(range(nbforce.getNumParticles())) - set(group)) self.setUseLongRangeCorrection(False) global_vars = map(nbforce.getGlobalParameterName, range(nbforce.getNumGlobalParameters())) if scaling_parameter_name not in global_vars: nbforce.addGlobalParameter(scaling_parameter_name, 0.0) for i in group: charge, sigma, epsilon = parameters[i] nbforce.setParticleParameters(i, 0.0, sigma, epsilon) input = [scaling_parameter_name, i, charge, 0.0, 0.0] if i in offset_index: nbforce.setParticleParameterOffset(offset_index[i], *input) else: nbforce.addParticleParameterOffset(*input)