예제 #1
0
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})
예제 #2
0
    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,
            )
예제 #3
0
    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
예제 #4
0
    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,
        )
예제 #5
0
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
예제 #6
0
    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
예제 #7
0
    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]
예제 #8
0
    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")
예제 #9
0
    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"])
예제 #10
0
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
예제 #11
0
    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))
예제 #12
0
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
예제 #13
0
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
예제 #14
0
    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
예제 #15
0
    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
예제 #16
0
    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
예제 #17
0
    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
예제 #18
0
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