diff --git a/.github/workflows/pr.yaml b/.github/workflows/pr.yaml index 0f4d8e4e8..0574186e8 100644 --- a/.github/workflows/pr.yaml +++ b/.github/workflows/pr.yaml @@ -45,6 +45,7 @@ jobs: SIRE_SILENT_PHONEHOME: 1 SIRE_EMLE: 1 REPO: "${{ github.event.pull_request.head.repo.full_name || github.repository }}" + SIRE_REMOTE: "https://github.com/${{ github.event.pull_request.head.repo.full_name || github.repository }}.git" steps: # - uses: actions/checkout@v4 diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 000000000..b2406c49d --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,38 @@ +repos: + # General file quality checks (Python source files only) + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v5.0.0 + hooks: + - id: trailing-whitespace + files: ^(src|tests|wrapper)/ + types: [python] + - id: end-of-file-fixer + files: ^(src|tests|wrapper)/ + types: [python] + - id: check-added-large-files + args: [--maxkb=1000] + - id: check-merge-conflict + + # Python formatting and linting (src/, tests/, and wrapper/ Python files) + - repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.15.4 + hooks: + # Run the formatter + - id: ruff-format + files: ^(src|tests|wrapper)/ + # Run the linter (auto-fix but don't block commits) + - id: ruff + files: ^(src|tests|wrapper)/ + args: [--fix, --exit-zero] + + # C++ formatting (corelib/ and wrapper/ only) + # Runs clang-format only on files staged for commit, so the codebase + # drifts gradually toward the .clang-format standard rather than + # requiring a blanket one-time reformatting pass. + # NOTE: do NOT run 'pre-commit run --all-files' for C++ — use per-file + # formatting instead to preserve the drift-based approach. + - repo: https://github.com/pre-commit/mirrors-clang-format + rev: v22.1.3 + hooks: + - id: clang-format + files: ^(corelib|wrapper)/.*\.(cpp|h|hpp)$ diff --git a/README.rst b/README.rst index 7ea785b2c..c9314975d 100644 --- a/README.rst +++ b/README.rst @@ -166,6 +166,22 @@ Developers guide Please `visit the website `__ for information on how to develop applications using sire. +The repository uses `pre-commit `__ to enforce +consistent code style. After setting up your development environment, install +the hooks with: + +.. code-block:: bash + + pre-commit install + +Python code in ``src/``, ``tests/``, and ``wrapper/`` is formatted with +`ruff `__; C++ code in ``corelib/`` and +``wrapper/`` is formatted with +`clang-format `__ using the +``.clang-format`` file in the repository root. The C++ formatter runs only +on files staged for commit, allowing the codebase to drift gradually toward +a consistent style without a blanket one-time reformatting. + GitHub actions -------------- Since sire is quite large, a build can take quite long and might not be neccessary diff --git a/actions/generate_recipe.py b/actions/generate_recipe.py index 09be4c500..90b5dc3e2 100644 --- a/actions/generate_recipe.py +++ b/actions/generate_recipe.py @@ -91,11 +91,20 @@ def get_git_info(srcdir): """Get the git remote URL and branch/tag.""" gitdir = os.path.join(srcdir, ".git") - remote = run_cmd( - f"git --git-dir={gitdir} --work-tree={srcdir} config --get remote.origin.url" - ) - if not remote.endswith(".git"): - remote += ".git" + # For PR builds from external forks the checkout remote points to the base + # repo, not the fork. The workflow sets SIRE_REMOTE to the fork's clone + # URL so that rattler-build fetches from the right place. + env_remote = os.environ.get("SIRE_REMOTE") + if env_remote: + remote = env_remote + if not remote.endswith(".git"): + remote += ".git" + else: + remote = run_cmd( + f"git --git-dir={gitdir} --work-tree={srcdir} config --get remote.origin.url" + ) + if not remote.endswith(".git"): + remote += ".git" branch = run_cmd( f"git --git-dir={gitdir} --work-tree={srcdir} rev-parse --abbrev-ref HEAD" diff --git a/corelib/CMakeLists.txt b/corelib/CMakeLists.txt index dd1d5f79e..340056dbe 100644 --- a/corelib/CMakeLists.txt +++ b/corelib/CMakeLists.txt @@ -649,6 +649,9 @@ elseif (MSVC) add_compile_options("/EHsc") add_compile_options("/permissive-") add_compile_options("/Zc:twoPhase-") + # Provide stdext::make_checked_array_iterator stub for MSVC 14.38+ where it was removed. + # Qt5's qvector.h uses it unconditionally on MSVC, so force-include a polyfill. + add_compile_options("/FI${CMAKE_CURRENT_SOURCE_DIR}/build/cmake/stdext_compat.h") message( STATUS "Compiling with MSVC" ) set( SIRE_COMPILER "MSVC" ) diff --git a/corelib/build/cmake/stdext_compat.h b/corelib/build/cmake/stdext_compat.h new file mode 100644 index 000000000..ec76cc3e2 --- /dev/null +++ b/corelib/build/cmake/stdext_compat.h @@ -0,0 +1,22 @@ +#pragma once +// stdext::make_checked_array_iterator and make_unchecked_array_iterator were +// removed in MSVC 14.38 (VS 2022 17.8). Qt5's qcompilerdetection.h maps +// QT_MAKE_CHECKED_ARRAY_ITERATOR and QT_MAKE_UNCHECKED_ARRAY_ITERATOR to these +// functions unconditionally on MSVC, so provide no-op stubs. +#if defined(__cplusplus) && defined(_MSC_VER) && _MSC_VER >= 1938 +#include +namespace stdext +{ + template + T *make_checked_array_iterator(T *ptr, std::size_t, std::size_t index = 0) + { + return ptr + index; + } + + template + T *make_unchecked_array_iterator(T *ptr) + { + return ptr; + } +} +#endif diff --git a/corelib/src/libs/SireCAS/lambdaschedule.cpp b/corelib/src/libs/SireCAS/lambdaschedule.cpp index 1c0b43d71..bcca60fa1 100644 --- a/corelib/src/libs/SireCAS/lambdaschedule.cpp +++ b/corelib/src/libs/SireCAS/lambdaschedule.cpp @@ -28,6 +28,8 @@ #include "lambdaschedule.h" +#include "SireCAS/identities.h" +#include "SireCAS/symbolexpression.h" #include "SireCAS/values.h" #include "SireBase/console.h" @@ -65,7 +67,7 @@ static RegisterMetaType r_schedule; QDataStream &operator<<(QDataStream &ds, const LambdaSchedule &schedule) { - writeHeader(ds, r_schedule, 3); + writeHeader(ds, r_schedule, 5); SharedDataStream sds(ds); @@ -75,6 +77,8 @@ QDataStream &operator<<(QDataStream &ds, const LambdaSchedule &schedule) << schedule.default_equations << schedule.stage_equations << schedule.mol_schedules + << schedule.coupled_levers + << schedule.stage_weights << static_cast(schedule); return ds; @@ -91,21 +95,35 @@ QDataStream &operator>>(QDataStream &ds, LambdaSchedule &schedule) { VersionID v = readHeader(ds, r_schedule); - if (v == 1 or v == 2 or v == 3) + if (v == 1 or v == 2 or v == 3 or v == 4 or v == 5) { SharedDataStream sds(ds); sds >> schedule.constant_values; - if (v == 3) + if (v >= 3) sds >> schedule.force_names; sds >> schedule.lever_names >> schedule.stage_names >> schedule.default_equations >> schedule.stage_equations; - if (v == 2 or v == 3) + if (v >= 2) sds >> schedule.mol_schedules; + if (v >= 4) + sds >> schedule.coupled_levers; + else + { + // Populate the default coupling for streams written before v4 + schedule.coupled_levers[_get_lever_name("cmap", "cmap_grid")] = + _get_lever_name("torsion", "torsion_k"); + } + + if (v >= 5) + sds >> schedule.stage_weights; + else + schedule.stage_weights = QVector(schedule.stage_names.count(), 1.0); + if (v < 3) { // need to make sure that the lever names are namespaced @@ -146,13 +164,17 @@ QDataStream &operator>>(QDataStream &ds, LambdaSchedule &schedule) } } else - throw version_error(v, "1, 2, 3", r_schedule, CODELOC); + throw version_error(v, "1, 2, 3, 4, 5", r_schedule, CODELOC); return ds; } LambdaSchedule::LambdaSchedule() : ConcreteProperty() { + // By default, cmap_grid falls back to torsion_k so that customising + // torsion scaling automatically keeps the CMAP correction in sync. + coupled_levers[_get_lever_name("cmap", "cmap_grid")] = + _get_lever_name("torsion", "torsion_k"); } LambdaSchedule::LambdaSchedule(const LambdaSchedule &other) @@ -162,7 +184,9 @@ LambdaSchedule::LambdaSchedule(const LambdaSchedule &other) force_names(other.force_names), lever_names(other.lever_names), stage_names(other.stage_names), default_equations(other.default_equations), - stage_equations(other.stage_equations) + stage_equations(other.stage_equations), + coupled_levers(other.coupled_levers), + stage_weights(other.stage_weights) { } @@ -181,6 +205,8 @@ LambdaSchedule &LambdaSchedule::operator=(const LambdaSchedule &other) stage_names = other.stage_names; default_equations = other.default_equations; stage_equations = other.stage_equations; + coupled_levers = other.coupled_levers; + stage_weights = other.stage_weights; Property::operator=(other); } @@ -195,7 +221,9 @@ bool LambdaSchedule::operator==(const LambdaSchedule &other) const lever_names == other.lever_names and stage_names == other.stage_names and default_equations == other.default_equations and - stage_equations == other.stage_equations; + stage_equations == other.stage_equations and + coupled_levers == other.coupled_levers and + stage_weights == other.stage_weights; } bool LambdaSchedule::operator!=(const LambdaSchedule &other) const @@ -230,12 +258,27 @@ QString LambdaSchedule::toString() const QStringList lines; - for (int i = 0; i < this->stage_names.count(); ++i) + bool any_non_default_weight = false; + for (const auto &w : this->stage_weights) { + if (w != 1.0) + { + any_non_default_weight = true; + break; + } + } - lines.append(QString(" %1: %2") - .arg(this->stage_names[i]) - .arg(this->default_equations[i].toOpenMMString())); + for (int i = 0; i < this->stage_names.count(); ++i) + { + if (any_non_default_weight) + lines.append(QString(" %1 (weight=%2): %3") + .arg(this->stage_names[i]) + .arg(this->stage_weights[i]) + .arg(this->default_equations[i].toOpenMMString())); + else + lines.append(QString(" %1: %2") + .arg(this->stage_names[i]) + .arg(this->default_equations[i].toOpenMMString())); auto keys = this->stage_equations[i].keys(); std::sort(keys.begin(), keys.end()); @@ -617,13 +660,27 @@ std::tuple LambdaSchedule::resolve_lambda(double lambda_value) cons return std::tuple(this->nStages() - 1, 1.0); } - double stage_width = 1.0 / this->nStages(); + double total_weight = 0.0; + for (const auto &w : this->stage_weights) + total_weight += w; - double resolved = lambda_value / stage_width; + double cumulative = 0.0; + for (int i = 0; i < this->nStages(); ++i) + { + double stage_start = cumulative / total_weight; + double stage_width = this->stage_weights[i] / total_weight; + double stage_end = stage_start + stage_width; - double stage = std::floor(resolved); + if (lambda_value < stage_end) + { + double local_lambda = (lambda_value - stage_start) / stage_width; + return std::tuple(i, local_lambda); + } + + cumulative += this->stage_weights[i]; + } - return std::tuple(int(stage), resolved - stage); + return std::tuple(this->nStages() - 1, 1.0); } /** Return the name of the stage that controls the forcefield parameters @@ -655,6 +712,7 @@ void LambdaSchedule::clear() this->stage_names.clear(); this->stage_equations.clear(); this->default_equations.clear(); + this->stage_weights.clear(); this->constant_values = Values(); } @@ -662,9 +720,9 @@ void LambdaSchedule::clear() * standard stage that scales each forcefield parameter by * (1-:lambda:).initial + :lambda:.final */ -void LambdaSchedule::addMorphStage(const QString &name) +void LambdaSchedule::addMorphStage(const QString &name, double weight) { - this->addStage(name, default_morph_equation); + this->addStage(name, default_morph_equation, weight); } /** Append a morph stage onto this schedule. The morph stage is a @@ -689,9 +747,10 @@ void LambdaSchedule::addDecoupleStage(bool perturbed_is_decoupled) * state if `perturbed_is_decoupled` is true, otherwise the * reference state is decoupled. */ -void LambdaSchedule::addDecoupleStage(const QString &name, bool perturbed_is_decoupled) +void LambdaSchedule::addDecoupleStage(const QString &name, bool perturbed_is_decoupled, + double weight) { - this->addStage(name, default_morph_equation); + this->addStage(name, default_morph_equation, weight); // we now need to ensure that the ghost/ghost and ghost-14 parameters are // not perturbed @@ -728,9 +787,10 @@ void LambdaSchedule::addAnnihilateStage(bool perturbed_is_annihilated) * state if `perturbed_is_annihilated` is true, otherwise the * reference state is annihilated. */ -void LambdaSchedule::addAnnihilateStage(const QString &name, bool perturbed_is_annihilated) +void LambdaSchedule::addAnnihilateStage(const QString &name, bool perturbed_is_annihilated, + double weight) { - this->addStage(name, default_morph_equation); + this->addStage(name, default_morph_equation, weight); } /** Sandwich the current set of stages with a charge-descaling and @@ -781,13 +841,20 @@ void LambdaSchedule::addChargeScaleStages(double scale) * a custom lever for this stage. */ void LambdaSchedule::prependStage(const QString &name, - const SireCAS::Expression &equation) + const SireCAS::Expression &equation, + double weight) { if (name == "*") throw SireError::invalid_key(QObject::tr( "The stage name '*' is reserved and cannot be used."), CODELOC); + if (weight <= 0.0) + throw SireError::invalid_arg(QObject::tr( + "The stage weight must be positive. Got %1.") + .arg(weight), + CODELOC); + auto e = equation; if (e == default_morph_equation) @@ -795,7 +862,7 @@ void LambdaSchedule::prependStage(const QString &name, if (this->nStages() == 0) { - this->appendStage(name, e); + this->appendStage(name, e, weight); return; } @@ -808,6 +875,7 @@ void LambdaSchedule::prependStage(const QString &name, this->stage_names.prepend(name); this->default_equations.prepend(e); this->stage_equations.prepend(QHash()); + this->stage_weights.prepend(weight); } /** Append a stage called 'name' which uses the passed 'equation' @@ -816,7 +884,8 @@ void LambdaSchedule::prependStage(const QString &name, * a custom lever for this stage. */ void LambdaSchedule::appendStage(const QString &name, - const SireCAS::Expression &equation) + const SireCAS::Expression &equation, + double weight) { if (name == "*") throw SireError::invalid_key(QObject::tr( @@ -829,6 +898,12 @@ void LambdaSchedule::appendStage(const QString &name, .arg(name), CODELOC); + if (weight <= 0.0) + throw SireError::invalid_arg(QObject::tr( + "The stage weight must be positive. Got %1.") + .arg(weight), + CODELOC); + auto e = equation; if (e == default_morph_equation) @@ -837,6 +912,7 @@ void LambdaSchedule::appendStage(const QString &name, this->stage_names.append(name); this->default_equations.append(e); this->stage_equations.append(QHash()); + this->stage_weights.append(weight); } /** Insert a stage called 'name' at position `i` which uses the passed @@ -846,13 +922,20 @@ void LambdaSchedule::appendStage(const QString &name, */ void LambdaSchedule::insertStage(int i, const QString &name, - const SireCAS::Expression &equation) + const SireCAS::Expression &equation, + double weight) { if (name == "*") throw SireError::invalid_key(QObject::tr( "The stage name '*' is reserved and cannot be used."), CODELOC); + if (weight <= 0.0) + throw SireError::invalid_arg(QObject::tr( + "The stage weight must be positive. Got %1.") + .arg(weight), + CODELOC); + auto e = equation; if (e == default_morph_equation) @@ -860,12 +943,12 @@ void LambdaSchedule::insertStage(int i, if (i == 0) { - this->prependStage(name, e); + this->prependStage(name, e, weight); return; } else if (i >= this->nStages()) { - this->appendStage(name, e); + this->appendStage(name, e, weight); return; } @@ -878,6 +961,7 @@ void LambdaSchedule::insertStage(int i, this->stage_names.insert(i, name); this->default_equations.insert(i, e); this->stage_equations.insert(i, QHash()); + this->stage_weights.insert(i, weight); } /** Remove the stage 'stage' */ @@ -891,6 +975,7 @@ void LambdaSchedule::removeStage(const QString &stage) this->stage_names.removeAt(idx); this->default_equations.removeAt(idx); this->stage_equations.removeAt(idx); + this->stage_weights.removeAt(idx); } /** Append a stage called 'name' which uses the passed 'equation' @@ -899,14 +984,15 @@ void LambdaSchedule::removeStage(const QString &stage) * a custom lever for this stage. */ void LambdaSchedule::addStage(const QString &name, - const Expression &equation) + const Expression &equation, + double weight) { if (name == "*") throw SireError::invalid_key(QObject::tr( "The stage name '*' is reserved and cannot be used."), CODELOC); - this->appendStage(name, equation); + this->appendStage(name, equation, weight); } /** Find the index of the stage called 'stage'. This returns @@ -927,6 +1013,33 @@ int LambdaSchedule::find_stage(const QString &stage) const return idx; } +/** Set the relative weight of the stage 'stage' in lambda space. + * A stage with weight 2 occupies twice the lambda range as a + * stage with weight 1. Weights must be positive. + */ +void LambdaSchedule::setStageWeight(const QString &stage, double weight) +{ + if (weight <= 0.0) + throw SireError::invalid_arg(QObject::tr( + "The stage weight must be positive. Got %1.") + .arg(weight), + CODELOC); + + this->stage_weights[this->find_stage(stage)] = weight; +} + +/** Return the relative weight of the stage 'stage' in lambda space. */ +double LambdaSchedule::getStageWeight(const QString &stage) const +{ + return this->stage_weights[this->find_stage(stage)]; +} + +/** Return the relative weights of all stages, in stage order. */ +QVector LambdaSchedule::getStageWeights() const +{ + return this->stage_weights; +} + /** Set the default equation used to control levers for the * stage 'stage' to 'equation'. This equation will be used * to control any levers in this stage that don't have @@ -1006,6 +1119,32 @@ void LambdaSchedule::removeEquation(const QString &stage, this->stage_equations[idx].remove(_get_lever_name(force, lever)); } +/** Couple the lever 'force::lever' to 'fallback_force::fallback_lever'. + * If no custom equation has been set for 'force::lever' at any stage, + * the equation for 'fallback_force::fallback_lever' will be used instead + * of the stage default. This is a single level of indirection — the + * fallback lever is not itself followed further. + * + * By default, 'cmap::cmap_grid' is coupled to 'torsion::torsion_k' so + * that custom torsion schedules automatically keep the CMAP correction + * in sync. + */ +void LambdaSchedule::coupleLever(const QString &force, const QString &lever, + const QString &fallback_force, + const QString &fallback_lever) +{ + coupled_levers[_get_lever_name(force, lever)] = + _get_lever_name(fallback_force, fallback_lever); +} + +/** Remove any coupling for the lever 'force::lever', reverting it to + * use the stage default equation when no custom equation is set. + */ +void LambdaSchedule::removeCoupledLever(const QString &force, const QString &lever) +{ + coupled_levers.remove(_get_lever_name(force, lever)); +} + /** Return whether or not the specified 'lever' in the specified 'force' * at the specified 'stage' has a custom equation set for it */ @@ -1077,6 +1216,32 @@ SireCAS::Expression LambdaSchedule::_getEquation(int stage, return it.value(); } + // Check coupled levers: if this lever is coupled to another, try to + // find a custom equation for the coupled lever before falling back to + // the stage default. This is a single level of indirection (no recursion) + // to prevent loops. + auto coupled_it = this->coupled_levers.find(lever_name); + + if (coupled_it != this->coupled_levers.end()) + { + const auto &coupled_name = coupled_it.value(); + const int sep = coupled_name.indexOf("::"); + const QString coupled_force = sep >= 0 ? coupled_name.left(sep) : "*"; + const QString coupled_lever_part = sep >= 0 ? coupled_name.mid(sep + 2) : coupled_name; + + it = equations.find(coupled_name); + if (it != equations.end()) + return it.value(); + + it = equations.find(_get_lever_name(coupled_force, "*")); + if (it != equations.end()) + return it.value(); + + it = equations.find(_get_lever_name("*", coupled_lever_part)); + if (it != equations.end()) + return it.value(); + } + // we don't have any match, so return the default equation for this stage return this->default_equations[stage]; } @@ -1507,3 +1672,54 @@ QVector LambdaSchedule::morph(const QString &force, return morphed; } + +/** Return a new LambdaSchedule that is the reverse of this schedule. + * The stages are reversed in order, and within each stage's equations + * the lambda symbol is replaced by (1 - lambda) and the initial and + * final symbols are swapped simultaneously. This flips the schedule + * about its midpoint so that the end state becomes the start state + * and vice versa. + * + * The invariant is: + * reversed.morph(force, lever, initial, final, λ) + * == original.morph(force, lever, final, initial, 1-λ) + */ +LambdaSchedule LambdaSchedule::reverse() const +{ + if (this->isNull()) + return *this; + + // Simultaneous substitution: λ → (1-λ), initial ↔ final + const Identities ids( + SymbolExpression(lambda_symbol, 1.0 - Expression(lambda_symbol)), + SymbolExpression(initial_symbol, Expression(final_symbol)), + SymbolExpression(final_symbol, Expression(initial_symbol))); + + auto transform = [&](const Expression &e) -> Expression + { + auto r = e.substitute(ids); + if (r == default_morph_equation) + r = default_morph_equation; + return r; + }; + + LambdaSchedule result(*this); + + std::reverse(result.stage_names.begin(), result.stage_names.end()); + std::reverse(result.default_equations.begin(), result.default_equations.end()); + std::reverse(result.stage_equations.begin(), result.stage_equations.end()); + std::reverse(result.stage_weights.begin(), result.stage_weights.end()); + + for (int i = 0; i < result.nStages(); ++i) + { + result.default_equations[i] = transform(result.default_equations[i]); + + for (auto &eq : result.stage_equations[i]) + eq = transform(eq); + } + + for (auto &mol_sched : result.mol_schedules) + mol_sched = mol_sched.reverse(); + + return result; +} diff --git a/corelib/src/libs/SireCAS/lambdaschedule.h b/corelib/src/libs/SireCAS/lambdaschedule.h index 1ac9fa9f6..5ee79ecef 100644 --- a/corelib/src/libs/SireCAS/lambdaschedule.h +++ b/corelib/src/libs/SireCAS/lambdaschedule.h @@ -120,22 +120,26 @@ namespace SireCAS double getLambdaInStage(double lambda_value) const; void addStage(const QString &stage, - const SireCAS::Expression &equation); + const SireCAS::Expression &equation, + double weight = 1.0); void prependStage(const QString &stage, - const SireCAS::Expression &equation); + const SireCAS::Expression &equation, + double weight = 1.0); void appendStage(const QString &stage, - const SireCAS::Expression &equation); + const SireCAS::Expression &equation, + double weight = 1.0); void insertStage(int i, const QString &stage, - const SireCAS::Expression &equation); + const SireCAS::Expression &equation, + double weight = 1.0); void removeStage(const QString &stage); void addMorphStage(); - void addMorphStage(const QString &name); + void addMorphStage(const QString &name, double weight = 1.0); void addChargeScaleStages(double scale = 0.2); void addChargeScaleStages(const QString &decharge_name, @@ -143,10 +147,16 @@ namespace SireCAS double scale = 0.2); void addDecoupleStage(bool perturbed_is_decoupled = true); - void addDecoupleStage(const QString &name, bool perturbed_is_decoupled = true); + void addDecoupleStage(const QString &name, bool perturbed_is_decoupled = true, + double weight = 1.0); void addAnnihilateStage(bool perturbed_is_annihilated = true); - void addAnnihilateStage(const QString &name, bool perturbed_is_annihilated = true); + void addAnnihilateStage(const QString &name, bool perturbed_is_annihilated = true, + double weight = 1.0); + + void setStageWeight(const QString &stage, double weight); + double getStageWeight(const QString &stage) const; + QVector getStageWeights() const; void setDefaultStageEquation(const QString &stage, const SireCAS::Expression &equation); @@ -164,6 +174,12 @@ namespace SireCAS const QString &force = "*", const QString &lever = "*") const; + void coupleLever(const QString &force, const QString &lever, + const QString &fallback_force, + const QString &fallback_lever); + + void removeCoupledLever(const QString &force, const QString &lever); + SireCAS::Expression getEquation(const QString &stage = "*", const QString &force = "*", const QString &lever = "*") const; @@ -217,6 +233,8 @@ namespace SireCAS double clamp(double lambda_value) const; + LambdaSchedule reverse() const; + protected: int find_stage(const QString &stage) const; @@ -248,6 +266,17 @@ namespace SireCAS particular stage */ QVector> stage_equations; + /** The relative weight of each stage in lambda space (default 1.0). + A stage with weight 2 occupies twice the lambda range of one + with weight 1. */ + QVector stage_weights; + + /** Coupled lever fallbacks: if a lever (force::lever) has no custom + equation set, use the equation for the paired lever instead of + falling straight back to the stage default. Stored as + _get_lever_name(force, lever) → _get_lever_name(fb_force, fb_lever). */ + QHash coupled_levers; + /** The symbol used to represent the lambda value */ static SireCAS::Symbol lambda_symbol; diff --git a/corelib/src/libs/SireIO/amberprm.cpp b/corelib/src/libs/SireIO/amberprm.cpp index 031f85e00..67a4ff563 100644 --- a/corelib/src/libs/SireIO/amberprm.cpp +++ b/corelib/src/libs/SireIO/amberprm.cpp @@ -5396,6 +5396,63 @@ System AmberPrm::startSystem(const PropertyMap &map) const bar.success(); } + if (not in_expected_order) + { + // Collect all residues in loaded (mol-index) order and check for + // residue-number inversions. These arise when covalent bonds cross + // AMBER's molecule boundaries (e.g. a metal ion bonded to a small + // molecule), forcing Sire to group the bonded atoms into the same + // molecule and thereby placing some residues out of their original + // residue-number sequence. + struct ResEntry + { + int resnum; + QString resname; + }; + + QVector all_res; + + for (const auto &mol : mols) + { + const auto &molinfo = mol.info(); + const int nres = molinfo.nResidues(); + + for (int j = 0; j < nres; ++j) + { + all_res.append({molinfo.number(ResIdx(j)).value(), + molinfo.name(ResIdx(j)).value()}); + } + } + + QStringList out_of_order; + int prev_resnum = 0; + + for (const auto &res : all_res) + { + if (res.resnum < prev_resnum) + { + out_of_order.append( + QString("%1(%2)").arg(res.resname).arg(res.resnum)); + } + + prev_resnum = res.resnum; + } + + if (not out_of_order.isEmpty()) + { + qWarning().noquote() << QObject::tr( + "WARNING: One or more residues have been reordered relative to the " + "original topology when loading this file. This happens when covalent " + "bonds cross AMBER's molecule boundaries (e.g. a metal ion bonded to a " + "small molecule ligand), which forces Sire to group the bonded atoms " + "into the same molecule. The following residues appear out of " + "residue-number order in the loaded system: %1. " + "To avoid ordering issues, access residues by name rather than by " + "position (e.g. mols.residues()[\"resname ML1\"] instead of mols.residues()[1]).") + .arg(out_of_order.join(", ")); + } + } + MoleculeGroup molgroup("all"); for (auto mol : mols) diff --git a/corelib/src/libs/SireIO/biosimspace.cpp b/corelib/src/libs/SireIO/biosimspace.cpp index 742fe95ab..fa293afac 100644 --- a/corelib/src/libs/SireIO/biosimspace.cpp +++ b/corelib/src/libs/SireIO/biosimspace.cpp @@ -28,6 +28,8 @@ #include "biosimspace.h" #include "moleculeparser.h" +#include + #include "SireBase/getinstalldir.h" #include "SireError/errors.h" @@ -1225,6 +1227,16 @@ namespace SireIO // Reduce the mass. mass -= delta_mass; + if (mass.value() < 0) + { + qWarning().noquote() << QObject::tr( + "Hydrogen mass repartitioning with factor %1 has resulted " + "in a negative mass for atom index %2. Consider using a " + "smaller repartitioning factor.") + .arg(factor) + .arg(idx.value()); + } + // Set the new mass. edit_mol = edit_mol.atom(idx).setProperty(mass_prop, mass).molecule(); @@ -1758,84 +1770,50 @@ namespace SireIO return Vector(nx, ny, nz); } - SireBase::PropertyList mergeIntrascale(const CLJNBPairs &nb0, - const CLJNBPairs &nb1, - const MoleculeInfoData &merged_info, - const QHash &mol0_merged_mapping, - const QHash &mol1_merged_mapping) + boost::tuple patchIntrascale(const CLJNBPairs &nb0, + const CLJNBPairs &nb1, + CLJNBPairs intra0, + CLJNBPairs intra1, + const QHash &mol0_merged_mapping, + const QHash &mol1_merged_mapping) { - // Helper lambda: copy scaling factors from 'nb' to 'nb_merged' according - // to the provided mapping. Takes nb_merged by reference to avoid copies. - // When copy_all is true, ALL pairs between mapped atoms are written - // (including the default (1,1)), so that the correct end-state values - // always overwrite any values set by a prior ghost-topology pass. - // When copy_all is false, only non-(1,1) pairs are written (sufficient - // for the first/ghost pass, where unset pairs default to (1,1) anyway). - auto copyIntrascale = [&](const CLJNBPairs &nb, CLJNBPairs &nb_merged, - const QHash &mapping, - bool copy_all = false) + // Apply per-pair scale factors from nb to nb_merged wherever they differ + // from the connectivity-derived base values. For standard AMBER molecules + // this is a no-op. For force fields with non-default per-pair values + // (e.g. GLYCAM funct=2 (1,1) instead of global sf14 for 1-4 pairs) + // it replaces the base value with the correct per-pair value. + auto patch = [&](const CLJNBPairs &nb, CLJNBPairs &nb_merged, + const QHash &mapping) { - const int n = nb.nAtoms(); + // Iterate only over the mapped atoms rather than all atoms in nb. + // This is O(k²) in the number of mapped atoms k, which is always + // ≤ nb.nAtoms() and can be much smaller. + const QList keys = mapping.keys(); + const int k = keys.size(); - for (int i = 0; i < n; ++i) + for (int i = 0; i < k; ++i) { - const AtomIdx ai(i); - - // Get the index of this atom in the merged system. - const AtomIdx merged_ai = mapping.value(ai, AtomIdx(-1)); + const AtomIdx ai = keys.at(i); + const AtomIdx merged_ai = mapping.value(ai); - // If this atom hasn't been mapped to the merged system, then we - // can skip it, as any scaling factors involving this atom will - // just use the default. - if (merged_ai == AtomIdx(-1)) - continue; - - for (int j = i; j < n; ++j) + for (int j = i; j < k; ++j) { - const AtomIdx aj(j); + const AtomIdx aj = keys.at(j); + const AtomIdx merged_aj = mapping.value(aj); - // Get the scaling factor for this pair of atoms. - const CLJScaleFactor sf = nb.get(ai, aj); + const CLJScaleFactor nb_sf = nb.get(ai, aj); + const CLJScaleFactor base_sf = nb_merged.get(merged_ai, merged_aj); - // Copy if this is a non-default scaling factor, or if - // copy_all is set (second pass: must overwrite ghost-topology - // values even when the correct end-state value is (1,1)). - if (copy_all or sf.coulomb() != 1.0 or sf.lj() != 1.0) - { - // Get the index of the second atom in the merged system. - const AtomIdx merged_aj = mapping.value(aj, AtomIdx(-1)); - - // Only set the scaling factor if both atoms have been - // mapped to the merged system. If one of the atoms - // hasn't been mapped, then we can just use the default. - if (merged_aj != AtomIdx(-1)) - nb_merged.set(merged_ai, merged_aj, sf); - } + if (nb_sf.coulomb() != base_sf.coulomb() or nb_sf.lj() != base_sf.lj()) + nb_merged.set(merged_ai, merged_aj, nb_sf); } } }; - // Create the intrascale objects for the merged end-states. - CLJNBPairs intra0(merged_info); - CLJNBPairs intra1(merged_info); - - // Copy scaling factors from the original intrascale objects to the - // merged intrascale objects. For each end state, the ghost molecule's - // topology is written first (non-default pairs only), then the correct - // end-state topology overwrites with copy_all=true so that (1,1) pairs - // are also written. This handles ring-breaking perturbations where a - // pair that is excluded/1-4 in one state becomes fully interacting (1,1) - // in the other state and must not be left at the ghost state's value. - copyIntrascale(nb1, intra0, mol1_merged_mapping); - copyIntrascale(nb0, intra0, mol0_merged_mapping, true); - copyIntrascale(nb0, intra1, mol0_merged_mapping); - copyIntrascale(nb1, intra1, mol1_merged_mapping, true); - - // Assemble the intrascale objects into a property list to return. - SireBase::PropertyList ret; - ret.append(intra0); - ret.append(intra1); - return ret; + patch(nb0, intra0, mol0_merged_mapping); + patch(nb1, intra1, mol1_merged_mapping); + + return boost::make_tuple(intra0, intra1); } } // namespace SireIO diff --git a/corelib/src/libs/SireIO/biosimspace.h b/corelib/src/libs/SireIO/biosimspace.h index 5a6c0d2e2..bd58fe41c 100644 --- a/corelib/src/libs/SireIO/biosimspace.h +++ b/corelib/src/libs/SireIO/biosimspace.h @@ -36,8 +36,6 @@ #include "SireMaths/vector.h" -#include "SireBase/propertylist.h" - #include "SireMM/cljnbpairs.h" #include "SireMol/atomidxmapping.h" @@ -393,25 +391,32 @@ namespace SireIO \param nb1 The CLJNBPairs for molecule1 in its original atom index space. - \param merged_info - The MoleculeInfoData for the merged molecule. + \param intra0 + The connectivity-derived CLJNBPairs for the lambda=0 end state, + built in merged-molecule atom index space. Per-pair non-default + values from nb0 will be applied on top. + + \param intra1 + The connectivity-derived CLJNBPairs for the lambda=1 end state. + Per-pair non-default values from nb1 will be applied on top. \param mol0_merged_mapping A hash mapping each AtomIdx in molecule0's original space to the corresponding AtomIdx in the merged molecule's space. - \param atom_mapping - The AtomIdxMapping describing how merged-molecule atom indices - correspond to molecule1 atom indices (used by CLJNBPairs::merge). + \param mol1_merged_mapping + A hash mapping each AtomIdx in molecule1's original space to + the corresponding AtomIdx in the merged molecule's space. - \retval [intrascale0, intrascale1] - A PropertyList containing the CLJNBPairs for the lambda=0 and - lambda=1 end states of the merged molecule. + \retval (intrascale0, intrascale1) + A tuple of the updated CLJNBPairs for the lambda=0 and lambda=1 + end states of the merged molecule. */ - SIREIO_EXPORT SireBase::PropertyList mergeIntrascale( + SIREIO_EXPORT boost::tuple patchIntrascale( const SireMM::CLJNBPairs &nb0, const SireMM::CLJNBPairs &nb1, - const SireMol::MoleculeInfoData &merged_info, + SireMM::CLJNBPairs intra0, + SireMM::CLJNBPairs intra1, const QHash &mol0_merged_mapping, const QHash &mol1_merged_mapping); @@ -430,7 +435,7 @@ SIRE_EXPOSE_FUNCTION(SireIO::updateCoordinatesAndVelocities) SIRE_EXPOSE_FUNCTION(SireIO::createSodiumIon) SIRE_EXPOSE_FUNCTION(SireIO::createChlorineIon) SIRE_EXPOSE_FUNCTION(SireIO::setCoordinates) -SIRE_EXPOSE_FUNCTION(SireIO::mergeIntrascale) +SIRE_EXPOSE_FUNCTION(SireIO::patchIntrascale) SIRE_END_HEADER diff --git a/corelib/src/libs/SireMM/amberparams.cpp b/corelib/src/libs/SireMM/amberparams.cpp index 5312666f6..fdfedb950 100644 --- a/corelib/src/libs/SireMM/amberparams.cpp +++ b/corelib/src/libs/SireMM/amberparams.cpp @@ -1539,24 +1539,12 @@ QStringList AmberParams::validateAndFix() } if (has_short_path) { - if (not is_perturbable) - { - QMutexLocker lkr(&mutex); - qWarning().noquote() - << QObject::tr( - "WARNING: Have a 1-4 scaling factor " - "(%1/%2) between atoms %3:%4 and %5:%6 " - "that are fewer than 4 bonds apart. " - "Skipping — connectivity will enforce " - "their exclusion. This may indicate a " - "topology issue.") - .arg(s.coulomb()) - .arg(s.lj()) - .arg(molinfo.name(atm0).value()) - .arg(atm0.value()) - .arg(molinfo.name(atm3).value()) - .arg(atm3.value()); - } + // These atoms are fewer than 4 bonds apart so + // connectivity will enforce their exclusion + // regardless of what the intrascale says. Repair + // the entry so exc_atoms is self-consistent. + QMutexLocker lkr(&mutex); + new_exc.set(atm0, atm3, CLJScaleFactor(0, 0)); continue; } } diff --git a/corelib/src/libs/SireMM/bondrestraints.cpp b/corelib/src/libs/SireMM/bondrestraints.cpp index ff63cd678..b87aba1f1 100644 --- a/corelib/src/libs/SireMM/bondrestraints.cpp +++ b/corelib/src/libs/SireMM/bondrestraints.cpp @@ -188,6 +188,7 @@ BondRestraint &BondRestraint::operator=(const BondRestraint &other) { if (this != &other) { + Property::operator=(other); atms0 = other.atms0; atms1 = other.atms1; _k = other._k; diff --git a/corelib/src/libs/SireMM/inversebondrestraints.cpp b/corelib/src/libs/SireMM/inversebondrestraints.cpp index f88acc3fe..c110c0a70 100644 --- a/corelib/src/libs/SireMM/inversebondrestraints.cpp +++ b/corelib/src/libs/SireMM/inversebondrestraints.cpp @@ -188,6 +188,7 @@ InverseBondRestraint &InverseBondRestraint::operator=(const InverseBondRestraint { if (this != &other) { + Property::operator=(other); atms0 = other.atms0; atms1 = other.atms1; _k = other._k; diff --git a/corelib/src/libs/SireMM/morsepotentialrestraints.cpp b/corelib/src/libs/SireMM/morsepotentialrestraints.cpp index 1322e08b1..666f7426c 100644 --- a/corelib/src/libs/SireMM/morsepotentialrestraints.cpp +++ b/corelib/src/libs/SireMM/morsepotentialrestraints.cpp @@ -190,6 +190,7 @@ MorsePotentialRestraint &MorsePotentialRestraint::operator=(const MorsePotential { if (this != &other) { + Property::operator=(other); atms0 = other.atms0; atms1 = other.atms1; _k = other._k; diff --git a/corelib/src/libs/SireMM/positionalrestraints.cpp b/corelib/src/libs/SireMM/positionalrestraints.cpp index 101ce0f57..cd3ac6902 100644 --- a/corelib/src/libs/SireMM/positionalrestraints.cpp +++ b/corelib/src/libs/SireMM/positionalrestraints.cpp @@ -146,6 +146,7 @@ PositionalRestraint &PositionalRestraint::operator=(const PositionalRestraint &o { if (this != &other) { + Property::operator=(other); atms = other.atms; pos = other.pos; _k = other._k; diff --git a/corelib/src/libs/SireSystem/merge.cpp b/corelib/src/libs/SireSystem/merge.cpp index d01703f76..e62d41d5c 100644 --- a/corelib/src/libs/SireSystem/merge.cpp +++ b/corelib/src/libs/SireSystem/merge.cpp @@ -131,6 +131,7 @@ namespace SireSystem "atomtype", "bond", "charge", + "cmap", "connectivity", "coordinates", "dihedral", diff --git a/doc/source/changelog.rst b/doc/source/changelog.rst index ec690e1d3..a843f7e74 100644 --- a/doc/source/changelog.rst +++ b/doc/source/changelog.rst @@ -12,10 +12,8 @@ Development was migrated into the `OpenBioSim `__ organisation on `GitHub `__. -`2026.1.0 `__ - April 2026 ------------------------------------------------------------------------------------------- - -* Please add an item to this CHANGELOG for any new features or bug fixes when creating a PR. +`2026.1.0 `__ - June 2026 +----------------------------------------------------------------------------------------- * Fixed duplicate converter registrations in the Python wrappers for OpenMM-related classes, which caused ``RuntimeWarning: to-Python converter already registered`` warnings at import @@ -30,12 +28,30 @@ organisation on `GitHub `__. * Fixed parsing of AMBER and GROMACS GLYCAM force field topologies. +* Add support for CMAP terms in the OpenMM conversion layer. + * Fix hang in ``sire.load`` function when shared GROMACS topology path is missing. +* Add support for 4- and 5-point water models in the OpenMM conversion layer. + +* Named forces (``clj``, ``bond``, ``angle``, ``torsion``, ``cmap``, ghost + forces, restraints, etc.) are each assigned a unique OpenMM force group when + the system is built, enabling energy decomposition by force type via + ``getState(groups=...)``. + +* Add functionality for coupling one lambda lever to another. + +* Added support for Direct Morse Replacement (DMR) feature in ``sire.restraints.morse_potential`` + which is enabled by default. + * Don't mutate input system in the ``sire.legacy.IO.setCoordinates`` function. * Store OpenMM state at start of a dynamics run to use for crash recovery. +* Print warning when ``sire.legacy.IO.AmberPrm`` parser re-orders residues due to covalent bonds between molecules. + +* Added internal implementation of virtual sites in the Sire-to-OpenMM conversion layer + * Use ``RDKit::determineBondOrders()`` in Sire-to-RDKit conversion to infer bond orders. * Map the end-state ``element`` property when performing hydrogen mass repartitioning on perturbable molecules. @@ -47,6 +63,43 @@ organisation on `GitHub `__. * Reassign end-state mass and element properties in the ``sire.morph.create_from_pertfile`` to undo ``SOMD`` modifications. +* Added per-stage weights to :class:`~sire.cas.LambdaSchedule`, allowing stages to occupy unequal fractions of lambda space (e.g. ``add_morph_stage("morph", weight=2.0)``). + +* Added support for the Beutler et al. softcore potential (``use_beutler_softening``, + ``beutler_alpha`` map options), currently intended for ABFE calculations. Removed the + ``coulomb_power`` parameter, which was invalid for any non-zero value. + +* Added analytic LJ long-range correction (LRC) for periodic simulations + (``use_dispersion_correction`` map option). A background LRC is computed for all + non-ghost atoms. When ghost atoms are present, a separate ghost LRC covers ghost–ghost + and ghost–non-ghost interactions. Both are updated efficiently via the lambda lever + without recomputing the full dispersion correction on every λ change. + +* Added a ``reverse()`` method to :class:`~sire.cas.LambdaSchedule` that returns a new + schedule flipped about its midpoint. Stages are reversed in order and each equation is + transformed by substituting ``λ → (1-λ)`` and swapping ``initial`` ↔ ``final`` + simultaneously. The invariant is that + ``reversed.morph(force, lever, initial, final, λ)`` equals + ``original.morph(force, lever, final, initial, 1-λ)``. + +* Add support for using a switching function for QM/MM simulations. + +* Add softcore ``CustomBondForce`` for ring-breaking and ring-making pairs. + +* Add ``max_h_mass`` map option (default ``3.5`` g/mol) to control the mass threshold + used when identifying hydrogen atoms in the OpenMM conversion layer. Previously this + was hardcoded to ``2.5`` g/mol, which caused hydrogen constraints to be skipped when + an HMR factor greater than ~2.5 was applied. + +* Warn if hydrogen mass repartiting produces a negative heavy atom mass. + +* Update merge code to handle ``kartograf`` API changes in version 2.0. + +* Allocate ``ghost-14`` slot if either end-state exception scale is nonzero. + +* Constrain perturbable bonds with unchanged parameters regardless of atom mass, matching + SOMD1, instead of requiring a light (hydrogen) atom to be present. + `2025.4.0 `__ - February 2026 --------------------------------------------------------------------------------------------- diff --git a/doc/source/cheatsheet/openmm.rst b/doc/source/cheatsheet/openmm.rst index 9f46bdd04..005a70773 100644 --- a/doc/source/cheatsheet/openmm.rst +++ b/doc/source/cheatsheet/openmm.rst @@ -122,9 +122,9 @@ Available keys and allowable values are listed below. | | ``bonds-h-angles-not-perturbed`` and | | | ``bonds-h-angles-not-heavy-perturbed | +------------------------------+----------------------------------------------------------+ -| coulomb_power | The coulomb power parameter used by the softening | -| | potential used to soften electrostatic interactions | -| | involving ghost atoms. This defaults to 0. | +| beutler_alpha | The alpha parameter for the Beutler softcore LJ | +| | potential. Controls the width of the softening in | +| | r^6-space. This defaults to 0.5. | +------------------------------+----------------------------------------------------------+ | cutoff | Size of the non-bonded cutoff, e.g. | | | ``7.5*sr.units.angstrom`` | @@ -226,14 +226,18 @@ Available keys and allowable values are listed below. | use_dispersion_correction | Whether or not to use the dispersion correction to | | | deal with cutoff issues. This is very expensive. | +------------------------------+----------------------------------------------------------+ +| use_beutler_softening | Whether or not to use the Beutler softcore potential to | +| | soften interactions involving ghost atoms. Currently | +| | designed for ABFE calculations only. This defaults to | +| | False. If True, overrides taylor and zacharias softening.| ++------------------------------+----------------------------------------------------------+ | use_taylor_softening | Whether or not to use the taylor algorithm to soften | | | interactions involving ghost atoms. This defaults to | | | False. | +------------------------------+----------------------------------------------------------+ | use_zacharias_softening | Whether or not to use the zacharias algorithm to soften | | | interactions involving ghost atoms. This defaults to | -| | True. Note that one of zacharias or taylor softening | -| | must be True, with zacharias taking precedence. | +| | True. | +------------------------------+----------------------------------------------------------+ Higher level API diff --git a/doc/source/contributing/codestyle.rst b/doc/source/contributing/codestyle.rst index 779770414..1f3301111 100644 --- a/doc/source/contributing/codestyle.rst +++ b/doc/source/contributing/codestyle.rst @@ -9,20 +9,54 @@ throughout, we ask that you please follow the below coding styles (and that you aren't offended if we modify your submission so that it meets these styles). +Pre-commit hooks +================ + +The repository uses `pre-commit `__ to automate code +formatting. To install the hooks, run: + +.. code-block:: bash + + pre-commit install + +Once installed, the hooks run automatically on each commit. To run them +manually against staged files: + +.. code-block:: bash + + pre-commit run + +Python formatting is applied to ``src/``, ``tests/``, and Python files in +``wrapper/`` using `ruff `__. C++ formatting +is applied to ``corelib/`` and ``wrapper/`` using +`clang-format `__. The +C++ hook only runs on files staged for commit, so the codebase drifts +gradually toward the standard rather than requiring a blanket one-time +reformatting pass. + +.. note:: + + Please do **not** run ``pre-commit run --all-files`` for C++ — this + would format the entire codebase at once, which is intentionally + avoided. Format only the files you are actively editing. + Python ====== -Python code should be written to be `PEP8-compliant `__, -ideally autoformatted using a tool such as -`black `__. +Python code should be written to be `PEP8-compliant `__ +and is automatically formatted using `ruff `__. .. note:: - The developers use `black `__ - and will autoformat all python code with this tool to maintain - a consistent style. Please use `black `__ - if you can, as this will make diffs, pull requests and other changes - smaller and easier for everyone to review. Thanks :-) + The developers use `ruff `__ via + the pre-commit hooks above. The formatter is run automatically on + ``src/``, ``tests/``, and Python files in ``wrapper/`` on each commit, + keeping a consistent style. If you prefer to format manually before + committing, run: + + .. code-block:: bash + + ruff format src/ tests/ wrapper/ C++ === @@ -201,13 +235,14 @@ an existing rule or propose a new rule then please .. note:: - The developers use the C++ autoformatting tool built into the C++ - extension of VSCode (e.g. as `described here `__). - This is based on (and formats identically to `clang-format `__ - using the ``Visual Studio`` theme - roughly equivalent to - ``clang-format -style="{ BasedOnStyle: LLVM, UseTab: Never, IndentWidth: 4, TabWidth: 4, BreakBeforeBraces: Allman, AllowShortIfStatementsOnASingleLine: false, IndentCaseLabels: false, ColumnLimit: 0, AccessModifierOffset: -4, NamespaceIndentation: All, FixNamespaceComments: false })"``, - and so can be installed on any IDE (or run from the command line). - Please use the VSCode autoformatter, or - `clang-format `__ if you - can, as this will make diffs, pull requests and other changes - smaller and easier for everyone to review. Thanks :-) + The pre-commit hooks (see above) automatically run + `clang-format `__ on + any C++ files you stage for commit, using the ``.clang-format`` file at + the root of the repository. This is equivalent to the style: + + ``clang-format -style="{ BasedOnStyle: LLVM, UseTab: Never, IndentWidth: 4, TabWidth: 4, BreakBeforeBraces: Allman, AllowShortIfStatementsOnASingleLine: false, IndentCaseLabels: false, ColumnLimit: 0, AccessModifierOffset: -4, NamespaceIndentation: All, FixNamespaceComments: false }"`` + + IDE integration is also available — the VSCode C++ extension supports + ``clang-format`` natively (see + `this guide `__) + and will pick up the ``.clang-format`` file automatically. diff --git a/doc/source/tutorial/part06/03_restraints.rst b/doc/source/tutorial/part06/03_restraints.rst index da56cd4e9..b7a69da47 100644 --- a/doc/source/tutorial/part06/03_restraints.rst +++ b/doc/source/tutorial/part06/03_restraints.rst @@ -360,7 +360,7 @@ Morse Potential Restraints --------------------------- The :func:`sire.restraints.morse_potential` function is used to create Morse potential restraints, -which can be used to carry harmonic bond annihilations or creations in alchemical relative binding free energy calculations. +which can be used to carry out bond annihilations or creations in alchemical relative binding free energy calculations. To create a Morse potential restraint, you need to specify the two atoms to be restrained. Like the distance restraints, the atoms can be specified using a search string, passing lists of atom indexes, or @@ -370,7 +370,7 @@ If not supplied, automatic parametrisation feature can be used, which will detec annihilated or created and set the parameters accordingly (dissociation energy value still needs to be provided). For example, >>> mols = sr.load_test_files("cyclopentane_cyclohexane.bss") ->>> morse_restraints = sr.restraints.morse_potential( +>>> morse_restraints, mols = sr.restraints.morse_potential( ... mols, ... atoms0=mols["molecule property is_perturbable and atomidx 0"], ... atoms1=mols["molecule property is_perturbable and atomidx 4"], @@ -385,14 +385,22 @@ MorsePotentialRestraint( 0 <=> 4, k=100 kcal mol-1 Å-2 : r0=1.5 Å : de=50 kcal creates a Morse potential restraint between atoms 0 and 4 using the specified parameters. Alternatively, if the molecule contains a bond that is being alchemically annihilated, e.g. ->>> morse_restraints = sr.restraints.morse_potential(mols, auto_parametrise=True, de="50 kcal mol-1") +>>> morse_restraints, mols = sr.restraints.morse_potential(mols, auto_parametrise=True, de="50 kcal mol-1") >>> morse_restraint = morse_restraints[0] >>> print(morse_restraint) -MorsePotentialRestraint( 0 <=> 4, k=228.89 kcal mol-1 Å-2 : r0=1.5354 Å : de=50 kcal mol-1 ) +MorsePotentialRestraint( 0 <=> 4, k=457.78 kcal mol-1 Å-2 : r0=1.5354 Å : de=50 kcal mol-1 ) creates a Morse potential restraint between atoms 0 and 4 by attempting to match the bond parameters of the bond being alchemically annihilated. +.. note:: + + Automatic bond parametrisation is enabled by default and will strip away the bond being + alchemically annihilated or created from the system, and use the parameters of that bond + to set the parameters of the Morse potential restraint. If you want to keep the original bond + in the system, then you can disable automatic parametrisation by setting ``auto_parametrise=False`` + and explicitly providing the parameters for the Morse potential restraint. + Boresch Restraints --------------------------- diff --git a/doc/source/tutorial/part07/02_levers.rst b/doc/source/tutorial/part07/02_levers.rst index 5d240fe34..9d1854653 100644 --- a/doc/source/tutorial/part07/02_levers.rst +++ b/doc/source/tutorial/part07/02_levers.rst @@ -159,6 +159,31 @@ In this case, as we have a perturbable system, the Force objects used are; It uses parameters that are controlled by the ``charge``, ``sigma``, ``epsilon``, ``alpha``, ``kappa``, ``charge_scale`` and ``lj_scale`` levers. +For systems that use a force field with CMAP backbone torsion corrections (such +as AMBER ff19SB), an additional Force object is present: + +* ``cmap``: `OpenMM::CMAPTorsionForce `__. + This models the cross-term energy correction (CMAP) for backbone φ/ψ dihedral + pairs. It uses a parameter controlled by the ``cmap_grid`` lever, which + interpolates the two-dimensional energy grid from its λ=0 value to its λ=1 value. + +.. note:: + + By default, ``cmap::cmap_grid`` is coupled to ``torsion::torsion_k``: + if you set a custom equation for ``torsion_k``, the same equation will + automatically be used for ``cmap_grid``, keeping the CMAP correction + in sync with the torsion perturbation. You can override this by + setting an explicit equation for ``cmap::cmap_grid``:: + + # freeze CMAP at its λ=0 grid while still perturbing torsions + s.set_equation(stage="morph", force="cmap", lever="cmap_grid", + equation=s.initial()) + + To remove the coupling entirely (so that ``cmap_grid`` falls back to + the stage default independently of ``torsion_k``):: + + s.remove_coupled_lever(force="cmap", lever="cmap_grid") + Some levers, like ``bond_length``, are used only by a single Force object. However, others, like ``charge``, are used by multiple Force objects. diff --git a/doc/source/tutorial/part07/03_ghosts.rst b/doc/source/tutorial/part07/03_ghosts.rst index 2fafe665e..4ff7f4e4b 100644 --- a/doc/source/tutorial/part07/03_ghosts.rst +++ b/doc/source/tutorial/part07/03_ghosts.rst @@ -55,8 +55,22 @@ Ghost atoms are then added to three custom OpenMM Forces: interaction and subtracts this from the total (to remove the real-space contribution that was calculated in the standard OpenMM NonBondedForce). -There are two different soft-core potentials available. The default is -the Zacharias potential, while the second is the Taylor potential. +When ``use_dispersion_correction=True`` and the system uses a periodic +cutoff, two additional ``CustomVolumeForce`` objects are added: + +* A **background LRC** force (``lrc_background/v``) that evaluates the + LJ long-range correction for all non-ghost atoms analytically. This + replaces the built-in dispersion correction of the ``NonbondedForce``, + which would otherwise be recomputed on every λ change. The coefficient + is cached per λ state by the lambda lever. + +* A **ghost LRC** force (``lrc_coeff/v``) that evaluates the LJ + long-range correction for all ghost–ghost and ghost–non-ghost + interactions analytically. Its coefficient is also cached per λ state. + +There are three different soft-core potentials available. The default is +the Zacharias potential, while the second is the Taylor potential, and +the third is the Beutler potential. Zacharias softening ------------------- @@ -68,7 +82,7 @@ It is based on the following electrostatic and Lennard-Jones potentials: .. math:: - V_{\text{elec}}(r) = q_i q_j \left[ \frac{(1 - \alpha)^n}{\sqrt{r^2 + \delta_\text{coulomb}}} - \frac{\kappa}{r} \right] + V_{\text{elec}}(r) = q_i q_j \left[ \frac{1}{\sqrt{r^2 + \delta_\text{coulomb}}} - \frac{\kappa}{r} \right] V_{\text{LJ}}(r) = 4\epsilon \left[ \frac{\sigma^{12}}{(\delta_\text{LJ} \sigma + r^2)^6} - \frac{\sigma^6}{(\delta_\text{LJ} \sigma + r^2)^3} \right] @@ -103,10 +117,6 @@ The soft-core parameters are: state, and 1 in the perturbed state. These values can be perturbed via the ``alpha`` lever in the λ-schedule. -* ``n`` is the "coulomb power", and is set to 0 by default. It can be - any integer between 0 and 4. It is set via ``coulomb_power`` map - parameter. - * ``shift_coulomb`` and ``shift_LJ`` are the so-called "shift delta" parameters, which are specified individually for the coulomb and LJ\ potentials. They are set via the ``shift_coulomb`` and ``shift_delta`` @@ -132,7 +142,7 @@ It is based on the following electrostatic and Lennard-Jones potentials: .. math:: - V_{\text{elec}}(r) = q_i q_j \left[ \frac{(1 - \alpha)^n}{\sqrt{r^2 + \delta^2}} - \frac{\kappa}{r} \right] + V_{\text{elec}}(r) = q_i q_j \left[ \frac{1}{\sqrt{r^2 + \delta^2}} - \frac{\kappa}{r} \right] V_{\text{LJ}}(r) = 4\epsilon \left[ \frac{\sigma^{12}}{(\alpha^m \sigma^6 + r^6)^2} - \frac{\sigma^6}{\alpha^m \sigma^6 + r^6} \right] @@ -169,10 +179,6 @@ The soft-core parameters are: any integer between 0 and 4. It is set via ``taylor_power`` map parameter. -* ``n`` is the "coulomb power", and is set to 0 by default. It can be - any integer between 0 and 4. It is set via ``coulomb_power`` map - parameter. - * ``shift_coulomb`` is the so-called "shift delta" parameters, which are specified only for the coulomb potential. This is set via the ``shift_coulomb`` @@ -187,6 +193,74 @@ The soft-core parameters are: intramolecular electrostatic interactions, when the "hard" interaction would not be calculated in the NonbondedForce. +Beutler softening +----------------- + +This is the third soft-core potential, based on Beutler et al., +*Chem. Phys. Lett.*, 1994. You can use it by setting the map option +``use_beutler_softening`` to True. + +.. note:: + + The Beutler potential is currently designed for absolute binding free + energy (ABFE) calculations only. It is not recommended for relative + free energy (RBFE) calculations. + +It is based on the following electrostatic and Lennard-Jones potentials: + +.. math:: + + V_{\text{elec}}(r) = q_i q_j \left[ \frac{1}{\sqrt{r^2 + \delta^2}} - \frac{\kappa}{r} \right] + + V_{\text{LJ}}(r) = (1 - \alpha) \cdot 4\epsilon \left[ \frac{\sigma^{12}}{(\beta \sigma^6 \alpha + r^6)^2} - \frac{\sigma^6}{\beta \sigma^6 \alpha + r^6} \right] + +where + +.. math:: + + \delta = \alpha \times \text{shift_coulomb} + +and + +.. math:: + + \alpha = \max(\alpha_i, \alpha_j) + + \kappa = \max(\kappa_i, \kappa_j) + +The parameters ``r``, ``q_i``, ``q_j``, ``\epsilon``, and ``\sigma`` +are the standard parameters for the electrostatic and Lennard-Jones +potentials. + +The soft-core parameters are: + +* ``α_i`` and ``α_j`` control the amount of "softening" of the + electrostatic and LJ interactions. A value of 0 means no softening + (fully hard), while a value of 1 means fully soft. Ghost atoms which + disappear as a function of λ have a value of α of 1 in the + reference state, and 0 in the perturbed state. Ghost atoms which appear + as a function of λ have a value of α of 0 in the reference + state, and 1 in the perturbed state. These values can be perturbed + via the ``alpha`` lever in the λ-schedule. + +* ``β`` is the Beutler alpha parameter that controls the softening of the + LJ interaction. It is set via the ``beutler_alpha`` map parameter and + defaults to 0.5. + +* ``shift_coulomb`` is the "shift delta" parameter for the electrostatic + potential. It is set via the ``shift_coulomb`` map parameter and + defaults to 1 Å. + +* ``κ_i`` and ``κ_j`` are the "hard" electrostatic parameters, + which control whether or not to calculate the "hard" electrostatic + interaction to subtract from the total energy and force (thus cancelling + out the double-counting of this interaction from the NonbondedForce). + By default, these are always equal to 1. You can perturb these via the + ``kappa`` lever in the λ-schedule, e.g. if you want to decouple the + intramolecular electrostatic interactions, when the "hard" interaction + would not be calculated in the NonbondedForce. + + Good practice ------------- diff --git a/doc/source/tutorial/part08/01_intro.rst b/doc/source/tutorial/part08/01_intro.rst index f68beef45..f9559715c 100644 --- a/doc/source/tutorial/part08/01_intro.rst +++ b/doc/source/tutorial/part08/01_intro.rst @@ -37,6 +37,7 @@ an engine to perform the calculation: ... cutoff="7.5A", ... neighbour_list_frequency=20, ... mechanical_embedding=False, +... switch_width=0.2, ... ) Here the first argument is the molecules that we are simulating, the second @@ -65,7 +66,7 @@ signature: xyz_mm: List[List[float]], cell: Optional[List[List[float]]] = None, idx_mm: Optional[List[int]] = None, - ) -> Tuple[float, List[List[float]], List[List[float]]]: + ) -> Tuple[float, List[List[float]], List[List[float]], Optional[List[float]]]: The function takes the atomic numbers of the QM atoms, the charges of the MM atoms in mod electron charge, the coordinates of the QM atoms in Angstrom, and @@ -75,10 +76,18 @@ QM/MM region. This is useful for obtaining any additional atomic properties that may be required by the callback. (Note that link atoms and virtual charges are always placed last in the list of MM charges and positions.) The function should return the calculated energy in kJ/mol, the forces on the QM atoms in -kJ/mol/nm, and the forces on the MM atoms in kJ/mol/nm. The remaining arguments -are optional and specify the QM cutoff distance, the neighbour list update -frequency, and whether the electrostatics should be treated with mechanical -embedding. When mechanical embedding is used, the electrostatics are treated +kJ/mol/nm, and the forces on the MM atoms in kJ/mol/nm. Optionally, it may also +return a fourth element: the gradient of the energy with respect to the effective +MM charges (dE/dq) in kJ/mol/e. When present, this is used to apply a chain-rule +force correction that accounts for the positional dependence of the charge switching +function (see below), which is important for systems with a charged ML region. The +remaining arguments are optional and specify the QM cutoff distance, the neighbour +list update frequency, whether the electrostatics should be treated with mechanical +embedding, and the width of the switching region as a fraction of the cutoff +(``switch_width``, default 0.2). The switching function smoothly scales the MM +charges to zero between ``(1 - switch_width) * cutoff`` and ``cutoff``, which +avoids force discontinuities as MM atoms enter or leave the cutoff sphere. Setting +``switch_width=0`` disables switching (not recommended for charged ML regions). When mechanical embedding is used, the electrostatics are treated at the MM level by ``OpenMM``. Note that this doesn't change the signature of the callback function, i.e. it will be passed empty lists for the MM specific arguments and should return an empty list for the MM forces. Atomic positions diff --git a/doc/source/tutorial/part08/02_emle.rst b/doc/source/tutorial/part08/02_emle.rst index b171d6292..d75662225 100644 --- a/doc/source/tutorial/part08/02_emle.rst +++ b/doc/source/tutorial/part08/02_emle.rst @@ -55,14 +55,20 @@ an engine to perform the calculation: ... mols[0], ... calculator, ... cutoff="7.5A", -... neighbour_list_frequency=20 +... neighbour_list_frequency=20, +... switch_width=0.2, ... ) Here the first argument is the molecules that we are simulating, the second selection coresponding to the QM region (here this is the first molecule), and -the third is calculator that was created above. The fourth and fifth arguments -are optional, and specify the QM cutoff distance and the neighbour list update -frequency respectively. (Shown are the default values.) The function returns a +the third is calculator that was created above. The remaining arguments are +optional and specify the QM cutoff distance, the neighbour list update frequency, +and the width of the switching region as a fraction of the cutoff (``switch_width``, +default 0.2). (Shown are the default values.) The switching function smoothly +scales the MM charges to zero between ``(1 - switch_width) * cutoff`` and +``cutoff``, which avoids force discontinuities as MM atoms enter or leave the +cutoff sphere. This is particularly important for systems with a charged ML region. +Setting ``switch_width=0`` disables switching. The function returns a modified version of the molecules containing a "merged" dipeptide that can be interpolated between MM and QM levels of theory, along with an engine. The engine registers a Python callback that uses ``emle-engine`` to perform the QM @@ -182,7 +188,7 @@ computes the electrostatic embedding: Next we create a new engine bound to the calculator: >>> _, engine = sr.qm.emle( ->>> ... mols, mols[0], calculator, cutoff="7.5A", neighbour_list_frequency=20 +>>> ... mols, mols[0], calculator, cutoff="7.5A", neighbour_list_frequency=20, switch_width=0.2 >>> ... ) .. note:: @@ -423,7 +429,7 @@ The model is serialisable, so can be saved and loaded using the standard It is also possible to use the model with Sire when performing QM/MM dynamics: >>> qm_mols, engine = sr.qm.emle( -... mols, mols[0], model, cutoff="7.5A", neighbour_list_frequency=20 +... mols, mols[0], model, cutoff="7.5A", neighbour_list_frequency=20, switch_width=0.2 ... ) The model will be serialised and loaded into a C++ ``TorchQMEngine`` object, diff --git a/pixi.toml b/pixi.toml index 29f2d820f..f0b424f23 100644 --- a/pixi.toml +++ b/pixi.toml @@ -95,12 +95,14 @@ gromacs = "*" alchemlyb = "*" mdtraj = "*" mdanalysis = "*" +openff-nagl = "*" # loch pyopencl = "*" pycuda = "*" [feature.obs.target.linux-aarch64.dependencies] gromacs = "*" +openff-nagl = "*" # ambertools, alchemlyb, mdtraj, mdanalysis not available on linux-aarch64 [feature.obs.target.osx-arm64.dependencies] @@ -109,6 +111,7 @@ ambertools = ">=22" alchemlyb = "*" mdtraj = "*" mdanalysis = "*" +openff-nagl = "*" # loch pyopencl = "*" @@ -130,6 +133,9 @@ loguru = "*" pygit2 = "*" pyyaml = "*" +[feature.emle.system-requirements] +cuda = "12" + [feature.emle.target.linux-64.dependencies] ambertools = ">=22" deepmd-kit = "*" diff --git a/ruff.toml b/ruff.toml new file mode 100644 index 000000000..cd7595702 --- /dev/null +++ b/ruff.toml @@ -0,0 +1,5 @@ +[lint] +ignore = ["E402"] + +[lint.per-file-ignores] +"tests/**" = ["F841"] diff --git a/setup.py b/setup.py index 5e12e62f0..f10ae77da 100644 --- a/setup.py +++ b/setup.py @@ -271,6 +271,31 @@ def add_default_cmake_defs(cmake_defs, ncores): cmake_defs.append(["SIRE_DISABLE_AVX512F=ON"]) +def _detect_vs_generator(): + # Map the installed VS major version to the CMake generator name. + # VS 2019=16, VS 2022=17, VS 2026=18 (and beyond). + _VS_GENERATORS = { + 18: "Visual Studio 18 2026", + 17: "Visual Studio 17 2022", + 16: "Visual Studio 16 2019", + 15: "Visual Studio 15 2017", + } + try: + result = subprocess.run( + ["vswhere.exe", "-nologo", "-latest", "-property", "installationVersion"], + capture_output=True, + text=True, + ) + if result.returncode == 0 and result.stdout.strip(): + major = int(result.stdout.strip().split(".")[0]) + for version in sorted(_VS_GENERATORS, reverse=True): + if major >= version: + return _VS_GENERATORS[version] + except Exception: + pass + return "Visual Studio 17 2022" + + def make_cmd(ncores, install=False): if is_windows: action = "INSTALL" if install else "ALL_BUILD" @@ -661,7 +686,7 @@ def install(ncores: int = 1, npycores: int = 1): action = args.action[0] if is_windows and (args.generator is None or len(args.generator) == 0): - args.generator = [["Visual Studio 17 2022"]] + args.generator = [[_detect_vs_generator()]] args.architecture = [["x64"]] elif is_macos: # fix compile bug when INSTALL_NAME_TOOL is not set diff --git a/src/sire/_match.py b/src/sire/_match.py index de83f9003..7ecffb8d4 100644 --- a/src/sire/_match.py +++ b/src/sire/_match.py @@ -91,7 +91,13 @@ def match_atoms( elif "KartografAtomMapper" in str(match.__class__): # use Kartograf to get the mapping - convert to RDKit then Kartograf from kartograf.atom_aligner import align_mol_shape - from kartograf import KartografAtomMapper, SmallMoleculeComponent + from kartograf import KartografAtomMapper + + try: + # kartograf >= 2.0 moved SmallMoleculeComponent to gufe + from gufe import SmallMoleculeComponent + except ImportError: + from kartograf import SmallMoleculeComponent if not isinstance(match, KartografAtomMapper): raise TypeError("match must be a KartografAtomMapper") diff --git a/src/sire/_pythonize.py b/src/sire/_pythonize.py index 00378867f..5eed9b85d 100644 --- a/src/sire/_pythonize.py +++ b/src/sire/_pythonize.py @@ -105,6 +105,10 @@ def _pythonize(C, delete_old: bool = True) -> None: # change "RDKit" to "Rdkit" new_attr = new_attr.replace("RDKit", "Rdkit") + # change "CMAP" into "Cmap" (it will then be converted to _cmap by + # the code below) + new_attr = new_attr.replace("CMAP", "Cmap") + # change "MCS" into "Mcs" (it will then be converted to _mcs by # the code below) new_attr = new_attr.replace("MCS", "Mcs") @@ -238,12 +242,13 @@ def _load_new_api_modules(delete_old: bool = True, is_base: bool = False): delete_old=delete_old, ) - # Pythonize the QM classes. - _pythonize(Convert._SireOpenMM.PyQMCallback, delete_old=delete_old) - _pythonize(Convert._SireOpenMM.PyQMEngine, delete_old=delete_old) - _pythonize(Convert._SireOpenMM.PyQMForce, delete_old=delete_old) - _pythonize(Convert._SireOpenMM.TorchQMEngine, delete_old=delete_old) - _pythonize(Convert._SireOpenMM.TorchQMForce, delete_old=delete_old) + # Pythonize the QM classes (only if OpenMM loaded successfully). + if Convert._has_openmm: + _pythonize(Convert._SireOpenMM.PyQMCallback, delete_old=delete_old) + _pythonize(Convert._SireOpenMM.PyQMEngine, delete_old=delete_old) + _pythonize(Convert._SireOpenMM.PyQMForce, delete_old=delete_old) + _pythonize(Convert._SireOpenMM.TorchQMEngine, delete_old=delete_old) + _pythonize(Convert._SireOpenMM.TorchQMForce, delete_old=delete_old) try: import lazy_import diff --git a/src/sire/mol/__init__.py b/src/sire/mol/__init__.py index 20a0d5ca8..77409ac90 100644 --- a/src/sire/mol/__init__.py +++ b/src/sire/mol/__init__.py @@ -971,7 +971,7 @@ def __fixed__dihedral__(obj, idx=None, idx1=None, idx2=None, idx3=None, map=None raise KeyError("There is no matching dihedral in this view.") elif len(dihedrals) > 1: raise KeyError( - "More than one dihedral matches. Number of " f"matches is {len(dihedrals)}." + f"More than one dihedral matches. Number of matches is {len(dihedrals)}." ) return dihedrals[0] @@ -986,7 +986,7 @@ def __fixed__improper__(obj, idx=None, idx1=None, idx2=None, idx3=None, map=None raise KeyError("There is no matching improper in this view.") elif len(impropers) > 1: raise KeyError( - "More than one improper matches. Number of " f"matches is {len(impropers)}." + f"More than one improper matches. Number of matches is {len(impropers)}." ) return impropers[0] @@ -1573,7 +1573,6 @@ def _dynamics( vacuum=None, shift_delta=None, shift_coulomb=None, - coulomb_power=None, restraints=None, fixed=None, platform=None, @@ -1735,11 +1734,6 @@ def _dynamics( softening potential that smooths the creation and deletion of ghost atoms during a potential. This defaults to 1.0 A. - coulomb_power: int - The coulomb power parmeter that controls the electrostatic - softening potential that smooths the creation and deletion - of ghost atoms during a potential. This defaults to 0. - restraints: sire.mm.Restraints or list[sire.mm.Restraints] A single set of restraints, or a list of sets of restraints that will be applied to the atoms during @@ -1978,7 +1972,6 @@ def _dynamics( lambda_value=lambda_value, shift_delta=shift_delta, shift_coulomb=shift_coulomb, - coulomb_power=coulomb_power, swap_end_states=swap_end_states, ignore_perturbations=ignore_perturbations, restraints=restraints, @@ -2003,7 +1996,6 @@ def _minimisation( vacuum=None, shift_delta=None, shift_coulomb=None, - coulomb_power=None, platform=None, device=None, precision=None, @@ -2087,11 +2079,6 @@ def _minimisation( softening potential that smooths the creation and deletion of ghost atoms during a potential. This defaults to 1.0 A. - coulomb_power: int - The coulomb power parmeter that controls the electrostatic - softening potential that smooths the creation and deletion - of ghost atoms during a potential. This defaults to 0. - restraints: sire.mm.Restraints or list[sire.mm.Restraints] A single set of restraints, or a list of sets of restraints that will be applied to the atoms during @@ -2211,7 +2198,6 @@ def _minimisation( ignore_perturbations=ignore_perturbations, shift_delta=shift_delta, shift_coulomb=shift_coulomb, - coulomb_power=coulomb_power, restraints=restraints, fixed=fixed, map=map, diff --git a/src/sire/mol/_dynamics.py b/src/sire/mol/_dynamics.py index 7b3ac4dbf..65feefb4a 100644 --- a/src/sire/mol/_dynamics.py +++ b/src/sire/mol/_dynamics.py @@ -483,6 +483,10 @@ def _exit_dynamics_block( nrg_sim_lambda_value = nrg if lambda_windows is not None: + # Positions have just changed (dynamics completed), so + # invalidate all cached per-group energies before the scan. + self._omm_mols.clear_energy_cache() + # get the index of the simulation lambda value in the # lambda windows list try: @@ -542,6 +546,10 @@ def _exit_dynamics_block( self._nrgs = nrgs self._nrgs_array = nrgs_array + # Repex synchronisation point: a peer replica may push new + # positions into this context, so the cache must be invalidated. + self._omm_mols.clear_energy_cache() + # update the interpolation lambda value if self._is_interpolate: if delta_lambda: @@ -853,14 +861,11 @@ def current_potential_energy(self): if self.is_null(): return 0 else: - from openmm.unit import kilocalorie_per_mole as _omm_kcal_mol - from ..units import kcal_per_mol as _sire_kcal_mol + return self._omm_mols.get_potential_energy(to_sire_units=True) - state = self._get_current_state() - - nrg = state.getPotentialEnergy() - - return nrg.value_in_unit(_omm_kcal_mol) * _sire_kcal_mol + def clear_energy_cache(self): + if not self.is_null(): + self._omm_mols.clear_energy_cache() def current_kinetic_energy(self): if self.is_null(): @@ -989,6 +994,9 @@ def run_minimisation( timeout=timeout.to(second), ) + # Positions changed during minimisation; invalidate the energy cache. + self._omm_mols.clear_energy_cache() + def _rebuild_and_minimise(self): if self.is_null(): return @@ -1030,38 +1038,28 @@ def _rebuild_and_minimise(self): if self._save_crash_report: import openmm import numpy as np - from copy import deepcopy from uuid import uuid4 # Create a unique identifier for this crash report. crash_id = str(uuid4())[:8] - # Get the current context and system. context = self._omm_mols - system = deepcopy(context.getSystem()) - - # Add each force to a unique group. - for i, f in enumerate(system.getForces()): - f.setForceGroup(i) - - # Create a new context. - new_context = openmm.Context(system, deepcopy(context.getIntegrator())) - new_context.setPositions(context.getState(getPositions=True).getPositions()) - # Write the energies for each force group. + # Write per-force-group energies using the groups already assigned + # by sire_to_openmm_system. with open(f"crash_{crash_id}.log", "w") as f: f.write(f"Current lambda: {str(self.get_lambda())}\n") - for i, force in enumerate(system.getForces()): - state = new_context.getState(getEnergy=True, groups={i}) - f.write(f"{force.getName()}, {state.getPotentialEnergy()}\n") + for name, grp in context._force_group_map.items(): + state = context.getState(getEnergy=True, groups=(1 << grp)) + f.write(f"{name}, {state.getPotentialEnergy()}\n") # Save the serialised system. with open(f"system_{crash_id}.xml", "w") as f: - f.write(openmm.XmlSerializer.serialize(system)) + f.write(openmm.XmlSerializer.serialize(context.getSystem())) # Save the positions. positions = ( - new_context.getState(getPositions=True).getPositions(asNumpy=True) + context.getState(getPositions=True).getPositions(asNumpy=True) / openmm.unit.nanometer ) np.savetxt(f"positions_{crash_id}.txt", positions) @@ -1615,7 +1613,6 @@ def __init__( ignore_perturbations=None, shift_delta=None, shift_coulomb=None, - coulomb_power=None, restraints=None, fixed=None, qm_engine=None, @@ -1645,7 +1642,6 @@ def __init__( if shift_coulomb is not None: _add_extra(extras, "shift_coulomb", u(shift_coulomb)) - _add_extra(extras, "coulomb_power", coulomb_power) _add_extra(extras, "restraints", restraints) _add_extra(extras, "fixed", fixed) _add_extra(extras, "qm_engine", qm_engine) @@ -2252,6 +2248,14 @@ def _current_energy_array(self): """ return self._d._current_energy_array() + def clear_energy_cache(self): + """ + Invalidate the energy cache. Call this whenever positions have been + changed externally (e.g. after a replica-exchange swap) so that the + next energy evaluation fully re-computes the potential energy. + """ + self._d.clear_energy_cache() + def to_xml(self, f=None): """ Save the current state of the dynamics to XML. diff --git a/src/sire/mol/_minimisation.py b/src/sire/mol/_minimisation.py index b6c89b9fe..088256d36 100644 --- a/src/sire/mol/_minimisation.py +++ b/src/sire/mol/_minimisation.py @@ -22,7 +22,6 @@ def __init__( ignore_perturbations=None, shift_delta=None, shift_coulomb=None, - coulomb_power=None, restraints=None, fixed=None, ): @@ -46,7 +45,6 @@ def __init__( if shift_coulomb is not None: _add_extra(extras, "shift_coulomb", u(shift_coulomb)) - _add_extra(extras, "coulomb_power", coulomb_power) _add_extra(extras, "restraints", restraints) _add_extra(extras, "fixed", fixed) diff --git a/src/sire/morph/_perturbation.py b/src/sire/morph/_perturbation.py index 4c7bab201..a42153d07 100644 --- a/src/sire/morph/_perturbation.py +++ b/src/sire/morph/_perturbation.py @@ -143,6 +143,7 @@ def __init__(self, mol, map=None): "atomtype", "bond", "charge", + "cmap", "coordinates", "dihedral", "element", diff --git a/src/sire/morph/_xml.py b/src/sire/morph/_xml.py index 19c3c864e..29bee2a1d 100644 --- a/src/sire/morph/_xml.py +++ b/src/sire/morph/_xml.py @@ -35,8 +35,6 @@ def evaluate_xml_force(mols, xml, force): The Lennard-Jones energies for each atom pair. """ - from math import sqrt - import xml.etree.ElementTree as ET import sys @@ -78,21 +76,23 @@ def evaluate_xml_force(mols, xml, force): ) # Validate the force name. - if not force in ["ghostghost", "ghostnonghost", "ghost14"]: + valid = [ + "ghostghost", + "ghostnonghost", + "ghost14", + ] + if force not in valid: raise ValueError( "'force' must be one of 'ghost-ghost', 'ghost-nonghost', or 'ghost-14'." ) - # Create the name and index based on the force type. - if force == "ghostghost": - name = "GhostGhostNonbondedForce" - elif force == "ghostnonghost": - name = "GhostNonGhostNonbondedForce" - elif force == "ghost14": - name = "Ghost14BondForce" - - # Get the root of the XML tree. - root = tree.getroot() + # Map sanitised name to the OpenMM force name in the XML. + _force_name_map = { + "ghostghost": "GhostGhostNonbondedForce", + "ghostnonghost": "GhostNonGhostNonbondedForce", + "ghost14": "Ghost14BondForce", + } + name = _force_name_map[force] # Loop over the forces until we find the named CustomNonbondedForce. is_found = False @@ -163,11 +163,8 @@ def evaluate_xml_force(mols, xml, force): atom_i = atoms[i] # Set the parameters for this particle. - setattr(module, parameters[0] + "1", float(particle_i.get("param1"))) - setattr(module, parameters[1] + "1", float(particle_i.get("param2"))) - setattr(module, parameters[2] + "1", float(particle_i.get("param3"))) - setattr(module, parameters[3] + "1", float(particle_i.get("param4"))) - setattr(module, parameters[4] + "1", float(particle_i.get("param5"))) + for k, param in enumerate(parameters): + setattr(module, param + "1", float(particle_i.get(f"param{k + 1}"))) # Loop over particles in set2. for y in range(len(set2)): @@ -190,11 +187,8 @@ def evaluate_xml_force(mols, xml, force): atom_j = atoms[j] # Set the parameters for this particle. - setattr(module, parameters[0] + "2", float(particle_j.get("param1"))) - setattr(module, parameters[1] + "2", float(particle_j.get("param2"))) - setattr(module, parameters[2] + "2", float(particle_j.get("param3"))) - setattr(module, parameters[3] + "2", float(particle_j.get("param4"))) - setattr(module, parameters[4] + "2", float(particle_j.get("param5"))) + for k, param in enumerate(parameters): + setattr(module, param + "2", float(particle_j.get(f"param{k + 1}"))) # Get the distance between the particles. r = measure(atom_i, atom_j).to(nanometer) @@ -239,11 +233,8 @@ def evaluate_xml_force(mols, xml, force): atom_j = atoms[int(bond.get("p2"))] # Set the parameters for this bond. - setattr(module, parameters[0], float(bond.get("param1"))) - setattr(module, parameters[1], float(bond.get("param2"))) - setattr(module, parameters[2], float(bond.get("param3"))) - setattr(module, parameters[3], float(bond.get("param4"))) - setattr(module, parameters[4], float(bond.get("param5"))) + for k, param in enumerate(parameters): + setattr(module, param, float(bond.get(f"param{k + 1}"))) # Get the distance between the particles. r = measure(atom_i, atom_j).to(nanometer) diff --git a/src/sire/qm/__init__.py b/src/sire/qm/__init__.py index 6e02245f0..090c31f22 100644 --- a/src/sire/qm/__init__.py +++ b/src/sire/qm/__init__.py @@ -19,6 +19,7 @@ def create_engine( neighbour_list_frequency=0, mechanical_embedding=False, redistribute_charge=True, + switch_width=0.2, map=None, ): """ @@ -74,6 +75,13 @@ def create_engine( molecule, the excess charge is redistributed over the MM atoms within the residues of the QM region. + switch_width : float, optional, default=0.2 + The width of the switching region as a fraction of the cutoff (0 to 1). + A quintic switching function is applied to the MM charges over the last + ``switch_width * cutoff`` angstroms before the cutoff, smoothly scaling + them to zero. Set to 0 or None to disable switching (not recommended + for production MD with charged ML regions). + Returns ------- @@ -133,6 +141,15 @@ def create_engine( if not isinstance(redistribute_charge, bool): raise TypeError("'redistribute_charge' must be of type 'bool'") + if switch_width is None: + switch_width = 0.0 + if not isinstance(switch_width, (int, float)): + raise TypeError("'switch_width' must be of type 'int' or 'float'") + switch_width = float(switch_width) + if switch_width < 0.0 or switch_width > 1.0: + raise ValueError("'switch_width' must be between 0 and 1") + use_switch = switch_width > 0.0 + if map is not None: if not isinstance(map, dict): raise TypeError("'map' must be of type 'dict'") @@ -147,6 +164,10 @@ def create_engine( mechanical_embedding, ) + # Configure the switching function. + engine.set_switch_width(switch_width) + engine.set_use_switch(use_switch) + from ._utils import ( _check_charge, _create_qm_mol_to_atoms, diff --git a/src/sire/qm/_emle.py b/src/sire/qm/_emle.py index 7e6b0eec6..0e619ae46 100644 --- a/src/sire/qm/_emle.py +++ b/src/sire/qm/_emle.py @@ -108,6 +108,7 @@ def emle( cutoff="7.5A", neighbour_list_frequency=0, redistribute_charge=True, + switch_width=0.2, map=None, ): """ @@ -143,6 +144,13 @@ def emle( molecule, the excess charge is redistributed over the MM atoms within the residues of the QM region. + switch_width : float, optional, default=0.2 + The width of the switching region as a fraction of the cutoff (0 to 1). + A quintic switching function is applied to the MM charges over the last + ``switch_width * cutoff`` angstroms before the cutoff, smoothly scaling + them to zero. Set to 0 or None to disable switching (not recommended + for production MD with charged ML regions). + Returns ------- @@ -159,7 +167,6 @@ def emle( try: import torch as _torch - from emle.models import EMLE as _EMLE has_model = True except: @@ -227,6 +234,15 @@ def emle( if not isinstance(redistribute_charge, bool): raise TypeError("'redistribute_charge' must be of type 'bool'") + if switch_width is None: + switch_width = 0.0 + if not isinstance(switch_width, (int, float)): + raise TypeError("'switch_width' must be of type 'int' or 'float'") + switch_width = float(switch_width) + if switch_width < 0.0 or switch_width > 1.0: + raise ValueError("'switch_width' must be between 0 and 1") + use_switch = switch_width > 0.0 + if map is not None: if not isinstance(map, dict): raise TypeError("'map' must be of type 'dict'") @@ -246,7 +262,7 @@ def emle( # Create an engine from an EMLE model. else: try: - from emle.models import EMLE as _EMLE + pass except: raise ImportError( "Could not import emle.models. Please reinstall emle-engine and try again." @@ -276,6 +292,10 @@ def emle( except Exception as e: raise ValueError("Unable to create a TorchEMLEEngine: " + str(e)) + # Configure the switching function. + engine.set_switch_width(switch_width) + engine.set_use_switch(use_switch) + from ._utils import ( _check_charge, _create_qm_mol_to_atoms, diff --git a/src/sire/restraints/_restraints.py b/src/sire/restraints/_restraints.py index c7b23ea83..37bc78723 100644 --- a/src/sire/restraints/_restraints.py +++ b/src/sire/restraints/_restraints.py @@ -732,7 +732,9 @@ def morse_potential( de=None, use_pbc=None, name=None, - auto_parametrise=False, + auto_parametrise=True, + direct_morse_replacement=True, + retain_harmonic_bond=False, map=None, ): """ @@ -795,6 +797,16 @@ def morse_potential( and 'atoms1', the equilibrium distance r0 will be set to the original bond length, and the force constant k will be set to the force constant of the bond in the unperturbed state. Note that 'de' must still be provided. + Default is True. + + direct_morse_replacement : bool, optional + If True, and if auto_parametrise is True, then the function will attempt to directly + replace an existing bond with a Morse potential. Default is True. + + retain_harmonic_bond : bool, optional + If True, and if auto_parametrise is True, then the function will only nullify the force + constant of the existing harmonic bond, rather than removing the bond potential entirely. + If False, then the existing harmonic bond will be removed entirely. Default is False. Returns @@ -803,6 +815,9 @@ def morse_potential( A container of Morse restraints, where the first restraint is the MorsePotentialRestraint created. The Morse restraint created can be extracted with MorsePotentialRestraints[0]. + + mols : sire.system._system.System + The system containing the atoms, which will have been modified if auto_parametrise is True and direct_morse_replacement is True. """ from .. import u @@ -875,10 +890,7 @@ def morse_potential( atom0_idx = [bond_name.atom0().index().value()][0] atom1_idx = [bond_name.atom1().index().value()][0] - # Divide k0 by 2 to convert from force constant to sire half - # force constant k if k is None: - k0 = k0 / 2.0 k0 = u(f"{k0} kJ mol-1 nm-2") k = [k0] @@ -897,6 +909,47 @@ def morse_potential( f"molecule property is_perturbable and atomidx {atom1_idx}" ] break + if direct_morse_replacement: + from ..legacy import MM as _MM + import re as _re + from ..legacy.CAS import Symbol as _Symbol + + search_pattern = r"r - (\d+\.\d+)" + mol = mol[0] + info = mol.info() + + # We need to loop through both bond0 and bond1 properties, as we don't know + # which one the bond of interest will be in (bond forming or bond breaking) + for bond_prop in ("bond0", "bond1"): + bonds = mol.property(bond_prop) + new_bonds = _MM.TwoAtomFunctions(info) + + for p in bonds.potentials(): + idx0 = info.atom_idx(p.atom0()) + idx1 = info.atom_idx(p.atom1()) + + # Attempt to match the bond of interest using previously identified atom indices + if idx0.value() == atom0_idx and idx1.value() == atom1_idx: + bond_potential_string = p.function().to_string() + match = _re.search(search_pattern, bond_potential_string) + + if not match: + raise ValueError( + f"No match found in the string: {bond_potential_string}" + ) + + # If retaining the harmonic bond, then set the harmonic bond force constant + # to zero. Otherwise, remove the harmonic bond from the force list. + if retain_harmonic_bond: + r = float(match.group(1)) + amber_bond = _MM.AmberBond(0, r) + expression = amber_bond.to_expression(_Symbol("r")) + new_bonds.set(idx0, idx1, expression) + else: + new_bonds.set(idx0, idx1, p.function()) + + mol = mol.edit().set_property(bond_prop, new_bonds).molecule().commit() + mols.update(mol) try: atoms0 = _to_atoms(mols, atoms0) @@ -943,7 +996,7 @@ def morse_potential( except: raise ValueError(f"Unable to parse 'de' as a Sire GeneralUnit: {de}") - mols = mols.atoms() + atoms = mols.atoms() if name is None: restraints = MorsePotentialRestraints() @@ -951,8 +1004,8 @@ def morse_potential( restraints = MorsePotentialRestraints(name=name) for i, (atom0, atom1) in enumerate(zip(atoms0, atoms1)): - idxs0 = mols.find(atom0) - idxs1 = mols.find(atom1) + idxs0 = atoms.find(atom0) + idxs1 = atoms.find(atom1) if type(idxs0) is int: idxs0 = [idxs0] @@ -989,7 +1042,7 @@ def morse_potential( # Set the use_pbc flag. restraints.set_uses_pbc(use_pbc) - return restraints + return restraints, mols def bond(*args, use_pbc=False, **kwargs): diff --git a/src/sire/system/_system.py b/src/sire/system/_system.py index 0846f535b..37a7175ee 100644 --- a/src/sire/system/_system.py +++ b/src/sire/system/_system.py @@ -440,11 +440,6 @@ def minimisation(self, *args, **kwargs): creation and deletion of ghost atoms during a potential. This defaults to 2.0 A. - coulomb_power: int - The coulomb power parmeter that controls the electrostatic - softening potential that smooths the creation and deletion - of ghost atoms during a potential. This defaults to 0. - vacuum: bool Whether or not to run the simulation in vacuum. If this is set to `True`, then the simulation space automatically be @@ -626,11 +621,6 @@ def dynamics(self, *args, **kwargs): creation and deletion of ghost atoms during a potential. This defaults to 2.0 A. - coulomb_power: int - The coulomb power parmeter that controls the electrostatic - softening potential that smooths the creation and deletion - of ghost atoms during a potential. This defaults to 0. - restraints: sire.mm.Restraints or list[sire.mm.Restraints] A single set of restraints, or a list of sets of restraints that will be applied to the atoms during @@ -948,7 +938,7 @@ def set_energy_trajectory(self, trajectory, map=None): if trajectory.what() != "SireMaths::EnergyTrajectory": raise TypeError( - f"You cannot set a {type(trajectory)} as an " "energy trajectory!" + f"You cannot set a {type(trajectory)} as an energy trajectory!" ) self._system.set_property(traj_propname.source(), trajectory) diff --git a/tests/cas/test_lambdaschedule.py b/tests/cas/test_lambdaschedule.py index 3b4213784..1f411d222 100644 --- a/tests/cas/test_lambdaschedule.py +++ b/tests/cas/test_lambdaschedule.py @@ -154,3 +154,476 @@ def test_lambdaschedule(): 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 + + +def test_coupled_lever_default(): + """cmap::cmap_grid should follow torsion::torsion_k by default.""" + l = sr.cas.LambdaSchedule.standard_morph() + morph_equation = (1 - l.lam()) * l.initial() + l.lam() * l.final() + + # With no custom equations, both should return the stage default. + _assert_same_equation( + l.lam(), + l.get_equation(stage="morph", force="torsion", lever="torsion_k"), + morph_equation, + ) + _assert_same_equation( + l.lam(), + l.get_equation(stage="morph", force="cmap", lever="cmap_grid"), + morph_equation, + ) + + +def test_coupled_lever_follows_torsion_k(): + """Setting a custom torsion_k equation should automatically apply to cmap_grid.""" + l = sr.cas.LambdaSchedule.standard_morph() + custom_eq = l.lam() ** 2 * l.final() + (1 - l.lam() ** 2) * l.initial() + + l.set_equation( + stage="morph", force="torsion", lever="torsion_k", equation=custom_eq + ) + + # cmap_grid should now follow the custom torsion_k equation via coupling. + _assert_same_equation( + l.lam(), + l.get_equation(stage="morph", force="cmap", lever="cmap_grid"), + custom_eq, + ) + + +def test_coupled_lever_explicit_override(): + """An explicit cmap_grid equation should take precedence over the coupling.""" + l = sr.cas.LambdaSchedule.standard_morph() + torsion_eq = l.lam() ** 2 * l.final() + (1 - l.lam() ** 2) * l.initial() + cmap_eq = l.initial() # freeze CMAP at λ=0 + + l.set_equation( + stage="morph", force="torsion", lever="torsion_k", equation=torsion_eq + ) + l.set_equation(stage="morph", force="cmap", lever="cmap_grid", equation=cmap_eq) + + # cmap_grid should use its own explicit equation, not torsion_k's. + _assert_same_equation( + l.lam(), + l.get_equation(stage="morph", force="cmap", lever="cmap_grid"), + cmap_eq, + ) + # torsion_k should be unaffected. + _assert_same_equation( + l.lam(), + l.get_equation(stage="morph", force="torsion", lever="torsion_k"), + torsion_eq, + ) + + +def test_remove_coupled_lever(): + """Removing the coupling makes cmap_grid fall back to the stage default.""" + l = sr.cas.LambdaSchedule.standard_morph() + morph_equation = (1 - l.lam()) * l.initial() + l.lam() * l.final() + custom_eq = l.lam() ** 2 * l.final() + (1 - l.lam() ** 2) * l.initial() + + l.set_equation( + stage="morph", force="torsion", lever="torsion_k", equation=custom_eq + ) + l.remove_coupled_lever(force="cmap", lever="cmap_grid") + + # cmap_grid should now use the stage default, not follow torsion_k. + _assert_same_equation( + l.lam(), + l.get_equation(stage="morph", force="cmap", lever="cmap_grid"), + morph_equation, + ) + + +def test_couple_lever_custom(): + """coupleLever can set an arbitrary coupling between levers.""" + l = sr.cas.LambdaSchedule.standard_morph() + custom_eq = l.lam() ** 2 * l.final() + (1 - l.lam() ** 2) * l.initial() + + # Couple bond_k to torsion_k (unusual, but should work). + l.couple_lever( + force="bond", + lever="bond_k", + fallback_force="torsion", + fallback_lever="torsion_k", + ) + l.set_equation( + stage="morph", force="torsion", lever="torsion_k", equation=custom_eq + ) + + _assert_same_equation( + l.lam(), + l.get_equation(stage="morph", force="bond", lever="bond_k"), + custom_eq, + ) + + +def test_stage_weights_default(): + """New stages should have weight 1.0 by default.""" + l = sr.cas.LambdaSchedule.standard_morph() + + assert l.get_stage_weights() == [1.0] + assert l.get_stage_weight("morph") == pytest.approx(1.0) + + +def test_stage_weights_equal_split(): + """With equal weights, lambda space is split evenly (backward compat).""" + l = sr.cas.LambdaSchedule.standard_morph() + l.add_morph_stage("morph2") + + assert l.get_stage_weights() == [1.0, 1.0] + + # Boundary between stages is at 0.5 + assert l.get_stage(0.0) == "morph" + assert l.get_stage(0.49) == "morph" + assert l.get_stage(0.5) == "morph2" + assert l.get_stage(1.0) == "morph2" + + # Local lambda at midpoint of each stage + assert l.get_lambda_in_stage(0.25) == pytest.approx(0.5) + assert l.get_lambda_in_stage(0.75) == pytest.approx(0.5) + + +def test_stage_weights_three_equal(): + """Three equal-weight stages each occupy one third of lambda space.""" + l = sr.cas.LambdaSchedule() + l.add_stage("a", l.lam()) + l.add_stage("b", l.lam()) + l.add_stage("c", l.lam()) + + assert l.get_stage_weights() == [1.0, 1.0, 1.0] + + assert l.get_stage(0.0) == "a" + assert l.get_stage(1 / 3 - 0.001) == "a" + assert l.get_stage(1 / 3) == "b" + assert l.get_stage(2 / 3 - 0.001) == "b" + assert l.get_stage(2 / 3) == "c" + assert l.get_stage(1.0) == "c" + + # Local lambda at the start of each stage + assert l.get_lambda_in_stage(0.0) == pytest.approx(0.0) + assert l.get_lambda_in_stage(1 / 3) == pytest.approx(0.0, abs=1e-10) + assert l.get_lambda_in_stage(2 / 3) == pytest.approx(0.0, abs=1e-10) + + +def test_stage_weights_unequal_two_stages(): + """A stage with weight 2 occupies twice the lambda range of weight 1.""" + l = sr.cas.LambdaSchedule() + l.add_stage("small", l.lam(), weight=1.0) + l.add_stage("large", l.lam(), weight=2.0) + + assert l.get_stage_weights() == [1.0, 2.0] + + # Stage boundary is at 1/3 + assert l.get_stage(0.0) == "small" + assert l.get_stage(1 / 3 - 0.001) == "small" + assert l.get_stage(1 / 3) == "large" + assert l.get_stage(1.0) == "large" + + # Local lambda at midpoint of small stage (lambda=1/6) + assert l.get_lambda_in_stage(1 / 6) == pytest.approx(0.5) + + # Local lambda at midpoint of large stage (lambda=1/3 + 1/3 = 2/3) + assert l.get_lambda_in_stage(2 / 3) == pytest.approx(0.5) + + # lambda=0 is start of small stage + assert l.get_lambda_in_stage(0.0) == pytest.approx(0.0) + + # lambda=1 is end of large stage + assert l.get_lambda_in_stage(1.0) == pytest.approx(1.0) + + +def test_stage_weights_morph_value(): + """With weights [1, 2], morph produces correct interpolated values.""" + l = sr.cas.LambdaSchedule() + l.add_morph_stage("a", weight=1.0) + l.add_morph_stage("b", weight=2.0) + + # At global lambda=1/6 we are halfway through stage "a" (local lam=0.5) + assert l.morph(initial=0.0, final=1.0, lambda_value=1 / 6) == pytest.approx(0.5) + + # At global lambda=1/3 we are at the start of stage "b" (local lam=0.0) + assert l.morph(initial=0.0, final=1.0, lambda_value=1 / 3) == pytest.approx(0.0) + + # At global lambda=2/3 we are halfway through stage "b" (local lam=0.5) + assert l.morph(initial=0.0, final=1.0, lambda_value=2 / 3) == pytest.approx(0.5) + + +def test_set_stage_weight(): + """set_stage_weight updates a weight after the stage is added.""" + l = sr.cas.LambdaSchedule.standard_morph() + l.add_morph_stage("morph2") + + l.set_stage_weight("morph", 1.0) + l.set_stage_weight("morph2", 3.0) + + assert l.get_stage_weight("morph") == pytest.approx(1.0) + assert l.get_stage_weight("morph2") == pytest.approx(3.0) + assert l.get_stage_weights() == pytest.approx([1.0, 3.0]) + + # Boundary is now at 0.25 (1 out of 4 total weight units) + assert l.get_stage(0.24) == "morph" + assert l.get_stage(0.25) == "morph2" + + +def test_stage_weight_prepend(): + """prepend_stage respects the weight argument.""" + l = sr.cas.LambdaSchedule() + l.add_stage("b", l.lam(), weight=1.0) + l.prepend_stage("a", l.lam(), weight=3.0) + + assert l.get_stage_weights() == pytest.approx([3.0, 1.0]) + assert l.get_stages() == ["a", "b"] + + # Stage "a" occupies 3/4 of lambda space; boundary at 0.75 + assert l.get_stage(0.74) == "a" + assert l.get_stage(0.75) == "b" + + +def test_stage_weight_insert(): + """insert_stage respects the weight argument.""" + l = sr.cas.LambdaSchedule() + l.add_stage("first", l.lam(), weight=1.0) + l.add_stage("last", l.lam(), weight=1.0) + l.insert_stage(1, "middle", l.lam(), weight=2.0) + + assert l.get_stages() == ["first", "middle", "last"] + assert l.get_stage_weights() == pytest.approx([1.0, 2.0, 1.0]) + + # Total weight=4; boundaries at 0.25 and 0.75 + assert l.get_stage(0.24) == "first" + assert l.get_stage(0.25) == "middle" + assert l.get_stage(0.74) == "middle" + assert l.get_stage(0.75) == "last" + + +def test_stage_weight_remove(): + """remove_stage also removes the corresponding weight.""" + l = sr.cas.LambdaSchedule() + l.add_stage("a", l.lam(), weight=1.0) + l.add_stage("b", l.lam(), weight=2.0) + l.add_stage("c", l.lam(), weight=3.0) + + l.remove_stage("b") + + assert l.get_stages() == ["a", "c"] + assert l.get_stage_weights() == pytest.approx([1.0, 3.0]) + + +def test_stage_weight_clear(): + """clear() removes stage weights alongside stages.""" + l = sr.cas.LambdaSchedule() + l.add_stage("a", l.lam(), weight=2.0) + l.clear() + + assert l.get_stages() == [] + assert l.get_stage_weights() == [] + + +def test_stage_weight_invalid(): + """A non-positive weight should raise an error.""" + l = sr.cas.LambdaSchedule() + + with pytest.raises(Exception): + l.add_stage("a", l.lam(), weight=0.0) + + with pytest.raises(Exception): + l.add_stage("b", l.lam(), weight=-1.0) + + l.add_stage("c", l.lam()) + + with pytest.raises(Exception): + l.set_stage_weight("c", 0.0) + + with pytest.raises(Exception): + l.set_stage_weight("c", -0.5) + + +def test_stage_weight_add_morph_stage(): + """add_morph_stage accepts a weight argument.""" + l = sr.cas.LambdaSchedule() + l.add_morph_stage("decharge", weight=1.0) + l.add_morph_stage("morph", weight=2.0) + l.add_morph_stage("recharge", weight=1.0) + + assert l.get_stage_weights() == pytest.approx([1.0, 2.0, 1.0]) + + # "morph" occupies the middle half (0.25 to 0.75) + assert l.get_stage(0.24) == "decharge" + assert l.get_stage(0.25) == "morph" + assert l.get_stage(0.74) == "morph" + assert l.get_stage(0.75) == "recharge" + + +def test_stage_weight_add_decouple_stage(): + """add_decouple_stage accepts a weight argument.""" + l = sr.cas.LambdaSchedule() + l.add_morph_stage("pre", weight=1.0) + l.add_decouple_stage("decouple", weight=2.0) + + assert l.get_stage_weights() == pytest.approx([1.0, 2.0]) + assert l.get_stage(0.33) == "pre" + assert l.get_stage(0.34) == "decouple" + + +def test_stage_weight_add_annihilate_stage(): + """add_annihilate_stage accepts a weight argument.""" + l = sr.cas.LambdaSchedule() + l.add_annihilate_stage("annihilate", weight=3.0) + l.add_morph_stage("morph", weight=1.0) + + assert l.get_stage_weights() == pytest.approx([3.0, 1.0]) + # Boundary at 0.75 + assert l.get_stage(0.74) == "annihilate" + assert l.get_stage(0.75) == "morph" + + +def test_stage_weight_copy(): + """Copying a LambdaSchedule preserves weights.""" + import copy + + l = sr.cas.LambdaSchedule() + l.add_morph_stage("a", weight=1.0) + l.add_morph_stage("b", weight=3.0) + + l2 = copy.copy(l) + assert l2.get_stage_weights() == pytest.approx([1.0, 3.0]) + + # Modifying the copy should not affect the original + l2.set_stage_weight("b", 1.0) + assert l.get_stage_weight("b") == pytest.approx(3.0) + + +def test_stage_weight_equality(): + """Two schedules with different weights are not equal.""" + l1 = sr.cas.LambdaSchedule() + l1.add_morph_stage("a", weight=1.0) + l1.add_morph_stage("b", weight=2.0) + + l2 = sr.cas.LambdaSchedule() + l2.add_morph_stage("a", weight=1.0) + l2.add_morph_stage("b", weight=1.0) + + assert l1 != l2 + + l2.set_stage_weight("b", 2.0) + assert l1 == l2 + + +def test_stage_weight_tostring(): + """toString shows weights when any stage has a non-default weight.""" + l = sr.cas.LambdaSchedule() + l.add_morph_stage("a", weight=1.0) + l.add_morph_stage("b", weight=2.0) + + s = str(l) + assert "weight=1" in s + assert "weight=2" in s + + # With all equal weights, no weight annotation + l2 = sr.cas.LambdaSchedule.standard_morph() + assert "weight" not in str(l2) + + +def test_stage_weight_get_stage_boundaries(): + """get_stage and get_lambda_in_stage agree at stage boundaries.""" + l = sr.cas.LambdaSchedule() + l.add_morph_stage("a", weight=1.0) + l.add_morph_stage("b", weight=3.0) + + # Total weight = 4; boundary at 0.25 + assert l.get_stage(0.0) == "a" + assert l.get_stage(1.0) == "b" + + # Local lambda is 0.0 at start of each stage + assert l.get_lambda_in_stage(0.0) == pytest.approx(0.0) + assert l.get_lambda_in_stage(0.25) == pytest.approx(0.0, abs=1e-10) + + # Local lambda is 1.0 at end of last stage + assert l.get_lambda_in_stage(1.0) == pytest.approx(1.0) + + # Midpoint of stage "a": lambda=0.125 → local lam=0.5 + assert l.get_lambda_in_stage(0.125) == pytest.approx(0.5) + + # Midpoint of stage "b": lambda=0.25 + 1.5/4 = 0.625 → local lam=0.5 + assert l.get_lambda_in_stage(0.625) == pytest.approx(0.5) + + +def test_reverse_stage_order(): + """Reversing reverses the order of stage names.""" + l = sr.cas.LambdaSchedule() + l.add_stage("a", l.lam()) + l.add_stage("b", l.lam()) + l.add_stage("c", l.lam()) + + r = l.reverse() + assert r.get_stages() == ["c", "b", "a"] + + +def test_reverse_stage_weights(): + """Reversing preserves stage weights in reversed order.""" + l = sr.cas.LambdaSchedule() + l.add_stage("a", l.lam(), weight=1.0) + l.add_stage("b", l.lam(), weight=2.0) + l.add_stage("c", l.lam(), weight=3.0) + + r = l.reverse() + assert r.get_stage_weights() == pytest.approx([3.0, 2.0, 1.0]) + + +def test_reverse_invariant(): + """reversed.morph(init, fin, λ) == original.morph(fin, init, 1-λ).""" + import random + + random.seed(42) + + for schedule in [ + sr.cas.LambdaSchedule.standard_morph(), + sr.cas.LambdaSchedule.charge_scaled_morph(), + sr.cas.LambdaSchedule.standard_decouple(), + ]: + r = schedule.reverse() + for _ in range(50): + lam_val = random.uniform(0.0, 1.0) + assert r.morph( + initial=0.0, final=1.0, lambda_value=lam_val + ) == pytest.approx( + schedule.morph(initial=1.0, final=0.0, lambda_value=1.0 - lam_val), + abs=1e-10, + ) + + +def test_reverse_standard_morph_is_identity(): + """Reversing a standard morph schedule gives the same behaviour (it is symmetric).""" + import random + + random.seed(42) + l = sr.cas.LambdaSchedule.standard_morph() + r = l.reverse() + + for _ in range(50): + lam_val = random.uniform(0.0, 1.0) + assert l.morph(initial=0.0, final=1.0, lambda_value=lam_val) == pytest.approx( + r.morph(initial=0.0, final=1.0, lambda_value=lam_val), abs=1e-10 + ) + + +def test_reverse_twice_is_identity(): + """Reversing a schedule twice recovers the original behaviour.""" + import random + + random.seed(42) + + for schedule in [ + sr.cas.LambdaSchedule.standard_morph(), + sr.cas.LambdaSchedule.charge_scaled_morph(), + sr.cas.LambdaSchedule.standard_decouple(), + ]: + rr = schedule.reverse().reverse() + for _ in range(50): + lam_val = random.uniform(0.0, 1.0) + assert schedule.morph( + initial=0.0, final=1.0, lambda_value=lam_val + ) == pytest.approx( + rr.morph(initial=0.0, final=1.0, lambda_value=lam_val), abs=1e-10 + ) diff --git a/tests/conftest.py b/tests/conftest.py index 49f3b42c8..816684d5e 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -228,6 +228,16 @@ def solvated_neopentane_methane(): return sr.load_test_files("neo_meth_solv.bss") +@pytest.fixture(scope="session") +def ghost_14_bug(): + return sr.load_test_files("ghost_14_bug.bss") + + +@pytest.fixture(scope="session") +def cyclopropyl_constraint_bug(): + return sr.load_test_files("ejm_31_jmc_28.bss") + + @pytest.fixture(scope="session") def zero_lj_mols(): return sr.load_test_files("zero_lj.prm7", "zero_lj.rst7") diff --git a/tests/convert/test_openmm.py b/tests/convert/test_openmm.py index d2efb7c15..c5335ccea 100644 --- a/tests/convert/test_openmm.py +++ b/tests/convert/test_openmm.py @@ -489,3 +489,85 @@ def test_openmm_membrane_barostat(ala_mols, openmm_platform): assert barostat.getXYMode() == MonteCarloMembraneBarostat.XYIsotropic assert barostat.getZMode() == MonteCarloMembraneBarostat.ZFree assert barostat.getFrequency() == 50 + + +@pytest.mark.skipif( + "openmm" not in sr.convert.supported_formats(), + reason="openmm support is not available", +) +def test_openmm_ghost_14_bug(ghost_14_bug, openmm_platform): + """ + Regression test for a bug where a pair involving a ghost atom whose + intrascale exclusion differs between end states (excluded at lambda=0, + a real 1-4 scale at lambda=1, because the connectivity path between the + two atoms only exists at lambda=1) never had a slot allocated in the + Ghost14BondForce. The construction-time check that decides whether a + pair needs a ghost-14 slot only looked at the lambda=0 exception scale, + so a pair that's excluded at lambda=0 but not at lambda=1 was silently + skipped, then permanently excluded from every nonbonded force for the + lifetime of the simulation - even at lambda=1, where it should have a + real interaction. + + The test molecule (cyclopropane -> propane, a minimal "ring-breaking" + perturbation with no protein or water) reproduces this exactly: atom 2 + (a ring/chain carbon) and atoms 9, 10, 11 (new hydrogens that only + exist on atom 0 at lambda=1, once the propane chain needs an extra H + that the cyclopropane ring didn't) are 1-3 excluded at lambda=0 and a + real 1-4 pair, (0.8333, 0.5), at lambda=1. + """ + mols = sr.morph.link_to_reference(ghost_14_bug) + mol = mols[0] + + d = mol.dynamics( + schedule=sr.cas.LambdaSchedule.standard_morph(), + platform=openmm_platform, + cutoff="none", + ) + + def get_force(d, name): + for force in d.context().getSystem().getForces(): + if force.getName() == name: + return force + return None + + # the bug-triggering pairs - atom 2 (C3) with the three new hydrogens + # on atom 0 (C1) that only exist at lambda=1 + pairs = {(2, 9), (2, 10), (2, 11)} + + def get_ghost14_params(d): + ghost14 = get_force(d, "Ghost14BondForce") + assert ghost14 is not None + + found = {} + for i in range(ghost14.getNumBonds()): + p1, p2, params = ghost14.getBondParameters(i) + key = (min(p1, p2), max(p1, p2)) + if key in pairs: + found[key] = list(params) + return found + + # at every lambda, all three pairs must have a slot in Ghost14BondForce + # at all (this is what the bug broke - they were silently missing) + d.set_lambda(0.0) + params0 = get_ghost14_params(d) + assert set(params0.keys()) == pairs + + d.set_lambda(0.5) + params_mid = get_ghost14_params(d) + assert set(params_mid.keys()) == pairs + + d.set_lambda(1.0) + params1 = get_ghost14_params(d) + assert set(params1.keys()) == pairs + + # at lambda=0 the pair is fully excluded, matching intrascale0 = (0,0) + # (four_epsilon is parameter index 2) + for params in params0.values(): + assert params[2] == pytest.approx(0.0, abs=1e-6) + + # the interaction should grow smoothly and monotonically as lambda + # goes from 0 to 1, ending up clearly nonzero (matching the real + # intrascale1 = (0.8333, 0.5) 1-4 scale) + for key in pairs: + assert params0[key][2] < params_mid[key][2] < params1[key][2] + assert params1[key][2] > 0.1 diff --git a/tests/convert/test_openmm_cmap.py b/tests/convert/test_openmm_cmap.py new file mode 100644 index 000000000..496554afb --- /dev/null +++ b/tests/convert/test_openmm_cmap.py @@ -0,0 +1,170 @@ +import sire as sr +import pytest + + +@pytest.mark.skipif( + "openmm" not in sr.convert.supported_formats(), + reason="openmm support is not available", +) +def test_openmm_cmap_energy(tmpdir, multichain_cmap, openmm_platform): + """ + Verify that Sire correctly adds CMAPTorsionForce to the OpenMM context by + comparing the total potential energy against a context built directly via + the OpenMM Python API from the same input files. + + The multichain_cmap fixture is a periodic solvated system with three protein + chains, each carrying CMAP backbone correction terms. Using it exercises the + multi-molecule CMAP code path in the conversion layer. + """ + import openmm + import openmm.app + import openmm.unit + + mols = sr.system.System() + mols.add(multichain_cmap[0]) + mols.add(multichain_cmap[1]) + mols.add(multichain_cmap[2]) + + # Sanity-check: at least two molecules must carry CMAP so that the + # multi-chain code path is exercised. + cmap_mol_count = sum(1 for mol in mols.molecules() if mol.has_property("cmap")) + assert ( + cmap_mol_count >= 2 + ), "Expected at least two molecules with CMAP terms in multichain_cmap" + + # Save the Sire system to AMBER files so the direct OpenMM path reads the + # same topology and coordinates that Sire uses internally. + dir_path = str(tmpdir.mkdir("cmap_omm")) + prm7 = str(sr.save(mols, f"{dir_path}/system.prm7")[0]) + rst7 = str(sr.save(mols, f"{dir_path}/system.rst7")[0]) + + platform_name = openmm_platform or "CPU" + + # Create an OpenMM context via Sire's conversion layer, then get the + # potential energy. + sire_map = { + "constraint": "none", + "cutoff": "none", + "cutoff_type": "none", + "platform": platform_name, + } + omm_sire = sr.convert.to(mols, "openmm", map=sire_map) + sire_energy = ( + omm_sire.getState(getEnergy=True) + .getPotentialEnergy() + .value_in_unit(openmm.unit.kilojoules_per_mole) + ) + + # Create an OpenMM context directly from the AMBER files and get the + # potential energy. + prmtop = openmm.app.AmberPrmtopFile(prm7) + inpcrd = openmm.app.AmberInpcrdFile(rst7) + + omm_system = prmtop.createSystem( + nonbondedMethod=openmm.app.NoCutoff, + constraints=None, + ) + + integrator = openmm.VerletIntegrator(0.001) + platform = openmm.Platform.getPlatformByName(platform_name) + omm_context = openmm.Context(omm_system, integrator, platform) + omm_context.setPositions(inpcrd.positions) + if inpcrd.boxVectors is not None: + omm_context.setPeriodicBoxVectors(*inpcrd.boxVectors) + + direct_energy = ( + omm_context.getState(getEnergy=True) + .getPotentialEnergy() + .value_in_unit(openmm.unit.kilojoules_per_mole) + ) + + # Energies should agree to within 1 kJ/mol. + assert sire_energy == pytest.approx(direct_energy, abs=1.0) + + +@pytest.mark.skipif( + "openmm" not in sr.convert.supported_formats(), + reason="openmm support is not available", +) +def test_openmm_cmap_perturbable(openmm_platform): + """ + Verify that CMAPTorsionForce grids are correctly updated by the lambda lever + for a perturbable molecule with a genuine single-residue mutation. + + The pre-merged stream file merged_molecule_cmap.s3 contains a perturbable + ubiquitin chain with a T9A mutation. cmap0 and cmap1 differ for exactly + one torsion (the one centred on the mutated residue). The test checks that: + + - CMAP torsions are present at both end states. + - At least one torsion grid differs between lambda=0 and lambda=1. + - Most torsion grids are unchanged (only the mutated residue is affected). + """ + import openmm + import openmm.unit + + platform_name = openmm_platform or "CPU" + + mols_pert = sr.load_test_files("merged_molecule_cmap.s3") + mols_pert = sr.morph.link_to_reference(mols_pert) + + omm_map = { + "constraint": "none", + "cutoff": "none", + "cutoff_type": "none", + "platform": platform_name, + } + + def get_cmap_torsion_grids(context): + """Return list of (size, grid) for each CMAP torsion, dereferencing + the map index. Grid values are plain floats (kJ/mol).""" + system = context.getSystem() + for force in system.getForces(): + if isinstance(force, openmm.CMAPTorsionForce): + maps = [] + for i in range(force.getNumMaps()): + size, grid = force.getMapParameters(i) + grid_floats = [ + v.value_in_unit(openmm.unit.kilojoules_per_mole) for v in grid + ] + maps.append((size, grid_floats)) + result = [] + for t in range(force.getNumTorsions()): + map_idx = force.getTorsionParameters(t)[0] + result.append(maps[map_idx]) + return result + return [] + + omm_pert = sr.convert.to(mols_pert, "openmm", map=omm_map) + + omm_pert.set_lambda(0.0) + grids_lam0 = get_cmap_torsion_grids(omm_pert) + + omm_pert.set_lambda(1.0) + grids_lam1 = get_cmap_torsion_grids(omm_pert) + + assert len(grids_lam0) > 0, "No CMAP torsions at lambda=0" + assert len(grids_lam1) == len( + grids_lam0 + ), f"Torsion count differs between end states: {len(grids_lam0)} vs {len(grids_lam1)}" + + differing = sum( + 1 + for (s0, g0), (s1, g1) in zip(grids_lam0, grids_lam1) + if s0 != s1 or any(round(a, 3) != round(b, 3) for a, b in zip(g0, g1)) + ) + + assert differing > 0, ( + "Expected at least one torsion grid to differ between lambda=0 and lambda=1 " + "for a genuine single-residue mutation" + ) + assert differing < len( + grids_lam0 + ), "Expected most torsion grids to be unchanged for a single-residue mutation" + + # Verify that changed_cmaps() correctly identifies the same set of + # differing torsions as the direct grid comparison above. + p_omm = mols_pert.molecule(0).perturbation().to_openmm(map=omm_map) + changed = p_omm.changed_cmaps() + assert ( + len(changed) == differing + ), f"changed_cmaps() returned {len(changed)} torsions but expected {differing}" diff --git a/tests/convert/test_openmm_constraints.py b/tests/convert/test_openmm_constraints.py index ef98bb276..e856864e2 100644 --- a/tests/convert/test_openmm_constraints.py +++ b/tests/convert/test_openmm_constraints.py @@ -454,6 +454,72 @@ def test_auto_constraints(ala_mols, openmm_platform): assert bond in constrained +@pytest.mark.skipif( + "openmm" not in sr.convert.supported_formats(), + reason="openmm support is not available", +) +def test_cyclopropyl_anchor_bond_constraint( + cyclopropyl_constraint_bug, openmm_platform +): + """ + Regression test for an unperturbed anchor bond (to two alternate + heavy-atom substituents) not being constrained under + "h-bonds-not-heavy-perturbed", unlike SOMD1. + """ + mols = sr.morph.link_to_reference(cyclopropyl_constraint_bug) + mol = mols[0] + + # restrict light-atom detection to the maximum end-state mass, as SOMD2 + # does, rather than Sire's broader (and individually buggy) defaults + map = { + "check_for_h_by_max_mass": True, + "check_for_h_by_mass": False, + "check_for_h_by_element": False, + "check_for_h_by_ambertype": False, + } + + d = mol.dynamics( + constraint="h-bonds-not-heavy-perturbed", + platform=openmm_platform, + cutoff="none", + map=map, + ) + + anchor_idx = sr.mol.AtomIdx(17) + substituent_idxs = {sr.mol.AtomIdx(19), sr.mol.AtomIdx(32)} + + def get_anchor_constraints(d): + found = {} + + for constraint in d.get_constraints(): + idx0 = constraint[0][0].index() + idx1 = constraint[0][1].index() + + if anchor_idx in (idx0, idx1): + other = idx1 if idx0 == anchor_idx else idx0 + + if other in substituent_idxs: + found[other] = constraint[1].value() + + return found + + found = get_anchor_constraints(d) + + assert set(found.keys()) == substituent_idxs + + # both anchor bonds should have the same, static length, matching + # SOMD1's value for this generic aromatic-C/sp3-C bond + for length in found.values(): + assert length == pytest.approx(1.51234, abs=1e-3) + + # the constraint should remain static (not lambda-dependent), since the + # bond's own parameters don't change between end states + d.set_lambda(1.0) + found_at_1 = get_anchor_constraints(d) + + assert found_at_1 == found + + @pytest.mark.skipif( "openmm" not in sr.convert.supported_formats(), reason="openmm support is not available", diff --git a/tests/convert/test_openmm_force_groups.py b/tests/convert/test_openmm_force_groups.py new file mode 100644 index 000000000..d3c3c2359 --- /dev/null +++ b/tests/convert/test_openmm_force_groups.py @@ -0,0 +1,281 @@ +""" +Tests for force-group assignment and energy caching in SOMMContext / LambdaLever. + +Checks: + - Named forces are assigned unique, valid OpenMM force group indices. + - The cached energy matches a direct getState() evaluation at several lambda values. + - Repeated get_potential_energy() calls without a state change return the same value. + - The energy cache is cleared by setPositions(), setState(), setPeriodicBoxVectors(), + and set_lambda(). +""" + +import pytest +import sire as sr + + +@pytest.fixture(scope="module") +def perturbable_omm(merged_ethane_methanol, openmm_platform): + """Return a SOMMContext built from the merged ethane/methanol molecule.""" + mols = merged_ethane_methanol.clone() + + # Pin coordinates to lambda-0 end state so energies are well-behaved. + c = mols.cursor() + c["molidx 0"]["coordinates"] = c["molidx 0"]["coordinates0"] + c["molidx 0"]["coordinates1"] = c["molidx 0"]["coordinates0"] + mols = c.commit() + + l = sr.cas.LambdaSchedule() + l.add_stage("morph", (1 - l.lam()) * l.initial() + l.lam() * l.final()) + + map = { + "platform": openmm_platform, + "schedule": l, + "constraint": "h-bonds-not-perturbed", + "include_constrained_energies": True, + "dynamic_constraints": False, + } + + return sr.convert.to(mols[0], "openmm", map=map) + + +pytestmark = pytest.mark.skipif( + "openmm" not in sr.convert.supported_formats(), + reason="openmm support is not available", +) + + +def test_force_groups_assigned(perturbable_omm): + """Every expected named force has a non-negative force group index.""" + lever = perturbable_omm.get_lambda_lever() + + force_names = lever.get_force_names() + assert len(force_names) > 0, "No force names registered on LambdaLever" + + for name in force_names: + grp = lever.get_force_group(name) + assert grp >= 0, f"Force '{name}' has invalid group index {grp}" + + +def test_cached_energy_matches_full(perturbable_omm): + """ + get_potential_energy() must match a direct getState() at lambda=0, 0.5, and 1. + """ + import openmm + + omm = perturbable_omm + + for lam in (0.0, 0.5, 1.0): + omm.set_lambda(lam) + + cached_kj = omm.get_potential_energy(to_sire_units=False).value_in_unit( + openmm.unit.kilojoule_per_mole + ) + full_kj = ( + omm.getState(getEnergy=True) + .getPotentialEnergy() + .value_in_unit(openmm.unit.kilojoule_per_mole) + ) + + assert cached_kj == pytest.approx(full_kj, abs=1e-3), ( + f"Cached energy {cached_kj:.6f} kJ/mol != full energy {full_kj:.6f} kJ/mol " + f"at lambda={lam}" + ) + + +def test_cache_stable_without_state_change(perturbable_omm): + """ + Two consecutive get_potential_energy() calls with no intervening state + change must return identical values (second call served from cache). + """ + omm = perturbable_omm + omm.set_lambda(0.5) + + nrg1 = omm.get_potential_energy(to_sire_units=True).value() + nrg2 = omm.get_potential_energy(to_sire_units=True).value() + + assert nrg1 == pytest.approx( + nrg2, rel=1e-10 + ), "Energy changed between two consecutive calls with no state change" + + +def test_set_positions_invalidates_cache(perturbable_omm): + """setPositions() must clear the energy cache.""" + omm = perturbable_omm + omm.set_lambda(0.0) + + # Populate the cache. + _ = omm.get_potential_energy(to_sire_units=False) + assert "_total" in omm._energy_cache, "Cache should be populated after evaluation" + + # Set the same positions back — content unchanged but must still invalidate. + positions = omm.getState(getPositions=True).getPositions() + omm.setPositions(positions) + + assert len(omm._energy_cache) == 0, "Cache should be empty after setPositions()" + + +def test_set_state_invalidates_cache(perturbable_omm): + """setState() must clear the energy cache.""" + omm = perturbable_omm + omm.set_lambda(0.0) + + # Populate the cache. + _ = omm.get_potential_energy(to_sire_units=False) + assert "_total" in omm._energy_cache, "Cache should be populated after evaluation" + + # Round-trip through setState using the current state. + state = omm.getState(getPositions=True) + omm.setState(state) + + assert len(omm._energy_cache) == 0, "Cache should be empty after setState()" + + +def test_set_periodic_box_vectors_invalidates_cache(perturbable_omm): + """setPeriodicBoxVectors() must clear the energy cache.""" + omm = perturbable_omm + omm.set_lambda(0.0) + + # Populate the cache. + _ = omm.get_potential_energy(to_sire_units=False) + assert "_total" in omm._energy_cache, "Cache should be populated after evaluation" + + # Set the same box vectors back — must still invalidate. + box = omm.getState(getPositions=True).getPeriodicBoxVectors() + omm.setPeriodicBoxVectors(*box) + + assert ( + len(omm._energy_cache) == 0 + ), "Cache should be empty after setPeriodicBoxVectors()" + + +def test_set_lambda_invalidates_cache(perturbable_omm): + """set_lambda() must clear the energy cache.""" + omm = perturbable_omm + omm.set_lambda(0.0) + + # Populate the cache. + _ = omm.get_potential_energy(to_sire_units=False) + assert "_total" in omm._energy_cache, "Cache should be populated after evaluation" + + omm.set_lambda(0.5) + + assert len(omm._energy_cache) == 0, "Cache should be empty after set_lambda()" + + +# --------------------------------------------------------------------------- +# Tests for hasChanged() / wasForceChanged() — verifying that the C++ lambda +# lever correctly detects when morphed parameter values actually change vs +# when they are pinned and unchanged. +# --------------------------------------------------------------------------- + +# Levers that control each named force. +_FORCE_LEVERS = { + "bond": ["bond_k", "bond_length"], + "angle": ["angle_k", "angle_size"], + "torsion": ["torsion_k", "torsion_phase"], + "clj": ["charge", "sigma", "epsilon", "alpha", "kappa", "charge_scale", "lj_scale"], + # cmap_grid is the only lever for CMAPTorsionForce. By default it is coupled + # to torsion_k, so without an explicit equation it would morph whenever + # torsion_k does. _make_fixed_schedule sets an explicit equation (l.initial()) + # which breaks that coupling and pins CMAP independently. + # Tested with a molecule that has perturbable CMAP terms (merged_molecule_cmap.s3). + "cmap": ["cmap_grid"], +} + +# Forces whose changed-state is tied to the "clj" levers. +_CLJ_RELATED = {"clj", "ghost/ghost", "ghost/non-ghost", "ghost-14"} + + +def _make_fixed_schedule(fixed_levers): + """ + Return a single-stage LambdaSchedule that morphs all parameters linearly, + except for the levers listed in *fixed_levers* which are pinned to initial. + """ + l = sr.cas.LambdaSchedule() + l.add_stage("morph", (1 - l.lam()) * l.initial() + l.lam() * l.final()) + for lever in fixed_levers: + l.set_equation(stage="morph", lever=lever, equation=l.initial()) + return l + + +@pytest.mark.parametrize("fixed_force", list(_FORCE_LEVERS.keys())) +def test_fixed_lever_not_changed(merged_ethane_methanol, openmm_platform, fixed_force): + """ + When all levers controlling a force are pinned to their initial values, + wasForceChanged() must return False for that force after a lambda step. + All other forces (whose levers still morph) must return True. + """ + fixed_levers = _FORCE_LEVERS[fixed_force] + schedule = _make_fixed_schedule(fixed_levers) + + if fixed_force == "cmap": + # merged_molecule_cmap.s3 is a perturbable ubiquitin chain (T9A mutation) + # that carries genuine CMAP backbone correction terms at both end states. + mols = sr.load_test_files("merged_molecule_cmap.s3") + mols = sr.morph.link_to_reference(mols) + omm = sr.convert.to( + mols, + "openmm", + map={ + "platform": openmm_platform or "CPU", + "schedule": schedule, + "constraint": "none", + "cutoff": "none", + "cutoff_type": "none", + }, + ) + else: + mols = merged_ethane_methanol.clone() + c = mols.cursor() + c["molidx 0"]["coordinates"] = c["molidx 0"]["coordinates0"] + c["molidx 0"]["coordinates1"] = c["molidx 0"]["coordinates0"] + mols = c.commit() + omm = sr.convert.to( + mols[0], + "openmm", + map={ + "platform": openmm_platform, + "schedule": schedule, + "constraint": "h-bonds-not-perturbed", + "include_constrained_energies": True, + "dynamic_constraints": False, + }, + ) + + lever = omm.get_lambda_lever() + + # Prime at lambda=0 so prev_cache is populated for the next step. + omm.set_lambda(0.0) + + # Advance lambda — hasChanged() now compares against the lambda=0 values. + omm.set_lambda(0.5) + + # The pinned force must NOT be marked changed. + if fixed_force == "clj": + for name in _CLJ_RELATED: + if lever.get_force_group(name) >= 0: + assert not lever.was_force_changed( + name + ), f"'{name}' should not be changed when its levers are pinned" + else: + assert not lever.was_force_changed( + fixed_force + ), f"'{fixed_force}' should not be changed when its levers are pinned" + + # All other morphing forces must be marked changed. + # Exclude "cmap": molecules without CMAP terms have no CMAP parameters to + # change, so was_force_changed("cmap") is correctly False regardless of + # pinning. The reverse direction (CMAP not changed when pinned) is covered + # by the fixed_force="cmap" case which uses a CMAP molecule. + other_forces = set(_FORCE_LEVERS.keys()) - {fixed_force, "cmap"} + for other in other_forces: + if other == "clj": + if lever.get_force_group("clj") >= 0: + assert lever.was_force_changed( + "clj" + ), f"'clj' should be changed (fixed_force='{fixed_force}')" + else: + if lever.get_force_group(other) >= 0: + assert lever.was_force_changed( + other + ), f"'{other}' should be changed (fixed_force='{fixed_force}')" diff --git a/tests/convert/test_openmm_lrc.py b/tests/convert/test_openmm_lrc.py new file mode 100644 index 000000000..f575b353c --- /dev/null +++ b/tests/convert/test_openmm_lrc.py @@ -0,0 +1,175 @@ +""" +Validate that the CustomVolumeForce-based LRC in the perturbable OpenMM system +gives the same total energy as a non-perturbable end-state system that uses the +standard NonbondedForce dispersion correction. + +Tests use merged_ethane_methanol (merged_molecule.s3) with the 5 nearest waters +so they run quickly while still exercising a periodic cutoff system. + +A separate test exercises the GCMC water LRC (GCMCLRCForce) using the ala_mols +system (alanine-dipeptide in water). GCMC is faked by decrementing n_w by hand +and verifying the energy change against the analytical formula. +""" + +import pytest +import sire as sr + + +def _get_end_state(mol, state, remove_state): + c = mol.cursor() + for key in c.keys(): + if key.endswith(state): + c[key.removesuffix(state)] = c[key] + del c[key] + elif key.endswith(remove_state): + del c[key] + c["is_perturbable"] = False + return c.commit() + + +def _build_systems(mols, platform): + space = mols.space() + + c = mols.cursor() + c["molidx 0"]["coordinates"] = c["molidx 0"]["coordinates0"] + c["molidx 0"]["coordinates1"] = c["molidx 0"]["coordinates0"] + mols = c.commit() + + merge = mols[0] + water = mols["closest 5 waters to molidx 0"] + + mols_pert = merge + water + mols_ref = _get_end_state(merge, "0", "1") + water + mols_pert_end = _get_end_state(merge, "1", "0") + water + + l = sr.cas.LambdaSchedule() + l.add_stage("morph", (1 - l.lam()) * l.initial() + l.lam() * l.final()) + + map = { + "platform": platform, + "schedule": l, + "constraint": "h-bonds-not-perturbed", + "include_constrained_energies": True, + "dynamic_constraints": False, + "use_dispersion_correction": True, + "space": space, + } + + omm_pert = sr.convert.to(mols_pert, "openmm", map=map) + omm_ref = sr.convert.to(mols_ref, "openmm", map=map) + omm_pert_end = sr.convert.to(mols_pert_end, "openmm", map=map) + + return omm_pert, omm_ref, omm_pert_end + + +@pytest.mark.skipif( + "openmm" not in sr.convert.supported_formats(), + reason="openmm support is not available", +) +def test_lrc_lambda0_matches_reference_end_state( + merged_ethane_methanol, openmm_platform +): + """ + Perturbable system at lambda=0 must give the same energy as a freshly built + non-perturbable reference (lambda=0) end state with standard NonbondedForce LRC. + """ + omm_pert, omm_ref, _ = _build_systems( + merged_ethane_methanol.clone(), openmm_platform + ) + + omm_pert.set_lambda(0.0) + nrg_pert = omm_pert.get_energy().value() + nrg_ref = omm_ref.get_energy().value() + + assert nrg_pert == pytest.approx(nrg_ref, rel=1e-3) + + +@pytest.mark.skipif( + "openmm" not in sr.convert.supported_formats(), + reason="openmm support is not available", +) +def test_lrc_lambda1_matches_perturbed_end_state( + merged_ethane_methanol, openmm_platform +): + """ + Perturbable system at lambda=1 must give the same energy as a freshly built + non-perturbable perturbed (lambda=1) end state with standard NonbondedForce LRC. + """ + omm_pert, _, omm_pert_end = _build_systems( + merged_ethane_methanol.clone(), openmm_platform + ) + + omm_pert.set_lambda(1.0) + nrg_pert = omm_pert.get_energy().value() + nrg_pert_end = omm_pert_end.get_energy().value() + + assert nrg_pert == pytest.approx(nrg_pert_end, rel=1e-3) + + +@pytest.mark.skipif( + "openmm" not in sr.convert.supported_formats(), + reason="openmm support is not available", +) +def test_gcmc_lrc(ala_mols, openmm_platform): + """ + Test the GCMCLRCForce by faking a GCMC move: decrement n_w by 1 and verify + the energy change matches the analytical formula. + + E(n_w) = (n_w * lrc_w_solute + n_w * (n_w - 1) * lrc_ww_half) / V + + dE = E(n_w - 1) - E(n_w) = -(lrc_w_solute + 2 * (n_w - 1) * lrc_ww_half) / V + """ + # Build with GCMC LRC enabled, reserving 1 buffer water so n_w starts at + # n_waters - 1. + d = ala_mols.dynamics( + platform=openmm_platform, + map={ + "use_dispersion_correction": True, + "use_gcmc_lrc": True, + "num_gcmc_waters": 1, + }, + ) + d.set_lambda(0.0) + + context = d.context() + omm_system = context.getSystem() + + # Locate the GCMCLRCForce and its force group. + gcmc_force = None + for force in omm_system.getForces(): + if force.getName() == "GCMCLRCForce": + gcmc_force = force + break + assert gcmc_force is not None, "GCMCLRCForce not found in system" + fg = gcmc_force.getForceGroup() + + # Read the pre-computed LRC coefficients and initial n_w from the context. + state = context.getState(getParameters=True, getPositions=True) + params = state.getParameters() + n_w = params["n_w"] + lrc_w_solute = params["lrc_w_solute"] + lrc_ww_half = params["lrc_ww_half"] + + # The LJ tail is attractive so both coefficients must be negative. + assert lrc_w_solute < 0 + assert lrc_ww_half < 0 + assert n_w > 0 + + # Get the GCMC LRC energy at the initial n_w. + nrg1 = ( + context.getState(getEnergy=True, groups=(1 << fg)).getPotentialEnergy()._value + ) + + # "Turn off" one water by decrementing n_w. + context.setParameter("n_w", n_w - 1) + nrg2 = ( + context.getState(getEnergy=True, groups=(1 << fg)).getPotentialEnergy()._value + ) + + # Compute the expected energy change analytically. + # Box vectors are in nm; volume in nm^3 matches the lrc_coeff units (kJ/mol·nm^3). + box = state.getPeriodicBoxVectors() + V = box[0][0]._value * box[1][1]._value * box[2][2]._value + + expected_delta = -(lrc_w_solute + 2 * (n_w - 1) * lrc_ww_half) / V + assert nrg2 - nrg1 == pytest.approx(expected_delta, rel=1e-5) diff --git a/tests/convert/test_openmm_vsites.py b/tests/convert/test_openmm_vsites.py new file mode 100644 index 000000000..fd1208c58 --- /dev/null +++ b/tests/convert/test_openmm_vsites.py @@ -0,0 +1,300 @@ +import sire as sr +import pytest + + +@pytest.mark.skipif( + "openmm" not in sr.convert.supported_formats(), + reason="openmm support is not available", +) +def test_vsite_params(ethane_12dichloroethane, openmm_platform): + # Can we create an openmm system with the correct vsite parameters and charges + mols = ethane_12dichloroethane + + # Just dichloroethane + mol = mols[0].property("molecule1") + + # Set vsite properties + vsite_dict = { + "0": { + "vs_indices": [0, 1, 2], + "vs_ows": [1, 0, 0], + "vs_xs": [1, -1, 0], + "vs_ys": [0, 1, -1], + "vs_local": [0.03, 0, 0], + }, + "1": { + "vs_indices": [3, 2, 1], + "vs_ows": [1, 0, 0], + "vs_xs": [1, -1, 0], + "vs_ys": [0, 1, -1], + "vs_local": [0.03, 0, 0], + }, + } + + parents_dict = {str(atom_i): [] for atom_i in range(mol.num_atoms())} + for v, vs in enumerate(vsite_dict): + parent = vsite_dict[vs]["vs_indices"][0] + parents_dict[str(parent)].append(v) + + n_virtual_sites = len(vsite_dict) + vs_charges = [0.2, 0.2] + + cursor = mol.cursor() + cursor.set("n_virtual_sites", n_virtual_sites) + cursor.set("vs_charges", vs_charges) + cursor.set("virtual_sites", vsite_dict) + cursor.set("parents", parents_dict) + mol = cursor.commit() + + map = {"platform": openmm_platform} + + # Create openmm system + omm = sr.convert.to(mol, "openmm", map=map) + + omm_system = omm.getSystem() + + # Check the right number of virtual sites are added + assert omm_system.getNumParticles() == mol.num_atoms() + n_virtual_sites + + # Check the VS parameters and charges are correct + + from openmm import LocalCoordinatesSite, Vec3, unit + + nb_force = next( + force for force in omm_system.getForces() if force.getName() == "NonbondedForce" + ) + + for vs_index in range(n_virtual_sites): + omm_vs = omm_system.getVirtualSite(mol.num_atoms() + vs_index) + assert isinstance(omm_vs, LocalCoordinatesSite) + + origin_weights = omm_vs.getOriginWeights() + assert list(origin_weights) == vsite_dict[str(vs_index)]["vs_ows"] + + x_weights = omm_vs.getXWeights() + assert list(x_weights) == vsite_dict[str(vs_index)]["vs_xs"] + + y_weights = omm_vs.getYWeights() + assert list(y_weights) == vsite_dict[str(vs_index)]["vs_ys"] + + local_position = omm_vs.getLocalPosition() + assert local_position.value_in_unit(unit.nanometer) == Vec3( + *[x for x in vsite_dict[str(vs_index)]["vs_local"]] + ) + + nb_params = nb_force.getParticleParameters(mol.num_atoms() + vs_index) + assert vs_charges[vs_index] == nb_params[0].value_in_unit( + unit.elementary_charge + ) + + +@pytest.mark.skipif( + "openmm" not in sr.convert.supported_formats(), + reason="openmm support is not available", +) +def test_vsite_pertubation(ethane_12dichloroethane, openmm_platform): + # Are vsite parameters scaled correctly by lambda + mols = ethane_12dichloroethane + + # Just dichloroethane + mol = mols[0] + + # Set vsite properties + vsite_dict = { + "0": { + "vs_indices": [0, 1, 2], + "vs_ows": [1, 0, 0], + "vs_xs": [1, -1, 0], + "vs_ys": [0, 1, -1], + "vs_local": [0.03, 0, 0], + }, + "1": { + "vs_indices": [3, 2, 1], + "vs_ows": [1, 0, 0], + "vs_xs": [1, -1, 0], + "vs_ys": [0, 1, -1], + "vs_local": [0.03, 0, 0], + }, + } + + parents_dict = {str(atom_i): [] for atom_i in range(mol.num_atoms())} + for v, vs in enumerate(vsite_dict): + parent = vsite_dict[vs]["vs_indices"][0] + parents_dict[str(parent)].append(v) + + n_virtual_sites = len(vsite_dict) + vs_charges0 = [0.1, 0.1] + vs_charges1 = [0.2, 0.2] + + cursor = mol.cursor() + cursor.set("n_virtual_sites", n_virtual_sites) + cursor.set("vs_charges0", vs_charges0) + cursor.set("vs_charges1", vs_charges1) + cursor.set("virtual_sites", vsite_dict) + cursor.set("parents", parents_dict) + mol = cursor.commit() + + mol = sr.morph.link_to_reference(mol) + + from openmm import unit + + for lam in [0.0, 0.5, 1.0]: + d = mol.dynamics(lambda_value=lam, platform=openmm_platform) + system = d.context().getSystem() + nb_force = next( + force for force in system.getForces() if force.getName() == "NonbondedForce" + ) + for vs_index in range(n_virtual_sites): + expected_charge = (1 - lam) * vs_charges0[vs_index] + lam * vs_charges1[ + vs_index + ] + nb_charge = nb_force.getParticleParameters(mol.num_atoms() + vs_index)[0] + assert expected_charge == nb_charge.value_in_unit(unit.elementary_charge) + + +@pytest.mark.skipif( + "openmm" not in sr.convert.supported_formats(), + reason="openmm support is not available", +) +def test_vsite_restraints(solvated_neopentane_methane, openmm_platform): + # Are restraints added correctly to vsite systems + mols = solvated_neopentane_methane + + mol0 = mols[0] + + # Arbitrary vsite definition, we just want to check that the restraints + # are correctly mapped to the new system with vsites + vsite_dict = { + "0": { + "vs_indices": [0, 1, 2], + "vs_ows": [1, 0, 0], + "vs_xs": [1, -1, 0], + "vs_ys": [0, 1, -1], + "vs_local": [0.03, 0, 0], + } + } + + parents_dict = {str(atom_i): [] for atom_i in range(mol0.num_atoms())} + for v, vs in enumerate(vsite_dict): + parent = vsite_dict[vs]["vs_indices"][0] + parents_dict[str(parent)].append(v) + + n_virtual_sites = len(vsite_dict) + vs_charges0 = [0.1, 0.1] + vs_charges1 = [0.2, 0.2] + + cursor = mol0.cursor() + cursor.set("n_virtual_sites", n_virtual_sites) + cursor.set("vs_charges0", vs_charges0) + cursor.set("vs_charges1", vs_charges1) + cursor.set("virtual_sites", vsite_dict) + cursor.set("parents", parents_dict) + mol0 = cursor.commit() + mols.update(mol0) + + mols = sr.morph.link_to_reference(mols) + + start_mol1 = mol0.num_atoms() + n_virtual_sites + base_particles = sum(m.num_atoms() for m in mols) + n_virtual_sites + + # Applying restraints to the first N water atoms (not realistic but we + # just want to check the mapping is correct) + restr_atoms = mols[1:].atoms() + + restraint_cases = [ + ( + "bond", + lambda system: sr.restraints.bond( + system, atoms0=restr_atoms[0], atoms1=restr_atoms[1] + ), + "BondRestraintForce", + lambda force: ( + force.getBondParameters(0)[:2] == [start_mol1 + 0, start_mol1 + 1] + ), + ), + ( + "inverse_bond", + lambda system: sr.restraints.inverse_bond( + system, atoms0=restr_atoms[1], atoms1=restr_atoms[2] + ), + "InverseBondRestraintForce", + lambda force: ( + force.getBondParameters(0)[:2] == [start_mol1 + 1, start_mol1 + 2] + ), + ), + ( + "morse_potential", + lambda system: sr.restraints.morse_potential( + system, + atoms0=restr_atoms[0], + atoms1=restr_atoms[1], + r0="1.5A", + k="100 kcal mol-1 A-2", + de="25 kcal mol-1", + auto_parametrise=False, + )[0], + "MorsePotentialRestraintForce", + lambda force: ( + force.getBondParameters(0)[:2] == [start_mol1 + 0, start_mol1 + 1] + ), + ), + ( + "positional", + lambda system: sr.restraints.positional(system, atoms=restr_atoms[0]), + "PositionalRestraintForce", + lambda force: ( + force.getBondParameters(0)[0] == start_mol1 + 0 + and force.getBondParameters(0)[1] >= base_particles + or force.getBondParameters(0)[1] == start_mol1 + 0 + and force.getBondParameters(0)[0] >= base_particles + ), + ), + ( + "angle", + lambda system: sr.restraints.angle(system, atoms=restr_atoms[:3]), + "AngleRestraintForce", + lambda force: ( + force.getAngleParameters(0)[:3] + == [start_mol1 + 0, start_mol1 + 1, start_mol1 + 2] + ), + ), + ( + "dihedral", + lambda system: sr.restraints.dihedral(system, atoms=restr_atoms[:4]), + "TorsionRestraintForce", + lambda force: ( + force.getTorsionParameters(0)[:4] + == [start_mol1 + 0, start_mol1 + 1, start_mol1 + 2, start_mol1 + 3] + ), + ), + ( + "boresch", + lambda system: sr.restraints.boresch( + system, + receptor=restr_atoms[0:3], + ligand=restr_atoms[3:6], + ), + "BoreschRestraintForce", + lambda force: ( + list(force.getBondParameters(0)[0]) + == [ + start_mol1 + 0, + start_mol1 + 1, + start_mol1 + 2, + start_mol1 + 3, + start_mol1 + 4, + start_mol1 + 5, + ] + ), + ), + ] + + for restraint_name, build_restraints, force_name, validate in restraint_cases: + restraints = build_restraints(mols) + d = mols.dynamics(restraints=restraints, platform=openmm_platform) + system = d.context().getSystem() + force = next( + force for force in system.getForces() if force.getName() == force_name + ) + + assert validate(force), f"Incorrect atom mapping for {restraint_name}" diff --git a/tests/convert/test_openmm_water.py b/tests/convert/test_openmm_water.py new file mode 100644 index 000000000..7d671dda4 --- /dev/null +++ b/tests/convert/test_openmm_water.py @@ -0,0 +1,197 @@ +import math + +import pytest +import sire as sr + +_skip_no_openmm = pytest.mark.skipif( + "openmm" not in sr.convert.supported_formats(), + reason="openmm support is not available", +) + + +@pytest.fixture(scope="module") +def tip3p_mols(): + return sr.load_test_files("tip3p.s3") + + +@pytest.fixture(scope="module") +def tip4p_mols(): + return sr.load_test_files("tip4p.s3") + + +@pytest.fixture(scope="module") +def tip5p_mols(): + return sr.load_test_files("tip5p.s3") + + +@pytest.fixture(scope="module") +def opc_mols(): + return sr.load_test_files("opc.s3") + + +def _get_vsite_info(omm_context): + """Return (n_vsites, n_zero_mass, n_bad_constraints) for an OpenMM context.""" + import openmm + + sys = omm_context.getSystem() + natoms = sys.getNumParticles() + + zero_mass = { + i + for i in range(natoms) + if sys.getParticleMass(i).value_in_unit(openmm.unit.dalton) == 0.0 + } + n_vsites = sum(1 for i in zero_mass if sys.isVirtualSite(i)) + + bad_constraints = sum( + 1 + for k in range(sys.getNumConstraints()) + if sys.getConstraintParameters(k)[0] in zero_mass + or sys.getConstraintParameters(k)[1] in zero_mass + ) + + return n_vsites, len(zero_mass), bad_constraints + + +def _potential_energy(omm_context): + import openmm + + return ( + omm_context.getState(getEnergy=True) + .getPotentialEnergy() + .value_in_unit(openmm.unit.kilojoules_per_mole) + ) + + +@_skip_no_openmm +def test_tip3p_no_virtual_sites(tip3p_mols, openmm_platform): + """TIP3P has no EP atoms — no virtual sites should be registered.""" + omm_map = { + "constraint": "h-bonds", + "cutoff_type": "none", + "cutoff": "none", + "platform": openmm_platform or "CPU", + } + + omm = sr.convert.to(tip3p_mols, "openmm", map=omm_map) + n_vsites, n_zero_mass, bad_constraints = _get_vsite_info(omm) + + assert ( + n_zero_mass == 0 + ), f"TIP3P should have no zero-mass particles, got {n_zero_mass}" + assert n_vsites == 0, f"TIP3P should have no virtual sites, got {n_vsites}" + assert bad_constraints == 0 + + e = _potential_energy(omm) + assert not math.isnan(e), "Potential energy is NaN" + + +@_skip_no_openmm +def test_tip4p_virtual_sites(tip4p_mols, openmm_platform): + """TIP4P: one ThreeParticleAverageSite per water, no bad constraints, finite energy.""" + import openmm + + omm_map = { + "constraint": "h-bonds", + "cutoff_type": "none", + "cutoff": "none", + "platform": openmm_platform or "CPU", + } + + omm = sr.convert.to(tip4p_mols, "openmm", map=omm_map) + n_vsites, n_zero_mass, bad_constraints = _get_vsite_info(omm) + + n_waters = tip4p_mols.num_molecules() + + assert ( + n_zero_mass == n_waters + ), f"Expected {n_waters} zero-mass EP atoms, got {n_zero_mass}" + assert n_vsites == n_waters, f"Expected {n_waters} virtual sites, got {n_vsites}" + assert bad_constraints == 0, f"Constraints on virtual sites: {bad_constraints}" + + # All virtual sites should be ThreeParticleAverageSite + sys = omm.getSystem() + natoms = sys.getNumParticles() + for i in range(natoms): + if sys.isVirtualSite(i): + vs = sys.getVirtualSite(i) + assert isinstance( + vs, openmm.ThreeParticleAverageSite + ), f"Particle {i}: expected ThreeParticleAverageSite, got {type(vs).__name__}" + + e = _potential_energy(omm) + assert not math.isnan(e), "Potential energy is NaN" + + +@_skip_no_openmm +def test_opc_virtual_sites(opc_mols, openmm_platform): + """OPC: one ThreeParticleAverageSite per water, no bad constraints, finite energy.""" + import openmm + + omm_map = { + "constraint": "h-bonds", + "cutoff_type": "none", + "cutoff": "none", + "platform": openmm_platform or "CPU", + } + + omm = sr.convert.to(opc_mols, "openmm", map=omm_map) + n_vsites, n_zero_mass, bad_constraints = _get_vsite_info(omm) + + n_waters = opc_mols.num_molecules() + + assert ( + n_zero_mass == n_waters + ), f"Expected {n_waters} zero-mass EP atoms, got {n_zero_mass}" + assert n_vsites == n_waters, f"Expected {n_waters} virtual sites, got {n_vsites}" + assert bad_constraints == 0, f"Constraints on virtual sites: {bad_constraints}" + + sys = omm.getSystem() + natoms = sys.getNumParticles() + for i in range(natoms): + if sys.isVirtualSite(i): + vs = sys.getVirtualSite(i) + assert isinstance( + vs, openmm.ThreeParticleAverageSite + ), f"Particle {i}: expected ThreeParticleAverageSite, got {type(vs).__name__}" + + e = _potential_energy(omm) + assert not math.isnan(e), "Potential energy is NaN" + + +@_skip_no_openmm +def test_tip5p_virtual_sites(tip5p_mols, openmm_platform): + """TIP5P: two OutOfPlaneSite virtual sites per water, no bad constraints, finite energy.""" + import openmm + + omm_map = { + "constraint": "h-bonds", + "cutoff_type": "none", + "cutoff": "none", + "platform": openmm_platform or "CPU", + } + + omm = sr.convert.to(tip5p_mols, "openmm", map=omm_map) + n_vsites, n_zero_mass, bad_constraints = _get_vsite_info(omm) + + n_waters = tip5p_mols.num_molecules() + + assert ( + n_zero_mass == 2 * n_waters + ), f"Expected {2 * n_waters} zero-mass EP atoms, got {n_zero_mass}" + assert ( + n_vsites == 2 * n_waters + ), f"Expected {2 * n_waters} virtual sites, got {n_vsites}" + assert bad_constraints == 0, f"Constraints on virtual sites: {bad_constraints}" + + sys = omm.getSystem() + natoms = sys.getNumParticles() + for i in range(natoms): + if sys.isVirtualSite(i): + vs = sys.getVirtualSite(i) + assert isinstance( + vs, openmm.OutOfPlaneSite + ), f"Particle {i}: expected OutOfPlaneSite, got {type(vs).__name__}" + + e = _potential_energy(omm) + assert not math.isnan(e), "Potential energy is NaN" diff --git a/tests/mol/test_complex_indexing.py b/tests/mol/test_complex_indexing.py index e87134b0a..159e912b8 100644 --- a/tests/mol/test_complex_indexing.py +++ b/tests/mol/test_complex_indexing.py @@ -442,7 +442,7 @@ def test_search_terms(ala_mols): check_mass = mols[0][0].mass().value() atoms = mols[0][ - f"atom mass >= {check_mass-0.001} and atom mass <= {check_mass+0.001}" + f"atom mass >= {check_mass - 0.001} and atom mass <= {check_mass + 0.001}" ] assert len(atoms) > 0 @@ -460,7 +460,7 @@ def test_search_terms(ala_mols): check_charge = mols[0][1].charge().value() atoms = mols[0][ - f"atom charge >= {check_charge-0.001} and atom charge <= {check_charge+0.001}" + f"atom charge >= {check_charge - 0.001} and atom charge <= {check_charge + 0.001}" ] assert len(atoms) > 0 @@ -527,8 +527,6 @@ def test_in_searches(ala_mols): def test_with_searches(ala_mols): mols = ala_mols - import sire as sr - for mol in mols["molecules with count(atoms) >= 3"]: assert mol.num_atoms() >= 3 @@ -593,3 +591,29 @@ def test_count_searches(ala_mols): mol = mols["molecules with count(residues) == 3"] assert mol.num_residues() == 3 + + +def test_oob_molidx(ala_mols): + """ + Regression test for issue #286: out-of-bounds molidx should raise KeyError, + not silently return the last molecule. + """ + mols = ala_mols + + n = mols.num_molecules() + + # Valid boundary indices should work + assert mols["molidx 0"] == mols[0] + assert mols[f"molidx {n - 1}"] == mols[-1] + assert mols["molidx -1"] == mols[-1] + + # Out-of-bounds positive index must raise KeyError + with pytest.raises(KeyError): + mols[f"molidx {n}"] + + with pytest.raises(KeyError): + mols["molidx 10000"] + + # Out-of-bounds negative index must raise KeyError + with pytest.raises(KeyError): + mols[f"molidx -{n + 1}"] diff --git a/tests/morph/test_merge.py b/tests/morph/test_merge.py index 25f9550b6..cc8e130b0 100644 --- a/tests/morph/test_merge.py +++ b/tests/morph/test_merge.py @@ -53,7 +53,11 @@ def test_merge(ose_mols, zan_mols, openmm_platform): zan = zan_mols[0] merged = sr.morph.merge( - ose, zan, match=KartografAtomMapper(atom_map_hydrogens=True) + ose, + zan, + match=KartografAtomMapper( + atom_map_hydrogens=True, map_hydrogens_on_hydrogens_only=False + ), ) ose_nrg = ose.dynamics(platform=openmm_platform).current_potential_energy() @@ -74,7 +78,11 @@ def test_merge(ose_mols, zan_mols, openmm_platform): assert extracted_zan_nrg.value() == pytest.approx(zan_nrg.value()) merged = sr.morph.merge( - zan, ose, match=KartografAtomMapper(atom_map_hydrogens=True) + zan, + ose, + match=KartografAtomMapper( + atom_map_hydrogens=True, map_hydrogens_on_hydrogens_only=False + ), ) extracted_zan = merged.perturbation().extract_reference() @@ -152,7 +160,11 @@ def test_merge_neopentane_methane(neopentane_methane, openmm_platform): nrg_met = methane.dynamics(platform=openmm_platform).current_potential_energy() merged = sr.morph.merge( - neopentane, methane, match=KartografAtomMapper(atom_map_hydrogens=True) + neopentane, + methane, + match=KartografAtomMapper( + atom_map_hydrogens=True, map_hydrogens_on_hydrogens_only=False + ), ) nrg_merged_0 = merged.dynamics( diff --git a/tests/qm/test_qm.py b/tests/qm/test_qm.py index 897d81f75..c21866b22 100644 --- a/tests/qm/test_qm.py +++ b/tests/qm/test_qm.py @@ -451,3 +451,38 @@ def callback(numbers_qm, charges_mm, xyz_qm, xyz_mm, cell=None, idx_mm=None): # Make sure the energy is correct. assert nrg == 42 + + +def test_qmff_was_force_changed(ala_mols): + """ + Verify that wasForceChanged("qmff") correctly tracks whether the qmff + lambda value changed between set_lambda calls. + """ + + def callback(numbers_qm, charges_mm, xyz_qm, xyz_mm, cell=None, idx_mm=None): + return (0.0, xyz_qm, xyz_mm) + + mols = ala_mols.clone() + qm_mols, engine = sr.qm.create_engine(mols, mols[0], callback, callback=None) + + d = qm_mols.dynamics( + timestep="1fs", constraint="none", qm_engine=engine, platform="cpu" + ) + + ctx = d._d._omm_mols + lever = ctx.get_lambda_lever() + + # The context is initialised at lambda=1.0 (QM/MM default). Move to a + # different lambda so the qmff lambda changes. + ctx.set_lambda(0.5) + assert lever.was_force_changed("qmff"), "qmff should be changed when lambda changes" + + # Same lambda again: qmff lambda is unchanged, so not changed. + ctx.set_lambda(0.5) + assert not lever.was_force_changed( + "qmff" + ), "qmff should not be changed when lambda is repeated" + + # Different lambda: qmff lambda changes again. + ctx.set_lambda(1.0) + assert lever.was_force_changed("qmff"), "qmff should be changed when lambda changes" diff --git a/tests/restraints/test_morse_potential_restraints.py b/tests/restraints/test_morse_potential_restraints.py index f994614a5..3040155a4 100644 --- a/tests/restraints/test_morse_potential_restraints.py +++ b/tests/restraints/test_morse_potential_restraints.py @@ -5,7 +5,7 @@ def test_morse_potential_restraints_setup(cyclopentane_cyclohexane): """Tests that morse_potential restraints can be set up correctly with custom parameters.""" mols = cyclopentane_cyclohexane.clone() - restraints = sr.restraints.morse_potential( + restraints, mols = sr.restraints.morse_potential( mols, atoms0=mols["molecule property is_perturbable and atomidx 0"], atoms1=mols["molecule property is_perturbable and atomidx 4"], @@ -25,7 +25,7 @@ def test_morse_potential_restraint_annihiliation_auto_param(cyclopentane_cyclohe """Tests that morse_potential restraints can be set up correctly with automatic parametrisation when a bond is to be annihilated.""" mols = cyclopentane_cyclohexane.clone() - restraints = sr.restraints.morse_potential( + restraints, mols = sr.restraints.morse_potential( mols, de="25 kcal mol-1", auto_parametrise=True, @@ -33,7 +33,7 @@ def test_morse_potential_restraint_annihiliation_auto_param(cyclopentane_cyclohe assert restraints.num_restraints() == 1 assert restraints[0].atom0() == 0 assert restraints[0].atom1() == 4 - assert restraints[0].k().value() == pytest.approx(228.89, rel=0.1) + assert restraints[0].k().value() == pytest.approx(457.78, rel=0.1) assert restraints[0].de().value() == 25.0 @@ -41,7 +41,7 @@ def test_morse_potential_restraint_creation_auto_param(propane_cyclopropane): """Tests that morse_potential restraints can be set up correctly with automatic parametrisation when a bond is to be created.""" mols = propane_cyclopropane.clone() - restraints = sr.restraints.morse_potential( + restraints, mols = sr.restraints.morse_potential( mols, de="25 kcal mol-1", auto_parametrise=True, @@ -54,7 +54,7 @@ def test_morse_potential_restraint_creation_auto_param(propane_cyclopropane): def test_morse_potential_restraint_auto_param_override(cyclopentane_cyclohexane): """Tests that morse_potential restraints can be set up correctly with automatic parametrisation and some parameters can be overwritten.""" mols = cyclopentane_cyclohexane.clone() - restraints = sr.restraints.morse_potential( + restraints, mols = sr.restraints.morse_potential( mols, de="25 kcal mol-1", k="100 kcal mol-1 A-2", @@ -70,18 +70,75 @@ def test_morse_potential_restraint_auto_param_override(cyclopentane_cyclohexane) def test_multiple_morse_potential_restraints(cyclopentane_cyclohexane): """Tests that multiple morse_potential restraints can be set up correctly.""" mols = cyclopentane_cyclohexane.clone() - restraints = sr.restraints.morse_potential( + restraints, mols = sr.restraints.morse_potential( mols, de="25 kcal mol-1", auto_parametrise=True, + direct_morse_replacement=False, ) - restraint1 = sr.restraints.morse_potential( + restraint1, mols = sr.restraints.morse_potential( mols, atoms0=mols["molecule property is_perturbable and atomidx 0"], atoms1=mols["molecule property is_perturbable and atomidx 1"], k="100 kcal mol-1 A-2", r0="1.5 A", de="50 kcal mol-1", + direct_morse_replacement=False, ) restraints.add(restraint1) assert restraints.num_restraints() == 2 + + +def test_morse_potential_direct_morse_replacement(cyclopentane_cyclohexane): + """Tests that morse_potential restraints by default will remove the annihilated harmonic bond.""" + mols = cyclopentane_cyclohexane.clone() + bonds0_org_mol = mols[0].property("bond0") + bonds1_org_mol = mols[0].property("bond1") + num_bonds0_org_mol = len(bonds0_org_mol.potentials()) + num_bonds1_org_mol = len(bonds1_org_mol.potentials()) + + restraints, mols = sr.restraints.morse_potential( + mols, + de="25 kcal mol-1", + auto_parametrise=True, + ) + + bonds0_mod_mol = mols[0].property("bond0") + bonds1_mod_mol = mols[0].property("bond1") + num_bonds0_mod_mol = len(bonds0_mod_mol.potentials()) + num_bonds1_mod_mol = len(bonds1_mod_mol.potentials()) + + # New bonds0 should be smaller by 1 than the original bonds0 if the function removed the bond + assert num_bonds0_mod_mol == num_bonds0_org_mol - 1 + + # New bonds1 should be same as the original bonds1, as the bonds1 will already be smaller + # by 1 than the original bonds0 which is introduced during the merge. + assert num_bonds1_mod_mol == num_bonds1_org_mol + assert num_bonds0_org_mol == num_bonds1_org_mol + 1 + + +def test_morse_potential_direct_morse_replacement_retain_harmonic( + cyclopentane_cyclohexane, +): + """Tests that morse_potential restraints can retain the annihilated harmonic bond, if specified.""" + mols = cyclopentane_cyclohexane.clone() + bonds0_org_mol = mols[0].property("bond0") + bonds1_org_mol = mols[0].property("bond1") + num_bonds0_org_mol = len(bonds0_org_mol.potentials()) + num_bonds1_org_mol = len(bonds1_org_mol.potentials()) + + restraints, mols = sr.restraints.morse_potential( + mols, + de="25 kcal mol-1", + auto_parametrise=True, + retain_harmonic_bond=True, + ) + bonds0_mod_mol = mols[0].property("bond0") + bonds1_mod_mol = mols[0].property("bond1") + num_bonds0_mod_mol = len(bonds0_mod_mol.potentials()) + num_bonds1_mod_mol = len(bonds1_mod_mol.potentials()) + + # If the harmonic bond is retained, the number of bonds in bonds0 and bonds1 + # should be the same as the original molecule + assert num_bonds0_mod_mol == num_bonds0_org_mol + assert num_bonds1_mod_mol == num_bonds1_org_mol diff --git a/version.txt b/version.txt index f2cedddf7..4df38dcc2 100644 --- a/version.txt +++ b/version.txt @@ -1 +1 @@ -2025.4.0 +2026.1.0 diff --git a/wrapper/CAS/LambdaSchedule.pypp.cpp b/wrapper/CAS/LambdaSchedule.pypp.cpp index 34f75ae16..fc64963de 100644 --- a/wrapper/CAS/LambdaSchedule.pypp.cpp +++ b/wrapper/CAS/LambdaSchedule.pypp.cpp @@ -2,9 +2,9 @@ // (C) Christopher Woods, GPL >= 3 License -#include "boost/python.hpp" -#include "Helpers/clone_const_reference.hpp" #include "LambdaSchedule.pypp.hpp" +#include "Helpers/clone_const_reference.hpp" +#include "boost/python.hpp" namespace bp = boost::python; @@ -22,7 +22,7 @@ namespace bp = boost::python; #include "lambdaschedule.h" -SireCAS::LambdaSchedule __copy__(const SireCAS::LambdaSchedule &other){ return SireCAS::LambdaSchedule(other); } +SireCAS::LambdaSchedule __copy__(const SireCAS::LambdaSchedule &other) { return SireCAS::LambdaSchedule(other); } #include "Qt/qdatastream.hpp" @@ -30,869 +30,619 @@ SireCAS::LambdaSchedule __copy__(const SireCAS::LambdaSchedule &other){ return S #include "Helpers/release_gil_policy.hpp" -void register_LambdaSchedule_class(){ +void register_LambdaSchedule_class() +{ { //::SireCAS::LambdaSchedule - typedef bp::class_< SireCAS::LambdaSchedule, bp::bases< SireBase::Property > > LambdaSchedule_exposer_t; - LambdaSchedule_exposer_t LambdaSchedule_exposer = LambdaSchedule_exposer_t( "LambdaSchedule", "This is a schedule that specifies how parameters are changed according\nto a global lambda value. The change can be broken up by sub lever,\nand by stage.\n", bp::init< >("") ); - bp::scope LambdaSchedule_scope( LambdaSchedule_exposer ); - LambdaSchedule_exposer.def( bp::init< SireCAS::LambdaSchedule const & >(( bp::arg("other") ), "") ); + typedef bp::class_> LambdaSchedule_exposer_t; + LambdaSchedule_exposer_t LambdaSchedule_exposer = LambdaSchedule_exposer_t("LambdaSchedule", "This is a schedule that specifies how parameters are changed according\nto a global lambda value. The change can be broken up by sub lever,\nand by stage.\n", bp::init<>("")); + bp::scope LambdaSchedule_scope(LambdaSchedule_exposer); + LambdaSchedule_exposer.def(bp::init((bp::arg("other")), "")); { //::SireCAS::LambdaSchedule::addAnnihilateStage - - typedef void ( ::SireCAS::LambdaSchedule::*addAnnihilateStage_function_type)( bool ) ; - addAnnihilateStage_function_type addAnnihilateStage_function_value( &::SireCAS::LambdaSchedule::addAnnihilateStage ); - - LambdaSchedule_exposer.def( - "addAnnihilateStage" - , addAnnihilateStage_function_value - , ( bp::arg("perturbed_is_annihilated")=(bool)(true) ) - , "Add a stage to the schedule that will annihilate the perturbed\n state if `perturbed_is_annihilated` is true, otherwise the\n reference state is annihilated. The stage will be called annihilate.\n" ); - + + typedef void (::SireCAS::LambdaSchedule::*addAnnihilateStage_function_type)(bool); + addAnnihilateStage_function_type addAnnihilateStage_function_value(&::SireCAS::LambdaSchedule::addAnnihilateStage); + + LambdaSchedule_exposer.def( + "addAnnihilateStage", addAnnihilateStage_function_value, (bp::arg("perturbed_is_annihilated") = (bool)(true)), "Add a stage to the schedule that will annihilate the perturbed\n state if `perturbed_is_annihilated` is true, otherwise the\n reference state is annihilated. The stage will be called annihilate.\n"); } { //::SireCAS::LambdaSchedule::addAnnihilateStage - - typedef void ( ::SireCAS::LambdaSchedule::*addAnnihilateStage_function_type)( ::QString const &,bool ) ; - addAnnihilateStage_function_type addAnnihilateStage_function_value( &::SireCAS::LambdaSchedule::addAnnihilateStage ); - - LambdaSchedule_exposer.def( - "addAnnihilateStage" - , addAnnihilateStage_function_value - , ( bp::arg("name"), bp::arg("perturbed_is_annihilated")=(bool)(true) ) - , "Add a named stage to the schedule that will annihilate the perturbed\n state if `perturbed_is_annihilated` is true, otherwise the\n reference state is annihilated.\n" ); - + + typedef void (::SireCAS::LambdaSchedule::*addAnnihilateStage_function_type)(::QString const &, bool, double); + addAnnihilateStage_function_type addAnnihilateStage_function_value(&::SireCAS::LambdaSchedule::addAnnihilateStage); + + LambdaSchedule_exposer.def( + "addAnnihilateStage", addAnnihilateStage_function_value, (bp::arg("name"), bp::arg("perturbed_is_annihilated") = (bool)(true), bp::arg("weight") = 1.0), "Add a named stage to the schedule that will annihilate the perturbed\n state if `perturbed_is_annihilated` is true, otherwise the\n reference state is annihilated. The stage occupies a fraction of\n lambda space proportional to its weight relative to other stages.\n"); } { //::SireCAS::LambdaSchedule::addChargeScaleStages - - typedef void ( ::SireCAS::LambdaSchedule::*addChargeScaleStages_function_type)( double ) ; - addChargeScaleStages_function_type addChargeScaleStages_function_value( &::SireCAS::LambdaSchedule::addChargeScaleStages ); - - LambdaSchedule_exposer.def( - "addChargeScaleStages" - , addChargeScaleStages_function_value - , ( bp::arg("scale")=0.20000000000000001 ) - , "Sandwich the current set of stages with a charge-descaling and\n a charge-scaling stage. This prepends a charge-descaling stage\n that scales the charge parameter down from `initial` to\n :gamma:.initial (where :gamma:=`scale`). The charge parameter in all of\n the exising stages in this schedule are then multiplied\n by :gamma:. A final charge-rescaling stage is then appended that\n scales the charge parameter from :gamma:.final to final.\n" ); - + + typedef void (::SireCAS::LambdaSchedule::*addChargeScaleStages_function_type)(double); + addChargeScaleStages_function_type addChargeScaleStages_function_value(&::SireCAS::LambdaSchedule::addChargeScaleStages); + + LambdaSchedule_exposer.def( + "addChargeScaleStages", addChargeScaleStages_function_value, (bp::arg("scale") = 0.20000000000000001), "Sandwich the current set of stages with a charge-descaling and\n a charge-scaling stage. This prepends a charge-descaling stage\n that scales the charge parameter down from `initial` to\n :gamma:.initial (where :gamma:=`scale`). The charge parameter in all of\n the exising stages in this schedule are then multiplied\n by :gamma:. A final charge-rescaling stage is then appended that\n scales the charge parameter from :gamma:.final to final.\n"); } { //::SireCAS::LambdaSchedule::addChargeScaleStages - - typedef void ( ::SireCAS::LambdaSchedule::*addChargeScaleStages_function_type)( ::QString const &,::QString const &,double ) ; - addChargeScaleStages_function_type addChargeScaleStages_function_value( &::SireCAS::LambdaSchedule::addChargeScaleStages ); - - LambdaSchedule_exposer.def( - "addChargeScaleStages" - , addChargeScaleStages_function_value - , ( bp::arg("decharge_name"), bp::arg("recharge_name"), bp::arg("scale")=0.20000000000000001 ) - , "Sandwich the current set of stages with a charge-descaling and\n a charge-scaling stage. This prepends a charge-descaling stage\n that scales the charge parameter down from `initial` to\n :gamma:.initial (where :gamma:=`scale`). The charge parameter in all of\n the exising stages in this schedule are then multiplied\n by :gamma:. A final charge-rescaling stage is then appended that\n scales the charge parameter from :gamma:.final to final.\n" ); - + + typedef void (::SireCAS::LambdaSchedule::*addChargeScaleStages_function_type)(::QString const &, ::QString const &, double); + addChargeScaleStages_function_type addChargeScaleStages_function_value(&::SireCAS::LambdaSchedule::addChargeScaleStages); + + LambdaSchedule_exposer.def( + "addChargeScaleStages", addChargeScaleStages_function_value, (bp::arg("decharge_name"), bp::arg("recharge_name"), bp::arg("scale") = 0.20000000000000001), "Sandwich the current set of stages with a charge-descaling and\n a charge-scaling stage. This prepends a charge-descaling stage\n that scales the charge parameter down from `initial` to\n :gamma:.initial (where :gamma:=`scale`). The charge parameter in all of\n the exising stages in this schedule are then multiplied\n by :gamma:. A final charge-rescaling stage is then appended that\n scales the charge parameter from :gamma:.final to final.\n"); } { //::SireCAS::LambdaSchedule::addDecoupleStage - - typedef void ( ::SireCAS::LambdaSchedule::*addDecoupleStage_function_type)( bool ) ; - addDecoupleStage_function_type addDecoupleStage_function_value( &::SireCAS::LambdaSchedule::addDecoupleStage ); - - LambdaSchedule_exposer.def( - "addDecoupleStage" - , addDecoupleStage_function_value - , ( bp::arg("perturbed_is_decoupled")=(bool)(true) ) - , "Add a stage to the schedule that will decouple the perturbed\n state if `perturbed_is_decoupled` is true, otherwise the\n reference state is decoupled. The stage will be called decouple.\n" ); - + + typedef void (::SireCAS::LambdaSchedule::*addDecoupleStage_function_type)(bool); + addDecoupleStage_function_type addDecoupleStage_function_value(&::SireCAS::LambdaSchedule::addDecoupleStage); + + LambdaSchedule_exposer.def( + "addDecoupleStage", addDecoupleStage_function_value, (bp::arg("perturbed_is_decoupled") = (bool)(true)), "Add a stage to the schedule that will decouple the perturbed\n state if `perturbed_is_decoupled` is true, otherwise the\n reference state is decoupled. The stage will be called decouple.\n"); } { //::SireCAS::LambdaSchedule::addDecoupleStage - - typedef void ( ::SireCAS::LambdaSchedule::*addDecoupleStage_function_type)( ::QString const &,bool ) ; - addDecoupleStage_function_type addDecoupleStage_function_value( &::SireCAS::LambdaSchedule::addDecoupleStage ); - - LambdaSchedule_exposer.def( - "addDecoupleStage" - , addDecoupleStage_function_value - , ( bp::arg("name"), bp::arg("perturbed_is_decoupled")=(bool)(true) ) - , "Add a named stage to the schedule that will decouple the perturbed\n state if `perturbed_is_decoupled` is true, otherwise the\n reference state is decoupled.\n" ); - + + typedef void (::SireCAS::LambdaSchedule::*addDecoupleStage_function_type)(::QString const &, bool, double); + addDecoupleStage_function_type addDecoupleStage_function_value(&::SireCAS::LambdaSchedule::addDecoupleStage); + + LambdaSchedule_exposer.def( + "addDecoupleStage", addDecoupleStage_function_value, (bp::arg("name"), bp::arg("perturbed_is_decoupled") = (bool)(true), bp::arg("weight") = 1.0), "Add a named stage to the schedule that will decouple the perturbed\n state if `perturbed_is_decoupled` is true, otherwise the\n reference state is decoupled. The stage occupies a fraction of\n lambda space proportional to its weight relative to other stages.\n"); } { //::SireCAS::LambdaSchedule::addForce - - typedef void ( ::SireCAS::LambdaSchedule::*addForce_function_type)( ::QString const & ) ; - addForce_function_type addForce_function_value( &::SireCAS::LambdaSchedule::addForce ); - - LambdaSchedule_exposer.def( - "addForce" - , addForce_function_value - , ( bp::arg("force") ) - , bp::release_gil_policy() - , "Add a force to a schedule. This is only useful if you want to\n plot how the equations would affect the lever. Forces will be\n automatically added by any perturbation run that needs them,\n so you dont need to add them manually yourself.\n" ); - + + typedef void (::SireCAS::LambdaSchedule::*addForce_function_type)(::QString const &); + addForce_function_type addForce_function_value(&::SireCAS::LambdaSchedule::addForce); + + LambdaSchedule_exposer.def( + "addForce", addForce_function_value, (bp::arg("force")), bp::release_gil_policy(), "Add a force to a schedule. This is only useful if you want to\n plot how the equations would affect the lever. Forces will be\n automatically added by any perturbation run that needs them,\n so you dont need to add them manually yourself.\n"); } { //::SireCAS::LambdaSchedule::addForces - - typedef void ( ::SireCAS::LambdaSchedule::*addForces_function_type)( ::QStringList const & ) ; - addForces_function_type addForces_function_value( &::SireCAS::LambdaSchedule::addForces ); - - LambdaSchedule_exposer.def( - "addForces" - , addForces_function_value - , ( bp::arg("forces") ) - , bp::release_gil_policy() - , "Add some forces to a schedule. This is only useful if you want to\n plot how the equations would affect the lever. Forces will be\n automatically added by any perturbation run that needs them,\n so you dont need to add them manually yourself.\n" ); - + + typedef void (::SireCAS::LambdaSchedule::*addForces_function_type)(::QStringList const &); + addForces_function_type addForces_function_value(&::SireCAS::LambdaSchedule::addForces); + + LambdaSchedule_exposer.def( + "addForces", addForces_function_value, (bp::arg("forces")), bp::release_gil_policy(), "Add some forces to a schedule. This is only useful if you want to\n plot how the equations would affect the lever. Forces will be\n automatically added by any perturbation run that needs them,\n so you dont need to add them manually yourself.\n"); } { //::SireCAS::LambdaSchedule::addLever - - typedef void ( ::SireCAS::LambdaSchedule::*addLever_function_type)( ::QString const & ) ; - addLever_function_type addLever_function_value( &::SireCAS::LambdaSchedule::addLever ); - - LambdaSchedule_exposer.def( - "addLever" - , addLever_function_value - , ( bp::arg("lever") ) - , bp::release_gil_policy() - , "Add a lever to the schedule. This is only useful if you want to\n plot how the equations would affect the lever. Levers will be\n automatically added by any perturbation run that needs them,\n so you dont need to add them manually yourself.\n" ); - + + typedef void (::SireCAS::LambdaSchedule::*addLever_function_type)(::QString const &); + addLever_function_type addLever_function_value(&::SireCAS::LambdaSchedule::addLever); + + LambdaSchedule_exposer.def( + "addLever", addLever_function_value, (bp::arg("lever")), bp::release_gil_policy(), "Add a lever to the schedule. This is only useful if you want to\n plot how the equations would affect the lever. Levers will be\n automatically added by any perturbation run that needs them,\n so you dont need to add them manually yourself.\n"); } { //::SireCAS::LambdaSchedule::addLevers - - typedef void ( ::SireCAS::LambdaSchedule::*addLevers_function_type)( ::QStringList const & ) ; - addLevers_function_type addLevers_function_value( &::SireCAS::LambdaSchedule::addLevers ); - - LambdaSchedule_exposer.def( - "addLevers" - , addLevers_function_value - , ( bp::arg("levers") ) - , bp::release_gil_policy() - , "Add some levers to the schedule. This is only useful if you want to\n plot how the equations would affect the lever. Levers will be\n automatically added by any perturbation run that needs them,\n so you dont need to add them manually yourself.\n" ); - + + typedef void (::SireCAS::LambdaSchedule::*addLevers_function_type)(::QStringList const &); + addLevers_function_type addLevers_function_value(&::SireCAS::LambdaSchedule::addLevers); + + LambdaSchedule_exposer.def( + "addLevers", addLevers_function_value, (bp::arg("levers")), bp::release_gil_policy(), "Add some levers to the schedule. This is only useful if you want to\n plot how the equations would affect the lever. Levers will be\n automatically added by any perturbation run that needs them,\n so you dont need to add them manually yourself.\n"); } { //::SireCAS::LambdaSchedule::addMorphStage - - typedef void ( ::SireCAS::LambdaSchedule::*addMorphStage_function_type)( ) ; - addMorphStage_function_type addMorphStage_function_value( &::SireCAS::LambdaSchedule::addMorphStage ); - - LambdaSchedule_exposer.def( - "addMorphStage" - , addMorphStage_function_value - , bp::release_gil_policy() - , "Append a morph stage onto this schedule. The morph stage is a\n standard stage that scales each forcefield parameter by\n (1-:lambda:).initial + :lambda:.final\n" ); - + + typedef void (::SireCAS::LambdaSchedule::*addMorphStage_function_type)(); + addMorphStage_function_type addMorphStage_function_value(&::SireCAS::LambdaSchedule::addMorphStage); + + LambdaSchedule_exposer.def( + "addMorphStage", addMorphStage_function_value, bp::release_gil_policy(), "Append a morph stage onto this schedule. The morph stage is a\n standard stage that scales each forcefield parameter by\n (1-:lambda:).initial + :lambda:.final\n"); } { //::SireCAS::LambdaSchedule::addMorphStage - - typedef void ( ::SireCAS::LambdaSchedule::*addMorphStage_function_type)( ::QString const & ) ; - addMorphStage_function_type addMorphStage_function_value( &::SireCAS::LambdaSchedule::addMorphStage ); - - LambdaSchedule_exposer.def( - "addMorphStage" - , addMorphStage_function_value - , ( bp::arg("name") ) - , bp::release_gil_policy() - , "Append a morph stage onto this schedule. The morph stage is a\n standard stage that scales each forcefield parameter by\n (1-:lambda:).initial + :lambda:.final\n" ); - + + typedef void (::SireCAS::LambdaSchedule::*addMorphStage_function_type)(::QString const &, double); + addMorphStage_function_type addMorphStage_function_value(&::SireCAS::LambdaSchedule::addMorphStage); + + LambdaSchedule_exposer.def( + "addMorphStage", addMorphStage_function_value, (bp::arg("name"), bp::arg("weight") = 1.0), bp::release_gil_policy(), "Append a morph stage onto this schedule. The morph stage is a\n standard stage that scales each forcefield parameter by\n (1-:lambda:).initial + :lambda:.final. The stage occupies a\n fraction of lambda space proportional to its weight relative\n to other stages.\n"); } { //::SireCAS::LambdaSchedule::addStage - - typedef void ( ::SireCAS::LambdaSchedule::*addStage_function_type)( ::QString const &,::SireCAS::Expression const & ) ; - addStage_function_type addStage_function_value( &::SireCAS::LambdaSchedule::addStage ); - - LambdaSchedule_exposer.def( - "addStage" - , addStage_function_value - , ( bp::arg("stage"), bp::arg("equation") ) - , bp::release_gil_policy() - , "Append a stage called name which uses the passed equation\n to the end of this schedule. The equation will be the default\n equation that scales all parameters (levers) that dont have\n a custom lever for this stage.\n" ); - + + typedef void (::SireCAS::LambdaSchedule::*addStage_function_type)(::QString const &, ::SireCAS::Expression const &, double); + addStage_function_type addStage_function_value(&::SireCAS::LambdaSchedule::addStage); + + LambdaSchedule_exposer.def( + "addStage", addStage_function_value, (bp::arg("stage"), bp::arg("equation"), bp::arg("weight") = 1.0), bp::release_gil_policy(), "Append a stage called name which uses the passed equation\n to the end of this schedule. The equation will be the default\n equation that scales all parameters (levers) that dont have\n a custom lever for this stage. The optional weight controls the\n fraction of lambda space the stage occupies relative to other stages.\n"); } { //::SireCAS::LambdaSchedule::appendStage - - typedef void ( ::SireCAS::LambdaSchedule::*appendStage_function_type)( ::QString const &,::SireCAS::Expression const & ) ; - appendStage_function_type appendStage_function_value( &::SireCAS::LambdaSchedule::appendStage ); - - LambdaSchedule_exposer.def( - "appendStage" - , appendStage_function_value - , ( bp::arg("stage"), bp::arg("equation") ) - , bp::release_gil_policy() - , "Append a stage called name which uses the passed equation\n to the end of this schedule. The equation will be the default\n equation that scales all parameters (levers) that dont have\n a custom lever for this stage.\n" ); - + + typedef void (::SireCAS::LambdaSchedule::*appendStage_function_type)(::QString const &, ::SireCAS::Expression const &, double); + appendStage_function_type appendStage_function_value(&::SireCAS::LambdaSchedule::appendStage); + + LambdaSchedule_exposer.def( + "appendStage", appendStage_function_value, (bp::arg("stage"), bp::arg("equation"), bp::arg("weight") = 1.0), bp::release_gil_policy(), "Append a stage called name which uses the passed equation\n to the end of this schedule. The equation will be the default\n equation that scales all parameters (levers) that dont have\n a custom lever for this stage. The optional weight controls the\n fraction of lambda space the stage occupies relative to other stages.\n"); } { //::SireCAS::LambdaSchedule::charge_scaled_annihilate - - typedef ::SireCAS::LambdaSchedule ( *charge_scaled_annihilate_function_type )( double,bool ); - charge_scaled_annihilate_function_type charge_scaled_annihilate_function_value( &::SireCAS::LambdaSchedule::charge_scaled_annihilate ); - - LambdaSchedule_exposer.def( - "charge_scaled_annihilate" - , charge_scaled_annihilate_function_value - , ( bp::arg("scale")=0.20000000000000001, bp::arg("perturbed_is_annihilated")=(bool)(true) ) - , "Return a schedule that can be used for a standard double-annihilation\n free energy perturbation. If `perturbed_is_annihilated` is true, then\n the perturbed state is annihilated, otherwise the reference state is\n annihilated. In this case also add states to decharge and recharge\n the molecule either side of the annihilation stage, where the charges\n are scaled to scale times their original value.\n" ); - + + typedef ::SireCAS::LambdaSchedule (*charge_scaled_annihilate_function_type)(double, bool); + charge_scaled_annihilate_function_type charge_scaled_annihilate_function_value(&::SireCAS::LambdaSchedule::charge_scaled_annihilate); + + LambdaSchedule_exposer.def( + "charge_scaled_annihilate", charge_scaled_annihilate_function_value, (bp::arg("scale") = 0.20000000000000001, bp::arg("perturbed_is_annihilated") = (bool)(true)), "Return a schedule that can be used for a standard double-annihilation\n free energy perturbation. If `perturbed_is_annihilated` is true, then\n the perturbed state is annihilated, otherwise the reference state is\n annihilated. In this case also add states to decharge and recharge\n the molecule either side of the annihilation stage, where the charges\n are scaled to scale times their original value.\n"); } { //::SireCAS::LambdaSchedule::charge_scaled_decouple - - typedef ::SireCAS::LambdaSchedule ( *charge_scaled_decouple_function_type )( double,bool ); - charge_scaled_decouple_function_type charge_scaled_decouple_function_value( &::SireCAS::LambdaSchedule::charge_scaled_decouple ); - - LambdaSchedule_exposer.def( - "charge_scaled_decouple" - , charge_scaled_decouple_function_value - , ( bp::arg("scale")=0.20000000000000001, bp::arg("perturbed_is_decoupled")=(bool)(true) ) - , "Return a schedule that can be used for a standard double-decoupling\n free energy perturbation. If `perturbed_is_decoupled` is true, then\n the perturbed state is decoupled, otherwise the reference state is\n decoupled. In this case also add states to decharge and recharge\n the molecule either side of the decoupling stage, where the charges\n are scaled to scale times their original value.\n" ); - + + typedef ::SireCAS::LambdaSchedule (*charge_scaled_decouple_function_type)(double, bool); + charge_scaled_decouple_function_type charge_scaled_decouple_function_value(&::SireCAS::LambdaSchedule::charge_scaled_decouple); + + LambdaSchedule_exposer.def( + "charge_scaled_decouple", charge_scaled_decouple_function_value, (bp::arg("scale") = 0.20000000000000001, bp::arg("perturbed_is_decoupled") = (bool)(true)), "Return a schedule that can be used for a standard double-decoupling\n free energy perturbation. If `perturbed_is_decoupled` is true, then\n the perturbed state is decoupled, otherwise the reference state is\n decoupled. In this case also add states to decharge and recharge\n the molecule either side of the decoupling stage, where the charges\n are scaled to scale times their original value.\n"); } { //::SireCAS::LambdaSchedule::charge_scaled_morph - - typedef ::SireCAS::LambdaSchedule ( *charge_scaled_morph_function_type )( double ); - charge_scaled_morph_function_type charge_scaled_morph_function_value( &::SireCAS::LambdaSchedule::charge_scaled_morph ); - - LambdaSchedule_exposer.def( - "charge_scaled_morph" - , charge_scaled_morph_function_value - , ( bp::arg("scale")=0.20000000000000001 ) - , "Return a LambdaSchedule that represents a central morph\n stage that is sandwiched between a charge descaling,\n and a charge rescaling stage. The first stage scales\n the charge lever down from 1.0 to `scale`. This\n is followed by a standard morph stage using the\n descaled charges. This the finished with a recharging\n stage that restores the charges back to their\n original values.\n" ); - + + typedef ::SireCAS::LambdaSchedule (*charge_scaled_morph_function_type)(double); + charge_scaled_morph_function_type charge_scaled_morph_function_value(&::SireCAS::LambdaSchedule::charge_scaled_morph); + + LambdaSchedule_exposer.def( + "charge_scaled_morph", charge_scaled_morph_function_value, (bp::arg("scale") = 0.20000000000000001), "Return a LambdaSchedule that represents a central morph\n stage that is sandwiched between a charge descaling,\n and a charge rescaling stage. The first stage scales\n the charge lever down from 1.0 to `scale`. This\n is followed by a standard morph stage using the\n descaled charges. This the finished with a recharging\n stage that restores the charges back to their\n original values.\n"); } { //::SireCAS::LambdaSchedule::clamp - - typedef double ( ::SireCAS::LambdaSchedule::*clamp_function_type)( double ) const; - clamp_function_type clamp_function_value( &::SireCAS::LambdaSchedule::clamp ); - - LambdaSchedule_exposer.def( - "clamp" - , clamp_function_value - , ( bp::arg("lambda_value") ) - , bp::release_gil_policy() - , "Clamp and return the passed lambda value so that it is between a valid\n range for this schedule (typically between [0.0-1.0] inclusive).\n" ); - + + typedef double (::SireCAS::LambdaSchedule::*clamp_function_type)(double) const; + clamp_function_type clamp_function_value(&::SireCAS::LambdaSchedule::clamp); + + LambdaSchedule_exposer.def( + "clamp", clamp_function_value, (bp::arg("lambda_value")), bp::release_gil_policy(), "Clamp and return the passed lambda value so that it is between a valid\n range for this schedule (typically between [0.0-1.0] inclusive).\n"); } { //::SireCAS::LambdaSchedule::clear - - typedef void ( ::SireCAS::LambdaSchedule::*clear_function_type)( ) ; - clear_function_type clear_function_value( &::SireCAS::LambdaSchedule::clear ); - - LambdaSchedule_exposer.def( - "clear" - , clear_function_value - , bp::release_gil_policy() - , "Completely clear all stages and levers" ); - + + typedef void (::SireCAS::LambdaSchedule::*clear_function_type)(); + clear_function_type clear_function_value(&::SireCAS::LambdaSchedule::clear); + + LambdaSchedule_exposer.def( + "clear", clear_function_value, bp::release_gil_policy(), "Completely clear all stages and levers"); } { //::SireCAS::LambdaSchedule::final - - typedef ::SireCAS::Symbol ( *final_function_type )( ); - final_function_type final_function_value( &::SireCAS::LambdaSchedule::final ); - - LambdaSchedule_exposer.def( - "final" - , final_function_value - , bp::release_gil_policy() - , "Return the symbol used to represent the final\n (:lambda:=1) value of the forcefield parameter\n" ); - + + typedef ::SireCAS::Symbol (*final_function_type)(); + final_function_type final_function_value(&::SireCAS::LambdaSchedule::final); + + LambdaSchedule_exposer.def( + "final", final_function_value, bp::release_gil_policy(), "Return the symbol used to represent the final\n (:lambda:=1) value of the forcefield parameter\n"); } { //::SireCAS::LambdaSchedule::getConstant - - typedef double ( ::SireCAS::LambdaSchedule::*getConstant_function_type)( ::QString const & ) ; - getConstant_function_type getConstant_function_value( &::SireCAS::LambdaSchedule::getConstant ); - - LambdaSchedule_exposer.def( - "getConstant" - , getConstant_function_value - , ( bp::arg("constant") ) - , bp::release_gil_policy() - , "Return the value of the passed constant that may be\n used in any of the stage equations\n" ); - + + typedef double (::SireCAS::LambdaSchedule::*getConstant_function_type)(::QString const &); + getConstant_function_type getConstant_function_value(&::SireCAS::LambdaSchedule::getConstant); + + LambdaSchedule_exposer.def( + "getConstant", getConstant_function_value, (bp::arg("constant")), bp::release_gil_policy(), "Return the value of the passed constant that may be\n used in any of the stage equations\n"); } { //::SireCAS::LambdaSchedule::getConstant - - typedef double ( ::SireCAS::LambdaSchedule::*getConstant_function_type)( ::SireCAS::Symbol const & ) const; - getConstant_function_type getConstant_function_value( &::SireCAS::LambdaSchedule::getConstant ); - - LambdaSchedule_exposer.def( - "getConstant" - , getConstant_function_value - , ( bp::arg("constant") ) - , bp::release_gil_policy() - , "Return the value of the passed constant that may be\n used in any of the stage equations\n" ); - + + typedef double (::SireCAS::LambdaSchedule::*getConstant_function_type)(::SireCAS::Symbol const &) const; + getConstant_function_type getConstant_function_value(&::SireCAS::LambdaSchedule::getConstant); + + LambdaSchedule_exposer.def( + "getConstant", getConstant_function_value, (bp::arg("constant")), bp::release_gil_policy(), "Return the value of the passed constant that may be\n used in any of the stage equations\n"); } { //::SireCAS::LambdaSchedule::getConstantSymbol - - typedef ::SireCAS::Symbol ( ::SireCAS::LambdaSchedule::*getConstantSymbol_function_type)( ::QString const & ) const; - getConstantSymbol_function_type getConstantSymbol_function_value( &::SireCAS::LambdaSchedule::getConstantSymbol ); - - LambdaSchedule_exposer.def( - "getConstantSymbol" - , getConstantSymbol_function_value - , ( bp::arg("constant") ) - , bp::release_gil_policy() - , "Get the Symbol used to represent the named constant constant" ); - + + typedef ::SireCAS::Symbol (::SireCAS::LambdaSchedule::*getConstantSymbol_function_type)(::QString const &) const; + getConstantSymbol_function_type getConstantSymbol_function_value(&::SireCAS::LambdaSchedule::getConstantSymbol); + + LambdaSchedule_exposer.def( + "getConstantSymbol", getConstantSymbol_function_value, (bp::arg("constant")), bp::release_gil_policy(), "Get the Symbol used to represent the named constant constant"); } { //::SireCAS::LambdaSchedule::getEquation - - typedef ::SireCAS::Expression ( ::SireCAS::LambdaSchedule::*getEquation_function_type)( ::QString const &,::QString const &,::QString const & ) const; - getEquation_function_type getEquation_function_value( &::SireCAS::LambdaSchedule::getEquation ); - - LambdaSchedule_exposer.def( - "getEquation" - , getEquation_function_value - , ( bp::arg("stage")="*", bp::arg("force")="*", bp::arg("lever")="*" ) - , "Return the equation used to control the specified lever\n in the specified force at the specified stage. This will\n be a custom equation if that has been set for this lever in this\n force, or else it will be a custom equation set for this lever,\n else it will be the default equation for this stage\n" ); - + + typedef ::SireCAS::Expression (::SireCAS::LambdaSchedule::*getEquation_function_type)(::QString const &, ::QString const &, ::QString const &) const; + getEquation_function_type getEquation_function_value(&::SireCAS::LambdaSchedule::getEquation); + + LambdaSchedule_exposer.def( + "getEquation", getEquation_function_value, (bp::arg("stage") = "*", bp::arg("force") = "*", bp::arg("lever") = "*"), "Return the equation used to control the specified lever\n in the specified force at the specified stage. This will\n be a custom equation if that has been set for this lever in this\n force, or else it will be a custom equation set for this lever,\n else it will be the default equation for this stage\n"); } { //::SireCAS::LambdaSchedule::getForces - - typedef ::QStringList ( ::SireCAS::LambdaSchedule::*getForces_function_type)( ) const; - getForces_function_type getForces_function_value( &::SireCAS::LambdaSchedule::getForces ); - - LambdaSchedule_exposer.def( - "getForces" - , getForces_function_value - , bp::release_gil_policy() - , "Return all of the forces that have been explicitly added\n to the schedule. Note that forces will be automatically added\n by any perturbation run that needs them, so you dont normally\n need to manage them manually yourself.\n" ); - + + typedef ::QStringList (::SireCAS::LambdaSchedule::*getForces_function_type)() const; + getForces_function_type getForces_function_value(&::SireCAS::LambdaSchedule::getForces); + + LambdaSchedule_exposer.def( + "getForces", getForces_function_value, bp::release_gil_policy(), "Return all of the forces that have been explicitly added\n to the schedule. Note that forces will be automatically added\n by any perturbation run that needs them, so you dont normally\n need to manage them manually yourself.\n"); } { //::SireCAS::LambdaSchedule::getLambdaInStage - - typedef double ( ::SireCAS::LambdaSchedule::*getLambdaInStage_function_type)( double ) const; - getLambdaInStage_function_type getLambdaInStage_function_value( &::SireCAS::LambdaSchedule::getLambdaInStage ); - - LambdaSchedule_exposer.def( - "getLambdaInStage" - , getLambdaInStage_function_value - , ( bp::arg("lambda_value") ) - , bp::release_gil_policy() - , "Return the stage-local value of :lambda: that corresponds to the\n global value of :lambda: at `lambda_value`\n" ); - + + typedef double (::SireCAS::LambdaSchedule::*getLambdaInStage_function_type)(double) const; + getLambdaInStage_function_type getLambdaInStage_function_value(&::SireCAS::LambdaSchedule::getLambdaInStage); + + LambdaSchedule_exposer.def( + "getLambdaInStage", getLambdaInStage_function_value, (bp::arg("lambda_value")), bp::release_gil_policy(), "Return the stage-local value of :lambda: that corresponds to the\n global value of :lambda: at `lambda_value`\n"); } { //::SireCAS::LambdaSchedule::getLeverStages - - typedef ::QStringList ( ::SireCAS::LambdaSchedule::*getLeverStages_function_type)( ::QVector< double > const & ) const; - getLeverStages_function_type getLeverStages_function_value( &::SireCAS::LambdaSchedule::getLeverStages ); - - LambdaSchedule_exposer.def( - "getLeverStages" - , getLeverStages_function_value - , ( bp::arg("lambda_values") ) - , bp::release_gil_policy() - , "Return the list of stages that are used for the passed list\n of lambda values. The stage names will be returned in the matching\n order of the lambda values.\n" ); - + + typedef ::QStringList (::SireCAS::LambdaSchedule::*getLeverStages_function_type)(::QVector const &) const; + getLeverStages_function_type getLeverStages_function_value(&::SireCAS::LambdaSchedule::getLeverStages); + + LambdaSchedule_exposer.def( + "getLeverStages", getLeverStages_function_value, (bp::arg("lambda_values")), bp::release_gil_policy(), "Return the list of stages that are used for the passed list\n of lambda values. The stage names will be returned in the matching\n order of the lambda values.\n"); } { //::SireCAS::LambdaSchedule::getLeverStages - - typedef ::QStringList ( ::SireCAS::LambdaSchedule::*getLeverStages_function_type)( int ) const; - getLeverStages_function_type getLeverStages_function_value( &::SireCAS::LambdaSchedule::getLeverStages ); - - LambdaSchedule_exposer.def( - "getLeverStages" - , getLeverStages_function_value - , ( bp::arg("num_lambda")=(int)(101) ) - , "Return the stages used for the list of `nvalue` lambda values\n generated for the global lambda value between 0 and 1 inclusive.\n" ); - + + typedef ::QStringList (::SireCAS::LambdaSchedule::*getLeverStages_function_type)(int) const; + getLeverStages_function_type getLeverStages_function_value(&::SireCAS::LambdaSchedule::getLeverStages); + + LambdaSchedule_exposer.def( + "getLeverStages", getLeverStages_function_value, (bp::arg("num_lambda") = (int)(101)), "Return the stages used for the list of `nvalue` lambda values\n generated for the global lambda value between 0 and 1 inclusive.\n"); } { //::SireCAS::LambdaSchedule::getLeverValues - - typedef ::QHash< QString, QVector< double > > ( ::SireCAS::LambdaSchedule::*getLeverValues_function_type)( ::QVector< double > const &,double,double ) const; - getLeverValues_function_type getLeverValues_function_value( &::SireCAS::LambdaSchedule::getLeverValues ); - - LambdaSchedule_exposer.def( - "getLeverValues" - , getLeverValues_function_value - , ( bp::arg("lambda_values"), bp::arg("initial")=1., bp::arg("final")=2. ) - , "Return the stage name and parameter values for that lever\n for the specified list of lambda values, assuming that a\n parameter for that stage has an initial value of\n `initial_value` and a final value of `final_value`. This\n is mostly useful for testing and graphing how this\n schedule would change some hyperthetical forcefield\n parameters for the specified lambda values.\n" ); - + + typedef ::QHash> (::SireCAS::LambdaSchedule::*getLeverValues_function_type)(::QVector const &, double, double) const; + getLeverValues_function_type getLeverValues_function_value(&::SireCAS::LambdaSchedule::getLeverValues); + + LambdaSchedule_exposer.def( + "getLeverValues", getLeverValues_function_value, (bp::arg("lambda_values"), bp::arg("initial") = 1., bp::arg("final") = 2.), "Return the stage name and parameter values for that lever\n for the specified list of lambda values, assuming that a\n parameter for that stage has an initial value of\n `initial_value` and a final value of `final_value`. This\n is mostly useful for testing and graphing how this\n schedule would change some hyperthetical forcefield\n parameters for the specified lambda values.\n"); } { //::SireCAS::LambdaSchedule::getLeverValues - - typedef ::QHash< QString, QVector< double > > ( ::SireCAS::LambdaSchedule::*getLeverValues_function_type)( int,double,double ) const; - getLeverValues_function_type getLeverValues_function_value( &::SireCAS::LambdaSchedule::getLeverValues ); - - LambdaSchedule_exposer.def( - "getLeverValues" - , getLeverValues_function_value - , ( bp::arg("num_lambda")=(int)(101), bp::arg("initial")=1., bp::arg("final")=2. ) - , "Return the lever name and parameter values for that lever\n for the specified number of lambda values generated\n evenly between 0 and 1, assuming that a\n parameter for that lever has an initial value of\n `initial_value` and a final value of `final_value`. This\n is mostly useful for testing and graphing how this\n schedule would change some hyperthetical forcefield\n parameters for the specified lambda values.\n" ); - + + typedef ::QHash> (::SireCAS::LambdaSchedule::*getLeverValues_function_type)(int, double, double) const; + getLeverValues_function_type getLeverValues_function_value(&::SireCAS::LambdaSchedule::getLeverValues); + + LambdaSchedule_exposer.def( + "getLeverValues", getLeverValues_function_value, (bp::arg("num_lambda") = (int)(101), bp::arg("initial") = 1., bp::arg("final") = 2.), "Return the lever name and parameter values for that lever\n for the specified number of lambda values generated\n evenly between 0 and 1, assuming that a\n parameter for that lever has an initial value of\n `initial_value` and a final value of `final_value`. This\n is mostly useful for testing and graphing how this\n schedule would change some hyperthetical forcefield\n parameters for the specified lambda values.\n"); } { //::SireCAS::LambdaSchedule::getLevers - - typedef ::QStringList ( ::SireCAS::LambdaSchedule::*getLevers_function_type)( ) const; - getLevers_function_type getLevers_function_value( &::SireCAS::LambdaSchedule::getLevers ); - - LambdaSchedule_exposer.def( - "getLevers" - , getLevers_function_value - , bp::release_gil_policy() - , "Return all of the levers that have been explicitly added\n to the schedule. Note that levers will be automatically added\n by any perturbation run that needs them, so you dont normally\n need to manage them manually yourself.\n" ); - + + typedef ::QStringList (::SireCAS::LambdaSchedule::*getLevers_function_type)() const; + getLevers_function_type getLevers_function_value(&::SireCAS::LambdaSchedule::getLevers); + + LambdaSchedule_exposer.def( + "getLevers", getLevers_function_value, bp::release_gil_policy(), "Return all of the levers that have been explicitly added\n to the schedule. Note that levers will be automatically added\n by any perturbation run that needs them, so you dont normally\n need to manage them manually yourself.\n"); } { //::SireCAS::LambdaSchedule::getMoleculeSchedule - - typedef ::SireCAS::LambdaSchedule const & ( ::SireCAS::LambdaSchedule::*getMoleculeSchedule_function_type)( int ) const; - getMoleculeSchedule_function_type getMoleculeSchedule_function_value( &::SireCAS::LambdaSchedule::getMoleculeSchedule ); - - LambdaSchedule_exposer.def( - "getMoleculeSchedule" - , getMoleculeSchedule_function_value - , ( bp::arg("pert_mol_id") ) - , bp::return_value_policy() - , "Return the schedule used to control perturbations for the\n perturbable molecule (or part of molecule) that is identified by the\n passed pert_mol_id. This schedule will be used to control\n all of the levers for this molecule (or part of molecule).\n\n This returns this schedule if there is no specified schedule\n for this molecule\n" ); - + + typedef ::SireCAS::LambdaSchedule const &(::SireCAS::LambdaSchedule::*getMoleculeSchedule_function_type)(int) const; + getMoleculeSchedule_function_type getMoleculeSchedule_function_value(&::SireCAS::LambdaSchedule::getMoleculeSchedule); + + LambdaSchedule_exposer.def( + "getMoleculeSchedule", getMoleculeSchedule_function_value, (bp::arg("pert_mol_id")), bp::return_value_policy(), "Return the schedule used to control perturbations for the\n perturbable molecule (or part of molecule) that is identified by the\n passed pert_mol_id. This schedule will be used to control\n all of the levers for this molecule (or part of molecule).\n\n This returns this schedule if there is no specified schedule\n for this molecule\n"); } { //::SireCAS::LambdaSchedule::getStage - - typedef ::QString ( ::SireCAS::LambdaSchedule::*getStage_function_type)( double ) const; - getStage_function_type getStage_function_value( &::SireCAS::LambdaSchedule::getStage ); - - LambdaSchedule_exposer.def( - "getStage" - , getStage_function_value - , ( bp::arg("lambda_value") ) - , bp::release_gil_policy() - , "Return the name of the stage that controls the forcefield parameters\n at the global value of :lambda: equal to `lambda_value`\n" ); - + + typedef ::QString (::SireCAS::LambdaSchedule::*getStage_function_type)(double) const; + getStage_function_type getStage_function_value(&::SireCAS::LambdaSchedule::getStage); + + LambdaSchedule_exposer.def( + "getStage", getStage_function_value, (bp::arg("lambda_value")), bp::release_gil_policy(), "Return the name of the stage that controls the forcefield parameters\n at the global value of :lambda: equal to `lambda_value`\n"); } { //::SireCAS::LambdaSchedule::getStages - - typedef ::QStringList ( ::SireCAS::LambdaSchedule::*getStages_function_type)( ) const; - getStages_function_type getStages_function_value( &::SireCAS::LambdaSchedule::getStages ); - - LambdaSchedule_exposer.def( - "getStages" - , getStages_function_value - , bp::release_gil_policy() - , "Return the names of all of the stages in this schedule, in\n the order they will be performed\n" ); - + + typedef ::QStringList (::SireCAS::LambdaSchedule::*getStages_function_type)() const; + getStages_function_type getStages_function_value(&::SireCAS::LambdaSchedule::getStages); + + LambdaSchedule_exposer.def( + "getStages", getStages_function_value, bp::release_gil_policy(), "Return the names of all of the stages in this schedule, in\n the order they will be performed\n"); } { //::SireCAS::LambdaSchedule::hasForceSpecificEquation - - typedef bool ( ::SireCAS::LambdaSchedule::*hasForceSpecificEquation_function_type)( ::QString const &,::QString const &,::QString const & ) const; - hasForceSpecificEquation_function_type hasForceSpecificEquation_function_value( &::SireCAS::LambdaSchedule::hasForceSpecificEquation ); - - LambdaSchedule_exposer.def( - "hasForceSpecificEquation" - , hasForceSpecificEquation_function_value - , ( bp::arg("stage")="*", bp::arg("force")="*", bp::arg("lever")="*" ) - , "Return whether or not the specified lever in the specified force\n at the specified stage has a custom equation set for it\n" ); - + + typedef bool (::SireCAS::LambdaSchedule::*hasForceSpecificEquation_function_type)(::QString const &, ::QString const &, ::QString const &) const; + hasForceSpecificEquation_function_type hasForceSpecificEquation_function_value(&::SireCAS::LambdaSchedule::hasForceSpecificEquation); + + LambdaSchedule_exposer.def( + "hasForceSpecificEquation", hasForceSpecificEquation_function_value, (bp::arg("stage") = "*", bp::arg("force") = "*", bp::arg("lever") = "*"), "Return whether or not the specified lever in the specified force\n at the specified stage has a custom equation set for it\n"); } { //::SireCAS::LambdaSchedule::hasMoleculeSchedule - - typedef bool ( ::SireCAS::LambdaSchedule::*hasMoleculeSchedule_function_type)( int ) const; - hasMoleculeSchedule_function_type hasMoleculeSchedule_function_value( &::SireCAS::LambdaSchedule::hasMoleculeSchedule ); - - LambdaSchedule_exposer.def( - "hasMoleculeSchedule" - , hasMoleculeSchedule_function_value - , ( bp::arg("pert_mol_id") ) - , bp::release_gil_policy() - , "Return whether or not the perturbable molecule (or part of molecule)\n that is identified by passed pert_mol_id has its own schedule" ); - + + typedef bool (::SireCAS::LambdaSchedule::*hasMoleculeSchedule_function_type)(int) const; + hasMoleculeSchedule_function_type hasMoleculeSchedule_function_value(&::SireCAS::LambdaSchedule::hasMoleculeSchedule); + + LambdaSchedule_exposer.def( + "hasMoleculeSchedule", hasMoleculeSchedule_function_value, (bp::arg("pert_mol_id")), bp::release_gil_policy(), "Return whether or not the perturbable molecule (or part of molecule)\n that is identified by passed pert_mol_id has its own schedule"); } { //::SireCAS::LambdaSchedule::initial - - typedef ::SireCAS::Symbol ( *initial_function_type )( ); - initial_function_type initial_function_value( &::SireCAS::LambdaSchedule::initial ); - - LambdaSchedule_exposer.def( - "initial" - , initial_function_value - , bp::release_gil_policy() - , "Return the symbol used to represent the initial\n (:lambda:=0) value of the forcefield parameter\n" ); - + + typedef ::SireCAS::Symbol (*initial_function_type)(); + initial_function_type initial_function_value(&::SireCAS::LambdaSchedule::initial); + + LambdaSchedule_exposer.def( + "initial", initial_function_value, bp::release_gil_policy(), "Return the symbol used to represent the initial\n (:lambda:=0) value of the forcefield parameter\n"); } { //::SireCAS::LambdaSchedule::insertStage - - typedef void ( ::SireCAS::LambdaSchedule::*insertStage_function_type)( int,::QString const &,::SireCAS::Expression const & ) ; - insertStage_function_type insertStage_function_value( &::SireCAS::LambdaSchedule::insertStage ); - - LambdaSchedule_exposer.def( - "insertStage" - , insertStage_function_value - , ( bp::arg("i"), bp::arg("stage"), bp::arg("equation") ) - , bp::release_gil_policy() - , "Insert a stage called name at position `i` which uses the passed\n equation. The equation will be the default\n equation that scales all parameters (levers) that dont have\n a custom lever for this stage.\n" ); - + + typedef void (::SireCAS::LambdaSchedule::*insertStage_function_type)(int, ::QString const &, ::SireCAS::Expression const &, double); + insertStage_function_type insertStage_function_value(&::SireCAS::LambdaSchedule::insertStage); + + LambdaSchedule_exposer.def( + "insertStage", insertStage_function_value, (bp::arg("i"), bp::arg("stage"), bp::arg("equation"), bp::arg("weight") = 1.0), bp::release_gil_policy(), "Insert a stage called name at position `i` which uses the passed\n equation. The equation will be the default\n equation that scales all parameters (levers) that dont have\n a custom lever for this stage. The optional weight controls the\n fraction of lambda space the stage occupies relative to other stages.\n"); } { //::SireCAS::LambdaSchedule::isNull - - typedef bool ( ::SireCAS::LambdaSchedule::*isNull_function_type)( ) const; - isNull_function_type isNull_function_value( &::SireCAS::LambdaSchedule::isNull ); - - LambdaSchedule_exposer.def( - "isNull" - , isNull_function_value - , bp::release_gil_policy() - , "" ); - + + typedef bool (::SireCAS::LambdaSchedule::*isNull_function_type)() const; + isNull_function_type isNull_function_value(&::SireCAS::LambdaSchedule::isNull); + + LambdaSchedule_exposer.def( + "isNull", isNull_function_value, bp::release_gil_policy(), ""); } { //::SireCAS::LambdaSchedule::lam - - typedef ::SireCAS::Symbol ( *lam_function_type )( ); - lam_function_type lam_function_value( &::SireCAS::LambdaSchedule::lam ); - - LambdaSchedule_exposer.def( - "lam" - , lam_function_value - , bp::release_gil_policy() - , "Return the symbol used to represent the :lambda: coordinate.\n This symbol is used to represent the per-stage :lambda:\n variable that goes from 0.0-1.0 within that stage.\n" ); - + + typedef ::SireCAS::Symbol (*lam_function_type)(); + lam_function_type lam_function_value(&::SireCAS::LambdaSchedule::lam); + + LambdaSchedule_exposer.def( + "lam", lam_function_value, bp::release_gil_policy(), "Return the symbol used to represent the :lambda: coordinate.\n This symbol is used to represent the per-stage :lambda:\n variable that goes from 0.0-1.0 within that stage.\n"); } { //::SireCAS::LambdaSchedule::morph - - typedef double ( ::SireCAS::LambdaSchedule::*morph_function_type)( ::QString const &,::QString const &,double,double,double ) const; - morph_function_type morph_function_value( &::SireCAS::LambdaSchedule::morph ); - - LambdaSchedule_exposer.def( - "morph" - , morph_function_value - , ( bp::arg("force")="*", bp::arg("lever")="*", bp::arg("initial")=0, bp::arg("final")=1, bp::arg("lambda_value")=0 ) - , "Return the parameters for the specified lever called `lever_name`\n in the force force\n that have been morphed from the passed list of initial values\n (in `initial`) to the passed list of final values (in `final`)\n for the specified global value of :lambda: (in `lambda_value`).\n\n The morphed parameters will be returned in the matching\n order to `initial` and `final`.\n\n This morphs a single floating point parameters.\n" ); - + + typedef double (::SireCAS::LambdaSchedule::*morph_function_type)(::QString const &, ::QString const &, double, double, double) const; + morph_function_type morph_function_value(&::SireCAS::LambdaSchedule::morph); + + LambdaSchedule_exposer.def( + "morph", morph_function_value, (bp::arg("force") = "*", bp::arg("lever") = "*", bp::arg("initial") = 0, bp::arg("final") = 1, bp::arg("lambda_value") = 0), "Return the parameters for the specified lever called `lever_name`\n in the force force\n that have been morphed from the passed list of initial values\n (in `initial`) to the passed list of final values (in `final`)\n for the specified global value of :lambda: (in `lambda_value`).\n\n The morphed parameters will be returned in the matching\n order to `initial` and `final`.\n\n This morphs a single floating point parameters.\n"); } { //::SireCAS::LambdaSchedule::morph - - typedef ::QVector< double > ( ::SireCAS::LambdaSchedule::*morph_function_type)( ::QString const &,::QString const &,::QVector< double > const &,::QVector< double > const &,double ) const; - morph_function_type morph_function_value( &::SireCAS::LambdaSchedule::morph ); - - LambdaSchedule_exposer.def( - "morph" - , morph_function_value - , ( bp::arg("force")="*", bp::arg("lever")="*", bp::arg("initial")=::QVector( ), bp::arg("final")=::QVector( ), bp::arg("lambda_value")=0. ) - , "Return the parameters for the specified lever called `lever_name`\n in the specified force,\n that have been morphed from the passed list of initial values\n (in `initial`) to the passed list of final values (in `final`)\n for the specified global value of :lambda: (in `lambda_value`).\n\n The morphed parameters will be returned in the matching\n order to `initial` and `final`.\n\n This morphs floating point parameters. There is an overload\n of this function that morphs integer parameters, in which\n case the result would be rounded to the nearest integer.\n" ); - + + typedef ::QVector (::SireCAS::LambdaSchedule::*morph_function_type)(::QString const &, ::QString const &, ::QVector const &, ::QVector const &, double) const; + morph_function_type morph_function_value(&::SireCAS::LambdaSchedule::morph); + + LambdaSchedule_exposer.def( + "morph", morph_function_value, (bp::arg("force") = "*", bp::arg("lever") = "*", bp::arg("initial") = ::QVector(), bp::arg("final") = ::QVector(), bp::arg("lambda_value") = 0.), "Return the parameters for the specified lever called `lever_name`\n in the specified force,\n that have been morphed from the passed list of initial values\n (in `initial`) to the passed list of final values (in `final`)\n for the specified global value of :lambda: (in `lambda_value`).\n\n The morphed parameters will be returned in the matching\n order to `initial` and `final`.\n\n This morphs floating point parameters. There is an overload\n of this function that morphs integer parameters, in which\n case the result would be rounded to the nearest integer.\n"); } { //::SireCAS::LambdaSchedule::morph - - typedef ::QVector< int > ( ::SireCAS::LambdaSchedule::*morph_function_type)( ::QString const &,::QString const &,::QVector< int > const &,::QVector< int > const &,double ) const; - morph_function_type morph_function_value( &::SireCAS::LambdaSchedule::morph ); - - LambdaSchedule_exposer.def( - "morph" - , morph_function_value - , ( bp::arg("force")="*", bp::arg("lever")="*", bp::arg("initial")=::QVector( ), bp::arg("final")=::QVector( ), bp::arg("lambda_value")=0. ) - , "Return the parameters for the specified lever called `lever_name`\n for the specified force\n that have been morphed from the passed list of initial values\n (in `initial`) to the passed list of final values (in `final`)\n for the specified global value of :lambda: (in `lambda_value`).\n\n The morphed parameters will be returned in the matching\n order to `initial` and `final`.\n\n This function morphs integer parameters. In this case,\n the result will be the rounded to the nearest integer.\n" ); - + + typedef ::QVector (::SireCAS::LambdaSchedule::*morph_function_type)(::QString const &, ::QString const &, ::QVector const &, ::QVector const &, double) const; + morph_function_type morph_function_value(&::SireCAS::LambdaSchedule::morph); + + LambdaSchedule_exposer.def( + "morph", morph_function_value, (bp::arg("force") = "*", bp::arg("lever") = "*", bp::arg("initial") = ::QVector(), bp::arg("final") = ::QVector(), bp::arg("lambda_value") = 0.), "Return the parameters for the specified lever called `lever_name`\n for the specified force\n that have been morphed from the passed list of initial values\n (in `initial`) to the passed list of final values (in `final`)\n for the specified global value of :lambda: (in `lambda_value`).\n\n The morphed parameters will be returned in the matching\n order to `initial` and `final`.\n\n This function morphs integer parameters. In this case,\n the result will be the rounded to the nearest integer.\n"); } { //::SireCAS::LambdaSchedule::nForces - - typedef int ( ::SireCAS::LambdaSchedule::*nForces_function_type)( ) const; - nForces_function_type nForces_function_value( &::SireCAS::LambdaSchedule::nForces ); - - LambdaSchedule_exposer.def( - "nForces" - , nForces_function_value - , bp::release_gil_policy() - , "Return the number of forces that have been explicitly added\n to the schedule. Note that forces will be automatically added\n by any perturbation run that needs them, so you dont normally\n need to manage them manually yourself.\n" ); - + + typedef int (::SireCAS::LambdaSchedule::*nForces_function_type)() const; + nForces_function_type nForces_function_value(&::SireCAS::LambdaSchedule::nForces); + + LambdaSchedule_exposer.def( + "nForces", nForces_function_value, bp::release_gil_policy(), "Return the number of forces that have been explicitly added\n to the schedule. Note that forces will be automatically added\n by any perturbation run that needs them, so you dont normally\n need to manage them manually yourself.\n"); } { //::SireCAS::LambdaSchedule::nLevers - - typedef int ( ::SireCAS::LambdaSchedule::*nLevers_function_type)( ) const; - nLevers_function_type nLevers_function_value( &::SireCAS::LambdaSchedule::nLevers ); - - LambdaSchedule_exposer.def( - "nLevers" - , nLevers_function_value - , bp::release_gil_policy() - , "Return the number of levers that have been explicitly added\n to the schedule. Note that levers will be automatically added\n by any perturbation run that needs them, so you dont normally\n need to manage them manually yourself.\n" ); - + + typedef int (::SireCAS::LambdaSchedule::*nLevers_function_type)() const; + nLevers_function_type nLevers_function_value(&::SireCAS::LambdaSchedule::nLevers); + + LambdaSchedule_exposer.def( + "nLevers", nLevers_function_value, bp::release_gil_policy(), "Return the number of levers that have been explicitly added\n to the schedule. Note that levers will be automatically added\n by any perturbation run that needs them, so you dont normally\n need to manage them manually yourself.\n"); } { //::SireCAS::LambdaSchedule::nStages - - typedef int ( ::SireCAS::LambdaSchedule::*nStages_function_type)( ) const; - nStages_function_type nStages_function_value( &::SireCAS::LambdaSchedule::nStages ); - - LambdaSchedule_exposer.def( - "nStages" - , nStages_function_value - , bp::release_gil_policy() - , "Return the number of stages in this schedule" ); - - } - LambdaSchedule_exposer.def( bp::self != bp::self ); + + typedef int (::SireCAS::LambdaSchedule::*nStages_function_type)() const; + nStages_function_type nStages_function_value(&::SireCAS::LambdaSchedule::nStages); + + LambdaSchedule_exposer.def( + "nStages", nStages_function_value, bp::release_gil_policy(), "Return the number of stages in this schedule"); + } + LambdaSchedule_exposer.def(bp::self != bp::self); { //::SireCAS::LambdaSchedule::operator= - - typedef ::SireCAS::LambdaSchedule & ( ::SireCAS::LambdaSchedule::*assign_function_type)( ::SireCAS::LambdaSchedule const & ) ; - assign_function_type assign_function_value( &::SireCAS::LambdaSchedule::operator= ); - - LambdaSchedule_exposer.def( - "assign" - , assign_function_value - , ( bp::arg("other") ) - , bp::return_self< >() - , "" ); - - } - LambdaSchedule_exposer.def( bp::self == bp::self ); + + typedef ::SireCAS::LambdaSchedule &(::SireCAS::LambdaSchedule::*assign_function_type)(::SireCAS::LambdaSchedule const &); + assign_function_type assign_function_value(&::SireCAS::LambdaSchedule::operator=); + + LambdaSchedule_exposer.def( + "assign", assign_function_value, (bp::arg("other")), bp::return_self<>(), ""); + } + LambdaSchedule_exposer.def(bp::self == bp::self); { //::SireCAS::LambdaSchedule::prependStage - - typedef void ( ::SireCAS::LambdaSchedule::*prependStage_function_type)( ::QString const &,::SireCAS::Expression const & ) ; - prependStage_function_type prependStage_function_value( &::SireCAS::LambdaSchedule::prependStage ); - - LambdaSchedule_exposer.def( - "prependStage" - , prependStage_function_value - , ( bp::arg("stage"), bp::arg("equation") ) - , bp::release_gil_policy() - , "Prepend a stage called name which uses the passed equation\n to the start of this schedule. The equation will be the default\n equation that scales all parameters (levers) that dont have\n a custom lever for this stage.\n" ); - + + typedef void (::SireCAS::LambdaSchedule::*prependStage_function_type)(::QString const &, ::SireCAS::Expression const &, double); + prependStage_function_type prependStage_function_value(&::SireCAS::LambdaSchedule::prependStage); + + LambdaSchedule_exposer.def( + "prependStage", prependStage_function_value, (bp::arg("stage"), bp::arg("equation"), bp::arg("weight") = 1.0), bp::release_gil_policy(), "Prepend a stage called name which uses the passed equation\n to the start of this schedule. The equation will be the default\n equation that scales all parameters (levers) that dont have\n a custom lever for this stage. The optional weight controls the\n fraction of lambda space the stage occupies relative to other stages.\n"); } { //::SireCAS::LambdaSchedule::removeEquation - - typedef void ( ::SireCAS::LambdaSchedule::*removeEquation_function_type)( ::QString const &,::QString const &,::QString const & ) ; - removeEquation_function_type removeEquation_function_value( &::SireCAS::LambdaSchedule::removeEquation ); - - LambdaSchedule_exposer.def( - "removeEquation" - , removeEquation_function_value - , ( bp::arg("stage")="*", bp::arg("force")="*", bp::arg("lever")="*" ) - , "Remove the custom equation for the specified `lever` in the\n specified force at the specified `stage`.\n The lever will now use the equation specified for this\n lever for this stage, or the default lever for the stage\n if this isnt set\n" ); - + + typedef void (::SireCAS::LambdaSchedule::*removeEquation_function_type)(::QString const &, ::QString const &, ::QString const &); + removeEquation_function_type removeEquation_function_value(&::SireCAS::LambdaSchedule::removeEquation); + + LambdaSchedule_exposer.def( + "removeEquation", removeEquation_function_value, (bp::arg("stage") = "*", bp::arg("force") = "*", bp::arg("lever") = "*"), "Remove the custom equation for the specified `lever` in the\n specified force at the specified `stage`.\n The lever will now use the equation specified for this\n lever for this stage, or the default lever for the stage\n if this isnt set\n"); + } + { //::SireCAS::LambdaSchedule::coupleLever + + typedef void (::SireCAS::LambdaSchedule::*coupleLever_function_type)(::QString const &, ::QString const &, ::QString const &, ::QString const &); + coupleLever_function_type coupleLever_function_value(&::SireCAS::LambdaSchedule::coupleLever); + + LambdaSchedule_exposer.def( + "coupleLever", coupleLever_function_value, (bp::arg("force"), bp::arg("lever"), bp::arg("fallback_force"), bp::arg("fallback_lever")), bp::release_gil_policy(), "Couple force::lever to fallback_force::fallback_lever. When no\n custom equation is set for force::lever, the equation for\n fallback_force::fallback_lever will be used instead of the stage\n default. By default cmap::cmap_grid is coupled to torsion::torsion_k.\n"); + } + { //::SireCAS::LambdaSchedule::removeCoupledLever + + typedef void (::SireCAS::LambdaSchedule::*removeCoupledLever_function_type)(::QString const &, ::QString const &); + removeCoupledLever_function_type removeCoupledLever_function_value(&::SireCAS::LambdaSchedule::removeCoupledLever); + + LambdaSchedule_exposer.def( + "removeCoupledLever", removeCoupledLever_function_value, (bp::arg("force"), bp::arg("lever")), bp::release_gil_policy(), "Remove any coupling for force::lever, reverting it to use the\n stage default equation when no custom equation is set.\n"); } { //::SireCAS::LambdaSchedule::removeForce - - typedef void ( ::SireCAS::LambdaSchedule::*removeForce_function_type)( ::QString const & ) ; - removeForce_function_type removeForce_function_value( &::SireCAS::LambdaSchedule::removeForce ); - - LambdaSchedule_exposer.def( - "removeForce" - , removeForce_function_value - , ( bp::arg("force") ) - , bp::release_gil_policy() - , "Remove a force from a schedule. This will not impact any\n perturbation runs that use this schedule, as any missing\n forces will be re-added.\n" ); - + + typedef void (::SireCAS::LambdaSchedule::*removeForce_function_type)(::QString const &); + removeForce_function_type removeForce_function_value(&::SireCAS::LambdaSchedule::removeForce); + + LambdaSchedule_exposer.def( + "removeForce", removeForce_function_value, (bp::arg("force")), bp::release_gil_policy(), "Remove a force from a schedule. This will not impact any\n perturbation runs that use this schedule, as any missing\n forces will be re-added.\n"); } { //::SireCAS::LambdaSchedule::removeForces - - typedef void ( ::SireCAS::LambdaSchedule::*removeForces_function_type)( ::QStringList const & ) ; - removeForces_function_type removeForces_function_value( &::SireCAS::LambdaSchedule::removeForces ); - - LambdaSchedule_exposer.def( - "removeForces" - , removeForces_function_value - , ( bp::arg("forces") ) - , bp::release_gil_policy() - , "Remove some forces from a schedule. This will not impact any\n perturbation runs that use this schedule, as any missing\n forces will be re-added.\n" ); - + + typedef void (::SireCAS::LambdaSchedule::*removeForces_function_type)(::QStringList const &); + removeForces_function_type removeForces_function_value(&::SireCAS::LambdaSchedule::removeForces); + + LambdaSchedule_exposer.def( + "removeForces", removeForces_function_value, (bp::arg("forces")), bp::release_gil_policy(), "Remove some forces from a schedule. This will not impact any\n perturbation runs that use this schedule, as any missing\n forces will be re-added.\n"); } { //::SireCAS::LambdaSchedule::removeLever - - typedef void ( ::SireCAS::LambdaSchedule::*removeLever_function_type)( ::QString const & ) ; - removeLever_function_type removeLever_function_value( &::SireCAS::LambdaSchedule::removeLever ); - - LambdaSchedule_exposer.def( - "removeLever" - , removeLever_function_value - , ( bp::arg("lever") ) - , bp::release_gil_policy() - , "Remove a lever from the schedule. This will not impact any\n perturbation runs that use this schedule, as any missing\n levers will be re-added.\n" ); - + + typedef void (::SireCAS::LambdaSchedule::*removeLever_function_type)(::QString const &); + removeLever_function_type removeLever_function_value(&::SireCAS::LambdaSchedule::removeLever); + + LambdaSchedule_exposer.def( + "removeLever", removeLever_function_value, (bp::arg("lever")), bp::release_gil_policy(), "Remove a lever from the schedule. This will not impact any\n perturbation runs that use this schedule, as any missing\n levers will be re-added.\n"); } { //::SireCAS::LambdaSchedule::removeLevers - - typedef void ( ::SireCAS::LambdaSchedule::*removeLevers_function_type)( ::QStringList const & ) ; - removeLevers_function_type removeLevers_function_value( &::SireCAS::LambdaSchedule::removeLevers ); - - LambdaSchedule_exposer.def( - "removeLevers" - , removeLevers_function_value - , ( bp::arg("levers") ) - , bp::release_gil_policy() - , "Remove some levers from the schedule. This will not impact any\n perturbation runs that use this schedule, as any missing\n levers will be re-added.\n" ); - + + typedef void (::SireCAS::LambdaSchedule::*removeLevers_function_type)(::QStringList const &); + removeLevers_function_type removeLevers_function_value(&::SireCAS::LambdaSchedule::removeLevers); + + LambdaSchedule_exposer.def( + "removeLevers", removeLevers_function_value, (bp::arg("levers")), bp::release_gil_policy(), "Remove some levers from the schedule. This will not impact any\n perturbation runs that use this schedule, as any missing\n levers will be re-added.\n"); } { //::SireCAS::LambdaSchedule::removeMoleculeSchedule - - typedef void ( ::SireCAS::LambdaSchedule::*removeMoleculeSchedule_function_type)( int ) ; - removeMoleculeSchedule_function_type removeMoleculeSchedule_function_value( &::SireCAS::LambdaSchedule::removeMoleculeSchedule ); - - LambdaSchedule_exposer.def( - "removeMoleculeSchedule" - , removeMoleculeSchedule_function_value - , ( bp::arg("pert_mol_id") ) - , bp::release_gil_policy() - , "Remove the perturbable molecule-specific schedule associated\n with the perturbable molecule (or part of molecule) that is\n identified by the passed pert_mol_id.\n" ); - + + typedef void (::SireCAS::LambdaSchedule::*removeMoleculeSchedule_function_type)(int); + removeMoleculeSchedule_function_type removeMoleculeSchedule_function_value(&::SireCAS::LambdaSchedule::removeMoleculeSchedule); + + LambdaSchedule_exposer.def( + "removeMoleculeSchedule", removeMoleculeSchedule_function_value, (bp::arg("pert_mol_id")), bp::release_gil_policy(), "Remove the perturbable molecule-specific schedule associated\n with the perturbable molecule (or part of molecule) that is\n identified by the passed pert_mol_id.\n"); + } + { //::SireCAS::LambdaSchedule::getStageWeight + + typedef double (::SireCAS::LambdaSchedule::*getStageWeight_function_type)(::QString const &) const; + getStageWeight_function_type getStageWeight_function_value(&::SireCAS::LambdaSchedule::getStageWeight); + + LambdaSchedule_exposer.def( + "getStageWeight", getStageWeight_function_value, (bp::arg("stage")), bp::release_gil_policy(), "Return the relative weight of the stage in lambda space. A stage\n with weight 2 occupies twice the lambda range as a stage with weight 1.\n"); + } + { //::SireCAS::LambdaSchedule::getStageWeights + + typedef ::QVector (::SireCAS::LambdaSchedule::*getStageWeights_function_type)() const; + getStageWeights_function_type getStageWeights_function_value(&::SireCAS::LambdaSchedule::getStageWeights); + + LambdaSchedule_exposer.def( + "getStageWeights", getStageWeights_function_value, bp::release_gil_policy(), "Return the relative weights of all stages, in stage order.\n"); + } + { //::SireCAS::LambdaSchedule::setStageWeight + + typedef void (::SireCAS::LambdaSchedule::*setStageWeight_function_type)(::QString const &, double); + setStageWeight_function_type setStageWeight_function_value(&::SireCAS::LambdaSchedule::setStageWeight); + + LambdaSchedule_exposer.def( + "setStageWeight", setStageWeight_function_value, (bp::arg("stage"), bp::arg("weight")), bp::release_gil_policy(), "Set the relative weight of the stage in lambda space. A stage with\n weight 2 occupies twice the lambda range as a stage with weight 1.\n Weights must be positive.\n"); } { //::SireCAS::LambdaSchedule::removeStage - - typedef void ( ::SireCAS::LambdaSchedule::*removeStage_function_type)( ::QString const & ) ; - removeStage_function_type removeStage_function_value( &::SireCAS::LambdaSchedule::removeStage ); - - LambdaSchedule_exposer.def( - "removeStage" - , removeStage_function_value - , ( bp::arg("stage") ) - , bp::release_gil_policy() - , "Remove the stage stage" ); - + + typedef void (::SireCAS::LambdaSchedule::*removeStage_function_type)(::QString const &); + removeStage_function_type removeStage_function_value(&::SireCAS::LambdaSchedule::removeStage); + + LambdaSchedule_exposer.def( + "removeStage", removeStage_function_value, (bp::arg("stage")), bp::release_gil_policy(), "Remove the stage stage"); + } + { //::SireCAS::LambdaSchedule::reverse + + typedef ::SireCAS::LambdaSchedule (::SireCAS::LambdaSchedule::*reverse_function_type)() const; + reverse_function_type reverse_function_value(&::SireCAS::LambdaSchedule::reverse); + + LambdaSchedule_exposer.def( + "reverse", reverse_function_value, bp::release_gil_policy(), "Return a new LambdaSchedule that is the reverse of this schedule.\n The stages are reversed in order and in each stages equations\n the lambda symbol is replaced by (1 - lambda) and the initial and\n final symbols are swapped simultaneously. The invariant is:\n reversed.morph(force, lever, initial, final, lam) ==\n original.morph(force, lever, final, initial, 1-lam)\n"); } { //::SireCAS::LambdaSchedule::setConstant - - typedef ::SireCAS::Symbol ( ::SireCAS::LambdaSchedule::*setConstant_function_type)( ::QString const &,double ) ; - setConstant_function_type setConstant_function_value( &::SireCAS::LambdaSchedule::setConstant ); - - LambdaSchedule_exposer.def( - "setConstant" - , setConstant_function_value - , ( bp::arg("constant"), bp::arg("value") ) - , bp::release_gil_policy() - , "Set the value of a constant that may be used in any\n of the stage equations.\n" ); - + + typedef ::SireCAS::Symbol (::SireCAS::LambdaSchedule::*setConstant_function_type)(::QString const &, double); + setConstant_function_type setConstant_function_value(&::SireCAS::LambdaSchedule::setConstant); + + LambdaSchedule_exposer.def( + "setConstant", setConstant_function_value, (bp::arg("constant"), bp::arg("value")), bp::release_gil_policy(), "Set the value of a constant that may be used in any\n of the stage equations.\n"); } { //::SireCAS::LambdaSchedule::setConstant - - typedef ::SireCAS::Symbol ( ::SireCAS::LambdaSchedule::*setConstant_function_type)( ::SireCAS::Symbol const &,double ) ; - setConstant_function_type setConstant_function_value( &::SireCAS::LambdaSchedule::setConstant ); - - LambdaSchedule_exposer.def( - "setConstant" - , setConstant_function_value - , ( bp::arg("constant"), bp::arg("value") ) - , bp::release_gil_policy() - , "Set the value of a constant that may be used in any\n of the stage equations.\n" ); - + + typedef ::SireCAS::Symbol (::SireCAS::LambdaSchedule::*setConstant_function_type)(::SireCAS::Symbol const &, double); + setConstant_function_type setConstant_function_value(&::SireCAS::LambdaSchedule::setConstant); + + LambdaSchedule_exposer.def( + "setConstant", setConstant_function_value, (bp::arg("constant"), bp::arg("value")), bp::release_gil_policy(), "Set the value of a constant that may be used in any\n of the stage equations.\n"); } { //::SireCAS::LambdaSchedule::setDefaultStageEquation - - typedef void ( ::SireCAS::LambdaSchedule::*setDefaultStageEquation_function_type)( ::QString const &,::SireCAS::Expression const & ) ; - setDefaultStageEquation_function_type setDefaultStageEquation_function_value( &::SireCAS::LambdaSchedule::setDefaultStageEquation ); - - LambdaSchedule_exposer.def( - "setDefaultStageEquation" - , setDefaultStageEquation_function_value - , ( bp::arg("stage"), bp::arg("equation") ) - , bp::release_gil_policy() - , "Set the default equation used to control levers for the\n stage stage to equation. This equation will be used\n to control any levers in this stage that dont have\n their own custom equation.\n" ); - + + typedef void (::SireCAS::LambdaSchedule::*setDefaultStageEquation_function_type)(::QString const &, ::SireCAS::Expression const &); + setDefaultStageEquation_function_type setDefaultStageEquation_function_value(&::SireCAS::LambdaSchedule::setDefaultStageEquation); + + LambdaSchedule_exposer.def( + "setDefaultStageEquation", setDefaultStageEquation_function_value, (bp::arg("stage"), bp::arg("equation")), bp::release_gil_policy(), "Set the default equation used to control levers for the\n stage stage to equation. This equation will be used\n to control any levers in this stage that dont have\n their own custom equation.\n"); } { //::SireCAS::LambdaSchedule::setEquation - - typedef void ( ::SireCAS::LambdaSchedule::*setEquation_function_type)( ::QString const &,::QString const &,::QString const &,::SireCAS::Expression const & ) ; - setEquation_function_type setEquation_function_value( &::SireCAS::LambdaSchedule::setEquation ); - - LambdaSchedule_exposer.def( - "setEquation" - , setEquation_function_value - , ( bp::arg("stage")="*", bp::arg("force")="*", bp::arg("lever")="*", bp::arg("equation")=SireCAS::Expression() ) - , "Set the custom equation used to control the specified lever\n for the specified force at the stage stage to equation.\n This equation will only be used to control the parameters for the\n specified lever in the specified force at the specified stage\n" ); - + + typedef void (::SireCAS::LambdaSchedule::*setEquation_function_type)(::QString const &, ::QString const &, ::QString const &, ::SireCAS::Expression const &); + setEquation_function_type setEquation_function_value(&::SireCAS::LambdaSchedule::setEquation); + + LambdaSchedule_exposer.def( + "setEquation", setEquation_function_value, (bp::arg("stage") = "*", bp::arg("force") = "*", bp::arg("lever") = "*", bp::arg("equation") = SireCAS::Expression()), "Set the custom equation used to control the specified lever\n for the specified force at the stage stage to equation.\n This equation will only be used to control the parameters for the\n specified lever in the specified force at the specified stage\n"); } { //::SireCAS::LambdaSchedule::setMoleculeSchedule - - typedef void ( ::SireCAS::LambdaSchedule::*setMoleculeSchedule_function_type)( int,::SireCAS::LambdaSchedule const & ) ; - setMoleculeSchedule_function_type setMoleculeSchedule_function_value( &::SireCAS::LambdaSchedule::setMoleculeSchedule ); - - LambdaSchedule_exposer.def( - "setMoleculeSchedule" - , setMoleculeSchedule_function_value - , ( bp::arg("pert_mol_id"), bp::arg("schedule") ) - , bp::release_gil_policy() - , "Set schedule as the molecule-specific schedule for the\n perturbable molecule (or part of molecule) that is identified by the\n passed pert_mol_id. This schedule will be used to control\n all of the levers for this molecule (or part of molecule),\n and replaces any levers provided by this schedule\n" ); - + + typedef void (::SireCAS::LambdaSchedule::*setMoleculeSchedule_function_type)(int, ::SireCAS::LambdaSchedule const &); + setMoleculeSchedule_function_type setMoleculeSchedule_function_value(&::SireCAS::LambdaSchedule::setMoleculeSchedule); + + LambdaSchedule_exposer.def( + "setMoleculeSchedule", setMoleculeSchedule_function_value, (bp::arg("pert_mol_id"), bp::arg("schedule")), bp::release_gil_policy(), "Set schedule as the molecule-specific schedule for the\n perturbable molecule (or part of molecule) that is identified by the\n passed pert_mol_id. This schedule will be used to control\n all of the levers for this molecule (or part of molecule),\n and replaces any levers provided by this schedule\n"); } { //::SireCAS::LambdaSchedule::standard_annihilate - - typedef ::SireCAS::LambdaSchedule ( *standard_annihilate_function_type )( bool ); - standard_annihilate_function_type standard_annihilate_function_value( &::SireCAS::LambdaSchedule::standard_annihilate ); - - LambdaSchedule_exposer.def( - "standard_annihilate" - , standard_annihilate_function_value - , ( bp::arg("perturbed_is_annihilated")=(bool)(true) ) - , "Return a schedule that can be used for a standard double-annihilation\n free energy perturbation. If `perturbed_is_annihilated` is true, then\n the perturbed state is annihilated, otherwise the reference state is\n annihilated.\n" ); - + + typedef ::SireCAS::LambdaSchedule (*standard_annihilate_function_type)(bool); + standard_annihilate_function_type standard_annihilate_function_value(&::SireCAS::LambdaSchedule::standard_annihilate); + + LambdaSchedule_exposer.def( + "standard_annihilate", standard_annihilate_function_value, (bp::arg("perturbed_is_annihilated") = (bool)(true)), "Return a schedule that can be used for a standard double-annihilation\n free energy perturbation. If `perturbed_is_annihilated` is true, then\n the perturbed state is annihilated, otherwise the reference state is\n annihilated.\n"); } { //::SireCAS::LambdaSchedule::standard_decouple - - typedef ::SireCAS::LambdaSchedule ( *standard_decouple_function_type )( bool ); - standard_decouple_function_type standard_decouple_function_value( &::SireCAS::LambdaSchedule::standard_decouple ); - - LambdaSchedule_exposer.def( - "standard_decouple" - , standard_decouple_function_value - , ( bp::arg("perturbed_is_decoupled")=(bool)(true) ) - , "Return a schedule that can be used for a standard double-decoupling\n free energy perturbation. If `perturbed_is_decoupled` is true, then\n the perturbed state is decoupled, otherwise the reference state is\n decoupled.\n" ); - + + typedef ::SireCAS::LambdaSchedule (*standard_decouple_function_type)(bool); + standard_decouple_function_type standard_decouple_function_value(&::SireCAS::LambdaSchedule::standard_decouple); + + LambdaSchedule_exposer.def( + "standard_decouple", standard_decouple_function_value, (bp::arg("perturbed_is_decoupled") = (bool)(true)), "Return a schedule that can be used for a standard double-decoupling\n free energy perturbation. If `perturbed_is_decoupled` is true, then\n the perturbed state is decoupled, otherwise the reference state is\n decoupled.\n"); } { //::SireCAS::LambdaSchedule::standard_morph - - typedef ::SireCAS::LambdaSchedule ( *standard_morph_function_type )( ); - standard_morph_function_type standard_morph_function_value( &::SireCAS::LambdaSchedule::standard_morph ); - - LambdaSchedule_exposer.def( - "standard_morph" - , standard_morph_function_value - , bp::release_gil_policy() - , "Return a LambdaSchedule that represents a standard morph,\n where every forcefield parameter is scaled by\n (1-:lambda:).initial + :lambda:.final\n" ); - + + typedef ::SireCAS::LambdaSchedule (*standard_morph_function_type)(); + standard_morph_function_type standard_morph_function_value(&::SireCAS::LambdaSchedule::standard_morph); + + LambdaSchedule_exposer.def( + "standard_morph", standard_morph_function_value, bp::release_gil_policy(), "Return a LambdaSchedule that represents a standard morph,\n where every forcefield parameter is scaled by\n (1-:lambda:).initial + :lambda:.final\n"); } { //::SireCAS::LambdaSchedule::takeMoleculeSchedule - - typedef ::SireCAS::LambdaSchedule ( ::SireCAS::LambdaSchedule::*takeMoleculeSchedule_function_type)( int ) ; - takeMoleculeSchedule_function_type takeMoleculeSchedule_function_value( &::SireCAS::LambdaSchedule::takeMoleculeSchedule ); - - LambdaSchedule_exposer.def( - "takeMoleculeSchedule" - , takeMoleculeSchedule_function_value - , ( bp::arg("pert_mol_id") ) - , bp::release_gil_policy() - , "Remove the perturbable molecule-specific schedule associated\n with the perturbable molecule (or part of molecule) that is\n identified by the passed pert_mol_id. This returns the\n schedule that was removed. If no such schedule exists, then\n a copy of this schedule is returned.\n" ); - + + typedef ::SireCAS::LambdaSchedule (::SireCAS::LambdaSchedule::*takeMoleculeSchedule_function_type)(int); + takeMoleculeSchedule_function_type takeMoleculeSchedule_function_value(&::SireCAS::LambdaSchedule::takeMoleculeSchedule); + + LambdaSchedule_exposer.def( + "takeMoleculeSchedule", takeMoleculeSchedule_function_value, (bp::arg("pert_mol_id")), bp::release_gil_policy(), "Remove the perturbable molecule-specific schedule associated\n with the perturbable molecule (or part of molecule) that is\n identified by the passed pert_mol_id. This returns the\n schedule that was removed. If no such schedule exists, then\n a copy of this schedule is returned.\n"); } { //::SireCAS::LambdaSchedule::toString - - typedef ::QString ( ::SireCAS::LambdaSchedule::*toString_function_type)( ) const; - toString_function_type toString_function_value( &::SireCAS::LambdaSchedule::toString ); - - LambdaSchedule_exposer.def( - "toString" - , toString_function_value - , bp::release_gil_policy() - , "" ); - + + typedef ::QString (::SireCAS::LambdaSchedule::*toString_function_type)() const; + toString_function_type toString_function_value(&::SireCAS::LambdaSchedule::toString); + + LambdaSchedule_exposer.def( + "toString", toString_function_value, bp::release_gil_policy(), ""); } { //::SireCAS::LambdaSchedule::typeName - - typedef char const * ( *typeName_function_type )( ); - typeName_function_type typeName_function_value( &::SireCAS::LambdaSchedule::typeName ); - - LambdaSchedule_exposer.def( - "typeName" - , typeName_function_value - , bp::release_gil_policy() - , "" ); - + + typedef char const *(*typeName_function_type)(); + typeName_function_type typeName_function_value(&::SireCAS::LambdaSchedule::typeName); + + LambdaSchedule_exposer.def( + "typeName", typeName_function_value, bp::release_gil_policy(), ""); } { //::SireCAS::LambdaSchedule::what - - typedef char const * ( ::SireCAS::LambdaSchedule::*what_function_type)( ) const; - what_function_type what_function_value( &::SireCAS::LambdaSchedule::what ); - - LambdaSchedule_exposer.def( - "what" - , what_function_value - , bp::release_gil_policy() - , "" ); - - } - LambdaSchedule_exposer.staticmethod( "charge_scaled_annihilate" ); - LambdaSchedule_exposer.staticmethod( "charge_scaled_decouple" ); - LambdaSchedule_exposer.staticmethod( "charge_scaled_morph" ); - LambdaSchedule_exposer.staticmethod( "final" ); - LambdaSchedule_exposer.staticmethod( "initial" ); - LambdaSchedule_exposer.staticmethod( "lam" ); - LambdaSchedule_exposer.staticmethod( "standard_annihilate" ); - LambdaSchedule_exposer.staticmethod( "standard_decouple" ); - LambdaSchedule_exposer.staticmethod( "standard_morph" ); - LambdaSchedule_exposer.staticmethod( "typeName" ); - LambdaSchedule_exposer.def( "__copy__", &__copy__); - LambdaSchedule_exposer.def( "__deepcopy__", &__copy__); - LambdaSchedule_exposer.def( "clone", &__copy__); - LambdaSchedule_exposer.def( "__rlshift__", &__rlshift__QDataStream< ::SireCAS::LambdaSchedule >, - bp::return_internal_reference<1, bp::with_custodian_and_ward<1,2> >() ); - LambdaSchedule_exposer.def( "__rrshift__", &__rrshift__QDataStream< ::SireCAS::LambdaSchedule >, - bp::return_internal_reference<1, bp::with_custodian_and_ward<1,2> >() ); - LambdaSchedule_exposer.def_pickle(sire_pickle_suite< ::SireCAS::LambdaSchedule >()); - LambdaSchedule_exposer.def( "__str__", &__str__< ::SireCAS::LambdaSchedule > ); - LambdaSchedule_exposer.def( "__repr__", &__str__< ::SireCAS::LambdaSchedule > ); - } + typedef char const *(::SireCAS::LambdaSchedule::*what_function_type)() const; + what_function_type what_function_value(&::SireCAS::LambdaSchedule::what); + + LambdaSchedule_exposer.def( + "what", what_function_value, bp::release_gil_policy(), ""); + } + LambdaSchedule_exposer.staticmethod("charge_scaled_annihilate"); + LambdaSchedule_exposer.staticmethod("charge_scaled_decouple"); + LambdaSchedule_exposer.staticmethod("charge_scaled_morph"); + LambdaSchedule_exposer.staticmethod("final"); + LambdaSchedule_exposer.staticmethod("initial"); + LambdaSchedule_exposer.staticmethod("lam"); + LambdaSchedule_exposer.staticmethod("standard_annihilate"); + LambdaSchedule_exposer.staticmethod("standard_decouple"); + LambdaSchedule_exposer.staticmethod("standard_morph"); + LambdaSchedule_exposer.staticmethod("typeName"); + LambdaSchedule_exposer.def("__copy__", &__copy__); + LambdaSchedule_exposer.def("__deepcopy__", &__copy__); + LambdaSchedule_exposer.def("clone", &__copy__); + LambdaSchedule_exposer.def("__rlshift__", &__rlshift__QDataStream<::SireCAS::LambdaSchedule>, + bp::return_internal_reference<1, bp::with_custodian_and_ward<1, 2>>()); + LambdaSchedule_exposer.def("__rrshift__", &__rrshift__QDataStream<::SireCAS::LambdaSchedule>, + bp::return_internal_reference<1, bp::with_custodian_and_ward<1, 2>>()); + LambdaSchedule_exposer.def_pickle(sire_pickle_suite<::SireCAS::LambdaSchedule>()); + LambdaSchedule_exposer.def("__str__", &__str__<::SireCAS::LambdaSchedule>); + LambdaSchedule_exposer.def("__repr__", &__str__<::SireCAS::LambdaSchedule>); + } } diff --git a/wrapper/CMakeLists.txt b/wrapper/CMakeLists.txt index f4508b3d3..89bc5b7d4 100644 --- a/wrapper/CMakeLists.txt +++ b/wrapper/CMakeLists.txt @@ -456,6 +456,7 @@ if ( CMAKE_CXX_COMPILER_ID MATCHES "MSVC" ) message( STATUS "Adding useful MSVC compiler options" ) add_compile_options("/EHsc") # exceptions are only thrown from C++ throw calls add_compile_options("/permissive-") # disable MSVC's permissive mode - ensures code is standards conforming + add_compile_options("/FI${CMAKE_CURRENT_SOURCE_DIR}/../corelib/build/cmake/stdext_compat.h") endif() if (NOT DEFINED ${SIRE_VERSION}) diff --git a/wrapper/Convert/SireOpenMM/CMakeLists.txt b/wrapper/Convert/SireOpenMM/CMakeLists.txt index b0df4b54d..60e89f928 100644 --- a/wrapper/Convert/SireOpenMM/CMakeLists.txt +++ b/wrapper/Convert/SireOpenMM/CMakeLists.txt @@ -30,6 +30,15 @@ if (${SIRE_USE_OPENMM}) # We're only building against OpenMM 8.1+, so include CustomCPPForce support. add_definitions("-DSIRE_USE_CUSTOMCPPFORCE") + # CustomVolumeForce was introduced in OpenMM 8.3. Guard its use so that + # older versions can still build (LJ dispersion correction will be unavailable). + if(EXISTS "${OpenMM_INCLUDE_DIR}/openmm/CustomVolumeForce.h") + message(STATUS "OpenMM CustomVolumeForce found - LJ dispersion correction support enabled") + add_definitions("-DSIRE_USE_CUSTOMVOLUMEFORCE") + else() + message(STATUS "OpenMM CustomVolumeForce not found (requires OpenMM >= 8.3) - LJ dispersion correction support disabled") + endif() + # Check to see if Torch support has been disabled. if (NOT DEFINED ENV{SIRE_NO_TORCH}) find_package(Torch) @@ -42,25 +51,6 @@ if (${SIRE_USE_OPENMM}) endif() endif() - # Check to see if we have support for updating some parameters in context - include(CheckCXXSourceCompiles) - check_cxx_source_compiles( "#include - int main() { - OpenMM::CustomNonbondedForce *force; - OpenMM::Context *context; - force->updateSomeParametersInContext(0, 0, *context); - return 0; - }" - SIREOPENMM_HAS_UPDATESOMEPARAMETERSINCONTEXT ) - - if ( ${SIREOPENMM_HAS_UPDATESOMEPARAMETERSINCONTEXT} ) - message( STATUS "OpenMM has support for updating some parameters in context") - add_definitions("-DSIRE_HAS_UPDATE_SOME_IN_CONTEXT") - else() - message( STATUS "OpenMM does not have support for updating some parameters in context") - message( STATUS "The free energy code will be a little slower.") - endif() - # Get the list of autogenerated files include(CMakeAutogenFile.txt) diff --git a/wrapper/Convert/SireOpenMM/LambdaLever.pypp.cpp b/wrapper/Convert/SireOpenMM/LambdaLever.pypp.cpp index 891398949..2e4d52185 100644 --- a/wrapper/Convert/SireOpenMM/LambdaLever.pypp.cpp +++ b/wrapper/Convert/SireOpenMM/LambdaLever.pypp.cpp @@ -217,17 +217,68 @@ void register_LambdaLever_class(){ } { //::SireOpenMM::LambdaLever::setForceIndex - + typedef void ( ::SireOpenMM::LambdaLever::*setForceIndex_function_type)( ::QString const &,int ) ; setForceIndex_function_type setForceIndex_function_value( &::SireOpenMM::LambdaLever::setForceIndex ); - - LambdaLever_exposer.def( + + LambdaLever_exposer.def( "setForceIndex" , setForceIndex_function_value , ( bp::arg("force"), bp::arg("index") ) , bp::release_gil_policy() , "Set the index of the force called force in the OpenMM System.\n There can only be one force with this name. Attempts to add\n a duplicate will cause an error to be raised.\n" ); - + + } + { //::SireOpenMM::LambdaLever::setForceGroup + + typedef void ( ::SireOpenMM::LambdaLever::*setForceGroup_function_type)( ::QString const &,int ) ; + setForceGroup_function_type setForceGroup_function_value( &::SireOpenMM::LambdaLever::setForceGroup ); + + LambdaLever_exposer.def( + "setForceGroup" + , setForceGroup_function_value + , ( bp::arg("name"), bp::arg("group_idx") ) + , bp::release_gil_policy() + , "Set the force group index for the named force." ); + + } + { //::SireOpenMM::LambdaLever::getForceGroup + + typedef int ( ::SireOpenMM::LambdaLever::*getForceGroup_function_type)( ::QString const & ) const; + getForceGroup_function_type getForceGroup_function_value( &::SireOpenMM::LambdaLever::getForceGroup ); + + LambdaLever_exposer.def( + "getForceGroup" + , getForceGroup_function_value + , ( bp::arg("name") ) + , bp::release_gil_policy() + , "Get the force group index for the named force. Returns -1 if not found." ); + + } + { //::SireOpenMM::LambdaLever::getForceNames + + typedef ::QStringList ( ::SireOpenMM::LambdaLever::*getForceNames_function_type)( ) const; + getForceNames_function_type getForceNames_function_value( &::SireOpenMM::LambdaLever::getForceNames ); + + LambdaLever_exposer.def( + "getForceNames" + , getForceNames_function_value + , bp::release_gil_policy() + , "Return the names of all forces and restraints that have been assigned a force group index." ); + + } + { //::SireOpenMM::LambdaLever::wasForceChanged + + typedef bool ( ::SireOpenMM::LambdaLever::*wasForceChanged_function_type)( ::QString const & ) const; + wasForceChanged_function_type wasForceChanged_function_value( &::SireOpenMM::LambdaLever::wasForceChanged ); + + LambdaLever_exposer.def( + "wasForceChanged" + , wasForceChanged_function_value + , ( bp::arg("name") ) + , bp::release_gil_policy() + , "Return whether the named force had parameters changed in the last setLambda call." ); + } { //::SireOpenMM::LambdaLever::setLambda diff --git a/wrapper/Convert/SireOpenMM/PerturbableOpenMMMolecule.pypp.cpp b/wrapper/Convert/SireOpenMM/PerturbableOpenMMMolecule.pypp.cpp index 95cd479f1..7622c75f3 100644 --- a/wrapper/Convert/SireOpenMM/PerturbableOpenMMMolecule.pypp.cpp +++ b/wrapper/Convert/SireOpenMM/PerturbableOpenMMMolecule.pypp.cpp @@ -584,9 +584,64 @@ void register_PerturbableOpenMMMolecule_class(){ , bp::release_gil_policy() , "Return the torsion phase parameters of the perturbed state" ); + } + { //::SireOpenMM::PerturbableOpenMMMolecule::getCMAPGrids0 + + typedef ::QVector< double > const & ( ::SireOpenMM::PerturbableOpenMMMolecule::*getCMAPGrids0_function_type)( ) const; + getCMAPGrids0_function_type getCMAPGrids0_function_value( &::SireOpenMM::PerturbableOpenMMMolecule::getCMAPGrids0 ); + + PerturbableOpenMMMolecule_exposer.def( + "getCMAPGrids0" + , getCMAPGrids0_function_value + , bp::return_value_policy< bp::copy_const_reference >() + , "Return the flat concatenated CMAP grid values (column-major, kJ/mol) " + "for the reference state. Grid k has getCMAPGridSizes()[k]^2 entries." ); + + } + { //::SireOpenMM::PerturbableOpenMMMolecule::getCMAPGrids1 + + typedef ::QVector< double > const & ( ::SireOpenMM::PerturbableOpenMMMolecule::*getCMAPGrids1_function_type)( ) const; + getCMAPGrids1_function_type getCMAPGrids1_function_value( &::SireOpenMM::PerturbableOpenMMMolecule::getCMAPGrids1 ); + + PerturbableOpenMMMolecule_exposer.def( + "getCMAPGrids1" + , getCMAPGrids1_function_value + , bp::return_value_policy< bp::copy_const_reference >() + , "Return the flat concatenated CMAP grid values (column-major, kJ/mol) " + "for the perturbed state. Grid k has getCMAPGridSizes()[k]^2 entries." ); + + } + { //::SireOpenMM::PerturbableOpenMMMolecule::getCMAPGridSizes + + typedef ::QVector< int > const & ( ::SireOpenMM::PerturbableOpenMMMolecule::*getCMAPGridSizes_function_type)( ) const; + getCMAPGridSizes_function_type getCMAPGridSizes_function_value( &::SireOpenMM::PerturbableOpenMMMolecule::getCMAPGridSizes ); + + PerturbableOpenMMMolecule_exposer.def( + "getCMAPGridSizes" + , getCMAPGridSizes_function_value + , bp::return_value_policy< bp::copy_const_reference >() + , "Return the grid dimension N for each CMAP torsion (grid is N x N). " + "Entries correspond to the grids in getCMAPGrids0/1." ); + + } + { //::SireOpenMM::PerturbableOpenMMMolecule::getCMAPAtoms + + typedef ::QVector< boost::tuples::tuple< int, int, int, int, int, + boost::tuples::null_type, boost::tuples::null_type, boost::tuples::null_type, + boost::tuples::null_type, boost::tuples::null_type > > + ( ::SireOpenMM::PerturbableOpenMMMolecule::*getCMAPAtoms_function_type)( ) const; + getCMAPAtoms_function_type getCMAPAtoms_function_value( &::SireOpenMM::PerturbableOpenMMMolecule::getCMAPAtoms ); + + PerturbableOpenMMMolecule_exposer.def( + "getCMAPAtoms" + , getCMAPAtoms_function_value + , bp::release_gil_policy() + , "Return the molecule-local 5-atom indices for each CMAP torsion, " + "in the same order as getCMAPGridSizes(). Used for REST2 scaling." ); + } { //::SireOpenMM::PerturbableOpenMMMolecule::isGhostAtom - + typedef bool ( ::SireOpenMM::PerturbableOpenMMMolecule::*isGhostAtom_function_type)( int ) const; isGhostAtom_function_type isGhostAtom_function_value( &::SireOpenMM::PerturbableOpenMMMolecule::isGhostAtom ); diff --git a/wrapper/Convert/SireOpenMM/PyQMCallback.pypp.cpp b/wrapper/Convert/SireOpenMM/PyQMCallback.pypp.cpp index 885a15bda..f5fa83fd9 100644 --- a/wrapper/Convert/SireOpenMM/PyQMCallback.pypp.cpp +++ b/wrapper/Convert/SireOpenMM/PyQMCallback.pypp.cpp @@ -2,8 +2,8 @@ // (C) Christopher Woods, GPL >= 3 License -#include "boost/python.hpp" #include "PyQMCallback.pypp.hpp" +#include "boost/python.hpp" namespace bp = boost::python; @@ -51,69 +51,56 @@ namespace bp = boost::python; #include -SireOpenMM::PyQMCallback __copy__(const SireOpenMM::PyQMCallback &other){ return SireOpenMM::PyQMCallback(other); } +SireOpenMM::PyQMCallback __copy__(const SireOpenMM::PyQMCallback &other) { return SireOpenMM::PyQMCallback(other); } #include "Qt/qdatastream.hpp" -const char* pvt_get_name(const SireOpenMM::PyQMCallback&){ return "SireOpenMM::PyQMCallback";} +const char *pvt_get_name(const SireOpenMM::PyQMCallback &) { return "SireOpenMM::PyQMCallback"; } #include "Helpers/release_gil_policy.hpp" -void register_PyQMCallback_class(){ +void register_PyQMCallback_class() +{ { //::SireOpenMM::PyQMCallback - typedef bp::class_< SireOpenMM::PyQMCallback > PyQMCallback_exposer_t; - PyQMCallback_exposer_t PyQMCallback_exposer = PyQMCallback_exposer_t( "PyQMCallback", "A callback wrapper class to interface with external QM engines\nvia the CustomCPPForceImpl.", bp::init< >("Default constructor.") ); - bp::scope PyQMCallback_scope( PyQMCallback_exposer ); - PyQMCallback_exposer.def( bp::init< bp::api::object, bp::optional< QString > >(( bp::arg("arg0"), bp::arg("name")="" ), "Constructor\nPar:am py_object\nA Python object that contains the callback function.\n\nPar:am name\nThe name of a callback method that take the following arguments:\n- numbers_qm: A list of atomic numbers for the atoms in the ML region.\n- charges_mm: A list of the MM charges in mod electron charge.\n- xyz_qm: A list of positions for the atoms in the ML region in Angstrom.\n- xyz_mm: A list of positions for the atoms in the MM region in Angstrom.\n- cell: A list of cell vectors in Angstrom.\n- idx_mm: A list of indices for the MM atoms in the QM/MM region.\nThe callback should return a tuple containing:\n- The energy in kJmol.\n- A list of forces for the QM atoms in kJmolnm.\n- A list of forces for the MM atoms in kJmolnm.\nIf empty, then the object is assumed to be a callable.\n") ); + typedef bp::class_ PyQMCallback_exposer_t; + PyQMCallback_exposer_t PyQMCallback_exposer = PyQMCallback_exposer_t("PyQMCallback", "A callback wrapper class to interface with external QM engines\nvia the CustomCPPForceImpl.", bp::init<>("Default constructor.")); + bp::scope PyQMCallback_scope(PyQMCallback_exposer); + PyQMCallback_exposer.def(bp::init>((bp::arg("arg0"), bp::arg("name") = ""), "Constructor\nPar:am py_object\nA Python object that contains the callback function.\n\nPar:am name\nThe name of a callback method that take the following arguments:\n- numbers_qm: A list of atomic numbers for the atoms in the ML region.\n- charges_mm: A list of the MM charges in mod electron charge.\n- xyz_qm: A list of positions for the atoms in the ML region in Angstrom.\n- xyz_mm: A list of positions for the atoms in the MM region in Angstrom.\n- cell: A list of cell vectors in Angstrom.\n- idx_mm: A list of indices for the MM atoms in the QM/MM region.\nThe callback should return a tuple containing:\n- The energy in kJmol.\n- A list of forces for the QM atoms in kJmolnm.\n- A list of forces for the MM atoms in kJmolnm.\n- (Optional) The gradient of the energy w.r.t. the effective MM charges in kJmol/e, used for the chain-rule switching correction.\nIf empty, then the object is assumed to be a callable.\n")); { //::SireOpenMM::PyQMCallback::call - - typedef ::boost::tuples::tuple< double, QVector< QVector< double > >, QVector< QVector< double > >, boost::tuples::null_type, boost::tuples::null_type, boost::tuples::null_type, boost::tuples::null_type, boost::tuples::null_type, boost::tuples::null_type, boost::tuples::null_type > ( ::SireOpenMM::PyQMCallback::*call_function_type)( ::QVector< int >,::QVector< double >,::QVector< QVector< double > >,::QVector< QVector< double > >,::QVector >, ::QVector< int > ) const; - call_function_type call_function_value( &::SireOpenMM::PyQMCallback::call ); - - PyQMCallback_exposer.def( - "call" - , call_function_value - , ( bp::arg("numbers_qm"), bp::arg("charges_mm"), bp::arg("xyz_qm"), bp::arg("xyz_mm"), bp::arg("cell"), bp::arg("idx_mm") ) - , bp::release_gil_policy() - , "Call the callback function.\nPar:am numbers_qm\nA vector of atomic numbers for the atoms in the ML region.\n\nPar:am charges_mm\nA vector of the charges on the MM atoms in mod electron charge.\n\nPar:am xyz_qm\nA vector of positions for the atoms in the ML region in Angstrom.\n\nPar:am xyz_mm\nA vector of positions for the atoms in the MM region in Angstrom.\n\nPar:am cell A list of cell vectors in Angstrom.\n\nPar:am idx_mm A vector of indices for the MM atoms in the QM/MM region. Note that len(idx_mm) <= len(charges_mm) since it only contains the indices of true MM atoms, not link atoms or virtual charges.\n\nReturn:s\nA tuple containing:\n- The energy in kJmol.\n- A vector of forces for the QM atoms in kJmolnm.\n- A vector of forces for the MM atoms in kJmolnm.\n" ); - + + typedef ::boost::tuples::tuple>, QVector>, boost::tuples::null_type, boost::tuples::null_type, boost::tuples::null_type, boost::tuples::null_type, boost::tuples::null_type, boost::tuples::null_type, boost::tuples::null_type> (::SireOpenMM::PyQMCallback::*call_function_type)(::QVector, ::QVector, ::QVector>, ::QVector>, ::QVector>, ::QVector) const; + call_function_type call_function_value(&::SireOpenMM::PyQMCallback::call); + + PyQMCallback_exposer.def( + "call", call_function_value, (bp::arg("numbers_qm"), bp::arg("charges_mm"), bp::arg("xyz_qm"), bp::arg("xyz_mm"), bp::arg("cell"), bp::arg("idx_mm")), bp::release_gil_policy(), "Call the callback function.\nPar:am numbers_qm\nA vector of atomic numbers for the atoms in the ML region.\n\nPar:am charges_mm\nA vector of the charges on the MM atoms in mod electron charge.\n\nPar:am xyz_qm\nA vector of positions for the atoms in the ML region in Angstrom.\n\nPar:am xyz_mm\nA vector of positions for the atoms in the MM region in Angstrom.\n\nPar:am cell A list of cell vectors in Angstrom.\n\nPar:am idx_mm A vector of indices for the MM atoms in the QM/MM region. Note that len(idx_mm) <= len(charges_mm) since it only contains the indices of true MM atoms, not link atoms or virtual charges.\n\nReturn:s\nA tuple containing:\n- The energy in kJmol.\n- A vector of forces for the QM atoms in kJmolnm.\n- A vector of forces for the MM atoms in kJmolnm.\n- (Optional) The gradient of the energy w.r.t. the effective MM charges in kJmol/e, used for the chain-rule switching correction.\n"); } { //::SireOpenMM::PyQMCallback::typeName - - typedef char const * ( *typeName_function_type )( ); - typeName_function_type typeName_function_value( &::SireOpenMM::PyQMCallback::typeName ); - - PyQMCallback_exposer.def( - "typeName" - , typeName_function_value - , bp::release_gil_policy() - , "Return the C++ name for this class." ); - + + typedef char const *(*typeName_function_type)(); + typeName_function_type typeName_function_value(&::SireOpenMM::PyQMCallback::typeName); + + PyQMCallback_exposer.def( + "typeName", typeName_function_value, bp::release_gil_policy(), "Return the C++ name for this class."); } { //::SireOpenMM::PyQMCallback::what - - typedef char const * ( ::SireOpenMM::PyQMCallback::*what_function_type)( ) const; - what_function_type what_function_value( &::SireOpenMM::PyQMCallback::what ); - - PyQMCallback_exposer.def( - "what" - , what_function_value - , bp::release_gil_policy() - , "Return the C++ name for this class." ); - + + typedef char const *(::SireOpenMM::PyQMCallback::*what_function_type)() const; + what_function_type what_function_value(&::SireOpenMM::PyQMCallback::what); + + PyQMCallback_exposer.def( + "what", what_function_value, bp::release_gil_policy(), "Return the C++ name for this class."); } - PyQMCallback_exposer.staticmethod( "typeName" ); - PyQMCallback_exposer.def( "__copy__", &__copy__); - PyQMCallback_exposer.def( "__deepcopy__", &__copy__); - PyQMCallback_exposer.def( "clone", &__copy__); - PyQMCallback_exposer.def( "__rlshift__", &__rlshift__QDataStream< ::SireOpenMM::PyQMCallback >, - bp::return_internal_reference<1, bp::with_custodian_and_ward<1,2> >() ); - PyQMCallback_exposer.def( "__rrshift__", &__rrshift__QDataStream< ::SireOpenMM::PyQMCallback >, - bp::return_internal_reference<1, bp::with_custodian_and_ward<1,2> >() ); - PyQMCallback_exposer.def_pickle(sire_pickle_suite< ::SireOpenMM::PyQMCallback >()); - PyQMCallback_exposer.def( "__str__", &pvt_get_name); - PyQMCallback_exposer.def( "__repr__", &pvt_get_name); + PyQMCallback_exposer.staticmethod("typeName"); + PyQMCallback_exposer.def("__copy__", &__copy__); + PyQMCallback_exposer.def("__deepcopy__", &__copy__); + PyQMCallback_exposer.def("clone", &__copy__); + PyQMCallback_exposer.def("__rlshift__", &__rlshift__QDataStream<::SireOpenMM::PyQMCallback>, + bp::return_internal_reference<1, bp::with_custodian_and_ward<1, 2>>()); + PyQMCallback_exposer.def("__rrshift__", &__rrshift__QDataStream<::SireOpenMM::PyQMCallback>, + bp::return_internal_reference<1, bp::with_custodian_and_ward<1, 2>>()); + PyQMCallback_exposer.def_pickle(sire_pickle_suite<::SireOpenMM::PyQMCallback>()); + PyQMCallback_exposer.def("__str__", &pvt_get_name); + PyQMCallback_exposer.def("__repr__", &pvt_get_name); } - } diff --git a/wrapper/Convert/SireOpenMM/PyQMEngine.pypp.cpp b/wrapper/Convert/SireOpenMM/PyQMEngine.pypp.cpp index ee197ff51..37308fa66 100644 --- a/wrapper/Convert/SireOpenMM/PyQMEngine.pypp.cpp +++ b/wrapper/Convert/SireOpenMM/PyQMEngine.pypp.cpp @@ -2,8 +2,8 @@ // (C) Christopher Woods, GPL >= 3 License -#include "boost/python.hpp" #include "PyQMEngine.pypp.hpp" +#include "boost/python.hpp" namespace bp = boost::python; @@ -51,313 +51,242 @@ namespace bp = boost::python; #include -SireOpenMM::PyQMEngine __copy__(const SireOpenMM::PyQMEngine &other){ return SireOpenMM::PyQMEngine(other); } +SireOpenMM::PyQMEngine __copy__(const SireOpenMM::PyQMEngine &other) { return SireOpenMM::PyQMEngine(other); } #include "Helpers/str.hpp" #include "Helpers/release_gil_policy.hpp" -void register_PyQMEngine_class(){ +void register_PyQMEngine_class() +{ { //::SireOpenMM::PyQMEngine - typedef bp::class_< SireOpenMM::PyQMEngine, bp::bases< SireBase::Property, SireOpenMM::QMEngine > > PyQMEngine_exposer_t; - PyQMEngine_exposer_t PyQMEngine_exposer = PyQMEngine_exposer_t( "PyQMEngine", "", bp::init< >("Default constructor.") ); - bp::scope PyQMEngine_scope( PyQMEngine_exposer ); - PyQMEngine_exposer.def( bp::init< bp::api::object, bp::optional< QString, SireUnits::Dimension::Length, int, bool, double > >(( bp::arg("arg0"), bp::arg("method")="", bp::arg("cutoff")=7.5 * SireUnits::angstrom, bp::arg("neighbour_list_frequency")=(int)(0), bp::arg("is_mechanical")=(bool)(false), bp::arg("lambda")=1. ), "Constructor\nPar:am py_object\nA Python object.\n\nPar:am name\nThe name of the callback method. If empty, then the object is\nassumed to be a callable.\n\nPar:am cutoff\nThe ML cutoff distance.\n\nPar:am neighbour_list_frequency\nThe frequency at which the neighbour list is updated. (Number of steps.)\nIf zero, then no neighbour list is used.\n\nPar:am is_mechanical\nA flag to indicate if mechanical embedding is being used.\n\nPar:am lambda\nThe lambda weighting factor. This can be used to interpolate between\npotentials for end-state correction calculations.\n") ); - PyQMEngine_exposer.def( bp::init< SireOpenMM::PyQMEngine const & >(( bp::arg("other") ), "Copy constructor.") ); + typedef bp::class_> PyQMEngine_exposer_t; + PyQMEngine_exposer_t PyQMEngine_exposer = PyQMEngine_exposer_t("PyQMEngine", "", bp::init<>("Default constructor.")); + bp::scope PyQMEngine_scope(PyQMEngine_exposer); + PyQMEngine_exposer.def(bp::init>((bp::arg("arg0"), bp::arg("method") = "", bp::arg("cutoff") = 7.5 * SireUnits::angstrom, bp::arg("neighbour_list_frequency") = (int)(0), bp::arg("is_mechanical") = (bool)(false), bp::arg("lambda") = 1.), "Constructor\nPar:am py_object\nA Python object.\n\nPar:am name\nThe name of the callback method. If empty, then the object is\nassumed to be a callable.\n\nPar:am cutoff\nThe ML cutoff distance.\n\nPar:am neighbour_list_frequency\nThe frequency at which the neighbour list is updated. (Number of steps.)\nIf zero, then no neighbour list is used.\n\nPar:am is_mechanical\nA flag to indicate if mechanical embedding is being used.\n\nPar:am lambda\nThe lambda weighting factor. This can be used to interpolate between\npotentials for end-state correction calculations.\n")); + PyQMEngine_exposer.def(bp::init((bp::arg("other")), "Copy constructor.")); { //::SireOpenMM::PyQMEngine::call - - typedef ::boost::tuples::tuple< double, QVector< QVector< double > >, QVector< QVector< double > >, boost::tuples::null_type, boost::tuples::null_type, boost::tuples::null_type, boost::tuples::null_type, boost::tuples::null_type, boost::tuples::null_type, boost::tuples::null_type > ( ::SireOpenMM::PyQMEngine::*call_function_type)( ::QVector< int >,::QVector< double >,::QVector< QVector< double > >,::QVector< QVector< double > >,::QVector< QVector< double > >,::QVector < int > ) const; - call_function_type call_function_value( &::SireOpenMM::PyQMEngine::call ); - - PyQMEngine_exposer.def( - "call" - , call_function_value - , ( bp::arg("numbers_qm"), bp::arg("charges_mm"), bp::arg("xyz_qm"), bp::arg("xyz_mm"), bp::arg("cell"), bp::arg("idx_mm") ) - , bp::release_gil_policy() - , "Call the callback function.\nPar:am numbers_qm\nA vector of atomic numbers for the atoms in the ML region.\n\nPar:am charges_mm\nA vector of the charges on the MM atoms in mod electron charge.\n\nPar:am xyz_qm\nA vector of positions for the atoms in the ML region in Angstrom.\n\nPar:am xyz_mm\nA vector of positions for the atoms in the MM region in Angstrom.\n\nPar:am cell A list of cell vectors in Angstrom.\n\nPar:am idx_mm A vector of indices for the MM atoms in the QM/MM region. Note that len(idx_mm) <= len(charges_mm) since it only contains the indices of the true MM atoms, not link atoms or virtual charges.\n\nReturn:s\nA tuple containing:\n- The energy in kJmol.\n- A vector of forces for the QM atoms in kJmolnm.\n- A vector of forces for the MM atoms in kJmolnm.\n" ); - + + typedef ::boost::tuples::tuple>, QVector>, boost::tuples::null_type, boost::tuples::null_type, boost::tuples::null_type, boost::tuples::null_type, boost::tuples::null_type, boost::tuples::null_type, boost::tuples::null_type> (::SireOpenMM::PyQMEngine::*call_function_type)(::QVector, ::QVector, ::QVector>, ::QVector>, ::QVector>, ::QVector) const; + call_function_type call_function_value(&::SireOpenMM::PyQMEngine::call); + + PyQMEngine_exposer.def( + "call", call_function_value, (bp::arg("numbers_qm"), bp::arg("charges_mm"), bp::arg("xyz_qm"), bp::arg("xyz_mm"), bp::arg("cell"), bp::arg("idx_mm")), bp::release_gil_policy(), "Call the callback function.\nPar:am numbers_qm\nA vector of atomic numbers for the atoms in the ML region.\n\nPar:am charges_mm\nA vector of the charges on the MM atoms in mod electron charge.\n\nPar:am xyz_qm\nA vector of positions for the atoms in the ML region in Angstrom.\n\nPar:am xyz_mm\nA vector of positions for the atoms in the MM region in Angstrom.\n\nPar:am cell A list of cell vectors in Angstrom.\n\nPar:am idx_mm A vector of indices for the MM atoms in the QM/MM region. Note that len(idx_mm) <= len(charges_mm) since it only contains the indices of the true MM atoms, not link atoms or virtual charges.\n\nReturn:s\nA tuple containing:\n- The energy in kJmol.\n- A vector of forces for the QM atoms in kJmolnm.\n- A vector of forces for the MM atoms in kJmolnm.\n- (Optional) The gradient of the energy w.r.t. the effective MM charges in kJmol/e, used for the chain-rule switching correction.\n"); } { //::SireOpenMM::PyQMEngine::getAtoms - - typedef ::QVector< int > ( ::SireOpenMM::PyQMEngine::*getAtoms_function_type)( ) const; - getAtoms_function_type getAtoms_function_value( &::SireOpenMM::PyQMEngine::getAtoms ); - - PyQMEngine_exposer.def( - "getAtoms" - , getAtoms_function_value - , bp::release_gil_policy() - , "Get the indices of the atoms in the QM region.\nReturn:s\nA vector of atom indices for the QM region.\n" ); - + + typedef ::QVector (::SireOpenMM::PyQMEngine::*getAtoms_function_type)() const; + getAtoms_function_type getAtoms_function_value(&::SireOpenMM::PyQMEngine::getAtoms); + + PyQMEngine_exposer.def( + "getAtoms", getAtoms_function_value, bp::release_gil_policy(), "Get the indices of the atoms in the QM region.\nReturn:s\nA vector of atom indices for the QM region.\n"); } { //::SireOpenMM::PyQMEngine::getCallback - - typedef ::SireOpenMM::PyQMCallback ( ::SireOpenMM::PyQMEngine::*getCallback_function_type)( ) const; - getCallback_function_type getCallback_function_value( &::SireOpenMM::PyQMEngine::getCallback ); - - PyQMEngine_exposer.def( - "getCallback" - , getCallback_function_value - , bp::release_gil_policy() - , "Get the callback object.\nReturn:s\nA Python object that contains the callback function.\n" ); - + + typedef ::SireOpenMM::PyQMCallback (::SireOpenMM::PyQMEngine::*getCallback_function_type)() const; + getCallback_function_type getCallback_function_value(&::SireOpenMM::PyQMEngine::getCallback); + + PyQMEngine_exposer.def( + "getCallback", getCallback_function_value, bp::release_gil_policy(), "Get the callback object.\nReturn:s\nA Python object that contains the callback function.\n"); } { //::SireOpenMM::PyQMEngine::getCharges - - typedef ::QVector< double > ( ::SireOpenMM::PyQMEngine::*getCharges_function_type)( ) const; - getCharges_function_type getCharges_function_value( &::SireOpenMM::PyQMEngine::getCharges ); - - PyQMEngine_exposer.def( - "getCharges" - , getCharges_function_value - , bp::release_gil_policy() - , "Get the atomic charges of all atoms in the system.\nReturn:s\nA vector of atomic charges for all atoms in the system.\n" ); - + + typedef ::QVector (::SireOpenMM::PyQMEngine::*getCharges_function_type)() const; + getCharges_function_type getCharges_function_value(&::SireOpenMM::PyQMEngine::getCharges); + + PyQMEngine_exposer.def( + "getCharges", getCharges_function_value, bp::release_gil_policy(), "Get the atomic charges of all atoms in the system.\nReturn:s\nA vector of atomic charges for all atoms in the system.\n"); } { //::SireOpenMM::PyQMEngine::getCutoff - - typedef ::SireUnits::Dimension::Length ( ::SireOpenMM::PyQMEngine::*getCutoff_function_type)( ) const; - getCutoff_function_type getCutoff_function_value( &::SireOpenMM::PyQMEngine::getCutoff ); - - PyQMEngine_exposer.def( - "getCutoff" - , getCutoff_function_value - , bp::release_gil_policy() - , "Get the QM cutoff distance.\nReturn:s\nThe QM cutoff distance.\n" ); - + + typedef ::SireUnits::Dimension::Length (::SireOpenMM::PyQMEngine::*getCutoff_function_type)() const; + getCutoff_function_type getCutoff_function_value(&::SireOpenMM::PyQMEngine::getCutoff); + + PyQMEngine_exposer.def( + "getCutoff", getCutoff_function_value, bp::release_gil_policy(), "Get the QM cutoff distance.\nReturn:s\nThe QM cutoff distance.\n"); } { //::SireOpenMM::PyQMEngine::getIsMechanical - - typedef bool ( ::SireOpenMM::PyQMEngine::*getIsMechanical_function_type)( ) const; - getIsMechanical_function_type getIsMechanical_function_value( &::SireOpenMM::PyQMEngine::getIsMechanical ); - - PyQMEngine_exposer.def( - "getIsMechanical" - , getIsMechanical_function_value - , bp::release_gil_policy() - , "Get the mechanical embedding flag.\nReturn:s\nA flag to indicate if mechanical embedding is being used.\n" ); - + + typedef bool (::SireOpenMM::PyQMEngine::*getIsMechanical_function_type)() const; + getIsMechanical_function_type getIsMechanical_function_value(&::SireOpenMM::PyQMEngine::getIsMechanical); + + PyQMEngine_exposer.def( + "getIsMechanical", getIsMechanical_function_value, bp::release_gil_policy(), "Get the mechanical embedding flag.\nReturn:s\nA flag to indicate if mechanical embedding is being used.\n"); } { //::SireOpenMM::PyQMEngine::getLambda - - typedef double ( ::SireOpenMM::PyQMEngine::*getLambda_function_type)( ) const; - getLambda_function_type getLambda_function_value( &::SireOpenMM::PyQMEngine::getLambda ); - - PyQMEngine_exposer.def( - "getLambda" - , getLambda_function_value - , bp::release_gil_policy() - , "Get the lambda weighting factor.\nReturn:s\nThe lambda weighting factor.\n" ); - + + typedef double (::SireOpenMM::PyQMEngine::*getLambda_function_type)() const; + getLambda_function_type getLambda_function_value(&::SireOpenMM::PyQMEngine::getLambda); + + PyQMEngine_exposer.def( + "getLambda", getLambda_function_value, bp::release_gil_policy(), "Get the lambda weighting factor.\nReturn:s\nThe lambda weighting factor.\n"); } { //::SireOpenMM::PyQMEngine::getLinkAtoms - - typedef ::boost::tuples::tuple< QMap< int, int >, QMap< int, QVector< int > >, QMap< int, double >, boost::tuples::null_type, boost::tuples::null_type, boost::tuples::null_type, boost::tuples::null_type, boost::tuples::null_type, boost::tuples::null_type, boost::tuples::null_type > ( ::SireOpenMM::PyQMEngine::*getLinkAtoms_function_type)( ) const; - getLinkAtoms_function_type getLinkAtoms_function_value( &::SireOpenMM::PyQMEngine::getLinkAtoms ); - - PyQMEngine_exposer.def( - "getLinkAtoms" - , getLinkAtoms_function_value - , bp::release_gil_policy() - , "Get the link atoms associated with each QM atom.\nReturn:s\nA tuple containing:\n\nmm1_to_qm\nA dictionary mapping link atom (MM1) indices to the QM atoms to\nwhich they are bonded.\n\nmm1_to_mm2\nA dictionary of link atoms indices (MM1) to a list of the MM\natoms to which they are bonded (MM2).\n\nbond_scale_factors\nA dictionary of link atom indices (MM1) to a list of the bond\nlength scale factors between the QM and MM1 atoms. The scale\nfactors are the ratio of the equilibrium bond lengths for the\nQM-L (QM-link) atom and QM-MM1 atom, i.e. R0(QM-L) R0(QM-MM1),\ntaken from the MM force field parameters for the molecule.\n\n" ); - + + typedef ::boost::tuples::tuple, QMap>, QMap, boost::tuples::null_type, boost::tuples::null_type, boost::tuples::null_type, boost::tuples::null_type, boost::tuples::null_type, boost::tuples::null_type, boost::tuples::null_type> (::SireOpenMM::PyQMEngine::*getLinkAtoms_function_type)() const; + getLinkAtoms_function_type getLinkAtoms_function_value(&::SireOpenMM::PyQMEngine::getLinkAtoms); + + PyQMEngine_exposer.def( + "getLinkAtoms", getLinkAtoms_function_value, bp::release_gil_policy(), "Get the link atoms associated with each QM atom.\nReturn:s\nA tuple containing:\n\nmm1_to_qm\nA dictionary mapping link atom (MM1) indices to the QM atoms to\nwhich they are bonded.\n\nmm1_to_mm2\nA dictionary of link atoms indices (MM1) to a list of the MM\natoms to which they are bonded (MM2).\n\nbond_scale_factors\nA dictionary of link atom indices (MM1) to a list of the bond\nlength scale factors between the QM and MM1 atoms. The scale\nfactors are the ratio of the equilibrium bond lengths for the\nQM-L (QM-link) atom and QM-MM1 atom, i.e. R0(QM-L) R0(QM-MM1),\ntaken from the MM force field parameters for the molecule.\n\n"); } { //::SireOpenMM::PyQMEngine::getMM2Atoms - - typedef ::QVector< int > ( ::SireOpenMM::PyQMEngine::*getMM2Atoms_function_type)( ) const; - getMM2Atoms_function_type getMM2Atoms_function_value( &::SireOpenMM::PyQMEngine::getMM2Atoms ); - - PyQMEngine_exposer.def( - "getMM2Atoms" - , getMM2Atoms_function_value - , bp::release_gil_policy() - , "Get the vector of MM2 atoms.\nReturn:s\nA vector of MM2 atom indices.\n" ); - + + typedef ::QVector (::SireOpenMM::PyQMEngine::*getMM2Atoms_function_type)() const; + getMM2Atoms_function_type getMM2Atoms_function_value(&::SireOpenMM::PyQMEngine::getMM2Atoms); + + PyQMEngine_exposer.def( + "getMM2Atoms", getMM2Atoms_function_value, bp::release_gil_policy(), "Get the vector of MM2 atoms.\nReturn:s\nA vector of MM2 atom indices.\n"); } { //::SireOpenMM::PyQMEngine::getNeighbourListFrequency - - typedef int ( ::SireOpenMM::PyQMEngine::*getNeighbourListFrequency_function_type)( ) const; - getNeighbourListFrequency_function_type getNeighbourListFrequency_function_value( &::SireOpenMM::PyQMEngine::getNeighbourListFrequency ); - - PyQMEngine_exposer.def( - "getNeighbourListFrequency" - , getNeighbourListFrequency_function_value - , bp::release_gil_policy() - , "Get the neighbour list frequency.\nReturn:s\nThe neighbour list frequency.\n" ); - + + typedef int (::SireOpenMM::PyQMEngine::*getNeighbourListFrequency_function_type)() const; + getNeighbourListFrequency_function_type getNeighbourListFrequency_function_value(&::SireOpenMM::PyQMEngine::getNeighbourListFrequency); + + PyQMEngine_exposer.def( + "getNeighbourListFrequency", getNeighbourListFrequency_function_value, bp::release_gil_policy(), "Get the neighbour list frequency.\nReturn:s\nThe neighbour list frequency.\n"); } { //::SireOpenMM::PyQMEngine::getNumbers - - typedef ::QVector< int > ( ::SireOpenMM::PyQMEngine::*getNumbers_function_type)( ) const; - getNumbers_function_type getNumbers_function_value( &::SireOpenMM::PyQMEngine::getNumbers ); - - PyQMEngine_exposer.def( - "getNumbers" - , getNumbers_function_value - , bp::release_gil_policy() - , "Get the atomic numbers for the atoms in the QM region.\nReturn:s\nA vector of atomic numbers for the atoms in the QM region.\n" ); - + + typedef ::QVector (::SireOpenMM::PyQMEngine::*getNumbers_function_type)() const; + getNumbers_function_type getNumbers_function_value(&::SireOpenMM::PyQMEngine::getNumbers); + + PyQMEngine_exposer.def( + "getNumbers", getNumbers_function_value, bp::release_gil_policy(), "Get the atomic numbers for the atoms in the QM region.\nReturn:s\nA vector of atomic numbers for the atoms in the QM region.\n"); } { //::SireOpenMM::PyQMEngine::operator= - - typedef ::SireOpenMM::PyQMEngine & ( ::SireOpenMM::PyQMEngine::*assign_function_type)( ::SireOpenMM::PyQMEngine const & ) ; - assign_function_type assign_function_value( &::SireOpenMM::PyQMEngine::operator= ); - - PyQMEngine_exposer.def( - "assign" - , assign_function_value - , ( bp::arg("other") ) - , bp::return_self< >() - , "Assignment operator." ); - + + typedef ::SireOpenMM::PyQMEngine &(::SireOpenMM::PyQMEngine::*assign_function_type)(::SireOpenMM::PyQMEngine const &); + assign_function_type assign_function_value(&::SireOpenMM::PyQMEngine::operator=); + + PyQMEngine_exposer.def( + "assign", assign_function_value, (bp::arg("other")), bp::return_self<>(), "Assignment operator."); } { //::SireOpenMM::PyQMEngine::setAtoms - - typedef void ( ::SireOpenMM::PyQMEngine::*setAtoms_function_type)( ::QVector< int > ) ; - setAtoms_function_type setAtoms_function_value( &::SireOpenMM::PyQMEngine::setAtoms ); - - PyQMEngine_exposer.def( - "setAtoms" - , setAtoms_function_value - , ( bp::arg("atoms") ) - , bp::release_gil_policy() - , "Set the list of atom indices for the QM region.\nPar:am atoms\nA vector of atom indices for the QM region.\n" ); - + + typedef void (::SireOpenMM::PyQMEngine::*setAtoms_function_type)(::QVector); + setAtoms_function_type setAtoms_function_value(&::SireOpenMM::PyQMEngine::setAtoms); + + PyQMEngine_exposer.def( + "setAtoms", setAtoms_function_value, (bp::arg("atoms")), bp::release_gil_policy(), "Set the list of atom indices for the QM region.\nPar:am atoms\nA vector of atom indices for the QM region.\n"); } { //::SireOpenMM::PyQMEngine::setCallback - - typedef void ( ::SireOpenMM::PyQMEngine::*setCallback_function_type)( ::SireOpenMM::PyQMCallback ) ; - setCallback_function_type setCallback_function_value( &::SireOpenMM::PyQMEngine::setCallback ); - - PyQMEngine_exposer.def( - "setCallback" - , setCallback_function_value - , ( bp::arg("callback") ) - , bp::release_gil_policy() - , "Set the callback object.\nPar:am callback\nA Python object that contains the callback function.\n" ); - + + typedef void (::SireOpenMM::PyQMEngine::*setCallback_function_type)(::SireOpenMM::PyQMCallback); + setCallback_function_type setCallback_function_value(&::SireOpenMM::PyQMEngine::setCallback); + + PyQMEngine_exposer.def( + "setCallback", setCallback_function_value, (bp::arg("callback")), bp::release_gil_policy(), "Set the callback object.\nPar:am callback\nA Python object that contains the callback function.\n"); } { //::SireOpenMM::PyQMEngine::setCharges - - typedef void ( ::SireOpenMM::PyQMEngine::*setCharges_function_type)( ::QVector< double > ) ; - setCharges_function_type setCharges_function_value( &::SireOpenMM::PyQMEngine::setCharges ); - - PyQMEngine_exposer.def( - "setCharges" - , setCharges_function_value - , ( bp::arg("charges") ) - , bp::release_gil_policy() - , "Set the atomic charges of all atoms in the system.\nPar:am charges\nA vector of atomic charges for all atoms in the system.\n" ); - + + typedef void (::SireOpenMM::PyQMEngine::*setCharges_function_type)(::QVector); + setCharges_function_type setCharges_function_value(&::SireOpenMM::PyQMEngine::setCharges); + + PyQMEngine_exposer.def( + "setCharges", setCharges_function_value, (bp::arg("charges")), bp::release_gil_policy(), "Set the atomic charges of all atoms in the system.\nPar:am charges\nA vector of atomic charges for all atoms in the system.\n"); } { //::SireOpenMM::PyQMEngine::setCutoff - - typedef void ( ::SireOpenMM::PyQMEngine::*setCutoff_function_type)( ::SireUnits::Dimension::Length ) ; - setCutoff_function_type setCutoff_function_value( &::SireOpenMM::PyQMEngine::setCutoff ); - - PyQMEngine_exposer.def( - "setCutoff" - , setCutoff_function_value - , ( bp::arg("cutoff") ) - , bp::release_gil_policy() - , "Set the QM cutoff distance.\nPar:am cutoff\nThe QM cutoff distance.\n" ); - + + typedef void (::SireOpenMM::PyQMEngine::*setCutoff_function_type)(::SireUnits::Dimension::Length); + setCutoff_function_type setCutoff_function_value(&::SireOpenMM::PyQMEngine::setCutoff); + + PyQMEngine_exposer.def( + "setCutoff", setCutoff_function_value, (bp::arg("cutoff")), bp::release_gil_policy(), "Set the QM cutoff distance.\nPar:am cutoff\nThe QM cutoff distance.\n"); } { //::SireOpenMM::PyQMEngine::setIsMechanical - - typedef void ( ::SireOpenMM::PyQMEngine::*setIsMechanical_function_type)( bool ) ; - setIsMechanical_function_type setIsMechanical_function_value( &::SireOpenMM::PyQMEngine::setIsMechanical ); - - PyQMEngine_exposer.def( - "setIsMechanical" - , setIsMechanical_function_value - , ( bp::arg("is_mechanical") ) - , bp::release_gil_policy() - , "Set the mechanical embedding flag.\nPar:am is_mechanical\nA flag to indicate if mechanical embedding is being used.\n" ); - + + typedef void (::SireOpenMM::PyQMEngine::*setIsMechanical_function_type)(bool); + setIsMechanical_function_type setIsMechanical_function_value(&::SireOpenMM::PyQMEngine::setIsMechanical); + + PyQMEngine_exposer.def( + "setIsMechanical", setIsMechanical_function_value, (bp::arg("is_mechanical")), bp::release_gil_policy(), "Set the mechanical embedding flag.\nPar:am is_mechanical\nA flag to indicate if mechanical embedding is being used.\n"); } { //::SireOpenMM::PyQMEngine::setLambda - - typedef void ( ::SireOpenMM::PyQMEngine::*setLambda_function_type)( double ) ; - setLambda_function_type setLambda_function_value( &::SireOpenMM::PyQMEngine::setLambda ); - - PyQMEngine_exposer.def( - "setLambda" - , setLambda_function_value - , ( bp::arg("lambda") ) - , bp::release_gil_policy() - , "Set the lambda weighting factor.\nPar:am lambda\nThe lambda weighting factor.\n" ); - + + typedef void (::SireOpenMM::PyQMEngine::*setLambda_function_type)(double); + setLambda_function_type setLambda_function_value(&::SireOpenMM::PyQMEngine::setLambda); + + PyQMEngine_exposer.def( + "setLambda", setLambda_function_value, (bp::arg("lambda")), bp::release_gil_policy(), "Set the lambda weighting factor.\nPar:am lambda\nThe lambda weighting factor.\n"); } { //::SireOpenMM::PyQMEngine::setLinkAtoms - - typedef void ( ::SireOpenMM::PyQMEngine::*setLinkAtoms_function_type)( ::QMap< int, int >,::QMap< int, QVector< int > >,::QMap< int, double > ) ; - setLinkAtoms_function_type setLinkAtoms_function_value( &::SireOpenMM::PyQMEngine::setLinkAtoms ); - - PyQMEngine_exposer.def( - "setLinkAtoms" - , setLinkAtoms_function_value - , ( bp::arg("mm1_to_qm"), bp::arg("mm1_to_mm2"), bp::arg("bond_scale_factors") ) - , bp::release_gil_policy() - , "Set the link atoms associated with each QM atom.\nPar:am mm1_to_qm\nA dictionary mapping link atom (MM1) indices to the QM atoms to\nwhich they are bonded.\n\nPar:am mm1_to_mm2\nA dictionary of link atoms indices (MM1) to a list of the MM\natoms to which they are bonded (MM2).\n\nPar:am bond_scale_factors\nA dictionary of link atom indices (MM1) to a list of the bond\nlength scale factors between the QM and MM1 atoms. The scale\nfactors are the ratio of the equilibrium bond lengths for the\nQM-L (QM-link) atom and QM-MM1 atom, i.e. R0(QM-L) R0(QM-MM1),\ntaken from the MM force field parameters for the molecule.\n\n" ); - + + typedef void (::SireOpenMM::PyQMEngine::*setLinkAtoms_function_type)(::QMap, ::QMap>, ::QMap); + setLinkAtoms_function_type setLinkAtoms_function_value(&::SireOpenMM::PyQMEngine::setLinkAtoms); + + PyQMEngine_exposer.def( + "setLinkAtoms", setLinkAtoms_function_value, (bp::arg("mm1_to_qm"), bp::arg("mm1_to_mm2"), bp::arg("bond_scale_factors")), bp::release_gil_policy(), "Set the link atoms associated with each QM atom.\nPar:am mm1_to_qm\nA dictionary mapping link atom (MM1) indices to the QM atoms to\nwhich they are bonded.\n\nPar:am mm1_to_mm2\nA dictionary of link atoms indices (MM1) to a list of the MM\natoms to which they are bonded (MM2).\n\nPar:am bond_scale_factors\nA dictionary of link atom indices (MM1) to a list of the bond\nlength scale factors between the QM and MM1 atoms. The scale\nfactors are the ratio of the equilibrium bond lengths for the\nQM-L (QM-link) atom and QM-MM1 atom, i.e. R0(QM-L) R0(QM-MM1),\ntaken from the MM force field parameters for the molecule.\n\n"); } { //::SireOpenMM::PyQMEngine::setNeighbourListFrequency - - typedef void ( ::SireOpenMM::PyQMEngine::*setNeighbourListFrequency_function_type)( int ) ; - setNeighbourListFrequency_function_type setNeighbourListFrequency_function_value( &::SireOpenMM::PyQMEngine::setNeighbourListFrequency ); - - PyQMEngine_exposer.def( - "setNeighbourListFrequency" - , setNeighbourListFrequency_function_value - , ( bp::arg("neighbour_list_frequency") ) - , bp::release_gil_policy() - , "Set the neighbour list frequency.\nPar:am neighbour_list_frequency\nThe neighbour list frequency.\n" ); - + + typedef void (::SireOpenMM::PyQMEngine::*setNeighbourListFrequency_function_type)(int); + setNeighbourListFrequency_function_type setNeighbourListFrequency_function_value(&::SireOpenMM::PyQMEngine::setNeighbourListFrequency); + + PyQMEngine_exposer.def( + "setNeighbourListFrequency", setNeighbourListFrequency_function_value, (bp::arg("neighbour_list_frequency")), bp::release_gil_policy(), "Set the neighbour list frequency.\nPar:am neighbour_list_frequency\nThe neighbour list frequency.\n"); } { //::SireOpenMM::PyQMEngine::setNumbers - - typedef void ( ::SireOpenMM::PyQMEngine::*setNumbers_function_type)( ::QVector< int > ) ; - setNumbers_function_type setNumbers_function_value( &::SireOpenMM::PyQMEngine::setNumbers ); - - PyQMEngine_exposer.def( - "setNumbers" - , setNumbers_function_value - , ( bp::arg("numbers") ) - , bp::release_gil_policy() - , "Set the atomic numbers for the atoms in the QM region.\nPar:am numbers\nA vector of atomic numbers for the atoms in the QM region.\n" ); - + + typedef void (::SireOpenMM::PyQMEngine::*setNumbers_function_type)(::QVector); + setNumbers_function_type setNumbers_function_value(&::SireOpenMM::PyQMEngine::setNumbers); + + PyQMEngine_exposer.def( + "setNumbers", setNumbers_function_value, (bp::arg("numbers")), bp::release_gil_policy(), "Set the atomic numbers for the atoms in the QM region.\nPar:am numbers\nA vector of atomic numbers for the atoms in the QM region.\n"); + } + { //::SireOpenMM::PyQMEngine::getSwitchWidth + + typedef double (::SireOpenMM::PyQMEngine::*getSwitchWidth_function_type)() const; + getSwitchWidth_function_type getSwitchWidth_function_value(&::SireOpenMM::PyQMEngine::getSwitchWidth); + + PyQMEngine_exposer.def( + "getSwitchWidth", getSwitchWidth_function_value, bp::release_gil_policy(), "Get the switch width as a fraction of the cutoff.\n"); + } + { //::SireOpenMM::PyQMEngine::setSwitchWidth + + typedef void (::SireOpenMM::PyQMEngine::*setSwitchWidth_function_type)(double); + setSwitchWidth_function_type setSwitchWidth_function_value(&::SireOpenMM::PyQMEngine::setSwitchWidth); + + PyQMEngine_exposer.def( + "setSwitchWidth", setSwitchWidth_function_value, (bp::arg("switch_width")), bp::release_gil_policy(), "Set the switch width as a fraction of the cutoff (0 to 1).\n"); + } + { //::SireOpenMM::PyQMEngine::getUseSwitch + + typedef bool (::SireOpenMM::PyQMEngine::*getUseSwitch_function_type)() const; + getUseSwitch_function_type getUseSwitch_function_value(&::SireOpenMM::PyQMEngine::getUseSwitch); + + PyQMEngine_exposer.def( + "getUseSwitch", getUseSwitch_function_value, bp::release_gil_policy(), "Get whether a switching function is used.\n"); + } + { //::SireOpenMM::PyQMEngine::setUseSwitch + + typedef void (::SireOpenMM::PyQMEngine::*setUseSwitch_function_type)(bool); + setUseSwitch_function_type setUseSwitch_function_value(&::SireOpenMM::PyQMEngine::setUseSwitch); + + PyQMEngine_exposer.def( + "setUseSwitch", setUseSwitch_function_value, (bp::arg("use_switch")), bp::release_gil_policy(), "Set whether to use a switching function.\n"); } { //::SireOpenMM::PyQMEngine::typeName - - typedef char const * ( *typeName_function_type )( ); - typeName_function_type typeName_function_value( &::SireOpenMM::PyQMEngine::typeName ); - - PyQMEngine_exposer.def( - "typeName" - , typeName_function_value - , bp::release_gil_policy() - , "Return the C++ name for this class." ); - + + typedef char const *(*typeName_function_type)(); + typeName_function_type typeName_function_value(&::SireOpenMM::PyQMEngine::typeName); + + PyQMEngine_exposer.def( + "typeName", typeName_function_value, bp::release_gil_policy(), "Return the C++ name for this class."); } { //::SireOpenMM::PyQMEngine::what - - typedef char const * ( ::SireOpenMM::PyQMEngine::*what_function_type)( ) const; - what_function_type what_function_value( &::SireOpenMM::PyQMEngine::what ); - - PyQMEngine_exposer.def( - "what" - , what_function_value - , bp::release_gil_policy() - , "Return the C++ name for this class." ); - + + typedef char const *(::SireOpenMM::PyQMEngine::*what_function_type)() const; + what_function_type what_function_value(&::SireOpenMM::PyQMEngine::what); + + PyQMEngine_exposer.def( + "what", what_function_value, bp::release_gil_policy(), "Return the C++ name for this class."); } - PyQMEngine_exposer.staticmethod( "typeName" ); - PyQMEngine_exposer.def( "__copy__", &__copy__); - PyQMEngine_exposer.def( "__deepcopy__", &__copy__); - PyQMEngine_exposer.def( "clone", &__copy__); - PyQMEngine_exposer.def( "__str__", &__str__< ::SireOpenMM::PyQMEngine > ); - PyQMEngine_exposer.def( "__repr__", &__str__< ::SireOpenMM::PyQMEngine > ); + PyQMEngine_exposer.staticmethod("typeName"); + PyQMEngine_exposer.def("__copy__", &__copy__); + PyQMEngine_exposer.def("__deepcopy__", &__copy__); + PyQMEngine_exposer.def("clone", &__copy__); + PyQMEngine_exposer.def("__str__", &__str__<::SireOpenMM::PyQMEngine>); + PyQMEngine_exposer.def("__repr__", &__str__<::SireOpenMM::PyQMEngine>); } - } diff --git a/wrapper/Convert/SireOpenMM/PyQMForce.pypp.cpp b/wrapper/Convert/SireOpenMM/PyQMForce.pypp.cpp index b8b8bebd3..31a6c82d2 100644 --- a/wrapper/Convert/SireOpenMM/PyQMForce.pypp.cpp +++ b/wrapper/Convert/SireOpenMM/PyQMForce.pypp.cpp @@ -2,8 +2,8 @@ // (C) Christopher Woods, GPL >= 3 License -#include "boost/python.hpp" #include "PyQMForce.pypp.hpp" +#include "boost/python.hpp" namespace bp = boost::python; @@ -51,229 +51,161 @@ namespace bp = boost::python; #include -SireOpenMM::PyQMForce __copy__(const SireOpenMM::PyQMForce &other){ return SireOpenMM::PyQMForce(other); } +SireOpenMM::PyQMForce __copy__(const SireOpenMM::PyQMForce &other) { return SireOpenMM::PyQMForce(other); } #include "Qt/qdatastream.hpp" -const char* pvt_get_name(const SireOpenMM::PyQMForce&){ return "SireOpenMM::PyQMForce";} +const char *pvt_get_name(const SireOpenMM::PyQMForce &) { return "SireOpenMM::PyQMForce"; } #include "Helpers/release_gil_policy.hpp" -void register_PyQMForce_class(){ +void register_PyQMForce_class() +{ { //::SireOpenMM::PyQMForce - typedef bp::class_< SireOpenMM::PyQMForce, bp::bases< SireOpenMM::QMForce > > PyQMForce_exposer_t; - PyQMForce_exposer_t PyQMForce_exposer = PyQMForce_exposer_t( "PyQMForce", "", bp::init< >("Default constructor.") ); - bp::scope PyQMForce_scope( PyQMForce_exposer ); - PyQMForce_exposer.def( bp::init< SireOpenMM::PyQMCallback, SireUnits::Dimension::Length, int, bool, double, QVector< int >, QMap< int, int >, QMap< int, QVector< int > >, QMap< int, double >, QVector< int >, QVector< int >, QVector< double > >(( bp::arg("callback"), bp::arg("cutoff"), bp::arg("neighbour_list_frequency"), bp::arg("is_mechanical"), bp::arg("lambda"), bp::arg("atoms"), bp::arg("mm1_to_qm"), bp::arg("mm1_to_mm2"), bp::arg("bond_scale_factors"), bp::arg("mm2_atoms"), bp::arg("numbers"), bp::arg("charges") ), "Constructor.\nPar:am callback\nThe PyQMCallback object.\n\nPar:am cutoff\nThe ML cutoff distance.\n\nPar:am neighbour_list_frequency\nThe frequency at which the neighbour list is updated. (Number of steps.)\nIf zero, then no neighbour list is used.\n\nPar:am is_mechanical\nA flag to indicate if mechanical embedding is being used.\n\nPar:am lambda\nThe lambda weighting factor. This can be used to interpolate between\npotentials for end-state correction calculations.\n\nPar:am atoms\nA vector of atom indices for the QM region.\n\nPar:am mm1_to_qm\nA dictionary mapping link atom (MM1) indices to the QM atoms to\nwhich they are bonded.\n\nPar:am mm1_to_mm2\nA dictionary of link atoms indices (MM1) to a list of the MM\natoms to which they are bonded (MM2).\n\nPar:am bond_scale_factors\nA dictionary of link atom indices (MM1) to a list of the bond\nlength scale factors between the QM and MM1 atoms. The scale\nfactors are the ratio of the equilibrium bond lengths for the\nQM-L (QM-link) atom and QM-MM1 atom, i.e. R0(QM-L) R0(QM-MM1),\ntaken from the MM force field parameters for the molecule.\n\nPar:am mm2_atoms\nA vector of MM2 atom indices.\n\nPar:am numbers\nA vector of atomic charges for all atoms in the system.\n\nPar:am charges\nA vector of atomic charges for all atoms in the system.\n") ); - PyQMForce_exposer.def( bp::init< SireOpenMM::PyQMForce const & >(( bp::arg("other") ), "Copy constructor.") ); + typedef bp::class_> PyQMForce_exposer_t; + PyQMForce_exposer_t PyQMForce_exposer = PyQMForce_exposer_t("PyQMForce", "", bp::init<>("Default constructor.")); + bp::scope PyQMForce_scope(PyQMForce_exposer); + PyQMForce_exposer.def(bp::init, QMap, QMap>, QMap, QVector, QVector, QVector>((bp::arg("callback"), bp::arg("cutoff"), bp::arg("neighbour_list_frequency"), bp::arg("is_mechanical"), bp::arg("lambda"), bp::arg("atoms"), bp::arg("mm1_to_qm"), bp::arg("mm1_to_mm2"), bp::arg("bond_scale_factors"), bp::arg("mm2_atoms"), bp::arg("numbers"), bp::arg("charges")), "Constructor.\nPar:am callback\nThe PyQMCallback object.\n\nPar:am cutoff\nThe ML cutoff distance.\n\nPar:am neighbour_list_frequency\nThe frequency at which the neighbour list is updated. (Number of steps.)\nIf zero, then no neighbour list is used.\n\nPar:am is_mechanical\nA flag to indicate if mechanical embedding is being used.\n\nPar:am lambda\nThe lambda weighting factor. This can be used to interpolate between\npotentials for end-state correction calculations.\n\nPar:am atoms\nA vector of atom indices for the QM region.\n\nPar:am mm1_to_qm\nA dictionary mapping link atom (MM1) indices to the QM atoms to\nwhich they are bonded.\n\nPar:am mm1_to_mm2\nA dictionary of link atoms indices (MM1) to a list of the MM\natoms to which they are bonded (MM2).\n\nPar:am bond_scale_factors\nA dictionary of link atom indices (MM1) to a list of the bond\nlength scale factors between the QM and MM1 atoms. The scale\nfactors are the ratio of the equilibrium bond lengths for the\nQM-L (QM-link) atom and QM-MM1 atom, i.e. R0(QM-L) R0(QM-MM1),\ntaken from the MM force field parameters for the molecule.\n\nPar:am mm2_atoms\nA vector of MM2 atom indices.\n\nPar:am numbers\nA vector of atomic charges for all atoms in the system.\n\nPar:am charges\nA vector of atomic charges for all atoms in the system.\n")); + PyQMForce_exposer.def(bp::init((bp::arg("other")), "Copy constructor.")); { //::SireOpenMM::PyQMForce::call - - typedef ::boost::tuples::tuple< double, QVector< QVector< double > >, QVector< QVector< double > >, boost::tuples::null_type, boost::tuples::null_type, boost::tuples::null_type, boost::tuples::null_type, boost::tuples::null_type, boost::tuples::null_type, boost::tuples::null_type > ( ::SireOpenMM::PyQMForce::*call_function_type)( ::QVector< int >,::QVector< double >,::QVector< QVector< double > >,::QVector< QVector< double > >, ::QVector< QVector< double > >, ::QVector < int > ) const; - call_function_type call_function_value( &::SireOpenMM::PyQMForce::call ); - - PyQMForce_exposer.def( - "call" - , call_function_value - , ( bp::arg("numbers_qm"), bp::arg("charges_mm"), bp::arg("xyz_qm"), bp::arg("xyz_mm"), bp::arg("cell"), bp::arg("idx_mm") ) - , bp::release_gil_policy() - , "Call the callback function.\nPar:am numbers_qm\nA vector of atomic numbers for the atoms in the ML region.\n\nPar:am charges_mm\nA vector of the charges on the MM atoms in mod electron charge.\n\nPar:am xyz_qm\nA vector of positions for the atoms in the ML region in Angstrom.\n\nPar:am xyz_mm\nA vector of positions for the atoms in the MM region in Angstrom.\n\nPar:am cell A list of cell vectors in Angstrom.\n\nPar:am idx_mm A vector of indices for the MM atoms in the QM/MM region. Note that len(idx_mm) <= len(charges_mm) since it only contains the indices of true MM atoms, not link atoms or virtual charges.\n\nReturn:s\nA tuple containing:\n- The energy in kJmol.\n- A vector of forces for the QM atoms in kJmolnm.\n- A vector of forces for the MM atoms in kJmolnm.\n" ); - + + typedef ::boost::tuples::tuple>, QVector>, boost::tuples::null_type, boost::tuples::null_type, boost::tuples::null_type, boost::tuples::null_type, boost::tuples::null_type, boost::tuples::null_type, boost::tuples::null_type> (::SireOpenMM::PyQMForce::*call_function_type)(::QVector, ::QVector, ::QVector>, ::QVector>, ::QVector>, ::QVector) const; + call_function_type call_function_value(&::SireOpenMM::PyQMForce::call); + + PyQMForce_exposer.def( + "call", call_function_value, (bp::arg("numbers_qm"), bp::arg("charges_mm"), bp::arg("xyz_qm"), bp::arg("xyz_mm"), bp::arg("cell"), bp::arg("idx_mm")), bp::release_gil_policy(), "Call the callback function.\nPar:am numbers_qm\nA vector of atomic numbers for the atoms in the ML region.\n\nPar:am charges_mm\nA vector of the charges on the MM atoms in mod electron charge.\n\nPar:am xyz_qm\nA vector of positions for the atoms in the ML region in Angstrom.\n\nPar:am xyz_mm\nA vector of positions for the atoms in the MM region in Angstrom.\n\nPar:am cell A list of cell vectors in Angstrom.\n\nPar:am idx_mm A vector of indices for the MM atoms in the QM/MM region. Note that len(idx_mm) <= len(charges_mm) since it only contains the indices of true MM atoms, not link atoms or virtual charges.\n\nReturn:s\nA tuple containing:\n- The energy in kJmol.\n- A vector of forces for the QM atoms in kJmolnm.\n- A vector of forces for the MM atoms in kJmolnm.\n- (Optional) The gradient of the energy w.r.t. the effective MM charges in kJmol/e, used for the chain-rule switching correction.\n"); } { //::SireOpenMM::PyQMForce::getAtoms - - typedef ::QVector< int > ( ::SireOpenMM::PyQMForce::*getAtoms_function_type)( ) const; - getAtoms_function_type getAtoms_function_value( &::SireOpenMM::PyQMForce::getAtoms ); - - PyQMForce_exposer.def( - "getAtoms" - , getAtoms_function_value - , bp::release_gil_policy() - , "Get the indices of the atoms in the QM region.\nReturn:s\nA vector of atom indices for the QM region.\n" ); - + + typedef ::QVector (::SireOpenMM::PyQMForce::*getAtoms_function_type)() const; + getAtoms_function_type getAtoms_function_value(&::SireOpenMM::PyQMForce::getAtoms); + + PyQMForce_exposer.def( + "getAtoms", getAtoms_function_value, bp::release_gil_policy(), "Get the indices of the atoms in the QM region.\nReturn:s\nA vector of atom indices for the QM region.\n"); } { //::SireOpenMM::PyQMForce::getCallback - - typedef ::SireOpenMM::PyQMCallback ( ::SireOpenMM::PyQMForce::*getCallback_function_type)( ) const; - getCallback_function_type getCallback_function_value( &::SireOpenMM::PyQMForce::getCallback ); - - PyQMForce_exposer.def( - "getCallback" - , getCallback_function_value - , bp::release_gil_policy() - , "Get the callback object.\nReturn:s\nA Python object that contains the callback function.\n" ); - + + typedef ::SireOpenMM::PyQMCallback (::SireOpenMM::PyQMForce::*getCallback_function_type)() const; + getCallback_function_type getCallback_function_value(&::SireOpenMM::PyQMForce::getCallback); + + PyQMForce_exposer.def( + "getCallback", getCallback_function_value, bp::release_gil_policy(), "Get the callback object.\nReturn:s\nA Python object that contains the callback function.\n"); } { //::SireOpenMM::PyQMForce::getCharges - - typedef ::QVector< double > ( ::SireOpenMM::PyQMForce::*getCharges_function_type)( ) const; - getCharges_function_type getCharges_function_value( &::SireOpenMM::PyQMForce::getCharges ); - - PyQMForce_exposer.def( - "getCharges" - , getCharges_function_value - , bp::release_gil_policy() - , "Get the atomic charges of all atoms in the system.\nReturn:s\nA vector of atomic charges for all atoms in the system.\n" ); - + + typedef ::QVector (::SireOpenMM::PyQMForce::*getCharges_function_type)() const; + getCharges_function_type getCharges_function_value(&::SireOpenMM::PyQMForce::getCharges); + + PyQMForce_exposer.def( + "getCharges", getCharges_function_value, bp::release_gil_policy(), "Get the atomic charges of all atoms in the system.\nReturn:s\nA vector of atomic charges for all atoms in the system.\n"); } { //::SireOpenMM::PyQMForce::getCutoff - - typedef ::SireUnits::Dimension::Length ( ::SireOpenMM::PyQMForce::*getCutoff_function_type)( ) const; - getCutoff_function_type getCutoff_function_value( &::SireOpenMM::PyQMForce::getCutoff ); - - PyQMForce_exposer.def( - "getCutoff" - , getCutoff_function_value - , bp::release_gil_policy() - , "Get the QM cutoff distance.\nReturn:s\nThe QM cutoff distance.\n" ); - + + typedef ::SireUnits::Dimension::Length (::SireOpenMM::PyQMForce::*getCutoff_function_type)() const; + getCutoff_function_type getCutoff_function_value(&::SireOpenMM::PyQMForce::getCutoff); + + PyQMForce_exposer.def( + "getCutoff", getCutoff_function_value, bp::release_gil_policy(), "Get the QM cutoff distance.\nReturn:s\nThe QM cutoff distance.\n"); } { //::SireOpenMM::PyQMForce::getIsMechanical - - typedef bool ( ::SireOpenMM::PyQMForce::*getIsMechanical_function_type)( ) const; - getIsMechanical_function_type getIsMechanical_function_value( &::SireOpenMM::PyQMForce::getIsMechanical ); - - PyQMForce_exposer.def( - "getIsMechanical" - , getIsMechanical_function_value - , bp::release_gil_policy() - , "Get the mechanical embedding flag.\nReturn:s\nA flag to indicate if mechanical embedding is being used.\n" ); - + + typedef bool (::SireOpenMM::PyQMForce::*getIsMechanical_function_type)() const; + getIsMechanical_function_type getIsMechanical_function_value(&::SireOpenMM::PyQMForce::getIsMechanical); + + PyQMForce_exposer.def( + "getIsMechanical", getIsMechanical_function_value, bp::release_gil_policy(), "Get the mechanical embedding flag.\nReturn:s\nA flag to indicate if mechanical embedding is being used.\n"); } { //::SireOpenMM::PyQMForce::getLambda - - typedef double ( ::SireOpenMM::PyQMForce::*getLambda_function_type)( ) const; - getLambda_function_type getLambda_function_value( &::SireOpenMM::PyQMForce::getLambda ); - - PyQMForce_exposer.def( - "getLambda" - , getLambda_function_value - , bp::release_gil_policy() - , "Get the lambda weighting factor.\nReturn:s\nThe lambda weighting factor.\n" ); - + + typedef double (::SireOpenMM::PyQMForce::*getLambda_function_type)() const; + getLambda_function_type getLambda_function_value(&::SireOpenMM::PyQMForce::getLambda); + + PyQMForce_exposer.def( + "getLambda", getLambda_function_value, bp::release_gil_policy(), "Get the lambda weighting factor.\nReturn:s\nThe lambda weighting factor.\n"); } { //::SireOpenMM::PyQMForce::getLinkAtoms - - typedef ::boost::tuples::tuple< QMap< int, int >, QMap< int, QVector< int > >, QMap< int, double >, boost::tuples::null_type, boost::tuples::null_type, boost::tuples::null_type, boost::tuples::null_type, boost::tuples::null_type, boost::tuples::null_type, boost::tuples::null_type > ( ::SireOpenMM::PyQMForce::*getLinkAtoms_function_type)( ) const; - getLinkAtoms_function_type getLinkAtoms_function_value( &::SireOpenMM::PyQMForce::getLinkAtoms ); - - PyQMForce_exposer.def( - "getLinkAtoms" - , getLinkAtoms_function_value - , bp::release_gil_policy() - , "Get the link atoms associated with each QM atom.\nReturn:s\nA tuple containing:\n\nmm1_to_qm\nA dictionary mapping link atom (MM1) indices to the QM atoms to\nwhich they are bonded.\n\nmm1_to_mm2\nA dictionary of link atoms indices (MM1) to a list of the MM\natoms to which they are bonded (MM2).\n\nbond_scale_factors\nA dictionary of link atom indices (MM1) to a list of the bond\nlength scale factors between the QM and MM1 atoms. The scale\nfactors are the ratio of the equilibrium bond lengths for the\nQM-L (QM-link) atom and QM-MM1 atom, i.e. R0(QM-L) R0(QM-MM1),\ntaken from the MM force field parameters for the molecule.\n\n" ); - + + typedef ::boost::tuples::tuple, QMap>, QMap, boost::tuples::null_type, boost::tuples::null_type, boost::tuples::null_type, boost::tuples::null_type, boost::tuples::null_type, boost::tuples::null_type, boost::tuples::null_type> (::SireOpenMM::PyQMForce::*getLinkAtoms_function_type)() const; + getLinkAtoms_function_type getLinkAtoms_function_value(&::SireOpenMM::PyQMForce::getLinkAtoms); + + PyQMForce_exposer.def( + "getLinkAtoms", getLinkAtoms_function_value, bp::release_gil_policy(), "Get the link atoms associated with each QM atom.\nReturn:s\nA tuple containing:\n\nmm1_to_qm\nA dictionary mapping link atom (MM1) indices to the QM atoms to\nwhich they are bonded.\n\nmm1_to_mm2\nA dictionary of link atoms indices (MM1) to a list of the MM\natoms to which they are bonded (MM2).\n\nbond_scale_factors\nA dictionary of link atom indices (MM1) to a list of the bond\nlength scale factors between the QM and MM1 atoms. The scale\nfactors are the ratio of the equilibrium bond lengths for the\nQM-L (QM-link) atom and QM-MM1 atom, i.e. R0(QM-L) R0(QM-MM1),\ntaken from the MM force field parameters for the molecule.\n\n"); } { //::SireOpenMM::PyQMForce::getMM2Atoms - - typedef ::QVector< int > ( ::SireOpenMM::PyQMForce::*getMM2Atoms_function_type)( ) const; - getMM2Atoms_function_type getMM2Atoms_function_value( &::SireOpenMM::PyQMForce::getMM2Atoms ); - - PyQMForce_exposer.def( - "getMM2Atoms" - , getMM2Atoms_function_value - , bp::release_gil_policy() - , "Get the vector of MM2 atoms.\nReturn:s\nA vector of MM2 atom indices.\n" ); - + + typedef ::QVector (::SireOpenMM::PyQMForce::*getMM2Atoms_function_type)() const; + getMM2Atoms_function_type getMM2Atoms_function_value(&::SireOpenMM::PyQMForce::getMM2Atoms); + + PyQMForce_exposer.def( + "getMM2Atoms", getMM2Atoms_function_value, bp::release_gil_policy(), "Get the vector of MM2 atoms.\nReturn:s\nA vector of MM2 atom indices.\n"); } { //::SireOpenMM::PyQMForce::getNeighbourListFrequency - - typedef int ( ::SireOpenMM::PyQMForce::*getNeighbourListFrequency_function_type)( ) const; - getNeighbourListFrequency_function_type getNeighbourListFrequency_function_value( &::SireOpenMM::PyQMForce::getNeighbourListFrequency ); - - PyQMForce_exposer.def( - "getNeighbourListFrequency" - , getNeighbourListFrequency_function_value - , bp::release_gil_policy() - , "Get the neighbour list frequency.\nReturn:s\nThe neighbour list frequency.\n" ); - + + typedef int (::SireOpenMM::PyQMForce::*getNeighbourListFrequency_function_type)() const; + getNeighbourListFrequency_function_type getNeighbourListFrequency_function_value(&::SireOpenMM::PyQMForce::getNeighbourListFrequency); + + PyQMForce_exposer.def( + "getNeighbourListFrequency", getNeighbourListFrequency_function_value, bp::release_gil_policy(), "Get the neighbour list frequency.\nReturn:s\nThe neighbour list frequency.\n"); } { //::SireOpenMM::PyQMForce::getNumbers - - typedef ::QVector< int > ( ::SireOpenMM::PyQMForce::*getNumbers_function_type)( ) const; - getNumbers_function_type getNumbers_function_value( &::SireOpenMM::PyQMForce::getNumbers ); - - PyQMForce_exposer.def( - "getNumbers" - , getNumbers_function_value - , bp::release_gil_policy() - , "Get the atomic numbers for the atoms in the QM region.\nReturn:s\nA vector of atomic numbers for the atoms in the QM region.\n" ); - + + typedef ::QVector (::SireOpenMM::PyQMForce::*getNumbers_function_type)() const; + getNumbers_function_type getNumbers_function_value(&::SireOpenMM::PyQMForce::getNumbers); + + PyQMForce_exposer.def( + "getNumbers", getNumbers_function_value, bp::release_gil_policy(), "Get the atomic numbers for the atoms in the QM region.\nReturn:s\nA vector of atomic numbers for the atoms in the QM region.\n"); } { //::SireOpenMM::PyQMForce::operator= - - typedef ::SireOpenMM::PyQMForce & ( ::SireOpenMM::PyQMForce::*assign_function_type)( ::SireOpenMM::PyQMForce const & ) ; - assign_function_type assign_function_value( &::SireOpenMM::PyQMForce::operator= ); - - PyQMForce_exposer.def( - "assign" - , assign_function_value - , ( bp::arg("other") ) - , bp::return_self< >() - , "Assignment operator." ); - + + typedef ::SireOpenMM::PyQMForce &(::SireOpenMM::PyQMForce::*assign_function_type)(::SireOpenMM::PyQMForce const &); + assign_function_type assign_function_value(&::SireOpenMM::PyQMForce::operator=); + + PyQMForce_exposer.def( + "assign", assign_function_value, (bp::arg("other")), bp::return_self<>(), "Assignment operator."); } { //::SireOpenMM::PyQMForce::setCallback - - typedef void ( ::SireOpenMM::PyQMForce::*setCallback_function_type)( ::SireOpenMM::PyQMCallback ) ; - setCallback_function_type setCallback_function_value( &::SireOpenMM::PyQMForce::setCallback ); - - PyQMForce_exposer.def( - "setCallback" - , setCallback_function_value - , ( bp::arg("callback") ) - , bp::release_gil_policy() - , "Set the callback object.\nPar:am callback\nA Python object that contains the callback function.\n" ); - + + typedef void (::SireOpenMM::PyQMForce::*setCallback_function_type)(::SireOpenMM::PyQMCallback); + setCallback_function_type setCallback_function_value(&::SireOpenMM::PyQMForce::setCallback); + + PyQMForce_exposer.def( + "setCallback", setCallback_function_value, (bp::arg("callback")), bp::release_gil_policy(), "Set the callback object.\nPar:am callback\nA Python object that contains the callback function.\n"); } { //::SireOpenMM::PyQMForce::setLambda - - typedef void ( ::SireOpenMM::PyQMForce::*setLambda_function_type)( double ) ; - setLambda_function_type setLambda_function_value( &::SireOpenMM::PyQMForce::setLambda ); - - PyQMForce_exposer.def( - "setLambda" - , setLambda_function_value - , ( bp::arg("lambda") ) - , bp::release_gil_policy() - , "Set the lambda weighting factor\nPar:am lambda\nThe lambda weighting factor.\n" ); - + + typedef void (::SireOpenMM::PyQMForce::*setLambda_function_type)(double); + setLambda_function_type setLambda_function_value(&::SireOpenMM::PyQMForce::setLambda); + + PyQMForce_exposer.def( + "setLambda", setLambda_function_value, (bp::arg("lambda")), bp::release_gil_policy(), "Set the lambda weighting factor\nPar:am lambda\nThe lambda weighting factor.\n"); } { //::SireOpenMM::PyQMForce::typeName - - typedef char const * ( *typeName_function_type )( ); - typeName_function_type typeName_function_value( &::SireOpenMM::PyQMForce::typeName ); - - PyQMForce_exposer.def( - "typeName" - , typeName_function_value - , bp::release_gil_policy() - , "Return the C++ name for this class." ); - + + typedef char const *(*typeName_function_type)(); + typeName_function_type typeName_function_value(&::SireOpenMM::PyQMForce::typeName); + + PyQMForce_exposer.def( + "typeName", typeName_function_value, bp::release_gil_policy(), "Return the C++ name for this class."); } { //::SireOpenMM::PyQMForce::what - - typedef char const * ( ::SireOpenMM::PyQMForce::*what_function_type)( ) const; - what_function_type what_function_value( &::SireOpenMM::PyQMForce::what ); - - PyQMForce_exposer.def( - "what" - , what_function_value - , bp::release_gil_policy() - , "Return the C++ name for this class." ); - - } - PyQMForce_exposer.staticmethod( "typeName" ); - PyQMForce_exposer.def( "__copy__", &__copy__); - PyQMForce_exposer.def( "__deepcopy__", &__copy__); - PyQMForce_exposer.def( "clone", &__copy__); - PyQMForce_exposer.def( "__rlshift__", &__rlshift__QDataStream< ::SireOpenMM::PyQMForce >, - bp::return_internal_reference<1, bp::with_custodian_and_ward<1,2> >() ); - PyQMForce_exposer.def( "__rrshift__", &__rrshift__QDataStream< ::SireOpenMM::PyQMForce >, - bp::return_internal_reference<1, bp::with_custodian_and_ward<1,2> >() ); - PyQMForce_exposer.def_pickle(sire_pickle_suite< ::SireOpenMM::PyQMForce >()); - PyQMForce_exposer.def( "__str__", &pvt_get_name); - PyQMForce_exposer.def( "__repr__", &pvt_get_name); - } + typedef char const *(::SireOpenMM::PyQMForce::*what_function_type)() const; + what_function_type what_function_value(&::SireOpenMM::PyQMForce::what); + + PyQMForce_exposer.def( + "what", what_function_value, bp::release_gil_policy(), "Return the C++ name for this class."); + } + PyQMForce_exposer.staticmethod("typeName"); + PyQMForce_exposer.def("__copy__", &__copy__); + PyQMForce_exposer.def("__deepcopy__", &__copy__); + PyQMForce_exposer.def("clone", &__copy__); + PyQMForce_exposer.def("__rlshift__", &__rlshift__QDataStream<::SireOpenMM::PyQMForce>, + bp::return_internal_reference<1, bp::with_custodian_and_ward<1, 2>>()); + PyQMForce_exposer.def("__rrshift__", &__rrshift__QDataStream<::SireOpenMM::PyQMForce>, + bp::return_internal_reference<1, bp::with_custodian_and_ward<1, 2>>()); + PyQMForce_exposer.def_pickle(sire_pickle_suite<::SireOpenMM::PyQMForce>()); + PyQMForce_exposer.def("__str__", &pvt_get_name); + PyQMForce_exposer.def("__repr__", &pvt_get_name); + } } diff --git a/wrapper/Convert/SireOpenMM/TorchQMEngine.pypp.cpp b/wrapper/Convert/SireOpenMM/TorchQMEngine.pypp.cpp index 29847e98d..b40473582 100644 --- a/wrapper/Convert/SireOpenMM/TorchQMEngine.pypp.cpp +++ b/wrapper/Convert/SireOpenMM/TorchQMEngine.pypp.cpp @@ -2,8 +2,8 @@ // (C) Christopher Woods, GPL >= 3 License -#include "boost/python.hpp" #include "TorchQMEngine.pypp.hpp" +#include "boost/python.hpp" namespace bp = boost::python; @@ -39,300 +39,234 @@ namespace bp = boost::python; #include "torchqm.h" -SireOpenMM::TorchQMEngine __copy__(const SireOpenMM::TorchQMEngine &other){ return SireOpenMM::TorchQMEngine(other); } +SireOpenMM::TorchQMEngine __copy__(const SireOpenMM::TorchQMEngine &other) { return SireOpenMM::TorchQMEngine(other); } #include "Helpers/str.hpp" #include "Helpers/release_gil_policy.hpp" -void register_TorchQMEngine_class(){ +void register_TorchQMEngine_class() +{ { //::SireOpenMM::TorchQMEngine - typedef bp::class_< SireOpenMM::TorchQMEngine, bp::bases< SireBase::Property, SireOpenMM::QMEngine > > TorchQMEngine_exposer_t; - TorchQMEngine_exposer_t TorchQMEngine_exposer = TorchQMEngine_exposer_t( "TorchQMEngine", "", bp::init< >("Default constructor.") ); - bp::scope TorchQMEngine_scope( TorchQMEngine_exposer ); - TorchQMEngine_exposer.def( bp::init< QString, bp::optional< SireUnits::Dimension::Length, int, bool, double > >(( bp::arg("arg0"), bp::arg("cutoff")=7.5 * SireUnits::angstrom, bp::arg("neighbour_list_frequency")=(int)(0), bp::arg("is_mechanical")=(bool)(false), bp::arg("lambda")=1. ), "Constructor\nPar:am module_path\nThe path to the serialised TorchScript module.\n\nPar:am cutoff\nThe ML cutoff distance.\n\nPar:am neighbour_list_frequency\nThe frequency at which the neighbour list is updated. (Number of steps.)\nIf zero, then no neighbour list is used.\n\nPar:am is_mechanical\nA flag to indicate if mechanical embedding is being used.\n\nPar:am lambda\nThe lambda weighting factor. This can be used to interpolate between\npotentials for end-state correction calculations.\n") ); - TorchQMEngine_exposer.def( bp::init< SireOpenMM::TorchQMEngine const & >(( bp::arg("other") ), "Copy constructor.") ); + typedef bp::class_> TorchQMEngine_exposer_t; + TorchQMEngine_exposer_t TorchQMEngine_exposer = TorchQMEngine_exposer_t("TorchQMEngine", "", bp::init<>("Default constructor.")); + bp::scope TorchQMEngine_scope(TorchQMEngine_exposer); + TorchQMEngine_exposer.def(bp::init>((bp::arg("arg0"), bp::arg("cutoff") = 7.5 * SireUnits::angstrom, bp::arg("neighbour_list_frequency") = (int)(0), bp::arg("is_mechanical") = (bool)(false), bp::arg("lambda") = 1.), "Constructor\nPar:am module_path\nThe path to the serialised TorchScript module.\n\nPar:am cutoff\nThe ML cutoff distance.\n\nPar:am neighbour_list_frequency\nThe frequency at which the neighbour list is updated. (Number of steps.)\nIf zero, then no neighbour list is used.\n\nPar:am is_mechanical\nA flag to indicate if mechanical embedding is being used.\n\nPar:am lambda\nThe lambda weighting factor. This can be used to interpolate between\npotentials for end-state correction calculations.\n")); + TorchQMEngine_exposer.def(bp::init((bp::arg("other")), "Copy constructor.")); { //::SireOpenMM::TorchQMEngine::getModulePath - - typedef ::QString ( ::SireOpenMM::TorchQMEngine::*getModulePath_function_type)( ) const; - getModulePath_function_type getModulePath_function_value( &::SireOpenMM::TorchQMEngine::getModulePath ); - - TorchQMEngine_exposer.def( - "getModulePath" - , getModulePath_function_value - , bp::release_gil_policy() - , "Get the path to the serialised TorchScript module.\n" ); - + + typedef ::QString (::SireOpenMM::TorchQMEngine::*getModulePath_function_type)() const; + getModulePath_function_type getModulePath_function_value(&::SireOpenMM::TorchQMEngine::getModulePath); + + TorchQMEngine_exposer.def( + "getModulePath", getModulePath_function_value, bp::release_gil_policy(), "Get the path to the serialised TorchScript module.\n"); } { //::SireOpenMM::TorchQMEngine::getAtoms - - typedef ::QVector< int > ( ::SireOpenMM::TorchQMEngine::*getAtoms_function_type)( ) const; - getAtoms_function_type getAtoms_function_value( &::SireOpenMM::TorchQMEngine::getAtoms ); - - TorchQMEngine_exposer.def( - "getAtoms" - , getAtoms_function_value - , bp::release_gil_policy() - , "Get the indices of the atoms in the QM region.\nReturn:s\nA vector of atom indices for the QM region.\n" ); - + + typedef ::QVector (::SireOpenMM::TorchQMEngine::*getAtoms_function_type)() const; + getAtoms_function_type getAtoms_function_value(&::SireOpenMM::TorchQMEngine::getAtoms); + + TorchQMEngine_exposer.def( + "getAtoms", getAtoms_function_value, bp::release_gil_policy(), "Get the indices of the atoms in the QM region.\nReturn:s\nA vector of atom indices for the QM region.\n"); } { //::SireOpenMM::TorchQMEngine::getCharges - - typedef ::QVector< double > ( ::SireOpenMM::TorchQMEngine::*getCharges_function_type)( ) const; - getCharges_function_type getCharges_function_value( &::SireOpenMM::TorchQMEngine::getCharges ); - - TorchQMEngine_exposer.def( - "getCharges" - , getCharges_function_value - , bp::release_gil_policy() - , "Get the atomic charges of all atoms in the system.\nReturn:s\nA vector of atomic charges for all atoms in the system.\n" ); - + + typedef ::QVector (::SireOpenMM::TorchQMEngine::*getCharges_function_type)() const; + getCharges_function_type getCharges_function_value(&::SireOpenMM::TorchQMEngine::getCharges); + + TorchQMEngine_exposer.def( + "getCharges", getCharges_function_value, bp::release_gil_policy(), "Get the atomic charges of all atoms in the system.\nReturn:s\nA vector of atomic charges for all atoms in the system.\n"); } { //::SireOpenMM::TorchQMEngine::getCutoff - - typedef ::SireUnits::Dimension::Length ( ::SireOpenMM::TorchQMEngine::*getCutoff_function_type)( ) const; - getCutoff_function_type getCutoff_function_value( &::SireOpenMM::TorchQMEngine::getCutoff ); - - TorchQMEngine_exposer.def( - "getCutoff" - , getCutoff_function_value - , bp::release_gil_policy() - , "Get the QM cutoff distance.\nReturn:s\nThe QM cutoff distance.\n" ); - + + typedef ::SireUnits::Dimension::Length (::SireOpenMM::TorchQMEngine::*getCutoff_function_type)() const; + getCutoff_function_type getCutoff_function_value(&::SireOpenMM::TorchQMEngine::getCutoff); + + TorchQMEngine_exposer.def( + "getCutoff", getCutoff_function_value, bp::release_gil_policy(), "Get the QM cutoff distance.\nReturn:s\nThe QM cutoff distance.\n"); } { //::SireOpenMM::TorchQMEngine::getIsMechanical - - typedef bool ( ::SireOpenMM::TorchQMEngine::*getIsMechanical_function_type)( ) const; - getIsMechanical_function_type getIsMechanical_function_value( &::SireOpenMM::TorchQMEngine::getIsMechanical ); - - TorchQMEngine_exposer.def( - "getIsMechanical" - , getIsMechanical_function_value - , bp::release_gil_policy() - , "Get the mechanical embedding flag.\nReturn:s\nA flag to indicate if mechanical embedding is being used.\n" ); - + + typedef bool (::SireOpenMM::TorchQMEngine::*getIsMechanical_function_type)() const; + getIsMechanical_function_type getIsMechanical_function_value(&::SireOpenMM::TorchQMEngine::getIsMechanical); + + TorchQMEngine_exposer.def( + "getIsMechanical", getIsMechanical_function_value, bp::release_gil_policy(), "Get the mechanical embedding flag.\nReturn:s\nA flag to indicate if mechanical embedding is being used.\n"); } { //::SireOpenMM::TorchQMEngine::getLambda - - typedef double ( ::SireOpenMM::TorchQMEngine::*getLambda_function_type)( ) const; - getLambda_function_type getLambda_function_value( &::SireOpenMM::TorchQMEngine::getLambda ); - - TorchQMEngine_exposer.def( - "getLambda" - , getLambda_function_value - , bp::release_gil_policy() - , "Get the lambda weighting factor.\nReturn:s\nThe lambda weighting factor.\n" ); - + + typedef double (::SireOpenMM::TorchQMEngine::*getLambda_function_type)() const; + getLambda_function_type getLambda_function_value(&::SireOpenMM::TorchQMEngine::getLambda); + + TorchQMEngine_exposer.def( + "getLambda", getLambda_function_value, bp::release_gil_policy(), "Get the lambda weighting factor.\nReturn:s\nThe lambda weighting factor.\n"); } { //::SireOpenMM::TorchQMEngine::getLinkAtoms - - typedef ::boost::tuples::tuple< QMap< int, int >, QMap< int, QVector< int > >, QMap< int, double >, boost::tuples::null_type, boost::tuples::null_type, boost::tuples::null_type, boost::tuples::null_type, boost::tuples::null_type, boost::tuples::null_type, boost::tuples::null_type > ( ::SireOpenMM::TorchQMEngine::*getLinkAtoms_function_type)( ) const; - getLinkAtoms_function_type getLinkAtoms_function_value( &::SireOpenMM::TorchQMEngine::getLinkAtoms ); - - TorchQMEngine_exposer.def( - "getLinkAtoms" - , getLinkAtoms_function_value - , bp::release_gil_policy() - , "Get the link atoms associated with each QM atom.\nReturn:s\nA tuple containing:\n\nmm1_to_qm\nA dictionary mapping link atom (MM1) indices to the QM atoms to\nwhich they are bonded.\n\nmm1_to_mm2\nA dictionary of link atoms indices (MM1) to a list of the MM\natoms to which they are bonded (MM2).\n\nbond_scale_factors\nA dictionary of link atom indices (MM1) to a list of the bond\nlength scale factors between the QM and MM1 atoms. The scale\nfactors are the ratio of the equilibrium bond lengths for the\nQM-L (QM-link) atom and QM-MM1 atom, i.e. R0(QM-L) R0(QM-MM1),\ntaken from the MM force field parameters for the molecule.\n\n" ); - + + typedef ::boost::tuples::tuple, QMap>, QMap, boost::tuples::null_type, boost::tuples::null_type, boost::tuples::null_type, boost::tuples::null_type, boost::tuples::null_type, boost::tuples::null_type, boost::tuples::null_type> (::SireOpenMM::TorchQMEngine::*getLinkAtoms_function_type)() const; + getLinkAtoms_function_type getLinkAtoms_function_value(&::SireOpenMM::TorchQMEngine::getLinkAtoms); + + TorchQMEngine_exposer.def( + "getLinkAtoms", getLinkAtoms_function_value, bp::release_gil_policy(), "Get the link atoms associated with each QM atom.\nReturn:s\nA tuple containing:\n\nmm1_to_qm\nA dictionary mapping link atom (MM1) indices to the QM atoms to\nwhich they are bonded.\n\nmm1_to_mm2\nA dictionary of link atoms indices (MM1) to a list of the MM\natoms to which they are bonded (MM2).\n\nbond_scale_factors\nA dictionary of link atom indices (MM1) to a list of the bond\nlength scale factors between the QM and MM1 atoms. The scale\nfactors are the ratio of the equilibrium bond lengths for the\nQM-L (QM-link) atom and QM-MM1 atom, i.e. R0(QM-L) R0(QM-MM1),\ntaken from the MM force field parameters for the molecule.\n\n"); } { //::SireOpenMM::TorchQMEngine::getMM2Atoms - - typedef ::QVector< int > ( ::SireOpenMM::TorchQMEngine::*getMM2Atoms_function_type)( ) const; - getMM2Atoms_function_type getMM2Atoms_function_value( &::SireOpenMM::TorchQMEngine::getMM2Atoms ); - - TorchQMEngine_exposer.def( - "getMM2Atoms" - , getMM2Atoms_function_value - , bp::release_gil_policy() - , "Get the vector of MM2 atoms.\nReturn:s\nA vector of MM2 atom indices.\n" ); - + + typedef ::QVector (::SireOpenMM::TorchQMEngine::*getMM2Atoms_function_type)() const; + getMM2Atoms_function_type getMM2Atoms_function_value(&::SireOpenMM::TorchQMEngine::getMM2Atoms); + + TorchQMEngine_exposer.def( + "getMM2Atoms", getMM2Atoms_function_value, bp::release_gil_policy(), "Get the vector of MM2 atoms.\nReturn:s\nA vector of MM2 atom indices.\n"); } { //::SireOpenMM::TorchQMEngine::getNeighbourListFrequency - - typedef int ( ::SireOpenMM::TorchQMEngine::*getNeighbourListFrequency_function_type)( ) const; - getNeighbourListFrequency_function_type getNeighbourListFrequency_function_value( &::SireOpenMM::TorchQMEngine::getNeighbourListFrequency ); - - TorchQMEngine_exposer.def( - "getNeighbourListFrequency" - , getNeighbourListFrequency_function_value - , bp::release_gil_policy() - , "Get the neighbour list frequency.\nReturn:s\nThe neighbour list frequency.\n" ); - + + typedef int (::SireOpenMM::TorchQMEngine::*getNeighbourListFrequency_function_type)() const; + getNeighbourListFrequency_function_type getNeighbourListFrequency_function_value(&::SireOpenMM::TorchQMEngine::getNeighbourListFrequency); + + TorchQMEngine_exposer.def( + "getNeighbourListFrequency", getNeighbourListFrequency_function_value, bp::release_gil_policy(), "Get the neighbour list frequency.\nReturn:s\nThe neighbour list frequency.\n"); } { //::SireOpenMM::TorchQMEngine::getNumbers - - typedef ::QVector< int > ( ::SireOpenMM::TorchQMEngine::*getNumbers_function_type)( ) const; - getNumbers_function_type getNumbers_function_value( &::SireOpenMM::TorchQMEngine::getNumbers ); - - TorchQMEngine_exposer.def( - "getNumbers" - , getNumbers_function_value - , bp::release_gil_policy() - , "Get the atomic numbers for the atoms in the QM region.\nReturn:s\nA vector of atomic numbers for the atoms in the QM region.\n" ); - + + typedef ::QVector (::SireOpenMM::TorchQMEngine::*getNumbers_function_type)() const; + getNumbers_function_type getNumbers_function_value(&::SireOpenMM::TorchQMEngine::getNumbers); + + TorchQMEngine_exposer.def( + "getNumbers", getNumbers_function_value, bp::release_gil_policy(), "Get the atomic numbers for the atoms in the QM region.\nReturn:s\nA vector of atomic numbers for the atoms in the QM region.\n"); } { //::SireOpenMM::TorchQMEngine::operator= - - typedef ::SireOpenMM::TorchQMEngine & ( ::SireOpenMM::TorchQMEngine::*assign_function_type)( ::SireOpenMM::TorchQMEngine const & ) ; - assign_function_type assign_function_value( &::SireOpenMM::TorchQMEngine::operator= ); - - TorchQMEngine_exposer.def( - "assign" - , assign_function_value - , ( bp::arg("other") ) - , bp::return_self< >() - , "Assignment operator." ); - + + typedef ::SireOpenMM::TorchQMEngine &(::SireOpenMM::TorchQMEngine::*assign_function_type)(::SireOpenMM::TorchQMEngine const &); + assign_function_type assign_function_value(&::SireOpenMM::TorchQMEngine::operator=); + + TorchQMEngine_exposer.def( + "assign", assign_function_value, (bp::arg("other")), bp::return_self<>(), "Assignment operator."); } { //::SireOpenMM::TorchQMEngine::setModulePath - - typedef void ( ::SireOpenMM::TorchQMEngine::*setModulePath_function_type)( ::QString ) ; - setModulePath_function_type setModulePath_function_value( &::SireOpenMM::TorchQMEngine::setModulePath ); - - TorchQMEngine_exposer.def( - "setModulePath" - , setModulePath_function_value - , ( bp::arg("module_path") ) - , bp::release_gil_policy() - , "Set the path to the serialised TorchScript module.\n" ); - + + typedef void (::SireOpenMM::TorchQMEngine::*setModulePath_function_type)(::QString); + setModulePath_function_type setModulePath_function_value(&::SireOpenMM::TorchQMEngine::setModulePath); + + TorchQMEngine_exposer.def( + "setModulePath", setModulePath_function_value, (bp::arg("module_path")), bp::release_gil_policy(), "Set the path to the serialised TorchScript module.\n"); } { //::SireOpenMM::TorchQMEngine::setAtoms - - typedef void ( ::SireOpenMM::TorchQMEngine::*setAtoms_function_type)( ::QVector< int > ) ; - setAtoms_function_type setAtoms_function_value( &::SireOpenMM::TorchQMEngine::setAtoms ); - - TorchQMEngine_exposer.def( - "setAtoms" - , setAtoms_function_value - , ( bp::arg("atoms") ) - , bp::release_gil_policy() - , "Set the list of atom indices for the QM region.\nPar:am atoms\nA vector of atom indices for the QM region.\n" ); - + + typedef void (::SireOpenMM::TorchQMEngine::*setAtoms_function_type)(::QVector); + setAtoms_function_type setAtoms_function_value(&::SireOpenMM::TorchQMEngine::setAtoms); + + TorchQMEngine_exposer.def( + "setAtoms", setAtoms_function_value, (bp::arg("atoms")), bp::release_gil_policy(), "Set the list of atom indices for the QM region.\nPar:am atoms\nA vector of atom indices for the QM region.\n"); } { //::SireOpenMM::TorchQMEngine::setCharges - - typedef void ( ::SireOpenMM::TorchQMEngine::*setCharges_function_type)( ::QVector< double > ) ; - setCharges_function_type setCharges_function_value( &::SireOpenMM::TorchQMEngine::setCharges ); - - TorchQMEngine_exposer.def( - "setCharges" - , setCharges_function_value - , ( bp::arg("charges") ) - , bp::release_gil_policy() - , "Set the atomic charges of all atoms in the system.\nPar:am charges\nA vector of atomic charges for all atoms in the system.\n" ); - + + typedef void (::SireOpenMM::TorchQMEngine::*setCharges_function_type)(::QVector); + setCharges_function_type setCharges_function_value(&::SireOpenMM::TorchQMEngine::setCharges); + + TorchQMEngine_exposer.def( + "setCharges", setCharges_function_value, (bp::arg("charges")), bp::release_gil_policy(), "Set the atomic charges of all atoms in the system.\nPar:am charges\nA vector of atomic charges for all atoms in the system.\n"); } { //::SireOpenMM::TorchQMEngine::setCutoff - - typedef void ( ::SireOpenMM::TorchQMEngine::*setCutoff_function_type)( ::SireUnits::Dimension::Length ) ; - setCutoff_function_type setCutoff_function_value( &::SireOpenMM::TorchQMEngine::setCutoff ); - - TorchQMEngine_exposer.def( - "setCutoff" - , setCutoff_function_value - , ( bp::arg("cutoff") ) - , bp::release_gil_policy() - , "Set the QM cutoff distance.\nPar:am cutoff\nThe QM cutoff distance.\n" ); - + + typedef void (::SireOpenMM::TorchQMEngine::*setCutoff_function_type)(::SireUnits::Dimension::Length); + setCutoff_function_type setCutoff_function_value(&::SireOpenMM::TorchQMEngine::setCutoff); + + TorchQMEngine_exposer.def( + "setCutoff", setCutoff_function_value, (bp::arg("cutoff")), bp::release_gil_policy(), "Set the QM cutoff distance.\nPar:am cutoff\nThe QM cutoff distance.\n"); } { //::SireOpenMM::TorchQMEngine::setIsMechanical - - typedef void ( ::SireOpenMM::TorchQMEngine::*setIsMechanical_function_type)( bool ) ; - setIsMechanical_function_type setIsMechanical_function_value( &::SireOpenMM::TorchQMEngine::setIsMechanical ); - - TorchQMEngine_exposer.def( - "setIsMechanical" - , setIsMechanical_function_value - , ( bp::arg("is_mechanical") ) - , bp::release_gil_policy() - , "Set the mechanical embedding flag.\nPar:am is_mechanical\nA flag to indicate if mechanical embedding is being used.\n" ); - + + typedef void (::SireOpenMM::TorchQMEngine::*setIsMechanical_function_type)(bool); + setIsMechanical_function_type setIsMechanical_function_value(&::SireOpenMM::TorchQMEngine::setIsMechanical); + + TorchQMEngine_exposer.def( + "setIsMechanical", setIsMechanical_function_value, (bp::arg("is_mechanical")), bp::release_gil_policy(), "Set the mechanical embedding flag.\nPar:am is_mechanical\nA flag to indicate if mechanical embedding is being used.\n"); } { //::SireOpenMM::TorchQMEngine::setLambda - - typedef void ( ::SireOpenMM::TorchQMEngine::*setLambda_function_type)( double ) ; - setLambda_function_type setLambda_function_value( &::SireOpenMM::TorchQMEngine::setLambda ); - - TorchQMEngine_exposer.def( - "setLambda" - , setLambda_function_value - , ( bp::arg("lambda") ) - , bp::release_gil_policy() - , "Set the lambda weighting factor.\nPar:am lambda\nThe lambda weighting factor.\n" ); - + + typedef void (::SireOpenMM::TorchQMEngine::*setLambda_function_type)(double); + setLambda_function_type setLambda_function_value(&::SireOpenMM::TorchQMEngine::setLambda); + + TorchQMEngine_exposer.def( + "setLambda", setLambda_function_value, (bp::arg("lambda")), bp::release_gil_policy(), "Set the lambda weighting factor.\nPar:am lambda\nThe lambda weighting factor.\n"); } { //::SireOpenMM::TorchQMEngine::setLinkAtoms - - typedef void ( ::SireOpenMM::TorchQMEngine::*setLinkAtoms_function_type)( ::QMap< int, int >,::QMap< int, QVector< int > >,::QMap< int, double > ) ; - setLinkAtoms_function_type setLinkAtoms_function_value( &::SireOpenMM::TorchQMEngine::setLinkAtoms ); - - TorchQMEngine_exposer.def( - "setLinkAtoms" - , setLinkAtoms_function_value - , ( bp::arg("mm1_to_qm"), bp::arg("mm1_to_mm2"), bp::arg("bond_scale_factors") ) - , bp::release_gil_policy() - , "Set the link atoms associated with each QM atom.\nPar:am mm1_to_qm\nA dictionary mapping link atom (MM1) indices to the QM atoms to\nwhich they are bonded.\n\nPar:am mm1_to_mm2\nA dictionary of link atoms indices (MM1) to a list of the MM\natoms to which they are bonded (MM2).\n\nPar:am bond_scale_factors\nA dictionary of link atom indices (MM1) to a list of the bond\nlength scale factors between the QM and MM1 atoms. The scale\nfactors are the ratio of the equilibrium bond lengths for the\nQM-L (QM-link) atom and QM-MM1 atom, i.e. R0(QM-L) R0(QM-MM1),\ntaken from the MM force field parameters for the molecule.\n\n" ); - + + typedef void (::SireOpenMM::TorchQMEngine::*setLinkAtoms_function_type)(::QMap, ::QMap>, ::QMap); + setLinkAtoms_function_type setLinkAtoms_function_value(&::SireOpenMM::TorchQMEngine::setLinkAtoms); + + TorchQMEngine_exposer.def( + "setLinkAtoms", setLinkAtoms_function_value, (bp::arg("mm1_to_qm"), bp::arg("mm1_to_mm2"), bp::arg("bond_scale_factors")), bp::release_gil_policy(), "Set the link atoms associated with each QM atom.\nPar:am mm1_to_qm\nA dictionary mapping link atom (MM1) indices to the QM atoms to\nwhich they are bonded.\n\nPar:am mm1_to_mm2\nA dictionary of link atoms indices (MM1) to a list of the MM\natoms to which they are bonded (MM2).\n\nPar:am bond_scale_factors\nA dictionary of link atom indices (MM1) to a list of the bond\nlength scale factors between the QM and MM1 atoms. The scale\nfactors are the ratio of the equilibrium bond lengths for the\nQM-L (QM-link) atom and QM-MM1 atom, i.e. R0(QM-L) R0(QM-MM1),\ntaken from the MM force field parameters for the molecule.\n\n"); } { //::SireOpenMM::TorchQMEngine::setNeighbourListFrequency - - typedef void ( ::SireOpenMM::TorchQMEngine::*setNeighbourListFrequency_function_type)( int ) ; - setNeighbourListFrequency_function_type setNeighbourListFrequency_function_value( &::SireOpenMM::TorchQMEngine::setNeighbourListFrequency ); - - TorchQMEngine_exposer.def( - "setNeighbourListFrequency" - , setNeighbourListFrequency_function_value - , ( bp::arg("neighbour_list_frequency") ) - , bp::release_gil_policy() - , "Set the neighbour list frequency.\nPar:am neighbour_list_frequency\nThe neighbour list frequency.\n" ); - + + typedef void (::SireOpenMM::TorchQMEngine::*setNeighbourListFrequency_function_type)(int); + setNeighbourListFrequency_function_type setNeighbourListFrequency_function_value(&::SireOpenMM::TorchQMEngine::setNeighbourListFrequency); + + TorchQMEngine_exposer.def( + "setNeighbourListFrequency", setNeighbourListFrequency_function_value, (bp::arg("neighbour_list_frequency")), bp::release_gil_policy(), "Set the neighbour list frequency.\nPar:am neighbour_list_frequency\nThe neighbour list frequency.\n"); } { //::SireOpenMM::TorchQMEngine::setNumbers - - typedef void ( ::SireOpenMM::TorchQMEngine::*setNumbers_function_type)( ::QVector< int > ) ; - setNumbers_function_type setNumbers_function_value( &::SireOpenMM::TorchQMEngine::setNumbers ); - - TorchQMEngine_exposer.def( - "setNumbers" - , setNumbers_function_value - , ( bp::arg("numbers") ) - , bp::release_gil_policy() - , "Set the atomic numbers for the atoms in the QM region.\nPar:am numbers\nA vector of atomic numbers for the atoms in the QM region.\n" ); - + + typedef void (::SireOpenMM::TorchQMEngine::*setNumbers_function_type)(::QVector); + setNumbers_function_type setNumbers_function_value(&::SireOpenMM::TorchQMEngine::setNumbers); + + TorchQMEngine_exposer.def( + "setNumbers", setNumbers_function_value, (bp::arg("numbers")), bp::release_gil_policy(), "Set the atomic numbers for the atoms in the QM region.\nPar:am numbers\nA vector of atomic numbers for the atoms in the QM region.\n"); + } + { //::SireOpenMM::TorchQMEngine::getSwitchWidth + + typedef double (::SireOpenMM::TorchQMEngine::*getSwitchWidth_function_type)() const; + getSwitchWidth_function_type getSwitchWidth_function_value(&::SireOpenMM::TorchQMEngine::getSwitchWidth); + + TorchQMEngine_exposer.def( + "getSwitchWidth", getSwitchWidth_function_value, bp::release_gil_policy(), "Get the switch width as a fraction of the cutoff.\n"); + } + { //::SireOpenMM::TorchQMEngine::setSwitchWidth + + typedef void (::SireOpenMM::TorchQMEngine::*setSwitchWidth_function_type)(double); + setSwitchWidth_function_type setSwitchWidth_function_value(&::SireOpenMM::TorchQMEngine::setSwitchWidth); + + TorchQMEngine_exposer.def( + "setSwitchWidth", setSwitchWidth_function_value, (bp::arg("switch_width")), bp::release_gil_policy(), "Set the switch width as a fraction of the cutoff (0 to 1).\n"); + } + { //::SireOpenMM::TorchQMEngine::getUseSwitch + + typedef bool (::SireOpenMM::TorchQMEngine::*getUseSwitch_function_type)() const; + getUseSwitch_function_type getUseSwitch_function_value(&::SireOpenMM::TorchQMEngine::getUseSwitch); + + TorchQMEngine_exposer.def( + "getUseSwitch", getUseSwitch_function_value, bp::release_gil_policy(), "Get whether a switching function is used.\n"); + } + { //::SireOpenMM::TorchQMEngine::setUseSwitch + + typedef void (::SireOpenMM::TorchQMEngine::*setUseSwitch_function_type)(bool); + setUseSwitch_function_type setUseSwitch_function_value(&::SireOpenMM::TorchQMEngine::setUseSwitch); + + TorchQMEngine_exposer.def( + "setUseSwitch", setUseSwitch_function_value, (bp::arg("use_switch")), bp::release_gil_policy(), "Set whether to use a switching function.\n"); } { //::SireOpenMM::TorchQMEngine::typeName - - typedef char const * ( *typeName_function_type )( ); - typeName_function_type typeName_function_value( &::SireOpenMM::TorchQMEngine::typeName ); - - TorchQMEngine_exposer.def( - "typeName" - , typeName_function_value - , bp::release_gil_policy() - , "Return the C++ name for this class." ); - + + typedef char const *(*typeName_function_type)(); + typeName_function_type typeName_function_value(&::SireOpenMM::TorchQMEngine::typeName); + + TorchQMEngine_exposer.def( + "typeName", typeName_function_value, bp::release_gil_policy(), "Return the C++ name for this class."); } { //::SireOpenMM::TorchQMEngine::what - - typedef char const * ( ::SireOpenMM::TorchQMEngine::*what_function_type)( ) const; - what_function_type what_function_value( &::SireOpenMM::TorchQMEngine::what ); - - TorchQMEngine_exposer.def( - "what" - , what_function_value - , bp::release_gil_policy() - , "Return the C++ name for this class." ); - + + typedef char const *(::SireOpenMM::TorchQMEngine::*what_function_type)() const; + what_function_type what_function_value(&::SireOpenMM::TorchQMEngine::what); + + TorchQMEngine_exposer.def( + "what", what_function_value, bp::release_gil_policy(), "Return the C++ name for this class."); } - TorchQMEngine_exposer.staticmethod( "typeName" ); - TorchQMEngine_exposer.def( "__copy__", &__copy__); - TorchQMEngine_exposer.def( "__deepcopy__", &__copy__); - TorchQMEngine_exposer.def( "clone", &__copy__); - TorchQMEngine_exposer.def( "__str__", &__str__< ::SireOpenMM::TorchQMEngine > ); - TorchQMEngine_exposer.def( "__repr__", &__str__< ::SireOpenMM::TorchQMEngine > ); + TorchQMEngine_exposer.staticmethod("typeName"); + TorchQMEngine_exposer.def("__copy__", &__copy__); + TorchQMEngine_exposer.def("__deepcopy__", &__copy__); + TorchQMEngine_exposer.def("clone", &__copy__); + TorchQMEngine_exposer.def("__str__", &__str__<::SireOpenMM::TorchQMEngine>); + TorchQMEngine_exposer.def("__repr__", &__str__<::SireOpenMM::TorchQMEngine>); } - } diff --git a/wrapper/Convert/SireOpenMM/_perturbablemol.py b/wrapper/Convert/SireOpenMM/_perturbablemol.py index 8697ba817..135e44cdc 100644 --- a/wrapper/Convert/SireOpenMM/_perturbablemol.py +++ b/wrapper/Convert/SireOpenMM/_perturbablemol.py @@ -5,10 +5,58 @@ "_changed_torsions", "_changed_exceptions", "_changed_constraints", + "_changed_cmaps", "_get_lever_values", ] +def _changed_cmaps(obj, to_pandas: bool = True): + """ + Return a list of the CMAP torsions that change parameters in this + perturbation + + Parameters + ---------- + + to_pandas: bool, optional, default=True + If True then the list of CMAP torsions will be returned as a pandas + DataFrame + """ + changed_cmaps = [] + + atoms = obj.atoms() + sizes = list(obj.get_cmap_grid_sizes()) + grids0 = list(obj.get_cmap_grids0()) + grids1 = list(obj.get_cmap_grids1()) + + offset = 0 + for torsion, n in zip(obj.get_cmap_atoms(), sizes): + ns = n * n + g0 = grids0[offset : offset + ns] + g1 = grids1[offset : offset + ns] + offset += ns + + if g0 != g1: + atom0, atom1, atom2, atom3, atom4 = (atoms[i] for i in torsion) + + torsion_atoms = (atom0, atom1, atom2, atom3, atom4) + + if to_pandas: + torsion_atoms = "-".join(_name(a) for a in torsion_atoms) + + changed_cmaps.append((torsion_atoms, n)) + + if to_pandas: + import pandas as pd + + changed_cmaps = pd.DataFrame( + changed_cmaps, + columns=["torsion", "grid_size"], + ) + + return changed_cmaps + + def _get_lever_values( obj, schedule=None, diff --git a/wrapper/Convert/SireOpenMM/_sommcontext.py b/wrapper/Convert/SireOpenMM/_sommcontext.py index 8d990cc46..b5d8233cf 100644 --- a/wrapper/Convert/SireOpenMM/_sommcontext.py +++ b/wrapper/Convert/SireOpenMM/_sommcontext.py @@ -69,6 +69,9 @@ def __init__( # place the coordinates and velocities into the context set_openmm_coordinates_and_velocities(self, metadata) + # set the positions of any virtual sites + self.computeVirtualSites() + # Check for a REST2 scaling factor. if map.specified("rest2_scale"): try: @@ -96,11 +99,24 @@ def __init__( ) self._map = map + + # Build a name → force-group-index map from the lambda lever. + # Used externally (e.g. by SOMD2) to evaluate per-component energies + # via getState(groups=(1 << grp)). + self._force_group_map = { + name: self._lambda_lever.get_force_group(name) + for name in self._lambda_lever.get_force_names() + if self._lambda_lever.get_force_group(name) >= 0 + } + + self._energy_cache = {} else: self._atom_index = None self._lambda_lever = None self._lambda_value = 0.0 self._map = None + self._force_group_map = {} + self._energy_cache = {} self._is_non_pert_rest2 = False @@ -277,6 +293,7 @@ def set_lambda( rest2_scale=rest2_scale, update_constraints=update_constraints, ) + self.clear_energy_cache() # Update any additional parameters in the REST2 region. if self._is_non_pert_rest2 and rest2_scale != self._rest2_scale: @@ -293,7 +310,7 @@ def set_rest2_scale(self, rest2_scale): """ Set the temperature scale factor for the REST2 region. """ - self._set_lambda(self._lambda_value, rest2_scale=rest2_scale) + self.set_lambda(self._lambda_value, rest2_scale=rest2_scale) def set_temperature(self, temperature, rescale_velocities=True): """ @@ -317,18 +334,62 @@ def set_surface_tension(self, surface_tension): def get_potential_energy(self, to_sire_units: bool = True): """ - Calculate and return the potential energy of the system + Calculate and return the potential energy of the system. + + Uses energy caching: if neither lambda nor positions have changed since + the last call, the cached total is returned without any GPU call. + Otherwise a single full getState() evaluation is performed and cached. """ - s = self.getState(getEnergy=True) - nrg = s.getPotentialEnergy() + import openmm + + if "_total" not in self._energy_cache: + total_kj = ( + self.getState(getEnergy=True) + .getPotentialEnergy() + .value_in_unit(openmm.unit.kilojoule_per_mole) + ) + self._energy_cache["_total"] = total_kj + + total_kj = self._energy_cache["_total"] if to_sire_units: - import openmm from ...units import kcal_per_mol - return nrg.value_in_unit(openmm.unit.kilocalorie_per_mole) * kcal_per_mol + return (total_kj / 4.184) * kcal_per_mol else: - return nrg + return total_kj * openmm.unit.kilojoule_per_mole + + def setPositions(self, positions, *args, **kwargs): + """ + Set the positions of all particles. Overridden to automatically + invalidate the per-force-group energy cache. + """ + super().setPositions(positions, *args, **kwargs) + self.clear_energy_cache() + + def setState(self, state, *args, **kwargs): + """ + Set the complete state of the context (positions, velocities, box + vectors). Overridden to automatically invalidate the per-force-group + energy cache. + """ + super().setState(state, *args, **kwargs) + self.clear_energy_cache() + + def setPeriodicBoxVectors(self, a, b, c, *args, **kwargs): + """ + Set the periodic box vectors. Overridden to automatically invalidate + the per-force-group energy cache, since a box change affects PME energy. + """ + super().setPeriodicBoxVectors(a, b, c, *args, **kwargs) + self.clear_energy_cache() + + def clear_energy_cache(self): + """ + Invalidate the energy cache. Call this whenever positions or lambda + change so that the next get_potential_energy() call re-evaluates. + """ + self._energy_cache.clear() def get_energy(self, to_sire_units: bool = True): """ diff --git a/wrapper/Convert/SireOpenMM/lambdalever.cpp b/wrapper/Convert/SireOpenMM/lambdalever.cpp index 8651e2951..d271e4a0b 100644 --- a/wrapper/Convert/SireOpenMM/lambdalever.cpp +++ b/wrapper/Convert/SireOpenMM/lambdalever.cpp @@ -53,8 +53,15 @@ MolLambdaCache::MolLambdaCache(double lam) { } +MolLambdaCache::MolLambdaCache(double lam, const MolLambdaCache &prev) + : lam_val(lam) +{ + QReadLocker lkr(&(const_cast(&prev)->lock)); + prev_cache = prev.cache; +} + MolLambdaCache::MolLambdaCache(const MolLambdaCache &other) - : lam_val(other.lam_val), cache(other.cache) + : lam_val(other.lam_val), cache(other.cache), prev_cache(other.prev_cache) { } @@ -68,11 +75,44 @@ MolLambdaCache &MolLambdaCache::operator=(const MolLambdaCache &other) { lam_val = other.lam_val; cache = other.cache; + prev_cache = other.prev_cache; } return *this; } +bool MolLambdaCache::hasChanged(const QString &force, const QString &key) const +{ + return this->hasChanged(force, key, QString()); +} + +bool MolLambdaCache::hasChanged(const QString &force, const QString &key, + const QString &subkey) const +{ + if (prev_cache.isEmpty()) + return true; + + QString cache_key = key; + + if (not subkey.isEmpty()) + cache_key += ("::" + subkey); + + const QString force_key = force + "::" + cache_key; + + const auto prev_it = prev_cache.constFind(force_key); + if (prev_it == prev_cache.constEnd()) + return true; + + auto nonconst_this = const_cast(this); + QReadLocker lkr(&(nonconst_this->lock)); + + const auto curr_it = cache.constFind(force_key); + if (curr_it == cache.constEnd()) + return true; + + return curr_it.value() != prev_it.value(); +} + const QVector &MolLambdaCache::morph(const LambdaSchedule &schedule, const QString &force, const QString &key, @@ -155,7 +195,8 @@ LeverCache::LeverCache() { } -LeverCache::LeverCache(const LeverCache &other) : cache(other.cache) +LeverCache::LeverCache(const LeverCache &other) + : cache(other.cache), prev_lam_vals(other.prev_lam_vals) { } @@ -168,6 +209,7 @@ LeverCache &LeverCache::operator=(const LeverCache &other) if (this != &other) { cache = other.cache; + prev_lam_vals = other.prev_lam_vals; } return *this; @@ -188,10 +230,25 @@ const MolLambdaCache &LeverCache::get(int molidx, double lam_val) const if (it == mol_cache.constEnd()) { - // need to create a new cache for this lambda value - it = mol_cache.insert(lam_val, MolLambdaCache(lam_val)); + // Create a new cache for this lambda value, initialising prev_cache + // from the previous lambda's computed values so hasChanged() works. + auto prev_it = nonconst_this->prev_lam_vals.constFind(molidx); + if (prev_it != nonconst_this->prev_lam_vals.constEnd()) + { + auto old_it = mol_cache.constFind(prev_it.value()); + if (old_it != mol_cache.constEnd()) + it = mol_cache.insert(lam_val, MolLambdaCache(lam_val, old_it.value())); + else + it = mol_cache.insert(lam_val, MolLambdaCache(lam_val)); + } + else + { + it = mol_cache.insert(lam_val, MolLambdaCache(lam_val)); + } } + nonconst_this->prev_lam_vals[molidx] = lam_val; + return it.value(); } @@ -204,7 +261,10 @@ void LeverCache::clear() ////// Implementation of LambdaLever ////// -LambdaLever::LambdaLever() : SireBase::ConcreteProperty() +LambdaLever::LambdaLever() + : SireBase::ConcreteProperty(), + last_rest2_scale(-1.0), + last_qmff_lam(-1.0) { } @@ -212,11 +272,14 @@ LambdaLever::LambdaLever(const LambdaLever &other) : SireBase::ConcreteProperty(other), name_to_ffidx(other.name_to_ffidx), name_to_restraintidx(other.name_to_restraintidx), + name_to_groupidx(other.name_to_groupidx), lambda_schedule(other.lambda_schedule), perturbable_mols(other.perturbable_mols), start_indices(other.start_indices), perturbable_maps(other.perturbable_maps), - lambda_cache(other.lambda_cache) + lambda_cache(other.lambda_cache), + last_rest2_scale(-1.0), + last_qmff_lam(-1.0) { } @@ -230,6 +293,7 @@ LambdaLever &LambdaLever::operator=(const LambdaLever &other) { name_to_ffidx = other.name_to_ffidx; name_to_restraintidx = other.name_to_restraintidx; + name_to_groupidx = other.name_to_groupidx; lambda_schedule = other.lambda_schedule; perturbable_mols = other.perturbable_mols; start_indices = other.start_indices; @@ -324,6 +388,61 @@ QString LambdaLever::getForceType(const QString &name, return QString::fromStdString(force.getName()); } +/** Set the force group index for the force called 'name'. */ +void LambdaLever::setForceGroup(const QString &name, int group_idx) +{ + name_to_groupidx.insert(name, group_idx); +} + +/** Set the force group index for the restraint called 'name'. + * Unlike setForceGroup, this is a no-op if the name is already registered, + * since multiple restraint forces can share the same name and group. + */ +void LambdaLever::setRestraintForceGroup(const QString &name, int group_idx) +{ + if (!name_to_groupidx.contains(name)) + name_to_groupidx.insert(name, group_idx); +} + +/** Get the force group index for the force called 'name'. + * Returns -1 if there is no force with this name. + */ +int LambdaLever::getForceGroup(const QString &name) const +{ + auto it = name_to_groupidx.constFind(name); + + if (it == name_to_groupidx.constEnd()) + return -1; + + return it.value(); +} + +/** Return the names of all forces and restraints that have been assigned + * a force group index. + */ +QStringList LambdaLever::getForceNames() const +{ + return name_to_groupidx.keys(); +} + +/** Return whether the named force had parameters changed in the last + * setLambda call. Returns false if the name is not recognised. + */ +bool LambdaLever::wasForceChanged(const QString &name) const +{ + auto it = last_changed_forces.constFind(name); + + if (it == last_changed_forces.constEnd()) + return false; + + return it.value(); +} + +void LambdaLever::setGCMCWaterAtoms(const QVector &atoms) +{ + gcmc_water_atoms = QSet(atoms.begin(), atoms.end()); +} + boost::tuple get_exception(int atom0, int atom1, int start_index, double coul_14_scl, double lj_14_scl, @@ -1107,6 +1226,27 @@ PropertyList LambdaLever::getLeverValues(const QVector &lambda_values, idx += 1; } + const auto morphed_cmap_grid = cache.morph( + schedule, + "cmap", "cmap_grid", + mol.getCMAPGrids0(), + mol.getCMAPGrids1()); + + if (is_first) + { + for (int i = 0; i < morphed_cmap_grid.count(); ++i) + { + column_names.append(QString("cmap-cmap_grid-%1").arg(i + 1)); + lever_values.append(QVector()); + } + } + + for (const auto &val : morphed_cmap_grid) + { + lever_values[idx].append(val); + idx += 1; + } + is_first = false; } @@ -1140,6 +1280,12 @@ double LambdaLever::setLambda(OpenMM::Context &context, // scale factor. rest2_scale = 1.0 / rest2_scale; + // Detect whether REST2 scaling changed since the last setLambda call. + // REST2 is applied on top of morphed parameters, so a change in scale + // requires re-uploading parameters even if morphed values are unchanged. + const bool rest2_changed = (rest2_scale != last_rest2_scale); + last_rest2_scale = rest2_scale; + // Store the REST charge scaling factor for non-bonded interactions. const auto sqrt_rest2_scale = std::sqrt(rest2_scale); @@ -1153,9 +1299,12 @@ double LambdaLever::setLambda(OpenMM::Context &context, auto ghost_ghostff = this->getForce("ghost/ghost", system); auto ghost_nonghostff = this->getForce("ghost/non-ghost", system); auto ghost_14ff = this->getForce("ghost-14", system); + auto ring_breaking_ff = this->getForce("ring-break", system); + auto ring_making_ff = this->getForce("ring-make", system); auto bondff = this->getForce("bond", system); auto angff = this->getForce("angle", system); auto dihff = this->getForce("torsion", system); + auto cmapff = this->getForce("cmap", system); // we know if we have peturbable ghost atoms if we have the ghost forcefields const bool have_ghost_atoms = (ghost_ghostff != 0 or ghost_nonghostff != 0); @@ -1168,21 +1317,47 @@ double LambdaLever::setLambda(OpenMM::Context &context, if (qmff != 0) { double lam = this->lambda_schedule.morph("qmff", "*", 0.0, 1.0, lambda_value); + last_changed_forces["qmff"] = (lam != last_qmff_lam); qmff->setLambda(lam); + last_qmff_lam = lam; } - // record the range of indices of the atoms, bonds, angles, - // torsions which change - int start_change_atom = -1; - int end_change_atom = -1; - int start_change_14 = -1; - int end_change_14 = -1; - int start_change_bond = -1; - int end_change_bond = -1; - int start_change_angle = -1; - int end_change_angle = -1; - int start_change_torsion = -1; - int end_change_torsion = -1; + // track whether parameters actually changed for each force, so we only + // call updateParametersInContext when necessary + bool has_changed_cljff = false; + bool has_changed_ghostff = false; + bool has_changed_ghost14ff = false; + bool has_changed_ring_breaking_ff = false; + bool has_changed_ring_making_ff = false; + bool has_changed_bondff = false; + bool has_changed_angff = false; + bool has_changed_dihff = false; + bool has_changed_cmap = false; + + // Pre-compute ring-break/make alpha and coul_kappa values so the per-mol + // exception update loop and the later global-parameter block both use the + // same values. + const double rb_alpha = (ring_breaking_ff != nullptr) + ? std::max(0.0, std::min(1.0, this->lambda_schedule.morph( + "ring-break", "alpha", 1.0, 0.0, lambda_value))) + : 1.0; + const double rm_alpha = (ring_making_ff != nullptr) + ? std::max(0.0, std::min(1.0, this->lambda_schedule.morph( + "ring-make", "alpha", 0.0, 1.0, lambda_value))) + : 0.0; + + // coul_kappa levers decouple Coulomb onset from LJ onset: zero during + // potential_swap/restraints_off/ring_open, ramps 0→1 in morph only. + // This prevents spurious Coulomb attraction when atoms are still at + // covalent distances during the ring_open stage. + const double rb_coul_kappa = (ring_breaking_ff != nullptr) + ? std::max(0.0, std::min(1.0, this->lambda_schedule.morph( + "ring-break", "coul_kappa", 0.0, 1.0, lambda_value))) + : 0.0; + const double rm_coul_kappa = (ring_making_ff != nullptr) + ? std::max(0.0, std::min(1.0, this->lambda_schedule.morph( + "ring-make", "coul_kappa", 1.0, 0.0, lambda_value))) + : 1.0; // change the parameters for all of the perturbable molecules for (int i = 0; i < this->perturbable_mols.count(); ++i) @@ -1348,15 +1523,38 @@ double LambdaLever::setLambda(OpenMM::Context &context, const int nparams = morphed_charges.count(); - if (start_change_atom == -1) - { - start_change_atom = start_index; - end_change_atom = start_index + nparams; - } - else if (start_index >= end_change_atom) - { - end_change_atom = start_index + nparams; - } + // Detect whether any CLJ or ghost-14 parameters changed + // clang-format off + has_changed_cljff |= rest2_changed + || cache.hasChanged("clj", "charge") + || cache.hasChanged("clj", "sigma") + || cache.hasChanged("clj", "epsilon") + || cache.hasChanged("clj", "alpha") + || cache.hasChanged("clj", "kappa") + || cache.hasChanged("clj", "charge_scale") + || cache.hasChanged("clj", "lj_scale"); + + has_changed_ghostff |= rest2_changed + || cache.hasChanged("ghost/ghost", "charge") + || cache.hasChanged("ghost/ghost", "sigma") + || cache.hasChanged("ghost/ghost", "epsilon") + || cache.hasChanged("ghost/ghost", "alpha") + || cache.hasChanged("ghost/ghost", "kappa") + || cache.hasChanged("ghost/non-ghost", "charge") + || cache.hasChanged("ghost/non-ghost", "sigma") + || cache.hasChanged("ghost/non-ghost", "epsilon") + || cache.hasChanged("ghost/non-ghost", "alpha") + || cache.hasChanged("ghost/non-ghost", "kappa"); + + has_changed_ghost14ff |= rest2_changed + || cache.hasChanged("ghost-14", "charge") + || cache.hasChanged("ghost-14", "sigma") + || cache.hasChanged("ghost-14", "epsilon") + || cache.hasChanged("ghost-14", "alpha") + || cache.hasChanged("ghost-14", "kappa") + || cache.hasChanged("ghost-14", "charge_scale") + || cache.hasChanged("ghost-14", "lj_scale"); + // clang-format on if (have_ghost_atoms) { @@ -1433,7 +1631,7 @@ double LambdaLever::setLambda(OpenMM::Context &context, else { cljff->setParticleParameters( - start_index + j, sqrt_scale* morphed_charges[j], + start_index + j, sqrt_scale * morphed_charges[j], morphed_sigmas[j], scale * morphed_epsilons[j]); } } @@ -1453,7 +1651,7 @@ double LambdaLever::setLambda(OpenMM::Context &context, } cljff->setParticleParameters(start_index + j, sqrt_scale * morphed_charges[j], - morphed_sigmas[j], scale * morphed_epsilons[j]); + morphed_sigmas[j], scale * morphed_epsilons[j]); } } @@ -1489,6 +1687,12 @@ double LambdaLever::setLambda(OpenMM::Context &context, scale = rest2_scale; } + // ring-breaking/making pairs have idx=-1 (their CLJ + // exception is fixed at 1e-9 and must not be updated; + // the ring force handles the interaction via global params) + if (boost::get<0>(idxs[j]) == -1) + continue; + // don't set LJ terms for ghost atoms if (atom0_is_ghost or atom1_is_ghost) { @@ -1516,8 +1720,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]; @@ -1536,22 +1740,7 @@ double LambdaLever::setLambda(OpenMM::Context &context, boost::get<3>(p), 4.0 * boost::get<4>(p) * scale, boost::get<5>(p), - boost::get<6>(p) - }; - - if (start_change_14 == -1) - { - start_change_14 = nbidx; - end_change_14 = nbidx + 1; - } - else - { - if (nbidx < start_change_14) - start_change_14 = nbidx; - - if (nbidx + 1 > end_change_14) - end_change_14 = nbidx + 1; - } + boost::get<6>(p)}; ghost_14ff->setBondParameters(nbidx, boost::get<0>(p), @@ -1571,6 +1760,64 @@ double LambdaLever::setLambda(OpenMM::Context &context, } } + // Update CLJ exceptions for ring-breaking pairs. + // The CLJ exception carries coul_kappa*q_a0*q_a1 for the hard Coulomb + // (including RF/PME long-range). coul_kappa is zero during + // potential_swap/restraints_off/ring_open and ramps 0→1 in morph only, + // so Coulomb only activates once the LJ softcore has already separated + // the atoms. The CustomBondForce handles softcore LJ only (no q param). + if (cljff != nullptr and ring_breaking_ff != nullptr) + { + const auto rb_exc = perturbable_mol.getExceptionIndicies("ring-break"); + for (int j = 0; j < rb_exc.count(); ++j) + { + const int clj_idx = boost::get<0>(rb_exc[j]); + if (clj_idx < 0) + continue; + int rb_ea0, rb_ea1; + double rb_old_charge, rb_old_sig, rb_old_eps; + cljff->getExceptionParameters(clj_idx, rb_ea0, rb_ea1, + rb_old_charge, rb_old_sig, rb_old_eps); + double q_a0, sig_a0, eps_a0; + double q_a1, sig_a1, eps_a1; + cljff->getParticleParameters(rb_ea0, q_a0, sig_a0, eps_a0); + cljff->getParticleParameters(rb_ea1, q_a1, sig_a1, eps_a1); + const double rb_new_charge = rb_coul_kappa * q_a0 * q_a1; + if (rb_new_charge != rb_old_charge) + { + cljff->setExceptionParameters(clj_idx, rb_ea0, rb_ea1, + rb_new_charge, 1e-9, 1e-9); + has_changed_cljff = true; + } + } + } + + if (cljff != nullptr and ring_making_ff != nullptr) + { + const auto rm_exc = perturbable_mol.getExceptionIndicies("ring-make"); + for (int j = 0; j < rm_exc.count(); ++j) + { + const int clj_idx = boost::get<0>(rm_exc[j]); + if (clj_idx < 0) + continue; + int rm_ea0, rm_ea1; + double rm_old_charge, rm_old_sig, rm_old_eps; + cljff->getExceptionParameters(clj_idx, rm_ea0, rm_ea1, + rm_old_charge, rm_old_sig, rm_old_eps); + double q_a0, sig_a0, eps_a0; + double q_a1, sig_a1, eps_a1; + cljff->getParticleParameters(rm_ea0, q_a0, sig_a0, eps_a0); + cljff->getParticleParameters(rm_ea1, q_a1, sig_a1, eps_a1); + const double rm_new_charge = rm_coul_kappa * q_a0 * q_a1; + if (rm_new_charge != rm_old_charge) + { + cljff->setExceptionParameters(clj_idx, rm_ea0, rm_ea1, + rm_new_charge, 1e-9, 1e-9); + has_changed_cljff = true; + } + } + } + // update all of the perturbable constraints if (update_constraints) { @@ -1625,20 +1872,7 @@ double LambdaLever::setLambda(OpenMM::Context &context, const int nparams = morphed_bond_k.count(); - if (start_change_bond == -1) - { - start_change_bond = start_index; - end_change_bond = start_index + nparams; - } - else if (start_index < start_change_bond) - { - start_change_bond = start_index; - } - - if (start_index + nparams > end_change_bond) - { - end_change_bond = start_index + nparams; - } + has_changed_bondff |= cache.hasChanged("bond", "bond_k") || cache.hasChanged("bond", "bond_length"); for (int j = 0; j < nparams; ++j) { @@ -1648,11 +1882,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]); } } @@ -1674,20 +1908,7 @@ double LambdaLever::setLambda(OpenMM::Context &context, const int nparams = morphed_angle_k.count(); - if (start_change_angle == -1) - { - start_change_angle = start_index; - end_change_angle = start_index + nparams; - } - else if (start_index < start_change_angle) - { - start_change_angle = start_index; - } - - if (start_index + nparams > end_change_angle) - { - end_change_angle = start_index + nparams; - } + has_changed_angff |= cache.hasChanged("angle", "angle_k") || cache.hasChanged("angle", "angle_size"); for (int j = 0; j < nparams; ++j) { @@ -1697,13 +1918,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]); } } @@ -1727,20 +1948,7 @@ double LambdaLever::setLambda(OpenMM::Context &context, const auto is_improper = perturbable_mol.getIsImproper(); - if (start_change_torsion == -1) - { - start_change_torsion = start_index; - end_change_torsion = start_index + nparams; - } - else if (start_index < start_change_torsion) - { - start_change_torsion = start_index; - } - - if (start_index + nparams > end_change_torsion) - { - end_change_torsion = start_index + nparams; - } + has_changed_dihff |= rest2_changed || cache.hasChanged("torsion", "torsion_k") || cache.hasChanged("torsion", "torsion_phase"); for (int j = 0; j < nparams; ++j) { @@ -1781,66 +1989,270 @@ double LambdaLever::setLambda(OpenMM::Context &context, morphed_torsion_k[j] * scale); } } + + // update CMAP parameters for this perturbable molecule + start_index = start_idxs.value("cmap", -1); + + if (start_index != -1 and cmapff != 0) + { + const auto &grid0 = perturbable_mol.getCMAPGrids0(); + const auto &grid1 = perturbable_mol.getCMAPGrids1(); + const auto &sizes = perturbable_mol.getCMAPGridSizes(); + const auto &atoms = perturbable_mol.getCMAPAtoms(); + + // morph all grid values together using the lambda schedule + const auto morphed_grids = cache.morph( + schedule, + "cmap", "cmap_grid", + grid0, + grid1); + + if (rest2_changed or cache.hasChanged("cmap", "cmap_grid")) + { + has_changed_cmap = true; + + int offset = 0; + + for (int j = 0; j < sizes.count(); ++j) + { + const int N = sizes[j]; + const int map_size = N * N; + + // CMAP is always a proper backbone dihedral pair, never improper. + // Apply REST2 scaling if all 5 atoms are within the REST2 region. + double scale = 1.0; + + const auto &cmap_atms = atoms[j]; + + if (perturbable_mol.isRest2(boost::get<0>(cmap_atms)) and + perturbable_mol.isRest2(boost::get<1>(cmap_atms)) and + perturbable_mol.isRest2(boost::get<2>(cmap_atms)) and + perturbable_mol.isRest2(boost::get<3>(cmap_atms)) and + perturbable_mol.isRest2(boost::get<4>(cmap_atms))) + { + scale = rest2_scale; + } + + std::vector energy(map_size); + + for (int k = 0; k < map_size; ++k) + { + energy[k] = morphed_grids[offset + k] * scale; + } + + cmapff->setMapParameters(start_index + j, N, energy); + offset += map_size; + } + } + } } - // update the parameters in the context - const auto num_changed_atoms = end_change_atom - start_change_atom; - const auto num_changed_bonds = end_change_bond - start_change_bond; - const auto num_changed_angles = end_change_angle - start_change_angle; - const auto num_changed_torsions = end_change_torsion - start_change_torsion; - const auto num_changed_14 = end_change_14 - start_change_14; + // update the parameters in the context for forces whose parameters changed + if (has_changed_cljff and cljff) + cljff->updateParametersInContext(context); - if (num_changed_atoms > 0) + if (has_changed_ghostff) { - if (cljff) -#ifdef SIRE_HAS_UPDATE_SOME_IN_CONTEXT - cljff->updateSomeParametersInContext(start_change_atom, num_changed_atoms, context); -#else - cljff->updateParametersInContext(context); -#endif - if (ghost_ghostff) -#ifdef SIRE_HAS_UPDATE_SOME_IN_CONTEXT - ghost_ghostff->updateSomeParametersInContext(start_change_atom, num_changed_atoms, context); -#else ghost_ghostff->updateParametersInContext(context); -#endif - if (ghost_nonghostff) -#ifdef SIRE_HAS_UPDATE_SOME_IN_CONTEXT - ghost_nonghostff->updateSomeParametersInContext(start_change_atom, num_changed_atoms, context); -#else ghost_nonghostff->updateParametersInContext(context); -#endif } - if (ghost_14ff and num_changed_14 > 0) -#ifdef SIRE_HAS_UPDATE_SOME_IN_CONTEXT - ghost_14ff->updateSomeParametersInContext(start_change_14, num_changed_14, context); -#else +#ifdef SIRE_USE_CUSTOMVOLUMEFORCE + // Update the ghost LJ dispersion correction via the CustomVolumeForce. + // At r > rc the soft-core shift is negligible, so the standard closed-form + // LJ tail integral applies. Results are cached per lambda state. + auto ghost_lrc_ff = this->getForce("ghost-lrc", system); + if (ghost_lrc_ff != nullptr && ghost_ghostff != nullptr && ghost_nonghostff != nullptr) + { + const qint64 lam_key = qRound64(lambda_value * 1e5); + double lrc_coeff = 0.0; + + if (this->lrc_coeff_cache.contains(lam_key)) + { + lrc_coeff = this->lrc_coeff_cache[lam_key]; + } + else + { + const double cutoff = ghost_ghostff->getCutoffDistance(); + const double rc3 = cutoff * cutoff * cutoff; + const double rc9 = rc3 * rc3 * rc3; + const double four_pi = 4.0 * M_PI; + + // Interaction group sets: ghost atoms and non-ghost atoms. + std::set ghost_set, dummy_set, nonghost_set; + ghost_ghostff->getInteractionGroupParameters(0, ghost_set, dummy_set); + ghost_nonghostff->getInteractionGroupParameters(0, dummy_set, nonghost_set); + + // Cache half_sigma and two_sqrt_epsilon for each ghost atom. + QHash> ghost_params; + for (int i : ghost_set) + { + std::vector p; + ghost_ghostff->getParticleParameters(i, p); + ghost_params[i] = {p[1], p[2]}; // half_sigma, two_sqrt_epsilon + } + + // Cache non-ghost params. + QHash> nonghost_params; + for (int j : nonghost_set) + { + std::vector p; + ghost_nonghostff->getParticleParameters(j, p); + nonghost_params[j] = {p[1], p[2]}; + } + + // ghost-ghost unique pairs (i < j). + for (auto it_i = ghost_set.cbegin(); it_i != ghost_set.cend(); ++it_i) + { + const auto &pi = ghost_params[*it_i]; + auto it_j = it_i; + for (++it_j; it_j != ghost_set.cend(); ++it_j) + { + const auto &pj = ghost_params[*it_j]; + const double sig = pi.first + pj.first; + const double sig2 = sig * sig; + const double sig6 = sig2 * sig2 * sig2; + const double eps_pair = pi.second * pj.second; + lrc_coeff += four_pi * eps_pair * sig6 * (sig6 / (9.0 * rc9) - 1.0 / (3.0 * rc3)); + } + } + + // ghost-nonghost all pairs. + for (auto it_i = ghost_set.cbegin(); it_i != ghost_set.cend(); ++it_i) + { + const auto &pi = ghost_params[*it_i]; + for (int j : nonghost_set) + { + const auto &pj = nonghost_params[j]; + const double sig = pi.first + pj.first; + const double sig2 = sig * sig; + const double sig6 = sig2 * sig2 * sig2; + const double eps_pair = pi.second * pj.second; + lrc_coeff += four_pi * eps_pair * sig6 * (sig6 / (9.0 * rc9) - 1.0 / (3.0 * rc3)); + } + } + + this->lrc_coeff_cache[lam_key] = lrc_coeff; + } + + context.setParameter("lrc_coeff", lrc_coeff); + + // lrc_scale defaults to 1.0 (no effect). Schedules that fix epsilon + // (e.g. Beutler softcore) should set a force-specific equation for + // lever "lrc_scale" on force "ghost-lrc" to scale it to zero as the + // ghost is annihilated/decoupled. + double lrc_scale = this->lambda_schedule.morph( + "ghost-lrc", "lrc_scale", 1.0, 1.0, lambda_value); + context.setParameter("lrc_scale", lrc_scale); + } +#endif // SIRE_USE_CUSTOMVOLUMEFORCE + + // Update ring-breaking/making softcore force global parameters using the + // values pre-computed before the per-mol loop (rb_alpha/kappa, rm_alpha/kappa). + if (ring_breaking_ff != nullptr) + { + has_changed_ring_breaking_ff |= + (rb_alpha != context.getParameter("ring_break_alpha")); + context.setParameter("ring_break_alpha", rb_alpha); + } + + if (ring_making_ff != nullptr) + { + has_changed_ring_making_ff |= + (rm_alpha != context.getParameter("ring_make_alpha")); + context.setParameter("ring_make_alpha", rm_alpha); + } + + if (ring_breaking_ff and has_changed_ring_breaking_ff) + ring_breaking_ff->updateParametersInContext(context); + + if (ring_making_ff and has_changed_ring_making_ff) + ring_making_ff->updateParametersInContext(context); + +#ifdef SIRE_USE_CUSTOMVOLUMEFORCE + // Update the NonbondedForce (background) LRC via its own CustomVolumeForce. + // Ghost atoms have epsilon=0 in cljff so they contribute nothing naturally. + auto background_lrc_ff = this->getForce("background-lrc", system); + if (background_lrc_ff != nullptr && cljff != nullptr) + { + const qint64 lam_key = qRound64(lambda_value * 1e5); + double lrc_coeff = 0.0; + + if (this->background_lrc_coeff_cache.contains(lam_key)) + { + lrc_coeff = this->background_lrc_coeff_cache[lam_key]; + } + else + { + const double cutoff = cljff->getCutoffDistance(); + const double rc3 = cutoff * cutoff * cutoff; + const double rc9 = rc3 * rc3 * rc3; + const double four_pi = 4.0 * M_PI; + + // Classify particles by (sigma, epsilon); ghost atoms (epsilon=0), + // virtual sites, and GCMC water atoms are skipped. + std::map, int> class_counts; + for (int i = 0; i < cljff->getNumParticles(); ++i) + { + double charge, sigma, epsilon; + cljff->getParticleParameters(i, charge, sigma, epsilon); + if (epsilon == 0.0) + continue; + if (!gcmc_water_atoms.isEmpty() && gcmc_water_atoms.contains(i)) + continue; + class_counts[{sigma, epsilon}]++; + } + + // Diagonal pairs (same class, unique i(n) * (n - 1) * 0.5; + if (n_pairs == 0.0) + continue; + const double sig2 = key.first * key.first; + const double sig6 = sig2 * sig2 * sig2; + const double eps_pair = 4.0 * key.second; + lrc_coeff += n_pairs * four_pi * eps_pair * sig6 * (sig6 / (9.0 * rc9) - 1.0 / (3.0 * rc3)); + } + + // Off-diagonal pairs (class i != class j). + for (auto it1 = class_counts.cbegin(); it1 != class_counts.cend(); ++it1) + { + auto it2 = it1; + for (++it2; it2 != class_counts.cend(); ++it2) + { + const double sigma_ij = 0.5 * (it1->first.first + it2->first.first); + const double eps_pair = 4.0 * std::sqrt(it1->first.second * it2->first.second); + const double n_pairs = static_cast(it1->second) * it2->second; + const double sig2 = sigma_ij * sigma_ij; + const double sig6 = sig2 * sig2 * sig2; + lrc_coeff += n_pairs * four_pi * eps_pair * sig6 * (sig6 / (9.0 * rc9) - 1.0 / (3.0 * rc3)); + } + } + + this->background_lrc_coeff_cache[lam_key] = lrc_coeff; + } + + context.setParameter("lrc_background", lrc_coeff); + } +#endif // SIRE_USE_CUSTOMVOLUMEFORCE + + if (ghost_14ff and has_changed_ghost14ff) ghost_14ff->updateParametersInContext(context); -#endif - if (bondff and num_changed_bonds > 0) -#ifdef SIRE_HAS_UPDATE_SOME_IN_CONTEXT - bondff->updateSomeParametersInContext(start_change_bond, num_changed_bonds, context); -#else + if (bondff and has_changed_bondff) bondff->updateParametersInContext(context); -#endif - if (angff and num_changed_angles > 0) -#ifdef SIRE_HAS_UPDATE_SOME_IN_CONTEXT - angff->updateSomeParametersInContext(start_change_angle, num_changed_angles, context); -#else + if (angff and has_changed_angff) angff->updateParametersInContext(context); -#endif - if (dihff and num_changed_torsions > 0) -#ifdef SIRE_HAS_UPDATE_SOME_IN_CONTEXT - dihff->updateSomeParametersInContext(start_change_torsion, num_changed_torsions, context); -#else + if (dihff and has_changed_dihff) dihff->updateParametersInContext(context); -#endif + + if (cmapff and has_changed_cmap) + cmapff->updateParametersInContext(context); // now update any restraints that are scaled for (const auto &restraint : this->name_to_restraintidx.keys()) @@ -1853,11 +2265,18 @@ double LambdaLever::setLambda(OpenMM::Context &context, 1.0, 1.0, lambda_value); - for (auto &ff : this->getRestraints(restraint, system)) + const double prev_rho = last_restraint_rho.value(restraint, -1.0); + last_restraint_rho[restraint] = rho; + last_changed_forces[restraint] = (rho != prev_rho); + + if (rho != prev_rho) { - if (ff != 0) + for (auto &ff : this->getRestraints(restraint, system)) { - this->updateRestraintInContext(*ff, rho, context); + if (ff != 0) + { + this->updateRestraintInContext(*ff, rho, context); + } } } } @@ -1871,6 +2290,23 @@ double LambdaLever::setLambda(OpenMM::Context &context, context.reinitialize(true); } + // record which named forces had parameters changed in this call + last_changed_forces["clj"] = has_changed_cljff; + last_changed_forces["ghost/ghost"] = has_changed_ghostff; + last_changed_forces["ghost/non-ghost"] = has_changed_ghostff; + last_changed_forces["ghost-lrc"] = has_changed_ghostff; + last_changed_forces["background-lrc"] = has_changed_cljff; + last_changed_forces["gcmc-lrc"] = false; + last_changed_forces["ghost-14"] = has_changed_ghost14ff; + last_changed_forces["ring-break"] = has_changed_ring_breaking_ff; + last_changed_forces["ring-make"] = has_changed_ring_making_ff; + last_changed_forces["bond"] = has_changed_bondff; + last_changed_forces["angle"] = has_changed_angff; + last_changed_forces["torsion"] = has_changed_dihff; + last_changed_forces["cmap"] = has_changed_cmap; + if (qmff == 0) + last_changed_forces["qmff"] = false; + return lambda_value; } diff --git a/wrapper/Convert/SireOpenMM/lambdalever.h b/wrapper/Convert/SireOpenMM/lambdalever.h index b37a446e1..886e436db 100644 --- a/wrapper/Convert/SireOpenMM/lambdalever.h +++ b/wrapper/Convert/SireOpenMM/lambdalever.h @@ -49,6 +49,7 @@ namespace SireOpenMM public: MolLambdaCache(); MolLambdaCache(double lam_val); + MolLambdaCache(double lam_val, const MolLambdaCache &prev); MolLambdaCache(const MolLambdaCache &other); ~MolLambdaCache(); @@ -65,9 +66,14 @@ namespace SireOpenMM const QVector &initial, const QVector &final) const; + bool hasChanged(const QString &force, const QString &key) const; + bool hasChanged(const QString &force, const QString &key, + const QString &subkey) const; + private: QHash> cache; - QReadWriteLock lock; + QHash> prev_cache; + mutable QReadWriteLock lock; double lam_val; }; @@ -86,6 +92,7 @@ namespace SireOpenMM private: QHash> cache; + QHash prev_lam_vals; }; /** This is a lever that is used to change the parameters in an OpenMM @@ -152,6 +159,14 @@ namespace SireOpenMM QString getForceType(const QString &name, const OpenMM::System &system) const; + void setForceGroup(const QString &name, int group_idx); + void setRestraintForceGroup(const QString &name, int group_idx); + int getForceGroup(const QString &name) const; + QStringList getForceNames() const; + bool wasForceChanged(const QString &name) const; + + void setGCMCWaterAtoms(const QVector &atoms); + protected: void updateRestraintInContext(OpenMM::Force &ff, double rho, OpenMM::Context &context) const; @@ -163,6 +178,10 @@ namespace SireOpenMM * Note that multiple restraints can have the same name */ QMultiHash name_to_restraintidx; + /** Map from a force or restraint name to its OpenMM force group index. + * Multiple restraint forces sharing the same name share one group. */ + QHash name_to_groupidx; + /** The schedule used to set lambda */ SireCAS::LambdaSchedule lambda_schedule; @@ -178,6 +197,40 @@ namespace SireOpenMM /** Cache of the parameters for different lambda values */ LeverCache lambda_cache; + + /** Records the rho value used for each restraint in the last setLambda + * call, so we can avoid redundant updateRestraintInContext calls. */ + mutable QHash last_restraint_rho; + + /** Records the REST2 scale factor used in the last setLambda call, + * so we can detect when it changes (REST2 scaling is applied on top + * of the morphed parameters, so a change requires re-uploading + * parameters even if morphed values are unchanged). */ + mutable double last_rest2_scale; + + /** Records which forces had parameters changed in the last setLambda + * call. Mutable so it can be updated from the const setLambda method. */ + mutable QHash last_changed_forces; + + /** Records the morphed qmff lambda value from the last setLambda call, + * so we can detect when it actually changes. Initialised to -1 as a + * sentinel meaning "never been set". */ + mutable double last_qmff_lam; + + /** Cache of pre-computed ghost LJ dispersion coefficients keyed by + * rounded lambda (qRound64(lambda * 1e5)). Populated on first visit + * to each lambda state and reused on warm passes. */ + mutable QHash lrc_coeff_cache; + + /** Cache of pre-computed background (NonbondedForce) LJ dispersion + * coefficients keyed by rounded lambda. Mirrors lrc_coeff_cache but + * covers all non-ghost atoms in the clj force. */ + mutable QHash background_lrc_coeff_cache; + + /** OpenMM atom indices that belong to GCMC water molecules. When + * non-empty these atoms are excluded from background-lrc (their LRC + * is handled by the gcmc-lrc CustomVolumeForce instead). */ + QSet gcmc_water_atoms; }; #ifndef SIRE_SKIP_INLINE_FUNCTION @@ -206,6 +259,14 @@ namespace SireOpenMM return "OpenMM::CustomCVForce"; } +#ifdef SIRE_USE_CUSTOMVOLUMEFORCE + template <> + inline QString _get_typename() + { + return "OpenMM::CustomVolumeForce"; + } +#endif + template <> inline QString _get_typename() { diff --git a/wrapper/Convert/SireOpenMM/openmmmolecule.cpp b/wrapper/Convert/SireOpenMM/openmmmolecule.cpp index 4552afb66..78b53999f 100644 --- a/wrapper/Convert/SireOpenMM/openmmmolecule.cpp +++ b/wrapper/Convert/SireOpenMM/openmmmolecule.cpp @@ -1,21 +1,20 @@ - #include "openmmmolecule.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/amberparams.h" #include "SireMM/atomljs.h" #include "SireMM/selectorbond.h" -#include "SireMM/amberparams.h" #include "SireMM/twoatomfunctions.h" #include "SireMaths/vector.h" @@ -67,6 +66,8 @@ OpenMMMolecule::OpenMMMolecule(const Molecule &mol, return; } + // Set up virtual site properties + bool is_perturbable = false; bool swap_end_states = false; @@ -89,6 +90,26 @@ OpenMMMolecule::OpenMMMolecule(const Molecule &mol, ffinfo = mol.property(map["forcefield"]).asA(); } + if (mol.hasProperty("n_virtual_sites") and mol.property("n_virtual_sites").asAnInteger() > 0) + { + this->has_vs = true; + this->vs_parents = mol.property("parents").asA(); + this->vs_properties = mol.property("virtual_sites").asA(); + this->n_vs = mol.property("n_virtual_sites").asAnInteger(); + if (is_perturbable) + { + this->vs_charges = mol.property("vs_charges0").asAnArray(); + } + else + { + this->vs_charges = mol.property("vs_charges").asAnArray(); + } + } + else + { + this->has_vs = false; + } + if (map.specified("constraint")) { const auto c = map["constraint"].source().toLower().simplified().replace("_", "-"); @@ -257,11 +278,11 @@ OpenMMMolecule::OpenMMMolecule(const Molecule &mol, // its current coordinates (which should represent the // current lambda state) QStringList props = {"LJ", "ambertype", "angle", "atomtype", - "bond", "charge", + "bond", "charge", "cmap", "dihedral", "element", "forcefield", "gb_radii", "gb_screening", "improper", "intrascale", "mass", "name", - "parameters", "treechain"}; + "parameters", "treechain", "vs_charges"}; // we can't specialise these globally in case other molecules // are not of amber type @@ -273,6 +294,36 @@ OpenMMMolecule::OpenMMMolecule(const Molecule &mol, std::swap(map0, map1); } + // Read ring-breaking/making bond pairs from molecule properties, + // swapping them if end states are inverted so that the members + // always reflect the λ=0/λ=1 convention of the (possibly swapped) + // end states. + auto read_ring_pairs = [&](const QString &prop_name) + { + QVector> pairs; + if (mol.hasProperty(prop_name)) + { + const auto &flat = mol.property(prop_name) + .asA() + .toVector(); + pairs.reserve(flat.count() / 2); + for (int k = 0; k + 1 < flat.count(); k += 2) + pairs.append(QPair(flat[k], flat[k + 1])); + } + return pairs; + }; + + if (swap_end_states) + { + this->ring_breaking_pairs = read_ring_pairs("ring_making_bonds"); + this->ring_making_pairs = read_ring_pairs("ring_breaking_bonds"); + } + else + { + this->ring_breaking_pairs = read_ring_pairs("ring_breaking_bonds"); + this->ring_making_pairs = read_ring_pairs("ring_making_bonds"); + } + // save this perturbable map - this will help us set // new properties from the results of dynamics, e.g. // updating coordinates after minimisation @@ -563,6 +614,13 @@ void OpenMMMolecule::constructFromAmber(const Molecule &mol, check_for_h_by_max_mass = map["check_for_h_by_max_mass"].value().asABoolean(); } + double max_h_mass = 3.5; + + if (map.specified("max_h_mass")) + { + max_h_mass = map["max_h_mass"].value().asADouble(); + } + bool check_for_h_by_element = true; if (map.specified("check_for_h_by_element")) @@ -599,8 +657,8 @@ void OpenMMMolecule::constructFromAmber(const Molecule &mol, const double mass0 = params_masses.at(cgatomidx).to(SireUnits::g_per_mol); const double mass1 = params1_masses.at(cgatomidx).to(SireUnits::g_per_mol); - const bool mass0_is_light = (mass0 >= 1) and (mass0 < 2.5); - const bool mass1_is_light = (mass1 >= 1) and (mass1 < 2.5); + const bool mass0_is_light = (mass0 >= 1) and (mass0 < max_h_mass); + const bool mass1_is_light = (mass1 >= 1) and (mass1 < max_h_mass); double mass = std::max(mass0, mass1); @@ -614,9 +672,9 @@ void OpenMMMolecule::constructFromAmber(const Molecule &mol, // this must be a ghost in both end states? light_atoms.insert(i); } - else if (check_for_h_by_max_mass and mass < 2.5) + else if (check_for_h_by_max_mass and mass < max_h_mass) { - // the maximum mass is less than 2.5, so this is a H + // the maximum mass is less than max_h_mass, so this is a H light_atoms.insert(i); } else if (check_for_h_by_mass and (mass0_is_light or mass1_is_light)) @@ -674,12 +732,12 @@ void OpenMMMolecule::constructFromAmber(const Molecule &mol, // this must be a ghost light_atoms.insert(i); } - else if (check_for_h_by_max_mass and mass < 2.5) + else if (check_for_h_by_max_mass and mass < max_h_mass) { - // the maximum mass is less than 2.5, so this is a H + // the maximum mass is less than max_h_mass, so this is a H light_atoms.insert(i); } - else if (check_for_h_by_mass and (mass >= 1 and mass < 2.5)) + else if (check_for_h_by_mass and (mass >= 1 and mass < max_h_mass)) { // one of the atoms is H or He light_atoms.insert(i); @@ -701,7 +759,13 @@ void OpenMMMolecule::constructFromAmber(const Molecule &mol, const auto params_charges = params.charges(); const auto params_ljs = params.ljs(); - this->cljs = QVector>(nats, boost::make_tuple(0.0, 0.0, 0.0)); + int n_params = nats; + if (this->has_vs) + { + n_params += this->n_vs; + } + this->cljs = QVector>(n_params, boost::make_tuple(0.0, 0.0, 0.0)); + auto cljs_data = cljs.data(); for (int i = 0; i < nats; ++i) @@ -730,6 +794,19 @@ void OpenMMMolecule::constructFromAmber(const Molecule &mol, cljs_data[i] = boost::make_tuple(chg, sig, eps); } + if (this->has_vs) + { + auto vs_charges = mol.property(map["vs_charges"]).asAnArray(); + for (int vs = 0; vs < this->n_vs; ++vs) + { + double chg = vs_charges.at(vs).asADouble(); + double sig = 1e-9; + double eps = 0.0; + + cljs_data[nats + vs] = boost::make_tuple(chg, sig, eps); + } + } + this->bond_params.clear(); this->constraints.clear(); this->perturbable_constraints.clear(); @@ -742,6 +819,28 @@ void OpenMMMolecule::constructFromAmber(const Molecule &mol, this->unbonded_atoms.insert(i); } + // Detect virtual site (extra point) atoms by name. + // Only 4- and 5-atom molecules can have EP atoms (TIP4P, OPC, TIP5P). + // Supported names: AMBER EPW / EP1 / EP2, GROMACS MW / LP1 / LP2. + // We remove them from unbonded_atoms so that buildExceptions does + // not try to add an erroneous constraint for them. + static const QSet vsite_names = {"EPW", "EP1", "EP2", "MW", "LP1", "LP2"}; + + QSet vsite_idxs; + + if (nats == 4 or nats == 5) + { + for (int i = 0; i < nats; ++i) + { + if (masses_data[i] < 0.05 and + vsite_names.contains(mol.atom(SireMol::AtomIdx(i)).name().value())) + { + vsite_idxs.insert(i); + unbonded_atoms.remove(i); + } + } + } + // now the bonds const double bond_k_to_openmm = 2.0 * (SireUnits::kcal_per_mol / (SireUnits::angstrom * SireUnits::angstrom)).to(SireUnits::kJ_per_mol / (SireUnits::nanometer * SireUnits::nanometer)); const double bond_r0_to_openmm = SireUnits::angstrom.to(SireUnits::nanometer); @@ -796,6 +895,19 @@ void OpenMMMolecule::constructFromAmber(const Molecule &mol, auto bonds = params.bonds(); + // Whether an atom's amber type changes between end states - used below + // to identify bonds that are part of the alchemical junction. Amber + // type, not charge, since charges can be redistributed across the whole + // ligand without affecting bonded parameters. + const auto params1_ambertypes = params1.amberTypes(); + + auto is_perturbing_atom = [&](int atom_idx) -> bool + { + const auto &cgatomidx = idx_to_cgatomidx_data[atom_idx]; + + return ambertypes.at(cgatomidx) != params1_ambertypes.at(cgatomidx); + }; + if (is_perturbable) { // add in any bonds that exist only in the other state @@ -825,6 +937,11 @@ void OpenMMMolecule::constructFromAmber(const Molecule &mol, if (atom0 > atom1) std::swap(atom0, atom1); + // Skip bonds involving virtual site atoms: their positions are + // determined by OpenMM's VirtualSite machinery, not a HarmonicBondForce. + if (vsite_idxs.contains(atom0) or vsite_idxs.contains(atom1)) + continue; + const double k = bondparam.k() * bond_k_to_openmm; const double r0 = bondparam.r0() * bond_r0_to_openmm; double r0_1 = r0; @@ -848,6 +965,34 @@ void OpenMMMolecule::constructFromAmber(const Molecule &mol, bool bond_is_not_constrained = true; bool should_constrain_bond = false; + // Whether the bond's own k/r0 change between end states. + bool bond_is_perturbing = false; + + // Whether the bond is part of the alchemical junction (touches a + // ghost or transmuting atom). SOMD1 constrains an unperturbing bond + // unconditionally only if it's part of the perturbation; ordinary + // unperturbed bonds elsewhere in the molecule are not affected. + bool bond_is_ghost_junction = false; + + if (is_perturbable) + { + const auto bondparam1 = params1.bonds().value(it.key()).first; + + double k_1 = bondparam1.k() * bond_k_to_openmm; + r0_1 = bondparam1.r0() * bond_r0_to_openmm; + + if (r0_1 == 0) + { + // we cannot shrink the bond to 0 - this must be + // a bond that is disappearing - we should simply + // keep it the same length + r0_1 = r0; + } + + bond_is_perturbing = (std::abs(k_1 - k) > 1e-3 or std::abs(r0_1 - r0) > 1e-3); + bond_is_ghost_junction = is_perturbing_atom(atom0) or is_perturbing_atom(atom1); + } + if (this_constraint_type == CONSTRAIN_AUTO_BONDS) { // constrain the bond if its predicted vibrational frequency is less than @@ -869,45 +1014,31 @@ void OpenMMMolecule::constructFromAmber(const Molecule &mol, should_constrain_bond = vibrational_period < auto_constraints_factor * timestep_in_fs; } else if ((not has_massless_atom) and ((this_constraint_type & CONSTRAIN_BONDS) or - (has_light_atom and (this_constraint_type & CONSTRAIN_HBONDS)))) + (has_light_atom and (this_constraint_type & CONSTRAIN_HBONDS)) or + (is_perturbable and not bond_is_perturbing and + bond_is_ghost_junction and + (this_constraint_type & CONSTRAIN_HBONDS)))) { should_constrain_bond = true; - if (is_perturbable) + if (is_perturbable and bond_is_perturbing) { - // we need to see if this bond is being perturbed - const auto bondparam1 = params1.bonds().value(it.key()).first; - - double k_1 = bondparam1.k() * bond_k_to_openmm; - r0_1 = bondparam1.r0() * bond_r0_to_openmm; - - if (r0_1 == 0) + // we need to check against the "NOT_PERTURBED"-style constraints + if (this_constraint_type & CONSTRAIN_NOT_PERTURBED) { - // we cannot shrink the bond to 0 - this must be - // a bond that is disappearing - we should simply - // keep it the same length - r0_1 = r0; + // DO NOT CONSTRAIN any perturbing bonds + should_constrain_bond = false; } - - if (std::abs(k_1 - k) > 1e-3 or std::abs(r0_1 - r0) > 1e-3) + else if (this_constraint_type & CONSTRAIN_NOT_HEAVY_PERTURBED) { - // we need to check against the "NOT_PERTURBED"-style constraints - if (this_constraint_type & CONSTRAIN_NOT_PERTURBED) - { - // DO NOT CONSTRAIN any perturbing bonds - should_constrain_bond = false; - } - else if (this_constraint_type & CONSTRAIN_NOT_HEAVY_PERTURBED) - { - // DO NOT CONSTRAIN any perturbing bonds that DON'T contain hydrogen - should_constrain_bond = has_light_atom; - } - else - { - // DO CONSTRAIN any perturbing bonds - we only don't constrain - // perturbing bonds if we have one of the "NOT_PERTURBED" constraints - should_constrain_bond = true; - } + // DO NOT CONSTRAIN any perturbing bonds that DON'T contain hydrogen + should_constrain_bond = has_light_atom; + } + else + { + // DO CONSTRAIN any perturbing bonds - we only don't constrain + // perturbing bonds if we have one of the "NOT_PERTURBED" constraints + should_constrain_bond = true; } } } @@ -972,6 +1103,10 @@ void OpenMMMolecule::constructFromAmber(const Molecule &mol, if (atom0 > atom2) std::swap(atom0, atom2); + // Skip angles involving virtual site atoms. + if (vsite_idxs.contains(atom0) or vsite_idxs.contains(atom1) or vsite_idxs.contains(atom2)) + continue; + const double k = angparam.k() * angle_k_to_openmm; const double theta0 = angparam.theta0(); // already in radians @@ -1166,6 +1301,222 @@ void OpenMMMolecule::constructFromAmber(const Molecule &mol, } } + // now the CMAP terms + cmap_params.clear(); + + const auto cmap_funcs = params.cmapFunctions(); + + if (not cmap_funcs.isEmpty()) + { + for (const auto &func : cmap_funcs.parameters()) + { + const int atom0 = molinfo.atomIdx(func.atom0()).value(); + const int atom1 = molinfo.atomIdx(func.atom1()).value(); + const int atom2 = molinfo.atomIdx(func.atom2()).value(); + const int atom3 = molinfo.atomIdx(func.atom3()).value(); + const int atom4 = molinfo.atomIdx(func.atom4()).value(); + + cmap_params.append(boost::make_tuple(atom0, atom1, atom2, atom3, atom4, + func.parameter())); + } + } + + if (is_perturbable) + { + // Add any CMAP terms that exist only in the other state, + // using the same atoms but a zero grid for this (missing) state. + const auto cmap_funcs1 = params1.cmapFunctions(); + + for (const auto &func1 : cmap_funcs1.parameters()) + { + const int atom0 = molinfo.atomIdx(func1.atom0()).value(); + const int atom1 = molinfo.atomIdx(func1.atom1()).value(); + const int atom2 = molinfo.atomIdx(func1.atom2()).value(); + const int atom3 = molinfo.atomIdx(func1.atom3()).value(); + const int atom4 = molinfo.atomIdx(func1.atom4()).value(); + + bool found = false; + + for (const auto &existing : cmap_params) + { + if (boost::get<0>(existing) == atom0 and + boost::get<1>(existing) == atom1 and + boost::get<2>(existing) == atom2 and + boost::get<3>(existing) == atom3 and + boost::get<4>(existing) == atom4) + { + found = true; + break; + } + } + + if (not found) + { + const int N = func1.parameter().nRows(); + const SireBase::Array2D null_grid(N, N, 0.0); + cmap_params.append(boost::make_tuple(atom0, atom1, atom2, atom3, atom4, + SireMM::CMAPParameter(null_grid))); + } + } + } + + // Build virtual site definitions for any extra-point atoms detected above. + // Weights are derived from the actual atomic coordinates so that any 4- or + // 5-point water model (OPC, TIP4P, TIP5P, …) is handled automatically. + virtual_sites.clear(); + + if (not vsite_idxs.isEmpty()) + { + // Map atom name → molecule-local index (first match wins). + QHash name_to_idx; + + for (int i = 0; i < nats; ++i) + { + const QString name = mol.atom(SireMol::AtomIdx(i)).name().value(); + + if (not name_to_idx.contains(name)) + name_to_idx.insert(name, i); + } + + auto find_idx = [&](std::initializer_list candidates) -> int + { + for (const char *n : candidates) + { + auto it = name_to_idx.find(QString(n)); + + if (it != name_to_idx.end()) + return it.value(); + } + + return -1; + }; + + const int o_idx = find_idx({"O", "OW"}); + const int h1_idx = find_idx({"H1", "HW1"}); + const int h2_idx = find_idx({"H2", "HW2"}); + + if (o_idx < 0 or h1_idx < 0 or h2_idx < 0) + { + throw SireError::incompatible_error( + QObject::tr("Molecule %1 contains virtual site atoms (%2) but the expected " + "parent atoms (O/OW, H1/HW1, H2/HW2) could not be found. " + "Cannot register virtual sites for this molecule.") + .arg(number.toString()) + .arg(vsite_idxs.count()), + CODELOC); + } + else + { + const auto &O = coords[o_idx]; + const auto &H1 = coords[h1_idx]; + const auto &H2 = coords[h2_idx]; + + // Bisector vector in the molecular plane: (H1-O) + (H2-O) + const double bx = (H1[0] - O[0]) + (H2[0] - O[0]); + const double by = (H1[1] - O[1]) + (H2[1] - O[1]); + const double bz = (H1[2] - O[2]) + (H2[2] - O[2]); + const double bisect_sq = bx * bx + by * by + bz * bz; + + // Cross product (H1-O) x (H2-O) — needed for out-of-plane sites (TIP5P) + const double d1x = H1[0] - O[0], d1y = H1[1] - O[1], d1z = H1[2] - O[2]; + const double d2x = H2[0] - O[0], d2y = H2[1] - O[1], d2z = H2[2] - O[2]; + const double cx = d1y * d2z - d1z * d2y; + const double cy = d1z * d2x - d1x * d2z; + const double cz = d1x * d2y - d1y * d2x; + const double cross_sq = cx * cx + cy * cy + cz * cz; + + // ── 4-point water (OPC, TIP4P): single EP on the bisector ────── + const int ep_idx = find_idx({"EPW", "MW"}); + + if (ep_idx >= 0 and vsite_idxs.contains(ep_idx) and bisect_sq > 1e-20) + { + const auto &EP = coords[ep_idx]; + const double ex = EP[0] - O[0]; + const double ey = EP[1] - O[1]; + const double ez = EP[2] - O[2]; + const double a = (ex * bx + ey * by + ez * bz) / bisect_sq; + + VirtualSiteInfo vs; + vs.type = VirtualSiteInfo::ThreeParticleAverage; + vs.vsite_idx = ep_idx; + vs.p1_idx = o_idx; + vs.p2_idx = h1_idx; + vs.p3_idx = h2_idx; + vs.w1 = 1.0 - 2.0 * a; + vs.w2 = a; + vs.w3 = a; + vs.w12 = vs.w13 = vs.wCross = 0.0; + virtual_sites.append(vs); + } + + // ── 5-point water (TIP5P): two out-of-plane EPs ───────────────── + const int ep1_idx = find_idx({"EP1", "LP1"}); + const int ep2_idx = find_idx({"EP2", "LP2"}); + + if (ep1_idx >= 0 and ep2_idx >= 0 and + vsite_idxs.contains(ep1_idx) and vsite_idxs.contains(ep2_idx) and + bisect_sq > 1e-20 and cross_sq > 1e-20) + { + const auto &EP1 = coords[ep1_idx]; + const auto &EP2 = coords[ep2_idx]; + + // a = dot((EP1+EP2)/2 - O, bisect) / bisect_sq + const double mx = 0.5 * (EP1[0] + EP2[0]) - O[0]; + const double my = 0.5 * (EP1[1] + EP2[1]) - O[1]; + const double mz = 0.5 * (EP1[2] + EP2[2]) - O[2]; + const double a = (mx * bx + my * by + mz * bz) / bisect_sq; + + // b = dot(EP1-EP2, cross) / (2 * cross_sq) + const double dx = EP1[0] - EP2[0]; + const double dy = EP1[1] - EP2[1]; + const double dz = EP1[2] - EP2[2]; + const double b = (dx * cx + dy * cy + dz * cz) / (2.0 * cross_sq); + + // EP1: wCross = +b + { + VirtualSiteInfo vs; + vs.type = VirtualSiteInfo::OutOfPlane; + vs.vsite_idx = ep1_idx; + vs.p1_idx = o_idx; + vs.p2_idx = h1_idx; + vs.p3_idx = h2_idx; + vs.w1 = vs.w2 = vs.w3 = 0.0; + vs.w12 = a; + vs.w13 = a; + vs.wCross = b; + virtual_sites.append(vs); + } + + // EP2: wCross = -b + { + VirtualSiteInfo vs; + vs.type = VirtualSiteInfo::OutOfPlane; + vs.vsite_idx = ep2_idx; + vs.p1_idx = o_idx; + vs.p2_idx = h1_idx; + vs.p3_idx = h2_idx; + vs.w1 = vs.w2 = vs.w3 = 0.0; + vs.w12 = a; + vs.w13 = a; + vs.wCross = -b; + virtual_sites.append(vs); + } + } + } + + if (virtual_sites.size() != vsite_idxs.count()) + { + throw SireError::incompatible_error( + QObject::tr("Molecule %1: detected %2 virtual site atom(s) but only " + "registered %3. Check atom geometry (degenerate coordinates?) " + "or atom naming conventions.") + .arg(number.toString()) + .arg(vsite_idxs.count()) + .arg(virtual_sites.size()), + CODELOC); + } + } + this->buildExceptions(mol, constrained_pairs, map); } @@ -1207,7 +1558,7 @@ void OpenMMMolecule::alignInternals(const PropertyMap &map) this->perturbed->alphas = this->alphas; this->perturbed->kappas = this->kappas; - for (int i = 0; i < cljs.count(); ++i) + for (int i = 0; i < this->nAtoms(); ++i) { const auto &clj0 = cljs.at(i); const auto &clj1 = perturbed->cljs.at(i); @@ -1231,6 +1582,19 @@ void OpenMMMolecule::alignInternals(const PropertyMap &map) // kappa is 1 for both end states for ghost atoms this->kappas[i] = 1.0; this->perturbed->kappas[i] = 1.0; + + if (this->has_vs) + { + auto atom_vs = this->vs_parents.property(std::to_string(i).c_str()).asAnArray(); + for (int vs = 0; vs < atom_vs.size(); ++vs) + { + int vs_index = this->nAtoms() + atom_vs.at(vs).asAnInteger(); + from_ghost_idxs.insert(vs_index); + this->alphas[vs_index] = 1.0; + this->kappas[vs_index] = 1.0; + this->perturbed->kappas[vs_index] = 1.0; + } + } } } else if (is_ghost(clj1)) @@ -1244,6 +1608,19 @@ void OpenMMMolecule::alignInternals(const PropertyMap &map) // kappa is 1 for both end states for ghost atoms this->kappas[i] = 1.0; this->perturbed->kappas[i] = 1.0; + + if (this->has_vs) + { + auto atom_vs = this->vs_parents.property(std::to_string(i).c_str()).asAnArray(); + for (int vs = 0; vs < atom_vs.size(); ++vs) + { + int vs_index = this->nAtoms() + atom_vs.at(vs).asAnInteger(); + to_ghost_idxs.insert(vs_index); + this->perturbed->alphas[vs_index] = 1.0; + this->kappas[vs_index] = 1.0; + this->perturbed->kappas[vs_index] = 1.0; + } + } } } } @@ -1553,6 +1930,89 @@ void OpenMMMolecule::alignInternals(const PropertyMap &map) .arg(perturbed->exception_params.count()), CODELOC); } + + // align the CMAP parameters between the two end states + QVector> cmap_params_1; + cmap_params_1.reserve(cmap_params.count()); + + found_index_0 = QVector(cmap_params.count(), false); + found_index_1 = QVector(perturbed->cmap_params.count(), false); + + for (int i = 0; i < cmap_params.count(); ++i) + { + const auto &cmap0 = cmap_params.at(i); + + const int atom0 = boost::get<0>(cmap0); + const int atom1 = boost::get<1>(cmap0); + const int atom2 = boost::get<2>(cmap0); + const int atom3 = boost::get<3>(cmap0); + const int atom4 = boost::get<4>(cmap0); + + bool found = false; + + for (int j = 0; j < perturbed->cmap_params.count(); ++j) + { + if (not found_index_1[j]) + { + const auto &cmap1 = perturbed->cmap_params.at(j); + + if (boost::get<0>(cmap1) == atom0 and + boost::get<1>(cmap1) == atom1 and + boost::get<2>(cmap1) == atom2 and + boost::get<3>(cmap1) == atom3 and + boost::get<4>(cmap1) == atom4) + { + cmap_params_1.append(cmap1); + found_index_0[i] = true; + found_index_1[j] = true; + found = true; + break; + } + } + } + + if (not found) + { + // add a null CMAP (zero grid) for the missing perturbed state + found_index_0[i] = true; + const int N = boost::get<5>(cmap0).nRows(); + const SireBase::Array2D null_grid(N, N, 0.0); + cmap_params_1.append(boost::make_tuple(atom0, atom1, atom2, atom3, atom4, + SireMM::CMAPParameter(null_grid))); + } + } + + for (int j = 0; j < perturbed->cmap_params.count(); ++j) + { + if (not found_index_1[j]) + { + // add a CMAP term missing in the reference state + const auto &cmap1 = perturbed->cmap_params.at(j); + + const int atom0 = boost::get<0>(cmap1); + const int atom1 = boost::get<1>(cmap1); + const int atom2 = boost::get<2>(cmap1); + const int atom3 = boost::get<3>(cmap1); + const int atom4 = boost::get<4>(cmap1); + + // add a null CMAP to the reference state + const int N = boost::get<5>(cmap1).nRows(); + const SireBase::Array2D null_grid(N, N, 0.0); + cmap_params.append(boost::make_tuple(atom0, atom1, atom2, atom3, atom4, + SireMM::CMAPParameter(null_grid))); + cmap_params_1.append(cmap1); + found_index_1[j] = true; + } + } + + if (found_index_0.indexOf(false) != -1 or found_index_1.indexOf(false) != -1) + { + throw SireError::program_bug(QObject::tr( + "Failed to align the CMAP parameters!"), + CODELOC); + } + + perturbed->cmap_params = cmap_params_1; } /** Internal function that builds all of the exceptions for all of the @@ -1571,7 +2031,7 @@ void OpenMMMolecule::buildExceptions(const Molecule &mol, if (unbonded_atoms.isEmpty()) unbonded_atoms = QSet(); - const int nats = this->cljs.count(); + const int nats = this->atoms.count(); const auto &nbpairs = mol.property(map["intrascale"]).asA(); @@ -1805,6 +2265,45 @@ void OpenMMMolecule::buildExceptions(const Molecule &mol, } } } + + // Add virtual site exceptions + if (this->has_vs) + { + int n_exceptions = exception_params.count(); + for (int exc = 0; exc < n_exceptions; ++exc) + { + const auto ¶m = exception_params[exc]; + const auto atom0 = boost::get<0>(param); + const auto atom1 = boost::get<1>(param); + const auto coul_14_scale = boost::get<2>(param); + const auto lj_14_scale = boost::get<3>(param); + + int n_atoms = this->coords.count(); + + auto vs_on_0 = vs_parents.property(std::to_string(atom0).c_str()).asAnArray(); + auto vs_on_1 = vs_parents.property(std::to_string(atom1).c_str()).asAnArray(); + + // Not add_exception because we don't want to add constraints between virtual sites + // Scaling factors are inherited from the parent atom exception + for (int vs0 = 0; vs0 < vs_on_0.size(); ++vs0) + { + int vs0_index = vs_on_0.at(vs0).asAnInteger() + n_atoms; + exception_params.append(boost::make_tuple(vs0_index, atom1, coul_14_scale, lj_14_scale)); + for (int vs1 = 0; vs1 < vs_on_1.size(); ++vs1) + { + int vs1_index = vs_on_1.at(vs1).asAnInteger() + n_atoms; + exception_params.append(boost::make_tuple(vs0_index, vs1_index, coul_14_scale, lj_14_scale)); + } + } + + // We need an additional loop for the case where atom 0 has no virtual sites + for (int vs1 = 0; vs1 < vs_on_1.size(); ++vs1) + { + int vs1_index = vs_on_1.at(vs1).asAnInteger() + n_atoms; + exception_params.append(boost::make_tuple(atom0, vs1_index, coul_14_scale, lj_14_scale)); + } + } + } } void OpenMMMolecule::copyInCoordsAndVelocities(OpenMM::Vec3 *c, OpenMM::Vec3 *v) const @@ -2050,6 +2549,52 @@ QVector OpenMMMolecule::getTorsionKs() const return dih_ks; } +/** Return all CMAP grid values (column-major order, kJ/mol) for all CMAP terms + * concatenated. Grid k has getCMAPGridSizes()[k]^2 entries. + */ +QVector OpenMMMolecule::getCMAPGrids() const +{ + const double cmap_k_to_openmm = (SireUnits::kcal_per_mol).to(SireUnits::kJ_per_mol); + + QVector grids; + + for (const auto &cmap : this->cmap_params) + { + const auto ¶m = boost::get<5>(cmap); + // Apply the same AMBER→OpenMM grid transformation used by OpenMM's + // amber_file_parser.py: cyclic N/2 shift in both axes with phi↔psi swap. + // idx = ngrid*((j+ngrid//2)%ngrid)+((i+ngrid//2)%ngrid) + // where i=phi (outer/slow) and j=psi (inner/fast) in the output. + const auto flat_in = param.grid().toColumnMajorVector(); + const int N = param.nRows(); + + for (int phi = 0; phi < N; ++phi) + { + for (int psi = 0; psi < N; ++psi) + { + const int src = ((psi + N / 2) % N) * N + ((phi + N / 2) % N); + grids.append(flat_in[src] * cmap_k_to_openmm); + } + } + } + + return grids; +} + +/** Return the grid dimension N for each CMAP torsion (grid is N x N) */ +QVector OpenMMMolecule::getCMAPGridSizes() const +{ + QVector sizes; + sizes.reserve(this->cmap_params.count()); + + for (const auto &cmap : this->cmap_params) + { + sizes.append(boost::get<5>(cmap).nRows()); + } + + return sizes; +} + /** Return the atom indexes of the atoms in the exceptions, in * exception order for this molecule */ @@ -2237,6 +2782,18 @@ PerturbableOpenMMMolecule::PerturbableOpenMMMolecule(const OpenMMMolecule &mol, is_improper = mol.is_improper; is_rest2 = mol.is_rest2; + // populate the CMAP grid data and atom indices for both end states + cmap_grid0 = mol.getCMAPGrids(); + cmap_grid1 = mol.perturbed->getCMAPGrids(); + cmap_grid_sizes = mol.getCMAPGridSizes(); + + for (const auto &cmap : mol.cmap_params) + { + cmap_atoms.append(boost::make_tuple(boost::get<0>(cmap), boost::get<1>(cmap), + boost::get<2>(cmap), boost::get<3>(cmap), + boost::get<4>(cmap))); + } + bool fix_perturbable_zero_sigmas = false; if (map.specified("fix_perturbable_zero_sigmas")) @@ -2268,7 +2825,7 @@ PerturbableOpenMMMolecule::PerturbableOpenMMMolecule(const OpenMMMolecule &mol, sig0[i] = sig1_data[i]; sig0_data = sig0.constData(); } - else if (std::abs(sig1_data[i] <= 1e-9)) + else if (std::abs(sig1_data[i]) <= 1e-9) { sig1[i] = sig0_data[i]; sig1_data = sig1.constData(); @@ -2305,7 +2862,11 @@ PerturbableOpenMMMolecule::PerturbableOpenMMMolecule(const PerturbableOpenMMMole constraint_idxs(other.constraint_idxs), start_atom_idx(other.start_atom_idx), is_improper(other.is_improper), - is_rest2(other.is_rest2) + is_rest2(other.is_rest2), + cmap_grid0(other.cmap_grid0), + cmap_grid1(other.cmap_grid1), + cmap_grid_sizes(other.cmap_grid_sizes), + cmap_atoms(other.cmap_atoms) { } @@ -2333,7 +2894,9 @@ bool PerturbableOpenMMMolecule::operator==(const PerturbableOpenMMMolecule &othe lj_scl0 == other.lj_scl0 and lj_scl1 == other.lj_scl1 and to_ghost_idxs == other.to_ghost_idxs and from_ghost_idxs == other.from_ghost_idxs and exception_atoms == other.exception_atoms and exception_idxs == other.exception_idxs and - perturbable_constraints == other.perturbable_constraints and constraint_idxs == other.constraint_idxs; + perturbable_constraints == other.perturbable_constraints and constraint_idxs == other.constraint_idxs and + cmap_grid0 == other.cmap_grid0 and cmap_grid1 == other.cmap_grid1 and + cmap_grid_sizes == other.cmap_grid_sizes and cmap_atoms == other.cmap_atoms; } /** Comparison operator */ @@ -2402,6 +2965,11 @@ PerturbableOpenMMMolecule &PerturbableOpenMMMolecule::operator=(const Perturbabl perturbable_constraints = other.perturbable_constraints; constraint_idxs = other.constraint_idxs; + cmap_grid0 = other.cmap_grid0; + cmap_grid1 = other.cmap_grid1; + cmap_grid_sizes = other.cmap_grid_sizes; + cmap_atoms = other.cmap_atoms; + Property::operator=(other); } @@ -2756,6 +3324,33 @@ QVector PerturbableOpenMMMolecule::getIsImproper() const return is_improper; } +/** Return the 5-atom indices (molecule-local) for each CMAP torsion, + * in the same order as getCMAPGridSizes(). Used for REST2 scaling. + */ +QVector> +PerturbableOpenMMMolecule::getCMAPAtoms() const +{ + return cmap_atoms; +} + +/** Return flat concatenated CMAP grid values for state 0 (column-major, kJ/mol) */ +const QVector &PerturbableOpenMMMolecule::getCMAPGrids0() const +{ + return cmap_grid0; +} + +/** Return flat concatenated CMAP grid values for state 1 (column-major, kJ/mol) */ +const QVector &PerturbableOpenMMMolecule::getCMAPGrids1() const +{ + return cmap_grid1; +} + +/** Return the grid dimension N for each CMAP torsion (grid is N x N) */ +const QVector &PerturbableOpenMMMolecule::getCMAPGridSizes() const +{ + return cmap_grid_sizes; +} + /** Return the index of the first atom in the Sire system */ int PerturbableOpenMMMolecule::getStartAtomIdx() const { diff --git a/wrapper/Convert/SireOpenMM/openmmmolecule.h b/wrapper/Convert/SireOpenMM/openmmmolecule.h index a903048cb..39a1e2621 100644 --- a/wrapper/Convert/SireOpenMM/openmmmolecule.h +++ b/wrapper/Convert/SireOpenMM/openmmmolecule.h @@ -3,17 +3,17 @@ #include -#include "SireMol/moleculeinfo.h" -#include "SireMol/core.h" #include "SireMol/atom.h" +#include "SireMol/core.h" +#include "SireMol/moleculeinfo.h" #include "SireMol/selector.hpp" -#include "SireMM/mmdetail.h" -#include "SireMM/excludedpairs.h" #include "SireMM/amberparams.h" -#include "SireMM/bond.h" #include "SireMM/angle.h" +#include "SireMM/bond.h" #include "SireMM/dihedral.h" +#include "SireMM/excludedpairs.h" +#include "SireMM/mmdetail.h" #include @@ -22,6 +22,28 @@ SIRE_BEGIN_HEADER namespace SireOpenMM { + /** Describes a virtual site (extra point) in an OpenMM molecule. + * Used for 4- and 5-point water models such as OPC, TIP4P, and TIP5P. + */ + struct VirtualSiteInfo + { + enum Type + { + ThreeParticleAverage, // used for 4-point water (OPC, TIP4P) + OutOfPlane // used for 5-point water (TIP5P) + }; + + Type type; + int vsite_idx; // molecule-local index of the virtual site atom + int p1_idx, p2_idx, p3_idx; // molecule-local parent indices (O, H1, H2) + + // Weights for ThreeParticleAverageSite: pos = w1*p1 + w2*p2 + w3*p3 + double w1, w2, w3; + + // Weights for OutOfPlaneSite: pos = p1 + w12*(p2-p1) + w13*(p3-p1) + wCross*(p2-p1)x(p3-p1) + double w12, w13, wCross; + }; + /** Internal class used to hold all of the extracted information * of an OpenMM Molecule. You should not use this outside * of the sire_to_openmm_system function. It holds lots of scratch @@ -73,6 +95,9 @@ namespace SireOpenMM QVector getTorsionPhases() const; QVector getTorsionKs() const; + QVector getCMAPGrids() const; + QVector getCMAPGridSizes() const; + QVector> getExceptionAtoms() const; QVector getChargeScales() const; @@ -136,6 +161,12 @@ namespace SireOpenMM /** All the dihedral and improper parameters */ QVector> dih_params; + /** All the CMAP parameters (atom0..4 indices, CMAPParameter) */ + QVector> cmap_params; + + /** Virtual site definitions for extra-point water models (OPC, TIP4P, TIP5P) */ + QVector virtual_sites; + /** All the constraints */ QVector> constraints; @@ -177,6 +208,18 @@ namespace SireOpenMM */ QSet from_ghost_idxs; + /** Pairs of atom indices (molecule-local) whose bond is present + * at λ=0 but absent at λ=1 (ring-breaking). Already swapped if + * swap_end_states was active at construction time. + */ + QVector> ring_breaking_pairs; + + /** Pairs of atom indices (molecule-local) whose bond is absent + * at λ=0 but present at λ=1 (ring-making). Already swapped if + * swap_end_states was active at construction time. + */ + QVector> ring_making_pairs; + /** What type of constraint to use */ qint32 constraint_type; @@ -193,6 +236,13 @@ namespace SireOpenMM /** The starting index of the first OpenMM atom in the original Sire system. */ int start_atom_idx; + /** Virtual site properties */ + bool has_vs; + int n_vs; + SireBase::Properties vs_parents; + SireBase::PropertyList vs_charges; + SireBase::Properties vs_properties; + private: void constructFromAmber(const SireMol::Molecule &mol, const SireMM::AmberParams ¶ms, @@ -274,6 +324,14 @@ namespace SireOpenMM QVector getTorsionPhases0() const; QVector getTorsionPhases1() const; + const QVector &getCMAPGrids0() const; + const QVector &getCMAPGrids1() const; + const QVector &getCMAPGridSizes() const; + + /** Return the 5-atom indices (molecule-local) for each CMAP torsion, + * in the same order as getCMAPGridSizes(). Used for REST2 scaling. */ + QVector> getCMAPAtoms() const; + QVector getChargeScales0() const; QVector getChargeScales1() const; QVector getLJScales0() const; @@ -348,6 +406,16 @@ namespace SireOpenMM QVector charge_scl0, charge_scl1; QVector lj_scl0, lj_scl1; + /** Flat concatenation of all CMAP grid values (column-major, kJ/mol) for + * state 0 and state 1. Grid k occupies cmap_grid_sizes[k]^2 entries. */ + QVector cmap_grid0, cmap_grid1; + + /** The grid dimension N for each CMAP torsion (grid is N x N) */ + QVector cmap_grid_sizes; + + /** Molecule-local 5-atom indices for each CMAP torsion (for REST2 checks) */ + QVector> cmap_atoms; + /** The indexes of atoms that become ghosts in the * perturbed state */ diff --git a/wrapper/Convert/SireOpenMM/pyqm.cpp b/wrapper/Convert/SireOpenMM/pyqm.cpp index c8e0f1334..24e9ede63 100644 --- a/wrapper/Convert/SireOpenMM/pyqm.cpp +++ b/wrapper/Convert/SireOpenMM/pyqm.cpp @@ -26,6 +26,7 @@ * \*********************************************/ +#include #include #include @@ -54,8 +55,9 @@ static const double VIRTUAL_PC_DELTA = 0.01; class GILLock { public: - GILLock() { state_ = PyGILState_Ensure(); } - ~GILLock() { PyGILState_Release(state_); } + GILLock() { state_ = PyGILState_Ensure(); } + ~GILLock() { PyGILState_Release(state_); } + private: PyGILState_STATE state_; }; @@ -85,8 +87,9 @@ bp::object getPythonObject(QString uuid) if (not py_object_registry.contains(uuid)) { throw SireError::invalid_key(QObject::tr( - "Unable to find UUID %1 in the PyQMForce callback registry.").arg(uuid), - CODELOC); + "Unable to find UUID %1 in the PyQMForce callback registry.") + .arg(uuid), + CODELOC); } return py_object_registry[uuid]; @@ -137,8 +140,7 @@ PyQMCallback::PyQMCallback() { } -PyQMCallback::PyQMCallback(bp::object py_object, QString name) : - py_object(py_object), name(name) +PyQMCallback::PyQMCallback(bp::object py_object, QString name) : py_object(py_object), name(name) { // Is this a method or free function. if (name.isEmpty()) @@ -172,15 +174,14 @@ PyQMCallback::call( xyz_qm, xyz_mm, cell, - idx_mm - ); + idx_mm); } catch (const bp::error_already_set &) { PyErr_Print(); throw SireError::process_error(QObject::tr( - "An error occurred when calling the QM Python callback method"), - CODELOC); + "An error occurred when calling the QM Python callback method"), + CODELOC); } } else @@ -194,17 +195,74 @@ PyQMCallback::call( xyz_qm, xyz_mm, cell, - idx_mm - ); + idx_mm); } catch (const bp::error_already_set &) { PyErr_Print(); throw SireError::process_error(QObject::tr( - "An error occurred when calling the QM Python callback method"), - CODELOC); + "An error occurred when calling the QM Python callback method"), + CODELOC); + } + } +} + +boost::tuple>, QVector>, QVector> +PyQMCallback::call4( + QVector numbers_qm, + QVector charges_mm, + QVector> xyz_qm, + QVector> xyz_mm, + QVector> cell, + QVector idx_mm) const +{ + // Acquire GIL before calling Python code. + GILLock lock; + + bp::object result; + try + { + if (this->is_method) + { + result = bp::call_method( + this->py_object.ptr(), + this->name.toStdString().c_str(), + numbers_qm, charges_mm, xyz_qm, xyz_mm, cell, idx_mm); + } + else + { + result = bp::call( + this->py_object.ptr(), + numbers_qm, charges_mm, xyz_qm, xyz_mm, cell, idx_mm); + } + } + catch (const bp::error_already_set &) + { + PyErr_Print(); + throw SireError::process_error(QObject::tr( + "An error occurred when calling the QM Python callback method"), + CODELOC); + } + + // Extract mandatory first three elements while GIL is held. + const auto energy = bp::extract(result[0])(); + const auto forces_qm = bp::extract>>(result[1])(); + const auto forces_mm = bp::extract>>(result[2])(); + + // Extract optional fourth element: dE/dcharges_mm (empty if not returned). + QVector dE_dcharges_mm; + if (bp::len(result) > 3) + { + try + { + dE_dcharges_mm = bp::extract>(result[3])(); + } + catch (...) + { } } + + return boost::make_tuple(energy, forces_qm, forces_mm, dE_dcharges_mm); } const char *PyQMCallback::typeName() @@ -225,14 +283,15 @@ static const RegisterMetaType r_pyqmforce(NO_ROOT); QDataStream &operator<<(QDataStream &ds, const PyQMForce &pyqmforce) { - writeHeader(ds, r_pyqmforce, 1); + writeHeader(ds, r_pyqmforce, 2); SharedDataStream sds(ds); sds << pyqmforce.callback << pyqmforce.cutoff << pyqmforce.neighbour_list_frequency - << pyqmforce.is_mechanical << pyqmforce.lambda << pyqmforce.atoms + << pyqmforce.is_mechanical << pyqmforce.lambda << pyqmforce.atoms << pyqmforce.mm1_to_qm << pyqmforce.mm1_to_mm2 << pyqmforce.bond_scale_factors - << pyqmforce.mm2_atoms << pyqmforce.numbers << pyqmforce.charges; + << pyqmforce.mm2_atoms << pyqmforce.numbers << pyqmforce.charges + << pyqmforce.switch_width << pyqmforce.use_switch; return ds; } @@ -241,17 +300,23 @@ QDataStream &operator>>(QDataStream &ds, PyQMForce &pyqmforce) { VersionID v = readHeader(ds, r_pyqmforce); - if (v == 1) + if (v == 2) + { + SharedDataStream sds(ds); + + sds >> pyqmforce.callback >> pyqmforce.cutoff >> pyqmforce.neighbour_list_frequency >> pyqmforce.is_mechanical >> pyqmforce.lambda >> pyqmforce.atoms >> pyqmforce.mm1_to_qm >> pyqmforce.mm1_to_mm2 >> pyqmforce.bond_scale_factors >> pyqmforce.mm2_atoms >> pyqmforce.numbers >> pyqmforce.charges >> pyqmforce.switch_width >> pyqmforce.use_switch; + } + else if (v == 1) { SharedDataStream sds(ds); - sds >> pyqmforce.callback >> pyqmforce.cutoff >> pyqmforce.neighbour_list_frequency - >> pyqmforce.is_mechanical >> pyqmforce.lambda >> pyqmforce.atoms - >> pyqmforce.mm1_to_qm >> pyqmforce.mm1_to_mm2 >> pyqmforce.bond_scale_factors - >> pyqmforce.mm2_atoms >> pyqmforce.numbers >> pyqmforce.charges; + sds >> pyqmforce.callback >> pyqmforce.cutoff >> pyqmforce.neighbour_list_frequency >> pyqmforce.is_mechanical >> pyqmforce.lambda >> pyqmforce.atoms >> pyqmforce.mm1_to_qm >> pyqmforce.mm1_to_mm2 >> pyqmforce.bond_scale_factors >> pyqmforce.mm2_atoms >> pyqmforce.numbers >> pyqmforce.charges; + + pyqmforce.switch_width = 0.2; + pyqmforce.use_switch = true; } else - throw version_error(v, "1", r_pyqmforce, CODELOC); + throw version_error(v, "2", r_pyqmforce, CODELOC); return ds; } @@ -272,35 +337,39 @@ PyQMForce::PyQMForce( QMap bond_scale_factors, QVector mm2_atoms, QVector numbers, - QVector charges) : - callback(callback), - cutoff(cutoff), - neighbour_list_frequency(neighbour_list_frequency), - is_mechanical(is_mechanical), - lambda(lambda), - atoms(atoms), - mm1_to_qm(mm1_to_qm), - mm1_to_mm2(mm1_to_mm2), - bond_scale_factors(bond_scale_factors), - mm2_atoms(mm2_atoms), - numbers(numbers), - charges(charges) -{ -} - -PyQMForce::PyQMForce(const PyQMForce &other) : - callback(other.callback), - cutoff(other.cutoff), - neighbour_list_frequency(other.neighbour_list_frequency), - is_mechanical(other.is_mechanical), - lambda(other.lambda), - atoms(other.atoms), - mm1_to_qm(other.mm1_to_qm), - mm1_to_mm2(other.mm1_to_mm2), - mm2_atoms(other.mm2_atoms), - bond_scale_factors(other.bond_scale_factors), - numbers(other.numbers), - charges(other.charges) + QVector charges, + double switch_width, + bool use_switch) : callback(callback), + cutoff(cutoff), + neighbour_list_frequency(neighbour_list_frequency), + is_mechanical(is_mechanical), + lambda(lambda), + atoms(atoms), + mm1_to_qm(mm1_to_qm), + mm1_to_mm2(mm1_to_mm2), + bond_scale_factors(bond_scale_factors), + mm2_atoms(mm2_atoms), + numbers(numbers), + charges(charges), + switch_width(switch_width), + use_switch(use_switch) +{ +} + +PyQMForce::PyQMForce(const PyQMForce &other) : callback(other.callback), + cutoff(other.cutoff), + neighbour_list_frequency(other.neighbour_list_frequency), + is_mechanical(other.is_mechanical), + lambda(other.lambda), + atoms(other.atoms), + mm1_to_qm(other.mm1_to_qm), + mm1_to_mm2(other.mm1_to_mm2), + mm2_atoms(other.mm2_atoms), + bond_scale_factors(other.bond_scale_factors), + numbers(other.numbers), + charges(other.charges), + switch_width(other.switch_width), + use_switch(other.use_switch) { } @@ -318,6 +387,8 @@ PyQMForce &PyQMForce::operator=(const PyQMForce &other) this->bond_scale_factors = other.bond_scale_factors; this->numbers = other.numbers; this->charges = other.charges; + this->switch_width = other.switch_width; + this->use_switch = other.use_switch; return *this; } @@ -362,7 +433,8 @@ int PyQMForce::getNeighbourListFrequency() const bool PyQMForce::getIsMechanical() const { - return this->is_mechanical;; + return this->is_mechanical; + ; } QVector PyQMForce::getAtoms() const @@ -390,6 +462,16 @@ QVector PyQMForce::getCharges() const return this->charges; } +double PyQMForce::getSwitchWidth() const +{ + return this->switch_width; +} + +bool PyQMForce::getUseSwitch() const +{ + return this->use_switch; +} + const char *PyQMForce::typeName() { return QMetaType::typeName(qMetaTypeId()); @@ -412,75 +494,88 @@ PyQMForce::call( return this->callback.call(numbers_qm, charges_mm, xyz_qm, xyz_mm, cell, idx_mm); } +boost::tuple>, QVector>, QVector> +PyQMForce::call4( + QVector numbers_qm, + QVector charges_mm, + QVector> xyz_qm, + QVector> xyz_mm, + QVector> cell, + QVector idx_mm) const +{ + return this->callback.call4(numbers_qm, charges_mm, xyz_qm, xyz_mm, cell, idx_mm); +} + ///////// ///////// OpenMM Serialization ///////// namespace OpenMM { - class PyQMForceProxy : public SerializationProxy { - public: - PyQMForceProxy() : SerializationProxy("PyQMForce") - { - }; + class PyQMForceProxy : public SerializationProxy + { + public: + PyQMForceProxy() : SerializationProxy("PyQMForce") { + }; - void serialize(const void* object, SerializationNode& node) const - { - // Serialize the object. - QByteArray data; - QDataStream ds(&data, QIODevice::WriteOnly); - PyQMForce pyqmforce = *static_cast(object); - ds << pyqmforce; + void serialize(const void *object, SerializationNode &node) const + { + // Serialize the object. + QByteArray data; + QDataStream ds(&data, QIODevice::WriteOnly); + PyQMForce pyqmforce = *static_cast(object); + ds << pyqmforce; - // Set the version. - node.setIntProperty("version", 0); + // Set the version. + node.setIntProperty("version", 0); - // Set the note attribute. - node.setStringProperty("note", - "This force only supports partial serialization, so can only be used " - "within the same session and memory space."); + // Set the note attribute. + node.setStringProperty("note", + "This force only supports partial serialization, so can only be used " + "within the same session and memory space."); - // Set the data by converting the QByteArray to a hexidecimal string. - node.setStringProperty("data", data.toHex().data()); - }; + // Set the data by converting the QByteArray to a hexidecimal string. + node.setStringProperty("data", data.toHex().data()); + }; - void* deserialize(const SerializationNode& node) const + void *deserialize(const SerializationNode &node) const + { + // Check the version. + int version = node.getIntProperty("version"); + if (version != 0) { - // Check the version. - int version = node.getIntProperty("version"); - if (version != 0) - { - throw OpenMM::OpenMMException("Unsupported version number"); - } + throw OpenMM::OpenMMException("Unsupported version number"); + } - // Get the data as a std::string. - auto string = node.getStringProperty("data"); + // Get the data as a std::string. + auto string = node.getStringProperty("data"); - // Convert to hexidecimal. - auto hex = QByteArray::fromRawData(string.data(), string.size()); + // Convert to hexidecimal. + auto hex = QByteArray::fromRawData(string.data(), string.size()); - // Convert to a QByteArray. - auto data = QByteArray::fromHex(hex); + // Convert to a QByteArray. + auto data = QByteArray::fromHex(hex); - // Deserialize the object. - QDataStream ds(data); - PyQMForce pyqmforce; + // Deserialize the object. + QDataStream ds(data); + PyQMForce pyqmforce; - try - { - ds >> pyqmforce; - } - catch (...) - { - throw OpenMM::OpenMMException("Unable to find UUID in the PyQMForce callback registry."); - } + try + { + ds >> pyqmforce; + } + catch (...) + { + throw OpenMM::OpenMMException("Unable to find UUID in the PyQMForce callback registry."); + } - return new PyQMForce(pyqmforce); - }; + return new PyQMForce(pyqmforce); + }; }; // Register the PyQMForce serialization proxy. - extern "C" void registerPyQMSerializationProxies() { + extern "C" void registerPyQMSerializationProxies() + { SerializationProxy::registerProxy(typeid(PyQMForce), new PyQMForceProxy()); } }; @@ -503,9 +598,8 @@ OpenMM::ForceImpl *PyQMForce::createImpl() const } #ifdef SIRE_USE_CUSTOMCPPFORCE -PyQMForceImpl::PyQMForceImpl(const PyQMForce &owner) : - OpenMM::CustomCPPForceImpl(owner), - owner(owner) +PyQMForceImpl::PyQMForceImpl(const PyQMForce &owner) : OpenMM::CustomCPPForceImpl(owner), + owner(owner) { } @@ -530,13 +624,17 @@ double PyQMForceImpl::computeForce( this->cutoff = this->owner.getCutoff().value(); // The neighbour list cutoff is 20% larger than the cutoff. - this->neighbour_list_cutoff = 1.2*this->cutoff; + this->neighbour_list_cutoff = 1.2 * this->cutoff; // Store the neighbour list update frequency. this->neighbour_list_frequency = this->owner.getNeighbourListFrequency(); // Flag whether a neighbour list is used. this->is_neighbour_list = this->neighbour_list_frequency > 0; + + // Cache switching function parameters. + this->use_switch = this->owner.getUseSwitch(); + this->r_switch = (1.0 - this->owner.getSwitchWidth()) * this->cutoff; } // Get the current box vectors in nanometers. @@ -545,17 +643,15 @@ double PyQMForceImpl::computeForce( // Create a triclinic space, converting to Angstrom. TriclinicBox space( - Vector(10*box_x[0], 10*box_x[1], 10*box_x[2]), - Vector(10*box_y[0], 10*box_y[1], 10*box_y[2]), - Vector(10*box_z[0], 10*box_z[1], 10*box_z[2]) - ); + Vector(10 * box_x[0], 10 * box_x[1], 10 * box_x[2]), + Vector(10 * box_y[0], 10 * box_y[1], 10 * box_y[2]), + Vector(10 * box_z[0], 10 * box_z[1], 10 * box_z[2])); // Store the cell vectors in Angstrom. QVector> cell = { - {10*box_x[0], 10*box_x[1], 10*box_x[2]}, - {10*box_y[0], 10*box_y[1], 10*box_y[2]}, - {10*box_z[0], 10*box_z[1], 10*box_z[2]} - }; + {10 * box_x[0], 10 * box_x[1], 10 * box_x[2]}, + {10 * box_y[0], 10 * box_y[1], 10 * box_y[2]}, + {10 * box_z[0], 10 * box_z[1], 10 * box_z[2]}}; // Store the QM atomic indices and numbers. auto qm_atoms = this->owner.getAtoms(); @@ -578,12 +674,12 @@ double PyQMForceImpl::computeForce( for (const auto &idx : qm_atoms) { const auto &pos = positions[idx]; - Vector qm_vec(10*pos[0], 10*pos[1], 10*pos[2]); + Vector qm_vec(10 * pos[0], 10 * pos[1], 10 * pos[2]); xyz_qm_vec[i] = qm_vec; i++; } - // Next sure that the QM atoms are whole (unwrapped). + // Make sure that the QM atoms are whole (unwrapped). xyz_qm_vec = space.makeWhole(xyz_qm_vec); // Get the center of the QM atoms. We will use this as a reference when @@ -608,6 +704,24 @@ double PyQMForceImpl::computeForce( // Store the current number of MM atoms. unsigned int num_mm = 0; + // ds/dr of the quintic switching function (zero outside the switching region). + // Defined at outer scope so the chain-rule correction loop can use it after + // the electrostatic-embedding block closes. + auto switch_deriv = [&](double r) -> double + { + if (not this->use_switch or r <= this->r_switch or r >= this->cutoff) + return 0.0; + const double x = (r - this->r_switch) / (this->cutoff - this->r_switch); + return -30.0 * x * x * (x - 1.0) * (x - 1.0) / (this->cutoff - this->r_switch); + }; + + // Unscaled charges and chain-rule data for accepted MM atoms. + // Declared at outer scope for the same reason as switch_deriv above. + QVector charges_unscaled; + QVector min_dists; + QVector nearest_qm_vecs; + QVector nearest_qm_atom_idxs; + // If we are using electrostatic embedding, the work out the MM point charges and // build the neighbour list. if (not this->owner.getIsMechanical()) @@ -617,7 +731,19 @@ double PyQMForceImpl::computeForce( QVector> xyz_virtual; QVector charges_virtual; - // Manually work out the MM point charges and build the neigbour list. + // Quintic switching function: scales charges smoothly to zero at the cutoff. + // Continuous through second derivative; r_switch and use_switch cached at step 0. + auto switching_function = [&](double r) -> double + { + if (not this->use_switch or r <= this->r_switch) + return 1.0; + if (r >= this->cutoff) + return 0.0; + const double x = (r - this->r_switch) / (this->cutoff - this->r_switch); + return 1.0 - x * x * x * (6.0 * x * x - 15.0 * x + 10.0); + }; + + // Manually work out the MM point charges and build the neighbour list. if (not this->is_neighbour_list or this->step_count % this->neighbour_list_frequency == 0) { // Clear the neighbour list. @@ -636,36 +762,53 @@ double PyQMForceImpl::computeForce( not mm2_atoms.contains(i)) { // Store the MM atom position in Sire Vector format. - Vector mm_vec(10*pos[0], 10*pos[1], 10*pos[2]); + Vector mm_vec(10 * pos[0], 10 * pos[1], 10 * pos[2]); + + // Find the minimum distance to any QM atom. + double min_dist = std::numeric_limits::max(); + Vector nearest_qm_vec; + int nearest_qm_atom_idx = -1; // Loop over all of the QM atoms. - for (const auto &qm_vec : xyz_qm_vec) + for (int qm_j = 0; qm_j < xyz_qm_vec.size(); ++qm_j) { - // Work out the distance between the current MM atom and QM atoms. + const auto &qm_vec = xyz_qm_vec[qm_j]; + + // Work out the distance between the current MM atom and QM atom. const auto dist = space.calcDist(mm_vec, qm_vec); + if (dist < min_dist) + { + min_dist = dist; + nearest_qm_vec = qm_vec; + nearest_qm_atom_idx = qm_atoms[qm_j]; + } + // The current MM atom is within the neighbour list cutoff. if (this->is_neighbour_list and dist < this->neighbour_list_cutoff) { // Insert the MM atom index into the neighbour list. this->neighbour_list.insert(i); } + } - // The current MM atom is within the cutoff, add it. - if (dist < cutoff) - { - // Work out the minimum image position with respect to the - // reference position and add to the vector. - mm_vec = space.getMinimumImage(mm_vec, center); - xyz_mm.append(QVector({mm_vec[0], mm_vec[1], mm_vec[2]})); + // The current MM atom is within the cutoff: add it. + if (min_dist < cutoff) + { + // Work out the minimum image position with respect to the + // reference position and add to the vector. + mm_vec = space.getMinimumImage(mm_vec, center); + xyz_mm.append(QVector({mm_vec[0], mm_vec[1], mm_vec[2]})); - // Add the charge and index. - charges_mm.append(this->owner.getCharges()[i]); - idx_mm.append(i); + const double q = this->owner.getCharges()[i]; + charges_unscaled.append(q); + min_dists.append(min_dist); + nearest_qm_vecs.append(nearest_qm_vec); + nearest_qm_atom_idxs.append(nearest_qm_atom_idx); - // Exit the inner loop. - break; - } + // Scale charge by switching function. + charges_mm.append(q * switching_function(min_dist)); + idx_mm.append(i); } } @@ -680,41 +823,55 @@ double PyQMForceImpl::computeForce( for (const auto &idx : this->neighbour_list) { // Store the MM atom position in Sire Vector format. - Vector mm_vec(10*positions[idx][0], 10*positions[idx][1], 10*positions[idx][2]); + Vector mm_vec(10 * positions[idx][0], 10 * positions[idx][1], 10 * positions[idx][2]); + + // Find the minimum distance to any QM atom. + double min_dist = std::numeric_limits::max(); + Vector nearest_qm_vec; + int nearest_qm_atom_idx = -1; - // Loop over all of the QM atoms. - for (const auto &qm_vec : xyz_qm_vec) + for (int qm_j = 0; qm_j < xyz_qm_vec.size(); ++qm_j) { - // The current MM atom is within the cutoff, add it. - if (space.calcDist(mm_vec, qm_vec) < cutoff) + const auto &qm_vec = xyz_qm_vec[qm_j]; + const auto dist = space.calcDist(mm_vec, qm_vec); + if (dist < min_dist) { - // Work out the minimum image position with respect to the - // reference position and add to the vector. - mm_vec = space.getMinimumImage(mm_vec, center); - xyz_mm.append(QVector({mm_vec[0], mm_vec[1], mm_vec[2]})); - - // Add the charge and index. - charges_mm.append(this->owner.getCharges()[idx]); - idx_mm.append(idx); - - // Exit the inner loop. - break; + min_dist = dist; + nearest_qm_vec = qm_vec; + nearest_qm_atom_idx = qm_atoms[qm_j]; } } + + // The current MM atom is within the cutoff: add it. + if (min_dist < cutoff) + { + mm_vec = space.getMinimumImage(mm_vec, center); + xyz_mm.append(QVector({mm_vec[0], mm_vec[1], mm_vec[2]})); + + const double q = this->owner.getCharges()[idx]; + charges_unscaled.append(q); + min_dists.append(min_dist); + nearest_qm_vecs.append(nearest_qm_vec); + nearest_qm_atom_idxs.append(nearest_qm_atom_idx); + + // Scale charge by switching function. + charges_mm.append(q * switching_function(min_dist)); + idx_mm.append(idx); + } } } // Handle link atoms via the Charge Shift method. // See: https://www.ks.uiuc.edu/Research/qmmm - for (const auto &idx: mm1_to_mm2.keys()) + for (const auto &idx : mm1_to_mm2.keys()) { // Get the QM atom to which the current MM atom is bonded. const auto qm_idx = mm1_to_qm[idx]; // Store the MM1 position in Sire Vector format, along with the // position of the QM atom to which it is bonded. - Vector mm1_vec(10*positions[idx][0], 10*positions[idx][1], 10*positions[idx][2]); - Vector qm_vec(10*positions[qm_idx][0], 10*positions[qm_idx][1], 10*positions[qm_idx][2]); + Vector mm1_vec(10 * positions[idx][0], 10 * positions[idx][1], 10 * positions[idx][2]); + Vector qm_vec(10 * positions[qm_idx][0], 10 * positions[qm_idx][1], 10 * positions[qm_idx][2]); // Work out the minimum image positions with respect to the reference position. mm1_vec = space.getMinimumImage(mm1_vec, center); @@ -725,7 +882,7 @@ double PyQMForceImpl::computeForce( // where R0(QM-L) is the equilibrium bond length for the QM and link (L) // elements, and R0(QM-MM1) is the equilibrium bond length for the QM // and MM1 elements. - const auto link_vec = qm_vec + bond_scale_factors[idx]*(mm1_vec - qm_vec); + const auto link_vec = qm_vec + bond_scale_factors[idx] * (mm1_vec - qm_vec); // Add to the QM positions. xyz_qm.append(QVector({link_vec[0], link_vec[1], link_vec[2]})); @@ -747,10 +904,10 @@ double PyQMForceImpl::computeForce( // charge is redistributed over the MM2 atoms and two virtual point // charges are added either side of the MM2 atoms in order to preserve // the MM1-MM2 dipole. - for (const auto& mm2_idx : mm1_to_mm2[idx]) + for (const auto &mm2_idx : mm1_to_mm2[idx]) { // Store the MM2 position in Sire Vector format. - Vector mm2_vec(10*positions[mm2_idx][0], 10*positions[mm2_idx][1], 10*positions[mm2_idx][2]); + Vector mm2_vec(10 * positions[mm2_idx][0], 10 * positions[mm2_idx][1], 10 * positions[mm2_idx][2]); // Work out the minimum image position with respect to the reference position. mm2_vec = space.getMinimumImage(mm2_vec, center); @@ -768,12 +925,12 @@ double PyQMForceImpl::computeForce( const auto normal = (mm2_vec - mm1_vec).normalise(); // Positive direction. (Away from MM1 atom.) - auto xyz = mm2_vec + VIRTUAL_PC_DELTA*normal; + auto xyz = mm2_vec + VIRTUAL_PC_DELTA * normal; xyz_virtual.append(QVector({xyz[0], xyz[1], xyz[2]})); charges_virtual.append(-frac_charge); // Negative direction (Towards MM1 atom.) - xyz = mm2_vec - VIRTUAL_PC_DELTA*normal; + xyz = mm2_vec - VIRTUAL_PC_DELTA * normal; xyz_virtual.append(QVector({xyz[0], xyz[1], xyz[2]})); charges_virtual.append(frac_charge); } @@ -791,20 +948,20 @@ double PyQMForceImpl::computeForce( } } - // Call the callback. - auto result = this->owner.call( + // Call the callback, requesting the optional dE/dcharges_mm fourth element. + auto result = this->owner.call4( numbers, charges_mm, xyz_qm, xyz_mm, cell, - idx_mm - ); + idx_mm); // Extract the results. These will automatically be returned in OpenMM units. auto energy = result.get<0>(); auto forces_qm = result.get<1>(); auto forces_mm = result.get<2>(); + const auto dE_dcharges_mm = result.get<3>(); // The current interpolation (weighting) parameter. double lambda; @@ -841,7 +998,7 @@ double PyQMForceImpl::computeForce( // Now update the force vector. // First the QM atoms. - for (int i=0; i(), - callback(py_object, name), - cutoff(cutoff), - neighbour_list_frequency(neighbour_list_frequency), - is_mechanical(is_mechanical), - lambda(lambda) + double lambda, + double switch_width, + bool use_switch) : ConcreteProperty(), + callback(py_object, name), + cutoff(cutoff), + neighbour_list_frequency(neighbour_list_frequency), + is_mechanical(is_mechanical), + lambda(lambda), + switch_width(switch_width), + use_switch(use_switch) { // Register the serialization proxies. OpenMM::registerPyQMSerializationProxies(); @@ -915,19 +1100,20 @@ PyQMEngine::PyQMEngine( } } -PyQMEngine::PyQMEngine(const PyQMEngine &other) : - callback(other.callback), - cutoff(other.cutoff), - neighbour_list_frequency(other.neighbour_list_frequency), - is_mechanical(other.is_mechanical), - lambda(other.lambda), - atoms(other.atoms), - mm1_to_qm(other.mm1_to_qm), - mm1_to_mm2(other.mm1_to_mm2), - mm2_atoms(other.mm2_atoms), - bond_scale_factors(other.bond_scale_factors), - numbers(other.numbers), - charges(other.charges) +PyQMEngine::PyQMEngine(const PyQMEngine &other) : callback(other.callback), + cutoff(other.cutoff), + neighbour_list_frequency(other.neighbour_list_frequency), + is_mechanical(other.is_mechanical), + lambda(other.lambda), + switch_width(other.switch_width), + use_switch(other.use_switch), + atoms(other.atoms), + mm1_to_qm(other.mm1_to_qm), + mm1_to_mm2(other.mm1_to_mm2), + mm2_atoms(other.mm2_atoms), + bond_scale_factors(other.bond_scale_factors), + numbers(other.numbers), + charges(other.charges) { } @@ -938,6 +1124,8 @@ PyQMEngine &PyQMEngine::operator=(const PyQMEngine &other) this->neighbour_list_frequency = other.neighbour_list_frequency; this->is_mechanical = other.is_mechanical; this->lambda = other.lambda; + this->switch_width = other.switch_width; + this->use_switch = other.use_switch; this->atoms = other.atoms; this->mm1_to_qm = other.mm1_to_qm; this->mm1_to_mm2 = other.mm1_to_mm2; @@ -1069,6 +1257,30 @@ void PyQMEngine::setCharges(QVector charges) this->charges = charges; } +double PyQMEngine::getSwitchWidth() const +{ + return this->switch_width; +} + +void PyQMEngine::setSwitchWidth(double switch_width) +{ + if (switch_width < 0.0) + switch_width = 0.0; + else if (switch_width > 1.0) + switch_width = 1.0; + this->switch_width = switch_width; +} + +bool PyQMEngine::getUseSwitch() const +{ + return this->use_switch; +} + +void PyQMEngine::setUseSwitch(bool use_switch) +{ + this->use_switch = use_switch; +} + const char *PyQMEngine::typeName() { return QMetaType::typeName(qMetaTypeId()); @@ -1091,7 +1303,7 @@ PyQMEngine::call( return this->callback.call(numbers_qm, charges_mm, xyz_qm, xyz_mm, cell, idx_mm); } -QMForce* PyQMEngine::createForce() const +QMForce *PyQMEngine::createForce() const { return new PyQMForce( this->callback, @@ -1105,6 +1317,7 @@ QMForce* PyQMEngine::createForce() const this->bond_scale_factors, this->mm2_atoms, this->numbers, - this->charges - ); + this->charges, + this->switch_width, + this->use_switch); } diff --git a/wrapper/Convert/SireOpenMM/pyqm.h b/wrapper/Convert/SireOpenMM/pyqm.h index 5b149add6..2acb32b34 100644 --- a/wrapper/Convert/SireOpenMM/pyqm.h +++ b/wrapper/Convert/SireOpenMM/pyqm.h @@ -96,7 +96,7 @@ namespace SireOpenMM - A list of forces for the MM atoms in kJ/mol/nm. If empty, then the object is assumed to be a callable. */ - PyQMCallback(bp::object, QString name=""); + PyQMCallback(bp::object, QString name = ""); //! Call the callback function. /*! \param numbers_qm @@ -124,15 +124,31 @@ namespace SireOpenMM - The energy in kJ/mol. - A vector of forces for the QM atoms in kJ/mol/nm. - A vector of forces for the MM atoms in kJ/mol/nm. + - (Optional) The gradient of the energy w.r.t. the effective MM + charges in kJ/mol/e, used for the chain-rule switching correction. */ boost::tuple>, QVector>> call( - QVector numbers_qm, - QVector charges_mm, - QVector> xyz_qm, - QVector> xyz_mm, - QVector> cell, - QVector idx_mm - ) const; + QVector numbers_qm, + QVector charges_mm, + QVector> xyz_qm, + QVector> xyz_mm, + QVector> cell, + QVector idx_mm) const; + + //! Call the callback and return an optional dE/dcharges_mm fourth element. + /*! Same arguments as call(). The 4th element of the returned tuple is the + gradient of the energy w.r.t. the effective MM charges (dE/dq_eff). + If the callback returns only 3 elements the 4th is an empty vector, + providing backward compatibility with existing callbacks. + */ + boost::tuple>, QVector>, QVector> + call4( + QVector numbers_qm, + QVector charges_mm, + QVector> xyz_qm, + QVector> xyz_mm, + QVector> cell, + QVector idx_mm) const; //! Return the C++ name for this class. static const char *typeName(); @@ -212,8 +228,9 @@ namespace SireOpenMM QMap bond_scale_factors, QVector mm2_atoms, QVector numbers, - QVector charges - ); + QVector charges, + double switch_width = 0.2, + bool use_switch = true); //! Copy constructor. PyQMForce(const PyQMForce &other); @@ -309,6 +326,18 @@ namespace SireOpenMM */ QVector getCharges() const; + //! Get the switch width. + /*! \returns + The switch width as a fraction of the cutoff. + */ + double getSwitchWidth() const; + + //! Get whether a switching function is used. + /*! \returns + Whether a switching function is used. + */ + bool getUseSwitch() const; + //! Return the C++ name for this class. static const char *typeName(); @@ -316,40 +345,24 @@ namespace SireOpenMM const char *what() const; //! Call the callback function. - /*! \param numbers_qm - A vector of atomic numbers for the atoms in the ML region. - - \param charges_mm - A vector of the charges on the MM atoms in mod electron charge. - - \param xyz_qm - A vector of positions for the atoms in the ML region in Angstrom. - - \param xyz_mm - A vector of positions for the atoms in the MM region in Angstrom. - - \param cell - A vector of the 3 cell vectors in Angstrom. - - \param idx_mm - A vector of MM atom indices. Note that len(idx_mm) <= len(charges_mm) - since it only contains the indices of true MM atoms, not link atoms - or virtual charges. - - \returns - A tuple containing: - - The energy in kJ/mol. - - A vector of forces for the QM atoms in kJ/mol/nm. - - A vector of forces for the MM atoms in kJ/mol/nm. - */ + /*! \returns A tuple: (energy, forces_qm, forces_mm). */ boost::tuple>, QVector>> call( - QVector numbers_qm, - QVector charges_mm, - QVector> xyz_qm, - QVector> xyz_mm, - QVector> cell, - QVector idx_mm - ) const; + QVector numbers_qm, + QVector charges_mm, + QVector> xyz_qm, + QVector> xyz_mm, + QVector> cell, + QVector idx_mm) const; + + //! Call the callback, returning optional dE/dcharges_mm as fourth element. + boost::tuple>, QVector>, QVector> + call4( + QVector numbers_qm, + QVector charges_mm, + QVector> xyz_qm, + QVector> xyz_mm, + QVector> cell, + QVector idx_mm) const; protected: OpenMM::ForceImpl *createImpl() const; @@ -360,6 +373,8 @@ namespace SireOpenMM int neighbour_list_frequency; bool is_mechanical; double lambda; + double switch_width; + bool use_switch; QVector atoms; QMap mm1_to_qm; QMap> mm1_to_mm2; @@ -385,8 +400,10 @@ namespace SireOpenMM private: const PyQMForce &owner; - unsigned long long step_count=0; + unsigned long long step_count = 0; double cutoff; + double r_switch; + bool use_switch; bool is_neighbour_list; int neighbour_list_frequency; double neighbour_list_cutoff; @@ -424,12 +441,13 @@ namespace SireOpenMM */ PyQMEngine( bp::object, - QString method="", - SireUnits::Dimension::Length cutoff=7.5*SireUnits::angstrom, - int neighbour_list_frequency=0, - bool is_mechanical=false, - double lambda=1.0 - ); + QString method = "", + SireUnits::Dimension::Length cutoff = 7.5 * SireUnits::angstrom, + int neighbour_list_frequency = 0, + bool is_mechanical = false, + double lambda = 1.0, + double switch_width = 0.2, + bool use_switch = true); //! Copy constructor. PyQMEngine(const PyQMEngine &other); @@ -580,6 +598,30 @@ namespace SireOpenMM */ void setCharges(QVector charges); + //! Get the switch width. + /*! \returns + The switch width as a fraction of the cutoff. + */ + double getSwitchWidth() const; + + //! Set the switch width. + /*! \param switch_width + The switch width as a fraction of the cutoff (0 to 1). + */ + void setSwitchWidth(double switch_width); + + //! Get whether a switching function is used. + /*! \returns + Whether a switching function is used. + */ + bool getUseSwitch() const; + + //! Set whether a switching function is used. + /*! \param use_switch + Whether to use a switching function. + */ + void setUseSwitch(bool use_switch); + //! Return the C++ name for this class. static const char *typeName(); @@ -612,18 +654,19 @@ namespace SireOpenMM - The energy in kJ/mol. - A vector of forces for the QM atoms in kJ/mol/nm. - A vector of forces for the MM atoms in kJ/mol/nm. + - (Optional) The gradient of the energy w.r.t. the effective MM + charges in kJ/mol/e, used for the chain-rule switching correction. */ boost::tuple>, QVector>> call( - QVector numbers_qm, - QVector charges_mm, - QVector> xyz_qm, - QVector> xyz_mm, - QVector> cell, - QVector idx_mm - ) const; + QVector numbers_qm, + QVector charges_mm, + QVector> xyz_qm, + QVector> xyz_mm, + QVector> cell, + QVector idx_mm) const; //! Create an EMLE force object. - QMForce* createForce() const; + QMForce *createForce() const; private: PyQMCallback callback; @@ -631,6 +674,8 @@ namespace SireOpenMM int neighbour_list_frequency; bool is_mechanical; double lambda; + double switch_width; + bool use_switch; QVector atoms; QMap mm1_to_qm; QMap> mm1_to_mm2; diff --git a/wrapper/Convert/SireOpenMM/sire_openmm.cpp b/wrapper/Convert/SireOpenMM/sire_openmm.cpp index 36794bb13..efe5293b8 100644 --- a/wrapper/Convert/SireOpenMM/sire_openmm.cpp +++ b/wrapper/Convert/SireOpenMM/sire_openmm.cpp @@ -495,6 +495,10 @@ namespace SireOpenMM { offsets[i] = offset; offset += mols[i].nAtoms(); + if (mols[i].hasProperty("n_virtual_sites")) + { + offset += mols[i].property("n_virtual_sites").asAnInteger(); + } } const auto offsets_data = offsets.constData(); @@ -822,6 +826,10 @@ namespace SireOpenMM { offsets[i] = offset; offset += mols[i].nAtoms(); + if (mols[i].hasProperty("n_virtual_sites")) + { + offset += mols[i].property("n_virtual_sites").asAnInteger(); + } } const auto offsets_data = offsets.constData(); diff --git a/wrapper/Convert/SireOpenMM/sire_to_openmm_system.cpp b/wrapper/Convert/SireOpenMM/sire_to_openmm_system.cpp index 95275dbac..7e1f498b5 100644 --- a/wrapper/Convert/SireOpenMM/sire_to_openmm_system.cpp +++ b/wrapper/Convert/SireOpenMM/sire_to_openmm_system.cpp @@ -1,4 +1,3 @@ - #include "sire_openmm.h" #include @@ -24,9 +23,10 @@ #include "SireMM/anglerestraints.h" #include "SireMM/atomljs.h" #include "SireMM/bondrestraints.h" -#include "SireMM/inversebondrestraints.h" #include "SireMM/boreschrestraints.h" +#include "SireMM/cmapparameter.h" #include "SireMM/dihedralrestraints.h" +#include "SireMM/inversebondrestraints.h" #include "SireMM/morsepotentialrestraints.h" #include "SireMM/positionalrestraints.h" #include "SireMM/rmsdrestraints.h" @@ -40,6 +40,7 @@ #include "SireMaths/maths.h" #include "SireMaths/vector.h" +#include "SireBase/console.h" #include "SireBase/generalunitproperty.h" #include "SireBase/lengthproperty.h" #include "SireBase/parallel.h" @@ -71,7 +72,7 @@ using namespace SireOpenMM; */ void _add_boresch_restraints(const SireMM::BoreschRestraints &restraints, OpenMM::System &system, LambdaLever &lambda_lever, - int natoms) + int natoms, QVector &real_atoms, int &force_group_counter) { if (restraints.isEmpty()) return; @@ -136,8 +137,10 @@ void _add_boresch_restraints(const SireMM::BoreschRestraints &restraints, restraintff->setUsesPeriodicBoundaryConditions(restraints.usesPbc()); + restraintff->setForceGroup(force_group_counter); lambda_lever.addRestraintIndex(restraints.name(), system.addForce(restraintff)); + lambda_lever.setRestraintForceGroup(restraints.name(), force_group_counter++); const double internal_to_nm = (1 * SireUnits::angstrom).to(SireUnits::nanometer); const double internal_to_k = (1 * SireUnits::kcal_per_mol / (SireUnits::angstrom2)).to(SireUnits::kJ_per_mol / SireUnits::nanometer2); @@ -156,8 +159,8 @@ void _add_boresch_restraints(const SireMM::BoreschRestraints &restraints, for (int i = 0; i < 3; ++i) { - particles[i] = restraint.receptorAtoms()[i]; - particles[i + 3] = restraint.ligandAtoms()[i]; + particles[i] = real_atoms[restraint.receptorAtoms()[i]]; + particles[i + 3] = real_atoms[restraint.ligandAtoms()[i]]; if (particles[i] < 0 or particles[i] >= natoms or particles[i + 3] < 0 or particles[i + 3] >= natoms) @@ -196,7 +199,7 @@ void _add_boresch_restraints(const SireMM::BoreschRestraints &restraints, */ void _add_bond_restraints(const SireMM::BondRestraints &restraints, OpenMM::System &system, LambdaLever &lambda_lever, - int natoms) + int natoms, QVector &real_atoms, int &force_group_counter) { if (restraints.isEmpty()) return; @@ -223,8 +226,10 @@ void _add_bond_restraints(const SireMM::BondRestraints &restraints, restraintff->setUsesPeriodicBoundaryConditions(restraints.usesPbc()); + restraintff->setForceGroup(force_group_counter); lambda_lever.addRestraintIndex(restraints.name(), system.addForce(restraintff)); + lambda_lever.setRestraintForceGroup(restraints.name(), force_group_counter++); const auto atom_restraints = restraints.atomRestraints(); @@ -235,8 +240,8 @@ void _add_bond_restraints(const SireMM::BondRestraints &restraints, for (const auto &restraint : atom_restraints) { - int atom0_index = restraint.atom0(); - int atom1_index = restraint.atom1(); + int atom0_index = real_atoms[restraint.atom0()]; + int atom1_index = real_atoms[restraint.atom1()]; if (atom0_index < 0 or atom0_index >= natoms) throw SireError::invalid_index(QObject::tr( @@ -261,8 +266,8 @@ void _add_bond_restraints(const SireMM::BondRestraints &restraints, } void _add_inverse_bond_restraints(const SireMM::InverseBondRestraints &restraints, - OpenMM::System &system, LambdaLever &lambda_lever, - int natoms) + OpenMM::System &system, LambdaLever &lambda_lever, + int natoms, QVector &real_atoms, int &force_group_counter) { if (restraints.isEmpty()) return; @@ -275,10 +280,10 @@ void _add_inverse_bond_restraints(const SireMM::InverseBondRestraints &restraint } const auto energy_expression = QString( - "rho*k*delta*delta*step;" - "delta=(r-r0);" - "step=max(0,min(1,(r0 - r)))") - .toStdString(); + "rho*k*delta*delta*step;" + "delta=(r-r0);" + "step=max(0,min(1,(r0 - r)))") + .toStdString(); auto *restraintff = new OpenMM::CustomBondForce(energy_expression); restraintff->setName("InverseBondRestraintForce"); @@ -289,8 +294,10 @@ void _add_inverse_bond_restraints(const SireMM::InverseBondRestraints &restraint restraintff->setUsesPeriodicBoundaryConditions(restraints.usesPbc()); + restraintff->setForceGroup(force_group_counter); lambda_lever.addRestraintIndex(restraints.name(), - system.addForce(restraintff)); + system.addForce(restraintff)); + lambda_lever.setRestraintForceGroup(restraints.name(), force_group_counter++); const auto atom_restraints = restraints.atomRestraints(); @@ -303,22 +310,22 @@ void _add_inverse_bond_restraints(const SireMM::InverseBondRestraints &restraint for (const auto &restraint : atom_restraints) { - int atom0_index = restraint.atom0(); - int atom1_index = restraint.atom1(); + int atom0_index = real_atoms[restraint.atom0()]; + int atom1_index = real_atoms[restraint.atom1()]; if (atom0_index < 0 or atom0_index >= natoms) - throw SireError::invalid_index(QObject::tr( - "Invalid particle index! %1 from %2") - .arg(atom0_index) - .arg(natoms), - CODELOC); + throw SireError::invalid_index(QObject::tr( + "Invalid particle index! %1 from %2") + .arg(atom0_index) + .arg(natoms), + CODELOC); if (atom1_index < 0 or atom1_index >= natoms) - throw SireError::invalid_index(QObject::tr( - "Invalid particle index! %1 from %2") - .arg(atom1_index) - .arg(natoms), - CODELOC); + throw SireError::invalid_index(QObject::tr( + "Invalid particle index! %1 from %2") + .arg(atom1_index) + .arg(natoms), + CODELOC); custom_params[0] = 1.0; // rho - always equal to 1 (scaled by lever) custom_params[1] = restraint.k().value() * internal_to_k; // k @@ -332,30 +339,27 @@ void _add_inverse_bond_restraints(const SireMM::InverseBondRestraints &restraint * system, which is acted on by the passed LambdaLever. */ void _add_morse_potential_restraints(const SireMM::MorsePotentialRestraints &restraints, - OpenMM::System &system, LambdaLever &lambda_lever, - int natoms) + OpenMM::System &system, LambdaLever &lambda_lever, + int natoms, QVector &real_atoms, int &force_group_counter) { if (restraints.isEmpty()) - return; + return; if (restraints.hasCentroidRestraints()) { throw SireError::unsupported(QObject::tr( - "Centroid bond restraints aren't yet supported..."), - CODELOC); + "Centroid bond restraints aren't yet supported..."), + CODELOC); } - // energy expression of a harmonic bond potential, scaled by rho - // e_restraint = rho*DE*(1-exp(-Bij*dr))**2 - // Bij = sqrt(k/2*DE) - // dr = (r - r0) - const auto energy_expression = QString( - "rho*e_restraint;" - "e_restraint=de*(1-exp(-sqrt(k/(2*de))*delta))^2;" - "delta=(r-r0)") - .toStdString(); - + "e_total;" + "e_total = rho * e_morse + e_repulsion;" + "e_morse = de * (1 - exp(-alpha * delta))^2;" + "e_repulsion = e_rep * (r_sigma / r)^r_pow;" + "alpha = sqrt(k / (2 * de));" + "delta = (r - r0)") + .toStdString(); auto *restraintff = new OpenMM::CustomBondForce(energy_expression); restraintff->setName("MorsePotentialRestraintForce"); @@ -364,42 +368,49 @@ void _add_morse_potential_restraints(const SireMM::MorsePotentialRestraints &res restraintff->addPerBondParameter("k"); restraintff->addPerBondParameter("r0"); restraintff->addPerBondParameter("de"); - + restraintff->addPerBondParameter("e_rep"); + restraintff->addPerBondParameter("r_sigma"); + restraintff->addPerBondParameter("r_pow"); restraintff->setUsesPeriodicBoundaryConditions(restraints.usesPbc()); + restraintff->setForceGroup(force_group_counter); lambda_lever.addRestraintIndex(restraints.name(), - system.addForce(restraintff)); + system.addForce(restraintff)); + lambda_lever.setRestraintForceGroup(restraints.name(), force_group_counter++); const auto atom_restraints = restraints.atomRestraints(); const double internal_to_nm = (1 * SireUnits::angstrom).to(SireUnits::nanometer); const double internal_to_k = (1 * SireUnits::kcal_per_mol / (SireUnits::angstrom2)).to(SireUnits::kJ_per_mol / (SireUnits::nanometer2)); const double internal_to_de = (1 * SireUnits::kcal_per_mol).to(SireUnits::kJ_per_mol); - std::vector custom_params = {1.0, 0.0, 0.0, 0.0}; + std::vector custom_params = {1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0}; for (const auto &restraint : atom_restraints) { - int atom0_index = restraint.atom0(); - int atom1_index = restraint.atom1(); + int atom0_index = real_atoms[restraint.atom0()]; + int atom1_index = real_atoms[restraint.atom1()]; if (atom0_index < 0 or atom0_index >= natoms) - throw SireError::invalid_index(QObject::tr( - "Invalid particle index! %1 from %2") - .arg(atom0_index) - .arg(natoms), - CODELOC); + throw SireError::invalid_index(QObject::tr( + "Invalid particle index! %1 from %2") + .arg(atom0_index) + .arg(natoms), + CODELOC); if (atom1_index < 0 or atom1_index >= natoms) - throw SireError::invalid_index(QObject::tr( - "Invalid particle index! %1 from %2") - .arg(atom1_index) - .arg(natoms), - CODELOC); + throw SireError::invalid_index(QObject::tr( + "Invalid particle index! %1 from %2") + .arg(atom1_index) + .arg(natoms), + CODELOC); custom_params[0] = 1.0; // rho - always equal to 1 (scaled by lever) custom_params[1] = restraint.k().value() * internal_to_k; // k custom_params[2] = restraint.r0().value() * internal_to_nm; // r0 custom_params[3] = restraint.de().value() * internal_to_de; // de + custom_params[4] = 41.84; // e_rep (kJ/mol) + custom_params[5] = 0.025; // r_sigma (nm) + custom_params[6] = 12; // r_pow restraintff->addBond(atom0_index, atom1_index, custom_params); } @@ -414,7 +425,7 @@ void _add_morse_potential_restraints(const SireMM::MorsePotentialRestraints &res void _add_positional_restraints(const SireMM::PositionalRestraints &restraints, OpenMM::System &system, LambdaLever &lambda_lever, std::vector &anchor_coords, - int natoms) + int natoms, QVector &real_atoms, int &force_group_counter) { if (restraints.isEmpty()) return; @@ -441,8 +452,10 @@ void _add_positional_restraints(const SireMM::PositionalRestraints &restraints, restraintff->setUsesPeriodicBoundaryConditions(restraints.usesPbc()); + restraintff->setForceGroup(force_group_counter); lambda_lever.addRestraintIndex(restraints.name(), system.addForce(restraintff)); + lambda_lever.setRestraintForceGroup(restraints.name(), force_group_counter++); const auto atom_restraints = restraints.atomRestraints(); @@ -467,13 +480,13 @@ void _add_positional_restraints(const SireMM::PositionalRestraints &restraints, auto ghost_nonghostff = lambda_lever.getForce("ghost/non-ghost", system); std::vector custom_params = {1.0, 0.0, 0.0}; - // Define null parameters used to add these particles to the ghost forces (5 total) + // Null parameters for anchor particles added to the ghost forces {q, half_sigma, two_sqrt_epsilon, alpha, kappa} std::vector custom_clj_params = {0.0, 0.0, 0.0, 0.0, 0.0}; // we need to add all of the positions as anchor particles for (const auto &restraint : atom_restraints) { - int atom_index = restraint.atom(); + int atom_index = real_atoms[restraint.atom()]; if (atom_index < 0 or atom_index >= natoms) throw SireError::invalid_index(QObject::tr( @@ -547,8 +560,8 @@ void _add_positional_restraints(const SireMM::PositionalRestraints &restraints, } void _add_rmsd_restraints(const SireMM::RMSDRestraints &restraints, - OpenMM::System &system, LambdaLever &lambda_lever, - int natoms) + OpenMM::System &system, LambdaLever &lambda_lever, + int natoms, QVector &real_atoms, int &force_group_counter) { if (restraints.isEmpty()) return; @@ -568,16 +581,19 @@ void _add_rmsd_restraints(const SireMM::RMSDRestraints &restraints, }; // Count the number of existing RMSD forces in the system - std::vector forces; + std::vector forces; - for (int i = 0; i < system.getNumForces(); ++i) { + for (int i = 0; i < system.getNumForces(); ++i) + { forces.push_back(&system.getForce(i)); } int n_CVForces = 0; - for (auto* force : forces) { - if (dynamic_cast(force)) { + for (auto *force : forces) + { + if (dynamic_cast(force)) + { n_CVForces++; } } @@ -595,7 +611,7 @@ void _add_rmsd_restraints(const SireMM::RMSDRestraints &restraints, // energy expression of a flat-bottom well potential, scaled by rho const auto energy_expression = rho_unique + "*" + k_unique + "*step(delta)*delta*delta;" + - "delta=(" + rmsd_unique + "-" + rmsd_b_unique + ")"; + "delta=(" + rmsd_unique + "-" + rmsd_b_unique + ")"; double k = restraint.k().value() * internal_to_k; double r0 = restraint.r0().value() * internal_to_nm; @@ -607,7 +623,7 @@ void _add_rmsd_restraints(const SireMM::RMSDRestraints &restraints, for (int i = 0; i < n_particles; ++i) { - particles[i] = restraint.atoms()[i]; + particles[i] = real_atoms[restraint.atoms()[i]]; } // Extract reference positions and convert to correct units @@ -630,8 +646,17 @@ void _add_rmsd_restraints(const SireMM::RMSDRestraints &restraints, auto *rmsdCV = new OpenMM::RMSDForce(referencePositions, particles); restraintff->addCollectiveVariable(rmsd_unique, rmsdCV); + // All sub-restraints with the same name share a single force group so + // that one getState(groups=...) call sums their energies correctly. + int grp = lambda_lever.getForceGroup(restraints.name()); + if (grp < 0) + { + grp = force_group_counter++; + } + restraintff->setForceGroup(grp); lambda_lever.addRestraintIndex(restraints.name(), - system.addForce(restraintff)); + system.addForce(restraintff)); + lambda_lever.setRestraintForceGroup(restraints.name(), grp); // Update the counter for number of CustomCVForces n_CVForces++; @@ -644,7 +669,7 @@ void _add_rmsd_restraints(const SireMM::RMSDRestraints &restraints, */ void _add_angle_restraints(const SireMM::AngleRestraints &restraints, OpenMM::System &system, LambdaLever &lambda_lever, - int natoms) + int natoms, QVector &real_atoms, int &force_group_counter) { if (restraints.isEmpty()) return; @@ -671,8 +696,10 @@ void _add_angle_restraints(const SireMM::AngleRestraints &restraints, restraintff->setUsesPeriodicBoundaryConditions(restraints.usesPbc()); + restraintff->setForceGroup(force_group_counter); lambda_lever.addRestraintIndex(restraints.name(), system.addForce(restraintff)); + lambda_lever.setRestraintForceGroup(restraints.name(), force_group_counter++); const double internal_to_ktheta = (1 * SireUnits::kcal_per_mol / (SireUnits::radian2)).to(SireUnits::kJ_per_mol / SireUnits::radian2); @@ -685,7 +712,7 @@ void _add_angle_restraints(const SireMM::AngleRestraints &restraints, for (int i = 0; i < 3; ++i) { - particles[i] = restraint.atoms()[i]; + particles[i] = real_atoms[restraint.atoms()[i]]; } std::vector parameters; @@ -702,7 +729,7 @@ void _add_angle_restraints(const SireMM::AngleRestraints &restraints, void _add_dihedral_restraints(const SireMM::DihedralRestraints &restraints, OpenMM::System &system, LambdaLever &lambda_lever, - int natoms) + int natoms, QVector &real_atoms, int &force_group_counter) { if (restraints.isEmpty()) return; @@ -734,8 +761,10 @@ void _add_dihedral_restraints(const SireMM::DihedralRestraints &restraints, restraintff->setUsesPeriodicBoundaryConditions(restraints.usesPbc()); + restraintff->setForceGroup(force_group_counter); lambda_lever.addRestraintIndex(restraints.name(), system.addForce(restraintff)); + lambda_lever.setRestraintForceGroup(restraints.name(), force_group_counter++); const double internal_to_ktheta = (1 * SireUnits::kcal_per_mol / (SireUnits::radian2)).to(SireUnits::kJ_per_mol / SireUnits::radian2); @@ -748,7 +777,7 @@ void _add_dihedral_restraints(const SireMM::DihedralRestraints &restraints, for (int i = 0; i < 4; ++i) { - particles[i] = restraint.atoms()[i]; + particles[i] = real_atoms[restraint.atoms()[i]]; } std::vector parameters; @@ -1076,7 +1105,7 @@ OpenMMMetaData SireOpenMM::sire_to_openmm_system(OpenMM::System &system, 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(); + start_atom_index[i] = start_atom_index[i - 1] + mols[i - 1].nAtoms(); } if (SireBase::should_run_in_parallel(nmols, map)) @@ -1097,6 +1126,9 @@ OpenMMMetaData SireOpenMM::sire_to_openmm_system(OpenMM::System &system, } // check to see if there are any perturbable molecules + bool any_ring_breaking = false; + bool any_ring_making = false; + if (not ignore_perturbations) { for (int i = 0; i < nmols; ++i) @@ -1104,8 +1136,13 @@ OpenMMMetaData SireOpenMM::sire_to_openmm_system(OpenMM::System &system, if (openmm_mols_data[i].isPerturbable()) { any_perturbable = true; - break; } + + if (not openmm_mols_data[i].ring_breaking_pairs.isEmpty()) + any_ring_breaking = true; + + if (not openmm_mols_data[i].ring_making_pairs.isEmpty()) + any_ring_making = true; } } @@ -1143,25 +1180,48 @@ OpenMMMetaData SireOpenMM::sire_to_openmm_system(OpenMM::System &system, // all non-perturbable atoms OpenMM::NonbondedForce *cljff = new OpenMM::NonbondedForce(); +#ifdef SIRE_USE_CUSTOMVOLUMEFORCE bool use_dispersion_correction = false; if (map.specified("use_dispersion_correction")) { use_dispersion_correction = map["use_dispersion_correction"].value().asABoolean(); } +#else + if (map.specified("use_dispersion_correction")) + { + SireBase::Console::warning(QObject::tr( + "use_dispersion_correction is not supported because this version of " + "Sire was built against OpenMM < 8.3, which lacks CustomVolumeForce. " + "The option will be ignored.")); + } +#endif + + bool is_gcmc = false; + int num_gcmc_waters = 0; - // note that this will be very slow for perturbable systems, as - // it needs recalculating for every change of lambda - cljff->setUseDispersionCorrection(use_dispersion_correction); + if (map.specified("use_gcmc_lrc")) + { + is_gcmc = map["use_gcmc_lrc"].value().asABoolean(); + } + if (is_gcmc && map.specified("num_gcmc_waters")) + { + num_gcmc_waters = map["num_gcmc_waters"].value().asAnInteger(); + } + + // LRC for the NonbondedForce is handled analytically via a CustomVolumeForce + // (background-lrc) updated each lambda step, so we always disable it here. + cljff->setUseDispersionCorrection(false); // set the non-bonded cutoff type and length based on // the infomation in ffinfo _set_clj_cutoff(*cljff, ffinfo); - // now create the base bond, angle and torsion forcefields + // now create the base bond, angle, torsion and CMAP forcefields OpenMM::HarmonicBondForce *bondff = new OpenMM::HarmonicBondForce(); OpenMM::HarmonicAngleForce *angff = new OpenMM::HarmonicAngleForce(); OpenMM::PeriodicTorsionForce *dihff = new OpenMM::PeriodicTorsionForce(); + OpenMM::CMAPTorsionForce *cmapff = new OpenMM::CMAPTorsionForce(); // now create the engine for computing QM forces on atoms QMForce *qmff = 0; @@ -1207,17 +1267,26 @@ OpenMMMetaData SireOpenMM::sire_to_openmm_system(OpenMM::System &system, LambdaSchedule::standard_morph()); } + // Each named force is placed into its own force group so that energies + // can be queried and cached per-group. The counter starts at 0 and + // increments for each named force added to the system. + int force_group_counter = 0; + // Add any QM force first so that we can guarantee that it is index zero. if (qmff != 0) { + qmff->setForceGroup(force_group_counter); lambda_lever.setForceIndex("qmff", system.addForce(qmff)); + lambda_lever.setForceGroup("qmff", force_group_counter++); lambda_lever.addLever("qm_scale"); } // We can now add the standard forces to the OpenMM::System. // We do this here, so that we can capture the index of the // force and associate it with a name in the lever. + cljff->setForceGroup(force_group_counter); lambda_lever.setForceIndex("clj", system.addForce(cljff)); + lambda_lever.setForceGroup("clj", force_group_counter++); // We also want to name the levers available for this force, // e.g. we can change the charge, sigma and epsilon parameters @@ -1230,15 +1299,21 @@ OpenMMMetaData SireOpenMM::sire_to_openmm_system(OpenMM::System &system, lambda_lever.addLever("lj_scale"); // Do the same for the bond, angle and torsion forces + bondff->setForceGroup(force_group_counter); lambda_lever.setForceIndex("bond", system.addForce(bondff)); + lambda_lever.setForceGroup("bond", force_group_counter++); lambda_lever.addLever("bond_length"); lambda_lever.addLever("bond_k"); + angff->setForceGroup(force_group_counter); lambda_lever.setForceIndex("angle", system.addForce(angff)); + lambda_lever.setForceGroup("angle", force_group_counter++); lambda_lever.addLever("angle_size"); lambda_lever.addLever("angle_k"); + dihff->setForceGroup(force_group_counter); lambda_lever.setForceIndex("torsion", system.addForce(dihff)); + lambda_lever.setForceGroup("torsion", force_group_counter++); lambda_lever.addLever("torsion_phase"); lambda_lever.addLever("torsion_k"); @@ -1252,6 +1327,8 @@ OpenMMMetaData SireOpenMM::sire_to_openmm_system(OpenMM::System &system, /// OpenMM::CustomBondForce *ghost_14ff = 0; + OpenMM::CustomBondForce *ring_breaking_ff = 0; + OpenMM::CustomBondForce *ring_making_ff = 0; OpenMM::CustomNonbondedForce *ghost_ghostff = 0; OpenMM::CustomNonbondedForce *ghost_nonghostff = 0; @@ -1316,29 +1393,23 @@ OpenMMMetaData SireOpenMM::sire_to_openmm_system(OpenMM::System &system, use_taylor_softening = not map["use_zacharias_softening"].value().asABoolean(); } - int coulomb_power = 0; + // use_beutler_softening overrides taylor/zacharias if set + bool use_beutler_softening = false; - if (map.specified("coulomb_power")) + if (map.specified("use_beutler_softening")) { - coulomb_power = map["coulomb_power"].value().asAnInteger(); + use_beutler_softening = map["use_beutler_softening"].value().asABoolean(); } - if (coulomb_power < 0) - coulomb_power = 0; - else if (coulomb_power > 4) - coulomb_power = 4; + double beutler_alpha = 0.5; - auto coulomb_power_expression = [](const QString &alpha, int power) + if (map.specified("beutler_alpha")) { - if (power == 0) - return QString("1"); - else if (power == 1) - return QString("(1-%1)").arg(alpha); - else if (power == 2) - return QString("(1-%1)*(1-%1)").arg(alpha); - else - return QString("(1-%1)^%2").arg(alpha).arg(power); - }; + beutler_alpha = map["beutler_alpha"].value().asADouble(); + } + + if (beutler_alpha < 0.0) + beutler_alpha = 0.0; auto taylor_power_expression = [](const QString &alpha, int power) { @@ -1355,15 +1426,32 @@ OpenMMMetaData SireOpenMM::sire_to_openmm_system(OpenMM::System &system, // see below for the description of this energy expression std::string nb14_expression, clj_expression; - if (use_taylor_softening) + if (use_beutler_softening) + { + // Beutler et al., Chem. Phys. Lett., 1994 + // V_{LJ}(r) = (1-alpha) * 4 epsilon [ + // sigma^12 / (beutler_alpha*sigma^6*alpha + r^6)^2 + // - sigma^6 / (beutler_alpha*sigma^6*alpha + r^6) ] + // V_{coul}(r) = q_i q_j / 4 pi eps_0 sqrt(delta + r^2) + // delta = shift_coulomb^2 * alpha + nb14_expression = QString( + "coul_nrg+lj_nrg;" + "coul_nrg=138.9354558466661*q*((1/sqrt((%1*alpha)+r_safe^2))-(kappa/r_safe));" + "lj_nrg=(1-alpha)*four_epsilon*sig6*(sig6-1);" + "sig6=(sigma^6)/(%2*sigma^6*alpha + r_safe^6);" + "r_safe=max(r, 0.001);") + .arg(shift_coulomb) + .arg(beutler_alpha) + .toStdString(); + } + else if (use_taylor_softening) { nb14_expression = QString( "coul_nrg+lj_nrg;" - "coul_nrg=138.9354558466661*q*(((%1)/sqrt((%2*alpha)+r_safe^2))-(kappa/r_safe));" + "coul_nrg=138.9354558466661*q*((1/sqrt((%1*alpha)+r_safe^2))-(kappa/r_safe));" "lj_nrg=four_epsilon*sig6*(sig6-1);" - "sig6=(sigma^6)/(%3*sigma^6 + r_safe^6);" + "sig6=(sigma^6)/(%2*sigma^6 + r_safe^6);" "r_safe=max(r, 0.001);") - .arg(coulomb_power_expression("alpha", coulomb_power)) .arg(shift_coulomb) .arg(taylor_power_expression("alpha", taylor_power)) .toStdString(); @@ -1372,17 +1460,111 @@ OpenMMMetaData SireOpenMM::sire_to_openmm_system(OpenMM::System &system, { nb14_expression = QString( "coul_nrg+lj_nrg;" - "coul_nrg=138.9354558466661*q*(((%1)/sqrt((%2*alpha)+r_safe^2))-(kappa/r_safe));" + "coul_nrg=138.9354558466661*q*((1/sqrt((%1*alpha)+r_safe^2))-(kappa/r_safe));" "lj_nrg=four_epsilon*sig6*(sig6-1);" "sig6=(sigma^6)/(((sigma*delta) + r_safe^2)^3);" "r_safe=max(r, 0.001);" - "delta=%3*alpha;") - .arg(coulomb_power_expression("alpha", coulomb_power)) + "delta=%2*alpha;") .arg(shift_coulomb) .arg(shift_delta.to(SireUnits::nanometer)) .toStdString(); } + // Ring-breaking/making softcore expressions: same functional form as + // ghost-14 but using global parameters so a single alpha/kappa value + // is shared across all bonds in each force and can be driven by the + // schedule without per-bond tracking infrastructure. + std::string rb_expression, rm_expression; + const bool need_rb = any_ring_breaking or any_ring_making; + + // The ring-break/make CustomBondForce provides only the softcore LJ. + // Coulomb is handled separately: the CLJ exception in NonbondedForce + // carries coul_kappa*q_a0*q_a1, where coul_kappa is a dedicated schedule + // lever that is zero during potential_swap/restraints_off/ring_open and + // ramps 0→1 only during the morph stage (where atoms are already separated + // by the LJ softcore). Decoupling the Coulomb onset from the LJ onset + // avoids spurious attraction when the ring-break pair has opposite partial + // charges and the softcore LJ repulsion is still weak. + if (need_rb and use_beutler_softening) + { + rb_expression = QString( + "lj_nrg;" + "lj_nrg=(1-ring_break_alpha)*four_epsilon*sig6*(sig6-1);" + "sig6=(sigma^6)/(%1*sigma^6*ring_break_alpha + r_safe^6);" + "r_safe=max(r, 0.001);") + .arg(beutler_alpha) + .toStdString(); + rm_expression = QString( + "lj_nrg;" + "lj_nrg=(1-ring_make_alpha)*four_epsilon*sig6*(sig6-1);" + "sig6=(sigma^6)/(%1*sigma^6*ring_make_alpha + r_safe^6);" + "r_safe=max(r, 0.001);") + .arg(beutler_alpha) + .toStdString(); + } + else if (use_taylor_softening) + { + rb_expression = QString( + "lj_nrg;" + "lj_nrg=four_epsilon*sig6*(sig6-1);" + "sig6=(sigma^6)/(%1*sigma^6 + r_safe^6);" + "r_safe=max(r, 0.001);") + .arg(taylor_power_expression("ring_break_alpha", taylor_power)) + .toStdString(); + rm_expression = QString( + "lj_nrg;" + "lj_nrg=four_epsilon*sig6*(sig6-1);" + "sig6=(sigma^6)/(%1*sigma^6 + r_safe^6);" + "r_safe=max(r, 0.001);") + .arg(taylor_power_expression("ring_make_alpha", taylor_power)) + .toStdString(); + } + else + { + rb_expression = QString( + "lj_nrg;" + "lj_nrg=four_epsilon*sig6*(sig6-1);" + "sig6=(sigma^6)/(((sigma*delta)+r_safe^2)^3);" + "r_safe=max(r, 0.001);" + "delta=%1*ring_break_alpha;") + .arg(shift_delta.to(SireUnits::nanometer)) + .toStdString(); + rm_expression = QString( + "lj_nrg;" + "lj_nrg=four_epsilon*sig6*(sig6-1);" + "sig6=(sigma^6)/(((sigma*delta)+r_safe^2)^3);" + "r_safe=max(r, 0.001);" + "delta=%1*ring_make_alpha;") + .arg(shift_delta.to(SireUnits::nanometer)) + .toStdString(); + } + + // ring_break_alpha=1 initially: fully soft at the bonded (ring-closed) + // end state so the pair interaction grows from zero as lambda moves into + // the morph stage. Coulomb is handled by the CLJ exception (coul_kappa + // lever), not per-bond parameters — only sigma and four_epsilon are needed. + if (any_ring_breaking) + { + ring_breaking_ff = new OpenMM::CustomBondForce(rb_expression); + ring_breaking_ff->setName("RingBreakingBondForce"); + ring_breaking_ff->addGlobalParameter("ring_break_alpha", 1.0); + ring_breaking_ff->addPerBondParameter("sigma"); + ring_breaking_ff->addPerBondParameter("four_epsilon"); + ring_breaking_ff->setUsesPeriodicBoundaryConditions(false); + } + + // ring_make_alpha=0 initially: hard at the nonbonded (ring-open) end + // so the pair interacts normally there. Coulomb via CLJ exception only. + if (any_ring_making) + { + ring_making_ff = new OpenMM::CustomBondForce(rm_expression); + ring_making_ff->setName("RingMakingBondForce"); + ring_making_ff->addGlobalParameter("ring_make_alpha", 0.0); + ring_making_ff->addPerBondParameter("sigma"); + ring_making_ff->addPerBondParameter("four_epsilon"); + ring_making_ff->setUsesPeriodicBoundaryConditions(false); + } + ghost_14ff = new OpenMM::CustomBondForce(nb14_expression); ghost_14ff->setName("Ghost14BondForce"); @@ -1396,7 +1578,29 @@ OpenMMMetaData SireOpenMM::sire_to_openmm_system(OpenMM::System &system, // periodic boundaries or cutoffs ghost_14ff->setUsesPeriodicBoundaryConditions(false); - if (use_taylor_softening) + if (use_beutler_softening) + { + // Beutler et al., Chem. Phys. Lett., 1994 + // + // V_{LJ}(r) = (1-alpha) * 4 epsilon [ + // sigma^12 / (beutler_alpha*sigma^6*alpha + r^6)^2 + // - sigma^6 / (beutler_alpha*sigma^6*alpha + r^6) ] + // + // half_sigma and two_sqrt_epsilon are supplied to save cycles. + // + clj_expression = QString("coul_nrg+lj_nrg;" + "coul_nrg=138.9354558466661*q1*q2*((1/sqrt((%1*max_alpha)+r_safe^2))-(max_kappa/r_safe));" + "lj_nrg=(1-max_alpha)*two_sqrt_epsilon1*two_sqrt_epsilon2*sig6*(sig6-1);" + "sig6=(sigma^6)/(%2*sigma^6*max_alpha + r_safe^6);" + "r_safe=max(r, 0.001);" + "max_kappa=max(kappa1, kappa2);" + "max_alpha=max(alpha1, alpha2);" + "sigma=half_sigma1+half_sigma2;") + .arg(shift_coulomb) + .arg(beutler_alpha) + .toStdString(); + } + else if (use_taylor_softening) { // this uses the following potentials // Zacharias and McCammon, J. Chem. Phys., 1994, and also, @@ -1406,7 +1610,7 @@ OpenMMMetaData SireOpenMM::sire_to_openmm_system(OpenMM::System &system, // V_{LJ}(r) = 4 epsilon [ (sigma^12 / (alpha^m sigma^6 + r^6)^2) - // (sigma^6 / (alpha^m sigma^6 + r^6) ) ] // - // V_{coul}(r) = (1-alpha)^n q_i q_j / 4 pi eps_0 (delta+r^2)^(1/2) + // V_{coul}(r) = q_i q_j / 4 pi eps_0 (delta+r^2)^(1/2) // // delta = shift_coulomb^2 * alpha // @@ -1420,14 +1624,13 @@ OpenMMMetaData SireOpenMM::sire_to_openmm_system(OpenMM::System &system, // kJ mol-1 given the units of charge (|e|) and distance (nm) // clj_expression = QString("coul_nrg+lj_nrg;" - "coul_nrg=138.9354558466661*q1*q2*(((%1)/sqrt((%2*max_alpha)+r_safe^2))-(max_kappa/r_safe));" + "coul_nrg=138.9354558466661*q1*q2*((1/sqrt((%1*max_alpha)+r_safe^2))-(max_kappa/r_safe));" "lj_nrg=two_sqrt_epsilon1*two_sqrt_epsilon2*sig6*(sig6-1);" - "sig6=(sigma^6)/(%3*sigma^6 + r_safe^6);" + "sig6=(sigma^6)/(%2*sigma^6 + r_safe^6);" "r_safe=max(r, 0.001);" "max_kappa=max(kappa1, kappa2);" "max_alpha=max(alpha1, alpha2);" "sigma=half_sigma1+half_sigma2;") - .arg(coulomb_power_expression("max_alpha", coulomb_power)) .arg(shift_coulomb) .arg(taylor_power_expression("max_alpha", taylor_power)) .toStdString(); @@ -1443,7 +1646,7 @@ OpenMMMetaData SireOpenMM::sire_to_openmm_system(OpenMM::System &system, // // delta = shift_delta * alpha // - // V_{coul}(r) = (1-alpha)^n q_i q_j / 4 pi eps_0 (delta+r^2)^(1/2) + // V_{coul}(r) = q_i q_j / 4 pi eps_0 (delta+r^2)^(1/2) // // delta = shift_coulomb^2 * alpha // @@ -1458,15 +1661,14 @@ OpenMMMetaData SireOpenMM::sire_to_openmm_system(OpenMM::System &system, // kJ mol-1 given the units of charge (|e|) and distance (nm) // clj_expression = QString("coul_nrg+lj_nrg;" - "coul_nrg=138.9354558466661*q1*q2*(((%1)/sqrt((%2*max_alpha)+r_safe^2))-(max_kappa/r_safe));" + "coul_nrg=138.9354558466661*q1*q2*((1/sqrt((%1*max_alpha)+r_safe^2))-(max_kappa/r_safe));" "lj_nrg=two_sqrt_epsilon1*two_sqrt_epsilon2*sig6*(sig6-1);" "sig6=(sigma^6)/(((sigma*delta) + r_safe^2)^3);" - "delta=%3*max_alpha;" + "delta=%2*max_alpha;" "r_safe=max(r, 0.001);" "max_kappa=max(kappa1, kappa2);" "max_alpha=max(alpha1, alpha2);" "sigma=half_sigma1+half_sigma2;") - .arg(coulomb_power_expression("max_alpha", coulomb_power)) .arg(shift_coulomb) .arg(shift_delta.to(SireUnits::nanometer)) .toStdString(); @@ -1489,10 +1691,10 @@ OpenMMMetaData SireOpenMM::sire_to_openmm_system(OpenMM::System &system, ghost_nonghostff->addPerParticleParameter("alpha"); ghost_nonghostff->addPerParticleParameter("kappa"); - // this will be slow if switched on, as it needs recalculating - // for every change in parameters - ghost_ghostff->setUseLongRangeCorrection(use_dispersion_correction); - ghost_nonghostff->setUseLongRangeCorrection(use_dispersion_correction); + // LRC for the ghost soft-core is handled analytically via a CustomVolumeForce + // (Coulomb has no well-defined LRC; LJ tail is handled by the ghost-lrc force). + ghost_ghostff->setUseLongRangeCorrection(false); + ghost_nonghostff->setUseLongRangeCorrection(false); if (ffinfo.hasCutoff()) { @@ -1516,10 +1718,77 @@ OpenMMMetaData SireOpenMM::sire_to_openmm_system(OpenMM::System &system, ghost_nonghostff->setNonbondedMethod(OpenMM::CustomNonbondedForce::NoCutoff); } + ghost_ghostff->setForceGroup(force_group_counter); lambda_lever.setForceIndex("ghost/ghost", system.addForce(ghost_ghostff)); + lambda_lever.setForceGroup("ghost/ghost", force_group_counter++); + + ghost_nonghostff->setForceGroup(force_group_counter); lambda_lever.setForceIndex("ghost/non-ghost", system.addForce(ghost_nonghostff)); + lambda_lever.setForceGroup("ghost/non-ghost", force_group_counter++); + +#ifdef SIRE_USE_CUSTOMVOLUMEFORCE + // Analytic LJ LRC: E = lrc_coeff / V, updated each lambda step via + // a cached closed-form sum over interaction-group pairs. + if (use_dispersion_correction && ffinfo.hasCutoff() && ffinfo.space().isPeriodic()) + { + auto ghost_lrc_ff = new OpenMM::CustomVolumeForce("lrc_coeff*lrc_scale/v"); + ghost_lrc_ff->addGlobalParameter("lrc_coeff", 0.0); + ghost_lrc_ff->addGlobalParameter("lrc_scale", 1.0); + ghost_lrc_ff->setName("GhostLRCForce"); + ghost_lrc_ff->setForceGroup(force_group_counter); + lambda_lever.setForceIndex("ghost-lrc", system.addForce(ghost_lrc_ff)); + lambda_lever.setForceGroup("ghost-lrc", force_group_counter++); + } +#endif // SIRE_USE_CUSTOMVOLUMEFORCE + + ghost_14ff->setForceGroup(force_group_counter); lambda_lever.setForceIndex("ghost-14", system.addForce(ghost_14ff)); + lambda_lever.setForceGroup("ghost-14", force_group_counter++); + + if (ring_breaking_ff != 0) + { + ring_breaking_ff->setForceGroup(force_group_counter); + lambda_lever.setForceIndex("ring-break", system.addForce(ring_breaking_ff)); + lambda_lever.setForceGroup("ring-break", force_group_counter++); + } + + if (ring_making_ff != 0) + { + ring_making_ff->setForceGroup(force_group_counter); + lambda_lever.setForceIndex("ring-make", system.addForce(ring_making_ff)); + lambda_lever.setForceGroup("ring-make", force_group_counter++); + } + } + +#ifdef SIRE_USE_CUSTOMVOLUMEFORCE + // Analytic LRC for the NonbondedForce (all non-ghost atoms): E = lrc_background / V, + // updated each lambda step via a cached closed-form class-pair sum. + if (use_dispersion_correction && ffinfo.hasCutoff() && ffinfo.space().isPeriodic()) + { + auto background_lrc_ff = new OpenMM::CustomVolumeForce("lrc_background/v"); + background_lrc_ff->addGlobalParameter("lrc_background", 0.0); + background_lrc_ff->setName("BackgroundLRCForce"); + background_lrc_ff->setForceGroup(force_group_counter); + lambda_lever.setForceIndex("background-lrc", system.addForce(background_lrc_ff)); + lambda_lever.setForceGroup("background-lrc", force_group_counter++); + } + + // GCMC water LRC: E = (n_w * lrc_w_solute + n_w*(n_w-1) * lrc_ww_half) / V. + // lrc_w_solute and lrc_ww_half are pre-computed at setup; n_w is updated by + // the GCMC sampler at each insertion/deletion move. + if (is_gcmc && use_dispersion_correction && ffinfo.hasCutoff() && ffinfo.space().isPeriodic()) + { + auto gcmc_lrc_ff = new OpenMM::CustomVolumeForce( + "(n_w * lrc_w_solute + n_w * (n_w - 1) * lrc_ww_half) / v"); + gcmc_lrc_ff->addGlobalParameter("n_w", 0.0); + gcmc_lrc_ff->addGlobalParameter("lrc_w_solute", 0.0); + gcmc_lrc_ff->addGlobalParameter("lrc_ww_half", 0.0); + gcmc_lrc_ff->setName("GCMCLRCForce"); + gcmc_lrc_ff->setForceGroup(force_group_counter); + lambda_lever.setForceIndex("gcmc-lrc", system.addForce(gcmc_lrc_ff)); + lambda_lever.setForceGroup("gcmc-lrc", force_group_counter++); } +#endif // SIRE_USE_CUSTOMVOLUMEFORCE // Stage 4 is complete. We now have all(*) of the forces we need to run // a perturbable simulation. (*) well, we will define the restraint @@ -1560,29 +1829,45 @@ OpenMMMetaData SireOpenMM::sire_to_openmm_system(OpenMM::System &system, QVector ghost_atoms; QVector non_ghost_atoms; + // indices for real atoms (i.e. not virtual sites) + // required so that restraints are applied to the correct particles + QVector real_atoms; + // count the number of atoms and ghost atoms int n_atoms = 0; int n_ghost_atoms = 0; + int n_vs = 0; for (int i = 0; i < nmols; ++i) { const auto &mol = openmm_mols_data[i]; n_atoms += mol.nAtoms(); n_ghost_atoms += mol.nGhostAtoms(); + if (mol.has_vs) + { + n_vs += mol.n_vs; + } } // there's probably lots of optimisations we can make if the // number of ghost atoms is zero... - ghost_atoms.reserve(n_ghost_atoms); - non_ghost_atoms.reserve(n_atoms - n_ghost_atoms); + // Making sure there is space in all arrays for virtual sites to be ghosts + ghost_atoms.reserve(n_ghost_atoms + n_vs); + non_ghost_atoms.reserve(n_atoms - n_ghost_atoms + n_vs); // the set of all ghost atoms, with the value // indicating if this is a from-ghost (true) or // a to-ghost (false) QVector from_ghost_idxs; QVector to_ghost_idxs; - from_ghost_idxs.reserve(n_ghost_atoms); - to_ghost_idxs.reserve(n_ghost_atoms); + from_ghost_idxs.reserve(n_ghost_atoms + n_vs); + to_ghost_idxs.reserve(n_ghost_atoms + n_vs); + + // map from CMAPParameter to OpenMM map index for non-perturbable molecules + // (enables sharing identical grids across molecules) + QHash shared_cmap_map_indices; + + const double cmap_k_to_openmm = (SireUnits::kcal_per_mol).to(SireUnits::kJ_per_mol); // loop over every molecule and add them one by one for (int i = 0; i < nmols; ++i) @@ -1633,6 +1918,13 @@ OpenMMMetaData SireOpenMM::sire_to_openmm_system(OpenMM::System &system, start_indicies.insert("angle", angff->getNumAngles()); start_indicies.insert("torsion", dihff->getNumTorsions()); + // Only record a CMAP start index if this molecule actually has + // CMAP torsions. If we always insert here, start_index is never + // -1 in the lambda lever, and updateParametersInContext is called + // on an uninitialised GPU force for molecules with no CMAP terms. + if (not mol.cmap_params.isEmpty()) + start_indicies.insert("cmap", cmapff->getNumMaps()); + // we can now record this as a perturbable molecule // in the lambda lever. The returned index is the // index of this perturbable molecule in the list @@ -1709,6 +2001,8 @@ OpenMMMetaData SireOpenMM::sire_to_openmm_system(OpenMM::System &system, ghost_ghostff->addParticle(custom_params); ghost_nonghostff->addParticle(custom_params); + real_atoms.append(atom_index); + if (is_from_ghost or is_to_ghost) { // this is a ghost atom! We need to record this @@ -1768,6 +2062,8 @@ OpenMMMetaData SireOpenMM::sire_to_openmm_system(OpenMM::System &system, // Add the particle to the standard CLJ forcefield cljff->addParticle(boost::get<0>(clj), boost::get<1>(clj), boost::get<2>(clj)); + real_atoms.append(atom_index); + // We need to add this molecule to the ghost and ghost // forcefields if there are any perturbable molecules if (any_perturbable) @@ -1788,6 +2084,126 @@ OpenMMMetaData SireOpenMM::sire_to_openmm_system(OpenMM::System &system, } } } + if (mol.has_vs) + { + int start_vs = start_index + mol.molinfo.nAtoms(); + + for (int k = 0; k < mol.n_vs; ++k) + { + SireBase::Properties vs_params = mol.vs_properties.property(std::to_string(k).c_str()).asA(); + // Add parameters to system + // Virtual sites with non-zero LJ interactions are not supported + const int atom_index = start_vs + k; + system.addParticle(0.0); + + // Calculate virtual site parameters + SireBase::PropertyList indices = vs_params.property("vs_indices").asAnArray(); + std::vector indices_vec = {}; + for (int a = 0; a < indices.size(); ++a) + { + indices_vec.push_back(indices.at(a).asAnInteger() + start_index); + } + int parent_idx = indices.at(0).asAnInteger(); + + SireBase::PropertyList ows = vs_params.property("vs_ows").asAnArray(); + std::vector ows_vec = {}; + for (int a = 0; a < ows.size(); ++a) + { + ows_vec.push_back(ows.at(a).asADouble()); + } + + SireBase::PropertyList xs = vs_params.property("vs_xs").asAnArray(); + std::vector xs_vec = {}; + for (int a = 0; a < xs.size(); ++a) + { + xs_vec.push_back(xs.at(a).asADouble()); + } + + SireBase::PropertyList ys = vs_params.property("vs_ys").asAnArray(); + std::vector ys_vec = {}; + for (int a = 0; a < ys.size(); ++a) + { + ys_vec.push_back(ys.at(a).asADouble()); + } + + SireBase::PropertyList local = vs_params.property("vs_local").asAnArray(); + OpenMM::Vec3 local_vec = {local.at(0).asADouble(), local.at(1).asADouble(), local.at(2).asADouble()}; + + OpenMM::LocalCoordinatesSite *new_vs = new OpenMM::LocalCoordinatesSite(indices_vec, ows_vec, xs_vec, ys_vec, local_vec); + system.setVirtualSite(atom_index, new_vs); + + // Add to forcefield, depending on whether the system is perturbable + // Note that VS with LJ parameters are currently not supported, + // so epsilon and sigma are hard-coded to 0 in all cases + double vs_charge = mol.vs_charges.at(k).asADouble(); + cljff->addParticle(vs_charge, 1.0, 0.0); + + if (any_perturbable and mol.isPerturbable()) + { + // reduced_q + custom_params[0] = vs_charge; + // half_sigma + custom_params[1] = 1.0; + // two_sqrt_epsilon + custom_params[2] = 0.0; + // alpha + custom_params[3] = alphas_data[mol.molinfo.nAtoms() + k]; + // kappa + custom_params[4] = kappas_data[mol.molinfo.nAtoms() + k]; + + ghost_ghostff->addParticle(custom_params); + ghost_nonghostff->addParticle(custom_params); + + const bool vs_to_ghost = mol.to_ghost_idxs.contains(parent_idx); + const bool vs_from_ghost = mol.from_ghost_idxs.contains(parent_idx); + + // Append virtual sites to ghost atom list + + if (vs_to_ghost) + { + ghost_atoms.append(atom_index); + to_ghost_idxs.append(atom_index); + } + else if (vs_from_ghost) + { + ghost_atoms.append(atom_index); + from_ghost_idxs.append(atom_index); + } + } + else if (any_perturbable) + { + // Add to ghost FFs if necessary + custom_params = {vs_charge, 1.0, 0.0, 0.0, 0.0}; + ghost_ghostff->addParticle(custom_params); + ghost_nonghostff->addParticle(custom_params); + non_ghost_atoms.append(atom_index); + } + } + } + + // Register virtual sites (OPC, TIP4P, TIP5P, …). + // setVirtualSite() must be called after all particles have been added + // to the System but before the Context is created. + for (const auto &vs : mol.virtual_sites) + { + const int vsite_atom = vs.vsite_idx + start_index; + const int p1 = vs.p1_idx + start_index; + const int p2 = vs.p2_idx + start_index; + const int p3 = vs.p3_idx + start_index; + + if (vs.type == VirtualSiteInfo::ThreeParticleAverage) + { + system.setVirtualSite(vsite_atom, + new OpenMM::ThreeParticleAverageSite( + p1, p2, p3, vs.w1, vs.w2, vs.w3)); + } + else + { + system.setVirtualSite(vsite_atom, + new OpenMM::OutOfPlaneSite( + p1, p2, p3, vs.w12, vs.w13, vs.wCross)); + } + } // now add all of the bond parameters for (const auto &bond : mol.bond_params) @@ -1816,6 +2232,96 @@ OpenMMMetaData SireOpenMM::sire_to_openmm_system(OpenMM::System &system, boost::get<4>(dih), boost::get<5>(dih), boost::get<6>(dih)); } + // now add all of the CMAP torsions + // CMAP defines two consecutive dihedrals sharing atoms 1-3: + // phi: atom0-atom1-atom2-atom3 + // psi: atom1-atom2-atom3-atom4 + if (any_perturbable and mol.isPerturbable()) + { + // perturbable molecules get a dedicated map per CMAP torsion + // (so the map values can be updated at each lambda step) + for (const auto &cmap : mol.cmap_params) + { + const auto ¶m = boost::get<5>(cmap); + // Apply the same AMBER→OpenMM grid transformation used by OpenMM's + // amber_file_parser.py: cyclic N/2 shift in both axes with phi↔psi swap. + // idx = ngrid*((j+ngrid//2)%ngrid)+((i+ngrid//2)%ngrid) + // where i=phi (outer/slow) and j=psi (inner/fast) in the output. + const auto flat_in = param.grid().toColumnMajorVector(); + const int N = param.nRows(); + + std::vector energy(N * N); + + for (int phi = 0; phi < N; ++phi) + { + for (int psi = 0; psi < N; ++psi) + { + const int src = ((psi + N / 2) % N) * N + ((phi + N / 2) % N); + energy[phi * N + psi] = flat_in[src] * cmap_k_to_openmm; + } + } + + const int map_index = cmapff->addMap(N, energy); + + cmapff->addTorsion(map_index, + boost::get<0>(cmap) + start_index, + boost::get<1>(cmap) + start_index, + boost::get<2>(cmap) + start_index, + boost::get<3>(cmap) + start_index, + boost::get<1>(cmap) + start_index, + boost::get<2>(cmap) + start_index, + boost::get<3>(cmap) + start_index, + boost::get<4>(cmap) + start_index); + } + } + else + { + // non-perturbable molecules share maps (deduplicated by CMAPParameter) + for (const auto &cmap : mol.cmap_params) + { + const auto ¶m = boost::get<5>(cmap); + int map_index; + + if (shared_cmap_map_indices.contains(param)) + { + map_index = shared_cmap_map_indices[param]; + } + else + { + // Apply the same AMBER→OpenMM grid transformation used by OpenMM's + // amber_file_parser.py: cyclic N/2 shift in both axes with phi↔psi swap. + // idx = ngrid*((j+ngrid//2)%ngrid)+((i+ngrid//2)%ngrid) + // where i=phi (outer/slow) and j=psi (inner/fast) in the output. + const auto flat_in = param.grid().toColumnMajorVector(); + const int N = param.nRows(); + + std::vector energy(N * N); + + for (int phi = 0; phi < N; ++phi) + { + for (int psi = 0; psi < N; ++psi) + { + const int src = ((psi + N / 2) % N) * N + ((phi + N / 2) % N); + energy[phi * N + psi] = flat_in[src] * cmap_k_to_openmm; + } + } + + map_index = cmapff->addMap(N, energy); + shared_cmap_map_indices.insert(param, map_index); + } + + cmapff->addTorsion(map_index, + boost::get<0>(cmap) + start_index, + boost::get<1>(cmap) + start_index, + boost::get<2>(cmap) + start_index, + boost::get<3>(cmap) + start_index, + boost::get<1>(cmap) + start_index, + boost::get<2>(cmap) + start_index, + boost::get<3>(cmap) + start_index, + boost::get<4>(cmap) + start_index); + } + } + for (const auto &constraint : mol.constraints) { const auto atom0 = boost::get<0>(constraint); @@ -1836,6 +2342,10 @@ OpenMMMetaData SireOpenMM::sire_to_openmm_system(OpenMM::System &system, } start_index += mol.masses.count(); + if (mol.has_vs) + { + start_index += mol.n_vs; + } } /// Finally tell the ghost forcefields about the ghost and non-ghost @@ -1851,6 +2361,130 @@ OpenMMMetaData SireOpenMM::sire_to_openmm_system(OpenMM::System &system, ghost_nonghostff->addInteractionGroup(ghost_atoms_set, non_ghost_atoms_set); } +#ifdef SIRE_USE_CUSTOMVOLUMEFORCE + // Register GCMC water atom indices with the lambda lever and pre-compute the + // fixed LRC coefficients (lrc_w_solute and lrc_ww_half) for the gcmc-lrc force. + if (is_gcmc && use_dispersion_correction && ffinfo.hasCutoff() && ffinfo.space().isPeriodic()) + { + auto gcmc_lrc_ff = lambda_lever.getForce("gcmc-lrc", system); + if (gcmc_lrc_ff != nullptr) + { + const double cutoff = cljff->getCutoffDistance(); + const double rc3 = cutoff * cutoff * cutoff; + const double rc9 = rc3 * rc3 * rc3; + const double four_pi = 4.0 * M_PI; + + // All GCMC waters (real + virtual buffer) are excluded from the background + // LRC and tracked by n_w instead. Any water can be swapped in or out. + const auto water_result = mols.search("water"); + QSet water_mol_nums; + for (const auto &view : water_result.views()) + water_mol_nums.insert(view.data().number()); + + // Collect OpenMM atom indices for all water molecules. + QVector water_atom_indices; + for (int i = 0; i < nmols; ++i) + { + if (!water_mol_nums.contains(mols[i].number())) + continue; + const int mol_start = start_indexes[i]; + const int mol_natoms = openmm_mols_data[i].masses.count(); + for (int j = mol_start; j < mol_start + mol_natoms; ++j) + water_atom_indices.append(j); + } + + lambda_lever.setGCMCWaterAtoms(water_atom_indices); + + // Pre-compute lrc_w_solute (per active water molecule, interaction with + // all non-water atoms: protein, ligand, ions) and lrc_ww_half (per active + // water-molecule pair, halved). FEP ghost atoms have epsilon=0 and + // contribute zero automatically. + // n_w starts at n_all_waters - num_gcmc_waters (initially active count). + QSet water_set(water_atom_indices.begin(), water_atom_indices.end()); + + // Collect (sigma, epsilon) for water atom types and non-water solute atoms. + // Real waters are also in the GCMC pool so solute = protein + ligand + ions. + std::map, int> water_class_counts; + std::map, int> solute_class_counts; + for (int i = 0; i < cljff->getNumParticles(); ++i) + { + double charge, sigma, epsilon; + cljff->getParticleParameters(i, charge, sigma, epsilon); + if (epsilon == 0.0) + continue; + if (water_set.contains(i)) + water_class_counts[{sigma, epsilon}]++; + else + solute_class_counts[{sigma, epsilon}]++; + } + + // lrc_ww_half: half the LRC coefficient for one water-molecule pair. + // Sum over all water-atom-type pairs within a molecule pair. + // Each molecule pair contributes once (factor 1/2 already in the name). + const int n_water_mols = water_mol_nums.size(); + double lrc_ww = 0.0; + // diagonal water-water class pairs (same type within a water molecule pair) + for (const auto &[key, n] : water_class_counts) + { + // n atoms of this type spread across n_water_mols molecules: + // per-mol count = n / n_water_mols + const double per_mol = static_cast(n) / n_water_mols; + const double n_pairs = per_mol * per_mol; + const double sig2 = key.first * key.first; + const double sig6 = sig2 * sig2 * sig2; + const double eps_pair = 4.0 * key.second; + lrc_ww += n_pairs * four_pi * eps_pair * sig6 * (sig6 / (9.0 * rc9) - 1.0 / (3.0 * rc3)); + } + // off-diagonal water-water class pairs + for (auto it1 = water_class_counts.cbegin(); it1 != water_class_counts.cend(); ++it1) + { + auto it2 = it1; + for (++it2; it2 != water_class_counts.cend(); ++it2) + { + const double per_mol_1 = static_cast(it1->second) / n_water_mols; + const double per_mol_2 = static_cast(it2->second) / n_water_mols; + const double sigma_ij = 0.5 * (it1->first.first + it2->first.first); + const double eps_pair = 4.0 * std::sqrt(it1->first.second * it2->first.second); + const double sig2 = sigma_ij * sigma_ij; + const double sig6 = sig2 * sig2 * sig2; + lrc_ww += 2.0 * per_mol_1 * per_mol_2 * four_pi * eps_pair * sig6 * (sig6 / (9.0 * rc9) - 1.0 / (3.0 * rc3)); + } + } + const double lrc_ww_half = 0.5 * lrc_ww; + + // lrc_w_solute: LRC coefficient for one water molecule with all solute atoms. + double lrc_w_solute = 0.0; + for (const auto &[wkey, wn] : water_class_counts) + { + const double per_mol_w = static_cast(wn) / n_water_mols; + for (const auto &[skey, sn] : solute_class_counts) + { + const double sigma_ij = 0.5 * (wkey.first + skey.first); + const double eps_pair = 4.0 * std::sqrt(wkey.second * skey.second); + const double sig2 = sigma_ij * sigma_ij; + const double sig6 = sig2 * sig2 * sig2; + lrc_w_solute += per_mol_w * sn * four_pi * eps_pair * sig6 * (sig6 / (9.0 * rc9) - 1.0 / (3.0 * rc3)); + } + } + + // Update the CustomVolumeForce default parameter values so they are + // correct when the OpenMM Context is created. + // n_w starts at the number of initially active waters (total - buffer). + const double n_w_initial = static_cast(n_water_mols - num_gcmc_waters); + for (int p = 0; p < gcmc_lrc_ff->getNumGlobalParameters(); ++p) + { + const auto &name = gcmc_lrc_ff->getGlobalParameterName(p); + if (name == "n_w") + gcmc_lrc_ff->setGlobalParameterDefaultValue(p, n_w_initial); + else if (name == "lrc_w_solute") + gcmc_lrc_ff->setGlobalParameterDefaultValue(p, lrc_w_solute); + else if (name == "lrc_ww_half") + gcmc_lrc_ff->setGlobalParameterDefaultValue(p, lrc_ww_half); + } + } + } +#endif // SIRE_USE_CUSTOMVOLUMEFORCE + // see if we want to remove COM motion const auto com_remove_prop = map["com_reset_frequency"]; @@ -1868,6 +2502,22 @@ OpenMMMetaData SireOpenMM::sire_to_openmm_system(OpenMM::System &system, /// Stage 5 is complete. We have added all of the parameter data /// for the molecules to the OpenMM forces + // Only register the CMAP force if terms were actually added during the + // molecule loop. An empty CMAPTorsionForce wastes a force-group slot and + // launches a zero-work kernel on every step. + if (cmapff->getNumMaps() > 0) + { + cmapff->setForceGroup(force_group_counter); + lambda_lever.setForceIndex("cmap", system.addForce(cmapff)); + lambda_lever.setForceGroup("cmap", force_group_counter++); + lambda_lever.addLever("cmap_grid"); + } + else + { + delete cmapff; + cmapff = nullptr; + } + /// /// Stage 6 - Set up the exceptions and perturbable constraints /// @@ -1889,12 +2539,34 @@ OpenMMMetaData SireOpenMM::sire_to_openmm_system(OpenMM::System &system, { int start_index = start_indexes[i]; const auto &mol = openmm_mols_data[i]; + auto cljs_data = mol.cljs.constData(); QVector> exception_idxs; QVector constraint_idxs; const bool is_perturbable = any_perturbable and mol.isPerturbable(); + // Build local sets of ring-breaking/making pairs (molecule-local + // indices) for fast lookup during exception processing. + QSet rb_pairs_local, rm_pairs_local; + // Per-exception index arrays for ring-break/make CLJ exceptions. + // Filled alongside exception_idxs; (-1,-1) for non-ring pairs. + QVector> rb_exception_idxs; + QVector> rm_exception_idxs; + int rb_bond_count = 0; + int rm_bond_count = 0; + if (is_perturbable) + { + for (const auto &p : mol.ring_breaking_pairs) + rb_pairs_local.insert(IndexPair(p.first, p.second)); + for (const auto &p : mol.ring_making_pairs) + rm_pairs_local.insert(IndexPair(p.first, p.second)); + rb_exception_idxs = QVector>( + mol.exception_params.count(), boost::make_tuple(-1, -1)); + rm_exception_idxs = QVector>( + mol.exception_params.count(), boost::make_tuple(-1, -1)); + } + if (is_perturbable) { exception_idxs = QVector>(mol.exception_params.count(), @@ -1946,8 +2618,17 @@ OpenMMMetaData SireOpenMM::sire_to_openmm_system(OpenMM::System &system, int idx = -1; int nbidx = -1; + bool is_ring_breaking = rb_pairs_local.contains(IndexPair(atom0, atom1)); + bool is_ring_making = rm_pairs_local.contains(IndexPair(atom0, atom1)); + if (atom0_is_ghost or atom1_is_ghost) { + // don't add ring-breaking/making forces for pairs involving ghost atoms, + // since the GhostNonbondedForce already provides a softcore interaction + // for these pairs. + is_ring_breaking = false; + is_ring_making = false; + // don't include the LJ term, as this is calculated // elsewhere - note that we need to use 1e-9 to // make sure that OpenMM doesn't eagerly remove @@ -1960,7 +2641,25 @@ OpenMMMetaData SireOpenMM::sire_to_openmm_system(OpenMM::System &system, auto to_from_ghost = (from_ghost_idxs.contains(atom0) and to_ghost_idxs.contains(atom1)) or (from_ghost_idxs.contains(atom1) and to_ghost_idxs.contains(atom0)); - if (not to_from_ghost and (coul_14_scl != 0 or lj_14_scl != 0)) + // Whether a ghost-14 slot is needed is decided here, once, + // for the lifetime of the OpenMM::CustomBondForce - bonds + // can't be added to it later, only updated. coul_14_scl/ + // lj_14_scl are this pair's state0 scale only; also check + // the aligned state1 entry at the same index, since a pair + // can be fully excluded in one end state's connectivity + // and fully (or partially) unmasked in the other - e.g. + // a 1-3 pair that becomes unmasked once a ring-breaking + // bond removes the path between them. Missing this means + // the pair is permanently excluded from ghost/non-ghost + // and ghost/ghost too (their exclusion lists are built + // from this same state0-only loop below), with no way to + // recover the real interaction at the end where it exists. + const auto &pert_param = mol.perturbed->exception_params[j]; + const auto coul_14_scl1 = boost::get<2>(pert_param); + const auto lj_14_scl1 = boost::get<3>(pert_param); + + if (not to_from_ghost and (coul_14_scl != 0 or lj_14_scl != 0 or + coul_14_scl1 != 0 or lj_14_scl1 != 0)) { // this is a 1-4 interaction that should be added // to the ghost-14 forcefield @@ -1991,6 +2690,75 @@ OpenMMMetaData SireOpenMM::sire_to_openmm_system(OpenMM::System &system, excluded_ghost_pairs.insert(IndexPair(boost::get<0>(p), boost::get<1>(p))); } } + else if (is_ring_breaking or is_ring_making) + { + // LJ stays at 1e-9 throughout: the ring-break/make CustomBondForce + // provides the full softcore LJ. The Coulomb charge is initialised + // to the correct value for each direction so the NonbondedForce + // carries the right hard Coulomb (including RF/PME) from the start. + // nbidx stays -1 (no ghost-14 bond for ring-break/make pairs). + + if (is_ring_breaking and ring_breaking_ff != 0) + { + // LJ parameters from the nonbonded end state (λ=1, + // perturbed), where the bond is absent and the pair + // interacts normally. Coulomb is handled by the CLJ + // exception via the coul_kappa lever in lambdalever.cpp, + // not per-bond parameters. + auto pp = mol.perturbed->getException( + atom0, atom1, start_index, 1.0, 1.0); + std::vector params_rb = { + boost::get<3>(pp), + 4.0 * boost::get<4>(pp)}; + if (params_rb[0] == 0) + params_rb[0] = 1e-9; + // Initial coul_kappa=0 for ring-break: charge starts at zero. + // Use 1e-9 to prevent OpenMM from pruning the exception. + idx = cljff->addException(boost::get<0>(p), boost::get<1>(p), + 1e-9, 1e-9, 1e-9, true); + ring_breaking_ff->addBond(boost::get<0>(p), + boost::get<1>(p), + params_rb); + rb_exception_idxs[j] = boost::make_tuple(idx, rb_bond_count); + ++rb_bond_count; + } + else if (is_ring_making and ring_making_ff != 0) + { + // LJ parameters from the nonbonded end state (λ=0, + // reference), where the bond is absent. Coulomb is handled + // by the CLJ exception via the coul_kappa lever. + auto pp = mol.getException( + atom0, atom1, start_index, 1.0, 1.0); + std::vector params_rm = { + boost::get<3>(pp), + 4.0 * boost::get<4>(pp)}; + if (params_rm[0] == 0) + params_rm[0] = 1e-9; + // Initial coul_kappa=1 for ring-make: charge starts at the + // full state0 charge product so the CLJ exception carries the + // correct hard Coulomb from the very first energy evaluation. + double init_charge = boost::get<2>(pp); + if (init_charge == 0) + init_charge = 1e-9; + idx = cljff->addException(boost::get<0>(p), boost::get<1>(p), + init_charge, 1e-9, 1e-9, true); + ring_making_ff->addBond(boost::get<0>(p), + boost::get<1>(p), + params_rm); + rm_exception_idxs[j] = boost::make_tuple(idx, rm_bond_count); + ++rm_bond_count; + } + else + { + // force not yet created (should not occur in practice) + idx = cljff->addException(boost::get<0>(p), boost::get<1>(p), + 1e-9, 1e-9, 1e-9, true); + } + + // Reset idx to -1 so the main exception_idxs guard skips + // these pairs — they are tracked separately via rb/rm_exception_idxs. + idx = -1; + } else { idx = cljff->addException(boost::get<0>(p), boost::get<1>(p), @@ -2022,10 +2790,53 @@ OpenMMMetaData SireOpenMM::sire_to_openmm_system(OpenMM::System &system, { auto pert_idx = idx_to_pert_idx.value(i, openmm_mols.count() + 1); lambda_lever.setExceptionIndicies(pert_idx, - "clj", exception_idxs); + "clj", exception_idxs); + if (rb_bond_count > 0) + lambda_lever.setExceptionIndicies(pert_idx, + "ring-break", rb_exception_idxs); + if (rm_bond_count > 0) + lambda_lever.setExceptionIndicies(pert_idx, + "ring-make", rm_exception_idxs); lambda_lever.setConstraintIndicies(pert_idx, constraint_idxs); } + + // Exclusions/exceptions between virtual sites on the same atom, and with the parent atom + if (mol.has_vs) + { + int n_atom_vs; + int vs_start = start_index + mol.nAtoms(); + for (int a = 0; a < mol.nAtoms(); ++a) + { + SireBase::PropertyList atom_vs = mol.vs_parents.property(std::to_string(a).c_str()).asAnArray(); + n_atom_vs = atom_vs.size(); + for (int v0 = 0; v0 < atom_vs.size(); ++v0) + { + int vs0_index = vs_start + atom_vs.at(v0).asAnInteger(); + cljff->addException(vs0_index, start_index + a, + 0.0, 1, + 0, false); + if (ghost_ghostff != 0) + { + ghost_ghostff->addExclusion(vs0_index, start_index + a); + ghost_nonghostff->addExclusion(vs0_index, start_index + a); + } + + for (int v1 = v0 + 1; v1 < atom_vs.size(); ++v1) + { + int vs1_index = vs_start + atom_vs.at(v1).asAnInteger(); + cljff->addException(vs0_index, vs1_index, + 0.0, 1, + 0, false); + if (ghost_ghostff != 0) + { + ghost_ghostff->addExclusion(vs0_index, vs1_index); + ghost_nonghostff->addExclusion(vs0_index, vs1_index); + } + } + } + } + } } // go through all of the ghost atoms and exclude interactions @@ -2053,7 +2864,7 @@ OpenMMMetaData SireOpenMM::sire_to_openmm_system(OpenMM::System &system, ghost_ghostff->addExclusion(from_ghost_idx, to_ghost_idx); ghost_nonghostff->addExclusion(from_ghost_idx, to_ghost_idx); cljff->addException(from_ghost_idx, to_ghost_idx, - 0.0, 1e-9, 1e-9, true); + 0.0, 1e-9, 1e-9, false); } } } @@ -2099,42 +2910,43 @@ OpenMMMetaData SireOpenMM::sire_to_openmm_system(OpenMM::System &system, if (prop.read().isA()) { _add_dihedral_restraints(prop.read().asA(), - system, lambda_lever, start_index); + system, lambda_lever, start_index, real_atoms, force_group_counter); } else if (prop.read().isA()) { _add_angle_restraints(prop.read().asA(), - system, lambda_lever, start_index); + system, lambda_lever, start_index, real_atoms, force_group_counter); } else if (prop.read().isA()) { _add_positional_restraints(prop.read().asA(), - system, lambda_lever, anchor_coords, start_index); + system, lambda_lever, anchor_coords, start_index, real_atoms, + force_group_counter); } else if (prop.read().isA()) { _add_morse_potential_restraints(prop.read().asA(), - system, lambda_lever, start_index); + system, lambda_lever, start_index, real_atoms, force_group_counter); } else if (prop.read().isA()) { _add_rmsd_restraints(prop.read().asA(), - system, lambda_lever, start_index); + system, lambda_lever, start_index, real_atoms, force_group_counter); } else if (prop.read().isA()) { _add_bond_restraints(prop.read().asA(), - system, lambda_lever, start_index); + system, lambda_lever, start_index, real_atoms, force_group_counter); } else if (prop.read().isA()) { _add_inverse_bond_restraints(prop.read().asA(), - system, lambda_lever, start_index); + system, lambda_lever, start_index, real_atoms, force_group_counter); } else if (prop.read().isA()) { _add_boresch_restraints(prop.read().asA(), - system, lambda_lever, start_index); + system, lambda_lever, start_index, real_atoms, force_group_counter); } } } @@ -2159,7 +2971,7 @@ OpenMMMetaData SireOpenMM::sire_to_openmm_system(OpenMM::System &system, if (prop.read().isA()) { _add_inverse_bond_restraints(prop.read().asA(), - system, lambda_lever, start_index); + system, lambda_lever, start_index, real_atoms, force_group_counter); } } } @@ -2198,6 +3010,16 @@ OpenMMMetaData SireOpenMM::sire_to_openmm_system(OpenMM::System &system, const auto &mol = openmm_mols_data[i]; mol.copyInCoordsAndVelocities(coords_data + start_index, vels_data + start_index); + if (mol.has_vs) + { + // Initiate all VS with zero coords, as they will need to be + // calculated in the openmm context anyway + for (int vs = 0; vs < mol.n_vs; ++vs) + { + coords_data[start_index+mol.nAtoms()+vs] = OpenMM::Vec3(0,0,0); + vels_data[start_index+mol.nAtoms()+vs] = OpenMM::Vec3(0,0,0); + } + } } }); } else @@ -2208,6 +3030,14 @@ OpenMMMetaData SireOpenMM::sire_to_openmm_system(OpenMM::System &system, const auto &mol = openmm_mols_data[i]; mol.copyInCoordsAndVelocities(coords_data + start_index, vels_data + start_index); + if (mol.has_vs) + { + for (int vs = 0; vs < mol.n_vs; ++vs) + { + coords_data[start_index + mol.nAtoms() + vs] = OpenMM::Vec3(0, 0, 0); + vels_data[start_index + mol.nAtoms() + vs] = OpenMM::Vec3(0, 0, 0); + } + } } } diff --git a/wrapper/Convert/SireOpenMM/torchqm.cpp b/wrapper/Convert/SireOpenMM/torchqm.cpp index 8b36db7c8..40c56acb2 100644 --- a/wrapper/Convert/SireOpenMM/torchqm.cpp +++ b/wrapper/Convert/SireOpenMM/torchqm.cpp @@ -33,6 +33,8 @@ #include #endif +#include + #include "SireError/errors.h" #include "SireMaths/vector.h" #include "SireStream/datastream.h" @@ -61,14 +63,15 @@ static const RegisterMetaType r_torchqmforce(NO_ROOT); QDataStream &operator<<(QDataStream &ds, const TorchQMForce &torchqmforce) { - writeHeader(ds, r_torchqmforce, 1); + writeHeader(ds, r_torchqmforce, 2); SharedDataStream sds(ds); sds << torchqmforce.module_path << torchqmforce.cutoff << torchqmforce.neighbour_list_frequency << torchqmforce.is_mechanical << torchqmforce.lambda << torchqmforce.atoms << torchqmforce.mm1_to_qm << torchqmforce.mm1_to_mm2 << torchqmforce.bond_scale_factors - << torchqmforce.mm2_atoms << torchqmforce.numbers << torchqmforce.charges; + << torchqmforce.mm2_atoms << torchqmforce.numbers << torchqmforce.charges + << torchqmforce.switch_width << torchqmforce.use_switch; return ds; } @@ -77,20 +80,29 @@ QDataStream &operator>>(QDataStream &ds, TorchQMForce &torchqmforce) { VersionID v = readHeader(ds, r_torchqmforce); - if (v == 1) + if (v == 2) + { + SharedDataStream sds(ds); + + sds >> torchqmforce.module_path >> torchqmforce.cutoff >> torchqmforce.neighbour_list_frequency >> torchqmforce.is_mechanical >> torchqmforce.lambda >> torchqmforce.atoms >> torchqmforce.mm1_to_qm >> torchqmforce.mm1_to_mm2 >> torchqmforce.bond_scale_factors >> torchqmforce.mm2_atoms >> torchqmforce.numbers >> torchqmforce.charges >> torchqmforce.switch_width >> torchqmforce.use_switch; + + // Re-load the Torch module. + torchqmforce.setModulePath(torchqmforce.getModulePath()); + } + else if (v == 1) { SharedDataStream sds(ds); - sds >> torchqmforce.module_path >> torchqmforce.cutoff >> torchqmforce.neighbour_list_frequency - >> torchqmforce.is_mechanical >> torchqmforce.lambda >> torchqmforce.atoms - >> torchqmforce.mm1_to_qm >> torchqmforce.mm1_to_mm2 >> torchqmforce.bond_scale_factors - >> torchqmforce.mm2_atoms >> torchqmforce.numbers >> torchqmforce.charges; + sds >> torchqmforce.module_path >> torchqmforce.cutoff >> torchqmforce.neighbour_list_frequency >> torchqmforce.is_mechanical >> torchqmforce.lambda >> torchqmforce.atoms >> torchqmforce.mm1_to_qm >> torchqmforce.mm1_to_mm2 >> torchqmforce.bond_scale_factors >> torchqmforce.mm2_atoms >> torchqmforce.numbers >> torchqmforce.charges; + + torchqmforce.switch_width = 0.2; + torchqmforce.use_switch = true; // Re-load the Torch module. torchqmforce.setModulePath(torchqmforce.getModulePath()); } else - throw version_error(v, "1", r_torchqmforce, CODELOC); + throw version_error(v, "2", r_torchqmforce, CODELOC); return ds; } @@ -111,37 +123,41 @@ TorchQMForce::TorchQMForce( QMap bond_scale_factors, QVector mm2_atoms, QVector numbers, - QVector charges) : - cutoff(cutoff), - neighbour_list_frequency(neighbour_list_frequency), - is_mechanical(is_mechanical), - lambda(lambda), - atoms(atoms), - mm1_to_qm(mm1_to_qm), - mm1_to_mm2(mm1_to_mm2), - bond_scale_factors(bond_scale_factors), - mm2_atoms(mm2_atoms), - numbers(numbers), - charges(charges) + QVector charges, + double switch_width, + bool use_switch) : cutoff(cutoff), + neighbour_list_frequency(neighbour_list_frequency), + is_mechanical(is_mechanical), + lambda(lambda), + atoms(atoms), + mm1_to_qm(mm1_to_qm), + mm1_to_mm2(mm1_to_mm2), + bond_scale_factors(bond_scale_factors), + mm2_atoms(mm2_atoms), + numbers(numbers), + charges(charges), + switch_width(switch_width), + use_switch(use_switch) { // Try to load the Torch module. this->setModulePath(module_path); } -TorchQMForce::TorchQMForce(const TorchQMForce &other) : - module_path(other.module_path), - torch_module(other.torch_module), - cutoff(other.cutoff), - neighbour_list_frequency(other.neighbour_list_frequency), - is_mechanical(other.is_mechanical), - lambda(other.lambda), - atoms(other.atoms), - mm1_to_qm(other.mm1_to_qm), - mm1_to_mm2(other.mm1_to_mm2), - mm2_atoms(other.mm2_atoms), - bond_scale_factors(other.bond_scale_factors), - numbers(other.numbers), - charges(other.charges) +TorchQMForce::TorchQMForce(const TorchQMForce &other) : module_path(other.module_path), + torch_module(other.torch_module), + cutoff(other.cutoff), + neighbour_list_frequency(other.neighbour_list_frequency), + is_mechanical(other.is_mechanical), + lambda(other.lambda), + atoms(other.atoms), + mm1_to_qm(other.mm1_to_qm), + mm1_to_mm2(other.mm1_to_mm2), + mm2_atoms(other.mm2_atoms), + bond_scale_factors(other.bond_scale_factors), + numbers(other.numbers), + charges(other.charges), + switch_width(other.switch_width), + use_switch(other.use_switch) { } @@ -160,6 +176,8 @@ TorchQMForce &TorchQMForce::operator=(const TorchQMForce &other) this->bond_scale_factors = other.bond_scale_factors; this->numbers = other.numbers; this->charges = other.charges; + this->switch_width = other.switch_width; + this->use_switch = other.use_switch; return *this; } @@ -173,13 +191,14 @@ void TorchQMForce::setModulePath(QString module_path) this->torch_module = torch::jit::load(module_path.toStdString()); this->torch_module.eval(); } - catch (const c10::Error& e) + catch (const c10::Error &e) { throw SireError::io_error( - QObject::tr( - "Unable to load the TorchScript module '%1'. The error was '%2'.") - .arg(module_path).arg(e.what()), - CODELOC); + QObject::tr( + "Unable to load the TorchScript module '%1'. The error was '%2'.") + .arg(module_path) + .arg(e.what()), + CODELOC); } #endif @@ -194,7 +213,7 @@ QString TorchQMForce::getModulePath() const #ifdef SIRE_USE_TORCH torch::jit::script::Module TorchQMForce::getTorchModule() const #else -void* TorchQMForce::getTorchModule() const +void *TorchQMForce::getTorchModule() const #endif { return this->torch_module; @@ -231,7 +250,8 @@ int TorchQMForce::getNeighbourListFrequency() const bool TorchQMForce::getIsMechanical() const { - return this->is_mechanical;; + return this->is_mechanical; + ; } QVector TorchQMForce::getAtoms() const @@ -259,6 +279,16 @@ QVector TorchQMForce::getCharges() const return this->charges; } +double TorchQMForce::getSwitchWidth() const +{ + return this->switch_width; +} + +bool TorchQMForce::getUseSwitch() const +{ + return this->use_switch; +} + const char *TorchQMForce::typeName() { return QMetaType::typeName(qMetaTypeId()); @@ -275,69 +305,70 @@ const char *TorchQMForce::what() const namespace OpenMM { - class TorchQMForceProxy : public SerializationProxy { - public: - TorchQMForceProxy() : SerializationProxy("TorchQMForce") - { - }; + class TorchQMForceProxy : public SerializationProxy + { + public: + TorchQMForceProxy() : SerializationProxy("TorchQMForce") { + }; - void serialize(const void* object, SerializationNode& node) const - { - // Serialize the object. - QByteArray data; - QDataStream ds(&data, QIODevice::WriteOnly); - TorchQMForce torchqmforce = *static_cast(object); - ds << torchqmforce; + void serialize(const void *object, SerializationNode &node) const + { + // Serialize the object. + QByteArray data; + QDataStream ds(&data, QIODevice::WriteOnly); + TorchQMForce torchqmforce = *static_cast(object); + ds << torchqmforce; - // Set the version. - node.setIntProperty("version", 0); + // Set the version. + node.setIntProperty("version", 0); - // Set the note attribute. - node.setStringProperty("note", - "This force only supports partial serialization, so can only be used " - "within the same session and memory space."); + // Set the note attribute. + node.setStringProperty("note", + "This force only supports partial serialization, so can only be used " + "within the same session and memory space."); - // Set the data by converting the QByteArray to a hexidecimal string. - node.setStringProperty("data", data.toHex().data()); - }; + // Set the data by converting the QByteArray to a hexidecimal string. + node.setStringProperty("data", data.toHex().data()); + }; - void* deserialize(const SerializationNode& node) const + void *deserialize(const SerializationNode &node) const + { + // Check the version. + int version = node.getIntProperty("version"); + if (version != 0) { - // Check the version. - int version = node.getIntProperty("version"); - if (version != 0) - { - throw OpenMM::OpenMMException("Unsupported version number"); - } + throw OpenMM::OpenMMException("Unsupported version number"); + } - // Get the data as a std::string. - auto string = node.getStringProperty("data"); + // Get the data as a std::string. + auto string = node.getStringProperty("data"); - // Convert to hexidecimal. - auto hex = QByteArray::fromRawData(string.data(), string.size()); + // Convert to hexidecimal. + auto hex = QByteArray::fromRawData(string.data(), string.size()); - // Convert to a QByteArray. - auto data = QByteArray::fromHex(hex); + // Convert to a QByteArray. + auto data = QByteArray::fromHex(hex); - // Deserialize the object. - QDataStream ds(data); - TorchQMForce torchqmforce; + // Deserialize the object. + QDataStream ds(data); + TorchQMForce torchqmforce; - try - { - ds >> torchqmforce; - } - catch (...) - { - throw OpenMM::OpenMMException("Unable deserialize TorchQMForce"); - } + try + { + ds >> torchqmforce; + } + catch (...) + { + throw OpenMM::OpenMMException("Unable deserialize TorchQMForce"); + } - return new TorchQMForce(torchqmforce); - }; + return new TorchQMForce(torchqmforce); + }; }; // Register the TorchQMForce serialization proxy. - extern "C" void registerTorchQMSerializationProxies() { + extern "C" void registerTorchQMSerializationProxies() + { SerializationProxy::registerProxy(typeid(TorchQMForce), new TorchQMForceProxy()); } }; @@ -360,9 +391,8 @@ OpenMM::ForceImpl *TorchQMForce::createImpl() const } #if defined(SIRE_USE_CUSTOMCPPFORCE) and defined(SIRE_USE_TORCH) -TorchQMForceImpl::TorchQMForceImpl(const TorchQMForce &owner) : - OpenMM::CustomCPPForceImpl(owner), - owner(owner) +TorchQMForceImpl::TorchQMForceImpl(const TorchQMForce &owner) : OpenMM::CustomCPPForceImpl(owner), + owner(owner) { this->torch_module = owner.getTorchModule(); } @@ -409,13 +439,17 @@ double TorchQMForceImpl::computeForce( this->cutoff = this->owner.getCutoff().value(); // The neighbour list cutoff is 20% larger than the cutoff. - this->neighbour_list_cutoff = 1.2*this->cutoff; + this->neighbour_list_cutoff = 1.2 * this->cutoff; // Store the neighbour list update frequency. this->neighbour_list_frequency = this->owner.getNeighbourListFrequency(); // Flag whether a neighbour list is used. this->is_neighbour_list = this->neighbour_list_frequency > 0; + + // Cache switching function parameters. + this->use_switch = this->owner.getUseSwitch(); + this->r_switch = (1.0 - this->owner.getSwitchWidth()) * this->cutoff; } // Get the current box vectors in nanometers. @@ -424,17 +458,15 @@ double TorchQMForceImpl::computeForce( // Create a triclinic space, converting to Angstrom. TriclinicBox space( - Vector(10*box_x[0], 10*box_x[1], 10*box_x[2]), - Vector(10*box_y[0], 10*box_y[1], 10*box_y[2]), - Vector(10*box_z[0], 10*box_z[1], 10*box_z[2]) - ); + Vector(10 * box_x[0], 10 * box_x[1], 10 * box_x[2]), + Vector(10 * box_y[0], 10 * box_y[1], 10 * box_y[2]), + Vector(10 * box_z[0], 10 * box_z[1], 10 * box_z[2])); // Store the cell vectors in Angstrom. QVector> cell = { - {10*box_x[0], 10*box_x[1], 10*box_x[2]}, - {10*box_y[0], 10*box_y[1], 10*box_y[2]}, - {10*box_z[0], 10*box_z[1], 10*box_z[2]} - }; + {10 * box_x[0], 10 * box_x[1], 10 * box_x[2]}, + {10 * box_y[0], 10 * box_y[1], 10 * box_y[2]}, + {10 * box_z[0], 10 * box_z[1], 10 * box_z[2]}}; // Store the QM atomic indices and numbers. auto qm_atoms = this->owner.getAtoms(); @@ -450,19 +482,19 @@ double TorchQMForceImpl::computeForce( // Initialise a vector to hold the current positions for the QM atoms. QVector xyz_qm_vec(qm_atoms.size()); - std::vector xyz_qm(3*qm_atoms.size()); + std::vector xyz_qm(3 * qm_atoms.size()); // First loop over all QM atoms and store the positions. int i = 0; for (const auto &idx : qm_atoms) { const auto &pos = positions[idx]; - Vector qm_vec(10*pos[0], 10*pos[1], 10*pos[2]); + Vector qm_vec(10 * pos[0], 10 * pos[1], 10 * pos[2]); xyz_qm_vec[i] = qm_vec; i++; } - // Next sure that the QM atoms are whole (unwrapped). + // Make sure that the QM atoms are whole (unwrapped). xyz_qm_vec = space.makeWhole(xyz_qm_vec); // Get the center of the QM atoms. We will use this as a reference when @@ -471,9 +503,9 @@ double TorchQMForceImpl::computeForce( i = 0; for (const auto &qm_vec : xyz_qm_vec) { - xyz_qm[3*i] = qm_vec[0]; - xyz_qm[3*i+1] = qm_vec[1]; - xyz_qm[3*i+2] = qm_vec[2]; + xyz_qm[3 * i] = qm_vec[0]; + xyz_qm[3 * i + 1] = qm_vec[1]; + xyz_qm[3 * i + 2] = qm_vec[2]; center += qm_vec; i++; } @@ -489,6 +521,24 @@ double TorchQMForceImpl::computeForce( // Store the current number of MM atoms. unsigned int num_mm = 0; + // ds/dr of the quintic switching function (zero outside the switching region). + // Defined at outer scope so the chain-rule correction loop can use it after + // the electrostatic-embedding block closes. + auto switch_deriv = [&](double r) -> double + { + if (not this->use_switch or r <= this->r_switch or r >= this->cutoff) + return 0.0; + const double x = (r - this->r_switch) / (this->cutoff - this->r_switch); + return -30.0 * x * x * (x - 1.0) * (x - 1.0) / (this->cutoff - this->r_switch); + }; + + // Unscaled charges and chain-rule data for accepted MM atoms. + // Declared at outer scope for the same reason as switch_deriv above. + QVector charges_unscaled; + QVector min_dists; + QVector nearest_qm_vecs; + QVector nearest_qm_atom_idxs; + // If we are using electrostatic embedding, the work out the MM point charges and // build the neighbour list. if (not this->owner.getIsMechanical()) @@ -498,7 +548,19 @@ double TorchQMForceImpl::computeForce( std::vector xyz_virtual; QVector charges_virtual; - // Manually work out the MM point charges and build the neigbour list. + // Quintic switching function: scales charges smoothly to zero at the cutoff. + // Continuous through second derivative; r_switch and use_switch cached at step 0. + auto switching_function = [&](double r) -> double + { + if (not this->use_switch or r <= this->r_switch) + return 1.0; + if (r >= this->cutoff) + return 0.0; + const double x = (r - this->r_switch) / (this->cutoff - this->r_switch); + return 1.0 - x * x * x * (6.0 * x * x - 15.0 * x + 10.0); + }; + + // Manually work out the MM point charges and build the neighbour list. if (not this->is_neighbour_list or this->step_count % this->neighbour_list_frequency == 0) { // Clear the neighbour list. @@ -517,38 +579,55 @@ double TorchQMForceImpl::computeForce( not mm2_atoms.contains(i)) { // Store the MM atom position in Sire Vector format. - Vector mm_vec(10*pos[0], 10*pos[1], 10*pos[2]); + Vector mm_vec(10 * pos[0], 10 * pos[1], 10 * pos[2]); + + // Find the minimum distance to any QM atom. + double min_dist = std::numeric_limits::max(); + Vector nearest_qm_vec; + int nearest_qm_atom_idx = -1; // Loop over all of the QM atoms. - for (const auto &qm_vec : xyz_qm_vec) + for (int qm_j = 0; qm_j < xyz_qm_vec.size(); ++qm_j) { - // Work out the distance between the current MM atom and QM atoms. + const auto &qm_vec = xyz_qm_vec[qm_j]; + + // Work out the distance between the current MM atom and QM atom. const auto dist = space.calcDist(mm_vec, qm_vec); + if (dist < min_dist) + { + min_dist = dist; + nearest_qm_vec = qm_vec; + nearest_qm_atom_idx = qm_atoms[qm_j]; + } + // The current MM atom is within the neighbour list cutoff. if (this->is_neighbour_list and dist < this->neighbour_list_cutoff) { // Insert the MM atom index into the neighbour list. this->neighbour_list.insert(i); } + } - // The current MM atom is within the cutoff, add it. - if (dist < cutoff) - { - // Work out the minimum image position with respect to the - // reference position and add to the vector. - mm_vec = space.getMinimumImage(mm_vec, center); - xyz_mm.push_back(mm_vec[0]); - xyz_mm.push_back(mm_vec[1]); - xyz_mm.push_back(mm_vec[2]); - - // Add the charge and index. - charges_mm.append(this->owner.getCharges()[i]); - idx_mm.append(i); - - // Exit the inner loop. - break; - } + // The current MM atom is within the cutoff: add it. + if (min_dist < cutoff) + { + // Work out the minimum image position with respect to the + // reference position and add to the vector. + mm_vec = space.getMinimumImage(mm_vec, center); + xyz_mm.push_back(mm_vec[0]); + xyz_mm.push_back(mm_vec[1]); + xyz_mm.push_back(mm_vec[2]); + + const double q = this->owner.getCharges()[i]; + charges_unscaled.append(q); + min_dists.append(min_dist); + nearest_qm_vecs.append(nearest_qm_vec); + nearest_qm_atom_idxs.append(nearest_qm_atom_idx); + + // Scale charge by switching function. + charges_mm.append(q * switching_function(min_dist)); + idx_mm.append(i); } } @@ -563,43 +642,57 @@ double TorchQMForceImpl::computeForce( for (const auto &idx : this->neighbour_list) { // Store the MM atom position in Sire Vector format. - Vector mm_vec(10*positions[idx][0], 10*positions[idx][1], 10*positions[idx][2]); + Vector mm_vec(10 * positions[idx][0], 10 * positions[idx][1], 10 * positions[idx][2]); + + // Find the minimum distance to any QM atom. + double min_dist = std::numeric_limits::max(); + Vector nearest_qm_vec; + int nearest_qm_atom_idx = -1; - // Loop over all of the QM atoms. - for (const auto &qm_vec : xyz_qm_vec) + for (int qm_j = 0; qm_j < xyz_qm_vec.size(); ++qm_j) { - // The current MM atom is within the cutoff, add it. - if (space.calcDist(mm_vec, qm_vec) < cutoff) + const auto &qm_vec = xyz_qm_vec[qm_j]; + const auto dist = space.calcDist(mm_vec, qm_vec); + if (dist < min_dist) { - // Work out the minimum image position with respect to the - // reference position and add to the vector. - mm_vec = space.getMinimumImage(mm_vec, center); - xyz_mm.push_back(mm_vec[0]); - xyz_mm.push_back(mm_vec[1]); - xyz_mm.push_back(mm_vec[2]); - - // Add the charge and index. - charges_mm.append(this->owner.getCharges()[idx]); - idx_mm.append(idx); - - // Exit the inner loop. - break; + min_dist = dist; + nearest_qm_vec = qm_vec; + nearest_qm_atom_idx = qm_atoms[qm_j]; } } + + // The current MM atom is within the cutoff: add it. + if (min_dist < cutoff) + { + mm_vec = space.getMinimumImage(mm_vec, center); + xyz_mm.push_back(mm_vec[0]); + xyz_mm.push_back(mm_vec[1]); + xyz_mm.push_back(mm_vec[2]); + + const double q = this->owner.getCharges()[idx]; + charges_unscaled.append(q); + min_dists.append(min_dist); + nearest_qm_vecs.append(nearest_qm_vec); + nearest_qm_atom_idxs.append(nearest_qm_atom_idx); + + // Scale charge by switching function. + charges_mm.append(q * switching_function(min_dist)); + idx_mm.append(idx); + } } } // Handle link atoms via the Charge Shift method. // See: https://www.ks.uiuc.edu/Research/qmmm - for (const auto &idx: mm1_to_mm2.keys()) + for (const auto &idx : mm1_to_mm2.keys()) { // Get the QM atom to which the current MM atom is bonded. const auto qm_idx = mm1_to_qm[idx]; // Store the MM1 position in Sire Vector format, along with the // position of the QM atom to which it is bonded. - Vector mm1_vec(10*positions[idx][0], 10*positions[idx][1], 10*positions[idx][2]); - Vector qm_vec(10*positions[qm_idx][0], 10*positions[qm_idx][1], 10*positions[qm_idx][2]); + Vector mm1_vec(10 * positions[idx][0], 10 * positions[idx][1], 10 * positions[idx][2]); + Vector qm_vec(10 * positions[qm_idx][0], 10 * positions[qm_idx][1], 10 * positions[qm_idx][2]); // Work out the minimum image positions with respect to the reference position. mm1_vec = space.getMinimumImage(mm1_vec, center); @@ -610,7 +703,7 @@ double TorchQMForceImpl::computeForce( // where R0(QM-L) is the equilibrium bond length for the QM and link (L) // elements, and R0(QM-MM1) is the equilibrium bond length for the QM // and MM1 elements. - const auto link_vec = qm_vec + bond_scale_factors[idx]*(mm1_vec - qm_vec); + const auto link_vec = qm_vec + bond_scale_factors[idx] * (mm1_vec - qm_vec); // Add to the QM positions. xyz_qm.push_back(link_vec[0]); @@ -634,10 +727,10 @@ double TorchQMForceImpl::computeForce( // charge is redistributed over the MM2 atoms and two virtual point // charges are added either side of the MM2 atoms in order to preserve // the MM1-MM2 dipole. - for (const auto& mm2_idx : mm1_to_mm2[idx]) + for (const auto &mm2_idx : mm1_to_mm2[idx]) { // Store the MM2 position in Sire Vector format. - Vector mm2_vec(10*positions[mm2_idx][0], 10*positions[mm2_idx][1], 10*positions[mm2_idx][2]); + Vector mm2_vec(10 * positions[mm2_idx][0], 10 * positions[mm2_idx][1], 10 * positions[mm2_idx][2]); // Work out the minimum image position with respect to the reference position. mm2_vec = space.getMinimumImage(mm2_vec, center); @@ -657,14 +750,14 @@ double TorchQMForceImpl::computeForce( const auto normal = (mm2_vec - mm1_vec).normalise(); // Positive direction. (Away from MM1 atom.) - auto xyz = mm2_vec + VIRTUAL_PC_DELTA*normal; + auto xyz = mm2_vec + VIRTUAL_PC_DELTA * normal; xyz_virtual.push_back(xyz[0]); xyz_virtual.push_back(xyz[1]); xyz_virtual.push_back(xyz[2]); charges_virtual.append(-frac_charge); // Negative direction (Towards MM1 atom.) - xyz = mm2_vec - VIRTUAL_PC_DELTA*normal; + xyz = mm2_vec - VIRTUAL_PC_DELTA * normal; xyz_virtual.push_back(xyz[0]); xyz_virtual.push_back(xyz[1]); xyz_virtual.push_back(xyz[2]); @@ -694,38 +787,44 @@ double TorchQMForceImpl::computeForce( // Resize the charges and positions vectors to the maximum number of MM atoms. // This is to try to preserve a static compute graph to avoid re-jitting. charges_mm.resize(this->max_num_mm); - xyz_mm.resize(3*this->max_num_mm); + xyz_mm.resize(3 * this->max_num_mm); } } // Convert input to Torch tensors. - // MM charges. + // MM charges. requires_grad_(true) must be set before the forward pass so + // that the computation graph tracks the dependency on charges, enabling the + // chain-rule correction for the switching function. torch::Tensor charges_mm_torch = torch::from_blob(charges_mm.data(), {charges_mm.size()}, - torch::TensorOptions().dtype(torch::kFloat64)) - .to(torch::kFloat32).to(device); + torch::TensorOptions().dtype(torch::kFloat64)) + .to(torch::kFloat32) + .to(device); + charges_mm_torch.requires_grad_(true); // Atomic numbers. torch::Tensor atomic_numbers_torch = torch::from_blob(numbers.data(), {numbers.size()}, - torch::TensorOptions().dtype(torch::kInt32)) - .to(torch::kInt64).to(device); + torch::TensorOptions().dtype(torch::kInt32)) + .to(torch::kInt64) + .to(device); // QM positions. torch::Tensor xyz_qm_torch = torch::from_blob(xyz_qm.data(), {numbers.size(), 3}, - torch::TensorOptions().dtype(torch::kFloat32)) - .to(device); + torch::TensorOptions().dtype(torch::kFloat32)) + .to(device); xyz_qm_torch.requires_grad_(true); // MM positions. torch::Tensor xyz_mm_torch = torch::from_blob(xyz_mm.data(), {charges_mm.size(), 3}, - torch::TensorOptions().dtype(torch::kFloat32)) - .to(device); + torch::TensorOptions().dtype(torch::kFloat32)) + .to(device); xyz_mm_torch.requires_grad_(true); // Cell vectors. torch::Tensor cell_torch = torch::from_blob(cell.data(), {3, 3}, - torch::TensorOptions().dtype(torch::kFloat64)) - .to(torch::kFloat32).to(device); + torch::TensorOptions().dtype(torch::kFloat64)) + .to(torch::kFloat32) + .to(device); cell_torch.requires_grad_(false); // Create the input vector. @@ -734,8 +833,7 @@ double TorchQMForceImpl::computeForce( charges_mm_torch, xyz_qm_torch, xyz_mm_torch, - cell_torch - }; + cell_torch}; // Compute the energies. auto energies = this->torch_module.forward(input).toTensor(); @@ -743,19 +841,27 @@ double TorchQMForceImpl::computeForce( // Store the sum of the energy in kJ. const auto energy = energies.sum().item() * HARTREE_TO_KJ_MOL; - // If there are no MM atoms, then we need to allow unused tensors. - bool allow_unused = num_mm == 0; + // Always allow unused: xyz_mm/charges_mm may not be connected when there are + // no MM atoms, and the TorchScript model may not propagate gradients through + // charges even when they are in the graph. We guard each gradient before use. + const bool allow_unused = true; - // Compute the gradients. + // Compute the gradients w.r.t. QM positions, MM positions, and MM charges. const auto gradients = torch::autograd::grad( - {energies.sum()}, {xyz_qm_torch, xyz_mm_torch}, {}, c10::nullopt, false, allow_unused); + {energies.sum()}, {xyz_qm_torch, xyz_mm_torch, charges_mm_torch}, + {}, c10::nullopt, false, allow_unused); // Compute the forces, converting from Hatree/Anstrom to kJ/mol/nm. const auto forces_qm = -(gradients[0] * HARTREE_TO_KJ_MOL * 10).detach().cpu(); torch::Tensor forces_mm; + torch::Tensor dE_dq; if (num_mm > 0) { forces_mm = -(gradients[1] * HARTREE_TO_KJ_MOL * 10).detach().cpu(); + if (gradients[2].defined()) + { + dE_dq = gradients[2].detach().cpu(); + } } else { @@ -805,35 +911,58 @@ double TorchQMForceImpl::computeForce( forces_mm.data_ptr() + forces_mm.numel()); // First the QM atoms. - for (int i=0; i() * HARTREE_TO_KJ_MOL * 10.0 * charges_unscaled[i] * dsdr; + const OpenMM::Vec3 f_corr(correction * r_hat[0], + correction * r_hat[1], + correction * r_hat[2]); + // Apply to MM atom. + forces[idx] += lambda * f_corr; + // Apply equal and opposite force to the nearest QM atom (Newton's 3rd law). + forces[nearest_qm_atom_idxs[i]] -= lambda * f_corr; + } + } } // Update the step count. @@ -859,19 +988,22 @@ TorchQMEngine::TorchQMEngine( SireUnits::Dimension::Length cutoff, int neighbour_list_frequency, bool is_mechanical, - double lambda) : - ConcreteProperty(), - module_path(module_path), - cutoff(cutoff), - neighbour_list_frequency(neighbour_list_frequency), - is_mechanical(is_mechanical), - lambda(lambda) + double lambda, + double switch_width, + bool use_switch) : ConcreteProperty(), + module_path(module_path), + cutoff(cutoff), + neighbour_list_frequency(neighbour_list_frequency), + is_mechanical(is_mechanical), + lambda(lambda), + switch_width(switch_width), + use_switch(use_switch) { #ifndef SIRE_USE_TORCH throw SireError::unsupported(QObject::tr( - "Unable to create an TorchQMEngine because Sire has been compiled " - "without Torch support."), - CODELOC); + "Unable to create an TorchQMEngine because Sire has been compiled " + "without Torch support."), + CODELOC); #endif // Register the serialization proxies. @@ -891,19 +1023,20 @@ TorchQMEngine::TorchQMEngine( } } -TorchQMEngine::TorchQMEngine(const TorchQMEngine &other) : - module_path(other.module_path), - cutoff(other.cutoff), - neighbour_list_frequency(other.neighbour_list_frequency), - is_mechanical(other.is_mechanical), - lambda(other.lambda), - atoms(other.atoms), - mm1_to_qm(other.mm1_to_qm), - mm1_to_mm2(other.mm1_to_mm2), - mm2_atoms(other.mm2_atoms), - bond_scale_factors(other.bond_scale_factors), - numbers(other.numbers), - charges(other.charges) +TorchQMEngine::TorchQMEngine(const TorchQMEngine &other) : module_path(other.module_path), + cutoff(other.cutoff), + neighbour_list_frequency(other.neighbour_list_frequency), + is_mechanical(other.is_mechanical), + lambda(other.lambda), + switch_width(other.switch_width), + use_switch(other.use_switch), + atoms(other.atoms), + mm1_to_qm(other.mm1_to_qm), + mm1_to_mm2(other.mm1_to_mm2), + mm2_atoms(other.mm2_atoms), + bond_scale_factors(other.bond_scale_factors), + numbers(other.numbers), + charges(other.charges) { } @@ -914,6 +1047,8 @@ TorchQMEngine &TorchQMEngine::operator=(const TorchQMEngine &other) this->neighbour_list_frequency = other.neighbour_list_frequency; this->is_mechanical = other.is_mechanical; this->lambda = other.lambda; + this->switch_width = other.switch_width; + this->use_switch = other.use_switch; this->atoms = other.atoms; this->mm1_to_qm = other.mm1_to_qm; this->mm1_to_mm2 = other.mm1_to_mm2; @@ -1045,6 +1180,30 @@ void TorchQMEngine::setCharges(QVector charges) this->charges = charges; } +double TorchQMEngine::getSwitchWidth() const +{ + return this->switch_width; +} + +void TorchQMEngine::setSwitchWidth(double switch_width) +{ + if (switch_width < 0.0) + switch_width = 0.0; + else if (switch_width > 1.0) + switch_width = 1.0; + this->switch_width = switch_width; +} + +bool TorchQMEngine::getUseSwitch() const +{ + return this->use_switch; +} + +void TorchQMEngine::setUseSwitch(bool use_switch) +{ + this->use_switch = use_switch; +} + const char *TorchQMEngine::typeName() { return QMetaType::typeName(qMetaTypeId()); @@ -1055,7 +1214,7 @@ const char *TorchQMEngine::what() const return TorchQMEngine::typeName(); } -QMForce* TorchQMEngine::createForce() const +QMForce *TorchQMEngine::createForce() const { return new TorchQMForce( this->module_path, @@ -1069,6 +1228,7 @@ QMForce* TorchQMEngine::createForce() const this->bond_scale_factors, this->mm2_atoms, this->numbers, - this->charges - ); + this->charges, + this->switch_width, + this->use_switch); } diff --git a/wrapper/Convert/SireOpenMM/torchqm.h b/wrapper/Convert/SireOpenMM/torchqm.h index 97969d75c..a6b8e4ca2 100644 --- a/wrapper/Convert/SireOpenMM/torchqm.h +++ b/wrapper/Convert/SireOpenMM/torchqm.h @@ -136,8 +136,9 @@ namespace SireOpenMM QMap bond_scale_factors, QVector mm2_atoms, QVector numbers, - QVector charges - ); + QVector charges, + double switch_width = 0.2, + bool use_switch = true); //! Copy constructor. TorchQMForce(const TorchQMForce &other); @@ -164,7 +165,7 @@ namespace SireOpenMM #ifdef SIRE_USE_TORCH torch::jit::script::Module getTorchModule() const; #else - void* getTorchModule() const; + void *getTorchModule() const; #endif //! Get the lambda weighting factor. @@ -243,6 +244,18 @@ namespace SireOpenMM */ QVector getCharges() const; + //! Get the switch width. + /*! \returns + The switch width as a fraction of the cutoff. + */ + double getSwitchWidth() const; + + //! Get whether a switching function is used. + /*! \returns + Whether a switching function is used. + */ + bool getUseSwitch() const; + //! Return the C++ name for this class. static const char *typeName(); @@ -270,6 +283,8 @@ namespace SireOpenMM QVector mm2_atoms; QVector numbers; QVector charges; + double switch_width; + bool use_switch; }; #if defined(SIRE_USE_CUSTOMCPPFORCE) && defined(SIRE_USE_TORCH) @@ -289,13 +304,15 @@ namespace SireOpenMM private: const TorchQMForce &owner; torch::jit::script::Module torch_module; - unsigned long long step_count=0; + unsigned long long step_count = 0; double cutoff; + double r_switch; + bool use_switch; bool is_neighbour_list; int neighbour_list_frequency; double neighbour_list_cutoff; QSet neighbour_list; - int max_num_mm=0; + int max_num_mm = 0; c10::DeviceType device; }; #endif @@ -326,11 +343,12 @@ namespace SireOpenMM */ TorchQMEngine( QString module_path, - SireUnits::Dimension::Length cutoff=7.5*SireUnits::angstrom, - int neighbour_list_frequency=0, - bool is_mechanical=false, - double lambda=1.0 - ); + SireUnits::Dimension::Length cutoff = 7.5 * SireUnits::angstrom, + int neighbour_list_frequency = 0, + bool is_mechanical = false, + double lambda = 1.0, + double switch_width = 0.2, + bool use_switch = true); //! Copy constructor. TorchQMEngine(const TorchQMEngine &other); @@ -481,6 +499,30 @@ namespace SireOpenMM */ void setCharges(QVector charges); + //! Get the switch width. + /*! \returns + The switch width as a fraction of the cutoff. + */ + double getSwitchWidth() const; + + //! Set the switch width. + /*! \param switch_width + The switch width as a fraction of the cutoff (0 to 1). + */ + void setSwitchWidth(double switch_width); + + //! Get whether a switching function is used. + /*! \returns + Whether a switching function is used. + */ + bool getUseSwitch() const; + + //! Set whether a switching function is used. + /*! \param use_switch + Whether to use a switching function. + */ + void setUseSwitch(bool use_switch); + //! Return the C++ name for this class. static const char *typeName(); @@ -488,7 +530,7 @@ namespace SireOpenMM const char *what() const; //! Create an EMLE force object. - QMForce* createForce() const; + QMForce *createForce() const; private: QString module_path; @@ -496,6 +538,8 @@ namespace SireOpenMM int neighbour_list_frequency; bool is_mechanical; double lambda; + double switch_width; + bool use_switch; QVector atoms; QMap mm1_to_qm; QMap> mm1_to_mm2; diff --git a/wrapper/Convert/__init__.py b/wrapper/Convert/__init__.py index a68325e20..ae90aa618 100644 --- a/wrapper/Convert/__init__.py +++ b/wrapper/Convert/__init__.py @@ -93,6 +93,7 @@ def smarts_to_rdkit(*args, **kwargs): _changed_torsions, _changed_exceptions, _changed_constraints, + _changed_cmaps, _get_lever_values, ) @@ -127,6 +128,7 @@ def smarts_to_rdkit(*args, **kwargs): PerturbableOpenMMMolecule.changed_torsions = _changed_torsions PerturbableOpenMMMolecule.changed_exceptions = _changed_exceptions PerturbableOpenMMMolecule.changed_constraints = _changed_constraints + PerturbableOpenMMMolecule.changed_cmaps = _changed_cmaps PerturbableOpenMMMolecule.get_lever_values = _get_lever_values _has_openmm = True diff --git a/wrapper/IO/SireIO_containers.cpp b/wrapper/IO/SireIO_containers.cpp index 138c5c47a..0d572f667 100644 --- a/wrapper/IO/SireIO_containers.cpp +++ b/wrapper/IO/SireIO_containers.cpp @@ -37,14 +37,17 @@ #include "Helpers/convertlist.hpp" #include "Helpers/tuples.hpp" -#include "SireIO/moleculeparser.h" #include "SireIO/grotop.h" +#include "SireIO/moleculeparser.h" + +#include "SireMM/cljnbpairs.h" #include "SireMol/molidx.h" #include "SireSystem/system.h" using namespace SireIO; +using namespace SireMM; using namespace SireMol; using namespace SireSystem; @@ -52,13 +55,14 @@ using boost::python::register_tuple; void register_SireIO_containers() { - register_list< QVector> >(); - register_list< QList >(); + register_list>>(); + register_list>(); - register_list< QVector >(); - register_list< QVector >(); + register_list>(); + register_list>(); - register_dict< QHash >(); + register_dict>(); - register_tuple< boost::tuple> >(); + register_tuple>>(); + register_tuple>(); } diff --git a/wrapper/IO/_IO_free_functions.pypp.cpp b/wrapper/IO/_IO_free_functions.pypp.cpp index cdabc6b13..3bd14befc 100644 --- a/wrapper/IO/_IO_free_functions.pypp.cpp +++ b/wrapper/IO/_IO_free_functions.pypp.cpp @@ -2,8 +2,8 @@ // (C) Christopher Woods, GPL >= 3 License -#include "boost/python.hpp" #include "_IO_free_functions.pypp.hpp" +#include "boost/python.hpp" namespace bp = boost::python; @@ -628,295 +628,212 @@ namespace bp = boost::python; #include "biosimspace.h" -void register_free_functions(){ +void register_free_functions() +{ { //::SireIO::createChlorineIon - - typedef ::SireMol::Molecule ( *createChlorineIon_function_type )( ::SireMaths::Vector const &,::QString const,::SireBase::PropertyMap const & ); - createChlorineIon_function_type createChlorineIon_function_value( &::SireIO::createChlorineIon ); - - bp::def( - "createChlorineIon" - , createChlorineIon_function_value - , ( bp::arg("coords"), bp::arg("model"), bp::arg("map")=SireBase::PropertyMap() ) - , "Create a chlorine ion at the specified position.\nPar:am position\nThe position of the chlorine ion.\n\nPar:am model\nThe name of the water model.\n\nPar:am map\nA dictionary of user-defined molecular property names.\n\nRetval: chlorine\nThe chlorine ion.\n" ); - + + typedef ::SireMol::Molecule (*createChlorineIon_function_type)(::SireMaths::Vector const &, ::QString const, ::SireBase::PropertyMap const &); + createChlorineIon_function_type createChlorineIon_function_value(&::SireIO::createChlorineIon); + + bp::def( + "createChlorineIon", createChlorineIon_function_value, (bp::arg("coords"), bp::arg("model"), bp::arg("map") = SireBase::PropertyMap()), "Create a chlorine ion at the specified position.\nPar:am position\nThe position of the chlorine ion.\n\nPar:am model\nThe name of the water model.\n\nPar:am map\nA dictionary of user-defined molecular property names.\n\nRetval: chlorine\nThe chlorine ion.\n"); } { //::SireIO::createSodiumIon - - typedef ::SireMol::Molecule ( *createSodiumIon_function_type )( ::SireMaths::Vector const &,::QString const,::SireBase::PropertyMap const & ); - createSodiumIon_function_type createSodiumIon_function_value( &::SireIO::createSodiumIon ); - - bp::def( - "createSodiumIon" - , createSodiumIon_function_value - , ( bp::arg("coords"), bp::arg("model"), bp::arg("map")=SireBase::PropertyMap() ) - , "Create a sodium ion at the specified position.\nPar:am position\nThe position of the sodium ion.\n\nPar:am model\nThe name of the water model.\n\nPar:am map\nA dictionary of user-defined molecular property names.\n\nRetval: sodium\nThe sodium ion.\n" ); - + + typedef ::SireMol::Molecule (*createSodiumIon_function_type)(::SireMaths::Vector const &, ::QString const, ::SireBase::PropertyMap const &); + createSodiumIon_function_type createSodiumIon_function_value(&::SireIO::createSodiumIon); + + bp::def( + "createSodiumIon", createSodiumIon_function_value, (bp::arg("coords"), bp::arg("model"), bp::arg("map") = SireBase::PropertyMap()), "Create a sodium ion at the specified position.\nPar:am position\nThe position of the sodium ion.\n\nPar:am model\nThe name of the water model.\n\nPar:am map\nA dictionary of user-defined molecular property names.\n\nRetval: sodium\nThe sodium ion.\n"); } { //::SireIO::getCoordsArray - - typedef ::QVector< float > ( *getCoordsArray_function_type )( ::SireMol::MoleculeView const &,::SireUnits::Dimension::Length const &,::SireBase::PropertyMap const & ); - getCoordsArray_function_type getCoordsArray_function_value( &::SireIO::getCoordsArray ); - - bp::def( - "getCoordsArray" - , getCoordsArray_function_value - , ( bp::arg("mol"), bp::arg("to_unit"), bp::arg("map") ) - , "" ); - + + typedef ::QVector (*getCoordsArray_function_type)(::SireMol::MoleculeView const &, ::SireUnits::Dimension::Length const &, ::SireBase::PropertyMap const &); + getCoordsArray_function_type getCoordsArray_function_value(&::SireIO::getCoordsArray); + + bp::def( + "getCoordsArray", getCoordsArray_function_value, (bp::arg("mol"), bp::arg("to_unit"), bp::arg("map")), ""); } { //::SireIO::getCoordsArray - - typedef ::QVector< float > ( *getCoordsArray_function_type )( ::SireMol::MoleculeGroup const &,::SireUnits::Dimension::Length const &,::SireBase::PropertyMap const & ); - getCoordsArray_function_type getCoordsArray_function_value( &::SireIO::getCoordsArray ); - - bp::def( - "getCoordsArray" - , getCoordsArray_function_value - , ( bp::arg("mols"), bp::arg("to_unit"), bp::arg("map") ) - , "" ); - + + typedef ::QVector (*getCoordsArray_function_type)(::SireMol::MoleculeGroup const &, ::SireUnits::Dimension::Length const &, ::SireBase::PropertyMap const &); + getCoordsArray_function_type getCoordsArray_function_value(&::SireIO::getCoordsArray); + + bp::def( + "getCoordsArray", getCoordsArray_function_value, (bp::arg("mols"), bp::arg("to_unit"), bp::arg("map")), ""); } { //::SireIO::getCoordsArray - - typedef ::QVector< float > ( *getCoordsArray_function_type )( ::SireSystem::System const &,::SireUnits::Dimension::Length const &,::SireBase::PropertyMap const & ); - getCoordsArray_function_type getCoordsArray_function_value( &::SireIO::getCoordsArray ); - - bp::def( - "getCoordsArray" - , getCoordsArray_function_value - , ( bp::arg("system"), bp::arg("to_unit"), bp::arg("map") ) - , "" ); - + + typedef ::QVector (*getCoordsArray_function_type)(::SireSystem::System const &, ::SireUnits::Dimension::Length const &, ::SireBase::PropertyMap const &); + getCoordsArray_function_type getCoordsArray_function_value(&::SireIO::getCoordsArray); + + bp::def( + "getCoordsArray", getCoordsArray_function_value, (bp::arg("system"), bp::arg("to_unit"), bp::arg("map")), ""); } { //::SireIO::isAmberWater - - typedef bool ( *isAmberWater_function_type )( ::SireMol::Molecule const &,::SireBase::PropertyMap const & ); - isAmberWater_function_type isAmberWater_function_value( &::SireIO::isAmberWater ); - - bp::def( - "isAmberWater" - , isAmberWater_function_value - , ( bp::arg("molecule"), bp::arg("map")=SireBase::PropertyMap() ) - , "Test whether the passed water molecule matches standard AMBER\nformat water topologies.\n\nPar:am molecule\nThe molecule to test.\n\nPar:am map\nA dictionary of user-defined molecular property names.\n\nRetval: is_water\nWhether the molecule is an AMBER format water.\n" ); - + + typedef bool (*isAmberWater_function_type)(::SireMol::Molecule const &, ::SireBase::PropertyMap const &); + isAmberWater_function_type isAmberWater_function_value(&::SireIO::isAmberWater); + + bp::def( + "isAmberWater", isAmberWater_function_value, (bp::arg("molecule"), bp::arg("map") = SireBase::PropertyMap()), "Test whether the passed water molecule matches standard AMBER\nformat water topologies.\n\nPar:am molecule\nThe molecule to test.\n\nPar:am map\nA dictionary of user-defined molecular property names.\n\nRetval: is_water\nWhether the molecule is an AMBER format water.\n"); } { //::SireIO::isGromacsWater - - typedef bool ( *isGromacsWater_function_type )( ::SireMol::Molecule const &,::SireBase::PropertyMap const & ); - isGromacsWater_function_type isGromacsWater_function_value( &::SireIO::isGromacsWater ); - - bp::def( - "isGromacsWater" - , isGromacsWater_function_value - , ( bp::arg("molecule"), bp::arg("map")=SireBase::PropertyMap() ) - , "Test whether the passed water molecule matches standard GROMACS\nformat water topologies.\n\nPar:am molecule\nThe molecule to test.\n\nPar:am map\nA dictionary of user-defined molecular property names.\n\nRetval: is_water\nWhether the molecule is a GROMACS format water.\n" ); - + + typedef bool (*isGromacsWater_function_type)(::SireMol::Molecule const &, ::SireBase::PropertyMap const &); + isGromacsWater_function_type isGromacsWater_function_value(&::SireIO::isGromacsWater); + + bp::def( + "isGromacsWater", isGromacsWater_function_value, (bp::arg("molecule"), bp::arg("map") = SireBase::PropertyMap()), "Test whether the passed water molecule matches standard GROMACS\nformat water topologies.\n\nPar:am molecule\nThe molecule to test.\n\nPar:am map\nA dictionary of user-defined molecular property names.\n\nRetval: is_water\nWhether the molecule is a GROMACS format water.\n"); } { //::SireIO::isWater - - typedef bool ( *isWater_function_type )( ::SireMol::Molecule const &,::SireBase::PropertyMap const & ); - isWater_function_type isWater_function_value( &::SireIO::isWater ); - - bp::def( - "isWater" - , isWater_function_value - , ( bp::arg("molecule"), bp::arg("map")=SireBase::PropertyMap() ) - , "Test whether the passed water molecule matches standard water\ntopologies.\n\nPar:am molecule\nThe molecule to test.\n\nPar:am map\nA dictionary of user-defined molecular property names.\n\nRetval: is_water\nWhether the molecule is a water.\n" ); - + + typedef bool (*isWater_function_type)(::SireMol::Molecule const &, ::SireBase::PropertyMap const &); + isWater_function_type isWater_function_value(&::SireIO::isWater); + + bp::def( + "isWater", isWater_function_value, (bp::arg("molecule"), bp::arg("map") = SireBase::PropertyMap()), "Test whether the passed water molecule matches standard water\ntopologies.\n\nPar:am molecule\nThe molecule to test.\n\nPar:am map\nA dictionary of user-defined molecular property names.\n\nRetval: is_water\nWhether the molecule is a water.\n"); } { //::SireIO::removeProperty - - typedef ::SireSystem::System ( *removeProperty_function_type )( ::SireSystem::System &,::QString const & ); - removeProperty_function_type removeProperty_function_value( &::SireIO::removeProperty ); - - bp::def( - "removeProperty" - , removeProperty_function_value - , ( bp::arg("system"), bp::arg("property") ) - , "Remove a named property from all molecules in a system.\n\nPar:am system\nThe molecular system of interest.\n\nPar:am property\nThe name of the property to be removed.\n\nRetval: system\nThe system with renumbered constituents.\n" ); - + + typedef ::SireSystem::System (*removeProperty_function_type)(::SireSystem::System &, ::QString const &); + removeProperty_function_type removeProperty_function_value(&::SireIO::removeProperty); + + bp::def( + "removeProperty", removeProperty_function_value, (bp::arg("system"), bp::arg("property")), "Remove a named property from all molecules in a system.\n\nPar:am system\nThe molecular system of interest.\n\nPar:am property\nThe name of the property to be removed.\n\nRetval: system\nThe system with renumbered constituents.\n"); } { //::SireIO::renumberConstituents - - typedef ::SireSystem::System ( *renumberConstituents_function_type )( ::SireSystem::System const &,unsigned int ); - renumberConstituents_function_type renumberConstituents_function_value( &::SireIO::renumberConstituents ); - - bp::def( - "renumberConstituents" - , renumberConstituents_function_value - , ( bp::arg("system"), bp::arg("mol_offset")=(unsigned int)(0) ) - , "Renumber the constituents of a system (residues and atoms) so that\nthey are unique and are in ascending order.\n\nPar:am system\nThe molecular system of interest.\n\nPar:am mol_offset\nThe index of the molecule at which to begin renumbering.\n\nRetval: system\nThe system with renumbered constituents.\n" ); - + + typedef ::SireSystem::System (*renumberConstituents_function_type)(::SireSystem::System const &, unsigned int); + renumberConstituents_function_type renumberConstituents_function_value(&::SireIO::renumberConstituents); + + bp::def( + "renumberConstituents", renumberConstituents_function_value, (bp::arg("system"), bp::arg("mol_offset") = (unsigned int)(0)), "Renumber the constituents of a system (residues and atoms) so that\nthey are unique and are in ascending order.\n\nPar:am system\nThe molecular system of interest.\n\nPar:am mol_offset\nThe index of the molecule at which to begin renumbering.\n\nRetval: system\nThe system with renumbered constituents.\n"); } { //::SireIO::repartitionHydrogenMass - - typedef ::SireSystem::System ( *repartitionHydrogenMass_function_type )( ::SireSystem::System const &,double const,unsigned int const,::SireBase::PropertyMap const & ); - repartitionHydrogenMass_function_type repartitionHydrogenMass_function_value( &::SireIO::repartitionHydrogenMass ); - - bp::def( - "repartitionHydrogenMass" - , repartitionHydrogenMass_function_value - , ( bp::arg("system"), bp::arg("factor")=4, bp::arg("water")=(unsigned int const)(0), bp::arg("map")=SireBase::PropertyMap() ) - , "Redistribute mass of heavy atoms connected to bonded hydrogens into\nthe hydrogen atoms. This allows use of larger simulation integration\ntime steps without encountering instabilities related to high-frequency\nhydrogen motion.\n\nPar:am system\nThe molecular system of interest.\n\nPar:am factor\nThe repartitioning scale factor. Hydrogen masses are scaled by\nthis amount.\n\nPar:am water\nWhether to repartiotion masses for water molecules:\n0 = yes, 1 = no, 2 = only water molecules.\n\nPar:am map\nA dictionary of user-defined molecular property names.\n\nRetval: system\nThe system with repartitioned hydrogen mass.\n" ); - + + typedef ::SireSystem::System (*repartitionHydrogenMass_function_type)(::SireSystem::System const &, double const, unsigned int const, ::SireBase::PropertyMap const &); + repartitionHydrogenMass_function_type repartitionHydrogenMass_function_value(&::SireIO::repartitionHydrogenMass); + + bp::def( + "repartitionHydrogenMass", repartitionHydrogenMass_function_value, (bp::arg("system"), bp::arg("factor") = 4, bp::arg("water") = (unsigned int const)(0), bp::arg("map") = SireBase::PropertyMap()), "Redistribute mass of heavy atoms connected to bonded hydrogens into\nthe hydrogen atoms. This allows use of larger simulation integration\ntime steps without encountering instabilities related to high-frequency\nhydrogen motion.\n\nPar:am system\nThe molecular system of interest.\n\nPar:am factor\nThe repartitioning scale factor. Hydrogen masses are scaled by\nthis amount.\n\nPar:am water\nWhether to repartiotion masses for water molecules:\n0 = yes, 1 = no, 2 = only water molecules.\n\nPar:am map\nA dictionary of user-defined molecular property names.\n\nRetval: system\nThe system with repartitioned hydrogen mass.\n"); } { //::SireIO::repartitionHydrogenMass - - typedef ::SireMol::Molecule ( *repartitionHydrogenMass_function_type )( ::SireMol::Molecule &,double const,unsigned int const,::SireBase::PropertyMap const & ); - repartitionHydrogenMass_function_type repartitionHydrogenMass_function_value( &::SireIO::repartitionHydrogenMass ); - - bp::def( - "repartitionHydrogenMass" - , repartitionHydrogenMass_function_value - , ( bp::arg("molecule"), bp::arg("factor")=4, bp::arg("water")=(unsigned int const)(0), bp::arg("map")=SireBase::PropertyMap() ) - , "Redistribute mass of heavy atoms connected to bonded hydrogens into\nthe hydrogen atoms. This allows use of larger simulation integration\ntime steps without encountering instabilities related to high-frequency\nhydrogen motion.\n\nPar:am molecule\nThe molecule of interest.\n\nPar:am factor\nThe repartitioning scale factor. Hydrogen masses are scaled by\nthis amount.\n\nPar:am water\nWhether to repartiotion masses for water molecules:\n0 = yes, 1 = no, 2 = only water molecules.\n\nPar:am map\nA dictionary of user-defined molecular property names.\n\nRetval: system\nThe system with repartitioned hydrogen mass.\n" ); - + + typedef ::SireMol::Molecule (*repartitionHydrogenMass_function_type)(::SireMol::Molecule &, double const, unsigned int const, ::SireBase::PropertyMap const &); + repartitionHydrogenMass_function_type repartitionHydrogenMass_function_value(&::SireIO::repartitionHydrogenMass); + + bp::def( + "repartitionHydrogenMass", repartitionHydrogenMass_function_value, (bp::arg("molecule"), bp::arg("factor") = 4, bp::arg("water") = (unsigned int const)(0), bp::arg("map") = SireBase::PropertyMap()), "Redistribute mass of heavy atoms connected to bonded hydrogens into\nthe hydrogen atoms. This allows use of larger simulation integration\ntime steps without encountering instabilities related to high-frequency\nhydrogen motion.\n\nPar:am molecule\nThe molecule of interest.\n\nPar:am factor\nThe repartitioning scale factor. Hydrogen masses are scaled by\nthis amount.\n\nPar:am water\nWhether to repartiotion masses for water molecules:\n0 = yes, 1 = no, 2 = only water molecules.\n\nPar:am map\nA dictionary of user-defined molecular property names.\n\nRetval: system\nThe system with repartitioned hydrogen mass.\n"); } { //::SireIO::setAmberWater - - typedef ::SireSystem::System ( *setAmberWater_function_type )( ::SireSystem::System const &,::QString const &,::SireBase::PropertyMap const & ); - setAmberWater_function_type setAmberWater_function_value( &::SireIO::setAmberWater ); - - bp::def( - "setAmberWater" - , setAmberWater_function_value - , ( bp::arg("system"), bp::arg("model"), bp::arg("map")=SireBase::PropertyMap() ) - , "Set all water molecules in the passed system to the appropriate AMBER\nformat topology.\n\nPar:am system\nThe molecular system of interest.\n\nPar:am model\nThe name of the water model.\n\nPar:am map\nA dictionary of user-defined molecular property names.\n\nRetval: system\nThe system with updated water topology.\n" ); - + + typedef ::SireSystem::System (*setAmberWater_function_type)(::SireSystem::System const &, ::QString const &, ::SireBase::PropertyMap const &); + setAmberWater_function_type setAmberWater_function_value(&::SireIO::setAmberWater); + + bp::def( + "setAmberWater", setAmberWater_function_value, (bp::arg("system"), bp::arg("model"), bp::arg("map") = SireBase::PropertyMap()), "Set all water molecules in the passed system to the appropriate AMBER\nformat topology.\n\nPar:am system\nThe molecular system of interest.\n\nPar:am model\nThe name of the water model.\n\nPar:am map\nA dictionary of user-defined molecular property names.\n\nRetval: system\nThe system with updated water topology.\n"); } { //::SireIO::setAmberWater - - typedef ::SireMol::SelectResult ( *setAmberWater_function_type )( ::SireMol::SelectResult const &,::QString const &,::SireBase::PropertyMap const & ); - setAmberWater_function_type setAmberWater_function_value( &::SireIO::setAmberWater ); - - bp::def( - "setAmberWater" - , setAmberWater_function_value - , ( bp::arg("molecules"), bp::arg("model"), bp::arg("map")=SireBase::PropertyMap() ) - , "Set all water molecules in the passed system to the appropriate AMBER\nformat topology.\n\nPar:am system\nThe molecular system of interest.\n\nPar:am model\nThe name of the water model.\n\nPar:am map\nA dictionary of user-defined molecular property names.\n\nRetval: system\nThe system with updated water topology.\n" ); - + + typedef ::SireMol::SelectResult (*setAmberWater_function_type)(::SireMol::SelectResult const &, ::QString const &, ::SireBase::PropertyMap const &); + setAmberWater_function_type setAmberWater_function_value(&::SireIO::setAmberWater); + + bp::def( + "setAmberWater", setAmberWater_function_value, (bp::arg("molecules"), bp::arg("model"), bp::arg("map") = SireBase::PropertyMap()), "Set all water molecules in the passed system to the appropriate AMBER\nformat topology.\n\nPar:am system\nThe molecular system of interest.\n\nPar:am model\nThe name of the water model.\n\nPar:am map\nA dictionary of user-defined molecular property names.\n\nRetval: system\nThe system with updated water topology.\n"); } { //::SireIO::setCoordinates - - typedef ::SireSystem::System ( *setCoordinates_function_type )( ::SireSystem::System,::QVector> const &,bool const,::SireBase::PropertyMap const & ); - setCoordinates_function_type setCoordinates_function_value( &::SireIO::setCoordinates ); - - bp::def( - "setCoordinates" - , setCoordinates_function_value - , ( bp::arg("system"), bp::arg("coordinates"), bp::arg("is_lambda1")=(bool const)(false), bp::arg("map")=SireBase::PropertyMap() ) - , "Set the coordinates of the entire system.\nPar:am system\nThe molecular system of interest.\n\nPar:am coordinates\nThe new coordinates for the system.\n\nPar:am map\nA dictionary of user-defined molecular property names.\n\nRetval: system\nThe system with updated coordinates.\n" ); - + + typedef ::SireSystem::System (*setCoordinates_function_type)(::SireSystem::System, ::QVector> const &, bool const, ::SireBase::PropertyMap const &); + setCoordinates_function_type setCoordinates_function_value(&::SireIO::setCoordinates); + + bp::def( + "setCoordinates", setCoordinates_function_value, (bp::arg("system"), bp::arg("coordinates"), bp::arg("is_lambda1") = (bool const)(false), bp::arg("map") = SireBase::PropertyMap()), "Set the coordinates of the entire system.\nPar:am system\nThe molecular system of interest.\n\nPar:am coordinates\nThe new coordinates for the system.\n\nPar:am map\nA dictionary of user-defined molecular property names.\n\nRetval: system\nThe system with updated coordinates.\n"); } { //::SireIO::setGromacsWater - - typedef ::SireSystem::System ( *setGromacsWater_function_type )( ::SireSystem::System const &,::QString const &,::SireBase::PropertyMap const &,bool ); - setGromacsWater_function_type setGromacsWater_function_value( &::SireIO::setGromacsWater ); - - bp::def( - "setGromacsWater" - , setGromacsWater_function_value - , ( bp::arg("system"), bp::arg("model"), bp::arg("map")=SireBase::PropertyMap(), bp::arg("is_crystal")=(bool)(false) ) - , "Set all water molecules in the passed system to the appropriate GROMACS\nformat topology.\n\nPar:am system\nThe molecular system of interest.\n\nPar:am model\nThe name of the water model.\n\nPar:am map\nA dictionary of user-defined molecular property names.\n\nPar:am is_crystal\nWhether this is a crystal water molecule. If true, then the molecule\nand residue name will be set to XTL rather than SOL.\n\nRetval: system\nThe system with updated water topology.\n" ); - + + typedef ::SireSystem::System (*setGromacsWater_function_type)(::SireSystem::System const &, ::QString const &, ::SireBase::PropertyMap const &, bool); + setGromacsWater_function_type setGromacsWater_function_value(&::SireIO::setGromacsWater); + + bp::def( + "setGromacsWater", setGromacsWater_function_value, (bp::arg("system"), bp::arg("model"), bp::arg("map") = SireBase::PropertyMap(), bp::arg("is_crystal") = (bool)(false)), "Set all water molecules in the passed system to the appropriate GROMACS\nformat topology.\n\nPar:am system\nThe molecular system of interest.\n\nPar:am model\nThe name of the water model.\n\nPar:am map\nA dictionary of user-defined molecular property names.\n\nPar:am is_crystal\nWhether this is a crystal water molecule. If true, then the molecule\nand residue name will be set to XTL rather than SOL.\n\nRetval: system\nThe system with updated water topology.\n"); } { //::SireIO::setGromacsWater - - typedef ::SireMol::SelectResult ( *setGromacsWater_function_type )( ::SireMol::SelectResult const &,::QString const &,::SireBase::PropertyMap const & ); - setGromacsWater_function_type setGromacsWater_function_value( &::SireIO::setGromacsWater ); - - bp::def( - "setGromacsWater" - , setGromacsWater_function_value - , ( bp::arg("molecules"), bp::arg("model"), bp::arg("map")=SireBase::PropertyMap() ) - , "Set all water molecules in the passed system to the appropriate GROMACS\nformat topology.\n\nPar:am system\nThe molecular system of interest.\n\nPar:am model\nThe name of the water model.\n\nPar:am map\nA dictionary of user-defined molecular property names.\n\nRetval: system\nThe system with updated water topology.\n\n" ); - + + typedef ::SireMol::SelectResult (*setGromacsWater_function_type)(::SireMol::SelectResult const &, ::QString const &, ::SireBase::PropertyMap const &); + setGromacsWater_function_type setGromacsWater_function_value(&::SireIO::setGromacsWater); + + bp::def( + "setGromacsWater", setGromacsWater_function_value, (bp::arg("molecules"), bp::arg("model"), bp::arg("map") = SireBase::PropertyMap()), "Set all water molecules in the passed system to the appropriate GROMACS\nformat topology.\n\nPar:am system\nThe molecular system of interest.\n\nPar:am model\nThe name of the water model.\n\nPar:am map\nA dictionary of user-defined molecular property names.\n\nRetval: system\nThe system with updated water topology.\n\n"); } { //::SireIO::updateAndPreserveOrder - - typedef ::SireSystem::System ( *updateAndPreserveOrder_function_type )( ::SireSystem::System const &,::SireMol::Molecule const &,unsigned int ); - updateAndPreserveOrder_function_type updateAndPreserveOrder_function_value( &::SireIO::updateAndPreserveOrder ); - - bp::def( - "updateAndPreserveOrder" - , updateAndPreserveOrder_function_value - , ( bp::arg("system"), bp::arg("molecule"), bp::arg("index") ) - , "Update a molecule in the system with a different UUID while\npreserving the molecular ordering. Normally we would need to\ndelete and re-add the molecule, which would place it at the\nend, even if the MolNum was unchanged.\n\nPar:am system\nThe molecular system of interest.\n\nPar:am molecule\nThe updated molecule.\n\nPar:am index\nThe index of the molecule in the system.\n\nRetval: system\nThe system with renumbered constituents.\n" ); - + + typedef ::SireSystem::System (*updateAndPreserveOrder_function_type)(::SireSystem::System const &, ::SireMol::Molecule const &, unsigned int); + updateAndPreserveOrder_function_type updateAndPreserveOrder_function_value(&::SireIO::updateAndPreserveOrder); + + bp::def( + "updateAndPreserveOrder", updateAndPreserveOrder_function_value, (bp::arg("system"), bp::arg("molecule"), bp::arg("index")), "Update a molecule in the system with a different UUID while\npreserving the molecular ordering. Normally we would need to\ndelete and re-add the molecule, which would place it at the\nend, even if the MolNum was unchanged.\n\nPar:am system\nThe molecular system of interest.\n\nPar:am molecule\nThe updated molecule.\n\nPar:am index\nThe index of the molecule in the system.\n\nRetval: system\nThe system with renumbered constituents.\n"); } { //::SireIO::updateCoordinatesAndVelocities - - typedef ::boost::tuples::tuple< SireSystem::System, QHash< SireMol::MolIdx, SireMol::MolIdx >, 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 > ( *updateCoordinatesAndVelocities_function_type )( ::SireSystem::System const &,::SireSystem::System const &,::QHash< SireMol::MolIdx, SireMol::MolIdx > const &,bool const,::SireBase::PropertyMap const &,::SireBase::PropertyMap const & ); - updateCoordinatesAndVelocities_function_type updateCoordinatesAndVelocities_function_value( &::SireIO::updateCoordinatesAndVelocities ); - - bp::def( - "updateCoordinatesAndVelocities" - , updateCoordinatesAndVelocities_function_value - , ( bp::arg("system0"), bp::arg("system1"), bp::arg("molecule_mapping"), bp::arg("is_lambda1")=(bool const)(false), bp::arg("map0")=SireBase::PropertyMap(), bp::arg("map1")=SireBase::PropertyMap() ) - , "Update the coordinates and velocities of system0 with those from\nsystem1.\nPar:am system0\nThe reference system.\nPar:am system1\nThe updated system, where molecules may not be in the same order.\nPar:am map0\nA dictionary of user-defined molecular property names for system0.\nPar:am map1\nA dictionary of user-defined molecular property names for system1.\nRetval: system, mapping\nThe system with updated coordinates and velocities and a mapping\nbetween the molecule indices in both systems.\n" ); - + + typedef ::boost::tuples::tuple, 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> (*updateCoordinatesAndVelocities_function_type)(::SireSystem::System const &, ::SireSystem::System const &, ::QHash const &, bool const, ::SireBase::PropertyMap const &, ::SireBase::PropertyMap const &); + updateCoordinatesAndVelocities_function_type updateCoordinatesAndVelocities_function_value(&::SireIO::updateCoordinatesAndVelocities); + + bp::def( + "updateCoordinatesAndVelocities", updateCoordinatesAndVelocities_function_value, (bp::arg("system0"), bp::arg("system1"), bp::arg("molecule_mapping"), bp::arg("is_lambda1") = (bool const)(false), bp::arg("map0") = SireBase::PropertyMap(), bp::arg("map1") = SireBase::PropertyMap()), "Update the coordinates and velocities of system0 with those from\nsystem1.\nPar:am system0\nThe reference system.\nPar:am system1\nThe updated system, where molecules may not be in the same order.\nPar:am map0\nA dictionary of user-defined molecular property names for system0.\nPar:am map1\nA dictionary of user-defined molecular property names for system1.\nRetval: system, mapping\nThe system with updated coordinates and velocities and a mapping\nbetween the molecule indices in both systems.\n"); } { //::SireIO::updateCoordinatesAndVelocities - typedef ::boost::tuples::tuple< SireSystem::System, QHash< SireMol::MolIdx, SireMol::MolIdx >, 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 > ( *updateCoordinatesAndVelocities_function_type )( ::SireSystem::System const &,::SireSystem::System const &,::SireSystem::System const &,::QHash< SireMol::MolIdx, SireMol::MolIdx > const &,bool const,::SireBase::PropertyMap const &,::SireBase::PropertyMap const & ); - updateCoordinatesAndVelocities_function_type updateCoordinatesAndVelocities_function_value( &::SireIO::updateCoordinatesAndVelocities ); + typedef ::boost::tuples::tuple, 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> (*updateCoordinatesAndVelocities_function_type)(::SireSystem::System const &, ::SireSystem::System const &, ::SireSystem::System const &, ::QHash const &, bool const, ::SireBase::PropertyMap const &, ::SireBase::PropertyMap const &); + updateCoordinatesAndVelocities_function_type updateCoordinatesAndVelocities_function_value(&::SireIO::updateCoordinatesAndVelocities); bp::def( - "updateCoordinatesAndVelocities" - , updateCoordinatesAndVelocities_function_value - , ( bp::arg("original_system"), bp::arg("renumbered_system"), bp::arg("updated_system"), bp::arg("molecule_mapping"), bp::arg("is_lambda1")=(bool const)(false), bp::arg("map0")=SireBase::PropertyMap(), bp::arg("map1")=SireBase::PropertyMap() ) - , "Update the coordinates and velocities of original_system with those from\nupdated_system.\n\nPar:am system_original\nThe original system.\n\nPar:am system_renumbered\nThe original system, atoms and residues have been renumbered to be\nunique and in ascending order.\n\nPar:am system_updated\nThe updated system, where molecules may not be in the same order.\n\nPar:am map0\nA dictionary of user-defined molecular property names for system0.\n\nPar:am map1\nA dictionary of user-defined molecular property names for system1.\n\nRetval: system, mapping\nThe system with updated coordinates and velocities and a mapping\nbetween the molecule indices in both systems.\n" ); - + "updateCoordinatesAndVelocities", updateCoordinatesAndVelocities_function_value, (bp::arg("original_system"), bp::arg("renumbered_system"), bp::arg("updated_system"), bp::arg("molecule_mapping"), bp::arg("is_lambda1") = (bool const)(false), bp::arg("map0") = SireBase::PropertyMap(), bp::arg("map1") = SireBase::PropertyMap()), "Update the coordinates and velocities of original_system with those from\nupdated_system.\n\nPar:am system_original\nThe original system.\n\nPar:am system_renumbered\nThe original system, atoms and residues have been renumbered to be\nunique and in ascending order.\n\nPar:am system_updated\nThe updated system, where molecules may not be in the same order.\n\nPar:am map0\nA dictionary of user-defined molecular property names for system0.\n\nPar:am map1\nA dictionary of user-defined molecular property names for system1.\n\nRetval: system, mapping\nThe system with updated coordinates and velocities and a mapping\nbetween the molecule indices in both systems.\n"); } - { //::SireIO::mergeIntrascale + { //::SireIO::patchIntrascale - typedef ::SireBase::PropertyList ( *mergeIntrascale_function_type )( + typedef ::boost::tuple<::SireMM::CLJNBPairs, ::SireMM::CLJNBPairs> (*patchIntrascale_function_type)( ::SireMM::CLJNBPairs const &, ::SireMM::CLJNBPairs const &, - ::SireMol::MoleculeInfoData const &, - ::QHash< SireMol::AtomIdx, SireMol::AtomIdx > const &, - ::QHash< SireMol::AtomIdx, SireMol::AtomIdx > const & ); - mergeIntrascale_function_type mergeIntrascale_function_value( &::SireIO::mergeIntrascale ); + ::SireMM::CLJNBPairs, + ::SireMM::CLJNBPairs, + ::QHash const &, + ::QHash const &); + patchIntrascale_function_type patchIntrascale_function_value(&::SireIO::patchIntrascale); bp::def( - "mergeIntrascale" - , mergeIntrascale_function_value - , ( bp::arg("nb0"), bp::arg("nb1"), bp::arg("merged_info"), - bp::arg("mol0_merged_mapping"), bp::arg("mol1_merged_mapping") ) - , "Merge the CLJNBPairs (intrascale) of two molecules into the end-state\n" - "intrascales of a perturbable merged molecule.\n" - "\n" - "Uses a two-pass approach to correctly preserve actual per-pair scale factors\n" - "(including GLYCAM funct=2 overrides of 1.0/1.0) without relying on global\n" - "forcefield scale factors. For intrascale0, nb1 is copied first (so ghost\n" - "atoms from mol1 get correct exclusions), then nb0 overwrites (so mapped and\n" - "mol0-unique atoms get correct state-0 values). intrascale1 is built with the\n" - "same logic in reverse.\n" - "\n" - "Returns a PropertyList [intrascale0, intrascale1].\n" ); - + "patchIntrascale", patchIntrascale_function_value, + (bp::arg("nb0"), bp::arg("nb1"), bp::arg("intra0"), bp::arg("intra1"), + bp::arg("mol0_merged_mapping"), bp::arg("mol1_merged_mapping")), + "Patch connectivity-derived end-state intrascale matrices with non-default\n" + "per-pair scale factors from the individual molecule intrascales.\n" + "\n" + "For standard AMBER molecules this is a no-op. For force fields with\n" + "non-default per-pair values (e.g. GLYCAM funct=2 (1,1) instead of the\n" + "global 1-4 scale factor) it replaces the connectivity-derived value with\n" + "the correct per-pair value wherever they differ.\n" + "\n" + "Returns a PropertyList [intrascale0, intrascale1].\n"); } - } diff --git a/wrapper/Qt/sireqt_containers.cpp b/wrapper/Qt/sireqt_containers.cpp index 8e7c5c4d2..7bbdf83ca 100644 --- a/wrapper/Qt/sireqt_containers.cpp +++ b/wrapper/Qt/sireqt_containers.cpp @@ -92,10 +92,12 @@ void register_SireQt_containers() register_tuple>(); register_tuple>(); + register_tuple>(); register_tuple, QVector, QVector>>(); register_list>>(); register_list>>(); + register_list>>(); register_dict>>(); diff --git a/wrapper/Tools/OpenMMMD.py b/wrapper/Tools/OpenMMMD.py index 97fe9e401..ef36ca337 100644 --- a/wrapper/Tools/OpenMMMD.py +++ b/wrapper/Tools/OpenMMMD.py @@ -1,11 +1,7 @@ -"""RUN SCRIPT to perform an MD simulation in Sire with OpenMM - -""" +"""RUN SCRIPT to perform an MD simulation in Sire with OpenMM""" import os import sys -import re -import math import time import platform as pf import warnings @@ -47,7 +43,6 @@ from Sire.Tools import Parameter, resolveParameters import Sire.Stream - __author__ = "Julien Michel, Gaetano Calabro, Antonia Mey, Hannes H Loeffler" __version__ = "0.2" __license__ = "GPL" @@ -103,7 +98,7 @@ restart_file = Parameter( "restart file", "sim_restart.s3", - "Filename of the restart file to use to save progress during the " "simulation.", + "Filename of the restart file to use to save progress during the simulation.", ) dcd_root = Parameter( @@ -115,7 +110,7 @@ nmoves = Parameter( "nmoves", 1000, - "Number of Molecular Dynamics moves to be performed during the " "simulation.", + "Number of Molecular Dynamics moves to be performed during the simulation.", ) debug_seed = Parameter( @@ -130,8 +125,7 @@ ncycles = Parameter( "ncycles", 1, - "The number of MD cycles. The total elapsed time will be " - "nmoves*ncycles*timestep", + "The number of MD cycles. The total elapsed time will be nmoves*ncycles*timestep", ) maxcycles = Parameter( @@ -158,7 +152,7 @@ minimal_coordinate_saving = Parameter( "minimal coordinate saving", False, - "Reduce the number of coordiantes writing for states" "with lambda in ]0,1[", + "Reduce the number of coordiantes writing for states with lambda in ]0,1[", ) time_to_skip = Parameter( @@ -258,8 +252,7 @@ andersen = Parameter( "thermostat", True, - "Whether or not to use the Andersen thermostat (needed for NVT or NPT " - "simulation).", + "Whether or not to use the Andersen thermostat (needed for NVT or NPT simulation).", ) barostat = Parameter( @@ -432,7 +425,7 @@ morphfile = Parameter( "morphfile", "MORPH.pert", - "Name of the morph file containing the perturbation to apply to the " "system.", + "Name of the morph file containing the perturbation to apply to the system.", ) lambda_val = Parameter( @@ -1396,7 +1389,7 @@ def setupBoreschRestraints(system): if anchors_not_present: print("Error! The following anchor points do not not exist in the system:") for anchor in anchors_not_present: - print(f"{anchor}: index {anchors_dict[anchor]-1}") + print(f"{anchor}: index {anchors_dict[anchor] - 1}") sys.exit(-1) # The solute will store all the information related to the Boresch restraints in the system @@ -2515,7 +2508,10 @@ def selectWatersForPerturbation(system, charge_diff): # FIXME: select waters according to distance criterion # if mol.residue().name() == water_resname and cnt < nions: if mol.residues()[0].name() == water_resname and cnt < nions: - print ("Selected water residue %s for perturbation into ion" % (mol.residues()[0])) + print( + "Selected water residue %s for perturbation into ion" + % (mol.residues()[0]) + ) cnt += 1 perturbed_water = mol.edit() @@ -2527,6 +2523,10 @@ def selectWatersForPerturbation(system, charge_diff): mol = water_pert.applyTemplate(mol) mol = mol.edit().rename(WATER_NAME).commit() + print( + "Selecting water %s for charge perturbation\n" % repr(mol.residues()[0]) + ) + changedmols.add(mol) system.update(changedmols) @@ -2552,9 +2552,9 @@ def run(): print("\n### Running Molecular Dynamics simulation on %s ###" % host) if verbose.val: - print("###================= Simulation Parameters=====================" "###") + print("###================= Simulation Parameters=====================###") Parameter.printAll() - print("###===========================================================" "###\n") + print("###===========================================================###\n") timer = Sire.Qt.QTime() timer.start() @@ -2567,9 +2567,9 @@ def run(): amber = Sire.IO.Amber() if os.path.exists(s3file.val): - (molecules, space) = Sire.Stream.load(s3file.val) + molecules, space = Sire.Stream.load(s3file.val) else: - (molecules, space) = amber.readCrdTop(crdfile.val, topfile.val) + molecules, space = amber.readCrdTop(crdfile.val, topfile.val) Sire.Stream.save((molecules, space), s3file.val) system = createSystem(molecules) @@ -2582,12 +2582,10 @@ def run(): system = setupRestraints(system) if turn_on_restraints_mode.val: - print( - """In "turn on receptor-ligand restraints mode". Receptor-ligand + print("""In "turn on receptor-ligand restraints mode". Receptor-ligand restraint strengths will be scaled with lambda. Ensure that a dummy pert file which maps all original ligand atom parameters to themselves - has been supplied.""" - ) + has been supplied.""") system = saveTurnOnRestraintsModeProperty(system) if use_distance_restraints.val: @@ -2694,7 +2692,7 @@ def run(): print("Energy minimization done.") integrator.setConstraintType(constraint.val) print( - "###===========================================================" "###\n", + "###===========================================================###\n", flush=True, ) @@ -2713,7 +2711,7 @@ def run(): print("Energy after the equilibration: " + str(system.energy())) print("Equilibration done.\n") print( - "###===========================================================" "###\n", + "###===========================================================###\n", flush=True, ) @@ -2751,9 +2749,9 @@ def runFreeNrg(): ) if verbose.val: - print("###================= Simulation Parameters=====================" "###") + print("###================= Simulation Parameters=====================###") Parameter.printAll() - print("###===========================================================" "###\n") + print("###===========================================================###\n") timer = Sire.Qt.QTime() timer.start() @@ -2837,8 +2835,8 @@ def runFreeNrg(): if debug_seed.val != 0: print("Setting up the simulation with debugging seed %s" % debug_seed.val) + print("The difference in charge is", charge_diff.val) if charge_diff.val != 0: - print("The difference in charge is", charge_diff.val) system = selectWatersForPerturbation(system, charge_diff.val) moves = setupMovesFreeEnergy(system, debug_seed.val, gpu.val, lambda_val.val) @@ -2863,8 +2861,7 @@ def runFreeNrg(): ) outfile.write( bytes( - "#For more information visit: " - "https://github.com/openbiosim/sire\n#\n", + "#For more information visit: https://github.com/openbiosim/sire\n#\n", "UTF-8", ) ) @@ -2966,8 +2963,7 @@ def runFreeNrg(): print("Running minimization.") print(f"Tolerance for minimization: {str(minimise_tol.val)}") print( - "Maximum number of minimization iterations: " - f"{str(minimise_max_iter.val)}" + f"Maximum number of minimization iterations: {str(minimise_max_iter.val)}" ) system = integrator.minimiseEnergy( @@ -2976,11 +2972,9 @@ def runFreeNrg(): system.mustNowRecalculateFromScratch() - print( - "Energy after the minimization: " f"{integrator.getPotentialEnergy(system)}" - ) + print(f"Energy after the minimization: {integrator.getPotentialEnergy(system)}") print("Energy minimization done.") - print("###===========================================================" "###\n") + print("###===========================================================###\n") if equilibrate.val: print("###======================Equilibration========================###") @@ -2998,7 +2992,7 @@ def runFreeNrg(): f"Energy after the annealing: {integrator.getPotentialEnergy(system)}" ) print("Lambda annealing done.\n") - print("###===========================================================" "###\n") + print("###===========================================================###\n") print("###====================somd-freenrg run=======================###") print("Starting somd-freenrg run...") @@ -3025,8 +3019,8 @@ def runFreeNrg(): # saving all data beg = ( - nmoves.val * (i - 1) - ) + energy_frequency.val # Add energy_frequency beacuse energies not saved at t = 0 + (nmoves.val * (i - 1)) + energy_frequency.val + ) # Add energy_frequency beacuse energies not saved at t = 0 end = nmoves.val * (i - 1) + nmoves.val + energy_frequency.val steps = list(range(beg, end, energy_frequency.val)) outdata = getAllData(integrator, steps) diff --git a/wrapper/python/scripts/somd-freenrg.py b/wrapper/python/scripts/somd-freenrg.py index 394d2b33f..14e04be13 100644 --- a/wrapper/python/scripts/somd-freenrg.py +++ b/wrapper/python/scripts/somd-freenrg.py @@ -18,7 +18,6 @@ from Sire.Tools import OpenMMMD from Sire.Tools import readParams - parser = argparse.ArgumentParser( description="Perform molecular dynamics single topology free energy calculations " "using OpenMM", @@ -75,8 +74,7 @@ "-m", "--morph_file", nargs="?", - help="The morph file describing the single topology " - "calculation to be performed.", + help="The morph file describing the single topology calculation to be performed.", ) parser.add_argument( @@ -134,8 +132,7 @@ print("somd-freenrg -- from Sire release version <%s>" % Sire.__version__) print( "This particular release can be downloaded here: " - "https://github.com/openbiosim/sire/releases/tag/v%s" - % Sire.__version__ + "https://github.com/openbiosim/sire/releases/tag/v%s" % Sire.__version__ ) must_exit = True @@ -196,7 +193,8 @@ lambda_val = float(args.lambda_val) params["lambda_val"] = lambda_val -params["charge difference"] = args.charge_diff +if args.charge_diff: + params["charge difference"] = args.charge_diff if not ( os.path.exists(coord_file)