def _write_atoms(lmp_file: IO, openff_sys: System, atom_type_map: Dict): lmp_file.write("\nAtoms\n\n") molecule_map = dict(enumerate( openff_sys.topology.topology_molecules)) # type: ignore[union-attr] molecule_map_inv = dict({v: k for k, v in molecule_map.items()}) atom_type_map_inv = dict({v: k for k, v in atom_type_map.items()}) electrostatics_handler = openff_sys.handlers["Electrostatics"] vdw_hander = openff_sys.handlers["vdW"] for atom_idx, atom in enumerate( openff_sys.topology.topology_atoms): # type: ignore[union-attr] molecule_idx = molecule_map_inv[atom.topology_molecule] top_key = TopologyKey(atom_indices=(atom_idx, )) pot_key = vdw_hander.slot_map[top_key] atom_type = atom_type_map_inv[pot_key] top_key = TopologyKey(atom_indices=(atom_idx, )) charge = electrostatics_handler.charges[ top_key].magnitude # type: ignore[attr-defined] pos = openff_sys.positions[atom_idx].to(unit.angstrom).magnitude lmp_file.write( "{:d}\t{:d}\t{:d}\t{:.8g}\t{:.8g}\t{:.8g}\t{:.8g}\n".format( atom_idx + 1, molecule_idx + 1, atom_type + 1, charge, pos[0], pos[1], pos[2], ))
def _get_buck_parameters(openff_sys: "System", atom_idx: int) -> Dict: buck_hander = openff_sys.handlers["Buckingham-6"] atom_key = TopologyKey(atom_indices=(atom_idx,)) identifier = buck_hander.slot_map[atom_key] potential = buck_hander.potentials[identifier] parameters = potential.parameters return parameters
def _get_lj_parameters(openff_sys: "System", atom_idx: int) -> Dict: vdw_hander = openff_sys.handlers["vdW"] atom_key = TopologyKey(atom_indices=(atom_idx,)) identifier = vdw_hander.slot_map[atom_key] potential = vdw_hander.potentials[identifier] parameters = potential.parameters return parameters
def apply_library_charges(self, library_charges: SMIRNOFFLibraryChargeHandler): for top_key, pot_key in library_charges.slot_map.items(): ids = top_key.atom_indices charges = library_charges.potentials[pot_key].parameters["charges"] # Need to ensure this iterator follows ordering in force field for i, id_ in enumerate(ids): atom_key = TopologyKey(atom_indices=(id_, )) self.charges[atom_key] = charges[i]
def test_force_field_basic_constraints(self, parsley): """Test that a force field Constraints tag adds a Constraints handler with expected data""" # TODO: Replace with more minimal force field top = Topology.from_molecules(Molecule.from_smiles("CC")) sys_out = parsley.create_openff_system(top) assert "Constraints" in sys_out.handlers.keys() constraints = sys_out.handlers["Constraints"] c_c_bond = TopologyKey(atom_indices=(0, 1)) # C-C bond assert c_c_bond not in constraints.slot_map.keys() c_h_bond = TopologyKey(atom_indices=(0, 2)) # C-H bond assert c_h_bond in constraints.slot_map.keys() assert len(constraints.slot_map.keys()) == 6 # number of C-H bonds assert len({constraints.slot_map.values()}) == 1 # always True assert ("distance" in constraints.constraints[ constraints.slot_map[c_h_bond]].parameters)
def apply_charge_increments( self, charge_increments: SMIRNOFFChargeIncrementHandler): for top_key, pot_key in charge_increments.slot_map.items(): ids = top_key.atom_indices charges = charge_increments.potentials[pot_key].parameters[ "charge_increments"] for i, id_ in enumerate(ids): atom_key = TopologyKey(atom_indices=(id_, )) self.charges[atom_key] += charges[i]
def store_matches( self, parameter_handler: ChargeIncrementModelHandler, topology: Topology, ) -> None: matches = parameter_handler.find_matches(topology) for key, val in matches.items(): top_key = TopologyKey(atom_indices=key) pot_key = PotentialKey(id=val.parameter_type.smirks) self.slot_map[top_key] = pot_key
def store_matches(self, parameter_handler: AngleHandler, topology: Topology) -> None: """ Populate self.slot_map with key-val pairs of slots and unique potential identifiers """ matches = parameter_handler.find_matches(topology) for key, val in matches.items(): topology_key = TopologyKey(atom_indices=key) potential_key = PotentialKey(id=val.parameter_type.smirks) self.slot_map[topology_key] = potential_key
def store_matches(self, parameter_handler: ProperTorsionHandler, topology: Topology) -> None: """ Populate self.slot_map with key-val pairs of slots and unique potential identifiers """ matches = parameter_handler.find_matches(topology) for key, val in matches.items(): n_terms = len(val.parameter_type.k) for n in range(n_terms): smirks = val.parameter_type.smirks topology_key = TopologyKey(atom_indices=key, mult=n) potential_key = PotentialKey(id=smirks, mult=n) self.slot_map[topology_key] = potential_key
def test_argon_buck(): mol = Molecule.from_smiles("[#18]") top = Topology.from_molecules([mol, mol]) A = 1.69e-8 * unit.Unit("erg / mol") * Avogadro B = 1 / (0.273 * unit.angstrom) C = 102e-12 * unit.Unit("erg / mol") * unit.angstrom**6 * Avogadro A = A.to(unit.Unit("kilojoule/mol")) B = B.to(unit.Unit("1 / nanometer")) C = C.to(unit.Unit("kilojoule / mol * nanometer ** 6")) r = 0.3 * unit.nanometer buck = BuckinghamvdWHandler() coul = ElectrostaticsMetaHandler() # Just to pass compatibility checks pot_key = PotentialKey(id="[#18]") pot = Potential(parameters={"A": A, "B": B, "C": C}) for top_atom in top.topology_atoms: top_key = TopologyKey(atom_indices=(top_atom.topology_atom_index, )) buck.slot_map.update({top_key: pot_key}) coul.charges.update({top_key: 0 * unit.elementary_charge}) buck.potentials[pot_key] = pot from openff.system.components.system import System out = System() out.handlers["Buckingham-6"] = buck out.handlers["Electrostatics"] = coul out.topology = top out.box = [10, 10, 10] * unit.nanometer out.positions = [[0, 0, 0], [0.3, 0, 0]] * unit.nanometer out.to_gro("out.gro", writer="internal") out.to_top("out.top", writer="internal") gmx_energies = get_gromacs_energies(out, mdp="cutoff_buck") omm_energies = get_openmm_energies(out) by_hand = A * exp(-B * r) - C * r**-6 gmx_energies.compare(omm_energies) resid = simtk_to_pint(gmx_energies.energies["Nonbonded"]) - by_hand assert resid < 1e-5 * unit.kilojoule / unit.mol
def store_charges( self, forcefield: ForceField, topology: Topology, ) -> None: """ Populate self.slot_map with key-val pairs of slots and unique potential identifiers """ self.method = forcefield["Electrostatics"].method partial_charges = get_partial_charges_from_openmm_system( forcefield.create_openmm_system(topology=topology)) for i, charge in enumerate(partial_charges): topology_key = TopologyKey(atom_indices=(i, )) self.charge_map[topology_key] = charge * unit.elementary_charge
def _write_bonds(lmp_file: IO, openff_sys: System): lmp_file.write("\nBonds\n\n") bond_handler = openff_sys["Bonds"] bond_type_map = dict(enumerate(bond_handler.potentials)) bond_type_map_inv = dict({v: k for k, v in bond_type_map.items()}) for bond_idx, bond in enumerate( openff_sys.topology.topology_bonds): # type: ignore[union-attr] # These are "topology indices" indices = tuple(sorted(a.topology_atom_index for a in bond.atoms)) top_key = TopologyKey(atom_indices=indices) pot_key = bond_handler.slot_map[top_key] bond_type = bond_type_map_inv[pot_key] lmp_file.write("{:d}\t{:d}\t{:d}\t{:d}\n".format( bond_idx + 1, bond_type + 1, indices[0] + 1, indices[1] + 1, ))
def _write_atoms( top_file: IO, mol_name: str, mol_data: Dict, off_sys: "System", typemap: Dict, ): """Write the [ atoms ] section for a molecule""" top_file.write("[ atoms ]\n") top_file.write(";num, type, resnum, resname, atomname, cgnr, q, m\n") ref_mol = mol_data["reference_molecule"] top_mol = off_sys.topology._reference_molecule_to_topology_molecules[ref_mol][0] # type: ignore offset = top_mol.atom_start_topology_index for atom_idx, atom in enumerate(top_mol.atoms): atom_top_idx = atom_idx + offset atom_type = typemap[atom_top_idx] element = ele.element_from_atomic_number(atom.atomic_number) mass = element.mass top_key = TopologyKey(atom_indices=(atom_top_idx,)) charge = ( off_sys.handlers["Electrostatics"].charges[top_key].magnitude # type: ignore ) top_file.write( "{:6d} {:18s} {:6d} {:8s} {:8s} {:6d} " "{:18.8f} {:18.8f}\n".format( atom_idx + 1, atom_type, 1, # residue_index, always 1 while writing out per mol mol_name, # residue_name, element.symbol, atom_idx + 1, # cgnr charge, mass, ) )
def cache_charges(self, partial_charge_method: str, topology: Topology): charges: Dict[TopologyKey, FloatQuantity] = dict() for ref_mol in topology.reference_molecules: ref_mol.assign_partial_charges( partial_charge_method=partial_charge_method) for top_mol in topology._reference_molecule_to_topology_molecules[ ref_mol]: for topology_particle in top_mol.atoms: ref_mol_particle_index = ( topology_particle.atom.molecule_particle_index) topology_particle_index = topology_particle.topology_particle_index partial_charge = ref_mol._partial_charges[ ref_mol_particle_index] partial_charge = partial_charge / omm_unit.elementary_charge partial_charge = partial_charge * unit.elementary_charge top_key = TopologyKey( atom_indices=(topology_particle_index, )) charges[top_key] = partial_charge self.cache[partial_charge_method] = charges
def _write_angles(lmp_file: IO, openff_sys: System): lmp_file.write("\nAngles\n\n") angle_handler = openff_sys["Angles"] angle_type_map = dict(enumerate(angle_handler.potentials)) angle_type_map_inv = dict({v: k for k, v in angle_type_map.items()}) for angle_idx, angle in enumerate( openff_sys.topology.angles): # type: ignore[union-attr] # These are "topology indices" indices = tuple(a.topology_atom_index for a in angle) top_key = TopologyKey(atom_indices=indices) pot_key = angle_handler.slot_map[top_key] angle_type = angle_type_map_inv[pot_key] lmp_file.write("{:d}\t{:d}\t{:d}\t{:d}\t{:d}\n".format( angle_idx + 1, angle_type + 1, indices[0] + 1, indices[1] + 1, indices[2] + 1, ))
def test_angle_potential_handler(self): top = Topology.from_molecules(Molecule.from_smiles("CCC")) angle_handler = AngleHandler(version=0.3) angle_parameter = AngleHandler.AngleType( smirks="[*:1]~[*:2]~[*:3]", k=2.5 * omm_unit.kilocalorie_per_mole / omm_unit.radian**2, angle=100 * omm_unit.degree, id="b1000", ) angle_handler.add_parameter(angle_parameter.to_dict()) from openff.system.stubs import ForceField forcefield = ForceField() forcefield.register_parameter_handler(angle_handler) angle_potentials = forcefield["Angles"].create_potential(top) top_key = TopologyKey(atom_indices=(0, 1, 2)) pot = angle_potentials.potentials[angle_potentials.slot_map[top_key]] kcal_mol_rad2 = unit.Unit("kilocalorie / (mole * radian ** 2)") assert pot.parameters["k"].to( kcal_mol_rad2).magnitude == pytest.approx(2.5)
def test_bond_potential_handler(self): top = Topology.from_molecules(Molecule.from_smiles("O=O")) bond_handler = BondHandler(version=0.3) bond_parameter = BondHandler.BondType( smirks="[*:1]~[*:2]", k=1.5 * omm_unit.kilocalorie_per_mole / omm_unit.angstrom**2, length=1.5 * omm_unit.angstrom, id="b1000", ) bond_handler.add_parameter(bond_parameter.to_dict()) from openff.system.stubs import ForceField forcefield = ForceField() forcefield.register_parameter_handler(bond_handler) bond_potentials = forcefield["Bonds"].create_potential(top) top_key = TopologyKey(atom_indices=(0, 1)) pot = bond_potentials.potentials[bond_potentials.slot_map[top_key]] kcal_mol_a2 = unit.Unit("kilocalorie / (angstrom ** 2 * mole)") assert pot.parameters["k"].to(kcal_mol_a2).magnitude == pytest.approx( 1.5)
def from_parmed(cls) -> "System": from openff.system.components.system import System out = System() if cls.positions: out.positions = np.asarray(cls.positions._value) * unit.angstrom if any(cls.box[3:] != 3 * [90.0]): from openff.system.exceptions import UnsupportedBoxError raise UnsupportedBoxError(f"Found box with angles {cls.box[3:]}. Only" "rectangular boxes are currently supported.") out.box = cls.box[:3] * unit.angstrom from openff.toolkit.topology import Molecule, Topology top = Topology() for res in cls.residues: mol = Molecule() mol.name = res.name for atom in res.atoms: mol.add_atom(atomic_number=atom.atomic_number, formal_charge=0, is_aromatic=False) for atom in res.atoms: for bond in atom.bonds: try: mol.add_bond( atom1=bond.atom1.idx, atom2=bond.atom2.idx, bond_order=int(bond.order), is_aromatic=False, ) # TODO: Use a custom exception after # https://github.com/openforcefield/openff-toolkit/issues/771 except Exception as e: if "Bond already exists" in str(e): pass else: raise e top.add_molecule(mol) out.topology = top from openff.system.components.smirnoff import ( ElectrostaticsMetaHandler, SMIRNOFFAngleHandler, SMIRNOFFBondHandler, SMIRNOFFImproperTorsionHandler, SMIRNOFFProperTorsionHandler, SMIRNOFFvdWHandler, ) vdw_handler = SMIRNOFFvdWHandler() coul_handler = ElectrostaticsMetaHandler() for atom in cls.atoms: atom_idx = atom.idx sigma = atom.sigma * unit.angstrom epsilon = atom.epsilon * kcal_mol charge = atom.charge * unit.elementary_charge top_key = TopologyKey(atom_indices=(atom_idx, )) pot_key = PotentialKey(id=str(atom_idx)) pot = Potential(parameters={"sigma": sigma, "epsilon": epsilon}) vdw_handler.slot_map.update({top_key: pot_key}) vdw_handler.potentials.update({pot_key: pot}) coul_handler.charges.update({top_key: charge}) bond_handler = SMIRNOFFBondHandler() for bond in cls.bonds: atom1 = bond.atom1 atom2 = bond.atom2 k = bond.type.k * kcal_mol_a2 length = bond.type.req * unit.angstrom top_key = TopologyKey(atom_indices=(atom1.idx, atom2.idx)) pot_key = PotentialKey(id=f"{atom1.idx}-{atom2.idx}") pot = Potential(parameters={"k": k * 2, "length": length}) bond_handler.slot_map.update({top_key: pot_key}) bond_handler.potentials.update({pot_key: pot}) out.handlers.update({"vdW": vdw_handler}) out.handlers.update({"Electrostatics": coul_handler}) # type: ignore[dict-item] out.handlers.update({"Bonds": bond_handler}) angle_handler = SMIRNOFFAngleHandler() for angle in cls.angles: atom1 = angle.atom1 atom2 = angle.atom2 atom3 = angle.atom3 k = angle.type.k * kcal_mol_rad2 theta = angle.type.theteq * unit.degree top_key = TopologyKey(atom_indices=(atom1.idx, atom2.idx, atom3.idx)) pot_key = PotentialKey(id=f"{atom1.idx}-{atom2.idx}-{atom3.idx}") pot = Potential(parameters={"k": k * 2, "angle": theta}) angle_handler.slot_map.update({top_key: pot_key}) angle_handler.potentials.update({pot_key: pot}) proper_torsion_handler = SMIRNOFFProperTorsionHandler() improper_torsion_handler = SMIRNOFFImproperTorsionHandler() for dihedral in cls.dihedrals: atom1 = dihedral.atom1 atom2 = dihedral.atom2 atom3 = dihedral.atom3 atom4 = dihedral.atom4 k = dihedral.type.phi_k * kcal_mol_rad2 periodicity = dihedral.type.per * unit.dimensionless phase = dihedral.type.phase * unit.degree if dihedral.improper: # ParmEd stores the central atom _third_ (AMBER style) # SMIRNOFF stores the central atom _second_ # https://parmed.github.io/ParmEd/html/topobj/parmed.topologyobjects.Dihedral.html#parmed-topologyobjects-dihedral # https://open-forcefield-toolkit.readthedocs.io/en/latest/smirnoff.html#impropertorsions top_key = TopologyKey( atom_indices=(atom1.idx, atom2.idx, atom2.idx, atom4.idx), mult=1, ) pot_key = PotentialKey( id=f"{atom1.idx}-{atom3.idx}-{atom2.idx}-{atom4.idx}", mult=1, ) pot = Potential(parameters={ "k": k, "periodicity": periodicity, "phase": phase }) while pot_key in improper_torsion_handler.potentials: pot_key.mult += 1 # type: ignore[operator] top_key.mult += 1 # type: ignore[operator] improper_torsion_handler.slot_map.update({top_key: pot_key}) improper_torsion_handler.potentials.update({pot_key: pot}) else: top_key = TopologyKey( atom_indices=(atom1.idx, atom2.idx, atom3.idx, atom4.idx), mult=1, ) pot_key = PotentialKey( id=f"{atom1.idx}-{atom2.idx}-{atom3.idx}-{atom4.idx}", mult=1, ) pot = Potential(parameters={ "k": k, "periodicity": periodicity, "phase": phase }) while pot_key in proper_torsion_handler.potentials: pot_key.mult += 1 # type: ignore[operator] top_key.mult += 1 # type: ignore[operator] proper_torsion_handler.slot_map.update({top_key: pot_key}) proper_torsion_handler.potentials.update({pot_key: pot}) out.handlers.update({"Electrostatics": coul_handler}) # type: ignore[dict-item] out.handlers.update({"Bonds": bond_handler}) out.handlers.update({"Angles": angle_handler}) out.handlers.update({"ProperTorsions": proper_torsion_handler}) return out
def to_parmed(off_system: "System") -> pmd.Structure: """Convert an OpenFF System to a ParmEd Structure""" structure = pmd.Structure() _convert_box(off_system.box, structure) if "Electrostatics" in off_system.handlers.keys(): has_electrostatics = True electrostatics_handler = off_system.handlers["Electrostatics"] else: has_electrostatics = False for topology_molecule in off_system.topology.topology_molecules: # type: ignore[union-attr] for atom in topology_molecule.atoms: atomic_number = atom.atomic_number element = pmd.periodic_table.Element[atomic_number] mass = pmd.periodic_table.Mass[element] structure.add_atom( pmd.Atom( atomic_number=atomic_number, mass=mass, ), resname="FOO", resnum=0, ) if "Bonds" in off_system.handlers.keys(): bond_handler = off_system.handlers["Bonds"] bond_type_map: Dict = dict() for pot_key, pot in bond_handler.potentials.items(): k = pot.parameters["k"].to(kcal_mol_a2).magnitude / 2 length = pot.parameters["length"].to(unit.angstrom).magnitude bond_type = pmd.BondType(k=k, req=length) bond_type_map[pot_key] = bond_type structure.bond_types.append(bond_type) for top_key, pot_key in bond_handler.slot_map.items(): idx_1, idx_2 = top_key.atom_indices bond_type = bond_type_map[pot_key] bond = pmd.Bond( atom1=structure.atoms[idx_1], atom2=structure.atoms[idx_2], type=bond_type, ) structure.bonds.append(bond) structure.bond_types.claim() if "Angles" in off_system.handlers.keys(): angle_handler = off_system.handlers["Angles"] angle_type_map: Dict = dict() for pot_key, pot in angle_handler.potentials.items(): k = pot.parameters["k"].to(kcal_mol_rad2).magnitude / 2 theta = pot.parameters["angle"].to(unit.degree).magnitude # TODO: Look up if AngleType already exists in struct angle_type = pmd.AngleType(k=k, theteq=theta) angle_type_map[pot_key] = angle_type structure.angle_types.append(angle_type) for top_key, pot_key in angle_handler.slot_map.items(): idx_1, idx_2, idx_3 = top_key.atom_indices angle_type = angle_type_map[pot_key] structure.angles.append( pmd.Angle( atom1=structure.atoms[idx_1], atom2=structure.atoms[idx_2], atom3=structure.atoms[idx_3], type=angle_type, )) structure.angle_types.append(angle_type) structure.angle_types.claim() # ParmEd treats 1-4 scaling factors at the level of each DihedralType, # whereas SMIRNOFF captures them at the level of the non-bonded handler, # so they need to be stored here for processing dihedrals vdw_14 = off_system.handlers["vdW"].scale_14 # type: ignore[attr-defined] if has_electrostatics: coul_14 = off_system.handlers[ "Electrostatics"].scale_14 # type: ignore[attr-defined] else: coul_14 = 1.0 vdw_handler = off_system.handlers["vdW"] if "ProperTorsions" in off_system.handlers.keys(): proper_torsion_handler = off_system.handlers["ProperTorsions"] proper_type_map: Dict = dict() for pot_key, pot in proper_torsion_handler.potentials.items(): k = pot.parameters["k"].to(kcal_mol).magnitude periodicity = pot.parameters["periodicity"] phase = pot.parameters["phase"].magnitude proper_type = pmd.DihedralType( phi_k=k, per=periodicity, phase=phase, scnb=1 / vdw_14, scee=1 / coul_14, ) proper_type_map[pot_key] = proper_type structure.dihedral_types.append(proper_type) for top_key, pot_key in proper_torsion_handler.slot_map.items(): idx_1, idx_2, idx_3, idx_4 = top_key.atom_indices dihedral_type = proper_type_map[pot_key] structure.dihedrals.append( pmd.Dihedral( atom1=structure.atoms[idx_1], atom2=structure.atoms[idx_2], atom3=structure.atoms[idx_3], atom4=structure.atoms[idx_4], type=dihedral_type, )) structure.dihedral_types.append(dihedral_type) key1 = TopologyKey(atom_indices=(idx_1, )) key4 = TopologyKey(atom_indices=(idx_4, )) vdw1 = vdw_handler.potentials[vdw_handler.slot_map[key1]] vdw4 = vdw_handler.potentials[vdw_handler.slot_map[key4]] sig1, eps1 = _lj_params_from_potential(vdw1) sig4, eps4 = _lj_params_from_potential(vdw4) sig = (sig1 + sig4) * 0.5 eps = (eps1 * eps4)**0.5 nbtype = pmd.NonbondedExceptionType(rmin=sig * 2**(1 / 6), epsilon=eps * vdw_14, chgscale=coul_14) structure.adjusts.append( pmd.NonbondedException(structure.atoms[idx_1], structure.atoms[idx_4], type=nbtype)) structure.adjust_types.append(nbtype) structure.dihedral_types.claim() structure.adjust_types.claim() # if False: # "ImroperTorsions" in off_system.term_collection.terms: # improper_term = off_system.term_collection.terms["ImproperTorsions"] # for improper, smirks in improper_term.smirks_map.items(): # idx_1, idx_2, idx_3, idx_4 = improper # pot = improper_term.potentials[improper_term.smirks_map[improper]] # # TODO: Better way of storing periodic data in generally, probably need to improve Potential # n = re.search(r"\d", "".join(pot.parameters.keys())).group() # k = pot.parameters["k" + n].m # kcal/mol # periodicity = pot.parameters["periodicity" + n].m # dimless # phase = pot.parameters["phase" + n].m # degree # # dihedral_type = pmd.DihedralType(per=periodicity, phi_k=k, phase=phase) # structure.dihedrals.append( # pmd.Dihedral( # atom1=structure.atoms[idx_1], # atom2=structure.atoms[idx_2], # atom3=structure.atoms[idx_3], # atom4=structure.atoms[idx_4], # type=dihedral_type, # ) # ) vdw_handler = off_system.handlers["vdW"] for pmd_idx, pmd_atom in enumerate(structure.atoms): top_key = TopologyKey(atom_indices=(pmd_idx, )) smirks = vdw_handler.slot_map[top_key] potential = vdw_handler.potentials[smirks] element = pmd.periodic_table.Element[pmd_atom.element] sigma, epsilon = _lj_params_from_potential(potential) atom_type = pmd.AtomType( name=element + str(pmd_idx + 1), number=pmd_idx, atomic_number=pmd_atom.atomic_number, mass=pmd.periodic_table.Mass[element], ) atom_type.set_lj_params(eps=epsilon, rmin=sigma * 2**(1 / 6) / 2) pmd_atom.atom_type = atom_type pmd_atom.type = atom_type.name pmd_atom.name = pmd_atom.type for pmd_idx, pmd_atom in enumerate(structure.atoms): if has_electrostatics: top_key = TopologyKey(atom_indices=(pmd_idx, )) partial_charge = electrostatics_handler.charges[ top_key] # type: ignore[attr-defined] unitless_ = partial_charge.to(unit.elementary_charge).magnitude pmd_atom.charge = float(unitless_) pmd_atom.atom_type.charge = float(unitless_) else: pmd_atom.charge = 0 # Assign dummy residue names, GROMACS will not accept empty strings for res in structure.residues: res.name = "FOO" structure.positions = off_system.positions.to( unit.angstrom).magnitude # type: ignore[attr-defined] for idx, pos in enumerate(structure.positions): structure.atoms[idx].xx = pos._value[0] structure.atoms[idx].xy = pos._value[1] structure.atoms[idx].xz = pos._value[2] return structure
def test_ethanol_opls(): mol = Molecule.from_smiles("CC") mol.generate_conformers(n_conformers=1) top = mol.to_topology() parsley = ForceField("openff-1.0.0.offxml") out = parsley.create_openff_system(top) out.box = [4, 4, 4] out.positions = mol.conformers[0] out.positions = np.round(out.positions, 2) rb_torsions = RBTorsionHandler() smirks = "[#1:1]-[#6X4:2]-[#6X4:3]-[#1:4]" pot_key = PotentialKey(id=smirks) for proper in top.propers: top_key = TopologyKey(atom_indices=tuple(a.topology_atom_index for a in proper)) rb_torsions.slot_map.update({top_key: pot_key}) # Values from HC-CT-CT-HC RB torsion # https://github.com/mosdef-hub/foyer/blob/7816bf53a127502520a18d76c81510f96adfdbed/foyer/forcefields/xml/oplsaa.xml#L2585 pot = Potential( parameters={ "c0": 0.6276 * kj_mol, "c1": 1.8828 * kj_mol, "c2": 0.0 * kj_mol, "c3": -2.5104 * kj_mol, "c4": 0.0 * kj_mol, "c5": 0.0 * kj_mol, }) rb_torsions.potentials.update({pot_key: pot}) out.handlers.update({"RBTorsions": rb_torsions}) out.handlers.pop("ProperTorsions") gmx = get_openmm_energies(out, round_positions=3).energies["Torsion"] omm = get_gromacs_energies(out).energies["Torsion"] assert (gmx - omm).value_in_unit(omm_unit.kilojoule_per_mole) < 1e-3 # Given that these force constants are copied from Foyer's OPLS-AA file, # compare to processing through the current MoSDeF pipeline try: import foyer import mbuild except ModuleNotFoundError: return comp = mbuild.load("CC", smiles=True) comp.xyz = mol.conformers[0].value_in_unit(omm_unit.nanometer) ff = foyer.Forcefield(name="oplsaa") from_foyer = ff.apply(comp) from_foyer.box = [40, 40, 40, 90, 90, 90] from_foyer.save("from_foyer.top") from_foyer.save("from_foyer.gro") rb_torsion_energy_from_foyer = run_gmx_energy( top_file="from_foyer.top", gro_file="from_foyer.gro", mdp_file=get_mdp_file("default"), ).energies["Torsion"] assert (omm - rb_torsion_energy_from_foyer).value_in_unit( omm_unit.kilojoule_per_mole) < 1e-3