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 * Notched-plate impact driver (Silling / Trask / Bhat KW setups).
11 * Checks: prenotch bonds, tip damage growth, no blow-up.
12 * Paper-angle validation lives outside ctest.
13 * Flags: -quick, -traskDisp, -bhatKW, -dim3, -vImpact, -Gc, -meshSize,
14 * -finalTime, -outputDir, -nThreads, -selfContact, -bondBreak.
15 */
16
17#include "fracture/prenotch.h"
18#include "inp/deckIncludes.h"
20#include "periDEMModel.h"
21#include "time_int/integrator.h"
22#include "util/function.h"
23#include "util/io.h"
24#include "util/parallelUtil.h"
25#include "util/point.h"
26
27#include <cmath>
28#include <cstdlib>
29#include <algorithm>
30#include <filesystem>
31#include <format>
32#include <fstream>
33#include <memory>
34#include <stdexcept>
35#include <string>
36#include <vector>
37
38namespace {
39// The pre-notch functions are in src/fracture/prenotch.h, so that this driver
40// and the Python interface break the same bonds.
41size_t applyNotches(PeriDEMModel &dem, double x_left, double x_right, double y_lo,
42 double y_hi) {
44 *dem.d_fracture_p, dem.d_xRef, dem.d_neighPd, dem.d_ptId, 0,
45 {x_left, x_right}, y_lo, y_hi);
46}
47
48size_t applyNotchSlots(PeriDEMModel &dem, double notch_half, double notch_w,
49 double y_lo, double y_hi) {
51 dem.d_neighPd, dem.d_ptId, 0,
52 {-notch_half, notch_half}, notch_w, y_lo,
53 y_hi);
54}
55
56// A V-notch is seeded on its midplane, which is the same zero-width cut.
57size_t applyNotchMidplanes(PeriDEMModel &dem, double notch_half, double y_tip,
58 double y_top) {
60 *dem.d_fracture_p, dem.d_xRef, dem.d_neighPd, dem.d_ptId, 0,
61 {-notch_half, notch_half}, y_tip, y_top);
62}
63
64
65std::string directoryPathWithTrailingSep(const std::filesystem::path &dir) {
66 namespace fs = std::filesystem;
67 fs::path n = fs::absolute(dir).lexically_normal();
68 std::string s = n.string();
69 if (!s.empty() && s.back() != '/' && s.back() != '\\')
70 s += fs::path::preferred_separator;
71 return s;
72}
73
74
75
76
77// Break bonds that cross the midplane of each prenotch.
78
79double nodePhi(const PeriDEMModel &dem, size_t i) {
80 const auto &nb = dem.d_neighPd[i];
81 if (nb.empty())
82 return 0.;
83 size_t n_br = 0;
84 for (size_t k = 0; k < nb.size(); ++k) {
85 if (dem.d_fracture_p->getBondState(i, k))
86 ++n_br;
87 }
88 return static_cast<double>(n_br) / static_cast<double>(nb.size());
89}
90
91double nodeDamageForFit(const PeriDEMModel &dem, size_t i, bool use_bond_count) {
92 if (use_bond_count) {
93 if (!dem.d_phiBond.empty() && i < dem.d_phiBond.size())
94 return static_cast<double>(dem.d_phiBond[i]);
95 return nodePhi(dem, i);
96 }
97 if (!dem.d_phi.empty() && i < dem.d_phi.size())
98 return static_cast<double>(dem.d_phi[i]);
99 return nodePhi(dem, i);
100}
101
102struct CrackFit {
103 size_t n_pts = 0;
104 double angle_to_notch_deg = 0.;
105 double mean_dx = 0.;
106 double mean_dy = 0.;
107};
108
109// Max-damage ridge in outward bins from the tip; PCA for a rough path angle.
110CrackFit fitCrackFromPhi(const PeriDEMModel &dem, double tip_x, double tip_y,
111 double outward_sign, double phi_cut, double band_x, double band_y,
112 double exclude_r, double bin_h, bool use_bond_count = false) {
113 CrackFit fit;
114 std::vector<double> xs, ys;
115
116 const int nbin = std::max(4, static_cast<int>(std::ceil((band_x - exclude_r) / bin_h)));
117 for (int b = 0; b < nbin; ++b) {
118 const double r0 = exclude_r + b * bin_h;
119 const double r1 = std::min(band_x, r0 + bin_h);
120 double best_phi = phi_cut;
121 double best_dx = 0., best_dy = 0.;
122 bool found = false;
123 for (size_t i = 0; i < dem.d_xRef.size(); ++i) {
124 if (dem.d_ptId[i] != 0)
125 continue;
126 const double phi = nodeDamageForFit(dem, i, use_bond_count);
127 if (phi < best_phi)
128 continue;
129 const double dx_ref = outward_sign * (dem.d_xRef[i].d_x - tip_x);
130 const double dy_ref = tip_y - dem.d_xRef[i].d_y;
131 if (dx_ref < r0 || dx_ref >= r1)
132 continue;
133 if (dy_ref < -2.0 * bin_h || dy_ref > band_y)
134 continue;
135 const double dx = dem.d_xRef[i].d_x - tip_x;
136 const double dy = tip_y - dem.d_xRef[i].d_y;
137 best_phi = phi;
138 best_dx = dx;
139 best_dy = dy;
140 found = true;
141 }
142 if (found) {
143 xs.push_back(best_dx);
144 ys.push_back(best_dy);
145 }
146 }
147 fit.n_pts = xs.size();
148 if (fit.n_pts < 4)
149 return fit;
150 double mx = 0., my = 0.;
151 for (size_t i = 0; i < fit.n_pts; ++i) {
152 mx += xs[i];
153 my += ys[i];
154 }
155 mx /= static_cast<double>(fit.n_pts);
156 my /= static_cast<double>(fit.n_pts);
157 fit.mean_dx = mx;
158 fit.mean_dy = my;
159 double cxx = 0., cxy = 0., cyy = 0.;
160 for (size_t i = 0; i < fit.n_pts; ++i) {
161 const double x = xs[i] - mx;
162 const double y = ys[i] - my;
163 cxx += x * x;
164 cxy += x * y;
165 cyy += y * y;
166 }
167 const double trace = cxx + cyy;
168 const double det = cxx * cyy - cxy * cxy;
169 const double tmp = std::sqrt(std::max(0., 0.25 * trace * trace - det));
170 const double l1 = 0.5 * trace + tmp;
171 double vx = cxy;
172 double vy = l1 - cxx;
173 if (vx * vx + vy * vy < 1.e-30) {
174 vx = l1 - cyy;
175 vy = cxy;
176 }
177 const double angle_from_horiz =
178 std::atan2(std::abs(vy), std::abs(vx)) * 180. / M_PI;
179 fit.angle_to_notch_deg = 90. - angle_from_horiz;
180 return fit;
181}
182
183json meshSetJson(const std::filesystem::path &f, double mesh_size) {
184 return json{{"File", f.string()},
185 {"CreateMesh",
186 {{"Flag", true},
187 {"Info", "gmsh_builtin_mesh"},
188 {"Mesh_Size", mesh_size},
189 {"Write_Mesh_File", true}}}};
190}
191
192// Silling 2003: "a rectangular, equally spaced structured grid with dimensions
193// 200 x 100 x 9". `voids` carves the two notch slots out of that grid so they
194// are real gaps in the material, matching Fig. 2.
195json uniformMeshSetJson(const std::filesystem::path &f, double mesh_size,
196 const std::vector<std::vector<double>> &voids = {}) {
197 json cm = {{"Flag", true},
198 {"Info", "uniform"},
199 {"Mesh_Size", mesh_size},
200 {"Write_Mesh_File", true}};
201 if (!voids.empty())
202 cm["Void_Regions"] = voids;
203 return json{{"File", f.string()}, {"CreateMesh", cm}};
204}
205
206// The two 1.5 mm notch slots as boxes, open at the top edge.
207std::vector<std::vector<double>> notchVoidBoxes(double H, double notch_half,
208 double notch_w, double notch_depth,
209 double z_lo, double z_hi) {
210 const double hw = 0.5 * notch_w;
211 const double y_tip = 0.5 * H - notch_depth;
212 const double y_hi = 0.5 * H + 1.0e-9;
213 return {{-notch_half - hw, y_tip, z_lo, -notch_half + hw, y_hi, z_hi},
214 {notch_half - hw, y_tip, z_lo, notch_half + hw, y_hi, z_hi}};
215}
216
217// Silling Fig. 2: plate W×H with two open top notches (width notch_w, depth notch_depth),
218// centers at x=±notch_half (tip-to-tip = 2*notch_half).
219geom::GeomData sillingNotchedPlateGeom(double W, double H, double notch_half,
220 double notch_w, double notch_depth) {
221 const double x0 = -0.5 * W, y0 = -0.5 * H, x1 = 0.5 * W, y1 = 0.5 * H;
222 const double hw = 0.5 * notch_w;
223 const double y_tip = y1 - notch_depth;
224 geom::GeomData plate;
225 plate.d_geomName = "complex";
226 plate.d_geomComplexInfo = {
227 std::vector<std::string>{"rectangle", "rectangle", "rectangle"},
228 std::vector<std::string>{"plus", "minus", "minus"}};
229 plate.d_geomParams = {
230 // outer plate
231 x0, y0, 0., x1, y1, 0.,
232 // left notch cutout (opens at top)
233 -notch_half - hw, y_tip, 0., -notch_half + hw, y1 + 1.0e-6, 0.,
234 // right notch cutout
235 notch_half - hw, y_tip, 0., notch_half + hw, y1 + 1.0e-6, 0.};
236 return plate;
237}
238
239// Bhat Fig. 4(a): V-notches — 1.5 mm opening at the top edge, tapering to a
240// sharp tip at 50 mm depth. Triangle cutouts (9 params = three vertices each).
241geom::GeomData bhatVNotchedPlateGeom(double W, double H, double notch_half,
242 double notch_w, double notch_depth) {
243 const double x0 = -0.5 * W, y0 = -0.5 * H, x1 = 0.5 * W, y1 = 0.5 * H;
244 const double hw = 0.5 * notch_w;
245 const double y_tip = y1 - notch_depth;
246 const double y_top = y1 + 1.0e-6;
247 geom::GeomData plate;
248 plate.d_geomName = "complex";
249 plate.d_geomComplexInfo = {
250 std::vector<std::string>{"rectangle", "triangle", "triangle"},
251 std::vector<std::string>{"plus", "minus", "minus"}};
252 plate.d_geomParams = {
253 // outer plate
254 x0, y0, 0., x1, y1, 0.,
255 // left V: top-left, top-right, tip
256 -notch_half - hw, y_top, 0., -notch_half + hw, y_top, 0., -notch_half, y_tip, 0.,
257 // right V
258 notch_half - hw, y_top, 0., notch_half + hw, y_top, 0., notch_half, y_tip, 0.};
259 return plate;
260}
261
262json fixedTopOuterBC(double W, double H, double notch_half) {
263 const double fix_h = 0.08 * H;
264 return json{
265 {"Set_1",
266 {{"Particle_List", std::vector<size_t>{0}},
267 {"Region",
268 {{"Geometry",
269 {{"Type", "rectangle"},
270 {"Parameters",
271 std::vector<double>{-0.55 * W, 0.5 * H - fix_h, 0., -notch_half - 0.02 * W,
272 0.55 * H, 0.}}}}}},
273 {"Direction", std::vector<size_t>{1, 2}},
274 {"Time_Function", {{"Type", "constant"}, {"Parameters", std::vector<double>{0.}}}},
275 {"Spatial_Function", {{"Type", "constant"}}},
276 {"Zero_Displacement", true}}},
277 {"Set_2",
278 {{"Particle_List", std::vector<size_t>{0}},
279 {"Region",
280 {{"Geometry",
281 {{"Type", "rectangle"},
282 {"Parameters",
283 std::vector<double>{notch_half + 0.02 * W, 0.5 * H - fix_h, 0., 0.55 * W, 0.55 * H,
284 0.}}}}}},
285 {"Direction", std::vector<size_t>{1, 2}},
286 {"Time_Function", {{"Type", "constant"}, {"Parameters", std::vector<double>{0.}}}},
287 {"Spatial_Function", {{"Type", "constant"}}},
288 {"Zero_Displacement", true}}}};
289}
290
291// Trask: Single_Particle plate; between notches u=<0,-v t> (two BC sets — one scalar du each).
292json buildTraskInputJson(const std::string &output_path,
293 const std::filesystem::path &mesh_plate, double W, double H,
294 double notch_half, double notch_w, double notch_depth, double mesh_size,
295 double horizon, double rho, double E, double K, double G, double Gc,
296 double v_impact, double final_time, size_t num_steps) {
298 json{{"Dimension", 2},
299 {"Final_Time", final_time},
300 {"Time_Steps", num_steps},
301 {"Discretization_Type", json{{"Spatial", "finite_difference"}, {"Time", "central_difference"}}},
302 {"Populate_ElementNodeConnectivity", true},
303 {"Quad_Approximation_Order", 2},
304 {"Particle_Sim_Type", "Single_Particle"},
305 {"Seed", 0}});
306 // Trask §5: broken bond → weight 0 (no force). No self-contact.
307 model["Self_Contact"] = "none";
308 model["Bond_Break"] = "tension"; // literature PMB: break in tension only
309
311 json{{"File_Format", "vtu"},
312 {"Path", output_path},
313 {"Tags", std::vector<std::string>({"Displacement", "Velocity", "Force", "Damage", "Damage_Z",
314 "Particle_ID", "Fixity"})},
315 {"Output_Interval", std::max<size_t>(1, num_steps / 10)},
316 {"Debug", 1},
317 {"Perform_FE_Out", false},
318 {"Compress_Type", "zlib"},
319 {"Perform_Out", true},
320 {"Test_Output_Interval", num_steps},
321 {"Tag_PP", ""},
322 {"PVD_Collection", false}});
323
324 // Trask §6.2: top left/right of notches u=<0,0>; drive between notches
325 // u=<0,-v t>; sides and bottom free (collar bond-break is not implemented).
326 const double drive_h = std::max(2.0 * mesh_size, 0.002);
327 const std::vector<double> drive_strip{-notch_half, 0.5 * H - drive_h, 0., notch_half,
328 0.55 * H, 0.};
329 json outer = fixedTopOuterBC(W, H, notch_half);
330 json bc_disp = {
331 {"Sets", 4},
332 {"Set_1", outer["Set_1"]},
333 {"Set_2", outer["Set_2"]},
334 {"Set_3",
335 {{"Particle_List", std::vector<size_t>{0}},
336 {"Region", {{"Geometry", {{"Type", "rectangle"}, {"Parameters", drive_strip}}}}},
337 {"Direction", std::vector<size_t>{1}},
338 {"Time_Function", {{"Type", "constant"}, {"Parameters", std::vector<double>{0.}}}},
339 {"Spatial_Function", {{"Type", "constant"}}},
340 {"Zero_Displacement", true}}},
341 {"Set_4",
342 {{"Particle_List", std::vector<size_t>{0}},
343 {"Region", {{"Geometry", {{"Type", "rectangle"}, {"Parameters", drive_strip}}}}},
344 {"Direction", std::vector<size_t>{2}},
345 {"Time_Function",
346 {{"Type", "linear"}, {"Parameters", std::vector<double>{-v_impact}}}},
347 {"Spatial_Function", {{"Type", "constant"}}}}}};
348
349 geom::GeomData plate =
350 sillingNotchedPlateGeom(W, H, notch_half, notch_w, notch_depth);
352 json mesh = {{"Sets", 1}, {"Set_1", meshSetJson(mesh_plate, mesh_size)}};
355 json{{"Type", "PMBBond"},
356 {"Is_Plane_Strain", false},
357 {"Horizon", horizon},
358 {"Density", rho},
359 {"K", K},
360 {"G", G},
361 {"Gc", Gc},
362 {"E", E},
363 {"Compute_From_Classical", true},
364 {"Influence_Function", json{{"Type", 0}}}});
365 // Trask/Silling ω≡1 (ConstInfluence default a0=dim+1 would mis-scale c)
366 material["Set_1"]["Influence_Function"] = {
367 {"Type", 0}, {"Parameters", std::vector<double>{1.0}}};
368
369 return json{{"Model", model},
370 {"Output", output},
371 {"Displacement_BC", bc_disp},
372 {"Particle", particle},
373 {"Mesh", mesh},
374 {"Material", material}};
375}
376
377json buildImpactInputJson(const std::string &output_path,
378 const std::filesystem::path &mesh_plate,
379 const std::filesystem::path &mesh_impactor, double W, double H,
380 double notch_half, double notch_w, double notch_depth, double Iw,
381 double Ih, double gap, double mesh_size, double horizon,
382 double Rc_factor, double Kn, double rho, double E, double K, double G,
383 double Gc, double v_impact, double final_time, size_t num_steps,
384 double thickness = 0.) {
385 // Silling 2003 EMU KW: explicit central difference; brittle-microelastic PMB;
386 // broken bond force = 0; plate boundaries load-free (no Displacement_BC);
387 // rigid impactor of finite mass.
388 // thickness > 0 runs his actual 3D case (200 x 100 x 9 grid, 9 mm plate).
389 const bool dim3 = thickness > 0.;
390 const size_t dim = dim3 ? 3 : 2;
391 // Element-node connectivity is only used for strain/stress output and does not
392 // support hexahedra, which is what the 3D structured grid produces.
394 json{{"Dimension", dim},
395 {"Final_Time", final_time},
396 {"Time_Steps", num_steps},
397 {"Discretization_Type", json{{"Spatial", "finite_difference"}, {"Time", "central_difference"}}},
398 {"Populate_ElementNodeConnectivity", !dim3},
399 {"Quad_Approximation_Order", 2},
400 {"Particle_Sim_Type", "Multi_Particle"},
401 {"Seed", 0}});
402 model["Self_Contact"] = "none";
403 model["Bond_Break"] = "tension";
404 model["Wall_Contact"] = "meshed";
405 // Silling Fig. 2: rigid cylinder of 1.57 kg. In 2D the analogue is the mass
406 // per unit thickness of his 9 mm plate, M/t = 1.57/0.009 kg/m.
407 model["Rigid_Particles"] = json::array(
408 {json{{"Id", 1}, {"Mass", dim3 ? 1.57 : 1.57 / 0.009}}});
409
411 json{{"File_Format", "vtu"},
412 {"Path", output_path},
413 {"Tags", std::vector<std::string>({"Displacement", "Velocity", "Force", "Damage", "Damage_Bond",
414 "Damage_Z", "Particle_ID"})},
415 {"Output_Interval", std::max<size_t>(1, num_steps / 10)},
416 {"Debug", 1},
417 {"Perform_FE_Out", false},
418 {"Compress_Type", "zlib"},
419 {"Perform_Out", true},
420 {"Test_Output_Interval", num_steps},
421 {"Tag_PP", ""},
422 {"PVD_Collection", false}});
423
424 json ic = {
425 {"Sets", 1},
426 {"Set_1",
427 {{"Particle_List", std::vector<size_t>{1}},
428 {"Constant_Velocity",
429 {{"Velocity_Vector", std::vector<double>{0., -v_impact, 0.}}}}}}};
430
431 // The impactor is rigid but of finite mass (Model.Rigid_Particles above), so
432 // it decelerates on contact instead of being driven through the plate.
433 json bc_disp = {{"Sets", 1},
434 {"Set_1",
435 {{"Particle_List", std::vector<size_t>{1}},
436 {"Direction", std::vector<size_t>{1}},
437 {"Time_Function",
438 {{"Type", "constant"}, {"Parameters", std::vector<double>{0.}}}},
439 {"Spatial_Function", {{"Type", "constant"}}},
440 {"Zero_Displacement", true}}}};
441
442 // Plate is the full box on a structured grid with the two 1.5 mm notch slots
443 // removed from the mesh, so they are real gaps (Fig. 2) rather than material
444 // whose bonds have been cut.
445 geom::GeomData plate;
446 geom::GeomData impactor;
447 if (dim3) {
448 // Keep both bodies centred on z = 0 so particle generation places them
449 // consistently (it positions objects by their centre).
450 plate.d_geomName = "cuboid";
451 plate.d_geomParams = {-0.5 * W, -0.5 * H, -0.5 * thickness,
452 0.5 * W, 0.5 * H, 0.5 * thickness};
453 // Silling Fig. 4: the cylinder axis is along the impact direction and its
454 // flat end face strikes the plate edge. Params: radius, centre of the
455 // beginning cross-section, and the vector to the end cross-section.
456 impactor.d_geomName = "cylinder";
457 impactor.d_geomParams = {0.5 * Iw, 0., -0.5 * Ih, 0., 0., Ih, 0.};
458 } else {
459 plate.d_geomName = "rectangle";
460 plate.d_geomParams = {-0.5 * W, -0.5 * H, 0., 0.5 * W, 0.5 * H, 0.};
461 // In-plane section of that same cylinder: Iw wide, Ih long, flat face down.
462 impactor.d_geomName = "rectangle";
463 impactor.d_geomParams = {-0.5 * Iw, -0.5 * Ih, 0., 0.5 * Iw, 0.5 * Ih, 0.};
464 }
466
467 const auto voids =
468 notchVoidBoxes(H, notch_half, notch_w, notch_depth,
469 dim3 ? -0.5 * thickness - 1.0e-9 : -1.0e-9,
470 dim3 ? 0.5 * thickness + 1.0e-9 : 1.0e-9);
471 // Plate on Silling's equally spaced structured grid. In 2D the impactor is a
472 // rectangle so it goes on the same grid; in 3D it is a cylinder, which the
473 // uniform grid cannot represent, so it is meshed by Gmsh at the same spacing.
474 json mesh = {{"Sets", 2},
475 {"Set_1", uniformMeshSetJson(mesh_plate, mesh_size, voids)},
476 {"Set_2", dim3 ? meshSetJson(mesh_impactor, mesh_size)
477 : uniformMeshSetJson(mesh_impactor, mesh_size)}};
478
479 // Impactor is steel like the plate but unbreakable, and its nodal forces are
480 // replaced by the rigid-body acceleration each step, so its bond stiffness
481 // never enters the solution.
484 json{{"Type", "PMBBond"},
485 {"Is_Plane_Strain", false},
486 {"Horizon", horizon},
487 {"Density", rho},
488 {"K", K},
489 {"G", G},
490 {"Gc", Gc},
491 {"E", E},
492 {"Compute_From_Classical", true},
493 {"Influence_Function", json{{"Type", 0}}}});
495 json{{"Type", "PDElasticBond"},
496 {"Is_Plane_Strain", false},
497 {"Horizon", horizon},
498 {"Density", rho},
499 {"K", K},
500 {"G", G},
501 {"Gc", 0.},
502 {"E", E},
503 {"Compute_From_Classical", true},
504 {"Influence_Function", json{{"Type", 0}}}});
505 material["Set_1"]["Influence_Function"] = {
506 {"Type", 0}, {"Parameters", std::vector<double>{1.0}}};
507 material["Set_2"]["Influence_Function"] = {
508 {"Type", 0}, {"Parameters", std::vector<double>{1.0}}};
509
510 // Absolute Contact_Radius from the requested mesh size (not Factor×hMin).
511 const double Rc_abs = Rc_factor * mesh_size;
513 json{{"Contact_Radius", Rc_abs},
514 {"Kn", Kn},
515 {"Damping_On", false},
516 {"Epsilon", 1.0},
517 {"Friction_On", false},
518 {"Friction_Coeff", 0.0},
519 {"Kn_Factor", 1.0},
520 {"Beta_n_Factor", 0.0},
521 {"K", K}});
522 contact_base["Kn"] = Kn;
524 contact["Set_1_1"] = contact_base;
525 contact["Set_1_2"] = contact_base;
526 contact["Set_2_2"] = contact_base;
527 contact["Damping_Law"] = "off";
528 contact["Friction_Law"] = "coulomb_simple";
529
530 const double cy_imp = 0.5 * H + 0.5 * Ih + gap; // flat face `gap` above the edge
531 auto pgen = inp::PGenDeck::getExampleJson(json{{"Method", "From_File"}});
532 pgen["Random_Rotation"] = false;
533 pgen["Data"]["N"] = 2;
534 pgen["Data"]["0"] = json{{"x", 0.},
535 {"y", 0.},
536 {"z", 0.},
537 {"theta", 0.},
538 {"s", 1.},
539 {"geom_id", 0},
540 {"mat_id", 0},
541 {"contact_id", 0}};
542 pgen["Data"]["1"] = json{{"x", 0.},
543 {"y", cy_imp},
544 {"z", 0.},
545 {"theta", 0.},
546 {"s", 1.},
547 {"geom_id", 1},
548 {"mat_id", 1},
549 {"contact_id", 1}};
550
551 // Plate has no Displacement_BC (Silling: load-free all around); the only
552 // Displacement_BC drives the rigid impactor.
553 return json{{"Model", model},
554 {"Output", output},
555 {"Displacement_BC", bc_disp},
556 {"IC", ic},
557 {"Particle", particle},
558 {"Mesh", mesh},
559 {"Material", material},
560 {"Contact", contact},
562 json{{"Update_Criteria", "simple_all"},
563 {"Search_Factor", 5.0},
564 {"Search_Interval", 1},
565 {"Near_Bd_Nodes_Tol", 0.5}})},
566 {"Particle_Generation", pgen}};
567}
568
569// Bhat 2023 §6.2: V-notched plate, free impactor, reference_gap self-contact.
570json buildBhatInputJson(const std::string &output_path,
571 const std::filesystem::path &mesh_plate,
572 const std::filesystem::path &mesh_impactor, double W, double H,
573 double notch_half, double notch_w, double notch_depth, double Iw,
574 double Ih, double gap, double mesh_size, double horizon,
575 double Rc_factor, double rho, double E, double K, double G, double Gc,
576 double v_impact, double final_time, size_t num_steps) {
578 json{{"Dimension", 2},
579 {"Final_Time", final_time},
580 {"Time_Steps", num_steps},
581 {"Discretization_Type", json{{"Spatial", "finite_difference"}, {"Time", "velocity_verlet"}}},
582 {"Populate_ElementNodeConnectivity", true},
583 {"Quad_Approximation_Order", 2},
584 {"Particle_Sim_Type", "Multi_Particle"},
585 {"Seed", 0}});
586 model["Self_Contact"] = "reference_gap"; // Bhat 2023 §4.4
587 model["Bond_Break"] = "absolute_stretch"; // Bhat §3.1: |s| > |s0|
588 model["Wall_Contact"] = "meshed";
589
591 json{{"File_Format", "vtu"},
592 {"Path", output_path},
593 {"Tags", std::vector<std::string>({"Displacement", "Velocity", "Force", "Damage", "Damage_Bond",
594 "Damage_Z", "Particle_ID"})},
595 {"Output_Interval", std::max<size_t>(1, num_steps / 10)},
596 {"Debug", 1},
597 {"Perform_FE_Out", false},
598 {"Compress_Type", "zlib"},
599 {"Perform_Out", true},
600 {"Test_Output_Interval", num_steps},
601 {"Tag_PP", ""},
602 {"PVD_Collection", false}});
603
604 json ic = {
605 {"Sets", 1},
606 {"Set_1",
607 {{"Particle_List", std::vector<size_t>{1}},
608 {"Constant_Velocity",
609 {{"Velocity_Vector", std::vector<double>{0., -v_impact, 0.}}}}}}};
610
611 // Paper: only the outer top ligaments are fixed; impactor is free.
612 json outer = fixedTopOuterBC(W, H, notch_half);
613 json bc_disp = {{"Sets", 2}, {"Set_1", outer["Set_1"]}, {"Set_2", outer["Set_2"]}};
614
615 // Bhat specimen: OCC/Gmsh nonconvex plate with V-notch cutouts (§5, Fig. 4).
616 geom::GeomData plate =
617 bhatVNotchedPlateGeom(W, H, notch_half, notch_w, notch_depth);
618 geom::GeomData impactor;
619 impactor.d_geomName = "rectangle";
620 impactor.d_geomParams = {-0.5 * Iw, -0.5 * Ih, 0., 0.5 * Iw, 0.5 * Ih, 0.};
622
623 json mesh = {{"Sets", 2},
624 {"Set_1", meshSetJson(mesh_plate, mesh_size)},
625 {"Set_2", meshSetJson(mesh_impactor, mesh_size)}};
626
627 // Bhat Table 1, constant micromodulus, ν = 1/3 (bond-based, 2D).
628 const double nu_bhat = 1.0 / 3.0;
629 const double c_bhat =
630 6.0 * E / (M_PI * std::pow(horizon, 3.0) * (1.0 - nu_bhat));
631 const double s0_bhat = std::sqrt(4.0 * M_PI * Gc / (9.0 * E * horizon));
632
634 auto mat_bhat = inp::MaterialDeck::getExampleJson(
635 json{{"Type", "PMBBond"},
636 {"Is_Plane_Strain", false},
637 {"Horizon", horizon},
638 {"Density", rho},
639 {"K", K},
640 {"G", G},
641 {"Gc", Gc},
642 {"E", E},
643 {"Compute_From_Classical", false},
644 {"Influence_Function", json{{"Type", 0}}}});
645 mat_bhat["Bond_Potential_Params"] = std::vector<double>{c_bhat, s0_bhat};
646 mat_bhat["Influence_Function"] = {{"Type", 0}, {"Parameters", std::vector<double>{1.0}}};
647 material["Set_1"] = mat_bhat;
648 // Free deformable striker (paper). Unbreakable elastic so the contact face
649 // delivers bulk impulse; plate remains PMB with paper (c, s0).
650 auto mat_strike = inp::MaterialDeck::getExampleJson(
651 json{{"Type", "PDElasticBond"},
652 {"Is_Plane_Strain", false},
653 {"Horizon", horizon},
654 {"Density", rho},
655 {"K", K},
656 {"G", G},
657 {"Gc", 0.},
658 {"E", E},
659 {"Compute_From_Classical", true},
660 {"Influence_Function", json{{"Type", 0}}}});
661 mat_strike["Influence_Function"] = {{"Type", 0}, {"Parameters", std::vector<double>{1.0}}};
662 material["Set_2"] = mat_strike;
663
664 // Absolute Rc from mesh_size (not Factor×hMin).
665 const double Kn_bhat = 18.0 * K / (M_PI * std::pow(horizon, 4.0));
666 const double Rc_abs = Rc_factor * mesh_size;
668 json{{"Contact_Radius", Rc_abs},
669 {"Kn", Kn_bhat},
670 {"Damping_On", false},
671 {"Epsilon", 1.0},
672 {"Friction_On", false},
673 {"Friction_Coeff", 0.0},
674 {"Kn_Factor", 1.0},
675 {"Beta_n_Factor", 0.0},
676 {"K", K}});
677 contact_base["Kn"] = Kn_bhat;
679 contact["Set_1_1"] = contact_base;
680 contact["Set_1_2"] = contact_base;
681 contact["Set_2_2"] = contact_base;
682 contact["Damping_Law"] = "off"; // Bhat §6: β_d = 0
683 contact["Friction_Law"] = "coulomb_simple"; // μ = 0 below
684 contact["Set_1_1"]["Friction_Coefficient"] = 0.;
685 contact["Set_1_2"]["Friction_Coefficient"] = 0.;
686 contact["Set_2_2"]["Friction_Coefficient"] = 0.;
687
688 const double cy_imp = 0.5 * H + 0.5 * Ih + gap;
689 auto pgen = inp::PGenDeck::getExampleJson(json{{"Method", "From_File"}});
690 pgen["Random_Rotation"] = false;
691 pgen["Data"]["N"] = 2;
692 pgen["Data"]["0"] = json{{"x", 0.}, {"y", 0.}, {"z", 0.},
693 {"theta", 0.}, {"s", 1.}, {"geom_id", 0},
694 {"mat_id", 0}, {"contact_id", 0}};
695 pgen["Data"]["1"] = json{{"x", 0.}, {"y", cy_imp}, {"z", 0.},
696 {"theta", 0.}, {"s", 1.}, {"geom_id", 1},
697 {"mat_id", 1}, {"contact_id", 1}};
698
699 return json{{"Model", model},
700 {"Output", output},
701 {"Displacement_BC", bc_disp},
702 {"IC", ic},
703 {"Particle", particle},
704 {"Mesh", mesh},
705 {"Material", material},
706 {"Contact", contact},
708 json{{"Update_Criteria", "simple_all"},
709 {"Search_Factor", 5.0},
710 {"Search_Interval", 1},
711 {"Near_Bd_Nodes_Tol", 0.5}})},
712 {"Particle_Generation", pgen}};
713}
714
715} // namespace
716
717int main(int argc, char *argv[]) {
718 util::parallel::initMpi(argc, argv);
719 util::io::InputParser input(argc, argv);
720
721 unsigned n_threads = 1;
722 if (input.cmdOptionExists("-nThreads"))
723 n_threads = static_cast<unsigned>(std::stoi(input.getCmdOption("-nThreads")));
725
726 const bool quick = input.cmdOptionExists("-quick");
727 const bool trask = input.cmdOptionExists("-traskDisp");
728 const bool bhat = input.cmdOptionExists("-bhatKW");
729 // Silling's actual case is 3D: a 9 mm plate on a 200 x 100 x 9 grid.
730 const bool dim3 = input.cmdOptionExists("-dim3");
731
732 namespace fs = std::filesystem;
733 // Always write under the binary cwd (build/linux/.../notched_impact_inbuilt/) unless overridden.
734 std::string run_tag =
735 quick ? "quick" : (trask ? "trask" : (bhat ? "bhat" : (dim3 ? "silling3d" : "lit")));
736 // New campaign outputs go under runs_new/ (do not mix with archived runs/).
737 fs::path base = fs::current_path() / "runs_new" / run_tag;
738 if (input.cmdOptionExists("-outputDir"))
739 base = input.getCmdOption("-outputDir");
740 const fs::path out_dir = base / "out";
741 const fs::path inp_dir = base / "inp";
742 fs::create_directories(out_dir);
743 fs::create_directories(inp_dir);
744 std::cout << std::format("notched_impact: outputDir={}\n", base.string());
745
746 // Fig. 2 Silling 2003: 200×100 mm plate, two 1.5 mm notches depth 50 mm, tip-to-tip 50 mm.
747 double W = 0.200;
748 double H = 0.100;
749 double notch_depth = 0.050;
750 double notch_half = 0.025; // notch centerline ±25 mm
751 double notch_w = 0.0015; // Silling 1.5 mm open gap
752 double plate_thickness = 0.009; // Silling Fig. 2: 9 mm (only used with -dim3)
753 // Impactor: Silling Fig. 2/4 give a cylinder of 1.57 kg striking edge-on, as
754 // wide as the 50 mm ligament between the notches. At ρ = 8000 that mass fixes
755 // its length: 1.57/(8000·π·0.025²) = 0.100 m. Bhat Fig. 4(b) draws exactly
756 // this 50 × 100 mm section, giving the same width and length.
757 double Iw = 0.050;
758 double Ih = 0.100;
759 // Silling 2003 EMU grid 200×100×9 on the mm plate → h ≈ 1 mm; Trask KW: δ = 3h.
760 double mesh_size = 0.001;
761 double horizon = 3.0 * mesh_size; // 3 mm
762 double Rc_factor = 0.95;
763 // The impactor must start outside the contact radius, which the solver sets to
764 // Rc_factor·h_min: any closer and the first step applies a saturated repulsive
765 // force that fires the struck nodes off at kilometres per second. Nothing acts
766 // on the impactor while it closes this gap, so its only effect is to delay
767 // contact by gap/v, which the crack-speed fit measures from damage arrival.
768 double gap = 1.5 * Rc_factor * mesh_size;
769
770 // Table 2 M1 ≡ Silling maraging steel (E=191 GPa, ν≈0.3).
771 // Gc from KIc≈90 MPa√m: plane-stress KIc²/E = 42408 J/m² (Trask/Silling handbook).
772 double rho = 8000.0;
773 double E = 191.0e9;
774 double Kbulk = 159.2e9;
775 double nu = 0.5 * (1.0 - E / (3.0 * Kbulk));
776 double G = E / (2.0 * (1.0 + nu));
777 double Gc = 42408.0;
778 double v_impact = 32.0;
779 double dt = 2.5e-9; // Bhat §6.1
780 // Silling's cracks run all the way to the free edges (75 mm) at ~900 m/s, so
781 // ~85 µs of propagation on top of the free flight and the initiation delay.
782 double final_time = 1.7e-4; // 170 µs
783 if (input.cmdOptionExists("-Gc"))
784 Gc = std::stod(input.getCmdOption("-Gc"));
785 if (input.cmdOptionExists("-finalTime"))
786 final_time = std::stod(input.getCmdOption("-finalTime"));
787 if (input.cmdOptionExists("-vImpact"))
788 v_impact = std::stod(input.getCmdOption("-vImpact"));
789 if (input.cmdOptionExists("-meshSize")) {
790 mesh_size = std::stod(input.getCmdOption("-meshSize"));
791 horizon = 3.0 * mesh_size;
792 }
793 if (input.cmdOptionExists("-horizon"))
794 horizon = std::stod(input.getCmdOption("-horizon"));
795 if (input.cmdOptionExists("-horizonFactor"))
796 horizon = std::stod(input.getCmdOption("-horizonFactor")) * mesh_size;
797
798 if (quick) {
799 W = 0.040;
800 H = 0.020;
801 notch_depth = 0.5 * H;
802 notch_half = 0.125 * W;
803 mesh_size = H / 16.0;
804 notch_w = std::max(mesh_size, 0.1 * notch_half);
805 Iw = 0.008;
806 Ih = 0.004;
807 gap = 1.5 * Rc_factor * mesh_size;
808 horizon = 3.0 * mesh_size;
809 rho = 1200.0;
810 E = 1.23e9;
811 Kbulk = 2.0e9;
812 nu = 0.5 * (1.0 - E / (3.0 * Kbulk));
813 G = E / (2.0 * (1.0 + nu));
814 Gc = 424.0;
815 dt = 1.0e-8;
816 final_time = 1.0e-4;
817 }
818
819 // Contact stiffness, Bhat Eq. (4.1). The solver's contact force density is
820 // Kn·V_j·overlap, so Kn carries one power of δ per spatial dimension of the
821 // node weight: 18k/(πδ⁴) with 2D weights h²·1 m, 18k/(πδ⁵) with 3D weights h³.
822 // Using the 3D form in 2D makes contact 1/δ ≈ 333× too stiff.
823 const double Kn =
824 util::normalContactStiffness(Kbulk, Kbulk, horizon, dim3 ? 5 : 4);
825 const size_t num_steps = static_cast<size_t>(std::llround(final_time / dt));
826
827 json input_json;
828 if (trask) {
829 input_json = buildTraskInputJson(directoryPathWithTrailingSep(out_dir),
830 inp_dir / "mesh_plate.msh", W, H, notch_half, notch_w,
831 notch_depth, mesh_size, horizon, rho, E, Kbulk, G, Gc,
832 v_impact, final_time, num_steps);
833 } else if (bhat) {
834 input_json = buildBhatInputJson(
835 directoryPathWithTrailingSep(out_dir), inp_dir / "mesh_plate.msh",
836 inp_dir / "mesh_impactor.msh", W, H, notch_half, notch_w, notch_depth, Iw, Ih, gap,
837 mesh_size, horizon, Rc_factor, rho, E, Kbulk, G, Gc, v_impact, final_time, num_steps);
838 } else {
839 input_json = buildImpactInputJson(
840 directoryPathWithTrailingSep(out_dir), inp_dir / "mesh_plate.msh",
841 inp_dir / "mesh_impactor.msh", W, H, notch_half, notch_w, notch_depth, Iw, Ih, gap,
842 mesh_size, horizon, Rc_factor, Kn, rho, E, Kbulk, G, Gc, v_impact, final_time,
843 num_steps, dim3 ? plate_thickness : 0.);
844 }
845 // Optional override (e.g. Bhat paper default is reference_gap; -selfContact none
846 // isolates prenotch+void setups from restorative self-contact across the notch).
847 if (input.cmdOptionExists("-selfContact")) {
848 const auto sc = input.getCmdOption("-selfContact");
849 input_json["Model"]["Self_Contact"] = sc;
850 std::cout << std::format("notched_impact: Self_Contact override -> {}\n", sc);
851 }
852 if (input.cmdOptionExists("-bondBreak")) {
853 const auto bb = input.getCmdOption("-bondBreak");
854 input_json["Model"]["Bond_Break"] = bb;
855 std::cout << std::format("notched_impact: Bond_Break override -> {}\n", bb);
856 }
857 // The deck fixes Output_Interval at num_steps/10, which is right for a
858 // check run and far too coarse for a frame sequence. Overriding it costs
859 // nothing and leaves the physics untouched.
860 if (input.cmdOptionExists("-outInterval")) {
861 const size_t oi =
862 static_cast<size_t>(std::stoull(input.getCmdOption("-outInterval")));
863 input_json["Output"]["Output_Interval"] = std::max<size_t>(1, oi);
864 std::cout << std::format("notched_impact: Output_Interval override -> {}\n", oi);
865 }
866 if (input.cmdOptionExists("-knScale")) {
867 const double s = std::stod(input.getCmdOption("-knScale"));
868 for (auto &kv : input_json["Contact"].items()) {
869 if (!kv.value().is_object() || !kv.value().contains("Kn"))
870 continue;
871 kv.value()["Kn"] = kv.value()["Kn"].get<double>() * s;
872 }
873 std::cout << std::format("notched_impact: Contact Kn scaled by {}\n", s);
874 }
875 {
876 std::ofstream os(inp_dir / "input.json");
877 os << input_json.dump(2);
878 }
879
880 // -deckOnly stops after the deck is written. The Python comparison reads
881 // the deck and does not need this driver to run the simulation.
882 if (input.cmdOptionExists("-deckOnly")) {
883 util::io::print("deck written; -deckOnly, not running\n");
885 return 0;
886 }
887
888 auto deck = std::make_shared<inp::Input>(input_json);
889 PeriDEMModel dem(deck);
890 dem.init();
891
892 const double y_top = 0.5 * H;
893 const double y_tip = y_top - notch_depth;
894 // Prenotch bonds: Bhat §5 removes bonds whose segment leaves the nonconvex
895 // V-domain (midplane cuts through each V tip); Silling/Trask: slot spans.
896 const size_t n_pre =
897 bhat ? applyNotchMidplanes(dem, notch_half, y_tip, y_top + 0.01 * H)
898 : applyNotchSlots(dem, notch_half, notch_w, y_tip, y_top + 0.01 * H);
899 if (n_pre < 10) {
900 std::cerr << "notched_impact: expected prenotch bonds, got " << n_pre << "\n";
901 return 1;
902 }
903
904 const char *mode_str =
905 quick ? "quick"
906 : (trask ? "trask_disp"
907 : (bhat ? "bhat_kw" : (dim3 ? "silling_3d" : "silling_impact")));
908 std::cout << std::format(
909 "notched_impact: mode={} nodes={} prenotch={} h={:.4e} ε={:.4e} notch_w={:.4e} "
910 "Gc={:.0f} E={:.3e} v={:.1f} Nt={} T={:.3e}\n",
911 mode_str, dem.d_x.size(), n_pre, mesh_size, horizon, notch_w, Gc, E, v_impact, num_steps,
912 final_time);
913
914 // Drive the loop ourselves (same sequence as time_int::Integrator::integrate)
915 // so we can record when each node first becomes damaged. That arrival-time
916 // field gives the crack speed, which Silling reports as ~900 m/s.
917 std::vector<float> arrival(dem.d_x.size(), -1.f);
918 const size_t sample_every = std::max<size_t>(1, num_steps / 400);
919 auto sampleArrivals = [&]() {
920 for (size_t i = 0; i < dem.d_xRef.size(); ++i) {
921 if (arrival[i] >= 0.f || dem.d_ptId[i] != 0)
922 continue;
923 if (nodePhi(dem, i) >= 0.30)
924 arrival[i] = static_cast<float>(dem.d_time);
925 }
926 };
927
929 if (dem.performOutput())
930 dem.output();
931 dem.setCurrentDt(dem.timeStep());
933 dem.computeForces();
935 while (dem.currentStep() < dem.numTimeSteps()) {
936 dem.integrateStep();
937 if (dem.shouldOutput())
938 dem.output();
939 if (dem.currentStep() % sample_every == 0)
940 sampleArrivals();
941 dem.checkStop();
942 }
943 sampleArrivals();
944
945 double vmax = 0., phi_max = 0.;
946 size_t n_phi30 = 0;
947 for (size_t i = 0; i < dem.d_v.size(); ++i) {
948 vmax = std::max(vmax, dem.d_v[i].length());
949 if (dem.d_ptId[i] != 0)
950 continue;
951 const double phi = nodePhi(dem, i);
952 phi_max = std::max(phi_max, phi);
953 if (phi >= 0.30)
954 ++n_phi30;
955 }
956 const float zmax =
957 dem.d_Z.empty() ? 0.f : *std::max_element(dem.d_Z.begin(), dem.d_Z.end());
958 std::cout << std::format(
959 "notched_impact diag: max|v|={:.3e} maxφ={:.3f} maxZ={:.3f} n(φ≥0.3)={}\n", vmax,
960 phi_max, zmax, n_phi30);
961
962 if (!(vmax < 8.0e3) || !std::isfinite(vmax)) {
963 std::cerr << "notched_impact: blow-up, max |v| = " << vmax << "\n";
964 return 1;
965 }
966
967 size_t n_extra = 0;
968 for (size_t i = 0; i < dem.d_neighPd.size(); ++i) {
969 if (dem.d_ptId[i] != 0)
970 continue;
971 for (size_t k = 0; k < dem.d_neighPd[i].size(); ++k) {
972 if (!dem.d_fracture_p->getBondState(i, k))
973 continue;
974 const size_t j = dem.d_neighPd[i][k];
975 if (dem.d_ptId[j] != 0)
976 continue;
977 const auto &xi = dem.d_xRef[i];
978 const auto &xj = dem.d_xRef[j];
979 const bool on_notch =
980 geometry::segmentCrossesVerticalLine(xi.d_x, xi.d_y, xj.d_x, xj.d_y,
981 -notch_half, y_tip,
982 y_top + 0.01 * H) ||
983 geometry::segmentCrossesVerticalLine(xi.d_x, xi.d_y, xj.d_x, xj.d_y,
984 notch_half, y_tip,
985 y_top + 0.01 * H);
986 if (!on_notch)
987 ++n_extra;
988 }
989 }
990 if (n_extra < 20) {
991 std::cerr << "notched_impact: expected new broken bonds, got " << n_extra
992 << " (prenotch=" << n_pre << ")\n";
993 return 1;
994 }
995
996 // Dump the damage field so the crack-path metric can be re-fitted offline
997 // without repeating the simulation.
998 {
999 std::ofstream csv(base / "damage.csv");
1000 csv << "x_ref,y_ref,z_ref,x_cur,y_cur,z_cur,phi,phi_bond,arrival\n";
1001 for (size_t i = 0; i < dem.d_xRef.size(); ++i) {
1002 if (dem.d_ptId[i] != 0)
1003 continue;
1004 const double phi = nodeDamageForFit(dem, i, /*use_bond_count=*/false);
1005 const double phi_b = nodeDamageForFit(dem, i, /*use_bond_count=*/true);
1006 if (std::max(phi, phi_b) < 0.05)
1007 continue;
1008 csv << std::format("{:.6e},{:.6e},{:.6e},{:.6e},{:.6e},{:.6e},{:.4f},{:.4f},{:.6e}\n",
1009 dem.d_xRef[i].d_x, dem.d_xRef[i].d_y, dem.d_xRef[i].d_z,
1010 dem.d_x[i].d_x, dem.d_x[i].d_y, dem.d_x[i].d_z, phi, phi_b,
1011 double(arrival[i]));
1012 }
1013 }
1014
1015 const double band_x = 0.45 * W;
1016 const double band_y = 0.55 * H;
1017 const double phi_cut = quick ? 0.20 : 0.35;
1018 const double exclude_r = 2.0 * mesh_size;
1019 const bool use_bond_dmg = bhat;
1020 auto left = fitCrackFromPhi(dem, -notch_half, y_tip, -1.0, phi_cut, band_x, band_y, exclude_r,
1021 mesh_size, use_bond_dmg);
1022 auto right = fitCrackFromPhi(dem, notch_half, y_tip, +1.0, phi_cut, band_x, band_y, exclude_r,
1023 mesh_size, use_bond_dmg);
1024 if (left.n_pts < 4 || right.n_pts < 4) {
1025 std::cerr << "notched_impact: insufficient φ-ridge for path fit (L=" << left.n_pts
1026 << " R=" << right.n_pts << ")\n";
1027 return 1;
1028 }
1029 if (!(left.mean_dx < 0. && left.mean_dy > 0. && right.mean_dx > 0. && right.mean_dy > 0.)) {
1030 std::cerr << std::format(
1031 "notched_impact: tip damage not outward/down "
1032 "(L dx={:.3e} dy={:.3e} R dx={:.3e} dy={:.3e})\n",
1033 left.mean_dx, left.mean_dy, right.mean_dx, right.mean_dy);
1034 return 1;
1035 }
1036
1037 const double ang = 0.5 * (left.angle_to_notch_deg + right.angle_to_notch_deg);
1038 std::cout << std::format(
1039 "notched_impact PASS: prenotch={} new_broken={} ang_to_notch={:.1f}° "
1040 "(L={:.1f}/{} R={:.1f}/{}) max|v|={:.3e}\n",
1041 n_pre, n_extra, ang, left.angle_to_notch_deg, left.n_pts, right.angle_to_notch_deg,
1042 right.n_pts, vmax);
1043
1044 dem.close();
1045 return 0;
1046}
int main(int argc, char *argv[])
Definition main.cpp:29
void applyInitialCondition()
void applyDisplacementBC()
std::vector< util::Point > d_x
Current positions of the nodes.
Definition modelData.h:745
std::vector< util::Point > d_xRef
reference positions of the nodes
Definition modelData.h:742
std::vector< float > d_Z
Damage at nodes.
Definition modelData.h:818
size_t currentStep() const
Definition modelData.h:164
std::unique_ptr< geometry::Fracture > d_fracture_p
Fracture state of bonds.
Definition modelData.h:736
double d_time
Current time.
Definition modelData.h:595
std::vector< size_t > d_ptId
Global node to particle id (walls are assigned id after last particle id)
Definition modelData.h:764
std::vector< float > d_phi
Damage function at the nodes (volume-weighted, Silling 2000)
Definition modelData.h:834
std::vector< std::vector< size_t > > d_neighPd
Neighbor data for peridynamic forces.
Definition modelData.h:770
size_t numTimeSteps() const
Definition modelData.h:166
void setCurrentDt(double dt)
Definition modelData.h:145
double timeStep() const
Definition modelData.h:141
bool shouldOutput() const
Definition modelData.h:174
std::vector< util::Point > d_v
Velocity of the nodes.
Definition modelData.h:751
std::vector< float > d_phiBond
Damage as broken-bond count fraction (Bhattacharya & Lipton 2023)
Definition modelData.h:837
bool performOutput() const
Definition modelData.h:172
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
json buildBhatInputJson(const std::string &output_path, const std::filesystem::path &mesh_plate, const std::filesystem::path &mesh_impactor, double W, double H, double notch_half, double notch_w, double notch_depth, double Iw, double Ih, double gap, double mesh_size, double horizon, double Rc_factor, double rho, double E, double K, double G, double Gc, double v_impact, double final_time, size_t num_steps)
Definition main.cpp:570
size_t applyNotchMidplanes(PeriDEMModel &dem, double notch_half, double y_tip, double y_top)
Definition main.cpp:57
json buildTraskInputJson(const std::string &output_path, const std::filesystem::path &mesh_plate, double W, double H, double notch_half, double notch_w, double notch_depth, double mesh_size, double horizon, double rho, double E, double K, double G, double Gc, double v_impact, double final_time, size_t num_steps)
Definition main.cpp:292
size_t applyNotchSlots(PeriDEMModel &dem, double notch_half, double notch_w, double y_lo, double y_hi)
Definition main.cpp:48
json buildImpactInputJson(const std::string &output_path, const std::filesystem::path &mesh_plate, const std::filesystem::path &mesh_impactor, double W, double H, double notch_half, double notch_w, double notch_depth, double Iw, double Ih, double gap, double mesh_size, double horizon, double Rc_factor, double Kn, double rho, double E, double K, double G, double Gc, double v_impact, double final_time, size_t num_steps, double thickness=0.)
Definition main.cpp:377
double nodePhi(const PeriDEMModel &dem, size_t i)
Definition main.cpp:79
json meshSetJson(const std::filesystem::path &f, double mesh_size)
Definition main.cpp:183
std::vector< std::vector< double > > notchVoidBoxes(double H, double notch_half, double notch_w, double notch_depth, double z_lo, double z_hi)
Definition main.cpp:207
json uniformMeshSetJson(const std::filesystem::path &f, double mesh_size, const std::vector< std::vector< double > > &voids={})
Definition main.cpp:195
CrackFit fitCrackFromPhi(const PeriDEMModel &dem, double tip_x, double tip_y, double outward_sign, double phi_cut, double band_x, double band_y, double exclude_r, double bin_h, bool use_bond_count=false)
Definition main.cpp:110
size_t applyNotches(PeriDEMModel &dem, double x_left, double x_right, double y_lo, double y_hi)
Definition main.cpp:41
double nodeDamageForFit(const PeriDEMModel &dem, size_t i, bool use_bond_count)
Definition main.cpp:91
geom::GeomData bhatVNotchedPlateGeom(double W, double H, double notch_half, double notch_w, double notch_depth)
Definition main.cpp:241
geom::GeomData sillingNotchedPlateGeom(double W, double H, double notch_half, double notch_w, double notch_depth)
Definition main.cpp:219
json fixedTopOuterBC(double W, double H, double notch_half)
Definition main.cpp:262
bool segmentCrossesVerticalLine(double x0, double y0, double x1, double y1, double xLine, double yLo, double yHi)
Pre-notch by breaking peridynamic bonds.
Definition prenotch.cpp:52
std::size_t breakBondsCrossingVerticalLines(Fracture &fracture, const std::vector< util::Point > &xRef, const std::vector< std::vector< std::size_t > > &neighbors, const std::vector< std::size_t > &nodeParticleId, std::size_t particleId, const std::vector< double > &xLines, double yLo, double yHi)
Break the bonds of one particle that cross vertical cut lines.
Definition prenotch.cpp:84
std::size_t breakBondsInSlots(Fracture &fracture, const std::vector< util::Point > &xRef, const std::vector< std::vector< std::size_t > > &neighbors, const std::vector< std::size_t > &nodeParticleId, std::size_t particleId, const std::vector< double > &xCenters, double width, double yLo, double yHi)
Break the bonds of one particle that enter open notch slots.
Definition prenotch.cpp:99
Collection of methods and data related to finite element and mesh.
Definition mesh.cpp:29
Collection of methods and data related to particle object.
Definition modelData.h:36
void applyRigidBodyConstraint(data::ModelData &data)
void print(const T &msg, int nt=print_default_tab, int printMpiRank=print_default_mpi_rank)
Prints formatted information.
Definition io.h:171
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.
void finalizeMpi()
Call MPI_Finalize if this process initialized MPI.
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
Input data for geometrical objects.
std::vector< double > d_geomParams
Zone parameters.
std::string d_geomName
Zone type.
std::pair< std::vector< std::string >, std::vector< std::string > > d_geomComplexInfo
Zone geometry info if it is a complex type.
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.