ESPResSo
Extensible Simulation Package for Research on Soft Matter Systems
Loading...
Searching...
No Matches
particle_coupling.cpp
Go to the documentation of this file.
1/*
2 * Copyright (C) 2010-2026 The ESPResSo project
3 *
4 * This file is part of ESPResSo.
5 *
6 * ESPResSo is free software: you can redistribute it and/or modify
7 * it under the terms of the GNU General Public License as published by
8 * the Free Software Foundation, either version 3 of the License, or
9 * (at your option) any later version.
10 *
11 * ESPResSo is distributed in the hope that it will be useful,
12 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 * GNU General Public License for more details.
15 *
16 * You should have received a copy of the GNU General Public License
17 * along with this program. If not, see <http://www.gnu.org/licenses/>.
18 */
19
20#include <config/config.hpp>
21
22#include "BoxGeometry.hpp"
23#include "LocalBox.hpp"
24#include "Particle.hpp"
26#include "communication.hpp"
27#include "config/config.hpp"
28#include "errorhandling.hpp"
30#include "random.hpp"
31#include "system/System.hpp"
32#include "thermostat.hpp"
33
34#include <utils/Counter.hpp>
35#include <utils/Vector.hpp>
36
37#include <boost/mpi.hpp>
38
39#ifdef ESPRESSO_CALIPER
40#include <caliper/cali.h>
41#endif
42
43#include <cassert>
44#include <cmath>
45#include <cstdint>
46#include <initializer_list>
47#include <limits>
48#include <ranges>
49#include <vector>
50
52 double lb_gamma) {
53#ifdef ESPRESSO_PARTICLE_ANISOTROPY
54 auto const default_gamma = Thermostat::GammaType::broadcast(lb_gamma);
55#else
56 auto const default_gamma = lb_gamma;
57#endif // ESPRESSO_PARTICLE_ANISOTROPY
58#ifdef ESPRESSO_THERMOSTAT_PER_PARTICLE
59 return Thermostat::handle_particle_gamma(p.gamma(), default_gamma);
60#else
61 return default_gamma;
62#endif // ESPRESSO_THERMOSTAT_PER_PARTICLE
63}
64
65static Utils::Vector3d lb_drag_force(Particle const &p, double lb_gamma,
66 Utils::Vector3d const &v_fluid) {
67#ifdef ESPRESSO_LB_ELECTROHYDRODYNAMICS
68 auto const v_drift = v_fluid + p.mu_E();
69#else
70 auto const &v_drift = v_fluid;
71#endif
72 auto const gamma = lb_handle_particle_anisotropy(p, lb_gamma);
73
74 /* calculate viscous force (eq. (9) @cite ahlrichs99a) */
75 return Utils::hadamard_product(gamma, v_drift - p.v());
76}
77
78Utils::Vector3d lb_drag_force(LB::Solver const &lb, double lb_gamma,
79 Particle const &p,
80 Utils::Vector3d const &shifted_pos) {
81 auto const v_fluid = lb.get_coupling_interpolated_velocity(shifted_pos);
82 return lb_drag_force(p, lb_gamma, v_fluid);
83}
84
85/**
86 * @brief Check if a position is within the local box + halo.
87 *
88 * @param local_box Local geometry
89 * @param pos Position to check
90 * @param halo Halo
91 *
92 * @return True iff the point is inside of the box up to halo.
93 */
94static bool in_local_domain(LocalBox const &local_box,
95 Utils::Vector3d const &pos, double halo = 0.) {
96 auto const halo_vec = Utils::Vector3d::broadcast(halo);
97 auto const lower_corner = local_box.my_left() - halo_vec;
98 auto const upper_corner = local_box.my_right() + halo_vec;
99
100 return pos >= lower_corner and pos < upper_corner;
101}
102
103static bool in_box(Utils::Vector3d const &pos,
104 Utils::Vector3d const &lower_corner,
105 Utils::Vector3d const &upper_corner) {
106 return pos >= lower_corner and pos < upper_corner;
107}
108
109bool in_local_halo(LocalBox const &local_box, Utils::Vector3d const &pos,
110 double agrid) {
111 auto const halo = 0.5 * agrid;
112 return in_local_domain(local_box, pos, halo);
113}
114
115static void positions_in_halo_impl(Utils::Vector3d const &pos_folded,
116 Utils::Vector3d const &halo_lower_corner,
117 Utils::Vector3d const &halo_upper_corner,
118 BoxGeometry const &box_geo,
119 std::vector<Utils::Vector3d> &res) {
120
121 // Lees-Edwards: pre-calc positional offset folded into the simulation box
122 double folded_le_offset = 0.;
123 auto const &le = box_geo.lees_edwards_bc();
124 if (box_geo.type() == BoxType::LEES_EDWARDS) {
125 folded_le_offset = Algorithm::periodic_fold(
126 le.pos_offset, box_geo.length()[le.shear_direction]);
127 }
128
129 for (int i : {-1, 0, 1}) {
130 for (int j : {-1, 0, 1}) {
131 for (int k : {-1, 0, 1}) {
132 Utils::Vector3d shift{{double(i), double(j), double(k)}};
133
134 auto pos_shifted = pos_folded;
135 // Lees Edwards: folded position incl. LE pos offset
136 // This is needed to ensure that the position from which `pos_shifted`
137 // is calculated below, is always in the primary simulation box.
138 if (box_geo.type() == BoxType::LEES_EDWARDS) {
139 pos_shifted[le.shear_direction] = Algorithm::periodic_fold(
140 pos_folded[le.shear_direction] +
141 shift[le.shear_plane_normal] * folded_le_offset,
142 box_geo.length()[le.shear_direction]);
143 }
144 pos_shifted += Utils::hadamard_product(box_geo.length(), shift);
145
146 if (in_box(pos_shifted, halo_lower_corner, halo_upper_corner)) {
147 res.emplace_back(pos_shifted);
148 }
149 }
150 }
151 }
152}
153
154/**
155 * @brief Return a vector of positions shifted by +,- box length in each
156 * coordinate
157 */
158std::vector<Utils::Vector3d> positions_in_halo(Utils::Vector3d const &pos,
159 BoxGeometry const &box_geo,
160 LocalBox const &local_box,
161 double agrid) {
162 auto const halo = 0.5 * agrid;
163 auto const halo_vec = Utils::Vector3d::broadcast(halo);
164 auto const fully_inside_lower = local_box.my_left() + 2. * halo_vec;
165 auto const fully_inside_upper = local_box.my_right() - 2. * halo_vec;
166 auto const pos_folded = box_geo.folded_position(pos);
167 if (in_box(pos_folded, fully_inside_lower, fully_inside_upper)) {
168 return {pos_folded};
169 }
170 auto const halo_lower_corner = local_box.my_left() - halo_vec;
171 auto const halo_upper_corner = local_box.my_right() + halo_vec;
172 std::vector<Utils::Vector3d> res;
173 positions_in_halo_impl(pos_folded, halo_lower_corner, halo_upper_corner,
174 box_geo, res);
175 return res;
176}
177
178static auto lees_edwards_vel_shift(Utils::Vector3d const &pos_shifted_by_box_l,
179 Utils::Vector3d const &orig_pos,
180 BoxGeometry const &box_geo) {
181 Utils::Vector3d vel_shift{{0., 0., 0.}};
182 if (box_geo.type() == BoxType::LEES_EDWARDS) {
183 auto le = box_geo.lees_edwards_bc();
184 auto normal_shift =
185 (pos_shifted_by_box_l - orig_pos)[le.shear_plane_normal];
186 // normal_shift is +,- box_l or 0 up to floating point errors
187 if (normal_shift > std::numeric_limits<double>::epsilon()) {
188 vel_shift[le.shear_direction] -= le.shear_velocity;
189 }
190 if (normal_shift < -std::numeric_limits<double>::epsilon()) {
191 vel_shift[le.shear_direction] += le.shear_velocity;
192 }
193 }
194 return vel_shift;
195}
196
197namespace LB {
198
200 if (not m_thermalized) {
201 return Utils::Vector3d{};
202 }
203 using std::sqrt;
204 using Utils::sqrt;
205 auto const gamma = lb_handle_particle_anisotropy(p, m_thermostat.gamma);
206 auto const noise = Random::noise_uniform<RNGSalt::PARTICLES>(
207 m_thermostat.rng_counter(), m_thermostat.rng_seed(), p.id());
208 return m_noise_pref_wo_gamma * Utils::hadamard_product(sqrt(gamma), noise);
209}
210
211void ParticleCoupling::kernel(std::vector<Particle *> const &particles) {
212 if (particles.empty()) {
213 return;
214 }
215 enum coupling_modes { none, particle_force, swimmer_force_on_fluid };
216 auto const halo = 0.5 * m_lb.get_agrid();
217 auto const halo_vec = Utils::Vector3d::broadcast(halo);
218 auto const fully_inside_lower = m_local_box.my_left() + 2. * halo_vec;
219 auto const fully_inside_upper = m_local_box.my_right() - 2. * halo_vec;
220 auto const halo_lower_corner = m_local_box.my_left() - halo_vec;
221 auto const halo_upper_corner = m_local_box.my_right() + halo_vec;
222 std::vector<Utils::Vector3d> positions_velocity_coupling;
223 std::vector<Utils::Vector3d> positions_force_coupling;
224 std::vector<Utils::Vector3d> force_coupling_forces;
225 std::vector<uint8_t> positions_force_coupling_counter;
226 std::vector<Particle *> coupled_particles;
227 for (auto ptr : particles) {
228 auto &p = *ptr;
229 auto span_size = uint8_t{1u};
230 auto const folded_pos = m_box_geo.folded_position(p.pos());
231 if (in_box(folded_pos, fully_inside_lower, fully_inside_upper)) {
232 positions_force_coupling.emplace_back(folded_pos);
233 } else {
234 auto const old_size = positions_force_coupling.size();
235 positions_in_halo_impl(folded_pos, halo_lower_corner, halo_upper_corner,
236 m_box_geo, positions_force_coupling);
237 auto const new_size = positions_force_coupling.size();
238 span_size = static_cast<uint8_t>(new_size - old_size);
239 }
240 auto coupling_mode = none;
241#ifdef ESPRESSO_ENGINE
242 if (p.swimming().is_engine_force_on_fluid) {
243 coupling_mode = swimmer_force_on_fluid;
244 }
245#endif
246 if (coupling_mode == none) {
247 for (auto end = positions_force_coupling.end();
248 auto const &pos : std::views::counted(end - span_size, span_size)) {
249 if (pos >= halo_lower_corner and pos < halo_upper_corner) {
250 positions_velocity_coupling.emplace_back(pos);
251 coupling_mode = particle_force;
252 break;
253 }
254 }
255 }
256 if (coupling_mode == none) {
257 positions_force_coupling.erase(positions_force_coupling.end() - span_size,
258 positions_force_coupling.end());
259 } else {
260 coupled_particles.emplace_back(ptr);
261 positions_force_coupling_counter.emplace_back(span_size);
262 }
263 }
264
265 if (coupled_particles.empty()) {
266 return;
267 }
268 auto interpolated_velocities =
269 m_lb.get_coupling_interpolated_velocities(positions_velocity_coupling);
270
271 auto const &domain_lower_corner = m_local_box.my_left();
272 auto const &domain_upper_corner = m_local_box.my_right();
273 auto it_interpolated_velocities = interpolated_velocities.begin();
274 auto it_positions_force_coupling = positions_force_coupling.begin();
275 auto it_positions_velocity_coupling = positions_velocity_coupling.begin();
276 auto it_positions_force_coupling_counter =
277 positions_force_coupling_counter.begin();
278 for (auto ptr : coupled_particles) {
279 auto &p = *ptr;
280 auto coupling_mode = particle_force;
281#ifdef ESPRESSO_ENGINE
282 if (p.swimming().is_engine_force_on_fluid) {
283 coupling_mode = swimmer_force_on_fluid;
284 }
285#endif
286 Utils::Vector3d force_on_particle = {};
287 if (coupling_mode == particle_force) {
288#ifndef ESPRESSO_THERMOSTAT_PER_PARTICLE
289 if (m_thermostat.gamma > 0.)
290#endif
291 {
292 auto &v_fluid = *it_interpolated_velocities;
293 if (m_box_geo.type() == BoxType::LEES_EDWARDS) {
294 // Account for the case where the interpolated velocity has been read
295 // from a ghost of the particle across the LE boundary (or vice versa)
296 // Then the particle velocity is shifted by +,- the LE shear velocity
297 auto const vel_correction = lees_edwards_vel_shift(
298 *it_positions_velocity_coupling, p.pos(), m_box_geo);
299 v_fluid += vel_correction;
300 }
301 auto const drag_force = lb_drag_force(p, m_thermostat.gamma, v_fluid);
302 auto const random_force = get_noise_term(p);
303 force_on_particle = drag_force + random_force;
304 }
305 ++it_interpolated_velocities;
306 ++it_positions_velocity_coupling;
307 }
308
309 auto force_on_fluid = -force_on_particle;
310#ifdef ESPRESSO_ENGINE
311 if (coupling_mode == swimmer_force_on_fluid) {
312 force_on_fluid = p.calc_director() * p.swimming().f_swim;
313 }
314#endif
315
316 auto const span_size = *it_positions_force_coupling_counter;
317 ++it_positions_force_coupling_counter;
318 for (uint8_t i{0u}; i < span_size; ++i) {
319 auto &pos = *it_positions_force_coupling;
320 if (pos >= domain_lower_corner and pos < domain_upper_corner) {
321 /* Particle is in our LB volume, so this node
322 * is responsible to adding its force */
323 p.force() += force_on_particle;
324 }
325 force_coupling_forces.emplace_back(force_on_fluid);
326 ++it_positions_force_coupling;
327 }
328 }
329 m_lb.add_forces_at_pos(positions_force_coupling, force_coupling_forces);
330}
331
332#if defined(ESPRESSO_THERMOSTAT_PER_PARTICLE) and \
333 defined(ESPRESSO_PARTICLE_ANISOTROPY)
334static void lb_coupling_sanity_checks(Particle const &p) {
335 /*
336 lb does (at the moment) not support rotational particle coupling.
337 Consequently, anisotropic particles are also not supported.
338 */
339 auto const &p_gamma = p.gamma();
340 if (p_gamma[0] != p_gamma[1] or p_gamma[1] != p_gamma[2]) {
341 runtimeErrorMsg() << "anisotropic particle (id " << p.id()
342 << ") coupled to LB.";
343 }
344}
345#endif
346
347} // namespace LB
348
350#ifdef ESPRESSO_CALIPER
351 CALI_CXX_MARK_FUNCTION;
352#endif
353 assert(thermostat->lb != nullptr);
354 if (thermostat->lb->couple_to_md) {
355 if (not lb.is_solver_set()) {
356 runtimeErrorMsg() << "The LB thermostat requires a LB fluid";
357 return;
358 }
359 auto const real_particles = cell_structure->local_particles();
360 auto const ghost_particles = cell_structure->ghost_particles();
361 LB::ParticleCoupling coupling{*thermostat->lb, lb, *box_geo, *local_geo};
362 LB::CouplingBookkeeping bookkeeping{*cell_structure};
363 lb.ghost_communication_vel();
364 std::vector<Particle *> particles{};
365 for (auto const *particle_range : {&real_particles, &ghost_particles}) {
366 for (auto &p : *particle_range) {
367 if (not LB::is_tracer(p) and bookkeeping.should_be_coupled(p)) {
368#if defined(ESPRESSO_THERMOSTAT_PER_PARTICLE) and \
369 defined(ESPRESSO_PARTICLE_ANISOTROPY)
371#endif
372 particles.emplace_back(&p);
373 }
374 }
375 }
376 coupling.kernel(particles);
377 }
378}
@ LEES_EDWARDS
Vector implementation and trait types for boost qvm interoperability.
auto folded_position(Utils::Vector3d const &pos) const
Calculate coordinates folded to primary simulation box.
Utils::Vector3d const & length() const
Box length.
LeesEdwardsBC const & lees_edwards_bc() const
BoxType type() const
Keep track of ghost particles that have already been coupled once.
void kernel(std::vector< Particle * > const &particles)
Utils::Vector3d get_noise_term(Particle const &p) const
auto const & my_right() const
Right (top, back) corner of this nodes local box.
Definition LocalBox.hpp:47
auto const & my_left() const
Left (bottom, front) corner of this nodes local box.
Definition LocalBox.hpp:45
void lb_couple_particles()
Calculate particle-lattice interactions.
static DEVICE_QUALIFIER constexpr Vector< T, N > broadcast(typename Base::value_type const &value) noexcept
Create a vector that has all entries set to the same value.
Definition Vector.hpp:131
This file contains the errorhandling code for severe errors, like a broken bond or illegal parameter ...
#define runtimeErrorMsg()
auto periodic_fold(std::floating_point auto x, std::integral auto i, std::floating_point auto l)
Fold value into primary interval.
static void lb_coupling_sanity_checks(Particle const &p)
bool is_tracer(Particle const &p)
auto const & handle_particle_gamma(GammaType const &particle_gamma, GammaType const &default_gamma)
auto hadamard_product(Vector< T, N > const &a, Vector< U, N > const &b)
Definition Vector.hpp:377
auto sqrt(Vector< T, N > const &a)
Definition Vector.hpp:357
bool in_local_halo(LocalBox const &local_box, Utils::Vector3d const &pos, double agrid)
Check if a position is within the local LB domain plus halo.
static auto lees_edwards_vel_shift(Utils::Vector3d const &pos_shifted_by_box_l, Utils::Vector3d const &orig_pos, BoxGeometry const &box_geo)
static bool in_box(Utils::Vector3d const &pos, Utils::Vector3d const &lower_corner, Utils::Vector3d const &upper_corner)
static bool in_local_domain(LocalBox const &local_box, Utils::Vector3d const &pos, double halo=0.)
Check if a position is within the local box + halo.
static void positions_in_halo_impl(Utils::Vector3d const &pos_folded, Utils::Vector3d const &halo_lower_corner, Utils::Vector3d const &halo_upper_corner, BoxGeometry const &box_geo, std::vector< Utils::Vector3d > &res)
std::vector< Utils::Vector3d > positions_in_halo(Utils::Vector3d const &pos, BoxGeometry const &box_geo, LocalBox const &local_box, double agrid)
Return a vector of positions shifted by +,- box length in each coordinate.
static Utils::Vector3d lb_drag_force(Particle const &p, double lb_gamma, Utils::Vector3d const &v_fluid)
static Thermostat::GammaType lb_handle_particle_anisotropy(Particle const &p, double lb_gamma)
Random number generation using Philox.
uint64_t rng_counter() const
Get current value of the RNG.
uint32_t rng_seed() const
double gamma
Friction coefficient.
Utils::Vector3d get_coupling_interpolated_velocity(Utils::Vector3d const &pos) const
Calculate the interpolated fluid velocity in MD units.
void add_forces_at_pos(std::vector< Utils::Vector3d > const &pos, std::vector< Utils::Vector3d > const &forces)
double get_agrid() const
Get the LB grid spacing.
std::vector< Utils::Vector3d > get_coupling_interpolated_velocities(std::vector< Utils::Vector3d > const &pos) const
Calculate the interpolated fluid velocities in MD units.
Struct holding all information for one particle.
Definition Particle.hpp:435
auto const & gamma() const
Definition Particle.hpp:603
auto const & v() const
Definition Particle.hpp:473
auto const & mu_E() const
Definition Particle.hpp:584
auto const & id() const
Definition Particle.hpp:454