peclet-dem
Performance-portable XPBD Discrete Element Method (Kokkos + ArborX)
Loading...
Searching...
No Matches
test_dem_step.cpp
Go to the documentation of this file.
1// Track B, step 1: the distributed per-step Lagrangian loop, validated against a serial reference.
2//
3// This is the exact loop the real Simulation::step integration will follow, exercised with a simple
4// soft-sphere repulsion force in place of packing's XPBD/cuBQL (so it builds with just MPI +
5// transport-core). Per step, each rank:
6// migrate (reassign ownership) -> gatherGhosts(rcut) -> forces on OWNED from owned+ghost
7// within the interaction radius -> integrate owned.
8// Every rank also runs the full N-body system serially (replicated, deterministic) as the
9// reference; the distributed owned-particle states must match it for every particle, np=1,2,4.
10//
11// Bit-exactness across decompositions: forces are summed in a canonical order (sorted by neighbour
12// id) and separations use the minimum image, so the same neighbour set yields identical arithmetic
13// regardless of which rank owns which particle.
14#include <mpi.h>
15
16#include <algorithm>
17#include <array>
18#include <cmath>
19#include <cstdint>
20#include <cstdio>
21#include <cstring>
22#include <vector>
23
24#include "peclet/core/common/types.hpp"
25#include "peclet/core/decomp/block_decomposer.hpp"
26#include "peclet/core/halo/particle_migrator.hpp"
27
28using namespace tpx;
29using peclet::core::decomp::BlockDecomposer;
30using peclet::core::halo::DomainMap;
31using peclet::core::halo::ParticleMigrator;
32
33// Simulation constants.
34static constexpr int kSteps = 25;
35static const double L[3] = {10.0, 8.0, 6.0}; // periodic box size
36static const double dmin[3] = {0.0, 0.0, 0.0};
37static constexpr double kRadius = 0.4; // particle radius (contact at 2r)
38static constexpr double kRcut = 2.0 * kRadius; // interaction radius
39static constexpr double kStiff = 0.2; // contact stiffness
40static constexpr double kDt = 0.02;
41static constexpr double kDamp = 0.99; // velocity damping (keeps it bounded)
42
43struct Pay {
44 double vel[3];
45 std::int64_t id;
46};
47
48static double frac(std::uint64_t x, int s) {
49 x ^= (std::uint64_t)s * 2654435761u;
50 x ^= x >> 33;
51 x *= 0xff51afd7ed558ccdULL;
52 x ^= x >> 33;
53 return (double)(x & 0xFFFFFF) / (double)0x1000000;
54}
55static double minImage(double d, double Li) {
56 return d - Li * std::round(d / Li);
57}
58
59// Soft-sphere repulsion on particle at xi from neighbours (id, position), summed in id order.
60static std::array<double, 3> force(const Vec<3>& xi, std::int64_t selfId,
61 std::vector<std::pair<std::int64_t, Vec<3>>>& nbr) {
62 std::sort(nbr.begin(), nbr.end(), [](const auto& a, const auto& b) { return a.first < b.first; });
63 std::array<double, 3> F{0, 0, 0};
64 for (auto& [jid, xj] : nbr) {
65 if (jid == selfId)
66 continue;
67 double s[3] = {minImage(xi[0] - xj[0], L[0]), minImage(xi[1] - xj[1], L[1]),
68 minImage(xi[2] - xj[2], L[2])};
69 double d = std::sqrt(s[0] * s[0] + s[1] * s[1] + s[2] * s[2]);
70 if (d >= kRcut || d < 1e-12)
71 continue;
72 double mag = kStiff * (kRcut - d) / d; // push apart
73 F[0] += mag * s[0];
74 F[1] += mag * s[1];
75 F[2] += mag * s[2];
76 }
77 return F;
78}
79
80int main(int argc, char** argv) {
81 MPI_Init(&argc, &argv);
82 int rank = 0, size = 1;
83 MPI_Comm_rank(MPI_COMM_WORLD, &rank);
84 MPI_Comm_size(MPI_COMM_WORLD, &size);
85
86 const std::int64_t N = 400;
87
88 // Deterministic initial state (id 0..N-1).
89 std::vector<Vec<3>> p0(N);
90 std::vector<Vec<3>> v0(N);
91 for (std::int64_t i = 0; i < N; ++i) {
92 for (int a = 0; a < 3; ++a) {
93 p0[i][a] = dmin[a] + frac(i, a) * L[a];
94 v0[i][a] = (frac(i, 10 + a) - 0.5) * 0.2;
95 }
96 }
97
98 // --- Serial reference (replicated on every rank, deterministic) ---
99 std::vector<Vec<3>> sp = p0, sv = v0;
100 for (int step = 0; step < kSteps; ++step) {
101 std::vector<Vec<3>> F(N);
102 for (std::int64_t i = 0; i < N; ++i) {
103 std::vector<std::pair<std::int64_t, Vec<3>>> nbr;
104 nbr.reserve(N);
105 for (std::int64_t j = 0; j < N; ++j)
106 nbr.push_back({j, sp[j]});
107 F[i] = force(sp[i], i, nbr);
108 }
109 for (std::int64_t i = 0; i < N; ++i)
110 for (int a = 0; a < 3; ++a) {
111 sv[i][a] = (sv[i][a] + F[i][a] * kDt) * kDamp;
112 sp[i][a] += sv[i][a] * kDt;
113 sp[i][a] = dmin[a] + std::fmod(std::fmod(sp[i][a] - dmin[a], L[a]) + L[a], L[a]);
114 }
115 }
116
117 // --- Distributed ---
118 IVec<3> gsize{40, 32, 24};
119 BlockDecomposer<3> dec(static_cast<std::size_t>(size), gsize);
120 DomainMap<3> map;
121 for (int a = 0; a < 3; ++a) {
122 map.origin[a] = dmin[a];
123 map.cellSize[a] = L[a] / gsize[a];
124 map.periodic[a] = true;
125 }
126 ParticleMigrator<3> mig;
127 mig.init(dec, rank, map, MPI_COMM_WORLD);
128
129 const std::size_t stride = sizeof(Pay);
130 std::vector<Vec<3>> pos;
131 std::vector<char> payload;
132 for (std::int64_t id = rank; id < N; id += size) { // scatter by id%size; migrate fixes ownership
133 pos.push_back(p0[id]);
134 Pay pay{};
135 pay.vel[0] = v0[id][0];
136 pay.vel[1] = v0[id][1];
137 pay.vel[2] = v0[id][2];
138 pay.id = id;
139 std::size_t off = payload.size();
140 payload.resize(off + stride);
141 std::memcpy(&payload[off], &pay, stride);
142 }
143
144 for (int step = 0; step < kSteps; ++step) {
145 mig.migrate(pos, payload, stride);
146 std::vector<Vec<3>> gpos;
147 std::vector<char> gpay;
148 mig.gatherGhosts(pos, payload, stride, kRcut, gpos, gpay);
149
150 std::size_t n = pos.size();
151 std::vector<Vec<3>> F(n);
152 for (std::size_t i = 0; i < n; ++i) {
153 Pay pi;
154 std::memcpy(&pi, &payload[i * stride], stride);
155 std::vector<std::pair<std::int64_t, Vec<3>>> nbr;
156 nbr.reserve(n + gpos.size());
157 for (std::size_t j = 0; j < n; ++j) {
158 Pay pj;
159 std::memcpy(&pj, &payload[j * stride], stride);
160 nbr.push_back({pj.id, pos[j]});
161 }
162 for (std::size_t j = 0; j < gpos.size(); ++j) {
163 Pay pj;
164 std::memcpy(&pj, &gpay[j * stride], stride);
165 nbr.push_back({pj.id, gpos[j]});
166 }
167 F[i] = force(pos[i], pi.id, nbr);
168 }
169 for (std::size_t i = 0; i < n; ++i) {
170 Pay pi;
171 std::memcpy(&pi, &payload[i * stride], stride);
172 for (int a = 0; a < 3; ++a) {
173 pi.vel[a] = (pi.vel[a] + F[i][a] * kDt) * kDamp;
174 pos[i][a] += pi.vel[a] * kDt;
175 pos[i][a] = dmin[a] + std::fmod(std::fmod(pos[i][a] - dmin[a], L[a]) + L[a], L[a]);
176 }
177 std::memcpy(&payload[i * stride], &pi, stride);
178 }
179 }
180
181 // Compare each owned particle to the serial reference (by id).
182 int fail = 0;
183 for (std::size_t i = 0; i < pos.size(); ++i) {
184 Pay pi;
185 std::memcpy(&pi, &payload[i * stride], stride);
186 for (int a = 0; a < 3; ++a) {
187 if (std::fabs(pos[i][a] - sp[pi.id][a]) > 1e-9)
188 ++fail;
189 if (std::fabs(pi.vel[a] - sv[pi.id][a]) > 1e-9)
190 ++fail;
191 }
192 }
193 std::int64_t lcount = (std::int64_t)pos.size(), gcount = 0;
194 MPI_Reduce(&lcount, &gcount, 1, MPI_INT64_T, MPI_SUM, 0, MPI_COMM_WORLD);
195 int total = 0;
196 MPI_Allreduce(&fail, &total, 1, MPI_INT, MPI_SUM, MPI_COMM_WORLD);
197 if (rank == 0) {
198 std::printf("# DEM loop: %lld particles, %d steps (count check %lld==%lld)\n", (long long)N,
199 kSteps, (long long)gcount, (long long)N);
200 if (total == 0)
201 std::printf("OK (np=%d): distributed migrate+ghost+force+integrate matches serial\n", size);
202 else
203 std::fprintf(stderr, "FAILED (np=%d): %d state mismatches\n", size, total);
204 }
205 MPI_Finalize();
206 return total == 0 ? 0 : 1;
207}
std::int64_t id
double vel[3]
static constexpr double kRcut
int main(int argc, char **argv)
static constexpr double kDamp
static double frac(std::uint64_t x, int s)
static constexpr int kSteps
static const double L[3]
static constexpr double kStiff
static double minImage(double d, double Li)
static constexpr double kRadius
static std::array< double, 3 > force(const Vec< 3 > &xi, std::int64_t selfId, std::vector< std::pair< std::int64_t, Vec< 3 > > > &nbr)
static constexpr double kDt
static const double dmin[3]