Task: CustomCPPForceImpl for writing forces in C++ (openmm/openmm#4231)

org: openmm
repo: openmm
pr_number: 4231
issue_title: CustomCPPForceImpl for writing forces in C++
issue_body: This is a new piece of infrastructure.  It allows you to implement a new force by writing a single implementation in platform-independent C++.  This is mainly intended for plugins that interface to other codes.  It should allow us to enormously simplify the PLUMED plugin.  It also can be used for a plugin that interfaces to xtb, and possibly others in the future.  Having most of the infrastructure built in will eliminate a lot of code duplication between different plugins.

To give a sense of how simple it can be to write a Force with it, here's the code from the test case.  This is a complete, fully functional of implementation of a new Force class that works on all platforms.

```c++
class TestForce : public Force {
protected:
    ForceImpl* createImpl() const;
};

class TestForceImpl : public CustomCPPForceImpl {
public:
    const TestForce& owner;
    TestForceImpl(const TestForce& owner) : CustomCPPForceImpl(owner), owner(owner) {
    }
    double computeForce(ContextImpl& context, const vector<Vec3>& positions, vector<Vec3>& forces) {
        for (int i = 0; i < forces.size(); i++)
            forces[i] = Vec3(i, i+1, i+2);
        return 10.5;
    }
    const TestForce& getOwner() const {
        return owner;
    }
};

ForceImpl* TestForce::createImpl() const {
    return new TestForceImpl(*this);
}
```
resolved_issues: title: CustomCPPForceImpl for writing forces in C++
body: ### Issue Description

**Problem:**  
Developers creating plugins for OpenMM often need to implement custom forces that interface with external libraries or APIs. This process typically requires writing platform-specific code for each supported hardware platform, such as GPU or CPU, which introduces significant complexity and code duplication. Additionally, the need for platform-specific optimizations can be unnecessary when the heavy computations are handled by external libraries, leaving developers to focus solely on transferring data between OpenMM and the external library.

**Expected Behavior:**  
Developers should be able to implement custom forces in a straightforward, platform-independent manner using a single C++ implementation. The infrastructure should automatically ensure compatibility across all supported platforms without requiring additional GPU-specific code, kernel factories, or dynamic plugin libraries. This approach should simplify the development process, reduce code duplication, and provide a consistent API for interfacing with external libraries.

## IMPORTANT INSTRUCTIONS

1. If you edit a python file, you MUST run 'make PythonInstall' inside the build directory
2. If you edit a c++ file, you MUST run 'make -j4 && make install" inside the build directory

Reference solution:
fix_patch: diff --git a/docs-source/developerguide/09_customcppforceimpl.rst b/docs-source/developerguide/09_customcppforceimpl.rst
new file mode 100644
index 0000000000..9c6bccb48e
--- /dev/null
+++ b/docs-source/developerguide/09_customcppforceimpl.rst
@@ -0,0 +1,52 @@
+.. role:: code
+.. raw:: html
+
+    <style> .code {font-family:monospace;} </style>
+    <style> .caption {text-align:center;} </style>
+
+.. highlight:: c++
+
+
+.. _customcppforceimpl:
+
+Platform-Independent Code with CustomCPPForceImpl
+#################################################
+
+In most cases, Forces are written with platform-specific code that runs as
+efficiently as possible on the available hardware.  There are some situations
+where that is unnecessarily complicated.  For example, you might write a plugin
+that interfaces to an external library through its own public API.  Because all
+expensive calculations are done by the external library, there is little
+opportunity for any kind of platform-specific optimization.  All you can do is
+copy positions over to the external library and copy forces back, hopefully in
+a way that is not too inefficient, and preferably with as little code as
+possible.  CustomCPPForceImpl is a tool for doing that.
+
+To use it, you write your Force subclass in the usual way, providing whatever
+API is appropriate for it.  Then for the corresponding ForceImpl, simply
+subclass CustomCPPForceImpl and override :code:`computeForce()` to perform the
+calculation using platform-independent C++ code.  Nothing more is required.  It
+will automatically work on all platforms.  You do not need to write any GPU
+code, provide a Kernel or KernelFactory, define registration functions, or even
+create plugin libraries to be loaded dynamically.  The single library containing
+the Force and ForceImpl is all you need.
+
+Here is an example of what the code to implement your ForceImpl might look like.
+::
+
+    class ExampleForceImpl : public CustomCPPForceImpl {
+    public:
+        ExampleForceImpl(const ExampleForce& owner) : CustomCPPForceImpl(owner), owner(owner) {
+        }
+
+        double computeForce(ContextImpl& context, const vector<Vec3>& positions, vector<Vec3>& forces) {
+            // Compute the forces and energy here.  Store the forces into the
+            // vector and return the energy.
+        }
+
+        const ExampleForce& getOwner() const {
+            return owner;
+        }
+    private:
+        const ExampleForce& owner;
+    };
diff --git a/docs-source/developerguide/index.rst b/docs-source/developerguide/index.rst
index d35a7ddf32..85ae47ff7b 100644
--- a/docs-source/developerguide/index.rst
+++ b/docs-source/developerguide/index.rst
@@ -18,6 +18,7 @@ OpenMM Developer's Guide
    06_opencl_platform.rst
    07_cuda_platform.rst
    08_common_compute.rst
+   09_customcppforceimpl.rst
 
 .. only:: html
 
diff --git a/docs-source/developerguide/license.rst b/docs-source/developerguide/license.rst
index 1643bda88c..83e1f5c53b 100644
--- a/docs-source/developerguide/license.rst
+++ b/docs-source/developerguide/license.rst
@@ -1,4 +1,4 @@
-Portions copyright (c) 2011-2020 Stanford University and the Authors
+Portions copyright (c) 2011-2023 Stanford University and the Authors
 
 Contributors: Peter Eastman
 
diff --git a/olla/include/openmm/kernels.h b/olla/include/openmm/kernels.h
index 5c6a994bba..a0ca3fb4be 100644
--- a/olla/include/openmm/kernels.h
+++ b/olla/include/openmm/kernels.h
@@ -67,6 +67,7 @@
 #include "openmm/NoseHooverIntegrator.h"
 #include "openmm/NoseHooverChain.h"
 #include "openmm/ATMForce.h"
+#include "openmm/internal/CustomCPPForceImpl.h"
 #include <iosfwd>
 #include <set>
 #include <string>
@@ -1684,8 +1685,33 @@ class CalcATMForceKernel : public KernelImpl {
     virtual void copyState(ContextImpl& context, ContextImpl& innerContext0, ContextImpl& innerContext1) = 0;
 };
 
-  
-
+/**
+ * This kernel is invoked by CustomCPPForce to calculate the forces acting on the system and the energy of the system.
+ */
+class CalcCustomCPPForceKernel : public KernelImpl {
+public:
+    static std::string Name() {
+        return "CalcCustomCPPForce";
+    }
+    CalcCustomCPPForceKernel(std::string name, const Platform& platform) : KernelImpl(name, platform) {
+    }
+    /**
+     * Initialize the kernel.
+     *
+     * @param system     the System this kernel will be applied to
+     * @param force      the CustomCPPForceImpl this kernel will be used for
+     */
+    virtual void initialize(const System& system, CustomCPPForceImpl& force) = 0;
+    /**
+     * Execute the kernel to calculate the forces and/or energy.
+     *
+     * @param context        the context in which to execute this kernel
+     * @param includeForces  true if forces should be calculated
+     * @param includeEnergy  true if the energy should be calculated
+     * @return the potential energy due to the force
+     */
+    virtual double execute(ContextImpl& context, bool includeForces, bool includeEnergy) = 0;
+};
 
 } // namespace OpenMM
 
diff --git a/openmmapi/include/openmm/internal/CustomCPPForceImpl.h b/openmmapi/include/openmm/internal/CustomCPPForceImpl.h
new file mode 100644
index 0000000000..41fafb40e9
--- /dev/null
+++ b/openmmapi/include/openmm/internal/CustomCPPForceImpl.h
@@ -0,0 +1,120 @@
+#ifndef OPENMM_CUSTOMCPPFORCEIMPL_H_
+#define OPENMM_CUSTOMCPPFORCEIMPL_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) 2023 Stanford University and the Authors.           *
+ * Authors: Peter Eastman                                                     *
+ * Contributors:                                                              *
+ *                                                                            *
+ * Permission is hereby granted, free of charge, to any person obtaining a    *
+ * copy of this software and associated documentation files (the "Software"), *
+ * to deal in the Software without restriction, including without limitation  *
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,   *
+ * and/or sell copies of the Software, and to permit persons to whom the      *
+ * Software is furnished to do so, subject to the following conditions:       *
+ *                                                                            *
+ * The above copyright notice and this permission notice shall be included in *
+ * all copies or substantial portions of the Software.                        *
+ *                                                                            *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR *
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,   *
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL    *
+ * THE AUTHORS, CONTRIBUTORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,    *
+ * DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR      *
+ * OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE  *
+ * USE OR OTHER DEALINGS IN THE SOFTWARE.                                     *
+ * -------------------------------------------------------------------------- */
+
+#include "ForceImpl.h"
+#include "openmm/Kernel.h"
+#include "openmm/Vec3.h"
+#include <string>
+#include <vector>
+
+namespace OpenMM {
+
+/**
+ * This class provides an easy way of writing Forces that are implemented entirely
+ * in C++, and do not involve any platform-specific code.  To use it, have your
+ * ForceImpl extend this class, implementing computeForce() to compute the forces
+ * and energy in a platform-independent way.  You can optionally override other
+ * methods as well, such as if your Force defines global parameters or bonds
+ * between atoms.
+ * 
+ * Most Forces should not be implemented with this class.  Writing platform-specific
+ * code that can run on a GPU and interact directly with internal data structures
+ * usually leads to much faster performance.  This class is useful for special
+ * situations, such as plugins that interact with external libraries through their
+ * own public APIs.  In that case, there is little opportunity for optimization.
+ * This class allows you to write a single implementation that is automatically
+ * used for all platforms.
+ */
+
+class OPENMM_EXPORT CustomCPPForceImpl : public ForceImpl {
+public:
+    CustomCPPForceImpl(const Force& owner);
+    /**
+     * Subclasses may override this to do their own initialization, but they should
+     * also call CustomCPPForceImpl::initialize(context) to let the parent class
+     * initialize its kernel.
+     * 
+     * @param context    the context in which the system is being simulated
+     */
+    void initialize(ContextImpl& context);
+    /**
+     * Calculate the force on each particle generated by this ForceImpl and/or this ForceImpl's
+     * contribution to the potential energy of the system.
+     * 
+     * @param context    the context in which the system is being simulated
+     * @param positions  the position of every particle
+     * @param forces     store the force on each particle into this array
+     * @return this force's contribution to the potential energy of the system, or 0 if this
+     * force does not contribute to potential energy
+     */
+    virtual double computeForce(ContextImpl& context, const std::vector<Vec3>& positions, std::vector<Vec3>& forces) = 0;
+    /**
+     * Override this if the force updates the context state directly.  In general
+     * it should only make changes by invoking public methods of the ContextImpl.
+     * If you need to access platform-specific data structures, this class is
+     * probably not appropriate.
+     * 
+     * @param context        the context in which the system is being simulated
+     * @param forcesInvalid  if the state was modified in any way that might cause previously
+     *                       calculated forces to no longer be valid (such as modifying
+     *                       positions or parameters), the method should set this to true.
+     */
+    void updateContextState(ContextImpl& context, bool& forcesInvalid) {
+    }
+    /**
+     * Override this if the force defines global parameters.
+     */
+    std::map<std::string, double> getDefaultParameters() {
+        return {};
+    }
+    /**
+     * Override this if the force defines bonds between particles.
+     */
+    std::vector<std::pair<int, int> > getBondedParticles() const {
+        return {};
+    }
+    /**
+     * Override this if the force defines an updateParametersInContext() method.
+     */
+    void updateParametersInContext(ContextImpl& context) {
+    }
+    double calcForcesAndEnergy(ContextImpl& context, bool includeForces, bool includeEnergy, int groups);
+    std::vector<std::string> getKernelNames();
+private:
+    Kernel kernel;
+};
+
+} // namespace OpenMM
+
+#endif /*OPENMM_CUSTOMCPPFORCEIMPL_H_*/
diff --git a/openmmapi/src/CustomCPPForceImpl.cpp b/openmmapi/src/CustomCPPForceImpl.cpp
new file mode 100644
index 0000000000..415662acb1
--- /dev/null
+++ b/openmmapi/src/CustomCPPForceImpl.cpp
@@ -0,0 +1,58 @@
+/* -------------------------------------------------------------------------- *
+ *                                   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-2021 Stanford University and the Authors.      *
+ * Authors: Peter Eastman                                                     *
+ * 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/ContextImpl.h"
+#include "openmm/internal/CustomCPPForceImpl.h"
+#include "openmm/kernels.h"
+
+using namespace OpenMM;
+using namespace std;
+
+CustomCPPForceImpl::CustomCPPForceImpl(const Force& owner) {
+    forceGroup = owner.getForceGroup();
+}
+
+void CustomCPPForceImpl::initialize(ContextImpl& context) {
+    kernel = context.getPlatform().createKernel(CalcCustomCPPForceKernel::Name(), context);
+    kernel.getAs<CalcCustomCPPForceKernel>().initialize(context.getSystem(), *this);
+}
+
+double CustomCPPForceImpl::calcForcesAndEnergy(ContextImpl& context, bool includeForces, bool includeEnergy, int groups) {
+    if ((groups&(1<<forceGroup)) != 0)
+        return kernel.getAs<CalcCustomCPPForceKernel>().execute(context, includeForces, includeEnergy);
+    return 0.0;
+}
+
+vector<string> CustomCPPForceImpl::getKernelNames() {
+    vector<string> names;
+    names.push_back(CalcCustomCPPForceKernel::Name());
+    return names;
+}
diff --git a/platforms/common/include/openmm/common/CommonKernels.h b/platforms/common/include/openmm/common/CommonKernels.h
index 3c4badb2f7..b60faf54c6 100644
--- a/platforms/common/include/openmm/common/CommonKernels.h
+++ b/platforms/common/include/openmm/common/CommonKernels.h
@@ -1654,7 +1654,57 @@ class CommonCalcATMForceKernel : public CalcATMForceKernel {
     int numParticles;
 };
 
-  
+/**
+ * This kernel is invoked by CustomCPPForce to calculate the forces acting on the system and the energy of the system.
+ */
+class CommonCalcCustomCPPForceKernel : public CalcCustomCPPForceKernel {
+public:
+    CommonCalcCustomCPPForceKernel(std::string name, const Platform& platform, OpenMM::ContextImpl& contextImpl, ComputeContext& cc) :
+            CalcCustomCPPForceKernel(name, platform), contextImpl(contextImpl), cc(cc), force(NULL) {
+    }
+    /**
+     * Initialize the kernel.
+     *
+     * @param system     the System this kernel will be applied to
+     * @param force      the CustomCPPForceImpl this kernel will be used for
+     */
+    void initialize(const System& system, CustomCPPForceImpl& 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);
+    /**
+     * The is called by the pre-computation to start the calculation running.
+     */
+    void beginComputation(bool includeForces, bool includeEnergy, int groups);
+    /**
+     * This is called by the worker thread to do the computation.
+     */
+    void executeOnWorkerThread(bool includeForces);
+    /**
+     * This is called by the post-computation to add the forces to the main array.
+     */
+    double addForces(bool includeForces, bool includeEnergy, int groups);
+private:
+    class ExecuteTask;
+    class StartCalculationPreComputation;
+    class AddForcesPostComputation;
+    OpenMM::ContextImpl& contextImpl;
+    ComputeContext& cc;
+    CustomCPPForceImpl* force;
+    ComputeArray forcesArray;
+    ComputeKernel addForcesKernel;
+    std::vector<Vec3> positionsVec, forcesVec;
+    std::vector<float> floatForces;
+    int forceGroupFlag;
+    double energy;
+};
+
 } // namespace OpenMM
 
 #endif /*OPENMM_COMMONKERNELS_H_*/
diff --git a/platforms/common/src/CommonKernels.cpp b/platforms/common/src/CommonKernels.cpp
index 0bacfdc31b..6474959527 100644
--- a/platforms/common/src/CommonKernels.cpp
+++ b/platforms/common/src/CommonKernels.cpp
@@ -7974,3 +7974,111 @@ void CommonCalcATMForceKernel::copyParametersToContext(ContextImpl& context, con
     displ1.upload(displVectorContext1);
     displ0.upload(displVectorContext0);
 }
+
+class CommonCalcCustomCPPForceKernel::StartCalculationPreComputation : public ComputeContext::ForcePreComputation {
+public:
+    StartCalculationPreComputation(CommonCalcCustomCPPForceKernel& owner) : owner(owner) {
+    }
+    void computeForceAndEnergy(bool includeForces, bool includeEnergy, int groups) {
+        owner.beginComputation(includeForces, includeEnergy, groups);
+    }
+    CommonCalcCustomCPPForceKernel& owner;
+};
+
+class CommonCalcCustomCPPForceKernel::ExecuteTask : public ComputeContext::WorkTask {
+public:
+    ExecuteTask(CommonCalcCustomCPPForceKernel& owner, bool includeForces) : owner(owner), includeForces(includeForces) {
+    }
+    void execute() {
+        owner.executeOnWorkerThread(includeForces);
+    }
+    CommonCalcCustomCPPForceKernel& owner;
+    bool includeForces;
+};
+
+class CommonCalcCustomCPPForceKernel::AddForcesPostComputation : public ComputeContext::ForcePostComputation {
+public:
+    AddForcesPostComputation(CommonCalcCustomCPPForceKernel& owner) : owner(owner) {
+    }
+    double computeForceAndEnergy(bool includeForces, bool includeEnergy, int groups) {
+        return owner.addForces(includeForces, includeEnergy, groups);
+    }
+    CommonCalcCustomCPPForceKernel& owner;
+};
+
+void CommonCalcCustomCPPForceKernel::initialize(const System& system, CustomCPPForceImpl& force) {
+    ContextSelector selector(cc);
+    this->force = &force;
+    int numParticles = system.getNumParticles();
+    forcesVec.resize(numParticles);
+    positionsVec.resize(numParticles);
+    floatForces.resize(3*numParticles);
+    int elementSize = (cc.getUseDoublePrecision() ? sizeof(double) : sizeof(float));
+    forcesArray.initialize(cc, 3*numParticles, elementSize, "forces");
+    map<string, string> defines;
+    defines["NUM_ATOMS"] = cc.intToString(numParticles);
+    defines["PADDED_NUM_ATOMS"] = cc.intToString(cc.getPaddedNumAtoms());
+    ComputeProgram program = cc.compileProgram(CommonKernelSources::customCppForce, defines);
+    addForcesKernel = program->createKernel("addForces");
+    addForcesKernel->addArg(forcesArray);
+    addForcesKernel->addArg(cc.getLongForceBuffer());
+    addForcesKernel->addArg(cc.getAtomIndexArray());
+    forceGroupFlag = (1<<force.getOwner().getForceGroup());
+    cc.addPreComputation(new StartCalculationPreComputation(*this));
+    cc.addPostComputation(new AddForcesPostComputation(*this));
+}
+
+double CommonCalcCustomCPPForceKernel::execute(ContextImpl& context, bool includeForces, bool includeEnergy) {
+    // This method does nothing.  The actual calculation is started by the pre-computation, continued on
+    // the worker thread, and finished by the post-computation.
+    
+    return 0;
+}
+
+void CommonCalcCustomCPPForceKernel::beginComputation(bool includeForces, bool includeEnergy, int groups) {
+    if ((groups&forceGroupFlag) == 0)
+        return;
+    contextImpl.getPositions(positionsVec);
+    
+    // The actual force computation will be done on a different thread.
+    
+    cc.getWorkThread().addTask(new ExecuteTask(*this, includeForces));
+}
+
+void CommonCalcCustomCPPForceKernel::executeOnWorkerThread(bool includeForces) {
+    energy = force->computeForce(contextImpl, positionsVec, forcesVec);
+    if (includeForces) {
+        ContextSelector selector(cc);
+        int numParticles = cc.getNumAtoms();
+        if (cc.getUseDoublePrecision())
+            forcesArray.upload((double*) forcesVec.data());
+        else {
+            for (int i = 0; i < numParticles; i++) {
+                floatForces[3*i] = (float) forcesVec[i][0];
+                floatForces[3*i+1] = (float) forcesVec[i][1];
+                floatForces[3*i+2] = (float) forcesVec[i][2];
+            }
+            forcesArray.upload(floatForces);
+        }
+    }
+}
+
+double CommonCalcCustomCPPForceKernel::addForces(bool includeForces, bool includeEnergy, int groups) {
+    if ((groups&forceGroupFlag) == 0)
+        return 0;
+
+    // Wait until executeOnWorkerThread() is finished.
+    
+    cc.getWorkThread().flush();
+
+    // Add in the forces.
+    
+    if (includeForces) {
+        ContextSelector selector(cc);
+        addForcesKernel->execute(cc.getNumAtoms());
+    }
+    
+    // Return the energy.
+    
+    return energy;
+}
\ No newline at end of file
diff --git a/platforms/common/src/kernels/customCppForce.cc b/platforms/common/src/kernels/customCppForce.cc
new file mode 100644
index 0000000000..49e34a7a3a
--- /dev/null
+++ b/platforms/common/src/kernels/customCppForce.cc
@@ -0,0 +1,8 @@
+KERNEL void addForces(GLOBAL const real* RESTRICT forces, GLOBAL mm_long* RESTRICT forceBuffers, GLOBAL int* RESTRICT atomIndex) {
+    for (int atom = GLOBAL_ID; atom < NUM_ATOMS; atom += GLOBAL_SIZE) {
+        int index = atomIndex[atom];
+        forceBuffers[atom] += (mm_long) (forces[3*index]*0x100000000);
+        forceBuffers[atom+PADDED_NUM_ATOMS] += (mm_long) (forces[3*index+1]*0x100000000);
+        forceBuffers[atom+2*PADDED_NUM_ATOMS] += (mm_long) (forces[3*index+2]*0x100000000);
+    }
+}
diff --git a/platforms/cuda/src/CudaKernelFactory.cpp b/platforms/cuda/src/CudaKernelFactory.cpp
index 42b3fd943a..7298264b36 100644
--- a/platforms/cuda/src/CudaKernelFactory.cpp
+++ b/platforms/cuda/src/CudaKernelFactory.cpp
@@ -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) 2008-2019 Stanford University and the Authors.      *
+ * Portions copyright (c) 2008-2023 Stanford University and the Authors.      *
  * Authors: Peter Eastman                                                     *
  * Contributors:                                                              *
  *                                                                            *
@@ -113,6 +113,8 @@ KernelImpl* CudaKernelFactory::createKernelImpl(std::string name, const Platform
         return new CudaCalcCustomCVForceKernel(name, platform, cu);
     if (name == CalcATMForceKernel::Name())
         return new CudaCalcATMForceKernel(name, platform, cu);
+    if (name == CalcCustomCPPForceKernel::Name())
+        return new CommonCalcCustomCPPForceKernel(name, platform, context, cu);
     if (name == CalcRMSDForceKernel::Name())
         return new CommonCalcRMSDForceKernel(name, platform, cu);
     if (name == CalcCustomManyParticleForceKernel::Name())
diff --git a/platforms/cuda/src/CudaPlatform.cpp b/platforms/cuda/src/CudaPlatform.cpp
index 465eb0c944..1ff097ac93 100644
--- a/platforms/cuda/src/CudaPlatform.cpp
+++ b/platforms/cuda/src/CudaPlatform.cpp
@@ -91,6 +91,7 @@ CudaPlatform::CudaPlatform() {
     registerKernelFactory(CalcCustomHbondForceKernel::Name(), factory);
     registerKernelFactory(CalcCustomCentroidBondForceKernel::Name(), factory);
     registerKernelFactory(CalcCustomCompoundBondForceKernel::Name(), factory);
+    registerKernelFactory(CalcCustomCPPForceKernel::Name(), factory);
     registerKernelFactory(CalcCustomCVForceKernel::Name(), factory);
     registerKernelFactory(CalcATMForceKernel::Name(), factory);
     registerKernelFactory(CalcRMSDForceKernel::Name(), factory);
diff --git a/platforms/opencl/src/OpenCLKernelFactory.cpp b/platforms/opencl/src/OpenCLKernelFactory.cpp
index bdd7c4457b..b55f1a07cd 100644
--- a/platforms/opencl/src/OpenCLKernelFactory.cpp
+++ b/platforms/opencl/src/OpenCLKernelFactory.cpp
@@ -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) 2008-2019 Stanford University and the Authors.      *
+ * Portions copyright (c) 2008-2023 Stanford University and the Authors.      *
  * Authors: Peter Eastman                                                     *
  * Contributors:                                                              *
  *                                                                            *
@@ -110,7 +110,9 @@ KernelImpl* OpenCLKernelFactory::createKernelImpl(std::string name, const Platfo
     if (name == CalcCustomCVForceKernel::Name())
         return new OpenCLCalcCustomCVForceKernel(name, platform, cl);
     if (name == CalcATMForceKernel::Name())
-        return new OpenCLCalcATMForceKernel(name, platform, cl);    
+        return new OpenCLCalcATMForceKernel(name, platform, cl);
+    if (name == CalcCustomCPPForceKernel::Name())
+        return new CommonCalcCustomCPPForceKernel(name, platform, context, cl);
     if (name == CalcRMSDForceKernel::Name())
         return new CommonCalcRMSDForceKernel(name, platform, cl);
     if (name == CalcCustomManyParticleForceKernel::Name())
diff --git a/platforms/opencl/src/OpenCLPlatform.cpp b/platforms/opencl/src/OpenCLPlatform.cpp
index 8bfed33a3d..feaa15a25d 100644
--- a/platforms/opencl/src/OpenCLPlatform.cpp
+++ b/platforms/opencl/src/OpenCLPlatform.cpp
@@ -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) 2008-2018 Stanford University and the Authors.      *
+ * Portions copyright (c) 2008-2023 Stanford University and the Authors.      *
  * Authors: Peter Eastman                                                     *
  * Contributors:                                                              *
  *                                                                            *
@@ -82,6 +82,7 @@ OpenCLPlatform::OpenCLPlatform() {
     registerKernelFactory(CalcCustomHbondForceKernel::Name(), factory);
     registerKernelFactory(CalcCustomCentroidBondForceKernel::Name(), factory);
     registerKernelFactory(CalcCustomCompoundBondForceKernel::Name(), factory);
+    registerKernelFactory(CalcCustomCPPForceKernel::Name(), factory);
     registerKernelFactory(CalcCustomCVForceKernel::Name(), factory);
     registerKernelFactory(CalcATMForceKernel::Name(), factory);
     registerKernelFactory(CalcRMSDForceKernel::Name(), factory);
diff --git a/platforms/reference/include/ReferenceKernels.h b/platforms/reference/include/ReferenceKernels.h
index 22ea8f71a0..962cc26c13 100644
--- a/platforms/reference/include/ReferenceKernels.h
+++ b/platforms/reference/include/ReferenceKernels.h
@@ -34,6 +34,7 @@
 
 #include "ReferencePlatform.h"
 #include "openmm/kernels.h"
+#include "openmm/internal/CustomCPPForceImpl.h"
 #include "openmm/internal/CustomNonbondedForceImpl.h"
 #include "SimTKOpenMMRealType.h"
 #include "ReferenceNeighborList.h"
@@ -1680,6 +1681,34 @@ class ReferenceCalcATMForceKernel : public CalcATMForceKernel {
     std::vector<Vec3> displ0;
 };
 
+/**
+ * This kernel is invoked by CustomCPPForceImpl to calculate the forces acting on the system and the energy of the system.
+ */
+class ReferenceCalcCustomCPPForceKernel : public CalcCustomCPPForceKernel {
+public:
+    ReferenceCalcCustomCPPForceKernel(std::string name, const Platform& platform) : CalcCustomCPPForceKernel(name, platform), force(NULL) {
+    }
+    /**
+     * Initialize the kernel.
+     *
+     * @param system     the System this kernel will be applied to
+     * @param force      the CustomCPPForceImpl this kernel will be used for
+     */
+    void initialize(const System& system, CustomCPPForceImpl& 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);
+private:
+    CustomCPPForceImpl* force;
+    std::vector<Vec3> forces;
+};
+
 } // namespace OpenMM
 
 #endif /*OPENMM_REFERENCEKERNELS_H_*/
diff --git a/platforms/reference/src/ReferenceKernelFactory.cpp b/platforms/reference/src/ReferenceKernelFactory.cpp
index bba17d0439..35c72d049d 100644
--- a/platforms/reference/src/ReferenceKernelFactory.cpp
+++ b/platforms/reference/src/ReferenceKernelFactory.cpp
@@ -82,6 +82,8 @@ KernelImpl* ReferenceKernelFactory::createKernelImpl(std::string name, const Pla
         return new ReferenceCalcCustomCVForceKernel(name, platform);
     if (name == CalcATMForceKernel::Name())
         return new ReferenceCalcATMForceKernel(name, platform);
+    if (name == CalcCustomCPPForceKernel::Name())
+        return new ReferenceCalcCustomCPPForceKernel(name, platform);
     if (name == CalcRMSDForceKernel::Name())
         return new ReferenceCalcRMSDForceKernel(name, platform);
     if (name == CalcCustomManyParticleForceKernel::Name())
diff --git a/platforms/reference/src/ReferenceKernels.cpp b/platforms/reference/src/ReferenceKernels.cpp
index 0fc8eb177c..d9e94a4f11 100644
--- a/platforms/reference/src/ReferenceKernels.cpp
+++ b/platforms/reference/src/ReferenceKernels.cpp
@@ -3032,3 +3032,18 @@ void ReferenceCalcATMForceKernel::copyParametersToContext(ContextImpl& context,
         displ0[i] = displacement0;
     }
 }
+
+void ReferenceCalcCustomCPPForceKernel::initialize(const System& system, CustomCPPForceImpl& force) {
+    this->force = &force;
+    forces.resize(system.getNumParticles());
+}
+
+double ReferenceCalcCustomCPPForceKernel::execute(ContextImpl& context, bool includeForces, bool includeEnergy) {
+    vector<Vec3>& posData = extractPositions(context);
+    vector<Vec3>& forceData = extractForces(context);
+    double energy = force->computeForce(context, posData, forces);
+    if (includeForces)
+        for (int i = 0; i < forces.size(); i++)
+            forceData[i] += forces[i];
+    return energy;
+}
diff --git a/platforms/reference/src/ReferencePlatform.cpp b/platforms/reference/src/ReferencePlatform.cpp
index 09c7368058..8269f7cf7a 100644
--- a/platforms/reference/src/ReferencePlatform.cpp
+++ b/platforms/reference/src/ReferencePlatform.cpp
@@ -61,6 +61,7 @@ ReferencePlatform::ReferencePlatform() {
     registerKernelFactory(CalcCustomHbondForceKernel::Name(), factory);
     registerKernelFactory(CalcCustomCentroidBondForceKernel::Name(), factory);
     registerKernelFactory(CalcCustomCompoundBondForceKernel::Name(), factory);
+    registerKernelFactory(CalcCustomCPPForceKernel::Name(), factory);
     registerKernelFactory(CalcCustomCVForceKernel::Name(), factory);
     registerKernelFactory(CalcATMForceKernel::Name(), factory);
     registerKernelFactory(CalcRMSDForceKernel::Name(), factory);