From 9550aa1f24b215c090717a6b870b293293e9f535 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jean-No=C3=ABl=20Grad?= Date: Wed, 10 Jan 2024 23:33:01 +0100 Subject: [PATCH] Encapsulate thermostats API changes: thermalized bond random number generator seed moved to the thermostat class to avoid accidentally modifying the global seed, LB thermostat seed parameter is now interpreted as the RNG seed instead of the RNG Philox counter. --- doc/sphinx/integration.rst | 15 +- doc/sphinx/inter_bonded.rst | 3 +- doc/tutorials/polymers/polymers.ipynb | 5 +- samples/dancing.py | 1 + samples/drude_bmimpf6.py | 3 +- samples/load_checkpoint.py | 4 - src/core/CMakeLists.txt | 1 - src/core/PropagationMode.hpp | 20 +- .../thermalized_bond_kernel.hpp | 4 +- src/core/cell_system/CellStructure.cpp | 25 +- src/core/cell_system/HybridDecomposition.cpp | 12 +- src/core/cell_system/HybridDecomposition.hpp | 8 +- src/core/constraints/ShapeBasedConstraint.cpp | 18 +- src/core/dpd.cpp | 14 +- src/core/dpd.hpp | 16 +- src/core/ek/EKNone.hpp | 1 + src/core/ek/EKWalberla.cpp | 9 + src/core/ek/EKWalberla.hpp | 9 +- src/core/ek/Solver.cpp | 6 + src/core/ek/Solver.hpp | 5 + src/core/energy.cpp | 3 +- src/core/forces.cpp | 22 +- src/core/forces_inline.hpp | 8 +- src/core/ghosts.cpp | 36 +- src/core/ghosts.hpp | 14 +- src/core/global_ghost_flags.cpp | 57 -- src/core/integrate.cpp | 94 ++- src/core/integrators/Propagation.hpp | 2 +- .../integrators/stokesian_dynamics_inline.hpp | 6 +- src/core/integrators/velocity_verlet_npt.cpp | 32 +- src/core/integrators/velocity_verlet_npt.hpp | 9 +- src/core/lb/LBNone.hpp | 1 + src/core/lb/LBWalberla.cpp | 11 + src/core/lb/LBWalberla.hpp | 9 +- src/core/lb/Solver.cpp | 7 + src/core/lb/Solver.hpp | 5 + src/core/lb/particle_coupling.cpp | 180 ++--- src/core/lb/particle_coupling.hpp | 100 +-- src/core/stokesian_dynamics/sd_interface.cpp | 17 +- src/core/stokesian_dynamics/sd_interface.hpp | 7 +- src/core/system/System.cpp | 50 +- src/core/system/System.hpp | 17 +- src/core/system/System.impl.hpp | 1 + src/core/thermostat.cpp | 192 +---- src/core/thermostat.hpp | 237 +++--- src/core/thermostats/brownian_inline.hpp | 4 +- src/core/thermostats/langevin_inline.hpp | 12 +- .../EspressoSystemStandAlone_test.cpp | 12 + src/core/unit_tests/Verlet_list_test.cpp | 15 +- src/core/unit_tests/ek_interface_test.cpp | 23 +- .../unit_tests/lb_particle_coupling_test.cpp | 158 ++-- src/core/unit_tests/thermostats_test.cpp | 4 +- src/core/virtual_sites/lb_tracers.cpp | 57 +- src/core/virtual_sites/lb_tracers.hpp | 10 +- src/python/espressomd/interactions.py | 18 +- src/python/espressomd/system.py | 1 - src/python/espressomd/thermostat.pxd | 122 --- src/python/espressomd/thermostat.py | 187 +++++ src/python/espressomd/thermostat.pyx | 663 ---------------- src/script_interface/CMakeLists.txt | 1 + src/script_interface/initialize.cpp | 2 + .../interactions/BondedInteraction.hpp | 40 - src/script_interface/system/System.cpp | 3 + .../thermostat/CMakeLists.txt | 20 + .../thermostat/initialize.cpp | 47 ++ .../thermostat/initialize.hpp} | 16 +- .../thermostat/thermostat.hpp | 731 ++++++++++++++++++ src/utils/include/utils/Counter.hpp | 5 +- .../reactions/EKReactionBase.hpp | 4 +- testsuite/python/CMakeLists.txt | 2 +- testsuite/python/dpd.py | 21 +- testsuite/python/drude.py | 3 +- testsuite/python/ek_interface.py | 7 +- testsuite/python/field_test.py | 10 +- testsuite/python/integrator_exceptions.py | 76 ++ .../python/interactions_bonded_interface.py | 8 - testsuite/python/lb.py | 19 +- testsuite/python/lb_thermostat.py | 6 +- testsuite/python/long_range_actors.py | 3 + testsuite/python/propagation_langevin.py | 47 +- testsuite/python/propagation_lb.py | 104 ++- testsuite/python/propagation_npt.py | 14 +- testsuite/python/propagation_stokesian.py | 20 +- testsuite/python/save_checkpoint.py | 45 +- testsuite/python/test_checkpoint.py | 114 ++- testsuite/python/thermalized_bond.py | 35 +- testsuite/python/virtual_sites_relative.py | 23 +- .../python/virtual_sites_tracers_common.py | 10 +- testsuite/scripts/tutorials/test_polymers.py | 2 +- 89 files changed, 2164 insertions(+), 1856 deletions(-) delete mode 100644 src/core/global_ghost_flags.cpp delete mode 100644 src/python/espressomd/thermostat.pxd create mode 100644 src/python/espressomd/thermostat.py delete mode 100644 src/python/espressomd/thermostat.pyx create mode 100644 src/script_interface/thermostat/CMakeLists.txt create mode 100644 src/script_interface/thermostat/initialize.cpp rename src/{core/global_ghost_flags.hpp => script_interface/thermostat/initialize.hpp} (71%) create mode 100644 src/script_interface/thermostat/thermostat.hpp diff --git a/doc/sphinx/integration.rst b/doc/sphinx/integration.rst index 9e2a4e3967..2c82f18404 100644 --- a/doc/sphinx/integration.rst +++ b/doc/sphinx/integration.rst @@ -401,15 +401,14 @@ To add a thermostat, call the appropriate setter:: The different thermostats available in |es| will be described in the following subsections. -You may combine different thermostats at your own risk by turning them on -one by one. The list of active thermostats can be cleared at any time with +You may combine different thermostats by turning them on sequentially. +Not all combinations of thermostats are sensible, though, and some +integrators only work with a specific thermostat. The list of possible +combinations of integrators and thermostats is hardcoded and automatically +check against at the start of integration. +Note that there is only one temperature for all thermostats. +The list of active thermostats can be cleared at any time with :py:meth:`system.thermostat.turn_off() `. -Not all combinations of thermostats are allowed, though (see -:py:func:`espressomd.thermostat.AssertThermostatType` for details). -Some integrators only work with a specific thermostat and throw an -error otherwise. Note that there is only one temperature for all -thermostats, although for some thermostats like the Langevin thermostat, -particles can be assigned individual temperatures. Since |es| does not enforce a particular unit system, it cannot know about the current value of the Boltzmann constant. Therefore, when specifying diff --git a/doc/sphinx/inter_bonded.rst b/doc/sphinx/inter_bonded.rst index e56a710222..890224f345 100644 --- a/doc/sphinx/inter_bonded.rst +++ b/doc/sphinx/inter_bonded.rst @@ -197,8 +197,9 @@ A thermalized bond can be instantiated via thermalized_bond = espressomd.interactions.ThermalizedBond( temp_com=, gamma_com=, temp_distance=, gamma_distance=, - r_cut=, seed=) + r_cut=) system.bonded_inter.add(thermalized_bond) + system.thermostat.set_thermalized_bond(seed=) This bond can be used to apply Langevin thermalization on the centre of mass and the distance of a particle pair. Each thermostat can have its own diff --git a/doc/tutorials/polymers/polymers.ipynb b/doc/tutorials/polymers/polymers.ipynb index ed3ed3bb8d..18320e7aee 100644 --- a/doc/tutorials/polymers/polymers.ipynb +++ b/doc/tutorials/polymers/polymers.ipynb @@ -307,7 +307,7 @@ " kinematic_viscosity=5, tau=system.time_step,\n", " single_precision=True)\n", " system.lb = lbf\n", - " system.thermostat.set_lb(LB_fluid=lbf, gamma=gamma, seed=42)" + " system.thermostat.set_lb(LB_fluid=lbf, gamma=gamma, seed=0)" ] }, { @@ -326,6 +326,7 @@ "outputs": [], "source": [ "import logging\n", + "import tqdm\n", "import sys\n", "\n", "import numpy as np\n", @@ -419,7 +420,7 @@ " rhs = np.zeros(LOOPS)\n", " rfs = np.zeros(LOOPS)\n", " rgs = np.zeros(LOOPS)\n", - " for i in range(LOOPS):\n", + " for i in tqdm.trange(LOOPS):\n", " system.integrator.run(STEPS)\n", " rhs[i] = system.analysis.calc_rh(\n", " chain_start=0,\n", diff --git a/samples/dancing.py b/samples/dancing.py index 3404d1bd7e..923334bfe6 100644 --- a/samples/dancing.py +++ b/samples/dancing.py @@ -49,6 +49,7 @@ system.cell_system.skin = 0.4 system.periodicity = [False, False, False] +system.thermostat.set_stokesian(kT=0.) system.integrator.set_stokesian_dynamics( viscosity=1.0, radii={0: 1.0}, approximation_method=sd_method) diff --git a/samples/drude_bmimpf6.py b/samples/drude_bmimpf6.py index 8579c48057..fa07f15643 100644 --- a/samples/drude_bmimpf6.py +++ b/samples/drude_bmimpf6.py @@ -258,10 +258,11 @@ def combination_rule_sigma(rule, sig1, sig2): if args.drude: print("-->Adding Drude related bonds") + system.thermostat.set_thermalized_bond(seed=123) thermalized_dist_bond = espressomd.interactions.ThermalizedBond( temp_com=temperature_com, gamma_com=gamma_com, temp_distance=temperature_drude, gamma_distance=gamma_drude, - r_cut=min(lj_sigmas.values()) * 0.5, seed=123) + r_cut=min(lj_sigmas.values()) * 0.5) harmonic_bond = espressomd.interactions.HarmonicBond( k=k_drude, r_0=0.0, r_cut=1.0) system.bonded_inter.add(thermalized_dist_bond) diff --git a/samples/load_checkpoint.py b/samples/load_checkpoint.py index 662d95e9c0..0c6f845dfd 100644 --- a/samples/load_checkpoint.py +++ b/samples/load_checkpoint.py @@ -60,10 +60,6 @@ print("\n### system.part test ###") print(f"system.part.all().pos = {system.part.all().pos}") -# test "system.thermostat" -print("\n### system.thermostat test ###") -print(f"system.thermostat.get_state() = {system.thermostat.get_state()}") - # test "p3m" print("\n### p3m test ###") print(f"p3m.get_params() = {p3m.get_params()}") diff --git a/src/core/CMakeLists.txt b/src/core/CMakeLists.txt index 6a53208bea..6f93be1162 100644 --- a/src/core/CMakeLists.txt +++ b/src/core/CMakeLists.txt @@ -32,7 +32,6 @@ add_library( errorhandling.cpp forces.cpp ghosts.cpp - global_ghost_flags.cpp immersed_boundaries.cpp integrate.cpp npt.cpp diff --git a/src/core/PropagationMode.hpp b/src/core/PropagationMode.hpp index ff87400f10..61ef7c70b7 100644 --- a/src/core/PropagationMode.hpp +++ b/src/core/PropagationMode.hpp @@ -20,9 +20,7 @@ #pragma once namespace PropagationMode { -/** - * @brief Flags to create bitmasks for propagation modes. - */ +/** @brief Flags to create bitmasks for propagation modes. */ enum PropagationMode : int { NONE = 0, SYSTEM_DEFAULT = 1 << 0, @@ -42,11 +40,23 @@ enum PropagationMode : int { }; } // namespace PropagationMode -/** \name Integrator switches */ +/** @brief Integrator identifier. */ enum IntegratorSwitch : int { INTEG_METHOD_NPT_ISO = 0, INTEG_METHOD_NVT = 1, INTEG_METHOD_STEEPEST_DESCENT = 2, INTEG_METHOD_BD = 3, - INTEG_METHOD_SD = 7, + INTEG_METHOD_SD = 4, +}; + +/** @brief Thermostat flags. */ +enum ThermostatFlags : int { + THERMO_OFF = 0, + THERMO_LANGEVIN = 1 << 0, + THERMO_BROWNIAN = 1 << 1, + THERMO_NPT_ISO = 1 << 2, + THERMO_LB = 1 << 3, + THERMO_SD = 1 << 4, + THERMO_DPD = 1 << 5, + THERMO_BOND = 1 << 6, }; diff --git a/src/core/bonded_interactions/thermalized_bond_kernel.hpp b/src/core/bonded_interactions/thermalized_bond_kernel.hpp index d8585a70e5..6f0dc132f2 100644 --- a/src/core/bonded_interactions/thermalized_bond_kernel.hpp +++ b/src/core/bonded_interactions/thermalized_bond_kernel.hpp @@ -25,6 +25,7 @@ #include "Particle.hpp" #include "random.hpp" +#include "system/System.hpp" #include "thermostat.hpp" #include @@ -54,8 +55,9 @@ ThermalizedBond::forces(Particle const &p1, Particle const &p2, auto const sqrt_mass_red = sqrt(p1.mass() * p2.mass() / mass_tot); auto const com_vel = mass_tot_inv * (p1.mass() * p1.v() + p2.mass() * p2.v()); auto const dist_vel = p2.v() - p1.v(); + auto const &thermalized_bond = + *::System::get_system().thermostat->thermalized_bond; - extern ThermalizedBondThermostat thermalized_bond; Utils::Vector3d force1{}; Utils::Vector3d force2{}; auto const noise = Random::noise_uniform( diff --git a/src/core/cell_system/CellStructure.cpp b/src/core/cell_system/CellStructure.cpp index 8b3f35d5e1..8913d29376 100644 --- a/src/core/cell_system/CellStructure.cpp +++ b/src/core/cell_system/CellStructure.cpp @@ -171,33 +171,33 @@ unsigned map_data_parts(unsigned data_parts) { /* clang-format off */ return GHOSTTRANS_NONE - | ((DATA_PART_PROPERTIES & data_parts) ? GHOSTTRANS_PROPRTS : 0u) - | ((DATA_PART_POSITION & data_parts) ? GHOSTTRANS_POSITION : 0u) - | ((DATA_PART_MOMENTUM & data_parts) ? GHOSTTRANS_MOMENTUM : 0u) - | ((DATA_PART_FORCE & data_parts) ? GHOSTTRANS_FORCE : 0u) + | ((data_parts & DATA_PART_PROPERTIES) ? GHOSTTRANS_PROPRTS : 0u) + | ((data_parts & DATA_PART_POSITION) ? GHOSTTRANS_POSITION : 0u) + | ((data_parts & DATA_PART_MOMENTUM) ? GHOSTTRANS_MOMENTUM : 0u) + | ((data_parts & DATA_PART_FORCE) ? GHOSTTRANS_FORCE : 0u) #ifdef BOND_CONSTRAINT - | ((DATA_PART_RATTLE & data_parts) ? GHOSTTRANS_RATTLE : 0u) + | ((data_parts & DATA_PART_RATTLE) ? GHOSTTRANS_RATTLE : 0u) #endif - | ((DATA_PART_BONDS & data_parts) ? GHOSTTRANS_BONDS : 0u); + | ((data_parts & DATA_PART_BONDS) ? GHOSTTRANS_BONDS : 0u); /* clang-format on */ } void CellStructure::ghosts_count() { ghost_communicator(decomposition().exchange_ghosts_comm(), - GHOSTTRANS_PARTNUM); + *get_system().box_geo, GHOSTTRANS_PARTNUM); } void CellStructure::ghosts_update(unsigned data_parts) { ghost_communicator(decomposition().exchange_ghosts_comm(), - map_data_parts(data_parts)); + *get_system().box_geo, map_data_parts(data_parts)); } void CellStructure::ghosts_reduce_forces() { ghost_communicator(decomposition().collect_ghost_force_comm(), - GHOSTTRANS_FORCE); + *get_system().box_geo, GHOSTTRANS_FORCE); } #ifdef BOND_CONSTRAINT void CellStructure::ghosts_reduce_rattle_correction() { ghost_communicator(decomposition().collect_ghost_force_comm(), - GHOSTTRANS_RATTLE); + *get_system().box_geo, GHOSTTRANS_RATTLE); } #endif @@ -265,8 +265,9 @@ void CellStructure::set_hybrid_decomposition(double cutoff_regular, auto &local_geo = *system.local_geo; auto const &box_geo = *system.box_geo; set_particle_decomposition(std::make_unique( - ::comm_cart, cutoff_regular, m_verlet_skin, box_geo, local_geo, - n_square_types)); + ::comm_cart, cutoff_regular, m_verlet_skin, + [&system]() { return system.get_global_ghost_flags(); }, box_geo, + local_geo, n_square_types)); m_type = CellStructureType::HYBRID; local_geo.set_cell_structure_type(m_type); system.on_cell_structure_change(); diff --git a/src/core/cell_system/HybridDecomposition.cpp b/src/core/cell_system/HybridDecomposition.cpp index a503f77e5e..a54002f8df 100644 --- a/src/core/cell_system/HybridDecomposition.cpp +++ b/src/core/cell_system/HybridDecomposition.cpp @@ -26,7 +26,6 @@ #include "BoxGeometry.hpp" #include "LocalBox.hpp" -#include "global_ghost_flags.hpp" #include #include @@ -36,12 +35,14 @@ #include #include +#include #include #include #include HybridDecomposition::HybridDecomposition(boost::mpi::communicator comm, double cutoff_regular, double skin, + std::function get_ghost_flags, BoxGeometry const &box_geo, LocalBox const &local_box, std::set n_square_types) @@ -49,7 +50,8 @@ HybridDecomposition::HybridDecomposition(boost::mpi::communicator comm, m_regular_decomposition(RegularDecomposition( m_comm, cutoff_regular + skin, m_box, local_box)), m_n_square(AtomDecomposition(m_comm, m_box)), - m_n_square_types(std::move(n_square_types)) { + m_n_square_types(std::move(n_square_types)), + m_get_global_ghost_flags(std::move(get_ghost_flags)) { /* Vector containing cells of both child decompositions */ m_local_cells = m_regular_decomposition.get_local_cells(); @@ -155,11 +157,11 @@ void HybridDecomposition::resort(bool global, m_n_square.resort(global, diff); /* basically do CellStructure::ghost_count() */ - ghost_communicator(exchange_ghosts_comm(), GHOSTTRANS_PARTNUM); + ghost_communicator(exchange_ghosts_comm(), m_box, GHOSTTRANS_PARTNUM); /* basically do CellStructure::ghost_update(unsigned data_parts) */ - ghost_communicator(exchange_ghosts_comm(), - map_data_parts(global_ghost_flags())); + ghost_communicator(exchange_ghosts_comm(), m_box, + map_data_parts(m_get_global_ghost_flags())); } std::size_t HybridDecomposition::count_particles( diff --git a/src/core/cell_system/HybridDecomposition.hpp b/src/core/cell_system/HybridDecomposition.hpp index c8993027b7..ce73c8cd7f 100644 --- a/src/core/cell_system/HybridDecomposition.hpp +++ b/src/core/cell_system/HybridDecomposition.hpp @@ -39,6 +39,7 @@ #include #include +#include #include #include #include @@ -69,14 +70,17 @@ class HybridDecomposition : public ParticleDecomposition { /** Set containing the types that should be handled using n_square */ std::set const m_n_square_types; + std::function m_get_global_ghost_flags; + bool is_n_square_type(int type_id) const { return (m_n_square_types.find(type_id) != m_n_square_types.end()); } public: HybridDecomposition(boost::mpi::communicator comm, double cutoff_regular, - double skin, BoxGeometry const &box_geo, - LocalBox const &local_box, std::set n_square_types); + double skin, std::function get_ghost_flags, + BoxGeometry const &box_geo, LocalBox const &local_box, + std::set n_square_types); auto get_cell_grid() const { return m_regular_decomposition.cell_grid; } diff --git a/src/core/constraints/ShapeBasedConstraint.cpp b/src/core/constraints/ShapeBasedConstraint.cpp index 0122f37d8c..555a22fc0d 100644 --- a/src/core/constraints/ShapeBasedConstraint.cpp +++ b/src/core/constraints/ShapeBasedConstraint.cpp @@ -101,11 +101,12 @@ ParticleForce ShapeBasedConstraint::force(Particle const &p, calc_non_central_force(p, part_rep, ia_params, dist_vec, dist); #ifdef DPD - if (thermo_switch & THERMO_DPD) { - dpd_force = - dpd_pair_force(p, part_rep, ia_params, dist_vec, dist, dist * dist); + if (m_system.thermostat->thermo_switch & THERMO_DPD) { + dpd_force = dpd_pair_force(p, part_rep, *m_system.thermostat->dpd, + *m_system.box_geo, ia_params, dist_vec, dist, + dist * dist); // Additional use of DPD here requires counter increase - dpd.rng_increment(); + m_system.thermostat->dpd->rng_increment(); } #endif } else if (m_penetrable && (dist <= 0)) { @@ -117,11 +118,12 @@ ParticleForce ShapeBasedConstraint::force(Particle const &p, calc_non_central_force(p, part_rep, ia_params, dist_vec, -dist); #ifdef DPD - if (thermo_switch & THERMO_DPD) { - dpd_force = dpd_pair_force(p, part_rep, ia_params, dist_vec, dist, - dist * dist); + if (m_system.thermostat->thermo_switch & THERMO_DPD) { + dpd_force = dpd_pair_force(p, part_rep, *m_system.thermostat->dpd, + *m_system.box_geo, ia_params, dist_vec, + dist, dist * dist); // Additional use of DPD here requires counter increase - dpd.rng_increment(); + m_system.thermostat->dpd->rng_increment(); } #endif } diff --git a/src/core/dpd.cpp b/src/core/dpd.cpp index 9ee236f798..5885eeb53c 100644 --- a/src/core/dpd.cpp +++ b/src/core/dpd.cpp @@ -55,7 +55,7 @@ * 3. Two particle IDs (order-independent, decorrelates particles, gets rid of * seed-per-node) */ -Utils::Vector3d dpd_noise(int pid1, int pid2) { +Utils::Vector3d dpd_noise(DPDThermostat const &dpd, int pid1, int pid2) { return Random::noise_uniform( dpd.rng_counter(), dpd.rng_seed(), (pid1 < pid2) ? pid2 : pid1, (pid1 < pid2) ? pid1 : pid2); @@ -99,21 +99,19 @@ Utils::Vector3d dpd_pair_force(DPDParameters const ¶ms, return {}; } -Utils::Vector3d dpd_pair_force(Particle const &p1, Particle const &p2, - IA_parameters const &ia_params, - Utils::Vector3d const &d, double dist, - double dist2) { +Utils::Vector3d +dpd_pair_force(Particle const &p1, Particle const &p2, DPDThermostat const &dpd, + BoxGeometry const &box_geo, IA_parameters const &ia_params, + Utils::Vector3d const &d, double dist, double dist2) { if (ia_params.dpd.radial.cutoff <= 0.0 && ia_params.dpd.trans.cutoff <= 0.0) { return {}; } - auto const &box_geo = *System::get_system().box_geo; - auto const v21 = box_geo.velocity_difference(p1.pos(), p2.pos(), p1.v(), p2.v()); auto const noise_vec = (ia_params.dpd.radial.pref > 0.0 || ia_params.dpd.trans.pref > 0.0) - ? dpd_noise(p1.id(), p2.id()) + ? dpd_noise(dpd, p1.id(), p2.id()) : Utils::Vector3d{}; auto const f_r = dpd_pair_force(ia_params.dpd.radial, v21, dist, noise_vec); diff --git a/src/core/dpd.hpp b/src/core/dpd.hpp index d464e3aa98..1de9f42566 100644 --- a/src/core/dpd.hpp +++ b/src/core/dpd.hpp @@ -18,8 +18,9 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -#ifndef ESPRESSO_SRC_CORE_DPD_HPP -#define ESPRESSO_SRC_CORE_DPD_HPP + +#pragma once + /** \file * Routines to use DPD as thermostat or pair force @cite soddemann03a * @@ -30,7 +31,9 @@ #ifdef DPD +#include "BoxGeometry.hpp" #include "Particle.hpp" +#include "thermostat.hpp" #include @@ -43,11 +46,10 @@ struct IA_parameters; void dpd_init(double kT, double time_step); -Utils::Vector3d dpd_pair_force(Particle const &p1, Particle const &p2, - IA_parameters const &ia_params, - Utils::Vector3d const &d, double dist, - double dist2); +Utils::Vector3d +dpd_pair_force(Particle const &p1, Particle const &p2, DPDThermostat const &dpd, + BoxGeometry const &box_geo, IA_parameters const &ia_params, + Utils::Vector3d const &d, double dist, double dist2); Utils::Vector9d dpd_stress(boost::mpi::communicator const &comm); #endif // DPD -#endif diff --git a/src/core/ek/EKNone.hpp b/src/core/ek/EKNone.hpp index 09c643bc17..33a4cd6706 100644 --- a/src/core/ek/EKNone.hpp +++ b/src/core/ek/EKNone.hpp @@ -32,6 +32,7 @@ struct EKNone { void propagate() { throw NoEKActive{}; } double get_tau() const { throw NoEKActive{}; } void veto_time_step(double) const { throw NoEKActive{}; } + void veto_kT(double) const { throw NoEKActive{}; } void sanity_checks(System::System const &) const { throw NoEKActive{}; } void on_cell_structure_change() const { throw NoEKActive{}; } void on_boxl_change() const { throw NoEKActive{}; } diff --git a/src/core/ek/EKWalberla.cpp b/src/core/ek/EKWalberla.cpp index 1e23881178..50ecb5595d 100644 --- a/src/core/ek/EKWalberla.cpp +++ b/src/core/ek/EKWalberla.cpp @@ -37,6 +37,8 @@ #include #include +#include +#include #include namespace EK { @@ -110,6 +112,13 @@ void EKWalberla::veto_time_step(double time_step) const { walberla_tau_sanity_checks("EK", ek_container->get_tau(), time_step); } +void EKWalberla::veto_kT(double) const { + if (not ek_container->empty()) { + // can only throw, because without agrid, we can't do the unit conversion + throw std::runtime_error("Temperature change not supported by EK"); + } +} + void EKWalberla::sanity_checks(System::System const &system) const { auto const &box_geo = *system.box_geo; auto const &lattice = ek_container->get_lattice(); diff --git a/src/core/ek/EKWalberla.hpp b/src/core/ek/EKWalberla.hpp index 603b82630e..32243869ba 100644 --- a/src/core/ek/EKWalberla.hpp +++ b/src/core/ek/EKWalberla.hpp @@ -55,6 +55,7 @@ struct EKWalberla { double get_tau() const; void veto_time_step(double time_step) const; + void veto_kT(double kT) const; void sanity_checks(System::System const &system) const; bool is_ready_for_propagation() const noexcept; void propagate(); @@ -67,12 +68,8 @@ struct EKWalberla { void on_node_grid_change() const { throw std::runtime_error("MPI topology change not supported by EK"); } - void on_timestep_change() const { - throw std::runtime_error("Time step change not supported by EK"); - } - void on_temperature_change() const { - throw std::runtime_error("Temperature change not supported by EK"); - } + void on_timestep_change() const {} + void on_temperature_change() const {} }; } // namespace EK diff --git a/src/core/ek/Solver.cpp b/src/core/ek/Solver.cpp index 2de532a762..7190ce57d9 100644 --- a/src/core/ek/Solver.cpp +++ b/src/core/ek/Solver.cpp @@ -84,6 +84,12 @@ void Solver::veto_time_step(double time_step) const { } } +void Solver::veto_kT(double kT) const { + if (impl->solver) { + std::visit([=](auto &ptr) { ptr->veto_kT(kT); }, *impl->solver); + } +} + void Solver::on_cell_structure_change() { if (impl->solver) { auto &solver = *impl->solver; diff --git a/src/core/ek/Solver.hpp b/src/core/ek/Solver.hpp index 634b1b8b51..d5cc229d9d 100644 --- a/src/core/ek/Solver.hpp +++ b/src/core/ek/Solver.hpp @@ -85,6 +85,11 @@ struct Solver : public System::Leaf { */ void veto_time_step(double time_step) const; + /** + * @brief Check if a thermostat is compatible with the EK temperature. + */ + void veto_kT(double kT) const; + void on_boxl_change(); void on_node_grid_change(); void on_cell_structure_change(); diff --git a/src/core/energy.cpp b/src/core/energy.cpp index ae66fd23e5..3f8e21f327 100644 --- a/src/core/energy.cpp +++ b/src/core/energy.cpp @@ -24,7 +24,6 @@ #include "cell_system/CellStructure.hpp" #include "constraints.hpp" #include "energy_inline.hpp" -#include "global_ghost_flags.hpp" #include "nonbonded_interactions/nonbonded_interaction_data.hpp" #include "short_range_loop.hpp" #include "system/System.hpp" @@ -115,7 +114,7 @@ std::shared_ptr System::calculate_energy() { double System::particle_short_range_energy_contribution(int pid) { if (cell_structure->get_resort_particles()) { - cell_structure->update_ghosts_and_resort_particle(global_ghost_flags()); + cell_structure->update_ghosts_and_resort_particle(get_global_ghost_flags()); } auto ret = 0.0; diff --git a/src/core/forces.cpp b/src/core/forces.cpp index 1bc21bffcb..5ce4aee368 100644 --- a/src/core/forces.cpp +++ b/src/core/forces.cpp @@ -27,6 +27,7 @@ #include "BoxGeometry.hpp" #include "Particle.hpp" #include "ParticleRange.hpp" +#include "PropagationMode.hpp" #include "bond_breakage/bond_breakage.hpp" #include "cell_system/CellStructure.hpp" #include "cells.hpp" @@ -48,7 +49,6 @@ #include "short_range_loop.hpp" #include "system/System.hpp" #include "thermostat.hpp" -#include "thermostats/langevin_inline.hpp" #include "virtual_sites/relative.hpp" #include @@ -117,7 +117,7 @@ static void force_capping(ParticleRange const &particles, double force_cap) { } } -void System::System::calculate_forces(double kT) { +void System::System::calculate_forces() { #ifdef CALIPER CALI_CXX_MARK_FUNCTION; #endif @@ -149,7 +149,7 @@ void System::System::calculate_forces(double kT) { npt_reset_instantaneous_virials(); #endif init_forces(particles, ghost_particles); - thermostats_force_init(kT); + thermostat_force_init(); calc_long_range_forces(particles); @@ -178,13 +178,14 @@ void System::System::calculate_forces(double kT) { }, [coulomb_kernel_ptr = get_ptr(coulomb_kernel), dipoles_kernel_ptr = get_ptr(dipoles_kernel), - elc_kernel_ptr = get_ptr(elc_kernel), &nonbonded_ias = *nonbonded_ias]( - Particle &p1, Particle &p2, Distance const &d) { + elc_kernel_ptr = get_ptr(elc_kernel), &nonbonded_ias = *nonbonded_ias, + &thermostat = *thermostat, + &box_geo = *box_geo](Particle &p1, Particle &p2, Distance const &d) { auto const &ia_params = nonbonded_ias.get_ia_param(p1.type(), p2.type()); - add_non_bonded_pair_force(p1, p2, d.vec21, sqrt(d.dist2), d.dist2, - ia_params, coulomb_kernel_ptr, - dipoles_kernel_ptr, elc_kernel_ptr); + add_non_bonded_pair_force( + p1, p2, d.vec21, sqrt(d.dist2), d.dist2, ia_params, thermostat, + box_geo, coulomb_kernel_ptr, dipoles_kernel_ptr, elc_kernel_ptr); #ifdef COLLISION_DETECTION if (collision_params.mode != CollisionModeType::OFF) detect_collision(p1, p2, d.dist2); @@ -213,8 +214,9 @@ void System::System::calculate_forces(double kT) { // Must be done here. Forces need to be ghost-communicated immersed_boundaries.volume_conservation(*cell_structure); - if (lb.is_solver_set()) { - LB::couple_particles(particles, ghost_particles, time_step); + if (thermostat->lb and (propagation->used_propagations & + PropagationMode::TRANS_LB_MOMENTUM_EXCHANGE)) { + lb_couple_particles(time_step); } #ifdef CUDA diff --git a/src/core/forces_inline.hpp b/src/core/forces_inline.hpp index d9a95b0156..f6d643d1e7 100644 --- a/src/core/forces_inline.hpp +++ b/src/core/forces_inline.hpp @@ -195,6 +195,8 @@ inline ParticleForce calc_opposing_force(ParticleForce const &pf, * @param[in] dist distance between @p p1 and @p p2. * @param[in] dist2 distance squared between @p p1 and @p p2. * @param[in] ia_params non-bonded interaction kernels. + * @param[in] thermostat thermostat. + * @param[in] box_geo box geometry. * @param[in] coulomb_kernel Coulomb force kernel. * @param[in] dipoles_kernel Dipolar force kernel. * @param[in] elc_kernel ELC force correction kernel. @@ -202,6 +204,7 @@ inline ParticleForce calc_opposing_force(ParticleForce const &pf, inline void add_non_bonded_pair_force( Particle &p1, Particle &p2, Utils::Vector3d const &d, double dist, double dist2, IA_parameters const &ia_params, + Thermostat::Thermostat const &thermostat, BoxGeometry const &box_geo, Coulomb::ShortRangeForceKernel::kernel_type const *coulomb_kernel, Dipoles::ShortRangeForceKernel::kernel_type const *dipoles_kernel, Coulomb::ShortRangeForceCorrectionsKernel::kernel_type const *elc_kernel) { @@ -255,8 +258,9 @@ inline void add_non_bonded_pair_force( /* The inter dpd force should not be part of the virial */ #ifdef DPD - if (thermo_switch & THERMO_DPD) { - auto const force = dpd_pair_force(p1, p2, ia_params, d, dist, dist2); + if (thermostat.thermo_switch & THERMO_DPD) { + auto const force = dpd_pair_force(p1, p2, *thermostat.dpd, box_geo, + ia_params, d, dist, dist2); p1.force() += force; p2.force() -= force; } diff --git a/src/core/ghosts.cpp b/src/core/ghosts.cpp index c513656ffb..5a992d1870 100644 --- a/src/core/ghosts.cpp +++ b/src/core/ghosts.cpp @@ -247,9 +247,9 @@ static auto calc_transmit_size(GhostCommunication const &ghost_comm, } static void prepare_send_buffer(CommBuf &send_buffer, - const GhostCommunication &ghost_comm, + GhostCommunication const &ghost_comm, + BoxGeometry const &box_geo, unsigned int data_parts) { - auto const &box_geo = *System::get_system().box_geo; /* reallocate send buffer */ send_buffer.resize(calc_transmit_size(ghost_comm, box_geo, data_parts)); @@ -295,9 +295,9 @@ static void prepare_ghost_cell(ParticleList *cell, std::size_t size) { } static void prepare_recv_buffer(CommBuf &recv_buffer, - const GhostCommunication &ghost_comm, + GhostCommunication const &ghost_comm, + BoxGeometry const &box_geo, unsigned int data_parts) { - auto const &box_geo = *System::get_system().box_geo; /* reallocate recv buffer */ recv_buffer.resize(calc_transmit_size(ghost_comm, box_geo, data_parts)); /* clear bond buffer */ @@ -305,11 +305,11 @@ static void prepare_recv_buffer(CommBuf &recv_buffer, } static void put_recv_buffer(CommBuf &recv_buffer, - const GhostCommunication &ghost_comm, + GhostCommunication const &ghost_comm, + BoxGeometry const &box_geo, unsigned int data_parts) { /* put back data */ auto archiver = Utils::MemcpyIArchive{Utils::make_span(recv_buffer)}; - auto const &box_geo = *System::get_system().box_geo; if (data_parts & GHOSTTRANS_PARTNUM) { for (auto part_list : ghost_comm.part_lists) { @@ -372,9 +372,9 @@ static void add_forces_from_recv_buffer(CommBuf &recv_buffer, } } -static void cell_cell_transfer(const GhostCommunication &ghost_comm, +static void cell_cell_transfer(GhostCommunication const &ghost_comm, + BoxGeometry const &box_geo, unsigned int data_parts) { - auto const &box_geo = *System::get_system().box_geo; CommBuf buffer; if (!(data_parts & GHOSTTRANS_PARTNUM)) { buffer.resize(calc_transmit_size(box_geo, data_parts)); @@ -437,15 +437,13 @@ static bool is_poststorable(GhostCommunication const &ghost_comm, return is_recv_op(comm_type, node, this_node) && poststore; } -void ghost_communicator(const GhostCommunicator &gcr, unsigned int data_parts) { +void ghost_communicator(GhostCommunicator const &gcr, + BoxGeometry const &box_geo, unsigned int data_parts) { if (GHOSTTRANS_NONE == data_parts) return; static CommBuf send_buffer, recv_buffer; -#ifndef NDEBUG - auto const &box_geo = *System::get_system().box_geo; -#endif auto const &comm = gcr.mpi_comm; for (auto it = gcr.communications.begin(); it != gcr.communications.end(); @@ -454,7 +452,7 @@ void ghost_communicator(const GhostCommunicator &gcr, unsigned int data_parts) { int const comm_type = ghost_comm.type & GHOST_JOBMASK; if (comm_type == GHOST_LOCL) { - cell_cell_transfer(ghost_comm, data_parts); + cell_cell_transfer(ghost_comm, box_geo, data_parts); continue; } @@ -466,7 +464,7 @@ void ghost_communicator(const GhostCommunicator &gcr, unsigned int data_parts) { if (is_send_op(comm_type, node, comm.rank())) { /* ok, we send this step, prepare send buffer if not yet done */ if (!prefetch) { - prepare_send_buffer(send_buffer, ghost_comm, data_parts); + prepare_send_buffer(send_buffer, ghost_comm, box_geo, data_parts); } // Check prefetched send buffers (must also hold for buffers allocated // in the previous lines.) @@ -481,12 +479,13 @@ void ghost_communicator(const GhostCommunicator &gcr, unsigned int data_parts) { }); if (prefetch_ghost_comm != gcr.communications.end()) - prepare_send_buffer(send_buffer, *prefetch_ghost_comm, data_parts); + prepare_send_buffer(send_buffer, *prefetch_ghost_comm, box_geo, + data_parts); } /* recv buffer for recv and multinode operations to this node */ if (is_recv_op(comm_type, node, comm.rank())) - prepare_recv_buffer(recv_buffer, ghost_comm, data_parts); + prepare_recv_buffer(recv_buffer, ghost_comm, box_geo, data_parts); /* transfer data */ // Use two send/recvs in order to avoid having to serialize CommBuf @@ -540,7 +539,7 @@ void ghost_communicator(const GhostCommunicator &gcr, unsigned int data_parts) { add_rattle_correction_from_recv_buffer(recv_buffer, ghost_comm); #endif else - put_recv_buffer(recv_buffer, ghost_comm, data_parts); + put_recv_buffer(recv_buffer, ghost_comm, box_geo, data_parts); } } else if (poststore) { /* send op; write back delayed data from last recv, when this was a @@ -564,7 +563,8 @@ void ghost_communicator(const GhostCommunicator &gcr, unsigned int data_parts) { *poststore_ghost_comm); #endif else - put_recv_buffer(recv_buffer, *poststore_ghost_comm, data_parts); + put_recv_buffer(recv_buffer, *poststore_ghost_comm, box_geo, + data_parts); } } } diff --git a/src/core/ghosts.hpp b/src/core/ghosts.hpp index 4d52b49b53..22fdb3236f 100644 --- a/src/core/ghosts.hpp +++ b/src/core/ghosts.hpp @@ -18,8 +18,9 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -#ifndef CORE_GHOSTS_HPP -#define CORE_GHOSTS_HPP + +#pragma once + /** \file * Ghost particles and particle exchange. * @@ -83,6 +84,8 @@ * * The ghost communicators are created by the cell systems. */ + +#include "BoxGeometry.hpp" #include "ParticleList.hpp" #include @@ -164,8 +167,7 @@ struct GhostCommunicator { }; /** - * @brief Do a ghost communication with caller specified data parts. + * @brief Do a ghost communication with the specified data parts. */ -void ghost_communicator(const GhostCommunicator &gcr, unsigned int data_parts); - -#endif +void ghost_communicator(GhostCommunicator const &gcr, + BoxGeometry const &box_geo, unsigned int data_parts); diff --git a/src/core/global_ghost_flags.cpp b/src/core/global_ghost_flags.cpp deleted file mode 100644 index 3f590e700d..0000000000 --- a/src/core/global_ghost_flags.cpp +++ /dev/null @@ -1,57 +0,0 @@ -/* - * Copyright (C) 2010-2022 The ESPResSo project - * Copyright (C) 2002,2003,2004,2005,2006,2007,2008,2009,2010 - * Max-Planck-Institute for Polymer Research, Theory Group - * - * This file is part of ESPResSo. - * - * ESPResSo is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * ESPResSo is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -#include "global_ghost_flags.hpp" - -#include "bonded_interactions/bonded_interaction_data.hpp" -#include "collision.hpp" -#include "config/config.hpp" -#include "system/System.hpp" -#include "thermostat.hpp" - -/** - * @brief Returns the ghost flags required for running pair - * kernels for the global state, e.g. the force calculation. - * @return Required data parts; - */ -unsigned global_ghost_flags() { - /* Position and Properties are always requested. */ - unsigned data_parts = Cells::DATA_PART_POSITION | Cells::DATA_PART_PROPERTIES; - - if (::System::get_system().lb.is_solver_set()) - data_parts |= Cells::DATA_PART_MOMENTUM; - - if (::thermo_switch & THERMO_DPD) - data_parts |= Cells::DATA_PART_MOMENTUM; - - if (::bonded_ia_params.get_n_thermalized_bonds()) { - data_parts |= Cells::DATA_PART_MOMENTUM; - data_parts |= Cells::DATA_PART_BONDS; - } - -#ifdef COLLISION_DETECTION - if (::collision_params.mode != CollisionModeType::OFF) { - data_parts |= Cells::DATA_PART_BONDS; - } -#endif - - return data_parts; -} diff --git a/src/core/integrate.cpp b/src/core/integrate.cpp index e2c944c7f1..61b925b266 100644 --- a/src/core/integrate.cpp +++ b/src/core/integrate.cpp @@ -46,7 +46,6 @@ #include "communication.hpp" #include "errorhandling.hpp" #include "forces.hpp" -#include "global_ghost_flags.hpp" #include "lb/particle_coupling.hpp" #include "lb/utils.hpp" #include "lees_edwards/lees_edwards.hpp" @@ -128,14 +127,14 @@ void LeesEdwards::unset_protocol() { } // namespace LeesEdwards -void Propagation::update_default_propagation() { +void Propagation::update_default_propagation(int thermo_switch) { switch (integ_switch) { case INTEG_METHOD_STEEPEST_DESCENT: default_propagation = PropagationMode::NONE; break; case INTEG_METHOD_NVT: { // NOLINTNEXTLINE(bugprone-branch-clone) - if (thermo_switch & THERMO_LB & THERMO_LANGEVIN) { + if ((thermo_switch & THERMO_LB) and (thermo_switch & THERMO_LANGEVIN)) { default_propagation = PropagationMode::TRANS_LB_MOMENTUM_EXCHANGE; #ifdef ROTATION default_propagation |= PropagationMode::ROT_LANGEVIN; @@ -193,6 +192,7 @@ void System::System::update_used_propagations() { } void System::System::integrator_sanity_checks() const { + auto const thermo_switch = thermostat->thermo_switch; if (time_step <= 0.) { runtimeErrorMsg() << "time_step not set"; } @@ -210,7 +210,7 @@ void System::System::integrator_sanity_checks() const { } #ifdef NPT if (propagation->used_propagations & PropagationMode::TRANS_LANGEVIN_NPT) { - if (thermo_switch != THERMO_OFF and thermo_switch != THERMO_NPT_ISO) { + if (thermo_switch != THERMO_NPT_ISO) { runtimeErrorMsg() << "The NpT integrator requires the NpT thermostat"; } if (box_geo->type() == BoxType::LEES_EDWARDS) { @@ -225,11 +225,25 @@ void System::System::integrator_sanity_checks() const { } if (propagation->used_propagations & PropagationMode::TRANS_STOKESIAN) { #ifdef STOKESIAN_DYNAMICS - if (thermo_switch != THERMO_OFF && thermo_switch != THERMO_SD) { + if (thermo_switch != THERMO_SD) { runtimeErrorMsg() << "The SD integrator requires the SD thermostat"; } #endif } + if (lb.is_solver_set() and (propagation->used_propagations & + (PropagationMode::TRANS_LB_MOMENTUM_EXCHANGE | + PropagationMode::TRANS_LB_TRACER))) { + if (thermostat->lb == nullptr) { + runtimeErrorMsg() << "The LB integrator requires the LB thermostat"; + } + } + if (::bonded_ia_params.get_n_thermalized_bonds() >= 1 and + (thermostat->thermalized_bond == nullptr or + (thermo_switch & THERMO_BOND) == 0)) { + runtimeErrorMsg() + << "Thermalized bonds require the thermalized_bond thermostat"; + } + #ifdef ROTATION for (auto const &p : cell_structure->local_particles()) { using namespace PropagationMode; @@ -285,7 +299,7 @@ void walberla_agrid_sanity_checks(std::string method, "waLBerla and ESPResSo disagree about domain decomposition."); } } -#endif +#endif // WALBERLA static void resort_particles_if_needed(System::System &system) { auto &cell_structure = *system.cell_structure; @@ -296,8 +310,15 @@ static void resort_particles_if_needed(System::System &system) { } } -void System::System::thermostats_force_init(double kT) { +void System::System::thermostat_force_init() { auto const &propagation = *this->propagation; + if ((not thermostat->langevin) or ((propagation.used_propagations & + (PropagationMode::TRANS_LANGEVIN | + PropagationMode::ROT_LANGEVIN)) == 0)) { + return; + } + auto const &langevin = *thermostat->langevin; + auto const kT = thermostat->kT; for (auto &p : cell_structure->local_particles()) { if (propagation.should_propagate_with(p, PropagationMode::TRANS_LANGEVIN)) p.force() += friction_thermo_langevin(langevin, p, time_step, kT); @@ -313,12 +334,14 @@ void System::System::thermostats_force_init(double kT) { * @return whether or not to stop the integration loop early. */ static bool integrator_step_1(ParticleRange const &particles, - Propagation const &propagation, double kT, - double time_step) { + Propagation const &propagation, + System::System &system, double time_step) { // steepest decent if (propagation.integ_switch == INTEG_METHOD_STEEPEST_DESCENT) return steepest_descent_step(particles); + auto const &thermostat = *system.thermostat; + auto const kT = thermostat.kT; for (auto &p : particles) { #ifdef VIRTUAL_SITES // virtual sites are updated later in the integration loop @@ -341,10 +364,10 @@ static bool integrator_step_1(ParticleRange const &particles, velocity_verlet_rotator_1(p, time_step); #endif if (propagation.should_propagate_with(p, PropagationMode::TRANS_BROWNIAN)) - brownian_dynamics_propagator(brownian, p, time_step, kT); + brownian_dynamics_propagator(*thermostat.brownian, p, time_step, kT); #ifdef ROTATION if (propagation.should_propagate_with(p, PropagationMode::ROT_BROWNIAN)) - brownian_dynamics_rotator(brownian, p, time_step, kT); + brownian_dynamics_rotator(*thermostat.brownian, p, time_step, kT); #endif } @@ -352,7 +375,8 @@ static bool integrator_step_1(ParticleRange const &particles, if ((propagation.used_propagations & PropagationMode::TRANS_LANGEVIN_NPT) and (propagation.default_propagation & PropagationMode::TRANS_LANGEVIN_NPT)) { auto pred = PropagationPredicateNPT(propagation.default_propagation); - velocity_verlet_npt_step_1(particles.filter(pred), time_step); + velocity_verlet_npt_step_1(particles.filter(pred), *thermostat.npt_iso, + time_step, system); } #endif @@ -360,7 +384,8 @@ static bool integrator_step_1(ParticleRange const &particles, if ((propagation.used_propagations & PropagationMode::TRANS_STOKESIAN) and (propagation.default_propagation & PropagationMode::TRANS_STOKESIAN)) { auto pred = PropagationPredicateStokesian(propagation.default_propagation); - stokesian_dynamics_step_1(particles.filter(pred), time_step); + stokesian_dynamics_step_1(particles.filter(pred), *thermostat.stokesian, + time_step, kT); } #endif // STOKESIAN_DYNAMICS @@ -368,7 +393,8 @@ static bool integrator_step_1(ParticleRange const &particles, } static void integrator_step_2(ParticleRange const &particles, - Propagation const &propagation, double kT, + Propagation const &propagation, + Thermostat::Thermostat const &thermostat, double time_step) { if (propagation.integ_switch == INTEG_METHOD_STEEPEST_DESCENT) return; @@ -400,7 +426,8 @@ static void integrator_step_2(ParticleRange const &particles, if ((propagation.used_propagations & PropagationMode::TRANS_LANGEVIN_NPT) and (propagation.default_propagation & PropagationMode::TRANS_LANGEVIN_NPT)) { auto pred = PropagationPredicateNPT(propagation.default_propagation); - velocity_verlet_npt_step_2(particles.filter(pred), time_step); + velocity_verlet_npt_step_2(particles.filter(pred), *thermostat.npt_iso, + time_step); } #endif } @@ -421,7 +448,7 @@ int System::System::integrate(int n_steps, int reuse_forces) { #endif // Prepare particle structure and run sanity checks of all active algorithms - propagation.update_default_propagation(); + propagation.update_default_propagation(thermostat->thermo_switch); update_used_propagations(); on_integration_start(); @@ -436,7 +463,7 @@ int System::System::integrate(int n_steps, int reuse_forces) { #ifdef CALIPER CALI_MARK_BEGIN("Initial Force Calculation"); #endif - lb_lbcoupling_deactivate(); + thermostat->lb_coupling_deactivate(); #ifdef VIRTUAL_SITES_RELATIVE if (has_vs_rel()) { @@ -445,9 +472,9 @@ int System::System::integrate(int n_steps, int reuse_forces) { #endif // Communication step: distribute ghost positions - cell_structure->update_ghosts_and_resort_particle(global_ghost_flags()); + cell_structure->update_ghosts_and_resort_particle(get_global_ghost_flags()); - calculate_forces(::temperature); + calculate_forces(); if (propagation.integ_switch != INTEG_METHOD_STEEPEST_DESCENT) { #ifdef ROTATION @@ -460,7 +487,7 @@ int System::System::integrate(int n_steps, int reuse_forces) { #endif } - lb_lbcoupling_activate(); + thermostat->lb_coupling_activate(); if (check_runtime_errors(comm_cart)) return INTEG_ERROR_RUNTIME; @@ -506,7 +533,7 @@ int System::System::integrate(int n_steps, int reuse_forces) { lees_edwards->update_box_params(*box_geo, sim_time); bool early_exit = - integrator_step_1(particles, propagation, ::temperature, time_step); + integrator_step_1(particles, propagation, *this, time_step); if (early_exit) break; @@ -526,7 +553,7 @@ int System::System::integrate(int n_steps, int reuse_forces) { } // Propagate philox RNG counters - philox_counter_increment(); + thermostat->philox_counter_increment(); #ifdef BOND_CONSTRAINT // Correct particle positions that participate in a rigid/constrained bond @@ -551,18 +578,20 @@ int System::System::integrate(int n_steps, int reuse_forces) { n_verlet_updates++; // Communication step: distribute ghost positions - cell_structure->update_ghosts_and_resort_particle(global_ghost_flags()); + cell_structure->update_ghosts_and_resort_particle(get_global_ghost_flags()); particles = cell_structure->local_particles(); - calculate_forces(::temperature); + calculate_forces(); #ifdef VIRTUAL_SITES_INERTIALESS_TRACERS - if (propagation.used_propagations & PropagationMode::TRANS_LB_TRACER) { - lb_tracers_add_particle_force_to_fluid(*cell_structure, time_step); + if (thermostat->lb and + (propagation.used_propagations & PropagationMode::TRANS_LB_TRACER)) { + lb_tracers_add_particle_force_to_fluid(*cell_structure, *box_geo, + *local_geo, lb, time_step); } #endif - integrator_step_2(particles, propagation, ::temperature, time_step); + integrator_step_2(particles, propagation, *thermostat, time_step); if (propagation.integ_switch == INTEG_METHOD_BD) { resort_particles_if_needed(*this); } @@ -602,7 +631,6 @@ int System::System::integrate(int n_steps, int reuse_forces) { lb.propagate(); ek.propagate(); } - lb_lbcoupling_propagate(); } else if (lb_active) { auto const md_steps_per_lb_step = calc_md_steps_per_tau(lb.get_tau()); propagation.lb_skipped_md_steps += 1; @@ -610,7 +638,6 @@ int System::System::integrate(int n_steps, int reuse_forces) { propagation.lb_skipped_md_steps = 0; lb.propagate(); } - lb_lbcoupling_propagate(); } else if (ek_active) { auto const md_steps_per_ek_step = calc_md_steps_per_tau(ek.get_tau()); propagation.ek_skipped_md_steps += 1; @@ -619,10 +646,15 @@ int System::System::integrate(int n_steps, int reuse_forces) { ek.propagate(); } } + if (lb_active and (propagation.used_propagations & + PropagationMode::TRANS_LB_MOMENTUM_EXCHANGE)) { + thermostat->lb->rng_increment(); + } #ifdef VIRTUAL_SITES_INERTIALESS_TRACERS - if (propagation.used_propagations & PropagationMode::TRANS_LB_TRACER) { - lb_tracers_propagate(*cell_structure, time_step); + if (thermostat->lb and + (propagation.used_propagations & PropagationMode::TRANS_LB_TRACER)) { + lb_tracers_propagate(*cell_structure, lb, time_step); } #endif diff --git a/src/core/integrators/Propagation.hpp b/src/core/integrators/Propagation.hpp index f810c5f1ba..c0949246eb 100644 --- a/src/core/integrators/Propagation.hpp +++ b/src/core/integrators/Propagation.hpp @@ -33,7 +33,7 @@ class Propagation { /** If true, forces will be recalculated before the next integration. */ bool recalc_forces = true; - void update_default_propagation(); + void update_default_propagation(int thermo_switch); template bool should_propagate_with(Particle const &p, int mode) const { diff --git a/src/core/integrators/stokesian_dynamics_inline.hpp b/src/core/integrators/stokesian_dynamics_inline.hpp index 4150e8a819..ee0a8eedc2 100644 --- a/src/core/integrators/stokesian_dynamics_inline.hpp +++ b/src/core/integrators/stokesian_dynamics_inline.hpp @@ -25,10 +25,12 @@ #include "rotation.hpp" #include "stokesian_dynamics/sd_interface.hpp" +#include "thermostat.hpp" inline void stokesian_dynamics_step_1(ParticleRangeStokesian const &particles, - double time_step) { - propagate_vel_pos_sd(particles, time_step); + StokesianThermostat const &stokesian, + double time_step, double kT) { + propagate_vel_pos_sd(particles, stokesian, time_step, kT); for (auto &p : particles) { // translate diff --git a/src/core/integrators/velocity_verlet_npt.cpp b/src/core/integrators/velocity_verlet_npt.cpp index 328bf31859..ad6f350564 100644 --- a/src/core/integrators/velocity_verlet_npt.cpp +++ b/src/core/integrators/velocity_verlet_npt.cpp @@ -45,6 +45,7 @@ static constexpr Utils::Vector3i nptgeom_dir{{1, 2, 4}}; static void velocity_verlet_npt_propagate_vel_final(ParticleRangeNPT const &particles, + IsotropicNptThermostat const &npt_iso, double time_step) { nptiso.p_vel = {}; @@ -65,7 +66,9 @@ velocity_verlet_npt_propagate_vel_final(ParticleRangeNPT const &particles, } /** Scale and communicate instantaneous NpT pressure */ -static void velocity_verlet_npt_finalize_p_inst(double time_step) { +static void +velocity_verlet_npt_finalize_p_inst(IsotropicNptThermostat const &npt_iso, + double time_step) { /* finalize derivation of p_inst */ nptiso.p_inst = 0.0; for (unsigned int i = 0; i < 3; i++) { @@ -84,17 +87,18 @@ static void velocity_verlet_npt_finalize_p_inst(double time_step) { } } -static void velocity_verlet_npt_propagate_pos(ParticleRangeNPT const &particles, - double time_step) { +static void +velocity_verlet_npt_propagate_pos(ParticleRangeNPT const &particles, + IsotropicNptThermostat const &npt_iso, + double time_step, System::System &system) { - auto &system = System::get_system(); auto &box_geo = *system.box_geo; auto &cell_structure = *system.cell_structure; Utils::Vector3d scal{}; double L_new = 0.0; /* finalize derivation of p_inst */ - velocity_verlet_npt_finalize_p_inst(time_step); + velocity_verlet_npt_finalize_p_inst(npt_iso, time_step); /* adjust \ref NptIsoParameters::nptiso.volume; prepare pos- and * vel-rescaling @@ -158,8 +162,10 @@ static void velocity_verlet_npt_propagate_pos(ParticleRangeNPT const &particles, system.on_boxl_change(true); } -static void velocity_verlet_npt_propagate_vel(ParticleRangeNPT const &particles, - double time_step) { +static void +velocity_verlet_npt_propagate_vel(ParticleRangeNPT const &particles, + IsotropicNptThermostat const &npt_iso, + double time_step) { nptiso.p_vel = {}; for (auto &p : particles) { @@ -179,15 +185,17 @@ static void velocity_verlet_npt_propagate_vel(ParticleRangeNPT const &particles, } void velocity_verlet_npt_step_1(ParticleRangeNPT const &particles, - double time_step) { - velocity_verlet_npt_propagate_vel(particles, time_step); - velocity_verlet_npt_propagate_pos(particles, time_step); + IsotropicNptThermostat const &npt_iso, + double time_step, System::System &system) { + velocity_verlet_npt_propagate_vel(particles, npt_iso, time_step); + velocity_verlet_npt_propagate_pos(particles, npt_iso, time_step, system); } void velocity_verlet_npt_step_2(ParticleRangeNPT const &particles, + IsotropicNptThermostat const &npt_iso, double time_step) { - velocity_verlet_npt_propagate_vel_final(particles, time_step); - velocity_verlet_npt_finalize_p_inst(time_step); + velocity_verlet_npt_propagate_vel_final(particles, npt_iso, time_step); + velocity_verlet_npt_finalize_p_inst(npt_iso, time_step); } #endif // NPT diff --git a/src/core/integrators/velocity_verlet_npt.hpp b/src/core/integrators/velocity_verlet_npt.hpp index 127296dfee..e0992f2448 100644 --- a/src/core/integrators/velocity_verlet_npt.hpp +++ b/src/core/integrators/velocity_verlet_npt.hpp @@ -26,6 +26,7 @@ #include "ParticleRange.hpp" #include "PropagationMode.hpp" #include "PropagationPredicate.hpp" +#include "thermostat.hpp" struct PropagationPredicateNPT { int modes; @@ -41,6 +42,10 @@ struct PropagationPredicateNPT { using ParticleRangeNPT = ParticleRangeFiltered; +namespace System { +class System; +} + /** Special propagator for NpT isotropic. * Propagate the velocities and positions. Integration steps before force * calculation of the Velocity Verlet integrator: @@ -51,7 +56,8 @@ using ParticleRangeNPT = ParticleRangeFiltered; * positions and velocities and check Verlet list criterion (only NpT). */ void velocity_verlet_npt_step_1(ParticleRangeNPT const &particles, - double time_step); + IsotropicNptThermostat const &npt_iso, + double time_step, System::System &system); /** Final integration step of the Velocity Verlet+NpT integrator. * Finalize instantaneous pressure calculation: @@ -59,6 +65,7 @@ void velocity_verlet_npt_step_1(ParticleRangeNPT const &particles, * + 0.5 \Delta t \cdot F(t+\Delta t)/m \f] */ void velocity_verlet_npt_step_2(ParticleRangeNPT const &particles, + IsotropicNptThermostat const &npt_iso, double time_step); #endif // NPT diff --git a/src/core/lb/LBNone.hpp b/src/core/lb/LBNone.hpp index 690aececd6..bf913c5381 100644 --- a/src/core/lb/LBNone.hpp +++ b/src/core/lb/LBNone.hpp @@ -47,6 +47,7 @@ struct LBNone { } Utils::Vector3d get_momentum() const { throw NoLBActive{}; } void veto_time_step(double) const { throw NoLBActive{}; } + void veto_kT(double) const { throw NoLBActive{}; } void sanity_checks(System::System const &) const { throw NoLBActive{}; } void lebc_sanity_checks(unsigned int, unsigned int) const { throw NoLBActive{}; diff --git a/src/core/lb/LBWalberla.cpp b/src/core/lb/LBWalberla.cpp index 3bd7a421f4..8d8995a927 100644 --- a/src/core/lb/LBWalberla.cpp +++ b/src/core/lb/LBWalberla.cpp @@ -28,10 +28,12 @@ #include "errorhandling.hpp" #include "integrate.hpp" #include "system/System.hpp" +#include "thermostat.hpp" #include #include +#include #include @@ -75,6 +77,15 @@ void LBWalberla::veto_time_step(double time_step) const { walberla_tau_sanity_checks("LB", lb_params->get_tau(), time_step); } +void LBWalberla::veto_kT(double kT) const { + auto const energy_conversion = + Utils::int_pow<2>(lb_params->get_agrid() / lb_params->get_tau()); + auto const lb_kT = lb_fluid->get_kT() * energy_conversion; + if (not ::Thermostat::are_kT_equal(lb_kT, kT)) { + throw std::runtime_error("Temperature change not supported by LB"); + } +} + void LBWalberla::sanity_checks(System::System const &system) const { auto const agrid = lb_params->get_agrid(); auto [lb_left, lb_right] = lb_fluid->get_lattice().get_local_domain(); diff --git a/src/core/lb/LBWalberla.hpp b/src/core/lb/LBWalberla.hpp index 64d54e9e93..16e35eb2b9 100644 --- a/src/core/lb/LBWalberla.hpp +++ b/src/core/lb/LBWalberla.hpp @@ -68,6 +68,7 @@ struct LBWalberla { Utils::Vector3d const &force); void propagate(); void veto_time_step(double time_step) const; + void veto_kT(double kT) const; void sanity_checks(System::System const &system) const; void lebc_sanity_checks(unsigned int shear_direction, unsigned int shear_plane_normal) const; @@ -79,12 +80,8 @@ struct LBWalberla { void on_node_grid_change() const { throw std::runtime_error("MPI topology change not supported by LB"); } - void on_timestep_change() const { - throw std::runtime_error("Time step change not supported by LB"); - } - void on_temperature_change() const { - throw std::runtime_error("Temperature change not supported by LB"); - } + void on_timestep_change() const {} + void on_temperature_change() const {} }; } // namespace LB diff --git a/src/core/lb/Solver.cpp b/src/core/lb/Solver.cpp index 6dea3c09a1..8951595d82 100644 --- a/src/core/lb/Solver.cpp +++ b/src/core/lb/Solver.cpp @@ -28,6 +28,7 @@ #include "BoxGeometry.hpp" #include "system/System.hpp" +#include "thermostat.hpp" #ifdef WALBERLA #include @@ -83,6 +84,12 @@ void Solver::veto_time_step(double time_step) const { } } +void Solver::veto_kT(double kT) const { + if (impl->solver) { + std::visit([=](auto &ptr) { ptr->veto_kT(kT); }, *impl->solver); + } +} + void Solver::lebc_sanity_checks(unsigned int shear_direction, unsigned int shear_plane_normal) const { if (impl->solver) { diff --git a/src/core/lb/Solver.hpp b/src/core/lb/Solver.hpp index fdf2241ffa..e6012696d0 100644 --- a/src/core/lb/Solver.hpp +++ b/src/core/lb/Solver.hpp @@ -79,6 +79,11 @@ struct Solver : public System::Leaf { */ void veto_time_step(double time_step) const; + /** + * @brief Check if a thermostat is compatible with the LB temperature. + */ + void veto_kT(double kT) const; + /** * @brief Perform LB LEbc parameter checks. */ diff --git a/src/core/lb/particle_coupling.cpp b/src/core/lb/particle_coupling.cpp index 10dcc8be66..cf10874730 100644 --- a/src/core/lb/particle_coupling.cpp +++ b/src/core/lb/particle_coupling.cpp @@ -46,65 +46,6 @@ #include #include -LB::ParticleCouplingConfig lb_particle_coupling; - -static auto is_lb_active() { return System::get_system().lb.is_solver_set(); } - -void mpi_bcast_lb_particle_coupling_local() { - boost::mpi::broadcast(comm_cart, lb_particle_coupling, 0); -} - -REGISTER_CALLBACK(mpi_bcast_lb_particle_coupling_local) - -void mpi_bcast_lb_particle_coupling() { - mpi_call_all(mpi_bcast_lb_particle_coupling_local); -} - -void lb_lbcoupling_activate() { lb_particle_coupling.couple_to_md = true; } - -void lb_lbcoupling_deactivate() { - if (is_lb_active() and this_node == 0 and lb_particle_coupling.gamma > 0.) { - runtimeWarningMsg() - << "Recalculating forces, so the LB coupling forces are not " - "included in the particle force the first time step. This " - "only matters if it happens frequently during sampling."; - } - - lb_particle_coupling.couple_to_md = false; -} - -void lb_lbcoupling_set_gamma(double gamma) { - lb_particle_coupling.gamma = gamma; -} - -double lb_lbcoupling_get_gamma() { return lb_particle_coupling.gamma; } - -bool lb_lbcoupling_is_seed_required() { - if (is_lb_active()) { - return not lb_particle_coupling.rng_counter_coupling.is_initialized(); - } - return false; -} - -uint64_t lb_coupling_get_rng_state_cpu() { - return lb_particle_coupling.rng_counter_coupling->value(); -} - -uint64_t lb_lbcoupling_get_rng_state() { - if (is_lb_active()) { - return lb_coupling_get_rng_state_cpu(); - } - throw std::runtime_error("No LB active"); -} - -void lb_lbcoupling_set_rng_state(uint64_t counter) { - if (is_lb_active()) { - lb_particle_coupling.rng_counter_coupling = - Utils::Counter(counter); - } else - throw std::runtime_error("No LB active"); -} - void add_md_force(LB::Solver &lb, Utils::Vector3d const &pos, Utils::Vector3d const &force, double time_step) { /* transform momentum transfer to lattice units @@ -113,8 +54,8 @@ void add_md_force(LB::Solver &lb, Utils::Vector3d const &pos, lb.add_force_density(pos, delta_j); } -static Thermostat::GammaType lb_handle_particle_anisotropy(Particle const &p) { - auto const lb_gamma = lb_lbcoupling_get_gamma(); +static Thermostat::GammaType lb_handle_particle_anisotropy(Particle const &p, + double lb_gamma) { #ifdef THERMOSTAT_PER_PARTICLE auto const &partcl_gamma = p.gamma(); #ifdef PARTICLE_ANISOTROPY @@ -122,20 +63,21 @@ static Thermostat::GammaType lb_handle_particle_anisotropy(Particle const &p) { #else auto const default_gamma = lb_gamma; #endif // PARTICLE_ANISOTROPY - return handle_particle_gamma(partcl_gamma, default_gamma); + return Thermostat::handle_particle_gamma(partcl_gamma, default_gamma); #else return lb_gamma; #endif // THERMOSTAT_PER_PARTICLE } -Utils::Vector3d lb_drag_force(LB::Solver const &lb, Particle const &p, +Utils::Vector3d lb_drag_force(LB::Solver const &lb, double lb_gamma, + Particle const &p, Utils::Vector3d const &shifted_pos, Utils::Vector3d const &vel_offset) { /* calculate fluid velocity at particle's position this is done by linear interpolation (eq. (11) @cite ahlrichs99a) */ auto const v_fluid = lb.get_coupling_interpolated_velocity(shifted_pos); auto const v_drift = v_fluid + vel_offset; - auto const gamma = lb_handle_particle_anisotropy(p); + auto const gamma = lb_handle_particle_anisotropy(p, lb_gamma); /* calculate viscous force (eq. (9) @cite ahlrichs99a) */ return Utils::hadamard_product(gamma, v_drift - p.v()); @@ -149,11 +91,11 @@ Utils::Vector3d lb_drag_force(LB::Solver const &lb, Particle const &p, * * @return True iff the point is inside of the box up to halo. */ -static bool in_local_domain(Utils::Vector3d const &pos, double halo = 0.) { +static bool in_local_domain(LocalBox const &local_box, + Utils::Vector3d const &pos, double halo = 0.) { auto const halo_vec = Utils::Vector3d::broadcast(halo); - auto const &local_geo = *System::get_system().local_geo; - auto const lower_corner = local_geo.my_left() - halo_vec; - auto const upper_corner = local_geo.my_right() + halo_vec; + auto const lower_corner = local_box.my_left() - halo_vec; + auto const upper_corner = local_box.my_right() + halo_vec; return pos >= lower_corner and pos < upper_corner; } @@ -164,13 +106,10 @@ static bool in_box(Utils::Vector3d const &pos, return pos >= lower_corner and pos < upper_corner; } -static bool in_local_halo(Utils::Vector3d const &pos, double agrid) { +bool in_local_halo(LocalBox const &local_box, Utils::Vector3d const &pos, + double agrid) { auto const halo = 0.5 * agrid; - return in_local_domain(pos, halo); -} - -bool in_local_halo(Utils::Vector3d const &pos) { - return in_local_domain(pos, System::get_system().lb.get_agrid()); + return in_local_domain(local_box, pos, halo); } /** @@ -178,20 +117,18 @@ bool in_local_halo(Utils::Vector3d const &pos) { * coordinate */ std::vector positions_in_halo(Utils::Vector3d const &pos, - BoxGeometry const &box, + BoxGeometry const &box_geo, + LocalBox const &local_box, double agrid) { - auto const &system = System::get_system(); - auto const &box_geo = *system.box_geo; - auto const &local_geo = *system.local_geo; auto const halo = 0.5 * agrid; auto const halo_vec = Utils::Vector3d::broadcast(halo); - auto const fully_inside_lower = local_geo.my_left() + 2. * halo_vec; - auto const fully_inside_upper = local_geo.my_right() - 2. * halo_vec; + auto const fully_inside_lower = local_box.my_left() + 2. * halo_vec; + auto const fully_inside_upper = local_box.my_right() - 2. * halo_vec; if (in_box(pos, fully_inside_lower, fully_inside_upper)) { return {pos}; } - auto const halo_lower_corner = local_geo.my_left() - halo_vec; - auto const halo_upper_corner = local_geo.my_right() + halo_vec; + auto const halo_lower_corner = local_box.my_left() - halo_vec; + auto const halo_upper_corner = local_box.my_right() + halo_vec; std::vector res; for (int i : {-1, 0, 1}) { @@ -199,7 +136,7 @@ std::vector positions_in_halo(Utils::Vector3d const &pos, for (int k : {-1, 0, 1}) { Utils::Vector3d shift{{double(i), double(j), double(k)}}; Utils::Vector3d pos_shifted = - pos + Utils::hadamard_product(box.length(), shift); + pos + Utils::hadamard_product(box_geo.length(), shift); if (box_geo.type() == BoxType::LEES_EDWARDS) { auto le = box_geo.lees_edwards_bc(); @@ -225,36 +162,30 @@ Utils::Vector3d ParticleCoupling::get_noise_term(Particle const &p) const { if (not m_thermalized) { return Utils::Vector3d{}; } - auto const &rng_counter = lb_particle_coupling.rng_counter_coupling; - if (not rng_counter) { - throw std::runtime_error( - "Access to uninitialized LB particle coupling RNG counter"); - } using std::sqrt; using Utils::sqrt; - auto const counter = rng_counter->value(); - auto const gamma = lb_handle_particle_anisotropy(p); - return m_noise_pref_wo_gamma * - Utils::hadamard_product( - sqrt(gamma), - Random::noise_uniform(counter, 0, p.id())); + auto const gamma = lb_handle_particle_anisotropy(p, m_thermostat.gamma); + auto const noise = Random::noise_uniform( + m_thermostat.rng_counter(), m_thermostat.rng_seed(), p.id()); + return m_noise_pref_wo_gamma * Utils::hadamard_product(sqrt(gamma), noise); } void ParticleCoupling::kernel(Particle &p) { auto const agrid = m_lb.get_agrid(); - auto const &box_geo = *System::get_system().box_geo; // Calculate coupling force Utils::Vector3d force_on_particle = {}; - auto folded_pos = box_geo.folded_position(p.pos()); + auto const halo_pos = positions_in_halo(m_box_geo.folded_position(p.pos()), + m_box_geo, m_local_box, agrid); #ifdef ENGINE if (not p.swimming().is_engine_force_on_fluid) #endif - for (auto pos : positions_in_halo(folded_pos, box_geo, agrid)) { - if (in_local_halo(pos, agrid)) { + for (auto const &pos : halo_pos) { + if (in_local_halo(m_local_box, pos, agrid)) { auto const vel_offset = lb_drift_velocity_offset(p); - auto const drag_force = lb_drag_force(m_lb, p, pos, vel_offset); + auto const drag_force = + lb_drag_force(m_lb, m_thermostat.gamma, p, pos, vel_offset); auto const random_force = get_noise_term(p); force_on_particle = drag_force + random_force; break; @@ -270,8 +201,8 @@ void ParticleCoupling::kernel(Particle &p) { // couple positions including shifts by one box length to add // forces to ghost layers - for (auto pos : positions_in_halo(folded_pos, box_geo, agrid)) { - if (in_local_domain(pos)) { + for (auto const &pos : halo_pos) { + if (in_local_domain(m_local_box, pos)) { /* Particle is in our LB volume, so this node * is responsible to adding its force */ p.force() += force_on_particle; @@ -280,12 +211,6 @@ void ParticleCoupling::kernel(Particle &p) { } } -bool CouplingBookkeeping::is_ghost_for_local_particle(Particle const &p) const { - auto const &system = System::get_system(); - auto const &cell_structure = *system.cell_structure; - return not cell_structure.get_local_particle(p.id())->is_ghost(); -} - #if defined(THERMOSTAT_PER_PARTICLE) and defined(PARTICLE_ANISOTROPY) static void lb_coupling_sanity_checks(Particle const &p) { /* @@ -300,35 +225,32 @@ static void lb_coupling_sanity_checks(Particle const &p) { } #endif -void couple_particles(ParticleRange const &real_particles, - ParticleRange const &ghost_particles, double time_step) { +} // namespace LB + +void System::System::lb_couple_particles(double time_step) { #ifdef CALIPER CALI_CXX_MARK_FUNCTION; #endif - if (lb_particle_coupling.couple_to_md) { - auto &lb = System::get_system().lb; - if (lb.is_solver_set()) { - ParticleCoupling coupling{lb, time_step}; - CouplingBookkeeping bookkeeping{}; - for (auto const &particle_range : {real_particles, ghost_particles}) { - for (auto &p : particle_range) { - if (not LB::is_tracer(p) and bookkeeping.should_be_coupled(p)) { + assert(thermostat->lb != nullptr); + if (thermostat->lb->couple_to_md) { + if (not lb.is_solver_set()) { + runtimeErrorMsg() << "The LB thermostat requires a LB fluid"; + return; + } + auto const real_particles = cell_structure->local_particles(); + auto const ghost_particles = cell_structure->ghost_particles(); + LB::ParticleCoupling coupling{*thermostat->lb, lb, *box_geo, *local_geo, + time_step}; + LB::CouplingBookkeeping bookkeeping{*cell_structure}; + for (auto const *particle_range : {&real_particles, &ghost_particles}) { + for (auto &p : *particle_range) { + if (not LB::is_tracer(p) and bookkeeping.should_be_coupled(p)) { #if defined(THERMOSTAT_PER_PARTICLE) and defined(PARTICLE_ANISOTROPY) - lb_coupling_sanity_checks(p); + LB::lb_coupling_sanity_checks(p); #endif - coupling.kernel(p); - } + coupling.kernel(p); } } } } } - -} // namespace LB - -void lb_lbcoupling_propagate() { - auto const &lb = System::get_system().lb; - if (lb.is_solver_set() and lb.get_kT() > 0.0) { - lb_particle_coupling.rng_counter_coupling->increment(); - } -} diff --git a/src/core/lb/particle_coupling.hpp b/src/core/lb/particle_coupling.hpp index 2213c8ee3f..bacdd1061f 100644 --- a/src/core/lb/particle_coupling.hpp +++ b/src/core/lb/particle_coupling.hpp @@ -20,55 +20,30 @@ #pragma once #include "BoxGeometry.hpp" +#include "LocalBox.hpp" #include "Particle.hpp" #include "ParticleRange.hpp" #include "PropagationMode.hpp" +#include "cell_system/CellStructure.hpp" #include "lb/Solver.hpp" +#include "system/System.hpp" +#include "thermostat.hpp" -#include #include #include -#include -#include - #include #include -#include #include #include -using OptionalCounter = boost::optional>; - -void lb_lbcoupling_propagate(); -uint64_t lb_lbcoupling_get_rng_state(); -void lb_lbcoupling_set_rng_state(uint64_t counter); -void lb_lbcoupling_set_gamma(double friction); -double lb_lbcoupling_get_gamma(); -bool lb_lbcoupling_is_seed_required(); - -/** - * @brief Activate the coupling between LB and MD particles. - * @note This is a collective function and needs to be called from all - * processes. - */ -void lb_lbcoupling_activate(); - -/** - * @brief Deactivate the coupling between LB and MD particles. - * @note This is a collective function and needs to be called from all - * processes. - */ -void lb_lbcoupling_deactivate(); - /** * @brief Check if a position is within the local LB domain plus halo. * - * @param pos Position to check - * * @return True iff the point is inside of the domain. */ -bool in_local_halo(Utils::Vector3d const &pos); +bool in_local_halo(LocalBox const &local_box, Utils::Vector3d const &pos, + double agrid); /** * @brief Add a force to the lattice force density. @@ -83,18 +58,15 @@ void add_md_force(LB::Solver &lb, Utils::Vector3d const &pos, // internal function exposed for unit testing std::vector positions_in_halo(Utils::Vector3d const &pos, BoxGeometry const &box, + LocalBox const &local_geo, double agrid); -// internal function exposed for unit testing -void add_swimmer_force(Particle const &p, double time_step); - -void mpi_bcast_lb_particle_coupling(); - /** @brief Calculate drag force on a single particle. * * See section II.C. @cite ahlrichs99a * * @param[in] lb The coupled fluid + * @param[in] lb_gamma The friction coefficient * @param[in] p The coupled particle * @param[in] shifted_pos The particle position in LB units with optional shift * @param[in] vel_offset Velocity offset in MD units to be added to @@ -102,60 +74,40 @@ void mpi_bcast_lb_particle_coupling(); * * @return The viscous coupling force */ -Utils::Vector3d lb_drag_force(LB::Solver const &lb, Particle const &p, +Utils::Vector3d lb_drag_force(LB::Solver const &lb, double lb_gamma, + Particle const &p, Utils::Vector3d const &shifted_pos, Utils::Vector3d const &vel_offset); -namespace LB { -struct ParticleCouplingConfig { - OptionalCounter rng_counter_coupling = {}; - /** @brief Friction coefficient for the particle coupling. */ - double gamma = 0.0; - bool couple_to_md = false; - -private: - friend class boost::serialization::access; - - template void serialize(Archive &ar, const unsigned int) { - ar &rng_counter_coupling; - ar γ - ar &couple_to_md; - } -}; -} // namespace LB - -// internal global exposed for unit testing -extern LB::ParticleCouplingConfig lb_particle_coupling; - namespace LB { -/** @brief Calculate particle-lattice interactions. */ -void couple_particles(ParticleRange const &real_particles, - ParticleRange const &ghost_particles, double time_step); - class ParticleCoupling { + LBThermostat const &m_thermostat; LB::Solver &m_lb; - bool m_thermalized; + BoxGeometry const &m_box_geo; + LocalBox const &m_local_box; double m_time_step; double m_noise_pref_wo_gamma; + bool m_thermalized; public: - ParticleCoupling(LB::Solver &lb, double time_step, double kT) - : m_lb{lb}, m_thermalized{kT != 0.}, m_time_step{time_step} { - assert(kT >= 0.); + ParticleCoupling(LBThermostat const &thermostat, LB::Solver &lb, + BoxGeometry const &box_geo, LocalBox const &local_box, + double time_step, double kT = -1.) + : m_thermostat{thermostat}, m_lb{lb}, m_box_geo{box_geo}, + m_local_box{local_box}, m_time_step{time_step} { + assert(kT >= 0. or kT == -1.); /* Eq. (16) @cite ahlrichs99a, without the gamma term. * The factor 12 comes from the fact that we use random numbers * from -0.5 to 0.5 (equally distributed) which have variance 1/12. * The time step comes from the discretization. */ auto constexpr variance_inv = 12.; + kT = (kT >= 0.) ? kT : lb.get_kT() * Utils::sqr(lb.get_lattice_speed()); + m_thermalized = (kT != 0.); m_noise_pref_wo_gamma = std::sqrt(variance_inv * 2. * kT / time_step); } - ParticleCoupling(LB::Solver &lb, double time_step) - : ParticleCoupling(lb, time_step, - lb.get_kT() * Utils::sqr(lb.get_lattice_speed())) {} - Utils::Vector3d get_noise_term(Particle const &p) const; void kernel(Particle &p); @@ -179,11 +131,17 @@ class ParticleCoupling { */ class CouplingBookkeeping { std::unordered_set m_coupled_ghosts; + CellStructure const &m_cell_structure; /** @brief Check if there is locally a real particle for the given ghost. */ - bool is_ghost_for_local_particle(Particle const &p) const; + bool is_ghost_for_local_particle(Particle const &p) const { + return not m_cell_structure.get_local_particle(p.id())->is_ghost(); + } public: + explicit CouplingBookkeeping(CellStructure const &cell_structure) + : m_cell_structure{cell_structure} {} + /** @brief Determine if a given particle should be coupled. */ bool should_be_coupled(Particle const &p) { auto const propagation = p.propagation(); diff --git a/src/core/stokesian_dynamics/sd_interface.cpp b/src/core/stokesian_dynamics/sd_interface.cpp index e9802142dd..1468535946 100644 --- a/src/core/stokesian_dynamics/sd_interface.cpp +++ b/src/core/stokesian_dynamics/sd_interface.cpp @@ -71,8 +71,6 @@ BOOST_IS_BITWISE_SERIALIZABLE(SD_particle_data) static StokesianDynamicsParameters params{0., {}, 0}; -static double sd_kT = 0.0; - /** Buffer that holds the (translational and angular) velocities of the local * particles on each node, used for returning results. */ static std::vector v_sd{}; @@ -127,18 +125,9 @@ StokesianDynamicsParameters::StokesianDynamicsParameters( } } -void set_sd_kT(double kT) { - if (kT < 0.0) { - throw std::domain_error("kT has an invalid value: " + std::to_string(kT)); - } - - sd_kT = kT; -} - -double get_sd_kT() { return sd_kT; } - void propagate_vel_pos_sd(ParticleRangeStokesian const &particles, - double const time_step) { + StokesianThermostat const &stokesian, + double const time_step, double const kT) { static std::vector parts_buffer{}; @@ -184,7 +173,7 @@ void propagate_vel_pos_sd(ParticleRangeStokesian const &particles, } v_sd = sd_cpu(x_host, f_host, a_host, n_part, params.viscosity, - std::sqrt(sd_kT / time_step), + std::sqrt(kT / time_step), static_cast(stokesian.rng_counter()), static_cast(stokesian.rng_seed()), params.flags); } else { // if (this_node == 0) diff --git a/src/core/stokesian_dynamics/sd_interface.hpp b/src/core/stokesian_dynamics/sd_interface.hpp index 7977c31e06..695dc576df 100644 --- a/src/core/stokesian_dynamics/sd_interface.hpp +++ b/src/core/stokesian_dynamics/sd_interface.hpp @@ -31,6 +31,7 @@ #include "ParticleRange.hpp" #include "PropagationMode.hpp" #include "PropagationPredicate.hpp" +#include "thermostat.hpp" #include @@ -67,15 +68,13 @@ enum class sd_flags : int { void register_integrator(StokesianDynamicsParameters const &obj); -void set_sd_kT(double kT); -double get_sd_kT(); - /** Takes the forces and torques on all particles and computes their * velocities. Acts globally on particles on all nodes; i.e. particle data * is gathered from all nodes and their velocities and angular velocities are * set according to the Stokesian Dynamics method. */ void propagate_vel_pos_sd(ParticleRangeStokesian const &particles, - double time_step); + StokesianThermostat const &stokesian, + double time_step, double kT); #endif // STOKESIAN_DYNAMICS diff --git a/src/core/system/System.cpp b/src/core/system/System.cpp index 075bbb30be..66c0e35d78 100644 --- a/src/core/system/System.cpp +++ b/src/core/system/System.cpp @@ -26,6 +26,7 @@ #include "LocalBox.hpp" #include "PropagationMode.hpp" #include "bonded_interactions/bonded_interaction_data.hpp" +#include "bonded_interactions/thermalized_bond.hpp" #include "cell_system/CellStructure.hpp" #include "cell_system/CellStructureType.hpp" #include "cell_system/HybridDecomposition.hpp" @@ -34,7 +35,6 @@ #include "constraints.hpp" #include "electrostatics/icc.hpp" #include "errorhandling.hpp" -#include "global_ghost_flags.hpp" #include "immersed_boundaries.hpp" #include "npt.hpp" #include "particle_node.hpp" @@ -61,6 +61,7 @@ System::System(Private) { local_geo = std::make_shared(); cell_structure = std::make_shared(*box_geo); propagation = std::make_shared(); + thermostat = std::make_shared(); nonbonded_ias = std::make_shared(); comfixed = std::make_shared(); galilei = std::make_shared(); @@ -77,6 +78,7 @@ void System::initialize() { auto handle = shared_from_this(); cell_structure->bind_system(handle); lees_edwards->bind_system(handle); + thermostat->bind_system(handle); #ifdef CUDA gpu.bind_system(handle); gpu.initialize(); @@ -108,6 +110,15 @@ void System::set_time_step(double value) { on_timestep_change(); } +void System::check_kT(double value) const { + if (lb.is_solver_set()) { + lb.veto_kT(value); + } + if (ek.is_solver_set()) { + ek.veto_kT(value); + } +} + void System::set_force_cap(double value) { force_cap = value; propagation->recalc_forces = true; @@ -256,7 +267,7 @@ void System::on_lb_boundary_conditions_change() { } void System::on_particle_local_change() { - cell_structure->update_ghosts_and_resort_particle(global_ghost_flags()); + cell_structure->update_ghosts_and_resort_particle(get_global_ghost_flags()); propagation->recalc_forces = true; } @@ -289,7 +300,7 @@ void System::update_dependent_particles() { #ifdef VIRTUAL_SITES_RELATIVE vs_relative_update_particles(*cell_structure, *box_geo); #endif - cell_structure->update_ghosts_and_resort_particle(global_ghost_flags()); + cell_structure->update_ghosts_and_resort_particle(get_global_ghost_flags()); #endif #ifdef ELECTROSTATICS @@ -305,7 +316,7 @@ void System::update_dependent_particles() { void System::on_observable_calc() { /* Prepare particle structure: Communication step: number of ghosts and ghost * information */ - cell_structure->update_ghosts_and_resort_particle(global_ghost_flags()); + cell_structure->update_ghosts_and_resort_particle(get_global_ghost_flags()); update_dependent_particles(); #ifdef ELECTROSTATICS @@ -384,7 +395,7 @@ void System::on_integration_start() { /* Prepare the thermostat */ if (reinit_thermo) { - thermo_init(time_step); + thermostat->recalc_prefactors(time_step); reinit_thermo = false; propagation->recalc_forces = true; } @@ -422,6 +433,35 @@ void System::on_integration_start() { on_observable_calc(); } +/** + * @brief Returns the ghost flags required for running pair + * kernels for the global state, e.g. the force calculation. + * @return Required data parts; + */ +unsigned System::get_global_ghost_flags() const { + /* Position and Properties are always requested. */ + unsigned data_parts = Cells::DATA_PART_POSITION | Cells::DATA_PART_PROPERTIES; + + if (lb.is_solver_set()) + data_parts |= Cells::DATA_PART_MOMENTUM; + + if (thermostat->thermo_switch & THERMO_DPD) + data_parts |= Cells::DATA_PART_MOMENTUM; + + if (thermostat->thermo_switch & THERMO_BOND) { + data_parts |= Cells::DATA_PART_MOMENTUM; + data_parts |= Cells::DATA_PART_BONDS; + } + +#ifdef COLLISION_DETECTION + if (::collision_params.mode != CollisionModeType::OFF) { + data_parts |= Cells::DATA_PART_BONDS; + } +#endif + + return data_parts; +} + } // namespace System void mpi_init_stand_alone(int argc, char **argv) { diff --git a/src/core/system/System.hpp b/src/core/system/System.hpp index 0e6174457c..4a5b1e853a 100644 --- a/src/core/system/System.hpp +++ b/src/core/system/System.hpp @@ -41,6 +41,9 @@ class LocalBox; struct CellStructure; class Propagation; class InteractionsNonBonded; +namespace Thermostat { +class Thermostat; +} class ComFixed; class Galilei; class Observable_stat; @@ -121,6 +124,8 @@ class System : public std::enable_shared_from_this { /** @brief Get the interaction range. */ double get_interaction_range() const; + unsigned get_global_ghost_flags() const; + /** Check electrostatic and magnetostatic methods are properly initialized. * @return true if sanity checks failed. */ @@ -133,7 +138,7 @@ class System : public std::enable_shared_from_this { std::shared_ptr calculate_pressure(); /** @brief Calculate all forces. */ - void calculate_forces(double kT); + void calculate_forces(); #ifdef DIPOLE_FIELD_TRACKING /** @brief Calculate dipole fields. */ @@ -188,7 +193,10 @@ class System : public std::enable_shared_from_this { int integrate_with_signal_handler(int n_steps, int reuse_forces, bool update_accumulators); - void thermostats_force_init(double kT); + /** @brief Calculate initial particle forces from active thermostats. */ + void thermostat_force_init(); + /** @brief Calculate particle-lattice interactions. */ + void lb_couple_particles(double time_step); /** \name Hook procedures * These procedures are called if several significant changes to @@ -244,6 +252,10 @@ class System : public std::enable_shared_from_this { * @brief Update the global propagation bitmask. */ void update_used_propagations(); + /** + * @brief Veto temperature change. + */ + void check_kT(double value) const; Coulomb::Solver coulomb; Dipoles::Solver dipoles; @@ -254,6 +266,7 @@ class System : public std::enable_shared_from_this { std::shared_ptr cell_structure; std::shared_ptr propagation; std::shared_ptr nonbonded_ias; + std::shared_ptr thermostat; std::shared_ptr comfixed; std::shared_ptr galilei; std::shared_ptr bond_breakage; diff --git a/src/core/system/System.impl.hpp b/src/core/system/System.impl.hpp index 3a2c1e9f66..45dc104b29 100644 --- a/src/core/system/System.impl.hpp +++ b/src/core/system/System.impl.hpp @@ -32,6 +32,7 @@ #include "integrators/Propagation.hpp" #include "lees_edwards/lees_edwards.hpp" #include "nonbonded_interactions/nonbonded_interaction_data.hpp" +#include "thermostat.hpp" #include "BoxGeometry.hpp" #include "LocalBox.hpp" diff --git a/src/core/thermostat.cpp b/src/core/thermostat.cpp index ef92a2e185..ed55999f4f 100644 --- a/src/core/thermostat.cpp +++ b/src/core/thermostat.cpp @@ -35,199 +35,69 @@ #include -#include - -#include -#include - -int thermo_switch = THERMO_OFF; -double temperature = 0.0; - -using Thermostat::GammaType; - -/** - * @brief Create MPI callbacks of thermostat objects - * - * @param thermostat The thermostat object name - */ -#define REGISTER_THERMOSTAT_CALLBACKS(thermostat) \ - void mpi_##thermostat##_set_rng_seed(uint32_t const seed) { \ - (thermostat).rng_initialize(seed); \ - } \ - \ - REGISTER_CALLBACK(mpi_##thermostat##_set_rng_seed) \ - \ - void thermostat##_set_rng_seed(uint32_t const seed) { \ - mpi_call_all(mpi_##thermostat##_set_rng_seed, seed); \ - } \ - \ - void mpi_##thermostat##_set_rng_counter(uint64_t const value) { \ - (thermostat).set_rng_counter(value); \ - } \ - \ - REGISTER_CALLBACK(mpi_##thermostat##_set_rng_counter) \ - \ - void thermostat##_set_rng_counter(uint64_t const value) { \ - mpi_call_all(mpi_##thermostat##_set_rng_counter, value); \ +void Thermostat::Thermostat::recalc_prefactors(double time_step) { + if (thermalized_bond) { + thermalized_bond->recalc_prefactors(time_step); } - -LangevinThermostat langevin = {}; -BrownianThermostat brownian = {}; -#ifdef NPT -IsotropicNptThermostat npt_iso = {}; -#endif -ThermalizedBondThermostat thermalized_bond = {}; -#ifdef DPD -DPDThermostat dpd = {}; -#endif -#ifdef STOKESIAN_DYNAMICS -StokesianThermostat stokesian = {}; -#endif - -REGISTER_THERMOSTAT_CALLBACKS(langevin) -REGISTER_THERMOSTAT_CALLBACKS(brownian) -#ifdef NPT -REGISTER_THERMOSTAT_CALLBACKS(npt_iso) -#endif -REGISTER_THERMOSTAT_CALLBACKS(thermalized_bond) -#ifdef DPD -REGISTER_THERMOSTAT_CALLBACKS(dpd) -#endif -#ifdef STOKESIAN_DYNAMICS -REGISTER_THERMOSTAT_CALLBACKS(stokesian) -#endif - -static void thermalized_bond_init(double time_step); - -void thermo_init(double time_step) { - // initialize thermalized bond regardless of the current thermostat - if (::bonded_ia_params.get_n_thermalized_bonds()) { - thermalized_bond_init(time_step); + if (langevin) { + langevin->recalc_prefactors(kT, time_step); } - if (thermo_switch == THERMO_OFF) { - return; + if (brownian) { + brownian->recalc_prefactors(kT); } - if (thermo_switch & THERMO_LANGEVIN) - langevin.recalc_prefactors(temperature, time_step); #ifdef DPD - if (thermo_switch & THERMO_DPD) - dpd_init(temperature, time_step); + if (dpd) { + dpd_init(kT, time_step); + } #endif #ifdef NPT - if (thermo_switch & THERMO_NPT_ISO) { - npt_iso.recalc_prefactors(temperature, nptiso.piston, time_step); + if (npt_iso) { + npt_iso->recalc_prefactors(kT, nptiso.piston, time_step); } #endif - if (thermo_switch & THERMO_BROWNIAN) - brownian.recalc_prefactors(temperature); } -void philox_counter_increment() { +void Thermostat::Thermostat::philox_counter_increment() { if (thermo_switch & THERMO_LANGEVIN) { - langevin.rng_increment(); + langevin->rng_increment(); } if (thermo_switch & THERMO_BROWNIAN) { - brownian.rng_increment(); + brownian->rng_increment(); } #ifdef NPT if (thermo_switch & THERMO_NPT_ISO) { - npt_iso.rng_increment(); + npt_iso->rng_increment(); } #endif #ifdef DPD if (thermo_switch & THERMO_DPD) { - dpd.rng_increment(); + dpd->rng_increment(); } #endif #ifdef STOKESIAN_DYNAMICS if (thermo_switch & THERMO_SD) { - stokesian.rng_increment(); + stokesian->rng_increment(); } #endif - if (::bonded_ia_params.get_n_thermalized_bonds()) { - thermalized_bond.rng_increment(); + if (thermo_switch & THERMO_BOND) { + thermalized_bond->rng_increment(); } } -void mpi_set_brownian_gamma_local(GammaType const &gamma) { - brownian.gamma = gamma; -} - -void mpi_set_brownian_gamma_rot_local(GammaType const &gamma) { - brownian.gamma_rotation = gamma; -} - -void mpi_set_langevin_gamma_local(GammaType const &gamma) { - langevin.gamma = gamma; - System::get_system().on_thermostat_param_change(); -} - -void mpi_set_langevin_gamma_rot_local(GammaType const &gamma) { - langevin.gamma_rotation = gamma; - System::get_system().on_thermostat_param_change(); -} - -REGISTER_CALLBACK(mpi_set_brownian_gamma_local) -REGISTER_CALLBACK(mpi_set_brownian_gamma_rot_local) -REGISTER_CALLBACK(mpi_set_langevin_gamma_local) -REGISTER_CALLBACK(mpi_set_langevin_gamma_rot_local) - -void mpi_set_brownian_gamma(GammaType const &gamma) { - mpi_call_all(mpi_set_brownian_gamma_local, gamma); -} - -void mpi_set_brownian_gamma_rot(GammaType const &gamma) { - mpi_call_all(mpi_set_brownian_gamma_rot_local, gamma); -} - -void mpi_set_langevin_gamma(GammaType const &gamma) { - mpi_call_all(mpi_set_langevin_gamma_local, gamma); -} -void mpi_set_langevin_gamma_rot(GammaType const &gamma) { - mpi_call_all(mpi_set_langevin_gamma_rot_local, gamma); -} - -void mpi_set_temperature_local(double temperature) { - ::temperature = temperature; - try { - System::get_system().on_temperature_change(); - } catch (std::exception const &err) { - runtimeErrorMsg() << err.what(); +void Thermostat::Thermostat::lb_coupling_deactivate() { + if (lb) { + if (get_system().lb.is_solver_set() and ::comm_cart.rank() == 0 and + lb->gamma > 0.) { + runtimeWarningMsg() + << "Recalculating forces, so the LB coupling forces are not " + "included in the particle force the first time step. This " + "only matters if it happens frequently during sampling."; + } + lb->couple_to_md = false; } - System::get_system().on_thermostat_param_change(); -} - -REGISTER_CALLBACK(mpi_set_temperature_local) - -void mpi_set_temperature(double temperature) { - mpi_call_all(mpi_set_temperature_local, temperature); -} - -void mpi_set_thermo_switch_local(int thermo_switch) { - ::thermo_switch = thermo_switch; -} - -REGISTER_CALLBACK(mpi_set_thermo_switch_local) - -void mpi_set_thermo_switch(int thermo_switch) { - mpi_call_all(mpi_set_thermo_switch_local, thermo_switch); } -#ifdef NPT -void mpi_set_nptiso_gammas_local(double gamma0, double gammav) { - npt_iso.gamma0 = gamma0; - npt_iso.gammav = gammav; - System::get_system().on_thermostat_param_change(); -} - -REGISTER_CALLBACK(mpi_set_nptiso_gammas_local) - -void mpi_set_nptiso_gammas(double gamma0, double gammav) { - mpi_call_all(mpi_set_nptiso_gammas_local, gamma0, gammav); -} -#endif - -void thermalized_bond_init(double time_step) { +void ThermalizedBondThermostat::recalc_prefactors(double time_step) { for (auto &kv : ::bonded_ia_params) { if (auto *bond = boost::get(&(*kv.second))) { bond->recalc_prefactors(time_step); diff --git a/src/core/thermostat.hpp b/src/core/thermostat.hpp index 2a002f39ad..88ed175641 100644 --- a/src/core/thermostat.hpp +++ b/src/core/thermostat.hpp @@ -26,85 +26,53 @@ */ #include "Particle.hpp" +#include "PropagationMode.hpp" #include "rotation.hpp" +#include "system/Leaf.hpp" #include "config/config.hpp" #include #include -#include - #include #include #include -/** \name Thermostat switches */ -/**@{*/ -#define THERMO_OFF 0 -#define THERMO_LANGEVIN 1 -#define THERMO_DPD 2 -#define THERMO_NPT_ISO 4 -#define THERMO_LB 8 -#define THERMO_BROWNIAN 16 -#define THERMO_SD 32 -/**@}*/ - namespace Thermostat { #ifdef PARTICLE_ANISOTROPY using GammaType = Utils::Vector3d; #else using GammaType = double; #endif -} // namespace Thermostat - -namespace { /** * @brief Value for unset friction coefficient. * Sentinel value for the Langevin/Brownian parameters, * indicating that they have not been set yet. */ #ifdef PARTICLE_ANISOTROPY -constexpr Thermostat::GammaType gamma_sentinel{{-1.0, -1.0, -1.0}}; +constexpr GammaType gamma_sentinel{{-1.0, -1.0, -1.0}}; #else -constexpr Thermostat::GammaType gamma_sentinel{-1.0}; +constexpr GammaType gamma_sentinel{-1.0}; #endif /** * @brief Value for a null friction coefficient. */ #ifdef PARTICLE_ANISOTROPY -constexpr Thermostat::GammaType gamma_null{{0.0, 0.0, 0.0}}; +constexpr GammaType gamma_null{{0.0, 0.0, 0.0}}; #else -constexpr Thermostat::GammaType gamma_null{0.0}; +constexpr GammaType gamma_null{0.0}; #endif -} // namespace - -/************************************************ - * exported variables - ************************************************/ - -/** Switch determining which thermostat(s) to use. This is a or'd value - * of the different possible thermostats (defines: \ref THERMO_OFF, - * \ref THERMO_LANGEVIN, \ref THERMO_DPD \ref THERMO_NPT_ISO). If it - * is zero all thermostats are switched off and the temperature is - * set to zero. - */ -extern int thermo_switch; - -/** Temperature of the thermostat. */ -extern double temperature; #ifdef THERMOSTAT_PER_PARTICLE -inline auto const & -handle_particle_gamma(Thermostat::GammaType const &particle_gamma, - Thermostat::GammaType const &default_gamma) { +inline auto const &handle_particle_gamma(GammaType const &particle_gamma, + GammaType const &default_gamma) { return particle_gamma >= gamma_null ? particle_gamma : default_gamma; } #endif -inline auto -handle_particle_anisotropy(Particle const &p, - Thermostat::GammaType const &gamma_body) { +inline auto handle_particle_anisotropy(Particle const &p, + GammaType const &gamma_body) { #ifdef PARTICLE_ANISOTROPY auto const aniso_flag = (gamma_body[0] != gamma_body[1]) || (gamma_body[1] != gamma_body[2]); @@ -118,30 +86,45 @@ handle_particle_anisotropy(Particle const &p, #endif } -/************************************************ - * parameter structs - ************************************************/ +/** @brief Check that two kT values are close up to a small tolerance. */ +inline bool are_kT_equal(double old_kT, double new_kT) { + constexpr auto relative_tolerance = 1e-6; + if (old_kT == 0. and new_kT == 0.) { + return true; + } + if ((old_kT < 0. and new_kT >= 0.) or (old_kT >= 0. and new_kT < 0.)) { + return false; + } + auto const large_kT = (old_kT > new_kT) ? old_kT : new_kT; + auto const small_kT = (old_kT > new_kT) ? new_kT : old_kT; + return (large_kT / small_kT - 1. < relative_tolerance); +} +} // namespace Thermostat struct BaseThermostat { public: /** Initialize or re-initialize the RNG counter with a seed. */ - void rng_initialize(uint32_t const seed) { m_rng_seed = seed; } + void rng_initialize(uint32_t const seed) { + m_rng_seed = seed; + m_initialized = true; + } /** Increment the RNG counter */ void rng_increment() { m_rng_counter.increment(); } /** Get current value of the RNG */ uint64_t rng_counter() const { return m_rng_counter.value(); } void set_rng_counter(uint64_t value) { - m_rng_counter = Utils::Counter(0u, value); + m_rng_counter = Utils::Counter(uint64_t{0u}, value); } /** Is the RNG seed required */ - bool is_seed_required() const { return !m_rng_seed; } - uint32_t rng_seed() const { return m_rng_seed.value(); } + bool is_seed_required() const { return not m_initialized; } + uint32_t rng_seed() const { return m_rng_seed; } private: - /** RNG counter. */ - Utils::Counter m_rng_counter; - /** RNG seed */ - boost::optional m_rng_seed; + /** @brief RNG counter. */ + Utils::Counter m_rng_counter{uint64_t{0u}, uint64_t{0u}}; + /** @brief RNG seed. */ + uint32_t m_rng_seed{0u}; + bool m_initialized{false}; }; /** Thermostat for Langevin dynamics. */ @@ -154,13 +137,12 @@ struct LangevinThermostat : public BaseThermostat { * Needs to be called every time the parameters are changed. */ void recalc_prefactors(double kT, double time_step) { + // get_system().get_time_step() pref_friction = -gamma; pref_noise = sigma(kT, time_step, gamma); - // If gamma_rotation is not set explicitly, use the translational one. - if (gamma_rotation < GammaType{}) { - gamma_rotation = gamma; - } +#ifdef ROTATION pref_noise_rotation = sigma(kT, time_step, gamma_rotation); +#endif // ROTATION } /** Calculate the noise prefactor. * Evaluates the quantity @f$ \sqrt{2 k_B T \gamma / dt} / \sigma_\eta @f$ @@ -175,24 +157,28 @@ struct LangevinThermostat : public BaseThermostat { /** @name Parameters */ /**@{*/ /** Translational friction coefficient @f$ \gamma_{\text{trans}} @f$. */ - GammaType gamma = gamma_sentinel; + GammaType gamma = Thermostat::gamma_sentinel; +#ifdef ROTATION /** Rotational friction coefficient @f$ \gamma_{\text{rot}} @f$. */ - GammaType gamma_rotation = gamma_sentinel; + GammaType gamma_rotation = Thermostat::gamma_sentinel; +#endif // ROTATION /**@}*/ /** @name Prefactors */ /**@{*/ /** Prefactor for the friction. * Stores @f$ \gamma_{\text{trans}} @f$. */ - GammaType pref_friction; + GammaType pref_friction = Thermostat::gamma_sentinel; /** Prefactor for the translational velocity noise. * Stores @f$ \sqrt{2 k_B T \gamma_{\text{trans}} / dt} / \sigma_\eta @f$. */ - GammaType pref_noise; + GammaType pref_noise = Thermostat::gamma_sentinel; +#ifdef ROTATION /** Prefactor for the angular velocity noise. * Stores @f$ \sqrt{2 k_B T \gamma_{\text{rot}} / dt} / \sigma_\eta @f$. */ - GammaType pref_noise_rotation; + GammaType pref_noise_rotation = Thermostat::gamma_sentinel; +#endif // ROTATION /**@}*/ }; @@ -223,10 +209,6 @@ struct BrownianThermostat : public BaseThermostat { * They correspond to the friction tensor Z from the eq. (14.31) of * @cite schlick10a. */ - // If gamma_rotation is not set explicitly, use the translational one. - if (gamma_rotation < GammaType{}) { - gamma_rotation = gamma; - } sigma_vel_rotation = sigma(kT); sigma_pos_rotation = sigma(kT, gamma_rotation); #endif // ROTATION @@ -252,9 +234,9 @@ struct BrownianThermostat : public BaseThermostat { /** @name Parameters */ /**@{*/ /** Translational friction coefficient @f$ \gamma_{\text{trans}} @f$. */ - GammaType gamma = gamma_sentinel; + GammaType gamma = Thermostat::gamma_sentinel; /** Rotational friction coefficient @f$ \gamma_{\text{rot}} @f$. */ - GammaType gamma_rotation = gamma_sentinel; + GammaType gamma_rotation = Thermostat::gamma_sentinel; /**@}*/ /** @name Prefactors */ /**@{*/ @@ -263,21 +245,25 @@ struct BrownianThermostat : public BaseThermostat { * @f$ D_{\text{trans}} = k_B T/\gamma_{\text{trans}} @f$ * the translational diffusion coefficient. */ - GammaType sigma_pos = gamma_sentinel; + GammaType sigma_pos = Thermostat::gamma_sentinel; +#ifdef ROTATION /** Rotational noise standard deviation. * Stores @f$ \sqrt{2D_{\text{rot}}} @f$ with * @f$ D_{\text{rot}} = k_B T/\gamma_{\text{rot}} @f$ * the rotational diffusion coefficient. */ - GammaType sigma_pos_rotation = gamma_sentinel; + GammaType sigma_pos_rotation = Thermostat::gamma_sentinel; +#endif // ROTATION /** Translational velocity noise standard deviation. * Stores @f$ \sqrt{k_B T} @f$. */ - double sigma_vel = 0; + double sigma_vel = 0.; +#ifdef ROTATION /** Angular velocity noise standard deviation. * Stores @f$ \sqrt{k_B T} @f$. */ - double sigma_vel_rotation = 0; + double sigma_vel_rotation = 0.; +#endif // ROTATION /**@}*/ }; @@ -313,34 +299,47 @@ struct IsotropicNptThermostat : public BaseThermostat { /** @name Parameters */ /**@{*/ /** Friction coefficient of the particles @f$ \gamma^0 @f$ */ - double gamma0; + double gamma0 = 0.; /** Friction coefficient for the box @f$ \gamma^V @f$ */ - double gammav; + double gammav = 0.; /**@}*/ /** @name Prefactors */ /**@{*/ /** Particle velocity rescaling at half the time step. * Stores @f$ \gamma^{0}\cdot\frac{dt}{2} @f$. */ - double pref_rescale_0; + double pref_rescale_0 = 0.; /** Particle velocity rescaling noise standard deviation. * Stores @f$ \sqrt{k_B T \gamma^{0} dt} / \sigma_\eta @f$. */ - double pref_noise_0; + double pref_noise_0 = 0.; /** Volume rescaling at half the time step. * Stores @f$ \frac{\gamma^{V}}{Q}\cdot\frac{dt}{2} @f$. */ - double pref_rescale_V; + double pref_rescale_V = 0.; /** Volume rescaling noise standard deviation. * Stores @f$ \sqrt{k_B T \gamma^{V} dt} / \sigma_\eta @f$. */ - double pref_noise_V; + double pref_noise_V = 0.; /**@}*/ }; #endif +/** Thermostat for lattice-Boltzmann particle coupling. */ +struct LBThermostat : public BaseThermostat { + /** @name Parameters */ + /**@{*/ + /** Friction coefficient. */ + double gamma = -1.; + /** Internal flag to disable particle coupling during force recalculation. */ + bool couple_to_md = false; + /**@}*/ +}; + /** Thermostat for thermalized bonds. */ -struct ThermalizedBondThermostat : public BaseThermostat {}; +struct ThermalizedBondThermostat : public BaseThermostat { + void recalc_prefactors(double time_step); +}; #ifdef DPD /** Thermostat for dissipative particle dynamics. */ @@ -354,66 +353,38 @@ struct StokesianThermostat : public BaseThermostat { }; #endif -/************************************************ - * functions - ************************************************/ - -/** - * @brief Register MPI callbacks of thermostat objects - * - * @param thermostat The thermostat object name - */ -#define NEW_THERMOSTAT(thermostat) \ - void mpi_##thermostat##_set_rng_seed(uint32_t seed); \ - void thermostat##_set_rng_seed(uint32_t seed); \ - void thermostat##_set_rng_counter(uint64_t seed); - -NEW_THERMOSTAT(langevin) -NEW_THERMOSTAT(brownian) -#ifdef NPT -NEW_THERMOSTAT(npt_iso) -#endif -NEW_THERMOSTAT(thermalized_bond) -#ifdef DPD -NEW_THERMOSTAT(dpd) -#endif -#ifdef STOKESIAN_DYNAMICS -NEW_THERMOSTAT(stokesian) -#endif - -/* Exported thermostat globals */ -extern LangevinThermostat langevin; -extern BrownianThermostat brownian; +namespace Thermostat { +class Thermostat : public System::Leaf { +public: + /** @brief Thermal energy of the simulated heat bath. */ + double kT = -1.; + /** @brief Bitmask of currently active thermostats. */ + int thermo_switch = THERMO_OFF; + std::shared_ptr langevin; + std::shared_ptr brownian; #ifdef NPT -extern IsotropicNptThermostat npt_iso; + std::shared_ptr npt_iso; #endif -extern ThermalizedBondThermostat thermalized_bond; + std::shared_ptr lb; #ifdef DPD -extern DPDThermostat dpd; + std::shared_ptr dpd; #endif #ifdef STOKESIAN_DYNAMICS -extern StokesianThermostat stokesian; + std::shared_ptr stokesian; #endif + std::shared_ptr thermalized_bond; -/** Initialize constants of the thermostat at the start of integration */ -void thermo_init(double time_step); - -/** Increment RNG counters */ -void philox_counter_increment(); - -void mpi_set_brownian_gamma(Thermostat::GammaType const &gamma); -void mpi_set_brownian_gamma_rot(Thermostat::GammaType const &gamma); - -void mpi_set_langevin_gamma(Thermostat::GammaType const &gamma); -void mpi_set_langevin_gamma_rot(Thermostat::GammaType const &gamma); - -void mpi_set_temperature(double temperature); -void mpi_set_temperature_local(double temperature); + /** Increment RNG counters */ + void philox_counter_increment(); -void mpi_set_thermo_switch(int thermo_switch); -void mpi_set_thermo_switch_local(int thermo_switch); + /** Initialize constants of all thermostats. */ + void recalc_prefactors(double time_step); -#ifdef NPT -void mpi_set_nptiso_gammas(double gamma0, double gammav); -void mpi_set_nptiso_gammas_local(double gamma0, double gammav); -#endif + void lb_coupling_activate() { + if (lb) { + lb->couple_to_md = true; + } + } + void lb_coupling_deactivate(); +}; +} // namespace Thermostat diff --git a/src/core/thermostats/brownian_inline.hpp b/src/core/thermostats/brownian_inline.hpp index 3f2a74cf0a..4d70e5977f 100644 --- a/src/core/thermostats/brownian_inline.hpp +++ b/src/core/thermostats/brownian_inline.hpp @@ -145,7 +145,7 @@ inline Utils::Vector3d bd_drag_vel(Thermostat::GammaType const &brownian_gamma, * @param[in] brownian Parameters * @param[in] p Particle * @param[in] dt Time step - * @param[in] kT Temperature + * @param[in] kT Thermal energy */ inline Utils::Vector3d bd_random_walk(BrownianThermostat const &brownian, Particle const &p, double dt, double kT) { @@ -311,7 +311,7 @@ bd_drag_vel_rot(Thermostat::GammaType const &brownian_gamma_rotation, * @param[in] brownian Parameters * @param[in] p Particle * @param[in] dt Time step - * @param[in] kT Temperature + * @param[in] kT Thermal energy */ inline Utils::Quaternion bd_random_walk_rot(BrownianThermostat const &brownian, Particle const &p, diff --git a/src/core/thermostats/langevin_inline.hpp b/src/core/thermostats/langevin_inline.hpp index 64935d0f4e..1f40e90889 100644 --- a/src/core/thermostats/langevin_inline.hpp +++ b/src/core/thermostats/langevin_inline.hpp @@ -32,17 +32,15 @@ #include /** Langevin thermostat for particle translational velocities. - * Collects the particle velocity (different for ENGINE, PARTICLE_ANISOTROPY). - * Collects the langevin parameters kT, gamma (different for - * THERMOSTAT_PER_PARTICLE). Applies the noise and friction term. * @param[in] langevin Parameters * @param[in] p Particle * @param[in] time_step Time step - * @param[in] kT Temperature + * @param[in] kT Thermal energy */ inline Utils::Vector3d friction_thermo_langevin(LangevinThermostat const &langevin, Particle const &p, double time_step, double kT) { + using namespace Thermostat; // Determine prefactors for the friction and the noise term #ifdef THERMOSTAT_PER_PARTICLE auto const gamma = handle_particle_gamma(p.gamma(), langevin.gamma); @@ -62,18 +60,16 @@ friction_thermo_langevin(LangevinThermostat const &langevin, Particle const &p, #ifdef ROTATION /** Langevin thermostat for particle angular velocities. - * Collects the particle velocity (different for PARTICLE_ANISOTROPY). - * Collects the langevin parameters kT, gamma_rot (different for - * THERMOSTAT_PER_PARTICLE). Applies the noise and friction term. * @param[in] langevin Parameters * @param[in] p Particle * @param[in] time_step Time step - * @param[in] kT Temperature + * @param[in] kT Thermal energy */ inline Utils::Vector3d friction_thermo_langevin_rotation(LangevinThermostat const &langevin, Particle const &p, double time_step, double kT) { + using namespace Thermostat; #ifdef THERMOSTAT_PER_PARTICLE auto const gamma = diff --git a/src/core/unit_tests/EspressoSystemStandAlone_test.cpp b/src/core/unit_tests/EspressoSystemStandAlone_test.cpp index ee604d1f2b..257a2d7db7 100644 --- a/src/core/unit_tests/EspressoSystemStandAlone_test.cpp +++ b/src/core/unit_tests/EspressoSystemStandAlone_test.cpp @@ -445,6 +445,18 @@ BOOST_FIXTURE_TEST_CASE(espresso_system_stand_alone, ParticleFactory) { } } + // check propagator exceptions + { + auto &propagation = *espresso::system->propagation; + auto const old_integ_switch = propagation.integ_switch; + auto const old_default_propagation = propagation.default_propagation; + propagation.integ_switch = -1; + BOOST_CHECK_THROW(propagation.update_default_propagation(0), + std::runtime_error); + BOOST_CHECK_EQUAL(propagation.default_propagation, old_default_propagation); + propagation.integ_switch = old_integ_switch; + } + // check bond exceptions { BOOST_CHECK_THROW(throw BondResolutionError(), std::exception); diff --git a/src/core/unit_tests/Verlet_list_test.cpp b/src/core/unit_tests/Verlet_list_test.cpp index 04d2ca1f4d..54a7fed4db 100644 --- a/src/core/unit_tests/Verlet_list_test.cpp +++ b/src/core/unit_tests/Verlet_list_test.cpp @@ -86,7 +86,7 @@ struct IntegratorHelper : public ParticleFactory { #ifdef EXTERNAL_FORCES struct : public IntegratorHelper { void set_integrator() const override { - mpi_set_thermo_switch_local(THERMO_OFF); + espresso::system->thermostat->thermo_switch = THERMO_OFF; register_integrator(SteepestDescentParameters(0., 0.01, 100.)); espresso::system->propagation->set_integ_switch( INTEG_METHOD_STEEPEST_DESCENT); @@ -101,7 +101,7 @@ struct : public IntegratorHelper { struct : public IntegratorHelper { void set_integrator() const override { - mpi_set_thermo_switch_local(THERMO_OFF); + espresso::system->thermostat->thermo_switch = THERMO_OFF; espresso::system->propagation->set_integ_switch(INTEG_METHOD_NVT); } void set_particle_properties(int pid) const override { @@ -113,12 +113,15 @@ struct : public IntegratorHelper { #ifdef NPT struct : public IntegratorHelper { void set_integrator() const override { + auto &npt_iso = espresso::system->thermostat->npt_iso; ::nptiso = NptIsoParameters(1., 1e9, {true, true, true}, true); espresso::system->propagation->set_integ_switch(INTEG_METHOD_NPT_ISO); - mpi_set_temperature_local(1.); - mpi_npt_iso_set_rng_seed(0); - mpi_set_thermo_switch_local(thermo_switch | THERMO_NPT_ISO); - mpi_set_nptiso_gammas_local(0., 0.); // disable box volume change + espresso::system->thermostat->thermo_switch = THERMO_NPT_ISO; + espresso::system->thermostat->kT = 1.; + npt_iso = std::make_shared(); + npt_iso->rng_initialize(0u); + npt_iso->gammav = 0.; // disable box volume change + npt_iso->gamma0 = 0.; } void set_particle_properties(int pid) const override { set_particle_v(pid, {20., 0., 0.}); diff --git a/src/core/unit_tests/ek_interface_test.cpp b/src/core/unit_tests/ek_interface_test.cpp index fcb2880c9e..b2f5a9659f 100644 --- a/src/core/unit_tests/ek_interface_test.cpp +++ b/src/core/unit_tests/ek_interface_test.cpp @@ -181,14 +181,22 @@ BOOST_AUTO_TEST_CASE(ek_interface_walberla) { BOOST_REQUIRE(not espresso::ek_container->contains(ek_species)); ek.propagate(); // no-op BOOST_REQUIRE_EQUAL(get_n_runtime_errors(), 0); - } - { - // EK prevents changing most of the system state - BOOST_CHECK_THROW(ek.on_boxl_change(), std::runtime_error); - BOOST_CHECK_THROW(ek.on_timestep_change(), std::runtime_error); - BOOST_CHECK_THROW(ek.on_temperature_change(), std::runtime_error); - BOOST_CHECK_THROW(ek.on_node_grid_change(), std::runtime_error); + { + // EK prevents changing most of the system state + ek.veto_kT(params.kT + 1.); + espresso::ek_container->add(ek_species); + BOOST_CHECK_THROW(ek.veto_kT(params.kT + 1.), std::runtime_error); + BOOST_CHECK_THROW(ek.on_boxl_change(), std::runtime_error); + BOOST_CHECK_THROW(ek.veto_time_step(ek.get_tau() * 2.), + std::invalid_argument); + BOOST_CHECK_THROW(ek.veto_time_step(ek.get_tau() / 2.5), + std::invalid_argument); + BOOST_CHECK_THROW(ek.on_node_grid_change(), std::runtime_error); + ek.on_timestep_change(); + ek.on_temperature_change(); + espresso::ek_container->remove(ek_species); + } } } #endif // WALBERLA @@ -208,6 +216,7 @@ BOOST_AUTO_TEST_CASE(ek_interface_none) { BOOST_CHECK_THROW(ek.get_tau(), NoEKActive); BOOST_CHECK_THROW(ek.sanity_checks(), NoEKActive); BOOST_CHECK_THROW(ek.veto_time_step(0.), NoEKActive); + BOOST_CHECK_THROW(ek.veto_kT(0.), NoEKActive); BOOST_CHECK_THROW(ek.on_cell_structure_change(), NoEKActive); BOOST_CHECK_THROW(ek.on_boxl_change(), NoEKActive); BOOST_CHECK_THROW(ek.on_node_grid_change(), NoEKActive); diff --git a/src/core/unit_tests/lb_particle_coupling_test.cpp b/src/core/unit_tests/lb_particle_coupling_test.cpp index 50e512ceb5..2d4e6893dc 100644 --- a/src/core/unit_tests/lb_particle_coupling_test.cpp +++ b/src/core/unit_tests/lb_particle_coupling_test.cpp @@ -33,12 +33,13 @@ namespace utf = boost::unit_test; #include "ParticleFactory.hpp" #include "particle_management.hpp" +#include "BoxGeometry.hpp" +#include "LocalBox.hpp" #include "Particle.hpp" #include "cell_system/CellStructure.hpp" #include "cell_system/CellStructureType.hpp" #include "communication.hpp" #include "errorhandling.hpp" -#include "global_ghost_flags.hpp" #include "lb/LBNone.hpp" #include "lb/LBWalberla.hpp" #include "lb/particle_coupling.hpp" @@ -152,41 +153,37 @@ struct CleanupActorLB : public ParticleFactory { BOOST_FIXTURE_TEST_SUITE(suite, CleanupActorLB) -static void lb_lbcoupling_broadcast() { - boost::mpi::communicator world; - boost::mpi::broadcast(world, lb_particle_coupling, 0); -} - -BOOST_AUTO_TEST_CASE(activate) { - lb_lbcoupling_deactivate(); - lb_lbcoupling_broadcast(); - lb_lbcoupling_activate(); - lb_lbcoupling_broadcast(); - BOOST_CHECK(lb_particle_coupling.couple_to_md); +BOOST_AUTO_TEST_CASE(lb_reactivate) { + espresso::system->thermostat->lb_coupling_deactivate(); + espresso::system->thermostat->lb_coupling_activate(); + BOOST_CHECK(espresso::system->thermostat->lb->couple_to_md); } -BOOST_AUTO_TEST_CASE(de_activate) { - lb_lbcoupling_activate(); - lb_lbcoupling_broadcast(); - lb_lbcoupling_deactivate(); - lb_lbcoupling_broadcast(); - BOOST_CHECK(not lb_particle_coupling.couple_to_md); +BOOST_AUTO_TEST_CASE(lb_deactivate) { + espresso::system->thermostat->lb_coupling_activate(); + espresso::system->thermostat->lb_coupling_deactivate(); + BOOST_CHECK(not espresso::system->thermostat->lb->couple_to_md); } BOOST_AUTO_TEST_CASE(rng_initial_state) { - BOOST_CHECK(lb_lbcoupling_is_seed_required()); - BOOST_CHECK(!lb_particle_coupling.rng_counter_coupling); + BOOST_CHECK(espresso::system->thermostat->lb->is_seed_required()); + BOOST_CHECK(espresso::system->thermostat->lb->rng_counter() == 0ul); } BOOST_AUTO_TEST_CASE(rng) { - lb_lbcoupling_set_rng_state(17); - lb_particle_coupling.gamma = 0.2; auto &lb = espresso::system->lb; - - LB::ParticleCoupling coupling{lb, params.time_step, 1.}; - BOOST_REQUIRE(lb_particle_coupling.rng_counter_coupling); - BOOST_CHECK_EQUAL(lb_lbcoupling_get_rng_state(), 17); - BOOST_CHECK(not lb_lbcoupling_is_seed_required()); + auto &thermostat = *espresso::system->thermostat->lb; + auto const &box_geo = *espresso::system->box_geo; + auto const &local_box = *espresso::system->local_geo; + thermostat.rng_initialize(17u); + thermostat.set_rng_counter(11ul); + thermostat.gamma = 0.2; + + LB::ParticleCoupling coupling{thermostat, lb, box_geo, local_box, + params.time_step, 1.}; + BOOST_CHECK_EQUAL(thermostat.rng_seed(), 17u); + BOOST_CHECK_EQUAL(thermostat.rng_counter(), 11ul); + BOOST_CHECK(not thermostat.is_seed_required()); Particle test_partcl_1{}; test_partcl_1.id() = 1; Particle test_partcl_2{}; @@ -200,16 +197,17 @@ BOOST_AUTO_TEST_CASE(rng) { // Propagation queries kT from LB, so LB needs to be initialized espresso::set_lb_kT(1E-4); - lb_lbcoupling_propagate(); + thermostat.rng_increment(); - BOOST_REQUIRE(lb_particle_coupling.rng_counter_coupling); - BOOST_CHECK_EQUAL(lb_lbcoupling_get_rng_state(), 18); + BOOST_CHECK_EQUAL(thermostat.rng_seed(), 17u); + BOOST_CHECK_EQUAL(thermostat.rng_counter(), 12ul); auto const step2_random1 = coupling.get_noise_term(test_partcl_1); auto const step2_random2 = coupling.get_noise_term(test_partcl_2); BOOST_CHECK(step1_random1 != step2_random1); BOOST_CHECK(step1_random1 != step2_random2); - LB::ParticleCoupling coupling_unthermalized{lb, params.time_step, 0.}; + LB::ParticleCoupling coupling_unthermalized{ + thermostat, lb, box_geo, local_box, params.time_step, 0.}; auto const step3_norandom = coupling_unthermalized.get_noise_term(test_partcl_2); BOOST_CHECK((step3_norandom == Utils::Vector3d{0., 0., 0.})); @@ -218,7 +216,11 @@ BOOST_AUTO_TEST_CASE(rng) { BOOST_AUTO_TEST_CASE(drift_vel_offset) { Particle p{}; auto &lb = espresso::system->lb; - LB::ParticleCoupling coupling{lb, params.time_step}; + auto &thermostat = *espresso::system->thermostat->lb; + auto const &box_geo = *espresso::system->box_geo; + auto const &local_box = *espresso::system->local_geo; + LB::ParticleCoupling coupling{thermostat, lb, box_geo, local_box, + params.time_step}; BOOST_CHECK_EQUAL(coupling.lb_drift_velocity_offset(p).norm(), 0); Utils::Vector3d expected{}; #ifdef LB_ELECTROHYDRODYNAMICS @@ -232,15 +234,16 @@ BOOST_AUTO_TEST_CASE(drift_vel_offset) { BOOST_DATA_TEST_CASE(drag_force, bdata::make(kTs), kT) { espresso::set_lb_kT(kT); auto &lb = espresso::system->lb; + auto &thermostat = *espresso::system->thermostat->lb; Particle p{}; p.v() = {-2.5, 1.5, 2.}; p.pos() = espresso::lb_fluid->get_lattice().get_local_domain().first; - lb_lbcoupling_set_gamma(0.2); + thermostat.gamma = 0.2; Utils::Vector3d drift_offset{-1., 1., 1.}; // Drag force in quiescent fluid { - auto const observed = lb_drag_force(lb, p, p.pos(), drift_offset); + auto const observed = lb_drag_force(lb, 0.2, p, p.pos(), drift_offset); const Utils::Vector3d expected{0.3, -0.1, -.2}; BOOST_CHECK_SMALL((observed - expected).norm(), eps); } @@ -250,6 +253,9 @@ BOOST_DATA_TEST_CASE(drag_force, bdata::make(kTs), kT) { BOOST_DATA_TEST_CASE(swimmer_force, bdata::make(kTs), kT) { espresso::set_lb_kT(kT); auto &lb = espresso::system->lb; + auto &thermostat = *espresso::system->thermostat->lb; + auto const &box_geo = *espresso::system->box_geo; + auto const &local_box = *espresso::system->local_geo; auto const first_lb_node = espresso::lb_fluid->get_lattice().get_local_domain().first; Particle p{}; @@ -260,8 +266,9 @@ BOOST_DATA_TEST_CASE(swimmer_force, bdata::make(kTs), kT) { // swimmer coupling { - if (in_local_halo(p.pos())) { - LB::ParticleCoupling coupling{lb, params.time_step}; + if (in_local_halo(local_box, p.pos(), params.agrid)) { + LB::ParticleCoupling coupling{thermostat, lb, box_geo, local_box, + params.time_step}; coupling.kernel(p); auto const interpolated = LB::get_force_to_be_applied(p.pos()); auto const expected = @@ -282,7 +289,7 @@ BOOST_DATA_TEST_CASE(swimmer_force, bdata::make(kTs), kT) { }; if ((pos - p.pos()).norm() < 1e-6) continue; - if (in_local_halo(pos)) { + if (in_local_halo(local_box, pos, params.agrid)) { auto const interpolated = LB::get_force_to_be_applied(pos); BOOST_CHECK_SMALL(interpolated.norm(), eps); } @@ -293,7 +300,7 @@ BOOST_DATA_TEST_CASE(swimmer_force, bdata::make(kTs), kT) { // remove force of the particle from the fluid { - if (in_local_halo(p.pos())) { + if (in_local_halo(local_box, p.pos(), params.agrid)) { add_md_force(lb, p.pos(), -Utils::Vector3d{0., 0., p.swimming().f_swim}, params.time_step); auto const reset = LB::get_force_to_be_applied(p.pos()); @@ -304,25 +311,28 @@ BOOST_DATA_TEST_CASE(swimmer_force, bdata::make(kTs), kT) { #endif // ENGINE BOOST_DATA_TEST_CASE(particle_coupling, bdata::make(kTs), kT) { - espresso::set_lb_kT(kT); - lb_lbcoupling_set_rng_state(17); auto &lb = espresso::system->lb; + auto &thermostat = *espresso::system->thermostat->lb; + espresso::set_lb_kT(kT); + thermostat.rng_initialize(17u); + auto const &box_geo = *espresso::system->box_geo; + auto const &local_box = *espresso::system->local_geo; auto const first_lb_node = espresso::lb_fluid->get_lattice().get_local_domain().first; - auto const gamma = 0.2; - lb_lbcoupling_set_gamma(gamma); + thermostat.gamma = 0.2; Particle p{}; - LB::ParticleCoupling coupling{lb, params.time_step}; + LB::ParticleCoupling coupling{thermostat, lb, box_geo, local_box, + params.time_step}; auto expected = coupling.get_noise_term(p); #ifdef LB_ELECTROHYDRODYNAMICS p.mu_E() = Utils::Vector3d{-2., 1.5, 1.}; - expected += gamma * p.mu_E(); + expected += thermostat.gamma * p.mu_E(); #endif p.pos() = first_lb_node + Utils::Vector3d::broadcast(0.5); // coupling { - if (in_local_halo(p.pos())) { + if (in_local_halo(local_box, p.pos(), params.agrid)) { coupling.kernel(p); BOOST_CHECK_SMALL((p.force() - expected).norm(), eps); @@ -334,7 +344,7 @@ BOOST_DATA_TEST_CASE(particle_coupling, bdata::make(kTs), kT) { // remove force of the particle from the fluid { - if (in_local_halo(p.pos())) { + if (in_local_halo(local_box, p.pos(), params.agrid)) { add_md_force(lb, p.pos(), -expected, params.time_step); } } @@ -342,14 +352,16 @@ BOOST_DATA_TEST_CASE(particle_coupling, bdata::make(kTs), kT) { BOOST_DATA_TEST_CASE_F(CleanupActorLB, coupling_particle_lattice_ia, bdata::make(kTs), kT) { + auto &lb = espresso::system->lb; + auto &thermostat = *espresso::system->thermostat->lb; auto const comm = boost::mpi::communicator(); auto const rank = comm.rank(); espresso::set_lb_kT(kT); - lb_lbcoupling_set_rng_state(17); + thermostat.rng_initialize(17u); auto &system = *espresso::system; auto &cell_structure = *system.cell_structure; - auto &lb = system.lb; auto const &box_geo = *system.box_geo; + auto const &local_box = *system.local_geo; auto const first_lb_node = espresso::lb_fluid->get_lattice().get_local_domain().first; auto const gamma = 0.2; @@ -371,7 +383,8 @@ BOOST_DATA_TEST_CASE_F(CleanupActorLB, coupling_particle_lattice_ia, set_particle_property(pid, &Particle::mu_E, Utils::Vector3d{-2., 1.5, 1.}); #endif - LB::ParticleCoupling coupling{lb, params.time_step}; + LB::ParticleCoupling coupling{thermostat, lb, box_geo, local_box, + params.time_step}; auto const p_opt = copy_particle_to_head_node(comm, system, pid); auto expected = Utils::Vector3d{}; if (rank == 0) { @@ -384,12 +397,13 @@ BOOST_DATA_TEST_CASE_F(CleanupActorLB, coupling_particle_lattice_ia, boost::mpi::broadcast(comm, expected, 0); auto const p_pos = first_lb_node + Utils::Vector3d::broadcast(0.5); set_particle_pos(pid, p_pos); - lb_lbcoupling_set_gamma(gamma); + thermostat.gamma = gamma; for (bool with_ghosts : {false, true}) { { if (with_ghosts) { - cell_structure.update_ghosts_and_resort_particle(global_ghost_flags()); + cell_structure.update_ghosts_and_resort_particle( + system.get_global_ghost_flags()); } if (rank == 0) { auto const particles = cell_structure.local_particles(); @@ -415,7 +429,8 @@ BOOST_DATA_TEST_CASE_F(CleanupActorLB, coupling_particle_lattice_ia, auto const cutoff = 8 / world.size(); auto const agrid = params.agrid; { - auto const shifts = positions_in_halo({0., 0., 0.}, box_geo, agrid); + auto const shifts = + positions_in_halo({0., 0., 0.}, box_geo, local_box, agrid); BOOST_REQUIRE_EQUAL(shifts.size(), cutoff); for (std::size_t i = 0; i < shifts.size(); ++i) { BOOST_REQUIRE_EQUAL(shifts[i], reference_shifts[i]); @@ -423,14 +438,16 @@ BOOST_DATA_TEST_CASE_F(CleanupActorLB, coupling_particle_lattice_ia, } { auto const reference_shift = Utils::Vector3d{1., 1., 1.}; - auto const shifts = positions_in_halo({1., 1., 1.}, box_geo, agrid); + auto const shifts = + positions_in_halo({1., 1., 1.}, box_geo, local_box, agrid); BOOST_REQUIRE_EQUAL(shifts.size(), 1); BOOST_REQUIRE_EQUAL(shifts[0], reference_shift); } { auto const reference_origin = Utils::Vector3d{1., 2., 0.}; auto const reference_shift = Utils::Vector3d{1., 2., 8.}; - auto const shifts = positions_in_halo({1., 2., 0.}, box_geo, agrid); + auto const shifts = + positions_in_halo({1., 2., 0.}, box_geo, local_box, agrid); BOOST_REQUIRE_EQUAL(shifts.size(), 2); BOOST_REQUIRE_EQUAL(shifts[0], reference_origin); BOOST_REQUIRE_EQUAL(shifts[1], reference_shift); @@ -439,11 +456,8 @@ BOOST_DATA_TEST_CASE_F(CleanupActorLB, coupling_particle_lattice_ia, // check without LB coupling { - lb_lbcoupling_deactivate(); - lb_lbcoupling_broadcast(); - auto const particles = cell_structure.local_particles(); - auto const ghost_particles = cell_structure.ghost_particles(); - LB::couple_particles(particles, ghost_particles, params.time_step); + system.thermostat->lb_coupling_deactivate(); + system.lb_couple_particles(params.time_step); auto const p_opt = copy_particle_to_head_node(comm, system, pid); if (rank == 0) { auto const &p = *p_opt; @@ -453,10 +467,7 @@ BOOST_DATA_TEST_CASE_F(CleanupActorLB, coupling_particle_lattice_ia, // check with LB coupling { - lb_lbcoupling_activate(); - lb_lbcoupling_broadcast(); - auto const particles = cell_structure.local_particles(); - auto const ghost_particles = cell_structure.ghost_particles(); + system.thermostat->lb_coupling_activate(); Utils::Vector3d lb_before{}; { auto const p_opt = copy_particle_to_head_node(comm, system, pid); @@ -467,7 +478,7 @@ BOOST_DATA_TEST_CASE_F(CleanupActorLB, coupling_particle_lattice_ia, } } // couple particle to LB - LB::couple_particles(particles, ghost_particles, params.time_step); + system.lb_couple_particles(params.time_step); { auto const p_opt = copy_particle_to_head_node(comm, system, pid); if (rank == 0) { @@ -498,13 +509,6 @@ BOOST_DATA_TEST_CASE_F(CleanupActorLB, coupling_particle_lattice_ia, for (auto const &error_message : error_messages) { BOOST_CHECK_EQUAL(error_message.what(), error_message_ref); } - lb_particle_coupling.rng_counter_coupling = {}; - if (kT == 0.) { - BOOST_CHECK_EQUAL(coupling.get_noise_term(Particle{}).norm(), 0.); - } else { - BOOST_CHECK_THROW(coupling.get_noise_term(Particle{}), - std::runtime_error); - } } } @@ -514,9 +518,13 @@ BOOST_AUTO_TEST_CASE(runtime_exceptions) { // LB prevents changing most of the system state { BOOST_CHECK_THROW(lb.on_boxl_change(), std::runtime_error); - BOOST_CHECK_THROW(lb.on_timestep_change(), std::runtime_error); - BOOST_CHECK_THROW(lb.on_temperature_change(), std::runtime_error); + BOOST_CHECK_THROW(lb.veto_time_step(lb.get_tau() * 2.), + std::invalid_argument); + BOOST_CHECK_THROW(lb.veto_time_step(lb.get_tau() / 2.5), + std::invalid_argument); + BOOST_CHECK_THROW(lb.veto_kT(lb.get_kT() + 7.), std::runtime_error); BOOST_CHECK_THROW(lb.on_node_grid_change(), std::runtime_error); + lb.on_timestep_change(); } // check access out of the LB local domain { @@ -572,8 +580,6 @@ BOOST_AUTO_TEST_CASE(lb_exceptions) { BOOST_CHECK_THROW(LB::get_force_to_be_applied({-10., -10., -10.}), std::runtime_error); // coupling, interpolation, boundaries - BOOST_CHECK_THROW(lb_lbcoupling_get_rng_state(), std::runtime_error); - BOOST_CHECK_THROW(lb_lbcoupling_set_rng_state(0ul), std::runtime_error); BOOST_CHECK_THROW(lb.get_momentum(), exception); } @@ -611,6 +617,7 @@ BOOST_AUTO_TEST_CASE(lb_exceptions) { BOOST_CHECK_THROW(lb.get_momentum(), NoLBActive); BOOST_CHECK_THROW(lb.sanity_checks(), NoLBActive); BOOST_CHECK_THROW(lb.veto_time_step(0.), NoLBActive); + BOOST_CHECK_THROW(lb.veto_kT(0.), NoLBActive); BOOST_CHECK_THROW(lb.lebc_sanity_checks(0u, 1u), NoLBActive); BOOST_CHECK_THROW(lb.propagate(), NoLBActive); BOOST_CHECK_THROW(lb.on_cell_structure_change(), NoLBActive); @@ -632,6 +639,7 @@ int main(int argc, char **argv) { espresso::system->set_time_step(params.time_step); espresso::system->set_cell_structure_topology(CellStructureType::REGULAR); espresso::system->cell_structure->set_verlet_skin(params.skin); + espresso::system->thermostat->lb = std::make_shared(); ::System::set_system(espresso::system); boost::mpi::communicator world; diff --git a/src/core/unit_tests/thermostats_test.cpp b/src/core/unit_tests/thermostats_test.cpp index 2e6d00930b..564cbcfaca 100644 --- a/src/core/unit_tests/thermostats_test.cpp +++ b/src/core/unit_tests/thermostats_test.cpp @@ -61,7 +61,9 @@ template T thermostat_factory(Args... args) { #else thermostat.gamma = 2.0; #endif +#ifdef ROTATION thermostat.gamma_rotation = 3.0 * thermostat.gamma; +#endif thermostat.rng_initialize(0); thermostat.recalc_prefactors(args...); return thermostat; @@ -314,8 +316,6 @@ BOOST_AUTO_TEST_CASE(test_langevin_randomness) { #ifdef NPT BOOST_AUTO_TEST_CASE(test_npt_iso_randomness) { - extern int thermo_switch; - thermo_switch |= THERMO_NPT_ISO; constexpr double time_step = 1.0; constexpr double kT = 2.0; constexpr std::size_t const sample_size = 10'000; diff --git a/src/core/virtual_sites/lb_tracers.cpp b/src/core/virtual_sites/lb_tracers.cpp index b688a5ab13..9edea02a1d 100644 --- a/src/core/virtual_sites/lb_tracers.cpp +++ b/src/core/virtual_sites/lb_tracers.cpp @@ -21,44 +21,37 @@ #ifdef VIRTUAL_SITES_INERTIALESS_TRACERS #include "BoxGeometry.hpp" -#include "PropagationMode.hpp" +#include "LocalBox.hpp" #include "cell_system/CellStructure.hpp" #include "errorhandling.hpp" #include "forces.hpp" +#include "lb/Solver.hpp" #include "lb/particle_coupling.hpp" -#include "system/System.hpp" -struct DeferredActiveLBChecks { - DeferredActiveLBChecks() { - auto const &lb = System::get_system().lb; - m_value = lb.is_solver_set(); - } - auto operator()() const { return m_value; } - bool m_value; -}; - -static bool lb_active_check(DeferredActiveLBChecks const &check) { - if (not check()) { +static bool lb_sanity_checks(LB::Solver const &lb) { + if (not lb.is_solver_set()) { runtimeErrorMsg() << "LB needs to be active for inertialess tracers."; - return false; + return true; } - return true; + return false; } void lb_tracers_add_particle_force_to_fluid(CellStructure &cell_structure, - double time_step) { - DeferredActiveLBChecks check_lb_solver_set{}; - auto &system = System::get_system(); - auto const &box_geo = *system.box_geo; - auto const agrid = (check_lb_solver_set()) ? system.lb.get_agrid() : 0.; - auto const to_lb_units = (check_lb_solver_set()) ? 1. / agrid : 0.; + BoxGeometry const &box_geo, + LocalBox const &local_box, + LB::Solver &lb, double time_step) { + if (lb_sanity_checks(lb)) { + return; + } + auto const agrid = lb.get_agrid(); + auto const to_lb_units = 1. / agrid; // Distribute summed-up forces from physical particles to ghosts init_forces_ghosts(cell_structure.ghost_particles()); cell_structure.update_ghosts_and_resort_particle(Cells::DATA_PART_FORCE); // Keep track of ghost particles (ids) that have already been coupled - LB::CouplingBookkeeping bookkeeping{}; + LB::CouplingBookkeeping bookkeeping{cell_structure}; // Apply particle forces to the LB fluid at particle positions. // For physical particles, also set particle velocity = fluid velocity. for (auto const &particle_range : @@ -66,12 +59,10 @@ void lb_tracers_add_particle_force_to_fluid(CellStructure &cell_structure, for (auto const &p : particle_range) { if (!LB::is_tracer(p)) continue; - if (!lb_active_check(check_lb_solver_set)) { - return; - } if (bookkeeping.should_be_coupled(p)) { - for (auto pos : positions_in_halo(p.pos(), box_geo, agrid)) { - add_md_force(system.lb, pos * to_lb_units, p.force(), time_step); + for (auto const &pos : + positions_in_halo(p.pos(), box_geo, local_box, agrid)) { + add_md_force(lb, pos * to_lb_units, p.force(), time_step); } } } @@ -81,10 +72,11 @@ void lb_tracers_add_particle_force_to_fluid(CellStructure &cell_structure, init_forces_ghosts(cell_structure.ghost_particles()); } -void lb_tracers_propagate(CellStructure &cell_structure, double time_step) { - DeferredActiveLBChecks check_lb_solver_set{}; - auto &system = System::get_system(); - auto const &lb = system.lb; +void lb_tracers_propagate(CellStructure &cell_structure, LB::Solver const &lb, + double time_step) { + if (lb_sanity_checks(lb)) { + return; + } auto const verlet_skin = cell_structure.get_verlet_skin(); auto const verlet_skin_sq = verlet_skin * verlet_skin; @@ -92,9 +84,6 @@ void lb_tracers_propagate(CellStructure &cell_structure, double time_step) { for (auto &p : cell_structure.local_particles()) { if (!LB::is_tracer(p)) continue; - if (!lb_active_check(check_lb_solver_set)) { - return; - } p.v() = lb.get_coupling_interpolated_velocity(p.pos()); for (unsigned int i = 0u; i < 3u; i++) { if (!p.is_fixed_along(i)) { diff --git a/src/core/virtual_sites/lb_tracers.hpp b/src/core/virtual_sites/lb_tracers.hpp index 927d82de61..bbc9b4ce90 100644 --- a/src/core/virtual_sites/lb_tracers.hpp +++ b/src/core/virtual_sites/lb_tracers.hpp @@ -24,9 +24,15 @@ #ifdef VIRTUAL_SITES_INERTIALESS_TRACERS #include "BoxGeometry.hpp" +#include "LocalBox.hpp" +#include "cell_system/CellStructure.hpp" +#include "lb/Solver.hpp" void lb_tracers_add_particle_force_to_fluid(CellStructure &cell_structure, - double time_step); -void lb_tracers_propagate(CellStructure &cell_structure, double time_step); + BoxGeometry const &box_geo, + LocalBox const &local_box, + LB::Solver &lb, double time_step); +void lb_tracers_propagate(CellStructure &cell_structure, LB::Solver const &lb, + double time_step); #endif // VIRTUAL_SITES_INERTIALESS_TRACERS diff --git a/src/python/espressomd/interactions.py b/src/python/espressomd/interactions.py index 164f22112f..4f0600f308 100644 --- a/src/python/espressomd/interactions.py +++ b/src/python/espressomd/interactions.py @@ -1010,33 +1010,17 @@ class ThermalizedBond(BondedInteraction): distance vector of the particle pair. r_cut: :obj:`float`, optional Maximum distance beyond which the bond is considered broken. - seed : :obj:`int` - Seed of the philox RNG. Must be positive. - Required for the first thermalized bond in the system. Subsequent - thermalized bonds don't need a seed; if one is provided nonetheless, - it will overwrite the seed of all previously defined thermalized bonds, - even if the new bond is not added to the system. """ _so_name = "Interactions::ThermalizedBond" _type_number = BONDED_IA.THERMALIZED_DIST - def __init__(self, *args, **kwargs): - if kwargs and "sip" not in kwargs: - kwargs["rng_state"] = kwargs.get("rng_state") - super().__init__(*args, **kwargs) - - def _serialize(self): - params = self._ctor_params.copy() - params["rng_state"] = self.call_method("get_rng_state") - return params - def get_default_params(self): """Gets default values of optional parameters. """ - return {"r_cut": 0., "seed": None} + return {"r_cut": 0.} @script_interface_register diff --git a/src/python/espressomd/system.py b/src/python/espressomd/system.py index 1fd8802017..ed4e594b65 100644 --- a/src/python/espressomd/system.py +++ b/src/python/espressomd/system.py @@ -245,7 +245,6 @@ def __getstate__(self): checkpointable_properties.append("collision_detection") if has_features("WALBERLA"): checkpointable_properties += ["_lb", "_ekcontainer"] - checkpointable_properties += ["thermostat"] odict = collections.OrderedDict() odict["_system_handle"] = self.call_method("get_system_handle") diff --git a/src/python/espressomd/thermostat.pxd b/src/python/espressomd/thermostat.pxd deleted file mode 100644 index 5830f75948..0000000000 --- a/src/python/espressomd/thermostat.pxd +++ /dev/null @@ -1,122 +0,0 @@ -# -# Copyright (C) 2013-2022 The ESPResSo project -# -# This file is part of ESPResSo. -# -# ESPResSo is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# ESPResSo is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . -# -from libcpp cimport bool as cbool -from libc cimport stdint - -include "myconfig.pxi" -from .utils cimport Vector3d - -cdef extern from "thermostat.hpp": - double temperature - int thermo_switch - int THERMO_OFF - int THERMO_LANGEVIN - int THERMO_LB - int THERMO_NPT_ISO - int THERMO_DPD - int THERMO_BROWNIAN - int THERMO_SD - - cdef cppclass BaseThermostat: - stdint.uint32_t rng_seed() - stdint.uint64_t rng_counter() - cbool is_seed_required() - - IF PARTICLE_ANISOTROPY: - cdef cppclass LangevinThermostat(BaseThermostat): - Vector3d gamma_rotation - Vector3d gamma - cdef cppclass BrownianThermostat(BaseThermostat): - Vector3d gamma_rotation - Vector3d gamma - ELSE: - cdef cppclass LangevinThermostat(BaseThermostat): - double gamma_rotation - double gamma - cdef cppclass BrownianThermostat(BaseThermostat): - double gamma_rotation - double gamma - cdef cppclass IsotropicNptThermostat(BaseThermostat): - double gamma0 - double gammav - cdef cppclass ThermalizedBondThermostat(BaseThermostat): - pass - IF DPD: - cdef cppclass DPDThermostat(BaseThermostat): - pass - IF STOKESIAN_DYNAMICS: - cdef cppclass StokesianThermostat(BaseThermostat): - pass - - LangevinThermostat langevin - BrownianThermostat brownian - IsotropicNptThermostat npt_iso - ThermalizedBondThermostat thermalized_bond - IF DPD: - DPDThermostat dpd - IF STOKESIAN_DYNAMICS: - StokesianThermostat stokesian - - void langevin_set_rng_seed(stdint.uint32_t seed) - void brownian_set_rng_seed(stdint.uint32_t seed) - void npt_iso_set_rng_seed(stdint.uint32_t seed) - IF DPD: - void dpd_set_rng_seed(stdint.uint32_t seed) - IF STOKESIAN_DYNAMICS: - void stokesian_set_rng_seed(stdint.uint32_t seed) - - void langevin_set_rng_counter(stdint.uint64_t counter) - void brownian_set_rng_counter(stdint.uint64_t counter) - void npt_iso_set_rng_counter(stdint.uint64_t counter) - IF DPD: - void dpd_set_rng_counter(stdint.uint64_t counter) - IF STOKESIAN_DYNAMICS: - void stokesian_set_rng_counter(stdint.uint64_t counter) - - IF PARTICLE_ANISOTROPY: - void mpi_set_brownian_gamma(const Vector3d & gamma) - void mpi_set_brownian_gamma_rot(const Vector3d & gamma) - - void mpi_set_langevin_gamma(const Vector3d & gamma) - void mpi_set_langevin_gamma_rot(const Vector3d & gamma) - ELSE: - void mpi_set_brownian_gamma(const double & gamma) - void mpi_set_brownian_gamma_rot(const double & gamma) - - void mpi_set_langevin_gamma(const double & gamma) - void mpi_set_langevin_gamma_rot(const double & gamma) - - void mpi_set_temperature(double temperature) - void mpi_set_thermo_switch(int thermo_switch) - - IF NPT: - void mpi_set_nptiso_gammas(double gamma0, double gammav) - -cdef extern from "stokesian_dynamics/sd_interface.hpp": - IF STOKESIAN_DYNAMICS: - void set_sd_kT(double kT) except + - double get_sd_kT() - -cdef extern from "lb/particle_coupling.hpp": - void lb_lbcoupling_set_rng_state(stdint.uint64_t) except + - stdint.uint64_t lb_lbcoupling_get_rng_state() except + - void lb_lbcoupling_set_gamma(double) except + - double lb_lbcoupling_get_gamma() except + - cbool lb_lbcoupling_is_seed_required() except + - void mpi_bcast_lb_particle_coupling() diff --git a/src/python/espressomd/thermostat.py b/src/python/espressomd/thermostat.py new file mode 100644 index 0000000000..0d8f02d72b --- /dev/null +++ b/src/python/espressomd/thermostat.py @@ -0,0 +1,187 @@ +# +# Copyright (C) 2013-2022 The ESPResSo project +# +# This file is part of ESPResSo. +# +# ESPResSo is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# ESPResSo is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# + +from .script_interface import script_interface_register, ScriptInterfaceHelper + + +@script_interface_register +class Thermostat(ScriptInterfaceHelper): + """ + Container for the system thermostat. Multiple thermostats can be active + simultaneously. When setting up a thermostat raises an exception, e.g. + due to invalid parameters, the thermostat gets disabled. Therefore, + updating an already active thermostat with invalid parameters will + deactivate it. + + Methods + ------- + turn_off() + Turn off all thermostats. + + set_langevin() + Set the Langevin thermostat. + + Parameters + ----------- + kT : :obj:`float` + Thermal energy of the simulated heat bath. + gamma : :obj:`float` + Contains the friction coefficient of the bath. If the feature + ``PARTICLE_ANISOTROPY`` is compiled in, then ``gamma`` can be a list + of three positive floats, for the friction coefficient in each + cardinal direction. + gamma_rotation : :obj:`float`, optional + The same applies to ``gamma_rotation``, which requires the feature + ``ROTATION`` to work properly. But also accepts three floats + if ``PARTICLE_ANISOTROPY`` is also compiled in. + seed : :obj:`int` + Initial counter value (or seed) of the philox RNG. + Required on first activation of the Langevin thermostat. + Must be positive. + + set_brownian() + Set the Brownian thermostat. + + Parameters + ---------- + kT : :obj:`float` + Thermal energy of the simulated heat bath. + gamma : :obj:`float` + Contains the friction coefficient of the bath. If the feature + ``PARTICLE_ANISOTROPY`` is compiled in, then ``gamma`` can be a list + of three positive floats, for the friction coefficient in each + cardinal direction. + gamma_rotation : :obj:`float`, optional + The same applies to ``gamma_rotation``, which requires the feature + ``ROTATION`` to work properly. But also accepts three floats + if ``PARTICLE_ANISOTROPY`` is also compiled in. + seed : :obj:`int` + Initial counter value (or seed) of the philox RNG. + Required on first activation of the Brownian thermostat. + Must be positive. + + set_lb() + Set the LB thermostat. The kT parameter is automatically extracted + from the currently active LB solver. + + This thermostat requires the feature ``WALBERLA``. + + Parameters + ---------- + seed : :obj:`int` + Seed for the random number generator, required if kT > 0. + Must be positive. + gamma : :obj:`float` + Frictional coupling constant for the MD particle coupling. + + set_npt() + Set the NPT thermostat. + + Parameters + ---------- + kT : :obj:`float` + Thermal energy of the heat bath. + gamma0 : :obj:`float` + Friction coefficient of the bath. + gammav : :obj:`float` + Artificial friction coefficient for the volume fluctuations. + seed : :obj:`int` + Initial counter value (or seed) of the philox RNG. + Required on first activation of the Langevin thermostat. + Must be positive. + + set_dpd() + Set the DPD thermostat with required parameters 'kT'. + This also activates the DPD interactions. + + Parameters + ---------- + kT : :obj:`float` + Thermal energy of the heat bath. + seed : :obj:`int` + Initial counter value (or seed) of the philox RNG. + Required on first activation of the DPD thermostat. + Must be positive. + + set_stokesian() + Set the SD thermostat with required parameters. + + This thermostat requires the feature ``STOKESIAN_DYNAMICS``. + + Parameters + ---------- + kT : :obj:`float`, optional + Temperature. + seed : :obj:`int`, optional + Seed for the random number generator. + + """ + _so_name = "Thermostat::Thermostat" + _so_creation_policy = "GLOBAL" + _so_bind_methods = ( + "set_langevin", + "set_brownian", + "set_npt", + "set_dpd", + "set_lb", + "set_stokesian", + "set_thermalized_bond", + "turn_off") + + +@script_interface_register +class Langevin(ScriptInterfaceHelper): + _so_name = "Thermostat::Langevin" + _so_creation_policy = "GLOBAL" + + +@script_interface_register +class Brownian(ScriptInterfaceHelper): + _so_name = "Thermostat::Brownian" + _so_creation_policy = "GLOBAL" + + +@script_interface_register +class IsotropicNpt(ScriptInterfaceHelper): + _so_name = "Thermostat::IsotropicNpt" + _so_creation_policy = "GLOBAL" + + +@script_interface_register +class Stokesian(ScriptInterfaceHelper): + _so_name = "Thermostat::Stokesian" + _so_creation_policy = "GLOBAL" + + +@script_interface_register +class LBThermostat(ScriptInterfaceHelper): + _so_name = "Thermostat::LB" + _so_creation_policy = "GLOBAL" + + +@script_interface_register +class DPDThermostat(ScriptInterfaceHelper): + _so_name = "Thermostat::DPD" + _so_creation_policy = "GLOBAL" + + +@script_interface_register +class ThermalizedBond(ScriptInterfaceHelper): + _so_name = "Thermostat::ThermalizedBond" + _so_creation_policy = "GLOBAL" diff --git a/src/python/espressomd/thermostat.pyx b/src/python/espressomd/thermostat.pyx deleted file mode 100644 index ae269c4d38..0000000000 --- a/src/python/espressomd/thermostat.pyx +++ /dev/null @@ -1,663 +0,0 @@ -# -# Copyright (C) 2013-2022 The ESPResSo project -# -# This file is part of ESPResSo. -# -# ESPResSo is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# ESPResSo is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . -# -import functools -include "myconfig.pxi" -from . cimport utils - - -def AssertThermostatType(*allowed_thermostats): - """Assert that only a certain group of thermostats is active at a time. - - Decorator class to ensure that only specific combinations of thermostats - can be activated together by the user. Usage: - - .. code-block:: cython - - cdef class Thermostat: - @AssertThermostatType(THERMO_LANGEVIN, THERMO_DPD) - def set_langevin(self, kT=None, gamma=None, gamma_rotation=None, - seed=None): - ... - - This will prefix an assertion that prevents setting up the Langevin - thermostat if the list of active thermostats contains anything other - than the DPD and Langevin thermostats. - - Parameters - ---------- - allowed_thermostats : :obj:`str` - Allowed list of thermostats which are known to be compatible - with one another. - - """ - def decoratorfunction(function): - @functools.wraps(function, assigned=('__name__', '__doc__')) - def wrapper(*args, **kwargs): - if (not (thermo_switch in allowed_thermostats) and - (thermo_switch != THERMO_OFF)): - raise Exception( - "This combination of thermostats is not allowed!") - function(*args, **kwargs) - return wrapper - return decoratorfunction - - -cdef class Thermostat: - - # We have to cdef the state variable because it is a cdef class - cdef _state - cdef _LB_fluid - - def __init__(self): - self._state = None - pass - - def suspend(self): - """Suspend the thermostat - - The thermostat can be suspended, e.g. to perform an energy - minimization. - - """ - self._state = self.__getstate__() - self.turn_off() - - def recover(self): - """Recover a suspended thermostat - - If the thermostat had been suspended using :meth:`suspend`, it can - be recovered with this method. - - """ - if self._state is not None: - self.__setstate__(self._state) - - # __getstate__ and __setstate__ define the pickle interaction - def __getstate__(self): - # Attributes to pickle. - thermolist = self.get_state() - return thermolist - - def __setstate__(self, thermolist): - if thermolist == []: - return - - for thmst in thermolist: - if thmst["type"] == "OFF": - self.turn_off() - if thmst["type"] == "LANGEVIN": - self.set_langevin(kT=thmst["kT"], gamma=thmst["gamma"], - gamma_rotation=thmst["gamma_rotation"], - seed=thmst["seed"]) - langevin_set_rng_counter(thmst["counter"]) - if thmst["type"] == "LB": - # The LB fluid object is a deep copy of the original, we must - # attach it to be able to set up the particle coupling global - # variable, and then detach it to allow the original LB - # fluid object to be attached by the System class. - # This workaround will be removed when the thermostat class - # gets rewritten as a ScriptInterface class (#4266, #4594) - thmst["LB_fluid"].call_method("activate") - self.set_lb( - LB_fluid=thmst["LB_fluid"], - gamma=thmst["gamma"], - seed=thmst["rng_counter_fluid"]) - thmst["LB_fluid"].call_method("deactivate") - if thmst["type"] == "NPT_ISO": - if NPT: - self.set_npt(kT=thmst["kT"], gamma0=thmst["gamma0"], - gammav=thmst["gammav"], seed=thmst["seed"]) - npt_iso_set_rng_counter(thmst["counter"]) - if thmst["type"] == "DPD": - if DPD: - self.set_dpd(kT=thmst["kT"], seed=thmst["seed"]) - dpd_set_rng_counter(thmst["counter"]) - if thmst["type"] == "BROWNIAN": - self.set_brownian(kT=thmst["kT"], gamma=thmst["gamma"], - gamma_rotation=thmst["gamma_rotation"], - seed=thmst["seed"]) - brownian_set_rng_counter(thmst["counter"]) - if thmst["type"] == "SD": - IF STOKESIAN_DYNAMICS: - self.set_stokesian(kT=thmst["kT"], seed=thmst["seed"]) - stokesian_set_rng_counter(thmst["counter"]) - - def get_ts(self): - return thermo_switch - - def get_state(self): - """Returns the thermostat status.""" - thermo_list = [] - if temperature == -1: - raise Exception("Thermostat is not initialized") - if thermo_switch == THERMO_OFF: - return [{"type": "OFF"}] - if thermo_switch & THERMO_LANGEVIN: - lang_dict = {} - lang_dict["type"] = "LANGEVIN" - lang_dict["kT"] = temperature - lang_dict["seed"] = langevin.rng_seed() - lang_dict["counter"] = langevin.rng_counter() - IF PARTICLE_ANISOTROPY: - lang_dict["gamma"] = [langevin.gamma[0], - langevin.gamma[1], - langevin.gamma[2]] - ELSE: - lang_dict["gamma"] = langevin.gamma - IF ROTATION: - IF PARTICLE_ANISOTROPY: - lang_dict["gamma_rotation"] = [langevin.gamma_rotation[0], - langevin.gamma_rotation[1], - langevin.gamma_rotation[2]] - ELSE: - lang_dict["gamma_rotation"] = langevin.gamma_rotation - ELSE: - lang_dict["gamma_rotation"] = None - - thermo_list.append(lang_dict) - if thermo_switch & THERMO_BROWNIAN: - lang_dict = {} - lang_dict["type"] = "BROWNIAN" - lang_dict["kT"] = temperature - lang_dict["seed"] = brownian.rng_seed() - lang_dict["counter"] = brownian.rng_counter() - IF PARTICLE_ANISOTROPY: - lang_dict["gamma"] = [brownian.gamma[0], - brownian.gamma[1], - brownian.gamma[2]] - ELSE: - lang_dict["gamma"] = brownian.gamma - IF ROTATION: - IF PARTICLE_ANISOTROPY: - lang_dict["gamma_rotation"] = [brownian.gamma_rotation[0], - brownian.gamma_rotation[1], - brownian.gamma_rotation[2]] - ELSE: - lang_dict["gamma_rotation"] = brownian.gamma_rotation - ELSE: - lang_dict["gamma_rotation"] = None - - thermo_list.append(lang_dict) - if thermo_switch & THERMO_LB: - lb_dict = {} - lb_dict["LB_fluid"] = self._LB_fluid - lb_dict["gamma"] = lb_lbcoupling_get_gamma() - lb_dict["type"] = "LB" - lb_dict["rng_counter_fluid"] = lb_lbcoupling_get_rng_state() - thermo_list.append(lb_dict) - if thermo_switch & THERMO_NPT_ISO: - if NPT: - npt_dict = {} - npt_dict["type"] = "NPT_ISO" - npt_dict["kT"] = temperature - npt_dict["seed"] = npt_iso.rng_seed() - npt_dict["counter"] = npt_iso.rng_counter() - npt_dict["gamma0"] = npt_iso.gamma0 - npt_dict["gammav"] = npt_iso.gammav - thermo_list.append(npt_dict) - if thermo_switch & THERMO_DPD: - IF DPD: - dpd_dict = {} - dpd_dict["type"] = "DPD" - dpd_dict["kT"] = temperature - dpd_dict["seed"] = dpd.rng_seed() - dpd_dict["counter"] = dpd.rng_counter() - thermo_list.append(dpd_dict) - if thermo_switch & THERMO_SD: - IF STOKESIAN_DYNAMICS: - sd_dict = {} - sd_dict["type"] = "SD" - sd_dict["kT"] = get_sd_kT() - sd_dict["seed"] = stokesian.rng_seed() - sd_dict["counter"] = stokesian.rng_counter() - thermo_list.append(sd_dict) - return thermo_list - - def _set_temperature(self, kT): - mpi_set_temperature(kT) - utils.handle_errors("Temperature change failed") - - def turn_off(self): - """ - Turns off all the thermostat and sets all the thermostat variables to zero. - - """ - - self._set_temperature(0.) - IF PARTICLE_ANISOTROPY: - mpi_set_langevin_gamma(utils.make_Vector3d((0., 0., 0.))) - mpi_set_brownian_gamma(utils.make_Vector3d((0., 0., 0.))) - IF ROTATION: - mpi_set_langevin_gamma_rot(utils.make_Vector3d((0., 0., 0.))) - mpi_set_brownian_gamma_rot(utils.make_Vector3d((0., 0., 0.))) - ELSE: - mpi_set_langevin_gamma(0.) - mpi_set_brownian_gamma(0.) - IF ROTATION: - mpi_set_langevin_gamma_rot(0.) - mpi_set_brownian_gamma_rot(0.) - - mpi_set_thermo_switch(THERMO_OFF) - lb_lbcoupling_set_gamma(0.0) - mpi_bcast_lb_particle_coupling() - - @AssertThermostatType(THERMO_LANGEVIN, THERMO_DPD, THERMO_LB) - def set_langevin(self, kT, gamma, gamma_rotation=None, seed=None): - """ - Sets the Langevin thermostat. - - Parameters - ----------- - kT : :obj:`float` - Thermal energy of the simulated heat bath. - gamma : :obj:`float` - Contains the friction coefficient of the bath. If the feature - ``PARTICLE_ANISOTROPY`` is compiled in, then ``gamma`` can be a list - of three positive floats, for the friction coefficient in each - cardinal direction. - gamma_rotation : :obj:`float`, optional - The same applies to ``gamma_rotation``, which requires the feature - ``ROTATION`` to work properly. But also accepts three floats - if ``PARTICLE_ANISOTROPY`` is also compiled in. - seed : :obj:`int` - Initial counter value (or seed) of the philox RNG. - Required on first activation of the Langevin thermostat. - Must be positive. - - """ - - scalar_gamma_def = True - scalar_gamma_rot_def = True - IF PARTICLE_ANISOTROPY: - if hasattr(gamma, "__iter__"): - scalar_gamma_def = False - else: - scalar_gamma_def = True - - IF PARTICLE_ANISOTROPY: - if hasattr(gamma_rotation, "__iter__"): - scalar_gamma_rot_def = False - else: - scalar_gamma_rot_def = True - - utils.check_type_or_throw_except(kT, 1, float, "kT must be a number") - if scalar_gamma_def: - utils.check_type_or_throw_except( - gamma, 1, float, "gamma must be a number") - else: - utils.check_type_or_throw_except( - gamma, 3, float, "diagonal elements of the gamma tensor must be numbers") - if gamma_rotation is not None: - if scalar_gamma_rot_def: - utils.check_type_or_throw_except( - gamma_rotation, 1, float, "gamma_rotation must be a number") - else: - utils.check_type_or_throw_except( - gamma_rotation, 3, float, "diagonal elements of the gamma_rotation tensor must be numbers") - - if scalar_gamma_def: - if float(kT) < 0. or float(gamma) < 0.: - raise ValueError( - "temperature and gamma must be positive numbers") - else: - if float(kT) < 0. or float(gamma[0]) < 0. or float( - gamma[1]) < 0. or float(gamma[2]) < 0.: - raise ValueError( - "temperature and diagonal elements of the gamma tensor must be positive numbers") - if gamma_rotation is not None: - if scalar_gamma_rot_def: - if float(gamma_rotation) < 0.: - raise ValueError( - "gamma_rotation must be positive number") - else: - if float(gamma_rotation[0]) < 0. or float( - gamma_rotation[1]) < 0. or float(gamma_rotation[2]) < 0.: - raise ValueError( - "diagonal elements of the gamma_rotation tensor must be positive numbers") - - # Seed is required if the RNG is not initialized - if seed is None and langevin.is_seed_required(): - raise ValueError( - "A seed has to be given as keyword argument on first activation of the thermostat") - - if seed is not None: - utils.check_type_or_throw_except( - seed, 1, int, "seed must be a positive integer") - if seed < 0: - raise ValueError("seed must be a positive integer") - langevin_set_rng_seed(seed) - - self._set_temperature(kT) - IF PARTICLE_ANISOTROPY: - cdef utils.Vector3d gamma_vec - if scalar_gamma_def: - for i in range(3): - gamma_vec[i] = gamma - else: - gamma_vec = utils.make_Vector3d(gamma) - IF ROTATION: - IF PARTICLE_ANISOTROPY: - cdef utils.Vector3d gamma_rot_vec - if gamma_rotation is None: - # rotational gamma is translational gamma - gamma_rot_vec = gamma_vec - else: - if scalar_gamma_rot_def: - for i in range(3): - gamma_rot_vec[i] = gamma_rotation - else: - gamma_rot_vec = utils.make_Vector3d(gamma_rotation) - ELSE: - if gamma_rotation is None: - # rotational gamma is translational gamma - gamma_rotation = gamma - - global thermo_switch - mpi_set_thermo_switch(thermo_switch | THERMO_LANGEVIN) - IF PARTICLE_ANISOTROPY: - mpi_set_langevin_gamma(gamma_vec) - IF ROTATION: - mpi_set_langevin_gamma_rot(gamma_rot_vec) - ELSE: - mpi_set_langevin_gamma(gamma) - IF ROTATION: - mpi_set_langevin_gamma_rot(gamma_rotation) - - @AssertThermostatType(THERMO_BROWNIAN) - def set_brownian(self, kT, gamma, gamma_rotation=None, seed=None): - """Sets the Brownian thermostat. - - Parameters - ----------- - kT : :obj:`float` - Thermal energy of the simulated heat bath. - gamma : :obj:`float` - Contains the friction coefficient of the bath. If the feature - ``PARTICLE_ANISOTROPY`` is compiled in, then ``gamma`` can be a list - of three positive floats, for the friction coefficient in each - cardinal direction. - gamma_rotation : :obj:`float`, optional - The same applies to ``gamma_rotation``, which requires the feature - ``ROTATION`` to work properly. But also accepts three floats - if ``PARTICLE_ANISOTROPY`` is also compiled in. - seed : :obj:`int` - Initial counter value (or seed) of the philox RNG. - Required on first activation of the Brownian thermostat. - Must be positive. - - """ - - scalar_gamma_def = True - scalar_gamma_rot_def = True - IF PARTICLE_ANISOTROPY: - if hasattr(gamma, "__iter__"): - scalar_gamma_def = False - else: - scalar_gamma_def = True - - IF PARTICLE_ANISOTROPY: - if hasattr(gamma_rotation, "__iter__"): - scalar_gamma_rot_def = False - else: - scalar_gamma_rot_def = True - - utils.check_type_or_throw_except(kT, 1, float, "kT must be a number") - if scalar_gamma_def: - utils.check_type_or_throw_except( - gamma, 1, float, "gamma must be a number") - else: - utils.check_type_or_throw_except( - gamma, 3, float, "diagonal elements of the gamma tensor must be numbers") - if gamma_rotation is not None: - if scalar_gamma_rot_def: - utils.check_type_or_throw_except( - gamma_rotation, 1, float, "gamma_rotation must be a number") - else: - utils.check_type_or_throw_except( - gamma_rotation, 3, float, "diagonal elements of the gamma_rotation tensor must be numbers") - - if scalar_gamma_def: - if float(kT) < 0. or float(gamma) < 0.: - raise ValueError( - "temperature and gamma must be positive numbers") - else: - if float(kT) < 0. or float(gamma[0]) < 0. or float( - gamma[1]) < 0. or float(gamma[2]) < 0.: - raise ValueError( - "temperature and diagonal elements of the gamma tensor must be positive numbers") - if gamma_rotation is not None: - if scalar_gamma_rot_def: - if float(gamma_rotation) < 0.: - raise ValueError( - "gamma_rotation must be positive number") - else: - if float(gamma_rotation[0]) < 0. or float( - gamma_rotation[1]) < 0. or float(gamma_rotation[2]) < 0.: - raise ValueError( - "diagonal elements of the gamma_rotation tensor must be positive numbers") - - # Seed is required if the RNG is not initialized - if seed is None and brownian.is_seed_required(): - raise ValueError( - "A seed has to be given as keyword argument on first activation of the thermostat") - - if seed is not None: - utils.check_type_or_throw_except( - seed, 1, int, "seed must be a positive integer") - if seed < 0: - raise ValueError("seed must be a positive integer") - brownian_set_rng_seed(seed) - - self._set_temperature(kT) - IF PARTICLE_ANISOTROPY: - cdef utils.Vector3d gamma_vec - if scalar_gamma_def: - for i in range(3): - gamma_vec[i] = gamma - else: - gamma_vec = utils.make_Vector3d(gamma) - IF ROTATION: - IF PARTICLE_ANISOTROPY: - cdef utils.Vector3d gamma_rot_vec - if gamma_rotation is None: - # rotational gamma is translational gamma - gamma_rot_vec = gamma_vec - else: - if scalar_gamma_rot_def: - for i in range(3): - gamma_rot_vec[i] = gamma_rotation - else: - gamma_rot_vec = utils.make_Vector3d(gamma_rotation) - ELSE: - if gamma_rotation is None: - # rotational gamma is translational gamma - gamma_rotation = gamma - - global thermo_switch - mpi_set_thermo_switch(thermo_switch | THERMO_BROWNIAN) - - IF PARTICLE_ANISOTROPY: - mpi_set_brownian_gamma(gamma_vec) - IF ROTATION: - mpi_set_brownian_gamma_rot(gamma_rot_vec) - ELSE: - mpi_set_brownian_gamma(gamma) - IF ROTATION: - mpi_set_brownian_gamma_rot(gamma_rotation) - - @AssertThermostatType(THERMO_LB, THERMO_DPD, THERMO_LANGEVIN) - def set_lb(self, seed=None, LB_fluid=None, gamma=0.0): - """ - Sets the LB thermostat. - - This thermostat requires the feature ``WALBERLA``. - - Parameters - ---------- - LB_fluid : :class:`~espressomd.lb.LBFluidWalberla` - seed : :obj:`int` - Seed for the random number generator, required if kT > 0. - Must be positive. - gamma : :obj:`float` - Frictional coupling constant for the MD particle coupling. - - """ - from .lb import HydrodynamicInteraction - if not isinstance(LB_fluid, HydrodynamicInteraction): - raise ValueError( - "The LB thermostat requires a LB instance as a keyword arg.") - - self._LB_fluid = LB_fluid - if LB_fluid.kT > 0.: - if seed is None and lb_lbcoupling_is_seed_required(): - raise ValueError( - "seed has to be given as keyword arg") - if seed is not None: - utils.check_type_or_throw_except( - seed, 1, int, "seed must be a positive integer") - if seed < 0: - raise ValueError("seed must be a positive integer") - lb_lbcoupling_set_rng_state(seed) - mpi_bcast_lb_particle_coupling() - else: - lb_lbcoupling_set_rng_state(0) - mpi_bcast_lb_particle_coupling() - - global thermo_switch - mpi_set_thermo_switch(thermo_switch | THERMO_LB) - - lb_lbcoupling_set_gamma(gamma) - mpi_bcast_lb_particle_coupling() - - IF NPT: - @AssertThermostatType(THERMO_NPT_ISO) - def set_npt(self, kT, gamma0, gammav, seed=None): - """ - Sets the NPT thermostat. - - Parameters - ---------- - kT : :obj:`float` - Thermal energy of the heat bath - gamma0 : :obj:`float` - Friction coefficient of the bath - gammav : :obj:`float` - Artificial friction coefficient for the volume fluctuations. - seed : :obj:`int` - Initial counter value (or seed) of the philox RNG. - Required on first activation of the Langevin thermostat. - Must be positive. - - """ - - utils.check_type_or_throw_except( - kT, 1, float, "kT must be a number") - - # Seed is required if the RNG is not initialized - if seed is None and npt_iso.is_seed_required(): - raise ValueError( - "A seed has to be given as keyword argument on first activation of the thermostat") - - if seed is not None: - utils.check_type_or_throw_except( - seed, 1, int, "seed must be a positive integer") - if seed < 0: - raise ValueError("seed must be a positive integer") - npt_iso_set_rng_seed(seed) - - self._set_temperature(kT) - global thermo_switch - mpi_set_thermo_switch(thermo_switch | THERMO_NPT_ISO) - mpi_set_nptiso_gammas(gamma0, gammav) - - IF DPD: - @AssertThermostatType(THERMO_DPD, THERMO_LANGEVIN, THERMO_LB) - def set_dpd(self, kT, seed=None): - """ - Sets the DPD thermostat with required parameters 'kT'. - This also activates the DPD interactions. - - Parameters - ---------- - kT : :obj:`float` - Thermal energy of the heat bath. - seed : :obj:`int` - Initial counter value (or seed) of the philox RNG. - Required on first activation of the DPD thermostat. - Must be positive. - - """ - utils.check_type_or_throw_except( - kT, 1, float, "kT must be a number") - - # Seed is required if the RNG is not initialized - if seed is None and dpd.is_seed_required(): - raise ValueError( - "A seed has to be given as keyword argument on first activation of the thermostat") - - if seed is not None: - utils.check_type_or_throw_except( - seed, 1, int, "seed must be a positive integer") - if seed < 0: - raise ValueError("seed must be a positive integer") - dpd_set_rng_seed(seed) - - self._set_temperature(kT) - global thermo_switch - mpi_set_thermo_switch(thermo_switch | THERMO_DPD) - - IF STOKESIAN_DYNAMICS: - @AssertThermostatType(THERMO_SD) - def set_stokesian(self, kT=None, seed=None): - """ - Sets the SD thermostat with required parameters. - - This thermostat requires the feature ``STOKESIAN_DYNAMICS``. - - Parameters - ---------- - kT : :obj:`float`, optional - Temperature. - seed : :obj:`int`, optional - Seed for the random number generator - - """ - - if (kT is None) or (kT == 0): - set_sd_kT(0.0) - if stokesian.is_seed_required(): - stokesian_set_rng_seed(0) - else: - utils.check_type_or_throw_except( - kT, 1, float, "kT must be a float") - set_sd_kT(kT) - - # Seed is required if the RNG is not initialized - if seed is None and stokesian.is_seed_required(): - raise ValueError( - "A seed has to be given as keyword argument on first activation of the thermostat") - if seed is not None: - utils.check_type_or_throw_except( - seed, 1, int, "seed must be an integer") - if seed < 0: - raise ValueError("seed must be a positive integer") - stokesian_set_rng_seed(seed) - - global thermo_switch - mpi_set_thermo_switch(thermo_switch | THERMO_SD) diff --git a/src/script_interface/CMakeLists.txt b/src/script_interface/CMakeLists.txt index 94030a0639..14a9f75bd9 100644 --- a/src/script_interface/CMakeLists.txt +++ b/src/script_interface/CMakeLists.txt @@ -50,6 +50,7 @@ add_subdirectory(reaction_methods) add_subdirectory(scafacos) add_subdirectory(shapes) add_subdirectory(system) +add_subdirectory(thermostat) add_subdirectory(walberla) install(TARGETS espresso_script_interface diff --git a/src/script_interface/initialize.cpp b/src/script_interface/initialize.cpp index a114aae664..8ddda473d5 100644 --- a/src/script_interface/initialize.cpp +++ b/src/script_interface/initialize.cpp @@ -45,6 +45,7 @@ #include "reaction_methods/initialize.hpp" #include "shapes/initialize.hpp" #include "system/initialize.hpp" +#include "thermostat/initialize.hpp" #include "walberla/initialize.hpp" namespace ScriptInterface { @@ -71,6 +72,7 @@ void initialize(Utils::Factory *f) { Profiler::initialize(f); Shapes::initialize(f); System::initialize(f); + Thermostat::initialize(f); ReactionMethods::initialize(f); #ifdef H5MD Writer::initialize(f); diff --git a/src/script_interface/interactions/BondedInteraction.hpp b/src/script_interface/interactions/BondedInteraction.hpp index 2c04ad1055..9843ed82ff 100644 --- a/src/script_interface/interactions/BondedInteraction.hpp +++ b/src/script_interface/interactions/BondedInteraction.hpp @@ -409,22 +409,9 @@ class ThermalizedBond : public BondedInteractionImpl<::ThermalizedBond> { [this]() { return get_struct().gamma_distance; }}, {"r_cut", AutoParameter::read_only, [this]() { return get_struct().r_cut; }}, - {"seed", AutoParameter::read_only, - []() { return static_cast(thermalized_bond.rng_seed()); }}, }); } - Variant do_call_method(std::string const &name, - VariantMap const ¶ms) override { - if (name == "get_rng_state") { - auto const state = ::thermalized_bond.rng_counter(); - // check it's safe to forward the current state as an integer - assert(state < static_cast(std::numeric_limits::max())); - return static_cast(state); - } - return BondedInteraction::do_call_method(name, params); - } - private: void construct_bond(VariantMap const ¶ms) override { m_bonded_ia = std::make_shared<::Bonded_IA_Parameters>( @@ -433,33 +420,6 @@ class ThermalizedBond : public BondedInteractionImpl<::ThermalizedBond> { get_value(params, "temp_distance"), get_value(params, "gamma_distance"), get_value(params, "r_cut"))); - - if (is_none(params.at("seed"))) { - if (::thermalized_bond.is_seed_required()) { - throw std::invalid_argument("A parameter 'seed' has to be given on " - "first activation of a thermalized bond"); - } - } else { - auto const seed = get_value(params, "seed"); - if (seed < 0) { - throw std::domain_error("Parameter 'seed' must be >= 0"); - } - ::thermalized_bond.rng_initialize(static_cast(seed)); - } - - // handle checkpointing - if (params.count("rng_state") and not is_none(params.at("rng_state"))) { - auto const state = get_value(params, "rng_state"); - assert(state >= 0); - ::thermalized_bond.set_rng_counter(static_cast(state)); - } - } - - std::set get_valid_parameters() const override { - auto names = - BondedInteractionImpl::get_valid_parameters(); - names.insert("rng_state"); - return names; } }; diff --git a/src/script_interface/system/System.cpp b/src/script_interface/system/System.cpp index 1a8f1a4522..a6ffb25e8e 100644 --- a/src/script_interface/system/System.cpp +++ b/src/script_interface/system/System.cpp @@ -45,6 +45,7 @@ #include "script_interface/integrators/IntegratorHandle.hpp" #include "script_interface/lees_edwards/LeesEdwards.hpp" #include "script_interface/magnetostatics/Container.hpp" +#include "script_interface/thermostat/thermostat.hpp" #include #include @@ -72,6 +73,7 @@ struct System::Leaves { Leaves() = default; std::shared_ptr cell_system; std::shared_ptr integrator; + std::shared_ptr thermostat; std::shared_ptr analysis; std::shared_ptr comfixed; std::shared_ptr galilei; @@ -143,6 +145,7 @@ System::System() : m_instance{}, m_leaves{std::make_shared()} { }); add_parameter("cell_system", &Leaves::cell_system); add_parameter("integrator", &Leaves::integrator); + add_parameter("thermostat", &Leaves::thermostat); add_parameter("analysis", &Leaves::analysis); add_parameter("comfixed", &Leaves::comfixed); add_parameter("galilei", &Leaves::galilei); diff --git a/src/script_interface/thermostat/CMakeLists.txt b/src/script_interface/thermostat/CMakeLists.txt new file mode 100644 index 0000000000..f1f07590b3 --- /dev/null +++ b/src/script_interface/thermostat/CMakeLists.txt @@ -0,0 +1,20 @@ +# +# Copyright (C) 2023 The ESPResSo project +# +# This file is part of ESPResSo. +# +# ESPResSo is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# ESPResSo is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# + +target_sources(espresso_script_interface PRIVATE initialize.cpp) diff --git a/src/script_interface/thermostat/initialize.cpp b/src/script_interface/thermostat/initialize.cpp new file mode 100644 index 0000000000..b7908f1345 --- /dev/null +++ b/src/script_interface/thermostat/initialize.cpp @@ -0,0 +1,47 @@ +/* + * Copyright (C) 2023 The ESPResSo project + * + * This file is part of ESPResSo. + * + * ESPResSo is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * ESPResSo is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "initialize.hpp" + +#include "thermostat.hpp" + +namespace ScriptInterface { +namespace Thermostat { + +void initialize(Utils::Factory *om) { + om->register_new("Thermostat::Thermostat"); + om->register_new("Thermostat::Langevin"); + om->register_new("Thermostat::Brownian"); +#ifdef NPT + om->register_new("Thermostat::IsotropicNpt"); +#endif +#ifdef WALBERLA + om->register_new("Thermostat::LB"); +#endif +#ifdef DPD + om->register_new("Thermostat::DPD"); +#endif +#ifdef STOKESIAN_DYNAMICS + om->register_new("Thermostat::Stokesian"); +#endif + om->register_new("Thermostat::ThermalizedBond"); +} + +} // namespace Thermostat +} // namespace ScriptInterface diff --git a/src/core/global_ghost_flags.hpp b/src/script_interface/thermostat/initialize.hpp similarity index 71% rename from src/core/global_ghost_flags.hpp rename to src/script_interface/thermostat/initialize.hpp index 38d0dc5868..a86be5e1b0 100644 --- a/src/core/global_ghost_flags.hpp +++ b/src/script_interface/thermostat/initialize.hpp @@ -1,7 +1,5 @@ /* - * Copyright (C) 2010-2022 The ESPResSo project - * Copyright (C) 2002,2003,2004,2005,2006,2007,2008,2009,2010 - * Max-Planck-Institute for Polymer Research, Theory Group + * Copyright (C) 2023 The ESPResSo project * * This file is part of ESPResSo. * @@ -21,4 +19,14 @@ #pragma once -unsigned global_ghost_flags(); +#include + +#include + +namespace ScriptInterface { +namespace Thermostat { + +void initialize(Utils::Factory *om); + +} // namespace Thermostat +} // namespace ScriptInterface diff --git a/src/script_interface/thermostat/thermostat.hpp b/src/script_interface/thermostat/thermostat.hpp new file mode 100644 index 0000000000..1125f04200 --- /dev/null +++ b/src/script_interface/thermostat/thermostat.hpp @@ -0,0 +1,731 @@ +/* + * Copyright (C) 2023 The ESPResSo project + * + * This file is part of ESPResSo. + * + * ESPResSo is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * ESPResSo is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#pragma once + +#include "core/PropagationMode.hpp" +#include "core/bonded_interactions/bonded_interaction_data.hpp" +#include "core/thermostat.hpp" + +#include +#include +#include +#ifdef WALBERLA +#include +#endif + +#include +#include +#include +#include +#include + +namespace ScriptInterface { +namespace Thermostat { + +template +class Interface : public AutoParameters, System::Leaf> { + using BaseClass = AutoParameters, System::Leaf>; + +public: + using CoreThermostat = CoreClass; + using BaseClass::do_set_parameter; + using BaseClass::get_parameter; + using System::Leaf::bind_system; + +protected: + using BaseClass::add_parameters; + using BaseClass::context; + using BaseClass::get_parameter_insertion_order; + using System::Leaf::m_system; + + bool is_active = false; + std::shared_ptr m_handle; + /** @brief Basic lock mechanism that follows RAII. */ + std::weak_ptr m_edit_lock; + + void check_lock() { + if (m_edit_lock.expired()) { + throw AutoParameter::WriteError{}; + } + } + + void on_bind_system(::System::System &system) override { + get_member_handle(*system.thermostat) = m_handle; + system.on_thermostat_param_change(); + is_active = true; + } + + void on_detach_system(::System::System &system) override { + get_member_handle(*system.thermostat).reset(); + system.on_thermostat_param_change(); + is_active = false; + } + + void sanity_checks_positive(double value, std::string const &name) const { + if (value < 0.) { + throw std::domain_error("Parameter '" + name + "' cannot be negative"); + } + } + + void sanity_checks_positive(Utils::Vector3d const &value, + std::string const &name) const { + if (not(value >= Utils::Vector3d::broadcast(0.))) { + throw std::domain_error("Parameter '" + name + "' cannot be negative"); + } + } + + virtual bool invalid_rng_state(VariantMap const ¶ms) const { + return (not params.count("seed") or is_none(params.at("seed"))) and + is_seed_required(); + } + +private: + virtual std::shared_ptr & + get_member_handle(::Thermostat::Thermostat &thermostat) = 0; + + void set_new_parameters(VariantMap const ¶ms) { + if (params.count("__check_rng_state") and invalid_rng_state(params)) { + context()->parallel_try_catch([]() { + throw std::invalid_argument("Parameter 'seed' is needed on first " + "activation of the thermostat"); + }); + } + for (auto const &key : get_parameter_insertion_order()) { + if (params.count(key)) { + auto const &v = params.at(key); + if (key == "is_active") { + is_active = get_value(v); + } else { + do_set_parameter(key.c_str(), v); + } + } + } + } + +protected: + template + auto make_autoparameter(T CoreThermostat::*member, char const *name) { + return AutoParameter{ + name, + [this, member, name = std::string(name)](Variant const &v) { + check_lock(); + auto const value = get_value(v); + context()->parallel_try_catch( + [&]() { sanity_checks_positive(value, name); }); + m_handle.get()->*member = std::move(value); + }, + [this, member]() { return m_handle.get()->*member; }}; + } + + template + auto make_autogamma(T CoreThermostat::*member, char const *name) { + return AutoParameter{ + name, + [this, member, name = std::string(name)](Variant const &v) { + check_lock(); + if (is_none(v)) { + return; + } +#ifdef PARTICLE_ANISOTROPY + static_assert(std::is_same_v); + T gamma{}; + try { + gamma = T::broadcast(get_value(v)); + } catch (...) { + gamma = get_value(v); + } +#else + auto const gamma = get_value(v); +#endif // PARTICLE_ANISOTROPY + context()->parallel_try_catch( + [&]() { sanity_checks_positive(gamma, name); }); + m_handle.get()->*member = gamma; + }, + [this, member]() { + auto constexpr gamma_null = ::Thermostat::gamma_null; + auto const gamma = m_handle.get()->*member; + return (gamma >= gamma_null) ? Variant{gamma} : Variant{None{}}; + }}; + } + + virtual VariantMap extend_parameters(VariantMap const ¶meters) const { + auto params = parameters; + if (not is_seed_required()) { + for (auto key : {std::string("seed"), std::string("philox_counter")}) { + if (params.count(key) == 0ul) { + params[key] = get_parameter(key); + } + } + } + return params; + } + + Variant do_call_method(std::string const &name, + VariantMap const ¶ms) override { + if (name == "override_philox_counter") { + // only call this method if you know what you are doing + set_rng_counter(params.at("counter")); + return {}; + } + return {}; + } + +public: + Interface() { + add_parameters({ + {"seed", + [this](Variant const &v) { + check_lock(); + context()->parallel_try_catch([&]() { + if (not is_none(v)) + set_rng_seed(v); + }); + }, + [this]() { + return m_handle->is_seed_required() ? Variant{None{}} + : Variant{get_rng_seed()}; + }}, + {"philox_counter", + [this](Variant const &v) { + check_lock(); + context()->parallel_try_catch([&]() { + if (not is_none(v)) + set_rng_counter(v); + }); + }, + [this]() { return get_rng_counter(); }}, + {"is_active", AutoParameter::read_only, [this]() { return is_active; }}, + }); + } + + virtual std::optional extract_kT(VariantMap const ¶ms) const { + if (params.count("kT")) { + auto const value = get_value(params, "kT"); + sanity_checks_positive(value, "kT"); + return value; + } + return {std::nullopt}; + } + + auto release_lock() { + auto lock = std::make_shared(false); + m_edit_lock = lock; + return lock; + } + + auto is_activated() const { return is_active; } + + virtual bool is_seed_required() const { return m_handle->is_seed_required(); } + + auto get_rng_seed() const { + auto const seed = m_handle->rng_seed(); + assert(seed <= static_cast(std::numeric_limits::max())); + return static_cast(seed); + } + + auto get_rng_counter() const { + auto const counter = m_handle->rng_counter(); + assert(counter <= static_cast(std::numeric_limits::max())); + return static_cast(counter); + } + + void set_rng_seed(Variant const &value) { + auto const seed = get_value(value); + if (seed < 0) { + throw std::domain_error("Parameter 'seed' must be a positive integer"); + } + assert(static_cast(seed) <= + static_cast(std::numeric_limits::max())); + m_handle->rng_initialize(static_cast(seed)); + } + + void set_rng_counter(Variant const &value) { + auto const counter = get_value(value); + assert(counter >= 0); + assert(static_cast(counter) <= + std::numeric_limits::max()); + m_handle->set_rng_counter(static_cast(counter)); + } + + void do_construct(VariantMap const ¶ms) override { + m_handle = std::make_shared(); + if (not params.empty()) { + auto const read_write_lock = release_lock(); + set_new_parameters(params); + } + } + + void update_and_bind(VariantMap const ¶ms, bool was_active, + std::shared_ptr<::System::System> system) { + auto const old_handle = m_handle; + auto new_params = extend_parameters(params); + new_params["__global_kT"] = system->thermostat->kT; + new_params["__check_rng_state"] = true; + try { + m_handle = std::make_shared(); + set_new_parameters(new_params); + bind_system(system); + } catch (...) { + assert(not is_active); + m_handle = old_handle; + if (was_active) { + bind_system(system); + } + throw; + } + } + + virtual ::ThermostatFlags get_thermo_flag() const = 0; +}; + +class Langevin : public Interface<::LangevinThermostat> { + std::shared_ptr & + get_member_handle(::Thermostat::Thermostat &thermostat) override { + return thermostat.langevin; + } + +public: + Langevin() { + add_parameters({ + make_autogamma(&CoreThermostat::gamma, "gamma"), +#ifdef ROTATION + make_autogamma(&CoreThermostat::gamma_rotation, "gamma_rotation"), +#endif + }); + } + + ::ThermostatFlags get_thermo_flag() const final { return THERMO_LANGEVIN; } + +protected: + VariantMap extend_parameters(VariantMap const ¶meters) const override { + auto params = + Interface<::LangevinThermostat>::extend_parameters(parameters); +#ifdef ROTATION + // If gamma_rotation is not set explicitly, use the translational one. + if (params.count("gamma_rotation") == 0ul and params.count("gamma")) { + params["gamma_rotation"] = params.at("gamma"); + } +#endif // ROTATION + return params; + } +}; + +class Brownian : public Interface<::BrownianThermostat> { + std::shared_ptr & + get_member_handle(::Thermostat::Thermostat &thermostat) override { + return thermostat.brownian; + } + +public: + Brownian() { + add_parameters({ + make_autogamma(&CoreThermostat::gamma, "gamma"), +#ifdef ROTATION + make_autogamma(&CoreThermostat::gamma_rotation, "gamma_rotation"), +#endif + }); + } + + ::ThermostatFlags get_thermo_flag() const final { return THERMO_BROWNIAN; } + +protected: + VariantMap extend_parameters(VariantMap const ¶meters) const override { + auto params = + Interface<::BrownianThermostat>::extend_parameters(parameters); +#ifdef ROTATION + // If gamma_rotation is not set explicitly, use the translational one. + if (params.count("gamma_rotation") == 0ul and params.count("gamma")) { + params["gamma_rotation"] = params.at("gamma"); + } +#endif // ROTATION + return params; + } +}; + +#ifdef NPT +class IsotropicNpt : public Interface<::IsotropicNptThermostat> { + std::shared_ptr & + get_member_handle(::Thermostat::Thermostat &thermostat) override { + return thermostat.npt_iso; + } + +public: + IsotropicNpt() { + add_parameters({ + make_autoparameter(&CoreThermostat::gamma0, "gamma0"), + make_autoparameter(&CoreThermostat::gammav, "gammav"), + }); + } + + ::ThermostatFlags get_thermo_flag() const final { return THERMO_NPT_ISO; } +}; +#endif // NPT + +#ifdef WALBERLA +class LBThermostat : public Interface<::LBThermostat> { + std::shared_ptr & + get_member_handle(::Thermostat::Thermostat &thermostat) override { + return thermostat.lb; + } + +public: + LBThermostat() { + add_parameters({ + {"gamma", + [this](Variant const &v) { + check_lock(); + if (is_none(v)) { + return; + } + auto const gamma = get_value(v); + context()->parallel_try_catch( + [&]() { sanity_checks_positive(gamma, "gamma"); }); + m_handle->gamma = gamma; + }, + [this]() { + auto const gamma = m_handle->gamma; + return (gamma >= 0.) ? Variant{gamma} : Variant{None{}}; + }}, + }); + } + + ::ThermostatFlags get_thermo_flag() const final { return THERMO_LB; } + + std::optional extract_kT(VariantMap const ¶ms) const override { + auto const obj = + get_value>(params, "LB_fluid"); + auto const value = get_value(obj->get_parameter("kT")); + sanity_checks_positive(value, "kT"); + return value; + } + +protected: + bool invalid_rng_state(VariantMap const ¶ms) const override { + return (not params.count("seed") or is_none(params.at("seed"))) and + params.count("__global_kT") and is_seed_required() and + get_value(params, "__global_kT") != 0.; + } +}; +#endif // WALBERLA + +#ifdef DPD +class DPDThermostat : public Interface<::DPDThermostat> { + std::shared_ptr & + get_member_handle(::Thermostat::Thermostat &thermostat) override { + return thermostat.dpd; + } + +public: + ::ThermostatFlags get_thermo_flag() const final { return THERMO_DPD; } +}; +#endif + +#ifdef STOKESIAN_DYNAMICS +class Stokesian : public Interface<::StokesianThermostat> { + std::shared_ptr & + get_member_handle(::Thermostat::Thermostat &thermostat) override { + return thermostat.stokesian; + } + +public: + ::ThermostatFlags get_thermo_flag() const final { return THERMO_SD; } +}; +#endif + +class ThermalizedBond : public Interface<::ThermalizedBondThermostat> { + std::shared_ptr & + get_member_handle(::Thermostat::Thermostat &thermostat) override { + return thermostat.thermalized_bond; + } + +public: + ::ThermostatFlags get_thermo_flag() const final { return THERMO_BOND; } +}; + +class Thermostat : public AutoParameters { + std::shared_ptr langevin; + std::shared_ptr brownian; +#ifdef NPT + std::shared_ptr npt_iso; +#endif +#ifdef WALBERLA + std::shared_ptr lb; +#endif +#ifdef DPD + std::shared_ptr dpd; +#endif +#ifdef STOKESIAN_DYNAMICS + std::shared_ptr stokesian; +#endif + std::shared_ptr thermalized_bond; + std::shared_ptr<::Thermostat::Thermostat> m_handle; + std::unique_ptr m_params; + + template void apply(Fun fun) { + fun(*langevin); + fun(*brownian); +#ifdef NPT + fun(*npt_iso); +#endif +#ifdef WALBERLA + fun(*lb); +#endif +#ifdef DPD + fun(*dpd); +#endif +#ifdef STOKESIAN_DYNAMICS + fun(*stokesian); +#endif + fun(*thermalized_bond); + } + +protected: + template + auto make_autoparameter(T Thermostat::*member, char const *name) { + return AutoParameter{ + name, + [this, member, name = std::string(name)](Variant const &v) { + auto &thermostat = this->*member; + if (thermostat) { + throw WriteError{name}; + } + thermostat = get_value(v); + }, + [this, member]() { return this->*member; }}; + } + + template + void setup_thermostat(std::shared_ptr &thermostat, + VariantMap const ¶ms) { + auto const original_kT = m_handle->kT; + std::optional new_kT; + context()->parallel_try_catch( + [&]() { new_kT = thermostat->extract_kT(params); }); + auto const thermo_flag = thermostat->get_thermo_flag(); + if (new_kT) { + context()->parallel_try_catch( + [&]() { update_global_kT(original_kT, *new_kT, thermo_flag); }); + } + auto const was_active = thermostat->is_activated(); + turn_thermostat_off(*thermostat); + auto read_write_lock = thermostat->release_lock(); + context()->parallel_try_catch([&]() { + try { + if (new_kT) { + m_handle->kT = *new_kT; + } + thermostat->update_and_bind(params, was_active, m_system.lock()); + m_handle->thermo_switch |= thermo_flag; + } catch (...) { + auto success = false; + try { + m_handle->kT = original_kT; + if (was_active) { + m_handle->thermo_switch |= thermo_flag; + thermostat->bind_system(m_system.lock()); + } + success = true; + throw success; + } catch (...) { + assert(success && + "An exception occurred when setting up the thermostat. " + "An exception also occurred when attempting to restore the " + "original thermostat. The system is now in an invalid state."); + } + throw; + } + }); + } + + template void turn_thermostat_off(T &thermostat) { + auto const thermo_flag = thermostat.get_thermo_flag(); + if (m_handle->thermo_switch & thermo_flag) { + thermostat.detach_system(); + m_handle->thermo_switch &= ~thermo_flag; + if (m_handle->thermo_switch == 0) { + m_handle->kT = -1.; + } + } + } + + void update_global_kT(double old_kT, double new_kT, int thermo_flag) { + if (new_kT >= 0.) { + auto const same_kT = ::Thermostat::are_kT_equal(old_kT, new_kT); + auto const thermo_switch = m_handle->thermo_switch; + if (thermo_switch != THERMO_OFF and thermo_switch != thermo_flag and + thermo_switch != THERMO_BOND and not same_kT and old_kT >= 0.) { + throw std::runtime_error( + "Cannot set parameter 'kT' to " + std::to_string(new_kT) + + ": there are currently active thermostats with kT=" + + std::to_string(old_kT)); + } + get_system().check_kT(new_kT); + if (not same_kT) { + m_handle->kT = new_kT; + get_system().on_temperature_change(); + } + } + } + +public: + Thermostat() { + add_parameters({ + {"kT", AutoParameter::read_only, + [this]() { + return (m_handle->kT >= 0.) ? Variant{m_handle->kT} + : Variant{None{}}; + }}, + make_autoparameter(&Thermostat::langevin, "langevin"), + make_autoparameter(&Thermostat::brownian, "brownian"), +#ifdef NPT + make_autoparameter(&Thermostat::npt_iso, "npt_iso"), +#endif +#ifdef WALBERLA + make_autoparameter(&Thermostat::lb, "lb"), +#endif +#ifdef DPD + make_autoparameter(&Thermostat::dpd, "dpd"), +#endif +#ifdef STOKESIAN_DYNAMICS + make_autoparameter(&Thermostat::stokesian, "stokesian"), +#endif + make_autoparameter(&Thermostat::thermalized_bond, "thermalized_bond"), + }); + } + + Variant do_call_method(std::string const &name, + VariantMap const ¶ms) override { + if (name == "set_langevin") { + setup_thermostat(langevin, params); + return {}; + } + if (name == "set_brownian") { + setup_thermostat(brownian, params); + return {}; + } +#ifdef NPT + if (name == "set_npt") { + setup_thermostat(npt_iso, params); + return {}; + } +#endif // NPT +#ifdef WALBERLA + if (name == "set_lb") { + setup_thermostat(lb, params); + return {}; + } +#endif // WALBERLA +#ifdef DPD + if (name == "set_dpd") { + setup_thermostat(dpd, params); + return {}; + } +#endif // DPD +#ifdef STOKESIAN_DYNAMICS + if (name == "set_stokesian") { + setup_thermostat(stokesian, params); + return {}; + } +#endif // STOKESIAN_DYNAMICS + if (name == "set_thermalized_bond") { + setup_thermostat(thermalized_bond, params); + return {}; + } + if (name == "turn_off") { + apply([this](auto &thermostat) { turn_thermostat_off(thermostat); }); + assert(m_handle->thermo_switch == THERMO_OFF); + get_system().on_temperature_change(); + return {}; + } + return {}; + } + + void do_construct(VariantMap const ¶ms) override { + m_params = std::make_unique(params); + } + + void on_bind_system(::System::System &system) override { + assert(m_params != nullptr); + m_handle = system.thermostat; + auto const ¶ms = *m_params; + if (not params.empty()) { + reload_checkpointed_thermostats(params); + m_params.reset(); + return; + } + m_params.reset(); + if (not context()->is_head_node()) { + return; + } + make_default_constructed_thermostats(); + } + +private: + /** + * @brief Reload thermostats from checkpointed data. + */ + void reload_checkpointed_thermostats(VariantMap const ¶ms) { + for (auto const &key : get_parameter_insertion_order()) { + if (key != "kT") { + auto const &v = params.at(key); + do_set_parameter(key.c_str(), v); + } + } + if (not is_none(params.at("kT"))) { + m_handle->kT = get_value(params, "kT"); + } + apply([this](auto &thermostat) { + if (get_value(thermostat.get_parameter("is_active"))) { + thermostat.bind_system(m_system.lock()); + m_handle->thermo_switch |= thermostat.get_thermo_flag(); + } + }); + get_system().on_thermostat_param_change(); + } + + /** + * @brief Instantiate default-contructed thermostats. + * Can only be run on the head node! + */ + void make_default_constructed_thermostats() { + assert(context()->is_head_node()); + auto const make_thermostat = [this](char const *name, char const *so_name) { + set_parameter(name, Variant{context()->make_shared(so_name, {})}); + }; + make_thermostat("langevin", "Thermostat::Langevin"); + make_thermostat("brownian", "Thermostat::Brownian"); +#ifdef NPT + make_thermostat("npt_iso", "Thermostat::IsotropicNpt"); +#endif +#ifdef WALBERLA + make_thermostat("lb", "Thermostat::LB"); +#endif +#ifdef DPD + make_thermostat("dpd", "Thermostat::DPD"); +#endif +#ifdef STOKESIAN_DYNAMICS + make_thermostat("stokesian", "Thermostat::Stokesian"); +#endif + make_thermostat("thermalized_bond", "Thermostat::ThermalizedBond"); + } +}; + +} // namespace Thermostat +} // namespace ScriptInterface diff --git a/src/utils/include/utils/Counter.hpp b/src/utils/include/utils/Counter.hpp index 0501eefa24..dca2726b89 100644 --- a/src/utils/include/utils/Counter.hpp +++ b/src/utils/include/utils/Counter.hpp @@ -16,8 +16,8 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -#ifndef UTILS_COUNTER_HPP -#define UTILS_COUNTER_HPP + +#pragma once #include @@ -44,4 +44,3 @@ template class Counter { T initial_value() const { return m_initial; } }; } // namespace Utils -#endif // UTILS_COUNTER_HPP diff --git a/src/walberla_bridge/include/walberla_bridge/electrokinetics/reactions/EKReactionBase.hpp b/src/walberla_bridge/include/walberla_bridge/electrokinetics/reactions/EKReactionBase.hpp index bdfb8c1eb5..049bd3226d 100644 --- a/src/walberla_bridge/include/walberla_bridge/electrokinetics/reactions/EKReactionBase.hpp +++ b/src/walberla_bridge/include/walberla_bridge/electrokinetics/reactions/EKReactionBase.hpp @@ -51,7 +51,9 @@ class EKReactionBase { return m_coefficient; } [[nodiscard]] auto get_lattice() const noexcept { return m_lattice; } - [[nodiscard]] auto get_reactants() const noexcept { return m_reactants; } + [[nodiscard]] auto const &get_reactants() const noexcept { + return m_reactants; + } virtual void perform_reaction() = 0; }; diff --git a/testsuite/python/CMakeLists.txt b/testsuite/python/CMakeLists.txt index c89f315f0a..23763096f2 100644 --- a/testsuite/python/CMakeLists.txt +++ b/testsuite/python/CMakeLists.txt @@ -397,7 +397,7 @@ python_test(FILE ek_fixeddensity.py MAX_NUM_PROC 1) python_test(FILE ek_boundary.py MAX_NUM_PROC 2) python_test(FILE ek_slice.py MAX_NUM_PROC 2) python_test(FILE propagation_newton.py MAX_NUM_PROC 4) -python_test(FILE propagation_langevin.py MAX_NUM_PROC 1) +python_test(FILE propagation_langevin.py MAX_NUM_PROC 2) python_test(FILE propagation_brownian.py MAX_NUM_PROC 1) python_test(FILE propagation_lb.py MAX_NUM_PROC 2 GPU_SLOTS 1) python_test(FILE propagation_npt.py MAX_NUM_PROC 4 GPU_SLOTS 1) diff --git a/testsuite/python/dpd.py b/testsuite/python/dpd.py index 64f9dfc003..d69f4fe64a 100644 --- a/testsuite/python/dpd.py +++ b/testsuite/python/dpd.py @@ -59,9 +59,12 @@ def reset_particles(): gamma = 1.5 # No seed should throw exception - with self.assertRaisesRegex(ValueError, "A seed has to be given as keyword argument on first activation of the thermostat"): + with self.assertRaisesRegex(ValueError, "Parameter 'seed' is needed on first activation of the thermostat"): system.thermostat.set_dpd(kT=kT) + self.assertIsNone(system.thermostat.kT) + self.assertFalse(system.thermostat.dpd.is_active) + system.thermostat.set_dpd(kT=kT, seed=41) system.non_bonded_inter[0, 0].dpd.set_params( weight_function=0, gamma=gamma, r_cut=1.5, @@ -71,14 +74,20 @@ def reset_particles(): # force p = reset_particles() system.integrator.run(0, recalc_forces=True) + self.assertEqual(system.thermostat.dpd.seed, 41) + self.assertEqual(system.thermostat.dpd.philox_counter, 0) force0 = np.copy(p.f) system.integrator.run(0, recalc_forces=True) + self.assertEqual(system.thermostat.dpd.seed, 41) + self.assertEqual(system.thermostat.dpd.philox_counter, 0) force1 = np.copy(p.f) np.testing.assert_almost_equal(force0, force1) # run(1) should give a different force p = reset_particles() system.integrator.run(1) + self.assertEqual(system.thermostat.dpd.seed, 41) + self.assertEqual(system.thermostat.dpd.philox_counter, 1) force2 = np.copy(p.f) self.assertTrue(np.all(np.not_equal(force1, force2))) @@ -87,9 +96,13 @@ def reset_particles(): # force3: dpd.rng_counter() = 1, dpd.rng_seed() = 42 p = reset_particles() system.integrator.run(0, recalc_forces=True) + self.assertEqual(system.thermostat.dpd.seed, 41) + self.assertEqual(system.thermostat.dpd.philox_counter, 1) force2 = np.copy(p.f) system.thermostat.set_dpd(kT=kT, seed=42) system.integrator.run(0, recalc_forces=True) + self.assertEqual(system.thermostat.dpd.seed, 42) + self.assertEqual(system.thermostat.dpd.philox_counter, 1) force3 = np.copy(p.f) self.assertTrue(np.all(np.not_equal(force2, force3))) @@ -97,6 +110,8 @@ def reset_particles(): p = reset_particles() system.thermostat.set_dpd(kT=kT, seed=42) system.integrator.run(1) + self.assertEqual(system.thermostat.dpd.seed, 42) + self.assertEqual(system.thermostat.dpd.philox_counter, 2) force4 = np.copy(p.f) self.assertTrue(np.all(np.not_equal(force3, force4))) @@ -106,8 +121,12 @@ def reset_particles(): reset_particles() system.thermostat.set_dpd(kT=kT, seed=41) system.integrator.run(1) + self.assertEqual(system.thermostat.dpd.seed, 41) + self.assertEqual(system.thermostat.dpd.philox_counter, 3) p = reset_particles() system.integrator.run(0, recalc_forces=True) + self.assertEqual(system.thermostat.dpd.seed, 41) + self.assertEqual(system.thermostat.dpd.philox_counter, 3) force5 = np.copy(p.f) self.assertTrue(np.all(np.not_equal(force4, force5))) diff --git a/testsuite/python/drude.py b/testsuite/python/drude.py index d432cd87ca..72ba60832e 100644 --- a/testsuite/python/drude.py +++ b/testsuite/python/drude.py @@ -155,6 +155,7 @@ def test(self): kT=temperature_com, gamma=gamma_com, seed=42) + system.thermostat.set_thermalized_bond(seed=123) p3m = espressomd.electrostatics.P3M(prefactor=coulomb_prefactor, accuracy=1e-4, mesh=3 * [18], cao=5) @@ -165,7 +166,7 @@ def test(self): thermalized_dist_bond = espressomd.interactions.ThermalizedBond( temp_com=temperature_com, gamma_com=gamma_com, r_cut=1.0, - temp_distance=temperature_drude, gamma_distance=gamma_drude, seed=123) + temp_distance=temperature_drude, gamma_distance=gamma_drude) harmonic_bond = espressomd.interactions.HarmonicBond( k=k_drude, r_0=0.0, r_cut=1.0) system.bonded_inter.add(thermalized_dist_bond) diff --git a/testsuite/python/ek_interface.py b/testsuite/python/ek_interface.py index a4da6f2fde..e439edea90 100644 --- a/testsuite/python/ek_interface.py +++ b/testsuite/python/ek_interface.py @@ -218,9 +218,10 @@ def test_parameter_change_exceptions(self): self.system.ekcontainer.add(ek_species) self.system.ekcontainer.solver = ek_solver with self.assertRaisesRegex(Exception, "Temperature change not supported by EK"): - self.system.thermostat.turn_off() - with self.assertRaisesRegex(Exception, "Time step change not supported by EK"): - self.system.time_step /= 2. + self.system.thermostat.set_langevin(kT=1., seed=42, gamma=1.) + with self.assertRaisesRegex(ValueError, "must be an integer multiple of the MD time_step"): + self.system.time_step /= 1.7 + self.system.time_step *= 1. if espressomd.has_features("ELECTROSTATICS"): self.system.electrostatics.solver = espressomd.electrostatics.DH( prefactor=1., kappa=1., r_cut=1.) # should not fail diff --git a/testsuite/python/field_test.py b/testsuite/python/field_test.py index 476cd356e5..dd07b44be6 100644 --- a/testsuite/python/field_test.py +++ b/testsuite/python/field_test.py @@ -59,14 +59,16 @@ def test_gravity(self): self.system.integrator.run(0) - np.testing.assert_almost_equal(g_const, np.copy(p.f) / p.mass) + np.testing.assert_allclose(np.copy(p.f / p.mass), g_const) self.assertAlmostEqual(self.system.analysis.energy()['total'], 0.) # Virtual sites don't feel gravity - if espressomd.has_features("VIRTUAL_SITES_INERTIALESS_TRACERS"): - p.propagation = espressomd.propagation.Propagation.TRANS_LB_TRACER + if espressomd.has_features("VIRTUAL_SITES_RELATIVE"): + p_vs = self.system.part.add(pos=[0, 1, 0]) + p_vs.vs_auto_relate_to(p) self.system.integrator.run(0) - np.testing.assert_allclose(np.copy(p.f), 0) + np.testing.assert_allclose(np.copy(p.f / p.mass), g_const) + np.testing.assert_allclose(np.copy(p_vs.f), [0., 0., 0.]) @utx.skipIfMissingFeatures("ELECTROSTATICS") def test_linear_electric_potential(self): diff --git a/testsuite/python/integrator_exceptions.py b/testsuite/python/integrator_exceptions.py index f02d6e06c1..6e11c04c36 100644 --- a/testsuite/python/integrator_exceptions.py +++ b/testsuite/python/integrator_exceptions.py @@ -21,6 +21,7 @@ import espressomd.lees_edwards import espressomd.shapes import espressomd.propagation +import numpy as np import unittest as ut import unittest_decorators as utx @@ -56,6 +57,8 @@ def test_00_common_interface(self): with self.assertRaisesRegex(ValueError, 'cannot reuse old forces and recalculate forces'): self.system.integrator.run(recalc_forces=True, reuse_forces=True) self.assertIsNone(self.system.integrator.integrator.call_method("unk")) + self.assertIsNone(self.system.thermostat.call_method("unk")) + self.assertIsNone(self.system.thermostat.kT) if espressomd.has_features("WCA"): wca = self.system.non_bonded_inter[0, 0].wca wca.set_params(epsilon=1., sigma=0.01) @@ -75,6 +78,27 @@ def test_00_common_interface(self): p.rotation = [False, False, True] self.system.integrator.run(0, recalc_forces=True) + def test_01_statefulness(self): + # setting a thermostat with invalid values should be a no-op + self.assertIsNone(self.system.thermostat.kT) + self.assertIsNone(self.system.thermostat.langevin.seed) + self.assertIsNone(self.system.thermostat.langevin.gamma) + with self.assertRaisesRegex(ValueError, "Parameter 'seed' must be a positive integer"): + self.system.thermostat.set_langevin(kT=1., gamma=1., seed=-1) + self.assertIsNone(self.system.thermostat.kT) + self.assertIsNone(self.system.thermostat.langevin.seed) + self.assertIsNone(self.system.thermostat.langevin.gamma) + with self.assertRaisesRegex(ValueError, "Parameter 'kT' cannot be negative"): + self.system.thermostat.set_langevin(kT=-1., gamma=1., seed=42) + self.assertIsNone(self.system.thermostat.kT) + self.assertIsNone(self.system.thermostat.langevin.seed) + self.assertIsNone(self.system.thermostat.langevin.gamma) + with self.assertRaisesRegex(ValueError, "Parameter 'gamma' cannot be negative"): + self.system.thermostat.set_langevin(kT=1., gamma=-1., seed=42) + self.assertIsNone(self.system.thermostat.kT) + self.assertIsNone(self.system.thermostat.langevin.seed) + self.assertIsNone(self.system.thermostat.langevin.gamma) + def test_vv_integrator(self): self.system.cell_system.skin = 0.4 self.system.thermostat.set_brownian(kT=1.0, gamma=1.0, seed=42) @@ -85,8 +109,50 @@ def test_vv_integrator(self): def test_brownian_integrator(self): self.system.cell_system.skin = 0.4 self.system.integrator.set_brownian_dynamics() + self.assertIsNone(self.system.thermostat.kT) with self.assertRaisesRegex(Exception, self.msg + 'The BD integrator requires the BD thermostat'): self.system.integrator.run(0) + with self.assertRaisesRegex(RuntimeError, "Parameter 'brownian' is read-only."): + self.system.thermostat.brownian = 1 + self.assertIsNone(self.system.thermostat.kT) + + def test_langevin_integrator(self): + self.system.cell_system.skin = 0.4 + self.system.integrator.set_vv() + self.system.thermostat.set_langevin(kT=2., gamma=3., seed=42) + + def check_original_params(thermo_off): + langevin = self.system.thermostat.langevin + np.testing.assert_allclose(np.copy(langevin.gamma), 3.) + np.testing.assert_allclose(np.copy(langevin.seed), 42) + if thermo_off: + self.assertIsNone(self.system.thermostat.kT) + else: + np.testing.assert_allclose(self.system.thermostat.kT, 2.) + + # updating a thermostat with invalid parameters should raise an + # exception and roll back to the last valid state of the thermostat + for thermo_off in [False, True]: + if thermo_off: + self.system.thermostat.turn_off() + with self.assertRaisesRegex(ValueError, "Parameter 'seed' must be a positive integer"): + self.system.thermostat.set_langevin(kT=1., gamma=1., seed=-1) + check_original_params(thermo_off) + with self.assertRaisesRegex(ValueError, "Parameter 'kT' cannot be negative"): + self.system.thermostat.set_langevin(kT=-1., gamma=1., seed=42) + check_original_params(thermo_off) + with self.assertRaisesRegex(ValueError, "Parameter 'gamma' cannot .* negative"): + self.system.thermostat.set_langevin(kT=1., gamma=-1., seed=42) + check_original_params(thermo_off) + + with self.assertRaisesRegex(RuntimeError, "Parameter 'langevin' is read-only."): + self.system.thermostat.langevin = 1 + with self.assertRaisesRegex(RuntimeError, "Parameter 'gamma' is read-only."): + self.system.thermostat.langevin.gamma = 1 + with self.assertRaisesRegex(RuntimeError, "Parameter 'seed' is read-only."): + self.system.thermostat.langevin.seed = 1 + with self.assertRaisesRegex(RuntimeError, "Parameter 'philox_counter' is read-only."): + self.system.thermostat.langevin.philox_counter = 1 @utx.skipIfMissingFeatures("NPT") def test_npt_integrator(self): @@ -134,6 +200,16 @@ def test_steepest_descent_integrator(self): with self.assertRaisesRegex(Exception, self.msg + 'The steepest descent integrator is incompatible with thermostats'): self.system.integrator.run(0) + def test_temperature_change(self): + # temperature change only allowed when no other thermostat is active + self.system.thermostat.set_langevin(kT=1., gamma=1., seed=42) + self.system.thermostat.set_langevin(kT=2., gamma=1., seed=42) + self.system.thermostat.set_brownian(kT=2., gamma=1., seed=42) + with self.assertRaisesRegex(RuntimeError, "Cannot set parameter 'kT' to 1.0*: there are currently active thermostats with kT=2.0*"): + self.system.thermostat.set_brownian(kT=1., gamma=1., seed=42) + with self.assertRaisesRegex(RuntimeError, f"Parameter 'kT' is read-only"): + self.system.thermostat.kT = 2. + if __name__ == "__main__": ut.main() diff --git a/testsuite/python/interactions_bonded_interface.py b/testsuite/python/interactions_bonded_interface.py index e8c2758861..57062a5bed 100644 --- a/testsuite/python/interactions_bonded_interface.py +++ b/testsuite/python/interactions_bonded_interface.py @@ -253,14 +253,6 @@ def test_exceptions(self): with self.assertRaisesRegex(ValueError, "Invalid value for parameter 'elasticLaw': 'Unknown'"): espressomd.interactions.IBM_Triel( ind1=0, ind2=1, ind3=2, k1=1.1, k2=1.2, maxDist=1.6, elasticLaw='Unknown') - with self.assertRaisesRegex(ValueError, "A parameter 'seed' has to be given on first activation of a thermalized bond"): - espressomd.interactions.ThermalizedBond( - temp_com=1., gamma_com=1., temp_distance=1., gamma_distance=1., - r_cut=2.) - with self.assertRaisesRegex(ValueError, "Parameter 'seed' must be >= 0"): - espressomd.interactions.ThermalizedBond( - temp_com=1., gamma_com=1., temp_distance=1., gamma_distance=1., - r_cut=2., seed=-1) # sanity checks when removing bonds self.system.bonded_inter.clear() diff --git a/testsuite/python/lb.py b/testsuite/python/lb.py index f0d0cd4d52..704c866928 100644 --- a/testsuite/python/lb.py +++ b/testsuite/python/lb.py @@ -386,15 +386,28 @@ def test_lb_node_set_get(self): with self.assertRaisesRegex(ValueError, "Parameter 'rng_state' must be >= 0"): lbf.rng_state = -5 + def test_temperature_mismatch(self): + self.system.thermostat.set_langevin(kT=2., seed=23, gamma=2.) + lbf = self.lb_class(kT=1., seed=42, **self.params, **self.lb_params) + self.system.lb = lbf + with self.assertRaisesRegex(RuntimeError, "Cannot set parameter 'kT' to 1.0*: there are currently active thermostats with kT=2.0*"): + self.system.thermostat.set_lb(LB_fluid=lbf, seed=23, gamma=2.) + self.assertFalse(self.system.thermostat.lb.is_active) + self.assertTrue(self.system.thermostat.langevin.is_active) + def test_parameter_change_without_seed(self): lbf = self.lb_class(kT=1.0, seed=42, **self.params, **self.lb_params) self.system.lb = lbf self.system.thermostat.set_lb(LB_fluid=lbf, seed=23, gamma=2.0) self.system.thermostat.set_lb(LB_fluid=lbf, gamma=3.0) - with self.assertRaisesRegex(Exception, "Temperature change not supported by LB"): + self.assertAlmostEqual(self.system.thermostat.lb.gamma, 3.) + with self.assertRaisesRegex(RuntimeError, "Temperature change not supported by LB"): self.system.thermostat.turn_off() - with self.assertRaisesRegex(Exception, "Time step change not supported by LB"): - self.system.time_step /= 2. + self.system.thermostat.set_langevin(kT=2., seed=23, gamma=2.) + self.assertFalse(self.system.thermostat.langevin.is_active) + with self.assertRaisesRegex(ValueError, "must be an integer multiple of the MD time_step"): + self.system.time_step /= 1.7 + self.system.time_step *= 1. if espressomd.has_features("ELECTROSTATICS"): self.system.electrostatics.solver = espressomd.electrostatics.DH( prefactor=1., kappa=1., r_cut=1.) # should not fail diff --git a/testsuite/python/lb_thermostat.py b/testsuite/python/lb_thermostat.py index 046b4801db..8a56d94080 100644 --- a/testsuite/python/lb_thermostat.py +++ b/testsuite/python/lb_thermostat.py @@ -63,13 +63,15 @@ class LBThermostatCommon(thermostats_common.ThermostatsCommon): # relaxation time 0.2 sim time units partcl_mass = 0.2 * partcl_gamma - np.random.seed(41) - def setUp(self): self.lbf = self.lb_class(**LB_PARAMS, **self.lb_params) self.system.lb = self.lbf + # make test results independent of execution order + np.random.seed(41) self.system.thermostat.set_lb( LB_fluid=self.lbf, seed=5, gamma=self.global_gamma) + self.system.thermostat.lb.call_method( + "override_philox_counter", counter=0) def tearDown(self): self.system.lb = None diff --git a/testsuite/python/long_range_actors.py b/testsuite/python/long_range_actors.py index 8611542187..ae8249fb00 100644 --- a/testsuite/python/long_range_actors.py +++ b/testsuite/python/long_range_actors.py @@ -105,9 +105,12 @@ def test_electrostatics_registration(self): self.system.electrostatics.extension = icc if espressomd.has_features(["NPT"]): with self.assertRaisesRegex(Exception, "ERROR: ICC does not work in the NPT ensemble"): + self.system.thermostat.set_npt( + kT=1., gamma0=2., gammav=0., seed=42) self.system.integrator.set_isotropic_npt( ext_pressure=2., piston=0.01) self.system.integrator.run(0) + self.system.thermostat.turn_off() self.system.integrator.set_vv() with self.assertRaisesRegex(RuntimeError, "Cannot change solver when an extension is active"): self.system.electrostatics.solver = p3m_new diff --git a/testsuite/python/propagation_langevin.py b/testsuite/python/propagation_langevin.py index 05785c738f..0834265a98 100644 --- a/testsuite/python/propagation_langevin.py +++ b/testsuite/python/propagation_langevin.py @@ -19,6 +19,7 @@ import unittest as ut import unittest_decorators as utx import espressomd +import espressomd.lb import espressomd.propagation import itertools import numpy as np @@ -27,9 +28,10 @@ class LangevinThermostat(ut.TestCase): """Test Langevin Dynamics""" - system = espressomd.System(box_l=[1.0, 1.0, 1.0]) + system = espressomd.System(box_l=[12., 12., 12.]) system.cell_system.set_regular_decomposition(use_verlet_lists=True) - system.cell_system.skin = 0 + system.cell_system.skin = 0. + system.min_global_cut = 2. system.periodicity = [False, False, False] def setUp(self): @@ -41,6 +43,8 @@ def setUp(self): def tearDown(self): self.system.part.clear() self.system.thermostat.turn_off() + if espressomd.has_features("WALBERLA"): + self.system.lb = None def check_rng(self, per_particle_gamma=False): """Test for RNG consistency.""" @@ -66,9 +70,22 @@ def reset_particle(): system = self.system system.time_step = 0.01 + self.assertIsNone(system.thermostat.kT) + self.assertFalse(system.thermostat.langevin.is_active) + system.thermostat.set_langevin(kT=kT, gamma=gamma, seed=41) + system.thermostat.langevin.call_method( + "override_philox_counter", counter=0) system.integrator.set_vv() + self.assertIsNotNone(system.thermostat.kT) + np.testing.assert_almost_equal(system.thermostat.kT, kT) + np.testing.assert_almost_equal( + np.copy(system.thermostat.langevin.gamma), gamma) + if espressomd.has_features("ROTATION"): + np.testing.assert_almost_equal( + np.copy(system.thermostat.langevin.gamma_rotation), gamma) + # run(0) does not increase the philox counter and should give the same # force p = reset_particle() @@ -86,6 +103,7 @@ def reset_particle(): # run(1) should give a different force p = reset_particle() system.integrator.run(1) + self.assertEqual(system.thermostat.langevin.philox_counter, 1) force2 = np.copy(p.f) self.assertTrue(np.all(np.not_equal(force1, force2))) if espressomd.has_features("ROTATION"): @@ -97,11 +115,15 @@ def reset_particle(): # force3: langevin.rng_counter() = 1, langevin.rng_seed() = 42 p = reset_particle() system.integrator.run(0, recalc_forces=True) + self.assertEqual(system.thermostat.langevin.seed, 41) + self.assertEqual(system.thermostat.langevin.philox_counter, 1) force2 = np.copy(p.f) if espressomd.has_features("ROTATION"): torque2 = np.copy(p.torque_lab) system.thermostat.set_langevin(kT=kT, gamma=gamma, seed=42) system.integrator.run(0, recalc_forces=True) + self.assertEqual(system.thermostat.langevin.seed, 42) + self.assertEqual(system.thermostat.langevin.philox_counter, 1) force3 = np.copy(p.f) self.assertTrue(np.all(np.not_equal(force2, force3))) if espressomd.has_features("ROTATION"): @@ -112,6 +134,8 @@ def reset_particle(): p = reset_particle() system.thermostat.set_langevin(kT=kT, gamma=gamma, seed=42) system.integrator.run(1) + self.assertEqual(system.thermostat.langevin.seed, 42) + self.assertEqual(system.thermostat.langevin.philox_counter, 2) force4 = np.copy(p.f) self.assertTrue(np.all(np.not_equal(force3, force4))) if espressomd.has_features("ROTATION"): @@ -124,6 +148,8 @@ def reset_particle(): reset_particle() system.thermostat.set_langevin(kT=kT, gamma=gamma, seed=41) system.integrator.run(1) + self.assertEqual(system.thermostat.langevin.seed, 41) + self.assertEqual(system.thermostat.langevin.philox_counter, 3) p = reset_particle() system.integrator.run(0, recalc_forces=True) force5 = np.copy(p.f) @@ -132,6 +158,15 @@ def reset_particle(): torque5 = np.copy(p.torque_lab) self.assertTrue(np.all(np.not_equal(torque4, torque5))) + with self.assertRaises(ValueError): + system.thermostat.set_langevin(kT=-1., gamma=2.) + with self.assertRaises(ValueError): + system.thermostat.set_langevin(kT=1., gamma=-2.) + + self.system.thermostat.turn_off() + self.assertFalse(system.thermostat.langevin.is_active) + self.assertIsNone(system.thermostat.kT) + def test_01__rng(self): """Test for RNG consistency.""" # No seed should throw exception @@ -219,12 +254,16 @@ def test_07__virtual(self): np.testing.assert_almost_equal(np.copy(virtual.f), [0, 0, 0]) np.testing.assert_almost_equal(np.copy(physical.f), dt * v0 / 2. - v0) - @utx.skipIfMissingFeatures(["VIRTUAL_SITES_RELATIVE"]) + @utx.skipIfMissingFeatures(["VIRTUAL_SITES_RELATIVE", "WALBERLA"]) def test_virtual_sites_relative(self): Propagation = espressomd.propagation.Propagation system = self.system + system.time_step = 0.01 o0 = np.array([0, 0, 0]) + system.lb = espressomd.lb.LBFluidWalberla( + tau=0.01, agrid=2., density=1., kinematic_viscosity=1., kT=0.) + system.thermostat.set_lb(LB_fluid=system.lb, seed=42, gamma=1.) system.thermostat.set_langevin( kT=0., gamma=1., gamma_rotation=1., seed=42) @@ -232,6 +271,8 @@ def test_virtual_sites_relative(self): virt_lb = system.part.add(pos=[2, 0, 0], v=[1, 2, 3], omega_lab=o0) virt_lg = system.part.add(pos=[2, 0, 0], v=[4, 5, 6], omega_lab=o0) virt_lx = system.part.add(pos=[2, 0, 0], v=[7, 8, 9], omega_lab=o0) + system.part.all().propagation = ( + Propagation.TRANS_LANGEVIN | Propagation.ROT_LANGEVIN) refs = { "real.f": [-3, -4, -5], "real.torque_lab": [0, 0, 0], diff --git a/testsuite/python/propagation_lb.py b/testsuite/python/propagation_lb.py index 09bd1710cd..91582099a2 100644 --- a/testsuite/python/propagation_lb.py +++ b/testsuite/python/propagation_lb.py @@ -41,6 +41,104 @@ def tearDown(self): self.system.thermostat.turn_off() self.system.part.clear() + def test_01__rng(self): + """Test for RNG consistency.""" + + system = self.system + system.time_step = 0.01 + kT = 1.1 + gamma = 3.5 + + def reset_fluid(): + lb_fluid = self.lb_class(agrid=1., tau=0.01, density=1., kT=kT, + kinematic_viscosity=1., **self.lb_params) + self.system.lb = lb_fluid + return lb_fluid + + def reset_particle(): + self.system.part.clear() + return system.part.add(pos=[0, 0, 0]) + + self.assertIsNone(system.thermostat.kT) + self.assertFalse(system.thermostat.lb.is_active) + + lb_fluid = reset_fluid() + system.thermostat.set_lb(LB_fluid=lb_fluid, gamma=gamma, seed=41) + system.thermostat.lb.call_method("override_philox_counter", counter=0) + system.integrator.set_vv() + + self.assertIsNotNone(system.thermostat.kT) + self.assertTrue(system.thermostat.lb.is_active) + np.testing.assert_almost_equal(system.thermostat.kT, kT) + np.testing.assert_almost_equal( + np.copy(system.thermostat.lb.gamma), gamma) + + # run(0) does not increase the philox counter and should give the same + # force + p = reset_particle() + system.integrator.run(0, recalc_forces=True) + force0 = np.copy(p.f) + p = reset_particle() + system.integrator.run(0, recalc_forces=True) + force1 = np.copy(p.f) + np.testing.assert_almost_equal(force0, force1) + np.testing.assert_almost_equal(force0, [0., 0., 0.]) + + # run(1) should give a different force + p = reset_particle() + system.integrator.run(1) + self.assertEqual(system.thermostat.lb.seed, 41) + self.assertEqual(system.thermostat.lb.philox_counter, 1) + force2 = np.copy(p.f) + self.assertTrue(np.all(np.not_equal(force1, force2))) + + # Different seed should give a different force with same counter state + # force2: lb.rng_counter() = 2, lb.rng_seed() = 41 + # force3: lb.rng_counter() = 2, lb.rng_seed() = 42 + reset_fluid() + p = reset_particle() + system.integrator.run(1) + self.assertEqual(system.thermostat.lb.seed, 41) + self.assertEqual(system.thermostat.lb.philox_counter, 2) + force2 = np.copy(p.f) + lb_fluid = reset_fluid() + system.thermostat.set_lb(LB_fluid=lb_fluid, gamma=gamma, seed=42) + system.thermostat.lb.call_method("override_philox_counter", counter=1) + system.integrator.run(1) + self.assertEqual(system.thermostat.lb.seed, 42) + self.assertEqual(system.thermostat.lb.philox_counter, 2) + force3 = np.copy(p.f) + self.assertTrue(np.all(np.not_equal(force2, force3))) + + # Same seed should not give the same force with different counter state + # force3: lb.rng_counter() = 2, lb.rng_seed() = 42 + # force4: lb.rng_counter() = 3, lb.rng_seed() = 42 + lb_fluid = reset_fluid() + p = reset_particle() + system.thermostat.set_lb(LB_fluid=lb_fluid, gamma=gamma, seed=42) + system.integrator.run(1) + self.assertEqual(system.thermostat.lb.seed, 42) + self.assertEqual(system.thermostat.lb.philox_counter, 3) + force4 = np.copy(p.f) + self.assertTrue(np.all(np.not_equal(force3, force4))) + + # Seed offset should not give the same force with a lag + # force4: lb.rng_counter() = 3, lb.rng_seed() = 42 + # force5: lb.rng_counter() = 4, lb.rng_seed() = 41 + lb_fluid = reset_fluid() + reset_particle() + system.thermostat.set_lb( + LB_fluid=lb_fluid, kT=kT, gamma=gamma, seed=41) + system.integrator.run(1) + self.assertEqual(system.thermostat.lb.seed, 41) + self.assertEqual(system.thermostat.lb.philox_counter, 4) + force5 = np.copy(p.f) + self.assertTrue(np.all(np.not_equal(force4, force5))) + + self.system.thermostat.turn_off() + self.assertFalse(system.thermostat.langevin.is_active) + self.assertIsNone(system.thermostat.kT) + @utx.skipIfMissingFeatures(["VIRTUAL_SITES_RELATIVE", "VIRTUAL_SITES_INERTIALESS_TRACERS"]) def test_virtual_sites_relative(self): @@ -118,12 +216,12 @@ def calc_trajectory(p, r0): ref_vel = v0 ref_pos = r0 if (p.propagation & (Propagation.SYSTEM_DEFAULT | - Propagation.ROT_EULER)): - ref_rot = o0 + p.ext_torque / p.rinertia * t - elif p.propagation & Propagation.ROT_LANGEVIN: + Propagation.ROT_LANGEVIN)): friction = np.exp(-gamma_rot / p.rinertia * t) o_term = p.ext_torque / gamma_rot ref_rot = o_term + (o0 - o_term) * friction + elif p.propagation & Propagation.ROT_EULER: + ref_rot = o0 + p.ext_torque / p.rinertia * t else: ref_rot = o0 return np.copy(ref_pos), np.copy(ref_vel), np.copy(ref_rot) diff --git a/testsuite/python/propagation_npt.py b/testsuite/python/propagation_npt.py index 032444ade8..0df370dd08 100644 --- a/testsuite/python/propagation_npt.py +++ b/testsuite/python/propagation_npt.py @@ -42,21 +42,14 @@ def tearDown(self): self.system.electrostatics.clear() if espressomd.has_features(["DIPOLES"]): self.system.magnetostatics.clear() - self.reset_rng_counter() + # reset RNG counter to make tests independent of execution order + self.system.thermostat.npt_iso.call_method( + "override_philox_counter", counter=0) self.system.thermostat.turn_off() self.system.integrator.set_vv() if espressomd.has_features(["LENNARD_JONES"]): self.system.non_bonded_inter[0, 0].lennard_jones.deactivate() - def reset_rng_counter(self): - # reset RNG counter to make tests independent of execution order - self.system.thermostat.set_npt(kT=0., gamma0=0., gammav=1e-6, seed=42) - thmst_list = self.system.thermostat.get_state() - for thmst in thmst_list: - if thmst["type"] == "NPT_ISO": - thmst["counter"] = 0 - self.system.thermostat.__setstate__(thmst_list) - def test_integrator_exceptions(self): # invalid parameters should throw exceptions with self.assertRaises(RuntimeError): @@ -248,6 +241,7 @@ def run_with_p3m(self, container, p3m, method): system.integrator.integrator, espressomd.integrate.VelocityVerlet) container.solver = None + system.thermostat.set_npt(kT=1.0, gamma0=2, gammav=0.04, seed=42) system.integrator.set_isotropic_npt(**npt_kwargs_rectangular) container.solver = p3m with self.assertRaisesRegex(Exception, err_msg): diff --git a/testsuite/python/propagation_stokesian.py b/testsuite/python/propagation_stokesian.py index 9a7ac25393..a1bdd81a35 100644 --- a/testsuite/python/propagation_stokesian.py +++ b/testsuite/python/propagation_stokesian.py @@ -58,7 +58,7 @@ def reset_particle(): viscosity = 2.4 # invalid parameters should throw exceptions - with self.assertRaisesRegex(ValueError, "kT has an invalid value"): + with self.assertRaisesRegex(ValueError, "Parameter 'kT' cannot be negative"): system.thermostat.set_stokesian(kT=-1) with self.assertRaises(ValueError): system.thermostat.set_stokesian(kT=1, seed=-1) @@ -72,34 +72,44 @@ def reset_particle(): # run(0) does not increase the philox counter and should give no force p = reset_particle() system.integrator.run(0) + self.assertEqual(system.thermostat.stokesian.seed, 41) + self.assertEqual(system.thermostat.stokesian.philox_counter, 0) force0 = np.copy(p.pos) / pos2force np.testing.assert_almost_equal(force0, 0) # run(1) should give a force p = reset_particle() system.integrator.run(1) + self.assertEqual(system.thermostat.stokesian.seed, 41) + self.assertEqual(system.thermostat.stokesian.philox_counter, 1) force1 = np.copy(p.pos) / pos2force self.assertTrue(np.all(np.not_equal(force1, [0, 0, 0]))) # Same seed should not give the same force with different counter state - # force1: brownian.rng_counter() = 0, brownian.rng_seed() = 41 - # force2: brownian.rng_counter() = 1, brownian.rng_seed() = 41 + # force1: brownian.rng_counter() = 1, brownian.rng_seed() = 41 + # force2: brownian.rng_counter() = 2, brownian.rng_seed() = 41 p = reset_particle() system.thermostat.set_stokesian(kT=kT, seed=41) system.integrator.run(1) + self.assertEqual(system.thermostat.stokesian.seed, 41) + self.assertEqual(system.thermostat.stokesian.philox_counter, 2) force2 = np.copy(p.pos) / pos2force self.assertTrue(np.all(np.not_equal(force2, force1))) # Seed offset should not give the same force with a lag - # force3: brownian.rng_counter() = 2, brownian.rng_seed() = 42 - # force4: brownian.rng_counter() = 3, brownian.rng_seed() = 41 + # force3: brownian.rng_counter() = 3, brownian.rng_seed() = 42 + # force4: brownian.rng_counter() = 4, brownian.rng_seed() = 41 p = reset_particle() system.thermostat.set_stokesian(kT=kT, seed=42) system.integrator.run(1) + self.assertEqual(system.thermostat.stokesian.seed, 42) + self.assertEqual(system.thermostat.stokesian.philox_counter, 3) force3 = np.copy(p.pos) / pos2force p = reset_particle() system.thermostat.set_stokesian(kT=kT, seed=41) system.integrator.run(1) + self.assertEqual(system.thermostat.stokesian.seed, 41) + self.assertEqual(system.thermostat.stokesian.philox_counter, 4) force4 = np.copy(p.pos) / pos2force self.assertTrue(np.all(np.not_equal(force3, force4))) diff --git a/testsuite/python/save_checkpoint.py b/testsuite/python/save_checkpoint.py index 13bb2740ae..04b752e443 100644 --- a/testsuite/python/save_checkpoint.py +++ b/testsuite/python/save_checkpoint.py @@ -262,24 +262,26 @@ harmonic_bond = espressomd.interactions.HarmonicBond(r_0=0.0, k=1.0) system.bonded_inter.add(harmonic_bond) p2.add_bond((harmonic_bond, p1)) -# create 3 thermalized bonds that will overwrite each other's seed -therm_params = dict(temp_com=0.1, temp_distance=0.2, gamma_com=0.3, - gamma_distance=0.5, r_cut=2.) -therm_bond1 = espressomd.interactions.ThermalizedBond(seed=1, **therm_params) -therm_bond2 = espressomd.interactions.ThermalizedBond(seed=2, **therm_params) -therm_bond3 = espressomd.interactions.ThermalizedBond(seed=3, **therm_params) -system.bonded_inter.add(therm_bond1) -p2.add_bond((therm_bond1, p1)) -checkpoint.register("therm_bond2") -checkpoint.register("therm_params") -# create Drude particles -if espressomd.has_features(['ELECTROSTATICS', 'MASS', 'ROTATION']): - dh = espressomd.drude_helpers.DrudeHelpers() - dh.add_drude_particle_to_core( - system=system, harmonic_bond=harmonic_bond, - thermalized_bond=therm_bond1, p_core=p2, type_drude=10, - alpha=1., mass_drude=0.6, coulomb_prefactor=0.8, thole_damping=2.) - checkpoint.register("dh") +if 'THERM.LB' in modes or 'THERM.LANGEVIN' in modes: + # create Drude particles + system.thermostat.set_thermalized_bond(seed=3) + system.thermostat.thermalized_bond.call_method( + "override_philox_counter", counter=5) + therm_params = dict(temp_com=0.1, temp_distance=0.2, gamma_com=0.3, + gamma_distance=0.5, r_cut=2.) + therm_bond1 = espressomd.interactions.ThermalizedBond(**therm_params) + therm_bond2 = espressomd.interactions.ThermalizedBond(**therm_params) + system.bonded_inter.add(therm_bond1) + p2.add_bond((therm_bond1, p1)) + checkpoint.register("therm_bond2") + checkpoint.register("therm_params") + if espressomd.has_features(['ELECTROSTATICS', 'MASS', 'ROTATION']): + dh = espressomd.drude_helpers.DrudeHelpers() + dh.add_drude_particle_to_core( + system=system, harmonic_bond=harmonic_bond, + thermalized_bond=therm_bond1, p_core=p2, type_drude=10, + alpha=1., mass_drude=0.6, coulomb_prefactor=0.8, thole_damping=2.) + checkpoint.register("dh") strong_harmonic_bond = espressomd.interactions.HarmonicBond(r_0=0.0, k=5e5) system.bonded_inter.add(strong_harmonic_bond) p4.add_bond((strong_harmonic_bond, p3)) @@ -360,9 +362,9 @@ if lbf_class: system.lb = lbf - system.ekcontainer = ekcontainer if 'THERM.LB' in modes: system.thermostat.set_lb(LB_fluid=lbf, seed=23, gamma=2.0) + system.ekcontainer = ekcontainer # Create a 3D grid with deterministic values to fill the LB fluid lattice m = np.pi / 12 grid_3D = np.fromfunction( @@ -435,7 +437,10 @@ if espressomd.has_features('THERMOSTAT_PER_PARTICLE'): gamma = 2. if espressomd.has_features('PARTICLE_ANISOTROPY'): - gamma = np.array([2., 3., 4.]) + if 'THERM.LB' in modes: + gamma = np.array([2., 2., 2.]) + else: + gamma = np.array([2., 3., 4.]) p4.gamma = gamma if espressomd.has_features('ROTATION'): p3.gamma_rot = 2. * gamma diff --git a/testsuite/python/test_checkpoint.py b/testsuite/python/test_checkpoint.py index 93054bbe6e..550a8c33a7 100644 --- a/testsuite/python/test_checkpoint.py +++ b/testsuite/python/test_checkpoint.py @@ -50,6 +50,9 @@ has_lb_mode = ('LB.WALBERLA' in modes and espressomd.has_features('WALBERLA') and ('LB.CPU' in modes or 'LB.GPU' in modes and is_gpu_available)) has_p3m_mode = 'P3M.CPU' in modes or 'P3M.GPU' in modes and is_gpu_available +has_thermalized_bonds = 'THERM.LB' in modes or 'THERM.LANGEVIN' in modes +has_drude = (espressomd.has_features(['ELECTROSTATICS' and 'MASS', 'ROTATION']) + and has_thermalized_bonds) class CheckpointTest(ut.TestCase): @@ -397,7 +400,7 @@ def test_particle_properties(self): np.testing.assert_allclose(np.copy(p4.rinertia), [1., 1., 1.]) if espressomd.has_features('ELECTROSTATICS'): np.testing.assert_allclose(p1.q, 1.) - if espressomd.has_features(['MASS', 'ROTATION']): + if has_drude: # check Drude particles p5 = system.part.by_id(5) np.testing.assert_allclose(p2.q, +0.118, atol=1e-3) @@ -428,7 +431,10 @@ def test_particle_properties(self): if espressomd.has_features('THERMOSTAT_PER_PARTICLE'): gamma = 2. if espressomd.has_features('PARTICLE_ANISOTROPY'): - gamma = np.array([2., 3., 4.]) + if 'THERM.LB' in modes: + gamma = np.array([2., 2., 2.]) + else: + gamma = np.array([2., 3., 4.]) np.testing.assert_allclose(p4.gamma, gamma) if espressomd.has_features('ROTATION'): np.testing.assert_allclose(p3.gamma_rot, 2. * gamma) @@ -497,63 +503,85 @@ def test_shape_based_constraints_serialization(self): @ut.skipIf(not has_lb_mode, "Skipping test due to missing LB feature.") @ut.skipIf('THERM.LB' not in modes, 'LB thermostat not in modes') def test_thermostat_LB(self): - thmst = system.thermostat.get_state()[0] - self.assertEqual(thmst['type'], 'LB') - # rng_counter_fluid = seed, seed is 0 because kT=0 - self.assertEqual(thmst['rng_counter_fluid'], 0) - self.assertEqual(thmst['gamma'], 2.0) + thmst = system.thermostat.lb + self.assertTrue(thmst.is_active) + self.assertEqual(thmst.seed, 23) + self.assertEqual(thmst.philox_counter, 0) + self.assertAlmostEqual(thmst.gamma, 2., delta=1e-10) + self.assertAlmostEqual(system.thermostat.kT, 0., delta=1e-10) @ut.skipIf('THERM.LANGEVIN' not in modes, 'Langevin thermostat not in modes') def test_thermostat_Langevin(self): - thmst = system.thermostat.get_state()[0] - self.assertEqual(thmst['type'], 'LANGEVIN') - self.assertEqual(thmst['kT'], 1.0) - self.assertEqual(thmst['seed'], 42) - self.assertEqual(thmst['counter'], 0) - np.testing.assert_array_equal(thmst['gamma'], 3 * [2.0]) + thmst = system.thermostat.langevin + self.assertTrue(thmst.is_active) + self.assertEqual(thmst.seed, 42) + self.assertEqual(thmst.philox_counter, 0) + self.assertAlmostEqual(system.thermostat.kT, 1., delta=1e-10) + np.testing.assert_allclose(np.copy(thmst.gamma), 2., atol=1e-10) if espressomd.has_features('ROTATION'): - np.testing.assert_array_equal(thmst['gamma_rotation'], 3 * [2.0]) + np.testing.assert_allclose( + np.copy(thmst.gamma_rotation), 2., atol=1e-10) @ut.skipIf('THERM.BD' not in modes, 'Brownian thermostat not in modes') def test_thermostat_Brownian(self): - thmst = system.thermostat.get_state()[0] - self.assertEqual(thmst['type'], 'BROWNIAN') - self.assertEqual(thmst['kT'], 1.0) - self.assertEqual(thmst['seed'], 42) - self.assertEqual(thmst['counter'], 0) - np.testing.assert_array_equal(thmst['gamma'], 3 * [2.0]) + thmst = system.thermostat.brownian + self.assertTrue(thmst.is_active) + self.assertEqual(thmst.seed, 42) + self.assertEqual(thmst.philox_counter, 0) + self.assertAlmostEqual(system.thermostat.kT, 1., delta=1e-10) + np.testing.assert_allclose(np.copy(thmst.gamma), 2., atol=1e-10) if espressomd.has_features('ROTATION'): - np.testing.assert_array_equal(thmst['gamma_rotation'], 3 * [2.0]) + np.testing.assert_allclose( + np.copy(thmst.gamma_rotation), 2., atol=1e-10) @utx.skipIfMissingFeatures('DPD') @ut.skipIf('THERM.DPD' not in modes, 'DPD thermostat not in modes') def test_thermostat_DPD(self): - thmst = system.thermostat.get_state()[0] - self.assertEqual(thmst['type'], 'DPD') - self.assertEqual(thmst['kT'], 1.0) - self.assertEqual(thmst['seed'], 42) - self.assertEqual(thmst['counter'], 0) + thmst = system.thermostat.dpd + self.assertTrue(thmst.is_active) + self.assertEqual(thmst.seed, 42) + self.assertEqual(thmst.philox_counter, 0) + self.assertAlmostEqual(system.thermostat.kT, 1., delta=1e-10) @utx.skipIfMissingFeatures('NPT') @ut.skipIf('THERM.NPT' not in modes, 'NPT thermostat not in modes') def test_thermostat_NPT(self): - thmst = system.thermostat.get_state()[0] - self.assertEqual(thmst['type'], 'NPT_ISO') - self.assertEqual(thmst['seed'], 42) - self.assertEqual(thmst['counter'], 0) - self.assertEqual(thmst['gamma0'], 2.0) - self.assertEqual(thmst['gammav'], 0.1) + thmst = system.thermostat.npt_iso + self.assertTrue(thmst.is_active) + self.assertEqual(thmst.seed, 42) + self.assertEqual(thmst.philox_counter, 0) + self.assertAlmostEqual(thmst.gamma0, 2.0, delta=1e-10) + self.assertAlmostEqual(thmst.gammav, 0.1, delta=1e-10) + self.assertAlmostEqual(system.thermostat.kT, 1., delta=1e-10) @utx.skipIfMissingFeatures('STOKESIAN_DYNAMICS') @ut.skipIf('THERM.SDM' not in modes, 'SDM thermostat not in modes') def test_thermostat_SDM(self): - thmst = system.thermostat.get_state()[0] - self.assertEqual(thmst['type'], 'SD') - self.assertEqual(thmst['kT'], 1.0) - self.assertEqual(thmst['seed'], 42) - self.assertEqual(thmst['counter'], 0) + thmst = system.thermostat.stokesian + self.assertTrue(thmst.is_active) + self.assertEqual(thmst.seed, 42) + self.assertEqual(thmst.philox_counter, 0) + self.assertAlmostEqual(system.thermostat.kT, 1., delta=1e-10) + + @ut.skipIf(not has_thermalized_bonds, + 'thermalized bond thermostat not in modes') + def test_thermostat_thermalized_bond(self): + thmst = system.thermostat.thermalized_bond + self.assertEqual(thmst.seed, 3) + self.assertEqual(thmst.philox_counter, 5) + therm_bonds = [therm_bond2] + for bond in system.bonded_inter: + if isinstance(bond, espressomd.interactions.ThermalizedBond): + therm_bonds.append(bond) + self.assertEqual(len(therm_bonds), 2) + for bond in therm_bonds: + self.assertAlmostEqual(bond.temp_com, 0.1, delta=1e-10) + self.assertAlmostEqual(bond.gamma_com, 0.3, delta=1e-10) + self.assertAlmostEqual(bond.temp_distance, 0.2, delta=1e-10) + self.assertAlmostEqual(bond.gamma_distance, 0.5, delta=1e-10) + self.assertAlmostEqual(bond.r_cut, 2., delta=1e-10) def test_integrator(self): params = system.integrator.get_params() @@ -655,10 +683,11 @@ def test_bonded_inter(self): self.assertEqual(partcl_1.bonds[0][0].params, reference) self.assertEqual(system.bonded_inter[0].params, reference) # all thermalized bonds should be identical - reference = {**therm_params, 'seed': 3} - self.assertEqual(partcl_1.bonds[1][0].params, reference) - self.assertEqual(system.bonded_inter[1].params, reference) - self.assertEqual(therm_bond2.params, reference) + if has_drude: + reference = therm_params + self.assertEqual(partcl_1.bonds[1][0].params, reference) + self.assertEqual(system.bonded_inter[1].params, reference) + self.assertEqual(therm_bond2.params, reference) # immersed boundary bonds self.assertEqual( ibm_volcons_bond.params, {'softID': 15, 'kappaV': 0.01}) @@ -686,7 +715,7 @@ def test_bond_breakage_specs(self): delta=1e-10) self.assertEqual(break_spec.action_type, cpt_spec.action_type) - @utx.skipIfMissingFeatures(['ELECTROSTATICS', 'MASS', 'ROTATION']) + @ut.skipIf(not has_drude, 'no Drude particles') def test_drude_helpers(self): drude_type = 10 core_type = 0 @@ -973,6 +1002,7 @@ def test_constraints(self): @utx.skipIfMissingFeatures("WCA") @ut.skipIf(has_lb_mode, "LB not supported") @ut.skipIf("INT.SDM" in modes, "Stokesian integrator not supported") + @ut.skipIf("INT.NPT" in modes, "NPT integrator not supported") @ut.skipIf("INT.BD" in modes, "Brownian integrator not supported") @ut.skipIf("INT.SD" in modes, "Steepest descent not supported") def test_union(self): diff --git a/testsuite/python/thermalized_bond.py b/testsuite/python/thermalized_bond.py index a89717b806..527bef5095 100644 --- a/testsuite/python/thermalized_bond.py +++ b/testsuite/python/thermalized_bond.py @@ -28,25 +28,30 @@ @utx.skipIfMissingFeatures(["MASS"]) class ThermalizedBond(ut.TestCase, thermostats_common.ThermostatsCommon): - """Tests the two velocity distributions for COM and distance created by the - thermalized bond independently against the single component Maxwell - distribution. Adapted from langevin_thermostat testcase.""" + """ + Test the two velocity distributions for COM and distance created by the + thermalized bond independently against the single component Maxwell + distribution. + """ - box_l = 10.0 - system = espressomd.System(box_l=[box_l] * 3) + system = espressomd.System(box_l=[10., 10., 10.]) system.cell_system.set_n_square() system.cell_system.skin = 0.3 - @classmethod - def setUpClass(cls): + def setUp(self): np.random.seed(42) + self.system.time_step = 0.01 + self.system.periodicity = [True, True, True] + + def tearDown(self): + self.system.part.clear() + self.system.thermostat.turn_off() def test_com_langevin(self): """Test for COM thermalization.""" N = 200 N_half = int(N / 2) - self.system.part.clear() self.system.time_step = 0.02 self.system.periodicity = [False, False, False] @@ -66,9 +71,10 @@ def test_com_langevin(self): t_com = 2.0 g_com = 4.0 + self.system.thermostat.set_thermalized_bond(seed=55) thermalized_dist_bond = espressomd.interactions.ThermalizedBond( temp_com=t_com, gamma_com=g_com, temp_distance=t_dist, - gamma_distance=g_dist, r_cut=2.0, seed=55) + gamma_distance=g_dist, r_cut=2.0) self.system.bonded_inter.add(thermalized_dist_bond) for p1, p2 in zip(partcls_m1, partcls_m2): @@ -98,7 +104,6 @@ def test_dist_langevin(self): N = 100 N_half = int(N / 2) - self.system.part.clear() self.system.time_step = 0.02 self.system.periodicity = [True, True, True] @@ -117,9 +122,10 @@ def test_dist_langevin(self): t_com = 0.0 g_com = 0.0 + self.system.thermostat.set_thermalized_bond(seed=51) thermalized_dist_bond = espressomd.interactions.ThermalizedBond( temp_com=t_com, gamma_com=g_com, temp_distance=t_dist, - gamma_distance=g_dist, r_cut=9, seed=51) + gamma_distance=g_dist, r_cut=9) self.system.bonded_inter.add(thermalized_dist_bond) for p1, p2 in zip(partcls_m1, partcls_m2): @@ -142,6 +148,13 @@ def test_dist_langevin(self): self.check_velocity_distribution( v_stored, v_minmax, bins, error_tol, t_dist) + def test_exceptions(self): + espressomd.interactions.ThermalizedBond( + temp_com=1., gamma_com=1., temp_distance=1., gamma_distance=1., + r_cut=3.) + with self.assertRaisesRegex(Exception, "Thermalized bonds require the thermalized_bond thermostat"): + self.system.integrator.run(0) + if __name__ == "__main__": ut.main() diff --git a/testsuite/python/virtual_sites_relative.py b/testsuite/python/virtual_sites_relative.py index dc01d655dc..be74b08c38 100644 --- a/testsuite/python/virtual_sites_relative.py +++ b/testsuite/python/virtual_sites_relative.py @@ -19,6 +19,7 @@ import unittest as ut import unittest_decorators as utx import espressomd +import espressomd.lb import espressomd.propagation import numpy as np import tests_common @@ -31,7 +32,7 @@ class VirtualSites(ut.TestCase): np.random.seed(42) def setUp(self): - self.system.box_l = [10.0, 10.0, 10.0] + self.system.box_l = [12.0, 12.0, 12.0] self.system.cell_system.set_regular_decomposition( use_verlet_lists=True) @@ -40,6 +41,8 @@ def tearDown(self): self.system.thermostat.turn_off() self.system.integrator.set_vv() self.system.non_bonded_inter[0, 0].lennard_jones.deactivate() + if espressomd.has_features("WALBERLA"): + self.system.lb = None def multiply_quaternions(self, a, b): return np.array( @@ -82,9 +85,13 @@ def verify_vs(self, vs, verify_velocity=True): v_d - vs_r[1] * self.director_from_quaternion( self.multiply_quaternions(rel.quat, vs_r[2]))), 1E-6) + @utx.skipIfMissingFeatures(["WALBERLA"]) def test_vs_quat(self): self.system.time_step = 0.01 self.system.min_global_cut = 0.23 + self.system.lb = lb_fluid = espressomd.lb.LBFluidWalberla( + tau=0.01, agrid=2., density=1., kinematic_viscosity=1., kT=0.) + self.system.thermostat.set_lb(LB_fluid=lb_fluid, seed=42, gamma=1.) Propagation = espressomd.propagation.Propagation p1 = self.system.part.add(pos=[1, 1, 1], rotation=3 * [True], omega_lab=[1, 1, 1]) @@ -256,16 +263,10 @@ def run_test_lj(self): rotation=3 * [True], id=3 * i, pos=np.random.random(3) * l, type=1, omega_lab=0.3 * np.random.random(3), v=np.random.random(3)) # lj spheres - p3ip1 = system.part.add(rotation=3 * [True], - id=3 * i + 1, - pos=p3i.pos + - p3i.director / 2., - type=0) - p3ip2 = system.part.add(rotation=3 * [True], - id=3 * i + 2, - pos=p3i.pos - - p3i.director / 2., - type=0) + p3ip1 = system.part.add(rotation=3 * [True], id=3 * i + 1, + pos=p3i.pos + p3i.director / 2., type=0) + p3ip2 = system.part.add(rotation=3 * [True], id=3 * i + 2, + pos=p3i.pos - p3i.director / 2., type=0) p3ip1.vs_auto_relate_to(p3i.id) self.verify_vs(p3ip1, verify_velocity=False) p3ip2.vs_auto_relate_to(p3i.id) diff --git a/testsuite/python/virtual_sites_tracers_common.py b/testsuite/python/virtual_sites_tracers_common.py index 06aca47ae8..df44fa77fe 100644 --- a/testsuite/python/virtual_sites_tracers_common.py +++ b/testsuite/python/virtual_sites_tracers_common.py @@ -153,14 +153,20 @@ def test_advection(self): def test_zz_exceptions_without_lb(self): """ - Check behaviour without LB. Ignore real particles, complain on tracers. + Check behaviour without LB. """ self.set_lb() system = self.system + lbf = system.lb system.lb = None system.part.clear() p = system.part.add(pos=(0, 0, 0)) - system.integrator.run(1) + with self.assertRaisesRegex(Exception, "The LB thermostat requires a LB fluid"): + system.integrator.run(1) p.propagation = espressomd.propagation.Propagation.TRANS_LB_TRACER with self.assertRaisesRegex(Exception, "LB needs to be active for inertialess tracers"): system.integrator.run(1) + system.lb = lbf + self.system.thermostat.turn_off() + with self.assertRaisesRegex(Exception, "The LB integrator requires the LB thermostat"): + system.integrator.run(1) diff --git a/testsuite/scripts/tutorials/test_polymers.py b/testsuite/scripts/tutorials/test_polymers.py index 2da7532db8..f938bf0e0c 100644 --- a/testsuite/scripts/tutorials/test_polymers.py +++ b/testsuite/scripts/tutorials/test_polymers.py @@ -50,7 +50,7 @@ def test_diffusion_coefficients(self): # polymer diffusion ref_D = [0.0363, 0.0269, 0.0234] np.testing.assert_allclose(tutorial.diffusion_msd, ref_D, rtol=0.30) - np.testing.assert_allclose(tutorial.diffusion_gk, ref_D, rtol=0.15) + np.testing.assert_allclose(tutorial.diffusion_gk, ref_D, rtol=0.30) # monomer diffusion if tutorial.POLYMER_MODEL == 'Rouse': ref_D0 = tutorial.KT / tutorial.GAMMA