def test_create_openmm_topology(): system, _ = get_system('cau13') with pytest.raises(AssertionError): create_openmm_topology(system) rvecs = system.cell._get_rvecs().copy() transform_lower_triangular(system.pos, rvecs, reorder=True) reduce_box_vectors(rvecs) system.cell.update_rvecs(rvecs) topology = create_openmm_topology(system) # verify box vectors are correct a, b, c = topology.getPeriodicBoxVectors() assert np.allclose( a.value_in_unit(unit.angstrom), rvecs[0, :] / molmod.units.angstrom, ) assert np.allclose( b.value_in_unit(unit.angstrom), rvecs[1, :] / molmod.units.angstrom, ) assert np.allclose( c.value_in_unit(unit.angstrom), rvecs[2, :] / molmod.units.angstrom, )
def test_wrap_coordinates(): for name in ['cau13', 'uio66', 'ppycof', 'mof5', 'mil53', 'cof5']: ff = get_system(name, return_forcefield=True) positions = ff.system.pos.copy() rvecs = ff.system.cell._get_rvecs().copy() rvecs_ = ff.system.cell._get_rvecs().copy() ff.update_pos(positions) ff.update_rvecs(rvecs) e = ff.compute() # make random periodic displacements for i in range(100): coefficients = np.random.randint(0, high=3, size=(3, 1)) atom = np.random.randint(0, high=ff.system.natom) positions[atom, :] += np.sum(coefficients * rvecs, axis=0) ff.update_pos(positions) ff.update_rvecs(rvecs) e0 = ff.compute() assert np.allclose(e, e0) wrap_coordinates(positions, rvecs, rectangular=False) frac = np.dot(positions, np.linalg.inv(rvecs)) # fractional coordinates assert np.all(frac >= 0) assert np.all(frac <= 1) assert np.allclose(rvecs, rvecs_) # rvecs should not change ff.update_pos(positions) ff.update_rvecs(rvecs) e1 = ff.compute() assert np.allclose(e0, e1) with pytest.raises(AssertionError): wrap_coordinates(positions, rvecs, rectangular=True) # transform rvecs transform_lower_triangular(positions, rvecs, reorder=False) reduce_box_vectors(rvecs) wrap_coordinates(positions, rvecs, rectangular=True) for i in range(positions.shape[0]): assert np.all(np.abs(positions[i, :]) < np.diag(rvecs)) ff.update_pos(positions) ff.update_rvecs(rvecs) e2 = ff.compute() assert np.allclose(e0, e2) # reorder rvecs transform_lower_triangular(positions, rvecs, reorder=True) reduce_box_vectors(rvecs) wrap_coordinates(positions, rvecs, rectangular=True) for i in range(positions.shape[0]): assert np.all(np.abs(positions[i, :]) < np.diag(rvecs)) ff.update_pos(positions) ff.update_rvecs(rvecs) e3 = ff.compute() assert np.allclose(e0, e3)
def log_system(self): """Logs information about this system""" log_header('system information', logger) logger.info('') logger.info('') natom = self.system.natom if self.box is not None: system_type = 'periodic' else: system_type = 'non-periodic' logger.info('number of atoms: {}'.format(natom)) logger.info('system type: ' + system_type) logger.info('') if self.box is not None: lengths, angles = compute_lengths_angles(self.box, degree=True) logger.info('initial box vectors (in angstrom):') logger.info('\ta: {}'.format(self.box[0, :])) logger.info('\tb: {}'.format(self.box[1, :])) logger.info('\tc: {}'.format(self.box[2, :])) logger.info('') logger.info('initial box lengths (in angstrom):') logger.info('\ta: {:.4f}'.format(lengths[0])) logger.info('\tb: {:.4f}'.format(lengths[1])) logger.info('\tc: {:.4f}'.format(lengths[2])) logger.info('') logger.info('initial box angles (in degrees):') logger.info('\talpha: {:.4f}'.format(angles[0])) logger.info('\tbeta : {:.4f}'.format(angles[1])) logger.info('\tgamma: {:.4f}'.format(angles[2])) logger.info('') logger.info('') rvecs = np.array(self.box) # create copy transform_lower_triangular(np.zeros((1, 3)), rvecs, reorder=True) reduce_box_vectors(rvecs) lengths, angles = compute_lengths_angles(rvecs, degree=True) logger.info('REDUCED box vectors (in angstrom):') logger.info('\ta: {}'.format(rvecs[0, :])) logger.info('\tb: {}'.format(rvecs[1, :])) logger.info('\tc: {}'.format(rvecs[2, :])) logger.info('') logger.info('REDUCED box lengths (in angstrom):') logger.info('\ta: {:.4f}'.format(lengths[0])) logger.info('\tb: {:.4f}'.format(lengths[1])) logger.info('\tc: {:.4f}'.format(lengths[2])) logger.info('') logger.info('REDUCED box angles (in degrees):') logger.info('\talpha: {:.4f}'.format(angles[0])) logger.info('\tbeta : {:.4f}'.format(angles[1])) logger.info('\tgamma: {:.4f}'.format(angles[2])) logger.info('') logger.info('found {} prefixes:'.format(len(self.prefixes))) for prefix in self.prefixes: logger.info('\t' + prefix) logger.info('') logger.info('')
def test_lattice_reduction(): system, pars = get_system('cau13') pos = system.pos.copy() rvecs = system.cell._get_rvecs().copy() # use reduction algorithm from Bekker, and transform to diagonal reduced = do_gram_schmidt_reduction(rvecs) reduced_LT = np.linalg.cholesky(reduced @ reduced.T) assert np.allclose(reduced_LT, np.diag(np.diag(reduced_LT))) # diagonal # transform to lower triangular transform_lower_triangular(pos, rvecs, reorder=True) reduce_box_vectors(rvecs) # assert equality of diagonal elements from both methods np.testing.assert_almost_equal(np.diag(rvecs), np.diag(reduced_LT))
def test_periodic(): systems = ['uio66', 'cau13', 'mil53', 'ppycof', 'cof5', 'mof5'] platforms = ['Reference'] seed_kinds = ['covalent', 'dispersion', 'electrostatic'] # systematic constant offset in dispersion energy for COFs, unclear why tolerance = { ('Reference', 'covalent'): 1e-6, # some MM3 terms have error 1e-7 ('Reference', 'dispersion'): 1e-2, # some MM3 terms have error 1e-3 ('Reference', 'electrostatic'): 1e-3, #('CUDA', 'covalent'): 1e-3, #('CUDA', 'dispersion'): 1e-3, #('CUDA', 'electrostatic'): 1e-3, } nstates = 5 disp_ampl = 0.3 box_ampl = 0.3 for name in systems: for platform in platforms: for kind in seed_kinds: system, pars = get_system(name) configuration = Configuration(system, pars) tol = tolerance[(platform, kind)] # YAFF and OpenMM use a different switching function. If it is disabled, # the results between both are identical up to 6 decimals configuration.switch_width = 0.0 # disable switching configuration.rcut = 13.0 # request cutoff of 13 angstorm configuration.interaction_radius = 15.0 configuration.update_properties(configuration.write()) conversion = ExplicitConversion(pme_error_thres=5e-4) seed_mm = conversion.apply(configuration, seed_kind=kind) seed_yaff = configuration.create_seed(kind=kind) wrapper_mm = OpenMMForceFieldWrapper.from_seed( seed_mm, platform) wrapper_yaff = YaffForceFieldWrapper.from_seed(seed_yaff) assert wrapper_yaff.periodic # system should not be considered periodic assert wrapper_mm.periodic # system should not be considered periodic pos = seed_yaff.system.pos.copy() rvecs = seed_yaff.system.cell._get_rvecs().copy() for i in range(nstates): dpos = np.random.uniform(-disp_ampl, disp_ampl, size=pos.shape) drvecs = np.random.uniform(-box_ampl, box_ampl, size=rvecs.shape) drvecs[0, 1] = 0 drvecs[0, 2] = 0 drvecs[1, 2] = 0 tmp = rvecs + drvecs reduce_box_vectors(tmp) energy_mm, forces_mm = wrapper_mm.evaluate( (pos + dpos) / molmod.units.angstrom, rvecs=tmp / molmod.units.angstrom, ) energy, forces = wrapper_yaff.evaluate( (pos + dpos) / molmod.units.angstrom, rvecs=tmp / molmod.units.angstrom, ) assert_tol(energy, energy_mm, tol) assert_tol(forces, forces_mm, 10 * tol)
def _internal_validate(self, configuration, conversion, platform, kind): """Calculates the numerical stress over a series of states""" assert configuration.box is not None, ( 'cannot compute numerical stress' ' for nonperiodic systems') # perform conversion, initialize arrays and wrappers seed_yaff = configuration.create_seed(kind) seed_mm = conversion.apply(configuration, seed_kind=kind) stress = np.zeros( (3, 6, self.nstates)) # stores energies and rel error wrapper_yaff = YaffForceFieldWrapper.from_seed(seed_yaff) wrapper_mm = OpenMMForceFieldWrapper.from_seed(seed_mm, platform) # generate states states = [] positions = seed_yaff.system.pos.copy() / angstrom rvecs = seed_yaff.system.cell._get_rvecs().copy() / angstrom for i in range(self.nstates): delta = 2 * self.disp_ampl * np.random.uniform( size=positions.shape) state = (positions + delta, ) delta = 2 * self.box_ampl * np.random.uniform(size=rvecs.shape) delta[0, 1] = 0 delta[0, 2] = 0 delta[1, 2] = 0 drvecs = rvecs + delta reduce_box_vectors(drvecs) # possibly no longer reduced state += (drvecs, ) states.append(state) logger.info('') logger.info('') logger.info('\t\tPLATFORM: {} \t\t INTERACTION: {}'.format( platform, kind)) logger.info('-' * 90) prefixes = configuration.get_prefixes(kind) if len(prefixes) > 0: # ignore empty parts nspaces = 4 header = ' ' * (9) + 'YAFF [kJ/angstrom**3]' header += nspaces * ' ' header += ' OpenMM [kJ/angstrom**3]' header += nspaces * ' ' header += ' delta [kJ/angstrom**3]' logger.info(header) for i, state in enumerate(states): stress_yaff = wrapper_yaff.compute_stress( *state, dh=self.dh, use_symmetric=True, ) stress_mm = wrapper_mm.compute_stress( *state, dh=self.dh, use_symmetric=True, ) # symmetrize and print six components stress_yaff = (stress_yaff + stress_yaff.T) / 2 stress_mm = (stress_mm + stress_mm.T) / 2 nspaces = 9 start = 3 value_yaff = np.trace(stress_yaff) / 3 value_mm = np.trace(stress_mm) / 3 line = 'PRESSURE:' + start * ' ' line += '{:17.4f}'.format(value_yaff) line += nspaces * ' ' line += '{:17.4f}'.format(value_mm) line += nspaces * ' ' line += '{:17.4f}'.format(value_yaff - value_mm) logger.info(line) value_yaff = stress_yaff[0, 0] / 3 value_mm = stress_mm[0, 0] / 3 line = 'sigma_xx:' + start * ' ' line += '{:17.4f}'.format(value_yaff) line += nspaces * ' ' line += '{:17.4f}'.format(value_mm) line += nspaces * ' ' line += '{:17.4f}'.format(value_yaff - value_mm) logger.info(line) value_yaff = stress_yaff[1, 1] / 3 value_mm = stress_mm[1, 1] / 3 line = 'sigma_yy:' + start * ' ' line += '{:17.4f}'.format(value_yaff) line += nspaces * ' ' line += '{:17.4f}'.format(value_mm) line += nspaces * ' ' line += '{:17.4f}'.format(value_yaff - value_mm) logger.info(line) value_yaff = stress_yaff[2, 2] / 3 value_mm = stress_mm[2, 2] / 3 line = 'sigma_zz:' + start * ' ' line += '{:17.4f}'.format(value_yaff) line += nspaces * ' ' line += '{:17.4f}'.format(value_mm) line += nspaces * ' ' line += '{:17.4f}'.format(value_yaff - value_mm) logger.info(line) value_yaff = stress_yaff[1, 2] / 3 value_mm = stress_mm[1, 2] / 3 line = 'sigma_yz:' + start * ' ' line += '{:17.4f}'.format(value_yaff) line += nspaces * ' ' line += '{:17.4f}'.format(value_mm) line += nspaces * ' ' line += '{:17.4f}'.format(value_yaff - value_mm) logger.info(line) value_yaff = stress_yaff[0, 2] / 3 value_mm = stress_mm[0, 2] / 3 line = 'sigma_xz:' + start * ' ' line += '{:17.4f}'.format(value_yaff) line += nspaces * ' ' line += '{:17.4f}'.format(value_mm) line += nspaces * ' ' line += '{:17.4f}'.format(value_yaff - value_mm) logger.info(line) value_yaff = stress_yaff[0, 1] / 3 value_mm = stress_mm[0, 1] / 3 line = 'sigma_xy:' + start * ' ' line += '{:17.4f}'.format(value_yaff) line += nspaces * ' ' line += '{:17.4f}'.format(value_mm) line += nspaces * ' ' line += '{:17.4f}'.format(value_yaff - value_mm) logger.info(line) logger.info('') else: logger.info('\tno {} interactions present'.format(kind))
def _internal_validate(self, configuration, conversion, platform, kind): """Performs single point validations""" # perform conversion, initialize arrays and wrappers seed_yaff = configuration.create_seed(kind) seed_mm = conversion.apply(configuration, seed_kind=kind) energy = np.zeros((4, self.nstates)) # stores energies and rel error forces = np.zeros((3, self.nstates, seed_yaff.system.natom, 3)) wrapper_yaff = YaffForceFieldWrapper.from_seed(seed_yaff) wrapper_mm = OpenMMForceFieldWrapper.from_seed(seed_mm, platform) # generate states states = [] positions = seed_yaff.system.pos.copy() / angstrom if configuration.box is not None: rvecs = seed_yaff.system.cell._get_rvecs().copy() / angstrom for i in range(self.nstates): delta = 2 * self.disp_ampl * np.random.uniform( size=positions.shape) state = (positions + delta, ) if configuration.box is not None: delta = 2 * self.box_ampl * np.random.uniform(size=rvecs.shape) delta[0, 1] = 0 delta[0, 2] = 0 delta[1, 2] = 0 drvecs = rvecs + delta reduce_box_vectors(drvecs) # possibly no longer reduced state += (drvecs, ) states.append(state) logger.info('') logger.info('\t\tPLATFORM: {} \t\t INTERACTION: {}'.format( platform, kind)) logger.info('-' * 91) prefixes = configuration.get_prefixes(kind) if len(prefixes) > 0: # ignore empty parts nspaces = 10 last = 5 header = ' YAFF [kJ/mol]' header += nspaces * ' ' header += ' OpenMM [kJ/mol]' header += nspaces * ' ' header += ' delta [kJ/mol]' header += last * ' ' header += 'relative error' logger.info(header) for i, state in enumerate(states): energy[0, i], forces[0, i] = wrapper_yaff.evaluate( *state, do_forces=True, ) energy[1, i], forces[1, i] = wrapper_mm.evaluate( *state, do_forces=True, ) energy[2, i] = energy[1, i] - energy[0, i] energy[3, i] = np.abs(energy[2, i]) / np.abs(energy[0, i]) line = ' {:17.4f}'.format(energy[0, i]) line += nspaces * ' ' line += '{:17.4f}'.format(energy[1, i]) line += nspaces * ' ' line += '{:17.4f}'.format(energy[2, i]) line += last * ' ' line += '{:14.4e}'.format(energy[3, i]) logger.info(line) df = np.abs(forces[1, :] - forces[0, :]) error = np.linalg.norm(df, axis=2) norm = np.mean(np.linalg.norm(forces[0, :], axis=2)) logger.info('') nspaces = 4 line = '\tFORCES RELATIVE ERROR: \t' line += 'mean={:.1e}'.format(np.mean(error) / norm) line += nspaces * ' ' line += 'median={:.1e}'.format(np.median(error) / norm) line += nspaces * ' ' line += 'min={:.1e}'.format(np.min(error) / norm) line += nspaces * ' ' line += 'max={:.1e}'.format(np.max(error) / norm) logger.info(line) logger.info('-' * 91) logger.info('') logger.info('') else: logger.info('\tno {} interactions present'.format(kind))
def create_topology(self): """Creates the topology for the current configuration""" if self.topology is not None: positions = self.system.pos / molmod.units.angstrom if self.box is not None: assert tuple(self.determine_supercell()) == (1, 1, 1) box = self.box.copy() transform_lower_triangular(positions, box, reorder=True) reduce_box_vectors(box) #wrap_coordinates(positions, box) # copy topology topology = deepcopy(self.topology) topology.setPeriodicBoxVectors(box * unit.angstrom) return self.topology, positions topology = mm.app.Topology() chain = topology.addChain() natoms = self.system.natom count = 0 if self.box is not None: supercell = self.determine_supercell() # compute box vectors and allocate positions array box = np.array(supercell)[:, np.newaxis] * self.box positions = np.zeros((np.prod(supercell) * natoms, 3)) # construct map of atom indices to residues atom_index_mapping = {} for template, residues in self.residues.items(): for i, residue in enumerate(residues): for j, atom in enumerate(residue): atom_index_mapping[atom] = (template, i, j) included_atoms = list(atom_index_mapping.keys()) assert tuple(sorted(included_atoms)) == tuple(range(natoms)) def name_residue(image, template, residue_index): """Defines the name of a specific residue""" return str(image) + '_' + str(template) + '_' + str(i) atoms_list = [] # necessary for adding bonds to topology for image, index in enumerate(np.ndindex(tuple(supercell))): # initialize residues and track them in a dict current_residues = {} for template, residues in self.residues.items(): for i, residue in enumerate(residues): name = name_residue(image, template, i) residue = topology.addResidue( name=name, chain=chain, id=name, ) current_residues[(template, i)] = residue # add atoms to corresponding residue in topology (in their # original order). Atoms are named with their element symbol # as well as an index that starts counting at one count = np.ones(118, dtype=np.int32) # counts elements for j in range(natoms): key = atom_index_mapping[j] template = key[0] residue_index = key[1] atom_index = key[2] number = self.system.numbers[j] e = mm.app.Element.getByAtomicNumber(number) atom_name = e.symbol + str(count[number]) atom = topology.addAtom( name=atom_name, element=e, residue=current_residues[(template, residue_index)]) atoms_list.append(atom) count[number] += 1 # generate positions for this image image_pos = self.system.pos / molmod.units.angstrom translate = np.dot(np.array(index), self.box).reshape(1, 3) image_pos += translate start = (image) * natoms stop = (image + 1) * natoms positions[start:stop, :] = image_pos.copy() # apply cell reduction and wrap coordinates (similar to create_seed) transform_lower_triangular(positions, box, reorder=True) reduce_box_vectors(box) #wrap_coordinates(positions, box) topology.setPeriodicBoxVectors(box * unit.angstrom) # add bonds from supercell system object system = self.system.supercell(*supercell) for bond in system.bonds: topology.addBond( atoms_list[bond[0]], atoms_list[bond[1]], ) else: # similar workflow, but without the supercell generation positions = self.system.pos / molmod.units.angstrom # construct map of atom indices to residues atom_index_mapping = {} for template, residues in self.residues.items(): for i, residue in enumerate(residues): for j, atom in enumerate(residue): atom_index_mapping[atom] = (template, i, j) included_atoms = list(atom_index_mapping.keys()) assert tuple(sorted(included_atoms)) == tuple(range(natoms)) def name_residue(template, residue_index): """Defines the name of a specific residue""" return str(template) + '_' + str(residue_index) atoms_list = [] # necessary for adding bonds to topology # initialize residues and track them in a dict current_residues = {} for template, residues in self.residues.items(): for i, residue in enumerate(residues): name = name_residue(template, i) residue = topology.addResidue( name='r' + str(i), chain=chain, #id='r' + str(i), ) current_residues[(template, i)] = residue # add atoms to corresponding residue in topology (in their # original order) for j in range(natoms): key = atom_index_mapping[j] template = key[0] residue_index = key[1] atom_index = key[2] e = mm.app.Element.getByAtomicNumber(self.system.numbers[j]) atom_name = 'a' + str(j) atom = topology.addAtom( name=atom_name, element=e, residue=current_residues[(template, residue_index)]) atoms_list.append(atom) # add bonds from system object for bond in self.system.bonds: topology.addBond( atoms_list[bond[0]], atoms_list[bond[1]], ) return topology, positions
def create_seed(self, kind='all'): """Creates a seed for constructing a yaff.ForceField object The returned seed contains all objects that are required in order to generate the force field object unambiguously. Specifically, this involves a yaff.System object, a yaff.FFArgs object with the correct parameter settings, and a yaff.Parameters object that contains the actual force field parameters. Because tests are typically performed on isolated parts of a force field, it is possible to generate different seeds corresponding to different parts -- this is done using the kind parameter. Allowed values are: - all: generates the entire force field - covalent generates only the covalent part of the force field. - nonbonded generates only the nonbonded part of the force field, including both dispersion and electrostatics. - dispersion generates only the dispersion part of the force field, which is basically the nonbonded part minus the electrostatics. - electrostatic generates only the electrostatic part Parameters ---------- kind : str, optional specifies the kind of seed to be created. Allowed values are 'all', 'covalent', 'nonbonded', 'dispersion', 'electrostatic' """ assert kind in [ 'all', 'covalent', 'nonbonded', 'dispersion', 'electrostatic' ] parameters = self.parameters.copy() if kind == 'all': pass # do nothing, all prefixes should be retained elif kind == 'covalent': # pop all dispersion and electrostatic prefixes: for key in (DISPERSION_PREFIXES + ELECTROSTATIC_PREFIXES): parameters.sections.pop(key, None) # returns None if not present elif kind == 'nonbonded': # retain only dispersion and electrostatic sections = {} for key in (DISPERSION_PREFIXES + ELECTROSTATIC_PREFIXES): section = parameters.sections.get(key, None) if section is not None: # only add if present sections[key] = section parameters = yaff.Parameters(sections) elif kind == 'dispersion': # retain only dispersion sections = {} for key in DISPERSION_PREFIXES: section = parameters.sections.get(key, None) if section is not None: # only add if present sections[key] = section parameters = yaff.Parameters(sections) elif kind == 'electrostatic': # retain only electrostatic sections = {} for key in ELECTROSTATIC_PREFIXES: section = parameters.sections.get(key, None) if section is not None: # only add if present sections[key] = section parameters = yaff.Parameters(sections) else: raise NotImplementedError(kind + ' not known.') # construct FFArgs instance and set properties ff_args = yaff.FFArgs() if self.box is not None: supercell = self.determine_supercell() system = self.system.supercell(*supercell) # apply reduction rvecs = system.cell._get_rvecs().copy() transform_lower_triangular(system.pos, rvecs, reorder=True) reduce_box_vectors(rvecs) #wrap_coordinates(system.pos, rvecs) system.cell.update_rvecs(rvecs) else: system = self.system if self.rcut is not None: ff_args.rcut = self.rcut * molmod.units.angstrom else: ff_args.rcut = 1e10 # humongous value; for nonperiodic systems if self.switch_width is not None and (self.switch_width != 0.0): ff_args.tr = yaff.Switch3(self.switch_width * molmod.units.angstrom) else: ff_args.tr = None if self.tailcorrections is not None: ff_args.tailcorrections = self.tailcorrections if self.ewald_alphascale is not None: ff_args.alpha_scale = self.ewald_alphascale if self.ewald_gcutscale is not None: ff_args.gcut_scale = self.ewald_gcutscale return YaffSeed(system, parameters, ff_args)