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 * Distributed under the Boost Software License, Version 1.0. (See accompanying
8 * file LICENSE)
9 *
10 * Two-particle circle test with fully in-process setup:
11 * - No input file on disk (deck built in C++ only).
12 * - Particle meshes from in-process Gmsh (CreateMesh.Info = gmsh_builtin_mesh).
13 *
14 * Default layout under the current working directory:
15 * ./out/ — VTU results and log.txt (Output.Path must end with a separator)
16 * ./inp/ — input.json (full deck) and mesh_cir_1.msh, mesh_cir_2.msh
17 *
18 * With PVD_Collection enabled (default here), ./out/output.pvd lists all particle
19 * VTU timesteps — open output.pvd in ParaView for a single time animation (not one
20 * monolithic VTU; VTK uses one VTU per snapshot plus this index file).
21 *
22 * -outputDir <path> sets the output directory (absolute or relative to cwd).
23 * Input files go to a sibling ./inp next to that directory's parent
24 * if -outputDir points at .../out; otherwise use -inputDir.
25 * -inputDir <path> optional; defaults to cwd/inp, or <parent of outputDir>/inp when
26 * -outputDir is used.
27 * -finalTime <t> integration end time (default 0.002; example_twop_circ_contact uses 0.012).
28 * -numSteps <n> number of steps (default 6000; example uses 36000). dt = finalTime/numSteps.
29 * -requireContact fail if max Damage_Z is 0 (particles never damaged / no contact).
30 * -jha2021Table2 Jha et al. JMPS 2021 Table 2 test 1 (ε_n = 1).
31 * -jha2021Table2Test <n> Table 2 test n = 1..5 (sets time, IC, ε_n, CR, damping).
32 * -inbuiltMesh with -jha2021Table2*: Gmsh in-process mesh (Mesh_Size=R/5)
33 * instead of the frozen v0.1.0 .msh. Same dt, IC, horizon, μ.
34 * -epsN <ε> contact Epsilon (paper ε̄_n). ε < 1 turns damping on (C̄ = 100).
35 * -betaNFactor <C> contact Beta_n_Factor (paper C̄, default 100).
36 * -zeroIC drop from rest (paper Fig. 5); default is a free-fall velocity IC.
37 * -assertCR <ref> fail unless sqrt(H1/H0) is within -crTol of <ref>.
38 * -crTol <tol> absolute CR tolerance (default 0.1).
39 */
40
41#include "inp/deckIncludes.h"
42#include "util/io.h"
43#include "util/function.h"
44#include "util/parallelUtil.h"
47#include "periDEMModel.h"
51#include <mpi.h>
52#include <algorithm>
53#include <cmath>
54#include <cstdlib>
55#include <cstring>
56#include <filesystem>
57#include <format>
58#include <fstream>
59#include <memory>
60#include <stdexcept>
61#include <thread>
62#include <utility>
63#include <vector>
64
65// Jha et al. JMPS 2021 Table 2: same R, M1, H0; CR vs ε̄_n (paper Epsilon).
66// The paper's CRs (1, 0.946, 0.893, 0.845, 0.796) came from the earlier 2D
67// state-based model, which was about twice too stiff. The values below are
68// from the corrected model on the same frozen mesh; softer grains stay in
69// contact longer, so damping removes more energy.
71 double eps;
72 double cr;
73};
74
76 static const Jha2021Table2 k[] = {
77 {1.0, 0.996}, {0.95, 0.925}, {0.9, 0.861}, {0.85, 0.801}, {0.8, 0.744}};
78 if (test_id < 1 || test_id > 5)
79 throw std::runtime_error("jha2021Table2Test must be 1..5");
80 return k[test_id - 1];
81}
82
83namespace {
84
86std::string directoryPathWithTrailingSep(const std::filesystem::path &dir) {
87 namespace fs = std::filesystem;
88 fs::path n = fs::absolute(dir).lexically_normal();
89 std::string s = n.string();
90 if (!s.empty() && s.back() != '/' && s.back() != '\\')
91 s += fs::path::preferred_separator;
92 return s;
93}
94
95json buildInputJson(const std::string &output_path_for_deck,
96 const std::filesystem::path &mesh_file_1,
97 const std::filesystem::path &mesh_file_2,
98 double final_time, size_t num_steps, bool zero_ic,
99 double mesh_size_in, double horizon_in, bool damping_on,
100 double eps_n, bool two_particle_test, bool file_mesh,
101 double beta_n_factor,
102 const std::string &damping_law = "com_and_node",
103 const std::string &friction_law = "coulomb_simple",
104 bool friction_on = false,
105 double friction_mu = -1.,
106 double ic_vx = 0.,
107 bool bottom_patch_bc = false,
108 const std::string &mpi_strategy = "auto") {
109
110 const std::vector<double> center = {0.0, 0.0, 0.0};
111 const double R1 = 0.001;
112 const double R2 = 0.001;
113 const double mesh_size =
114 mesh_size_in > 0. ? mesh_size_in : std::min(R1, R2) / 5.0;
115 const double horizon = horizon_in > 0. ? horizon_in : 3.0 * mesh_size;
116 const double particle_dist = 0.001;
117
118 const double poisson1 = 0.25;
119 const double rho1 = 1200.0;
120 const double K1 = 2.16e+7;
121 const double E1 = material::toE(K1, poisson1);
122 const double G1 = material::toGE(E1, poisson1);
123 const double Gc1 = 50.0;
124
125 const double poisson2 = 0.25;
126 const double rho2 = 1200.0;
127 const double K2 = 2.16e+7;
128 const double E2 = material::toE(K2, poisson2);
129 const double G2 = material::toGE(E2, poisson2);
130 const double Gc2 = 50.0;
131
132 const double R_contact_factor = 0.95;
133 const double Kn_11 = util::normalContactStiffness(K1, K1, horizon);
134 const double Kn_22 = util::normalContactStiffness(K2, K2, horizon);
135 const double Kn_12 = util::normalContactStiffness(K1, K2, horizon);
136 const double friction_coeff = 0.5;
137
138 std::vector<double> p1_center = center;
139 std::vector<double> p2_center = center;
140
141 const double H0_drop = particle_dist; // paper drop height, 1 mm
142 // Table 2 energy: remaining gap = horizon, rest of H0 already in IC velocity.
143 const double top_gap = two_particle_test ? horizon : particle_dist;
144
145 const size_t dt_out_n = num_steps / 10;
146 auto modelDeckJson = inp::ModelDeck::getExampleJson(
147 json{{"Dimension", 2},
148 {"Final_Time", final_time},
149 {"Time_Steps", num_steps},
150 {"Discretization_Type", json{{"Spatial", "finite_difference"}, {"Time", "central_difference"}}},
151 {"Populate_ElementNodeConnectivity", true},
152 {"Quad_Approximation_Order", 2},
153 {"Particle_Sim_Type", "Multi_Particle"},
154 {"Seed", 0}});
155 modelDeckJson["MPI_Strategy"] = mpi_strategy;
156
157 auto outputDeckJson = inp::OutputDeck::getExampleJson(
158 json{{"File_Format", "vtu"},
159 {"Path", output_path_for_deck},
160 {"Tags", std::vector<std::string>({"Displacement", "Velocity", "Force", "Damage_Z", "Damage", "Particle_ID"})},
161 {"Output_Interval", dt_out_n},
162 {"Debug", 2},
163 {"Perform_FE_Out", true},
164 {"Compress_Type", "zlib"},
165 {"Perform_Out", true},
166 {"Test_Output_Interval", 1},
167 {"Tag_PP", ""},
168 {"PVD_Collection", true}});
169
170 auto bcDeckJson = inp::BCDeck::getExampleJson(
171 json{{"Displacement_BC_Sets", 1},
172 {"IC_Sets", 1},
173 {"Gravity", util::Point(0, -10, 0).toVec()}});
174
175 if (bottom_patch_bc) {
176 // Particle 0 centered at (R1,R1); fix only a bottom strip (not the whole grain).
177 geom::GeomData patch;
178 patch.d_geomName = "rectangle";
179 patch.d_geomParams = {R1 - 1.1 * R1, -0.1 * R1, 0., R1 + 1.1 * R1, 0.35 * R1,
180 0.};
181 json j_geom;
182 geom::writeGeometry(j_geom, patch);
183 bcDeckJson["Displacement_BC"]["Set_1"] = json{
184 {"Particle_List", json::array({0})},
185 {"Region", {{"Geometry", j_geom}}},
186 {"Direction", json::array({1, 2})},
187 {"Zero_Displacement", true}};
188 } else {
189 bcDeckJson["Displacement_BC"]["Set_1"] =
191 json{{"Type", "Displacement_BC"},
192 {"Particle_List", {0}},
193 {"Direction", {1, 2}},
194 {"Zero_Displacement", true}});
195 }
196
197 std::vector<double> ic_vel = {0.0, 0.0, 0.0};
198 if (!zero_ic) {
199 const double fallen =
200 two_particle_test ? (H0_drop - top_gap) : (particle_dist - horizon);
201 if (fallen > 0.)
202 ic_vel[1] = -std::sqrt(2.0 * std::abs(-10.0) * fallen);
203 }
204 ic_vel[0] = ic_vx;
205 bcDeckJson["IC"]["Set_1"] = inp::BCBaseDeck::getExampleJson(
206 json{{"Type", "IC"},
207 {"Particle_List", {1}},
208 {"IC_Type", "Constant_Velocity"},
209 {"IC_Vector", ic_vel}});
210
211 auto pDeckJson = json({});
212
213 std::vector<geom::GeomData> pGeomVec(2);
214 pGeomVec[0].d_geomName = "circle";
215 pGeomVec[0].d_geomParams = {R1, p1_center[0], p1_center[1], p1_center[2]};
216 pGeomVec[1].d_geomName = "circle";
217 pGeomVec[1].d_geomParams = {R2, p2_center[0], p2_center[1], p2_center[2]};
218
219 pDeckJson["Particle"] = inp::ParticleDeck::getParticleGeomExampleJson(pGeomVec);
220
221 const std::string f1 = mesh_file_1.string();
222 const std::string f2 = mesh_file_2.string();
223 json meshSet1, meshSet2;
224 if (file_mesh) {
225 meshSet1 = json({{"File", f1}});
226 meshSet2 = json({{"File", f2}});
227 } else {
228 meshSet1 = json({{"File", f1},
229 {"CreateMesh",
230 {{"Flag", true},
231 {"Info", "gmsh_builtin_mesh"},
232 {"Mesh_Size", mesh_size},
233 {"Write_Mesh_File", true}}}});
234 meshSet2 = json({{"File", f2},
235 {"CreateMesh",
236 {{"Flag", true},
237 {"Info", "gmsh_builtin_mesh"},
238 {"Mesh_Size", mesh_size},
239 {"Write_Mesh_File", true}}}});
240 }
241 pDeckJson["Mesh"] = json({{"Sets", 2}, {"Set_1", meshSet1}, {"Set_2", meshSet2}});
242
244 pMatJson["Set_1"] = inp::MaterialDeck::getExampleJson(
245 json{{"Type", "PDState"},
246 {"Is_Plane_Strain", false},
247 {"Horizon", horizon},
248 {"Density", rho1},
249 {"K", K1},
250 {"G", G1},
251 {"Gc", Gc1},
252 {"Compute_From_Classical", true},
253 {"Influence_Function", json{{"Type", 0}}}});
254 pMatJson["Set_2"] = inp::MaterialDeck::getExampleJson(
255 json{{"Type", "PDState"},
256 {"Is_Plane_Strain", false},
257 {"Horizon", horizon},
258 {"Density", rho2},
259 {"K", K2},
260 {"G", G2},
261 {"Gc", Gc2},
262 {"Compute_From_Classical", true},
263 {"Influence_Function", json{{"Type", 0}}}});
264 pDeckJson["Material"] = pMatJson;
265
267 // v0.1.0 circ_damp: Friction_On: false leaves μ = 0 (coeff is not read).
268 // Nonzero μ with the flag off still applies a tangential force in PairForce,
269 // which gives the falling particle a lateral velocity (Table 2 test 4).
270 const double mu =
271 (friction_mu >= 0.) ? friction_mu
272 : (two_particle_test ? 0. : friction_coeff);
274 json{{"Contact_Radius_Factor", R_contact_factor},
275 {"Kn", Kn_11},
276 {"Damping_On", damping_on},
277 {"Epsilon", eps_n},
278 {"Friction_On", friction_on},
279 {"Friction_Coeff", mu},
280 {"Kn_Factor", 1.0},
281 {"Beta_n_Factor", beta_n_factor},
282 {"K", K1}});
283
284 pContactJson["Set_1_1"] = contact_base;
285 pContactJson["Set_1_1"]["Kn"] = Kn_11;
286 pContactJson["Set_1_1"]["K"] = K1;
287 pContactJson["Set_1_2"] = contact_base;
288 pContactJson["Set_1_2"]["Kn"] = Kn_12;
289 pContactJson["Set_1_2"]["K"] = util::harmonicMean(K1, K2);
290 pContactJson["Set_2_2"] = contact_base;
291 pContactJson["Set_2_2"]["Kn"] = Kn_22;
292 pContactJson["Set_2_2"]["K"] = K2;
293 pContactJson["Damping_Law"] = damping_law;
294 pContactJson["Friction_Law"] = friction_law;
295 pDeckJson["Contact"] = pContactJson;
296
297 pDeckJson["Neighbor"] = two_particle_test
299 json{{"Update_Criteria", "simple_all"},
300 {"Search_Factor", 5.0},
301 {"Search_Interval", 1},
302 {"Near_Bd_Nodes_Tol", 0.5}})
304 json{{"Update_Criteria", "simple_all"},
305 {"Search_Factor", 10.0},
306 {"Search_Interval", 40},
307 {"Near_Bd_Nodes_Tol", 0.5}});
308
309 auto pGenJson = inp::PGenDeck::getExampleJson(json{{"Method", "From_File"}});
310 pGenJson["Random_Rotation"] = false;
311 pGenJson["Data"]["N"] = 2;
312 pGenJson["Data"]["0"] = {
313 {"x", R1}, {"y", R1}, {"z", 0.0},
314 {"theta", 0.0}, {"s", 1.0},
315 {"geom_id", 0}, {"mat_id", 0}, {"contact_id", 0}
316 };
317 pGenJson["Data"]["1"] = {
318 {"x", R1}, {"y", 2.0 * R1 + R2 + top_gap}, {"z", 0.0},
319 {"theta", two_particle_test ? M_PI / 2.0 : M_PI}, {"s", 1.0},
320 {"geom_id", 1}, {"mat_id", 1}, {"contact_id", 1}
321 };
322 pDeckJson["Particle_Generation"] = pGenJson;
323
324 auto j = json({{"Model", modelDeckJson},
325 {"Output", outputDeckJson},
326 {"Force_BC", bcDeckJson["Force_BC"]},
327 {"Displacement_BC", bcDeckJson["Displacement_BC"]},
328 {"IC", bcDeckJson["IC"]},
329 {"Particle", pDeckJson["Particle"]},
330 {"Mesh", pDeckJson["Mesh"]},
331 {"Material", pDeckJson["Material"]},
332 {"Contact", pDeckJson["Contact"]},
333 {"Neighbor", pDeckJson["Neighbor"]},
334 {"Particle_Generation", pDeckJson["Particle_Generation"]}});
335 if (two_particle_test)
336 j["Test"] = json{{"Test_Name", "two_particle"}};
337 // bottom_patch_bc keeps PD force on particle 0 (deformable base).
338 return j;
339}
340
341} // namespace
342
344public:
345 explicit RestitutionProbe(double H0) : d_H0(H0) {}
346
349 if (data.d_particlesListTypeAll.size() < 2)
350 return;
351 const auto *p0 = data.d_particlesListTypeAll[0];
352 const auto *p1 = data.d_particlesListTypeAll[1];
353 const double gap = p0->getXCenter().dist(p1->getXCenter()) -
354 p0->d_geom_p->boundingRadius() -
355 p1->d_geom_p->boundingRadius();
356 if (data.currentStep() % 50 == 0)
357 d_samples.push_back({data.d_time, gap});
358 if (d_done)
359 return;
360 const double leave = 1.0e-5;
361 const double significant = 0.05 * d_H0;
362 if (!d_contacted) {
363 if (gap < d_minGap) {
364 d_minGap = gap;
365 d_tMin = data.d_time;
366 } else if (d_minGap < 0.3 * d_H0 && gap > d_minGap + leave) {
367 d_contacted = true;
368 d_H1 = gap;
369 d_tH1 = data.d_time;
370 }
371 } else if (gap > d_H1) {
372 d_H1 = gap;
373 d_tH1 = data.d_time;
374 } else if (d_H1 > d_minGap + significant && gap < d_H1 - leave) {
375 d_done = true;
376 }
377 }
378
379 bool contacted() const { return d_contacted; }
380 double H1() const { return d_H1; }
381 double minGap() const { return d_minGap; }
382 double tMin() const { return d_tMin; }
383 double tH1() const { return d_tH1; }
384 const std::vector<std::pair<double, double>> &samples() const {
385 return d_samples;
386 }
387 double CR() const {
388 if (d_H0 <= 0. || d_H1 <= 0.)
389 return 0.;
390 return std::sqrt(d_H1 / d_H0);
391 }
392
393private:
394 double d_H0;
395 double d_H1 = 0.;
396 double d_minGap = 1.0e9;
397 double d_tMin = 0.;
398 double d_tH1 = 0.;
399 bool d_contacted = false;
400 bool d_done = false;
401 std::vector<std::pair<double, double>> d_samples;
402};
403
407public:
408 explicit LateralProbe(double ic_abs_vx) : d_icAbsVx(std::abs(ic_abs_vx)) {}
409
412 if (data.d_particlesListTypeAll.size() < 2)
413 return;
414 const auto *p0 = data.d_particlesListTypeAll[0];
415 const auto *p1 = data.d_particlesListTypeAll[1];
416 const double gap = p0->getXCenter().dist(p1->getXCenter()) -
417 p0->d_geom_p->boundingRadius() -
418 p1->d_geom_p->boundingRadius();
419 if (gap < 0.5 * 0.001)
420 d_contacted = true;
421 const double vx = std::abs(p1->getVCenter().d_x);
422 if (vx > d_maxAbsVx)
423 d_maxAbsVx = vx;
426 }
427 bool contacted() const { return d_contacted; }
428 double maxAbsVx() const { return d_maxAbsVx; }
432 double icAbsVx() const { return d_icAbsVx; }
433
434private:
435 double d_icAbsVx = 0.;
436 double d_maxAbsVx = 0.;
437 double d_minAbsVxAfterContact = 1.0e300;
438 bool d_contacted = false;
439};
440
443public:
444 MpiMetricTsProbe(std::filesystem::path out_dir, size_t interval)
445 : d_outDir(std::move(out_dir)),
446 d_interval(std::max<size_t>(1, interval)) {
447 if (util::parallel::mpiRank() == 0) {
448 std::filesystem::create_directories(d_outDir / "nodal");
449 d_os.open(d_outDir / "mpi_metric_ts.csv");
450 d_os << "step,t,max_u,com1x,com1y\n";
451 }
452 }
453
456 const size_t nstep = data.currentStep();
457 if (nstep % d_interval != 0 && nstep < data.numTimeSteps())
458 return;
459
460 double max_u = 0.;
461 for (const auto &u : data.d_u)
462 max_u = std::max(max_u, u.length());
463 if (util::parallel::mpiSize() > 1)
464 MPI_Allreduce(MPI_IN_PLACE, &max_u, 1, MPI_DOUBLE, MPI_MAX,
466 double com1x = 0., com1y = 0.;
467 if (data.d_particlesListTypeParticle.size() > 1) {
468 const auto c = data.d_particlesListTypeParticle[1]->getXCenter();
469 com1x = c.d_x;
470 com1y = c.d_y;
471 }
472 if (util::parallel::mpiRank() == 0) {
473 d_os << std::format("{},{:.12e},{:.12e},{:.12e},{:.12e}\n", nstep,
474 data.d_time, max_u, com1x, com1y);
475 d_os.flush();
476 }
477 dumpNodalUV(data, nstep);
478 }
479
480private:
481 static bool ownsNode(const data::ModelData &data, size_t i) {
482 const int rank = util::parallel::mpiRank();
483 if (data.d_pdDofMpi) {
484 if (i >= data.d_pdNodePartition.size())
485 return false;
486 return static_cast<int>(data.d_pdNodePartition[i]) == rank;
487 }
488 // Particle-MPI / serial: owned grains; walls only from rank 0 (replicated).
489 for (const auto *p : data.d_particlesListTypeAll) {
490 const size_t i0 = p->d_globStart;
491 const size_t i1 = i0 + p->getNumNodes();
492 if (i < i0 || i >= i1)
493 continue;
494 if (p->isWall())
495 return rank == 0;
496 return particle::isLocallyOwned(*p);
497 }
498 return rank == 0;
499 }
500
501 void dumpNodalUV(data::ModelData &data, size_t nstep) {
502 const size_t n = data.d_u.size();
503 std::vector<double> buf(6 * n, 0.);
504 for (size_t i = 0; i < n; ++i) {
505 if (!ownsNode(data, i))
506 continue;
507 buf[6 * i + 0] = data.d_u[i].d_x;
508 buf[6 * i + 1] = data.d_u[i].d_y;
509 buf[6 * i + 2] = data.d_u[i].d_z;
510 buf[6 * i + 3] = data.d_v[i].d_x;
511 buf[6 * i + 4] = data.d_v[i].d_y;
512 buf[6 * i + 5] = data.d_v[i].d_z;
513 }
514 if (util::parallel::mpiSize() > 1)
515 MPI_Allreduce(MPI_IN_PLACE, buf.data(), static_cast<int>(buf.size()),
516 MPI_DOUBLE, MPI_SUM, util::parallel::mpiComm());
517 if (util::parallel::mpiRank() != 0)
518 return;
519 if (!d_wroteXref) {
520 std::ofstream xr(d_outDir / "nodal" / "x_ref.bin", std::ios::binary);
521 const uint32_t nn = static_cast<uint32_t>(n);
522 xr.write(reinterpret_cast<const char *>(&nn), sizeof(nn));
523 for (size_t i = 0; i < n; ++i) {
524 const double p[3] = {data.d_xRef[i].d_x, data.d_xRef[i].d_y,
525 data.d_xRef[i].d_z};
526 xr.write(reinterpret_cast<const char *>(p), sizeof(p));
527 }
528 d_wroteXref = true;
529 }
530 const auto path =
531 d_outDir / "nodal" / std::format("uv_{:06d}.bin", nstep);
532 std::ofstream os(path, std::ios::binary);
533 const char magic[4] = {'P', 'D', 'U', 'V'};
534 const uint32_t step32 = static_cast<uint32_t>(nstep);
535 const uint32_t nn = static_cast<uint32_t>(n);
536 os.write(magic, 4);
537 os.write(reinterpret_cast<const char *>(&step32), sizeof(step32));
538 os.write(reinterpret_cast<const char *>(&nn), sizeof(nn));
539 os.write(reinterpret_cast<const char *>(buf.data()),
540 static_cast<std::streamsize>(buf.size() * sizeof(double)));
541 }
542
543 std::filesystem::path d_outDir;
545 std::ofstream d_os;
546 bool d_wroteXref = false;
547};
548
549int main(int argc, char *argv[]) {
550
551 util::parallel::initMpi(argc, argv);
552 int mpiSize = util::parallel::mpiSize(), mpiRank = util::parallel::mpiRank();
553 util::io::print(std::format("Initialized MPI. MPI size = {}, MPI rank = {}\n", mpiSize, mpiRank));
555
556 util::io::InputParser input(argc, argv);
557
558 unsigned int nThreads;
559 if (input.cmdOptionExists("-nThreads"))
560 nThreads = std::stoi(input.getCmdOption("-nThreads"));
561 else {
562 nThreads = std::thread::hardware_concurrency();
563 util::io::print(std::format("Running test with default number of threads = {}\n", nThreads));
564 }
566 util::io::print(std::format("Number of threads = {}\n", util::parallel::getNThreads()));
567
568#ifndef TWOP_CONTACT_EXAMPLE
569 double final_time = 0.002;
570 size_t num_steps = 6000;
571#else
572 double final_time = 0.012;
573 size_t num_steps = 36000;
574#endif
575 bool zero_ic = false;
576 double mesh_size_in = -1.;
577 double horizon_in = -1.;
578 bool assert_cr = false;
579 double cr_ref = 1.;
580 double cr_tol = 0.1;
581 bool damping_on = false;
582 double eps_n = 0.9;
583 double beta_n_factor = 100.0;
584 std::string damping_law = "com_and_node";
585 std::string friction_law = "coulomb_simple";
586 bool friction_on = false;
587 double friction_mu = -1.;
588 double ic_vx = 0.;
589 bool assert_lat = false;
590 double lat_min = 0.;
591 bool bottom_patch_bc = false;
592 std::string mpi_strategy = "auto";
593 bool write_mpi_metric = false;
594 int jha_test = 0;
595 if (input.cmdOptionExists("-jha2021Table2Test"))
596 jha_test = std::stoi(input.getCmdOption("-jha2021Table2Test"));
597 else if (input.cmdOptionExists("-jha2021Table2"))
598 jha_test = 1;
599 if (jha_test > 0) {
600 const auto c = jha2021Table2(jha_test);
601 // Paper §4.1 / v0.1.0 circ_damp: T=0.04 s, Δt=0.2 μs, horizon 0.6 mm.
602 // Mesh is the frozen v0.1.0 .msh (hmin = 0.1423 mm), not in-process Gmsh.
603 final_time = 0.04;
604 num_steps = 200000;
605 horizon_in = 0.0006;
606 // v0.1.0 circ_damp: not drop-from-rest. Gap is shortened to the horizon
607 // and the leftover 0.4 mm of the 1 mm drop is an impact velocity.
608 zero_ic = false;
609 assert_cr = true;
610 cr_ref = c.cr;
611 cr_tol = 0.05;
612 eps_n = c.eps;
613 damping_on = c.eps < 1.0 - 1.0e-12;
614 }
615 if (input.cmdOptionExists("-finalTime"))
616 final_time = std::stod(input.getCmdOption("-finalTime"));
617 if (input.cmdOptionExists("-numSteps"))
618 num_steps = std::stoul(input.getCmdOption("-numSteps"));
619 if (input.cmdOptionExists("-zeroIC"))
620 zero_ic = true;
621 if (input.cmdOptionExists("-meshSize"))
622 mesh_size_in = std::stod(input.getCmdOption("-meshSize"));
623 if (input.cmdOptionExists("-horizon"))
624 horizon_in = std::stod(input.getCmdOption("-horizon"));
625 if (input.cmdOptionExists("-epsN")) {
626 eps_n = std::stod(input.getCmdOption("-epsN"));
627 damping_on = eps_n < 1.0 - 1.0e-12;
628 }
629 if (input.cmdOptionExists("-betaNFactor"))
630 beta_n_factor = std::stod(input.getCmdOption("-betaNFactor"));
631 if (input.cmdOptionExists("-dampingLaw"))
632 damping_law = input.getCmdOption("-dampingLaw");
633 if (input.cmdOptionExists("-frictionLaw"))
634 friction_law = input.getCmdOption("-frictionLaw");
635 if (input.cmdOptionExists("-enableFriction"))
636 friction_on = true;
637 if (input.cmdOptionExists("-mu"))
638 friction_mu = std::stod(input.getCmdOption("-mu"));
639 if (input.cmdOptionExists("-icVx"))
640 ic_vx = std::stod(input.getCmdOption("-icVx"));
641 if (input.cmdOptionExists("-assertLateralVx")) {
642 assert_lat = true;
643 lat_min = std::stod(input.getCmdOption("-assertLateralVx"));
644 assert_cr = false; // skip CR gate from table2 defaults for this lateral check
645 }
646 if (input.cmdOptionExists("-assertCR")) {
647 assert_cr = true;
648 cr_ref = std::stod(input.getCmdOption("-assertCR"));
649 }
650 if (input.cmdOptionExists("-crTol"))
651 cr_tol = std::stod(input.getCmdOption("-crTol"));
652 if (input.cmdOptionExists("-bottomPatchBC"))
653 bottom_patch_bc = true;
654 if (input.cmdOptionExists("-mpiStrategy"))
655 mpi_strategy = input.getCmdOption("-mpiStrategy");
656 if (input.cmdOptionExists("-writeMpiMetric"))
657 write_mpi_metric = true;
658 util::io::print(std::format(
659 "final_time = {}, num_steps = {}, zero_ic = {}, eps_n = {}, C_bar = {}, "
660 "damping = {}, Damping_Law = {}, Friction_Law = {}, bottomPatchBC = {}, "
661 "MPI_Strategy = {}\n",
662 final_time, num_steps, zero_ic, eps_n, beta_n_factor, damping_on,
663 damping_law, friction_law, bottom_patch_bc, mpi_strategy));
664
665 namespace fs = std::filesystem;
666 const fs::path cwd = fs::current_path();
667
668 fs::path out_dir = cwd / "out";
669 fs::path inp_dir = cwd / "inp";
670
671 if (input.cmdOptionExists("-outputDir")) {
672 fs::path p = input.getCmdOption("-outputDir");
673 out_dir = p.is_absolute() ? std::move(p) : cwd / p;
674 }
675 if (input.cmdOptionExists("-inputDir")) {
676 fs::path p = input.getCmdOption("-inputDir");
677 inp_dir = p.is_absolute() ? std::move(p) : cwd / p;
678 } else if (input.cmdOptionExists("-outputDir")) {
679 // Place inp next to out: .../run/out -> .../run/inp
680 inp_dir = out_dir.parent_path() / "inp";
681 }
682
683 fs::create_directories(out_dir);
684 fs::create_directories(inp_dir);
685
686 const std::string output_path_for_deck = directoryPathWithTrailingSep(out_dir);
687 const fs::path mesh1 = inp_dir / "mesh_cir_1.msh";
688 const fs::path mesh2 = inp_dir / "mesh_cir_2.msh";
689 const bool inbuilt_mesh = input.cmdOptionExists("-inbuiltMesh");
690 const bool file_mesh = jha_test > 0 && !inbuilt_mesh;
691 if (file_mesh) {
692#ifndef JHA2021_MESH_DIR
693 throw std::runtime_error("JHA2021_MESH_DIR is not set (v0.1.0 circ_damp meshes).");
694#else
695 const fs::path src(JHA2021_MESH_DIR);
696 fs::copy_file(src / "mesh_cir_1.msh", mesh1, fs::copy_options::overwrite_existing);
697 fs::copy_file(src / "mesh_cir_2.msh", mesh2, fs::copy_options::overwrite_existing);
698 util::io::print(std::format("Using v0.1.0 circ_damp meshes from {}\n", src.string()));
699#endif
700 } else if (jha_test > 0)
701 util::io::print("Using in-process Gmsh (gmsh_builtin_mesh) instead of frozen v0.1.0 .msh\n");
702
703 util::io::print(std::format("Output directory (VTU, log.txt): {}\n", fs::absolute(out_dir).string()));
704 util::io::print(std::format("Input directory (input.json, meshes): {}\n", fs::absolute(inp_dir).string()));
705
706 auto inputJson = buildInputJson(output_path_for_deck, mesh1, mesh2, final_time,
707 num_steps, zero_ic, mesh_size_in, horizon_in,
708 damping_on, eps_n, jha_test > 0, file_mesh,
709 beta_n_factor, damping_law,
710 friction_law, friction_on, friction_mu,
711 ic_vx, bottom_patch_bc, mpi_strategy);
712
713 const fs::path input_json_path = inp_dir / "input.json";
714 {
715 std::ofstream os(input_json_path);
716 if (!os)
717 throw std::runtime_error("Failed to open " + input_json_path.string() + " for writing.");
718 os << inputJson.dump(2);
719 }
720 util::io::print(std::format("Wrote deck to {}\n", fs::absolute(input_json_path).string()));
721
722 // -deckOnly stops after the deck is written. The Python comparison reads
723 // the deck and does not need this driver to run the simulation.
724 if (input.cmdOptionExists("-deckOnly")) {
725 util::io::print("deck written; -deckOnly, not running\n");
727 return 0;
728 }
729 if (jha_test > 0) {
730 const auto &p1 = inputJson["Particle_Generation"]["Data"]["1"];
731 const auto &v = inputJson["IC"]["Set_1"]["Constant_Velocity"]["Velocity_Vector"];
732 const double y1 = p1.at("y").get<double>();
733 const double gap0 = y1 - 0.001 - 0.001 - 0.001; // y_top - R_bottom_center - R_bot - R_top
734 util::io::print(std::format(
735 "Table 2 kinematics: top y = {}, surface gap = {}, v_y = {}\n",
736 y1, gap0, v.at(1).get<double>()));
737 }
738
739 auto deck = std::make_shared<inp::Input>(inputJson);
740
741 PeriDEMModel dem(deck);
742 RestitutionProbe *probe = nullptr;
743 LateralProbe *lat_probe = nullptr;
744 if (assert_cr) {
745 auto p = std::make_unique<RestitutionProbe>(0.001);
746 probe = p.get();
747 dem.setPostprocess(std::move(p));
748 } else if (assert_lat) {
749 auto p = std::make_unique<LateralProbe>(ic_vx);
750 lat_probe = p.get();
751 dem.setPostprocess(std::move(p));
752 } else if (write_mpi_metric) {
753 const size_t ts_every = std::max<size_t>(1, num_steps / 100);
754 dem.setPostprocess(
755 std::make_unique<MpiMetricTsProbe>(out_dir, ts_every));
756 }
757 dem.run(deck);
758
759 if (input.cmdOptionExists("-requireContact")) {
760 const float zmax =
761 dem.d_Z.empty() ? 0.f : *std::max_element(dem.d_Z.begin(), dem.d_Z.end());
762 util::io::print(std::format("requireContact: max Damage_Z = {}\n", zmax));
763 if (zmax <= 0.f) {
764 util::io::print("requireContact: no damage; particles did not contact.\n");
765 return EXIT_FAILURE;
766 }
767 }
768
769 if (assert_cr) {
770 {
771 const fs::path gap_csv = out_dir / "gap.csv";
772 std::ofstream gs(gap_csv);
773 gs << "t,gap\n";
774 for (const auto &s : probe->samples())
775 gs << s.first << "," << s.second << "\n";
776 util::io::print(std::format("Wrote {}\n", fs::absolute(gap_csv).string()));
777 }
778 const double cr = probe->CR();
779 util::io::print(std::format(
780 "assertCR: contacted={}, min_gap={} (t={}), H1={} (t={}), CR={}, ref={}, tol={}\n",
781 probe->contacted(), probe->minGap(), probe->tMin(), probe->H1(),
782 probe->tH1(), cr, cr_ref, cr_tol));
783 if (cr > 1.02) {
785 "assertCR: CR > 1.02 on the 1 mm energy-equivalent drop "
786 "(surface gap = horizon, leftover height already in v_y). "
787 "Unphysical energy gain.\n");
788 return EXIT_FAILURE;
789 }
790 if (!probe->contacted() || std::abs(cr - cr_ref) > cr_tol) {
791 util::io::print("assertCR: coefficient of restitution out of range.\n");
792 return EXIT_FAILURE;
793 }
794 }
795
796 if (assert_lat) {
797 const double vmin = lat_probe->minAbsVxAfterContact();
798 const double vmax = lat_probe->maxAbsVx();
799 const double ic = lat_probe->icAbsVx();
800 util::io::print(std::format(
801 "assertLateralVx: contacted={}, max|vx|={}, min|vx|_after_contact={}, "
802 "ic|vx|={}, slow_factor_max={}\n",
803 lat_probe->contacted(), vmax, vmin, ic, lat_min));
804 if (!lat_probe->contacted()) {
805 util::io::print("assertLateralVx: particles never contacted.\n");
806 return EXIT_FAILURE;
807 }
808 // lat_min is the maximum allowed ratio min_vx/ic_vx after contact (e.g. 0.95).
809 if (!(ic > 0.) || !(vmin < lat_min * ic)) {
811 "assertLateralVx: friction did not slow lateral speed enough "
812 "(or kinematics missed).\n");
813 return EXIT_FAILURE;
814 }
815 }
816
817 if (write_mpi_metric) {
818 double max_u = 0.;
819 for (const auto &u : dem.d_u)
820 max_u = std::max(max_u, u.length());
821 double com1x = 0., com1y = 0.;
822 if (dem.d_particlesListTypeParticle.size() > 1) {
823 const auto c = dem.d_particlesListTypeParticle[1]->getXCenter();
824 com1x = c.d_x;
825 com1y = c.d_y;
826 }
828 MPI_Allreduce(MPI_IN_PLACE, &max_u, 1, MPI_DOUBLE, MPI_MAX,
830 // Center node is unique; take from owning rank via MAX of abs then
831 // broadcast of values — simpler: Allreduce SUM after zeroing non-owners
832 // is wrong for COM. After DOF sync every rank has the same center x.
833 }
834 if (util::parallel::mpiRank() == 0) {
835 std::ofstream os(out_dir / "mpi_metric.txt");
836 os << std::format("{:.12e} {:.12e} {:.12e}\n", max_u, com1x, com1y);
837 util::io::print(std::format(
838 "mpi_metric: max|u|={:.12e} grain1_com=({:.12e},{:.12e})\n", max_u,
839 com1x, com1y));
840 }
841 }
842
844 return EXIT_SUCCESS;
845}
int main(int argc, char *argv[])
Definition main.cpp:29
double icAbsVx() const
Definition main.cpp:432
double minAbsVxAfterContact() const
Definition main.cpp:429
double d_icAbsVx
Definition main.cpp:435
bool d_contacted
Definition main.cpp:438
LateralProbe(double ic_abs_vx)
Definition main.cpp:408
bool contacted() const
Definition main.cpp:427
double d_minAbsVxAfterContact
Definition main.cpp:437
double d_maxAbsVx
Definition main.cpp:436
void checkStop(data::ModelData &data) override
Definition main.cpp:410
double maxAbsVx() const
Definition main.cpp:428
void checkStop(data::ModelData &data) override
Definition main.cpp:454
MpiMetricTsProbe(std::filesystem::path out_dir, size_t interval)
Definition main.cpp:444
static bool ownsNode(const data::ModelData &data, size_t i)
Definition main.cpp:481
std::ofstream d_os
Definition main.cpp:545
std::filesystem::path d_outDir
Definition main.cpp:543
size_t d_interval
Definition main.cpp:544
void dumpNodalUV(data::ModelData &data, size_t nstep)
Definition main.cpp:501
void setPostprocess(std::unique_ptr< postprocess::Postprocess > p)
void run(std::shared_ptr< inp::Input > &deck)
bool contacted() const
Definition main.cpp:379
double H1() const
Definition main.cpp:380
double CR() const
Definition main.cpp:387
RestitutionProbe(double H0)
Definition main.cpp:345
std::vector< std::pair< double, double > > d_samples
Definition main.cpp:401
const std::vector< std::pair< double, double > > & samples() const
Definition main.cpp:384
double tH1() const
Definition main.cpp:383
double d_minGap
Definition main.cpp:396
void checkStop(data::ModelData &data) override
Definition main.cpp:347
double tMin() const
Definition main.cpp:382
double minGap() const
Definition main.cpp:381
double d_tMin
Definition main.cpp:397
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_file_1, const std::filesystem::path &mesh_file_2, double final_time, size_t num_steps, bool zero_ic, double mesh_size_in, double horizon_in, bool damping_on, double eps_n, bool two_particle_test, bool file_mesh, double beta_n_factor, const std::string &damping_law="com_and_node", const std::string &friction_law="coulomb_simple", bool friction_on=false, double friction_mu=-1., double ic_vx=0., bool bottom_patch_bc=false, const std::string &mpi_strategy="auto")
Definition main.cpp:95
Definition contact.h:20
void writeGeometry(json &j, const geom::GeomData &geomData)
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
bool isMpiEnabled()
Function to check if MPI is enabled.
unsigned int getNThreads()
Get number of threads to be used by taskflow.
void initNThreads(unsigned int nThreads=std::thread::hardware_concurrency())
Initializes MpiStatus struct.
const MpiStatus * getMpiStatus()
Returns pointer to 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
double harmonicMean(const double &m1, const double &m2)
Definition function.cpp:135
double cr
Definition main.cpp:72
double eps
Definition main.cpp:71
Input data for geometrical objects.
std::vector< double > d_geomParams
Zone parameters.
std::string d_geomName
Zone type.
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.
static json getParticleMaterialExampleJson(size_t nSets=0)
Returns example JSON object for ModelDeck configuration.
A structure to represent 3d vectors.
Definition point.h:30
std::vector< double > toVec() const
Returns coordinate in stl form.
Definition point.h:118
Jha2021Table2 jha2021Table2(int test_id)
Definition main.cpp:75