10"""Attrition sim2: thin rotating container with an off-centre spin axis.
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.
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.
21from __future__
import annotations
26from pathlib
import Path
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"))
36from peridem
import Deck, Geometry
37from peridem.deck
import MeshSpec
39MESH_DIR = HERE /
"meshes"
40CSV = HERE /
"particle_locations_0.csv"
42R_SMALL, R_LARGE = 0.001, 0.003
43MESH_SIZE = R_SMALL / 5.0
44HORIZON = 2.0 * MESH_SIZE
46R_OUT = R_IN + 1.5 * MESH_SIZE
48W_BAR = 1.5 * MESH_SIZE
52OMEGA = -40.0 * math.pi
53ROT_CENTER = (-0.2 * R_IN, 0.2 * R_IN, 0.0)
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}
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",
70WALL_PARAMS = [R_OUT, 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]
82K_MAT = {0: 1.0e4, 1: 1.0e5, 2: 1.0e5}
87CONTACT_RADIUS_FACTOR = 0.95
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]),
104 "short": (0.01, 100_000, 2000),
105 "medium": (0.03, 300_000, 3000),
106 "paper": (0.1, 1_000_000, 2500),
109TAGS = [
"Displacement",
"Velocity",
"Force",
"Damage_Z",
"Damage",
110 "Particle_ID",
"Fixity",
"Contact_Nodes"]
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:
118 parts = [p.strip()
for p
in line.split(
",")]
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,
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']}")
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")
149 raise ValueError(
"initial pack invalid:\n " +
"\n ".join(errors[:40]))
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
165 d = Deck(dim=2, t_final=final_time, n_steps=num_steps,
166 bond_break=
"tension",
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",
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)
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()))
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)
192 for (i, j), kn
in KN.items():
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)
204 zone = int(s[
"zone"])
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])
211 wcx, wcy, wcz = wall.center
213 d.place(wcx, wcy, wcz, geometry=g_wall, material=m_large, contact=2,
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")
223def main(argv: list[str] |
None =
None) -> int:
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)
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)
243 print(d.write(args.write_deck))
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:
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}")
259 print(peridem.snapshot(peridem.last_vtu(out), args.snapshot,
261 title=
"attrition sim2: damage"))
262 print(f
"VTU under {out}")
266if __name__ ==
"__main__":
267 raise SystemExit(main())
list[dict[str, float]] read_sites(str|os.PathLike[str] csv=CSV)
None validate_sites(list[dict[str, float]] rows)
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)