Task: DrudeForce supports periodic boundary conditions (openmm/openmm#4523)

org: openmm
repo: openmm
pr_number: 4523
issue_title: DrudeForce supports periodic boundary conditions
issue_body: Fixes #4521.
resolved_issues: number: 4521
title: Periodic boundary conditions with DrudeForce 
body: Hi, I have a question regarding periodic boundary conditions and the DrudeForce. From the documentation for `DrudeForce` I understand that `drudeforce.usesPeriodicBoundaryConditions()` will return `True` or `False` depending on if the `NonbondedForce` uses PBC. However in my case `drudeforce.usesPeriodicBoundaryConditions()` returns `False` although `nonbondedforce.usesPeriodicBoundaryConditions()` returns `True`.  

Since there is no `drudeforce.setUsesPeriodicBoundaryConditions(True)` how can I set `drudeforce.usesPeriodicBoundaryConditions()` to `True`?

I attach my code:

```
# Import openmm
from openmm.app import *
from openmm import *
from openmm.unit import *
from sys import stdout


# Read force field from XML file

forcefield = ForceField("forcefield.xml")


# Read positions and topology from Gromacs files, create a modeller object and add extra particles

gro = GromacsGroFile("slab.gro")

top = GromacsTopFile("topol.top", periodicBoxVectors=gro.getPeriodicBoxVectors(), includeDir='/usr/local/gromacs/share/gromacs/top') #gromacs top file

modeller = Modeller(top.topology,gro.positions)

modeller.addExtraParticles(forcefield)


# Create a system

system = forcefield.createSystem(modeller.topology, nonbondedMethod=PME, nonbondedCutoff=1.0*nanometer, constraints=HBonds)


# Activate PBC for bonds and angles

forces = system.getForces()

for force in forces:

    force_name = force.__class__.__name__

    if force_name == "HarmonicBondForce":

        force.setUsesPeriodicBoundaryConditions(True)

    elif force_name == "HarmonicAngleForce":

        force.setUsesPeriodicBoundaryConditions(True)


# Check options for periodic boundary conditions

for force in forces:

    force_name = force.__class__.__name__

    if force.usesPeriodicBoundaryConditions():
        
        print(f"{force_name} uses PBC")

    else: 
        print(f"{force_name} does not use PBC")
```
The output is

```
HarmonicBondForce uses PBC
NonbondedForce uses PBC
DrudeForce does not use PBC
CMMotionRemover does not use PBC
HarmonicAngleForce uses PBC
```



### Minimal API referenced by tests (typed signatures)
_Added only when tests call brand‑new symbols introduced by the fix; types reflect annotations present in the repo or `Unknown` if not annotated._

- Function in `plugins/drude/openmmapi/src/DrudeForce.cpp`: `def void DrudeForce::setUsesPeriodicBoundaryConditions(periodic: bool) -> void: ...`
  **Inputs:** periodic: bool
  **Output:** void


```python
def void DrudeForce::setUsesPeriodicBoundaryConditions(periodic) -> void: ...
```

## 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/plugins/drude/openmmapi/include/openmm/DrudeForce.h b/plugins/drude/openmmapi/include/openmm/DrudeForce.h
index 0ae8980b95..c4483c4331 100644
--- a/plugins/drude/openmmapi/include/openmm/DrudeForce.h
+++ b/plugins/drude/openmmapi/include/openmm/DrudeForce.h
@@ -9,7 +9,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-2024 Stanford University and the Authors.      *
  * Authors: Peter Eastman                                                     *
  * Contributors:                                                              *
  *                                                                            *
@@ -134,7 +134,7 @@ class OPENMM_EXPORT_DRUDE DrudeForce : public Force {
      */
     int addScreenedPair(int particle1, int particle2, double thole);
     /**
-     * Get the force field parameters for screened pair.
+     * Get the force field parameters for a screened pair.
      *
      * @param index           the index of the pair for which to get parameters
      * @param[out] particle1  the index within this Force of the first particle involved in the interaction
@@ -143,7 +143,7 @@ class OPENMM_EXPORT_DRUDE DrudeForce : public Force {
      */
     void getScreenedPairParameters(int index, int& particle1, int& particle2, double& thole) const;
     /**
-     * Set the force field parameters for screened pair.
+     * Set the force field parameters for a screened pair.
      *
      * @param index      the index of the pair for which to get parameters
      * @param particle1  the index within this Force of the first particle involved in the interaction
@@ -162,15 +162,19 @@ class OPENMM_EXPORT_DRUDE DrudeForce : public Force {
      * be used to add new particles or screenedPairs, only to change the parameters of existing ones.
      */
     void updateParametersInContext(Context& context);
+    /**
+     * Set whether this force should apply periodic boundary conditions when calculating displacements.
+     * Usually this is not appropriate for bonded forces, but there are situations when it can be useful.
+     * 
+     * Periodic boundary conditions are only applied to screened pairs.  They are never used for the
+     * force between a Drude particle and its parent particle, regardless of this setting.
+     */
+    void setUsesPeriodicBoundaryConditions(bool periodic);
     /**
      * Returns whether or not this force makes use of periodic boundary
      * conditions.
-     *
-     * @returns true if nonbondedMethod uses PBC and false otherwise
      */
-    bool usesPeriodicBoundaryConditions() const {
-        return false;
-    }
+    bool usesPeriodicBoundaryConditions() const;
 protected:
     ForceImpl* createImpl() const;
 private:
@@ -178,6 +182,7 @@ class OPENMM_EXPORT_DRUDE DrudeForce : public Force {
     class ScreenedPairInfo;
     std::vector<ParticleInfo> particles;
     std::vector<ScreenedPairInfo> screenedPairs;
+    bool usePeriodic;
 };
 
 /**
diff --git a/plugins/drude/openmmapi/src/DrudeForce.cpp b/plugins/drude/openmmapi/src/DrudeForce.cpp
index b46b0a8f32..b07f23844a 100644
--- a/plugins/drude/openmmapi/src/DrudeForce.cpp
+++ b/plugins/drude/openmmapi/src/DrudeForce.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) 2013-2021 Stanford University and the Authors.      *
+ * Portions copyright (c) 2013-2024 Stanford University and the Authors.      *
  * Authors: Peter Eastman                                                     *
  * Contributors:                                                              *
  *                                                                            *
@@ -38,7 +38,7 @@
 using namespace OpenMM;
 using namespace std;
 
-DrudeForce::DrudeForce() {
+DrudeForce::DrudeForce() : usePeriodic(false) {
 }
 
 int DrudeForce::addParticle(int particle, int particle1, int particle2, int particle3, int particle4, double charge, double polarizability, double aniso12, double aniso34) {
@@ -105,3 +105,11 @@ ForceImpl* DrudeForce::createImpl() const {
 void DrudeForce::updateParametersInContext(Context& context) {
     dynamic_cast<DrudeForceImpl&>(getImplInContext(context)).updateParametersInContext(getContextImpl(context));
 }
+
+void DrudeForce::setUsesPeriodicBoundaryConditions(bool periodic) {
+    usePeriodic = periodic;
+}
+
+bool DrudeForce::usesPeriodicBoundaryConditions() const {
+    return usePeriodic;
+}
diff --git a/plugins/drude/platforms/common/src/CommonDrudeKernels.cpp b/plugins/drude/platforms/common/src/CommonDrudeKernels.cpp
index 77c9aaa026..2da3aeba1e 100644
--- a/plugins/drude/platforms/common/src/CommonDrudeKernels.cpp
+++ b/plugins/drude/platforms/common/src/CommonDrudeKernels.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) 2013-2023 Stanford University and the Authors.      *
+ * Portions copyright (c) 2013-2024 Stanford University and the Authors.      *
  * Authors: Peter Eastman                                                     *
  * Contributors:                                                              *
  *                                                                            *
@@ -158,6 +158,7 @@ void CommonCalcDrudeForceKernel::initialize(const System& system, const DrudeFor
         }
         pairParams.upload(paramVector);
         map<string, string> replacements;
+        replacements["APPLY_PERIODIC"] = (force.usesPeriodicBoundaryConditions() ? "1" : "0");
         replacements["PARAMS"] = cc.getBondedUtilities().addArgument(pairParams, "float2");
         cc.getBondedUtilities().addInteraction(atoms, cc.replaceStrings(CommonDrudeKernelSources::drudePairForce, replacements), force.getForceGroup());
     }
diff --git a/plugins/drude/platforms/common/src/kernels/drudePairForce.cc b/plugins/drude/platforms/common/src/kernels/drudePairForce.cc
index 3cf79f07fa..ced2638ee1 100644
--- a/plugins/drude/platforms/common/src/kernels/drudePairForce.cc
+++ b/plugins/drude/platforms/common/src/kernels/drudePairForce.cc
@@ -7,6 +7,9 @@ real3 force4 = make_real3(0);
 // First pair.
 
 real3 delta = make_real3(pos1.x-pos3.x, pos1.y-pos3.y, pos1.z-pos3.z);
+#if APPLY_PERIODIC
+APPLY_PERIODIC_TO_DELTA(delta)
+#endif
 real rInv = RSQRT(dot(delta, delta));
 real r = RECIP(rInv);
 real u = drudeParams.x*r;
@@ -20,6 +23,9 @@ force3 -= f;
 // Second pair.
 
 delta = make_real3(pos1.x-pos4.x, pos1.y-pos4.y, pos1.z-pos4.z);
+#if APPLY_PERIODIC
+APPLY_PERIODIC_TO_DELTA(delta)
+#endif
 rInv = RSQRT(dot(delta, delta));
 r = RECIP(rInv);
 u = drudeParams.x*r;
@@ -33,6 +39,9 @@ force4 -= f;
 // Third pair.
 
 delta = make_real3(pos2.x-pos3.x, pos2.y-pos3.y, pos2.z-pos3.z);
+#if APPLY_PERIODIC
+APPLY_PERIODIC_TO_DELTA(delta)
+#endif
 rInv = RSQRT(dot(delta, delta));
 r = RECIP(rInv);
 u = drudeParams.x*r;
@@ -46,6 +55,9 @@ force3 -= f;
 // Fourth pair.
 
 delta = make_real3(pos2.x-pos4.x, pos2.y-pos4.y, pos2.z-pos4.z);
+#if APPLY_PERIODIC
+APPLY_PERIODIC_TO_DELTA(delta)
+#endif
 rInv = RSQRT(dot(delta, delta));
 r = RECIP(rInv);
 u = drudeParams.x*r;
diff --git a/plugins/drude/platforms/reference/src/ReferenceDrudeKernels.cpp b/plugins/drude/platforms/reference/src/ReferenceDrudeKernels.cpp
index b97e836d31..087719f145 100644
--- a/plugins/drude/platforms/reference/src/ReferenceDrudeKernels.cpp
+++ b/plugins/drude/platforms/reference/src/ReferenceDrudeKernels.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) 2011-2023 Stanford University and the Authors.      *
+ * Portions copyright (c) 2011-2024 Stanford University and the Authors.      *
  * Authors: Peter Eastman                                                     *
  * Contributors:                                                              *
  *                                                                            *
@@ -36,6 +36,7 @@
 #include "SimTKOpenMMUtilities.h"
 #include "ReferenceConstraints.h"
 #include "ReferenceVirtualSites.h"
+#include "ReferenceForce.h"
 #include <set>
 
 using namespace OpenMM;
@@ -56,6 +57,11 @@ static vector<Vec3>& extractForces(ContextImpl& context) {
     return *data->forces;
 }
 
+static Vec3* extractBoxVectors(ContextImpl& context) {
+    ReferencePlatform::PlatformData* data = reinterpret_cast<ReferencePlatform::PlatformData*>(context.getPlatformData());
+    return data->periodicBoxVectors;
+}
+
 static ReferenceConstraints& extractConstraints(ContextImpl& context) {
     ReferencePlatform::PlatformData* data = reinterpret_cast<ReferencePlatform::PlatformData*>(context.getPlatformData());
     return *data->constraints;
@@ -103,6 +109,7 @@ void ReferenceCalcDrudeForceKernel::initialize(const System& system, const Drude
     pairThole.resize(numPairs);
     for (int i = 0; i < numPairs; i++)
         force.getScreenedPairParameters(i, pair1[i], pair2[i], pairThole[i]);
+    periodic = force.usesPeriodicBoundaryConditions();
 }
 
 double ReferenceCalcDrudeForceKernel::execute(ContextImpl& context, bool includeForces, bool includeEnergy) {
@@ -170,6 +177,7 @@ double ReferenceCalcDrudeForceKernel::execute(ContextImpl& context, bool include
     // Compute the screened interaction between bonded dipoles.
     
     int numPairs = pair1.size();
+    Vec3* boxVectors = extractBoxVectors(context);
     for (int i = 0; i < numPairs; i++) {
         int dipole1 = pair1[i];
         int dipole2 = pair2[i];
@@ -181,8 +189,13 @@ double ReferenceCalcDrudeForceKernel::execute(ContextImpl& context, bool include
                 int p1 = dipole1Particles[j];
                 int p2 = dipole2Particles[k];
                 double chargeProduct = charge[dipole1]*charge[dipole2]*(j == k ? 1 : -1);
-                Vec3 delta = pos[p1]-pos[p2];
-                double r = sqrt(delta.dot(delta));
+                double deltaR[ReferenceForce::LastDeltaRIndex];
+                if (periodic)
+                    ReferenceForce::getDeltaRPeriodic(pos[p2], pos[p1], boxVectors, deltaR);
+                else
+                    ReferenceForce::getDeltaR(pos[p2], pos[p1], deltaR);
+                Vec3 delta(deltaR[ReferenceForce::XIndex], deltaR[ReferenceForce::YIndex], deltaR[ReferenceForce::ZIndex]);
+                double r = deltaR[ReferenceForce::RIndex];
                 double u = r*uscale;
                 double screening = 1.0 - (1.0+0.5*u)*exp(-u);
                 energy += ONE_4PI_EPS0*chargeProduct*screening/r;
diff --git a/plugins/drude/platforms/reference/src/ReferenceDrudeKernels.h b/plugins/drude/platforms/reference/src/ReferenceDrudeKernels.h
index 2f462c948c..05f0f22924 100644
--- a/plugins/drude/platforms/reference/src/ReferenceDrudeKernels.h
+++ b/plugins/drude/platforms/reference/src/ReferenceDrudeKernels.h
@@ -9,7 +9,7 @@
  * Biological Structures at Stanford, funded under the NIH Roadmap for        *
  * Medical Research, grant U54 GM072970. See https://simtk.org.               *
  *                                                                            *
- * Portions copyright (c) 2013-2023 Stanford University and the Authors.      *
+ * Portions copyright (c) 2013-2024 Stanford University and the Authors.      *
  * Authors: Peter Eastman                                                     *
  * Contributors:                                                              *
  *                                                                            *
@@ -77,6 +77,7 @@ class ReferenceCalcDrudeForceKernel : public CalcDrudeForceKernel {
     std::vector<double> charge, polarizability, aniso12, aniso34;
     std::vector<int> pair1, pair2;
     std::vector<double> pairThole;
+    bool periodic;
 };
 
 /**