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