Source code for plot3d.facefunctions

from typing import Dict, List, Optional, Tuple
from .listfunctions import unique_pairs
from .block import Block, reduce_blocks
from .face import Face 
from copy import deepcopy
import numpy.typing as npt
import numpy as np
import math

[docs] def faces_match(face1: Tuple[npt.NDArray, npt.NDArray, npt.NDArray],face2: Tuple[npt.NDArray, npt.NDArray, npt.NDArray],tol: float = 1e-12) -> Tuple[bool, Optional[Tuple[bool, bool]]]: """ Compare two block faces and return whether they match and the flip required on face2 to match face1. Returns (True, (flip_ud, flip_lr)) if matching, otherwise (False, None). """ def get_corners(X, Y, Z): return np.array([ [X[0, 0], Y[0, 0], Z[0, 0]], [X[0, -1], Y[0, -1], Z[0, -1]], [X[-1, 0], Y[-1, 0], Z[-1, 0]], [X[-1, -1], Y[-1, -1], Z[-1, -1]], ]) X1, Y1, Z1 = face1 X2, Y2, Z2 = face2 if X1.shape != X2.shape: return False, None corners1 = get_corners(X1, Y1, Z1) for flip_ud in [False, True]: for flip_lr in [False, True]: X2f, Y2f, Z2f = X2.copy(), Y2.copy(), Z2.copy() if flip_ud: X2f, Y2f, Z2f = np.flip(X2f, axis=0), np.flip(Y2f, axis=0), np.flip(Z2f, axis=0) if flip_lr: X2f, Y2f, Z2f = np.flip(X2f, axis=1), np.flip(Y2f, axis=1), np.flip(Z2f, axis=1) corners2 = get_corners(X2f, Y2f, Z2f) diffs = np.linalg.norm(corners1 - corners2, axis=1) if np.all(diffs <= tol): return True, (flip_ud, flip_lr) return False, None
[docs] def find_matching_faces(block1, block2, tol=1e-8): """ Returns a tuple (face1_name, face2_name, flip_flags) if a matching face is found. Otherwise returns (None, None, None). """ faces1 = block1.get_faces() faces2 = block2.get_faces() for face1_name, face1_data in faces1.items(): for face2_name, face2_data in faces2.items(): match, flip_flags = faces_match(face1_data, face2_data, tol=tol) if match: return face1_name, face2_name, flip_flags return None, None, None
[docs] def get_outer_faces(block1:Block): """Get the outer faces of a block Args: block1 (Block): A plot3D block Returns: List[Face]: Non matching faces of the block List[(Face,Face)]: Matching faces inside the block """ I = [0,block1.IMAX-1] # Python index starts at 0, need to subtract 1 for it to get the i,j,k J = [0,block1.JMAX-1] K = [0,block1.KMAX-1] # Create the outer faces faces = list() face = Face(4) i=I[0] for j in J: for k in K: face.add_vertex(block1.X[i,j,k], block1.Y[i,j,k], block1.Z[i,j,k],i,j,k) faces.append(face) face = Face(4) i=I[1] for j in J: for k in K: face.add_vertex(block1.X[i,j,k], block1.Y[i,j,k], block1.Z[i,j,k],i,j,k) faces.append(face) face = Face(4) j=J[0] for i in I: for k in K: face.add_vertex(block1.X[i,j,k], block1.Y[i,j,k], block1.Z[i,j,k],i,j,k) faces.append(face) face = Face(4) j=J[1] for i in I: for k in K: face.add_vertex(block1.X[i,j,k], block1.Y[i,j,k], block1.Z[i,j,k],i,j,k) faces.append(face) face = Face(4) k=K[0] for i in I: for j in J: face.add_vertex(block1.X[i,j,k], block1.Y[i,j,k], block1.Z[i,j,k],i,j,k) faces.append(face) face = Face(4) k=K[1] for i in I: for j in J: face.add_vertex(block1.X[i,j,k], block1.Y[i,j,k], block1.Z[i,j,k],i,j,k) faces.append(face) # Check if faces match each other matching = list() non_matching = list() for i in range(len(faces)): matchFound = False for j in range(len(faces)): if (i!=j and faces[i].vertices_equals(faces[j])): matching.append((i,j)) matchFound = True if not matchFound: non_matching.append(faces[i]) # these are guaranteed to be exterior matching = list(unique_pairs(matching)) matching = [(faces[i],faces[j]) for i,j in matching] # Make sure normals do not intersect # block_center_to_face_center = block1.cx return non_matching, matching # these should be the outer faces
[docs] def create_face_from_diagonals(block:Block,imin:int,jmin:int,kmin:int,imax:int,jmax:int,kmax:int) -> Face: """Creates a face on a block given a the diagonals defined as (IMIN,JMIN,KMIN), (IMAX, JMAX, KMAX) Args: block (Block): Block to create a face on imin (int): Lower Corner IMIN jmin (int): Lower Corner JMIN kmin (int): Lower Corner KMIN imax (int): Upper Corner IMAX jmax (int): Upper Corner JMAX kmax (int): Upper Corner Returns: (Face): Face created from diagonals """ newFace = Face(4) # This is because two of the corners either imin or imax can be equal if imin==imax: i = imin for j in [jmin,jmax]: for k in [kmin,kmax]: x = block.X[i,j,k] y = block.Y[i,j,k] z = block.Z[i,j,k] newFace.add_vertex(x,y,z,i,j,k) elif jmin==jmax: j = jmin for i in [imin,imax]: for k in [kmin,kmax]: x = block.X[i,j,k] y = block.Y[i,j,k] z = block.Z[i,j,k] newFace.add_vertex(x,y,z,i,j,k) elif kmin==kmax: k = kmin for i in [imin,imax]: for j in [jmin,jmax]: x = block.X[i,j,k] y = block.Y[i,j,k] z = block.Z[i,j,k] newFace.add_vertex(x,y,z,i,j,k) return newFace
AxisMap = {"x": 0, "y": 1, "z": 2} def _face_axis_extreme(face: Face, axis: str) -> Tuple[float, float]: """Return (vmin, vmax) of the face along axis ∈ {'x','y','z'} using stored vertices.""" if axis == "x": arr = face.x[:face.nvertex] elif axis == "y": arr = face.y[:face.nvertex] else: arr = face.z[:face.nvertex] return float(np.min(arr)), float(np.max(arr)) def _global_axis_extreme(blocks: List[Block], axis: str) -> Tuple[float, float]: """Global (min, max) along axis across all blocks.""" vals = [] idx = AxisMap[axis] for b in blocks: if idx == 0: vals.append(b.X.reshape(-1)) elif idx == 1: vals.append(b.Y.reshape(-1)) else: vals.append(b.Z.reshape(-1)) cat = np.concatenate(vals, axis=0) return float(cat.min()), float(cat.max()) def _select_seed_faces(outer_faces: List[Face], blocks: List[Block], axis: str, side: str, tol_abs: float) -> List[Face]: """Pick all outer faces whose face extreme equals the global extreme within tol.""" gmin, gmax = _global_axis_extreme(blocks, axis) target = gmin if side == "min" else gmax seeds: List[Face] = [] for f in outer_faces: fmin, fmax = _face_axis_extreme(f, axis) face_ext = fmin if side == "min" else fmax if abs(face_ext - target) <= tol_abs: seeds.append(f) return seeds def _bfs_collect_boundary(seed_faces: List[Face], all_outer_faces: List[Face], blocks: List[Block], axis: str, side: str, tol_abs: float, node_tol_xyz: float, min_shared_abs: int = 2, min_shared_frac: float = 0.005) -> List[Face]: """ Flood-fill over outer faces: neighbors share nodes (not just centroids) and lie on the same extreme plane (within tol_abs). """ # index faces by (block, IMIN..KMAX) for visited bookkeeping def key(f: Face) -> Tuple[int,int,int,int,int,int,int]: return (f.BlockIndex, f.IMIN, f.JMIN, f.KMIN, f.IMAX, f.JMAX, f.KMAX) # all faces that lie on the extreme plane on_plane = [] for f in all_outer_faces: fmin, fmax = _face_axis_extreme(f, axis) v = fmin if side == "min" else fmax # Require the *entire* face to be on/above(below) the plane within tol to avoid picking X side gmin, gmax = _global_axis_extreme(blocks, axis) plane = gmin if side == "min" else gmax # Face must touch the plane and not protrude past it more than tol_abs touch_plane = abs(v - plane) <= tol_abs # The opposite extreme should not "cross" the plane the wrong way opp = fmax if side == "min" else fmin not_past = (opp - plane <= tol_abs) if side == "min" else (plane - opp <= tol_abs) if touch_plane and not_past: on_plane.append(f) pool = {key(f): f for f in on_plane} q = [f for f in seed_faces if key(f) in pool] visited = set() result: List[Face] = [] while q: cur = q.pop() kcur = key(cur) if kcur in visited: continue visited.add(kcur) result.append(cur) # find neighbors by node-sharing (fast and reliable for structured grids) for k2, cand in list(pool.items()): if k2 in visited or k2 == kcur: continue if cur.touches_by_nodes(cand, blocks[cur.BlockIndex], blocks[cand.BlockIndex], tol_xyz=node_tol_xyz, min_shared_abs=min_shared_abs, min_shared_frac=min_shared_frac, stride_u=1, stride_v=1): q.append(cand) return result
[docs] def find_bounding_faces(blocks: List[Block], outer_faces: List[Dict[str,int]], direction: str = "z", side: str = "both", tol_rel: float = 1e-8, node_tol_xyz: float = 1e-6) -> Tuple[List[Dict[str,int]], List[Dict[str,int]],List[Face], List[Face]]: """ Find *outer* bounding faces at the global min/max of a given direction ('x','y','z'). Uses node-sharing BFS to gather continuous boundary patches across blocks. Args: blocks: list of Block outer_faces: optional precomputed outer faces (dict form) direction: 'x','y','z' side: 'both' | 'min' | 'max' tol_rel: relative tolerance against global extreme value node_tol_xyz: absolute node matching tolerance (geometry units) Returns: lower_connected_faces_export, upper_connected_faces_export, lower_connected_faces, upper_connected_faces """ # 1) Reduce by GCD so grids line up gcd_array = [math.gcd(b.IMAX-1, math.gcd(b.JMAX-1, b.KMAX-1)) for b in blocks] gcd_to_use = min(gcd_array) blocks_r = reduce_blocks(deepcopy(blocks), gcd_to_use) # 2) Build outer face list at reduced resolution if len(outer_faces) == 0: outer_faces_all: List[Face] = [] for bi, b in enumerate(blocks_r): outs, _ = get_outer_faces(b) for o in outs: o.set_block_index(bi) outer_faces_all.extend(outs) else: outer_faces_all = outer_face_dict_to_list(blocks_r, outer_faces, gcd_to_use) # 3) Absolute tolerance for plane selection gmin, gmax = _global_axis_extreme(blocks_r, direction) # scale tolerance by magnitude so meshes near origin work too tol_abs = max(1.0, abs(gmin) + abs(gmax)) * tol_rel # 4) Seeds on min/max planes want_min = (side in ("min", "both")) want_max = (side in ("max", "both")) lower_connected_faces: List[Face] = [] upper_connected_faces: List[Face] = [] if want_min: seeds_min = _select_seed_faces(outer_faces_all, blocks_r, direction, "min", tol_abs) lower_connected_faces = _bfs_collect_boundary( seed_faces=seeds_min, all_outer_faces=outer_faces_all, blocks=blocks_r, axis=direction, side="min", tol_abs=tol_abs, node_tol_xyz=node_tol_xyz, min_shared_abs=2, # edge-wise continuity OK min_shared_frac=0.005 # small overlap fraction enough to chain ) if want_max: seeds_max = _select_seed_faces(outer_faces_all, blocks_r, direction, "max", tol_abs) upper_connected_faces = _bfs_collect_boundary( seed_faces=seeds_max, all_outer_faces=outer_faces_all, blocks=blocks_r, axis=direction, side="max", tol_abs=tol_abs, node_tol_xyz=node_tol_xyz, min_shared_abs=2, min_shared_frac=0.005 ) # 5) Scale indices back up to the original grid # (Make copies so we don't mutate faces held elsewhere.) def _rescale_faces(faces: List[Face]) -> List[Face]: out: List[Face] = [] for f in faces: g = Face() g.x = f.x.copy(); g.y = f.y.copy(); g.z = f.z.copy() g.I = (f.I * gcd_to_use).astype(f.I.dtype) g.J = (f.J * gcd_to_use).astype(f.J.dtype) g.K = (f.K * gcd_to_use).astype(f.K.dtype) g.nvertex = f.nvertex g.cx, g.cy, g.cz = f.cx, f.cy, f.cz g.blockIndex = f.blockIndex g.id = f.id out.append(g) # de-duplicate by index ranges uniq = {} for f in out: k = (f.BlockIndex, f.IMIN, f.JMIN, f.KMIN, f.IMAX, f.JMAX, f.KMAX) uniq[k] = f return list(uniq.values()) lower_connected_faces = _rescale_faces(lower_connected_faces) upper_connected_faces = _rescale_faces(upper_connected_faces) lower_connected_faces_export = [f.to_dict() for f in lower_connected_faces] upper_connected_faces_export = [f.to_dict() for f in upper_connected_faces] return (lower_connected_faces_export, upper_connected_faces_export, lower_connected_faces, upper_connected_faces)
[docs] def find_closest_block(blocks:List[Block],x:np.ndarray,y:np.ndarray,z:np.ndarray,centroid:np.ndarray,translational_direction:str="x",minvalue:bool=True): """Find the closest block to an extreme in the x,y, or z direction and returns the targeting point. Target point is the reference point where we want the closest block and the closest face Args: x (np.ndarray): x coordinate of all the blocks' centroid y (np.ndarray): y coordinate of all the blocks' centroid z (np.ndarray): z coordinate of all the blocks' centroid centroid (np.ndarray): centroid (cx,cy,cz) translational_direction (str, optional): _description_. Defaults to "x". minvalue (bool, optional): _description_. Defaults to True. Returns: (tuple): containing *selected block index* (int): index of closest block *target_x* (float): this is the x value where selected block is closest to *target_y* (float): this is the y value where selected block is closest to *target_z* (float): this is the z value where selected block is closest to """ cx = centroid[0]; cy = centroid[1]; cz = centroid[2] target_x = cx; target_y = cy; target_z = cz if translational_direction=="x": xmins = [b.X.min() for b in blocks] xmaxes = [b.X.max() for b in blocks] xmin = min(xmins) xmax = max(xmaxes) dx = xmax - xmin if minvalue: target_x = xmin - dx*0.5 selected_block_indx = np.argmin(np.sqrt((target_x-x)**2 + (cy-y)**2 + (cz-z)**2)) else: target_x = xmax + dx*0.5 selected_block_indx = np.argmin(np.sqrt((target_x-x)**2 + (cy-y)**2 + (cz-z)**2)) target_y= cy target_z = cz elif translational_direction=="y": ymins = [b.Y.min() for b in blocks] ymaxes = [b.Y.max() for b in blocks] ymin = min(ymins) ymax = max(ymaxes) dy = ymax-ymin if minvalue: target_y = ymin - dy*0.5 selected_block_indx = np.argmin(np.sqrt((cx-x)**2 + (target_y-y)**2 + (cz-z)**2)) else: target_y = ymax + dy*0.5 selected_block_indx = np.argmin(np.sqrt((cx-x)**2 + (target_y-y)**2 + (cz-z)**2)) target_x = cx target_z = cz else: # translational_direction=="z": zmins = [b.Z.min() for b in blocks] zmaxes = [b.Z.max() for b in blocks] zmin = min(zmins) zmax = max(zmaxes) dz = zmax - zmin if minvalue: target_z = zmin - dz*0.5 selected_block_indx = np.argmin(np.sqrt((cx-x)**2 + (cy-y)**2 + (target_z-z)**2)) else: target_z = zmax + dz*0.5 selected_block_indx = np.argmin(np.sqrt((cx-x)**2 + (cy-y)**2 + (target_z-z)**2)) target_x = cx target_y = cy return selected_block_indx,target_x,target_y,target_z
[docs] def split_face(face_to_split:Face, block:Block,imin:int,jmin:int,kmin:int,imax:int,jmax:int,kmax:int): """Splits a face with another face within the same block picture the split as a two rectangles inside each other Args: face_to_split (Face): Face on the block to be split block (Block): Block the split is occuring on imin (int): IMIN index of the split (diagonals) jmin (int): JMIN index of the split (diagonals) kmin (int): KMIN index of the split (diagonals) imax (int): IMAX index of the split jmax (int): JMAX index of the split kmax (int): KMAX index of the split :: left face top face right face ________ __ __ __ | __ | | | |__| | | __ | |__| | | | __ | | |__| face_to_split/center face |________| |__| |__| |__| bottom face Returns: [List[Faces]]: List of unique faces from the split """ center_face = create_face_from_diagonals(block, imin=imin,imax=imax, jmin=jmin,jmax=jmax, kmin=kmin,kmax=kmax) if kmin == kmax: # In the picture above Horizontal = i, vertical = j left_face = create_face_from_diagonals(block, imin=face_to_split.IMIN,imax=imin, jmin=face_to_split.JMIN,jmax=face_to_split.JMAX, kmin=kmin, kmax=kmax) right_face = create_face_from_diagonals(block, imin=imax, imax=face_to_split.IMAX, jmin=face_to_split.JMIN, jmax=face_to_split.JMAX, kmin=kmin, kmax=kmax) top_face = create_face_from_diagonals(block, imin=imin,imax=imax, jmin=jmax,jmax=face_to_split.JMAX, kmin=kmin,kmax=kmax) bottom_face = create_face_from_diagonals(block, imin=imin,imax=imax, jmin=face_to_split.JMIN,jmax=jmin, kmin=kmin,kmax=kmax) elif (imin==imax): # In the picture above Horizontal = j, vertical = k left_face = create_face_from_diagonals(block, imin=imin,imax=imax, jmin=face_to_split.JMIN, jmax=jmin, kmin=face_to_split.KMIN,kmax=face_to_split.KMAX) right_face = create_face_from_diagonals(block, imin=imin,imax=imax, jmin=jmax, jmax=face_to_split.JMAX, kmin=face_to_split.KMIN,kmax=face_to_split.KMAX) top_face = create_face_from_diagonals(block, imin=imin,imax=imax, jmin=jmin,jmax=jmax, kmin=kmax,kmax=face_to_split.KMAX) bottom_face = create_face_from_diagonals(block, imin=imin,imax=imax, jmin=jmin,jmax=jmax, kmin=face_to_split.KMIN,kmax=kmin) elif (jmin==jmax): # In the picture above Horizontal = i, vertical = k left_face = create_face_from_diagonals(block, imin=face_to_split.IMIN,imax=imin, jmin=jmin,jmax=jmax, kmin=face_to_split.KMIN,kmax=face_to_split.KMAX) right_face = create_face_from_diagonals(block, imin=imax,imax=face_to_split.IMAX, jmin=jmin,jmax=jmax, kmin=face_to_split.KMIN,kmax=face_to_split.KMAX) top_face = create_face_from_diagonals(block, imin=imin,imax=imax, jmin=jmin,jmax=jmax, kmin=kmax,kmax=face_to_split.KMAX) bottom_face = create_face_from_diagonals(block, imin=imin, imax=imax, jmin=jmin, jmax=jmax, kmin=face_to_split.KMIN, kmax=kmin) faces = [top_face,bottom_face,left_face,right_face] faces = [f for f in faces if not f.isEdge and not f.index_equals(center_face)] # Remove edges [f.set_block_index(face_to_split.blockIndex) for f in faces] return faces
[docs] def find_face_nearest_point(faces:List[Face], x:float,y:float,z:float): """Find a face nearest to a given point Args: blocks (List[Block]): List of blocks faces (List[Face]): List of faces x (float): x coordinate of a reference point y (float): y coordinate of a reference point z (float): z coordinate of a reference point """ n = list(range(len(faces))) dv = list() for i in n: dx = x-faces[i].cx dy = y-faces[i].cy dz = z-faces[i].cz dv.append(math.sqrt(dx*dx + dy*dy + dz*dz)) face_index = np.argmin(np.array(dv)) return face_index
[docs] def outer_face_dict_to_list(blocks:List[Block],outer_faces:List[Dict[str,int]],gcd:int=1) -> List[Face]: """Converts a list of dictionary face representations to a list of faces. Use this only for outer faces Args: blocks (List[Block]): List of blocks outer_faces (List[Dict[str,int]]): List of outer faces represented as a dictionary gcd (int, optional): Greatst common divisor. Defaults to 1. Returns: List[Face]: List of Face objects """ outer_faces_all = list() for o in outer_faces: face = create_face_from_diagonals(blocks[o['block_index']], int(o['IMIN']/gcd), int(o['JMIN']/gcd), int(o['KMIN']/gcd), int(o['IMAX']/gcd), int(o['JMAX']/gcd), int(o['KMAX']/gcd)) if 'id' in o.keys(): face.id = o['id'] face.set_block_index(o['block_index']) outer_faces_all.append(face) return outer_faces_all
[docs] def match_faces_dict_to_list(blocks:List[Block],matched_faces:List[Dict[str,int]],gcd:int=1): """Converts a list of dictionaries representing matched faces to a list of Faces Args: blocks (List[Block]): List of blocks matched_faces (List[Dict[str,int]]): List of matched faces represented as a dictionary gcd (int, optional): _description_. Defaults to 1. Returns: _type_: _description_ """ matched_faces_all = list() for _,m in enumerate(matched_faces): face1 = create_face_from_diagonals(blocks[m['block1']['block_index']], int(m['block1']['IMIN']/gcd), int(m['block1']['JMIN']/gcd), int(m['block1']['KMIN']/gcd), int(m['block1']['IMAX']/gcd), int(m['block1']['JMAX']/gcd), int(m['block1']['KMAX']/gcd)) face2 = create_face_from_diagonals(blocks[m['block2']['block_index']], int(m['block2']['IMIN']/gcd), int(m['block2']['JMIN']/gcd), int(m['block2']['KMIN']/gcd), int(m['block2']['IMAX']/gcd), int(m['block2']['JMAX']/gcd), int(m['block2']['KMAX']/gcd)) face1.set_block_index(m['block1']['block_index']) if 'id' in m['block1'].keys(): face1.id = m['block1']['id'] face2.set_block_index(m['block2']['block_index']) if 'id' in m['block2'].keys(): face2.id = m['block2']['id'] matched_faces_all.append(face1) matched_faces_all.append(face2) return matched_faces_all