Skip to content

Commit

Permalink
tests: Improve code coverage
Browse files Browse the repository at this point in the history
Re-use existing utility functions in tests_common to reduce code
duplication. Replace unreachable exceptions by unittest assertions.
Fix tolerance of LBGPU test case up to single-precision accuracy.
  • Loading branch information
jngrad committed May 25, 2022
1 parent 0d1628c commit 6d91ec5
Show file tree
Hide file tree
Showing 15 changed files with 61 additions and 94 deletions.
5 changes: 3 additions & 2 deletions testsuite/python/cell_system.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,11 +46,12 @@ def test_cell_system(self):

@ut.skipIf(n_nodes == 1, "Skipping test: only runs for n_nodes >= 2")
def check_node_grid(self):
system = self.system
for i in range(3):
node_grid_ref = [1, 1, 1]
node_grid_ref[i] = self.n_nodes
self.system.cell_system.node_grid = node_grid_ref
node_grid = self.system.cell_system.get_state()['node_grid']
system.cell_system.node_grid = node_grid_ref
node_grid = system.cell_system.get_state()['node_grid']
np.testing.assert_array_equal(node_grid, node_grid_ref)

def test_exceptions(self):
Expand Down
14 changes: 8 additions & 6 deletions testsuite/python/collision_detection.py
Original file line number Diff line number Diff line change
Expand Up @@ -530,6 +530,12 @@ def test_glue_to_surface_random(self):
# Bond type and partner type
# part_type_after_glueing can have a bond to a vs or to a
# non_virtual particle
allowed_types = (self.part_type_after_glueing,
self.part_type_to_attach_vs_to)
self.assertIn(
p.type,
allowed_types,
msg=f"Particle {p.id} of type {p.type} should not have bonds, yet has {p.bonds}.")
if p.type == self.part_type_after_glueing:
self.assertIn(bond[0], (self.H, self.H2))
# Bonds to virtual sites:
Expand All @@ -546,9 +552,6 @@ def test_glue_to_surface_random(self):
self.assertEqual(
system.part.by_id(bond[1]).type,
self.part_type_after_glueing)
else:
raise Exception(
f"Particle {p.id} of type {p.type} should not have bonds, yet has {p.bonds}.")

# Collect bonds
# Sort bond partners to make them unique independently of
Expand Down Expand Up @@ -689,16 +692,15 @@ def verify_triangle_binding(self, distance, first_bond, angle_res):
found_angle_bonds = []
for i in range(n):
for b in system.part.by_id(i).bonds:
self.assertIn(
len(b), (2, 3), msg="There should only be 2- and 3-particle bonds")
if len(b) == 2:
self.assertEqual(b[0]._bond_id, self.H._bond_id)
found_pairs.append(tuple(sorted((i, b[1]))))
elif len(b) == 3:
partners = sorted(b[1:])
found_angle_bonds.append(
(i, b[0]._bond_id, partners[0], partners[1]))
else:
raise Exception(
"There should be only 2- and 3-particle bonds")

# The order between expected and found bonds does not always match
# because collisions occur in random order. Sort stuff
Expand Down
6 changes: 3 additions & 3 deletions testsuite/python/constraint_shape_based.py
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ def z(y, r1, r2, l): return l / (r1 - r2) * \
cyl_transform_params=ctp, r1=R1, r2=R2, thickness=D, length=LENGTH)
for y in y_vals:
dist = shape.calc_distance(position=[0.0, y, z(y, R1, R2, LENGTH)])
self.assertAlmostEqual(dist[0], -0.5 * D)
self.assertAlmostEqual(dist[0], -0.5 * D)

# check sign of dist
shape = espressomd.shapes.HollowConicalFrustum(
Expand Down Expand Up @@ -190,12 +190,12 @@ def z(y, r1, r2, l): return l / (r1 - r2) * \
r2=0,
thickness=0.,
length=LENGTH,
central_angle=np.pi)
central_angle=np.pi)
# with this setup, the edges coincide with the xy angle bisectors

# point inside LENGTH
probe_pos = [LENGTH / 2., LENGTH / 2., 5]
d_vec_expected = np.array([0, 0, 5])
d_vec_expected = np.array([0, 0, 5])
dist = shape.calc_distance(position=probe_pos)
self.assertAlmostEqual(dist[0], np.linalg.norm(d_vec_expected))
np.testing.assert_array_almost_equal(d_vec_expected, np.copy(dist[1]))
Expand Down
52 changes: 5 additions & 47 deletions testsuite/python/hybrid_decomposition.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
#
import unittest as ut
import unittest_decorators as utx
import tests_common
import espressomd
import itertools
import numpy as np
Expand Down Expand Up @@ -155,48 +156,9 @@ def test_position_rounding(self):

@utx.skipIfMissingFeatures(["LENNARD_JONES"])
def test_non_bonded_loop_trace(self):
"""Validates that the distances used by the non-bonded loop
match with the minimum image distance accessible by Python,
checks that no pairs are lost or double-counted.
"""
self.prepare_hybrid_setup(n_part_small=50, n_part_large=50)
# regular cutoff used in preparation
cutoff_regular = 2.5

cs_pairs = self.system.cell_system.non_bonded_loop_trace()
distance_vec = self.system.distance_vec

# Distance for all pairs of particles obtained by Python
py_distances = {}
for p1, p2 in self.system.part.pairs():
py_distances[p1.id, p2.id] = np.copy(distance_vec(p1, p2))

# Go through pairs found by the non-bonded loop and check distance
for p in cs_pairs:
# p is a tuple with (id1, id2, pos1, pos2, vec21, mpi_node)
# Note that system.distance_vec uses the opposite sign convention
# as the minimum image distance in the core
self.assertTrue(
(p[0], p[1]) in py_distances or
(p[1], p[0]) in py_distances,
msg=f"Extra pair from core {p}")
if (p[0], p[1]) in py_distances:
np.testing.assert_allclose(
np.copy(p[4]), -py_distances[p[0], p[1]])
del py_distances[p[0], p[1]]
elif (p[1], p[0]) in py_distances:
np.testing.assert_allclose(
np.copy(p[4]), py_distances[p[1], p[0]])
del py_distances[p[1], p[0]]

# test for pairs from regular child decomposition with more than one
# cell
for ids, dist in py_distances.items():
self.assertGreaterEqual(
np.linalg.norm(dist),
cutoff_regular,
msg=f"Pair not found by the core {ids}")
cutoff = 2.5
tests_common.check_non_bonded_loop_trace(self, self.system, cutoff)

@utx.skipIfMissingFeatures(["LENNARD_JONES"])
def test_against_nsquare(self):
Expand Down Expand Up @@ -237,12 +199,8 @@ def test_sort_into_child_decs(self):
"""
n_parts = 3
parts = self.system.part.add(
pos=np.random.random(
(n_parts,
3)) * self.system.box_l[0],
type=np.random.randint(
2,
size=n_parts))
pos=np.random.random((n_parts, 3)) * self.system.box_l[0],
type=np.random.randint(2, size=n_parts))
for ndx, types in enumerate(itertools.product([0, 1], repeat=n_parts)):
parts.type = types
n_square_type = ndx % 2
Expand Down
7 changes: 2 additions & 5 deletions testsuite/python/lb_momentum_conservation.py
Original file line number Diff line number Diff line change
Expand Up @@ -81,11 +81,8 @@ def test(self):
np.testing.assert_allclose(np.copy(p.v), v_final, atol=2.2E-3)

# Make sure, the particle has crossed the periodic boundaries
self.assertGreater(
np.amax(
np.abs(v_final) *
self.system.time),
BOX_SIZE)
self.assertGreater(np.amax(np.abs(v_final) * self.system.time),
BOX_SIZE)


@utx.skipIfMissingGPU()
Expand Down
4 changes: 2 additions & 2 deletions testsuite/python/lb_slice.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,8 @@

class LBSliceTest(ut.TestCase):

"""This simple test first writes random numbers and then reads them
to same slices of LB nodes and compares if the results are the same,
"""This simple test first writes random numbers and then reads them
to same slices of LB nodes and compares if the results are the same,
shape-wise and value-wise.
"""

Expand Down
4 changes: 2 additions & 2 deletions testsuite/python/lees_edwards.py
Original file line number Diff line number Diff line change
Expand Up @@ -678,15 +678,15 @@ def test_zz_lj(self):
system.lees_edwards.shear_direction = 2
system.lees_edwards.shear_plane_normal = 0
system.integrator.run(1, recalc_forces=True)
tests_common.check_non_bonded_loop_trace(system)
tests_common.check_non_bonded_loop_trace(self, system)

# Rewind the clock to get back the LE offset applied during force calc
system.time = system.time - system.time_step
tests_common.verify_lj_forces(system, 1E-7)

system.thermostat.set_langevin(kT=.1, gamma=5, seed=2)
system.integrator.run(50)
tests_common.check_non_bonded_loop_trace(system)
tests_common.check_non_bonded_loop_trace(self, system)


if __name__ == "__main__":
Expand Down
2 changes: 1 addition & 1 deletion testsuite/python/linear_momentum_lb.py
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ class LBGPULinearMomentum(LinearMomentumTest, ut.TestCase):
"""Test for the GPU implementation of the LB."""

lb_class = espressomd.lb.LBFluidGPU
atol = 1e-6
atol = 1e-5


if __name__ == '__main__':
Expand Down
7 changes: 2 additions & 5 deletions testsuite/python/oif_volume_conservation.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,11 +64,8 @@ def test(self):

# Test that restoring forces net to zero and don't produce a torque
system.integrator.run(1)
np.testing.assert_allclose(
np.sum(
partcls.f, axis=0), [
0., 0., 0.], atol=1E-12)

total_force = np.sum(partcls.f, axis=0)
np.testing.assert_allclose(total_force, [0., 0., 0.], atol=1E-12)
total_torque = np.zeros(3)
for p in system.part:
total_torque += np.cross(p.pos, p.f)
Expand Down
2 changes: 1 addition & 1 deletion testsuite/python/pairs.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
class PairTest(ut.TestCase):
"""
Tests the particle pair finder of the cell system.
It checks that particles are found if their distance is below the threshold,
It checks that particles are found if their distance is below the threshold,
no matter the type of cell system, periodicity and the image box they are in.
Also tests that the ``types`` argument works as expected and an exception is raised
when the distance threshold is larger than the cell size.
Expand Down
19 changes: 15 additions & 4 deletions testsuite/python/polymer_linear.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ class LinearPolymerPositions(ut.TestCase):
* bond lengths
* bond angles
* starting positions
* minimum distance for self avoiding walks
* minimum distance for self-avoiding walks
* distance to constraints
"""

Expand All @@ -37,6 +37,10 @@ class LinearPolymerPositions(ut.TestCase):

system = espressomd.System(box_l=[box_l, box_l, box_l])

def tearDown(self):
self.system.part.clear()
self.system.constraints.clear()

def assertShape(self, positions, n_poly, n_mono):
"""
Assert that positions array has expected shape and
Expand Down Expand Up @@ -188,15 +192,14 @@ def test_respect_constraints_wall(self):
self.assertGreaterEqual(z, 0.5 * self.box_l)

# assert that illegal start position raises error
with self.assertRaisesRegex(Exception, 'Invalid start positions.'):
with self.assertRaisesRegex(RuntimeError, "Invalid start positions"):
illegal_start = np.array([[1., 1., 0.2 * self.box_l]])
positions = espressomd.polymer.linear_polymer_positions(
n_polymers=1,
beads_per_chain=10,
start_positions=illegal_start,
bond_length=bond_length,
respect_constraints=True, seed=self.seed)
self.system.constraints.remove(wall_constraint)

def test_exceptions(self):
"""
Expand All @@ -212,7 +215,15 @@ def test_exceptions(self):
espressomd.polymer.linear_polymer_positions(
n_polymers=1, beads_per_chain=10, bond_length=0.1, seed=10,
bondangle=0.1)
with self.assertRaisesRegex(Exception, 'Failed to create polymer positions.'):
with self.assertRaisesRegex(RuntimeError, "Failed to create polymer positions"):
espressomd.polymer.linear_polymer_positions(
n_polymers=1, beads_per_chain=10,
start_positions=np.array([[0, 0, 0]]),
bond_length=self.box_l / 2, min_distance=self.box_l / 2.1,
bond_angle=np.pi, max_tries=2, seed=self.seed)
# check that existing particles are detected
with self.assertRaisesRegex(RuntimeError, "Failed to create polymer positions"):
self.system.part.add(pos=self.system.box_l / 2.)
espressomd.polymer.linear_polymer_positions(
n_polymers=1, beads_per_chain=10,
start_positions=np.array([[0, 0, 0]]),
Expand Down
5 changes: 2 additions & 3 deletions testsuite/python/random_pairs.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,7 @@
import numpy as np
import itertools
import collections

from tests_common import check_non_bonded_loop_trace
import tests_common


class RandomPairTest(ut.TestCase):
Expand Down Expand Up @@ -92,7 +91,7 @@ def check_n_squared(self, n2_pairs):
def test(self):
periods = [0, 1]
self.system.periodicity = [True, True, True]
check_non_bonded_loop_trace(self.system)
tests_common.check_non_bonded_loop_trace(self, self.system)

for periodicity in itertools.product(periods, periods, periods):
self.system.periodicity = periodicity
Expand Down
22 changes: 12 additions & 10 deletions testsuite/python/tests_common.py
Original file line number Diff line number Diff line change
Expand Up @@ -262,6 +262,8 @@ def get_histogram(pos, obs_params, coord_system, **kwargs):
Bins and bin edges.
"""
assert coord_system in ('cartesian', 'cylindrical'), \
f"Unknown coord system '{coord_system}'"
if coord_system == 'cartesian':
bins = (obs_params['n_x_bins'],
obs_params['n_y_bins'],
Expand All @@ -276,8 +278,6 @@ def get_histogram(pos, obs_params, coord_system, **kwargs):
extent = [(obs_params['min_r'], obs_params['max_r']),
(obs_params['min_phi'], obs_params['max_phi']),
(obs_params['min_z'], obs_params['max_z'])]
else:
raise ValueError(f"Unknown coord system '{coord_system}'")
return np.histogramdd(pos, bins=bins, range=extent, **kwargs)


Expand Down Expand Up @@ -348,7 +348,7 @@ def random_dipoles(n_particles):
return dip


def check_non_bonded_loop_trace(system):
def check_non_bonded_loop_trace(ut_obj, system, cutoff=None):
"""Validates that the distances used by the non-bonded loop
match with the minimum image distance accessible by Python,
checks that no pairs are lost or double-counted.
Expand All @@ -358,7 +358,8 @@ def check_non_bonded_loop_trace(system):
# format [id1, id2, pos1, pos2, vec2, mpi_node]

distance_vec = system.distance_vec
cutoff = system.cell_system.max_cut_nonbonded
if cutoff is None:
cutoff = system.cell_system.max_cut_nonbonded

# Distance for all pairs of particles obtained by Python
py_distances = {}
Expand All @@ -367,10 +368,13 @@ def check_non_bonded_loop_trace(system):

# Go through pairs found by the non-bonded loop and check distance
for p in cs_pairs:
# p is a tuple with (id1,id2,pos1,pos2,vec21)
# p is a tuple with (id1, id2, pos1, pos2, vec21, mpi_node)
# Note that system.distance_vec uses the opposite sign convention
# as the minimum image distance in the core

ut_obj.assertTrue(
(p[0], p[1]) in py_distances or
(p[1], p[0]) in py_distances,
msg=f"Extra pair from core {p}")
if (p[0], p[1]) in py_distances:
np.testing.assert_allclose(
np.copy(p[4]), -py_distances[p[0], p[1]])
Expand All @@ -379,9 +383,7 @@ def check_non_bonded_loop_trace(system):
np.testing.assert_allclose(
np.copy(p[4]), py_distances[p[1], p[0]])
del py_distances[p[1], p[0]]
else:
raise Exception("Extra pair from core", p)

for ids, dist in py_distances.items():
if np.linalg.norm(dist) < cutoff:
raise Exception("Pair not found by the core", ids)
ut_obj.assertGreaterEqual(np.linalg.norm(dist), cutoff,
msg=f"Pair not found by the core {ids}")
4 changes: 2 additions & 2 deletions testsuite/scripts/tutorials/test_active_matter.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
import importlib_wrapper

tutorial, skipIfMissingFeatures = importlib_wrapper.configure_and_import(
"@TUTORIALS_DIR@/active_matter/active_matter.py",
"@TUTORIALS_DIR@/active_matter/active_matter.py",
gpu=True,
ED_N_SAMPLING_STEPS=100000,
RECT_N_SAMPLES=150,
Expand Down Expand Up @@ -54,4 +54,4 @@ def test_hydrodynamics(self):


if __name__ == "__main__":
ut.main()
ut.main()
2 changes: 1 addition & 1 deletion testsuite/scripts/tutorials/test_charged_system.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ class Tutorial02_1(ut.TestCase):
system = tutorial.system

def test_overcharging(self):
"""
"""
Test that adding salt leads to a positive layer around the rod
"""

Expand Down

0 comments on commit 6d91ec5

Please sign in to comment.