Source code for packing_defect.core.cluster

# packing_defect/core/cluster.py

import numpy as np


[docs] class DefectClustering: """Connected-component analysis for defect masks. Methods operate on 2D integer masks, typically produced from ``DefectGrid``. Non-zero entries are treated as part of a defect. """
[docs] @staticmethod def defect_size(matrices, nbins, bin_max, fname, prob=True): """Compute and write a histogram of defect cluster sizes. Parameters ---------- matrices : sequence[np.ndarray] Iterable of 2D binary masks (1 for defect cell, 0 otherwise). nbins : int Number of histogram bins. bin_max : float Upper edge of the histogram range. fname : str Output path to write two-column text: center, value. prob : bool, optional If True, normalize counts to probabilities. """ bins = np.linspace(0, bin_max, nbins) defects = [] for matrix in matrices: graph = DefectClustering._make_graph(matrix) visited = set() for node in graph: if node not in visited: cluster = DefectClustering._dfs(graph, node) visited |= cluster defects.append(len(cluster)) hist, _ = np.histogram(defects, bins) hist = hist.astype(np.float64) binp = 0.5 * (_[1:] + _[:-1]) if np.sum(hist) == 0: return if prob: hist /= np.sum(hist) np.savetxt(fname, np.column_stack((binp, hist)), fmt="%8.5f")
[docs] @staticmethod def cluster_sizes_from_mask(matrix: np.ndarray) -> list[int]: """Return sizes of all connected components in a binary mask. Parameters ---------- matrix : np.ndarray 2D array with 1 for defect cells and 0 otherwise. Returns ------- list[int] Sizes (in cells) of each connected component. """ graph = DefectClustering._make_graph(matrix) visited, sizes = set(), [] for node in graph: if node not in visited: comp = DefectClustering._dfs(graph, node) visited |= comp sizes.append(len(comp)) return sizes
@staticmethod def _dfs(graph, start): """Depth-first search on an adjacency graph from ``start``.""" visited, stack = set(), [start] while stack: node = stack.pop() if node not in visited: visited.add(node) stack.extend(graph[node] - visited) return visited @staticmethod def _make_graph(matrix): """Build an 8-connected adjacency graph from a binary mask. Nodes are linear indices; neighbors wrap around the domain (PBC). """ graph = {} rows, cols = matrix.shape for (x, y), val in np.ndenumerate(matrix): if val == 0: continue idx = x * cols + y neighbors = set() for dx in [-1, 0, 1]: for dy in [-1, 0, 1]: nx = (x + dx) % rows ny = (y + dy) % cols if matrix[nx, ny] == 1: nidx = nx * cols + ny neighbors.add(nidx) neighbors.discard(idx) graph[idx] = neighbors return graph