You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
985 lines
35 KiB
985 lines
35 KiB
import sys
|
|
sys.path.append('/media/digitalstorm/edf2449b-a566-40a2-8d0f-393bbe8ea1c0/maxwell_td_hybrid_mesher/Maxwell_PML_Regular/HybridMesher')
|
|
import Geometry as geo # Custom mesh/geometry handling module
|
|
import numpy as np
|
|
import pandas as pd
|
|
from numpy import linalg as la
|
|
from Geometry import Vector # pybind11 C++ binding
|
|
from scipy.spatial import cKDTree
|
|
|
|
#######################################################
|
|
# CONSTANTS #
|
|
#######################################################
|
|
|
|
PEC = 1
|
|
ABC_PW = 2
|
|
ABC = 3
|
|
Interface_PW = 4
|
|
NCBC = 1000
|
|
|
|
unit2trunc = 4 # Number of significant digits in the regular mesh
|
|
|
|
|
|
########################################
|
|
## AUXILIARY FUNCTIONS ##
|
|
########################################
|
|
|
|
def floor(number, sig_figs):
|
|
return np.trunc(number * (10 ** sig_figs)) / (10 ** sig_figs)
|
|
|
|
|
|
# Handles scalars or arrays and truncates each to the desired significant figures
|
|
def truncate_to_significant_figures(number, sig_figs):
|
|
isNumber = False
|
|
if not isinstance(number, np.ndarray):
|
|
number = np.array([number])
|
|
isNumber = True
|
|
|
|
maskLower = number < 0
|
|
maskGreater = number > 0
|
|
|
|
order_of_magnitude = np.zeros_like(number)
|
|
order_of_magnitude[maskGreater] = np.floor(np.log10(number[maskGreater]))
|
|
order_of_magnitude[maskLower] = np.floor(np.log10(-number[maskLower]))
|
|
|
|
if isNumber:
|
|
return floor(number, -(order_of_magnitude - sig_figs + 1))[0]
|
|
|
|
return floor(number, -(order_of_magnitude - sig_figs + 1))
|
|
|
|
|
|
def build_face_bc_array(mesh, BC, PEC, ABC, NCBC):
|
|
|
|
number_mesh_elems = mesh.info.num_elems
|
|
number_mesh_faces = mesh.info.num_face_sets
|
|
|
|
faceBC = np.zeros((number_mesh_elems, 4), dtype=np.int64)
|
|
|
|
print(number_mesh_faces, " face sets found in the mesh.")
|
|
|
|
# Loops over each face set in the regular mesh (each face set groups faces sharing the same BC).
|
|
# Assign face BCs
|
|
for i in range(number_mesh_faces):
|
|
bc_val = 0
|
|
bc_type = mesh.get_face_set_bc(i)
|
|
if bc_type == BC.PEC:
|
|
bc_val = PEC
|
|
elif bc_type == BC.ABC:
|
|
bc_val = ABC
|
|
elif bc_type == BC.NCB:
|
|
bc_val = NCBC
|
|
|
|
# Each face in the set is identified by a global face ID.
|
|
# faceID // 4 gives the element index.
|
|
# faceID % 4 gives the local face number (0–3).
|
|
# This maps the BC code to the correct element face in regularFacesBC.
|
|
for face_id in mesh.face_set(i):
|
|
elem_idx = face_id // 4
|
|
local_face = face_id % 4
|
|
faceBC[elem_idx][local_face] = bc_val
|
|
return faceBC
|
|
|
|
|
|
def build_element_and_bc_arrays(mesh, faceBC, verticesMap, offset=0):
|
|
|
|
# Pre-allocate space for all tetrahedral elements and face BCs
|
|
# Each tetrahedron has 4 vertices and 4 faces
|
|
number_mesh_elems = mesh.info.num_elems
|
|
number_mesh_faces = mesh.info.num_face_sets
|
|
number_mesh_sets = mesh.info.num_elem_sets
|
|
|
|
# Element and BC arrays for regular mesh
|
|
# elementArray[i] = [v0, v1, v2, v3] stores the vertex indices (global IDs) of the
|
|
# ith tetrahedral element.
|
|
# bcArray[i] = [bc0, bc1, bc2, bc3] stores boundary condition codes for each face (0–3) of element i.
|
|
elementArray = np.empty((number_mesh_elems, 4), dtype=np.int64)
|
|
bcArray = np.zeros((number_mesh_elems, 4), dtype=np.int64)
|
|
nElements = 0
|
|
for i in range(number_mesh_sets):
|
|
|
|
# mesh.elem_set(i) returns a (4, num_elems) array: vertex indices of each tetrahedron in set i
|
|
elemSet = np.array(mesh.elem_set(i))
|
|
num_elems = elemSet.shape[1]
|
|
|
|
# verticesMap[...] remaps vertex indices to global deduplicated indices
|
|
# + offset shifts local indices (e.g., when merging meshes)
|
|
row_range = range(nElements, nElements + num_elems)
|
|
elementArray[row_range, :] = verticesMap[elemSet + offset].T
|
|
|
|
# faceBC[...] holds boundary condition codes per face (e.g., PEC, ABC, etc.)
|
|
# The face permutation [3, 1, 2, 0] is used to reorder face BCs consistently
|
|
bcArray[row_range, :] = faceBC[row_range][:, [3, 1, 2, 0]]
|
|
|
|
nElements += num_elems
|
|
|
|
sorted_indices = np.argsort(elementArray, axis=1)
|
|
|
|
# Sort vertex indices within each tetrahedron to normalize ordering (for hashing, comparison, deduplication)
|
|
# Also permute boundary conditions accordingly so they stay matched to the correct face
|
|
elementArray = np.take_along_axis(elementArray, sorted_indices, axis=1)
|
|
bcArray = np.take_along_axis(bcArray, sorted_indices, axis=1)
|
|
|
|
return elementArray, bcArray
|
|
|
|
|
|
|
|
def snap_to_grid(reference_node, grid_size):
|
|
reference_node = np.asarray(reference_node)
|
|
abs_node = np.abs(reference_node)
|
|
shifted = abs_node + (grid_size / 2.0)
|
|
floored = np.floor(shifted / grid_size)
|
|
snapped_unsigned = floored * grid_size
|
|
snapped = snapped_unsigned * np.sign(reference_node)
|
|
return snapped
|
|
|
|
def snap_vertices_relative_to_center(regVertices, referenceNode, regularElementSize, unit2trunc):
|
|
"""
|
|
Snap regVertices to a grid aligned with a coarsely-snapped referenceNode.
|
|
|
|
Parameters:
|
|
- regVertices: np.ndarray, shape (N, 3)
|
|
The vertices to be snapped to a regular grid.
|
|
- referenceNode: np.ndarray, shape (3,)
|
|
The point around which snapping is aligned.
|
|
- regularElementSize: float
|
|
The fine grid spacing.
|
|
- unit2trunc: float
|
|
The coarser unit to truncate the offset from referenceNode.
|
|
|
|
Returns:
|
|
- snappedVertices: np.ndarray, shape (N, 3)
|
|
The grid-aligned version of regVertices.
|
|
"""
|
|
regVertices = np.asarray(regVertices)
|
|
referenceNode = np.asarray(referenceNode)
|
|
|
|
# Step 1: Snap referenceNode to regular grid
|
|
snapped = snap_to_grid(referenceNode, regularElementSize)
|
|
|
|
# Step 2: Offset from actual reference to snapped
|
|
offset = referenceNode - snapped
|
|
|
|
print("Offset from referenceNode to snapped grid:", offset)
|
|
|
|
# Step 3: Truncate that offset to a coarser grid
|
|
offset_trunc = np.floor(offset / unit2trunc) * unit2trunc
|
|
|
|
# Step 4: Center vertices relative to this truncated offset, then snap
|
|
centered_vertices = regVertices - offset_trunc[:,None] # shape (N, 3)
|
|
|
|
# Apply grid snapping around the center
|
|
snapped_vertices = (
|
|
np.floor((np.abs(centered_vertices) + regularElementSize / 2) / regularElementSize)
|
|
* regularElementSize
|
|
* np.sign(centered_vertices)
|
|
+ offset_trunc[:,None]
|
|
).T
|
|
|
|
return snapped_vertices, offset_trunc
|
|
|
|
|
|
|
|
def snap_offset(vertices, regularElementSize, offset_trunc):
|
|
|
|
# Step 4: Center vertices relative to this truncated offset, then snap
|
|
centered_vertices = vertices - offset_trunc[:,None] # shape (N, 3)
|
|
|
|
# Apply grid snapping around the center
|
|
snapped_vertices = (
|
|
np.floor((np.abs(centered_vertices) + regularElementSize / 2) / regularElementSize)
|
|
* regularElementSize
|
|
* np.sign(centered_vertices)
|
|
+ offset_trunc[:,None]
|
|
).T
|
|
|
|
return snapped_vertices
|
|
|
|
|
|
|
|
|
|
def generate_stacked_pml_layers(base_mesh, number_of_layers, base_size, normal, corner, length, width):
|
|
"""
|
|
Generate multiple stacked PML layers without using geo.Vector.
|
|
|
|
Parameters:
|
|
base_mesh : Initial VolumeMesh (starting layer)
|
|
number_of_layers : Number of stacked layers to generate
|
|
base_size : Grid cube size
|
|
normal : (3,) numpy array of floats
|
|
corner : (3,) numpy array of floats
|
|
length : Length in in-plane axis 1
|
|
width : Width in in-plane axis 2
|
|
|
|
Returns:
|
|
layers : List of VolumeMesh layers
|
|
layer_verts : List of (N_i, 3) numpy vertex arrays per layer
|
|
"""
|
|
import numpy as np
|
|
|
|
layers = []
|
|
layer_verts = []
|
|
|
|
axis = int(np.argmax(np.abs(normal)))
|
|
direction = np.sign(normal[axis])
|
|
|
|
current_mesh = base_mesh
|
|
|
|
for n in range(number_of_layers):
|
|
adjusted_corner = corner.copy()
|
|
adjusted_corner[axis] += direction * base_size * n
|
|
print(f"adjusted_corner: {adjusted_corner}")
|
|
|
|
# Convert to geo.Vector
|
|
normal_vec = Vector(*normal)
|
|
corner_vec = Vector(*adjusted_corner)
|
|
|
|
# Call C++ layer generator
|
|
layer = base_mesh.makeGridPMLLayer(base_size, normal_vec, corner_vec, length, width)
|
|
|
|
verts = np.array(layer.vertices).T
|
|
verts = verts.T
|
|
print(f" Vertices: min = {verts.min(axis=1)}, max = {verts.max(axis=1)}")
|
|
|
|
layers.append(layer)
|
|
layer_verts.append(verts)
|
|
current_mesh = layer
|
|
|
|
return layers, layer_verts
|
|
|
|
|
|
|
|
|
|
def generate_pml_layer(mesh, base_size, reg_min, reg_max):
|
|
"""
|
|
Generate 6-directional PML layers using bounding box of regular region.
|
|
|
|
Parameters:
|
|
mesh : Initial VolumeMesh
|
|
base_size : Cube size for grid elements
|
|
reg_min : Tuple (x, y, z), lower corner of regular mesh
|
|
reg_max : Tuple (x, y, z), upper corner of regular mesh
|
|
|
|
Returns:
|
|
layers: list of VolumeMesh objects (one per layer)
|
|
layer_verts: list of (N_i, 3) numpy arrays of vertices (one per layer)
|
|
"""
|
|
max_x, max_y, max_z = reg_max
|
|
min_x, min_y, min_z = reg_min
|
|
|
|
Length_X = max_x - min_x
|
|
Length_Y = max_y - min_y
|
|
Length_Z = max_z - min_z
|
|
|
|
number_of_layers = 10
|
|
layers = []
|
|
layer_verts = []
|
|
|
|
directions = [
|
|
("+X", [1.0, 0.0, 0.0], [max_x, min_y, min_z], Length_Y, Length_Z),
|
|
("-X", [-1.0, 0.0, 0.0], [min_x, min_y, min_z], Length_Y, Length_Z),
|
|
("+Y", [0.0, 1.0, 0.0], [min_x, max_y, min_z], Length_X, Length_Z),
|
|
("-Y", [0.0, -1.0, 0.0], [min_x, min_y, min_z], Length_X, Length_Z),
|
|
("+Z", [0.0, 0.0, 1.0], [min_x, min_y, max_z], Length_X, Length_Y),
|
|
("-Z", [0.0, 0.0, -1.0], [min_x, min_y, min_z], Length_X, Length_Y),
|
|
]
|
|
|
|
for label, normal, corner, length, width in directions:
|
|
print(f"\n########## GENERATING PML LAYER {label} ##########")
|
|
print(f"Normal: {normal}, Corner: {corner}, Length: {length}, Width: {width}")
|
|
|
|
sub_layers, sub_verts = generate_stacked_pml_layers(
|
|
mesh, number_of_layers, base_size,
|
|
np.array(normal, dtype=float),
|
|
np.array(corner, dtype=float),
|
|
length, width
|
|
)
|
|
|
|
layers.extend(sub_layers)
|
|
layer_verts.extend(sub_verts)
|
|
|
|
return layers, layer_verts
|
|
|
|
|
|
|
|
def generate_cuboid_pml_layer(mesh, base_size, reg_min, reg_max, layer_thickness=10):
|
|
"""
|
|
Generate a full PML box (6-directional shell) with a hollow core surrounding the regular region.
|
|
|
|
Parameters:
|
|
mesh : Initial VolumeMesh object (with method makeGridPMLBox)
|
|
base_size : Cube side length
|
|
reg_min : Tuple (x, y, z) — lower corner of the regular region
|
|
reg_max : Tuple (x, y, z) — upper corner of the regular region
|
|
layer_thickness: Number of grid cells for PML thickness in each direction
|
|
|
|
Returns:
|
|
layers : list with one VolumeMesh PML box
|
|
layer_verts : list with one (N, 3) numpy array of vertices
|
|
"""
|
|
reg_min = np.array(reg_min)
|
|
reg_max = np.array(reg_max)
|
|
reg_dims = reg_max - reg_min
|
|
|
|
# Compute number of grid cells along each axis
|
|
num_cells = np.ceil((reg_dims + 2 * layer_thickness * base_size) / base_size).astype(int)
|
|
total_min = reg_min - layer_thickness * base_size
|
|
total_max = reg_max + layer_thickness * base_size
|
|
|
|
# Compute indices of regular (hollow) region in the grid
|
|
inner_start = np.array([layer_thickness, layer_thickness, layer_thickness])
|
|
inner_end = inner_start + np.floor(reg_dims / base_size).astype(int)
|
|
|
|
# Create the PML box using the C++ function via pybind11
|
|
pml_mesh = mesh.makeGridPMLBox(
|
|
size=base_size,
|
|
numCells=tuple(num_cells),
|
|
innerStart=tuple(inner_start),
|
|
innerEnd=tuple(inner_end),
|
|
corner=tuple(total_min)
|
|
)
|
|
|
|
# Extract vertices as (N, 3) numpy array
|
|
verts = np.array(pml_mesh.vertices) # verts() → shape (3, N), transpose to (N, 3)
|
|
|
|
return pml_mesh, verts
|
|
|
|
|
|
|
|
def export_bc_surfaces(mesh, name, BC):
|
|
"""
|
|
Extracts and saves STL files for selected boundary conditions.
|
|
|
|
Parameters:
|
|
- mesh: VolumeMesh object (e.g., regular or irregular)
|
|
- name: output name prefix
|
|
- BC: BoundaryCondition enum from geo.BoundaryCondition
|
|
"""
|
|
|
|
print(BC)
|
|
|
|
bc_map = {
|
|
BC.PEC: "pec",
|
|
BC.NCB: "ncbc",
|
|
BC.ABC: "abc",
|
|
BC.PML_Excitation: "pml_excitation",
|
|
BC.PML_ABC: "pml_abc"
|
|
}
|
|
|
|
for bc_type, label in bc_map.items():
|
|
face_ids = mesh.face_sets_with_bc(bc_type)
|
|
if face_ids:
|
|
surfs = mesh.extract_surfaces(face_ids)
|
|
surfs.save_stl(f"{name}_{label}_surfaces.stl")
|
|
print(f"Saved: {name}_{label}_surfaces.stl")
|
|
else:
|
|
print(f"[Warning] No faces found for BC.{label.upper()}")
|
|
|
|
|
|
def print_boundary_conditions():
|
|
print("Available Boundary Conditions:")
|
|
for name in dir(geo.BoundaryCondition):
|
|
if not name.startswith("__"):
|
|
value = getattr(geo.BoundaryCondition, name)
|
|
print(f"BC.{name} = {value}")
|
|
|
|
|
|
|
|
def build_face_bc_array_custom(mesh, BC, selected_BCs, output_BC_Number):
|
|
"""
|
|
Builds an array of boundary condition codes for each element face with custom output values.
|
|
|
|
Parameters:
|
|
- mesh: the VolumeMesh object.
|
|
- BC: the BoundaryCondition enum class.
|
|
- selected_BCs: list of BC enum types to assign, e.g., [BC.PEC, BC.ABC]
|
|
- output_BC_Number: list of integer codes corresponding to selected_BCs, e.g., [1, 2]
|
|
|
|
Returns:
|
|
- faceBC: (num_elems, 4) NumPy array of integer BC codes.
|
|
"""
|
|
assert len(selected_BCs) == len(output_BC_Number), "Length mismatch between BC list and output values"
|
|
|
|
number_mesh_elems = mesh.info.num_elems
|
|
number_mesh_faces = mesh.info.num_face_sets
|
|
faceBC = np.zeros((number_mesh_elems, 4), dtype=np.int64)
|
|
|
|
print(number_mesh_faces, "face sets found in the mesh.")
|
|
|
|
# Build dictionary for fast lookup
|
|
bc_to_value = dict(zip(selected_BCs, output_BC_Number))
|
|
|
|
for i in range(number_mesh_faces):
|
|
bc_type = mesh.get_face_set_bc(i)
|
|
|
|
if bc_type not in bc_to_value:
|
|
continue # skip unselected BCs
|
|
|
|
bc_val = bc_to_value[bc_type]
|
|
|
|
for face_id in mesh.face_set(i):
|
|
elem_idx = face_id // 4
|
|
local_face = face_id % 4
|
|
faceBC[elem_idx][local_face] = bc_val
|
|
|
|
return faceBC
|
|
|
|
|
|
def auto_merge_duplicate_nodes(totalNodes, tetra_data, tol=1e-8):
|
|
"""
|
|
Merges nodes that are within `tol` distance and updates tetra connectivity.
|
|
|
|
Parameters:
|
|
- totalNodes: (N, 3) array of node coordinates
|
|
- tetra_data: list of tetrahedra, each as [n0, n1, n2, n3]
|
|
- tol: tolerance to treat nodes as duplicates
|
|
|
|
Returns:
|
|
- new_totalNodes: deduplicated node coordinates
|
|
- new_tetra_data: tetra connectivity updated to deduplicated nodes
|
|
"""
|
|
|
|
tree = cKDTree(totalNodes)
|
|
close_pairs = tree.query_pairs(r=tol)
|
|
|
|
# Step 1: Build a mapping from old index → representative (canonical) index
|
|
parent = np.arange(len(totalNodes)) # Initially, each node is its own parent
|
|
|
|
def find(i):
|
|
while parent[i] != i:
|
|
parent[i] = parent[parent[i]]
|
|
i = parent[i]
|
|
return i
|
|
|
|
def union(i, j):
|
|
pi, pj = find(i), find(j)
|
|
if pi != pj:
|
|
parent[max(pi, pj)] = min(pi, pj)
|
|
|
|
for i, j in close_pairs:
|
|
union(i, j)
|
|
|
|
canonical = np.array([find(i) for i in range(len(totalNodes))])
|
|
|
|
# Step 2: Get unique canonical indices and build remapping
|
|
_, new_indices, inverse = np.unique(totalNodes[canonical], axis=0, return_index=True, return_inverse=True)
|
|
new_totalNodes = totalNodes[canonical][new_indices]
|
|
|
|
# Step 3: Update tetra connectivity using canonical + inverse mapping
|
|
new_tetra_data = []
|
|
for tet in tetra_data:
|
|
updated = [inverse[canonical[v]] for v in tet]
|
|
new_tetra_data.append(updated)
|
|
|
|
return new_totalNodes, new_tetra_data
|
|
|
|
|
|
|
|
def parse_node_file(lines):
|
|
"""
|
|
Parses the `.node` file.
|
|
Each node spans two lines: a header and a line with x y z coordinates.
|
|
"""
|
|
num_nodes = int(lines[1].strip())
|
|
totalNodes = []
|
|
|
|
for i in range(num_nodes):
|
|
coords_line = lines[3 + i * 2].strip() # Skip unit, node count, and header lines
|
|
coords = list(map(float, coords_line.split()))
|
|
totalNodes.append(coords)
|
|
|
|
return np.array(totalNodes)
|
|
|
|
|
|
def parse_tetra_file(lines):
|
|
"""
|
|
Parses the `.tetra` file:
|
|
- First line: number of tets (optional)
|
|
- Even lines: [flag node0 node1 node2 node3]
|
|
- Odd lines: face BCs (ignored)
|
|
Returns:
|
|
- tetra_data: list of [n0, n1, n2, n3]
|
|
"""
|
|
num_tets = int(lines[0].strip())
|
|
tetra_data = []
|
|
|
|
for i in range(num_tets):
|
|
parts = lines[1 + 2 * i].strip().split()
|
|
if len(parts) != 5:
|
|
continue
|
|
_, n0, n1, n2, n3 = map(int, parts)
|
|
tetra_data.append([n0, n1, n2, n3])
|
|
|
|
return tetra_data
|
|
|
|
|
|
|
|
import numpy as np
|
|
|
|
def group_tets_by_shape_material_bc(
|
|
elementArray, bcArray, totalNodes,
|
|
materialArray,
|
|
excluded_bc_values=[],
|
|
tolerance=1e-10):
|
|
"""
|
|
Groups tetrahedra by shape, material, and BC pattern.
|
|
Tets with any face BC in excluded_bc_values are excluded (group ID = 0).
|
|
|
|
Parameters:
|
|
- elementArray: (N, 4) vertex indices per tet
|
|
- bcArray: (N, 4) per-face BC codes
|
|
- totalNodes: (M, 3) node coordinates
|
|
- materialArray: (N,) material ID per tet
|
|
- excluded_bc_values: list of BCs to exclude
|
|
- tolerance: float, edge length quantization threshold
|
|
|
|
Returns:
|
|
- group_indices: (N,) array of group IDs (0 = ungrouped)
|
|
- uniqueGroups: array of unique group IDs
|
|
"""
|
|
num_elems = elementArray.shape[0]
|
|
|
|
# Step 1: Get node positions for each tet
|
|
nodesArray = totalNodes[elementArray] # shape: (N, 4, 3)
|
|
|
|
# Step 2: Create exclusion mask
|
|
exclude_mask = np.isin(bcArray, excluded_bc_values).any(axis=1)
|
|
valid_mask = ~exclude_mask
|
|
valid_indices = np.where(valid_mask)[0]
|
|
|
|
# Step 3: Extract shape info via edge vectors
|
|
valid_nodes = nodesArray[valid_indices]
|
|
edgesArray = np.empty((valid_nodes.shape[0], 3, 3))
|
|
edgesArray[:, 0, :] = valid_nodes[:, 0, :] - valid_nodes[:, 1, :]
|
|
edgesArray[:, 1, :] = valid_nodes[:, 0, :] - valid_nodes[:, 2, :]
|
|
edgesArray[:, 2, :] = valid_nodes[:, 0, :] - valid_nodes[:, 3, :]
|
|
rounded_edges = np.round(edgesArray / tolerance) * tolerance
|
|
shape_key = rounded_edges.reshape(valid_nodes.shape[0], -1)
|
|
|
|
# Step 4: Combine with material and BC
|
|
valid_materials = materialArray[valid_indices].reshape(-1, 1)
|
|
valid_bcs = bcArray[valid_indices]
|
|
full_key = np.hstack([shape_key, valid_materials, valid_bcs])
|
|
|
|
# Step 5: Group by unique key
|
|
_, group_ids = np.unique(full_key, axis=0, return_inverse=True)
|
|
group_ids = group_ids + 1 # reserve group ID 0 for excluded
|
|
|
|
# Step 6: Final group array
|
|
group_indices = np.zeros(num_elems, dtype=np.int64)
|
|
group_indices[valid_indices] = group_ids
|
|
|
|
# Step 7: Recompress
|
|
uniqueGroups, compressed = np.unique(group_indices, return_inverse=True)
|
|
group_indices = compressed
|
|
|
|
# === Diagnostics ===
|
|
print(f"🔢 Total tets: {num_elems}")
|
|
print(f"🚫 Excluded by BC list: {exclude_mask.sum()}")
|
|
print(f"✅ Grouped tets: {len(valid_indices)}")
|
|
print(f"🔁 Unique groups (excluding group 0): {len(uniqueGroups) - 1}")
|
|
|
|
return group_indices, uniqueGroups
|
|
|
|
import numpy as np
|
|
|
|
def group_tets_by_box_and_conductivity(
|
|
elementArray, bcArray, totalNodes,
|
|
materialArray,
|
|
region_corner, region_dims, pml_bounds,
|
|
excluded_bc_values=[],
|
|
sigma_base=0.01, m=3, tolerance=1e-10,
|
|
min_group_size=1,
|
|
):
|
|
num_elems = elementArray.shape[0]
|
|
nodesArray = totalNodes[elementArray]
|
|
centroids = nodesArray.mean(axis=1)
|
|
|
|
core_lower = np.asarray(region_corner)
|
|
core_upper = core_lower + np.asarray(region_dims)
|
|
|
|
inside_mask = np.all((centroids >= core_lower) & (centroids <= core_upper), axis=1)
|
|
outside_mask = ~inside_mask
|
|
|
|
group_indices = np.zeros(num_elems, dtype=np.int64)
|
|
|
|
inside_gid_set = set()
|
|
outside_gid_set = set()
|
|
|
|
# ========================
|
|
# Inside grouping
|
|
# ========================
|
|
inside_indices = np.where(inside_mask)[0]
|
|
inside_nodes = nodesArray[inside_indices]
|
|
inside_bcs = bcArray[inside_indices]
|
|
inside_materials = materialArray[inside_indices]
|
|
|
|
exclude_mask = np.isin(inside_bcs, excluded_bc_values).any(axis=1)
|
|
valid_inside_mask = ~exclude_mask
|
|
valid_inside_indices = inside_indices[valid_inside_mask]
|
|
|
|
valid_nodes = nodesArray[valid_inside_indices]
|
|
edges = np.empty((valid_nodes.shape[0], 3, 3))
|
|
edges[:, 0, :] = valid_nodes[:, 0, :] - valid_nodes[:, 1, :]
|
|
edges[:, 1, :] = valid_nodes[:, 0, :] - valid_nodes[:, 2, :]
|
|
edges[:, 2, :] = valid_nodes[:, 0, :] - valid_nodes[:, 3, :]
|
|
shape_key = np.round(edges / tolerance) * tolerance
|
|
shape_key = shape_key.reshape(valid_nodes.shape[0], -1)
|
|
|
|
valid_materials = materialArray[valid_inside_indices].reshape(-1, 1)
|
|
valid_bcs = bcArray[valid_inside_indices]
|
|
key_inside = np.hstack([shape_key, valid_materials, valid_bcs])
|
|
|
|
_, group_ids_inside = np.unique(key_inside, axis=0, return_inverse=True)
|
|
group_ids_inside = group_ids_inside + 1
|
|
group_indices[valid_inside_indices] = group_ids_inside
|
|
next_group_id = group_ids_inside.max() + 1
|
|
|
|
inside_gid_set.update(group_ids_inside)
|
|
|
|
# ========================
|
|
# Outside grouping
|
|
# ========================
|
|
outside_indices = np.where(outside_mask)[0]
|
|
if outside_indices.size > 0:
|
|
outside_nodes = nodesArray[outside_indices]
|
|
outside_materials = materialArray[outside_indices]
|
|
outside_bcs = bcArray[outside_indices]
|
|
|
|
edges_out = np.empty((outside_nodes.shape[0], 3, 3))
|
|
edges_out[:, 0, :] = outside_nodes[:, 0, :] - outside_nodes[:, 1, :]
|
|
edges_out[:, 1, :] = outside_nodes[:, 0, :] - outside_nodes[:, 2, :]
|
|
edges_out[:, 2, :] = outside_nodes[:, 0, :] - outside_nodes[:, 3, :]
|
|
shape_key_out = np.round(edges_out / tolerance) * tolerance
|
|
shape_key_out = shape_key_out.reshape(outside_nodes.shape[0], -1)
|
|
|
|
rc = centroids[outside_indices]
|
|
dx = np.maximum.reduce([region_corner[0] - rc[:, 0], np.zeros_like(rc[:, 0]), rc[:, 0] - (region_corner[0] + region_dims[0])])
|
|
dy = np.maximum.reduce([region_corner[1] - rc[:, 1], np.zeros_like(rc[:, 1]), rc[:, 1] - (region_corner[1] + region_dims[1])])
|
|
dz = np.maximum.reduce([region_corner[2] - rc[:, 2], np.zeros_like(rc[:, 2]), rc[:, 2] - (region_corner[2] + region_dims[2])])
|
|
|
|
s1 = sigma_base * (dx / pml_bounds[0])**m
|
|
s2 = sigma_base * (dy / pml_bounds[1])**m
|
|
s3 = sigma_base * (dz / pml_bounds[2])**m
|
|
sigmas = np.stack([np.round(s1, 6), np.round(s2, 6), np.round(s3, 6)], axis=1)
|
|
|
|
materials_out = outside_materials.reshape(-1, 1)
|
|
key_outside = np.hstack([shape_key_out, materials_out, outside_bcs, sigmas])
|
|
|
|
_, group_ids_outside = np.unique(key_outside, axis=0, return_inverse=True)
|
|
group_ids_outside = group_ids_outside + next_group_id
|
|
group_indices[outside_indices] = group_ids_outside
|
|
|
|
outside_gid_set.update(group_ids_outside)
|
|
|
|
# ========================
|
|
# Filter small groups
|
|
# ========================
|
|
final = group_indices.copy()
|
|
surviving_inside_gid_set = set()
|
|
surviving_outside_gid_set = set()
|
|
|
|
for gid in np.unique(group_indices):
|
|
if gid == 0:
|
|
continue
|
|
count = np.sum(group_indices == gid)
|
|
if count < min_group_size:
|
|
final[group_indices == gid] = 0
|
|
else:
|
|
if gid in inside_gid_set:
|
|
surviving_inside_gid_set.add(gid)
|
|
elif gid in outside_gid_set:
|
|
surviving_outside_gid_set.add(gid)
|
|
|
|
# ========================
|
|
# Remap surviving group IDs to be contiguous
|
|
# ========================
|
|
uniqueGroups = np.unique(final)
|
|
remap = {old_gid: new_gid for new_gid, old_gid in enumerate(uniqueGroups)}
|
|
group_indices_remapped = np.array([remap[gid] for gid in final], dtype=np.int64)
|
|
|
|
# Compute remapped surviving GIDs
|
|
remapped_inside_ids = sorted([remap[gid] for gid in surviving_inside_gid_set if gid in remap])
|
|
remapped_outside_ids = sorted([remap[gid] for gid in surviving_outside_gid_set if gid in remap])
|
|
|
|
# ========================
|
|
# Diagnostics
|
|
# ========================
|
|
def print_gid_range(name, gid_list):
|
|
if gid_list:
|
|
print(f"🔢 {name} group IDs: min={min(gid_list)}, max={max(gid_list)}")
|
|
else:
|
|
print(f"🔢 {name} group IDs: No valid groups")
|
|
|
|
print(f"🔢 Total tets: {num_elems}")
|
|
print(f"📦 Inside box: {len(inside_indices)} (grouped by shape+material+BC)")
|
|
print(f"🌐 Outside box: {len(outside_indices)} (grouped by +conductivity)")
|
|
print(f"🚫 Groups filtered out (<{min_group_size} tets)")
|
|
print(f"🔁 Unique groups (excluding group 0): {len(uniqueGroups) - (1 if 0 in uniqueGroups else 0)}")
|
|
print_gid_range("Inside", remapped_inside_ids)
|
|
print_gid_range("Outside", remapped_outside_ids)
|
|
|
|
return group_indices_remapped
|
|
|
|
def group_tets_by_box_and_pml(
|
|
elementArray, bcArray, totalNodes,
|
|
materialArray,
|
|
region_corner, region_dims,
|
|
excluded_bc_values=[],
|
|
tolerance=1e-10,
|
|
min_group_size=1,
|
|
):
|
|
num_elems = elementArray.shape[0]
|
|
nodesArray = totalNodes[elementArray]
|
|
centroids = nodesArray.mean(axis=1)
|
|
|
|
core_lower = np.asarray(region_corner)
|
|
core_upper = core_lower + np.asarray(region_dims)
|
|
|
|
inside_mask = np.all((centroids >= core_lower) & (centroids <= core_upper), axis=1)
|
|
outside_mask = ~inside_mask
|
|
|
|
group_indices = np.zeros(num_elems, dtype=np.int64)
|
|
|
|
inside_gid_set = set()
|
|
|
|
# ========================
|
|
# Inside grouping only
|
|
# ========================
|
|
inside_indices = np.where(inside_mask)[0]
|
|
inside_nodes = nodesArray[inside_indices]
|
|
inside_bcs = bcArray[inside_indices]
|
|
inside_materials = materialArray[inside_indices]
|
|
|
|
exclude_mask = np.isin(inside_bcs, excluded_bc_values).any(axis=1)
|
|
valid_inside_mask = ~exclude_mask
|
|
valid_inside_indices = inside_indices[valid_inside_mask]
|
|
|
|
valid_nodes = nodesArray[valid_inside_indices]
|
|
edges = np.empty((valid_nodes.shape[0], 3, 3))
|
|
edges[:, 0, :] = valid_nodes[:, 0, :] - valid_nodes[:, 1, :]
|
|
edges[:, 1, :] = valid_nodes[:, 0, :] - valid_nodes[:, 2, :]
|
|
edges[:, 2, :] = valid_nodes[:, 0, :] - valid_nodes[:, 3, :]
|
|
shape_key = np.round(edges / tolerance) * tolerance
|
|
shape_key = shape_key.reshape(valid_nodes.shape[0], -1)
|
|
|
|
valid_materials = materialArray[valid_inside_indices].reshape(-1, 1)
|
|
valid_bcs = bcArray[valid_inside_indices]
|
|
key_inside = np.hstack([shape_key, valid_materials, valid_bcs])
|
|
|
|
_, group_ids_inside = np.unique(key_inside, axis=0, return_inverse=True)
|
|
group_ids_inside = group_ids_inside + 1
|
|
group_indices[valid_inside_indices] = group_ids_inside
|
|
|
|
inside_gid_set.update(group_ids_inside)
|
|
|
|
# ========================
|
|
# Filter small groups
|
|
# ========================
|
|
final = group_indices.copy()
|
|
surviving_inside_gid_set = set()
|
|
|
|
for gid in np.unique(group_indices):
|
|
if gid == 0:
|
|
continue
|
|
count = np.sum(group_indices == gid)
|
|
if count < min_group_size:
|
|
final[group_indices == gid] = 0
|
|
else:
|
|
surviving_inside_gid_set.add(gid)
|
|
|
|
# ========================
|
|
# Remap surviving group IDs to be contiguous
|
|
# ========================
|
|
uniqueGroups = np.unique(final)
|
|
remap = {old_gid: new_gid for new_gid, old_gid in enumerate(uniqueGroups)}
|
|
group_indices_remapped = np.array([remap[gid] for gid in final], dtype=np.int64)
|
|
|
|
# Compute remapped surviving GIDs
|
|
remapped_inside_ids = sorted([remap[gid] for gid in surviving_inside_gid_set if gid in remap])
|
|
|
|
# ========================
|
|
# Diagnostics
|
|
# ========================
|
|
def print_gid_range(name, gid_list):
|
|
if gid_list:
|
|
print(f"🔢 {name} group IDs: min={min(gid_list)}, max={max(gid_list)}")
|
|
else:
|
|
print(f"🔢 {name} group IDs: No valid groups")
|
|
|
|
print(f"🔢 Total tets: {num_elems}")
|
|
print(f"📦 Inside box: {len(inside_indices)} (grouped by shape+material+BC)")
|
|
print(f"🌐 Outside box: {len(np.where(outside_mask)[0])} (set to group 0)")
|
|
print(f"🚫 Groups filtered out (<{min_group_size} tets)")
|
|
print(f"🔁 Unique groups (excluding group 0): {len(uniqueGroups) - (1 if 0 in uniqueGroups else 0)}")
|
|
print_gid_range("Inside", remapped_inside_ids)
|
|
|
|
return group_indices_remapped
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def group_tets_by_box_and_conductivity_zero_bc_only(
|
|
elementArray, bcArray, totalNodes,
|
|
materialArray,
|
|
region_corner, region_dims, pml_bounds,
|
|
sigma_base=0.01, m=3, tolerance=1e-10,
|
|
min_group_size=1,
|
|
):
|
|
"""
|
|
Group tets by shape+material (+BC==all-zero) inside the box,
|
|
and by shape+material+BC==all-zero+PML conductivity outside the box.
|
|
Only tets with per-face BCs == [0,0,0,0] are eligible for any group.
|
|
Others are forced to group 0.
|
|
"""
|
|
num_elems = elementArray.shape[0]
|
|
nodesArray = totalNodes[elementArray]
|
|
centroids = nodesArray.mean(axis=1)
|
|
|
|
# ---- core box masks ----
|
|
core_lower = np.asarray(region_corner)
|
|
core_upper = core_lower + np.asarray(region_dims)
|
|
inside_mask = np.all((centroids >= core_lower) & (centroids <= core_upper), axis=1)
|
|
outside_mask = ~inside_mask
|
|
|
|
# ---- NEW: require all-zero BCs ----
|
|
bc_all_zero_mask = np.all(bcArray == 0, axis=1)
|
|
|
|
# result container
|
|
group_indices = np.zeros(num_elems, dtype=np.int64)
|
|
|
|
inside_gid_set = set()
|
|
outside_gid_set = set()
|
|
|
|
# ========================
|
|
# Inside grouping (only BC==0 rows)
|
|
# ========================
|
|
inside_indices = np.where(inside_mask & bc_all_zero_mask)[0]
|
|
|
|
if inside_indices.size > 0:
|
|
valid_nodes = nodesArray[inside_indices]
|
|
# Geometry shape key (three edges from node 0)
|
|
edges = np.empty((valid_nodes.shape[0], 3, 3))
|
|
edges[:, 0, :] = valid_nodes[:, 0, :] - valid_nodes[:, 1, :]
|
|
edges[:, 1, :] = valid_nodes[:, 0, :] - valid_nodes[:, 2, :]
|
|
edges[:, 2, :] = valid_nodes[:, 0, :] - valid_nodes[:, 3, :]
|
|
shape_key = np.round(edges / tolerance) * tolerance
|
|
shape_key = shape_key.reshape(valid_nodes.shape[0], -1)
|
|
|
|
valid_materials = materialArray[inside_indices].reshape(-1, 1)
|
|
|
|
# BCs are known to be zeros; you can omit them in the key or keep them for clarity.
|
|
key_inside = np.hstack([shape_key, valid_materials]) # BCs omitted because all-zero
|
|
|
|
_, group_ids_inside = np.unique(key_inside, axis=0, return_inverse=True)
|
|
group_ids_inside = group_ids_inside + 1 # start at 1
|
|
group_indices[inside_indices] = group_ids_inside
|
|
next_group_id = group_ids_inside.max() + 1
|
|
|
|
inside_gid_set.update(group_ids_inside)
|
|
else:
|
|
next_group_id = 1
|
|
|
|
# ========================
|
|
# Outside grouping (only BC==0 rows)
|
|
# ========================
|
|
outside_indices = np.where(outside_mask & bc_all_zero_mask)[0]
|
|
if outside_indices.size > 0:
|
|
outside_nodes = nodesArray[outside_indices]
|
|
outside_materials = materialArray[outside_indices]
|
|
|
|
# Geometry key (same as inside)
|
|
edges_out = np.empty((outside_nodes.shape[0], 3, 3))
|
|
edges_out[:, 0, :] = outside_nodes[:, 0, :] - outside_nodes[:, 1, :]
|
|
edges_out[:, 1, :] = outside_nodes[:, 0, :] - outside_nodes[:, 2, :]
|
|
edges_out[:, 2, :] = outside_nodes[:, 0, :] - outside_nodes[:, 3, :]
|
|
shape_key_out = np.round(edges_out / tolerance) * tolerance
|
|
shape_key_out = shape_key_out.reshape(outside_nodes.shape[0], -1)
|
|
|
|
# Simple “distance outside box” PML conductivity profile (component-wise)
|
|
rc = centroids[outside_indices]
|
|
dx = np.maximum.reduce([
|
|
region_corner[0] - rc[:, 0],
|
|
np.zeros_like(rc[:, 0]),
|
|
rc[:, 0] - (region_corner[0] + region_dims[0]),
|
|
])
|
|
dy = np.maximum.reduce([
|
|
region_corner[1] - rc[:, 1],
|
|
np.zeros_like(rc[:, 1]),
|
|
rc[:, 1] - (region_corner[1] + region_dims[1]),
|
|
])
|
|
dz = np.maximum.reduce([
|
|
region_corner[2] - rc[:, 2],
|
|
np.zeros_like(rc[:, 2]),
|
|
rc[:, 2] - (region_corner[2] + region_dims[2]),
|
|
])
|
|
|
|
s1 = sigma_base * (dx / pml_bounds[0])**m
|
|
s2 = sigma_base * (dy / pml_bounds[1])**m
|
|
s3 = sigma_base * (dz / pml_bounds[2])**m
|
|
sigmas = np.stack([np.round(s1, 6), np.round(s2, 6), np.round(s3, 6)], axis=1)
|
|
|
|
materials_out = outside_materials.reshape(-1, 1)
|
|
|
|
# BCs are all-zero here too; key uses geometry + material + sigmas
|
|
key_outside = np.hstack([shape_key_out, materials_out, sigmas])
|
|
|
|
_, group_ids_outside = np.unique(key_outside, axis=0, return_inverse=True)
|
|
group_ids_outside = group_ids_outside + next_group_id
|
|
group_indices[outside_indices] = group_ids_outside
|
|
|
|
outside_gid_set.update(group_ids_outside)
|
|
|
|
# ========================
|
|
# Filter small groups
|
|
# ========================
|
|
final = group_indices.copy()
|
|
surviving_inside_gid_set = set()
|
|
surviving_outside_gid_set = set()
|
|
|
|
for gid in np.unique(group_indices):
|
|
if gid == 0:
|
|
continue
|
|
count = np.sum(group_indices == gid)
|
|
if count < min_group_size:
|
|
final[group_indices == gid] = 0
|
|
else:
|
|
if gid in inside_gid_set:
|
|
surviving_inside_gid_set.add(gid)
|
|
elif gid in outside_gid_set:
|
|
surviving_outside_gid_set.add(gid)
|
|
|
|
# ========================
|
|
# Remap surviving group IDs to be contiguous (0..K)
|
|
# ========================
|
|
uniqueGroups = np.unique(final)
|
|
remap = {old_gid: new_gid for new_gid, old_gid in enumerate(uniqueGroups)}
|
|
group_indices_remapped = np.array([remap[gid] for gid in final], dtype=np.int64)
|
|
|
|
# Compute remapped surviving GIDs
|
|
remapped_inside_ids = sorted([remap[gid] for gid in surviving_inside_gid_set if gid in remap])
|
|
remapped_outside_ids = sorted([remap[gid] for gid in surviving_outside_gid_set if gid in remap])
|
|
|
|
# ========================
|
|
# Diagnostics
|
|
# ========================
|
|
def print_gid_range(name, gid_list):
|
|
if gid_list:
|
|
print(f"🔢 {name} group IDs: min={min(gid_list)}, max={max(gid_list)}")
|
|
else:
|
|
print(f"🔢 {name} group IDs: No valid groups")
|
|
|
|
print(f"🔢 Total tets: {num_elems}")
|
|
print(f"✅ Eligible (BC==[0,0,0,0]): {int(bc_all_zero_mask.sum())}")
|
|
print(f"🚫 Ineligible (any non-zero BC): {int((~bc_all_zero_mask).sum())}")
|
|
print(f"📦 Inside box (eligible only): {len(inside_indices)}")
|
|
print(f"🌐 Outside box (eligible only): {len(outside_indices)}")
|
|
print(f"🚫 Groups filtered out (<{min_group_size} tets)")
|
|
print(f"🔁 Unique groups (excluding group 0): {len(uniqueGroups) - (1 if 0 in uniqueGroups else 0)}")
|
|
print_gid_range("Inside", remapped_inside_ids)
|
|
print_gid_range("Outside", remapped_outside_ids)
|
|
|
|
return group_indices_remapped
|
|
|
|
|