def get_parameter_handler_from_forcefield(self, parameter_handler_name, forcefield): """ It returns a parameter handler from the forcefield based on its name. Parameters ---------- parameter_handler_name : str The name of the parameter handler that is requested forcefield : an openforcefield.typing.engines.smirnoff.ForceField object The forcefield from which the parameter handler will be obtained Returns ------- parameter_handler : an openforcefield.typing.engines.smirnoff.parameters.ParameterHandler object The ParameterHandler that was requested """ from openff.toolkit.typing.engines.smirnoff import ForceField if isinstance(forcefield, str): forcefield = ForceField(forcefield) elif isinstance(forcefield, ForceField): pass else: raise Exception('Invalid forcefield type') return forcefield.get_parameter_handler(parameter_handler_name)
def test_force_field_custom_handler(mock_entry_point_plugins): """Tests a force field can make use of a custom parameter handler registered through the entrypoint plugin system. """ # Construct a simple FF which only uses the custom handler. force_field_contents = "\n".join([ "<?xml version='1.0' encoding='ASCII'?>", "<SMIRNOFF version='0.3' aromaticity_model='OEAroModel_MDL'>", " <CustomHandler version='0.3'></CustomHandler>", "</SMIRNOFF>", ]) # An exception should be raised when plugins aren't allowed. with pytest.raises(KeyError) as error_info: ForceField(force_field_contents) assert ( "Cannot find a registered parameter handler class for tag 'CustomHandler'" in error_info.value.args[0]) # Otherwise the FF should be created as expected. force_field = ForceField(force_field_contents, load_plugins=True) parameter_handler = force_field.get_parameter_handler("CustomHandler") assert parameter_handler is not None assert parameter_handler.__class__.__name__ == "CustomHandler"
def test_openmm_nonbonded_methods(inputs): """See test_nonbonded_method_resolution in openff/toolkit/tests/test_forcefield.py""" vdw_method = inputs["vdw_method"] electrostatics_method = inputs["electrostatics_method"] periodic = inputs["periodic"] result = inputs["result"] molecules = [create_ethanol()] forcefield = ForceField("test_forcefields/test_forcefield.offxml") pdbfile = app.PDBFile(get_data_file_path("systems/test_systems/1_ethanol.pdb")) topology = Topology.from_openmm(pdbfile.topology, unique_molecules=molecules) if not periodic: topology.box_vectors = None if type(result) == int: nonbonded_method = result # The method is validated and may raise an exception if it's not supported. forcefield.get_parameter_handler("vdW", {}).method = vdw_method forcefield.get_parameter_handler( "Electrostatics", {} ).method = electrostatics_method openff_interchange = Interchange.from_smirnoff( force_field=forcefield, topology=topology ) openmm_system = openff_interchange.to_openmm(combine_nonbonded_forces=True) for force in openmm_system.getForces(): if isinstance(force, openmm.NonbondedForce): assert force.getNonbondedMethod() == nonbonded_method break else: raise Exception elif issubclass(result, (BaseException, Exception)): exception = result forcefield.get_parameter_handler("vdW", {}).method = vdw_method forcefield.get_parameter_handler( "Electrostatics", {} ).method = electrostatics_method openff_interchange = Interchange.from_smirnoff( force_field=forcefield, topology=topology ) with pytest.raises(exception): openff_interchange.to_openmm(combine_nonbonded_forces=True) else: raise Exception("uh oh")
def test_combine_molecules_offxml_plugin_deepdiff(tmpdir, coumarin, rfree_data): """Make sure that systems made from molecules using the xml method match offxmls with plugins""" # we need to recalculate the Nonbonded terms using the fake Rfree coumarin_copy = coumarin.copy(deep=True) # apply symmetry to make sure systems match MBISCharges.apply_symmetrisation(coumarin_copy) with tmpdir.as_cwd(): # make the offxml using the plugin interface _combine_molecules_offxml( molecules=[coumarin_copy], parameters=elements, rfree_data=rfree_data, filename="openff.offxml", water_model="tip3p", ) offxml = ForceField( "openff.offxml", load_plugins=True, allow_cosmetic_attributes=True ) # check the plugin is being used vdw = offxml.get_parameter_handler("QUBEKitvdWTS") # make sure we have the parameterize tags assert len(vdw._cosmetic_attribs) == 1 assert len(vdw.parameters) == 28 alpha = rfree_data.pop("alpha") beta = rfree_data.pop("beta") lj = LennardJones612(free_parameters=rfree_data, alpha=alpha, beta=beta) # get new Rfree data lj.run(coumarin_copy) coumarin_copy.write_parameters("coumarin.xml") coumarin_ref_system = xmltodict.parse( XmlSerializer.serialize( app.ForceField("coumarin.xml").createSystem( topology=coumarin_copy.to_openmm_topology(), nonbondedCutoff=9 * unit.angstroms, removeCMMotion=False, ) ) ) coumarin_off_system = xmltodict.parse( XmlSerializer.serialize( offxml.create_openmm_system( Molecule.from_rdkit(coumarin_copy.to_rdkit()).to_topology() ) ) ) coumarin_diff = DeepDiff( coumarin_ref_system, coumarin_off_system, ignore_order=True, significant_digits=6, exclude_regex_paths="mass", ) assert len(coumarin_diff) == 1 for item in coumarin_diff["iterable_item_added"].values(): assert item["@k"] == "0"
def test_prepare_force_field(self, optimization): """Test that the correct cosmetic attributes are attached to the FF, especially in the special case of BCC handlers.""" optimization.parameters_to_train.append( Parameter( handler_type="ChargeIncrementModel", smirks="[#6:1]-[#6:2]", attribute_name="charge_increment1", )) optimization.parameters_to_train.append( Parameter( handler_type="vdW", smirks=None, attribute_name="scale14", )) with temporary_cd(): OptimizationInputFactory._prepare_force_field(optimization, None) assert os.path.isfile( os.path.join("forcefield", "force-field.offxml")) off_force_field = OFFForceField( os.path.join("forcefield", "force-field.offxml"), allow_cosmetic_attributes=True, ) vdw_handler = off_force_field.get_parameter_handler("vdW") assert vdw_handler._parameterize == "scale14" assert len(vdw_handler.parameters) == 1 parameter = vdw_handler.parameters["[#6:1]"] assert parameter._parameterize == "epsilon, sigma" bcc_handler = off_force_field.get_parameter_handler( "ChargeIncrementModel") assert len(bcc_handler.parameters) == 1 parameter = bcc_handler.parameters["[#6:1]-[#6:2]"] assert len(parameter.charge_increment) == 1 assert parameter._parameterize == "charge_increment1"
def ideal_water_force_field() -> ForceField: """Returns a force field that will assign constraints, a vdW handler and a library charge handler to a three site water molecule with all LJ ``epsilon=0.0`` and all ``q=0.0``. """ ff = ForceField(load_plugins=True) constraint_handler = ff.get_parameter_handler("Constraints") constraint_handler.add_parameter({ "smirks": "[#1:1]-[#8X2H2+0:2]-[#1]", "distance": 0.9572 * unit.angstrom }) constraint_handler.add_parameter({ "smirks": "[#1:1]-[#8X2H2+0]-[#1:2]", "distance": 1.5139 * unit.angstrom }) # add a dummy vdW term vdw_handler = ff.get_parameter_handler("vdW") vdw_handler.add_parameter({ "smirks": "[#1:1]-[#8X2H2+0]-[#1]", "epsilon": 0.0 * unit.kilojoule_per_mole, "sigma": 1.0 * unit.angstrom, }) vdw_handler.add_parameter({ "smirks": "[#1]-[#8X2H2+0:1]-[#1]", "epsilon": 0.0 * unit.kilojoules_per_mole, "sigma": 0.0 * unit.nanometers, }) # add the library charges library_charge = ff.get_parameter_handler("LibraryCharges") library_charge.add_parameter({ "smirks": "[#1]-[#8X2H2+0:1]-[#1]", "charge1": 0 * unit.elementary_charge }) library_charge.add_parameter({ "smirks": "[#1:1]-[#8X2H2+0]-[#1]", "charge1": 0 * unit.elementary_charge }) return ff
def test_combine_cli_all_offxml( run_cli, tmpdir, acetone, coumarin, openff, rdkit_workflow, parameters, expected ): """ Test combining offxmls via the cli with different parameters. """ workflow = rdkit_workflow.copy(deep=True) workflow.non_bonded = get_protocol(protocol_name="5b") with tmpdir.as_cwd(): openff.run(coumarin) openff.run(acetone) # make some dummy dirs os.mkdir("QUBEKit_acetone") os.mkdir("QUBEKit_coumarin") result = workflow._build_initial_results(molecule=acetone) result.to_file(os.path.join("QUBEKit_acetone", "workflow_result.json")) result = workflow._build_initial_results(molecule=coumarin) result.to_file(os.path.join("QUBEKit_coumarin", "workflow_result.json")) output = run_cli.invoke( combine, args=["combined.offxml", "-offxml", *parameters] ) assert output.exit_code == 0 assert "2 molecules found, combining..." in output.output if expected != 0: ff = ForceField( "combined.offxml", allow_cosmetic_attributes=True, load_plugins=True ) # make sure we have used the plugin method vdw_handler = ff.get_parameter_handler("QUBEKitvdWTS") assert len(vdw_handler.parameters) == 32 assert "parameterize" in vdw_handler._cosmetic_attribs assert len(getattr(vdw_handler, "_parameterize").split(",")) == expected else: # we are using the normal format so make sure it complies ff = ForceField("combined.offxml") vdw_handler = ff.get_parameter_handler("vdW") assert len(vdw_handler.parameters) == 34
def test_scaled_de_energy(): """For a molecule with 1-4 interactions make sure the scaling is correctly applied. Note that only nonbonded parameters are non zero. """ ff = ForceField(load_plugins=True) ff.get_parameter_handler("Electrostatics") ff.get_parameter_handler( "ChargeIncrementModel", { "version": "0.3", "partial_charge_method": "formal_charge" }, ) vdw_handler = ff.get_parameter_handler("vdW") vdw_handler.add_parameter({ "smirks": "[*:1]", "epsilon": 0.0 * unit.kilojoule_per_mole, "sigma": 1.0 * unit.angstrom, }) double_exp = ff.get_parameter_handler("DoubleExponential") double_exp.alpha = 18.7 double_exp.beta = 3.3 double_exp.scale14 = 1 double_exp.add_parameter({ "smirks": "[#6X4:1]", "r_min": 3.816 * unit.angstrom, "epsilon": 0.1094 * unit.kilocalorie_per_mole, }) double_exp.add_parameter({ "smirks": "[#1:1]-[#6X4]", "r_min": 2.974 * unit.angstrom, "epsilon": 0.0157 * unit.kilocalorie_per_mole, }) ethane = Molecule.from_smiles("CC") ethane.generate_conformers(n_conformers=1) off_top = ethane.to_topology() omm_top = off_top.to_openmm() system_no_scale = ff.create_openmm_system(topology=off_top) energy_no_scale = evaluate_energy(system=system_no_scale, topology=omm_top, positions=ethane.conformers[0]) # now scale 1-4 by half double_exp.scale14 = 0.5 system_scaled = ff.create_openmm_system(topology=off_top) energy_scaled = evaluate_energy(system=system_scaled, topology=omm_top, positions=ethane.conformers[0]) assert double_exp.scale14 * energy_no_scale == pytest.approx(energy_scaled, abs=1e-6)
def serialise_system(self): """Create the OpenMM system; parametrise using frost; serialise the system.""" # Create an openFF molecule from the rdkit molecule off_molecule = Molecule.from_rdkit(self.molecule.to_rdkit(), allow_undefined_stereo=True) # Make the OpenMM system off_topology = off_molecule.to_topology() forcefield = ForceField("openff_unconstrained-1.3.0.offxml") try: # Parametrise the topology and create an OpenMM System. system = forcefield.create_openmm_system(off_topology) except ( UnassignedValenceParameterException, UnassignedBondParameterException, UnassignedProperTorsionParameterException, UnassignedAngleParameterException, UnassignedMoleculeChargeException, TypeError, ): # If this does not work then we have a molecule that is not in SMIRNOFF so we must add generics # and remove the charge handler to get some basic parameters for the moleucle new_bond = BondHandler.BondType( smirks="[*:1]~[*:2]", length="0 * angstrom", k="0.0 * angstrom**-2 * mole**-1 * kilocalorie", ) new_angle = AngleHandler.AngleType( smirks="[*:1]~[*:2]~[*:3]", angle="0.0 * degree", k="0.0 * mole**-1 * radian**-2 * kilocalorie", ) new_torsion = ProperTorsionHandler.ProperTorsionType( smirks="[*:1]~[*:2]~[*:3]~[*:4]", periodicity1="1", phase1="0.0 * degree", k1="0.0 * mole**-1 * kilocalorie", periodicity2="2", phase2="180.0 * degree", k2="0.0 * mole**-1 * kilocalorie", periodicity3="3", phase3="0.0 * degree", k3="0.0 * mole**-1 * kilocalorie", periodicity4="4", phase4="180.0 * degree", k4="0.0 * mole**-1 * kilocalorie", idivf1="1.0", idivf2="1.0", idivf3="1.0", idivf4="1.0", ) new_vdw = vdWHandler.vdWType( smirks="[*:1]", epsilon=0 * unit.kilocalories_per_mole, sigma=0 * unit.angstroms, ) new_generics = { "Bonds": new_bond, "Angles": new_angle, "ProperTorsions": new_torsion, "vdW": new_vdw, } for key, val in new_generics.items(): forcefield.get_parameter_handler(key).parameters.insert(0, val) # This has to be removed as sqm will fail with unknown elements del forcefield._parameter_handlers["ToolkitAM1BCC"] del forcefield._parameter_handlers["Electrostatics"] # Parametrize the topology and create an OpenMM System. system = forcefield.create_openmm_system(off_topology) # This will tag the molecule so run.py knows that generics have been used. self.fftype = "generics" # Serialise the OpenMM system into the xml file with open("serialised.xml", "w+") as out: out.write(XmlSerializer.serializeSystem(system))
ff = ForceField() ff.aromaticity_model = 'OEAroModel_MDL' # Loop over the parameters to convert and their corresponding SMIRNOFF header tags for smirnoff_tag, param_dicts in { "Bonds": bond_dicts, "Angles": angle_dicts, "ProperTorsions": proper_dicts, "AmberImproperTorsions": improper_dicts, #'LibraryCharges': charge_dicts, #'vdW': nonbond_dicts }.items(): # Get the parameter handler for the given tag. The # "get_parameter_handler" method will initialize an # empty handler if none exists yet (as is the case here). handler = ff.get_parameter_handler(smirnoff_tag) # Loop over the list of parameter dictionaries, using each one as an input to # handler.add_parameter. This mimics deserializing an OFFXML into a ForceField # object, and will do any sanitization that we might otherwise miss from openff.toolkit.typing.engines.smirnoff.parameters import DuplicateParameterError for param_dict in param_dicts: try: handler.add_parameter(param_dict) except DuplicateParameterError: continue # Add the ElectrostaticsHandler, with the proper 1-4 scaling factors handler = ff.get_parameter_handler('Electrostatics') # Write the now-populated forcefield out to OFFXML ff.to_file('result_backbone.offxml')
def system_subset( parameter_key: ParameterGradientKey, force_field: "ForceField", topology: "Topology", scale_amount: Optional[float] = None, ) -> Tuple["openmm.System", "simtk_unit.Quantity"]: """Produces an OpenMM system containing the minimum number of forces while still containing a specified force field parameter, and those other parameters which may interact with it (e.g. in the case of vdW parameters). The value of the parameter of interest may optionally be perturbed by an amount specified by ``scale_amount``. Parameters ---------- parameter_key The parameter of interest. force_field The force field to create the system from (and optionally perturb). topology The topology of the system to apply the force field to. scale_amount: float, optional The optional amount to perturb the ``parameter`` by such that ``parameter = (1.0 + scale_amount) * parameter``. Returns ------- The created system as well as the value of the specified ``parameter``. """ # As this method deals mainly with the toolkit, we stick to # simtk units here. from openff.toolkit.typing.engines.smirnoff import ForceField # Create the force field subset. force_field_subset = ForceField() handlers_to_register = {parameter_key.tag} if parameter_key.tag in {"ChargeIncrementModel", "LibraryCharges"}: # Make sure to retain all of the electrostatic handlers when dealing with # charges as the applied charges will depend on which charges have been applied # by previous handlers. handlers_to_register.update( {"Electrostatics", "ChargeIncrementModel", "LibraryCharges"}) registered_handlers = force_field.registered_parameter_handlers for handler_to_register in handlers_to_register: if handler_to_register not in registered_handlers: continue force_field_subset.register_parameter_handler( copy.deepcopy( force_field.get_parameter_handler(handler_to_register))) handler = force_field_subset.get_parameter_handler(parameter_key.tag) parameter = handler.parameters[parameter_key.smirks] parameter_value = getattr(parameter, parameter_key.attribute) # Optionally perturb the parameter of interest. if scale_amount is not None: if numpy.isclose(parameter_value.value_in_unit(parameter_value.unit), 0.0): # Careful thought needs to be given to this. Consider cases such as # epsilon or sigma where negative values are not allowed. parameter_value = (scale_amount if scale_amount > 0.0 else 0.0) * parameter_value.unit else: parameter_value *= 1.0 + scale_amount setattr(parameter, parameter_key.attribute, parameter_value) # Create the parameterized sub-system. system = force_field_subset.create_openmm_system(topology) return system, parameter_value
def generate_coverage_report(input_molecules: List[Molecule], forcefield_name: str, processors: Optional[int] = None): """ For the given set of molecules produce a coverage report of the smirks parameters used in typing the molecule. Also try and produce charges for the molecules as some may have missing bccs. Parameters ---------- input_molecules: The List of 3d sdf files to run the coverage on or the name of the directory containing the files. forcefield_name: The name of the openFF forcefield to run the coverage for. processors: The number of processors we can use to build the coverage report Returns ------- coverage_report: A dictionary split into parameter types which lists the number of occurrences of each parameter. success_mols: A list of openff.toolkit.topology.Molecule objects that were successful in this step error_mols: A list of tuples (Molecule, Exception) of molecules that failed this step """ if isinstance(input_molecules, Molecule): input_molecules = [ input_molecules, ] coverage = { "Angles": {}, "Bonds": {}, "ProperTorsions": {}, "ImproperTorsions": {}, "vdW": {} } # make the forcefield ff = ForceField(forcefield_name) # For speed, don't test charge assignment for now ff.deregister_parameter_handler('ToolkitAM1BCC') # ff.deregister_parameter_handler("Electrostatics") ff.get_parameter_handler('ChargeIncrementModel', { 'partial_charge_method': 'formal_charge', 'version': '0.3' }) # now run coverage on each molecule success_mols = [] error_mols = [] if processors is None or processors > 1: from multiprocessing import Pool with Pool(processes=processors) as pool: # generate the work list work_list = [ pool.apply_async(single_molecule_coverage, (molecule, ff)) for molecule in input_molecules ] for work in tqdm.tqdm( work_list, total=len(work_list), ncols=80, desc="{:30s}".format("Generating Coverage"), ): report, e = work.get() molecule = report["molecule"] if e is None: _update_coverage(coverage, report) success_mols.append(molecule) #except Exception as e: else: error_mols.append((molecule, e)) else: for molecule in tqdm.tqdm(input_molecules, total=len(input_molecules), ncols=80, desc="{:30s}".format("Generating Coverage")): #try: report, e = single_molecule_coverage(molecule, ff) # update the master coverage dict mol: Molecule = report["molecule"] if e is None: _update_coverage(coverage, report) success_mols.append(mol) #except Exception as e: else: error_mols.append((molecule, e)) # Sort the keys of the coverage dict, so that it doesn't show which parameters were found first for parameter_type in coverage.keys(): sorted_sub_dict = dict(sorted(coverage[parameter_type].items())) coverage[parameter_type] = sorted_sub_dict # record how many molecules we processed coverage["passed_unique_molecules"] = len(success_mols) coverage["total_unique_molecules"] = len(success_mols) + len(error_mols) coverage["forcefield_name"] = forcefield_name return coverage, success_mols, error_mols
def buckingham_water_force_field() -> ForceField: """Create a buckingham water model Forcefield object.""" force_field = ForceField(load_plugins=True) # Add in a constraint handler to ensure the correct H-O-H geometry. constraint_handler = force_field.get_parameter_handler("Constraints") # Keep the H-O bond length fixed at 0.9572 angstroms. constraint_handler.add_parameter({ "smirks": "[#1:1]-[#8X2H2+0:2]-[#1]", "distance": 0.9572 * unit.angstrom }) # Keep the H-O-H angle fixed at 104.52 degrees. constraint_handler.add_parameter({ "smirks": "[#1:1]-[#8X2H2+0]-[#1:2]", "distance": 1.5139 * unit.angstrom }) # Add a default vdW handler which is currently required by the OFF TK. vdw_handler = force_field.get_parameter_handler("vdW") vdw_handler.add_parameter({ "smirks": "[#1:1]-[#8X2H2+0]-[#1]", "epsilon": 0.0 * unit.kilojoule_per_mole, "sigma": 1.0 * unit.angstrom, }) vdw_handler.add_parameter({ "smirks": "[#1]-[#8X2H2+0:1]-[#1]", "epsilon": 0.0 * unit.kilojoules_per_mole, "sigma": 0.0 * unit.nanometers, # "epsilon": 0.680946 * unit.kilojoules_per_mole, # "sigma": 0.316435 * unit.nanometers, }) # Add a charge handler to zero the charges on water. The charges will be # applied by the virtual site handler instead. force_field.get_parameter_handler("Electrostatics") force_field.get_parameter_handler( "ChargeIncrementModel", { "version": "0.3", "partial_charge_method": "formal_charge" }, ) # Add a virtual site handler to add the virtual charge site. virtual_site_handler = force_field.get_parameter_handler("VirtualSites") virtual_site_handler.add_parameter({ "smirks": "[#1:2]-[#8X2H2+0:1]-[#1:3]", "type": "DivalentLonePair", "distance": -0.0106 * unit.nanometers, "outOfPlaneAngle": 0.0 * unit.degrees, "match": "once", "charge_increment1": 1.0552 * 0.5 * unit.elementary_charge, "charge_increment2": 0.0 * unit.elementary_charge, "charge_increment3": 1.0552 * 0.5 * unit.elementary_charge, }) virtual_site_handler._parameters = ParameterList( virtual_site_handler._parameters) # Finally add the custom buckingham charge handler. buckingham_handler = force_field.get_parameter_handler( "DampedBuckingham68") buckingham_handler.add_parameter({ "smirks": "[#1:1]-[#8X2H2+0]-[#1]", "a": 0.0 * unit.kilojoule_per_mole, "b": 0.0 / unit.nanometer, "c6": 0.0 * unit.kilojoule_per_mole * unit.nanometer**6, "c8": 0.0 * unit.kilojoule_per_mole * unit.nanometer**8, }) buckingham_handler.add_parameter({ "smirks": "[#1]-[#8X2H2+0:1]-[#1]", "a": 1600000.0 * unit.kilojoule_per_mole, "b": 42.00 / unit.nanometer, "c6": 0.003 * unit.kilojoule_per_mole * unit.nanometer**6, "c8": 0.00003 * unit.kilojoule_per_mole * unit.nanometer**8, }) return force_field
def _build_system(self, molecule: "Ligand", input_files: Optional[List[str]] = None) -> System: """Create the OpenMM system; parametrise using frost; serialise the system.""" # Create an openFF molecule from the rdkit molecule, we always have hydrogen by this point off_molecule = Molecule.from_rdkit( molecule.to_rdkit(), allow_undefined_stereo=True, hydrogens_are_explicit=True, ) # Make the OpenMM system off_topology = off_molecule.to_topology() forcefield = ForceField(self.force_field) # we need to remove the constraints if "Constraints" in forcefield._parameter_handlers: del forcefield._parameter_handlers["Constraints"] try: # Parametrise the topology and create an OpenMM System. system = forcefield.create_openmm_system(off_topology, ) except ( UnassignedValenceParameterException, UnassignedBondParameterException, UnassignedProperTorsionParameterException, UnassignedAngleParameterException, UnassignedMoleculeChargeException, TypeError, ): # If this does not work then we have a molecule that is not in SMIRNOFF so we must add generics # and remove the charge handler to get some basic parameters for the moleucle new_bond = BondHandler.BondType( smirks="[*:1]~[*:2]", length="0 * angstrom", k="0.0 * angstrom**-2 * mole**-1 * kilocalorie", ) new_angle = AngleHandler.AngleType( smirks="[*:1]~[*:2]~[*:3]", angle="0.0 * degree", k="0.0 * mole**-1 * radian**-2 * kilocalorie", ) new_torsion = ProperTorsionHandler.ProperTorsionType( smirks="[*:1]~[*:2]~[*:3]~[*:4]", periodicity1="1", phase1="0.0 * degree", k1="0.0 * mole**-1 * kilocalorie", periodicity2="2", phase2="180.0 * degree", k2="0.0 * mole**-1 * kilocalorie", periodicity3="3", phase3="0.0 * degree", k3="0.0 * mole**-1 * kilocalorie", periodicity4="4", phase4="180.0 * degree", k4="0.0 * mole**-1 * kilocalorie", idivf1="1.0", idivf2="1.0", idivf3="1.0", idivf4="1.0", ) new_vdw = vdWHandler.vdWType( smirks="[*:1]", epsilon=0 * unit.kilocalories_per_mole, sigma=0 * unit.angstroms, ) new_generics = { "Bonds": new_bond, "Angles": new_angle, "ProperTorsions": new_torsion, "vdW": new_vdw, } for key, val in new_generics.items(): forcefield.get_parameter_handler(key).parameters.insert(0, val) # This has to be removed as sqm will fail with unknown elements del forcefield._parameter_handlers["ToolkitAM1BCC"] del forcefield._parameter_handlers["Electrostatics"] # Parametrize the topology and create an OpenMM System. system = forcefield.create_openmm_system(off_topology) return system
def _combine_molecules_offxml( molecules: List["Ligand"], parameters: List[str], rfree_data: Dict[str, Dict[str, Union[str, float]]], filename: str, water_model: Literal["tip3p"] = "tip3p", ): """ Main worker function to build the combined offxmls. """ if sum([molecule.extra_sites.n_sites for molecule in molecules]) > 0: raise NotImplementedError( "Virtual sites can not be safely converted into offxml format yet." ) if sum([molecule.RBTorsionForce.n_parameters for molecule in molecules]) > 0: raise NotImplementedError( "RBTorsions can not yet be safely converted into offxml format yet." ) try: from chemper.graphs.cluster_graph import ClusterGraph except ModuleNotFoundError: raise ModuleNotFoundError( "chemper is required to make an offxml, please install with `conda install chemper -c conda-forge`." ) fit_ab = False # if alpha and beta should be fit if "AB" in parameters: fit_ab = True rfree_codes = set() # keep track of all rfree codes used by these molecules # create the master ff offxml = ForceField(allow_cosmetic_attributes=True, load_plugins=True) offxml.author = f"QUBEKit_version_{qubekit.__version__}" offxml.date = datetime.now().strftime("%Y_%m_%d") # get all of the handlers _ = offxml.get_parameter_handler("Constraints") bond_handler = offxml.get_parameter_handler("Bonds") angle_handler = offxml.get_parameter_handler("Angles") proper_torsions = offxml.get_parameter_handler("ProperTorsions") improper_torsions = offxml.get_parameter_handler("ImproperTorsions") _ = offxml.get_parameter_handler( "Electrostatics", handler_kwargs={"scale14": 0.8333333333, "version": 0.3} ) using_plugin = False if parameters: # if we want to optimise the Rfree we need our custom handler vdw_handler = offxml.get_parameter_handler( "QUBEKitvdWTS", allow_cosmetic_attributes=True ) using_plugin = True else: vdw_handler = offxml.get_parameter_handler( "vdW", allow_cosmetic_attributes=True ) library_charges = offxml.get_parameter_handler("LibraryCharges") for molecule in molecules: rdkit_mol = molecule.to_rdkit() bond_types = molecule.bond_types # for each bond type collection create a single smirks pattern for bonds in bond_types.values(): graph = ClusterGraph( mols=[rdkit_mol], smirks_atoms_lists=[bonds], layers="all" ) qube_bond = molecule.BondForce[bonds[0]] bond_handler.add_parameter( parameter_kwargs={ "smirks": graph.as_smirks(), "length": qube_bond.length * unit.nanometers, "k": qube_bond.k * unit.kilojoule_per_mole / unit.nanometers**2, } ) angle_types = molecule.angle_types for angles in angle_types.values(): graph = ClusterGraph( mols=[rdkit_mol], smirks_atoms_lists=[angles], layers="all", ) qube_angle = molecule.AngleForce[angles[0]] angle_handler.add_parameter( parameter_kwargs={ "smirks": graph.as_smirks(), "angle": qube_angle.angle * unit.radian, "k": qube_angle.k * unit.kilojoule_per_mole / unit.radians**2, } ) torsion_types = molecule.dihedral_types for dihedrals in torsion_types.values(): graph = ClusterGraph( mols=[rdkit_mol], smirks_atoms_lists=[dihedrals], layers="all", ) qube_dihedral = molecule.TorsionForce[dihedrals[0]] proper_torsions.add_parameter( parameter_kwargs={ "smirks": graph.as_smirks(), "k1": qube_dihedral.k1 * unit.kilojoule_per_mole, "k2": qube_dihedral.k2 * unit.kilojoule_per_mole, "k3": qube_dihedral.k3 * unit.kilojoule_per_mole, "k4": qube_dihedral.k4 * unit.kilojoule_per_mole, "periodicity1": qube_dihedral.periodicity1, "periodicity2": qube_dihedral.periodicity2, "periodicity3": qube_dihedral.periodicity3, "periodicity4": qube_dihedral.periodicity4, "phase1": qube_dihedral.phase1 * unit.radians, "phase2": qube_dihedral.phase2 * unit.radians, "phase3": qube_dihedral.phase3 * unit.radians, "phase4": qube_dihedral.phase4 * unit.radians, "idivf1": 1, "idivf2": 1, "idivf3": 1, "idivf4": 1, } ) improper_types = molecule.improper_types for torsions in improper_types.values(): impropers = [ (improper[1], improper[0], *improper[2:]) for improper in torsions ] graph = ClusterGraph( mols=[rdkit_mol], smirks_atoms_lists=[impropers], layers="all" ) qube_improper = molecule.ImproperTorsionForce[torsions[0]] # we need to multiply each k value by as they will be applied as trefoil see # <https://openforcefield.github.io/standards/standards/smirnoff/#impropertorsions> for more details # we assume we only have a k2 term for improper torsions via a periodic term improper_torsions.add_parameter( parameter_kwargs={ "smirks": graph.as_smirks(), "k1": qube_improper.k2 * 3 * unit.kilojoule_per_mole, "periodicity1": qube_improper.periodicity2, "phase1": qube_improper.phase2 * unit.radians, } ) atom_types = {} for atom_index, cip_type in molecule.atom_types.items(): atom_types.setdefault(cip_type, []).append((atom_index,)) for sym_set in atom_types.values(): graph = ClusterGraph( mols=[rdkit_mol], smirks_atoms_lists=[sym_set], layers="all" ) qube_non_bond = molecule.NonbondedForce[sym_set[0]] rfree_code = _get_parameter_code( molecule=molecule, atom_index=sym_set[0][0] ) atom_data = { "smirks": graph.as_smirks(), } if rfree_code in parameters or fit_ab: # keep track of present codes to optimise rfree_codes.add(rfree_code) if using_plugin: # this is to be refit atom = molecule.atoms[qube_non_bond.atoms[0]] atom_data["volume"] = atom.aim.volume * unit.angstroms**3 else: atom_data["epsilon"] = qube_non_bond.epsilon * unit.kilojoule_per_mole atom_data["sigma"] = qube_non_bond.sigma * unit.nanometers vdw_handler.add_parameter(parameter_kwargs=atom_data) charge_data = dict( (f"charge{param.atoms[0] + 1}", param.charge * unit.elementary_charge) for param in molecule.NonbondedForce ) charge_data["smirks"] = molecule.to_smiles(mapped=True) library_charges.add_parameter(parameter_kwargs=charge_data) # now loop over all the parameters to be fit and add them as cosmetic attributes to_parameterize = [] for parameter_to_fit in parameters: if parameter_to_fit != "AB" and parameter_to_fit in rfree_codes: setattr( vdw_handler, f"{parameter_to_fit.lower()}free", unit.Quantity( rfree_data[parameter_to_fit]["r_free"], unit=unit.angstroms ), ) to_parameterize.append(f"{parameter_to_fit.lower()}free") if fit_ab: vdw_handler.alpha = rfree_data["alpha"] vdw_handler.beta = rfree_data["beta"] to_parameterize.extend(["alpha", "beta"]) if to_parameterize: vdw_handler.add_cosmetic_attribute("parameterize", ", ".join(to_parameterize)) # now add a water model to the force field _add_water_model( force_field=offxml, water_model=water_model, using_plugin=using_plugin ) offxml.to_file(filename=filename)
def _add_water_model( force_field: ForceField, using_plugin: bool, water_model: Literal["tip3p"] = "tip3p" ): """Add a water model to an offxml force field""" # add generic bond and angle smirks which will only hit water bond_handler = force_field.get_parameter_handler("Bonds") bond_handler.add_parameter( parameter_kwargs={ "smirks": "[#1:1]-[#8X2H2+0:2]-[#1]", "k": 1087.053566377 * unit.kilocalorie_per_mole / unit.angstroms**2, "length": 0.9572 * unit.angstroms, } ) angle_handler = force_field.get_parameter_handler("Angles") angle_handler.add_parameter( parameter_kwargs={ "smirks": "[#1:1]-[#8X2H2+0:2]-[#1:3]", "k": 130.181232192 * unit.kilocalorie_per_mole / unit.radian**2, "angle": 110.3538806181 * unit.degree, } ) if water_model == "tip3p": constrains = force_field.get_parameter_handler("Constraints") constrains.add_parameter( parameter_kwargs={ "smirks": "[#1:1]-[#8X2H2+0:2]-[#1]", "id": "c-tip3p-H-O", "distance": 0.9572 * unit.angstroms, } ) constrains.add_parameter( parameter_kwargs={ "smirks": "[#1:1]-[#8X2H2+0]-[#1:2]", "id": "c-tip3p-H-O-H", "distance": 1.5139006545247014 * unit.angstroms, } ) if using_plugin: # if we are using the plugin to optimise the molecule we need a special vdw handler vdw_handler = force_field.get_parameter_handler("QUBEKitvdW") else: vdw_handler = force_field.get_parameter_handler("vdW") vdw_handler.add_parameter( parameter_kwargs={ "smirks": "[#1]-[#8X2H2+0:1]-[#1]", "epsilon": 0.1521 * unit.kilocalorie_per_mole, "id": "n-tip3p-O", "sigma": 3.1507 * unit.angstroms, } ) vdw_handler.add_parameter( parameter_kwargs={ "smirks": "[#1:1]-[#8X2H2+0]-[#1]", "epsilon": 0 * unit.kilocalorie_per_mole, "id": "n-tip3p-H", "sigma": 1 * unit.angstroms, } ) library_charges = force_field.get_parameter_handler("LibraryCharges") library_charges.add_parameter( parameter_kwargs={ "smirks": "[#1]-[#8X2H2+0:1]-[#1]", "charge1": -0.834 * unit.elementary_charge, "id": "q-tip3p-O", } ) library_charges.add_parameter( parameter_kwargs={ "smirks": "[#1:1]-[#8X2H2+0]-[#1]", "charge1": 0.417 * unit.elementary_charge, "id": "q-tip3p-H", } ) else: raise NotImplementedError( "Only the tip3p water model is support for offxmls so far." )