PeriDEM 0.3.0
PeriDEM -- Peridynamics-based high-fidelity model for granular media
Loading...
Searching...
No Matches
problem.py
Go to the documentation of this file.
1#!/usr/bin/env python3
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"""Attrition sim2: thin rotating container with an off-centre spin axis.
11
12Circles, triangles, drums and hexagons in two sizes inside a thin-walled
13cylinder with an inward bar. The container spins twice as fast as sim1 and
14about a point offset from the origin, which throws the pack against the bar.
15
16The deck is assembled through :class:`peridem.Deck`. Every parameter whose
17default changes the result is set here. ``INPUT_DEFAULTS.md`` in this folder
18records which those are.
19"""
20
21from __future__ import annotations
22
23import math
24import os
25import sys
26from pathlib import Path
27
28HERE = Path(__file__).resolve().parent
29for _p in HERE.parents:
30 if (_p / "examples" / "peridem_env.py").is_file():
31 sys.path.insert(0, str(_p / "examples"))
32 break
33import peridem_env # noqa: F401,E402
34
35import peridem # noqa: E402
36from peridem import Deck, Geometry # noqa: E402
37from peridem.deck import MeshSpec # noqa: E402
38
39MESH_DIR = HERE / "meshes"
40CSV = HERE / "particle_locations_0.csv"
41
42R_SMALL, R_LARGE = 0.001, 0.003
43MESH_SIZE = R_SMALL / 5.0
44HORIZON = 2.0 * MESH_SIZE
45R_IN = 0.02
46R_OUT = R_IN + 1.5 * MESH_SIZE # thin wall
47L_BAR = 0.005 # inward protrusion length
48W_BAR = 1.5 * MESH_SIZE
49DENSITY = 1200.0
50GRAVITY = 10.0
51
52OMEGA = -40.0 * math.pi
53ROT_CENTER = (-0.2 * R_IN, 0.2 * R_IN, 0.0)
54
55R_REF = {0: R_SMALL, 1: R_SMALL, 2: R_SMALL, 3: R_SMALL,
56 4: R_LARGE, 5: R_LARGE, 6: R_LARGE, 7: R_LARGE}
57
58MESH_FILES = [
59 "mesh_cir_small_0.msh",
60 "mesh_tri_small_0.msh",
61 "mesh_drum2d_small_0.msh",
62 "mesh_hex_small_0.msh",
63 "mesh_cir_large_0.msh",
64 "mesh_tri_large_0.msh",
65 "mesh_drum2d_large_0.msh",
66 "mesh_hex_large_0.msh",
67 "mesh_wall_0.msh",
68]
69
70WALL_PARAMS = [R_OUT, 0.0, 0.0, 0.0,
71 R_IN, 0.0, 0.0, 0.0,
72 R_IN - L_BAR, -0.5 * W_BAR, 0.0, R_IN, 0.5 * W_BAR, 0.0]
73
74KN = {
75 (0, 0): 5.595291e21,
76 (0, 1): 1.017326e22,
77 (0, 2): 1.017326e22,
78 (1, 1): 5.595291e22,
79 (1, 2): 5.595291e22,
80 (2, 2): 5.595291e22,
81}
82K_MAT = {0: 1.0e4, 1: 1.0e5, 2: 1.0e5}
83
84KN_FACTOR = 1.0
85BETA_N_FACTOR = 100.0
86EPSILON = 0.95
87CONTACT_RADIUS_FACTOR = 0.95
88FRICTION_COEFF = 0.5
89SEARCH_INTERVAL = 40
90SEARCH_FACTOR = 10.0
91
92GRAIN_SHAPES = [
93 ("circle", [R_SMALL, 0.0, 0.0, 0.0]),
94 ("triangle", [R_SMALL, 0.0, 0.0, 0.0]),
95 ("drum2d", [R_SMALL, R_SMALL * 0.4, 0.0, 0.0, 0.0]),
96 ("hexagon", [R_SMALL, 0.0, 0.0, 0.0]),
97 ("circle", [R_LARGE, 0.0, 0.0, 0.0]),
98 ("triangle", [R_LARGE, 0.0, 0.0, 0.0]),
99 ("drum2d", [R_LARGE, R_LARGE * 0.4, 0.0, 0.0, 0.0]),
100 ("hexagon", [R_LARGE, 0.0, 0.0, 0.0]),
101]
102
103PRESETS = {
104 "short": (0.01, 100_000, 2000),
105 "medium": (0.03, 300_000, 3000),
106 "paper": (0.1, 1_000_000, 2500),
107}
108
109TAGS = ["Displacement", "Velocity", "Force", "Damage_Z", "Damage",
110 "Particle_ID", "Fixity", "Contact_Nodes"]
111
112
113def read_sites(csv: str | os.PathLike[str] = CSV) -> list[dict[str, float]]:
114 rows: list[dict[str, float]] = []
115 with Path(csv).open() as f:
116 next(f)
117 for line in f:
118 parts = [p.strip() for p in line.split(",")]
119 if len(parts) < 6:
120 continue
121 zone = int(float(parts[0]))
122 x, y, z, r, theta = (float(v) for v in parts[1:6])
123 rows.append({"zone": zone, "x": x, "y": y, "z": z, "r": r,
124 "theta": theta})
125 validate_sites(rows)
126 return rows
127
128
129def validate_sites(rows: list[dict[str, float]]) -> None:
130 """Reject a pack that starts inside the wall, the bar, or another grain."""
131 bar = (R_IN - L_BAR, -0.5 * W_BAR, R_IN, 0.5 * W_BAR)
132 errors: list[str] = []
133 for i, a in enumerate(rows):
134 if a["zone"] not in R_REF:
135 errors.append(f"row {i}: unknown shape id {a['zone']}")
136 continue
137 if math.hypot(a["x"], a["y"]) + a["r"] > R_IN - 1.0e-9:
138 errors.append(f"row {i}: reaches past R_in")
139 if not (a["x"] + a["r"] < bar[0] or a["x"] - a["r"] > bar[2]
140 or a["y"] + a["r"] < bar[1] or a["y"] - a["r"] > bar[3]):
141 errors.append(f"row {i}: overlaps the protrusion")
142 for i in range(len(rows)):
143 for j in range(i + 1, len(rows)):
144 a, b = rows[i], rows[j]
145 if math.hypot(a["x"] - b["x"], a["y"] - b["y"]) < \
146 a["r"] + b["r"] - 1.0e-9:
147 errors.append(f"rows {i},{j} overlap")
148 if errors:
149 raise ValueError("initial pack invalid:\n " + "\n ".join(errors[:40]))
150
151
152def build_deck(output_path: str | os.PathLike[str] = "runs/out/", *,
153 preset: str = "short",
154 final_time: float | None = None,
155 num_steps: int | None = None,
156 output_interval: int | None = None,
157 omega: float = OMEGA,
158 mesh_dir: str | os.PathLike[str] = MESH_DIR,
159 csv: str | os.PathLike[str] = CSV) -> Deck:
160 t_default, n_default, out_default = PRESETS[preset]
161 final_time = t_default if final_time is None else final_time
162 num_steps = n_default if num_steps is None else num_steps
163 output_interval = out_default if output_interval is None else output_interval
164
165 d = Deck(dim=2, t_final=final_time, n_steps=num_steps,
166 bond_break="tension",
167 # sim2 keeps broken-bond self-contact inside each grain.
168 self_contact="broken_bond_kn", wall_contact="meshed")
169 d.set_output(output_path, tags=TAGS, interval=output_interval, debug=3,
170 perform_fe_out=False,
171 dt_test_out=max(1, output_interval // 100), tag_pp="0",
172 pvd_collection=True)
173 d.set_gravity(0.0, -GRAVITY)
174 d.set_neighbor(update_criteria="simple_all", s_factor=SEARCH_FACTOR,
175 update_interval=SEARCH_INTERVAL, near_bd_nodes_tol=0.5)
176
177 base = Path(mesh_dir)
178 for i, (name, params) in enumerate(GRAIN_SHAPES):
179 d.add_particle_type(Geometry(name, params),
180 MeshSpec(file=(base / MESH_FILES[i]).resolve()))
181 wall = Geometry("complex", WALL_PARAMS,
182 vec_type=["circle", "circle", "rectangle"],
183 vec_flag=["plus", "minus", "plus"])
184 g_wall = d.add_particle_type(
185 wall, MeshSpec(file=(base / MESH_FILES[8]).resolve()))
186
187 m_small = d.add_material(horizon=HORIZON, density=DENSITY, K=1.0e4, G=6.0e3,
188 Gc=50.0, influence_fn_type=1)
189 m_large = d.add_material(horizon=HORIZON, density=DENSITY, K=1.0e5, G=6.0e4,
190 Gc=100.0, influence_fn_type=1)
191
192 for (i, j), kn in KN.items():
193 d.add_contact_pair(
194 i, j, contact_radius_factor=CONTACT_RADIUS_FACTOR, Kn=kn,
195 K=peridem.harmonic_mean(K_MAT[i], K_MAT[j]),
196 damping_on=False, friction_on=False, eps=EPSILON,
197 mu=FRICTION_COEFF, Kn_factor=KN_FACTOR,
198 raw={"Beta_n_Factor": BETA_N_FACTOR})
199 d.set_contact_laws(damping_law="off", friction_law="coulomb_simple",
200 correct_volume=False)
201
202 sites = read_sites(csv)
203 for s in sites:
204 zone = int(s["zone"])
205 large = zone >= 4
206 d.place(s["x"], s["y"], s["z"], geometry=zone,
207 material=m_large if large else m_small,
208 contact=1 if large else 0,
209 theta=s["theta"], scale=s["r"] / R_REF[zone])
210
211 wcx, wcy, wcz = wall.center
212 wall_id = len(sites)
213 d.place(wcx, wcy, wcz, geometry=g_wall, material=m_large, contact=2,
214 wall=True)
215
216 d.add_displacement_bc(particles=[wall_id], direction=[1, 2],
217 time_fn_type="rotation",
218 time_fn_params=[omega, *ROT_CENTER],
219 spatial_fn_type="rotation")
220 return d
221
222
223def main(argv: list[str] | None = None) -> int:
224 import argparse
225
226 p = argparse.ArgumentParser(description=__doc__)
227 p.add_argument("-o", "--output", default=str(HERE / "runs_py/out"))
228 p.add_argument("--preset", choices=sorted(PRESETS), default="short")
229 p.add_argument("--final-time", type=float, default=None)
230 p.add_argument("--num-steps", type=int, default=None)
231 p.add_argument("--output-interval", type=int, default=None)
232 p.add_argument("--nthreads", type=int,
233 default=int(os.environ.get("NTHREADS", "4")))
234 p.add_argument("--snapshot", metavar="PNG", default=None)
235 p.add_argument("--write-deck", metavar="PATH", default=None)
236 args = p.parse_args(argv)
237
238 out = Path(args.output).resolve()
239 d = build_deck(str(out) + "/", preset=args.preset,
240 final_time=args.final_time, num_steps=args.num_steps,
241 output_interval=args.output_interval)
242 if args.write_deck:
243 print(d.write(args.write_deck))
244 return 0
245
246 out.mkdir(parents=True, exist_ok=True)
247 peridem.init(n_threads=args.nthreads)
248 sim = d.run(workdir=HERE)
249 if peridem.mpi_rank() != 0:
250 return 0
251 print(f"done: grains={sim.n_particles} walls={sim.n_walls} "
252 f"nodes={sim.n_nodes} t={sim.time:g}")
253 worst = max(sim.particles, key=lambda q: float(q.damage.max()), default=None)
254 if worst is not None:
255 print(f"most damaged grain: id={worst.id} "
256 f"max Z={float(worst.damage.max()):.4f} "
257 f"shape={worst.geometry_name}")
258 if args.snapshot:
259 print(peridem.snapshot(peridem.last_vtu(out), args.snapshot,
260 color="Damage_Z",
261 title="attrition sim2: damage"))
262 print(f"VTU under {out}")
263 return 0
264
265
266if __name__ == "__main__":
267 raise SystemExit(main())
list[dict[str, float]] read_sites(str|os.PathLike[str] csv=CSV)
Definition problem.py:105
None validate_sites(list[dict[str, float]] rows)
Definition problem.py:129
Deck build_deck(str|os.PathLike[str] output_path="runs/out/", *str preset="short", float|None final_time=None, int|None num_steps=None, int|None output_interval=None, float omega=OMEGA, str|os.PathLike[str] mesh_dir=MESH_DIR, str|os.PathLike[str] csv=CSV)
Definition problem.py:132