Files
openmm/wrappers/python/tests/TestPythonForce.py
Peter Eastman 9aae4bb35c PythonForce can be restricted to a subset of particles (#5246)
* PythonForce can be restricted to a subset of particles

* Fix exception with CUDA
2026-04-06 14:11:45 -07:00

198 lines
7.9 KiB
Python

import unittest
from openmm import *
from openmm.unit import *
import numpy as np
import copy
def compute(state):
"""This is a computation function used by the test cases."""
pos = state.getPositions(asNumpy=True).value_in_unit(nanometer)
k = state.getParameters()['k']
energy = k*np.sum(pos*pos)
force = -0.5*k*pos
return energy*kilojoules_per_mole, force*kilojoules_per_mole/nanometer
class TestPythonForce(unittest.TestCase):
"""Test the PythonForce class"""
def testComputeForce(self):
"""Test using PythonForce to compute forces."""
system = System()
for i in range(5):
system.addParticle(1.0)
force = PythonForce(compute, {'k':2.5})
system.addForce(force)
positions = np.random.rand(5, 3)
for i in range(Platform.getNumPlatforms()):
integrator = VerletIntegrator(0.001)
try:
context = Context(system, integrator, Platform.getPlatform(i))
except OpenMMException:
if i == 0:
raise
else:
# This happens on CI when no GPU is available.
continue
context.setPositions(positions)
state = context.getState(energy=True, forces=True)
self.assertAlmostEqual(2.5*np.sum(positions*positions), state.getPotentialEnergy().value_in_unit(kilojoules_per_mole), places=5)
self.assertTrue(np.allclose(-1.25*positions, state.getForces(asNumpy=True).value_in_unit(kilojoules_per_mole/nanometer)))
def testParticleSubset(self):
"""Test a PythonForce appled to a subset of particles."""
system = System()
for i in range(10):
system.addParticle(1.0)
force = PythonForce(compute, {'k':2.5})
particles = [1,3,5,7,9]
force.setParticles(particles)
system.addForce(force)
positions = np.random.rand(10, 3)
for i in range(Platform.getNumPlatforms()):
integrator = VerletIntegrator(0.001)
try:
context = Context(system, integrator, Platform.getPlatform(i))
except OpenMMException:
if i == 0:
raise
else:
# This happens on CI when no GPU is available.
continue
context.setPositions(positions)
state = context.getState(energy=True, forces=True)
filtered = np.zeros(positions.shape)
filtered[particles] = positions[particles]
self.assertAlmostEqual(2.5*np.sum(filtered*filtered), state.getPotentialEnergy().value_in_unit(kilojoules_per_mole), places=5)
self.assertTrue(np.allclose(-1.25*filtered, state.getForces(asNumpy=True).value_in_unit(kilojoules_per_mole/nanometer)))
def testExceptions(self):
"""Test that PythonForce handles exceptions correctly."""
def compute2(state):
raise ValueError('This should fail')
system = System()
system.addParticle(1.0)
force = PythonForce(compute2)
system.addForce(force)
positions = np.random.rand(1, 3)
for i in range(Platform.getNumPlatforms()):
integrator = VerletIntegrator(0.001)
try:
context = Context(system, integrator, Platform.getPlatform(i))
except OpenMMException:
if i == 0:
raise
else:
# This happens on CI when no GPU is available.
continue
context.setPositions(positions)
with self.assertRaises(OpenMMException) as cm:
context.getState(energy=True)
self.assertEqual('This should fail', str(cm.exception))
def testSerialize(self):
"""Test that PythonForce can be serialized."""
force1 = PythonForce(compute, {'k':2.5})
force1.setUsesPeriodicBoundaryConditions(True)
force1.setParticles([1,3,5])
# Make a copy by serializing and the deserializing it.
force2 = copy.copy(force1)
# They should be identical.
self.assertEqual(XmlSerializer.serialize(force1), XmlSerializer.serialize(force2))
self.assertEqual(dict(force2.getGlobalParameters()), {'k':2.5})
self.assertEqual(force1.getParticles(), force2.getParticles())
self.assertTrue(force2.usesPeriodicBoundaryConditions())
# A locally defined function cannot be pickled. We should not be able to serialize a force
# that uses it.
def compute2(state):
return 1.0, np.zeros(len(state.getPositions()), 3)
force3 = PythonForce(compute2)
with self.assertRaises(OpenMMException):
XmlSerializer.serialize(force3)
def testMinimization(self):
"""Test that PythonForce works correctly with the minimizer."""
system = System()
for i in range(5):
system.addParticle(1.0)
force = PythonForce(compute, {'k':2.5})
system.addForce(force)
positions = np.random.rand(5, 3)
integrator = VerletIntegrator(0.001)
context = Context(system, integrator, Platform.getPlatform('Reference'))
context.setPositions(positions)
# The PythonForce and the MinimizationReporter both involve calling back into Python code,
# possibly from different threads. Make sure it doesn't cause any problems.
class Reporter(MinimizationReporter):
count = 0
def report(self, iteration, x, grad, args):
self.count += 1
return False
reporter = Reporter()
LocalEnergyMinimizer.minimize(context, tolerance=1e-3, reporter=reporter)
self.assertTrue(reporter.count > 0)
state = context.getState(energy=True, positions=True)
self.assertAlmostEqual(0.0, state.getPotentialEnergy().value_in_unit(kilojoules_per_mole))
def testMemory(self):
"""Test for memory leaks in the Python/C++ interface."""
try:
import resource
except:
# The resource module is not available on Windows.
return
system = System()
for i in range(1000):
system.addParticle(1.0)
force = PythonForce(compute, {'k':2.5})
system.addForce(force)
positions = np.random.rand(1000, 3)
integrator = VerletIntegrator(0.001)
context = Context(system, integrator, Platform.getPlatform('Reference'))
context.setPositions(positions)
integrator.step(5000)
memory1 = resource.getrusage(resource.RUSAGE_SELF).ru_maxrss
integrator.step(5000)
memory2 = resource.getrusage(resource.RUSAGE_SELF).ru_maxrss
self.assertTrue(memory2 < 1.05*memory1)
def testDtypes(self):
"""Test returning forces with different types."""
for dtype in [np.float32, np.float64, int]:
def compute2(state):
return 0, np.array([[1,2,3],[4,5,6]], dtype=dtype)
system = System()
system.addParticle(1.0)
system.addParticle(1.0)
force = PythonForce(compute2)
system.addForce(force)
positions = np.random.rand(2, 3)
for i in range(Platform.getNumPlatforms()):
integrator = VerletIntegrator(0.001)
try:
context = Context(system, integrator, Platform.getPlatform(i))
except OpenMMException:
if i == 0:
raise
else:
# This happens on CI when no GPU is available.
continue
context.setPositions(positions)
forces = context.getState(forces=True).getForces().value_in_unit(kilojoules_per_mole/nanometer)
self.assertEqual(Vec3(1,2,3), forces[0])
self.assertEqual(Vec3(4,5,6), forces[1])
if __name__ == '__main__':
unittest.main()