42 X: np.ndarray, D: np.ndarray |
None =
None, link: float = CLUSTER_LINK, dam_cut: float = 0.75
43) -> list[tuple[np.ndarray, int]]:
44 """Union-find clusters. Highly damaged nodes are removed so a crack is a cut."""
50 alive = np.ones(len(X), dtype=bool)
55 def find(a: int) -> int:
57 parent[a] = parent[parent[a]]
61 def union(a: int, b: int) ->
None:
62 ra, rb = find(a), find(b)
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:
71 groups: dict[int, list[int]] = {}
74 groups.setdefault(r, []).append(i)
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)
84 files = sorted(out.glob(
"output_*.vtu"), key=frame_index)
89 pid = m.point_data[
"Particle_ID"].ravel().astype(int)
91 D_all = np.asarray(m.point_data[
"Damage"]).ravel()
92 V_all = np.asarray(m.point_data[
"Velocity"])
95 tip = X[tri][np.argmax(X[tri, 1])]
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))
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)
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]))
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,
121 "min_d_tip": float(d_tip[j]),
122 "stuck_dam": float(D[j]),
124 "n_big_clusters": n_big,
126 "cluster_sizes": [sz
for _, sz
in clusters[:4]],