plot3d 1.6.8__tar.gz → 1.7.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,15 +1,15 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: plot3d
3
- Version: 1.6.8
3
+ Version: 1.7.0
4
4
  Summary: Plot3D python utilities for reading and writing and also finding connectivity between blocks
5
5
  Author: Paht Juangphanich
6
6
  Author-email: paht.juangphanich@nasa.gov
7
- Requires-Python: >=3.10.1,<4.0.0
7
+ Requires-Python: >=3.10.12,<4.0.0
8
8
  Classifier: Programming Language :: Python :: 3
9
9
  Classifier: Programming Language :: Python :: 3.11
10
10
  Classifier: Programming Language :: Python :: 3.12
11
11
  Requires-Dist: networkx
12
- Requires-Dist: numpy (==2.*)
12
+ Requires-Dist: numpy
13
13
  Requires-Dist: pandas
14
14
  Requires-Dist: scipy
15
15
  Requires-Dist: tqdm
@@ -1,7 +1,10 @@
1
1
  from __future__ import absolute_import
2
+ from importlib import import_module
3
+ import os, warnings
2
4
 
3
- from .block import Block, reduce_blocks
4
- from .blockfunctions import rotate_block,get_outer_bounds,block_connection_matrix
5
+ from .block import Block
6
+ from .blockfunctions import rotate_block, get_outer_bounds, block_connection_matrix,split_blocks, plot_blocks, reduce_blocks, find_matching_faces
7
+ from .block_merging_mixed_facepairs import combine_nxnxn_cubes_mixed_pairs
5
8
  from .connectivity import find_matching_blocks, get_face_intersection, connectivity_fast, face_matches_to_dict
6
9
  from .face import Face
7
10
  from .facefunctions import create_face_from_diagonals, get_outer_faces, find_connected_faces, find_bounding_faces,split_face,find_face_nearest_point,match_faces_dict_to_list,outer_face_dict_to_list,find_closest_block
@@ -14,8 +17,9 @@ from .split_block import split_blocks, Direction
14
17
  from .listfunctions import unique_pairs
15
18
 
16
19
  # Try importing metis
17
- try:
18
- import metis
19
- from .graph import block_to_graph,get_face_vertex_indices,get_starting_vertex,add_connectivity_to_graph, block_connectivity_to_graph
20
- except ImportError as ex:
21
- print("Could not import metis. metis may not be configured \n {ex}")
20
+ if os.getenv('METIS_DLL') is not None:
21
+ if import_module('metis') is not None:
22
+ import metis
23
+ from .graph import block_to_graph,get_face_vertex_indices,get_starting_vertex,add_connectivity_to_graph, block_connectivity_to_graph
24
+ else:
25
+ print("METIS_DLL is not set. metis may not be configured. plot3D will function without metis")
@@ -2,17 +2,25 @@ import numpy as np
2
2
  import math
3
3
  from tqdm import trange
4
4
  from typing import List
5
+ from copy import deepcopy
6
+ import numpy.typing as npt
5
7
 
6
8
  class Block:
7
9
  """Plot3D Block definition
8
10
  """
9
- def __init__(self, X:np.ndarray,Y:np.ndarray,Z:np.ndarray):
11
+ X: npt.NDArray
12
+ Y: npt.NDArray
13
+ Z: npt.NDArray
14
+ IMAX:int
15
+ JMAX:int
16
+ KMAX:int
17
+ def __init__(self, X:npt.NDArray,Y:npt.NDArray,Z:npt.NDArray):
10
18
  """Initializes the block using all the X,Y,Z coordinates of the block
11
19
 
12
20
  Args:
13
- X (np.ndarray): All the X coordinates (i,j,k)
14
- Y (np.ndarray): All the Y coordinates (i,j,k)
15
- Z (np.ndarray): All the Z coordinates (i,j,k)
21
+ X (npt.NDArray): All the X coordinates (i,j,k)
22
+ Y (npt.NDArray): All the Y coordinates (i,j,k)
23
+ Z (npt.NDArray): All the Z coordinates (i,j,k)
16
24
 
17
25
  """
18
26
  self.IMAX,self.JMAX,self.KMAX = X.shape;
@@ -24,6 +32,8 @@ class Block:
24
32
  self.cy = np.mean(Y)
25
33
  self.cz = np.mean(Z)
26
34
 
35
+ def __repr__(self):
36
+ return f"({self.IMAX},{self.JMAX},{self.KMAX})"
27
37
 
28
38
  def scale(self,factor:float):
29
39
  """Scales a mesh by a certain factor
@@ -164,6 +174,20 @@ class Block:
164
174
  v[i,j,k]= vol12/12
165
175
  return v
166
176
 
177
+ def get_faces(self):
178
+ """
179
+ Returns a dictionary of the six faces of the block.
180
+ Each face is a tuple of (X_face, Y_face, Z_face).
181
+ """
182
+ return {
183
+ 'imin': (self.X[0,:,:], self.Y[0,:,:], self.Z[0,:,:]),
184
+ 'imax': (self.X[-1,:,:], self.Y[-1,:,:], self.Z[-1,:,:]),
185
+ 'jmin': (self.X[:,0,:], self.Y[:,0,:], self.Z[:,0,:]),
186
+ 'jmax': (self.X[:,-1,:], self.Y[:,-1,:], self.Z[:,-1,:]),
187
+ 'kmin': (self.X[:,:,0], self.Y[:,:,0], self.Z[:,:,0]),
188
+ 'kmax': (self.X[:,:,-1], self.Y[:,:,-1], self.Z[:,:,-1]),
189
+ }
190
+
167
191
  @property
168
192
  def size(self)->int:
169
193
  """returns the total number of nodes
@@ -173,77 +197,6 @@ class Block:
173
197
  """
174
198
  return self.IMAX*self.JMAX*self.KMAX
175
199
 
176
- def checkCollinearity(v1:np.ndarray, v2:np.ndarray):
177
- # Calculate their cross product
178
- cross_P = np.cross(v1,v2)
179
-
180
- # Check if their cross product
181
- # is a NULL Vector or not
182
- if (cross_P[0] == 0 and
183
- cross_P[1] == 0 and
184
- cross_P[2] == 0):
185
- return True
186
- else:
187
- return False
188
-
189
- def calculate_outward_normals(block:Block):
190
- # Calculate Normals
191
- X = block.X
192
- Y = block.Y
193
- Z = block.Z
194
- imax = block.IMAX
195
- jmax = block.JMAX
196
- kmax = block.KMAX
197
- # IMAX - Normal should be out of the page
198
- # Normals I direction: IMIN https://www.khronos.org/opengl/wiki/Calculating_a_Surface_Normal
199
- x = [X[0,0,0],X[0,jmax,0],X[0,0,kmax]]
200
- y = [Y[0,0,0],Y[0,jmax,0],Y[0,0,kmax]]
201
- z = [Z[0,0,0],Z[0,jmax,0],Z[0,0,kmax]]
202
- u = np.array([x[1]-x[0],y[1]-y[0],z[1]-z[0]])
203
- v = np.array([x[2]-x[0],y[2]-y[0],z[2]-z[0]])
204
- n_imin = np.cross(v1,v2)
205
-
206
- # Normals I direction: IMAX
207
- x = [X[imax,0,0],X[imax,jmax,0],X[imax,0,kmax]]
208
- y = [Y[imax,0,0],Y[imax,jmax,0],Y[imax,0,kmax]]
209
- z = [Z[imax,0,0],Z[imax,jmax,0],Z[imax,0,kmax]]
210
- v1 = np.array([x[1]-x[0],y[1]-y[0],z[1]-z[0]])
211
- v2 = np.array([x[2]-x[0],y[2]-y[0],z[2]-z[0]])
212
- n_imax = np.cross(v1,v2)
213
-
214
- # Normals J direction: JMIN
215
- x = [X[0,0,0],X[imax,0,0],X[0,0,kmax]]
216
- y = [Y[0,0,0],Y[imax,0,0],Y[0,0,kmax]]
217
- z = [Z[0,0,0],Z[imax,0,0],Z[0,0,kmax]]
218
- v1 = np.array([x[1]-x[0],y[1]-y[0],z[1]-z[0]])
219
- v2 = np.array([x[2]-x[0],y[2]-y[0],z[2]-z[0]])
220
- n_jmin = np.cross(v1,v2)
221
-
222
- # Normals J direction: JMAX
223
- x = [X[0,jmax,0],X[imax,jmax,0],X[0,jmax,kmax]]
224
- y = [Y[0,jmax,0],Y[imax,jmax,0],Y[0,jmax,kmax]]
225
- z = [Z[0,jmax,0],Z[imax,jmax,0],Z[0,jmax,kmax]]
226
- v1 = np.array([x[1]-x[0],y[1]-y[0],z[1]-z[0]])
227
- v2 = np.array([x[2]-x[0],y[2]-y[0],z[2]-z[0]])
228
- n_jmax = np.cross(v1,v2)
229
-
230
- # Normals K direction: KMIN
231
- x = [X[imax,0,0],X[0,jmax,0],X[0,0,0]]
232
- y = [Y[imax,0,0],Y[0,jmax,0],Y[0,0,0]]
233
- z = [Z[imax,0,0],Z[0,jmax,0],Z[0,0,0]]
234
- v1 = np.array([x[1]-x[0],y[1]-y[0],z[1]-z[0]])
235
- v2 = np.array([x[2]-x[0],y[2]-y[0],z[2]-z[0]])
236
- n_kmin = np.cross(v1,v2)
237
-
238
- # Normals K direction: KMAX
239
- x = [X[imax,0,kmax],X[0,jmax,kmax],X[0,0,kmax]]
240
- y = [Y[imax,0,kmax],Y[0,jmax,kmax],Y[0,0,kmax]]
241
- z = [Z[imax,0,kmax],Z[0,jmax,kmax],Z[0,0,kmax]]
242
- v1 = np.array([x[1]-x[0],y[1]-y[0],z[1]-z[0]])
243
- v2 = np.array([x[2]-x[0],y[2]-y[0],z[2]-z[0]])
244
- n_kmax = np.cross(v1,v2)
245
-
246
- return n_imin,n_jmin,n_kmin,n_imax,n_jmax,n_kmax
247
200
 
248
201
  def reduce_blocks(blocks:List[Block],factor:int):
249
202
  """reduce the blocks by a factor of (factor)
@@ -0,0 +1,301 @@
1
+ from typing import Dict, List, Set, Tuple
2
+ import numpy as np
3
+ from .block import Block
4
+ from .blockfunctions import find_matching_faces, build_connectivity_graph, standardize_block_orientation
5
+ from .write import write_plot3D
6
+
7
+
8
+ def rotate_block_to_align_faces(X, Y, Z, face_from: str, face_to: str):
9
+ """
10
+ Rotate block2 geometry so that face_from aligns with face_to.
11
+ Currently supports: jmax → imin.
12
+ """
13
+ if (face_from, face_to) == ('jmax', 'imin'):
14
+ Xr = np.transpose(X, (1, 0, 2))
15
+ Yr = np.transpose(Y, (1, 0, 2))
16
+ Zr = np.transpose(Z, (1, 0, 2))
17
+ Xr = np.flip(Xr, axis=0)
18
+ Yr = np.flip(Yr, axis=0)
19
+ Zr = np.flip(Zr, axis=0)
20
+ return Xr, Yr, Zr
21
+ raise NotImplementedError(f"Rotation from {face_from} to {face_to} not implemented.")
22
+
23
+ def combine_2_blocks_mixed_pairing(block1, block2, tol=1e-8):
24
+ """
25
+ Combine block1 and block2 by matching and aligning any pair of faces, including cross-axis combinations.
26
+ Automatically transposes and flips block2, and flips block1 if necessary to maintain monotonic physical direction.
27
+
28
+ This version generalizes the direction check to detect which of X, Y, or Z is varying most along the stacking axis,
29
+ and uses that to determine whether block1 needs to be flipped before concatenation.
30
+
31
+ Returns:
32
+ Block: The merged block with physically consistent orientation.
33
+ """
34
+ face1, face2, flip_flags = find_matching_faces(block1, block2, tol=tol)
35
+ if face1 is None or flip_flags is None:
36
+ print("No matching faces or incompatible orientation.")
37
+ return block1
38
+
39
+ flip_ud, flip_lr = flip_flags
40
+
41
+ face_axis_normal = {
42
+ 'imin': (0, -1), 'imax': (0, 1),
43
+ 'jmin': (1, -1), 'jmax': (1, 1),
44
+ 'kmin': (2, -1), 'kmax': (2, 1),
45
+ }
46
+
47
+ axis1, dir1 = face_axis_normal[face1]
48
+ axis2, dir2 = face_axis_normal[face2] # type: ignore
49
+
50
+ # Transpose block2 to align its face axis with block1's
51
+ transpose = None
52
+ if axis1 != axis2:
53
+ if (axis1, axis2) in [(0, 1), (1, 0)]:
54
+ transpose = (1, 0, 2)
55
+ elif (axis1, axis2) in [(0, 2), (2, 0)]:
56
+ transpose = (2, 1, 0)
57
+ elif (axis1, axis2) in [(1, 2), (2, 1)]:
58
+ transpose = (0, 2, 1)
59
+
60
+ X2, Y2, Z2 = block2.X.copy(), block2.Y.copy(), block2.Z.copy()
61
+ if transpose:
62
+ X2, Y2, Z2 = np.transpose(X2, transpose), np.transpose(Y2, transpose), np.transpose(Z2, transpose)
63
+
64
+ # Apply local flips from face alignment
65
+ if face2 in ['imin', 'imax']:
66
+ if flip_ud: X2, Y2, Z2 = np.flip(X2, 1), np.flip(Y2, 1), np.flip(Z2, 1)
67
+ if flip_lr: X2, Y2, Z2 = np.flip(X2, 2), np.flip(Y2, 2), np.flip(Z2, 2)
68
+ elif face2 in ['jmin', 'jmax']:
69
+ if flip_ud: X2, Y2, Z2 = np.flip(X2, 0), np.flip(Y2, 0), np.flip(Z2, 0)
70
+ if flip_lr: X2, Y2, Z2 = np.flip(X2, 2), np.flip(Y2, 2), np.flip(Z2, 2)
71
+ elif face2 in ['kmin', 'kmax']:
72
+ if flip_ud: X2, Y2, Z2 = np.flip(X2, 0), np.flip(Y2, 0), np.flip(Z2, 0)
73
+ if flip_lr: X2, Y2, Z2 = np.flip(X2, 1), np.flip(Y2, 1), np.flip(Z2, 1)
74
+
75
+ # Determine stacking axis
76
+ stack_axis = axis1
77
+
78
+ # Identify which physical axis (X/Y/Z) changes most along the stacking axis
79
+ def get_dominant_step(A, axis):
80
+ n = A.shape[axis]
81
+ center = [s // 2 for s in A.shape]
82
+ center[axis] = slice(None)
83
+ values = A[tuple(center)]
84
+ return np.nanmean(values[-1] - values[0])
85
+
86
+ # Step values in block1
87
+ stepX1 = get_dominant_step(block1.X, stack_axis)
88
+ stepY1 = get_dominant_step(block1.Y, stack_axis)
89
+ stepZ1 = get_dominant_step(block1.Z, stack_axis)
90
+ dominant_axis = np.argmax(np.abs([stepX1, stepY1, stepZ1]))
91
+ step1 = [stepX1, stepY1, stepZ1][dominant_axis]
92
+
93
+ # Step values in block2 (transformed and flipped)
94
+ stepX2 = get_dominant_step(X2, stack_axis)
95
+ stepY2 = get_dominant_step(Y2, stack_axis)
96
+ stepZ2 = get_dominant_step(Z2, stack_axis)
97
+ step2 = [stepX2, stepY2, stepZ2][dominant_axis]
98
+
99
+ # Flip block1 if physical step directions are inconsistent
100
+ if np.sign(step1) != np.sign(step2):
101
+ block1.X = np.flip(block1.X, axis=stack_axis)
102
+ block1.Y = np.flip(block1.Y, axis=stack_axis)
103
+ block1.Z = np.flip(block1.Z, axis=stack_axis)
104
+
105
+ # Slice off overlapping face from block2
106
+ slicer = [slice(None)] * 3
107
+ slicer[stack_axis] = slice(1, None) if face2.endswith('min') else slice(0, -1) # type: ignore
108
+ X2s, Y2s, Z2s = X2[tuple(slicer)], Y2[tuple(slicer)], Z2[tuple(slicer)]
109
+
110
+ # Concatenate along stack axis
111
+ if face2.endswith('min'): # type: ignore
112
+ X = np.concatenate([block1.X, X2s], axis=stack_axis)
113
+ Y = np.concatenate([block1.Y, Y2s], axis=stack_axis)
114
+ Z = np.concatenate([block1.Z, Z2s], axis=stack_axis)
115
+ else:
116
+ X = np.concatenate([X2s, block1.X], axis=stack_axis)
117
+ Y = np.concatenate([Y2s, block1.Y], axis=stack_axis)
118
+ Z = np.concatenate([Z2s, block1.Z], axis=stack_axis)
119
+ return standardize_block_orientation(Block(X, Y, Z))
120
+
121
+
122
+
123
+ def combine_blocks_mixed_pairs(blocks: List[Block], tol: float = 1e-8, max_tries: int = 4) -> Tuple[List[Block], List[int]]:
124
+ """
125
+ Combine as many blocks as possible from a group of up to 8 blocks via face matching.
126
+
127
+ Parameters
128
+ ----------
129
+ blocks : List[Block]
130
+ List of up to 8 Block objects.
131
+ tol : float
132
+ Tolerance for face matching.
133
+ max_tries : int
134
+ Number of passes to try merging the blocks further.
135
+
136
+ Returns
137
+ -------
138
+ merged_blocks : List[Block]
139
+ List of successfully merged Block objects (1 or more).
140
+ used_indices : List[int]
141
+ Indices of blocks that were used in any successful merge.
142
+ """
143
+ from itertools import combinations
144
+
145
+ remaining = list(enumerate(blocks)) # [(index, Block)]
146
+ used_indices = set()
147
+
148
+ # Initial merged_blocks are just the input blocks
149
+ merged_blocks = [blk for _, blk in remaining]
150
+ index_lookup = {id(blk): idx for idx, blk in remaining}
151
+
152
+ tries = 0
153
+ while len(merged_blocks) > 1 and tries < max_tries:
154
+ new_merged = []
155
+ merged_flags = [False] * len(merged_blocks)
156
+ used_this_pass = set()
157
+ skip = set()
158
+
159
+ i = 0
160
+ while i < len(merged_blocks):
161
+ if i in skip:
162
+ i += 1
163
+ continue
164
+
165
+ blk_a = merged_blocks[i]
166
+ merged = None
167
+ found = False
168
+
169
+ for j in range(i + 1, len(merged_blocks)):
170
+ if j in skip:
171
+ continue
172
+ blk_b = merged_blocks[j]
173
+ face1, face2,_ = find_matching_faces(blk_a, blk_b, tol=tol)
174
+ if face1 is not None:
175
+ try:
176
+ merged = combine_2_blocks_mixed_pairing(blk_a, blk_b, tol=tol)
177
+ found = True
178
+ break
179
+ except Exception as e:
180
+ print(f"⚠️ Failed to merge blocks {i} and {j}: {e}")
181
+
182
+ if found:
183
+ new_merged.append(merged)
184
+ skip.update([i, j]) # type: ignore
185
+ else:
186
+ new_merged.append(blk_a)
187
+ skip.add(i)
188
+
189
+ i += 1
190
+
191
+ # Add any unmerged blocks at the end
192
+ for k in range(len(merged_blocks)):
193
+ if k not in skip:
194
+ new_merged.append(merged_blocks[k])
195
+
196
+ merged_blocks = new_merged
197
+ tries += 1
198
+
199
+ # Recover used indices (conservatively: all blocks involved in merging)
200
+ used_indices = list(range(len(blocks))) # all blocks are assumed used here
201
+
202
+ return merged_blocks, used_indices
203
+
204
+ def combine_nxnxn_cubes_mixed_pairs(
205
+ blocks: List[Block],
206
+ connectivities: List[List[Dict]],
207
+ cube_size: int = 2,
208
+ tol: float = 1e-8
209
+ ) -> List[Tuple[Block, Set[int]]]:
210
+ """
211
+ Find and combine all non-overlapping nxnxn cube groups of blocks
212
+ using face connectivity data and return the merged components.
213
+
214
+ Parameters
215
+ ----------
216
+ blocks : List[Block]
217
+ All input block objects.
218
+ connectivities : List of face match metadata pairs
219
+ Face connectivity between blocks.
220
+ cube_size : int
221
+ Size of the cube (e.g. 2 for 2x2x2, 4 for 4x4x4).
222
+ tol : float
223
+ Face match tolerance.
224
+
225
+ Returns
226
+ -------
227
+ List[Tuple[Block, Set[int]]]
228
+ A list of merged Block objects and their source block indices.
229
+ """
230
+ from itertools import product
231
+
232
+ used = set()
233
+ merged_groups = []
234
+ G = build_connectivity_graph(connectivities)
235
+
236
+ remaining_indices = list(range(len(blocks)))
237
+
238
+ def find_nxnxn_group(seed_index):
239
+ from collections import deque
240
+ visited = set()
241
+ queue = deque([seed_index])
242
+ group = set()
243
+
244
+ while queue and len(group) < cube_size ** 3:
245
+ idx = queue.popleft()
246
+ if idx in visited or idx in used:
247
+ continue
248
+ visited.add(idx)
249
+ group.add(idx)
250
+ for nbr in G.neighbors(idx):
251
+ if nbr not in visited and nbr not in used:
252
+ queue.append(nbr)
253
+
254
+ return group if len(group) == cube_size ** 3 else None
255
+
256
+ while True:
257
+ before_len = len(remaining_indices)
258
+ merged_this_round = False
259
+ new_used = set()
260
+
261
+ i = 0
262
+ while i < len(remaining_indices):
263
+ seed_index = remaining_indices[i]
264
+ if seed_index in used:
265
+ i += 1
266
+ continue
267
+
268
+ group_indices = find_nxnxn_group(seed_index)
269
+ if not group_indices or group_indices & new_used:
270
+ i += 1
271
+ continue
272
+
273
+ group_block_list = [blocks[k] for k in sorted(group_indices)]
274
+ index_mapping = {i: orig_idx for i, orig_idx in enumerate(sorted(group_indices))}
275
+
276
+ try:
277
+ partial_merges, local_indices = combine_blocks_mixed_pairs(group_block_list, tol=tol)
278
+ for merged_block in partial_merges:
279
+ merged_group = {index_mapping[i] for i in local_indices}
280
+ merged_groups.append((merged_block, merged_group))
281
+ # write_plot3D('block1.xyz',[merged_block])
282
+ new_used.update(merged_group)
283
+ merged_this_round = True
284
+ except Exception as e:
285
+ print(f"⚠️ Skipping group {group_indices} due to error: {e}")
286
+ i += 1
287
+ continue
288
+
289
+ # Update remaining_indices and restart inner loop
290
+ remaining_indices = [idx for idx in remaining_indices if idx not in new_used]
291
+ i = 0
292
+
293
+ used.update(new_used)
294
+
295
+ if not merged_this_round or len(remaining_indices) == before_len:
296
+ print("✅ No further merges possible. Appending unmerged blocks.")
297
+ for idx in remaining_indices:
298
+ merged_groups.append((blocks[idx], {idx}))
299
+ break
300
+
301
+ return merged_groups
@@ -0,0 +1,361 @@
1
+ from copy import deepcopy
2
+ from itertools import combinations, product
3
+ import math
4
+ import numpy as np
5
+ from typing import Dict, List, Optional, Set, Tuple
6
+ from tqdm import trange
7
+ from .facefunctions import create_face_from_diagonals, get_outer_faces, find_matching_faces,faces_match
8
+ from .block import Block, reduce_blocks
9
+ from .write import write_plot3D
10
+ from .face import Face
11
+ import tqdm
12
+ import networkx as nx
13
+ import matplotlib.pyplot as plt
14
+ from mpl_toolkits.mplot3d import Axes3D
15
+ import numpy.typing as npt
16
+
17
+ def rotate_block(block,rotation_matrix:np.ndarray) -> Block:
18
+ """Rotates a block by a rotation matrix
19
+
20
+ Args:
21
+ rotation_matrix (np.ndarray): 3x3 rotation matrix
22
+
23
+ Returns:
24
+ Block: returns a new rotated block
25
+ """
26
+ X = block.X.copy()
27
+ Y = block.Y.copy()
28
+ Z = block.Z.copy()
29
+ points = np.zeros(shape=(3,block.IMAX*block.JMAX*block.KMAX))
30
+ indx = 0
31
+ for i in range(block.IMAX):
32
+ for j in range(block.JMAX):
33
+ for k in range(block.KMAX):
34
+ points[0,indx] = block.X[i,j,k]
35
+ points[1,indx] = block.Y[i,j,k]
36
+ points[2,indx] = block.Z[i,j,k]
37
+ indx+=1
38
+ points_rotated = np.matmul(rotation_matrix,points)
39
+ indx=0
40
+ for i in range(block.IMAX):
41
+ for j in range(block.JMAX):
42
+ for k in range(block.KMAX):
43
+ X[i,j,k] = points_rotated[0,indx]
44
+ Y[i,j,k] = points_rotated[1,indx]
45
+ Z[i,j,k] = points_rotated[2,indx]
46
+ indx+=1
47
+
48
+ return Block(X,Y,Z)
49
+
50
+ def get_outer_bounds(blocks:List[Block]):
51
+ """Get outer bounds for a set of blocks
52
+
53
+ Args:
54
+ blocks (List[Block]): Blocks defining your shape
55
+
56
+ Returns:
57
+ (Tuple) containing:
58
+
59
+ **xbounds** (Tuple[float,float]): xmin,xmax
60
+ **ybounds** (Tuple[float,float]): ymin,ymax
61
+ **zbounds** (Tuple[float,float]): zmin,zmax
62
+ """
63
+ xbounds = [blocks[0].X.min(),blocks[0].X.max()]
64
+ ybounds = [blocks[0].Y.min(),blocks[0].Y.max()]
65
+ zbounds = [blocks[0].Z.min(),blocks[0].Z.max()]
66
+
67
+ for i in range(1,len(blocks)):
68
+ xmin = blocks[i].X.min()
69
+ xmax = blocks[i].X.max()
70
+
71
+ ymin = blocks[i].Y.min()
72
+ ymax = blocks[i].Y.max()
73
+
74
+ zmin = blocks[i].Z.min()
75
+ zmax = blocks[i].Z.max()
76
+
77
+ if xmin<xbounds[0]:
78
+ xbounds[0] = xmin
79
+ elif xmax>xbounds[1]:
80
+ xbounds[1] = xmax
81
+
82
+ if ymin<ybounds[0]:
83
+ ybounds[0] = ymin
84
+ elif ymax>ybounds[1]:
85
+ ybounds[1] = ymax
86
+
87
+ if zmin<zbounds[0]:
88
+ zbounds[0] = zmin
89
+ elif zmax>zbounds[1]:
90
+ zbounds[1] = zmax
91
+
92
+ return tuple(xbounds),tuple(ybounds),tuple(zbounds)
93
+
94
+ def block_connection_matrix(blocks:List[Block],outer_faces:List[Dict[str,int]]=[],tol:float=1E-8):
95
+ """Creates a matrix representing how block edges are connected to each other
96
+
97
+ Args:
98
+ blocks (List[Block]): List of blocks that describe the Plot3D mesh
99
+ outer_faces (List[Dict[str,int]], optional): List of outer faces remaining from connectivity. Useful if you are interested in finding faces that are exterior to the block. Also useful if you combine outerfaces with match faces, this will help identify connections by looking at split faces. Defaults to [].
100
+ tol (float, optional): Matching tolerance to look for when comparing face centroids.
101
+
102
+ Returns:
103
+ (Tuple): containing
104
+
105
+ *connectivity* (np.ndarray): integer matrix defining how the blocks are connected to each other
106
+ *connectivity_i* (np.ndarray): integer matrix defining connectivity of all blocks where IMAX=IMIN
107
+ *connectivity_j* (np.ndarray): integer matrix defining connectivity of all blocks where JMAX=JMIN
108
+ *connectivity_k* (np.ndarray): integer matrix defining connectivity of all blocks where KMAX=KMIN
109
+
110
+ """
111
+ # Reduce the size of the blocks by the GCD
112
+ gcd_array = list()
113
+ for block_indx in range(len(blocks)):
114
+ block = blocks[block_indx]
115
+ gcd_array.append(math.gcd(block.IMAX-1, math.gcd(block.JMAX-1, block.KMAX-1)))
116
+ gcd_to_use = min(gcd_array) # You need to use the minimum gcd otherwise 1 block may not exactly match the next block. They all have to be scaled the same way.
117
+ blocks = reduce_blocks(deepcopy(blocks),gcd_to_use)
118
+
119
+ # Face to List
120
+ outer_faces_all = list()
121
+ for o in outer_faces:
122
+ face = create_face_from_diagonals(blocks[o['block_index']], int(o['IMIN']/gcd_to_use), int(o['JMIN']/gcd_to_use),
123
+ int(o['KMIN']/gcd_to_use), int(o['IMAX']/gcd_to_use), int(o['JMAX']/gcd_to_use), int(o['KMAX']/gcd_to_use))
124
+ face.set_block_index(o['block_index'])
125
+ if "id" in o:
126
+ face.id = o['id']
127
+ outer_faces_all.append(face)
128
+
129
+ outer_faces = outer_faces_all
130
+
131
+ n = len(blocks)
132
+ connectivity = np.eye(n,dtype=np.int8)
133
+ combos = list(combinations(range(n),2))
134
+ for indx in (pbar:=trange(len(combos))):
135
+ i,j = combos[indx]
136
+ pbar.set_description(f"Building block to block connectivity matrix: checking {i}")
137
+ b1 = blocks[i]
138
+
139
+ if len(outer_faces)==0: # Get the outerfaces to search
140
+ b1_outer_faces,_ = get_outer_faces(b1)
141
+ else:
142
+ b1_outer_faces = [o for o in outer_faces if o.BlockIndex == i] # type: ignore
143
+
144
+ if i != j and connectivity[i,j]!=-1:
145
+ b2 = blocks[j]
146
+
147
+ if len(outer_faces)==0: # Get the outerfaces to search
148
+ b2_outer_faces,_ = get_outer_faces(b2)
149
+ else:
150
+ b2_outer_faces = [o for o in outer_faces if o.BlockIndex == j] # type: ignore
151
+
152
+ # Check to see if any of the outer faces of the blocks match
153
+ connection_found=False
154
+ for f1 in b1_outer_faces:
155
+ for f2 in b2_outer_faces:
156
+ if (f1.is_connected(f2,tol)): # type: ignore # Check if face centroid is the same
157
+ connectivity[i,j] = 1 # Default block to block connection matrix
158
+ connectivity[j,i] = 1
159
+ connection_found=True
160
+ # c = np.sum(connectivity[i,:]==1)
161
+ # print(f"block {i} connections {c}")
162
+ break
163
+ if connection_found:
164
+ break
165
+ if not connection_found:
166
+ connectivity[i,j] = -1
167
+ connectivity[j,i] = -1
168
+ return connectivity
169
+
170
+ def plot_blocks(blocks):
171
+ gcd_array = list()
172
+ for block_indx in range(len(blocks)):
173
+ block = blocks[block_indx]
174
+ gcd_array.append(math.gcd(block.IMAX-1, math.gcd(block.JMAX-1, block.KMAX-1)))
175
+ gcd_to_use = min(gcd_array) # You need to use the minimum gcd otherwise 1 block may not exactly match the next block. They all have to be scaled the same way.
176
+ blocks = reduce_blocks(deepcopy(blocks),4)
177
+
178
+ fig = plt.figure(figsize=(10, 8))
179
+ ax = fig.add_subplot(111, projection='3d')
180
+ markers = ['o', 's'] # alternate between circle and square
181
+
182
+ for i, b in enumerate(blocks):
183
+ color = f"C{i % 10}"
184
+ X, Y, Z = b.X, b.Y, b.Z
185
+ ax.scatter(X.ravel(), Y.ravel(), Z.ravel(), s=20, alpha=0.4, # type: ignore
186
+ marker=markers[i % len(markers)], label=f'Block {i}', color=color)
187
+
188
+ # Draw lines along i-direction (axis 0)
189
+ for j in range(X.shape[1]):
190
+ for k in range(X.shape[2]):
191
+ ax.plot(X[:, j, k], Y[:, j, k], Z[:, j, k], color=color, linewidth=0.8, alpha=0.6)
192
+
193
+ # Draw lines along j-direction (axis 1)
194
+ for i_ in range(X.shape[0]):
195
+ for k in range(X.shape[2]):
196
+ ax.plot(X[i_, :, k], Y[i_, :, k], Z[i_, :, k], color=color, linewidth=0.8, alpha=0.6)
197
+
198
+ # Draw lines along k-direction (axis 2)
199
+ for i_ in range(X.shape[0]):
200
+ for j_ in range(X.shape[1]):
201
+ ax.plot(X[i_, j_, :], Y[i_, j_, :], Z[i_, j_, :], color=color, linewidth=0.8, alpha=0.6)
202
+
203
+ ax.set_xlabel('X')
204
+ ax.set_ylabel('Y')
205
+ ax.set_zlabel('Z') # type: ignore
206
+ ax.set_title('3D Block Grid with Connected Lines')
207
+ ax.legend()
208
+ plt.tight_layout()
209
+ plt.show()
210
+
211
+ def standardize_block_orientation(block:Block):
212
+ """Standardizes the orientation of a block so that its physical coordinates increase
213
+ consistently along each of the indexing axes:
214
+
215
+ - X increases along the i-axis
216
+ - Y increases along the j-axis
217
+ - Z increases along the k-axis
218
+
219
+ This ensures consistent face orientation and alignment across multiple blocks,
220
+ especially useful when merging, visualizing, or exporting grids. The function
221
+ checks the dominant physical component (X, Y, or Z) along each axis, and flips
222
+ the block along that axis if the component decreases.
223
+
224
+ Parameters:
225
+ block (Block): The input block to be standardized.
226
+
227
+ Returns:
228
+ Block: A new block instance with all three axes oriented consistently.
229
+ """
230
+
231
+ X, Y, Z = block.X.copy(), block.Y.copy(), block.Z.copy()
232
+ i_center = X.shape[0] // 2
233
+ j_center = X.shape[1] // 2
234
+ k_center = X.shape[2] // 2
235
+
236
+ # Check i-direction
237
+ dx_i = X[-1, j_center, k_center] - X[0, j_center, k_center]
238
+ if dx_i < 0:
239
+ X = np.flip(X, axis=0)
240
+ Y = np.flip(Y, axis=0)
241
+ Z = np.flip(Z, axis=0)
242
+
243
+ # Check j-direction
244
+ dy_j = Y[i_center, -1, k_center] - Y[i_center, 0, k_center]
245
+ if dy_j < 0:
246
+ X = np.flip(X, axis=1)
247
+ Y = np.flip(Y, axis=1)
248
+ Z = np.flip(Z, axis=1)
249
+
250
+ # Check k-direction
251
+ dz_k = Z[i_center, j_center, -1] - Z[i_center, j_center, 0]
252
+ if dz_k < 0:
253
+ X = np.flip(X, axis=2)
254
+ Y = np.flip(Y, axis=2)
255
+ Z = np.flip(Z, axis=2)
256
+
257
+ return Block(X, Y, Z)
258
+
259
+ def checkCollinearity(v1:npt.NDArray, v2:npt.NDArray):
260
+ # Calculate their cross product
261
+ cross_P = np.cross(v1,v2)
262
+
263
+ # Check if their cross product
264
+ # is a NULL Vector or not
265
+ if (cross_P[0] == 0 and
266
+ cross_P[1] == 0 and
267
+ cross_P[2] == 0):
268
+ return True
269
+ else:
270
+ return False
271
+
272
+ def calculate_outward_normals(block:Block):
273
+ # Calculate Normals
274
+ X = block.X
275
+ Y = block.Y
276
+ Z = block.Z
277
+ imax = block.IMAX
278
+ jmax = block.JMAX
279
+ kmax = block.KMAX
280
+ # IMAX - Normal should be out of the page
281
+ # Normals I direction: IMIN https://www.khronos.org/opengl/wiki/Calculating_a_Surface_Normal
282
+ x = [X[0,0,0],X[0,jmax,0],X[0,0,kmax]]
283
+ y = [Y[0,0,0],Y[0,jmax,0],Y[0,0,kmax]]
284
+ z = [Z[0,0,0],Z[0,jmax,0],Z[0,0,kmax]]
285
+ u = np.array([x[1]-x[0],y[1]-y[0],z[1]-z[0]])
286
+ v = np.array([x[2]-x[0],y[2]-y[0],z[2]-z[0]])
287
+ n_imin = np.cross(v1,v2) # type: ignore
288
+
289
+ # Normals I direction: IMAX
290
+ x = [X[imax,0,0],X[imax,jmax,0],X[imax,0,kmax]]
291
+ y = [Y[imax,0,0],Y[imax,jmax,0],Y[imax,0,kmax]]
292
+ z = [Z[imax,0,0],Z[imax,jmax,0],Z[imax,0,kmax]]
293
+ v1 = np.array([x[1]-x[0],y[1]-y[0],z[1]-z[0]])
294
+ v2 = np.array([x[2]-x[0],y[2]-y[0],z[2]-z[0]])
295
+ n_imax = np.cross(v1,v2)
296
+
297
+ # Normals J direction: JMIN
298
+ x = [X[0,0,0],X[imax,0,0],X[0,0,kmax]]
299
+ y = [Y[0,0,0],Y[imax,0,0],Y[0,0,kmax]]
300
+ z = [Z[0,0,0],Z[imax,0,0],Z[0,0,kmax]]
301
+ v1 = np.array([x[1]-x[0],y[1]-y[0],z[1]-z[0]])
302
+ v2 = np.array([x[2]-x[0],y[2]-y[0],z[2]-z[0]])
303
+ n_jmin = np.cross(v1,v2)
304
+
305
+ # Normals J direction: JMAX
306
+ x = [X[0,jmax,0],X[imax,jmax,0],X[0,jmax,kmax]]
307
+ y = [Y[0,jmax,0],Y[imax,jmax,0],Y[0,jmax,kmax]]
308
+ z = [Z[0,jmax,0],Z[imax,jmax,0],Z[0,jmax,kmax]]
309
+ v1 = np.array([x[1]-x[0],y[1]-y[0],z[1]-z[0]])
310
+ v2 = np.array([x[2]-x[0],y[2]-y[0],z[2]-z[0]])
311
+ n_jmax = np.cross(v1,v2)
312
+
313
+ # Normals K direction: KMIN
314
+ x = [X[imax,0,0],X[0,jmax,0],X[0,0,0]]
315
+ y = [Y[imax,0,0],Y[0,jmax,0],Y[0,0,0]]
316
+ z = [Z[imax,0,0],Z[0,jmax,0],Z[0,0,0]]
317
+ v1 = np.array([x[1]-x[0],y[1]-y[0],z[1]-z[0]])
318
+ v2 = np.array([x[2]-x[0],y[2]-y[0],z[2]-z[0]])
319
+ n_kmin = np.cross(v1,v2)
320
+
321
+ # Normals K direction: KMAX
322
+ x = [X[imax,0,kmax],X[0,jmax,kmax],X[0,0,kmax]]
323
+ y = [Y[imax,0,kmax],Y[0,jmax,kmax],Y[0,0,kmax]]
324
+ z = [Z[imax,0,kmax],Z[0,jmax,kmax],Z[0,0,kmax]]
325
+ v1 = np.array([x[1]-x[0],y[1]-y[0],z[1]-z[0]])
326
+ v2 = np.array([x[2]-x[0],y[2]-y[0],z[2]-z[0]])
327
+ n_kmax = np.cross(v1,v2)
328
+
329
+ return n_imin,n_jmin,n_kmin,n_imax,n_jmax,n_kmax
330
+
331
+ def split_blocks(blocks:List[Block],gcd:int=4):
332
+ """Split blocks but also keep greatest common divisor
333
+
334
+ Args:
335
+ blocks (List[]): _description_
336
+ gcd (int, optional): _description_. Defaults to 4.
337
+ """
338
+ pass
339
+
340
+ def common_neighbor(G: nx.Graph, a: int, b: int, exclude: Set[int]) -> Optional[int]:
341
+ """
342
+ Return a node that is connected to both `a` and `b` and not in `exclude`.
343
+ """
344
+ for n in G.neighbors(a):
345
+ if n in exclude:
346
+ continue
347
+ if G.has_edge(n, b):
348
+ return n
349
+ return None
350
+
351
+ def build_connectivity_graph(connectivities: List[List[Dict]]) -> nx.Graph:
352
+ """
353
+ Build an undirected graph from a list of face-to-face block connectivities.
354
+ Each edge connects two block indices.
355
+ """
356
+ G = nx.Graph()
357
+ for pair in connectivities:
358
+ block1 = pair['block1']['block_index'] # type: ignore
359
+ block2 = pair['block2']['block_index'] # type: ignore
360
+ G.add_edge(block1, block2)
361
+ return G
@@ -58,8 +58,8 @@ def find_matching_blocks(block1:Block,block2:Block,block1_outer:List[Face], bloc
58
58
  if match:
59
59
  break
60
60
  if match:
61
- block1_outer.pop(p)
62
- block2_outer.pop(q)
61
+ block1_outer.pop(p) # type: ignore
62
+ block2_outer.pop(q) # type: ignore
63
63
  block1_outer.extend(block1_split_faces)
64
64
  block2_outer.extend(block2_split_faces)
65
65
  block1_split_faces.clear()
@@ -395,10 +395,10 @@ def connectivity(blocks:List[Block]):
395
395
  outer_faces = list()
396
396
  face_matches = list()
397
397
  matches_to_remove = list()
398
- # df_matches, blocki_outerfaces, blockj_outerfaces = find_matching_blocks(blocks[4],blocks[7],1E-12) # This function finds partial matches between blocks
399
398
  temp = [get_outer_faces(b) for b in blocks]
400
399
  block_outer_faces = [t[0] for t in temp]
401
400
  combos = combinations_of_nearest_blocks(blocks,6) # Find the 6 nearest Blocks and search through all that.
401
+ # df_matches, blocki_outerfaces, blockj_outerfaces = find_matching_blocks(blocks[0],blocks[1],[block_outer_faces[0][0],block_outer_faces[0][1]],[block_outer_faces[1][0],block_outer_faces[1][1]],1E-12) # This function finds partial matches between blocks
402
402
 
403
403
  t = trange(len(combos))
404
404
  for indx in t: # block i
@@ -1,8 +1,21 @@
1
1
  from typing import Dict, List, Tuple
2
2
  import numpy as np
3
+ import numpy.typing as npt
3
4
  import math
4
5
 
5
6
  class Face:
7
+ x:npt.NDArray
8
+ y:npt.NDArray
9
+ z:npt.NDArray
10
+ I:npt.NDArray
11
+ J:npt.NDArray
12
+ K:npt.NDArray
13
+ cx:float = 0
14
+ cy:float = 0
15
+ cz:float = 0
16
+ nvertex:int = 0
17
+ blockIndex:int = 0 # not really needed except in periodicity
18
+ id:int = 0
6
19
  """Defines a Face of a block for example IMIN,JMIN,JMIN to IMAX,JMIN,JMIN
7
20
  """
8
21
  def __init__(self,nvertex:int=4):
@@ -18,12 +31,6 @@ class Face:
18
31
  self.I = np.zeros(4,dtype=np.int64)
19
32
  self.J = np.zeros(4,dtype=np.int64)
20
33
  self.K = np.zeros(4,dtype=np.int64)
21
- self.cx = 0 # centroid
22
- self.cy = 0
23
- self.cz = 0
24
- self.nvertex=0
25
- self.blockIndex = 0 # not really needed except in periodicity
26
- self.id = 0
27
34
 
28
35
  def to_dict(self):
29
36
  """Returns a dictionary representaon of a face
@@ -116,11 +123,6 @@ class Face:
116
123
  indx = list(set([indx_i]))[0] # Get the common one through a union
117
124
  return self.x[indx], self.y[indx], self.z[indx]
118
125
 
119
- def scale(self,gcd:int):
120
- self.I = int(self.I*gcd)
121
- self.J = int(self.J*gcd)
122
- self.K = int(self.K*gcd)
123
-
124
126
  def add_vertex(self, x:float,y:float,z:float, i:int, j:int, k:int):
125
127
  """Add vertex to define a face
126
128
 
@@ -1,10 +1,59 @@
1
- from typing import Dict, List
1
+ from typing import Dict, List, Optional, Tuple
2
2
  from .listfunctions import unique_pairs
3
3
  from .block import Block, reduce_blocks
4
4
  from .face import Face
5
5
  from copy import deepcopy
6
- import math
6
+ import numpy.typing as npt
7
7
  import numpy as np
8
+ import math
9
+
10
+ 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]]]:
11
+ """
12
+ Compare two block faces and return whether they match and the flip required on face2 to match face1.
13
+ Returns (True, (flip_ud, flip_lr)) if matching, otherwise (False, None).
14
+ """
15
+ def get_corners(X, Y, Z):
16
+ return np.array([
17
+ [X[0, 0], Y[0, 0], Z[0, 0]],
18
+ [X[0, -1], Y[0, -1], Z[0, -1]],
19
+ [X[-1, 0], Y[-1, 0], Z[-1, 0]],
20
+ [X[-1, -1], Y[-1, -1], Z[-1, -1]],
21
+ ])
22
+
23
+ X1, Y1, Z1 = face1
24
+ X2, Y2, Z2 = face2
25
+
26
+ if X1.shape != X2.shape:
27
+ return False, None
28
+
29
+ corners1 = get_corners(X1, Y1, Z1)
30
+ for flip_ud in [False, True]:
31
+ for flip_lr in [False, True]:
32
+ X2f, Y2f, Z2f = X2.copy(), Y2.copy(), Z2.copy()
33
+ if flip_ud:
34
+ X2f, Y2f, Z2f = np.flip(X2f, axis=0), np.flip(Y2f, axis=0), np.flip(Z2f, axis=0)
35
+ if flip_lr:
36
+ X2f, Y2f, Z2f = np.flip(X2f, axis=1), np.flip(Y2f, axis=1), np.flip(Z2f, axis=1)
37
+
38
+ corners2 = get_corners(X2f, Y2f, Z2f)
39
+ diffs = np.linalg.norm(corners1 - corners2, axis=1)
40
+ if np.all(diffs <= tol):
41
+ return True, (flip_ud, flip_lr)
42
+
43
+ return False, None
44
+ def find_matching_faces(block1, block2, tol=1e-8):
45
+ """
46
+ Returns a tuple (face1_name, face2_name, flip_flags) if a matching face is found.
47
+ Otherwise returns (None, None, None).
48
+ """
49
+ faces1 = block1.get_faces()
50
+ faces2 = block2.get_faces()
51
+ for face1_name, face1_data in faces1.items():
52
+ for face2_name, face2_data in faces2.items():
53
+ match, flip_flags = faces_match(face1_data, face2_data, tol=tol)
54
+ if match:
55
+ return face1_name, face2_name, flip_flags
56
+ return None, None, None
8
57
 
9
58
 
10
59
  def get_outer_faces(block1:Block):
@@ -169,7 +218,6 @@ def find_connected_faces(face_to_search:Face,outer_faces:List[Face],connectivity
169
218
  all_matching_faces = list(set(all_matching_faces))
170
219
  return all_matching_faces
171
220
 
172
-
173
221
  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):
174
222
  """Find the closest block to an extreme in the x,y, or z direction and returns the targetting point.
175
223
  Target point is the reference point where we want the closest block and the closest face
@@ -1,18 +1,18 @@
1
1
  [tool.poetry]
2
2
  name = "plot3d"
3
- version = "1.6.8"
3
+ version = "1.7.0"
4
4
  description = "Plot3D python utilities for reading and writing and also finding connectivity between blocks"
5
5
  authors = ["Paht Juangphanich <paht.juangphanich@nasa.gov>"]
6
6
 
7
7
  [tool.poetry.dependencies]
8
- python = "^3.10.1"
9
- numpy = "2.*"
8
+ python = "^3.10.12"
9
+ numpy = "*"
10
10
  scipy = "*"
11
11
  pandas = "*"
12
12
  tqdm = "*"
13
13
  networkx = "*"
14
14
 
15
- [tool.poetry.dev-dependencies]
15
+ [tool.poetry.group.deve.dependencies]
16
16
 
17
17
  [build-system]
18
18
  requires = ["poetry>=1.1.2"]
@@ -1,179 +0,0 @@
1
- from copy import deepcopy
2
- from itertools import combinations
3
- import math
4
- import numpy as np
5
- from typing import Dict, List
6
- from tqdm import trange
7
- from .facefunctions import create_face_from_diagonals, get_outer_faces
8
- from .block import Block
9
- from .face import Face
10
-
11
- def rotate_block(block,rotation_matrix:np.ndarray) -> Block:
12
- """Rotates a block by a rotation matrix
13
-
14
- Args:
15
- rotation_matrix (np.ndarray): 3x3 rotation matrix
16
-
17
- Returns:
18
- Block: returns a new rotated block
19
- """
20
- X = block.X.copy()
21
- Y = block.Y.copy()
22
- Z = block.Z.copy()
23
- points = np.zeros(shape=(3,block.IMAX*block.JMAX*block.KMAX))
24
- indx = 0
25
- for i in range(block.IMAX):
26
- for j in range(block.JMAX):
27
- for k in range(block.KMAX):
28
- points[0,indx] = block.X[i,j,k]
29
- points[1,indx] = block.Y[i,j,k]
30
- points[2,indx] = block.Z[i,j,k]
31
- indx+=1
32
- points_rotated = np.matmul(rotation_matrix,points)
33
- indx=0
34
- for i in range(block.IMAX):
35
- for j in range(block.JMAX):
36
- for k in range(block.KMAX):
37
- X[i,j,k] = points_rotated[0,indx]
38
- Y[i,j,k] = points_rotated[1,indx]
39
- Z[i,j,k] = points_rotated[2,indx]
40
- indx+=1
41
-
42
- return Block(X,Y,Z)
43
-
44
- def reduce_blocks(blocks:List[Block],factor:int):
45
- """reduce the blocks by a factor of (factor)
46
-
47
- Args:
48
- blocks (List[Block]): list of blocks to reduce in size
49
- factor (int, optional): Number of indicies to skip . Defaults to 2.
50
-
51
- Returns:
52
- [type]: [description]
53
- """
54
- for i in range(len(blocks)):
55
- blocks[i].X = blocks[i].X[::factor,::factor,::factor]
56
- blocks[i].Y = blocks[i].Y[::factor,::factor,::factor]
57
- blocks[i].Z = blocks[i].Z[::factor,::factor,::factor]
58
- blocks[i].IMAX,blocks[i].JMAX,blocks[i].KMAX = blocks[i].X.shape
59
- return blocks
60
-
61
- def get_outer_bounds(blocks:List[Block]):
62
- """Get outer bounds for a set of blocks
63
-
64
- Args:
65
- blocks (List[Block]): Blocks defining your shape
66
-
67
- Returns:
68
- (Tuple) containing:
69
-
70
- **xbounds** (Tuple[float,float]): xmin,xmax
71
- **ybounds** (Tuple[float,float]): ymin,ymax
72
- **zbounds** (Tuple[float,float]): zmin,zmax
73
- """
74
- xbounds = [blocks[0].X.min(),blocks[0].X.max()]
75
- ybounds = [blocks[0].Y.min(),blocks[0].Y.max()]
76
- zbounds = [blocks[0].Z.min(),blocks[0].Z.max()]
77
-
78
- for i in range(1,len(blocks)):
79
- xmin = blocks[i].X.min()
80
- xmax = blocks[i].X.max()
81
-
82
- ymin = blocks[i].Y.min()
83
- ymax = blocks[i].Y.max()
84
-
85
- zmin = blocks[i].Z.min()
86
- zmax = blocks[i].Z.max()
87
-
88
- if xmin<xbounds[0]:
89
- xbounds[0] = xmin
90
- elif xmax>xbounds[1]:
91
- xbounds[1] = xmax
92
-
93
- if ymin<ybounds[0]:
94
- ybounds[0] = ymin
95
- elif ymax>ybounds[1]:
96
- ybounds[1] = ymax
97
-
98
- if zmin<zbounds[0]:
99
- zbounds[0] = zmin
100
- elif zmax>zbounds[1]:
101
- zbounds[1] = zmax
102
-
103
- return tuple(xbounds),tuple(ybounds),tuple(zbounds)
104
-
105
- def block_connection_matrix(blocks:List[Block],outer_faces:List[Dict[str,int]]=[],tol:float=1E-8):
106
- """Creates a matrix representing how block edges are connected to each other
107
-
108
- Args:
109
- blocks (List[Block]): List of blocks that describe the Plot3D mesh
110
- outer_faces (List[Dict[str,int]], optional): List of outer faces remaining from connectivity. Useful if you are interested in finding faces that are exterior to the block. Also useful if you combine outerfaces with match faces, this will help identify connections by looking at split faces. Defaults to [].
111
- tol (float, optional): Matching tolerance to look for when comparing face centroids.
112
-
113
- Returns:
114
- (Tuple): containing
115
-
116
- *connectivity* (np.ndarray): integer matrix defining how the blocks are connected to each other
117
- *connectivity_i* (np.ndarray): integer matrix defining connectivity of all blocks where IMAX=IMIN
118
- *connectivity_j* (np.ndarray): integer matrix defining connectivity of all blocks where JMAX=JMIN
119
- *connectivity_k* (np.ndarray): integer matrix defining connectivity of all blocks where KMAX=KMIN
120
-
121
- """
122
- # Reduce the size of the blocks by the GCD
123
- gcd_array = list()
124
- for block_indx in range(len(blocks)):
125
- block = blocks[block_indx]
126
- gcd_array.append(math.gcd(block.IMAX-1, math.gcd(block.JMAX-1, block.KMAX-1)))
127
- gcd_to_use = min(gcd_array) # You need to use the minimum gcd otherwise 1 block may not exactly match the next block. They all have to be scaled the same way.
128
- blocks = reduce_blocks(deepcopy(blocks),gcd_to_use)
129
-
130
- # Face to List
131
- outer_faces_all = list()
132
- for o in outer_faces:
133
- face = create_face_from_diagonals(blocks[o['block_index']], int(o['IMIN']/gcd_to_use), int(o['JMIN']/gcd_to_use),
134
- int(o['KMIN']/gcd_to_use), int(o['IMAX']/gcd_to_use), int(o['JMAX']/gcd_to_use), int(o['KMAX']/gcd_to_use))
135
- face.set_block_index(o['block_index'])
136
- if "id" in o:
137
- face.id = o['id']
138
- outer_faces_all.append(face)
139
-
140
- outer_faces = outer_faces_all
141
-
142
- n = len(blocks)
143
- connectivity = np.eye(n,dtype=np.int8)
144
- combos = list(combinations(range(n),2))
145
- for indx in (pbar:=trange(len(combos))):
146
- i,j = combos[indx]
147
- pbar.set_description(f"Building block to block connectivity matrix: checking {i}")
148
- b1 = blocks[i]
149
-
150
- if len(outer_faces)==0: # Get the outerfaces to search
151
- b1_outer_faces,_ = get_outer_faces(b1)
152
- else:
153
- b1_outer_faces = [o for o in outer_faces if o.BlockIndex == i]
154
-
155
- if i != j and connectivity[i,j]!=-1:
156
- b2 = blocks[j]
157
-
158
- if len(outer_faces)==0: # Get the outerfaces to search
159
- b2_outer_faces,_ = get_outer_faces(b2)
160
- else:
161
- b2_outer_faces = [o for o in outer_faces if o.BlockIndex == j]
162
-
163
- # Check to see if any of the outer faces of the blocks match
164
- connection_found=False
165
- for f1 in b1_outer_faces:
166
- for f2 in b2_outer_faces:
167
- if (f1.is_connected(f2,tol)): # Check if face centroid is the same
168
- connectivity[i,j] = 1 # Default block to block connection matrix
169
- connectivity[j,i] = 1
170
- connection_found=True
171
- # c = np.sum(connectivity[i,:]==1)
172
- # print(f"block {i} connections {c}")
173
- break
174
- if connection_found:
175
- break
176
- if not connection_found:
177
- connectivity[i,j] = -1
178
- connectivity[j,i] = -1
179
- return connectivity
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes