PeriDEM 0.3.0
PeriDEM -- Peridynamics-based high-fidelity model for granular media
Loading...
Searching...
No Matches
main.cpp
Go to the documentation of this file.
1/*
2 * -------------------------------------------
3 * Copyright (c) 2021 - 2026 Prashant K. Jha
4 * -------------------------------------------
5 * PeriDEM https://github.com/prashjha/PeriDEM
6 *
7 * Short multi-particle compression example derived from Jha et al. JMPS 2021
8 * §4.3 (same M1, lc=R/5, horizon 3 lc, Rc=0.95 h, C-bar=100, plate vy=-0.06).
9 * Not the paper’s 502-grain run: 4×3 equal circles, gaps just outside Rc so a
10 * few grains enter contact under a stable Δt (≈ 0.25 h/c, same 0.2 μs as Table 2).
11 *
12 * -inbuiltMesh Gmsh in-process (default; cup size follows the tight pack)
13 * -fileMesh frozen meshes in JHA2021_COMP_MESH_DIR
14 * -finalTime / -numSteps default T=0.004 s, 20000 steps (Δt=0.2 μs)
15 * -requireContact default on; fail unless a grain–grain pair enters Rc
16 * -noRequireContact
17 * -assertForce fail unless plate reaction csv is nonzero
18 */
19
21#include "inp/deckIncludes.h"
22#include "inp/input.h"
24#include "data/modelData.h"
27#include "periDEMModel.h"
29#include "util/function.h"
30#include "util/io.h"
31#include "util/parallelUtil.h"
32
33#include <mpi.h>
34#include <algorithm>
35#include <cmath>
36#include <cstring>
37#include <filesystem>
38#include <format>
39#include <iostream>
40#include <fstream>
41#include <stdexcept>
42#include <string>
43#include <thread>
44#include <vector>
45
46namespace {
47
48std::string directoryPathWithTrailingSep(const std::filesystem::path &dir) {
49 namespace fs = std::filesystem;
50 fs::path n = fs::absolute(dir).lexically_normal();
51 std::string s = n.string();
52 if (!s.empty() && s.back() != '/' && s.back() != '\\')
53 s += fs::path::preferred_separator;
54 return s;
55}
56
57struct PackedParticle {
58 double x{}, y{}, z{}, r{}, theta{};
59};
60
61std::vector<PackedParticle> generateCircularGrid(int ncols, int nrows, double x_lo,
62 double y_lo, double R, double gap) {
63 std::vector<PackedParticle> particles;
64 particles.reserve(static_cast<size_t>(ncols * nrows));
65 const double step = 2.0 * R + gap;
66 const double x0 = x_lo + gap + R;
67 const double y0 = y_lo + gap + R;
68 for (int j = 0; j < nrows; ++j) {
69 for (int i = 0; i < ncols; ++i) {
71 p.x = x0 + static_cast<double>(i) * step;
72 p.y = y0 + static_cast<double>(j) * step;
73 p.z = 0.0;
74 p.r = R;
75 p.theta = 0.0;
76 particles.push_back(p);
77 }
78 }
79 return particles;
80}
81
82// Default 4×3 = 12 grains; override with -nCols / -nRows for scaling studies.
83json buildInputJson(const std::string &output_path_for_deck,
84 const std::filesystem::path &mesh_cir,
85 const std::filesystem::path &mesh_fixed,
86 const std::filesystem::path &mesh_moving, double final_time,
87 size_t num_steps, bool file_mesh, bool write_meshes,
88 size_t search_interval, int ncols, int nrows,
89 const std::string &mpi_strategy = "auto") {
90
91 if (ncols < 1 || nrows < 1)
92 throw std::runtime_error("jha2021_comp: ncols and nrows must be >= 1");
93 const int ngrains = ncols * nrows;
94
95 const std::vector<double> center = {0.0, 0.0, 0.0};
96 const double R = 0.001;
97 const double mesh_size = R / 5.0;
98 const double horizon = 3.0 * mesh_size;
99 // Realized hmin on this disk is ~0.7 lc. Start just outside Rc so contact
100 // is not present at t=0; plate + gravity pull a few pairs into Rc.
101 const double h_est = 0.7 * mesh_size;
102 const double Rc_est = 0.95 * h_est;
103 const double particle_padding = 1.15 * Rc_est;
104 const double wpd = particle_padding;
105 const double rwp = horizon + wpd;
106 constexpr double wall_vy = -0.06;
107
108 const double Lin = 2.0 * particle_padding + 2.0 * R +
109 static_cast<double>(ncols - 1) * (2.0 * R + particle_padding);
110 const double Win = 2.0 * particle_padding + 2.0 * R +
111 static_cast<double>(nrows - 1) * (2.0 * R + particle_padding);
112
113 const double wall_t = rwp - wpd;
114 const std::vector<double> mw_rect = {center[0] - wpd, Win, center[2],
115 Lin + wpd, Win + wall_t, center[2]};
116 const std::vector<double> cup_channel = {center[0] - rwp, center[1] - rwp,
117 Lin + rwp, Win + wall_t, wall_t,
118 center[2]};
119
120 std::vector<geom::GeomData> pGeomVec(3);
121 pGeomVec[0].d_geomName = "circle";
122 pGeomVec[0].d_geomParams = {R, center[0], center[1], center[2]};
123 pGeomVec[1].d_geomName = "open_rect_channel_2d";
124 pGeomVec[1].d_geomParams = cup_channel;
125 pGeomVec[2].d_geomName = "rectangle";
126 pGeomVec[2].d_geomParams = mw_rect;
127
128 std::vector<PackedParticle> packed =
129 generateCircularGrid(ncols, nrows, 0.0, 0.0, R, particle_padding);
130 if (static_cast<int>(packed.size()) != ngrains)
131 throw std::runtime_error("jha2021_comp: grid count mismatch");
132
133 const double poisson = 0.25;
134 const double rho = 1200.0;
135 const double K = 2.16e+7;
136 const double E = material::toE(K, poisson);
137 const double G = material::toGE(E, poisson);
138 const double Gc = 50.0;
139 const double Kn = 18.0 * util::harmonicMean(K, K) / (M_PI * std::pow(horizon, 5));
140
141 const double dt = final_time / static_cast<double>(num_steps);
142 const double c_wave = std::sqrt(E / rho);
143 const double dt_cfl = h_est / c_wave;
144 util::io::print(std::format(
145 "jha2021_comp: N={} ({}x{}), gap={:.6e} (1.15 Rc_est), wpd={:.6e}, "
146 "dt={:.6e} s, h/c={:.6e} s, dt/(h/c)={:.3f}\n",
147 ngrains, ncols, nrows, particle_padding, wpd, dt, dt_cfl, dt / dt_cfl));
148
149 const size_t num_outputs = 4;
150 const size_t dt_out_n = std::max<size_t>(1, num_steps / num_outputs);
151 const size_t test_dt_out_n = std::max<size_t>(1, dt_out_n / 10);
152
153 for (auto &g : pGeomVec)
155
156 const util::Point cfix = pGeomVec[1].d_geom_p->center();
157 const util::Point cmov = pGeomVec[2].d_geom_p->center();
158 const size_t n_pack = packed.size();
159 const size_t n_wall_fixed = n_pack;
160 const size_t n_wall_moving = n_pack + 1;
161 const size_t n_total = n_pack + 2;
162
163 auto modelDeckJson = inp::ModelDeck::getExampleJson(
164 2, final_time, num_steps, "finite_difference", "central_difference", true, 2,
165 "Multi_Particle", 0);
166 modelDeckJson["MPI_Strategy"] = mpi_strategy;
167
168 std::vector<std::string> out_tags = {"Displacement", "Velocity", "Force", "Damage_Z",
169 "Damage", "Particle_ID", "Contact_Nodes"};
170 auto outputDeckJson = inp::OutputDeck::getExampleJson(
171 "vtu", output_path_for_deck, out_tags, dt_out_n, 1, true, "zlib", true,
172 test_dt_out_n, "0", true);
173
174 auto bcDeckJson =
175 inp::BCDeck::getExampleJson(0, 2, 0, true, util::Point(0, -10, 0));
176 bcDeckJson["Displacement_BC"]["Set_1"] = inp::BCBaseDeck::getExampleJson(
177 "Displacement_BC", false, geom::GeomData(), {n_wall_fixed}, {}, "", {}, "", {},
178 {1, 2}, true, "", {});
179 json set2;
180 set2["Particle_List"] = std::vector<size_t>{n_wall_moving};
181 set2["Direction"] = std::vector<size_t>{2};
182 set2["Time_Function"] = json{{"Type", "linear"}, {"Parameters", std::vector<double>{wall_vy}}};
183 set2["Spatial_Function"] = json{{"Type", "constant"}};
184 bcDeckJson["Displacement_BC"]["Set_2"] = set2;
185
186 json pDeckJson = json::object();
187 pDeckJson["Particle"] = inp::ParticleDeck::getParticleGeomExampleJson(pGeomVec);
188
189 auto meshEntry = [&](const std::filesystem::path &f) -> json {
190 if (file_mesh)
191 return json{{"File", f.string()}};
192 return json{{"File", f.string()},
193 {"CreateMesh",
194 json{{"Flag", true},
195 {"Info", "gmsh_builtin_mesh"},
196 {"Mesh_Size", mesh_size},
197 {"Write_Mesh_File", write_meshes}}}};
198 };
199 pDeckJson["Mesh"] = json{{"Sets", 3},
200 {"Set_1", meshEntry(mesh_cir)},
201 {"Set_2", meshEntry(mesh_fixed)},
202 {"Set_3", meshEntry(mesh_moving)}};
203
204 json matRoot = json{{"Sets", 2}};
205 matRoot["Set_1"] =
206 inp::MaterialDeck::getExampleJson("PDState", false, horizon, 0, rho, K, G, Gc, true, 1);
207 matRoot["Set_2"] =
208 inp::MaterialDeck::getExampleJson("PDState", false, horizon, 0, rho, K, G, Gc, true, 1);
209 pDeckJson["Material"] = matRoot;
210
212 0.95, true, true, false, Kn, 0.95, 0.0, 1.0, 100.0, 1.0, 0.0, K);
214 contactRoot["Set_1_1"] = contact_base;
215 contactRoot["Set_1_2"] = contact_base;
216 contactRoot["Set_2_2"] = contact_base;
217 pDeckJson["Contact"] = contactRoot;
218 pDeckJson["Neighbor"] =
219 inp::PNeighborDeck::getExampleJson("simple_all", 5.0, search_interval, 0.5);
220
221 auto pGenJson = inp::PGenDeck::getExampleJson("From_File");
222 pGenJson["Random_Rotation"] = false;
223 pGenJson["Data"]["N"] = n_total;
224 for (size_t pi = 0; pi < n_pack; ++pi) {
225 const auto &p = packed[pi];
226 pGenJson["Data"][std::to_string(pi)] = json{{"x", p.x},
227 {"y", p.y},
228 {"z", 0.0},
229 {"theta", 0.0},
230 {"s", 1.0},
231 {"geom_id", size_t(0)},
232 {"mat_id", size_t(0)},
233 {"contact_id", size_t(0)}};
234 }
235 pGenJson["Data"][std::to_string(n_pack)] = json{{"x", cfix.d_x},
236 {"y", cfix.d_y},
237 {"z", 0.0},
238 {"theta", 0.0},
239 {"s", 1.0},
240 {"geom_id", size_t(1)},
241 {"mat_id", size_t(1)},
242 {"contact_id", size_t(1)},
243 {"is_wall", true}};
244 pGenJson["Data"][std::to_string(n_pack + 1)] = json{{"x", cmov.d_x},
245 {"y", cmov.d_y},
246 {"z", 0.0},
247 {"theta", 0.0},
248 {"s", 1.0},
249 {"geom_id", size_t(2)},
250 {"mat_id", size_t(1)},
251 {"contact_id", size_t(1)},
252 {"is_wall", true}};
253 pDeckJson["Particle_Generation"] = pGenJson;
254
255 util::io::print(std::format(
256 "jha2021_comp: Lin={:.6f}, Win={:.6f}, horizon={:.6f}, plate v_y={}, Wall_Id={}\n",
257 Lin, Win, horizon, wall_vy, n_wall_moving));
258
259 return json{{"Comment", "jha2021_comp_contact"},
260 {"Model", modelDeckJson},
261 {"Output", outputDeckJson},
262 {"Force_BC", bcDeckJson["Force_BC"]},
263 {"Displacement_BC", bcDeckJson["Displacement_BC"]},
264 {"Particle", pDeckJson["Particle"]},
265 {"Mesh", pDeckJson["Mesh"]},
266 {"Material", pDeckJson["Material"]},
267 {"Contact", pDeckJson["Contact"]},
268 {"Neighbor", pDeckJson["Neighbor"]},
269 {"Particle_Generation", pDeckJson["Particle_Generation"]},
270 {"Test", json{{"Test_Name", "compressive_test"},
271 {"Compressive_Test",
272 json{{"Wall_Id", n_wall_moving},
273 {"Wall_Force_Direction", 2}}}}}};
274}
275
276void writeLocations(const std::filesystem::path &csv,
277 const std::vector<PackedParticle> &packed) {
278 std::ofstream os(csv);
279 os << "i, x, y, z, r, o\n";
280 for (const auto &p : packed)
281 os << std::format("0, {:.6f}, {:.6f}, {:.6f}, {:.6f}, 0.000000\n", p.x, p.y, p.z, p.r);
282}
283
284double maxAbsForceFromCsv(const std::filesystem::path &csv) {
285 std::ifstream is(csv);
286 if (!is)
287 return 0.;
288 std::string line;
289 std::getline(is, line);
290 double max_abs = 0.;
291 while (std::getline(is, line)) {
292 if (line.empty())
293 continue;
294 const auto c2 = line.rfind(',');
295 if (c2 == std::string::npos)
296 continue;
297 max_abs = std::max(max_abs, std::abs(std::stod(line.substr(c2 + 1))));
298 }
299 return max_abs;
300}
301
303public:
304 GrainContactProbe(std::filesystem::path out_dir = {}, size_t interval = 0)
305 : d_outDir(std::move(out_dir)), d_interval(interval) {
306 if (!d_outDir.empty() && util::parallel::mpiRank() == 0) {
307 std::filesystem::create_directories(d_outDir / "nodal");
308 d_metricOs.open(d_outDir / "mpi_metric_ts.csv");
309 d_metricOs << "step,t,max_u,com0x,com0y\n";
310 }
311 }
312
315 sampleMetric(data);
316 if (data.d_particlesListTypeAll.size() < 3)
317 return;
318 double Rc = 0.;
319 try {
320 Rc = data.d_particleDeck_p->d_contactDeck.getContact(0, 0).d_contactR;
321 } catch (...) {
322 return;
323 }
324 if (Rc <= 0.)
325 return;
326 d_Rc = Rc;
327 double min_gap = 1.0e9;
328 int n_in = 0;
329 const auto &all = data.d_particlesListTypeAll;
330 for (size_t i = 0; i < all.size(); ++i) {
331 if (all[i]->getGroupId("contact_id") != 0)
332 continue;
333 for (size_t j = i + 1; j < all.size(); ++j) {
334 if (all[j]->getGroupId("contact_id") != 0)
335 continue;
336 const double gap = all[i]->getXCenter().dist(all[j]->getXCenter()) -
337 all[i]->d_geom_p->boundingRadius() -
338 all[j]->d_geom_p->boundingRadius();
339 min_gap = std::min(min_gap, gap);
340 if (gap < Rc)
341 ++n_in;
342 }
343 }
344 if (d_gap0 == 0. && min_gap < 1.0e8)
345 d_gap0 = min_gap;
346 if (min_gap < d_minGap) {
347 d_minGap = min_gap;
348 d_tMin = data.d_time;
349 }
350 if (n_in > d_maxPairs) {
351 d_maxPairs = n_in;
352 util::io::print(std::format(
353 "grain contact: {} pair(s) in Rc at t={:.6e}, min_gap={:.6e}, Rc={:.6e}\n",
354 n_in, data.d_time, min_gap, Rc));
355 }
356 }
357
358 double minGap() const { return d_minGap; }
359 double gap0() const { return d_gap0; }
360 double Rc() const { return d_Rc; }
361 double tMin() const { return d_tMin; }
362 int maxPairs() const { return d_maxPairs; }
363
364private:
365 static bool ownsNode(const data::ModelData &data, size_t i) {
366 const int rank = util::parallel::mpiRank();
367 if (data.d_pdDofMpi) {
368 if (i >= data.d_pdNodePartition.size())
369 return false;
370 return static_cast<int>(data.d_pdNodePartition[i]) == rank;
371 }
372 for (const auto *p : data.d_particlesListTypeAll) {
373 const size_t i0 = p->d_globStart;
374 const size_t i1 = i0 + p->getNumNodes();
375 if (i < i0 || i >= i1)
376 continue;
377 if (p->isWall())
378 return rank == 0;
379 return particle::isLocallyOwned(*p);
380 }
381 return rank == 0;
382 }
383
385 if (d_outDir.empty() || d_interval == 0)
386 return;
387 const size_t nstep = data.currentStep();
388 if (nstep % d_interval != 0 && nstep < data.numTimeSteps())
389 return;
390 double max_u = 0.;
391 for (const auto &u : data.d_u)
392 max_u = std::max(max_u, u.length());
393 if (util::parallel::mpiSize() > 1)
394 MPI_Allreduce(MPI_IN_PLACE, &max_u, 1, MPI_DOUBLE, MPI_MAX,
396 double com0x = 0., com0y = 0.;
397 if (!data.d_particlesListTypeParticle.empty()) {
398 const auto c = data.d_particlesListTypeParticle[0]->getXCenter();
399 com0x = c.d_x;
400 com0y = c.d_y;
401 }
402 if (util::parallel::mpiRank() == 0) {
403 d_metricOs << std::format("{},{:.12e},{:.12e},{:.12e},{:.12e}\n", nstep,
404 data.d_time, max_u, com0x, com0y);
405 d_metricOs.flush();
406 }
407 const size_t n = data.d_u.size();
408 std::vector<double> buf(6 * n, 0.);
409 for (size_t i = 0; i < n; ++i) {
410 if (!ownsNode(data, i))
411 continue;
412 buf[6 * i + 0] = data.d_u[i].d_x;
413 buf[6 * i + 1] = data.d_u[i].d_y;
414 buf[6 * i + 2] = data.d_u[i].d_z;
415 buf[6 * i + 3] = data.d_v[i].d_x;
416 buf[6 * i + 4] = data.d_v[i].d_y;
417 buf[6 * i + 5] = data.d_v[i].d_z;
418 }
419 if (util::parallel::mpiSize() > 1)
420 MPI_Allreduce(MPI_IN_PLACE, buf.data(), static_cast<int>(buf.size()),
421 MPI_DOUBLE, MPI_SUM, util::parallel::mpiComm());
422 if (util::parallel::mpiRank() != 0)
423 return;
424 if (!d_wroteXref) {
425 std::ofstream xr(d_outDir / "nodal" / "x_ref.bin", std::ios::binary);
426 const uint32_t nn = static_cast<uint32_t>(n);
427 xr.write(reinterpret_cast<const char *>(&nn), sizeof(nn));
428 for (size_t i = 0; i < n; ++i) {
429 const double p[3] = {data.d_xRef[i].d_x, data.d_xRef[i].d_y,
430 data.d_xRef[i].d_z};
431 xr.write(reinterpret_cast<const char *>(p), sizeof(p));
432 }
433 d_wroteXref = true;
434 }
435 std::ofstream os(d_outDir / "nodal" / std::format("uv_{:06d}.bin", nstep),
436 std::ios::binary);
437 const char magic[4] = {'P', 'D', 'U', 'V'};
438 const uint32_t step32 = static_cast<uint32_t>(nstep);
439 const uint32_t nn = static_cast<uint32_t>(n);
440 os.write(magic, 4);
441 os.write(reinterpret_cast<const char *>(&step32), sizeof(step32));
442 os.write(reinterpret_cast<const char *>(&nn), sizeof(nn));
443 os.write(reinterpret_cast<const char *>(buf.data()),
444 static_cast<std::streamsize>(buf.size() * sizeof(double)));
445 }
446
447 double d_minGap = 1.0e9;
448 double d_gap0 = 0.;
449 double d_Rc = 0.;
450 double d_tMin = 0.;
451 int d_maxPairs = 0;
452 std::filesystem::path d_outDir;
453 size_t d_interval = 0;
454 std::ofstream d_metricOs;
455 bool d_wroteXref = false;
456};
457
458} // namespace
459
460int main(int argc, char *argv[]) {
461
462 util::parallel::initMpi(argc, argv);
463 util::io::InputParser input(argc, argv);
464
465 unsigned int nThreads;
466 if (input.cmdOptionExists("-nThreads"))
467 nThreads = std::stoi(input.getCmdOption("-nThreads"));
468 else
469 nThreads = std::thread::hardware_concurrency();
471 util::io::print(std::format("Number of threads = {}\n", util::parallel::getNThreads()));
472
473 // Close ~1.08 Rc at vy=-0.06, then squeeze. 0.004 s × 0.06 m/s = 0.24 mm.
474 double final_time = 0.004;
475 size_t num_steps = 20000;
476 if (input.cmdOptionExists("-finalTime"))
477 final_time = std::stod(input.getCmdOption("-finalTime"));
478 if (input.cmdOptionExists("-numSteps"))
479 num_steps = std::stoul(input.getCmdOption("-numSteps"));
480 size_t search_interval = 40;
481 if (input.cmdOptionExists("-searchInterval"))
482 search_interval = std::stoul(input.getCmdOption("-searchInterval"));
483 int ncols = 4;
484 int nrows = 3;
485 if (input.cmdOptionExists("-nCols"))
486 ncols = std::stoi(input.getCmdOption("-nCols"));
487 if (input.cmdOptionExists("-nRows"))
488 nrows = std::stoi(input.getCmdOption("-nRows"));
489 std::string mpi_strategy = "auto";
490 if (input.cmdOptionExists("-mpiStrategy"))
491 mpi_strategy = input.getCmdOption("-mpiStrategy");
492 util::io::print(std::format("MPI_Strategy = {}, pack = {}x{} ({} grains)\n",
493 mpi_strategy, ncols, nrows, ncols * nrows));
494
495 namespace fs = std::filesystem;
496 const fs::path cwd = fs::current_path();
497 fs::path out_dir = cwd / "out";
498 fs::path inp_dir = cwd / "inp";
499 if (input.cmdOptionExists("-outputDir")) {
500 fs::path p = input.getCmdOption("-outputDir");
501 out_dir = p.is_absolute() ? std::move(p) : cwd / p;
502 }
503 if (input.cmdOptionExists("-inputDir")) {
504 fs::path p = input.getCmdOption("-inputDir");
505 inp_dir = p.is_absolute() ? std::move(p) : cwd / p;
506 } else if (input.cmdOptionExists("-outputDir"))
507 inp_dir = out_dir.parent_path() / "inp";
508
509 fs::create_directories(out_dir);
510 fs::create_directories(inp_dir);
511
512 const fs::path mesh_cir = inp_dir / "mesh_cir.msh";
513 const fs::path mesh_fixed = inp_dir / "mesh_fixed_container.msh";
514 const fs::path mesh_moving = inp_dir / "mesh_moving_container.msh";
515 const bool file_mesh = input.cmdOptionExists("-fileMesh");
516 const bool inbuilt = !file_mesh;
517 const bool write_meshes = input.cmdOptionExists("-writeMeshes") || inbuilt;
518 if (file_mesh) {
519#ifndef JHA2021_COMP_MESH_DIR
520 throw std::runtime_error("JHA2021_COMP_MESH_DIR is not set.");
521#else
522 const fs::path src(JHA2021_COMP_MESH_DIR);
523 for (const char *name :
524 {"mesh_cir.msh", "mesh_fixed_container.msh", "mesh_moving_container.msh"}) {
525 const fs::path from = src / name;
526 if (!fs::exists(from))
527 throw std::runtime_error("Missing frozen mesh " + from.string());
528 fs::copy_file(from, inp_dir / name, fs::copy_options::overwrite_existing);
529 }
530 util::io::print(std::format("Using frozen meshes from {}\n", src.string()));
531#endif
532 }
533
534 const std::string output_path_for_deck = directoryPathWithTrailingSep(out_dir);
535 auto inputJson = buildInputJson(output_path_for_deck, mesh_cir, mesh_fixed, mesh_moving,
536 final_time, num_steps, file_mesh, write_meshes,
537 search_interval, ncols, nrows, mpi_strategy);
538
539 {
540 std::ofstream os(inp_dir / "input.json");
541 os << inputJson.dump(2);
542 }
543
544 {
545 const double R = 0.001;
546 const double mesh_size = R / 5.0;
547 const double pad = 1.15 * 0.95 * 0.7 * mesh_size;
548 writeLocations(inp_dir / "particle_locations.csv",
549 generateCircularGrid(ncols, nrows, 0.0, 0.0, R, pad));
550 }
551
552 auto deck = std::make_shared<inp::Input>(inputJson);
553 PeriDEMModel dem(deck);
554 const size_t ts_every = std::max<size_t>(1, num_steps / 100);
555 auto probe = std::make_unique<GrainContactProbe>(out_dir, ts_every);
556 GrainContactProbe *probe_p = probe.get();
557 dem.setPostprocess(std::move(probe));
558 dem.run(deck);
559
560 // Under particle-MPI, ranks only refresh ghost centers; far grains stay stale.
561 // Reduce contact probe metrics so requireContact is rank-consistent.
562 int max_pairs = probe_p->maxPairs();
563 double min_gap = probe_p->minGap();
564 double gap0 = probe_p->gap0();
565 if (util::parallel::mpiSize() > 1) {
566 int max_pairs_g = 0;
567 double min_gap_g = 0., gap0_g = 0.;
568 MPI_Allreduce(&max_pairs, &max_pairs_g, 1, MPI_INT, MPI_MAX,
570 MPI_Allreduce(&min_gap, &min_gap_g, 1, MPI_DOUBLE, MPI_MIN,
572 MPI_Allreduce(&gap0, &gap0_g, 1, MPI_DOUBLE, MPI_MAX,
574 max_pairs = max_pairs_g;
575 min_gap = min_gap_g;
576 gap0 = gap0_g;
577 }
578
579 const float zmax =
580 dem.d_Z.empty() ? 0.f : *std::max_element(dem.d_Z.begin(), dem.d_Z.end());
581 util::io::print(std::format(
582 "grain contact: gap0={}, min_gap={} (t={}), Rc={}, pairs_in_Rc={}, max Damage_Z={}\n",
583 gap0, min_gap, probe_p->tMin(), probe_p->Rc(), max_pairs, zmax));
584
585 const bool require_contact = !input.cmdOptionExists("-noRequireContact");
586 if (require_contact) {
587 const bool grains_touched = max_pairs > 0 && min_gap < gap0 - 1.0e-8;
588 if (!grains_touched) {
589 util::io::print("requireContact: no grain–grain pair entered the contact radius.\n");
591 return EXIT_FAILURE;
592 }
593 if (!(gap0 > probe_p->Rc())) {
594 util::io::print("requireContact: grains already in Rc at t=0; packing is too tight.\n");
596 return EXIT_FAILURE;
597 }
598 }
599
600 if (input.cmdOptionExists("-assertForce")) {
601 // Rank 0 owns the reaction CSV; broadcast pass/fail.
602 int force_ok = 1;
603 if (util::parallel::mpiRank() == 0) {
604 const fs::path csv = out_dir / "pp_compressive_test_0.csv";
605 const double fmax = maxAbsForceFromCsv(csv);
606 util::io::print(std::format("assertForce: max |plate reaction| = {} from {}\n", fmax,
607 csv.string()));
608 if (fmax <= 0.) {
609 util::io::print("assertForce: plate reaction is zero.\n");
610 force_ok = 0;
611 }
612 }
613 if (util::parallel::mpiSize() > 1)
614 MPI_Bcast(&force_ok, 1, MPI_INT, 0, util::parallel::mpiComm());
615 if (!force_ok) {
617 return EXIT_FAILURE;
618 }
619 }
620
621 // Metric for serial vs MPI checks (rank 0). Allreduce max|u| for DOF-MPI.
622 {
623 double max_u = 0.;
624 for (const auto &u : dem.d_u)
625 max_u = std::max(max_u, u.length());
626 double com0x = 0., com0y = 0.;
627 if (!dem.d_particlesListTypeParticle.empty()) {
628 const auto c = dem.d_particlesListTypeParticle[0]->getXCenter();
629 com0x = c.d_x;
630 com0y = c.d_y;
631 }
632 if (util::parallel::mpiSize() > 1)
633 MPI_Allreduce(MPI_IN_PLACE, &max_u, 1, MPI_DOUBLE, MPI_MAX,
635 if (util::parallel::mpiRank() == 0) {
636 std::ofstream os(out_dir / "mpi_metric.txt");
637 os << std::format("{:.12e} {:.12e} {:.12e}\n", max_u, com0x, com0y);
638 util::io::print(std::format("mpi_metric: max|u|={:.12e} grain0_com=({:.12e},{:.12e})\n",
639 max_u, com0x, com0y));
640 }
641 }
642
644 return EXIT_SUCCESS;
645}
void setPostprocess(std::unique_ptr< postprocess::Postprocess > p)
void run(std::shared_ptr< inp::Input > &deck)
static bool ownsNode(const data::ModelData &data, size_t i)
Definition main.cpp:365
GrainContactProbe(std::filesystem::path out_dir={}, size_t interval=0)
Definition main.cpp:304
void sampleMetric(data::ModelData &data)
Definition main.cpp:384
void checkStop(data::ModelData &data) override
Definition main.cpp:313
A class to store model data.
Definition modelData.h:50
std::vector< float > d_Z
Damage at nodes.
Definition modelData.h:818
std::vector< util::Point > d_u
Displacement of the nodes.
Definition modelData.h:748
std::vector< particle::BaseParticle * > d_particlesListTypeParticle
List of particles.
Definition modelData.h:707
Extra postprocessing and stop criteria (VTU writing is in rw::).
Definition postprocess.h:24
virtual void checkStop(data::ModelData &data)
Input command line argument parser.
Definition inputParser.h:28
bool cmdOptionExists(const std::string &option) const
Check if argument exists.
Definition inputParser.h:60
const std::string & getCmdOption(const std::string &option) const
Get value of argument specified by key.
Definition inputParser.h:45
nlohmann::ordered_json json
std::string directoryPathWithTrailingSep(const std::filesystem::path &dir)
Definition main.cpp:34
json buildInputJson(const std::string &output_path_for_deck, const std::filesystem::path &mesh_cir, const std::filesystem::path &mesh_fixed, const std::filesystem::path &mesh_moving, double final_time, size_t num_steps, bool file_mesh, bool write_meshes, size_t search_interval, int ncols, int nrows, const std::string &mpi_strategy="auto")
Definition main.cpp:83
std::vector< PackedParticle > generateCircularGrid(int ncols, int nrows, double x_lo, double y_lo, double R, double gap)
Definition main.cpp:61
double maxAbsForceFromCsv(const std::filesystem::path &csv)
Definition main.cpp:284
bool ownsNode(const data::ModelData &data, size_t i)
Definition main.cpp:55
void writeLocations(const std::filesystem::path &csv, const std::vector< PackedParticle > &packed)
Definition main.cpp:276
Definition contact.h:20
void createGeomObject(const std::string &geom_type, const std::vector< double > &params, const std::vector< std::string > &vec_type, const std::vector< std::string > &vec_flag, std::shared_ptr< geom::GeomObject > &obj, bool perform_check)
double toGE(double E, double nu)
Compute shear modulus from Young's modulus E and Poisson's ratio nu.
double toE(double K, double nu)
Compute Young's modulus E from Bulk modulus K and Poisson's ratio nu.
bool isLocallyOwned(const BaseParticle &p)
True if this rank updates / assembles forces for the particle. Walls are replicated on every rank....
void print(const T &msg, int nt=print_default_tab, int printMpiRank=print_default_mpi_rank)
Prints formatted information.
Definition io.h:128
unsigned int getNThreads()
Get number of threads to be used by taskflow.
void initNThreads(unsigned int nThreads=std::thread::hardware_concurrency())
Initializes MpiStatus struct.
void initMpi(int argc=0, char *argv[]=nullptr)
Initializes MPI and also creates MpiStatus struct.
int mpiSize()
Get size (number) of processors.
int mpiRank()
get rank (id) of this processor
void finalizeMpi()
Call MPI_Finalize if this process initialized MPI.
MPI_Comm mpiComm()
Get MPI comm.
double harmonicMean(const double &m1, const double &m2)
Definition function.cpp:131
Input data for geometrical objects.
static json getExampleJson(std::string type="Foce_BC", bool isRegionActive=false, geom::GeomData regionGeomData=geom::GeomData(), std::vector< size_t > pList=std::vector< size_t >(), std::vector< size_t > pNotList=std::vector< size_t >(), std::string timeFnType="", std::vector< double > timeFnParams=std::vector< double >(), std::string spatialFnType="", std::vector< double > spatialFnParams=std::vector< double >(), std::vector< size_t > direction=std::vector< size_t >(), bool isDisplacementZero=false, std::string icType="", std::vector< double > icVec=std::vector< double >())
Returns example JSON object for ModelDeck configuration.
Definition bcBaseDeck.h:157
static json getExampleJson(size_t nForceSets=0, size_t nDispSets=0, size_t nICSets=0, bool gravityActive=false, util::Point gravity=util::Point())
Returns example JSON object for ModelDeck configuration.
Definition bcDeck.h:69
static json getExampleJson(double contactR=0., bool computeContactR=true, bool dampingOn=true, bool frictionOn=true, double Kn=0., double eps=1., double mu=0., double KnFactor=1., double betanFactor=1., double deltaMax=1., double vMax=0., double K=0.)
Returns example JSON object for ModelDeck configuration.
static json getExampleJson(std::string materialType="PDState", bool isPlainStrain=false, double horizon=-1., double horizonMeshRatio=-1., double density=1., double K=0., double G=0., double Gc=0., bool computeParamsFromElastic=true, size_t influenceFnType=0, double E=-1.)
Returns example JSON object for ModelDeck configuration.
static json getExampleJson(size_t dim=2, double tFinal=1.0, size_t Nt=10, std::string spatialDiscretization="finite_difference", std::string timeDiscretization="central_difference", bool populateElementNodeConnectivity=true, size_t quadOrder=2, std::string particleSimType="Multi_Particle", int seed=0)
Returns example JSON object for ModelDeck configuration.
Definition modelDeck.h:164
static json getExampleJson(std::string outFormat="vtu", std::string path="./", std::vector< std::string > outTags={"Displacement"}, size_t outputInterval=1, size_t debug=2, bool performFEOut=true, std::string compressType="zlib", bool performOut=true, size_t dtTestOut=1, std::string tagPPFile="", bool pvdCollection=false)
Returns example JSON object for ModelDeck configuration.
Definition outputDeck.h:144
static json getExampleJson(std::string genMethod="From_File")
Returns example JSON object for ModelDeck configuration.
Definition pGenDeck.h:65
static json getExampleJson(std::string updateCriteria="simple_all", double sFactor=1., size_t neighUpdateInterval=1, double nearBdNodesTol=0.5)
Returns example JSON object for ModelDeck configuration.
static json getParticleContactExampleJson(size_t nSets=0)
Returns example JSON object for ModelDeck configuration.
static json getParticleGeomExampleJson(std::vector< geom::GeomData > pGeomVec=std::vector< geom::GeomData >())
Returns example JSON object for ModelDeck configuration.
A structure to represent 3d vectors.
Definition point.h:30
double d_y
the y coordinate
Definition point.h:36
double d_x
the x coordinate
Definition point.h:33
int main()
Definition main.cpp:31