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
......@@ -75,6 +75,7 @@ OpenCLPlatform::OpenCLPlatform() {
registerKernelFactory(CalcCMAPTorsionForceKernel::Name(), factory);
registerKernelFactory(CalcCustomTorsionForceKernel::Name(), factory);
registerKernelFactory(CalcNonbondedForceKernel::Name(), factory);
registerKernelFactory(CalcConstantPotentialForceKernel::Name(), factory);
registerKernelFactory(CalcCustomNonbondedForceKernel::Name(), factory);
registerKernelFactory(CalcGBSAOBCForceKernel::Name(), factory);
registerKernelFactory(CalcCustomGBForceKernel::Name(), factory);
......
/* -------------------------------------------------------------------------- *
* 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: *
* *
* 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 "OpenCLTests.h"
#include "TestConstantPotentialForce.h"
void platformInitialize() {
}
void runPlatformTests(ConstantPotentialForce::ConstantPotentialMethod method, bool usePreconditioner) {
testEnergyConservation(method, usePreconditioner, 10);
testCompareToReferencePlatform(method, usePreconditioner);
}
#ifndef OPENMM_REFERENCECONSTANTPOTENTIAL_H_
#define OPENMM_REFERENCECONSTANTPOTENTIAL_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) 2025 Stanford University and the Authors. *
* Authors: 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 <array>
#include "ReferenceNeighborList.h"
#include "ReferencePME.h"
#include "tnt_array2d.h"
#include "jama_cholesky.h"
namespace OpenMM {
class ReferenceConstantPotential;
/**
* A generic charge solver for the constant potential method.
*/
class ReferenceConstantPotentialSolver {
protected:
bool valid;
public:
/**
* Creates a ReferenceConstantPotentialSolver.
*/
ReferenceConstantPotentialSolver();
virtual ~ReferenceConstantPotentialSolver();
/**
* Mark precomputed data stored by the solver as invalid due to a change in
* electrode parameters.
*/
void invalidate();
/**
* Updates precomputed data stored by the solver.
*
* @param numParticles the total number of particles
* @param numElectrodeParticles the number of electrode (fluctuating-charge) particles
* @param posData particle positions
* @param charges particle charges
* @param exclusions particle exclusions
* @param sysToElec mapping from system particle indices to electrode particle indices
* @param elecToSys mapping from electrode particle indices to system particle indices
* @param electrodeParamArray electrode particle parameters
* @param conp constant potential derivative evaluation class
*/
virtual void update(int numParticles, int numElectrodeParticles,
const std::vector<Vec3>& posData, const std::vector<double>& charges,
const std::vector<std::set<int>>& exclusions,
const std::vector<int>& sysToElec, const std::vector<int>& elecToSys,
const std::vector<std::array<double, 3> >& electrodeParamArray,
ReferenceConstantPotential& conp) = 0;
/**
* Solves for charges.
*
* @param numParticles the total number of particles
* @param numElectrodeParticles the number of electrode (fluctuating-charge) particles
* @param posData particle positions
* @param charges output particle charges
* @param exclusions particle exclusions
* @param sysToElec mapping from system particle indices to electrode particle indices
* @param elecToSys mapping from electrode particle indices to system particle indices
* @param electrodeParamArray electrode particle parameters
* @param conp constant potential derivative evaluation class
* @param pmeData reference PME solver
*/
virtual void solve(int numParticles, int numElectrodeParticles,
const std::vector<Vec3>& posData, std::vector<double>& charges,
const std::vector<std::set<int>>& exclusions,
const std::vector<int>& sysToElec, const std::vector<int>& elecToSys,
const std::vector<std::array<double, 3> >& electrodeParamArray,
ReferenceConstantPotential& conp, pme_t pmeData) = 0;
};
/**
* A constant potential solver using direct inversion of the Coulomb matrix.
* Suitable only when electrode particle positions are fixed.
*/
class ReferenceConstantPotentialMatrixSolver : public ReferenceConstantPotentialSolver {
private:
Vec3 boxVectors[3];
std::vector<Vec3> electrodePosData;
JAMA::Cholesky<double> capacitance;
std::vector<double> constraintVector;
public:
/**
* Creates a ReferenceConstantPotentialMatrixSolver.
*
* @param numElectrodeParticles the number of electrode (fluctuating-charge) particles
*/
ReferenceConstantPotentialMatrixSolver(int numElectrodeParticles);
/**
* Updates precomputed data stored by the solver.
*
* @param numParticles the total number of particles
* @param numElectrodeParticles the number of electrode (fluctuating-charge) particles
* @param posData particle positions
* @param charges particle charges
* @param exclusions particle exclusions
* @param sysToElec mapping from system particle indices to electrode particle indices
* @param elecToSys mapping from electrode particle indices to system particle indices
* @param electrodeParamArray electrode particle parameters
* @param conp constant potential derivative evaluation class
*/
void update(int numParticles, int numElectrodeParticles,
const std::vector<Vec3>& posData, const std::vector<double>& charges,
const std::vector<std::set<int>>& exclusions,
const std::vector<int>& sysToElec, const std::vector<int>& elecToSys,
const std::vector<std::array<double, 3> >& electrodeParamArray,
ReferenceConstantPotential& conp);
/**
* Solves for charges.
*
* @param numParticles the total number of particles
* @param numElectrodeParticles the number of electrode (fluctuating-charge) particles
* @param posData particle positions
* @param charges output particle charges
* @param exclusions particle exclusions
* @param sysToElec mapping from system particle indices to electrode particle indices
* @param elecToSys mapping from electrode particle indices to system particle indices
* @param electrodeParamArray electrode particle parameters
* @param conp constant potential derivative evaluation class
* @param pmeData reference PME solver
*/
void solve(int numParticles, int numElectrodeParticles,
const std::vector<Vec3>& posData, std::vector<double>& charges,
const std::vector<std::set<int>>& exclusions,
const std::vector<int>& sysToElec, const std::vector<int>& elecToSys,
const std::vector<std::array<double, 3> >& electrodeParamArray,
ReferenceConstantPotential& conp, pme_t pmeData);
};
/**
* A constant potential solver using the conjugate gradient method. Suitable
* for both fixed and variable electrode particle positions.
*/
class ReferenceConstantPotentialCGSolver : public ReferenceConstantPotentialSolver {
private:
Vec3 boxVectors[3];
bool precond;
std::vector<double> precondVector;
public:
/**
* Creates a ReferenceConstantPotentialCGSolver.
*
* @param numElectrodeParticles the number of electrode (fluctuating-charge) particles
* @param precond whether or not to use a preconditioner
*/
ReferenceConstantPotentialCGSolver(int numElectrodeParticles, bool precond);
/**
* Updates precomputed data stored by the solver.
*
* @param numParticles the total number of particles
* @param numElectrodeParticles the number of electrode (fluctuating-charge) particles
* @param posData particle positions
* @param charges particle charges
* @param exclusions particle exclusions
* @param sysToElec mapping from system particle indices to electrode particle indices
* @param elecToSys mapping from electrode particle indices to system particle indices
* @param electrodeParamArray electrode particle parameters
* @param conp constant potential derivative evaluation class
*/
void update(int numParticles, int numElectrodeParticles,
const std::vector<Vec3>& posData, const std::vector<double>& charges,
const std::vector<std::set<int>>& exclusions,
const std::vector<int>& sysToElec, const std::vector<int>& elecToSys,
const std::vector<std::array<double, 3> >& electrodeParamArray,
ReferenceConstantPotential& conp);
/**
* Solves for charges.
*
* @param numParticles the total number of particles
* @param numElectrodeParticles the number of electrode (fluctuating-charge) particles
* @param posData particle positions
* @param charges output particle charges
* @param exclusions particle exclusions
* @param sysToElec mapping from system particle indices to electrode particle indices
* @param elecToSys mapping from electrode particle indices to system particle indices
* @param electrodeParamArray electrode particle parameters
* @param conp constant potential derivative evaluation class
* @param pmeData reference PME solver
*/
void solve(int numParticles, int numElectrodeParticles,
const std::vector<Vec3>& posData, std::vector<double>& charges,
const std::vector<std::set<int>>& exclusions,
const std::vector<int>& sysToElec, const std::vector<int>& elecToSys,
const std::vector<std::array<double, 3> >& electrodeParamArray,
ReferenceConstantPotential& conp, pme_t pmeData);
};
/**
* Performs energy, force, and charge derivative calculations for the reference
* kernel for ConstantPotentialForce.
*/
class ReferenceConstantPotential {
friend class ReferenceConstantPotentialMatrixSolver;
friend class ReferenceConstantPotentialCGSolver;
private:
double nonbondedCutoff, ewaldAlpha, cgErrorTol, chargeTarget;
const NeighborList* neighborList;
Vec3 boxVectors[3], externalField;
bool exceptionsArePeriodic, useChargeConstraint;
int gridSize[3];
static const int PotentialIndex = 0;
static const int GaussianWidthIndex = 1;
static const int ThomasFermiScaleIndex = 2;
public:
/**
* Creates a ReferenceConstantPotential.
*
* @param nonbondedCutoff direct space cutoff
* @param neighborList neighbor list for direct space calculation
* @param boxVectors periodic box vectors
* @param exceptionsArePeriodic whether or not exceptions use periodic boundary conditions
* @param ewaldAlpha Ewald reciprocal Gaussian width parameter
* @param gridSize Ewald mesh dimensions
* @param cgErrorTol constant potential conjugate gradient error tolerance
* @param useChargeConstraint whether or not to constrain total charge
* @param chargeTarget target sum of charges on electrode particles only
* @param externalField electric field vector
*/
ReferenceConstantPotential(double nonbondedCutoff,
const NeighborList* neighborList, const Vec3* boxVectors,
bool exceptionsArePeriodic, double ewaldAlpha, const int* gridSize,
double cgErrorTol, bool useChargeConstraint, double chargeTarget,
Vec3 externalField);
/**
* Solves for charges and computes energies and forces.
*
* @param numParticles the total number of particles
* @param numElectrodeParticles the number of electrode (fluctuating-charge) particles
* @param posData particle positions
* @param forceData output forces on particles
* @param charges output particle charges
* @param exclusions particle exclusions
* @param sysToElec mapping from system particle indices to electrode particle indices
* @param elecToSys mapping from electrode particle indices to system particle indices
* @param electrodeParamArray electrode particle parameters
* @param energy output system energy
* @param solver charge solver implementation
*/
void execute(int numParticles, int numElectrodeParticles,
const std::vector<Vec3>& posData, std::vector<Vec3>& forceData,
std::vector<double>& charges,
const std::vector<std::set<int>>& exclusions,
const std::vector<int>& sysToElec, const std::vector<int>& elecToSys,
const std::vector<std::array<double, 3> >& electrodeParamArray,
double* energy, ReferenceConstantPotentialSolver* solver);
/**
* Solves for charges without computing energies and forces.
*
* @param numParticles the total number of particles
* @param numElectrodeParticles the number of electrode (fluctuating-charge) particles
* @param posData particle positions
* @param charges output particle charges
* @param exclusions particle exclusions
* @param sysToElec mapping from system particle indices to electrode particle indices
* @param elecToSys mapping from electrode particle indices to system particle indices
* @param electrodeParamArray electrode particle parameters
* @param solver charge solver implementation
*/
void getCharges(int numParticles, int numElectrodeParticles,
const std::vector<Vec3>& posData, std::vector<double>& charges,
const std::vector<std::set<int>>& exclusions,
const std::vector<int>& sysToElec, const std::vector<int>& elecToSys,
const std::vector<std::array<double, 3> >& electrodeParamArray,
ReferenceConstantPotentialSolver* solver);
private:
/**
* Computes energies and forces for fixed (solved) charges.
*
* @param numParticles the total number of particles
* @param numElectrodeParticles the number of electrode (fluctuating-charge) particles
* @param posData particle positions
* @param forceData output forces on particles
* @param charges particle charges
* @param exclusions particle exclusions
* @param sysToElec mapping from system particle indices to electrode particle indices
* @param elecToSys mapping from electrode particle indices to system particle indices
* @param electrodeParamArray electrode particle parameters
* @param energy output system energy
* @param pmeData reference PME solver
*/
void getEnergyForces(int numParticles, int numElectrodeParticles,
const std::vector<Vec3>& posData, std::vector<Vec3>& forceData,
std::vector<double>& charges,
const std::vector<std::set<int>>& exclusions,
const std::vector<int>& sysToElec, const std::vector<int>& elecToSys,
const std::vector<std::array<double, 3> >& electrodeParamArray,
double* energy, pme_t pmeData);
/**
* Computes energy derivatives with respect to charges.
*
* @param numParticles the total number of particles
* @param numElectrodeParticles the number of electrode (fluctuating-charge) particles
* @param posData particle positions
* @param charges particle charges
* @param exclusions particle exclusions
* @param sysToElec mapping from system particle indices to electrode particle indices
* @param elecToSys mapping from electrode particle indices to system particle indices
* @param electrodeParamArray electrode particle parameters
* @param chargeDerivatives output charge derivatives
* @param pmeData reference PME solver
*/
void getDerivatives(int numParticles, int numElectrodeParticles,
const std::vector<Vec3>& posData, std::vector<double>& charges,
const std::vector<std::set<int>>& exclusions,
const std::vector<int>& sysToElec, const std::vector<int>& elecToSys,
const std::vector<std::array<double, 3> >& electrodeParamArray,
std::vector<double>& chargeDerivatives, pme_t pmeData);
};
} // namespace OpenMM
#endif // OPENMM_REFERENCECONSTANTPOTENTIAL_H_
#ifndef OPENMM_REFERENCECONSTANTPOTENTIAL14_H_
#define OPENMM_REFERENCECONSTANTPOTENTIAL14_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) 2006-2025 Stanford University and the Authors. *
* Authors: Pande Group, 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 "ReferenceBondIxn.h"
#include "openmm/internal/windowsExport.h"
namespace OpenMM {
class OPENMM_EXPORT ReferenceConstantPotential14 : public ReferenceBondIxn {
public:
ReferenceConstantPotential14();
~ReferenceConstantPotential14();
/**
* Sets the force to use periodic boundary conditions with the specified
* vectors.
*/
void setPeriodic(OpenMM::Vec3* vectors);
/**
* Calculates 1-4 nonbonded interactions (i.e., exceptions with a non-zero
* charge product that should behave effectively as bonded interactions.
* parameters should contain a single item (the charge product).
*/
void calculateBondIxn(std::vector<int>& atomIndices, std::vector<OpenMM::Vec3>& atomCoordinates,
std::vector<double>& parameters, std::vector<OpenMM::Vec3>& forces,
double* totalEnergy, double* energyParamDerivs);
private:
bool periodic;
OpenMM::Vec3 periodicBoxVectors[3];
};
} // namespace OpenMM
#endif // OPENMM_REFERENCECONSTANTPOTENTIAL14_H_
......@@ -38,6 +38,7 @@
#include "openmm/internal/CustomNonbondedForceImpl.h"
#include "openmm/internal/windowsExport.h"
#include "SimTKOpenMMRealType.h"
#include "ReferenceConstantPotential.h"
#include "ReferenceNeighborList.h"
#include "lepton/CompiledExpression.h"
#include "lepton/CustomFunction.h"
......@@ -674,6 +675,78 @@ private:
NeighborList* neighborList;
};
/**
* This kernel is invoked by ConstantPotentialForce to calculate the forces acting on the system and the energy of the system.
*/
class ReferenceCalcConstantPotentialForceKernel : public CalcConstantPotentialForceKernel {
public:
ReferenceCalcConstantPotentialForceKernel(std::string name, const Platform& platform) : CalcConstantPotentialForceKernel(name, platform), neighborList(NULL), solver(NULL) {
}
~ReferenceCalcConstantPotentialForceKernel();
/**
* Initialize the kernel.
*
* @param system the System this kernel will be applied to
* @param force the ConstantPotentialForce this kernel will be used for
*/
void initialize(const System& system, const ConstantPotentialForce& force);
/**
* 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 updateNeighborList(const Vec3* boxVectors, const std::vector<Vec3>& posData);
private:
int numParticles, num14, numElectrodeParticles;
std::vector<double> charges;
std::vector<std::vector<double> > bonded14ParamArray;
std::vector<std::vector<int> > bonded14IndexArray;
std::map<int, int> nb14Index;
std::vector<std::set<int> > exclusions;
std::vector<int> sysToElec, elecToSys;
std::vector<std::array<double, 3> > electrodeParamArray;
double nonbondedCutoff, ewaldAlpha, cgErrorTol, chargeTarget;
int gridSize[3];
bool exceptionsArePeriodic, useChargeConstraint;
Vec3 externalField;
NeighborList* neighborList;
ReferenceConstantPotentialSolver* solver;
};
/**
* This kernel is invoked by CustomNonbondedForce to calculate the forces acting on the system.
*/
......
......@@ -71,12 +71,12 @@ pme_init(pme_t* ppme,
*
* Args:
*
* pme Opaque pme_t object, must have been initialized with pme_init()
* x Pointer to coordinate data array (nm)
* f Pointer to force data array (will be written as kJ/mol/nm)
* charge Array of charges (units of e)
* box Simulation cell dimensions (nm)
* energy Total energy (will be written in units of kJ/mol)
* pme Opaque pme_t object, must have been initialized with pme_init()
* atomCoordinates Pointer to coordinate data array (nm)
* forces Pointer to force data array (will be written as kJ/mol/nm)
* charges Array of charges (units of e)
* periodicBoxVectors Simulation cell dimensions (nm)
* energy Total energy (will be written in units of kJ/mol)
*/
int OPENMM_EXPORT
pme_exec(pme_t pme,
......@@ -86,18 +86,37 @@ pme_exec(pme_t pme,
const OpenMM::Vec3 periodicBoxVectors[3],
double* energy);
/*
* Evaluate reciprocal space PME energy and charge derivatives.
*
* Args:
*
* pme Opaque pme_t object, must have been initialized with pme_init()
* atomCoordinates Pointer to coordinate data array (nm)
* chargeDerivatives Pointer to charge derivative data array (will be written as kJ/mol/e)
* chargeIndices Pointer to array of indices of particles to compute charge derivatives for
* charges Array of charges (units of e)
* periodicBoxVectors Simulation cell dimensions (nm)
*/
int OPENMM_EXPORT
pme_exec_charge_derivatives(pme_t pme,
const std::vector<OpenMM::Vec3>& atomCoordinates,
std::vector<double>& chargeDerivatives,
const std::vector<int>& chargeIndices,
const std::vector<double>& charges,
const OpenMM::Vec3 periodicBoxVectors[3]);
/**
* Evaluate reciprocal space PME dispersion energy and forces.
*
* Args:
*
* pme Opaque pme_t object, must have been initialized with pme_init()
* x Pointer to coordinate data array (nm)
* f Pointer to force data array (will be written as kJ/mol/nm)
* c6s Array of c6 coefficients (units of sqrt(kJ/mol).nm^3 )
* box Simulation cell dimensions (nm)
* energy Total energy (will be written in units of kJ/mol)
* pme Opaque pme_t object, must have been initialized with pme_init()
* atomCoordinates Pointer to coordinate data array (nm)
* forces Pointer to force data array (will be written as kJ/mol/nm)
* c6s Array of c6 coefficients (units of sqrt(kJ/mol).nm^3 )
* periodicBoxVectors Simulation cell dimensions (nm)
* energy Total energy (will be written in units of kJ/mol)
*/
int OPENMM_EXPORT
pme_exec_dpme(pme_t pme,
......@@ -107,9 +126,6 @@ pme_exec_dpme(pme_t pme,
const OpenMM::Vec3 periodicBoxVectors[3],
double* energy);
/* Release all memory in pme structure */
int OPENMM_EXPORT
pme_destroy(pme_t pme);
......
......@@ -48,6 +48,8 @@ KernelImpl* ReferenceKernelFactory::createKernelImpl(std::string name, const Pla
return new ReferenceVirtualSitesKernel(name, platform);
if (name == CalcNonbondedForceKernel::Name())
return new ReferenceCalcNonbondedForceKernel(name, platform);
if (name == CalcConstantPotentialForceKernel::Name())
return new ReferenceCalcConstantPotentialForceKernel(name, platform);
if (name == CalcCustomNonbondedForceKernel::Name())
return new ReferenceCalcCustomNonbondedForceKernel(name, platform);
if (name == CalcHarmonicBondForceKernel::Name())
......
......@@ -36,6 +36,8 @@
#include "ReferenceBondForce.h"
#include "ReferenceBrownianDynamics.h"
#include "ReferenceCMAPTorsionIxn.h"
#include "ReferenceConstantPotential.h"
#include "ReferenceConstantPotential14.h"
#include "ReferenceConstraints.h"
#include "ReferenceCustomAngleIxn.h"
#include "ReferenceCustomBondIxn.h"
......@@ -80,6 +82,7 @@
#include "openmm/internal/CustomHbondForceImpl.h"
#include "openmm/internal/CMAPTorsionForceImpl.h"
#include "openmm/internal/NonbondedForceImpl.h"
#include "openmm/internal/ConstantPotentialForceImpl.h"
#include "openmm/Integrator.h"
#include "openmm/OpenMMException.h"
#include "SimTKOpenMMUtilities.h"
......@@ -1184,6 +1187,239 @@ void ReferenceCalcNonbondedForceKernel::computeParameters(ContextImpl& context)
}
}
ReferenceCalcConstantPotentialForceKernel::~ReferenceCalcConstantPotentialForceKernel() {
if (neighborList != NULL) {
delete neighborList;
}
if (solver != NULL) {
delete solver;
}
}
void ReferenceCalcConstantPotentialForceKernel::initialize(const System& system, const ConstantPotentialForce& force) {
// Get particle parameters.
numParticles = force.getNumParticles();
charges.resize(numParticles);
for (int i = 0; i < numParticles; i++) {
force.getParticleParameters(i, charges[i]);
}
// Get "1-4" exceptions (those that don't zero the charge product).
exclusions.resize(numParticles);
vector<int> nb14s;
for (int i = 0; i < force.getNumExceptions(); i++) {
int particle1, particle2;
double chargeProd;
force.getExceptionParameters(i, particle1, particle2, chargeProd);
exclusions[particle1].insert(particle2);
exclusions[particle2].insert(particle1);
if (chargeProd != 0.0) {
nb14Index[i] = nb14s.size();
nb14s.push_back(i);
}
}
// Get exception parameters.
num14 = nb14s.size();
bonded14ParamArray.resize(num14, vector<double>(1));
bonded14IndexArray.resize(num14, vector<int>(2));
for (int i = 0; i < num14; ++i) {
int particle1, particle2;
force.getExceptionParameters(nb14s[i], particle1, particle2, bonded14ParamArray[i][0]);
bonded14IndexArray[i][0] = particle1;
bonded14IndexArray[i][1] = particle2;
}
// 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.
sysToElec.resize(numParticles, -1);
for (int ie = 0; ie < force.getNumElectrodes(); ie++) {
std::set<int> electrodeParticles;
double potential;
double gaussianWidth;
double thomasFermiScale;
force.getElectrodeParameters(ie, electrodeParticles, potential, gaussianWidth, thomasFermiScale);
for (int i : electrodeParticles) {
sysToElec[i] = electrodeParamArray.size();
elecToSys.push_back(i);
electrodeParamArray.push_back({potential, gaussianWidth, thomasFermiScale});
}
}
// Clear (initial guess) charges on electrode particles.
numElectrodeParticles = elecToSys.size();
for (int ii = 0; ii < numElectrodeParticles; ii++) {
charges[elecToSys[ii]] = 0.0;
}
// Set options from force.
nonbondedCutoff = force.getCutoffDistance();
neighborList = new NeighborList();
ConstantPotentialForceImpl::calcPMEParameters(system, force, ewaldAlpha, gridSize[0], gridSize[1], gridSize[2]);
exceptionsArePeriodic = force.getExceptionsUsePeriodicBoundaryConditions();
cgErrorTol = force.getCGErrorTolerance();
useChargeConstraint = force.getUseChargeConstraint();
chargeTarget = force.getChargeConstraintTarget();
force.getExternalField(externalField);
// 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).
for (int i = 0; i < numParticles; i++) {
if (sysToElec[i] == -1) {
chargeTarget -= charges[i];
}
}
ConstantPotentialForce::ConstantPotentialMethod method = force.getConstantPotentialMethod();
if (method == ConstantPotentialForce::Matrix) {
solver = new ReferenceConstantPotentialMatrixSolver(numElectrodeParticles);
}
else if (method == ConstantPotentialForce::CG) {
solver = new ReferenceConstantPotentialCGSolver(numElectrodeParticles, force.getUsePreconditioner());
}
else {
throw OpenMMException("internal error: invalid constant potential method");
}
}
double ReferenceCalcConstantPotentialForceKernel::execute(ContextImpl& context, bool includeForces, bool includeEnergy) {
Vec3* boxVectors = extractBoxVectors(context);
vector<Vec3>& posData = extractPositions(context);
vector<Vec3>& forceData = extractForces(context);
double energy = 0.0;
// Solve for charges, then calculate forces and energy.
updateNeighborList(boxVectors, posData);
ReferenceConstantPotential conp(nonbondedCutoff, neighborList, boxVectors, exceptionsArePeriodic, ewaldAlpha, gridSize, cgErrorTol, useChargeConstraint, chargeTarget, externalField);
solver->update(numParticles, numElectrodeParticles, posData, charges, exclusions, sysToElec, elecToSys, electrodeParamArray, conp);
conp.execute(numParticles, numElectrodeParticles, posData, forceData, charges, exclusions, sysToElec, elecToSys, electrodeParamArray, includeEnergy ? &energy : NULL, solver);
// Process non-zeroing exceptions. Since exceptions and electrodes should
// involve disjoint sets of atoms, this isn't required for the energy
// minimization with respect to the electrode atom charges.
ReferenceBondForce refBondForce;
ReferenceConstantPotential14 conp14;
if (exceptionsArePeriodic) {
conp14.setPeriodic(boxVectors);
}
refBondForce.calculateForce(num14, bonded14IndexArray, posData, bonded14ParamArray, forceData, includeEnergy ? &energy : NULL, conp14);
return energy;
}
void ReferenceCalcConstantPotentialForceKernel::copyParametersToContext(ContextImpl& context, const ConstantPotentialForce& force, int firstParticle, int lastParticle, int firstException, int lastException, int firstElectrode, int lastElectrode) {
// Get particle parameters.
if (force.getNumParticles() != numParticles) {
throw OpenMMException("updateParametersInContext: The number of particles has changed");
}
for (int i = firstParticle; i <= lastParticle; i++) {
// Only update charges on non-electrode particles; keep current guesses
// for electrode particles.
if (sysToElec[i] == -1) {
force.getParticleParameters(i, charges[i]);
}
}
// Get "1-4" (non-zeroing) exceptions.
vector<int> nb14s;
for (int i = 0; i < force.getNumExceptions(); i++) {
int particle1, particle2;
double chargeProd;
force.getExceptionParameters(i, particle1, particle2, chargeProd);
if (nb14Index.find(i) == nb14Index.end()) {
if (chargeProd != 0.0) {
throw OpenMMException("updateParametersInContext: The set of non-excluded exceptions has changed");
}
}
else {
nb14s.push_back(i);
}
}
if (nb14s.size() != num14) {
throw OpenMMException("updateParametersInContext: The number of non-excluded exceptions has changed");
}
// Get exception parameters.
for (int i = 0; i < num14; i++) {
int particle1, particle2;
force.getExceptionParameters(nb14s[i], particle1, particle2, bonded14ParamArray[i][0]);
bonded14IndexArray[i][0] = particle1;
bonded14IndexArray[i][1] = particle2;
}
// Get electrode parameters.
std::set<int> allElectrodeParticles;
for (int ie = 0; ie < force.getNumElectrodes(); ie++) {
std::set<int> electrodeParticles;
double potential;
double gaussianWidth;
double thomasFermiScale;
force.getElectrodeParameters(ie, electrodeParticles, potential, gaussianWidth, thomasFermiScale);
for (int i : electrodeParticles) {
int ii = sysToElec[i];
if (ii == -1) {
// Particle was not an electrode particle but is now.
throw OpenMMException("updateParametersInContext: The electrode state of a particle has changed");
}
electrodeParamArray[ii][0] = potential;
electrodeParamArray[ii][1] = gaussianWidth;
electrodeParamArray[ii][2] = thomasFermiScale;
allElectrodeParticles.insert(i);
}
}
if (allElectrodeParticles.size() != numElectrodeParticles) {
// Particle that was an electrode particle might not be now.
throw OpenMMException("updateParametersInContext: The electrode state of a particle has changed");
}
// Update external field.
force.getExternalField(externalField);
// Update charge target.
chargeTarget = force.getChargeConstraintTarget();
for (int i = 0; i < numParticles; i++) {
if (sysToElec[i] == -1) {
chargeTarget -= charges[i];
}
}
// Invalidate matrix or CG data if electrode parameters changed.
if (firstElectrode <= lastElectrode) {
solver->invalidate();
}
}
void ReferenceCalcConstantPotentialForceKernel::getPMEParameters(double& alpha, int& nx, int& ny, int& nz) const {
alpha = ewaldAlpha;
nx = gridSize[0];
ny = gridSize[1];
nz = gridSize[2];
}
void ReferenceCalcConstantPotentialForceKernel::getCharges(ContextImpl& context, std::vector<double>& chargesOut) {
Vec3* boxVectors = extractBoxVectors(context);
vector<Vec3>& posData = extractPositions(context);
// Solve for charges only.
updateNeighborList(boxVectors, posData);
ReferenceConstantPotential conp(nonbondedCutoff, neighborList, boxVectors, exceptionsArePeriodic, ewaldAlpha, gridSize, cgErrorTol, useChargeConstraint, chargeTarget, externalField);
solver->update(numParticles, numElectrodeParticles, posData, charges, exclusions, sysToElec, elecToSys, electrodeParamArray, conp);
conp.getCharges(numParticles, numElectrodeParticles, posData, charges, exclusions, sysToElec, elecToSys, electrodeParamArray, solver);
chargesOut = charges;
}
void ReferenceCalcConstantPotentialForceKernel::updateNeighborList(const Vec3* boxVectors, const std::vector<Vec3>& posData) {
double minAllowedSize = 1.999999*nonbondedCutoff;
if (boxVectors[0][0] < minAllowedSize || boxVectors[1][1] < minAllowedSize || boxVectors[2][2] < minAllowedSize) {
throw OpenMMException("The periodic box size has decreased to less than twice the nonbonded cutoff.");
}
computeNeighborListVoxelHash(*neighborList, numParticles, posData, exclusions, boxVectors, true, nonbondedCutoff, 0.0);
}
ReferenceCalcCustomNonbondedForceKernel::~ReferenceCalcCustomNonbondedForceKernel() {
if (neighborList != NULL)
delete neighborList;
......
......@@ -55,6 +55,7 @@ ReferencePlatform::ReferencePlatform() {
registerKernelFactory(CalcCMAPTorsionForceKernel::Name(), factory);
registerKernelFactory(CalcCustomTorsionForceKernel::Name(), factory);
registerKernelFactory(CalcNonbondedForceKernel::Name(), factory);
registerKernelFactory(CalcConstantPotentialForceKernel::Name(), factory);
registerKernelFactory(CalcCustomNonbondedForceKernel::Name(), factory);
registerKernelFactory(CalcGBSAOBCForceKernel::Name(), factory);
registerKernelFactory(CalcCustomGBForceKernel::Name(), factory);
......
/* -------------------------------------------------------------------------- *
* 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) 2025 Stanford University and the Authors. *
* Authors: 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 "ReferenceConstantPotential.h"
#include "ReferenceForce.h"
#include "ReferencePME.h"
#include "SimTKOpenMMUtilities.h"
#include "openmm/OpenMMException.h"
#include "openmm/internal/MSVC_erfc.h"
using namespace OpenMM;
ReferenceConstantPotentialSolver::ReferenceConstantPotentialSolver() : valid(false) {
}
ReferenceConstantPotentialSolver::~ReferenceConstantPotentialSolver() {
}
void ReferenceConstantPotentialSolver::invalidate() {
valid = false;
}
ReferenceConstantPotentialMatrixSolver::ReferenceConstantPotentialMatrixSolver(int numElectrodeParticles) : ReferenceConstantPotentialSolver(),
electrodePosData(numElectrodeParticles), constraintVector(numElectrodeParticles) {
}
void ReferenceConstantPotentialMatrixSolver::update(
int numParticles,
int numElectrodeParticles,
const std::vector<Vec3>& posData,
const std::vector<double>& charges,
const std::vector<std::set<int>>& exclusions,
const std::vector<int>& sysToElec,
const std::vector<int>& elecToSys,
const std::vector<std::array<double, 3> >& electrodeParamArray,
ReferenceConstantPotential& conp
) {
// 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] != conp.boxVectors[0] || boxVectors[1] != conp.boxVectors[1] || boxVectors[2] != conp.boxVectors[2]) {
valid = false;
}
}
if (valid) {
for (int ii = 0; ii < numElectrodeParticles; ii++) {
if (electrodePosData[ii] != posData[elecToSys[ii]]) {
valid = false;
break;
}
}
}
if (valid) {
return;
}
// Store the current box vectors and electrode positions before updating the
// capacitance matrix.
valid = true;
boxVectors[0] = conp.boxVectors[0];
boxVectors[1] = conp.boxVectors[1];
boxVectors[2] = conp.boxVectors[2];
for (int ii = 0; ii < numElectrodeParticles; ii++) {
electrodePosData[ii] = posData[elecToSys[ii]];
}
TNT::Array2D<double> A(numElectrodeParticles, numElectrodeParticles);
std::vector<double> dUdQ0(numElectrodeParticles);
std::vector<double> dUdQ(numElectrodeParticles);
pme_t pmeData;
pme_init(&pmeData, conp.ewaldAlpha, numParticles, conp.gridSize, 5, 1);
// Get derivatives when all electrode charges are zeroed.
std::vector<double> q(charges);
for (int ii = 0; ii < numElectrodeParticles; ii++) {
q[elecToSys[ii]] = 0.0;
}
conp.getDerivatives(numParticles, numElectrodeParticles, posData, q, exclusions, sysToElec, elecToSys, electrodeParamArray, dUdQ0, pmeData);
for (int ii = 0; ii < numElectrodeParticles; ii++) {
int i = elecToSys[ii];
// Get derivatives when one electrode charge is set.
q[i] = 1.0;
conp.getDerivatives(numParticles, numElectrodeParticles, posData, q, exclusions, sysToElec, elecToSys, electrodeParamArray, dUdQ, pmeData);
q[i] = 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];
}
pme_destroy(pmeData);
// Compute Cholesky decomposition representation of the inverse.
capacitance = JAMA::Cholesky<double>(A);
if (!capacitance.is_spd()) {
throw OpenMMException("Electrode matrix not positive definite");
}
// 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 (conp.useChargeConstraint) {
TNT::Array1D<double> solution = capacitance.solve(TNT::Array1D<double>(numElectrodeParticles, 1.0));
constraintVector.assign(static_cast<double*>(solution), static_cast<double*>(solution) + numElectrodeParticles);
double constraintScaleInv = 0.0;
for (int ii = 0; ii < numElectrodeParticles; ii++) {
constraintScaleInv += constraintVector[ii];
}
double constraintScale = 1.0 / constraintScaleInv;
for (int ii = 0; ii < numElectrodeParticles; ii++) {
constraintVector[ii] *= constraintScale;
}
}
}
void ReferenceConstantPotentialMatrixSolver::solve(
int numParticles,
int numElectrodeParticles,
const std::vector<Vec3>& posData,
std::vector<double>& charges,
const std::vector<std::set<int>>& exclusions,
const std::vector<int>& sysToElec,
const std::vector<int>& elecToSys,
const std::vector<std::array<double, 3> >& electrodeParamArray,
ReferenceConstantPotential& conp,
pme_t pmeData
) {
// Solves for charges using the matrix method.
// Zero electrode charges and get derivatives at zero charge.
for (int ii = 0; ii < numElectrodeParticles; ii++) {
charges[elecToSys[ii]] = 0.0;
}
std::vector<double> b(numElectrodeParticles);
conp.getDerivatives(numParticles, numElectrodeParticles, posData, charges, exclusions, sysToElec, elecToSys, electrodeParamArray, b, pmeData);
for (int ii = 0; ii < numElectrodeParticles; ii++) {
b[ii] = -b[ii];
}
// Solve for electrode charges directly using capacitance matrix and
// calculated derivatives.
TNT::Array1D<double> q = capacitance.solve(TNT::Array1D<double>(numElectrodeParticles, b.data()));
for (int ii = 0; ii < numElectrodeParticles; ii++) {
charges[elecToSys[ii]] = q[ii];
}
// Enforce total charge constraint if requested.
if (conp.useChargeConstraint) {
double chargeOffset = conp.chargeTarget;
for (int ii = 0; ii < numElectrodeParticles; ii++) {
chargeOffset -= q[ii];
}
for (int ii = 0; ii < numElectrodeParticles; ii++) {
charges[elecToSys[ii]] += chargeOffset * constraintVector[ii];
}
}
}
ReferenceConstantPotentialCGSolver::ReferenceConstantPotentialCGSolver(int numElectrodeParticles, bool precond) : ReferenceConstantPotentialSolver(),
precond(precond), precondVector(numElectrodeParticles) {
}
void ReferenceConstantPotentialCGSolver::update(
int numParticles,
int numElectrodeParticles,
const std::vector<Vec3>& posData,
const std::vector<double>& charges,
const std::vector<std::set<int>>& exclusions,
const std::vector<int>& sysToElec,
const std::vector<int>& elecToSys,
const std::vector<std::array<double, 3> >& electrodeParamArray,
ReferenceConstantPotential& conp
) {
// 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.
const double SQRT_PI = sqrt(PI_M);
const double SELF_ALPHA_SCALE = 2.0 * ONE_4PI_EPS0 / SQRT_PI;
const double SELF_ETA_SCALE = SELF_ALPHA_SCALE / sqrt(2.0);
const double TF_SCALE = 1.0 / EPSILON0;
const double PLASMA_SCALE = TF_SCALE / 4.0;
// No action is required if the box vectors have not changed.
if (valid && boxVectors[0] == conp.boxVectors[0] && boxVectors[1] == conp.boxVectors[1] && boxVectors[2] == conp.boxVectors[2]) {
return;
}
valid = true;
boxVectors[0] = conp.boxVectors[0];
boxVectors[1] = conp.boxVectors[1];
boxVectors[2] = conp.boxVectors[2];
if (precond) {
// 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.
pme_t pmeData;
pme_init(&pmeData, conp.ewaldAlpha, 1, conp.gridSize, 5, 1);
std::vector<Vec3> pmePosData(1);
std::vector<double> pmeChargeDerivatives(1);
std::vector<int> pmeElectrodeIndices(1);
std::vector<double> pmeCharges(1, 1.0);
pme_exec_charge_derivatives(pmeData, pmePosData, pmeChargeDerivatives, pmeElectrodeIndices, pmeCharges, boxVectors);
pme_destroy(pmeData);
// The diagonal has a contribution from reciprocal space, Ewald
// self-interaction, Ewald neutralizing plasma, Gaussian self-interaction,
// and Thomas-Fermi contributions.
double volume = boxVectors[0][0] * boxVectors[1][1] * boxVectors[2][2];
double ewaldTerm = pmeChargeDerivatives[0] - SELF_ALPHA_SCALE * conp.ewaldAlpha - PLASMA_SCALE / (volume * conp.ewaldAlpha * conp.ewaldAlpha);
double precondScaleInv = 0.0;
for (int ii = 0; ii < numElectrodeParticles; ii++) {
precondVector[ii] = 1.0 / (SELF_ETA_SCALE / electrodeParamArray[ii][conp.GaussianWidthIndex]
+ TF_SCALE * electrodeParamArray[ii][conp.ThomasFermiScaleIndex] + ewaldTerm);
precondScaleInv += precondVector[ii];
}
double precondScale = 1.0 / precondScaleInv;
for (int ii = 0; ii < numElectrodeParticles; ii++) {
precondVector[ii] *= precondScale;
}
}
}
void ReferenceConstantPotentialCGSolver::solve(
int numParticles,
int numElectrodeParticles,
const std::vector<Vec3>& posData,
std::vector<double>& charges,
const std::vector<std::set<int>>& exclusions,
const std::vector<int>& sysToElec,
const std::vector<int>& elecToSys,
const std::vector<std::array<double, 3> >& electrodeParamArray,
ReferenceConstantPotential& conp,
pme_t pmeData
) {
// Solves for charges using the conjugate gradient method.
std::vector<double> q(numElectrodeParticles);
std::vector<double> grad(numElectrodeParticles);
std::vector<double> projGrad(numElectrodeParticles);
std::vector<double> precGrad(numElectrodeParticles);
std::vector<double> qStep(numElectrodeParticles);
std::vector<double> gradStep(numElectrodeParticles);
std::vector<double> grad0(numElectrodeParticles);
double offset, error, paramScale, alpha, beta;
const double errorTarget = conp.cgErrorTol * conp.cgErrorTol * numElectrodeParticles;
// Ensure that initial guess charges satisfy the constraint.
if (conp.useChargeConstraint) {
offset = conp.chargeTarget;
for (int ii = 0; ii < numElectrodeParticles; ii++) {
offset -= charges[elecToSys[ii]];
}
offset /= numElectrodeParticles;
for (int ii = 0; ii < numElectrodeParticles; ii++) {
charges[elecToSys[ii]] += offset;
}
}
// Evaluate the initial gradient Aq - b.
conp.getDerivatives(numParticles, numElectrodeParticles, posData, charges, exclusions, sysToElec, elecToSys, electrodeParamArray, grad, pmeData);
// Project the initial gradient without preconditioning.
offset = 0.0;
if (conp.useChargeConstraint) {
for (int ii = 0; ii < numElectrodeParticles; ii++) {
offset += grad[ii];
}
offset /= numElectrodeParticles;
}
for (int ii = 0; ii < numElectrodeParticles; ii++) {
projGrad[ii] = grad[ii] - offset;
}
// Check for convergence at the initial guess charges.
error = 0.0;
for (int ii = 0; ii < numElectrodeParticles; ii++) {
error += projGrad[ii] * projGrad[ii];
}
if (error <= errorTarget) {
return;
}
// Save the current charges, then evaluate the gradient with zero
// charges (-b) so that we can later compute Ap as (Ap - b) - (-b).
for (int ii = 0; ii < numElectrodeParticles; ii++) {
int i = elecToSys[ii];
q[ii] = charges[i];
charges[i] = 0.0;
}
conp.getDerivatives(numParticles, numElectrodeParticles, posData, charges, exclusions, sysToElec, elecToSys, electrodeParamArray, grad0, pmeData);
// Project the initial gradient with preconditioning.
if (precond) {
offset = 0.0;
if (conp.useChargeConstraint) {
for (int ii = 0; ii < numElectrodeParticles; ii++) {
offset += precondVector[ii] * grad[ii];
}
}
for (int ii = 0; ii < numElectrodeParticles; ii++) {
precGrad[ii] = precondVector[ii] * (grad[ii] - offset);
}
}
else {
precGrad.assign(projGrad.begin(), projGrad.end());
}
// Initialize step vector for conjugate gradient iterations.
for (int ii = 0; ii < numElectrodeParticles; ii++) {
qStep[ii] = -precGrad[ii];
}
// Perform conjugate gradient iterations.
bool converged = false;
for (int iter = 0; iter <= numElectrodeParticles; iter++) {
// Evaluate the matrix-vector product A qStep.
for (int ii = 0; ii < numElectrodeParticles; ii++) {
charges[elecToSys[ii]] = qStep[ii];
}
conp.getDerivatives(numParticles, numElectrodeParticles, posData, charges, exclusions, sysToElec, elecToSys, electrodeParamArray, gradStep, pmeData);
for (int ii = 0; ii < numElectrodeParticles; ii++) {
gradStep[ii] -= grad0[ii];
}
// If A qStep is small enough, stop to prevent, e.g., division by
// zero in the calculation of alpha, or too large step sizes.
error = 0.0;
for (int ii = 0; ii < numElectrodeParticles; ii++) {
error += gradStep[ii] * gradStep[ii];
}
if (error <= errorTarget) {
converged = true;
break;
}
// Evaluate the scalar 1 / (qStep^T A qStep).
paramScale = 0.0;
for (int ii = 0; ii < numElectrodeParticles; ii++) {
paramScale += qStep[ii] * gradStep[ii];
}
paramScale = 1.0 / paramScale;
// Evaluate the conjugate gradient parameter alpha.
alpha = 0.0;
for (int ii = 0; ii < numElectrodeParticles; ii++) {
alpha -= qStep[ii] * grad[ii];
}
alpha *= paramScale;
// Update the charge vector.
for (int ii = 0; ii < numElectrodeParticles; ii++) {
q[ii] += alpha * qStep[ii];
}
if (conp.useChargeConstraint) {
// Remove any accumulated drift from the charge vector. This
// would be zero in exact arithmetic, but error can accumulate
// over time in finite precision.
offset = conp.chargeTarget;
for (int ii = 0; ii < numElectrodeParticles; ii++) {
offset -= q[ii];
}
offset /= numElectrodeParticles;
for (int ii = 0; ii < numElectrodeParticles; ii++) {
q[ii] += offset;
}
}
// Update the gradient vector (but periodically recompute it instead
// of updating to reduce the accumulation of roundoff error).
if (iter != 0 && iter % 32 == 0) {
for (int ii = 0; ii < numElectrodeParticles; ii++) {
charges[elecToSys[ii]] = q[ii];
}
conp.getDerivatives(numParticles, numElectrodeParticles, posData, charges, exclusions, sysToElec, elecToSys, electrodeParamArray, grad, pmeData);
}
else {
for (int ii = 0; ii < numElectrodeParticles; ii++) {
grad[ii] += alpha * gradStep[ii];
}
}
// Project the current gradient without preconditioning.
offset = 0.0;
if (conp.useChargeConstraint) {
for (int ii = 0; ii < numElectrodeParticles; ii++) {
offset += grad[ii];
}
offset /= numElectrodeParticles;
}
for (int ii = 0; ii < numElectrodeParticles; ii++) {
projGrad[ii] = grad[ii] - offset;
}
// Check for convergence.
error = 0.0;
for (int ii = 0; ii < numElectrodeParticles; ii++) {
error += projGrad[ii] * projGrad[ii];
}
if (error <= errorTarget) {
converged = true;
break;
}
// Project the current gradient with preconditioning.
if (precond) {
offset = 0.0;
if (conp.useChargeConstraint) {
for (int ii = 0; ii < numElectrodeParticles; ii++) {
offset += precondVector[ii] * grad[ii];
}
}
for (int ii = 0; ii < numElectrodeParticles; ii++) {
precGrad[ii] = precondVector[ii] * (grad[ii] - offset);
}
}
else {
precGrad.assign(projGrad.begin(), projGrad.end());
}
// Evaluate the conjugate gradient parameter beta.
beta = 0.0;
for (int ii = 0; ii < numElectrodeParticles; ii++) {
beta += precGrad[ii] * gradStep[ii];
}
beta *= paramScale;
// Update the step vector.
for (int ii = 0; ii < numElectrodeParticles; ii++) {
qStep[ii] = beta * qStep[ii] - precGrad[ii];
}
if (conp.useChargeConstraint) {
// Project out any deviation off of the constraint plane from
// the step vector. This would be zero in exact arithmetic, but
// error can accumulate over time in finite precision.
offset = 0.0;
for (int ii = 0; ii < numElectrodeParticles; ii++) {
offset += qStep[ii];
}
offset /= numElectrodeParticles;
for (int ii = 0; ii < numElectrodeParticles; ii++) {
qStep[ii] -= offset;
}
}
}
if (!converged) {
throw OpenMMException("Constant potential conjugate gradient iterations not converged");
}
// Store the final charges.
for (int ii = 0; ii < numElectrodeParticles; ii++) {
charges[elecToSys[ii]] = q[ii];
}
}
ReferenceConstantPotential::ReferenceConstantPotential(
double nonbondedCutoff,
const NeighborList* neighborList,
const Vec3* boxVectors,
bool exceptionsArePeriodic,
double ewaldAlpha,
const int* gridSize,
double cgErrorTol,
bool useChargeConstraint,
double chargeTarget,
Vec3 externalField
) : nonbondedCutoff(nonbondedCutoff),
neighborList(neighborList),
exceptionsArePeriodic(exceptionsArePeriodic),
ewaldAlpha(ewaldAlpha),
cgErrorTol(cgErrorTol),
useChargeConstraint(useChargeConstraint),
chargeTarget(chargeTarget),
externalField(externalField)
{
this->boxVectors[0] = boxVectors[0];
this->boxVectors[1] = boxVectors[1];
this->boxVectors[2] = boxVectors[2];
this->gridSize[0] = gridSize[0];
this->gridSize[1] = gridSize[1];
this->gridSize[2] = gridSize[2];
}
void ReferenceConstantPotential::execute(
int numParticles,
int numElectrodeParticles,
const std::vector<Vec3>& posData,
std::vector<Vec3>& forceData,
std::vector<double>& charges,
const std::vector<std::set<int>>& exclusions,
const std::vector<int>& sysToElec,
const std::vector<int>& elecToSys,
const std::vector<std::array<double, 3> >& electrodeParamArray,
double* energy,
ReferenceConstantPotentialSolver* solver
) {
pme_t pmeData;
pme_init(&pmeData, ewaldAlpha, numParticles, gridSize, 5, 1);
solver->solve(numParticles, numElectrodeParticles, posData, charges, exclusions, sysToElec, elecToSys, electrodeParamArray, *this, pmeData);
getEnergyForces(numParticles, numElectrodeParticles, posData, forceData, charges, exclusions, sysToElec, elecToSys, electrodeParamArray, energy, pmeData);
pme_destroy(pmeData);
}
void ReferenceConstantPotential::getCharges(
int numParticles,
int numElectrodeParticles,
const std::vector<Vec3>& posData,
std::vector<double>& charges,
const std::vector<std::set<int>>& exclusions,
const std::vector<int>& sysToElec,
const std::vector<int>& elecToSys,
const std::vector<std::array<double, 3> >& electrodeParamArray,
ReferenceConstantPotentialSolver* solver
) {
pme_t pmeData;
pme_init(&pmeData, ewaldAlpha, numParticles, gridSize, 5, 1);
solver->solve(numParticles, numElectrodeParticles, posData, charges, exclusions, sysToElec, elecToSys, electrodeParamArray, *this, pmeData);
pme_destroy(pmeData);
}
void ReferenceConstantPotential::getEnergyForces(
int numParticles,
int numElectrodeParticles,
const std::vector<Vec3>& posData,
std::vector<Vec3>& forceData,
std::vector<double>& charges,
const std::vector<std::set<int>>& exclusions,
const std::vector<int>& sysToElec,
const std::vector<int>& elecToSys,
const std::vector<std::array<double, 3> >& electrodeParamArray,
double* energy,
pme_t pmeData
) {
const double SQRT_PI = sqrt(PI_M);
const double TWO_OVER_SQRT_PI = 2.0 / SQRT_PI;
const double SELF_ALPHA_SCALE = ONE_4PI_EPS0 / SQRT_PI;
const double SELF_ETA_SCALE = SELF_ALPHA_SCALE / sqrt(2.0);
const double TF_SCALE = 1.0 / (2.0 * EPSILON0);
const double PLASMA_SCALE = TF_SCALE / 4.0;
double energyAccum = 0.0;
// Direct space.
for (auto& pair : *neighborList) {
int i = pair.first;
int j = pair.second;
int ii = sysToElec[i];
int jj = sysToElec[j];
double iWidth = ii == -1 ? 0.0 : electrodeParamArray[ii][GaussianWidthIndex];
double jWidth = jj == -1 ? 0.0 : electrodeParamArray[jj][GaussianWidthIndex];
double width = sqrt(iWidth * iWidth + jWidth * jWidth);
double deltaR[ReferenceForce::LastDeltaRIndex];
ReferenceForce::getDeltaRPeriodic(posData[i], posData[j], boxVectors, deltaR);
double r = deltaR[ReferenceForce::RIndex];
double inverseR = 1.0 / r;
double alphaR = ewaldAlpha * r;
double erfcAlphaR = erfc(alphaR);
double etaR = 0.0;
double erfcEtaR = 0.0;
double expEtaRTerm = 0.0;
if (width != 0.0) {
etaR = r / width;
erfcEtaR = erfc(etaR);
expEtaRTerm = etaR * exp(-etaR * etaR);
}
double qqFactor = ONE_4PI_EPS0 * charges[i] * charges[j];
double forceFactor = qqFactor * inverseR * inverseR * inverseR * (erfcAlphaR - erfcEtaR + TWO_OVER_SQRT_PI * (alphaR * exp(-alphaR * alphaR) - expEtaRTerm));
for (int k = 0; k < 3; k++) {
double force = forceFactor * deltaR[k];
forceData[i][k] -= force;
forceData[j][k] += force;
}
energyAccum += qqFactor * inverseR * (erfcAlphaR - erfcEtaR);
}
// Exceptions.
for (int i = 0; i < numParticles; i++) {
for (int j : exclusions[i]) {
if (j <= i) {
continue;
}
double deltaR[ReferenceForce::LastDeltaRIndex];
if (exceptionsArePeriodic) {
ReferenceForce::getDeltaRPeriodic(posData[i], posData[j], boxVectors, deltaR);
}
else {
ReferenceForce::getDeltaR(posData[i], posData[j], deltaR);
}
double r = deltaR[ReferenceForce::RIndex];
double inverseR = 1.0 / r;
double qqFactor = ONE_4PI_EPS0 * charges[i] * charges[j];
double alphaR = ewaldAlpha * r;
if (alphaR > 1e-6) {
double erfAlphaR = erf(alphaR);
double forceFactor = qqFactor * inverseR * inverseR * inverseR * (erfAlphaR - TWO_OVER_SQRT_PI * alphaR * exp(-alphaR * alphaR));
for (int k = 0; k < 3; k++) {
double force = forceFactor * deltaR[k];
forceData[i][k] += force;
forceData[j][k] -= force;
}
energyAccum -= qqFactor * inverseR * erfAlphaR;
}
else {
energyAccum -= qqFactor * TWO_OVER_SQRT_PI * ewaldAlpha;
}
}
}
// Reciprocal space.
double pmeEnergy = 0.0;
pme_exec(pmeData, posData, forceData, charges, boxVectors, &pmeEnergy);
energyAccum += pmeEnergy;
// Ewald self-interaction and external field contributions (all particles).
double qTotal = 0.0;
for (int i = 0; i < numParticles; i++) {
double q = charges[i];
qTotal += q;
energyAccum -= q * (SELF_ALPHA_SCALE * q * ewaldAlpha + posData[i].dot(externalField));
forceData[i] += q * externalField;
}
// Ewald neutralizing plasma.
double volume = boxVectors[0][0] * boxVectors[1][1] * boxVectors[2][2];
energyAccum -= PLASMA_SCALE * qTotal * qTotal / (volume * ewaldAlpha * ewaldAlpha);
// Gaussian self-interaction, potential, and Thomas-Fermi contributions
// (electrode particles only).
for (int ii = 0; ii < numElectrodeParticles; ii++) {
int i = elecToSys[ii];
double q = charges[i];
energyAccum += q * (q * (SELF_ETA_SCALE / electrodeParamArray[ii][GaussianWidthIndex] + TF_SCALE * electrodeParamArray[ii][ThomasFermiScaleIndex]) - electrodeParamArray[ii][PotentialIndex]);
}
if (energy) {
*energy += energyAccum;
}
}
void ReferenceConstantPotential::getDerivatives(
int numParticles,
int numElectrodeParticles,
const std::vector<Vec3>& posData,
std::vector<double>& charges,
const std::vector<std::set<int>>& exclusions,
const std::vector<int>& sysToElec,
const std::vector<int>& elecToSys,
const std::vector<std::array<double, 3> >& electrodeParamArray,
std::vector<double>& chargeDerivatives,
pme_t pmeData
) {
const double SQRT_PI = sqrt(PI_M);
const double SELF_ALPHA_SCALE = 2.0 * ONE_4PI_EPS0 / SQRT_PI;
const double SELF_ETA_SCALE = SELF_ALPHA_SCALE / sqrt(2.0);
const double TF_SCALE = 1.0 / EPSILON0;
const double PLASMA_SCALE = TF_SCALE / 4.0;
// chargeDerivatives is to be overwritten by this function, not incremented,
// so zero all derivatives initially.
for (int ii = 0; ii < numElectrodeParticles; ii++) {
chargeDerivatives[ii] = 0.0;
}
// Direct space (both particles in each exception will be non-electrode, so
// we do not need to loop over exceptions when computing derivatives with
// respect to electrode charges).
for (auto& pair : *neighborList) {
int i = pair.first;
int j = pair.second;
int ii = sysToElec[i];
int jj = sysToElec[j];
if (ii == -1 && jj == -1) {
continue;
}
double iWidth = ii == -1 ? 0.0 : electrodeParamArray[ii][GaussianWidthIndex];
double jWidth = jj == -1 ? 0.0 : electrodeParamArray[jj][GaussianWidthIndex];
double width = sqrt(iWidth * iWidth + jWidth * jWidth);
double deltaR[ReferenceForce::LastDeltaRIndex];
ReferenceForce::getDeltaRPeriodic(posData[i], posData[j], boxVectors, deltaR);
double r = deltaR[ReferenceForce::RIndex];
double erfcEtaR = width == 0.0 ? 0.0 : erfc(r / width);
double factor = ONE_4PI_EPS0 * (erfc(ewaldAlpha * r) - erfcEtaR) / r;
if (ii != -1) {
chargeDerivatives[ii] += charges[j] * factor;
}
if (jj != -1) {
chargeDerivatives[jj] += charges[i] * factor;
}
}
// Reciprocal space.
pme_exec_charge_derivatives(pmeData, posData, chargeDerivatives, elecToSys, charges, boxVectors);
// Ewald neutralizing plasma precalculation.
double qTotal = 0.0;
for (int i = 0; i < numParticles; i++) {
qTotal += charges[i];
}
double volume = boxVectors[0][0] * boxVectors[1][1] * boxVectors[2][2];
double plasmaTerm = PLASMA_SCALE * qTotal / (volume * ewaldAlpha * ewaldAlpha);
// Ewald self-interaction, Ewald neutralizing plasma, Gaussian
// self-interaction, potential, external field, and Thomas-Fermi
// contributions.
for (int ii = 0; ii < numElectrodeParticles; ii++) {
int i = elecToSys[ii];
double q = charges[i];
chargeDerivatives[ii] += q * (SELF_ETA_SCALE / electrodeParamArray[ii][GaussianWidthIndex] - SELF_ALPHA_SCALE * ewaldAlpha + TF_SCALE * electrodeParamArray[ii][ThomasFermiScaleIndex])
- plasmaTerm - electrodeParamArray[ii][PotentialIndex] - posData[i].dot(externalField);
}
}
/* -------------------------------------------------------------------------- *
* 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) 2006-2025 Stanford University and the Authors. *
* Authors: Pande Group, 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 "SimTKOpenMMUtilities.h"
#include "ReferenceConstantPotential14.h"
#include "ReferenceForce.h"
using std::vector;
using namespace OpenMM;
ReferenceConstantPotential14::ReferenceConstantPotential14() : periodic(false) {
}
ReferenceConstantPotential14::~ReferenceConstantPotential14() {
}
void ReferenceConstantPotential14::setPeriodic(OpenMM::Vec3* vectors) {
periodic = true;
periodicBoxVectors[0] = vectors[0];
periodicBoxVectors[1] = vectors[1];
periodicBoxVectors[2] = vectors[2];
}
void ReferenceConstantPotential14::calculateBondIxn(
vector<int>& atomIndices, vector<Vec3>& atomCoordinates,
vector<double>& parameters, vector<Vec3>& forces,
double* totalEnergy, double* energyParamDerivs
) {
double deltaR[ReferenceForce::LastDeltaRIndex];
int atomAIndex = atomIndices[0];
int atomBIndex = atomIndices[1];
if (periodic) {
ReferenceForce::getDeltaRPeriodic(atomCoordinates[atomBIndex], atomCoordinates[atomAIndex], periodicBoxVectors, deltaR);
}
else {
ReferenceForce::getDeltaR(atomCoordinates[atomBIndex], atomCoordinates[atomAIndex], deltaR);
}
double inverseR = 1.0 / deltaR[ReferenceForce::RIndex];
double energy = ONE_4PI_EPS0 * parameters[0] * inverseR;
double dEdR = energy * inverseR * inverseR;
for (int ii = 0; ii < 3; ii++) {
double force = dEdR * deltaR[ii];
forces[atomAIndex][ii] += force;
forces[atomBIndex][ii] -= force;
}
if (totalEnergy != NULL) {
*totalEnergy += energy;
}
}
/*
* Reference implementation of PME reciprocal space interactions.
*
* Copyright (c) 2009-2023, Erik Lindahl, Rossen Apostolov, Szilard Pall, Peter Eastman
* Copyright (c) 2009-2025, Erik Lindahl, Rossen Apostolov, Szilard Pall, Peter Eastman, Evan Pretti
* All rights reserved.
* Contact: lindahl@cbr.su.se Stockholm University, Sweden.
*
......@@ -459,16 +459,18 @@ pme_reciprocal_convolution(pme_t pme,
for (kz=0;kz<nz;kz++)
{
/* If the net charge of the system is 0.0, there will not be any DC (direct current, zero frequency) component. However,
* we can still handle charged systems through a charge correction, in which case the DC
* component should be excluded from recprocal space. We will anyway run into problems below when dividing with the
* frequency if it is zero...
*
* In cuda you could probably work around this by setting something to 0.0 instead, but the short story is that we
* should skip the zero frequency case!
/* Pointer to the grid cell in question */
ptr = pme->grid + kx*ny*nz + ky*nz + kz;
/* The zero frequency term is undefined due to division by the frequency below. Set this term to zero;
* in the case that the net charge of the system is non-zero, this is equivalent to applying a uniform
* neutralizing background charge density. The contribution to the energy and charge derivatives of
* this neutralizing plasma is applied elsewhere. If this term is not zeroed, however, energies and
* forces will be unaffected but charge derivatives for non-neutral systems will be incorrect!
*/
if (kx==0 && ky==0 && kz==0)
{
*ptr = 0;
continue;
}
......@@ -476,9 +478,6 @@ pme_reciprocal_convolution(pme_t pme,
mz = (kz<maxkz) ? kz : (kz-nz);
mhz = mx*recipBoxVectors[2][0]+my*recipBoxVectors[2][1]+mz*recipBoxVectors[2][2];
/* Pointer to the grid cell in question */
ptr = pme->grid + kx*ny*nz + ky*nz + kz;
/* Get grid data for this frequency */
d1 = ptr->real();
d2 = ptr->imag();
......@@ -707,6 +706,95 @@ pme_grid_interpolate_force(pme_t pme,
}
static void
pme_grid_interpolate_charge_derivatives(pme_t pme,
const Vec3 recipBoxVectors[3],
const vector<double>& charges,
vector<double>& chargeDerivatives,
const vector<int>& chargeIndices)
{
int nderiv, ideriv, i;
int ix,iy,iz;
int x0index,y0index,z0index;
int xindex,yindex,zindex;
int index;
int order;
double q;
double * thetax;
double * thetay;
double * thetaz;
double tx,ty,tz;
double dq;
double gridvalue;
int nx,ny,nz;
nx = pme->ngrid[0];
ny = pme->ngrid[1];
nz = pme->ngrid[2];
order = pme->order;
/* This is similar to pme_grid_interpolate_force() */
nderiv = chargeIndices.size();
for (ideriv=0;ideriv<nderiv;ideriv++)
{
i = chargeIndices[ideriv];
dq = 0;
q = charges[i];
/* Grid index for the actual atom position */
x0index = pme->particleindex[i][0];
y0index = pme->particleindex[i][1];
z0index = pme->particleindex[i][2];
/* Bspline factors for this atom in each dimension , calculated from fractional coordinates */
thetax = &(pme->bsplines_theta[0][i*order]);
thetay = &(pme->bsplines_theta[1][i*order]);
thetaz = &(pme->bsplines_theta[2][i*order]);
/* See pme_grid_spread_charge() for comments about the order here, and only interpolation in one direction */
/* Since we will add order^3 (typically 5*5*5=125) terms to the charge
* derivative on each particle, we use a temporary dq variable, and only
* add it to memory forces[] at the end.
*/
for (ix=0;ix<order;ix++)
{
xindex = (x0index + ix) % pme->ngrid[0];
/* Get the bspline factor with respect to the x coordinate */
tx = thetax[ix];
for (iy=0;iy<order;iy++)
{
yindex = (y0index + iy) % pme->ngrid[1];
/* bspline wrt y */
ty = thetay[iy];
for (iz=0;iz<order;iz++)
{
/* Can be optimized, but we keep it simple here */
zindex = (z0index + iz) % pme->ngrid[2];
/* bspline wrt z */
tz = thetaz[iz];
index = xindex*pme->ngrid[1]*pme->ngrid[2] + yindex*pme->ngrid[2] + zindex;
/* Get the fft+convoluted+ifft:d data from the grid, which must be real by definition */
/* Checking that the imaginary part is indeed zero might be a good check :-) */
gridvalue = pme->grid[index].real();
/* The d component of the force is calculated by taking the derived bspline in dimension d, normal bsplines in the other two */
dq += tx*ty*tz*gridvalue;
}
}
}
chargeDerivatives[ideriv] += dq;
}
}
/* EXPORTED ROUTINES */
......@@ -802,6 +890,56 @@ int pme_exec(pme_t pme,
}
int pme_exec_charge_derivatives(pme_t pme,
const vector<Vec3>& atomCoordinates,
vector<double>& chargeDerivatives,
const vector<int>& chargeIndices,
const vector<double>& charges,
const Vec3 periodicBoxVectors[3])
{
/* Routine is called with coordinates in x, a box, and charges in q */
Vec3 recipBoxVectors[3];
ReferenceForce::invertBoxVectors(periodicBoxVectors, recipBoxVectors);
/* Before we can do the actual interpolation, we need to recalculate and update
* the indices for each particle in the charge grid (initialized in pme_init()),
* and what its fractional offset in this grid cell is.
*/
/* Update charge grid indices and fractional offsets for each atom.
* The indices/fractions are stored internally in the pme datatype
*/
pme_update_grid_index_and_fraction(pme,atomCoordinates,periodicBoxVectors,recipBoxVectors);
/* Calculate bsplines (and their differentials) from current fractional coordinates, store in pme structure */
pme_update_bsplines(pme);
/* Spread the charges on grid (using newly calculated bsplines in the pme structure) */
pme_grid_spread_charge(pme, charges);
/* do 3d-fft */
vector<size_t> shape = {(size_t) pme->ngrid[0], (size_t) pme->ngrid[1], (size_t) pme->ngrid[2]};
vector<size_t> axes = {0, 1, 2};
vector<ptrdiff_t> stride = {(ptrdiff_t) (pme->ngrid[1]*pme->ngrid[2]*sizeof(complex<double>)),
(ptrdiff_t) (pme->ngrid[2]*sizeof(complex<double>)),
(ptrdiff_t) sizeof(complex<double>)};
pocketfft::c2c(shape, stride, stride, axes, true, pme->grid, pme->grid, 1.0, 0);
/* solve in k-space */
double energy;
pme_reciprocal_convolution(pme,periodicBoxVectors,recipBoxVectors,&energy);
/* do 3d-invfft */
pocketfft::c2c(shape, stride, stride, axes, false, pme->grid, pme->grid, 1.0, 0);
/* Get the charge derivatives from the grid and bsplines in the pme structure */
pme_grid_interpolate_charge_derivatives(pme,recipBoxVectors,charges,chargeDerivatives,chargeIndices);
return 0;
}
int pme_exec_dpme(pme_t pme,
const vector<Vec3>& atomCoordinates,
vector<Vec3>& forces,
......
/* -------------------------------------------------------------------------- *
* 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) 2015-2025 Stanford University and the Authors. *
* Authors: 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 "ReferenceTests.h"
#include "TestConstantPotentialForce.h"
void platformInitialize() {
}
void testGradientFiniteDifference(ConstantPotentialForce::ConstantPotentialMethod method, bool usePreconditioner) {
// Ensures that computed forces match actual changes in energy with particle
// perturbations, accounting for changes in electrode atom charges.
// Finite differences with single precision PME have a lot of error, so we
// only run this test on the reference platform, and the other platforms'
// forces are compared to forces computed by the reference platform.
System system;
ConstantPotentialForce* force;
vector<Vec3> positions;
makeTestUpdateSystem(method, usePreconditioner, system, force, positions);
force->setEwaldErrorTolerance(2e-6);
VerletIntegrator integrator(0.001);
Context context(system, integrator, platform);
context.setPositions(positions);
State state = context.getState(State::Energy | State::Forces);
double energy = state.getPotentialEnergy();
vector<Vec3> forces = state.getForces();
double delta = 1e-3;
for (int i = 0; i < forces.size(); i++) {
for (int d = 0; d < 3; d++) {
double refPos = positions[i][d];
positions[i][d] = refPos - delta;
context.setPositions(positions);
vector<double> c;
double energyL = context.getState(State::Energy).getPotentialEnergy();
positions[i][d] = refPos + delta;
context.setPositions(positions);
double energyR = context.getState(State::Energy).getPotentialEnergy();
positions[i][d] = refPos;
ASSERT_EQUAL_TOL((energyR - energyL) / (2 * delta), -forces[i][d], 5e-4);
}
}
}
void runPlatformTests(ConstantPotentialForce::ConstantPotentialMethod method, bool usePreconditioner) {
testEnergyConservation(method, usePreconditioner, 10);
testGradientFiniteDifference(method, usePreconditioner);
}
......@@ -6,8 +6,8 @@
* Biological Structures at Stanford, funded under the NIH Roadmap for *
* Medical Research, grant U54 GM072970. See https://simtk.org. *
* *
* Portions copyright (c) 2015 Stanford University and the Authors. *
* Authors: Peter Eastman *
* Portions copyright (c) 2015-2025 Stanford University and the Authors. *
* Authors: Peter Eastman, Evan Prettt *
* Contributors: *
* *
* Permission is hereby granted, free of charge, to any person obtaining a *
......@@ -31,6 +31,85 @@
#include "ReferenceTests.h"
#include "TestEwald.h"
#include "ReferencePME.h"
void testReferencePmeDerivatives() {
// Ensures that derivatives reported by the reference PME implementation
// match those estimated by finite differences. Checks both forces and
// charge derivatives for a system with a non-zero net charge.
static const double DELTA = 1e-5;
static const double EPSILON = 1e-5;
Vec3 boxVectors[3] = {
Vec3(10, 0, 0),
Vec3(1, 9, 0),
Vec3(2, 3, 8)
};
int numParticles = 10;
vector<Vec3> positions;
vector<double> charges;
vector<int> indices;
for (int i = 0; i < numParticles; i++) {
double f = (double)i / numParticles;
positions.push_back(f * boxVectors[0] + f * f * boxVectors[1] + f * f * f * boxVectors[2]);
charges.push_back((i % 2 ? -1 : 1) + f * f);
indices.push_back(i);
}
double ewaldAlpha = 1.752;
int gridSize[3] = {54, 49, 43};
pme_t pme;
pme_init(&pme, ewaldAlpha, numParticles, gridSize, 5, 1);
double dummyEnergy=0;
vector<Vec3> dummyForces(numParticles);
vector<Vec3> testForces(numParticles);
pme_exec(pme, positions, testForces, charges, boxVectors, &dummyEnergy);
for (int i = 0; i < numParticles; i++) {
for (int j = 0; j < 3; j++) {
double referencePosition = positions[i][j];
double energyLess = 0.0;
positions[i][j] = referencePosition - DELTA;
pme_exec(pme, positions, dummyForces, charges, boxVectors, &energyLess);
double energyMore = 0.0;
positions[i][j] = referencePosition + DELTA;
pme_exec(pme, positions, dummyForces, charges, boxVectors, &energyMore);
positions[i][j] = referencePosition;
ASSERT_EQUAL_TOL((energyMore - energyLess) / (2 * DELTA), -testForces[i][j], EPSILON);
}
}
vector<double> testDerivatives(numParticles);
pme_exec_charge_derivatives(pme, positions, testDerivatives, indices, charges, boxVectors);
for (int i = 0; i < numParticles; i++) {
double referenceCharge = charges[i];
double energyLess = 0.0;
charges[i] = referenceCharge - DELTA;
pme_exec(pme, positions, dummyForces, charges, boxVectors, &energyLess);
double energyMore = 0.0;
charges[i] = referenceCharge + DELTA;
pme_exec(pme, positions, dummyForces, charges, boxVectors, &energyMore);
charges[i] = referenceCharge;
ASSERT_EQUAL_TOL((energyMore - energyLess) / (2 * DELTA), testDerivatives[i], EPSILON);
}
pme_destroy(pme);
}
void runPlatformTests() {
testReferencePmeDerivatives();
}
......@@ -726,6 +726,7 @@ void CommonCalcAmoebaMultipoleForceKernel::initialize(const System& system, cons
pmeDefines["PME_ORDER"] = cc.intToString(PmeOrder);
pmeDefines["NUM_ATOMS"] = cc.intToString(numMultipoles);
pmeDefines["PADDED_NUM_ATOMS"] = cc.intToString(cc.getPaddedNumAtoms());
pmeDefines["NUM_INDICES"] = "0";
pmeDefines["EPSILON_FACTOR"] = cc.doubleToString(ONE_4PI_EPS0);
pmeDefines["GRID_SIZE_X"] = cc.intToString(gridSizeX);
pmeDefines["GRID_SIZE_Y"] = cc.intToString(gridSizeY);
......@@ -2687,6 +2688,7 @@ void CommonCalcHippoNonbondedForceKernel::initialize(const System& system, const
pmeDefines["PME_ORDER"] = cc.intToString(PmeOrder);
pmeDefines["NUM_ATOMS"] = cc.intToString(numParticles);
pmeDefines["PADDED_NUM_ATOMS"] = cc.intToString(cc.getPaddedNumAtoms());
pmeDefines["NUM_INDICES"] = "0";
pmeDefines["EPSILON_FACTOR"] = cc.doubleToString(ONE_4PI_EPS0);
pmeDefines["GRID_SIZE_X"] = cc.intToString(gridSizeX);
pmeDefines["GRID_SIZE_Y"] = cc.intToString(gridSizeY);
......
......@@ -8,7 +8,7 @@
* *
* Portions copyright (c) 2013-2025 Stanford University and the Authors. *
* Authors: Peter Eastman *
* Contributors: *
* Contributors: Evan Pretti *
* *
* Permission is hereby granted, free of charge, to any person obtaining a *
* copy of this software and associated documentation files (the "Software"), *
......@@ -422,13 +422,101 @@ static void interpolateForces(float* posq, vector<float>& force, vector<float>&
}
}
static void interpolateChargeDerivatives(float* posq, const vector<int>& chargeIndices, vector<float>& chargeDerivatives, vector<float>& grid, int gridx, int gridy, int gridz, int numIndices, Vec3* periodicBoxVectors, Vec3* recipBoxVectors, atomic<int>& atomicCounter, const float epsilonFactor, int numThreads) {
fvec4 boxSize((float) periodicBoxVectors[0][0], (float) periodicBoxVectors[1][1], (float) periodicBoxVectors[2][2], 0);
fvec4 invBoxSize((float) recipBoxVectors[0][0], (float) recipBoxVectors[1][1], (float) recipBoxVectors[2][2], 0);
fvec4 recipBoxVec0((float) recipBoxVectors[0][0], (float) recipBoxVectors[0][1], (float) recipBoxVectors[0][2], 0);
fvec4 recipBoxVec1((float) recipBoxVectors[1][0], (float) recipBoxVectors[1][1], (float) recipBoxVectors[1][2], 0);
fvec4 recipBoxVec2((float) recipBoxVectors[2][0], (float) recipBoxVectors[2][1], (float) recipBoxVectors[2][2], 0);
fvec4 gridSize(gridx, gridy, gridz, 0);
ivec4 gridSizeInt(gridx, gridy, gridz, 0);
fvec4 one(1);
fvec4 scale(1.0f/(PME_ORDER-1));
const int groupSize = max(1, numIndices / (10 * numThreads));
while (true) {
int start = atomicCounter.fetch_add(groupSize);
if (start >= numIndices)
break;
int end = min(start + groupSize, numIndices);
for (int ii = start; ii < end; ii++) {
// Find the position relative to the nearest grid point.
fvec4 pos(&posq[4*chargeIndices[ii]]);
float posInBox[4];
(pos-boxSize*floor(pos*invBoxSize)).store(posInBox);
fvec4 t = posInBox[0]*recipBoxVec0 + posInBox[1]*recipBoxVec1 + posInBox[2]*recipBoxVec2;
t = (t-floor(t))*gridSize;
ivec4 ti = t;
fvec4 dr = t-ti;
ivec4 gridIndex = ti-(gridSizeInt&ti==gridSizeInt);
// Compute the B-spline coefficients.
fvec4 data[PME_ORDER];
data[PME_ORDER-1] = 0.0f;
data[1] = dr;
data[0] = one-dr;
for (int j = 3; j < PME_ORDER; j++) {
fvec4 div(1.0f/(j-1));
data[j-1] = div*dr*data[j-2];
for (int k = 1; k < j-1; k++)
data[j-k-1] = div*((dr+k)*data[j-k-2]+(fvec4(j-k)-dr)*data[j-k-1]);
data[0] = div*(one-dr)*data[0];
}
data[PME_ORDER-1] = scale*dr*data[PME_ORDER-2];
for (int j = 1; j < (PME_ORDER-1); j++)
data[PME_ORDER-j-1] = scale*((dr+j)*data[PME_ORDER-j-2]+(fvec4(PME_ORDER-j)-dr)*data[PME_ORDER-j-1]);
data[0] = scale*(one-dr)*data[0];
// Compute the charge derivative for this atom.
int gridIndexX = gridIndex[0];
int gridIndexY = gridIndex[1];
int gridIndexZ = gridIndex[2];
if (gridIndexX < 0)
return; // This happens when a simulation blows up and coordinates become NaN.
int zindex[PME_ORDER];
for (int j = 0; j < PME_ORDER; j++) {
zindex[j] = gridIndexZ+j;
zindex[j] -= (zindex[j] >= gridz ? gridz : 0);
}
float f = 0.0f;
for (int ix = 0; ix < PME_ORDER; ix++) {
int xbase = gridIndexX+ix;
xbase -= (xbase >= gridx ? gridx : 0);
xbase = xbase*gridy*gridz;
float dx = data[ix][0];
for (int iy = 0; iy < PME_ORDER; iy++) {
int ybase = gridIndexY+iy;
ybase -= (ybase >= gridy ? gridy : 0);
ybase = xbase + ybase*gridz;
float dy = data[iy][1];
for (int iz = 0; iz < PME_ORDER; iz++) {
float dz = data[iz][2];
float gridValue = grid[ybase+zindex[iz]];
f += dx*dy*dz*gridValue;
}
}
}
chargeDerivatives[ii] = epsilonFactor * f;
}
}
}
static void* threadBody(void* args) {
CpuCalcPmeReciprocalForceKernel& owner = *reinterpret_cast<CpuCalcPmeReciprocalForceKernel*>(args);
owner.runMainThread();
return 0;
}
void CpuCalcPmeReciprocalForceKernel::initialize(int xsize, int ysize, int zsize, int numParticles, double alpha, bool deterministic) {
void CpuCalcPmeReciprocalForceKernel::initialize(int xsize, int ysize, int zsize, int numParticles, const vector<int>& indices, double alpha, bool deterministic) {
if (!hasInitializedThreads) {
numThreads = getNumProcessors();
char* threadsEnv = getenv("OPENMM_CPU_THREADS");
......@@ -456,6 +544,9 @@ void CpuCalcPmeReciprocalForceKernel::initialize(int xsize, int ysize, int zsize
this->alpha = alpha;
this->deterministic = deterministic;
force.resize(4*numParticles);
chargeIndices = indices;
numIndices = chargeIndices.size();
chargeDerivatives.resize(numIndices);
recipEterm.resize(gridx*gridy*gridz);
// Initialize threads.
......@@ -570,11 +661,27 @@ void CpuCalcPmeReciprocalForceKernel::runMainThread() {
for (auto e : threadEnergy)
energy += e;
}
threads.resumeThreads(); // Signal threads to perform reciprocal convolution.
threads.waitForThreads();
pocketfft::c2r(gridShape, complexGridStride, realGridStride, fftAxes, false, complexGrid.data(), realGrids[0].data(), 1.0f, 0);
atomicCounter = 0;
threads.resumeThreads(); // Signal threads to interpolate forces.
if (includeForces || includeChargeDerivatives) {
// Explicitly zero out the zero frequency component or charge
// derivatives will be incorrect. The neutralizing plasma
// interaction energy contribution is computed separately.
complexGrid[0] = 0;
threads.resumeThreads(); // Signal threads to perform reciprocal convolution.
threads.waitForThreads();
pocketfft::c2r(gridShape, complexGridStride, realGridStride, fftAxes, false, complexGrid.data(), realGrids[0].data(), 1.0f, 0);
if (includeForces) {
atomicCounter = 0;
threads.resumeThreads(); // Signal threads to interpolate forces.
threads.waitForThreads();
}
if (includeChargeDerivatives) {
atomicCounter = 0;
threads.resumeThreads(); // Signal threads to interpolate charge derivatives.
threads.waitForThreads();
}
}
threads.resumeThreads(); // Signal threads to finish.
threads.waitForThreads();
isFinished = true;
lastBoxVectors[0] = periodicBoxVectors[0];
......@@ -612,17 +719,28 @@ void CpuCalcPmeReciprocalForceKernel::runWorkerThread(ThreadPool& threads, int i
threadEnergy[index] = reciprocalEnergy(gridxStart, gridxEnd, complexGrid, recipEterm, gridx, gridy, gridz, alpha, bsplineModuli, periodicBoxVectors, recipBoxVectors);
threads.syncThreads();
}
reciprocalConvolution(complexStart, complexEnd, complexGrid, recipEterm);
threads.syncThreads();
interpolateForces(posq, force, realGrids[0], gridx, gridy, gridz, numParticles, periodicBoxVectors, recipBoxVectors, atomicCounter, epsilonFactor, numThreads);
if (includeForces || includeChargeDerivatives) {
reciprocalConvolution(complexStart, complexEnd, complexGrid, recipEterm);
threads.syncThreads();
if (includeForces) {
interpolateForces(posq, force, realGrids[0], gridx, gridy, gridz, numParticles, periodicBoxVectors, recipBoxVectors, atomicCounter, epsilonFactor, numThreads);
threads.syncThreads();
}
if (includeChargeDerivatives) {
interpolateChargeDerivatives(posq, chargeIndices, chargeDerivatives, realGrids[0], gridx, gridy, gridz, numIndices, periodicBoxVectors, recipBoxVectors, atomicCounter, epsilonFactor, numThreads);
threads.syncThreads();
}
}
}
void CpuCalcPmeReciprocalForceKernel::beginComputation(IO& io, const Vec3* periodicBoxVectors, bool includeEnergy) {
void CpuCalcPmeReciprocalForceKernel::beginComputation(IO& io, const Vec3* periodicBoxVectors, bool includeEnergy, bool includeForces, bool includeChargeDerivatives) {
this->io = &io;
this->periodicBoxVectors[0] = periodicBoxVectors[0];
this->periodicBoxVectors[1] = periodicBoxVectors[1];
this->periodicBoxVectors[2] = periodicBoxVectors[2];
this->includeEnergy = includeEnergy;
this->includeForces = includeForces;
this->includeChargeDerivatives = includeChargeDerivatives;
energy = 0.0;
ReferenceForce::invertBoxVectors(periodicBoxVectors, recipBoxVectors);
......@@ -640,7 +758,12 @@ double CpuCalcPmeReciprocalForceKernel::finishComputation(IO& io) {
endCondition.wait(ul);
}
}
io.setForce(&force[0]);
if (includeForces) {
io.setForce(&force[0]);
}
if (includeChargeDerivatives) {
io.setChargeDerivatives(&chargeDerivatives[0]);
}
return energy;
}
......
......@@ -11,7 +11,7 @@
* *
* Portions copyright (c) 2013-2025 Stanford University and the Authors. *
* Authors: Peter Eastman *
* Contributors: *
* Contributors: Evan Pretti *
* *
* Permission is hereby granted, free of charge, to any person obtaining a *
* copy of this software and associated documentation files (the "Software"), *
......@@ -63,19 +63,22 @@ public:
* @param gridy the y size of the PME grid
* @param gridz the z size of the PME grid
* @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 deterministic whether it should attempt to make the resulting forces deterministic
*/
void initialize(int xsize, int ysize, int zsize, int numParticles, double alpha, bool deterministic);
void initialize(int xsize, int ysize, int zsize, int numParticles, const std::vector<int>& indices, double alpha, bool deterministic);
~CpuCalcPmeReciprocalForceKernel();
/**
* Begin computing the force and energy.
*
* @param io an object that coordinates data transfer
* @param periodicBoxVectors the vectors defining the periodic box (measured in nm)
* @param includeEnergy true if potential energy should be computed
*
* @param io an object that coordinates data transfer
* @param periodicBoxVectors the vectors defining the periodic box (measured in nm)
* @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
*/
void beginComputation(IO& io, const Vec3* periodicBoxVectors, bool includeEnergy);
void beginComputation(IO& io, const Vec3* periodicBoxVectors, bool includeEnergy, bool includeForces, bool includeChargeDerivatives);
/**
* Finish computing the force and energy.
*
......@@ -111,11 +114,13 @@ private:
int findFFTDimension(int minimum);
static bool hasInitializedThreads;
static int numThreads;
int gridx, gridy, gridz, numParticles;
int gridx, gridy, gridz, numParticles, numIndices;
double alpha;
bool deterministic;
bool isFinished, isDeleted;
std::vector<int> chargeIndices;
std::vector<float> force;
std::vector<float> chargeDerivatives;
std::vector<float> bsplineModuli[3];
std::vector<float> recipEterm;
Vec3 lastBoxVectors[3];
......@@ -133,7 +138,7 @@ private:
float energy;
float* posq;
Vec3 periodicBoxVectors[3], recipBoxVectors[3];
bool includeEnergy;
bool includeEnergy, includeForces, includeChargeDerivatives;
std::atomic<int> atomicCounter;
};
......
......@@ -6,7 +6,7 @@
* Biological Structures at Stanford, funded under the NIH Roadmap for *
* Medical Research, grant U54 GM072970. See https://simtk.org. *
* *
* Portions copyright (c) 2013 Stanford University and the Authors. *
* Portions copyright (c) 2013-2025 Stanford University and the Authors. *
* Authors: Peter Eastman *
* Contributors: *
* *
......@@ -43,6 +43,7 @@
#include "openmm/Units.h"
#include "../src/CpuPmeKernels.h"
#include "SimTKOpenMMRealType.h"
#include "ReferencePME.h"
#include "sfmt/SFMT.h"
#include <iostream>
#include <vector>
......@@ -54,12 +55,16 @@ class IO : public CalcPmeReciprocalForceKernel::IO {
public:
vector<float> posq;
float* force;
float* derivatives;
float* getPosq() {
return &posq[0];
}
void setForce(float* force) {
this->force = force;
}
void setChargeDerivatives(float* derivatives) {
this->derivatives = derivatives;
}
};
void make_waterbox(int natoms, double boxEdgeLength, NonbondedForce *forceField, vector<Vec3> &positions, vector<double>& eps, vector<double>& sig,
......@@ -565,7 +570,7 @@ void test_water2_dpme_energies_forces_no_exclusions() {
}
void testPME(bool triclinic) {
void testPME(bool triclinic, bool nonNeutral) {
// Create a cloud of random point charges.
const int numParticles = 51;
......@@ -590,15 +595,20 @@ void testPME(bool triclinic) {
OpenMM_SFMT::SFMT sfmt;
init_gen_rand(0, sfmt);
vector<double> testCharges;
vector<int> testIndices;
for (int i = 0; i < numParticles; i++) {
system.addParticle(1.0);
force->addParticle(-1.0+i*2.0/(numParticles-1), 1.0, 0.0);
double testCharge = -1.0+i*2.0/(numParticles-1) + (nonNeutral ? 0.001 * i * i : 0);
force->addParticle(testCharge, 1.0, 0.0);
positions[i] = Vec3(boxWidth*genrand_real2(sfmt), boxWidth*genrand_real2(sfmt), boxWidth*genrand_real2(sfmt));
testCharges.push_back(testCharge);
testIndices.push_back(i);
}
force->setNonbondedMethod(NonbondedForce::PME);
force->setCutoffDistance(cutoff);
force->setReciprocalSpaceForceGroup(1);
force->setEwaldErrorTolerance(1e-4);
force->setEwaldErrorTolerance(5e-5);
// Compute the reciprocal space forces with the reference platform.
......@@ -615,6 +625,7 @@ void testPME(bool triclinic) {
NonbondedForceImpl::calcPMEParameters(system, *force, alpha, gridx, gridy, gridz, false);
CpuCalcPmeReciprocalForceKernel pme(CalcPmeReciprocalForceKernel::Name(), platform);
IO io;
double sumCharges = 0;
double sumSquaredCharges = 0;
for (int i = 0; i < numParticles; i++) {
io.posq.push_back(positions[i][0]);
......@@ -623,18 +634,35 @@ void testPME(bool triclinic) {
double charge, sigma, epsilon;
force->getParticleParameters(i, charge, sigma, epsilon);
io.posq.push_back(charge);
sumCharges += charge;
sumSquaredCharges += charge*charge;
}
double ewaldSelfEnergy = -ONE_4PI_EPS0*alpha*sumSquaredCharges/sqrt(M_PI);
pme.initialize(gridx, gridy, gridz, numParticles, alpha, true);
pme.beginComputation(io, boxVectors, true);
double ewaldPlasmaEnergy = -sumCharges*sumCharges/(8.0*EPSILON0*alpha*alpha*boxVectors[0][0]*boxVectors[1][1]*boxVectors[2][2]);
pme.initialize(gridx, gridy, gridz, numParticles, testIndices, alpha, true);
pme.beginComputation(io, boxVectors, true, true, true);
double energy = pme.finishComputation(io);
// See if they match.
ASSERT_EQUAL_TOL(refState.getPotentialEnergy(), energy+ewaldSelfEnergy, 1e-3);
for (int i = 0; i < numParticles; i++)
ASSERT_EQUAL_TOL(refState.getPotentialEnergy(), energy+ewaldSelfEnergy+ewaldPlasmaEnergy, 1e-3);
for (int i = 0; i < numParticles; i++) {
ASSERT_EQUAL_VEC(refState.getForces()[i], Vec3(io.force[4*i], io.force[4*i+1], io.force[4*i+2]), 1e-3);
}
// Get charge derivatives from the reference PME implementation.
pme_t referencePme;
int gridSize[3] = {gridx, gridy, gridz};
pme_init(&referencePme, alpha, numParticles, gridSize, 5, 1);
vector<double> testDerivatives(numParticles);
pme_exec_charge_derivatives(referencePme, positions, testDerivatives, testIndices, testCharges, boxVectors);
pme_destroy(referencePme);
// See if they match.
for (int i = 0; i < numParticles; i++) {
ASSERT_EQUAL_TOL(testDerivatives[i], io.derivatives[i], 1e-3);
}
}
void testLJPME(bool triclinic) {
......@@ -712,8 +740,10 @@ int main(int argc, char* argv[]) {
cout << "CPU is not supported. Exiting." << endl;
return 0;
}
testPME(false);
testPME(true);
testPME(false, false);
testPME(false, true);
testPME(true, false);
testPME(true, true);
testLJPME(false);
testLJPME(true);
test_water2_dpme_energies_forces_no_exclusions();
......
#ifndef OPENMM_CONSTANTPOTENTIALFORCE_PROXY_H_
#define OPENMM_CONSTANTPOTENTIALFORCE_PROXY_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) 2010-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/internal/windowsExport.h"
#include "openmm/serialization/SerializationProxy.h"
namespace OpenMM {
/**
* This is a proxy for serializing ConstantPotentialForce objects.
*/
class OPENMM_EXPORT ConstantPotentialForceProxy : public SerializationProxy {
public:
ConstantPotentialForceProxy();
void serialize(const void* object, SerializationNode& node) const;
void* deserialize(const SerializationNode& node) const;
};
} // namespace OpenMM
#endif /*OPENMM_CONSTANTPOTENTIALFORCE_PROXY_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) 2010-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/serialization/ConstantPotentialForceProxy.h"
#include "openmm/serialization/SerializationNode.h"
#include "openmm/Force.h"
#include "openmm/ConstantPotentialForce.h"
#include <sstream>
using namespace OpenMM;
using namespace std;
ConstantPotentialForceProxy::ConstantPotentialForceProxy() : SerializationProxy("ConstantPotentialForce") {
}
void ConstantPotentialForceProxy::serialize(const void* object, SerializationNode& node) const {
node.setIntProperty("version", 1);
const ConstantPotentialForce& force = *reinterpret_cast<const ConstantPotentialForce*>(object);
node.setIntProperty("forceGroup", force.getForceGroup());
node.setStringProperty("name", force.getName());
node.setDoubleProperty("cutoff", force.getCutoffDistance());
node.setDoubleProperty("ewaldTolerance", force.getEwaldErrorTolerance());
double alpha;
int nx, ny, nz;
force.getPMEParameters(alpha, nx, ny, nz);
node.setDoubleProperty("alpha", alpha);
node.setIntProperty("nx", nx);
node.setIntProperty("ny", ny);
node.setIntProperty("nz", nz);
node.setBoolProperty("exceptionsUsePeriodic", force.getExceptionsUsePeriodicBoundaryConditions());
node.setIntProperty("constantPotentialMethod", (int) force.getConstantPotentialMethod());
node.setDoubleProperty("cgTolerance", force.getCGErrorTolerance());
node.setBoolProperty("usePreconditioner", force.getUsePreconditioner());
node.setBoolProperty("useChargeConstraint", force.getUseChargeConstraint());
node.setDoubleProperty("chargeConstraintTarget", force.getChargeConstraintTarget());
Vec3 externalField;
force.getExternalField(externalField);
node.createChildNode("ExternalField").setDoubleProperty("x", externalField[0]).setDoubleProperty("y", externalField[1]).setDoubleProperty("z", externalField[2]);
SerializationNode& particles = node.createChildNode("Particles");
for (int i = 0; i < force.getNumParticles(); i++) {
double charge;
force.getParticleParameters(i, charge);
particles.createChildNode("Particle").setDoubleProperty("q", charge);
}
SerializationNode& exceptions = node.createChildNode("Exceptions");
for (int i = 0; i < force.getNumExceptions(); i++) {
int particle1, particle2;
double chargeProd;
force.getExceptionParameters(i, particle1, particle2, chargeProd);
exceptions.createChildNode("Exception").setIntProperty("p1", particle1).setIntProperty("p2", particle2).setDoubleProperty("q", chargeProd);
}
SerializationNode& electrodes = node.createChildNode("Electrodes");
for(int i = 0; i < force.getNumElectrodes(); i++) {
SerializationNode& electrode = electrodes.createChildNode("Electrode");
std::set<int> electrodeParticles;
double potential;
double gaussianWidth;
double thomasFermiScale;
force.getElectrodeParameters(i, electrodeParticles, potential, gaussianWidth, thomasFermiScale);
electrode.setDoubleProperty("potential", potential);
electrode.setDoubleProperty("gaussianWidth", gaussianWidth);
electrode.setDoubleProperty("thomasFermiScale", thomasFermiScale);
SerializationNode& electrodeParticlesNode = electrode.createChildNode("Particles");
for (int p : electrodeParticles) {
electrodeParticlesNode.createChildNode("Particle").setIntProperty("index", p);
}
}
}
void* ConstantPotentialForceProxy::deserialize(const SerializationNode& node) const {
int version = node.getIntProperty("version");
if (version != 1)
throw OpenMMException("Unsupported version number");
ConstantPotentialForce* force = new ConstantPotentialForce();
try {
force->setForceGroup(node.getIntProperty("forceGroup", 0));
force->setName(node.getStringProperty("name", force->getName()));
force->setCutoffDistance(node.getDoubleProperty("cutoff"));
force->setEwaldErrorTolerance(node.getDoubleProperty("ewaldTolerance"));
double alpha = node.getDoubleProperty("alpha", 0.0);
int nx = node.getIntProperty("nx", 0);
int ny = node.getIntProperty("ny", 0);
int nz = node.getIntProperty("nz", 0);
force->setPMEParameters(alpha, nx, ny, nz);
force->setExceptionsUsePeriodicBoundaryConditions(node.getBoolProperty("exceptionsUsePeriodic"));
force->setConstantPotentialMethod((ConstantPotentialForce::ConstantPotentialMethod) node.getIntProperty("constantPotentialMethod"));
force->setCGErrorTolerance(node.getDoubleProperty("cgTolerance"));
force->setUsePreconditioner(node.getBoolProperty("usePreconditioner"));
force->setUseChargeConstraint(node.getBoolProperty("useChargeConstraint"));
force->setChargeConstraintTarget(node.getDoubleProperty("chargeConstraintTarget"));
const SerializationNode& externalFieldNode = node.getChildNode("ExternalField");
Vec3 externalField(externalFieldNode.getDoubleProperty("x"), externalFieldNode.getDoubleProperty("y"), externalFieldNode.getDoubleProperty("z"));
force->setExternalField(externalField);
const SerializationNode& particles = node.getChildNode("Particles");
for (auto& particle : particles.getChildren())
force->addParticle(particle.getDoubleProperty("q"));
const SerializationNode& exceptions = node.getChildNode("Exceptions");
for (auto& exception : exceptions.getChildren())
force->addException(exception.getIntProperty("p1"), exception.getIntProperty("p2"), exception.getDoubleProperty("q"));
const SerializationNode& electrodes = node.getChildNode("Electrodes");
for (auto& electrode : electrodes.getChildren()) {
std::set<int> electrodeParticles;
const SerializationNode& electrodeParticlesNode = electrode.getChildNode("Particles");
for (auto& child : electrodeParticlesNode.getChildren()) {
electrodeParticles.insert(child.getIntProperty("index"));
}
force->addElectrode(electrodeParticles, electrode.getDoubleProperty("potential"), electrode.getDoubleProperty("gaussianWidth"), electrode.getDoubleProperty("thomasFermiScale"));
}
}
catch (...) {
delete force;
throw;
}
return force;
}
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