# Copyright (c) 2022-2026, Yongchao Wu in Aalto University# This file is from the mdapy project, released under the BSD 3-Clause License.frommdapyimport_neighborfrommdapy.systemimportSystemfrommdapy.parallelimportget_num_threadsimportnumpyasnpimportpolarsasplfromtypingimportOptional
[docs]classVoidAnalysis:""" Detect voids in an atomic system by discretizing the simulation box into cells and locating empty cells. Parameters ---------- system : System The atomic system containing coordinates and box information. rc : float Cutoff radius that controls the size of the spatial grid cells. Attributes ---------- system : System Input system. rc : float Grid cell size parameter. void_system : Optional[System] A ``System`` containing the centers of detected void regions. ``None`` if no voids are found. void_number : int Number of distinct void clusters. void_volume : float Estimated total void volume, computed as ``N_void_cells * rc^3``. """def__init__(self,system:System,rc:float):self.system=systemself.rc=rcself.void_system:Optional[System]=None
[docs]defcompute(self):""" Perform the void detection. 1. Compute the number of grid cells ``ncell`` along each dimension based on the system thickness and cutoff ``rc``. 2. Use ``_neighbor._fill_cell_for_void`` to assign each cell a value: ``0`` for empty, ``1`` for occupied. 3. If empty cells exist, compute their geometric centers and create a ``System`` object containing void positions. 4. Perform cluster analysis on these void positions. 5. Filter clusters that contain only one void cell (noise avoidance). 6. Renumber clusters and compute final ``void_number`` and ``void_volume``. """ncell=np.zeros(3,np.int32)thickness=self.system.box.get_thickness()foriinrange(3):ncell[i]=max(int(np.floor(thickness[i]/self.rc)),3)cell_id_list=_neighbor._fill_cell_for_void(self.system.data["x"].to_numpy(allow_copy=False),self.system.data["y"].to_numpy(allow_copy=False),self.system.data["z"].to_numpy(allow_copy=False),self.system.box.box,self.system.box.origin,self.system.box.boundary,self.rc,get_num_threads(),)if0incell_id_list:# Compute the geometric center of empty cellsvoid_pos=((np.argwhere(cell_id_list==0)+0.5)/ncell)@self.system.box.box+self.system.box.originvoid_system=System(pos=void_pos,box=self.system.box)# Cluster detection among void pointsvoid_system.cal_cluster_analysis(rc=self.rc*1.1)res=(void_system.data.group_by(["cluster_id"]).len().filter(pl.col("len")>1)["cluster_id"])iflen(res)>0:unique_res=res.unique().sort()new_cluster_id={j:ifori,jinenumerate(unique_res,start=1)}void_system.update_data(void_system.data.filter(pl.col("cluster_id").is_in(res.implode())).with_columns(pl.col("cluster_id").replace_strict(new_cluster_id).rechunk(),pl.lit("X").alias("element"),))self.void_system=void_systemself.void_number=len(unique_res)self.void_volume=self.void_system.N*self.rc**3else:self.void_number=0self.void_volume=0.0else:self.void_number=0self.void_volume=0.0