47 xs, ids, dm, vs = [], [], [], []
48 for r
in sorted(out.glob(f
"output_0_{fr}_r*.vtu")):
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)
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))
65 vmax = float(np.linalg.norm(V, axis=1).max())
if len(V)
else 0.0
67 for pid
in set(Ig.tolist()):
68 if D[Ig == pid].max() > 0.1:
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:
75 return R, D, vmax, Rw, n_part_dmg, n_com_out
79 here = Path(__file__).resolve().parent
80 out = Path(sys.argv[1]
if len(sys.argv) > 1
else here /
"runs" /
"out").resolve()
83 ranks0 = sorted(out.glob(
"output_0_*_r0.vtu"), key=frame_key)
86 print(f
"FAIL: need ≥4 frames in {out}, got {len(frames)}")
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))"
94 "fr maxR_ax n_pen n_thru n_com_out DamMax DamMean n_part_dmg |v|max wallR"
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())
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}]"
124 if s[9] < 0.013
or s[10] > 0.0225:
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()"
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}")
132 fails.append(f
"fr={fr}: {n_thru} nodes through wall (R>{R_OUT})")
134 fails.append(f
"fr={fr}: {n_pen} nodes past R_in+tol (maxR={R.max():.6f})")
136 fails.append(f
"fr={fr}: {n_com} particle COMs outside R_in")
138 early = series[1]
if len(series) > 1
else series[0]
140 peak_v = max(s[8]
for s
in series)
143 fails.append(f
"early |v|={early[8]:.1f} > {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")
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}"