ESPResSo
Extensible Simulation Package for Research on Soft Matter Systems
Loading...
Searching...
No Matches
forces_cabana.hpp
Go to the documentation of this file.
1/*
2 * Copyright (C) 2025-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#pragma once
21
22#include <config/config.hpp>
23
24#include "aosoa_pack.hpp"
25#include "forces_inline.hpp"
27
28#include <utils/Vector.hpp>
29
30#include <Cabana_Core.hpp>
31#include <Kokkos_ScatterView.hpp>
32
33#include <cstddef>
34#include <memory>
35#include <optional>
36#include <variant>
37#include <vector>
38
39#ifdef ESPRESSO_P3M
40/** @brief The active P3M solver, or nullptr. Deliberately checks only the
41 * top level of the solver variant (unlike @ref get_actor_by_type, which
42 * recurses into layer corrections): under ELC the pair force must go through
43 * the generic coulomb kernel plus the ELC corrections, not the bare P3M
44 * fast path.
45 */
46inline CoulombP3M const *
48 if (auto const &solver = coulomb.impl->solver; solver.has_value()) {
49 if (std::holds_alternative<std::shared_ptr<CoulombP3M>>(*solver)) {
50 return std::get<std::shared_ptr<CoulombP3M>>(*solver).get();
51 }
52 }
53 return nullptr;
54}
55#endif
56
57#ifdef ESPRESSO_ELECTROSTATICS
58/** @brief Real-space charge-charge pair force: the P3M fast path when the
59 * solver is (top-level) P3M, the type-erased coulomb kernel otherwise.
60 * Shared by @ref ForcesKernel and @ref SpecializedForcesKernel so the
61 * dispatch cannot drift between them.
62 */
64 double q1q2, Utils::Vector3d const &d, double dist,
67 ,
68 CoulombP3M const *p3m
69#endif
70) {
71#ifdef ESPRESSO_P3M
72 if (p3m) [[likely]] {
73 return p3m->pair_force(q1q2, d, dist);
74 }
75#endif
76 return (*coulomb_kernel)(q1q2, d, dist);
77}
78#endif
79
89 std::vector<Particle *> const &unique_particles;
91#ifdef ESPRESSO_ROTATION
93#endif
94#ifdef ESPRESSO_DIPOLE_FIELD_TRACKING
96#endif
97#ifdef ESPRESSO_NPT
100#endif
102#ifdef ESPRESSO_P3M
104#endif
106
116 std::vector<Particle *> const &unique_particles_,
120#endif
123#endif
127#endif
137#endif
140#endif
143#endif
145#ifdef ESPRESSO_P3M
147#endif
148 }
149
150#ifdef ESPRESSO_NPT
154#endif
155
156 ESPRESSO_ATTR_ALWAYS_INLINE inline void operator()(std::size_t i,
157 std::size_t j) const {
158
159 // calc distance (component-wise, avoids constructing pos1/pos2 Vector3d
160 // on the hot early-exit path; pos1/pos2 are built lazily below only
161 // where kernels actually require them)
162 auto const d = box_geo.get_mi_vector(
163 aosoa.position(i, 0), aosoa.position(i, 1), aosoa.position(i, 2),
164 aosoa.position(j, 0), aosoa.position(j, 1), aosoa.position(j, 2));
165 auto const dist_sq = d.norm2();
166
167 // Early exit if distance > maximal global cutoff
169 return;
170 auto const dist = std::sqrt(dist_sq);
171 auto const &ia_params =
173
175
176 // Determine which data needs to be loaded based on active algorithms
177#if defined(ESPRESSO_EXCLUSIONS) or defined(ESPRESSO_THOLE)
178 bool need_particle_pointers = false;
179#ifdef ESPRESSO_EXCLUSIONS
181#endif
182#ifdef ESPRESSO_THOLE
185#endif
186
187 Particle const *p1_ptr = nullptr;
188 Particle const *p2_ptr = nullptr;
190 p1_ptr = unique_particles.at(i);
192 }
193#endif
194
195 /***********************************************/
196 /* non-bonded pair potentials */
197 /***********************************************/
198
199 if (dist <= ia_params.max_cut) {
200#ifdef ESPRESSO_EXCLUSIONS
201 bool skip_non_bonded = false;
204 }
205#else
206 constexpr bool skip_non_bonded = false;
207#endif
208 if (not skip_non_bonded) {
210
211 // Only call Thole force kernel if active
212#ifdef ESPRESSO_THOLE
213 if (thole_active(ia_params, coulomb_kernel != nullptr)) {
216 }
217#endif
218 // Only call Gay-Berne force kernel if active
219#ifdef ESPRESSO_GAY_BERNE
221 auto const dir1 = aosoa.get_vector_at(aosoa.director, i);
222 auto const dir2 = aosoa.get_vector_at(aosoa.director, j);
224 }
225#endif
226#ifdef ESPRESSO_DPD
228 auto const pos1 = aosoa.get_vector_at(aosoa.position, i);
229 auto const pos2 = aosoa.get_vector_at(aosoa.position, j);
230 auto const vel1 = aosoa.get_vector_at(aosoa.velocity, i);
231 auto const vel2 = aosoa.get_vector_at(aosoa.velocity, j);
232 auto const force = dpd_pair_force(
233 pos1, vel1, aosoa.id(i), pos2, vel2, aosoa.id(j), *thermostat.dpd,
235 pf += force;
236 }
237#endif // ESPRESSO_DPD
238
239 } // not skip_non_bonded
240 } // not dist > ia_params.max_cut
241
242 /*********************************************************************/
243 /* everything before this contributes to the virial pressure in NpT */
244 /* via d (x) pf.f; electrostatic and dipolar real-space contributions */
245 /* are added in explicitly below instead: Coulomb reuses the pair */
246 /* energy as a virial proxy, dipoles compute d . F directly (see */
247 /* rationale below) */
248 /*********************************************************************/
249#ifdef ESPRESSO_NPT
251 if (npt_active()) {
252 virial = hadamard_product(pf.f, d);
253 }
254#endif // ESPRESSO_NPT
255
256#ifdef ESPRESSO_ELECTROSTATICS
259 // real-space electrostatic charge-charge interaction
260 if (coulomb_kernel != nullptr) {
261 if ((aosoa.charge(i) != 0.) and (aosoa.charge(j) != 0.)) {
262 auto const q1q2 = aosoa.charge(i) * aosoa.charge(j);
265 ,
266 p3m
267#endif
268 );
269 if (elc_kernel) {
270 auto const pos1 = aosoa.get_vector_at(aosoa.position, i);
271 auto const pos2 = aosoa.get_vector_at(aosoa.position, j);
272 (*elc_kernel)(pos1, pos2, f1_asym, f2_asym, q1q2);
273 }
274#ifdef ESPRESSO_NPT
275 if (npt_active()) {
276 auto const pos1 = aosoa.get_vector_at(aosoa.position, i);
277 auto const pos2 = aosoa.get_vector_at(aosoa.position, j);
278 virial[0] += (*coulomb_u_kernel)(pos1, pos2, q1q2, d, dist);
279 }
280#endif // ESPRESSO_NPT
281 }
282 }
283#endif // ESPRESSO_ELECTROSTATICS
284
285#ifdef ESPRESSO_DIPOLES
286 if (dipoles_kernel != nullptr) {
287 auto const d1d2 = aosoa.dipm(i) * aosoa.dipm(j);
288 if (d1d2 != 0.) {
289 auto const dir1 = aosoa.get_vector_at(aosoa.director, i);
290 auto const dir2 = aosoa.get_vector_at(aosoa.director, j);
291#ifdef ESPRESSO_DIPOLE_FIELD_TRACKING
294#endif
295 auto const dip_pf =
296 (*dipoles_kernel)(d1d2, aosoa.dipm(i) * dir1, aosoa.dipm(j) * dir2,
297#ifdef ESPRESSO_DIPOLE_FIELD_TRACKING
299#endif
300 d, dist, dist_sq);
301#ifdef ESPRESSO_DIPOLE_FIELD_TRACKING
302 auto access_dip_fld = local_dip_fld.access();
303 access_dip_fld(i, 0) += dip_fld_i[0];
304 access_dip_fld(i, 1) += dip_fld_i[1];
305 access_dip_fld(i, 2) += dip_fld_i[2];
306 access_dip_fld(j, 0) += dip_fld_j[0];
307 access_dip_fld(j, 1) += dip_fld_j[1];
308 access_dip_fld(j, 2) += dip_fld_j[2];
309#endif
310#ifdef ESPRESSO_NPT
311 if (npt_active()) {
312 // d . F = -n * U for a homogeneous potential of degree n
313 // (Euler's theorem, independent of centrality); n=-3 here vs
314 // n=-1 for Coulomb. Ewald screening makes that only
315 // approximate, and for dipoles the approximation measurably
316 // fails NpT pressure consistency (see test_pressure_with_dp3m),
317 // so d . F is computed explicitly here instead.
318 virial[0] += d * dip_pf.f;
319 }
320#endif // ESPRESSO_NPT
321 pf += dip_pf;
322 }
323 }
324#endif // ESPRESSO_DIPOLES
325
326 auto opf = calc_opposing_force(pf, d);
327#ifdef ESPRESSO_ELECTROSTATICS
328 pf.f += f1_asym;
329 opf.f += f2_asym;
330#endif // ESPRESSO_ELECTROSTATICS
331
332 auto access_force = local_force.access();
333
334 access_force(i, 0) += pf.f[0];
335 access_force(i, 1) += pf.f[1];
336 access_force(i, 2) += pf.f[2];
337#ifdef ESPRESSO_ROTATION
338 auto access_torque = local_torque.access();
339 access_torque(i, 0) += pf.torque[0];
340 access_torque(i, 1) += pf.torque[1];
341 access_torque(i, 2) += pf.torque[2];
342#endif
343
344 access_force(j, 0) += opf.f[0];
345 access_force(j, 1) += opf.f[1];
346 access_force(j, 2) += opf.f[2];
347#ifdef ESPRESSO_ROTATION
348 access_torque(j, 0) += opf.torque[0];
349 access_torque(j, 1) += opf.torque[1];
350 access_torque(j, 2) += opf.torque[2];
351#endif
352#ifdef ESPRESSO_NPT
353 if (npt_active()) {
354 auto access_virial = local_virial.access();
355 access_virial(0) += virial[0];
356 access_virial(1) += virial[1];
357 access_virial(2) += virial[2];
358 }
359#endif
360 }
361};
362
363/** @brief Pair potentials fully handled by @ref SpecializedForcesKernel,
364 * i.e. exactly the central-radial family dispatched through
365 * @ref calc_central_radial_force. Potentials with their own branch in
366 * @ref ForcesKernel (Gay-Berne, DPD) are deliberately absent, and so is any
367 * newly added potential until it is proven compatible -- the dispatch gate
368 * in forces.cpp rejects any pair mask with a bit outside this allowlist, so
369 * new potentials fall back to the generic kernel by default.
370 */
386
387/**
388 * @brief Own-the-loop specialization of the non-bonded pair kernel.
389 *
390 * Handles the common case of a cuboid box with only central radial pair
391 * forces active, optionally with real-space electrostatics (@p HasCoulomb).
392 * The dispatch in forces.cpp selects it only when no torque-producing,
393 * asymmetric, thermostat or exclusion interaction is active (no dipoles, ELC,
394 * DPD, NPT virial, exclusions, Thole or Gay-Berne, no Lees-Edwards), and
395 * falls back to @ref ForcesKernel otherwise.
396 *
397 * Unlike @ref ForcesKernel (invoked once per pair by
398 * Cabana::neighbor_parallel_for), this owns the Verlet-list loop and runs once
399 * per particle. That lets it hoist the per-particle position, type and charge,
400 * capture the cuboid box parameters by value, and obtain the ScatterView
401 * accessor once per particle instead of once per pair -- the per-pair
402 * `access()` is an omp_get_thread_num call that dominates the LJ pair cost. It
403 * also accumulates the i-side force in a local register over all of particle
404 * i's neighbors and updates the ScatterView once per particle instead of once
405 * per pair; the j-side write stays per-pair (each neighbor j is distinct). This
406 * removes the per-pair i-side accessor update at the cost of changing the
407 * i-side summation order (a single register sum added once, rather than
408 * incremental scatter adds), so the result matches @ref ForcesKernel to
409 * floating-point round-off rather than bitwise. `if constexpr (HasCoulomb)`
410 * compiles the electrostatics path in or out entirely.
411 *
412 * The neighbors are processed in fixed-size tiles, each in three passes: a
413 * scalar gather of the neighbor positions into SoA scratch, a vectorized
414 * minimum-image pass (@ref CuboidMinimumImage::batch_vector_dist2) that folds
415 * the whole tile at once and squares the distances, and a scalar pass that
416 * runs the short-range force only for the pairs whose squared distance passes
417 * the cutoff gate. The vectorized pass yields the same per-pair fold vector and
418 * squared distance as the scalar path, so identity is preserved.
419 */
420template <bool HasCoulomb> struct SpecializedForcesKernel {
421 static constexpr int tile_size = 64;
422
426 Kokkos::View<int const *, Kokkos::HostSpace> counts;
427 Kokkos::View<int const **, Kokkos::LayoutRight, Kokkos::HostSpace> neighbors;
430#ifdef ESPRESSO_ELECTROSTATICS
432#ifdef ESPRESSO_P3M
434#endif
435#endif
436
438 operator()(std::size_t const i) const {
439 auto const n_neighbors = counts(i);
440 if (n_neighbors == 0)
441 return;
442
443 auto const x_i = aosoa.position(i, 0);
444 auto const y_i = aosoa.position(i, 1);
445 auto const z_i = aosoa.position(i, 2);
446 auto const type_i = aosoa.type(i);
447#ifdef ESPRESSO_ELECTROSTATICS
448 double charge_i = 0.;
449 if constexpr (HasCoulomb) {
450 charge_i = aosoa.charge(i);
451 }
452#endif
453
454 // One ScatterView accessor for all of this particle's pairs.
455 auto access_force = local_force.access();
456
457 // Accumulate the i-side force over all neighbors in a local register and
458 // write it to the ScatterView once at the end (see class docs).
460
461 // Per-tile SoA scratch (thread-local, on the stack).
462 int js[tile_size];
463 double sx[tile_size], sy[tile_size], sz[tile_size];
465
466 for (int base = 0; base < n_neighbors; base += tile_size) {
467 // ``+tile_size`` creates a prvalue and avoids an ODR-use of a host-space
468 // variable from device code (Kokkos::min() takes arguments by const &T)
469 auto const m = Kokkos::min(+tile_size, n_neighbors - base);
470
471 // Pass 1: scalar gather of the tile's neighbor positions.
472 for (int t = 0; t < m; ++t) {
473 auto const j = neighbors(i, base + t);
474 js[t] = j;
475 auto const row_j = static_cast<std::size_t>(j);
476 sx[t] = aosoa.position(row_j, 0);
477 sy[t] = aosoa.position(row_j, 1);
478 sz[t] = aosoa.position(row_j, 2);
479 }
480
481 // Pass 2: vectorized minimum-image fold + squared distance.
483 dx2, dsq);
484
485 // Pass 3: scalar short-range force for the pairs that pass the gate,
486 // in neighbor order (i before j) to preserve the accumulation order.
487 for (int t = 0; t < m; ++t) {
488 if (dsq[t] > system_max_cutoff_sq)
489 continue;
490 auto const j = static_cast<std::size_t>(js[t]);
491 Utils::Vector3d const d{dx0[t], dx1[t], dx2[t]};
492 auto const dist = std::sqrt(dsq[t]);
493 auto const &ia_params =
495
496 Utils::Vector3d f{};
497 if (dist <= ia_params.max_cut) {
499 }
500#ifdef ESPRESSO_ELECTROSTATICS
501 if constexpr (HasCoulomb) {
502 auto const charge_j = aosoa.charge(j);
503 if (charge_i != 0. and charge_j != 0.) {
504 auto const q1q2 = charge_i * charge_j;
507 ,
508 p3m
509#endif
510 );
511 }
512 }
513#endif
514
515 f_i += f;
516 access_force(j, 0) -= f[0];
517 access_force(j, 1) -= f[1];
518 access_force(j, 2) -= f[2];
519 }
520 }
521
522 // Single i-side ScatterView update for the whole neighbor loop.
523 access_force(i, 0) += f_i[0];
524 access_force(i, 1) += f_i[1];
525 access_force(i, 2) += f_i[2];
526 }
527};
Vector implementation and trait types for boost qvm interoperability.
#define ESPRESSO_ATTR_ALWAYS_INLINE
container for bonded interactions.
ESPRESSO_ATTR_ALWAYS_INLINE Utils::Vector3< T > get_mi_vector(Utils::Vector3< T > const &a, Utils::Vector3< T > const &b) const
Get the minimum-image vector between two coordinates.
Kokkos::Experimental::ScatterView< double *[3], Kokkos::LayoutRight, memory_space > ScatterForce
Kokkos::Experimental::ScatterView< double[3], Kokkos::LayoutRight, memory_space > ScatterVirial
Cuboid minimum-image fold parameters for hot pair loops.
ESPRESSO_ATTR_ALWAYS_INLINE void batch_vector_dist2(double xi, double yi, double zi, int m, double const *sx, double const *sy, double const *sz, double *dx0, double *dx1, double *dx2, double *dsq) const
Batched minimum-image vector and squared distance: one point (xi, yi, zi) against m others held in th...
auto & get_ia_param(int i, int j)
Get interaction parameters between particle types i and j.
std::shared_ptr< DPDThermostat > dpd
int thermo_switch
Bitmask of currently active thermostats.
cudaStream_t stream[1]
CUDA streams for parallel computing on CPU and GPU.
Utils::Vector3d dpd_pair_force(Utils::Vector3d const &p1_position, Utils::Vector3d const &p1_velocity, int p1_id, Utils::Vector3d const &p2_position, Utils::Vector3d const &p2_velocity, int p2_id, DPDThermostat const &dpd, BoxGeometry const &box_geo, IA_parameters const &ia_params, Utils::Vector3d const &d, double dist, double dist2)
Definition dpd.cpp:75
bool do_nonbonded(Particle const &p1, Particle const &p2)
Determine if the non-bonded interactions between p1 and p2 should be calculated.
CoulombP3M const * get_toplevel_p3m_solver(Coulomb::Solver const &coulomb)
The active P3M solver, or nullptr.
constexpr unsigned specialized_kernel_pair_mask
Pair potentials fully handled by SpecializedForcesKernel, i.e.
ESPRESSO_ATTR_ALWAYS_INLINE Utils::Vector3d coulomb_pair_force(double q1q2, Utils::Vector3d const &d, double dist, Coulomb::ShortRangeForceKernel::kernel_type const *coulomb_kernel, CoulombP3M const *p3m)
Real-space charge-charge pair force: the P3M fast path when the solver is (top-level) P3M,...
Force calculation.
ParticleForce calc_opposing_force(ParticleForce const &pf, Utils::Vector3d const &d)
ESPRESSO_ATTR_ALWAYS_INLINE Utils::Vector3d calc_central_radial_force(IA_parameters const &ia_params, Utils::Vector3d const &d, double const dist)
ParticleForce gb_pair_force(Utils::Vector3d const &ui, Utils::Vector3d const &uj, IA_parameters const &ia_params, Utils::Vector3d const &d, double dist)
Calculate Gay-Berne force and torques.
Definition gay_berne.hpp:49
STL namespace.
DEVICE_QUALIFIER constexpr unsigned pair_potential_bit(PairPotential p)
Bitmask for a pair potential.
ESPRESSO_ATTR_ALWAYS_INLINE KOKKOS_INLINE_FUNCTION bool gay_berne_active(double dist, IA_parameters const &ia_params)
KOKKOS_INLINE_FUNCTION bool thole_active(IA_parameters const &ia_params, bool has_coulomb_kernel)
ESPRESSO_ATTR_ALWAYS_INLINE KOKKOS_INLINE_FUNCTION bool dpd_active(IA_parameters const &ia_params, int thermo_switch)
DEVICE_QUALIFIER bool has_exclusion(std::size_t i) const
PositionViewType position
DEVICE_QUALIFIER Utils::Vector< T, N > get_vector_at(Kokkos::View< T *[N], array_layout, Kokkos::HostSpace > const &view, std::size_t i) const
DirectorViewType director
VelocityViewType velocity
P3M solver.
Definition p3m.hpp:55
Solver::ShortRangeEnergyKernel kernel_type
Solver::ShortRangeForceCorrectionsKernel kernel_type
Solver::ShortRangeForceKernel kernel_type
std::unique_ptr< Implementation > impl
Pointer-to-implementation.
Solver::ShortRangeForceKernel kernel_type
std::vector< Particle * > const & unique_particles
CellStructure::ScatterForce local_force
Coulomb::ShortRangeEnergyKernel::kernel_type const *const coulomb_u_kernel
Coulomb::ShortRangeForceCorrectionsKernel::kernel_type const * elc_kernel
ESPRESSO_ATTR_ALWAYS_INLINE KOKKOS_INLINE_FUNCTION bool npt_active() const
CellStructure::ScatterVirial local_virial
CellStructure::ScatterForce local_dip_fld
BoxGeometry const & box_geo
double system_max_cutoff_sq
Dipoles::ShortRangeForceKernel::kernel_type const *const dipoles_kernel
CellStructure::AoSoA_pack const & aosoa
CellStructure::ScatterForce local_torque
InteractionsNonBonded const & nonbonded_ias
Utils::Vector3d *const global_virial
ForcesKernel(BondedInteractionsMap const &bonded_ias_, InteractionsNonBonded const &nonbonded_ias_, Coulomb::ShortRangeForceKernel::kernel_type const *coulomb_kernel_, Dipoles::ShortRangeForceKernel::kernel_type const *dipoles_kernel_, Coulomb::ShortRangeForceCorrectionsKernel::kernel_type const *elc_kernel_, Coulomb::ShortRangeEnergyKernel::kernel_type const *coulomb_u_kernel_, Coulomb::Solver const &coulomb_, Thermostat::Thermostat const &thermostat_, BoxGeometry const &box_geo_, std::vector< Particle * > const &unique_particles_, CellStructure::ScatterForce local_force_, CellStructure::ScatterForce local_torque_, CellStructure::ScatterForce local_dip_fld_, Utils::Vector3d *const global_virial_, CellStructure::ScatterVirial local_virial_, CellStructure::AoSoA_pack const &aosoa_, double system_max_cutoff_)
BondedInteractionsMap const & bonded_ias
CoulombP3M const * p3m
Coulomb::ShortRangeForceKernel::kernel_type const *const coulomb_kernel
Thermostat::Thermostat const & thermostat
ESPRESSO_ATTR_ALWAYS_INLINE void operator()(std::size_t i, std::size_t j) const
Force information on a particle.
Definition Particle.hpp:331
Struct holding all information for one particle.
Definition Particle.hpp:436
Own-the-loop specialization of the non-bonded pair kernel.
ESPRESSO_ATTR_ALWAYS_INLINE void operator()(std::size_t const i) const
Kokkos::View< int const *, Kokkos::HostSpace > counts
CellStructure::ScatterForce local_force
Coulomb::ShortRangeForceKernel::kernel_type const * coulomb_kernel
InteractionsNonBonded const & nonbonded_ias
static constexpr int tile_size
Kokkos::View< int const **, Kokkos::LayoutRight, Kokkos::HostSpace > neighbors
CellStructure::AoSoA_pack const & aosoa
CoulombP3M const * p3m
CuboidMinimumImage minimum_image
Utils::Vector3d thole_pair_force(Particle const &p1, Particle const &p2, IA_parameters const &ia_params, Utils::Vector3d const &d, double dist, BondedInteractionsMap const &bonded_ias, Coulomb::ShortRangeForceKernel::kernel_type const *kernel)
Calculate Thole force.
Definition thole.hpp:45