Unverified Commit f55abcaa authored by Evan Pretti's avatar Evan Pretti Committed by GitHub
Browse files

Add constant potential method (#4870)



* Initial implementation of C++ API

* Add kernel interface and information for API generation

* API updates for updating electrode parameters

* Add serialization proxy for ConstantPotentialForce

* Update file headers

* Add CG error tolerance and fix units on getCharges() return value

* Initial implementation of matrix solver

* Fixes and conjugate gradient solver

* Try to fix Linux and Windows builds

* Make sure charge constraint target is on total charge

* Restore handling of exceptions like NonbondedForce since they won't involve electrode atoms

* Ameliorate numerical instability in constrained conjugate gradient

* Fix uninitialized pointers, memory leak, and style

* Set CG tolerance units in Python API

* Test ConstantPotentialForce serialization

* Read/write ExceptionsUsePeriodicBoundaryConditions as bool

* Improve constrained conjugate gradient robustness to roundoff error accumulation

* Recompute matrix if electrode atoms move due to setPositions()

* Tolerance is now in gradient (potential) units again

* Add neutralizing background correction

* Add Python API tests

* Fixes for CG and nonbonded exceptions

* Add initial tests checking against existing NonbondedForce behavior

* Expand test suite and fix some implementation issues

* Add additional tests using larger reference system

* Add Gaussian test

* Finish test against reference computation

* CPU platform implementation

* Fixes for compilation on some platforms

* Fixes for constant potential with AVX/AVX2

* Test linking CPU PME library to constant potential test directly

* Older SWIG versions don't support Python set to C++ set conversion

* Add user guide entry

* Increase speed of reference test

* Conditional building constant potential CPU test is unreliable

* Debugging

* Miscellaneous fixes and improvements for CI

* Cache charges so solver will not run if system and coordinates have not changed

* Preconditioner flag, stability, and automatic detection improvements

* Add GPU platform-specific constant potential kernel classes

* PME and device-host I/O changes to support constant potential

* Initial common constant potential implementation

* Constant potential fixes:

* Fix preconditioner PME position/charge save/restore logic

* Fix reduction synchronization in constant potential solver kernels

* Add double-float accumulation for conjugate gradient solver when
  double unsupported by hardware

* Improve conditioning of a test system, and make sure particles are in or
out of cutoff for consistency and ease of comparing between platforms

* Reorder guess charges for CG when atom reordering changes positions

* Remove PME queue for now

* Trying to debug optimized direct space derivative kernel

* Remove extraneous debugging lines

* Style updates; just make CPU preconditioner double precision

* Debugging updated optimized direct derivatives kernel for all but OpenCL CPU

* OpenCL CPU implementation of direct space derivatives, and cleanup

* Try to make test even shorter to not time out on CI

* Temporary - Debugging

* Debugging

* Debugging

* Debugging

* Debugging

* Remove debugging code and fix reduction synchronization

* Fix other reductions

* Debugging - are tests hanging or just slow on CI?

* Debugging

* Debugging

* Fix macro for case when double precision is available on hardware

* Remove changes for debugging again

* Try to improve matrix solver cache locality by uploading transpose

* Fixes for atom ordering and periodic images

* Can't rely on reorder listener for cell offset updates

* Test reducing number of contexts and timing for CI

* Debugging

* Remove timing code and revert debugging changes

* Matrix solver and plasma term optimizations

* Reduce CG solver kernel calls and downloads

* Don't read back convergence flag from global memory

* Update PME due to refactoring in master branch

* Faster matrix solver (1st step)

* Faster matrix solver for CUDA

* Faster matrix solver compatibility with non-CUDA platforms

* Matrix solver fixes

* Use warp shuffle reductions when possible

* Attempt to work around intermittent compiler crash in Intel CPU OpenCL

* Optimize CG solver kernel 1

* Rework CG solver so some kernels can use more than 1 block

* Don't run out of shared memory

* Asynchronously download convergence flag while clearing buffers

---------
Co-authored-by: default avatarEvan Pretti <pretti@sh03-17n15.int>
parent 0ad62341
...@@ -463,6 +463,9 @@ ENDIF (EXECUTABLE_OUTPUT_PATH) ...@@ -463,6 +463,9 @@ ENDIF (EXECUTABLE_OUTPUT_PATH)
ADD_SUBDIRECTORY(docs-source) ADD_SUBDIRECTORY(docs-source)
IF(BUILD_TESTING) IF(BUILD_TESTING)
ADD_SUBDIRECTORY(tests) ADD_SUBDIRECTORY(tests)
IF(OPENMM_BUILD_CPU_LIB AND OPENMM_BUILD_CPU_TESTS AND (NOT OPENMM_BUILD_PME_PLUGIN))
MESSAGE(SEND_ERROR "The CPU platform tests require that the CPU PME plugin be built.")
ENDIF(OPENMM_BUILD_CPU_LIB AND OPENMM_BUILD_CPU_TESTS AND (NOT OPENMM_BUILD_PME_PLUGIN))
ENDIF(BUILD_TESTING) ENDIF(BUILD_TESTING)
SET(OPENMM_BUILD_EXAMPLES ON CACHE BOOL "Build example executables") SET(OPENMM_BUILD_EXAMPLES ON CACHE BOOL "Build example executables")
......
...@@ -7,7 +7,7 @@ ...@@ -7,7 +7,7 @@
set -euxo pipefail set -euxo pipefail
# This is the image for PowerPC + CUDA # This is the image for PowerPC + CUDA
export DOCKER_IMAGE="quay.io/condaforge/linux-anvil-ppc64le-cuda:10.2" export DOCKER_IMAGE="quay.io/condaforge/linux-anvil-ppc64le-cuda:11.8"
# # Use this other one for ARM debugging # # Use this other one for ARM debugging
# export DOCKER_IMAGE="quay.io/condaforge/linux-anvil-aarch64" # export DOCKER_IMAGE="quay.io/condaforge/linux-anvil-aarch64"
...@@ -17,7 +17,7 @@ export COMPILERS="compilers" ...@@ -17,7 +17,7 @@ export COMPILERS="compilers"
# export COMPILERS="devtoolset-7" # export COMPILERS="devtoolset-7"
# Choose your Python version # Choose your Python version
export PYTHON_VER="3.9" export PYTHON_VER="3.13"
# Number of CPUs to use # Number of CPUs to use
export CPU_COUNT=2 export CPU_COUNT=2
...@@ -51,4 +51,4 @@ docker run \ ...@@ -51,4 +51,4 @@ docker run \
# Once you are inside the Docker session, you can use this to reproduce the CI steps: # Once you are inside the Docker session, you can use this to reproduce the CI steps:
# #
# bash /home/conda/workspace/devtools/ci/gh-actions/scripts/run_steps_inside_docker_image.sh # bash /home/conda/workspace/devtools/ci/gh-actions/scripts/run_steps_inside_docker_image.sh
\ No newline at end of file
...@@ -24,6 +24,7 @@ These classes implement forces that are widely used in biomolecular simulation. ...@@ -24,6 +24,7 @@ These classes implement forces that are widely used in biomolecular simulation.
.. toctree:: .. toctree::
:maxdepth: 2 :maxdepth: 2
generated/ConstantPotentialForce
generated/CMAPTorsionForce generated/CMAPTorsionForce
generated/DrudeForce generated/DrudeForce
generated/GBSAOBCForce generated/GBSAOBCForce
......
...@@ -125,6 +125,16 @@ ...@@ -125,6 +125,16 @@
type = {Journal Article} type = {Journal Article}
} }
@article{Dufils2019
author = {Dufils, Thomas and Jeanmairet, Guillaume and Rotenberg, Benjamin and Sprik, Michiel and Salanne, Mathieu},
title = {Simulating electrochemical systems by combining the finite field method with a constant potential electrode},
journal = {Physical Review Letters},
volume = {123},
pages = {195501},
year = {2019},
type = {Journal Article}
}
@article{Espanol1995 @article{Espanol1995
author = {Español, P. and Warren, P.}, author = {Español, P. and Warren, P.},
title = {Statistical Mechanics of Dissipative Particle Dynamics}, title = {Statistical Mechanics of Dissipative Particle Dynamics},
...@@ -490,6 +500,16 @@ ...@@ -490,6 +500,16 @@
type = {Journal Article} type = {Journal Article}
} }
@article{Scalfi2020
author = {Scalfi, Laura and Dufils, Thomas and Reeves, Kyle G. and Rotenberg, Benjamin and Salanne, Mathieu},
title = {A semiclassical Thomas-Fermi model to tune the metallicity of electrodes in molecular simulations},
journal = {Journal of Chemical Physics},
volume = {153},
pages = {174704},
year = {2020},
type = {Journal Article}
}
@article{Schaefer1998 @article{Schaefer1998
author = {Schaefer, Michael and Bartels, Christian and Karplus, Martin}, author = {Schaefer, Michael and Bartels, Christian and Karplus, Martin},
title = {Solution conformations and thermodynamics of structured peptides: molecular dynamics simulation with an implicit solvation model}, title = {Solution conformations and thermodynamics of structured peptides: molecular dynamics simulation with an implicit solvation model},
......
...@@ -321,6 +321,8 @@ systems and parameter values, but no guarantees are made. It is important to ...@@ -321,6 +321,8 @@ systems and parameter values, but no guarantees are made. It is important to
validate your own simulations, and identify parameter values that produce validate your own simulations, and identify parameter values that produce
acceptable accuracy for each system. acceptable accuracy for each system.
.. _coulomb-interaction-pme:
Coulomb Interaction With Particle Mesh Ewald Coulomb Interaction With Particle Mesh Ewald
============================================ ============================================
...@@ -460,6 +462,101 @@ where :math:`r_i` is the atomic radius of particle *i*\ , :math:`r_i` is ...@@ -460,6 +462,101 @@ where :math:`r_i` is the atomic radius of particle *i*\ , :math:`r_i` is
its atomic radius, and :math:`r_\mathit{solvent}` is the solvent radius, which is taken its atomic radius, and :math:`r_\mathit{solvent}` is the solvent radius, which is taken
to be 0.14 nm. The default value for the energy scale :math:`E_{SA}` is 2.25936 kJ/mol/nm\ :sup:`2`\ . to be 0.14 nm. The default value for the energy scale :math:`E_{SA}` is 2.25936 kJ/mol/nm\ :sup:`2`\ .
ConstantPotentialForce
**********************
This force is an implementation of the periodic finite field constant potential
method :cite:`Dufils2019`. The constant potential method implements Coulomb
interactions between particles using the particle mesh Ewald method as described
in section :numref:`coulomb-interaction-pme`. Unlike Coulomb interactions
implemented by a NonbondedForce that use fixed charges, the constant potential
method allows certain particles in a system to be designated electrode particles
whose charges can fluctuate. Additionally, instead of point charges, these
electrode particles have Gaussian charge densities, are held at given electric
potentials, and can optionally employ a simple Thomas-Fermi metallicity model
:cite:`Scalfi2020`. Finally, an electric field can be applied to all charged
particles in the system. For a given configuration of all particles, the
charges on electrode particles are set to minimize the total electrostatic
energy, given by
.. math::
E=E_{PME}+E_{gauss}+E_{self}+E_{field}+E_{potential}+E_{TF}
Here :math:`E_{PME}` is the interaction of point particles computed by the
particle mesh Ewald method. Now
.. math::
E_{gauss}=-\frac{1}{2}\sum_{i,j}q_iq_i\frac{\text{erfc}\left(\eta_{ij}r_{ij}\right)}{r_{ij}}
with :math:`\eta_{ij}=1/\sqrt{w_i^2+w_j^2}`, where :math:`w_i=0` for a
non-electrode (point) particle and :math:`w_i>0` for an electrode particle. No
term is included in the sum when :math:`i=j` or :math:`w_i=w_j=0`. Note that in
OpenMM's implementation, specification of an electrode requires the Gaussian
width :math:`w_i`, while the reciprocal width :math:`\eta_i=1/w_i` is often used
in the literature. In addition to this correction to interactions between
particles, the presence of Gaussian charge densities on electrode particles
contributes a self-interaction energy:
.. math::
E_{self}=\frac{1}{\sqrt{2\pi}}\sum_{i\in\text{elec}}\frac{q_i^2}{w_i}
Next, for the electric field :math:`\mathbf{E}` (on all particles) and applied
potential (:math:`\Psi_i` on electrode particles :math:`i`),
.. math::
E_{field}=-\sum_{i}q_i\mathbf{r}_i\cdot\mathbf{E}
.. math::
E_{potential}=-\sum_{i\in\text{elec}}q_i\Psi_i
Finally, for the Thomas-Fermi contribution on the electrode particles,
.. math::
E_{TF}=2\pi\sum_{i\in\text{elec}}q_i^2\left(\frac{\ell_{TF,i}^2}{v_{TF,i}}\right)
where :math:`\ell_{TF,i}` is the Thomas-Fermi length and :math:`v_{TF,i}` is a
characteristic volume scale (often referred to as a "Voronoi volume" in other
implementations). In OpenMM's implementation, the parameter
:math:`\ell_{TF,i}^2/v_{TF,i}` is specified as a single Thomas-Fermi parameter,
with units of reciprocal length, for all particles in an electrode.
Besides these additional terms, and the fluctuating nature of electrode
particles, there are a few other important differences between the
implementation of electrostatic interactions in ConstantPotentialForce and that
in NonbondedForce using PME:
1. ConstantPotentialForce does not include any capability for computing
Lennard-Jones interactions. If Lennard-Jones interactions as NonbondedForce
computes them are desired, a separate NonbondedForce should be added to the
system with the appropriate sigma and epsilon parameters set, and with all
charges set to zero.
2. For the solved charges on electrode particles to be valid, all particles in
the system must use a single ConstantPotentialForce. Setting charges in more
than one ConstantPotentialForce, or a NonbondedForce, will produce invalid
results, as the solution of electrode charges requires global minimization of
the total electrostatic energy of the system.
3. The Gaussian charge correction to pairwise interactions given by
:math:`E_{gauss}` above is calculated using the minimum image convention and
truncated with a fixed cutoff :math:`r_{cut}`. If decreasing :math:`r_{cut}`
to tune PME performance, ensure it stays large enough with respect to the
largest Gaussian width :math:`w_i` in the system. Using the formula for the
direct space error in section :numref:`coulomb-interaction-pme` with
:math:`1/w_i` in place of :math:`\alpha` (owing to the similarities in the
functional forms of these terms), :math:`r_{cut}\ge2.7w_{max}` should be
sufficient for the default error tolerance :math:`\delta=5\times10^{-4}`.
ConstantPotentialForce provides two algorithms to solve for electrode charges.
The matrix solver precomputes a capacitance matrix for the system before a
simulation and solves directly for charges, while the conjugate gradient (CG)
solver finds charges iteratively without requiring a matrix inversion. The
matrix solver may be faster if the number of electrode particles is small
enough, although it cannot be used unless the electrode particles are fixed in
place during a simulation by setting their masses to zero. Unless the positions
of electrode particles are allowed to fluctuate (in which case only the CG
solver can be used), both solvers can be benchmarked on a given problem to find
the one with the best performance.
GayBerneForce GayBerneForce
************* *************
......
...@@ -192,7 +192,7 @@ Array2D<Real> Cholesky<Real>::solve(const Array2D<Real> &B) ...@@ -192,7 +192,7 @@ Array2D<Real> Cholesky<Real>::solve(const Array2D<Real> &B)
return Array2D<Real>(); return Array2D<Real>();
if (!isspd) if (!isspd)
return Arran2D<Real>(); return Array2D<Real>();
Array2D<Real> X = B.copy(); Array2D<Real> X = B.copy();
......
...@@ -37,6 +37,7 @@ ...@@ -37,6 +37,7 @@
#include "openmm/BrownianIntegrator.h" #include "openmm/BrownianIntegrator.h"
#include "openmm/CMAPTorsionForce.h" #include "openmm/CMAPTorsionForce.h"
#include "openmm/CMMotionRemover.h" #include "openmm/CMMotionRemover.h"
#include "openmm/ConstantPotentialForce.h"
#include "openmm/CustomAngleForce.h" #include "openmm/CustomAngleForce.h"
#include "openmm/CustomBondForce.h" #include "openmm/CustomBondForce.h"
#include "openmm/CustomCentroidBondForce.h" #include "openmm/CustomCentroidBondForce.h"
...@@ -655,6 +656,64 @@ public: ...@@ -655,6 +656,64 @@ public:
virtual void getLJPMEParameters(double& alpha, int& nx, int& ny, int& nz) const = 0; virtual void getLJPMEParameters(double& alpha, int& nx, int& ny, int& nz) const = 0;
}; };
/**
* This kernel is invoked by ConstantPotentialForce to calculate the forces acting on the system and the energy of the system.
*/
class CalcConstantPotentialForceKernel : public KernelImpl {
public:
static std::string Name() {
return "CalcConstantPotentialForce";
}
CalcConstantPotentialForceKernel(std::string name, const Platform& platform) : KernelImpl(name, platform) {
}
/**
* Initialize the kernel.
*
* @param system the System this kernel will be applied to
* @param force the ConstantPotentialForce this kernel will be used for
*/
virtual void initialize(const System& system, const ConstantPotentialForce& force) = 0;
/**
* Execute the kernel to calculate the forces and/or energy.
*
* @param context the context in which to execute this kernel
* @param includeForces true if forces should be calculated
* @param includeEnergy true if the energy should be calculated
* @return the potential energy due to the force
*/
virtual double execute(ContextImpl& context, bool includeForces, bool includeEnergy) = 0;
/**
* Copy changed parameters over to a context.
*
* @param context the context to copy parameters to
* @param force the ConstantPotentialForce to copy the parameters from
* @param firstParticle the index of the first particle whose parameters might have changed
* @param lastParticle the index of the last particle whose parameters might have changed
* @param firstException the index of the first exception whose parameters might have changed
* @param lastException the index of the last exception whose parameters might have changed
* @param firstElectrode the index of the first electrode whose parameters might have changed
* @param lastElectrode the index of the last electrode whose parameters might have changed
*/
virtual void copyParametersToContext(ContextImpl& context, const ConstantPotentialForce& force, int firstParticle, int lastParticle, int firstException, int lastException, int firstElectrode, int lastElectrode) = 0;
/**
* Get the parameters being used for PME.
*
* @param alpha the separation parameter
* @param nx the number of grid points along the X axis
* @param ny the number of grid points along the Y axis
* @param nz the number of grid points along the Z axis
*/
virtual void getPMEParameters(double& alpha, int& nx, int& ny, int& nz) const = 0;
/**
* Get the charges on all particles.
*
* @param context the context to copy parameters to
* @param[out] charges a vector to populate with particle charges
*/
virtual void getCharges(ContextImpl& context, std::vector<double>& charges) = 0;
};
/** /**
* This kernel is invoked by CustomNonbondedForce to calculate the forces acting on the system and the energy of the system. * This kernel is invoked by CustomNonbondedForce to calculate the forces acting on the system and the energy of the system.
*/ */
...@@ -1698,18 +1757,21 @@ public: ...@@ -1698,18 +1757,21 @@ public:
* @param gridy the y size of the PME grid * @param gridy the y size of the PME grid
* @param gridz the z size of the PME grid * @param gridz the z size of the PME grid
* @param numParticles the number of particles in the system * @param numParticles the number of particles in the system
* @param indices indices of particles to compute charge derivatives for
* @param alpha the Ewald blending parameter * @param alpha the Ewald blending parameter
* @param deterministic whether it should attempt to make the resulting forces deterministic * @param deterministic whether it should attempt to make the resulting forces deterministic
*/ */
virtual void initialize(int gridx, int gridy, int gridz, int numParticles, double alpha, bool deterministic) = 0; virtual void initialize(int gridx, int gridy, int gridz, int numParticles, const std::vector<int>& indices, double alpha, bool deterministic) = 0;
/** /**
* Begin computing the force and energy. * Begin computing the force and energy.
* *
* @param io an object that coordinates data transfer * @param io an object that coordinates data transfer
* @param periodicBoxVectors the vectors defining the periodic box (measured in nm) * @param periodicBoxVectors the vectors defining the periodic box (measured in nm)
* @param includeEnergy true if potential energy should be computed * @param includeEnergy true if potential energy should be computed
* @param includeForces true if forces should be computed
* @param includeChargeDerivatives true if charge derivatives should be computed
*/ */
virtual void beginComputation(IO& io, const Vec3* periodicBoxVectors, bool includeEnergy) = 0; virtual void beginComputation(IO& io, const Vec3* periodicBoxVectors, bool includeEnergy, bool includeForces, bool includeChargeDerivatives) = 0;
/** /**
* Finish computing the force and energy. * Finish computing the force and energy.
* *
...@@ -1747,9 +1809,15 @@ public: ...@@ -1747,9 +1809,15 @@ public:
* should be ignored. * should be ignored.
*/ */
virtual void setForce(float* force) = 0; virtual void setForce(float* force) = 0;
/**
* Record the charge derivatives calculated by the kernel.
*
* @param chargeDerivatives an array containing one element for each atom
* to compute charge derivatives for.
*/
virtual void setChargeDerivatives(float* chargeDerivatives) = 0;
}; };
/** /**
* This kernel performs the dispersion reciprocal space calculation for LJPME. In most cases, this * This kernel performs the dispersion reciprocal space calculation for LJPME. In most cases, this
* calculation is done directly by CalcNonbondedForceKernel so this kernel is unneeded. * calculation is done directly by CalcNonbondedForceKernel so this kernel is unneeded.
......
...@@ -37,6 +37,7 @@ ...@@ -37,6 +37,7 @@
#include "openmm/CMAPTorsionForce.h" #include "openmm/CMAPTorsionForce.h"
#include "openmm/CMMotionRemover.h" #include "openmm/CMMotionRemover.h"
#include "openmm/CompoundIntegrator.h" #include "openmm/CompoundIntegrator.h"
#include "openmm/ConstantPotentialForce.h"
#include "openmm/CustomBondForce.h" #include "openmm/CustomBondForce.h"
#include "openmm/CustomCentroidBondForce.h" #include "openmm/CustomCentroidBondForce.h"
#include "openmm/CustomCompoundBondForce.h" #include "openmm/CustomCompoundBondForce.h"
......
#ifndef OPENMM_CONSTANTPOTENTIALFORCE_H_
#define OPENMM_CONSTANTPOTENTIALFORCE_H_
/* -------------------------------------------------------------------------- *
* OpenMM *
* -------------------------------------------------------------------------- *
* This is part of the OpenMM molecular simulation toolkit originating from *
* Simbios, the NIH National Center for Physics-Based Simulation of *
* Biological Structures at Stanford, funded under the NIH Roadmap for *
* Medical Research, grant U54 GM072970. See https://simtk.org. *
* *
* Portions copyright (c) 2008-2025 Stanford University and the Authors. *
* Authors: Peter Eastman, Evan Pretti *
* Contributors: *
* *
* Permission is hereby granted, free of charge, to any person obtaining a *
* copy of this software and associated documentation files (the "Software"), *
* to deal in the Software without restriction, including without limitation *
* the rights to use, copy, modify, merge, publish, distribute, sublicense, *
* and/or sell copies of the Software, and to permit persons to whom the *
* Software is furnished to do so, subject to the following conditions: *
* *
* The above copyright notice and this permission notice shall be included in *
* all copies or substantial portions of the Software. *
* *
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR *
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, *
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL *
* THE AUTHORS, CONTRIBUTORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, *
* DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR *
* OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE *
* USE OR OTHER DEALINGS IN THE SOFTWARE. *
* -------------------------------------------------------------------------- */
#include "Context.h"
#include "Force.h"
#include <map>
#include <set>
#include <utility>
#include <vector>
#include "internal/windowsExport.h"
namespace OpenMM {
/**
* This class implements the periodic finite field constant potential method.
* This is described in Dufils et al., Phys. Rev. Lett. 123, 195501 (2019).
* This class implements a standard Coulomb force using the particle mesh Ewald
* method, but it can also solve for the magnitudes of Gaussian charge
* distributions on conducting electrodes held at fixed potentials and apply
* external electric fields. An implementation of a semiclassical Thomas-Fermi
* model described in Scalfi et al., J. Chem. Phys. 153, 174704 (2020) is also
* included. Unlike NonbondedForce, Lennard-Jones interactions are not computed
* by this force, and should be specified in another force if they are desired.
*
* To use this class, create a ConstantPotentialForce object, then call
* addParticle() once for each particle in the System to define its parameters.
* The number of particles for which you define nonbonded parameters must be
* exactly equal to the number of particles in the System, or else an exception
* will be thrown when you try to create a Context. After a particle has been
* added, you can modify its force field parameters by calling
* setParticleParameters(). This will have no effect on Contexts that already
* exist unless you call updateParametersInContext().
*
* ConstantPotentialForce also lets you specify "exceptions", particular pairs
* of particles whose interactions should be computed based on different
* parameters than those defined for the individual particles. This can be used
* to completely exclude certain interactions from the force calculation, or to
* alter how they interact with each other.
*
* Many molecular force fields omit Coulomb and Lennard-Jones interactions
* between particles separated by one or two bonds, while using modified
* parameters for those separated by three bonds (known as "1-4 interactions").
* This class provides a convenience method for this case called
* createExceptionsFromBonds(). You pass to it a list of bonds and the scale
* factors to use for 1-4 interactions. It identifies all pairs of particles
* which are separated by 1, 2, or 3 bonds, then automatically creates
* exceptions for them.
*
* To treat a group of particles as an electrode (such that the charges
* specified for them using addParticle() or setParticleParameters() will be
* ignored, and charges will be solved for over the course of the simulation
* instead), call addElectrode() with a set of particle indices. After an
* electrode has been added, you can modify its parameters by calling
* setElectrodeParameters(). A constraint on the total charge of the system can
* be enabled with setUseChargeConstraint() and setChargeConstraintTarget(), and
* an external field can be applied by using setExternalField() to specify a
* non-zero electric field strength. Once a Context has been created, calling
* getCharges() with the Context will return a vector of the actual current
* charges on each particle, including the solved fluctuating charges on the
* electrode particles.
*/
class OPENMM_EXPORT ConstantPotentialForce : public Force {
public:
/**
* This is an enumeration of the different methods that may be used for
* solving for electrode charges.
*/
enum ConstantPotentialMethod {
/**
* A conjugate gradient method is used to iteratively solve for the
* electrode charges at each simulation step.
*/
CG = 0,
/**
* A capacitance matrix is precomputed at the start of the simulation
* and is used to directly calculate the electrode charges at each
* simulation step. This method can only be used if all electrode
* particles have fixed positions and the periodic box does not change.
*/
Matrix = 1,
};
/**
* Create a ConstantPotentialForce.
*/
ConstantPotentialForce();
/**
* Get the number of particles for which force field parameters have been defined.
*/
int getNumParticles() const {
return particles.size();
}
/**
* Get the number of special interactions that should be calculated differently from other interactions.
*/
int getNumExceptions() const {
return exceptions.size();
}
/**
* Get the cutoff distance (in nm) being used for nonbonded interactions.
*
* @return the cutoff distance, measured in nm
*/
double getCutoffDistance() const;
/**
* Set the cutoff distance (in nm) being used for nonbonded interactions.
*
* @param distance the cutoff distance, measured in nm
*/
void setCutoffDistance(double distance);
/**
* Get the error tolerance for Ewald summation. This corresponds to the fractional error in the forces
* which is acceptable. This value is used to select the reciprocal space cutoff and separation
* parameter so that the average error level will be less than the tolerance. There is not a
* rigorous guarantee that all forces on all atoms will be less than the tolerance, however.
*
* For PME calculations, if setPMEParameters() is used to set alpha to something other than 0,
* this value is ignored.
*/
double getEwaldErrorTolerance() const;
/**
* Set the error tolerance for Ewald summation. This corresponds to the fractional error in the forces
* which is acceptable. This value is used to select the reciprocal space cutoff and separation
* parameter so that the average error level will be less than the tolerance. There is not a
* rigorous guarantee that all forces on all atoms will be less than the tolerance, however.
*
* For PME calculations, if setPMEParameters() is used to set alpha to something other than 0,
* this value is ignored.
*/
void setEwaldErrorTolerance(double tol);
/**
* Get the parameters to use for PME calculations. If alpha is 0 (the default), these parameters are
* ignored and instead their values are chosen based on the Ewald error tolerance.
*
* @param[out] alpha the separation parameter
* @param[out] nx the number of grid points along the X axis
* @param[out] ny the number of grid points along the Y axis
* @param[out] nz the number of grid points along the Z axis
*/
void getPMEParameters(double& alpha, int& nx, int& ny, int& nz) const;
/**
* Set the parameters to use for PME calculations. If alpha is 0 (the default), these parameters are
* ignored and instead their values are chosen based on the Ewald error tolerance.
*
* @param alpha the separation parameter
* @param nx the number of grid points along the X axis
* @param ny the number of grid points along the Y axis
* @param nz the number of grid points along the Z axis
*/
void setPMEParameters(double alpha, int nx, int ny, int nz);
/**
* Get the parameters being used for PME in a particular Context. Because some platforms have restrictions
* on the allowed grid sizes, the values that are actually used may be slightly different from those
* specified with setPMEParameters(), or the standard values calculated based on the Ewald error tolerance.
* See the manual for details.
*
* @param context the Context for which to get the parameters
* @param[out] alpha the separation parameter
* @param[out] nx the number of grid points along the X axis
* @param[out] ny the number of grid points along the Y axis
* @param[out] nz the number of grid points along the Z axis
*/
void getPMEParametersInContext(const Context& context, double& alpha, int& nx, int& ny, int& nz) const;
/**
* Add the nonbonded force parameters for a particle. This should be called once for each particle
* in the System. When it is called for the i'th time, it specifies the parameters for the i'th particle.
*
* @param charge the charge of the particle, measured in units of the proton charge
* @return the index of the particle that was added
*/
int addParticle(double charge);
/**
* Get the nonbonded force parameters for a particle.
*
* @param index the index of the particle for which to get parameters
* @param[out] charge the charge of the particle, measured in units of the proton charge
*/
void getParticleParameters(int index, double& charge) const;
/**
* Set the nonbonded force parameters for a particle.
*
* @param index the index of the particle for which to set parameters
* @param charge the charge of the particle, measured in units of the proton charge
*/
void setParticleParameters(int index, double charge);
/**
* Add an interaction to the list of exceptions that should be calculated
* differently from other interactions. If chargeProd is equal to 0, this
* will cause the interaction to be completely omitted from force and energy
* calculations.
*
* Cutoffs are never applied to exceptions. That is because they are
* primarily used for 1-4 interactions, which are really a type of bonded
* interaction and are parametrized together with the other bonded
* interactions.
*
* In many cases, you can use createExceptionsFromBonds() rather than adding
* each exception explicitly.
*
* @param particle1 the index of the first particle involved in the interaction
* @param particle2 the index of the second particle involved in the interaction
* @param chargeProd the scaled product of the atomic charges (i.e. the strength of the Coulomb interaction), measured in units of the proton charge squared
* @param replace determines the behavior if there is already an exception for the same two particles. If true, the existing one is replaced. If false,
* an exception is thrown.
* @return the index of the exception that was added
*/
int addException(int particle1, int particle2, double chargeProd, bool replace = false);
/**
* Get the force field parameters for an interaction that should be calculated differently from others.
*
* @param index the index of the interaction for which to get parameters
* @param[out] particle1 the index of the first particle involved in the interaction
* @param[out] particle2 the index of the second particle involved in the interaction
* @param[out] chargeProd the scaled product of the atomic charges (i.e. the strength of the Coulomb interaction), measured in units of the proton charge squared
*/
void getExceptionParameters(int index, int& particle1, int& particle2, double& chargeProd) const;
/**
* Set the force field parameters for an interaction that should be
* calculated differently from others. If chargeProd is equal to 0, this
* will cause the interaction to be completely omitted from force and energy
* calculations.
*
* Cutoffs are never applied to exceptions. That is because they are
* primarily used for 1-4 interactions, which are really a type of bonded
* interaction and are parametrized together with the other bonded
* interactions.
*
* @param index the index of the interaction for which to get parameters
* @param particle1 the index of the first particle involved in the interaction
* @param particle2 the index of the second particle involved in the interaction
* @param chargeProd the scaled product of the atomic charges (i.e. the strength of the Coulomb interaction), measured in units of the proton charge squared
*/
void setExceptionParameters(int index, int particle1, int particle2, double chargeProd);
/**
* Identify exceptions based on the molecular topology. Particles which are separated by one or two bonds are set
* to not interact at all, while pairs of particles separated by three bonds (known as "1-4 interactions") have
* their Coulomb interactions reduced by a fixed factor.
*
* @param bonds the set of bonds based on which to construct exceptions. Each element specifies the indices of
* two particles that are bonded to each other.
* @param coulomb14Scale pairs of particles separated by three bonds will have the strength of their Coulomb interaction
* multiplied by this factor
*/
void createExceptionsFromBonds(const std::vector<std::pair<int, int> >& bonds, double coulomb14Scale);
/**
* Update particle, exception, and electrode parameters in a Context to
* match those stored in this Force object. This method provides an
* efficient method to update certain parameters in an existing Context
* without needing to reinitialize it. Simply call setParticleParameters()
* and setExceptionParameters() to modify this object's parameters, then
* call updateParametersInContext() to copy them over to the Context.
*
* This method has several limitations. The only information it updates is
* the parameters of particles, exceptions, and electrodes, as well as the
* target total charge for the charge constraint. All other aspects of the
* Force (the constant potential method, the cutoff distance, etc.) are
* unaffected and can only be changed by reinitializing the Context.
* Furthermore, only the chargeProd value of an exception can be changed;
* the pair of particles involved in the exception cannot change.
* Similarly, for electrodes, the set of particles involved in the electrode
* cannot be updated with this method. Finally, this method cannot be used
* to add new particles, exceptions, or electrodes, only to change the
* parameters of existing ones.
*/
void updateParametersInContext(Context& context);
/**
* Returns whether or not this force makes use of periodic boundary
* conditions.
*
* @returns true if force uses PBC and false otherwise
*/
bool usesPeriodicBoundaryConditions() const {
return true;
}
/**
* Get whether periodic boundary conditions should be applied to exceptions.
* Usually this is not appropriate, because exceptions are normally used to
* represent bonded interactions (1-2, 1-3, and 1-4 pairs), but there are
* situations when it does make sense. For example, you may want to
* simulate an infinite chain where one end of a molecule is bonded to the
* opposite end of the next periodic copy. Note that cutoffs are never
* applied to exceptions, again because they are normally used to represent
* bonded interactions.
*/
bool getExceptionsUsePeriodicBoundaryConditions() const;
/**
* Set whether periodic boundary conditions should be applied to exceptions.
* Usually this is not appropriate, because exceptions are normally used to
* represent bonded interactions (1-2, 1-3, and 1-4 pairs), but there are
* situations when it does make sense. For example, you may want to
* simulate an infinite chain where one end of a molecule is bonded to the
* opposite end of the next periodic copy. Note that cutoffs are never
* applied to exceptions, again because they are normally used to represent
* bonded interactions.
*/
void setExceptionsUsePeriodicBoundaryConditions(bool periodic);
/**
* Get the method used for calculating electrode charges.
*/
ConstantPotentialMethod getConstantPotentialMethod() const;
/**
* Set the method used for calculating electrode charges.
*/
void setConstantPotentialMethod(ConstantPotentialMethod method);
/**
* Get whether or not to use a preconditioner when solving for electrode
* charges with the conjugate gradient method.
*/
bool getUsePreconditioner() const;
/**
* Set whether or not to use a preconditioner when solving for electrode
* charges with the conjugate gradient method.
*/
void setUsePreconditioner(bool use);
/**
* Get the tolerance, in units of kJ/mol per proton charge, used for the
* conjugate gradient method of calculating electrode charges. The method
* will iterate until the RMS error in the electrode potentials (gradient of
* the energy with respect to the electrode particle charges) no longer
* exceeds this tolerance.
*/
double getCGErrorTolerance() const;
/**
* Set the tolerance, in units of kJ/mol per proton charge, used for the
* conjugate gradient method of calculating electrode charges. The method
* will iterate until the RMS error in the electrode potentials (gradient of
* the energy with respect to the electrode particle charges) no longer
* exceeds this tolerance.
*/
void setCGErrorTolerance(double tol);
/**
* Get the number of electrodes that have been added.
*/
int getNumElectrodes() const {
return electrodes.size();
}
/**
* Add a new electrode from a set of particles. The specified particles
* will have their charges solved for such that they are held at the
* specified electric potential. Particles in a system may belong to at
* most a single electrode.
*
* @param electrodeParticles the indices of the particles to be included in this electrode
* @param potential the electric potential of the particles in this electrode, measured in kJ/mol per proton charge
* @param gaussianWidth the width of the Gaussian charge distribution assigned to particles in this electrode, measured in nm
* @param thomasFermiScale the square of the Thomas-Fermi length divided by the Thomas-Fermi Voronoi volume, measured in 1/nm
* @return the index of the electrode added
*/
int addElectrode(const std::set<int>& electrodeParticles, double potential, double gaussianWidth, double thomasFermiScale);
/**
* Get the parameters for an electrode.
*
* @param index the index of the electrode for which to get parameters
* @param[out] electrodeParticles the indices of the particles to be included in this electrode
* @param[out] potential the electric potential of the particles in this electrode, measured in kJ/mol per proton charge
* @param[out] gaussianWidth the width of the Gaussian charge distribution assigned to particles in this electrode, measured in nm
* @param[out] thomasFermiScale the square of the Thomas-Fermi length divided by the Thomas-Fermi Voronoi volume, measured in 1/nm
*/
void getElectrodeParameters(int index, std::set<int>& electrodeParticles, double& potential, double& gaussianWidth, double& thomasFermiScale) const;
/**
* Set the parameters for an electrode.
*
* @param index the index of the electrode for which to set parameters
* @param electrodeParticles the indices of the particles to be included in this electrode
* @param potential the electric potential of the particles in this electrode, measured in kJ/mol per proton charge
* @param gaussianWidth the width of the Gaussian charge distribution assigned to particles in this electrode, measured in nm
* @param thomasFermiScale the square of the Thomas-Fermi length divided by the Thomas-Fermi Voronoi volume, measured in 1/nm
*/
void setElectrodeParameters(int index, const std::set<int>& electrodeParticles, double potential, double gaussianWidth, double thomasFermiScale);
/**
* Get whether or not to apply a constraint to hold the total charge of the
* system constant.
*/
bool getUseChargeConstraint() const;
/**
* Set whether or not to apply a constraint to hold the total charge of the
* system constant.
*/
void setUseChargeConstraint(bool use);
/**
* Get the desired charge, in units of the proton charge, at which to hold
* the system if the total charge constraint is active. This includes the
* (fluctuating) charges on all electrode particles as well as the (fixed)
* charges on all non-electrode particles.
*/
double getChargeConstraintTarget() const;
/**
* Get the desired charge, in units of the proton charge, at which to hold
* the system if the total charge constraint is active. This includes the
* (fluctuating) charges on all electrode particles as well as the (fixed)
* charges on all non-electrode particles.
*/
void setChargeConstraintTarget(double charge);
/**
* Get the external electric field strength, measured in kJ/mol/nm per
* proton charge.
*/
void getExternalField(Vec3& field) const;
/**
* Set the external electric field strength, measured in kJ/mol/nm per
* proton charge.
*/
void setExternalField(const Vec3& field);
/**
* Get the charges on all particles: for non-electrode particles, these will
* simply be the fixed charges set, while for electrode particles, they will
* be the current charges solved for by the constant potential method.
*/
void getCharges(Context& context, std::vector<double>& charges) const;
protected:
ForceImpl* createImpl() const;
private:
class ParticleInfo;
class ExceptionInfo;
class ElectrodeInfo;
ConstantPotentialMethod constantPotentialMethod;
double cutoffDistance, ewaldErrorTol, alpha, cgErrorTol, chargeTarget;
Vec3 externalField;
bool exceptionsUsePeriodic, useChargeConstraint, usePreconditioner;
int nx, ny, nz;
void addExclusionsToSet(const std::vector<std::set<int> >& bonded12, std::set<int>& exclusions, int baseParticle, int fromParticle, int currentLevel) const;
std::vector<ParticleInfo> particles;
std::vector<ExceptionInfo> exceptions;
std::vector<ElectrodeInfo> electrodes;
std::map<std::pair<int, int>, int> exceptionMap;
mutable int numContexts, firstChangedParticle, lastChangedParticle, firstChangedException, lastChangedException, firstChangedElectrode, lastChangedElectrode;
};
/**
* This is an internal class used to record information about a particle.
* @private
*/
class ConstantPotentialForce::ParticleInfo {
public:
double charge;
ParticleInfo() {
charge = 0.0;
}
ParticleInfo(double charge) :
charge(charge) {
}
};
/**
* This is an internal class used to record information about an exception.
* @private
*/
class ConstantPotentialForce::ExceptionInfo {
public:
int particle1, particle2;
double chargeProd;
ExceptionInfo() {
particle1 = particle2 = -1;
chargeProd = 0.0;
}
ExceptionInfo(int particle1, int particle2, double chargeProd) :
particle1(particle1), particle2(particle2), chargeProd(chargeProd) {
}
};
/**
* This is an internal class used to record information about an electrode.
* @private
*/
class ConstantPotentialForce::ElectrodeInfo {
public:
std::set<int> particles;
double potential;
double gaussianWidth;
double thomasFermiScale;
ElectrodeInfo() {
potential = 0.0;
gaussianWidth = 0.0;
thomasFermiScale = 0.0;
}
ElectrodeInfo(const std::set<int>& particles, double potential, double gaussianWidth, double thomasFermiScale) :
particles(particles), potential(potential), gaussianWidth(gaussianWidth), thomasFermiScale(thomasFermiScale) {
}
};
} // namespace OpenMM
#endif /*OPENMM_CONSTANTPOTENTIALFORCE_H_*/
#ifndef OPENMM_CONSTANTPOTENTIALFORCEIMPL_H_
#define OPENMM_CONSTANTPOTENTIALFORCEIMPL_H_
/* -------------------------------------------------------------------------- *
* OpenMM *
* -------------------------------------------------------------------------- *
* This is part of the OpenMM molecular simulation toolkit originating from *
* Simbios, the NIH National Center for Physics-Based Simulation of *
* Biological Structures at Stanford, funded under the NIH Roadmap for *
* Medical Research, grant U54 GM072970. See https://simtk.org. *
* *
* Portions copyright (c) 2008-2025 Stanford University and the Authors. *
* Authors: Peter Eastman, Evan Pretti *
* Contributors: *
* *
* Permission is hereby granted, free of charge, to any person obtaining a *
* copy of this software and associated documentation files (the "Software"), *
* to deal in the Software without restriction, including without limitation *
* the rights to use, copy, modify, merge, publish, distribute, sublicense, *
* and/or sell copies of the Software, and to permit persons to whom the *
* Software is furnished to do so, subject to the following conditions: *
* *
* The above copyright notice and this permission notice shall be included in *
* all copies or substantial portions of the Software. *
* *
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR *
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, *
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL *
* THE AUTHORS, CONTRIBUTORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, *
* DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR *
* OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE *
* USE OR OTHER DEALINGS IN THE SOFTWARE. *
* -------------------------------------------------------------------------- */
#include "ForceImpl.h"
#include "openmm/ConstantPotentialForce.h"
#include "openmm/Kernel.h"
#include <utility>
#include <set>
#include <string>
namespace OpenMM {
class System;
/**
* This is the internal implementation of ConstantPotentialForce.
*/
class OPENMM_EXPORT ConstantPotentialForceImpl : public ForceImpl {
public:
ConstantPotentialForceImpl(const ConstantPotentialForce& owner);
~ConstantPotentialForceImpl();
void initialize(ContextImpl& context);
const ConstantPotentialForce& getOwner() const {
return owner;
}
void updateContextState(ContextImpl& context, bool& forcesInvalid) {
// This force field doesn't update the state directly.
}
double calcForcesAndEnergy(ContextImpl& context, bool includeForces, bool includeEnergy, int groups);
std::map<std::string, double> getDefaultParameters() {
return std::map<std::string, double>(); // This force field doesn't define any parameters.
}
std::vector<std::string> getKernelNames();
void updateParametersInContext(ContextImpl& context, int firstParticle, int lastParticle, int firstException, int lastException, int firstElectrode, int lastElectrode);
void getPMEParameters(double& alpha, int& nx, int& ny, int& nz) const;
void getCharges(ContextImpl& context, std::vector<double>& charges);
/**
* This is a utility routine that calculates the values to use for alpha and
* grid size when using Particle Mesh Ewald.
*/
static void calcPMEParameters(const System& system, const ConstantPotentialForce& force, double& alpha, int& xsize, int& ysize, int& zsize);
private:
const ConstantPotentialForce& owner;
Kernel kernel;
};
} // namespace OpenMM
#endif /*OPENMM_CONSTANTPOTENTIALFORCEIMPL_H_*/
...@@ -130,12 +130,49 @@ static int getNumProcessors() { ...@@ -130,12 +130,49 @@ static int getNumProcessors() {
* Get whether this is an x86 CPU that supports AVX. * Get whether this is an x86 CPU that supports AVX.
*/ */
static bool isAvxSupported() { static bool isAvxSupported() {
#ifdef __AVX__
int cpuInfo[4]; int cpuInfo[4];
cpuid(cpuInfo, 0); cpuid(cpuInfo, 0);
if (cpuInfo[0] >= 1) { if (cpuInfo[0] >= 1) {
cpuid(cpuInfo, 1); cpuid(cpuInfo, 1);
return ((cpuInfo[2] & ((int) 1 << 28)) != 0); return ((cpuInfo[2] & ((int) 1 << 28)) != 0);
} }
#endif /* __AVX__ */
return false;
}
/**
* Get whether this is an x86 CPU that supports AVX2.
*/
static bool isAvx2Supported() {
#ifdef __AVX2__
// Provide an alternative implementation of CPUID to support AVX2. On older
// non-Windows OSes the hardware.h support for CPUID doesn't set the CX register
// properly and gives the wrong answer when detecting AVX2 and beyond. On Windows
// the cpuid seems to work as expected so can be used.
#if !(defined(_WIN32) || defined(WIN32))
auto cpuid = [](int output[4], int functionnumber) {
int a, b, c, d;
__asm("cpuid" : "=a"(a),"=b"(b),"=c"(c),"=d"(d) : "a"(functionnumber), "c"(0) : );
output[0] = a;
output[1] = b;
output[2] = c;
output[3] = d;
};
#endif
int cpuInfo[4];
cpuid(cpuInfo, 0);
if (cpuInfo[0] >= 7) {
cpuInfo[2] = 0;
cpuid(cpuInfo, 7);
return ((cpuInfo[1] & ((int) 1 << 5)) != 0);
}
#endif /* __AVX2__ */
return false; return false;
} }
......
...@@ -163,6 +163,14 @@ public: ...@@ -163,6 +163,14 @@ public:
ivec8 operator|(ivec8 other) const { ivec8 operator|(ivec8 other) const {
return _mm256_castps_si256(_mm256_or_ps(_mm256_castsi256_ps(val), _mm256_castsi256_ps(other.val))); return _mm256_castps_si256(_mm256_or_ps(_mm256_castsi256_ps(val), _mm256_castsi256_ps(other.val)));
} }
ivec8 operator==(ivec8 other) const {
// _mm256_cmpeq_epi32() compiles to an AVX2-only instruction, so compare
// the lower and upper 128-bit 4-vectors of ints separately.
return _mm256_setr_m128i(lowerVec() == other.lowerVec(), upperVec() == other.upperVec());
}
ivec8 operator!=(ivec8 other) const {
return _mm256_castps_si256(_mm256_xor_ps(_mm256_castsi256_ps(*this == other), _mm256_castsi256_ps(_mm256_set1_epi32(0xFFFFFFFF))));
}
operator fvec8() const; operator fvec8() const;
}; };
......
...@@ -37,35 +37,6 @@ ...@@ -37,35 +37,6 @@
// This file defines classes and functions to simplify vectorizing code with AVX. // This file defines classes and functions to simplify vectorizing code with AVX.
bool isAvx2Supported() {
// Provide an alternative implementation of CPUID to support AVX2. On older
// non-Windows OSes the hardware.h support for CPUID doesn't set the CX register
// properly and gives the wrong answer when detecting AVX2 and beyond. On Windows
// the cpuid seems to work as expected so can be used.
#if !(defined(_WIN32) || defined(WIN32))
auto cpuid = [](int output[4], int functionnumber) {
int a, b, c, d;
__asm("cpuid" : "=a"(a),"=b"(b),"=c"(c),"=d"(d) : "a"(functionnumber), "c"(0) : );
output[0] = a;
output[1] = b;
output[2] = c;
output[3] = d;
};
#endif
int cpuInfo[4];
cpuid(cpuInfo, 0);
if (cpuInfo[0] >= 7) {
cpuInfo[2] = 0;
cpuid(cpuInfo, 7);
return ((cpuInfo[1] & ((int) 1 << 5)) != 0);
}
return false;
}
/** /**
* Derive from fvec8 so that default implementations of everything are provided, * Derive from fvec8 so that default implementations of everything are provided,
* but can be overriden with AVX2-specific variants where possible. * but can be overriden with AVX2-specific variants where possible.
......
/* -------------------------------------------------------------------------- *
* OpenMM *
* -------------------------------------------------------------------------- *
* This is part of the OpenMM molecular simulation toolkit originating from *
* Simbios, the NIH National Center for Physics-Based Simulation of *
* Biological Structures at Stanford, funded under the NIH Roadmap for *
* Medical Research, grant U54 GM072970. See https://simtk.org. *
* *
* Portions copyright (c) 2008-2025 Stanford University and the Authors. *
* Authors: Peter Eastman, Evan Pretti *
* Contributors: *
* *
* Permission is hereby granted, free of charge, to any person obtaining a *
* copy of this software and associated documentation files (the "Software"), *
* to deal in the Software without restriction, including without limitation *
* the rights to use, copy, modify, merge, publish, distribute, sublicense, *
* and/or sell copies of the Software, and to permit persons to whom the *
* Software is furnished to do so, subject to the following conditions: *
* *
* The above copyright notice and this permission notice shall be included in *
* all copies or substantial portions of the Software. *
* *
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR *
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, *
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL *
* THE AUTHORS, CONTRIBUTORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, *
* DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR *
* OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE *
* USE OR OTHER DEALINGS IN THE SOFTWARE. *
* -------------------------------------------------------------------------- */
#include "openmm/Force.h"
#include "openmm/OpenMMException.h"
#include "openmm/ConstantPotentialForce.h"
#include "openmm/internal/AssertionUtilities.h"
#include "openmm/internal/ConstantPotentialForceImpl.h"
#include <cmath>
#include <map>
#include <sstream>
#include <utility>
using namespace OpenMM;
using namespace std;
ConstantPotentialForce::ConstantPotentialForce() : constantPotentialMethod(CG),
cutoffDistance(1.0), ewaldErrorTol(5e-4), alpha(0.0), cgErrorTol(1e-4),
chargeTarget(0.0), exceptionsUsePeriodic(false),
useChargeConstraint(false), usePreconditioner(true),
nx(0), ny(0), nz(0), numContexts(0) {
}
double ConstantPotentialForce::getCutoffDistance() const {
return cutoffDistance;
}
void ConstantPotentialForce::setCutoffDistance(double distance) {
cutoffDistance = distance;
}
double ConstantPotentialForce::getEwaldErrorTolerance() const {
return ewaldErrorTol;
}
void ConstantPotentialForce::setEwaldErrorTolerance(double tol) {
ewaldErrorTol = tol;
}
void ConstantPotentialForce::getPMEParameters(double& alpha, int& nx, int& ny, int& nz) const {
alpha = this->alpha;
nx = this->nx;
ny = this->ny;
nz = this->nz;
}
void ConstantPotentialForce::setPMEParameters(double alpha, int nx, int ny, int nz) {
this->alpha = alpha;
this->nx = nx;
this->ny = ny;
this->nz = nz;
}
void ConstantPotentialForce::getPMEParametersInContext(const Context& context, double& alpha, int& nx, int& ny, int& nz) const {
dynamic_cast<const ConstantPotentialForceImpl&>(getImplInContext(context)).getPMEParameters(alpha, nx, ny, nz);
}
int ConstantPotentialForce::addParticle(double charge) {
particles.push_back(ParticleInfo(charge));
return particles.size()-1;
}
void ConstantPotentialForce::getParticleParameters(int index, double& charge) const {
ASSERT_VALID_INDEX(index, particles);
charge = particles[index].charge;
}
void ConstantPotentialForce::setParticleParameters(int index, double charge) {
ASSERT_VALID_INDEX(index, particles);
particles[index].charge = charge;
if (numContexts > 0) {
firstChangedParticle = min(index, firstChangedParticle);
lastChangedParticle = max(index, lastChangedParticle);
}
}
int ConstantPotentialForce::addException(int particle1, int particle2, double chargeProd, bool replace) {
map<pair<int, int>, int>::iterator iter = exceptionMap.find(pair<int, int>(particle1, particle2));
int newIndex;
if (iter == exceptionMap.end())
iter = exceptionMap.find(pair<int, int>(particle2, particle1));
if (iter != exceptionMap.end()) {
if (!replace) {
stringstream msg;
msg << "ConstantPotentialForce: There is already an exception for particles ";
msg << particle1;
msg << " and ";
msg << particle2;
throw OpenMMException(msg.str());
}
exceptions[iter->second] = ExceptionInfo(particle1, particle2, chargeProd);
newIndex = iter->second;
exceptionMap.erase(iter->first);
}
else {
exceptions.push_back(ExceptionInfo(particle1, particle2, chargeProd));
newIndex = exceptions.size()-1;
}
exceptionMap[pair<int, int>(particle1, particle2)] = newIndex;
return newIndex;
}
void ConstantPotentialForce::getExceptionParameters(int index, int& particle1, int& particle2, double& chargeProd) const {
ASSERT_VALID_INDEX(index, exceptions);
particle1 = exceptions[index].particle1;
particle2 = exceptions[index].particle2;
chargeProd = exceptions[index].chargeProd;
}
void ConstantPotentialForce::setExceptionParameters(int index, int particle1, int particle2, double chargeProd) {
ASSERT_VALID_INDEX(index, exceptions);
exceptions[index].particle1 = particle1;
exceptions[index].particle2 = particle2;
exceptions[index].chargeProd = chargeProd;
if (numContexts > 0) {
firstChangedException = min(index, firstChangedException);
lastChangedException = max(index, lastChangedException);
}}
ForceImpl* ConstantPotentialForce::createImpl() const {
if (numContexts == 0) {
// Begin tracking changes to particles and exceptions.
firstChangedParticle = particles.size();
lastChangedParticle = -1;
firstChangedException = exceptions.size();
lastChangedException = -1;
firstChangedElectrode = electrodes.size();
lastChangedElectrode = -1;
}
numContexts++;
return new ConstantPotentialForceImpl(*this);
}
void ConstantPotentialForce::createExceptionsFromBonds(const vector<pair<int, int> >& bonds, double coulomb14Scale) {
for (auto& bond : bonds)
if (bond.first < 0 || bond.second < 0 || bond.first >= particles.size() || bond.second >= particles.size())
throw OpenMMException("createExceptionsFromBonds: Illegal particle index in list of bonds");
// Find particles separated by 1, 2, or 3 bonds.
vector<set<int> > exclusions(particles.size());
vector<set<int> > bonded12(exclusions.size());
for (auto& bond : bonds) {
bonded12[bond.first].insert(bond.second);
bonded12[bond.second].insert(bond.first);
}
for (int i = 0; i < (int) exclusions.size(); ++i)
addExclusionsToSet(bonded12, exclusions[i], i, i, 2);
// Find particles separated by 1 or 2 bonds and create the exceptions.
for (int i = 0; i < (int) exclusions.size(); ++i) {
set<int> bonded13;
addExclusionsToSet(bonded12, bonded13, i, i, 1);
for (int j : exclusions[i]) {
if (j < i) {
if (bonded13.find(j) == bonded13.end()) {
// This is a 1-4 interaction.
const ParticleInfo& particle1 = particles[j];
const ParticleInfo& particle2 = particles[i];
const double chargeProd = coulomb14Scale*particle1.charge*particle2.charge;
addException(j, i, chargeProd);
}
else {
// This interaction should be completely excluded.
addException(j, i, 0.0);
}
}
}
}
}
void ConstantPotentialForce::addExclusionsToSet(const vector<set<int> >& bonded12, set<int>& exclusions, int baseParticle, int fromParticle, int currentLevel) const {
for (int i : bonded12[fromParticle]) {
if (i != baseParticle)
exclusions.insert(i);
if (currentLevel > 0)
addExclusionsToSet(bonded12, exclusions, baseParticle, i, currentLevel-1);
}
}
void ConstantPotentialForce::updateParametersInContext(Context& context) {
dynamic_cast<ConstantPotentialForceImpl&>(getImplInContext(context)).updateParametersInContext(getContextImpl(context),
firstChangedParticle, lastChangedParticle, firstChangedException, lastChangedException, firstChangedElectrode, lastChangedElectrode);
if (numContexts == 1) {
// We just updated the only existing context for this force, so we can
// reset the tracking of changed particles, exceptions, and electrodes.
firstChangedParticle = particles.size();
lastChangedParticle = -1;
firstChangedException = exceptions.size();
lastChangedException = -1;
firstChangedElectrode = electrodes.size();
lastChangedElectrode = -1;
}
}
bool ConstantPotentialForce::getExceptionsUsePeriodicBoundaryConditions() const {
return exceptionsUsePeriodic;
}
void ConstantPotentialForce::setExceptionsUsePeriodicBoundaryConditions(bool periodic) {
exceptionsUsePeriodic = periodic;
}
ConstantPotentialForce::ConstantPotentialMethod ConstantPotentialForce::getConstantPotentialMethod() const {
return constantPotentialMethod;
}
void ConstantPotentialForce::setConstantPotentialMethod(ConstantPotentialMethod method) {
if (method < 0 || method > 1) {
throw OpenMMException("ConstantPotentialForce: Illegal value for constant potential method");
}
constantPotentialMethod = method;
}
bool ConstantPotentialForce::getUsePreconditioner() const {
return usePreconditioner;
}
void ConstantPotentialForce::setUsePreconditioner(bool use) {
usePreconditioner = use;
}
double ConstantPotentialForce::getCGErrorTolerance() const {
return cgErrorTol;
}
void ConstantPotentialForce::setCGErrorTolerance(double tol) {
cgErrorTol = tol;
}
int ConstantPotentialForce::addElectrode(const std::set<int>& electrodeParticles, double potential, double gaussianWidth, double thomasFermiScale) {
electrodes.push_back(ElectrodeInfo(electrodeParticles, potential, gaussianWidth, thomasFermiScale));
return electrodes.size() - 1;
}
void ConstantPotentialForce::getElectrodeParameters(int index, std::set<int>& electrodeParticles, double& potential, double& gaussianWidth, double& thomasFermiScale) const {
ASSERT_VALID_INDEX(index, electrodes);
electrodeParticles = electrodes[index].particles;
potential = electrodes[index].potential;
gaussianWidth = electrodes[index].gaussianWidth;
thomasFermiScale = electrodes[index].thomasFermiScale;
}
void ConstantPotentialForce::setElectrodeParameters(int index, const std::set<int>& electrodeParticles, double potential, double gaussianWidth, double thomasFermiScale) {
ASSERT_VALID_INDEX(index, electrodes);
electrodes[index].particles = electrodeParticles;
electrodes[index].potential = potential;
electrodes[index].gaussianWidth = gaussianWidth;
electrodes[index].thomasFermiScale = thomasFermiScale;
if (numContexts > 0) {
firstChangedElectrode = min(index, firstChangedElectrode);
lastChangedElectrode = max(index, lastChangedElectrode);
}
}
bool ConstantPotentialForce::getUseChargeConstraint() const {
return useChargeConstraint;
}
void ConstantPotentialForce::setUseChargeConstraint(bool use) {
useChargeConstraint = use;
}
double ConstantPotentialForce::getChargeConstraintTarget() const {
return chargeTarget;
}
void ConstantPotentialForce::setChargeConstraintTarget(double charge) {
chargeTarget = charge;
}
void ConstantPotentialForce::getExternalField(Vec3& field) const {
field = externalField;
}
void ConstantPotentialForce::setExternalField(const Vec3& field) {
externalField = field;
}
void ConstantPotentialForce::getCharges(Context& context, std::vector<double>& charges) const {
dynamic_cast<ConstantPotentialForceImpl&>(getImplInContext(context)).getCharges(getContextImpl(context), charges);
}
/* -------------------------------------------------------------------------- *
* OpenMM *
* -------------------------------------------------------------------------- *
* This is part of the OpenMM molecular simulation toolkit originating from *
* Simbios, the NIH National Center for Physics-Based Simulation of *
* Biological Structures at Stanford, funded under the NIH Roadmap for *
* Medical Research, grant U54 GM072970. See https://simtk.org. *
* *
* Portions copyright (c) 2008-2025 Stanford University and the Authors. *
* Authors: Peter Eastman, Evan Pretti *
* Contributors: *
* *
* Permission is hereby granted, free of charge, to any person obtaining a *
* copy of this software and associated documentation files (the "Software"), *
* to deal in the Software without restriction, including without limitation *
* the rights to use, copy, modify, merge, publish, distribute, sublicense, *
* and/or sell copies of the Software, and to permit persons to whom the *
* Software is furnished to do so, subject to the following conditions: *
* *
* The above copyright notice and this permission notice shall be included in *
* all copies or substantial portions of the Software. *
* *
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR *
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, *
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL *
* THE AUTHORS, CONTRIBUTORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, *
* DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR *
* OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE *
* USE OR OTHER DEALINGS IN THE SOFTWARE. *
* -------------------------------------------------------------------------- */
#ifdef WIN32
#define _USE_MATH_DEFINES // Needed to get M_PI
#endif
#include "openmm/OpenMMException.h"
#include "openmm/internal/ContextImpl.h"
#include "openmm/internal/Messages.h"
#include "openmm/internal/ConstantPotentialForceImpl.h"
#include "openmm/kernels.h"
#include <cmath>
#include <map>
#include <sstream>
#include <algorithm>
using namespace OpenMM;
using namespace std;
ConstantPotentialForceImpl::ConstantPotentialForceImpl(const ConstantPotentialForce& owner) : owner(owner) {
forceGroup = owner.getForceGroup();
}
ConstantPotentialForceImpl::~ConstantPotentialForceImpl() {
}
void ConstantPotentialForceImpl::initialize(ContextImpl& context) {
kernel = context.getPlatform().createKernel(CalcConstantPotentialForceKernel::Name(), context);
const System& system = context.getSystem();
if (owner.getNumParticles() != system.getNumParticles())
throw OpenMMException("ConstantPotentialForce must have exactly as many particles as the System it belongs to.");
// Check for errors in the specification of exceptions.
vector<set<int> > exceptions(owner.getNumParticles());
for (int i = 0; i < owner.getNumExceptions(); i++) {
int particle[2];
double chargeProd;
owner.getExceptionParameters(i, particle[0], particle[1], chargeProd);
int minp = min(particle[0], particle[1]);
int maxp = max(particle[0], particle[1]);
for (int j = 0; j < 2; j++) {
if (particle[j] < 0 || particle[j] >= owner.getNumParticles()) {
stringstream msg;
msg << "ConstantPotentialForce: Illegal particle index for an exception: ";
msg << particle[j];
throw OpenMMException(msg.str());
}
}
if (exceptions[minp].count(maxp) > 0) {
stringstream msg;
msg << "ConstantPotentialForce: Multiple exceptions are specified for particles ";
msg << particle[0];
msg << " and ";
msg << particle[1];
throw OpenMMException(msg.str());
}
exceptions[minp].insert(maxp);
}
// Check for problems with the periodic box vectors.
Vec3 boxVectors[3];
system.getDefaultPeriodicBoxVectors(boxVectors[0], boxVectors[1], boxVectors[2]);
double cutoff = owner.getCutoffDistance();
if (cutoff > 0.5*boxVectors[0][0] || cutoff > 0.5*boxVectors[1][1] || cutoff > 0.5*boxVectors[2][2])
throw OpenMMException("ConstantPotentialForce: "+Messages::cutoffTooLarge);
// Check for errors in the specification of electrodes.
vector<int> electrodeIndexMap(owner.getNumParticles(), -1);
vector<int> electrodeIndices;
for (int electrode = 0; electrode < owner.getNumElectrodes(); electrode++) {
std::set<int> particles;
double potential;
double gaussianWidth;
double thomasFermiScale;
owner.getElectrodeParameters(electrode, particles, potential, gaussianWidth, thomasFermiScale);
for (int particle : particles) {
if (electrodeIndexMap[particle] != -1) {
stringstream msg;
msg << "ConstantPotentialForce: Particle " << particle << " belongs to electrodes " << electrodeIndexMap[particle] << " and " << electrode;
throw OpenMMException(msg.str());
}
electrodeIndexMap[particle] = electrode;
electrodeIndices.push_back(particle);
}
if (gaussianWidth < 0) {
stringstream msg;
msg << "ConstantPotentialForce: Electrode " << electrode << " has negative Gaussian width";
throw OpenMMException(msg.str());
}
if (thomasFermiScale < 0) {
stringstream msg;
msg << "ConstantPotentialForce: Electrode " << electrode << " has negative Thomas-Fermi scale";
throw OpenMMException(msg.str());
}
}
// Make sure no exceptions involve electrode particles.
for (int i = 0; i < owner.getNumExceptions(); i++) {
int particle1, particle2;
double chargeProd;
owner.getExceptionParameters(i, particle1, particle2, chargeProd);
int electrode1 = electrodeIndexMap[particle1];
int electrode2 = electrodeIndexMap[particle2];
if (electrode1 != -1) {
stringstream msg;
msg << "ConstantPotentialForce: Particle " << particle1 << " belongs to exception " << i << " and electrode " << electrode1;
throw OpenMMException(msg.str());
}
if (electrode2 != -1) {
stringstream msg;
msg << "ConstantPotentialForce: Particle " << particle2 << " belongs to exception " << i << " and electrode " << electrode2;
throw OpenMMException(msg.str());
}
}
// Make sure that we do not try to apply a constraint when zero electrode
// particles are present.
if (!electrodeIndices.size() && owner.getUseChargeConstraint()) {
throw OpenMMException("At least one electrode particle must exist to apply a total charge constraint");
}
// If we are precomputing a matrix, electrode particles must be frozen.
if (owner.getConstantPotentialMethod() == ConstantPotentialForce::ConstantPotentialMethod::Matrix) {
for (int i = 0; i < electrodeIndices.size(); i++) {
if (system.getParticleMass(electrodeIndices[i]) != 0.0) {
throw OpenMMException("Electrode particles must have zero mass for the matrix method");
}
}
}
kernel.getAs<CalcConstantPotentialForceKernel>().initialize(context.getSystem(), owner);
}
double ConstantPotentialForceImpl::calcForcesAndEnergy(ContextImpl& context, bool includeForces, bool includeEnergy, int groups) {
if ((groups & (1 << forceGroup)) != 0) {
return kernel.getAs<CalcConstantPotentialForceKernel>().execute(context, includeForces, includeEnergy);
}
return 0.0;
}
std::vector<std::string> ConstantPotentialForceImpl::getKernelNames() {
std::vector<std::string> names;
names.push_back(CalcConstantPotentialForceKernel::Name());
return names;
}
void ConstantPotentialForceImpl::updateParametersInContext(ContextImpl& context, int firstParticle, int lastParticle, int firstException, int lastException, int firstElectrode, int lastElectrode) {
kernel.getAs<CalcConstantPotentialForceKernel>().copyParametersToContext(context, owner, firstParticle, lastParticle, firstException, lastException, firstElectrode, lastElectrode);
context.systemChanged();
}
void ConstantPotentialForceImpl::getPMEParameters(double& alpha, int& nx, int& ny, int& nz) const {
kernel.getAs<CalcConstantPotentialForceKernel>().getPMEParameters(alpha, nx, ny, nz);
}
void ConstantPotentialForceImpl::getCharges(ContextImpl& context, std::vector<double>& charges) {
kernel.getAs<CalcConstantPotentialForceKernel>().getCharges(context, charges);
}
void ConstantPotentialForceImpl::calcPMEParameters(const System& system, const ConstantPotentialForce& force, double& alpha, int& xsize, int& ysize, int& zsize) {
force.getPMEParameters(alpha, xsize, ysize, zsize);
if (alpha == 0.0) {
Vec3 boxVectors[3];
system.getDefaultPeriodicBoxVectors(boxVectors[0], boxVectors[1], boxVectors[2]);
double tol = force.getEwaldErrorTolerance();
alpha = (1.0/force.getCutoffDistance())*std::sqrt(-log(2.0*tol));
xsize = max((int) ceil(2*alpha*boxVectors[0][0]/(3*pow(tol, 0.2))), 6);
ysize = max((int) ceil(2*alpha*boxVectors[1][1]/(3*pow(tol, 0.2))), 6);
zsize = max((int) ceil(2*alpha*boxVectors[2][2]/(3*pow(tol, 0.2))), 6);
}
}
...@@ -122,9 +122,37 @@ public: ...@@ -122,9 +122,37 @@ public:
} }
/** /**
* Copy the values in the array to a vector. * Copy the values in the array to a vector.
*
* @param data the vector to receive the values
* @param convert if true, automatic conversions between single and double
* precision will be performed as necessary
*/ */
template <class T> template <class T>
void download(std::vector<T>& data) const { void download(std::vector<T>& data, bool convert=false) const {
if (convert && sizeof(T) != getElementSize()) {
if (sizeof(T) == 2*getElementSize()) {
// Convert values from single to double precision.
std::vector<float> v(getElementSize()*getSize()/sizeof(float));
download(&v[0], true);
if (data.size() != getSize())
data.resize(getSize());
double* d = reinterpret_cast<double*>(&data[0]);
for (int i = 0; i < v.size(); i++)
d[i] = (double) v[i];
return;
}
if (2*sizeof(T) == getElementSize()) {
// Convert values from double to single precision.
std::vector<double> v(getElementSize()*getSize()/sizeof(double));
download(&v[0], true);
if (data.size() != getSize())
data.resize(getSize());
float* d = reinterpret_cast<float*>(&data[0]);
for (int i = 0; i < v.size(); i++)
d[i] = (float) v[i];
return;
}
}
if (sizeof(T) != getElementSize()) if (sizeof(T) != getElementSize())
throw OpenMMException("Error downloading array "+getName()+": The specified vector has the wrong element size"); throw OpenMMException("Error downloading array "+getName()+": The specified vector has the wrong element size");
if (data.size() != getSize()) if (data.size() != getSize())
......
#ifndef OPENMM_COMMONCALCCONSTANTPOTENTIALFORCE_H_
#define OPENMM_COMMONCALCCONSTANTPOTENTIALFORCE_H_
/* -------------------------------------------------------------------------- *
* OpenMM *
* -------------------------------------------------------------------------- *
* This is part of the OpenMM molecular simulation toolkit originating from *
* Simbios, the NIH National Center for Physics-Based Simulation of *
* Biological Structures at Stanford, funded under the NIH Roadmap for *
* Medical Research, grant U54 GM072970. See https://simtk.org. *
* *
* Portions copyright (c) 2008-2025 Stanford University and the Authors. *
* Authors: Evan Pretti *
* Contributors: *
* *
* This program is free software: you can redistribute it and/or modify *
* it under the terms of the GNU Lesser General Public License as published *
* by the Free Software Foundation, either version 3 of the License, or *
* (at your option) any later version. *
* *
* This program is distributed in the hope that it will be useful, *
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
* GNU Lesser General Public License for more details. *
* *
* You should have received a copy of the GNU Lesser General Public License *
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
* -------------------------------------------------------------------------- */
#include "openmm/kernels.h"
#include "openmm/common/ComputeArray.h"
#include "openmm/common/ComputeContext.h"
#include "openmm/common/ComputeEvent.h"
#include "openmm/common/ComputeQueue.h"
#include "openmm/common/ComputeSort.h"
#include "openmm/common/FFT3D.h"
#include <map>
#include <string>
#include <utility>
#include <vector>
namespace OpenMM {
class CommonCalcConstantPotentialForceKernel;
/**
* A generic charge solver for the constant potential method.
*/
class CommonConstantPotentialSolver {
public:
/**
* Creates a CommonConstantPotentialSolver.
*
* @param cc the compute context (should be selected)
* @param numParticles the number of particles
* @param numElectrodeParticles the number of electrode (fluctuating-charge) particles (should be positive)
* @param paddedProblemSize the padded number of electrode particles
*/
CommonConstantPotentialSolver(ComputeContext& cc, int numParticles, int numElectrodeParticles, int paddedProblemSize);
virtual ~CommonConstantPotentialSolver();
/**
* Sets up solver kernels. Should be called by derived classes if
* overridden.
*
* @param kernel main constant potential kernel
*/
virtual void compileKernels(CommonCalcConstantPotentialForceKernel& kernel);
/**
* Mark precomputed data stored by the solver as invalid due to a change in
* electrode parameters.
*/
void invalidate();
/**
* Mark any existing solution stored by the solver as invalid due to a
* change in system parameters.
*/
void discardSavedSolution();
/**
* Solves for charges if needed.
*
* @param kernel main constant potential kernel
*/
void solve(CommonCalcConstantPotentialForceKernel& kernel);
/**
* Solves for charges.
*
* @param kernel main constant potential kernel
*/
virtual void solveImpl(CommonCalcConstantPotentialForceKernel& kernel) = 0;
/**
* Retrieves a list of arrays of initial guess charges to be reordered.
*
* @param arrays Arrays to be reordered.
*/
virtual void getGuessChargeArrays(std::vector<ComputeArray*>& arrays);
protected:
int numParticles, numElectrodeParticles, paddedProblemSize;
bool valid, hasSavedSolution;
Vec3 savedBoxVectors[3];
ComputeArray savedPositions;
ComputeArray savedCharges;
ComputeArray checkSavedPositionsKernelResult;
ComputeKernel checkSavedPositionsKernel;
};
/**
* A constant potential solver using direct inversion of the Coulomb matrix.
* Suitable only when electrode particle positions are fixed.
*/
class CommonConstantPotentialMatrixSolver : public CommonConstantPotentialSolver {
private:
Vec3 boxVectors[3];
ComputeArray electrodePosData;
ComputeArray capacitance;
ComputeArray constraintVector;
ComputeArray checkSavedElectrodePositionsKernelResult;
ComputeKernel checkSavedElectrodePositionsKernel;
ComputeKernel saveElectrodePositionsKernel;
ComputeKernel solveKernel;
public:
/**
* Creates a CommonConstantPotentialMatrixSolver.
*
* @param cc the compute context (should be selected)
* @param numParticles the number of particles
* @param numElectrodeParticles the number of electrode (fluctuating-charge) particles
* @param paddedProblemSize the padded number of electrode particles
*/
CommonConstantPotentialMatrixSolver(ComputeContext& cc, int numParticles, int numElectrodeParticles, int paddedProblemSize);
/**
* Sets up solver kernels.
*
* @param kernel main constant potential kernel
*/
void compileKernels(CommonCalcConstantPotentialForceKernel& kernel);
/**
* Solves for charges.
*
* @param kernel main constant potential kernel
*/
void solveImpl(CommonCalcConstantPotentialForceKernel& kernel);
private:
/**
* Solves for charges.
*
* @param kernel main constant potential kernel
*/
void ensureValid(CommonCalcConstantPotentialForceKernel& kernel);
};
/**
* A constant potential solver using direct inversion of the Coulomb matrix.
* Suitable only when electrode particle positions are fixed.
*/
class CommonConstantPotentialCGSolver : public CommonConstantPotentialSolver {
private:
Vec3 boxVectors[3];
bool precondRequested, precondActivated;
int threadBlockCount, threadBlockSize;
ComputeArray precondVector;
ComputeArray q;
ComputeArray grad;
ComputeArray projGrad;
ComputeArray precGrad;
ComputeArray qStep;
ComputeArray gradStep;
ComputeArray grad0;
ComputeArray qLast;
ComputeArray blockSums1;
ComputeArray blockSums2;
ComputeArray convergedResult;
ComputeKernel solveInitializeStep1Kernel;
ComputeKernel solveInitializeStep2Kernel;
ComputeKernel solveInitializeStep3Kernel;
ComputeKernel solveLoopStep1Kernel;
ComputeKernel solveLoopStep2Kernel;
ComputeKernel solveLoopStep3Kernel;
ComputeKernel solveLoopStep4Kernel;
ComputeKernel solveLoopStep5Kernel;
ComputeEvent convergedDownloadStartEvent;
ComputeEvent convergedDownloadFinishEvent;
ComputeQueue convergedDownloadQueue;
public:
/**
* Creates a CommonConstantPotentialCGSolver.
*
* @param cc the compute context (should be selected)
* @param numParticles the number of particles
* @param numElectrodeParticles the number of electrode (fluctuating-charge) particles
* @param paddedProblemSize the padded number of electrode particles
* @param precond whether or not to use a preconditioner
*/
CommonConstantPotentialCGSolver(ComputeContext& cc, int numParticles, int numElectrodeParticles, int paddedProblemSize, bool precond);
/**
* Sets up solver kernels.
*
* @param kernel main constant potential kernel
*/
virtual void compileKernels(CommonCalcConstantPotentialForceKernel& kernel);
/**
* Solves for charges.
*
* @param kernel main constant potential kernel
*/
void solveImpl(CommonCalcConstantPotentialForceKernel& kernel);
/**
* Retrieves a list of arrays of initial guess charges to be reordered.
*
* @param arrays Arrays to be reordered.
*/
virtual void getGuessChargeArrays(std::vector<ComputeArray*>& arrays);
private:
/**
* Solves for charges.
*
* @param kernel main constant potential kernel
*/
void ensureValid(CommonCalcConstantPotentialForceKernel& kernel);
};
/**
* This kernel is invoked by ConstantPotentialForce to calculate the forces acting on the system.
*/
class CommonCalcConstantPotentialForceKernel : public CalcConstantPotentialForceKernel {
friend class CommonConstantPotentialSolver;
friend class CommonConstantPotentialMatrixSolver;
friend class CommonConstantPotentialCGSolver;
public:
CommonCalcConstantPotentialForceKernel(std::string name, const Platform& platform, ComputeContext& cc, const System& system) : CalcConstantPotentialForceKernel(name, platform),
cc(cc), info(NULL), solver(NULL), hasInitializedKernel(false) {
}
~CommonCalcConstantPotentialForceKernel();
/**
* Initialize the kernel. Subclasses should call this from their initialize() method.
*
* @param system the System this kernel will be applied to
* @param force the ConstantPotentialForce this kernel will be used for
* @param deviceIsCpu whether the device this calculation is running on is a CPU
* @param useFixedPointChargeSpreading whether PME charge spreading should be done in fixed point or floating point
*/
void commonInitialize(const System& system, const ConstantPotentialForce& force, bool deviceIsCpu, bool useFixedPointChargeSpreading);
/**
* Execute the kernel to calculate the forces and/or energy.
*
* @param context the context in which to execute this kernel
* @param includeForces true if forces should be calculated
* @param includeEnergy true if the energy should be calculated
* @return the potential energy due to the force
*/
double execute(ContextImpl& context, bool includeForces, bool includeEnergy);
/**
* Copy changed parameters over to a context.
*
* @param context the context to copy parameters to
* @param force the ConstantPotentialForce to copy the parameters from
* @param firstParticle the index of the first particle whose parameters might have changed
* @param lastParticle the index of the last particle whose parameters might have changed
* @param firstException the index of the first exception whose parameters might have changed
* @param lastException the index of the last exception whose parameters might have changed
* @param firstElectrode the index of the first electrode whose parameters might have changed
* @param lastElectrode the index of the last electrode whose parameters might have changed
*/
void copyParametersToContext(ContextImpl& context, const ConstantPotentialForce& force, int firstParticle, int lastParticle, int firstException, int lastException, int firstElectrode, int lastElectrode);
/**
* Get the parameters being used for PME.
*
* @param alpha the separation parameter
* @param nx the number of grid points along the X axis
* @param ny the number of grid points along the Y axis
* @param nz the number of grid points along the Z axis
*/
void getPMEParameters(double& alpha, int& nx, int& ny, int& nz) const;
/**
* Get the charges on all particles.
*
* @param context the context to copy parameters to
* @param[out] charges a vector to populate with particle charges
*/
void getCharges(ContextImpl& context, std::vector<double>& charges);
private:
void ensureInitialized(ContextImpl& context);
double doEnergyForces(bool includeForces, bool includeEnergy);
void initDoDerivatives();
void doDerivatives(bool init = true);
void pmeSetup();
void pmeCompileKernels();
void initPmeExecute();
void pmeExecute(bool includeEnergy, bool includeForces, bool includeChargeDerivatives, bool init = true);
void setKernelInputs(bool includeEnergy, bool includeForces);
class SortTrait : public ComputeSortImpl::SortTrait {
int getDataSize() const {return 8;}
int getKeySize() const {return 4;}
const char* getDataType() const {return "int2";}
const char* getKeyType() const {return "int";}
const char* getMinKey() const {return "(-2147483647-1)";}
const char* getMaxKey() const {return "2147483647";}
const char* getMaxValue() const {return "make_int2(2147483647, 2147483647)";}
const char* getSortKey() const {return "value.y";}
};
class ForceInfo;
class ReorderListener;
ComputeContext& cc;
ForceInfo* info;
CommonConstantPotentialSolver* solver;
bool hasInitializedKernel, hasElectrodes, mustUpdateNonElectrodeCharges, mustUpdateElectrodeCharges, pmeShouldSort;
int numParticles, numElectrodeParticles, numElectrodes, chunkSize, chunkCount, paddedProblemSize;
ComputeArray charges;
ComputeArray nonElectrodeCharges;
ComputeArray electrodeCharges;
ComputeArray chargeDerivatives;
ComputeArray chargeDerivativesFixed;
ComputeArray totalChargeBuffer;
ComputeArray sysToElec;
ComputeArray elecToSys;
ComputeArray sysElec;
ComputeArray elecElec;
ComputeArray electrodeParams;
ComputeArray exclusionScales;
ComputeArray exceptionScales;
ComputeArray pmeGrid1;
ComputeArray pmeGrid2;
ComputeArray pmeBsplineModuliX;
ComputeArray pmeBsplineModuliY;
ComputeArray pmeBsplineModuliZ;
ComputeArray pmeAtomGridIndex;
ComputeArray posCellOffsets;
ComputeSort sort;
FFT3D fft;
ComputeKernel updateNonElectrodeChargesKernel;
ComputeKernel updateElectrodeChargesKernel;
ComputeKernel getTotalChargeKernel;
ComputeKernel evaluateSelfEnergyForcesKernel;
ComputeKernel evaluateDirectDerivativesKernel;
ComputeKernel finishDerivativesKernel;
ComputeKernel pmeGridIndexKernel;
ComputeKernel pmeSpreadChargeKernel;
ComputeKernel pmeFinishSpreadChargeKernel;
ComputeKernel pmeConvolutionKernel;
ComputeKernel pmeEvalEnergyKernel;
ComputeKernel pmeInterpolateForceKernel;
ComputeKernel pmeInterpolateChargeDerivativesKernel;
std::vector<mm_int4> hostPosCellOffsets;
std::vector<double> setCharges, hostNonElectrodeCharges, hostElectrodeCharges;
std::vector<std::pair<int, int> > exclusions;
std::vector<int> exceptions;
std::vector<int> hostSysToElec, hostElecToSys, hostSysElec, hostElecElec;
std::vector<mm_double4> hostElectrodeParams;
std::map<int, int> exceptionIndex;
std::map<std::string, std::string> pmeDefines;
ConstantPotentialForce::ConstantPotentialMethod method;
Vec3 boxVectors[3], externalField;
double cutoff, ewaldAlpha, chargeTarget, cgErrorTol;
int maxThreadBlockSize, gridSizeX, gridSizeY, gridSizeZ;
bool deviceIsCpu, useFixedPointChargeSpreading, usePosqCharges, useChargeConstraint;
int forceGroup;
static const int PmeOrder = 5;
static const double SELF_ALPHA_SCALE, SELF_ETA_SCALE, SELF_TF_SCALE, PLASMA_SCALE;
};
} // namespace OpenMM
#endif /* OPENMM_COMMONCALCCONSTANTPOTENTIALFORCE_H_ */
...@@ -107,8 +107,8 @@ public: ...@@ -107,8 +107,8 @@ public:
* Copy the values in the Buffer to a vector. * Copy the values in the Buffer to a vector.
*/ */
template <class T> template <class T>
void download(std::vector<T>& data) const { void download(std::vector<T>& data, bool convert=false) const {
ArrayInterface::download(data); ArrayInterface::download(data, convert);
} }
/** /**
* Copy the values from host memory to the array. * Copy the values from host memory to the array.
......
/* -------------------------------------------------------------------------- *
* OpenMM *
* -------------------------------------------------------------------------- *
* This is part of the OpenMM molecular simulation toolkit originating from *
* Simbios, the NIH National Center for Physics-Based Simulation of *
* Biological Structures at Stanford, funded under the NIH Roadmap for *
* Medical Research, grant U54 GM072970. See https://simtk.org. *
* *
* Portions copyright (c) 2008-2025 Stanford University and the Authors. *
* Authors: Peter Eastman, Evan Pretti *
* Contributors: *
* *
* This program is free software: you can redistribute it and/or modify *
* it under the terms of the GNU Lesser General Public License as published *
* by the Free Software Foundation, either version 3 of the License, or *
* (at your option) any later version. *
* *
* This program is distributed in the hope that it will be useful, *
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
* GNU Lesser General Public License for more details. *
* *
* You should have received a copy of the GNU Lesser General Public License *
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
* -------------------------------------------------------------------------- */
#include "openmm/Context.h"
#include "openmm/internal/ConstantPotentialForceImpl.h"
#include "openmm/common/BondedUtilities.h"
#include "openmm/common/CommonCalcConstantPotentialForce.h"
#include "openmm/common/CommonKernelUtilities.h"
#include "openmm/common/ContextSelector.h"
#include "openmm/common/NonbondedUtilities.h"
#include "CommonKernelSources.h"
#include "SimTKOpenMMRealType.h"
#include <algorithm>
#include <assert.h>
#include <cmath>
#include <iterator>
#include <set>
#include "tnt_array2d.h"
#include "jama_cholesky.h"
using namespace OpenMM;
using namespace std;
class CommonCalcConstantPotentialForceKernel::ForceInfo : public ComputeForceInfo {
public:
ForceInfo(const ConstantPotentialForce& force, const vector<int>& sysElec, const vector<mm_double4>& electrodeParams) : force(force), sysElec(sysElec), electrodeParams(electrodeParams) {
}
bool areParticlesIdentical(int particle1, int particle2) {
int elec1 = sysElec[particle1];
int elec2 = sysElec[particle2];
if (elec1 == -1 && elec2 == -1) {
// Both particles are non-electrode; check their fixed charges.
double charge1, charge2;
force.getParticleParameters(particle1, charge1);
force.getParticleParameters(particle2, charge2);
return (charge1 == charge2);
}
if (elec1 == -1 || elec2 == -1) {
// One particle is non-electrode (but not both, handled above).
return false;
}
// Both particles are electrode particles.
mm_double4 electrodeParams1 = electrodeParams[elec1];
mm_double4 electrodeParams2 = electrodeParams[elec2];
return electrodeParams1.x == electrodeParams2.x && electrodeParams1.y == electrodeParams2.y && electrodeParams1.z == electrodeParams2.z;
}
int getNumParticleGroups() {
return force.getNumExceptions();
}
void getParticlesInGroup(int index, vector<int>& particles) {
int particle1, particle2;
double chargeProd;
force.getExceptionParameters(index, particle1, particle2, chargeProd);
particles.resize(2);
particles[0] = particle1;
particles[1] = particle2;
}
bool areGroupsIdentical(int group1, int group2) {
int particle1, particle2;
double chargeProd1, chargeProd2;
force.getExceptionParameters(group1, particle1, particle2, chargeProd1);
force.getExceptionParameters(group2, particle1, particle2, chargeProd2);
return (chargeProd1 == chargeProd2);
}
private:
const ConstantPotentialForce& force;
const vector<int>& sysElec;
const vector<mm_double4>& electrodeParams;
};
class CommonCalcConstantPotentialForceKernel::ReorderListener : public ComputeContext::ReorderListener {
public:
ReorderListener(ComputeContext& cc, int numElectrodeParticles, int paddedProblemSize, const vector<int>& sysToElec, const vector<int>& elecToSys) :
cc(cc), numElectrodeParticles(numElectrodeParticles), paddedProblemSize(paddedProblemSize), sysToElec(sysToElec), elecToSys(elecToSys) {
numParticles = cc.getNumAtoms();
lastOrder.assign(cc.getAtomIndex().begin(), cc.getAtomIndex().end());
}
void addChargeArray(ComputeArray& chargeArray) {
chargeArrays.push_back(&chargeArray);
}
void execute() {
// Reorder guess charges.
if (chargeArrays.empty()) {
return;
}
const vector<int>& order = cc.getAtomIndex();
for (int index = 0; index < chargeArrays.size(); index++) {
ComputeArray* chargeArray = chargeArrays[index];
vector<double> charges(paddedProblemSize), swap(numElectrodeParticles);
chargeArray->download(charges, true);
for (int ii = 0; ii < numElectrodeParticles; ii++) {
swap[sysToElec[lastOrder[elecToSys[ii]]]] = charges[ii];
}
for (int ii = 0; ii < numElectrodeParticles; ii++) {
charges[ii] = swap[sysToElec[order[elecToSys[ii]]]];
}
chargeArray->upload(charges, true);
}
lastOrder.assign(order.begin(), order.end());
}
private:
ComputeContext& cc;
int numParticles, numElectrodeParticles, paddedProblemSize;
const vector<int>& sysToElec;
const vector<int>& elecToSys;
vector<int> lastOrder;
vector<ComputeArray*> chargeArrays;
};
CommonConstantPotentialSolver::CommonConstantPotentialSolver(ComputeContext& cc, int numParticles, int numElectrodeParticles, int paddedProblemSize) :
numParticles(numParticles),
numElectrodeParticles(numElectrodeParticles),
paddedProblemSize(paddedProblemSize),
valid(false),
hasSavedSolution(false)
{
savedPositions.initialize(cc, cc.getPosq().getSize(), cc.getPosq().getElementSize(), "savedPositions");
savedCharges.initialize(cc, paddedProblemSize, cc.getUseDoublePrecision() ? sizeof(double) : sizeof(float), "savedCharges");
checkSavedPositionsKernelResult.initialize<int>(cc, 1, "checkSavedPositionsKernelResult");
}
CommonConstantPotentialSolver::~CommonConstantPotentialSolver() {
}
void CommonConstantPotentialSolver::compileKernels(CommonCalcConstantPotentialForceKernel& kernel) {
map<string, string> defines;
defines["NUM_PARTICLES"] = kernel.cc.intToString(numParticles);
ComputeProgram program = kernel.cc.compileProgram(CommonKernelSources::constantPotentialSolver, defines);
checkSavedPositionsKernel = program->createKernel("checkSavedPositions");
checkSavedPositionsKernel->addArg(kernel.cc.getPosq());
checkSavedPositionsKernel->addArg(savedPositions);
checkSavedPositionsKernel->addArg(checkSavedPositionsKernelResult);
}
void CommonConstantPotentialSolver::invalidate() {
valid = false;
hasSavedSolution = false;
}
void CommonConstantPotentialSolver::discardSavedSolution() {
hasSavedSolution = false;
}
void CommonConstantPotentialSolver::solve(CommonCalcConstantPotentialForceKernel& kernel) {
// If box vectors or positions have not changed, and there is a solution
// already computed, we can simply reload it instead of solving again.
if (hasSavedSolution) {
if (savedBoxVectors[0] != kernel.boxVectors[0] || savedBoxVectors[1] != kernel.boxVectors[1] || savedBoxVectors[2] != kernel.boxVectors[2]) {
hasSavedSolution = false;
}
}
if (hasSavedSolution) {
kernel.cc.clearBuffer(checkSavedPositionsKernelResult);
checkSavedPositionsKernel->execute(numParticles);
int result;
checkSavedPositionsKernelResult.download(&result);
if (result) {
hasSavedSolution = false;
}
}
if (hasSavedSolution) {
savedCharges.copyTo(kernel.electrodeCharges);
kernel.mustUpdateElectrodeCharges = true;
return;
}
solveImpl(kernel);
hasSavedSolution = true;
savedBoxVectors[0] = kernel.boxVectors[0];
savedBoxVectors[1] = kernel.boxVectors[1];
savedBoxVectors[2] = kernel.boxVectors[2];
kernel.cc.getPosq().copyTo(savedPositions);
kernel.electrodeCharges.copyTo(savedCharges);
}
void CommonConstantPotentialSolver::getGuessChargeArrays(vector<ComputeArray*>& arrays) {
arrays.clear();
}
CommonConstantPotentialMatrixSolver::CommonConstantPotentialMatrixSolver(ComputeContext& cc, int numParticles, int numElectrodeParticles, int paddedProblemSize) : CommonConstantPotentialSolver(cc, numParticles, numElectrodeParticles, paddedProblemSize) {
int elementSize = cc.getUseDoublePrecision() ? sizeof(double) : sizeof(float);
electrodePosData.initialize(cc, numElectrodeParticles, cc.getPosq().getElementSize(), "electrodePosData");
capacitance.initialize(cc, (size_t) paddedProblemSize * paddedProblemSize, elementSize, "capacitance");
constraintVector.initialize(cc, numElectrodeParticles, elementSize, "constraintVector");
checkSavedElectrodePositionsKernelResult.initialize<int>(cc, 1, "checkSavedElectrodePositionsKernelResult");
}
void CommonConstantPotentialMatrixSolver::compileKernels(CommonCalcConstantPotentialForceKernel& kernel) {
CommonConstantPotentialSolver::compileKernels(kernel);
map<string, string> defines;
defines["NUM_PARTICLES"] = kernel.cc.intToString(numParticles);
defines["NUM_ELECTRODE_PARTICLES"] = kernel.cc.intToString(numElectrodeParticles);
defines["THREAD_BLOCK_SIZE"] = kernel.cc.intToString(kernel.maxThreadBlockSize);
defines["CHUNK_SIZE"] = kernel.cc.intToString(kernel.chunkSize);
defines["CHUNK_COUNT"] = kernel.cc.intToString(kernel.chunkCount);
defines["PADDED_PROBLEM_SIZE"] = kernel.cc.intToString(kernel.paddedProblemSize);
if (kernel.useChargeConstraint) {
defines["USE_CHARGE_CONSTRAINT"] = "1";
}
ComputeProgram program = kernel.cc.compileProgram(CommonKernelSources::constantPotentialMatrixSolver, defines);
checkSavedElectrodePositionsKernel = program->createKernel("checkSavedElectrodePositions");
checkSavedElectrodePositionsKernel->addArg(kernel.cc.getPosq());
checkSavedElectrodePositionsKernel->addArg(electrodePosData);
checkSavedElectrodePositionsKernel->addArg(kernel.elecToSys);
checkSavedElectrodePositionsKernel->addArg(checkSavedElectrodePositionsKernelResult);
saveElectrodePositionsKernel = program->createKernel("saveElectrodePositions");
saveElectrodePositionsKernel->addArg(kernel.cc.getPosq());
saveElectrodePositionsKernel->addArg(electrodePosData);
saveElectrodePositionsKernel->addArg(kernel.elecToSys);
solveKernel = program->createKernel("solve");
solveKernel->addArg(kernel.electrodeCharges);
solveKernel->addArg(kernel.chargeDerivatives);
solveKernel->addArg(capacitance);
if (kernel.useChargeConstraint) {
solveKernel->addArg(constraintVector);
solveKernel->addArg(); // chargeTarget
}
}
void CommonConstantPotentialMatrixSolver::solveImpl(CommonCalcConstantPotentialForceKernel& kernel) {
ensureValid(kernel);
// Zero electrode charges and get derivatives at zero charge.
kernel.cc.clearBuffer(kernel.electrodeCharges);
kernel.mustUpdateElectrodeCharges = true;
kernel.doDerivatives();
// Solve for electrode charges directly using capacitance matrix and
// calculated derivatives.
if (kernel.useChargeConstraint) {
if (kernel.cc.getUseDoublePrecision()) {
solveKernel->setArg(4, kernel.chargeTarget);
}
else {
solveKernel->setArg(4, (float) kernel.chargeTarget);
}
}
solveKernel->execute(kernel.maxThreadBlockSize, kernel.maxThreadBlockSize);
kernel.mustUpdateElectrodeCharges = true;
}
void CommonConstantPotentialMatrixSolver::ensureValid(CommonCalcConstantPotentialForceKernel& kernel) {
// Initializes or updates the precomputed capacitance matrix if this is its
// first use or electrode parameters have changed since its initialization.
// Check for changes to box vectors or electrode positions that might
// invalidate a matrix that is currently marked valid.
if (valid) {
if (boxVectors[0] != kernel.boxVectors[0] || boxVectors[1] != kernel.boxVectors[1] || boxVectors[2] != kernel.boxVectors[2]) {
valid = false;
}
}
if (valid) {
kernel.cc.clearBuffer(checkSavedElectrodePositionsKernelResult);
checkSavedElectrodePositionsKernel->execute(numElectrodeParticles);
int result;
checkSavedElectrodePositionsKernelResult.download(&result);
if (result) {
valid = false;
}
}
if (valid) {
return;
}
// Store the current box vectors and electrode positions before updating the
// capacitance matrix.
valid = true;
boxVectors[0] = kernel.boxVectors[0];
boxVectors[1] = kernel.boxVectors[1];
boxVectors[2] = kernel.boxVectors[2];
saveElectrodePositionsKernel->execute(numElectrodeParticles);
TNT::Array2D<double> A(paddedProblemSize, paddedProblemSize);
vector<double> dUdQ0(numElectrodeParticles);
vector<double> dUdQ(numElectrodeParticles);
// Get derivatives when all electrode charges are zeroed.
kernel.cc.clearBuffer(kernel.electrodeCharges);
kernel.mustUpdateElectrodeCharges = true;
kernel.doDerivatives();
kernel.chargeDerivatives.download(dUdQ0, true);
vector<double> electrodeCharges(paddedProblemSize);
for (int ii = 0; ii < numElectrodeParticles; ii++) {
// Get derivatives when one electrode charge is set.
electrodeCharges[ii] = 1.0;
kernel.electrodeCharges.upload(electrodeCharges, true);
kernel.mustUpdateElectrodeCharges = true;
kernel.doDerivatives();
kernel.chargeDerivatives.download(dUdQ, true);
electrodeCharges[ii] = 0.0;
// Set matrix elements, subtracting zero charge derivatives so that the
// matrix will end up being the (charge-independent) Hessian.
for (int jj = 0; jj < ii; jj++) {
A[ii][jj] = A[jj][ii] = dUdQ[jj] - dUdQ0[jj];
}
A[ii][ii] = dUdQ[ii] - dUdQ0[ii];
for (int jj = numElectrodeParticles; jj < paddedProblemSize; jj++) {
A[ii][jj] = A[jj][ii] = 0.0;
}
}
for (int ii = numElectrodeParticles; ii < paddedProblemSize; ii++) {
for (int jj = numElectrodeParticles; jj < paddedProblemSize; jj++) {
A[ii][jj] = ii == jj ? 1.0 : 0.0;
}
}
// Compute Cholesky decomposition representation of the inverse.
JAMA::Cholesky<double> choleskyInverse(A);
if (!choleskyInverse.is_spd()) {
throw OpenMMException("Electrode matrix not positive definite");
}
// Load the results of the Cholesky decomposition into a buffer.
TNT::Array2D<double> choleskyLower = choleskyInverse.getL();
vector<double> hostCapacitance(capacitance.getSize());
size_t index = 0;
for (int ii = 0; ii < paddedProblemSize; ii++) {
double scale = 1.0 / choleskyLower[ii][ii];
// Load the lower triangle.
for (int jj = 0; jj < ii; jj++) {
hostCapacitance[index++] = choleskyLower[ii][jj] * scale;
}
// Load the reciprocal of the diagonal element.
hostCapacitance[index++] = scale;
// Load the transpose of the lower triangle into the upper triangle.
for (int jj = ii + 1; jj < paddedProblemSize; jj++) {
hostCapacitance[index++] = choleskyLower[jj][ii] * scale;
}
}
capacitance.upload(hostCapacitance, true);
// Precompute the appropriate scaling vector to enforce constant total
// charge if requested. The vector is parallel to one obtained by solving
// Aq = b for all q_i = 1 (ensuring the constrained charges will actually be
// the correct constrained minimum of the quadratic form for the energy),
// and is scaled so that adding it to a vector of charges increases the
// total charge by 1 (making it easy to calculate the necessary offset).
if (kernel.useChargeConstraint) {
TNT::Array1D<double> solution = choleskyInverse.solve(TNT::Array1D<double>(paddedProblemSize, 1.0));
vector<double> hostConstraintVector(static_cast<double*>(solution), static_cast<double*>(solution) + numElectrodeParticles);
double constraintScaleInv = 0.0;
for (int ii = 0; ii < numElectrodeParticles; ii++) {
constraintScaleInv += hostConstraintVector[ii];
}
double constraintScale = 1.0 / constraintScaleInv;
for (int ii = 0; ii < numElectrodeParticles; ii++) {
hostConstraintVector[ii] *= constraintScale;
}
constraintVector.upload(hostConstraintVector, true);
}
}
CommonConstantPotentialCGSolver::CommonConstantPotentialCGSolver(ComputeContext& cc, int numParticles, int numElectrodeParticles, int paddedProblemSize, bool precond) :
CommonConstantPotentialSolver(cc, numParticles, numElectrodeParticles, paddedProblemSize), precondRequested(precond) {
int elementSize = cc.getUseDoublePrecision() ? sizeof(double) : sizeof(float);
threadBlockCount = cc.getNumThreadBlocks();
threadBlockSize = min(cc.getMaxThreadBlockSize(), cc.computeThreadBlockSize(max(5 * elementSize, 4 * elementSize + (int) sizeof(double))));
q.initialize(cc, paddedProblemSize, elementSize, "q");
grad.initialize(cc, numElectrodeParticles, elementSize, "grad");
projGrad.initialize(cc, numElectrodeParticles, elementSize, "projGrad");
precGrad.initialize(cc, numElectrodeParticles, elementSize, "precGrad");
qStep.initialize(cc, paddedProblemSize, elementSize, "qStep");
gradStep.initialize(cc, numElectrodeParticles, elementSize, "gradStep");
grad0.initialize(cc, numElectrodeParticles, elementSize, "grad0");
qLast.initialize(cc, paddedProblemSize, elementSize, "qLast");
blockSums1.initialize(cc, threadBlockCount, 5 * elementSize, "blockSums1");
blockSums2.initialize(cc, 1, 3 * elementSize, "blockSums2");
convergedResult.initialize(cc, 1, sizeof(int), "convergedResult");
if (precondRequested) {
// If double precision is supported, this will hold double values;
// otherwise, it will hold mm_float2 values.
precondVector.initialize(cc, numElectrodeParticles, sizeof(double), "precondVector");
}
convergedDownloadStartEvent = cc.createEvent();
convergedDownloadFinishEvent = cc.createEvent();
convergedDownloadQueue = cc.createQueue();
cc.clearBuffer(qLast);
}
void CommonConstantPotentialCGSolver::compileKernels(CommonCalcConstantPotentialForceKernel& kernel) {
CommonConstantPotentialSolver::compileKernels(kernel);
map<string, string> defines;
defines["ERROR_TARGET"] = kernel.cc.doubleToString(kernel.cgErrorTol * kernel.cgErrorTol * numElectrodeParticles);
defines["NUM_ELECTRODE_PARTICLES"] = kernel.cc.intToString(numElectrodeParticles);
defines["THREAD_BLOCK_SIZE"] = kernel.cc.intToString(threadBlockSize);
defines["THREAD_BLOCK_COUNT"] = kernel.cc.intToString(threadBlockCount);
if (kernel.useChargeConstraint) {
defines["USE_CHARGE_CONSTRAINT"] = "1";
}
if (precondRequested) {
defines["PRECOND_REQUESTED"] = "1";
}
ComputeProgram program = kernel.cc.compileProgram(CommonKernelSources::constantPotentialCGSolver, defines);
solveInitializeStep1Kernel = program->createKernel("solveInitializeStep1");
solveInitializeStep1Kernel->addArg(kernel.electrodeCharges);
solveInitializeStep1Kernel->addArg(qLast);
if (kernel.useChargeConstraint) {
solveInitializeStep1Kernel->addArg(); // chargeTarget
}
solveInitializeStep2Kernel = program->createKernel("solveInitializeStep2");
solveInitializeStep2Kernel->addArg(kernel.chargeDerivatives);
solveInitializeStep2Kernel->addArg(grad);
solveInitializeStep2Kernel->addArg(projGrad);
solveInitializeStep2Kernel->addArg(convergedResult);
solveInitializeStep3Kernel = program->createKernel("solveInitializeStep3");
solveInitializeStep3Kernel->addArg(kernel.electrodeCharges);
solveInitializeStep3Kernel->addArg(kernel.chargeDerivatives);
solveInitializeStep3Kernel->addArg(grad);
solveInitializeStep3Kernel->addArg(projGrad);
solveInitializeStep3Kernel->addArg(precGrad);
solveInitializeStep3Kernel->addArg(qStep);
solveInitializeStep3Kernel->addArg(grad0);
if (precondRequested) {
solveInitializeStep3Kernel->addArg(precondVector);
solveInitializeStep3Kernel->addArg(); // precondActivated
}
solveLoopStep1Kernel = program->createKernel("solveLoopStep1");
solveLoopStep1Kernel->addArg(kernel.chargeDerivatives);
solveLoopStep1Kernel->addArg(q);
solveLoopStep1Kernel->addArg(grad);
solveLoopStep1Kernel->addArg(qStep);
solveLoopStep1Kernel->addArg(gradStep);
solveLoopStep1Kernel->addArg(grad0);
solveLoopStep1Kernel->addArg(blockSums1);
solveLoopStep2Kernel = program->createKernel("solveLoopStep2");
solveLoopStep2Kernel->addArg(blockSums1);
solveLoopStep2Kernel->addArg(convergedResult);
solveLoopStep3Kernel = program->createKernel("solveLoopStep3");
solveLoopStep3Kernel->addArg(q);
solveLoopStep3Kernel->addArg(grad);
solveLoopStep3Kernel->addArg(qStep);
solveLoopStep3Kernel->addArg(gradStep);
solveLoopStep3Kernel->addArg(blockSums1);
solveLoopStep3Kernel->addArg(convergedResult);
if (kernel.useChargeConstraint) {
solveLoopStep3Kernel->addArg(); // chargeTarget
}
solveLoopStep4Kernel = program->createKernel("solveLoopStep4");
solveLoopStep4Kernel->addArg(grad);
solveLoopStep4Kernel->addArg(projGrad);
solveLoopStep4Kernel->addArg(precGrad);
solveLoopStep4Kernel->addArg(gradStep);
solveLoopStep4Kernel->addArg(blockSums2);
solveLoopStep4Kernel->addArg(convergedResult);
if (precondRequested) {
solveLoopStep4Kernel->addArg(precondVector);
solveLoopStep4Kernel->addArg(); // precondActivated
}
solveLoopStep5Kernel = program->createKernel("solveLoopStep5");
solveLoopStep5Kernel->addArg(kernel.electrodeCharges);
solveLoopStep5Kernel->addArg(precGrad);
solveLoopStep5Kernel->addArg(qStep);
solveLoopStep5Kernel->addArg(blockSums1);
solveLoopStep5Kernel->addArg(blockSums2);
solveLoopStep5Kernel->addArg(convergedResult);
}
void CommonConstantPotentialCGSolver::solveImpl(CommonCalcConstantPotentialForceKernel& kernel) {
ensureValid(kernel);
if (kernel.useChargeConstraint) {
if (kernel.cc.getUseDoublePrecision()) {
solveInitializeStep1Kernel->setArg(2, kernel.chargeTarget);
solveLoopStep3Kernel->setArg(6, kernel.chargeTarget);
}
else {
solveInitializeStep1Kernel->setArg(2, (float) kernel.chargeTarget);
solveLoopStep3Kernel->setArg(6, (float) kernel.chargeTarget);
}
}
if (precondRequested) {
solveInitializeStep3Kernel->setArg(8, (int) precondActivated);
solveLoopStep4Kernel->setArg(7, (int) precondActivated);
}
int converged;
// Evaluate the initial gradient Aq - b.
solveInitializeStep1Kernel->execute(threadBlockSize, threadBlockSize);
kernel.mustUpdateElectrodeCharges = true;
kernel.doDerivatives();
// Check for convergence at the initial guess charges.
solveInitializeStep2Kernel->execute(threadBlockSize, threadBlockSize);
convergedResult.download(&converged);
if (converged) {
return;
}
// Save the current charges, then evaluate the gradient with zero charges
// (-b) so that we can later compute Ap as (Ap - b) - (-b).
kernel.electrodeCharges.copyTo(q);
kernel.cc.clearBuffer(kernel.electrodeCharges);
kernel.mustUpdateElectrodeCharges = true;
kernel.doDerivatives();
// Prepare for conjugate gradient iterations.
solveInitializeStep3Kernel->execute(threadBlockSize, threadBlockSize);
kernel.cc.clearBuffer(blockSums1);
// Perform conjugate gradient iterations.
int* convergedPinned = (int*) kernel.cc.getPinnedBuffer();
for (int iter = 0; iter <= numElectrodeParticles; iter++) {
// Evaluate the matrix-vector product A qStep.
kernel.mustUpdateElectrodeCharges = true;
if (iter > 0) {
// This is a subsequent iteration; check for convergence. We can
// start clearing buffers for the derivatives while we wait for the
// flag to finish being copied from the device back to the host.
kernel.initDoDerivatives();
kernel.initPmeExecute();
convergedDownloadFinishEvent->wait();
converged = *convergedPinned;
if (converged) {
break;
}
kernel.doDerivatives(false);
} else {
// This is the first iteration; evaluate derivatives normally.
kernel.doDerivatives();
}
solveLoopStep1Kernel->execute(numElectrodeParticles, threadBlockSize);
solveLoopStep2Kernel->execute(threadBlockSize, threadBlockSize);
solveLoopStep3Kernel->execute(numElectrodeParticles, threadBlockSize);
// Periodically recompute the gradient vector instead of updating it to
// reduce the accumulation of roundoff error.
if (iter != 0 && iter % 32 == 0) {
kernel.mustUpdateElectrodeCharges = true;
kernel.doDerivatives();
kernel.chargeDerivatives.copyTo(grad);
}
solveLoopStep4Kernel->execute(threadBlockSize, threadBlockSize);
convergedDownloadStartEvent->enqueue();
kernel.cc.setCurrentQueue(convergedDownloadQueue);
convergedDownloadStartEvent->queueWait(convergedDownloadQueue);
convergedResult.download(convergedPinned, false);
convergedDownloadFinishEvent->enqueue();
kernel.cc.restoreDefaultQueue();
solveLoopStep5Kernel->execute(numElectrodeParticles, threadBlockSize);
if (iter == numElectrodeParticles) {
// No more iterations are allowed: download the convergence flag.
convergedDownloadFinishEvent->wait();
converged = *convergedPinned;
}
}
if (!converged) {
throw OpenMMException("Constant potential conjugate gradient iterations not converged");
}
// Store the final charges.
q.copyTo(kernel.electrodeCharges);
kernel.mustUpdateElectrodeCharges = true;
}
void CommonConstantPotentialCGSolver::getGuessChargeArrays(vector<ComputeArray*>& arrays) {
CommonConstantPotentialSolver::getGuessChargeArrays(arrays);
arrays.push_back(&qLast);
}
void CommonConstantPotentialCGSolver::ensureValid(CommonCalcConstantPotentialForceKernel& kernel) {
// Initializes or updates information for a preconditioner for the conjugate
// gradient method if this is its first use or electrode parameters have
// changed since its initialization.
// No action is required if the box vectors have not changed.
if (valid && boxVectors[0] == kernel.boxVectors[0] && boxVectors[1] == kernel.boxVectors[1] && boxVectors[2] == kernel.boxVectors[2]) {
return;
}
valid = true;
boxVectors[0] = kernel.boxVectors[0];
boxVectors[1] = kernel.boxVectors[1];
boxVectors[2] = kernel.boxVectors[2];
precondActivated = false;
if (precondRequested) {
// If electrode self-energy contributions differ between electrodes, a
// preconditioner may help convergence; otherwise, it provides no
// benefit and may slow convergence due to roundoff error.
for (int ie = 1; ie < kernel.numElectrodes; ie++) {
// Note hostElectrodeParams[1].w has the scale for electrode 0, etc.
if (kernel.hostElectrodeParams[ie + 1].w != kernel.hostElectrodeParams[ie].w) {
precondActivated = true;
break;
}
}
}
float pmeTerm = 0.0f;
if (precondActivated) {
// Save all positions and charges.
vector<mm_double4> posqSave(kernel.cc.getPaddedNumAtoms());
vector<double> qSave;
kernel.cc.getPosq().download(posqSave, true);
if (!kernel.usePosqCharges) {
qSave.resize(kernel.cc.getPaddedNumAtoms());
kernel.charges.download(qSave, true);
}
vector<mm_double4> posqCopy(posqSave);
vector<double> qCopy(qSave);
// Zero all charges.
if (kernel.usePosqCharges) {
for (int i = 0; i < numParticles; i++) {
posqCopy[i].w = 0.0;
}
}
else {
for (int i = 0; i < numParticles; i++) {
qCopy[i] = 0.0;
}
}
// Place a unit charge at the origin.
int i0 = kernel.hostElecToSys[0];
posqCopy[i0] = mm_double4(0.0, 0.0, 0.0, 1.0);
if (!kernel.usePosqCharges) {
qCopy[i0] = 1.0;
}
kernel.cc.getPosq().upload(posqCopy, true);
if (!kernel.usePosqCharges) {
kernel.charges.upload(qCopy, true);
}
// Perform a reference PME calculation with a single charge at the
// origin to find the constant offset on the preconditioner diagonal due
// to the PME calculation. This will actually vary slightly with
// position but only due to finite accuracy of the PME splines, so it is
// fine to assume it will be constant for the preconditioner.
kernel.cc.clearBuffer(kernel.chargeDerivativesFixed);
kernel.pmeShouldSort = true;
kernel.pmeExecute(false, false, true);
kernel.pmeShouldSort = true;
vector<long> derivatives(numElectrodeParticles);
kernel.chargeDerivativesFixed.download(derivatives);
double pmeTerm = derivatives[0] / (double) 0x100000000;
// Restore all positions and charges.
kernel.cc.getPosq().upload(posqSave, true);
if (!kernel.usePosqCharges) {
kernel.charges.upload(qSave, true);
}
// The diagonal has a contribution from reciprocal space, Ewald
// self-interaction, Ewald neutralizing plasma, Gaussian
// self-interaction, and Thomas-Fermi contributions.
double plasmaScale = CommonCalcConstantPotentialForceKernel::PLASMA_SCALE / (boxVectors[0][0] * boxVectors[1][1] * boxVectors[2][2] * kernel.ewaldAlpha * kernel.ewaldAlpha);
vector<double> hostPrecondVector(numElectrodeParticles);
double precondScaleInv = 0.0;
for (int ii = 0; ii < numElectrodeParticles; ii++) {
hostPrecondVector[ii] = 1.0 / (2.0 * (kernel.hostElectrodeParams[kernel.hostElecElec[ii] + 1].w - plasmaScale) + pmeTerm);
precondScaleInv += hostPrecondVector[ii];
}
double precondScale = 1.0 / precondScaleInv;
for (int ii = 0; ii < numElectrodeParticles; ii++) {
hostPrecondVector[ii] *= precondScale;
}
if (kernel.cc.getSupportsDoublePrecision()) {
precondVector.upload(hostPrecondVector);
}
else {
vector<mm_float2> hostPrecondVectorSplit(numElectrodeParticles);
for (int ii = 0; ii < numElectrodeParticles; ii++) {
float const x = (float) hostPrecondVector[ii];
hostPrecondVectorSplit[ii].x = x;
hostPrecondVectorSplit[ii].y = (float) (hostPrecondVector[ii] - (double) x);
}
precondVector.upload(hostPrecondVectorSplit);
}
}
}
const double CommonCalcConstantPotentialForceKernel::SELF_ALPHA_SCALE = ONE_4PI_EPS0 / sqrt(PI_M);
const double CommonCalcConstantPotentialForceKernel::SELF_ETA_SCALE = ONE_4PI_EPS0 / sqrt(2.0 * PI_M);
const double CommonCalcConstantPotentialForceKernel::SELF_TF_SCALE = 1.0 / (2.0 * EPSILON0);
const double CommonCalcConstantPotentialForceKernel::PLASMA_SCALE = 1.0 / (8.0 * EPSILON0);
CommonCalcConstantPotentialForceKernel::~CommonCalcConstantPotentialForceKernel() {
ContextSelector selector(cc);
if (solver != NULL) {
delete solver;
}
}
void CommonCalcConstantPotentialForceKernel::commonInitialize(const System& system, const ConstantPotentialForce& force, bool deviceIsCpu, bool useFixedPointChargeSpreading) {
ContextSelector selector(cc);
if (cc.getNumContexts() > 1) {
throw OpenMMException("ConstantPotentialForce does not support using multiple devices");
}
forceGroup = force.getForceGroup();
maxThreadBlockSize = cc.getMaxThreadBlockSize();
int elementSize = cc.getUseDoublePrecision() ? sizeof(double) : sizeof(float);
this->deviceIsCpu = deviceIsCpu;
this->useFixedPointChargeSpreading = useFixedPointChargeSpreading;
this->usePosqCharges = cc.requestPosqCharges();
int forceIndex;
for (forceIndex = 0; forceIndex < system.getNumForces() && &system.getForce(forceIndex) != &force; ++forceIndex) {
}
string prefix = "constantPotential" + cc.intToString(forceIndex) + "_";
// Get particle parameters.
numParticles = force.getNumParticles();
setCharges.resize(numParticles);
for (int i = 0; i < numParticles; i++) {
force.getParticleParameters(i, setCharges[i]);
}
// Get exceptions and identify "1-4" exceptions (those that don't zero the
// charge product).
for (int i = 0; i < force.getNumExceptions(); i++) {
int particle1, particle2;
double chargeProd;
force.getExceptionParameters(i, particle1, particle2, chargeProd);
exclusions.push_back(pair<int, int>(particle1, particle2));
if (chargeProd != 0.0) {
exceptionIndex[i] = exceptions.size();
exceptions.push_back(i);
}
}
// Get a list of all exclusions per particle, including exclusions for
// self-interaction.
vector<vector<int> > exclusionList(numParticles);
for (int i = 0; i < numParticles; i++) {
exclusionList[i].push_back(i);
}
for (auto exclusion : exclusions) {
exclusionList[exclusion.first].push_back(exclusion.second);
exclusionList[exclusion.second].push_back(exclusion.first);
}
// Get nonbonded parameters.
cutoff = force.getCutoffDistance();
ConstantPotentialForceImpl::calcPMEParameters(system, force, ewaldAlpha, gridSizeX, gridSizeY, gridSizeZ);
gridSizeX = cc.findLegalFFTDimension(gridSizeX);
gridSizeY = cc.findLegalFFTDimension(gridSizeY);
gridSizeZ = cc.findLegalFFTDimension(gridSizeZ);
// Get electrode parameters. sysToElec will be a map from system particle
// indices to electrode particle indices (or -1 if the particle is not an
// electrode particle), while elecToSys will be a map from electrode
// particle indices to system particle indices. sysElec will be a map from
// system particle indices to electrode indices (or -1 if the particle is
// not an electrode particle), while elecElec will be a map from electrode
// particle indices to electrode indices. Precompute and store electrode
// parameters for electrodes [-1, numElectrodes - 1] as [0, numElectrodes]
// in hostElectrodeParams for convenient lookup:
// x: potential
// y: gaussianWidth
// z: thomasFermiScale
// w: Precomputed self-energy scale
numElectrodes = force.getNumElectrodes();
hostSysToElec.resize(cc.getPaddedNumAtoms(), -1);
hostSysElec.resize(cc.getPaddedNumAtoms(), -1);
hostElectrodeParams.resize(numElectrodes + 1);
for (int ie = -1; ie < numElectrodes; ie++) {
double potential = 0.0;
double gaussianWidth = 0.0;
double thomasFermiScale = 0.0;
double selfScale = -SELF_ALPHA_SCALE * ewaldAlpha;
if (ie != -1) {
set<int> electrodeParticles;
force.getElectrodeParameters(ie, electrodeParticles, potential, gaussianWidth, thomasFermiScale);
for (int i : electrodeParticles) {
hostSysToElec[i] = hostElecToSys.size();
hostSysElec[i] = ie;
hostElecToSys.push_back(i);
hostElecElec.push_back(ie);
}
selfScale += SELF_ETA_SCALE / gaussianWidth + SELF_TF_SCALE * thomasFermiScale;
}
hostElectrodeParams[ie + 1] = mm_double4(potential, gaussianWidth, thomasFermiScale, selfScale);
}
numElectrodeParticles = hostElecToSys.size();
hasElectrodes = (numElectrodeParticles != 0);
chunkSize = deviceIsCpu ? 1 : 32;
chunkCount = (numElectrodeParticles + chunkSize - 1) / chunkSize;
paddedProblemSize = chunkCount * chunkSize;
// Clear charges on electrode particles.
for (int ii = 0; ii < numElectrodeParticles; ii++) {
setCharges[hostElecToSys[ii]] = 0.0;
}
// If using a charge constraint, set the charge target to be that on the
// electrode particles (so that the overall charge is constrained correctly
// if the non-electrolyte particles are non-neutral).
useChargeConstraint = force.getUseChargeConstraint();
if (useChargeConstraint) {
chargeTarget = force.getChargeConstraintTarget();
for (int i = 0; i < numParticles; i++) {
chargeTarget -= setCharges[i];
}
}
// Save external field.
force.getExternalField(externalField);
// Set up PME.
pmeSetup();
// Create bonded force for exclusion parameters.
int numExclusions = force.getNumExceptions();
if (numExclusions > 0) {
exclusionScales.initialize(cc, numExclusions, elementSize, "exclusionScales");
vector<vector<int> > hostExclusionAtoms(numExclusions, vector<int>(2));
vector<double> hostExclusionScales(numExclusions);
for (int i = 0; i < numExclusions; i++) {
hostExclusionAtoms[i][0] = exclusions[i].first;
hostExclusionAtoms[i][1] = exclusions[i].second;
hostExclusionScales[i] = ONE_4PI_EPS0 * setCharges[exclusions[i].first] * setCharges[exclusions[i].second];
}
exclusionScales.upload(hostExclusionScales, true);
map<string, string> replacements;
replacements["APPLY_PERIODIC"] = force.getExceptionsUsePeriodicBoundaryConditions() ? "1" : "0";
replacements["EWALD_ALPHA"] = cc.doubleToString(ewaldAlpha);
replacements["PARAMS"] = cc.getBondedUtilities().addArgument(exclusionScales, "real");
replacements["TWO_OVER_SQRT_PI"] = cc.doubleToString(2.0 / sqrt(M_PI));
cc.getBondedUtilities().addInteraction(hostExclusionAtoms, cc.replaceStrings(CommonKernelSources::constantPotentialExclusions, replacements), forceGroup);
}
// Upload 1-4 exception parameters and create bonded force.
int numExceptions = exceptions.size();
if (numExceptions > 0) {
exceptionScales.initialize(cc, numExceptions, elementSize, "exceptionScales");
vector<vector<int> > hostExceptionAtoms(numExceptions, vector<int>(2));
vector<double> hostExceptionScales(numExceptions);
for (int i = 0; i < numExceptions; i++) {
double chargeProd;
force.getExceptionParameters(exceptions[i], hostExceptionAtoms[i][0], hostExceptionAtoms[i][1], chargeProd);
hostExceptionScales[i] = ONE_4PI_EPS0 * chargeProd;
}
exceptionScales.upload(hostExceptionScales, true);
map<string, string> replacements;
replacements["APPLY_PERIODIC"] = force.getExceptionsUsePeriodicBoundaryConditions() ? "1" : "0";
replacements["PARAMS"] = cc.getBondedUtilities().addArgument(exceptionScales, "real");
cc.getBondedUtilities().addInteraction(hostExceptionAtoms, cc.replaceStrings(CommonKernelSources::constantPotentialExceptions, replacements), forceGroup);
}
// Upload parameters for electrodes.
electrodeParams.initialize(cc, numElectrodes + 1, 4 * elementSize, "electrodeParams");
electrodeParams.upload(hostElectrodeParams, true);
// Initialize lookup tables for constant potential on the device.
sysToElec.initialize<int>(cc, cc.getPaddedNumAtoms(), "sysToElec");
sysElec.initialize<int>(cc, cc.getPaddedNumAtoms(), "sysElec");
sysToElec.upload(hostSysToElec);
sysElec.upload(hostSysElec);
if (hasElectrodes) {
elecToSys.initialize<int>(cc, numElectrodeParticles, "elecToSys");
elecElec.initialize<int>(cc, numElectrodeParticles, "elecElec");
elecToSys.upload(hostElecToSys);
elecElec.upload(hostElecElec);
}
// Initialize nonbonded force.
map<string, string> nonbondedReplacements;
nonbondedReplacements["EWALD_ALPHA"] = cc.doubleToString(ewaldAlpha);
nonbondedReplacements["ONE_4PI_EPS0"] = cc.doubleToString(ONE_4PI_EPS0);
nonbondedReplacements["TWO_OVER_SQRT_PI"] = cc.doubleToString(2.0 / sqrt(M_PI));
if (usePosqCharges) {
nonbondedReplacements["CHARGE1"] = "posq1.w";
nonbondedReplacements["CHARGE2"] = "posq2.w";
}
else {
nonbondedReplacements["CHARGE1"] = prefix + "charge1";
nonbondedReplacements["CHARGE2"] = prefix + "charge2";
cc.getNonbondedUtilities().addParameter(ComputeParameterInfo(charges, prefix + "charge", "real", 1));
}
nonbondedReplacements["SYSELEC1"] = prefix + "sysElec1";
nonbondedReplacements["SYSELEC2"] = prefix + "sysElec2";
cc.getNonbondedUtilities().addParameter(ComputeParameterInfo(sysElec, prefix + "sysElec", "int", 1));
nonbondedReplacements["PARAMS"] = prefix + "params";
cc.getNonbondedUtilities().addArgument(ComputeParameterInfo(electrodeParams, prefix + "params", "real", 4));
cc.getNonbondedUtilities().addInteraction(true, true, true, force.getCutoffDistance(), exclusionList, cc.replaceStrings(CommonKernelSources::constantPotentialCoulombEnergyForces, nonbondedReplacements), force.getForceGroup(), true, false);
// Initialize the constant potential solver.
method = force.getConstantPotentialMethod();
cgErrorTol = force.getCGErrorTolerance();
if (method == ConstantPotentialForce::Matrix) {
if (hasElectrodes) {
solver = new CommonConstantPotentialMatrixSolver(cc, numParticles, numElectrodeParticles, paddedProblemSize);
}
}
else if (method == ConstantPotentialForce::CG) {
if (hasElectrodes) {
solver = new CommonConstantPotentialCGSolver(cc, numParticles, numElectrodeParticles, paddedProblemSize, force.getUsePreconditioner());
}
}
else {
throw OpenMMException("internal error: invalid constant potential method");
}
// Upload fixed charges and initial guesses for electrode charges.
hostNonElectrodeCharges = setCharges;
hostNonElectrodeCharges.resize(cc.getPaddedNumAtoms());
nonElectrodeCharges.initialize(cc, cc.getPaddedNumAtoms(), elementSize, "nonElectrodeCharges");
nonElectrodeCharges.upload(hostNonElectrodeCharges, true);
if (hasElectrodes) {
hostElectrodeCharges.resize(paddedProblemSize);
electrodeCharges.initialize(cc, paddedProblemSize, elementSize, "electrodeCharges");
electrodeCharges.upload(hostElectrodeCharges, true);
chargeDerivatives.initialize(cc, numElectrodeParticles, elementSize, "chargeDerivatives");
chargeDerivativesFixed.initialize<int64_t>(cc, numElectrodeParticles, "chargeDerivativesFixed");
}
// nonElectrodeCharges holds all fixed charges, and all electrode charges in
// nonElectrodeCharges should be zeroed. electrodeCharges holds all current
// electrode charges. charges holds charges to actually use for evaluation
// if usePosqCharges is false; otherwise, those charges will be in posq.
// The mustUpdate flags indicate that something has changed and charges or
// posq must be updated before energy/force/derivative evaluation.
charges.initialize(cc, cc.getPaddedNumAtoms(), elementSize, "charges");
mustUpdateNonElectrodeCharges = true;
mustUpdateElectrodeCharges = hasElectrodes;
totalChargeBuffer.initialize(cc, 1, elementSize, "totalChargeBuffer");
info = new ForceInfo(force, hostSysElec, hostElectrodeParams);
cc.addForce(info);
// Initialize cell offsets for finite field computation.
posCellOffsets.initialize<mm_int4>(cc, cc.getPaddedNumAtoms(), "posCellOffsets");
hostPosCellOffsets = cc.getPosCellOffsets();
posCellOffsets.upload(hostPosCellOffsets);
// Create a reorder listener to swap electrode guess charges.
ReorderListener* listener = new ReorderListener(cc, numElectrodeParticles, paddedProblemSize, hostSysToElec, hostElecToSys);
if (hasElectrodes) {
vector<ComputeArray*> guessChargeArrays;
solver->getGuessChargeArrays(guessChargeArrays);
for (ComputeArray* guessChargeArray : guessChargeArrays) {
listener->addChargeArray(*guessChargeArray);
}
}
cc.addReorderListener(listener);
}
void CommonCalcConstantPotentialForceKernel::copyParametersToContext(ContextImpl& context, const ConstantPotentialForce& force, int firstParticle, int lastParticle, int firstException, int lastException, int firstElectrode, int lastElectrode) {
ContextSelector selector(cc);
// Get particle parameters.
if (force.getNumParticles() != numParticles) {
throw OpenMMException("updateParametersInContext: The number of particles has changed");
}
if (firstParticle <= lastParticle) {
for (int i = firstParticle; i <= lastParticle; i++) {
if (hostSysElec[i] == -1) {
force.getParticleParameters(i, setCharges[i]);
hostNonElectrodeCharges[i] = setCharges[i];
}
}
if (cc.getUseDoublePrecision()) {
nonElectrodeCharges.uploadSubArray(&hostNonElectrodeCharges[firstParticle], firstParticle, lastParticle - firstParticle + 1);
}
else {
vector<float> hostNonElectrodeChargesFloat(lastParticle - firstParticle + 1);
for (int i = firstParticle; i <= lastParticle; i++) {
hostNonElectrodeChargesFloat[i - firstParticle] = (float) hostNonElectrodeCharges[i];
}
nonElectrodeCharges.uploadSubArray(&hostNonElectrodeChargesFloat[0], firstParticle, lastParticle - firstParticle + 1);
}
mustUpdateNonElectrodeCharges = true;
}
// Check exceptions.
int numExclusions = force.getNumExceptions();
int numExceptions = exceptions.size();
vector<int> checkExceptions;
for (int i = 0; i < numExclusions; i++) {
int particle1, particle2;
double chargeProd;
force.getExceptionParameters(i, particle1, particle2, chargeProd);
if (exceptionIndex.find(i) == exceptionIndex.end()) {
if (chargeProd != 0.0) {
throw OpenMMException("updateParametersInContext: The set of non-excluded exceptions has changed");
}
}
else {
checkExceptions.push_back(i);
}
}
if (checkExceptions.size() != numExceptions) {
throw OpenMMException("updateParametersInContext: The set of non-excluded exceptions has changed");
}
// Upload exclusion parameters.
if (numExclusions > 0 && (firstParticle <= lastParticle || firstException <= lastException)) {
vector<double> hostExclusionScales(numExclusions);
for (int i = 0; i < numExclusions; i++) {
hostExclusionScales[i] = ONE_4PI_EPS0 * setCharges[exclusions[i].first] * setCharges[exclusions[i].second];
}
exclusionScales.upload(hostExclusionScales, true);
}
// Upload exception parameters.
if (numExceptions > 0 && firstException <= lastException) {
vector<double> hostExceptionScales(numExceptions);
for (int i = 0; i < numExceptions; i++) {
int particle1, particle2;
double chargeProd;
force.getExceptionParameters(exceptions[i], particle1, particle2, chargeProd);
hostExceptionScales[i] = ONE_4PI_EPS0 * chargeProd;
}
exceptionScales.upload(hostExceptionScales, true);
}
// Get electrode parameters.
set<int> allElectrodeParticles;
for (int ie = 0; ie < force.getNumElectrodes(); ie++) {
set<int> electrodeParticles;
double potential;
double gaussianWidth;
double thomasFermiScale;
force.getElectrodeParameters(ie, electrodeParticles, potential, gaussianWidth, thomasFermiScale);
for (int i : electrodeParticles) {
if (hostSysElec[i] != ie) {
throw OpenMMException("updateParametersInContext: The electrode assignment of a particle has changed");
}
allElectrodeParticles.insert(i);
}
if (ie >= firstElectrode && ie <= lastElectrode) {
double selfScale = SELF_ETA_SCALE / gaussianWidth + SELF_TF_SCALE * thomasFermiScale - SELF_ALPHA_SCALE * ewaldAlpha;
hostElectrodeParams[ie + 1] = mm_double4(potential, gaussianWidth, thomasFermiScale, selfScale);
}
}
if (allElectrodeParticles.size() != numElectrodeParticles) {
throw OpenMMException("updateParametersInContext: The electrode state of a particle has changed");
}
if (firstElectrode <= lastElectrode) {
electrodeParams.upload(hostElectrodeParams, true);
}
// Update charge target.
if (useChargeConstraint) {
chargeTarget = force.getChargeConstraintTarget();
for (int i = 0; i < numParticles; i++) {
chargeTarget -= setCharges[i];
}
}
// Update external field.
force.getExternalField(externalField);
cc.invalidateMolecules(info, firstParticle <= lastParticle || firstElectrode <= lastElectrode, firstException <= lastException);
if (solver != NULL) {
solver->discardSavedSolution();
if (firstElectrode <= lastElectrode) {
solver->invalidate();
}
}
}
void CommonCalcConstantPotentialForceKernel::getPMEParameters(double& alpha, int& nx, int& ny, int& nz) const {
alpha = ewaldAlpha;
nx = gridSizeX;
ny = gridSizeY;
nz = gridSizeZ;
}
double CommonCalcConstantPotentialForceKernel::execute(ContextImpl& context, bool includeForces, bool includeEnergy) {
ContextSelector selector(cc);
ensureInitialized(context);
cc.getPeriodicBoxVectors(boxVectors[0], boxVectors[1], boxVectors[2]);
setKernelInputs(includeEnergy, includeForces);
pmeShouldSort = true;
if (solver != NULL) {
solver->solve(*this);
}
return doEnergyForces(includeForces, includeEnergy);
}
void CommonCalcConstantPotentialForceKernel::getCharges(ContextImpl& context, vector<double>& chargesOut) {
ContextSelector selector(cc);
ensureInitialized(context);
// We need to have a neighbor list to evaluate direct space derivatives.
cc.getNonbondedUtilities().prepareInteractions(1 << forceGroup);
cc.getPeriodicBoxVectors(boxVectors[0], boxVectors[1], boxVectors[2]);
setKernelInputs(false, false);
pmeShouldSort = true;
if (solver != NULL) {
solver->solve(*this);
}
// Preserve fixed charges exactly and load solved values of fluctuating
// charges.
chargesOut = setCharges;
if (hasElectrodes) {
const vector<int>& order = cc.getAtomIndex();
electrodeCharges.download(hostElectrodeCharges, true);
for (int ii = 0; ii < numElectrodeParticles; ii++) {
chargesOut[order[hostElecToSys[ii]]] = hostElectrodeCharges[ii];
}
}
}
void CommonCalcConstantPotentialForceKernel::ensureInitialized(ContextImpl& context) {
if (hasInitializedKernel) {
return;
}
NonbondedUtilities& nb = cc.getNonbondedUtilities();
map<string, string> defines;
defines["CUTOFF"] = cc.doubleToString(cutoff);
defines["CUTOFF_SQUARED"] = cc.doubleToString(cutoff * cutoff);
defines["EWALD_ALPHA"] = cc.doubleToString(ewaldAlpha);
defines["ONE_4PI_EPS0"] = cc.doubleToString(ONE_4PI_EPS0);
defines["NUM_PARTICLES"] = cc.intToString(numParticles);
defines["NUM_ELECTRODE_PARTICLES"] = cc.intToString(numElectrodeParticles);
defines["NUM_EXCLUSION_TILES"] = cc.intToString(nb.getExclusionTiles().getSize());
defines["PADDED_NUM_ATOMS"] = cc.intToString(cc.getPaddedNumAtoms());
defines["PLASMA_SCALE"] = cc.doubleToString(PLASMA_SCALE);
defines["THREAD_BLOCK_SIZE"] = cc.intToString(maxThreadBlockSize);
defines["TILE_SIZE"] = cc.intToString(ComputeContext::TileSize);
defines["WORK_GROUP_SIZE"] = cc.intToString(nb.getForceThreadBlockSize());
if (usePosqCharges) {
defines["USE_POSQ_CHARGES"] = "1";
}
if (deviceIsCpu) {
defines["DEVICE_IS_CPU"] = "1";
}
ComputeProgram program = cc.compileProgram(CommonKernelSources::constantPotential, defines);
updateNonElectrodeChargesKernel = program->createKernel("updateNonElectrodeCharges");
updateNonElectrodeChargesKernel->addArg(cc.getPosq());
updateNonElectrodeChargesKernel->addArg(charges);
updateNonElectrodeChargesKernel->addArg(nonElectrodeCharges);
updateNonElectrodeChargesKernel->addArg(sysElec);
if (hasElectrodes) {
updateElectrodeChargesKernel = program->createKernel("updateElectrodeCharges");
updateElectrodeChargesKernel->addArg(cc.getPosq());
updateElectrodeChargesKernel->addArg(charges);
updateElectrodeChargesKernel->addArg(electrodeCharges);
updateElectrodeChargesKernel->addArg(elecToSys);
}
getTotalChargeKernel = program->createKernel("getTotalCharge");
getTotalChargeKernel->addArg(cc.getPosq());
getTotalChargeKernel->addArg(charges);
getTotalChargeKernel->addArg(totalChargeBuffer);
evaluateSelfEnergyForcesKernel = program->createKernel("evaluateSelfEnergyForces");
evaluateSelfEnergyForcesKernel->addArg(cc.getPosq());
evaluateSelfEnergyForcesKernel->addArg(charges);
evaluateSelfEnergyForcesKernel->addArg(sysElec);
evaluateSelfEnergyForcesKernel->addArg(electrodeParams);
evaluateSelfEnergyForcesKernel->addArg(totalChargeBuffer);
evaluateSelfEnergyForcesKernel->addArg(posCellOffsets);
evaluateSelfEnergyForcesKernel->addArg(); // periodicBoxVecX
evaluateSelfEnergyForcesKernel->addArg(); // periodicBoxVecY
evaluateSelfEnergyForcesKernel->addArg(); // periodicBoxVecZ
evaluateSelfEnergyForcesKernel->addArg(); // externalField
evaluateSelfEnergyForcesKernel->addArg(cc.getEnergyBuffer());
evaluateSelfEnergyForcesKernel->addArg(cc.getLongForceBuffer());
if (hasElectrodes) {
evaluateDirectDerivativesKernel = program->createKernel("evaluateDirectDerivatives");
evaluateDirectDerivativesKernel->addArg(cc.getPosq());
evaluateDirectDerivativesKernel->addArg(charges);
evaluateDirectDerivativesKernel->addArg(sysToElec);
evaluateDirectDerivativesKernel->addArg(sysElec);
evaluateDirectDerivativesKernel->addArg(electrodeParams);
evaluateDirectDerivativesKernel->addArg(chargeDerivativesFixed);
evaluateDirectDerivativesKernel->addArg(); // periodicBoxSize
evaluateDirectDerivativesKernel->addArg(); // invPeriodicBoxSize
evaluateDirectDerivativesKernel->addArg(); // periodicBoxVecX
evaluateDirectDerivativesKernel->addArg(); // periodicBoxVecY
evaluateDirectDerivativesKernel->addArg(); // periodicBoxVecZ
evaluateDirectDerivativesKernel->addArg(nb.getExclusionTiles());
evaluateDirectDerivativesKernel->addArg(nb.getInteractingTiles());
evaluateDirectDerivativesKernel->addArg(nb.getInteractionCount());
evaluateDirectDerivativesKernel->addArg(nb.getBlockCenters());
evaluateDirectDerivativesKernel->addArg(nb.getBlockBoundingBoxes());
evaluateDirectDerivativesKernel->addArg(nb.getInteractingAtoms());
evaluateDirectDerivativesKernel->addArg(); // maxTiles
finishDerivativesKernel = program->createKernel("finishDerivatives");
finishDerivativesKernel->addArg(cc.getPosq());
finishDerivativesKernel->addArg(charges);
finishDerivativesKernel->addArg(elecToSys);
finishDerivativesKernel->addArg(elecElec);
finishDerivativesKernel->addArg(electrodeParams);
finishDerivativesKernel->addArg(totalChargeBuffer);
finishDerivativesKernel->addArg(posCellOffsets);
finishDerivativesKernel->addArg(); // periodicBoxVecX
finishDerivativesKernel->addArg(); // periodicBoxVecY
finishDerivativesKernel->addArg(); // periodicBoxVecZ
finishDerivativesKernel->addArg(); // externalField
finishDerivativesKernel->addArg(chargeDerivatives);
finishDerivativesKernel->addArg(chargeDerivativesFixed);
}
pmeCompileKernels();
if (solver != NULL) {
solver->compileKernels(*this);
}
hasInitializedKernel = true;
}
double CommonCalcConstantPotentialForceKernel::doEnergyForces(bool includeForces, bool includeEnergy) {
if (mustUpdateNonElectrodeCharges) {
updateNonElectrodeChargesKernel->execute(numParticles);
mustUpdateNonElectrodeCharges = false;
}
if (mustUpdateElectrodeCharges) {
if (hasElectrodes) {
updateElectrodeChargesKernel->execute(numElectrodeParticles);
}
mustUpdateElectrodeCharges = false;
}
pmeExecute(includeEnergy, includeForces, false);
// Ewald neutralizing plasma and per-particle energy.
if (includeEnergy || includeForces) {
getTotalChargeKernel->execute(maxThreadBlockSize, maxThreadBlockSize);
if (cc.getUseDoublePrecision()) {
evaluateSelfEnergyForcesKernel->setArg(9, mm_double4(externalField[0], externalField[1], externalField[2], 0));
}
else {
evaluateSelfEnergyForcesKernel->setArg(9, mm_float4((float) externalField[0], (float) externalField[1], (float) externalField[2], 0));
}
evaluateSelfEnergyForcesKernel->execute(numParticles);
}
return 0.0;
}
void CommonCalcConstantPotentialForceKernel::initDoDerivatives() {
if (mustUpdateNonElectrodeCharges) {
updateNonElectrodeChargesKernel->execute(numParticles);
mustUpdateNonElectrodeCharges = false;
}
if (mustUpdateElectrodeCharges) {
if (hasElectrodes) {
updateElectrodeChargesKernel->execute(numElectrodeParticles);
}
mustUpdateElectrodeCharges = false;
}
cc.clearBuffer(chargeDerivatives);
cc.clearBuffer(chargeDerivativesFixed);
}
void CommonCalcConstantPotentialForceKernel::doDerivatives(bool init) {
if (init) {
initDoDerivatives();
}
pmeExecute(false, false, true, init);
NonbondedUtilities& nb = cc.getNonbondedUtilities();
evaluateDirectDerivativesKernel->execute(nb.getNumForceThreadBlocks() * nb.getForceThreadBlockSize(), nb.getForceThreadBlockSize());
// Ewald neutralizing plasma and per-particle derivatives.
getTotalChargeKernel->execute(maxThreadBlockSize, maxThreadBlockSize);
finishDerivativesKernel->execute(numElectrodeParticles);
}
void CommonCalcConstantPotentialForceKernel::pmeSetup() {
pmeDefines["PME_ORDER"] = cc.intToString(PmeOrder);
pmeDefines["NUM_ATOMS"] = cc.intToString(numParticles);
pmeDefines["PADDED_NUM_ATOMS"] = cc.intToString(cc.getPaddedNumAtoms());
pmeDefines["NUM_INDICES"] = cc.intToString(numElectrodeParticles);
pmeDefines["RECIP_EXP_FACTOR"] = cc.doubleToString(M_PI * M_PI / (ewaldAlpha * ewaldAlpha));
pmeDefines["GRID_SIZE_X"] = cc.intToString(gridSizeX);
pmeDefines["GRID_SIZE_Y"] = cc.intToString(gridSizeY);
pmeDefines["GRID_SIZE_Z"] = cc.intToString(gridSizeZ);
pmeDefines["EPSILON_FACTOR"] = cc.doubleToString(sqrt(ONE_4PI_EPS0));
pmeDefines["M_PI"] = cc.doubleToString(M_PI);
if (deviceIsCpu) {
pmeDefines["DEVICE_IS_CPU"] = "1";
}
if (useFixedPointChargeSpreading) {
pmeDefines["USE_FIXED_POINT_CHARGE_SPREADING"] = "1";
}
// Create required data structures.
int elementSize = (cc.getUseDoublePrecision() ? sizeof(double) : sizeof(float));
int gridElements = gridSizeX*gridSizeY*gridSizeZ;
pmeGrid1.initialize(cc, gridElements, 2*elementSize, "pmeGrid1");
pmeGrid2.initialize(cc, gridElements, 2*elementSize, "pmeGrid2");
pmeBsplineModuliX.initialize(cc, gridSizeX, elementSize, "pmeBsplineModuliX");
pmeBsplineModuliY.initialize(cc, gridSizeY, elementSize, "pmeBsplineModuliY");
pmeBsplineModuliZ.initialize(cc, gridSizeZ, elementSize, "pmeBsplineModuliZ");
pmeAtomGridIndex.initialize<mm_int2>(cc, numParticles, "pmeAtomGridIndex");
int energyElementSize = (cc.getUseDoublePrecision() || cc.getUseMixedPrecision() ? sizeof(double) : sizeof(float));
sort = cc.createSort(new SortTrait(), cc.getNumAtoms());
fft = cc.createFFT(gridSizeX, gridSizeY, gridSizeZ, true);
// Initialize the b-spline moduli.
int maxSize = max(max(gridSizeX, gridSizeY), gridSizeZ);
vector<double> data(PmeOrder);
vector<double> ddata(PmeOrder);
vector<double> bsplines_data(maxSize);
data[PmeOrder-1] = 0.0;
data[1] = 0.0;
data[0] = 1.0;
for (int i = 3; i < PmeOrder; i++) {
double div = 1.0/(i-1.0);
data[i-1] = 0.0;
for (int j = 1; j < (i-1); j++)
data[i-j-1] = div*(j*data[i-j-2]+(i-j)*data[i-j-1]);
data[0] = div*data[0];
}
// Differentiate.
ddata[0] = -data[0];
for (int i = 1; i < PmeOrder; i++)
ddata[i] = data[i-1]-data[i];
double div = 1.0/(PmeOrder-1);
data[PmeOrder-1] = 0.0;
for (int i = 1; i < (PmeOrder-1); i++)
data[PmeOrder-i-1] = div*(i*data[PmeOrder-i-2]+(PmeOrder-i)*data[PmeOrder-i-1]);
data[0] = div*data[0];
for (int i = 0; i < maxSize; i++)
bsplines_data[i] = 0.0;
for (int i = 1; i <= PmeOrder; i++)
bsplines_data[i] = data[i-1];
// Evaluate the actual bspline moduli for X/Y/Z.
for (int dim = 0; dim < 3; dim++) {
int ndata = (dim == 0 ? gridSizeX : dim == 1 ? gridSizeY : gridSizeZ);
vector<double> moduli(ndata);
for (int i = 0; i < ndata; i++) {
double sc = 0.0;
double ss = 0.0;
for (int j = 0; j < ndata; j++) {
double arg = (2.0*M_PI*i*j)/ndata;
sc += bsplines_data[j]*cos(arg);
ss += bsplines_data[j]*sin(arg);
}
moduli[i] = sc*sc+ss*ss;
}
for (int i = 0; i < ndata; i++)
if (moduli[i] < 1.0e-7)
moduli[i] = (moduli[(i-1+ndata)%ndata]+moduli[(i+1)%ndata])*0.5;
if (dim == 0)
pmeBsplineModuliX.upload(moduli, true);
else if (dim == 1)
pmeBsplineModuliY.upload(moduli, true);
else
pmeBsplineModuliZ.upload(moduli, true);
}
}
void CommonCalcConstantPotentialForceKernel::pmeCompileKernels() {
map<string, string> replacements;
replacements["CHARGE"] = (usePosqCharges ? "pos.w" : "charges[atom]");
ComputeProgram program = cc.compileProgram(cc.replaceStrings(CommonKernelSources::pme, replacements), pmeDefines);
pmeGridIndexKernel = program->createKernel("findAtomGridIndex");
pmeGridIndexKernel->addArg(cc.getPosq());
pmeGridIndexKernel->addArg(pmeAtomGridIndex);
for (int i = 0; i < 8; i++) {
pmeGridIndexKernel->addArg();
}
pmeSpreadChargeKernel = program->createKernel("gridSpreadCharge");
pmeSpreadChargeKernel->addArg(cc.getPosq());
if (useFixedPointChargeSpreading) {
pmeSpreadChargeKernel->addArg(pmeGrid2);
}
else {
pmeSpreadChargeKernel->addArg(pmeGrid1);
}
for (int i = 0; i < 8; i++) {
pmeSpreadChargeKernel->addArg();
}
pmeSpreadChargeKernel->addArg(pmeAtomGridIndex);
pmeSpreadChargeKernel->addArg(charges);
pmeConvolutionKernel = program->createKernel("reciprocalConvolution");
pmeConvolutionKernel->addArg(pmeGrid2);
pmeConvolutionKernel->addArg(pmeBsplineModuliX);
pmeConvolutionKernel->addArg(pmeBsplineModuliY);
pmeConvolutionKernel->addArg(pmeBsplineModuliZ);
for (int i = 0; i < 3; i++) {
pmeConvolutionKernel->addArg();
}
pmeEvalEnergyKernel = program->createKernel("gridEvaluateEnergy");
pmeEvalEnergyKernel->addArg(pmeGrid2);
pmeEvalEnergyKernel->addArg(cc.getEnergyBuffer());
pmeEvalEnergyKernel->addArg(pmeBsplineModuliX);
pmeEvalEnergyKernel->addArg(pmeBsplineModuliY);
pmeEvalEnergyKernel->addArg(pmeBsplineModuliZ);
for (int i = 0; i < 3; i++) {
pmeEvalEnergyKernel->addArg();
}
pmeInterpolateForceKernel = program->createKernel("gridInterpolateForce");
pmeInterpolateForceKernel->addArg(cc.getPosq());
pmeInterpolateForceKernel->addArg(cc.getLongForceBuffer());
pmeInterpolateForceKernel->addArg(pmeGrid1);
for (int i = 0; i < 8; i++) {
pmeInterpolateForceKernel->addArg();
}
pmeInterpolateForceKernel->addArg(pmeAtomGridIndex);
pmeInterpolateForceKernel->addArg(charges);
if (hasElectrodes) {
pmeInterpolateChargeDerivativesKernel = program->createKernel("gridInterpolateChargeDerivatives");
pmeInterpolateChargeDerivativesKernel->addArg(cc.getPosq());
pmeInterpolateChargeDerivativesKernel->addArg(chargeDerivativesFixed);
pmeInterpolateChargeDerivativesKernel->addArg(pmeGrid1);
for (int i = 0; i < 8; i++) {
pmeInterpolateChargeDerivativesKernel->addArg();
}
pmeInterpolateChargeDerivativesKernel->addArg(elecToSys);
}
if (useFixedPointChargeSpreading) {
pmeFinishSpreadChargeKernel = program->createKernel("finishSpreadCharge");
pmeFinishSpreadChargeKernel->addArg(pmeGrid2);
pmeFinishSpreadChargeKernel->addArg(pmeGrid1);
}
}
void CommonCalcConstantPotentialForceKernel::initPmeExecute() {
if (useFixedPointChargeSpreading) {
cc.clearBuffer(pmeGrid2);
}
else {
cc.clearBuffer(pmeGrid1);
}
}
void CommonCalcConstantPotentialForceKernel::pmeExecute(bool includeEnergy, bool includeForces, bool includeChargeDerivatives, bool init) {
if (init) {
initPmeExecute();
}
if (pmeShouldSort) {
pmeGridIndexKernel->execute(cc.getNumAtoms());
sort->sort(pmeAtomGridIndex);
pmeShouldSort = false;
}
pmeSpreadChargeKernel->execute(cc.getNumAtoms());
if (useFixedPointChargeSpreading) {
pmeFinishSpreadChargeKernel->execute(gridSizeX*gridSizeY*gridSizeZ);
}
fft->execFFT(pmeGrid1, pmeGrid2, true);
if (includeEnergy) {
pmeEvalEnergyKernel->execute(gridSizeX*gridSizeY*gridSizeZ);
}
if (includeForces || includeChargeDerivatives) {
pmeConvolutionKernel->execute(gridSizeX*gridSizeY*gridSizeZ);
fft->execFFT(pmeGrid2, pmeGrid1, false);
if (includeForces) {
if (deviceIsCpu) {
pmeInterpolateForceKernel->execute(cc.getNumThreadBlocks(), 1);
}
else {
pmeInterpolateForceKernel->execute(cc.getNumAtoms());
}
}
if (includeChargeDerivatives && hasElectrodes) {
if (deviceIsCpu) {
pmeInterpolateChargeDerivativesKernel->execute(cc.getNumThreadBlocks(), 1);
}
else {
pmeInterpolateChargeDerivativesKernel->execute(numElectrodeParticles);
}
}
}
}
void CommonCalcConstantPotentialForceKernel::setKernelInputs(bool includeEnergy, bool includeForces) {
double determinant = boxVectors[0][0] * boxVectors[1][1] * boxVectors[2][2];
double scale = 1.0 / determinant;
mm_double4 recipBoxVectors[3];
recipBoxVectors[0] = mm_double4(boxVectors[1][1] * boxVectors[2][2] * scale, 0, 0, 0);
recipBoxVectors[1] = mm_double4(-boxVectors[1][0] * boxVectors[2][2] * scale, boxVectors[0][0] * boxVectors[2][2] * scale, 0, 0);
recipBoxVectors[2] = mm_double4((boxVectors[1][0] * boxVectors[2][1] - boxVectors[1][1] * boxVectors[2][0]) * scale, -boxVectors[0][0] * boxVectors[2][1] * scale, boxVectors[0][0] * boxVectors[1][1] * scale, 0);
mm_float4 recipBoxVectorsFloat[3];
for (int i = 0; i < 3; i++) {
recipBoxVectorsFloat[i] = mm_float4((float) recipBoxVectors[i].x, (float) recipBoxVectors[i].y, (float) recipBoxVectors[i].z, 0);
}
setPeriodicBoxArgs(cc, pmeGridIndexKernel, 2);
setPeriodicBoxArgs(cc, pmeSpreadChargeKernel, 2);
if (includeEnergy || includeForces) {
if (cc.getUseDoublePrecision()) {
evaluateSelfEnergyForcesKernel->setArg(6, mm_double4(boxVectors[0][0], boxVectors[0][1], boxVectors[0][2], 0));
evaluateSelfEnergyForcesKernel->setArg(7, mm_double4(boxVectors[1][0], boxVectors[1][1], boxVectors[1][2], 0));
evaluateSelfEnergyForcesKernel->setArg(8, mm_double4(boxVectors[2][0], boxVectors[2][1], boxVectors[2][2], 0));
}
else {
evaluateSelfEnergyForcesKernel->setArg(6, mm_float4((float) boxVectors[0][0], (float) boxVectors[0][1], (float) boxVectors[0][2], 0));
evaluateSelfEnergyForcesKernel->setArg(7, mm_float4((float) boxVectors[1][0], (float) boxVectors[1][1], (float) boxVectors[1][2], 0));
evaluateSelfEnergyForcesKernel->setArg(8, mm_float4((float) boxVectors[2][0], (float) boxVectors[2][1], (float) boxVectors[2][2], 0));
}
}
if (includeForces) {
setPeriodicBoxArgs(cc, pmeInterpolateForceKernel, 3);
}
if (hasElectrodes) {
setPeriodicBoxArgs(cc, pmeInterpolateChargeDerivativesKernel, 3);
setPeriodicBoxArgs(cc, evaluateDirectDerivativesKernel, 6);
evaluateDirectDerivativesKernel->setArg(17, (unsigned int) cc.getNonbondedUtilities().getInteractingTiles().getSize());
if (cc.getUseDoublePrecision()) {
finishDerivativesKernel->setArg(7, mm_double4(boxVectors[0][0], boxVectors[0][1], boxVectors[0][2], 0));
finishDerivativesKernel->setArg(8, mm_double4(boxVectors[1][0], boxVectors[1][1], boxVectors[1][2], 0));
finishDerivativesKernel->setArg(9, mm_double4(boxVectors[2][0], boxVectors[2][1], boxVectors[2][2], 0));
finishDerivativesKernel->setArg(10, mm_double4(externalField[0], externalField[1], externalField[2], 0));
}
else {
finishDerivativesKernel->setArg(7, mm_float4((float) boxVectors[0][0], (float) boxVectors[0][1], (float) boxVectors[0][2], 0));
finishDerivativesKernel->setArg(8, mm_float4((float) boxVectors[1][0], (float) boxVectors[1][1], (float) boxVectors[1][2], 0));
finishDerivativesKernel->setArg(9, mm_float4((float) boxVectors[2][0], (float) boxVectors[2][1], (float) boxVectors[2][2], 0));
finishDerivativesKernel->setArg(10, mm_float4((float) externalField[0], (float) externalField[1], (float) externalField[2], 0));
}
}
if (cc.getUseDoublePrecision()) {
pmeGridIndexKernel->setArg(7, recipBoxVectors[0]);
pmeGridIndexKernel->setArg(8, recipBoxVectors[1]);
pmeGridIndexKernel->setArg(9, recipBoxVectors[2]);
pmeSpreadChargeKernel->setArg(7, recipBoxVectors[0]);
pmeSpreadChargeKernel->setArg(8, recipBoxVectors[1]);
pmeSpreadChargeKernel->setArg(9, recipBoxVectors[2]);
if (includeEnergy) {
pmeEvalEnergyKernel->setArg<mm_double4>(5, recipBoxVectors[0]);
pmeEvalEnergyKernel->setArg<mm_double4>(6, recipBoxVectors[1]);
pmeEvalEnergyKernel->setArg<mm_double4>(7, recipBoxVectors[2]);
}
if (includeForces || hasElectrodes) {
pmeConvolutionKernel->setArg<mm_double4>(4, recipBoxVectors[0]);
pmeConvolutionKernel->setArg<mm_double4>(5, recipBoxVectors[1]);
pmeConvolutionKernel->setArg<mm_double4>(6, recipBoxVectors[2]);
if (includeForces) {
pmeInterpolateForceKernel->setArg(8, recipBoxVectors[0]);
pmeInterpolateForceKernel->setArg(9, recipBoxVectors[1]);
pmeInterpolateForceKernel->setArg(10, recipBoxVectors[2]);
}
if (hasElectrodes) {
pmeInterpolateChargeDerivativesKernel->setArg(8, recipBoxVectors[0]);
pmeInterpolateChargeDerivativesKernel->setArg(9, recipBoxVectors[1]);
pmeInterpolateChargeDerivativesKernel->setArg(10, recipBoxVectors[2]);
}
}
}
else {
pmeGridIndexKernel->setArg(7, recipBoxVectorsFloat[0]);
pmeGridIndexKernel->setArg(8, recipBoxVectorsFloat[1]);
pmeGridIndexKernel->setArg(9, recipBoxVectorsFloat[2]);
pmeSpreadChargeKernel->setArg(7, recipBoxVectorsFloat[0]);
pmeSpreadChargeKernel->setArg(8, recipBoxVectorsFloat[1]);
pmeSpreadChargeKernel->setArg(9, recipBoxVectorsFloat[2]);
if (includeEnergy) {
pmeEvalEnergyKernel->setArg<mm_float4>(5, recipBoxVectorsFloat[0]);
pmeEvalEnergyKernel->setArg<mm_float4>(6, recipBoxVectorsFloat[1]);
pmeEvalEnergyKernel->setArg<mm_float4>(7, recipBoxVectorsFloat[2]);
}
if (includeForces || hasElectrodes) {
pmeConvolutionKernel->setArg<mm_float4>(4, recipBoxVectorsFloat[0]);
pmeConvolutionKernel->setArg<mm_float4>(5, recipBoxVectorsFloat[1]);
pmeConvolutionKernel->setArg<mm_float4>(6, recipBoxVectorsFloat[2]);
if (includeForces) {
pmeInterpolateForceKernel->setArg(8, recipBoxVectorsFloat[0]);
pmeInterpolateForceKernel->setArg(9, recipBoxVectorsFloat[1]);
pmeInterpolateForceKernel->setArg(10, recipBoxVectorsFloat[2]);
}
if (hasElectrodes) {
pmeInterpolateChargeDerivativesKernel->setArg(8, recipBoxVectorsFloat[0]);
pmeInterpolateChargeDerivativesKernel->setArg(9, recipBoxVectorsFloat[1]);
pmeInterpolateChargeDerivativesKernel->setArg(10, recipBoxVectorsFloat[2]);
}
}
}
const vector<mm_int4>& newPosCellOffsets = cc.getPosCellOffsets();
bool mustUpdatePosCellOffsets = false;
for (int i = 0; i < numParticles; i++) {
mm_int4 oldOffset = hostPosCellOffsets[i];
mm_int4 newOffset = newPosCellOffsets[i];
if (newOffset.x != oldOffset.x || newOffset.y != oldOffset.y || newOffset.z != oldOffset.z) {
mustUpdatePosCellOffsets = true;
break;
}
}
if (mustUpdatePosCellOffsets) {
hostPosCellOffsets.assign(newPosCellOffsets.begin(), newPosCellOffsets.end());
posCellOffsets.upload(hostPosCellOffsets);
}
}
...@@ -130,6 +130,8 @@ public: ...@@ -130,6 +130,8 @@ public:
addForcesKernel->setArg(1, cc.getLongForceBuffer()); addForcesKernel->setArg(1, cc.getLongForceBuffer());
addForcesKernel->execute(cc.getNumAtoms()); addForcesKernel->execute(cc.getNumAtoms());
} }
void setChargeDerivatives(float* chargeDerivatives) {
}
private: private:
ComputeContext& cc; ComputeContext& cc;
vector<mm_float4> posq; vector<mm_float4> posq;
...@@ -144,7 +146,7 @@ public: ...@@ -144,7 +146,7 @@ public:
void computeForceAndEnergy(bool includeForces, bool includeEnergy, int groups) { void computeForceAndEnergy(bool includeForces, bool includeEnergy, int groups) {
Vec3 boxVectors[3]; Vec3 boxVectors[3];
cc.getPeriodicBoxVectors(boxVectors[0], boxVectors[1], boxVectors[2]); cc.getPeriodicBoxVectors(boxVectors[0], boxVectors[1], boxVectors[2]);
pme.getAs<CalcPmeReciprocalForceKernel>().beginComputation(io, boxVectors, includeEnergy); pme.getAs<CalcPmeReciprocalForceKernel>().beginComputation(io, boxVectors, includeEnergy, true, false);
} }
private: private:
ComputeContext& cc; ComputeContext& cc;
...@@ -407,6 +409,7 @@ void CommonCalcNonbondedForceKernel::commonInitialize(const System& system, cons ...@@ -407,6 +409,7 @@ void CommonCalcNonbondedForceKernel::commonInitialize(const System& system, cons
pmeDefines["PME_ORDER"] = cc.intToString(PmeOrder); pmeDefines["PME_ORDER"] = cc.intToString(PmeOrder);
pmeDefines["NUM_ATOMS"] = cc.intToString(numParticles); pmeDefines["NUM_ATOMS"] = cc.intToString(numParticles);
pmeDefines["PADDED_NUM_ATOMS"] = cc.intToString(cc.getPaddedNumAtoms()); pmeDefines["PADDED_NUM_ATOMS"] = cc.intToString(cc.getPaddedNumAtoms());
pmeDefines["NUM_INDICES"] = "0";
pmeDefines["RECIP_EXP_FACTOR"] = cc.doubleToString(M_PI*M_PI/(alpha*alpha)); pmeDefines["RECIP_EXP_FACTOR"] = cc.doubleToString(M_PI*M_PI/(alpha*alpha));
pmeDefines["GRID_SIZE_X"] = cc.intToString(gridSizeX); pmeDefines["GRID_SIZE_X"] = cc.intToString(gridSizeX);
pmeDefines["GRID_SIZE_Y"] = cc.intToString(gridSizeY); pmeDefines["GRID_SIZE_Y"] = cc.intToString(gridSizeY);
...@@ -422,7 +425,7 @@ void CommonCalcNonbondedForceKernel::commonInitialize(const System& system, cons ...@@ -422,7 +425,7 @@ void CommonCalcNonbondedForceKernel::commonInitialize(const System& system, cons
try { try {
cpuPme = getPlatform().createKernel(CalcPmeReciprocalForceKernel::Name(), *cc.getContextImpl()); cpuPme = getPlatform().createKernel(CalcPmeReciprocalForceKernel::Name(), *cc.getContextImpl());
cpuPme.getAs<CalcPmeReciprocalForceKernel>().initialize(gridSizeX, gridSizeY, gridSizeZ, numParticles, alpha, false); cpuPme.getAs<CalcPmeReciprocalForceKernel>().initialize(gridSizeX, gridSizeY, gridSizeZ, numParticles, {}, alpha, false);
ComputeProgram program = cc.compileProgram(CommonKernelSources::pme, pmeDefines); ComputeProgram program = cc.compileProgram(CommonKernelSources::pme, pmeDefines);
ComputeKernel addForcesKernel = program->createKernel("addForces"); ComputeKernel addForcesKernel = program->createKernel("addForces");
pmeio = new PmeIO(cc, addForcesKernel); pmeio = new PmeIO(cc, addForcesKernel);
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment