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 sim2 (thin container, R_out=0.0203).
3
4Containment about drum axis (0,0). Wall rotates about an offset, but the
5cavity geometry is still centered at the origin.
6"""
7
8from __future__ import annotations
9
10import json
11import re
12import sys
13from pathlib import Path
14
15import meshio
16import numpy as np
17
18R_IN = 0.02
19R_OUT = 0.0203
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_and_T(here: Path, deck_name: str | None) -> tuple[int, float]:
36 """Pick wall id / Final_Time from the deck that was actually run."""
37 names = []
38 if deck_name:
39 names.append(Path(deck_name).name)
40 names.extend(["input_short.json", "input_medium.json", "input.json"])
41 seen = set()
42 for name in names:
43 if name in seen:
44 continue
45 seen.add(name)
46 p = here / name
47 if not p.is_file():
48 continue
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"])
52 return wid, T
53 return 58, 0.1
54
55
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")):
59 m = meshio.read(r)
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)
68 wall = I == wall_id
69 grain = ~wall
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))
74 Ig = I[grain]
75 vmax = float(np.linalg.norm(V, axis=1).max()) if len(V) else 0.0
76 n_part_dmg = 0
77 for pid in set(Ig.tolist()):
78 if D[Ig == pid].max() > 0.1:
79 n_part_dmg += 1
80 n_com_out = 0
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:
84 n_com_out += 1
85 return R, D, vmax, Rw, n_part_dmg, n_com_out
86
87
88def main() -> int:
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
92 wall_id, T = wall_id_and_T(here, deck_arg)
93 require_wear = T >= 0.03 and "--ic-only" not in sys.argv
94
95 ranks0 = sorted(out.glob("output_0_*_r0.vtu"), key=frame_key)
96 frames = [frame_key(p) for p in ranks0 if frame_key(p) >= 0]
97 if len(frames) < 2:
98 print(f"FAIL: need ≥2 frames in {out}, got {len(frames)}")
99 return 1
100
101 print(
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))"
104 )
105 print(
106 "fr maxR_ax n_pen n_thru n_com_out DamMax DamMean n_part_dmg |v|max wallR"
107 )
108 series = []
109 fails = []
110 for fr in frames:
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())
114 series.append(
115 (
116 fr,
117 float(R.max()),
118 n_pen,
119 n_thru,
120 n_com,
121 float(D.max()),
122 float(D.mean()),
123 n_part,
124 vmax,
125 float(Rw.min()),
126 float(Rw.max()),
127 )
128 )
129 s = series[-1]
130 print(
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}]"
134 )
135 if fr == frames[0]:
136 if s[9] < 0.014 or s[10] > 0.021:
137 fails.append(
138 f"IC wall mesh shifted: wall R=[{s[9]:.5f},{s[10]:.5f}] "
139 f"(expect ~[0.015,0.0203])"
140 )
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}")
143 if n_thru > 0:
144 fails.append(f"fr={fr}: {n_thru} nodes through wall (R>{R_OUT})")
145 elif n_pen > 0:
146 fails.append(f"fr={fr}: {n_pen} nodes past R_in+tol (maxR={R.max():.6f})")
147 if n_com > 0:
148 fails.append(f"fr={fr}: {n_com} particle COMs outside R_in")
149
150 early = series[1] if len(series) > 1 else series[0]
151 late = series[-1]
152 peak_v = max(s[8] for s in series)
153
154 if early[8] > MAX_V:
155 fails.append(f"early |v|={early[8]:.1f} > {MAX_V}")
156 if peak_v > 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}")
160
161 if require_wear:
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")
173 else:
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})")
176
177 if fails:
178 seen = set()
179 print("FAIL:")
180 for f in fails:
181 if f not in seen:
182 print(" ", f)
183 seen.add(f)
184 return 1
185 print(
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}"
188 )
189 return 0
190
191
192if __name__ == "__main__":
193 raise SystemExit(main())
int frame_key(Path p)
load_frame(Path out, int fr, int wall_id)
tuple[int, float] wall_id_and_T(Path here, str|None deck_name)