peclet-dem
Performance-portable XPBD Discrete Element Method (Kokkos + ArborX)
Loading...
Searching...
No Matches
test_dem_scheme_b.cpp
Go to the documentation of this file.
1// Scheme B (Newton-on: compute each pair once, reverse the ghost force to owners, integrate owned)
2// validated against a serial reference, using transport-core's ParticleHaloTopology.reverse. Each
3// pair is computed exactly ONCE (owned-ghost pairs are computed only on the rank whose particle has
4// the lower id), the reaction force is placed on the ghost, and reverse(ghostForce, ownedForce, +=)
5// accumulates those contributions back onto the owners. The resulting total force on every owned
6// particle must equal the serial all-pairs force, so the trajectories match (to round-off). Schemes
7// A (replicate) and C (local ghost integration) are in test_dem_step.cpp / test_dem_scheme_c.cpp.
8//
9// (Scheme A — frozen ghosts, replicated pair computation — is validated in test_dem_step.cpp.)
10#include <mpi.h>
11
12#include <algorithm>
13#include <array>
14#include <cmath>
15#include <cstdint>
16#include <cstdio>
17#include <cstring>
18#include <vector>
19
20#include "peclet/core/common/types.hpp"
21#include "peclet/core/decomp/block_decomposer.hpp"
22#include "peclet/core/halo/particle_halo_topology.hpp"
23#include "peclet/core/halo/particle_migrator.hpp"
24
25using namespace tpx;
26using peclet::core::decomp::BlockDecomposer;
27using peclet::core::halo::DomainMap;
28using peclet::core::halo::ParticleHaloTopology;
29using peclet::core::halo::ParticleMigrator;
30
31static constexpr int kSteps = 25;
32static const double Ld[3] = {10.0, 8.0, 6.0};
33static const double dmin[3] = {0, 0, 0};
34static constexpr double kR = 0.4, kRcut = 2.0 * kR, kStiff = 0.2, kDt = 0.02, kDamp = 0.99;
35
36struct F3 {
37 double v[3] = {0, 0, 0};
38 F3& operator+=(const F3& o) {
39 v[0] += o.v[0];
40 v[1] += o.v[1];
41 v[2] += o.v[2];
42 return *this;
43 }
44};
45struct Pay {
46 double vel[3];
47 std::int64_t id;
48};
49
50static double frac(std::uint64_t x, int s) {
51 x ^= (std::uint64_t)s * 2654435761u;
52 x ^= x >> 33;
53 x *= 0xff51afd7ed558ccdULL;
54 x ^= x >> 33;
55 return (double)(x & 0xFFFFFF) / (double)0x1000000;
56}
57static double mi(double d, double L) {
58 return d - L * std::round(d / L);
59}
60
61// repulsion on particle at a from particle at b (zero if beyond contact). min-image.
62static F3 pair_force(const Vec<3>& a, const Vec<3>& b) {
63 double s[3] = {mi(a[0] - b[0], Ld[0]), mi(a[1] - b[1], Ld[1]), mi(a[2] - b[2], Ld[2])};
64 double d = std::sqrt(s[0] * s[0] + s[1] * s[1] + s[2] * s[2]);
65 F3 f;
66 if (d < kRcut && d > 1e-12) {
67 double m = kStiff * (kRcut - d) / d;
68 f.v[0] = m * s[0];
69 f.v[1] = m * s[1];
70 f.v[2] = m * s[2];
71 }
72 return f;
73}
74
75int main(int argc, char** argv) {
76 MPI_Init(&argc, &argv);
77 int rank = 0, size = 1;
78 MPI_Comm_rank(MPI_COMM_WORLD, &rank);
79 MPI_Comm_size(MPI_COMM_WORLD, &size);
80 const std::int64_t N = 400;
81
82 std::vector<Vec<3>> p0(N), v0(N);
83 for (std::int64_t i = 0; i < N; ++i)
84 for (int a = 0; a < 3; ++a) {
85 p0[i][a] = dmin[a] + frac(i, a) * Ld[a];
86 v0[i][a] = (frac(i, 10 + a) - 0.5) * 0.2;
87 }
88
89 // --- serial reference (replicated, all-pairs) ---
90 std::vector<Vec<3>> sp = p0, sv = v0;
91 for (int step = 0; step < kSteps; ++step) {
92 std::vector<F3> F(N);
93 for (std::int64_t i = 0; i < N; ++i)
94 for (std::int64_t j = 0; j < N; ++j)
95 if (j != i) {
96 F3 f = pair_force(sp[i], sp[j]);
97 F[i] += f;
98 }
99 for (std::int64_t i = 0; i < N; ++i)
100 for (int a = 0; a < 3; ++a) {
101 sv[i][a] = (sv[i][a] + F[i].v[a] * kDt) * kDamp;
102 sp[i][a] += sv[i][a] * kDt;
103 sp[i][a] = dmin[a] + std::fmod(std::fmod(sp[i][a] - dmin[a], Ld[a]) + Ld[a], Ld[a]);
104 }
105 }
106
107 // --- distributed scheme C ---
108 IVec<3> gsize{40, 32, 24};
109 BlockDecomposer<3> dec(static_cast<std::size_t>(size), gsize);
110 DomainMap<3> map;
111 for (int a = 0; a < 3; ++a) {
112 map.origin[a] = dmin[a];
113 map.cellSize[a] = Ld[a] / gsize[a];
114 map.periodic[a] = true;
115 }
116 ParticleMigrator<3> mig;
117 mig.init(dec, rank, map, MPI_COMM_WORLD);
118 ParticleHaloTopology<3> halo;
119 halo.init(mig);
120
121 const std::size_t stride = sizeof(Pay);
122 std::vector<Vec<3>> pos;
123 std::vector<char> payload;
124 for (std::int64_t id = rank; id < N; id += size) {
125 pos.push_back(p0[id]);
126 Pay pay{};
127 for (int a = 0; a < 3; ++a)
128 pay.vel[a] = v0[id][a];
129 pay.id = id;
130 std::size_t off = payload.size();
131 payload.resize(off + stride);
132 std::memcpy(&payload[off], &pay, stride);
133 }
134
135 for (int step = 0; step < kSteps; ++step) {
136 mig.migrate(pos, payload, stride);
137 std::size_t n = pos.size();
138 std::vector<std::int64_t> id(n);
139 std::vector<Vec<3>> vel(n);
140 for (std::size_t i = 0; i < n; ++i) {
141 Pay p;
142 std::memcpy(&p, &payload[i * stride], stride);
143 id[i] = p.id;
144 vel[i] = {p.vel[0], p.vel[1], p.vel[2]};
145 }
146
147 halo.build(pos, kRcut);
148 std::size_t G = halo.numGhost();
149 const auto& gpos = halo.ghostPositions();
150 std::vector<double> ownIdD(n), ghIdD(G);
151 for (std::size_t i = 0; i < n; ++i)
152 ownIdD[i] = (double)id[i];
153 halo.forward(ownIdD.data(), ghIdD.data());
154
155 std::vector<F3> Fown(n), Fgh(G);
156 // owned-owned: each unordered pair once
157 for (std::size_t i = 0; i < n; ++i)
158 for (std::size_t j = i + 1; j < n; ++j) {
159 F3 f = pair_force(pos[i], pos[j]);
160 Fown[i] += f;
161 F3 mf{{-f.v[0], -f.v[1], -f.v[2]}};
162 Fown[j] += mf;
163 }
164 // owned-ghost: compute once, on the rank whose id is lower; reaction goes to the ghost
165 for (std::size_t i = 0; i < n; ++i)
166 for (std::size_t g = 0; g < G; ++g) {
167 if (id[i] >= (std::int64_t)std::llround(ghIdD[g]))
168 continue;
169 F3 f = pair_force(pos[i], gpos[g]);
170 Fown[i] += f;
171 F3 mf{{-f.v[0], -f.v[1], -f.v[2]}};
172 Fgh[g] += mf;
173 }
174 // accumulate ghost reactions back onto their owners
175 halo.reverse(Fgh.data(), Fown.data());
176
177 for (std::size_t i = 0; i < n; ++i)
178 for (int a = 0; a < 3; ++a) {
179 vel[i][a] = (vel[i][a] + Fown[i].v[a] * kDt) * kDamp;
180 pos[i][a] += vel[i][a] * kDt;
181 pos[i][a] = dmin[a] + std::fmod(std::fmod(pos[i][a] - dmin[a], Ld[a]) + Ld[a], Ld[a]);
182 }
183 for (std::size_t i = 0; i < n; ++i) {
184 Pay p;
185 p.id = id[i];
186 for (int a = 0; a < 3; ++a)
187 p.vel[a] = vel[i][a];
188 std::memcpy(&payload[i * stride], &p, stride);
189 }
190 }
191
192 int fail = 0;
193 for (std::size_t i = 0; i < pos.size(); ++i) {
194 Pay p;
195 std::memcpy(&p, &payload[i * stride], stride);
196 for (int a = 0; a < 3; ++a)
197 if (std::fabs(pos[i][a] - sp[p.id][a]) > 1e-9 || std::fabs(p.vel[a] - sv[p.id][a]) > 1e-9)
198 ++fail;
199 }
200 int total = 0;
201 MPI_Allreduce(&fail, &total, 1, MPI_INT, MPI_SUM, MPI_COMM_WORLD);
202 if (rank == 0) {
203 if (total == 0)
204 std::printf(
205 "OK (np=%d): scheme B (compute-once + reverse-force, integrate owned) matches serial\n",
206 size);
207 else
208 std::fprintf(stderr, "FAILED (np=%d): %d state mismatches\n", size, total);
209 }
210 MPI_Finalize();
211 return total == 0 ? 0 : 1;
212}
F3 & operator+=(const F3 &o)
double v[3]
std::int64_t id
double vel[3]
static F3 pair_force(const Vec< 3 > &a, const Vec< 3 > &b)
static constexpr double kRcut
int main(int argc, char **argv)
static constexpr double kDamp
static const double Ld[3]
static double frac(std::uint64_t x, int s)
static constexpr int kSteps
static double mi(double d, double L)
static constexpr double kR
static constexpr double kStiff
static constexpr double kDt
static const double dmin[3]
static const double L[3]