diff --git a/.github/workflows/choose_branch.yaml b/.github/workflows/choose_branch.yaml index 41541f42d..4d95e94b4 100644 --- a/.github/workflows/choose_branch.yaml +++ b/.github/workflows/choose_branch.yaml @@ -46,6 +46,7 @@ jobs: env: SIRE_DONT_PHONEHOME: 1 SIRE_SILENT_PHONEHOME: 1 + SIRE_EMLE: 1 REPO: "${{ github.repository }}" steps: # diff --git a/.github/workflows/devel.yaml b/.github/workflows/devel.yaml index 9f623d3e2..bf520b79a 100644 --- a/.github/workflows/devel.yaml +++ b/.github/workflows/devel.yaml @@ -41,6 +41,7 @@ jobs: env: SIRE_DONT_PHONEHOME: 1 SIRE_SILENT_PHONEHOME: 1 + SIRE_EMLE: 1 REPO: "${{ github.repository }}" steps: # diff --git a/.github/workflows/main.yaml b/.github/workflows/main.yaml index 0b2c9ad5e..3a1f0761f 100644 --- a/.github/workflows/main.yaml +++ b/.github/workflows/main.yaml @@ -32,7 +32,7 @@ jobs: exclude: - platform: { name: "macos", os: "macos-latest", shell: "bash -l {0}" } - python-version: "3.12" # MacOS can't run 3.12 yet... + python-version: "3.12" # MacOS can't run 3.12 yet... environment: name: sire-build defaults: @@ -41,6 +41,7 @@ jobs: env: SIRE_DONT_PHONEHOME: 1 SIRE_SILENT_PHONEHOME: 1 + SIRE_EMLE: 1 REPO: "${{ github.event.pull_request.head.repo.full_name || github.repository }}" steps: # diff --git a/.github/workflows/pr.yaml b/.github/workflows/pr.yaml index a49c5cc71..4b4dd4a45 100644 --- a/.github/workflows/pr.yaml +++ b/.github/workflows/pr.yaml @@ -43,6 +43,7 @@ jobs: env: SIRE_DONT_PHONEHOME: 1 SIRE_SILENT_PHONEHOME: 1 + SIRE_EMLE: 1 REPO: "${{ github.event.pull_request.head.repo.full_name || github.repository }}" steps: # diff --git a/actions/update_recipe.py b/actions/update_recipe.py index b21922146..6db706895 100644 --- a/actions/update_recipe.py +++ b/actions/update_recipe.py @@ -9,6 +9,9 @@ from parse_requirements import parse_requirements +# Check whether we are building a sire-emle package. +is_emle = os.environ.get("SIRE_EMLE", "False") + # has the user supplied an environment.yml file? if len(sys.argv) > 1: from pathlib import Path @@ -46,6 +49,11 @@ print(run_reqs) bss_reqs = parse_requirements(os.path.join(srcdir, "requirements_bss.txt")) print(bss_reqs) +if is_emle: + emle_reqs = parse_requirements(os.path.join(srcdir, "requirements_emle.txt")) + print(emle_reqs) +else: + emle_reqs = [] test_reqs = parse_requirements(os.path.join(srcdir, "requirements_test.txt")) @@ -222,6 +230,7 @@ def check_reqs(reqs0, reqs1): build_reqs = dep_lines(check_reqs(build_reqs, env_reqs)) host_reqs = combine(host_reqs, bss_reqs) +host_reqs = combine(host_reqs, emle_reqs) host_reqs = dep_lines(combine(host_reqs, env_reqs)) run_reqs = dep_lines(check_reqs(run_reqs, env_reqs)) test_reqs = dep_lines(check_reqs(test_reqs, env_reqs)) diff --git a/corelib/src/libs/SireIO/amber.cpp b/corelib/src/libs/SireIO/amber.cpp index 4f6c824e4..f42ffe5d2 100644 --- a/corelib/src/libs/SireIO/amber.cpp +++ b/corelib/src/libs/SireIO/amber.cpp @@ -2151,7 +2151,7 @@ tuple Amber::readCrdTop(const QString &crdfile, const Q // Now the box information SpacePtr spce; - if (pointers[IFBOX] == 1) + if (pointers[IFBOX] == 1 or pointers[IFBOX] == 2 or pointers[IFBOX] == 3) { /** Rectangular box, dimensions read from the crd file */ Vector dimensions(crd_box[0], crd_box[1], crd_box[2]); @@ -2173,12 +2173,6 @@ tuple Amber::readCrdTop(const QString &crdfile, const Q // spce = PeriodicBox( Vector ( crdBox[0], crdBox[1], crdBox[2] ) ).asA() ; // qDebug() << " periodic box " << spce.toString() ; } - else if (pointers[IFBOX] == 2) - { - /** Truncated Octahedral box*/ - throw SireError::incompatible_error(QObject::tr("Sire does not yet support a truncated octahedral box"), - CODELOC); - } else { /** Default is a non periodic system */ diff --git a/corelib/src/libs/SireIO/amberprm.cpp b/corelib/src/libs/SireIO/amberprm.cpp index e3a1071d2..21a926d63 100644 --- a/corelib/src/libs/SireIO/amberprm.cpp +++ b/corelib/src/libs/SireIO/amberprm.cpp @@ -2181,7 +2181,16 @@ QStringList toLines(const QVector ¶ms, const Space &space, int if (has_periodic_box) { - pointers[27] = 1; + // Orthorhombic box. + if (space.isA()) + { + pointers[27] = 1; + } + // General triclinic box. + else if (space.isA()) + { + pointers[27] = 3; + } } // here is the number of solvent molecules, and the index of the last diff --git a/corelib/src/libs/SireIO/biosimspace.cpp b/corelib/src/libs/SireIO/biosimspace.cpp index 94a9a74cf..0eb4ade06 100644 --- a/corelib/src/libs/SireIO/biosimspace.cpp +++ b/corelib/src/libs/SireIO/biosimspace.cpp @@ -1589,6 +1589,68 @@ namespace SireIO return retval; } + Molecule createSodiumIon(const Vector &coords, const QString model, const PropertyMap &map) + { + // Strip all whitespace from the model name and convert to upper case. + auto _model = model.simplified().replace(" ", "").toUpper(); + + // Create a hash between the allowed model names and their templace files. + QHash models; + models["TIP3P"] = getShareDir() + "/templates/ions/na_tip3p"; + models["TIP4P"] = getShareDir() + "/templates/ions/na_tip4p"; + + // Make sure the user has passed a valid water model. + if (not models.contains(_model)) + { + throw SireError::incompatible_error(QObject::tr("Unsupported AMBER ion model '%1'").arg(model), CODELOC); + } + + // Extract the water model template path. + auto path = models[_model]; + + // Load the ion template. + auto ion_template = MoleculeParser::read(path + ".prm7", map); + + // Extract the ion the template. + auto ion = ion_template[MolIdx(0)].molecule(); + + // Set the coordinates of the ion. + ion = ion.edit().atom(AtomIdx(0)).setProperty(map["coordinates"], coords).molecule().commit(); + + return ion; + } + + Molecule createChlorineIon(const Vector &coords, const QString model, const PropertyMap &map) + { + // Strip all whitespace from the model name and convert to upper case. + auto _model = model.simplified().replace(" ", "").toUpper(); + + // Create a hash between the allowed model names and their templace files. + QHash models; + models["TIP3P"] = getShareDir() + "/templates/ions/cl_tip3p"; + models["TIP4P"] = getShareDir() + "/templates/ions/cl_tip4p"; + + // Make sure the user has passed a valid water model. + if (not models.contains(_model)) + { + throw SireError::incompatible_error(QObject::tr("Unsupported AMBER ion model '%1'").arg(model), CODELOC); + } + + // Extract the water model template path. + auto path = models[_model]; + + // Load the ion template. + auto ion_template = MoleculeParser::read(path + ".prm7"); + + // Extract the ion the template. + auto ion = ion_template[MolIdx(0)].molecule(); + + // Set the coordinates of the ion. + ion = ion.edit().atom(AtomIdx(0)).setProperty(map["coordinates"], coords).molecule().commit(); + + return ion; + } + Vector cross(const Vector &v0, const Vector &v1) { double nx = v0.y() * v1.z() - v0.z() * v1.y(); diff --git a/corelib/src/libs/SireIO/biosimspace.h b/corelib/src/libs/SireIO/biosimspace.h index 5f7541c5a..b90c6db8a 100644 --- a/corelib/src/libs/SireIO/biosimspace.h +++ b/corelib/src/libs/SireIO/biosimspace.h @@ -319,6 +319,38 @@ namespace SireIO const QHash &molecule_mapping, const bool is_lambda1 = false, const PropertyMap &map0 = PropertyMap(), const PropertyMap &map1 = PropertyMap()); + //! Create a sodium ion at the specified position. + /*! \param position + The position of the sodium ion. + + \param model + The name of the water model. + + \param map + A dictionary of user-defined molecular property names. + + \retval sodium + The sodium ion. + */ + SIREIO_EXPORT Molecule createSodiumIon( + const Vector &coords, const QString model, const PropertyMap &map = PropertyMap()); + + //! Create a chlorine ion at the specified position. + /*! \param position + The position of the chlorine ion. + + \param model + The name of the water model. + + \param map + A dictionary of user-defined molecular property names. + + \retval chlorine + The chlorine ion. + */ + SIREIO_EXPORT Molecule createChlorineIon( + const Vector &coords, const QString model, const PropertyMap &map = PropertyMap()); + Vector cross(const Vector &v0, const Vector &v1); } // namespace SireIO @@ -332,6 +364,8 @@ SIRE_EXPOSE_FUNCTION(SireIO::setAmberWater) SIRE_EXPOSE_FUNCTION(SireIO::setGromacsWater) SIRE_EXPOSE_FUNCTION(SireIO::updateAndPreserveOrder) SIRE_EXPOSE_FUNCTION(SireIO::updateCoordinatesAndVelocities) +SIRE_EXPOSE_FUNCTION(SireIO::createSodiumIon) +SIRE_EXPOSE_FUNCTION(SireIO::createChlorineIon) SIRE_END_HEADER diff --git a/corelib/src/libs/SireIO/grotop.cpp b/corelib/src/libs/SireIO/grotop.cpp index 43c169a15..87908ea6b 100644 --- a/corelib/src/libs/SireIO/grotop.cpp +++ b/corelib/src/libs/SireIO/grotop.cpp @@ -2869,6 +2869,7 @@ static QStringList writeAtomTypes(QMap, GroMolType> &moltyps QString particle_type = "A"; // A is for Atom + // This is a dummy atom. if (elem.nProtons() == 0 and lj.isDummy()) { if (is_perturbable) @@ -2877,6 +2878,9 @@ static QStringList writeAtomTypes(QMap, GroMolType> &moltyps // Only label dummies for regular simulations. else if (not was_perturbable) particle_type = "D"; + + // Flag that we need to update the atoms. + update_atoms0 = true; } // This is a new atom type. @@ -2893,6 +2897,15 @@ static QStringList writeAtomTypes(QMap, GroMolType> &moltyps // Hash the atom type against its parameter string, minus the type. param_hash.insert(atomtypes[atomtype].mid(6), atomtype); + + if (update_atoms0) + { + // Set the type. + atom.setAtomType(atomtype); + + // Update the atoms in the vector. + atoms[i] = atom; + } } // This type has been seen before. else @@ -2977,6 +2990,17 @@ static QStringList writeAtomTypes(QMap, GroMolType> &moltyps update_atoms0 = true; } } + else + { + if (update_atoms0) + { + // Set the type. + atom.setAtomType(atomtype); + + // Update the atoms in the vector. + atoms[i] = atom; + } + } } } @@ -3019,9 +3043,15 @@ static QStringList writeAtomTypes(QMap, GroMolType> &moltyps QString particle_type = "A"; // A is for Atom + // This is a dummy atom. if (elem.nProtons() == 0 and lj.isDummy()) + { atomtype += "_du"; + // Flag that we need to update the atoms. + update_atoms1 = true; + } + // This is a new atom type. if (not atomtypes.contains(atomtype)) { @@ -3036,6 +3066,15 @@ static QStringList writeAtomTypes(QMap, GroMolType> &moltyps // Hash the atom type against its parameter string, minus the type. param_hash.insert(atomtypes[atomtype].mid(6), atomtype); + + if (update_atoms1) + { + // Set the type. + atom.setAtomType(atomtype); + + // Update the atoms in the vector. + atoms[i] = atom; + } } // This type has been seen before. @@ -3121,6 +3160,17 @@ static QStringList writeAtomTypes(QMap, GroMolType> &moltyps update_atoms1 = true; } } + else + { + if (update_atoms1) + { + // Set the type. + atom.setAtomType(atomtype); + + // Update the atoms in the vector. + atoms[i] = atom; + } + } } } @@ -3217,14 +3267,6 @@ static QStringList writeMolType(const QString &name, const GroMolType &moltype, elem1 = Element::elementWithMass(mol.property("mass1").asA()[cgatomidx]); } - // Update the atom types. - - if (elem0.nProtons() == 0) - atomtype0 += "_du"; - - if (elem1.nProtons() == 0) - atomtype1 += "_du"; - QString resnum = QString::number(atom0.residueNumber().value()); if (not atom0.chainName().isNull()) diff --git a/corelib/src/libs/SireMol/atommapping.cpp b/corelib/src/libs/SireMol/atommapping.cpp index c38616017..5722d1de8 100644 --- a/corelib/src/libs/SireMol/atommapping.cpp +++ b/corelib/src/libs/SireMol/atommapping.cpp @@ -31,6 +31,7 @@ #include "SireMaths/align.h" #include "SireMol/core.h" +#include "SireMol/moleditor.h" #include "SireStream/datastream.h" #include "SireStream/shareddatastream.h" @@ -723,20 +724,40 @@ AtomMapping AtomMapping::alignTo0() const QVector coords0 = this->atms0.property(map0["coordinates"]).toVector(); QVector coords1 = this->atms1.property(map1["coordinates"]).toVector(); - // calculate the transform to do a RMSD aligment of the two sets of coordinates - auto transform = SireMaths::getAlignment(coords0, coords1, true); - - auto mols1 = this->orig_atms1.molecules(); - AtomMapping ret(*this); - for (int i = 0; i < mols1.count(); ++i) + if ((this->count() == 1) and ((coords0.count() == 1) or (coords1.count() == 1))) { - auto mol = mols1[i].move().transform(transform, map1).commit(); + // if we've only mapped a single atom and one molecule is a monatomic ion, + // then simply replace the coordinates of the mapped atom. + + auto atom0 = this->atms0[0]; + auto atom1 = this->atms1[0]; + + auto mol = this->orig_atms1.molecules()[0]; + + mol = mol.edit().atom(atom1.index()) + .setProperty(map1["coordinates"].source(), coords0[0]) + .molecule().commit(); ret.atms1.update(mol); ret.orig_atms1.update(mol); } + else + { + // calculate the transform to do a RMSD aligment of the two sets of coordinates + auto transform = SireMaths::getAlignment(coords0, coords1, true); + + auto mols1 = this->orig_atms1.molecules(); + + for (int i = 0; i < mols1.count(); ++i) + { + auto mol = mols1[i].move().transform(transform, map1).commit(); + + ret.atms1.update(mol); + ret.orig_atms1.update(mol); + } + } return ret; } @@ -754,20 +775,40 @@ AtomMapping AtomMapping::alignTo1() const QVector coords0 = this->atms0.property(map0["coordinates"]).toVector(); QVector coords1 = this->atms1.property(map1["coordinates"]).toVector(); - // calculate the transform to do a RMSD aligment of the two sets of coordinates - auto transform = SireMaths::getAlignment(coords1, coords0, true); - - auto mols0 = this->orig_atms0.molecules(); - AtomMapping ret(*this); - for (int i = 0; i < mols0.count(); ++i) + if ((this->count() == 1) and ((coords0.count() == 1) or (coords1.count() == 1))) { - auto mol = mols0[i].move().transform(transform, map0).commit(); + // if we've only mapped a single atom and one molecule is a monatomic ion, + // then simply replace the coordinates of the mapped atom. + + auto atom0 = this->atms0[0]; + auto atom1 = this->atms1[0]; + + auto mol = this->orig_atms0.molecules()[0]; + + mol = mol.edit().atom(atom0.index()) + .setProperty(map0["coordinates"].source(), coords0[0]) + .molecule().commit(); ret.atms0.update(mol); ret.orig_atms0.update(mol); } + else + { + // calculate the transform to do a RMSD aligment of the two sets of coordinates + auto transform = SireMaths::getAlignment(coords1, coords0, true); + + auto mols0 = this->orig_atms0.molecules(); + + for (int i = 0; i < mols0.count(); ++i) + { + auto mol = mols0[i].move().transform(transform, map0).commit(); + + ret.atms0.update(mol); + ret.orig_atms0.update(mol); + } + } return ret; } diff --git a/corelib/src/libs/SireMol/selectorm.hpp b/corelib/src/libs/SireMol/selectorm.hpp index 7852e66ed..94eeb6ecf 100644 --- a/corelib/src/libs/SireMol/selectorm.hpp +++ b/corelib/src/libs/SireMol/selectorm.hpp @@ -331,6 +331,9 @@ namespace SireMol template QList metadata(const PropertyName &key, const PropertyName &metakey) const; + QList> toSelectorList() const; + QVector> toSelectorVector() const; + protected: void _append(const T &view); void _append(const Selector &views); @@ -2866,6 +2869,24 @@ namespace SireMol return ret; } + /** Return a QList containing all of the underlying Selector + * objects that make up this container + */ + template + SIRE_OUTOFLINE_TEMPLATE QList> SelectorM::toSelectorList() const + { + return vws; + } + + /** Return a QVector containing all of the underlying Selector + * objects that make up this container + */ + template + SIRE_OUTOFLINE_TEMPLATE QVector> SelectorM::toSelectorVector() const + { + return vws.toVector(); + } + template SIRE_OUTOFLINE_TEMPLATE bool SelectorM::isSingleMolecule() const { diff --git a/corelib/src/libs/SireMove/openmmpmefep.cpp b/corelib/src/libs/SireMove/openmmpmefep.cpp index 6c2c8096d..1332bf8ae 100644 --- a/corelib/src/libs/SireMove/openmmpmefep.cpp +++ b/corelib/src/libs/SireMove/openmmpmefep.cpp @@ -836,15 +836,80 @@ void OpenMMPMEFEP::initialise(bool fullPME) qDebug() << "\n\nRestraint is ON\n\n"; } + /************************************RECEPTOR-lIGAND RESTRAINTS**************************/ + // Check if we are in turn on receptor-ligand restraint mode + + bool turn_on_restraints_mode{false}; + + for (int i = 0; i < nmols; i++) + { + Molecule molecule = moleculegroup.moleculeAt(i).molecule(); + + if (molecule.hasProperty("turn_on_restraints_mode")) + { + turn_on_restraints_mode = true; // Lambda will be used to turn on the receptor-ligand restraints + if (Debug) + qDebug() << "Lambda will be used to turn on the receptor-ligand restraints"; + break; // We've found the solute - exit loop over molecules in system. + } + } + /*** BOND LINK FORCE FIELD ***/ - /* NOTE: CustomBondForce does not (OpenMM 6.2) apply PBC checks so code will be buggy if - restraints involve one atom that diffuses out of the box. */ + /* FC 12/21 CustomBondForce now (OpenMM 7.4.0) allows application of PBC checks*/ - auto custom_link_bond = new OpenMM::CustomBondForce("kl * max(0, d - dl*dl);" - "d = (r-reql) * (r-reql)"); + OpenMM::CustomBondForce * custom_link_bond = new OpenMM::CustomBondForce("delta(min(0, r_eff))*(lamrest^5)*kl*r_eff^2;" + "r_eff=abs(r-reql)-dl"); custom_link_bond->addPerBondParameter("reql"); custom_link_bond->addPerBondParameter("kl"); custom_link_bond->addPerBondParameter("dl"); + custom_link_bond->setUsesPeriodicBoundaryConditions(true); + // If in turn on receptor-ligand restraints mode, default value of lamrest needs to be lambda, because + // the default value is used for the first nrg_freq timesteps before being set by updateOpenMMContextLambda + if (turn_on_restraints_mode) + custom_link_bond->addGlobalParameter("lamrest", current_lambda); + // We are not in turn on receptor-ligand restraints mode - set lamrest to 1 + else + custom_link_bond->addGlobalParameter("lamrest", 1); + + /****************************************BORESCH DISTANCE POTENTIAL*****************************/ + + OpenMM::CustomBondForce *custom_boresch_dist_rest = + new OpenMM::CustomBondForce("lamrest*force_const*(r-equil_val)^2"); + custom_boresch_dist_rest->addPerBondParameter("force_const"); + custom_boresch_dist_rest->addPerBondParameter("equil_val"); + custom_boresch_dist_rest->setUsesPeriodicBoundaryConditions(true); + if (turn_on_restraints_mode) + custom_boresch_dist_rest->addGlobalParameter("lamrest", current_lambda); + // We are not in turn on receptor-ligand restraints mode - set lamrest to 1 + else + custom_boresch_dist_rest->addGlobalParameter("lamrest", 1); + + /****************************************BORESCH ANGLE POTENTIAL*****************************/ + + OpenMM::CustomAngleForce *custom_boresch_angle_rest = + new OpenMM::CustomAngleForce("lamrest*force_const*(theta-equil_val)^2"); + custom_boresch_angle_rest->addPerAngleParameter("force_const"); + custom_boresch_angle_rest->addPerAngleParameter("equil_val"); + custom_boresch_angle_rest->setUsesPeriodicBoundaryConditions(true); + if (turn_on_restraints_mode) + custom_boresch_angle_rest->addGlobalParameter("lamrest", current_lambda); + // We are not in turn on receptor-ligand restraints mode - set lamrest to 1 + else + custom_boresch_angle_rest->addGlobalParameter("lamrest", 1); + + /****************************************BORESCH DIHEDRAL POTENTIAL*****************************/ + + OpenMM::CustomTorsionForce *custom_boresch_dihedral_rest = + new OpenMM::CustomTorsionForce("lamrest*force_const*min(dtheta, 2*pi-dtheta)^2;" + "dtheta = abs(theta-equil_val); pi = 3.1415926535"); + custom_boresch_dihedral_rest->addPerTorsionParameter("force_const"); + custom_boresch_dihedral_rest->addPerTorsionParameter("equil_val"); + custom_boresch_dihedral_rest->setUsesPeriodicBoundaryConditions(true); + if (turn_on_restraints_mode) + custom_boresch_dihedral_rest->addGlobalParameter("lamrest", current_lambda); + // We are not in turn on receptor-ligand restraints mode - set lamrest to 1 + else + custom_boresch_dihedral_rest->addGlobalParameter("lamrest", 1); /*** BUILD OpenMM SYSTEM ***/ @@ -970,7 +1035,10 @@ void OpenMMPMEFEP::initialise(bool fullPME) // Molecule solutemol = solute.moleculeAt(0).molecule(); int nions = 0; - QVector perturbed_energies_tmp{false, false, false, false, false, false, false, false, false}; + QVector perturbed_energies_tmp(10); + + for (int i = 0; i < perturbed_energies_tmp.size(); i++) + perturbed_energies_tmp[i] = false; // the default AMBER 1-4 scaling factors double const Coulomb14Scale = 1.0 / 1.2; @@ -1853,7 +1921,7 @@ void OpenMMPMEFEP::initialise(bool fullPME) dihedral_pert_list.append(DihedralID(four.atom0(), four.atom1(), four.atom2(), four.atom3())); dihedral_pert_swap_list.append( - DihedralID(four.atom3(), four.atom1(), four.atom2(), four.atom0())); + DihedralID(four.atom3(), four.atom2(), four.atom1(), four.atom0())); improper_pert_list.append(ImproperID(four.atom0(), four.atom1(), four.atom2(), four.atom3())); improper_pert_swap_list.append( @@ -2501,6 +2569,14 @@ void OpenMMPMEFEP::initialise(bool fullPME) } } // if (!fullPME) + + if (turn_on_restraints_mode) + { + perturbed_energies_tmp[9] = true; //Lambda will be used to turn on the receptor-ligand restraints + if (Debug) + qDebug() << "Added Perturbed Receptor-Ligand Restraint energy term"; + } + perturbed_energies = perturbed_energies_tmp; // IMPORTANT: PERTURBED ENERGY TORSIONS ARE ADDED ABOVE @@ -2556,6 +2632,174 @@ void OpenMMPMEFEP::initialise(bool fullPME) } // end of bond link flag + bool UseBoresch_flag = true; + + // Boresch Restraints. All the information is stored in the solute only. + + if (UseBoresch_flag == true) + { + bool found_solute{false}; + for (int i = 0; i < nmols; i++) + { + Molecule molecule = moleculegroup.moleculeAt(i).molecule(); + + bool has_boresch_dist = molecule.hasProperty("boresch_dist_restraint"); + bool has_boresch_angle = molecule.hasProperty("boresch_angle_restraints"); + bool has_boresch_dihedral = molecule.hasProperty("boresch_dihedral_restraints"); + + if (has_boresch_dist) + { + found_solute = true; // We have found the solute, but before breaking we must also check + // if there are Boresch angle and torsion restraints. + + if (Debug) + { + qDebug() << "Boresch distance restraint properties stored = true"; + qDebug() << "Boresch angle restraint properties stored = " << has_boresch_angle; + qDebug() << "Boresch dihedral restraint properties stored = " << has_boresch_dihedral; + } + + std::vector custom_boresch_dist_par(2); + + const auto boresch_dist_prop = molecule.property("boresch_dist_restraint").asA(); + + const auto atomnum0 = boresch_dist_prop.property(QString("AtomNum0")).asA().toInt(); + const auto atomnum1 = boresch_dist_prop.property(QString("AtomNum1")).asA().toInt(); + const auto force_const = + boresch_dist_prop.property(QString("force_const")).asA().toDouble(); + const auto equil_val = + boresch_dist_prop.property(QString("equil_val")).asA().toDouble(); + + const auto openmmindex0 = AtomNumToOpenMMIndex[atomnum0]; + const auto openmmindex1 = AtomNumToOpenMMIndex[atomnum1]; + + custom_boresch_dist_par[0] = + force_const * (OpenMM::KJPerKcal * OpenMM::AngstromsPerNm * OpenMM::AngstromsPerNm); // force_const + custom_boresch_dist_par[1] = equil_val * OpenMM::NmPerAngstrom; // equil_val + + if (Debug) + { + qDebug() << "Boresch distance restraint implemented"; + qDebug() << "atomnum0 = " << atomnum0 << " openmmindex0 =" << openmmindex0; + qDebug() << "atomnum1 = " << atomnum1 << " openmmindex1 =" << openmmindex1; + qDebug() << "force_const = " << force_const << " equil_val = " << equil_val; + } + + custom_boresch_dist_rest->addBond(openmmindex0, openmmindex1, custom_boresch_dist_par); + + system_openmm->addForce(custom_boresch_dist_rest); + } + + if (has_boresch_angle) + { + std::vector custom_boresch_angle_par(2); + + const auto boresch_angle_prop = molecule.property("boresch_angle_restraints").asA(); + + const auto n_angles = + boresch_angle_prop.property(QString("n_boresch_angle_restraints")).asA().toInt(); + + if (Debug) + qDebug() << "Number of Boresch angle restraints = " << n_angles; + + for (int i = 0; i < n_angles; i++) + { + const auto atomnum0 = + boresch_angle_prop.property(QString("AtomNum0-%1").arg(i)).asA().toInt(); + const auto atomnum1 = + boresch_angle_prop.property(QString("AtomNum1-%1").arg(i)).asA().toInt(); + const auto atomnum2 = + boresch_angle_prop.property(QString("AtomNum2-%1").arg(i)).asA().toInt(); + const auto force_const = + boresch_angle_prop.property(QString("force_const-%1").arg(i)).asA().toDouble(); + const auto equil_val = + boresch_angle_prop.property(QString("equil_val-%1").arg(i)).asA().toDouble(); + + const auto openmmindex0 = AtomNumToOpenMMIndex[atomnum0]; + const auto openmmindex1 = AtomNumToOpenMMIndex[atomnum1]; + const auto openmmindex2 = AtomNumToOpenMMIndex[atomnum2]; + + custom_boresch_angle_par[0] = force_const * (OpenMM::KJPerKcal); // force_const + custom_boresch_angle_par[1] = equil_val; // equil_val + + if (Debug) + { + qDebug() << "atomnum0 = " << atomnum0 << " openmmindex0 =" << openmmindex0; + qDebug() << "atomnum1 = " << atomnum1 << " openmmindex1 =" << openmmindex1; + qDebug() << "atomnum2 = " << atomnum2 << " openmmindex2 =" << openmmindex2; + qDebug() << "force_const = " << force_const << " equil_val = " << equil_val; + } + + custom_boresch_angle_rest->addAngle(openmmindex0, openmmindex1, openmmindex2, + custom_boresch_angle_par); + } + + system_openmm->addForce(custom_boresch_angle_rest); + } + + if (has_boresch_dihedral) + { + std::vector custom_boresch_dihedral_par(2); + + const auto boresch_dihedral_prop = molecule.property("boresch_dihedral_restraints").asA(); + + const auto n_dihedrals = boresch_dihedral_prop.property(QString("n_boresch_dihedral_restraints")) + .asA() + .toInt(); + + if (Debug) + qDebug() << "Number of Boresch dihedral restraints = " << n_dihedrals; + + for (int i = 0; i < n_dihedrals; i++) + { + const auto atomnum0 = + boresch_dihedral_prop.property(QString("AtomNum0-%1").arg(i)).asA().toInt(); + const auto atomnum1 = + boresch_dihedral_prop.property(QString("AtomNum1-%1").arg(i)).asA().toInt(); + const auto atomnum2 = + boresch_dihedral_prop.property(QString("AtomNum2-%1").arg(i)).asA().toInt(); + const auto atomnum3 = + boresch_dihedral_prop.property(QString("AtomNum3-%1").arg(i)).asA().toInt(); + const auto force_const = boresch_dihedral_prop.property(QString("force_const-%1").arg(i)) + .asA() + .toDouble(); + const auto equil_val = boresch_dihedral_prop.property(QString("equil_val-%1").arg(i)) + .asA() + .toDouble(); + + const auto openmmindex0 = AtomNumToOpenMMIndex[atomnum0]; + const auto openmmindex1 = AtomNumToOpenMMIndex[atomnum1]; + const auto openmmindex2 = AtomNumToOpenMMIndex[atomnum2]; + const auto openmmindex3 = AtomNumToOpenMMIndex[atomnum3]; + + custom_boresch_dihedral_par[0] = force_const * (OpenMM::KJPerKcal); // force_const + custom_boresch_dihedral_par[1] = equil_val; // equil_val + + if (Debug) + { + qDebug() << "atomnum0 = " << atomnum0 << " openmmindex0 =" << openmmindex0; + qDebug() << "atomnum1 = " << atomnum1 << " openmmindex1 =" << openmmindex1; + qDebug() << "atomnum2 = " << atomnum2 << " openmmindex2 =" << openmmindex2; + qDebug() << "atomnum3 = " << atomnum3 << " openmmindex3 =" << openmmindex3; + qDebug() << "force_const = " << force_const << " equil_val = " << equil_val; + } + + custom_boresch_dihedral_rest->addTorsion(openmmindex0, openmmindex1, openmmindex2, openmmindex3, + custom_boresch_dihedral_par); + } + + system_openmm->addForce(custom_boresch_dihedral_rest); + } + + if (found_solute) + break; // We've found the molecule, exit the outer loop. If a molecule has Boresch + // distance restraints it must be the solute, but we cannot break immediately + // because it may also have angle/ dihedral restraints + + } // End of loop over molecules in system + + } // End of Boresch flag + this->openmm_system = system_openmm; this->isSystemInitialised = true; } // OpenMMPMEFEP::initialise END @@ -3414,6 +3658,10 @@ void OpenMMPMEFEP::updateOpenMMContextLambda(double lambda) if (perturbed_energies[7]) openmm_context->setParameter("lamdih", lambda); // Torsions + // RECEPTOR-LIGAND RESTRAINTS + if (perturbed_energies[9]) + openmm_context->setParameter("lamrest", lambda); //Receptor-ligand restraints + // lambda for the offsets (linear scaling) of the charges in // reciprocal space openmm_context->setParameter("lambda_offset", lambda); diff --git a/corelib/templates/ions/cl_tip3p.prm7 b/corelib/templates/ions/cl_tip3p.prm7 new file mode 100644 index 000000000..ac016b750 --- /dev/null +++ b/corelib/templates/ions/cl_tip3p.prm7 @@ -0,0 +1,130 @@ +%VERSION VERSION_STAMP = V0001.000 DATE = 07/10/24 14:04:15 +%FLAG TITLE +%FORMAT(20a4) + +%FLAG POINTERS +%FORMAT(10I8) + 1 1 0 0 0 0 0 0 0 0 + 1 1 0 0 0 0 0 0 1 0 + 0 0 0 0 0 0 0 0 1 0 + 0 0 0 +%FLAG ATOM_NAME +%FORMAT(20a4) +Cl- +%FLAG CHARGE +%FORMAT(5E16.8) + -1.82223000E+01 +%FLAG ATOMIC_NUMBER +%FORMAT(10I8) + 17 +%FLAG MASS +%FORMAT(5E16.8) + 3.54500000E+01 +%FLAG ATOM_TYPE_INDEX +%FORMAT(10I8) + 1 +%FLAG NUMBER_EXCLUDED_ATOMS +%FORMAT(10I8) + 1 +%FLAG NONBONDED_PARM_INDEX +%FORMAT(10I8) + 1 +%FLAG RESIDUE_LABEL +%FORMAT(20a4) +Cl- +%FLAG RESIDUE_POINTER +%FORMAT(10I8) + 1 +%FLAG BOND_FORCE_CONSTANT +%FORMAT(5E16.8) + +%FLAG BOND_EQUIL_VALUE +%FORMAT(5E16.8) + +%FLAG ANGLE_FORCE_CONSTANT +%FORMAT(5E16.8) + +%FLAG ANGLE_EQUIL_VALUE +%FORMAT(5E16.8) + +%FLAG DIHEDRAL_FORCE_CONSTANT +%FORMAT(5E16.8) + +%FLAG DIHEDRAL_PERIODICITY +%FORMAT(5E16.8) + +%FLAG DIHEDRAL_PHASE +%FORMAT(5E16.8) + +%FLAG SCEE_SCALE_FACTOR +%FORMAT(5E16.8) + +%FLAG SCNB_SCALE_FACTOR +%FORMAT(5E16.8) + +%FLAG SOLTY +%FORMAT(5E16.8) + 0.00000000E+00 +%FLAG LENNARD_JONES_ACOEF +%FORMAT(5E16.8) + 9.24719470E+06 +%FLAG LENNARD_JONES_BCOEF +%FORMAT(5E16.8) + 1.14737423E+03 +%FLAG BONDS_INC_HYDROGEN +%FORMAT(10I8) + +%FLAG BONDS_WITHOUT_HYDROGEN +%FORMAT(10I8) + +%FLAG ANGLES_INC_HYDROGEN +%FORMAT(10I8) + +%FLAG ANGLES_WITHOUT_HYDROGEN +%FORMAT(10I8) + +%FLAG DIHEDRALS_INC_HYDROGEN +%FORMAT(10I8) + +%FLAG DIHEDRALS_WITHOUT_HYDROGEN +%FORMAT(10I8) + +%FLAG EXCLUDED_ATOMS_LIST +%FORMAT(10I8) + 0 +%FLAG HBOND_ACOEF +%FORMAT(5E16.8) + +%FLAG HBOND_BCOEF +%FORMAT(5E16.8) + +%FLAG HBCUT +%FORMAT(5E16.8) + +%FLAG AMBER_ATOM_TYPE +%FORMAT(20a4) +Cl- +%FLAG TREE_CHAIN_CLASSIFICATION +%FORMAT(20a4) +M +%FLAG JOIN_ARRAY +%FORMAT(10I8) + 0 +%FLAG IROTAT +%FORMAT(10I8) + 0 +%FLAG RADIUS_SET +%FORMAT(1a80) +modified Bondi radii (mbondi) +%FLAG RADII +%FORMAT(5E16.8) + 1.70000000E+00 +%FLAG SCREEN +%FORMAT(5E16.8) + 8.00000000E-01 +%FLAG ATOMS_PER_MOLECULE +%FORMAT(10I8) + 1 +%FLAG IPOL +%FORMAT(1I8) + 0 diff --git a/corelib/templates/ions/cl_tip4p.prm7 b/corelib/templates/ions/cl_tip4p.prm7 new file mode 100644 index 000000000..2a91cfd7d --- /dev/null +++ b/corelib/templates/ions/cl_tip4p.prm7 @@ -0,0 +1,130 @@ +%VERSION VERSION_STAMP = V0001.000 DATE = 07/10/24 14:04:34 +%FLAG TITLE +%FORMAT(20a4) + +%FLAG POINTERS +%FORMAT(10I8) + 1 1 0 0 0 0 0 0 0 0 + 1 1 0 0 0 0 0 0 1 0 + 0 0 0 0 0 0 0 0 1 0 + 0 0 0 +%FLAG ATOM_NAME +%FORMAT(20a4) +Cl- +%FLAG CHARGE +%FORMAT(5E16.8) + -1.82223000E+01 +%FLAG ATOMIC_NUMBER +%FORMAT(10I8) + 17 +%FLAG MASS +%FORMAT(5E16.8) + 3.54500000E+01 +%FLAG ATOM_TYPE_INDEX +%FORMAT(10I8) + 1 +%FLAG NUMBER_EXCLUDED_ATOMS +%FORMAT(10I8) + 1 +%FLAG NONBONDED_PARM_INDEX +%FORMAT(10I8) + 1 +%FLAG RESIDUE_LABEL +%FORMAT(20a4) +Cl- +%FLAG RESIDUE_POINTER +%FORMAT(10I8) + 1 +%FLAG BOND_FORCE_CONSTANT +%FORMAT(5E16.8) + +%FLAG BOND_EQUIL_VALUE +%FORMAT(5E16.8) + +%FLAG ANGLE_FORCE_CONSTANT +%FORMAT(5E16.8) + +%FLAG ANGLE_EQUIL_VALUE +%FORMAT(5E16.8) + +%FLAG DIHEDRAL_FORCE_CONSTANT +%FORMAT(5E16.8) + +%FLAG DIHEDRAL_PERIODICITY +%FORMAT(5E16.8) + +%FLAG DIHEDRAL_PHASE +%FORMAT(5E16.8) + +%FLAG SCEE_SCALE_FACTOR +%FORMAT(5E16.8) + +%FLAG SCNB_SCALE_FACTOR +%FORMAT(5E16.8) + +%FLAG SOLTY +%FORMAT(5E16.8) + 0.00000000E+00 +%FLAG LENNARD_JONES_ACOEF +%FORMAT(5E16.8) + 9.33304478E+06 +%FLAG LENNARD_JONES_BCOEF +%FORMAT(5E16.8) + 6.59809978E+02 +%FLAG BONDS_INC_HYDROGEN +%FORMAT(10I8) + +%FLAG BONDS_WITHOUT_HYDROGEN +%FORMAT(10I8) + +%FLAG ANGLES_INC_HYDROGEN +%FORMAT(10I8) + +%FLAG ANGLES_WITHOUT_HYDROGEN +%FORMAT(10I8) + +%FLAG DIHEDRALS_INC_HYDROGEN +%FORMAT(10I8) + +%FLAG DIHEDRALS_WITHOUT_HYDROGEN +%FORMAT(10I8) + +%FLAG EXCLUDED_ATOMS_LIST +%FORMAT(10I8) + 0 +%FLAG HBOND_ACOEF +%FORMAT(5E16.8) + +%FLAG HBOND_BCOEF +%FORMAT(5E16.8) + +%FLAG HBCUT +%FORMAT(5E16.8) + +%FLAG AMBER_ATOM_TYPE +%FORMAT(20a4) +Cl- +%FLAG TREE_CHAIN_CLASSIFICATION +%FORMAT(20a4) +M +%FLAG JOIN_ARRAY +%FORMAT(10I8) + 0 +%FLAG IROTAT +%FORMAT(10I8) + 0 +%FLAG RADIUS_SET +%FORMAT(1a80) +modified Bondi radii (mbondi) +%FLAG RADII +%FORMAT(5E16.8) + 1.70000000E+00 +%FLAG SCREEN +%FORMAT(5E16.8) + 8.00000000E-01 +%FLAG ATOMS_PER_MOLECULE +%FORMAT(10I8) + 1 +%FLAG IPOL +%FORMAT(1I8) + 0 diff --git a/corelib/templates/ions/na_tip3p.prm7 b/corelib/templates/ions/na_tip3p.prm7 new file mode 100644 index 000000000..0ef0e27e4 --- /dev/null +++ b/corelib/templates/ions/na_tip3p.prm7 @@ -0,0 +1,130 @@ +%VERSION VERSION_STAMP = V0001.000 DATE = 07/10/24 14:04:59 +%FLAG TITLE +%FORMAT(20a4) + +%FLAG POINTERS +%FORMAT(10I8) + 1 1 0 0 0 0 0 0 0 0 + 1 1 0 0 0 0 0 0 1 0 + 0 0 0 0 0 0 0 0 1 0 + 0 0 0 +%FLAG ATOM_NAME +%FORMAT(20a4) +Na+ +%FLAG CHARGE +%FORMAT(5E16.8) + 1.82223000E+01 +%FLAG ATOMIC_NUMBER +%FORMAT(10I8) + 11 +%FLAG MASS +%FORMAT(5E16.8) + 2.29900000E+01 +%FLAG ATOM_TYPE_INDEX +%FORMAT(10I8) + 1 +%FLAG NUMBER_EXCLUDED_ATOMS +%FORMAT(10I8) + 1 +%FLAG NONBONDED_PARM_INDEX +%FORMAT(10I8) + 1 +%FLAG RESIDUE_LABEL +%FORMAT(20a4) +Na+ +%FLAG RESIDUE_POINTER +%FORMAT(10I8) + 1 +%FLAG BOND_FORCE_CONSTANT +%FORMAT(5E16.8) + +%FLAG BOND_EQUIL_VALUE +%FORMAT(5E16.8) + +%FLAG ANGLE_FORCE_CONSTANT +%FORMAT(5E16.8) + +%FLAG ANGLE_EQUIL_VALUE +%FORMAT(5E16.8) + +%FLAG DIHEDRAL_FORCE_CONSTANT +%FORMAT(5E16.8) + +%FLAG DIHEDRAL_PERIODICITY +%FORMAT(5E16.8) + +%FLAG DIHEDRAL_PHASE +%FORMAT(5E16.8) + +%FLAG SCEE_SCALE_FACTOR +%FORMAT(5E16.8) + +%FLAG SCNB_SCALE_FACTOR +%FORMAT(5E16.8) + +%FLAG SOLTY +%FORMAT(5E16.8) + 0.00000000E+00 +%FLAG LENNARD_JONES_ACOEF +%FORMAT(5E16.8) + 1.55205818E+04 +%FLAG LENNARD_JONES_BCOEF +%FORMAT(5E16.8) + 7.36779156E+01 +%FLAG BONDS_INC_HYDROGEN +%FORMAT(10I8) + +%FLAG BONDS_WITHOUT_HYDROGEN +%FORMAT(10I8) + +%FLAG ANGLES_INC_HYDROGEN +%FORMAT(10I8) + +%FLAG ANGLES_WITHOUT_HYDROGEN +%FORMAT(10I8) + +%FLAG DIHEDRALS_INC_HYDROGEN +%FORMAT(10I8) + +%FLAG DIHEDRALS_WITHOUT_HYDROGEN +%FORMAT(10I8) + +%FLAG EXCLUDED_ATOMS_LIST +%FORMAT(10I8) + 0 +%FLAG HBOND_ACOEF +%FORMAT(5E16.8) + +%FLAG HBOND_BCOEF +%FORMAT(5E16.8) + +%FLAG HBCUT +%FORMAT(5E16.8) + +%FLAG AMBER_ATOM_TYPE +%FORMAT(20a4) +Na+ +%FLAG TREE_CHAIN_CLASSIFICATION +%FORMAT(20a4) +M +%FLAG JOIN_ARRAY +%FORMAT(10I8) + 0 +%FLAG IROTAT +%FORMAT(10I8) + 0 +%FLAG RADIUS_SET +%FORMAT(1a80) +modified Bondi radii (mbondi) +%FLAG RADII +%FORMAT(5E16.8) + 1.50000000E+00 +%FLAG SCREEN +%FORMAT(5E16.8) + 8.00000000E-01 +%FLAG ATOMS_PER_MOLECULE +%FORMAT(10I8) + 1 +%FLAG IPOL +%FORMAT(1I8) + 0 diff --git a/corelib/templates/ions/na_tip4p.prm7 b/corelib/templates/ions/na_tip4p.prm7 new file mode 100644 index 000000000..543f65f71 --- /dev/null +++ b/corelib/templates/ions/na_tip4p.prm7 @@ -0,0 +1,130 @@ +%VERSION VERSION_STAMP = V0001.000 DATE = 07/10/24 14:04:42 +%FLAG TITLE +%FORMAT(20a4) + +%FLAG POINTERS +%FORMAT(10I8) + 1 1 0 0 0 0 0 0 0 0 + 1 1 0 0 0 0 0 0 1 0 + 0 0 0 0 0 0 0 0 1 0 + 0 0 0 +%FLAG ATOM_NAME +%FORMAT(20a4) +Na+ +%FLAG CHARGE +%FORMAT(5E16.8) + 1.82223000E+01 +%FLAG ATOMIC_NUMBER +%FORMAT(10I8) + 11 +%FLAG MASS +%FORMAT(5E16.8) + 2.29900000E+01 +%FLAG ATOM_TYPE_INDEX +%FORMAT(10I8) + 1 +%FLAG NUMBER_EXCLUDED_ATOMS +%FORMAT(10I8) + 1 +%FLAG NONBONDED_PARM_INDEX +%FORMAT(10I8) + 1 +%FLAG RESIDUE_LABEL +%FORMAT(20a4) +Na+ +%FLAG RESIDUE_POINTER +%FORMAT(10I8) + 1 +%FLAG BOND_FORCE_CONSTANT +%FORMAT(5E16.8) + +%FLAG BOND_EQUIL_VALUE +%FORMAT(5E16.8) + +%FLAG ANGLE_FORCE_CONSTANT +%FORMAT(5E16.8) + +%FLAG ANGLE_EQUIL_VALUE +%FORMAT(5E16.8) + +%FLAG DIHEDRAL_FORCE_CONSTANT +%FORMAT(5E16.8) + +%FLAG DIHEDRAL_PERIODICITY +%FORMAT(5E16.8) + +%FLAG DIHEDRAL_PHASE +%FORMAT(5E16.8) + +%FLAG SCEE_SCALE_FACTOR +%FORMAT(5E16.8) + +%FLAG SCNB_SCALE_FACTOR +%FORMAT(5E16.8) + +%FLAG SOLTY +%FORMAT(5E16.8) + 0.00000000E+00 +%FLAG LENNARD_JONES_ACOEF +%FORMAT(5E16.8) + 7.95580953E+03 +%FLAG LENNARD_JONES_BCOEF +%FORMAT(5E16.8) + 7.32135689E+01 +%FLAG BONDS_INC_HYDROGEN +%FORMAT(10I8) + +%FLAG BONDS_WITHOUT_HYDROGEN +%FORMAT(10I8) + +%FLAG ANGLES_INC_HYDROGEN +%FORMAT(10I8) + +%FLAG ANGLES_WITHOUT_HYDROGEN +%FORMAT(10I8) + +%FLAG DIHEDRALS_INC_HYDROGEN +%FORMAT(10I8) + +%FLAG DIHEDRALS_WITHOUT_HYDROGEN +%FORMAT(10I8) + +%FLAG EXCLUDED_ATOMS_LIST +%FORMAT(10I8) + 0 +%FLAG HBOND_ACOEF +%FORMAT(5E16.8) + +%FLAG HBOND_BCOEF +%FORMAT(5E16.8) + +%FLAG HBCUT +%FORMAT(5E16.8) + +%FLAG AMBER_ATOM_TYPE +%FORMAT(20a4) +Na+ +%FLAG TREE_CHAIN_CLASSIFICATION +%FORMAT(20a4) +M +%FLAG JOIN_ARRAY +%FORMAT(10I8) + 0 +%FLAG IROTAT +%FORMAT(10I8) + 0 +%FLAG RADIUS_SET +%FORMAT(1a80) +modified Bondi radii (mbondi) +%FLAG RADII +%FORMAT(5E16.8) + 1.50000000E+00 +%FLAG SCREEN +%FORMAT(5E16.8) + 8.00000000E-01 +%FLAG ATOMS_PER_MOLECULE +%FORMAT(10I8) + 1 +%FLAG IPOL +%FORMAT(1I8) + 0 diff --git a/doc/source/changelog.rst b/doc/source/changelog.rst index 006faeb46..8d01fad32 100644 --- a/doc/source/changelog.rst +++ b/doc/source/changelog.rst @@ -12,6 +12,83 @@ Development was migrated into the `OpenBioSim `__ organisation on `GitHub `__. +`2024.3.0 `__ - October 2024 +-------------------------------------------------------------------------------------------- + +* Print residue indices of perturbed water molecules to SOMD1 log to allow + for easier debugging. + +* Add support for creating Na+ and Cl- ions as a means of generating templates for + uses as alchemical ions. + +* Fix ``sire.morph.merge`` function when one molecule is a monatomic ion. This prevents + the attempted rigid-body alignment, which would fail due to there being too few + degrees of freedom. + +* Remove ``sire.move.OpenMMPMEFEP`` wrappers from build when OpenMM is not available. + +* Set ``IFBOX`` pointer to 3 for general triclinic boxes in ``sire.IO.AmberPrm`` parser. + +* Only exclude nonbonded interactions between ``from_ghost`` and ``to_ghost`` atoms + if they are in the same molecule. This prevents spurious intermolcular interactions + between molecules containing ghost atoms, e.g. a ligand and an alchemical water. + +* Add Docker support for building wrappers on Linux x86. + +* Port SOMD1 Boresch restraint implementation to PME code. (This feature was present + in the reaction field implementation, but not for PME.) + +* Port SOMD1 torsion fix to PME code. (This had been fixed for the reaction field implementation, + but not for PME.) + +* Fix issues with ``atomtype`` and ``atom`` records for dummy atoms in GROMACS topology files. + +* Fixed buffer overflow when computing molecule indices to excluded to/from + ghost atom interactions which caused corruption of the exclusion list. + +* Fixed calculation of ``delta^2`` in soft-core Couloumb potential. + +* Exclude to/from ghost atom interactions from the ``ghost_14ff``. Exclusions were + already added to the ``ghost_ghostff``, but not the ``ghost_14ff``. + +* Fixed description of soft-core alpha parameter in :doc:`tutorial `. + +* Added debugging function to evaluate custom forces in OpenMM XML files. This + allows a user to decompose the pair-wise contribtions to the custom OpenMM + forces created by :mod:`sire`. + +* Added a timeout to the OpenMM minimiser function. This gives the user a single tunable + parameter to control roughly how long a minimisation should last before being aborted. + +* Exposed missing pickle operator on the ``LambdaLever`` class. + +* Fix bug setting custom nonbonded parameters for ghost atoms used in + positional restraints in OpenMM. + +* Fix exchange probability equations in ``sire.morph.replica_exchange`` function. + +* Fix calculation of energy change following final constraint projection + after energy minimisation. Previously the energy change was calculated from + the final step of the minimisation, rather than the change in energy + following the application of the constraints. + +* Clear internal OpenMM state from dynamics object during minimisation, + preventing the previous, pre-minimisation, state from being used when + ``get_state()`` is called. + +* Add support for QM/MM simulations using OpenMM. This uses the recent ``CustomCPPForceImpl`` + introduced in OpenMM 8.1 to allow an interface between OpenMM and external + QM or ML codes. We support a generic Python callback interface and a ``Torch`` + based interface for ML models. This is documented in the new :doc:`tutorial `. + +* Reinitialise OpenMM context if constraints change when setting lambda. Updating + constraints in an OpenMM system does not update the associated data structures + in the context. A full reinitialiasation is required. + +* Give custom OpenMM forces meaningful names. This makes it easier to parse OpenMM + XML files and debug custom forces, particularly when multiple forces of the same + type are present. + `2024.2.0 `__ - June 2024 ----------------------------------------------------------------------------------------- diff --git a/doc/source/tutorial/index.rst b/doc/source/tutorial/index.rst index bcb473c25..befe1a2dd 100644 --- a/doc/source/tutorial/index.rst +++ b/doc/source/tutorial/index.rst @@ -28,3 +28,4 @@ please :doc:`ask for support. <../support>` index_part05 index_part06 index_part07 + index_part08 diff --git a/doc/source/tutorial/index_part08.rst b/doc/source/tutorial/index_part08.rst new file mode 100644 index 000000000..d2629dc51 --- /dev/null +++ b/doc/source/tutorial/index_part08.rst @@ -0,0 +1,21 @@ +=============== +Part 08 - QM/MM +=============== + +QM/MM is a method that combines the accuracy of quantum mechanics with the +speed of molecular mechanics. In QM/MM, a small region of the system is treated +at the quantum mechanical level, while the rest of the system is treated at the +molecular mechanical level. This allows us to perform accurate calculations on +the region of interest, while still being able to simulate the rest of the system +at a much lower computational cost. Due to the recent development of cheap and +accurate machine learning based QM models, there has been a resurgence of +interest in QM/MM methods. In this tutorial we will show how to perform +QM/MM simulations using ``sire``. + +.. toctree:: + :maxdepth: 1 + + part08/01_intro + part08/02_emle + part08/03_adp_pmf + part08/04_diels_alder diff --git a/doc/source/tutorial/part07/03_ghosts.rst b/doc/source/tutorial/part07/03_ghosts.rst index c044b1057..ba24b459b 100644 --- a/doc/source/tutorial/part07/03_ghosts.rst +++ b/doc/source/tutorial/part07/03_ghosts.rst @@ -97,10 +97,10 @@ The soft-core parameters are: * ``α_i`` and ``α_j`` control the amount of "softening" of the electrostatic and LJ interactions. A value of 0 means no softening (fully hard), while a value of 1 means fully soft. Ghost atoms which - disappear as a function of λ have a value of α of 0 in the - reference state, and 1 in the perturbed state. Ghost atoms which appear - as a function of λ have a value of α of 1 in the reference - state, and 0 in the perturbed state. These values can be perturbed + disappear as a function of λ have a value of α of 1 in the + reference state, and 0 in the perturbed state. Ghost atoms which appear + as a function of λ have a value of α of 0 in the reference + state, and 1 in the perturbed state. These values can be perturbed via the ``alpha`` lever in the λ-schedule. * ``n`` is the "coulomb power", and is set to 0 by default. It can be @@ -159,10 +159,10 @@ The soft-core parameters are: * ``α_i`` and ``α_j`` control the amount of "softening" of the electrostatic and LJ interactions. A value of 0 means no softening (fully hard), while a value of 1 means fully soft. Ghost atoms which - disappear as a function of λ have a value of α of 0 in the - reference state, and 1 in the perturbed state. Ghost atoms which appear - as a function of λ have a value of α of 1 in the reference - state, and 0 in the perturbed state. These values can be perturbed + disappear as a function of λ have a value of α of 1 in the + reference state, and 0 in the perturbed state. Ghost atoms which appear + as a function of λ have a value of α of 0 in the reference + state, and 1 in the perturbed state. These values can be perturbed via the ``alpha`` lever in the λ-schedule. * ``m`` is the "taylor power", and is set to 1 by default. It can be diff --git a/doc/source/tutorial/part08/01_intro.rst b/doc/source/tutorial/part08/01_intro.rst new file mode 100644 index 000000000..166b27e17 --- /dev/null +++ b/doc/source/tutorial/part08/01_intro.rst @@ -0,0 +1,131 @@ +============ +Introduction +============ + +The ``sire`` QM/MM implementation takes advantage of the new means of writing +`platform independent force calculations `_ +introduced in `OpenMM `_ 8.1. This allows us to interface +with any external package to modify atomic forces within the ``OpenMM`` context. +While OpenMM already directly supports ML/MM simulations via the `OpenMM-ML `_ +package, it is currently limited to specific backends and only supports mechanical +embedding. The ``sire`` QM/MM implementation provides a simple way to interface +with any external QM package using a simple Python callback. This approach is +designed with generarilty and flexibility in mind, rather than performance, +allowing a user to quickly prototype new ideas. + +Creating a QM engine +-------------------- + +In order to run QM/MM with ``sire``, we first need to create a QM engine. This +is passed as a keyword argument to the ``dynamics`` function and is used to +perform the QM part of the calculation at each timestep. + +As an example, we will consider the case of running a QM/MM simulation of alanine +dipeptide in water. First, let us load the molecular system: + +>>> import sire as sr +>>> mols = sr.load_test_files("ala.crd", "ala.top") + +We now need to set up the molecular system for the QM/MM simulation and create +an engine to perform the calculation: + +>>> qm_mols, engine = sr.qm.create_engine( +... mols, +... mols[0], +... py_object, +... callback="callback", +... cutoff="7.5A", +... neighbour_list_frequency=20, +... mechanical_embedding=False, +... ) + +Here the first argument is the molecules that we are simulating, the second +selection coresponding to the QM region (here this is the first molecule). +The selection syntax for QM atoms is extremely flexible. Any valid search string, +atom index, list of atom indicies, or molecule view/container that can be used. +Support for modelling partial molecules at the QM level is provided via the link +atom approach, via the charge shifting method. For details of this implementation, +see, e.g., the NAMD user guide `here `_. +While we support multiple QM fragments, we do not currently support multiple +*independent* QM regions. We plan on adding support for this in the near future. +The third argument is the Python object that will be used to perform the QM +calculation. The fourth argument is the name of the callback function that will +be used. If ``None``, then it assumed that the ``py_object`` itself is a callable, +i.e. it is the callback function. The callback function should have the following +signature: + +.. code-block:: python + + from typing import List, Optional, Tuple + + def callback( + numbers_qm: List[int], + charges_mm: List[float], + xyz_qm: List[List[float]], + xyz_mm: List[List[float]], + idx_mm: Optional[List[int]] = None, + ) -> Tuple[float, List[List[float]], List[List[float]]]: + +The function takes the atomic numbers of the QM atoms, the charges of the MM +atoms in mod electron charge, the coordinates of the QM atoms in Angstrom, and +the coordinates of the MM atoms in Angstrom. Optionally, it should also take the +indices of the true MM atoms (not link atoms or virtual charges) within the +QM/MM region. This is useful for obtaining any additional atomic properties +that may be required by the callback. (Note that link atoms and virtual charges +are always placed last in the list of MM charges and positions.) The function +should return the calculated energy in kJ/mol, the forces on the QM atoms in +kJ/mol/nm, and the forces on the MM atoms in kJ/mol/nm. The remaining arguments +are optional and specify the QM cutoff distance, the neighbour list update +frequency, and whether the electrostatics should be treated with mechanical +embedding. When mechanical embedding is used, the electrostatics are treated +at the MM level by ``OpenMM``. Note that this doesn't change the signature of +the callback function, i.e. it will be passed empty lists for the MM specific +arguments and should return an empty list for the MM forces. Atomic positions +passed to the callback function will already be unwrapped with the QM region +in the center. By default, no neighbour list will be used. (The same thing +can be achieved by passing ``neighbour_list_frequency=0``.) This is useful +when using the engine as a calculator for different input structures, where +there may be no correlation between coordinates. For regular molecular +dynamics simulations, setting a non-zero neighbour list frequency can +improve performance. + +The ``create_engine`` function returns a modified version of the molecules +containing a "merged" dipeptide that can be interpolated between MM and QM +levels of theory, along with the QM engine. This approach is extremely flexible +and allows the user to easily create a QM engine for a wide variety of QM packages. + +Note that while the callback interface described above is designed to be used +for QM/MM, it is completely general so could be used to apply *any* external +force based on the local environment around a subset of atoms. For example, you +could apply a biasing potential on top of the regular MM force field. + +Running a QM/MM simulation +-------------------------- + +In order to run a QM/MM simulation with ``sire`` we just need to specify our +QM engine when creating a dynamics object, for example: + +>>> d = qm_mols.dynamics( +... timestep="1fs", +... constraint="none", +... qm_engine=engine, +... platform="cpu", +... ) + +For QM/MM simulations it is recommended to use a 1 femtosecond timestep and no +constraints. The simulation can then be run as usual: + +>>> d.run("100ps", energy_frequency="1ps", frame_frequency="1ps") + +This will run 100 picoseconds of dynamics, recording the energy and coordinates +every picosecond. + +If you are using the callback interface and wish to apply a force on top of the +existing MM force field, rather than perform QM/MM, then you can pass +``swap_end_states=True`` to the ``dynamics`` function. This will swap the QM and +MM end states of all *perturbable* molecules within ``qm_mols``, so that the MM +state corresponds to λ = 1. More details on on λ interpolation can be found in +the `next section `_. + +In next section we will show how to use `emle-engine `_ +package as QM engine via a simple specialisation of the interface shown above. diff --git a/doc/source/tutorial/part08/02_emle.rst b/doc/source/tutorial/part08/02_emle.rst new file mode 100644 index 000000000..84305c4c5 --- /dev/null +++ b/doc/source/tutorial/part08/02_emle.rst @@ -0,0 +1,418 @@ +========= +Sire-EMLE +========= + +In this section we will show how to use the `emle-engine `_ +package as a QM/MM engine within ``sire``. The ``emle-engine`` package provides +support for a wide range of backends and embedding models, importantly providing +a simple and efficient ML model for electrostatic embedding. + +In order to use EMLE you will first need to create the following ``conda`` +environment: + +.. code-block:: bash + + $ git clone https://github.com/chemle/emle-engine.git + $ cd emle-engine + $ conda env create -f environment_sire.yaml + $ conda activate emle-sire + $ pip install -e . + +In this tutorial, we will perform a short ML/MM simulation of alanine dipeptide +in water. First, let us load the molecular system: + +>>> import sire as sr +>>> mols = sr.load_test_files("ala.crd", "ala.top") + +Creating an EMLE calculator +--------------------------- + +Next we will create an ``emle-engine`` calculator to perform the QM (or ML) calculation +for the dipeptide along with the ML electrostatic embedding. Since this is a small molecule +it isn't beneficial to perform the calculation on a GPU, so we will use the CPU instead. + +>>> from emle.calculator import EMLECalculator +>>> calculator = EMLECalculator(device="cpu") + +By default, ``emle-engine`` will use `TorchANI `_ +as the backend for in vacuo calculation of energies and gradients. However, +it is possible to use a wide variety of other backends, including your own +as long as it supports the standand `Atomic Simulation Environment (ASE) `_ +`calculator `_ interface. +For details, see the `backends `_ +section of the ``emle-engine`` documentation. At present, the default embedding +model provided with ``emle-engine`` supports only the elements H, C, N, O, and S. +We plan on adding support for other elements in the near future. + +Creating a QM engine +-------------------- + +We now need to set up the molecular system for the QM/MM simulation and create +an engine to perform the calculation: + +>>> qm_mols, engine = sr.qm.emle( +... mols, +... mols[0], +... calculator, +... cutoff="7.5A", +... neighbour_list_frequency=20 +... ) + +Here the first argument is the molecules that we are simulating, the second +selection coresponding to the QM region (here this is the first molecule), and +the third is calculator that was created above. The fourth and fifth arguments +are optional, and specify the QM cutoff distance and the neighbour list update +frequency respectively. (Shown are the default values.) The function returns a +modified version of the molecules containing a "merged" dipeptide that can be +interpolated between MM and QM levels of theory, along with an engine. The +engine registers a Python callback that uses ``emle-engine`` to perform the QM +calculation. + +Running a QM/MM simulation +-------------------------- + +Next we need to create a dynamics object to perform the simulation. For QM/MM +simulations it is recommended to use a 1 femtosecond timestep and no constraints. +In this example we will use the ``lambda_interpolate`` keyword to interpolate +the dipeptide potential between pure MM (λ=0) and QM (λ=1) over the course of +the simulation, which can be used for end-state correction of binding free +energy calculations. + +>>> d = qm_mols.dynamics( +... timestep="1fs", +... constraint="none", +... qm_engine=engine, +... lambda_interpolate=[0, 1], +... platform="cpu", +... ) + +We can now run the simulation. The options below specify the run time, the +frequency at which trajectory frames are saved, and the frequency at which +energies are recorded. The ``energy_frequency`` also specifies the frequency +at which the λ value is updated. + +>>> d.run("1ps", frame_frequency="0.05ps", energy_frequency="0.05ps") + +.. note:: + + If you don't require a trajectory file, then better performance can be achieved + leaving the ``frame_frequency`` keyword argument unset. + +Once the simulation has finished we can get back the trajectory of energy values. +This can be obtained as a `pandas `_ ``DataFrame``, +allowing for easy plotting and analysis. The table below shows the instantaneous +kinetic and potential energies as a function of λ, along with the accumulated +non-equilibrium work. (Times are in picoseconds and energies are in kcal/mol.) + +>>> nrg_traj = d.energy_trajectory(to_pandas=True) +>>> print(nrg_traj) + lambda kinetic potential work +time +6000.05 0.000000 1004.360323 -6929.923522 0.000000 +6000.10 0.052632 907.430686 -23199.383591 -856.287372 +6000.15 0.105263 1103.734847 -39773.815961 -1728.625918 +6000.20 0.157895 982.097859 -56012.557224 -2583.296511 +6000.25 0.210526 1035.727824 -72437.484783 -3447.766382 +6000.30 0.263158 1029.009153 -88803.629979 -4309.142445 +6000.35 0.315789 1014.269847 -105159.643486 -5169.985261 +6000.40 0.368421 1021.246476 -121532.624612 -6031.721110 +6000.45 0.421053 1022.233858 -137904.993921 -6893.424758 +6000.50 0.473684 1025.310039 -154284.677129 -7755.513348 +6000.55 0.526316 1025.001630 -170655.548776 -8617.138171 +6000.60 0.578947 1016.891585 -187011.341345 -9477.969359 +6000.65 0.631579 1022.910901 -203389.408932 -10339.972916 +6000.70 0.684211 1024.431575 -219765.627241 -11201.879143 +6000.75 0.736842 1052.484710 -236168.647435 -12065.195995 +6000.80 0.789474 1032.732604 -252520.971205 -12925.844615 +6000.85 0.842105 1061.216013 -268919.903129 -13788.946295 +6000.90 0.894737 1062.979311 -285305.108112 -14651.325505 +6000.95 0.947368 1057.025646 -301673.184597 -15512.803215 +6001.00 1.000000 1024.034371 -318006.345331 -16372.443253 + +.. note:: + + In the table above, the time doesn't start from zero because the example + molecular system was loaded from an existing trajectory restart file. + +.. note:: + + Unlike the ``sander`` interface of ``emle-engine``, the interpolated potential + energy is non-linear with respect to λ, i.e. it is not precisely a linear + combination of MM and QM energies. This is because the ``sire`` interface + performs a *perturbation* of the system parameters from MM to QM as λ is + changed, e.g. scaling down the force constants for bonded terms in the QM + region and scaling down the charges. Perturbing charges linearly results in + an energy change *between* charges that is quadratic in λ. + +Interfacing with OpenMM-ML +-------------------------- + +In the example above we used a sire dynamics object ``d`` to run the simulation. +This is wrapper around a standard OpenMM context object, providing a simple +convenience functions to make it easier to run and analyse simulations. However, +if you are already familiar with OpenMM, then it is possible to use ``emle-engine`` +with OpenMM directly. This allows for fully customised simulations, or the use +of `OpenMM-ML `_ as the backend for +calculation of the intramolecular force for the QM region. + +To use ``OpenMM-ML`` as the backend for the QM calculation, you will first need +to install the package: + +.. code-block:: bash + + $ conda install -c conda-forge openmm-ml + +Next, you will need to create an ``MLPotential`` for desired backend. Here we +will use the ANI-2x, as was used for the ``EMLECalculator`` above. The + +>>> import openmm +>>> from openmmml import MLPotential +>>> potential = MLPotential("ani2x") + +Since we are now using the ``MLPotential`` for the QM calculation, we need to +create a new ``EMLECalculator`` object with no backend, i.e. one that only +computes the electrostatic embedding: + +>>> calculator = EMLECalculator(backend=None, device="cpu") + +Next we create a new engine bound to the calculator: + +>>> _, engine = sr.qm.emle( +>>> ... mols, mols[0], calculator, cutoff="7.5A", neighbour_list_frequency=20 +>>> ... ) + +.. note:: + + ``qm_mols`` is not needed when using ``OpenMM-ML``, since it will perform + its own internal modifications for performing interpolation. + +Rather than using this engine with a ``sire`` dynamics object, we can instead +extract the underlying ``OpenMM`` force object and add it to an existing +``OpenMM`` system. The forces can be extracted from the engine as follows: + +>>> emle_force, interpolation_force = engine.get_forces() + +The ``emle_force`` object is the ``OpenMM`` force object that calculates the +electrostatic embedding interaction. The ``interpolation_force`` is a null +``CustomBondForce`` object that contains a ``lambda_emle`` global parameter +than can be used to scale the electrostatic embedding interaction. (By default, +this is set to 1, but can be set to any value between 0 and 1.) + +.. note:: + + The ``interpolation_force`` has no energy contribution. It is only required + as there is currently no way to add global parameters to the ``EMLEForce``. + +Next we need to save the original molecular system to disk so that we can load it +with ``OpenMM``. Here we will use AMBER format files, but any format supported by +``OpenMM`` can be used. + +>>> sr.save(mols, "ala", ["prm7", "rst7"]) + +We can now read them back in with ``OpenMM``: + +>>> prmtop = openmm.app.AmberPrmtopFile("ala.prm7") +>>> inpcrd = openmm.app.AmberInpcrdFile("ala.rst7") + +Next we use the ``prmtop`` to create the MM system: + +>>> mm_system = prmtop.createSystem( +... nonbondedMethod=openmm.app.PME, +... nonbondedCutoff=1 * openmm.unit.nanometer, +... constraints=openmm.app.HBonds, +... ) + +In oder to create the ML system, we first define the ML region. This is a list +of atom indices that are to be treated with the ML model. + +>>> ml_atoms = list(range(qm_mols[0].num_atoms())) + +We can now create the ML system: + +>>> ml_system = potential.createMixedSystem( +... prmtop.topology, mm_system, ml_atoms, interpolate=True +... ) + +By setting ``interpolate=True`` we are telling the ``MLPotential`` to create +a *mixed* system that can be interpolated between MM and ML levels of theory +using the ``lambda_interpolate`` global parameter. (By default this is set to 1.) + +.. note:: + + If you choose not to add the ``emle`` interpolation force to the system, then + the ``EMLEForce`` will also use the ``lambda_interpolate`` global parameter. + This allows for the electrostatic embedding to be alongside or independent of + the ML model. + +We can now add the ``emle`` forces to the system: + +>>> ml_system.addForce(emle_force) +>>> ml_system.addForce(interpolation_force) + +In order to ensure that ``OpenMM-ML`` doesn't perform mechanical embedding, we +next need to zero the charges of the QM atoms in the MM system: + +>>> for force in ml_system.getForces(): +... if isinstance(force, mm.NonbondedForce): +... for i in ml_atoms: +... _, sigma, epsilon = force.getParticleParameters(i) +... force.setParticleParameters(i, 0, sigma, epsilon) + +In order to run a simulation we need to create an integrator and context. First +we create the integrator: + +>>> integrator = openmm.LangevinMiddleIntegrator( +... 300 * openmm.unit.kelvin, +... 1.0 / openmm.unit.picosecond, +... 0.002 * openmm.unit.picosecond, +... ) + +And finally the context: + +>>> context = openmm.Context(ml_system, integrator) +>>> context.setPositions(inpcrd.positions) + +Creating an EMLE torch module +----------------------------- + +As well as the ``EMLECalculator``, the ``emle-engine`` package provides Torch +modules for the calculation of the electrostatic embedding. These can be used +to create derived modules for the calculation of in vacuo and electrostatic +embedding energies for different backends. For example, we provide an optimised +``ANI2xEMLE`` module that can be used to add electrostatic embedding to the +existing ``ANI2x`` model from `TorchANI `_. + +.. note:: + + Torch support is currently not available for our Windows conda pacakge + since ``pytorch`` is not available for Windows on the ``conda-forge``. + It is possible to compile Sire from source using a local ``pytorch`` + installation, or using the pacakge from the official ``pytorch`` conda + channel. + +As an example for how to use the module, let's again use the example alanine +dipeptide system. First, let's reload the system and center the solute within +the simulation box: + +>>> mols = sr.load_test_files("ala.crd", "ala.top") +>>> center = mols[0].coordinates() +>>> mols.make_whole(center=center) + +To obtain the point charges around the QM region we can take advantage of +Sire's powerful search syntax, e.g: + +>>> mols["mols within 7.5 of molidx 0"].view() + +.. image:: images/ala.png + :target: images/ala.png + :alt: Alanine-dipeptide in water. + +Next we will set the device and dtype for our Torch tensors: + +>>> import torch +>>> device = torch.device("cuda") +>>> dtype = torch.float32 + +Now we can create the input tensors for our calculation. First the coordinates +of the QM region: + +>>> coords_qm = torch.tensor( +... sr.io.get_coords_array(mols[0]), +... device=device, +... dtype=dtype, +... requires_grad=True, +... ) + +Next the coordinates of the MM region, which can be obtained using the search +term above: + +>>> mm_atoms = mols["water within 7.5 of molidx 0"].atoms() +>>> coords_mm = torch.tensor( +... sr.io.get_coords_array(mm_atoms), +... device=device, +... dtype=dtype, +... requires_grad=True, +... ) + +Now the atomic numbers for the atoms within the QM region: + +>>> atomic_numbers = torch.tensor( +... [element.num_protons() for element in mols[0].property("element")], +... device=device, +... dtype=torch.int64, +... ) + +And finally the charges of the MM atoms: + +>>> charges_mm = torch.tensor([atom.property("charge").value() for atom in mm_atoms], +... device=device, +... dtype=dtype +... ) + +In order to perform a calculation we need to create an instance of the +``ANI2xEMLE`` module: + +>>> from emle.models import ANI2xEMLE +>>> model = ANI2xEMLE().to(device) + +We can now calculate the in vacuo and electrostatic embedding energies: + +>>> energies = model(atomic_numbers, charges_mm, coords_qm, coords_mm) +>>> print(energies) +tensor([-4.9570e+02, -4.2597e-02, -1.2952e-02], device='cuda:0', + dtype=torch.float64, grad_fn=) + +The first element of the tensor is the in vacuo energy of the QM region, the +second is the static electrostatic embedding energy, and the third is the +induced electrostatic embedding energy. + +Then we can use ``autograd`` to compute the gradients of the energies with respect +to the QM and MM coordinates: + +>>> grad_qm, grad_mm = torch.autograd.grad(energies.sum(), (coords_qm, coords_mm)) +>>> print(grad_qm) +>>> print(grad_mm) +tensor([[-2.4745e-03, -1.2421e-02, 1.1079e-02], + [-7.0100e-03, -2.9659e-02, -6.8182e-03], + [-1.8393e-03, 1.1682e-02, 1.1509e-02], + [-3.4777e-03, 1.5750e-03, -1.9650e-02], + [-3.4737e-02, 7.3493e-02, 3.7996e-02], + [-9.3575e-03, -3.7101e-02, -2.0774e-02], + [ 9.2816e-02, -7.5343e-03, -5.0656e-02], + [ 4.9443e-03, 1.1114e-02, -4.0737e-04], + [-1.6362e-03, 3.0464e-03, 3.0192e-02], + [-6.2813e-03, -1.3678e-02, -3.4606e-03], + [ 4.5878e-03, 3.0234e-02, -2.9871e-02], + [-3.8999e-03, -1.3376e-02, -2.6382e-03], + [ 4.4184e-03, -7.4247e-03, 5.1742e-04], + [ 8.8851e-05, -8.5786e-03, 1.2712e-02], + [-5.9939e-02, 1.1648e-01, 1.6692e-01], + [-6.4231e-03, -4.4771e-02, 3.0655e-03], + [ 1.1274e-01, -6.4833e-02, -1.5494e-01], + [ 1.8500e-03, 5.5206e-03, -7.0060e-03], + [-6.3634e-02, -1.5340e-02, -2.7031e-03], + [ 7.7061e-03, 3.7852e-02, 6.0927e-03], + [-2.9915e-03, -3.5084e-02, 2.3909e-02], + [-1.5018e-02, 8.6911e-03, -2.5789e-03]], device='cuda:0') +tensor([[ 1.8065e-03, -1.4048e-03, -6.0694e-04], + [-9.0640e-04, 5.1307e-04, 9.6374e-06], + [-8.4827e-04, 9.5815e-04, 1.7164e-04], + ..., + [-5.7833e-04, -1.9125e-04, 2.0395e-03], + [ 3.2311e-04, 2.1525e-04, -7.8029e-04], + [ 3.5424e-04, 4.0781e-04, -1.5014e-03]], device='cuda:0') + +The model is serialisable, so can be saved and loaded using the standard +``torch.jit`` functions, e.g.: + +>>> script_model = torch.jit.script(model) +>>> torch.jit.save(script_model, "ani2xemle.pt") + +It is also possible to use the model with Sire when performing QM/MM dynamics: + +>>> qm_mols, engine = sr.qm.emle( +... mols, mols[0], model, cutoff="7.5A", neighbour_list_frequency=20 +... ) + +The model will be serialised and loaded into a C++ ``TorchQMEngine`` object, +bypassing the need for a Python callback. diff --git a/doc/source/tutorial/part08/03_adp_pmf.rst b/doc/source/tutorial/part08/03_adp_pmf.rst new file mode 100644 index 000000000..d016a1735 --- /dev/null +++ b/doc/source/tutorial/part08/03_adp_pmf.rst @@ -0,0 +1,180 @@ +========================================== +Alanine-dipeptide conformational landscape +========================================== + +..note:: + + The code in this tutorial was adapted from `FastMBAR `_. + +In a recent `preprint `_ +we used the ``emle-engine`` interface to ``sander`` to compute free-energy +surfaces for alanine-dipeptide as a function of the Φ and Ψ dihedral +angles shown below. Compared to regular mechanical embedding, ``EMLE`` was +found to be closer to the reference density-functional theory (DFT) surface. +In this tutorial we will show how to use the ``sire-emle`` interface to set +up and run the same calculations using ``OpenMM``. + +.. image:: https://raw.githubusercontent.com/CCPBioSim/biosimspace-advanced-simulation/de3f65372b49879b788f46618e0bfef78b2559b9/metadynamics/assets/alanine_dipeptide.png + :target: https://raw.githubusercontent.com/CCPBioSim/biosimspace-advanced-simulation/de3f65372b49879b788f46618e0bfef78b2559b9/metadynamics/assets/alanine_dipeptide.png + :alt: Alanine-dipeptide backbone angles + +Creating a context with sire-emle +--------------------------------- + +As in the previous section, we can first use ``sire-emle`` to create +a QM/MM capable dynamics object for the alanine-dipeptide example +system. We can then extract the underlying ``OpenMM`` context from +this. + +First we will create an ``EMLECalculator`` to compute the QM intramolecular +interaction using `ANI-2x `_ along with +the electrostatic embedding interaction. + +>>> from emle import EMLECalculator +>>> calculator = EMLECalculator(device="cpu") + +Next we will load the alanine-dipeptide system using ``sire``: + +>>> import sire as sr +>>> mols = sr.load_test_files("ala.crd", "ala.top") + +We can then create an ``EMLEEngine`` that can be be used to perform QM/MM: + +>>> qm_mols, engine = sr.qm.emle(mols, mols[0], calculator) + +Here the first argument is the molecules that we are simulating, the second +selection coresponding to the QM region (here this is the first molecule), and +the third is calculator that was created above. The fourth and fifth arguments +are optional, and specify the QM cutoff distance and the neigbour list update +frequency respectively. (Shown are the default values.) The function returns a +modified version of the molecules containing a "merged" dipeptide that can be +interpolated between MM and QM levels of theory, along with an engine. The +engine registers a Python callback that uses ``emle-engine`` to perform the QM +calculation. + +We can now create a ``dynamics`` that will create an ``OpenMM`` context for us +and can be used to run a simulation: + +>>> d = mols.dynamics( +... timestep="1fs", +... constraint="none", +... engine=engine, +... platform="cpu", +... ) + +Before extracting the context we will use the dynamics object to minimise the +alanine-dipeptide system: + +>>> d.minimise() + +Setting up umbrella sampling with OpenMM +---------------------------------------- + +We can now extract the underlying ``OpenMM`` context from the dynamics object, +then create a copy of the integrator and system. + +>>> from copy import deepcopy +>>> context = d.context() +>>> omm_system = context.getSystem() +>>> integrator = deepcopy(context.getIntegrator()) + +In order to perform umbrella sampling we will need to add a biasing potentials +for the two dihedral angles. Here we will use simple harmonic biasing potentials: + +First the Φ dihedral, which is formed by atom indices 4, 6, 8, and 14: + +>>> import openmm +>>> import openmm.app +>>> bias_torsion_phi = openmm.CustomTorsionForce( +... "0.5*k_phi*dtheta^2; dtheta = min(tmp, 2*pi-tmp); tmp = abs(theta - phi)" +... ) +>>> bias_torsion_phi.addGlobalParameter("pi", math.pi) +>>> bias_torsion_phi.addGlobalParameter("k_phi", 100.0) +>>> bias_torsion_phi.addGlobalParameter("phi", 0.0) +>>> bias_torsion_phi.addTorsion(4, 6, 8, 14) + +Next the Ψ dihedral, which is formed by atom indices 6, 8, 14, and 16: + +>>> bias_torsion_psi = openmm.CustomTorsionForce( +... "0.5*k_psi*dtheta^2; dtheta = min(tmp, 2*pi-tmp); tmp = abs(theta - psi)" +... ) +>>> bias_torsion_psi.addGlobalParameter("pi", math.pi) +>>> bias_torsion_psi.addGlobalParameter("k_psi", 100.0) +>>> bias_torsion_psi.addGlobalParameter("psi", 0.0) +>>> bias_torsion_psi.addTorsion(6, 8, 14, 16) + +We can now add these forces to the system: + +>>> omm_system.addForce(bias_torsion_phi) +>>> omm_system.addForce(bias_torsion_psi) + +In order to run the simulation we will create a new context using the system +and integrator and set the initial positions. + +>>> new_context = openmm.Context(omm_system, integrator, context.getPlatform()) +>>> new_context.setPositions(context.getState(getPositions=True).getPositions()) + +Running the simulation +---------------------- + +We are almost ready to run an umbrella sampling simulation. In this example we +will sample the Φ and Ψ dihedral angles on a 36x36 grid. We will first set the +biasing potential centers: + +>>> m = 36 +>>> M = m * m +>>> phi = np.linspace(-math.pi, math.pi, m, endpoint=False) +>>> psi = np.linspace(-math.pi, math.pi, m, endpoint=False) + +During the simulation we will save trajectories to disk which can later be +post-processed to compute the dihedral angles. We will create a directory +in which to store the files: + +>>> os.makedirs("./output/traj", exist_ok=True) + +The sampling is performed by looping over each of the umbrella windows +sequentially. For each window we set the biasing potential center and run +an initial equilibration of 5000 steps. We then run a production simulation +of 100 cycles of 100 steps each, saving trajectory after each cycle: + +>>> for idx in range(M): +... phi_index = idx // m +... psi_index = idx % m +... +... # Set the center of the biasing potentials. +... new_context.setParameter("phi", phi[phi_index]) +... new_context.setParameter("psi", psi[psi_index]) +... +... # Initial equilibrium. +... integrator.step(5000) +... +... # Production sampling. +... file_handle = open(f"./output/traj/phi_{phi_index}_phi_{psi_index}.dcd", "bw") +... dcd_file = DCDFile(file_handle, prm.topology, dt=integrator.getStepSize()) +... for x in range(100): +... integrator.step(100) +... state = new_context.getState(getPositions=True) +... positions = state.getPositions() +... dcd_file.writeModel(positions) +... file_handle.close() + +..note:: + + This is not a particulary efficient way to perform the sampling. In practice, + since it's possible to get good single core performance it is better to run + the windows in parallel, either individually, or in blocks. + +Analysing the results +--------------------- + +The trajectories saved to disk can be post-processed to compute the dihedral +angles, for example using the approach +`here `_. +The free-energy surface can then be compute using MBAR, or UWHAM. Example code +is provided in the `FastMBAR tutorial `_. + +The resulting free-energy surface should look similar to the one shown below: + +.. image:: images/pmf_adp.png + :target: images/pmf_adp.png + :alt: Free-energy surface for alanine-dipeptide dihedral angles. diff --git a/doc/source/tutorial/part08/04_diels_alder.rst b/doc/source/tutorial/part08/04_diels_alder.rst new file mode 100644 index 000000000..a32eb781e --- /dev/null +++ b/doc/source/tutorial/part08/04_diels_alder.rst @@ -0,0 +1,251 @@ +==================== +Diels-Alder reaction +==================== + +In this section we will show how to use the ``sire-emle`` interface to set up +simulations of the Diels-Alder reaction catalsed by the +`AbyU `_ enzyme. +This tutorial is intended to show how to set up a simulation in a similar +manner to how it would be performed with a standard QM/MM code, such as ``sander`` +from the `AmberTools `_ suite. + +Setting up the system +--------------------- + +Since the system is quite large it is convenient to restrict the simulation to +only consider solvent within a restricted region around the reaction site. In +``sander`` this can be performed by creating a solvent sphere and using an +``ibelly`` restraint to keep solvent molecules outside of this sphere fixed. +The same approach is easy to implement using ``sire`` and ``OpenMM``. First +let us load the full AbyU system: + +>>> import sire as sr +>>> mols = sr.load_test_files("abyu.prm7", "abyu.rst7") + +Next we use a ``sire`` `selection `_ +to create a sphere around the reaction site: + +>>> water_sphere = mols["water within 22A of atomidx 3 in molidx 1"] + +.. note:: + + Here we choose a sphere of radius 22 Å around atom 3 in the second molecule. + This is the reaction site in the AbyU system. In the simulation we will fix + all atoms more than 20 Å from this site. + +Next, we need to recreate the system by adding the water sphere to protein and +enzyme molecules: + +>>> new_mols = mols[:2] + water_sphere +>>> system = sr.system.System() +>>> for mol in new_mols: +... system.add(mol) + +.. note:: + + Here we add the molecules to the new system one-by-one to ensure that + the order is preserved. + +Now we'll add the original simulation box to the new system: + +>>> system.set_property("space", mols.property("space")) + +Next we'll minimise the system ready for simulation: + +>>> m = system.minimisation() +>>> m.run() +>>> mols = m.commit() + +Let's take a look at the system: + +.. image:: images/abyu.png + :target: images/abyu.png + :alt: AbyU system with a sphere of water molecules around the reaction site. + +Finally, we will write the system to an AMBER topology file for later use: + +>>> sr.save(mols, "abyu_sphere.prm7") + +Setting up the EMLE engine +-------------------------- + +We can now create an ``EMLECalculator`` to compute the QM intramolecular +interaction and the electrostatic embedding interaction. Here we will use +`xtb `_ as the QM backend: + +>>> from emle import EMLECalculator +>>> calculator = EMLECalculator(backend="xtb", device="cpu") + +Next we will create an ``EMLEEngine`` that can be be used to perform QM/MM +calculation: + +>>> qm_mols, engine = sr.qm.emle( +... mols, +... "atomnum 1804:1822,2083:2132", +... calculator, +... redistribute_charge=True +... ) + +Here the selection for the QM region includes tryptophan side-chain atoms +(1804-1822) and the substrate (2083-2132). The ``redistribute_charge`` keyword +ensures that the charge on atoms in the QM region is integer valued by +redistributing the remaining fractional charge over MM atoms within the +residues containing the QM atoms. + +Creating a context +------------------ + +We can now create a dynamics object that will create an ``OpenMM`` context for +us. In order to use the solvent sphere we will need to specify the ``fixed`` +keyword argument. This specifies a selection for the atoms that should be +fixed during simulation. Here we will fix all atoms more than 20 Å from the +reaction site: + +>>> d = mols.dynamics( +... timestep="1fs", +... constraint="none", +... perturbable_constraint="none", +... integrator="langevin_middle", +... cutoff_type="rf", +... qm_engine=engine, +... platform="cpu", +... fixed="not atoms within 20A of atomidx 3 in molidx 1", +) + +.. note:: + + In ``OpenMM``, fixed atoms are implemented by setting atomic masses to zero. + This means that the atoms are still involved in interactions, but do not move. + +Now we will extract the context from the dynamics object: + +>>> context = d.context() + +Creating a reaction coordinate +------------------------------ + +In order to study the Diels-Alder reaction we need to define a reaction coordinate. +With ``sander``, a typical choice is to use a generalised distance coordinate +restraint, using a weighted sum of distances between specific atom pairs involved +in the reaction. It is easy to implement this in ``OpenMM`` using a ``CustomBondForce`` +combined with a ``CustomCVForce``. + +First we will specify the the atom pairs involved in the bonds, along with the weights. + +>> pairs = ((2125, 2094, 0.7), (2119, 2087, 0.3)) + +Here the first two values in each tuple are the atom indices of the atoms involved +in the bond, and the third value is the weight of the bond. + +We will now define a force constant for our collective variable and an initial +equilibrium value: + +>>> import openmm +>>> import openmm.app +>>> from openmm import unit as unit +>>> k0 = (200 * unit.kilocalorie_per_mole / unit.angstrom**2).value_in_unit( +... unit.kilojoule_per_mole / unit.nanometer**2 +... ) +... r0 = 2.9 * unit.angstroms + +Next we will create a ``CustomBondForce`` to calculate the distance between the +atom pairs: + +>>> cv0 = openmm.CustomBondForce("weight*r") +>>> cv0.addPerBondParameter("weight") +>>> for atom1, atom2, weight in pairs: +... cv0.addBond(atom1, atom2, [weight]) + +We will also create two null forces to monitor the individual bond distances: + +>>> bond1 = openmm.CustomBondForce("r") +>>> bond1.addBond(2125, 2094) +>>> bond2 = openmm.CustomBondForce("r") +>>> bond2.addBond(2119, 2087) + +We can now create our restraint force using the collective variables above. +First let us define the energy expression. This is a simple harmonic potential: + +>>> energy_expression0 = "k0*(weighted_distance-r0)^2" + +Next we will create the force: + +>>> restraint_force0 = openmm.CustomCVForce(energy_expression0) +>>> restraint_force0.addCollectiveVariable("weighted_distance", cv0) +>>> restraint_force0.addCollectiveVariable("bond1", bond1) +>>> restraint_force0.addCollectiveVariable("bond2", bond2) +>>> restraint_force0.addGlobalParameter("k0", k0) +>>> restraint_force0.addGlobalParameter("r0", r0) + +During simulation we might also wish to prevent the formation of a spurious bond +between atoms 2115 and 2084. We can do this by adding an additional ``CustomCVForce``: + +>>> k1 = (100*unit.kilocalorie_per_mole/unit.angstrom**2).value_in_unit( +... unit.kilojoule_per_mole/unit.nanometer**2 +... ) +... r1 = 3.2*unit.angstroms +... cv1 = CustomBondForce("r") +... cv1..addBond(2115, 2084) +... energy_expression1 =("k1*(dist-r1)^2") +... restraint_force1 = openmm.CustomCVForce(energy_expression1) +... restraint_force1.addCollectiveVariable("dist", cv1) +... restraint_force1.addGlobalParameter("k1", k1) +... restraint_force1.addGlobalParameter("r1", r1) + +Setting up a new OpenMM context +------------------------------- + +We can now create a new OpenMM context with the restraint force added to the +system from the original context. First let us extract copies of the original +system and integrator: + +>>> from copy import deepcopy +>>> system = context.getSystem() +>>> integrator = deepcopy(context.getIntegrator()) + +Next we will add the restraint forces to the system: + +>>> system.addForce(restraint_force0) +>>> system.addForce(restraint_force1) + +Finally we will create a new context with the modified system and integrator, +setting the platform to the same as the original context: + +>>> new_context = openmm.Context(system, integrator, context.getPlatform()) +>>> new_context.setPositions(context.getState(getPositions=True).getPositions()) + +Running the simulation +---------------------- + +We can now run the simulation. Here we will run a short umbrella sampling +simpluation for a single window using 100 cycles of 100 integration steps. +After each cycle we will append to a trajectory file and print the current +values of the collective variables. + +First we will create a trajectory file using the topology saved earlier as +a reference: + +>>> prm = openmm.app.AmberPrmtopFile("abyu_sphere.prm7") +>>> file_handle = open("traj.dcd", "wb") +>>> dcd_file = openmm.app.DCDFile(file_handle, prm.topology, dt=integrator.getStepSize()) + +And now we will run the simulation: + +>>> for x in range(100): +... integrator.step(10) +... state = new_context.getState(getPositions=True) +... positions = state.getPositions() +... dcd_file.writeModel(positions) +... cv_vals = restraint_force0.getCollectiveVariableValues(new_context) +... print(f"Step {x:>3} of 100: CVs = {cv_vals[0]:.3f}, {cv_vals[1]:.3f}, {cv_vals[2]:.3f}") +... file_handle.close() + +In order to compute the free energy profile of the reaction we would need to +perform umbrella sampling simulations along the reaction coordinate. The resulting +free energy profile should looks similar to the one shown in the left panel of +the figure below. The right panel shows the two bond distances of interest +monitored within each sampling window. + +.. image:: images/pmf_abyu.png + :target: images/pmf_abyu.png + :alt: Free-energy profile for diels-alder reaction catalysed by AbyU. diff --git a/doc/source/tutorial/part08/images/abyu.png b/doc/source/tutorial/part08/images/abyu.png new file mode 100644 index 000000000..252dc8b48 Binary files /dev/null and b/doc/source/tutorial/part08/images/abyu.png differ diff --git a/doc/source/tutorial/part08/images/ala.png b/doc/source/tutorial/part08/images/ala.png new file mode 100644 index 000000000..3e7c85489 Binary files /dev/null and b/doc/source/tutorial/part08/images/ala.png differ diff --git a/doc/source/tutorial/part08/images/pmf_abyu.png b/doc/source/tutorial/part08/images/pmf_abyu.png new file mode 100644 index 000000000..e82117dcd Binary files /dev/null and b/doc/source/tutorial/part08/images/pmf_abyu.png differ diff --git a/doc/source/tutorial/part08/images/pmf_adp.png b/doc/source/tutorial/part08/images/pmf_adp.png new file mode 100644 index 000000000..307d643c9 Binary files /dev/null and b/doc/source/tutorial/part08/images/pmf_adp.png differ diff --git a/doc/source/tutorial/part08/sire_emle.ipynb b/doc/source/tutorial/part08/sire_emle.ipynb new file mode 100644 index 000000000..c5795a00b --- /dev/null +++ b/doc/source/tutorial/part08/sire_emle.ipynb @@ -0,0 +1,502 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "653dd6e1-f537-4a7c-b708-0652f3aea7c2", + "metadata": {}, + "source": [ + "# Sire-EMLE\n", + "\n", + "The `sire` QM/MM implementation takes advantage of the new means of writing [platform independent force calculations](http://docs.openmm.org/development/developerguide/09_customcppforceimpl.html) introduced in [OpenMM](http://openmm.org/) 8.1. This allows us to interface with any external package to modify atomic forces within the OpenMM context. While OpenMM already directly supports ML/MM simulations via the [OpenMM-ML](https://github.com/openmm/openmm-ml) package, it is currently limited to specific backends and only supports mechanical embedding. The `sire` QM/MM implementation performs the QM calculation using the [emle-engine](https://github.com/chemle/emle-engine) package, which has support for a wide range of backends and embedding models, importantly providing a simple and efficient ML model for electrostatic embedding.\n", + "\n", + "Here are some useful links:\n", + "\n", + "* [Paper](https://doi.org/10.26434/chemrxiv-2022-rknwt-v3) on the original EMLE methodology.\n", + "* [emle-engine](https://github.com/chemle/emle-engine) GitHub repository.\n", + "* [Preprint](https://doi.org/10.26434/chemrxiv-2023-6rng3-v2) on alanine-dipeptide conformational landscape study.\n", + "* Sire-EMLE [tutorials](https://github.com/OpenBioSim/sire/blob/feature_emle/doc/source/tutorial/partXX).\n", + "\n", + "In order to use QM/MM functionality within `sire` you will first need to create the following `conda` environment:\n", + "\n", + "```bash\n", + "$ git clone https://github.com/chemle/emle-engine.git\n", + "$ cd emle-engine\n", + "$ conda env create -f environment_sire.yaml\n", + "$ conda activate emle-sire\n", + "$ pip install -e .\n", + "```\n", + "\n", + "In this tutorial, we will perform a short ML/MM simulation of alanine dipeptide in water. First, let us load the molecular system:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "ab513df4-dce9-4650-96d2-8b64ffd25c17", + "metadata": {}, + "outputs": [], + "source": [ + "import sire as sr\n", + "mols = sr.load_test_files(\"ala.crd\", \"ala.top\")" + ] + }, + { + "cell_type": "markdown", + "id": "aec5d4db-64f7-4540-a695-02953ce38ed9", + "metadata": {}, + "source": [ + "## Creating an EMLE calculator\n", + "\n", + "Next we will create an `emle-engine` calculator to perform the QM (or ML) calculation for the dipeptide along with the ML electrostatic embedding. Since this is a small molecule it isn't beneficial to perform the calculation on a GPU, so we will use the CPU instead." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "1e7a8d25-eae9-434c-9293-cf6058aa5690", + "metadata": {}, + "outputs": [], + "source": [ + "from emle.calculator import EMLECalculator\n", + "calculator = EMLECalculator(device=\"cpu\")" + ] + }, + { + "cell_type": "markdown", + "id": "11d55aa7-c355-4f0a-a4bf-ea53cdeaa7a6", + "metadata": {}, + "source": [ + "By default, `emle-engine` will use [TorchANI](https://aiqm.github.io/torchani/) as the backend for in vacuo calculation of energies and gradients using the ANI-2x model. However, it is possible to use a wide variety of other backends, including your own as long as it supports the standand [Atomic Simulation Environment (ASE) calculator interface](https://wiki.fysik.dtu.dk/ase/). For details, see the [backends](https://github.com/chemle/emle-engine#backends) section of the emle-engine documentation. At present, the default embedding model provided with emle-engine supports only the elements H, C, N, O, and S. We plan on adding support for other elements in the near future.\n", + "\n", + "## Creating a QM engine\n", + "\n", + "We now need to set up the molecular system for the QM/MM simulation and create an engine to perform the calculation:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "7ef98e4b-2458-4e7a-8404-32141f7654a2", + "metadata": {}, + "outputs": [], + "source": [ + "qm_mols, engine = sr.qm.emle(mols, mols[0], calculator, \"7.5A\", 20)" + ] + }, + { + "cell_type": "markdown", + "id": "b4dd507a-20b5-41be-98f8-d0cf57f0dfe0", + "metadata": {}, + "source": [ + "Here the first argument is the molecules that we are simulating, the second selection coresponding to the QM region (here this is the first molecule), and the third is calculator that was created above. The fourth and fifth arguments are optional, and specify the QM cutoff distance and the neigbour list update frequency respectively. (Shown are the default values.) The function returns a modified version of the molecules containing a \"merged\" dipeptide that can be interpolated between MM and QM levels of theory, along with an engine. The engine registers a Python callback that uses `emle-engine` to perform the QM calculation.\n", + "\n", + "The selection syntax for QM atoms is extremely flexible. Any valid search string, atom index, list of atom indicies, or molecule view/container that can be used. Support for modelling partial molecules at the QM level is provided via the link atom approach, via the charge shifting method. For details of this implementation, see, e.g., the NAMD user guide [here](https://www.ks.uiuc.edu/Research/qmmm/). While we support multiple QM fragments, we do not currently support multiple independent QM regions. We plan on adding support for this in the near future.\n", + "\n", + "## Running a QM/MM simulation\n", + "\n", + "Next we need to create a dynamics object to perform the simulation. For QM/MM simulations it is recommended to use a 1 femtosecond timestep and no constraints. In this example we will use the `lambda_interpolate` keyword to interpolate the dipeptide potential between pure MM (λ=0) and QM (λ=1) over the course of the simulation, which can be used for end-state correction of binding free energy calculations." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "d25b14ba-c0da-4e7f-868b-b86ecf791bfc", + "metadata": {}, + "outputs": [], + "source": [ + "d = qm_mols.dynamics(\n", + " timestep=\"1fs\",\n", + " constraint=\"none\",\n", + " qm_engine=engine,\n", + " lambda_interpolate=[0, 1],\n", + " platform=\"cpu\",\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "4d97d2f4-c686-4e20-99c6-d76b9b6c41f9", + "metadata": {}, + "source": [ + "We can now run the simulation. The options below specify the run time, the frequency at which trajectory frames are saved, and the frequency at which energies are recorded. The energy_frequency also specifies the frequency at which the λ value is updated." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "29224f7c-e3c4-4131-a406-17af3af980d3", + "metadata": {}, + "outputs": [], + "source": [ + "import os\n", + "os.environ[\"OMP_NUM_THREADS\"] = \"1\"\n", + "d.run(\"0.1ps\", frame_frequency=\"0.01ps\", energy_frequency=\"0.01ps\")" + ] + }, + { + "cell_type": "markdown", + "id": "1101ac69-5701-473e-971f-bb8d2cba0923", + "metadata": {}, + "source": [ + "
\n", + "⚠️ Updating λ requires the updating of force field parameters in the OpenMM context. For large systems, this can be quite slow so it isn't recommended to set the energy_frequency to a value that is too small. We have a custom fork of OpenMM that provides a significant speedup for this operation by only updating a subset of the parameters. Installation instructions can be provided on request.\n", + "
\n", + "\n", + "
\n", + "⚠️ If you don't require a trajectory file, then better performance can be achieved leaving the frame_frequency keyword argument unset.\n", + "
\n", + "\n", + "
\n", + "⚠️ Currently requires the use of librascal for the calculation of SOAP (Smooth Overlap of Atomic Positions) descriptors. This is a serial code, so you may see better performance by restricting the number of OpenMP threads to 1, e.g. by setting the OMP_NUM_THREADS environment variable.\n", + "
" + ] + }, + { + "cell_type": "markdown", + "id": "47c807f2-6f9b-4b0d-88c2-3ff11e458c10", + "metadata": {}, + "source": [ + "Once the simulation has finished we can get back the trajectory of energy values. This can be obtained as a [pandas](https://pandas.pydata.org/) `DataFrame`, allowing for easy plotting and analysis. The table below shows the instantaneous kinetic and potential energies as a function of λ, along with the accumulated non-equilibrium work. (Times are in picoseconds and energies are in kcal/mol.)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "2f0b7725-f272-439f-8f72-ae6fe954d2e4", + "metadata": {}, + "outputs": [], + "source": [ + "d.energy_trajectory(to_pandas=True)" + ] + }, + { + "cell_type": "markdown", + "id": "ae5d0547-e6f5-424c-ba56-193b6ad34bd7", + "metadata": {}, + "source": [ + "
\n", + "⚠️ In the table above, the time doesn't start from zero because the example molecular system was loaded from an existing trajectory restart file.\n", + "
\n", + "\n", + "
\n", + "⚠️ Unlike the sander interface of emle-engine, the interpolated potential energy is non-linear with respect to λ, i.e. it is not precisely a linear combination of MM and QM energies. This is because the sire interface performs a *perturbation* of the system parameters from MM to QM as λ is changed, e.g. scaling down the force constants for bonded terms in the QM region and scaling down the charges. Perturbing charges linearly results in an energy change between charges that is quadratic in λ.\n", + "
" + ] + }, + { + "cell_type": "markdown", + "id": "ccfa5f0c-5f41-4353-88c4-b4784ccba1f3", + "metadata": {}, + "source": [ + "## Interfacing with OpenMM-ML\n", + "\n", + "In the example above we used a sire dynamics object d to run the simulation. This is wrapper around a standard `OpenMM` context object, providing a simple convenience functions to make it easier to run and analyse simulations. (It is easy to extract the system and forces from the context in order to create a customised simulation of your own.) However, if you are already familiar with OpenMM, then it is possible to use emle-engine with OpenMM directly. This allows for fully customised simulations, or the use of [OpenMM-ML](https://github.com/openmm/openmm-ml) as the backend for calculation of the intramolecular force for the QM region.\n", + "\n", + "To use `OpenMM-ML` as the backend for the QM calculation, you will first need to install the package:\n", + "\n", + "```bash\n", + "$ conda install -c conda-forge openmm-ml\n", + "```\n", + "\n", + "Next, you will need to create an `MLPotential` for desired backend. Here we will use ANI-2x, as was used for the EMLECalculator above. The" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "4a01fe20-3f61-4699-8fa7-03345b7b5644", + "metadata": {}, + "outputs": [], + "source": [ + "import openmm\n", + "from openmmml import MLPotential\n", + "potential = MLPotential(\"ani2x\")" + ] + }, + { + "cell_type": "markdown", + "id": "ed93d257-fceb-4a42-800b-d0a96512e504", + "metadata": {}, + "source": [ + "Since we are now using the `MLPotential` for the QM calculation, we need to create a new `EMLECalculator` object with no backend, i.e. one that only computes the electrostatic embedding:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "e0196581-0bc6-4eb2-9cff-a37674eb504f", + "metadata": {}, + "outputs": [], + "source": [ + "calculator = EMLECalculator(backend=None, device=\"cpu\")" + ] + }, + { + "cell_type": "markdown", + "id": "44b6caff-d6af-46b2-bb1b-eac0afb5283f", + "metadata": {}, + "source": [ + "Next we create a new engine bound to the calculator:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "a01fa8a3-85d1-4747-afc8-a072b02f1384", + "metadata": {}, + "outputs": [], + "source": [ + "qm_mols, engine = sr.qm.emle(mols, mols[0], calculator)" + ] + }, + { + "cell_type": "markdown", + "id": "c57f3983-6a2c-4094-9bb7-b8c62d7e4553", + "metadata": {}, + "source": [ + "Rather than using this engine with a `sire` dynamics object, we can instead extract the underlying `OpenMM` force object and add it to an existing `OpenMM` system. The forces can be extracted from the engine as follows:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "5d7bb677-c0be-4a67-b852-1e77682fa179", + "metadata": {}, + "outputs": [], + "source": [ + "emle_force, interpolation_force = engine.get_forces()" + ] + }, + { + "cell_type": "markdown", + "id": "ced9f87a-7f67-4e61-8537-4af00bbbf989", + "metadata": {}, + "source": [ + "The `emle_force` object is the `OpenMM` force object that calculates the electrostatic embedding interaction. The `interpolation_force` is a null `CustomBondForce` object that contains a `lambda_emle` global parameter than can be used to scale the electrostatic embedding interaction. (By default, this is set to 1, but can be set to any value between 0 and 1.)\n", + "\n", + "
\n", + "⚠️ The interpolation_force has no energy contribution. It is only required as there is currently no way to add global parameters to the EMLEForce.\n", + "
\n", + "\n", + "Since we want to use electrostatic embedding, we will also need to zero the charges on the atoms within the QM region before creating an `OpenMM` system. This can be done by passing the molecules through the ``sr.qm.zero_charge`` function along with the selection for the QM region:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "3c4429bd-d9a2-4a46-af2c-d33921e47f64", + "metadata": {}, + "outputs": [], + "source": [ + "qm_mols = sr.qm.zero_charge(qm_mols, qm_mols[0])" + ] + }, + { + "cell_type": "markdown", + "id": "448cb006-5ea5-46ae-9f19-f6060ae520e4", + "metadata": {}, + "source": [ + "We now write the modified system to an AMBER format topology and coordinate file so that we can load them with `OpenMM`:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "e9c636fe-92f2-4546-98e7-49c6a6f91ccb", + "metadata": {}, + "outputs": [], + "source": [ + "sr.save(qm_mols, \"ala_qm\", [\"prm7\", \"rst7\"])" + ] + }, + { + "cell_type": "markdown", + "id": "a18835fd-0eca-4424-910e-274dffbeda08", + "metadata": {}, + "source": [ + "We can now read them back in with OpenMM:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "e933ff6d-2d81-4d6a-af16-8a61e3cf0ea3", + "metadata": {}, + "outputs": [], + "source": [ + "prmtop = openmm.app.AmberPrmtopFile(\"ala_qm.prm7\")\n", + "inpcrd = openmm.app.AmberInpcrdFile(\"ala_qm.rst7\")" + ] + }, + { + "cell_type": "markdown", + "id": "827c4965-e53a-427c-b855-294d4d4a1761", + "metadata": {}, + "source": [ + "Next we use the prmtop to create the MM system:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "38ec7d50-44a3-46ea-83be-031230b568c1", + "metadata": {}, + "outputs": [], + "source": [ + "mm_system = prmtop.createSystem(\n", + " nonbondedMethod=openmm.app.PME,\n", + " nonbondedCutoff=7.5 * openmm.unit.angstrom,\n", + " constraints=openmm.app.HBonds\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "4453c713-8b9d-4289-b420-ab01c0c20869", + "metadata": {}, + "source": [ + "In oder to create the ML system, we first define the ML region. This is a list of atom indices that are to be treated with the ML model." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "7921e1a5-76ce-4a11-abf0-99919c8ad5bf", + "metadata": {}, + "outputs": [], + "source": [ + "ml_atoms = list(range(qm_mols[0].num_atoms()))" + ] + }, + { + "cell_type": "markdown", + "id": "0426b8e5-0c51-46f5-861e-71501b222508", + "metadata": {}, + "source": [ + "We can now create the ML system:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "952cb2fe-ca9e-46d8-a0b9-b6646449ccfd", + "metadata": {}, + "outputs": [], + "source": [ + "ml_system = potential.createMixedSystem(prmtop.topology, mm_system, ml_atoms, interpolate=True)" + ] + }, + { + "cell_type": "markdown", + "id": "4396f10b-0d29-452d-bb2f-4bb494e95619", + "metadata": {}, + "source": [ + "By setting `interpolate=True` we are telling the `MLPotential` to create a mixed system that can be interpolated between MM and ML levels of theory using the `lambda_interpolate` global parameter. (By default this is set to 1.)\n", + "\n", + "
\n", + "⚠️ If you choose not to add the emle interpolation force to the system, then the EMLEForce will also use the lambda_interpolate global parameter. This allows for the electrostatic embedding to be alongside or independent of the ML model.\n", + "
" + ] + }, + { + "cell_type": "markdown", + "id": "e1d45ac2-f34f-48e1-884a-243b63ddf9af", + "metadata": {}, + "source": [ + "We can now add the `emle` forces to the system:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "63ab7538-9267-43ae-8ae7-1317438d41c9", + "metadata": {}, + "outputs": [], + "source": [ + "ml_system.addForce(emle_force)\n", + "ml_system.addForce(interpolation_force)" + ] + }, + { + "cell_type": "markdown", + "id": "11cda2ef-e907-4e8f-8f79-1ab158f6188d", + "metadata": {}, + "source": [ + "In order to run a simulation we need to create an integrator and context. First we create the integrator:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "ed684bf8-c8e3-40d7-88d1-7231217de606", + "metadata": {}, + "outputs": [], + "source": [ + "integrator = openmm.LangevinMiddleIntegrator(\n", + " 300 * openmm.unit.kelvin,\n", + " 1.0 / openmm.unit.picosecond,\n", + " 0.002 * openmm.unit.picosecond\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "fbad1f00-b2cc-42b4-93ee-43b87bd7053b", + "metadata": {}, + "source": [ + "And finally the context:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "266bb2bd-0797-4f7d-bc27-35524dd85513", + "metadata": {}, + "outputs": [], + "source": [ + "context = openmm.Context(ml_system, integrator)\n", + "context.setPositions(inpcrd.positions)" + ] + }, + { + "cell_type": "markdown", + "id": "7c2ecc57-61a2-409d-a434-b880ccf37262", + "metadata": {}, + "source": [ + "Let's check the global parameters:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "539a8f46-1205-4c96-92e4-f97d346d70e1", + "metadata": {}, + "outputs": [], + "source": [ + "for param in context.getParameters():\n", + " print(param, context.getParameter(param))" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.10.13" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/docker/sire-generate-wrappers/Dockerfile b/docker/sire-generate-wrappers/Dockerfile_arm64 similarity index 100% rename from docker/sire-generate-wrappers/Dockerfile rename to docker/sire-generate-wrappers/Dockerfile_arm64 diff --git a/docker/sire-generate-wrappers/Dockerfile_x86 b/docker/sire-generate-wrappers/Dockerfile_x86 new file mode 100644 index 000000000..d225a62f7 --- /dev/null +++ b/docker/sire-generate-wrappers/Dockerfile_x86 @@ -0,0 +1,42 @@ +FROM continuumio/miniconda3:4.12.0 + +RUN apt-get update && apt-get -y upgrade \ + && apt-get install -y --no-install-recommends \ + git \ + wget \ + g++ \ + gcc \ + nano \ + ca-certificates \ + && apt-get clean \ + && rm -rf /var/lib/apt/lists/* + +WORKDIR /root + +RUN conda install make clang clangdev llvmdev cmake python=3.7 + +RUN conda install -c conda-forge python-levenshtein && \ + conda clean -a -f -y + +RUN pip install pyplusplus==1.8.2 pygccxml==1.8.5 fuzzywuzzy && \ + rm -fr ~/.cache/pip /tmp* + +RUN git clone https://github.com/CastXML/CastXML && \ + cd CastXML && \ + mkdir build && \ + cd build && \ + cmake -DCMAKE_INSTALL_PREFIX=/opt/conda/ .. && \ + make -j 4 && \ + make -j 4 install && \ + cd $HOME && \ + rm -rf CastXML + +COPY includes.tar.bz2 /opt/conda +COPY generate_wrappers /usr/bin +COPY unpack_headers /usr/bin +RUN chmod a+x /usr/bin/generate_wrappers +COPY bashrc /root/.bashrc +COPY push_wrappers /usr/bin +RUN chmod a+x /usr/bin/push_wrappers + +RUN mkdir /tmp diff --git a/docker/sire-generate-wrappers/README.md b/docker/sire-generate-wrappers/README.md index 5a2d7547e..4bf3a342a 100644 --- a/docker/sire-generate-wrappers/README.md +++ b/docker/sire-generate-wrappers/README.md @@ -29,8 +29,8 @@ this; ``` Unable to find image 'openbiosim/sire-generate-wrappers:arm64' locally -Trying to pull repository docker.io/siremol/sire-generate-wrappers ... -latest: Pulling from docker.io/siremol/sire-generate-wrappers +Trying to pull repository docker.io/openbiosim/sire-generate-wrappers ... +latest: Pulling from docker.io/openbiosim/sire-generate-wrappers a2abf6c4d29d: Pull complete c256cb8a03f5: Pull complete 96470ebef4ad: Pull complete @@ -43,7 +43,7 @@ d719333d5423: Pull complete 4642f7c70ef4: Pull complete b70e0e58d6c8: Pull complete Digest: sha256:a8a4513655dd43cebac306f77ac85a3fd953802759029bc771fe51db058eea95 -Status: Downloaded newer image for siremol/sire-generate-wrappers:latest +Status: Downloaded newer image for openbiosim/sire-generate-wrappers:latest (base) root:~# ``` @@ -66,6 +66,15 @@ would type This will run in parallel, but be aware that this can take a long time! +If you only wish to build the wrappers for a specificy library, this can +be done by passing the ``--lib`` argument, e.g.: + +``` +(base) root:~# generate_wrappers --lib IO +``` + +would only generate the wrappers for the `IO` library. + ## Checking the wrappers The `generate_wrappers` command will check out your branch @@ -174,17 +183,17 @@ Create the `includes.tar.bz2` file by running the ./create_includes_tarball --sire $HOME/sire.app ``` -Next, create the container via +Next, create the container via, e.g: ``` -docker build -t siremol/sire-generate-wrappers . +docker build -t openbiosim/sire-generate-wrappers -f Dockerfile_x86 ``` Assuming this worked, you can run the container via the same command as at the top, e.g. ``` -docker run -it siremol/sire-generate-wrappers +docker run -it openbiosim/sire-generate-wrappers ``` and the follow the rest of the instructions. diff --git a/docker/sire-generate-wrappers/generate_wrappers b/docker/sire-generate-wrappers/generate_wrappers index b6d8e0218..4f5c38e4a 100755 --- a/docker/sire-generate-wrappers/generate_wrappers +++ b/docker/sire-generate-wrappers/generate_wrappers @@ -11,6 +11,16 @@ while (( "$#" )); do exit 1 fi ;; + -l|--lib) + if [ -n "$2" ] && [ ${2:0:1} != "-" ]; then + LIB=$2 + shift 2 + else + echo "Error: Argument for $1 is missing" >&2 + exit 1 + fi + ;; + -*|--*=) # unsupported flags echo "Error: Unsupported flag $1" >&2 exit 1 @@ -50,7 +60,14 @@ cd sire/wrapper echo "Running scanheaders..." python AutoGenerate/scanheaders.py $HOME/sire/corelib/src/libs . -echo "Now generating all of the headers..." -python create_all_wrappers.py +if [ -z "$LIB" ] +then + echo "No library specified, generating all wrappers..." + python create_all_wrappers.py +else + echo "Generating wrappers for $LIB..." + cd $LIB + python ../AutoGenerate/create_wrappers.py +fi echo "Complete" diff --git a/requirements_bss.txt b/requirements_bss.txt index 98a166778..33ae86a9f 100644 --- a/requirements_bss.txt +++ b/requirements_bss.txt @@ -10,9 +10,10 @@ openmmtools >= 0.21.5 -# Both ambertools and gromacs aren't available on Windows +# Both ambertools and gromacs aren't available on Windows. +# The arm64 gromacs package is current broken. ambertools >= 22 ; sys_platform != "win32" -gromacs ; sys_platform != "win32" +gromacs ; sys_platform != "win32" and platform_machine != "arm64" # kartograf on Windows pulls in an openfe that has an old / incompatble # ambertools @@ -32,8 +33,6 @@ py3dmol pydot pygtail pyyaml -rdkit >=2023.0.0 -gemmi >=0.6.4 # The below are packages that aren't available on all # platforms/OSs and so need to be conditionally included diff --git a/requirements_build.txt b/requirements_build.txt index a9ea6f100..1ab5d8a1a 100644 --- a/requirements_build.txt +++ b/requirements_build.txt @@ -11,11 +11,6 @@ make ; sys_platform == "linux" libtool ; sys_platform == "linux" sysroot_linux-64==2.17 ; sys_platform == "linux" -# These packages are needed to compile -# the SireRDKit plugin -rdkit >=2023.0.0 -rdkit-dev >=2023.0.0 - # These packages are needed to compile # the SireGemmi plugin gemmi >=0.6.4 diff --git a/requirements_emle.txt b/requirements_emle.txt new file mode 100644 index 000000000..23f445dcd --- /dev/null +++ b/requirements_emle.txt @@ -0,0 +1,11 @@ +ambertools >= 22 ; sys_platform != "win32" +ase +deepmd-kit ; platform_machine != "aarch64" and sys_platform != "win32" +loguru +nnpops ; platform_machine != "aarch64" and sys_platform != "win32" +pygit2 +pytorch ; sys_platform != "win32" +python +pyyaml +torchani ; sys_platform != "win32" and (sys_platform != "linux" and platform_machine != "aarch64") +xtb-python ; sys_platform != "win32" and (sys_platform != "linux" and python_version != "3.12") diff --git a/requirements_host.txt b/requirements_host.txt index 9b0e2a797..9d6ca6ca9 100644 --- a/requirements_host.txt +++ b/requirements_host.txt @@ -5,14 +5,14 @@ gsl lazy_import libcblas libnetcdf -openmm +librdkit-dev +openmm >= 8.1 pandas python qt-main rich tbb tbb-devel -rdkit >=2023.0.0 gemmi >=0.6.4 # kartograf on Windows pulls in an openfe that has an old / incompatble diff --git a/setup.py b/setup.py index 48239a2ff..128607fcc 100644 --- a/setup.py +++ b/setup.py @@ -255,6 +255,12 @@ def parse_args(): help="Install BioSimSpace's dependencies too. This helps ensure " "compatibility between Sire's and BioSimSpace's dependencies.", ) + parser.add_argument( + "--install-emle-deps", + action="store_true", + default=False, + help="Install emle-engine's dependencies too.", + ) parser.add_argument( "--skip-deps", action="store_true", @@ -326,7 +332,9 @@ def _add_to_dependencies(dependencies, lines): dependencies_to_skip = [] -def conda_install(dependencies, install_bss_reqs=False, yes=True): +def conda_install( + dependencies, install_bss_reqs=False, install_emle_reqs=False, yes=True +): """Install the passed list of dependencies using conda""" conda_exe = conda @@ -409,10 +417,21 @@ def conda_install(dependencies, install_bss_reqs=False, yes=True): print("from running again. Please re-execute this script.") sys.exit(-1) + # Install emle-engine. + if install_emle_reqs: + cmd = [ + "pip", + "install", + "git+https://github.com/chemle/emle-engine.git", + ] + status = subprocess.run(cmd) + if status.returncode != 0: + print("Something went wrong installing emle-engine!") + sys.exit(-1) -def install_requires(install_bss_reqs=False, yes=True): - """ - Installs all of the dependencies. This can safely be called + +def install_requires(install_bss_reqs=False, install_emle_reqs=False, yes=True): + """Installs all of the dependencies. This can safely be called multiple times, as it will cache the result to prevent future installs taking too long """ @@ -433,7 +452,12 @@ def install_requires(install_bss_reqs=False, yes=True): except Exception: # this didn't import - maybe we are missing pip-requirements-parser print("Installing pip-requirements-parser") - conda_install(["pip-requirements-parser"], install_bss_reqs, yes=yes) + conda_install( + ["pip-requirements-parser"], + install_bss_reqs, + install_emle_reqs=False, + yes=yes, + ) try: from parse_requirements import parse_requirements except ImportError as e: @@ -448,7 +472,12 @@ def install_requires(install_bss_reqs=False, yes=True): bss_reqs = parse_requirements("requirements_bss.txt") reqs = reqs + bss_reqs + if install_emle_reqs: + emle_reqs = parse_requirements("requirements_emle.txt") + reqs = reqs + emle_reqs + dependencies = build_reqs + reqs + conda_install(dependencies, install_bss_reqs, install_emle_reqs, yes=yes) conda_install(dependencies, install_bss_reqs, yes=yes) @@ -879,6 +908,10 @@ def install(ncores: int = 1, npycores: int = 1): sys.exit(-1) install_bss = args.install_bss_deps + install_emle = args.install_emle_deps + + if install_emle and is_windows: + raise NotImplementedError("EMLE is current not supported on Windows") if args.skip_dep is not None: dependencies_to_skip = args.skip_dep @@ -895,7 +928,9 @@ def install(ncores: int = 1, npycores: int = 1): if action == "install": if not (args.skip_deps or args.skip_build): - install_requires(install_bss_reqs=install_bss) + install_requires( + install_bss_reqs=install_bss, install_emle_reqs=install_emle + ) if not args.skip_build: build( @@ -919,7 +954,9 @@ def install(ncores: int = 1, npycores: int = 1): ) elif action == "install_requires": - install_requires(install_bss_reqs=install_bss, yes=False) + install_requires( + install_bss_reqs=install_bss, install_emle_reqs=install_emle, yes=False + ) elif action == "install_module": install_module(ncores=args.ncores[0]) diff --git a/src/sire/CMakeLists.txt b/src/sire/CMakeLists.txt index c152f4342..db12d268b 100644 --- a/src/sire/CMakeLists.txt +++ b/src/sire/CMakeLists.txt @@ -104,6 +104,7 @@ add_subdirectory (mol) add_subdirectory (morph) add_subdirectory (move) add_subdirectory (options) +add_subdirectory (qm) add_subdirectory (restraints) add_subdirectory (search) add_subdirectory (stream) diff --git a/src/sire/__init__.py b/src/sire/__init__.py index 7422391a6..44049f4a2 100644 --- a/src/sire/__init__.py +++ b/src/sire/__init__.py @@ -844,6 +844,7 @@ def _convert(id): morph = _lazy_import.lazy_module("sire.morph") move = _lazy_import.lazy_module("sire.move") options = _lazy_import.lazy_module("sire.options") + qm = _lazy_import.lazy_module("sire.qm") qt = _lazy_import.lazy_module("sire.qt") restraints = _lazy_import.lazy_module("sire.restraints") search = _lazy_import.lazy_module("sire.search") diff --git a/src/sire/_pythonize.py b/src/sire/_pythonize.py index ec6f4d3c7..00378867f 100644 --- a/src/sire/_pythonize.py +++ b/src/sire/_pythonize.py @@ -238,6 +238,13 @@ def _load_new_api_modules(delete_old: bool = True, is_base: bool = False): delete_old=delete_old, ) + # Pythonize the QM classes. + _pythonize(Convert._SireOpenMM.PyQMCallback, delete_old=delete_old) + _pythonize(Convert._SireOpenMM.PyQMEngine, delete_old=delete_old) + _pythonize(Convert._SireOpenMM.PyQMForce, delete_old=delete_old) + _pythonize(Convert._SireOpenMM.TorchQMEngine, delete_old=delete_old) + _pythonize(Convert._SireOpenMM.TorchQMForce, delete_old=delete_old) + try: import lazy_import diff --git a/src/sire/mol/__init__.py b/src/sire/mol/__init__.py index fba3e2f7c..b6adf10d8 100644 --- a/src/sire/mol/__init__.py +++ b/src/sire/mol/__init__.py @@ -1579,6 +1579,8 @@ def _dynamics( com_reset_frequency=None, barostat_frequency=None, dynamic_constraints: bool = True, + qm_engine=None, + lambda_interpolate=None, map=None, ): """ @@ -1747,6 +1749,18 @@ def _dynamics( that lambda value. If this is false, then the constraint is set based on the current length. + qm_engine: + A sire.qm.QMMMEngine object to used to compute QM/MM forces + and energies on a subset of the atoms in the system. + + lambda_interpolate: float + The lambda value at which to interpolate the QM/MM forces and + energies, which can be used to perform end-state correction + simulations. A value of 1.0 is full QM, whereas a value of 0.0 is + full MM. If two values are specified, then lambda will be linearly + interpolated between the two values over the course of the + simulation, which lambda updated at the energy_frequency. + map: dict A dictionary of additional options. Note that any options set in this dictionary that are also specified via one of @@ -1927,6 +1941,8 @@ def _dynamics( ignore_perturbations=ignore_perturbations, restraints=restraints, fixed=fixed, + qm_engine=qm_engine, + lambda_interpolate=lambda_interpolate, map=map, ) diff --git a/src/sire/mol/_dynamics.py b/src/sire/mol/_dynamics.py index 71c98b52c..ca23bceb0 100644 --- a/src/sire/mol/_dynamics.py +++ b/src/sire/mol/_dynamics.py @@ -35,6 +35,56 @@ def __init__(self, mols=None, map=None, **kwargs): mols.atoms().find(selection_to_atoms(mols, fixed_atoms)), ) + # see if there is a QM/MM engine + if map.specified("qm_engine"): + qm_engine = map["qm_engine"].value() + + from ..legacy.Convert import QMEngine + from warnings import warn + + if qm_engine and not isinstance(qm_engine, QMEngine): + raise ValueError( + "'qm_engine' must be an instance of 'sire.legacy.Convert.QMEngine'" + ) + + # If a QM/MM engine is specified, then we need to check that there is a + # perturbable molecule. + try: + pert_mol = mols["property is_perturbable"] + except: + raise ValueError( + "You are trying to run QM/MM dynamics for a system without " + "a QM/MM enabled molecule!" + ) + + # Check the constraints and raise a warning if the perturbable_constraint + # is not "none". + + if map.specified("perturbable_constraint"): + perturbable_constraint = map["perturbable_constraint"].source() + if perturbable_constraint.lower() != "none": + warn( + "Running a QM/MM simulation with constraints on the QM " + "region is not recommended." + ) + else: + # The perturbable constraint is unset, so will follow the constraint. + # Make sure this is "none". + if map.specified("constraint"): + constraint = map["constraint"].source() + if constraint.lower() != "none": + warn( + "Running a QM/MM simulation with constraints on the QM " + "region is not recommended." + ) + # Constraints will be automatically applied, so we can't guarantee that + # the constraint is "none". + else: + warn( + "Running a QM/MM simulation with constraints on the QM " + "region is not recommended." + ) + if map.specified("cutoff"): cutoff = map["cutoff"] @@ -68,6 +118,39 @@ def __init__(self, mols=None, map=None, **kwargs): self._sire_mols._system.add(mols.molecules().to_molecule_group()) self._sire_mols._system.set_property("space", self._ffinfo.space()) + # see if this is an interpolation simulation + if map.specified("lambda_interpolate"): + if map["lambda_interpolate"].has_value(): + lambda_interpolate = map["lambda_interpolate"].value() + else: + lambda_interpolate = map["lambda_interpolate"].source() + + # Single lambda value. + try: + lambda_interpolate = float(lambda_interpolate) + map.set("lambda_value", lambda_interpolate) + # Two lambda values. + except: + try: + if not len(lambda_interpolate) == 2: + raise + lambda_interpolate = [float(x) for x in lambda_interpolate] + map.set("lambda_value", lambda_interpolate[0]) + except: + raise ValueError( + "'lambda_interpolate' must be a float or a list of two floats" + ) + + from ..units import kcal_per_mol + + self._is_interpolate = True + self._lambda_interpolate = lambda_interpolate + self._work = 0 * kcal_per_mol + self._nrg_prev = 0 * kcal_per_mol + + else: + self._is_interpolate = False + # find the existing energy trajectory - we will build on this self._energy_trajectory = self._sire_mols.energy_trajectory( to_pandas=False, map=self._map @@ -99,8 +182,7 @@ def __init__(self, mols=None, map=None, **kwargs): from ..convert import to self._omm_mols = to(self._sire_mols, "openmm", map=self._map) - self._omm_state = None - self._omm_state_has_cv = (False, False) + self._clear_state() if self._ffinfo.space().is_periodic(): self._enforce_periodic_box = True @@ -137,19 +219,29 @@ def _update_from(self, state, state_has_cv, nsteps_completed): openmm_extract_space, ) + if self._sire_mols.num_atoms() == self._omm_mols.get_atom_index().count(): + # all of the atoms in all molecules are in the context, + # and we can assume they are in atom index order + mols_to_update = self._sire_mols.molecules() + else: + # some of the atoms aren't in the context, and they may be + # in a different order + mols_to_update = self._omm_mols.get_atom_index().atoms() + mols_to_update.update(self._sire_mols.molecules()) + if state_has_cv[1]: # get velocities too - mols = openmm_extract_coordinates_and_velocities( + mols_to_update = openmm_extract_coordinates_and_velocities( state, - self._sire_mols.molecules(), + mols_to_update, # black auto-formats this to a long line perturbable_maps=self._omm_mols.get_lambda_lever().get_perturbable_molecule_maps(), # noqa: E501 map=self._map, ) else: - mols = openmm_extract_coordinates( + mols_to_update = openmm_extract_coordinates( state, - self._sire_mols.molecules(), + mols_to_update, # black auto-formats this to a long line perturbable_maps=self._omm_mols.get_lambda_lever().get_perturbable_molecule_maps(), # noqa: E501 map=self._map, @@ -157,7 +249,7 @@ def _update_from(self, state, state_has_cv, nsteps_completed): self._current_step = nsteps_completed - self._sire_mols.update(mols.to_molecules()) + self._sire_mols.update(mols_to_update.molecules()) if self._ffinfo.space().is_periodic(): # don't change the space if it is infinite - this @@ -174,8 +266,7 @@ def _enter_dynamics_block(self): raise SystemError("Cannot start dynamics while it is already running!") self._is_running = True - self._omm_state = None - self._omm_state_has_cv = (False, False) + self._clear_state() def _exit_dynamics_block( self, @@ -183,6 +274,7 @@ def _exit_dynamics_block( save_energy: bool = False, lambda_windows=[], save_velocities: bool = False, + delta_lambda: float = None, ): if not self._is_running: raise SystemError("Cannot stop dynamics that is not running!") @@ -231,26 +323,52 @@ def _exit_dynamics_block( ) sim_lambda_value = self._omm_mols.get_lambda() - nrgs[str(sim_lambda_value)] = nrgs["potential"] - if lambda_windows is not None: - for lambda_value in lambda_windows: - if lambda_value != sim_lambda_value: - self._omm_mols.set_lambda( - lambda_value, update_constraints=False - ) - nrgs[str(lambda_value)] = ( - self._omm_mols.get_potential_energy( - to_sire_units=False - ).value_in_unit(openmm.unit.kilocalorie_per_mole) - * kcal_per_mol - ) + # Store the potential energy and accumulated non-equilibrium work. + if self._is_interpolate: + nrg = nrgs["potential"] + + if sim_lambda_value != 0.0: + self._work += delta_lambda * (nrg - self._nrg_prev) + self._nrg_prev = nrg + nrgs["work"] = self._work + else: + nrgs[str(sim_lambda_value)] = nrgs["potential"] + + if lambda_windows is not None: + for lambda_value in lambda_windows: + if lambda_value != sim_lambda_value: + self._omm_mols.set_lambda( + lambda_value, update_constraints=False + ) + nrgs[str(lambda_value)] = ( + self._omm_mols.get_potential_energy( + to_sire_units=False + ).value_in_unit(openmm.unit.kilocalorie_per_mole) + * kcal_per_mol + ) self._omm_mols.set_lambda(sim_lambda_value, update_constraints=False) - self._energy_trajectory.set( - self._current_time, nrgs, {"lambda": str(sim_lambda_value)} - ) + if self._is_interpolate: + self._energy_trajectory.set( + self._current_time, nrgs, {"lambda": f"{sim_lambda_value:.8f}"} + ) + else: + self._energy_trajectory.set( + self._current_time, nrgs, {"lambda": str(sim_lambda_value)} + ) + + # update the interpolation lambda value + if self._is_interpolate: + if delta_lambda: + sim_lambda_value += delta_lambda + # clamp to [0, 1] + if sim_lambda_value < 0.0: + sim_lambda_value = 0.0 + elif sim_lambda_value > 1.0: + sim_lambda_value = 1.0 + self._omm_mols.set_lambda(sim_lambda_value) self._is_running = False @@ -559,8 +677,7 @@ def step(self, num_steps: int = 1): if self._is_running: raise SystemError("Cannot step dynamics while it is already running!") - self._omm_state = None - self._omm_state_has_cv = (False, False) + self._clear_state() self._omm_mols.getIntegrator().step(num_steps) @@ -586,6 +703,7 @@ def run_minimisation( starting_k: float = 100.0, ratchet_scale: float = 2.0, max_constraint_error: float = 0.001, + timeout: str = "300s", ): """ Internal method that runs minimisation on the molecules. @@ -619,12 +737,26 @@ def run_minimisation( - starting_k (float): The starting value of k for the minimisation - ratchet_scale (float): The amount to scale k at each ratchet - max_constraint_error (float): The maximum error in the constraint in nm + - timeout (float): The maximum time to run the minimisation for in seconds. + A value of <=0 will disable the timeout. """ from ..legacy.Convert import minimise_openmm_context if max_iterations <= 0: max_iterations = 0 + try: + from ..units import second + from .. import u + + timeout = u(timeout) + if not timeout.has_same_units(second): + raise ValueError("'timeout' must have units of time") + except: + raise ValueError("Unable to parse 'timeout' as a time") + + self._clear_state() + self._minimisation_log = minimise_openmm_context( self._omm_mols, tolerance=tolerance, @@ -635,6 +767,7 @@ def run_minimisation( starting_k=starting_k, ratchet_scale=ratchet_scale, max_constraint_error=max_constraint_error, + timeout=timeout.to(second), ) def _rebuild_and_minimise(self): @@ -700,10 +833,6 @@ def run( if energy_frequency is not None: energy_frequency = u(energy_frequency) - if lambda_windows is not None: - if type(lambda_windows) is not list: - lambda_windows = [lambda_windows] - try: steps_to_run = int(time.to(picosecond) / self.timestep().to(picosecond)) except Exception: @@ -748,9 +877,33 @@ def run( else: frame_frequency = frame_frequency.to(picosecond) - if lambda_windows is None: - if self._map.specified("lambda_windows"): - lambda_windows = self._map["lambda_windows"].value() + completed = 0 + + frame_frequency_steps = int(frame_frequency / self.timestep().to(picosecond)) + + energy_frequency_steps = int(energy_frequency / self.timestep().to(picosecond)) + + # If performing QM/MM lambda interpolation, then we just compute energies + # for the pure MM (0.0) and QM (1.0) potentials. + if self._is_interpolate: + lambda_windows = [0.0, 1.0] + # Work out the lambda increment. + if isinstance(self._lambda_interpolate, list): + divisor = (steps_to_run / energy_frequency_steps) - 1.0 + delta_lambda = ( + self._lambda_interpolate[1] - self._lambda_interpolate[0] + ) / divisor + # Fixed lambda value. + else: + delta_lambda = None + else: + delta_lambda = None + if lambda_windows is not None: + if not isinstance(lambda_windows, list): + lambda_windows = [lambda_windows] + else: + if self._map.specified("lambda_windows"): + lambda_windows = self._map["lambda_windows"].value() def runfunc(num_steps): try: @@ -771,12 +924,6 @@ def process_block(state, state_has_cv, nsteps_completed): } ) - completed = 0 - - frame_frequency_steps = int(frame_frequency / self.timestep().to(picosecond)) - - energy_frequency_steps = int(energy_frequency / self.timestep().to(picosecond)) - def get_steps_till_save(completed: int, total: int): """Internal function to calculate the number of steps to run before the next save. This returns a tuple @@ -920,6 +1067,7 @@ class NeedsMinimiseError(Exception): save_energy=save_energy, lambda_windows=lambda_windows, save_velocities=save_velocities, + delta_lambda=delta_lambda, ) saved_last_frame = False @@ -962,8 +1110,7 @@ class NeedsMinimiseError(Exception): # try to fix this problem by minimising, # then running again self._is_running = False - self._omm_state = None - self._omm_state_has_cv = (False, False) + self._clear_state() self._rebuild_and_minimise() orig_args["auto_fix_minimise"] = False self.run(**orig_args) @@ -999,7 +1146,7 @@ def commit(self, return_as_system: bool = False): from ..system import System if System.is_system(self._orig_mols): - return self._sire_mols + return self._sire_mols.clone() else: r = self._orig_mols.clone() r.update(self._sire_mols.molecules()) @@ -1039,6 +1186,8 @@ def __init__( coulomb_power=None, restraints=None, fixed=None, + qm_engine=None, + lambda_interpolate=None, ): from ..base import create_map from .. import u @@ -1067,6 +1216,8 @@ def __init__( _add_extra(extras, "coulomb_power", coulomb_power) _add_extra(extras, "restraints", restraints) _add_extra(extras, "fixed", fixed) + _add_extra(extras, "qm_engine", qm_engine) + _add_extra(extras, "lambda_interpolate", lambda_interpolate) map = create_map(map, extras) @@ -1382,6 +1533,13 @@ def integrator(self): """ return self._d.integrator() + def context(self): + """ + Return the underlying OpenMM context that is being driven by this + dynamics object. + """ + return self._d._omm_mols + def info(self): """ Return the information that describes the forcefield that will @@ -1492,7 +1650,7 @@ def current_potential_energy(self, lambda_values=None): if lambda_values is None: return self._d.current_potential_energy() else: - if not type(lambda_values) is list: + if not isinstance(lambda_values, list): lambda_values = [lambda_values] # save the current value of lambda so we diff --git a/src/sire/mol/_minimisation.py b/src/sire/mol/_minimisation.py index 514523f69..32e3dc8f9 100644 --- a/src/sire/mol/_minimisation.py +++ b/src/sire/mol/_minimisation.py @@ -96,6 +96,7 @@ def run( starting_k: float = 400.0, ratchet_scale: float = 10.0, max_constraint_error: float = 0.001, + timeout: str = "300s", ): """ Internal method that runs minimisation on the molecules. @@ -129,6 +130,8 @@ def run( - starting_k (float): The starting value of k for the minimisation - ratchet_scale (float): The amount to scale k at each ratchet - max_constraint_error (float): The maximum error in the constraint in nm + - timeout (float): The maximum time to run the minimisation for in seconds. + A value of <=0 will disable the timeout. """ if not self._d.is_null(): self._d.run_minimisation( @@ -140,6 +143,7 @@ def run( starting_k=starting_k, ratchet_scale=ratchet_scale, max_constraint_error=max_constraint_error, + timeout=timeout, ) return self diff --git a/src/sire/morph/CMakeLists.txt b/src/sire/morph/CMakeLists.txt index 11ea25882..3c63ff840 100644 --- a/src/sire/morph/CMakeLists.txt +++ b/src/sire/morph/CMakeLists.txt @@ -16,6 +16,7 @@ set ( SCRIPTS _pertfile.py _perturbation.py _repex.py + _xml.py ) # installation diff --git a/src/sire/morph/__init__.py b/src/sire/morph/__init__.py index e5931ee5e..caf66cb84 100644 --- a/src/sire/morph/__init__.py +++ b/src/sire/morph/__init__.py @@ -16,6 +16,7 @@ "zero_ghost_bonds", "zero_ghost_angles", "zero_ghost_torsions", + "evaluate_xml_force", "Perturbation", ] @@ -48,3 +49,5 @@ from ._mutate import mutate from ._decouple import annihilate, decouple + +from ._xml import evaluate_xml_force diff --git a/src/sire/morph/_decouple.py b/src/sire/morph/_decouple.py index d7ca2e98c..a414aad35 100644 --- a/src/sire/morph/_decouple.py +++ b/src/sire/morph/_decouple.py @@ -6,7 +6,10 @@ def annihilate(mol, as_new_molecule: bool = True, map=None): Return a merged molecule that represents the perturbation that completely annihilates the molecule. The returned merged molecule will be suitable for using in a double-annihilation free energy - simulation, e.g. to calculate absolute binding free energies. + simulation, e.g. to calculate absolute binding free energies. Note that + this perturbation will remove all intramolecular interactions, not just + nonbonded intramolecular interactions. You should add positional restraints + to all atoms in the molecule to prevent to prevent it drifting apart. Parameters ---------- diff --git a/src/sire/morph/_repex.py b/src/sire/morph/_repex.py index ffd328063..8edbd6057 100644 --- a/src/sire/morph/_repex.py +++ b/src/sire/morph/_repex.py @@ -87,13 +87,15 @@ def replica_exchange( # delta = beta_b * [ H_b_i - H_b_j + P_b (V_b_i - V_b_j) ] + # beta_a * [ H_a_i - H_a_j + P_a (V_a_i - V_a_j) ] - from ..units import k_boltz + from ..units import k_boltz, mole + + N_A = 6.02214076e23 / mole beta0 = 1.0 / (k_boltz * temperature0) beta1 = 1.0 / (k_boltz * temperature1) if not ensemble0.is_constant_pressure(): - delta = beta1 * (nrgs1[0] - nrgs1[1]) + beta0 * (nrgs0[0] - nrgs0[1]) + delta = beta1 * (nrgs1[1] - nrgs1[0]) + beta0 * (nrgs0[0] - nrgs0[1]) else: volume0 = replica0.current_space().volume() volume1 = replica1.current_space().volume() @@ -102,8 +104,8 @@ def replica_exchange( pressure1 = ensemble1.pressure() delta = beta1 * ( - nrgs1[0] - nrgs1[1] + pressure1 * (volume1 - volume0) - ) + beta0 * (nrgs0[0] - nrgs0[1] + pressure0 * (volume0 - volume1)) + (nrgs1[1] - nrgs1[0]) + (pressure1 * (volume1 - volume0) * N_A) + ) + beta0 * ((nrgs0[0] - nrgs0[1]) + (pressure0 * (volume0 - volume1) * N_A)) from math import exp @@ -118,12 +120,8 @@ def replica_exchange( replica1.set_lambda(lam0) if ensemble0 != ensemble1: - replica0.set_ensemble( - ensemble1, rescale_velocities=rescale_velocities - ) - replica1.set_ensemble( - ensemble0, rescale_velocities=rescale_velocities - ) + replica0.set_ensemble(ensemble1, rescale_velocities=rescale_velocities) + replica1.set_ensemble(ensemble0, rescale_velocities=rescale_velocities) return (replica1, replica0, True) else: diff --git a/src/sire/morph/_xml.py b/src/sire/morph/_xml.py new file mode 100644 index 000000000..2e61b2276 --- /dev/null +++ b/src/sire/morph/_xml.py @@ -0,0 +1,268 @@ +__all__ = ["evaluate_xml_force"] + + +def evaluate_xml_force(mols, xml, force): + """ + Evaluate the custom force defined in the passed XML file. + The passed molecules must be the ones used to create the + OpenMM context associated with the XML file. + + + Parameters + ---------- + + mols : sire.system.System, sire.mol.Molecule + The perturbable molecular system or molecule to evaluate the force on. + This should have already been linked to the appropriate end state. + + xml : str + The path to the XML file containing the custom force. + + force : str + The name of the custom force to evaluate. Options are: + "ghost-ghost", "ghost-nonghost", "ghost-14". + + Returns + ------- + + pairs : [(sire.mol.Atom, sire.mol.Atom)] + The atom pairs that interacted. + + nrg_coul : [sr.units.GeneralUnit] + The Coulomb energies for each atom pair. + + nrg_lj : [sr.units.GeneralUnit] + The Lennard-Jones energies for each atom pair. + """ + + from math import sqrt + + import xml.etree.ElementTree as ET + import sys + + from .._measure import measure + from ..legacy.Mol import Molecule + from ..system import System + from ..units import nanometer, kJ_per_mol + + # Store the name of the current module. + module = sys.modules[__name__] + + # Validate the molecules. + if not isinstance(mols, (System, Molecule)): + raise TypeError( + "'mols' must be of type 'sire.system.System' or 'sire.mol.Molecule'." + ) + + # Validate the XML file. + if not isinstance(xml, str): + raise TypeError("'xml' must be of type 'str'.") + + # Try to parse the XML file. + try: + tree = ET.parse(xml) + except: + raise ValueError(f"Could not parse the XML file: {xml}") + + # Validate the force type. + if not isinstance(force, str): + raise TypeError("'force' must be of type 'str'.") + + # Sanitize the force name. + force = ( + force.lower() + .replace(" ", "") + .replace("-", "") + .replace("_", "") + .replace("/", "") + ) + + # Validate the force name. + if not force in ["ghostghost", "ghostnonghost", "ghost14"]: + raise ValueError( + "'force' must be one of 'ghost-ghost', 'ghost-nonghost', or 'ghost-14'." + ) + + # Create the name and index based on the force type. + if force == "ghostghost": + name = "GhostGhostNonbondedForce" + elif force == "ghostnonghost": + name = "GhostNonGhostNonbondedForce" + elif force == "ghost14": + name = "Ghost14BondForce" + + # Get the root of the XML tree. + root = tree.getroot() + + # Loop over the forces until we find the first CustomNonbondedForce. + is_found = False + for force in tree.find("Forces"): + if force.get("name") == name: + is_found = True + break + + # Raise an error if the force was not found. + if not is_found: + raise ValueError(f"Could not find the force: {name}") + + # Get the energy terms. + terms = list(reversed(force.get("energy").split(";")[1:-1])) + + # Create a list to store the results. + pairs = [] + nrg_coul_list = [] + nrg_lj_list = [] + + # CustomNonbondedForce: ghost-ghost or ghost-nonghost. + if name != "Ghost14BondForce": + # Get the parameters for this force. + parameters = [p.get("name") for p in force.find("PerParticleParameters")] + + # Get all the particle parameters. + particles = force.find("Particles") + + # Get the two sets of particles that interact. + set1 = [ + int(p.get("index")) + for p in force.find("InteractionGroups") + .find("InteractionGroup") + .find("Set1") + ] + set2 = [ + int(p.get("index")) + for p in force.find("InteractionGroups") + .find("InteractionGroup") + .find("Set2") + ] + + # Get the exclusions. + exclusions = [ + (int(e.get("p1")), int(e.get("p2"))) + for e in force.find("Exclusions").findall("Exclusion") + ] + for x, (i, j) in enumerate(exclusions): + if i > j: + exclusions[x] = (j, i) + exclusions = set(exclusions) + + # Get the cutoff distance. + cutoff = float(force.get("cutoff")) + + # Get the list of atoms. + atoms = mols.atoms() + + # Loop over all particles in set1. + for x in range(len(set1)): + # Get the index from set1. + i = set1[x] + + # Get the parameters for this particle. + particle_i = particles[i] + + # Get the atom. + atom_i = atoms[i] + + # Set the parameters for this particle. + setattr(module, parameters[0] + "1", float(particle_i.get("param1"))) + setattr(module, parameters[1] + "1", float(particle_i.get("param2"))) + setattr(module, parameters[2] + "1", float(particle_i.get("param3"))) + setattr(module, parameters[3] + "1", float(particle_i.get("param4"))) + setattr(module, parameters[4] + "1", float(particle_i.get("param5"))) + + # Loop over all other particles in set1. + for y in range(x + 1, len(set1)): + # Get the index from set2. + j = set1[y] + + # Check if this pair is excluded. + pair = (i, j) if i < j else (j, i) + if pair in exclusions: + continue + + # Get the parameters for this particle. + particle_j = particles[j] + + # Get the atom. + atom_j = atoms[j] + + # Set the parameters for this particle. + setattr(module, parameters[0] + "2", float(particle_j.get("param1"))) + setattr(module, parameters[1] + "2", float(particle_j.get("param2"))) + setattr(module, parameters[2] + "2", float(particle_j.get("param3"))) + setattr(module, parameters[3] + "2", float(particle_j.get("param4"))) + setattr(module, parameters[4] + "2", float(particle_j.get("param5"))) + + # Get the distance between the particles. + r = measure(atom_i, atom_j).to(nanometer) + + # Atoms are within the cutoff. + if r < cutoff: + # Evaluate the energy term by term. + for term in terms: + # Replace any instances of ^ with **. + term = term.replace("^", "**") + + # Split the term into the result and the expression. + result, expression = term.split("=") + + # Evaluate the expression. + setattr(module, result, eval(expression)) + + # Give energies units. + coul_nrg = module.coul_nrg * kJ_per_mol + lj_nrg = module.lj_nrg * kJ_per_mol + + # Append the results for this pair. + pairs.append((atom_i, atom_j)) + nrg_coul_list.append(coul_nrg) + nrg_lj_list.append(lj_nrg) + + # CustomBondForce: ghost-14. + else: + # Get the parameters for this force. + parameters = [p.get("name") for p in force.find("PerBondParameters")] + + # Get all the bond parameters. + bonds = force.find("Bonds").findall("Bond") + + # Get the list of atoms. + atoms = mols.atoms() + + # Loop over all bonds. + for bond in bonds: + # Get the atoms involved in the bond. + atom_i = atoms[int(bond.get("p1"))] + atom_j = atoms[int(bond.get("p2"))] + + # Set the parameters for this bond. + setattr(module, parameters[0], float(bond.get("param1"))) + setattr(module, parameters[1], float(bond.get("param2"))) + setattr(module, parameters[2], float(bond.get("param3"))) + setattr(module, parameters[3], float(bond.get("param4"))) + setattr(module, parameters[4], float(bond.get("param5"))) + + # Get the distance between the particles. + r = measure(atom_i, atom_j).to(nanometer) + + # Evaluate the energy term by term. + for term in terms: + # Replace any instances of ^ with **. + term = term.replace("^", "**") + + # Split the term into the result and the expression. + result, expression = term.split("=") + + # Evaluate the expression. + setattr(module, result, eval(expression)) + + # Give energies units. + coul_nrg = module.coul_nrg * kJ_per_mol + lj_nrg = module.lj_nrg * kJ_per_mol + + # Append the results for this bond. + pairs.append((atom_i, atom_j)) + nrg_coul_list.append(coul_nrg) + nrg_lj_list.append(lj_nrg) + + # Return the results. + return pairs, nrg_coul_list, nrg_lj_list diff --git a/src/sire/qm/CMakeLists.txt b/src/sire/qm/CMakeLists.txt new file mode 100644 index 000000000..d2a02efb8 --- /dev/null +++ b/src/sire/qm/CMakeLists.txt @@ -0,0 +1,15 @@ +######################################## +# +# sire.qm +# +######################################## + +# Add your script to this list +set ( SCRIPTS + __init__.py + _emle.py + _utils.py + ) + +# installation +install( FILES ${SCRIPTS} DESTINATION ${SIRE_PYTHON}/sire/qm ) diff --git a/src/sire/qm/__init__.py b/src/sire/qm/__init__.py new file mode 100644 index 000000000..cb79a420a --- /dev/null +++ b/src/sire/qm/__init__.py @@ -0,0 +1,182 @@ +__all__ = ["create_engine", "emle", "zero_charge"] + +from .. import use_new_api as _use_new_api + +_use_new_api() + +from ..legacy import Convert as _Convert + +from ._emle import emle +from ._utils import _zero_charge as zero_charge + + +def create_engine( + mols, + qm_atoms, + py_object, + callback=None, + cutoff="7.5A", + neighbour_list_frequency=0, + mechanical_embedding=False, + redistribute_charge=False, + map=None, +): + """ + Create a QM engine to that can be used for QM/MM simulations with sire.mol.dynamics. + + Parameters + ---------- + + mols : sire.system.System + The molecular system. + + qm_atoms : str, int, list, molecule view/collection etc. + Any valid search string, atom index, list of atom indicies, + or molecule view/container that can be used to select + qm_atoms from 'mols'. + + py_object : object + The Python object that will contains the callback for the QM calculation. + This can be a class instance with a "callback" method, or a callable. + + callback : str, optional, default=None + The name of the callback. If None, then the py_object is assumed to + be a callable, i.e. it is itself the callback. The callback should + take the following arguments: + - numbers_qm: A list of atomic numbers for the atoms in the QM region. + - charges_mm: A list of the MM charges in mod electron charge. + - xyz_qm: A list of positions for the atoms in the QM region in Angstrom. + - xyz_mm: A list of positions for the atoms in the MM region in Angstrom. + In addition, it should return a tuple containing the following: + - energy: The QM energy in kJ/mol. + - forces_qm: A list of forces on the atoms in the QM region in kJ/mol/nm. + - forces_mm: A list of forces on the atoms in the MM region in kJ/mol/nm. + + cutoff : str or sire.legacy.Units.GeneralUnit, optional, default="7.5A" + The cutoff to use for the QM/MM calculation. + + neighbour_list_frequency : int, optional, default=0 + The frequency with which to update the neighbour list. A value of + zero means that no neighbour list will be used. + + mechanical_embedding: bool, optional, default=False + Whether to use mechanical embedding. If True, then electrostatics will + be computed at the MM level by OpenMM. Note that the signature of the + callback does not change when mechanical embedding is used, i.e. it will + take an empty lists for the MM charges and positions and return an empty + list of forces for the MM region. + + redistribute_charge : bool + Whether to redistribute charge of the QM atoms to ensure that the total + charge of the QM region is an integer. Excess charge is redistributed + over the non QM atoms within the residues involved in the QM region. + + Returns + ------- + + engine : sire.legacy.Convert._SireOpenMM.PyQMEngine + The QM engine. + """ + + from ..base import create_map as _create_map + from ..mol import selection_to_atoms as _selection_to_atoms + from ..system import System as _System + from ..legacy import Units as _Units + from ..units import angstrom as _angstrom + from .. import u as _u + + if not isinstance(mols, _System): + raise TypeError("mols must be a of type 'sire.System'") + + # Clone the system. + mols = mols.clone() + + try: + qm_atoms = _selection_to_atoms(mols, qm_atoms) + except: + raise ValueError("Unable to select 'qm_atoms' from 'mols'") + + if callback is not None: + if not isinstance(callback, str): + raise TypeError("'callback' must be of type 'str'") + if not hasattr(py_object, callback): + raise ValueError(f"'py_object' does not have a method called '{callback}'.") + else: + callback = "" + + if not isinstance(cutoff, (str, _Units.GeneralUnit)): + raise TypeError( + "cutoff must be of type 'str' or 'sire.legacy.Units.GeneralUnit'" + ) + + if isinstance(cutoff, str): + try: + cutoff = _u(cutoff) + except: + raise ValueError("Unable to parse cutoff as a GeneralUnit") + + if not cutoff.has_same_units(_angstrom): + raise ValueError("'cutoff' must be in units of length") + + if not isinstance(neighbour_list_frequency, int): + raise TypeError("'neighbour_list_frequency' must be of type 'int'") + + if neighbour_list_frequency < 0: + raise ValueError("'neighbour_list_frequency' must be >= 0") + + if not isinstance(mechanical_embedding, bool): + raise TypeError("'mechanical_embedding' must be of type 'bool'") + + if not isinstance(redistribute_charge, bool): + raise TypeError("'redistribute_charge' must be of type 'bool'") + + if map is not None: + if not isinstance(map, dict): + raise TypeError("'map' must be of type 'dict'") + map = _create_map(map) + + # Create the QM engine. + engine = _Convert.PyQMEngine( + py_object, + callback, + cutoff, + neighbour_list_frequency, + mechanical_embedding, + ) + + from ._utils import ( + _check_charge, + _create_qm_mol_to_atoms, + _configure_engine, + _create_merged_mols, + _get_link_atoms, + ) + + # Check that the charge of the QM region is integer valued. + _check_charge(mols, qm_atoms, map, redistribute_charge) + + # Get the mapping between molecule numbers and QM atoms. + qm_mol_to_atoms = _create_qm_mol_to_atoms(qm_atoms) + + # Get link atom information. + mm1_to_qm, mm1_to_mm2, bond_scale_factors, mm1_indices = _get_link_atoms( + mols, qm_mol_to_atoms, map + ) + + # Configure the engine. + engine = _configure_engine( + engine, mols, qm_atoms, mm1_to_qm, mm1_to_mm2, bond_scale_factors, map + ) + + # Create the merged molecule. + qm_mols = _create_merged_mols( + qm_mol_to_atoms, mm1_indices, mechanical_embedding, map + ) + + # Update the molecule in the system. + mols.update(qm_mols) + + # Bind the system as a private attribute of the engine. + engine._mols = mols + + return mols, engine diff --git a/src/sire/qm/_emle.py b/src/sire/qm/_emle.py new file mode 100644 index 000000000..591ac599d --- /dev/null +++ b/src/sire/qm/_emle.py @@ -0,0 +1,316 @@ +__all__ = ["emle"] + +from ..legacy import Convert as _Convert + + +class EMLEEngine(_Convert._SireOpenMM.PyQMEngine): + """A class to enable use of EMLE as a QM engine.""" + + def get_forces(self): + """ + Get the OpenMM forces for this engine. The first force is the actual + EMLE force, which uses a CustomCPPForceImpl to calculate the electrostatic + embedding force. The second is a null CustomBondForce that can be used to + add a "lambda_emle" global parameter to a context to allow the force to be + scaled. + + Returns + ------- + + emle_force : openmm.Force + The EMLE force object to compute the electrostatic embedding force. + + interpolation_force : openmm.CustomBondForce + A null CustomBondForce object that can be used to add a "lambda_emle" + global parameter to an OpenMM context. This allows the electrostatic + embedding force to be scaled. + """ + + from copy import deepcopy as _deepcopy + from openmm import CustomBondForce as _CustomBondForce + + # Create a dynamics object for the QM region. + d = self._mols["property is_perturbable"].dynamics( + timestep="1fs", + constraint="none", + platform="cpu", + qm_engine=self, + ) + + # Get the OpenMM EMLE force. + emle_force = _deepcopy(d._d._omm_mols.getSystem().getForce(0)) + + # Create a null CustomBondForce to add the EMLE interpolation + # parameter. + interpolation_force = _CustomBondForce("") + interpolation_force.addGlobalParameter("lambda_emle", 1.0) + + # Return the forces. + return emle_force, interpolation_force + + +class TorchEMLEEngine(_Convert._SireOpenMM.TorchQMEngine): + """A class to enable use of EMLE as a QM engine using C++ Torch.""" + + def get_forces(self): + """ + Get the OpenMM forces for this engine. The first force is the actual + EMLE force, which uses a CustomCPPForceImpl to calculate the electrostatic + embedding force. The second is a null CustomBondForce that can be used to + add a "lambda_emle" global parameter to a context to allow the force to be + scaled. + + Returns + ------- + + emle_force : openmm.Force + The EMLE force object to compute the electrostatic embedding force. + + interpolation_force : openmm.CustomBondForce + A null CustomBondForce object that can be used to add a "lambda_emle" + global parameter to an OpenMM context. This allows the electrostatic + embedding force to be scaled. + """ + + from copy import deepcopy as _deepcopy + from openmm import CustomBondForce as _CustomBondForce + + # Create a dynamics object for the QM region. + d = self._mols["property is_perturbable"].dynamics( + timestep="1fs", + constraint="none", + platform="cpu", + qm_engine=self, + ) + + # Get the OpenMM EMLE force. + emle_force = _deepcopy(d._d._omm_mols.getSystem().getForce(0)) + + # Create a null CustomBondForce to add the EMLE interpolation + # parameter. + interpolation_force = _CustomBondForce("") + interpolation_force.addGlobalParameter("lambda_emle", 1.0) + + # Return the forces. + return emle_force, interpolation_force + + +def emle( + mols, + qm_atoms, + calculator, + cutoff="7.5A", + neighbour_list_frequency=0, + redistribute_charge=False, + map=None, +): + """ + Create an EMLE engine object to allow QM/MM simulations using sire.mol.dynamics. + + Parameters + ---------- + + mols : sire.system.System + The molecular system. + + qm_atoms : str, int, list, molecule view/collection etc. + Any valid search string, atom index, list of atom indicies, + or molecule view/container that can be used to select + qm_atoms from 'mols'. + + calculator : emle.calculator.EMLECalculator, emle.models.EMLE + The EMLE calculator or model to use for elecotrostatic embedding + calculations. + + cutoff : str or sire.legacy.Units.GeneralUnit, optional, default="7.5A" + The cutoff to use for the QM/MM calculation. + + neighbour_list_frequency : int, optional, default=0 + The frequency with which to update the neighbour list. A value of + zero means that no neighbour list will be used. + + redistribute_charge : bool + Whether to redistribute charge of the QM atoms to ensure that the total + charge of the QM region is an integer. Excess charge is redistributed + over the non QM atoms within the residues involved in the QM region. + + Returns + ------- + + engine : sire.qm.EMLEEngine + The EMLE engine object. + """ + + try: + from emle.calculator import EMLECalculator as _EMLECalculator + except: + raise ImportError( + "Could not import emle. Please install emle-engine and try again." + ) + + try: + import torch as _torch + from emle.models import EMLE as _EMLE + + has_model = True + except: + has_model = False + + from ..base import create_map as _create_map + from ..mol import selection_to_atoms as _selection_to_atoms + from ..system import System as _System + from ..legacy import Units as _Units + from ..units import angstrom as _angstrom + from .. import u as _u + + if not isinstance(mols, _System): + raise TypeError("mols must be a of type 'sire.System'") + + # Clone the system. + mols = mols.clone() + + try: + qm_atoms = _selection_to_atoms(mols, qm_atoms) + except: + raise ValueError("Unable to select 'qm_atoms' from 'mols'") + + if has_model: + # EMLECalculator. + if isinstance(calculator, _EMLECalculator): + pass + # EMLE model. Note that TorchScript doesn't support inheritance, so + # we need to check whether this is a torch.nn.Module and whether it + # has the "_is_emle" attribute, which is added to all EMLE models. + elif isinstance(calculator, _torch.nn.Module) and hasattr( + calculator, "_is_emle" + ): + pass + else: + raise TypeError( + "'calculator' must be a of type 'emle.calculator.EMLECalculator' or 'emle.models.EMLE'" + ) + else: + if not isinstance(calculator, _EMLECalculator): + raise TypeError( + "'calculator' must be a of type 'emle.calculator.EMLECalculator'" + ) + + if not isinstance(cutoff, (str, _Units.GeneralUnit)): + raise TypeError( + "cutoff must be of type 'str' or 'sire.legacy.Units.GeneralUnit'" + ) + + if isinstance(cutoff, str): + try: + cutoff = _u(cutoff) + except: + raise ValueError("Unable to parse cutoff as a GeneralUnit") + + if not cutoff.has_same_units(_angstrom): + raise ValueError("'cutoff' must be in units of length") + + if not isinstance(neighbour_list_frequency, int): + raise TypeError("'neighbour_list_frequency' must be of type 'int'") + + if neighbour_list_frequency < 0: + raise ValueError("'neighbour_list_frequency' must be >= 0") + + if not isinstance(redistribute_charge, bool): + raise TypeError("'redistribute_charge' must be of type 'bool'") + + if map is not None: + if not isinstance(map, dict): + raise TypeError("'map' must be of type 'dict'") + map = _create_map(map) + + # Create an engine from an EMLE calculator. + if isinstance(calculator, _EMLECalculator): + # Determine the callback name. Use an optimised version of the callback + # if the user has specified "torchani" as the backend and is using + # "electrostatic" embedding. + if calculator._backend == "torchani" and calculator._method == "electrostatic": + try: + from emle.models import ANI2xEMLE as _ANI2xEMLE + + callback = "_sire_callback_optimised" + except: + callback = "_sire_callback" + else: + callback = "_sire_callback" + + # Create the EMLE engine. + engine = EMLEEngine( + calculator, + callback, + cutoff, + neighbour_list_frequency, + False, + ) + + # Create an engine from an EMLE model. + else: + try: + from emle.models import EMLE as _EMLE + except: + raise ImportError( + "Could not import emle.models. Please reinstall emle-engine and try again." + ) + + import torch as _torch + + try: + script_module = _torch.jit.script(calculator) + except: + raise ValueError( + "Unable to compile the EMLE model to a TorchScript module." + ) + + # Save the script module to a file. + module_path = calculator.__class__.__name__ + ".pt" + _torch.jit.save(script_module, calculator.__class__.__name__ + ".pt") + + try: + # Create the EMLE engine. + engine = TorchEMLEEngine( + module_path, + cutoff, + neighbour_list_frequency, + False, + ) + except Exception as e: + raise ValueError("Unable to create a TorchEMLEEngine: " + str(e)) + + from ._utils import ( + _check_charge, + _create_qm_mol_to_atoms, + _configure_engine, + _create_merged_mols, + _get_link_atoms, + ) + + # Check that the charge of the QM region is integer valued. + _check_charge(mols, qm_atoms, map, redistribute_charge) + + # Get the mapping between molecule numbers and QM atoms. + qm_mol_to_atoms = _create_qm_mol_to_atoms(qm_atoms) + + # Get link atom information. + mm1_to_qm, mm1_to_mm2, bond_scale_factors, mm1_indices = _get_link_atoms( + mols, qm_mol_to_atoms, map + ) + + # Configure the engine. + engine = _configure_engine( + engine, mols, qm_atoms, mm1_to_qm, mm1_to_mm2, bond_scale_factors, map + ) + + # Create the merged molecule. + qm_mols = _create_merged_mols(qm_mol_to_atoms, mm1_indices, False, map) + + # Update the molecule in the system. + mols.update(qm_mols) + + # Bind the system as a private attribute of the engine. + engine._mols = mols + + return mols, engine diff --git a/src/sire/qm/_utils.py b/src/sire/qm/_utils.py new file mode 100644 index 000000000..5dfe95455 --- /dev/null +++ b/src/sire/qm/_utils.py @@ -0,0 +1,807 @@ +def _zero_charge(mols, qm_atoms, map=None): + """ + Zero the charge for the QM atoms in the system. + + Parameters + ---------- + + mols : sire.system.System + The molecular system. + + qm_atoms : str, int, list, molecule view/collection etc. + Any valid search string, atom index, list of atom indicies, + or molecule view/container that can be used to select + qm_atoms from 'mols'. + + Returns + ------- + + mols : sire.system.System + The molecular system with the QM atom charges zeroed. + """ + + from ..base import create_map as _create_map + from ..mol import selection_to_atoms as _selection_to_atoms + from ..morph import extract_reference as _extract_reference + from ..units import e_charge as _e_charge + + # Clone the molecules. + mols = mols.clone() + + # Try to extract the reference state. + try: + mols = _extract_reference(mols) + # This is a regular molecule, so pass. + except: + pass + + try: + qm_atoms = _selection_to_atoms(mols, qm_atoms) + except: + raise ValueError("Unable to select 'qm_atoms' from 'mols'") + + if map is not None: + if not isinstance(map, dict): + raise TypeError("'map' must be of type 'dict'") + map = _create_map(map) + + # Create a dictionary mapping molecular numbers to QM atoms. + qm_mol_to_atoms = _create_qm_mol_to_atoms(qm_atoms) + + # Get the charge property. + charge_prop = map["charge"].source() + + # Loop over the molecules. + for mol_num, qm_atoms in qm_mol_to_atoms.items(): + # Create a cursor for the molecule. + cursor = mols[mol_num].cursor() + + # Zero the charges for the QM atoms. + for atom in qm_atoms: + cursor[atom][charge_prop] = 0.0 * _e_charge + + # Commit the changes. + mols.update(cursor.commit()) + + return mols + + +def _check_charge(mols, qm_atoms, map, redistribute_charge=False, tol=1e-6): + """ + Internal helper function to check that the QM region has integer charge. + + Parameters + ---------- + + mols : sire.system.System + The system containing the QM atoms. + + qm_atoms: [sire.legacy.Mol.AtomIdx] + A list of QM atoms. + + redistribute_charge: bool + Whether to redistribute charge to ensure that the QM region has an + integer charge. + + map: sire.legacy.Base.PropertyMap + The property map for the molecule. + + tol: float + The tolerance for the charge check. + + Returns + ------- + + Raises + ------ + + Exception + If the charge of the QM region is not an integer and charge redistribution + is not allowed. + + """ + + import math as _math + + from sire.units import e_charge as _e_charge + + # Get the charge property. + charge_prop = map["charge"].source() + + # Work out the charge of the QM atoms. + qm_charge = 0 + for atom in qm_atoms: + qm_charge += atom.property(charge_prop).value() + + # Check that the charge is an integer. + if _math.isclose(qm_charge, round(qm_charge), abs_tol=tol): + return + else: + if redistribute_charge: + # Find the residues containing the QM atoms. + residues = qm_atoms.residues() + + # Work out the fractional excess charge to the nearest integer. + excess_charge = (round(qm_charge) - qm_charge) * _e_charge + + # Redistribute the charge over the QM atoms. + qm_frac = excess_charge / len(qm_atoms) + + # Redistribute the charge over the non QM atoms. + rem_frac = excess_charge / (residues.num_atoms() - len(qm_atoms)) + + # Loop over the residues. + for res in residues: + # Get the molecule from the system. + mol = mols[res.molecule()] + + # Create a cursor for the molecule. + cursor = mol.cursor() + + # Loop over the atoms in the residue. + for atom in res: + # Shift the charge. + if atom in qm_atoms: + cursor[atom][charge_prop] -= qm_frac + else: + cursor[atom][charge_prop] += rem_frac + + # Commit the changes. + mol = cursor.commit() + + # Update the molecule in the system. + mols.update(mol) + + else: + raise Exception( + f"Charge of the QM region ({qm_charge:.5f}) is not an integer!" + ) + + +def _create_qm_mol_to_atoms(qm_atoms): + """ + Internal helper function to create a mapping between molecule numbers and + a list of QM atoms. + + Parameters + ---------- + + qm_atoms: [sire.legacy.Mol.AtomIdx] + A list of QM atoms. + + Returns + ------- + + qm_mol_to_atoms: {int: [sire.legacy.Mol.AtomIdx]} + A dictionary with molecule numbers as keys and a list of QM atoms as + values. + """ + qm_mol_to_atoms = {} + for atom in qm_atoms: + mol_num = atom.molecule().number() + if mol_num not in qm_mol_to_atoms: + qm_mol_to_atoms[mol_num] = [atom] + else: + qm_mol_to_atoms[mol_num].append(atom) + + return qm_mol_to_atoms + + +def _check_qm_atom_bonds(mol, atom, qm_idxs, map): + """ + Internal helper function to check the bonding for QM atoms. + + Parameters + ---------- + + mol: sire.legacy.Mol.Molecule + The molecule containing the QM atoms. + + atom: sire.legacy.Mol.Atom + The QM atom to check the bonding for. + + qm_idxs: [sire.legacy.Mol.AtomIdx] + The indices of the QM atoms. + + map: sire.legacy.Base.PropertyMap + The property map for the molecule. + + Returns + ------- + + mm_atoms: [sire.legacy.Mol.AtomIdx] + A list of MM atoms that are bonded to the QM atom. + + has_qm_bond: bool + A flag to indicate if the QM atom has a bond to another QM atom. + """ + + # Get the bonds for the molecule. + bonds = mol.property(map["bond"]).potentials() + + # Store the info for the molecule. + info = mol.info() + + # Store the cut-group atom index pair. + cg_atom_idx = info.cg_atom_idx(atom.index()) + + # Initialise a list to store the MM atoms. + mm_atoms = [] + + # A flag to indicate if the atom has a bond to another QM atom. + has_qm_bond = False + + # Loop over all of the bonds. + for bond in bonds: + # Get the indices of the atoms in the bond. + idx0 = bond.atom0() + idx1 = bond.atom1() + + # Work out which is the other atom in the bond. + if idx0 == cg_atom_idx: + idx = idx1 + elif idx1 == cg_atom_idx: + idx = idx0 + else: + continue + + # Convert to an atom index. + idx = info.atom_idx(idx) + + # The atom is not in the QM region. + if idx not in qm_idxs: + mm_atoms.append(idx) + else: + has_qm_bond = True + + return mm_atoms, has_qm_bond + + +def _get_link_atoms(mols, qm_mol_to_atoms, map): + """ + Internal helper function to get a dictionary with link atoms for each QM atom. + + Parameters + ---------- + + mols: sire.legacy.System.System + The Sire system containing the QM atoms. + + qm_mol_to_atoms: {sire.legacy.Mol.MolNum: [sire.legacy.Mol.AtomIdx]} + A dictionary with molecule numbers as keys and a list of QM atoms as + values. + + map: sire.legacy.Base.PropertyMap + The property map for the system. + + Returns + + mm1_to_qm: {sire.legacy.Mol.AtomIdx: sire.legacy.Mol.AtomIdx} + A dictionary with link atoms as keys and QM atoms as values. + + mm1_to_mm2: {sire.legacy.Mol.AtomIdx: [sire.legacy.Mol.AtomIdx]} + A dictionary with link atoms as keys and a list of MM atoms as values. + + bond_scale_factors: {sire.legacy.Mol.AtomIdx: float} + A dictionary with link atoms as keys and bond scale factors as values. + + mm1_indices: [[sire.legacy.Mol.AtomIdx]] + A list of lists of MM1 atom indices. + """ + + import warnings as _warnings + + from ..legacy import CAS as _CAS + from ..legacy import Mol as _Mol + from ..legacy import MM as _MM + + # Initialise the dictionaries. + + # List of MM1 atom indices as sire.legacy.Mol.AtomIdx objects. + mm1_indices = [] + + # Link atoms to QM atoms. + mm1_to_qm = {} + + # Link atoms to MM atoms. + mm1_to_mm2 = {} + + # QM to link atom bond scale factors. These are the ratios of the equilibrium + # bond lengths for the QM-L bonds and the QM-MM1 bonds, taken from the MM + # bond potentials, i.e. R0(QM-L) / R0(QM-MM1). + bond_scale_factors = {} + + # Store carbon and hydrogen elements. + carbon = _Mol.Element("C") + hydrogen = _Mol.Element("H") + + # Store the element property. + elem_prop = map["element"] + + # Loop over all molecules containing QM atoms. + for mol_num, qm_atoms in qm_mol_to_atoms.items(): + # Get the molecule. + qm_mol = qm_atoms[0].molecule() + + # Store the indices of the QM atoms. + qm_idxs = [atom.index() for atom in qm_atoms] + + # Create a connectivity object. + connectivity = _Mol.Connectivity(qm_mol, _Mol.CovalentBondHunter(), map) + + # Dictionary to store the MM1 atoms. + mm1_atoms = {} + + # Loop over the QM atoms and find any MM atoms that are bonded to them. + for atom in qm_atoms: + # Store the atom index. + idx = atom.index() + + # Store the element of the atom. + elem = atom.property(elem_prop) + + # Check the bonding for this atom. + mm_atoms, has_qm_bond = _check_qm_atom_bonds(qm_mol, atom, qm_idxs, map) + + # If there are no QM bonds for this atom, raise an exception. + if not has_qm_bond: + raise ValueError( + f"Atom {idx} in the QM region has no bonds to other QM atoms!" + ) + + # Store the list of MM atoms. + if len(mm_atoms) > 0: + if len(mm_atoms) > 1: + raise Exception(f"QM atom {idx} has more than one MM bond!") + else: + # Get the element of the cut atom. + link_elem = qm_mol[mm_atoms[0]].property(elem_prop) + + # If the element is hydrogen, raise an exception. + if elem == hydrogen: + abs_idx = mols.atoms().find(atom) + raise Exception( + "Attempting replace a hydrogen with a link atom " + f"(atom index {abs_idx})!" + ) + + # Warn if the link atom is not for a carbon-carbon bond. + if elem != carbon or link_elem != carbon: + abs_idx = mols.atoms().find(atom) + _warnings.warn( + "Attempting to add a link atom for a non carbon-carbon " + f"bond (atom index {abs_idx})!" + ) + + # Store the link (MM1) atom. + mm1_atoms[idx] = mm_atoms[0] + + # Now work out the MM atoms that are bonded to the link atoms. (MM2 atoms.) + mm2_atoms = {} + for qm_idx, mm1_idx in mm1_atoms.items(): + if mm1_idx not in mm2_atoms: + bonds = connectivity.get_bonds(mm1_idx) + mm_bonds = [] + for bond in bonds: + idx0 = bond.atom0() + idx1 = bond.atom1() + if idx0 != mm1_idx: + bond_idx = idx0 + else: + bond_idx = idx1 + if bond_idx not in qm_idxs: + mm_bonds.append(bond_idx) + mm2_atoms[mm1_idx] = mm_bonds + + # Convert MM1 to QM atom dictionary to absolute indices. + mm1_to_qm_local = {} + for k, v in mm1_atoms.items(): + qm_idx = mols.atoms().find(qm_mol.atoms()[k]) + link_idx = mols.atoms().find(qm_mol.atoms()[v]) + # Make sure that we haven't assigned this link atom already. + if link_idx in mm1_to_qm_local or link_idx in mm1_to_qm: + raise Exception( + f"Cannot substitue the same MM atom (index {link_idx}) " + "for more than one link atom!" + ) + mm1_to_qm_local[link_idx] = qm_idx + + # Convert MM1 to MM2 atom dictionary to absolute indices. + mm1_to_mm2_local = {} + for k, v in mm2_atoms.items(): + link_idx = mols.atoms().find(qm_mol.atoms()[k]) + mm_idx = [mols.atoms().find(qm_mol.atoms()[x]) for x in v] + mm1_to_mm2_local[link_idx] = mm_idx + + # Store the MM1 atom indices. + mm1_indices.append(list(mm1_atoms.values())) + + # Now work out the QM-MM1 bond distances based on the equilibrium + # MM bond lengths. + + # A dictionary to store the bond lengths. Here 'bond_lengths' are the + # equilibrium bond lengths for the QM-MM1 bonds, and 'link_bond_lengths', + # are the equilibrium bond lengths for the QM-L (QM-link) bonds. Both of + # these are evaluated from the MM bond potentials. + bond_lengths = {} + link_bond_lengths = {} + + # Get the MM bond potentials. + bonds = qm_mol.property(map["bond"]).potentials() + + # Store the info for the QM molecule. + info = qm_mol.info() + + # Store the bond potential symbol. + r = _CAS.Symbol("r") + + # Loop over the link atoms. + for qm_idx, mm1_idx in mm1_atoms.items(): + # Convert to cg_atom_idx objects. + cg_qm_idx = info.cg_atom_idx(qm_idx) + cg_mm1_idx = info.cg_atom_idx(mm1_idx) + + # Store the element of the QM atom. + qm_elem = qm_mol[cg_qm_idx].element() + hydrogen = _Mol.Element("H") + + qm_m1_bond_found = False + qm_link_bond_found = False + + # Loop over the bonds. + for bond in bonds: + # Get the indices of the atoms in the bond. + bond_idx0 = bond.atom0() + bond_idx1 = bond.atom1() + + # If the bond is between the QM atom and the MM atom, store the + # bond length. + if ( + not qm_m1_bond_found + and cg_qm_idx == bond_idx0 + and cg_mm1_idx == bond_idx1 + or cg_qm_idx == bond_idx1 + and cg_mm1_idx == bond_idx0 + ): + # Cast as an AmberBond. + ab = _MM.AmberBond(bond.function(), r) + bond_lengths[mm1_idx] = ab.r0() + qm_m1_bond_found = True + if qm_link_bond_found: + break + else: + elem0 = qm_mol[bond_idx0].element() + elem1 = qm_mol[bond_idx1].element() + + # Is this bond is between a hydrogen and and the same element + # as the QM atom? If so, store the bond length. + if ( + not qm_link_bond_found + and elem0 == hydrogen + and elem1 == qm_elem + or elem0 == qm_elem + and elem1 == hydrogen + ): + # Cast as an AmberBond. + ab = _MM.AmberBond(bond.function(), r) + link_bond_lengths[mm1_idx] = ab.r0() + qm_link_bond_found = True + if qm_m1_bond_found: + break + + # Work out the bond scale factors: R0(QM-L) / R0(QM-MM1) + try: + bond_scale_factors_local = {} + for idx in bond_lengths: + abs_idx = mols.atoms().find(qm_mol.atoms()[idx]) + bond_scale_factors_local[abs_idx] = ( + link_bond_lengths[idx] / bond_lengths[idx] + ) + except: + raise Exception( + f"Unable to compute the scaled the QM-MM1 bond lengths for MM1 atom {idx}!" + ) + + # Update the dictionaries. + mm1_to_qm.update(mm1_to_qm_local) + mm1_to_mm2.update(mm1_to_mm2_local) + bond_scale_factors.update(bond_scale_factors_local) + + return mm1_to_qm, mm1_to_mm2, bond_scale_factors, mm1_indices + + +def _create_merged_mols(qm_mol_to_atoms, mm1_indices, mechanical_embedding, map): + """ + Internal helper function to create a merged molecule from the QM molecule. + + Parameters + ---------- + + qm_mol_to_atoms: {sire.legacy.Mol.MolNum: [sire.legacy.Mol.AtomIdx]} + A dictionary with molecule numbers as keys and a list of QM atoms as + values. + + mm1_indices: [[sire.legacy.Mol.AtomIdx]] + A list of lists of MM1 atom indices. + + mechanical_embedding: bool + Whether mechanical embedding is being used. + + map: sire.legacy.Base.PropertyMap + The property map for the system. + + Returns + ------- + + qm_mols: [sire.legacy.Mol.Molecule] + A list of merged molecules. + """ + + from ..legacy import Mol as _Mol + from ..legacy import MM as _MM + from ..morph import link_to_reference as _link_to_reference + + # Initialise a list to store the merged molecules. + qm_mols = [] + + # Loop over all molecules containing QM atoms. + for (mol_num, qm_atoms), mm1_idxs in zip(qm_mol_to_atoms.items(), mm1_indices): + # Get the molecule. + qm_mol = qm_atoms[0].molecule() + + # Store the indices of the QM atoms. + qm_idxs = [atom.index() for atom in qm_atoms] + + # Get the user defined names for the properties that we need to + # merge. + bond_prop = map["bond"] + angle_prop = map["angle"] + dihedral_prop = map["dihedral"] + improper_prop = map["improper"] + charge_prop = map["charge"] + connectivity_prop = map["connectivity"] + intrascale_prop = map["intrascale"] + + # Get the molecular info object. + info = qm_mol.info() + + # Make an editable version of the molecule. + edit_mol = qm_mol.edit() + + for prop in qm_mol.property_keys(): + # For all bonded properties we copy the MM terms to the lambda = 0 + # (MM) end state, create a null set of terms for the lambda = 1 (QM) + # end state for any terms that only involve QM atoms, then remove + # the existing property. Charges also zeroed for the MM end state. + # All other properties remain the same in both states. + + # Bonds. + if prop == bond_prop: + edit_mol = edit_mol.set_property( + prop + "0", qm_mol.property(prop) + ).molecule() + + bonds = _MM.TwoAtomFunctions(info) + + for bond in qm_mol.property(prop).potentials(): + atom0 = info.atom_idx(bond.atom0()) + atom1 = info.atom_idx(bond.atom1()) + + # This bond doesn't only involve QM atoms. + if atom0 not in qm_idxs or atom1 not in qm_idxs: + bonds.set(atom0, atom1, bond.function()) + + edit_mol = edit_mol.set_property(prop + "1", bonds).molecule() + edit_mol = edit_mol.remove_property(prop).molecule() + + # Angles. + elif prop == angle_prop: + edit_mol = edit_mol.set_property( + prop + "0", qm_mol.property(prop) + ).molecule() + + angles = _MM.ThreeAtomFunctions(info) + + for angle in qm_mol.property(prop).potentials(): + atom0 = info.atom_idx(angle.atom0()) + atom1 = info.atom_idx(angle.atom1()) + atom2 = info.atom_idx(angle.atom2()) + + # This angle doesn't only involve QM atoms. + if ( + atom0 not in qm_idxs + or atom1 not in qm_idxs + or atom2 not in qm_idxs + ): + angles.set(atom0, atom1, atom2, angle.function()) + + edit_mol = edit_mol.set_property(prop + "1", angles).molecule() + edit_mol = edit_mol.remove_property(prop).molecule() + + # Dihedrals. + elif prop == dihedral_prop: + edit_mol = edit_mol.set_property( + prop + "0", qm_mol.property(prop) + ).molecule() + + dihedrals = _MM.FourAtomFunctions(info) + + for dihedral in qm_mol.property(prop).potentials(): + atom0 = info.atom_idx(dihedral.atom0()) + atom1 = info.atom_idx(dihedral.atom1()) + atom2 = info.atom_idx(dihedral.atom2()) + atom3 = info.atom_idx(dihedral.atom3()) + + # This dihedral doesn't only involve QM atoms. + if ( + atom0 not in qm_idxs + or atom1 not in qm_idxs + or atom2 not in qm_idxs + or atom3 not in qm_idxs + ): + dihedrals.set(atom0, atom1, atom2, atom3, dihedral.function()) + + edit_mol = edit_mol.set_property(prop + "1", dihedrals).molecule() + edit_mol = edit_mol.remove_property(prop).molecule() + + # Impropers. + elif prop == improper_prop: + edit_mol = edit_mol.set_property( + prop + "0", qm_mol.property(prop) + ).molecule() + + impropers = _MM.FourAtomFunctions(info) + + for improper in qm_mol.property(prop).potentials(): + atom0 = info.atom_idx(improper.atom0()) + atom1 = info.atom_idx(improper.atom1()) + atom2 = info.atom_idx(improper.atom2()) + atom3 = info.atom_idx(improper.atom3()) + + # This improper doesn't only involve QM atoms. + if ( + atom0 not in qm_idxs + or atom1 not in qm_idxs + or atom2 not in qm_idxs + or atom3 not in qm_idxs + ): + impropers.set(atom0, atom1, atom2, atom3, improper.function()) + + edit_mol = edit_mol.set_property(prop + "1", impropers).molecule() + edit_mol = edit_mol.remove_property(prop).molecule() + + # Charge. + elif not mechanical_embedding and prop == charge_prop: + edit_mol = edit_mol.set_property( + prop + "0", qm_mol.property(prop) + ).molecule() + + charges = _Mol.AtomCharges(info) + + # Set the charge for all non-QM and non-MM1 atoms to the MM value. + for atom in qm_mol.atoms(): + idx = info.atom_idx(atom.index()) + if idx not in qm_idxs and idx not in mm1_idxs: + idx = info.cg_atom_idx(idx) + charges.set(idx, atom.property(prop)) + + edit_mol = edit_mol.set_property(prop + "1", charges).molecule() + edit_mol = edit_mol.remove_property(prop).molecule() + + # Connectivity. + elif prop == connectivity_prop: + pass + + # Intrascale. + elif prop == intrascale_prop: + # We need to remove intramolecular non-bonded exceptions between QM atoms. + + # Get the existing property. + intrascale = qm_mol.property(prop) + + # Set as the lambda = 0 (MM) end state. + edit_mol = edit_mol.set_property(prop + "0", intrascale).molecule() + + # Zero the scale factors for all QM-QM interactions. + for idx in qm_idxs: + for idx2 in qm_idxs: + intrascale.set(idx, idx2, _MM.CLJScaleFactor(0, 0)) + + # Set as the lambda = 1 (QM) end state. + edit_mol = edit_mol.set_property(prop + "1", intrascale).molecule() + + # Remove the existing property. + edit_mol = edit_mol.remove_property(prop).molecule() + + # All other properties remain the same in both end states. + else: + edit_mol = edit_mol.set_property( + prop + "0", qm_mol.property(prop) + ).molecule() + edit_mol = edit_mol.set_property( + prop + "1", qm_mol.property(prop) + ).molecule() + + edit_mol = edit_mol.remove_property(prop).molecule() + + # Flag the molecule as perturbable. + edit_mol = edit_mol.set_property(map["is_perturbable"], True).molecule() + + # Commit the changes. + qm_mol = edit_mol.commit() + + # Link to the perturbation to the reference state. + qm_mol = _link_to_reference(qm_mol) + + # Add the molecule to the list. + qm_mols.append(qm_mol) + + # Return the merged molecule. + return qm_mols + + +def _configure_engine(engine, mols, qm_atoms, mm1_to_qm, mm1_to_mm2, bond_lengths, map): + """ + Internal helper function to configure a QM engine ready for dynamics. + + Parameters + ---------- + + engine: sire.legacy.QM.Engine + The QM engine to configure. + + mols: sire.legacy.System.System + The Sire system containing the QM atoms. + + qm_atoms: [sire.legacy.Mol.AtomIdx] + A list of QM atoms. + + mm1_to_qm: {sire.legacy.Mol.AtomIdx: sire.legacy.Mol.AtomIdx} + A dictionary with link atoms as keys and QM atoms as values. + + mm1_to_mm2: {sire.legacy.Mol.AtomIdx: [sire.legacy.Mol.AtomIdx]} + A dictionary with link atoms as keys and a list of MM atoms as values. + + bond_lengths: {sire.legacy.Mol.AtomIdx: float} + A dictionary with link atoms as keys and bond lengths as values. + + map: sire.legacy.Base.PropertyMap + The property map for the system. + + Returns + ------- + + engine: sire.legacy.Convert.PyQMEngine + The configured QM engine. + """ + + # Work out the indices of the QM atoms. + try: + idxs = mols.atoms().find(qm_atoms) + engine.set_atoms(idxs) + except: + raise Exception("Unable to set atom indices for the QM region.") + + # Work out the atomic numbers of the QM atoms. + try: + elem_prop = map["element"] + numbers = [atom.property(f"{elem_prop}").num_protons() for atom in qm_atoms] + engine.set_numbers(numbers) + except: + raise Exception("Unable to set atomic numbers for the QM region.") + + # Work out the atomic charge for all atoms in the system. + try: + charge_prop = map["charge"] + charges = [atom.property(f"{charge_prop}").value() for atom in mols.atoms()] + engine.set_charges(charges) + except: + raise Exception("Unable to set atomic charges for the system.") + + # Set the link atom information. + try: + engine.set_link_atoms(mm1_to_qm, mm1_to_mm2, bond_lengths) + except: + raise Exception("Unable to set link atom information.") + + return engine diff --git a/src/sire/system/_system.py b/src/sire/system/_system.py index 9f00b81ea..e2bd72a7f 100644 --- a/src/sire/system/_system.py +++ b/src/sire/system/_system.py @@ -624,6 +624,18 @@ def dynamics(self, *args, **kwargs): that should be fixed in place during the simulation. These atoms will not be moved by dynamics. + qm_engine: + A sire.qm.QMMMEngine object to used to compute QM/MM forces + and energies on a subset of the atoms in the system. + + lambda_interpolate: float + The lambda value at which to interpolate the QM/MM forces and + energies, which can be used to perform end-state correction + simulations. A value of 1.0 is full QM, whereas a value of 0.0 is + full MM. If two values are specified, then lambda will be linearly + interpolated between the two values over the course of the + simulation, which lambda updated at the energy_frequency. + platform: str The name of the OpenMM platform on which to run the dynamics, e.g. "CUDA", "OpenCL", "Metal" etc. diff --git a/tests/convert/test_openmm_constraints.py b/tests/convert/test_openmm_constraints.py index a8cf77371..c7427391a 100644 --- a/tests/convert/test_openmm_constraints.py +++ b/tests/convert/test_openmm_constraints.py @@ -1,5 +1,6 @@ import sire as sr import pytest +import platform @pytest.mark.skipif( @@ -451,3 +452,77 @@ def test_auto_constraints(ala_mols, openmm_platform): constraint[0].atom(0).index(), constraint[0].atom(1).index() ) assert bond in constrained + + +@pytest.mark.skipif( + "openmm" not in sr.convert.supported_formats(), + reason="openmm support is not available", +) +@pytest.mark.skipif( + platform.system() != "Linux", + reason="XMLSerializer doesn't preserve precision on Windows and macOS", +) +def test_asymmetric_constraints(merged_ethane_methanol): + # Check that constraints are updated correctly when the end states have + # different constraints. + + from math import isclose + from openmm import XmlSerializer + from tempfile import NamedTemporaryFile + + # Extract the molecule. + mol = merged_ethane_methanol.clone()[0] + mol = sr.morph.link_to_reference(mol) + + # Create dynamics objects for the forward and backward perturbations. + d_forwards = mol.dynamics( + perturbable_constraint="h_bonds_not_heavy_perturbed", + dynamic_constraints=True, + include_constrained_energies=False, + platform="Reference", + ) + d_backwards = mol.dynamics( + perturbable_constraint="h_bonds_not_heavy_perturbed", + include_constrained_energies=False, + dynamic_constraints=True, + swap_end_states=True, + platform="Reference", + ) + + # Set lambda so the dynamics states are equivalent. + d_forwards.set_lambda(1.0, update_constraints=True) + d_backwards.set_lambda(0.0, update_constraints=True) + + # Serialise the systems in the contexts. + xml0 = NamedTemporaryFile() + xml1 = NamedTemporaryFile() + with open(xml0.name, "w") as f: + f.write(XmlSerializer.serialize(d_forwards._d._omm_mols.getSystem())) + with open(xml1.name, "w") as f: + f.write(XmlSerializer.serialize(d_backwards._d._omm_mols.getSystem())) + + # Load the serialised systems and sort. + with open(xml0.name, "r") as f: + xml0_lines = sorted(f.readlines()) + with open(xml1.name, "r") as f: + xml1_lines = sorted(f.readlines()) + + nrg_forwards = d_forwards.current_potential_energy().value() + nrg_backwards = d_backwards.current_potential_energy().value() + + # Check the potential energies are the same. + assert isclose(nrg_forwards, nrg_backwards, rel_tol=1e-5) + + # Minimise both dynamics objects. + d_forwards.minimise() + d_backwards.minimise() + + # Check the serialised systems are the same. + assert xml0_lines == xml1_lines + + # Now get the final potential energies. (Post constraint projection.) + nrg_forwards = d_forwards.current_potential_energy().value() + nrg_backwards = d_backwards.current_potential_energy().value() + + # Check the minimised potential energies are the same. (Post constraint projection.) + assert isclose(nrg_forwards, nrg_backwards, rel_tol=1e-4) diff --git a/tests/convert/test_openmm_restraints.py b/tests/convert/test_openmm_restraints.py index 7f2b5ee2e..c7dcd8bf7 100644 --- a/tests/convert/test_openmm_restraints.py +++ b/tests/convert/test_openmm_restraints.py @@ -6,8 +6,12 @@ "openmm" not in sr.convert.supported_formats(), reason="openmm support is not available", ) -def test_openmm_positional_restraints(kigaki_mols, openmm_platform): - mols = kigaki_mols +@pytest.mark.parametrize("molecules", ["kigaki_mols", "merged_ethane_methanol"]) +def test_openmm_positional_restraints(molecules, openmm_platform, request): + mols = request.getfixturevalue(molecules) + + if mols[0].is_perturbable(): + mols = sr.morph.link_to_reference(mols) mol = mols[0] diff --git a/tests/io/test_grotop.py b/tests/io/test_grotop.py index 5a23a87e4..09d93a603 100644 --- a/tests/io/test_grotop.py +++ b/tests/io/test_grotop.py @@ -39,3 +39,131 @@ def test_posre(): # Make sure we can parse a file with BioSimSpace position restraint include # directives. mols = sr.load_test_files("posre.top") + + +def test_fep_atoms(): + """ + Test that GROMACS FEP atomtypes and atoms are created correctly. + """ + + import os + import tempfile + + atomtypes = """[ atomtypes ] + ; name at.num mass charge ptype sigma epsilon + C1 6 12.010700 0.000000 A 0.348065 0.363503 + C1_du 0 0.000000 0.000000 A 0.348065 0.000000 + C2 6 12.010700 0.000000 A 0.337953 0.455389 + H1 1 1.007940 0.000000 A 0.110343 0.058956 + H1_du 0 0.000000 0.000000 A 0.110343 0.000000 + H2 1 1.007940 0.000000 A 0.257258 0.065318 + H2_du 0 0.000000 0.000000 A 0.264454 0.000000 + H2_dux 0 0.000000 0.000000 A 0.257258 0.000000 + H3 1 1.007940 0.000000 A 0.264454 0.066021 + H3_du 0 0.000000 0.000000 A 0.258323 0.000000 + H4 1 1.007940 0.000000 A 0.258323 0.068656 + H5 1 1.007940 0.000000 A 0.245363 0.054840 + N1 7 14.006700 0.000000 A 0.320688 0.701621 + O1 8 15.999400 0.000000 A 0.303981 0.879502 + O1_du 0 0.000000 0.000000 A 0.303981 0.000000 + O2 8 15.999400 0.000000 A 0.302511 0.704858 + """ + + atoms = """[ atoms ] + ; nr type0 resnr residue atom cgnr charge0 mass0 type1 charge1 mass1 + 1 O1 1 LIG O1x 1 -0.803190 15.999430 O1_du 0.000000 15.999430 + 2 C1 1 LIG C1x 2 0.916780 12.010780 N1 -0.662150 14.006720 + 3 O1 1 LIG O2x 3 -0.803190 15.999430 O1_du 0.000000 15.999430 + 4 C1 1 LIG C2x 4 0.082410 12.010780 C1 -0.105710 12.010780 + 5 N1 1 LIG N1x 5 -0.564860 14.006720 N1 -0.484790 14.006720 + 6 H1 1 LIG H1x 6 0.449360 1.007947 H1 0.407460 1.007947 + 7 C1 1 LIG C3x 7 0.061040 12.010780 C1 0.001920 12.010780 + 8 C1 1 LIG C4x 8 -0.087870 12.010780 C1 -0.087450 12.010780 + 9 C1 1 LIG C5x 9 -0.101450 12.010780 N1 -0.015090 14.006720 + 10 C1 1 LIG C6x 10 -0.165020 12.010780 C1 -0.045730 12.010780 + 11 H2 1 LIG H2x 11 0.106400 1.007947 H5 0.196160 1.007947 + 12 C1 1 LIG C7x 12 -0.107040 12.010780 C1_du 0.000000 12.010780 + 13 H2 1 LIG H3x 13 0.120980 1.007947 H2_dux 0.000000 1.007947 + 14 C1 1 LIG C8x 14 -0.057540 12.010780 C1 -0.204760 12.010780 + 15 C1 1 LIG C9x 15 -0.203310 12.010780 C1 0.114810 12.010780 + 16 C2 1 LIG C10x 16 0.013180 12.010780 C2 -0.054450 12.010780 + 17 H3 1 LIG H4x 17 0.044890 1.007947 H3 0.058540 1.007947 + 18 H3 1 LIG H5x 18 0.044890 1.007947 H3 0.058540 1.007947 + 19 C2 1 LIG C11x 19 -0.084960 12.010780 C2 -0.080770 12.010780 + 20 H3 1 LIG H6x 20 0.058790 1.007947 H3 0.071370 1.007947 + 21 H3 1 LIG H7x 21 0.058790 1.007947 H3 0.071370 1.007947 + 22 C2 1 LIG C12x 22 0.126550 12.010780 C2 0.130690 12.010780 + 23 H4 1 LIG H8x 23 0.037860 1.007947 H4 0.038330 1.007947 + 24 H4 1 LIG H9x 24 0.037860 1.007947 H4 0.038330 1.007947 + 25 O2 1 LIG O3x 25 -0.332540 15.999430 O2 -0.334080 15.999430 + 26 C1 1 LIG C13x 26 0.142880 12.010780 C1 0.108960 12.010780 + 27 C1 1 LIG C14x 27 -0.181170 12.010780 C1 -0.171770 12.010780 + 28 H2 1 LIG H10x 28 0.144890 1.007947 H2 0.138510 1.007947 + 29 C1 1 LIG C15x 29 -0.052890 12.010780 C1 -0.042000 12.010780 + 30 C2 1 LIG C16x 30 -0.051460 12.010780 C2 -0.059900 12.010780 + 31 H3 1 LIG H11x 31 0.040320 1.007947 H3 0.050460 1.007947 + 32 H3 1 LIG H12x 32 0.040320 1.007947 H3 0.050460 1.007947 + 33 H3 1 LIG H13x 33 0.040320 1.007947 H3 0.050460 1.007947 + 34 C1 1 LIG C17x 34 -0.175410 12.010780 C1 -0.147890 12.010780 + 35 H2 1 LIG H14x 35 0.122060 1.007947 H2 0.142460 1.007947 + 36 C1 1 LIG C18x 36 -0.101660 12.010780 C1 -0.094400 12.010780 + 37 H2 1 LIG H15x 37 0.123170 1.007947 H2 0.139870 1.007947 + 38 C1 1 LIG C19x 38 -0.184410 12.010780 C1 -0.174770 12.010780 + 39 H2 1 LIG H16x 39 0.144680 1.007947 H2 0.138300 1.007947 + 40 C2 1 LIG C20x 40 -0.038120 12.010780 C2 -0.032020 12.010780 + 41 H3 1 LIG H17x 41 0.030840 1.007947 H2_du 0.000000 1.007947 + 42 H3 1 LIG H18x 42 0.030840 1.007947 H2_du 0.000000 1.007947 + 43 H3 1 LIG H19x 43 0.030840 1.007947 H2_du 0.000000 1.007947 + 44 C2 1 LIG C21x 44 -0.035100 12.010780 C2 0.001470 12.010780 + 45 H3 1 LIG H20x 45 0.026750 1.007947 H2_du 0.000000 1.007947 + 46 H3 1 LIG H21x 46 0.026750 1.007947 H2_du 0.000000 1.007947 + 47 H3 1 LIG H22x 47 0.026750 1.007947 H2_du 0.000000 1.007947 + 48 H1_du 1 LIG H1x 48 0.000000 1.007947 H1 0.459340 1.007947 + 49 H1_du 1 LIG H2x 49 0.000000 1.007947 H1 0.459340 1.007947 + 50 H1_du 1 LIG H3x 50 0.000000 1.007947 H1 0.459340 1.007947 + 51 H2_du 1 LIG H19x 51 0.000000 1.007947 H3 0.062430 1.007947 + 52 H2_du 1 LIG H20x 52 0.000000 1.007947 H3 0.062430 1.007947 + 53 H2_du 1 LIG H21x 53 0.000000 1.007947 H3 0.062430 1.007947 + 54 H3_du 1 LIG H22x 54 0.000000 1.007947 H4 0.074650 1.007947 + 55 H3_du 1 LIG H23x 55 0.000000 1.007947 H4 0.074650 1.007947 + 56 H3_du 1 LIG H24x 56 0.000000 1.007947 H4 0.074650 1.007947 + """ + + # Load the molecule. + merged_mol = sr.load_test_files("merged_molecule_grotop.s3") + + # Save to a temporary file. + with tempfile.TemporaryDirectory() as tmpdir: + sr.save(merged_mol, os.path.join(tmpdir, "merged_molecule"), format="GroTop") + + # Read the [ atomtypes ] and [ atoms ] sections from the file. + with open(os.path.join(tmpdir, "merged_molecule.grotop"), "r") as f: + atomtypes_lines = [] + atoms_lines = [] + found_atomtypes = False + found_atoms = False + for line in f: + if line.startswith("[ atomtypes ]"): + found_atomtypes = True + elif line.startswith("[ atoms ]"): + found_atoms = True + + if found_atomtypes and not found_atoms: + if line != "\n": + atomtypes_lines.append(line) + else: + found_atomtypes = False + + if found_atoms: + if line != "\n": + atoms_lines.append(line) + else: + found_atoms = False + + # Check that the atomtypes and atoms are as expected. + atomtypes_list = atomtypes.split("\n") + for a, b in zip(atomtypes_list, atomtypes_lines): + assert a.strip() == b.strip() + atoms_list = atoms.split("\n") + for a, b in zip(atoms_list, atoms_lines): + assert a.strip() == b.strip() diff --git a/tests/morph/test_merge.py b/tests/morph/test_merge.py index 8d5311569..25f9550b6 100644 --- a/tests/morph/test_merge.py +++ b/tests/morph/test_merge.py @@ -202,3 +202,23 @@ def test_merge_neopentane_methane(neopentane_methane, openmm_platform): # These energies aren't correct - extra ghost atom internals? assert nrg_neo.value() == pytest.approx(nrg_merged_0.value(), abs=1e-3) # assert nrg_met.value() == pytest.approx(nrg_merged_1.value(), abs=1e-3) + + +@pytest.mark.skipif(sys.platform == "win32", reason="Not supported on Windows") +def test_ion_merge(ala_mols): + water = ala_mols[-1] + ion = sr.legacy.IO.createSodiumIon(water.atoms()[-1].coordinates(), "tip3p") + + merged = sr.morph.merge(water, ion) + + coords0 = merged.property("coordinates0").to_vector()[0] + coords1 = merged.property("coordinates1").to_vector()[0] + + assert coords0 == coords1 + + merged = sr.morph.merge(ion, water) + + coords0 = merged.property("coordinates0").to_vector()[0] + coords1 = merged.property("coordinates1").to_vector()[0] + + assert coords0 == coords1 diff --git a/tests/qm/test_qm.py b/tests/qm/test_qm.py new file mode 100644 index 000000000..11318986e --- /dev/null +++ b/tests/qm/test_qm.py @@ -0,0 +1,393 @@ +import numpy as np +import pytest +import tempfile + +from sire.legacy.Convert import PyQMCallback + +import sire as sr + +try: + from emle.calculator import EMLECalculator + + has_emle = True +except: + has_emle = False + +try: + from openmmml import MLPotential + + has_openmm_ml = True +except: + has_openmm_ml = False + + +def test_callback_method(): + """Makes sure that a callback method works correctly""" + + class Test: + def callback(self, a, b, c, d, e=None): + return (42, d, c) + + # Instantiate the class. + test = Test() + + # Create a callback object. + cb = PyQMCallback(test, "callback") + + # Create some lists to hold test data. + a = [1, 2] + b = [3, 4] + c = [a, b] + d = [b, a] + e = [4, 5] + + # Call the callback. + result = cb.call(a, b, c, d, e) + + # Make sure the result is correct. + assert result == (42, d, c) == test.callback(a, b, c, d) + + +def test_callback_function(): + """Makes sure that a callback function works correctly""" + + def callback(a, b, c, d, e=None): + return (42, d, c) + + # Create a callback object. + cb = PyQMCallback(callback, "") + + # Create some lists to hold test data. + a = [1, 2] + b = [3, 4] + c = [a, b] + d = [b, a] + e = [4, 5] + + # Call the callback. + result = cb.call(a, b, c, d, e) + + # Make sure the result is correct. + assert result == (42, d, c) == callback(a, b, c, d) + + +@pytest.mark.parametrize( + "selection, expected", + [ + ( + "residx 0", + ( + {6: 4}, + {6: [7, 8]}, + {6: 0.8164794007490638}, + ), + ), + ( + "residx 1", + ( + {4: 6, 16: 14}, + {4: [1, 5], 16: [17, 18]}, + {4: 0.7565543071161049, 16: 0.8164794007490638}, + ), + ), + ( + "residx 2", + ( + {14: 16}, + {14: [8, 15]}, + {14: 0.7565543071161049}, + ), + ), + ], +) +def test_link_atoms(ala_mols, selection, expected): + """ + Make sure that the link atoms are correctly identified. + """ + + from sire.base import create_map as _create_map + from sire.qm._utils import _create_qm_mol_to_atoms, _get_link_atoms + + # Create a local copy of the test system. + mols = ala_mols + + # Extract the QM atom selection. + qm_atoms = mols[0][selection].atoms() + + # Create the mapping between molecule numbers and QM atoms. + qm_mol_to_atoms = _create_qm_mol_to_atoms(qm_atoms) + + # Get link atom information. + mm1_to_qm, mm1_to_mm2, bond_scale_factors, mm1_indices = _get_link_atoms( + mols, qm_mol_to_atoms, _create_map({}) + ) + + assert mm1_to_qm == expected[0] + assert mm1_to_mm2 == expected[1] + assert bond_scale_factors == expected[2] + + +def test_charge_redistribution(): + """ + Make sure that charge redistribution works correctly. + """ + + import sire as sr + + from sire.base import create_map + from sire.qm._utils import _check_charge + from sire.mol import selection_to_atoms + + # A selection for the QM region. This is a subset of a TRP residue. + selection = "residx 118 and not atomname C, CA, H, HA, N, O" + + # The inverse selection. + not_selection = f"not ({selection})" + + # Load the AbyU test system. + mols = sr.load_test_files("abyu.prm7", "abyu.rst7") + + # Selet the QM atoms. + qm_atoms = selection_to_atoms(mols, selection) + + # Get the charges of both regions. + charge0 = mols[selection].charge() + charge1 = mols[not_selection].charge() + + # Check the charges, redistributing to the nearest integer. + _check_charge(mols, qm_atoms, create_map({}), redistribute_charge=True) + + # Get the new charges. + new_charge0 = mols[selection].charge() + new_charge1 = mols[not_selection].charge() + + # Make sure the QM charge has been redistributed to the nearest integer. + assert np.isclose(round(charge0.value()), new_charge0.value(), rtol=1e-4) + + # Make sure the remainder has beeen redistributed to the other atoms. + assert np.isclose((charge0 + charge1).value(), new_charge1.value(), rtol=1e-4) + + # Make sure the check fails if we don't redistribute the charge. + with pytest.raises(Exception): + _check_charge(mols, qm_atoms, create_map({}), redistribute_charge=False) + + +@pytest.mark.skipif(not has_emle, reason="emle-engine is not installed") +@pytest.mark.parametrize("selection", ["molidx 0", "resname ALA"]) +def test_emle_interpolate(ala_mols, selection): + """ + Make sure that lambda interpolation between pure MM and EMLE potentials works. + """ + + # Create a local copy of the test system. + mols = ala_mols.clone() + + # Create an EMLE calculator. + calculator = EMLECalculator(device="cpu") + + # Create a dynamics object. + d = mols.dynamics(timestep="1fs", constraint="none", platform="cpu") + + # Get the pure MM energy. + nrg_mm = d.current_potential_energy() + + # Create an EMLE engine bound to the calculator. + mols, engine = sr.qm.emle(mols, selection, calculator) + + # Create a QM/MM capable dynamics object. + d = mols.dynamics( + timestep="1fs", constraint="none", qm_engine=engine, platform="cpu" + ) + + # Get the pure EMLE energy. + nrg_emle = d.current_potential_energy() + + # Get interpolated MM energy. + d.set_lambda(0.0) + nrg_mm_interp = d.current_potential_energy() + + # Make sure this agrees with the standard MM energy. + assert np.isclose(nrg_mm_interp.value(), nrg_mm.value(), rtol=1e-4) + + # Now get the interpolated energy at lambda = 0.5. + d.set_lambda(0.5) + nrg_interp = d.current_potential_energy() + + # Make sure the interpolated energy is correct. Note that the interpolation + # is actually non-linear so the energies are not exactly the average of the + # two states. + assert np.isclose(nrg_interp.value(), 0.5 * (nrg_mm + nrg_emle).value(), rtol=1e-4) + + +@pytest.mark.skipif(not has_emle, reason="emle-engine is not installed") +@pytest.mark.skipif(not has_openmm_ml, reason="openmm-ml is not installed") +def test_emle_openmm_ml(ala_mols): + """ + Make sure that the EMLE engine can be used with OpenMM-ML. + """ + + import openmm + import sire as sr + + # Create a local copy of the test system. + mols = ala_mols.clone() + + # Create an EMLE calculator. + calculator = EMLECalculator(backend="torchani", device="cpu") + + # Create an EMLE engine bound to the calculator. + emle_mols, engine = sr.qm.emle(mols, mols[0], calculator) + + # Create a QM/MM capable dynamics object. + d = emle_mols.dynamics( + timestep="1fs", + constraint="none", + qm_engine=engine, + cutoff_type="pme", + cutoff="7.5 A", + platform="cpu", + ) + + # Get the energy. + nrg_sire = d.current_potential_energy().to("kJ_per_mol") + + with tempfile.TemporaryDirectory() as tmpdir: + # Create a new EMLECalculator without a backend. + calculator = EMLECalculator(backend=None, device="cpu") + + # Create an EMLE engine bound to the calculator. + emle_mols, engine = sr.qm.emle(mols, mols[0], calculator) + + # Write the sytem to an AMBER coordinate and topology file. + files = sr.expand(tmpdir, ["ala.rst7", "ala.prm7"]) + for file in files: + sr.save(mols, file) + + # Load back the files and create an OpenMM topology. + inpcrd = openmm.app.AmberInpcrdFile(f"{tmpdir}/ala.rst7") + prmtop = openmm.app.AmberPrmtopFile(f"{tmpdir}/ala.prm7") + topology = prmtop.topology + + # Create the MM system. + mm_system = prmtop.createSystem( + nonbondedMethod=openmm.app.PME, + nonbondedCutoff=7.5 * openmm.unit.angstrom, + constraints=openmm.app.HBonds, + ) + + # Define the ML region. + ml_atoms = list(range(mols[0].num_atoms())) + + # Create the mixed ML/MM system. + potential = MLPotential("ani2x") + ml_system = potential.createMixedSystem( + topology, mm_system, ml_atoms, interpolate=False + ) + + # Get the OpenMM forces from the engine. + emle_force, interpolation_force = engine.get_forces() + + # Add the EMLE force to the system. + ml_system.addForce(emle_force) + + # Turn off the dispersion correction to match Sire's calculation + # and set the QM atoms to have zero charge. + for force in ml_system.getForces(): + if isinstance(force, openmm.NonbondedForce): + for i in ml_atoms: + _, sigma, epsilon = force.getParticleParameters(i) + force.setParticleParameters(i, 0, sigma, epsilon) + force.setUseDispersionCorrection(False) + + # Create the integrator. + integrator = openmm.LangevinMiddleIntegrator( + 300 * openmm.unit.kelvin, + 1.0 / openmm.unit.picosecond, + 0.002 * openmm.unit.picosecond, + ) + + # Create the context. + context = openmm.Context(ml_system, integrator) + + # Set the positions. + context.setPositions(inpcrd.positions) + + # Get the energy. + state = context.getState(getEnergy=True) + nrg_openmm = state.getPotentialEnergy().value_in_unit( + openmm.unit.kilojoules_per_mole + ) + + # Make sure the energies are close. + assert np.isclose(nrg_openmm, nrg_sire, rtol=1e-6) + + +@pytest.mark.skipif(not has_emle, reason="emle-engine is not installed") +def test_emle_indirect(ala_mols): + """ + Make sure that a QM/MM dynamics object can be created using the indirect + setup for EMLE engines. + """ + + # Create a local copy of the test system. + mols = ala_mols.clone() + + # Create an EMLE calculator. + calculator = EMLECalculator(backend="torchani", device="cpu") + + # Create an EMLE engine bound to the calculator. + emle_mols, engine = sr.qm.create_engine( + mols, mols[0], calculator, callback="_sire_callback" + ) + + # Create a QM/MM capable dynamics object. + d = emle_mols.dynamics( + timestep="1fs", + constraint="none", + qm_engine=engine, + cutoff_type="pme", + cutoff="7.5 A", + platform="cpu", + ) + + # Get the potential energy. This will fail if the callback can't be found. + d.current_potential_energy() + + +def test_create_engine(ala_mols): + """ + Make sure that a QM/MM engine can be created and used via a simple callback + function. + """ + + # A test callback function. Returns a known energy and dummy forces. + def callback(numbers_qm, charges_mm, xyz_qm, xyz_mm, idx_mm=None): + return (42, xyz_qm, xyz_mm) + + # Create a local copy of the test system. + mols = ala_mols.clone() + + # Create a QM engine bound to the callback. + qm_mols, engine = sr.qm.create_engine( + mols, + mols[0], + callback, + callback=None, + ) + + # Create a QM/MM capable dynamics object for the QM molecule only. + d = qm_mols[0].dynamics( + timestep="1fs", + constraint="none", + qm_engine=engine, + cutoff_type="pme", + cutoff="7.5 A", + platform="cpu", + ) + + # Get the potential energy. This should equal the value returned by the + # callback, i.e. 42. + nrg = d.current_potential_energy().to("kJ_per_mol") + + # Make sure the energy is correct. + assert nrg == 42 diff --git a/version.txt b/version.txt index 7e08acf8e..3cb3977c1 100644 --- a/version.txt +++ b/version.txt @@ -1 +1 @@ -2024.2.0 +2024.3.0.dev diff --git a/wrapper/Convert/SireOpenMM/CMakeAutogenFile.txt b/wrapper/Convert/SireOpenMM/CMakeAutogenFile.txt index 1a668ae58..0a7247be3 100644 --- a/wrapper/Convert/SireOpenMM/CMakeAutogenFile.txt +++ b/wrapper/Convert/SireOpenMM/CMakeAutogenFile.txt @@ -1,9 +1,18 @@ # WARNING - AUTOGENERATED FILE - CONTENTS WILL BE OVERWRITTEN! set ( PYPP_SOURCES - _SireOpenMM_free_functions.pypp.cpp LambdaLever.pypp.cpp - vector_less__OpenMM_scope_Vec3__greater_.pypp.cpp PerturbableOpenMMMolecule.pypp.cpp + PyQMCallback.pypp.cpp + PyQMForce.pypp.cpp + PyQMEngine.pypp.cpp + TorchQMForce.pypp.cpp + TorchQMEngine.pypp.cpp + QMEngine.pypp.cpp + _SireOpenMM_free_functions.pypp.cpp + QMForce.pypp.cpp + vector_less__OpenMM_scope_Vec3__greater_.pypp.cpp + NullQMEngine.pypp.cpp OpenMMMetaData.pypp.cpp + SireOpenMM_properties.cpp SireOpenMM_registrars.cpp ) diff --git a/wrapper/Convert/SireOpenMM/CMakeLists.txt b/wrapper/Convert/SireOpenMM/CMakeLists.txt index 9157141cb..61d30e978 100644 --- a/wrapper/Convert/SireOpenMM/CMakeLists.txt +++ b/wrapper/Convert/SireOpenMM/CMakeLists.txt @@ -17,6 +17,7 @@ if (${SIRE_USE_OPENMM}) # Third Party dependencies of this module include_directories( ${OpenMM_INCLUDE_DIR} ) add_definitions("-DSIRE_USE_OPENMM") + add_definitions("-DQT_NO_SIGNALS_SLOTS_KEYWORDS") # Sire include paths include_directories( BEFORE ${SIRE_INCLUDE_DIR} ) @@ -24,6 +25,21 @@ if (${SIRE_USE_OPENMM}) # Other python wrapping directories include_directories(${CMAKE_SOURCE_DIR}) + # We're only building against OpenMM 8.1+, so include CustomCPPForce support. + add_definitions("-DSIRE_USE_CUSTOMCPPFORCE") + + # Check to see if Torch support has been disabled. + if (NOT DEFINED ENV{SIRE_NO_TORCH}) + find_package(Torch) + if (TORCH_FOUND) + message(STATUS "Torch found") + add_definitions("-DSIRE_USE_TORCH") + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} ${TORCH_CXX_FLAGS}") + else() + message(STATUS "Torch not found") + endif() + endif() + # Check to see if we have support for updating some parameters in context include(CheckCXXSourceCompiles) check_cxx_source_compiles( "#include @@ -49,6 +65,12 @@ if (${SIRE_USE_OPENMM}) # Define the sources in SireOpenMM set ( SIREOPENMM_SOURCES + pyqm.cpp + torchqm.cpp + lambdalever.cpp + openmmminimise.cpp + openmmmolecule.cpp + qmmm.cpp lambdalever.cpp openmmminimise.cpp openmmmolecule.cpp @@ -85,6 +107,7 @@ if (${SIRE_USE_OPENMM}) SIRE_SireStream SIRE_SireError ${OpenMM_LIBRARIES} + ${TORCH_LIBRARIES} ) include( LimitSirePythonExportSymbols ) diff --git a/wrapper/Convert/SireOpenMM/LambdaLever.pypp.cpp b/wrapper/Convert/SireOpenMM/LambdaLever.pypp.cpp index 85786b4a2..54e828a53 100644 --- a/wrapper/Convert/SireOpenMM/LambdaLever.pypp.cpp +++ b/wrapper/Convert/SireOpenMM/LambdaLever.pypp.cpp @@ -15,6 +15,8 @@ namespace bp = boost::python; #include "lambdalever.h" +#include "pyqm.h" + #include "tostring.h" #include "SireBase/arrayproperty.hpp" @@ -25,6 +27,8 @@ namespace bp = boost::python; #include "lambdalever.h" +#include "pyqm.h" + #include "tostring.h" SireOpenMM::LambdaLever __copy__(const SireOpenMM::LambdaLever &other){ return SireOpenMM::LambdaLever(other); } @@ -35,6 +39,8 @@ SireOpenMM::LambdaLever __copy__(const SireOpenMM::LambdaLever &other){ return S #include "Helpers/release_gil_policy.hpp" +#include "Qt/qdatastream.hpp" + void register_LambdaLever_class(){ { //::SireOpenMM::LambdaLever @@ -275,6 +281,7 @@ void register_LambdaLever_class(){ LambdaLever_exposer.staticmethod( "typeName" ); LambdaLever_exposer.def( "__copy__", &__copy__); LambdaLever_exposer.def( "__deepcopy__", &__copy__); + LambdaLever_exposer.def_pickle(sire_pickle_suite< ::SireOpenMM::LambdaLever >()); LambdaLever_exposer.def( "clone", &__copy__); LambdaLever_exposer.def( "__str__", &__str__< ::SireOpenMM::LambdaLever > ); LambdaLever_exposer.def( "__repr__", &__str__< ::SireOpenMM::LambdaLever > ); diff --git a/wrapper/Convert/SireOpenMM/NullQMEngine.pypp.cpp b/wrapper/Convert/SireOpenMM/NullQMEngine.pypp.cpp new file mode 100644 index 000000000..990175cda --- /dev/null +++ b/wrapper/Convert/SireOpenMM/NullQMEngine.pypp.cpp @@ -0,0 +1,57 @@ +// This file has been generated by Py++. + +// (C) Christopher Woods, GPL >= 3 License + +#include "boost/python.hpp" +#include "NullQMEngine.pypp.hpp" + +namespace bp = boost::python; + +#include "SireError/errors.h" + +#include "qmmm.h" + +#include "SireError/errors.h" + +#include "qmmm.h" + +#include "Helpers/str.hpp" + +#include "Helpers/release_gil_policy.hpp" + +void register_NullQMEngine_class(){ + + { //::SireOpenMM::NullQMEngine + typedef bp::class_< SireOpenMM::NullQMEngine, bp::bases< SireOpenMM::QMEngine >, boost::noncopyable > NullQMEngine_exposer_t; + NullQMEngine_exposer_t NullQMEngine_exposer = NullQMEngine_exposer_t( "NullQMEngine", "" ); + bp::scope NullQMEngine_scope( NullQMEngine_exposer ); + { //::SireOpenMM::NullQMEngine::typeName + + typedef char const * ( *typeName_function_type )( ); + typeName_function_type typeName_function_value( &::SireOpenMM::NullQMEngine::typeName ); + + NullQMEngine_exposer.def( + "typeName" + , typeName_function_value + , bp::release_gil_policy() + , "Get the name of the QM engine." ); + + } + { //::SireOpenMM::NullQMEngine::what + + typedef char const * ( ::SireOpenMM::NullQMEngine::*what_function_type)( ) const; + what_function_type what_function_value( &::SireOpenMM::NullQMEngine::what ); + + NullQMEngine_exposer.def( + "what" + , what_function_value + , bp::release_gil_policy() + , "Get the name of the QM engine." ); + + } + NullQMEngine_exposer.staticmethod( "typeName" ); + NullQMEngine_exposer.def( "__str__", &__str__< ::SireOpenMM::NullQMEngine > ); + NullQMEngine_exposer.def( "__repr__", &__str__< ::SireOpenMM::NullQMEngine > ); + } + +} diff --git a/wrapper/Convert/SireOpenMM/NullQMEngine.pypp.hpp b/wrapper/Convert/SireOpenMM/NullQMEngine.pypp.hpp new file mode 100644 index 000000000..0c4147d7b --- /dev/null +++ b/wrapper/Convert/SireOpenMM/NullQMEngine.pypp.hpp @@ -0,0 +1,10 @@ +// This file has been generated by Py++. + +// (C) Christopher Woods, GPL >= 3 License + +#ifndef NullQMEngine_hpp__pyplusplus_wrapper +#define NullQMEngine_hpp__pyplusplus_wrapper + +void register_NullQMEngine_class(); + +#endif//NullQMEngine_hpp__pyplusplus_wrapper diff --git a/wrapper/Convert/SireOpenMM/OpenMMMetaData.pypp.cpp b/wrapper/Convert/SireOpenMM/OpenMMMetaData.pypp.cpp index 07b317ddb..cc3dfe5b7 100644 --- a/wrapper/Convert/SireOpenMM/OpenMMMetaData.pypp.cpp +++ b/wrapper/Convert/SireOpenMM/OpenMMMetaData.pypp.cpp @@ -45,6 +45,8 @@ namespace bp = boost::python; #include "SireMol/moleditor.h" +#include "SireMol/selectorm.hpp" + #include "SireStream/datastream.h" #include "SireStream/shareddatastream.h" @@ -105,6 +107,8 @@ namespace bp = boost::python; #include "SireMol/moleditor.h" +#include "SireMol/selectorm.hpp" + #include "SireStream/datastream.h" #include "SireStream/shareddatastream.h" diff --git a/wrapper/Convert/SireOpenMM/PyQMCallback.pypp.cpp b/wrapper/Convert/SireOpenMM/PyQMCallback.pypp.cpp new file mode 100644 index 000000000..1264f0745 --- /dev/null +++ b/wrapper/Convert/SireOpenMM/PyQMCallback.pypp.cpp @@ -0,0 +1,119 @@ +// This file has been generated by Py++. + +// (C) Christopher Woods, GPL >= 3 License + +#include "boost/python.hpp" +#include "PyQMCallback.pypp.hpp" + +namespace bp = boost::python; + +#include "SireError/errors.h" + +#include "SireMaths/vector.h" + +#include "SireStream/datastream.h" + +#include "SireStream/shareddatastream.h" + +#include "SireVol/triclinicbox.h" + +#include "openmm/serialization/SerializationNode.h" + +#include "openmm/serialization/SerializationProxy.h" + +#include "pyqm.h" + +#include + +#include + +#include + +#include "SireError/errors.h" + +#include "SireMaths/vector.h" + +#include "SireStream/datastream.h" + +#include "SireStream/shareddatastream.h" + +#include "SireVol/triclinicbox.h" + +#include "openmm/serialization/SerializationNode.h" + +#include "openmm/serialization/SerializationProxy.h" + +#include "pyqm.h" + +#include + +#include + +#include + +SireOpenMM::PyQMCallback __copy__(const SireOpenMM::PyQMCallback &other){ return SireOpenMM::PyQMCallback(other); } + +#include "Qt/qdatastream.hpp" + +const char* pvt_get_name(const SireOpenMM::PyQMCallback&){ return "SireOpenMM::PyQMCallback";} + +#include "Helpers/release_gil_policy.hpp" + +void register_PyQMCallback_class(){ + + { //::SireOpenMM::PyQMCallback + typedef bp::class_< SireOpenMM::PyQMCallback > PyQMCallback_exposer_t; + PyQMCallback_exposer_t PyQMCallback_exposer = PyQMCallback_exposer_t( "PyQMCallback", "A callback wrapper class to interface with external QM engines\nvia the CustomCPPForceImpl.", bp::init< >("Default constructor.") ); + bp::scope PyQMCallback_scope( PyQMCallback_exposer ); + PyQMCallback_exposer.def( bp::init< bp::api::object, bp::optional< QString > >(( bp::arg("arg0"), bp::arg("name")="" ), "Constructor\nPar:am py_object\nA Python object that contains the callback function.\n\nPar:am name\nThe name of a callback method that take the following arguments:\n- numbers_qm: A list of atomic numbers for the atoms in the ML region.\n- charges_mm: A list of the MM charges in mod electron charge.\n- xyz_qm: A list of positions for the atoms in the ML region in Angstrom.\n- xyz_mm: A list of positions for the atoms in the MM region in Angstrom.\n- idx_mm: A list of indices for the MM atoms in the QM/MM region.\nThe callback should return a tuple containing:\n- The energy in kJmol.\n- A list of forces for the QM atoms in kJmolnm.\n- A list of forces for the MM atoms in kJmolnm.\nIf empty, then the object is assumed to be a callable.\n") ); + { //::SireOpenMM::PyQMCallback::call + + typedef ::boost::tuples::tuple< double, QVector< QVector< double > >, QVector< QVector< double > >, boost::tuples::null_type, boost::tuples::null_type, boost::tuples::null_type, boost::tuples::null_type, boost::tuples::null_type, boost::tuples::null_type, boost::tuples::null_type > ( ::SireOpenMM::PyQMCallback::*call_function_type)( ::QVector< int >,::QVector< double >,::QVector< QVector< double > >,::QVector< QVector< double > >,::QVector< int > ) const; + call_function_type call_function_value( &::SireOpenMM::PyQMCallback::call ); + + PyQMCallback_exposer.def( + "call" + , call_function_value + , ( bp::arg("numbers_qm"), bp::arg("charges_mm"), bp::arg("xyz_qm"), bp::arg("xyz_mm"), bp::arg("idx_mm") ) + , bp::release_gil_policy() + , "Call the callback function.\nPar:am numbers_qm\nA vector of atomic numbers for the atoms in the ML region.\n\nPar:am charges_mm\nA vector of the charges on the MM atoms in mod electron charge.\n\nPar:am xyz_qm\nA vector of positions for the atoms in the ML region in Angstrom.\n\nPar:am xyz_mm\nA vector of positions for the atoms in the MM region in Angstrom.\n\nPar:am idx_mm A vector of indices for the MM atoms in the QM/MM region. Note that len(idx_mm) <= len(charges_mm) since it only contains the indices of true MM atoms, not link atoms or virtual charges.\n\nReturn:s\nA tuple containing:\n- The energy in kJmol.\n- A vector of forces for the QM atoms in kJmolnm.\n- A vector of forces for the MM atoms in kJmolnm.\n" ); + + } + { //::SireOpenMM::PyQMCallback::typeName + + typedef char const * ( *typeName_function_type )( ); + typeName_function_type typeName_function_value( &::SireOpenMM::PyQMCallback::typeName ); + + PyQMCallback_exposer.def( + "typeName" + , typeName_function_value + , bp::release_gil_policy() + , "Return the C++ name for this class." ); + + } + { //::SireOpenMM::PyQMCallback::what + + typedef char const * ( ::SireOpenMM::PyQMCallback::*what_function_type)( ) const; + what_function_type what_function_value( &::SireOpenMM::PyQMCallback::what ); + + PyQMCallback_exposer.def( + "what" + , what_function_value + , bp::release_gil_policy() + , "Return the C++ name for this class." ); + + } + PyQMCallback_exposer.staticmethod( "typeName" ); + PyQMCallback_exposer.def( "__copy__", &__copy__); + PyQMCallback_exposer.def( "__deepcopy__", &__copy__); + PyQMCallback_exposer.def( "clone", &__copy__); + PyQMCallback_exposer.def( "__rlshift__", &__rlshift__QDataStream< ::SireOpenMM::PyQMCallback >, + bp::return_internal_reference<1, bp::with_custodian_and_ward<1,2> >() ); + PyQMCallback_exposer.def( "__rrshift__", &__rrshift__QDataStream< ::SireOpenMM::PyQMCallback >, + bp::return_internal_reference<1, bp::with_custodian_and_ward<1,2> >() ); + PyQMCallback_exposer.def_pickle(sire_pickle_suite< ::SireOpenMM::PyQMCallback >()); + PyQMCallback_exposer.def( "__str__", &pvt_get_name); + PyQMCallback_exposer.def( "__repr__", &pvt_get_name); + } + +} diff --git a/wrapper/Convert/SireOpenMM/PyQMCallback.pypp.hpp b/wrapper/Convert/SireOpenMM/PyQMCallback.pypp.hpp new file mode 100644 index 000000000..f3b426b39 --- /dev/null +++ b/wrapper/Convert/SireOpenMM/PyQMCallback.pypp.hpp @@ -0,0 +1,10 @@ +// This file has been generated by Py++. + +// (C) Christopher Woods, GPL >= 3 License + +#ifndef PyQMCallback_hpp__pyplusplus_wrapper +#define PyQMCallback_hpp__pyplusplus_wrapper + +void register_PyQMCallback_class(); + +#endif//PyQMCallback_hpp__pyplusplus_wrapper diff --git a/wrapper/Convert/SireOpenMM/PyQMEngine.pypp.cpp b/wrapper/Convert/SireOpenMM/PyQMEngine.pypp.cpp new file mode 100644 index 000000000..b4589667e --- /dev/null +++ b/wrapper/Convert/SireOpenMM/PyQMEngine.pypp.cpp @@ -0,0 +1,363 @@ +// This file has been generated by Py++. + +// (C) Christopher Woods, GPL >= 3 License + +#include "boost/python.hpp" +#include "PyQMEngine.pypp.hpp" + +namespace bp = boost::python; + +#include "SireError/errors.h" + +#include "SireMaths/vector.h" + +#include "SireStream/datastream.h" + +#include "SireStream/shareddatastream.h" + +#include "SireVol/triclinicbox.h" + +#include "openmm/serialization/SerializationNode.h" + +#include "openmm/serialization/SerializationProxy.h" + +#include "pyqm.h" + +#include + +#include + +#include + +#include "SireError/errors.h" + +#include "SireMaths/vector.h" + +#include "SireStream/datastream.h" + +#include "SireStream/shareddatastream.h" + +#include "SireVol/triclinicbox.h" + +#include "openmm/serialization/SerializationNode.h" + +#include "openmm/serialization/SerializationProxy.h" + +#include "pyqm.h" + +#include + +#include + +#include + +SireOpenMM::PyQMEngine __copy__(const SireOpenMM::PyQMEngine &other){ return SireOpenMM::PyQMEngine(other); } + +#include "Helpers/str.hpp" + +#include "Helpers/release_gil_policy.hpp" + +void register_PyQMEngine_class(){ + + { //::SireOpenMM::PyQMEngine + typedef bp::class_< SireOpenMM::PyQMEngine, bp::bases< SireBase::Property, SireOpenMM::QMEngine > > PyQMEngine_exposer_t; + PyQMEngine_exposer_t PyQMEngine_exposer = PyQMEngine_exposer_t( "PyQMEngine", "", bp::init< >("Default constructor.") ); + bp::scope PyQMEngine_scope( PyQMEngine_exposer ); + PyQMEngine_exposer.def( bp::init< bp::api::object, bp::optional< QString, SireUnits::Dimension::Length, int, bool, double > >(( bp::arg("arg0"), bp::arg("method")="", bp::arg("cutoff")=7.5 * SireUnits::angstrom, bp::arg("neighbour_list_frequency")=(int)(0), bp::arg("is_mechanical")=(bool)(false), bp::arg("lambda")=1. ), "Constructor\nPar:am py_object\nA Python object.\n\nPar:am name\nThe name of the callback method. If empty, then the object is\nassumed to be a callable.\n\nPar:am cutoff\nThe ML cutoff distance.\n\nPar:am neighbour_list_frequency\nThe frequency at which the neighbour list is updated. (Number of steps.)\nIf zero, then no neighbour list is used.\n\nPar:am is_mechanical\nA flag to indicate if mechanical embedding is being used.\n\nPar:am lambda\nThe lambda weighting factor. This can be used to interpolate between\npotentials for end-state correction calculations.\n") ); + PyQMEngine_exposer.def( bp::init< SireOpenMM::PyQMEngine const & >(( bp::arg("other") ), "Copy constructor.") ); + { //::SireOpenMM::PyQMEngine::call + + typedef ::boost::tuples::tuple< double, QVector< QVector< double > >, QVector< QVector< double > >, boost::tuples::null_type, boost::tuples::null_type, boost::tuples::null_type, boost::tuples::null_type, boost::tuples::null_type, boost::tuples::null_type, boost::tuples::null_type > ( ::SireOpenMM::PyQMEngine::*call_function_type)( ::QVector< int >,::QVector< double >,::QVector< QVector< double > >,::QVector< QVector< double > >,::QVector < int > ) const; + call_function_type call_function_value( &::SireOpenMM::PyQMEngine::call ); + + PyQMEngine_exposer.def( + "call" + , call_function_value + , ( bp::arg("numbers_qm"), bp::arg("charges_mm"), bp::arg("xyz_qm"), bp::arg("xyz_mm"), bp::arg("idx_mm") ) + , bp::release_gil_policy() + , "Call the callback function.\nPar:am numbers_qm\nA vector of atomic numbers for the atoms in the ML region.\n\nPar:am charges_mm\nA vector of the charges on the MM atoms in mod electron charge.\n\nPar:am xyz_qm\nA vector of positions for the atoms in the ML region in Angstrom.\n\nPar:am xyz_mm\nA vector of positions for the atoms in the MM region in Angstrom.\n\nPar:am idx_mm A vector of indices for the MM atoms in the QM/MM region. Note that len(idx_mm) <= len(charges_mm) since it only contains the indices of the true MM atoms, not link atoms or virtual charges.\n\nReturn:s\nA tuple containing:\n- The energy in kJmol.\n- A vector of forces for the QM atoms in kJmolnm.\n- A vector of forces for the MM atoms in kJmolnm.\n" ); + + } + { //::SireOpenMM::PyQMEngine::getAtoms + + typedef ::QVector< int > ( ::SireOpenMM::PyQMEngine::*getAtoms_function_type)( ) const; + getAtoms_function_type getAtoms_function_value( &::SireOpenMM::PyQMEngine::getAtoms ); + + PyQMEngine_exposer.def( + "getAtoms" + , getAtoms_function_value + , bp::release_gil_policy() + , "Get the indices of the atoms in the QM region.\nReturn:s\nA vector of atom indices for the QM region.\n" ); + + } + { //::SireOpenMM::PyQMEngine::getCallback + + typedef ::SireOpenMM::PyQMCallback ( ::SireOpenMM::PyQMEngine::*getCallback_function_type)( ) const; + getCallback_function_type getCallback_function_value( &::SireOpenMM::PyQMEngine::getCallback ); + + PyQMEngine_exposer.def( + "getCallback" + , getCallback_function_value + , bp::release_gil_policy() + , "Get the callback object.\nReturn:s\nA Python object that contains the callback function.\n" ); + + } + { //::SireOpenMM::PyQMEngine::getCharges + + typedef ::QVector< double > ( ::SireOpenMM::PyQMEngine::*getCharges_function_type)( ) const; + getCharges_function_type getCharges_function_value( &::SireOpenMM::PyQMEngine::getCharges ); + + PyQMEngine_exposer.def( + "getCharges" + , getCharges_function_value + , bp::release_gil_policy() + , "Get the atomic charges of all atoms in the system.\nReturn:s\nA vector of atomic charges for all atoms in the system.\n" ); + + } + { //::SireOpenMM::PyQMEngine::getCutoff + + typedef ::SireUnits::Dimension::Length ( ::SireOpenMM::PyQMEngine::*getCutoff_function_type)( ) const; + getCutoff_function_type getCutoff_function_value( &::SireOpenMM::PyQMEngine::getCutoff ); + + PyQMEngine_exposer.def( + "getCutoff" + , getCutoff_function_value + , bp::release_gil_policy() + , "Get the QM cutoff distance.\nReturn:s\nThe QM cutoff distance.\n" ); + + } + { //::SireOpenMM::PyQMEngine::getIsMechanical + + typedef bool ( ::SireOpenMM::PyQMEngine::*getIsMechanical_function_type)( ) const; + getIsMechanical_function_type getIsMechanical_function_value( &::SireOpenMM::PyQMEngine::getIsMechanical ); + + PyQMEngine_exposer.def( + "getIsMechanical" + , getIsMechanical_function_value + , bp::release_gil_policy() + , "Get the mechanical embedding flag.\nReturn:s\nA flag to indicate if mechanical embedding is being used.\n" ); + + } + { //::SireOpenMM::PyQMEngine::getLambda + + typedef double ( ::SireOpenMM::PyQMEngine::*getLambda_function_type)( ) const; + getLambda_function_type getLambda_function_value( &::SireOpenMM::PyQMEngine::getLambda ); + + PyQMEngine_exposer.def( + "getLambda" + , getLambda_function_value + , bp::release_gil_policy() + , "Get the lambda weighting factor.\nReturn:s\nThe lambda weighting factor.\n" ); + + } + { //::SireOpenMM::PyQMEngine::getLinkAtoms + + typedef ::boost::tuples::tuple< QMap< int, int >, QMap< int, QVector< int > >, QMap< int, double >, boost::tuples::null_type, boost::tuples::null_type, boost::tuples::null_type, boost::tuples::null_type, boost::tuples::null_type, boost::tuples::null_type, boost::tuples::null_type > ( ::SireOpenMM::PyQMEngine::*getLinkAtoms_function_type)( ) const; + getLinkAtoms_function_type getLinkAtoms_function_value( &::SireOpenMM::PyQMEngine::getLinkAtoms ); + + PyQMEngine_exposer.def( + "getLinkAtoms" + , getLinkAtoms_function_value + , bp::release_gil_policy() + , "Get the link atoms associated with each QM atom.\nReturn:s\nA tuple containing:\n\nmm1_to_qm\nA dictionary mapping link atom (MM1) indices to the QM atoms to\nwhich they are bonded.\n\nmm1_to_mm2\nA dictionary of link atoms indices (MM1) to a list of the MM\natoms to which they are bonded (MM2).\n\nbond_scale_factors\nA dictionary of link atom indices (MM1) to a list of the bond\nlength scale factors between the QM and MM1 atoms. The scale\nfactors are the ratio of the equilibrium bond lengths for the\nQM-L (QM-link) atom and QM-MM1 atom, i.e. R0(QM-L) R0(QM-MM1),\ntaken from the MM force field parameters for the molecule.\n\n" ); + + } + { //::SireOpenMM::PyQMEngine::getMM2Atoms + + typedef ::QVector< int > ( ::SireOpenMM::PyQMEngine::*getMM2Atoms_function_type)( ) const; + getMM2Atoms_function_type getMM2Atoms_function_value( &::SireOpenMM::PyQMEngine::getMM2Atoms ); + + PyQMEngine_exposer.def( + "getMM2Atoms" + , getMM2Atoms_function_value + , bp::release_gil_policy() + , "Get the vector of MM2 atoms.\nReturn:s\nA vector of MM2 atom indices.\n" ); + + } + { //::SireOpenMM::PyQMEngine::getNeighbourListFrequency + + typedef int ( ::SireOpenMM::PyQMEngine::*getNeighbourListFrequency_function_type)( ) const; + getNeighbourListFrequency_function_type getNeighbourListFrequency_function_value( &::SireOpenMM::PyQMEngine::getNeighbourListFrequency ); + + PyQMEngine_exposer.def( + "getNeighbourListFrequency" + , getNeighbourListFrequency_function_value + , bp::release_gil_policy() + , "Get the neighbour list frequency.\nReturn:s\nThe neighbour list frequency.\n" ); + + } + { //::SireOpenMM::PyQMEngine::getNumbers + + typedef ::QVector< int > ( ::SireOpenMM::PyQMEngine::*getNumbers_function_type)( ) const; + getNumbers_function_type getNumbers_function_value( &::SireOpenMM::PyQMEngine::getNumbers ); + + PyQMEngine_exposer.def( + "getNumbers" + , getNumbers_function_value + , bp::release_gil_policy() + , "Get the atomic numbers for the atoms in the QM region.\nReturn:s\nA vector of atomic numbers for the atoms in the QM region.\n" ); + + } + { //::SireOpenMM::PyQMEngine::operator= + + typedef ::SireOpenMM::PyQMEngine & ( ::SireOpenMM::PyQMEngine::*assign_function_type)( ::SireOpenMM::PyQMEngine const & ) ; + assign_function_type assign_function_value( &::SireOpenMM::PyQMEngine::operator= ); + + PyQMEngine_exposer.def( + "assign" + , assign_function_value + , ( bp::arg("other") ) + , bp::return_self< >() + , "Assignment operator." ); + + } + { //::SireOpenMM::PyQMEngine::setAtoms + + typedef void ( ::SireOpenMM::PyQMEngine::*setAtoms_function_type)( ::QVector< int > ) ; + setAtoms_function_type setAtoms_function_value( &::SireOpenMM::PyQMEngine::setAtoms ); + + PyQMEngine_exposer.def( + "setAtoms" + , setAtoms_function_value + , ( bp::arg("atoms") ) + , bp::release_gil_policy() + , "Set the list of atom indices for the QM region.\nPar:am atoms\nA vector of atom indices for the QM region.\n" ); + + } + { //::SireOpenMM::PyQMEngine::setCallback + + typedef void ( ::SireOpenMM::PyQMEngine::*setCallback_function_type)( ::SireOpenMM::PyQMCallback ) ; + setCallback_function_type setCallback_function_value( &::SireOpenMM::PyQMEngine::setCallback ); + + PyQMEngine_exposer.def( + "setCallback" + , setCallback_function_value + , ( bp::arg("callback") ) + , bp::release_gil_policy() + , "Set the callback object.\nPar:am callback\nA Python object that contains the callback function.\n" ); + + } + { //::SireOpenMM::PyQMEngine::setCharges + + typedef void ( ::SireOpenMM::PyQMEngine::*setCharges_function_type)( ::QVector< double > ) ; + setCharges_function_type setCharges_function_value( &::SireOpenMM::PyQMEngine::setCharges ); + + PyQMEngine_exposer.def( + "setCharges" + , setCharges_function_value + , ( bp::arg("charges") ) + , bp::release_gil_policy() + , "Set the atomic charges of all atoms in the system.\nPar:am charges\nA vector of atomic charges for all atoms in the system.\n" ); + + } + { //::SireOpenMM::PyQMEngine::setCutoff + + typedef void ( ::SireOpenMM::PyQMEngine::*setCutoff_function_type)( ::SireUnits::Dimension::Length ) ; + setCutoff_function_type setCutoff_function_value( &::SireOpenMM::PyQMEngine::setCutoff ); + + PyQMEngine_exposer.def( + "setCutoff" + , setCutoff_function_value + , ( bp::arg("cutoff") ) + , bp::release_gil_policy() + , "Set the QM cutoff distance.\nPar:am cutoff\nThe QM cutoff distance.\n" ); + + } + { //::SireOpenMM::PyQMEngine::setIsMechanical + + typedef void ( ::SireOpenMM::PyQMEngine::*setIsMechanical_function_type)( bool ) ; + setIsMechanical_function_type setIsMechanical_function_value( &::SireOpenMM::PyQMEngine::setIsMechanical ); + + PyQMEngine_exposer.def( + "setIsMechanical" + , setIsMechanical_function_value + , ( bp::arg("is_mechanical") ) + , bp::release_gil_policy() + , "Set the mechanical embedding flag.\nPar:am is_mechanical\nA flag to indicate if mechanical embedding is being used.\n" ); + + } + { //::SireOpenMM::PyQMEngine::setLambda + + typedef void ( ::SireOpenMM::PyQMEngine::*setLambda_function_type)( double ) ; + setLambda_function_type setLambda_function_value( &::SireOpenMM::PyQMEngine::setLambda ); + + PyQMEngine_exposer.def( + "setLambda" + , setLambda_function_value + , ( bp::arg("lambda") ) + , bp::release_gil_policy() + , "Set the lambda weighting factor.\nPar:am lambda\nThe lambda weighting factor.\n" ); + + } + { //::SireOpenMM::PyQMEngine::setLinkAtoms + + typedef void ( ::SireOpenMM::PyQMEngine::*setLinkAtoms_function_type)( ::QMap< int, int >,::QMap< int, QVector< int > >,::QMap< int, double > ) ; + setLinkAtoms_function_type setLinkAtoms_function_value( &::SireOpenMM::PyQMEngine::setLinkAtoms ); + + PyQMEngine_exposer.def( + "setLinkAtoms" + , setLinkAtoms_function_value + , ( bp::arg("mm1_to_qm"), bp::arg("mm1_to_mm2"), bp::arg("bond_scale_factors") ) + , bp::release_gil_policy() + , "Set the link atoms associated with each QM atom.\nPar:am mm1_to_qm\nA dictionary mapping link atom (MM1) indices to the QM atoms to\nwhich they are bonded.\n\nPar:am mm1_to_mm2\nA dictionary of link atoms indices (MM1) to a list of the MM\natoms to which they are bonded (MM2).\n\nPar:am bond_scale_factors\nA dictionary of link atom indices (MM1) to a list of the bond\nlength scale factors between the QM and MM1 atoms. The scale\nfactors are the ratio of the equilibrium bond lengths for the\nQM-L (QM-link) atom and QM-MM1 atom, i.e. R0(QM-L) R0(QM-MM1),\ntaken from the MM force field parameters for the molecule.\n\n" ); + + } + { //::SireOpenMM::PyQMEngine::setNeighbourListFrequency + + typedef void ( ::SireOpenMM::PyQMEngine::*setNeighbourListFrequency_function_type)( int ) ; + setNeighbourListFrequency_function_type setNeighbourListFrequency_function_value( &::SireOpenMM::PyQMEngine::setNeighbourListFrequency ); + + PyQMEngine_exposer.def( + "setNeighbourListFrequency" + , setNeighbourListFrequency_function_value + , ( bp::arg("neighbour_list_frequency") ) + , bp::release_gil_policy() + , "Set the neighbour list frequency.\nPar:am neighbour_list_frequency\nThe neighbour list frequency.\n" ); + + } + { //::SireOpenMM::PyQMEngine::setNumbers + + typedef void ( ::SireOpenMM::PyQMEngine::*setNumbers_function_type)( ::QVector< int > ) ; + setNumbers_function_type setNumbers_function_value( &::SireOpenMM::PyQMEngine::setNumbers ); + + PyQMEngine_exposer.def( + "setNumbers" + , setNumbers_function_value + , ( bp::arg("numbers") ) + , bp::release_gil_policy() + , "Set the atomic numbers for the atoms in the QM region.\nPar:am numbers\nA vector of atomic numbers for the atoms in the QM region.\n" ); + + } + { //::SireOpenMM::PyQMEngine::typeName + + typedef char const * ( *typeName_function_type )( ); + typeName_function_type typeName_function_value( &::SireOpenMM::PyQMEngine::typeName ); + + PyQMEngine_exposer.def( + "typeName" + , typeName_function_value + , bp::release_gil_policy() + , "Return the C++ name for this class." ); + + } + { //::SireOpenMM::PyQMEngine::what + + typedef char const * ( ::SireOpenMM::PyQMEngine::*what_function_type)( ) const; + what_function_type what_function_value( &::SireOpenMM::PyQMEngine::what ); + + PyQMEngine_exposer.def( + "what" + , what_function_value + , bp::release_gil_policy() + , "Return the C++ name for this class." ); + + } + PyQMEngine_exposer.staticmethod( "typeName" ); + PyQMEngine_exposer.def( "__copy__", &__copy__); + PyQMEngine_exposer.def( "__deepcopy__", &__copy__); + PyQMEngine_exposer.def( "clone", &__copy__); + PyQMEngine_exposer.def( "__str__", &__str__< ::SireOpenMM::PyQMEngine > ); + PyQMEngine_exposer.def( "__repr__", &__str__< ::SireOpenMM::PyQMEngine > ); + } + +} diff --git a/wrapper/Convert/SireOpenMM/PyQMEngine.pypp.hpp b/wrapper/Convert/SireOpenMM/PyQMEngine.pypp.hpp new file mode 100644 index 000000000..e49bc2a0f --- /dev/null +++ b/wrapper/Convert/SireOpenMM/PyQMEngine.pypp.hpp @@ -0,0 +1,10 @@ +// This file has been generated by Py++. + +// (C) Christopher Woods, GPL >= 3 License + +#ifndef PyQMEngine_hpp__pyplusplus_wrapper +#define PyQMEngine_hpp__pyplusplus_wrapper + +void register_PyQMEngine_class(); + +#endif//PyQMEngine_hpp__pyplusplus_wrapper diff --git a/wrapper/Convert/SireOpenMM/PyQMForce.pypp.cpp b/wrapper/Convert/SireOpenMM/PyQMForce.pypp.cpp new file mode 100644 index 000000000..18797552d --- /dev/null +++ b/wrapper/Convert/SireOpenMM/PyQMForce.pypp.cpp @@ -0,0 +1,279 @@ +// This file has been generated by Py++. + +// (C) Christopher Woods, GPL >= 3 License + +#include "boost/python.hpp" +#include "PyQMForce.pypp.hpp" + +namespace bp = boost::python; + +#include "SireError/errors.h" + +#include "SireMaths/vector.h" + +#include "SireStream/datastream.h" + +#include "SireStream/shareddatastream.h" + +#include "SireVol/triclinicbox.h" + +#include "openmm/serialization/SerializationNode.h" + +#include "openmm/serialization/SerializationProxy.h" + +#include "pyqm.h" + +#include + +#include + +#include + +#include "SireError/errors.h" + +#include "SireMaths/vector.h" + +#include "SireStream/datastream.h" + +#include "SireStream/shareddatastream.h" + +#include "SireVol/triclinicbox.h" + +#include "openmm/serialization/SerializationNode.h" + +#include "openmm/serialization/SerializationProxy.h" + +#include "pyqm.h" + +#include + +#include + +#include + +SireOpenMM::PyQMForce __copy__(const SireOpenMM::PyQMForce &other){ return SireOpenMM::PyQMForce(other); } + +#include "Qt/qdatastream.hpp" + +const char* pvt_get_name(const SireOpenMM::PyQMForce&){ return "SireOpenMM::PyQMForce";} + +#include "Helpers/release_gil_policy.hpp" + +void register_PyQMForce_class(){ + + { //::SireOpenMM::PyQMForce + typedef bp::class_< SireOpenMM::PyQMForce, bp::bases< SireOpenMM::QMForce > > PyQMForce_exposer_t; + PyQMForce_exposer_t PyQMForce_exposer = PyQMForce_exposer_t( "PyQMForce", "", bp::init< >("Default constructor.") ); + bp::scope PyQMForce_scope( PyQMForce_exposer ); + PyQMForce_exposer.def( bp::init< SireOpenMM::PyQMCallback, SireUnits::Dimension::Length, int, bool, double, QVector< int >, QMap< int, int >, QMap< int, QVector< int > >, QMap< int, double >, QVector< int >, QVector< int >, QVector< double > >(( bp::arg("callback"), bp::arg("cutoff"), bp::arg("neighbour_list_frequency"), bp::arg("is_mechanical"), bp::arg("lambda"), bp::arg("atoms"), bp::arg("mm1_to_qm"), bp::arg("mm1_to_mm2"), bp::arg("bond_scale_factors"), bp::arg("mm2_atoms"), bp::arg("numbers"), bp::arg("charges") ), "Constructor.\nPar:am callback\nThe PyQMCallback object.\n\nPar:am cutoff\nThe ML cutoff distance.\n\nPar:am neighbour_list_frequency\nThe frequency at which the neighbour list is updated. (Number of steps.)\nIf zero, then no neighbour list is used.\n\nPar:am is_mechanical\nA flag to indicate if mechanical embedding is being used.\n\nPar:am lambda\nThe lambda weighting factor. This can be used to interpolate between\npotentials for end-state correction calculations.\n\nPar:am atoms\nA vector of atom indices for the QM region.\n\nPar:am mm1_to_qm\nA dictionary mapping link atom (MM1) indices to the QM atoms to\nwhich they are bonded.\n\nPar:am mm1_to_mm2\nA dictionary of link atoms indices (MM1) to a list of the MM\natoms to which they are bonded (MM2).\n\nPar:am bond_scale_factors\nA dictionary of link atom indices (MM1) to a list of the bond\nlength scale factors between the QM and MM1 atoms. The scale\nfactors are the ratio of the equilibrium bond lengths for the\nQM-L (QM-link) atom and QM-MM1 atom, i.e. R0(QM-L) R0(QM-MM1),\ntaken from the MM force field parameters for the molecule.\n\nPar:am mm2_atoms\nA vector of MM2 atom indices.\n\nPar:am numbers\nA vector of atomic charges for all atoms in the system.\n\nPar:am charges\nA vector of atomic charges for all atoms in the system.\n") ); + PyQMForce_exposer.def( bp::init< SireOpenMM::PyQMForce const & >(( bp::arg("other") ), "Copy constructor.") ); + { //::SireOpenMM::PyQMForce::call + + typedef ::boost::tuples::tuple< double, QVector< QVector< double > >, QVector< QVector< double > >, boost::tuples::null_type, boost::tuples::null_type, boost::tuples::null_type, boost::tuples::null_type, boost::tuples::null_type, boost::tuples::null_type, boost::tuples::null_type > ( ::SireOpenMM::PyQMForce::*call_function_type)( ::QVector< int >,::QVector< double >,::QVector< QVector< double > >,::QVector< QVector< double > >, ::QVector < int > ) const; + call_function_type call_function_value( &::SireOpenMM::PyQMForce::call ); + + PyQMForce_exposer.def( + "call" + , call_function_value + , ( bp::arg("numbers_qm"), bp::arg("charges_mm"), bp::arg("xyz_qm"), bp::arg("xyz_mm"), bp::arg("idx_mm") ) + , bp::release_gil_policy() + , "Call the callback function.\nPar:am numbers_qm\nA vector of atomic numbers for the atoms in the ML region.\n\nPar:am charges_mm\nA vector of the charges on the MM atoms in mod electron charge.\n\nPar:am xyz_qm\nA vector of positions for the atoms in the ML region in Angstrom.\n\nPar:am xyz_mm\nA vector of positions for the atoms in the MM region in Angstrom.\n\nPar:am idx_mm A vector of indices for the MM atoms in the QM/MM region. Note that len(idx_mm) <= len(charges_mm) since it only contains the indices of true MM atoms, not link atoms or virtual charges.\n\nReturn:s\nA tuple containing:\n- The energy in kJmol.\n- A vector of forces for the QM atoms in kJmolnm.\n- A vector of forces for the MM atoms in kJmolnm.\n" ); + + } + { //::SireOpenMM::PyQMForce::getAtoms + + typedef ::QVector< int > ( ::SireOpenMM::PyQMForce::*getAtoms_function_type)( ) const; + getAtoms_function_type getAtoms_function_value( &::SireOpenMM::PyQMForce::getAtoms ); + + PyQMForce_exposer.def( + "getAtoms" + , getAtoms_function_value + , bp::release_gil_policy() + , "Get the indices of the atoms in the QM region.\nReturn:s\nA vector of atom indices for the QM region.\n" ); + + } + { //::SireOpenMM::PyQMForce::getCallback + + typedef ::SireOpenMM::PyQMCallback ( ::SireOpenMM::PyQMForce::*getCallback_function_type)( ) const; + getCallback_function_type getCallback_function_value( &::SireOpenMM::PyQMForce::getCallback ); + + PyQMForce_exposer.def( + "getCallback" + , getCallback_function_value + , bp::release_gil_policy() + , "Get the callback object.\nReturn:s\nA Python object that contains the callback function.\n" ); + + } + { //::SireOpenMM::PyQMForce::getCharges + + typedef ::QVector< double > ( ::SireOpenMM::PyQMForce::*getCharges_function_type)( ) const; + getCharges_function_type getCharges_function_value( &::SireOpenMM::PyQMForce::getCharges ); + + PyQMForce_exposer.def( + "getCharges" + , getCharges_function_value + , bp::release_gil_policy() + , "Get the atomic charges of all atoms in the system.\nReturn:s\nA vector of atomic charges for all atoms in the system.\n" ); + + } + { //::SireOpenMM::PyQMForce::getCutoff + + typedef ::SireUnits::Dimension::Length ( ::SireOpenMM::PyQMForce::*getCutoff_function_type)( ) const; + getCutoff_function_type getCutoff_function_value( &::SireOpenMM::PyQMForce::getCutoff ); + + PyQMForce_exposer.def( + "getCutoff" + , getCutoff_function_value + , bp::release_gil_policy() + , "Get the QM cutoff distance.\nReturn:s\nThe QM cutoff distance.\n" ); + + } + { //::SireOpenMM::PyQMForce::getIsMechanical + + typedef bool ( ::SireOpenMM::PyQMForce::*getIsMechanical_function_type)( ) const; + getIsMechanical_function_type getIsMechanical_function_value( &::SireOpenMM::PyQMForce::getIsMechanical ); + + PyQMForce_exposer.def( + "getIsMechanical" + , getIsMechanical_function_value + , bp::release_gil_policy() + , "Get the mechanical embedding flag.\nReturn:s\nA flag to indicate if mechanical embedding is being used.\n" ); + + } + { //::SireOpenMM::PyQMForce::getLambda + + typedef double ( ::SireOpenMM::PyQMForce::*getLambda_function_type)( ) const; + getLambda_function_type getLambda_function_value( &::SireOpenMM::PyQMForce::getLambda ); + + PyQMForce_exposer.def( + "getLambda" + , getLambda_function_value + , bp::release_gil_policy() + , "Get the lambda weighting factor.\nReturn:s\nThe lambda weighting factor.\n" ); + + } + { //::SireOpenMM::PyQMForce::getLinkAtoms + + typedef ::boost::tuples::tuple< QMap< int, int >, QMap< int, QVector< int > >, QMap< int, double >, boost::tuples::null_type, boost::tuples::null_type, boost::tuples::null_type, boost::tuples::null_type, boost::tuples::null_type, boost::tuples::null_type, boost::tuples::null_type > ( ::SireOpenMM::PyQMForce::*getLinkAtoms_function_type)( ) const; + getLinkAtoms_function_type getLinkAtoms_function_value( &::SireOpenMM::PyQMForce::getLinkAtoms ); + + PyQMForce_exposer.def( + "getLinkAtoms" + , getLinkAtoms_function_value + , bp::release_gil_policy() + , "Get the link atoms associated with each QM atom.\nReturn:s\nA tuple containing:\n\nmm1_to_qm\nA dictionary mapping link atom (MM1) indices to the QM atoms to\nwhich they are bonded.\n\nmm1_to_mm2\nA dictionary of link atoms indices (MM1) to a list of the MM\natoms to which they are bonded (MM2).\n\nbond_scale_factors\nA dictionary of link atom indices (MM1) to a list of the bond\nlength scale factors between the QM and MM1 atoms. The scale\nfactors are the ratio of the equilibrium bond lengths for the\nQM-L (QM-link) atom and QM-MM1 atom, i.e. R0(QM-L) R0(QM-MM1),\ntaken from the MM force field parameters for the molecule.\n\n" ); + + } + { //::SireOpenMM::PyQMForce::getMM2Atoms + + typedef ::QVector< int > ( ::SireOpenMM::PyQMForce::*getMM2Atoms_function_type)( ) const; + getMM2Atoms_function_type getMM2Atoms_function_value( &::SireOpenMM::PyQMForce::getMM2Atoms ); + + PyQMForce_exposer.def( + "getMM2Atoms" + , getMM2Atoms_function_value + , bp::release_gil_policy() + , "Get the vector of MM2 atoms.\nReturn:s\nA vector of MM2 atom indices.\n" ); + + } + { //::SireOpenMM::PyQMForce::getNeighbourListFrequency + + typedef int ( ::SireOpenMM::PyQMForce::*getNeighbourListFrequency_function_type)( ) const; + getNeighbourListFrequency_function_type getNeighbourListFrequency_function_value( &::SireOpenMM::PyQMForce::getNeighbourListFrequency ); + + PyQMForce_exposer.def( + "getNeighbourListFrequency" + , getNeighbourListFrequency_function_value + , bp::release_gil_policy() + , "Get the neighbour list frequency.\nReturn:s\nThe neighbour list frequency.\n" ); + + } + { //::SireOpenMM::PyQMForce::getNumbers + + typedef ::QVector< int > ( ::SireOpenMM::PyQMForce::*getNumbers_function_type)( ) const; + getNumbers_function_type getNumbers_function_value( &::SireOpenMM::PyQMForce::getNumbers ); + + PyQMForce_exposer.def( + "getNumbers" + , getNumbers_function_value + , bp::release_gil_policy() + , "Get the atomic numbers for the atoms in the QM region.\nReturn:s\nA vector of atomic numbers for the atoms in the QM region.\n" ); + + } + { //::SireOpenMM::PyQMForce::operator= + + typedef ::SireOpenMM::PyQMForce & ( ::SireOpenMM::PyQMForce::*assign_function_type)( ::SireOpenMM::PyQMForce const & ) ; + assign_function_type assign_function_value( &::SireOpenMM::PyQMForce::operator= ); + + PyQMForce_exposer.def( + "assign" + , assign_function_value + , ( bp::arg("other") ) + , bp::return_self< >() + , "Assignment operator." ); + + } + { //::SireOpenMM::PyQMForce::setCallback + + typedef void ( ::SireOpenMM::PyQMForce::*setCallback_function_type)( ::SireOpenMM::PyQMCallback ) ; + setCallback_function_type setCallback_function_value( &::SireOpenMM::PyQMForce::setCallback ); + + PyQMForce_exposer.def( + "setCallback" + , setCallback_function_value + , ( bp::arg("callback") ) + , bp::release_gil_policy() + , "Set the callback object.\nPar:am callback\nA Python object that contains the callback function.\n" ); + + } + { //::SireOpenMM::PyQMForce::setLambda + + typedef void ( ::SireOpenMM::PyQMForce::*setLambda_function_type)( double ) ; + setLambda_function_type setLambda_function_value( &::SireOpenMM::PyQMForce::setLambda ); + + PyQMForce_exposer.def( + "setLambda" + , setLambda_function_value + , ( bp::arg("lambda") ) + , bp::release_gil_policy() + , "Set the lambda weighting factor\nPar:am lambda\nThe lambda weighting factor.\n" ); + + } + { //::SireOpenMM::PyQMForce::typeName + + typedef char const * ( *typeName_function_type )( ); + typeName_function_type typeName_function_value( &::SireOpenMM::PyQMForce::typeName ); + + PyQMForce_exposer.def( + "typeName" + , typeName_function_value + , bp::release_gil_policy() + , "Return the C++ name for this class." ); + + } + { //::SireOpenMM::PyQMForce::what + + typedef char const * ( ::SireOpenMM::PyQMForce::*what_function_type)( ) const; + what_function_type what_function_value( &::SireOpenMM::PyQMForce::what ); + + PyQMForce_exposer.def( + "what" + , what_function_value + , bp::release_gil_policy() + , "Return the C++ name for this class." ); + + } + PyQMForce_exposer.staticmethod( "typeName" ); + PyQMForce_exposer.def( "__copy__", &__copy__); + PyQMForce_exposer.def( "__deepcopy__", &__copy__); + PyQMForce_exposer.def( "clone", &__copy__); + PyQMForce_exposer.def( "__rlshift__", &__rlshift__QDataStream< ::SireOpenMM::PyQMForce >, + bp::return_internal_reference<1, bp::with_custodian_and_ward<1,2> >() ); + PyQMForce_exposer.def( "__rrshift__", &__rrshift__QDataStream< ::SireOpenMM::PyQMForce >, + bp::return_internal_reference<1, bp::with_custodian_and_ward<1,2> >() ); + PyQMForce_exposer.def_pickle(sire_pickle_suite< ::SireOpenMM::PyQMForce >()); + PyQMForce_exposer.def( "__str__", &pvt_get_name); + PyQMForce_exposer.def( "__repr__", &pvt_get_name); + } + +} diff --git a/wrapper/Convert/SireOpenMM/PyQMForce.pypp.hpp b/wrapper/Convert/SireOpenMM/PyQMForce.pypp.hpp new file mode 100644 index 000000000..40b5f0f11 --- /dev/null +++ b/wrapper/Convert/SireOpenMM/PyQMForce.pypp.hpp @@ -0,0 +1,10 @@ +// This file has been generated by Py++. + +// (C) Christopher Woods, GPL >= 3 License + +#ifndef PyQMForce_hpp__pyplusplus_wrapper +#define PyQMForce_hpp__pyplusplus_wrapper + +void register_PyQMForce_class(); + +#endif//PyQMForce_hpp__pyplusplus_wrapper diff --git a/wrapper/Convert/SireOpenMM/QMEngine.pypp.cpp b/wrapper/Convert/SireOpenMM/QMEngine.pypp.cpp new file mode 100644 index 000000000..d1656fbb5 --- /dev/null +++ b/wrapper/Convert/SireOpenMM/QMEngine.pypp.cpp @@ -0,0 +1,58 @@ +// This file has been generated by Py++. + +// (C) Christopher Woods, GPL >= 3 License + +#include "boost/python.hpp" +#include "QMEngine.pypp.hpp" + +namespace bp = boost::python; + +#include "SireError/errors.h" + +#include "qmmm.h" + +#include "SireError/errors.h" + +#include "qmmm.h" + +#include "Helpers/str.hpp" + +#include "Helpers/release_gil_policy.hpp" + +void register_QMEngine_class(){ + + { //::SireOpenMM::QMEngine + typedef bp::class_< SireOpenMM::QMEngine, boost::noncopyable > QMEngine_exposer_t; + QMEngine_exposer_t QMEngine_exposer = QMEngine_exposer_t( "QMEngine", "", bp::no_init ); + bp::scope QMEngine_scope( QMEngine_exposer ); + { //::SireOpenMM::QMEngine::null + + typedef ::SireOpenMM::NullQMEngine const & ( *null_function_type )( ); + null_function_type null_function_value( &::SireOpenMM::QMEngine::null ); + + QMEngine_exposer.def( + "null" + , null_function_value + , bp::return_value_policy< bp::copy_const_reference >() + , "Get a null QM engine." ); + + } + { //::SireOpenMM::QMEngine::typeName + + typedef char const * ( *typeName_function_type )( ); + typeName_function_type typeName_function_value( &::SireOpenMM::QMEngine::typeName ); + + QMEngine_exposer.def( + "typeName" + , typeName_function_value + , bp::release_gil_policy() + , "" ); + + } + QMEngine_exposer.staticmethod( "null" ); + QMEngine_exposer.staticmethod( "typeName" ); + QMEngine_exposer.def( "__str__", &__str__< ::SireOpenMM::QMEngine > ); + QMEngine_exposer.def( "__repr__", &__str__< ::SireOpenMM::QMEngine > ); + } + +} diff --git a/wrapper/Convert/SireOpenMM/QMEngine.pypp.hpp b/wrapper/Convert/SireOpenMM/QMEngine.pypp.hpp new file mode 100644 index 000000000..bf9bd075c --- /dev/null +++ b/wrapper/Convert/SireOpenMM/QMEngine.pypp.hpp @@ -0,0 +1,10 @@ +// This file has been generated by Py++. + +// (C) Christopher Woods, GPL >= 3 License + +#ifndef QMEngine_hpp__pyplusplus_wrapper +#define QMEngine_hpp__pyplusplus_wrapper + +void register_QMEngine_class(); + +#endif//QMEngine_hpp__pyplusplus_wrapper diff --git a/wrapper/Convert/SireOpenMM/QMForce.pypp.cpp b/wrapper/Convert/SireOpenMM/QMForce.pypp.cpp new file mode 100644 index 000000000..6988a0647 --- /dev/null +++ b/wrapper/Convert/SireOpenMM/QMForce.pypp.cpp @@ -0,0 +1,45 @@ +// This file has been generated by Py++. + +// (C) Christopher Woods, GPL >= 3 License + +#include "boost/python.hpp" +#include "QMForce.pypp.hpp" + +namespace bp = boost::python; + +#include "SireError/errors.h" + +#include "qmmm.h" + +#include "SireError/errors.h" + +#include "qmmm.h" + +const char* pvt_get_name(const SireOpenMM::QMForce&){ return "SireOpenMM::QMForce";} + +#include "Helpers/release_gil_policy.hpp" + +void register_QMForce_class(){ + + { //::SireOpenMM::QMForce + typedef bp::class_< SireOpenMM::QMForce, boost::noncopyable > QMForce_exposer_t; + QMForce_exposer_t QMForce_exposer = QMForce_exposer_t( "QMForce", "", bp::no_init ); + bp::scope QMForce_scope( QMForce_exposer ); + { //::SireOpenMM::QMForce::setLambda + + typedef void ( ::SireOpenMM::QMForce::*setLambda_function_type)( double ) ; + setLambda_function_type setLambda_function_value( &::SireOpenMM::QMForce::setLambda ); + + QMForce_exposer.def( + "setLambda" + , setLambda_function_value + , ( bp::arg("lambda") ) + , bp::release_gil_policy() + , "Set the lambda weighting factor." ); + + } + QMForce_exposer.def( "__str__", &pvt_get_name); + QMForce_exposer.def( "__repr__", &pvt_get_name); + } + +} diff --git a/wrapper/Convert/SireOpenMM/QMForce.pypp.hpp b/wrapper/Convert/SireOpenMM/QMForce.pypp.hpp new file mode 100644 index 000000000..71dde7d32 --- /dev/null +++ b/wrapper/Convert/SireOpenMM/QMForce.pypp.hpp @@ -0,0 +1,10 @@ +// This file has been generated by Py++. + +// (C) Christopher Woods, GPL >= 3 License + +#ifndef QMForce_hpp__pyplusplus_wrapper +#define QMForce_hpp__pyplusplus_wrapper + +void register_QMForce_class(); + +#endif//QMForce_hpp__pyplusplus_wrapper diff --git a/wrapper/Convert/SireOpenMM/SireOpenMM_properties.cpp b/wrapper/Convert/SireOpenMM/SireOpenMM_properties.cpp new file mode 100644 index 000000000..1bb490cf7 --- /dev/null +++ b/wrapper/Convert/SireOpenMM/SireOpenMM_properties.cpp @@ -0,0 +1,15 @@ +#include +#include + +#include "Base/convertproperty.hpp" +#include "SireOpenMM_properties.h" + +#include "SireError/errors.h" +#include "qmmm.h" +#include "SireError/errors.h" +#include "qmmm.h" +void register_SireOpenMM_properties() +{ + register_property_container< SireOpenMM::QMEnginePtr, SireOpenMM::QMEngine >(); + register_property_container< SireOpenMM::QMEnginePtr, SireOpenMM::QMEngine >(); +} diff --git a/wrapper/Convert/SireOpenMM/SireOpenMM_properties.h b/wrapper/Convert/SireOpenMM/SireOpenMM_properties.h new file mode 100644 index 000000000..346789c91 --- /dev/null +++ b/wrapper/Convert/SireOpenMM/SireOpenMM_properties.h @@ -0,0 +1,6 @@ +#ifndef SireOpenMM_PROPERTIES_H +#define SireOpenMM_PROPERTIES_H + +void register_SireOpenMM_properties(); + +#endif diff --git a/wrapper/Convert/SireOpenMM/SireOpenMM_registrars.cpp b/wrapper/Convert/SireOpenMM/SireOpenMM_registrars.cpp index de06e3b14..7a88a370c 100644 --- a/wrapper/Convert/SireOpenMM/SireOpenMM_registrars.cpp +++ b/wrapper/Convert/SireOpenMM/SireOpenMM_registrars.cpp @@ -3,18 +3,31 @@ #include "SireOpenMM_registrars.h" -#include "openmmmolecule.h" +#include "qmmm.h" +#include "pyqm.h" #include "lambdalever.h" +#include "openmmmolecule.h" +#include "torchqm.h" #include "Helpers/objectregistry.hpp" void register_SireOpenMM_objects() { - ObjectRegistry::registerConverterFor< SireOpenMM::PerturbableOpenMMMolecule >(); - ObjectRegistry::registerConverterFor< SireOpenMM::PerturbableOpenMMMolecule >(); + ObjectRegistry::registerConverterFor< SireOpenMM::NullQMEngine >(); + ObjectRegistry::registerConverterFor< SireOpenMM::NullQMEngine >(); + ObjectRegistry::registerConverterFor< SireOpenMM::PyQMCallback >(); + ObjectRegistry::registerConverterFor< SireOpenMM::PyQMForce >(); + ObjectRegistry::registerConverterFor< SireOpenMM::PyQMEngine >(); + ObjectRegistry::registerConverterFor< SireOpenMM::PyQMCallback >(); + ObjectRegistry::registerConverterFor< SireOpenMM::PyQMForce >(); + ObjectRegistry::registerConverterFor< SireOpenMM::PyQMEngine >(); ObjectRegistry::registerConverterFor< SireOpenMM::LambdaLever >(); ObjectRegistry::registerConverterFor< SireOpenMM::LambdaLever >(); + ObjectRegistry::registerConverterFor< SireOpenMM::PerturbableOpenMMMolecule >(); + ObjectRegistry::registerConverterFor< SireOpenMM::PerturbableOpenMMMolecule >(); + ObjectRegistry::registerConverterFor< SireOpenMM::TorchQMForce >(); + ObjectRegistry::registerConverterFor< SireOpenMM::TorchQMEngine >(); } diff --git a/wrapper/Convert/SireOpenMM/TorchQMEngine.pypp.cpp b/wrapper/Convert/SireOpenMM/TorchQMEngine.pypp.cpp new file mode 100644 index 000000000..29847e98d --- /dev/null +++ b/wrapper/Convert/SireOpenMM/TorchQMEngine.pypp.cpp @@ -0,0 +1,338 @@ +// This file has been generated by Py++. + +// (C) Christopher Woods, GPL >= 3 License + +#include "boost/python.hpp" +#include "TorchQMEngine.pypp.hpp" + +namespace bp = boost::python; + +#include "SireError/errors.h" + +#include "SireMaths/vector.h" + +#include "SireStream/datastream.h" + +#include "SireStream/shareddatastream.h" + +#include "SireVol/triclinicbox.h" + +#include "openmm/serialization/SerializationNode.h" + +#include "openmm/serialization/SerializationProxy.h" + +#include "torchqm.h" + +#include "SireError/errors.h" + +#include "SireMaths/vector.h" + +#include "SireStream/datastream.h" + +#include "SireStream/shareddatastream.h" + +#include "SireVol/triclinicbox.h" + +#include "openmm/serialization/SerializationNode.h" + +#include "openmm/serialization/SerializationProxy.h" + +#include "torchqm.h" + +SireOpenMM::TorchQMEngine __copy__(const SireOpenMM::TorchQMEngine &other){ return SireOpenMM::TorchQMEngine(other); } + +#include "Helpers/str.hpp" + +#include "Helpers/release_gil_policy.hpp" + +void register_TorchQMEngine_class(){ + + { //::SireOpenMM::TorchQMEngine + typedef bp::class_< SireOpenMM::TorchQMEngine, bp::bases< SireBase::Property, SireOpenMM::QMEngine > > TorchQMEngine_exposer_t; + TorchQMEngine_exposer_t TorchQMEngine_exposer = TorchQMEngine_exposer_t( "TorchQMEngine", "", bp::init< >("Default constructor.") ); + bp::scope TorchQMEngine_scope( TorchQMEngine_exposer ); + TorchQMEngine_exposer.def( bp::init< QString, bp::optional< SireUnits::Dimension::Length, int, bool, double > >(( bp::arg("arg0"), bp::arg("cutoff")=7.5 * SireUnits::angstrom, bp::arg("neighbour_list_frequency")=(int)(0), bp::arg("is_mechanical")=(bool)(false), bp::arg("lambda")=1. ), "Constructor\nPar:am module_path\nThe path to the serialised TorchScript module.\n\nPar:am cutoff\nThe ML cutoff distance.\n\nPar:am neighbour_list_frequency\nThe frequency at which the neighbour list is updated. (Number of steps.)\nIf zero, then no neighbour list is used.\n\nPar:am is_mechanical\nA flag to indicate if mechanical embedding is being used.\n\nPar:am lambda\nThe lambda weighting factor. This can be used to interpolate between\npotentials for end-state correction calculations.\n") ); + TorchQMEngine_exposer.def( bp::init< SireOpenMM::TorchQMEngine const & >(( bp::arg("other") ), "Copy constructor.") ); + { //::SireOpenMM::TorchQMEngine::getModulePath + + typedef ::QString ( ::SireOpenMM::TorchQMEngine::*getModulePath_function_type)( ) const; + getModulePath_function_type getModulePath_function_value( &::SireOpenMM::TorchQMEngine::getModulePath ); + + TorchQMEngine_exposer.def( + "getModulePath" + , getModulePath_function_value + , bp::release_gil_policy() + , "Get the path to the serialised TorchScript module.\n" ); + + } + { //::SireOpenMM::TorchQMEngine::getAtoms + + typedef ::QVector< int > ( ::SireOpenMM::TorchQMEngine::*getAtoms_function_type)( ) const; + getAtoms_function_type getAtoms_function_value( &::SireOpenMM::TorchQMEngine::getAtoms ); + + TorchQMEngine_exposer.def( + "getAtoms" + , getAtoms_function_value + , bp::release_gil_policy() + , "Get the indices of the atoms in the QM region.\nReturn:s\nA vector of atom indices for the QM region.\n" ); + + } + { //::SireOpenMM::TorchQMEngine::getCharges + + typedef ::QVector< double > ( ::SireOpenMM::TorchQMEngine::*getCharges_function_type)( ) const; + getCharges_function_type getCharges_function_value( &::SireOpenMM::TorchQMEngine::getCharges ); + + TorchQMEngine_exposer.def( + "getCharges" + , getCharges_function_value + , bp::release_gil_policy() + , "Get the atomic charges of all atoms in the system.\nReturn:s\nA vector of atomic charges for all atoms in the system.\n" ); + + } + { //::SireOpenMM::TorchQMEngine::getCutoff + + typedef ::SireUnits::Dimension::Length ( ::SireOpenMM::TorchQMEngine::*getCutoff_function_type)( ) const; + getCutoff_function_type getCutoff_function_value( &::SireOpenMM::TorchQMEngine::getCutoff ); + + TorchQMEngine_exposer.def( + "getCutoff" + , getCutoff_function_value + , bp::release_gil_policy() + , "Get the QM cutoff distance.\nReturn:s\nThe QM cutoff distance.\n" ); + + } + { //::SireOpenMM::TorchQMEngine::getIsMechanical + + typedef bool ( ::SireOpenMM::TorchQMEngine::*getIsMechanical_function_type)( ) const; + getIsMechanical_function_type getIsMechanical_function_value( &::SireOpenMM::TorchQMEngine::getIsMechanical ); + + TorchQMEngine_exposer.def( + "getIsMechanical" + , getIsMechanical_function_value + , bp::release_gil_policy() + , "Get the mechanical embedding flag.\nReturn:s\nA flag to indicate if mechanical embedding is being used.\n" ); + + } + { //::SireOpenMM::TorchQMEngine::getLambda + + typedef double ( ::SireOpenMM::TorchQMEngine::*getLambda_function_type)( ) const; + getLambda_function_type getLambda_function_value( &::SireOpenMM::TorchQMEngine::getLambda ); + + TorchQMEngine_exposer.def( + "getLambda" + , getLambda_function_value + , bp::release_gil_policy() + , "Get the lambda weighting factor.\nReturn:s\nThe lambda weighting factor.\n" ); + + } + { //::SireOpenMM::TorchQMEngine::getLinkAtoms + + typedef ::boost::tuples::tuple< QMap< int, int >, QMap< int, QVector< int > >, QMap< int, double >, boost::tuples::null_type, boost::tuples::null_type, boost::tuples::null_type, boost::tuples::null_type, boost::tuples::null_type, boost::tuples::null_type, boost::tuples::null_type > ( ::SireOpenMM::TorchQMEngine::*getLinkAtoms_function_type)( ) const; + getLinkAtoms_function_type getLinkAtoms_function_value( &::SireOpenMM::TorchQMEngine::getLinkAtoms ); + + TorchQMEngine_exposer.def( + "getLinkAtoms" + , getLinkAtoms_function_value + , bp::release_gil_policy() + , "Get the link atoms associated with each QM atom.\nReturn:s\nA tuple containing:\n\nmm1_to_qm\nA dictionary mapping link atom (MM1) indices to the QM atoms to\nwhich they are bonded.\n\nmm1_to_mm2\nA dictionary of link atoms indices (MM1) to a list of the MM\natoms to which they are bonded (MM2).\n\nbond_scale_factors\nA dictionary of link atom indices (MM1) to a list of the bond\nlength scale factors between the QM and MM1 atoms. The scale\nfactors are the ratio of the equilibrium bond lengths for the\nQM-L (QM-link) atom and QM-MM1 atom, i.e. R0(QM-L) R0(QM-MM1),\ntaken from the MM force field parameters for the molecule.\n\n" ); + + } + { //::SireOpenMM::TorchQMEngine::getMM2Atoms + + typedef ::QVector< int > ( ::SireOpenMM::TorchQMEngine::*getMM2Atoms_function_type)( ) const; + getMM2Atoms_function_type getMM2Atoms_function_value( &::SireOpenMM::TorchQMEngine::getMM2Atoms ); + + TorchQMEngine_exposer.def( + "getMM2Atoms" + , getMM2Atoms_function_value + , bp::release_gil_policy() + , "Get the vector of MM2 atoms.\nReturn:s\nA vector of MM2 atom indices.\n" ); + + } + { //::SireOpenMM::TorchQMEngine::getNeighbourListFrequency + + typedef int ( ::SireOpenMM::TorchQMEngine::*getNeighbourListFrequency_function_type)( ) const; + getNeighbourListFrequency_function_type getNeighbourListFrequency_function_value( &::SireOpenMM::TorchQMEngine::getNeighbourListFrequency ); + + TorchQMEngine_exposer.def( + "getNeighbourListFrequency" + , getNeighbourListFrequency_function_value + , bp::release_gil_policy() + , "Get the neighbour list frequency.\nReturn:s\nThe neighbour list frequency.\n" ); + + } + { //::SireOpenMM::TorchQMEngine::getNumbers + + typedef ::QVector< int > ( ::SireOpenMM::TorchQMEngine::*getNumbers_function_type)( ) const; + getNumbers_function_type getNumbers_function_value( &::SireOpenMM::TorchQMEngine::getNumbers ); + + TorchQMEngine_exposer.def( + "getNumbers" + , getNumbers_function_value + , bp::release_gil_policy() + , "Get the atomic numbers for the atoms in the QM region.\nReturn:s\nA vector of atomic numbers for the atoms in the QM region.\n" ); + + } + { //::SireOpenMM::TorchQMEngine::operator= + + typedef ::SireOpenMM::TorchQMEngine & ( ::SireOpenMM::TorchQMEngine::*assign_function_type)( ::SireOpenMM::TorchQMEngine const & ) ; + assign_function_type assign_function_value( &::SireOpenMM::TorchQMEngine::operator= ); + + TorchQMEngine_exposer.def( + "assign" + , assign_function_value + , ( bp::arg("other") ) + , bp::return_self< >() + , "Assignment operator." ); + + } + { //::SireOpenMM::TorchQMEngine::setModulePath + + typedef void ( ::SireOpenMM::TorchQMEngine::*setModulePath_function_type)( ::QString ) ; + setModulePath_function_type setModulePath_function_value( &::SireOpenMM::TorchQMEngine::setModulePath ); + + TorchQMEngine_exposer.def( + "setModulePath" + , setModulePath_function_value + , ( bp::arg("module_path") ) + , bp::release_gil_policy() + , "Set the path to the serialised TorchScript module.\n" ); + + } + { //::SireOpenMM::TorchQMEngine::setAtoms + + typedef void ( ::SireOpenMM::TorchQMEngine::*setAtoms_function_type)( ::QVector< int > ) ; + setAtoms_function_type setAtoms_function_value( &::SireOpenMM::TorchQMEngine::setAtoms ); + + TorchQMEngine_exposer.def( + "setAtoms" + , setAtoms_function_value + , ( bp::arg("atoms") ) + , bp::release_gil_policy() + , "Set the list of atom indices for the QM region.\nPar:am atoms\nA vector of atom indices for the QM region.\n" ); + + } + { //::SireOpenMM::TorchQMEngine::setCharges + + typedef void ( ::SireOpenMM::TorchQMEngine::*setCharges_function_type)( ::QVector< double > ) ; + setCharges_function_type setCharges_function_value( &::SireOpenMM::TorchQMEngine::setCharges ); + + TorchQMEngine_exposer.def( + "setCharges" + , setCharges_function_value + , ( bp::arg("charges") ) + , bp::release_gil_policy() + , "Set the atomic charges of all atoms in the system.\nPar:am charges\nA vector of atomic charges for all atoms in the system.\n" ); + + } + { //::SireOpenMM::TorchQMEngine::setCutoff + + typedef void ( ::SireOpenMM::TorchQMEngine::*setCutoff_function_type)( ::SireUnits::Dimension::Length ) ; + setCutoff_function_type setCutoff_function_value( &::SireOpenMM::TorchQMEngine::setCutoff ); + + TorchQMEngine_exposer.def( + "setCutoff" + , setCutoff_function_value + , ( bp::arg("cutoff") ) + , bp::release_gil_policy() + , "Set the QM cutoff distance.\nPar:am cutoff\nThe QM cutoff distance.\n" ); + + } + { //::SireOpenMM::TorchQMEngine::setIsMechanical + + typedef void ( ::SireOpenMM::TorchQMEngine::*setIsMechanical_function_type)( bool ) ; + setIsMechanical_function_type setIsMechanical_function_value( &::SireOpenMM::TorchQMEngine::setIsMechanical ); + + TorchQMEngine_exposer.def( + "setIsMechanical" + , setIsMechanical_function_value + , ( bp::arg("is_mechanical") ) + , bp::release_gil_policy() + , "Set the mechanical embedding flag.\nPar:am is_mechanical\nA flag to indicate if mechanical embedding is being used.\n" ); + + } + { //::SireOpenMM::TorchQMEngine::setLambda + + typedef void ( ::SireOpenMM::TorchQMEngine::*setLambda_function_type)( double ) ; + setLambda_function_type setLambda_function_value( &::SireOpenMM::TorchQMEngine::setLambda ); + + TorchQMEngine_exposer.def( + "setLambda" + , setLambda_function_value + , ( bp::arg("lambda") ) + , bp::release_gil_policy() + , "Set the lambda weighting factor.\nPar:am lambda\nThe lambda weighting factor.\n" ); + + } + { //::SireOpenMM::TorchQMEngine::setLinkAtoms + + typedef void ( ::SireOpenMM::TorchQMEngine::*setLinkAtoms_function_type)( ::QMap< int, int >,::QMap< int, QVector< int > >,::QMap< int, double > ) ; + setLinkAtoms_function_type setLinkAtoms_function_value( &::SireOpenMM::TorchQMEngine::setLinkAtoms ); + + TorchQMEngine_exposer.def( + "setLinkAtoms" + , setLinkAtoms_function_value + , ( bp::arg("mm1_to_qm"), bp::arg("mm1_to_mm2"), bp::arg("bond_scale_factors") ) + , bp::release_gil_policy() + , "Set the link atoms associated with each QM atom.\nPar:am mm1_to_qm\nA dictionary mapping link atom (MM1) indices to the QM atoms to\nwhich they are bonded.\n\nPar:am mm1_to_mm2\nA dictionary of link atoms indices (MM1) to a list of the MM\natoms to which they are bonded (MM2).\n\nPar:am bond_scale_factors\nA dictionary of link atom indices (MM1) to a list of the bond\nlength scale factors between the QM and MM1 atoms. The scale\nfactors are the ratio of the equilibrium bond lengths for the\nQM-L (QM-link) atom and QM-MM1 atom, i.e. R0(QM-L) R0(QM-MM1),\ntaken from the MM force field parameters for the molecule.\n\n" ); + + } + { //::SireOpenMM::TorchQMEngine::setNeighbourListFrequency + + typedef void ( ::SireOpenMM::TorchQMEngine::*setNeighbourListFrequency_function_type)( int ) ; + setNeighbourListFrequency_function_type setNeighbourListFrequency_function_value( &::SireOpenMM::TorchQMEngine::setNeighbourListFrequency ); + + TorchQMEngine_exposer.def( + "setNeighbourListFrequency" + , setNeighbourListFrequency_function_value + , ( bp::arg("neighbour_list_frequency") ) + , bp::release_gil_policy() + , "Set the neighbour list frequency.\nPar:am neighbour_list_frequency\nThe neighbour list frequency.\n" ); + + } + { //::SireOpenMM::TorchQMEngine::setNumbers + + typedef void ( ::SireOpenMM::TorchQMEngine::*setNumbers_function_type)( ::QVector< int > ) ; + setNumbers_function_type setNumbers_function_value( &::SireOpenMM::TorchQMEngine::setNumbers ); + + TorchQMEngine_exposer.def( + "setNumbers" + , setNumbers_function_value + , ( bp::arg("numbers") ) + , bp::release_gil_policy() + , "Set the atomic numbers for the atoms in the QM region.\nPar:am numbers\nA vector of atomic numbers for the atoms in the QM region.\n" ); + + } + { //::SireOpenMM::TorchQMEngine::typeName + + typedef char const * ( *typeName_function_type )( ); + typeName_function_type typeName_function_value( &::SireOpenMM::TorchQMEngine::typeName ); + + TorchQMEngine_exposer.def( + "typeName" + , typeName_function_value + , bp::release_gil_policy() + , "Return the C++ name for this class." ); + + } + { //::SireOpenMM::TorchQMEngine::what + + typedef char const * ( ::SireOpenMM::TorchQMEngine::*what_function_type)( ) const; + what_function_type what_function_value( &::SireOpenMM::TorchQMEngine::what ); + + TorchQMEngine_exposer.def( + "what" + , what_function_value + , bp::release_gil_policy() + , "Return the C++ name for this class." ); + + } + TorchQMEngine_exposer.staticmethod( "typeName" ); + TorchQMEngine_exposer.def( "__copy__", &__copy__); + TorchQMEngine_exposer.def( "__deepcopy__", &__copy__); + TorchQMEngine_exposer.def( "clone", &__copy__); + TorchQMEngine_exposer.def( "__str__", &__str__< ::SireOpenMM::TorchQMEngine > ); + TorchQMEngine_exposer.def( "__repr__", &__str__< ::SireOpenMM::TorchQMEngine > ); + } + +} diff --git a/wrapper/Convert/SireOpenMM/TorchQMEngine.pypp.hpp b/wrapper/Convert/SireOpenMM/TorchQMEngine.pypp.hpp new file mode 100644 index 000000000..30458e40a --- /dev/null +++ b/wrapper/Convert/SireOpenMM/TorchQMEngine.pypp.hpp @@ -0,0 +1,10 @@ +// This file has been generated by Py++. + +// (C) Christopher Woods, GPL >= 3 License + +#ifndef TorchQMEngine_hpp__pyplusplus_wrapper +#define TorchQMEngine_hpp__pyplusplus_wrapper + +void register_TorchQMEngine_class(); + +#endif//TorchQMEngine_hpp__pyplusplus_wrapper diff --git a/wrapper/Convert/SireOpenMM/TorchQMForce.pypp.cpp b/wrapper/Convert/SireOpenMM/TorchQMForce.pypp.cpp new file mode 100644 index 000000000..7e13a3dda --- /dev/null +++ b/wrapper/Convert/SireOpenMM/TorchQMForce.pypp.cpp @@ -0,0 +1,254 @@ +// This file has been generated by Py++. + +// (C) Christopher Woods, GPL >= 3 License + +#include "boost/python.hpp" +#include "TorchQMForce.pypp.hpp" + +namespace bp = boost::python; + +#include "SireError/errors.h" + +#include "SireMaths/vector.h" + +#include "SireStream/datastream.h" + +#include "SireStream/shareddatastream.h" + +#include "SireVol/triclinicbox.h" + +#include "openmm/serialization/SerializationNode.h" + +#include "openmm/serialization/SerializationProxy.h" + +#include "torchqm.h" + +#include "SireError/errors.h" + +#include "SireMaths/vector.h" + +#include "SireStream/datastream.h" + +#include "SireStream/shareddatastream.h" + +#include "SireVol/triclinicbox.h" + +#include "openmm/serialization/SerializationNode.h" + +#include "openmm/serialization/SerializationProxy.h" + +#include "torchqm.h" + +SireOpenMM::TorchQMForce __copy__(const SireOpenMM::TorchQMForce &other){ return SireOpenMM::TorchQMForce(other); } + +#include "Qt/qdatastream.hpp" + +const char* pvt_get_name(const SireOpenMM::TorchQMForce&){ return "SireOpenMM::TorchQMForce";} + +#include "Helpers/release_gil_policy.hpp" + +void register_TorchQMForce_class(){ + + { //::SireOpenMM::TorchQMForce + typedef bp::class_< SireOpenMM::TorchQMForce, bp::bases< SireOpenMM::QMForce > > TorchQMForce_exposer_t; + TorchQMForce_exposer_t TorchQMForce_exposer = TorchQMForce_exposer_t( "TorchQMForce", "", bp::init< >("Default constructor.") ); + bp::scope TorchQMForce_scope( TorchQMForce_exposer ); + TorchQMForce_exposer.def( bp::init< QString, SireUnits::Dimension::Length, int, bool, double, QVector< int >, QMap< int, int >, QMap< int, QVector< int > >, QMap< int, double >, QVector< int >, QVector< int >, QVector< double > >(( bp::arg("module_path"), bp::arg("cutoff"), bp::arg("neighbour_list_frequency"), bp::arg("is_mechanical"), bp::arg("lambda"), bp::arg("atoms"), bp::arg("mm1_to_qm"), bp::arg("mm1_to_mm2"), bp::arg("bond_scale_factors"), bp::arg("mm2_atoms"), bp::arg("numbers"), bp::arg("charges") ), "Constructor.\nPar:am module_path\nThe path to the serialised TorchScript module.\n\nPar:am cutoff\nThe ML cutoff distance.\n\nPar:am neighbour_list_frequency\nThe frequency at which the neighbour list is updated. (Number of steps.)\nIf zero, then no neighbour list is used.\n\nPar:am is_mechanical\nA flag to indicate if mechanical embedding is being used.\n\nPar:am lambda\nThe lambda weighting factor. This can be used to interpolate between\npotentials for end-state correction calculations.\n\nPar:am atoms\nA vector of atom indices for the QM region.\n\nPar:am mm1_to_qm\nA dictionary mapping link atom (MM1) indices to the QM atoms to\nwhich they are bonded.\n\nPar:am mm1_to_mm2\nA dictionary of link atoms indices (MM1) to a list of the MM\natoms to which they are bonded (MM2).\n\nPar:am bond_scale_factors\nA dictionary of link atom indices (MM1) to a list of the bond\nlength scale factors between the QM and MM1 atoms. The scale\nfactors are the ratio of the equilibrium bond lengths for the\nQM-L (QM-link) atom and QM-MM1 atom, i.e. R0(QM-L) R0(QM-MM1),\ntaken from the MM force field parameters for the molecule.\n\nPar:am mm2_atoms\nA vector of MM2 atom indices.\n\nPar:am numbers\nA vector of atomic charges for all atoms in the system.\n\nPar:am charges\nA vector of atomic charges for all atoms in the system.\n") ); + TorchQMForce_exposer.def( bp::init< SireOpenMM::TorchQMForce const & >(( bp::arg("other") ), "Copy constructor.") ); + { //::SireOpenMM::TorchQMForce::getModulePath + + typedef ::QString ( ::SireOpenMM::TorchQMForce::*getModulePath_function_type)( ) const; + getModulePath_function_type getModulePath_function_value( &::SireOpenMM::TorchQMForce::getModulePath ); + + TorchQMForce_exposer.def( + "getModulePath" + , getModulePath_function_value + , bp::release_gil_policy() + , "Get the path to the serialised TorchScript module.\n" ); + + } + { //::SireOpenMM::TorchQMForce::getAtoms + + typedef ::QVector< int > ( ::SireOpenMM::TorchQMForce::*getAtoms_function_type)( ) const; + getAtoms_function_type getAtoms_function_value( &::SireOpenMM::TorchQMForce::getAtoms ); + + TorchQMForce_exposer.def( + "getAtoms" + , getAtoms_function_value + , bp::release_gil_policy() + , "Get the indices of the atoms in the QM region.\nReturn:s\nA vector of atom indices for the QM region.\n" ); + + } + { //::SireOpenMM::TorchQMForce::getCharges + + typedef ::QVector< double > ( ::SireOpenMM::TorchQMForce::*getCharges_function_type)( ) const; + getCharges_function_type getCharges_function_value( &::SireOpenMM::TorchQMForce::getCharges ); + + TorchQMForce_exposer.def( + "getCharges" + , getCharges_function_value + , bp::release_gil_policy() + , "Get the atomic charges of all atoms in the system.\nReturn:s\nA vector of atomic charges for all atoms in the system.\n" ); + + } + { //::SireOpenMM::TorchQMForce::getCutoff + + typedef ::SireUnits::Dimension::Length ( ::SireOpenMM::TorchQMForce::*getCutoff_function_type)( ) const; + getCutoff_function_type getCutoff_function_value( &::SireOpenMM::TorchQMForce::getCutoff ); + + TorchQMForce_exposer.def( + "getCutoff" + , getCutoff_function_value + , bp::release_gil_policy() + , "Get the QM cutoff distance.\nReturn:s\nThe QM cutoff distance.\n" ); + + } + { //::SireOpenMM::TorchQMForce::getIsMechanical + + typedef bool ( ::SireOpenMM::TorchQMForce::*getIsMechanical_function_type)( ) const; + getIsMechanical_function_type getIsMechanical_function_value( &::SireOpenMM::TorchQMForce::getIsMechanical ); + + TorchQMForce_exposer.def( + "getIsMechanical" + , getIsMechanical_function_value + , bp::release_gil_policy() + , "Get the mechanical embedding flag.\nReturn:s\nA flag to indicate if mechanical embedding is being used.\n" ); + + } + { //::SireOpenMM::TorchQMForce::getLambda + + typedef double ( ::SireOpenMM::TorchQMForce::*getLambda_function_type)( ) const; + getLambda_function_type getLambda_function_value( &::SireOpenMM::TorchQMForce::getLambda ); + + TorchQMForce_exposer.def( + "getLambda" + , getLambda_function_value + , bp::release_gil_policy() + , "Get the lambda weighting factor.\nReturn:s\nThe lambda weighting factor.\n" ); + + } + { //::SireOpenMM::TorchQMForce::getLinkAtoms + + typedef ::boost::tuples::tuple< QMap< int, int >, QMap< int, QVector< int > >, QMap< int, double >, boost::tuples::null_type, boost::tuples::null_type, boost::tuples::null_type, boost::tuples::null_type, boost::tuples::null_type, boost::tuples::null_type, boost::tuples::null_type > ( ::SireOpenMM::TorchQMForce::*getLinkAtoms_function_type)( ) const; + getLinkAtoms_function_type getLinkAtoms_function_value( &::SireOpenMM::TorchQMForce::getLinkAtoms ); + + TorchQMForce_exposer.def( + "getLinkAtoms" + , getLinkAtoms_function_value + , bp::release_gil_policy() + , "Get the link atoms associated with each QM atom.\nReturn:s\nA tuple containing:\n\nmm1_to_qm\nA dictionary mapping link atom (MM1) indices to the QM atoms to\nwhich they are bonded.\n\nmm1_to_mm2\nA dictionary of link atoms indices (MM1) to a list of the MM\natoms to which they are bonded (MM2).\n\nbond_scale_factors\nA dictionary of link atom indices (MM1) to a list of the bond\nlength scale factors between the QM and MM1 atoms. The scale\nfactors are the ratio of the equilibrium bond lengths for the\nQM-L (QM-link) atom and QM-MM1 atom, i.e. R0(QM-L) R0(QM-MM1),\ntaken from the MM force field parameters for the molecule.\n\n" ); + + } + { //::SireOpenMM::TorchQMForce::getMM2Atoms + + typedef ::QVector< int > ( ::SireOpenMM::TorchQMForce::*getMM2Atoms_function_type)( ) const; + getMM2Atoms_function_type getMM2Atoms_function_value( &::SireOpenMM::TorchQMForce::getMM2Atoms ); + + TorchQMForce_exposer.def( + "getMM2Atoms" + , getMM2Atoms_function_value + , bp::release_gil_policy() + , "Get the vector of MM2 atoms.\nReturn:s\nA vector of MM2 atom indices.\n" ); + + } + { //::SireOpenMM::TorchQMForce::getNeighbourListFrequency + + typedef int ( ::SireOpenMM::TorchQMForce::*getNeighbourListFrequency_function_type)( ) const; + getNeighbourListFrequency_function_type getNeighbourListFrequency_function_value( &::SireOpenMM::TorchQMForce::getNeighbourListFrequency ); + + TorchQMForce_exposer.def( + "getNeighbourListFrequency" + , getNeighbourListFrequency_function_value + , bp::release_gil_policy() + , "Get the neighbour list frequency.\nReturn:s\nThe neighbour list frequency.\n" ); + + } + { //::SireOpenMM::TorchQMForce::getNumbers + + typedef ::QVector< int > ( ::SireOpenMM::TorchQMForce::*getNumbers_function_type)( ) const; + getNumbers_function_type getNumbers_function_value( &::SireOpenMM::TorchQMForce::getNumbers ); + + TorchQMForce_exposer.def( + "getNumbers" + , getNumbers_function_value + , bp::release_gil_policy() + , "Get the atomic numbers for the atoms in the QM region.\nReturn:s\nA vector of atomic numbers for the atoms in the QM region.\n" ); + + } + { //::SireOpenMM::TorchQMForce::operator= + + typedef ::SireOpenMM::TorchQMForce & ( ::SireOpenMM::TorchQMForce::*assign_function_type)( ::SireOpenMM::TorchQMForce const & ) ; + assign_function_type assign_function_value( &::SireOpenMM::TorchQMForce::operator= ); + + TorchQMForce_exposer.def( + "assign" + , assign_function_value + , ( bp::arg("other") ) + , bp::return_self< >() + , "Assignment operator." ); + + } + { //::SireOpenMM::TorchQMForce::setLambda + + typedef void ( ::SireOpenMM::TorchQMForce::*setLambda_function_type)( double ) ; + setLambda_function_type setLambda_function_value( &::SireOpenMM::TorchQMForce::setLambda ); + + TorchQMForce_exposer.def( + "setLambda" + , setLambda_function_value + , ( bp::arg("lambda") ) + , bp::release_gil_policy() + , "Set the lambda weighting factor\nPar:am lambda\nThe lambda weighting factor.\n" ); + + } + { //::SireOpenMM::TorchQMForce::setModulePath + + typedef void ( ::SireOpenMM::TorchQMForce::*setModulePath_function_type)( ::QString ) ; + setModulePath_function_type setModulePath_function_value( &::SireOpenMM::TorchQMForce::setModulePath ); + + TorchQMForce_exposer.def( + "setModulePath" + , setModulePath_function_value + , ( bp::arg("module_path") ) + , bp::release_gil_policy() + , "Set the path to the serialised TorchScript module.\n" ); + + } + { //::SireOpenMM::TorchQMForce::typeName + + typedef char const * ( *typeName_function_type )( ); + typeName_function_type typeName_function_value( &::SireOpenMM::TorchQMForce::typeName ); + + TorchQMForce_exposer.def( + "typeName" + , typeName_function_value + , bp::release_gil_policy() + , "Return the C++ name for this class." ); + + } + { //::SireOpenMM::TorchQMForce::what + + typedef char const * ( ::SireOpenMM::TorchQMForce::*what_function_type)( ) const; + what_function_type what_function_value( &::SireOpenMM::TorchQMForce::what ); + + TorchQMForce_exposer.def( + "what" + , what_function_value + , bp::release_gil_policy() + , "Return the C++ name for this class." ); + + } + TorchQMForce_exposer.staticmethod( "typeName" ); + TorchQMForce_exposer.def( "__copy__", &__copy__); + TorchQMForce_exposer.def( "__deepcopy__", &__copy__); + TorchQMForce_exposer.def( "clone", &__copy__); + TorchQMForce_exposer.def( "__rlshift__", &__rlshift__QDataStream< ::SireOpenMM::TorchQMForce >, + bp::return_internal_reference<1, bp::with_custodian_and_ward<1,2> >() ); + TorchQMForce_exposer.def( "__rrshift__", &__rrshift__QDataStream< ::SireOpenMM::TorchQMForce >, + bp::return_internal_reference<1, bp::with_custodian_and_ward<1,2> >() ); + TorchQMForce_exposer.def_pickle(sire_pickle_suite< ::SireOpenMM::TorchQMForce >()); + TorchQMForce_exposer.def( "__str__", &pvt_get_name); + TorchQMForce_exposer.def( "__repr__", &pvt_get_name); + } + +} diff --git a/wrapper/Convert/SireOpenMM/TorchQMForce.pypp.hpp b/wrapper/Convert/SireOpenMM/TorchQMForce.pypp.hpp new file mode 100644 index 000000000..6bba6b23d --- /dev/null +++ b/wrapper/Convert/SireOpenMM/TorchQMForce.pypp.hpp @@ -0,0 +1,10 @@ +// This file has been generated by Py++. + +// (C) Christopher Woods, GPL >= 3 License + +#ifndef TorchQMForce_hpp__pyplusplus_wrapper +#define TorchQMForce_hpp__pyplusplus_wrapper + +void register_TorchQMForce_class(); + +#endif//TorchQMForce_hpp__pyplusplus_wrapper diff --git a/wrapper/Convert/SireOpenMM/_SireOpenMM.main.cpp b/wrapper/Convert/SireOpenMM/_SireOpenMM.main.cpp index 63aa4e9fd..b686def19 100644 --- a/wrapper/Convert/SireOpenMM/_SireOpenMM.main.cpp +++ b/wrapper/Convert/SireOpenMM/_SireOpenMM.main.cpp @@ -9,10 +9,26 @@ #include "LambdaLever.pypp.hpp" +#include "NullQMEngine.pypp.hpp" + #include "OpenMMMetaData.pypp.hpp" #include "PerturbableOpenMMMolecule.pypp.hpp" +#include "PyQMCallback.pypp.hpp" + +#include "PyQMEngine.pypp.hpp" + +#include "PyQMForce.pypp.hpp" + +#include "QMEngine.pypp.hpp" + +#include "QMForce.pypp.hpp" + +#include "TorchQMEngine.pypp.hpp" + +#include "TorchQMForce.pypp.hpp" + #include "_SireOpenMM_free_functions.pypp.hpp" #include "vector_less__OpenMM_scope_Vec3__greater_.pypp.hpp" @@ -21,6 +37,8 @@ namespace bp = boost::python; #include "SireOpenMM_registrars.h" +#include "SireOpenMM_properties.h" + #include "./register_extras.h" BOOST_PYTHON_MODULE(_SireOpenMM){ @@ -30,12 +48,30 @@ BOOST_PYTHON_MODULE(_SireOpenMM){ register_LambdaLever_class(); + register_QMEngine_class(); + + register_NullQMEngine_class(); + register_OpenMMMetaData_class(); register_PerturbableOpenMMMolecule_class(); + register_PyQMCallback_class(); + + register_PyQMEngine_class(); + + register_QMForce_class(); + + register_PyQMForce_class(); + + register_SireOpenMM_properties(); + SireOpenMM::register_extras(); register_free_functions(); + + register_TorchQMEngine_class(); + + register_TorchQMForce_class(); } diff --git a/wrapper/Convert/SireOpenMM/_SireOpenMM_free_functions.pypp.cpp b/wrapper/Convert/SireOpenMM/_SireOpenMM_free_functions.pypp.cpp index e75947576..ed771318d 100644 --- a/wrapper/Convert/SireOpenMM/_SireOpenMM_free_functions.pypp.cpp +++ b/wrapper/Convert/SireOpenMM/_SireOpenMM_free_functions.pypp.cpp @@ -45,6 +45,504 @@ namespace bp = boost::python; #include "SireMol/moleditor.h" +#include "SireMol/selectorm.hpp" + +#include "SireStream/datastream.h" + +#include "SireStream/shareddatastream.h" + +#include "SireSystem/forcefieldinfo.h" + +#include "SireUnits/units.h" + +#include "SireVol/periodicbox.h" + +#include "SireVol/triclinicbox.h" + +#include "openmmmolecule.h" + +#include "sire_openmm.h" + +#include "tostring.h" + +#include + +#include + +#include "SireBase/parallel.h" + +#include "SireBase/propertylist.h" + +#include "SireCAS/lambdaschedule.h" + +#include "SireError/errors.h" + +#include "SireMM/amberparams.h" + +#include "SireMM/atomljs.h" + +#include "SireMM/selectorbond.h" + +#include "SireMaths/vector.h" + +#include "SireMol/atomcharges.h" + +#include "SireMol/atomcoords.h" + +#include "SireMol/atomelements.h" + +#include "SireMol/atommasses.h" + +#include "SireMol/atomproperty.hpp" + +#include "SireMol/atomvelocities.h" + +#include "SireMol/bondid.h" + +#include "SireMol/bondorder.h" + +#include "SireMol/connectivity.h" + +#include "SireMol/core.h" + +#include "SireMol/moleditor.h" + +#include "SireMol/selectorm.hpp" + +#include "SireStream/datastream.h" + +#include "SireStream/shareddatastream.h" + +#include "SireSystem/forcefieldinfo.h" + +#include "SireUnits/units.h" + +#include "SireVol/periodicbox.h" + +#include "SireVol/triclinicbox.h" + +#include "openmmmolecule.h" + +#include "sire_openmm.h" + +#include "tostring.h" + +#include + +#include + +#include "SireBase/parallel.h" + +#include "SireBase/propertylist.h" + +#include "SireCAS/lambdaschedule.h" + +#include "SireError/errors.h" + +#include "SireMM/amberparams.h" + +#include "SireMM/atomljs.h" + +#include "SireMM/selectorbond.h" + +#include "SireMaths/vector.h" + +#include "SireMol/atomcharges.h" + +#include "SireMol/atomcoords.h" + +#include "SireMol/atomelements.h" + +#include "SireMol/atommasses.h" + +#include "SireMol/atomproperty.hpp" + +#include "SireMol/atomvelocities.h" + +#include "SireMol/bondid.h" + +#include "SireMol/bondorder.h" + +#include "SireMol/connectivity.h" + +#include "SireMol/core.h" + +#include "SireMol/moleditor.h" + +#include "SireMol/selectorm.hpp" + +#include "SireStream/datastream.h" + +#include "SireStream/shareddatastream.h" + +#include "SireSystem/forcefieldinfo.h" + +#include "SireUnits/units.h" + +#include "SireVol/periodicbox.h" + +#include "SireVol/triclinicbox.h" + +#include "openmmmolecule.h" + +#include "sire_openmm.h" + +#include "tostring.h" + +#include + +#include + +#include "SireBase/parallel.h" + +#include "SireBase/propertylist.h" + +#include "SireCAS/lambdaschedule.h" + +#include "SireError/errors.h" + +#include "SireMM/amberparams.h" + +#include "SireMM/atomljs.h" + +#include "SireMM/selectorbond.h" + +#include "SireMaths/vector.h" + +#include "SireMol/atomcharges.h" + +#include "SireMol/atomcoords.h" + +#include "SireMol/atomelements.h" + +#include "SireMol/atommasses.h" + +#include "SireMol/atomproperty.hpp" + +#include "SireMol/atomvelocities.h" + +#include "SireMol/bondid.h" + +#include "SireMol/bondorder.h" + +#include "SireMol/connectivity.h" + +#include "SireMol/core.h" + +#include "SireMol/moleditor.h" + +#include "SireMol/selectorm.hpp" + +#include "SireStream/datastream.h" + +#include "SireStream/shareddatastream.h" + +#include "SireSystem/forcefieldinfo.h" + +#include "SireUnits/units.h" + +#include "SireVol/periodicbox.h" + +#include "SireVol/triclinicbox.h" + +#include "openmmmolecule.h" + +#include "sire_openmm.h" + +#include "tostring.h" + +#include + +#include + +#include "SireBase/parallel.h" + +#include "SireBase/propertylist.h" + +#include "SireCAS/lambdaschedule.h" + +#include "SireError/errors.h" + +#include "SireMM/amberparams.h" + +#include "SireMM/atomljs.h" + +#include "SireMM/selectorbond.h" + +#include "SireMaths/vector.h" + +#include "SireMol/atomcharges.h" + +#include "SireMol/atomcoords.h" + +#include "SireMol/atomelements.h" + +#include "SireMol/atommasses.h" + +#include "SireMol/atomproperty.hpp" + +#include "SireMol/atomvelocities.h" + +#include "SireMol/bondid.h" + +#include "SireMol/bondorder.h" + +#include "SireMol/connectivity.h" + +#include "SireMol/core.h" + +#include "SireMol/moleditor.h" + +#include "SireMol/selectorm.hpp" + +#include "SireStream/datastream.h" + +#include "SireStream/shareddatastream.h" + +#include "SireSystem/forcefieldinfo.h" + +#include "SireUnits/units.h" + +#include "SireVol/periodicbox.h" + +#include "SireVol/triclinicbox.h" + +#include "openmmmolecule.h" + +#include "sire_openmm.h" + +#include "tostring.h" + +#include + +#include + +#include "SireBase/parallel.h" + +#include "SireBase/propertylist.h" + +#include "SireCAS/lambdaschedule.h" + +#include "SireError/errors.h" + +#include "SireMM/amberparams.h" + +#include "SireMM/atomljs.h" + +#include "SireMM/selectorbond.h" + +#include "SireMaths/vector.h" + +#include "SireMol/atomcharges.h" + +#include "SireMol/atomcoords.h" + +#include "SireMol/atomelements.h" + +#include "SireMol/atommasses.h" + +#include "SireMol/atomproperty.hpp" + +#include "SireMol/atomvelocities.h" + +#include "SireMol/bondid.h" + +#include "SireMol/bondorder.h" + +#include "SireMol/connectivity.h" + +#include "SireMol/core.h" + +#include "SireMol/moleditor.h" + +#include "SireMol/selectorm.hpp" + +#include "SireStream/datastream.h" + +#include "SireStream/shareddatastream.h" + +#include "SireSystem/forcefieldinfo.h" + +#include "SireUnits/units.h" + +#include "SireVol/periodicbox.h" + +#include "SireVol/triclinicbox.h" + +#include "openmmmolecule.h" + +#include "sire_openmm.h" + +#include "tostring.h" + +#include + +#include + +#include "SireBase/parallel.h" + +#include "SireBase/propertylist.h" + +#include "SireCAS/lambdaschedule.h" + +#include "SireError/errors.h" + +#include "SireMM/amberparams.h" + +#include "SireMM/atomljs.h" + +#include "SireMM/selectorbond.h" + +#include "SireMaths/vector.h" + +#include "SireMol/atomcharges.h" + +#include "SireMol/atomcoords.h" + +#include "SireMol/atomelements.h" + +#include "SireMol/atommasses.h" + +#include "SireMol/atomproperty.hpp" + +#include "SireMol/atomvelocities.h" + +#include "SireMol/bondid.h" + +#include "SireMol/bondorder.h" + +#include "SireMol/connectivity.h" + +#include "SireMol/core.h" + +#include "SireMol/moleditor.h" + +#include "SireMol/selectorm.hpp" + +#include "SireStream/datastream.h" + +#include "SireStream/shareddatastream.h" + +#include "SireSystem/forcefieldinfo.h" + +#include "SireUnits/units.h" + +#include "SireVol/periodicbox.h" + +#include "SireVol/triclinicbox.h" + +#include "openmmmolecule.h" + +#include "sire_openmm.h" + +#include "tostring.h" + +#include + +#include + +#include "SireBase/parallel.h" + +#include "SireBase/propertylist.h" + +#include "SireCAS/lambdaschedule.h" + +#include "SireError/errors.h" + +#include "SireMM/amberparams.h" + +#include "SireMM/atomljs.h" + +#include "SireMM/selectorbond.h" + +#include "SireMaths/vector.h" + +#include "SireMol/atomcharges.h" + +#include "SireMol/atomcoords.h" + +#include "SireMol/atomelements.h" + +#include "SireMol/atommasses.h" + +#include "SireMol/atomproperty.hpp" + +#include "SireMol/atomvelocities.h" + +#include "SireMol/bondid.h" + +#include "SireMol/bondorder.h" + +#include "SireMol/connectivity.h" + +#include "SireMol/core.h" + +#include "SireMol/moleditor.h" + +#include "SireMol/selectorm.hpp" + +#include "SireStream/datastream.h" + +#include "SireStream/shareddatastream.h" + +#include "SireSystem/forcefieldinfo.h" + +#include "SireUnits/units.h" + +#include "SireVol/periodicbox.h" + +#include "SireVol/triclinicbox.h" + +#include "openmmmolecule.h" + +#include "sire_openmm.h" + +#include "tostring.h" + +#include + +#include + +#include "SireBase/parallel.h" + +#include "SireBase/propertylist.h" + +#include "SireCAS/lambdaschedule.h" + +#include "SireError/errors.h" + +#include "SireMM/amberparams.h" + +#include "SireMM/atomljs.h" + +#include "SireMM/selectorbond.h" + +#include "SireMaths/vector.h" + +#include "SireMol/atomcharges.h" + +#include "SireMol/atomcoords.h" + +#include "SireMol/atomelements.h" + +#include "SireMol/atommasses.h" + +#include "SireMol/atomproperty.hpp" + +#include "SireMol/atomvelocities.h" + +#include "SireMol/bondid.h" + +#include "SireMol/bondorder.h" + +#include "SireMol/connectivity.h" + +#include "SireMol/core.h" + +#include "SireMol/moleditor.h" + +#include "SireMol/selectorm.hpp" + #include "SireStream/datastream.h" #include "SireStream/shareddatastream.h" @@ -105,6 +603,8 @@ namespace bp = boost::python; #include "SireMol/moleditor.h" +#include "SireMol/selectorm.hpp" + #include "SireStream/datastream.h" #include "SireStream/shareddatastream.h" @@ -165,6 +665,8 @@ namespace bp = boost::python; #include "SireMol/moleditor.h" +#include "SireMol/selectorm.hpp" + #include "SireStream/datastream.h" #include "SireStream/shareddatastream.h" @@ -225,6 +727,8 @@ namespace bp = boost::python; #include "SireMol/moleditor.h" +#include "SireMol/selectorm.hpp" + #include "SireStream/datastream.h" #include "SireStream/shareddatastream.h" @@ -285,6 +789,8 @@ namespace bp = boost::python; #include "SireMol/moleditor.h" +#include "SireMol/selectorm.hpp" + #include "SireStream/datastream.h" #include "SireStream/shareddatastream.h" @@ -345,6 +851,8 @@ namespace bp = boost::python; #include "SireMol/moleditor.h" +#include "SireMol/selectorm.hpp" + #include "SireStream/datastream.h" #include "SireStream/shareddatastream.h" @@ -405,6 +913,8 @@ namespace bp = boost::python; #include "SireMol/moleditor.h" +#include "SireMol/selectorm.hpp" + #include "SireStream/datastream.h" #include "SireStream/shareddatastream.h" @@ -465,6 +975,8 @@ namespace bp = boost::python; #include "SireMol/moleditor.h" +#include "SireMol/selectorm.hpp" + #include "SireStream/datastream.h" #include "SireStream/shareddatastream.h" @@ -525,6 +1037,8 @@ namespace bp = boost::python; #include "SireMol/moleditor.h" +#include "SireMol/selectorm.hpp" + #include "SireStream/datastream.h" #include "SireStream/shareddatastream.h" @@ -585,6 +1099,8 @@ namespace bp = boost::python; #include "SireMol/moleditor.h" +#include "SireMol/selectorm.hpp" + #include "SireStream/datastream.h" #include "SireStream/shareddatastream.h" @@ -645,6 +1161,8 @@ namespace bp = boost::python; #include "SireMol/moleditor.h" +#include "SireMol/selectorm.hpp" + #include "SireStream/datastream.h" #include "SireStream/shareddatastream.h" @@ -705,6 +1223,8 @@ namespace bp = boost::python; #include "SireMol/moleditor.h" +#include "SireMol/selectorm.hpp" + #include "SireStream/datastream.h" #include "SireStream/shareddatastream.h" @@ -765,6 +1285,8 @@ namespace bp = boost::python; #include "SireMol/moleditor.h" +#include "SireMol/selectorm.hpp" + #include "SireStream/datastream.h" #include "SireStream/shareddatastream.h" @@ -825,6 +1347,8 @@ namespace bp = boost::python; #include "SireMol/moleditor.h" +#include "SireMol/selectorm.hpp" + #include "SireStream/datastream.h" #include "SireStream/shareddatastream.h" @@ -885,6 +1409,8 @@ namespace bp = boost::python; #include "SireMol/moleditor.h" +#include "SireMol/selectorm.hpp" + #include "SireStream/datastream.h" #include "SireStream/shareddatastream.h" @@ -945,6 +1471,8 @@ namespace bp = boost::python; #include "SireMol/moleditor.h" +#include "SireMol/selectorm.hpp" + #include "SireStream/datastream.h" #include "SireStream/shareddatastream.h" @@ -1165,6 +1693,8 @@ namespace bp = boost::python; #include "SireMol/moleditor.h" +#include "SireMol/selectorm.hpp" + #include "SireStream/datastream.h" #include "SireStream/shareddatastream.h" @@ -1225,6 +1755,8 @@ namespace bp = boost::python; #include "SireMol/moleditor.h" +#include "SireMol/selectorm.hpp" + #include "SireStream/datastream.h" #include "SireStream/shareddatastream.h" @@ -1285,6 +1817,8 @@ namespace bp = boost::python; #include "SireMol/moleditor.h" +#include "SireMol/selectorm.hpp" + #include "SireStream/datastream.h" #include "SireStream/shareddatastream.h" @@ -1345,6 +1879,8 @@ namespace bp = boost::python; #include "SireMol/moleditor.h" +#include "SireMol/selectorm.hpp" + #include "SireStream/datastream.h" #include "SireStream/shareddatastream.h" @@ -1405,6 +1941,8 @@ namespace bp = boost::python; #include "SireMol/moleditor.h" +#include "SireMol/selectorm.hpp" + #include "SireStream/datastream.h" #include "SireStream/shareddatastream.h" @@ -1465,6 +2003,8 @@ namespace bp = boost::python; #include "SireMol/moleditor.h" +#include "SireMol/selectorm.hpp" + #include "SireStream/datastream.h" #include "SireStream/shareddatastream.h" @@ -1525,6 +2065,8 @@ namespace bp = boost::python; #include "SireMol/moleditor.h" +#include "SireMol/selectorm.hpp" + #include "SireStream/datastream.h" #include "SireStream/shareddatastream.h" @@ -1585,6 +2127,8 @@ namespace bp = boost::python; #include "SireMol/moleditor.h" +#include "SireMol/selectorm.hpp" + #include "SireStream/datastream.h" #include "SireStream/shareddatastream.h" @@ -1645,6 +2189,8 @@ namespace bp = boost::python; #include "SireMol/moleditor.h" +#include "SireMol/selectorm.hpp" + #include "SireStream/datastream.h" #include "SireStream/shareddatastream.h" @@ -1705,6 +2251,8 @@ namespace bp = boost::python; #include "SireMol/moleditor.h" +#include "SireMol/selectorm.hpp" + #include "SireStream/datastream.h" #include "SireStream/shareddatastream.h" @@ -1765,6 +2313,8 @@ namespace bp = boost::python; #include "SireMol/moleditor.h" +#include "SireMol/selectorm.hpp" + #include "SireStream/datastream.h" #include "SireStream/shareddatastream.h" @@ -1825,6 +2375,8 @@ namespace bp = boost::python; #include "SireMol/moleditor.h" +#include "SireMol/selectorm.hpp" + #include "SireStream/datastream.h" #include "SireStream/shareddatastream.h" @@ -1885,6 +2437,8 @@ namespace bp = boost::python; #include "SireMol/moleditor.h" +#include "SireMol/selectorm.hpp" + #include "SireStream/datastream.h" #include "SireStream/shareddatastream.h" @@ -1945,6 +2499,8 @@ namespace bp = boost::python; #include "SireMol/moleditor.h" +#include "SireMol/selectorm.hpp" + #include "SireStream/datastream.h" #include "SireStream/shareddatastream.h" @@ -2005,6 +2561,8 @@ namespace bp = boost::python; #include "SireMol/moleditor.h" +#include "SireMol/selectorm.hpp" + #include "SireStream/datastream.h" #include "SireStream/shareddatastream.h" @@ -2065,6 +2623,8 @@ namespace bp = boost::python; #include "SireMol/moleditor.h" +#include "SireMol/selectorm.hpp" + #include "SireStream/datastream.h" #include "SireStream/shareddatastream.h" @@ -2089,6 +2649,19 @@ namespace bp = boost::python; void register_free_functions(){ + { //::SireOpenMM::extract_coordinates + + typedef ::SireMol::SelectorM< SireMol::Atom > ( *extract_coordinates_function_type )( ::OpenMM::State const &,::SireMol::SelectorM< SireMol::Atom > const &,::QHash< SireMol::MolNum, SireBase::PropertyMap > const &,::SireBase::PropertyMap const & ); + extract_coordinates_function_type extract_coordinates_function_value( &::SireOpenMM::extract_coordinates ); + + bp::def( + "extract_coordinates" + , extract_coordinates_function_value + , ( bp::arg("state"), bp::arg("mols"), bp::arg("perturbable_maps"), bp::arg("map") ) + , "" ); + + } + { //::SireOpenMM::extract_coordinates typedef ::SireMol::SelectorMol ( *extract_coordinates_function_type )( ::OpenMM::State const &,::SireMol::SelectorMol const &,::QHash< SireMol::MolNum, SireBase::PropertyMap > const &,::SireBase::PropertyMap const & ); @@ -2102,6 +2675,19 @@ void register_free_functions(){ } + { //::SireOpenMM::extract_coordinates_and_velocities + + typedef ::SireMol::SelectorM< SireMol::Atom > ( *extract_coordinates_and_velocities_function_type )( ::OpenMM::State const &,::SireMol::SelectorM< SireMol::Atom > const &,::QHash< SireMol::MolNum, SireBase::PropertyMap > const &,::SireBase::PropertyMap const & ); + extract_coordinates_and_velocities_function_type extract_coordinates_and_velocities_function_value( &::SireOpenMM::extract_coordinates_and_velocities ); + + bp::def( + "extract_coordinates_and_velocities" + , extract_coordinates_and_velocities_function_value + , ( bp::arg("state"), bp::arg("mols"), bp::arg("perturbable_maps"), bp::arg("map") ) + , "" ); + + } + { //::SireOpenMM::extract_coordinates_and_velocities typedef ::SireMol::SelectorMol ( *extract_coordinates_and_velocities_function_type )( ::OpenMM::State const &,::SireMol::SelectorMol const &,::QHash< SireMol::MolNum, SireBase::PropertyMap > const &,::SireBase::PropertyMap const & ); @@ -2143,13 +2729,13 @@ void register_free_functions(){ { //::SireOpenMM::minimise_openmm_context - typedef ::QString ( *minimise_openmm_context_function_type )( ::OpenMM::Context &,double,int,int,int,int,double,double,double ); + typedef ::QString ( *minimise_openmm_context_function_type )( ::OpenMM::Context &,double,int,int,int,int,double,double,double,double ); minimise_openmm_context_function_type minimise_openmm_context_function_value( &::SireOpenMM::minimise_openmm_context ); bp::def( "minimise_openmm_context" , minimise_openmm_context_function_value - , ( bp::arg("context"), bp::arg("tolerance")=10., bp::arg("max_iterations")=(int)(-1), bp::arg("max_restarts")=(int)(10), bp::arg("max_ratchets")=(int)(20), bp::arg("ratchet_frequency")=(int)(500), bp::arg("starting_k")=100., bp::arg("ratchet_scale")=2., bp::arg("max_constraint_error")=0.01 ) + , ( bp::arg("context"), bp::arg("tolerance")=10., bp::arg("max_iterations")=(int)(-1), bp::arg("max_restarts")=(int)(10), bp::arg("max_ratchets")=(int)(20), bp::arg("ratchet_frequency")=(int)(500), bp::arg("starting_k")=100., bp::arg("ratchet_scale")=2., bp::arg("max_constraint_error")=0.01, bp::arg("timeout")=300. ) , "This is a minimiser heavily inspired by the\nLocalEnergyMinimizer included in OpenMM. This is re-written\nfor sire to;\n\n1. Better integrate minimisation into the sire progress\nmonitoring interupting framework.\n2. Avoid errors caused by OpenMM switching from the desired\ncontext to the CPU context, thus triggering spurious exceptions\nrelated to exclusions exceptions not matching\n\nThis exposes more controls from the underlying minimisation\nlibrary, and also logs events and progress, which is returned\nas a string.\n\nThis raises an exception if minimisation fails.\n" ); } diff --git a/wrapper/Convert/SireOpenMM/_sommcontext.py b/wrapper/Convert/SireOpenMM/_sommcontext.py index 3757b0078..cceab24ab 100644 --- a/wrapper/Convert/SireOpenMM/_sommcontext.py +++ b/wrapper/Convert/SireOpenMM/_sommcontext.py @@ -45,6 +45,9 @@ def __init__( lambda_value = map["lambda"].value().as_double() elif map.specified("lambda_value"): lambda_value = map["lambda_value"].value().as_double() + elif map.specified("qm_engine"): + # Default to full QM. + lambda_value = 1.0 else: lambda_value = 0.0 diff --git a/wrapper/Convert/SireOpenMM/active_headers.h b/wrapper/Convert/SireOpenMM/active_headers.h index 6803f33f1..8338cbad1 100644 --- a/wrapper/Convert/SireOpenMM/active_headers.h +++ b/wrapper/Convert/SireOpenMM/active_headers.h @@ -6,7 +6,10 @@ #include "lambdalever.h" #include "openmmminimise.h" #include "openmmmolecule.h" +#include "pyqm.h" +#include "qmmm.h" #include "sire_openmm.h" +#include "torchqm.h" #endif diff --git a/wrapper/Convert/SireOpenMM/lambdalever.cpp b/wrapper/Convert/SireOpenMM/lambdalever.cpp index a70a0c18f..67707a398 100644 --- a/wrapper/Convert/SireOpenMM/lambdalever.cpp +++ b/wrapper/Convert/SireOpenMM/lambdalever.cpp @@ -26,6 +26,7 @@ * \*********************************************/ +#include "pyqm.h" #include "lambdalever.h" #include "SireBase/propertymap.h" @@ -213,7 +214,7 @@ LambdaLever::LambdaLever(const LambdaLever &other) name_to_restraintidx(other.name_to_restraintidx), lambda_schedule(other.lambda_schedule), perturbable_mols(other.perturbable_mols), - start_indicies(other.start_indicies), + start_indices(other.start_indices), perturbable_maps(other.perturbable_maps), lambda_cache(other.lambda_cache) { @@ -231,7 +232,7 @@ LambdaLever &LambdaLever::operator=(const LambdaLever &other) name_to_restraintidx = other.name_to_restraintidx; lambda_schedule = other.lambda_schedule; perturbable_mols = other.perturbable_mols; - start_indicies = other.start_indicies; + start_indices = other.start_indices; perturbable_maps = other.perturbable_maps; lambda_cache = other.lambda_cache; Property::operator=(other); @@ -246,7 +247,7 @@ bool LambdaLever::operator==(const LambdaLever &other) const name_to_restraintidx == other.name_to_restraintidx and lambda_schedule == other.lambda_schedule and perturbable_mols == other.perturbable_mols and - start_indicies == other.start_indicies and + start_indices == other.start_indices and perturbable_maps == other.perturbable_maps; } @@ -1139,6 +1140,7 @@ double LambdaLever::setLambda(OpenMM::Context &context, OpenMM::System &system = const_cast(context.getSystem()); // get copies of the forcefields in which the parameters will be changed + auto qmff = this->getForce("qmff", system); auto cljff = this->getForce("clj", system); auto ghost_ghostff = this->getForce("ghost/ghost", system); auto ghost_nonghostff = this->getForce("ghost/non-ghost", system); @@ -1150,9 +1152,18 @@ double LambdaLever::setLambda(OpenMM::Context &context, // we know if we have peturbable ghost atoms if we have the ghost forcefields const bool have_ghost_atoms = (ghost_ghostff != 0 or ghost_nonghostff != 0); + // whether the constraints have changed + bool have_constraints_changed = false; + std::vector custom_params = {0.0, 0.0, 0.0, 0.0, 0.0}; - // record the range of indicies of the atoms, bonds, angles, + if (qmff != 0) + { + double lam = this->lambda_schedule.morph("qmff", "*", 0.0, 1.0, lambda_value); + qmff->setLambda(lam); + } + + // record the range of indices of the atoms, bonds, angles, // torsions which change int start_change_atom = -1; int end_change_atom = -1; @@ -1169,7 +1180,7 @@ double LambdaLever::setLambda(OpenMM::Context &context, for (int i = 0; i < this->perturbable_mols.count(); ++i) { const auto &perturbable_mol = this->perturbable_mols[i]; - const auto &start_idxs = this->start_indicies[i]; + const auto &start_idxs = this->start_indices[i]; const auto &cache = this->lambda_cache.get(i, lambda_value); const auto &schedule = this->lambda_schedule.getMoleculeSchedule(i); @@ -1438,12 +1449,23 @@ double LambdaLever::setLambda(OpenMM::Context &context, // don't set LJ terms for ghost atoms if (atom0_is_ghost or atom1_is_ghost) { + // are the atoms perturbing from ghosts? + const auto from_ghost0 = perturbable_mol.getFromGhostIdxs().contains(atom0); + const auto from_ghost1 = perturbable_mol.getFromGhostIdxs().contains(atom1); + // are the atoms perturbing to ghosts? + const auto to_ghost0 = perturbable_mol.getToGhostIdxs().contains(atom0); + const auto to_ghost1 = perturbable_mol.getToGhostIdxs().contains(atom1); + + // is this interaction between an to/from ghost atom? + const auto to_from_ghost = (from_ghost0 and to_ghost1) or (from_ghost1 and to_ghost0); + cljff->setExceptionParameters( boost::get<0>(idxs[j]), boost::get<0>(p), boost::get<1>(p), boost::get<2>(p), 1e-9, 1e-9); - if (ghost_14ff != 0) + // exclude 14s for to/from ghost interactions + if (not to_from_ghost and ghost_14ff != 0) { // this is a 1-4 parameter - need to update // the ghost 1-4 forcefield @@ -1451,8 +1473,8 @@ double LambdaLever::setLambda(OpenMM::Context &context, if (nbidx < 0) throw SireError::program_bug(QObject::tr( - "Unset NB14 index for a ghost atom?"), - CODELOC); + "Unset NB14 index for a ghost atom?"), + CODELOC); coul_14_scale = morphed_ghost14_charge_scale[j]; lj_14_scale = morphed_ghost14_lj_scale[j]; @@ -1533,6 +1555,7 @@ double LambdaLever::setLambda(OpenMM::Context &context, if (orig_distance != constraint_length) { system.setConstraintParameters(idx, particle1, particle2, constraint_length); + have_constraints_changed = true; } } } @@ -1579,11 +1602,11 @@ double LambdaLever::setLambda(OpenMM::Context &context, double length, k; bondff->getBondParameters(index, particle1, particle2, - length, k); + length, k); bondff->setBondParameters(index, particle1, particle2, - morphed_bond_length[j], - morphed_bond_k[j]); + morphed_bond_length[j], + morphed_bond_k[j]); } } @@ -1628,13 +1651,13 @@ double LambdaLever::setLambda(OpenMM::Context &context, double size, k; angff->getAngleParameters(index, - particle1, particle2, particle3, - size, k); + particle1, particle2, particle3, + size, k); angff->setAngleParameters(index, - particle1, particle2, particle3, - morphed_angle_size[j], - morphed_angle_k[j]); + particle1, particle2, particle3, + morphed_angle_size[j], + morphed_angle_k[j]); } } @@ -1773,6 +1796,15 @@ double LambdaLever::setLambda(OpenMM::Context &context, } } + // we need to reinitialize the context if the constraints have changed + // since updating the parameters in the system will not update the context + // itself + if (have_constraints_changed) + { + // reinitialize the context, preserving the state + context.reinitialize(true); + } + return lambda_value; } @@ -1880,13 +1912,13 @@ void LambdaLever::updateRestraintInContext(OpenMM::Force &ff, double rho, // what is the type of this force...? const auto ff_type = ff.getName(); - if (ff_type == "CustomBondForce") + if (ff_type == "BondRestraintForce" or ff_type == "PositionalRestraintForce") { _update_restraint_in_context( dynamic_cast(&ff), rho, context); } - else if (ff_type == "CustomCompoundBondForce") + else if (ff_type == "BoreschRestraintForce") { _update_restraint_in_context( dynamic_cast(&ff), @@ -1897,7 +1929,7 @@ void LambdaLever::updateRestraintInContext(OpenMM::Force &ff, double rho, throw SireError::unknown_type(QObject::tr( "Unable to update the restraints for the passed force as it has " "an unknown type (%1). We currently only support a limited number " - "of force types, e.g. CustomBondForce etc") + "of force types, e.g. BondRestraintForce etc") .arg(QString::fromStdString(ff_type)), CODELOC); } @@ -1969,12 +2001,13 @@ int LambdaLever::addPerturbableMolecule(const OpenMMMolecule &molecule, { // should add in some sanity checks for these inputs this->perturbable_mols.append(PerturbableOpenMMMolecule(molecule, map)); - this->start_indicies.append(starts); + this->start_indices.append(starts); this->perturbable_maps.insert(molecule.number, molecule.perturtable_map); this->lambda_cache.clear(); return this->perturbable_mols.count() - 1; } + /** Set the exception indices for the perturbable molecule at * index 'mol_idx' */ @@ -1983,7 +2016,6 @@ void LambdaLever::setExceptionIndicies(int mol_idx, const QVector> &exception_idxs) { mol_idx = SireID::Index(mol_idx).map(this->perturbable_mols.count()); - this->perturbable_mols[mol_idx].setExceptionIndicies(name, exception_idxs); } diff --git a/wrapper/Convert/SireOpenMM/lambdalever.h b/wrapper/Convert/SireOpenMM/lambdalever.h index 46cb254d1..513025800 100644 --- a/wrapper/Convert/SireOpenMM/lambdalever.h +++ b/wrapper/Convert/SireOpenMM/lambdalever.h @@ -30,6 +30,7 @@ #define SIREOPENMM_LAMBDALEVER_H #include "openmmmolecule.h" +#include "qmmm.h" #include "SireCAS/lambdaschedule.h" @@ -168,9 +169,9 @@ namespace SireOpenMM /** The list of perturbable molecules */ QVector perturbable_mols; - /** The start indicies of the parameters in each named - forcefield for each perturbable moleucle */ - QVector> start_indicies; + /** The start indices of the parameters in each named + forcefield for each perturbable molecule */ + QVector> start_indices; /** All of the property maps for the perturbable molecules */ QHash perturbable_maps; @@ -199,13 +200,11 @@ namespace SireOpenMM return "OpenMM::CustomBondForce"; } - // LESTER - UNCOMMENT BELOW FOR FEATURE_EMLE - - /*template <> - inline QString _get_typename() + template <> + inline QString _get_typename() { - return "SireOpenMM::QMMMForce"; - }*/ + return "SireOpenMM::QMForce"; + } /** Return the OpenMM::Force (of type T) that is called 'name' * from the passed OpenMM::System. This returns 0 if the force diff --git a/wrapper/Convert/SireOpenMM/openmmminimise.cpp b/wrapper/Convert/SireOpenMM/openmmminimise.cpp index 949b27eb3..dd5a5f536 100644 --- a/wrapper/Convert/SireOpenMM/openmmminimise.cpp +++ b/wrapper/Convert/SireOpenMM/openmmminimise.cpp @@ -42,6 +42,7 @@ // COPIED FROM SO POST - https://stackoverflow.com/questions/570669/checking-if-a-double-or-float-is-nan-in-c +#include #include // std::isnan, std::fpclassify #include #include // std::setw @@ -616,13 +617,18 @@ namespace SireOpenMM int max_restarts, int max_ratchets, int ratchet_frequency, double starting_k, double ratchet_scale, - double max_constraint_error) + double max_constraint_error, double timeout) { if (max_iterations < 0) { max_iterations = std::numeric_limits::max(); } + if (timeout <= 0) + { + timeout = std::numeric_limits::max(); + } + auto gil = SireBase::release_gil(); const OpenMM::System &system = context.getSystem(); @@ -650,6 +656,7 @@ namespace SireOpenMM data.addLog(QString("Minimising with a tolerance of %1").arg(tolerance)); data.addLog(QString("Minimising with constraint tolerance %1").arg(working_constraint_tol)); + data.addLog(QString("Minimising with a timeout of %1 seconds").arg(timeout)); data.addLog(QString("Minimising with k = %1").arg(k)); data.addLog(QString("Minimising with %1 particles").arg(num_particles)); data.addLog(QString("Minimising with a maximum of %1 iterations").arg(max_iterations)); @@ -679,6 +686,9 @@ namespace SireOpenMM int max_linesearch = 100; const int max_linesearch_delta = 100; + // Store the starting time. + auto start_time = std::chrono::high_resolution_clock::now(); + while (data.getIteration() < data.getMaxIterations()) { if (not is_success) @@ -686,6 +696,16 @@ namespace SireOpenMM // try one more time with the real starting positions if (not have_hard_reset) { + // Check the current time and see if we've exceeded the timeout. + auto current_time = std::chrono::high_resolution_clock::now(); + auto elapsed_time = std::chrono::duration_cast(current_time - start_time).count(); + + if (elapsed_time > timeout) + { + data.addLog("Minimisation timed out!"); + break; + } + data.hardReset(); context.setPositions(starting_pos); @@ -709,6 +729,7 @@ namespace SireOpenMM } } + data.addLog(QString("Minimisation loop - %1 steps from %2").arg(data.getIteration()).arg(data.getMaxIterations())); try @@ -762,6 +783,17 @@ namespace SireOpenMM // Repeatedly minimize, steadily increasing the strength of the springs until all constraints are satisfied. while (data.getIteration() < data.getMaxIterations()) { + // Check the current time and see if we've exceeded the timeout. + auto current_time = std::chrono::high_resolution_clock::now(); + auto elapsed_time = std::chrono::duration_cast(current_time - start_time).count(); + + if (elapsed_time > timeout) + { + data.addLog("Minimisation timed out!"); + is_success = false; + break; + } + param.max_iterations = data.getMaxIterations() - data.getIteration(); lbfgsfloatval_t fx; // final energy auto last_it = data.getIteration(); @@ -993,9 +1025,13 @@ namespace SireOpenMM // to the full precision requested by the user. context.applyConstraints(working_constraint_tol); + // Recalculate the energy after the constraints have been applied. + energy_before = energy_after; + energy_after = context.getState(OpenMM::State::Energy).getPotentialEnergy(); + const auto delta_energy = energy_after - energy_before; - data.addLog(QString("Change in energy: %1 kJ mol-1").arg(delta_energy)); + data.addLog(QString("Change in energy following constraint projection: %1 kJ mol-1").arg(delta_energy)); if (std::abs(delta_energy) < 1000.0) { @@ -1063,7 +1099,6 @@ namespace SireOpenMM { data.addLog("Minimisation failed!"); bar.failure("Minimisation failed! Could not satisfy constraints!"); - qDebug() << data.getLog().join("\n"); throw SireError::invalid_state(QObject::tr( "Despite repeated attempts, the minimiser could not minimise the system " "while simultaneously satisfying the constraints."), diff --git a/wrapper/Convert/SireOpenMM/openmmminimise.h b/wrapper/Convert/SireOpenMM/openmmminimise.h index 3e8a04a84..b9a09e073 100644 --- a/wrapper/Convert/SireOpenMM/openmmminimise.h +++ b/wrapper/Convert/SireOpenMM/openmmminimise.h @@ -33,7 +33,8 @@ namespace SireOpenMM int ratchet_frequency = 500, double starting_k = 100.0, double ratchet_scale = 2.0, - double max_constraint_error = 0.01); + double max_constraint_error = 0.01, + double timeout = 300.0); } diff --git a/wrapper/Convert/SireOpenMM/pyqm.cpp b/wrapper/Convert/SireOpenMM/pyqm.cpp new file mode 100644 index 000000000..8c37cc2ad --- /dev/null +++ b/wrapper/Convert/SireOpenMM/pyqm.cpp @@ -0,0 +1,1097 @@ +/********************************************\ + * + * Sire - Molecular Simulation Framework + * + * Copyright (C) 2023 Christopher Woods + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + * For full details of the license please see the COPYING file + * that should have come with this distribution. + * + * You can contact the authors via the website + * at https://sire.openbiosim.org + * +\*********************************************/ + +#include + +#include +#include + +#include "openmm/serialization/SerializationNode.h" +#include "openmm/serialization/SerializationProxy.h" + +#include "SireError/errors.h" +#include "SireMaths/vector.h" +#include "SireStream/datastream.h" +#include "SireStream/shareddatastream.h" +#include "SireVol/triclinicbox.h" + +#include "pyqm.h" + +using namespace SireMaths; +using namespace SireOpenMM; +using namespace SireStream; +using namespace SireVol; + +// The delta used to place virtual point charges either side of the MM2 +// atoms, in nanometers. +static const double VIRTUAL_PC_DELTA = 0.01; + +class GILLock +{ +public: + GILLock() { state_ = PyGILState_Ensure(); } + ~GILLock() { PyGILState_Release(state_); } +private: + PyGILState_STATE state_; +}; + +///////// +///////// Implementation of PyQMCallback +///////// + +// A registry to store Python callback objects. +QHash py_object_registry; + +// A mutex to protect the registry. +std::mutex py_object_mutex; + +// Set a callback Python object in the registry using a mutex. +void setPyObject(bp::object cb, QString uuid) +{ + std::lock_guard lock(py_object_mutex); + py_object_registry[uuid] = cb; +}; + +// Get a callback object from the registry using a mutex. +bp::object getPythonObject(QString uuid) +{ + std::lock_guard lock(py_object_mutex); + + if (not py_object_registry.contains(uuid)) + { + throw SireError::invalid_key(QObject::tr( + "Unable to find UUID %1 in the PyQMForce callback registry.").arg(uuid), + CODELOC); + } + + return py_object_registry[uuid]; +} + +static const RegisterMetaType r_pyqmcallback(NO_ROOT); + +QDataStream &operator<<(QDataStream &ds, const PyQMCallback &pyqmcallback) +{ + writeHeader(ds, r_pyqmcallback, 1); + + SharedDataStream sds(ds); + + // Generate a unique identifier for the callback. + auto uuid = QUuid::createUuid().toString(); + + sds << uuid << pyqmcallback.name << pyqmcallback.is_method; + + // Set the Python object in the registry. + setPyObject(pyqmcallback.py_object, uuid); + + return ds; +} + +QDataStream &operator>>(QDataStream &ds, PyQMCallback &pyqmcallback) +{ + VersionID v = readHeader(ds, r_pyqmcallback); + + if (v == 1) + { + SharedDataStream sds(ds); + + QString uuid; + + // Get the UUID of the Python object and the callback name. + sds >> uuid >> pyqmcallback.name >> pyqmcallback.is_method; + + // Set the Python object. + pyqmcallback.py_object = getPythonObject(uuid); + } + else + throw version_error(v, "1", r_pyqmcallback, CODELOC); + + return ds; +} + +PyQMCallback::PyQMCallback() +{ +} + +PyQMCallback::PyQMCallback(bp::object py_object, QString name) : + py_object(py_object), name(name) +{ + // Is this a method or free function. + if (name.isEmpty()) + { + this->is_method = false; + } +} + +boost::tuple>, QVector>> +PyQMCallback::call( + QVector numbers_qm, + QVector charges_mm, + QVector> xyz_qm, + QVector> xyz_mm, + QVector idx_mm) const +{ + + // Acquire GIL before calling Python code. + GILLock lock; + + if (this->is_method) + { + try + { + return bp::call_method>, QVector>>>( + this->py_object.ptr(), + this->name.toStdString().c_str(), + numbers_qm, + charges_mm, + xyz_qm, + xyz_mm, + idx_mm + ); + } + catch (const bp::error_already_set &) + { + PyErr_Print(); + throw SireError::process_error(QObject::tr( + "An error occurred when calling the QM Python callback method"), + CODELOC); + } + } + else + { + try + { + return bp::call>, QVector>>>( + this->py_object.ptr(), + numbers_qm, + charges_mm, + xyz_qm, + xyz_mm, + idx_mm + ); + } + catch (const bp::error_already_set &) + { + PyErr_Print(); + throw SireError::process_error(QObject::tr( + "An error occurred when calling the QM Python callback method"), + CODELOC); + } + } +} + +const char *PyQMCallback::typeName() +{ + return QMetaType::typeName(qMetaTypeId()); +} + +const char *PyQMCallback::what() const +{ + return PyQMCallback::typeName(); +} + +///////// +///////// Implementation of PyQMForce +///////// + +static const RegisterMetaType r_pyqmforce(NO_ROOT); + +QDataStream &operator<<(QDataStream &ds, const PyQMForce &pyqmforce) +{ + writeHeader(ds, r_pyqmforce, 1); + + SharedDataStream sds(ds); + + sds << pyqmforce.callback << pyqmforce.cutoff << pyqmforce.neighbour_list_frequency + << pyqmforce.is_mechanical << pyqmforce.lambda << pyqmforce.atoms + << pyqmforce.mm1_to_qm << pyqmforce.mm1_to_mm2 << pyqmforce.bond_scale_factors + << pyqmforce.mm2_atoms << pyqmforce.numbers << pyqmforce.charges; + + return ds; +} + +QDataStream &operator>>(QDataStream &ds, PyQMForce &pyqmforce) +{ + VersionID v = readHeader(ds, r_pyqmforce); + + if (v == 1) + { + SharedDataStream sds(ds); + + sds >> pyqmforce.callback >> pyqmforce.cutoff >> pyqmforce.neighbour_list_frequency + >> pyqmforce.is_mechanical >> pyqmforce.lambda >> pyqmforce.atoms + >> pyqmforce.mm1_to_qm >> pyqmforce.mm1_to_mm2 >> pyqmforce.bond_scale_factors + >> pyqmforce.mm2_atoms >> pyqmforce.numbers >> pyqmforce.charges; + } + else + throw version_error(v, "1", r_pyqmforce, CODELOC); + + return ds; +} + +PyQMForce::PyQMForce() +{ +} + +PyQMForce::PyQMForce( + PyQMCallback callback, + SireUnits::Dimension::Length cutoff, + int neighbour_list_frequency, + bool is_mechanical, + double lambda, + QVector atoms, + QMap mm1_to_qm, + QMap> mm1_to_mm2, + QMap bond_scale_factors, + QVector mm2_atoms, + QVector numbers, + QVector charges) : + callback(callback), + cutoff(cutoff), + neighbour_list_frequency(neighbour_list_frequency), + is_mechanical(is_mechanical), + lambda(lambda), + atoms(atoms), + mm1_to_qm(mm1_to_qm), + mm1_to_mm2(mm1_to_mm2), + bond_scale_factors(bond_scale_factors), + mm2_atoms(mm2_atoms), + numbers(numbers), + charges(charges) +{ +} + +PyQMForce::PyQMForce(const PyQMForce &other) : + callback(other.callback), + cutoff(other.cutoff), + neighbour_list_frequency(other.neighbour_list_frequency), + is_mechanical(other.is_mechanical), + lambda(other.lambda), + atoms(other.atoms), + mm1_to_qm(other.mm1_to_qm), + mm1_to_mm2(other.mm1_to_mm2), + mm2_atoms(other.mm2_atoms), + bond_scale_factors(other.bond_scale_factors), + numbers(other.numbers), + charges(other.charges) +{ +} + +PyQMForce &PyQMForce::operator=(const PyQMForce &other) +{ + this->callback = other.callback; + this->cutoff = other.cutoff; + this->neighbour_list_frequency = other.neighbour_list_frequency; + this->is_mechanical = other.is_mechanical; + this->lambda = other.lambda; + this->atoms = other.atoms; + this->mm1_to_qm = other.mm1_to_qm; + this->mm1_to_mm2 = other.mm1_to_mm2; + this->mm2_atoms = other.mm2_atoms; + this->bond_scale_factors = other.bond_scale_factors; + this->numbers = other.numbers; + this->charges = other.charges; + return *this; +} + +void PyQMForce::setCallback(PyQMCallback callback) +{ + this->callback = callback; +} + +PyQMCallback PyQMForce::getCallback() const +{ + return this->callback; +} + +void PyQMForce::setLambda(double lambda) +{ + // Clamp the lambda value. + if (lambda < 0.0) + { + lambda = 0.0; + } + else if (lambda > 1.0) + { + lambda = 1.0; + } + this->lambda = lambda; +} + +double PyQMForce::getLambda() const +{ + return this->lambda; +} + +SireUnits::Dimension::Length PyQMForce::getCutoff() const +{ + return this->cutoff; +} + +int PyQMForce::getNeighbourListFrequency() const +{ + return this->neighbour_list_frequency; +} + +bool PyQMForce::getIsMechanical() const +{ + return this->is_mechanical;; +} + +QVector PyQMForce::getAtoms() const +{ + return this->atoms; +} + +boost::tuple, QMap>, QMap> PyQMForce::getLinkAtoms() const +{ + return boost::make_tuple(this->mm1_to_qm, this->mm1_to_mm2, this->bond_scale_factors); +} + +QVector PyQMForce::getMM2Atoms() const +{ + return this->mm2_atoms; +} + +QVector PyQMForce::getNumbers() const +{ + return this->numbers; +} + +QVector PyQMForce::getCharges() const +{ + return this->charges; +} + +const char *PyQMForce::typeName() +{ + return QMetaType::typeName(qMetaTypeId()); +} + +const char *PyQMForce::what() const +{ + return PyQMForce::typeName(); +} + +boost::tuple>, QVector>> +PyQMForce::call( + QVector numbers_qm, + QVector charges_mm, + QVector> xyz_qm, + QVector> xyz_mm, + QVector idx_mm) const +{ + return this->callback.call(numbers_qm, charges_mm, xyz_qm, xyz_mm, idx_mm); +} + +///////// +///////// OpenMM Serialization +///////// + +namespace OpenMM +{ + class PyQMForceProxy : public SerializationProxy { + public: + PyQMForceProxy() : SerializationProxy("PyQMForce") + { + }; + + void serialize(const void* object, SerializationNode& node) const + { + // Serialize the object. + QByteArray data; + QDataStream ds(&data, QIODevice::WriteOnly); + PyQMForce pyqmforce = *static_cast(object); + ds << pyqmforce; + + // Set the version. + node.setIntProperty("version", 0); + + // Set the note attribute. + node.setStringProperty("note", + "This force only supports partial serialization, so can only be used " + "within the same session and memory space."); + + // Set the data by converting the QByteArray to a hexidecimal string. + node.setStringProperty("data", data.toHex().data()); + }; + + void* deserialize(const SerializationNode& node) const + { + // Check the version. + int version = node.getIntProperty("version"); + if (version != 0) + { + throw OpenMM::OpenMMException("Unsupported version number"); + } + + // Get the data as a std::string. + auto string = node.getStringProperty("data"); + + // Convert to hexidecimal. + auto hex = QByteArray::fromRawData(string.data(), string.size()); + + // Convert to a QByteArray. + auto data = QByteArray::fromHex(hex); + + // Deserialize the object. + QDataStream ds(data); + PyQMForce pyqmforce; + + try + { + ds >> pyqmforce; + } + catch (...) + { + throw OpenMM::OpenMMException("Unable to find UUID in the PyQMForce callback registry."); + } + + return new PyQMForce(pyqmforce); + }; + }; + + // Register the PyQMForce serialization proxy. + extern "C" void registerPyQMSerializationProxies() { + SerializationProxy::registerProxy(typeid(PyQMForce), new PyQMForceProxy()); + } +}; + +///////// +///////// Implementation of PyQMForceImpl +///////// + +OpenMM::ForceImpl *PyQMForce::createImpl() const +{ +#ifdef SIRE_USE_CUSTOMCPPFORCE + return new PyQMForceImpl(*this); +#else + throw SireError::unsupported(QObject::tr( + "Unable to create an PyQMForceImpl because OpenMM::CustomCPPForceImpl " + "is not available. You need to use OpenMM 8.1 or later."), + CODELOC); + return 0; +#endif +} + +#ifdef SIRE_USE_CUSTOMCPPFORCE +PyQMForceImpl::PyQMForceImpl(const PyQMForce &owner) : + OpenMM::CustomCPPForceImpl(owner), + owner(owner) +{ +} + +PyQMForceImpl::~PyQMForceImpl() +{ +} + +const PyQMForce &PyQMForceImpl::getOwner() const +{ + return this->owner; +} + +double PyQMForceImpl::computeForce( + OpenMM::ContextImpl &context, + const std::vector &positions, + std::vector &forces) +{ + // If this is the first step, then setup information for the neighbour list. + if (this->step_count == 0) + { + // Store the cutoff as a double in Angstom. + this->cutoff = this->owner.getCutoff().value(); + + // The neighbour list cutoff is 20% larger than the cutoff. + this->neighbour_list_cutoff = 1.2*this->cutoff; + + // Store the neighbour list update frequency. + this->neighbour_list_frequency = this->owner.getNeighbourListFrequency(); + + // Flag whether a neighbour list is used. + this->is_neighbour_list = this->neighbour_list_frequency > 0; + } + + // Get the current box vectors in nanometers. + OpenMM::Vec3 box_x, box_y, box_z; + context.getPeriodicBoxVectors(box_x, box_y, box_z); + + // Create a triclinic space, converting to Angstrom. + TriclinicBox space( + Vector(10*box_x[0], 10*box_x[1], 10*box_x[2]), + Vector(10*box_y[0], 10*box_y[1], 10*box_y[2]), + Vector(10*box_z[0], 10*box_z[1], 10*box_z[2]) + ); + + // Store the QM atomic indices and numbers. + auto qm_atoms = this->owner.getAtoms(); + auto numbers = this->owner.getNumbers(); + + // Store the link atom info. Link atoms are handled using the Charge Shift + // method. See: https://www.ks.uiuc.edu/Research/qmmm + const auto link_atoms = this->owner.getLinkAtoms(); + const auto mm1_to_qm = link_atoms.get<0>(); + const auto mm1_to_mm2 = link_atoms.get<1>(); + const auto bond_scale_factors = link_atoms.get<2>(); + const auto mm2_atoms = this->owner.getMM2Atoms(); + + // Initialise a vector to hold the current positions for the QM atoms. + QVector> xyz_qm(qm_atoms.size()); + QVector xyz_qm_vec(qm_atoms.size()); + + // First loop over all QM atoms and store the positions. + int i = 0; + for (const auto &idx : qm_atoms) + { + const auto &pos = positions[idx]; + Vector qm_vec(10*pos[0], 10*pos[1], 10*pos[2]); + xyz_qm_vec[i] = qm_vec; + i++; + } + + // Next sure that the QM atoms are whole (unwrapped). + xyz_qm_vec = space.makeWhole(xyz_qm_vec); + + // Get the center of the QM atoms. We will use this as a reference when + // re-imaging the MM atoms. Also store the QM atoms in the xyz_qm vector. + Vector center; + i = 0; + for (const auto &qm_vec : xyz_qm_vec) + { + xyz_qm[i] = QVector({qm_vec[0], qm_vec[1], qm_vec[2]}); + center += qm_vec; + i++; + } + center /= i; + + // Initialise a vector to hold the current positions and charges for the MM atoms. + QVector> xyz_mm; + QVector charges_mm; + + // Initialise a list to hold the indices of the MM atoms. + QVector idx_mm; + + // Store the current number of MM atoms. + unsigned int num_mm = 0; + + // If we are using electrostatic embedding, the work out the MM point charges and + // build the neighbour list. + if (not this->owner.getIsMechanical()) + { + // Initialise a vector to hold the current positions and charges for the virtual + // point charges. + QVector> xyz_virtual; + QVector charges_virtual; + + // Manually work out the MM point charges and build the neigbour list. + if (not this->is_neighbour_list or this->step_count % this->neighbour_list_frequency == 0) + { + // Clear the neighbour list. + if (this->is_neighbour_list) + { + this->neighbour_list.clear(); + } + + i = 0; + // Loop over all of the OpenMM positions. + for (const auto &pos : positions) + { + // Exclude QM atoms or link atoms, which are handled later. + if (not qm_atoms.contains(i) and + not mm1_to_mm2.contains(i) and + not mm2_atoms.contains(i)) + { + // Store the MM atom position in Sire Vector format. + Vector mm_vec(10*pos[0], 10*pos[1], 10*pos[2]); + + // Loop over all of the QM atoms. + for (const auto &qm_vec : xyz_qm_vec) + { + // Work out the distance between the current MM atom and QM atoms. + const auto dist = space.calcDist(mm_vec, qm_vec); + + // The current MM atom is within the neighbour list cutoff. + if (this->is_neighbour_list and dist < this->neighbour_list_cutoff) + { + // Insert the MM atom index into the neighbour list. + this->neighbour_list.insert(i); + } + + // The current MM atom is within the cutoff, add it. + if (dist < cutoff) + { + // Work out the minimum image position with respect to the + // reference position and add to the vector. + mm_vec = space.getMinimumImage(mm_vec, center); + xyz_mm.append(QVector({mm_vec[0], mm_vec[1], mm_vec[2]})); + + // Add the charge and index. + charges_mm.append(this->owner.getCharges()[i]); + idx_mm.append(i); + + // Exit the inner loop. + break; + } + } + } + + // Update the atom index. + i++; + } + } + // Use the neighbour list. + else + { + // Loop over the MM atoms in the neighbour list. + for (const auto &idx : this->neighbour_list) + { + // Store the MM atom position in Sire Vector format. + Vector mm_vec(10*positions[idx][0], 10*positions[idx][1], 10*positions[idx][2]); + + // Loop over all of the QM atoms. + for (const auto &qm_vec : xyz_qm_vec) + { + // The current MM atom is within the cutoff, add it. + if (space.calcDist(mm_vec, qm_vec) < cutoff) + { + // Work out the minimum image position with respect to the + // reference position and add to the vector. + mm_vec = space.getMinimumImage(mm_vec, center); + xyz_mm.append(QVector({mm_vec[0], mm_vec[1], mm_vec[2]})); + + // Add the charge and index. + charges_mm.append(this->owner.getCharges()[idx]); + idx_mm.append(idx); + + // Exit the inner loop. + break; + } + } + } + } + + // Handle link atoms via the Charge Shift method. + // See: https://www.ks.uiuc.edu/Research/qmmm + for (const auto &idx: mm1_to_mm2.keys()) + { + // Get the QM atom to which the current MM atom is bonded. + const auto qm_idx = mm1_to_qm[idx]; + + // Store the MM1 position in Sire Vector format, along with the + // position of the QM atom to which it is bonded. + Vector mm1_vec(10*positions[idx][0], 10*positions[idx][1], 10*positions[idx][2]); + Vector qm_vec(10*positions[qm_idx][0], 10*positions[qm_idx][1], 10*positions[qm_idx][2]); + + // Work out the minimum image positions with respect to the reference position. + mm1_vec = space.getMinimumImage(mm1_vec, center); + qm_vec = space.getMinimumImage(qm_vec, center); + + // Work out the position of the link atom. Here we use a bond length + // scale factor taken from the MM bond potential, i.e. R0(QM-L) / R0(QM-MM1), + // where R0(QM-L) is the equilibrium bond length for the QM and link (L) + // elements, and R0(QM-MM1) is the equilibrium bond length for the QM + // and MM1 elements. + const auto link_vec = qm_vec + bond_scale_factors[idx]*(mm1_vec - qm_vec); + + // Add to the QM positions. + xyz_qm.append(QVector({link_vec[0], link_vec[1], link_vec[2]})); + + // Add the MM1 index to the QM atoms vector. + qm_atoms.append(qm_idx); + + // Append a hydrogen element to the numbers vector. + numbers.append(1); + + // Store the number of MM2 atoms. + const auto num_mm2 = mm1_to_mm2[idx].size(); + + // Store the fractional charge contribution to the MM2 atoms and + // virtual point charges. + const auto frac_charge = this->owner.getCharges()[idx] / num_mm2; + + // Loop over the MM2 atoms and perform charge shifting. Here the MM1 + // charge is redistributed over the MM2 atoms and two virtual point + // charges are added either side of the MM2 atoms in order to preserve + // the MM1-MM2 dipole. + for (const auto& mm2_idx : mm1_to_mm2[idx]) + { + // Store the MM2 position in Sire Vector format. + Vector mm2_vec(10*positions[mm2_idx][0], 10*positions[mm2_idx][1], 10*positions[mm2_idx][2]); + + // Work out the minimum image position with respect to the reference position. + mm2_vec = space.getMinimumImage(mm2_vec, center); + + // Add to the MM positions. + xyz_mm.append(QVector({mm2_vec[0], mm2_vec[1], mm2_vec[2]})); + + // Add the charge and index. + charges_mm.append(this->owner.getCharges()[mm2_idx] + frac_charge); + idx_mm.append(mm2_idx); + + // Now add the virtual point charges. + + // Compute the normal vector from the MM1 to MM2 atom. + const auto normal = (mm2_vec - mm1_vec).normalise(); + + // Positive direction. (Away from MM1 atom.) + auto xyz = mm2_vec + VIRTUAL_PC_DELTA*normal; + xyz_virtual.append(QVector({xyz[0], xyz[1], xyz[2]})); + charges_virtual.append(-frac_charge); + + // Negative direction (Towards MM1 atom.) + xyz = mm2_vec - VIRTUAL_PC_DELTA*normal; + xyz_virtual.append(QVector({xyz[0], xyz[1], xyz[2]})); + charges_virtual.append(frac_charge); + } + } + + // Store the current number of MM atoms. + num_mm = xyz_mm.size(); + + // If there are any virtual point charges, then add to the MM positions + // and charges. + if (xyz_virtual.size() > 0) + { + xyz_mm.append(xyz_virtual); + charges_mm.append(charges_virtual); + } + } + + // Call the callback. + auto result = this->owner.call( + numbers, + charges_mm, + xyz_qm, + xyz_mm, + idx_mm + ); + + // Extract the results. These will automatically be returned in OpenMM units. + auto energy = result.get<0>(); + auto forces_qm = result.get<1>(); + auto forces_mm = result.get<2>(); + + // The current interpolation (weighting) parameter. + double lambda; + + // Try to get the "lambda_emle" global parameter from the context. + try + { + lambda = context.getParameter("lambda_emle"); + } + catch (...) + { + // Try to get the "lambda_interpolate" global parameter from the context. + try + { + lambda = context.getParameter("lambda_interpolate"); + } + // Fall back on the lambda value stored in the PyQMForce object. + catch (...) + { + lambda = this->owner.getLambda(); + } + } + + // Clamp the lambda value. + if (lambda < 0.0) + { + lambda = 0.0; + } + else if (lambda > 1.0) + { + lambda = 1.0; + } + + // Now update the force vector. + + // First the QM atoms. + for (int i=0; istep_count++; + + // Finally, return the energy. + return lambda * energy; +} +#endif + +///////// +///////// Implementation of PyQMEngine +///////// + +PyQMEngine::PyQMEngine() : ConcreteProperty() +{ + // Register the serialization proxies. + OpenMM::registerPyQMSerializationProxies(); +} + +PyQMEngine::PyQMEngine( + bp::object py_object, + QString name, + SireUnits::Dimension::Length cutoff, + int neighbour_list_frequency, + bool is_mechanical, + double lambda) : + ConcreteProperty(), + callback(py_object, name), + cutoff(cutoff), + neighbour_list_frequency(neighbour_list_frequency), + is_mechanical(is_mechanical), + lambda(lambda) +{ + // Register the serialization proxies. + OpenMM::registerPyQMSerializationProxies(); + + if (this->neighbour_list_frequency < 0) + { + neighbour_list_frequency = 0; + } + if (this->lambda < 0.0) + { + this->lambda = 0.0; + } + else if (this->lambda > 1.0) + { + this->lambda = 1.0; + } +} + +PyQMEngine::PyQMEngine(const PyQMEngine &other) : + callback(other.callback), + cutoff(other.cutoff), + neighbour_list_frequency(other.neighbour_list_frequency), + is_mechanical(other.is_mechanical), + lambda(other.lambda), + atoms(other.atoms), + mm1_to_qm(other.mm1_to_qm), + mm1_to_mm2(other.mm1_to_mm2), + mm2_atoms(other.mm2_atoms), + bond_scale_factors(other.bond_scale_factors), + numbers(other.numbers), + charges(other.charges) +{ +} + +PyQMEngine &PyQMEngine::operator=(const PyQMEngine &other) +{ + this->callback = other.callback; + this->cutoff = other.cutoff; + this->neighbour_list_frequency = other.neighbour_list_frequency; + this->is_mechanical = other.is_mechanical; + this->lambda = other.lambda; + this->atoms = other.atoms; + this->mm1_to_qm = other.mm1_to_qm; + this->mm1_to_mm2 = other.mm1_to_mm2; + this->mm2_atoms = other.mm2_atoms; + this->bond_scale_factors = other.bond_scale_factors; + this->numbers = other.numbers; + this->charges = other.charges; + return *this; +} + +void PyQMEngine::setCallback(PyQMCallback callback) +{ + this->callback = callback; +} + +PyQMCallback PyQMEngine::getCallback() const +{ + return this->callback; +} + +void PyQMEngine::setLambda(double lambda) +{ + // Clamp the lambda value. + if (lambda < 0.0) + { + lambda = 0.0; + } + else if (lambda > 1.0) + { + lambda = 1.0; + } + this->lambda = lambda; +} + +double PyQMEngine::getLambda() const +{ + return this->lambda; +} + +void PyQMEngine::setCutoff(SireUnits::Dimension::Length cutoff) +{ + this->cutoff = cutoff; +} + +SireUnits::Dimension::Length PyQMEngine::getCutoff() const +{ + return this->cutoff; +} + +int PyQMEngine::getNeighbourListFrequency() const +{ + return this->neighbour_list_frequency; +} + +void PyQMEngine::setNeighbourListFrequency(int neighbour_list_frequency) +{ + // Assume anything less than zero means no neighbour list. + if (neighbour_list_frequency < 0) + { + neighbour_list_frequency = 0; + } + this->neighbour_list_frequency = neighbour_list_frequency; +} + +bool PyQMEngine::getIsMechanical() const +{ + return this->is_mechanical; +} + +void PyQMEngine::setIsMechanical(bool is_mechanical) +{ + this->is_mechanical = is_mechanical; +} + +QVector PyQMEngine::getAtoms() const +{ + return this->atoms; +} + +void PyQMEngine::setAtoms(QVector atoms) +{ + this->atoms = atoms; +} + +boost::tuple, QMap>, QMap> PyQMEngine::getLinkAtoms() const +{ + return boost::make_tuple(this->mm1_to_qm, this->mm1_to_mm2, this->bond_scale_factors); +} + +void PyQMEngine::setLinkAtoms( + QMap mm1_to_qm, + QMap> mm1_to_mm2, + QMap bond_scale_factors) +{ + this->mm1_to_qm = mm1_to_qm; + this->mm1_to_mm2 = mm1_to_mm2; + this->bond_scale_factors = bond_scale_factors; + + // Build a vector of all of the MM2 atoms. + this->mm2_atoms.clear(); + for (const auto &mm2 : this->mm1_to_mm2.values()) + { + this->mm2_atoms.append(mm2); + } +} + +QVector PyQMEngine::getMM2Atoms() const +{ + return this->mm2_atoms; +} + +QVector PyQMEngine::getNumbers() const +{ + return this->numbers; +} + +void PyQMEngine::setNumbers(QVector numbers) +{ + this->numbers = numbers; +} + +QVector PyQMEngine::getCharges() const +{ + return this->charges; +} + +void PyQMEngine::setCharges(QVector charges) +{ + this->charges = charges; +} + +const char *PyQMEngine::typeName() +{ + return QMetaType::typeName(qMetaTypeId()); +} + +const char *PyQMEngine::what() const +{ + return PyQMEngine::typeName(); +} + +boost::tuple>, QVector>> +PyQMEngine::call( + QVector numbers_qm, + QVector charges_mm, + QVector> xyz_qm, + QVector> xyz_mm, + QVector idx_mm) const +{ + return this->callback.call(numbers_qm, charges_mm, xyz_qm, xyz_mm, idx_mm); +} + +QMForce* PyQMEngine::createForce() const +{ + return new PyQMForce( + this->callback, + this->cutoff, + this->neighbour_list_frequency, + this->is_mechanical, + this->lambda, + this->atoms, + this->mm1_to_qm, + this->mm1_to_mm2, + this->bond_scale_factors, + this->mm2_atoms, + this->numbers, + this->charges + ); +} diff --git a/wrapper/Convert/SireOpenMM/pyqm.h b/wrapper/Convert/SireOpenMM/pyqm.h new file mode 100644 index 000000000..f4e94a658 --- /dev/null +++ b/wrapper/Convert/SireOpenMM/pyqm.h @@ -0,0 +1,641 @@ +/********************************************\ + * + * Sire - Molecular Simulation Framework + * + * Copyright (C) 2023 Christopher Woods + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + * For full details of the license please see the COPYING file + * that should have come with this distribution. + * + * You can contact the authors via the website + * at https://sire.openbiosim.org + * +\*********************************************/ + +#ifndef SIREOPENMM_PYQM_H +#define SIREOPENMM_PYQM_H + +#include "OpenMM.h" +#include "openmm/Force.h" +#ifdef SIRE_USE_CUSTOMCPPFORCE +#include "openmm/internal/ContextImpl.h" +#include "openmm/internal/CustomCPPForceImpl.h" +#endif + +#include "boost/python.hpp" +#include + +#include +#include + +#include "sireglobal.h" + +#include "SireUnits/dimensions.h" +#include "SireUnits/units.h" + +#include "qmmm.h" + +namespace bp = boost::python; + +SIRE_BEGIN_HEADER + +namespace SireOpenMM +{ + class PyQMCallback; + class PyQMForce; +} + +QDataStream &operator<<(QDataStream &, const SireOpenMM::PyQMCallback &); +QDataStream &operator>>(QDataStream &, SireOpenMM::PyQMCallback &); + +QDataStream &operator<<(QDataStream &, const SireOpenMM::PyQMForce &); +QDataStream &operator>>(QDataStream &, SireOpenMM::PyQMForce &); + +namespace SireOpenMM +{ + // A callback wrapper class to interface with external QM engines + // via the CustomCPPForceImpl. + class PyQMCallback + { + friend QDataStream & ::operator<<(QDataStream &, const PyQMCallback &); + friend QDataStream & ::operator>>(QDataStream &, PyQMCallback &); + + public: + //! Default constructor. + PyQMCallback(); + + //! Constructor + /*! \param py_object + A Python object that contains the callback function. + + \param name + The name of a callback method that take the following arguments: + - numbers_qm: A list of atomic numbers for the atoms in the ML region. + - charges_mm: A list of the MM charges in mod electron charge. + - xyz_qm: A list of positions for the atoms in the ML region in Angstrom. + - xyz_mm: A list of positions for the atoms in the MM region in Angstrom. + - idx_mm: A list of indices for MM atom indices in the QM/MM region. + The callback should return a tuple containing: + - The energy in kJ/mol. + - A list of forces for the QM atoms in kJ/mol/nm. + - A list of forces for the MM atoms in kJ/mol/nm. + If empty, then the object is assumed to be a callable. + */ + PyQMCallback(bp::object, QString name=""); + + //! Call the callback function. + /*! \param numbers_qm + A vector of atomic numbers for the atoms in the ML region. + + \param charges_mm + A vector of the charges on the MM atoms in mod electron charge. + + \param xyz_qm + A vector of positions for the atoms in the ML region in Angstrom. + + \param xyz_mm + A vector of positions for the atoms in the MM region in Angstrom. + + \param idx_mm + A vector of MM atom indices. Note that len(idx_mm) <= len(charges_mm) + since it only contains the indices of true MM atoms, not link atoms + or virtual charges. + + \returns + A tuple containing: + - The energy in kJ/mol. + - A vector of forces for the QM atoms in kJ/mol/nm. + - A vector of forces for the MM atoms in kJ/mol/nm. + */ + boost::tuple>, QVector>> call( + QVector numbers_qm, + QVector charges_mm, + QVector> xyz_qm, + QVector> xyz_mm, + QVector idx_mm + ) const; + + //! Return the C++ name for this class. + static const char *typeName(); + + //! Return the C++ name for this class. + const char *what() const; + + private: + bp::object py_object; + QString name; + bool is_method = true; + }; + + class PyQMForce : public QMForce + { + friend QDataStream & ::operator<<(QDataStream &, const PyQMForce &); + friend QDataStream & ::operator>>(QDataStream &, PyQMForce &); + + public: + //! Default constructor. + PyQMForce(); + + //! Constructor. + /* \param callback + The PyQMCallback object. + + \param cutoff + The ML cutoff distance. + + \param neighbour_list_frequency + The frequency at which the neighbour list is updated. (Number of steps.) + If zero, then no neighbour list is used. + + \param is_mechanical + A flag to indicate if mechanical embedding is being used. + + \param lambda + The lambda weighting factor. This can be used to interpolate between + potentials for end-state correction calculations. + + \param atoms + A vector of atom indices for the QM region. + + \param mm1_to_qm + A dictionary mapping link atom (MM1) indices to the QM atoms to + which they are bonded. + + \param mm1_to_mm2 + A dictionary of link atoms indices (MM1) to a list of the MM + atoms to which they are bonded (MM2). + + \param bond_scale_factors + A dictionary of link atom indices (MM1) to a list of the bond + length scale factors between the QM and MM1 atoms. The scale + factors are the ratio of the equilibrium bond lengths for the + QM-L (QM-link) atom and QM-MM1 atom, i.e. R0(QM-L) / R0(QM-MM1), + taken from the MM force field parameters for the molecule. + + \param mm2_atoms + A vector of MM2 atom indices. + + \param numbers + A vector of atomic charges for all atoms in the system. + + \param charges + A vector of atomic charges for all atoms in the system. + */ + PyQMForce( + PyQMCallback callback, + SireUnits::Dimension::Length cutoff, + int neighbour_list_frequency, + bool is_mechanical, + double lambda, + QVector atoms, + QMap mm1_to_qm, + QMap> mm1_to_mm2, + QMap bond_scale_factors, + QVector mm2_atoms, + QVector numbers, + QVector charges + ); + + //! Copy constructor. + PyQMForce(const PyQMForce &other); + + //! Assignment operator. + PyQMForce &operator=(const PyQMForce &other); + + //! Set the callback object. + /*! \param callback + A Python object that contains the callback function. + */ + void setCallback(PyQMCallback callback); + + //! Get the callback object. + /*! \returns + A Python object that contains the callback function. + */ + PyQMCallback getCallback() const; + + //! Get the lambda weighting factor. + /*! \returns + The lambda weighting factor. + */ + double getLambda() const; + + //! Set the lambda weighting factor + /* \param lambda + The lambda weighting factor. + */ + void setLambda(double lambda); + + //! Get the QM cutoff distance. + /*! \returns + The QM cutoff distance. + */ + SireUnits::Dimension::Length getCutoff() const; + + //! Get the neighbour list frequency. + /*! \returns + The neighbour list frequency. + */ + int getNeighbourListFrequency() const; + + //! Get the mechanical embedding flag. + /*! \returns + A flag to indicate if mechanical embedding is being used. + */ + bool getIsMechanical() const; + + //! Get the indices of the atoms in the QM region. + /*! \returns + A vector of atom indices for the QM region. + */ + QVector getAtoms() const; + + //! Get the link atoms associated with each QM atom. + /*! \returns + A tuple containing: + + mm1_to_qm + A dictionary mapping link atom (MM1) indices to the QM atoms to + which they are bonded. + + mm1_to_mm2 + A dictionary of link atoms indices (MM1) to a list of the MM + atoms to which they are bonded (MM2). + + bond_scale_factors + A dictionary of link atom indices (MM1) to a list of the bond + length scale factors between the QM and MM1 atoms. The scale + factors are the ratio of the equilibrium bond lengths for the + QM-L (QM-link) atom and QM-MM1 atom, i.e. R0(QM-L) / R0(QM-MM1), + taken from the MM force field parameters for the molecule. + + */ + boost::tuple, QMap>, QMap> getLinkAtoms() const; + + //! Get the vector of MM2 atoms. + /*! \returns + A vector of MM2 atom indices. + */ + QVector getMM2Atoms() const; + + //! Get the atomic numbers for the atoms in the QM region. + /*! \returns + A vector of atomic numbers for the atoms in the QM region. + */ + QVector getNumbers() const; + + //! Get the atomic charges of all atoms in the system. + /*! \returns + A vector of atomic charges for all atoms in the system. + */ + QVector getCharges() const; + + //! Return the C++ name for this class. + static const char *typeName(); + + //! Return the C++ name for this class. + const char *what() const; + + //! Call the callback function. + /*! \param numbers_qm + A vector of atomic numbers for the atoms in the ML region. + + \param charges_mm + A vector of the charges on the MM atoms in mod electron charge. + + \param xyz_qm + A vector of positions for the atoms in the ML region in Angstrom. + + \param xyz_mm + A vector of positions for the atoms in the MM region in Angstrom. + + \param idx_mm + A vector of MM atom indices. Note that len(idx_mm) <= len(charges_mm) + since it only contains the indices of true MM atoms, not link atoms + or virtual charges. + + \returns + A tuple containing: + - The energy in kJ/mol. + - A vector of forces for the QM atoms in kJ/mol/nm. + - A vector of forces for the MM atoms in kJ/mol/nm. + */ + boost::tuple>, QVector>> call( + QVector numbers_qm, + QVector charges_mm, + QVector> xyz_qm, + QVector> xyz_mm, + QVector idx_mm + ) const; + + protected: + OpenMM::ForceImpl *createImpl() const; + + private: + PyQMCallback callback; + SireUnits::Dimension::Length cutoff; + int neighbour_list_frequency; + bool is_mechanical; + double lambda; + QVector atoms; + QMap mm1_to_qm; + QMap> mm1_to_mm2; + QMap bond_scale_factors; + QVector mm2_atoms; + QVector numbers; + QVector charges; + }; + +#ifdef SIRE_USE_CUSTOMCPPFORCE + class PyQMForceImpl : public OpenMM::CustomCPPForceImpl + { + public: + PyQMForceImpl(const PyQMForce &owner); + + ~PyQMForceImpl(); + + double computeForce(OpenMM::ContextImpl &context, + const std::vector &positions, + std::vector &forces); + + const PyQMForce &getOwner() const; + + private: + const PyQMForce &owner; + unsigned long long step_count=0; + double cutoff; + bool is_neighbour_list; + int neighbour_list_frequency; + double neighbour_list_cutoff; + QSet neighbour_list; + }; +#endif + + class PyQMEngine : public SireBase::ConcreteProperty + { + public: + //! Default constructor. + PyQMEngine(); + + //! Constructor + /*! \param py_object + A Python object. + + \param name + The name of the callback method. If empty, then the object is + assumed to be a callable. + + \param cutoff + The ML cutoff distance. + + \param neighbour_list_frequency + The frequency at which the neighbour list is updated. (Number of steps.) + If zero, then no neighbour list is used. + + \param is_mechanical + A flag to indicate if mechanical embedding is being used. + + \param lambda + The lambda weighting factor. This can be used to interpolate between + potentials for end-state correction calculations. + */ + PyQMEngine( + bp::object, + QString method="", + SireUnits::Dimension::Length cutoff=7.5*SireUnits::angstrom, + int neighbour_list_frequency=0, + bool is_mechanical=false, + double lambda=1.0 + ); + + //! Copy constructor. + PyQMEngine(const PyQMEngine &other); + + //! Assignment operator. + PyQMEngine &operator=(const PyQMEngine &other); + + //! Set the callback object. + /*! \param callback + A Python object that contains the callback function. + */ + void setCallback(PyQMCallback callback); + + //! Get the callback object. + /*! \returns + A Python object that contains the callback function. + */ + PyQMCallback getCallback() const; + + //! Get the lambda weighting factor. + /*! \returns + The lambda weighting factor. + */ + double getLambda() const; + + //! Set the lambda weighting factor. + /*! \param lambda + The lambda weighting factor. + */ + void setLambda(double lambda); + + //! Get the QM cutoff distance. + /*! \returns + The QM cutoff distance. + */ + SireUnits::Dimension::Length getCutoff() const; + + //! Set the QM cutoff distance. + /*! \param cutoff + The QM cutoff distance. + */ + void setCutoff(SireUnits::Dimension::Length cutoff); + + //! Get the neighbour list frequency. + /*! \returns + The neighbour list frequency. + */ + int getNeighbourListFrequency() const; + + //! Set the neighbour list frequency. + /*! \param neighbour_list_frequency + The neighbour list frequency. + */ + void setNeighbourListFrequency(int neighbour_list_frequency); + + //! Get the mechanical embedding flag. + /*! \returns + A flag to indicate if mechanical embedding is being used. + */ + bool getIsMechanical() const; + + //! Set the mechanical embedding flag. + /*! \param is_mechanical + A flag to indicate if mechanical embedding is being used. + */ + void setIsMechanical(bool is_mechanical); + + //! Get the indices of the atoms in the QM region. + /*! \returns + A vector of atom indices for the QM region. + */ + QVector getAtoms() const; + + //! Set the list of atom indices for the QM region. + /*! \param atoms + A vector of atom indices for the QM region. + */ + void setAtoms(QVector atoms); + + //! Get the link atoms associated with each QM atom. + /*! \returns + A tuple containing: + + mm1_to_qm + A dictionary mapping link atom (MM1) indices to the QM atoms to + which they are bonded. + + mm1_to_mm2 + A dictionary of link atoms indices (MM1) to a list of the MM + atoms to which they are bonded (MM2). + + bond_scale_factors + A dictionary of link atom indices (MM1) to a list of the bond + length scale factors between the QM and MM1 atoms. The scale + factors are the ratio of the equilibrium bond lengths for the + QM-L (QM-link) atom and QM-MM1 atom, i.e. R0(QM-L) / R0(QM-MM1), + taken from the MM force field parameters for the molecule. + + */ + boost::tuple, QMap>, QMap> getLinkAtoms() const; + + //! Set the link atoms associated with each QM atom. + /*! \param mm1_to_qm + A dictionary mapping link atom (MM1) indices to the QM atoms to + which they are bonded. + + \param mm1_to_mm2 + A dictionary of link atoms indices (MM1) to a list of the MM + atoms to which they are bonded (MM2). + + \param bond_scale_factors + A dictionary of link atom indices (MM1) to a list of the bond + length scale factors between the QM and MM1 atoms. The scale + factors are the ratio of the equilibrium bond lengths for the + QM-L (QM-link) atom and QM-MM1 atom, i.e. R0(QM-L) / R0(QM-MM1), + taken from the MM force field parameters for the molecule. + + */ + void setLinkAtoms(QMap mm1_to_qm, QMap> mm1_to_mm2, QMap bond_scale_factors); + + //! Get the vector of MM2 atoms. + /*! \returns + A vector of MM2 atom indices. + */ + QVector getMM2Atoms() const; + + //! Get the atomic numbers for the atoms in the QM region. + /*! \returns + A vector of atomic numbers for the atoms in the QM region. + */ + QVector getNumbers() const; + + //! Set the atomic numbers for the atoms in the QM region. + /*! \param numbers + A vector of atomic numbers for the atoms in the QM region. + */ + void setNumbers(QVector numbers); + + //! Get the atomic charges of all atoms in the system. + /*! \returns + A vector of atomic charges for all atoms in the system. + */ + QVector getCharges() const; + + //! Set the atomic charges of all atoms in the system. + /*! \param charges + A vector of atomic charges for all atoms in the system. + */ + void setCharges(QVector charges); + + //! Return the C++ name for this class. + static const char *typeName(); + + //! Return the C++ name for this class. + const char *what() const; + + //! Call the callback function. + /*! \param numbers_qm + A vector of atomic numbers for the atoms in the ML region. + + \param charges_mm + A vector of the charges on the MM atoms in mod electron charge. + + \param xyz_qm + A vector of positions for the atoms in the ML region in Angstrom. + + \param xyz_mm + A vector of positions for the atoms in the MM region in Angstrom. + + \param idx_mm + A vector of MM atom indices. Note that len(idx_mm) <= len(charges_mm) + since it only contains the indices of true MM atoms, not link atoms + or virtual charges. + + \returns + A tuple containing: + - The energy in kJ/mol. + - A vector of forces for the QM atoms in kJ/mol/nm. + - A vector of forces for the MM atoms in kJ/mol/nm. + */ + boost::tuple>, QVector>> call( + QVector numbers_qm, + QVector charges_mm, + QVector> xyz_qm, + QVector> xyz_mm, + QVector idx_mm + ) const; + + //! Create an EMLE force object. + QMForce* createForce() const; + + private: + PyQMCallback callback; + SireUnits::Dimension::Length cutoff; + int neighbour_list_frequency; + bool is_mechanical; + double lambda; + QVector atoms; + QMap mm1_to_qm; + QMap> mm1_to_mm2; + QMap bond_scale_factors; + QVector mm2_atoms; + QVector numbers; + QVector charges; + }; +} + +Q_DECLARE_METATYPE(SireOpenMM::PyQMCallback) +Q_DECLARE_METATYPE(SireOpenMM::PyQMForce) +Q_DECLARE_METATYPE(SireOpenMM::PyQMEngine) + +SIRE_EXPOSE_CLASS(SireOpenMM::PyQMCallback) +SIRE_EXPOSE_CLASS(SireOpenMM::PyQMForce) +SIRE_EXPOSE_CLASS(SireOpenMM::PyQMEngine) + +SIRE_END_HEADER + +#endif diff --git a/wrapper/Convert/SireOpenMM/qmmm.cpp b/wrapper/Convert/SireOpenMM/qmmm.cpp new file mode 100644 index 000000000..e73844a65 --- /dev/null +++ b/wrapper/Convert/SireOpenMM/qmmm.cpp @@ -0,0 +1,99 @@ +/********************************************\ + * + * Sire - Molecular Simulation Framework + * + * Copyright (C) 2023 Christopher Woods + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + * For full details of the license please see the COPYING file + * that should have come with this distribution. + * + * You can contact the authors via the website + * at https://sire.openbiosim.org + * +\*********************************************/ + +#include "qmmm.h" + +#include "SireError/errors.h" + +using namespace SireBase; +using namespace SireOpenMM; + +///////// +///////// Implementation of QMForce +///////// + +/** Constructor */ +QMForce::QMForce() : OpenMM::Force() +{ +} + +QMForce::~QMForce() +{ +} + +///////// +///////// Implementation of QMEngine +///////// + +/** Constructor */ +QMEngine::QMEngine() : Property() +{ +} + +/** Destructor */ +QMEngine::~QMEngine() +{ +} + +/** Return a null QM engine */ +const NullQMEngine &QMEngine::null() +{ + return *(create_shared_null()); +} + +///////// +///////// Implementation of NullQMEngine +///////// + +/** Constructor */ +NullQMEngine::NullQMEngine() : ConcreteProperty() +{ +} + +/** Destructor */ +NullQMEngine::~NullQMEngine() +{ +} + +/** Return the type name */ +const char *NullQMEngine::typeName() +{ + return QMetaType::typeName(qMetaTypeId()); +} + +/** Return the type name */ +const char *NullQMEngine::what() const +{ + return NullQMEngine::typeName(); +} + +/** Create a QM force object */ +QMForce *NullQMEngine::createForce() const +{ + throw SireError::invalid_operation(QObject::tr("NullQMEngine::createForce() called"), CODELOC); +} diff --git a/wrapper/Convert/SireOpenMM/qmmm.h b/wrapper/Convert/SireOpenMM/qmmm.h new file mode 100644 index 000000000..ba662949b --- /dev/null +++ b/wrapper/Convert/SireOpenMM/qmmm.h @@ -0,0 +1,113 @@ +/********************************************\ + * + * Sire - Molecular Simulation Framework + * + * Copyright (C) 2023 Christopher Woods + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + * For full details of the license please see the COPYING file + * that should have come with this distribution. + * + * You can contact the authors via the website + * at https://sire.openbiosim.org + * +\*********************************************/ + +#ifndef SIREOPENMM_QMMM_H +#define SIREOPENMM_QMMM_H + +#include "OpenMM.h" +#include "openmm/Force.h" + +#include "sireglobal.h" + +#include "SireBase/property.h" + +SIRE_BEGIN_HEADER + +namespace SireOpenMM +{ + class QMForce; + class QMEngine; + class NullQMEngine; + + class QMForce : public OpenMM::Force + { + public: + QMForce(); + + virtual ~QMForce(); + + //! Set the lambda weighting factor. + virtual void setLambda(double lambda) = 0; + + protected: + virtual OpenMM::ForceImpl *createImpl() const = 0; + }; + + class QMEngine : public SireBase::Property + { + public: + QMEngine(); + + virtual ~QMEngine(); + + //! Clone the QM engine. + virtual QMEngine *clone() const = 0; + + static const char *typeName() + { + return "SireOpenMM::QMEngine"; + } + + //! Create a QM force object. + virtual QMForce* createForce() const=0; + + //! Get a null QM engine. + static const NullQMEngine &null(); + }; + + typedef SireBase::PropPtr QMEnginePtr; + + class NullQMEngine : public SireBase::ConcreteProperty + { + public: + NullQMEngine(); + + ~NullQMEngine(); + + //! Get the name of the QM engine. + static const char *typeName(); + + //! Get the name of the QM engine. + const char *what() const; + + //! Create a QM force object. + QMForce* createForce() const; + }; +} + +Q_DECLARE_METATYPE(SireOpenMM::NullQMEngine) + +SIRE_EXPOSE_CLASS(SireOpenMM::QMForce) +SIRE_EXPOSE_CLASS(SireOpenMM::QMEngine) +SIRE_EXPOSE_CLASS(SireOpenMM::NullQMEngine) + +SIRE_EXPOSE_PROPERTY(SireOpenMM::QMEnginePtr, SireOpenMM::QMEngine) + +SIRE_END_HEADER + +#endif diff --git a/wrapper/Convert/SireOpenMM/register_extras.cpp b/wrapper/Convert/SireOpenMM/register_extras.cpp index 88a50155d..1a18f9497 100644 --- a/wrapper/Convert/SireOpenMM/register_extras.cpp +++ b/wrapper/Convert/SireOpenMM/register_extras.cpp @@ -5,6 +5,12 @@ #include +#include +#include + +#include "Helpers/convertdict.hpp" +#include "Helpers/tuples.hpp" + namespace bp = boost::python; using namespace SireOpenMM; @@ -44,5 +50,16 @@ namespace SireOpenMM bp::converter::registry::insert(&extract_swig_wrapped_pointer, bp::type_id()); bp::converter::registry::insert(&extract_swig_wrapped_pointer, bp::type_id()); bp::converter::registry::insert(&extract_swig_wrapped_pointer, bp::type_id()); + + // A tuple return type container for the PyQMCallback. (Energy, QM forces, MM forces) + bp::register_tuple>, QVector>>>(); + + // Dictionary for mapping link atoms to QM and MM2 atoms. + register_dict>(); + register_dict>(); + register_dict>>(); + + // A tuple for passing link atom information to PyQMEngine. + bp::register_tuple, QMap>>>(); } } diff --git a/wrapper/Convert/SireOpenMM/sire_openmm.cpp b/wrapper/Convert/SireOpenMM/sire_openmm.cpp index 29d374efc..36794bb13 100644 --- a/wrapper/Convert/SireOpenMM/sire_openmm.cpp +++ b/wrapper/Convert/SireOpenMM/sire_openmm.cpp @@ -19,6 +19,7 @@ #include "SireMol/bondid.h" #include "SireMol/bondorder.h" #include "SireMol/atomvelocities.h" +#include "SireMol/selectorm.hpp" #include "SireMM/atomljs.h" #include "SireMM/selectorbond.h" @@ -46,8 +47,10 @@ using SireBase::PropertyMap; using SireCAS::LambdaSchedule; +using SireMol::Atom; using SireMol::Molecule; using SireMol::MolNum; +using SireMol::SelectorM; using SireMol::SelectorMol; using SireSystem::ForceFieldInfo; @@ -290,6 +293,173 @@ namespace SireOpenMM CODELOC); } + SelectorM extract_coordinates(const OpenMM::State &state, + const SireMol::SelectorM &atoms, + const QHash &perturbable_maps, + const SireBase::PropertyMap &map) + { + const auto positions = state.getPositions(); + + const int natoms = positions.size(); + const auto positions_data = positions.data(); + + if (atoms.count() > natoms) + { + throw SireError::incompatible_error(QObject::tr( + "Different number of atoms from OpenMM and sire. " + "%1 versus %2. Cannot extract the coordinates.") + .arg(natoms) + .arg(atoms.count()), + CODELOC); + } + + const auto mols = atoms.toSelectorVector(); + const auto mols_data = mols.constData(); + const int nmols = mols.count(); + + QVector> ret(nmols); + auto ret_data = ret.data(); + + const auto coords_prop = map["coordinates"]; + + QVector offsets(nmols); + + int offset = 0; + + for (int i = 0; i < nmols; ++i) + { + offsets[i] = offset; + offset += mols_data[i].count(); + } + + const auto offsets_data = offsets.constData(); + + if (SireBase::should_run_in_parallel(nmols, map)) + { + tbb::parallel_for(tbb::blocked_range(0, nmols), [&](const tbb::blocked_range &r) + { + QVector converted_coords; + + for (int i=r.begin(); i(my_coords_prop, + converted_coords, false)) + { + SireMol::AtomCoords c(mol.data().info()); + c.copyFrom(converted_coords); + molecule.setProperty(my_coords_prop, c); + } + + ret_data[i] = mol; + ret_data[i].update(molecule.commit().data()); + } + else + { + auto molecule = mol.molecule().edit(); + + SireMol::AtomCoords atomcoords; + + if (molecule.hasProperty(coords_prop)) + { + atomcoords = molecule.property(coords_prop).asA(); + } + else + { + atomcoords = SireMol::AtomCoords(molecule.data().info()); + } + + atomcoords.copyFrom(converted_coords, mol.selection()); + + molecule.setProperty(coords_prop, atomcoords); + + ret_data[i] = mol; + ret_data[i].update(molecule.commit().data()); + } + } }); + } + else + { + QVector converted_coords; + + for (int i = 0; i < nmols; ++i) + { + const auto &mol = mols_data[i]; + const int mol_natoms = mol.count(); + + if (mol_natoms == 0) + continue; + + _populate_coords(converted_coords, positions_data + offsets_data[i], mol_natoms); + + auto my_coords_prop = coords_prop.source(); + + if (perturbable_maps.contains(mol.data().number())) + { + my_coords_prop = perturbable_maps[mol.data().number()]["coordinates"].source(); + } + + if (mol.selectedAll()) + { + auto molecule = mol.molecule().edit(); + + if (not molecule.updatePropertyFrom(my_coords_prop, + converted_coords, false)) + { + SireMol::AtomCoords c(mol.data().info()); + c.copyFrom(converted_coords); + molecule.setProperty(my_coords_prop, c); + } + + ret_data[i] = mol; + ret_data[i].update(molecule.commit().data()); + } + else + { + auto molecule = mol.molecule().edit(); + + SireMol::AtomCoords atomcoords; + + if (molecule.hasProperty(coords_prop)) + { + atomcoords = molecule.property(coords_prop).asA(); + } + else + { + atomcoords = SireMol::AtomCoords(molecule.data().info()); + } + + atomcoords.copyFrom(converted_coords, mol.selection()); + + molecule.setProperty(coords_prop, atomcoords); + + ret_data[i] = mol; + ret_data[i].update(molecule.commit().data()); + } + } + } + + return SelectorM(ret.toList()); + } + SelectorMol extract_coordinates(const OpenMM::State &state, const SireMol::SelectorMol &mols, const QHash &perturbable_maps, @@ -393,6 +563,227 @@ namespace SireOpenMM return SelectorMol(ret); } + SelectorM extract_coordinates_and_velocities(const OpenMM::State &state, + const SireMol::SelectorM &atoms, + const QHash &perturbable_maps, + const SireBase::PropertyMap &map) + { + const auto positions = state.getPositions(); + const auto velocities = state.getVelocities(); + + const int natoms = positions.size(); + const auto positions_data = positions.data(); + const auto velocities_data = velocities.data(); + + if (atoms.count() > natoms) + { + throw SireError::incompatible_error(QObject::tr( + "Different number of atoms from OpenMM and sire. " + "%1 versus %2. Cannot extract the coordinates.") + .arg(natoms) + .arg(atoms.count()), + CODELOC); + } + + const auto mols = atoms.toSelectorVector(); + const auto mols_data = mols.constData(); + const int nmols = mols.count(); + + QVector> ret(nmols); + auto ret_data = ret.data(); + + const auto coords_prop = map["coordinates"]; + const auto vels_prop = map["velocity"]; + + QVector offsets(nmols); + + int offset = 0; + + for (int i = 0; i < nmols; ++i) + { + offsets[i] = offset; + offset += mols_data[i].count(); + } + + const auto offsets_data = offsets.constData(); + + if (SireBase::should_run_in_parallel(nmols, map)) + { + tbb::parallel_for(tbb::blocked_range(0, nmols), [&](const tbb::blocked_range &r) + { + QVector converted_coords; + QVector converted_vels; + + for (int i=r.begin(); i(my_coords_prop, + converted_coords, false)) + { + SireMol::AtomCoords c(mol.data().info()); + c.copyFrom(converted_coords); + molecule.setProperty(my_coords_prop, c); + } + + if (not molecule.updatePropertyFrom(my_vels_prop, + converted_vels, false)) + { + SireMol::AtomVelocities v(mol.data().info()); + v.copyFrom(converted_vels); + molecule.setProperty(my_vels_prop, v); + } + + ret_data[i] = mol; + ret_data[i].update(molecule.commit().data()); + } + else + { + auto molecule = mol.molecule().edit(); + + SireMol::AtomCoords atomcoords; + + if (molecule.hasProperty(coords_prop)) + { + atomcoords = molecule.property(coords_prop).asA(); + } + else + { + atomcoords = SireMol::AtomCoords(molecule.data().info()); + } + + atomcoords.copyFrom(converted_coords, mol.selection()); + + SireMol::AtomVelocities atomvels; + + if (molecule.hasProperty(vels_prop)) + { + atomvels = molecule.property(vels_prop).asA(); + } + else + { + atomvels = SireMol::AtomVelocities(molecule.data().info()); + } + + atomvels.copyFrom(converted_vels, mol.selection()); + + molecule.setProperty(coords_prop, atomcoords); + molecule.setProperty(vels_prop, atomvels); + + ret_data[i] = mol; + ret_data[i].update(molecule.commit().data()); + } + } }); + } + else + { + QVector converted_coords; + QVector converted_vels; + + for (int i = 0; i < nmols; ++i) + { + const auto &mol = mols_data[i]; + const int mol_natoms = mol.count(); + + if (mol_natoms == 0) + continue; + + _populate_coords(converted_coords, positions_data + offsets_data[i], mol_natoms); + _populate_vels(converted_vels, velocities_data + offsets_data[i], mol_natoms); + + auto my_coords_prop = coords_prop.source(); + auto my_vels_prop = vels_prop.source(); + + if (perturbable_maps.contains(mol.data().number())) + { + my_coords_prop = perturbable_maps[mol.data().number()]["coordinates"].source(); + my_vels_prop = perturbable_maps[mol.data().number()]["velocity"].source(); + } + + if (mol.selectedAll()) + { + auto molecule = mol.molecule().edit(); + + if (not molecule.updatePropertyFrom(my_coords_prop, + converted_coords, false)) + { + SireMol::AtomCoords c(mol.data().info()); + c.copyFrom(converted_coords); + molecule.setProperty(my_coords_prop, c); + } + + if (not molecule.updatePropertyFrom(my_vels_prop, + converted_vels, false)) + { + SireMol::AtomVelocities v(mol.data().info()); + v.copyFrom(converted_vels); + molecule.setProperty(my_vels_prop, v); + } + + ret_data[i] = mol; + ret_data[i].update(molecule.commit().data()); + } + else + { + auto molecule = mol.molecule().edit(); + + SireMol::AtomCoords atomcoords; + SireMol::AtomVelocities atomvels; + + if (molecule.hasProperty(coords_prop)) + { + atomcoords = molecule.property(coords_prop).asA(); + } + else + { + atomcoords = SireMol::AtomCoords(molecule.data().info()); + } + + atomcoords.copyFrom(converted_coords, mol.selection()); + + if (molecule.hasProperty(vels_prop)) + { + atomvels = molecule.property(vels_prop).asA(); + } + else + { + atomvels = SireMol::AtomVelocities(molecule.data().info()); + } + + atomvels.copyFrom(converted_vels, mol.selection()); + + molecule.setProperty(coords_prop, atomcoords); + molecule.setProperty(vels_prop, atomvels); + + ret_data[i] = mol; + ret_data[i].update(molecule.commit().data()); + } + } + } + + return SelectorM(ret.toList()); + } + SelectorMol extract_coordinates_and_velocities(const OpenMM::State &state, const SireMol::SelectorMol &mols, const QHash &perturbable_maps, diff --git a/wrapper/Convert/SireOpenMM/sire_openmm.h b/wrapper/Convert/SireOpenMM/sire_openmm.h index b083a2c20..01b2a3517 100644 --- a/wrapper/Convert/SireOpenMM/sire_openmm.h +++ b/wrapper/Convert/SireOpenMM/sire_openmm.h @@ -86,11 +86,21 @@ namespace SireOpenMM SireUnits::Dimension::MolarEnergy get_potential_energy(OpenMM::Context &context); + SireMol::SelectorM extract_coordinates(const OpenMM::State &state, + const SireMol::SelectorM &mols, + const QHash &perturbable_maps, + const SireBase::PropertyMap &map); + SireMol::SelectorMol extract_coordinates(const OpenMM::State &state, const SireMol::SelectorMol &mols, const QHash &perturbable_maps, const SireBase::PropertyMap &map); + SireMol::SelectorM extract_coordinates_and_velocities(const OpenMM::State &state, + const SireMol::SelectorM &mols, + const QHash &perturbable_maps, + const SireBase::PropertyMap &map); + SireMol::SelectorMol extract_coordinates_and_velocities(const OpenMM::State &state, const SireMol::SelectorMol &mols, const QHash &perturbable_maps, diff --git a/wrapper/Convert/SireOpenMM/sire_to_openmm_system.cpp b/wrapper/Convert/SireOpenMM/sire_to_openmm_system.cpp index 03e2ad242..e3c46a03e 100644 --- a/wrapper/Convert/SireOpenMM/sire_to_openmm_system.cpp +++ b/wrapper/Convert/SireOpenMM/sire_to_openmm_system.cpp @@ -47,6 +47,7 @@ #include "tostring.h" #include "openmmmolecule.h" +#include "pyqm.h" #include @@ -112,6 +113,7 @@ void _add_boresch_restraints(const SireMM::BoreschRestraints &restraints, .toStdString(); auto *restraintff = new OpenMM::CustomCompoundBondForce(6, energy_expression); + restraintff->setName("BoreschRestraintForce"); restraintff->addPerBondParameter("rho"); restraintff->addPerBondParameter("kr"); @@ -208,6 +210,7 @@ void _add_bond_restraints(const SireMM::BondRestraints &restraints, .toStdString(); auto *restraintff = new OpenMM::CustomBondForce(energy_expression); + restraintff->setName("BondRestraintForce"); restraintff->addPerBondParameter("rho"); restraintff->addPerBondParameter("k"); @@ -282,6 +285,7 @@ void _add_positional_restraints(const SireMM::PositionalRestraints &restraints, .toStdString(); auto *restraintff = new OpenMM::CustomBondForce(energy_expression); + restraintff->setName("PositionalRestraintForce"); restraintff->addPerBondParameter("rho"); restraintff->addPerBondParameter("k"); @@ -315,7 +319,8 @@ void _add_positional_restraints(const SireMM::PositionalRestraints &restraints, auto ghost_nonghostff = lambda_lever.getForce("ghost/non-ghost", system); std::vector custom_params = {1.0, 0.0, 0.0}; - std::vector custom_clj_params = {0.0, 0.0, 0.0, 0.0}; + // Define null parameters used to add these particles to the ghost forces (5 total) + std::vector custom_clj_params = {0.0, 0.0, 0.0, 0.0, 0.0}; // we need to add all of the positions as anchor particles for (const auto &restraint : atom_restraints) @@ -744,6 +749,23 @@ OpenMMMetaData SireOpenMM::sire_to_openmm_system(OpenMM::System &system, OpenMM::HarmonicAngleForce *angff = new OpenMM::HarmonicAngleForce(); OpenMM::PeriodicTorsionForce *dihff = new OpenMM::PeriodicTorsionForce(); + // now create the engine for computing QM forces on atoms + QMForce *qmff = 0; + + if (map.specified("qm_engine")) + { + try + { + auto &engine = map["qm_engine"].value().asA(); + qmff = engine.createForce(); + qmff->setName("QMForce"); + } + catch (...) + { + throw SireError::incompatible_error(QObject::tr("Invalid QM engine!"), CODELOC); + } + } + // end of stage 2 - we now have the base forces /// @@ -766,11 +788,18 @@ OpenMMMetaData SireOpenMM::sire_to_openmm_system(OpenMM::System &system, } else if (any_perturbable) { - // use a standard morph if we have an alchemical perturbation + // use a standard morph if we have an alchemical or QM perturbation lambda_lever.setSchedule( LambdaSchedule::standard_morph()); } + // Add any QM force first so that we can guarantee that it is index zero. + if (qmff != 0) + { + lambda_lever.setForceIndex("qmff", system.addForce(qmff)); + lambda_lever.addLever("qm_scale"); + } + // We can now add the standard forces to the OpenMM::System. // We do this here, so that we can capture the index of the // force and associate it with a name in the lever. @@ -916,7 +945,7 @@ OpenMMMetaData SireOpenMM::sire_to_openmm_system(OpenMM::System &system, { nb14_expression = QString( "coul_nrg+lj_nrg;" - "coul_nrg=138.9354558466661*q*(((%1)/sqrt((%2*alpha)+r_safe^2))-(kappa/r_safe));" + "coul_nrg=138.9354558466661*q*(((%1)/sqrt((%2*alpha*alpha)+r_safe^2))-(kappa/r_safe));" "lj_nrg=four_epsilon*sig6*(sig6-1);" "sig6=(sigma^6)/(%3*sigma^6 + r_safe^6);" "r_safe=max(r, 0.001);") @@ -929,7 +958,7 @@ OpenMMMetaData SireOpenMM::sire_to_openmm_system(OpenMM::System &system, { nb14_expression = QString( "coul_nrg+lj_nrg;" - "coul_nrg=138.9354558466661*q*(((%1)/sqrt((%2*alpha)+r_safe^2))-(kappa/r_safe));" + "coul_nrg=138.9354558466661*q*(((%1)/sqrt((%2*alpha*alpha)+r_safe^2))-(kappa/r_safe));" "lj_nrg=four_epsilon*sig6*(sig6-1);" "sig6=(sigma^6)/(((sigma*delta) + r_safe^2)^3);" "r_safe=max(r, 0.001);" @@ -941,6 +970,7 @@ OpenMMMetaData SireOpenMM::sire_to_openmm_system(OpenMM::System &system, } ghost_14ff = new OpenMM::CustomBondForce(nb14_expression); + ghost_14ff->setName("Ghost14BondForce"); ghost_14ff->addPerBondParameter("q"); ghost_14ff->addPerBondParameter("sigma"); @@ -976,7 +1006,7 @@ OpenMMMetaData SireOpenMM::sire_to_openmm_system(OpenMM::System &system, // kJ mol-1 given the units of charge (|e|) and distance (nm) // clj_expression = QString("coul_nrg+lj_nrg;" - "coul_nrg=138.9354558466661*q1*q2*(((%1)/sqrt((%2*max_alpha)+r_safe^2))-(max_kappa/r_safe));" + "coul_nrg=138.9354558466661*q1*q2*(((%1)/sqrt((%2*max_alpha*max_alpha)+r_safe^2))-(max_kappa/r_safe));" "lj_nrg=two_sqrt_epsilon1*two_sqrt_epsilon2*sig6*(sig6-1);" "sig6=(sigma^6)/(%3*sigma^6 + r_safe^6);" "r_safe=max(r, 0.001);" @@ -1014,7 +1044,7 @@ OpenMMMetaData SireOpenMM::sire_to_openmm_system(OpenMM::System &system, // kJ mol-1 given the units of charge (|e|) and distance (nm) // clj_expression = QString("coul_nrg+lj_nrg;" - "coul_nrg=138.9354558466661*q1*q2*(((%1)/sqrt((%2*max_alpha)+r_safe^2))-(max_kappa/r_safe));" + "coul_nrg=138.9354558466661*q1*q2*(((%1)/sqrt((%2*max_alpha*max_alpha)+r_safe^2))-(max_kappa/r_safe));" "lj_nrg=two_sqrt_epsilon1*two_sqrt_epsilon2*sig6*(sig6-1);" "sig6=(sigma^6)/(((sigma*delta) + r_safe^2)^3);" "delta=%3*max_alpha;" @@ -1029,7 +1059,9 @@ OpenMMMetaData SireOpenMM::sire_to_openmm_system(OpenMM::System &system, } ghost_ghostff = new OpenMM::CustomNonbondedForce(clj_expression); + ghost_ghostff->setName("GhostGhostNonbondedForce"); ghost_nonghostff = new OpenMM::CustomNonbondedForce(clj_expression); + ghost_nonghostff->setName("GhostNonGhostNonbondedForce"); ghost_ghostff->addPerParticleParameter("q"); ghost_ghostff->addPerParticleParameter("half_sigma"); @@ -1090,6 +1122,9 @@ OpenMMMetaData SireOpenMM::sire_to_openmm_system(OpenMM::System &system, // start_index keeps track of the index of the first atom in each molecule int start_index = 0; + // this is the list of atoms added, in atom order + QList> order_of_added_atoms; + // get the 1-4 scaling factors from the first molecule const double coul_14_scl = openmm_mols_data[0].ffinfo.electrostatic14ScaleFactor(); const double lj_14_scl = openmm_mols_data[0].ffinfo.vdw14ScaleFactor(); @@ -1098,9 +1133,10 @@ OpenMMMetaData SireOpenMM::sire_to_openmm_system(OpenMM::System &system, // particle in that molecule QVector start_indexes(nmols); - // the index to the perturbable molecule for the specified molecule + // the index to the perturbable or QM molecule for the specified molecule // (i.e. the 5th perturbable molecule is the 10th molecule in the System) QHash idx_to_pert_idx; + QHash idx_to_qm_idx; // just a holder for all of the custom parameters for the // ghost forces (prevents us having to continually re-allocate it) @@ -1142,6 +1178,8 @@ OpenMMMetaData SireOpenMM::sire_to_openmm_system(OpenMM::System &system, start_indexes[i] = start_index; const auto &mol = openmm_mols_data[i]; + order_of_added_atoms.append(mol.atoms); + // double-check that the molecule has a compatible forcefield with // the other molecules in this system if (std::abs(mol.ffinfo.electrostatic14ScaleFactor() - coul_14_scl) > 0.001 or @@ -1158,14 +1196,13 @@ OpenMMMetaData SireOpenMM::sire_to_openmm_system(OpenMM::System &system, CODELOC); } - // this hash holds the start indicies for the various - // parameters for this molecule (e.g. bond, angle, CLJ parameters) - // We only need to record this if this is a perturbable molecule - QHash start_indicies; - // is this a perturbable molecule (and we haven't disabled perturbations)? if (any_perturbable and mol.isPerturbable()) { + // this hash holds the start indicies for the various + // parameters for this molecule (e.g. bond, angle, CLJ parameters) + QHash start_indicies; + // add a perturbable molecule, recording the start index // for each of the forcefields start_indicies.reserve(7); @@ -1219,6 +1256,7 @@ OpenMMMetaData SireOpenMM::sire_to_openmm_system(OpenMM::System &system, // masses is used const int atom_index = start_index + j; + // NEED TO UPDATE FIXED ATOMS WITH FIELD ATOMS INDEXES!!! if (fixed_atoms.contains(atom_index)) { // this is a fixed (zero mass) atom @@ -1298,6 +1336,7 @@ OpenMMMetaData SireOpenMM::sire_to_openmm_system(OpenMM::System &system, // Add the particle to the system const int atom_index = start_index + j; + // NEED TO UPDATE FIXED ATOMS WITH FIELD ATOMS INDEXES!!! if (fixed_atoms.contains(atom_index)) { // this is a fixed (zero mass) atom @@ -1503,7 +1542,11 @@ OpenMMMetaData SireOpenMM::sire_to_openmm_system(OpenMM::System &system, boost::get<2>(p), 1e-9, 1e-9, true); - if (coul_14_scl != 0 or lj_14_scl != 0) + // Whether this is a to/from ghost interaction. + auto to_from_ghost = (from_ghost_idxs.contains(atom0) and to_ghost_idxs.contains(atom1)) or + (from_ghost_idxs.contains(atom1) and to_ghost_idxs.contains(atom0)); + + if (not to_from_ghost and (coul_14_scl != 0 or lj_14_scl != 0)) { // this is a 1-4 interaction that should be added // to the ghost-14 forcefield @@ -1565,8 +1608,7 @@ OpenMMMetaData SireOpenMM::sire_to_openmm_system(OpenMM::System &system, { auto pert_idx = idx_to_pert_idx.value(i, openmm_mols.count() + 1); lambda_lever.setExceptionIndicies(pert_idx, - "clj", exception_idxs); - + "clj", exception_idxs); lambda_lever.setConstraintIndicies(pert_idx, constraint_idxs); } @@ -1576,14 +1618,29 @@ OpenMMMetaData SireOpenMM::sire_to_openmm_system(OpenMM::System &system, // between from_ghosts and to_ghosts for (const auto &from_ghost_idx : from_ghost_idxs) { + // work out the molecule index for the from ghost atom + int mol_from = 0; + while (start_indexes[mol_from] <= from_ghost_idx and mol_from < nmols) + mol_from++; + for (const auto &to_ghost_idx : to_ghost_idxs) { + // work out the molecule index for the to ghost atom + int mol_to = 0; + while (start_indexes[mol_to] <= to_ghost_idx and mol_to < nmols) + mol_to++; + if (not excluded_ghost_pairs.contains(IndexPair(from_ghost_idx, to_ghost_idx))) { - ghost_ghostff->addExclusion(from_ghost_idx, to_ghost_idx); - ghost_nonghostff->addExclusion(from_ghost_idx, to_ghost_idx); - cljff->addException(from_ghost_idx, to_ghost_idx, - 0.0, 1e-9, 1e-9, true); + // only exclude if we haven't already excluded this pair + // and if the two atoms are in the same molecule + if (mol_from == mol_to) + { + ghost_ghostff->addExclusion(from_ghost_idx, to_ghost_idx); + ghost_nonghostff->addExclusion(from_ghost_idx, to_ghost_idx); + cljff->addException(from_ghost_idx, to_ghost_idx, + 0.0, 1e-9, 1e-9, true); + } } } } @@ -1700,7 +1757,7 @@ OpenMMMetaData SireOpenMM::sire_to_openmm_system(OpenMM::System &system, } } - // All done - we can return the metadata (atoms are always added in - // molidx/atomidx order) - return OpenMMMetaData(mols.atoms(), coords, vels, boxvecs, lambda_lever); + // All done - we can return the metadata + return OpenMMMetaData(SireMol::SelectorM(order_of_added_atoms), + coords, vels, boxvecs, lambda_lever); } diff --git a/wrapper/Convert/SireOpenMM/torchqm.cpp b/wrapper/Convert/SireOpenMM/torchqm.cpp new file mode 100644 index 000000000..1332c91c7 --- /dev/null +++ b/wrapper/Convert/SireOpenMM/torchqm.cpp @@ -0,0 +1,1052 @@ +/********************************************\ + * + * Sire - Molecular Simulation Framework + * + * Copyright (C) 2023 Christopher Woods + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + * For full details of the license please see the COPYING file + * that should have come with this distribution. + * + * You can contact the authors via the website + * at https://sire.openbiosim.org + * +\*********************************************/ + +#include "openmm/serialization/SerializationNode.h" +#include "openmm/serialization/SerializationProxy.h" + +#ifdef SIRE_USE_TORCH +#include +#endif + +#include "SireError/errors.h" +#include "SireMaths/vector.h" +#include "SireStream/datastream.h" +#include "SireStream/shareddatastream.h" +#include "SireVol/triclinicbox.h" + +#include "torchqm.h" + +using namespace SireMaths; +using namespace SireOpenMM; +using namespace SireStream; +using namespace SireVol; + +// The delta used to place virtual point charges either side of the MM2 +// atoms, in nanometers. +static const double VIRTUAL_PC_DELTA = 0.01; + +// Conversion factor from Hartree to kJ/mol. +static const double HARTREE_TO_KJ_MOL = 2625.499638755248; + +///////// +///////// Implementation of TorchQMForce +///////// + +static const RegisterMetaType r_torchqmforce(NO_ROOT); + +QDataStream &operator<<(QDataStream &ds, const TorchQMForce &torchqmforce) +{ + writeHeader(ds, r_torchqmforce, 1); + + SharedDataStream sds(ds); + + sds << torchqmforce.module_path << torchqmforce.cutoff << torchqmforce.neighbour_list_frequency + << torchqmforce.is_mechanical << torchqmforce.lambda << torchqmforce.atoms + << torchqmforce.mm1_to_qm << torchqmforce.mm1_to_mm2 << torchqmforce.bond_scale_factors + << torchqmforce.mm2_atoms << torchqmforce.numbers << torchqmforce.charges; + + return ds; +} + +QDataStream &operator>>(QDataStream &ds, TorchQMForce &torchqmforce) +{ + VersionID v = readHeader(ds, r_torchqmforce); + + if (v == 1) + { + SharedDataStream sds(ds); + + sds >> torchqmforce.module_path >> torchqmforce.cutoff >> torchqmforce.neighbour_list_frequency + >> torchqmforce.is_mechanical >> torchqmforce.lambda >> torchqmforce.atoms + >> torchqmforce.mm1_to_qm >> torchqmforce.mm1_to_mm2 >> torchqmforce.bond_scale_factors + >> torchqmforce.mm2_atoms >> torchqmforce.numbers >> torchqmforce.charges; + + // Re-load the Torch module. + torchqmforce.setModulePath(torchqmforce.getModulePath()); + } + else + throw version_error(v, "1", r_torchqmforce, CODELOC); + + return ds; +} + +TorchQMForce::TorchQMForce() +{ +} + +TorchQMForce::TorchQMForce( + QString module_path, + SireUnits::Dimension::Length cutoff, + int neighbour_list_frequency, + bool is_mechanical, + double lambda, + QVector atoms, + QMap mm1_to_qm, + QMap> mm1_to_mm2, + QMap bond_scale_factors, + QVector mm2_atoms, + QVector numbers, + QVector charges) : + cutoff(cutoff), + neighbour_list_frequency(neighbour_list_frequency), + is_mechanical(is_mechanical), + lambda(lambda), + atoms(atoms), + mm1_to_qm(mm1_to_qm), + mm1_to_mm2(mm1_to_mm2), + bond_scale_factors(bond_scale_factors), + mm2_atoms(mm2_atoms), + numbers(numbers), + charges(charges) +{ + // Try to load the Torch module. + this->setModulePath(module_path); +} + +TorchQMForce::TorchQMForce(const TorchQMForce &other) : + module_path(other.module_path), + torch_module(other.torch_module), + cutoff(other.cutoff), + neighbour_list_frequency(other.neighbour_list_frequency), + is_mechanical(other.is_mechanical), + lambda(other.lambda), + atoms(other.atoms), + mm1_to_qm(other.mm1_to_qm), + mm1_to_mm2(other.mm1_to_mm2), + mm2_atoms(other.mm2_atoms), + bond_scale_factors(other.bond_scale_factors), + numbers(other.numbers), + charges(other.charges) +{ +} + +TorchQMForce &TorchQMForce::operator=(const TorchQMForce &other) +{ + this->module_path = other.module_path; + this->torch_module = other.torch_module; + this->cutoff = other.cutoff; + this->neighbour_list_frequency = other.neighbour_list_frequency; + this->is_mechanical = other.is_mechanical; + this->lambda = other.lambda; + this->atoms = other.atoms; + this->mm1_to_qm = other.mm1_to_qm; + this->mm1_to_mm2 = other.mm1_to_mm2; + this->mm2_atoms = other.mm2_atoms; + this->bond_scale_factors = other.bond_scale_factors; + this->numbers = other.numbers; + this->charges = other.charges; + return *this; +} + +void TorchQMForce::setModulePath(QString module_path) +{ +#ifdef SIRE_USE_TORCH + // Try to load the Torch module. + try + { + torch::jit::getProfilingMode() = false; + this->torch_module = torch::jit::load(module_path.toStdString()); + this->torch_module.eval(); + } + catch (const c10::Error& e) + { + throw SireError::io_error( + QObject::tr( + "Unable to load the TorchScript module '%1'. The error was '%2'.") + .arg(module_path).arg(e.what()), + CODELOC); + } +#endif + + this->module_path = module_path; +} + +QString TorchQMForce::getModulePath() const +{ + return this->module_path; +} + +#ifdef SIRE_USE_TORCH +torch::jit::script::Module TorchQMForce::getTorchModule() const +#else +void* TorchQMForce::getTorchModule() const +#endif +{ + return this->torch_module; +} + +void TorchQMForce::setLambda(double lambda) +{ + // Clamp the lambda value. + if (lambda < 0.0) + { + lambda = 0.0; + } + else if (lambda > 1.0) + { + lambda = 1.0; + } + this->lambda = lambda; +} + +double TorchQMForce::getLambda() const +{ + return this->lambda; +} + +SireUnits::Dimension::Length TorchQMForce::getCutoff() const +{ + return this->cutoff; +} + +int TorchQMForce::getNeighbourListFrequency() const +{ + return this->neighbour_list_frequency; +} + +bool TorchQMForce::getIsMechanical() const +{ + return this->is_mechanical;; +} + +QVector TorchQMForce::getAtoms() const +{ + return this->atoms; +} + +boost::tuple, QMap>, QMap> TorchQMForce::getLinkAtoms() const +{ + return boost::make_tuple(this->mm1_to_qm, this->mm1_to_mm2, this->bond_scale_factors); +} + +QVector TorchQMForce::getMM2Atoms() const +{ + return this->mm2_atoms; +} + +QVector TorchQMForce::getNumbers() const +{ + return this->numbers; +} + +QVector TorchQMForce::getCharges() const +{ + return this->charges; +} + +const char *TorchQMForce::typeName() +{ + return QMetaType::typeName(qMetaTypeId()); +} + +const char *TorchQMForce::what() const +{ + return TorchQMForce::typeName(); +} + +///////// +///////// OpenMM Serialization +///////// + +namespace OpenMM +{ + class TorchQMForceProxy : public SerializationProxy { + public: + TorchQMForceProxy() : SerializationProxy("TorchQMForce") + { + }; + + void serialize(const void* object, SerializationNode& node) const + { + // Serialize the object. + QByteArray data; + QDataStream ds(&data, QIODevice::WriteOnly); + TorchQMForce torchqmforce = *static_cast(object); + ds << torchqmforce; + + // Set the version. + node.setIntProperty("version", 0); + + // Set the note attribute. + node.setStringProperty("note", + "This force only supports partial serialization, so can only be used " + "within the same session and memory space."); + + // Set the data by converting the QByteArray to a hexidecimal string. + node.setStringProperty("data", data.toHex().data()); + }; + + void* deserialize(const SerializationNode& node) const + { + // Check the version. + int version = node.getIntProperty("version"); + if (version != 0) + { + throw OpenMM::OpenMMException("Unsupported version number"); + } + + // Get the data as a std::string. + auto string = node.getStringProperty("data"); + + // Convert to hexidecimal. + auto hex = QByteArray::fromRawData(string.data(), string.size()); + + // Convert to a QByteArray. + auto data = QByteArray::fromHex(hex); + + // Deserialize the object. + QDataStream ds(data); + TorchQMForce torchqmforce; + + try + { + ds >> torchqmforce; + } + catch (...) + { + throw OpenMM::OpenMMException("Unable deserialize TorchQMForce"); + } + + return new TorchQMForce(torchqmforce); + }; + }; + + // Register the TorchQMForce serialization proxy. + extern "C" void registerTorchQMSerializationProxies() { + SerializationProxy::registerProxy(typeid(TorchQMForce), new TorchQMForceProxy()); + } +}; + +///////// +///////// Implementation of TorchQMForceImpl +///////// + +OpenMM::ForceImpl *TorchQMForce::createImpl() const +{ +#if defined(SIRE_USE_CUSTOMCPPFORCE) and defined(SIRE_USE_TORCH) + return new TorchQMForceImpl(*this); +#else + throw SireError::unsupported(QObject::tr( + "Unable to create an TorchQMForceImpl because OpenMM::CustomCPPForceImpl " + "is not available. You need to use OpenMM 8.1 or later."), + CODELOC); + return 0; +#endif +} + +#if defined(SIRE_USE_CUSTOMCPPFORCE) and defined(SIRE_USE_TORCH) +TorchQMForceImpl::TorchQMForceImpl(const TorchQMForce &owner) : + OpenMM::CustomCPPForceImpl(owner), + owner(owner) +{ + this->torch_module = owner.getTorchModule(); +} + +TorchQMForceImpl::~TorchQMForceImpl() +{ +} + +const TorchQMForce &TorchQMForceImpl::getOwner() const +{ + return this->owner; +} + +double TorchQMForceImpl::computeForce( + OpenMM::ContextImpl &context, + const std::vector &positions, + std::vector &forces) +{ + // Get the platform name from the context. + const auto platform = context.getPlatform().getName(); + + // Set the Torch device. + auto device = torch::kCUDA; + if (platform != "CUDA") + { + device = torch::kCPU; + } + + // If this is the first step, then setup information for the neighbour list. + if (this->step_count == 0) + { + // Move the Torch module to the correct device. + this->torch_module.to(device); + + // Store the cutoff as a double in Angstom. + this->cutoff = this->owner.getCutoff().value(); + + // The neighbour list cutoff is 20% larger than the cutoff. + this->neighbour_list_cutoff = 1.2*this->cutoff; + + // Store the neighbour list update frequency. + this->neighbour_list_frequency = this->owner.getNeighbourListFrequency(); + + // Flag whether a neighbour list is used. + this->is_neighbour_list = this->neighbour_list_frequency > 0; + } + + // Get the current box vectors in nanometers. + OpenMM::Vec3 box_x, box_y, box_z; + context.getPeriodicBoxVectors(box_x, box_y, box_z); + + // Create a triclinic space, converting to Angstrom. + TriclinicBox space( + Vector(10*box_x[0], 10*box_x[1], 10*box_x[2]), + Vector(10*box_y[0], 10*box_y[1], 10*box_y[2]), + Vector(10*box_z[0], 10*box_z[1], 10*box_z[2]) + ); + + // Store the QM atomic indices and numbers. + auto qm_atoms = this->owner.getAtoms(); + auto numbers = this->owner.getNumbers(); + + // Store the link atom info. Link atoms are handled using the Charge Shift + // method. See: https://www.ks.uiuc.edu/Research/qmmm + const auto link_atoms = this->owner.getLinkAtoms(); + const auto mm1_to_qm = link_atoms.get<0>(); + const auto mm1_to_mm2 = link_atoms.get<1>(); + const auto bond_scale_factors = link_atoms.get<2>(); + const auto mm2_atoms = this->owner.getMM2Atoms(); + + // Initialise a vector to hold the current positions for the QM atoms. + QVector xyz_qm_vec(qm_atoms.size()); + std::vector xyz_qm(3*qm_atoms.size()); + + // First loop over all QM atoms and store the positions. + int i = 0; + for (const auto &idx : qm_atoms) + { + const auto &pos = positions[idx]; + Vector qm_vec(10*pos[0], 10*pos[1], 10*pos[2]); + xyz_qm_vec[i] = qm_vec; + i++; + } + + // Next sure that the QM atoms are whole (unwrapped). + xyz_qm_vec = space.makeWhole(xyz_qm_vec); + + // Get the center of the QM atoms. We will use this as a reference when + // re-imaging the MM atoms. Also store the QM atoms in the xyz_qm vector. + Vector center; + i = 0; + for (const auto &qm_vec : xyz_qm_vec) + { + xyz_qm[3*i] = qm_vec[0]; + xyz_qm[3*i+1] = qm_vec[1]; + xyz_qm[3*i+2] = qm_vec[2]; + center += qm_vec; + i++; + } + center /= i; + + // Initialise a vector to hold the current positions and charges for the MM atoms. + std::vector xyz_mm; + QVector charges_mm; + + // Initialise a list to hold the indices of the MM atoms. + QVector idx_mm; + + // Store the current number of MM atoms. + unsigned int num_mm = 0; + + // If we are using electrostatic embedding, the work out the MM point charges and + // build the neighbour list. + if (not this->owner.getIsMechanical()) + { + // Initialise a vector to hold the current positions and charges for the virtual + // point charges. + std::vector xyz_virtual; + QVector charges_virtual; + + // Manually work out the MM point charges and build the neigbour list. + if (not this->is_neighbour_list or this->step_count % this->neighbour_list_frequency == 0) + { + // Clear the neighbour list. + if (this->is_neighbour_list) + { + this->neighbour_list.clear(); + } + + i = 0; + // Loop over all of the OpenMM positions. + for (const auto &pos : positions) + { + // Exclude QM atoms or link atoms, which are handled later. + if (not qm_atoms.contains(i) and + not mm1_to_mm2.contains(i) and + not mm2_atoms.contains(i)) + { + // Store the MM atom position in Sire Vector format. + Vector mm_vec(10*pos[0], 10*pos[1], 10*pos[2]); + + // Loop over all of the QM atoms. + for (const auto &qm_vec : xyz_qm_vec) + { + // Work out the distance between the current MM atom and QM atoms. + const auto dist = space.calcDist(mm_vec, qm_vec); + + // The current MM atom is within the neighbour list cutoff. + if (this->is_neighbour_list and dist < this->neighbour_list_cutoff) + { + // Insert the MM atom index into the neighbour list. + this->neighbour_list.insert(i); + } + + // The current MM atom is within the cutoff, add it. + if (dist < cutoff) + { + // Work out the minimum image position with respect to the + // reference position and add to the vector. + mm_vec = space.getMinimumImage(mm_vec, center); + xyz_mm.push_back(mm_vec[0]); + xyz_mm.push_back(mm_vec[1]); + xyz_mm.push_back(mm_vec[2]); + + // Add the charge and index. + charges_mm.append(this->owner.getCharges()[i]); + idx_mm.append(i); + + // Exit the inner loop. + break; + } + } + } + + // Update the atom index. + i++; + } + } + // Use the neighbour list. + else + { + // Loop over the MM atoms in the neighbour list. + for (const auto &idx : this->neighbour_list) + { + // Store the MM atom position in Sire Vector format. + Vector mm_vec(10*positions[idx][0], 10*positions[idx][1], 10*positions[idx][2]); + + // Loop over all of the QM atoms. + for (const auto &qm_vec : xyz_qm_vec) + { + // The current MM atom is within the cutoff, add it. + if (space.calcDist(mm_vec, qm_vec) < cutoff) + { + // Work out the minimum image position with respect to the + // reference position and add to the vector. + mm_vec = space.getMinimumImage(mm_vec, center); + xyz_mm.push_back(mm_vec[0]); + xyz_mm.push_back(mm_vec[1]); + xyz_mm.push_back(mm_vec[2]); + + // Add the charge and index. + charges_mm.append(this->owner.getCharges()[idx]); + idx_mm.append(idx); + + // Exit the inner loop. + break; + } + } + } + } + + // Handle link atoms via the Charge Shift method. + // See: https://www.ks.uiuc.edu/Research/qmmm + for (const auto &idx: mm1_to_mm2.keys()) + { + // Get the QM atom to which the current MM atom is bonded. + const auto qm_idx = mm1_to_qm[idx]; + + // Store the MM1 position in Sire Vector format, along with the + // position of the QM atom to which it is bonded. + Vector mm1_vec(10*positions[idx][0], 10*positions[idx][1], 10*positions[idx][2]); + Vector qm_vec(10*positions[qm_idx][0], 10*positions[qm_idx][1], 10*positions[qm_idx][2]); + + // Work out the minimum image positions with respect to the reference position. + mm1_vec = space.getMinimumImage(mm1_vec, center); + qm_vec = space.getMinimumImage(qm_vec, center); + + // Work out the position of the link atom. Here we use a bond length + // scale factor taken from the MM bond potential, i.e. R0(QM-L) / R0(QM-MM1), + // where R0(QM-L) is the equilibrium bond length for the QM and link (L) + // elements, and R0(QM-MM1) is the equilibrium bond length for the QM + // and MM1 elements. + const auto link_vec = qm_vec + bond_scale_factors[idx]*(mm1_vec - qm_vec); + + // Add to the QM positions. + xyz_qm.push_back(link_vec[0]); + xyz_qm.push_back(link_vec[1]); + xyz_qm.push_back(link_vec[2]); + + // Add the MM1 index to the QM atoms vector. + qm_atoms.append(qm_idx); + + // Append a hydrogen element to the numbers vector. + numbers.append(1); + + // Store the number of MM2 atoms. + const auto num_mm2 = mm1_to_mm2[idx].size(); + + // Store the fractional charge contribution to the MM2 atoms and + // virtual point charges. + const auto frac_charge = this->owner.getCharges()[idx] / num_mm2; + + // Loop over the MM2 atoms and perform charge shifting. Here the MM1 + // charge is redistributed over the MM2 atoms and two virtual point + // charges are added either side of the MM2 atoms in order to preserve + // the MM1-MM2 dipole. + for (const auto& mm2_idx : mm1_to_mm2[idx]) + { + // Store the MM2 position in Sire Vector format. + Vector mm2_vec(10*positions[mm2_idx][0], 10*positions[mm2_idx][1], 10*positions[mm2_idx][2]); + + // Work out the minimum image position with respect to the reference position. + mm2_vec = space.getMinimumImage(mm2_vec, center); + + // Add to the MM positions. + xyz_mm.push_back(mm2_vec[0]); + xyz_mm.push_back(mm2_vec[1]); + xyz_mm.push_back(mm2_vec[2]); + + // Add the charge and index. + charges_mm.append(this->owner.getCharges()[mm2_idx] + frac_charge); + idx_mm.append(mm2_idx); + + // Now add the virtual point charges. + + // Compute the normal vector from the MM1 to MM2 atom. + const auto normal = (mm2_vec - mm1_vec).normalise(); + + // Positive direction. (Away from MM1 atom.) + auto xyz = mm2_vec + VIRTUAL_PC_DELTA*normal; + xyz_virtual.push_back(xyz[0]); + xyz_virtual.push_back(xyz[1]); + xyz_virtual.push_back(xyz[2]); + charges_virtual.append(-frac_charge); + + // Negative direction (Towards MM1 atom.) + xyz = mm2_vec - VIRTUAL_PC_DELTA*normal; + xyz_virtual.push_back(xyz[0]); + xyz_virtual.push_back(xyz[1]); + xyz_virtual.push_back(xyz[2]); + charges_virtual.append(frac_charge); + } + } + + // Store the current number of MM atoms. + num_mm = charges_mm.size(); + + // If there are any virtual point charges, then add to the MM positions + // and charges. + if (xyz_virtual.size() > 0) + { + xyz_mm.reserve(xyz_mm.size() + xyz_virtual.size()); + xyz_mm.insert(xyz_mm.end(), xyz_virtual.begin(), xyz_virtual.end()); + charges_mm.append(charges_virtual); + } + + // Update the maximum number of MM atoms that we've seen. + if (charges_mm.size() > this->max_num_mm) + { + this->max_num_mm = charges_mm.size(); + } + else + { + // Resize the charges and positions vectors to the maximum number of MM atoms. + // This is to try to preserve a static compute graph to avoid re-jitting. + charges_mm.resize(this->max_num_mm); + xyz_mm.resize(3*this->max_num_mm); + } + } + + // Convert input to Torch tensors. + + // MM charges. + torch::Tensor charges_mm_torch = torch::from_blob(charges_mm.data(), {charges_mm.size()}, + torch::TensorOptions().dtype(torch::kFloat64)) + .to(torch::kFloat32).to(device); + + // Atomic numbers. + torch::Tensor atomic_numbers_torch = torch::from_blob(numbers.data(), {numbers.size()}, + torch::TensorOptions().dtype(torch::kInt32)) + .to(torch::kInt64).to(device); + + // QM positions. + torch::Tensor xyz_qm_torch = torch::from_blob(xyz_qm.data(), {numbers.size(), 3}, + torch::TensorOptions().dtype(torch::kFloat32)) + .to(device); + xyz_qm_torch.requires_grad_(true); + + // MM positions. + torch::Tensor xyz_mm_torch = torch::from_blob(xyz_mm.data(), {charges_mm.size(), 3}, + torch::TensorOptions().dtype(torch::kFloat32)) + .to(device); + xyz_mm_torch.requires_grad_(true); + + // Create the input vector. + auto input = std::vector{ + atomic_numbers_torch, + charges_mm_torch, + xyz_qm_torch, + xyz_mm_torch + }; + + // Compute the energies. + auto energies = this->torch_module.forward(input).toTensor(); + + // Store the sum of the energy in kJ. + const auto energy = energies.sum().item() * HARTREE_TO_KJ_MOL; + + // If there are no MM atoms, then we need to allow unused tensors. + bool allow_unused = num_mm == 0; + + // Compute the gradients. + const auto gradients = torch::autograd::grad( + {energies.sum()}, {xyz_qm_torch, xyz_mm_torch}, {}, c10::nullopt, false, allow_unused); + + // Compute the forces, converting from Hatree/Anstrom to kJ/mol/nm. + const auto forces_qm = -(gradients[0] * HARTREE_TO_KJ_MOL * 10).detach().cpu(); + torch::Tensor forces_mm; + if (num_mm > 0) + { + forces_mm = -(gradients[1] * HARTREE_TO_KJ_MOL * 10).detach().cpu(); + } + else + { + forces_mm = torch::zeros({0, 3}, torch::TensorOptions().dtype(torch::kFloat32)); + } + + // The current interpolation (weighting) parameter. + double lambda; + + // Try to get the "lambda_emle" global parameter from the context. + try + { + lambda = context.getParameter("lambda_emle"); + } + catch (...) + { + // Try to get the "lambda_interpolate" global parameter from the context. + try + { + lambda = context.getParameter("lambda_interpolate"); + } + // Fall back on the lambda value stored in the TorchQMForce object. + catch (...) + { + lambda = this->owner.getLambda(); + } + } + + // Clamp the lambda value. + if (lambda < 0.0) + { + lambda = 0.0; + } + else if (lambda > 1.0) + { + lambda = 1.0; + } + + // Now update the force vector. + + // Flatten the forces to std::vector. + std::vector forces_qm_flat( + forces_qm.data_ptr(), + forces_qm.data_ptr() + forces_qm.numel()); + std::vector forces_mm_flat( + forces_mm.data_ptr(), + forces_mm.data_ptr() + forces_mm.numel()); + + // First the QM atoms. + for (int i=0; istep_count++; + + // Finally, return the energy. + return lambda * energy; +} +#endif + +///////// +///////// Implementation of TorchQMEngine +///////// + +TorchQMEngine::TorchQMEngine() : ConcreteProperty() +{ + // Register the serialization proxies. + OpenMM::registerTorchQMSerializationProxies(); +} + +TorchQMEngine::TorchQMEngine( + QString module_path, + SireUnits::Dimension::Length cutoff, + int neighbour_list_frequency, + bool is_mechanical, + double lambda) : + ConcreteProperty(), + module_path(module_path), + cutoff(cutoff), + neighbour_list_frequency(neighbour_list_frequency), + is_mechanical(is_mechanical), + lambda(lambda) +{ +#ifndef SIRE_USE_TORCH + throw SireError::unsupported(QObject::tr( + "Unable to create an TorchQMEngine because Sire has been compiled " + "without Torch support."), + CODELOC); +#endif + + // Register the serialization proxies. + OpenMM::registerTorchQMSerializationProxies(); + + if (this->neighbour_list_frequency < 0) + { + neighbour_list_frequency = 0; + } + if (this->lambda < 0.0) + { + this->lambda = 0.0; + } + else if (this->lambda > 1.0) + { + this->lambda = 1.0; + } +} + +TorchQMEngine::TorchQMEngine(const TorchQMEngine &other) : + module_path(other.module_path), + cutoff(other.cutoff), + neighbour_list_frequency(other.neighbour_list_frequency), + is_mechanical(other.is_mechanical), + lambda(other.lambda), + atoms(other.atoms), + mm1_to_qm(other.mm1_to_qm), + mm1_to_mm2(other.mm1_to_mm2), + mm2_atoms(other.mm2_atoms), + bond_scale_factors(other.bond_scale_factors), + numbers(other.numbers), + charges(other.charges) +{ +} + +TorchQMEngine &TorchQMEngine::operator=(const TorchQMEngine &other) +{ + this->module_path = other.module_path; + this->cutoff = other.cutoff; + this->neighbour_list_frequency = other.neighbour_list_frequency; + this->is_mechanical = other.is_mechanical; + this->lambda = other.lambda; + this->atoms = other.atoms; + this->mm1_to_qm = other.mm1_to_qm; + this->mm1_to_mm2 = other.mm1_to_mm2; + this->mm2_atoms = other.mm2_atoms; + this->bond_scale_factors = other.bond_scale_factors; + this->numbers = other.numbers; + this->charges = other.charges; + return *this; +} + +void TorchQMEngine::setModulePath(QString module_path) +{ + this->module_path = module_path; +} + +QString TorchQMEngine::getModulePath() const +{ + return this->module_path; +} + +void TorchQMEngine::setLambda(double lambda) +{ + // Clamp the lambda value. + if (lambda < 0.0) + { + lambda = 0.0; + } + else if (lambda > 1.0) + { + lambda = 1.0; + } + this->lambda = lambda; +} + +double TorchQMEngine::getLambda() const +{ + return this->lambda; +} + +void TorchQMEngine::setCutoff(SireUnits::Dimension::Length cutoff) +{ + this->cutoff = cutoff; +} + +SireUnits::Dimension::Length TorchQMEngine::getCutoff() const +{ + return this->cutoff; +} + +int TorchQMEngine::getNeighbourListFrequency() const +{ + return this->neighbour_list_frequency; +} + +void TorchQMEngine::setNeighbourListFrequency(int neighbour_list_frequency) +{ + // Assume anything less than zero means no neighbour list. + if (neighbour_list_frequency < 0) + { + neighbour_list_frequency = 0; + } + this->neighbour_list_frequency = neighbour_list_frequency; +} + +bool TorchQMEngine::getIsMechanical() const +{ + return this->is_mechanical; +} + +void TorchQMEngine::setIsMechanical(bool is_mechanical) +{ + this->is_mechanical = is_mechanical; +} + +QVector TorchQMEngine::getAtoms() const +{ + return this->atoms; +} + +void TorchQMEngine::setAtoms(QVector atoms) +{ + this->atoms = atoms; +} + +boost::tuple, QMap>, QMap> TorchQMEngine::getLinkAtoms() const +{ + return boost::make_tuple(this->mm1_to_qm, this->mm1_to_mm2, this->bond_scale_factors); +} + +void TorchQMEngine::setLinkAtoms( + QMap mm1_to_qm, + QMap> mm1_to_mm2, + QMap bond_scale_factors) +{ + this->mm1_to_qm = mm1_to_qm; + this->mm1_to_mm2 = mm1_to_mm2; + this->bond_scale_factors = bond_scale_factors; + + // Build a vector of all of the MM2 atoms. + this->mm2_atoms.clear(); + for (const auto &mm2 : this->mm1_to_mm2.values()) + { + this->mm2_atoms.append(mm2); + } +} + +QVector TorchQMEngine::getMM2Atoms() const +{ + return this->mm2_atoms; +} + +QVector TorchQMEngine::getNumbers() const +{ + return this->numbers; +} + +void TorchQMEngine::setNumbers(QVector numbers) +{ + this->numbers = numbers; +} + +QVector TorchQMEngine::getCharges() const +{ + return this->charges; +} + +void TorchQMEngine::setCharges(QVector charges) +{ + this->charges = charges; +} + +const char *TorchQMEngine::typeName() +{ + return QMetaType::typeName(qMetaTypeId()); +} + +const char *TorchQMEngine::what() const +{ + return TorchQMEngine::typeName(); +} + +QMForce* TorchQMEngine::createForce() const +{ + return new TorchQMForce( + this->module_path, + this->cutoff, + this->neighbour_list_frequency, + this->is_mechanical, + this->lambda, + this->atoms, + this->mm1_to_qm, + this->mm1_to_mm2, + this->bond_scale_factors, + this->mm2_atoms, + this->numbers, + this->charges + ); +} diff --git a/wrapper/Convert/SireOpenMM/torchqm.h b/wrapper/Convert/SireOpenMM/torchqm.h new file mode 100644 index 000000000..839ecf071 --- /dev/null +++ b/wrapper/Convert/SireOpenMM/torchqm.h @@ -0,0 +1,516 @@ +/********************************************\ + * + * Sire - Molecular Simulation Framework + * + * Copyright (C) 2023 Christopher Woods + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + * For full details of the license please see the COPYING file + * that should have come with this distribution. + * + * You can contact the authors via the website + * at https://sire.openbiosim.org + * +\*********************************************/ + +#ifndef SIREOPENMM_TORCHQM_H +#define SIREOPENMM_TORCHQM_H + +#include "OpenMM.h" +#include "openmm/Force.h" +#ifdef SIRE_USE_CUSTOMCPPFORCE +#include "openmm/internal/ContextImpl.h" +#include "openmm/internal/CustomCPPForceImpl.h" +#endif + +#include "boost/python.hpp" +#include + +#ifdef SIRE_USE_TORCH +#include +#endif + +#include +#include + +#include "sireglobal.h" + +#include "SireUnits/dimensions.h" +#include "SireUnits/units.h" + +#include "qmmm.h" + +namespace bp = boost::python; + +SIRE_BEGIN_HEADER + +namespace SireOpenMM +{ + class TorchQMForce; +} + +QDataStream &operator<<(QDataStream &, const SireOpenMM::TorchQMForce &); +QDataStream &operator>>(QDataStream &, SireOpenMM::TorchQMForce &); + +namespace SireOpenMM +{ + class TorchQMForce : public QMForce + { + friend QDataStream & ::operator<<(QDataStream &, const TorchQMForce &); + friend QDataStream & ::operator>>(QDataStream &, TorchQMForce &); + + public: + //! Default constructor. + TorchQMForce(); + + //! Constructor. + /* \param module_path + The path to the serialised TorchScript module. + + \param torch_module + The TorchScript module. + + \param cutoff + The ML cutoff distance. + + \param neighbour_list_frequency + The frequency at which the neighbour list is updated. (Number of steps.) + If zero, then no neighbour list is used. + + \param is_mechanical + A flag to indicate if mechanical embedding is being used. + + \param lambda + The lambda weighting factor. This can be used to interpolate between + potentials for end-state correction calculations. + + \param atoms + A vector of atom indices for the QM region. + + \param mm1_to_qm + A dictionary mapping link atom (MM1) indices to the QM atoms to + which they are bonded. + + \param mm1_to_mm2 + A dictionary of link atoms indices (MM1) to a list of the MM + atoms to which they are bonded (MM2). + + \param bond_scale_factors + A dictionary of link atom indices (MM1) to a list of the bond + length scale factors between the QM and MM1 atoms. The scale + factors are the ratio of the equilibrium bond lengths for the + QM-L (QM-link) atom and QM-MM1 atom, i.e. R0(QM-L) / R0(QM-MM1), + taken from the MM force field parameters for the molecule. + + \param mm2_atoms + A vector of MM2 atom indices. + + \param numbers + A vector of atomic charges for all atoms in the system. + + \param charges + A vector of atomic charges for all atoms in the system. + */ + TorchQMForce( + QString module_path, + SireUnits::Dimension::Length cutoff, + int neighbour_list_frequency, + bool is_mechanical, + double lambda, + QVector atoms, + QMap mm1_to_qm, + QMap> mm1_to_mm2, + QMap bond_scale_factors, + QVector mm2_atoms, + QVector numbers, + QVector charges + ); + + //! Copy constructor. + TorchQMForce(const TorchQMForce &other); + + //! Assignment operator. + TorchQMForce &operator=(const TorchQMForce &other); + + //! Get the path to the serialised TorchScript module. + /*! \returns + The path to the serialised TorchScript module. + */ + QString getModulePath() const; + + //! Set the path to the serialised TorchScript module. + /*! \param module_path + The path to the serialised TorchScript module. + */ + void setModulePath(QString module_path); + + //! Get the TorchScript module. + /*! \returns + The TorchScript module. + */ +#ifdef SIRE_USE_TORCH + torch::jit::script::Module getTorchModule() const; +#else + void* getTorchModule() const; +#endif + + //! Get the lambda weighting factor. + /*! \returns + The lambda weighting factor. + */ + double getLambda() const; + + //! Set the lambda weighting factor + /* \param lambda + The lambda weighting factor. + */ + void setLambda(double lambda); + + //! Get the QM cutoff distance. + /*! \returns + The QM cutoff distance. + */ + SireUnits::Dimension::Length getCutoff() const; + + //! Get the neighbour list frequency. + /*! \returns + The neighbour list frequency. + */ + int getNeighbourListFrequency() const; + + //! Get the mechanical embedding flag. + /*! \returns + A flag to indicate if mechanical embedding is being used. + */ + bool getIsMechanical() const; + + //! Get the indices of the atoms in the QM region. + /*! \returns + A vector of atom indices for the QM region. + */ + QVector getAtoms() const; + + //! Get the link atoms associated with each QM atom. + /*! \returns + A tuple containing: + + mm1_to_qm + A dictionary mapping link atom (MM1) indices to the QM atoms to + which they are bonded. + + mm1_to_mm2 + A dictionary of link atoms indices (MM1) to a list of the MM + atoms to which they are bonded (MM2). + + bond_scale_factors + A dictionary of link atom indices (MM1) to a list of the bond + length scale factors between the QM and MM1 atoms. The scale + factors are the ratio of the equilibrium bond lengths for the + QM-L (QM-link) atom and QM-MM1 atom, i.e. R0(QM-L) / R0(QM-MM1), + taken from the MM force field parameters for the molecule. + + */ + boost::tuple, QMap>, QMap> getLinkAtoms() const; + + //! Get the vector of MM2 atoms. + /*! \returns + A vector of MM2 atom indices. + */ + QVector getMM2Atoms() const; + + //! Get the atomic numbers for the atoms in the QM region. + /*! \returns + A vector of atomic numbers for the atoms in the QM region. + */ + QVector getNumbers() const; + + //! Get the atomic charges of all atoms in the system. + /*! \returns + A vector of atomic charges for all atoms in the system. + */ + QVector getCharges() const; + + //! Return the C++ name for this class. + static const char *typeName(); + + //! Return the C++ name for this class. + const char *what() const; + + protected: + OpenMM::ForceImpl *createImpl() const; + + private: + QString module_path; +#ifdef SIRE_USE_TORCH + torch::jit::script::Module torch_module; +#else + void *torch_module; +#endif + SireUnits::Dimension::Length cutoff; + int neighbour_list_frequency; + bool is_mechanical; + double lambda; + QVector atoms; + QMap mm1_to_qm; + QMap> mm1_to_mm2; + QMap bond_scale_factors; + QVector mm2_atoms; + QVector numbers; + QVector charges; + }; + +#if defined(SIRE_USE_CUSTOMCPPFORCE) && defined(SIRE_USE_TORCH) + class TorchQMForceImpl : public OpenMM::CustomCPPForceImpl + { + public: + TorchQMForceImpl(const TorchQMForce &owner); + + ~TorchQMForceImpl(); + + double computeForce(OpenMM::ContextImpl &context, + const std::vector &positions, + std::vector &forces); + + const TorchQMForce &getOwner() const; + + private: + const TorchQMForce &owner; + torch::jit::script::Module torch_module; + unsigned long long step_count=0; + double cutoff; + bool is_neighbour_list; + int neighbour_list_frequency; + double neighbour_list_cutoff; + QSet neighbour_list; + int max_num_mm=0; + }; +#endif + + class TorchQMEngine : public SireBase::ConcreteProperty + { + public: + //! Default constructor. + TorchQMEngine(); + + //! Constructor + /*! \param module_path + The path to the serialised TorchScript module. + + \param cutoff + The ML cutoff distance. + + \param neighbour_list_frequency + The frequency at which the neighbour list is updated. (Number of steps.) + If zero, then no neighbour list is used. + + \param is_mechanical + A flag to indicate if mechanical embedding is being used. + + \param lambda + The lambda weighting factor. This can be used to interpolate between + potentials for end-state correction calculations. + */ + TorchQMEngine( + QString module_path, + SireUnits::Dimension::Length cutoff=7.5*SireUnits::angstrom, + int neighbour_list_frequency=0, + bool is_mechanical=false, + double lambda=1.0 + ); + + //! Copy constructor. + TorchQMEngine(const TorchQMEngine &other); + + //! Assignment operator. + TorchQMEngine &operator=(const TorchQMEngine &other); + + //! Get the path to the serialised TorchScript module. + /*! \returns + The path to the serialised TorchScript module. + */ + QString getModulePath() const; + + //! Set the path to the serialised TorchScript module. + /*! \param module_path + The path to the serialised TorchScript module. + */ + void setModulePath(QString module_path); + + //! Get the lambda weighting factor. + /*! \returns + The lambda weighting factor. + */ + double getLambda() const; + + //! Set the lambda weighting factor. + /*! \param lambda + The lambda weighting factor. + */ + void setLambda(double lambda); + + //! Get the QM cutoff distance. + /*! \returns + The QM cutoff distance. + */ + SireUnits::Dimension::Length getCutoff() const; + + //! Set the QM cutoff distance. + /*! \param cutoff + The QM cutoff distance. + */ + void setCutoff(SireUnits::Dimension::Length cutoff); + + //! Get the neighbour list frequency. + /*! \returns + The neighbour list frequency. + */ + int getNeighbourListFrequency() const; + + //! Set the neighbour list frequency. + /*! \param neighbour_list_frequency + The neighbour list frequency. + */ + void setNeighbourListFrequency(int neighbour_list_frequency); + + //! Get the mechanical embedding flag. + /*! \returns + A flag to indicate if mechanical embedding is being used. + */ + bool getIsMechanical() const; + + //! Set the mechanical embedding flag. + /*! \param is_mechanical + A flag to indicate if mechanical embedding is being used. + */ + void setIsMechanical(bool is_mechanical); + + //! Get the indices of the atoms in the QM region. + /*! \returns + A vector of atom indices for the QM region. + */ + QVector getAtoms() const; + + //! Set the list of atom indices for the QM region. + /*! \param atoms + A vector of atom indices for the QM region. + */ + void setAtoms(QVector atoms); + + //! Get the link atoms associated with each QM atom. + /*! \returns + A tuple containing: + + mm1_to_qm + A dictionary mapping link atom (MM1) indices to the QM atoms to + which they are bonded. + + mm1_to_mm2 + A dictionary of link atoms indices (MM1) to a list of the MM + atoms to which they are bonded (MM2). + + bond_scale_factors + A dictionary of link atom indices (MM1) to a list of the bond + length scale factors between the QM and MM1 atoms. The scale + factors are the ratio of the equilibrium bond lengths for the + QM-L (QM-link) atom and QM-MM1 atom, i.e. R0(QM-L) / R0(QM-MM1), + taken from the MM force field parameters for the molecule. + + */ + boost::tuple, QMap>, QMap> getLinkAtoms() const; + + //! Set the link atoms associated with each QM atom. + /*! \param mm1_to_qm + A dictionary mapping link atom (MM1) indices to the QM atoms to + which they are bonded. + + \param mm1_to_mm2 + A dictionary of link atoms indices (MM1) to a list of the MM + atoms to which they are bonded (MM2). + + \param bond_scale_factors + A dictionary of link atom indices (MM1) to a list of the bond + length scale factors between the QM and MM1 atoms. The scale + factors are the ratio of the equilibrium bond lengths for the + QM-L (QM-link) atom and QM-MM1 atom, i.e. R0(QM-L) / R0(QM-MM1), + taken from the MM force field parameters for the molecule. + + */ + void setLinkAtoms(QMap mm1_to_qm, QMap> mm1_to_mm2, QMap bond_scale_factors); + + //! Get the vector of MM2 atoms. + /*! \returns + A vector of MM2 atom indices. + */ + QVector getMM2Atoms() const; + + //! Get the atomic numbers for the atoms in the QM region. + /*! \returns + A vector of atomic numbers for the atoms in the QM region. + */ + QVector getNumbers() const; + + //! Set the atomic numbers for the atoms in the QM region. + /*! \param numbers + A vector of atomic numbers for the atoms in the QM region. + */ + void setNumbers(QVector numbers); + + //! Get the atomic charges of all atoms in the system. + /*! \returns + A vector of atomic charges for all atoms in the system. + */ + QVector getCharges() const; + + //! Set the atomic charges of all atoms in the system. + /*! \param charges + A vector of atomic charges for all atoms in the system. + */ + void setCharges(QVector charges); + + //! Return the C++ name for this class. + static const char *typeName(); + + //! Return the C++ name for this class. + const char *what() const; + + //! Create an EMLE force object. + QMForce* createForce() const; + + private: + QString module_path; + SireUnits::Dimension::Length cutoff; + int neighbour_list_frequency; + bool is_mechanical; + double lambda; + QVector atoms; + QMap mm1_to_qm; + QMap> mm1_to_mm2; + QMap bond_scale_factors; + QVector mm2_atoms; + QVector numbers; + QVector charges; + }; +} + +Q_DECLARE_METATYPE(SireOpenMM::TorchQMForce) +Q_DECLARE_METATYPE(SireOpenMM::TorchQMEngine) + +SIRE_EXPOSE_CLASS(SireOpenMM::TorchQMForce) +SIRE_EXPOSE_CLASS(SireOpenMM::TorchQMEngine) + +SIRE_END_HEADER + +#endif diff --git a/wrapper/Convert/__init__.py b/wrapper/Convert/__init__.py index cf71f3f31..a481b4b86 100644 --- a/wrapper/Convert/__init__.py +++ b/wrapper/Convert/__init__.py @@ -18,6 +18,10 @@ "PerturbableOpenMMMolecule", "OpenMMMetaData", "SOMMContext", + "QMEngine", + "PyQMCallback", + "PyQMEngine", + "TorchQMEngine", ] try: @@ -92,12 +96,29 @@ def smarts_to_rdkit(*args, **kwargs): _get_lever_values, ) - from ._SireOpenMM import LambdaLever, PerturbableOpenMMMolecule, OpenMMMetaData + from ._SireOpenMM import ( + LambdaLever, + PerturbableOpenMMMolecule, + OpenMMMetaData, + QMEngine, + PyQMCallback, + PyQMEngine, + TorchQMEngine, + ) from ..._pythonize import _pythonize _pythonize( - [LambdaLever, PerturbableOpenMMMolecule, OpenMMMetaData], delete_old=True + [ + LambdaLever, + PerturbableOpenMMMolecule, + OpenMMMetaData, + QMEngine, + PyQMCallback, + PyQMEngine, + TorchQMEngine, + ], + delete_old=True, ) PerturbableOpenMMMolecule.changed_atoms = _changed_atoms @@ -492,6 +513,7 @@ def minimise_openmm_context( starting_k: float = 100.0, ratchet_scale: float = 2.0, max_constraint_error: float = 0.01, + timeout: str = "300s", ): return _minimise_openmm_context( context, @@ -503,6 +525,7 @@ def minimise_openmm_context( starting_k=starting_k, ratchet_scale=ratchet_scale, max_constraint_error=max_constraint_error, + timeout=timeout, ) except Exception as e: diff --git a/wrapper/IO/_IO_free_functions.pypp.cpp b/wrapper/IO/_IO_free_functions.pypp.cpp index 4787605a1..fa5466500 100644 --- a/wrapper/IO/_IO_free_functions.pypp.cpp +++ b/wrapper/IO/_IO_free_functions.pypp.cpp @@ -7,6 +7,70 @@ namespace bp = boost::python; +#include "SireBase/getinstalldir.h" + +#include "SireError/errors.h" + +#include "SireMol/atomelements.h" + +#include "SireMol/atommasses.h" + +#include "SireMol/connectivity.h" + +#include "SireMol/core.h" + +#include "SireMol/mgname.h" + +#include "SireMol/moleditor.h" + +#include "SireMol/molidx.h" + +#include "SireSystem/system.h" + +#include "SireUnits/units.h" + +#include "SireVol/periodicbox.h" + +#include "SireVol/triclinicbox.h" + +#include "biosimspace.h" + +#include "moleculeparser.h" + +#include "biosimspace.h" + +#include "SireBase/getinstalldir.h" + +#include "SireError/errors.h" + +#include "SireMol/atomelements.h" + +#include "SireMol/atommasses.h" + +#include "SireMol/connectivity.h" + +#include "SireMol/core.h" + +#include "SireMol/mgname.h" + +#include "SireMol/moleditor.h" + +#include "SireMol/molidx.h" + +#include "SireSystem/system.h" + +#include "SireUnits/units.h" + +#include "SireVol/periodicbox.h" + +#include "SireVol/triclinicbox.h" + +#include "biosimspace.h" + +#include "moleculeparser.h" + +#include "biosimspace.h" + #include "SireBase/parallel.h" #include "SireMol/core.h" @@ -493,6 +557,32 @@ namespace bp = boost::python; void register_free_functions(){ + { //::SireIO::createChlorineIon + + typedef ::SireMol::Molecule ( *createChlorineIon_function_type )( ::SireMaths::Vector const &,::QString const,::SireBase::PropertyMap const & ); + createChlorineIon_function_type createChlorineIon_function_value( &::SireIO::createChlorineIon ); + + bp::def( + "createChlorineIon" + , createChlorineIon_function_value + , ( bp::arg("coords"), bp::arg("model"), bp::arg("map")=SireBase::PropertyMap() ) + , "Create a chlorine ion at the specified position.\nPar:am position\nThe position of the chlorine ion.\n\nPar:am model\nThe name of the water model.\n\nPar:am map\nA dictionary of user-defined molecular property names.\n\nRetval: chlorine\nThe chlorine ion.\n" ); + + } + + { //::SireIO::createSodiumIon + + typedef ::SireMol::Molecule ( *createSodiumIon_function_type )( ::SireMaths::Vector const &,::QString const,::SireBase::PropertyMap const & ); + createSodiumIon_function_type createSodiumIon_function_value( &::SireIO::createSodiumIon ); + + bp::def( + "createSodiumIon" + , createSodiumIon_function_value + , ( bp::arg("coords"), bp::arg("model"), bp::arg("map")=SireBase::PropertyMap() ) + , "Create a sodium ion at the specified position.\nPar:am position\nThe position of the sodium ion.\n\nPar:am model\nThe name of the water model.\n\nPar:am map\nA dictionary of user-defined molecular property names.\n\nRetval: sodium\nThe sodium ion.\n" ); + + } + { //::SireIO::getCoordsArray typedef ::QVector< float > ( *getCoordsArray_function_type )( ::SireMol::MoleculeView const &,::SireUnits::Dimension::Length const &,::SireBase::PropertyMap const & ); diff --git a/wrapper/Move/CMakeNoOpenMM.txt b/wrapper/Move/CMakeNoOpenMM.txt index 833386a2a..1f5c40eac 100644 --- a/wrapper/Move/CMakeNoOpenMM.txt +++ b/wrapper/Move/CMakeNoOpenMM.txt @@ -3,4 +3,5 @@ set ( SIRE_OPENMM_WRAPPERS NoOpenMM/OpenMMFrEnergyST.pypp.cpp NoOpenMM/OpenMMFrEnergyDT.pypp.cpp NoOpenMM/OpenMMMDIntegrator.pypp.cpp + NoOpenMM/OpenMMPMEFEP.pypp.cpp ) diff --git a/wrapper/Move/CMakeNoOpenMMFile.txt b/wrapper/Move/CMakeNoOpenMMFile.txt index 11f644756..c4fd3a780 100644 --- a/wrapper/Move/CMakeNoOpenMMFile.txt +++ b/wrapper/Move/CMakeNoOpenMMFile.txt @@ -3,6 +3,7 @@ set ( PYPP_OPENMM_SOURCES NoOpenMM/OpenMMFrEnergyST.pypp.cpp NoOpenMM/OpenMMFrEnergyDT.pypp.cpp NoOpenMM/OpenMMMDIntegrator.pypp.cpp + NoOpenMM/OpenMMPMEFEP.pypp.cpp ) set( SIRE_OPENMM_LIBRARIES "" ) diff --git a/wrapper/Move/CMakeOpenMM.txt b/wrapper/Move/CMakeOpenMM.txt index a4dc0fd98..5d72e5a88 100644 --- a/wrapper/Move/CMakeOpenMM.txt +++ b/wrapper/Move/CMakeOpenMM.txt @@ -3,4 +3,5 @@ set ( SIRE_OPENMM_WRAPPERS OpenMMFrEnergyST.pypp.cpp OpenMMFrEnergyDT.pypp.cpp OpenMMMDIntegrator.pypp.cpp + OpenMMPMEFEP.pypp.cpp ) diff --git a/wrapper/Move/CMakeOpenMMFile.txt b/wrapper/Move/CMakeOpenMMFile.txt index cb3d0ff95..00cb8fd84 100644 --- a/wrapper/Move/CMakeOpenMMFile.txt +++ b/wrapper/Move/CMakeOpenMMFile.txt @@ -3,4 +3,5 @@ set ( PYPP_OPENMM_SOURCES OpenMMMDIntegrator.pypp.cpp OpenMMFrEnergyDT.pypp.cpp OpenMMFrEnergyST.pypp.cpp + OpenMMPMEFEP.pypp.cpp ) diff --git a/wrapper/Move/NoOpenMM/OpenMMPMEFEP.pycpp.cpp b/wrapper/Move/NoOpenMM/OpenMMPMEFEP.pycpp.cpp new file mode 100644 index 000000000..864d0bb09 --- /dev/null +++ b/wrapper/Move/NoOpenMM/OpenMMPMEFEP.pycpp.cpp @@ -0,0 +1,108 @@ +// This file has been generated by Py++. + +// (C) Christopher Woods, GPL >= 3 License + +#include "boost/python.hpp" +#include "OpenMMPMEFEP.pypp.hpp" + +namespace bp = boost::python; + +#include "SireFF/forcetable.h" + +#include "SireIO/amber.h" + +#include "SireMM/atomljs.h" + +#include "SireMM/internalff.h" + +#include "SireMM/internalperturbation.h" + +#include "SireMaths/constants.h" + +#include "SireMaths/rangenerator.h" + +#include "SireMaths/vector.h" + +#include "SireMol/amberparameters.h" + +#include "SireMol/atomcharges.h" + +#include "SireMol/atomcoords.h" + +#include "SireMol/atommasses.h" + +#include "SireMol/bondid.h" + +#include "SireMol/connectivity.h" + +#include "SireMol/mgname.h" + +#include "SireMol/molecule.h" + +#include "SireMol/core.h" + +#include "SireMol/moleculegroup.h" + +#include "SireMol/moleditor.h" + +#include "SireMol/partialmolecule.h" + +#include "SireMol/perturbation.h" + +#include "SireMove/flexibility.h" + +#include "SireStream/datastream.h" + +#include "SireStream/shareddatastream.h" + +#include "SireSystem/system.h" + +#include "SireUnits/convert.h" + +#include "SireUnits/temperature.h" + +#include "SireUnits/units.h" + +#include "SireVol/periodicbox.h" + +#include "ensemble.h" + +#include "openmmfrenergyst.h" + +#include + +#include + +#include + +#include "openmmfrenergyst.h" + +SireMove::OpenMMPMEFEP __copy__(const SireMove::OpenMMPMEFEP &other){ return SireMove::OpenMMPMEFEP(other); } + +const char* pvt_get_name(const SireMove::OpenMMPMEFEP&){ return "SireMove::OpenMMPMEFEP";} + +void register_OpenMMPMEFEP_class(){ + + { //::SireMove::OpenMMPMEFEP + typedef bp::class_< SireMove::OpenMMPMEFEP > OpenMMPMEFEP_exposer_t; + OpenMMPMEFEP_exposer_t OpenMMPMEFEP_exposer = OpenMMPMEFEP_exposer_t( "OpenMMPMEFEP", bp::init< >() ); + bp::scope OpenMMPMEFEP_scope( OpenMMPMEFEP_exposer ); + { //::SireMove::OpenMMPMEFEP::typeName + + typedef char const * ( *typeName_function_type )( ); + typeName_function_type typeName_function_value( &::SireMove::OpenMMPMEFEP::typeName ); + + OpenMMPMEFEP_exposer.def( + "typeName" + , typeName_function_value ); + + } + OpenMMPMEFEP_exposer.staticmethod( "typeName" ); + OpenMMPMEFEP_exposer.def( "__copy__", &__copy__); + OpenMMPMEFEP_exposer.def( "__deepcopy__", &__copy__); + OpenMMPMEFEP_exposer.def( "clone", &__copy__); + OpenMMPMEFEP_exposer.def( "__str__", &pvt_get_name); + OpenMMPMEFEP_exposer.def( "__repr__", &pvt_get_name); + } + +} diff --git a/wrapper/Move/NoOpenMM/OpenMMPMEFEP.pycpp.hpp b/wrapper/Move/NoOpenMM/OpenMMPMEFEP.pycpp.hpp new file mode 100644 index 000000000..3b3ee9cdc --- /dev/null +++ b/wrapper/Move/NoOpenMM/OpenMMPMEFEP.pycpp.hpp @@ -0,0 +1,10 @@ +// This file has been generated by Py++. + +// (C) Christopher Woods, GPL >= 3 License + +#ifndef OpenMMPMEFEP_hpp__pyplusplus_wrapper +#define OpenMMPMEFEP_hpp__pyplusplus_wrapper + +void register_OpenMMPMEFEP_class(); + +#endif//OpenMMPMEFEP_hpp__pyplusplus_wrapper diff --git a/wrapper/Tools/OpenMMMD.py b/wrapper/Tools/OpenMMMD.py index 6114c80f3..45f7017ee 100644 --- a/wrapper/Tools/OpenMMMD.py +++ b/wrapper/Tools/OpenMMMD.py @@ -2505,6 +2505,7 @@ def selectWatersForPerturbation(system, charge_diff): # FIXME: select waters according to distance criterion # if mol.residue().name() == water_resname and cnt < nions: if mol.residues()[0].name() == water_resname and cnt < nions: + print ("Selected water residue %s for perturbation into ion" % (mol.residues()[0])) cnt += 1 perturbed_water = mol.edit()