From 6e7baaded59cfc2a059762a0cc22dde87ee57e38 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jean-No=C3=ABl=20Grad?= Date: Mon, 4 Apr 2022 20:30:52 +0200 Subject: [PATCH 01/10] CMake: Bump Python requirements --- CMakeLists.txt | 2 +- testsuite/scripts/importlib_wrapper.py | 7 ++----- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index e9c9a0286b9..7a47427941e 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -170,7 +170,7 @@ if(WITH_CUDA) endif() endif(WITH_CUDA) -find_package(PythonInterp 3.7 REQUIRED) +find_package(PythonInterp 3.8 REQUIRED) if(WITH_PYTHON) find_package(Cython 0.29.14 REQUIRED) diff --git a/testsuite/scripts/importlib_wrapper.py b/testsuite/scripts/importlib_wrapper.py index 87ec2229749..d5576798a44 100644 --- a/testsuite/scripts/importlib_wrapper.py +++ b/testsuite/scripts/importlib_wrapper.py @@ -90,11 +90,8 @@ def configure_and_import(filepath, return module, skipIfMissingGPU filepath = os.path.abspath(filepath) # load original script - # read in binary mode, then decode as UTF-8 to avoid this python3.5 error: - # UnicodeDecodeError: 'ascii' codec can't decode byte 0xc3 in position 915: - # ordinal not in range(128) - with open(filepath, "rb") as f: - code = f.read().decode(encoding="utf-8") + with open(filepath, "r") as f: + code = f.read() # custom substitutions code = substitutions(code) assert code.strip() From 0f1353b716cfe587d0bb2a9be14343db1a9ea864 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jean-No=C3=ABl=20Grad?= Date: Mon, 4 Apr 2022 20:35:26 +0200 Subject: [PATCH 02/10] core: Dihedral code maintenance Replace raw pointers by references, make the regular and tabulated dihedral bond kernels more similar, document singularity issue. --- doc/sphinx/inter_bonded.rst | 5 ++ src/core/bonded_interactions/bonded_tab.hpp | 23 ++++-- src/core/bonded_interactions/dihedral.hpp | 81 +++++++++++---------- 3 files changed, 63 insertions(+), 46 deletions(-) diff --git a/doc/sphinx/inter_bonded.rst b/doc/sphinx/inter_bonded.rst index 465fb40f046..cf9b921bcc6 100644 --- a/doc/sphinx/inter_bonded.rst +++ b/doc/sphinx/inter_bonded.rst @@ -399,6 +399,9 @@ angle between the planes defined by the particle triples :math:`p_1`, Together with appropriate Lennard-Jones interactions, this potential can mimic a large number of atomic torsion potentials. +Note that there is a singularity in the forces, but not in the energy, when +:math:`\phi = 0` and :math:`\phi = \pi`. + Tabulated dihedral potential ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -414,6 +417,8 @@ A tabulated dihedral interaction can be instantiated via The energy and force tables must be sampled from :math:`0` to :math:`2\pi`. For details of the interpolation, see :ref:`Tabulated interaction`. +Note that there is a singularity in the forces, but not in the energy, when +:math:`\phi = 0` and :math:`\phi = \pi`. .. _Immersed Boundary Method interactions: diff --git a/src/core/bonded_interactions/bonded_tab.hpp b/src/core/bonded_interactions/bonded_tab.hpp index 4863697bd1d..1cd483f7939 100644 --- a/src/core/bonded_interactions/bonded_tab.hpp +++ b/src/core/bonded_interactions/bonded_tab.hpp @@ -204,6 +204,8 @@ inline double TabulatedAngleBond::energy(Utils::Vector3d const &r_mid, /** Compute the four-body dihedral interaction force. * This function is not tested yet. + * The forces have a singularity at @f$ \phi = 0 @f$ and @f$ \phi = \pi @f$ + * (see @cite swope92a page 592). * * @param[in] r1 Position of the first particle. * @param[in] r2 Position of the second particle. @@ -220,18 +222,18 @@ TabulatedDihedralBond::forces(Utils::Vector3d const &r1, /* vectors for dihedral angle calculation */ Utils::Vector3d v12, v23, v34, v12Xv23, v23Xv34; double l_v12Xv23, l_v23Xv34; - /* dihedral angle, cosine of the dihedral angle, cosine of the bond angles */ + /* dihedral angle, cosine of the dihedral angle */ double phi, cos_phi; /* dihedral angle */ - calc_dihedral_angle(r1, r2, r3, r4, v12, v23, v34, v12Xv23, &l_v12Xv23, - v23Xv34, &l_v23Xv34, &cos_phi, &phi); + auto const angle_is_undefined = + calc_dihedral_angle(r1, r2, r3, r4, v12, v23, v34, v12Xv23, l_v12Xv23, + v23Xv34, l_v23Xv34, cos_phi, phi); /* dihedral angle not defined - force zero */ - if (phi == -1.0) { + if (angle_is_undefined) { return {}; } - /* calculate force components (directions) */ auto const f1 = (v23Xv34 - cos_phi * v12Xv23) / l_v12Xv23; auto const f4 = (v12Xv23 - cos_phi * v23Xv34) / l_v23Xv34; @@ -253,6 +255,7 @@ TabulatedDihedralBond::forces(Utils::Vector3d const &r1, /** Compute the four-body dihedral interaction energy. * This function is not tested yet. + * The energy doesn't have any singularity if the angle phi is well-defined. * * @param[in] r1 Position of the first particle. * @param[in] r2 Position of the second particle. @@ -267,8 +270,14 @@ inline boost::optional TabulatedDihedralBond::energy( double l_v12Xv23, l_v23Xv34; /* dihedral angle, cosine of the dihedral angle */ double phi, cos_phi; - calc_dihedral_angle(r1, r2, r3, r4, v12, v23, v34, v12Xv23, &l_v12Xv23, - v23Xv34, &l_v23Xv34, &cos_phi, &phi); + auto const angle_is_undefined = + calc_dihedral_angle(r1, r2, r3, r4, v12, v23, v34, v12Xv23, l_v12Xv23, + v23Xv34, l_v23Xv34, cos_phi, phi); + /* dihedral angle not defined - energy zero */ + if (angle_is_undefined) { + return {}; + } + return pot->energy(phi); } diff --git a/src/core/bonded_interactions/dihedral.hpp b/src/core/bonded_interactions/dihedral.hpp index f02ea9c26d2..0991f8304ee 100644 --- a/src/core/bonded_interactions/dihedral.hpp +++ b/src/core/bonded_interactions/dihedral.hpp @@ -83,7 +83,7 @@ struct DihedralBond { * specified by the particle triples (p1,p2,p3) and (p2,p3,p4). * Vectors a, b and c are the bond vectors between consecutive particles. * If the a,b or b,c are parallel the dihedral angle is not defined in which - * case the routine returns phi=-1. Calling functions should check for that + * case the function returns true. Calling functions should check for that. * * @param[in] r1 , r2 , r3 , r4 Positions of the particles forming the dihedral * @param[out] a Vector from @p p1 to @p p2 @@ -94,14 +94,15 @@ struct DihedralBond { * @param[out] bXc Vector product of b and c * @param[out] l_bXc |bXc| * @param[out] cosphi Cosine of the dihedral angle - * @param[out] phi Dihedral angle + * @param[out] phi Dihedral angle in the range [0, pi] + * @return Whether the angle is undefined. */ -inline void +inline bool calc_dihedral_angle(Utils::Vector3d const &r1, Utils::Vector3d const &r2, Utils::Vector3d const &r3, Utils::Vector3d const &r4, Utils::Vector3d &a, Utils::Vector3d &b, Utils::Vector3d &c, - Utils::Vector3d &aXb, double *l_aXb, Utils::Vector3d &bXc, - double *l_bXc, double *cosphi, double *phi) { + Utils::Vector3d &aXb, double &l_aXb, Utils::Vector3d &bXc, + double &l_bXc, double &cosphi, double &phi) { a = box_geo.get_mi_vector(r2, r1); b = box_geo.get_mi_vector(r3, r2); c = box_geo.get_mi_vector(r4, r3); @@ -111,31 +112,34 @@ calc_dihedral_angle(Utils::Vector3d const &r1, Utils::Vector3d const &r2, bXc = vector_product(b, c); /* calculate the unit vectors */ - *l_aXb = aXb.norm(); - *l_bXc = bXc.norm(); + l_aXb = aXb.norm(); + l_bXc = bXc.norm(); /* catch case of undefined dihedral angle */ - if (*l_aXb <= TINY_LENGTH_VALUE || *l_bXc <= TINY_LENGTH_VALUE) { - *phi = -1.0; - *cosphi = 0; - return; + if (l_aXb <= TINY_LENGTH_VALUE || l_bXc <= TINY_LENGTH_VALUE) { + phi = -1.0; + cosphi = 0.0; + return true; } - aXb /= *l_aXb; - bXc /= *l_bXc; + aXb /= l_aXb; + bXc /= l_bXc; - *cosphi = aXb * bXc; + cosphi = aXb * bXc; - if (fabs(fabs(*cosphi) - 1) < TINY_SIN_VALUE) - *cosphi = std::round(*cosphi); + if (fabs(fabs(cosphi) - 1) < TINY_SIN_VALUE) + cosphi = std::round(cosphi); /* Calculate dihedral angle */ - *phi = acos(*cosphi); + phi = acos(cosphi); if ((aXb * c) < 0.0) - *phi = (2.0 * Utils::pi()) - *phi; + phi = (2.0 * Utils::pi()) - phi; + return false; } /** Compute the four-body dihedral interaction force. + * The forces have a singularity at @f$ \phi = 0 @f$ and @f$ \phi = \pi @f$ + * (see @cite swope92a page 592). * * @param[in] r1 Position of the first particle. * @param[in] r2 Position of the second particle. @@ -152,20 +156,19 @@ DihedralBond::forces(Utils::Vector3d const &r1, Utils::Vector3d const &r2, Utils::Vector3d v12, v23, v34, v12Xv23, v23Xv34; double l_v12Xv23, l_v23Xv34; /* dihedral angle, cosine of the dihedral angle */ - double phi, cosphi, sinmphi_sinphi; - /* force factors */ - double fac; + double phi, cos_phi, sin_mphi_over_sin_phi; /* dihedral angle */ - calc_dihedral_angle(r1, r2, r3, r4, v12, v23, v34, v12Xv23, &l_v12Xv23, - v23Xv34, &l_v23Xv34, &cosphi, &phi); + auto const angle_is_undefined = + calc_dihedral_angle(r1, r2, r3, r4, v12, v23, v34, v12Xv23, l_v12Xv23, + v23Xv34, l_v23Xv34, cos_phi, phi); /* dihedral angle not defined - force zero */ - if (phi == -1.0) { + if (angle_is_undefined) { return {}; } - auto const f1 = (v23Xv34 - cosphi * v12Xv23) / l_v12Xv23; - auto const f4 = (v12Xv23 - cosphi * v23Xv34) / l_v23Xv34; + auto const f1 = (v23Xv34 - cos_phi * v12Xv23) / l_v12Xv23; + auto const f4 = (v12Xv23 - cos_phi * v23Xv34) / l_v23Xv34; auto const v23Xf1 = vector_product(v23, f1); auto const v23Xf4 = vector_product(v23, f4); @@ -173,19 +176,17 @@ DihedralBond::forces(Utils::Vector3d const &r1, Utils::Vector3d const &r2, auto const v12Xf1 = vector_product(v12, f1); /* calculate force magnitude */ - fac = -bend * mult; + auto fac = -bend * mult; if (fabs(sin(phi)) < TINY_SIN_VALUE) { - /*(comes from taking the first term of the MacLaurin expansion of - sin(n*phi - phi0) and sin(phi) and then making the division). - The original code had a 2PI term in the cosine (cos(2PI - nPhi)) - but I removed it because it wasn't doing anything. AnaVV*/ - sinmphi_sinphi = mult * cos(mult * phi - phase) / cosphi; + /* comes from taking the first term of the MacLaurin expansion of + * sin(n * phi - phi0) and sin(phi) and then making the division */ + sin_mphi_over_sin_phi = mult * cos(mult * phi - phase) / cos_phi; } else { - sinmphi_sinphi = sin(mult * phi - phase) / sin(phi); + sin_mphi_over_sin_phi = sin(mult * phi - phase) / sin(phi); } - fac *= sinmphi_sinphi; + fac *= sin_mphi_over_sin_phi; /* store dihedral forces */ auto const force1 = fac * v23Xf1; @@ -196,6 +197,7 @@ DihedralBond::forces(Utils::Vector3d const &r1, Utils::Vector3d const &r2, } /** Compute the four-body dihedral interaction energy. + * The energy doesn't have any singularity if the angle phi is well-defined. * * @param[in] r1 Position of the first particle. * @param[in] r2 Position of the second particle. @@ -210,12 +212,13 @@ DihedralBond::energy(Utils::Vector3d const &r1, Utils::Vector3d const &r2, Utils::Vector3d v12, v23, v34, v12Xv23, v23Xv34; double l_v12Xv23, l_v23Xv34; /* dihedral angle, cosine of the dihedral angle */ - double phi, cosphi; + double phi, cos_phi; - calc_dihedral_angle(r1, r2, r3, r4, v12, v23, v34, v12Xv23, &l_v12Xv23, - v23Xv34, &l_v23Xv34, &cosphi, &phi); - /* dihedral angle not defined - force zero */ - if (phi == -1.0) { + auto const angle_is_undefined = + calc_dihedral_angle(r1, r2, r3, r4, v12, v23, v34, v12Xv23, l_v12Xv23, + v23Xv34, l_v23Xv34, cos_phi, phi); + /* dihedral angle not defined - energy zero */ + if (angle_is_undefined) { return {}; } From 1f562a2db103aaeefc4679d6e9bd073f24b4535c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jean-No=C3=ABl=20Grad?= Date: Mon, 4 Apr 2022 20:42:21 +0200 Subject: [PATCH 03/10] tests: Properly check dihedral bonds Fix broken analytical formula for the dihedral potential and forces in the test case. Check regular and tabulated dihedral bond kernels for multiple choices of multiplicity and phase. --- src/core/bonded_interactions/bonded_tab.hpp | 2 - testsuite/python/interactions_dihedral.py | 253 ++++++++------------ 2 files changed, 106 insertions(+), 149 deletions(-) diff --git a/src/core/bonded_interactions/bonded_tab.hpp b/src/core/bonded_interactions/bonded_tab.hpp index 1cd483f7939..99498d5c4bb 100644 --- a/src/core/bonded_interactions/bonded_tab.hpp +++ b/src/core/bonded_interactions/bonded_tab.hpp @@ -203,7 +203,6 @@ inline double TabulatedAngleBond::energy(Utils::Vector3d const &r_mid, } /** Compute the four-body dihedral interaction force. - * This function is not tested yet. * The forces have a singularity at @f$ \phi = 0 @f$ and @f$ \phi = \pi @f$ * (see @cite swope92a page 592). * @@ -254,7 +253,6 @@ TabulatedDihedralBond::forces(Utils::Vector3d const &r1, } /** Compute the four-body dihedral interaction energy. - * This function is not tested yet. * The energy doesn't have any singularity if the angle phi is well-defined. * * @param[in] r1 Position of the first particle. diff --git a/testsuite/python/interactions_dihedral.py b/testsuite/python/interactions_dihedral.py index 91c1cbb5c45..0c68450c0e2 100644 --- a/testsuite/python/interactions_dihedral.py +++ b/testsuite/python/interactions_dihedral.py @@ -22,28 +22,19 @@ import espressomd -# Dihedral interaction needs more rigorous tests. -# The geometry checked here is rather simple and special. -# I also found that as the dihedral angle approaches to 0, the simulation -# values deviate from the analytic values by roughly 10%. - def rotate_vector(v, k, phi): """Rotates vector v around unit vector k by angle phi. Uses Rodrigues' rotation formula.""" - vrot = v * np.cos(phi) + np.cross(k, v) * \ - np.sin(phi) + k * np.dot(k, v) * (1.0 - np.cos(phi)) + vrot = np.array(v) * np.cos(phi) + np.cross(k, v) * \ + np.sin(phi) + np.array(k) * np.dot(k, v) * (1.0 - np.cos(phi)) return vrot -def dihedral_potential(k, phi, n, phase): - if phi == -1: - return 0 - else: - return k * (1 - np.cos(n * phi - phase)) - - -def dihedral_force(k, n, phase, p1, p2, p3, p4): +def dihedral_potential_and_forces(k, n, phase, p1, p2, p3, p4): + """ + Calculate the potential and forces for a dihedral angle. + """ v12 = p2 - p1 v23 = p3 - p2 v34 = p4 - p3 @@ -52,158 +43,126 @@ def dihedral_force(k, n, phase, p1, p2, p3, p4): l_v12Xv23 = np.linalg.norm(v12Xv23) v23Xv34 = np.cross(v23, v34) l_v23Xv34 = np.linalg.norm(v23Xv34) - # if dihedral angle is not defined, no forces - if l_v12Xv23 <= 1e-8 or l_v23Xv34 <= 1e-8: - return 0, 0, 0 - else: - cosphi = np.abs(np.dot(v12Xv23, v23Xv34)) / (l_v12Xv23 * l_v23Xv34) - phi = np.arccos(cosphi) - f1 = (v23Xv34 - cosphi * v12Xv23) / l_v12Xv23 - f4 = (v12Xv23 - cosphi * v23Xv34) / l_v23Xv34 - v23Xf1 = np.cross(v23, f1) - v23Xf4 = np.cross(v23, f4) - v34Xf4 = np.cross(v34, f4) - v12Xf1 = np.cross(v12, f1) + phi = np.arctan2(np.dot(v23, np.cross(v12Xv23, v23Xv34)), + np.dot(v23, v23) * np.dot(v12Xv23, v23Xv34)) + + f1 = (v23Xv34 - np.cos(phi) * v12Xv23) / l_v12Xv23 + f4 = (v12Xv23 - np.cos(phi) * v23Xv34) / l_v23Xv34 + v23Xf1 = np.cross(v23, f1) + v23Xf4 = np.cross(v23, f4) + v34Xf4 = np.cross(v34, f4) + v12Xf1 = np.cross(v12, f1) + + # handle singularity near TINY_SIN_VALUE + if np.abs(np.sin(phi)) < 1e-10: + coeff = -k * n**2 * np.cos(n * phi - phase) / np.cos(phi) + else: coeff = -k * n * np.sin(n * phi - phase) / np.sin(phi) - force1 = coeff * v23Xf1 - force2 = coeff * (v34Xf4 - v12Xf1 - v23Xf1) - force3 = coeff * (v12Xf1 - v23Xf4 - v34Xf4) - return force1, force2, force3 + force1 = coeff * v23Xf1 + force2 = coeff * (v34Xf4 - v12Xf1 - v23Xf1) + force3 = coeff * (v12Xf1 - v23Xf4 - v34Xf4) + force4 = coeff * v23Xf4 + potential = k * (1 - np.cos(n * phi - phase)) + return (potential, (force1, force2, force3, force4)) class InteractionsBondedTest(ut.TestCase): - system = espressomd.System(box_l=[1.0, 1.0, 1.0]) + system = espressomd.System(box_l=[10.0, 10.0, 10.0]) + system.cell_system.skin = 0.4 + system.time_step = 0.1 np.random.seed(seed=42) - box_l = 10. - - start_pos = [5., 5., 5.] - axis = np.array([1., 0., 0.]) - axis /= np.linalg.norm(axis) - rel_pos_1 = np.array([0., 1., 0.]) - rel_pos_2 = np.array([0., 0., 1.]) - - def setUp(self): - - self.system.box_l = [self.box_l] * 3 - self.system.cell_system.skin = 0.4 - self.system.time_step = .1 - - self.system.part.add(pos=4 * [self.start_pos], type=4 * [0]) - def tearDown(self): self.system.part.clear() - # Analytical Expression - def dihedral_angle(self, p1, p2, p3, p4): - """ - Calculate the dihedral angle phi based on particles' position p1, p2, p3, p4. - """ - v12 = p2 - p1 - v23 = p3 - p2 - v34 = p4 - p3 - - v12Xv23 = np.cross(v12, v23) - l_v12Xv23 = np.linalg.norm(v12Xv23) - v23Xv34 = np.cross(v23, v34) - l_v23Xv34 = np.linalg.norm(v23Xv34) - - # if dihedral angle is not defined, phi := -1. - if l_v12Xv23 <= 1e-8 or l_v23Xv34 <= 1e-8: - return -1 - else: - cosphi = np.abs(np.dot(v12Xv23, v23Xv34)) / ( - l_v12Xv23 * l_v23Xv34) - return np.arccos(cosphi) + def check_values(self, E_ref, forces_ref, tol=1e-12): + E_sim = self.system.analysis.energy()["bonded"] + np.testing.assert_allclose(E_sim, E_ref, atol=tol) + if forces_ref: + f0, f1, f2, f3 = self.system.part.all().f + f0_ref, f1_ref, f2_ref, f3_ref = forces_ref + np.testing.assert_allclose(np.copy(f0), f0_ref, atol=tol) + np.testing.assert_allclose(np.copy(f1), f1_ref, atol=tol) + np.testing.assert_allclose(np.copy(f2), f2_ref, atol=tol) + np.testing.assert_allclose(np.copy(f3), f3_ref, atol=tol) # Test Dihedral Angle def test_dihedral(self): - p0, p1, p2, p3 = self.system.part.all() - - dh_k = 1 - dh_phase = np.pi / 6 - dh_n = 1 - - dh = espressomd.interactions.Dihedral( - bend=dh_k, mult=dh_n, phase=dh_phase) - self.system.bonded_inter.add(dh) - p1.add_bond((dh, p0, p2, p3)) - p2.pos = p1.pos + [1, 0, 0] - - N = 111 - d_phi = np.pi / (N * 4) - for i in range(N): - p0.pos = p1.pos + \ - rotate_vector(self.rel_pos_1, self.axis, i * d_phi) - p3.pos = p2.pos + \ - rotate_vector(self.rel_pos_2, self.axis, -i * d_phi) - self.system.integrator.run(recalc_forces=True, steps=0) - - # Calculate energies - E_sim = self.system.analysis.energy()["bonded"] - phi = self.dihedral_angle(p0.pos, p1.pos, p2.pos, p3.pos) - E_ref = dihedral_potential(dh_k, phi, dh_n, dh_phase) - - # Calculate forces - f2_sim = p1.f - _, f2_ref, _ = dihedral_force(dh_k, dh_n, dh_phase, - p0.pos, p1.pos, p2.pos, p3.pos) - - # Check that energies match, ... - np.testing.assert_almost_equal(E_sim, E_ref) - # and has correct value. - f2_sim_copy = np.copy(f2_sim) - np.testing.assert_almost_equal(f2_sim_copy, f2_ref) + axis = np.array([1., 0., 0.]) + dh_k = 2. + N = 100 # even number to get singularities at phi=0 and phi=pi + d_phi = 2 * np.pi / N + for dh_n, dh_phi0_div in [(2, 3), (3, 6)]: + with self.subTest(multiplicity=dh_n, phi_0=f"pi / {dh_phi0_div}"): + dh_phi0 = np.pi / dh_phi0_div + dihedral = espressomd.interactions.Dihedral( + bend=dh_k, mult=dh_n, phase=dh_phi0) + self.system.bonded_inter.add(dihedral) + self.system.part.clear() + p0, p1, p2, p3 = self.system.part.add(pos=4 * [(0., 0., 0.)]) + p1.add_bond((dihedral, p0, p2, p3)) + p1.pos = [5., 5., 5.] + p2.pos = p1.pos + [1., 0., 0.] + p0.pos = p1.pos + [0., 1., 0.] + + for i in range(N): + phi = i * d_phi + p3.pos = p2.pos + rotate_vector([0., 1., 0.], axis, phi) + self.system.integrator.run(recalc_forces=True, steps=0) + + # Calculate expected forces and energies + E_ref, forces_ref = dihedral_potential_and_forces( + dh_k, dh_n, dh_phi0, p0.pos, p1.pos, p2.pos, p3.pos) + + self.check_values(E_ref, forces_ref) # Test Tabulated Dihedral Angle @utx.skipIfMissingFeatures(["TABULATED"]) def test_tabulated_dihedral(self): - p0, p1, p2, p3 = self.system.part.all() - - N = 111 + axis = np.array([1., 0., 0.]) + dh_k = 2. + N = 100 # even number to get singularities at phi=0 and phi=pi d_phi = 2 * np.pi / N - # tabulated values for the range [0, 2*pi] - tab_energy = [np.cos(i * d_phi) for i in range(N + 1)] - tab_force = [np.cos(i * d_phi) for i in range(N + 1)] - - dihedral_tabulated = espressomd.interactions.TabulatedDihedral( - energy=tab_energy, force=tab_force) - self.system.bonded_inter.add(dihedral_tabulated) - p1.add_bond((dihedral_tabulated, p0, p2, p3)) - p2.pos = p1.pos + [1, 0, 0] - - # check stored parameters - interaction_id = len(self.system.bonded_inter) - 1 - tabulated = self.system.bonded_inter[interaction_id] - np.testing.assert_allclose(tabulated.params['force'], tab_force) - np.testing.assert_allclose(tabulated.params['energy'], tab_energy) - np.testing.assert_almost_equal(tabulated.params['min'], 0.) - np.testing.assert_almost_equal(tabulated.params['max'], 2 * np.pi) - - # measure at half the angular resolution to observe interpolation - for i in range(2 * N - 1): - # increase dihedral angle by d_phi (phi ~ 0 at i = 0) - p0.pos = p1.pos + \ - rotate_vector(self.rel_pos_1, self.axis, -i * d_phi / 4) - p3.pos = p2.pos + \ - rotate_vector(self.rel_pos_1, self.axis, i * d_phi / 4) - self.system.integrator.run(recalc_forces=True, steps=0) - - # Calculate energies - E_sim = self.system.analysis.energy()["bonded"] - - # Get tabulated values - j = i // 2 - if i % 2 == 0: - E_ref = tab_energy[j] - else: - E_ref = (tab_energy[j] + tab_energy[j + 1]) / 2.0 - - # Check that energies match, ... - np.testing.assert_almost_equal(E_sim, E_ref) + for dh_n, dh_phi0_div in [(2, 3), (3, 6)]: + with self.subTest(multiplicity=dh_n, phi_0=f"pi / {dh_phi0_div}"): + dh_phi0 = np.pi / dh_phi0_div + # tabulated values for the range [0, 2*pi] + phi = d_phi * np.arange(N + 1) + tab_energy = dh_k * (1. - np.cos(dh_n * phi - dh_phi0)) + div = np.sin(phi) + div[0] = div[N // 2] = div[N] = 1. + tab_force = -dh_k * dh_n * np.sin(dh_n * phi - dh_phi0) / div + tab_force[0] = tab_force[N // 2] = tab_force[N] = 0. + dihedral_tabulated = espressomd.interactions.TabulatedDihedral( + energy=tab_energy, force=tab_force) + self.system.bonded_inter.add(dihedral_tabulated) + self.system.part.clear() + p0, p1, p2, p3 = self.system.part.add(pos=4 * [(0., 0., 0.)]) + p1.add_bond((dihedral_tabulated, p0, p2, p3)) + p1.pos = [5., 5., 5.] + p2.pos = p1.pos + [1., 0., 0.] + p0.pos = p1.pos + [0., 1., 0.] + + # use half the angular resolution to observe interpolation + for i in range(2 * N - 1): + phi = i * d_phi / 2. + p3.pos = p2.pos + rotate_vector([0., 1., 0.], axis, phi) + self.system.integrator.run(recalc_forces=True, steps=0) + + # Calculate expected forces and energies + j = i // 2 + if i % 2 == 0: + E_ref = tab_energy[j] + _, forces_ref = dihedral_potential_and_forces( + dh_k, dh_n, dh_phi0, p0.pos, p1.pos, p2.pos, p3.pos) + else: + E_ref = (tab_energy[j] + tab_energy[j + 1]) / 2.0 + forces_ref = None + + self.check_values(E_ref, forces_ref) if __name__ == '__main__': From 4ddd129efaced3b2be20becf67e50189e169a092 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jean-No=C3=ABl=20Grad?= Date: Mon, 4 Apr 2022 21:36:51 +0200 Subject: [PATCH 04/10] tests: Refactor IO test cases Use temporary directories in tests that need to output data to disk to automate file cleanup. Use the pathlib module instead of the os module for operations on filepaths to improve portability. --- testsuite/python/coulomb_cloud_wall.py | 4 +- .../python/coulomb_cloud_wall_duplicated.py | 2 +- testsuite/python/coulomb_mixed_periodicity.py | 4 +- testsuite/python/coulomb_tuning.py | 2 +- testsuite/python/dipolar_direct_summation.py | 15 +- .../dipolar_mdlc_p3m_scafacos_p2nfft.py | 14 +- testsuite/python/ek_eof_one_species.py | 76 +++--- testsuite/python/h5md.py | 64 ++--- testsuite/python/integrator_npt_stats.py | 2 +- testsuite/python/lb_vtk.py | 238 +++++++++--------- testsuite/python/lj.py | 2 +- testsuite/python/mmm1d.py | 2 +- testsuite/python/npt_thermostat.py | 2 +- testsuite/python/oif_volume_conservation.py | 5 +- testsuite/python/save_checkpoint.py | 39 ++- testsuite/python/scafacos_dipoles_1d_2d.py | 12 +- testsuite/python/sigint.py | 4 +- testsuite/python/stokesian_dynamics.py | 2 +- testsuite/python/test_checkpoint.py | 16 +- testsuite/python/tests_common.py | 9 +- testsuite/python/unittest_generator.py | 4 +- testsuite/scripts/importlib_wrapper.py | 32 +-- .../scripts/samples/test_ekboundaries.py | 11 +- .../scripts/samples/test_immersed_boundary.py | 8 +- .../test_object_in_fluid__motivation.py | 11 +- .../scripts/samples/test_save_checkpoint.py | 6 +- testsuite/scripts/test_importlib_wrapper.py | 12 +- testsuite/scripts/tutorials/test_convert.py | 47 ++-- 28 files changed, 317 insertions(+), 328 deletions(-) diff --git a/testsuite/python/coulomb_cloud_wall.py b/testsuite/python/coulomb_cloud_wall.py index c661660f837..26d3f048c8a 100644 --- a/testsuite/python/coulomb_cloud_wall.py +++ b/testsuite/python/coulomb_cloud_wall.py @@ -36,8 +36,8 @@ class CoulombCloudWall(ut.TestCase): """ system = espressomd.System(box_l=[1.0, 1.0, 1.0]) - data = np.genfromtxt(tests_common.abspath( - "data/coulomb_cloud_wall_system.data")) + data = np.genfromtxt(tests_common.data_path( + "coulomb_cloud_wall_system.data")) tolerance = 1E-3 diff --git a/testsuite/python/coulomb_cloud_wall_duplicated.py b/testsuite/python/coulomb_cloud_wall_duplicated.py index 79289a0f360..492f170dd91 100644 --- a/testsuite/python/coulomb_cloud_wall_duplicated.py +++ b/testsuite/python/coulomb_cloud_wall_duplicated.py @@ -35,7 +35,7 @@ class CoulombCloudWall(ut.TestCase): system.time_step = 0.01 system.cell_system.skin = 0.4 data = np.genfromtxt( - tests_common.abspath("data/coulomb_cloud_wall_duplicated_system.data")) + tests_common.data_path("coulomb_cloud_wall_duplicated_system.data")) tolerance = 1E-3 diff --git a/testsuite/python/coulomb_mixed_periodicity.py b/testsuite/python/coulomb_mixed_periodicity.py index 706f6774690..730e42a0993 100644 --- a/testsuite/python/coulomb_mixed_periodicity.py +++ b/testsuite/python/coulomb_mixed_periodicity.py @@ -31,8 +31,8 @@ class CoulombMixedPeriodicity(ut.TestCase): """Test mixed periodicity electrostatics""" system = espressomd.System(box_l=[10, 10, 10]) - data = np.genfromtxt(tests_common.abspath( - "data/coulomb_mixed_periodicity_system.data")) + data = np.genfromtxt(tests_common.data_path( + "coulomb_mixed_periodicity_system.data")) tolerance_force = 5E-4 tolerance_energy = 1.8E-3 diff --git a/testsuite/python/coulomb_tuning.py b/testsuite/python/coulomb_tuning.py index 737cb5c4d47..dbe70141ded 100644 --- a/testsuite/python/coulomb_tuning.py +++ b/testsuite/python/coulomb_tuning.py @@ -39,7 +39,7 @@ def setUp(self): self.system.time_step = 0.01 self.system.cell_system.skin = 0.4 - data = np.load(tests_common.abspath("data/coulomb_tuning_system.npz")) + data = np.load(tests_common.data_path("coulomb_tuning_system.npz")) self.forces = data['forces'] self.system.part.add(pos=data['pos'], q=data['charges']) diff --git a/testsuite/python/dipolar_direct_summation.py b/testsuite/python/dipolar_direct_summation.py index acb688ef84d..f341261e504 100644 --- a/testsuite/python/dipolar_direct_summation.py +++ b/testsuite/python/dipolar_direct_summation.py @@ -19,15 +19,15 @@ import espressomd import espressomd.magnetostatics import espressomd.magnetostatic_extensions -import os +import pathlib import numpy as np import unittest as ut import unittest_decorators as utx import tests_common -OPEN_BOUNDARIES_REF_ENERGY = tests_common.abspath( - "data/dipolar_open_boundaries_energy.npy") -OPEN_BOUNDARIES_REF_ARRAYS = tests_common.abspath( - "data/dipolar_open_boundaries_arrays.npy") +OPEN_BOUNDARIES_REF_ENERGY = tests_common.data_path( + "dipolar_open_boundaries_energy.npy") +OPEN_BOUNDARIES_REF_ARRAYS = tests_common.data_path( + "dipolar_open_boundaries_arrays.npy") @utx.skipIfMissingFeatures(["DIPOLES"]) @@ -114,12 +114,11 @@ def test_gen_reference_data(self): filepaths = ('dipolar_direct_summation_energy.npy', 'dipolar_direct_summation_arrays.npy') for filepath in filepaths: - if os.path.isfile(filepath): - os.remove(filepath) + pathlib.Path(filepath).unlink(missing_ok=True) self.gen_reference_data(filepaths[0], filepaths[1]) for filepath in filepaths: - self.assertTrue(os.path.isfile(filepath)) + self.assertTrue(pathlib.Path(filepath).is_file()) def gen_reference_data(self, filepath_energy=OPEN_BOUNDARIES_REF_ENERGY, filepath_arrays=OPEN_BOUNDARIES_REF_ARRAYS): diff --git a/testsuite/python/dipolar_mdlc_p3m_scafacos_p2nfft.py b/testsuite/python/dipolar_mdlc_p3m_scafacos_p2nfft.py index bdcca976a83..c70460dce7d 100644 --- a/testsuite/python/dipolar_mdlc_p3m_scafacos_p2nfft.py +++ b/testsuite/python/dipolar_mdlc_p3m_scafacos_p2nfft.py @@ -62,14 +62,14 @@ def test_mdlc(self): particle_radius = 0.5 box_l = np.cbrt(4 * n_particle * np.pi / (3 * rho)) * particle_radius s.box_l = 3 * [box_l] - ref_E_path = tests_common.abspath( - "data/mdlc_reference_data_energy.dat") + ref_E_path = tests_common.data_path( + "mdlc_reference_data_energy.dat") ref_E = float(np.genfromtxt(ref_E_path)) * DIPOLAR_PREFACTOR gap_size = 2.0 # Particles data = np.genfromtxt( - tests_common.abspath("data/mdlc_reference_data_forces_torques.dat")) + tests_common.data_path("mdlc_reference_data_forces_torques.dat")) partcls = s.part.add(pos=data[:, 1:4], dip=data[:, 4:7]) partcls.rotation = 3 * [True] @@ -126,7 +126,7 @@ def test_p3m(self): # Particles data = np.genfromtxt( - tests_common.abspath("data/p3m_magnetostatics_system.data")) + tests_common.data_path("p3m_magnetostatics_system.data")) partcls = s.part.add(pos=data[:, 1:4], dip=data[:, 4:7]) partcls.rotation = 3 * [True] @@ -135,7 +135,7 @@ def test_p3m(self): s.actors.add(p3m) s.integrator.run(0) expected = np.genfromtxt( - tests_common.abspath("data/p3m_magnetostatics_expected.data"))[:, 1:] + tests_common.data_path("p3m_magnetostatics_expected.data"))[:, 1:] err_f = self.vector_error( partcls.f, expected[:, 0:3] * DIPOLAR_PREFACTOR) err_t = self.vector_error( @@ -166,7 +166,7 @@ def test_scafacos_dipoles(self): # Particles data = np.genfromtxt( - tests_common.abspath("data/p3m_magnetostatics_system.data")) + tests_common.data_path("p3m_magnetostatics_system.data")) partcls = s.part.add(pos=data[:, 1:4], dip=data[:, 4:7]) partcls.rotation = 3 * [True] @@ -186,7 +186,7 @@ def test_scafacos_dipoles(self): s.actors.add(scafacos) s.integrator.run(0) expected = np.genfromtxt( - tests_common.abspath("data/p3m_magnetostatics_expected.data"))[:, 1:] + tests_common.data_path("p3m_magnetostatics_expected.data"))[:, 1:] err_f = self.vector_error( partcls.f, expected[:, 0:3] * DIPOLAR_PREFACTOR) err_t = self.vector_error( diff --git a/testsuite/python/ek_eof_one_species.py b/testsuite/python/ek_eof_one_species.py index 420b5d70ea6..93251de487f 100644 --- a/testsuite/python/ek_eof_one_species.py +++ b/testsuite/python/ek_eof_one_species.py @@ -19,6 +19,7 @@ import unittest_decorators as utx import unittest_generator as utg import pathlib +import tempfile import sys import math @@ -219,7 +220,7 @@ class ek_eof_one_species(ut.TestCase): def parse_vtk(self, filepath, name, shape): reader = vtk.vtkStructuredPointsReader() - reader.SetFileName(filepath) + reader.SetFileName(str(filepath)) reader.ReadAllVectorsOn() reader.ReadAllScalarsOn() reader.Update() @@ -433,41 +434,44 @@ def test_vtk(self): map(int, np.round(self.system.box_l / params_base['agrid']))) # write VTK files - vtk_root = f"vtk_out/ek_eof_{axis}" - pathlib.Path(vtk_root).mkdir(parents=True, exist_ok=True) - path_vtk_boundary = f"{vtk_root}/boundary.vtk" - path_vtk_velocity = f"{vtk_root}/velocity.vtk" - path_vtk_potential = f"{vtk_root}/potential.vtk" - path_vtk_lbdensity = f"{vtk_root}/density.vtk" - path_vtk_lbforce = f"{vtk_root}/lbforce.vtk" - path_vtk_density = f"{vtk_root}/lbdensity.vtk" - path_vtk_flux = f"{vtk_root}/flux.vtk" - path_vtk_flux_link = f"{vtk_root}/flux_link.vtk" - if espressomd.has_features('EK_DEBUG'): - path_vtk_flux_fluc = f"{vtk_root}/flux_fluc.vtk" - ek.write_vtk_boundary(path_vtk_boundary) - ek.write_vtk_velocity(path_vtk_velocity) - ek.write_vtk_potential(path_vtk_potential) - ek.write_vtk_density(path_vtk_lbdensity) - ek.write_vtk_lbforce(path_vtk_lbforce) - counterions.write_vtk_density(path_vtk_density) - counterions.write_vtk_flux(path_vtk_flux) - if espressomd.has_features('EK_DEBUG'): - counterions.write_vtk_flux_fluc(path_vtk_flux_fluc) - counterions.write_vtk_flux_link(path_vtk_flux_link) - - # load VTK files to check they are correctly formatted - get_vtk = self.parse_vtk - vtk_boundary = get_vtk(path_vtk_boundary, "boundary", grid_dims) - vtk_velocity = get_vtk(path_vtk_velocity, "velocity", grid_dims + [3]) - vtk_potential = get_vtk(path_vtk_potential, "potential", grid_dims) - vtk_lbdensity = get_vtk(path_vtk_lbdensity, "density_lb", grid_dims) - get_vtk(path_vtk_lbforce, "lbforce", grid_dims + [3]) - vtk_density = get_vtk(path_vtk_density, "density_1", grid_dims) - vtk_flux = get_vtk(path_vtk_flux, "flux_1", grid_dims + [3]) - if espressomd.has_features('EK_DEBUG'): - get_vtk(path_vtk_flux_fluc, "flux_fluc_1", grid_dims + [4]) - get_vtk(path_vtk_flux_link, "flux_link_1", grid_dims + [13]) + with tempfile.TemporaryDirectory() as tmp_directory: + path_vtk_root = pathlib.Path(tmp_directory) + path_vtk_root.mkdir(parents=True, exist_ok=True) + path_vtk_boundary = path_vtk_root / "boundary.vtk" + path_vtk_velocity = path_vtk_root / "velocity.vtk" + path_vtk_potential = path_vtk_root / "potential.vtk" + path_vtk_lbdensity = path_vtk_root / "density.vtk" + path_vtk_lbforce = path_vtk_root / "lbforce.vtk" + path_vtk_density = path_vtk_root / "lbdensity.vtk" + path_vtk_flux = path_vtk_root / "flux.vtk" + path_vtk_flux_link = path_vtk_root / "flux_link.vtk" + if espressomd.has_features('EK_DEBUG'): + path_vtk_flux_fluc = path_vtk_root / "flux_fluc.vtk" + ek.write_vtk_boundary(str(path_vtk_boundary)) + ek.write_vtk_velocity(str(path_vtk_velocity)) + ek.write_vtk_potential(str(path_vtk_potential)) + ek.write_vtk_density(str(path_vtk_lbdensity)) + ek.write_vtk_lbforce(str(path_vtk_lbforce)) + counterions.write_vtk_density(str(path_vtk_density)) + counterions.write_vtk_flux(str(path_vtk_flux)) + if espressomd.has_features('EK_DEBUG'): + counterions.write_vtk_flux_fluc(str(path_vtk_flux_fluc)) + counterions.write_vtk_flux_link(str(path_vtk_flux_link)) + + # load VTK files to check they are correctly formatted + get_vtk = self.parse_vtk + vtk_boundary = get_vtk(path_vtk_boundary, "boundary", grid_dims) + vtk_velocity = get_vtk( + path_vtk_velocity, "velocity", grid_dims + [3]) + vtk_potential = get_vtk(path_vtk_potential, "potential", grid_dims) + vtk_lbdensity = get_vtk( + path_vtk_lbdensity, "density_lb", grid_dims) + get_vtk(path_vtk_lbforce, "lbforce", grid_dims + [3]) + vtk_density = get_vtk(path_vtk_density, "density_1", grid_dims) + vtk_flux = get_vtk(path_vtk_flux, "flux_1", grid_dims + [3]) + if espressomd.has_features('EK_DEBUG'): + get_vtk(path_vtk_flux_fluc, "flux_fluc_1", grid_dims + [4]) + get_vtk(path_vtk_flux_link, "flux_link_1", grid_dims + [13]) # check VTK files against the EK grid species_density = np.zeros(grid_dims) diff --git a/testsuite/python/h5md.py b/testsuite/python/h5md.py index 47dbdf51023..f345d3ce09c 100644 --- a/testsuite/python/h5md.py +++ b/testsuite/python/h5md.py @@ -20,7 +20,7 @@ """ Testmodule for the H5MD interface. """ -import os +import pathlib import sys import unittest as ut import unittest_decorators as utx @@ -83,11 +83,12 @@ class H5mdTests(ut.TestCase): @classmethod def setUpClass(cls): cls.temp_dir = tempfile.TemporaryDirectory() - cls.temp_file = os.path.join(cls.temp_dir.name, 'test.h5') + cls.temp_path = pathlib.Path(cls.temp_dir.name) + cls.temp_file = cls.temp_path / 'test.h5' h5_units = espressomd.io.writer.h5md.UnitSystem( time='ps', mass='u', length='m', charge='e') h5 = espressomd.io.writer.h5md.H5md( - file_path=cls.temp_file, unit_system=h5_units) + file_path=str(cls.temp_file), unit_system=h5_units) h5.write() h5.write() h5.flush() @@ -116,18 +117,18 @@ def tearDownClass(cls): cls.temp_dir.cleanup() def test_opening(self): - h5 = espressomd.io.writer.h5md.H5md(file_path=self.temp_file) + h5 = espressomd.io.writer.h5md.H5md(file_path=str(self.temp_file)) h5.close() def test_appending(self): # write one frame to the file - temp_file = os.path.join(self.temp_dir.name, 'appending.h5') - h5 = espressomd.io.writer.h5md.H5md(file_path=temp_file) + temp_file = self.temp_path / 'appending.h5' + h5 = espressomd.io.writer.h5md.H5md(file_path=str(temp_file)) h5.write() h5.flush() h5.close() # append one frame to the file - h5 = espressomd.io.writer.h5md.H5md(file_path=temp_file) + h5 = espressomd.io.writer.h5md.H5md(file_path=str(temp_file)) h5.write() h5.flush() h5.close() @@ -147,46 +148,45 @@ def predicate(cur, key): def test_exceptions(self): h5md = espressomd.io.writer.h5md h5_units = h5md.UnitSystem(time='ps', mass='u', length='m', charge='e') - temp_file = os.path.join(self.temp_dir.name, 'exceptions.h5') + temp_file = self.temp_path / 'exceptions.h5' # write a non-compliant file - with open(temp_file, 'wb'): - pass + temp_file.write_bytes(b'') with self.assertRaisesRegex(RuntimeError, 'not a valid HDF5 file'): - h5md.H5md(file_path=temp_file, unit_system=h5_units) + h5md.H5md(file_path=str(temp_file), unit_system=h5_units) # cannot append to a closed file with a leftover backup file - main_file = os.path.join(self.temp_dir.name, 'main.h5') - h5 = h5md.H5md(file_path=main_file) + main_file = self.temp_path / 'main.h5' + h5 = h5md.H5md(file_path=str(main_file)) h5.write() h5.flush() h5.close() - with open(main_file + '.bak', 'wb') as backup: - with open(main_file, 'rb') as original: - backup.write(original.read()) + main_file.with_suffix(temp_file.suffix + '.bak').write_bytes(b'') with self.assertRaisesRegex(RuntimeError, 'A backup of the .h5 file exists'): - h5md.H5md(file_path=main_file) - # cannot create a new file with a leftover backup file - os.remove(main_file) + h5md.H5md(file_path=str(main_file)) + # cannot create a new file when a leftover backup file exists + main_file.unlink() with self.assertRaisesRegex(RuntimeError, 'A backup of the .h5 file exists'): - h5md.H5md(file_path=main_file) + h5md.H5md(file_path=str(main_file)) # open a file with different specifications - temp_file = os.path.join(self.temp_dir.name, 'wrong_spec.h5') - h5 = espressomd.io.writer.h5md.H5md(file_path=temp_file, fields=[]) + temp_file = self.temp_path / 'wrong_spec.h5' + h5 = espressomd.io.writer.h5md.H5md( + file_path=str(temp_file), fields=[]) h5.write() h5.flush() h5.close() with self.assertRaisesRegex(RuntimeError, "The given .h5 file does not match the specifications in 'fields'"): - h5md.H5md(file_path=temp_file, fields='all') + h5md.H5md(file_path=str(temp_file), fields='all') # open a file with invalid specifications with self.assertRaisesRegex(ValueError, "Unknown field 'lb'"): - h5md.H5md(file_path=temp_file, fields='lb') + h5md.H5md(file_path=str(temp_file), fields='lb') # check read-only parameters for key in self.h5_obj.get_params(): with self.assertRaisesRegex(RuntimeError, f"Parameter '{key}' is read-only"): setattr(self.h5_obj, key, None) def test_empty(self): - temp_file = os.path.join(self.temp_dir.name, 'empty.h5') - h5 = espressomd.io.writer.h5md.H5md(file_path=temp_file, fields=[]) + temp_file = self.temp_path / 'empty.h5' + h5 = espressomd.io.writer.h5md.H5md( + file_path=str(temp_file), fields=[]) h5.write() h5.flush() h5.close() @@ -249,7 +249,7 @@ def test_img(self): @utx.skipIfMissingFeatures("MASS") def test_mass(self): - """Test if masses have been written correct.""" + """Test if masses have been written properly.""" np.testing.assert_allclose(self.py_mass, 2.3) @utx.skipIfMissingFeatures(['ELECTROSTATICS']) @@ -291,9 +291,9 @@ def test_script(self): data = self.py_file['parameters/files'].attrs['script'].decode('utf-8') self.assertEqual(data, ref) # case #2: running an interactive Python session - temp_file = os.path.join(self.temp_dir.name, 'no_script.h5') + temp_file = self.temp_path / 'no_script.h5' sys.argv[0] = '' - h5 = espressomd.io.writer.h5md.H5md(file_path=temp_file) + h5 = espressomd.io.writer.h5md.H5md(file_path=str(temp_file)) sys.argv[0] = __file__ h5.write() h5.flush() @@ -317,9 +317,9 @@ def get_unit(path): self.assertEqual(get_unit('particles/atoms/velocity/value'), b'm ps-1') def test_getters(self): - self.assertEqual(self.h5_params['file_path'], self.temp_file) - self.assertEqual(os.path.abspath(self.h5_params['script_path']), - os.path.abspath(__file__)) + self.assertEqual(self.h5_params['file_path'], str(self.temp_file)) + self.assertEqual(pathlib.Path(self.h5_params['script_path']).resolve(), + pathlib.Path(__file__).resolve()) self.assertEqual(self.h5_params['fields'], ['all']) self.assertEqual(self.h5_params['time_unit'], 'ps') if espressomd.has_features(['ELECTROSTATICS']): diff --git a/testsuite/python/integrator_npt_stats.py b/testsuite/python/integrator_npt_stats.py index f9d15d428e2..54e2cde202f 100644 --- a/testsuite/python/integrator_npt_stats.py +++ b/testsuite/python/integrator_npt_stats.py @@ -44,7 +44,7 @@ def test_compressibility(self): system = self.system system.box_l = [5.86326165] * 3 - data = np.genfromtxt(tests_common.abspath("data/npt_lj_system.data")) + data = np.genfromtxt(tests_common.data_path("npt_lj_system.data")) p_ext = 2.0 system.part.add(pos=data[:, :3], v=data[:, 3:]) diff --git a/testsuite/python/lb_vtk.py b/testsuite/python/lb_vtk.py index 2e4e28f8088..bce6fb68ab9 100644 --- a/testsuite/python/lb_vtk.py +++ b/testsuite/python/lb_vtk.py @@ -19,7 +19,8 @@ import unittest as ut import unittest_decorators as utx -import os +import pathlib +import tempfile import numpy as np try: @@ -61,7 +62,7 @@ def set_lbf(self): def parse_vtk(self, filepath, name, shape): reader = vtk.vtkStructuredPointsReader() - reader.SetFileName(filepath) + reader.SetFileName(str(filepath)) reader.ReadAllVectorsOn() reader.ReadAllScalarsOn() reader.Update() @@ -76,136 +77,121 @@ def test_vtk(self): Check VTK files. ''' - os.makedirs('vtk_out', exist_ok=True) - filepaths = ['vtk_out/boundary.vtk', 'vtk_out/velocity.vtk', - 'vtk_out/velocity_bb.vtk'] - - # cleanup action - for filepath in filepaths: - if os.path.exists(filepath): - os.remove(filepath) - - shape = [10, 11, 12] - lbf = self.set_lbf() - self.system.integrator.run(100) - - # write VTK files - with self.assertRaises(RuntimeError): - lbf.write_vtk_velocity('non_existent_folder/file') - with self.assertRaises(RuntimeError): - lbf.write_vtk_boundary('non_existent_folder/file') - lbf.write_vtk_boundary('vtk_out/boundary.vtk') - lbf.write_vtk_velocity('vtk_out/velocity.vtk') - with self.assertRaises(ValueError): - lbf.write_vtk_velocity('vtk_out/delme', 3 * [0], None) - with self.assertRaises(ValueError): - lbf.write_vtk_velocity('vtk_out/delme', None, 3 * [0]) - with self.assertRaises(RuntimeError): - lbf.write_vtk_velocity('vtk_out/delme', [-2, 1, 1], 3 * [1]) - with self.assertRaises(RuntimeError): - lbf.write_vtk_velocity('vtk_out/delme', 3 * [0], [1, 2, 16]) - with self.assertRaises(ValueError): - lbf.write_vtk_velocity('vtk_out/delme', [1, 1], 3 * [1]) - with self.assertRaises(ValueError): - lbf.write_vtk_velocity('vtk_out/delme', 3 * [1], np.array([2, 3])) - bb1, bb2 = ([1, 2, 3], [9, 10, 11]) - lbf.write_vtk_velocity('vtk_out/velocity_bb.vtk', bb1, bb2) - - # check VTK files exist - for filepath in filepaths: - self.assertTrue( - os.path.exists(filepath), - f'VTK file "{filepath}" not written to disk') - - # check VTK values match node values - node_velocity = np.zeros(shape + [3]) - node_boundary = np.zeros(shape, dtype=int) - for i in range(shape[0]): - for j in range(shape[1]): - for k in range(shape[2]): - node = lbf[i, j, k] - node_velocity[i, j, k] = node.velocity - node_boundary[i, j, k] = node.boundary - node_velocity_bb = node_velocity[bb1[0]:bb2[0], - bb1[1]:bb2[1], - bb1[2]:bb2[2]] - - vtk_velocity = self.parse_vtk('vtk_out/velocity.vtk', 'velocity', - node_velocity.shape) - np.testing.assert_allclose(vtk_velocity, node_velocity, atol=5e-7) - - vtk_velocity_bb = self.parse_vtk('vtk_out/velocity_bb.vtk', 'velocity', - node_velocity_bb.shape) - np.testing.assert_allclose( - vtk_velocity_bb, node_velocity_bb, atol=5e-7) - - vtk_boundary = self.parse_vtk( - 'vtk_out/boundary.vtk', 'boundary', shape) - np.testing.assert_equal(vtk_boundary, node_boundary.astype(int)) - if self.system.lbboundaries is None: - np.testing.assert_equal(np.sum(node_boundary), 0.) + with tempfile.TemporaryDirectory() as tmp_directory: + path_vtk_root = pathlib.Path(tmp_directory) + path_vtk_boundary = path_vtk_root / 'boundary.vtk' + path_vtk_velocity = path_vtk_root / 'velocity.vtk' + path_vtk_velocity_bb = path_vtk_root / 'velocity_bb.vtk' + path_vtk_skip = path_vtk_root / 'skip.vtk' + path_vtk_invalid = path_vtk_root / 'non_existent_folder' / 'file' + + shape = [10, 11, 12] + lbf = self.set_lbf() + self.system.integrator.run(100) + + # write VTK files + with self.assertRaises(RuntimeError): + lbf.write_vtk_velocity(str(path_vtk_invalid)) + with self.assertRaises(RuntimeError): + lbf.write_vtk_boundary(str(path_vtk_invalid)) + lbf.write_vtk_boundary(str(path_vtk_boundary)) + lbf.write_vtk_velocity(str(path_vtk_velocity)) + with self.assertRaises(ValueError): + lbf.write_vtk_velocity(str(path_vtk_skip), 3 * [0], None) + with self.assertRaises(ValueError): + lbf.write_vtk_velocity(str(path_vtk_skip), None, 3 * [0]) + with self.assertRaises(RuntimeError): + lbf.write_vtk_velocity(str(path_vtk_skip), [-2, 1, 1], 3 * [1]) + with self.assertRaises(RuntimeError): + lbf.write_vtk_velocity(str(path_vtk_skip), 3 * [0], [1, 2, 16]) + with self.assertRaises(ValueError): + lbf.write_vtk_velocity(str(path_vtk_skip), [1, 1], 3 * [1]) + with self.assertRaises(ValueError): + lbf.write_vtk_velocity( + str(path_vtk_skip), 3 * [1], np.array([2, 3])) + bb1, bb2 = ([1, 2, 3], [9, 10, 11]) + lbf.write_vtk_velocity(str(path_vtk_velocity_bb), bb1, bb2) + + # check VTK values match node values + node_velocity = np.zeros(shape + [3]) + node_boundary = np.zeros(shape, dtype=int) + for i in range(shape[0]): + for j in range(shape[1]): + for k in range(shape[2]): + node = lbf[i, j, k] + node_velocity[i, j, k] = node.velocity + node_boundary[i, j, k] = node.boundary + node_velocity_bb = node_velocity[bb1[0]:bb2[0], + bb1[1]:bb2[1], + bb1[2]:bb2[2]] + + vtk_velocity = self.parse_vtk(path_vtk_velocity, 'velocity', + node_velocity.shape) + np.testing.assert_allclose(vtk_velocity, node_velocity, atol=5e-7) + + vtk_velocity_bb = self.parse_vtk(path_vtk_velocity_bb, 'velocity', + node_velocity_bb.shape) + np.testing.assert_allclose( + vtk_velocity_bb, node_velocity_bb, atol=5e-7) + + vtk_boundary = self.parse_vtk(path_vtk_boundary, 'boundary', shape) + np.testing.assert_equal(vtk_boundary, node_boundary.astype(int)) + if self.system.lbboundaries is None: + np.testing.assert_equal(np.sum(node_boundary), 0.) def test_print(self): ''' Check data files. ''' - os.makedirs('vtk_out', exist_ok=True) - filepaths = ['vtk_out/boundary.dat', 'vtk_out/velocity.dat'] - - # cleanup action - for filepath in filepaths: - if os.path.exists(filepath): - os.remove(filepath) - - shape = [10, 11, 12] - lbf = self.set_lbf() - self.system.integrator.run(100) - - # write data files - with self.assertRaises(RuntimeError): - lbf.write_velocity('non_existent_folder/file') - with self.assertRaises(RuntimeError): - lbf.write_boundary('non_existent_folder/file') - lbf.write_boundary('vtk_out/boundary.dat') - lbf.write_velocity('vtk_out/velocity.dat') - - # check data files exist - for filepath in filepaths: - self.assertTrue( - os.path.exists(filepath), - f'data file "{filepath}" not written to disk') - - # check data values match node values - node_velocity = np.zeros(shape + [3]) - node_boundary = np.zeros(shape, dtype=int) - for i in range(shape[0]): - for j in range(shape[1]): - for k in range(shape[2]): - node = lbf[i, j, k] - node_velocity[i, j, k] = node.velocity - node_boundary[i, j, k] = node.boundary - - ref_coord = np.array([ - np.tile(np.arange(shape[0]), shape[1] * shape[2]), - np.tile(np.repeat(np.arange(shape[1]), shape[0]), shape[2]), - np.repeat(np.arange(shape[2]), shape[0] * shape[1])]).T - - dat_velocity = np.loadtxt('vtk_out/velocity.dat') - dat_coord = (dat_velocity[:, 0:3] - 0.5).astype(int) - np.testing.assert_equal(dat_coord, ref_coord) - dat_vel = dat_velocity[:, 3:] - ref_vel = np.swapaxes(node_velocity, 0, 2).reshape((-1, 3)) - np.testing.assert_allclose(dat_vel, ref_vel, atol=5e-7) - - dat_boundary = np.loadtxt('vtk_out/boundary.dat') - dat_coord = (dat_boundary[:, 0:3] - 0.5).astype(int) - np.testing.assert_equal(dat_coord, ref_coord) - dat_bound = dat_boundary[:, 3].astype(int) - ref_bound = np.swapaxes(node_boundary, 0, 2).reshape(-1) - if isinstance(lbf, espressomd.lb.LBFluid): - ref_bound = (ref_bound != 0).astype(int) - np.testing.assert_equal(dat_bound, ref_bound) + with tempfile.TemporaryDirectory() as tmp_directory: + path_dat_root = pathlib.Path(tmp_directory) + path_dat_boundary = path_dat_root / 'boundary.vtk' + path_dat_velocity = path_dat_root / 'velocity.vtk' + path_dat_invalid = path_dat_root / 'non_existent_folder' / 'file' + + shape = [10, 11, 12] + lbf = self.set_lbf() + self.system.integrator.run(100) + + # write data files + with self.assertRaises(RuntimeError): + lbf.write_velocity(str(path_dat_invalid)) + with self.assertRaises(RuntimeError): + lbf.write_boundary(str(path_dat_invalid)) + lbf.write_boundary(str(path_dat_boundary)) + lbf.write_velocity(str(path_dat_velocity)) + + # check data values match node values + node_velocity = np.zeros(shape + [3]) + node_boundary = np.zeros(shape, dtype=int) + for i in range(shape[0]): + for j in range(shape[1]): + for k in range(shape[2]): + node = lbf[i, j, k] + node_velocity[i, j, k] = node.velocity + node_boundary[i, j, k] = node.boundary + + ref_coord = np.array([ + np.tile(np.arange(shape[0]), shape[1] * shape[2]), + np.tile(np.repeat(np.arange(shape[1]), shape[0]), shape[2]), + np.repeat(np.arange(shape[2]), shape[0] * shape[1])]).T + + dat_velocity = np.loadtxt(path_dat_velocity) + dat_coord = (dat_velocity[:, 0:3] - 0.5).astype(int) + np.testing.assert_equal(dat_coord, ref_coord) + dat_vel = dat_velocity[:, 3:] + ref_vel = np.swapaxes(node_velocity, 0, 2).reshape((-1, 3)) + np.testing.assert_allclose(dat_vel, ref_vel, atol=5e-7) + + dat_boundary = np.loadtxt(path_dat_boundary) + dat_coord = (dat_boundary[:, 0:3] - 0.5).astype(int) + np.testing.assert_equal(dat_coord, ref_coord) + dat_bound = dat_boundary[:, 3].astype(int) + ref_bound = np.swapaxes(node_boundary, 0, 2).reshape(-1) + if isinstance(lbf, espressomd.lb.LBFluid): + ref_bound = (ref_bound != 0).astype(int) + np.testing.assert_equal(dat_bound, ref_bound) @skipIfMissingPythonPackage diff --git a/testsuite/python/lj.py b/testsuite/python/lj.py index aaa6ad46e52..1fdd104beda 100644 --- a/testsuite/python/lj.py +++ b/testsuite/python/lj.py @@ -26,7 +26,7 @@ @utx.skipIfMissingFeatures(["LENNARD_JONES"]) class LennardJonesTest(ut.TestCase): system = espressomd.System(box_l=[1.0, 1.0, 1.0]) - data = np.loadtxt(tests_common.abspath('data/lj_system.dat')) + data = np.loadtxt(tests_common.data_path('lj_system.dat')) pos = data[:, 1:4] forces = data[:, 4:7] diff --git a/testsuite/python/mmm1d.py b/testsuite/python/mmm1d.py index a63c56afa09..921ecdedbe1 100644 --- a/testsuite/python/mmm1d.py +++ b/testsuite/python/mmm1d.py @@ -35,7 +35,7 @@ class ElectrostaticInteractionsTests: system.cell_system.set_n_square() system.thermostat.set_langevin(kT=0, gamma=1, seed=8) - data = np.loadtxt(tests_common.abspath("data/mmm1d_data.txt")) + data = np.loadtxt(tests_common.data_path("mmm1d_data.txt")) p_pos = data[:, 1:4] p_q = data[:, 4] forces_target = data[:, 5:8] diff --git a/testsuite/python/npt_thermostat.py b/testsuite/python/npt_thermostat.py index 7a76573c661..83e27d3fece 100644 --- a/testsuite/python/npt_thermostat.py +++ b/testsuite/python/npt_thermostat.py @@ -117,7 +117,7 @@ def reset_particle_and_box(): def test_02__direction(self): """Test for NpT constrained in one direction.""" - data = np.genfromtxt(tests_common.abspath("data/npt_lj_system.data")) + data = np.genfromtxt(tests_common.data_path("npt_lj_system.data")) ref_box_l = 1.01 * np.max(data[:, 0:3]) system = self.system diff --git a/testsuite/python/oif_volume_conservation.py b/testsuite/python/oif_volume_conservation.py index 7169b718280..5a74175235a 100644 --- a/testsuite/python/oif_volume_conservation.py +++ b/testsuite/python/oif_volume_conservation.py @@ -37,8 +37,9 @@ def test(self): # creating the template for OIF object cell_type = oif.OifCellType( - nodes_file=tests_common.abspath("data/sphere393nodes.dat"), - triangles_file=tests_common.abspath("data/sphere393triangles.dat"), + nodes_file=str(tests_common.data_path("sphere393nodes.dat")), + triangles_file=str( + tests_common.data_path("sphere393triangles.dat")), system=system, ks=1.0, kb=1.0, kal=1.0, kag=0.1, kv=0.1, check_orientation=False, resize=(3.0, 3.0, 3.0)) diff --git a/testsuite/python/save_checkpoint.py b/testsuite/python/save_checkpoint.py index 9c762382b57..57ac7e6a0b6 100644 --- a/testsuite/python/save_checkpoint.py +++ b/testsuite/python/save_checkpoint.py @@ -17,7 +17,7 @@ import unittest as ut import unittest_generator as utg import numpy as np -import os +import pathlib import espressomd import espressomd.checkpointing @@ -52,10 +52,11 @@ # create checkpoint folder checkpoint = espressomd.checkpointing.Checkpoint( **config.get_checkpoint_params()) +path_cpt_root = pathlib.Path(checkpoint.checkpoint_dir) # cleanup old checkpoint files -for filepath in os.listdir(checkpoint.checkpoint_dir): - os.remove(os.path.join(checkpoint.checkpoint_dir, filepath)) +for filepath in path_cpt_root.iterdir(): + filepath.unlink(missing_ok=True) n_nodes = system.cell_system.get_state()["n_nodes"] @@ -245,7 +246,7 @@ h5_units = espressomd.io.writer.h5md.UnitSystem( time="ps", mass="u", length="m", charge="e") h5 = espressomd.io.writer.h5md.H5md( - file_path=os.path.join(checkpoint.checkpoint_dir, "test.h5"), + file_path=str(path_cpt_root / "test.h5"), unit_system=h5_units) h5.write() h5.flush() @@ -328,8 +329,8 @@ for k in range(nz): lbf[i, j, k].population = grid_3D[i, j, k] * np.arange(1, 20) # save LB checkpoint file - lbf_cpt_path = checkpoint.checkpoint_dir + "/lb.cpt" - lbf.save_checkpoint(lbf_cpt_path, lbf_cpt_mode) + lbf_cpt_path = path_cpt_root / "lb.cpt" + lbf.save_checkpoint(str(lbf_cpt_path), lbf_cpt_mode) # save checkpoint file checkpoint.save(0) @@ -341,15 +342,15 @@ def test_checkpointing(self): ''' Check for the presence of the checkpoint files. ''' - self.assertTrue(os.path.isdir(checkpoint.checkpoint_dir), + self.assertTrue(path_cpt_root.is_dir(), "checkpoint directory not created") - checkpoint_filepath = checkpoint.checkpoint_dir + "/0.checkpoint" - self.assertTrue(os.path.isfile(checkpoint_filepath), + checkpoint_filepath = path_cpt_root / "0.checkpoint" + self.assertTrue(checkpoint_filepath.is_file(), "checkpoint file not created") if lbf_actor: - self.assertTrue(os.path.isfile(lbf_cpt_path), + self.assertTrue(lbf_cpt_path.is_file(), "LB checkpoint file not created") @ut.skipIf(lbf_actor is None, "Skipping test due to missing mode.") @@ -361,25 +362,23 @@ def test_lb_checkpointing_exceptions(self): # check exception mechanism with self.assertRaisesRegex(RuntimeError, 'could not open file'): - dirname, filename = os.path.split(lbf_cpt_path) - invalid_path = os.path.join(dirname, 'unknown_dir', filename) - lbf.save_checkpoint(invalid_path, lbf_cpt_mode) + invalid_path = lbf_cpt_path.parent / "unknown_dir" / "lb.cpt" + lbf.save_checkpoint(str(invalid_path), lbf_cpt_mode) system.actors.remove(lbf) with self.assertRaisesRegex(RuntimeError, 'one needs to have already initialized the LB fluid'): - lbf.load_checkpoint(lbf_cpt_path, lbf_cpt_mode) + lbf.load_checkpoint(str(lbf_cpt_path), lbf_cpt_mode) # read the valid LB checkpoint file - with open(lbf_cpt_path, "rb") as f: - lbf_cpt_str = f.read() - cpt_path = checkpoint.checkpoint_dir + "/lb{}.cpt" + lbf_cpt_data = lbf_cpt_path.read_bytes() + cpt_path = str(path_cpt_root / "lb") + "{}.cpt" # write checkpoint file with missing data with open(cpt_path.format("-missing-data"), "wb") as f: - f.write(lbf_cpt_str[:len(lbf_cpt_str) // 2]) + f.write(lbf_cpt_data[:len(lbf_cpt_data) // 2]) # write checkpoint file with extra data with open(cpt_path.format("-extra-data"), "wb") as f: - f.write(lbf_cpt_str + lbf_cpt_str[-8:]) + f.write(lbf_cpt_data + lbf_cpt_data[-8:]) if lbf_cpt_mode == 0: - boxsize, data = lbf_cpt_str.split(b"\n", 1) + boxsize, data = lbf_cpt_data.split(b"\n", 1) # write checkpoint file with incorrectly formatted data with open(cpt_path.format("-wrong-format"), "wb") as f: f.write(boxsize + b"\ntext string\n" + data) diff --git a/testsuite/python/scafacos_dipoles_1d_2d.py b/testsuite/python/scafacos_dipoles_1d_2d.py index 9d1cde69274..b32b6a30aa7 100644 --- a/testsuite/python/scafacos_dipoles_1d_2d.py +++ b/testsuite/python/scafacos_dipoles_1d_2d.py @@ -62,19 +62,19 @@ def test_scafacos(self): # Read reference data if dim == 2: - file_prefix = "data/mdlc" + file_prefix = "mdlc" s.periodicity = [1, 1, 0] else: s.periodicity = [1, 0, 0] - file_prefix = "data/scafacos_dipoles_1d" + file_prefix = "scafacos_dipoles_1d" - ref_E_path = tests_common.abspath( - file_prefix + "_reference_data_energy.dat") + ref_E_path = tests_common.data_path( + f"{file_prefix}_reference_data_energy.dat") ref_E = float(np.genfromtxt(ref_E_path)) # Particles - data = np.genfromtxt(tests_common.abspath( - file_prefix + "_reference_data_forces_torques.dat")) + data = np.genfromtxt(tests_common.data_path( + f"{file_prefix}_reference_data_forces_torques.dat")) s.part.add(pos=data[:, 1:4], dip=data[:, 4:7]) s.part.all().rotation = 3 * [True] diff --git a/testsuite/python/sigint.py b/testsuite/python/sigint.py index 1627fba0633..17fc6eed6fa 100644 --- a/testsuite/python/sigint.py +++ b/testsuite/python/sigint.py @@ -21,13 +21,13 @@ import subprocess import time import sys -import os +import pathlib class SigintTest(ut.TestCase): def setUp(self): - script = os.path.join(os.path.dirname(__file__), 'sigint_child.py') + script = str(pathlib.Path(__file__).parent / 'sigint_child.py') self.process = subprocess.Popen([sys.executable, script]) def test_signal_handling(self): diff --git a/testsuite/python/stokesian_dynamics.py b/testsuite/python/stokesian_dynamics.py index 0712455d351..deb95245844 100644 --- a/testsuite/python/stokesian_dynamics.py +++ b/testsuite/python/stokesian_dynamics.py @@ -35,7 +35,7 @@ class StokesianDynamicsTest(ut.TestCase): # Digitized reference data of Figure 5b from # Durlofsky et al., J. Fluid Mech. 180, 21 (1987) # https://doi.org/10.1017/S002211208700171X - data = np.loadtxt(tests_common.abspath('data/dancing.txt')) + data = np.loadtxt(tests_common.data_path('dancing.txt')) def setUp(self): self.system.box_l = [10] * 3 diff --git a/testsuite/python/test_checkpoint.py b/testsuite/python/test_checkpoint.py index 5012ff24da3..d6f99bc7400 100644 --- a/testsuite/python/test_checkpoint.py +++ b/testsuite/python/test_checkpoint.py @@ -20,6 +20,7 @@ import unittest_decorators as utx import unittest_generator as utg import numpy as np +import pathlib import espressomd import espressomd.checkpointing @@ -32,7 +33,6 @@ import espressomd.shapes import espressomd.constraints -import os try: import h5py # h5py has to be imported *after* espressomd (MPI) except ImportError: @@ -50,6 +50,7 @@ class CheckpointTest(ut.TestCase): checkpoint = espressomd.checkpointing.Checkpoint( **config.get_checkpoint_params()) checkpoint.load(0) + path_cpt_root = pathlib.Path(checkpoint.checkpoint_dir) n_nodes = system.cell_system.get_state()["n_nodes"] @classmethod @@ -81,7 +82,8 @@ def test_lb_fluid(self): lbf = self.get_active_actor_of_type( espressomd.lb.HydrodynamicInteraction) cpt_mode = 0 if 'LB.ASCII' in modes else 1 - cpt_path = self.checkpoint.checkpoint_dir + "/lb{}.cpt" + cpt_root = pathlib.Path(self.checkpoint.checkpoint_dir) + cpt_path = str(cpt_root / "lb") + "{}.cpt" # check exception mechanism with corrupted LB checkpoint files with self.assertRaisesRegex(RuntimeError, 'EOF found'): @@ -419,12 +421,12 @@ def test_correlator(self): "Skipping test due to missing python module 'h5py'.") def test_h5md(self): # check attributes - file_path = os.path.join(self.checkpoint.checkpoint_dir, "test.h5") - script_path = os.path.join( - os.path.dirname(__file__), "save_checkpoint.py") + file_path = self.path_cpt_root / "test.h5" + script_path = pathlib.Path( + __file__).resolve().parent / "save_checkpoint.py" self.assertEqual(h5.fields, ['all']) - self.assertEqual(h5.script_path, script_path) - self.assertEqual(h5.file_path, file_path) + self.assertEqual(h5.script_path, str(script_path)) + self.assertEqual(h5.file_path, str(file_path)) # write new frame h5.write() diff --git a/testsuite/python/tests_common.py b/testsuite/python/tests_common.py index f57327f01ca..d8a7b80437c 100644 --- a/testsuite/python/tests_common.py +++ b/testsuite/python/tests_common.py @@ -14,7 +14,7 @@ # # You should have received a copy of the GNU General Public License # along with this program. If not, see . -import os +import pathlib import numpy as np @@ -128,8 +128,11 @@ def verify_lj_forces(system, tolerance, ids_to_skip=()): err_msg=f"LJ force verification failed on particle {p.id}.") -def abspath(path): - return os.path.join(os.path.dirname(os.path.abspath(__file__)), path) +def data_path(filename): + """ + Resolve abosulte path to a resource. + """ + return pathlib.Path(__file__).resolve().parent / "data" / filename def transform_pos_from_cartesian_to_polar_coordinates(pos): diff --git a/testsuite/python/unittest_generator.py b/testsuite/python/unittest_generator.py index 6e16257d772..b399f8eac28 100644 --- a/testsuite/python/unittest_generator.py +++ b/testsuite/python/unittest_generator.py @@ -17,9 +17,9 @@ # along with this program. If not, see . # -import os import sys import inspect +import pathlib class TestGenerator: @@ -141,4 +141,4 @@ def get_checkpoint_params(self): Generate parameters to instantiate an ESPResSo checkpoint file. """ return {"checkpoint_id": f"checkpoint_{self.test_idx}", - "checkpoint_path": os.path.dirname(__file__)} + "checkpoint_path": str(pathlib.Path(__file__).parent)} diff --git a/testsuite/scripts/importlib_wrapper.py b/testsuite/scripts/importlib_wrapper.py index d5576798a44..67e3d9f9186 100644 --- a/testsuite/scripts/importlib_wrapper.py +++ b/testsuite/scripts/importlib_wrapper.py @@ -23,6 +23,7 @@ import tokenize import unittest import importlib +import pathlib import espressomd from unittest.mock import MagicMock @@ -40,7 +41,7 @@ def configure_and_import(filepath, gpu=False, substitutions=lambda x: x, cmd_arguments=None, - script_suffix=None, + script_suffix="", move_to_script_dir=True, mock_visualizers=True, **parameters): @@ -79,6 +80,7 @@ def configure_and_import(filepath, global variables to replace """ + filepath = pathlib.Path(filepath).resolve() if skip_future_imports: module = MagicMock() skipIfMissingImport = skip_future_imports_dependency(filepath) @@ -88,10 +90,8 @@ def configure_and_import(filepath, skipIfMissingGPU = unittest.skip("gpu not available, skipping test!") module = MagicMock() return module, skipIfMissingGPU - filepath = os.path.abspath(filepath) # load original script - with open(filepath, "r") as f: - code = f.read() + code = filepath.read_text() # custom substitutions code = substitutions(code) assert code.strip() @@ -106,23 +106,17 @@ def configure_and_import(filepath, if mock_visualizers: code = mock_es_visualization(code) # save changes to a new file - if script_suffix: - if script_suffix[0] != "_": - script_suffix = "_" + script_suffix - else: - script_suffix = "" - script_suffix += "_processed.py" - output_filepath = os.path.splitext(filepath)[0] + script_suffix - assert os.path.isfile(output_filepath) is False, \ + output_filepath = filepath.parent / \ + f"{filepath.stem}_{script_suffix}_processed.py" + assert not output_filepath.exists(), \ f"File {output_filepath} already processed, cannot overwrite" - with open(output_filepath, "wb") as f: - f.write(code.encode(encoding="utf-8")) + output_filepath.write_bytes(code.encode(encoding="utf-8")) # import - dirname, basename = os.path.split(output_filepath) + dirname = output_filepath.parent if move_to_script_dir: os.chdir(dirname) - sys.path.insert(0, dirname) - module_name = os.path.splitext(basename)[0] + sys.path.insert(0, str(dirname)) + module_name = output_filepath.stem try: module = importlib.import_module(module_name) except espressomd.FeaturesError as err: @@ -162,7 +156,7 @@ def set_cmd(code, filepath, cmd_arguments): """ assert isinstance(cmd_arguments, (list, tuple)) sys_argv = list(map(str, cmd_arguments)) - sys_argv.insert(0, os.path.basename(filepath)) + sys_argv.insert(0, filepath.name) visitor = GetSysArgparseImports() visitor.visit(ast.parse(protect_ipython_magics(code))) assert visitor.linenos, "module sys (or argparse) is not imported" @@ -636,7 +630,7 @@ def skip_future_imports_dependency(filepath): """ global skip_future_imports if not skip_future_imports: - module_name = os.path.splitext(os.path.basename(filepath))[0] + module_name = filepath.stem assert module_name != "" skip_future_imports = module_name return unittest.skip( diff --git a/testsuite/scripts/samples/test_ekboundaries.py b/testsuite/scripts/samples/test_ekboundaries.py index 0b2eecf914b..ab10be10f2e 100644 --- a/testsuite/scripts/samples/test_ekboundaries.py +++ b/testsuite/scripts/samples/test_ekboundaries.py @@ -17,7 +17,7 @@ import unittest as ut import importlib_wrapper -import os +import pathlib sample, skipIfMissingFeatures = importlib_wrapper.configure_and_import( "@SAMPLES_DIR@/ekboundaries.py", gpu=True, n_int_cycles=50) @@ -28,13 +28,12 @@ class Sample(ut.TestCase): system = sample.system def test_file_generation(self): - # test .checkpoint files exist + # test .vtk files exist + path_vtk_root = pathlib.Path("ek") for basename in ["pos_dens_0.vtk", "pos_flux_0.vtk", "ekv_0.vtk", "neg_dens_0.vtk", "neg_flux_0.vtk", "ekb_0.vtk"]: - filepath = os.path.join("ek", basename) - self.assertTrue( - os.path.isfile(filepath), - filepath + " not created") + filepath = path_vtk_root / basename + self.assertTrue(filepath.is_file(), f"File {filepath} not created") if __name__ == "__main__": diff --git a/testsuite/scripts/samples/test_immersed_boundary.py b/testsuite/scripts/samples/test_immersed_boundary.py index e2b1aae57fe..7c32d17b294 100644 --- a/testsuite/scripts/samples/test_immersed_boundary.py +++ b/testsuite/scripts/samples/test_immersed_boundary.py @@ -17,7 +17,7 @@ import unittest as ut import importlib_wrapper -import os +import pathlib sample, skipIfMissingFeatures = importlib_wrapper.configure_and_import( "@SAMPLES_DIR@/immersed_boundary/sampleImmersedBoundary.py", @@ -31,10 +31,8 @@ class Sample(ut.TestCase): def test_file_generation(self): # test .vtk files exist for i in range(sample.numSteps + 1): - filepath = os.path.join("outputVolParaCUDA", "cell_%i.vtk" % i) - self.assertTrue( - os.path.isfile(filepath), - filepath + " not created") + filepath = pathlib.Path("outputVolParaCUDA") / f"cell_{i}.vtk" + self.assertTrue(filepath.is_file(), f"File {filepath} not created") if __name__ == "__main__": diff --git a/testsuite/scripts/samples/test_object_in_fluid__motivation.py b/testsuite/scripts/samples/test_object_in_fluid__motivation.py index 558d57941d5..191346001e3 100644 --- a/testsuite/scripts/samples/test_object_in_fluid__motivation.py +++ b/testsuite/scripts/samples/test_object_in_fluid__motivation.py @@ -18,6 +18,7 @@ import unittest as ut import importlib_wrapper import os +import pathlib os.chdir("@SAMPLES_DIR@/object_in_fluid") sample, skipIfMissingFeatures = importlib_wrapper.configure_and_import( @@ -39,13 +40,13 @@ def test_file_generation(self): "wallFront.vtk"] for i in [0, 1]: for j in range(sample.maxCycle): - basenames.append("cell{}_{}.vtk".format(i, j)) + basenames.append(f"cell{i}_{j}.vtk") + # test .vtk files exist + path_vtk_root = pathlib.Path("output") for name in basenames: - filepath = os.path.join("output", "sim0", name) - self.assertTrue( - os.path.isfile(filepath), - filepath + " not created") + filepath = path_vtk_root / "sim0" / name + self.assertTrue(filepath.is_file(), f"File {filepath} not created") if __name__ == "__main__": diff --git a/testsuite/scripts/samples/test_save_checkpoint.py b/testsuite/scripts/samples/test_save_checkpoint.py index 55b61ac5405..b06e265ee41 100644 --- a/testsuite/scripts/samples/test_save_checkpoint.py +++ b/testsuite/scripts/samples/test_save_checkpoint.py @@ -17,7 +17,7 @@ import unittest as ut import importlib_wrapper -import os +import pathlib sample, skipIfMissingFeatures = importlib_wrapper.configure_and_import( "@SAMPLES_DIR@/save_checkpoint.py") @@ -29,8 +29,8 @@ class Sample(ut.TestCase): def test_file_generation(self): # test .checkpoint files exist - filepath = os.path.join("mycheckpoint", "0.checkpoint") - self.assertTrue(os.path.isfile(filepath), filepath + " not created") + filepath = pathlib.Path("mycheckpoint") / "0.checkpoint" + self.assertTrue(filepath.exists(), f"File {filepath} not created") if __name__ == "__main__": diff --git a/testsuite/scripts/test_importlib_wrapper.py b/testsuite/scripts/test_importlib_wrapper.py index e21904ecb6f..e42fb4b4e44 100644 --- a/testsuite/scripts/test_importlib_wrapper.py +++ b/testsuite/scripts/test_importlib_wrapper.py @@ -19,6 +19,7 @@ import importlib_wrapper as iw import sys import ast +import pathlib class importlib_wrapper(ut.TestCase): @@ -49,20 +50,21 @@ def test_substitute_variable_values(self): def test_set_cmd(self): original_sys_argv = list(sys.argv) sys.argv = [0, "test"] + path = pathlib.Path("a.py") # test substitutions str_inp = "import sys\nimport argparse" - str_exp = "import sys;sys.argv = ['a.py', '1', '2'];" + str_inp - str_out, sys_argv = iw.set_cmd(str_inp, "a.py", (1, 2)) + str_exp = f"import sys;sys.argv = ['{path.name}', '1', '2'];" + str_inp + str_out, sys_argv = iw.set_cmd(str_inp, path, (1, 2)) self.assertEqual(str_out, str_exp) self.assertEqual(sys_argv, [0, "test"]) str_inp = "import argparse" - str_exp = "import sys;sys.argv = ['a.py', '1', '2'];" + str_inp - str_out, sys_argv = iw.set_cmd(str_inp, "a.py", ["1", 2]) + str_exp = f"import sys;sys.argv = ['{path.name}', '1', '2'];" + str_inp + str_out, sys_argv = iw.set_cmd(str_inp, path, ["1", 2]) self.assertEqual(str_out, str_exp) self.assertEqual(sys_argv, [0, "test"]) # test exceptions str_inp = "import re" - self.assertRaises(AssertionError, iw.set_cmd, str_inp, "a.py", (1, 2)) + self.assertRaises(AssertionError, iw.set_cmd, str_inp, path, (1, 2)) # restore sys.argv sys.argv = original_sys_argv diff --git a/testsuite/scripts/tutorials/test_convert.py b/testsuite/scripts/tutorials/test_convert.py index 0f24519cdc2..b35aab8482f 100644 --- a/testsuite/scripts/tutorials/test_convert.py +++ b/testsuite/scripts/tutorials/test_convert.py @@ -16,10 +16,10 @@ # along with this program. If not, see . import unittest as ut -import os import sys import nbformat import traceback +import pathlib sys.path.insert(0, '@CMAKE_BINARY_DIR@/doc/tutorials') import convert @@ -96,17 +96,16 @@ def run_command(self, cmd, output=None): traceback.print_exc() self.fail(error_msg) if output is not None: - self.assertTrue(os.path.isfile(output), f"{output} not created") + self.assertTrue(output.exists(), f"File {output} not created") def test_html_wrapper(self): - f_input = '@CMAKE_CURRENT_BINARY_DIR@/test_convert_notebook.ipynb' - f_output = '@CMAKE_CURRENT_BINARY_DIR@/test_convert_notebook.run.ipynb' - f_script = '@CMAKE_CURRENT_BINARY_DIR@/test_convert_script.py' + root = pathlib.Path("@CMAKE_CURRENT_BINARY_DIR@") + f_input = root / "test_convert_notebook.ipynb" + f_output = root / "test_convert_notebook.run.ipynb" + f_script = root / "test_convert_script.py" # setup - if os.path.isfile(f_output): - os.remove(f_output) - with open(f_script, 'w') as f: - f.write('global_var = 5') + f_output.unlink(missing_ok=True) + f_script.write_text('global_var = 5') with open(f_input, 'w', encoding='utf-8') as f: nb = nbformat.v4.new_notebook(metadata=self.nb_metadata) cell_md = nbformat.v4.new_markdown_cell(source=self.cell_md_src) @@ -116,9 +115,9 @@ def test_html_wrapper(self): nbformat.write(nb, f) # run command and check for errors cmd = ['ci', - '--input', f_input, - '--output', f_output, - '--scripts', f_script, + '--input', str(f_input), + '--output', str(f_output), + '--scripts', str(f_script), '--substitutions', 'global_var=20', '--execute'] self.run_command(cmd, f_output) @@ -155,11 +154,11 @@ def test_html_wrapper(self): self.assertEqual(nb_output['cells'][4]['source'], 'global_var = 20') def test_exercise2_plugin(self): - f_input = '@CMAKE_CURRENT_BINARY_DIR@/test_convert_exercise2.ipynb' - f_output = '@CMAKE_CURRENT_BINARY_DIR@/test_convert_exercise2.run.ipynb' + root = pathlib.Path("@CMAKE_CURRENT_BINARY_DIR@") + f_input = root / "test_convert_exercise2.ipynb" + f_output = root / "test_convert_exercise2.run.ipynb" # setup - if os.path.isfile(f_output): - os.remove(f_output) + f_output.unlink(missing_ok=True) with open(f_input, 'w', encoding='utf-8') as f: nb = nbformat.v4.new_notebook(metadata=self.nb_metadata) # question with 2 answers and an empty cell @@ -191,8 +190,8 @@ def test_exercise2_plugin(self): nbformat.write(nb, f) # run command and check for errors cmd = ['ci', - '--input', f_input, - '--output', f_output, + '--input', str(f_input), + '--output', str(f_output), '--substitutions', 'global_var=20', '--exercise2', '--remove-empty-cells'] self.run_command(cmd, f_output) @@ -226,7 +225,8 @@ def test_exercise2_plugin(self): self.assertEqual(next(cells, 'EOF'), 'EOF') def test_exercise2_conversion(self): - f_input = '@CMAKE_CURRENT_BINARY_DIR@/test_convert_exercise2_conversion.ipynb' + root = pathlib.Path("@CMAKE_CURRENT_BINARY_DIR@") + f_input = root / "test_convert_exercise2_conversion.ipynb" # setup with open(f_input, 'w', encoding='utf-8') as f: nb = nbformat.v4.new_notebook(metadata=self.nb_metadata) @@ -242,7 +242,7 @@ def test_exercise2_conversion(self): nb['cells'].append(cell_md) nbformat.write(nb, f) # run command and check for errors - cmd = ['exercise2', '--to-py', f_input] + cmd = ['exercise2', '--to-py', str(f_input)] self.run_command(cmd, f_input) # read processed notebook with open(f_input, encoding='utf-8') as f: @@ -259,7 +259,7 @@ def test_exercise2_conversion(self): self.assertEqual(cell['metadata']['key'], 'value') self.assertEqual(next(cells, 'EOF'), 'EOF') # run command and check for errors - cmd = ['exercise2', '--to-md', f_input] + cmd = ['exercise2', '--to-md', str(f_input)] self.run_command(cmd, f_input) # read processed notebook with open(f_input, encoding='utf-8') as f: @@ -278,7 +278,8 @@ def test_exercise2_conversion(self): @skipIfMissingModules def test_exercise2_autopep8(self): - f_input = '@CMAKE_CURRENT_BINARY_DIR@/test_convert_exercise2_autopep8.ipynb' + root = pathlib.Path("@CMAKE_CURRENT_BINARY_DIR@") + f_input = root / "test_convert_exercise2_autopep8.ipynb" # setup with open(f_input, 'w', encoding='utf-8') as f: nb = nbformat.v4.new_notebook(metadata=self.nb_metadata) @@ -293,7 +294,7 @@ def test_exercise2_autopep8(self): nb['cells'].append(cell_md) nbformat.write(nb, f) # run command and check for errors - cmd = ['exercise2', '--pep8', f_input] + cmd = ['exercise2', '--pep8', str(f_input)] self.run_command(cmd) # read processed notebook with open(f_input, encoding='utf-8') as f: From 7bc8bc770a012b9c22749ab71646ff4c71b46a08 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jean-No=C3=ABl=20Grad?= Date: Mon, 4 Apr 2022 21:44:40 +0200 Subject: [PATCH 05/10] tests: Cleanup python testsuite Replace try/except import guards by contextlib.suppress(ImportError). Remove unreachable code. Improve code coverage of utility functions. Fix a minor bug when a variable assignment takes multiple lines. Run the MMM1D tuning function once per test method to reduce overhead. --- src/python/espressomd/analyze.pyx | 15 +-- testsuite/python/analyze_chains.py | 10 ++ testsuite/python/cluster_analysis.py | 42 +++--- testsuite/python/decorators.py | 3 + testsuite/python/ek_eof_one_species.py | 17 ++- testsuite/python/h5md.py | 10 +- testsuite/python/integrator_npt.py | 12 +- testsuite/python/lb_vtk.py | 18 +-- testsuite/python/mdanalysis.py | 14 +- testsuite/python/mmm1d.py | 44 +++---- .../python/p3m_electrostatic_pressure.py | 15 ++- testsuite/python/script_interface.py | 2 +- testsuite/python/test_checkpoint.py | 15 ++- testsuite/scripts/importlib_wrapper.py | 22 ++-- testsuite/scripts/test_importlib_wrapper.py | 121 ++++++++++++------ 15 files changed, 202 insertions(+), 158 deletions(-) diff --git a/src/python/espressomd/analyze.pyx b/src/python/espressomd/analyze.pyx index 48cd2253111..d14b9b8cf8c 100644 --- a/src/python/espressomd/analyze.pyx +++ b/src/python/espressomd/analyze.pyx @@ -374,10 +374,7 @@ class Analysis: """ self.check_topology(chain_start, number_of_chains, chain_length) - re = analyze.calc_re( - chain_start, - number_of_chains, - chain_length) + re = analyze.calc_re(chain_start, number_of_chains, chain_length) return np.array([re[0], re[1], re[2], re[3]]) def calc_rg(self, chain_start=None, number_of_chains=None, @@ -410,10 +407,7 @@ class Analysis: """ self.check_topology(chain_start, number_of_chains, chain_length) - rg = analyze.calc_rg( - chain_start, - number_of_chains, - chain_length) + rg = analyze.calc_rg(chain_start, number_of_chains, chain_length) return np.array([rg[0], rg[1], rg[2], rg[3]]) def calc_rh(self, chain_start=None, number_of_chains=None, @@ -445,10 +439,7 @@ class Analysis: """ self.check_topology(chain_start, number_of_chains, chain_length) - rh = analyze.calc_rh( - chain_start, - number_of_chains, - chain_length) + rh = analyze.calc_rh(chain_start, number_of_chains, chain_length) return np.array([rh[0], rh[1]]) def check_topology(self, chain_start=None, number_of_chains=None, diff --git a/testsuite/python/analyze_chains.py b/testsuite/python/analyze_chains.py index 655a6e7de48..4f2d0025692 100644 --- a/testsuite/python/analyze_chains.py +++ b/testsuite/python/analyze_chains.py @@ -133,6 +133,16 @@ def test_radii(self): self.system.box_l = self.system.box_l / 2. all_partcls.pos = old_pos + def test_exceptions(self): + err_msg = """particle with id 10 does not exist +cannot perform analysis on the range chain_start=0, number_of_chains=2, chain_length=10 +please provide a contiguous range of particle ids""" + analysis = self.system.analysis + for method in (analysis.calc_re, analysis.calc_rg, analysis.calc_rh): + with self.assertRaisesRegex(ValueError, err_msg): + method(chain_start=0, number_of_chains=self.num_poly, + chain_length=2 * self.num_mono) + if __name__ == "__main__": ut.main() diff --git a/testsuite/python/cluster_analysis.py b/testsuite/python/cluster_analysis.py index 89945dfa1e9..aad912b4cfe 100644 --- a/testsuite/python/cluster_analysis.py +++ b/testsuite/python/cluster_analysis.py @@ -149,28 +149,26 @@ def test_single_cluster_analysis(self): # Unknown method should return None self.assertIsNone(c.call_method("unknown")) - # Fractal dimension calc require gsl - if not espressomd.has_features("GSL"): - print("Skipping fractal dimension tests due to missing GSL dependency") - return - - # The fractal dimension of a line should be 1 - dr = 0. - self.assertAlmostEqual(c.fractal_dimension(dr=dr)[0], 1, delta=0.05) - - # The fractal dimension of a disk should be close to 2 - self.system.part.clear() - center = np.array((0.1, .02, 0.15)) - for _ in range(3000): - r_inv, phi = np.random.random(2) * np.array((0.2, 2 * np.pi)) - r = 1 / r_inv - self.system.part.add( - pos=center + r * np.array((np.sin(phi), np.cos(phi), 0))) - self.cs.clear() - self.cs.run_for_all_pairs() - cid = self.cs.cluster_ids()[0] - df = self.cs.clusters[cid].fractal_dimension(dr=0.001) - self.assertAlmostEqual(df[0], 2, delta=0.08) + # Fractal dimension calculation requires gsl + if espressomd.has_features("GSL"): + # The fractal dimension of a line should be 1 + dr = 0. + self.assertAlmostEqual( + c.fractal_dimension(dr=dr)[0], 1, delta=0.05) + + # The fractal dimension of a disk should be close to 2 + self.system.part.clear() + center = np.array((0.1, .02, 0.15)) + for _ in range(3000): + r_inv, phi = np.random.random(2) * np.array((0.2, 2 * np.pi)) + r = 1 / r_inv + self.system.part.add( + pos=center + r * np.array((np.sin(phi), np.cos(phi), 0))) + self.cs.clear() + self.cs.run_for_all_pairs() + cid = self.cs.cluster_ids()[0] + df = self.cs.clusters[cid].fractal_dimension(dr=0.001) + self.assertAlmostEqual(df[0], 2, delta=0.08) def test_analysis_for_bonded_particles(self): self.set_two_clusters() diff --git a/testsuite/python/decorators.py b/testsuite/python/decorators.py index 139dcfa6b9f..1907441cad0 100644 --- a/testsuite/python/decorators.py +++ b/testsuite/python/decorators.py @@ -54,6 +54,9 @@ def test_missing_modules(self): decorator = utx.skipIfMissingModules(['numpy___', 'scipy___']) args = self.get_skip_reason(decorator) self.assertEqual(args, (err_msg + 'modules numpy___, scipy___',)) + decorator = utx.skipIfMissingModules(['espressomd']) + args = self.get_skip_reason(decorator) + self.assertIsNone(args) def test_missing_gpu(self): espressomd.gpu_available = lambda: False diff --git a/testsuite/python/ek_eof_one_species.py b/testsuite/python/ek_eof_one_species.py index 93251de487f..b347339ec70 100644 --- a/testsuite/python/ek_eof_one_species.py +++ b/testsuite/python/ek_eof_one_species.py @@ -20,18 +20,15 @@ import unittest_generator as utg import pathlib import tempfile +import contextlib import sys import math import numpy as np -try: - import vtk - from vtk.util import numpy_support as VN - skipIfMissingPythonPackage = utx.no_skip -except ImportError: - skipIfMissingPythonPackage = ut.skip( - "Python module vtk not available, skipping test!") +with contextlib.suppress(ImportError): + import vtk + import vtk.util.numpy_support import espressomd import espressomd.electrokinetics @@ -214,6 +211,7 @@ def bisection(): @utx.skipIfMissingGPU() @utx.skipIfMissingFeatures(["ELECTROKINETICS", "EK_BOUNDARIES"]) +@utx.skipIfMissingModules("vtk") class ek_eof_one_species(ut.TestCase): system = espressomd.System(box_l=[1.0, 1.0, 1.0]) xi = bisection() @@ -228,7 +226,8 @@ def parse_vtk(self, filepath, name, shape): data = reader.GetOutput() points = data.GetPointData() - return VN.vtk_to_numpy(points.GetArray(name)).reshape(shape, order='F') + return vtk.util.numpy_support.vtk_to_numpy( + points.GetArray(name)).reshape(shape, order='F') @classmethod def setUpClass(cls): @@ -426,7 +425,7 @@ def test(self): self.assertLess(total_pressure_difference_xz, 1.0e-04, "Pressure accuracy xz component not achieved") - @skipIfMissingPythonPackage + @utx.skipIfMissingModules("vtk") def test_vtk(self): ek = self.ek counterions = self.counterions diff --git a/testsuite/python/h5md.py b/testsuite/python/h5md.py index f345d3ce09c..a4919a22396 100644 --- a/testsuite/python/h5md.py +++ b/testsuite/python/h5md.py @@ -31,19 +31,17 @@ import espressomd.lees_edwards import espressomd.version import tempfile -try: +import contextlib + +with contextlib.suppress(ImportError): import h5py # h5py has to be imported *after* espressomd (MPI) - skipIfMissingPythonPackage = utx.no_skip -except ImportError: - skipIfMissingPythonPackage = ut.skip( - "Python module h5py not available, skipping test!") N_PART = 26 @utx.skipIfMissingFeatures(['H5MD']) -@skipIfMissingPythonPackage +@utx.skipIfMissingModules("h5py") class H5mdTests(ut.TestCase): """ Test the core implementation of writing hdf5 files. diff --git a/testsuite/python/integrator_npt.py b/testsuite/python/integrator_npt.py index 95a8828022f..3e9eaa44427 100644 --- a/testsuite/python/integrator_npt.py +++ b/testsuite/python/integrator_npt.py @@ -154,10 +154,8 @@ def test_dp3m_exception(self): self.run_with_p3m( dp3m, cubic_box=False, direction=(False, True, True)) self.tearDown() - try: - self.run_with_p3m(dp3m) - except Exception as err: - self.fail(f'integrator raised ValueError("{err}")') + # should not raise an exception + self.run_with_p3m(dp3m) @utx.skipIfMissingFeatures(["P3M"]) def test_p3m_exception(self): @@ -170,10 +168,8 @@ def test_p3m_exception(self): self.run_with_p3m( p3m, cubic_box=False, direction=(False, True, True)) self.tearDown() - try: - self.run_with_p3m(p3m) - except Exception as err: - self.fail(f'integrator raised ValueError("{err}")') + # should not raise an exception + self.run_with_p3m(p3m) @utx.skipIfMissingGPU() @utx.skipIfMissingFeatures(["P3M"]) diff --git a/testsuite/python/lb_vtk.py b/testsuite/python/lb_vtk.py index bce6fb68ab9..d48d3a1148e 100644 --- a/testsuite/python/lb_vtk.py +++ b/testsuite/python/lb_vtk.py @@ -21,15 +21,12 @@ import pathlib import tempfile +import contextlib import numpy as np -try: +with contextlib.suppress(ImportError): import vtk - from vtk.util import numpy_support as VN - skipIfMissingPythonPackage = utx.no_skip -except ImportError: - skipIfMissingPythonPackage = ut.skip( - "Python module vtk not available, skipping test!") + import vtk.util.numpy_support import espressomd import espressomd.lb @@ -70,7 +67,8 @@ def parse_vtk(self, filepath, name, shape): data = reader.GetOutput() points = data.GetPointData() - return VN.vtk_to_numpy(points.GetArray(name)).reshape(shape, order='F') + return vtk.util.numpy_support.vtk_to_numpy( + points.GetArray(name)).reshape(shape, order='F') def test_vtk(self): ''' @@ -136,8 +134,6 @@ def test_vtk(self): vtk_boundary = self.parse_vtk(path_vtk_boundary, 'boundary', shape) np.testing.assert_equal(vtk_boundary, node_boundary.astype(int)) - if self.system.lbboundaries is None: - np.testing.assert_equal(np.sum(node_boundary), 0.) def test_print(self): ''' @@ -194,7 +190,7 @@ def test_print(self): np.testing.assert_equal(dat_bound, ref_bound) -@skipIfMissingPythonPackage +@utx.skipIfMissingModules("vtk") class TestLBWriteCPU(TestLBWrite, ut.TestCase): def setUp(self): @@ -202,7 +198,7 @@ def setUp(self): @utx.skipIfMissingGPU() -@skipIfMissingPythonPackage +@utx.skipIfMissingModules("vtk") class TestLBWriteGPU(TestLBWrite, ut.TestCase): def setUp(self): diff --git a/testsuite/python/mdanalysis.py b/testsuite/python/mdanalysis.py index 8a2659f10c4..8d07e58df78 100644 --- a/testsuite/python/mdanalysis.py +++ b/testsuite/python/mdanalysis.py @@ -25,16 +25,14 @@ import numpy as np import unittest as ut import unittest_decorators as utx -try: - import MDAnalysis as mda +import contextlib + +with contextlib.suppress(ImportError): + import MDAnalysis import espressomd.MDA_ESP - skipIfMissingPythonPackage = utx.no_skip -except ImportError: - skipIfMissingPythonPackage = ut.skip( - "Python module MDAnalysis not available, skipping test!") -@skipIfMissingPythonPackage +@utx.skipIfMissingModules("MDAnalysis") class TestMDAnalysis(ut.TestCase): system = espressomd.System(box_l=[10.0, 10.0, 10.0]) system.time_step = 0.001 @@ -58,7 +56,7 @@ def test_universe(self): system = self.system partcls = system.part.all() eos = espressomd.MDA_ESP.Stream(system) - u = mda.Universe(eos.topology, eos.trajectory) + u = MDAnalysis.Universe(eos.topology, eos.trajectory) # check atoms self.assertEqual(len(u.atoms), 10) np.testing.assert_equal(u.atoms.ids, np.arange(10) + 1) diff --git a/testsuite/python/mmm1d.py b/testsuite/python/mmm1d.py index 921ecdedbe1..c0c7f6f624b 100644 --- a/testsuite/python/mmm1d.py +++ b/testsuite/python/mmm1d.py @@ -46,36 +46,30 @@ class ElectrostaticInteractionsTests: def setUp(self): self.system.periodicity = [0, 0, 1] self.system.cell_system.set_n_square() - self.system.part.add(pos=self.p_pos, q=self.p_q) - self.mmm1d = self.MMM1D(prefactor=1.0, maxPWerror=1e-20) - self.system.actors.add(self.mmm1d) - self.system.integrator.run(steps=0) def tearDown(self): self.system.part.clear() self.system.actors.clear() - def test_forces(self): + def test_forces_and_energy(self): + self.system.part.add(pos=self.p_pos, q=self.p_q) + mmm1d = self.MMM1D(prefactor=1.0, maxPWerror=1e-20) + self.system.actors.add(mmm1d) + self.system.integrator.run(steps=0) measured_f = np.copy(self.system.part.all().f) np.testing.assert_allclose(measured_f, self.forces_target, atol=self.allowed_error) - - def test_energy(self): measured_el_energy = self.system.analysis.energy()["coulomb"] self.assertAlmostEqual( measured_el_energy, self.energy_target, delta=self.allowed_error, msg="Measured energy deviates too much from stored result") - def test_with_analytical_result(self, prefactor=1.0, accuracy=1e-4): - self.system.part.clear() - p = self.system.part.add(pos=[0, 0, 0], q=1) - self.system.part.add(pos=[0, 0, 1], q=1) - - self.system.integrator.run(steps=0) + def check_with_analytical_result(self, prefactor, accuracy): + p = self.system.part.by_id(0) f_measured = p.f energy_measured = self.system.analysis.energy()["total"] - target_energy_config = 1.00242505606 * prefactor - target_force_z_config = -0.99510759 * prefactor + target_energy_config = -1.00242505606 * prefactor + target_force_z_config = 0.99510759 * prefactor self.assertAlmostEqual( f_measured[0], 0, delta=self.allowed_error, @@ -90,17 +84,23 @@ def test_with_analytical_result(self, prefactor=1.0, accuracy=1e-4): energy_measured, target_energy_config, delta=self.allowed_error, msg="Measured energy deviates too much from analytical result") + def test_with_analytical_result(self): + self.system.part.add(pos=[0, 0, 0], q=1) + self.system.part.add(pos=[0, 0, 1], q=-1) + mmm1d = self.MMM1D(prefactor=1.0, maxPWerror=1e-20) + self.system.actors.add(mmm1d) + self.system.integrator.run(steps=0) + self.check_with_analytical_result(prefactor=1.0, accuracy=0.0004) + def test_bjerrum_length_change(self): - self.system.part.clear() - self.system.actors.clear() - prefactor = 2 - mmm1d = self.MMM1D(prefactor=prefactor, maxPWerror=1e-20) + self.system.part.add(pos=[0, 0, 0], q=1) + self.system.part.add(pos=[0, 0, 1], q=-1) + mmm1d = self.MMM1D(prefactor=2.0, maxPWerror=1e-20) self.system.actors.add(mmm1d) - self.test_with_analytical_result(prefactor=prefactor, accuracy=0.0017) + self.system.integrator.run(steps=0) + self.check_with_analytical_result(prefactor=2.0, accuracy=0.0017) def test_exceptions(self): - self.system.actors.clear() - del self.mmm1d # check periodicity exceptions for periodicity in itertools.product(range(2), range(2), range(2)): if periodicity == (0, 0, 1): diff --git a/testsuite/python/p3m_electrostatic_pressure.py b/testsuite/python/p3m_electrostatic_pressure.py index 26927ed9422..0e0cc065660 100644 --- a/testsuite/python/p3m_electrostatic_pressure.py +++ b/testsuite/python/p3m_electrostatic_pressure.py @@ -105,10 +105,14 @@ def setUp(self): max_displacement=0.01) # warmup - while self.system.analysis.energy()["total"] > 10 * num_part: - print("minimization: {:.1f}".format( - self.system.analysis.energy()["total"])) + energy = self.system.analysis.energy()["total"] + print(f"minimization: {energy:.1f}") + for _ in range(10): self.system.integrator.run(10) + energy = self.system.analysis.energy()["total"] + print(f"minimization: {energy:.1f}") + if energy < 2 * num_part: + break self.system.integrator.set_vv() self.system.thermostat.set_langevin(kT=self.kT, gamma=1.0, seed=41) @@ -121,8 +125,9 @@ def test_p3m_pressure(self): cao=6, r_cut=1.4941e-01 * self.system.box_l[0]) self.system.actors.add(p3m) - print("Tune skin: {}".format(self.system.cell_system.tune_skin( - min_skin=0.0, max_skin=2.5, tol=0.05, int_steps=100))) + skin = self.system.cell_system.tune_skin( + min_skin=0.0, max_skin=2.5, tol=0.05, int_steps=100) + print(f"Tuned skin: {skin}") num_samples = 25 pressure_via_volume_scaling = pressureViaVolumeScaling( diff --git a/testsuite/python/script_interface.py b/testsuite/python/script_interface.py index 9940654e417..dc3c0c7294f 100644 --- a/testsuite/python/script_interface.py +++ b/testsuite/python/script_interface.py @@ -102,7 +102,7 @@ def test_variant_exceptions(self): constraint = espressomd.constraints.ShapeBasedConstraint() # check conversion of unsupported types with self.assertRaisesRegex(TypeError, "No conversion from type 'module' to 'Variant'"): - espressomd.constraints.ShapeBasedConstraint(unknown=ut) + espressomd.constraints.ShapeBasedConstraint(shape=ut) with self.assertRaisesRegex(TypeError, "No conversion from type 'module' to 'Variant'"): constraint.set_params(shape=ut) with self.assertRaisesRegex(TypeError, "No conversion from type 'module' to 'Variant'"): diff --git a/testsuite/python/test_checkpoint.py b/testsuite/python/test_checkpoint.py index d6f99bc7400..b298de69af0 100644 --- a/testsuite/python/test_checkpoint.py +++ b/testsuite/python/test_checkpoint.py @@ -20,6 +20,7 @@ import unittest_decorators as utx import unittest_generator as utg import numpy as np +import contextlib import pathlib import espressomd @@ -33,10 +34,8 @@ import espressomd.shapes import espressomd.constraints -try: +with contextlib.suppress(ImportError): import h5py # h5py has to be imported *after* espressomd (MPI) -except ImportError: - h5py = None config = utg.TestGenerator() is_gpu_available = espressomd.gpu_available() @@ -70,6 +69,13 @@ def get_active_actor_of_type(self, actor_type): self.fail( f"system doesn't have an actor of type {actor_type.__name__}") + def test_get_active_actor_of_type(self): + if system.actors.active_actors: + actor = system.actors.active_actors[0] + self.assertEqual(self.get_active_actor_of_type(type(actor)), actor) + with self.assertRaisesRegex(AssertionError, "system doesn't have an actor of type Wall"): + self.get_active_actor_of_type(espressomd.shapes.Wall) + @ut.skipIf(not has_lb_mode, "Skipping test due to missing mode.") def test_lb_fluid(self): ''' @@ -417,8 +423,7 @@ def test_correlator(self): expected) @utx.skipIfMissingFeatures('H5MD') - @ut.skipIf(h5py is None, - "Skipping test due to missing python module 'h5py'.") + @utx.skipIfMissingModules("h5py") def test_h5md(self): # check attributes file_path = self.path_cpt_root / "test.h5" diff --git a/testsuite/scripts/importlib_wrapper.py b/testsuite/scripts/importlib_wrapper.py index 67e3d9f9186..5e7ce1e97c7 100644 --- a/testsuite/scripts/importlib_wrapper.py +++ b/testsuite/scripts/importlib_wrapper.py @@ -22,14 +22,10 @@ import ast import tokenize import unittest +import unittest.mock import importlib import pathlib import espressomd -from unittest.mock import MagicMock - - -def _id(x): - return x # global variable: if one import failed, all subsequent imports will be skipped, @@ -82,13 +78,13 @@ def configure_and_import(filepath, """ filepath = pathlib.Path(filepath).resolve() if skip_future_imports: - module = MagicMock() + module = unittest.mock.MagicMock() skipIfMissingImport = skip_future_imports_dependency(filepath) return module, skipIfMissingImport if gpu and not espressomd.gpu_available(): skip_future_imports_dependency(filepath) skipIfMissingGPU = unittest.skip("gpu not available, skipping test!") - module = MagicMock() + module = unittest.mock.MagicMock() return module, skipIfMissingGPU # load original script code = filepath.read_text() @@ -122,9 +118,9 @@ def configure_and_import(filepath, except espressomd.FeaturesError as err: skip_future_imports_dependency(filepath) skipIfMissingFeatures = unittest.skip(f"{err}, skipping test!") - module = MagicMock() + module = unittest.mock.MagicMock() else: - skipIfMissingFeatures = _id + def skipIfMissingFeatures(x): return x if cmd_arguments is not None: # restore original command line arguments sys.argv = old_sys_argv @@ -236,8 +232,8 @@ def substitute_variable_values(code, strings_as_is=False, keep_original=True, if keep_original: lines[lineno - 1] += "; _" + varname + "__original" + old_value else: - for lineno in range(lineno + 1, mapping[lineno]): - lines[lineno - 1] = "" + for lineno in range(lineno, mapping[lineno]): + lines[lineno] = "" return "\n".join(lines) @@ -587,9 +583,9 @@ def mock_es_visualization(code): try: {0}{1} except ImportError: - from unittest.mock import MagicMock + import unittest.mock import espressomd - {2} = MagicMock() + {2} = unittest.mock.MagicMock() """.lstrip() def check_for_deferred_ImportError(line, alias): diff --git a/testsuite/scripts/test_importlib_wrapper.py b/testsuite/scripts/test_importlib_wrapper.py index e42fb4b4e44..67d800a450c 100644 --- a/testsuite/scripts/test_importlib_wrapper.py +++ b/testsuite/scripts/test_importlib_wrapper.py @@ -20,21 +20,24 @@ import sys import ast import pathlib +import tempfile +import espressomd class importlib_wrapper(ut.TestCase): def test_substitute_variable_values(self): - str_inp = "n_steps=5000\nnsteps == 5\n" - str_exp = "n_steps = 10; _n_steps__original=5000\nnsteps == 5\n" + str_inp = "n_steps=(\n5000)\nnsteps == 5\n" + str_exp = "n_steps = 10; _n_steps__original=(\n5000)\nnsteps == 5\n" str_out = iw.substitute_variable_values(str_inp, n_steps=10) self.assertEqual(str_out, str_exp) str_out = iw.substitute_variable_values(str_inp, n_steps='10', strings_as_is=True) self.assertEqual(str_out, str_exp) - str_inp = "N=5000\nnsteps == 5\n" - str_exp = "N = 10\nnsteps == 5\n" - str_out = iw.substitute_variable_values(str_inp, N=10, keep_original=0) + str_inp = "N=(\n5000)\nnsteps == 5\n" + str_exp = "N = 10\n\nnsteps == 5\n" + str_out = iw.substitute_variable_values(str_inp, N=10, + keep_original=False) self.assertEqual(str_out, str_exp) # test exceptions str_inp = "n_steps=5000\nnsteps == 5\n" @@ -93,9 +96,9 @@ def test_mock_es_visualization(self): hasattr(espressomd.visualization.openGLLive, 'deferred_ImportError'): raise ImportError() except ImportError: - from unittest.mock import MagicMock + import unittest.mock import espressomd - espressomd.visualization = MagicMock() + espressomd.visualization = unittest.mock.MagicMock() """ self.assertEqual(iw.mock_es_visualization(statement), expected[1:]) @@ -107,9 +110,9 @@ def test_mock_es_visualization(self): hasattr(test.openGLLive, 'deferred_ImportError'): raise ImportError() except ImportError: - from unittest.mock import MagicMock + import unittest.mock import espressomd - test = MagicMock() + test = unittest.mock.MagicMock() """ self.assertEqual(iw.mock_es_visualization(statement), expected[1:]) @@ -121,18 +124,18 @@ def test_mock_es_visualization(self): hasattr(espressomd.visualization.openGLLive, 'deferred_ImportError'): raise ImportError() except ImportError: - from unittest.mock import MagicMock + import unittest.mock import espressomd - espressomd.visualization = MagicMock() + espressomd.visualization = unittest.mock.MagicMock() try: import espressomd.visualization as test if hasattr(test.mayaviLive, 'deferred_ImportError') or \\ hasattr(test.openGLLive, 'deferred_ImportError'): raise ImportError() except ImportError: - from unittest.mock import MagicMock + import unittest.mock import espressomd - test = MagicMock() + test = unittest.mock.MagicMock() """ self.assertEqual(iw.mock_es_visualization(statement), expected[1:]) @@ -144,9 +147,9 @@ def test_mock_es_visualization(self): hasattr(visualization.openGLLive, 'deferred_ImportError'): raise ImportError() except ImportError: - from unittest.mock import MagicMock + import unittest.mock import espressomd - visualization = MagicMock() + visualization = unittest.mock.MagicMock() """ self.assertEqual(iw.mock_es_visualization(statement), expected[1:]) @@ -158,9 +161,9 @@ def test_mock_es_visualization(self): hasattr(test.openGLLive, 'deferred_ImportError'): raise ImportError() except ImportError: - from unittest.mock import MagicMock + import unittest.mock import espressomd - test = MagicMock() + test = unittest.mock.MagicMock() """ self.assertEqual(iw.mock_es_visualization(statement), expected[1:]) @@ -169,9 +172,9 @@ def test_mock_es_visualization(self): try: from espressomd import visualization_mayavi except ImportError: - from unittest.mock import MagicMock + import unittest.mock import espressomd - visualization_mayavi = MagicMock() + visualization_mayavi = unittest.mock.MagicMock() """ self.assertEqual(iw.mock_es_visualization(statement), expected[1:]) @@ -180,9 +183,9 @@ def test_mock_es_visualization(self): try: from espressomd import visualization_mayavi as test except ImportError: - from unittest.mock import MagicMock + import unittest.mock import espressomd - test = MagicMock() + test = unittest.mock.MagicMock() """ self.assertEqual(iw.mock_es_visualization(statement), expected[1:]) @@ -191,9 +194,9 @@ def test_mock_es_visualization(self): try: from espressomd.visualization_mayavi import mayaviLive except ImportError: - from unittest.mock import MagicMock + import unittest.mock import espressomd - mayaviLive = MagicMock() + mayaviLive = unittest.mock.MagicMock() """ self.assertEqual(iw.mock_es_visualization(statement), expected[1:]) @@ -202,9 +205,9 @@ def test_mock_es_visualization(self): try: from espressomd.visualization_mayavi import mayaviLive as test except ImportError: - from unittest.mock import MagicMock + import unittest.mock import espressomd - test = MagicMock() + test = unittest.mock.MagicMock() """ self.assertEqual(iw.mock_es_visualization(statement), expected[1:]) @@ -213,15 +216,15 @@ def test_mock_es_visualization(self): try: from espressomd.visualization_mayavi import a as b except ImportError: - from unittest.mock import MagicMock + import unittest.mock import espressomd - b = MagicMock() + b = unittest.mock.MagicMock() try: from espressomd.visualization_mayavi import mayaviLive except ImportError: - from unittest.mock import MagicMock + import unittest.mock import espressomd - mayaviLive = MagicMock() + mayaviLive = unittest.mock.MagicMock() """ self.assertEqual(iw.mock_es_visualization(statement), expected[1:]) @@ -232,9 +235,9 @@ def test_mock_es_visualization(self): if hasattr(openGLLive, 'deferred_ImportError'): raise openGLLive.deferred_ImportError except ImportError: - from unittest.mock import MagicMock + import unittest.mock import espressomd - openGLLive = MagicMock() + openGLLive = unittest.mock.MagicMock() """ self.assertEqual(iw.mock_es_visualization(statement), expected[1:]) @@ -245,9 +248,9 @@ def test_mock_es_visualization(self): if hasattr(test, 'deferred_ImportError'): raise test.deferred_ImportError except ImportError: - from unittest.mock import MagicMock + import unittest.mock import espressomd - test = MagicMock() + test = unittest.mock.MagicMock() """ self.assertEqual(iw.mock_es_visualization(statement), expected[1:]) @@ -313,6 +316,7 @@ def test_prng_seed_espressomd_system_visitor(self): 'import espressomd.System as s1, espressomd.system.System as s2', 'from espressomd import System as s3, electrostatics', 'from espressomd.system import System as s4', + 'from espressomd import system as es5', 'sys1 = es1.System()', 'sys2 = es1.system.System()', 'sys3 = es2.System()', @@ -320,6 +324,7 @@ def test_prng_seed_espressomd_system_visitor(self): 'sys5 = s2()', 'sys6 = s3()', 'sys7 = s4()', + 'sys8 = es5.System()', 'import numpy as np', 'import numpy.random as npr1', 'from numpy import random as npr2', @@ -332,13 +337,14 @@ def test_prng_seed_espressomd_system_visitor(self): v.visit(tree) # find all aliases for espressomd.system.System expected_es_sys_aliases = {'es1.System', 'es1.system.System', - 'es2.System', 's1', 's2', 's3', 's4'} + 'es2.System', 's1', 's2', 's3', 's4', + 'es5.System'} self.assertEqual(v.es_system_aliases, expected_es_sys_aliases) # find all variables of type espressomd.system.System - expected_es_sys_objs = set(f'sys{i}' for i in range(1, 8)) + expected_es_sys_objs = set(f'sys{i}' for i in range(1, 9)) self.assertEqual(v.variable_system_aliases, expected_es_sys_objs) # find all seeds setup - self.assertEqual(v.numpy_seeds, [17, 18, 19]) + self.assertEqual(v.numpy_seeds, [19, 20, 21]) # test exceptions str_es_sys_list = [ 'import espressomd.System', @@ -397,6 +403,49 @@ def test_ipython_magics(self): code_deprotected = iw.deprotect_ipython_magics(code_protected) self.assertEqual(code_deprotected, code) + def test_configure_and_import(self): + with tempfile.TemporaryDirectory() as temp_dir: + path_in = pathlib.Path(temp_dir) / "sample.py" + path_out = pathlib.Path(temp_dir) / "sample_test_processed.py" + path_features = pathlib.Path(temp_dir) / "sample_impossible.py" + + # test importing a simple sample + sys.argv.append("42") + sys_argv_ref = list(sys.argv) + path_in.write_text(""" +import sys +from argparse import ArgumentParser +value = 42 +argv = list(sys.argv) +import espressomd.visualization_opengl +""") + sample, _ = iw.configure_and_import( + path_in, move_to_script_dir=False, cmd_arguments=["TestCase"], + gpu=False, script_suffix="test", value=43) + self.assertEqual(sys.argv, sys_argv_ref) + self.assertEqual(sample.argv, [path_in.name, "TestCase"]) + self.assertTrue(path_out.exists(), f"File {path_out} not found") + self.assertEqual(sample.value, 43) + self.assertIn( + "espressomd.visualization_opengl = unittest.mock.MagicMock()", + path_out.read_text()) + + # test importing a sample that relies on features not compiled in + inactive_features = espressomd.all_features() - set(espressomd.features()) + if inactive_features: + path_features.write_text(f""" +import espressomd +espressomd.assert_features({list(inactive_features)}) +""") + module, _ = iw.configure_and_import(path_features) + self.assertIsInstance(module, ut.mock.MagicMock) + + # importing a valid sample is impossible after a failed import + sample, _ = iw.configure_and_import( + path_in, move_to_script_dir=True, script_suffix="test", + cmd_arguments=["TestCase"], gpu=False, value=43) + self.assertIsInstance(sample, ut.mock.MagicMock) + if __name__ == "__main__": ut.main() From 72f35b39e2a21c7d0bd6d3a6179d01c72244665b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jean-No=C3=ABl=20Grad?= Date: Mon, 4 Apr 2022 21:56:58 +0200 Subject: [PATCH 06/10] maintainer: Use pathlib module --- maintainer/CI/deploy_tutorials.py | 12 +++++------- maintainer/CI/dox_warnings.py | 4 ++-- maintainer/CI/jupyter_warnings.py | 16 +++++++--------- maintainer/benchmarks/benchmarks.py | 6 +++--- 4 files changed, 17 insertions(+), 21 deletions(-) diff --git a/maintainer/CI/deploy_tutorials.py b/maintainer/CI/deploy_tutorials.py index 39494a2f7cf..adae4b36dc1 100755 --- a/maintainer/CI/deploy_tutorials.py +++ b/maintainer/CI/deploy_tutorials.py @@ -20,20 +20,18 @@ """List all tutorial files to deploy (PDF, HTML and figures)""" -import os -import glob +import pathlib import lxml.html -deploy_list = glob.glob('*/*.pdf') -for filepath in glob.glob('*/*.html'): +deploy_list = list(pathlib.Path().glob('*/*.pdf')) +for filepath in pathlib.Path().glob('*/*.html'): deploy_list.append(filepath) # extract all figures - dirname = os.path.dirname(filepath) with open(filepath, encoding='utf-8') as f: html = lxml.html.parse(f) figures = filter(lambda src: not src.startswith('data:image'), html.xpath('//img/@src')) - deploy_list += list(map(lambda src: os.path.join(dirname, src), figures)) + deploy_list += list(map(lambda src: filepath.parent / src, figures)) with open('deploy_list.txt', 'w') as f: - f.write('\n'.join(deploy_list)) + f.write('\n'.join(map(str, deploy_list))) diff --git a/maintainer/CI/dox_warnings.py b/maintainer/CI/dox_warnings.py index 897b37f8512..035bf1d4f9d 100755 --- a/maintainer/CI/dox_warnings.py +++ b/maintainer/CI/dox_warnings.py @@ -18,7 +18,7 @@ # along with this program. If not, see . # import re -import os +import pathlib # collect list of Doxygen warnings with open('doc/doxygen/warnings.log') as f: @@ -38,7 +38,7 @@ for line in content.strip().split('\n'): m = re.search(r'^(.+):(\d+):[\s\*]*([@\\]t?param).*\s(\S+)\s*$', line) filepath, lineno, paramtype, varname = m.groups() - ext = os.path.splitext(filepath)[1] + ext = pathlib.Path(filepath).suffix if ext.lower() not in source_code_ext: continue warning = (f'argument \'{varname}\' of {paramtype} has no description,' diff --git a/maintainer/CI/jupyter_warnings.py b/maintainer/CI/jupyter_warnings.py index 19bda8e477e..ce0ec832711 100755 --- a/maintainer/CI/jupyter_warnings.py +++ b/maintainer/CI/jupyter_warnings.py @@ -22,9 +22,8 @@ pointing to sections of the notebooks or to the online Sphinx documentation. """ -import os import sys -import glob +import pathlib import lxml.etree import nbformat @@ -61,7 +60,7 @@ def detect_invalid_urls(nb, sphinx_root='.'): root = lxml.etree.fromstring(html_string, parser=html_parser) # process all links espressomd_website_root = 'https://espressomd.github.io/doc/' - sphinx_html_root = os.path.join(sphinx_root, 'doc', 'sphinx', 'html') + sphinx_html_root = pathlib.Path(sphinx_root) / 'doc' / 'sphinx' / 'html' broken_links = [] for link in root.xpath('//a'): url = link.attrib.get('href', '') @@ -75,15 +74,15 @@ def detect_invalid_urls(nb, sphinx_root='.'): url = url.split('?', 1)[0] # check file exists basename = url.split(espressomd_website_root, 1)[1] - filepath = os.path.join(sphinx_html_root, basename) - if not os.path.isfile(filepath): + filepath = sphinx_html_root / basename + if not filepath.is_file(): broken_links.append(f'{url} does not exist') continue # check anchor exists if anchor is not None: if filepath not in sphinx_docs: sphinx_docs[filepath] = lxml.etree.parse( - filepath, parser=html_parser) + str(filepath), parser=html_parser) doc = sphinx_docs[filepath] nodes = doc.xpath(f'//*[@id="{anchor}"]') if not nodes: @@ -99,14 +98,13 @@ def detect_invalid_urls(nb, sphinx_root='.'): if __name__ == '__main__': error_code = 0 - for nb_filepath in sorted(glob.glob('doc/tutorials/*/*.ipynb')): + for nb_filepath in sorted(pathlib.Path().glob('doc/tutorials/*/*.ipynb')): with open(nb_filepath, encoding='utf-8') as f: nb = nbformat.read(f, as_version=4) issues = detect_invalid_urls(nb) if issues: error_code = 1 - basename = os.path.basename(nb_filepath) - print(f'In notebook {basename}:', file=sys.stderr) + print(f'In notebook {nb_filepath.name}:', file=sys.stderr) for issue in issues: print(f'* {issue}', file=sys.stderr) if not error_code: diff --git a/maintainer/benchmarks/benchmarks.py b/maintainer/benchmarks/benchmarks.py index 2e37a6a46d4..dfb3fc81f33 100644 --- a/maintainer/benchmarks/benchmarks.py +++ b/maintainer/benchmarks/benchmarks.py @@ -16,9 +16,9 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . # -import os import sys import time +import pathlib import numpy as np @@ -127,12 +127,12 @@ def write_report(filepath, n_proc, timings, n_steps, label=''): Label to distinguish e.g. MD from MC or LB steps. ''' - script = os.path.basename(sys.argv[0]) + script = pathlib.Path(sys.argv[0]).name cmd = " ".join(x for x in sys.argv[1:] if not x.startswith("--output")) avg, ci = get_average_time(timings) header = '"script","arguments","cores","mean","ci","nsteps","duration","label"\n' report = f'"{script}","{cmd}",{n_proc},{avg:.3e},{ci:.3e},{n_steps},{np.sum(timings):.1f},"{label}"\n' - if os.path.isfile(filepath): + if pathlib.Path(filepath).is_file(): header = '' with open(filepath, "a") as f: f.write(header + report) From 8031fba2b92d515e14fc084e73d526ee76953889 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jean-No=C3=ABl=20Grad?= Date: Tue, 5 Apr 2022 00:07:46 +0200 Subject: [PATCH 07/10] python: Document Python classes --- doc/sphinx/system_setup.rst | 6 +-- src/python/espressomd/lees_edwards.py | 53 ++++++++++++++------------ testsuite/scripts/importlib_wrapper.py | 28 +++++++------- 3 files changed, 46 insertions(+), 41 deletions(-) diff --git a/doc/sphinx/system_setup.rst b/doc/sphinx/system_setup.rst index 038520c1038..62dc61fe2a7 100644 --- a/doc/sphinx/system_setup.rst +++ b/doc/sphinx/system_setup.rst @@ -158,7 +158,7 @@ Lees--Edwards boundary conditions ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Lees--Edwards boundary conditions (LEbc) are special periodic boundary -conditions to simulation systems under shear stress :cite:`lees72a`. +conditions to simulate systems under shear stress :cite:`lees72a`. Periodic images of particles across the shear boundary appear with a time-dependent position offset. When a particle crosses the shear boundary, it appears to the opposite side of the simulation box with a position offset @@ -227,13 +227,13 @@ The properties of the cell system can be accessed by * :py:attr:`~espressomd.cellsystem.CellSystem.node_grid` - (int[3]) 3D node grid for real space domain decomposition (optional, if + 3D node grid for real space domain decomposition (optional, if unset an optimal set is chosen automatically). The domain decomposition can be visualized with :file:`samples/visualization_cellsystem.py`. * :py:attr:`~espressomd.cellsystem.CellSystem.skin` - (float) Skin for the Verlet list. This value has to be set, otherwise the simulation will not start. + Skin for the Verlet list. This value has to be set, otherwise the simulation will not start. Details about the cell system can be obtained by :meth:`espressomd.system.System.cell_system.get_state() `: diff --git a/src/python/espressomd/lees_edwards.py b/src/python/espressomd/lees_edwards.py index 8b64df06059..7b7ac911351 100644 --- a/src/python/espressomd/lees_edwards.py +++ b/src/python/espressomd/lees_edwards.py @@ -22,9 +22,23 @@ @script_interface_register class LeesEdwards(ScriptInterfaceHelper): - """Interface to the Lees-Edwards boundary conditions. + """ + Interface to the :ref:`Lees-Edwards boundary conditions`. - See documentation. + Attributes + ---------- + protocol : :obj:`object` + Lees--Edwards protocol. + shear_velocity: :obj:`float` + Current shear velocity. + pos_offset : :obj:`float` + Current position offset + shear_direction : :obj:`int` + Shear direction: 0 for the *x*-axis, 1 for the *y*-axis, + 2 for the *z*-axis. + shear_plane_normal : :obj:`int` + Shear plane normal: 0 for the *x*-axis, 1 for the *y*-axis, + 2 for the *z*-axis. """ @@ -34,25 +48,21 @@ class LeesEdwards(ScriptInterfaceHelper): @script_interface_register class Off(ScriptInterfaceHelper): - """Lees-Edwards protocol resulting in un-shifted boundaries.""" + """Lees--Edwards protocol resulting in un-shifted boundaries.""" _so_name = "LeesEdwards::Off" @script_interface_register class LinearShear(ScriptInterfaceHelper): - """Lees-Edwards protocol for linear shear. + """Lees--Edwards protocol for linear shear. Parameters ---------- - shear_direction - Cartesian coordinate of the shear direction (0=x, 1=y, 2=z) - shear_plane_normal - Cartesian coordinate of the shear plane normal - initial_pos_offset - Positional offset at the Lees-Edwards boundary at t=0 - shear_velocity - Shear velocity (velocity jump) across the Lees-Edwards boundary + initial_pos_offset : :obj:`float` + Positional offset at the Lees--Edwards boundary at t=0. + shear_velocity : :obj:`float` + Shear velocity (velocity jump) across the Lees--Edwards boundary. """ _so_name = "LeesEdwards::LinearShear" @@ -61,21 +71,16 @@ class LinearShear(ScriptInterfaceHelper): @script_interface_register class OscillatoryShear(ScriptInterfaceHelper): - """Lees-Edwards protocol for oscillatory shear. + """Lees--Edwards protocol for oscillatory shear. Parameters ---------- - shear_direction - Cartesian coordinate of the shear direction (0=x, 1=y, 2=z) - shear_plane_normal - Cartesian coordinate of the shear plane normal - amplitude - Maximum amplitude of the positional offset at the Lees-Edwards boundary - frequency - Frequency of the shear - time_0 - Time offset of the oscillation - + amplitude : :obj:`float` + Maximum amplitude of the positional offset at the Lees--Edwards boundary. + omega : :obj:`float` + Radian frequency of the oscillation. + time_0 : :obj:`float` + Time offset of the oscillation. """ _so_name = "LeesEdwards::OscillatoryShear" diff --git a/testsuite/scripts/importlib_wrapper.py b/testsuite/scripts/importlib_wrapper.py index 5e7ce1e97c7..f7377ead943 100644 --- a/testsuite/scripts/importlib_wrapper.py +++ b/testsuite/scripts/importlib_wrapper.py @@ -52,23 +52,23 @@ def configure_and_import(filepath, Parameters ---------- - filepath : str + filepath : :obj:`str` python script to import - gpu : bool + gpu : :obj:`bool` whether GPU is necessary or not - substitutions : function + substitutions : :obj:`function` custom text replacement operation (useful to edit out calls to the OpenGL or Mayavi visualizers' ``run()`` method) - cmd_arguments : list + cmd_arguments : :obj:`list` command line arguments, i.e. sys.argv without the script path - script_suffix : str + script_suffix : :obj:`str` suffix to append to the configured script (useful when a single module is being tested by multiple tests in parallel) - mock_visualizers : bool - if ``True``, substitute ES visualizers with `Mock()` classes in case - of `ImportError()` (use ``False`` if an `ImportError()` is relevant + mock_visualizers : :obj:`bool` + if ``True``, substitute ES visualizers with ``Mock`` classes in case + of ``ImportError`` (use ``False`` if an ``ImportError`` is relevant to your test) - move_to_script_dir : bool + move_to_script_dir : :obj:`bool` if ``True``, move to the script's directory (useful when the script needs to load files hardcoded as relative paths, or when files are generated and need cleanup); this is enabled by default @@ -203,12 +203,12 @@ def substitute_variable_values(code, strings_as_is=False, keep_original=True, Parameters ---------- - code : str + code : :obj:`str` Source code to edit. - strings_as_is : bool + strings_as_is : :obj:`bool` If ``True``, consider all values in \*\*parameters are strings and substitute them in-place without formatting by ``repr()``. - keep_original : bool + keep_original : :obj:`bool` Keep the original value (e.g. ``N = 10; _N__original = 1000``), helps with debugging. \*\*parameters : @@ -569,9 +569,9 @@ def visit_ImportFrom(self, node): def mock_es_visualization(code): """ - Replace ``import espressomd.visualization_`` by a ``MagicMock()`` + Replace ``import espressomd.visualization_`` by a ``MagicMock`` when the visualization module is unavailable, by catching the - ``ImportError()`` exception. Please note that ``espressomd.visualization`` + ``ImportError`` exception. Please note that ``espressomd.visualization`` is deferring the exception, thus requiring additional checks. Import aliases are supported, however please don't use From ece1656613935d3c0b2bdb73d61e286e77768922 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jean-No=C3=ABl=20Grad?= Date: Tue, 5 Apr 2022 00:26:05 +0200 Subject: [PATCH 08/10] core: Add initial_pos_offset to Oscillatory LEbc --- src/core/lees_edwards/protocols.hpp | 16 +++++++++++----- src/core/unit_tests/lees_edwards_test.cpp | 6 ++++-- src/python/espressomd/lees_edwards.py | 2 ++ .../lees_edwards/OscillatoryShear.hpp | 5 ++++- testsuite/python/lees_edwards.py | 9 +++++---- 5 files changed, 26 insertions(+), 12 deletions(-) diff --git a/src/core/lees_edwards/protocols.hpp b/src/core/lees_edwards/protocols.hpp index 65a275497b4..29737168b40 100644 --- a/src/core/lees_edwards/protocols.hpp +++ b/src/core/lees_edwards/protocols.hpp @@ -37,7 +37,8 @@ struct Off { /** Lees-Edwards protocol for linear shearing */ struct LinearShear { - LinearShear() : m_initial_pos_offset{0}, m_shear_velocity{0}, m_time_0{0} {} + LinearShear() + : m_initial_pos_offset{0.}, m_shear_velocity{0.}, m_time_0{0.} {} LinearShear(double initial_offset, double shear_velocity, double time_0) : m_initial_pos_offset{initial_offset}, m_shear_velocity{shear_velocity}, m_time_0{time_0} {} @@ -52,15 +53,20 @@ struct LinearShear { /** Lees-Edwards protocol for oscillatory shearing */ struct OscillatoryShear { - OscillatoryShear() : m_amplitude{0}, m_omega{0}, m_time_0{0} {} - OscillatoryShear(double amplitude, double omega, double time_0) - : m_amplitude{amplitude}, m_omega{omega}, m_time_0{time_0} {} + OscillatoryShear() + : m_initial_pos_offset{0.}, m_amplitude{0.}, m_omega{0.}, m_time_0{0.} {} + OscillatoryShear(double initial_offset, double amplitude, double omega, + double time_0) + : m_initial_pos_offset{initial_offset}, + m_amplitude{amplitude}, m_omega{omega}, m_time_0{time_0} {} double pos_offset(double time) const { - return m_amplitude * std::sin(m_omega * (time - m_time_0)); + return m_initial_pos_offset + + m_amplitude * std::sin(m_omega * (time - m_time_0)); } double shear_velocity(double time) const { return m_omega * m_amplitude * std::cos(m_omega * (time - m_time_0)); } + double m_initial_pos_offset; double m_amplitude; double m_omega; double m_time_0; diff --git a/src/core/unit_tests/lees_edwards_test.cpp b/src/core/unit_tests/lees_edwards_test.cpp index 0f9003f3eaf..8739059a363 100644 --- a/src/core/unit_tests/lees_edwards_test.cpp +++ b/src/core/unit_tests/lees_edwards_test.cpp @@ -132,10 +132,12 @@ BOOST_AUTO_TEST_CASE(protocol_lin) { BOOST_AUTO_TEST_CASE(protocol_osc) { auto const t0 = 1.2; + auto const x0 = 0.1; auto const a = 3.1; auto const o = 2.1; - auto osc = OscillatoryShear(a, o, t0); - BOOST_CHECK_CLOSE(get_pos_offset(3.3, osc), a * sin(o * (3.3 - t0)), tol); + auto osc = OscillatoryShear(x0, a, o, t0); + BOOST_CHECK_CLOSE(get_pos_offset(3.3, osc), x0 + a * sin(o * (3.3 - t0)), + tol); BOOST_CHECK_CLOSE(get_shear_velocity(3.3, osc), a * o * cos(o * (3.3 - t0)), tol); } diff --git a/src/python/espressomd/lees_edwards.py b/src/python/espressomd/lees_edwards.py index 7b7ac911351..b4e18a929e5 100644 --- a/src/python/espressomd/lees_edwards.py +++ b/src/python/espressomd/lees_edwards.py @@ -75,6 +75,8 @@ class OscillatoryShear(ScriptInterfaceHelper): Parameters ---------- + initial_pos_offset : :obj:`float` + Positional offset at the Lees--Edwards boundary at t=0. amplitude : :obj:`float` Maximum amplitude of the positional offset at the Lees--Edwards boundary. omega : :obj:`float` diff --git a/src/script_interface/lees_edwards/OscillatoryShear.hpp b/src/script_interface/lees_edwards/OscillatoryShear.hpp index 9129d3ad4b3..7bf9923b193 100644 --- a/src/script_interface/lees_edwards/OscillatoryShear.hpp +++ b/src/script_interface/lees_edwards/OscillatoryShear.hpp @@ -37,7 +37,10 @@ class OscillatoryShear : public Protocol { : m_protocol{std::make_shared<::LeesEdwards::ActiveProtocol>( ::LeesEdwards::OscillatoryShear())} { add_parameters( - {{"amplitude", + {{"initial_pos_offset", + boost::get<::LeesEdwards::OscillatoryShear>(*m_protocol) + .m_initial_pos_offset}, + {"amplitude", boost::get<::LeesEdwards::OscillatoryShear>(*m_protocol).m_amplitude}, {"omega", boost::get<::LeesEdwards::OscillatoryShear>(*m_protocol).m_omega}, diff --git a/testsuite/python/lees_edwards.py b/testsuite/python/lees_edwards.py index f0e47174bbb..731eb09c4ac 100644 --- a/testsuite/python/lees_edwards.py +++ b/testsuite/python/lees_edwards.py @@ -30,8 +30,9 @@ np.random.seed(42) -params_lin = {'shear_velocity': 1.2, 'initial_pos_offset': 0.1, 'time_0': 0.1} -params_osc = {'amplitude': 2.3, 'omega': 2.51, 'time_0': -2.1} +params_lin = {'initial_pos_offset': 0.1, 'time_0': 0.1, 'shear_velocity': 1.2} +params_osc = {'initial_pos_offset': 0.1, 'time_0': -2.1, 'amplitude': 2.3, + 'omega': 2.51} lin_protocol = espressomd.lees_edwards.LinearShear(**params_lin) osc_protocol = espressomd.lees_edwards.OscillatoryShear(**params_osc) off_protocol = espressomd.lees_edwards.Off() @@ -113,7 +114,7 @@ def test_protocols(self): # check that LE offsets are recalculated on simulation time change for time in [0., 2.3]: system.time = time - expected_pos = params_osc['amplitude'] * \ + expected_pos = params_osc['initial_pos_offset'] + params_osc['amplitude'] * \ np.sin(params_osc['omega'] * (time - params_osc['time_0'])) expected_vel = params_osc['amplitude'] * params_osc['omega'] * \ np.cos(params_osc['omega'] * (time - params_osc['time_0'])) @@ -124,7 +125,7 @@ def test_protocols(self): # Check that time change during integration updates offsets system.integrator.run(1) time = system.time - expected_pos = params_osc['amplitude'] * \ + expected_pos = params_osc['initial_pos_offset'] + params_osc['amplitude'] * \ np.sin(params_osc['omega'] * (time - params_osc['time_0'])) expected_vel = params_osc['amplitude'] * params_osc['omega'] * \ np.cos(params_osc['omega'] * (time - params_osc['time_0'])) From b3f442423601d9bc92f3efda0a3addf432201ea1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jean-No=C3=ABl=20Grad?= Date: Tue, 5 Apr 2022 02:42:12 +0200 Subject: [PATCH 09/10] CI: Use new CI infrastructure Improve CPU/GPU load balancing by restricting CI jobs that don't need a GPU on CI runners that don't provide a GPU. Use 4 cores instead of 2 cores when the runner allows it. Execute slow jobs on NUMA runners. --- .github/workflows/push_pull.yml | 4 +- .gitlab-ci.yml | 72 ++++++++++++++++++++++----------- maintainer/CI/build_cmake.sh | 8 +++- 3 files changed, 57 insertions(+), 27 deletions(-) diff --git a/.github/workflows/push_pull.yml b/.github/workflows/push_pull.yml index 3d8f0376700..26dfebcad9f 100644 --- a/.github/workflows/push_pull.yml +++ b/.github/workflows/push_pull.yml @@ -16,7 +16,7 @@ jobs: - name: Setup Python environment uses: actions/setup-python@v2 with: - python-version: '3.7' + python-version: '3.8' - name: Check without sanitizer uses: ./.github/actions/build_and_check with: @@ -33,7 +33,7 @@ jobs: - name: Setup Python environment uses: actions/setup-python@v2 with: - python-version: '3.7' + python-version: '3.8' - name: Check with sanitizer uses: ./.github/actions/build_and_check with: diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index c8a54090ae1..2fec11330c3 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -21,7 +21,9 @@ stages: timeout: 40m interruptible: false tags: - - linux + - docker + - espresso + - no-cuda variables: GIT_SUBMODULE_STRATEGY: recursive @@ -44,7 +46,8 @@ style: - sh maintainer/CI/fix_style.sh tags: - docker - - linux + - espresso + - no-cuda variables: GIT_SUBMODULE_STRATEGY: none artifacts: @@ -65,7 +68,8 @@ style_doxygen: - sh ../maintainer/CI/dox_warnings.sh tags: - docker - - linux + - espresso + - no-cuda ### Builds without CUDA @@ -85,7 +89,8 @@ default: - bash maintainer/CI/build_cmake.sh tags: - docker - - linux + - espresso + - no-cuda maxset: <<: *global_job_definition @@ -105,7 +110,9 @@ maxset: - bash maintainer/CI/build_cmake.sh tags: - docker - - linux + - espresso + - no-cuda + - numa no_rotation: <<: *global_job_definition @@ -122,7 +129,9 @@ no_rotation: - bash maintainer/CI/build_cmake.sh tags: - docker - - linux + - espresso + - no-cuda + - numa ubuntu:wo-dependencies: <<: *global_job_definition @@ -138,7 +147,8 @@ ubuntu:wo-dependencies: - bash maintainer/CI/build_cmake.sh tags: - docker - - linux + - espresso + - no-cuda ### Builds with different distributions @@ -155,7 +165,8 @@ debian:10: - bash maintainer/CI/build_cmake.sh tags: - docker - - linux + - espresso + - no-cuda fedora:34: <<: *global_job_definition @@ -170,7 +181,8 @@ fedora:34: - bash maintainer/CI/build_cmake.sh tags: - docker - - linux + - espresso + - no-cuda ### Builds with CUDA @@ -195,8 +207,9 @@ clang-sanitizer: timeout: 2h tags: - docker - - linux + - espresso - cuda + - numa fast_math: <<: *global_job_definition @@ -215,7 +228,7 @@ fast_math: - bash maintainer/CI/build_cmake.sh tags: - docker - - linux + - espresso - cuda when: manual @@ -238,8 +251,9 @@ cuda11-maxset: - bash maintainer/CI/build_cmake.sh tags: - docker - - linux + - espresso - cuda + - numa cuda10-maxset: <<: *global_job_definition @@ -263,8 +277,9 @@ cuda10-maxset: expire_in: 1 week tags: - docker - - linux + - espresso - cuda + - numa tutorials-samples-maxset: <<: *global_job_definition @@ -287,7 +302,7 @@ tutorials-samples-maxset: - bash maintainer/CI/build_cmake.sh tags: - docker - - linux + - espresso - cuda tutorials-samples-default: @@ -310,7 +325,7 @@ tutorials-samples-default: - bash maintainer/CI/build_cmake.sh tags: - docker - - linux + - espresso - cuda only: - schedules @@ -336,7 +351,7 @@ tutorials-samples-empty: - bash maintainer/CI/build_cmake.sh tags: - docker - - linux + - espresso - cuda only: - schedules @@ -362,7 +377,8 @@ tutorials-samples-no-gpu: - bash maintainer/CI/build_cmake.sh tags: - docker - - linux + - espresso + - no-cuda only: - schedules @@ -397,7 +413,7 @@ installation: - make -j2 check_tutorials tags: - docker - - linux + - espresso - cuda when: manual @@ -417,8 +433,9 @@ empty: - bash maintainer/CI/build_cmake.sh tags: - docker - - linux + - espresso - cuda + - numa check_sphinx: <<: *global_job_definition @@ -440,8 +457,9 @@ check_sphinx: expire_in: 1 week tags: - docker - - linux + - espresso - cuda + - numa run_tutorials: <<: *global_job_definition @@ -464,8 +482,9 @@ run_tutorials: expire_in: 1 week tags: - docker - - linux + - espresso - cuda + - numa only: - schedules @@ -486,7 +505,9 @@ run_doxygen: expire_in: 1 week tags: - docker - - linux + - espresso + - no-cuda + - numa maxset_no_gpu: <<: *global_job_definition @@ -500,7 +521,9 @@ maxset_no_gpu: - make -t && make check tags: - docker - - linux + - espresso + - no-cuda + - numa maxset_3_cores: <<: *global_job_definition @@ -514,8 +537,9 @@ maxset_3_cores: - make -t && make check_unit_tests && make check_python_parallel_odd tags: - docker - - linux + - espresso - cuda + - numa status_success: <<: *notification_job_definition diff --git a/maintainer/CI/build_cmake.sh b/maintainer/CI/build_cmake.sh index 91b44886c75..29a553b8f54 100755 --- a/maintainer/CI/build_cmake.sh +++ b/maintainer/CI/build_cmake.sh @@ -72,6 +72,12 @@ set_default_value() { fi } +# the number of available processors depends on the CI runner +if grep -q "i7-3820" /proc/cpuinfo; then + ci_procs=2 +else + ci_procs=4 +fi # handle environment variables set_default_value srcdir "$(pwd)" @@ -82,7 +88,7 @@ set_default_value with_ubsan false set_default_value with_asan false set_default_value with_static_analysis false set_default_value myconfig "default" -set_default_value build_procs 2 +set_default_value build_procs ${ci_procs} set_default_value check_procs ${build_procs} set_default_value check_odd_only false set_default_value check_gpu_only false From de529f7141713661fc699ebc3dd9903d39ccb5fe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jean-No=C3=ABl=20Grad?= Date: Tue, 5 Apr 2022 16:24:22 +0200 Subject: [PATCH 10/10] CI: Remove flaky test on macOS --- testsuite/python/script_interface.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/testsuite/python/script_interface.py b/testsuite/python/script_interface.py index dc3c0c7294f..b762063db21 100644 --- a/testsuite/python/script_interface.py +++ b/testsuite/python/script_interface.py @@ -105,8 +105,6 @@ def test_variant_exceptions(self): espressomd.constraints.ShapeBasedConstraint(shape=ut) with self.assertRaisesRegex(TypeError, "No conversion from type 'module' to 'Variant'"): constraint.set_params(shape=ut) - with self.assertRaisesRegex(TypeError, "No conversion from type 'module' to 'Variant'"): - constraint.call_method('unknown', unknown=ut) # check restrictions on the dict type with self.assertRaisesRegex(TypeError, r"No conversion from type 'dict_item\(\[\(str, int\)\]\)' to 'Variant\[std::(__1::)?unordered_map\]'"): constraint.shape = {'1': 2}