def store_potentials(self, parameter_handler: vdWHandler) -> None: """ Populate self.potentials with key-val pairs of unique potential identifiers and their associated Potential objects """ self.method = parameter_handler.method self.cutoff = parameter_handler.cutoff / omm_unit.angstrom for potential_key in self.slot_map.values(): smirks = potential_key.id parameter_type = parameter_handler.get_parameter( {"smirks": smirks})[0] try: potential = Potential(parameters={ "sigma": parameter_type.sigma, "epsilon": parameter_type.epsilon, }, ) except AttributeError: # Handle rmin_half pending https://github.com/openforcefield/openff-toolkit/pull/750 potential = Potential(parameters={ "sigma": parameter_type.sigma, "epsilon": parameter_type.epsilon, }, ) self.potentials[potential_key] = potential
def store_potentials(self, parameter_handler: ImproperTorsionHandler) -> None: """ Populate self.potentials with key-val pairs of unique potential identifiers and their associated Potential objects """ for key in self.slot_map.values(): # ParameterHandler.get_parameter returns a list, although this # should only ever be length 1 smirks = key.split("_")[0] parameter_type = parameter_handler.get_parameter( {"smirks": smirks})[0] n_terms = len(parameter_type.k) for n in range(n_terms): identifier = key parameters = { "k": parameter_type.k[n], "periodicity": parameter_type.periodicity[n] * unit.dimensionless, "phase": parameter_type.phase[n], "idivf": 3.0 * unit.dimensionless, } potential = Potential(parameters=parameters) self.potentials[identifier] = potential
def store_potentials(self, parameter_handler: LibraryChargeHandler) -> None: if self.potentials: self.potentials = dict() for potential_key in self.slot_map.values(): smirks = potential_key.id parameter_type = parameter_handler.get_parameter( {"smirks": smirks})[0] charges_unitless = [val._value for val in parameter_type.charge] potential = Potential(parameters={ "charges": charges_unitless * unit.elementary_charge }, ) self.potentials[potential_key] = potential
def store_potentials(self, parameter_handler: BondHandler) -> None: """ Populate self.potentials with key-val pairs of unique potential identifiers and their associated Potential objects """ if self.potentials: self.potentials = dict() for smirks in self.slot_map.values(): parameter_type = parameter_handler.get_parameter( {"smirks": smirks})[0] potential = Potential(parameters={ "k": parameter_type.k, "length": parameter_type.length, }, ) self.potentials[smirks] = potential
def store_potentials(self, parameter_handler: AngleHandler) -> None: """ Populate self.potentials with key-val pairs of unique potential identifiers and their associated Potential objects """ for smirks in self.slot_map.values(): # ParameterHandler.get_parameter returns a list, although this # should only ever be length 1 parameter_type = parameter_handler.get_parameter( {"smirks": smirks})[0] potential = Potential(parameters={ "k": parameter_type.k, "angle": parameter_type.angle, }, ) self.potentials[smirks] = potential
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_potentials(self, parameter_handler: ImproperTorsionHandler) -> None: """ Populate self.potentials with key-val pairs of unique potential identifiers and their associated Potential objects """ for potential_key in self.slot_map.values(): smirks = potential_key.id n = potential_key.mult parameter_type = parameter_handler.get_parameter( {"smirks": smirks})[0] parameters = { "k": parameter_type.k[n], "periodicity": parameter_type.periodicity[n] * unit.dimensionless, "phase": parameter_type.phase[n], "idivf": 3.0 * unit.dimensionless, } potential = Potential(parameters=parameters) self.potentials[potential_key] = potential
def store_constraints( self, parameter_handler: ConstraintHandler, bond_handler: SMIRNOFFBondHandler = None, ) -> None: """ Populate self.constraints with key-val pairs of unique potential identifiers and their associated Potential objects TODO: Raname to store_potentials potentials for consistency? """ if self.constraints: self.constraints = dict() for top_key, pot_key in self.slot_map.items(): smirks = pot_key.id parameter_type = parameter_handler.get_parameter( {"smirks": smirks})[0] if parameter_type.distance: distance = parameter_type.distance else: if not bond_handler: from openff.system.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.") # Look up by atom indices because constraint and bond SMIRKS may not match bond_key = bond_handler.slot_map[top_key] bond_parameter = bond_handler.potentials[bond_key].parameters distance = bond_parameter["length"] potential = Potential(parameters={ "distance": distance, }) self.constraints[pot_key] = potential # type: ignore[assignment]
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 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