From 05e38a9d45d402e071eb6e0b4779b897519bd954 Mon Sep 17 00:00:00 2001 From: Audrius Kalpokas Date: Wed, 10 Jul 2024 11:14:00 +0100 Subject: [PATCH 001/102] Initial update for angle restraints --- corelib/src/libs/SireMM/anglerestraint.cpp | 1108 ++++++++++++++------ corelib/src/libs/SireMM/anglerestraint.h | 155 +-- 2 files changed, 863 insertions(+), 400 deletions(-) diff --git a/corelib/src/libs/SireMM/anglerestraint.cpp b/corelib/src/libs/SireMM/anglerestraint.cpp index a765049c0..06e6d536e 100644 --- a/corelib/src/libs/SireMM/anglerestraint.cpp +++ b/corelib/src/libs/SireMM/anglerestraint.cpp @@ -27,16 +27,16 @@ #include "anglerestraint.h" -#include "SireFF/forcetable.h" +// #include "SireFF/forcetable.h" -#include "SireCAS/conditional.h" -#include "SireCAS/power.h" -#include "SireCAS/symbols.h" -#include "SireCAS/values.h" +// #include "SireCAS/conditional.h" +// #include "SireCAS/power.h" +// #include "SireCAS/symbols.h" +// #include "SireCAS/values.h" #include "SireID/index.h" -#include "SireUnits/angle.h" +// #include "SireUnits/angle.h" #include "SireUnits/units.h" #include "SireStream/datastream.h" @@ -44,12 +44,14 @@ #include "SireCAS/errors.h" +#include + using namespace SireMM; -using namespace SireMol; -using namespace SireFF; +// using namespace SireMol; +// using namespace SireFF; using namespace SireID; using namespace SireBase; -using namespace SireCAS; +// using namespace SireCAS; using namespace SireMaths; using namespace SireStream; using namespace SireUnits; @@ -62,14 +64,14 @@ using namespace SireUnits::Dimension; static const RegisterMetaType r_angrest; /** Serialise to a binary datastream */ + QDataStream &operator<<(QDataStream &ds, const AngleRestraint &angrest) { writeHeader(ds, r_angrest, 1); SharedDataStream sds(ds); - sds << angrest.p[0] << angrest.p[1] << angrest.p[2] << angrest.force_expression - << static_cast(angrest); + sds << angrest.atms << angrest._theta0 << angrest._ktheta; return ds; } @@ -83,11 +85,7 @@ QDataStream &operator>>(QDataStream &ds, AngleRestraint &angrest) { SharedDataStream sds(ds); - sds >> angrest.p[0] >> angrest.p[1] >> angrest.p[2] >> angrest.force_expression >> - static_cast(angrest); - - angrest.intra_molecule_points = Point::areIntraMoleculePoints(angrest.p[0], angrest.p[1]) and - Point::areIntraMoleculePoints(angrest.p[0], angrest.p[2]); + sds >> angrest.atms >> angrest._theta0 >> angrest._ktheta; } else throw version_error(v, "1", r_angrest, CODELOC); @@ -95,461 +93,903 @@ QDataStream &operator>>(QDataStream &ds, AngleRestraint &angrest) return ds; } -Q_GLOBAL_STATIC_WITH_ARGS(Symbol, getThetaSymbol, ("theta")); - -/** Return the symbol that represents the angle between the points (theta) */ -const Symbol &AngleRestraint::theta() -{ - return *(getThetaSymbol()); -} - -/** Constructor */ -AngleRestraint::AngleRestraint() : ConcreteProperty() +/** Null constructor */ +AngleRestraint::AngleRestraint() + : ConcreteProperty(), + _ktheta(0), _theta0(0) { } -void AngleRestraint::calculateTheta() -{ - if (this->restraintFunction().isFunction(theta())) +/** Construct a restraint that acts on the angle within the + three atoms 'atom0', 'atom1' and 'atom2' (theta == a(012)), + restraining the angle within these atoms */ +AngleRestraint::AngleRestraint(const QList &atoms, + const SireUnits::Dimension::Angle &theta0, + const SireUnits::Dimension::HarmonicAngleConstant &ktheta) + : ConcreteProperty(), + _theta0(theta0), _ktheta(ktheta) +{ + // Need to think here about validating the angle and force constant values + // if (atoms.count() != 3) + // { + // throw SireError::invalid_arg(QObject::tr( + // "Wrong number of inputs for an Angle restraint. You need to " + // "provide 3 atoms (%1).") + // .arg(atoms.count()), + // // .arg(theta0.count()) + // // .arg(ktheta.count()), + // CODELOC); + // } + + // make sure that we have 3 distinct atoms + QSet distinct; + distinct.reserve(3); + + for (const auto &atom : atoms) { - SireUnits::Dimension::Angle angle; - - if (intra_molecule_points) - // we don't use the space when calculating intra-molecular angles - angle = Vector::angle(p[0].read().point(), p[1].read().point(), p[2].read().point()); - else - angle = this->space().calcAngle(p[0].read().point(), p[1].read().point(), p[2].read().point()); - - ExpressionRestraint3D::_pvt_setValue(theta(), angle); + if (atom >= 0) + distinct.insert(atom); } -} - -/** Construct a restraint that acts on the angle within the - three points 'point0', 'point1' and 'point2' (theta == a(012)), - restraining the angle within these points using the expression - 'restraint' */ -AngleRestraint::AngleRestraint(const PointRef &point0, const PointRef &point1, const PointRef &point2, - const Expression &restraint) - : ConcreteProperty(restraint) -{ - p[0] = point0; - p[1] = point1; - p[2] = point2; - force_expression = this->restraintFunction().differentiate(theta()); + // if (distinct.count() != 3) + // throw SireError::invalid_arg(QObject::tr( + // "There is something wrong with the atoms provided. " + // "They should all be unique and all greater than or equal to 0."), + // CODELOC); - if (force_expression.isConstant()) - force_expression = force_expression.evaluate(Values()); + atms = atoms.toVector(); +} - intra_molecule_points = Point::areIntraMoleculePoints(p[0], p[1]) and Point::areIntraMoleculePoints(p[0], p[2]); +/* Copy constructor*/ +AngleRestraint::AngleRestraint(const AngleRestraint &other) + : ConcreteProperty(other), + atms(other.atms), _theta0(other._theta0), _ktheta(other._ktheta) - this->calculateTheta(); +{ } -/** Construct a restraint that acts on the angle within the - three points 'point0', 'point1' and 'point2' (theta == a(012)), - restraining the angle within these points using the expression - 'restraint' */ -AngleRestraint::AngleRestraint(const PointRef &point0, const PointRef &point1, const PointRef &point2, - const Expression &restraint, const Values &values) - : ConcreteProperty(restraint, values) +/* Destructor */ +AngleRestraint::~AngleRestraint() { - p[0] = point0; - p[1] = point1; - p[2] = point2; +} - force_expression = this->restraintFunction().differentiate(theta()); +AngleRestraint &AngleRestraint::operator=(const AngleRestraint &other) +{ + if (this != &other) + { + Property::operator=(other); + atms = other.atms; + _theta0 = other._theta0; + _ktheta = other._ktheta; + } - if (force_expression.isConstant()) - force_expression = force_expression.evaluate(Values()); + return *this; +} - intra_molecule_points = Point::areIntraMoleculePoints(p[0], p[1]) and Point::areIntraMoleculePoints(p[0], p[2]); +bool AngleRestraint::operator==(const AngleRestraint &other) const +{ + return atms == other.atms and + _theta0 == other._theta0 and + _ktheta == other._ktheta; +} - this->calculateTheta(); +bool AngleRestraint::operator!=(const AngleRestraint &other) const +{ + return not operator==(other); } -/** Internal constructor used to construct a restraint using the specified - points, energy expression and force expression */ -AngleRestraint::AngleRestraint(const PointRef &point0, const PointRef &point1, const PointRef &point2, - const Expression &nrg_restraint, const Expression &force_restraint) - : ConcreteProperty(nrg_restraint), force_expression(force_restraint) +AngleRestraints AngleRestraint::operator+(const AngleRestraint &other) const { - p[0] = point0; - p[1] = point1; - p[2] = point2; + return AngleRestraints(*this) + other; +} - if (force_expression.isConstant()) - { - force_expression = force_expression.evaluate(Values()); - } - else - { - if (not this->restraintFunction().symbols().contains(force_expression.symbols())) - throw SireError::incompatible_error( - QObject::tr("You cannot use a force function which uses more symbols " - "(%1) than the energy function (%2).") - .arg(Sire::toString(force_expression.symbols()), Sire::toString(restraintFunction().symbols())), - CODELOC); - } +AngleRestraints AngleRestraint::operator+(const AngleRestraints &other) const +{ + return AngleRestraints(*this) + other; +} - intra_molecule_points = Point::areIntraMoleculePoints(p[0], p[1]) and Point::areIntraMoleculePoints(p[0], p[2]); +const char *AngleRestraint::typeName() +{ + return QMetaType::typeName(qMetaTypeId()); +} - this->calculateTheta(); +const char *AngleRestraint::what() const +{ + return AngleRestraint::typeName(); } -/** Copy constructor */ -AngleRestraint::AngleRestraint(const AngleRestraint &other) - : ConcreteProperty(other), force_expression(other.force_expression), - intra_molecule_points(other.intra_molecule_points) +AngleRestraint *AngleRestraint::clone() const { - for (int i = 0; i < this->nPoints(); ++i) - { - p[i] = other.p[i]; - } + return new AngleRestraint(*this); } -/** Destructor */ -AngleRestraint::~AngleRestraint() +bool AngleRestraint::isNull() const { + return atms.isEmpty(); } -/** Copy assignment operator */ -AngleRestraint &AngleRestraint::operator=(const AngleRestraint &other) +QString AngleRestraint::toString() const { - if (this != &other) + if (this->isNull()) + return QObject::tr("AngleRestraint::null"); + else { - ExpressionRestraint3D::operator=(other); + QStringList a; - for (int i = 0; i < this->nPoints(); ++i) + for (const auto &atom : atms) { - p[i] = other.p[i]; + a.append(QString::number(atom)); } - - force_expression = other.force_expression; - intra_molecule_points = other.intra_molecule_points; + return QString("AngleRestraint( [%1], theta0=%2, ktheta=%3 )") + .arg(a.join(", ")) + .arg(_theta0.toString()) + .arg(_ktheta.toString()); } - - return *this; } -/** Comparison operator */ -bool AngleRestraint::operator==(const AngleRestraint &other) const +/** Return the force constant for the restraint */ +SireUnits::Dimension::HarmonicAngleConstant AngleRestraint::ktheta() const { - return this == &other or (ExpressionRestraint3D::operator==(other) and p[0] == other.p[0] and p[1] == other.p[1] and - p[2] == other.p[2] and force_expression == other.force_expression); + return this->_ktheta; } -/** Comparison operator */ -bool AngleRestraint::operator!=(const AngleRestraint &other) const +/** Return the equilibrium angle for the restraint */ +SireUnits::Dimension::Angle AngleRestraint::theta0() const { - return not AngleRestraint::operator==(other); + return this->_theta0; } -const char *AngleRestraint::typeName() +/** Return the atoms involved in the restraint */ +QVector AngleRestraint::atoms() const { - return QMetaType::typeName(qMetaTypeId()); + return this->atms; } -/** This restraint involves three points */ -int AngleRestraint::nPoints() const +/////// +/////// Implementation of AngleRestraints +/////// + +/** Serialise to a binary datastream */ + +static const RegisterMetaType r_angrests; + +QDataStream &operator<<(QDataStream &ds, const AngleRestraints &angrests) { - return 3; + writeHeader(ds, r_angrests, 1); + + SharedDataStream sds(ds); + + sds << angrests.r + << static_cast(angrests); + + return ds; } -/** Return the ith point */ -const Point &AngleRestraint::point(int i) const +/** Extract from a binary datastream */ +QDataStream &operator>>(QDataStream &ds, AngleRestraints &angrests) { - i = Index(i).map(this->nPoints()); + VersionID v = readHeader(ds, r_angrests); + + if (v == 1) + { + SharedDataStream sds(ds); - return p[i].read(); + sds >> angrests.r >> + static_cast(angrests); + } + else + throw version_error(v, "1", r_angrests, CODELOC); + + return ds; } -/** Return the first point */ -const Point &AngleRestraint::point0() const +/** Null constructor */ +AngleRestraints::AngleRestraints() + : ConcreteProperty() { - return p[0].read(); } -/** Return the second point */ -const Point &AngleRestraint::point1() const +AngleRestraints::AngleRestraints(const QString &name) + : ConcreteProperty(name) { - return p[1].read(); } -/** Return the third point */ -const Point &AngleRestraint::point2() const +AngleRestraints::AngleRestraints(const AngleRestraint &restraint) + : ConcreteProperty() { - return p[2].read(); + if (not restraint.isNull()) + r.append(restraint); } -/** Return the built-in symbols of this restraint */ -Symbols AngleRestraint::builtinSymbols() const +AngleRestraints::AngleRestraints(const QList &restraints) + : ConcreteProperty() { - if (this->restraintFunction().isFunction(theta())) - return theta(); - else - return Symbols(); + for (const auto &restraint : restraints) + { + if (not restraint.isNull()) + r.append(restraint); + } } -/** Return the values of the built-in symbols of this restraint */ -Values AngleRestraint::builtinValues() const +AngleRestraints::AngleRestraints(const QString &name, + const AngleRestraint &restraint) + : ConcreteProperty(name) { - if (this->restraintFunction().isFunction(theta())) - return theta() == this->values()[theta()]; - else - return Values(); + if (not restraint.isNull()) + r.append(restraint); } -/** Return the differential of this restraint with respect to - the symbol 'symbol' +AngleRestraints::AngleRestraints(const QString &name, + const QList &restraints) + : ConcreteProperty(name) +{ + for (const auto &restraint : restraints) + { + if (not restraint.isNull()) + r.append(restraint); + } +} - \throw SireCAS::unavailable_differential -*/ -RestraintPtr AngleRestraint::differentiate(const Symbol &symbol) const +AngleRestraints::AngleRestraints(const AngleRestraints &other) + : ConcreteProperty(other), r(other.r) { - if (this->restraintFunction().isFunction(symbol)) - return AngleRestraint(p[0], p[1], p[2], restraintFunction().differentiate(symbol), this->values()); - else - return NullRestraint(); } -/** Set the space used to evaluate the energy of this restraint +/* Desctructor */ +AngleRestraints::~AngleRestraints() +{ +} - \throw SireVol::incompatible_space -*/ -void AngleRestraint::setSpace(const Space &new_space) +AngleRestraints &AngleRestraints::operator=(const AngleRestraints &other) { - if (not this->space().equals(new_space)) + if (this != &other) { - AngleRestraint old_state(*this); - - try - { - for (int i = 0; i < this->nPoints(); ++i) - { - p[i].edit().setSpace(new_space); - } - - Restraint3D::setSpace(new_space); - - this->calculateTheta(); - } - catch (...) - { - AngleRestraint::operator=(old_state); - throw; - } + Restraints::operator=(other); + r = other.r; } + + return *this; } -/** Return the function used to calculate the restraint force */ -const Expression &AngleRestraint::differentialRestraintFunction() const +bool AngleRestraints::operator==(const AngleRestraints &other) const { - return force_expression; + return r == other.r and Restraints::operator==(other); } -/** Calculate the force acting on the molecule in the forcetable 'forcetable' - caused by this restraint, and add it on to the forcetable scaled by - 'scale_force' */ -void AngleRestraint::force(MolForceTable &forcetable, double scale_force) const +bool AngleRestraints::operator!=(const AngleRestraints &other) const { - bool in_p0 = p[0].read().contains(forcetable.molNum()); - bool in_p1 = p[1].read().contains(forcetable.molNum()); - bool in_p2 = p[2].read().contains(forcetable.molNum()); - - if (not(in_p0 or in_p1 or in_p2)) - // this molecule is not affected by the restraint - return; - - throw SireError::incomplete_code(QObject::tr("Haven't yet written the code to calculate forces caused " - "by an angle restraint."), - CODELOC); + return not operator==(other); } -/** Calculate the force acting on the molecules in the forcetable 'forcetable' - caused by this restraint, and add it on to the forcetable scaled by - 'scale_force' */ -void AngleRestraint::force(ForceTable &forcetable, double scale_force) const +const char *AngleRestraints::typeName() { - bool in_p0 = p[0].read().usesMoleculesIn(forcetable); - bool in_p1 = p[1].read().usesMoleculesIn(forcetable); - bool in_p2 = p[2].read().usesMoleculesIn(forcetable); - - if (not(in_p0 or in_p1 or in_p2)) - // this molecule is not affected by the restraint - return; + return QMetaType::typeName(qMetaTypeId()); +} - throw SireError::incomplete_code(QObject::tr("Haven't yet written the code to calculate forces caused " - "by an angle restraint."), - CODELOC); +const char *AngleRestraints::what() const +{ + return AngleRestraints::typeName(); } -/** Update the points of this restraint using new molecule data from 'moldata' +AngleRestraints *AngleRestraints::clone() const +{ + return new AngleRestraints(*this); +} - \throw SireBase::missing_property - \throw SireError::invalid_cast - \throw SireError::incompatible_error -*/ -void AngleRestraint::update(const MoleculeData &moldata) +QString AngleRestraints::toString() const { - if (this->contains(moldata.number())) - { - AngleRestraint old_state(*this); + if (this->isEmpty()) + return QObject::tr("AngleRestraints::null"); - try - { - for (int i = 0; i < this->nPoints(); ++i) - { - p[i].edit().update(moldata); - } + QStringList parts; - this->calculateTheta(); - } - catch (...) + const auto n = this->count(); + + if (n <= 10) + { + for (int i = 0; i < n; i++) { - AngleRestraint::operator=(old_state); - throw; + parts.append(QObject::tr("%1: %2").arg(i).arg(this->r.at(i).toString())); } } -} - -/** Update the points of this restraint using new molecule data from 'molecules' - - \throw SireBase::missing_property - \throw SireError::invalid_cast - \throw SireError::incompatible_error -*/ -void AngleRestraint::update(const Molecules &molecules) -{ - if (this->usesMoleculesIn(molecules)) + else { - AngleRestraint old_state(*this); - - try + for (int i = 0; i < 5; i++) { - for (int i = 0; i < this->nPoints(); ++i) - { - p[i].edit().update(molecules); - } - - this->calculateTheta(); + parts.append(QObject::tr("%1: %2").arg(i).arg(this->r.at(i).toString())); } - catch (...) + + parts.append("..."); + + for (int i = n - 5; i < n; i++) { - AngleRestraint::operator=(old_state); - throw; + parts.append(QObject::tr("%1: %2").arg(i).arg(this->r.at(i).toString())); } } + + return QObject::tr("AngleRestraints( name=%1, size=%2\n%3\n )") + .arg(this->name()) + .arg(n) + .arg(parts.join("\n")); } -/** Return the molecules used in this restraint */ -Molecules AngleRestraint::molecules() const +/** Return whether or not this is empty */ +bool AngleRestraints::isEmpty() const { - Molecules mols; + return this->r.isEmpty(); +} - for (int i = 0; i < this->nPoints(); ++i) - { - mols += p[i].read().molecules(); - } +/** Return whether or not this is empty */ +bool AngleRestraints::isNull() const +{ + return this->isEmpty(); +} - return mols; +/** Return the number of restraints */ +int AngleRestraints::nRestraints() const +{ + return this->r.count(); } -/** Return whether or not this restraint affects the molecule - with number 'molnum' */ -bool AngleRestraint::contains(MolNum molnum) const +/** Return the number of restraints */ +int AngleRestraints::count() const { - return p[0].read().contains(molnum) or p[1].read().contains(molnum) or p[2].read().contains(molnum); + return this->nRestraints(); } -/** Return whether or not this restraint affects the molecule - with ID 'molid' */ -bool AngleRestraint::contains(const MolID &molid) const +/** Return the number of restraints */ +int AngleRestraints::size() const { - return p[0].read().contains(molid) or p[1].read().contains(molid) or p[2].read().contains(molid); + return this->nRestraints(); } -/** Return whether or not this restraint involves any of the molecules - that are in the forcetable 'forcetable' */ -bool AngleRestraint::usesMoleculesIn(const ForceTable &forcetable) const +/** Return the ith restraint */ +const AngleRestraint &AngleRestraints::at(int i) const { - return p[0].read().usesMoleculesIn(forcetable) or p[1].read().usesMoleculesIn(forcetable) or - p[2].read().usesMoleculesIn(forcetable); + i = SireID::Index(i).map(this->r.count()); + + return this->r.at(i); } -/** Return whether or not this restraint involves any of the molecules - in 'molecules' */ -bool AngleRestraint::usesMoleculesIn(const Molecules &molecules) const +/** Return the ith restraint */ +const AngleRestraint &AngleRestraints::operator[](int i) const { - return p[0].read().usesMoleculesIn(molecules) or p[1].read().usesMoleculesIn(molecules) or - p[2].read().usesMoleculesIn(molecules); + return this->at(i); } -static Expression harmonicFunction(double force_constant) +/** Return all of the restraints */ +QList AngleRestraints::restraints() const { - if (SireMaths::isZero(force_constant)) - return 0; - else - return force_constant * pow(AngleRestraint::theta(), 2); + return this->r; } -static Expression diffHarmonicFunction(double force_constant) +/** Add a restraints onto the list */ +void AngleRestraints::add(const AngleRestraint &restraint) { - if (SireMaths::isZero(force_constant)) - return 0; - else - return (2 * force_constant) * AngleRestraint::theta(); + if (not restraint.isNull()) + r.append(restraint); } -/** Return a distance restraint that applies a harmonic potential between - the points 'point0' and 'point1' using a force constant 'force_constant' */ -AngleRestraint AngleRestraint::harmonic(const PointRef &point0, const PointRef &point1, const PointRef &point2, - const HarmonicAngleForceConstant &force_constant) +/** Add a restraint onto the list */ +void AngleRestraints::add(const AngleRestraints &restraints) { - return AngleRestraint(point0, point1, point2, ::harmonicFunction(force_constant), - ::diffHarmonicFunction(force_constant)); + this->r += restraints.r; } -static Expression halfHarmonicFunction(double force_constant, double angle) +/** Add a restraint onto the list */ +AngleRestraints &AngleRestraints::operator+=(const AngleRestraint &restraint) { - if (SireMaths::isZero(force_constant)) - return 0; + this->add(restraint); + return *this; +} - else if (angle <= 0) - // this is just a harmonic function - return ::harmonicFunction(force_constant); +/** Add a restraint onto the list */ +AngleRestraints AngleRestraints::operator+(const AngleRestraint &restraint) const +{ + AngleRestraints ret(*this); + ret += restraint; + return *this; +} - else - { - const Symbol &theta = AngleRestraint::theta(); - return Conditional(GreaterThan(theta, angle), force_constant * pow(theta - angle, 2), 0); - } +/** Add restraints onto the list */ +AngleRestraints &AngleRestraints::operator+=(const AngleRestraints &restraints) +{ + this->add(restraints); + return *this; } -static Expression diffHalfHarmonicFunction(double force_constant, double angle) +/** Add restraints onto the list */ +AngleRestraints AngleRestraints::operator+(const AngleRestraints &restraints) const { - if (SireMaths::isZero(force_constant)) - return 0; + AngleRestraints ret(*this); + ret += restraints; + return *this; +} - else if (angle <= 0) - // this is just a harmonic function - return ::diffHarmonicFunction(force_constant); +// QDataStream &operator<<(QDataStream &ds, const AngleRestraint &angrest) +// { +// writeHeader(ds, r_angrest, 1); - else - { - const Symbol &theta = AngleRestraint::theta(); - return Conditional(GreaterThan(theta, angle), (2 * force_constant) * (theta - angle), 0); - } -} +// SharedDataStream sds(ds); -/** Return a distance restraint that applied a half-harmonic potential - between the points 'point0' and 'point1' above a distance 'distance' - using a force constant 'force_constant' */ -AngleRestraint AngleRestraint::halfHarmonic(const PointRef &point0, const PointRef &point1, const PointRef &point2, - const Angle &angle, const HarmonicAngleForceConstant &force_constant) -{ - double acute_angle = acute(angle).to(radians); +// sds << angrest.p[0] << angrest.p[1] << angrest.p[2] << angrest.force_expression +// << static_cast(angrest); - return AngleRestraint(point0, point1, point2, ::halfHarmonicFunction(force_constant, acute_angle), - ::diffHalfHarmonicFunction(force_constant, acute_angle)); -} +// return ds; +// } + +// /** Extract from a binary datastream */ +// QDataStream &operator>>(QDataStream &ds, AngleRestraint &angrest) +// { +// VersionID v = readHeader(ds, r_angrest); + +// if (v == 1) +// { +// SharedDataStream sds(ds); + +// sds >> angrest.p[0] >> angrest.p[1] >> angrest.p[2] >> angrest.force_expression >> +// static_cast(angrest); + +// angrest.intra_molecule_points = Point::areIntraMoleculePoints(angrest.p[0], angrest.p[1]) and +// Point::areIntraMoleculePoints(angrest.p[0], angrest.p[2]); +// } +// else +// throw version_error(v, "1", r_angrest, CODELOC); + +// return ds; +// } + +// Q_GLOBAL_STATIC_WITH_ARGS(Symbol, getThetaSymbol, ("theta")); + +/** Return the symbol that represents the angle between the points (theta) */ +// const Symbol &AngleRestraint::theta() +// { +// return *(getThetaSymbol()); +// } + +// /** Constructor */ +// AngleRestraint::AngleRestraint() : ConcreteProperty() +// { +// } + +// void AngleRestraint::calculateTheta() +// { +// if (this->restraintFunction().isFunction(theta())) +// { +// SireUnits::Dimension::Angle angle; + +// if (intra_molecule_points) +// // we don't use the space when calculating intra-molecular angles +// angle = Vector::angle(p[0].read().point(), p[1].read().point(), p[2].read().point()); +// else +// angle = this->space().calcAngle(p[0].read().point(), p[1].read().point(), p[2].read().point()); + +// ExpressionRestraint3D::_pvt_setValue(theta(), angle); +// } +// } + +// AngleRestraint::AngleRestraint(const PointRef &point0, const PointRef &point1, const PointRef &point2, +// const Expression &restraint) +// : ConcreteProperty(restraint) +// { +// p[0] = point0; +// p[1] = point1; +// p[2] = point2; + +// force_expression = this->restraintFunction().differentiate(theta()); + +// if (force_expression.isConstant()) +// force_expression = force_expression.evaluate(Values()); + +// intra_molecule_points = Point::areIntraMoleculePoints(p[0], p[1]) and Point::areIntraMoleculePoints(p[0], p[2]); + +// this->calculateTheta(); +// } + +// /** Construct a restraint that acts on the angle within the +// three points 'point0', 'point1' and 'point2' (theta == a(012)), +// restraining the angle within these points using the expression +// 'restraint' */ +// AngleRestraint::AngleRestraint(const PointRef &point0, const PointRef &point1, const PointRef &point2, +// const Expression &restraint, const Values &values) +// : ConcreteProperty(restraint, values) +// { +// p[0] = point0; +// p[1] = point1; +// p[2] = point2; + +// force_expression = this->restraintFunction().differentiate(theta()); + +// if (force_expression.isConstant()) +// force_expression = force_expression.evaluate(Values()); + +// intra_molecule_points = Point::areIntraMoleculePoints(p[0], p[1]) and Point::areIntraMoleculePoints(p[0], p[2]); + +// this->calculateTheta(); +// } + +// /** Internal constructor used to construct a restraint using the specified +// points, energy expression and force expression */ +// AngleRestraint::AngleRestraint(const PointRef &point0, const PointRef &point1, const PointRef &point2, +// const Expression &nrg_restraint, const Expression &force_restraint) +// : ConcreteProperty(nrg_restraint), force_expression(force_restraint) +// { +// p[0] = point0; +// p[1] = point1; +// p[2] = point2; + +// if (force_expression.isConstant()) +// { +// force_expression = force_expression.evaluate(Values()); +// } +// else +// { +// if (not this->restraintFunction().symbols().contains(force_expression.symbols())) +// throw SireError::incompatible_error( +// QObject::tr("You cannot use a force function which uses more symbols " +// "(%1) than the energy function (%2).") +// .arg(Sire::toString(force_expression.symbols()), Sire::toString(restraintFunction().symbols())), +// CODELOC); +// } + +// intra_molecule_points = Point::areIntraMoleculePoints(p[0], p[1]) and Point::areIntraMoleculePoints(p[0], p[2]); + +// this->calculateTheta(); +// } + +// /** Copy constructor */ +// AngleRestraint::AngleRestraint(const AngleRestraint &other) +// : ConcreteProperty(other), force_expression(other.force_expression), +// intra_molecule_points(other.intra_molecule_points) +// { +// for (int i = 0; i < this->nPoints(); ++i) +// { +// p[i] = other.p[i]; +// } +// } + +// /** Destructor */ +// AngleRestraint::~AngleRestraint() +// { +// } + +// /** Copy assignment operator */ +// AngleRestraint &AngleRestraint::operator=(const AngleRestraint &other) +// { +// if (this != &other) +// { +// ExpressionRestraint3D::operator=(other); + +// for (int i = 0; i < this->nPoints(); ++i) +// { +// p[i] = other.p[i]; +// } + +// force_expression = other.force_expression; +// intra_molecule_points = other.intra_molecule_points; +// } + +// return *this; +// } + +// /** Comparison operator */ +// bool AngleRestraint::operator==(const AngleRestraint &other) const +// { +// return this == &other or (ExpressionRestraint3D::operator==(other) and p[0] == other.p[0] and p[1] == other.p[1] and +// p[2] == other.p[2] and force_expression == other.force_expression); +// } + +// /** Comparison operator */ +// bool AngleRestraint::operator!=(const AngleRestraint &other) const +// { +// return not AngleRestraint::operator==(other); +// } + +// const char *AngleRestraint::typeName() +// { +// return QMetaType::typeName(qMetaTypeId()); +// } + +// /** This restraint involves three points */ +// int AngleRestraint::nPoints() const +// { +// return 3; +// } + +// /** Return the ith point */ +// const Point &AngleRestraint::point(int i) const +// { +// i = Index(i).map(this->nPoints()); + +// return p[i].read(); +// } + +// /** Return the first point */ +// const Point &AngleRestraint::point0() const +// { +// return p[0].read(); +// } + +// /** Return the second point */ +// const Point &AngleRestraint::point1() const +// { +// return p[1].read(); +// } + +// /** Return the third point */ +// const Point &AngleRestraint::point2() const +// { +// return p[2].read(); +// } + +// /** Return the built-in symbols of this restraint */ +// Symbols AngleRestraint::builtinSymbols() const +// { +// if (this->restraintFunction().isFunction(theta())) +// return theta(); +// else +// return Symbols(); +// } + +// /** Return the values of the built-in symbols of this restraint */ +// Values AngleRestraint::builtinValues() const +// { +// if (this->restraintFunction().isFunction(theta())) +// return theta() == this->values()[theta()]; +// else +// return Values(); +// } + +// /** Return the differential of this restraint with respect to +// the symbol 'symbol' + +// \throw SireCAS::unavailable_differential +// */ +// RestraintPtr AngleRestraint::differentiate(const Symbol &symbol) const +// { +// if (this->restraintFunction().isFunction(symbol)) +// return AngleRestraint(p[0], p[1], p[2], restraintFunction().differentiate(symbol), this->values()); +// else +// return NullRestraint(); +// } + +// /** Set the space used to evaluate the energy of this restraint + +// \throw SireVol::incompatible_space +// */ +// void AngleRestraint::setSpace(const Space &new_space) +// { +// if (not this->space().equals(new_space)) +// { +// AngleRestraint old_state(*this); + +// try +// { +// for (int i = 0; i < this->nPoints(); ++i) +// { +// p[i].edit().setSpace(new_space); +// } + +// Restraint3D::setSpace(new_space); + +// this->calculateTheta(); +// } +// catch (...) +// { +// AngleRestraint::operator=(old_state); +// throw; +// } +// } +// } + +// /** Return the function used to calculate the restraint force */ +// const Expression &AngleRestraint::differentialRestraintFunction() const +// { +// return force_expression; +// } + +// /** Calculate the force acting on the molecule in the forcetable 'forcetable' +// caused by this restraint, and add it on to the forcetable scaled by +// 'scale_force' */ +// void AngleRestraint::force(MolForceTable &forcetable, double scale_force) const +// { +// bool in_p0 = p[0].read().contains(forcetable.molNum()); +// bool in_p1 = p[1].read().contains(forcetable.molNum()); +// bool in_p2 = p[2].read().contains(forcetable.molNum()); + +// if (not(in_p0 or in_p1 or in_p2)) +// // this molecule is not affected by the restraint +// return; + +// throw SireError::incomplete_code(QObject::tr("Haven't yet written the code to calculate forces caused " +// "by an angle restraint."), +// CODELOC); +// } + +// /** Calculate the force acting on the molecules in the forcetable 'forcetable' +// caused by this restraint, and add it on to the forcetable scaled by +// 'scale_force' */ +// void AngleRestraint::force(ForceTable &forcetable, double scale_force) const +// { +// bool in_p0 = p[0].read().usesMoleculesIn(forcetable); +// bool in_p1 = p[1].read().usesMoleculesIn(forcetable); +// bool in_p2 = p[2].read().usesMoleculesIn(forcetable); + +// if (not(in_p0 or in_p1 or in_p2)) +// // this molecule is not affected by the restraint +// return; + +// throw SireError::incomplete_code(QObject::tr("Haven't yet written the code to calculate forces caused " +// "by an angle restraint."), +// CODELOC); +// } + +// /** Update the points of this restraint using new molecule data from 'moldata' + +// \throw SireBase::missing_property +// \throw SireError::invalid_cast +// \throw SireError::incompatible_error +// */ +// void AngleRestraint::update(const MoleculeData &moldata) +// { +// if (this->contains(moldata.number())) +// { +// AngleRestraint old_state(*this); + +// try +// { +// for (int i = 0; i < this->nPoints(); ++i) +// { +// p[i].edit().update(moldata); +// } + +// this->calculateTheta(); +// } +// catch (...) +// { +// AngleRestraint::operator=(old_state); +// throw; +// } +// } +// } + +// /** Update the points of this restraint using new molecule data from 'molecules' + +// \throw SireBase::missing_property +// \throw SireError::invalid_cast +// \throw SireError::incompatible_error +// */ +// void AngleRestraint::update(const Molecules &molecules) +// { +// if (this->usesMoleculesIn(molecules)) +// { +// AngleRestraint old_state(*this); + +// try +// { +// for (int i = 0; i < this->nPoints(); ++i) +// { +// p[i].edit().update(molecules); +// } + +// this->calculateTheta(); +// } +// catch (...) +// { +// AngleRestraint::operator=(old_state); +// throw; +// } +// } +// } + +// /** Return the molecules used in this restraint */ +// Molecules AngleRestraint::molecules() const +// { +// Molecules mols; + +// for (int i = 0; i < this->nPoints(); ++i) +// { +// mols += p[i].read().molecules(); +// } + +// return mols; +// } + +// /** Return whether or not this restraint affects the molecule +// with number 'molnum' */ +// bool AngleRestraint::contains(MolNum molnum) const +// { +// return p[0].read().contains(molnum) or p[1].read().contains(molnum) or p[2].read().contains(molnum); +// } + +// /** Return whether or not this restraint affects the molecule +// with ID 'molid' */ +// bool AngleRestraint::contains(const MolID &molid) const +// { +// return p[0].read().contains(molid) or p[1].read().contains(molid) or p[2].read().contains(molid); +// } + +// /** Return whether or not this restraint involves any of the molecules +// that are in the forcetable 'forcetable' */ +// bool AngleRestraint::usesMoleculesIn(const ForceTable &forcetable) const +// { +// return p[0].read().usesMoleculesIn(forcetable) or p[1].read().usesMoleculesIn(forcetable) or +// p[2].read().usesMoleculesIn(forcetable); +// } + +// /** Return whether or not this restraint involves any of the molecules +// in 'molecules' */ +// bool AngleRestraint::usesMoleculesIn(const Molecules &molecules) const +// { +// return p[0].read().usesMoleculesIn(molecules) or p[1].read().usesMoleculesIn(molecules) or +// p[2].read().usesMoleculesIn(molecules); +// } + +// static Expression harmonicFunction(double force_constant) +// { +// if (SireMaths::isZero(force_constant)) +// return 0; +// else +// return force_constant * pow(AngleRestraint::theta(), 2); +// } + +// static Expression diffHarmonicFunction(double force_constant) +// { +// if (SireMaths::isZero(force_constant)) +// return 0; +// else +// return (2 * force_constant) * AngleRestraint::theta(); +// } + +// /** Return a distance restraint that applies a harmonic potential between +// the points 'point0' and 'point1' using a force constant 'force_constant' */ +// AngleRestraint AngleRestraint::harmonic(const PointRef &point0, const PointRef &point1, const PointRef &point2, +// const HarmonicAngleForceConstant &force_constant) +// { +// return AngleRestraint(point0, point1, point2, ::harmonicFunction(force_constant), +// ::diffHarmonicFunction(force_constant)); +// } + +// static Expression halfHarmonicFunction(double force_constant, double angle) +// { +// if (SireMaths::isZero(force_constant)) +// return 0; + +// else if (angle <= 0) +// // this is just a harmonic function +// return ::harmonicFunction(force_constant); + +// else +// { +// const Symbol &theta = AngleRestraint::theta(); +// return Conditional(GreaterThan(theta, angle), force_constant * pow(theta - angle, 2), 0); +// } +// } + +// static Expression diffHalfHarmonicFunction(double force_constant, double angle) +// { +// if (SireMaths::isZero(force_constant)) +// return 0; + +// else if (angle <= 0) +// // this is just a harmonic function +// return ::diffHarmonicFunction(force_constant); + +// else +// { +// const Symbol &theta = AngleRestraint::theta(); +// return Conditional(GreaterThan(theta, angle), (2 * force_constant) * (theta - angle), 0); +// } +// } + +// /** Return a distance restraint that applied a half-harmonic potential +// between the points 'point0' and 'point1' above a distance 'distance' +// using a force constant 'force_constant' */ +// AngleRestraint AngleRestraint::halfHarmonic(const PointRef &point0, const PointRef &point1, const PointRef &point2, +// const Angle &angle, const HarmonicAngleForceConstant &force_constant) +// { +// double acute_angle = acute(angle).to(radians); + +// return AngleRestraint(point0, point1, point2, ::halfHarmonicFunction(force_constant, acute_angle), +// ::diffHalfHarmonicFunction(force_constant, acute_angle)); +// } diff --git a/corelib/src/libs/SireMM/anglerestraint.h b/corelib/src/libs/SireMM/anglerestraint.h index 6901c78ca..c8a4f228e 100644 --- a/corelib/src/libs/SireMM/anglerestraint.h +++ b/corelib/src/libs/SireMM/anglerestraint.h @@ -28,56 +28,49 @@ #ifndef SIREMM_ANGLERESTRAINT_H #define SIREMM_ANGLERESTRAINT_H -#include "SireFF/point.h" +// #include "SireFF/point.h" -#include "restraint.h" +#include "restraints.h" -#include "SireCAS/expression.h" -#include "SireCAS/symbol.h" +// #include "SireCAS/expression.h" +// #include "SireCAS/symbol.h" #include "SireUnits/dimensions.h" +#include "SireUnits/generalunit.h" SIRE_BEGIN_HEADER namespace SireMM { class AngleRestraint; + class AngleRestraints; } SIREMM_EXPORT QDataStream &operator<<(QDataStream &, const SireMM::AngleRestraint &); SIREMM_EXPORT QDataStream &operator>>(QDataStream &, SireMM::AngleRestraint &); +SIREMM_EXPORT QDataStream &operator<<(QDataStream &, const SireMM::AngleRestraints &); +SIREMM_EXPORT QDataStream &operator>>(QDataStream &, SireMM::AngleRestraints &); + namespace SireMM { - using SireFF::Point; - using SireFF::PointRef; - - using SireCAS::Expression; - using SireCAS::Symbol; - using SireCAS::Symbols; - - // typedef the unit of a harmonic force constant ( MolarEnergy / Angle^2 ) - typedef SireUnits::Dimension::PhysUnit<1, 2, -2, 0, 0, -1, -2> HarmonicAngleForceConstant; - - /** This is a restraint that operates on the angle between - three SireMM::Point objects (e.g. three atoms in a molecule) - - @author Christopher Woods - */ - class SIREMM_EXPORT AngleRestraint : public SireBase::ConcreteProperty + /** This class represents a single angle restraint between any three + * atoms in a system + * @author Christopher Woods + */ + class SIREMM_EXPORT AngleRestraint + : public SireBase::ConcreteProperty { - friend SIREMM_EXPORT QDataStream & ::operator<<(QDataStream &, const AngleRestraint &); - friend SIREMM_EXPORT QDataStream & ::operator>>(QDataStream &, AngleRestraint &); + friend SIREMM_EXPORT QDataStream & ::operator<<(QDataStream &, const SireMM::AngleRestraint &); + friend SIREMM_EXPORT QDataStream & ::operator>>(QDataStream &, SireMM::AngleRestraint &); public: AngleRestraint(); - - AngleRestraint(const PointRef &point0, const PointRef &point1, const PointRef &point2, const Expression &restraint); - - AngleRestraint(const PointRef &point0, const PointRef &point1, const PointRef &point2, const Expression &restraint, - const Values &values); + AngleRestraint(const QList &atoms, + const SireUnits::Dimension::Angle &theta0, + const SireUnits::Dimension::HarmonicAngleConstant &ktheta); AngleRestraint(const AngleRestraint &other); @@ -88,72 +81,102 @@ namespace SireMM bool operator==(const AngleRestraint &other) const; bool operator!=(const AngleRestraint &other) const; + AngleRestraints operator+(const AngleRestraint &other) const; + AngleRestraints operator+(const AngleRestraints &other) const; + static const char *typeName(); + const char *what() const; - const Point &point(int i) const; + AngleRestraint *clone() const; - const Point &point0() const; - const Point &point1() const; - const Point &point2() const; + QString toString() const; - int nPoints() const; + bool isNull() const; - static const Symbol &theta(); + QVector atoms() const; - Symbols builtinSymbols() const; - Values builtinValues() const; + SireUnits::Dimension::Angle theta0() const; + SireUnits::Dimension::HarmonicAngleConstant ktheta() const; - RestraintPtr differentiate(const Symbol &symbol) const; + private: + /** Atoms involved in the angle restraint */ + QVector atms; - void setSpace(const Space &space); + /** Equilibrium angle */ + SireUnits::Dimension::Angle _theta0; - const Expression &differentialRestraintFunction() const; + /** Harmonic angle constant */ + SireUnits::Dimension::HarmonicAngleConstant _ktheta; + }; - void force(MolForceTable &forcetable, double scale_force = 1) const; - void force(ForceTable &forcetable, double scale_force = 1) const; + /** This class represents a collection of angle restraints */ + class SIREMM_EXPORT AngleRestraints + : public SireBase::ConcreteProperty + { + friend SIREMM_EXPORT QDataStream & ::operator<<(QDataStream &, const SireMM::AngleRestraints &); + friend SIREMM_EXPORT QDataStream & ::operator>>(QDataStream &, SireMM::AngleRestraints &); - void update(const MoleculeData &moldata); + public: + AngleRestraints(); + AngleRestraints(const QString &name); - void update(const Molecules &molecules); + AngleRestraints(const AngleRestraint &restraint); + AngleRestraints(const QList &restraints); - Molecules molecules() const; + AngleRestraints(const QString &name, + const AngleRestraint &restraint); - bool contains(MolNum molnum) const; - bool contains(const MolID &molid) const; + AngleRestraints(const QString &name, + const QList &restraints); - bool usesMoleculesIn(const ForceTable &forcetable) const; - bool usesMoleculesIn(const Molecules &molecules) const; + AngleRestraints(const AngleRestraints &other); - static AngleRestraint harmonic(const PointRef &point0, const PointRef &point1, const PointRef &point2, - const HarmonicAngleForceConstant &force_constant); + ~AngleRestraints(); - static AngleRestraint halfHarmonic(const PointRef &point0, const PointRef &point1, const PointRef &point2, - const SireUnits::Dimension::Angle &angle, - const HarmonicAngleForceConstant &force_constant); + AngleRestraints &operator=(const AngleRestraints &other); - protected: - AngleRestraint(const PointRef &point0, const PointRef &point1, const PointRef &point2, - const Expression &nrg_restraint, const Expression &force_restraint); + bool operator==(const AngleRestraints &other) const; + bool operator!=(const AngleRestraints &other) const; - private: - void calculateTheta(); + static const char *typeName(); + const char *what() const; - /** The three points between which the restraint is calculated */ - SireFF::PointPtr p[3]; + AngleRestraints *clone() const; - /** The expression used to calculate the force */ - Expression force_expression; + QString toString() const; - /** Whether or not all three points are within the same molecule */ - bool intra_molecule_points; - }; + bool isEmpty() const; + bool isNull() const; + + int count() const; + int size() const; + int nRestraints() const; + + const AngleRestraint &at(int i) const; + const AngleRestraint &operator[](int i) const; + + QList restraints() const; + + void add(const AngleRestraint &restraint); + void add(const AngleRestraints &restraints); -} // namespace SireMM + AngleRestraints &operator+=(const AngleRestraint &restraint); + AngleRestraints &operator+=(const AngleRestraints &restraints); + + AngleRestraints operator+(const AngleRestraint &restraint) const; + AngleRestraints operator+(const AngleRestraints &restraints) const; + + private: + /** List of restraints */ + QList r; + }; +} Q_DECLARE_METATYPE(SireMM::AngleRestraint) +Q_DECLARE_METATYPE(SireMM::AngleRestraints) SIRE_EXPOSE_CLASS(SireMM::AngleRestraint) - +SIRE_EXPOSE_CLASS(SireMM::AngleRestraints) SIRE_END_HEADER #endif From 12af94a60299cda9aa7b90aff0eed9cf547dafa8 Mon Sep 17 00:00:00 2001 From: Audrius Kalpokas Date: Wed, 10 Jul 2024 18:14:45 +0100 Subject: [PATCH 002/102] Intial addition of alchemical dihedral restraints and angle restraints update --- corelib/src/libs/SireMM/anglerestraint.cpp | 2 +- corelib/src/libs/SireMM/dihedralrestraint.cpp | 639 ++++++++---------- corelib/src/libs/SireMM/dihedralrestraint.h | 152 +++-- wrapper/MM/AngleRestraints.pypp.cpp | 254 +++++++ wrapper/MM/AngleRestraints.pypp.hpp | 10 + wrapper/MM/DihedralRestraints.pypp.cpp | 254 +++++++ wrapper/MM/DihedralRestraints.pypp.hpp | 10 + 7 files changed, 910 insertions(+), 411 deletions(-) create mode 100644 wrapper/MM/AngleRestraints.pypp.cpp create mode 100644 wrapper/MM/AngleRestraints.pypp.hpp create mode 100644 wrapper/MM/DihedralRestraints.pypp.cpp create mode 100644 wrapper/MM/DihedralRestraints.pypp.hpp diff --git a/corelib/src/libs/SireMM/anglerestraint.cpp b/corelib/src/libs/SireMM/anglerestraint.cpp index 06e6d536e..00e93dde5 100644 --- a/corelib/src/libs/SireMM/anglerestraint.cpp +++ b/corelib/src/libs/SireMM/anglerestraint.cpp @@ -770,7 +770,7 @@ AngleRestraints AngleRestraints::operator+(const AngleRestraints &restraints) co // p[i].edit().setSpace(new_space); // } -// Restraint3D::setSpace(new_space); +// Restraint::setSpace(new_space); // this->calculateTheta(); // } diff --git a/corelib/src/libs/SireMM/dihedralrestraint.cpp b/corelib/src/libs/SireMM/dihedralrestraint.cpp index bea13ed94..68ed915f5 100644 --- a/corelib/src/libs/SireMM/dihedralrestraint.cpp +++ b/corelib/src/libs/SireMM/dihedralrestraint.cpp @@ -27,16 +27,16 @@ #include "dihedralrestraint.h" -#include "SireFF/forcetable.h" +// #include "SireFF/forcetable.h" -#include "SireCAS/conditional.h" -#include "SireCAS/power.h" -#include "SireCAS/symbols.h" -#include "SireCAS/values.h" +// #include "SireCAS/conditional.h" +// #include "SireCAS/power.h" +// #include "SireCAS/symbols.h" +// #include "SireCAS/values.h" #include "SireID/index.h" -#include "SireUnits/angle.h" +// #include "SireUnits/angle.h" #include "SireUnits/units.h" #include "SireStream/datastream.h" @@ -44,12 +44,14 @@ #include "SireCAS/errors.h" +#include + using namespace SireMM; -using namespace SireMol; -using namespace SireFF; +// using namespace SireMol; +// using namespace SireFF; using namespace SireID; using namespace SireBase; -using namespace SireCAS; +// using namespace SireCAS; using namespace SireMaths; using namespace SireStream; using namespace SireUnits; @@ -62,14 +64,14 @@ using namespace SireUnits::Dimension; static const RegisterMetaType r_dihrest; /** Serialise to a binary datastream */ + QDataStream &operator<<(QDataStream &ds, const DihedralRestraint &dihrest) { writeHeader(ds, r_dihrest, 1); SharedDataStream sds(ds); - sds << dihrest.p[0] << dihrest.p[1] << dihrest.p[2] << dihrest.p[3] << dihrest.force_expression - << static_cast(dihrest); + sds << dihrest.atms << dihrest._phi0 << dihrest._kphi; return ds; } @@ -83,12 +85,7 @@ QDataStream &operator>>(QDataStream &ds, DihedralRestraint &dihrest) { SharedDataStream sds(ds); - sds >> dihrest.p[0] >> dihrest.p[1] >> dihrest.p[2] >> dihrest.p[3] >> dihrest.force_expression >> - static_cast(dihrest); - - dihrest.intra_molecule_points = Point::areIntraMoleculePoints(dihrest.p[0], dihrest.p[1]) and - Point::areIntraMoleculePoints(dihrest.p[0], dihrest.p[2]) and - Point::areIntraMoleculePoints(dihrest.p[0], dihrest.p[3]); + sds >> dihrest.atms >> dihrest._phi0 >> dihrest._kphi; } else throw version_error(v, "1", r_dihrest, CODELOC); @@ -96,163 +93,101 @@ QDataStream &operator>>(QDataStream &ds, DihedralRestraint &dihrest) return ds; } -Q_GLOBAL_STATIC_WITH_ARGS(Symbol, getPhiSymbol, ("phi")); - -/** Return the symbol that represents the dihedral angle between the points (phi) */ -const Symbol &DihedralRestraint::phi() -{ - return *(getPhiSymbol()); -} - -/** Constructor */ -DihedralRestraint::DihedralRestraint() : ConcreteProperty() -{ -} -void DihedralRestraint::calculatePhi() +/** Null constructor */ +DihedralRestraint::DihedralRestraint() + : ConcreteProperty(), + _phi0(0), _kphi(0) { - if (this->restraintFunction().isFunction(phi())) - { - SireUnits::Dimension::Angle angle; - - if (intra_molecule_points) - // we don't use the space when calculating intra-molecular angles - angle = - Vector::dihedral(p[0].read().point(), p[1].read().point(), p[2].read().point(), p[3].read().point()); - else - angle = this->space().calcDihedral(p[0].read().point(), p[1].read().point(), p[2].read().point(), - p[3].read().point()); - - ExpressionRestraint3D::_pvt_setValue(phi(), angle); - } } -/** Construct a restraint that acts on the angle within the - three points 'point0', 'point1' and 'point2' (theta == a(012)), - restraining the angle within these points using the expression - 'restraint' */ -DihedralRestraint::DihedralRestraint(const PointRef &point0, const PointRef &point1, const PointRef &point2, - const PointRef &point3, const Expression &restraint) - : ConcreteProperty(restraint) -{ - p[0] = point0; - p[1] = point1; - p[2] = point2; - p[3] = point3; - - force_expression = this->restraintFunction().differentiate(phi()); - - if (force_expression.isConstant()) - force_expression = force_expression.evaluate(Values()); - - intra_molecule_points = Point::areIntraMoleculePoints(p[0], p[1]) and Point::areIntraMoleculePoints(p[0], p[2]) and - Point::areIntraMoleculePoints(p[0], p[3]); - - this->calculatePhi(); -} /** Construct a restraint that acts on the angle within the - three points 'point0', 'point1' and 'point2' (theta == a(012)), - restraining the angle within these points using the expression - 'restraint' */ -DihedralRestraint::DihedralRestraint(const PointRef &point0, const PointRef &point1, const PointRef &point2, - const PointRef &point3, const Expression &restraint, const Values &values) - : ConcreteProperty(restraint, values) -{ - p[0] = point0; - p[1] = point1; - p[2] = point2; - p[3] = point3; - - force_expression = this->restraintFunction().differentiate(phi()); - - if (force_expression.isConstant()) - force_expression = force_expression.evaluate(Values()); - - intra_molecule_points = Point::areIntraMoleculePoints(p[0], p[1]) and Point::areIntraMoleculePoints(p[0], p[2]) and - Point::areIntraMoleculePoints(p[0], p[3]); - - this->calculatePhi(); -} - -/** Internal constructor used to construct a restraint using the specified - points, energy expression and force expression */ -DihedralRestraint::DihedralRestraint(const PointRef &point0, const PointRef &point1, const PointRef &point2, - const PointRef &point3, const Expression &nrg_restraint, - const Expression &force_restraint) - : ConcreteProperty(nrg_restraint), force_expression(force_restraint) -{ - p[0] = point0; - p[1] = point1; - p[2] = point2; - p[3] = point3; - - if (force_expression.isConstant()) - { - force_expression = force_expression.evaluate(Values()); - } - else + four atoms 'atom0', 'atom1', 'atom2' 'atom3' (phi == a(0123)), + restraining the angle within these atoms */ +DihedralRestraint::DihedralRestraint(const QList &atoms, + const SireUnits::Dimension::Angle &phi0, + const SireUnits::Dimension::HarmonicAngleConstant &kphi) + : ConcreteProperty(), + _phi0(phi0), _kphi(kphi) +{ + // Need to think here about validating the angle and force constant values + // if (atoms.count() != 3) + // { + // throw SireError::invalid_arg(QObject::tr( + // "Wrong number of inputs for an Angle restraint. You need to " + // "provide 3 atoms (%1).") + // .arg(atoms.count()), + // // .arg(phi0.count()) + // // .arg(kphi.count()), + // CODELOC); + // } + + // make sure that we have 3 distinct atoms + QSet distinct; + distinct.reserve(4); + + for (const auto &atom : atoms) { - if (not this->restraintFunction().symbols().contains(force_expression.symbols())) - throw SireError::incompatible_error( - QObject::tr("You cannot use a force function which uses more symbols " - "(%1) than the energy function (%2).") - .arg(Sire::toString(force_expression.symbols()), Sire::toString(restraintFunction().symbols())), - CODELOC); + if (atom >= 0) + distinct.insert(atom); } - intra_molecule_points = Point::areIntraMoleculePoints(p[0], p[1]) and Point::areIntraMoleculePoints(p[0], p[2]) and - Point::areIntraMoleculePoints(p[0], p[3]); + // if (distinct.count() != 3) + // throw SireError::invalid_arg(QObject::tr( + // "There is something wrong with the atoms provided. " + // "They should all be unique and all greater than or equal to 0."), + // CODELOC); - this->calculatePhi(); + atms = atoms.toVector(); } -/** Copy constructor */ +/* Copy constructor*/ DihedralRestraint::DihedralRestraint(const DihedralRestraint &other) - : ConcreteProperty(other), force_expression(other.force_expression), - intra_molecule_points(other.intra_molecule_points) + : ConcreteProperty(other), + atms(other.atms), _phi0(other._phi0), _kphi(other._kphi) + { - for (int i = 0; i < this->nPoints(); ++i) - { - p[i] = other.p[i]; - } } -/** Destructor */ +/* Destructor */ DihedralRestraint::~DihedralRestraint() { } -/** Copy assignment operator */ DihedralRestraint &DihedralRestraint::operator=(const DihedralRestraint &other) { if (this != &other) { - ExpressionRestraint3D::operator=(other); - - for (int i = 0; i < this->nPoints(); ++i) - { - p[i] = other.p[i]; - } - - force_expression = other.force_expression; - intra_molecule_points = other.intra_molecule_points; + Property::operator=(other); + atms = other.atms; + _phi0 = other._phi0; + _kphi = other._kphi; } return *this; } -/** Comparison operator */ bool DihedralRestraint::operator==(const DihedralRestraint &other) const { - return this == &other or (ExpressionRestraint3D::operator==(other) and p[0] == other.p[0] and p[1] == other.p[1] and - p[2] == other.p[2] and p[3] == other.p[3] and force_expression == other.force_expression); + return atms == other.atms and + _phi0 == other._phi0 and + _kphi == other._kphi; } -/** Comparison operator */ bool DihedralRestraint::operator!=(const DihedralRestraint &other) const { - return not DihedralRestraint::operator==(other); + return not operator==(other); +} + +DihedralRestraints DihedralRestraint::operator+(const DihedralRestraint &other) const +{ + return DihedralRestraints(*this) + other; +} + +DihedralRestraints DihedralRestraint::operator+(const DihedralRestraints &other) const +{ + return DihedralRestraints(*this) + other; } const char *DihedralRestraint::typeName() @@ -260,317 +195,315 @@ const char *DihedralRestraint::typeName() return QMetaType::typeName(qMetaTypeId()); } -/** This restraint involves four points */ -int DihedralRestraint::nPoints() const +const char *DihedralRestraint::what() const { - return 4; + return DihedralRestraint::typeName(); } -/** Return the ith point */ -const Point &DihedralRestraint::point(int i) const +DihedralRestraint *DihedralRestraint::clone() const { - i = Index(i).map(this->nPoints()); + return new DihedralRestraint(*this); +} - return p[i].read(); +bool DihedralRestraint::isNull() const +{ + return atms.isEmpty(); } -/** Return the first point */ -const Point &DihedralRestraint::point0() const +QString DihedralRestraint::toString() const { - return p[0].read(); + if (this->isNull()) + return QObject::tr("DihedralRestraint::null"); + else + { + QStringList a; + + for (const auto &atom : atms) + { + a.append(QString::number(atom)); + } + return QString("DihedralRestraint( [%1], phi0=%2, kphi=%3 )") + .arg(a.join(", ")) + .arg(_phi0.toString()) + .arg(_kphi.toString()); + } } -/** Return the second point */ -const Point &DihedralRestraint::point1() const +/** Return the force constant for the restraint */ +SireUnits::Dimension::HarmonicAngleConstant DihedralRestraint::kphi() const { - return p[1].read(); + return this->_kphi; } -/** Return the third point */ -const Point &DihedralRestraint::point2() const +/** Return the equilibrium angle for the restraint */ +SireUnits::Dimension::Angle DihedralRestraint::phi0() const { - return p[2].read(); + return this->_phi0; } -/** Return the fourth point */ -const Point &DihedralRestraint::point3() const +/** Return the atoms involved in the restraint */ +QVector DihedralRestraint::atoms() const { - return p[3].read(); + return this->atms; } -/** Return the built-in symbols for this restraint */ -Symbols DihedralRestraint::builtinSymbols() const +/////// +/////// Implementation of DihedralRestraints +/////// + +/** Serialise to a binary datastream */ + +static const RegisterMetaType r_dihrests; + +QDataStream &operator<<(QDataStream &ds, const DihedralRestraints &dihrests) { - if (this->restraintFunction().isFunction(phi())) - return phi(); - else - return Symbols(); + writeHeader(ds, r_dihrests, 1); + + SharedDataStream sds(ds); + + sds << dihrests.r + << static_cast(dihrests); + + return ds; } -/** Return the values of the built-in symbols of this restraint */ -Values DihedralRestraint::builtinValues() const +/** Extract from a binary datastream */ +QDataStream &operator>>(QDataStream &ds, DihedralRestraints &dihrests) { - if (this->restraintFunction().isFunction(phi())) - return phi() == this->values()[phi()]; + VersionID v = readHeader(ds, r_dihrests); + + if (v == 1) + { + SharedDataStream sds(ds); + + sds >> dihrests.r >> + static_cast(dihrests); + } else - return Values(); + throw version_error(v, "1", r_dihrests, CODELOC); + + return ds; } -/** Return the differential of this restraint with respect to - the symbol 'symbol' +/** Null constructor */ +DihedralRestraints::DihedralRestraints() + : ConcreteProperty() +{ +} - \throw SireCAS::unavailable_differential -*/ -RestraintPtr DihedralRestraint::differentiate(const Symbol &symbol) const +DihedralRestraints::DihedralRestraints(const QString &name) + : ConcreteProperty(name) { - if (this->restraintFunction().isFunction(symbol)) - return DihedralRestraint(p[0], p[1], p[2], p[3], restraintFunction().differentiate(symbol), this->values()); - else - return NullRestraint(); } -/** Set the space used to evaluate the energy of this restraint +DihedralRestraints::DihedralRestraints(const DihedralRestraint &restraint) + : ConcreteProperty() +{ + if (not restraint.isNull()) + r.append(restraint); +} - \throw SireVol::incompatible_space -*/ -void DihedralRestraint::setSpace(const Space &new_space) +DihedralRestraints::DihedralRestraints(const QList &restraints) + : ConcreteProperty() { - if (not this->space().equals(new_space)) + for (const auto &restraint : restraints) { - DihedralRestraint old_state(*this); - - try - { - for (int i = 0; i < this->nPoints(); ++i) - { - p[i].edit().setSpace(new_space); - } + if (not restraint.isNull()) + r.append(restraint); + } +} - Restraint3D::setSpace(new_space); +DihedralRestraints::DihedralRestraints(const QString &name, + const DihedralRestraint &restraint) + : ConcreteProperty(name) +{ + if (not restraint.isNull()) + r.append(restraint); +} - this->calculatePhi(); - } - catch (...) - { - DihedralRestraint::operator=(old_state); - throw; - } +DihedralRestraints::DihedralRestraints(const QString &name, + const QList &restraints) + : ConcreteProperty(name) +{ + for (const auto &restraint : restraints) + { + if (not restraint.isNull()) + r.append(restraint); } } -/** Return the function used to calculate the restraint force */ -const Expression &DihedralRestraint::differentialRestraintFunction() const +DihedralRestraints::DihedralRestraints(const DihedralRestraints &other) + : ConcreteProperty(other), r(other.r) { - return force_expression; } -/** Calculate the force acting on the molecule in the forcetable 'forcetable' - caused by this restraint, and add it on to the forcetable scaled by - 'scale_force' */ -void DihedralRestraint::force(MolForceTable &forcetable, double scale_force) const +/* Desctructor */ +DihedralRestraints::~DihedralRestraints() { - bool in_p0 = p[0].read().contains(forcetable.molNum()); - bool in_p1 = p[1].read().contains(forcetable.molNum()); - bool in_p2 = p[2].read().contains(forcetable.molNum()); - bool in_p3 = p[3].read().contains(forcetable.molNum()); +} - if (not(in_p0 or in_p1 or in_p2 or in_p3)) - // this molecule is not affected by the restraint - return; +DihedralRestraints &DihedralRestraints::operator=(const DihedralRestraints &other) +{ + if (this != &other) + { + Restraints::operator=(other); + r = other.r; + } - throw SireError::incomplete_code(QObject::tr("Haven't yet written the code to calculate forces caused " - "by a dihedral restraint."), - CODELOC); + return *this; } -/** Calculate the force acting on the molecules in the forcetable 'forcetable' - caused by this restraint, and add it on to the forcetable scaled by - 'scale_force' */ -void DihedralRestraint::force(ForceTable &forcetable, double scale_force) const +bool DihedralRestraints::operator==(const DihedralRestraints &other) const { - bool in_p0 = p[0].read().usesMoleculesIn(forcetable); - bool in_p1 = p[1].read().usesMoleculesIn(forcetable); - bool in_p2 = p[2].read().usesMoleculesIn(forcetable); - bool in_p3 = p[3].read().usesMoleculesIn(forcetable); + return r == other.r and Restraints::operator==(other); +} - if (not(in_p0 or in_p1 or in_p2 or in_p3)) - // this molecule is not affected by the restraint - return; +bool DihedralRestraints::operator!=(const DihedralRestraints &other) const +{ + return not operator==(other); +} - throw SireError::incomplete_code(QObject::tr("Haven't yet written the code to calculate forces caused " - "by a dihedral restraint."), - CODELOC); +const char *DihedralRestraints::typeName() +{ + return QMetaType::typeName(qMetaTypeId()); } -/** Update the points of this restraint using new molecule data from 'moldata' +const char *DihedralRestraints::what() const +{ + return DihedralRestraints::typeName(); +} - \throw SireBase::missing_property - \throw SireError::invalid_cast - \throw SireError::incompatible_error -*/ -void DihedralRestraint::update(const MoleculeData &moldata) +DihedralRestraints *DihedralRestraints::clone() const { - if (this->contains(moldata.number())) - { - DihedralRestraint old_state(*this); + return new DihedralRestraints(*this); +} - try - { - for (int i = 0; i < this->nPoints(); ++i) - { - p[i].edit().update(moldata); - } +QString DihedralRestraints::toString() const +{ + if (this->isEmpty()) + return QObject::tr("DihedralRestraints::null"); - this->calculatePhi(); - } - catch (...) - { - DihedralRestraint::operator=(old_state); - throw; - } - } -} + QStringList parts; -/** Update the points of this restraint using new molecule data from 'molecules' + const auto n = this->count(); - \throw SireBase::missing_property - \throw SireError::invalid_cast - \throw SireError::incompatible_error -*/ -void DihedralRestraint::update(const Molecules &molecules) -{ - if (this->usesMoleculesIn(molecules)) + if (n <= 10) { - DihedralRestraint old_state(*this); - - try + for (int i = 0; i < n; i++) { - for (int i = 0; i < this->nPoints(); ++i) - { - p[i].edit().update(molecules); - } - - this->calculatePhi(); + parts.append(QObject::tr("%1: %2").arg(i).arg(this->r.at(i).toString())); } - catch (...) + } + else + { + for (int i = 0; i < 5; i++) { - DihedralRestraint::operator=(old_state); - throw; + parts.append(QObject::tr("%1: %2").arg(i).arg(this->r.at(i).toString())); } - } -} -/** Return the molecules used in this restraint */ -Molecules DihedralRestraint::molecules() const -{ - Molecules mols; + parts.append("..."); - for (int i = 0; i < this->nPoints(); ++i) - { - mols += p[i].read().molecules(); + for (int i = n - 5; i < n; i++) + { + parts.append(QObject::tr("%1: %2").arg(i).arg(this->r.at(i).toString())); + } } - return mols; + return QObject::tr("DihedralRestraints( name=%1, size=%2\n%3\n )") + .arg(this->name()) + .arg(n) + .arg(parts.join("\n")); } -/** Return whether or not this restraint affects the molecule - with number 'molnum' */ -bool DihedralRestraint::contains(MolNum molnum) const +/** Return whether or not this is empty */ +bool DihedralRestraints::isEmpty() const { - return p[0].read().contains(molnum) or p[1].read().contains(molnum) or p[2].read().contains(molnum) or - p[3].read().contains(molnum); + return this->r.isEmpty(); } -/** Return whether or not this restraint affects the molecule - with ID 'molid' */ -bool DihedralRestraint::contains(const MolID &molid) const +/** Return whether or not this is empty */ +bool DihedralRestraints::isNull() const { - return p[0].read().contains(molid) or p[1].read().contains(molid) or p[2].read().contains(molid) or - p[3].read().contains(molid); + return this->isEmpty(); } -/** Return whether or not this restraint involves any of the molecules - that are in the forcetable 'forcetable' */ -bool DihedralRestraint::usesMoleculesIn(const ForceTable &forcetable) const +/** Return the number of restraints */ +int DihedralRestraints::nRestraints() const { - return p[0].read().usesMoleculesIn(forcetable) or p[1].read().usesMoleculesIn(forcetable) or - p[2].read().usesMoleculesIn(forcetable) or p[3].read().usesMoleculesIn(forcetable); + return this->r.count(); } -/** Return whether or not this restraint involves any of the molecules - in 'molecules' */ -bool DihedralRestraint::usesMoleculesIn(const Molecules &molecules) const +/** Return the number of restraints */ +int DihedralRestraints::count() const { - return p[0].read().usesMoleculesIn(molecules) or p[1].read().usesMoleculesIn(molecules) or - p[2].read().usesMoleculesIn(molecules) or p[3].read().usesMoleculesIn(molecules); + return this->nRestraints(); } -static Expression harmonicFunction(double force_constant) +/** Return the number of restraints */ +int DihedralRestraints::size() const { - if (SireMaths::isZero(force_constant)) - return 0; - else - return force_constant * pow(DihedralRestraint::phi(), 2); + return this->nRestraints(); } -static Expression diffHarmonicFunction(double force_constant) +/** Return the ith restraint */ +const DihedralRestraint &DihedralRestraints::at(int i) const { - if (SireMaths::isZero(force_constant)) - return 0; - else - return (2 * force_constant) * DihedralRestraint::phi(); + i = SireID::Index(i).map(this->r.count()); + + return this->r.at(i); } -/** Return a distance restraint that applies a harmonic potential between - the points 'point0' and 'point1' using a force constant 'force_constant' */ -DihedralRestraint DihedralRestraint::harmonic(const PointRef &point0, const PointRef &point1, const PointRef &point2, - const PointRef &point3, const HarmonicAngleForceConstant &force_constant) +/** Return the ith restraint */ +const DihedralRestraint &DihedralRestraints::operator[](int i) const { - return DihedralRestraint(point0, point1, point2, point3, ::harmonicFunction(force_constant), - ::diffHarmonicFunction(force_constant)); + return this->at(i); } -static Expression halfHarmonicFunction(double force_constant, double angle) +/** Return all of the restraints */ +QList DihedralRestraints::restraints() const { - if (SireMaths::isZero(force_constant)) - return 0; - - else if (angle <= 0) - // this is just a harmonic function - return ::harmonicFunction(force_constant); - - else - { - const Symbol &phi = DihedralRestraint::phi(); - return Conditional(GreaterThan(phi, angle), force_constant * pow(phi - angle, 2), 0); - } + return this->r; } -static Expression diffHalfHarmonicFunction(double force_constant, double angle) +/** Add a restraints onto the list */ +void DihedralRestraints::add(const DihedralRestraint &restraint) { - if (SireMaths::isZero(force_constant)) - return 0; + if (not restraint.isNull()) + r.append(restraint); +} - else if (angle <= 0) - // this is just a harmonic function - return ::diffHarmonicFunction(force_constant); +/** Add a restraint onto the list */ +void DihedralRestraints::add(const DihedralRestraints &restraints) +{ + this->r += restraints.r; +} - else - { - const Symbol &phi = DihedralRestraint::phi(); - return Conditional(GreaterThan(phi, angle), (2 * force_constant) * (phi - angle), 0); - } +/** Add a restraint onto the list */ +DihedralRestraints &DihedralRestraints::operator+=(const DihedralRestraint &restraint) +{ + this->add(restraint); + return *this; } -/** Return a distance restraint that applied a half-harmonic potential - between the points 'point0' and 'point1' above a distance 'distance' - using a force constant 'force_constant' */ -DihedralRestraint DihedralRestraint::halfHarmonic(const PointRef &point0, const PointRef &point1, - const PointRef &point2, const PointRef &point3, const Angle &angle, - const HarmonicAngleForceConstant &force_constant) +/** Add a restraint onto the list */ +DihedralRestraints DihedralRestraints::operator+(const DihedralRestraint &restraint) const { - double ang = angle.to(radians); + DihedralRestraints ret(*this); + ret += restraint; + return *this; +} - return DihedralRestraint(point0, point1, point2, point3, ::halfHarmonicFunction(force_constant, ang), - ::diffHalfHarmonicFunction(force_constant, ang)); +/** Add restraints onto the list */ +DihedralRestraints &DihedralRestraints::operator+=(const DihedralRestraints &restraints) +{ + this->add(restraints); + return *this; } + +/** Add restraints onto the list */ +DihedralRestraints DihedralRestraints::operator+(const DihedralRestraints &restraints) const +{ + DihedralRestraints ret(*this); + ret += restraints; + return *this; +} \ No newline at end of file diff --git a/corelib/src/libs/SireMM/dihedralrestraint.h b/corelib/src/libs/SireMM/dihedralrestraint.h index 606ee3b78..b340a3411 100644 --- a/corelib/src/libs/SireMM/dihedralrestraint.h +++ b/corelib/src/libs/SireMM/dihedralrestraint.h @@ -28,41 +28,49 @@ #ifndef SIREMM_DIHEDRALRESTRAINT_H #define SIREMM_DIHEDRALRESTRAINT_H -#include "anglerestraint.h" -#include "restraint.h" +// #include "SireFF/point.h" + +#include "restraints.h" + +// #include "SireCAS/expression.h" +// #include "SireCAS/symbol.h" + +#include "SireUnits/dimensions.h" +#include "SireUnits/generalunit.h" SIRE_BEGIN_HEADER namespace SireMM { class DihedralRestraint; + class DihedralRestraints; } SIREMM_EXPORT QDataStream &operator<<(QDataStream &, const SireMM::DihedralRestraint &); SIREMM_EXPORT QDataStream &operator>>(QDataStream &, SireMM::DihedralRestraint &); +SIREMM_EXPORT QDataStream &operator<<(QDataStream &, const SireMM::DihedralRestraints &); +SIREMM_EXPORT QDataStream &operator>>(QDataStream &, SireMM::DihedralRestraints &); + namespace SireMM { - /** This is a restraint that operates on the dihedral angle between - four SireMM::Point objects (e.g. four atoms in a molecule) - - @author Christopher Woods - */ - class SIREMM_EXPORT DihedralRestraint : public SireBase::ConcreteProperty + /** This class represents a single angle restraint between any three + * atoms in a system + * @author Christopher Woods + */ + class SIREMM_EXPORT DihedralRestraint + : public SireBase::ConcreteProperty { - friend SIREMM_EXPORT QDataStream & ::operator<<(QDataStream &, const DihedralRestraint &); - friend SIREMM_EXPORT QDataStream & ::operator>>(QDataStream &, DihedralRestraint &); + friend SIREMM_EXPORT QDataStream & ::operator<<(QDataStream &, const SireMM::DihedralRestraint &); + friend SIREMM_EXPORT QDataStream & ::operator>>(QDataStream &, SireMM::DihedralRestraint &); public: DihedralRestraint(); - - DihedralRestraint(const PointRef &point0, const PointRef &point1, const PointRef &point2, const PointRef &point3, - const Expression &restraint); - - DihedralRestraint(const PointRef &point0, const PointRef &point1, const PointRef &point2, const PointRef &point3, - const Expression &restraint, const Values &values); + DihedralRestraint(const QList &atoms, + const SireUnits::Dimension::Angle &phi0, + const SireUnits::Dimension::HarmonicAngleConstant &kphi); DihedralRestraint(const DihedralRestraint &other); @@ -73,72 +81,102 @@ namespace SireMM bool operator==(const DihedralRestraint &other) const; bool operator!=(const DihedralRestraint &other) const; + DihedralRestraints operator+(const DihedralRestraint &other) const; + DihedralRestraints operator+(const DihedralRestraints &other) const; + static const char *typeName(); + const char *what() const; - const Point &point(int i) const; + DihedralRestraint *clone() const; - const Point &point0() const; - const Point &point1() const; - const Point &point2() const; - const Point &point3() const; + QString toString() const; - int nPoints() const; + bool isNull() const; - static const Symbol &phi(); + QVector atoms() const; - Symbols builtinSymbols() const; - Values builtinValues() const; + SireUnits::Dimension::Angle phi0() const; + SireUnits::Dimension::HarmonicAngleConstant kphi() const; - RestraintPtr differentiate(const Symbol &symbol) const; + private: + /** Atoms involved in the angle restraint */ + QVector atms; - void setSpace(const Space &space); + /** Equilibrium angle */ + SireUnits::Dimension::Angle _phi0; - const Expression &differentialRestraintFunction() const; + /** Harmonic angle constant */ + SireUnits::Dimension::HarmonicAngleConstant _kphi; + }; - void force(MolForceTable &forcetable, double scale_force = 1) const; - void force(ForceTable &forcetable, double scale_force = 1) const; + /** This class represents a collection of angle restraints */ + class SIREMM_EXPORT DihedralRestraints + : public SireBase::ConcreteProperty + { + friend SIREMM_EXPORT QDataStream & ::operator<<(QDataStream &, const SireMM::DihedralRestraints &); + friend SIREMM_EXPORT QDataStream & ::operator>>(QDataStream &, SireMM::DihedralRestraints &); - void update(const MoleculeData &moldata); - void update(const Molecules &molecules); + public: + DihedralRestraints(); + DihedralRestraints(const QString &name); - Molecules molecules() const; + DihedralRestraints(const DihedralRestraint &restraint); + DihedralRestraints(const QList &restraints); - bool contains(MolNum molnum) const; - bool contains(const MolID &molid) const; + DihedralRestraints(const QString &name, + const DihedralRestraint &restraint); - bool usesMoleculesIn(const ForceTable &forcetable) const; - bool usesMoleculesIn(const Molecules &molecules) const; + DihedralRestraints(const QString &name, + const QList &restraints); - static DihedralRestraint harmonic(const PointRef &point0, const PointRef &point1, const PointRef &point2, - const PointRef &point3, const HarmonicAngleForceConstant &force_constant); + DihedralRestraints(const DihedralRestraints &other); - static DihedralRestraint halfHarmonic(const PointRef &point0, const PointRef &point1, const PointRef &point2, - const PointRef &point3, const SireUnits::Dimension::Angle &angle, - const HarmonicAngleForceConstant &force_constant); + ~DihedralRestraints(); - protected: - DihedralRestraint(const PointRef &point0, const PointRef &point1, const PointRef &point2, const PointRef &point3, - const Expression &nrg_restraint, const Expression &force_restraint); + DihedralRestraints &operator=(const DihedralRestraints &other); - private: - void calculatePhi(); + bool operator==(const DihedralRestraints &other) const; + bool operator!=(const DihedralRestraints &other) const; - /** The four points between which the restraint is calculated */ - SireFF::PointPtr p[4]; + static const char *typeName(); + const char *what() const; - /** The expression used to calculate the force */ - Expression force_expression; + DihedralRestraints *clone() const; - /** Whether or not all four points are within the same molecule */ - bool intra_molecule_points; - }; + QString toString() const; + + bool isEmpty() const; + bool isNull() const; + + int count() const; + int size() const; + int nRestraints() const; + + const DihedralRestraint &at(int i) const; + const DihedralRestraint &operator[](int i) const; + + QList restraints() const; + + void add(const DihedralRestraint &restraint); + void add(const DihedralRestraints &restraints); -} // namespace SireMM + DihedralRestraints &operator+=(const DihedralRestraint &restraint); + DihedralRestraints &operator+=(const DihedralRestraints &restraints); + + DihedralRestraints operator+(const DihedralRestraint &restraint) const; + DihedralRestraints operator+(const DihedralRestraints &restraints) const; + + private: + /** List of restraints */ + QList r; + }; +} Q_DECLARE_METATYPE(SireMM::DihedralRestraint) +Q_DECLARE_METATYPE(SireMM::DihedralRestraints) SIRE_EXPOSE_CLASS(SireMM::DihedralRestraint) - +SIRE_EXPOSE_CLASS(SireMM::DihedralRestraints) SIRE_END_HEADER -#endif +#endif \ No newline at end of file diff --git a/wrapper/MM/AngleRestraints.pypp.cpp b/wrapper/MM/AngleRestraints.pypp.cpp new file mode 100644 index 000000000..fdf557c21 --- /dev/null +++ b/wrapper/MM/AngleRestraints.pypp.cpp @@ -0,0 +1,254 @@ +// This file has been generated by Py++. + +// (C) Christopher Woods, GPL >= 3 License + +#include "boost/python.hpp" +#include "Helpers/clone_const_reference.hpp" +#include "AngleRestraints.pypp.hpp" + +namespace bp = boost::python; + +#include "SireCAS/conditional.h" + +#include "SireCAS/errors.h" + +#include "SireCAS/power.h" + +#include "SireCAS/symbols.h" + +#include "SireCAS/values.h" + +#include "SireFF/forcetable.h" + +#include "SireID/index.h" + +#include "SireStream/datastream.h" + +#include "SireStream/shareddatastream.h" + +#include "SireUnits/angle.h" + +#include "SireUnits/units.h" + +#include "anglerestraint.h" + +#include + +#include "anglerestraint.h" + +SireMM::AngleRestraints __copy__(const SireMM::AngleRestraints &other){ return SireMM::AngleRestraints(other); } + +#include "Helpers/copy.hpp" + +#include "Qt/qdatastream.hpp" + +#include "Helpers/str.hpp" + +#include "Helpers/release_gil_policy.hpp" + +#include "Helpers/len.hpp" + +void register_AngleRestraints_class(){ + + { //::SireMM::AngleRestraints + typedef bp::class_< SireMM::AngleRestraints, bp::bases< SireMM::Restraints, SireBase::Property > > AngleRestraints_exposer_t; + AngleRestraints_exposer_t AngleRestraints_exposer = AngleRestraints_exposer_t( "AngleRestraints", "This class represents a collection of angle restraints", bp::init< >("Null constructor") ); + bp::scope AngleRestraints_scope( AngleRestraints_exposer ); + AngleRestraints_exposer.def( bp::init< QString const & >(( bp::arg("name") ), "") ); + AngleRestraints_exposer.def( bp::init< SireMM::AngleRestraint const & >(( bp::arg("restraint") ), "") ); + AngleRestraints_exposer.def( bp::init< QList< SireMM::AngleRestraint > const & >(( bp::arg("restraints") ), "") ); + AngleRestraints_exposer.def( bp::init< QString const &, SireMM::AngleRestraint const & >(( bp::arg("name"), bp::arg("restraint") ), "") ); + AngleRestraints_exposer.def( bp::init< QString const &, QList< SireMM::AngleRestraint > const & >(( bp::arg("name"), bp::arg("restraints") ), "") ); + AngleRestraints_exposer.def( bp::init< SireMM::AngleRestraints const & >(( bp::arg("other") ), "") ); + { //::SireMM::AngleRestraints::add + + typedef void ( ::SireMM::AngleRestraints::*add_function_type)( ::SireMM::AngleRestraint const & ) ; + add_function_type add_function_value( &::SireMM::AngleRestraints::add ); + + AngleRestraints_exposer.def( + "add" + , add_function_value + , ( bp::arg("restraint") ) + , bp::release_gil_policy() + , "Add a restraint onto the list" ); + + } + { //::SireMM::AngleRestraints::add + + typedef void ( ::SireMM::AngleRestraints::*add_function_type)( ::SireMM::AngleRestraints const & ) ; + add_function_type add_function_value( &::SireMM::AngleRestraints::add ); + + AngleRestraints_exposer.def( + "add" + , add_function_value + , ( bp::arg("restraints") ) + , bp::release_gil_policy() + , "Add a restraint onto the list" ); + + } + { //::SireMM::AngleRestraints::at + + typedef ::SireMM::AngleRestraint const & ( ::SireMM::AngleRestraints::*at_function_type)( int ) const; + at_function_type at_function_value( &::SireMM::AngleRestraints::at ); + + AngleRestraints_exposer.def( + "at" + , at_function_value + , ( bp::arg("i") ) + , bp::return_value_policy() + , "Return the ith restraint" ); + + } + { //::SireMM::AngleRestraints::count + + typedef int ( ::SireMM::AngleRestraints::*count_function_type)( ) const; + count_function_type count_function_value( &::SireMM::AngleRestraints::count ); + + AngleRestraints_exposer.def( + "count" + , count_function_value + , bp::release_gil_policy() + , "Return the number of restraints" ); + + } + { //::SireMM::AngleRestraints::isEmpty + + typedef bool ( ::SireMM::AngleRestraints::*isEmpty_function_type)( ) const; + isEmpty_function_type isEmpty_function_value( &::SireMM::AngleRestraints::isEmpty ); + + AngleRestraints_exposer.def( + "isEmpty" + , isEmpty_function_value + , bp::release_gil_policy() + , "Return whether or not this is empty" ); + + } + { //::SireMM::AngleRestraints::isNull + + typedef bool ( ::SireMM::AngleRestraints::*isNull_function_type)( ) const; + isNull_function_type isNull_function_value( &::SireMM::AngleRestraints::isNull ); + + AngleRestraints_exposer.def( + "isNull" + , isNull_function_value + , bp::release_gil_policy() + , "Return whether or not this is empty" ); + + } + { //::SireMM::AngleRestraints::nRestraints + + typedef int ( ::SireMM::AngleRestraints::*nRestraints_function_type)( ) const; + nRestraints_function_type nRestraints_function_value( &::SireMM::AngleRestraints::nRestraints ); + + AngleRestraints_exposer.def( + "nRestraints" + , nRestraints_function_value + , bp::release_gil_policy() + , "Return the number of restraints" ); + + } + AngleRestraints_exposer.def( bp::self != bp::self ); + AngleRestraints_exposer.def( bp::self + bp::other< SireMM::AngleRestraint >() ); + AngleRestraints_exposer.def( bp::self + bp::self ); + { //::SireMM::AngleRestraints::operator= + + typedef ::SireMM::AngleRestraints & ( ::SireMM::AngleRestraints::*assign_function_type)( ::SireMM::AngleRestraints const & ) ; + assign_function_type assign_function_value( &::SireMM::AngleRestraints::operator= ); + + AngleRestraints_exposer.def( + "assign" + , assign_function_value + , ( bp::arg("other") ) + , bp::return_self< >() + , "" ); + + } + AngleRestraints_exposer.def( bp::self == bp::self ); + { //::SireMM::AngleRestraints::operator[] + + typedef ::SireMM::AngleRestraint const & ( ::SireMM::AngleRestraints::*__getitem___function_type)( int ) const; + __getitem___function_type __getitem___function_value( &::SireMM::AngleRestraints::operator[] ); + + AngleRestraints_exposer.def( + "__getitem__" + , __getitem___function_value + , ( bp::arg("i") ) + , bp::return_value_policy() + , "" ); + + } + { //::SireMM::AngleRestraints::restraints + + typedef ::QList< SireMM::AngleRestraint > ( ::SireMM::AngleRestraints::*restraints_function_type)( ) const; + restraints_function_type restraints_function_value( &::SireMM::AngleRestraints::restraints ); + + AngleRestraints_exposer.def( + "restraints" + , restraints_function_value + , bp::release_gil_policy() + , "Return all of the restraints" ); + + } + { //::SireMM::AngleRestraints::size + + typedef int ( ::SireMM::AngleRestraints::*size_function_type)( ) const; + size_function_type size_function_value( &::SireMM::AngleRestraints::size ); + + AngleRestraints_exposer.def( + "size" + , size_function_value + , bp::release_gil_policy() + , "Return the number of restraints" ); + + } + { //::SireMM::AngleRestraints::toString + + typedef ::QString ( ::SireMM::AngleRestraints::*toString_function_type)( ) const; + toString_function_type toString_function_value( &::SireMM::AngleRestraints::toString ); + + AngleRestraints_exposer.def( + "toString" + , toString_function_value + , bp::release_gil_policy() + , "" ); + + } + { //::SireMM::AngleRestraints::typeName + + typedef char const * ( *typeName_function_type )( ); + typeName_function_type typeName_function_value( &::SireMM::AngleRestraints::typeName ); + + AngleRestraints_exposer.def( + "typeName" + , typeName_function_value + , bp::release_gil_policy() + , "" ); + + } + { //::SireMM::AngleRestraints::what + + typedef char const * ( ::SireMM::AngleRestraints::*what_function_type)( ) const; + what_function_type what_function_value( &::SireMM::AngleRestraints::what ); + + AngleRestraints_exposer.def( + "what" + , what_function_value + , bp::release_gil_policy() + , "" ); + + } + AngleRestraints_exposer.staticmethod( "typeName" ); + AngleRestraints_exposer.def( "__copy__", &__copy__); + AngleRestraints_exposer.def( "__deepcopy__", &__copy__); + AngleRestraints_exposer.def( "clone", &__copy__); + AngleRestraints_exposer.def( "__rlshift__", &__rlshift__QDataStream< ::SireMM::AngleRestraints >, + bp::return_internal_reference<1, bp::with_custodian_and_ward<1,2> >() ); + AngleRestraints_exposer.def( "__rrshift__", &__rrshift__QDataStream< ::SireMM::AngleRestraints >, + bp::return_internal_reference<1, bp::with_custodian_and_ward<1,2> >() ); + AngleRestraints_exposer.def_pickle(sire_pickle_suite< ::SireMM::AngleRestraints >()); + AngleRestraints_exposer.def( "__str__", &__str__< ::SireMM::AngleRestraints > ); + AngleRestraints_exposer.def( "__repr__", &__str__< ::SireMM::AngleRestraints > ); + AngleRestraints_exposer.def( "__len__", &__len_size< ::SireMM::AngleRestraints > ); + } + +} diff --git a/wrapper/MM/AngleRestraints.pypp.hpp b/wrapper/MM/AngleRestraints.pypp.hpp new file mode 100644 index 000000000..b1292a5cd --- /dev/null +++ b/wrapper/MM/AngleRestraints.pypp.hpp @@ -0,0 +1,10 @@ +// This file has been generated by Py++. + +// (C) Christopher Woods, GPL >= 3 License + +#ifndef AngleRestraints_hpp__pyplusplus_wrapper +#define AngleRestraints_hpp__pyplusplus_wrapper + +void register_AngleRestraints_class(); + +#endif//AngleRestraints_hpp__pyplusplus_wrapper diff --git a/wrapper/MM/DihedralRestraints.pypp.cpp b/wrapper/MM/DihedralRestraints.pypp.cpp new file mode 100644 index 000000000..21f56e7e0 --- /dev/null +++ b/wrapper/MM/DihedralRestraints.pypp.cpp @@ -0,0 +1,254 @@ +// This file has been generated by Py++. + +// (C) Christopher Woods, GPL >= 3 License + +#include "boost/python.hpp" +#include "Helpers/clone_const_reference.hpp" +#include "DihedralRestraints.pypp.hpp" + +namespace bp = boost::python; + +#include "SireCAS/conditional.h" + +#include "SireCAS/errors.h" + +#include "SireCAS/power.h" + +#include "SireCAS/symbols.h" + +#include "SireCAS/values.h" + +#include "SireFF/forcetable.h" + +#include "SireID/index.h" + +#include "SireStream/datastream.h" + +#include "SireStream/shareddatastream.h" + +#include "SireUnits/angle.h" + +#include "SireUnits/units.h" + +#include "dihedralrestraint.h" + +#include + +#include "dihedralrestraint.h" + +SireMM::DihedralRestraints __copy__(const SireMM::DihedralRestraints &other){ return SireMM::DihedralRestraints(other); } + +#include "Helpers/copy.hpp" + +#include "Qt/qdatastream.hpp" + +#include "Helpers/str.hpp" + +#include "Helpers/release_gil_policy.hpp" + +#include "Helpers/len.hpp" + +void register_DihedralRestraints_class(){ + + { //::SireMM::DihedralRestraints + typedef bp::class_< SireMM::DihedralRestraints, bp::bases< SireMM::Restraints, SireBase::Property > > DihedralRestraints_exposer_t; + DihedralRestraints_exposer_t DihedralRestraints_exposer = DihedralRestraints_exposer_t( "DihedralRestraints", "This class represents a collection of angle restraints", bp::init< >("Null constructor") ); + bp::scope DihedralRestraints_scope( DihedralRestraints_exposer ); + DihedralRestraints_exposer.def( bp::init< QString const & >(( bp::arg("name") ), "") ); + DihedralRestraints_exposer.def( bp::init< SireMM::DihedralRestraint const & >(( bp::arg("restraint") ), "") ); + DihedralRestraints_exposer.def( bp::init< QList< SireMM::DihedralRestraint > const & >(( bp::arg("restraints") ), "") ); + DihedralRestraints_exposer.def( bp::init< QString const &, SireMM::DihedralRestraint const & >(( bp::arg("name"), bp::arg("restraint") ), "") ); + DihedralRestraints_exposer.def( bp::init< QString const &, QList< SireMM::DihedralRestraint > const & >(( bp::arg("name"), bp::arg("restraints") ), "") ); + DihedralRestraints_exposer.def( bp::init< SireMM::DihedralRestraints const & >(( bp::arg("other") ), "") ); + { //::SireMM::DihedralRestraints::add + + typedef void ( ::SireMM::DihedralRestraints::*add_function_type)( ::SireMM::DihedralRestraint const & ) ; + add_function_type add_function_value( &::SireMM::DihedralRestraints::add ); + + DihedralRestraints_exposer.def( + "add" + , add_function_value + , ( bp::arg("restraint") ) + , bp::release_gil_policy() + , "Add a restraints onto the list" ); + + } + { //::SireMM::DihedralRestraints::add + + typedef void ( ::SireMM::DihedralRestraints::*add_function_type)( ::SireMM::DihedralRestraints const & ) ; + add_function_type add_function_value( &::SireMM::DihedralRestraints::add ); + + DihedralRestraints_exposer.def( + "add" + , add_function_value + , ( bp::arg("restraints") ) + , bp::release_gil_policy() + , "Add a restraint onto the list" ); + + } + { //::SireMM::DihedralRestraints::at + + typedef ::SireMM::DihedralRestraint const & ( ::SireMM::DihedralRestraints::*at_function_type)( int ) const; + at_function_type at_function_value( &::SireMM::DihedralRestraints::at ); + + DihedralRestraints_exposer.def( + "at" + , at_function_value + , ( bp::arg("i") ) + , bp::return_value_policy() + , "Return the ith restraint" ); + + } + { //::SireMM::DihedralRestraints::count + + typedef int ( ::SireMM::DihedralRestraints::*count_function_type)( ) const; + count_function_type count_function_value( &::SireMM::DihedralRestraints::count ); + + DihedralRestraints_exposer.def( + "count" + , count_function_value + , bp::release_gil_policy() + , "Return the number of restraints" ); + + } + { //::SireMM::DihedralRestraints::isEmpty + + typedef bool ( ::SireMM::DihedralRestraints::*isEmpty_function_type)( ) const; + isEmpty_function_type isEmpty_function_value( &::SireMM::DihedralRestraints::isEmpty ); + + DihedralRestraints_exposer.def( + "isEmpty" + , isEmpty_function_value + , bp::release_gil_policy() + , "Return whether or not this is empty" ); + + } + { //::SireMM::DihedralRestraints::isNull + + typedef bool ( ::SireMM::DihedralRestraints::*isNull_function_type)( ) const; + isNull_function_type isNull_function_value( &::SireMM::DihedralRestraints::isNull ); + + DihedralRestraints_exposer.def( + "isNull" + , isNull_function_value + , bp::release_gil_policy() + , "Return whether or not this is empty" ); + + } + { //::SireMM::DihedralRestraints::nRestraints + + typedef int ( ::SireMM::DihedralRestraints::*nRestraints_function_type)( ) const; + nRestraints_function_type nRestraints_function_value( &::SireMM::DihedralRestraints::nRestraints ); + + DihedralRestraints_exposer.def( + "nRestraints" + , nRestraints_function_value + , bp::release_gil_policy() + , "Return the number of restraints" ); + + } + DihedralRestraints_exposer.def( bp::self != bp::self ); + DihedralRestraints_exposer.def( bp::self + bp::other< SireMM::DihedralRestraint >() ); + DihedralRestraints_exposer.def( bp::self + bp::self ); + { //::SireMM::DihedralRestraints::operator= + + typedef ::SireMM::DihedralRestraints & ( ::SireMM::DihedralRestraints::*assign_function_type)( ::SireMM::DihedralRestraints const & ) ; + assign_function_type assign_function_value( &::SireMM::DihedralRestraints::operator= ); + + DihedralRestraints_exposer.def( + "assign" + , assign_function_value + , ( bp::arg("other") ) + , bp::return_self< >() + , "" ); + + } + DihedralRestraints_exposer.def( bp::self == bp::self ); + { //::SireMM::DihedralRestraints::operator[] + + typedef ::SireMM::DihedralRestraint const & ( ::SireMM::DihedralRestraints::*__getitem___function_type)( int ) const; + __getitem___function_type __getitem___function_value( &::SireMM::DihedralRestraints::operator[] ); + + DihedralRestraints_exposer.def( + "__getitem__" + , __getitem___function_value + , ( bp::arg("i") ) + , bp::return_value_policy() + , "" ); + + } + { //::SireMM::DihedralRestraints::restraints + + typedef ::QList< SireMM::DihedralRestraint > ( ::SireMM::DihedralRestraints::*restraints_function_type)( ) const; + restraints_function_type restraints_function_value( &::SireMM::DihedralRestraints::restraints ); + + DihedralRestraints_exposer.def( + "restraints" + , restraints_function_value + , bp::release_gil_policy() + , "Return all of the restraints" ); + + } + { //::SireMM::DihedralRestraints::size + + typedef int ( ::SireMM::DihedralRestraints::*size_function_type)( ) const; + size_function_type size_function_value( &::SireMM::DihedralRestraints::size ); + + DihedralRestraints_exposer.def( + "size" + , size_function_value + , bp::release_gil_policy() + , "Return the number of restraints" ); + + } + { //::SireMM::DihedralRestraints::toString + + typedef ::QString ( ::SireMM::DihedralRestraints::*toString_function_type)( ) const; + toString_function_type toString_function_value( &::SireMM::DihedralRestraints::toString ); + + DihedralRestraints_exposer.def( + "toString" + , toString_function_value + , bp::release_gil_policy() + , "" ); + + } + { //::SireMM::DihedralRestraints::typeName + + typedef char const * ( *typeName_function_type )( ); + typeName_function_type typeName_function_value( &::SireMM::DihedralRestraints::typeName ); + + DihedralRestraints_exposer.def( + "typeName" + , typeName_function_value + , bp::release_gil_policy() + , "" ); + + } + { //::SireMM::DihedralRestraints::what + + typedef char const * ( ::SireMM::DihedralRestraints::*what_function_type)( ) const; + what_function_type what_function_value( &::SireMM::DihedralRestraints::what ); + + DihedralRestraints_exposer.def( + "what" + , what_function_value + , bp::release_gil_policy() + , "" ); + + } + DihedralRestraints_exposer.staticmethod( "typeName" ); + DihedralRestraints_exposer.def( "__copy__", &__copy__); + DihedralRestraints_exposer.def( "__deepcopy__", &__copy__); + DihedralRestraints_exposer.def( "clone", &__copy__); + DihedralRestraints_exposer.def( "__rlshift__", &__rlshift__QDataStream< ::SireMM::DihedralRestraints >, + bp::return_internal_reference<1, bp::with_custodian_and_ward<1,2> >() ); + DihedralRestraints_exposer.def( "__rrshift__", &__rrshift__QDataStream< ::SireMM::DihedralRestraints >, + bp::return_internal_reference<1, bp::with_custodian_and_ward<1,2> >() ); + DihedralRestraints_exposer.def_pickle(sire_pickle_suite< ::SireMM::DihedralRestraints >()); + DihedralRestraints_exposer.def( "__str__", &__str__< ::SireMM::DihedralRestraints > ); + DihedralRestraints_exposer.def( "__repr__", &__str__< ::SireMM::DihedralRestraints > ); + DihedralRestraints_exposer.def( "__len__", &__len_size< ::SireMM::DihedralRestraints > ); + } + +} diff --git a/wrapper/MM/DihedralRestraints.pypp.hpp b/wrapper/MM/DihedralRestraints.pypp.hpp new file mode 100644 index 000000000..0aab6519a --- /dev/null +++ b/wrapper/MM/DihedralRestraints.pypp.hpp @@ -0,0 +1,10 @@ +// This file has been generated by Py++. + +// (C) Christopher Woods, GPL >= 3 License + +#ifndef DihedralRestraints_hpp__pyplusplus_wrapper +#define DihedralRestraints_hpp__pyplusplus_wrapper + +void register_DihedralRestraints_class(); + +#endif//DihedralRestraints_hpp__pyplusplus_wrapper From 8809ac5a4e9163fad41fbb65cef4030d11673930 Mon Sep 17 00:00:00 2001 From: Audrius Kalpokas Date: Tue, 16 Jul 2024 15:45:14 +0100 Subject: [PATCH 003/102] Update wrappers for angle and dihedral restraints classes --- wrapper/MM/AngleRestraint.pypp.cpp | 296 ++++--------------------- wrapper/MM/DihedralRestraint.pypp.cpp | 308 ++++---------------------- wrapper/MM/_MM.main.cpp | 38 +++- 3 files changed, 125 insertions(+), 517 deletions(-) diff --git a/wrapper/MM/AngleRestraint.pypp.cpp b/wrapper/MM/AngleRestraint.pypp.cpp index 052916bb4..33954da56 100644 --- a/wrapper/MM/AngleRestraint.pypp.cpp +++ b/wrapper/MM/AngleRestraint.pypp.cpp @@ -3,7 +3,6 @@ // (C) Christopher Woods, GPL >= 3 License #include "boost/python.hpp" -#include "Helpers/clone_const_reference.hpp" #include "AngleRestraint.pypp.hpp" namespace bp = boost::python; @@ -32,6 +31,8 @@ namespace bp = boost::python; #include "anglerestraint.h" +#include + #include "anglerestraint.h" SireMM::AngleRestraint __copy__(const SireMM::AngleRestraint &other){ return SireMM::AngleRestraint(other); } @@ -47,162 +48,50 @@ SireMM::AngleRestraint __copy__(const SireMM::AngleRestraint &other){ return Sir void register_AngleRestraint_class(){ { //::SireMM::AngleRestraint - typedef bp::class_< SireMM::AngleRestraint, bp::bases< SireMM::Restraint3D, SireMM::Restraint, SireBase::Property > > AngleRestraint_exposer_t; - AngleRestraint_exposer_t AngleRestraint_exposer = AngleRestraint_exposer_t( "AngleRestraint", "This is a restraint that operates on the angle between\nthree SireMM::Point objects (e.g. three atoms in a molecule)\n\nAuthor: Christopher Woods\n", bp::init< >("Constructor") ); + typedef bp::class_< SireMM::AngleRestraint, bp::bases< SireBase::Property > > AngleRestraint_exposer_t; + AngleRestraint_exposer_t AngleRestraint_exposer = AngleRestraint_exposer_t( "AngleRestraint", "This class represents a single angle restraint between any three\natoms in a system\nAuthor: Christopher Woods\n", bp::init< >("Null constructor") ); bp::scope AngleRestraint_scope( AngleRestraint_exposer ); - AngleRestraint_exposer.def( bp::init< SireFF::PointRef const &, SireFF::PointRef const &, SireFF::PointRef const &, SireCAS::Expression const & >(( bp::arg("point0"), bp::arg("point1"), bp::arg("point2"), bp::arg("restraint") ), "Construct a restraint that acts on the angle within the\nthree points point0, point1 and point2 (theta == a(012)),\nrestraining the angle within these points using the expression\nrestraint") ); - AngleRestraint_exposer.def( bp::init< SireFF::PointRef const &, SireFF::PointRef const &, SireFF::PointRef const &, SireCAS::Expression const &, SireCAS::Values const & >(( bp::arg("point0"), bp::arg("point1"), bp::arg("point2"), bp::arg("restraint"), bp::arg("values") ), "Construct a restraint that acts on the angle within the\nthree points point0, point1 and point2 (theta == a(012)),\nrestraining the angle within these points using the expression\nrestraint") ); + AngleRestraint_exposer.def( bp::init< QList< long long > const &, SireUnits::Dimension::Angle const &, SireUnits::Dimension::HarmonicAngleConstant const & >(( bp::arg("atoms"), bp::arg("theta0"), bp::arg("ktheta") ), "Construct a restraint that acts on the angle within the\nthree atoms atom0, atom1 and atom2 (theta == a(012)),\nrestraining the angle within these atoms") ); AngleRestraint_exposer.def( bp::init< SireMM::AngleRestraint const & >(( bp::arg("other") ), "Copy constructor") ); - { //::SireMM::AngleRestraint::builtinSymbols - - typedef ::SireCAS::Symbols ( ::SireMM::AngleRestraint::*builtinSymbols_function_type)( ) const; - builtinSymbols_function_type builtinSymbols_function_value( &::SireMM::AngleRestraint::builtinSymbols ); - - AngleRestraint_exposer.def( - "builtinSymbols" - , builtinSymbols_function_value - , bp::release_gil_policy() - , "Return the built-in symbols of this restraint" ); - - } - { //::SireMM::AngleRestraint::builtinValues - - typedef ::SireCAS::Values ( ::SireMM::AngleRestraint::*builtinValues_function_type)( ) const; - builtinValues_function_type builtinValues_function_value( &::SireMM::AngleRestraint::builtinValues ); - - AngleRestraint_exposer.def( - "builtinValues" - , builtinValues_function_value - , bp::release_gil_policy() - , "Return the values of the built-in symbols of this restraint" ); - - } - { //::SireMM::AngleRestraint::contains - - typedef bool ( ::SireMM::AngleRestraint::*contains_function_type)( ::SireMol::MolNum ) const; - contains_function_type contains_function_value( &::SireMM::AngleRestraint::contains ); - - AngleRestraint_exposer.def( - "contains" - , contains_function_value - , ( bp::arg("molnum") ) - , bp::release_gil_policy() - , "Return whether or not this restraint affects the molecule\nwith number molnum" ); - - } - { //::SireMM::AngleRestraint::contains - - typedef bool ( ::SireMM::AngleRestraint::*contains_function_type)( ::SireMol::MolID const & ) const; - contains_function_type contains_function_value( &::SireMM::AngleRestraint::contains ); - - AngleRestraint_exposer.def( - "contains" - , contains_function_value - , ( bp::arg("molid") ) - , bp::release_gil_policy() - , "Return whether or not this restraint affects the molecule\nwith ID molid" ); - - } - { //::SireMM::AngleRestraint::differentialRestraintFunction - - typedef ::SireCAS::Expression const & ( ::SireMM::AngleRestraint::*differentialRestraintFunction_function_type)( ) const; - differentialRestraintFunction_function_type differentialRestraintFunction_function_value( &::SireMM::AngleRestraint::differentialRestraintFunction ); - - AngleRestraint_exposer.def( - "differentialRestraintFunction" - , differentialRestraintFunction_function_value - , bp::return_value_policy< bp::copy_const_reference >() - , "Return the function used to calculate the restraint force" ); - - } - { //::SireMM::AngleRestraint::differentiate - - typedef ::SireMM::RestraintPtr ( ::SireMM::AngleRestraint::*differentiate_function_type)( ::SireCAS::Symbol const & ) const; - differentiate_function_type differentiate_function_value( &::SireMM::AngleRestraint::differentiate ); - - AngleRestraint_exposer.def( - "differentiate" - , differentiate_function_value - , ( bp::arg("symbol") ) - , bp::release_gil_policy() - , "Return the differential of this restraint with respect to\nthe symbol symbol\nThrow: SireCAS::unavailable_differential\n" ); - - } - { //::SireMM::AngleRestraint::force - - typedef void ( ::SireMM::AngleRestraint::*force_function_type)( ::SireFF::MolForceTable &,double ) const; - force_function_type force_function_value( &::SireMM::AngleRestraint::force ); - - AngleRestraint_exposer.def( - "force" - , force_function_value - , ( bp::arg("forcetable"), bp::arg("scale_force")=1 ) - , "Calculate the force acting on the molecule in the forcetable forcetable\ncaused by this restraint, and add it on to the forcetable scaled by\nscale_force" ); - - } - { //::SireMM::AngleRestraint::force - - typedef void ( ::SireMM::AngleRestraint::*force_function_type)( ::SireFF::ForceTable &,double ) const; - force_function_type force_function_value( &::SireMM::AngleRestraint::force ); - - AngleRestraint_exposer.def( - "force" - , force_function_value - , ( bp::arg("forcetable"), bp::arg("scale_force")=1 ) - , "Calculate the force acting on the molecules in the forcetable forcetable\ncaused by this restraint, and add it on to the forcetable scaled by\nscale_force" ); - - } - { //::SireMM::AngleRestraint::halfHarmonic - - typedef ::SireMM::AngleRestraint ( *halfHarmonic_function_type )( ::SireFF::PointRef const &,::SireFF::PointRef const &,::SireFF::PointRef const &,::SireUnits::Dimension::Angle const &,::SireMM::HarmonicAngleForceConstant const & ); - halfHarmonic_function_type halfHarmonic_function_value( &::SireMM::AngleRestraint::halfHarmonic ); - - AngleRestraint_exposer.def( - "halfHarmonic" - , halfHarmonic_function_value - , ( bp::arg("point0"), bp::arg("point1"), bp::arg("point2"), bp::arg("angle"), bp::arg("force_constant") ) - , bp::release_gil_policy() - , "Return a distance restraint that applied a half-harmonic potential\nbetween the points point0 and point1 above a distance distance\nusing a force constant force_constant" ); + { //::SireMM::AngleRestraint::atoms - } - { //::SireMM::AngleRestraint::harmonic - - typedef ::SireMM::AngleRestraint ( *harmonic_function_type )( ::SireFF::PointRef const &,::SireFF::PointRef const &,::SireFF::PointRef const &,::SireMM::HarmonicAngleForceConstant const & ); - harmonic_function_type harmonic_function_value( &::SireMM::AngleRestraint::harmonic ); + typedef ::QVector< long long > ( ::SireMM::AngleRestraint::*atoms_function_type)( ) const; + atoms_function_type atoms_function_value( &::SireMM::AngleRestraint::atoms ); AngleRestraint_exposer.def( - "harmonic" - , harmonic_function_value - , ( bp::arg("point0"), bp::arg("point1"), bp::arg("point2"), bp::arg("force_constant") ) + "atoms" + , atoms_function_value , bp::release_gil_policy() - , "Return a distance restraint that applies a harmonic potential between\nthe points point0 and point1 using a force constant force_constant" ); + , "Return the atoms involved in the restraint" ); } - { //::SireMM::AngleRestraint::molecules + { //::SireMM::AngleRestraint::isNull - typedef ::SireMol::Molecules ( ::SireMM::AngleRestraint::*molecules_function_type)( ) const; - molecules_function_type molecules_function_value( &::SireMM::AngleRestraint::molecules ); + typedef bool ( ::SireMM::AngleRestraint::*isNull_function_type)( ) const; + isNull_function_type isNull_function_value( &::SireMM::AngleRestraint::isNull ); AngleRestraint_exposer.def( - "molecules" - , molecules_function_value + "isNull" + , isNull_function_value , bp::release_gil_policy() - , "Return the molecules used in this restraint" ); + , "" ); } - { //::SireMM::AngleRestraint::nPoints + { //::SireMM::AngleRestraint::ktheta - typedef int ( ::SireMM::AngleRestraint::*nPoints_function_type)( ) const; - nPoints_function_type nPoints_function_value( &::SireMM::AngleRestraint::nPoints ); + typedef ::SireUnits::Dimension::HarmonicAngleConstant ( ::SireMM::AngleRestraint::*ktheta_function_type)( ) const; + ktheta_function_type ktheta_function_value( &::SireMM::AngleRestraint::ktheta ); AngleRestraint_exposer.def( - "nPoints" - , nPoints_function_value + "ktheta" + , ktheta_function_value , bp::release_gil_policy() - , "This restraint involves three points" ); + , "Return the force constant for the restraint" ); } AngleRestraint_exposer.def( bp::self != bp::self ); + AngleRestraint_exposer.def( bp::self + bp::self ); + AngleRestraint_exposer.def( bp::self + bp::other< SireMM::AngleRestraints >() ); { //::SireMM::AngleRestraint::operator= typedef ::SireMM::AngleRestraint & ( ::SireMM::AngleRestraint::*assign_function_type)( ::SireMM::AngleRestraint const & ) ; @@ -217,78 +106,28 @@ void register_AngleRestraint_class(){ } AngleRestraint_exposer.def( bp::self == bp::self ); - { //::SireMM::AngleRestraint::point - - typedef ::SireFF::Point const & ( ::SireMM::AngleRestraint::*point_function_type)( int ) const; - point_function_type point_function_value( &::SireMM::AngleRestraint::point ); - - AngleRestraint_exposer.def( - "point" - , point_function_value - , ( bp::arg("i") ) - , bp::return_value_policy() - , "Return the ith point" ); - - } - { //::SireMM::AngleRestraint::point0 - - typedef ::SireFF::Point const & ( ::SireMM::AngleRestraint::*point0_function_type)( ) const; - point0_function_type point0_function_value( &::SireMM::AngleRestraint::point0 ); - - AngleRestraint_exposer.def( - "point0" - , point0_function_value - , bp::return_value_policy() - , "Return the first point" ); - - } - { //::SireMM::AngleRestraint::point1 - - typedef ::SireFF::Point const & ( ::SireMM::AngleRestraint::*point1_function_type)( ) const; - point1_function_type point1_function_value( &::SireMM::AngleRestraint::point1 ); - - AngleRestraint_exposer.def( - "point1" - , point1_function_value - , bp::return_value_policy() - , "Return the second point" ); - - } - { //::SireMM::AngleRestraint::point2 + { //::SireMM::AngleRestraint::theta0 - typedef ::SireFF::Point const & ( ::SireMM::AngleRestraint::*point2_function_type)( ) const; - point2_function_type point2_function_value( &::SireMM::AngleRestraint::point2 ); + typedef ::SireUnits::Dimension::Angle ( ::SireMM::AngleRestraint::*theta0_function_type)( ) const; + theta0_function_type theta0_function_value( &::SireMM::AngleRestraint::theta0 ); AngleRestraint_exposer.def( - "point2" - , point2_function_value - , bp::return_value_policy() - , "Return the third point" ); - - } - { //::SireMM::AngleRestraint::setSpace - - typedef void ( ::SireMM::AngleRestraint::*setSpace_function_type)( ::SireVol::Space const & ) ; - setSpace_function_type setSpace_function_value( &::SireMM::AngleRestraint::setSpace ); - - AngleRestraint_exposer.def( - "setSpace" - , setSpace_function_value - , ( bp::arg("space") ) + "theta0" + , theta0_function_value , bp::release_gil_policy() - , "Set the space used to evaluate the energy of this restraint\nThrow: SireVol::incompatible_space\n" ); + , "Return the equilibrium angle for the restraint" ); } - { //::SireMM::AngleRestraint::theta + { //::SireMM::AngleRestraint::toString - typedef ::SireCAS::Symbol const & ( *theta_function_type )( ); - theta_function_type theta_function_value( &::SireMM::AngleRestraint::theta ); + typedef ::QString ( ::SireMM::AngleRestraint::*toString_function_type)( ) const; + toString_function_type toString_function_value( &::SireMM::AngleRestraint::toString ); AngleRestraint_exposer.def( - "theta" - , theta_function_value - , bp::return_value_policy() - , "Return the symbol that represents the angle between the points (theta)" ); + "toString" + , toString_function_value + , bp::release_gil_policy() + , "" ); } { //::SireMM::AngleRestraint::typeName @@ -303,69 +142,26 @@ void register_AngleRestraint_class(){ , "" ); } - { //::SireMM::AngleRestraint::update - - typedef void ( ::SireMM::AngleRestraint::*update_function_type)( ::SireMol::MoleculeData const & ) ; - update_function_type update_function_value( &::SireMM::AngleRestraint::update ); - - AngleRestraint_exposer.def( - "update" - , update_function_value - , ( bp::arg("moldata") ) - , bp::release_gil_policy() - , "Update the points of this restraint using new molecule data from moldata\nThrow: SireBase::missing_property\nThrow: SireError::invalid_cast\nThrow: SireError::incompatible_error\n" ); - - } - { //::SireMM::AngleRestraint::update + { //::SireMM::AngleRestraint::what - typedef void ( ::SireMM::AngleRestraint::*update_function_type)( ::SireMol::Molecules const & ) ; - update_function_type update_function_value( &::SireMM::AngleRestraint::update ); + typedef char const * ( ::SireMM::AngleRestraint::*what_function_type)( ) const; + what_function_type what_function_value( &::SireMM::AngleRestraint::what ); AngleRestraint_exposer.def( - "update" - , update_function_value - , ( bp::arg("molecules") ) + "what" + , what_function_value , bp::release_gil_policy() - , "Update the points of this restraint using new molecule data from molecules\nThrow: SireBase::missing_property\nThrow: SireError::invalid_cast\nThrow: SireError::incompatible_error\n" ); - - } - { //::SireMM::AngleRestraint::usesMoleculesIn - - typedef bool ( ::SireMM::AngleRestraint::*usesMoleculesIn_function_type)( ::SireFF::ForceTable const & ) const; - usesMoleculesIn_function_type usesMoleculesIn_function_value( &::SireMM::AngleRestraint::usesMoleculesIn ); - - AngleRestraint_exposer.def( - "usesMoleculesIn" - , usesMoleculesIn_function_value - , ( bp::arg("forcetable") ) - , bp::release_gil_policy() - , "Return whether or not this restraint involves any of the molecules\nthat are in the forcetable forcetable" ); - - } - { //::SireMM::AngleRestraint::usesMoleculesIn - - typedef bool ( ::SireMM::AngleRestraint::*usesMoleculesIn_function_type)( ::SireMol::Molecules const & ) const; - usesMoleculesIn_function_type usesMoleculesIn_function_value( &::SireMM::AngleRestraint::usesMoleculesIn ); - - AngleRestraint_exposer.def( - "usesMoleculesIn" - , usesMoleculesIn_function_value - , ( bp::arg("molecules") ) - , bp::release_gil_policy() - , "Return whether or not this restraint involves any of the molecules\nin molecules" ); + , "" ); } - AngleRestraint_exposer.staticmethod( "halfHarmonic" ); - AngleRestraint_exposer.staticmethod( "harmonic" ); - AngleRestraint_exposer.staticmethod( "theta" ); AngleRestraint_exposer.staticmethod( "typeName" ); AngleRestraint_exposer.def( "__copy__", &__copy__); AngleRestraint_exposer.def( "__deepcopy__", &__copy__); AngleRestraint_exposer.def( "clone", &__copy__); AngleRestraint_exposer.def( "__rlshift__", &__rlshift__QDataStream< ::SireMM::AngleRestraint >, - bp::return_internal_reference<1, bp::with_custodian_and_ward<1,2> >() ); + bp::return_internal_reference<1, bp::with_custodian_and_ward<1,2> >() ); AngleRestraint_exposer.def( "__rrshift__", &__rrshift__QDataStream< ::SireMM::AngleRestraint >, - bp::return_internal_reference<1, bp::with_custodian_and_ward<1,2> >() ); + bp::return_internal_reference<1, bp::with_custodian_and_ward<1,2> >() ); AngleRestraint_exposer.def_pickle(sire_pickle_suite< ::SireMM::AngleRestraint >()); AngleRestraint_exposer.def( "__str__", &__str__< ::SireMM::AngleRestraint > ); AngleRestraint_exposer.def( "__repr__", &__str__< ::SireMM::AngleRestraint > ); diff --git a/wrapper/MM/DihedralRestraint.pypp.cpp b/wrapper/MM/DihedralRestraint.pypp.cpp index 65ed2feb1..accddf41c 100644 --- a/wrapper/MM/DihedralRestraint.pypp.cpp +++ b/wrapper/MM/DihedralRestraint.pypp.cpp @@ -3,7 +3,6 @@ // (C) Christopher Woods, GPL >= 3 License #include "boost/python.hpp" -#include "Helpers/clone_const_reference.hpp" #include "DihedralRestraint.pypp.hpp" namespace bp = boost::python; @@ -32,6 +31,8 @@ namespace bp = boost::python; #include "dihedralrestraint.h" +#include + #include "dihedralrestraint.h" SireMM::DihedralRestraint __copy__(const SireMM::DihedralRestraint &other){ return SireMM::DihedralRestraint(other); } @@ -47,162 +48,50 @@ SireMM::DihedralRestraint __copy__(const SireMM::DihedralRestraint &other){ retu void register_DihedralRestraint_class(){ { //::SireMM::DihedralRestraint - typedef bp::class_< SireMM::DihedralRestraint, bp::bases< SireMM::Restraint3D, SireMM::Restraint, SireBase::Property > > DihedralRestraint_exposer_t; - DihedralRestraint_exposer_t DihedralRestraint_exposer = DihedralRestraint_exposer_t( "DihedralRestraint", "This is a restraint that operates on the dihedral angle between\nfour SireMM::Point objects (e.g. four atoms in a molecule)\n\nAuthor: Christopher Woods\n", bp::init< >("Constructor") ); + typedef bp::class_< SireMM::DihedralRestraint, bp::bases< SireBase::Property > > DihedralRestraint_exposer_t; + DihedralRestraint_exposer_t DihedralRestraint_exposer = DihedralRestraint_exposer_t( "DihedralRestraint", "This class represents a single angle restraint between any three\natoms in a system\nAuthor: Christopher Woods\n", bp::init< >("Null constructor") ); bp::scope DihedralRestraint_scope( DihedralRestraint_exposer ); - DihedralRestraint_exposer.def( bp::init< SireFF::PointRef const &, SireFF::PointRef const &, SireFF::PointRef const &, SireFF::PointRef const &, SireCAS::Expression const & >(( bp::arg("point0"), bp::arg("point1"), bp::arg("point2"), bp::arg("point3"), bp::arg("restraint") ), "Construct a restraint that acts on the angle within the\nthree points point0, point1 and point2 (theta == a(012)),\nrestraining the angle within these points using the expression\nrestraint") ); - DihedralRestraint_exposer.def( bp::init< SireFF::PointRef const &, SireFF::PointRef const &, SireFF::PointRef const &, SireFF::PointRef const &, SireCAS::Expression const &, SireCAS::Values const & >(( bp::arg("point0"), bp::arg("point1"), bp::arg("point2"), bp::arg("point3"), bp::arg("restraint"), bp::arg("values") ), "Construct a restraint that acts on the angle within the\nthree points point0, point1 and point2 (theta == a(012)),\nrestraining the angle within these points using the expression\nrestraint") ); + DihedralRestraint_exposer.def( bp::init< QList< long long > const &, SireUnits::Dimension::Angle const &, SireUnits::Dimension::HarmonicAngleConstant const & >(( bp::arg("atoms"), bp::arg("phi0"), bp::arg("kphi") ), "Construct a restraint that acts on the angle within the\nfour atoms atom0, atom1, atom2 atom3 (phi == a(0123)),\nrestraining the angle within these atoms") ); DihedralRestraint_exposer.def( bp::init< SireMM::DihedralRestraint const & >(( bp::arg("other") ), "Copy constructor") ); - { //::SireMM::DihedralRestraint::builtinSymbols - - typedef ::SireCAS::Symbols ( ::SireMM::DihedralRestraint::*builtinSymbols_function_type)( ) const; - builtinSymbols_function_type builtinSymbols_function_value( &::SireMM::DihedralRestraint::builtinSymbols ); - - DihedralRestraint_exposer.def( - "builtinSymbols" - , builtinSymbols_function_value - , bp::release_gil_policy() - , "Return the built-in symbols for this restraint" ); - - } - { //::SireMM::DihedralRestraint::builtinValues - - typedef ::SireCAS::Values ( ::SireMM::DihedralRestraint::*builtinValues_function_type)( ) const; - builtinValues_function_type builtinValues_function_value( &::SireMM::DihedralRestraint::builtinValues ); - - DihedralRestraint_exposer.def( - "builtinValues" - , builtinValues_function_value - , bp::release_gil_policy() - , "Return the values of the built-in symbols of this restraint" ); - - } - { //::SireMM::DihedralRestraint::contains - - typedef bool ( ::SireMM::DihedralRestraint::*contains_function_type)( ::SireMol::MolNum ) const; - contains_function_type contains_function_value( &::SireMM::DihedralRestraint::contains ); - - DihedralRestraint_exposer.def( - "contains" - , contains_function_value - , ( bp::arg("molnum") ) - , bp::release_gil_policy() - , "Return whether or not this restraint affects the molecule\nwith number molnum" ); - - } - { //::SireMM::DihedralRestraint::contains - - typedef bool ( ::SireMM::DihedralRestraint::*contains_function_type)( ::SireMol::MolID const & ) const; - contains_function_type contains_function_value( &::SireMM::DihedralRestraint::contains ); - - DihedralRestraint_exposer.def( - "contains" - , contains_function_value - , ( bp::arg("molid") ) - , bp::release_gil_policy() - , "Return whether or not this restraint affects the molecule\nwith ID molid" ); - - } - { //::SireMM::DihedralRestraint::differentialRestraintFunction + { //::SireMM::DihedralRestraint::atoms - typedef ::SireCAS::Expression const & ( ::SireMM::DihedralRestraint::*differentialRestraintFunction_function_type)( ) const; - differentialRestraintFunction_function_type differentialRestraintFunction_function_value( &::SireMM::DihedralRestraint::differentialRestraintFunction ); + typedef ::QVector< long long > ( ::SireMM::DihedralRestraint::*atoms_function_type)( ) const; + atoms_function_type atoms_function_value( &::SireMM::DihedralRestraint::atoms ); DihedralRestraint_exposer.def( - "differentialRestraintFunction" - , differentialRestraintFunction_function_value - , bp::return_value_policy< bp::copy_const_reference >() - , "Return the function used to calculate the restraint force" ); - - } - { //::SireMM::DihedralRestraint::differentiate - - typedef ::SireMM::RestraintPtr ( ::SireMM::DihedralRestraint::*differentiate_function_type)( ::SireCAS::Symbol const & ) const; - differentiate_function_type differentiate_function_value( &::SireMM::DihedralRestraint::differentiate ); - - DihedralRestraint_exposer.def( - "differentiate" - , differentiate_function_value - , ( bp::arg("symbol") ) + "atoms" + , atoms_function_value , bp::release_gil_policy() - , "Return the differential of this restraint with respect to\nthe symbol symbol\nThrow: SireCAS::unavailable_differential\n" ); + , "Return the atoms involved in the restraint" ); } - { //::SireMM::DihedralRestraint::force + { //::SireMM::DihedralRestraint::isNull - typedef void ( ::SireMM::DihedralRestraint::*force_function_type)( ::SireFF::MolForceTable &,double ) const; - force_function_type force_function_value( &::SireMM::DihedralRestraint::force ); + typedef bool ( ::SireMM::DihedralRestraint::*isNull_function_type)( ) const; + isNull_function_type isNull_function_value( &::SireMM::DihedralRestraint::isNull ); DihedralRestraint_exposer.def( - "force" - , force_function_value - , ( bp::arg("forcetable"), bp::arg("scale_force")=1 ) - , "Calculate the force acting on the molecule in the forcetable forcetable\ncaused by this restraint, and add it on to the forcetable scaled by\nscale_force" ); - - } - { //::SireMM::DihedralRestraint::force - - typedef void ( ::SireMM::DihedralRestraint::*force_function_type)( ::SireFF::ForceTable &,double ) const; - force_function_type force_function_value( &::SireMM::DihedralRestraint::force ); - - DihedralRestraint_exposer.def( - "force" - , force_function_value - , ( bp::arg("forcetable"), bp::arg("scale_force")=1 ) - , "Calculate the force acting on the molecules in the forcetable forcetable\ncaused by this restraint, and add it on to the forcetable scaled by\nscale_force" ); - - } - { //::SireMM::DihedralRestraint::halfHarmonic - - typedef ::SireMM::DihedralRestraint ( *halfHarmonic_function_type )( ::SireFF::PointRef const &,::SireFF::PointRef const &,::SireFF::PointRef const &,::SireFF::PointRef const &,::SireUnits::Dimension::Angle const &,::SireMM::HarmonicAngleForceConstant const & ); - halfHarmonic_function_type halfHarmonic_function_value( &::SireMM::DihedralRestraint::halfHarmonic ); - - DihedralRestraint_exposer.def( - "halfHarmonic" - , halfHarmonic_function_value - , ( bp::arg("point0"), bp::arg("point1"), bp::arg("point2"), bp::arg("point3"), bp::arg("angle"), bp::arg("force_constant") ) - , bp::release_gil_policy() - , "Return a distance restraint that applied a half-harmonic potential\nbetween the points point0 and point1 above a distance distance\nusing a force constant force_constant" ); - - } - { //::SireMM::DihedralRestraint::harmonic - - typedef ::SireMM::DihedralRestraint ( *harmonic_function_type )( ::SireFF::PointRef const &,::SireFF::PointRef const &,::SireFF::PointRef const &,::SireFF::PointRef const &,::SireMM::HarmonicAngleForceConstant const & ); - harmonic_function_type harmonic_function_value( &::SireMM::DihedralRestraint::harmonic ); - - DihedralRestraint_exposer.def( - "harmonic" - , harmonic_function_value - , ( bp::arg("point0"), bp::arg("point1"), bp::arg("point2"), bp::arg("point3"), bp::arg("force_constant") ) + "isNull" + , isNull_function_value , bp::release_gil_policy() - , "Return a distance restraint that applies a harmonic potential between\nthe points point0 and point1 using a force constant force_constant" ); - - } - { //::SireMM::DihedralRestraint::molecules - - typedef ::SireMol::Molecules ( ::SireMM::DihedralRestraint::*molecules_function_type)( ) const; - molecules_function_type molecules_function_value( &::SireMM::DihedralRestraint::molecules ); - - DihedralRestraint_exposer.def( - "molecules" - , molecules_function_value - , bp::release_gil_policy() - , "Return the molecules used in this restraint" ); + , "" ); } - { //::SireMM::DihedralRestraint::nPoints + { //::SireMM::DihedralRestraint::kphi - typedef int ( ::SireMM::DihedralRestraint::*nPoints_function_type)( ) const; - nPoints_function_type nPoints_function_value( &::SireMM::DihedralRestraint::nPoints ); + typedef ::SireUnits::Dimension::HarmonicAngleConstant ( ::SireMM::DihedralRestraint::*kphi_function_type)( ) const; + kphi_function_type kphi_function_value( &::SireMM::DihedralRestraint::kphi ); DihedralRestraint_exposer.def( - "nPoints" - , nPoints_function_value + "kphi" + , kphi_function_value , bp::release_gil_policy() - , "This restraint involves four points" ); + , "Return the force constant for the restraint" ); } DihedralRestraint_exposer.def( bp::self != bp::self ); + DihedralRestraint_exposer.def( bp::self + bp::self ); + DihedralRestraint_exposer.def( bp::self + bp::other< SireMM::DihedralRestraints >() ); { //::SireMM::DihedralRestraint::operator= typedef ::SireMM::DihedralRestraint & ( ::SireMM::DihedralRestraint::*assign_function_type)( ::SireMM::DihedralRestraint const & ) ; @@ -217,90 +106,28 @@ void register_DihedralRestraint_class(){ } DihedralRestraint_exposer.def( bp::self == bp::self ); - { //::SireMM::DihedralRestraint::phi - - typedef ::SireCAS::Symbol const & ( *phi_function_type )( ); - phi_function_type phi_function_value( &::SireMM::DihedralRestraint::phi ); - - DihedralRestraint_exposer.def( - "phi" - , phi_function_value - , bp::return_value_policy() - , "Return the symbol that represents the dihedral angle between the points (phi)" ); - - } - { //::SireMM::DihedralRestraint::point - - typedef ::SireFF::Point const & ( ::SireMM::DihedralRestraint::*point_function_type)( int ) const; - point_function_type point_function_value( &::SireMM::DihedralRestraint::point ); - - DihedralRestraint_exposer.def( - "point" - , point_function_value - , ( bp::arg("i") ) - , bp::return_value_policy() - , "Return the ith point" ); - - } - { //::SireMM::DihedralRestraint::point0 + { //::SireMM::DihedralRestraint::phi0 - typedef ::SireFF::Point const & ( ::SireMM::DihedralRestraint::*point0_function_type)( ) const; - point0_function_type point0_function_value( &::SireMM::DihedralRestraint::point0 ); + typedef ::SireUnits::Dimension::Angle ( ::SireMM::DihedralRestraint::*phi0_function_type)( ) const; + phi0_function_type phi0_function_value( &::SireMM::DihedralRestraint::phi0 ); DihedralRestraint_exposer.def( - "point0" - , point0_function_value - , bp::return_value_policy() - , "Return the first point" ); - - } - { //::SireMM::DihedralRestraint::point1 - - typedef ::SireFF::Point const & ( ::SireMM::DihedralRestraint::*point1_function_type)( ) const; - point1_function_type point1_function_value( &::SireMM::DihedralRestraint::point1 ); - - DihedralRestraint_exposer.def( - "point1" - , point1_function_value - , bp::return_value_policy() - , "Return the second point" ); - - } - { //::SireMM::DihedralRestraint::point2 - - typedef ::SireFF::Point const & ( ::SireMM::DihedralRestraint::*point2_function_type)( ) const; - point2_function_type point2_function_value( &::SireMM::DihedralRestraint::point2 ); - - DihedralRestraint_exposer.def( - "point2" - , point2_function_value - , bp::return_value_policy() - , "Return the third point" ); - - } - { //::SireMM::DihedralRestraint::point3 - - typedef ::SireFF::Point const & ( ::SireMM::DihedralRestraint::*point3_function_type)( ) const; - point3_function_type point3_function_value( &::SireMM::DihedralRestraint::point3 ); - - DihedralRestraint_exposer.def( - "point3" - , point3_function_value - , bp::return_value_policy() - , "Return the fourth point" ); + "phi0" + , phi0_function_value + , bp::release_gil_policy() + , "Return the equilibrium angle for the restraint" ); } - { //::SireMM::DihedralRestraint::setSpace + { //::SireMM::DihedralRestraint::toString - typedef void ( ::SireMM::DihedralRestraint::*setSpace_function_type)( ::SireVol::Space const & ) ; - setSpace_function_type setSpace_function_value( &::SireMM::DihedralRestraint::setSpace ); + typedef ::QString ( ::SireMM::DihedralRestraint::*toString_function_type)( ) const; + toString_function_type toString_function_value( &::SireMM::DihedralRestraint::toString ); DihedralRestraint_exposer.def( - "setSpace" - , setSpace_function_value - , ( bp::arg("space") ) + "toString" + , toString_function_value , bp::release_gil_policy() - , "Set the space used to evaluate the energy of this restraint\nThrow: SireVol::incompatible_space\n" ); + , "" ); } { //::SireMM::DihedralRestraint::typeName @@ -315,69 +142,26 @@ void register_DihedralRestraint_class(){ , "" ); } - { //::SireMM::DihedralRestraint::update - - typedef void ( ::SireMM::DihedralRestraint::*update_function_type)( ::SireMol::MoleculeData const & ) ; - update_function_type update_function_value( &::SireMM::DihedralRestraint::update ); - - DihedralRestraint_exposer.def( - "update" - , update_function_value - , ( bp::arg("moldata") ) - , bp::release_gil_policy() - , "Update the points of this restraint using new molecule data from moldata\nThrow: SireBase::missing_property\nThrow: SireError::invalid_cast\nThrow: SireError::incompatible_error\n" ); - - } - { //::SireMM::DihedralRestraint::update - - typedef void ( ::SireMM::DihedralRestraint::*update_function_type)( ::SireMol::Molecules const & ) ; - update_function_type update_function_value( &::SireMM::DihedralRestraint::update ); - - DihedralRestraint_exposer.def( - "update" - , update_function_value - , ( bp::arg("molecules") ) - , bp::release_gil_policy() - , "Update the points of this restraint using new molecule data from molecules\nThrow: SireBase::missing_property\nThrow: SireError::invalid_cast\nThrow: SireError::incompatible_error\n" ); - - } - { //::SireMM::DihedralRestraint::usesMoleculesIn - - typedef bool ( ::SireMM::DihedralRestraint::*usesMoleculesIn_function_type)( ::SireFF::ForceTable const & ) const; - usesMoleculesIn_function_type usesMoleculesIn_function_value( &::SireMM::DihedralRestraint::usesMoleculesIn ); - - DihedralRestraint_exposer.def( - "usesMoleculesIn" - , usesMoleculesIn_function_value - , ( bp::arg("forcetable") ) - , bp::release_gil_policy() - , "Return whether or not this restraint involves any of the molecules\nthat are in the forcetable forcetable" ); - - } - { //::SireMM::DihedralRestraint::usesMoleculesIn + { //::SireMM::DihedralRestraint::what - typedef bool ( ::SireMM::DihedralRestraint::*usesMoleculesIn_function_type)( ::SireMol::Molecules const & ) const; - usesMoleculesIn_function_type usesMoleculesIn_function_value( &::SireMM::DihedralRestraint::usesMoleculesIn ); + typedef char const * ( ::SireMM::DihedralRestraint::*what_function_type)( ) const; + what_function_type what_function_value( &::SireMM::DihedralRestraint::what ); DihedralRestraint_exposer.def( - "usesMoleculesIn" - , usesMoleculesIn_function_value - , ( bp::arg("molecules") ) + "what" + , what_function_value , bp::release_gil_policy() - , "Return whether or not this restraint involves any of the molecules\nin molecules" ); + , "" ); } - DihedralRestraint_exposer.staticmethod( "halfHarmonic" ); - DihedralRestraint_exposer.staticmethod( "harmonic" ); - DihedralRestraint_exposer.staticmethod( "phi" ); DihedralRestraint_exposer.staticmethod( "typeName" ); DihedralRestraint_exposer.def( "__copy__", &__copy__); DihedralRestraint_exposer.def( "__deepcopy__", &__copy__); DihedralRestraint_exposer.def( "clone", &__copy__); DihedralRestraint_exposer.def( "__rlshift__", &__rlshift__QDataStream< ::SireMM::DihedralRestraint >, - bp::return_internal_reference<1, bp::with_custodian_and_ward<1,2> >() ); + bp::return_internal_reference<1, bp::with_custodian_and_ward<1,2> >() ); DihedralRestraint_exposer.def( "__rrshift__", &__rrshift__QDataStream< ::SireMM::DihedralRestraint >, - bp::return_internal_reference<1, bp::with_custodian_and_ward<1,2> >() ); + bp::return_internal_reference<1, bp::with_custodian_and_ward<1,2> >() ); DihedralRestraint_exposer.def_pickle(sire_pickle_suite< ::SireMM::DihedralRestraint >()); DihedralRestraint_exposer.def( "__str__", &__str__< ::SireMM::DihedralRestraint > ); DihedralRestraint_exposer.def( "__repr__", &__str__< ::SireMM::DihedralRestraint > ); diff --git a/wrapper/MM/_MM.main.cpp b/wrapper/MM/_MM.main.cpp index 5a98d9755..b8559a1bd 100644 --- a/wrapper/MM/_MM.main.cpp +++ b/wrapper/MM/_MM.main.cpp @@ -27,6 +27,8 @@ #include "AngleRestraint.pypp.hpp" +#include "AngleRestraints.pypp.hpp" + #include "AngleSymbols.pypp.hpp" #include "AtomFunction.pypp.hpp" @@ -161,6 +163,8 @@ #include "DihedralRestraint.pypp.hpp" +#include "DihedralRestraints.pypp.hpp" + #include "DihedralSymbols.pypp.hpp" #include "DistanceRestraint.pypp.hpp" @@ -546,11 +550,11 @@ BOOST_PYTHON_MODULE(_MM){ register_AngleParameterName_class(); - register_Restraint_class(); + register_AngleRestraint_class(); - register_Restraint3D_class(); + register_Restraints_class(); - register_AngleRestraint_class(); + register_AngleRestraints_class(); register_InternalSymbolsBase_class(); @@ -586,8 +590,6 @@ BOOST_PYTHON_MODULE(_MM){ register_BondRestraint_class(); - register_Restraints_class(); - register_BondRestraints_class(); register_BondSymbols_class(); @@ -646,6 +648,14 @@ BOOST_PYTHON_MODULE(_MM){ register_CLJParameterNames3D_class(); + register_CLJPotentialInterface_InterCLJPotential__class(); + + register_CLJPotentialInterface_InterSoftCLJPotential__class(); + + register_CLJPotentialInterface_IntraCLJPotential__class(); + + register_CLJPotentialInterface_IntraSoftCLJPotential__class(); + register_CLJProbe_class(); register_CLJRFFunction_class(); @@ -672,6 +682,10 @@ BOOST_PYTHON_MODULE(_MM){ register_CoulombNBPairs_class(); + register_CoulombPotentialInterface_InterCoulombPotential__class(); + + register_CoulombPotentialInterface_IntraCoulombPotential__class(); + register_CoulombProbe_class(); register_Dihedral_class(); @@ -682,8 +696,14 @@ BOOST_PYTHON_MODULE(_MM){ register_DihedralRestraint_class(); + register_DihedralRestraints_class(); + register_DihedralSymbols_class(); + register_Restraint_class(); + + register_Restraint3D_class(); + register_DistanceRestraint_class(); register_DoubleDistanceRestraint_class(); @@ -776,6 +796,10 @@ BOOST_PYTHON_MODULE(_MM){ register_LJPerturbation_class(); + register_LJPotentialInterface_InterLJPotential__class(); + + register_LJPotentialInterface_IntraLJPotential__class(); + register_LJProbe_class(); register_MMDetail_class(); @@ -820,6 +844,10 @@ BOOST_PYTHON_MODULE(_MM){ register_SoftCLJComponent_class(); + register_SoftCLJPotentialInterface_InterSoftCLJPotential__class(); + + register_SoftCLJPotentialInterface_IntraSoftCLJPotential__class(); + register_StretchBendComponent_class(); register_StretchBendSymbols_class(); From 386912d8b56e4613414c49542f9375a909c7b0e7 Mon Sep 17 00:00:00 2001 From: Audrius Kalpokas Date: Tue, 16 Jul 2024 16:05:36 +0100 Subject: [PATCH 004/102] Added intial python code for implementing alchemical dihedral restraints --- src/sire/mm/__init__.py | 5 ++ src/sire/restraints/__init__.py | 2 +- src/sire/restraints/_restraints.py | 100 +++++++++++++++++++++++++++++ 3 files changed, 106 insertions(+), 1 deletion(-) diff --git a/src/sire/mm/__init__.py b/src/sire/mm/__init__.py index 2255fd1ac..047d4a585 100644 --- a/src/sire/mm/__init__.py +++ b/src/sire/mm/__init__.py @@ -11,6 +11,8 @@ "Improper", "PositionRestraint", "PositionRestraints", + "DihedralRestraint", + "DihedralRestraints", "SelectorAngle", "SelectorBond", "SelectorDihedral", @@ -40,6 +42,9 @@ PositionalRestraint = _MM.PositionalRestraint PositionalRestraints = _MM.PositionalRestraints +DihedralRestraint = _MM.DihedralRestraint +DihedralRestraints = _MM.DihedralRestraints + AmberBond = _MM.AmberBond AmberAngle = _MM.AmberAngle AmberDihPart = _MM.AmberDihPart diff --git a/src/sire/restraints/__init__.py b/src/sire/restraints/__init__.py index 5f5af1465..f8283671f 100644 --- a/src/sire/restraints/__init__.py +++ b/src/sire/restraints/__init__.py @@ -1,4 +1,4 @@ __all__ = ["positional", "bond", "distance", "boresch", "get_standard_state_correction"] -from ._restraints import bond, boresch, distance, positional +from ._restraints import bond, boresch, dihedral, distance, positional from ._standard_state_correction import get_standard_state_correction diff --git a/src/sire/restraints/_restraints.py b/src/sire/restraints/_restraints.py index d4c64774e..0d505bd1e 100644 --- a/src/sire/restraints/_restraints.py +++ b/src/sire/restraints/_restraints.py @@ -1,6 +1,7 @@ __all__ = [ "boresch", "bond", + "dihedral", "distance", "positional", ] @@ -374,6 +375,105 @@ def _check_stability_boresch_restraint(restraint_components, temperature=u("298 " values further from 0 or pi radians." ) +def dihedral(mols, atoms, phi0=None, kphi=None, name=None, map=None): + """ + Create a set of dihedral restraints from all of the atoms in 'atoms' + where all atoms are contained in the container 'mols', using the + passed values of the force constant 'ktheta' and equilibrium + bond length theta0. + + These restraints will be per atom-atom distance. If a list of k and/or r0 + values are passed, then different values could be used for + different atom-atom distances (assuming the same number as the number of + atom-atom distances). Otherwise, all atom-atom distances will use the + same parameters. + + If r0 is None, then the current atom-atom distance for + each atom-atom pair will be used as the equilibium value. + + If k is None, then a default value of 150 kcal mol-1 A-2 will be used + + Parameters + ---------- + mols : sire.system._system.System + The system containing the atoms. + + atoms : SireMol::Selector + The atoms to restrain. + + kphi : str or SireUnits::Dimension::GeneralUnit or list of str or SireUnits::Dimension::GeneralUnit, optional + The force constants for the torsion restraints. + If None, this will default to 100 kcal mol-1 rad-2. + If a list, then this should be a. + Default is None. + + theta0 : list of str or SireUnits::Dimension::GeneralUnit, optional + The equilibrium angles for the angle restraints. If None, these + will be measured from the current coordinates of the atoms. + Default is None. + + Returns + ------- + DihedralRestraints : SireMM::DihedralRestraints + A container of Dihedral restraints, where the first restraint is + the DihedralRestraint created. The Dihedral restraint created can be + extracted with DihedralRestraints[0]. + + Examples + -------- + Create a set of Dihedral restraints for the ligand in the system + 'system', specifying all of the force constants and equilibrium + values: + """ + from .. import u + from ..base import create_map + # from ..mm import DihedralRestraint + from ..mm import DihedralRestraint, DihedralRestraints + + map = create_map(map) + map_dict = map.to_dict() + kphi = kphi if kphi is not None else map_dict.get("kphi", None) + phi0 = phi0 if phi0 is not None else map_dict.get("phi0", None) + name = name if name is not None else map_dict.get("name", None) + + atoms = _to_atoms(mols, atoms) + + if len(atoms) != 4: + raise ValueError( + "You need to provide 4 atoms to create a dihedral restraint" + f"but only {len(atoms)} atoms were provided." + ) + + from .. import measure + + if kphi is None: + kphi = u("100 kcal mol-1 rad-2") + + # TODO: Add support for multiple dihedral restraints + if phi0 is None: + # calculate all of the current angles + from .. import measure + # only support 1 dihedral restraint at the moment + phi0 = measure(atoms[0], atoms[1], atoms[2], atoms[3]) + + elif type(phi0) is list: + phi0 = [u(x) for x in phi0] + else: + phi0 = u(phi0) + + mols = mols.atoms() + + if name is None: + restraints = DihedralRestraints() + else: + restraints = DihedralRestraints(name=name) + + print(f"mols.find(atoms): {mols.find(atoms)}") + print(f"phi0: {phi0}") + print(f"kphi: {kphi}") + restraints.add(DihedralRestraint(mols.find(atoms), phi0, kphi)) + return restraints + def distance(mols, atoms0, atoms1, r0=None, k=None, name=None, map=None): """ From 69a7ee282c3a26e57f5160f8df0a17ebbc9a1a3a Mon Sep 17 00:00:00 2001 From: Audrius Kalpokas Date: Tue, 16 Jul 2024 16:11:58 +0100 Subject: [PATCH 005/102] Added prototype code for adding alchemical dihedral restraints to OpenMM system --- .../SireOpenMM/sire_to_openmm_system.cpp | 95 ++++++++++++++++--- 1 file changed, 83 insertions(+), 12 deletions(-) diff --git a/wrapper/Convert/SireOpenMM/sire_to_openmm_system.cpp b/wrapper/Convert/SireOpenMM/sire_to_openmm_system.cpp index 03e2ad242..67720812b 100644 --- a/wrapper/Convert/SireOpenMM/sire_to_openmm_system.cpp +++ b/wrapper/Convert/SireOpenMM/sire_to_openmm_system.cpp @@ -8,37 +8,38 @@ #include "SireSystem/forcefieldinfo.h" -#include "SireMol/core.h" -#include "SireMol/moleditor.h" -#include "SireMol/atomelements.h" #include "SireMol/atomcharges.h" #include "SireMol/atomcoords.h" +#include "SireMol/atomelements.h" #include "SireMol/atommasses.h" #include "SireMol/atomproperty.hpp" -#include "SireMol/connectivity.h" +#include "SireMol/atomvelocities.h" #include "SireMol/bondid.h" #include "SireMol/bondorder.h" -#include "SireMol/atomvelocities.h" +#include "SireMol/connectivity.h" +#include "SireMol/core.h" +#include "SireMol/moleditor.h" -#include "SireMM/atomljs.h" -#include "SireMM/selectorbond.h" #include "SireMM/amberparams.h" +#include "SireMM/atomljs.h" #include "SireMM/bondrestraints.h" -#include "SireMM/positionalrestraints.h" #include "SireMM/boreschrestraints.h" +#include "SireMM/dihedralrestraint.h" +#include "SireMM/positionalrestraints.h" +#include "SireMM/selectorbond.h" #include "SireVol/periodicbox.h" #include "SireVol/triclinicbox.h" #include "SireCAS/lambdaschedule.h" -#include "SireMaths/vector.h" #include "SireMaths/maths.h" +#include "SireMaths/vector.h" +#include "SireBase/generalunitproperty.h" +#include "SireBase/lengthproperty.h" #include "SireBase/parallel.h" #include "SireBase/propertylist.h" -#include "SireBase/lengthproperty.h" -#include "SireBase/generalunitproperty.h" #include "SireUnits/units.h" @@ -393,6 +394,71 @@ void _add_positional_restraints(const SireMM::PositionalRestraints &restraints, } } +/** Add all of the dihedral restraints from 'restraints' to the passed + * system, which is acted on by the passed LambdaLever. The number + * of real (non-anchor) atoms in the OpenMM::System is 'natoms' + */ +void _add_dihedral_restraints(const SireMM::DihedralRestraints &restraints, + OpenMM::System &system, LambdaLever &lambda_lever, + int natoms) +{ + if (restraints.isEmpty()) + return; + + // energy expression of the dihedral restraint, which acts over four atoms + // + // phi = dihedral(P1, P2, P3, P4) + // + // The energies are + // + // e_restraint = rho * (e_torsion) + // e_torsion = k_phi(dphi)^2 where + // dphi = abs(phi - phi0) + + // TODO: Add support for multiple dihedral restraints + + const auto energy_expression = QString( + "rho*k*delta*delta;" + "delta=(phi-phi0)") + .toStdString(); + + auto *restraintff = new OpenMM::CustomTorsionForce(energy_expression); + + restraintff->addPerTorsionParameter("rho"); + restraintff->addPerTorsionParameter("k"); + restraintff->addPerTorsionParameter("phi0"); + + restraintff->setUsesPeriodicBoundaryConditions(true); + + lambda_lever.addRestraintIndex(restraints.name(), + system.addForce(restraintff)); + + const double internal_to_ktheta = (1 * SireUnits::kcal_per_mol / (SireUnits::radian2)).to(SireUnits::kJ_per_mol / SireUnits::radian2); + + const auto atom_restraints = restraints.restraints(); + + for (const auto &restraint : atom_restraints) + { + std::vector particles; + particles.resize(4); + + for (int i = 0; i < 3; ++i) + { + particles[i] = restraint.atoms()[i]; + } + + std::vector parameters; + parameters.resize(3); + + parameters[0] = 1.0; // rho + parameters[1] = restraint.kphi().value() * internal_to_ktheta; // kphi + parameters[2] = restraint.phi0().value(); // phi0 (already in radians) + + // restraintff->addTorsion(particles, parameters); + restraintff->addTorsion(particles[0], particles[1], particles[2], particles[3], parameters); + } +} + /** Set the coulomb and LJ cutoff in the passed NonbondedForce, * based on the information in the passed ForceFieldInfo. * This sets the cutoff type (e.g. PME) and the actual @@ -1625,7 +1691,12 @@ OpenMMMetaData SireOpenMM::sire_to_openmm_system(OpenMM::System &system, CODELOC); // we now need to choose what to do based on the type of restraint... - if (prop.read().isA()) + if (prop.read().isA()) + { + _add_dihedral_restraints(prop.read().asA(), + system, lambda_lever, start_index); + } + else if (prop.read().isA()) { _add_positional_restraints(prop.read().asA(), system, lambda_lever, anchor_coords, start_index); From c29c4eb7ebac487ff2a9a39f6edcc206f1e2b20b Mon Sep 17 00:00:00 2001 From: Audrius Kalpokas Date: Tue, 16 Jul 2024 17:04:30 +0100 Subject: [PATCH 006/102] Fix conversion of dihedral restraints to OpenMM system --- .../SireOpenMM/sire_to_openmm_system.cpp | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/wrapper/Convert/SireOpenMM/sire_to_openmm_system.cpp b/wrapper/Convert/SireOpenMM/sire_to_openmm_system.cpp index 67720812b..10089d82d 100644 --- a/wrapper/Convert/SireOpenMM/sire_to_openmm_system.cpp +++ b/wrapper/Convert/SireOpenMM/sire_to_openmm_system.cpp @@ -418,15 +418,24 @@ void _add_dihedral_restraints(const SireMM::DihedralRestraints &restraints, // TODO: Add support for multiple dihedral restraints const auto energy_expression = QString( - "rho*k*delta*delta;" - "delta=(phi-phi0)") + "rho*k*min(dtheta, 2*pi-dtheta)^2;" + "dtheta = abs(theta-theta0);" + "pi = 3.1415926535;") .toStdString(); auto *restraintff = new OpenMM::CustomTorsionForce(energy_expression); + // OLD CODE + // restraintff->addPerTorsionParameter("rho"); + // restraintff->addPerTorsionParameter("k"); + // restraintff->addPerTorsionParameter("phi0"); + + // it seems that OpenMM wants to call the torsion angle theta rather than phi + // we need to rename our parameters accordingly + // NEW CODE restraintff->addPerTorsionParameter("rho"); restraintff->addPerTorsionParameter("k"); - restraintff->addPerTorsionParameter("phi0"); + restraintff->addPerTorsionParameter("theta0"); restraintff->setUsesPeriodicBoundaryConditions(true); @@ -450,7 +459,7 @@ void _add_dihedral_restraints(const SireMM::DihedralRestraints &restraints, std::vector parameters; parameters.resize(3); - parameters[0] = 1.0; // rho + parameters[0] = 1.0; // rho parameters[1] = restraint.kphi().value() * internal_to_ktheta; // kphi parameters[2] = restraint.phi0().value(); // phi0 (already in radians) From 4a0726d5c2a36f7e0954252cab75432bf8f6f195 Mon Sep 17 00:00:00 2001 From: Audrius Kalpokas Date: Wed, 17 Jul 2024 11:47:06 +0100 Subject: [PATCH 007/102] Added full end-to-end alchemical dihedral restraints implementation - bugs might still be lurking somewhere --- .../Convert/SireOpenMM/LambdaLever.pypp.cpp | 2 +- wrapper/Convert/SireOpenMM/lambdalever.cpp | 55 ++++++++++++++++++- .../SireOpenMM/sire_to_openmm_system.cpp | 2 +- 3 files changed, 56 insertions(+), 3 deletions(-) diff --git a/wrapper/Convert/SireOpenMM/LambdaLever.pypp.cpp b/wrapper/Convert/SireOpenMM/LambdaLever.pypp.cpp index 85786b4a2..152f5fc2f 100644 --- a/wrapper/Convert/SireOpenMM/LambdaLever.pypp.cpp +++ b/wrapper/Convert/SireOpenMM/LambdaLever.pypp.cpp @@ -199,7 +199,7 @@ void register_LambdaLever_class(){ } { //::SireOpenMM::LambdaLever::setExceptionIndicies - typedef void ( ::SireOpenMM::LambdaLever::*setExceptionIndicies_function_type)( int,::QString const &,::QVector< boost::tuples::tuple< int, int, 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, boost::tuples::null_type > > const & ) ; + typedef void ( ::SireOpenMM::LambdaLever::*setExceptionIndicies_function_type)( int,::QString const &,::QVector< boost::tuples::tuple< int, int > > const & ) ; setExceptionIndicies_function_type setExceptionIndicies_function_value( &::SireOpenMM::LambdaLever::setExceptionIndicies ); LambdaLever_exposer.def( diff --git a/wrapper/Convert/SireOpenMM/lambdalever.cpp b/wrapper/Convert/SireOpenMM/lambdalever.cpp index a70a0c18f..e7d4e62fd 100644 --- a/wrapper/Convert/SireOpenMM/lambdalever.cpp +++ b/wrapper/Convert/SireOpenMM/lambdalever.cpp @@ -28,8 +28,8 @@ #include "lambdalever.h" -#include "SireBase/propertymap.h" #include "SireBase/arrayproperty.hpp" +#include "SireBase/propertymap.h" #include "SireCAS/values.h" @@ -1825,6 +1825,53 @@ void _update_restraint_in_context(OpenMM::CustomCompoundBondForce *ff, double rh ff->updateParametersInContext(context); } +/** Update the parameters for a CustomTorsionForce for scale factor 'rho' + * in the passed context */ +void _update_restraint_in_context(OpenMM::CustomTorsionForce *ff, double rho, + OpenMM::Context &context) +{ + if (ff == 0) + throw SireError::invalid_cast(QObject::tr( + "Unable to cast the restraint force to an OpenMM::CustomTorsionForce, " + "despite it reporting that is was an object of this type..."), + CODELOC); + + const int ntorsions = ff->getNumTorsions(); + + if (ntorsions == 0) + // nothing to update + return; + + const int nparams = ff->getNumPerTorsionParameters(); + + if (nparams == 0) + throw SireError::incompatible_error(QObject::tr( + "Unable to set 'rho' for this restraint as it has no custom parameters!"), + CODELOC); + + // we set the first parameter - we can see what the current value + // is from the first restraint. This is because rho should be the + // first parameter and have the same value for all restraints + std::vector custom_params; + custom_params.resize(nparams); + int atom0, atom1, atom2, atom3; + + ff->getTorsionParameters(0, atom0, atom1, atom2, atom3, custom_params); + + if (custom_params[0] == rho) + // nothing to do - it is already equal to this value + return; + + for (int i = 0; i < ntorsions; ++i) + { + ff->getTorsionParameters(i, atom0, atom1, atom2, atom3, custom_params); + custom_params[0] = rho; + ff->setTorsionParameters(i, atom0, atom1, atom2, atom3, custom_params); + } + + ff->updateParametersInContext(context); +} + /** Update the parameters for a CustomBondForce for scale factor 'rho' * in the passed context */ void _update_restraint_in_context(OpenMM::CustomBondForce *ff, double rho, @@ -1886,6 +1933,12 @@ void LambdaLever::updateRestraintInContext(OpenMM::Force &ff, double rho, dynamic_cast(&ff), rho, context); } + else if (ff_type == "CustomTorsionForce") + { + _update_restraint_in_context( + dynamic_cast(&ff), + rho, context); + } else if (ff_type == "CustomCompoundBondForce") { _update_restraint_in_context( diff --git a/wrapper/Convert/SireOpenMM/sire_to_openmm_system.cpp b/wrapper/Convert/SireOpenMM/sire_to_openmm_system.cpp index 10089d82d..d660d5dc7 100644 --- a/wrapper/Convert/SireOpenMM/sire_to_openmm_system.cpp +++ b/wrapper/Convert/SireOpenMM/sire_to_openmm_system.cpp @@ -451,7 +451,7 @@ void _add_dihedral_restraints(const SireMM::DihedralRestraints &restraints, std::vector particles; particles.resize(4); - for (int i = 0; i < 3; ++i) + for (int i = 0; i < 4; ++i) { particles[i] = restraint.atoms()[i]; } From 9eeb82435b366ef833bfcfb5fd4363430131ba11 Mon Sep 17 00:00:00 2001 From: Audrius Kalpokas Date: Fri, 19 Jul 2024 16:55:18 +0100 Subject: [PATCH 008/102] Added full end-to-end alchemical angle restraints implementation, and also renamed the core alchemical angle/dihedral restraint c++ files --- corelib/src/libs/SireMM/CMakeLists.txt | 8 +- corelib/src/libs/SireMM/anglerestraint.cpp | 995 ------------------ corelib/src/libs/SireMM/anglerestraint.h | 182 ---- ...alrestraint.cpp => dihedralrestraints.cpp} | 15 +- ...hedralrestraint.h => dihedralrestraints.h} | 9 +- src/sire/mm/__init__.py | 5 + src/sire/restraints/__init__.py | 4 +- src/sire/restraints/_restraints.py | 130 ++- wrapper/Convert/SireOpenMM/lambdalever.cpp | 53 + .../SireOpenMM/sire_to_openmm_system.cpp | 84 +- wrapper/MM/AngleRestraint.pypp.cpp | 16 +- wrapper/MM/AngleRestraints.pypp.cpp | 16 +- wrapper/MM/CMakeAutogenFile.txt | 2 + wrapper/MM/DihedralRestraint.pypp.cpp | 16 +- wrapper/MM/DihedralRestraints.pypp.cpp | 16 +- wrapper/MM/SireMM_registrars.cpp | 6 +- wrapper/MM/_MM.main.cpp | 28 +- wrapper/MM/active_headers.h | 4 +- 18 files changed, 262 insertions(+), 1327 deletions(-) delete mode 100644 corelib/src/libs/SireMM/anglerestraint.cpp delete mode 100644 corelib/src/libs/SireMM/anglerestraint.h rename corelib/src/libs/SireMM/{dihedralrestraint.cpp => dihedralrestraints.cpp} (97%) rename corelib/src/libs/SireMM/{dihedralrestraint.h => dihedralrestraints.h} (97%) diff --git a/corelib/src/libs/SireMM/CMakeLists.txt b/corelib/src/libs/SireMM/CMakeLists.txt index f228580aa..f0e650fba 100644 --- a/corelib/src/libs/SireMM/CMakeLists.txt +++ b/corelib/src/libs/SireMM/CMakeLists.txt @@ -16,7 +16,7 @@ include_directories(${TBB_INCLUDE_DIR}) # Define the headers in SireMM set ( SIREMM_HEADERS amberparams.h - anglerestraint.h + anglerestraints.h angle.h atomfunctions.h atomljs.h @@ -43,7 +43,7 @@ set ( SIREMM_HEADERS cljworkspace.h coulombpotential.h dihedral.h - dihedralrestraint.h + dihedralrestraints.h distancerestraint.h errors.h excludedpairs.h @@ -110,7 +110,7 @@ set ( SIREMM_SOURCES amberparams.cpp angle.cpp - anglerestraint.cpp + anglerestraints.cpp atomfunctions.cpp atomljs.cpp bond.cpp @@ -135,7 +135,7 @@ set ( SIREMM_SOURCES cljworkspace.cpp coulombpotential.cpp dihedral.cpp - dihedralrestraint.cpp + dihedralrestraints.cpp distancerestraint.cpp errors.cpp excludedpairs.cpp diff --git a/corelib/src/libs/SireMM/anglerestraint.cpp b/corelib/src/libs/SireMM/anglerestraint.cpp deleted file mode 100644 index 00e93dde5..000000000 --- a/corelib/src/libs/SireMM/anglerestraint.cpp +++ /dev/null @@ -1,995 +0,0 @@ -/********************************************\ - * - * Sire - Molecular Simulation Framework - * - * Copyright (C) 2009 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 at https://sire.openbiosim.org - * -\*********************************************/ - -#include "anglerestraint.h" - -// #include "SireFF/forcetable.h" - -// #include "SireCAS/conditional.h" -// #include "SireCAS/power.h" -// #include "SireCAS/symbols.h" -// #include "SireCAS/values.h" - -#include "SireID/index.h" - -// #include "SireUnits/angle.h" -#include "SireUnits/units.h" - -#include "SireStream/datastream.h" -#include "SireStream/shareddatastream.h" - -#include "SireCAS/errors.h" - -#include - -using namespace SireMM; -// using namespace SireMol; -// using namespace SireFF; -using namespace SireID; -using namespace SireBase; -// using namespace SireCAS; -using namespace SireMaths; -using namespace SireStream; -using namespace SireUnits; -using namespace SireUnits::Dimension; - -//////////// -//////////// Implementation of AngleRestraint -//////////// - -static const RegisterMetaType r_angrest; - -/** Serialise to a binary datastream */ - -QDataStream &operator<<(QDataStream &ds, const AngleRestraint &angrest) -{ - writeHeader(ds, r_angrest, 1); - - SharedDataStream sds(ds); - - sds << angrest.atms << angrest._theta0 << angrest._ktheta; - - return ds; -} - -/** Extract from a binary datastream */ -QDataStream &operator>>(QDataStream &ds, AngleRestraint &angrest) -{ - VersionID v = readHeader(ds, r_angrest); - - if (v == 1) - { - SharedDataStream sds(ds); - - sds >> angrest.atms >> angrest._theta0 >> angrest._ktheta; - } - else - throw version_error(v, "1", r_angrest, CODELOC); - - return ds; -} - -/** Null constructor */ -AngleRestraint::AngleRestraint() - : ConcreteProperty(), - _ktheta(0), _theta0(0) -{ -} - -/** Construct a restraint that acts on the angle within the - three atoms 'atom0', 'atom1' and 'atom2' (theta == a(012)), - restraining the angle within these atoms */ -AngleRestraint::AngleRestraint(const QList &atoms, - const SireUnits::Dimension::Angle &theta0, - const SireUnits::Dimension::HarmonicAngleConstant &ktheta) - : ConcreteProperty(), - _theta0(theta0), _ktheta(ktheta) -{ - // Need to think here about validating the angle and force constant values - // if (atoms.count() != 3) - // { - // throw SireError::invalid_arg(QObject::tr( - // "Wrong number of inputs for an Angle restraint. You need to " - // "provide 3 atoms (%1).") - // .arg(atoms.count()), - // // .arg(theta0.count()) - // // .arg(ktheta.count()), - // CODELOC); - // } - - // make sure that we have 3 distinct atoms - QSet distinct; - distinct.reserve(3); - - for (const auto &atom : atoms) - { - if (atom >= 0) - distinct.insert(atom); - } - - // if (distinct.count() != 3) - // throw SireError::invalid_arg(QObject::tr( - // "There is something wrong with the atoms provided. " - // "They should all be unique and all greater than or equal to 0."), - // CODELOC); - - atms = atoms.toVector(); -} - -/* Copy constructor*/ -AngleRestraint::AngleRestraint(const AngleRestraint &other) - : ConcreteProperty(other), - atms(other.atms), _theta0(other._theta0), _ktheta(other._ktheta) - -{ -} - -/* Destructor */ -AngleRestraint::~AngleRestraint() -{ -} - -AngleRestraint &AngleRestraint::operator=(const AngleRestraint &other) -{ - if (this != &other) - { - Property::operator=(other); - atms = other.atms; - _theta0 = other._theta0; - _ktheta = other._ktheta; - } - - return *this; -} - -bool AngleRestraint::operator==(const AngleRestraint &other) const -{ - return atms == other.atms and - _theta0 == other._theta0 and - _ktheta == other._ktheta; -} - -bool AngleRestraint::operator!=(const AngleRestraint &other) const -{ - return not operator==(other); -} - -AngleRestraints AngleRestraint::operator+(const AngleRestraint &other) const -{ - return AngleRestraints(*this) + other; -} - -AngleRestraints AngleRestraint::operator+(const AngleRestraints &other) const -{ - return AngleRestraints(*this) + other; -} - -const char *AngleRestraint::typeName() -{ - return QMetaType::typeName(qMetaTypeId()); -} - -const char *AngleRestraint::what() const -{ - return AngleRestraint::typeName(); -} - -AngleRestraint *AngleRestraint::clone() const -{ - return new AngleRestraint(*this); -} - -bool AngleRestraint::isNull() const -{ - return atms.isEmpty(); -} - -QString AngleRestraint::toString() const -{ - if (this->isNull()) - return QObject::tr("AngleRestraint::null"); - else - { - QStringList a; - - for (const auto &atom : atms) - { - a.append(QString::number(atom)); - } - return QString("AngleRestraint( [%1], theta0=%2, ktheta=%3 )") - .arg(a.join(", ")) - .arg(_theta0.toString()) - .arg(_ktheta.toString()); - } -} - -/** Return the force constant for the restraint */ -SireUnits::Dimension::HarmonicAngleConstant AngleRestraint::ktheta() const -{ - return this->_ktheta; -} - -/** Return the equilibrium angle for the restraint */ -SireUnits::Dimension::Angle AngleRestraint::theta0() const -{ - return this->_theta0; -} - -/** Return the atoms involved in the restraint */ -QVector AngleRestraint::atoms() const -{ - return this->atms; -} - -/////// -/////// Implementation of AngleRestraints -/////// - -/** Serialise to a binary datastream */ - -static const RegisterMetaType r_angrests; - -QDataStream &operator<<(QDataStream &ds, const AngleRestraints &angrests) -{ - writeHeader(ds, r_angrests, 1); - - SharedDataStream sds(ds); - - sds << angrests.r - << static_cast(angrests); - - return ds; -} - -/** Extract from a binary datastream */ -QDataStream &operator>>(QDataStream &ds, AngleRestraints &angrests) -{ - VersionID v = readHeader(ds, r_angrests); - - if (v == 1) - { - SharedDataStream sds(ds); - - sds >> angrests.r >> - static_cast(angrests); - } - else - throw version_error(v, "1", r_angrests, CODELOC); - - return ds; -} - -/** Null constructor */ -AngleRestraints::AngleRestraints() - : ConcreteProperty() -{ -} - -AngleRestraints::AngleRestraints(const QString &name) - : ConcreteProperty(name) -{ -} - -AngleRestraints::AngleRestraints(const AngleRestraint &restraint) - : ConcreteProperty() -{ - if (not restraint.isNull()) - r.append(restraint); -} - -AngleRestraints::AngleRestraints(const QList &restraints) - : ConcreteProperty() -{ - for (const auto &restraint : restraints) - { - if (not restraint.isNull()) - r.append(restraint); - } -} - -AngleRestraints::AngleRestraints(const QString &name, - const AngleRestraint &restraint) - : ConcreteProperty(name) -{ - if (not restraint.isNull()) - r.append(restraint); -} - -AngleRestraints::AngleRestraints(const QString &name, - const QList &restraints) - : ConcreteProperty(name) -{ - for (const auto &restraint : restraints) - { - if (not restraint.isNull()) - r.append(restraint); - } -} - -AngleRestraints::AngleRestraints(const AngleRestraints &other) - : ConcreteProperty(other), r(other.r) -{ -} - -/* Desctructor */ -AngleRestraints::~AngleRestraints() -{ -} - -AngleRestraints &AngleRestraints::operator=(const AngleRestraints &other) -{ - if (this != &other) - { - Restraints::operator=(other); - r = other.r; - } - - return *this; -} - -bool AngleRestraints::operator==(const AngleRestraints &other) const -{ - return r == other.r and Restraints::operator==(other); -} - -bool AngleRestraints::operator!=(const AngleRestraints &other) const -{ - return not operator==(other); -} - -const char *AngleRestraints::typeName() -{ - return QMetaType::typeName(qMetaTypeId()); -} - -const char *AngleRestraints::what() const -{ - return AngleRestraints::typeName(); -} - -AngleRestraints *AngleRestraints::clone() const -{ - return new AngleRestraints(*this); -} - -QString AngleRestraints::toString() const -{ - if (this->isEmpty()) - return QObject::tr("AngleRestraints::null"); - - QStringList parts; - - const auto n = this->count(); - - if (n <= 10) - { - for (int i = 0; i < n; i++) - { - parts.append(QObject::tr("%1: %2").arg(i).arg(this->r.at(i).toString())); - } - } - else - { - for (int i = 0; i < 5; i++) - { - parts.append(QObject::tr("%1: %2").arg(i).arg(this->r.at(i).toString())); - } - - parts.append("..."); - - for (int i = n - 5; i < n; i++) - { - parts.append(QObject::tr("%1: %2").arg(i).arg(this->r.at(i).toString())); - } - } - - return QObject::tr("AngleRestraints( name=%1, size=%2\n%3\n )") - .arg(this->name()) - .arg(n) - .arg(parts.join("\n")); -} - -/** Return whether or not this is empty */ -bool AngleRestraints::isEmpty() const -{ - return this->r.isEmpty(); -} - -/** Return whether or not this is empty */ -bool AngleRestraints::isNull() const -{ - return this->isEmpty(); -} - -/** Return the number of restraints */ -int AngleRestraints::nRestraints() const -{ - return this->r.count(); -} - -/** Return the number of restraints */ -int AngleRestraints::count() const -{ - return this->nRestraints(); -} - -/** Return the number of restraints */ -int AngleRestraints::size() const -{ - return this->nRestraints(); -} - -/** Return the ith restraint */ -const AngleRestraint &AngleRestraints::at(int i) const -{ - i = SireID::Index(i).map(this->r.count()); - - return this->r.at(i); -} - -/** Return the ith restraint */ -const AngleRestraint &AngleRestraints::operator[](int i) const -{ - return this->at(i); -} - -/** Return all of the restraints */ -QList AngleRestraints::restraints() const -{ - return this->r; -} - -/** Add a restraints onto the list */ -void AngleRestraints::add(const AngleRestraint &restraint) -{ - if (not restraint.isNull()) - r.append(restraint); -} - -/** Add a restraint onto the list */ -void AngleRestraints::add(const AngleRestraints &restraints) -{ - this->r += restraints.r; -} - -/** Add a restraint onto the list */ -AngleRestraints &AngleRestraints::operator+=(const AngleRestraint &restraint) -{ - this->add(restraint); - return *this; -} - -/** Add a restraint onto the list */ -AngleRestraints AngleRestraints::operator+(const AngleRestraint &restraint) const -{ - AngleRestraints ret(*this); - ret += restraint; - return *this; -} - -/** Add restraints onto the list */ -AngleRestraints &AngleRestraints::operator+=(const AngleRestraints &restraints) -{ - this->add(restraints); - return *this; -} - -/** Add restraints onto the list */ -AngleRestraints AngleRestraints::operator+(const AngleRestraints &restraints) const -{ - AngleRestraints ret(*this); - ret += restraints; - return *this; -} - -// QDataStream &operator<<(QDataStream &ds, const AngleRestraint &angrest) -// { -// writeHeader(ds, r_angrest, 1); - -// SharedDataStream sds(ds); - -// sds << angrest.p[0] << angrest.p[1] << angrest.p[2] << angrest.force_expression -// << static_cast(angrest); - -// return ds; -// } - -// /** Extract from a binary datastream */ -// QDataStream &operator>>(QDataStream &ds, AngleRestraint &angrest) -// { -// VersionID v = readHeader(ds, r_angrest); - -// if (v == 1) -// { -// SharedDataStream sds(ds); - -// sds >> angrest.p[0] >> angrest.p[1] >> angrest.p[2] >> angrest.force_expression >> -// static_cast(angrest); - -// angrest.intra_molecule_points = Point::areIntraMoleculePoints(angrest.p[0], angrest.p[1]) and -// Point::areIntraMoleculePoints(angrest.p[0], angrest.p[2]); -// } -// else -// throw version_error(v, "1", r_angrest, CODELOC); - -// return ds; -// } - -// Q_GLOBAL_STATIC_WITH_ARGS(Symbol, getThetaSymbol, ("theta")); - -/** Return the symbol that represents the angle between the points (theta) */ -// const Symbol &AngleRestraint::theta() -// { -// return *(getThetaSymbol()); -// } - -// /** Constructor */ -// AngleRestraint::AngleRestraint() : ConcreteProperty() -// { -// } - -// void AngleRestraint::calculateTheta() -// { -// if (this->restraintFunction().isFunction(theta())) -// { -// SireUnits::Dimension::Angle angle; - -// if (intra_molecule_points) -// // we don't use the space when calculating intra-molecular angles -// angle = Vector::angle(p[0].read().point(), p[1].read().point(), p[2].read().point()); -// else -// angle = this->space().calcAngle(p[0].read().point(), p[1].read().point(), p[2].read().point()); - -// ExpressionRestraint3D::_pvt_setValue(theta(), angle); -// } -// } - -// AngleRestraint::AngleRestraint(const PointRef &point0, const PointRef &point1, const PointRef &point2, -// const Expression &restraint) -// : ConcreteProperty(restraint) -// { -// p[0] = point0; -// p[1] = point1; -// p[2] = point2; - -// force_expression = this->restraintFunction().differentiate(theta()); - -// if (force_expression.isConstant()) -// force_expression = force_expression.evaluate(Values()); - -// intra_molecule_points = Point::areIntraMoleculePoints(p[0], p[1]) and Point::areIntraMoleculePoints(p[0], p[2]); - -// this->calculateTheta(); -// } - -// /** Construct a restraint that acts on the angle within the -// three points 'point0', 'point1' and 'point2' (theta == a(012)), -// restraining the angle within these points using the expression -// 'restraint' */ -// AngleRestraint::AngleRestraint(const PointRef &point0, const PointRef &point1, const PointRef &point2, -// const Expression &restraint, const Values &values) -// : ConcreteProperty(restraint, values) -// { -// p[0] = point0; -// p[1] = point1; -// p[2] = point2; - -// force_expression = this->restraintFunction().differentiate(theta()); - -// if (force_expression.isConstant()) -// force_expression = force_expression.evaluate(Values()); - -// intra_molecule_points = Point::areIntraMoleculePoints(p[0], p[1]) and Point::areIntraMoleculePoints(p[0], p[2]); - -// this->calculateTheta(); -// } - -// /** Internal constructor used to construct a restraint using the specified -// points, energy expression and force expression */ -// AngleRestraint::AngleRestraint(const PointRef &point0, const PointRef &point1, const PointRef &point2, -// const Expression &nrg_restraint, const Expression &force_restraint) -// : ConcreteProperty(nrg_restraint), force_expression(force_restraint) -// { -// p[0] = point0; -// p[1] = point1; -// p[2] = point2; - -// if (force_expression.isConstant()) -// { -// force_expression = force_expression.evaluate(Values()); -// } -// else -// { -// if (not this->restraintFunction().symbols().contains(force_expression.symbols())) -// throw SireError::incompatible_error( -// QObject::tr("You cannot use a force function which uses more symbols " -// "(%1) than the energy function (%2).") -// .arg(Sire::toString(force_expression.symbols()), Sire::toString(restraintFunction().symbols())), -// CODELOC); -// } - -// intra_molecule_points = Point::areIntraMoleculePoints(p[0], p[1]) and Point::areIntraMoleculePoints(p[0], p[2]); - -// this->calculateTheta(); -// } - -// /** Copy constructor */ -// AngleRestraint::AngleRestraint(const AngleRestraint &other) -// : ConcreteProperty(other), force_expression(other.force_expression), -// intra_molecule_points(other.intra_molecule_points) -// { -// for (int i = 0; i < this->nPoints(); ++i) -// { -// p[i] = other.p[i]; -// } -// } - -// /** Destructor */ -// AngleRestraint::~AngleRestraint() -// { -// } - -// /** Copy assignment operator */ -// AngleRestraint &AngleRestraint::operator=(const AngleRestraint &other) -// { -// if (this != &other) -// { -// ExpressionRestraint3D::operator=(other); - -// for (int i = 0; i < this->nPoints(); ++i) -// { -// p[i] = other.p[i]; -// } - -// force_expression = other.force_expression; -// intra_molecule_points = other.intra_molecule_points; -// } - -// return *this; -// } - -// /** Comparison operator */ -// bool AngleRestraint::operator==(const AngleRestraint &other) const -// { -// return this == &other or (ExpressionRestraint3D::operator==(other) and p[0] == other.p[0] and p[1] == other.p[1] and -// p[2] == other.p[2] and force_expression == other.force_expression); -// } - -// /** Comparison operator */ -// bool AngleRestraint::operator!=(const AngleRestraint &other) const -// { -// return not AngleRestraint::operator==(other); -// } - -// const char *AngleRestraint::typeName() -// { -// return QMetaType::typeName(qMetaTypeId()); -// } - -// /** This restraint involves three points */ -// int AngleRestraint::nPoints() const -// { -// return 3; -// } - -// /** Return the ith point */ -// const Point &AngleRestraint::point(int i) const -// { -// i = Index(i).map(this->nPoints()); - -// return p[i].read(); -// } - -// /** Return the first point */ -// const Point &AngleRestraint::point0() const -// { -// return p[0].read(); -// } - -// /** Return the second point */ -// const Point &AngleRestraint::point1() const -// { -// return p[1].read(); -// } - -// /** Return the third point */ -// const Point &AngleRestraint::point2() const -// { -// return p[2].read(); -// } - -// /** Return the built-in symbols of this restraint */ -// Symbols AngleRestraint::builtinSymbols() const -// { -// if (this->restraintFunction().isFunction(theta())) -// return theta(); -// else -// return Symbols(); -// } - -// /** Return the values of the built-in symbols of this restraint */ -// Values AngleRestraint::builtinValues() const -// { -// if (this->restraintFunction().isFunction(theta())) -// return theta() == this->values()[theta()]; -// else -// return Values(); -// } - -// /** Return the differential of this restraint with respect to -// the symbol 'symbol' - -// \throw SireCAS::unavailable_differential -// */ -// RestraintPtr AngleRestraint::differentiate(const Symbol &symbol) const -// { -// if (this->restraintFunction().isFunction(symbol)) -// return AngleRestraint(p[0], p[1], p[2], restraintFunction().differentiate(symbol), this->values()); -// else -// return NullRestraint(); -// } - -// /** Set the space used to evaluate the energy of this restraint - -// \throw SireVol::incompatible_space -// */ -// void AngleRestraint::setSpace(const Space &new_space) -// { -// if (not this->space().equals(new_space)) -// { -// AngleRestraint old_state(*this); - -// try -// { -// for (int i = 0; i < this->nPoints(); ++i) -// { -// p[i].edit().setSpace(new_space); -// } - -// Restraint::setSpace(new_space); - -// this->calculateTheta(); -// } -// catch (...) -// { -// AngleRestraint::operator=(old_state); -// throw; -// } -// } -// } - -// /** Return the function used to calculate the restraint force */ -// const Expression &AngleRestraint::differentialRestraintFunction() const -// { -// return force_expression; -// } - -// /** Calculate the force acting on the molecule in the forcetable 'forcetable' -// caused by this restraint, and add it on to the forcetable scaled by -// 'scale_force' */ -// void AngleRestraint::force(MolForceTable &forcetable, double scale_force) const -// { -// bool in_p0 = p[0].read().contains(forcetable.molNum()); -// bool in_p1 = p[1].read().contains(forcetable.molNum()); -// bool in_p2 = p[2].read().contains(forcetable.molNum()); - -// if (not(in_p0 or in_p1 or in_p2)) -// // this molecule is not affected by the restraint -// return; - -// throw SireError::incomplete_code(QObject::tr("Haven't yet written the code to calculate forces caused " -// "by an angle restraint."), -// CODELOC); -// } - -// /** Calculate the force acting on the molecules in the forcetable 'forcetable' -// caused by this restraint, and add it on to the forcetable scaled by -// 'scale_force' */ -// void AngleRestraint::force(ForceTable &forcetable, double scale_force) const -// { -// bool in_p0 = p[0].read().usesMoleculesIn(forcetable); -// bool in_p1 = p[1].read().usesMoleculesIn(forcetable); -// bool in_p2 = p[2].read().usesMoleculesIn(forcetable); - -// if (not(in_p0 or in_p1 or in_p2)) -// // this molecule is not affected by the restraint -// return; - -// throw SireError::incomplete_code(QObject::tr("Haven't yet written the code to calculate forces caused " -// "by an angle restraint."), -// CODELOC); -// } - -// /** Update the points of this restraint using new molecule data from 'moldata' - -// \throw SireBase::missing_property -// \throw SireError::invalid_cast -// \throw SireError::incompatible_error -// */ -// void AngleRestraint::update(const MoleculeData &moldata) -// { -// if (this->contains(moldata.number())) -// { -// AngleRestraint old_state(*this); - -// try -// { -// for (int i = 0; i < this->nPoints(); ++i) -// { -// p[i].edit().update(moldata); -// } - -// this->calculateTheta(); -// } -// catch (...) -// { -// AngleRestraint::operator=(old_state); -// throw; -// } -// } -// } - -// /** Update the points of this restraint using new molecule data from 'molecules' - -// \throw SireBase::missing_property -// \throw SireError::invalid_cast -// \throw SireError::incompatible_error -// */ -// void AngleRestraint::update(const Molecules &molecules) -// { -// if (this->usesMoleculesIn(molecules)) -// { -// AngleRestraint old_state(*this); - -// try -// { -// for (int i = 0; i < this->nPoints(); ++i) -// { -// p[i].edit().update(molecules); -// } - -// this->calculateTheta(); -// } -// catch (...) -// { -// AngleRestraint::operator=(old_state); -// throw; -// } -// } -// } - -// /** Return the molecules used in this restraint */ -// Molecules AngleRestraint::molecules() const -// { -// Molecules mols; - -// for (int i = 0; i < this->nPoints(); ++i) -// { -// mols += p[i].read().molecules(); -// } - -// return mols; -// } - -// /** Return whether or not this restraint affects the molecule -// with number 'molnum' */ -// bool AngleRestraint::contains(MolNum molnum) const -// { -// return p[0].read().contains(molnum) or p[1].read().contains(molnum) or p[2].read().contains(molnum); -// } - -// /** Return whether or not this restraint affects the molecule -// with ID 'molid' */ -// bool AngleRestraint::contains(const MolID &molid) const -// { -// return p[0].read().contains(molid) or p[1].read().contains(molid) or p[2].read().contains(molid); -// } - -// /** Return whether or not this restraint involves any of the molecules -// that are in the forcetable 'forcetable' */ -// bool AngleRestraint::usesMoleculesIn(const ForceTable &forcetable) const -// { -// return p[0].read().usesMoleculesIn(forcetable) or p[1].read().usesMoleculesIn(forcetable) or -// p[2].read().usesMoleculesIn(forcetable); -// } - -// /** Return whether or not this restraint involves any of the molecules -// in 'molecules' */ -// bool AngleRestraint::usesMoleculesIn(const Molecules &molecules) const -// { -// return p[0].read().usesMoleculesIn(molecules) or p[1].read().usesMoleculesIn(molecules) or -// p[2].read().usesMoleculesIn(molecules); -// } - -// static Expression harmonicFunction(double force_constant) -// { -// if (SireMaths::isZero(force_constant)) -// return 0; -// else -// return force_constant * pow(AngleRestraint::theta(), 2); -// } - -// static Expression diffHarmonicFunction(double force_constant) -// { -// if (SireMaths::isZero(force_constant)) -// return 0; -// else -// return (2 * force_constant) * AngleRestraint::theta(); -// } - -// /** Return a distance restraint that applies a harmonic potential between -// the points 'point0' and 'point1' using a force constant 'force_constant' */ -// AngleRestraint AngleRestraint::harmonic(const PointRef &point0, const PointRef &point1, const PointRef &point2, -// const HarmonicAngleForceConstant &force_constant) -// { -// return AngleRestraint(point0, point1, point2, ::harmonicFunction(force_constant), -// ::diffHarmonicFunction(force_constant)); -// } - -// static Expression halfHarmonicFunction(double force_constant, double angle) -// { -// if (SireMaths::isZero(force_constant)) -// return 0; - -// else if (angle <= 0) -// // this is just a harmonic function -// return ::harmonicFunction(force_constant); - -// else -// { -// const Symbol &theta = AngleRestraint::theta(); -// return Conditional(GreaterThan(theta, angle), force_constant * pow(theta - angle, 2), 0); -// } -// } - -// static Expression diffHalfHarmonicFunction(double force_constant, double angle) -// { -// if (SireMaths::isZero(force_constant)) -// return 0; - -// else if (angle <= 0) -// // this is just a harmonic function -// return ::diffHarmonicFunction(force_constant); - -// else -// { -// const Symbol &theta = AngleRestraint::theta(); -// return Conditional(GreaterThan(theta, angle), (2 * force_constant) * (theta - angle), 0); -// } -// } - -// /** Return a distance restraint that applied a half-harmonic potential -// between the points 'point0' and 'point1' above a distance 'distance' -// using a force constant 'force_constant' */ -// AngleRestraint AngleRestraint::halfHarmonic(const PointRef &point0, const PointRef &point1, const PointRef &point2, -// const Angle &angle, const HarmonicAngleForceConstant &force_constant) -// { -// double acute_angle = acute(angle).to(radians); - -// return AngleRestraint(point0, point1, point2, ::halfHarmonicFunction(force_constant, acute_angle), -// ::diffHalfHarmonicFunction(force_constant, acute_angle)); -// } diff --git a/corelib/src/libs/SireMM/anglerestraint.h b/corelib/src/libs/SireMM/anglerestraint.h deleted file mode 100644 index c8a4f228e..000000000 --- a/corelib/src/libs/SireMM/anglerestraint.h +++ /dev/null @@ -1,182 +0,0 @@ -/********************************************\ - * - * Sire - Molecular Simulation Framework - * - * Copyright (C) 2009 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 at https://sire.openbiosim.org - * -\*********************************************/ - -#ifndef SIREMM_ANGLERESTRAINT_H -#define SIREMM_ANGLERESTRAINT_H - -// #include "SireFF/point.h" - -#include "restraints.h" - -// #include "SireCAS/expression.h" -// #include "SireCAS/symbol.h" - -#include "SireUnits/dimensions.h" -#include "SireUnits/generalunit.h" - -SIRE_BEGIN_HEADER - -namespace SireMM -{ - class AngleRestraint; - class AngleRestraints; -} - -SIREMM_EXPORT QDataStream &operator<<(QDataStream &, const SireMM::AngleRestraint &); -SIREMM_EXPORT QDataStream &operator>>(QDataStream &, SireMM::AngleRestraint &); - -SIREMM_EXPORT QDataStream &operator<<(QDataStream &, const SireMM::AngleRestraints &); -SIREMM_EXPORT QDataStream &operator>>(QDataStream &, SireMM::AngleRestraints &); - -namespace SireMM -{ - - /** This class represents a single angle restraint between any three - * atoms in a system - * @author Christopher Woods - */ - class SIREMM_EXPORT AngleRestraint - : public SireBase::ConcreteProperty - { - - friend SIREMM_EXPORT QDataStream & ::operator<<(QDataStream &, const SireMM::AngleRestraint &); - friend SIREMM_EXPORT QDataStream & ::operator>>(QDataStream &, SireMM::AngleRestraint &); - - public: - AngleRestraint(); - AngleRestraint(const QList &atoms, - const SireUnits::Dimension::Angle &theta0, - const SireUnits::Dimension::HarmonicAngleConstant &ktheta); - - AngleRestraint(const AngleRestraint &other); - - ~AngleRestraint(); - - AngleRestraint &operator=(const AngleRestraint &other); - - bool operator==(const AngleRestraint &other) const; - bool operator!=(const AngleRestraint &other) const; - - AngleRestraints operator+(const AngleRestraint &other) const; - AngleRestraints operator+(const AngleRestraints &other) const; - - static const char *typeName(); - const char *what() const; - - AngleRestraint *clone() const; - - QString toString() const; - - bool isNull() const; - - QVector atoms() const; - - SireUnits::Dimension::Angle theta0() const; - SireUnits::Dimension::HarmonicAngleConstant ktheta() const; - - private: - /** Atoms involved in the angle restraint */ - QVector atms; - - /** Equilibrium angle */ - SireUnits::Dimension::Angle _theta0; - - /** Harmonic angle constant */ - SireUnits::Dimension::HarmonicAngleConstant _ktheta; - }; - - /** This class represents a collection of angle restraints */ - class SIREMM_EXPORT AngleRestraints - : public SireBase::ConcreteProperty - { - friend SIREMM_EXPORT QDataStream & ::operator<<(QDataStream &, const SireMM::AngleRestraints &); - friend SIREMM_EXPORT QDataStream & ::operator>>(QDataStream &, SireMM::AngleRestraints &); - - public: - AngleRestraints(); - AngleRestraints(const QString &name); - - AngleRestraints(const AngleRestraint &restraint); - AngleRestraints(const QList &restraints); - - AngleRestraints(const QString &name, - const AngleRestraint &restraint); - - AngleRestraints(const QString &name, - const QList &restraints); - - AngleRestraints(const AngleRestraints &other); - - ~AngleRestraints(); - - AngleRestraints &operator=(const AngleRestraints &other); - - bool operator==(const AngleRestraints &other) const; - bool operator!=(const AngleRestraints &other) const; - - static const char *typeName(); - const char *what() const; - - AngleRestraints *clone() const; - - QString toString() const; - - bool isEmpty() const; - bool isNull() const; - - int count() const; - int size() const; - int nRestraints() const; - - const AngleRestraint &at(int i) const; - const AngleRestraint &operator[](int i) const; - - QList restraints() const; - - void add(const AngleRestraint &restraint); - void add(const AngleRestraints &restraints); - - AngleRestraints &operator+=(const AngleRestraint &restraint); - AngleRestraints &operator+=(const AngleRestraints &restraints); - - AngleRestraints operator+(const AngleRestraint &restraint) const; - AngleRestraints operator+(const AngleRestraints &restraints) const; - - private: - /** List of restraints */ - QList r; - }; -} - -Q_DECLARE_METATYPE(SireMM::AngleRestraint) -Q_DECLARE_METATYPE(SireMM::AngleRestraints) - -SIRE_EXPOSE_CLASS(SireMM::AngleRestraint) -SIRE_EXPOSE_CLASS(SireMM::AngleRestraints) -SIRE_END_HEADER - -#endif diff --git a/corelib/src/libs/SireMM/dihedralrestraint.cpp b/corelib/src/libs/SireMM/dihedralrestraints.cpp similarity index 97% rename from corelib/src/libs/SireMM/dihedralrestraint.cpp rename to corelib/src/libs/SireMM/dihedralrestraints.cpp index 68ed915f5..61a086e15 100644 --- a/corelib/src/libs/SireMM/dihedralrestraint.cpp +++ b/corelib/src/libs/SireMM/dihedralrestraints.cpp @@ -25,20 +25,10 @@ * \*********************************************/ -#include "dihedralrestraint.h" - -// #include "SireFF/forcetable.h" - -// #include "SireCAS/conditional.h" -// #include "SireCAS/power.h" -// #include "SireCAS/symbols.h" -// #include "SireCAS/values.h" +#include "dihedralrestraints.h" #include "SireID/index.h" - -// #include "SireUnits/angle.h" #include "SireUnits/units.h" - #include "SireStream/datastream.h" #include "SireStream/shareddatastream.h" @@ -47,11 +37,8 @@ #include using namespace SireMM; -// using namespace SireMol; -// using namespace SireFF; using namespace SireID; using namespace SireBase; -// using namespace SireCAS; using namespace SireMaths; using namespace SireStream; using namespace SireUnits; diff --git a/corelib/src/libs/SireMM/dihedralrestraint.h b/corelib/src/libs/SireMM/dihedralrestraints.h similarity index 97% rename from corelib/src/libs/SireMM/dihedralrestraint.h rename to corelib/src/libs/SireMM/dihedralrestraints.h index b340a3411..d3f6b8a35 100644 --- a/corelib/src/libs/SireMM/dihedralrestraint.h +++ b/corelib/src/libs/SireMM/dihedralrestraints.h @@ -25,16 +25,11 @@ * \*********************************************/ -#ifndef SIREMM_DIHEDRALRESTRAINT_H -#define SIREMM_DIHEDRALRESTRAINT_H - -// #include "SireFF/point.h" +#ifndef SIREMM_DIHEDRALRESTRAINTS_H +#define SIREMM_DIHEDRALRESTRAINTS_H #include "restraints.h" -// #include "SireCAS/expression.h" -// #include "SireCAS/symbol.h" - #include "SireUnits/dimensions.h" #include "SireUnits/generalunit.h" diff --git a/src/sire/mm/__init__.py b/src/sire/mm/__init__.py index 047d4a585..9df64e5d1 100644 --- a/src/sire/mm/__init__.py +++ b/src/sire/mm/__init__.py @@ -4,6 +4,8 @@ "AmberDihPart", "AmberDihedral", "Angle", + "AngleRestraint", + "AngleRestraints", "Bond", "BondRestraint", "BondRestraints", @@ -31,6 +33,9 @@ _use_new_api() +AngleRestraint = _MM.AngleRestraint +AngleRestraints = _MM.AngleRestraints + # It would be better if these were called "DistanceRestraints", # but there is already a legacy Sire.MM class with this name BondRestraint = _MM.BondRestraint diff --git a/src/sire/restraints/__init__.py b/src/sire/restraints/__init__.py index f8283671f..166cb985c 100644 --- a/src/sire/restraints/__init__.py +++ b/src/sire/restraints/__init__.py @@ -1,4 +1,4 @@ -__all__ = ["positional", "bond", "distance", "boresch", "get_standard_state_correction"] +__all__ = ["angle", "positional", "bond", "dihedral", "distance", "boresch", "get_standard_state_correction"] -from ._restraints import bond, boresch, dihedral, distance, positional +from ._restraints import angle, bond, boresch, dihedral, distance, positional from ._standard_state_correction import get_standard_state_correction diff --git a/src/sire/restraints/_restraints.py b/src/sire/restraints/_restraints.py index 0d505bd1e..f1cb8377f 100644 --- a/src/sire/restraints/_restraints.py +++ b/src/sire/restraints/_restraints.py @@ -19,6 +19,95 @@ def _to_atoms(mols, atoms): return selection_to_atoms(mols, atoms) +def angle(mols, atoms, theta0=None, ktheta=None, name=None, map=None): + """ + Create a set of anglel restraints from all of the atoms in 'atoms' + where all atoms are contained in the container 'mols', using the + passed values of the force constant 'ktheta' and equilibrium + angle value theta0. + + If theta0 is None, then the current angle for + provided atoms will be used as the equilibium value. + + If ktheta is None, then a default value of 100 kcal mol-1 rad-2 will be used + + Parameters + ---------- + mols : sire.system._system.System + The system containing the atoms. + + atoms : SireMol::Selector + The atoms to restrain. + + ktheta : str or SireUnits::Dimension::GeneralUnit or, optional + The force constants for the angle restraints. + If None, this will default to 100 kcal mol-1 rad-2. + Default is None. + + theta0 : str or SireUnits::Dimension::GeneralUnit, optional + The equilibrium angles for the angle restraints. If None, these + will be measured from the current coordinates of the atoms. + Default is None. + + Returns + ------- + AnglelRestraints : SireMM::AngleRestraints + A container of angle restraints, where the first restraint is + the AngleRestraint created. The angle restraint created can be + extracted with AngleRestraints[0]. + """ + from .. import u + from ..base import create_map + from ..mm import AngleRestraint, AngleRestraints + + map = create_map(map) + map_dict = map.to_dict() + ktheta = ktheta if ktheta is not None else map_dict.get("ktheta", None) + theta0 = theta0 if theta0 is not None else map_dict.get("theta0", None) + name = name if name is not None else map_dict.get("name", None) + + atoms = _to_atoms(mols, atoms) + + if len(atoms) != 3: + raise ValueError( + "You need to provide 3 atoms to create an angle restraint" + f"but only {len(atoms)} atoms were provided." + ) + + from .. import measure + + if ktheta is None: + ktheta = u("100 kcal mol-1 rad-2") + + elif type(ktheta) is list: + raise NotImplementedError( + "Setup of multiple angle restraints simultaneously is not currently supported. You can setup one restraint at a time however." + ) + + # TODO: Add support for multiple angle restraints + if theta0 is None: + # calculate all of the current angles + from .. import measure + theta0 = measure(atoms[0], atoms[1], atoms[2]) + + elif type(theta0) is list: + raise NotImplementedError( + "Setup of multiple angle restraints simultaneously is not currently supported. You can setup one restraint at a time however." + ) + else: + theta0 = u(theta0) + + mols = mols.atoms() + + if name is None: + restraints = AngleRestraints() + else: + restraints = AngleRestraints(name=name) + + restraints.add(AngleRestraint(mols.find(atoms), theta0, ktheta)) + return restraints + + def boresch( mols, receptor, @@ -379,19 +468,13 @@ def dihedral(mols, atoms, phi0=None, kphi=None, name=None, map=None): """ Create a set of dihedral restraints from all of the atoms in 'atoms' where all atoms are contained in the container 'mols', using the - passed values of the force constant 'ktheta' and equilibrium - bond length theta0. + passed values of the force constant 'kphi' and equilibrium + torsion angle phi0. - These restraints will be per atom-atom distance. If a list of k and/or r0 - values are passed, then different values could be used for - different atom-atom distances (assuming the same number as the number of - atom-atom distances). Otherwise, all atom-atom distances will use the - same parameters. - - If r0 is None, then the current atom-atom distance for - each atom-atom pair will be used as the equilibium value. + If phi0 is None, then the current torsional angle for + the provided atoms will be used as the equilibium value. - If k is None, then a default value of 150 kcal mol-1 A-2 will be used + If kphi is None, then a default value of 100 kcal mol-1 rad-2 will be used Parameters ---------- @@ -401,14 +484,13 @@ def dihedral(mols, atoms, phi0=None, kphi=None, name=None, map=None): atoms : SireMol::Selector The atoms to restrain. - kphi : str or SireUnits::Dimension::GeneralUnit or list of str or SireUnits::Dimension::GeneralUnit, optional + kphi : str or SireUnits::Dimension::GeneralUnit or, optional The force constants for the torsion restraints. If None, this will default to 100 kcal mol-1 rad-2. - If a list, then this should be a. Default is None. - theta0 : list of str or SireUnits::Dimension::GeneralUnit, optional - The equilibrium angles for the angle restraints. If None, these + phi0 : str or SireUnits::Dimension::GeneralUnit, optional + The equilibrium torsional angle for restraints. If None, these will be measured from the current coordinates of the atoms. Default is None. @@ -418,12 +500,6 @@ def dihedral(mols, atoms, phi0=None, kphi=None, name=None, map=None): A container of Dihedral restraints, where the first restraint is the DihedralRestraint created. The Dihedral restraint created can be extracted with DihedralRestraints[0]. - - Examples - -------- - Create a set of Dihedral restraints for the ligand in the system - 'system', specifying all of the force constants and equilibrium - values: """ from .. import u from ..base import create_map @@ -449,6 +525,11 @@ def dihedral(mols, atoms, phi0=None, kphi=None, name=None, map=None): if kphi is None: kphi = u("100 kcal mol-1 rad-2") + elif type(kphi) is list: + raise NotImplementedError( + "Setup of multiple dihedral restraints simultaneously is not currently supported. You can setup one restraint at a time however." + ) + # TODO: Add support for multiple dihedral restraints if phi0 is None: # calculate all of the current angles @@ -457,7 +538,9 @@ def dihedral(mols, atoms, phi0=None, kphi=None, name=None, map=None): phi0 = measure(atoms[0], atoms[1], atoms[2], atoms[3]) elif type(phi0) is list: - phi0 = [u(x) for x in phi0] + raise NotImplementedError( + "Setup of multiple dihedral restraints simultaneously is not currently supported. You can setup one restraint at a time however." + ) else: phi0 = u(phi0) @@ -468,9 +551,6 @@ def dihedral(mols, atoms, phi0=None, kphi=None, name=None, map=None): else: restraints = DihedralRestraints(name=name) - print(f"mols.find(atoms): {mols.find(atoms)}") - print(f"phi0: {phi0}") - print(f"kphi: {kphi}") restraints.add(DihedralRestraint(mols.find(atoms), phi0, kphi)) return restraints diff --git a/wrapper/Convert/SireOpenMM/lambdalever.cpp b/wrapper/Convert/SireOpenMM/lambdalever.cpp index e7d4e62fd..dde2e2eb0 100644 --- a/wrapper/Convert/SireOpenMM/lambdalever.cpp +++ b/wrapper/Convert/SireOpenMM/lambdalever.cpp @@ -1825,6 +1825,53 @@ void _update_restraint_in_context(OpenMM::CustomCompoundBondForce *ff, double rh ff->updateParametersInContext(context); } +/** Update the parameters for a CustomAngleForce for scale factor 'rho' + * in the passed context */ +void _update_restraint_in_context(OpenMM::CustomAngleForce *ff, double rho, + OpenMM::Context &context) +{ + if (ff == 0) + throw SireError::invalid_cast(QObject::tr( + "Unable to cast the restraint force to an OpenMM::CustomAngleForce, " + "despite it reporting that is was an object of this type..."), + CODELOC); + + const int nangles = ff->getNumAngles(); + + if (nangles == 0) + // nothing to update + return; + + const int nparams = ff->getNumPerAngleParameters(); + + if (nparams == 0) + throw SireError::incompatible_error(QObject::tr( + "Unable to set 'rho' for this restraint as it has no custom parameters!"), + CODELOC); + + // we set the first parameter - we can see what the current value + // is from the first restraint. This is because rho should be the + // first parameter and have the same value for all restraints + std::vector custom_params; + custom_params.resize(nparams); + int atom0, atom1, atom2; + + ff->getAngleParameters(0, atom0, atom1, atom2, custom_params); + + if (custom_params[0] == rho) + // nothing to do - it is already equal to this value + return; + + for (int i = 0; i < nangles; ++i) + { + ff->getAngleParameters(i, atom0, atom1, atom2, custom_params); + custom_params[0] = rho; + ff->setAngleParameters(i, atom0, atom1, atom2, custom_params); + } + + ff->updateParametersInContext(context); +} + /** Update the parameters for a CustomTorsionForce for scale factor 'rho' * in the passed context */ void _update_restraint_in_context(OpenMM::CustomTorsionForce *ff, double rho, @@ -1933,6 +1980,12 @@ void LambdaLever::updateRestraintInContext(OpenMM::Force &ff, double rho, dynamic_cast(&ff), rho, context); } + else if (ff_type == "CustomAngleForce") + { + _update_restraint_in_context( + dynamic_cast(&ff), + rho, context); + } else if (ff_type == "CustomTorsionForce") { _update_restraint_in_context( diff --git a/wrapper/Convert/SireOpenMM/sire_to_openmm_system.cpp b/wrapper/Convert/SireOpenMM/sire_to_openmm_system.cpp index d660d5dc7..551c1f0ba 100644 --- a/wrapper/Convert/SireOpenMM/sire_to_openmm_system.cpp +++ b/wrapper/Convert/SireOpenMM/sire_to_openmm_system.cpp @@ -21,10 +21,11 @@ #include "SireMol/moleditor.h" #include "SireMM/amberparams.h" +#include "SireMM/anglerestraints.h" #include "SireMM/atomljs.h" #include "SireMM/bondrestraints.h" #include "SireMM/boreschrestraints.h" -#include "SireMM/dihedralrestraint.h" +#include "SireMM/dihedralrestraints.h" #include "SireMM/positionalrestraints.h" #include "SireMM/selectorbond.h" @@ -394,10 +395,68 @@ void _add_positional_restraints(const SireMM::PositionalRestraints &restraints, } } -/** Add all of the dihedral restraints from 'restraints' to the passed +/** Add all of the angle restraints from 'restraints' to the passed * system, which is acted on by the passed LambdaLever. The number * of real (non-anchor) atoms in the OpenMM::System is 'natoms' */ + +void _add_angle_restraints(const SireMM::AngleRestraints &restraints, + OpenMM::System &system, LambdaLever &lambda_lever, + int natoms) +{ + if (restraints.isEmpty()) + return; + + // energy expression of the angle restraint, which acts over three atoms + // + // theta = angle(P1, P2, P3) + // + // The energies are + // + // e_restraint = rho * (e_angle) + // e_angle = ktheta(theta - theta0)^2 + + const auto energy_expression = QString( + "rho*k*(theta-theta0)^2;") + .toStdString(); + + auto *restraintff = new OpenMM::CustomAngleForce(energy_expression); + + restraintff->addPerAngleParameter("rho"); + restraintff->addPerAngleParameter("k"); + restraintff->addPerAngleParameter("theta0"); + + restraintff->setUsesPeriodicBoundaryConditions(true); + + lambda_lever.addRestraintIndex(restraints.name(), + system.addForce(restraintff)); + + const double internal_to_ktheta = (1 * SireUnits::kcal_per_mol / (SireUnits::radian2)).to(SireUnits::kJ_per_mol / SireUnits::radian2); + + const auto atom_restraints = restraints.restraints(); + + for (const auto &restraint : atom_restraints) + { + std::vector particles; + particles.resize(3); + + for (int i = 0; i < 3; ++i) + { + particles[i] = restraint.atoms()[i]; + } + + std::vector parameters; + parameters.resize(3); + + parameters[0] = 1.0; // rho + parameters[1] = restraint.ktheta().value() * internal_to_ktheta; // k + parameters[2] = restraint.theta0().value(); // theta0 (already in radians) + + // restraintff->addTorsion(particles, parameters); + restraintff->addAngle(particles[0], particles[1], particles[2], parameters); + } +} + void _add_dihedral_restraints(const SireMM::DihedralRestraints &restraints, OpenMM::System &system, LambdaLever &lambda_lever, int natoms) @@ -415,24 +474,16 @@ void _add_dihedral_restraints(const SireMM::DihedralRestraints &restraints, // e_torsion = k_phi(dphi)^2 where // dphi = abs(phi - phi0) - // TODO: Add support for multiple dihedral restraints - const auto energy_expression = QString( - "rho*k*min(dtheta, 2*pi-dtheta)^2;" + "rho*k*min(dtheta, two_pi-dtheta)^2;" "dtheta = abs(theta-theta0);" - "pi = 3.1415926535;") + "two_pi=6.283185307179586;") .toStdString(); auto *restraintff = new OpenMM::CustomTorsionForce(energy_expression); - // OLD CODE - // restraintff->addPerTorsionParameter("rho"); - // restraintff->addPerTorsionParameter("k"); - // restraintff->addPerTorsionParameter("phi0"); - // it seems that OpenMM wants to call the torsion angle theta rather than phi // we need to rename our parameters accordingly - // NEW CODE restraintff->addPerTorsionParameter("rho"); restraintff->addPerTorsionParameter("k"); restraintff->addPerTorsionParameter("theta0"); @@ -460,8 +511,8 @@ void _add_dihedral_restraints(const SireMM::DihedralRestraints &restraints, parameters.resize(3); parameters[0] = 1.0; // rho - parameters[1] = restraint.kphi().value() * internal_to_ktheta; // kphi - parameters[2] = restraint.phi0().value(); // phi0 (already in radians) + parameters[1] = restraint.kphi().value() * internal_to_ktheta; // k + parameters[2] = restraint.phi0().value(); // phi0 (already in radians) --> theta0 // restraintff->addTorsion(particles, parameters); restraintff->addTorsion(particles[0], particles[1], particles[2], particles[3], parameters); @@ -1705,6 +1756,11 @@ OpenMMMetaData SireOpenMM::sire_to_openmm_system(OpenMM::System &system, _add_dihedral_restraints(prop.read().asA(), system, lambda_lever, start_index); } + else if (prop.read().isA()) + { + _add_angle_restraints(prop.read().asA(), + system, lambda_lever, start_index); + } else if (prop.read().isA()) { _add_positional_restraints(prop.read().asA(), diff --git a/wrapper/MM/AngleRestraint.pypp.cpp b/wrapper/MM/AngleRestraint.pypp.cpp index 33954da56..b90320621 100644 --- a/wrapper/MM/AngleRestraint.pypp.cpp +++ b/wrapper/MM/AngleRestraint.pypp.cpp @@ -7,33 +7,21 @@ namespace bp = boost::python; -#include "SireCAS/conditional.h" - #include "SireCAS/errors.h" -#include "SireCAS/power.h" - -#include "SireCAS/symbols.h" - -#include "SireCAS/values.h" - -#include "SireFF/forcetable.h" - #include "SireID/index.h" #include "SireStream/datastream.h" #include "SireStream/shareddatastream.h" -#include "SireUnits/angle.h" - #include "SireUnits/units.h" -#include "anglerestraint.h" +#include "anglerestraints.h" #include -#include "anglerestraint.h" +#include "anglerestraints.h" SireMM::AngleRestraint __copy__(const SireMM::AngleRestraint &other){ return SireMM::AngleRestraint(other); } diff --git a/wrapper/MM/AngleRestraints.pypp.cpp b/wrapper/MM/AngleRestraints.pypp.cpp index fdf557c21..07edf38ca 100644 --- a/wrapper/MM/AngleRestraints.pypp.cpp +++ b/wrapper/MM/AngleRestraints.pypp.cpp @@ -8,33 +8,21 @@ namespace bp = boost::python; -#include "SireCAS/conditional.h" - #include "SireCAS/errors.h" -#include "SireCAS/power.h" - -#include "SireCAS/symbols.h" - -#include "SireCAS/values.h" - -#include "SireFF/forcetable.h" - #include "SireID/index.h" #include "SireStream/datastream.h" #include "SireStream/shareddatastream.h" -#include "SireUnits/angle.h" - #include "SireUnits/units.h" -#include "anglerestraint.h" +#include "anglerestraints.h" #include -#include "anglerestraint.h" +#include "anglerestraints.h" SireMM::AngleRestraints __copy__(const SireMM::AngleRestraints &other){ return SireMM::AngleRestraints(other); } diff --git a/wrapper/MM/CMakeAutogenFile.txt b/wrapper/MM/CMakeAutogenFile.txt index 3eeb5da11..3c3a4985a 100644 --- a/wrapper/MM/CMakeAutogenFile.txt +++ b/wrapper/MM/CMakeAutogenFile.txt @@ -68,6 +68,7 @@ set ( PYPP_SOURCES PositionalRestraints.pypp.cpp IntraGroupFF.pypp.cpp DihedralRestraint.pypp.cpp + DihedralRestraints.pypp.cpp IntraFF.pypp.cpp DihedralComponent.pypp.cpp ScaledChargeParameterNames3D.pypp.cpp @@ -181,6 +182,7 @@ set ( PYPP_SOURCES CoulombNBPairs.pypp.cpp AmberDihedral.pypp.cpp AngleRestraint.pypp.cpp + AngleRestraints.pypp.cpp TwoAtomFunctions.pypp.cpp IntraSoftCLJFFBase.pypp.cpp BondSymbols.pypp.cpp diff --git a/wrapper/MM/DihedralRestraint.pypp.cpp b/wrapper/MM/DihedralRestraint.pypp.cpp index accddf41c..a5d04277e 100644 --- a/wrapper/MM/DihedralRestraint.pypp.cpp +++ b/wrapper/MM/DihedralRestraint.pypp.cpp @@ -7,33 +7,21 @@ namespace bp = boost::python; -#include "SireCAS/conditional.h" - #include "SireCAS/errors.h" -#include "SireCAS/power.h" - -#include "SireCAS/symbols.h" - -#include "SireCAS/values.h" - -#include "SireFF/forcetable.h" - #include "SireID/index.h" #include "SireStream/datastream.h" #include "SireStream/shareddatastream.h" -#include "SireUnits/angle.h" - #include "SireUnits/units.h" -#include "dihedralrestraint.h" +#include "dihedralrestraints.h" #include -#include "dihedralrestraint.h" +#include "dihedralrestraints.h" SireMM::DihedralRestraint __copy__(const SireMM::DihedralRestraint &other){ return SireMM::DihedralRestraint(other); } diff --git a/wrapper/MM/DihedralRestraints.pypp.cpp b/wrapper/MM/DihedralRestraints.pypp.cpp index 21f56e7e0..44d49c86a 100644 --- a/wrapper/MM/DihedralRestraints.pypp.cpp +++ b/wrapper/MM/DihedralRestraints.pypp.cpp @@ -8,33 +8,21 @@ namespace bp = boost::python; -#include "SireCAS/conditional.h" - #include "SireCAS/errors.h" -#include "SireCAS/power.h" - -#include "SireCAS/symbols.h" - -#include "SireCAS/values.h" - -#include "SireFF/forcetable.h" - #include "SireID/index.h" #include "SireStream/datastream.h" #include "SireStream/shareddatastream.h" -#include "SireUnits/angle.h" - #include "SireUnits/units.h" -#include "dihedralrestraint.h" +#include "dihedralrestraints.h" #include -#include "dihedralrestraint.h" +#include "dihedralrestraints.h" SireMM::DihedralRestraints __copy__(const SireMM::DihedralRestraints &other){ return SireMM::DihedralRestraints(other); } diff --git a/wrapper/MM/SireMM_registrars.cpp b/wrapper/MM/SireMM_registrars.cpp index b7c70f668..9dabd1bd9 100644 --- a/wrapper/MM/SireMM_registrars.cpp +++ b/wrapper/MM/SireMM_registrars.cpp @@ -16,7 +16,7 @@ #include "selectordihedral.h" #include "clj14group.h" #include "improper.h" -#include "anglerestraint.h" +#include "anglerestraints.h" #include "bondrestraints.h" #include "twoatomfunctions.h" #include "boreschrestraints.h" @@ -54,7 +54,7 @@ #include "cljboxes.h" #include "internalgroupff.h" #include "internalcomponent.h" -#include "dihedralrestraint.h" +#include "dihedralrestraints.h" #include "selectorimproper.h" #include "intergroupff.h" #include "dihedral.h" @@ -112,6 +112,7 @@ void register_SireMM_objects() ObjectRegistry::registerConverterFor< SireMM::Improper >(); ObjectRegistry::registerConverterFor< SireMol::Mover >(); ObjectRegistry::registerConverterFor< SireMM::AngleRestraint >(); + ObjectRegistry::registerConverterFor< SireMM::AngleRestraints >(); ObjectRegistry::registerConverterFor< SireMM::BondRestraint >(); ObjectRegistry::registerConverterFor< SireMM::BondRestraints >(); ObjectRegistry::registerConverterFor< SireMM::TwoAtomFunctions >(); @@ -200,6 +201,7 @@ void register_SireMM_objects() ObjectRegistry::registerConverterFor< SireMM::Intra14Component >(); ObjectRegistry::registerConverterFor< SireMM::InternalComponent >(); ObjectRegistry::registerConverterFor< SireMM::DihedralRestraint >(); + ObjectRegistry::registerConverterFor< SireMM::DihedralRestraints >(); ObjectRegistry::registerConverterFor< SireMM::SelectorImproper >(); ObjectRegistry::registerConverterFor< SireMol::Mover >(); ObjectRegistry::registerConverterFor< SireMM::InterGroupFF >(); diff --git a/wrapper/MM/_MM.main.cpp b/wrapper/MM/_MM.main.cpp index b8559a1bd..addf5ef01 100644 --- a/wrapper/MM/_MM.main.cpp +++ b/wrapper/MM/_MM.main.cpp @@ -550,6 +550,10 @@ BOOST_PYTHON_MODULE(_MM){ register_AngleParameterName_class(); + register_Restraint_class(); + + register_Restraint3D_class(); + register_AngleRestraint_class(); register_Restraints_class(); @@ -648,14 +652,6 @@ BOOST_PYTHON_MODULE(_MM){ register_CLJParameterNames3D_class(); - register_CLJPotentialInterface_InterCLJPotential__class(); - - register_CLJPotentialInterface_InterSoftCLJPotential__class(); - - register_CLJPotentialInterface_IntraCLJPotential__class(); - - register_CLJPotentialInterface_IntraSoftCLJPotential__class(); - register_CLJProbe_class(); register_CLJRFFunction_class(); @@ -682,10 +678,6 @@ BOOST_PYTHON_MODULE(_MM){ register_CoulombNBPairs_class(); - register_CoulombPotentialInterface_InterCoulombPotential__class(); - - register_CoulombPotentialInterface_IntraCoulombPotential__class(); - register_CoulombProbe_class(); register_Dihedral_class(); @@ -700,10 +692,6 @@ BOOST_PYTHON_MODULE(_MM){ register_DihedralSymbols_class(); - register_Restraint_class(); - - register_Restraint3D_class(); - register_DistanceRestraint_class(); register_DoubleDistanceRestraint_class(); @@ -796,10 +784,6 @@ BOOST_PYTHON_MODULE(_MM){ register_LJPerturbation_class(); - register_LJPotentialInterface_InterLJPotential__class(); - - register_LJPotentialInterface_IntraLJPotential__class(); - register_LJProbe_class(); register_MMDetail_class(); @@ -844,10 +828,6 @@ BOOST_PYTHON_MODULE(_MM){ register_SoftCLJComponent_class(); - register_SoftCLJPotentialInterface_InterSoftCLJPotential__class(); - - register_SoftCLJPotentialInterface_IntraSoftCLJPotential__class(); - register_StretchBendComponent_class(); register_StretchBendSymbols_class(); diff --git a/wrapper/MM/active_headers.h b/wrapper/MM/active_headers.h index 3cb17a25a..c50bbe8c8 100644 --- a/wrapper/MM/active_headers.h +++ b/wrapper/MM/active_headers.h @@ -5,7 +5,7 @@ #include "amberparams.h" #include "angle.h" -#include "anglerestraint.h" +#include "anglerestraints.h" #include "atomfunctions.h" #include "atomljs.h" #include "bond.h" @@ -30,7 +30,7 @@ #include "cljworkspace.h" #include "coulombpotential.h" #include "dihedral.h" -#include "dihedralrestraint.h" +#include "dihedralrestraints.h" #include "distancerestraint.h" #include "excludedpairs.h" #include "fouratomfunctions.h" From a500bc9eb5a04447c7e631685a6c26a1e70a2d98 Mon Sep 17 00:00:00 2001 From: Audrius Kalpokas Date: Fri, 19 Jul 2024 18:13:32 +0100 Subject: [PATCH 009/102] Add the missing corelib files for angle restraints --- corelib/src/libs/SireMM/anglerestraints.cpp | 496 ++++++++++++++++++++ corelib/src/libs/SireMM/anglerestraints.h | 177 +++++++ 2 files changed, 673 insertions(+) create mode 100644 corelib/src/libs/SireMM/anglerestraints.cpp create mode 100644 corelib/src/libs/SireMM/anglerestraints.h diff --git a/corelib/src/libs/SireMM/anglerestraints.cpp b/corelib/src/libs/SireMM/anglerestraints.cpp new file mode 100644 index 000000000..42cec09df --- /dev/null +++ b/corelib/src/libs/SireMM/anglerestraints.cpp @@ -0,0 +1,496 @@ +/********************************************\ + * + * Sire - Molecular Simulation Framework + * + * Copyright (C) 2009 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 at https://sire.openbiosim.org + * +\*********************************************/ + +#include "anglerestraints.h" + +#include "SireID/index.h" + +#include "SireUnits/units.h" + +#include "SireStream/datastream.h" +#include "SireStream/shareddatastream.h" + +#include "SireCAS/errors.h" + +#include + +using namespace SireMM; +using namespace SireID; +using namespace SireBase; +using namespace SireMaths; +using namespace SireStream; +using namespace SireUnits; +using namespace SireUnits::Dimension; + +//////////// +//////////// Implementation of AngleRestraint +//////////// + +static const RegisterMetaType r_angrest; + +/** Serialise to a binary datastream */ + +QDataStream &operator<<(QDataStream &ds, const AngleRestraint &angrest) +{ + writeHeader(ds, r_angrest, 1); + + SharedDataStream sds(ds); + + sds << angrest.atms << angrest._theta0 << angrest._ktheta; + + return ds; +} + +/** Extract from a binary datastream */ +QDataStream &operator>>(QDataStream &ds, AngleRestraint &angrest) +{ + VersionID v = readHeader(ds, r_angrest); + + if (v == 1) + { + SharedDataStream sds(ds); + + sds >> angrest.atms >> angrest._theta0 >> angrest._ktheta; + } + else + throw version_error(v, "1", r_angrest, CODELOC); + + return ds; +} + +/** Null constructor */ +AngleRestraint::AngleRestraint() + : ConcreteProperty(), + _ktheta(0), _theta0(0) +{ +} + +/** Construct a restraint that acts on the angle within the + three atoms 'atom0', 'atom1' and 'atom2' (theta == a(012)), + restraining the angle within these atoms */ +AngleRestraint::AngleRestraint(const QList &atoms, + const SireUnits::Dimension::Angle &theta0, + const SireUnits::Dimension::HarmonicAngleConstant &ktheta) + : ConcreteProperty(), + _theta0(theta0), _ktheta(ktheta) +{ + // Need to think here about validating the angle and force constant values + // if (atoms.count() != 3) + // { + // throw SireError::invalid_arg(QObject::tr( + // "Wrong number of inputs for an Angle restraint. You need to " + // "provide 3 atoms (%1).") + // .arg(atoms.count()), + // // .arg(theta0.count()) + // // .arg(ktheta.count()), + // CODELOC); + // } + + // make sure that we have 3 distinct atoms + QSet distinct; + distinct.reserve(3); + + for (const auto &atom : atoms) + { + if (atom >= 0) + distinct.insert(atom); + } + + // if (distinct.count() != 3) + // throw SireError::invalid_arg(QObject::tr( + // "There is something wrong with the atoms provided. " + // "They should all be unique and all greater than or equal to 0."), + // CODELOC); + + atms = atoms.toVector(); +} + +/* Copy constructor*/ +AngleRestraint::AngleRestraint(const AngleRestraint &other) + : ConcreteProperty(other), + atms(other.atms), _theta0(other._theta0), _ktheta(other._ktheta) + +{ +} + +/* Destructor */ +AngleRestraint::~AngleRestraint() +{ +} + +AngleRestraint &AngleRestraint::operator=(const AngleRestraint &other) +{ + if (this != &other) + { + Property::operator=(other); + atms = other.atms; + _theta0 = other._theta0; + _ktheta = other._ktheta; + } + + return *this; +} + +bool AngleRestraint::operator==(const AngleRestraint &other) const +{ + return atms == other.atms and + _theta0 == other._theta0 and + _ktheta == other._ktheta; +} + +bool AngleRestraint::operator!=(const AngleRestraint &other) const +{ + return not operator==(other); +} + +AngleRestraints AngleRestraint::operator+(const AngleRestraint &other) const +{ + return AngleRestraints(*this) + other; +} + +AngleRestraints AngleRestraint::operator+(const AngleRestraints &other) const +{ + return AngleRestraints(*this) + other; +} + +const char *AngleRestraint::typeName() +{ + return QMetaType::typeName(qMetaTypeId()); +} + +const char *AngleRestraint::what() const +{ + return AngleRestraint::typeName(); +} + +AngleRestraint *AngleRestraint::clone() const +{ + return new AngleRestraint(*this); +} + +bool AngleRestraint::isNull() const +{ + return atms.isEmpty(); +} + +QString AngleRestraint::toString() const +{ + if (this->isNull()) + return QObject::tr("AngleRestraint::null"); + else + { + QStringList a; + + for (const auto &atom : atms) + { + a.append(QString::number(atom)); + } + return QString("AngleRestraint( [%1], theta0=%2, ktheta=%3 )") + .arg(a.join(", ")) + .arg(_theta0.toString()) + .arg(_ktheta.toString()); + } +} + +/** Return the force constant for the restraint */ +SireUnits::Dimension::HarmonicAngleConstant AngleRestraint::ktheta() const +{ + return this->_ktheta; +} + +/** Return the equilibrium angle for the restraint */ +SireUnits::Dimension::Angle AngleRestraint::theta0() const +{ + return this->_theta0; +} + +/** Return the atoms involved in the restraint */ +QVector AngleRestraint::atoms() const +{ + return this->atms; +} + +/////// +/////// Implementation of AngleRestraints +/////// + +/** Serialise to a binary datastream */ + +static const RegisterMetaType r_angrests; + +QDataStream &operator<<(QDataStream &ds, const AngleRestraints &angrests) +{ + writeHeader(ds, r_angrests, 1); + + SharedDataStream sds(ds); + + sds << angrests.r + << static_cast(angrests); + + return ds; +} + +/** Extract from a binary datastream */ +QDataStream &operator>>(QDataStream &ds, AngleRestraints &angrests) +{ + VersionID v = readHeader(ds, r_angrests); + + if (v == 1) + { + SharedDataStream sds(ds); + + sds >> angrests.r >> + static_cast(angrests); + } + else + throw version_error(v, "1", r_angrests, CODELOC); + + return ds; +} + +/** Null constructor */ +AngleRestraints::AngleRestraints() + : ConcreteProperty() +{ +} + +AngleRestraints::AngleRestraints(const QString &name) + : ConcreteProperty(name) +{ +} + +AngleRestraints::AngleRestraints(const AngleRestraint &restraint) + : ConcreteProperty() +{ + if (not restraint.isNull()) + r.append(restraint); +} + +AngleRestraints::AngleRestraints(const QList &restraints) + : ConcreteProperty() +{ + for (const auto &restraint : restraints) + { + if (not restraint.isNull()) + r.append(restraint); + } +} + +AngleRestraints::AngleRestraints(const QString &name, + const AngleRestraint &restraint) + : ConcreteProperty(name) +{ + if (not restraint.isNull()) + r.append(restraint); +} + +AngleRestraints::AngleRestraints(const QString &name, + const QList &restraints) + : ConcreteProperty(name) +{ + for (const auto &restraint : restraints) + { + if (not restraint.isNull()) + r.append(restraint); + } +} + +AngleRestraints::AngleRestraints(const AngleRestraints &other) + : ConcreteProperty(other), r(other.r) +{ +} + +/* Desctructor */ +AngleRestraints::~AngleRestraints() +{ +} + +AngleRestraints &AngleRestraints::operator=(const AngleRestraints &other) +{ + if (this != &other) + { + Restraints::operator=(other); + r = other.r; + } + + return *this; +} + +bool AngleRestraints::operator==(const AngleRestraints &other) const +{ + return r == other.r and Restraints::operator==(other); +} + +bool AngleRestraints::operator!=(const AngleRestraints &other) const +{ + return not operator==(other); +} + +const char *AngleRestraints::typeName() +{ + return QMetaType::typeName(qMetaTypeId()); +} + +const char *AngleRestraints::what() const +{ + return AngleRestraints::typeName(); +} + +AngleRestraints *AngleRestraints::clone() const +{ + return new AngleRestraints(*this); +} + +QString AngleRestraints::toString() const +{ + if (this->isEmpty()) + return QObject::tr("AngleRestraints::null"); + + QStringList parts; + + const auto n = this->count(); + + if (n <= 10) + { + for (int i = 0; i < n; i++) + { + parts.append(QObject::tr("%1: %2").arg(i).arg(this->r.at(i).toString())); + } + } + else + { + for (int i = 0; i < 5; i++) + { + parts.append(QObject::tr("%1: %2").arg(i).arg(this->r.at(i).toString())); + } + + parts.append("..."); + + for (int i = n - 5; i < n; i++) + { + parts.append(QObject::tr("%1: %2").arg(i).arg(this->r.at(i).toString())); + } + } + + return QObject::tr("AngleRestraints( name=%1, size=%2\n%3\n )") + .arg(this->name()) + .arg(n) + .arg(parts.join("\n")); +} + +/** Return whether or not this is empty */ +bool AngleRestraints::isEmpty() const +{ + return this->r.isEmpty(); +} + +/** Return whether or not this is empty */ +bool AngleRestraints::isNull() const +{ + return this->isEmpty(); +} + +/** Return the number of restraints */ +int AngleRestraints::nRestraints() const +{ + return this->r.count(); +} + +/** Return the number of restraints */ +int AngleRestraints::count() const +{ + return this->nRestraints(); +} + +/** Return the number of restraints */ +int AngleRestraints::size() const +{ + return this->nRestraints(); +} + +/** Return the ith restraint */ +const AngleRestraint &AngleRestraints::at(int i) const +{ + i = SireID::Index(i).map(this->r.count()); + + return this->r.at(i); +} + +/** Return the ith restraint */ +const AngleRestraint &AngleRestraints::operator[](int i) const +{ + return this->at(i); +} + +/** Return all of the restraints */ +QList AngleRestraints::restraints() const +{ + return this->r; +} + +/** Add a restraints onto the list */ +void AngleRestraints::add(const AngleRestraint &restraint) +{ + if (not restraint.isNull()) + r.append(restraint); +} + +/** Add a restraint onto the list */ +void AngleRestraints::add(const AngleRestraints &restraints) +{ + this->r += restraints.r; +} + +/** Add a restraint onto the list */ +AngleRestraints &AngleRestraints::operator+=(const AngleRestraint &restraint) +{ + this->add(restraint); + return *this; +} + +/** Add a restraint onto the list */ +AngleRestraints AngleRestraints::operator+(const AngleRestraint &restraint) const +{ + AngleRestraints ret(*this); + ret += restraint; + return *this; +} + +/** Add restraints onto the list */ +AngleRestraints &AngleRestraints::operator+=(const AngleRestraints &restraints) +{ + this->add(restraints); + return *this; +} + +/** Add restraints onto the list */ +AngleRestraints AngleRestraints::operator+(const AngleRestraints &restraints) const +{ + AngleRestraints ret(*this); + ret += restraints; + return *this; +} diff --git a/corelib/src/libs/SireMM/anglerestraints.h b/corelib/src/libs/SireMM/anglerestraints.h new file mode 100644 index 000000000..393d30849 --- /dev/null +++ b/corelib/src/libs/SireMM/anglerestraints.h @@ -0,0 +1,177 @@ +/********************************************\ + * + * Sire - Molecular Simulation Framework + * + * Copyright (C) 2009 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 at https://sire.openbiosim.org + * +\*********************************************/ + +#ifndef SIREMM_ANGLERESTRAINTS_H +#define SIREMM_ANGLERESTRAINTS_H + + +#include "restraints.h" +#include "SireUnits/dimensions.h" +#include "SireUnits/generalunit.h" + +SIRE_BEGIN_HEADER + +namespace SireMM +{ + class AngleRestraint; + class AngleRestraints; +} + +SIREMM_EXPORT QDataStream &operator<<(QDataStream &, const SireMM::AngleRestraint &); +SIREMM_EXPORT QDataStream &operator>>(QDataStream &, SireMM::AngleRestraint &); + +SIREMM_EXPORT QDataStream &operator<<(QDataStream &, const SireMM::AngleRestraints &); +SIREMM_EXPORT QDataStream &operator>>(QDataStream &, SireMM::AngleRestraints &); + +namespace SireMM +{ + + /** This class represents a single angle restraint between any three + * atoms in a system + * @author Christopher Woods + */ + class SIREMM_EXPORT AngleRestraint + : public SireBase::ConcreteProperty + { + + friend SIREMM_EXPORT QDataStream & ::operator<<(QDataStream &, const SireMM::AngleRestraint &); + friend SIREMM_EXPORT QDataStream & ::operator>>(QDataStream &, SireMM::AngleRestraint &); + + public: + AngleRestraint(); + AngleRestraint(const QList &atoms, + const SireUnits::Dimension::Angle &theta0, + const SireUnits::Dimension::HarmonicAngleConstant &ktheta); + + AngleRestraint(const AngleRestraint &other); + + ~AngleRestraint(); + + AngleRestraint &operator=(const AngleRestraint &other); + + bool operator==(const AngleRestraint &other) const; + bool operator!=(const AngleRestraint &other) const; + + AngleRestraints operator+(const AngleRestraint &other) const; + AngleRestraints operator+(const AngleRestraints &other) const; + + static const char *typeName(); + const char *what() const; + + AngleRestraint *clone() const; + + QString toString() const; + + bool isNull() const; + + QVector atoms() const; + + SireUnits::Dimension::Angle theta0() const; + SireUnits::Dimension::HarmonicAngleConstant ktheta() const; + + private: + /** Atoms involved in the angle restraint */ + QVector atms; + + /** Equilibrium angle */ + SireUnits::Dimension::Angle _theta0; + + /** Harmonic angle constant */ + SireUnits::Dimension::HarmonicAngleConstant _ktheta; + }; + + /** This class represents a collection of angle restraints */ + class SIREMM_EXPORT AngleRestraints + : public SireBase::ConcreteProperty + { + friend SIREMM_EXPORT QDataStream & ::operator<<(QDataStream &, const SireMM::AngleRestraints &); + friend SIREMM_EXPORT QDataStream & ::operator>>(QDataStream &, SireMM::AngleRestraints &); + + public: + AngleRestraints(); + AngleRestraints(const QString &name); + + AngleRestraints(const AngleRestraint &restraint); + AngleRestraints(const QList &restraints); + + AngleRestraints(const QString &name, + const AngleRestraint &restraint); + + AngleRestraints(const QString &name, + const QList &restraints); + + AngleRestraints(const AngleRestraints &other); + + ~AngleRestraints(); + + AngleRestraints &operator=(const AngleRestraints &other); + + bool operator==(const AngleRestraints &other) const; + bool operator!=(const AngleRestraints &other) const; + + static const char *typeName(); + const char *what() const; + + AngleRestraints *clone() const; + + QString toString() const; + + bool isEmpty() const; + bool isNull() const; + + int count() const; + int size() const; + int nRestraints() const; + + const AngleRestraint &at(int i) const; + const AngleRestraint &operator[](int i) const; + + QList restraints() const; + + void add(const AngleRestraint &restraint); + void add(const AngleRestraints &restraints); + + AngleRestraints &operator+=(const AngleRestraint &restraint); + AngleRestraints &operator+=(const AngleRestraints &restraints); + + AngleRestraints operator+(const AngleRestraint &restraint) const; + AngleRestraints operator+(const AngleRestraints &restraints) const; + + private: + /** List of restraints */ + QList r; + }; +} + +Q_DECLARE_METATYPE(SireMM::AngleRestraint) +Q_DECLARE_METATYPE(SireMM::AngleRestraints) + +SIRE_EXPOSE_CLASS(SireMM::AngleRestraint) +SIRE_EXPOSE_CLASS(SireMM::AngleRestraints) +SIRE_END_HEADER + +#endif From ff207f58ce4fa26777443395f1bc43a7aaeb573d Mon Sep 17 00:00:00 2001 From: Lester Hedges Date: Wed, 23 Oct 2024 11:26:13 +0100 Subject: [PATCH 010/102] Update to the 2024.4.0.dev release. [ci skip] --- version.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/version.txt b/version.txt index d1736a63b..75d5f7f71 100644 --- a/version.txt +++ b/version.txt @@ -1 +1 @@ -2024.3.0 +2024.4.0.dev From bee9b066d4a735f87fcfe6284ba05950cba926ff Mon Sep 17 00:00:00 2001 From: Lester Hedges Date: Wed, 23 Oct 2024 13:38:18 +0100 Subject: [PATCH 011/102] Fix note formatting. [ci skip] --- doc/source/tutorial/part08/03_adp_pmf.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/source/tutorial/part08/03_adp_pmf.rst b/doc/source/tutorial/part08/03_adp_pmf.rst index d016a1735..27b2b9a55 100644 --- a/doc/source/tutorial/part08/03_adp_pmf.rst +++ b/doc/source/tutorial/part08/03_adp_pmf.rst @@ -2,7 +2,7 @@ Alanine-dipeptide conformational landscape ========================================== -..note:: +.. note:: The code in this tutorial was adapted from `FastMBAR `_. @@ -158,7 +158,7 @@ of 100 cycles of 100 steps each, saving trajectory after each cycle: ... dcd_file.writeModel(positions) ... file_handle.close() -..note:: +.. 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 From 4fcc147db1afa4ff5c8e868484eaa18eda3d4ff2 Mon Sep 17 00:00:00 2001 From: Lester Hedges Date: Wed, 23 Oct 2024 15:42:29 +0100 Subject: [PATCH 012/102] Fix tutorial section formatting. [ci skip] --- doc/source/tutorial/index_part08.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/doc/source/tutorial/index_part08.rst b/doc/source/tutorial/index_part08.rst index d2629dc51..7517a720a 100644 --- a/doc/source/tutorial/index_part08.rst +++ b/doc/source/tutorial/index_part08.rst @@ -1,6 +1,6 @@ -=============== -Part 08 - QM/MM -=============== +============== +Part 8 - 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 From 926f0821f5d6891276dd32237ca2e04c299f75f5 Mon Sep 17 00:00:00 2001 From: Lester Hedges Date: Mon, 28 Oct 2024 15:14:30 +0000 Subject: [PATCH 013/102] Fix instantiation of QByteArray and counting of bytes. [closes #249] --- corelib/src/libs/SireMol/trajectory.cpp | 46 ++++++++++++------------- doc/source/changelog.rst | 6 ++++ 2 files changed, 29 insertions(+), 23 deletions(-) diff --git a/corelib/src/libs/SireMol/trajectory.cpp b/corelib/src/libs/SireMol/trajectory.cpp index 4e4414d73..64ab37e37 100644 --- a/corelib/src/libs/SireMol/trajectory.cpp +++ b/corelib/src/libs/SireMol/trajectory.cpp @@ -1380,9 +1380,9 @@ QByteArray Frame::toByteArray() const ds << spc << t.to(picosecond) << props; nbytes += sizeof(quint32); - nbytes += extra.count(); + nbytes += extra.size(); - QByteArray data("\0", nbytes); + QByteArray data(nbytes, '\0'); auto data_ptr = data.data(); @@ -1425,7 +1425,7 @@ QByteArray Frame::toByteArray() const data_ptr += val * sizeof(Force3D); } - val = extra.count(); + val = extra.size(); std::memcpy(data_ptr, &val, sizeof(quint32)); data_ptr += sizeof(quint32); @@ -1435,12 +1435,12 @@ QByteArray Frame::toByteArray() const data_ptr += val; } - if (data_ptr - data.constData() != data.count()) + if (data_ptr - data.constData() != data.size()) { throw SireError::program_bug(QObject::tr( "Memory corruption? %1 versus %2") .arg(data_ptr - data.constData()) - .arg(data.count()), + .arg(data.size()), CODELOC); } @@ -1449,9 +1449,9 @@ QByteArray Frame::toByteArray() const Frame Frame::fromByteArray(const QByteArray &data) { - if (data.count() < 4) + if (data.size() < 4) { - throw SireError::incompatible_error(QObject::tr("The data is too short to be a frame! %1").arg(data.count()), + throw SireError::incompatible_error(QObject::tr("The data is too short to be a frame! %1").arg(data.size()), CODELOC); } @@ -1474,9 +1474,9 @@ Frame Frame::fromByteArray(const QByteArray &data) throw SireStream::version_error(val, "1", r_frame, CODELOC); } - if (data_ptr + sizeof(quint32) > data.constData() + data.count()) + if (data_ptr + sizeof(quint32) > data.constData() + data.size()) { - throw SireError::incompatible_error(QObject::tr("The data is too short to be a frame! %1").arg(data.count()), + throw SireError::incompatible_error(QObject::tr("The data is too short to be a frame! %1").arg(data.size()), CODELOC); } @@ -1487,9 +1487,9 @@ Frame Frame::fromByteArray(const QByteArray &data) if (val != 0) { - if (data_ptr + val * sizeof(Vector) > data.constData() + data.count()) + if (data_ptr + val * sizeof(Vector) > data.constData() + data.size()) { - throw SireError::incompatible_error(QObject::tr("The data is too short to be a frame! %1").arg(data.count()), + throw SireError::incompatible_error(QObject::tr("The data is too short to be a frame! %1").arg(data.size()), CODELOC); } @@ -1498,9 +1498,9 @@ Frame Frame::fromByteArray(const QByteArray &data) data_ptr += val * sizeof(Vector); } - if (data_ptr + sizeof(quint32) > data.constData() + data.count()) + if (data_ptr + sizeof(quint32) > data.constData() + data.size()) { - throw SireError::incompatible_error(QObject::tr("The data is too short to be a frame! %1").arg(data.count()), + throw SireError::incompatible_error(QObject::tr("The data is too short to be a frame! %1").arg(data.size()), CODELOC); } @@ -1511,9 +1511,9 @@ Frame Frame::fromByteArray(const QByteArray &data) if (val != 0) { - if (data_ptr + val * sizeof(Velocity3D) > data.constData() + data.count()) + if (data_ptr + val * sizeof(Velocity3D) > data.constData() + data.size()) { - throw SireError::incompatible_error(QObject::tr("The data is too short to be a frame! %1").arg(data.count()), + throw SireError::incompatible_error(QObject::tr("The data is too short to be a frame! %1").arg(data.size()), CODELOC); } @@ -1522,9 +1522,9 @@ Frame Frame::fromByteArray(const QByteArray &data) data_ptr += val * sizeof(Velocity3D); } - if (data_ptr + sizeof(quint32) > data.constData() + data.count()) + if (data_ptr + sizeof(quint32) > data.constData() + data.size()) { - throw SireError::incompatible_error(QObject::tr("The data is too short to be a frame! %1").arg(data.count()), + throw SireError::incompatible_error(QObject::tr("The data is too short to be a frame! %1").arg(data.size()), CODELOC); } @@ -1535,9 +1535,9 @@ Frame Frame::fromByteArray(const QByteArray &data) if (val != 0) { - if (data_ptr + val * sizeof(Force3D) > data.constData() + data.count()) + if (data_ptr + val * sizeof(Force3D) > data.constData() + data.size()) { - throw SireError::incompatible_error(QObject::tr("The data is too short to be a frame! %1").arg(data.count()), + throw SireError::incompatible_error(QObject::tr("The data is too short to be a frame! %1").arg(data.size()), CODELOC); } @@ -1546,9 +1546,9 @@ Frame Frame::fromByteArray(const QByteArray &data) data_ptr += val * sizeof(Force3D); } - if (data_ptr + sizeof(quint32) > data.constData() + data.count()) + if (data_ptr + sizeof(quint32) > data.constData() + data.size()) { - throw SireError::incompatible_error(QObject::tr("The data is too short to be a frame! %1").arg(data.count()), + throw SireError::incompatible_error(QObject::tr("The data is too short to be a frame! %1").arg(data.size()), CODELOC); } @@ -1561,9 +1561,9 @@ Frame Frame::fromByteArray(const QByteArray &data) if (val != 0) { - if (data_ptr + val > data.constData() + data.count()) + if (data_ptr + val > data.constData() + data.size()) { - throw SireError::incompatible_error(QObject::tr("The data is too short to be a frame! %1").arg(data.count()), + throw SireError::incompatible_error(QObject::tr("The data is too short to be a frame! %1").arg(data.size()), CODELOC); } diff --git a/doc/source/changelog.rst b/doc/source/changelog.rst index 8d01fad32..8594e71dc 100644 --- a/doc/source/changelog.rst +++ b/doc/source/changelog.rst @@ -12,6 +12,12 @@ Development was migrated into the `OpenBioSim `__ organisation on `GitHub `__. +`2024.4.0 `__ - December 2024 +--------------------------------------------------------------------------------------------- + +* Please add an item to this CHANGELOG for any new features or bug fixes when creating a PR. +* Fixed instantiaton of ``QByteArray`` in ``Sire::Mol::Frame::toByteArray`` and count bytes with ``QByteArray::size``. + `2024.3.0 `__ - October 2024 -------------------------------------------------------------------------------------------- From a2370a4986c3d242940da8953bbce47dd9eed650 Mon Sep 17 00:00:00 2001 From: Lester Hedges Date: Tue, 29 Oct 2024 08:30:58 +0000 Subject: [PATCH 014/102] Increase wait before terminating QThreads. [closes #251] [ci skip] --- corelib/src/libs/SireBase/pagecache.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/corelib/src/libs/SireBase/pagecache.cpp b/corelib/src/libs/SireBase/pagecache.cpp index d2d6f2ee9..d2a37646a 100644 --- a/corelib/src/libs/SireBase/pagecache.cpp +++ b/corelib/src/libs/SireBase/pagecache.cpp @@ -662,7 +662,7 @@ void CacheData::cleanUpOnExit() { if (QThread::currentThread() != c.get()) { - if (not c->wait(50)) + if (not c->wait(100)) { // kill it c->terminate(); From 808dad7871dd2be9959e76d621212e5444106d85 Mon Sep 17 00:00:00 2001 From: Lester Hedges Date: Tue, 29 Oct 2024 12:27:04 +0000 Subject: [PATCH 015/102] Update CHANGELOG. [ci skip] --- doc/source/changelog.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/doc/source/changelog.rst b/doc/source/changelog.rst index 8594e71dc..978923fd5 100644 --- a/doc/source/changelog.rst +++ b/doc/source/changelog.rst @@ -17,6 +17,7 @@ organisation on `GitHub `__. * Please add an item to this CHANGELOG for any new features or bug fixes when creating a PR. * Fixed instantiaton of ``QByteArray`` in ``Sire::Mol::Frame::toByteArray`` and count bytes with ``QByteArray::size``. +* Increase timeout before terminating ``QThread`` objects during ``PageCache`` cleanup. `2024.3.0 `__ - October 2024 -------------------------------------------------------------------------------------------- From 9d9f51bed3fbbc7bbcddad501bb2a28051084864 Mon Sep 17 00:00:00 2001 From: Lester Hedges Date: Tue, 29 Oct 2024 13:45:29 +0000 Subject: [PATCH 016/102] Give thread a chance to finish and inform user. [ci skip] --- corelib/src/libs/SireBase/pagecache.cpp | 23 +++++++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/corelib/src/libs/SireBase/pagecache.cpp b/corelib/src/libs/SireBase/pagecache.cpp index d2a37646a..1db2ddc2a 100644 --- a/corelib/src/libs/SireBase/pagecache.cpp +++ b/corelib/src/libs/SireBase/pagecache.cpp @@ -662,9 +662,28 @@ void CacheData::cleanUpOnExit() { if (QThread::currentThread() != c.get()) { - if (not c->wait(100)) + bool is_finished = false; + + // give the thread a maximum of 10 attempts to finish + for (int i = 0; i < 10; i++) + { + // the thread didn't finish + if (not c->wait(100)) + { + qDebug() << "Waiting for cache thread to finish:" << c->cacheDir(); + } + // the thread finished + else + { + is_finished = true; + break; + } + } + + // the thread still didn't finish, so kill it + if (not is_finished) { - // kill it + qDebug() << "Terminating cache thread:" << c->cacheDir(); c->terminate(); } } From a5d704fcf4b3de5240f05ac811e030faa66688a9 Mon Sep 17 00:00:00 2001 From: Lester Hedges Date: Tue, 29 Oct 2024 14:07:53 +0000 Subject: [PATCH 017/102] Expose missing timeout kwarg in dynamics.minimise(). [closes #253] --- doc/source/changelog.rst | 1 + src/sire/mol/_dynamics.py | 4 ++++ 2 files changed, 5 insertions(+) diff --git a/doc/source/changelog.rst b/doc/source/changelog.rst index 978923fd5..20d88daa9 100644 --- a/doc/source/changelog.rst +++ b/doc/source/changelog.rst @@ -18,6 +18,7 @@ organisation on `GitHub `__. * Please add an item to this CHANGELOG for any new features or bug fixes when creating a PR. * Fixed instantiaton of ``QByteArray`` in ``Sire::Mol::Frame::toByteArray`` and count bytes with ``QByteArray::size``. * Increase timeout before terminating ``QThread`` objects during ``PageCache`` cleanup. +* Expose missing ``timeout`` kwarg in :meth:`dynamics.minimise()` method. `2024.3.0 `__ - October 2024 -------------------------------------------------------------------------------------------- diff --git a/src/sire/mol/_dynamics.py b/src/sire/mol/_dynamics.py index ca23bceb0..deeaa1505 100644 --- a/src/sire/mol/_dynamics.py +++ b/src/sire/mol/_dynamics.py @@ -1257,6 +1257,7 @@ def minimise( 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. @@ -1290,6 +1291,8 @@ def minimise( - 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 constraints 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( @@ -1301,6 +1304,7 @@ def minimise( starting_k=starting_k, ratchet_scale=ratchet_scale, max_constraint_error=max_constraint_error, + timeout=timeout, ) return self From 32f27a43d1160525757c021fc297c78b2b9e765b Mon Sep 17 00:00:00 2001 From: Lester Hedges Date: Tue, 29 Oct 2024 16:09:52 +0000 Subject: [PATCH 018/102] Add include_constrained_energies kwarg to map. [ref #255] --- src/sire/mol/__init__.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/sire/mol/__init__.py b/src/sire/mol/__init__.py index b6adf10d8..ad8004fa9 100644 --- a/src/sire/mol/__init__.py +++ b/src/sire/mol/__init__.py @@ -2156,6 +2156,9 @@ def _minimisation( if platform is not None: map.set("platform", str(platform)) + if include_constrained_energies is not None: + map.set("include_constrained_energies", include_constrained_energies) + return Minimisation( view, cutoff=cutoff, From 53182c38b8f87180967356b02affdbd24ffdcd87 Mon Sep 17 00:00:00 2001 From: Lester Hedges Date: Tue, 29 Oct 2024 16:10:25 +0000 Subject: [PATCH 019/102] Add consistent minimisation settings. [closes #255] --- src/sire/mol/_dynamics.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/sire/mol/_dynamics.py b/src/sire/mol/_dynamics.py index deeaa1505..40c8b125a 100644 --- a/src/sire/mol/_dynamics.py +++ b/src/sire/mol/_dynamics.py @@ -700,8 +700,8 @@ def run_minimisation( max_restarts: int = 10, max_ratchets: int = 20, ratchet_frequency: int = 500, - starting_k: float = 100.0, - ratchet_scale: float = 2.0, + starting_k: float = 400.0, + ratchet_scale: float = 10.0, max_constraint_error: float = 0.001, timeout: str = "300s", ): @@ -1254,8 +1254,8 @@ def minimise( max_restarts: int = 10, max_ratchets: int = 20, ratchet_frequency: int = 500, - starting_k: float = 100.0, - ratchet_scale: float = 2.0, + starting_k: float = 400.0, + ratchet_scale: float = 10.0, max_constraint_error: float = 0.001, timeout: str = "300s", ): From b7cee4becda8a39cc8092f83ca23022820242c54 Mon Sep 17 00:00:00 2001 From: Lester Hedges Date: Tue, 29 Oct 2024 16:11:45 +0000 Subject: [PATCH 020/102] Update CHANGELOG. --- doc/source/changelog.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/doc/source/changelog.rst b/doc/source/changelog.rst index 20d88daa9..3dd88e2b1 100644 --- a/doc/source/changelog.rst +++ b/doc/source/changelog.rst @@ -19,6 +19,8 @@ organisation on `GitHub `__. * Fixed instantiaton of ``QByteArray`` in ``Sire::Mol::Frame::toByteArray`` and count bytes with ``QByteArray::size``. * Increase timeout before terminating ``QThread`` objects during ``PageCache`` cleanup. * Expose missing ``timeout`` kwarg in :meth:`dynamics.minimise()` method. +* Expose missing ``include_constrained_energies`` kwarg in minimisation function. +* Make minimisation function settings consistent across API. `2024.3.0 `__ - October 2024 -------------------------------------------------------------------------------------------- From be8aa3a5c0bcb7b3fb2598ccc2048e305937942e Mon Sep 17 00:00:00 2001 From: finlayclark Date: Fri, 1 Nov 2024 13:34:23 +0000 Subject: [PATCH 021/102] Make Boresch restraints correction consistent with F=2kx Previously, the correction was implemented assuming F=kx. This lead to erroneously favourable absolute free energies of binding by 1.23 kcal mol-1. --- src/sire/restraints/_standard_state_correction.py | 4 ++++ tests/restraints/test_standard_state_correction.py | 12 ++++++------ 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/src/sire/restraints/_standard_state_correction.py b/src/sire/restraints/_standard_state_correction.py index 3420b5eb2..5366c8344 100644 --- a/src/sire/restraints/_standard_state_correction.py +++ b/src/sire/restraints/_standard_state_correction.py @@ -150,6 +150,10 @@ def _get_boresch_standard_state_correction(restraint, temperature): ).value() force_constants.append(force_const) + # Correct "force constants" by factor of two, because the force is defined as 2kx, + # rather than kx. + force_constants = [2 * k for k in force_constants] + n_nonzero_k = len(force_constants) # Calculation diff --git a/tests/restraints/test_standard_state_correction.py b/tests/restraints/test_standard_state_correction.py index 977e064dc..dab0e1bac 100644 --- a/tests/restraints/test_standard_state_correction.py +++ b/tests/restraints/test_standard_state_correction.py @@ -1,16 +1,16 @@ import pytest # Boresch parameters from old test_boresch_corr SOMD test so we can compare -# to previous results. +# to previous results. Note that the force constants are BORESCH_PARAMS_DEFAULT = { "receptor_selection": "atomidx 1538 or atomidx 1518 or atomidx 1540", "ligand_selection": "atomidx 4 or atomidx 3 or atomidx 5", - "kr": "50.98 kcal mol-1 A-2", - "ktheta": ["133.48 kcal mol-1 rad-2", "76.78 kcal mol-1 rad-2"], + "kr": "25.49 kcal mol-1 A-2", + "ktheta": ["66.74 kcal mol-1 rad-2", "38.39 kcal mol-1 rad-2"], "kphi": [ - "430.72 kcal mol-1 rad-2", - "98.46 kcal mol-1 rad-2", - "99.58 kcal mol-1 rad-2", + "215.36 kcal mol-1 rad-2", + "49.23 kcal mol-1 rad-2", + "49.79 kcal mol-1 rad-2", ], "r0": "5.92 A", "theta0": ["1.85 rad", "1.59 rad"], From 63e76ea91ac369a9388466c1af87d00b7b6575dd Mon Sep 17 00:00:00 2001 From: finlayclark Date: Fri, 1 Nov 2024 13:55:21 +0000 Subject: [PATCH 022/102] Update docs to make clear that F = 2kx for restraints Update the restraint function docstrings, restraints tutorial, and changelog. --- doc/source/changelog.rst | 3 ++ doc/source/tutorial/part06/03_restraints.rst | 31 ++++++++++------- src/sire/restraints/_restraints.py | 35 ++++++++++++-------- 3 files changed, 43 insertions(+), 26 deletions(-) diff --git a/doc/source/changelog.rst b/doc/source/changelog.rst index 3dd88e2b1..424e8dcf7 100644 --- a/doc/source/changelog.rst +++ b/doc/source/changelog.rst @@ -16,6 +16,9 @@ organisation on `GitHub `__. --------------------------------------------------------------------------------------------- * Please add an item to this CHANGELOG for any new features or bug fixes when creating a PR. +* Fixed :func:`sire.restraints.get_standard_state_correction` to be consistent with the definition of + the "force constanst" as ``F = 2 ** k ** x`` (rather than ``F = k ** x``). Updated docstrings and + restraints documentation to make it clear how the force constants are defined. * Fixed instantiaton of ``QByteArray`` in ``Sire::Mol::Frame::toByteArray`` and count bytes with ``QByteArray::size``. * Increase timeout before terminating ``QThread`` objects during ``PageCache`` cleanup. * Expose missing ``timeout`` kwarg in :meth:`dynamics.minimise()` method. diff --git a/doc/source/tutorial/part06/03_restraints.rst b/doc/source/tutorial/part06/03_restraints.rst index 0b35d49fd..341dca762 100644 --- a/doc/source/tutorial/part06/03_restraints.rst +++ b/doc/source/tutorial/part06/03_restraints.rst @@ -13,6 +13,13 @@ in the :mod:`sire.restraints` module. These functions return :class:`~sire.mm.Restraints` objects that contain all of the information that needs to be passed to OpenMM to add the restraints to the system. +.. note:: + + For all harmonic restraints, the restraint energy is defined as ``E = k * x ** 2``, + and not as ``E = 0.5 * k * x ** 2``. Hence, the force is ``F = 2 * k * x`` and all + ``k`` values supplied to the restraints functions are half the value of the force + constants. + Positional Restraints --------------------- @@ -65,11 +72,11 @@ PositionalRestraints( size=3 2: PositionalRestraint( 14 => ( 15.3698, 4.19397, 16.434 ), k=150 kcal mol-1 Ã…-2 : r0=0 Ã… ) ) -The default force constant is 150 kcal mol-1 Ã…-2. By default, the +The default half force constant is 150 kcal mol-1 Ã…-2. By default, the atoms are restrained to their current positions, and are held exactly in those positions (hence why the ``r0=0 Ã…`` in the output above). -You can set the force constant and width of the half-harmonic restraint +You can set the half force constant and width of the half-harmonic restraint used to hold the atoms in position using the ``k`` and ``r0`` arguments, e.g. >>> restraints = sr.restraints.positional(mols, atoms="resname ALA and element C", @@ -177,7 +184,7 @@ BondRestraints( size=1 creates a single harmonic distance (or bond) restraint that acts between atoms 0 and 1. By default, the equilibrium distance (r0) is the current -distance between the atoms (1.09 Ã…), and the force constant (k) is +distance between the atoms (1.09 Ã…), and the half force constant (k) is 150 kcal mol-1 Ã…-2. You can set these via the ``k`` and ``r0`` arguments, e.g. @@ -243,9 +250,9 @@ in the ligand. For more detail, please see J. Phys. Chem. B 2003, 107, 35, 9535 To create a Boresch restraint, you need to specify the receptor and ligand anchor atoms (note that the order of the atoms is important). Like the distance restraints, the atoms can be specified using a search string, passing lists of atom indexes, or -molecule views holding the atoms. You can also specify the force constants and equilibrium -values for the restraints. If not supplied, default force constants of 10 kcal mol-1 Ã…-2 -and 100 kcal mol-1 rad-2 are used for the distance and angle restraints, respectively, +molecule views holding the atoms. You can also specify the half force constants and equilibrium +values for the restraints. If not supplied, default half force constants of 5 kcal mol-1 Ã…-2 +and 50 kcal mol-1 rad-2 are used for the distance and angle restraints, respectively, and the equilibrium values are set to the current values of the distances and angles in the system supplied. For example, @@ -253,13 +260,13 @@ the system supplied. For example, >>> boresch_restraint = boresch_restraints[0] >>> print(boresch_restraint) BoreschRestraint( [1574, 1554, 1576] => [4, 3, 5], - k=[10 kcal mol-1 Ã…-2, 0.0304617 kcal mol-1 °-2, 0.0304617 kcal mol-1 °-2, - 0.0304617 kcal mol-1 °-2, 0.0304617 kcal mol-1 °-2, 0.0304617 kcal mol-1 °-2] + k=[5 kcal mol-1 Ã…-2, 0.0152309 kcal mol-1 °-2, 0.0152309 kcal mol-1 °-2, + 0.0152309 kcal mol-1 °-2, 0.0152309 kcal mol-1 °-2, 0.0152309 kcal mol-1 °-2] r0=15.1197 Ã…, theta0=[80.5212°, 59.818°], phi0=[170.562°Ⱐ128.435°Ⱐ192.21°] ) creates a Boresch restraint where the receptor anchor atoms are r1 = 1574, r2 = 1554, and r3 = 1576, -and the ligand anchor atoms are l1 = 4, l2 = 3, and l3 = 5. The default force constants have been set +and the ligand anchor atoms are l1 = 4, l2 = 3, and l3 = 5. The default half force constants have been set and the equilibrium values have been set to the current values of the distances and angles in the system supplied. @@ -268,10 +275,10 @@ system supplied. Boresch restraints can be subject to instabilities if any three contiguous anchor points approach collinearity (J. Chem. Theory Comput. 2023, 19, 12, 3686–3704). It is important to prevent this by ensuring the associated angles are sufficiently far from 0 or 180 degrees, - and that the `ktheta` force constants are high enough. Sire will raise a warning if the - `theta0` values are too close to 0 or 180 degrees for the given temperature and force constants. + and that the `ktheta` half force constants are high enough. Sire will raise a warning if the + `theta0` values are too close to 0 or 180 degrees for the given temperature and half force constants. -Alternatively, we could have explicitly set the force constants and equilibrium values, e.g. +Alternatively, we could have explicitly set the half force constants and equilibrium values, e.g. >>> boresch_restraints = sr.restraints.boresch(mols, receptor=[1574, 1554, 1576], ligand=[4,3,5], kr = "6.2012 kcal mol-1 A-2", diff --git a/src/sire/restraints/_restraints.py b/src/sire/restraints/_restraints.py index d4c64774e..077cb97c0 100644 --- a/src/sire/restraints/_restraints.py +++ b/src/sire/restraints/_restraints.py @@ -36,7 +36,9 @@ def boresch( Create a set of Boresch restraints that will restrain the 6 external degrees of freedom of the ligand relative to the receptor. All of the atoms in both 'ligand' and 'receptor' must be contained in - 'mols'. + 'mols'. Note that restraint energies are defined as k*x**2 (so forces + are defined as 2*k*x) and hence the 'kr', 'ktheta' and 'kphi' values are + half the force constants for the distance, angle and torsion restraints. The BoreschRestraint will be a set of six restraints between three identified ligand atoms, and three identified receptor @@ -47,7 +49,7 @@ def boresch( 2. Two angle restraints, with specified force constants (ktheta) and equilibrium angles (theta0) parameters. 3. Three torsion restraints, with specified force constants (kphi) - and equilibrium angles (phi0) parameters. + and equilibrium angles (phi0) parameters. This will create a single BoreschRestraint, which will be passed back in a BoreschRestraints object. @@ -64,20 +66,20 @@ def boresch( The ligand atoms to restrain. kr : str or SireUnits::Dimension::GeneralUnit, optional - The force constant for the distance restraint. If None, this will - default to 10 kcal mol-1 A-2. Default is None. + Half the force constant for the distance restraint. If None, this will + default to 5 kcal mol-1 A-2. Default is None. ktheta : str or SireUnits::Dimension::GeneralUnit or list of str or SireUnits::Dimension::GeneralUnit, optional - The force constants for the angle restraints, in the order kthetaA, - kthetaB If None, this will default to 100 kcal mol-1 rad-2 for + Half the force constants for the angle restraints, in the order kthetaA, + kthetaB If None, this will default to 50 kcal mol-1 rad-2 for both angle restraints. If a list, then this should be a list of length 2 containing the force constants for the two angle restraints. If a single value, then this will be used for both angle restraints. Default is None. kphi : str or SireUnits::Dimension::GeneralUnit or list of str or SireUnits::Dimension::GeneralUnit, optional - The force constants for the torsion restraints, in the order kthetaA, - kthetaB, kthetaC. If None, this will default to 100 kcal mol-1 rad-2 + Half the force constants for the torsion restraints, in the order kthetaA, + kthetaB, kthetaC. If None, this will default to 50 kcal mol-1 rad-2 for all three torsion restraints. If a list, then this should be a list of length 3 containing the force constants for the three torsion restraints. If a single value, then this will be used for @@ -173,8 +175,8 @@ def boresch( from .. import measure - default_distance_k = u("10 kcal mol-1 A-2") - default_angle_k = u("100 kcal mol-1 rad-2") + default_distance_k = u("5 kcal mol-1 A-2") + default_angle_k = u("50 kcal mol-1 rad-2") # Use the user-specified equilibrium values if they are provided. distance = [[ligand[0], receptor[0]]] @@ -380,8 +382,10 @@ def distance(mols, atoms0, atoms1, r0=None, k=None, name=None, map=None): Create a set of distance restraints from all of the atoms in 'atoms0' to all of the atoms in 'atoms1' where all atoms are contained in the container 'mols', using the - passed values of the force constant 'k' and equilibrium - bond length r0. + passed values of 'k' and equilibrium bond length r0. + Note that 'k' corresponds to half the force constant, because + the restraint energy is defined as k*(r - r0)**2 (hence the force is + defined as 2*k*(r-r0)). These restraints will be per atom-atom distance. If a list of k and/or r0 values are passed, then different values could be used for @@ -489,8 +493,11 @@ def positional(mols, atoms, k=None, r0=None, position=None, name=None, map=None) """ Create a set of position restraints for the atoms specified in 'atoms' that are contained in the container 'mols', using the - passed values of the force constant 'k' and flat-bottom potential - well-width 'r0' for the restraints. + passed values of 'k' and flat-bottom potential + well-width 'r0' for the restraints. Note that 'k' values + correspond to half the force constants for the harmonic + restraints, because the harmonic restraint energy is defined as + k*(r - r0)**2 (hence the force is defined as 2*(r - r0)). These restraints will be per atom. If a list of k and/or r0 values are passed, then different values could be used for From add86c032b0d11b0d975a34437b1a2010792822c Mon Sep 17 00:00:00 2001 From: Lester Hedges Date: Wed, 30 Oct 2024 16:42:32 +0000 Subject: [PATCH 023/102] Don't always save frames and energies after dynamics.run() [ref #256] --- src/sire/mol/_dynamics.py | 77 +++++++++++++-------------------------- 1 file changed, 25 insertions(+), 52 deletions(-) diff --git a/src/sire/mol/_dynamics.py b/src/sire/mol/_dynamics.py index 40c8b125a..f174a5216 100644 --- a/src/sire/mol/_dynamics.py +++ b/src/sire/mol/_dynamics.py @@ -209,6 +209,8 @@ def _update_from(self, state, state_has_cv, nsteps_completed): if self.is_null(): return + self._current_step = nsteps_completed + if not state_has_cv[0]: # there is no information to update return @@ -247,8 +249,6 @@ def _update_from(self, state, state_has_cv, nsteps_completed): map=self._map, ) - self._current_step = nsteps_completed - self._sire_mols.update(mols_to_update.molecules()) if self._ffinfo.space().is_periodic(): @@ -924,50 +924,6 @@ def process_block(state, state_has_cv, nsteps_completed): } ) - 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 - of number of steps, and then if a frame should be - saved and if the energy should be saved - """ - if completed < 0: - completed = 0 - - if completed >= total: - return ( - 0, - frame_frequency_steps > 0, - energy_frequency_steps > 0, - ) - - elif frame_frequency_steps <= 0 and energy_frequency_steps <= 0: - return (total, False, False) - - n_to_end = total - completed - - if frame_frequency_steps > 0: - n_to_frame = min( - frame_frequency_steps - (completed % frame_frequency_steps), - n_to_end, - ) - else: - n_to_frame = total - completed - - if energy_frequency_steps > 0: - n_to_energy = min( - energy_frequency_steps - (completed % energy_frequency_steps), - n_to_end, - ) - else: - n_to_energy = total - completed - - if n_to_frame == n_to_energy: - return (n_to_frame, True, True) - elif n_to_frame < n_to_energy: - return (n_to_frame, True, False) - else: - return (n_to_energy, False, True) - block_size = 50 state = None @@ -978,6 +934,9 @@ class NeedsMinimiseError(Exception): pass nsteps_before_run = self._current_step + if nsteps_before_run == 0: + self._next_save_frame = frame_frequency_steps + self._next_save_energy = energy_frequency_steps from ..base import ProgressBar from ..units import second @@ -992,13 +951,27 @@ class NeedsMinimiseError(Exception): with ThreadPoolExecutor() as pool: while completed < steps_to_run: - ( - nrun_till_save, - save_frame, - save_energy, - ) = get_steps_till_save(completed, steps_to_run) + steps_till_frame = self._next_save_frame - ( + completed + nsteps_before_run + ) + if steps_till_frame <= 0 or steps_till_frame == steps_to_run: + save_frame = True + self._next_save_frame += frame_frequency_steps + else: + save_frame = False + + steps_till_energy = self._next_save_energy - ( + completed + nsteps_before_run + ) + if steps_till_energy <= 0 or steps_till_energy == steps_to_run: + save_energy = True + self._next_save_energy += energy_frequency_steps + else: + save_energy = False + + nrun_till_save = min(steps_till_frame, steps_till_energy) - assert nrun_till_save > 0 + assert nrun_till_save >= 0 self._enter_dynamics_block() From 175ecf4fd4820c1f17787e3fca713159719503c0 Mon Sep 17 00:00:00 2001 From: Lester Hedges Date: Fri, 8 Nov 2024 14:43:12 +0000 Subject: [PATCH 024/102] Simplify test logic. --- tests/convert/test_openmm_constraints.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/tests/convert/test_openmm_constraints.py b/tests/convert/test_openmm_constraints.py index c7427391a..ef98bb276 100644 --- a/tests/convert/test_openmm_constraints.py +++ b/tests/convert/test_openmm_constraints.py @@ -467,7 +467,6 @@ def test_asymmetric_constraints(merged_ethane_methanol): # different constraints. from math import isclose - from openmm import XmlSerializer from tempfile import NamedTemporaryFile # Extract the molecule. @@ -497,9 +496,9 @@ def test_asymmetric_constraints(merged_ethane_methanol): xml0 = NamedTemporaryFile() xml1 = NamedTemporaryFile() with open(xml0.name, "w") as f: - f.write(XmlSerializer.serialize(d_forwards._d._omm_mols.getSystem())) + f.write(d_forwards.to_xml()) with open(xml1.name, "w") as f: - f.write(XmlSerializer.serialize(d_backwards._d._omm_mols.getSystem())) + f.write(d_backwards.to_xml()) # Load the serialised systems and sort. with open(xml0.name, "r") as f: From 46b5d3f17659a3611faf91744358a73162357394 Mon Sep 17 00:00:00 2001 From: Lester Hedges Date: Fri, 8 Nov 2024 16:22:10 +0000 Subject: [PATCH 025/102] Fix thread safety issue in OpenMM minimiser. [closes #259] --- wrapper/Convert/SireOpenMM/openmmminimise.cpp | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/wrapper/Convert/SireOpenMM/openmmminimise.cpp b/wrapper/Convert/SireOpenMM/openmmminimise.cpp index dd5a5f536..2f08f4509 100644 --- a/wrapper/Convert/SireOpenMM/openmmminimise.cpp +++ b/wrapper/Convert/SireOpenMM/openmmminimise.cpp @@ -50,6 +50,7 @@ #include // CHAR_BIT #include #include // uint64_t +#include inline auto is_ieee754_nan(double const x) -> bool @@ -619,6 +620,8 @@ namespace SireOpenMM double starting_k, double ratchet_scale, double max_constraint_error, double timeout) { + PyThreadState *_save = PyEval_SaveThread(); + if (max_iterations < 0) { max_iterations = std::numeric_limits::max(); @@ -629,8 +632,6 @@ namespace SireOpenMM timeout = std::numeric_limits::max(); } - auto gil = SireBase::release_gil(); - const OpenMM::System &system = context.getSystem(); int num_particles = system.getNumParticles(); @@ -1105,6 +1106,8 @@ namespace SireOpenMM CODELOC); } + PyEval_RestoreThread(_save); + return data.getLog().join("\n"); } From 627926ef98a44a76358f2830d02ffe10a3223637 Mon Sep 17 00:00:00 2001 From: Lester Hedges Date: Fri, 8 Nov 2024 16:55:30 +0000 Subject: [PATCH 026/102] Remove redundant include. --- wrapper/Convert/SireOpenMM/openmmminimise.cpp | 1 - 1 file changed, 1 deletion(-) diff --git a/wrapper/Convert/SireOpenMM/openmmminimise.cpp b/wrapper/Convert/SireOpenMM/openmmminimise.cpp index 2f08f4509..500401b4b 100644 --- a/wrapper/Convert/SireOpenMM/openmmminimise.cpp +++ b/wrapper/Convert/SireOpenMM/openmmminimise.cpp @@ -92,7 +92,6 @@ inline auto is_ieee754_nan(double const x) #include #include "SireError/errors.h" -#include "SireBase/releasegil.h" #include "SireBase/progressbar.h" #include "SireUnits/units.h" From a291ea4ed207186eaae06d50c01c187e46c4bd92 Mon Sep 17 00:00:00 2001 From: Lester Hedges Date: Mon, 11 Nov 2024 12:04:02 +0000 Subject: [PATCH 027/102] Update CHANGELOG. --- doc/source/changelog.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/doc/source/changelog.rst b/doc/source/changelog.rst index 3dd88e2b1..8ee6fe577 100644 --- a/doc/source/changelog.rst +++ b/doc/source/changelog.rst @@ -21,6 +21,9 @@ organisation on `GitHub `__. * Expose missing ``timeout`` kwarg in :meth:`dynamics.minimise()` method. * Expose missing ``include_constrained_energies`` kwarg in minimisation function. * Make minimisation function settings consistent across API. +* Don't automatically save energies and frames when ``dynamics.run()`` returns. +* Fix thread safety issue in Sire OpenMM minimiser. +* Improved handling of NaN errors during dynamics. `2024.3.0 `__ - October 2024 -------------------------------------------------------------------------------------------- From a1c1277a78f7c62d3dcf79df119f394686d000c6 Mon Sep 17 00:00:00 2001 From: Lester Hedges Date: Mon, 11 Nov 2024 12:04:26 +0000 Subject: [PATCH 028/102] Improve handling of OpenMM NaN errors at runtime. --- src/sire/mol/_dynamics.py | 53 ++++++++++++++++++++++++++++----------- 1 file changed, 39 insertions(+), 14 deletions(-) diff --git a/src/sire/mol/_dynamics.py b/src/sire/mol/_dynamics.py index f174a5216..472482a02 100644 --- a/src/sire/mol/_dynamics.py +++ b/src/sire/mol/_dynamics.py @@ -777,13 +777,11 @@ def _rebuild_and_minimise(self): from ..utils import Console Console.warning( - "Something went wrong when running dynamics. Since no steps " - "were completed, it is likely that the system needs minimising. " - "The system will be minimised, and then dynamics will be " - "attempted again. If an error still occurs, then it is likely " - "that the step size is too large, the molecules are " - "over-constrained, or there is something more fundemental " - "going wrong..." + "Something went wrong when running dynamics. The system will be " + "minimised, and then dynamics will be attempted again. If an " + "error still occurs, then it is likely that the step size is too " + "large, the molecules are over-constrained, or there is something " + "more fundemental going wrong..." ) # rebuild the molecules @@ -930,6 +928,10 @@ def process_block(state, state_has_cv, nsteps_completed): state_has_cv = (False, False) saved_last_frame = False + # whether the energy or frame were saved after the current block + have_saved_frame = False + have_saved_energy = False + class NeedsMinimiseError(Exception): pass @@ -1000,8 +1002,14 @@ class NeedsMinimiseError(Exception): while not run_promise.done(): try: result = run_promise.result(timeout=1.0) - except Exception: - pass + except Exception as e: + if ( + "NaN" in str(e) + and not have_saved_frame + and not have_saved_energy + and auto_fix_minimise + ): + raise NeedsMinimiseError() # catch rare edge case where the promise timed # out, but then completed before the .done() @@ -1019,10 +1027,20 @@ class NeedsMinimiseError(Exception): if process_promise is not None: try: process_promise.result() - except Exception: - pass - - if completed == 0 and auto_fix_minimise: + except Exception as e: + if ( + "NaN" in str(e) + and not have_saved_frame + and not have_saved_energy + and auto_fix_minimise + ): + raise NeedsMinimiseError() + + if ( + not have_saved_frame + and not have_saved_energy + and auto_fix_minimise + ): raise NeedsMinimiseError() # something went wrong - re-raise the exception @@ -1045,6 +1063,9 @@ class NeedsMinimiseError(Exception): saved_last_frame = False + have_saved_frame = save_frame + have_saved_energy = save_energy + kinetic_energy = state.getKineticEnergy().value_in_unit( openmm.unit.kilocalorie_per_mole ) @@ -1056,7 +1077,11 @@ class NeedsMinimiseError(Exception): state = None saved_last_frame = True - if completed == 0 and auto_fix_minimise: + if ( + not have_saved_frame + and not have_saved_energy + and auto_fix_minimise + ): raise NeedsMinimiseError() raise RuntimeError( From 597d8c3d117cc189b0c98c399c1251179cc2da55 Mon Sep 17 00:00:00 2001 From: Lester Hedges Date: Thu, 21 Nov 2024 11:12:20 +0000 Subject: [PATCH 029/102] Add support for REST2. --- src/sire/mol/__init__.py | 36 ++++ src/sire/mol/_dynamics.py | 56 ++++++ src/sire/morph/_perturbation.py | 2 +- src/sire/system/_system.py | 21 ++ .../Convert/SireOpenMM/LambdaLever.pypp.cpp | 4 +- .../PerturbableOpenMMMolecule.pypp.cpp | 2 +- wrapper/Convert/SireOpenMM/_sommcontext.py | 181 +++++++++++++++++- wrapper/Convert/SireOpenMM/lambdalever.cpp | 99 ++++++++-- wrapper/Convert/SireOpenMM/lambdalever.h | 2 +- wrapper/Convert/SireOpenMM/openmmmolecule.cpp | 62 +++++- wrapper/Convert/SireOpenMM/openmmmolecule.h | 26 ++- .../SireOpenMM/sire_to_openmm_system.cpp | 12 +- 12 files changed, 475 insertions(+), 28 deletions(-) diff --git a/src/sire/mol/__init__.py b/src/sire/mol/__init__.py index ad8004fa9..c652d1fba 100644 --- a/src/sire/mol/__init__.py +++ b/src/sire/mol/__init__.py @@ -1567,6 +1567,8 @@ def _dynamics( ignore_perturbations=None, temperature=None, pressure=None, + rest2_scale=None, + rest2_selection=None, vacuum=None, shift_delta=None, shift_coulomb=None, @@ -1686,6 +1688,34 @@ def _dynamics( microcanonical (NVE) or canonical (NVT) simulation will be run if the pressure is not set. + rest2_scale: float + The scaling factor to apply when running a REST2 simulation + (Replica Exchange with Solute Tempering). This defaults to 1.0. + This specifies the ratio of the temperature of the solute to + the temperature of the solvent. The scaling factor is linearly + interpolated between the two temperatures so that the solute has + a temperature of rest2_scale * temperature in the intermediate, + lambda = 0.5 state, and the end states are at the original + temperature. + + rest2_selection: str + A selection string for atoms to include in the REST2 region + in addition to any perturbable molecules. For example, + "molidx 0 and residx 0,1,2" would select atoms from the first + three residues of the first molecule. If None, then all atoms + within perturbable molecules will be included in the REST2 + region. When atoms within a perturbable molecule are also + included in the selection, then only those atoms will be + considered as part of the REST2 region. This allows REST2 to + be applied to protein mutations. + + A selection string for atoms to include in the REST2 region in + addition to the perturbable molecule. For example, + "molidx 0 and residx 0,1,2" would select atoms from the first + three residues of the first molecule. Selected atoms should be + within the same molecule. If None, then only the perturbable + molecule will be used for the REST2 region. + vacuum: bool (optional) Whether or not to run the simulation in vacuum. If this is set to `True`, then the simulation space automatically be @@ -1852,6 +1882,12 @@ def _dynamics( pressure = u(pressure) map.set("pressure", pressure) + if rest2_scale is not None: + map.set("rest2_scale", rest2_scale) + + if rest2_selection is not None: + map.set("rest2_selection", rest2_selection) + if device is not None: map.set("device", str(device)) diff --git a/src/sire/mol/_dynamics.py b/src/sire/mol/_dynamics.py index 472482a02..4bedc84c5 100644 --- a/src/sire/mol/_dynamics.py +++ b/src/sire/mol/_dynamics.py @@ -179,6 +179,59 @@ def __init__(self, mols=None, map=None, **kwargs): self._is_running = False self._schedule_changed = False + # Check for a REST2 scaling factor. + if map.specified("rest2_scale"): + try: + rest2_scale = map["rest2_scale"].value().as_double() + except: + raise ValueError("'rest2_scale' must be of type 'float'") + if rest2_scale < 1.0: + raise ValueError("'rest2_scale' must be >= 1.0") + + # Check for an additional REST2 selection. + if map.specified("rest2_selection"): + try: + rest2_selection = str(map["rest2_selection"]) + except: + raise ValueError("'rest2_selection' must be of type 'str'") + + try: + from . import selection_to_atoms + + # Try to find the REST2 selection. + atoms = selection_to_atoms(mols, rest2_selection) + except: + raise ValueError( + "Invalid 'rest2_selection' string. Please check the selection syntax." + ) + + # Store all the perturbable molecules associated with the selection. + pert_mols = {} + for atom in atoms: + mol = atom.molecule() + if mol.has_property("is_perturbable"): + if mol not in pert_mols: + pert_mols[mol] = [atom] + else: + pert_mols[mol].append(atom) + + # Now create a boolean is_rest2 mask for the atoms in the perturbable molecules. + for mol in pert_mols: + is_rest2 = [False] * mol.num_atoms() + for atom in pert_mols[mol]: + is_rest2[atom.index().value()] = True + + # Set the is_rest2 property for each perturbable molecule. + mol = ( + mol.edit() + .set_property("is_rest2", is_rest2) + .molecule() + .commit() + ) + + # Update the system. + self._sire_mols.update(mol) + from ..convert import to self._omm_mols = to(self._sire_mols, "openmm", map=self._map) @@ -194,6 +247,9 @@ def __init__(self, mols=None, map=None, **kwargs): else: self._enforce_periodic_box = False + # Prepare the OpenMM REST2 data structures. + if map.specified("rest2_scale") and map.specified("rest2_selection"): + self._omm_mols._prepare_rest2(self._sire_mols, atoms) else: self._sire_mols = None self._energy_trajectory = None diff --git a/src/sire/morph/_perturbation.py b/src/sire/morph/_perturbation.py index 6f6698a5e..4c7bab201 100644 --- a/src/sire/morph/_perturbation.py +++ b/src/sire/morph/_perturbation.py @@ -840,7 +840,7 @@ def to_openmm( from ..convert.openmm import PerturbableOpenMMMolecule try: - return PerturbableOpenMMMolecule(self._mol.molecule(), map=map) + return PerturbableOpenMMMolecule(self._mol.molecule(), 0, map=map) except KeyError: # probably need to choose an end-state - default to reference return PerturbableOpenMMMolecule( diff --git a/src/sire/system/_system.py b/src/sire/system/_system.py index e2bd72a7f..9b9f3f2bb 100644 --- a/src/sire/system/_system.py +++ b/src/sire/system/_system.py @@ -592,6 +592,27 @@ def dynamics(self, *args, **kwargs): microcanonical (NVE) or canonical (NVT) simulation will be run if the pressure is not set. + rest2_scale: float + The scaling factor to apply when running a REST2 simulation + (Replica Exchange with Solute Tempering). This defaults to 1.0. + This specifies the ratio of the temperature of the solute to + the temperature of the solvent. The scaling factor is linearly + interpolated between the two temperatures so that the solute has + a temperature of rest2_scale * temperature in the intermediate, + lambda = 0.5 state, and the end states are at the original + temperature. + + rest2_selection: str + A selection string for atoms to include in the REST2 region + in addition to any perturbable molecules. For example, + "molidx 0 and residx 0,1,2" would select atoms from the first + three residues of the first molecule. If None, then all atoms + within perturbable molecules will be included in the REST2 + region. When atoms within a perturbable molecule are also + included in the selection, then only those atoms will be + considered as part of the REST2 region. This allows REST2 to + be applied to protein mutations. + vacuum: bool Whether or not to run the simulation in vacuum. If this is set to `True`, then the simulation space automatically be diff --git a/wrapper/Convert/SireOpenMM/LambdaLever.pypp.cpp b/wrapper/Convert/SireOpenMM/LambdaLever.pypp.cpp index 54e828a53..e85a63546 100644 --- a/wrapper/Convert/SireOpenMM/LambdaLever.pypp.cpp +++ b/wrapper/Convert/SireOpenMM/LambdaLever.pypp.cpp @@ -231,13 +231,13 @@ void register_LambdaLever_class(){ } { //::SireOpenMM::LambdaLever::setLambda - typedef double ( ::SireOpenMM::LambdaLever::*setLambda_function_type)( ::OpenMM::Context &,double,bool ) const; + typedef double ( ::SireOpenMM::LambdaLever::*setLambda_function_type)( ::OpenMM::Context &,double,double,bool ) const; setLambda_function_type setLambda_function_value( &::SireOpenMM::LambdaLever::setLambda ); LambdaLever_exposer.def( "setLambda" , setLambda_function_value - , ( bp::arg("system"), bp::arg("lambda_value"), bp::arg("update_constraints")=(bool)(true) ) + , ( bp::arg("system"), bp::arg("lambda_value"), bp::arg("rest2_scale")=(double)(1.0), bp::arg("update_constraints")=(bool)(true) ) , "Set the value of lambda in the passed context. Returns the\n actual value of lambda set.\n" ); } diff --git a/wrapper/Convert/SireOpenMM/PerturbableOpenMMMolecule.pypp.cpp b/wrapper/Convert/SireOpenMM/PerturbableOpenMMMolecule.pypp.cpp index 2c8453127..95cd479f1 100644 --- a/wrapper/Convert/SireOpenMM/PerturbableOpenMMMolecule.pypp.cpp +++ b/wrapper/Convert/SireOpenMM/PerturbableOpenMMMolecule.pypp.cpp @@ -126,7 +126,7 @@ void register_PerturbableOpenMMMolecule_class(){ PerturbableOpenMMMolecule_exposer_t PerturbableOpenMMMolecule_exposer = PerturbableOpenMMMolecule_exposer_t( "PerturbableOpenMMMolecule", "This class holds all of the information of an OpenMM molecule\nthat can be perturbed using a LambdaSchedule. The data is held\nin easy-to-access arrays, with guarantees that the arrays are\ncompatible and the data is aligned.\n", bp::init< >("Null constructor") ); bp::scope PerturbableOpenMMMolecule_scope( PerturbableOpenMMMolecule_exposer ); PerturbableOpenMMMolecule_exposer.def( bp::init< SireOpenMM::OpenMMMolecule const &, bp::optional< SireBase::PropertyMap const & > >(( bp::arg("mol"), bp::arg("map")=SireBase::PropertyMap() ), "Construct from the passed OpenMMMolecule") ); - PerturbableOpenMMMolecule_exposer.def( bp::init< SireMol::Molecule const &, bp::optional< SireBase::PropertyMap const & > >(( bp::arg("mol"), bp::arg("map")=SireBase::PropertyMap() ), "Construct from a passed molecule and map") ); + PerturbableOpenMMMolecule_exposer.def( bp::init< SireMol::Molecule const &, int, bp::optional< SireBase::PropertyMap const & > >(( bp::arg("mol"), bp::arg("map")=SireBase::PropertyMap() ), "Construct from a passed molecule and map") ); PerturbableOpenMMMolecule_exposer.def( bp::init< SireOpenMM::PerturbableOpenMMMolecule const & >(( bp::arg("other") ), "Copy constructor") ); { //::SireOpenMM::PerturbableOpenMMMolecule::angles diff --git a/wrapper/Convert/SireOpenMM/_sommcontext.py b/wrapper/Convert/SireOpenMM/_sommcontext.py index cceab24ab..f6d3a7f7a 100644 --- a/wrapper/Convert/SireOpenMM/_sommcontext.py +++ b/wrapper/Convert/SireOpenMM/_sommcontext.py @@ -69,8 +69,28 @@ def __init__( # place the coordinates and velocities into the context set_openmm_coordinates_and_velocities(self, metadata) + # Check for a REST2 scaling factor. + if map.specified("rest2_scale"): + try: + rest2_scale = map["rest2_scale"].value().as_double() + except: + raise ValueError("'rest2_scale' must be of type 'float'") + if rest2_scale < 1.0: + raise ValueError("'rest2_scale' must be >= 1.0") + self._rest2_scale = rest2_scale + + if map.specified("rest2_selection"): + self._has_rest2_selection = True + else: + self._has_rest2_selection = False + else: + self._rest2_scale = 1.0 + self._has_rest2_selection = False + self._lambda_value = self._lambda_lever.set_lambda( - self, lambda_value=lambda_value, update_constraints=True + self, + lambda_value=lambda_value, + update_constraints=True, ) self._map = map @@ -227,16 +247,23 @@ def set_lambda_schedule(self, schedule): def set_lambda(self, lambda_value: float, update_constraints: bool = True): """ Update the parameters in the context to set the lambda value - to 'lamval'. If update_constraints is True then the constraints - will be updated to match the new value of lambda + to 'lambda_value'. If update_constraints is True then the constraints + will be updated to match the new value of lambda. """ if self._lambda_lever is None: return self._lambda_value = self._lambda_lever.set_lambda( - self, lambda_value=lambda_value, update_constraints=update_constraints + self, + lambda_value=lambda_value, + rest2_scale=self._rest2_scale, + update_constraints=update_constraints, ) + # Update any additional parameters in the REST2 region. + if self._has_rest2_selection: + self._update_rest2(lambda_value, self._rest2_scale) + def set_temperature(self, temperature, rescale_velocities=True): """ Set the target temperature for the dynamics. If @@ -314,3 +341,149 @@ def to_xml(self, f=None): handle.write(_XmlSerializer.serialize(self.getSystem())) else: f.write(_XmlSerializer.serialize(self.getSystem())) + + def _prepare_rest2(self, system, atoms): + """ + Internal method to prepare the REST2 data structures. + """ + + # Adapted from code in meld: https://github.com/maccallumlab/meld + + import openmm + from ..Mol import AtomIdx, Connectivity + + # Work out the molecules to which the atoms belong. + mols = [] + for atom in atoms: + mol = atom.molecule() + # Perturbable molecules are handled separately. + if mol not in mols and not mol.has_property("is_perturbable"): + mols.append(mol) + + # Store the OpenMM system. + omm_system = self.getSystem() + + # Get the NonBonded force. + nonbonded_force = None + for force in omm_system.getForces(): + if isinstance(force, openmm.NonbondedForce): + nonbonded_force = force + break + if nonbonded_force is None: + raise ValueError("No NonbondedForce found in the OpenMM system.") + self._nonbonded_force = nonbonded_force + + # Get the PeriodicTorsionForce. + periodic_torsion_force = None + for force in omm_system.getForces(): + if isinstance(force, openmm.PeriodicTorsionForce): + periodic_torsion_force = force + break + if periodic_torsion_force is None: + raise ValueError("No PeriodicTorsionForce found in the OpenMM system.") + self._periodic_torsion_force = periodic_torsion_force + + # Initialise the parameter dictionaries. + self._nonbonded_params = {} + self._exception_params = {} + self._torsion_params = {} + + # Process each of the molecules. + for mol in mols: + # Create the connectivity object for the molecule. + connectivity = Connectivity(mol.info()).edit() + + # Loop over the bonds in the molecule and connect the atoms. + for bond in mol.bonds(): + connectivity.connect(bond.atom0().index(), bond.atom1().index()) + connectivity = connectivity.commit() + + # Find the index of the molecule in the system. + mol_idx = system._system.mol_nums().index(mol.number()) + + # Work out the offset to apply to the atom indices to convert to system indices. + num_atoms = 0 + for mol in system.molecules()[:mol_idx]: + num_atoms += mol.num_atoms() + + # Create a list of atom indices. + atom_idxs = [atom.index().value() + num_atoms for atom in atoms] + + # Gather the nonbonded parameters for the atoms in the selection. + for idx in atom_idxs: + self._nonbonded_params[idx] = nonbonded_force.getParticleParameters(idx) + + # Store the exception parameters. + for param_index in range(nonbonded_force.getNumExceptions()): + params = nonbonded_force.getExceptionParameters(param_index) + if params[0] in atom_idxs and params[1] in atom_idxs: + self._exception_params[param_index] = params + + # Gather the torsion parameters for the atoms in the selection. + for param_index in range(periodic_torsion_force.getNumTorsions()): + params = periodic_torsion_force.getTorsionParameters(param_index) + i, j, k, l, _, _, _ = params + + # Don't modify non-REST2 torsions. + if ( + i not in atom_idxs + or j not in atom_idxs + or k not in atom_idxs + or l not in atom_idxs + ): + continue + + # Convert to AtomIdx objects. + idx_i = AtomIdx(i - num_atoms) + idx_l = AtomIdx(l - num_atoms) + + if connectivity.are_dihedraled(idx_i, idx_l): + self._torsion_params[param_index] = params + + def _update_rest2(self, lambda_value, rest2_scale): + """ + Internal method to update the REST2 parameters. + """ + + # Adapted from code in meld: https://github.com/maccallumlab/meld + + from math import sqrt + + # Work out the actual REST scaling factor. The passed value is the ratio of + # the REST temperature to the simulation temperature. The full scaling factor + # is used at lambda = 0.5 and is scaled to 1.0 at lambda = 0 and lambda = 1. + scale = 1.0 + (rest2_scale - 1.0) * (1.0 - 2.0 * abs(lambda_value - 0.5)) + + # This is the temperature scale factor, so we need to invert to get the energy + # scale factor. + scale = 1.0 / scale + + # Store the REST2 charge scaling factor for non-bonded interactions. + sqrt_scale = sqrt(scale) + + # Update the non-bonded parameters. + for index, params in self._nonbonded_params.items(): + q, sigma, epsilon = params + self._nonbonded_force.setParticleParameters( + index, q * sqrt_scale, sigma, epsilon * scale + ) + + # Update the exception parameters. + for index, params in self._exception_params.items(): + i, j, q, sigma, epsilon = params + self._nonbonded_force.setExceptionParameters( + index, i, j, q * scale, sigma, epsilon * scale + ) + + # Update the parameters in the context. + self._nonbonded_force.updateParametersInContext(self) + + # Update the torsion parameters. + for index, params in self._torsion_params.items(): + i, j, k, l, periodicity, phase, k = params + self._periodic_torsion_force.setTorsionParameters( + index, i, j, k, l, periodicity, phase, k * scale + ) + + # Update the parameters in the context. + self._periodic_torsion_force.updateParametersInContext(self) diff --git a/wrapper/Convert/SireOpenMM/lambdalever.cpp b/wrapper/Convert/SireOpenMM/lambdalever.cpp index 67707a398..859ae9ff6 100644 --- a/wrapper/Convert/SireOpenMM/lambdalever.cpp +++ b/wrapper/Convert/SireOpenMM/lambdalever.cpp @@ -1126,6 +1126,7 @@ PropertyList LambdaLever::getLeverValues(const QVector &lambda_values, */ double LambdaLever::setLambda(OpenMM::Context &context, double lambda_value, + double rest2_scale, bool update_constraints) const { // go over each forcefield and update the parameters in the forcefield, @@ -1135,6 +1136,18 @@ double LambdaLever::setLambda(OpenMM::Context &context, lambda_value = this->lambda_schedule.clamp(lambda_value); + // Work out the actual REST scaling factor. The passed value is the ratio of + // the REST temperature to the simulation temperature. The full scaling factor + // is used at lambda = 0.5 and is scaled to 1.0 at lambda = 0 and lambda = 1. + rest2_scale = 1.0 + (rest2_scale - 1.0)*(1.0 - 2.0 * std::abs(lambda_value - 0.5)); + + // This is the temperature scale factor, so we need to invert to get the energy + // scale factor. + rest2_scale = 1.0 / rest2_scale; + + // Store the REST charge scaling factor for non-bonded interactions. + const auto sqrt_rest2_scale = std::sqrt(rest2_scale); + // we need an editable reference to the system to get editable // pointers to the forces... OpenMM::System &system = const_cast(context.getSystem()); @@ -1188,6 +1201,10 @@ double LambdaLever::setLambda(OpenMM::Context &context, // now update the forcefields int start_index = start_idxs.value("clj", -1); + // record the index of the first atom in the perturbable molecule within + // the original Sire system + const auto start_atom_idx = perturbable_mol.getStartAtomIdx(); + if (start_index != -1 and cljff != 0) { const auto morphed_charges = cache.morph( @@ -1353,12 +1370,22 @@ double LambdaLever::setLambda(OpenMM::Context &context, const bool is_from_ghost = perturbable_mol.getFromGhostIdxs().contains(j); const bool is_to_ghost = perturbable_mol.getToGhostIdxs().contains(j); + double scale = 1.0; + double sqrt_scale = 1.0; + + // apply the REST2 scaling. + if (perturbable_mol.isRest2(j)) + { + scale = rest2_scale; + sqrt_scale = sqrt_rest2_scale; + } + // reduced charge - custom_params[0] = morphed_ghost_charges[j]; + custom_params[0] = sqrt_scale * morphed_ghost_charges[j]; // half_sigma custom_params[1] = 0.5 * morphed_ghost_sigmas[j]; // two_sqrt_epsilon - custom_params[2] = 2.0 * std::sqrt(morphed_ghost_epsilons[j]); + custom_params[2] = 2.0 * sqrt_scale * std::sqrt(morphed_ghost_epsilons[j]); // alpha custom_params[3] = morphed_ghost_alphas[j]; // kappa @@ -1379,11 +1406,11 @@ double LambdaLever::setLambda(OpenMM::Context &context, ghost_ghostff->setParticleParameters(start_index + j, custom_params); // reduced charge - custom_params[0] = morphed_nonghost_charges[j]; + custom_params[0] = sqrt_scale * morphed_nonghost_charges[j]; // half_sigma custom_params[1] = 0.5 * morphed_nonghost_sigmas[j]; // two_sqrt_epsilon - custom_params[2] = 2.0 * std::sqrt(morphed_nonghost_epsilons[j]); + custom_params[2] = 2.0 * sqrt_scale * std::sqrt(morphed_nonghost_epsilons[j]); // alpha custom_params[3] = morphed_nonghost_alphas[j]; // kappa @@ -1406,11 +1433,13 @@ double LambdaLever::setLambda(OpenMM::Context &context, if (is_from_ghost or is_to_ghost) { // don't set the LJ parameters in the cljff - cljff->setParticleParameters(start_index + j, morphed_charges[j], 0.0, 0.0); + cljff->setParticleParameters(start_index + j, sqrt_scale * morphed_charges[j], 0.0, 0.0); } else { - cljff->setParticleParameters(start_index + j, morphed_charges[j], morphed_sigmas[j], morphed_epsilons[j]); + cljff->setParticleParameters( + start_index + j, sqrt_scale* morphed_charges[j], + morphed_sigmas[j], scale * morphed_epsilons[j]); } } } @@ -1418,7 +1447,18 @@ double LambdaLever::setLambda(OpenMM::Context &context, { for (int j = 0; j < nparams; ++j) { - cljff->setParticleParameters(start_index + j, morphed_charges[j], morphed_sigmas[j], morphed_epsilons[j]); + double scale = 1.0; + double sqrt_scale = 1.0; + + // apply the REST2 scaling. + if (perturbable_mol.isRest2(j)) + { + scale = rest2_scale; + sqrt_scale = sqrt_rest2_scale; + } + + cljff->setParticleParameters(start_index + j, sqrt_scale * morphed_charges[j], + morphed_sigmas[j], scale * morphed_epsilons[j]); } } @@ -1446,6 +1486,14 @@ double LambdaLever::setLambda(OpenMM::Context &context, morphed_charges, morphed_sigmas, morphed_epsilons, morphed_alphas, morphed_kappas); + double scale = 1.0; + + // apply the REST2 scaling. + if (perturbable_mol.isRest2(atom0) and perturbable_mol.isRest2(atom1)) + { + scale = rest2_scale; + } + // don't set LJ terms for ghost atoms if (atom0_is_ghost or atom1_is_ghost) { @@ -1488,10 +1536,13 @@ double LambdaLever::setLambda(OpenMM::Context &context, morphed_ghost14_kappas); // parameters are q, sigma, four_epsilon and alpha - std::vector params14 = - {boost::get<2>(p), boost::get<3>(p), - 4.0 * boost::get<4>(p), boost::get<5>(p), - boost::get<6>(p)}; + std::vector params14 = { + boost::get<2>(p) * scale, + boost::get<3>(p), + 4.0 * boost::get<4>(p) * scale, + boost::get<5>(p), + boost::get<6>(p) + }; if (start_change_14 == -1) { @@ -1518,8 +1569,8 @@ double LambdaLever::setLambda(OpenMM::Context &context, cljff->setExceptionParameters( boost::get<0>(idxs[j]), boost::get<0>(p), boost::get<1>(p), - boost::get<2>(p), boost::get<3>(p), - boost::get<4>(p)); + boost::get<2>(p) * scale, boost::get<3>(p), + boost::get<4>(p) * scale); } } } @@ -1679,6 +1730,8 @@ double LambdaLever::setLambda(OpenMM::Context &context, const int nparams = morphed_torsion_k.count(); + const auto is_improper = perturbable_mol.getIsImproper(); + if (start_change_torsion == -1) { start_change_torsion = start_index; @@ -1707,12 +1760,30 @@ double LambdaLever::setLambda(OpenMM::Context &context, particle3, particle4, periodicity, phase, k); + // get the indices of the particles in the Sire molecule + const auto atom1 = particle1 - start_atom_idx; + const auto atom2 = particle2 - start_atom_idx; + const auto atom3 = particle3 - start_atom_idx; + const auto atom4 = particle4 - start_atom_idx; + + // Only apply the REST2 scaling factor if this is a proper dihedral + // and all atoms are in the REST2 region. + double scale = 1.0; + if (not is_improper[j] and + perturbable_mol.isRest2(atom1) and + perturbable_mol.isRest2(atom2) and + perturbable_mol.isRest2(atom3) and + perturbable_mol.isRest2(atom4)) + { + scale = rest2_scale; + } + dihff->setTorsionParameters(index, particle1, particle2, particle3, particle4, periodicity, morphed_torsion_phase[j], - morphed_torsion_k[j]); + morphed_torsion_k[j] * scale); } } } diff --git a/wrapper/Convert/SireOpenMM/lambdalever.h b/wrapper/Convert/SireOpenMM/lambdalever.h index 513025800..c163ceca6 100644 --- a/wrapper/Convert/SireOpenMM/lambdalever.h +++ b/wrapper/Convert/SireOpenMM/lambdalever.h @@ -117,7 +117,7 @@ namespace SireOpenMM const PerturbableOpenMMMolecule &mol) const; double setLambda(OpenMM::Context &system, double lambda_value, - bool update_constraints = true) const; + double rest2_scale = 1.0, bool update_constraints = true) const; void setForceIndex(const QString &force, int index); diff --git a/wrapper/Convert/SireOpenMM/openmmmolecule.cpp b/wrapper/Convert/SireOpenMM/openmmmolecule.cpp index 40979610d..91dbf4b0d 100644 --- a/wrapper/Convert/SireOpenMM/openmmmolecule.cpp +++ b/wrapper/Convert/SireOpenMM/openmmmolecule.cpp @@ -51,11 +51,15 @@ OpenMMMolecule::OpenMMMolecule() } OpenMMMolecule::OpenMMMolecule(const Molecule &mol, + int start_atom_idx, const PropertyMap &map) { molinfo = mol.info(); number = mol.number(); + // store the starting index of the atoms in the OpenMM system + this->start_atom_idx = start_atom_idx; + if (molinfo.nAtoms() == 0) { // nothing to extract @@ -480,6 +484,20 @@ void OpenMMMolecule::constructFromAmber(const Molecule &mol, atoms = mol.atoms(); const int nats = atoms.count(); + // Create the REST2 region mask. + if (mol.hasProperty("is_rest2")) + { + const auto &is_rest2_prop = moldata.property("is_rest2").asA(); + for (int i = 0; i < nats; ++i) + { + is_rest2.append(is_rest2_prop.at(i) == 1); + } + } + else + { + is_rest2 = QVector(nats, true); + } + if (nats <= 0) { return; @@ -1057,6 +1075,7 @@ void OpenMMMolecule::constructFromAmber(const Molecule &mol, const double dihedral_k_to_openmm = (SireUnits::kcal_per_mol).to(SireUnits::kJ_per_mol); dih_params.clear(); + is_improper.clear(); for (auto it = params.dihedrals().constBegin(); it != params.dihedrals().constEnd(); @@ -1087,6 +1106,7 @@ void OpenMMMolecule::constructFromAmber(const Molecule &mol, dih_params.append(boost::make_tuple(atom0, atom1, atom2, atom3, periodicity, phase, v)); + is_improper.append(false); } else if (periodicity == 0 and v == 0) { @@ -1095,6 +1115,7 @@ void OpenMMMolecule::constructFromAmber(const Molecule &mol, dih_params.append(boost::make_tuple(atom0, atom1, atom2, atom3, 1, phase, v)); + is_improper.append(false); } else { @@ -1127,6 +1148,7 @@ void OpenMMMolecule::constructFromAmber(const Molecule &mol, dih_params.append(boost::make_tuple(atom0, atom1, atom2, atom3, periodicity, phase, v)); + is_improper.append(true); } else if (periodicity == 0 and v == 0) { @@ -1135,6 +1157,7 @@ void OpenMMMolecule::constructFromAmber(const Molecule &mol, dih_params.append(boost::make_tuple(atom0, atom1, atom2, atom3, 1, phase, v)); + is_improper.append(true); } else { @@ -1369,7 +1392,9 @@ void OpenMMMolecule::alignInternals(const PropertyMap &map) perturbed->ang_params = ang_params_1; QVector> dih_params_1; + QVector is_improper_1; dih_params_1.reserve(dih_params.count()); + is_improper_1.reserve(dih_params.count()); found_index_0 = QVector(dih_params.count(), false); found_index_1 = QVector(perturbed->dih_params.count(), false); @@ -1377,6 +1402,7 @@ void OpenMMMolecule::alignInternals(const PropertyMap &map) for (int i = 0; i < dih_params.count(); ++i) { const auto &dih0 = dih_params.at(i); + const bool is_improper0 = is_improper.at(i); int atom0 = boost::get<0>(dih0); int atom1 = boost::get<1>(dih0); @@ -1390,6 +1416,7 @@ void OpenMMMolecule::alignInternals(const PropertyMap &map) if (not found_index_1[j]) { const auto &dih1 = perturbed->dih_params.at(j); + const bool is_improper1 = perturbed->is_improper.at(j); // we need to match all of the atoms AND the periodicity if (boost::get<0>(dih1) == atom0 and boost::get<1>(dih1) == atom1 and @@ -1398,6 +1425,7 @@ void OpenMMMolecule::alignInternals(const PropertyMap &map) { // we have found the matching torsion! dih_params_1.append(dih1); + is_improper_1.append(is_improper1); found_index_0[i] = true; found_index_1[j] = true; found = true; @@ -1410,6 +1438,7 @@ void OpenMMMolecule::alignInternals(const PropertyMap &map) { // add a null dihedral with the same periodicity and phase, but null k dih_params_1.append(boost::tuple(atom0, atom1, atom2, atom3, boost::get<4>(dih0), boost::get<5>(dih0), 0.0)); + is_improper_1.append(is_improper0); found_index_0[i] = true; } } @@ -1420,6 +1449,7 @@ void OpenMMMolecule::alignInternals(const PropertyMap &map) { // need to add a dihedral missing in the reference state const auto &dih1 = perturbed->dih_params.at(j); + const auto is_improper1 = perturbed->is_improper.at(j); int atom0 = boost::get<0>(dih1); int atom1 = boost::get<1>(dih1); @@ -1428,7 +1458,9 @@ void OpenMMMolecule::alignInternals(const PropertyMap &map) // add a null dihedral with the same periodicity and phase, but null k dih_params.append(boost::tuple(atom0, atom1, atom2, atom3, boost::get<4>(dih1), boost::get<5>(dih1), 0.0)); + is_improper.append(is_improper1); dih_params_1.append(dih1); + is_improper_1.append(is_improper1); found_index_1[j] = true; } } @@ -1442,6 +1474,7 @@ void OpenMMMolecule::alignInternals(const PropertyMap &map) } perturbed->dih_params = dih_params_1; + perturbed->is_improper = is_improper_1; // now align all of the exceptions - this should allow the bonding // to change during the perturbation @@ -2089,10 +2122,11 @@ PerturbableOpenMMMolecule::PerturbableOpenMMMolecule() /** Construct from a passed molecule and map */ PerturbableOpenMMMolecule::PerturbableOpenMMMolecule(const Molecule &mol, + int start_atom_idx, const PropertyMap &map) : ConcreteProperty() { - this->operator=(PerturbableOpenMMMolecule(OpenMMMolecule(mol, map), map)); + this->operator=(PerturbableOpenMMMolecule(OpenMMMolecule(mol, start_atom_idx, map), map)); } /** Return whether or not this is null */ @@ -2116,6 +2150,9 @@ PerturbableOpenMMMolecule::PerturbableOpenMMMolecule(const OpenMMMolecule &mol, auto molecule = mol.atoms.molecule(); + // store the index of the first atom in the OpenMM system + this->start_atom_idx = mol.start_atom_idx; + for (int i = 0; i < mol.atoms.count(); ++i) { perturbed_atoms.append(mol.atoms(i)); @@ -2196,6 +2233,8 @@ PerturbableOpenMMMolecule::PerturbableOpenMMMolecule(const OpenMMMolecule &mol, perturbable_constraints = mol.perturbable_constraints; + is_improper = mol.is_improper; + bool fix_perturbable_zero_sigmas = false; if (map.specified("fix_perturbable_zero_sigmas")) @@ -2261,7 +2300,8 @@ PerturbableOpenMMMolecule::PerturbableOpenMMMolecule(const PerturbableOpenMMMole to_ghost_idxs(other.to_ghost_idxs), from_ghost_idxs(other.from_ghost_idxs), exception_atoms(other.exception_atoms), exception_idxs(other.exception_idxs), perturbable_constraints(other.perturbable_constraints), - constraint_idxs(other.constraint_idxs) + constraint_idxs(other.constraint_idxs), + start_atom_idx(other.start_atom_idx) { } @@ -2705,3 +2745,21 @@ QList PerturbableOpenMMMolecule::torsions() const { return perturbed_dihs; } + +/** Return the improper flag vector for the perturbed dihedrals */ +QVector PerturbableOpenMMMolecule::getIsImproper() const +{ + return is_improper; +} + +/** Return the index of the first atom in the Sire system */ +int PerturbableOpenMMMolecule::getStartAtomIdx() const +{ + return start_atom_idx; +} + +/** Whether the atom is in the REST2 region. */ +bool PerturbableOpenMMMolecule::isRest2(int atom) const +{ + return is_rest2[atom]; +} diff --git a/wrapper/Convert/SireOpenMM/openmmmolecule.h b/wrapper/Convert/SireOpenMM/openmmmolecule.h index f21c44746..a903048cb 100644 --- a/wrapper/Convert/SireOpenMM/openmmmolecule.h +++ b/wrapper/Convert/SireOpenMM/openmmmolecule.h @@ -44,6 +44,7 @@ namespace SireOpenMM OpenMMMolecule(); OpenMMMolecule(const SireMol::Molecule &mol, + int start_atom_idx, const SireBase::PropertyMap &map); ~OpenMMMolecule(); @@ -183,6 +184,15 @@ namespace SireOpenMM * perturbable atoms */ qint32 perturbable_constraint_type; + /** Whether each dihedral is an improper */ + QVector is_improper; + + /** Whether each atom is within the REST2 region */ + QVector is_rest2; + + /** The starting index of the first OpenMM atom in the original Sire system. */ + int start_atom_idx; + private: void constructFromAmber(const SireMol::Molecule &mol, const SireMM::AmberParams ¶ms, @@ -211,6 +221,7 @@ namespace SireOpenMM const SireBase::PropertyMap &map = SireBase::PropertyMap()); PerturbableOpenMMMolecule(const SireMol::Molecule &mol, + int start_atom_idx, const SireBase::PropertyMap &map = SireBase::PropertyMap()); PerturbableOpenMMMolecule(const PerturbableOpenMMMolecule &other); @@ -271,6 +282,9 @@ namespace SireOpenMM QSet getToGhostIdxs() const; QSet getFromGhostIdxs() const; + int getStartAtomIdx() const; + bool isRest2(int atom) const; + bool isGhostAtom(int atom) const; QVector> getExceptionAtoms() const; @@ -293,6 +307,8 @@ namespace SireOpenMM QList angles() const; QList torsions() const; + QVector getIsImproper() const; + private: /** The atoms that are perturbed, in the order they appear * in the arrays below @@ -358,8 +374,16 @@ namespace SireOpenMM * to the number of perturbable constraints in the molecule */ QVector constraint_idxs; - }; + /** Whether each dihedral is an improper */ + QVector is_improper; + + /** Whether each atom is within the REST2 region */ + QVector is_rest2; + + /** The starting index of the first OpenMM atom in the original Sire system. */ + int start_atom_idx; + }; } Q_DECLARE_METATYPE(SireOpenMM::PerturbableOpenMMMolecule) diff --git a/wrapper/Convert/SireOpenMM/sire_to_openmm_system.cpp b/wrapper/Convert/SireOpenMM/sire_to_openmm_system.cpp index e3c46a03e..61c50312c 100644 --- a/wrapper/Convert/SireOpenMM/sire_to_openmm_system.cpp +++ b/wrapper/Convert/SireOpenMM/sire_to_openmm_system.cpp @@ -665,20 +665,28 @@ OpenMMMetaData SireOpenMM::sire_to_openmm_system(OpenMM::System &system, QVector openmm_mols(nmols); auto openmm_mols_data = openmm_mols.data(); + // Create a vector containing the start index for the atoms in each molecule. + QVector start_atom_index(nmols); + start_atom_index[0] = 0; + for (int i = 1; i < nmols; ++i) + { + start_atom_index[i] = start_atom_index[i-1] + mols[i-1].nAtoms(); + } + if (SireBase::should_run_in_parallel(nmols, map)) { tbb::parallel_for(tbb::blocked_range(0, mols.count()), [&](const tbb::blocked_range &r) { for (int i=r.begin(); i Date: Mon, 25 Nov 2024 12:43:53 +0000 Subject: [PATCH 030/102] Allow per-lambda REST2 scaling factors. --- src/sire/mol/__init__.py | 8 +- src/sire/mol/_dynamics.py | 106 ++++++++++++++++++--- src/sire/mol/_minimisation.py | 2 + src/sire/system/_system.py | 8 +- wrapper/Convert/SireOpenMM/_sommcontext.py | 50 +++++++--- wrapper/Convert/SireOpenMM/lambdalever.cpp | 5 - 6 files changed, 137 insertions(+), 42 deletions(-) diff --git a/src/sire/mol/__init__.py b/src/sire/mol/__init__.py index c652d1fba..f3a7ded7d 100644 --- a/src/sire/mol/__init__.py +++ b/src/sire/mol/__init__.py @@ -1691,12 +1691,8 @@ def _dynamics( rest2_scale: float The scaling factor to apply when running a REST2 simulation (Replica Exchange with Solute Tempering). This defaults to 1.0. - This specifies the ratio of the temperature of the solute to - the temperature of the solvent. The scaling factor is linearly - interpolated between the two temperatures so that the solute has - a temperature of rest2_scale * temperature in the intermediate, - lambda = 0.5 state, and the end states are at the original - temperature. + This specifies the ratio of the temperature of the REST2 region + to the temperature of the rest of the system. rest2_selection: str A selection string for atoms to include in the REST2 region diff --git a/src/sire/mol/_dynamics.py b/src/sire/mol/_dynamics.py index 4bedc84c5..bf9fbeee6 100644 --- a/src/sire/mol/_dynamics.py +++ b/src/sire/mol/_dynamics.py @@ -329,6 +329,7 @@ def _exit_dynamics_block( save_frame: bool = False, save_energy: bool = False, lambda_windows=[], + rest2_scale_factors=[], save_velocities: bool = False, delta_lambda: float = None, ): @@ -379,6 +380,7 @@ def _exit_dynamics_block( ) sim_lambda_value = self._omm_mols.get_lambda() + sim_rest2_scale = self._omm_mols.get_rest2_scale() # Store the potential energy and accumulated non-equilibrium work. if self._is_interpolate: @@ -392,10 +394,14 @@ def _exit_dynamics_block( nrgs[str(sim_lambda_value)] = nrgs["potential"] if lambda_windows is not None: - for lambda_value in lambda_windows: + for lambda_value, rest2_scale in zip( + lambda_windows, rest2_scale_factors + ): if lambda_value != sim_lambda_value: self._omm_mols.set_lambda( - lambda_value, update_constraints=False + lambda_value, + rest2_scale=rest2_scale, + update_constraints=False, ) nrgs[str(lambda_value)] = ( self._omm_mols.get_potential_energy( @@ -598,7 +604,10 @@ def set_schedule(self, schedule): if not self.is_null(): self._omm_mols.set_lambda_schedule(schedule) self._schedule_changed = True - self.set_lambda(self._omm_mols.get_lambda()) + self.set_lambda( + self._omm_mols.get_lambda(), + rest2_scale=self._omm_mols.get_rest2_scale(), + ) def get_lambda(self): if self.is_null(): @@ -606,7 +615,12 @@ def get_lambda(self): else: return self._omm_mols.get_lambda() - def set_lambda(self, lambda_value: float, update_constraints: bool = True): + def set_lambda( + self, + lambda_value: float, + rest2_scale: float = 1.0, + update_constraints: bool = True, + ): if not self.is_null(): s = self.get_schedule() @@ -622,7 +636,9 @@ def set_lambda(self, lambda_value: float, update_constraints: bool = True): return self._omm_mols.set_lambda( - lambda_value=lambda_value, update_constraints=update_constraints + lambda_value=lambda_value, + rest2_scale=rest2_scale, + update_constraints=update_constraints, ) self._schedule_changed = False self._clear_state() @@ -854,6 +870,7 @@ def run( frame_frequency=None, energy_frequency=None, lambda_windows=None, + rest2_scale_factors=None, save_velocities: bool = None, auto_fix_minimise: bool = True, ): @@ -866,6 +883,7 @@ def run( "frame_frequency": frame_frequency, "energy_frequency": energy_frequency, "lambda_windows": lambda_windows, + "rest2_scale_factors": rest2_scale_factors, "save_velocities": save_velocities, "auto_fix_minimise": auto_fix_minimise, } @@ -950,6 +968,9 @@ def run( # Fixed lambda value. else: delta_lambda = None + + # Set the REST2 scaling factors. + rest2_scale_factors = [1.0, 1.0] else: delta_lambda = None if lambda_windows is not None: @@ -959,6 +980,14 @@ def run( if self._map.specified("lambda_windows"): lambda_windows = self._map["lambda_windows"].value() + if rest2_scale_factors is not None: + if len(rest2_scale_factors) != len(lambda_windows): + raise ValueError( + "len(rest2_scale_factors) must be equal to len(lambda_windows)" + ) + else: + rest2_scale_factors = [1.0] * len(lambda_windows) + def runfunc(num_steps): try: integrator = self._omm_mols.getIntegrator() @@ -1113,6 +1142,7 @@ class NeedsMinimiseError(Exception): save_frame=save_frame, save_energy=save_energy, lambda_windows=lambda_windows, + rest2_scale_factors=rest2_scale_factors, save_velocities=save_velocities, delta_lambda=delta_lambda, ) @@ -1382,6 +1412,7 @@ def run( frame_frequency=None, energy_frequency=None, lambda_windows=None, + rest2_scale_factors=None, save_velocities: bool = None, auto_fix_minimise: bool = True, ): @@ -1440,6 +1471,10 @@ def run( we always save the potential energy of the simulated lambda value, even if it is not in the list of lambda windows. + rest2_scale_factors: list[float] + The scaling factors for the REST2 region for each lambda + window. + save_velocities: bool Whether or not to save the velocities when running dynamics. By default this is False. Set this to True if you are @@ -1465,6 +1500,7 @@ def run( frame_frequency=frame_frequency, energy_frequency=energy_frequency, lambda_windows=lambda_windows, + rest2_scale_factors=rest2_scale_factors, save_velocities=save_velocities, auto_fix_minimise=auto_fix_minimise, ) @@ -1495,13 +1531,22 @@ def get_lambda(self): """ return self._d.get_lambda() - def set_lambda(self, lambda_value: float, update_constraints: bool = True): + def set_lambda( + self, + lambda_value: float, + rest2_scale: float = 1.0, + update_constraints: bool = True, + ): """ Set the current value of lambda for this system. This will update the forcefield parameters in the context according to the data in the LambdaSchedule. This does nothing if this isn't a perturbable system. + The `rest2_scale` parameter specifies the temperature of the + REST2 region relative to the rest of the system at the specified + lambda value. + If `update_constraints` is True, then this will also update the constraint length of any constrained perturbable bonds. These will be set to the r0 value for that bond at this @@ -1509,9 +1554,25 @@ def set_lambda(self, lambda_value: float, update_constraints: bool = True): the constraint will not be changed. """ self._d.set_lambda( - lambda_value=lambda_value, update_constraints=update_constraints + lambda_value=lambda_value, + rest2_scale=rest2_scale, + update_constraints=update_constraints, ) + def get_rest2_scale(self): + """ + Return the current REST2 scaling factor. + """ + if self.is_null(): + return None + return self._d.get_rest2_scale() + + def set_rest2_scale(self, rest2_scale: float): + """ + Set the current REST2 scaling factor. + """ + self._d.set_rest2_scale(rest2_scale=rest2_scale) + def ensemble(self): """ Return the ensemble in which the simulation is being performed @@ -1697,35 +1758,56 @@ def current_energy(self): """ return self._d.current_energy() - def current_potential_energy(self, lambda_values=None): + def current_potential_energy(self, lambda_values=None, rest2_scale_factors=None): """ Return the current potential energy. If `lambda_values` is passed (which should be a list of lambda values) then this will return the energies (as a list) at the requested lambda values + + If `rest2_scale_factors` is passed, then these will be + used to scale the temperature of the REST2 region at each + lambda value. """ if lambda_values is None: return self._d.current_potential_energy() else: if not isinstance(lambda_values, list): lambda_values = [lambda_values] + if rest2_scale_factors is None: + rest2_scale_factors = [1.0] * len(lambda_values) + else: + if not isinstance(rest2_scale_factors, list): + rest2_scale_factors = [rest2_scale_factors] + else: + if len(rest2_scale_factors) != len(lambda_values): + raise ValueError( + "len(rest2_scale_factors) must be equal to len(lambda_values)" + ) # save the current value of lambda so we # can restore it old_lambda = self.get_lambda() + old_rest2_scale = self.get_rest2_scale() nrgs = [] try: - for lambda_value in lambda_values: - self.set_lambda(lambda_value, update_constraints=False) + for lambda_value, rest2_scale in zip( + lambda_values, rest2_scale_factors + ): + self.set_lambda( + lambda_value, rest2_scale=rest2_scale, update_constraints=False + ) nrgs.append(self._d.current_potential_energy()) except Exception: - self.set_lambda(old_lambda, update_constraints=False) + self.set_lambda(old_lambda, old_rest2_scale, update_constraints=False) raise - self.set_lambda(old_lambda, update_constraints=False) + self.set_lambda( + old_lambda, rest2_scale=old_rest2_scale, update_constraints=False + ) return nrgs diff --git a/src/sire/mol/_minimisation.py b/src/sire/mol/_minimisation.py index 32e3dc8f9..b6c89b9fe 100644 --- a/src/sire/mol/_minimisation.py +++ b/src/sire/mol/_minimisation.py @@ -17,6 +17,7 @@ def __init__( cutoff_type=None, schedule=None, lambda_value=None, + rest2_scale=None, swap_end_states=None, ignore_perturbations=None, shift_delta=None, @@ -37,6 +38,7 @@ def __init__( _add_extra(extras, "lambda", lambda_value) _add_extra(extras, "swap_end_states", swap_end_states) _add_extra(extras, "ignore_perturbations", ignore_perturbations) + _add_extra(extras, "rest2_scale", rest2_scale) if shift_delta is not None: _add_extra(extras, "shift_delta", u(shift_delta)) diff --git a/src/sire/system/_system.py b/src/sire/system/_system.py index 9b9f3f2bb..0846f535b 100644 --- a/src/sire/system/_system.py +++ b/src/sire/system/_system.py @@ -595,12 +595,8 @@ def dynamics(self, *args, **kwargs): rest2_scale: float The scaling factor to apply when running a REST2 simulation (Replica Exchange with Solute Tempering). This defaults to 1.0. - This specifies the ratio of the temperature of the solute to - the temperature of the solvent. The scaling factor is linearly - interpolated between the two temperatures so that the solute has - a temperature of rest2_scale * temperature in the intermediate, - lambda = 0.5 state, and the end states are at the original - temperature. + This specifies the ratio of the temperature of the REST2 region + to the temperature of the rest of the system. rest2_selection: str A selection string for atoms to include in the REST2 region diff --git a/wrapper/Convert/SireOpenMM/_sommcontext.py b/wrapper/Convert/SireOpenMM/_sommcontext.py index f6d3a7f7a..05c539dcb 100644 --- a/wrapper/Convert/SireOpenMM/_sommcontext.py +++ b/wrapper/Convert/SireOpenMM/_sommcontext.py @@ -244,25 +244,56 @@ def set_lambda_schedule(self, schedule): self._lambda_lever.set_schedule(schedule) - def set_lambda(self, lambda_value: float, update_constraints: bool = True): + def set_lambda( + self, + lambda_value: float, + rest2_scale: float = None, + update_constraints: bool = True, + ): """ Update the parameters in the context to set the lambda value - to 'lambda_value'. If update_constraints is True then the constraints - will be updated to match the new value of lambda. + to 'lambda_value'. The 'rest2_scale' defines the temperature of + the REST2 region relative to the rest of the system. If + 'update_constraints' is True then the constraints will be updated + to match the new value of lambda. """ if self._lambda_lever is None: return + # If not provided, use the REST2 scaling factor used to initalise + # the context. + if rest2_scale is None: + rest2_scale = self._rest2_scale + self._lambda_value = self._lambda_lever.set_lambda( self, lambda_value=lambda_value, - rest2_scale=self._rest2_scale, + rest2_scale=rest2_scale, update_constraints=update_constraints, ) # Update any additional parameters in the REST2 region. if self._has_rest2_selection: - self._update_rest2(lambda_value, self._rest2_scale) + self._update_rest2(lambda_value, rest2_scale) + + def get_rest2_scale(self): + """ + Return the temperature scale factor for the REST2 region. + """ + return self._rest2_scale + + def set_rest2_scale(self, rest2_scale): + """ + Set the temperature scale factor for the REST2 region. + """ + if rest2_scale < 1.0: + raise ValueError("'rest2_scale' must be >= 1.0") + + if self._rest2_scale != rest2_scale: + self._set_lambda(self._lambda_value, rest2_scale=rest2_scale) + self._update_rest2(self._lambda_value, rest2_scale) + + self._rest2_scale = rest2_scale def set_temperature(self, temperature, rescale_velocities=True): """ @@ -445,18 +476,11 @@ def _update_rest2(self, lambda_value, rest2_scale): Internal method to update the REST2 parameters. """ - # Adapted from code in meld: https://github.com/maccallumlab/meld - from math import sqrt - # Work out the actual REST scaling factor. The passed value is the ratio of - # the REST temperature to the simulation temperature. The full scaling factor - # is used at lambda = 0.5 and is scaled to 1.0 at lambda = 0 and lambda = 1. - scale = 1.0 + (rest2_scale - 1.0) * (1.0 - 2.0 * abs(lambda_value - 0.5)) - # This is the temperature scale factor, so we need to invert to get the energy # scale factor. - scale = 1.0 / scale + scale = 1.0 / rest2_scale # Store the REST2 charge scaling factor for non-bonded interactions. sqrt_scale = sqrt(scale) diff --git a/wrapper/Convert/SireOpenMM/lambdalever.cpp b/wrapper/Convert/SireOpenMM/lambdalever.cpp index 859ae9ff6..0832d5572 100644 --- a/wrapper/Convert/SireOpenMM/lambdalever.cpp +++ b/wrapper/Convert/SireOpenMM/lambdalever.cpp @@ -1136,11 +1136,6 @@ double LambdaLever::setLambda(OpenMM::Context &context, lambda_value = this->lambda_schedule.clamp(lambda_value); - // Work out the actual REST scaling factor. The passed value is the ratio of - // the REST temperature to the simulation temperature. The full scaling factor - // is used at lambda = 0.5 and is scaled to 1.0 at lambda = 0 and lambda = 1. - rest2_scale = 1.0 + (rest2_scale - 1.0)*(1.0 - 2.0 * std::abs(lambda_value - 0.5)); - // This is the temperature scale factor, so we need to invert to get the energy // scale factor. rest2_scale = 1.0 / rest2_scale; From 9a3d8a5db5391336a22d7bf65d0f687b3525afad Mon Sep 17 00:00:00 2001 From: Lester Hedges Date: Tue, 26 Nov 2024 13:46:04 +0000 Subject: [PATCH 031/102] Fix update of PeriodicTorsionForce parameters. --- wrapper/Convert/SireOpenMM/_sommcontext.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/wrapper/Convert/SireOpenMM/_sommcontext.py b/wrapper/Convert/SireOpenMM/_sommcontext.py index 05c539dcb..8b8de86c2 100644 --- a/wrapper/Convert/SireOpenMM/_sommcontext.py +++ b/wrapper/Convert/SireOpenMM/_sommcontext.py @@ -504,9 +504,9 @@ def _update_rest2(self, lambda_value, rest2_scale): # Update the torsion parameters. for index, params in self._torsion_params.items(): - i, j, k, l, periodicity, phase, k = params + i, j, k, l, periodicity, phase, fc = params self._periodic_torsion_force.setTorsionParameters( - index, i, j, k, l, periodicity, phase, k * scale + index, i, j, k, l, periodicity, phase, fc * scale ) # Update the parameters in the context. From 67947e6e87c4ae5ad829286ad56ea73d0ad4b509 Mon Sep 17 00:00:00 2001 From: Lester Hedges Date: Tue, 26 Nov 2024 14:22:10 +0000 Subject: [PATCH 032/102] Fix calculation of molecule atom offset. --- wrapper/Convert/SireOpenMM/_sommcontext.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/wrapper/Convert/SireOpenMM/_sommcontext.py b/wrapper/Convert/SireOpenMM/_sommcontext.py index 8b8de86c2..afec9b6aa 100644 --- a/wrapper/Convert/SireOpenMM/_sommcontext.py +++ b/wrapper/Convert/SireOpenMM/_sommcontext.py @@ -419,6 +419,9 @@ def _prepare_rest2(self, system, atoms): self._exception_params = {} self._torsion_params = {} + # Store the molecules in the system. + system_mols = system.molecules() + # Process each of the molecules. for mol in mols: # Create the connectivity object for the molecule. @@ -434,8 +437,8 @@ def _prepare_rest2(self, system, atoms): # Work out the offset to apply to the atom indices to convert to system indices. num_atoms = 0 - for mol in system.molecules()[:mol_idx]: - num_atoms += mol.num_atoms() + for i in range(mol_idx): + num_atoms += system_mols[i].num_atoms() # Create a list of atom indices. atom_idxs = [atom.index().value() + num_atoms for atom in atoms] From e61b39d6e663c2c9d65a7fd3429d3208b42c23b9 Mon Sep 17 00:00:00 2001 From: Lester Hedges Date: Thu, 28 Nov 2024 10:05:58 +0000 Subject: [PATCH 033/102] Add missing scale factor in exception. --- wrapper/Convert/SireOpenMM/lambdalever.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/wrapper/Convert/SireOpenMM/lambdalever.cpp b/wrapper/Convert/SireOpenMM/lambdalever.cpp index 0832d5572..8df8b94db 100644 --- a/wrapper/Convert/SireOpenMM/lambdalever.cpp +++ b/wrapper/Convert/SireOpenMM/lambdalever.cpp @@ -1505,7 +1505,7 @@ double LambdaLever::setLambda(OpenMM::Context &context, cljff->setExceptionParameters( boost::get<0>(idxs[j]), boost::get<0>(p), boost::get<1>(p), - boost::get<2>(p), 1e-9, 1e-9); + boost::get<2>(p) * scale, 1e-9, 1e-9); // exclude 14s for to/from ghost interactions if (not to_from_ghost and ghost_14ff != 0) From 1bc4e46d6e552f77f2641269a6e7e8402535484a Mon Sep 17 00:00:00 2001 From: Lester Hedges Date: Fri, 29 Nov 2024 11:19:40 +0000 Subject: [PATCH 034/102] Workaround for device change during minimisation. [ref #262] --- wrapper/Convert/SireOpenMM/torchqm.cpp | 14 +++++++++++--- wrapper/Convert/SireOpenMM/torchqm.h | 1 + 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/wrapper/Convert/SireOpenMM/torchqm.cpp b/wrapper/Convert/SireOpenMM/torchqm.cpp index 1332c91c7..c75456c75 100644 --- a/wrapper/Convert/SireOpenMM/torchqm.cpp +++ b/wrapper/Convert/SireOpenMM/torchqm.cpp @@ -391,12 +391,20 @@ double TorchQMForceImpl::computeForce( 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. Annoyingly, we have to + // re-load the module if the device has changed. This is because it + // appears that the overloaded .to() method isn't called via C++. + if (device != this->device) { - // Move the Torch module to the correct device. + this->torch_module = torch::jit::load(this->getOwner().getModulePath().toStdString()); + this->torch_module.eval(); this->torch_module.to(device); + this->device = device; + } + // 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(); diff --git a/wrapper/Convert/SireOpenMM/torchqm.h b/wrapper/Convert/SireOpenMM/torchqm.h index 839ecf071..97969d75c 100644 --- a/wrapper/Convert/SireOpenMM/torchqm.h +++ b/wrapper/Convert/SireOpenMM/torchqm.h @@ -296,6 +296,7 @@ namespace SireOpenMM double neighbour_list_cutoff; QSet neighbour_list; int max_num_mm=0; + c10::DeviceType device; }; #endif From 3e866dee1d3967c7e2b7bceaa843235c8e9ccebc Mon Sep 17 00:00:00 2001 From: Lester Hedges Date: Fri, 29 Nov 2024 11:22:45 +0000 Subject: [PATCH 035/102] Add minimisation step to QM/MM tutorials. [ref #262] --- doc/source/tutorial/part08/01_intro.rst | 8 ++++++-- doc/source/tutorial/part08/02_emle.rst | 4 ++++ 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/doc/source/tutorial/part08/01_intro.rst b/doc/source/tutorial/part08/01_intro.rst index 166b27e17..632cbd442 100644 --- a/doc/source/tutorial/part08/01_intro.rst +++ b/doc/source/tutorial/part08/01_intro.rst @@ -112,8 +112,12 @@ QM engine when creating a dynamics object, for example: ... 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: +Next we will minimise the system ready for simulation: + +>>> d.minimise() + +When runinng 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") diff --git a/doc/source/tutorial/part08/02_emle.rst b/doc/source/tutorial/part08/02_emle.rst index 84305c4c5..be1442b37 100644 --- a/doc/source/tutorial/part08/02_emle.rst +++ b/doc/source/tutorial/part08/02_emle.rst @@ -86,6 +86,10 @@ energy calculations. ... platform="cpu", ... ) +Next we will minimise the system ready for simulation: + +>>> d.minimise() + 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 From c064358843a8d38a1be788c3ff1e6a70d03d30a5 Mon Sep 17 00:00:00 2001 From: Lester Hedges Date: Fri, 29 Nov 2024 11:43:24 +0000 Subject: [PATCH 036/102] Fix QM-to-MM non-equilibrium work. [ref #262] --- src/sire/mol/_dynamics.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/sire/mol/_dynamics.py b/src/sire/mol/_dynamics.py index 40c8b125a..fe6eb7d2d 100644 --- a/src/sire/mol/_dynamics.py +++ b/src/sire/mol/_dynamics.py @@ -328,7 +328,10 @@ def _exit_dynamics_block( if self._is_interpolate: nrg = nrgs["potential"] - if sim_lambda_value != 0.0: + if ( + len(self._lambda_interpolate) == 2 + and sim_lambda_value != self._lambda_interpolate[0] + ): self._work += delta_lambda * (nrg - self._nrg_prev) self._nrg_prev = nrg nrgs["work"] = self._work From 217906a09fd85319f5273af285bfce0a67dc0969 Mon Sep 17 00:00:00 2001 From: Lester Hedges Date: Mon, 2 Dec 2024 09:07:57 +0000 Subject: [PATCH 037/102] Update CHANGELOG. [ci skip] --- doc/source/changelog.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/doc/source/changelog.rst b/doc/source/changelog.rst index 3dd88e2b1..a23999dbb 100644 --- a/doc/source/changelog.rst +++ b/doc/source/changelog.rst @@ -21,6 +21,8 @@ organisation on `GitHub `__. * Expose missing ``timeout`` kwarg in :meth:`dynamics.minimise()` method. * Expose missing ``include_constrained_energies`` kwarg in minimisation function. * Make minimisation function settings consistent across API. +* Reload TorchQMForce module if OpenMM platform changes. +* Fix calculation of non-equilibrium work when starting from QM state. `2024.3.0 `__ - October 2024 -------------------------------------------------------------------------------------------- From 9c3aa12407b115355d6f231febb9935f6a54ae4c Mon Sep 17 00:00:00 2001 From: Lester Hedges Date: Mon, 2 Dec 2024 09:59:47 +0000 Subject: [PATCH 038/102] Fix setting of RDKit bond orders. [ref #261] --- wrapper/Convert/SireRDKit/sire_rdkit.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/wrapper/Convert/SireRDKit/sire_rdkit.cpp b/wrapper/Convert/SireRDKit/sire_rdkit.cpp index f5574cdc4..ab52f7452 100644 --- a/wrapper/Convert/SireRDKit/sire_rdkit.cpp +++ b/wrapper/Convert/SireRDKit/sire_rdkit.cpp @@ -711,7 +711,7 @@ namespace SireRDKit bondtype = string_to_bondtype(bond.property(map["order"]).asA().toRDKit()); // one bond has bond info, so assume that all do - has_bond_info = false; + has_bond_info = true; } catch (...) { From 41d1f07817f978310b191002ae7021c8ef461bd0 Mon Sep 17 00:00:00 2001 From: Lester Hedges Date: Mon, 2 Dec 2024 10:30:47 +0000 Subject: [PATCH 039/102] Set bond stereo information. [ref #261] --- wrapper/Convert/SireRDKit/sire_rdkit.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/wrapper/Convert/SireRDKit/sire_rdkit.cpp b/wrapper/Convert/SireRDKit/sire_rdkit.cpp index ab52f7452..412b45bbe 100644 --- a/wrapper/Convert/SireRDKit/sire_rdkit.cpp +++ b/wrapper/Convert/SireRDKit/sire_rdkit.cpp @@ -748,6 +748,7 @@ namespace SireRDKit molecule.addBond(bond.atom0().index().value(), bond.atom1().index().value(), bondtype); + molecule.getBondWithIdx(i)->setStereo(stereo); } if (has_coords) From b4a1c3b0a462e34e9d557fd7d1f5bd0379cf5802 Mon Sep 17 00:00:00 2001 From: Lester Hedges Date: Mon, 2 Dec 2024 15:01:22 +0000 Subject: [PATCH 040/102] Allow implicit hydrogens. [ref #261] --- wrapper/Convert/SireRDKit/sire_rdkit.cpp | 3 --- 1 file changed, 3 deletions(-) diff --git a/wrapper/Convert/SireRDKit/sire_rdkit.cpp b/wrapper/Convert/SireRDKit/sire_rdkit.cpp index 412b45bbe..8270583fc 100644 --- a/wrapper/Convert/SireRDKit/sire_rdkit.cpp +++ b/wrapper/Convert/SireRDKit/sire_rdkit.cpp @@ -640,9 +640,6 @@ namespace SireRDKit a->setAtomicNum(element.nProtons()); - // don't automatically add hydrogens to this atom - a->setNoImplicit(true); - elements.append(element); try From 3720ab91b63fe30949e38a52e3ef5729f5ee37eb Mon Sep 17 00:00:00 2001 From: Lester Hedges Date: Mon, 2 Dec 2024 15:01:56 +0000 Subject: [PATCH 041/102] Assign sterochemistry from 3D coordinates. [ref #261] [ci skip] --- wrapper/Convert/SireRDKit/sire_rdkit.cpp | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/wrapper/Convert/SireRDKit/sire_rdkit.cpp b/wrapper/Convert/SireRDKit/sire_rdkit.cpp index 8270583fc..c5b971203 100644 --- a/wrapper/Convert/SireRDKit/sire_rdkit.cpp +++ b/wrapper/Convert/SireRDKit/sire_rdkit.cpp @@ -853,6 +853,19 @@ namespace SireRDKit { } + // try assigning stereochemistry from 3D coordinates as it is the most + // reliable way to do it + if (has_coords) + { + try + { + RDKit::MolOps::assignStereochemistryFrom3D(molecule); + } + catch (...) + { + } + } + return ROMOL_SPTR(new RDKit::ROMol(molecule)); } From 88646ff71f05de814886b3fec62436b636ab3e55 Mon Sep 17 00:00:00 2001 From: Lester Hedges Date: Mon, 2 Dec 2024 16:27:24 +0000 Subject: [PATCH 042/102] XFail test since SDF stereochemistry is now preserved. [ref #261] --- tests/convert/test_rdkit.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/tests/convert/test_rdkit.py b/tests/convert/test_rdkit.py index 780f4e3a8..304630f15 100644 --- a/tests/convert/test_rdkit.py +++ b/tests/convert/test_rdkit.py @@ -11,7 +11,7 @@ [ "C1CCCCC1", "C", - "OCC(O)C(O)C(O)C(O)CO", + "OC[C@H](O)[C@H](O)[C@@H](O)[C@@H](O)CO", "C[C@H](N)C(=O)O", # L-alanine "C[C@@H](N)C(=O)O", # D-alanine ], @@ -118,10 +118,13 @@ def test_rdkit_returns_null(): "rdkit" not in sr.convert.supported_formats(), reason="rdkit support is not available", ) +@pytest.mark.xfail(reason="SMILES now mismatches since SDF stereochemistry is preserved") def test_rdkit_infer_bonds(ejm55_sdf, ejm55_gro): sdf = ejm55_sdf[0].molecule() gro = ejm55_gro["not (protein or water)"].molecule() + from rdkit import Chem + assert sdf.smiles() == gro.smiles() match_sdf = sdf["smarts [NX3][CX3](=[OX1])[#6]"] From 918690d76adc19554ae124f1529a8fb043f12e1a Mon Sep 17 00:00:00 2001 From: Lester Hedges Date: Mon, 2 Dec 2024 16:29:08 +0000 Subject: [PATCH 043/102] Update CHANGELOG. --- doc/source/changelog.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/doc/source/changelog.rst b/doc/source/changelog.rst index a23999dbb..2e256e704 100644 --- a/doc/source/changelog.rst +++ b/doc/source/changelog.rst @@ -23,6 +23,7 @@ organisation on `GitHub `__. * Make minimisation function settings consistent across API. * Reload TorchQMForce module if OpenMM platform changes. * Fix calculation of non-equilibrium work when starting from QM state. +* Fix stereochemistry in RDKit molecule conversion. `2024.3.0 `__ - October 2024 -------------------------------------------------------------------------------------------- From 1e253b1d79d604de141dc802bb40b00b2484ab7e Mon Sep 17 00:00:00 2001 From: Lester Hedges Date: Tue, 3 Dec 2024 09:18:31 +0000 Subject: [PATCH 044/102] Fix thread safety issue in OpenMM minimiser. [closes #259] --- doc/source/changelog.rst | 7 ++++--- wrapper/Convert/SireOpenMM/openmmminimise.cpp | 8 +++++--- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/doc/source/changelog.rst b/doc/source/changelog.rst index 9b07baad9..8b0262fe6 100644 --- a/doc/source/changelog.rst +++ b/doc/source/changelog.rst @@ -16,9 +16,6 @@ organisation on `GitHub `__. --------------------------------------------------------------------------------------------- * Please add an item to this CHANGELOG for any new features or bug fixes when creating a PR. -* Fixed :func:`sire.restraints.get_standard_state_correction` to be consistent with the definition of - the "force constanst" as ``F = 2 ** k ** x`` (rather than ``F = k ** x``). Updated docstrings and - restraints documentation to make it clear how the force constants are defined. * Fixed instantiaton of ``QByteArray`` in ``Sire::Mol::Frame::toByteArray`` and count bytes with ``QByteArray::size``. * Increase timeout before terminating ``QThread`` objects during ``PageCache`` cleanup. * Expose missing ``timeout`` kwarg in :meth:`dynamics.minimise()` method. @@ -27,6 +24,10 @@ organisation on `GitHub `__. * Reload TorchQMForce module if OpenMM platform changes. * Fix calculation of non-equilibrium work when starting from QM state. * Fix stereochemistry in RDKit molecule conversion. +* Fixed :func:`sire.restraints.get_standard_state_correction` to be consistent with the definition of + the "force constanst" as ``F = 2 ** k ** x`` (rather than ``F = k ** x``). Updated docstrings and + restraints documentation to make it clear how the force constants are defined. +* Fix thread safety issue in Sire OpenMM minimiser. `2024.3.0 `__ - October 2024 -------------------------------------------------------------------------------------------- diff --git a/wrapper/Convert/SireOpenMM/openmmminimise.cpp b/wrapper/Convert/SireOpenMM/openmmminimise.cpp index dd5a5f536..500401b4b 100644 --- a/wrapper/Convert/SireOpenMM/openmmminimise.cpp +++ b/wrapper/Convert/SireOpenMM/openmmminimise.cpp @@ -50,6 +50,7 @@ #include // CHAR_BIT #include #include // uint64_t +#include inline auto is_ieee754_nan(double const x) -> bool @@ -91,7 +92,6 @@ inline auto is_ieee754_nan(double const x) #include #include "SireError/errors.h" -#include "SireBase/releasegil.h" #include "SireBase/progressbar.h" #include "SireUnits/units.h" @@ -619,6 +619,8 @@ namespace SireOpenMM double starting_k, double ratchet_scale, double max_constraint_error, double timeout) { + PyThreadState *_save = PyEval_SaveThread(); + if (max_iterations < 0) { max_iterations = std::numeric_limits::max(); @@ -629,8 +631,6 @@ namespace SireOpenMM timeout = std::numeric_limits::max(); } - auto gil = SireBase::release_gil(); - const OpenMM::System &system = context.getSystem(); int num_particles = system.getNumParticles(); @@ -1105,6 +1105,8 @@ namespace SireOpenMM CODELOC); } + PyEval_RestoreThread(_save); + return data.getLog().join("\n"); } From 4fdbf8aba8863c74a8b9aafafb806972fba4d985 Mon Sep 17 00:00:00 2001 From: Lester Hedges Date: Tue, 3 Dec 2024 14:17:00 +0000 Subject: [PATCH 045/102] Update rest2_scale factor attribute when it changes. --- wrapper/Convert/SireOpenMM/_sommcontext.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/wrapper/Convert/SireOpenMM/_sommcontext.py b/wrapper/Convert/SireOpenMM/_sommcontext.py index afec9b6aa..0c5b6fa37 100644 --- a/wrapper/Convert/SireOpenMM/_sommcontext.py +++ b/wrapper/Convert/SireOpenMM/_sommcontext.py @@ -264,6 +264,8 @@ def set_lambda( # the context. if rest2_scale is None: rest2_scale = self._rest2_scale + else: + self._rest2_scale = rest2_scale self._lambda_value = self._lambda_lever.set_lambda( self, From bc287885a0f297a0a13e24bec01f77f707e89be8 Mon Sep 17 00:00:00 2001 From: Lester Hedges Date: Tue, 3 Dec 2024 14:17:53 +0000 Subject: [PATCH 046/102] Apply REST2 scale factor on instantiation. --- src/sire/mol/_dynamics.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/sire/mol/_dynamics.py b/src/sire/mol/_dynamics.py index bf9fbeee6..55ba8b8cc 100644 --- a/src/sire/mol/_dynamics.py +++ b/src/sire/mol/_dynamics.py @@ -235,6 +235,11 @@ def __init__(self, mols=None, map=None, **kwargs): from ..convert import to self._omm_mols = to(self._sire_mols, "openmm", map=self._map) + + # Reset the lambda value to the initial value so that any REST2 + # scaling can be applied. + self._omm_mols.set_lambda(self._omm_mols.get_lambda()) + self._clear_state() if self._ffinfo.space().is_periodic(): From 6cfa5c372e560e17096811b4c20f3e9f3757fc61 Mon Sep 17 00:00:00 2001 From: Lester Hedges Date: Tue, 3 Dec 2024 14:18:07 +0000 Subject: [PATCH 047/102] Udpate lambda if REST2 scale factor changes. --- src/sire/mol/_dynamics.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/sire/mol/_dynamics.py b/src/sire/mol/_dynamics.py index 55ba8b8cc..9cce83899 100644 --- a/src/sire/mol/_dynamics.py +++ b/src/sire/mol/_dynamics.py @@ -634,8 +634,10 @@ def set_lambda( lambda_value = s.clamp(lambda_value) - if (not self._schedule_changed) and ( - lambda_value == self._omm_mols.get_lambda() + if ( + (not self._schedule_changed) + and (lambda_value == self._omm_mols.get_lambda()) + and (rest2_scale == self._omm_mols.get_rest2_scale()) ): # nothing to do return From 0682d00a029e993d13a872ae588d7693fe7a5b30 Mon Sep 17 00:00:00 2001 From: Lester Hedges Date: Tue, 3 Dec 2024 14:31:02 +0000 Subject: [PATCH 048/102] Apply REST2 scaling factor in SOMMContext constructor. --- src/sire/mol/_dynamics.py | 5 ----- wrapper/Convert/SireOpenMM/_sommcontext.py | 1 + 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/src/sire/mol/_dynamics.py b/src/sire/mol/_dynamics.py index 9cce83899..91bc7efbd 100644 --- a/src/sire/mol/_dynamics.py +++ b/src/sire/mol/_dynamics.py @@ -235,11 +235,6 @@ def __init__(self, mols=None, map=None, **kwargs): from ..convert import to self._omm_mols = to(self._sire_mols, "openmm", map=self._map) - - # Reset the lambda value to the initial value so that any REST2 - # scaling can be applied. - self._omm_mols.set_lambda(self._omm_mols.get_lambda()) - self._clear_state() if self._ffinfo.space().is_periodic(): diff --git a/wrapper/Convert/SireOpenMM/_sommcontext.py b/wrapper/Convert/SireOpenMM/_sommcontext.py index 0c5b6fa37..edde66b2e 100644 --- a/wrapper/Convert/SireOpenMM/_sommcontext.py +++ b/wrapper/Convert/SireOpenMM/_sommcontext.py @@ -90,6 +90,7 @@ def __init__( self._lambda_value = self._lambda_lever.set_lambda( self, lambda_value=lambda_value, + rest2_scale=self._rest2_scale, update_constraints=True, ) From d1df185f83ca07ccc068dca62c84a6a5b8be5a78 Mon Sep 17 00:00:00 2001 From: Lester Hedges Date: Wed, 4 Dec 2024 08:35:06 +0000 Subject: [PATCH 049/102] Add missing Sire-EMLE documentation updates. [ci skip] --- doc/source/tutorial/part08/03_adp_pmf.rst | 4 ++-- doc/source/tutorial/part08/04_diels_alder.rst | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/doc/source/tutorial/part08/03_adp_pmf.rst b/doc/source/tutorial/part08/03_adp_pmf.rst index 27b2b9a55..f980c6c54 100644 --- a/doc/source/tutorial/part08/03_adp_pmf.rst +++ b/doc/source/tutorial/part08/03_adp_pmf.rst @@ -55,10 +55,10 @@ 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( +>>> d = qm_mols.dynamics( ... timestep="1fs", ... constraint="none", -... engine=engine, +... qm_engine=engine, ... platform="cpu", ... ) diff --git a/doc/source/tutorial/part08/04_diels_alder.rst b/doc/source/tutorial/part08/04_diels_alder.rst index a32eb781e..0e3de5413 100644 --- a/doc/source/tutorial/part08/04_diels_alder.rst +++ b/doc/source/tutorial/part08/04_diels_alder.rst @@ -101,7 +101,7 @@ 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( +>>> d = qm_mols.dynamics( ... timestep="1fs", ... constraint="none", ... perturbable_constraint="none", From 2e1ceb745b2838f0115f40966054d7fbf09bf96b Mon Sep 17 00:00:00 2001 From: Lester Hedges Date: Wed, 4 Dec 2024 09:52:30 +0000 Subject: [PATCH 050/102] Add REST2 related attributes to copy constructor. --- wrapper/Convert/SireOpenMM/openmmmolecule.cpp | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/wrapper/Convert/SireOpenMM/openmmmolecule.cpp b/wrapper/Convert/SireOpenMM/openmmmolecule.cpp index 91dbf4b0d..4552afb66 100644 --- a/wrapper/Convert/SireOpenMM/openmmmolecule.cpp +++ b/wrapper/Convert/SireOpenMM/openmmmolecule.cpp @@ -1475,6 +1475,7 @@ void OpenMMMolecule::alignInternals(const PropertyMap &map) perturbed->dih_params = dih_params_1; perturbed->is_improper = is_improper_1; + is_improper = is_improper_1; // now align all of the exceptions - this should allow the bonding // to change during the perturbation @@ -2234,6 +2235,7 @@ PerturbableOpenMMMolecule::PerturbableOpenMMMolecule(const OpenMMMolecule &mol, perturbable_constraints = mol.perturbable_constraints; is_improper = mol.is_improper; + is_rest2 = mol.is_rest2; bool fix_perturbable_zero_sigmas = false; @@ -2301,7 +2303,9 @@ PerturbableOpenMMMolecule::PerturbableOpenMMMolecule(const PerturbableOpenMMMole exception_atoms(other.exception_atoms), exception_idxs(other.exception_idxs), perturbable_constraints(other.perturbable_constraints), constraint_idxs(other.constraint_idxs), - start_atom_idx(other.start_atom_idx) + start_atom_idx(other.start_atom_idx), + is_improper(other.is_improper), + is_rest2(other.is_rest2) { } From 6f2421d45be59809d7e38bc449a3c7f3329a5e44 Mon Sep 17 00:00:00 2001 From: Lester Hedges Date: Wed, 4 Dec 2024 16:00:10 +0000 Subject: [PATCH 051/102] Make sure REST2 scale factor is reset after computing energies. --- src/sire/mol/_dynamics.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/sire/mol/_dynamics.py b/src/sire/mol/_dynamics.py index bcb5eabf5..9dfb7ac9f 100644 --- a/src/sire/mol/_dynamics.py +++ b/src/sire/mol/_dynamics.py @@ -413,7 +413,11 @@ def _exit_dynamics_block( * kcal_per_mol ) - self._omm_mols.set_lambda(sim_lambda_value, update_constraints=False) + self._omm_mols.set_lambda( + sim_lambda_value, + rest2_scale=sim_rest2_scale, + update_constraints=False, + ) if self._is_interpolate: self._energy_trajectory.set( From 913473f5d06b2e902a49fa1ba4319c72028b6a67 Mon Sep 17 00:00:00 2001 From: Lester Hedges Date: Wed, 4 Dec 2024 16:18:32 +0000 Subject: [PATCH 052/102] Only update lambda and REST2 scale factors if needed. --- wrapper/Convert/SireOpenMM/_sommcontext.py | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/wrapper/Convert/SireOpenMM/_sommcontext.py b/wrapper/Convert/SireOpenMM/_sommcontext.py index edde66b2e..64c13c1ff 100644 --- a/wrapper/Convert/SireOpenMM/_sommcontext.py +++ b/wrapper/Convert/SireOpenMM/_sommcontext.py @@ -266,7 +266,12 @@ def set_lambda( if rest2_scale is None: rest2_scale = self._rest2_scale else: - self._rest2_scale = rest2_scale + if rest2_scale < 1.0: + raise ValueError("'rest2_scale' must be >= 1.0") + + if (lambda_value == self._lambda_value) and (rest2_scale == self._rest2_scale): + # Nothing to do. + return self._lambda_value = self._lambda_lever.set_lambda( self, @@ -276,8 +281,9 @@ def set_lambda( ) # Update any additional parameters in the REST2 region. - if self._has_rest2_selection: + if self._has_rest2_selection and rest2_scale != self._rest2_scale: self._update_rest2(lambda_value, rest2_scale) + self._rest2_scale = rest2_scale def get_rest2_scale(self): """ @@ -289,14 +295,7 @@ def set_rest2_scale(self, rest2_scale): """ Set the temperature scale factor for the REST2 region. """ - if rest2_scale < 1.0: - raise ValueError("'rest2_scale' must be >= 1.0") - - if self._rest2_scale != rest2_scale: - self._set_lambda(self._lambda_value, rest2_scale=rest2_scale) - self._update_rest2(self._lambda_value, rest2_scale) - - self._rest2_scale = rest2_scale + self._set_lambda(self._lambda_value, rest2_scale=rest2_scale) def set_temperature(self, temperature, rescale_velocities=True): """ From 060325a2835f97d9bf529222a1b756d422eb0219 Mon Sep 17 00:00:00 2001 From: Lester Hedges Date: Mon, 9 Dec 2024 10:26:44 +0000 Subject: [PATCH 053/102] Fix update of triclinic box vectors. [closes #265] --- corelib/src/libs/SireMove/openmmfrenergydt.cpp | 12 +++++++----- corelib/src/libs/SireMove/openmmfrenergyst.cpp | 7 ------- corelib/src/libs/SireMove/openmmmdintegrator.cpp | 11 ++++++----- corelib/src/libs/SireMove/openmmpmefep.cpp | 7 ------- 4 files changed, 13 insertions(+), 24 deletions(-) diff --git a/corelib/src/libs/SireMove/openmmfrenergydt.cpp b/corelib/src/libs/SireMove/openmmfrenergydt.cpp index d8e20ca92..8d2cee119 100644 --- a/corelib/src/libs/SireMove/openmmfrenergydt.cpp +++ b/corelib/src/libs/SireMove/openmmfrenergydt.cpp @@ -1228,9 +1228,9 @@ void OpenMMFrEnergyDT::integrate(IntegratorWorkspace &workspace, const Symbol &n // Set Periodic Box Condition - context_openmm.setPeriodicBoxVectors(OpenMM::Vec3(Box_x_Edge_Length, 0, 0), - OpenMM::Vec3(0, Box_y_Edge_Length, 0), - OpenMM::Vec3(0, 0, Box_z_Edge_Length)); + system_openmm->setDefaultPeriodicBoxVectors(OpenMM::Vec3(Box_x_Edge_Length, 0, 0), + OpenMM::Vec3(0, Box_y_Edge_Length, 0), + OpenMM::Vec3(0, 0, Box_z_Edge_Length)); } // TriclinicBox. else if (ptr_sys.property(space_property).isA()) @@ -1253,9 +1253,11 @@ void OpenMMFrEnergyDT::integrate(IntegratorWorkspace &workspace, const Symbol &n const double zy = v2.y() * OpenMM::NmPerAngstrom; const double zz = v2.z() * OpenMM::NmPerAngstrom; - context_openmm.setPeriodicBoxVectors(OpenMM::Vec3(xx, xy, xz), OpenMM::Vec3(yx, yy, yz), - OpenMM::Vec3(zx, zy, zz)); + system_openmm->setDefaultPeriodicBoxVectors(OpenMM::Vec3(xx, xy, xz), OpenMM::Vec3(yx, yy, yz), + OpenMM::Vec3(zx, zy, zz)); } + + context_openmm.reinitialize(); } if (Debug) diff --git a/corelib/src/libs/SireMove/openmmfrenergyst.cpp b/corelib/src/libs/SireMove/openmmfrenergyst.cpp index 17481a97f..ded8dcf8f 100644 --- a/corelib/src/libs/SireMove/openmmfrenergyst.cpp +++ b/corelib/src/libs/SireMove/openmmfrenergyst.cpp @@ -3450,10 +3450,6 @@ void OpenMMFrEnergyST::createContext(IntegratorWorkspace &workspace, SireUnits:: system_openmm->setDefaultPeriodicBoxVectors(OpenMM::Vec3(Box_x_Edge_Length, 0, 0), OpenMM::Vec3(0, Box_y_Edge_Length, 0), OpenMM::Vec3(0, 0, Box_z_Edge_Length)); - - openmm_context->setPeriodicBoxVectors(OpenMM::Vec3(Box_x_Edge_Length, 0, 0), - OpenMM::Vec3(0, Box_y_Edge_Length, 0), - OpenMM::Vec3(0, 0, Box_z_Edge_Length)); } // TriclinicBox. else if (ptr_sys.property(space_property).isA()) @@ -3478,9 +3474,6 @@ void OpenMMFrEnergyST::createContext(IntegratorWorkspace &workspace, SireUnits:: system_openmm->setDefaultPeriodicBoxVectors(OpenMM::Vec3(xx, xy, xz), OpenMM::Vec3(yx, yy, yz), OpenMM::Vec3(zx, zy, zz)); - - openmm_context->setPeriodicBoxVectors(OpenMM::Vec3(xx, xy, xz), OpenMM::Vec3(yx, yy, yz), - OpenMM::Vec3(zx, zy, zz)); } openmm_context->reinitialize(); diff --git a/corelib/src/libs/SireMove/openmmmdintegrator.cpp b/corelib/src/libs/SireMove/openmmmdintegrator.cpp index d4f3b80c6..ee6200919 100644 --- a/corelib/src/libs/SireMove/openmmmdintegrator.cpp +++ b/corelib/src/libs/SireMove/openmmmdintegrator.cpp @@ -1157,9 +1157,9 @@ void OpenMMMDIntegrator::createContext(IntegratorWorkspace &workspace, SireUnits // Set Periodic Box Condition - openmm_context->setPeriodicBoxVectors(OpenMM::Vec3(Box_x_Edge_Length, 0, 0), - OpenMM::Vec3(0, Box_y_Edge_Length, 0), - OpenMM::Vec3(0, 0, Box_z_Edge_Length)); + system_openmm->setDefaultPeriodicBoxVectors(OpenMM::Vec3(Box_x_Edge_Length, 0, 0), + OpenMM::Vec3(0, Box_y_Edge_Length, 0), + OpenMM::Vec3(0, 0, Box_z_Edge_Length)); } // TriclinicBox. else if (ptr_sys.property(space_property).isA()) @@ -1182,11 +1182,12 @@ void OpenMMMDIntegrator::createContext(IntegratorWorkspace &workspace, SireUnits const double zy = v2.y() * OpenMM::NmPerAngstrom; const double zz = v2.z() * OpenMM::NmPerAngstrom; - openmm_context->setPeriodicBoxVectors(OpenMM::Vec3(xx, xy, xz), OpenMM::Vec3(yx, yy, yz), - OpenMM::Vec3(zx, zy, zz)); + system_openmm->setDefaultPeriodicBoxVectors(OpenMM::Vec3(xx, xy, xz), OpenMM::Vec3(yx, yy, yz), + OpenMM::Vec3(zx, zy, zz)); } is_periodic = true; + openmm_context->reinitialize(); } double AKMAPerPs = 0.04888821; diff --git a/corelib/src/libs/SireMove/openmmpmefep.cpp b/corelib/src/libs/SireMove/openmmpmefep.cpp index 1332bf8ae..f85086460 100644 --- a/corelib/src/libs/SireMove/openmmpmefep.cpp +++ b/corelib/src/libs/SireMove/openmmpmefep.cpp @@ -2954,10 +2954,6 @@ void OpenMMPMEFEP::createContext(IntegratorWorkspace &workspace, SireUnits::Dime system_openmm->setDefaultPeriodicBoxVectors(OpenMM::Vec3(Box_x_Edge_Length, 0, 0), OpenMM::Vec3(0, Box_y_Edge_Length, 0), OpenMM::Vec3(0, 0, Box_z_Edge_Length)); - - openmm_context->setPeriodicBoxVectors(OpenMM::Vec3(Box_x_Edge_Length, 0, 0), - OpenMM::Vec3(0, Box_y_Edge_Length, 0), - OpenMM::Vec3(0, 0, Box_z_Edge_Length)); } // TriclinicBox else if (ptr_sys.property(space_property).isA()) @@ -2983,9 +2979,6 @@ void OpenMMPMEFEP::createContext(IntegratorWorkspace &workspace, SireUnits::Dime system_openmm->setDefaultPeriodicBoxVectors(OpenMM::Vec3(xx, xy, xz), OpenMM::Vec3(yx, yy, yz), OpenMM::Vec3(zx, zy, zz)); - - openmm_context->setPeriodicBoxVectors(OpenMM::Vec3(xx, xy, xz), OpenMM::Vec3(yx, yy, yz), - OpenMM::Vec3(zx, zy, zz)); } openmm_context->reinitialize(); From c252141c5aad23e52004af7b077b76bea428faca Mon Sep 17 00:00:00 2001 From: Lester Hedges Date: Mon, 9 Dec 2024 10:42:04 +0000 Subject: [PATCH 054/102] Update CHANGELOG. --- doc/source/changelog.rst | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/doc/source/changelog.rst b/doc/source/changelog.rst index 8b0262fe6..84df9d070 100644 --- a/doc/source/changelog.rst +++ b/doc/source/changelog.rst @@ -16,17 +16,32 @@ organisation on `GitHub `__. --------------------------------------------------------------------------------------------- * Please add an item to this CHANGELOG for any new features or bug fixes when creating a PR. + +* Fixed update of triclinic box vectors in ``SOMD`` following ``OpenMM`` bug fix. + +`2024.3.1 `__ - December 2024 +-------------------------------------------------------------------------------------------- + * Fixed instantiaton of ``QByteArray`` in ``Sire::Mol::Frame::toByteArray`` and count bytes with ``QByteArray::size``. + * Increase timeout before terminating ``QThread`` objects during ``PageCache`` cleanup. + * Expose missing ``timeout`` kwarg in :meth:`dynamics.minimise()` method. + * Expose missing ``include_constrained_energies`` kwarg in minimisation function. + * Make minimisation function settings consistent across API. + * Reload TorchQMForce module if OpenMM platform changes. + * Fix calculation of non-equilibrium work when starting from QM state. + * Fix stereochemistry in RDKit molecule conversion. + * Fixed :func:`sire.restraints.get_standard_state_correction` to be consistent with the definition of the "force constanst" as ``F = 2 ** k ** x`` (rather than ``F = k ** x``). Updated docstrings and restraints documentation to make it clear how the force constants are defined. + * Fix thread safety issue in Sire OpenMM minimiser. `2024.3.0 `__ - October 2024 From 3696e31f97ab88f3ae0a9c27713bae1c8d857d7d Mon Sep 17 00:00:00 2001 From: Lester Hedges Date: Mon, 6 Jan 2025 09:19:12 +0000 Subject: [PATCH 055/102] Try reducing the number of cores used to compile wrappers on Windows. --- recipes/sire/bld.bat | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/recipes/sire/bld.bat b/recipes/sire/bld.bat index 3f9683041..15a76ebf4 100644 --- a/recipes/sire/bld.bat +++ b/recipes/sire/bld.bat @@ -1,4 +1,4 @@ :: Sire build script for Windows. :: Build and install Sire. -python setup.py install --skip-deps +python setup.py install --skip-deps --npycores 1 From a1642deffb133c4b5e06975521220d9a3b839bce Mon Sep 17 00:00:00 2001 From: Lester Hedges Date: Mon, 6 Jan 2025 12:32:41 +0000 Subject: [PATCH 056/102] Revert change to number of cores used to compile wrappers. [ci skip] --- recipes/sire/bld.bat | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/recipes/sire/bld.bat b/recipes/sire/bld.bat index 15a76ebf4..3f9683041 100644 --- a/recipes/sire/bld.bat +++ b/recipes/sire/bld.bat @@ -1,4 +1,4 @@ :: Sire build script for Windows. :: Build and install Sire. -python setup.py install --skip-deps --npycores 1 +python setup.py install --skip-deps From 0aeb6ca05727222a90dee5f41548b0a6befb08f6 Mon Sep 17 00:00:00 2001 From: Lester Hedges Date: Tue, 7 Jan 2025 09:24:08 +0000 Subject: [PATCH 057/102] Remove redundant optimised callback interface. [closes #269] --- src/sire/qm/_emle.py | 15 +-------------- 1 file changed, 1 insertion(+), 14 deletions(-) diff --git a/src/sire/qm/_emle.py b/src/sire/qm/_emle.py index 591ac599d..5c057cba9 100644 --- a/src/sire/qm/_emle.py +++ b/src/sire/qm/_emle.py @@ -225,23 +225,10 @@ def emle( # 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, + "_sire_callback", cutoff, neighbour_list_frequency, False, From 6705e14a8261b122dd6fb5c38e598be0639c06f6 Mon Sep 17 00:00:00 2001 From: Lester Hedges Date: Tue, 7 Jan 2025 11:55:05 +0000 Subject: [PATCH 058/102] Handle cis/trans double bond stereo value in SDF files. [closes #270] --- corelib/src/libs/SireMol/stereochemistry.cpp | 22 ++++++++++++++++++-- corelib/src/libs/SireMol/stereochemistry.h | 2 ++ doc/source/changelog.rst | 2 ++ tests/io/test_sdf.py | 8 +++++++ 4 files changed, 32 insertions(+), 2 deletions(-) create mode 100644 tests/io/test_sdf.py diff --git a/corelib/src/libs/SireMol/stereochemistry.cpp b/corelib/src/libs/SireMol/stereochemistry.cpp index e2570b8ef..cb9228e83 100644 --- a/corelib/src/libs/SireMol/stereochemistry.cpp +++ b/corelib/src/libs/SireMol/stereochemistry.cpp @@ -75,6 +75,8 @@ Stereochemistry::Stereochemistry(const QString &str) : ConcretePropertystereo_type = 1; + else if (s == "cis or trans") + this->stereo_type = 3; else if (s == "down") this->stereo_type = 6; else if (s == "not stereo") @@ -90,14 +92,14 @@ Stereochemistry Stereochemistry::fromSDF(int value) { Stereochemistry ret; - if (value == 0 or value == 1 or value == -1 or value == 6) + if (value == 0 or value == 1 or value == -1 or value == 3 or value == 6) { ret.stereo_type = value; } else { throw SireError::invalid_arg(QObject::tr("Invalid stereo type '%1'. Should be an integer in " - "[-1, 0, 1, 6]") + "[-1, 0, 1, 3, 6]") .arg(value), CODELOC); } @@ -194,6 +196,8 @@ QString Stereochemistry::toString() const return "not stereo"; case 1: return "up"; + case 3: + return "cis or trans"; case 6: return "down"; case -1: @@ -220,6 +224,8 @@ int Stereochemistry::toSDF() const { case 1: return 1; + case 3: + return 3; case 6: return 6; default: @@ -256,6 +262,12 @@ Stereochemistry Stereochemistry::up() return Stereochemistry::fromSDF(1); } +/** Return a cis or trans Stereochemistry */ +Stereochemistry Stereochemistry::cisOrTrans() +{ + return Stereochemistry::fromSDF(3); +} + /** Return a down Stereochemistry */ Stereochemistry Stereochemistry::down() { @@ -286,6 +298,12 @@ bool Stereochemistry::isUp() const return this->stereo_type == 1; } +/** Return whether or not this is a cis or trans bond */ +bool Stereochemistry::isCisOrTrans() const +{ + return this->stereo_type == 3; +} + /** Return whether or not this is a down bond */ bool Stereochemistry::isDown() const { diff --git a/corelib/src/libs/SireMol/stereochemistry.h b/corelib/src/libs/SireMol/stereochemistry.h index 0e45a81c9..60d368482 100644 --- a/corelib/src/libs/SireMol/stereochemistry.h +++ b/corelib/src/libs/SireMol/stereochemistry.h @@ -73,6 +73,7 @@ namespace SireMol static const char *typeName(); static Stereochemistry up(); + static Stereochemistry cisOrTrans(); static Stereochemistry down(); static Stereochemistry notStereo(); static Stereochemistry undefined(); @@ -85,6 +86,7 @@ namespace SireMol bool isDefined() const; bool isUp() const; + bool isCisOrTrans() const; bool isDown() const; bool isNotStereo() const; diff --git a/doc/source/changelog.rst b/doc/source/changelog.rst index 84df9d070..14b6e927d 100644 --- a/doc/source/changelog.rst +++ b/doc/source/changelog.rst @@ -19,6 +19,8 @@ organisation on `GitHub `__. * Fixed update of triclinic box vectors in ``SOMD`` following ``OpenMM`` bug fix. +* Handle cis/trans double bond stereochemistry values in SDF bond blocks. + `2024.3.1 `__ - December 2024 -------------------------------------------------------------------------------------------- diff --git a/tests/io/test_sdf.py b/tests/io/test_sdf.py new file mode 100644 index 000000000..5631caedc --- /dev/null +++ b/tests/io/test_sdf.py @@ -0,0 +1,8 @@ +import sire as sr + + +def test_cis_or_trans(): + """ + Make sure that we can read an SDF file containing a cis or trans double bond. + """ + mols = sr.load_test_files("cis_trans_double_bond.sdf") From 691223a0f2495f709a1e0296b8a486009d4704d7 Mon Sep 17 00:00:00 2001 From: Audrius Kalpokas Date: Fri, 10 Jan 2025 15:56:31 +0000 Subject: [PATCH 059/102] Updated restraints tutorial to include angle and dihedral restraints, also slightly tidied up python API for these restraints --- doc/source/tutorial/part06/03_restraints.rst | 68 ++++++++++++++++++++ src/sire/restraints/_restraints.py | 19 ++---- 2 files changed, 75 insertions(+), 12 deletions(-) diff --git a/doc/source/tutorial/part06/03_restraints.rst b/doc/source/tutorial/part06/03_restraints.rst index 341dca762..e510098f1 100644 --- a/doc/source/tutorial/part06/03_restraints.rst +++ b/doc/source/tutorial/part06/03_restraints.rst @@ -236,6 +236,74 @@ BondRestraints( size=630 the first atom in the first molecule and the oxygen atoms in all of the water molecules. +Angle or Dihedral Restraints +--------------------------- + +The :func:`sire.restraints.angle` or :func:`sire.restraints.dihedral` functions +are used to create angle or distance restraints. + +Just like the other restraint functions, these functions take +the set of molecules or system you want to simulate, +plus a search string, lists of atom indexes, or molecule views +holding the atoms that you want to restrain., e.g. + +>>> restraints = sr.restraints.angle(mols=mols, atoms=[0, 1, 2]) +>>> print(restraints) +AngleRestraints( name=restraint, size=1 +0: AngleRestraint( [0, 1, 2], theta0=112.8°, ktheta=0.0304617 kcal mol-1 °-2 ) + ) + +or + +>>> restraints = sr.restraints.dihedral(mols=mols, atoms=[0, 1, 2, 3]) +>>> print(restraints) +DihedralRestraints( name=restraint, size=1 +0: DihedralRestraint( [0, 1, 2, 3], phi0=244.528°, kphi=0.0304617 kcal mol-1 °-2 ) + ) + +creates a single harmonic angle or dihedral restraint that acts between +the specified atoms. By default, the equilibrium angle (theta0 or phi0) +is the current angle between the atoms (112.8° or 244.528°), +and the force constant (ktheta or kphi) is 100 kcal mol-1 rad-2. + +You can set these via the ``ktheta`` or ``kphi`` and ``theta0`` or ``phi0`` arguments depending +on the restraint used, e.g. + +>>> restraints = sr.restraints.angle(mols=mols, atoms=[0, 1, 2], +... theta0="1.5 rad", ktheta="50 kcal mol-1 rad-2") +>>> print(restraints) +AngleRestraints( name=restraint, size=1 +0: AngleRestraint( [0, 1, 2], theta0=85.9437°, ktheta=0.0152309 kcal mol-1 °-2 ) + ) + +or + +>>> restraints = sr.restraints.dihedral(mols=mols, atoms=[0, 1, 2, 3], +... phi0="2 rad", kphi="10 kcal mol-1 rad-2") +>>> print(restraints) +DihedralRestraints( name=restraint, size=1 +0: DihedralRestraint( [0, 1, 2, 3], phi0=114.592°, kphi=0.00304617 kcal mol-1 °-2 ) + ) + +You can specify the atoms using a search string, passing the atoms themselves, +or passing the atoms from a molecular container. + +>>> ang = mols.angles()[0] +>>> restraints = sr.restraints.angle(mols=mols, atoms=ang.atoms()) +>>> print(restraints) +AngleRestraints( name=restraint, size=1 +0: AngleRestraint( [0, 1, 2], theta0=112.8°, ktheta=0.0304617 kcal mol-1 °-2 ) + ) + +or + +>>> dih = mols.dihedrals()[0] +>>> restraints = sr.restraints.dihedral(mols=mols, atoms=dih.atoms()) +>>> print(restraints) +DihedralRestraints( name=restraint, size=1 +0: DihedralRestraint( [0, 1, 4, 5], phi0=243.281°, kphi=0.0304617 kcal mol-1 °-2 ) + ) + Boresch Restraints --------------------------- diff --git a/src/sire/restraints/_restraints.py b/src/sire/restraints/_restraints.py index 786163a73..83ea94717 100644 --- a/src/sire/restraints/_restraints.py +++ b/src/sire/restraints/_restraints.py @@ -1,4 +1,5 @@ __all__ = [ + "angle", "boresch", "bond", "dihedral", @@ -71,7 +72,7 @@ def angle(mols, atoms, theta0=None, ktheta=None, name=None, map=None): if len(atoms) != 3: raise ValueError( "You need to provide 3 atoms to create an angle restraint" - f"but only {len(atoms)} atoms were provided." + f"whereas {len(atoms)} atoms were provided." ) from .. import measure @@ -81,18 +82,16 @@ def angle(mols, atoms, theta0=None, ktheta=None, name=None, map=None): elif type(ktheta) is list: raise NotImplementedError( - "Setup of multiple angle restraints simultaneously is not currently supported. You can setup one restraint at a time however." + "Setup of multiple angle restraints simultaneously is not currently supported. Please set up each restraint individually and then combine them into multiple restraints." ) - # TODO: Add support for multiple angle restraints if theta0 is None: - # calculate all of the current angles from .. import measure theta0 = measure(atoms[0], atoms[1], atoms[2]) elif type(theta0) is list: raise NotImplementedError( - "Setup of multiple angle restraints simultaneously is not currently supported. You can setup one restraint at a time however." + "Setup of multiple angle restraints simultaneously is not currently supported. Please set up each restraint individually and then combine them into multiple restraints." ) else: theta0 = u(theta0) @@ -505,7 +504,6 @@ def dihedral(mols, atoms, phi0=None, kphi=None, name=None, map=None): """ from .. import u from ..base import create_map - # from ..mm import DihedralRestraint from ..mm import DihedralRestraint, DihedralRestraints map = create_map(map) @@ -519,7 +517,7 @@ def dihedral(mols, atoms, phi0=None, kphi=None, name=None, map=None): if len(atoms) != 4: raise ValueError( "You need to provide 4 atoms to create a dihedral restraint" - f"but only {len(atoms)} atoms were provided." + f"whereas {len(atoms)} atoms were provided." ) from .. import measure @@ -529,19 +527,16 @@ def dihedral(mols, atoms, phi0=None, kphi=None, name=None, map=None): elif type(kphi) is list: raise NotImplementedError( - "Setup of multiple dihedral restraints simultaneously is not currently supported. You can setup one restraint at a time however." + "Setup of multiple dihedral restraints simultaneously is not currently supported. Please set up each restraint individually and then combine them into multiple restraints." ) - # TODO: Add support for multiple dihedral restraints if phi0 is None: - # calculate all of the current angles from .. import measure - # only support 1 dihedral restraint at the moment phi0 = measure(atoms[0], atoms[1], atoms[2], atoms[3]) elif type(phi0) is list: raise NotImplementedError( - "Setup of multiple dihedral restraints simultaneously is not currently supported. You can setup one restraint at a time however." + "Setup of multiple dihedral restraints simultaneously is not currently supported. Please set up each restraint individually and then combine them into multiple restraints." ) else: phi0 = u(phi0) From ab2188dbb6e5ff51b3ba5fe10956996e126c6fbb Mon Sep 17 00:00:00 2001 From: Audrius Kalpokas Date: Fri, 10 Jan 2025 16:29:09 +0000 Subject: [PATCH 060/102] Add basic unit tests for the new restraints --- .../test_angle_dihedral_restraints.py | 88 +++++++++++++++++++ 1 file changed, 88 insertions(+) create mode 100644 tests/restraints/test_angle_dihedral_restraints.py diff --git a/tests/restraints/test_angle_dihedral_restraints.py b/tests/restraints/test_angle_dihedral_restraints.py new file mode 100644 index 000000000..9bacd8069 --- /dev/null +++ b/tests/restraints/test_angle_dihedral_restraints.py @@ -0,0 +1,88 @@ +import pytest +import sire as sr + +def test_default_angle_restraints_setup(ala_mols): + """Tests that angle restraints can be set up correctly with default parameters.""" + mols = ala_mols.clone() + restraints = sr.restraints.angle(mols=mols, atoms=[0, 1, 2]) + assert restraints.num_restraints() == 1 + assert restraints[0].atoms() == [0, 1, 2] + assert restraints[0].ktheta().value() == 100.0 + +def test_angle_restraint_custom_ktheta(ala_mols): + """Tests that angle restraints can be set up correctly with custom ktheta.""" + mols = ala_mols.clone() + restraints = sr.restraints.angle(mols=mols, atoms=[0, 1, 2], ktheta="10 kcal mol-1 rad-2") + assert restraints.num_restraints() == 1 + assert restraints[0].atoms() == [0, 1, 2] + assert restraints[0].ktheta().value() == 10.0 + +def test_angle_restraint_custom_ktheta_and_theta0(ala_mols): + """Tests that angle restraints can be set up correctly with custom ktheta and theta0.""" + mols = ala_mols.clone() + restraints = sr.restraints.angle(mols=mols, atoms=[0, 1, 2], ktheta="10 kcal mol-1 rad-2", theta0="2 rad") + assert restraints.num_restraints() == 1 + assert restraints[0].atoms() == [0, 1, 2] + assert restraints[0].ktheta().value() == 10.0 + assert restraints[0].theta0().value() == 2.0 + +def test_angle_restraint_molecular_container(ala_mols): + """Tests that angle restraints can be set up correctly with a molecular container (i.e. passing the angle of the molecule).""" + mols = ala_mols.clone() + ang = mols.angles()[0] + restraints = sr.restraints.angle(mols=mols, atoms=ang.atoms()) + + +def test_default_dihedral_restraints_setup(ala_mols): + """Tests that dihedral restraints can be set up correctly with default parameters.""" + mols = ala_mols.clone() + restraints = sr.restraints.dihedral(mols=mols, atoms=[0, 1, 2, 3]) + assert restraints.num_restraints() == 1 + assert restraints[0].atoms() == [0, 1, 2, 3] + assert restraints[0].kphi().value() == 100.0 + +def test_dihedral_restraint_custom_kphi(ala_mols): + """Tests that dihedral restraints can be set up correctly with custom kphi.""" + mols = ala_mols.clone() + restraints = sr.restraints.dihedral(mols=mols, atoms=[0, 1, 2, 3], kphi="10 kcal mol-1 rad-2") + assert restraints.num_restraints() == 1 + assert restraints[0].atoms() == [0, 1, 2, 3] + assert restraints[0].kphi().value() == 10.0 + +def test_dihedral_restraint_custom_kphi_and_phi0(ala_mols): + """Tests that dihedral restraints can be set up correctly with custom kphi and phi0.""" + mols = ala_mols.clone() + restraints = sr.restraints.dihedral(mols=mols, atoms=[0, 1, 2, 3], kphi="10 kcal mol-1 rad-2", phi0="2 rad") + assert restraints.num_restraints() == 1 + assert restraints[0].atoms() == [0, 1, 2, 3] + assert restraints[0].kphi().value() == 10.0 + assert restraints[0].phi0().value() == 2.0 + +def test_dihedral_restraint_molecular_container(ala_mols): + """Tests that dihedral restraints can be set up correctly with a molecular container (i.e. passing the dihedral of the molecule).""" + mols = ala_mols.clone() + dih = mols.dihedrals()[0] + restraints = sr.restraints.dihedral(mols=mols, atoms=dih.atoms()) + +def test_multiple_angle_restraints(ala_mols): + """Tests that multiple angle restraints can be set up correctly.""" + mols = ala_mols.clone() + ang0 = mols.angles()[0] + ang1 = mols.angles()[1] + restraints = sr.restraints.angle(mols=mols, atoms=ang0.atoms()) + restraint1 = sr.restraints.angle(mols=mols, atoms=ang1.atoms()) + restraints.add(restraint1) + assert restraints.num_restraints() == 2 + +def test_multiple_dihedral_restraints(ala_mols): + """Tests that multiple dihedral restraints can be set up correctly.""" + mols = ala_mols.clone() + dih0 = mols.dihedrals()[0] + dih1 = mols.dihedrals()[1] + dih2 = mols.dihedrals()[2] + restraints = sr.restraints.dihedral(mols=mols, atoms=dih0.atoms()) + restraint1 = sr.restraints.dihedral(mols=mols, atoms=dih1.atoms()) + restraint2 = sr.restraints.dihedral(mols=mols, atoms=dih2.atoms()) + restraints.add(restraint1) + restraints.add(restraint2) + assert restraints.num_restraints() == 3 \ No newline at end of file From 35725d8d89e8de8ba2aa0f41d9ebcc5a6fee1f9a Mon Sep 17 00:00:00 2001 From: Audrius Kalpokas Date: Fri, 10 Jan 2025 16:37:11 +0000 Subject: [PATCH 061/102] Updated changelog and contributor pages for the PR --- doc/source/changelog.rst | 2 ++ doc/source/contributors.rst | 1 + 2 files changed, 3 insertions(+) diff --git a/doc/source/changelog.rst b/doc/source/changelog.rst index 84df9d070..39b255b85 100644 --- a/doc/source/changelog.rst +++ b/doc/source/changelog.rst @@ -19,6 +19,8 @@ organisation on `GitHub `__. * Fixed update of triclinic box vectors in ``SOMD`` following ``OpenMM`` bug fix. +* Added support for angle and dihedral restraints which can be used in alchemical and standard simulations. + `2024.3.1 `__ - December 2024 -------------------------------------------------------------------------------------------- diff --git a/doc/source/contributors.rst b/doc/source/contributors.rst index 495575326..4622d0ba3 100644 --- a/doc/source/contributors.rst +++ b/doc/source/contributors.rst @@ -22,3 +22,4 @@ can be recognised. * `@msuruzon `__ * `@kexul `__ * `@mb2055 `__ +* `@akalpokas `__ From 6a41c930a9d2e6a098e144b4b13d6930afadc1b3 Mon Sep 17 00:00:00 2001 From: Lester Hedges Date: Mon, 13 Jan 2025 15:30:46 +0000 Subject: [PATCH 062/102] Add test for OpenMM REST2 modifications. --- tests/convert/test_openmm_rest2.py | 184 +++++++++++++++++++++++++++++ 1 file changed, 184 insertions(+) create mode 100644 tests/convert/test_openmm_rest2.py diff --git a/tests/convert/test_openmm_rest2.py b/tests/convert/test_openmm_rest2.py new file mode 100644 index 000000000..8f86fc347 --- /dev/null +++ b/tests/convert/test_openmm_rest2.py @@ -0,0 +1,184 @@ +from math import isclose + +import sire as sr + + +def test_rest2(): + """ + Test that REST2 modifications are correctly applied to the system. + """ + + # Load the test perturbation. + mol = sr.load_test_files("toluene_methane.s3") + + # Link to the reference state. + mol = sr.morph.link_to_reference(mol) + + # Work out the number of dihedrals in the system. + num_dihedrals = len(mol.dihedrals()) + + # Create a dynamics object. + d = mol.dynamics() + + # Extract the OpenMM system. + omm_system = d.context().getSystem() + + # Find the PeriodicTorsionForce. + for force in omm_system.getForces(): + if force.getName() == "PeriodicTorsionForce": + break + + # Store the initial parameters. + torsion_params_initial = [] + for i in range(force.getNumTorsions()): + torsion_params_initial.append(force.getTorsionParameters(i)[-1]) + + # Find the NonbondedForce. + for force in omm_system.getForces(): + if force.getName() == "NonbondedForce": + break + + # Store the initial parameters. + nonbonded_params_initial = [] + for i in range(force.getNumParticles()): + charge, sigma, epsilon = force.getParticleParameters(i) + nonbonded_params_initial.append((charge, epsilon)) + exception_params_initial = [] + for i in range(force.getNumExceptions()): + exception_params_initial.append(force.getExceptionParameters(i)[-3::2]) + + # Find the ghost/ghost nonbonded interaction. + for force in omm_system.getForces(): + if force.getName() == "GhostGhostNonbondedForce": + break + + # Store the initial parameters. + ghost_ghost_params_initial = [] + for i in range(force.getNumParticles()): + charge, half_sigma, two_sqrt_epsilon, alpha, kappa = ( + force.getParticleParameters(i) + ) + ghost_ghost_params_initial.append((charge, two_sqrt_epsilon)) + + # Find the ghost/non-ghost nonbonded interaction. + for force in omm_system.getForces(): + if force.getName() == "GhostNonGhostNonbondedForce": + break + + # Store the initial parameters. + ghost_non_ghost_params_initial = [] + for i in range(force.getNumParticles()): + charge, half_sigma, two_sqrt_epsilon, alpha, kappa = ( + force.getParticleParameters(i) + ) + ghost_non_ghost_params_initial.append((charge, two_sqrt_epsilon)) + + # Update the REST2 scaling factor. + d.set_lambda(0.0, rest2_scale=2.0) + + # Extract the OpenMM system. + omm_system = d.context().getSystem() + + # Find the PeriodicTorsionForce. + for force in omm_system.getForces(): + if force.getName() == "PeriodicTorsionForce": + break + + # Store the modified parameters. + torsion_params_modified = [] + for i in range(force.getNumTorsions()): + torsion_params_modified.append(force.getTorsionParameters(i)[-1]) + + # Find the NonbondedForce. + for force in omm_system.getForces(): + if force.getName() == "NonbondedForce": + break + + # Store the modified parameters. + nonbonded_params_modified = [] + for i in range(force.getNumParticles()): + charge, sigma, epsilon = force.getParticleParameters(i) + nonbonded_params_modified.append((charge, epsilon)) + exception_params_modified = [] + for i in range(force.getNumExceptions()): + exception_params_modified.append(force.getExceptionParameters(i)[-3::2]) + + # Find the ghost/ghost nonbonded interaction. + for force in omm_system.getForces(): + if force.getName() == "GhostGhostNonbondedForce": + break + + # Store the modified parameters. + ghost_ghost_params_modified = [] + for i in range(force.getNumParticles()): + charge, half_sigma, two_sqrt_epsilon, alpha, kappa = ( + force.getParticleParameters(i) + ) + ghost_ghost_params_modified.append((charge, two_sqrt_epsilon)) + + # Find the ghost/non-ghost nonbonded interaction. + for force in omm_system.getForces(): + if force.getName() == "GhostNonGhostNonbondedForce": + break + + # Store the modified parameters. + ghost_non_ghost_params_modified = [] + for i in range(force.getNumParticles()): + charge, half_sigma, two_sqrt_epsilon, alpha, kappa = ( + force.getParticleParameters(i) + ) + ghost_non_ghost_params_modified.append((charge, two_sqrt_epsilon)) + + # Store the scaling factor. + scale = 0.5 + + # All dihedral force constants should be halved. + for i in range(num_dihedrals): + assert isclose( + torsion_params_modified[i]._value, torsion_params_initial[i]._value * scale + ) + + # All impropers should be unchanged. + for i in range(num_dihedrals, len(torsion_params_initial)): + assert isclose( + torsion_params_modified[i]._value, torsion_params_initial[i]._value + ) + + # Nonbonded charages should be scaled by the square root of the scaling + # factor and epsilon should be scaled by the scaling factor. + for i in range(len(nonbonded_params_initial)): + charge, epsilon = nonbonded_params_initial[i] + charge_modified, epsilon_modified = nonbonded_params_modified[i] + assert isclose(charge_modified._value, charge._value * scale**0.5) + assert isclose(epsilon_modified._value, epsilon._value * scale) + + # For exceptions, both the charge and epsilon should be scaled by the + # scaling factor. + for i in range(len(exception_params_initial)): + charge, epsilon = exception_params_initial[i] + charge_modified, epsilon_modified = exception_params_modified[i] + assert isclose(charge_modified._value, charge._value * scale) + if epsilon._value > 1e-6: + assert isclose(epsilon_modified._value, epsilon._value * scale) + + # Ghost/ghost nonbonded charges should be scaled by the square root of the + # scaling factor and epsilon should be scaled by the scaling factor. + # (Note that epsilon is stored as sqrt(epsilon) so the scaling factor is + # also square rooted.) + for i in range(len(ghost_ghost_params_initial)): + charge, two_sqrt_epsilon = ghost_ghost_params_initial[i] + charge_modified, two_sqrt_epsilon_modified = ghost_ghost_params_modified[i] + assert isclose(charge_modified, charge * scale**0.5) + if two_sqrt_epsilon > 1e-6: + assert isclose(two_sqrt_epsilon_modified, two_sqrt_epsilon * scale**0.5) + + # Ghost/non-ghost nonbonded charges should be scaled by the square root of + # the scaling factor and epsilon should be scaled by the scaling factor. + # (Note that epsilon is stored as sqrt(epsilon) so the scaling factor is + # also square rooted.) + for i in range(len(ghost_non_ghost_params_initial)): + charge, two_sqrt_epsilon = ghost_non_ghost_params_initial[i] + charge_modified, two_sqrt_epsilon_modified = ghost_non_ghost_params_modified[i] + assert isclose(charge_modified, charge * scale**0.5) + if two_sqrt_epsilon > 1e-6: + assert isclose(two_sqrt_epsilon_modified, two_sqrt_epsilon * scale**0.5) From f02fcd1536023ddebb1135ba8bebbf060ca232b3 Mon Sep 17 00:00:00 2001 From: Lester Hedges Date: Mon, 13 Jan 2025 15:52:06 +0000 Subject: [PATCH 063/102] Update test to cover partial REST2 selection. --- tests/convert/test_openmm_rest2.py | 40 ++++++++++++++++++++++++------ 1 file changed, 33 insertions(+), 7 deletions(-) diff --git a/tests/convert/test_openmm_rest2.py b/tests/convert/test_openmm_rest2.py index 8f86fc347..f36eee63f 100644 --- a/tests/convert/test_openmm_rest2.py +++ b/tests/convert/test_openmm_rest2.py @@ -1,9 +1,18 @@ from math import isclose +import pytest + import sire as sr -def test_rest2(): +@pytest.mark.parametrize( + ["rest2_selection", "excluded_atoms"], + [ + (None, []), + ("not atomidx 0,1,2,3", [0, 1, 2, 3]), + ], +) +def test_rest2(rest2_selection, excluded_atoms): """ Test that REST2 modifications are correctly applied to the system. """ @@ -18,7 +27,7 @@ def test_rest2(): num_dihedrals = len(mol.dihedrals()) # Create a dynamics object. - d = mol.dynamics() + d = mol.dynamics(rest2_selection=rest2_selection) # Extract the OpenMM system. omm_system = d.context().getSystem() @@ -30,8 +39,18 @@ def test_rest2(): # Store the initial parameters. torsion_params_initial = [] - for i in range(force.getNumTorsions()): - torsion_params_initial.append(force.getTorsionParameters(i)[-1]) + excluded_torsion_indices = [] + for x in range(force.getNumTorsions()): + i, j, k, l, periodicity, phase, force_constant = force.getTorsionParameters(x) + torsion_params_initial.append(force_constant) + # This torsion is not in the REST2 region and should be excluded. + if ( + i in excluded_atoms + or j in excluded_atoms + or k in excluded_atoms + or l in excluded_atoms + ): + excluded_torsion_indices.append(force_constant) # Find the NonbondedForce. for force in omm_system.getForces(): @@ -134,9 +153,16 @@ def test_rest2(): # All dihedral force constants should be halved. for i in range(num_dihedrals): - assert isclose( - torsion_params_modified[i]._value, torsion_params_initial[i]._value * scale - ) + if i in excluded_torsion_indices: + # This torsion is not in the REST2 region so is unchanged. + assert isclose( + torsion_params_modified[i]._value, torsion_params_initial[i]._value + ) + else: + assert isclose( + torsion_params_modified[i]._value, + torsion_params_initial[i]._value * scale, + ) # All impropers should be unchanged. for i in range(num_dihedrals, len(torsion_params_initial)): From 2b4ab018830e21c49cc5145561d689c8a8ae31fb Mon Sep 17 00:00:00 2001 From: Lester Hedges Date: Mon, 13 Jan 2025 16:24:38 +0000 Subject: [PATCH 064/102] Typo. --- tests/convert/test_openmm_rest2.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/convert/test_openmm_rest2.py b/tests/convert/test_openmm_rest2.py index f36eee63f..b625b1074 100644 --- a/tests/convert/test_openmm_rest2.py +++ b/tests/convert/test_openmm_rest2.py @@ -170,7 +170,7 @@ def test_rest2(rest2_selection, excluded_atoms): torsion_params_modified[i]._value, torsion_params_initial[i]._value ) - # Nonbonded charages should be scaled by the square root of the scaling + # Nonbonded charges should be scaled by the square root of the scaling # factor and epsilon should be scaled by the scaling factor. for i in range(len(nonbonded_params_initial)): charge, epsilon = nonbonded_params_initial[i] From d84b7b30de6801ce0e63ee9fc841788506a531ca Mon Sep 17 00:00:00 2001 From: Lester Hedges Date: Tue, 14 Jan 2025 09:31:29 +0000 Subject: [PATCH 065/102] Update alchemical dynamics docs with REST2 information. --- .../tutorial/part06/02_alchemical_dynamics.rst | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/doc/source/tutorial/part06/02_alchemical_dynamics.rst b/doc/source/tutorial/part06/02_alchemical_dynamics.rst index 52c4861ab..f5069a853 100644 --- a/doc/source/tutorial/part06/02_alchemical_dynamics.rst +++ b/doc/source/tutorial/part06/02_alchemical_dynamics.rst @@ -23,6 +23,20 @@ the simulation that will control how it is run: λ is a parameter that controls the morph from the reference state (at λ=0) to the perturbed state (at λ=1). +* ``rest2_scale`` - this sets the scale factor for Replica Exchange + with Solute Tempering (REST2) simulations. This is a floating point + number that defaults to ``1.0``. The scale factor defines the temperature + of the REST2 region relative to the rest of the system, which can be used + to soften dihedral potentials and non-bonded interactions in the REST2 + region to aid conformational sampling. + +* ``rest2_selection`` - this is a selection string that specifies additional + atoms to include in the REST2 region, e.g. relevant atoms within the protein + binding site. (By default, the REST2 region comprises all atoms in perturbable + molecules.) If the selection includes atoms from a perturbable molecule, then + only those atoms within the perturbable molecule will be used, i.e. this + allows a user to specify a sub-set of atoms within a perturbable molecule. + * ``swap_end_states`` - if set to ``True``, this will swap the end states of the perturbation. The morph will run from the perturbed state (at λ=0) to the reference state (at λ=1). Note that the coordinates @@ -452,7 +466,8 @@ are; new value of λ for the context. Note that this should only really be used to change λ to evaluate energies at different λ-windows. It is better to re-create the context if you want to simulate - at a different λ-value. + at a different λ-value. This function can also be used to set the + ``rest2_scale`` parameter for the context. * :func:`~sire.Convert.SireOpenMM.SOMMContext.get_lambda_schedule` - return the λ-schedule used to control the morph. * :func:`~sire.Convert.SireOpenMM.SOMMContext.set_lambda_schedule` - set the From cac3e05b1e38076ef4e28a86536ab212531336c5 Mon Sep 17 00:00:00 2001 From: Lester Hedges Date: Tue, 14 Jan 2025 09:33:21 +0000 Subject: [PATCH 066/102] Use reference platform for test. --- tests/convert/test_openmm_rest2.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/convert/test_openmm_rest2.py b/tests/convert/test_openmm_rest2.py index b625b1074..ec75b7549 100644 --- a/tests/convert/test_openmm_rest2.py +++ b/tests/convert/test_openmm_rest2.py @@ -27,7 +27,7 @@ def test_rest2(rest2_selection, excluded_atoms): num_dihedrals = len(mol.dihedrals()) # Create a dynamics object. - d = mol.dynamics(rest2_selection=rest2_selection) + d = mol.dynamics(platform="Reference", rest2_selection=rest2_selection) # Extract the OpenMM system. omm_system = d.context().getSystem() From 9df2ddf6ef3094c2b6615963dfbbd4ba589b4b53 Mon Sep 17 00:00:00 2001 From: Lester Hedges Date: Tue, 14 Jan 2025 09:40:50 +0000 Subject: [PATCH 067/102] Only need rest2_scale_factors when lambda_windows is not None. --- src/sire/mol/_dynamics.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/sire/mol/_dynamics.py b/src/sire/mol/_dynamics.py index 9dfb7ac9f..0231ad33d 100644 --- a/src/sire/mol/_dynamics.py +++ b/src/sire/mol/_dynamics.py @@ -995,7 +995,8 @@ def run( "len(rest2_scale_factors) must be equal to len(lambda_windows)" ) else: - rest2_scale_factors = [1.0] * len(lambda_windows) + if lambda_windows is not None: + rest2_scale_factors = [1.0] * len(lambda_windows) def runfunc(num_steps): try: From 85b68f431e421f7a5c3af56f52e8d44c5a555bd1 Mon Sep 17 00:00:00 2001 From: Lester Hedges Date: Tue, 14 Jan 2025 11:26:10 +0000 Subject: [PATCH 068/102] Only loop until runtime is exceeded, not save interval. --- src/sire/mol/_dynamics.py | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/src/sire/mol/_dynamics.py b/src/sire/mol/_dynamics.py index 0231ad33d..b3ee102d0 100644 --- a/src/sire/mol/_dynamics.py +++ b/src/sire/mol/_dynamics.py @@ -1066,10 +1066,6 @@ class NeedsMinimiseError(Exception): else: save_energy = False - nrun_till_save = min(steps_till_frame, steps_till_energy) - - assert nrun_till_save >= 0 - self._enter_dynamics_block() # process the last block in the foreground @@ -1083,11 +1079,13 @@ class NeedsMinimiseError(Exception): else: process_promise = None - while nrun_till_save > 0: + while completed < steps_to_run: + nrun = block_size - if 2 * nrun > nrun_till_save: - nrun = nrun_till_save + # this block will exceed the run time so reduce the size + if 2 * nrun > steps_to_run - completed: + nrun = steps_to_run - completed # run the current block in the background run_promise = pool.submit(runfunc, nrun) @@ -1114,7 +1112,6 @@ class NeedsMinimiseError(Exception): if result == 0: completed += nrun - nrun_till_save -= nrun progress.set_progress(completed) run_promise = None else: From 70b5c68b5480d8a7899cf86f07de095900b09c3a Mon Sep 17 00:00:00 2001 From: Lester Hedges Date: Tue, 14 Jan 2025 11:27:50 +0000 Subject: [PATCH 069/102] Add test for dynamics save frequency modifications. [ref #256] --- tests/mol/test_dynamics.py | 42 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/tests/mol/test_dynamics.py b/tests/mol/test_dynamics.py index 78842ee75..c8d5166e9 100644 --- a/tests/mol/test_dynamics.py +++ b/tests/mol/test_dynamics.py @@ -75,3 +75,45 @@ def test_cutoff_options(ala_mols): assert d.info().cutoff() == sr.u("7.5 A") assert d.info().cutoff_type() == "PME" + + +@pytest.mark.skipif( + "openmm" not in sr.convert.supported_formats(), + reason="openmm support is not available", +) +def test_sample_frequency(ala_mols): + """ + Test that energies and frames are saved at the correct frequency. + """ + + from sire.base import ProgressBar + + ProgressBar.set_silent() + + mols = ala_mols + + d = mols.dynamics(platform="Reference", timestep="1 fs") + + # Create a list of lambda windows. + lambdas = [0.0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0] + + # Run 10 cycles of dynamics, saving energies every 2 fs and frames every 5 fs. + for i in range(10): + d.run( + "1 fs", + energy_frequency="2 fs", + frame_frequency="5 fs", + lambda_windows=lambdas, + ) + + # Get the energy trajectory. + nrg_traj = d.energy_trajectory() + + # Make sure the trajectory has 5 frames. + assert len(nrg_traj) == 5 + + # Get the updated system. + new_mols = d.commit() + + # Check that the trajectory has 2 frames. + assert len(new_mols.trajectory()) == 2 From 1a1b5e8aa3af9c0e7b4c2a263a49bdd02ecfdccf Mon Sep 17 00:00:00 2001 From: Lester Hedges Date: Tue, 14 Jan 2025 14:33:26 +0000 Subject: [PATCH 070/102] Attempt to fix threading exception handling issue. [ref OpenBioSim/somd2#63] --- wrapper/Convert/SireOpenMM/openmmminimise.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/wrapper/Convert/SireOpenMM/openmmminimise.cpp b/wrapper/Convert/SireOpenMM/openmmminimise.cpp index 500401b4b..4dbad32e8 100644 --- a/wrapper/Convert/SireOpenMM/openmmminimise.cpp +++ b/wrapper/Convert/SireOpenMM/openmmminimise.cpp @@ -619,7 +619,7 @@ namespace SireOpenMM double starting_k, double ratchet_scale, double max_constraint_error, double timeout) { - PyThreadState *_save = PyEval_SaveThread(); + PyGILState_STATE gstate = PyGILState_Ensure(); if (max_iterations < 0) { @@ -1105,7 +1105,7 @@ namespace SireOpenMM CODELOC); } - PyEval_RestoreThread(_save); + PyGILState_Release(gstate); return data.getLog().join("\n"); } From 36747f0e265112e4bb8c1fc63613b64b720a71f4 Mon Sep 17 00:00:00 2001 From: Lester Hedges Date: Tue, 14 Jan 2025 14:43:56 +0000 Subject: [PATCH 071/102] No need for inner while loop. --- src/sire/mol/_dynamics.py | 82 +++++++++++++++++++-------------------- 1 file changed, 40 insertions(+), 42 deletions(-) diff --git a/src/sire/mol/_dynamics.py b/src/sire/mol/_dynamics.py index b3ee102d0..2a835b5bf 100644 --- a/src/sire/mol/_dynamics.py +++ b/src/sire/mol/_dynamics.py @@ -1079,22 +1079,44 @@ class NeedsMinimiseError(Exception): else: process_promise = None - while completed < steps_to_run: + nrun = block_size - nrun = block_size + # this block will exceed the run time so reduce the size + if nrun > steps_to_run - completed: + nrun = steps_to_run - completed - # this block will exceed the run time so reduce the size - if 2 * nrun > steps_to_run - completed: - nrun = steps_to_run - completed + # run the current block in the background + run_promise = pool.submit(runfunc, nrun) - # run the current block in the background - run_promise = pool.submit(runfunc, nrun) + result = None - result = None + while not run_promise.done(): + try: + result = run_promise.result(timeout=1.0) + except Exception as e: + if ( + "NaN" in str(e) + and not have_saved_frame + and not have_saved_energy + and auto_fix_minimise + ): + raise NeedsMinimiseError() + + # catch rare edge case where the promise timed + # out, but then completed before the .done() + # test in the next loop iteration + if result is None: + result = run_promise.result() - while not run_promise.done(): + if result == 0: + completed += nrun + progress.set_progress(completed) + run_promise = None + else: + # make sure we finish processing the last block + if process_promise is not None: try: - result = run_promise.result(timeout=1.0) + process_promise.result() except Exception as e: if ( "NaN" in str(e) @@ -1104,39 +1126,15 @@ class NeedsMinimiseError(Exception): ): raise NeedsMinimiseError() - # catch rare edge case where the promise timed - # out, but then completed before the .done() - # test in the next loop iteration - if result is None: - result = run_promise.result() - - if result == 0: - completed += nrun - progress.set_progress(completed) - run_promise = None - else: - # make sure we finish processing the last block - if process_promise is not None: - try: - process_promise.result() - except Exception as e: - if ( - "NaN" in str(e) - and not have_saved_frame - and not have_saved_energy - and auto_fix_minimise - ): - raise NeedsMinimiseError() - - if ( - not have_saved_frame - and not have_saved_energy - and auto_fix_minimise - ): - raise NeedsMinimiseError() + if ( + not have_saved_frame + and not have_saved_energy + and auto_fix_minimise + ): + raise NeedsMinimiseError() - # something went wrong - re-raise the exception - raise result + # something went wrong - re-raise the exception + raise result # make sure we've finished processing the last block if process_promise is not None: From 420f016ca4f3e7fd01a4d8510a2348b96866c918 Mon Sep 17 00:00:00 2001 From: Lester Hedges Date: Tue, 14 Jan 2025 16:04:35 +0000 Subject: [PATCH 072/102] Remove early exit in set_lambda method. --- wrapper/Convert/SireOpenMM/_sommcontext.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/wrapper/Convert/SireOpenMM/_sommcontext.py b/wrapper/Convert/SireOpenMM/_sommcontext.py index 64c13c1ff..bef696eaa 100644 --- a/wrapper/Convert/SireOpenMM/_sommcontext.py +++ b/wrapper/Convert/SireOpenMM/_sommcontext.py @@ -269,10 +269,6 @@ def set_lambda( if rest2_scale < 1.0: raise ValueError("'rest2_scale' must be >= 1.0") - if (lambda_value == self._lambda_value) and (rest2_scale == self._rest2_scale): - # Nothing to do. - return - self._lambda_value = self._lambda_lever.set_lambda( self, lambda_value=lambda_value, From 65fbd034d2addf414690b1c1d7b86883d7fc9f21 Mon Sep 17 00:00:00 2001 From: Audrius Kalpokas Date: Tue, 14 Jan 2025 16:31:27 +0000 Subject: [PATCH 073/102] Removed whitespace that was added and tidied up python restraints files [ci skip] --- src/sire/restraints/_restraints.py | 19 ++++++++----- .../test_angle_dihedral_restraints.py | 27 +++++++++++++++---- wrapper/Convert/SireOpenMM/lambdalever.cpp | 20 +++++++------- 3 files changed, 45 insertions(+), 21 deletions(-) diff --git a/src/sire/restraints/_restraints.py b/src/sire/restraints/_restraints.py index 83ea94717..6e4d5d050 100644 --- a/src/sire/restraints/_restraints.py +++ b/src/sire/restraints/_restraints.py @@ -49,7 +49,7 @@ def angle(mols, atoms, theta0=None, ktheta=None, name=None, map=None): The equilibrium angles for the angle restraints. If None, these will be measured from the current coordinates of the atoms. Default is None. - + Returns ------- AnglelRestraints : SireMM::AngleRestraints @@ -82,16 +82,19 @@ def angle(mols, atoms, theta0=None, ktheta=None, name=None, map=None): elif type(ktheta) is list: raise NotImplementedError( - "Setup of multiple angle restraints simultaneously is not currently supported. Please set up each restraint individually and then combine them into multiple restraints." + "Setup of multiple angle restraints simultaneously is not currently supported. " + "Please set up each restraint individually and then combine them into multiple restraints." ) if theta0 is None: from .. import measure + theta0 = measure(atoms[0], atoms[1], atoms[2]) elif type(theta0) is list: raise NotImplementedError( - "Setup of multiple angle restraints simultaneously is not currently supported. Please set up each restraint individually and then combine them into multiple restraints." + "Setup of multiple angle restraints simultaneously is not currently supported. " + "Please set up each restraint individually and then combine them into multiple restraints." ) else: theta0 = u(theta0) @@ -465,6 +468,7 @@ def _check_stability_boresch_restraint(restraint_components, temperature=u("298 " values further from 0 or pi radians." ) + def dihedral(mols, atoms, phi0=None, kphi=None, name=None, map=None): """ Create a set of dihedral restraints from all of the atoms in 'atoms' @@ -494,7 +498,7 @@ def dihedral(mols, atoms, phi0=None, kphi=None, name=None, map=None): The equilibrium torsional angle for restraints. If None, these will be measured from the current coordinates of the atoms. Default is None. - + Returns ------- DihedralRestraints : SireMM::DihedralRestraints @@ -527,16 +531,19 @@ def dihedral(mols, atoms, phi0=None, kphi=None, name=None, map=None): elif type(kphi) is list: raise NotImplementedError( - "Setup of multiple dihedral restraints simultaneously is not currently supported. Please set up each restraint individually and then combine them into multiple restraints." + "Setup of multiple dihedral restraints simultaneously is not currently supported. " + "Please set up each restraint individually and then combine them into multiple restraints." ) if phi0 is None: from .. import measure + phi0 = measure(atoms[0], atoms[1], atoms[2], atoms[3]) elif type(phi0) is list: raise NotImplementedError( - "Setup of multiple dihedral restraints simultaneously is not currently supported. Please set up each restraint individually and then combine them into multiple restraints." + "Setup of multiple dihedral restraints simultaneously is not currently supported. " + "Please set up each restraint individually and then combine them into multiple restraints." ) else: phi0 = u(phi0) diff --git a/tests/restraints/test_angle_dihedral_restraints.py b/tests/restraints/test_angle_dihedral_restraints.py index 9bacd8069..f6096ec21 100644 --- a/tests/restraints/test_angle_dihedral_restraints.py +++ b/tests/restraints/test_angle_dihedral_restraints.py @@ -1,6 +1,7 @@ import pytest import sire as sr + def test_default_angle_restraints_setup(ala_mols): """Tests that angle restraints can be set up correctly with default parameters.""" mols = ala_mols.clone() @@ -9,23 +10,30 @@ def test_default_angle_restraints_setup(ala_mols): assert restraints[0].atoms() == [0, 1, 2] assert restraints[0].ktheta().value() == 100.0 + def test_angle_restraint_custom_ktheta(ala_mols): """Tests that angle restraints can be set up correctly with custom ktheta.""" mols = ala_mols.clone() - restraints = sr.restraints.angle(mols=mols, atoms=[0, 1, 2], ktheta="10 kcal mol-1 rad-2") + restraints = sr.restraints.angle( + mols=mols, atoms=[0, 1, 2], ktheta="10 kcal mol-1 rad-2" + ) assert restraints.num_restraints() == 1 assert restraints[0].atoms() == [0, 1, 2] assert restraints[0].ktheta().value() == 10.0 + def test_angle_restraint_custom_ktheta_and_theta0(ala_mols): """Tests that angle restraints can be set up correctly with custom ktheta and theta0.""" mols = ala_mols.clone() - restraints = sr.restraints.angle(mols=mols, atoms=[0, 1, 2], ktheta="10 kcal mol-1 rad-2", theta0="2 rad") + restraints = sr.restraints.angle( + mols=mols, atoms=[0, 1, 2], ktheta="10 kcal mol-1 rad-2", theta0="2 rad" + ) assert restraints.num_restraints() == 1 assert restraints[0].atoms() == [0, 1, 2] assert restraints[0].ktheta().value() == 10.0 assert restraints[0].theta0().value() == 2.0 + def test_angle_restraint_molecular_container(ala_mols): """Tests that angle restraints can be set up correctly with a molecular container (i.e. passing the angle of the molecule).""" mols = ala_mols.clone() @@ -41,29 +49,37 @@ def test_default_dihedral_restraints_setup(ala_mols): assert restraints[0].atoms() == [0, 1, 2, 3] assert restraints[0].kphi().value() == 100.0 + def test_dihedral_restraint_custom_kphi(ala_mols): """Tests that dihedral restraints can be set up correctly with custom kphi.""" mols = ala_mols.clone() - restraints = sr.restraints.dihedral(mols=mols, atoms=[0, 1, 2, 3], kphi="10 kcal mol-1 rad-2") + restraints = sr.restraints.dihedral( + mols=mols, atoms=[0, 1, 2, 3], kphi="10 kcal mol-1 rad-2" + ) assert restraints.num_restraints() == 1 assert restraints[0].atoms() == [0, 1, 2, 3] assert restraints[0].kphi().value() == 10.0 + def test_dihedral_restraint_custom_kphi_and_phi0(ala_mols): """Tests that dihedral restraints can be set up correctly with custom kphi and phi0.""" mols = ala_mols.clone() - restraints = sr.restraints.dihedral(mols=mols, atoms=[0, 1, 2, 3], kphi="10 kcal mol-1 rad-2", phi0="2 rad") + restraints = sr.restraints.dihedral( + mols=mols, atoms=[0, 1, 2, 3], kphi="10 kcal mol-1 rad-2", phi0="2 rad" + ) assert restraints.num_restraints() == 1 assert restraints[0].atoms() == [0, 1, 2, 3] assert restraints[0].kphi().value() == 10.0 assert restraints[0].phi0().value() == 2.0 + def test_dihedral_restraint_molecular_container(ala_mols): """Tests that dihedral restraints can be set up correctly with a molecular container (i.e. passing the dihedral of the molecule).""" mols = ala_mols.clone() dih = mols.dihedrals()[0] restraints = sr.restraints.dihedral(mols=mols, atoms=dih.atoms()) + def test_multiple_angle_restraints(ala_mols): """Tests that multiple angle restraints can be set up correctly.""" mols = ala_mols.clone() @@ -74,6 +90,7 @@ def test_multiple_angle_restraints(ala_mols): restraints.add(restraint1) assert restraints.num_restraints() == 2 + def test_multiple_dihedral_restraints(ala_mols): """Tests that multiple dihedral restraints can be set up correctly.""" mols = ala_mols.clone() @@ -85,4 +102,4 @@ def test_multiple_dihedral_restraints(ala_mols): restraint2 = sr.restraints.dihedral(mols=mols, atoms=dih2.atoms()) restraints.add(restraint1) restraints.add(restraint2) - assert restraints.num_restraints() == 3 \ No newline at end of file + assert restraints.num_restraints() == 3 diff --git a/wrapper/Convert/SireOpenMM/lambdalever.cpp b/wrapper/Convert/SireOpenMM/lambdalever.cpp index 32ffcf599..ad6ccb580 100644 --- a/wrapper/Convert/SireOpenMM/lambdalever.cpp +++ b/wrapper/Convert/SireOpenMM/lambdalever.cpp @@ -1473,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]; @@ -1602,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]); } } @@ -1651,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]); } } From 945674cf361b4f933a56c3b006715b3350feb8a3 Mon Sep 17 00:00:00 2001 From: Audrius Kalpokas Date: Tue, 14 Jan 2025 17:38:31 +0000 Subject: [PATCH 074/102] Added force names to custom OpenMM angle and torsion forces --- wrapper/Convert/SireOpenMM/lambdalever.cpp | 4 ++-- wrapper/Convert/SireOpenMM/sire_to_openmm_system.cpp | 2 ++ 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/wrapper/Convert/SireOpenMM/lambdalever.cpp b/wrapper/Convert/SireOpenMM/lambdalever.cpp index ad6ccb580..a05af04dc 100644 --- a/wrapper/Convert/SireOpenMM/lambdalever.cpp +++ b/wrapper/Convert/SireOpenMM/lambdalever.cpp @@ -2012,13 +2012,13 @@ void LambdaLever::updateRestraintInContext(OpenMM::Force &ff, double rho, dynamic_cast(&ff), rho, context); } - else if (ff_type == "CustomAngleForce") + else if (ff_type == "AngleRestraintForce") { _update_restraint_in_context( dynamic_cast(&ff), rho, context); } - else if (ff_type == "CustomTorsionForce") + else if (ff_type == "TorsionRestraintForce") { _update_restraint_in_context( dynamic_cast(&ff), diff --git a/wrapper/Convert/SireOpenMM/sire_to_openmm_system.cpp b/wrapper/Convert/SireOpenMM/sire_to_openmm_system.cpp index 3bee9af57..4c5b928d9 100644 --- a/wrapper/Convert/SireOpenMM/sire_to_openmm_system.cpp +++ b/wrapper/Convert/SireOpenMM/sire_to_openmm_system.cpp @@ -427,6 +427,7 @@ void _add_angle_restraints(const SireMM::AngleRestraints &restraints, auto *restraintff = new OpenMM::CustomAngleForce(energy_expression); + restraintff->setName("AngleRestraintForce"); restraintff->addPerAngleParameter("rho"); restraintff->addPerAngleParameter("k"); restraintff->addPerAngleParameter("theta0"); @@ -489,6 +490,7 @@ void _add_dihedral_restraints(const SireMM::DihedralRestraints &restraints, // it seems that OpenMM wants to call the torsion angle theta rather than phi // we need to rename our parameters accordingly + restraintff->setName("TorsionRestraintForce"); restraintff->addPerTorsionParameter("rho"); restraintff->addPerTorsionParameter("k"); restraintff->addPerTorsionParameter("theta0"); From 37648f1ccc8f14ee6ef8ac1c72218bc5c87ee675 Mon Sep 17 00:00:00 2001 From: Lester Hedges Date: Tue, 14 Jan 2025 17:48:57 +0000 Subject: [PATCH 075/102] Restore thread before throwing any exceptions. [ref OpenBioSim/somd2/#63] --- wrapper/Convert/SireOpenMM/openmmminimise.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/wrapper/Convert/SireOpenMM/openmmminimise.cpp b/wrapper/Convert/SireOpenMM/openmmminimise.cpp index 4dbad32e8..d42a038ed 100644 --- a/wrapper/Convert/SireOpenMM/openmmminimise.cpp +++ b/wrapper/Convert/SireOpenMM/openmmminimise.cpp @@ -619,7 +619,7 @@ namespace SireOpenMM double starting_k, double ratchet_scale, double max_constraint_error, double timeout) { - PyGILState_STATE gstate = PyGILState_Ensure(); + PyThreadState *_save = PyEval_SaveThread(); if (max_iterations < 0) { @@ -1090,6 +1090,8 @@ namespace SireOpenMM } } + PyEval_RestoreThread(_save); + if (is_success) { data.addLog("Minimisation successful!"); @@ -1105,8 +1107,6 @@ namespace SireOpenMM CODELOC); } - PyGILState_Release(gstate); - return data.getLog().join("\n"); } From 67f52c8bfeefbff4a1201dc26ea94adaa52fce78 Mon Sep 17 00:00:00 2001 From: Lester Hedges Date: Wed, 15 Jan 2025 10:42:38 +0000 Subject: [PATCH 076/102] Add test for non-perturbable REST2 atoms and fix partial selections. --- tests/convert/test_openmm_rest2.py | 222 ++++++++++++++++++----------- 1 file changed, 140 insertions(+), 82 deletions(-) diff --git a/tests/convert/test_openmm_rest2.py b/tests/convert/test_openmm_rest2.py index ec75b7549..7ac498a52 100644 --- a/tests/convert/test_openmm_rest2.py +++ b/tests/convert/test_openmm_rest2.py @@ -5,23 +5,37 @@ import sire as sr +@pytest.fixture(scope="module") +def toluene_methane(): + """ + Load the toluene methane perturbable system. + """ + return sr.load_test_files("toluene_methane.s3") + + @pytest.mark.parametrize( - ["rest2_selection", "excluded_atoms"], + ["mols", "rest2_selection", "excluded_atoms"], [ - (None, []), - ("not atomidx 0,1,2,3", [0, 1, 2, 3]), + ("toluene_methane", None, []), + ("toluene_methane", "not atomidx 0,1,2,3", [0, 1, 2, 3]), + ("ala_mols", "molidx 0", []), ], ) -def test_rest2(rest2_selection, excluded_atoms): +def test_rest2(mols, rest2_selection, excluded_atoms, request): """ Test that REST2 modifications are correctly applied to the system. """ # Load the test perturbation. - mol = sr.load_test_files("toluene_methane.s3") + mol = request.getfixturevalue(mols)[0] # Link to the reference state. - mol = sr.morph.link_to_reference(mol) + try: + mol = sr.morph.link_to_reference(mol) + is_perturbable = True + except: + is_perturbable = False + pass # Work out the number of dihedrals in the system. num_dihedrals = len(mol.dihedrals()) @@ -40,7 +54,7 @@ def test_rest2(rest2_selection, excluded_atoms): # Store the initial parameters. torsion_params_initial = [] excluded_torsion_indices = [] - for x in range(force.getNumTorsions()): + for x in range(num_dihedrals): i, j, k, l, periodicity, phase, force_constant = force.getTorsionParameters(x) torsion_params_initial.append(force_constant) # This torsion is not in the REST2 region and should be excluded. @@ -50,7 +64,7 @@ def test_rest2(rest2_selection, excluded_atoms): or k in excluded_atoms or l in excluded_atoms ): - excluded_torsion_indices.append(force_constant) + excluded_torsion_indices.append(x) # Find the NonbondedForce. for force in omm_system.getForces(): @@ -59,38 +73,53 @@ def test_rest2(rest2_selection, excluded_atoms): # Store the initial parameters. nonbonded_params_initial = [] + excluded_nonbonded_indices = [] for i in range(force.getNumParticles()): charge, sigma, epsilon = force.getParticleParameters(i) nonbonded_params_initial.append((charge, epsilon)) + if i in excluded_atoms: + excluded_nonbonded_indices.append(i) exception_params_initial = [] + excluded_exceptions = [] for i in range(force.getNumExceptions()): - exception_params_initial.append(force.getExceptionParameters(i)[-3::2]) - - # Find the ghost/ghost nonbonded interaction. - for force in omm_system.getForces(): - if force.getName() == "GhostGhostNonbondedForce": - break - - # Store the initial parameters. - ghost_ghost_params_initial = [] - for i in range(force.getNumParticles()): - charge, half_sigma, two_sqrt_epsilon, alpha, kappa = ( - force.getParticleParameters(i) - ) - ghost_ghost_params_initial.append((charge, two_sqrt_epsilon)) - - # Find the ghost/non-ghost nonbonded interaction. - for force in omm_system.getForces(): - if force.getName() == "GhostNonGhostNonbondedForce": - break - - # Store the initial parameters. - ghost_non_ghost_params_initial = [] - for i in range(force.getNumParticles()): - charge, half_sigma, two_sqrt_epsilon, alpha, kappa = ( - force.getParticleParameters(i) - ) - ghost_non_ghost_params_initial.append((charge, two_sqrt_epsilon)) + x, y, chargeProd, sigma, epsilon = force.getExceptionParameters(i) + exception_params_initial.append((chargeProd, epsilon)) + if x in excluded_atoms or y in excluded_atoms: + excluded_exceptions.append(i) + + # Handle custom forces for pertubable molecules. + if is_perturbable: + # Find the ghost/ghost nonbonded interaction. + for force in omm_system.getForces(): + if force.getName() == "GhostGhostNonbondedForce": + break + + # Store the initial parameters. + ghost_ghost_params_initial = [] + excluded_ghost_ghost_indices = [] + for i in range(force.getNumParticles()): + charge, half_sigma, two_sqrt_epsilon, alpha, kappa = ( + force.getParticleParameters(i) + ) + ghost_ghost_params_initial.append((charge, two_sqrt_epsilon)) + if i in excluded_atoms: + excluded_ghost_ghost_indices.append(i) + + # Find the ghost/non-ghost nonbonded interaction. + for force in omm_system.getForces(): + if force.getName() == "GhostNonGhostNonbondedForce": + break + + # Store the initial parameters. + ghost_non_ghost_params_initial = [] + excluded_ghost_non_ghost_indices = [] + for i in range(force.getNumParticles()): + charge, half_sigma, two_sqrt_epsilon, alpha, kappa = ( + force.getParticleParameters(i) + ) + ghost_non_ghost_params_initial.append((charge, two_sqrt_epsilon)) + if i in excluded_atoms: + excluded_ghost_non_ghost_indices.append(i) # Update the REST2 scaling factor. d.set_lambda(0.0, rest2_scale=2.0) @@ -127,26 +156,28 @@ def test_rest2(rest2_selection, excluded_atoms): if force.getName() == "GhostGhostNonbondedForce": break - # Store the modified parameters. - ghost_ghost_params_modified = [] - for i in range(force.getNumParticles()): - charge, half_sigma, two_sqrt_epsilon, alpha, kappa = ( - force.getParticleParameters(i) - ) - ghost_ghost_params_modified.append((charge, two_sqrt_epsilon)) - - # Find the ghost/non-ghost nonbonded interaction. - for force in omm_system.getForces(): - if force.getName() == "GhostNonGhostNonbondedForce": - break - - # Store the modified parameters. - ghost_non_ghost_params_modified = [] - for i in range(force.getNumParticles()): - charge, half_sigma, two_sqrt_epsilon, alpha, kappa = ( - force.getParticleParameters(i) - ) - ghost_non_ghost_params_modified.append((charge, two_sqrt_epsilon)) + # Handle custom forces for pertubable molecules. + if is_perturbable: + # Store the modified parameters. + ghost_ghost_params_modified = [] + for i in range(force.getNumParticles()): + charge, half_sigma, two_sqrt_epsilon, alpha, kappa = ( + force.getParticleParameters(i) + ) + ghost_ghost_params_modified.append((charge, two_sqrt_epsilon)) + + # Find the ghost/non-ghost nonbonded interaction. + for force in omm_system.getForces(): + if force.getName() == "GhostNonGhostNonbondedForce": + break + + # Store the modified parameters. + ghost_non_ghost_params_modified = [] + for i in range(force.getNumParticles()): + charge, half_sigma, two_sqrt_epsilon, alpha, kappa = ( + force.getParticleParameters(i) + ) + ghost_non_ghost_params_modified.append((charge, two_sqrt_epsilon)) # Store the scaling factor. scale = 0.5 @@ -175,36 +206,63 @@ def test_rest2(rest2_selection, excluded_atoms): for i in range(len(nonbonded_params_initial)): charge, epsilon = nonbonded_params_initial[i] charge_modified, epsilon_modified = nonbonded_params_modified[i] - assert isclose(charge_modified._value, charge._value * scale**0.5) - assert isclose(epsilon_modified._value, epsilon._value * scale) + if i in excluded_nonbonded_indices: + # This atom is not in the REST2 region so is unchanged. + assert isclose(charge_modified._value, charge._value) + assert isclose(epsilon_modified._value, epsilon._value) + else: + assert isclose(charge_modified._value, charge._value * scale**0.5) + assert isclose(epsilon_modified._value, epsilon._value * scale) # For exceptions, both the charge and epsilon should be scaled by the # scaling factor. for i in range(len(exception_params_initial)): charge, epsilon = exception_params_initial[i] charge_modified, epsilon_modified = exception_params_modified[i] - assert isclose(charge_modified._value, charge._value * scale) - if epsilon._value > 1e-6: - assert isclose(epsilon_modified._value, epsilon._value * scale) - - # Ghost/ghost nonbonded charges should be scaled by the square root of the - # scaling factor and epsilon should be scaled by the scaling factor. - # (Note that epsilon is stored as sqrt(epsilon) so the scaling factor is - # also square rooted.) - for i in range(len(ghost_ghost_params_initial)): - charge, two_sqrt_epsilon = ghost_ghost_params_initial[i] - charge_modified, two_sqrt_epsilon_modified = ghost_ghost_params_modified[i] - assert isclose(charge_modified, charge * scale**0.5) - if two_sqrt_epsilon > 1e-6: - assert isclose(two_sqrt_epsilon_modified, two_sqrt_epsilon * scale**0.5) - - # Ghost/non-ghost nonbonded charges should be scaled by the square root of - # the scaling factor and epsilon should be scaled by the scaling factor. - # (Note that epsilon is stored as sqrt(epsilon) so the scaling factor is - # also square rooted.) - for i in range(len(ghost_non_ghost_params_initial)): - charge, two_sqrt_epsilon = ghost_non_ghost_params_initial[i] - charge_modified, two_sqrt_epsilon_modified = ghost_non_ghost_params_modified[i] - assert isclose(charge_modified, charge * scale**0.5) - if two_sqrt_epsilon > 1e-6: - assert isclose(two_sqrt_epsilon_modified, two_sqrt_epsilon * scale**0.5) + if i in excluded_exceptions: + # This exception is not in the REST2 region so is unchanged. + assert isclose(charge_modified._value, charge._value) + assert isclose(epsilon_modified._value, epsilon._value) + else: + assert isclose(charge_modified._value, charge._value * scale) + if epsilon._value > 1e-6: + assert isclose(epsilon_modified._value, epsilon._value * scale) + + if is_perturbable: + # Ghost/ghost nonbonded charges should be scaled by the square root of the + # scaling factor and epsilon should be scaled by the scaling factor. + # (Note that epsilon is stored as sqrt(epsilon) so the scaling factor is + # also square rooted.) + for i in range(len(ghost_ghost_params_initial)): + charge, two_sqrt_epsilon = ghost_ghost_params_initial[i] + charge_modified, two_sqrt_epsilon_modified = ghost_ghost_params_modified[i] + if i in excluded_ghost_ghost_indices: + # This atom is not in the REST2 region so is unchanged. + assert isclose(charge_modified, charge) + assert isclose(two_sqrt_epsilon_modified, two_sqrt_epsilon) + else: + assert isclose(charge_modified, charge * scale**0.5) + if two_sqrt_epsilon > 1e-6: + assert isclose( + two_sqrt_epsilon_modified, two_sqrt_epsilon * scale**0.5 + ) + + # Ghost/non-ghost nonbonded charges should be scaled by the square root of + # the scaling factor and epsilon should be scaled by the scaling factor. + # (Note that epsilon is stored as sqrt(epsilon) so the scaling factor is + # also square rooted.) + for i in range(len(ghost_non_ghost_params_initial)): + charge, two_sqrt_epsilon = ghost_non_ghost_params_initial[i] + charge_modified, two_sqrt_epsilon_modified = ( + ghost_non_ghost_params_modified[i] + ) + if i in excluded_ghost_non_ghost_indices: + # This atom is not in the REST2 region so is unchanged. + assert isclose(charge_modified, charge) + assert isclose(two_sqrt_epsilon_modified, two_sqrt_epsilon) + else: + assert isclose(charge_modified, charge * scale**0.5) + if two_sqrt_epsilon > 1e-6: + assert isclose( + two_sqrt_epsilon_modified, two_sqrt_epsilon * scale**0.5 + ) From 3eff41b7982683ccc0bed50fb0dce18bfc32a2e7 Mon Sep 17 00:00:00 2001 From: Lester Hedges Date: Wed, 15 Jan 2025 10:44:13 +0000 Subject: [PATCH 077/102] Fix REST2 selection handling. --- src/sire/mol/_dynamics.py | 67 ++++++++++++---------- wrapper/Convert/SireOpenMM/_sommcontext.py | 31 +++++----- 2 files changed, 56 insertions(+), 42 deletions(-) diff --git a/src/sire/mol/_dynamics.py b/src/sire/mol/_dynamics.py index 2a835b5bf..9c71378fe 100644 --- a/src/sire/mol/_dynamics.py +++ b/src/sire/mol/_dynamics.py @@ -187,38 +187,46 @@ def __init__(self, mols=None, map=None, **kwargs): raise ValueError("'rest2_scale' must be of type 'float'") if rest2_scale < 1.0: raise ValueError("'rest2_scale' must be >= 1.0") + else: + rest2_scale = 1.0 - # Check for an additional REST2 selection. - if map.specified("rest2_selection"): - try: - rest2_selection = str(map["rest2_selection"]) - except: - raise ValueError("'rest2_selection' must be of type 'str'") + # Check for an additional REST2 selection. + if map.specified("rest2_selection"): + try: + rest2_selection = str(map["rest2_selection"]) + except: + raise ValueError("'rest2_selection' must be of type 'str'") - try: - from . import selection_to_atoms + try: + from . import selection_to_atoms - # Try to find the REST2 selection. - atoms = selection_to_atoms(mols, rest2_selection) - except: - raise ValueError( - "Invalid 'rest2_selection' string. Please check the selection syntax." - ) + # Try to find the REST2 selection. + atoms = selection_to_atoms(mols, rest2_selection) + except: + raise ValueError( + "Invalid 'rest2_selection' string. Please check the selection syntax." + ) + + # Store all the perturbable molecules associated with the selection + # and remove perturbable atoms from the selection. + pert_mols = {} + non_pert_atoms = atoms.to_list() + for atom in atoms: + mol = atom.molecule() + if mol.has_property("is_perturbable"): + non_pert_atoms.remove(atom) + if mol.number() not in pert_mols: + pert_mols[mol.number()] = [atom] + else: + pert_mols[mol.number()].append(atom) - # Store all the perturbable molecules associated with the selection. - pert_mols = {} - for atom in atoms: - mol = atom.molecule() - if mol.has_property("is_perturbable"): - if mol not in pert_mols: - pert_mols[mol] = [atom] - else: - pert_mols[mol].append(atom) - - # Now create a boolean is_rest2 mask for the atoms in the perturbable molecules. - for mol in pert_mols: + # Now create a boolean is_rest2 mask for the atoms in the perturbable molecules. + # Only do this if there are perturbable atoms in the selection. + if len(non_pert_atoms) != len(atoms): + for num in pert_mols: + mol = self._sire_mols[num] is_rest2 = [False] * mol.num_atoms() - for atom in pert_mols[mol]: + for atom in pert_mols[num]: is_rest2[atom.index().value()] = True # Set the is_rest2 property for each perturbable molecule. @@ -248,8 +256,9 @@ def __init__(self, mols=None, map=None, **kwargs): self._enforce_periodic_box = False # Prepare the OpenMM REST2 data structures. - if map.specified("rest2_scale") and map.specified("rest2_selection"): - self._omm_mols._prepare_rest2(self._sire_mols, atoms) + if map.specified("rest2_selection"): + if len(non_pert_atoms) > 0: + self._omm_mols._prepare_rest2(self._sire_mols, non_pert_atoms) else: self._sire_mols = None self._energy_trajectory = None diff --git a/wrapper/Convert/SireOpenMM/_sommcontext.py b/wrapper/Convert/SireOpenMM/_sommcontext.py index bef696eaa..15fcba546 100644 --- a/wrapper/Convert/SireOpenMM/_sommcontext.py +++ b/wrapper/Convert/SireOpenMM/_sommcontext.py @@ -79,12 +79,13 @@ def __init__( raise ValueError("'rest2_scale' must be >= 1.0") self._rest2_scale = rest2_scale - if map.specified("rest2_selection"): - self._has_rest2_selection = True - else: - self._has_rest2_selection = False else: self._rest2_scale = 1.0 + + # Check for a REST2 selection. + if map.specified("rest2_selection"): + self._has_rest2_selection = True + else: self._has_rest2_selection = False self._lambda_value = self._lambda_lever.set_lambda( @@ -101,6 +102,8 @@ def __init__( self._lambda_value = 0.0 self._map = None + self._is_non_pert_rest2 = False + def __str__(self): p = self.getPlatform() s = self.getSystem() @@ -258,8 +261,6 @@ def set_lambda( 'update_constraints' is True then the constraints will be updated to match the new value of lambda. """ - if self._lambda_lever is None: - return # If not provided, use the REST2 scaling factor used to initalise # the context. @@ -269,15 +270,16 @@ def set_lambda( if rest2_scale < 1.0: raise ValueError("'rest2_scale' must be >= 1.0") - self._lambda_value = self._lambda_lever.set_lambda( - self, - lambda_value=lambda_value, - rest2_scale=rest2_scale, - update_constraints=update_constraints, - ) + if self._lambda_lever is not None: + self._lambda_value = self._lambda_lever.set_lambda( + self, + lambda_value=lambda_value, + rest2_scale=rest2_scale, + update_constraints=update_constraints, + ) # Update any additional parameters in the REST2 region. - if self._has_rest2_selection and rest2_scale != self._rest2_scale: + if self._is_non_pert_rest2 and rest2_scale != self._rest2_scale: self._update_rest2(lambda_value, rest2_scale) self._rest2_scale = rest2_scale @@ -378,6 +380,9 @@ def _prepare_rest2(self, system, atoms): # Adapted from code in meld: https://github.com/maccallumlab/meld + # Flag that REST2 modifications are being applied to non-perturbable atoms. + self._is_non_pert_rest2 = True + import openmm from ..Mol import AtomIdx, Connectivity From e18be082595f243018928016d1ff9a33cfd50bd9 Mon Sep 17 00:00:00 2001 From: Lester Hedges Date: Wed, 15 Jan 2025 14:25:31 +0000 Subject: [PATCH 078/102] Update CHANGELOG. --- doc/source/changelog.rst | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/doc/source/changelog.rst b/doc/source/changelog.rst index 949cfb3b0..d49640979 100644 --- a/doc/source/changelog.rst +++ b/doc/source/changelog.rst @@ -23,6 +23,10 @@ organisation on `GitHub `__. * Improved handling of NaN errors during dynamics. +* Restore thread state before raising exceptions in the Sire OpenMM minimiser. + +* Add support for Replica Exchange with Solute Tempering (REST2) simulations. + `2024.3.1 `__ - December 2024 -------------------------------------------------------------------------------------------- From ce736586ec947e941ec6143a71901271edad672f Mon Sep 17 00:00:00 2001 From: Lester Hedges Date: Wed, 15 Jan 2025 17:06:51 +0000 Subject: [PATCH 079/102] Fix adjustment of block size based on sampling frequency. --- src/sire/mol/_dynamics.py | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/src/sire/mol/_dynamics.py b/src/sire/mol/_dynamics.py index 9c71378fe..18f804376 100644 --- a/src/sire/mol/_dynamics.py +++ b/src/sire/mol/_dynamics.py @@ -1026,8 +1026,6 @@ def process_block(state, state_has_cv, nsteps_completed): } ) - block_size = 50 - state = None state_has_cv = (False, False) saved_last_frame = False @@ -1057,21 +1055,31 @@ class NeedsMinimiseError(Exception): with ThreadPoolExecutor() as pool: while completed < steps_to_run: + block_size = 50 + steps_till_frame = self._next_save_frame - ( completed + nsteps_before_run ) - if steps_till_frame <= 0 or steps_till_frame == steps_to_run: + if steps_till_frame <= 0 or ( + steps_till_frame <= block_size + and steps_till_frame <= steps_to_run - completed + ): save_frame = True self._next_save_frame += frame_frequency_steps + block_size = frame_frequency_steps else: save_frame = False steps_till_energy = self._next_save_energy - ( completed + nsteps_before_run ) - if steps_till_energy <= 0 or steps_till_energy == steps_to_run: + if steps_till_energy <= 0 or ( + steps_till_energy <= block_size + and steps_till_energy <= steps_to_run - completed + ): save_energy = True self._next_save_energy += energy_frequency_steps + block_size = energy_frequency_steps else: save_energy = False From 02b4ee3f0fdcfa7b078668cd0570ccd9fe473c44 Mon Sep 17 00:00:00 2001 From: Lester Hedges Date: Thu, 16 Jan 2025 09:13:09 +0000 Subject: [PATCH 080/102] Find force by name to avoid issues if ordering changes. [ci skip] --- src/sire/qm/_emle.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/sire/qm/_emle.py b/src/sire/qm/_emle.py index 5c057cba9..a47ebaefa 100644 --- a/src/sire/qm/_emle.py +++ b/src/sire/qm/_emle.py @@ -38,7 +38,10 @@ def get_forces(self): ) # Get the OpenMM EMLE force. - emle_force = _deepcopy(d._d._omm_mols.getSystem().getForce(0)) + for force in d._d._omm_mols.getSystem().getForces(): + if "QMForce" in force.getName(): + break + emle_force = _deepcopy(force) # Create a null CustomBondForce to add the EMLE interpolation # parameter. @@ -84,7 +87,10 @@ def get_forces(self): ) # Get the OpenMM EMLE force. - emle_force = _deepcopy(d._d._omm_mols.getSystem().getForce(0)) + for force in d._d._omm_mols.getSystem().getForces(): + if "QMForce" in force.getName(): + break + emle_force = _deepcopy(force) # Create a null CustomBondForce to add the EMLE interpolation # parameter. From 5ca5ced97c964a2178bfa29c9d044223e5b55169 Mon Sep 17 00:00:00 2001 From: Lester Hedges Date: Thu, 16 Jan 2025 12:09:52 +0000 Subject: [PATCH 081/102] Use .num_frames() method for simplicity. [ci skip] --- tests/mol/test_dynamics.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/mol/test_dynamics.py b/tests/mol/test_dynamics.py index c8d5166e9..e8a6b5e5a 100644 --- a/tests/mol/test_dynamics.py +++ b/tests/mol/test_dynamics.py @@ -116,4 +116,4 @@ def test_sample_frequency(ala_mols): new_mols = d.commit() # Check that the trajectory has 2 frames. - assert len(new_mols.trajectory()) == 2 + assert new_mols.num_frames() == 2 From 5b52f8698d552bf3aeb48e06f7d6f1fda2b5a1bb Mon Sep 17 00:00:00 2001 From: Lester Hedges Date: Thu, 16 Jan 2025 12:34:46 +0000 Subject: [PATCH 082/102] Add option to always save a frame when .run() exits. --- src/sire/mol/_dynamics.py | 16 ++++++++++++++++ tests/mol/test_dynamics.py | 15 +++++++++++++++ 2 files changed, 31 insertions(+) diff --git a/src/sire/mol/_dynamics.py b/src/sire/mol/_dynamics.py index 18f804376..2bb90b511 100644 --- a/src/sire/mol/_dynamics.py +++ b/src/sire/mol/_dynamics.py @@ -890,6 +890,7 @@ def run( lambda_windows=None, rest2_scale_factors=None, save_velocities: bool = None, + save_frame_on_exit: bool = False, auto_fix_minimise: bool = True, ): if self.is_null(): @@ -903,6 +904,7 @@ def run( "lambda_windows": lambda_windows, "rest2_scale_factors": rest2_scale_factors, "save_velocities": save_velocities, + "save_frame_on_exit": save_frame_on_exit, "auto_fix_minimise": auto_fix_minimise, } @@ -1083,6 +1085,14 @@ class NeedsMinimiseError(Exception): else: save_energy = False + # save the last frame if we're about to exit and the user + # has requested it + if ( + save_frame_on_exit + and completed + block_size >= steps_to_run + ): + save_frame = True + self._enter_dynamics_block() # process the last block in the foreground @@ -1436,6 +1446,7 @@ def run( lambda_windows=None, rest2_scale_factors=None, save_velocities: bool = None, + save_frame_on_exit: bool = False, auto_fix_minimise: bool = True, ): """ @@ -1502,6 +1513,10 @@ def run( By default this is False. Set this to True if you are interested in saving the velocities. + save_frame_on_exit: bool + Whether to save a trajectory frame on exit, regardless of + whether the frame frequency has been reached. + auto_fix_minimise: bool Whether or not to automatically run minimisation if the trajectory exits with an error in the first few steps. @@ -1524,6 +1539,7 @@ def run( lambda_windows=lambda_windows, rest2_scale_factors=rest2_scale_factors, save_velocities=save_velocities, + save_frame_on_exit=save_frame_on_exit, auto_fix_minimise=auto_fix_minimise, ) diff --git a/tests/mol/test_dynamics.py b/tests/mol/test_dynamics.py index e8a6b5e5a..d79ddf6b4 100644 --- a/tests/mol/test_dynamics.py +++ b/tests/mol/test_dynamics.py @@ -117,3 +117,18 @@ def test_sample_frequency(ala_mols): # Check that the trajectory has 2 frames. assert new_mols.num_frames() == 2 + + # Now check when we request that a trajectory frame is saved when run exits. + + # Recreate the dynamics object. + d = mols.dynamics(platform="Reference", timestep="1 fs") + + # Run 10 cycles of dynamics saving frames every 5 fs. + for i in range(10): + d.run("1 fs", frame_frequency="5 fs", save_frame_on_exit=True) + + # Get the updated system. + new_mols = d.commit() + + # Check that the trajectory has 10 frames. + assert new_mols.num_frames() == 10 From b919d6e54c7aa9e281ddc46e9d396a4f4e4932d5 Mon Sep 17 00:00:00 2001 From: Lester Hedges Date: Thu, 16 Jan 2025 12:39:41 +0000 Subject: [PATCH 083/102] Add option to always save the energy when .run() exits. --- src/sire/mol/_dynamics.py | 16 ++++++++++++++++ tests/mol/test_dynamics.py | 17 +++++++++++++++-- 2 files changed, 31 insertions(+), 2 deletions(-) diff --git a/src/sire/mol/_dynamics.py b/src/sire/mol/_dynamics.py index 2bb90b511..994ca36ab 100644 --- a/src/sire/mol/_dynamics.py +++ b/src/sire/mol/_dynamics.py @@ -891,6 +891,7 @@ def run( rest2_scale_factors=None, save_velocities: bool = None, save_frame_on_exit: bool = False, + save_energy_on_exit: bool = False, auto_fix_minimise: bool = True, ): if self.is_null(): @@ -905,6 +906,7 @@ def run( "rest2_scale_factors": rest2_scale_factors, "save_velocities": save_velocities, "save_frame_on_exit": save_frame_on_exit, + "save_energy_on_exit": save_energy_on_exit, "auto_fix_minimise": auto_fix_minimise, } @@ -1093,6 +1095,14 @@ class NeedsMinimiseError(Exception): ): save_frame = True + # save the last energy if we're about to exit and the user + # has requested it + if ( + save_energy_on_exit + and completed + block_size >= steps_to_run + ): + save_energy = True + self._enter_dynamics_block() # process the last block in the foreground @@ -1447,6 +1457,7 @@ def run( rest2_scale_factors=None, save_velocities: bool = None, save_frame_on_exit: bool = False, + save_energy_on_exit: bool = False, auto_fix_minimise: bool = True, ): """ @@ -1517,6 +1528,10 @@ def run( Whether to save a trajectory frame on exit, regardless of whether the frame frequency has been reached. + save_energy_on_exit: bool + Whether to save the energy on exit, regardless of whether + the energy frequency has been reached. + auto_fix_minimise: bool Whether or not to automatically run minimisation if the trajectory exits with an error in the first few steps. @@ -1540,6 +1555,7 @@ def run( rest2_scale_factors=rest2_scale_factors, save_velocities=save_velocities, save_frame_on_exit=save_frame_on_exit, + save_energy_on_exit=save_energy_on_exit, auto_fix_minimise=auto_fix_minimise, ) diff --git a/tests/mol/test_dynamics.py b/tests/mol/test_dynamics.py index d79ddf6b4..7b86fad4a 100644 --- a/tests/mol/test_dynamics.py +++ b/tests/mol/test_dynamics.py @@ -123,9 +123,22 @@ def test_sample_frequency(ala_mols): # Recreate the dynamics object. d = mols.dynamics(platform="Reference", timestep="1 fs") - # Run 10 cycles of dynamics saving frames every 5 fs. + # Run 10 cycles of dynamics, saving energies frames on exit. for i in range(10): - d.run("1 fs", frame_frequency="5 fs", save_frame_on_exit=True) + d.run( + "1 fs", + energy_frequency="2 fs", + frame_frequency="5 fs", + lambda_windows=lambdas, + save_frame_on_exit=True, + save_energy_on_exit=True, + ) + + # Get the energy trajectory. + nrg_traj = d.energy_trajectory() + + # Make sure the trajectory has 10 frames. + assert len(nrg_traj) == 10 # Get the updated system. new_mols = d.commit() From 964a670ae507ada09d0c7bd357e02cc7a8388e95 Mon Sep 17 00:00:00 2001 From: Lester Hedges Date: Fri, 17 Jan 2025 09:55:45 +0000 Subject: [PATCH 084/102] Null the SystemTrajectory pointer when deleting all frames. [ci skip] --- corelib/src/libs/SireSystem/system.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/corelib/src/libs/SireSystem/system.cpp b/corelib/src/libs/SireSystem/system.cpp index 874d0abb1..868a2b883 100644 --- a/corelib/src/libs/SireSystem/system.cpp +++ b/corelib/src/libs/SireSystem/system.cpp @@ -4090,6 +4090,7 @@ void System::deleteAllFrames(const SireBase::PropertyMap &map) { this->accept(); this->mustNowRecalculateFromScratch(); + this->system_trajectory = 0; MolGroupsBase::deleteAllFrames(map); } From 50dc2deaaee92fe8ae8f92848cfccde01ee3b674 Mon Sep 17 00:00:00 2001 From: Lester Hedges Date: Fri, 17 Jan 2025 11:30:33 +0000 Subject: [PATCH 085/102] Fix handling of positive formal charge when writing SDF. [closes #274] --- corelib/src/libs/SireIO/sdf.cpp | 7 +++++++ doc/source/changelog.rst | 2 ++ 2 files changed, 9 insertions(+) diff --git a/corelib/src/libs/SireIO/sdf.cpp b/corelib/src/libs/SireIO/sdf.cpp index c5043dee0..44b9cd193 100644 --- a/corelib/src/libs/SireIO/sdf.cpp +++ b/corelib/src/libs/SireIO/sdf.cpp @@ -959,6 +959,13 @@ SDFMolecule parseMolecule(const Molecule &molecule, QStringList &errors, const P sdfmol.addProperty("CHG", i + 1, QString::number(charge)); charge = 0; } + else if (charge > 0) + { + if (charge < 4) + { + charge = 4 - charge; + } + } else if (charge < 0) { switch (charge) diff --git a/doc/source/changelog.rst b/doc/source/changelog.rst index d49640979..6e1919819 100644 --- a/doc/source/changelog.rst +++ b/doc/source/changelog.rst @@ -27,6 +27,8 @@ organisation on `GitHub `__. * Add support for Replica Exchange with Solute Tempering (REST2) simulations. +* Fix handling of positive formal charge when writing SDF files. + `2024.3.1 `__ - December 2024 -------------------------------------------------------------------------------------------- From 3b2f9841a5e0d687fa082aa86886acaee7170081 Mon Sep 17 00:00:00 2001 From: Lester Hedges Date: Fri, 17 Jan 2025 16:22:01 +0000 Subject: [PATCH 086/102] Update CHANGELOG. [ci skip] --- doc/source/changelog.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/doc/source/changelog.rst b/doc/source/changelog.rst index 6e1919819..a3ab939f9 100644 --- a/doc/source/changelog.rst +++ b/doc/source/changelog.rst @@ -27,6 +27,8 @@ organisation on `GitHub `__. * Add support for Replica Exchange with Solute Tempering (REST2) simulations. +* Null SystemTrajectory pointer when all frames are deleted. + * Fix handling of positive formal charge when writing SDF files. `2024.3.1 `__ - December 2024 From e4c4b52787bd9e74311499e84310a00859dce28b Mon Sep 17 00:00:00 2001 From: Matthew Date: Mon, 20 Jan 2025 15:42:52 +0000 Subject: [PATCH 087/102] Change hyperthetical->hypothetical --- doc/source/tutorial/part06/02_alchemical_dynamics.rst | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/doc/source/tutorial/part06/02_alchemical_dynamics.rst b/doc/source/tutorial/part06/02_alchemical_dynamics.rst index 52c4861ab..5b22120d8 100644 --- a/doc/source/tutorial/part06/02_alchemical_dynamics.rst +++ b/doc/source/tutorial/part06/02_alchemical_dynamics.rst @@ -214,7 +214,7 @@ a :class:`sire.cas.LambdaSchedule`. You can get the λ-schedule used by the dynamics simulation using the :func:`~sire.mol.Dynamics.lambda_schedule` function, e.g. ->>> s = d.lambda_schedule() +>>> s = d.get_schedule() >>> print(s) LambdaSchedule( morph: λ * final + initial * (-λ + 1) @@ -230,7 +230,7 @@ using the :func:`~sire.cas.LambdaSchedule.plot` function, e.g. :alt: View of the default λ-schedule This shows how the different levers available in this schedule would morph -a hyperthetical parameter that has an ``initial`` value of ``2.0`` and a +a hypothetical parameter that has an ``initial`` value of ``2.0`` and a ``final`` value of ``3.0``. In this case the levers are all identical, so would change the parameter @@ -276,7 +276,7 @@ LambdaSchedule( charge: final * (-λ + 1) ) -Again, it is worth plotting the impact of this schedule on a hyperthetical +Again, it is worth plotting the impact of this schedule on a hypothetical parameter. >>> s.get_lever_values(initial=2.0, final=3.0).plot() @@ -331,7 +331,7 @@ the charge lever from γ times the ``final`` value to the full ``final`` value. It also scales the charge lever in the ``morph`` stage by γ, which is set to 0.2 for all stages. -We can see how this would affect a hyperthetical parameter that goes +We can see how this would affect a hypothetical parameter that goes from an ``initial`` value of 2.0 to a ``final`` value of 3.0 via >>> s.get_lever_values(initial=2.0, final=3.0).plot() From ad7fa7b69433410ec20e576ab6af12badd285f37 Mon Sep 17 00:00:00 2001 From: Matthew Date: Mon, 20 Jan 2025 16:15:04 +0000 Subject: [PATCH 088/102] Update images to in line with actual outputs and remove redundant note --- .../part06/02_alchemical_dynamics.rst | 8 -------- .../tutorial/part06/images/06_02_01.jpg | Bin 55554 -> 40150 bytes .../tutorial/part06/images/06_02_02.jpg | Bin 56453 -> 53711 bytes .../tutorial/part06/images/06_02_03.jpg | Bin 49646 -> 51881 bytes .../tutorial/part06/images/06_02_04.jpg | Bin 43201 -> 53953 bytes 5 files changed, 8 deletions(-) diff --git a/doc/source/tutorial/part06/02_alchemical_dynamics.rst b/doc/source/tutorial/part06/02_alchemical_dynamics.rst index 5b22120d8..9ee68ec1e 100644 --- a/doc/source/tutorial/part06/02_alchemical_dynamics.rst +++ b/doc/source/tutorial/part06/02_alchemical_dynamics.rst @@ -340,14 +340,6 @@ from an ``initial`` value of 2.0 to a ``final`` value of 3.0 via :alt: View of the λ-schedule that sandwiches a standard morph stage between two stages that scale the charge lever. -.. note:: - - Schedules constructed outside of the dynamics simulation do not have - the full set of levers (e.g. torsion_k, dih_scale etc) as - levers are only added as they are needed (hence why only - ``default`` and ``charge`` are shown here). The additional levers - are added when the schedule is added to the simulation. - Once you have created your schedule you can add it via the :meth:`~sire.mol.Dynamics.set_schedule` function of the :class:`~sire.mol.Dynamics` object, e.g. diff --git a/doc/source/tutorial/part06/images/06_02_01.jpg b/doc/source/tutorial/part06/images/06_02_01.jpg index 8674ec7aa91c34b50e950794bba0f45f0da50467..dbac039e38c0c5fe234aee73a096f23e57176af4 100644 GIT binary patch literal 40150 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 n{9ob1haGo;5p`Vg1`9>`wpNc!n=p> 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

*1E1{vspJ#!MCNC` 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 K)928&*8cp5#2oee(2K4pvqk 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 From 5bdafc8cfec22cbf0222f4e706c0782313988934 Mon Sep 17 00:00:00 2001 From: Lester Hedges Date: Tue, 21 Jan 2025 15:39:06 +0000 Subject: [PATCH 091/102] Update CHANGELOG. [ci skip] --- doc/source/changelog.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/doc/source/changelog.rst b/doc/source/changelog.rst index 42cecb206..7ef7842ff 100644 --- a/doc/source/changelog.rst +++ b/doc/source/changelog.rst @@ -33,6 +33,8 @@ organisation on `GitHub `__. * Fix handling of positive formal charge when writing SDF files. +* Fix lambda schedule discussion and plots in the tutorial. + `2024.3.1 `__ - December 2024 -------------------------------------------------------------------------------------------- From 0e56c8eb18400da6797fc5169793c30f43744fe4 Mon Sep 17 00:00:00 2001 From: Audrius Kalpokas Date: Tue, 21 Jan 2025 15:50:59 +0000 Subject: [PATCH 092/102] Removed redundant comments for restraints code [ci skip] --- corelib/src/libs/SireMM/anglerestraints.cpp | 23 ++---------- corelib/src/libs/SireMM/anglerestraints.h | 2 +- .../src/libs/SireMM/dihedralrestraints.cpp | 35 +++++-------------- corelib/src/libs/SireMM/dihedralrestraints.h | 4 +-- 4 files changed, 14 insertions(+), 50 deletions(-) diff --git a/corelib/src/libs/SireMM/anglerestraints.cpp b/corelib/src/libs/SireMM/anglerestraints.cpp index 42cec09df..5eaf82fc1 100644 --- a/corelib/src/libs/SireMM/anglerestraints.cpp +++ b/corelib/src/libs/SireMM/anglerestraints.cpp @@ -2,7 +2,7 @@ * * Sire - Molecular Simulation Framework * - * Copyright (C) 2009 Christopher Woods + * Copyright (C) 2025 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 @@ -98,19 +98,8 @@ AngleRestraint::AngleRestraint(const QList &atoms, : ConcreteProperty(), _theta0(theta0), _ktheta(ktheta) { - // Need to think here about validating the angle and force constant values - // if (atoms.count() != 3) - // { - // throw SireError::invalid_arg(QObject::tr( - // "Wrong number of inputs for an Angle restraint. You need to " - // "provide 3 atoms (%1).") - // .arg(atoms.count()), - // // .arg(theta0.count()) - // // .arg(ktheta.count()), - // CODELOC); - // } - - // make sure that we have 3 distinct atoms + + // Make sure that we have 3 distinct atoms QSet distinct; distinct.reserve(3); @@ -120,12 +109,6 @@ AngleRestraint::AngleRestraint(const QList &atoms, distinct.insert(atom); } - // if (distinct.count() != 3) - // throw SireError::invalid_arg(QObject::tr( - // "There is something wrong with the atoms provided. " - // "They should all be unique and all greater than or equal to 0."), - // CODELOC); - atms = atoms.toVector(); } diff --git a/corelib/src/libs/SireMM/anglerestraints.h b/corelib/src/libs/SireMM/anglerestraints.h index 393d30849..b34de4822 100644 --- a/corelib/src/libs/SireMM/anglerestraints.h +++ b/corelib/src/libs/SireMM/anglerestraints.h @@ -2,7 +2,7 @@ * * Sire - Molecular Simulation Framework * - * Copyright (C) 2009 Christopher Woods + * Copyright (C) 2025 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 diff --git a/corelib/src/libs/SireMM/dihedralrestraints.cpp b/corelib/src/libs/SireMM/dihedralrestraints.cpp index 61a086e15..70054b34c 100644 --- a/corelib/src/libs/SireMM/dihedralrestraints.cpp +++ b/corelib/src/libs/SireMM/dihedralrestraints.cpp @@ -2,7 +2,7 @@ * * Sire - Molecular Simulation Framework * - * Copyright (C) 2009 Christopher Woods + * Copyright (C) 2025 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 @@ -28,9 +28,9 @@ #include "dihedralrestraints.h" #include "SireID/index.h" -#include "SireUnits/units.h" #include "SireStream/datastream.h" #include "SireStream/shareddatastream.h" +#include "SireUnits/units.h" #include "SireCAS/errors.h" @@ -80,7 +80,6 @@ QDataStream &operator>>(QDataStream &ds, DihedralRestraint &dihrest) return ds; } - /** Null constructor */ DihedralRestraint::DihedralRestraint() : ConcreteProperty(), @@ -88,29 +87,17 @@ DihedralRestraint::DihedralRestraint() { } - /** Construct a restraint that acts on the angle within the four atoms 'atom0', 'atom1', 'atom2' 'atom3' (phi == a(0123)), restraining the angle within these atoms */ DihedralRestraint::DihedralRestraint(const QList &atoms, - const SireUnits::Dimension::Angle &phi0, - const SireUnits::Dimension::HarmonicAngleConstant &kphi) + const SireUnits::Dimension::Angle &phi0, + const SireUnits::Dimension::HarmonicAngleConstant &kphi) : ConcreteProperty(), _phi0(phi0), _kphi(kphi) { - // Need to think here about validating the angle and force constant values - // if (atoms.count() != 3) - // { - // throw SireError::invalid_arg(QObject::tr( - // "Wrong number of inputs for an Angle restraint. You need to " - // "provide 3 atoms (%1).") - // .arg(atoms.count()), - // // .arg(phi0.count()) - // // .arg(kphi.count()), - // CODELOC); - // } - - // make sure that we have 3 distinct atoms + + // Make sure that we have 4 distinct atoms QSet distinct; distinct.reserve(4); @@ -120,12 +107,6 @@ DihedralRestraint::DihedralRestraint(const QList &atoms, distinct.insert(atom); } - // if (distinct.count() != 3) - // throw SireError::invalid_arg(QObject::tr( - // "There is something wrong with the atoms provided. " - // "They should all be unique and all greater than or equal to 0."), - // CODELOC); - atms = atoms.toVector(); } @@ -301,7 +282,7 @@ DihedralRestraints::DihedralRestraints(const QList &restraint } DihedralRestraints::DihedralRestraints(const QString &name, - const DihedralRestraint &restraint) + const DihedralRestraint &restraint) : ConcreteProperty(name) { if (not restraint.isNull()) @@ -309,7 +290,7 @@ DihedralRestraints::DihedralRestraints(const QString &name, } DihedralRestraints::DihedralRestraints(const QString &name, - const QList &restraints) + const QList &restraints) : ConcreteProperty(name) { for (const auto &restraint : restraints) diff --git a/corelib/src/libs/SireMM/dihedralrestraints.h b/corelib/src/libs/SireMM/dihedralrestraints.h index d3f6b8a35..f40b03e0c 100644 --- a/corelib/src/libs/SireMM/dihedralrestraints.h +++ b/corelib/src/libs/SireMM/dihedralrestraints.h @@ -2,7 +2,7 @@ * * Sire - Molecular Simulation Framework * - * Copyright (C) 2009 Christopher Woods + * Copyright (C) 2025 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 @@ -50,7 +50,7 @@ SIREMM_EXPORT QDataStream &operator>>(QDataStream &, SireMM::DihedralRestraints namespace SireMM { - /** This class represents a single angle restraint between any three + /** This class represents a single torsion restraint between any four * atoms in a system * @author Christopher Woods */ From 5dbc9557955c907b0f822e23dbc7f5ef39337ed8 Mon Sep 17 00:00:00 2001 From: Lester Hedges Date: Fri, 24 Jan 2025 12:28:07 +0000 Subject: [PATCH 093/102] Exclude alchemical ions from the REST2 region. --- src/sire/mol/_dynamics.py | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/src/sire/mol/_dynamics.py b/src/sire/mol/_dynamics.py index 994ca36ab..fba6ba276 100644 --- a/src/sire/mol/_dynamics.py +++ b/src/sire/mol/_dynamics.py @@ -208,12 +208,15 @@ def __init__(self, mols=None, map=None, **kwargs): ) # Store all the perturbable molecules associated with the selection - # and remove perturbable atoms from the selection. + # and remove perturbable atoms from the selection. Remove alchemical ions + # from the selection. pert_mols = {} non_pert_atoms = atoms.to_list() for atom in atoms: mol = atom.molecule() - if mol.has_property("is_perturbable"): + if mol.has_property("is_alchemical_ion"): + non_pert_atoms.remove(atom) + elif mol.has_property("is_perturbable"): non_pert_atoms.remove(atom) if mol.number() not in pert_mols: pert_mols[mol.number()] = [atom] @@ -240,6 +243,20 @@ def __init__(self, mols=None, map=None, **kwargs): # Update the system. self._sire_mols.update(mol) + # Search for alchemical ions and exclude them via a REST2 mask. + try: + for mol in self._sire_mols.molecules("property is_alchemical_ion"): + is_rest2 = [False] * mol.num_atoms() + mol = ( + mol.edit() + .set_property("is_rest2", is_rest2) + .molecule() + .commit() + ) + self._sire_mols.update(mol) + except: + pass + from ..convert import to self._omm_mols = to(self._sire_mols, "openmm", map=self._map) From af71419a56d1e4470bf802dec67cf3636c5abb94 Mon Sep 17 00:00:00 2001 From: Lester Hedges Date: Mon, 27 Jan 2025 13:42:01 +0000 Subject: [PATCH 094/102] Allow user to compute energies over subset of lambda windows. [ci skip] --- doc/source/changelog.rst | 2 + src/sire/mol/_dynamics.py | 90 +++++++++++++++++++++++++++++++++------ 2 files changed, 78 insertions(+), 14 deletions(-) diff --git a/doc/source/changelog.rst b/doc/source/changelog.rst index e050d206d..c0037c2fd 100644 --- a/doc/source/changelog.rst +++ b/doc/source/changelog.rst @@ -37,6 +37,8 @@ organisation on `GitHub `__. * Added support for angle and dihedral restraints which can be used in alchemical and standard simulations. +* Allow user to compute energy trajectory over a subset of the lambda windows for each lambda. + `2024.3.1 `__ - December 2024 -------------------------------------------------------------------------------------------- diff --git a/src/sire/mol/_dynamics.py b/src/sire/mol/_dynamics.py index fba6ba276..ebdd74e68 100644 --- a/src/sire/mol/_dynamics.py +++ b/src/sire/mol/_dynamics.py @@ -358,6 +358,8 @@ def _exit_dynamics_block( rest2_scale_factors=[], save_velocities: bool = False, delta_lambda: float = None, + num_energy_neighbours: int = None, + null_energy: float = None, ): if not self._is_running: raise SystemError("Cannot stop dynamics that is not running!") @@ -387,6 +389,13 @@ def _exit_dynamics_block( self._elapsed_time = current_time self._current_time += delta + # store the number of lambda windows + num_lambda_windows = len(lambda_windows) + + # compute energies for all windows + if num_energy_neighbours is None: + num_energy_neighbours = num_lambda_windows + if save_energy: # should save energy here nrgs = {} @@ -408,7 +417,7 @@ def _exit_dynamics_block( sim_lambda_value = self._omm_mols.get_lambda() sim_rest2_scale = self._omm_mols.get_rest2_scale() - # Store the potential energy and accumulated non-equilibrium work. + # store the potential energy and accumulated non-equilibrium work if self._is_interpolate: nrg = nrgs["potential"] @@ -423,21 +432,32 @@ def _exit_dynamics_block( nrgs[str(sim_lambda_value)] = nrgs["potential"] if lambda_windows is not None: - for lambda_value, rest2_scale in zip( - lambda_windows, rest2_scale_factors + # get the index of the simulation lambda value in the + # lambda windows list + try: + lambda_index = lambda_windows.index(sim_lambda_value) + except: + num_energy_neighbours = num_lambda_windows + lambda_index = i + + for i, (lambda_value, rest2_scale) in enumerate( + zip(lambda_windows, rest2_scale_factors) ): if lambda_value != sim_lambda_value: - self._omm_mols.set_lambda( - lambda_value, - rest2_scale=rest2_scale, - 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 - ) + if abs(lambda_index - i) <= num_energy_neighbours: + self._omm_mols.set_lambda( + lambda_value, + rest2_scale=rest2_scale, + 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 + ) + else: + nrgs[str(lambda_value)] = null_energy * kcal_per_mol self._omm_mols.set_lambda( sim_lambda_value, @@ -910,6 +930,8 @@ def run( save_frame_on_exit: bool = False, save_energy_on_exit: bool = False, auto_fix_minimise: bool = True, + num_energy_neighbours: int = None, + null_energy: str = None, ): if self.is_null(): return @@ -925,6 +947,8 @@ def run( "save_frame_on_exit": save_frame_on_exit, "save_energy_on_exit": save_energy_on_exit, "auto_fix_minimise": auto_fix_minimise, + "num_energy_neighbours": num_energy_neighbours, + "null_energy": null_energy, } from concurrent.futures import ThreadPoolExecutor @@ -944,6 +968,17 @@ def run( if energy_frequency is not None: energy_frequency = u(energy_frequency) + if null_energy is not None: + null_energy = u(null_energy) + else: + null_energy = u("10000 kcal/mol") + + if num_energy_neighbours is not None: + try: + num_energy_neighbours = int(num_energy_neighbours) + except: + num_energy_neighbours = len(lambda_windows) + try: steps_to_run = int(time.to(picosecond) / self.timestep().to(picosecond)) except Exception: @@ -1204,6 +1239,8 @@ class NeedsMinimiseError(Exception): rest2_scale_factors=rest2_scale_factors, save_velocities=save_velocities, delta_lambda=delta_lambda, + num_energy_neighbours=num_energy_neighbours, + null_energy=null_energy.value(), ) saved_last_frame = False @@ -1476,6 +1513,8 @@ def run( save_frame_on_exit: bool = False, save_energy_on_exit: bool = False, auto_fix_minimise: bool = True, + num_energy_neighbours: int = None, + null_energy: str = None, ): """ Perform dynamics on the molecules. @@ -1555,6 +1594,27 @@ def run( Such failures often indicate that the system needs minimsing. This automatically runs the minimisation in these cases, and then runs the requested dynamics. + + num_energy_neighbours: int + The number of neighbouring windows to use when computing + the energy trajectory for the simulation lambda value. + This can be used to compute energies over a subset of the + values in 'lambda_windows', hence reducing the cost of + computing the energy trajectory. Note that the simulation + lambda value must be contained in 'lambda_windows', so it + is recommended that the values are rounded. A value of + 'null_energy' will be added to the energy trajectory for the + lambda windows that are omitted. Note that a similar result + can be achieved by simply removing any lambda values from + 'lambda_windows' that you don't want, but this will result + in an energy trajectory that only contains results for + the specified lambda values. + + null_energy: str + The energy value to use for lambda windows that are not + being computed as part of the energy trajectory, i.e. when + 'num_energy_neighbours' is less than len(lambda_windows). + By default, a value of '10000 kcal mol-1' is used. """ if not self._d.is_null(): if save_velocities is None: @@ -1574,6 +1634,8 @@ def run( save_frame_on_exit=save_frame_on_exit, save_energy_on_exit=save_energy_on_exit, auto_fix_minimise=auto_fix_minimise, + num_energy_neighbours=num_energy_neighbours, + null_energy=null_energy, ) return self From ff8b4fc950a04b3b6909538c057cab8a72e3b26a Mon Sep 17 00:00:00 2001 From: Lester Hedges Date: Wed, 29 Jan 2025 12:11:50 +0000 Subject: [PATCH 095/102] Compute num_lambda_windows when lambda_windows is not None. [ci skip] --- src/sire/mol/_dynamics.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/sire/mol/_dynamics.py b/src/sire/mol/_dynamics.py index ebdd74e68..1d16046d4 100644 --- a/src/sire/mol/_dynamics.py +++ b/src/sire/mol/_dynamics.py @@ -390,11 +390,12 @@ def _exit_dynamics_block( self._current_time += delta # store the number of lambda windows - num_lambda_windows = len(lambda_windows) + if lambda_windows is not None: + num_lambda_windows = len(lambda_windows) - # compute energies for all windows - if num_energy_neighbours is None: - num_energy_neighbours = num_lambda_windows + # compute energies for all windows + if num_energy_neighbours is None: + num_energy_neighbours = num_lambda_windows if save_energy: # should save energy here From 790f7d26df8d72207f8d57816b5bf06b8ed7ded8 Mon Sep 17 00:00:00 2001 From: Lester Hedges Date: Wed, 29 Jan 2025 13:58:08 +0000 Subject: [PATCH 096/102] Clarify that all windows are used when None. [ci skip] --- src/sire/mol/_dynamics.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/sire/mol/_dynamics.py b/src/sire/mol/_dynamics.py index 1d16046d4..e07863ae6 100644 --- a/src/sire/mol/_dynamics.py +++ b/src/sire/mol/_dynamics.py @@ -1609,7 +1609,8 @@ def run( can be achieved by simply removing any lambda values from 'lambda_windows' that you don't want, but this will result in an energy trajectory that only contains results for - the specified lambda values. + the specified lambda values. If None, then all lambda windows + will be used. null_energy: str The energy value to use for lambda windows that are not From fc30fb2ae6c152567e79dfcf978cb23141d5229b Mon Sep 17 00:00:00 2001 From: finlayclark Date: Wed, 29 Jan 2025 16:16:14 +0000 Subject: [PATCH 097/102] Fix hasForceSpecificEquation This now returns true if there is a default equation for the force. --- corelib/src/libs/SireCAS/lambdaschedule.cpp | 8 ++++++-- tests/cas/test_lambdaschedule.py | 8 ++++++++ 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/corelib/src/libs/SireCAS/lambdaschedule.cpp b/corelib/src/libs/SireCAS/lambdaschedule.cpp index 11eae8e35..e1cc50913 100644 --- a/corelib/src/libs/SireCAS/lambdaschedule.cpp +++ b/corelib/src/libs/SireCAS/lambdaschedule.cpp @@ -1032,8 +1032,12 @@ bool LambdaSchedule::hasForceSpecificEquation(const QString &stage, if (force == "*") return false; - else - return this->stage_equations[idx].contains(_get_lever_name(force, lever)); + + // If there is a default equation for this force, always return true + if (this->stage_equations[idx].contains(_get_lever_name(force, "*"))) + return true; + + return this->stage_equations[idx].contains(_get_lever_name(force, lever)); } SireCAS::Expression LambdaSchedule::_getEquation(int stage, diff --git a/tests/cas/test_lambdaschedule.py b/tests/cas/test_lambdaschedule.py index 92dddb70b..5348f4611 100644 --- a/tests/cas/test_lambdaschedule.py +++ b/tests/cas/test_lambdaschedule.py @@ -145,3 +145,11 @@ def test_lambdaschedule(): ) _assert_same_equation(l.lam(), l.get_equation(stage="scale_up"), morph5_equation) + +@pytest.mark.parametrize("force, lever, contained", [ + ("ghost-14", "kappa", True), + ("ghost-14", "epsilon", True) +]) +def test_has_force_specific_equation(force, lever, contained): + l = sr.cas.LambdaSchedule.standard_decouple() + assert l.has_force_specific_equation("decouple", force, lever) == contained \ No newline at end of file From 5c2dacfb78a0bb91a240d51b51c85d659d9f97a6 Mon Sep 17 00:00:00 2001 From: finlayclark Date: Wed, 29 Jan 2025 19:15:29 +0000 Subject: [PATCH 098/102] Update changelog [ci skip] --- doc/source/changelog.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/doc/source/changelog.rst b/doc/source/changelog.rst index e050d206d..19e3897b3 100644 --- a/doc/source/changelog.rst +++ b/doc/source/changelog.rst @@ -37,6 +37,9 @@ organisation on `GitHub `__. * Added support for angle and dihedral restraints which can be used in alchemical and standard simulations. +* Fix the ``hasForceSpecificEquation`` function in the ``LambdaLever`` class so that it returns true if + there is a default equation for the force. + `2024.3.1 `__ - December 2024 -------------------------------------------------------------------------------------------- From b29e1687d388cd744c3c3d1f383099b3234bb459 Mon Sep 17 00:00:00 2001 From: Lester Hedges Date: Mon, 3 Feb 2025 09:24:49 +0000 Subject: [PATCH 099/102] Add support for OpenMM MonteCarloMembraneBarostat. --- .../src/libs/SireMove/openmmfrenergydt.cpp | 45 ++++++++++--- corelib/src/libs/SireMove/openmmfrenergydt.h | 4 ++ .../src/libs/SireMove/openmmfrenergyst.cpp | 53 ++++++++++++---- corelib/src/libs/SireMove/openmmfrenergyst.h | 4 ++ .../src/libs/SireMove/openmmmdintegrator.cpp | 45 ++++++++++--- .../src/libs/SireMove/openmmmdintegrator.h | 4 ++ corelib/src/libs/SireMove/openmmpmefep.cpp | 63 ++++++++++++++----- corelib/src/libs/SireMove/openmmpmefep.h | 4 ++ doc/source/changelog.rst | 2 + wrapper/Move/OpenMMFrEnergyDT.pypp.cpp | 22 +++++++ wrapper/Move/OpenMMFrEnergyST.pypp.cpp | 25 +++++++- wrapper/Move/OpenMMMDIntegrator.pypp.cpp | 23 +++++++ wrapper/Move/OpenMMPMEFEP.pypp.cpp | 24 +++++++ wrapper/Tools/OpenMMMD.py | 10 +++ 14 files changed, 284 insertions(+), 44 deletions(-) diff --git a/corelib/src/libs/SireMove/openmmfrenergydt.cpp b/corelib/src/libs/SireMove/openmmfrenergydt.cpp index 8d2cee119..9a8695d1b 100644 --- a/corelib/src/libs/SireMove/openmmfrenergydt.cpp +++ b/corelib/src/libs/SireMove/openmmfrenergydt.cpp @@ -129,7 +129,7 @@ QDataStream &operator<<(QDataStream &ds, const OpenMMFrEnergyDT &velver) sds << velver.frequent_save_velocities << velver.molgroup << velver.solutegroup << velver.CutoffType << velver.cutoff_distance << velver.field_dielectric << velver.Andersen_flag << velver.Andersen_frequency - << velver.MCBarostat_flag << velver.MCBarostat_frequency << velver.ConstraintType << velver.Pressure + << velver.MCBarostat_flag << velver.MCBarostat_membrane_flag << velver.MCBarostat_frequency << velver.ConstraintType << velver.Pressure << velver.Temperature << velver.platform_type << velver.Restraint_flag << velver.CMMremoval_frequency << velver.energy_frequency << velver.device_index << velver.Alchemical_value << velver.coulomb_power << velver.shift_delta << velver.delta_alchemical << velver.buffer_coords << velver.gradients @@ -151,7 +151,7 @@ QDataStream &operator>>(QDataStream &ds, OpenMMFrEnergyDT &velver) sds >> velver.frequent_save_velocities >> velver.molgroup >> velver.solutegroup >> velver.CutoffType >> velver.cutoff_distance >> velver.field_dielectric >> velver.Andersen_flag >> velver.Andersen_frequency >> - velver.MCBarostat_flag >> velver.MCBarostat_frequency >> velver.ConstraintType >> velver.Pressure >> + velver.MCBarostat_flag >> velver.MCBarostat_membrane_flag >> velver.MCBarostat_frequency >> velver.ConstraintType >> velver.Pressure >> velver.Temperature >> velver.platform_type >> velver.Restraint_flag >> velver.CMMremoval_frequency >> velver.energy_frequency >> velver.device_index >> velver.Alchemical_value >> velver.coulomb_power >> velver.shift_delta >> velver.delta_alchemical >> velver.buffer_coords >> velver.gradients >> @@ -175,7 +175,7 @@ OpenMMFrEnergyDT::OpenMMFrEnergyDT(bool frequent_save) : ConcreteProperty(), frequent_save_velocities(frequent_save), molgroup(MoleculeGroup()), solutegroup(MoleculeGroup()), openmm_system(0), isInitialised(false), CutoffType("nocutoff"), cutoff_distance(1.0 * nanometer), field_dielectric(78.3), Andersen_flag(false), - Andersen_frequency(90.0), MCBarostat_flag(false), MCBarostat_frequency(25), ConstraintType("none"), + Andersen_frequency(90.0), MCBarostat_flag(false), MCBarostat_membrane_flag(false), MCBarostat_frequency(25), ConstraintType("none"), Pressure(1.0 * bar), Temperature(300.0 * kelvin), platform_type("Reference"), Restraint_flag(false), CMMremoval_frequency(0), energy_frequency(100), device_index("0"), Alchemical_value(0.5), coulomb_power(0), shift_delta(2.0), delta_alchemical(0.001), buffer_coords(false), gradients() @@ -188,7 +188,7 @@ OpenMMFrEnergyDT::OpenMMFrEnergyDT(const MoleculeGroup &molecule_group, const Mo : ConcreteProperty(), frequent_save_velocities(frequent_save), molgroup(molecule_group), solutegroup(solute_group), openmm_system(0), isInitialised(false), CutoffType("nocutoff"), cutoff_distance(1.0 * nanometer), field_dielectric(78.3), Andersen_flag(false), - Andersen_frequency(90.0), MCBarostat_flag(false), MCBarostat_frequency(25), ConstraintType("none"), + Andersen_frequency(90.0), MCBarostat_flag(false), MCBarostat_membrane_flag(false), MCBarostat_frequency(25), ConstraintType("none"), Pressure(1.0 * bar), Temperature(300.0 * kelvin), platform_type("Reference"), Restraint_flag(false), CMMremoval_frequency(0), energy_frequency(100), device_index("0"), Alchemical_value(0.5), coulomb_power(0), shift_delta(2.0), delta_alchemical(0.001), buffer_coords(false), gradients() @@ -201,7 +201,7 @@ OpenMMFrEnergyDT::OpenMMFrEnergyDT(const OpenMMFrEnergyDT &other) molgroup(other.molgroup), solutegroup(other.solutegroup), openmm_system(other.openmm_system), isInitialised(other.isInitialised), CutoffType(other.CutoffType), cutoff_distance(other.cutoff_distance), field_dielectric(other.field_dielectric), Andersen_flag(other.Andersen_flag), - Andersen_frequency(other.Andersen_frequency), MCBarostat_flag(other.MCBarostat_flag), + Andersen_frequency(other.Andersen_frequency), MCBarostat_flag(other.MCBarostat_flag), MCBarostat_membrane_flag(other.MCBarostat_membrane_flag), MCBarostat_frequency(other.MCBarostat_frequency), ConstraintType(other.ConstraintType), Pressure(other.Pressure), Temperature(other.Temperature), platform_type(other.platform_type), Restraint_flag(other.Restraint_flag), CMMremoval_frequency(other.CMMremoval_frequency), energy_frequency(other.energy_frequency), @@ -232,6 +232,7 @@ OpenMMFrEnergyDT &OpenMMFrEnergyDT::operator=(const OpenMMFrEnergyDT &other) Andersen_flag = other.Andersen_flag; Andersen_frequency = other.Andersen_frequency; MCBarostat_flag = other.MCBarostat_flag; + MCBarostat_membrane_flag = other.MCBarostat_membrane_flag; MCBarostat_frequency = other.MCBarostat_frequency; ConstraintType = other.ConstraintType; Pressure = other.Pressure; @@ -521,9 +522,21 @@ void OpenMMFrEnergyDT::initialise() const double converted_Temperature = convertTo(Temperature.value(), kelvin); const double converted_Pressure = convertTo(Pressure.value(), bar); - OpenMM::MonteCarloBarostat *barostat = - new OpenMM::MonteCarloBarostat(converted_Pressure, converted_Temperature, MCBarostat_frequency); - system_openmm->addForce(barostat); + if (MCBarostat_membrane_flag) + { + // Simple options for now: zero surface tension, XY isotropic, Z free + const double surface_Tension = 0; + OpenMM::MonteCarloMembraneBarostat::XYMode xymode = OpenMM::MonteCarloMembraneBarostat::XYIsotropic; + OpenMM::MonteCarloMembraneBarostat::ZMode zmode = OpenMM::MonteCarloMembraneBarostat::ZFree; + OpenMM::MonteCarloMembraneBarostat * barostat = new OpenMM::MonteCarloMembraneBarostat(converted_Pressure, surface_Tension, converted_Temperature, xymode, zmode, MCBarostat_frequency); + system_openmm->addForce(barostat); + } + else + { + OpenMM::MonteCarloBarostat *barostat = + new OpenMM::MonteCarloBarostat(converted_Pressure, converted_Temperature, MCBarostat_frequency); + system_openmm->addForce(barostat); + } if (Debug) { @@ -531,6 +544,10 @@ void OpenMMFrEnergyDT::initialise() qDebug() << "Temperature = " << converted_Temperature << " K\n"; qDebug() << "Pressure = " << converted_Pressure << " bar\n"; qDebug() << "Frequency every " << MCBarostat_frequency << " steps\n"; + if (MCBarostat_membrane_flag) + { + qDebug() << "Membrane barostat, surface tension 0, XY isotropic, Z free\n"; + } } } @@ -1795,6 +1812,18 @@ bool OpenMMFrEnergyDT::getMCBarostat(void) return MCBarostat_flag; } +/** Set Monte Carlo membrane Barostat on/off */ +void OpenMMFrEnergyDT::setMCBarostat_membrane(bool MCBarostat_membrane) +{ + MCBarostat_membrane_flag = MCBarostat_membrane; +} + +bool OpenMMFrEnergyDT::getMCBarostat_membrane(void) +{ + + return MCBarostat_membrane_flag; +} + /** Get the Monte Carlo Barostat frequency in time speps */ int OpenMMFrEnergyDT::getMCBarostat_frequency(void) { diff --git a/corelib/src/libs/SireMove/openmmfrenergydt.h b/corelib/src/libs/SireMove/openmmfrenergydt.h index 88332e308..dded8a7da 100644 --- a/corelib/src/libs/SireMove/openmmfrenergydt.h +++ b/corelib/src/libs/SireMove/openmmfrenergydt.h @@ -114,6 +114,9 @@ namespace SireMove void setMCBarostat_frequency(int); int getMCBarostat_frequency(void); + bool getMCBarostat_membrane(void); + void setMCBarostat_membrane(bool); + QString getConstraintType(void); void setConstraintType(QString); @@ -180,6 +183,7 @@ namespace SireMove double Andersen_frequency; bool MCBarostat_flag; + bool MCBarostat_membrane_flag; int MCBarostat_frequency; QString ConstraintType; diff --git a/corelib/src/libs/SireMove/openmmfrenergyst.cpp b/corelib/src/libs/SireMove/openmmfrenergyst.cpp index ded8dcf8f..a27495e36 100644 --- a/corelib/src/libs/SireMove/openmmfrenergyst.cpp +++ b/corelib/src/libs/SireMove/openmmfrenergyst.cpp @@ -130,7 +130,7 @@ QDataStream &operator<<(QDataStream &ds, const OpenMMFrEnergyST &velver) sds << velver.frequent_save_velocities << velver.molgroup << velver.solute << velver.solutehard << velver.solutetodummy << velver.solutefromdummy << velver.combiningRules << velver.CutoffType << velver.cutoff_distance << velver.field_dielectric << velver.Andersen_flag << velver.Andersen_frequency - << velver.MCBarostat_flag << velver.MCBarostat_frequency << velver.ConstraintType << velver.Pressure + << velver.MCBarostat_flag << velver.MCBarostat_membrane_flag << velver.MCBarostat_frequency << velver.ConstraintType << velver.Pressure << velver.Temperature << velver.platform_type << velver.Restraint_flag << velver.CMMremoval_frequency << velver.buffer_frequency << velver.energy_frequency << velver.device_index << velver.precision << velver.Alchemical_value << velver.coulomb_power << velver.shift_delta << velver.delta_alchemical @@ -152,7 +152,7 @@ QDataStream &operator>>(QDataStream &ds, OpenMMFrEnergyST &velver) sds >> velver.frequent_save_velocities >> velver.molgroup >> velver.solute >> velver.solutehard >> velver.solutetodummy >> velver.solutefromdummy >> velver.combiningRules >> velver.CutoffType >> velver.cutoff_distance >> velver.field_dielectric >> velver.Andersen_flag >> velver.Andersen_frequency >> - velver.MCBarostat_flag >> velver.MCBarostat_frequency >> velver.ConstraintType >> velver.Pressure >> + velver.MCBarostat_flag >> velver.MCBarostat_membrane_flag >> velver.MCBarostat_frequency >> velver.ConstraintType >> velver.Pressure >> velver.Temperature >> velver.platform_type >> velver.Restraint_flag >> velver.CMMremoval_frequency >> velver.buffer_frequency >> velver.energy_frequency >> velver.device_index >> velver.precision >> velver.Alchemical_value >> velver.coulomb_power >> velver.shift_delta >> velver.delta_alchemical >> @@ -180,7 +180,7 @@ OpenMMFrEnergyST::OpenMMFrEnergyST(bool frequent_save) solutefromdummy(MoleculeGroup()), openmm_system(0), openmm_context(0), isSystemInitialised(false), isContextInitialised(false), combiningRules("arithmetic"), CutoffType("nocutoff"), cutoff_distance(1.0 * nanometer), field_dielectric(78.3), Andersen_flag(false), Andersen_frequency(90.0), - MCBarostat_flag(false), MCBarostat_frequency(25), ConstraintType("none"), Pressure(1.0 * bar), + MCBarostat_flag(false), MCBarostat_membrane_flag(false), MCBarostat_frequency(25), ConstraintType("none"), Pressure(1.0 * bar), Temperature(300.0 * kelvin), platform_type("Reference"), Restraint_flag(false), CMMremoval_frequency(0), buffer_frequency(0), energy_frequency(100), device_index("0"), precision("single"), Alchemical_value(0.5), coulomb_power(0), shift_delta(2.0), delta_alchemical(0.001), alchemical_array(), finite_diff_gradients(), @@ -199,7 +199,7 @@ OpenMMFrEnergyST::OpenMMFrEnergyST(const MoleculeGroup &molecule_group, const Mo solutefromdummy(solute_fromdummy), openmm_system(0), openmm_context(0), isSystemInitialised(false), isContextInitialised(false), combiningRules("arithmetic"), CutoffType("nocutoff"), cutoff_distance(1.0 * nanometer), field_dielectric(78.3), Andersen_flag(false), Andersen_frequency(90.0), - MCBarostat_flag(false), MCBarostat_frequency(25), ConstraintType("none"), Pressure(1.0 * bar), + MCBarostat_flag(false), MCBarostat_membrane_flag(false), MCBarostat_frequency(25), ConstraintType("none"), Pressure(1.0 * bar), Temperature(300.0 * kelvin), platform_type("Reference"), Restraint_flag(false), CMMremoval_frequency(0), buffer_frequency(0), energy_frequency(100), device_index("0"), precision("single"), Alchemical_value(0.5), coulomb_power(0), shift_delta(2.0), delta_alchemical(0.001), alchemical_array(), finite_diff_gradients(), @@ -217,7 +217,7 @@ OpenMMFrEnergyST::OpenMMFrEnergyST(const OpenMMFrEnergyST &other) isSystemInitialised(other.isSystemInitialised), isContextInitialised(other.isContextInitialised), combiningRules(other.combiningRules), CutoffType(other.CutoffType), cutoff_distance(other.cutoff_distance), field_dielectric(other.field_dielectric), Andersen_flag(other.Andersen_flag), - Andersen_frequency(other.Andersen_frequency), MCBarostat_flag(other.MCBarostat_flag), + Andersen_frequency(other.Andersen_frequency), MCBarostat_flag(other.MCBarostat_flag), MCBarostat_membrane_flag(other.MCBarostat_membrane_flag), MCBarostat_frequency(other.MCBarostat_frequency), ConstraintType(other.ConstraintType), Pressure(other.Pressure), Temperature(other.Temperature), platform_type(other.platform_type), Restraint_flag(other.Restraint_flag), CMMremoval_frequency(other.CMMremoval_frequency), buffer_frequency(other.buffer_frequency), @@ -259,6 +259,7 @@ OpenMMFrEnergyST &OpenMMFrEnergyST::operator=(const OpenMMFrEnergyST &other) Andersen_flag = other.Andersen_flag; Andersen_frequency = other.Andersen_frequency; MCBarostat_flag = other.MCBarostat_flag; + MCBarostat_membrane_flag = other.MCBarostat_membrane_flag; MCBarostat_frequency = other.MCBarostat_frequency; ConstraintType = other.ConstraintType; Pressure = other.Pressure; @@ -302,7 +303,7 @@ bool OpenMMFrEnergyST::operator==(const OpenMMFrEnergyST &other) const combiningRules == other.combiningRules and CutoffType == other.CutoffType and cutoff_distance == other.cutoff_distance and field_dielectric == other.field_dielectric and Andersen_flag == other.Andersen_flag and Andersen_frequency == other.Andersen_frequency and - MCBarostat_flag == other.MCBarostat_flag and MCBarostat_frequency == other.MCBarostat_frequency and + MCBarostat_flag == other.MCBarostat_flag and MCBarostat_membrane_flag == other.MCBarostat_membrane_flag and MCBarostat_frequency == other.MCBarostat_frequency and ConstraintType == other.ConstraintType and Pressure == other.Pressure and Temperature == other.Temperature and platform_type == other.platform_type and Restraint_flag == other.Restraint_flag and CMMremoval_frequency == other.CMMremoval_frequency and @@ -1241,13 +1242,29 @@ void OpenMMFrEnergyST::initialise() const double converted_Temperature = convertTo(Temperature.value(), kelvin); const double converted_Pressure = convertTo(Pressure.value(), bar); - OpenMM::MonteCarloBarostat *barostat = - new OpenMM::MonteCarloBarostat(converted_Pressure, converted_Temperature, MCBarostat_frequency); + if (MCBarostat_membrane_flag == true) + { + // Simple options for now: zero surface tension, XY isotropic, Z free + const double surface_Tension = 0; + OpenMM::MonteCarloMembraneBarostat::XYMode xymode = OpenMM::MonteCarloMembraneBarostat::XYIsotropic; + OpenMM::MonteCarloMembraneBarostat::ZMode zmode = OpenMM::MonteCarloMembraneBarostat::ZFree; + OpenMM::MonteCarloMembraneBarostat * barostat = new OpenMM::MonteCarloMembraneBarostat(converted_Pressure, surface_Tension, converted_Temperature, xymode, zmode, MCBarostat_frequency); - // Set The random seed - barostat->setRandomNumberSeed(random_seed); + //Set The random seed + barostat->setRandomNumberSeed(random_seed); + + system_openmm->addForce(barostat); + } + else + { + OpenMM::MonteCarloBarostat *barostat = + new OpenMM::MonteCarloBarostat(converted_Pressure, converted_Temperature, MCBarostat_frequency); - system_openmm->addForce(barostat); + // Set The random seed + barostat->setRandomNumberSeed(random_seed); + + system_openmm->addForce(barostat); + } if (Debug) { @@ -1255,6 +1272,10 @@ void OpenMMFrEnergyST::initialise() qDebug() << "Temperature = " << converted_Temperature << " K\n"; qDebug() << "Pressure = " << converted_Pressure << " bar\n"; qDebug() << "Frequency every " << MCBarostat_frequency << " steps\n"; + if (MCBarostat_membrane_flag) + { + qDebug() << "Membrane barostat, surface tension 0, XY isotropic, Z free\n"; + } } } /*******************************************************BONDED @@ -4351,6 +4372,16 @@ bool OpenMMFrEnergyST::getMCBarostat(void) return MCBarostat_flag; } +void OpenMMFrEnergyST::setMCBarostatMembrane(bool MCBarostat_membrane) +{ + MCBarostat_membrane_flag = MCBarostat_membrane; +} + +bool OpenMMFrEnergyST::getMCBarostatMembrane(void) +{ + return MCBarostat_membrane_flag; +} + /** Get the Monte Carlo Barostat frequency in time speps */ int OpenMMFrEnergyST::getMCBarostatFrequency(void) { diff --git a/corelib/src/libs/SireMove/openmmfrenergyst.h b/corelib/src/libs/SireMove/openmmfrenergyst.h index fded8e087..15fd80027 100644 --- a/corelib/src/libs/SireMove/openmmfrenergyst.h +++ b/corelib/src/libs/SireMove/openmmfrenergyst.h @@ -122,6 +122,9 @@ namespace SireMove bool getMCBarostat(void); void setMCBarostat(bool); + bool getMCBarostatMembrane(void); + void setMCBarostatMembrane(bool); + void setMCBarostatFrequency(int); int getMCBarostatFrequency(void); @@ -247,6 +250,7 @@ namespace SireMove double Andersen_frequency; bool MCBarostat_flag; + bool MCBarostat_membrane_flag; int MCBarostat_frequency; QString ConstraintType; diff --git a/corelib/src/libs/SireMove/openmmmdintegrator.cpp b/corelib/src/libs/SireMove/openmmmdintegrator.cpp index ee6200919..094952fa9 100644 --- a/corelib/src/libs/SireMove/openmmmdintegrator.cpp +++ b/corelib/src/libs/SireMove/openmmmdintegrator.cpp @@ -124,7 +124,7 @@ QDataStream &operator<<(QDataStream &ds, const OpenMMMDIntegrator &velver) sds << velver.frequent_save_velocities << velver.molgroup << velver.Integrator_type << velver.friction << velver.CutoffType << velver.cutoff_distance << velver.field_dielectric << velver.tolerance_ewald_pme - << velver.Andersen_flag << velver.Andersen_frequency << velver.MCBarostat_flag << velver.MCBarostat_frequency + << velver.Andersen_flag << velver.Andersen_frequency << velver.MCBarostat_flag << velver.MCBarostat_membrane_flag << velver.MCBarostat_frequency << velver.ConstraintType << velver.Pressure << velver.Temperature << velver.platform_type << velver.Restraint_flag << velver.CMMremoval_frequency << velver.buffer_frequency << velver.device_index << velver.LJ_dispersion << velver.precision << velver.integration_tol << velver.timeskip @@ -147,7 +147,7 @@ QDataStream &operator>>(QDataStream &ds, OpenMMMDIntegrator &velver) sds >> velver.frequent_save_velocities >> velver.molgroup >> velver.Integrator_type >> velver.friction >> velver.CutoffType >> velver.cutoff_distance >> velver.field_dielectric >> velver.tolerance_ewald_pme >> - velver.Andersen_flag >> velver.Andersen_frequency >> velver.MCBarostat_flag >> + velver.Andersen_flag >> velver.Andersen_frequency >> velver.MCBarostat_flag >> velver.MCBarostat_membrane_flag >> velver.MCBarostat_frequency >> velver.ConstraintType >> velver.Pressure >> velver.Temperature >> velver.platform_type >> velver.Restraint_flag >> velver.CMMremoval_frequency >> velver.buffer_frequency >> velver.device_index >> velver.LJ_dispersion >> velver.precision >> velver.integration_tol >> @@ -195,7 +195,7 @@ OpenMMMDIntegrator::OpenMMMDIntegrator(bool frequent_save) molgroup(MoleculeGroup()), openmm_system(0), openmm_context(0), isSystemInitialised(false), isContextInitialised(false), Integrator_type("leapfrogverlet"), friction(1.0 / picosecond), CutoffType("nocutoff"), cutoff_distance(1.0 * nanometer), field_dielectric(78.3), tolerance_ewald_pme(0.0001), - Andersen_flag(false), Andersen_frequency(90.0), MCBarostat_flag(false), MCBarostat_frequency(25), + Andersen_flag(false), Andersen_frequency(90.0), MCBarostat_flag(false), MCBarostat_membrane_flag(false), MCBarostat_frequency(25), ConstraintType("none"), Pressure(1.0 * bar), Temperature(300.0 * kelvin), platform_type("Reference"), Restraint_flag(false), CMMremoval_frequency(0), buffer_frequency(0), device_index("0"), LJ_dispersion(true), precision("single"), reinetialise_context(false), integration_tol(0.001), timeskip(0.0 * picosecond), @@ -211,7 +211,7 @@ OpenMMMDIntegrator::OpenMMMDIntegrator(const MoleculeGroup &molecule_group, bool molgroup(molecule_group), openmm_system(0), openmm_context(0), isSystemInitialised(false), isContextInitialised(false), Integrator_type("leapfrogverlet"), friction(1.0 / picosecond), CutoffType("nocutoff"), cutoff_distance(1.0 * nanometer), field_dielectric(78.3), tolerance_ewald_pme(0.0001), - Andersen_flag(false), Andersen_frequency(90.0), MCBarostat_flag(false), MCBarostat_frequency(25), + Andersen_flag(false), Andersen_frequency(90.0), MCBarostat_flag(false), MCBarostat_membrane_flag(false), MCBarostat_frequency(25), ConstraintType("none"), Pressure(1.0 * bar), Temperature(300.0 * kelvin), platform_type("Reference"), Restraint_flag(false), CMMremoval_frequency(0), buffer_frequency(0), device_index("0"), LJ_dispersion(true), precision("single"), reinetialise_context(false), integration_tol(0.001), timeskip(0.0 * picosecond), @@ -229,7 +229,7 @@ OpenMMMDIntegrator::OpenMMMDIntegrator(const OpenMMMDIntegrator &other) Integrator_type(other.Integrator_type), friction(other.friction), CutoffType(other.CutoffType), cutoff_distance(other.cutoff_distance), field_dielectric(other.field_dielectric), tolerance_ewald_pme(other.tolerance_ewald_pme), Andersen_flag(other.Andersen_flag), - Andersen_frequency(other.Andersen_frequency), MCBarostat_flag(other.MCBarostat_flag), + Andersen_frequency(other.Andersen_frequency), MCBarostat_flag(other.MCBarostat_flag), MCBarostat_membrane_flag(other.MCBarostat_membrane_flag), MCBarostat_frequency(other.MCBarostat_frequency), ConstraintType(other.ConstraintType), Pressure(other.Pressure), Temperature(other.Temperature), platform_type(other.platform_type), Restraint_flag(other.Restraint_flag), CMMremoval_frequency(other.CMMremoval_frequency), buffer_frequency(other.buffer_frequency), @@ -264,6 +264,7 @@ OpenMMMDIntegrator &OpenMMMDIntegrator::operator=(const OpenMMMDIntegrator &othe Andersen_flag = other.Andersen_flag; Andersen_frequency = other.Andersen_frequency; MCBarostat_flag = other.MCBarostat_flag; + MCBarostat_membrane_flag = other.MCBarostat_membrane_flag; MCBarostat_frequency = other.MCBarostat_frequency; ConstraintType = other.ConstraintType; Pressure = other.Pressure; @@ -291,7 +292,7 @@ bool OpenMMMDIntegrator::operator==(const OpenMMMDIntegrator &other) const isSystemInitialised == other.isSystemInitialised and isContextInitialised == other.isContextInitialised and CutoffType == other.CutoffType and cutoff_distance == other.cutoff_distance and field_dielectric == other.field_dielectric and Andersen_flag == other.Andersen_flag and - Andersen_frequency == other.Andersen_frequency and MCBarostat_flag == other.MCBarostat_flag and + Andersen_frequency == other.Andersen_frequency and MCBarostat_flag == other.MCBarostat_flag and MCBarostat_membrane_flag == other.MCBarostat_membrane_flag and MCBarostat_frequency == other.MCBarostat_frequency and ConstraintType == other.ConstraintType and Pressure == other.Pressure and Temperature == other.Temperature and platform_type == other.platform_type and Restraint_flag == other.Restraint_flag and CMMremoval_frequency == other.CMMremoval_frequency and @@ -487,9 +488,21 @@ void OpenMMMDIntegrator::initialise() const double converted_Temperature = convertTo(Temperature.value(), kelvin); const double converted_Pressure = convertTo(Pressure.value(), bar); - OpenMM::MonteCarloBarostat *barostat = - new OpenMM::MonteCarloBarostat(converted_Pressure, converted_Temperature, MCBarostat_frequency); - system_openmm->addForce(barostat); + if (MCBarostat_membrane_flag == true) + { + // Simple options for now: zero surface tension, XY isotropic, Z free + const double surface_Tension = 0; + OpenMM::MonteCarloMembraneBarostat::XYMode xymode = OpenMM::MonteCarloMembraneBarostat::XYIsotropic; + OpenMM::MonteCarloMembraneBarostat::ZMode zmode = OpenMM::MonteCarloMembraneBarostat::ZFree; + OpenMM::MonteCarloMembraneBarostat * barostat = new OpenMM::MonteCarloMembraneBarostat(converted_Pressure, surface_Tension, converted_Temperature, xymode, zmode, MCBarostat_frequency); + system_openmm->addForce(barostat); + } + else // normal barostat + { + OpenMM::MonteCarloBarostat *barostat = + new OpenMM::MonteCarloBarostat(converted_Pressure, converted_Temperature, MCBarostat_frequency); + system_openmm->addForce(barostat); + } if (Debug) { @@ -498,6 +511,10 @@ void OpenMMMDIntegrator::initialise() qDebug() << "Pressure = " << converted_Pressure << " bar\n"; qDebug() << "Frequency every " << MCBarostat_frequency << " steps\n"; qDebug() << "Lennard Jones Dispersion term is set to " << LJ_dispersion << "\n"; + if (MCBarostat_membrane_flag) + { + qDebug() << "Membrane barostat, surface tension 0, XY isotropic, Z free\n"; + } } } @@ -1928,6 +1945,16 @@ bool OpenMMMDIntegrator::getMCBarostat(void) return MCBarostat_flag; } +void OpenMMMDIntegrator::setMCBarostatMembrane(bool MCBarostatMembrane) +{ + MCBarostat_membrane_flag = MCBarostatMembrane; +} + +bool OpenMMMDIntegrator::getMCBarostatMembrane(void) +{ + return MCBarostat_membrane_flag; +} + /** Get the Monte Carlo Barostat frequency in time speps */ int OpenMMMDIntegrator::getMCBarostatFrequency(void) { diff --git a/corelib/src/libs/SireMove/openmmmdintegrator.h b/corelib/src/libs/SireMove/openmmmdintegrator.h index 0e026ca9f..48953cb7b 100644 --- a/corelib/src/libs/SireMove/openmmmdintegrator.h +++ b/corelib/src/libs/SireMove/openmmmdintegrator.h @@ -127,6 +127,9 @@ namespace SireMove bool getMCBarostat(void); void setMCBarostat(bool); + bool getMCBarostatMembrane(void); + void setMCBarostatMembrane(bool); + void setMCBarostatFrequency(int); int getMCBarostatFrequency(void); @@ -203,6 +206,7 @@ namespace SireMove double Andersen_frequency; bool MCBarostat_flag; + bool MCBarostat_membrane_flag; int MCBarostat_frequency; QString ConstraintType; diff --git a/corelib/src/libs/SireMove/openmmpmefep.cpp b/corelib/src/libs/SireMove/openmmpmefep.cpp index f85086460..9b00f0b0d 100644 --- a/corelib/src/libs/SireMove/openmmpmefep.cpp +++ b/corelib/src/libs/SireMove/openmmpmefep.cpp @@ -144,7 +144,7 @@ QDataStream &operator<<(QDataStream &ds, const OpenMMPMEFEP &velver) sds << velver.frequent_save_velocities << velver.molgroup << velver.solute << velver.solutehard << velver.solutetodummy << velver.solutefromdummy << velver.combiningRules << velver.CutoffType << velver.cutoff_distance << velver.field_dielectric << velver.Andersen_flag << velver.Andersen_frequency - << velver.MCBarostat_flag << velver.MCBarostat_frequency << velver.ConstraintType << velver.Pressure + << velver.MCBarostat_flag << velver.MCBarostat_membrane_flag << velver.MCBarostat_frequency << velver.ConstraintType << velver.Pressure << velver.Temperature << velver.platform_type << velver.Restraint_flag << velver.CMMremoval_frequency << velver.buffer_frequency << velver.energy_frequency << velver.device_index << velver.precision << velver.current_lambda << velver.coulomb_power << velver.shift_delta << velver.delta_alchemical @@ -166,7 +166,7 @@ QDataStream &operator>>(QDataStream &ds, OpenMMPMEFEP &velver) sds >> velver.frequent_save_velocities >> velver.molgroup >> velver.solute >> velver.solutehard >> velver.solutetodummy >> velver.solutefromdummy >> velver.combiningRules >> velver.CutoffType >> velver.cutoff_distance >> velver.field_dielectric >> velver.Andersen_flag >> velver.Andersen_frequency >> - velver.MCBarostat_flag >> velver.MCBarostat_frequency >> velver.ConstraintType >> velver.Pressure >> + velver.MCBarostat_flag >> velver.MCBarostat_membrane_flag >> velver.MCBarostat_frequency >> velver.ConstraintType >> velver.Pressure >> velver.Temperature >> velver.platform_type >> velver.Restraint_flag >> velver.CMMremoval_frequency >> velver.buffer_frequency >> velver.energy_frequency >> velver.device_index >> velver.precision >> velver.current_lambda >> velver.coulomb_power >> velver.shift_delta >> velver.delta_alchemical >> @@ -194,7 +194,7 @@ OpenMMPMEFEP::OpenMMPMEFEP(bool frequent_save) solutefromdummy(MoleculeGroup()), openmm_system(0), openmm_context(0), isSystemInitialised(false), isContextInitialised(false), combiningRules("arithmetic"), CutoffType("PME"), cutoff_distance(1.0 * nanometer), field_dielectric(78.3), Andersen_flag(false), Andersen_frequency(90.0), MCBarostat_flag(false), - MCBarostat_frequency(25), ConstraintType("none"), Pressure(1.0 * bar), Temperature(300.0 * kelvin), + MCBarostat_membrane_flag(false), MCBarostat_frequency(25), ConstraintType("none"), Pressure(1.0 * bar), Temperature(300.0 * kelvin), platform_type("Reference"), Restraint_flag(false), CMMremoval_frequency(0), buffer_frequency(0), energy_frequency(100), device_index("0"), precision("single"), current_lambda(0.5), coulomb_power(0), shift_delta(2.0), delta_alchemical(0.001), alchemical_array(), finite_diff_gradients(), pot_energies(), @@ -212,7 +212,7 @@ OpenMMPMEFEP::OpenMMPMEFEP(const MoleculeGroup &molecule_group, const MoleculeGr solute(solute_group), solutehard(solute_hard), solutetodummy(solute_todummy), solutefromdummy(solute_fromdummy), openmm_system(0), openmm_context(0), isSystemInitialised(false), isContextInitialised(false), combiningRules("arithmetic"), CutoffType("PME"), cutoff_distance(1.0 * nanometer), field_dielectric(78.3), - Andersen_flag(false), Andersen_frequency(90.0), MCBarostat_flag(false), MCBarostat_frequency(25), + Andersen_flag(false), Andersen_frequency(90.0), MCBarostat_flag(false), MCBarostat_membrane_flag(false), MCBarostat_frequency(25), ConstraintType("none"), Pressure(1.0 * bar), Temperature(300.0 * kelvin), platform_type("Reference"), Restraint_flag(false), CMMremoval_frequency(0), buffer_frequency(0), energy_frequency(100), device_index("0"), precision("single"), current_lambda(0.5), coulomb_power(0), shift_delta(2.0), delta_alchemical(0.001), @@ -230,7 +230,7 @@ OpenMMPMEFEP::OpenMMPMEFEP(const OpenMMPMEFEP &other) isSystemInitialised(other.isSystemInitialised), isContextInitialised(other.isContextInitialised), combiningRules(other.combiningRules), CutoffType(other.CutoffType), cutoff_distance(other.cutoff_distance), field_dielectric(other.field_dielectric), Andersen_flag(other.Andersen_flag), - Andersen_frequency(other.Andersen_frequency), MCBarostat_flag(other.MCBarostat_flag), + Andersen_frequency(other.Andersen_frequency), MCBarostat_flag(other.MCBarostat_flag), MCBarostat_membrane_flag(other.MCBarostat_membrane_flag), MCBarostat_frequency(other.MCBarostat_frequency), ConstraintType(other.ConstraintType), Pressure(other.Pressure), Temperature(other.Temperature), platform_type(other.platform_type), Restraint_flag(other.Restraint_flag), CMMremoval_frequency(other.CMMremoval_frequency), buffer_frequency(other.buffer_frequency), @@ -272,6 +272,7 @@ OpenMMPMEFEP &OpenMMPMEFEP::operator=(const OpenMMPMEFEP &other) Andersen_flag = other.Andersen_flag; Andersen_frequency = other.Andersen_frequency; MCBarostat_flag = other.MCBarostat_flag; + MCBarostat_membrane_flag = other.MCBarostat_membrane_flag; MCBarostat_frequency = other.MCBarostat_frequency; ConstraintType = other.ConstraintType; Pressure = other.Pressure; @@ -315,7 +316,7 @@ bool OpenMMPMEFEP::operator==(const OpenMMPMEFEP &other) const combiningRules == other.combiningRules and CutoffType == other.CutoffType and cutoff_distance == other.cutoff_distance and field_dielectric == other.field_dielectric and Andersen_flag == other.Andersen_flag and Andersen_frequency == other.Andersen_frequency and - MCBarostat_flag == other.MCBarostat_flag and MCBarostat_frequency == other.MCBarostat_frequency and + MCBarostat_flag == other.MCBarostat_flag and MCBarostat_membrane_flag == other.MCBarostat_membrane_flag and MCBarostat_frequency == other.MCBarostat_frequency and ConstraintType == other.ConstraintType and Pressure == other.Pressure and Temperature == other.Temperature and platform_type == other.platform_type and Restraint_flag == other.Restraint_flag and CMMremoval_frequency == other.CMMremoval_frequency and @@ -386,17 +387,39 @@ void OpenMMPMEFEP::addMCBarostat(OpenMM::System &system) const double converted_Temperature = convertTo(Temperature.value(), kelvin); const double converted_Pressure = convertTo(Pressure.value(), bar); - auto barostat = new OpenMM::MonteCarloBarostat(converted_Pressure, converted_Temperature, MCBarostat_frequency); + if (MCBarostat_membrane_flag == true) + { + // Simple options for now: zero surface tension, XY isotropic, Z free + const double surface_Tension = 0; + OpenMM::MonteCarloMembraneBarostat::XYMode xymode = OpenMM::MonteCarloMembraneBarostat::XYIsotropic; + OpenMM::MonteCarloMembraneBarostat::ZMode zmode = OpenMM::MonteCarloMembraneBarostat::ZFree; + auto barostat = new OpenMM::MonteCarloMembraneBarostat(converted_Pressure, surface_Tension, converted_Temperature, xymode, zmode, MCBarostat_frequency); + + //Set The random seed + barostat->setRandomNumberSeed(random_seed); + + system.addForce(barostat); + } + else + { + auto barostat = new OpenMM::MonteCarloBarostat(converted_Pressure, converted_Temperature, MCBarostat_frequency); - barostat->setRandomNumberSeed(random_seed); - system.addForce(barostat); + // Set The random seed + barostat->setRandomNumberSeed(random_seed); + + system.addForce(barostat); + } if (Debug) { - qDebug() << "Monte Carlo Barostat set"; - qDebug() << "Temperature =" << converted_Temperature << " K"; - qDebug() << "Pressure =" << converted_Pressure << " bar"; - qDebug() << "Frequency every" << MCBarostat_frequency << " steps"; + qDebug() << "Monte Carlo Barostat set\n"; + qDebug() << "Temperature = " << converted_Temperature << " K\n"; + qDebug() << "Pressure = " << converted_Pressure << " bar\n"; + qDebug() << "Frequency every " << MCBarostat_frequency << " steps\n"; + if (MCBarostat_membrane_flag) + { + qDebug() << "Membrane barostat, surface tension 0, XY isotropic, Z free\n"; + } } } @@ -3665,8 +3688,8 @@ boost::tuples::tuple OpenMMPMEFEP::calculateGradient(dou { double double_increment = incr_plus - incr_minus; double gradient = 0; - double potential_energy_lambda_plus_delta; - double potential_energy_lambda_minus_delta; + double potential_energy_lambda_plus_delta = 0; + double potential_energy_lambda_minus_delta = 0; double forward_m; double backward_m; if (incr_plus < 1.0) @@ -3887,6 +3910,16 @@ bool OpenMMPMEFEP::getMCBarostat(void) return MCBarostat_flag; } +void OpenMMPMEFEP::setMCBarostatMembrane(bool MCBarostat_membrane) +{ + MCBarostat_membrane_flag = MCBarostat_membrane; +} + +bool OpenMMPMEFEP::getMCBarostatMembrane(void) +{ + return MCBarostat_membrane_flag; +} + /** Get the Monte Carlo Barostat frequency in time speps */ int OpenMMPMEFEP::getMCBarostatFrequency(void) { diff --git a/corelib/src/libs/SireMove/openmmpmefep.h b/corelib/src/libs/SireMove/openmmpmefep.h index a6c57ae3c..c73b7267e 100644 --- a/corelib/src/libs/SireMove/openmmpmefep.h +++ b/corelib/src/libs/SireMove/openmmpmefep.h @@ -127,6 +127,9 @@ namespace SireMove bool getMCBarostat(void); void setMCBarostat(bool); + bool getMCBarostatMembrane(void); + void setMCBarostatMembrane(bool); + void setMCBarostatFrequency(int); int getMCBarostatFrequency(void); @@ -250,6 +253,7 @@ namespace SireMove double Andersen_frequency; bool MCBarostat_flag; + bool MCBarostat_membrane_flag; int MCBarostat_frequency; QString ConstraintType; diff --git a/doc/source/changelog.rst b/doc/source/changelog.rst index 82d41c920..5ff22ceb8 100644 --- a/doc/source/changelog.rst +++ b/doc/source/changelog.rst @@ -42,6 +42,8 @@ organisation on `GitHub `__. * Fix the ``hasForceSpecificEquation`` function in the ``LambdaLever`` class so that it returns true if there is a default equation for the force. +* Added support for the ``OpenMM`` ``MonteCarloMembraneBarostat`` to ``SOMD``. + `2024.3.1 `__ - December 2024 -------------------------------------------------------------------------------------------- diff --git a/wrapper/Move/OpenMMFrEnergyDT.pypp.cpp b/wrapper/Move/OpenMMFrEnergyDT.pypp.cpp index 98270db10..2967c7709 100644 --- a/wrapper/Move/OpenMMFrEnergyDT.pypp.cpp +++ b/wrapper/Move/OpenMMFrEnergyDT.pypp.cpp @@ -309,6 +309,16 @@ void register_OpenMMFrEnergyDT_class(){ , "" ); } + { //::SireMove::OpenMMFrEnergyDT::getMCBarostat_membrane + + typedef bool ( ::SireMove::OpenMMFrEnergyDT::*getMCBarostat_membrane_function_type)( ) ; + getMCBarostat_membrane_function_type getMCBarostat_membrane_function_value( &::SireMove::OpenMMFrEnergyDT::getMCBarostat_membrane ); + + OpenMMFrEnergyDT_exposer.def( + "getMCBarostat_membrane" + , getMCBarostat_membrane_function_value + , "" ); + } { //::SireMove::OpenMMFrEnergyDT::getMCBarostat_frequency typedef int ( ::SireMove::OpenMMFrEnergyDT::*getMCBarostat_frequency_function_type)( ) ; @@ -614,6 +624,18 @@ void register_OpenMMFrEnergyDT_class(){ , bp::release_gil_policy() , "Set Monte Carlo Barostat onoff" ); + } + { //::SireMove::OpenMMFrEnergyDT::setMCBarostat_membrane + + typedef void ( ::SireMove::OpenMMFrEnergyDT::*setMCBarostat_membrane_function_type)( bool ) ; + setMCBarostat_membrane_function_type setMCBarostat_membrane_function_value( &::SireMove::OpenMMFrEnergyDT::setMCBarostat_membrane ); + + OpenMMFrEnergyDT_exposer.def( + "setMCBarostat_membrane" + , setMCBarostat_membrane_function_value + , ( bp::arg("arg0") ) + , "Set Monte Carlo Membrane Barostat onoff (only has an effect if Monte Carlo Barostat is on)" ); + } { //::SireMove::OpenMMFrEnergyDT::setMCBarostat_frequency diff --git a/wrapper/Move/OpenMMFrEnergyST.pypp.cpp b/wrapper/Move/OpenMMFrEnergyST.pypp.cpp index 7bd2c4502..a7bf2cf5d 100644 --- a/wrapper/Move/OpenMMFrEnergyST.pypp.cpp +++ b/wrapper/Move/OpenMMFrEnergyST.pypp.cpp @@ -411,6 +411,17 @@ void register_OpenMMFrEnergyST_class(){ , bp::release_gil_policy() , "" ); + } + { //::SireMove::OpenMMFrEnergyST::getMCBarostatMembrane + + typedef bool ( ::SireMove::OpenMMFrEnergyST::*getMCBarostatMembrane_function_type)( ) ; + getMCBarostatMembrane_function_type getMCBarostatMembrane_function_value( &::SireMove::OpenMMFrEnergyST::getMCBarostatMembrane ); + + OpenMMFrEnergyST_exposer.def( + "getMCBarostatMembrane" + , getMCBarostatMembrane_function_value + , "" ); + } { //::SireMove::OpenMMFrEnergyST::getMCBarostatFrequency @@ -869,6 +880,18 @@ void register_OpenMMFrEnergyST_class(){ , bp::release_gil_policy() , "Set Monte Carlo Barostat onoff" ); + } + { //::SireMove::OpenMMFrEnergyST::setMCBarostatMembrane + + typedef void ( ::SireMove::OpenMMFrEnergyST::*setMCBarostatMembrane_function_type)( bool ) ; + setMCBarostatMembrane_function_type setMCBarostatMembrane_function_value( &::SireMove::OpenMMFrEnergyST::setMCBarostatMembrane ); + + OpenMMFrEnergyST_exposer.def( + "setMCBarostatMembrane" + , setMCBarostatMembrane_function_value + , ( bp::arg("arg0") ) + , "Set Monte Carlo Membrane Barostat onoff (only has an effect if Monte Carlo Barostat is on)" ); + } { //::SireMove::OpenMMFrEnergyST::setMCBarostatFrequency @@ -880,7 +903,7 @@ void register_OpenMMFrEnergyST_class(){ , setMCBarostatFrequency_function_value , ( bp::arg("arg0") ) , bp::release_gil_policy() - , "Set the Monte Carlo Barostat frequency in time speps" ); + , "Set the Monte Carlo Barostat frequency in time steps" ); } { //::SireMove::OpenMMFrEnergyST::setPlatform diff --git a/wrapper/Move/OpenMMMDIntegrator.pypp.cpp b/wrapper/Move/OpenMMMDIntegrator.pypp.cpp index d8a3e6b89..e577211bf 100644 --- a/wrapper/Move/OpenMMMDIntegrator.pypp.cpp +++ b/wrapper/Move/OpenMMMDIntegrator.pypp.cpp @@ -322,6 +322,17 @@ void register_OpenMMMDIntegrator_class(){ , "" ); } + { //::SireMove::OpenMMMDIntegrator::getMCBarostatMembrane + + typedef bool ( ::SireMove::OpenMMMDIntegrator::*getMCBarostatMembrane_function_type)( ) ; + getMCBarostatMembrane_function_type getMCBarostatMembrane_function_value( &::SireMove::OpenMMMDIntegrator::getMCBarostatMembrane ); + + OpenMMMDIntegrator_exposer.def( + "getMCBarostatMembrane" + , getMCBarostatMembrane_function_value + , "" ); + } + { //::SireMove::OpenMMMDIntegrator::getMCBarostatFrequency typedef int ( ::SireMove::OpenMMMDIntegrator::*getMCBarostatFrequency_function_type)( ) ; @@ -677,6 +688,18 @@ void register_OpenMMMDIntegrator_class(){ , bp::release_gil_policy() , "Set Monte Carlo Barostat onoff" ); + } + { //::SireMove::OpenMMMDIntegrator::setMCBarostatMembrane + + typedef void ( ::SireMove::OpenMMMDIntegrator::*setMCBarostatMembrane_function_type)( bool ) ; + setMCBarostatMembrane_function_type setMCBarostatMembrane_function_value( &::SireMove::OpenMMMDIntegrator::setMCBarostatMembrane ); + + OpenMMMDIntegrator_exposer.def( + "setMCBarostatMembrane" + , setMCBarostatMembrane_function_value + , ( bp::arg("arg0") ) + , "Set Monte Carlo Membrane Barostat onoff (only has an effect if Monte Carlo Barostat is on)" ); + } { //::SireMove::OpenMMMDIntegrator::setMCBarostatFrequency diff --git a/wrapper/Move/OpenMMPMEFEP.pypp.cpp b/wrapper/Move/OpenMMPMEFEP.pypp.cpp index c70629b6f..7b786da06 100644 --- a/wrapper/Move/OpenMMPMEFEP.pypp.cpp +++ b/wrapper/Move/OpenMMPMEFEP.pypp.cpp @@ -414,6 +414,17 @@ void register_OpenMMPMEFEP_class(){ , "" ); } + { //::SireMove::OpenMMPMEFEP::getMCBarostatMembrane + + typedef bool ( ::SireMove::OpenMMPMEFEP::*getMCBarostatMembrane_function_type)( ) ; + getMCBarostatMembrane_function_type getMCBarostatMembrane_function_value( &::SireMove::OpenMMPMEFEP::getMCBarostatMembrane ); + + OpenMMPMEFEP_exposer.def( + "getMCBarostatMembrane" + , getMCBarostatMembrane_function_value + , bp::release_gil_policy() + , "" ); + } { //::SireMove::OpenMMPMEFEP::getMCBarostatFrequency typedef int ( ::SireMove::OpenMMPMEFEP::*getMCBarostatFrequency_function_type)( ) ; @@ -858,6 +869,19 @@ void register_OpenMMPMEFEP_class(){ , bp::release_gil_policy() , "Set Monte Carlo Barostat onoff" ); + } + { //::SireMove::OpenMMPMEFEP::setMCBarostatMembrane + + typedef void ( ::SireMove::OpenMMPMEFEP::*setMCBarostatMembrane_function_type)( bool ) ; + setMCBarostatMembrane_function_type setMCBarostatMembrane_function_value( &::SireMove::OpenMMPMEFEP::setMCBarostatMembrane ); + + OpenMMPMEFEP_exposer.def( + "setMCBarostatMembrane" + , setMCBarostatMembrane_function_value + , ( bp::arg("arg0") ) + , bp::release_gil_policy() + , "Set Monte Carlo Membrane Barostat onoff (only has an effect if Monte Carlo Barostat is on)" ); + } { //::SireMove::OpenMMPMEFEP::setMCBarostatFrequency diff --git a/wrapper/Tools/OpenMMMD.py b/wrapper/Tools/OpenMMMD.py index 45f7017ee..97fe9e401 100644 --- a/wrapper/Tools/OpenMMMD.py +++ b/wrapper/Tools/OpenMMMD.py @@ -268,6 +268,12 @@ """Whether or not to use a barostat (needed for NPT simulation).""", ) +barostat_membrane = Parameter( + "membrane barostat", + False, + """Whether the barostat is a membrane barostat (needed for membrane simulation).""", +) + andersen_frequency = Parameter( "andersen frequency", 10.0, """Collision frequency in units of (1/ps)""" ) @@ -824,6 +830,8 @@ def setupMoves(system, debug_seed, GPUS): if barostat.val: Integrator_OpenMM.setPressure(pressure.val) Integrator_OpenMM.setMCBarostat(barostat.val) + if barostat_membrane.val: + Integrator_OpenMM.setMCBarostatMembrane(barostat_membrane.val) Integrator_OpenMM.setMCBarostatFrequency(barostat_frequency.val) # print Integrator_OpenMM.getDeviceIndex() @@ -2145,6 +2153,8 @@ def setupMovesFreeEnergy(system, debug_seed, gpu_idx, lam_val): if barostat.val: Integrator_OpenMM.setPressure(pressure.val) Integrator_OpenMM.setMCBarostat(barostat.val) + if barostat_membrane.val: + Integrator_OpenMM.setMCBarostatMembrane(barostat_membrane.val) Integrator_OpenMM.setMCBarostatFrequency(barostat_frequency.val) # Choose a random seed for Sire if a debugging seed hasn't been set. From 8e8582b80f394f033bb5056053ce4fbfc08e5d7e Mon Sep 17 00:00:00 2001 From: Lester Hedges Date: Tue, 4 Feb 2025 10:49:51 +0000 Subject: [PATCH 100/102] Update CHANGELOG for the 2024.4.0 release. --- doc/source/changelog.rst | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/doc/source/changelog.rst b/doc/source/changelog.rst index 5ff22ceb8..1ee8d41cc 100644 --- a/doc/source/changelog.rst +++ b/doc/source/changelog.rst @@ -12,16 +12,14 @@ Development was migrated into the `OpenBioSim `__ organisation on `GitHub `__. -`2024.4.0 `__ - December 2024 ---------------------------------------------------------------------------------------------- - -* Please add an item to this CHANGELOG for any new features or bug fixes when creating a PR. +`2024.4.0 `__ - Feb 2025 +---------------------------------------------------------------------------------------- * Fixed update of triclinic box vectors in ``SOMD`` following ``OpenMM`` bug fix. * Don't automatically save energies and frames when ``dynamics.run()`` returns. -* Improved handling of NaN errors during dynamics. +* Improved handling of ``OpenMM`` NaN errors during dynamics. * Restore thread state before raising exceptions in the Sire OpenMM minimiser. From 4199df487561ac20f6f9456bd092c9416b943603 Mon Sep 17 00:00:00 2001 From: Lester Hedges Date: Tue, 4 Feb 2025 10:51:39 +0000 Subject: [PATCH 101/102] Bump version. --- version.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/version.txt b/version.txt index 75d5f7f71..3f841c887 100644 --- a/version.txt +++ b/version.txt @@ -1 +1 @@ -2024.4.0.dev +2024.4.0 From 77a4447d7a0baa87666ac0e799d325f80778ee94 Mon Sep 17 00:00:00 2001 From: Lester Hedges Date: Tue, 4 Feb 2025 13:18:29 +0000 Subject: [PATCH 102/102] Handle zero and NoneType for dynamics save frequencies. [closes #283] --- src/sire/mol/_dynamics.py | 49 +++++++++++++++++++++++++++++++++++++-- 1 file changed, 47 insertions(+), 2 deletions(-) diff --git a/src/sire/mol/_dynamics.py b/src/sire/mol/_dynamics.py index e07863ae6..5403075e9 100644 --- a/src/sire/mol/_dynamics.py +++ b/src/sire/mol/_dynamics.py @@ -1001,8 +1001,13 @@ def run( save_frequency = 25 else: save_frequency = save_frequency.to(picosecond) + no_save = False + else: + no_save = True + save_frequency = steps_to_run + 1 if energy_frequency != 0: + no_save_energy = False if energy_frequency is None: if self._map.specified("energy_frequency"): energy_frequency = ( @@ -1010,10 +1015,15 @@ def run( ) else: energy_frequency = save_frequency + no_save_energy = no_save else: energy_frequency = energy_frequency.to(picosecond) + else: + energy_frequency = steps_to_run + 1 + no_save_energy = True if frame_frequency != 0: + no_save_frame = False if frame_frequency is None: if self._map.specified("frame_frequency"): frame_frequency = ( @@ -1021,8 +1031,12 @@ def run( ) else: frame_frequency = save_frequency + no_save_frame = no_save else: frame_frequency = frame_frequency.to(picosecond) + else: + frame_frequency = steps_to_run + 1 + no_save_frame = True completed = 0 @@ -1095,9 +1109,38 @@ class NeedsMinimiseError(Exception): pass nsteps_before_run = self._current_step + # if this is the first call, then set the save frequencies if nsteps_before_run == 0: self._next_save_frame = frame_frequency_steps self._next_save_energy = energy_frequency_steps + self._prev_frame_frequency_steps = frame_frequency_steps + self._prev_energy_frequency_steps = energy_frequency_steps + self._prev_no_frame = no_save_frame + self._prev_no_energy = no_save_energy + # handle adjustments to the save frequencies + else: + if frame_frequency_steps != self._prev_frame_frequency_steps: + if self._prev_no_frame: + self._next_save_frame = nsteps_before_run + frame_frequency_steps + else: + self._next_save_frame = ( + self._next_save_frame + + frame_frequency_steps + - self._prev_frame_frequency_steps + ) + if energy_frequency_steps != self._prev_energy_frequency_steps: + if self._prev_no_energy: + self._next_save_energy = nsteps_before_run + energy_frequency_steps + else: + self._next_save_energy = ( + self._next_save_energy + + energy_frequency_steps + - self._prev_energy_frequency_steps + ) + self._prev_no_frame = no_save_frame + self._prev_frame_frequency_steps = frame_frequency_steps + self._prev_no_energy = no_save_energy + self._prev_energy_frequency_steps = energy_frequency_steps from ..base import ProgressBar from ..units import second @@ -1123,7 +1166,8 @@ class NeedsMinimiseError(Exception): ): save_frame = True self._next_save_frame += frame_frequency_steps - block_size = frame_frequency_steps + if frame_frequency_steps < block_size: + block_size = frame_frequency_steps else: save_frame = False @@ -1136,7 +1180,8 @@ class NeedsMinimiseError(Exception): ): save_energy = True self._next_save_energy += energy_frequency_steps - block_size = energy_frequency_steps + if energy_frequency_steps < block_size: + block_size = energy_frequency_steps else: save_energy = False

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 literal 43201 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 A$N&HU From 1a3842201785e6caba6802739a8415fa4a02e38a Mon Sep 17 00:00:00 2001 From: Matthew Date: Mon, 20 Jan 2025 16:30:06 +0000 Subject: [PATCH 089/102] Subplots in code snippets in line with new plots [ci skip] --- doc/source/tutorial/part06/02_alchemical_dynamics.rst | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/doc/source/tutorial/part06/02_alchemical_dynamics.rst b/doc/source/tutorial/part06/02_alchemical_dynamics.rst index 9ee68ec1e..39e309e14 100644 --- a/doc/source/tutorial/part06/02_alchemical_dynamics.rst +++ b/doc/source/tutorial/part06/02_alchemical_dynamics.rst @@ -224,7 +224,7 @@ You can plot how this schedule would morph the perturbable properties using the :func:`~sire.cas.LambdaSchedule.plot` function, e.g. >>> df = get_lever_values(initial=2.0, final=3.0) ->>> df.plot() +>>> df.plot(subplots=True) .. image:: images/06_02_01.jpg :alt: View of the default λ-schedule @@ -249,7 +249,7 @@ This shows that in the default ``morph`` stage of this schedule, we will scale the ``charge`` parameters by λ^2, while all other parameters (the default) will scale using λ. We can plot the result of this using; ->>> s.get_lever_values(initial=2.0, final=3.0).plot() +>>> s.get_lever_values(initial=2.0, final=3.0).plot(subplots=True) .. image:: images/06_02_02.jpg :alt: View of the λ-schedule where charge is scaled by λ^2 @@ -279,7 +279,7 @@ LambdaSchedule( Again, it is worth plotting the impact of this schedule on a hypothetical parameter. ->>> s.get_lever_values(initial=2.0, final=3.0).plot() +>>> s.get_lever_values(initial=2.0, final=3.0).plot(subplots=True) .. image:: images/06_02_03.jpg :alt: View of the λ-schedule where charge is scaled in a second stage to zero. @@ -334,7 +334,7 @@ by γ, which is set to 0.2 for all stages. We can see how this would affect a hypothetical parameter that goes from an ``initial`` value of 2.0 to a ``final`` value of 3.0 via ->>> s.get_lever_values(initial=2.0, final=3.0).plot() +>>> s.get_lever_values(initial=2.0, final=3.0).plot(subplots=True) .. image:: images/06_02_04.jpg :alt: View of the λ-schedule that sandwiches a standard morph stage From d5bf9b5ac9f296b1e84e392cc706edd6a09e1ec6 Mon Sep 17 00:00:00 2001 From: Matthew Date: Tue, 21 Jan 2025 09:53:49 +0000 Subject: [PATCH 090/102] Reduce image sizes [ci skip] --- .../tutorial/part06/images/06_02_01.jpg | Bin 40150 -> 28434 bytes .../tutorial/part06/images/06_02_02.jpg | Bin 53711 -> 41637 bytes .../tutorial/part06/images/06_02_03.jpg | Bin 51881 -> 39435 bytes .../tutorial/part06/images/06_02_04.jpg | Bin 53953 -> 41514 bytes 4 files changed, 0 insertions(+), 0 deletions(-) diff --git a/doc/source/tutorial/part06/images/06_02_01.jpg b/doc/source/tutorial/part06/images/06_02_01.jpg index dbac039e38c0c5fe234aee73a096f23e57176af4..461cb16ebf59fabef1080e02b0cbb7642132c120 100644 GIT binary patch literal 28434 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 literal 40150 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 MEC1weCBhBS8iu5kM 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 literal 53711 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 literal 51881 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 YAkbxnZ%pL}1pL_o{a3zE|84UB0nZ}06#xJL literal 53953 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