def _write_bonds(lmp_file: IO, openff_sys: Interchange): """Write the Bonds section of a LAMMPS data file""" 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.mdtop.bonds): # These are "topology indices" indices = ( bond.atom1.index, bond.atom2.index, ) top_key = TopologyKey(atom_indices=indices) if top_key in bond_handler.slot_map: pot_key = bond_handler.slot_map[top_key] else: top_key = TopologyKey(atom_indices=indices[::-1]) 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 _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 test_store_improper_torsion_matches(self): formaldehyde: Molecule = Molecule.from_mapped_smiles( "[H:3][C:1]([H:4])=[O:2]") parameter_handler = ImproperTorsionHandler(version=0.3) parameter_handler.add_parameter( parameter=ImproperTorsionHandler.ImproperTorsionType( smirks="[*:1]~[#6X3:2](~[*:3])~[*:4]", periodicity1=2, phase1=180.0 * simtk_unit.degree, k1=1.1 * simtk_unit.kilocalorie_per_mole, )) potential_handler = SMIRNOFFImproperTorsionHandler() potential_handler.store_matches(parameter_handler, formaldehyde.to_topology()) assert len(potential_handler.slot_map) == 3 assert (TopologyKey(atom_indices=(0, 1, 2, 3), mult=0) in potential_handler.slot_map) assert (TopologyKey(atom_indices=(0, 2, 3, 1), mult=0) in potential_handler.slot_map) assert (TopologyKey(atom_indices=(0, 3, 1, 2), mult=0) in potential_handler.slot_map)
def store_matches( self, parameter_handler: Union["ElectrostaticsHandlerType", List["ElectrostaticsHandlerType"]], topology: Union["Topology", "OFFBioTop"], ) -> None: """ Populate self.slot_map with key-val pairs of slots and unique potential identifiers """ # Reshape the parameter handlers into a dictionary for easier referencing. parameter_handlers = { handler._TAGNAME: handler for handler in (parameter_handler if isinstance( parameter_handler, list) else [parameter_handler]) } self.potentials = dict() self.slot_map = dict() reference_molecules = [*topology.reference_molecules] for reference_molecule in reference_molecules: matches, potentials = self._find_reference_matches( parameter_handlers, reference_molecule) match_mults = defaultdict(set) for top_key in matches: match_mults[top_key.atom_indices].add(top_key.mult) self.potentials.update(potentials) for top_mol in topology._reference_molecule_to_topology_molecules[ reference_molecule]: for topology_particle in top_mol.atoms: reference_index = topology_particle.atom.molecule_particle_index topology_index = topology_particle.topology_particle_index for mult in match_mults[(reference_index, )]: top_key = TopologyKey(atom_indices=(topology_index, ), mult=mult) self.slot_map[top_key] = matches[TopologyKey( atom_indices=(reference_index, ), mult=mult)]
def _write_angles(lmp_file: IO, openff_sys: Interchange): """Write the Angles section of a LAMMPS data file""" from openff.interchange.components.mdtraj import ( _iterate_angles, _store_bond_partners, ) _store_bond_partners(openff_sys.topology.mdtop) 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(_iterate_angles(openff_sys.topology.mdtop)): # These are "topology indices" indices = tuple(a.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 _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 _write_atoms(lmp_file: IO, openff_sys: Interchange, atom_type_map: Dict): """Write the Atoms section of a LAMMPS data file""" lmp_file.write("\nAtoms\n\n") 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"] charges = electrostatics_handler.charges for atom in openff_sys.topology.mdtop.atoms: molecule_idx = atom.residue.index top_key = TopologyKey(atom_indices=(atom.index,)) pot_key = vdw_hander.slot_map[top_key] atom_type = atom_type_map_inv[pot_key] charge = charges[top_key].m_as(unit.e) pos = openff_sys.positions[atom.index].to(unit.angstrom).magnitude lmp_file.write( "{:d}\t{:d}\t{:d}\t{:.8g}\t{:.8g}\t{:.8g}\t{:.8g}\n".format( atom.index + 1, molecule_idx + 1, atom_type + 1, charge, pos[0], pos[1], pos[2], ) )
def test_bond_potential_handler(self): top = OFFBioTop.from_molecules(Molecule.from_smiles("O=O")) bond_handler = BondHandler(version=0.3) bond_parameter = BondHandler.BondType( smirks="[*:1]~[*:2]", k=1.5 * simtk_unit.kilocalorie_per_mole / simtk_unit.angstrom**2, length=1.5 * simtk_unit.angstrom, id="b1000", ) bond_handler.add_parameter(bond_parameter.to_dict()) from openff.toolkit.typing.engines.smirnoff import ForceField forcefield = ForceField() forcefield.register_parameter_handler(bond_handler) bond_potentials = SMIRNOFFBondHandler._from_toolkit( parameter_handler=forcefield["Bonds"], topology=top, ) top_key = TopologyKey(atom_indices=(0, 1)) pot_key = bond_potentials.slot_map[top_key] assert pot_key.associated_handler == "Bonds" pot = bond_potentials.potentials[pot_key] kcal_mol_a2 = unit.Unit("kilocalorie / (angstrom ** 2 * mole)") assert pot.parameters["k"].to(kcal_mol_a2).magnitude == pytest.approx( 1.5)
def test_angle_potential_handler(self): top = OFFBioTop.from_molecules(Molecule.from_smiles("CCC")) angle_handler = AngleHandler(version=0.3) angle_parameter = AngleHandler.AngleType( smirks="[*:1]~[*:2]~[*:3]", k=2.5 * simtk_unit.kilocalorie_per_mole / simtk_unit.radian**2, angle=100 * simtk_unit.degree, id="b1000", ) angle_handler.add_parameter(angle_parameter.to_dict()) forcefield = ForceField() forcefield.register_parameter_handler(angle_handler) angle_potentials = SMIRNOFFAngleHandler._from_toolkit( parameter_handler=forcefield["Angles"], topology=top, ) top_key = TopologyKey(atom_indices=(0, 1, 2)) pot_key = angle_potentials.slot_map[top_key] assert pot_key.associated_handler == "Angles" pot = angle_potentials.potentials[pot_key] kcal_mol_rad2 = unit.Unit("kilocalorie / (mole * radian ** 2)") assert pot.parameters["k"].to( kcal_mol_rad2).magnitude == pytest.approx(2.5)
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: 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 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 _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 _get_lj_parameters(openff_sys: "Interchange", 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 _get_buck_parameters(openff_sys: "Interchange", 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 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 __add__(self, other): """Combine two Interchange objects. This method is unstable and likely unsafe.""" import mdtraj as md from openff.interchange.models import TopologyKey warnings.warn( "Iterchange object combination is experimental and likely to produce " "strange results. Use with caution!") self_copy = Interchange() self_copy._inner_data = deepcopy(self._inner_data) atom_offset = self_copy.topology.mdtop.n_atoms other_top = deepcopy(other.topology) for top_mol in other_top.topology_molecules: self_copy.topology.add_molecule(top_mol.reference_molecule) self_copy.topology.mdtop = md.Topology.from_openmm( self_copy.topology.to_openmm()) for handler_name, handler in other.handlers.items(): self_handler = self_copy.handlers[handler_name] for top_key, pot_key in handler.slot_map.items(): new_atom_indices = tuple(idx + atom_offset for idx in top_key.atom_indices) new_top_key = TopologyKey( atom_indices=new_atom_indices, mult=top_key.mult, ) self_handler.slot_map.update({new_top_key: pot_key}) self_handler.potentials.update( {pot_key: handler.potentials[pot_key]}) new_positions = np.vstack([self_copy.positions, other.positions]) self_copy.positions = new_positions if not np.all(self_copy.box == other.box): raise NotImplementedError( "Combination with unequal box vectors is not curretnly supported" ) return self_copy
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_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 _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 charges(self) -> Dict[TopologyKey, unit.Quantity]: """Returns the total partial charge on each particle in the associated interchange.""" charges = defaultdict(lambda: 0.0 * unit.e) for topology_key, potential_key in self.slot_map.items(): potential = self.potentials[potential_key] for parameter_key, parameter_value in potential.parameters.items(): if parameter_key != "charge" and parameter_key != "charge_increment": raise NotImplementedError() charge = parameter_value charges[topology_key.atom_indices[0]] += charge return { TopologyKey(atom_indices=(index, )): charge for index, charge in charges.items() }
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 _find_reference_matches( cls, parameter_handlers: Dict[str, "ElectrostaticsHandlerType"], reference_molecule: Molecule, ) -> Tuple[Dict[TopologyKey, PotentialKey], Dict[PotentialKey, Potential]]: """Constructs a slot and potential map for a particular reference molecule and set of parameter handlers.""" matches = {} potentials = {} expected_matches = {i for i in range(reference_molecule.n_atoms)} for handler_type in cls.charge_precedence(): if handler_type not in parameter_handlers: continue parameter_handler = parameter_handlers[handler_type] slot_matches, slot_potentials = None, {} am1_matches, am1_potentials = None, {} if handler_type in ["LibraryCharges", "ChargeIncrementModel"]: slot_matches, slot_potentials = cls._find_slot_matches( parameter_handler, reference_molecule) if handler_type in ["ToolkitAM1BCC", "ChargeIncrementModel"]: am1_matches, am1_potentials = cls._find_am1_matches( parameter_handler, reference_molecule) if slot_matches is None and am1_matches is None: raise NotImplementedError() elif slot_matches is not None and am1_matches is not None: am1_matches = { TopologyKey(atom_indices=topology_key.atom_indices, mult=0): potential_key for topology_key, potential_key in am1_matches.items() } slot_matches = { TopologyKey(atom_indices=topology_key.atom_indices, mult=1): potential_key for topology_key, potential_key in slot_matches.items() } matched_atom_indices = { index for key in slot_matches for index in key.atom_indices } matched_atom_indices.intersection_update({ index for key in am1_matches for index in key.atom_indices }) elif slot_matches is not None: matched_atom_indices = { index for key in slot_matches for index in key.atom_indices } else: matched_atom_indices = { index for key in am1_matches for index in key.atom_indices } if matched_atom_indices != expected_matches: # Handle the case where a handler could not fully assign the charges # to the whole molecule. continue matches.update(slot_matches if slot_matches is not None else {}) matches.update(am1_matches if am1_matches is not None else {}) potentials.update(slot_potentials) potentials.update(am1_potentials) break found_matches = { index for key in matches for index in key.atom_indices } if found_matches != expected_matches: raise RuntimeError( f"{reference_molecule.to_smiles(explicit_hydrogens=False)} could " f"not be fully assigned charges.") return matches, potentials
def _get_potential_key_id(atom_slots: Dict[TopologyKey, PotentialKey], idx): """From a dictionary of TopologyKey: PotentialKey, get the PotentialKey id""" top_key = TopologyKey(atom_indices=(idx, )) return atom_slots[top_key].id