plot3d 1.7.14__tar.gz → 1.8.1__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.
Files changed (25) hide show
  1. {plot3d-1.7.14 → plot3d-1.8.1}/PKG-INFO +4 -2
  2. {plot3d-1.7.14 → plot3d-1.8.1}/plot3d/__init__.py +2 -2
  3. {plot3d-1.7.14 → plot3d-1.8.1}/plot3d/blockfunctions.py +2 -2
  4. {plot3d-1.7.14 → plot3d-1.8.1}/plot3d/connectivity.py +156 -0
  5. {plot3d-1.7.14 → plot3d-1.8.1}/plot3d/gridpro/import_functions.py +12 -2
  6. {plot3d-1.7.14 → plot3d-1.8.1}/plot3d/periodicity.py +195 -18
  7. {plot3d-1.7.14 → plot3d-1.8.1}/plot3d/split_block.py +14 -14
  8. {plot3d-1.7.14 → plot3d-1.8.1}/plot3d/write.py +1 -1
  9. {plot3d-1.7.14 → plot3d-1.8.1}/pyproject.toml +1 -1
  10. {plot3d-1.7.14 → plot3d-1.8.1}/plot3d/block.py +0 -0
  11. {plot3d-1.7.14 → plot3d-1.8.1}/plot3d/block_merging_mixed_facepairs.py +0 -0
  12. {plot3d-1.7.14 → plot3d-1.8.1}/plot3d/differencing.py +0 -0
  13. {plot3d-1.7.14 → plot3d-1.8.1}/plot3d/face.py +0 -0
  14. {plot3d-1.7.14 → plot3d-1.8.1}/plot3d/facefunctions.py +0 -0
  15. {plot3d-1.7.14 → plot3d-1.8.1}/plot3d/glennht/__init__.py +0 -0
  16. {plot3d-1.7.14 → plot3d-1.8.1}/plot3d/glennht/class_definitions.py +0 -0
  17. {plot3d-1.7.14 → plot3d-1.8.1}/plot3d/glennht/export_functions.py +0 -0
  18. {plot3d-1.7.14 → plot3d-1.8.1}/plot3d/glennht/import_functions.py +0 -0
  19. {plot3d-1.7.14 → plot3d-1.8.1}/plot3d/graph.py +0 -0
  20. {plot3d-1.7.14 → plot3d-1.8.1}/plot3d/gridpro/__init__.py +0 -0
  21. {plot3d-1.7.14 → plot3d-1.8.1}/plot3d/listfunctions.py +0 -0
  22. {plot3d-1.7.14 → plot3d-1.8.1}/plot3d/point_match.py +0 -0
  23. {plot3d-1.7.14 → plot3d-1.8.1}/plot3d/pointwise/__init__.py +0 -0
  24. {plot3d-1.7.14 → plot3d-1.8.1}/plot3d/pointwise/import_functions.py +0 -0
  25. {plot3d-1.7.14 → plot3d-1.8.1}/plot3d/read.py +0 -0
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.4
2
2
  Name: plot3d
3
- Version: 1.7.14
3
+ Version: 1.8.1
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
@@ -8,6 +8,8 @@ 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
+ Classifier: Programming Language :: Python :: 3.13
12
+ Classifier: Programming Language :: Python :: 3.14
11
13
  Requires-Dist: matplotlib
12
14
  Requires-Dist: networkx
13
15
  Requires-Dist: numpy
@@ -5,13 +5,13 @@ import os, warnings
5
5
  from .block import Block
6
6
  from .blockfunctions import rotate_block, get_outer_bounds, block_connection_matrix,split_blocks, plot_blocks, reduce_blocks, find_matching_faces
7
7
  from .block_merging_mixed_facepairs import combine_nxnxn_cubes_mixed_pairs
8
- from .connectivity import find_matching_blocks, get_face_intersection, connectivity_fast, face_matches_to_dict
8
+ from .connectivity import find_matching_blocks, get_face_intersection, connectivity_fast, face_matches_to_dict, verify_connectivity
9
9
  from .face import Face
10
10
  from .facefunctions import create_face_from_diagonals, get_outer_faces, find_bounding_faces,split_face,find_face_nearest_point,match_faces_dict_to_list,outer_face_dict_to_list,find_closest_block
11
11
  from .read import read_plot3D, read_ap_nasa
12
12
  from .write import write_plot3D
13
13
  from .differencing import find_edges, find_face_edges
14
- from .periodicity import periodicity, periodicity_fast, create_rotation_matrix, rotated_periodicity, translational_periodicity
14
+ from .periodicity import periodicity, periodicity_fast, create_rotation_matrix, rotated_periodicity, translational_periodicity, verify_periodicity
15
15
  from .point_match import point_match
16
16
  from .split_block import split_blocks, Direction
17
17
  from .listfunctions import unique_pairs
@@ -216,7 +216,7 @@ def plot_blocks(blocks):
216
216
  block = blocks[block_indx]
217
217
  gcd_array.append(math.gcd(block.IMAX-1, math.gcd(block.JMAX-1, block.KMAX-1)))
218
218
  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.
219
- blocks = reduce_blocks(deepcopy(blocks),4)
219
+ blocks = reduce_blocks(deepcopy(blocks),gcd_to_use)
220
220
 
221
221
  fig = plt.figure(figsize=(10, 8))
222
222
  ax = fig.add_subplot(111, projection='3d')
@@ -327,7 +327,7 @@ def calculate_outward_normals(block:Block):
327
327
  z = [Z[0,0,0],Z[0,jmax,0],Z[0,0,kmax]]
328
328
  u = np.array([x[1]-x[0],y[1]-y[0],z[1]-z[0]])
329
329
  v = np.array([x[2]-x[0],y[2]-y[0],z[2]-z[0]])
330
- n_imin = np.cross(v1,v2) # type: ignore
330
+ n_imin = np.cross(u,v)
331
331
 
332
332
  # Normals I direction: IMAX
333
333
  x = [X[imax,0,0],X[imax,jmax,0],X[imax,0,kmax]]
@@ -565,3 +565,159 @@ def face_matches_to_dict(face1:Face, face2:Face,block1:Block,block2:Block):
565
565
  match['block2']['JMAX'] = int(df.iloc[0]['J'])
566
566
  match['block2']['KMAX'] = int(df.iloc[0]['K'])
567
567
  return match
568
+
569
+
570
+ def verify_connectivity(blocks: List[Block], face_matches: list, tol: float = 1E-6):
571
+ """Verifies that the diagonal corners of face_matches are spatially consistent.
572
+
573
+ For each face_match, checks that block1's lower corner coordinates match
574
+ block2's lower corner coordinates (and similarly for upper corners) within
575
+ the specified tolerance. If the stored diagonal doesn't match, tries all
576
+ permutations of block2's face corners. If a valid permutation is found,
577
+ the face_match is corrected and added to the verified list.
578
+
579
+ Uses GCD reduction (same as connectivity_fast) for efficient coordinate lookups.
580
+
581
+ Args:
582
+ blocks (List[Block]): List of all blocks (original full-resolution)
583
+ face_matches (list): List of face_match dicts from connectivity or periodicity
584
+ tol (float, optional): Euclidean distance tolerance. Defaults to 1E-6.
585
+
586
+ Returns:
587
+ (list): verified face_matches whose diagonals are confirmed or corrected
588
+ (list): mismatched face_matches where no corner permutation matched
589
+ """
590
+ # Compute GCD and reduce blocks (same pattern as connectivity_fast)
591
+ gcd_array = list()
592
+ for block_indx in range(len(blocks)):
593
+ block = blocks[block_indx]
594
+ gcd_array.append(math.gcd(block.IMAX-1, math.gcd(block.JMAX-1, block.KMAX-1)))
595
+ gcd_to_use = min(gcd_array)
596
+ reduced_blocks = reduce_blocks(deepcopy(blocks), gcd_to_use)
597
+
598
+ # Scale down face_matches indices by GCD
599
+ scaled_matches = deepcopy(face_matches)
600
+ for fm in scaled_matches:
601
+ for side in ['block1', 'block2']:
602
+ for key in ['IMIN', 'JMIN', 'KMIN', 'IMAX', 'JMAX', 'KMAX']:
603
+ fm[side][key] = fm[side][key] // gcd_to_use
604
+
605
+ verified = list()
606
+ mismatched = list()
607
+
608
+ for idx in range(len(scaled_matches)):
609
+ fm = scaled_matches[idx]
610
+ b1 = fm['block1']
611
+ b2 = fm['block2']
612
+ b1_idx = b1['block_index']
613
+ b2_idx = b2['block_index']
614
+ block1 = reduced_blocks[b1_idx]
615
+ block2 = reduced_blocks[b2_idx]
616
+
617
+ # Block1 diagonal coordinates
618
+ x1_l = block1.X[b1['IMIN'], b1['JMIN'], b1['KMIN']]
619
+ y1_l = block1.Y[b1['IMIN'], b1['JMIN'], b1['KMIN']]
620
+ z1_l = block1.Z[b1['IMIN'], b1['JMIN'], b1['KMIN']]
621
+
622
+ x1_u = block1.X[b1['IMAX'], b1['JMAX'], b1['KMAX']]
623
+ y1_u = block1.Y[b1['IMAX'], b1['JMAX'], b1['KMAX']]
624
+ z1_u = block1.Z[b1['IMAX'], b1['JMAX'], b1['KMAX']]
625
+
626
+ # Enumerate unique corners of block2's face
627
+ I2 = [b2['IMIN'], b2['IMAX']]
628
+ J2 = [b2['JMIN'], b2['JMAX']]
629
+ K2 = [b2['KMIN'], b2['KMAX']]
630
+
631
+ unique_corners = list()
632
+ seen = set()
633
+ for i in I2:
634
+ for j in J2:
635
+ for k in K2:
636
+ key = (i, j, k)
637
+ if key not in seen:
638
+ seen.add(key)
639
+ unique_corners.append(key)
640
+
641
+ # Check stored diagonal first
642
+ x2_l = block2.X[b2['IMIN'], b2['JMIN'], b2['KMIN']]
643
+ y2_l = block2.Y[b2['IMIN'], b2['JMIN'], b2['KMIN']]
644
+ z2_l = block2.Z[b2['IMIN'], b2['JMIN'], b2['KMIN']]
645
+
646
+ x2_u = block2.X[b2['IMAX'], b2['JMAX'], b2['KMAX']]
647
+ y2_u = block2.Y[b2['IMAX'], b2['JMAX'], b2['KMAX']]
648
+ z2_u = block2.Z[b2['IMAX'], b2['JMAX'], b2['KMAX']]
649
+
650
+ dx = x2_l - x1_l; dy = y2_l - y1_l; dz = z2_l - z1_l
651
+ d_lower = math.sqrt(dx*dx + dy*dy + dz*dz)
652
+ dx = x2_u - x1_u; dy = y2_u - y1_u; dz = z2_u - z1_u
653
+ d_upper = math.sqrt(dx*dx + dy*dy + dz*dz)
654
+
655
+ if d_lower < tol and d_upper < tol:
656
+ verified.append(face_matches[idx])
657
+ continue
658
+
659
+ # Try all permutations of block2's corners
660
+ found = False
661
+ best_d_lower = d_lower
662
+ best_d_upper = d_upper
663
+
664
+ for corner_lower in unique_corners:
665
+ for corner_upper in unique_corners:
666
+ if corner_lower == corner_upper:
667
+ continue
668
+
669
+ il, jl, kl = corner_lower
670
+ iu, ju, ku = corner_upper
671
+
672
+ x2_l = block2.X[il, jl, kl]
673
+ y2_l = block2.Y[il, jl, kl]
674
+ z2_l = block2.Z[il, jl, kl]
675
+
676
+ x2_u = block2.X[iu, ju, ku]
677
+ y2_u = block2.Y[iu, ju, ku]
678
+ z2_u = block2.Z[iu, ju, ku]
679
+
680
+ dx = x2_l - x1_l; dy = y2_l - y1_l; dz = z2_l - z1_l
681
+ dl = math.sqrt(dx*dx + dy*dy + dz*dz)
682
+ dx = x2_u - x1_u; dy = y2_u - y1_u; dz = z2_u - z1_u
683
+ du = math.sqrt(dx*dx + dy*dy + dz*dz)
684
+
685
+ if dl < best_d_lower:
686
+ best_d_lower = dl
687
+ if du < best_d_upper:
688
+ best_d_upper = du
689
+
690
+ if dl < tol and du < tol:
691
+ corrected = deepcopy(face_matches[idx])
692
+ corrected['block2']['IMIN'] = il * gcd_to_use
693
+ corrected['block2']['JMIN'] = jl * gcd_to_use
694
+ corrected['block2']['KMIN'] = kl * gcd_to_use
695
+ corrected['block2']['IMAX'] = iu * gcd_to_use
696
+ corrected['block2']['JMAX'] = ju * gcd_to_use
697
+ corrected['block2']['KMAX'] = ku * gcd_to_use
698
+ verified.append(corrected)
699
+ if b1_idx == b2_idx:
700
+ print("verify_connectivity: Self-match corrected for block index {0}".format(b1_idx))
701
+ found = True
702
+ break
703
+ if found:
704
+ break
705
+
706
+ if not found:
707
+ orig = face_matches[idx]
708
+ b1_orig = orig['block1']
709
+ b2_orig = orig['block2']
710
+ print(f"verify_connectivity: MISMATCH at face_match index {idx}")
711
+ print(f" block1 (block_index={b1_orig['block_index']}): "
712
+ f"lower=({b1_orig['IMIN']},{b1_orig['JMIN']},{b1_orig['KMIN']}) "
713
+ f"upper=({b1_orig['IMAX']},{b1_orig['JMAX']},{b1_orig['KMAX']})")
714
+ print(f" block2 (block_index={b2_orig['block_index']}): "
715
+ f"lower=({b2_orig['IMIN']},{b2_orig['JMIN']},{b2_orig['KMIN']}) "
716
+ f"upper=({b2_orig['IMAX']},{b2_orig['JMAX']},{b2_orig['KMAX']})")
717
+ print(f" block1 lower xyz = ({x1_l:.6e}, {y1_l:.6e}, {z1_l:.6e})")
718
+ print(f" block1 upper xyz = ({x1_u:.6e}, {y1_u:.6e}, {z1_u:.6e})")
719
+ print(f" Closest block2 corner dist to block1 lower: {best_d_lower:.6e}")
720
+ print(f" Closest block2 corner dist to block1 upper: {best_d_upper:.6e}")
721
+ mismatched.append(face_matches[idx])
722
+
723
+ return verified, mismatched
@@ -1,8 +1,7 @@
1
- from typing import Dict, List, Tuple, Optional, Union, Set
1
+ from typing import Dict, List, Tuple, Optional, Union, Set, Any
2
2
  import numpy as np
3
3
  import pandas as pd
4
4
  from tqdm import tqdm
5
- from typing import List, Tuple, Optional, Any
6
5
  from plot3d import Block
7
6
  from collections import defaultdict
8
7
 
@@ -221,6 +220,8 @@ def read_gridpro_connectivity(
221
220
  parse_patch(toks)
222
221
 
223
222
  patches_df = pd.DataFrame(patch_rows)
223
+ patches_to_connectivity = patches_df[(patches_df['sb1'] > 0) & (patches_df['sb2'] > 0) & (patches_df['pty'] > 1)]
224
+
224
225
 
225
226
  # ---------------------------- helpers ----------------------------
226
227
  def face_dict(sb: int, imin: int, jmin: int, kmin: int, imax: int, jmax: int, kmax: int, pty:int=-1) -> Dict[str, int]:
@@ -253,6 +254,14 @@ def read_gridpro_connectivity(
253
254
  )
254
255
  )
255
256
 
257
+ # These are patches that should be in connectivity but aren't
258
+ connectivity_to_check: List[Dict[str, Dict[str, int]]] = []
259
+ for p in patches_to_connectivity.itertuples(index=False):
260
+ connectivity_to_check.append(pair_dict(
261
+ p.sb1, (p.L1i, p.L1j, p.L1k, p.H1i, p.H1j, p.H1k),
262
+ p.sb2, (p.L2i, p.L2j, p.L2k, p.H2i, p.H2j, p.H2k),
263
+ ))
264
+
256
265
  # Boundary-condition groups (by pty)
257
266
  def bc_faces_for(name: str, pty_vals: List[int], bc_type: str) -> List[Dict[str, int]]:
258
267
  targets = set(pty_vals)
@@ -384,6 +393,7 @@ def read_gridpro_connectivity(
384
393
  "periodic_faces": periodic_faces,
385
394
  "volume_zones": volume_zones,
386
395
  "blocksizes": superblock_sizes,
396
+ "connectivity_to_check": connectivity_to_check,
387
397
  "patches": patches_df,
388
398
  }
389
399
 
@@ -6,19 +6,19 @@ from .blockfunctions import rotate_block, reduce_blocks
6
6
  from .face import Face
7
7
  from .facefunctions import outer_face_dict_to_list,match_faces_dict_to_list, create_face_from_diagonals, find_bounding_faces,get_outer_faces
8
8
  from .connectivity import get_face_intersection, face_matches_to_dict
9
- from math import cos, radians, sin, sqrt, acos, radians
9
+ from math import cos, radians, sin, sqrt, acos
10
10
  from copy import deepcopy
11
11
  from tqdm import trange, tqdm
12
12
  import math
13
13
 
14
- def periodicity_fast(blocks:List[Block],outer_faces:List[Face], matched_faces:List[Dict[str,int]], periodic_direction:str='k', rotation_axis:str='x',nblades:int=55):
14
+ def periodicity_fast(blocks:List[Block],outer_faces:List[Dict[str,int]], matched_faces:List[Dict[str,int]], periodic_direction:str='k', rotation_axis:str='x',nblades:int=55):
15
15
  """Finds the connectivity of blocks when they are rotated by an angle defined by the number of blades. Only use this if your mesh is of an annulus.
16
16
  This function reduces the size of the blocks by a factor of the minimum gcd. This speeds up finding the connectivity
17
17
 
18
18
  Args:
19
19
  blocks (List[Block]): List of blocks that will be scanned for perodicity
20
20
  outer_faces (List[Dict[str,int]]): List of outer faces for each block as a dictionary format. You can get this from connectivity
21
- matched_faces (ListList[Dict[str,int]]): List of matched faces from connectivity. Matched faces was added so that it's always removed from outer faces
21
+ matched_faces (List[Dict[str,int]]): List of matched faces from connectivity. Matched faces was added so that it's always removed from outer faces
22
22
  periodic_direction (str): either i,j,k to look for
23
23
  rotation_axis (str): either x,y,z
24
24
  nblades (int): Number of blades to consider, this affects the rotation angle.
@@ -132,8 +132,10 @@ def create_rotation_matrix(rotation_angle:float, rotation_axis:str="x"):
132
132
  rotation_matrix = np.array([[cos(rotation_angle),-sin(rotation_angle), 0],
133
133
  [sin(rotation_angle),cos(rotation_angle), 0],
134
134
  [0, 0, 1]])
135
+ else:
136
+ raise ValueError(f"rotation_axis must be 'x', 'y', or 'z', got '{rotation_axis}'")
135
137
 
136
- return rotation_matrix
138
+ return rotation_matrix
137
139
 
138
140
  def periodicity(blocks:List[Block],outer_faces:List[Dict[str,int]], matched_faces:List[Dict[str,int]], periodic_direction:str='k', rotation_axis:str='x',nblades:int=55):
139
141
  """This function is used to check for periodicity of the other faces rotated about an axis. Use periodicity_fast instead. Periodicity_fast calls this function after reducing the size of the mesh.
@@ -142,7 +144,7 @@ def periodicity(blocks:List[Block],outer_faces:List[Dict[str,int]], matched_face
142
144
  Args:
143
145
  blocks (List[Block]): List of blocks that will be scanned for perodicity
144
146
  outer_faces (List[Dict[str,int]]): List of outer faces for each block as a dictionary format. You can get this from connectivity
145
- matched_faces (ListList[Dict[str,int]]): List of matched faces from connectivity. Matched faces was added so that it's always removed from outer faces
147
+ matched_faces (List[Dict[str,int]]): List of matched faces from connectivity. Matched faces was added so that it's always removed from outer faces
146
148
  periodic_direction (str): either i,j,k to look for
147
149
  rotation_axis (str): either x,y,z
148
150
  nblades (int): Number of blades to consider, this affects the rotation angle.
@@ -171,9 +173,10 @@ def periodicity(blocks:List[Block],outer_faces:List[Dict[str,int]], matched_face
171
173
  matched_faces_all = match_faces_dict_to_list(blocks,matched_faces)
172
174
 
173
175
  split_faces = list() # List of split but free surfaces, this will be appended to outer_faces_to_remove list
176
+ outer_faces_to_remove = list() # Integer list of which outer surfaces to remove
174
177
  while periodic_found:
175
- periodic_found = False
176
- outer_faces_to_remove = list() # Integer list of which outher surfaces to remove
178
+ periodic_found = False
179
+ outer_faces_to_remove = list()
177
180
  outer_face_combos = list(combinations_with_replacement(range(len(outer_faces_all)),2))
178
181
  t = trange(len(outer_face_combos))
179
182
  for i in t:
@@ -204,7 +207,7 @@ def periodicity(blocks:List[Block],outer_faces:List[Dict[str,int]], matched_face
204
207
  outer_faces_to_remove.append(periodic_faces_temp[0]) # Make sure periodic faces are also removed from outer faces during the loop
205
208
  outer_faces_to_remove.append(periodic_faces_temp[1])
206
209
  periodic_faces.append(periodic_faces_temp)
207
- periodic_faces_export.append((face1,face2,block1_rotated,block2))
210
+ periodic_faces_export.append(face_matches_to_dict(face1,face2,block1_rotated,block2))
208
211
  split_faces.extend(split_faces_temp)
209
212
  periodic_found = True
210
213
  break
@@ -339,7 +342,8 @@ def rotated_periodicity(blocks:List[Block], matched_faces:List[Dict[str,int]], o
339
342
  Rotates the set of blocks by an angle and checks the matching surfaces for R and L.
340
343
 
341
344
  Args:
342
- blocks (List[Block]): List of blocks for a particular geometry. Do not duplicate the geometry and pass it in!
345
+ blocks (List[Block]): List of blocks for a particular geometry. Do not duplicate the geometry and pass it in!
346
+ matched_faces (List[Dict[str,int]]): List of matched faces from connectivity. Matched faces was added so that it's always removed from outer faces
343
347
  outer_faces (List[Dict[str,int]]): List of outer faces in dictionary form
344
348
  rotation_angle (float): rotation angle in between geometry in degrees.
345
349
  rotation_axis (str, Optional): "x", "y", or "z"
@@ -364,7 +368,8 @@ def rotated_periodicity(blocks:List[Block], matched_faces:List[Dict[str,int]], o
364
368
  - **outer_faces_all** (List[Face]): This is a list of outer faces save as a list of Faces
365
369
  """
366
370
  gcd_array = list()
367
- # Find the gcd of all the blocks
371
+ gcd_to_use = 1
372
+ # Find the gcd of all the blocks
368
373
  if ReduceMesh:
369
374
  for block_indx in range(len(blocks)):
370
375
  block = blocks[block_indx]
@@ -388,29 +393,29 @@ def rotated_periodicity(blocks:List[Block], matched_faces:List[Dict[str,int]], o
388
393
 
389
394
  split_faces = list() # List of split but free surfaces, this will be appended to outer_faces_to_remove list
390
395
  non_matching = list()
396
+ outer_faces_to_remove = list() # Integer list of which outer surfaces to remove
391
397
  while periodic_found:
392
- periodic_found = False
393
- outer_faces_to_remove = list() # Integer list of which outer surfaces to remove
398
+ periodic_found = False
399
+ outer_faces_to_remove = list()
394
400
  outer_face_combos = list(permutations(range(len(outer_faces_all)),2))
395
401
  outer_face_combos = list(set(outer_face_combos) - set(non_matching)) # removes face combinations already checked
396
402
  t = trange(len(outer_face_combos))
397
- for i in t:
403
+ for i in t:
398
404
  # Check if surfaces are periodic with each other
399
405
  face1_indx = outer_face_combos[i][0]
400
406
  face2_indx = outer_face_combos[i][1]
401
407
  face1 = outer_faces_all[face1_indx]
402
408
  face2 = outer_faces_all[face2_indx]
403
- t.set_description(f"Checking connections block {face1.blockIndex} with {face2.blockIndex}")
404
409
 
405
410
  if (face1.IMIN == face1.IMAX) and (face2.IMIN == face2.IMAX) or \
406
411
  (face1.JMIN == face1.JMAX) and (face2.JMIN == face2.JMAX) or \
407
412
  (face1.KMIN == face1.KMAX) and (face2.KMIN == face2.KMAX):
408
-
413
+
409
414
  # Rotate Block 1 -> Check periodicity -> if not periodic -> Rotate Block 1 opposite direction -> Check periodicity
410
415
  # Rotate Block 1
411
416
  block1_rotated = blocks_rotated[face1.blockIndex]
412
417
  block2 = blocks[face2.blockIndex]
413
- print(f'evaluating working blocks: a={face1.blockIndex} b={face2.blockIndex}')
418
+ t.set_description(f"Blk {face1.blockIndex} <-> {face2.blockIndex} | found {len(periodic_faces)}")
414
419
  # Check periodicity
415
420
  df, periodic_faces_temp, split_faces_temp = __periodicity_check__(face1,face2,block1_rotated, block2)
416
421
 
@@ -473,8 +478,6 @@ def rotated_periodicity(blocks:List[Block], matched_faces:List[Dict[str,int]], o
473
478
  periodic_faces_export[i]['block1']['JMAX'] *= gcd_to_use
474
479
  periodic_faces_export[i]['block1']['KMAX'] *= gcd_to_use
475
480
 
476
- if (periodic_faces_export[i]['block2']['IMIN'] == 168):
477
- print("check")
478
481
  periodic_faces_export[i]['block2']['IMIN'] *= gcd_to_use
479
482
  periodic_faces_export[i]['block2']['JMIN'] *= gcd_to_use
480
483
  periodic_faces_export[i]['block2']['KMIN'] *= gcd_to_use
@@ -904,3 +907,177 @@ def __periodicity_check__(face1:Face, face2:Face,block1:Block,block2:Block,tol:f
904
907
  return df,periodic_faces,split_faces
905
908
 
906
909
 
910
+ def verify_periodicity(blocks: List[Block], face_matches: list, theta: float, rotation_axis: str = 'x', tol: float = 1E-6):
911
+ """Verifies that the diagonal corners of periodic face_matches are spatially
912
+ consistent after rotating block1 by ±theta about the given axis.
913
+
914
+ For each face_match, rotates block1 by +theta (and if needed -theta) and checks
915
+ that the rotated lower/upper corner coordinates match block2's lower/upper
916
+ corners within tolerance. If the stored diagonal doesn't match, tries all
917
+ permutations of block2's face corners. If a valid permutation is found,
918
+ the face_match is corrected and added to the verified list.
919
+
920
+ Uses GCD reduction (same as connectivity_fast / periodicity_fast) for
921
+ efficient coordinate lookups.
922
+
923
+ Args:
924
+ blocks (List[Block]): List of all blocks (original full-resolution)
925
+ face_matches (list): List of face_match dicts from periodicity or rotated_periodicity
926
+ theta (float): Rotation angle in degrees
927
+ rotation_axis (str, optional): Axis of rotation 'x', 'y', or 'z'. Defaults to 'x'.
928
+ tol (float, optional): Euclidean distance tolerance. Defaults to 1E-6.
929
+
930
+ Returns:
931
+ (list): verified face_matches whose diagonals are confirmed or corrected
932
+ (list): mismatched face_matches where no corner permutation matched
933
+ """
934
+ # Compute GCD and reduce blocks (same pattern as connectivity_fast)
935
+ gcd_array = list()
936
+ for block_indx in range(len(blocks)):
937
+ block = blocks[block_indx]
938
+ gcd_array.append(math.gcd(block.IMAX-1, math.gcd(block.JMAX-1, block.KMAX-1)))
939
+ gcd_to_use = min(gcd_array)
940
+ reduced_blocks = reduce_blocks(deepcopy(blocks), gcd_to_use)
941
+
942
+ # Build rotation matrices for +theta and -theta
943
+ rotation_matrix_pos = create_rotation_matrix(radians(theta), rotation_axis)
944
+ rotation_matrix_neg = create_rotation_matrix(radians(-theta), rotation_axis)
945
+
946
+ # Pre-rotate all reduced blocks in both directions
947
+ rotated_blocks_pos = [rotate_block(b, rotation_matrix_pos) for b in reduced_blocks]
948
+ rotated_blocks_neg = [rotate_block(b, rotation_matrix_neg) for b in reduced_blocks]
949
+
950
+ # Scale down face_matches indices by GCD
951
+ scaled_matches = deepcopy(face_matches)
952
+ for fm in scaled_matches:
953
+ for side in ['block1', 'block2']:
954
+ for key in ['IMIN', 'JMIN', 'KMIN', 'IMAX', 'JMAX', 'KMAX']:
955
+ fm[side][key] = fm[side][key] // gcd_to_use
956
+
957
+ verified = list()
958
+ mismatched = list()
959
+
960
+ for idx in range(len(scaled_matches)):
961
+ fm = scaled_matches[idx]
962
+ b1 = fm['block1']
963
+ b2 = fm['block2']
964
+ b1_idx = b1['block_index']
965
+ b2_idx = b2['block_index']
966
+ block2 = reduced_blocks[b2_idx]
967
+
968
+ # Enumerate unique corners of block2's face
969
+ I2 = [b2['IMIN'], b2['IMAX']]
970
+ J2 = [b2['JMIN'], b2['JMAX']]
971
+ K2 = [b2['KMIN'], b2['KMAX']]
972
+
973
+ unique_corners = list()
974
+ seen = set()
975
+ for i in I2:
976
+ for j in J2:
977
+ for k in K2:
978
+ key = (i, j, k)
979
+ if key not in seen:
980
+ seen.add(key)
981
+ unique_corners.append(key)
982
+
983
+ found = False
984
+ best_d_lower = float('inf')
985
+ best_d_upper = float('inf')
986
+
987
+ # Try +theta rotation first, then -theta
988
+ for rotated_blocks in [rotated_blocks_pos, rotated_blocks_neg]:
989
+ if found:
990
+ break
991
+
992
+ block1_rotated = rotated_blocks[b1_idx]
993
+
994
+ # Block1 rotated diagonal coordinates
995
+ x1_l = block1_rotated.X[b1['IMIN'], b1['JMIN'], b1['KMIN']]
996
+ y1_l = block1_rotated.Y[b1['IMIN'], b1['JMIN'], b1['KMIN']]
997
+ z1_l = block1_rotated.Z[b1['IMIN'], b1['JMIN'], b1['KMIN']]
998
+
999
+ x1_u = block1_rotated.X[b1['IMAX'], b1['JMAX'], b1['KMAX']]
1000
+ y1_u = block1_rotated.Y[b1['IMAX'], b1['JMAX'], b1['KMAX']]
1001
+ z1_u = block1_rotated.Z[b1['IMAX'], b1['JMAX'], b1['KMAX']]
1002
+
1003
+ # Check stored diagonal first
1004
+ x2_l = block2.X[b2['IMIN'], b2['JMIN'], b2['KMIN']]
1005
+ y2_l = block2.Y[b2['IMIN'], b2['JMIN'], b2['KMIN']]
1006
+ z2_l = block2.Z[b2['IMIN'], b2['JMIN'], b2['KMIN']]
1007
+
1008
+ x2_u = block2.X[b2['IMAX'], b2['JMAX'], b2['KMAX']]
1009
+ y2_u = block2.Y[b2['IMAX'], b2['JMAX'], b2['KMAX']]
1010
+ z2_u = block2.Z[b2['IMAX'], b2['JMAX'], b2['KMAX']]
1011
+
1012
+ dx = x2_l - x1_l; dy = y2_l - y1_l; dz = z2_l - z1_l
1013
+ d_lower = math.sqrt(dx*dx + dy*dy + dz*dz)
1014
+ dx = x2_u - x1_u; dy = y2_u - y1_u; dz = z2_u - z1_u
1015
+ d_upper = math.sqrt(dx*dx + dy*dy + dz*dz)
1016
+
1017
+ if d_lower < best_d_lower:
1018
+ best_d_lower = d_lower
1019
+ if d_upper < best_d_upper:
1020
+ best_d_upper = d_upper
1021
+
1022
+ if d_lower < tol and d_upper < tol:
1023
+ verified.append(face_matches[idx])
1024
+ found = True
1025
+ break
1026
+
1027
+ # Try all permutations of block2's corners
1028
+ for corner_lower in unique_corners:
1029
+ for corner_upper in unique_corners:
1030
+ if corner_lower == corner_upper:
1031
+ continue
1032
+
1033
+ il, jl, kl = corner_lower
1034
+ iu, ju, ku = corner_upper
1035
+
1036
+ x2_l = block2.X[il, jl, kl]
1037
+ y2_l = block2.Y[il, jl, kl]
1038
+ z2_l = block2.Z[il, jl, kl]
1039
+
1040
+ x2_u = block2.X[iu, ju, ku]
1041
+ y2_u = block2.Y[iu, ju, ku]
1042
+ z2_u = block2.Z[iu, ju, ku]
1043
+
1044
+ dx = x2_l - x1_l; dy = y2_l - y1_l; dz = z2_l - z1_l
1045
+ dl = math.sqrt(dx*dx + dy*dy + dz*dz)
1046
+ dx = x2_u - x1_u; dy = y2_u - y1_u; dz = z2_u - z1_u
1047
+ du = math.sqrt(dx*dx + dy*dy + dz*dz)
1048
+
1049
+ if dl < best_d_lower:
1050
+ best_d_lower = dl
1051
+ if du < best_d_upper:
1052
+ best_d_upper = du
1053
+
1054
+ if dl < tol and du < tol:
1055
+ corrected = deepcopy(face_matches[idx])
1056
+ corrected['block2']['IMIN'] = il * gcd_to_use
1057
+ corrected['block2']['JMIN'] = jl * gcd_to_use
1058
+ corrected['block2']['KMIN'] = kl * gcd_to_use
1059
+ corrected['block2']['IMAX'] = iu * gcd_to_use
1060
+ corrected['block2']['JMAX'] = ju * gcd_to_use
1061
+ corrected['block2']['KMAX'] = ku * gcd_to_use
1062
+ verified.append(corrected)
1063
+ found = True
1064
+ break
1065
+ if found:
1066
+ break
1067
+
1068
+ if not found:
1069
+ orig = face_matches[idx]
1070
+ b1_orig = orig['block1']
1071
+ b2_orig = orig['block2']
1072
+ print(f"verify_periodicity: MISMATCH at face_match index {idx}")
1073
+ print(f" block1 (block_index={b1_orig['block_index']}): "
1074
+ f"lower=({b1_orig['IMIN']},{b1_orig['JMIN']},{b1_orig['KMIN']}) "
1075
+ f"upper=({b1_orig['IMAX']},{b1_orig['JMAX']},{b1_orig['KMAX']})")
1076
+ print(f" block2 (block_index={b2_orig['block_index']}): "
1077
+ f"lower=({b2_orig['IMIN']},{b2_orig['JMIN']},{b2_orig['KMIN']}) "
1078
+ f"upper=({b2_orig['IMAX']},{b2_orig['JMAX']},{b2_orig['KMAX']})")
1079
+ print(f" Closest rotated block1 corner dist to block2 lower: {best_d_lower:.6e}")
1080
+ print(f" Closest rotated block1 corner dist to block2 upper: {best_d_upper:.6e}")
1081
+ mismatched.append(face_matches[idx])
1082
+
1083
+ return verified, mismatched
@@ -50,17 +50,17 @@ def max_aspect_ratio(X:np.ndarray,Y:np.ndarray,Z:np.ndarray,ix:int,jx:int,kx:int
50
50
 
51
51
  ds = list()
52
52
  for n in range(len(i2)):
53
- ds.append(sqrt((X(i2[n],j1[n],k1[n])-X(i1[n],j1[n],k1[n]))**2 +
54
- (Y(i2[n],j1[n],k1[n])-Y(i1[n],j1[n],k1[n]))**2 +
55
- (Z(i2[n],j1[n],k1[n])-Z(i1[n],j1[n],k1[n]))**2)
53
+ ds.append(sqrt((X[i2[n],j1[n],k1[n]]-X[i1[n],j1[n],k1[n]])**2 +
54
+ (Y[i2[n],j1[n],k1[n]]-Y[i1[n],j1[n],k1[n]])**2 +
55
+ (Z[i2[n],j1[n],k1[n]]-Z[i1[n],j1[n],k1[n]])**2)
56
56
  )
57
- ds.append(sqrt((X(i1[n],j2[n],k1[n])-X(i1[n],j1[n],k1[n]))**2 +
58
- (Y(i1[n],j2[n],k1[n])-Y(i1[n],j1[n],k1[n]))**2 +
59
- (Z(i1[n],j2[n],k1[n])-Z(i1[n],j1[n],k1[n]))**2)
57
+ ds.append(sqrt((X[i1[n],j2[n],k1[n]]-X[i1[n],j1[n],k1[n]])**2 +
58
+ (Y[i1[n],j2[n],k1[n]]-Y[i1[n],j1[n],k1[n]])**2 +
59
+ (Z[i1[n],j2[n],k1[n]]-Z[i1[n],j1[n],k1[n]])**2)
60
60
  )
61
- ds.append(sqrt((X(i1[n],j1[n],k2[n])-X(i1[n],j1[n],k1[n]))**2 +
62
- (Y(i1[n],j1[n],k2[n])-Y(i1[n],j1[n],k1[n]))**2 +
63
- (Z(i1[n],j1[n],k2[n])-Z(i1[n],j1[n],k1[n]))**2)
61
+ ds.append(sqrt((X[i1[n],j1[n],k2[n]]-X[i1[n],j1[n],k1[n]])**2 +
62
+ (Y[i1[n],j1[n],k2[n]]-Y[i1[n],j1[n],k1[n]])**2 +
63
+ (Z[i1[n],j1[n],k2[n]]-Z[i1[n],j1[n],k1[n]])**2)
64
64
  )
65
65
  aspect = [0,0,0]
66
66
  if ds[0]>0:
@@ -134,7 +134,7 @@ def split_blocks(blocks:List[Block], ncells_per_block:int,direction:Direction=No
134
134
  total_cells = block.IMAX*block.JMAX*block.KMAX
135
135
 
136
136
  if direction==None:
137
- indx = np.argmin(np.array([block.IMAX,block.JMAX,block.KMAX]))
137
+ indx = np.argmax(np.array([block.IMAX,block.JMAX,block.KMAX]))
138
138
  if indx == 0:
139
139
  direction_to_use=Direction.i
140
140
  elif indx == 1:
@@ -153,7 +153,7 @@ def split_blocks(blocks:List[Block], ncells_per_block:int,direction:Direction=No
153
153
  if step_size==-1:
154
154
  step_size = __step_search(total_cells,greatest_common_divisor,ncells_per_block,denominator,direction='forward')
155
155
  if step_size==-1:
156
- assert('no valid step size found, do you have multi-block? gcd > 1')
156
+ raise ValueError('no valid step size found, do you have multi-block? gcd > 1')
157
157
  # step_size-1 is the IMAX of the sub_blocks e.g. 0 to 92 this shows IMAX=93, (93-1) % 4 = 0 (good)
158
158
 
159
159
  iprev = 0
@@ -181,10 +181,10 @@ def split_blocks(blocks:List[Block], ncells_per_block:int,direction:Direction=No
181
181
  if step_size==-1:
182
182
  step_size = __step_search(total_cells,greatest_common_divisor,ncells_per_block,denominator,direction='forward')
183
183
  if step_size==-1:
184
- assert('no valid step size found, do you have multi-block? gcd > 1')
184
+ raise ValueError('no valid step size found, do you have multi-block? gcd > 1')
185
185
  jprev = 0
186
186
  for j in range(step_size,block.JMAX,step_size):
187
- if (j+1) > block.IMAX:
187
+ if (j+1) > block.JMAX:
188
188
  break
189
189
  X = block.X[:,jprev:j,:] # New X, Y, Z splits
190
190
  Y = block.Y[:,jprev:j,:]
@@ -205,7 +205,7 @@ def split_blocks(blocks:List[Block], ncells_per_block:int,direction:Direction=No
205
205
  if step_size==-1:
206
206
  step_size = __step_search(total_cells,greatest_common_divisor,ncells_per_block,denominator,direction='forward')
207
207
  if step_size==-1:
208
- assert('no valid step size found, do you have multi-block? gcd > 1')
208
+ raise ValueError('no valid step size found, do you have multi-block? gcd > 1')
209
209
  kprev = 0
210
210
  for k in range(step_size,block.KMAX,step_size):
211
211
  if (k+1) > block.KMAX:
@@ -51,7 +51,7 @@ def __write_plot3D_block_ASCII(f, B:Block, double_precision:bool=True, columns:i
51
51
  batch_size (int, optional): number of lines to buffer before writing. Defaults to 100.
52
52
  """
53
53
  # Scientific notation format: width.precision E
54
- fmt = '{0:23.15E}' if double_precision else '{0:15.8E}'
54
+ fmt = '{0:23.15f}' if double_precision else '{0:15.8f}'
55
55
 
56
56
  def write_var(V:np.ndarray):
57
57
  line_entries = []
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "plot3d"
3
- version = "1.7.14"
3
+ version = "1.8.1"
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
 
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes