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 sim1: a rotating drum with an inward protrusion.
11
12Circles, triangles and drums in two sizes tumble inside a cylinder that is
13spun by a rotation displacement BC; the protrusion grinds them. This is the
14deck behind ``attrition_test_sim1.gif`` in the top-level README.
15
16The deck is assembled through :class:`peridem.Deck`. The wall is a complex
17geometry, an outer circle with an inner circle removed and a protrusion
18rectangle added. It is placed at its signed-volume centroid, computed by the
19geometry object. The mesh axis is at the origin, so any other site translates
20the mesh.
21
22``gen_input.py`` next to this file writes the same decks as JSON for
23``bin/PeriDEM``. ``python/tests/test_example_parity.py`` compares the two.
24"""
25
26from __future__ import annotations
27
28import math
29import os
30import sys
31from pathlib import Path
32
33HERE = Path(__file__).resolve().parent
34for _p in HERE.parents:
35 if (_p / "examples" / "peridem_env.py").is_file():
36 sys.path.insert(0, str(_p / "examples"))
37 break
38import peridem_env # noqa: F401,E402
39
40import peridem # noqa: E402
41from peridem import Deck, Geometry # noqa: E402
42from peridem.deck import MeshSpec # noqa: E402
43
44MESH_DIR = HERE / "meshes"
45CSV = HERE / "particle_locations_0.csv"
46
47HORIZON = 6.0e-4
48DENSITY = 1200.0
49R_IN = 0.02 # inner radius of the drum
50R_SMALL, R_LARGE = 0.001, 0.003
51OMEGA = -20.0 * math.pi # drum spin rate, rad/s
52GRAVITY = 10.0
53
54# Reference radius of each of the six grain shapes (3 small, 3 large).
55R_REF = {0: R_SMALL, 1: R_SMALL, 2: R_SMALL, 3: R_LARGE, 4: R_LARGE, 5: R_LARGE}
56
57MESH_FILES = [
58 "mesh_cir_small_0.msh",
59 "mesh_tri_small_0.msh",
60 "mesh_drum2d_small_0.msh",
61 "mesh_cir_large_0.msh",
62 "mesh_tri_large_0.msh",
63 "mesh_drum2d_large_0.msh",
64 "mesh_wall_0.msh",
65]
66
67# outer circle (+), inner circle (-), protrusion rectangle (+)
68WALL_PARAMS = [0.021, 0.0, 0.0, 0.0,
69 0.02, 0.0, 0.0, 0.0,
70 0.014, -0.0015, 0.0, 0.02, 0.0015, 0.0]
71
72# Normal contact stiffness per pair of contact groups, from the sim1
73# calibration. These are not recomputed from K_MAT and HORIZON.
74KN = {
75 (0, 0): 7.368284e20,
76 (0, 1): 1.339688e21,
77 (0, 2): 1.339688e21,
78 (1, 1): 7.368284e21,
79 (1, 2): 7.368284e21,
80 (2, 2): 7.368284e21,
81}
82K_MAT = {0: 1.0e4, 1: 1.0e5, 2: 1.0e5}
83KN_FACTOR = 1.0
84
85# Small circle, triangle and drum, then the same three shapes larger.
86GRAIN_SHAPES = [
87 ("circle", [R_SMALL, 0.0, 0.0, 0.0]),
88 ("triangle", [R_SMALL, 0.0, 0.0, 0.0]),
89 ("drum2d", [R_SMALL, 0.0004, 0.0, 0.0, 0.0]),
90 ("circle", [R_LARGE, 0.0, 0.0, 0.0]),
91 ("triangle", [R_LARGE, 0.0, 0.0, 0.0]),
92 ("drum2d", [R_LARGE, 0.0012, 0.0, 0.0, 0.0]),
93]
94
95PRESETS = {
96 "short": (0.01, 100_000, 2000),
97 "medium": (0.03, 300_000, 3000),
98 "paper": (0.1, 1_000_000, 2500),
99}
100
101TAGS = ["Displacement", "Velocity", "Force", "Damage_Z", "Damage",
102 "Particle_ID", "Fixity", "Contact_Nodes"]
103
104
105def read_sites(csv: str | os.PathLike[str] = CSV) -> list[dict[str, float]]:
106 """Packed grain sites: ``zone, x, y, z, r, theta`` per row."""
107 sites: list[dict[str, float]] = []
108 with Path(csv).open() as f:
109 next(f)
110 for line in f:
111 parts = [p.strip() for p in line.split(",")]
112 if len(parts) < 6:
113 continue
114 zone = int(float(parts[0]))
115 x, y, z, r, theta = (float(v) for v in parts[1:6])
116 if math.hypot(x, y) + r > R_IN - 1.0e-6:
117 raise ValueError(
118 f"initial pack overlaps the drum: zone={zone} at "
119 f"({x}, {y}) r={r} reaches past R_in={R_IN}")
120 sites.append({"zone": zone, "x": x, "y": y, "z": z, "r": r,
121 "theta": theta})
122 return sites
123
124
125def build_deck(output_path: str | os.PathLike[str] = "runs/out/", *,
126 preset: str = "short",
127 final_time: float | None = None,
128 num_steps: int | None = None,
129 output_interval: int | None = None,
130 omega: float = OMEGA,
131 mesh_dir: str | os.PathLike[str] = MESH_DIR,
132 csv: str | os.PathLike[str] = CSV) -> Deck:
133 t_default, n_default, out_default = PRESETS[preset]
134 final_time = t_default if final_time is None else final_time
135 num_steps = n_default if num_steps is None else num_steps
136 output_interval = out_default if output_interval is None else output_interval
137
138 d = Deck(dim=2, t_final=final_time, n_steps=num_steps,
139 bond_break="tension", self_contact="none", wall_contact="meshed")
140 d.set_output(output_path, tags=TAGS, interval=output_interval, debug=1,
141 perform_fe_out=False,
142 dt_test_out=max(1, output_interval // 10), tag_pp="0",
143 pvd_collection=True)
144 d.set_gravity(0.0, -GRAVITY)
145 d.set_neighbor(update_criteria="simple_all", s_factor=10.0, update_interval=40,
146 near_bd_nodes_tol=0.5)
147
148 base = Path(mesh_dir)
149 for i, (name, params) in enumerate(GRAIN_SHAPES):
150 d.add_particle_type(Geometry(name, params),
151 MeshSpec(file=(base / MESH_FILES[i]).resolve()))
152 wall = Geometry("complex", WALL_PARAMS,
153 vec_type=["circle", "circle", "rectangle"],
154 vec_flag=["plus", "minus", "plus"])
155 g_wall = d.add_particle_type(wall,
156 MeshSpec(file=(base / MESH_FILES[6]).resolve()))
157
158 # Material 0 is the small grains, material 1 the large grains and drum.
159 m_small = d.add_material(horizon=HORIZON, density=DENSITY, K=1.0e4, G=6.0e3,
160 Gc=50.0, influence_fn_type=1)
161 m_large = d.add_material(horizon=HORIZON, density=DENSITY, K=1.0e5, G=6.0e4,
162 Gc=100.0, influence_fn_type=1)
163
164 # Contact groups: 0 small grains, 1 large grains, 2 the drum.
165 for (i, j), kn in KN.items():
166 d.add_contact_pair(
167 i, j, contact_radius_factor=0.95, Kn=kn,
168 K=peridem.harmonic_mean(K_MAT[i], K_MAT[j]),
169 damping_on=False, friction_on=False, eps=0.95, mu=0.5,
170 Kn_factor=KN_FACTOR,
171 # Damping is off, so Beta_n_Factor does not enter the force.
172 raw={"Beta_n_Factor": 100.0})
173 d.set_contact_laws(damping_law="off", friction_law="coulomb_simple",
174 correct_volume=False)
175
176 sites = read_sites(csv)
177 for s in sites:
178 zone = int(s["zone"])
179 large = zone >= 3
180 d.place(s["x"], s["y"], s["z"], geometry=zone,
181 material=m_large if large else m_small,
182 contact=1 if large else 0,
183 theta=s["theta"], scale=s["r"] / R_REF[zone])
184
185 wcx, wcy, wcz = wall.center
186 wall_id = len(sites)
187 d.place(wcx, wcy, wcz, geometry=g_wall, material=m_large, contact=2,
188 wall=True)
189
190 # The rotation centre is the last three time function parameters.
191 d.add_displacement_bc(particles=[wall_id], direction=[1, 2],
192 time_fn_type="rotation",
193 time_fn_params=[omega, 0.0, 0.0, 0.0],
194 spatial_fn_type="rotation")
195 return d
196
197
198def main(argv: list[str] | None = None) -> int:
199 import argparse
200
201 p = argparse.ArgumentParser(description=__doc__)
202 p.add_argument("-o", "--output", default=str(HERE / "runs_py/out"))
203 p.add_argument("--preset", choices=sorted(PRESETS), default="short")
204 p.add_argument("--final-time", type=float, default=None)
205 p.add_argument("--num-steps", type=int, default=None)
206 p.add_argument("--output-interval", type=int, default=None)
207 p.add_argument("--nthreads", type=int,
208 default=int(os.environ.get("NTHREADS", "4")))
209 p.add_argument("--snapshot", metavar="PNG", default=None)
210 p.add_argument("--write-deck", metavar="PATH", default=None)
211 args = p.parse_args(argv)
212
213 out = Path(args.output).resolve()
214 d = build_deck(str(out) + "/", preset=args.preset,
215 final_time=args.final_time, num_steps=args.num_steps,
216 output_interval=args.output_interval)
217 if args.write_deck:
218 print(d.write(args.write_deck))
219 return 0
220
221 out.mkdir(parents=True, exist_ok=True)
222 peridem.init(n_threads=args.nthreads)
223 sim = d.run(workdir=HERE)
224 if peridem.mpi_rank() != 0:
225 return 0
226 print(f"done: grains={sim.n_particles} walls={sim.n_walls} "
227 f"nodes={sim.n_nodes} t={sim.time:g}")
228 worst = max(sim.particles, key=lambda q: float(q.damage.max()), default=None)
229 if worst is not None:
230 print(f"most damaged grain: id={worst.id} "
231 f"max Z={float(worst.damage.max()):.4f} "
232 f"shape={worst.geometry_name}")
233 if args.snapshot:
234 print(peridem.snapshot(peridem.last_vtu(out), args.snapshot,
235 color="Damage_Z",
236 title="attrition sim1: damage"))
237 print(f"VTU under {out}")
238 return 0
239
240
241if __name__ == "__main__":
242 raise SystemExit(main())
list[dict[str, float]] read_sites(str|os.PathLike[str] csv=CSV)
Definition problem.py:105
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