55R_REF = {0: R_SMALL, 1: R_SMALL, 2: R_SMALL, 3: R_LARGE, 4: R_LARGE, 5: R_LARGE}
70 0.014, -0.0015, 0.0, 0.02, 0.0015, 0.0]
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]),
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:
111 parts = [p.strip()
for p
in line.split(
",")]
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:
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,
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
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",
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)
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()))
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)
165 for (i, j), kn
in KN.items():
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,
172 raw={
"Beta_n_Factor": 100.0})
173 d.set_contact_laws(damping_law=
"off", friction_law=
"coulomb_simple",
174 correct_volume=
False)
178 zone = int(s[
"zone"])
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])
185 wcx, wcy, wcz = wall.center
187 d.place(wcx, wcy, wcz, geometry=g_wall, material=m_large, contact=2,
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")
198def main(argv: list[str] |
None =
None) -> int:
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)
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)
218 print(d.write(args.write_deck))
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:
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}")
234 print(peridem.snapshot(peridem.last_vtu(out), args.snapshot,
236 title=
"attrition sim1: damage"))
237 print(f
"VTU under {out}")
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)