Task: Radius of gyration force (openmm/openmm#5031)

org: openmm
repo: openmm
pr_number: 5031
issue_title: Radius of gyration force
issue_body: This adds a RGForce class that computes the radius of gyration.  It is mainly intended for use with CustomCVForce so you can use Rg as part of restraint potentials, metadynamics, etc.

Fixes #5015.
resolved_issues: number: 5015
title: Radius of Gyration restraint force in OpenMM
body: Hello!

We are recently investigating moving from CHARMM to OpenMM for some coarse-grained MD runs for comparison with SAXS data. We are very excited with the potential performance of some initial runs. 

However, we are running into an issue with implementing a RGYR Restraint Force ala CHARMM in OpenMM. We followed your suggestion of creating one group per atom, and one group with all atoms, and a bond for each single-atom group with the group containing all atoms within a CustomCentroidBondForce wrapped in CustomCVForce that does the harmonic restraint. 
```   
num_atoms = len(atom_indices)
rg2_force = CustomCentroidBondForce(2, f"(distance(g1, g2)^2)/{num_atoms}")
for i in atom_indices:
    rg2_force.addGroup([i], [1.0])  # g1: single atom
rg2_force.addGroup(atom_indices, [1.0] * num_atoms
for i in range(num_atoms):
    rg2_force.addBond([i, num_atoms])  # last group is full group (index = num_atoms)
super().__init__("0.5 * k_rg * (sqrt(cv) - rg0)^2")
self.addGlobalParameter("k_rg", k_rg)
self.addGlobalParameter("rg0", rg0)
self.addCollectiveVariable("cv", rg2_force)
self.setForceGroup(force_group)
```

1. As you cautioned in the other issue - this works when the number of atoms involved in the calc of the collective variable is in 100s - time for setup is a few seconds. When the number of atoms which are involved in the calculation of Rg is closer to a 1000, then the setup time becomes slower but still few tens of seconds. However, when we go to ~8000 atoms, the code doesn't move past the Simulation creation step for ~30 mins (longest we waited to test it). Is this expected?
The attached test case uses ~8000 atoms for the Rg restraint - ```python scripts/md.py``` should repr it in the attached tar. There is an nsys profile attached in the tar in case its helpful. A profile for a successful run (around 1000 atoms) shows that most of this time, it is in the ```findMoleculeGroups()``` function. In the py code it is at the ```simulation = Simulation(modeller.topology, system, integrator)``` step. 
2. We see only one thread on the CPU being active when where it possibly hangs. Is this part expected to be multi-threaded or single threaded?  Is the setup part parallelizable? 
3. We are under the impression that the Rg / Rg-based force calculation itself is not time consuming, it is the setup part that is currently just hanging (or requiring time > 30 min to complete) - but the dynamics calculation after setup completes proceeds faster since its using the GPU/is optimized - is this right? 
4. So, if we implemented the restraint force via a plugin (i.e. without using the bond infrastructure), would that avoid the slow setup problem since we would not need to create a group of >> 1000 atoms involved in a bond - the possibly non-optimized single threaded part of the code? Is that a reasonable approach?


[openmm-test.tar.gz](https://github.com/user-attachments/files/21306632/openmm-test.tar.gz)

How you installed OpenMM (pip, conda, compile from source, etc.).
    - compiled from source 
- The operating system and hardware (e.g. GPU model) you are using.
    - Running on Perlmutter, A100
- The platform (CUDA, OpenCL, etc.) you are using.
   - CUDA 
- Any other software (Python libraries, setup tools, etc.) you are using along with OpenMM to implement your workflow.
   - None
- The output of the command `python -m openmm.testInstallation`. It reports details about your installation and checks for some basic problems.
```
OpenMM Version: 8.3.1
Git Revision: 6e13f1361dcc3ffb581c7c97e3adfcc61257b813

There are 3 Platforms available:

1 Reference - Successfully computed forces
2 CPU - Successfully computed forces
3 CUDA - Successfully computed forces

Median difference in forces between platforms:

Reference vs. CPU: 6.30823e-06
Reference vs. CUDA: 6.74442e-06
CPU vs. CUDA: 7.12587e-07

All differences are within tolerance.
```
Thanks!

## 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/api-c++/forces.rst b/docs-source/api-c++/forces.rst
index fdb99b272a..f1c45cb818 100644
--- a/docs-source/api-c++/forces.rst
+++ b/docs-source/api-c++/forces.rst
@@ -70,6 +70,7 @@ be combined in arbitrary ways.
     generated/MonteCarloBarostat
     generated/MonteCarloFlexibleBarostat
     generated/MonteCarloMembraneBarostat
+    generated/RGForce
     generated/RMSDForce
     generated/RPMDMonteCarloBarostat
 
diff --git a/docs-source/usersguide/theory/02_standard_forces.rst b/docs-source/usersguide/theory/02_standard_forces.rst
index 031990da80..9f8f6e5a1c 100644
--- a/docs-source/usersguide/theory/02_standard_forces.rst
+++ b/docs-source/usersguide/theory/02_standard_forces.rst
@@ -790,3 +790,21 @@ This force is normally used with a CustomCVForce (see Section :numref:`customcvf
 One rarely wants a force whose energy exactly equals the RMSD, but there are many
 situations where it is useful to have a restraining or biasing force that depends
 on the RMSD in some way.
+
+RGForce
+*********
+
+RGForce computes the radius of gyration (Rg) of a set of particle positions:
+
+.. math::
+   \text{Rg} = \sqrt{\frac{\sum_{i} \| \mathbf{x}_i - \mathbf{x}_c \|^2}{N}}
+
+where :math:`\mathbf{x}_c` is the center position,
+
+.. math::
+   \mathbf{x}_c = \frac{\sum_{i} \mathbf{x}_i}{N}
+
+This force is normally used with a CustomCVForce (see Section :numref:`customcvforce`).
+One rarely wants a force whose energy exactly equals Rg, but there are many
+situations where it is useful to have a restraining or biasing force that depends
+on Rg in some way.
diff --git a/olla/include/openmm/kernels.h b/olla/include/openmm/kernels.h
index bae1996a4d..be7e0bdbe0 100644
--- a/olla/include/openmm/kernels.h
+++ b/olla/include/openmm/kernels.h
@@ -59,6 +59,7 @@
 #include "openmm/PeriodicTorsionForce.h"
 #include "openmm/QTBIntegrator.h"
 #include "openmm/RBTorsionForce.h"
+#include "openmm/RGForce.h"
 #include "openmm/RMSDForce.h"
 #include "openmm/NonbondedForce.h"
 #include "openmm/System.h"
@@ -1071,6 +1072,34 @@ class CalcRMSDForceKernel : public KernelImpl {
     virtual void copyParametersToContext(ContextImpl& context, const RMSDForce& force) = 0;
 };
 
+/**
+ * This kernel is invoked by RGForce to calculate the forces acting on the system and the energy of the system.
+ */
+class CalcRGForceKernel : public KernelImpl {
+public:
+    static std::string Name() {
+        return "CalcRGForce";
+    }
+    CalcRGForceKernel(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 RGForce this kernel will be used for
+     */
+    virtual void initialize(const System& system, const RGForce& 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;
+};
+
 /**
  * This kernel is invoked by VerletIntegrator to take one time step.
  */
diff --git a/openmmapi/include/OpenMM.h b/openmmapi/include/OpenMM.h
index df064cbc28..3004cc0c30 100644
--- a/openmmapi/include/OpenMM.h
+++ b/openmmapi/include/OpenMM.h
@@ -70,6 +70,7 @@
 #include "openmm/PeriodicTorsionForce.h"
 #include "openmm/QTBIntegrator.h"
 #include "openmm/RBTorsionForce.h"
+#include "openmm/RGForce.h"
 #include "openmm/RMSDForce.h"
 #include "openmm/State.h"
 #include "openmm/System.h"
diff --git a/openmmapi/include/openmm/RGForce.h b/openmmapi/include/openmm/RGForce.h
new file mode 100644
index 0000000000..3c3e65ccb0
--- /dev/null
+++ b/openmmapi/include/openmm/RGForce.h
@@ -0,0 +1,89 @@
+#ifndef OPENMM_RGFORCE_H_
+#define OPENMM_RGFORCE_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: 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 "Force.h"
+#include "Vec3.h"
+#include <vector>
+#include "internal/windowsExport.h"
+
+namespace OpenMM {
+
+/**
+ * This is a force whose energy equals the radius of gyration (Rg) of a set of
+ * particles.  It is intended for use with CustomCVForce.  You will not normally
+ * want a force that exactly equals the radius of gyration, but there are many
+ * situations where it is useful to have a restraining or biasing force that
+ * depends on Rg in some way.
+ */
+
+class OPENMM_EXPORT RGForce : public Force {
+public:
+    /**
+     * Create an RGForce.
+     *
+     * @param particles    the indices of the particles to use when computing
+     *                     Rg.  If this is empty (the default), all particles in
+     *                     the system will be used.
+     */
+    explicit RGForce(const std::vector<int>& particles=std::vector<int>());
+    /**
+     * Get the indices of the particles to use when computing Rg.  If this
+     * is empty, all particles in the system will be used.
+     */
+    const std::vector<int>& getParticles() const {
+        return particles;
+    }
+    /**
+     * Set the indices of the particles to use when computing Rg.  If this
+     * is empty, all particles in the system will be used.
+     */
+    void setParticles(const std::vector<int>& particles);
+    /**
+     * Returns whether or not this force makes use of periodic boundary
+     * conditions.
+     *
+     * @returns true if force uses PBC and false otherwise
+     */
+    bool usesPeriodicBoundaryConditions() const {
+        return false;
+    }
+protected:
+    ForceImpl* createImpl() const;
+private:
+    std::vector<int> particles;
+};
+
+} // namespace OpenMM
+
+#endif /*OPENMM_RGFORCE_H_*/
diff --git a/openmmapi/include/openmm/internal/RGForceImpl.h b/openmmapi/include/openmm/internal/RGForceImpl.h
new file mode 100644
index 0000000000..7febebc1e3
--- /dev/null
+++ b/openmmapi/include/openmm/internal/RGForceImpl.h
@@ -0,0 +1,71 @@
+#ifndef OPENMM_RGFORCEIMPL_H_
+#define OPENMM_RGFORCEIMPL_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: 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/RGForce.h"
+#include "openmm/Kernel.h"
+#include <utility>
+#include <map>
+#include <string>
+
+namespace OpenMM {
+
+/**
+ * This is the internal implementation of RGForce.
+ */
+
+class RGForceImpl : public ForceImpl {
+public:
+    RGForceImpl(const RGForce& owner);
+    ~RGForceImpl();
+    void initialize(ContextImpl& context);
+    const RGForce& getOwner() const {
+        return owner;
+    }
+    void updateContextState(ContextImpl& context, bool& forcesInvalid) {
+        // This force field doesn't update the state directly.
+    }
+    double calcForcesAndEnergy(ContextImpl& context, bool includeForces, bool includeEnergy, int groups);
+    std::map<std::string, double> getDefaultParameters() {
+        return std::map<std::string, double>(); // This force doesn't define any parameters.
+    }
+    std::vector<std::string> getKernelNames();
+private:
+    const RGForce& owner;
+    Kernel kernel;
+};
+
+} // namespace OpenMM
+
+#endif /*OPENMM_RGFORCEIMPL_H_*/
diff --git a/openmmapi/src/RGForce.cpp b/openmmapi/src/RGForce.cpp
new file mode 100644
index 0000000000..ce30d02c17
--- /dev/null
+++ b/openmmapi/src/RGForce.cpp
@@ -0,0 +1,47 @@
+/* -------------------------------------------------------------------------- *
+ *                                   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: 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/RGForce.h"
+#include "openmm/internal/RGForceImpl.h"
+
+using namespace OpenMM;
+using namespace std;
+
+RGForce::RGForce(const vector<int>& particles) : particles(particles) {
+}
+
+void RGForce::setParticles(const std::vector<int>& particles) {
+    this->particles = particles;
+}
+
+ForceImpl* RGForce::createImpl() const {
+    return new RGForceImpl(*this);
+}
diff --git a/openmmapi/src/RGForceImpl.cpp b/openmmapi/src/RGForceImpl.cpp
new file mode 100644
index 0000000000..df50f77cf9
--- /dev/null
+++ b/openmmapi/src/RGForceImpl.cpp
@@ -0,0 +1,84 @@
+/* -------------------------------------------------------------------------- *
+ *                                   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: 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/OpenMMException.h"
+#include "openmm/internal/ContextImpl.h"
+#include "openmm/internal/RGForceImpl.h"
+#include "openmm/kernels.h"
+#include <set>
+#include <sstream>
+
+using namespace OpenMM;
+using namespace std;
+
+RGForceImpl::RGForceImpl(const RGForce& owner) : owner(owner) {
+    forceGroup = owner.getForceGroup();
+}
+
+RGForceImpl::~RGForceImpl() {
+}
+
+void RGForceImpl::initialize(ContextImpl& context) {
+    kernel = context.getPlatform().createKernel(CalcRGForceKernel::Name(), context);
+
+    // Check for errors in the specification of particles.
+    const System& system = context.getSystem();
+    int numParticles = system.getNumParticles();
+    set<int> particles;
+    for (int i : owner.getParticles()) {
+        if (i < 0 || i >= numParticles) {
+            stringstream msg;
+            msg << "RGForce: Illegal particle index for Rg: ";
+            msg << i;
+            throw OpenMMException(msg.str());
+        }
+        if (particles.find(i) != particles.end()) {
+            stringstream msg;
+            msg << "RGForce: Duplicated particle index for Rg: ";
+            msg << i;
+            throw OpenMMException(msg.str());
+        }
+        particles.insert(i);
+    }
+    kernel.getAs<CalcRGForceKernel>().initialize(context.getSystem(), owner);
+}
+
+double RGForceImpl::calcForcesAndEnergy(ContextImpl& context, bool includeForces, bool includeEnergy, int groups) {
+    if ((groups&(1<<forceGroup)) != 0)
+        return kernel.getAs<CalcRGForceKernel>().execute(context, includeForces, includeEnergy);
+    return 0.0;
+}
+
+vector<string> RGForceImpl::getKernelNames() {
+    vector<string> names;
+    names.push_back(CalcRGForceKernel::Name());
+    return names;
+}
diff --git a/platforms/common/include/openmm/common/CommonKernels.h b/platforms/common/include/openmm/common/CommonKernels.h
index ffda15d675..be7bf01c3c 100644
--- a/platforms/common/include/openmm/common/CommonKernels.h
+++ b/platforms/common/include/openmm/common/CommonKernels.h
@@ -1158,6 +1158,37 @@ class CommonCalcRMSDForceKernel : public CalcRMSDForceKernel {
     ComputeKernel kernel1, kernel2;
 };
 
+/**
+ * This kernel is invoked by RGForce to calculate the forces acting on the system and the energy of the system.
+ */
+class CommonCalcRGForceKernel : public CalcRGForceKernel {
+public:
+    CommonCalcRGForceKernel(std::string name, const Platform& platform, ComputeContext& cc) : CalcRGForceKernel(name, platform), cc(cc) {
+    }
+    /**
+     * Initialize the kernel.
+     *
+     * @param system     the System this kernel will be applied to
+     * @param force      the RGForce this kernel will be used for
+     */
+    void initialize(const System& system, const RGForce& 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:
+    class ReorderListener;
+    ComputeContext& cc;
+    int blockSize;
+    ComputeArray particles, centerBuffer, rgBuffer;
+    ComputeKernel centerKernel, rgKernel, forceKernel;
+};
+
 /**
  * This kernel is invoked by AndersenThermostat at the start of each time step to adjust the particle velocities.
  */
diff --git a/platforms/common/src/CommonKernels.cpp b/platforms/common/src/CommonKernels.cpp
index 6526df339f..b159bd880a 100644
--- a/platforms/common/src/CommonKernels.cpp
+++ b/platforms/common/src/CommonKernels.cpp
@@ -3592,6 +3592,90 @@ void CommonCalcRMSDForceKernel::copyParametersToContext(ContextImpl& context, co
     cc.invalidateMolecules(info);
 }
 
+class CommonCalcRGForceKernel::ReorderListener : public ComputeContext::ReorderListener {
+public:
+    ReorderListener(ComputeContext& cc, const vector<int>& particleIndices, ArrayInterface& particles) : cc(cc),
+            particleIndices(particleIndices), particles(particles) {
+    }
+    void execute() {
+        vector<int> particleVec(particles.getSize());
+        const vector<int>& order = cc.getAtomIndex();
+        vector<int> invOrder(cc.getPaddedNumAtoms());
+        for (int i = 0; i < order.size(); i++)
+            invOrder[order[i]] = i;
+        for (int i = 0; i < particleIndices.size(); i++)
+            particleVec[i] = invOrder[particleIndices[i]];
+        particles.upload(particleVec);
+    }
+private:
+    ComputeContext& cc;
+    const vector<int>& particleIndices;
+    ArrayInterface& particles;
+};
+
+void CommonCalcRGForceKernel::initialize(const System& system, const RGForce& force) {
+    // Create data structures.
+
+    ContextSelector selector(cc);
+    bool useDouble = cc.getUseDoublePrecision();
+    int elementSize = (useDouble ? sizeof(double) : sizeof(float));
+    int numParticles = force.getParticles().size();
+    if (numParticles == 0)
+        numParticles = system.getNumParticles();
+    particles.initialize<int>(cc, numParticles, "particles");
+    centerBuffer.initialize(cc, 3*(cc.getNumThreadBlocks()+1), elementSize, "centerBuffer");
+    rgBuffer.initialize(cc, cc.getNumThreadBlocks(), elementSize, "rgBuffer");
+
+    // Create the kernels.
+
+    blockSize = min(256, cc.getMaxThreadBlockSize());
+    map<string, string> defines;
+    defines["THREAD_BLOCK_SIZE"] = cc.intToString(blockSize);
+    defines["PADDED_NUM_ATOMS"] = cc.intToString(cc.getPaddedNumAtoms());
+    ComputeProgram program = cc.compileProgram(CommonKernelSources::rg, defines);
+    centerKernel = program->createKernel("computeCenterPosition");
+    rgKernel = program->createKernel("computeRg");
+    forceKernel = program->createKernel("computeForces");
+    centerKernel->addArg(numParticles);
+    centerKernel->addArg(cc.getPosq());
+    centerKernel->addArg(particles);
+    centerKernel->addArg(centerBuffer);
+    rgKernel->addArg(numParticles);
+    rgKernel->addArg(cc.getPosq());
+    rgKernel->addArg(particles);
+    rgKernel->addArg(centerBuffer);
+    rgKernel->addArg(rgBuffer);
+    forceKernel->addArg(numParticles);
+    forceKernel->addArg(cc.getPosq());
+    forceKernel->addArg(particles);
+    forceKernel->addArg(centerBuffer);
+    forceKernel->addArg(rgBuffer);
+    forceKernel->addArg(cc.getLongForceBuffer());
+    forceKernel->addArg(cc.getEnergyBuffer());
+
+    // Create the listener for updating the list of particles.
+
+    if (force.getParticles().size() == 0) {
+        vector<int> particleVec(numParticles);
+        for (int i = 0; i < numParticles; i++)
+            particleVec[i] = i;
+        particles.upload(particleVec);
+    }
+    else {
+        ReorderListener* listener = new ReorderListener(cc, force.getParticles(), particles);
+        cc.addReorderListener(listener);
+        listener->execute();
+    }
+}
+
+double CommonCalcRGForceKernel::execute(ContextImpl& context, bool includeForces, bool includeEnergy) {
+    ContextSelector selector(cc);
+    centerKernel->execute(particles.getSize(), blockSize);
+    rgKernel->execute(particles.getSize(), blockSize);
+    forceKernel->execute(particles.getSize(), blockSize);
+    return 0.0;
+}
+
 void CommonApplyAndersenThermostatKernel::initialize(const System& system, const AndersenThermostat& thermostat) {
     ContextSelector selector(cc);
     randomSeed = thermostat.getRandomNumberSeed();
diff --git a/platforms/common/src/kernels/rg.cc b/platforms/common/src/kernels/rg.cc
new file mode 100644
index 0000000000..f9ffdcca55
--- /dev/null
+++ b/platforms/common/src/kernels/rg.cc
@@ -0,0 +1,107 @@
+/**
+ * Sum a value over all threads.
+ */
+DEVICE real reduceValue(real value, LOCAL_ARG volatile real* temp) {
+    const int thread = LOCAL_ID;
+    SYNC_THREADS;
+    temp[thread] = value;
+    SYNC_THREADS;
+    for (int step = 1; step < 32; step *= 2) {
+        if (thread+step < LOCAL_SIZE && thread%(2*step) == 0)
+            temp[thread] = temp[thread] + temp[thread+step];
+        SYNC_WARPS;
+    }
+    for (int step = 32; step < LOCAL_SIZE; step *= 2) {
+        if (thread+step < LOCAL_SIZE && thread%(2*step) == 0)
+            temp[thread] = temp[thread] + temp[thread+step];
+        SYNC_THREADS;
+    }
+    return temp[0];
+}
+
+/**
+ * First step of computing the radius of gyration: each thread block computes a contribution
+ * to the center position.
+ */
+KERNEL void computeCenterPosition(int numParticles, GLOBAL const real4* RESTRICT posq, GLOBAL const int* RESTRICT particles,
+        GLOBAL real* centerBuffer) {
+    LOCAL volatile real temp[THREAD_BLOCK_SIZE];
+    real3 center = make_real3(0);
+    for (int i = GLOBAL_ID; i < numParticles; i += GLOBAL_SIZE)
+        center += trimTo3(posq[particles[i]]);
+    center.x = reduceValue(center.x, temp);
+    center.y = reduceValue(center.y, temp);
+    center.z = reduceValue(center.z, temp);
+    if (LOCAL_ID == 0) {
+        centerBuffer[3*GROUP_ID] = center.x;
+        centerBuffer[3*GROUP_ID+1] = center.y;
+        centerBuffer[3*GROUP_ID+2] = center.z;
+    }
+}
+
+/**
+ * Second step of computing the radius of gyration: each thread block computes a contribution to Rg^2.
+ */
+KERNEL void computeRg(int numParticles, GLOBAL const real4* RESTRICT posq, GLOBAL const int* RESTRICT particles,
+        GLOBAL real* centerBuffer, GLOBAL real* rgBuffer) {
+    LOCAL volatile real temp[THREAD_BLOCK_SIZE];
+
+    // Sum the contributions computed in the previous kernel to find the center position.
+
+    real3 center = make_real3(0);
+    for (int i = LOCAL_ID; i < NUM_GROUPS; i += LOCAL_SIZE) {
+        center.x += centerBuffer[3*i];
+        center.y += centerBuffer[3*i+1];
+        center.z += centerBuffer[3*i+2];
+    }
+    center.x = reduceValue(center.x, temp)/numParticles;
+    center.y = reduceValue(center.y, temp)/numParticles;
+    center.z = reduceValue(center.z, temp)/numParticles;
+    if (GLOBAL_ID == 0) {
+        // Save the result so we don't need to compute it again in the next kernel.
+
+        centerBuffer[3*NUM_GROUPS] = center.x;
+        centerBuffer[3*NUM_GROUPS+1] = center.y;
+        centerBuffer[3*NUM_GROUPS+2] = center.z;
+    }
+
+    // Compute the contributions to Rg^2.
+
+    real rg2 = 0;
+    for (int i = GLOBAL_ID; i < numParticles; i += GLOBAL_SIZE) {
+        real3 delta = trimTo3(posq[particles[i]]) - center;
+        rg2 += dot(delta, delta);
+    }
+    rg2 = reduceValue(rg2, temp);
+    if (LOCAL_ID == 0)
+        rgBuffer[GROUP_ID] = rg2;
+}
+
+/**
+ * Third step of computing the radius of gyration: compute the forces.
+ */
+KERNEL void computeForces(int numParticles, GLOBAL const real4* RESTRICT posq, GLOBAL const int* RESTRICT particles,
+        GLOBAL const real* centerBuffer, GLOBAL real* rgBuffer, GLOBAL mm_ulong* RESTRICT forceBuffers, GLOBAL mixed* RESTRICT energyBuffer) {
+    LOCAL volatile real temp[THREAD_BLOCK_SIZE];
+    real3 center = make_real3(centerBuffer[3*NUM_GROUPS], centerBuffer[3*NUM_GROUPS+1], centerBuffer[3*NUM_GROUPS+2]);
+
+    // Sum the contributions computed in the previous kernel to find Rg.
+
+    real rg2 = 0;
+    for (int i = LOCAL_ID; i < NUM_GROUPS; i += LOCAL_SIZE)
+        rg2 += rgBuffer[i];
+    real rg = SQRT(reduceValue(rg2, temp)/numParticles);
+    if (GLOBAL_ID == 0)
+        energyBuffer[0] += rg;
+
+    // Compute the forces.
+
+    real scale = 1/(rg*numParticles);
+    for (int i = GLOBAL_ID; i < numParticles; i += GLOBAL_SIZE) {
+        int particle = particles[i];
+        real3 delta = trimTo3(posq[particle]) - center;
+        ATOMIC_ADD(&forceBuffers[particle], (mm_ulong) realToFixedPoint(-scale*delta.x));
+        ATOMIC_ADD(&forceBuffers[particle+PADDED_NUM_ATOMS], (mm_ulong) realToFixedPoint(-scale*delta.y));
+        ATOMIC_ADD(&forceBuffers[particle+2*PADDED_NUM_ATOMS], (mm_ulong) realToFixedPoint(-scale*delta.z));
+    }
+}
diff --git a/platforms/cuda/src/CudaKernelFactory.cpp b/platforms/cuda/src/CudaKernelFactory.cpp
index 01f493fcc9..989584703f 100644
--- a/platforms/cuda/src/CudaKernelFactory.cpp
+++ b/platforms/cuda/src/CudaKernelFactory.cpp
@@ -123,6 +123,8 @@ KernelImpl* CudaKernelFactory::createKernelImpl(std::string name, const Platform
         return new CudaCalcATMForceKernel(name, platform, cu);
     if (name == CalcCustomCPPForceKernel::Name())
         return new CommonCalcCustomCPPForceKernel(name, platform, context, cu);
+    if (name == CalcRGForceKernel::Name())
+        return new CommonCalcRGForceKernel(name, platform, 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 6adc21fb23..4625eb2cd2 100644
--- a/platforms/cuda/src/CudaPlatform.cpp
+++ b/platforms/cuda/src/CudaPlatform.cpp
@@ -94,6 +94,7 @@ CudaPlatform::CudaPlatform() {
     registerKernelFactory(CalcCustomCPPForceKernel::Name(), factory);
     registerKernelFactory(CalcCustomCVForceKernel::Name(), factory);
     registerKernelFactory(CalcATMForceKernel::Name(), factory);
+    registerKernelFactory(CalcRGForceKernel::Name(), factory);
     registerKernelFactory(CalcRMSDForceKernel::Name(), factory);
     registerKernelFactory(CalcCustomManyParticleForceKernel::Name(), factory);
     registerKernelFactory(CalcGayBerneForceKernel::Name(), factory);
diff --git a/platforms/hip/src/HipKernelFactory.cpp b/platforms/hip/src/HipKernelFactory.cpp
index 16d2f7b782..b3eade589c 100644
--- a/platforms/hip/src/HipKernelFactory.cpp
+++ b/platforms/hip/src/HipKernelFactory.cpp
@@ -122,6 +122,8 @@ KernelImpl* HipKernelFactory::createKernelImpl(std::string name, const Platform&
         return new HipCalcCustomCVForceKernel(name, platform, cu);
     if (name == CalcCustomCPPForceKernel::Name())
         return new CommonCalcCustomCPPForceKernel(name, platform, context, cu);
+    if (name == CalcRGForceKernel::Name())
+        return new CommonCalcRGForceKernel(name, platform, cu);
     if (name == CalcRMSDForceKernel::Name())
         return new CommonCalcRMSDForceKernel(name, platform, cu);
     if (name == CalcCustomManyParticleForceKernel::Name())
diff --git a/platforms/hip/src/HipPlatform.cpp b/platforms/hip/src/HipPlatform.cpp
index dc0f9c2d52..69817904f6 100644
--- a/platforms/hip/src/HipPlatform.cpp
+++ b/platforms/hip/src/HipPlatform.cpp
@@ -94,6 +94,7 @@ HipPlatform::HipPlatform() {
     registerKernelFactory(CalcCustomCompoundBondForceKernel::Name(), factory);
     registerKernelFactory(CalcCustomCPPForceKernel::Name(), factory);
     registerKernelFactory(CalcCustomCVForceKernel::Name(), factory);
+    registerKernelFactory(CalcRGForceKernel::Name(), factory);
     registerKernelFactory(CalcRMSDForceKernel::Name(), factory);
     registerKernelFactory(CalcCustomManyParticleForceKernel::Name(), factory);
     registerKernelFactory(CalcGayBerneForceKernel::Name(), factory);
diff --git a/platforms/opencl/src/OpenCLKernelFactory.cpp b/platforms/opencl/src/OpenCLKernelFactory.cpp
index c758df70c7..2909350086 100644
--- a/platforms/opencl/src/OpenCLKernelFactory.cpp
+++ b/platforms/opencl/src/OpenCLKernelFactory.cpp
@@ -121,6 +121,8 @@ KernelImpl* OpenCLKernelFactory::createKernelImpl(std::string name, const Platfo
         return new OpenCLCalcATMForceKernel(name, platform, cl);
     if (name == CalcCustomCPPForceKernel::Name())
         return new CommonCalcCustomCPPForceKernel(name, platform, context, cl);
+    if (name == CalcRGForceKernel::Name())
+        return new CommonCalcRGForceKernel(name, platform, 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 fe81690b06..30388cb936 100644
--- a/platforms/opencl/src/OpenCLPlatform.cpp
+++ b/platforms/opencl/src/OpenCLPlatform.cpp
@@ -85,6 +85,7 @@ OpenCLPlatform::OpenCLPlatform() {
     registerKernelFactory(CalcCustomCPPForceKernel::Name(), factory);
     registerKernelFactory(CalcCustomCVForceKernel::Name(), factory);
     registerKernelFactory(CalcATMForceKernel::Name(), factory);
+    registerKernelFactory(CalcRGForceKernel::Name(), factory);
     registerKernelFactory(CalcRMSDForceKernel::Name(), factory);
     registerKernelFactory(CalcCustomManyParticleForceKernel::Name(), factory);
     registerKernelFactory(CalcGayBerneForceKernel::Name(), factory);
diff --git a/platforms/reference/include/ReferenceKernels.h b/platforms/reference/include/ReferenceKernels.h
index 6c614ff47e..9dd2b2edc0 100644
--- a/platforms/reference/include/ReferenceKernels.h
+++ b/platforms/reference/include/ReferenceKernels.h
@@ -1148,6 +1148,33 @@ class ReferenceCalcRMSDForceKernel : public CalcRMSDForceKernel {
     std::vector<int> particles;
 };
 
+/**
+ * This kernel is invoked by RGForce to calculate the forces acting on the system and the energy of the system.
+ */
+class ReferenceCalcRGForceKernel : public CalcRGForceKernel {
+public:
+    ReferenceCalcRGForceKernel(std::string name, const Platform& platform) : CalcRGForceKernel(name, platform) {
+    }
+    /**
+     * Initialize the kernel.
+     *
+     * @param system     the System this kernel will be applied to
+     * @param force      the RGForce this kernel will be used for
+     */
+    void initialize(const System& system, const RGForce& 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:
+    std::vector<int> particles;
+};
+
 /**
  * This kernel is invoked by VerletIntegrator to take one time step.
  */
diff --git a/platforms/reference/include/ReferenceRGForce.h b/platforms/reference/include/ReferenceRGForce.h
new file mode 100644
index 0000000000..a4948e0d71
--- /dev/null
+++ b/platforms/reference/include/ReferenceRGForce.h
@@ -0,0 +1,54 @@
+/* Portions copyright (c) 2025 Stanford University and Simbios.
+ * Contributors: Peter Eastman
+ *
+ * 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.
+ */
+
+#ifndef __ReferenceRGForce_H__
+#define __ReferenceRGForce_H__
+
+#include "openmm/RGForce.h"
+#include <vector>
+
+namespace OpenMM {
+
+class ReferenceRGForce {
+private:
+    std::vector<int> particles;
+
+public:
+    /**
+     * Constructor
+     */
+    ReferenceRGForce(std::vector<int>& particles);
+
+    /**
+     * Calculate the interaction.
+     * 
+     * @param atomCoordinates    atom coordinates
+     * @param forces             the forces are added to this
+     * @return the energy of the interaction
+     */
+   double calculateIxn(std::vector<OpenMM::Vec3>& atomCoordinates, std::vector<OpenMM::Vec3>& forces) const;
+};
+
+} // namespace OpenMM
+
+#endif // __ReferenceRGForce_H__
diff --git a/platforms/reference/src/ReferenceKernelFactory.cpp b/platforms/reference/src/ReferenceKernelFactory.cpp
index dbe1421596..54951966ff 100644
--- a/platforms/reference/src/ReferenceKernelFactory.cpp
+++ b/platforms/reference/src/ReferenceKernelFactory.cpp
@@ -84,6 +84,8 @@ KernelImpl* ReferenceKernelFactory::createKernelImpl(std::string name, const Pla
         return new ReferenceCalcATMForceKernel(name, platform);
     if (name == CalcCustomCPPForceKernel::Name())
         return new ReferenceCalcCustomCPPForceKernel(name, platform);
+    if (name == CalcRGForceKernel::Name())
+        return new ReferenceCalcRGForceKernel(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 4c8e0dea12..aaae045710 100644
--- a/platforms/reference/src/ReferenceKernels.cpp
+++ b/platforms/reference/src/ReferenceKernels.cpp
@@ -62,6 +62,7 @@
 #include "ReferenceProperDihedralBond.h"
 #include "ReferenceQTBDynamics.h"
 #include "ReferenceRbDihedralBond.h"
+#include "ReferenceRGForce.h"
 #include "ReferenceRMSDForce.h"
 #include "ReferenceTabulatedFunction.h"
 #include "ReferenceVariableStochasticDynamics.h"
@@ -2308,6 +2309,20 @@ void ReferenceCalcRMSDForceKernel::copyParametersToContext(ContextImpl& context,
         p -= center;
 }
 
+void ReferenceCalcRGForceKernel::initialize(const System& system, const RGForce& force) {
+    particles = force.getParticles();
+    if (particles.size() == 0)
+        for (int i = 0; i < system.getNumParticles(); i++)
+            particles.push_back(i);
+}
+
+double ReferenceCalcRGForceKernel::execute(ContextImpl& context, bool includeForces, bool includeEnergy) {
+    vector<Vec3>& posData = extractPositions(context);
+    vector<Vec3>& forceData = extractForces(context);
+    ReferenceRGForce rg(particles);
+    return rg.calculateIxn(posData, forceData);
+}
+
 ReferenceIntegrateVerletStepKernel::~ReferenceIntegrateVerletStepKernel() {
     if (dynamics)
         delete dynamics;
diff --git a/platforms/reference/src/ReferencePlatform.cpp b/platforms/reference/src/ReferencePlatform.cpp
index 70fc071ec4..17bd8b31c6 100644
--- a/platforms/reference/src/ReferencePlatform.cpp
+++ b/platforms/reference/src/ReferencePlatform.cpp
@@ -65,6 +65,7 @@ ReferencePlatform::ReferencePlatform() {
     registerKernelFactory(CalcCustomCPPForceKernel::Name(), factory);
     registerKernelFactory(CalcCustomCVForceKernel::Name(), factory);
     registerKernelFactory(CalcATMForceKernel::Name(), factory);
+    registerKernelFactory(CalcRGForceKernel::Name(), factory);
     registerKernelFactory(CalcRMSDForceKernel::Name(), factory);
     registerKernelFactory(CalcCustomManyParticleForceKernel::Name(), factory);
     registerKernelFactory(CalcGayBerneForceKernel::Name(), factory);
diff --git a/platforms/reference/src/SimTKReference/ReferenceRGForce.cpp b/platforms/reference/src/SimTKReference/ReferenceRGForce.cpp
new file mode 100644
index 0000000000..481814bebf
--- /dev/null
+++ b/platforms/reference/src/SimTKReference/ReferenceRGForce.cpp
@@ -0,0 +1,59 @@
+/* Portions copyright (c) 2025 Stanford University and Simbios.
+ * Contributors: Peter Eastman
+ *
+ * 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 "ReferenceRGForce.h"
+#include <cmath>
+
+using namespace OpenMM;
+using namespace std;
+
+ReferenceRGForce::ReferenceRGForce(vector<int>& particles) : particles(particles) {
+}
+
+double ReferenceRGForce::calculateIxn(vector<Vec3>& atomCoordinates, vector<Vec3>& forces) const {
+    // Compute the center position.
+
+    int numParticles = particles.size();
+    Vec3 center;
+    for (int i : particles)
+        center += atomCoordinates[i];
+    center /= numParticles;
+
+    // Compute the radius of gyration.
+
+    double sum = 0.0;
+    for (int i = 0; i < numParticles; i++) {
+        Vec3 delta = atomCoordinates[particles[i]]-center;
+        sum += delta.dot(delta);
+    }
+    double rg = sqrt(sum/numParticles);
+
+    // Compute the forces.
+
+    double scale = 1.0/(rg*numParticles);
+    for (int i = 0; i < numParticles; i++) {
+        Vec3 delta = atomCoordinates[particles[i]]-center;
+        forces[particles[i]] -= scale*delta;
+    }
+    return rg;
+}
diff --git a/serialization/include/openmm/serialization/RGForceProxy.h b/serialization/include/openmm/serialization/RGForceProxy.h
new file mode 100644
index 0000000000..3b218695b1
--- /dev/null
+++ b/serialization/include/openmm/serialization/RGForceProxy.h
@@ -0,0 +1,53 @@
+#ifndef OPENMM_RGFORCE_PROXY_H_
+#define OPENMM_RGFORCE_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) 2025 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/windowsExport.h"
+#include "openmm/serialization/SerializationProxy.h"
+
+namespace OpenMM {
+
+/**
+ * This is a proxy for serializing RGForce objects.
+ */
+
+class OPENMM_EXPORT RGForceProxy : public SerializationProxy {
+public:
+    RGForceProxy();
+    void serialize(const void* object, SerializationNode& node) const;
+    void* deserialize(const SerializationNode& node) const;
+};
+
+} // namespace OpenMM
+
+#endif /*OPENMM_RGFORCE_PROXY_H_*/
diff --git a/serialization/src/RGForceProxy.cpp b/serialization/src/RGForceProxy.cpp
new file mode 100644
index 0000000000..32f5af722b
--- /dev/null
+++ b/serialization/src/RGForceProxy.cpp
@@ -0,0 +1,73 @@
+/* -------------------------------------------------------------------------- *
+ *                                   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: 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/serialization/RGForceProxy.h"
+#include "openmm/serialization/SerializationNode.h"
+#include "openmm/Force.h"
+#include "openmm/RGForce.h"
+#include <sstream>
+
+using namespace OpenMM;
+using namespace std;
+
+RGForceProxy::RGForceProxy() : SerializationProxy("RGForce") {
+}
+
+void RGForceProxy::serialize(const void* object, SerializationNode& node) const {
+    node.setIntProperty("version", 0);
+    const RGForce& force = *reinterpret_cast<const RGForce*>(object);
+    node.setIntProperty("forceGroup", force.getForceGroup());
+    node.setStringProperty("name", force.getName());
+    SerializationNode& particlesNode = node.createChildNode("Particles");
+    for (int i : force.getParticles())
+       particlesNode.createChildNode("Particle").setIntProperty("index", i);
+}
+
+void* RGForceProxy::deserialize(const SerializationNode& node) const {
+    int version = node.getIntProperty("version");
+    if (version != 0)
+        throw OpenMMException("Unsupported version number");
+    RGForce* force = NULL;
+    try {
+        vector<int> particles;
+        for (auto& particle : node.getChildNode("Particles").getChildren())
+            particles.push_back(particle.getIntProperty("index"));
+        force = new RGForce(particles);
+        force->setForceGroup(node.getIntProperty("forceGroup", 0));
+        force->setName(node.getStringProperty("name", force->getName()));
+        return force;
+    }
+    catch (...) {
+        if (force != NULL)
+            delete force;
+        throw;
+    }
+}
diff --git a/serialization/src/SerializationProxyRegistration.cpp b/serialization/src/SerializationProxyRegistration.cpp
index cfeb746aca..eabafa0841 100644
--- a/serialization/src/SerializationProxyRegistration.cpp
+++ b/serialization/src/SerializationProxyRegistration.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) 2010-2021 Stanford University and the Authors.      *
+ * Portions copyright (c) 2010-2025 Stanford University and the Authors.      *
  * Authors: Peter Eastman                                                     *
  * Contributors:                                                              *
  *                                                                            *
@@ -64,6 +64,7 @@
 #include "openmm/PeriodicTorsionForce.h"
 #include "openmm/QTBIntegrator.h"
 #include "openmm/RBTorsionForce.h"
+#include "openmm/RGForce.h"
 #include "openmm/RMSDForce.h"
 #include "openmm/System.h"
 #include "openmm/TabulatedFunction.h"
@@ -107,6 +108,7 @@
 #include "openmm/serialization/PeriodicTorsionForceProxy.h"
 #include "openmm/serialization/QTBIntegratorProxy.h"
 #include "openmm/serialization/RBTorsionForceProxy.h"
+#include "openmm/serialization/RGForceProxy.h"
 #include "openmm/serialization/RMSDForceProxy.h"
 #include "openmm/serialization/StateProxy.h"
 #include "openmm/serialization/SystemProxy.h"
@@ -169,8 +171,9 @@ extern "C" void registerSerializationProxies() {
     SerializationProxy::registerProxy(typeid(NonbondedForce), new NonbondedForceProxy());
     SerializationProxy::registerProxy(typeid(NoseHooverIntegrator), new NoseHooverIntegratorProxy());
     SerializationProxy::registerProxy(typeid(PeriodicTorsionForce), new PeriodicTorsionForceProxy());
-    SerializationProxy::registerProxy(typeid(RBTorsionForce), new RBTorsionForceProxy());
     SerializationProxy::registerProxy(typeid(QTBIntegrator), new QTBIntegratorProxy());
+    SerializationProxy::registerProxy(typeid(RBTorsionForce), new RBTorsionForceProxy());
+    SerializationProxy::registerProxy(typeid(RGForce), new RGForceProxy());
     SerializationProxy::registerProxy(typeid(RMSDForce), new RMSDForceProxy());
     SerializationProxy::registerProxy(typeid(System), new SystemProxy());
     SerializationProxy::registerProxy(typeid(State), new StateProxy());
diff --git a/wrappers/python/src/swig_doxygen/swigInputConfig.py b/wrappers/python/src/swig_doxygen/swigInputConfig.py
index ebc155eac5..410d1a6039 100755
--- a/wrappers/python/src/swig_doxygen/swigInputConfig.py
+++ b/wrappers/python/src/swig_doxygen/swigInputConfig.py
@@ -491,6 +491,7 @@
 ("RMSDForce", "getReferencePositions") : ("unit.nanometer", ()),
 ("RMSDForce", "setReferencePositions") : (None, ("unit.nanometer",)),
 ("RMSDForce", "getParticles") : (None, ()),
+("RGForce", "getParticles") : (None, ()),
 ("NoseHooverChain", "NoseHooverChain") : (None, ("unit.kelvin", "unit.kelvin", "unit.picosecond**-1", "unit.picosecond**-1", None, None, None, None, None, None, None)),
 ("NoseHooverChain", "getThermostatedPairs") : (None, ()),
 ("NoseHooverChain", "getThermostatedAtoms") : (None, ()),