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 slightly 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 slightly outside Rc so contact
100 // is not present at t=0; plate and gravity pull 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 = util::normalContactStiffness(K, K, horizon);
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 json{{"Dimension", 2},
165 {"Final_Time", final_time},
166 {"Time_Steps", num_steps},
167 {"Discretization_Type", json{{"Spatial", "finite_difference"}, {"Time", "central_difference"}}},
168 {"Populate_ElementNodeConnectivity", true},
169 {"Quad_Approximation_Order", 2},
170 {"Particle_Sim_Type", "Multi_Particle"},
171 {"Seed", 0}});
172 modelDeckJson["MPI_Strategy"] = mpi_strategy;
173
174 std::vector<std::string> out_tags = {"Displacement", "Velocity", "Force", "Damage_Z",
175 "Damage", "Particle_ID", "Contact_Nodes"};
176 auto outputDeckJson = inp::OutputDeck::getExampleJson(
177 json{{"File_Format", "vtu"},
178 {"Path", output_path_for_deck},
179 {"Tags", out_tags},
180 {"Output_Interval", dt_out_n},
181 {"Debug", 1},
182 {"Perform_FE_Out", true},
183 {"Compress_Type", "zlib"},
184 {"Perform_Out", true},
185 {"Test_Output_Interval", test_dt_out_n},
186 {"Tag_PP", "0"},
187 {"PVD_Collection", true}});
188
189 auto bcDeckJson =
191 json{{"Displacement_BC_Sets", 2},
192 {"Gravity", util::Point(0, -10, 0).toVec()}});
193 bcDeckJson["Displacement_BC"]["Set_1"] = inp::BCBaseDeck::getExampleJson(
194 json{{"Type", "Displacement_BC"},
195 {"Particle_List", {n_wall_fixed}},
196 {"Direction", {1, 2}},
197 {"Zero_Displacement", true}});
198 json set2;
199 set2["Particle_List"] = std::vector<size_t>{n_wall_moving};
200 set2["Direction"] = std::vector<size_t>{2};
201 set2["Time_Function"] = json{{"Type", "linear"}, {"Parameters", std::vector<double>{wall_vy}}};
202 set2["Spatial_Function"] = json{{"Type", "constant"}};
203 bcDeckJson["Displacement_BC"]["Set_2"] = set2;
204
205 json pDeckJson = json::object();
206 pDeckJson["Particle"] = inp::ParticleDeck::getParticleGeomExampleJson(pGeomVec);
207
208 auto meshEntry = [&](const std::filesystem::path &f) -> json {
209 if (file_mesh)
210 return json{{"File", f.string()}};
211 return json{{"File", f.string()},
212 {"CreateMesh",
213 json{{"Flag", true},
214 {"Info", "gmsh_builtin_mesh"},
215 {"Mesh_Size", mesh_size},
216 {"Write_Mesh_File", write_meshes}}}};
217 };
218 pDeckJson["Mesh"] = json{{"Sets", 3},
219 {"Set_1", meshEntry(mesh_cir)},
220 {"Set_2", meshEntry(mesh_fixed)},
221 {"Set_3", meshEntry(mesh_moving)}};
222
223 json matRoot = json{{"Sets", 2}};
224 matRoot["Set_1"] =
226 json{{"Type", "PDState"},
227 {"Is_Plane_Strain", false},
228 {"Horizon", horizon},
229 {"Density", rho},
230 {"K", K},
231 {"G", G},
232 {"Gc", Gc},
233 {"Compute_From_Classical", true},
234 {"Influence_Function", json{{"Type", 0}}}});
235 matRoot["Set_2"] =
237 json{{"Type", "PDState"},
238 {"Is_Plane_Strain", false},
239 {"Horizon", horizon},
240 {"Density", rho},
241 {"K", K},
242 {"G", G},
243 {"Gc", Gc},
244 {"Compute_From_Classical", true},
245 {"Influence_Function", json{{"Type", 0}}}});
246 pDeckJson["Material"] = matRoot;
247
249 json{{"Contact_Radius_Factor", 0.95},
250 {"Kn", Kn},
251 {"Damping_On", true},
252 {"Epsilon", 0.95},
253 {"Friction_On", false},
254 {"Friction_Coeff", 0.0},
255 {"Kn_Factor", 1.0},
256 {"Beta_n_Factor", 100.0},
257 {"K", K}});
259 contactRoot["Set_1_1"] = contact_base;
260 contactRoot["Set_1_2"] = contact_base;
261 contactRoot["Set_2_2"] = contact_base;
262 pDeckJson["Contact"] = contactRoot;
263 pDeckJson["Neighbor"] =
265 json{{"Update_Criteria", "simple_all"},
266 {"Search_Factor", 5.0},
267 {"Search_Interval", search_interval},
268 {"Near_Bd_Nodes_Tol", 0.5}});
269
270 auto pGenJson = inp::PGenDeck::getExampleJson(json{{"Method", "From_File"}});
271 pGenJson["Random_Rotation"] = false;
272 pGenJson["Data"]["N"] = n_total;
273 for (size_t pi = 0; pi < n_pack; ++pi) {
274 const auto &p = packed[pi];
275 pGenJson["Data"][std::to_string(pi)] = json{{"x", p.x},
276 {"y", p.y},
277 {"z", 0.0},
278 {"theta", 0.0},
279 {"s", 1.0},
280 {"geom_id", size_t(0)},
281 {"mat_id", size_t(0)},
282 {"contact_id", size_t(0)}};
283 }
284 pGenJson["Data"][std::to_string(n_pack)] = json{{"x", cfix.d_x},
285 {"y", cfix.d_y},
286 {"z", 0.0},
287 {"theta", 0.0},
288 {"s", 1.0},
289 {"geom_id", size_t(1)},
290 {"mat_id", size_t(1)},
291 {"contact_id", size_t(1)},
292 {"is_wall", true}};
293 pGenJson["Data"][std::to_string(n_pack + 1)] = json{{"x", cmov.d_x},
294 {"y", cmov.d_y},
295 {"z", 0.0},
296 {"theta", 0.0},
297 {"s", 1.0},
298 {"geom_id", size_t(2)},
299 {"mat_id", size_t(1)},
300 {"contact_id", size_t(1)},
301 {"is_wall", true}};
302 pDeckJson["Particle_Generation"] = pGenJson;
303
304 util::io::print(std::format(
305 "jha2021_comp: Lin={:.6f}, Win={:.6f}, horizon={:.6f}, plate v_y={}, Wall_Id={}\n",
306 Lin, Win, horizon, wall_vy, n_wall_moving));
307
308 return json{{"Comment", "jha2021_comp_contact"},
309 {"Model", modelDeckJson},
310 {"Output", outputDeckJson},
311 {"Force_BC", bcDeckJson["Force_BC"]},
312 {"Displacement_BC", bcDeckJson["Displacement_BC"]},
313 {"Particle", pDeckJson["Particle"]},
314 {"Mesh", pDeckJson["Mesh"]},
315 {"Material", pDeckJson["Material"]},
316 {"Contact", pDeckJson["Contact"]},
317 {"Neighbor", pDeckJson["Neighbor"]},
318 {"Particle_Generation", pDeckJson["Particle_Generation"]},
319 {"Test", json{{"Test_Name", "compressive_test"},
320 {"Compressive_Test",
321 json{{"Wall_Id", n_wall_moving},
322 {"Wall_Force_Direction", 2}}}}}};
323}
324
325void writeLocations(const std::filesystem::path &csv,
326 const std::vector<PackedParticle> &packed) {
327 std::ofstream os(csv);
328 os << "i, x, y, z, r, o\n";
329 for (const auto &p : packed)
330 os << std::format("0, {:.6f}, {:.6f}, {:.6f}, {:.6f}, 0.000000\n", p.x, p.y, p.z, p.r);
331}
332
333double maxAbsForceFromCsv(const std::filesystem::path &csv) {
334 std::ifstream is(csv);
335 if (!is)
336 return 0.;
337 std::string line;
338 std::getline(is, line);
339 double max_abs = 0.;
340 while (std::getline(is, line)) {
341 if (line.empty())
342 continue;
343 const auto c2 = line.rfind(',');
344 if (c2 == std::string::npos)
345 continue;
346 max_abs = std::max(max_abs, std::abs(std::stod(line.substr(c2 + 1))));
347 }
348 return max_abs;
349}
350
352public:
353 GrainContactProbe(std::filesystem::path out_dir = {}, size_t interval = 0)
354 : d_outDir(std::move(out_dir)), d_interval(interval) {
355 if (!d_outDir.empty() && util::parallel::mpiRank() == 0) {
356 std::filesystem::create_directories(d_outDir / "nodal");
357 d_metricOs.open(d_outDir / "mpi_metric_ts.csv");
358 d_metricOs << "step,t,max_u,com0x,com0y\n";
359 }
360 }
361
364 sampleMetric(data);
365 if (data.d_particlesListTypeAll.size() < 3)
366 return;
367 double Rc = 0.;
368 try {
369 Rc = data.d_particleDeck_p->d_contactDeck.getContact(0, 0).d_contactR;
370 } catch (...) {
371 return;
372 }
373 if (Rc <= 0.)
374 return;
375 d_Rc = Rc;
376 double min_gap = 1.0e9;
377 int n_in = 0;
378 const auto &all = data.d_particlesListTypeAll;
379 for (size_t i = 0; i < all.size(); ++i) {
380 if (all[i]->getGroupId("contact_id") != 0)
381 continue;
382 for (size_t j = i + 1; j < all.size(); ++j) {
383 if (all[j]->getGroupId("contact_id") != 0)
384 continue;
385 const double gap = all[i]->getXCenter().dist(all[j]->getXCenter()) -
386 all[i]->d_geom_p->boundingRadius() -
387 all[j]->d_geom_p->boundingRadius();
388 min_gap = std::min(min_gap, gap);
389 if (gap < Rc)
390 ++n_in;
391 }
392 }
393 if (d_gap0 == 0. && min_gap < 1.0e8)
394 d_gap0 = min_gap;
395 if (min_gap < d_minGap) {
396 d_minGap = min_gap;
397 d_tMin = data.d_time;
398 }
399 if (n_in > d_maxPairs) {
400 d_maxPairs = n_in;
401 util::io::print(std::format(
402 "grain contact: {} pair(s) in Rc at t={:.6e}, min_gap={:.6e}, Rc={:.6e}\n",
403 n_in, data.d_time, min_gap, Rc));
404 }
405 }
406
407 double minGap() const { return d_minGap; }
408 double gap0() const { return d_gap0; }
409 double Rc() const { return d_Rc; }
410 double tMin() const { return d_tMin; }
411 int maxPairs() const { return d_maxPairs; }
412
413private:
414 static bool ownsNode(const data::ModelData &data, size_t i) {
415 const int rank = util::parallel::mpiRank();
416 if (data.d_pdDofMpi) {
417 if (i >= data.d_pdNodePartition.size())
418 return false;
419 return static_cast<int>(data.d_pdNodePartition[i]) == rank;
420 }
421 for (const auto *p : data.d_particlesListTypeAll) {
422 const size_t i0 = p->d_globStart;
423 const size_t i1 = i0 + p->getNumNodes();
424 if (i < i0 || i >= i1)
425 continue;
426 if (p->isWall())
427 return rank == 0;
428 return particle::isLocallyOwned(*p);
429 }
430 return rank == 0;
431 }
432
434 if (d_outDir.empty() || d_interval == 0)
435 return;
436 const size_t nstep = data.currentStep();
437 if (nstep % d_interval != 0 && nstep < data.numTimeSteps())
438 return;
439 double max_u = 0.;
440 for (const auto &u : data.d_u)
441 max_u = std::max(max_u, u.length());
442 if (util::parallel::mpiSize() > 1)
443 MPI_Allreduce(MPI_IN_PLACE, &max_u, 1, MPI_DOUBLE, MPI_MAX,
445 double com0x = 0., com0y = 0.;
446 if (!data.d_particlesListTypeParticle.empty()) {
447 const auto c = data.d_particlesListTypeParticle[0]->getXCenter();
448 com0x = c.d_x;
449 com0y = c.d_y;
450 }
451 if (util::parallel::mpiRank() == 0) {
452 d_metricOs << std::format("{},{:.12e},{:.12e},{:.12e},{:.12e}\n", nstep,
453 data.d_time, max_u, com0x, com0y);
454 d_metricOs.flush();
455 }
456 const size_t n = data.d_u.size();
457 std::vector<double> buf(6 * n, 0.);
458 for (size_t i = 0; i < n; ++i) {
459 if (!ownsNode(data, i))
460 continue;
461 buf[6 * i + 0] = data.d_u[i].d_x;
462 buf[6 * i + 1] = data.d_u[i].d_y;
463 buf[6 * i + 2] = data.d_u[i].d_z;
464 buf[6 * i + 3] = data.d_v[i].d_x;
465 buf[6 * i + 4] = data.d_v[i].d_y;
466 buf[6 * i + 5] = data.d_v[i].d_z;
467 }
468 if (util::parallel::mpiSize() > 1)
469 MPI_Allreduce(MPI_IN_PLACE, buf.data(), static_cast<int>(buf.size()),
470 MPI_DOUBLE, MPI_SUM, util::parallel::mpiComm());
471 if (util::parallel::mpiRank() != 0)
472 return;
473 if (!d_wroteXref) {
474 std::ofstream xr(d_outDir / "nodal" / "x_ref.bin", std::ios::binary);
475 const uint32_t nn = static_cast<uint32_t>(n);
476 xr.write(reinterpret_cast<const char *>(&nn), sizeof(nn));
477 for (size_t i = 0; i < n; ++i) {
478 const double p[3] = {data.d_xRef[i].d_x, data.d_xRef[i].d_y,
479 data.d_xRef[i].d_z};
480 xr.write(reinterpret_cast<const char *>(p), sizeof(p));
481 }
482 d_wroteXref = true;
483 }
484 std::ofstream os(d_outDir / "nodal" / std::format("uv_{:06d}.bin", nstep),
485 std::ios::binary);
486 const char magic[4] = {'P', 'D', 'U', 'V'};
487 const uint32_t step32 = static_cast<uint32_t>(nstep);
488 const uint32_t nn = static_cast<uint32_t>(n);
489 os.write(magic, 4);
490 os.write(reinterpret_cast<const char *>(&step32), sizeof(step32));
491 os.write(reinterpret_cast<const char *>(&nn), sizeof(nn));
492 os.write(reinterpret_cast<const char *>(buf.data()),
493 static_cast<std::streamsize>(buf.size() * sizeof(double)));
494 }
495
496 double d_minGap = 1.0e9;
497 double d_gap0 = 0.;
498 double d_Rc = 0.;
499 double d_tMin = 0.;
500 int d_maxPairs = 0;
501 std::filesystem::path d_outDir;
502 size_t d_interval = 0;
503 std::ofstream d_metricOs;
504 bool d_wroteXref = false;
505};
506
507} // namespace
508
509int main(int argc, char *argv[]) {
510
511 util::parallel::initMpi(argc, argv);
512 util::io::InputParser input(argc, argv);
513
514 unsigned int nThreads;
515 if (input.cmdOptionExists("-nThreads"))
516 nThreads = std::stoi(input.getCmdOption("-nThreads"));
517 else
518 nThreads = std::thread::hardware_concurrency();
520 util::io::print(std::format("Number of threads = {}\n", util::parallel::getNThreads()));
521
522 // Close ~1.08 Rc at vy=-0.06, then squeeze. 0.004 s × 0.06 m/s = 0.24 mm.
523 double final_time = 0.004;
524 size_t num_steps = 20000;
525 if (input.cmdOptionExists("-finalTime"))
526 final_time = std::stod(input.getCmdOption("-finalTime"));
527 if (input.cmdOptionExists("-numSteps"))
528 num_steps = std::stoul(input.getCmdOption("-numSteps"));
529 size_t search_interval = 40;
530 if (input.cmdOptionExists("-searchInterval"))
531 search_interval = std::stoul(input.getCmdOption("-searchInterval"));
532 int ncols = 4;
533 int nrows = 3;
534 if (input.cmdOptionExists("-nCols"))
535 ncols = std::stoi(input.getCmdOption("-nCols"));
536 if (input.cmdOptionExists("-nRows"))
537 nrows = std::stoi(input.getCmdOption("-nRows"));
538 std::string mpi_strategy = "auto";
539 if (input.cmdOptionExists("-mpiStrategy"))
540 mpi_strategy = input.getCmdOption("-mpiStrategy");
541 util::io::print(std::format("MPI_Strategy = {}, pack = {}x{} ({} grains)\n",
542 mpi_strategy, ncols, nrows, ncols * nrows));
543
544 namespace fs = std::filesystem;
545 const fs::path cwd = fs::current_path();
546 fs::path out_dir = cwd / "out";
547 fs::path inp_dir = cwd / "inp";
548 if (input.cmdOptionExists("-outputDir")) {
549 fs::path p = input.getCmdOption("-outputDir");
550 out_dir = p.is_absolute() ? std::move(p) : cwd / p;
551 }
552 if (input.cmdOptionExists("-inputDir")) {
553 fs::path p = input.getCmdOption("-inputDir");
554 inp_dir = p.is_absolute() ? std::move(p) : cwd / p;
555 } else if (input.cmdOptionExists("-outputDir"))
556 inp_dir = out_dir.parent_path() / "inp";
557
558 fs::create_directories(out_dir);
559 fs::create_directories(inp_dir);
560
561 const fs::path mesh_cir = inp_dir / "mesh_cir.msh";
562 const fs::path mesh_fixed = inp_dir / "mesh_fixed_container.msh";
563 const fs::path mesh_moving = inp_dir / "mesh_moving_container.msh";
564 const bool file_mesh = input.cmdOptionExists("-fileMesh");
565 const bool inbuilt = !file_mesh;
566 const bool write_meshes = input.cmdOptionExists("-writeMeshes") || inbuilt;
567 if (file_mesh) {
568#ifndef JHA2021_COMP_MESH_DIR
569 throw std::runtime_error("JHA2021_COMP_MESH_DIR is not set.");
570#else
571 const fs::path src(JHA2021_COMP_MESH_DIR);
572 for (const char *name :
573 {"mesh_cir.msh", "mesh_fixed_container.msh", "mesh_moving_container.msh"}) {
574 const fs::path from = src / name;
575 if (!fs::exists(from))
576 throw std::runtime_error("Missing frozen mesh " + from.string());
577 fs::copy_file(from, inp_dir / name, fs::copy_options::overwrite_existing);
578 }
579 util::io::print(std::format("Using frozen meshes from {}\n", src.string()));
580#endif
581 }
582
583 const std::string output_path_for_deck = directoryPathWithTrailingSep(out_dir);
584 auto inputJson = buildInputJson(output_path_for_deck, mesh_cir, mesh_fixed, mesh_moving,
585 final_time, num_steps, file_mesh, write_meshes,
586 search_interval, ncols, nrows, mpi_strategy);
587
588 {
589 std::ofstream os(inp_dir / "input.json");
590 os << inputJson.dump(2);
591 }
592
593 // -deckOnly stops after the deck is written. The Python comparison reads
594 // the deck and does not need this driver to run the simulation.
595 if (input.cmdOptionExists("-deckOnly")) {
596 util::io::print("deck written; -deckOnly, not running\n");
598 return 0;
599 }
600
601 {
602 const double R = 0.001;
603 const double mesh_size = R / 5.0;
604 const double pad = 1.15 * 0.95 * 0.7 * mesh_size;
605 writeLocations(inp_dir / "particle_locations.csv",
606 generateCircularGrid(ncols, nrows, 0.0, 0.0, R, pad));
607 }
608
609 auto deck = std::make_shared<inp::Input>(inputJson);
610 PeriDEMModel dem(deck);
611 const size_t ts_every = std::max<size_t>(1, num_steps / 100);
612 auto probe = std::make_unique<GrainContactProbe>(out_dir, ts_every);
613 GrainContactProbe *probe_p = probe.get();
614 dem.setPostprocess(std::move(probe));
615 dem.run(deck);
616
617 // Under particle-MPI, ranks only refresh ghost centers; far grains stay stale.
618 // Reduce contact probe metrics so requireContact is rank-consistent.
619 int max_pairs = probe_p->maxPairs();
620 double min_gap = probe_p->minGap();
621 double gap0 = probe_p->gap0();
622 if (util::parallel::mpiSize() > 1) {
623 int max_pairs_g = 0;
624 double min_gap_g = 0., gap0_g = 0.;
625 MPI_Allreduce(&max_pairs, &max_pairs_g, 1, MPI_INT, MPI_MAX,
627 MPI_Allreduce(&min_gap, &min_gap_g, 1, MPI_DOUBLE, MPI_MIN,
629 MPI_Allreduce(&gap0, &gap0_g, 1, MPI_DOUBLE, MPI_MAX,
631 max_pairs = max_pairs_g;
632 min_gap = min_gap_g;
633 gap0 = gap0_g;
634 }
635
636 const float zmax =
637 dem.d_Z.empty() ? 0.f : *std::max_element(dem.d_Z.begin(), dem.d_Z.end());
638 util::io::print(std::format(
639 "grain contact: gap0={}, min_gap={} (t={}), Rc={}, pairs_in_Rc={}, max Damage_Z={}\n",
640 gap0, min_gap, probe_p->tMin(), probe_p->Rc(), max_pairs, zmax));
641
642 const bool require_contact = !input.cmdOptionExists("-noRequireContact");
643 if (require_contact) {
644 const bool grains_touched = max_pairs > 0 && min_gap < gap0 - 1.0e-8;
645 if (!grains_touched) {
646 util::io::print("requireContact: no grain–grain pair entered the contact radius.\n");
648 return EXIT_FAILURE;
649 }
650 if (!(gap0 > probe_p->Rc())) {
651 util::io::print("requireContact: grains already in Rc at t=0; packing is too tight.\n");
653 return EXIT_FAILURE;
654 }
655 }
656
657 if (input.cmdOptionExists("-assertForce")) {
658 // Rank 0 owns the reaction CSV; broadcast pass/fail.
659 int force_ok = 1;
660 if (util::parallel::mpiRank() == 0) {
661 const fs::path csv = out_dir / "pp_compressive_test_0.csv";
662 const double fmax = maxAbsForceFromCsv(csv);
663 util::io::print(std::format("assertForce: max |plate reaction| = {} from {}\n", fmax,
664 csv.string()));
665 if (fmax <= 0.) {
666 util::io::print("assertForce: plate reaction is zero.\n");
667 force_ok = 0;
668 }
669 }
670 if (util::parallel::mpiSize() > 1)
671 MPI_Bcast(&force_ok, 1, MPI_INT, 0, util::parallel::mpiComm());
672 if (!force_ok) {
674 return EXIT_FAILURE;
675 }
676 }
677
678 // Metric for serial vs MPI checks (rank 0). Allreduce max|u| for DOF-MPI.
679 {
680 double max_u = 0.;
681 for (const auto &u : dem.d_u)
682 max_u = std::max(max_u, u.length());
683 double com0x = 0., com0y = 0.;
684 if (!dem.d_particlesListTypeParticle.empty()) {
685 const auto c = dem.d_particlesListTypeParticle[0]->getXCenter();
686 com0x = c.d_x;
687 com0y = c.d_y;
688 }
689 if (util::parallel::mpiSize() > 1)
690 MPI_Allreduce(MPI_IN_PLACE, &max_u, 1, MPI_DOUBLE, MPI_MAX,
692 if (util::parallel::mpiRank() == 0) {
693 std::ofstream os(out_dir / "mpi_metric.txt");
694 os << std::format("{:.12e} {:.12e} {:.12e}\n", max_u, com0x, com0y);
695 util::io::print(std::format("mpi_metric: max|u|={:.12e} grain0_com=({:.12e},{:.12e})\n",
696 max_u, com0x, com0y));
697 }
698 }
699
701 return EXIT_SUCCESS;
702}
int main(int argc, char *argv[])
Definition main.cpp:29
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:414
GrainContactProbe(std::filesystem::path out_dir={}, size_t interval=0)
Definition main.cpp:353
void sampleMetric(data::ModelData &data)
Definition main.cpp:433
void checkStop(data::ModelData &data) override
Definition main.cpp:362
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:333
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:325
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:171
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 normalContactStiffness(const double &K1, const double &K2, const double &horizon, int horizonPower=5)
Silling normal contact stiffness from two bulk moduli and the horizon.
Definition function.cpp:139
static json getExampleJson(const json &given=json::object(), const geom::GeomData *region=nullptr)
Returns example JSON object for ModelDeck configuration.
Definition bcBaseDeck.h:233
static json getExampleJson(const json &given=json::object())
Returns example JSON object for ModelDeck configuration.
Definition bcDeck.h:79
static json getExampleJson(const json &given=json::object())
Returns example JSON object for ModelDeck configuration.
static json getExampleJson(const json &given=json::object())
Returns example JSON object for ModelDeck configuration.
static json getExampleJson(const json &given=json::object())
Returns the block with the given fields set.
Definition modelDeck.h:217
static json getExampleJson(const std::string &)=delete
Returns example JSON object for ModelDeck configuration.
static json getExampleJson(const json &given=json::object())
Returns the block with the given fields set.
Definition pGenDeck.h:82
static json getExampleJson(const json &given=json::object())
Returns the block with the given fields set.
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
std::vector< double > toVec() const
Returns coordinate in stl form.
Definition point.h:118
double d_x
the x coordinate
Definition point.h:33