from mesh_utils import * ####################################################### ## ## ## PART TO MODIFY ## ## ## ####################################################### ####################################################### directory = "./" # Absolute path to the model files fileName = "PW" # Name of the .g and .json files unit = 1.0 # Unit of the input (in meters) -> 1.0 for meters or 2.54e-2 for inches mesh_size = 0.02 # Mesh size in the unit used unit2trunc = 4 # Number of significant digits in the regular mesh #number_of_layers = 5 # Number of PML layers to generate #sigma = 0.01 ######################################## ## MAIN FUNCTION ## ######################################## # Setup full file path and mesh size name = f"{directory}/{fileName}" regularElementSize = truncate_to_significant_figures(mesh_size, 4) # Load and configure volume mesh mesh = geo.VolumeMesh.from_exodus(f"{name}.g") config = mesh.apply_config(f"{name}.json") BC = geo.BoundaryCondition print_boundary_conditions() # Check PEC boundaries pec_faces = mesh.face_sets_with_bc(BC.PEC) if len(pec_faces) == 0: print("Error: No PEC faces found.") print("PEC face set size:", len(pec_faces)) ########################################### ## Generate Regular Airbox Mesh ## ########################################### regular = mesh.make_Airbox_pml_grid_mesh(regularElementSize, pec_faces) regVertices = np.array(regular.vertices) #print("Regular Vertices = ", regular.vertices) print("------------------------------------------------") print("Shape of regular vertices = ", regVertices.shape) print("------------------------------------------------") reg_min = np.min(regVertices, axis=0) reg_max = np.max(regVertices, axis=0) print("########################################") print(" Regular Grid Bounding Box") print("########################################") print(f"X range: {reg_min[0]:.6f} to {reg_max[0]:.6f}") print(f"Y range: {reg_min[1]:.6f} to {reg_max[1]:.6f}") print(f"Z range: {reg_min[2]:.6f} to {reg_max[2]:.6f}") print(f"Box Size (dx, dy, dz): {reg_max - reg_min}") print("########################################") # First vertex is used as the reference node referenceNode = regular.vertices[:, 0] print("First Node from regular tets = ", referenceNode) regularVertices = regular.vertices.shape[0] export_bc_surfaces(regular, "Regular", BC) ######################################## ## Generate Irregular Mesh ## ######################################## irregular = mesh.make_hybrid_mesh_Airbox( regularElementSize, pec_faces, max_volume=(regularElementSize ** 3) * (np.sqrt(2) / 12) ) print("------------------------------------------------") #print("Irregular Vertices = ", irregular.vertices) print("Irregular Vertices Shape = ", irregular.vertices.shape) print("------------------------------------------------") export_bc_surfaces(irregular, "Irregular", BC) # ------------------------------- # Deduplicate and export node list # The unique set of nodes # The indices of the first occurrences # A mapping array from original to deduplicated nodes # The count of how many times each unique row occurred (but unused here) # ------------------------------- print("Shape of regular vertices:", np.array(regular.vertices).T.shape) print("Shape of irregular vertices:", np.array(irregular.vertices).T.shape) # Stack all vertex coordinates for deduplication totalNodes = np.vstack(( np.array(irregular.vertices).T, np.array(regular.vertices).T, )) print("🔢 Number of nodes before rounding/deduplication:", totalNodes.shape[0]) print("TotalNodes = ",totalNodes.shape) # 2. Round coordinates to a precision (e.g., 1e-8) precision = 8 totalNodes = np.round(totalNodes, decimals=precision) # Deduplicate and get mapping to global IDs totalNodes, indices, verticesMap, _ = np.unique(totalNodes, axis=0, return_index=True, return_inverse=True, return_counts=True) print("✅ Number of unique nodes after rounding:", totalNodes.shape[0]) # Save to .node file np.savetxt(f'{name}.node', totalNodes, newline="\n3 0 0\n" , fmt='%f', header=f"{unit}\n{totalNodes.shape[0]}", comments='') print("Total deduplicated nodes:", totalNodes.shape[0]) ######################################## ## IRREGULAR ELEMENTS ## ######################################## offset = 0 # Build face BC array for irregular mesh selected_BC = [BC.PEC] output_BC_number = [1] irregularFacesBC = build_face_bc_array_custom(irregular, BC, selected_BC, output_BC_number) # Build element and face BC arrays for irregular mesh irregularElementArray, irregularBcArray = build_element_and_bc_arrays( irregular, irregularFacesBC, verticesMap, offset=offset) print("Unique face BC values in irregularFacesBC:") print(np.unique(irregularFacesBC)) offset += irregular.vertices.shape[1] ######################################## ## REGULAR ELEMENTS ## ######################################## ### This section processes regular mesh elements by assigning boundary conditions (BCs), ### building element connectivity arrays, and canonically ordering nodes for consistency. selected_BC = [BC.PML_Excitation] output_BC_number = [2] regularFacesBC = build_face_bc_array_custom(regular, BC, selected_BC, output_BC_number) elementArray, bcArray = build_element_and_bc_arrays(regular, regularFacesBC, verticesMap,offset=offset) offset += regular.vertices.shape[1] print("Unique face BC values in regularFacesBC:") print(np.unique(regularFacesBC)) print("Shape of elementArray:", elementArray.shape) print("Max vertex index used:", np.max(elementArray)) surfs = irregular.extract_surfaces(irregular.face_sets_with_bc(BC.PEC)) surfs.save_stl(f"{name}_regular_pec_surfaces.stl") surfs = regular.extract_surfaces(regular.face_sets_with_bc(BC.NCB)) surfs.save_stl(f"{name}_regular_ncbc_surfaces.stl") surfs = regular.extract_surfaces(regular.face_sets_with_bc(BC.ABC)) surfs.save_stl(f"{name}_regular_abc_surfaces.stl") ######################################## ## BUILD FULL ARRAYS ## ######################################## # Concatenate tetrahedral elements in order: PML, regular, irregular full_element_array = np.vstack([irregularElementArray, elementArray]) # Total element counts Nirreg = irregularElementArray.shape[0] Nreg = elementArray.shape[0] Ntotal = Nirreg + Nreg assert full_element_array.shape[0] == Ntotal ######################################## ## MATERIAL ASSIGNMENT ## ######################################## material_flags = np.zeros(Ntotal, dtype=np.int64) ######################################## ## BUILD BC ARRAY ## ######################################## # Ensure BC arrays match the stacked order bc_full_array = np.vstack([ irregularBcArray, # shape: (Nirreg, 4) bcArray, # regular, shape: (Nreg, 4) ]) assert bc_full_array.shape == (Ntotal, 4) print("Unique BC codes in full BC array:",np.unique(bc_full_array)) ################################# ## GROUPING REGULARS ## ################################# # Concatenate tetrahedral elements in order: PML, regular, irregular combined_element_array = np.vstack([elementArray]) combined_bc_array = np.vstack([ bcArray, # regular, shape: (Nreg, 4) ]) temp_material_flags = np.zeros(Nreg, dtype=np.int64) print("Unique bc IDS = ", np.unique(combined_bc_array)) print("==================================================") print("Grouping regular elements by geometry...") print("==================================================") # Bounding box of regular airbox minX = np.min(regVertices[0, :]) maxX = np.max(regVertices[0, :]) minY = np.min(regVertices[1, :]) maxY = np.max(regVertices[1, :]) minZ = np.min(regVertices[2, :]) maxZ = np.max(regVertices[2, :]) reg_corner = np.array([minX, minY, minZ]) reg_dims = np.array([maxX - minX, maxY - minY, maxZ - minZ]) print("Bounding box for regular region grouping:") print(reg_corner, reg_corner + reg_dims) Excitation_minZ = minZ Excitation_maxZ = maxZ Excitation_minX = minX Excitation_maxX = maxX Excitation_minY = minY Excitation_maxY = maxY thickness = 0 pml_bounds=(thickness,thickness,thickness) reg_indices = group_tets_by_box_and_pml( elementArray=combined_element_array, bcArray=combined_bc_array, totalNodes=totalNodes, materialArray=temp_material_flags, region_corner=reg_corner , region_dims=reg_dims , excluded_bc_values=[2,3], min_group_size=100) print("=====================================") vals, counts = np.unique(reg_indices, return_counts=True) print("Counts (including 0):") for v, c in zip(vals, counts): print(f"{int(v)}: {int(c)}") print("=====================================") ################################# ## CONSTRUCT FINAL INDEX ## ################################# print("==================================================") print("Constructing overall global regular indices ...") print("==================================================") # Combine all tets and group indices full_element_array = np.vstack([irregularElementArray, elementArray]) final_indices = np.hstack([ np.zeros(irregularElementArray.shape[0], dtype=np.int64), # Irregular reg_indices # Regular ]) print("=====================================") vals, counts = np.unique(final_indices, return_counts=True) print("Final Counts (including 0):") for v, c in zip(vals, counts): print(f"{int(v)}: {int(c)}") print("=====================================") print("final_indices shape:", final_indices.shape) print("Max group ID = ", np.sort(np.unique(final_indices))) assert final_indices.shape[0] == full_element_array.shape[0], \ "Mismatch between final_indices and total element count!" # Save .regular group file (now matches .tetra) total_group_count = np.max(final_indices) + 1 np.savetxt(f"{name}.regular", final_indices, fmt='%d', header=str(total_group_count), comments='') print(f"Saved {final_indices.shape[0]} group indices to {name}.regular") ######################################## ## WRITE TO .tetra FILE ## ######################################## c_with_extra = np.ones((2 * Ntotal, 5), dtype=np.int64) * (-1) # Even rows: material + connectivity c_with_extra[::2] = np.hstack(( material_flags.reshape(-1, 1), full_element_array )) # Odd rows: face BCs c_with_extra[1::2, 1:] = bc_full_array # Format as DataFrame for easier output df = pd.DataFrame(c_with_extra, dtype=np.int64).where(lambda x: x != -1, np.nan) # Insert element count header header_row = pd.Series([Ntotal] + [np.nan] * 4, index=df.columns) df = pd.concat([header_row.to_frame().T, df], ignore_index=True) # Save as .tetra file df = df.astype("Int64") df.to_csv(f"{name}.tetra", sep=' ', index=False, header=False) print(f"✅ Saved {Ntotal} tetrahedra to {name}.tetra") ################################# ## WRITE FILES ## ################################# print("Writing freespace.m...") with open("freespace.m", "w") as f: # First 6 rows: identity tensors f.write("standard\n") f.write("1.000000e+00 0.000000e+00 0.000000e+00 0.000000e+00 0.000000e+00 0.000000e+00\n") f.write("0.000000e+00 0.000000e+00 1.000000e+00 0.000000e+00 0.000000e+00 0.000000e+00\n") f.write("0.000000e+00 0.000000e+00 0.000000e+00 0.000000e+00 1.000000e+00 0.000000e+00\n") f.write("1.000000e+00 0.000000e+00 0.000000e+00 0.000000e+00 0.000000e+00 0.000000e+00\n") f.write("0.000000e+00 0.000000e+00 1.000000e+00 0.000000e+00 0.000000e+00 0.000000e+00\n") f.write("0.000000e+00 0.000000e+00 0.000000e+00 0.000000e+00 1.000000e+00 0.000000e+00\n") # 3 rows of zero vectors (dispersion parameters) for _ in range(3): f.write("0.000000e+00 0.000000e+00 0.000000e+00\n") print("Writing .bc file...") with open(f"{name}.bc", "w") as f: f.write("4\n") f.write("0 none\n") f.write("1 pec\n") f.write(f"2 planeWave Excitation 1.0 0.0 0.0 0 1 0 0.0 0.0 {Excitation_minZ}\n") f.write("3 abc 1\n") with open(f"{name}.prop", "w") as f: f.write("./\n") f.write("freespace\n")