PeriDEM 0.3.0
PeriDEM -- Peridynamics-based high-fidelity model for granular media
Loading...
Searching...
No Matches
check_health.py
Go to the documentation of this file.
1#!/usr/bin/env python3
2"""Health gates for attrition sim1 (increased Kn_Factor, R_out=0.021).
3
4Containment is measured about the drum axis (0,0) — the rotation center —
5not the wall-node COM (protrusion biases COM and falsely flags escape).
6IC also checks that the wall mesh was not translated into the packing.
7"""
8
9from __future__ import annotations
10
11import re
12import sys
13from pathlib import Path
14
15import meshio
16import numpy as np
17
18R_IN = 0.02
19R_OUT = 0.021
20PEN_TOL = 8.0e-5
21MAX_V = 40.0
22EARLY_MEAN_DAMAGE_MAX = 0.08
23MIN_FINAL_DAMAGE = 0.1
24MIN_DAMAGED_NODES = 10
25MIN_DAMAGED_PARTICLES = 2
26MIN_FRAC_DAMAGE = 0.001
27AXIS = np.array([0.0, 0.0])
28
29
30def frame_key(p: Path) -> int:
31 m = re.search(r"output_0_(\d+)_r", p.name)
32 return int(m.group(1)) if m else -1
33
34
35def wall_id_from_deck(here: Path) -> int:
36 import json
37
38 for name in ("input_short.json", "input.json"):
39 p = here / name
40 if p.is_file():
41 j = json.loads(p.read_text())
42 return int(j["Displacement_BC"]["Set_1"]["Particle_List"][0])
43 return 40
44
45
46def load_frame(out: Path, fr: int, wall_id: int):
47 xs, ids, dm, vs = [], [], [], []
48 for r in sorted(out.glob(f"output_0_{fr}_r*.vtu")):
49 m = meshio.read(r)
50 xs.append(m.points[:, :2])
51 ids.append(m.point_data["Particle_ID"].ravel())
52 if "Damage" in m.point_data:
53 dm.append(m.point_data["Damage"].ravel())
54 if "Velocity" in m.point_data:
55 vs.append(m.point_data["Velocity"])
56 X = np.concatenate(xs)
57 I = np.concatenate(ids).astype(int)
58 wall = I == wall_id
59 grain = ~wall
60 R = np.hypot(X[grain, 0] - AXIS[0], X[grain, 1] - AXIS[1])
61 Rw = np.hypot(X[wall, 0] - AXIS[0], X[wall, 1] - AXIS[1])
62 D = np.concatenate(dm)[grain] if dm else np.zeros(grain.sum())
63 V = np.concatenate(vs)[grain] if vs else np.zeros((grain.sum(), 3))
64 Ig = I[grain]
65 vmax = float(np.linalg.norm(V, axis=1).max()) if len(V) else 0.0
66 n_part_dmg = 0
67 for pid in set(Ig.tolist()):
68 if D[Ig == pid].max() > 0.1:
69 n_part_dmg += 1
70 n_com_out = 0
71 for pid in set(Ig.tolist()):
72 com = X[grain][Ig == pid].mean(axis=0)
73 if np.hypot(com[0] - AXIS[0], com[1] - AXIS[1]) > R_IN:
74 n_com_out += 1
75 return R, D, vmax, Rw, n_part_dmg, n_com_out
76
77
78def main() -> int:
79 here = Path(__file__).resolve().parent
80 out = Path(sys.argv[1] if len(sys.argv) > 1 else here / "runs" / "out").resolve()
81 wall_id = wall_id_from_deck(here)
82
83 ranks0 = sorted(out.glob("output_0_*_r0.vtu"), key=frame_key)
84 frames = [frame_key(p) for p in ranks0 if frame_key(p) >= 0]
85 if len(frames) < 4:
86 print(f"FAIL: need ≥4 frames in {out}, got {len(frames)}")
87 return 1
88
89 print(
90 f"wall_id={wall_id} R_in={R_IN} R_out={R_OUT} pen_tol={PEN_TOL} "
91 f"(radius about drum axis (0,0))"
92 )
93 print(
94 "fr maxR_ax n_pen n_thru n_com_out DamMax DamMean n_part_dmg |v|max wallR"
95 )
96 series = []
97 fails = []
98 for fr in frames:
99 R, D, vmax, Rw, n_part, n_com = load_frame(out, fr, wall_id)
100 n_pen = int((R > R_IN + PEN_TOL).sum())
101 n_thru = int((R > R_OUT).sum())
102 series.append(
103 (
104 fr,
105 float(R.max()),
106 n_pen,
107 n_thru,
108 n_com,
109 float(D.max()),
110 float(D.mean()),
111 n_part,
112 vmax,
113 float(Rw.min()),
114 float(Rw.max()),
115 )
116 )
117 s = series[-1]
118 print(
119 f"{fr:4d} {s[1]:.5f} {n_pen:5d} {n_thru:5d} {n_com:5d} "
120 f"{s[5]:.4f} {s[6]:.4f} {n_part:5d} {vmax:.3f} "
121 f"[{s[9]:.4f},{s[10]:.4f}]"
122 )
123 if fr == frames[0]:
124 if s[9] < 0.013 or s[10] > 0.0225:
125 fails.append(
126 f"IC wall mesh shifted: wall R=[{s[9]:.5f},{s[10]:.5f}] "
127 f"(expect ~[0.014,0.021]) — wall site must equal geom.center()"
128 )
129 if n_pen > 0 or n_thru > 0:
130 fails.append(f"IC grain/wall overlap: n_pen={n_pen} n_thru={n_thru}")
131 if n_thru > 0:
132 fails.append(f"fr={fr}: {n_thru} nodes through wall (R>{R_OUT})")
133 elif n_pen > 0:
134 fails.append(f"fr={fr}: {n_pen} nodes past R_in+tol (maxR={R.max():.6f})")
135 if n_com > 0:
136 fails.append(f"fr={fr}: {n_com} particle COMs outside R_in")
137
138 early = series[1] if len(series) > 1 else series[0]
139 late = series[-1]
140 peak_v = max(s[8] for s in series)
141
142 if early[8] > MAX_V:
143 fails.append(f"early |v|={early[8]:.1f} > {MAX_V}")
144 if peak_v > MAX_V:
145 fails.append(f"peak |v|={peak_v:.1f} > {MAX_V}")
146 if early[6] > EARLY_MEAN_DAMAGE_MAX:
147 fails.append(f"early DamMean={early[6]:.4f} > {EARLY_MEAN_DAMAGE_MAX}")
148 if late[5] < MIN_FINAL_DAMAGE:
149 fails.append(f"final DamMax={late[5]:.4f} < {MIN_FINAL_DAMAGE}")
150 n_dmg = int((load_frame(out, late[0], wall_id)[1] > 0.05).sum())
151 if n_dmg < MIN_DAMAGED_NODES:
152 fails.append(f"damaged nodes={n_dmg} < {MIN_DAMAGED_NODES}")
153 if late[7] < MIN_DAMAGED_PARTICLES:
154 fails.append(f"damaged particles={late[7]} < {MIN_DAMAGED_PARTICLES}")
155 if late[6] < MIN_FRAC_DAMAGE * 0.5 and n_dmg < MIN_DAMAGED_NODES:
156 fails.append("insufficient attrition damage")
157 if early[5] > 0.95 and early[6] > 0.1:
158 fails.append("early frame already mass-ruptured")
159
160 if fails:
161 seen = set()
162 print("FAIL:")
163 for f in fails:
164 if f not in seen:
165 print(" ", f)
166 seen.add(f)
167 return 1
168 print(
169 f"PASS: zero escape maxR={late[1]:.5f}, DamMax {early[5]:.3f}→{late[5]:.3f}, "
170 f"n_part_dmg={late[7]}, n_dmg_nodes={n_dmg}, peak|v|={peak_v:.2f}"
171 )
172 return 0
173
174
175if __name__ == "__main__":
176 raise SystemExit(main())
int wall_id_from_deck(Path here)
int frame_key(Path p)
load_frame(Path out, int fr, int wall_id)