PeriDEM 0.3.0
PeriDEM -- Peridynamics-based high-fidelity model for granular media
Loading...
Searching...
No Matches
check_health Namespace Reference

Functions

int frame_key (Path p)
 
int wall_id_from_deck (Path here)
 
 load_frame (Path out, int fr, int wall_id)
 
int main ()
 
tuple[int, float] wall_id_and_T (Path here, str|None deck_name)
 
int frame_index (Path p)
 
list[tuple[np.ndarray, int]] spatial_clusters (np.ndarray X, np.ndarray|None D=None, float link=CLUSTER_LINK, float dam_cut=0.75)
 
 load_frames (Path out)
 

Variables

float R_IN = 0.02
 
float R_OUT = 0.021
 
float PEN_TOL = 8.0e-5
 
float MAX_V = 40.0
 
float EARLY_MEAN_DAMAGE_MAX = 0.08
 
float MIN_FINAL_DAMAGE = 0.1
 
int MIN_DAMAGED_NODES = 10
 
int MIN_DAMAGED_PARTICLES = 2
 
float MIN_FRAC_DAMAGE = 0.001
 
 AXIS = np.array([0.0, 0.0])
 
float EARLY_DAMAGE_CAP = 0.98
 
float EARLY_MEAN_DAMAGE_CAP = 0.25
 
float MAX_FINAL_MEAN_DAMAGE = 0.50
 
int MIN_DAMAGE_RISE_FRAMES = 1
 
float MAX_COM_RADIUS = 0.012
 
float MAX_COM_DRIFT = 0.012
 
float TETHER_DIST = 1.5e-4
 
float TETHER_COM_LIFT = 5.0e-4
 
float TETHER_DAMAGE = 0.9
 
float CLUSTER_LINK = 1.9e-4
 
float MIN_CLUSTER_FRAC = 0.15
 
float MIN_CLUSTER_SEP = 6.0e-4
 
float MESH_SIZE = 1.0e-4
 

Detailed Description

Health gates for attrition sim1 (increased Kn_Factor, R_out=0.021).

Containment is measured about the drum axis (0,0) — the rotation center —
not the wall-node COM (protrusion biases COM and falsely flags escape).
IC also checks that the wall mesh was not translated into the packing.
Health gates for attrition sim2 (thin container, R_out=0.0203).

Containment about drum axis (0,0). Wall rotates about an offset, but the
cavity geometry is still centered at the origin.
Health gates for ellipse × tip-up triangle.

Desired: tip-seeded crack that grows into two spatially separated pieces
(not a glued tip tether, not fragment spray).

Function Documentation

◆ frame_index()

int check_health.frame_index ( Path  p)

Definition at line 36 of file check_health.py.

36def frame_index(p: Path) -> int:
37 m = re.search(r"output_(\d+)\.vtu$", p.name)
38 return int(m.group(1)) if m else -1
39
40

Referenced by load_frames().

Here is the caller graph for this function:

◆ frame_key()

int check_health.frame_key ( Path  p)

Definition at line 30 of file check_health.py.

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

Referenced by main(), and wall_id_and_T().

Here is the caller graph for this function:

◆ load_frame()

check_health.load_frame ( Path  out,
int  fr,
int  wall_id 
)

Definition at line 46 of file check_health.py.

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

Referenced by main(), and wall_id_and_T().

Here is the caller graph for this function:

◆ load_frames()

check_health.load_frames ( Path  out)

Definition at line 83 of file check_health.py.

83def load_frames(out: Path):
84 files = sorted(out.glob("output_*.vtu"), key=frame_index)
85 files = [p for p in files if frame_index(p) >= 0]
86 rows = []
87 for p in files:
88 m = meshio.read(p)
89 pid = m.point_data["Particle_ID"].ravel().astype(int)
90 X = m.points[:, :2]
91 D_all = np.asarray(m.point_data["Damage"]).ravel()
92 V_all = np.asarray(m.point_data["Velocity"])
93 ell = pid == 1
94 tri = pid == 0
95 tip = X[tri][np.argmax(X[tri, 1])]
96 D = D_all[ell]
97 V = V_all[ell]
98 Xe = X[ell]
99 com = Xe.mean(axis=0)
100 R = np.hypot(Xe[:, 0] - com[0], Xe[:, 1] - com[1])
101 rmax = float(np.percentile(R, 99)) if len(R) else 0.0
102 d_tip = np.linalg.norm(Xe - tip, axis=1)
103 j = int(np.argmin(d_tip))
104 clusters = spatial_clusters(Xe, D, CLUSTER_LINK)
105 n_ref = max(1, int((D < 0.75).sum()) if len(D) else len(Xe))
106 n_big = sum(1 for _, sz in clusters if sz >= MIN_CLUSTER_FRAC * n_ref)
107 sep = 0.0
108 if len(clusters) >= 2 and clusters[1][1] >= MIN_CLUSTER_FRAC * n_ref:
109 sep = float(np.linalg.norm(clusters[0][0] - clusters[1][0]))
110 rows.append(
111 {
112 "t_idx": frame_index(p),
113 "dmax": float(D.max()) if len(D) else 0.0,
114 "dmean": float(D.mean()) if len(D) else 0.0,
115 "nd05": int((D > 0.05).sum()),
116 "nd50": int((D > 0.5).sum()),
117 "vmax": float(np.linalg.norm(V, axis=1).max()) if len(V) else 0.0,
118 "rmax": rmax,
119 "com": com,
120 "tip": tip,
121 "min_d_tip": float(d_tip[j]),
122 "stuck_dam": float(D[j]),
123 "n": int(ell.sum()),
124 "n_big_clusters": n_big,
125 "cluster_sep": sep,
126 "cluster_sizes": [sz for _, sz in clusters[:4]],
127 }
128 )
129 return rows
130
131

References frame_index(), load_frames(), main(), and spatial_clusters().

Referenced by load_frames().

Here is the call graph for this function:
Here is the caller graph for this function:

◆ main()

int check_health.main ( )

Definition at line 78 of file check_health.py.

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
int main()
Definition main.cpp:31

References frame_key(), load_frame(), main(), and wall_id_from_deck().

Referenced by load_frames(), main(), and wall_id_and_T().

Here is the call graph for this function:
Here is the caller graph for this function:

◆ spatial_clusters()

list[tuple[np.ndarray, int]] check_health.spatial_clusters ( np.ndarray  X,
np.ndarray | None   D = None,
float   link = CLUSTER_LINK,
float   dam_cut = 0.75 
)
Union-find clusters. Highly damaged nodes are removed so a crack is a cut.

Definition at line 41 of file check_health.py.

43) -> list[tuple[np.ndarray, int]]:
44 """Union-find clusters. Highly damaged nodes are removed so a crack is a cut."""
45 if len(X) == 0:
46 return []
47 if D is not None:
48 alive = D < dam_cut
49 if alive.sum() < 4:
50 alive = np.ones(len(X), dtype=bool)
51 X = X[alive]
52 n = len(X)
53 parent = np.arange(n)
54
55 def find(a: int) -> int:
56 while parent[a] != a:
57 parent[a] = parent[parent[a]]
58 a = parent[a]
59 return a
60
61 def union(a: int, b: int) -> None:
62 ra, rb = find(a), find(b)
63 if ra != rb:
64 parent[rb] = ra
65
66 for i in range(n):
67 for j in range(i + 1, n):
68 if np.hypot(X[i, 0] - X[j, 0], X[i, 1] - X[j, 1]) < link:
69 union(i, j)
70
71 groups: dict[int, list[int]] = {}
72 for i in range(n):
73 r = find(i)
74 groups.setdefault(r, []).append(i)
75 out = []
76 for idxs in groups.values():
77 xi = X[np.asarray(idxs)]
78 out.append((xi.mean(axis=0), len(idxs)))
79 out.sort(key=lambda t: t[1], reverse=True)
80 return out
81
82

Referenced by load_frames().

Here is the caller graph for this function:

◆ wall_id_and_T()

tuple[int, float] check_health.wall_id_and_T ( Path  here,
str | None  deck_name 
)
Pick wall id / Final_Time from the deck that was actually run.

Definition at line 35 of file check_health.py.

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

References frame_key(), load_frame(), main(), and wall_id_and_T().

Referenced by wall_id_and_T().

Here is the call graph for this function:
Here is the caller graph for this function:

◆ wall_id_from_deck()

int check_health.wall_id_from_deck ( Path  here)

Definition at line 35 of file check_health.py.

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

Referenced by main().

Here is the caller graph for this function:

Variable Documentation

◆ AXIS

check_health.AXIS = np.array([0.0, 0.0])

Definition at line 27 of file check_health.py.

◆ CLUSTER_LINK

float check_health.CLUSTER_LINK = 1.9e-4

Definition at line 30 of file check_health.py.

◆ EARLY_DAMAGE_CAP

float check_health.EARLY_DAMAGE_CAP = 0.98

Definition at line 18 of file check_health.py.

◆ EARLY_MEAN_DAMAGE_CAP

float check_health.EARLY_MEAN_DAMAGE_CAP = 0.25

Definition at line 19 of file check_health.py.

◆ EARLY_MEAN_DAMAGE_MAX

float check_health.EARLY_MEAN_DAMAGE_MAX = 0.08

Definition at line 22 of file check_health.py.

◆ MAX_COM_DRIFT

float check_health.MAX_COM_DRIFT = 0.012

Definition at line 24 of file check_health.py.

◆ MAX_COM_RADIUS

float check_health.MAX_COM_RADIUS = 0.012

Definition at line 23 of file check_health.py.

◆ MAX_FINAL_MEAN_DAMAGE

float check_health.MAX_FINAL_MEAN_DAMAGE = 0.50

Definition at line 21 of file check_health.py.

◆ MAX_V

float check_health.MAX_V = 40.0

Definition at line 21 of file check_health.py.

◆ MESH_SIZE

float check_health.MESH_SIZE = 1.0e-4

Definition at line 33 of file check_health.py.

◆ MIN_CLUSTER_FRAC

float check_health.MIN_CLUSTER_FRAC = 0.15

Definition at line 31 of file check_health.py.

◆ MIN_CLUSTER_SEP

float check_health.MIN_CLUSTER_SEP = 6.0e-4

Definition at line 32 of file check_health.py.

◆ MIN_DAMAGE_RISE_FRAMES

int check_health.MIN_DAMAGE_RISE_FRAMES = 1

Definition at line 22 of file check_health.py.

◆ MIN_DAMAGED_NODES

int check_health.MIN_DAMAGED_NODES = 10

Definition at line 24 of file check_health.py.

◆ MIN_DAMAGED_PARTICLES

int check_health.MIN_DAMAGED_PARTICLES = 2

Definition at line 25 of file check_health.py.

◆ MIN_FINAL_DAMAGE

float check_health.MIN_FINAL_DAMAGE = 0.1

Definition at line 23 of file check_health.py.

◆ MIN_FRAC_DAMAGE

float check_health.MIN_FRAC_DAMAGE = 0.001

Definition at line 26 of file check_health.py.

◆ PEN_TOL

float check_health.PEN_TOL = 8.0e-5

Definition at line 20 of file check_health.py.

◆ R_IN

float check_health.R_IN = 0.02

Definition at line 18 of file check_health.py.

◆ R_OUT

float check_health.R_OUT = 0.021

Definition at line 19 of file check_health.py.

◆ TETHER_COM_LIFT

float check_health.TETHER_COM_LIFT = 5.0e-4

Definition at line 27 of file check_health.py.

◆ TETHER_DAMAGE

float check_health.TETHER_DAMAGE = 0.9

Definition at line 28 of file check_health.py.

◆ TETHER_DIST

float check_health.TETHER_DIST = 1.5e-4

Definition at line 26 of file check_health.py.