def _process_single_dihedral( dihedral: "pmd.Dihedral", dihedral_type: "pmd.DihedralType", handler: Union["SMIRNOFFImproperTorsionHandler", "SMIRNOFFProperTorsionHandler"], mult: Optional[int] = None, ): 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=mult, ) pot_key = PotentialKey( id=f"{atom1.idx}-{atom3.idx}-{atom2.idx}-{atom4.idx}", mult=mult, ) pot = Potential(parameters={ "k": k, "periodicity": periodicity, "phase": phase }) if pot_key in handler.potentials: raise Exception("fudging dihedral indices") handler.slot_map.update({top_key: pot_key}) 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 handler.potentials: pot_key.mult += 1 # type: ignore[operator] top_key.mult += 1 # type: ignore[operator] handler.slot_map.update({top_key: pot_key}) handler.potentials.update({pot_key: pot})
def store_matches( self, parameter_handler: ParameterHandler, topology: Union["Topology", "OFFBioTop"], ) -> None: """ Populate self.slot_map with key-val pairs of slots and unique potential identifiers """ parameter_handler_name = getattr(parameter_handler, "_TAGNAME", None) if self.slot_map: # TODO: Should the slot_map always be reset, or should we be able to partially # update it? Also Note the duplicated code in the child classes self.slot_map = dict() 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, associated_handler=parameter_handler_name) self.slot_map[topology_key] = potential_key if self.__class__.__name__ in [ "SMIRNOFFBondHandler", "SMIRNOFFAngleHandler" ]: valence_terms = self.valence_terms(topology) parameter_handler._check_all_valence_terms_assigned( assigned_terms=matches, valence_terms=valence_terms, exception_cls=UnassignedValenceParameterException, )
def _charge_increment_to_potentials( cls, atom_indices: Tuple[int, ...], parameter: ChargeIncrementModelHandler.ChargeIncrementType, ) -> Tuple[Dict[TopologyKey, PotentialKey], Dict[PotentialKey, Potential]]: """Maps a matched charge increment parameter to a set of potentials.""" matches = {} potentials = {} for i, atom_index in enumerate(atom_indices): topology_key = TopologyKey(atom_indices=(atom_index, )) potential_key = PotentialKey( id=parameter.smirks, mult=i, associated_handler="ChargeIncrementModel") # TODO: Handle the cases where n - 1 charge increments have been defined, # maybe by implementing this in the TK? charge_increment = getattr(parameter, f"charge_increment{i + 1}") potential = Potential( parameters={"charge_increment": from_simtk(charge_increment)}) matches[topology_key] = potential_key potentials[potential_key] = potential return matches, potentials
def store_matches( self, parameter_handler: "ProperTorsionHandler", topology: "OFFBioTop", ) -> None: """ Populate self.slot_map with key-val pairs of slots and unique potential identifiers """ if self.slot_map: self.slot_map = dict() 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, associated_handler="ProperTorsions") self.slot_map[topology_key] = potential_key parameter_handler._check_all_valence_terms_assigned( assigned_terms=matches, valence_terms=list(topology.propers), exception_cls=UnassignedProperTorsionParameterException, )
def _convert_periodic_torsion_force(force): # TODO: Can impropers be separated out from a PeriodicTorsionForce? # Maybe by seeing if a quartet is in mol/top.propers or .impropers from openff.interchange.components.smirnoff import SMIRNOFFProperTorsionHandler proper_torsion_handler = SMIRNOFFProperTorsionHandler() n_parametrized_torsions = force.getNumTorsions() for idx in range(n_parametrized_torsions): atom1, atom2, atom3, atom4, per, phase, k = force.getTorsionParameters( idx) # TODO: Process layered torsions top_key = TopologyKey(atom_indices=(atom1, atom2, atom3, atom4), mult=0) while top_key in proper_torsion_handler.slot_map: top_key.mult += 1 pot_key = PotentialKey(id=f"{atom1}-{atom2}-{atom3}-{atom4}", mult=top_key.mult) pot = Potential( parameters={ "periodicity": int(per) * unit.dimensionless, "phase": from_simtk(phase), "k": from_simtk(k), "idivf": 1 * unit.dimensionless, }) proper_torsion_handler.slot_map.update({top_key: pot_key}) proper_torsion_handler.potentials.update({pot_key: pot}) return proper_torsion_handler
def _find_am1_matches( cls, parameter_handler: Union["ToolkitAM1BCCHandler", ChargeIncrementModelHandler], reference_molecule: Molecule, ) -> Tuple[Dict[TopologyKey, PotentialKey], Dict[PotentialKey, Potential]]: """Constructs a slot and potential map for a charge model based parameter handler.""" reference_molecule = copy.deepcopy(reference_molecule) reference_smiles = reference_molecule.to_smiles( isomeric=True, explicit_hydrogens=True, mapped=True) method = getattr(parameter_handler, "partial_charge_method", "am1bcc") partial_charges = cls._compute_partial_charges(reference_molecule, method=method) matches = {} potentials = {} for i, partial_charge in enumerate(partial_charges): potential_key = PotentialKey(id=reference_smiles, mult=i, associated_handler="ToolkitAM1BCC") potentials[potential_key] = Potential( parameters={"charge": partial_charge}) matches[TopologyKey(atom_indices=(i, ))] = potential_key return matches, potentials
def store_constraints( self, parameter_handlers: Any, topology: "OFFBioTop", ) -> None: if self.slot_map: self.slot_map = dict() constraint_handler = [ p for p in parameter_handlers if type(p) == ConstraintHandler ][0] constraint_matches = constraint_handler.find_matches(topology) if any([type(p) == BondHandler for p in parameter_handlers]): bond_handler = [ p for p in parameter_handlers if type(p) == BondHandler ][0] bonds = SMIRNOFFBondHandler._from_toolkit( parameter_handler=bond_handler, topology=topology, ) else: bond_handler = None bonds = None for key, match in constraint_matches.items(): topology_key = TopologyKey(atom_indices=key) smirks = match.parameter_type.smirks distance = match.parameter_type.distance if distance is not None: # This constraint parameter is fully specified potential_key = PotentialKey(id=smirks, associated_handler="Constraints") distance = match.parameter_type.distance else: # This constraint parameter depends on the BondHandler ... if bond_handler is None: from openff.interchange.exceptions import MissingParametersError raise MissingParametersError( f"Constraint with SMIRKS pattern {smirks} found with no distance " "specified, and no corresponding bond parameters were found. The distance " "of this constraint is not specified.") # ... so use the same PotentialKey instance as the BondHandler to look up the distance potential_key = bonds.slot_map[topology_key] self.slot_map[topology_key] = potential_key distance = bonds.potentials[potential_key].parameters["length"] potential = Potential(parameters={ "distance": distance, }) self.constraints[ potential_key] = potential # type: ignore[assignment]
def test_argon_buck(self): """Test that Buckingham potentials are supported and can be exported""" from openff.interchange.components.smirnoff import SMIRNOFFElectrostaticsHandler mol = Molecule.from_smiles("[#18]") top = OFFBioTop.from_molecules([mol, mol]) top.mdtop = md.Topology.from_openmm(top.to_openmm()) # http://www.sklogwiki.org/SklogWiki/index.php/Argon#Buckingham_potential erg_mol = unit.erg / unit.mol * float(unit.avogadro_number) A = 1.69e-8 * erg_mol B = 1 / (0.273 * unit.angstrom) C = 102e-12 * erg_mol * unit.angstrom**6 r = 0.3 * unit.nanometer buck = BuckinghamvdWHandler() coul = SMIRNOFFElectrostaticsHandler(method="pme") pot_key = PotentialKey(id="[#18]") pot = Potential(parameters={"A": A, "B": B, "C": C}) for atom in top.mdtop.atoms: top_key = TopologyKey(atom_indices=(atom.index, )) buck.slot_map.update({top_key: pot_key}) coul.slot_map.update({top_key: pot_key}) coul.potentials.update({ pot_key: Potential(parameters={"charge": 0 * unit.elementary_charge}) }) buck.potentials[pot_key] = pot out = Interchange() 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") omm_energies = get_openmm_energies(out) by_hand = A * exp(-B * r) - C * r**-6 resid = omm_energies.energies["Nonbonded"] - by_hand assert resid < 1e-5 * unit.kilojoule / unit.mol # TODO: Add back comparison to GROMACS energies once GROMACS 2020+ # supports Buckingham potentials with pytest.raises(GMXMdrunError): get_gromacs_energies(out, mdp="cutoff_buck")
def store_matches( self, force_field: "Forcefield", topology: "OFFBioTop", ) -> None: """Populate slotmap with key-val pairs of slots and unique potential Identifiers""" from foyer.atomtyper import find_atomtypes top_graph = TopologyGraph.from_openff_topology( openff_topology=topology) type_map = find_atomtypes(top_graph, forcefield=force_field) for key, val in type_map.items(): top_key = TopologyKey(atom_indices=(key, )) self.slot_map[top_key] = PotentialKey(id=val["atomtype"])
def _convert_harmonic_angle_force(force): from openff.interchange.components.smirnoff import SMIRNOFFAngleHandler angle_handler = SMIRNOFFAngleHandler() n_parametrized_angles = force.getNumAngles() for idx in range(n_parametrized_angles): atom1, atom2, atom3, angle, k = force.getAngleParameters(idx) top_key = TopologyKey(atom_indices=(atom1, atom2, atom3)) pot_key = PotentialKey(id=f"{atom1}-{atom2}-{atom3}") pot = Potential(parameters={ "angle": from_simtk(angle), "k": from_simtk(k) }) angle_handler.slot_map.update({top_key: pot_key}) angle_handler.potentials.update({pot_key: pot}) return angle_handler
def store_matches( self, atom_slots: Dict[TopologyKey, PotentialKey], topology: "OFFBioTop", ) -> None: for connection in getattr(topology, self.connection_attribute): try: atoms_iterable = connection.atoms except AttributeError: atoms_iterable = connection atoms_indices = tuple(atom.topology_atom_index for atom in atoms_iterable) top_key = TopologyKey(atom_indices=atoms_indices) pot_key_ids = tuple( _get_potential_key_id(atom_slots, idx) for idx in atoms_indices) self.slot_map[top_key] = PotentialKey( id=POTENTIAL_KEY_SEPARATOR.join(pot_key_ids))
def _convert_harmonic_bond_force(force): from openff.interchange.components.smirnoff import SMIRNOFFBondHandler bond_handler = SMIRNOFFBondHandler() n_parametrized_bonds = force.getNumBonds() for idx in range(n_parametrized_bonds): atom1, atom2, length, k = force.getBondParameters(idx) top_key = TopologyKey(atom_indices=(atom1, atom2)) pot_key = PotentialKey(id=f"{atom1}-{atom2}") pot = Potential(parameters={ "length": from_simtk(length), "k": from_simtk(k) }) bond_handler.slot_map.update({top_key: pot_key}) bond_handler.potentials.update({pot_key: pot}) return bond_handler
def _convert_nonbonded_force(force): from openff.interchange.components.smirnoff import ( SMIRNOFFElectrostaticsHandler, SMIRNOFFvdWHandler, ) vdw_handler = SMIRNOFFvdWHandler() electrostatics = SMIRNOFFElectrostaticsHandler(method="pme") n_parametrized_particles = force.getNumParticles() for idx in range(n_parametrized_particles): charge, sigma, epsilon = force.getParticleParameters(idx) top_key = TopologyKey(atom_indices=(idx, )) pot_key = PotentialKey(id=f"{idx}") pot = Potential(parameters={ "sigma": from_simtk(sigma), "epsilon": from_simtk(epsilon), }) vdw_handler.slot_map.update({top_key: pot_key}) vdw_handler.potentials.update({pot_key: pot}) electrostatics.slot_map.update({top_key: pot_key}) electrostatics.potentials.update( {pot_key: Potential(parameters={"charge": from_simtk(charge)})}) vdw_handler.cutoff = force.getCutoffDistance() electrostatics.cutoff = force.getCutoffDistance() if force.getNonbondedMethod() == openmm.NonbondedForce.PME: electrostatics.method = "pme" elif force.getNonbondedMethod() in { openmm.NonbondedForce.CutoffPeriodic, openmm.NonbondedForce.CutoffNonPeriodic, }: # TODO: Store reaction-field dielectric electrostatics.method = "reactionfield" elif force.getNonbondedMethod() == openmm.NonbondedForce.NoCutoff: raise Exception("NonbondedMethod NoCutoff is not supported") return vdw_handler, electrostatics
def store_matches( self, parameter_handler: ParameterHandler, topology: Union["Topology", "OFFBioTop"], ) -> None: """ Populate self.slot_map with key-val pairs of slots and unique potential identifiers """ parameter_handler_name = getattr(parameter_handler, "_TAGNAME", None) if self.slot_map: # TODO: Should the slot_map always be reset, or should we be able to partially # update it? Also Note the duplicated code in the child classes self.slot_map = dict() 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, associated_handler=parameter_handler_name) self.slot_map[topology_key] = potential_key
def _library_charge_to_potentials( cls, atom_indices: Tuple[int, ...], parameter: LibraryChargeHandler.LibraryChargeType, ) -> Tuple[Dict[TopologyKey, PotentialKey], Dict[PotentialKey, Potential]]: """Maps a matched library charge parameter to a set of potentials.""" matches = {} potentials = {} for i, (atom_index, charge) in enumerate(zip(atom_indices, parameter.charge)): topology_key = TopologyKey(atom_indices=(atom_index, )) potential_key = PotentialKey(id=parameter.smirks, mult=i, associated_handler="LibraryCharges") potential = Potential(parameters={"charge": from_simtk(charge)}) matches[topology_key] = potential_key potentials[potential_key] = potential return matches, potentials
def store_matches(self, parameter_handler: "ImproperTorsionHandler", topology: "OFFBioTop") -> None: """ Populate self.slot_map with key-val pairs of slots and unique potential identifiers """ if self.slot_map: self.slot_map = dict() matches = parameter_handler.find_matches(topology) for key, val in matches.items(): parameter_handler._assert_correct_connectivity( val, [ (0, 1), (1, 2), (1, 3), ], ) n_terms = len(val.parameter_type.k) for n in range(n_terms): smirks = val.parameter_type.smirks non_central_indices = [key[0], key[2], key[3]] for permuted_key in [( non_central_indices[i], non_central_indices[j], non_central_indices[k], ) for (i, j, k) in [(0, 1, 2), (1, 2, 0), (2, 0, 1)]]: topology_key = TopologyKey(atom_indices=(key[1], *permuted_key), mult=n) potential_key = PotentialKey( id=smirks, mult=n, associated_handler="ImproperTorsions") self.slot_map[topology_key] = potential_key
def ethanol_with_rb_torsions(self): mol = Molecule.from_smiles("CC") mol.generate_conformers(n_conformers=1) top = mol.to_topology() parsley = ForceField("openff-1.0.0.offxml") out = Interchange.from_smirnoff(parsley, 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") return out
def _from_parmed(cls, structure) -> "Interchange": import parmed as pmd out = cls() if structure.positions: out.positions = np.asarray(structure.positions._value) * unit.angstrom if structure.box is not None: if any(structure.box[3:] != 3 * [90.0]): raise UnsupportedBoxError( f"Found box with angles {structure.box[3:]}. Only" "rectangular boxes are currently supported.") out.box = structure.box[:3] * unit.angstrom from openff.toolkit.topology import Molecule from openff.interchange.components.mdtraj import OFFBioTop if structure.topology is not None: mdtop = md.Topology.from_openmm( structure.topology) # type: ignore[attr-defined] top = OFFBioTop(mdtop=mdtop) out.topology = top else: # TODO: Remove this case # This code should not be reached, since a pathway # OpenFF -> OpenMM -> MDTraj already exists mdtop = md.Topology() # type: ignore[attr-defined] main_chain = md.core.topology.Chain( index=0, topology=mdtop) # type: ignore[attr-defined] top = OFFBioTop(mdtop=None) # There is no way to tell if ParmEd residues are connected (cannot be processed # as separate OFFMols) or disconnected (can be). For now, will have to accept the # inefficiency of putting everything into on OFFMol ... mol = Molecule() mol.name = getattr(structure, "name", "Mol") for res in structure.residues: # ... however, MDTraj's Topology class only stores residues, not molecules, # so this should roughly match up with ParmEd this_res = md.core.topology.Residue( # type: ignore[attr-defined] name=res.name, index=res.idx, chain=main_chain, resSeq=0, ) for atom in res.atoms: mol.add_atom(atomic_number=atom.atomic_number, formal_charge=0, is_aromatic=False) mdtop.add_atom( name=atom.name, element=md.element.Element.getByAtomicNumber( atom.element), # type: ignore[attr-defined] residue=this_res, ) main_chain._residues.append(this_res) for res in structure.residues: 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 mdtop.add_bond( atom1=mdtop.atom(bond.atom1.idx), atom2=mdtop.atom(bond.atom2.idx), order=int(bond.order) if bond.order is not None else None, ) # Topology.add_molecule requires a safe .to_smiles() call, so instead # do a dangerous molecule addition ref_mol = FrozenMolecule(mol) # This doesn't work because molecule hashing requires valid SMILES # top._reference_molecule_to_topology_molecules[ref_mol] = [] # so just tack it on for now top._reference_mm_molecule = ref_mol top_mol = TopologyMolecule(reference_molecule=ref_mol, topology=top) top._topology_molecules.append(top_mol) # top._reference_molecule_to_topology_molecules[ref_mol].append(top_mol) mdtop._chains.append(main_chain) out.topology = top from openff.interchange.components.smirnoff import ( SMIRNOFFAngleHandler, SMIRNOFFBondHandler, SMIRNOFFElectrostaticsHandler, SMIRNOFFImproperTorsionHandler, SMIRNOFFProperTorsionHandler, SMIRNOFFvdWHandler, ) vdw_handler = SMIRNOFFvdWHandler() coul_handler = SMIRNOFFElectrostaticsHandler(method="pme") for atom in structure.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.slot_map.update({top_key: pot_key}) coul_handler.potentials.update( {pot_key: Potential(parameters={"charge": charge})}) bond_handler = SMIRNOFFBondHandler() for bond in structure.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}) out.handlers.update({"Bonds": bond_handler}) angle_handler = SMIRNOFFAngleHandler() for angle in structure.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 structure.dihedrals: if isinstance(dihedral.type, pmd.DihedralType): if dihedral.improper: _process_single_dihedral(dihedral, dihedral.type, improper_torsion_handler, 0) else: _process_single_dihedral(dihedral, dihedral.type, proper_torsion_handler, 0) elif isinstance(dihedral.type, pmd.DihedralTypeList): for dih_idx, dihedral_type in enumerate(dihedral.type): if dihedral.improper: _process_single_dihedral(dihedral, dihedral_type, improper_torsion_handler, dih_idx) else: _process_single_dihedral( dihedral, dihedral_type, proper_torsion_handler, dih_idx, ) out.handlers.update({"Electrostatics": coul_handler}) out.handlers.update({"Bonds": bond_handler}) out.handlers.update({"Angles": angle_handler}) out.handlers.update({"ProperTorsions": proper_torsion_handler}) return out