2"""Health gates for attrition sim2 (thin container, R_out=0.0203).
4Containment about drum axis (0,0). Wall rotates about an offset, but the
5cavity geometry is still centered at the origin.
8from __future__
import annotations
13from pathlib
import Path
22EARLY_MEAN_DAMAGE_MAX = 0.08
25MIN_DAMAGED_PARTICLES = 2
26MIN_FRAC_DAMAGE = 0.001
27AXIS = np.array([0.0, 0.0])
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
35def wall_id_and_T(here: Path, deck_name: str |
None) -> tuple[int, float]:
36 """Pick wall id / Final_Time from the deck that was actually run."""
39 names.append(Path(deck_name).name)
40 names.extend([
"input_short.json",
"input_medium.json",
"input.json"])
49 j = json.loads(p.read_text())
50 wid = int(j[
"Displacement_BC"][
"Set_1"][
"Particle_List"][0])
51 T = float(j[
"Model"][
"Final_Time"])
56def load_frame(out: Path, fr: int, wall_id: int):
57 xs, ids, dm, vs = [], [], [], []
58 for r
in sorted(out.glob(f
"output_0_{fr}_r*.vtu")):
60 xs.append(m.points[:, :2])
61 ids.append(m.point_data[
"Particle_ID"].ravel())
62 if "Damage" in m.point_data:
63 dm.append(m.point_data[
"Damage"].ravel())
64 if "Velocity" in m.point_data:
65 vs.append(m.point_data[
"Velocity"])
66 X = np.concatenate(xs)
67 I = np.concatenate(ids).astype(int)
70 R = np.hypot(X[grain, 0] - AXIS[0], X[grain, 1] - AXIS[1])
71 Rw = np.hypot(X[wall, 0] - AXIS[0], X[wall, 1] - AXIS[1])
72 D = np.concatenate(dm)[grain]
if dm
else np.zeros(grain.sum())
73 V = np.concatenate(vs)[grain]
if vs
else np.zeros((grain.sum(), 3))
75 vmax = float(np.linalg.norm(V, axis=1).max())
if len(V)
else 0.0
77 for pid
in set(Ig.tolist()):
78 if D[Ig == pid].max() > 0.1:
81 for pid
in set(Ig.tolist()):
82 com = X[grain][Ig == pid].mean(axis=0)
83 if np.hypot(com[0] - AXIS[0], com[1] - AXIS[1]) > R_IN:
85 return R, D, vmax, Rw, n_part_dmg, n_com_out
89 here = Path(__file__).resolve().parent
90 out = Path(sys.argv[1]
if len(sys.argv) > 1
else here /
"runs" /
"out").resolve()
91 deck_arg = sys.argv[2]
if len(sys.argv) > 2
and not sys.argv[2].startswith(
"-")
else None
93 require_wear = T >= 0.03
and "--ic-only" not in sys.argv
95 ranks0 = sorted(out.glob(
"output_0_*_r0.vtu"), key=frame_key)
98 print(f
"FAIL: need ≥2 frames in {out}, got {len(frames)}")
102 f
"wall_id={wall_id} T={T} require_wear={require_wear} "
103 f
"R_in={R_IN} R_out={R_OUT} (radius about drum axis (0,0))"
106 "fr maxR_ax n_pen n_thru n_com_out DamMax DamMean n_part_dmg |v|max wallR"
111 R, D, vmax, Rw, n_part, n_com =
load_frame(out, fr, wall_id)
112 n_pen = int((R > R_IN + PEN_TOL).sum())
113 n_thru = int((R > R_OUT).sum())
131 f
"{fr:4d} {s[1]:.5f} {n_pen:5d} {n_thru:5d} {n_com:5d} "
132 f
"{s[5]:.4f} {s[6]:.4f} {n_part:5d} {vmax:.3f} "
133 f
"[{s[9]:.4f},{s[10]:.4f}]"
136 if s[9] < 0.014
or s[10] > 0.021:
138 f
"IC wall mesh shifted: wall R=[{s[9]:.5f},{s[10]:.5f}] "
139 f
"(expect ~[0.015,0.0203])"
141 if n_pen > 0
or n_thru > 0:
142 fails.append(f
"IC grain/wall overlap: n_pen={n_pen} n_thru={n_thru}")
144 fails.append(f
"fr={fr}: {n_thru} nodes through wall (R>{R_OUT})")
146 fails.append(f
"fr={fr}: {n_pen} nodes past R_in+tol (maxR={R.max():.6f})")
148 fails.append(f
"fr={fr}: {n_com} particle COMs outside R_in")
150 early = series[1]
if len(series) > 1
else series[0]
152 peak_v = max(s[8]
for s
in series)
155 fails.append(f
"early |v|={early[8]:.1f} > {MAX_V}")
157 fails.append(f
"peak |v|={peak_v:.1f} > {MAX_V}")
158 if early[6] > EARLY_MEAN_DAMAGE_MAX:
159 fails.append(f
"early DamMean={early[6]:.4f} > {EARLY_MEAN_DAMAGE_MAX}")
162 if late[5] < MIN_FINAL_DAMAGE:
163 fails.append(f
"final DamMax={late[5]:.4f} < {MIN_FINAL_DAMAGE}")
164 n_dmg = int((
load_frame(out, late[0], wall_id)[1] > 0.05).sum())
165 if n_dmg < MIN_DAMAGED_NODES:
166 fails.append(f
"damaged nodes={n_dmg} < {MIN_DAMAGED_NODES}")
167 if late[7] < MIN_DAMAGED_PARTICLES:
168 fails.append(f
"damaged particles={late[7]} < {MIN_DAMAGED_PARTICLES}")
169 if late[6] < MIN_FRAC_DAMAGE * 0.5
and n_dmg < MIN_DAMAGED_NODES:
170 fails.append(
"insufficient attrition damage")
171 if early[5] > 0.95
and early[6] > 0.1:
172 fails.append(
"early frame already mass-ruptured")
174 n_dmg = int((
load_frame(out, late[0], wall_id)[1] > 0.05).sum())
175 print(f
"(IC/short mode: skip wear gates; DamMax={late[5]:.4f} n_dmg={n_dmg})")
186 f
"PASS: zero escape maxR={late[1]:.5f}, DamMax {early[5]:.3f}→{late[5]:.3f}, "
187 f
"n_part_dmg={late[7]}, peak|v|={peak_v:.2f}"
192if __name__ ==
"__main__":
193 raise SystemExit(
main())
load_frame(Path out, int fr, int wall_id)
tuple[int, float] wall_id_and_T(Path here, str|None deck_name)