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 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_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 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_transform_symmetric(): system, pars = get_system('mil53') pos = system.pos.copy() rvecs = system.cell._get_rvecs().copy() # transform to symmetric form transform_symmetric(pos, rvecs) assert np.allclose(rvecs, rvecs.T) # transform to triangular, and back to symmetric rvecs_ = rvecs.copy() pos_ = pos.copy() transform_lower_triangular(pos_, rvecs_, reorder=False) transform_symmetric(pos_, rvecs_) # assert equality np.testing.assert_almost_equal(rvecs_, rvecs) np.testing.assert_almost_equal(pos_, pos)
def test_transform_lower_triangular(): for i in range(100): trial = np.random.uniform(-20, 20, size=(3, 3)) trial *= np.sign(np.linalg.det(trial)) assert np.linalg.det(trial) > 0 pos = np.random.uniform(-100, 100, size=(10, 3)) transform_lower_triangular(pos, trial) # in-place # comparison with cholesky made inside transform_lower_triangular for name in ['cau13', 'uio66']: # FAILS ON COBDP; ewald_reci changes ff = get_system(name, return_forcefield=True) # nonrectangular system gpos0 = np.zeros((ff.system.natom, 3)) energy0 = ff.compute(gpos0, None) rvecs = ff.system.cell._get_rvecs().copy() transform_lower_triangular(ff.system.pos, rvecs, reorder=True) assert is_lower_triangular(rvecs) ff.update_pos(ff.system.pos) ff.update_rvecs(rvecs) gpos1 = np.zeros((ff.system.natom, 3)) energy1 = ff.compute(gpos1, None) np.testing.assert_almost_equal( # energy should remain the same energy0, energy1, )
def test_estimate_virial_stress(): def energy_func(positions, rvecs): ff.update_pos(positions) ff.update_rvecs(rvecs) return ff.compute() # verify numerical pressure computation for number of benchmark systems # include anisotropic systems and LJ dh = 1e-5 for name in ['cau13', 'uio66', 'ppycof', 'lennardjones']: ff = get_system(name, return_forcefield=True) positions = ff.system.pos.copy() rvecs = ff.system.cell._get_rvecs().copy() vtens = np.zeros((3, 3)) ff.compute(None, vtens) unit = molmod.units.pascal * 1e6 pressure = np.trace(vtens) / np.linalg.det(rvecs) / unit dUdh = estimate_cell_derivative(positions, rvecs, energy_func, dh=dh, use_triangular_perturbation=False) vtens_numerical = rvecs.T @ dUdh pressure_ = np.trace(vtens_numerical) / np.linalg.det(rvecs) / unit assert abs(pressure - pressure_) < 1e-3 # require at least kPa accuracy assert np.allclose(vtens_numerical, vtens, atol=1e-5) transform_lower_triangular(positions, rvecs, reorder=True) ff.update_pos(positions) ff.update_rvecs(rvecs) vtens = np.zeros((3, 3)) ff.compute(None, vtens) pressure_LT = np.trace(vtens) / np.linalg.det(rvecs) / unit dUdh = estimate_cell_derivative(positions, rvecs, energy_func, dh=dh, use_triangular_perturbation=True) vtens_numerical = rvecs.T @ dUdh pressure_LT_ = np.trace(vtens_numerical) / np.linalg.det(rvecs) / unit assert abs(pressure_LT - pressure) < 1e-8 # should be identical assert abs(pressure_LT_ - pressure_) < 1e-3 # require kPa accuracy #assert np.allclose(vtens_numerical, vtens, atol=1e-5) # VTENS != VTENS_NUMERICAL HERE! transform_symmetric(positions, rvecs) ff.update_pos(positions) ff.update_rvecs(rvecs) vtens = np.zeros((3, 3)) ff.compute(None, vtens) pressure_S = np.trace(vtens) / np.linalg.det(rvecs) / unit dUdh = estimate_cell_derivative(positions, rvecs, energy_func, dh=dh) vtens_numerical = rvecs.T @ dUdh assert np.allclose(vtens_numerical, vtens_numerical.T, atol=1e5) pressure_S_ = np.trace(vtens_numerical) / np.linalg.det(rvecs) / unit assert abs(pressure_S - pressure) < 1e-8 # should be identical assert abs(pressure_S_ - pressure_) < 1e-3 # require kPa accuracy assert np.allclose(vtens_numerical, vtens, atol=1e-5) # check evaluate_using_reduced=True gives same results dUdh_r = estimate_cell_derivative(positions, rvecs, energy_func, dh=dh, evaluate_using_reduced=True) vtens_numerical_r = rvecs.T @ dUdh_r assert np.allclose(vtens_numerical_r, vtens_numerical_r.T, atol=1e-5) assert np.allclose(vtens_numerical_r, vtens_numerical, atol=1e-5)
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)