@@ -904,3 +904,177 @@ def __periodicity_check__(face1:Face, face2:Face,block1:Block,block2:Block,tol:f
904904 return df ,periodic_faces ,split_faces
905905
906906
907+ def verify_periodicity (blocks : List [Block ], face_matches : list , theta : float , rotation_axis : str = 'x' , tol : float = 1E-6 ):
908+ """Verifies that the diagonal corners of periodic face_matches are spatially
909+ consistent after rotating block1 by ±theta about the given axis.
910+
911+ For each face_match, rotates block1 by +theta (and if needed -theta) and checks
912+ that the rotated lower/upper corner coordinates match block2's lower/upper
913+ corners within tolerance. If the stored diagonal doesn't match, tries all
914+ permutations of block2's face corners. If a valid permutation is found,
915+ the face_match is corrected and added to the verified list.
916+
917+ Uses GCD reduction (same as connectivity_fast / periodicity_fast) for
918+ efficient coordinate lookups.
919+
920+ Args:
921+ blocks (List[Block]): List of all blocks (original full-resolution)
922+ face_matches (list): List of face_match dicts from periodicity or rotated_periodicity
923+ theta (float): Rotation angle in degrees
924+ rotation_axis (str, optional): Axis of rotation 'x', 'y', or 'z'. Defaults to 'x'.
925+ tol (float, optional): Euclidean distance tolerance. Defaults to 1E-6.
926+
927+ Returns:
928+ (list): verified face_matches whose diagonals are confirmed or corrected
929+ (list): mismatched face_matches where no corner permutation matched
930+ """
931+ # Compute GCD and reduce blocks (same pattern as connectivity_fast)
932+ gcd_array = list ()
933+ for block_indx in range (len (blocks )):
934+ block = blocks [block_indx ]
935+ gcd_array .append (math .gcd (block .IMAX - 1 , math .gcd (block .JMAX - 1 , block .KMAX - 1 )))
936+ gcd_to_use = min (gcd_array )
937+ reduced_blocks = reduce_blocks (deepcopy (blocks ), gcd_to_use )
938+
939+ # Build rotation matrices for +theta and -theta
940+ rotation_matrix_pos = create_rotation_matrix (radians (theta ), rotation_axis )
941+ rotation_matrix_neg = create_rotation_matrix (radians (- theta ), rotation_axis )
942+
943+ # Pre-rotate all reduced blocks in both directions
944+ rotated_blocks_pos = [rotate_block (b , rotation_matrix_pos ) for b in reduced_blocks ]
945+ rotated_blocks_neg = [rotate_block (b , rotation_matrix_neg ) for b in reduced_blocks ]
946+
947+ # Scale down face_matches indices by GCD
948+ scaled_matches = deepcopy (face_matches )
949+ for fm in scaled_matches :
950+ for side in ['block1' , 'block2' ]:
951+ for key in ['IMIN' , 'JMIN' , 'KMIN' , 'IMAX' , 'JMAX' , 'KMAX' ]:
952+ fm [side ][key ] = fm [side ][key ] // gcd_to_use
953+
954+ verified = list ()
955+ mismatched = list ()
956+
957+ for idx in range (len (scaled_matches )):
958+ fm = scaled_matches [idx ]
959+ b1 = fm ['block1' ]
960+ b2 = fm ['block2' ]
961+ b1_idx = b1 ['block_index' ]
962+ b2_idx = b2 ['block_index' ]
963+ block2 = reduced_blocks [b2_idx ]
964+
965+ # Enumerate unique corners of block2's face
966+ I2 = [b2 ['IMIN' ], b2 ['IMAX' ]]
967+ J2 = [b2 ['JMIN' ], b2 ['JMAX' ]]
968+ K2 = [b2 ['KMIN' ], b2 ['KMAX' ]]
969+
970+ unique_corners = list ()
971+ seen = set ()
972+ for i in I2 :
973+ for j in J2 :
974+ for k in K2 :
975+ key = (i , j , k )
976+ if key not in seen :
977+ seen .add (key )
978+ unique_corners .append (key )
979+
980+ found = False
981+ best_d_lower = float ('inf' )
982+ best_d_upper = float ('inf' )
983+
984+ # Try +theta rotation first, then -theta
985+ for rotated_blocks in [rotated_blocks_pos , rotated_blocks_neg ]:
986+ if found :
987+ break
988+
989+ block1_rotated = rotated_blocks [b1_idx ]
990+
991+ # Block1 rotated diagonal coordinates
992+ x1_l = block1_rotated .X [b1 ['IMIN' ], b1 ['JMIN' ], b1 ['KMIN' ]]
993+ y1_l = block1_rotated .Y [b1 ['IMIN' ], b1 ['JMIN' ], b1 ['KMIN' ]]
994+ z1_l = block1_rotated .Z [b1 ['IMIN' ], b1 ['JMIN' ], b1 ['KMIN' ]]
995+
996+ x1_u = block1_rotated .X [b1 ['IMAX' ], b1 ['JMAX' ], b1 ['KMAX' ]]
997+ y1_u = block1_rotated .Y [b1 ['IMAX' ], b1 ['JMAX' ], b1 ['KMAX' ]]
998+ z1_u = block1_rotated .Z [b1 ['IMAX' ], b1 ['JMAX' ], b1 ['KMAX' ]]
999+
1000+ # Check stored diagonal first
1001+ x2_l = block2 .X [b2 ['IMIN' ], b2 ['JMIN' ], b2 ['KMIN' ]]
1002+ y2_l = block2 .Y [b2 ['IMIN' ], b2 ['JMIN' ], b2 ['KMIN' ]]
1003+ z2_l = block2 .Z [b2 ['IMIN' ], b2 ['JMIN' ], b2 ['KMIN' ]]
1004+
1005+ x2_u = block2 .X [b2 ['IMAX' ], b2 ['JMAX' ], b2 ['KMAX' ]]
1006+ y2_u = block2 .Y [b2 ['IMAX' ], b2 ['JMAX' ], b2 ['KMAX' ]]
1007+ z2_u = block2 .Z [b2 ['IMAX' ], b2 ['JMAX' ], b2 ['KMAX' ]]
1008+
1009+ dx = x2_l - x1_l ; dy = y2_l - y1_l ; dz = z2_l - z1_l
1010+ d_lower = math .sqrt (dx * dx + dy * dy + dz * dz )
1011+ dx = x2_u - x1_u ; dy = y2_u - y1_u ; dz = z2_u - z1_u
1012+ d_upper = math .sqrt (dx * dx + dy * dy + dz * dz )
1013+
1014+ if d_lower < best_d_lower :
1015+ best_d_lower = d_lower
1016+ if d_upper < best_d_upper :
1017+ best_d_upper = d_upper
1018+
1019+ if d_lower < tol and d_upper < tol :
1020+ verified .append (face_matches [idx ])
1021+ found = True
1022+ break
1023+
1024+ # Try all permutations of block2's corners
1025+ for corner_lower in unique_corners :
1026+ for corner_upper in unique_corners :
1027+ if corner_lower == corner_upper :
1028+ continue
1029+
1030+ il , jl , kl = corner_lower
1031+ iu , ju , ku = corner_upper
1032+
1033+ x2_l = block2 .X [il , jl , kl ]
1034+ y2_l = block2 .Y [il , jl , kl ]
1035+ z2_l = block2 .Z [il , jl , kl ]
1036+
1037+ x2_u = block2 .X [iu , ju , ku ]
1038+ y2_u = block2 .Y [iu , ju , ku ]
1039+ z2_u = block2 .Z [iu , ju , ku ]
1040+
1041+ dx = x2_l - x1_l ; dy = y2_l - y1_l ; dz = z2_l - z1_l
1042+ dl = math .sqrt (dx * dx + dy * dy + dz * dz )
1043+ dx = x2_u - x1_u ; dy = y2_u - y1_u ; dz = z2_u - z1_u
1044+ du = math .sqrt (dx * dx + dy * dy + dz * dz )
1045+
1046+ if dl < best_d_lower :
1047+ best_d_lower = dl
1048+ if du < best_d_upper :
1049+ best_d_upper = du
1050+
1051+ if dl < tol and du < tol :
1052+ corrected = deepcopy (face_matches [idx ])
1053+ corrected ['block2' ]['IMIN' ] = il * gcd_to_use
1054+ corrected ['block2' ]['JMIN' ] = jl * gcd_to_use
1055+ corrected ['block2' ]['KMIN' ] = kl * gcd_to_use
1056+ corrected ['block2' ]['IMAX' ] = iu * gcd_to_use
1057+ corrected ['block2' ]['JMAX' ] = ju * gcd_to_use
1058+ corrected ['block2' ]['KMAX' ] = ku * gcd_to_use
1059+ verified .append (corrected )
1060+ found = True
1061+ break
1062+ if found :
1063+ break
1064+
1065+ if not found :
1066+ orig = face_matches [idx ]
1067+ b1_orig = orig ['block1' ]
1068+ b2_orig = orig ['block2' ]
1069+ print (f"verify_periodicity: MISMATCH at face_match index { idx } " )
1070+ print (f" block1 (block_index={ b1_orig ['block_index' ]} ): "
1071+ f"lower=({ b1_orig ['IMIN' ]} ,{ b1_orig ['JMIN' ]} ,{ b1_orig ['KMIN' ]} ) "
1072+ f"upper=({ b1_orig ['IMAX' ]} ,{ b1_orig ['JMAX' ]} ,{ b1_orig ['KMAX' ]} )" )
1073+ print (f" block2 (block_index={ b2_orig ['block_index' ]} ): "
1074+ f"lower=({ b2_orig ['IMIN' ]} ,{ b2_orig ['JMIN' ]} ,{ b2_orig ['KMIN' ]} ) "
1075+ f"upper=({ b2_orig ['IMAX' ]} ,{ b2_orig ['JMAX' ]} ,{ b2_orig ['KMAX' ]} )" )
1076+ print (f" Closest rotated block1 corner dist to block2 lower: { best_d_lower :.6e} " )
1077+ print (f" Closest rotated block1 corner dist to block2 upper: { best_d_upper :.6e} " )
1078+ mismatched .append (face_matches [idx ])
1079+
1080+ return verified , mismatched
0 commit comments