1use crate::block::Block;
48use crate::block_face_functions::{reduce_blocks, rotate_block};
49use crate::face_record::{
50 FaceMatch, FaceRecord, Orientation, OrientationPlane, PERMUTATION_MATRICES,
51};
52use crate::rotational_periodicity::create_rotation_matrix;
53use crate::utils::compute_min_gcd;
54use crate::Float;
55
56pub fn extract_canonical_grid(
67 block: &Block,
68 rec: &FaceRecord,
69) -> Option<(Vec<(Float, Float, Float)>, usize, usize)> {
70 let (raw_lo, raw_hi) = rec.bounds();
71 let imax = [
72 block.imax.saturating_sub(1),
73 block.jmax.saturating_sub(1),
74 block.kmax.saturating_sub(1),
75 ];
76 let lo = [
77 raw_lo[0].min(imax[0]),
78 raw_lo[1].min(imax[1]),
79 raw_lo[2].min(imax[2]),
80 ];
81 let hi = [
82 raw_hi[0].min(imax[0]),
83 raw_hi[1].min(imax[1]),
84 raw_hi[2].min(imax[2]),
85 ];
86
87 let const_dim = rec.constant_axis()?;
88 let varying: Vec<usize> = (0..3).filter(|&d| d != const_dim).collect();
89 let d0 = varying[0]; let d1 = varying[1]; let nu = hi[d0] - lo[d0] + 1;
92 let nv = hi[d1] - lo[d1] + 1;
93
94 let mut grid = Vec::with_capacity(nu * nv);
95 for u in 0..nu {
96 for v in 0..nv {
97 let mut idx = [0usize; 3];
98 idx[const_dim] = lo[const_dim];
99 idx[d0] = lo[d0] + u;
100 idx[d1] = lo[d1] + v;
101 grid.push(block.xyz(idx[0], idx[1], idx[2]));
102 }
103 }
104
105 Some((grid, nu, nv))
106}
107
108pub fn apply_permutation(
118 grid: &[(Float, Float, Float)],
119 nu: usize,
120 nv: usize,
121 perm_idx: u8,
122) -> (Vec<(Float, Float, Float)>, usize, usize) {
123 let _mat = PERMUTATION_MATRICES[perm_idx as usize];
124
125 let u_rev = perm_idx & 1 != 0;
126 let v_rev = perm_idx & 2 != 0;
127 let swap = perm_idx & 4 != 0;
128
129 let (out_nu, out_nv) = if swap { (nv, nu) } else { (nu, nv) };
130
131 let mut result = Vec::with_capacity(out_nu * out_nv);
132 for ou in 0..out_nu {
133 for ov in 0..out_nv {
134 let (gu, gv) = if swap { (ov, ou) } else { (ou, ov) };
136 let gu = if u_rev { nu - 1 - gu } else { gu };
137 let gv = if v_rev { nv - 1 - gv } else { gv };
138 result.push(grid[gu * nv + gv]);
139 }
140 }
141
142 (result, out_nu, out_nv)
143}
144
145pub fn verify_match(
150 pts_a: &[(Float, Float, Float)],
151 pts_b: &[(Float, Float, Float)],
152 tol: Float,
153) -> bool {
154 if pts_a.len() != pts_b.len() {
155 return false;
156 }
157 let tol2 = tol * tol;
158 for (a, b) in pts_a.iter().zip(pts_b.iter()) {
159 let d2 = (a.0 - b.0).powi(2) + (a.1 - b.1).powi(2) + (a.2 - b.2).powi(2);
160 if d2 > tol2 {
161 return false;
162 }
163 }
164 true
165}
166
167fn max_point_distance(
171 pts_a: &[(Float, Float, Float)],
172 pts_b: &[(Float, Float, Float)],
173) -> Float {
174 if pts_a.len() != pts_b.len() {
175 return Float::MAX;
176 }
177 let mut max_d2: Float = 0.0;
178 for (a, b) in pts_a.iter().zip(pts_b.iter()) {
179 let d2 = (a.0 - b.0).powi(2) + (a.1 - b.1).powi(2) + (a.2 - b.2).powi(2);
180 if d2 > max_d2 {
181 max_d2 = d2;
182 }
183 }
184 max_d2.sqrt()
185}
186
187pub fn verify_partial_match(
196 grid_a: &[(Float, Float, Float)],
197 grid_b_permuted: &[(Float, Float, Float)],
198 tol: Float,
199) -> (usize, usize) {
200 let tol2 = tol * tol;
201 let mut count = 0;
202 for b in grid_b_permuted {
203 for a in grid_a {
204 let d2 = (a.0 - b.0).powi(2) + (a.1 - b.1).powi(2) + (a.2 - b.2).powi(2);
205 if d2 <= tol2 {
206 count += 1;
207 break;
208 }
209 }
210 }
211 (count, grid_b_permuted.len())
212}
213
214pub fn determine_plane(rec_a: &FaceRecord, rec_b: &FaceRecord) -> OrientationPlane {
216 if rec_a.constant_axis() == rec_b.constant_axis() {
217 OrientationPlane::InPlane
218 } else {
219 OrientationPlane::CrossPlane
220 }
221}
222
223pub fn try_all_permutations(
234 grid_a: &[(Float, Float, Float)],
235 nu_a: usize,
236 nv_a: usize,
237 grid_b: &[(Float, Float, Float)],
238 nu_b: usize,
239 nv_b: usize,
240 tol: Float,
241) -> Option<u8> {
242 for perm_idx in 0u8..8 {
243 let (permuted, out_nu, out_nv) = apply_permutation(grid_b, nu_b, nv_b, perm_idx);
244
245 if out_nu != nu_a || out_nv != nv_a {
247 continue;
248 }
249
250 if verify_match(grid_a, &permuted, tol) {
251 return Some(perm_idx);
252 }
253 }
254 None
255}
256
257fn prepare_reduced(blocks: &[Block], face_matches: &[FaceMatch]) -> (Vec<Block>, Vec<FaceMatch>) {
263 let gcd_to_use = compute_min_gcd(blocks);
264 let reduced_blocks = reduce_blocks(blocks, gcd_to_use);
265 let scaled_matches: Vec<FaceMatch> = face_matches
266 .iter()
267 .map(|fm| {
268 let mut sfm = fm.clone();
269 sfm.divide_indices(gcd_to_use);
270 sfm
271 })
272 .collect();
273 (reduced_blocks, scaled_matches)
274}
275
276pub fn verify_connectivity(
286 blocks: &[Block],
287 face_matches: &[FaceMatch],
288 tol: Float,
289) -> (Vec<FaceMatch>, Vec<FaceMatch>) {
290 let (reduced_blocks, scaled_matches) = prepare_reduced(blocks, face_matches);
291
292 let mut verified = Vec::new();
293 let mut mismatched = Vec::new();
294
295 for (idx, sfm) in scaled_matches.iter().enumerate() {
296 let b1 = &sfm.block1;
297 let b2 = &sfm.block2;
298 let b1_idx = b1.block_index;
299 let b2_idx = b2.block_index;
300
301 if b1_idx >= reduced_blocks.len() || b2_idx >= reduced_blocks.len() {
302 mismatched.push(face_matches[idx].clone());
303 continue;
304 }
305
306 let block1 = &reduced_blocks[b1_idx];
307 let block2 = &reduced_blocks[b2_idx];
308
309 let grid_a = match extract_canonical_grid(block1, b1) {
311 Some(g) => g,
312 None => {
313 mismatched.push(face_matches[idx].clone());
314 continue;
315 }
316 };
317 let grid_b = match extract_canonical_grid(block2, b2) {
318 Some(g) => g,
319 None => {
320 mismatched.push(face_matches[idx].clone());
321 continue;
322 }
323 };
324
325 let (pts_a, nu_a, nv_a) = grid_a;
326 let (pts_b, nu_b, nv_b) = grid_b;
327
328 let stored_perm = sfm.orientation.as_ref().map(|o| o.permutation_index);
330 if let Some(perm_idx) = stored_perm {
331 let (permuted, out_nu, out_nv) = apply_permutation(&pts_b, nu_b, nv_b, perm_idx);
332 if out_nu == nu_a && out_nv == nv_a && verify_match(&pts_a, &permuted, tol) {
333 verified.push(face_matches[idx].clone());
334 continue;
335 }
336 }
337
338 if let Some(perm_idx) = try_all_permutations(&pts_a, nu_a, nv_a, &pts_b, nu_b, nv_b, tol) {
340 let mut corrected = face_matches[idx].clone();
341 let plane = determine_plane(b1, b2);
342 corrected.orientation = Some(Orientation {
343 permutation_index: perm_idx,
344 plane,
345 });
346 verified.push(corrected);
347 } else {
348 if std::env::var("PLOT3D_RS_VERIFY_CONNECTIVITY_VERBOSE").as_deref() == Ok("1") {
355 let orig = &face_matches[idx];
356 let ca1 = b1.constant_axis();
357 let ca2 = b2.constant_axis();
358 let axis_label = |a: Option<usize>| match a {
359 Some(0) => "I", Some(1) => "J", Some(2) => "K", _ => "?"
360 };
361 let cross_tag = if ca1 != ca2 { "CROSS-AXIS" } else { "SAME-AXIS" };
362 let mut best_dist: Float = Float::MAX;
363 for p in 0u8..8 {
364 let (permuted, out_nu, out_nv) = apply_permutation(&pts_b, nu_b, nv_b, p);
365 if out_nu != nu_a || out_nv != nv_a { continue; }
366 let d = max_point_distance(&pts_a, &permuted);
367 if d < best_dist { best_dist = d; }
368 }
369 eprintln!("verify_connectivity: MISMATCH at index {} [{}]", idx, cross_tag);
370 eprintln!(
371 " block {}: lo=({},{},{}) hi=({},{},{}) const={}",
372 orig.block1.block_index,
373 orig.block1.i_lo(), orig.block1.j_lo(), orig.block1.k_lo(),
374 orig.block1.i_hi(), orig.block1.j_hi(), orig.block1.k_hi(),
375 axis_label(ca1)
376 );
377 eprintln!(
378 " block {}: lo=({},{},{}) hi=({},{},{}) const={}",
379 orig.block2.block_index,
380 orig.block2.i_lo(), orig.block2.j_lo(), orig.block2.k_lo(),
381 orig.block2.i_hi(), orig.block2.j_hi(), orig.block2.k_hi(),
382 axis_label(ca2)
383 );
384 eprintln!(" grid_a: {}x{}, grid_b: {}x{}, best_dist: {:.6e}", nu_a, nv_a, nu_b, nv_b, best_dist);
385 }
386 mismatched.push(face_matches[idx].clone());
387 }
388 }
389
390 (verified, mismatched)
391}
392
393pub fn verify_periodicity(
404 blocks: &[Block],
405 face_matches: &[FaceMatch],
406 theta: Float,
407 rotation_axis: char,
408 tol: Float,
409) -> (Vec<FaceMatch>, Vec<FaceMatch>) {
410 let (reduced_blocks, scaled_matches) = prepare_reduced(blocks, face_matches);
411
412 let rotation_matrix_pos = create_rotation_matrix(theta, rotation_axis);
413 let rotation_matrix_neg = create_rotation_matrix(-theta, rotation_axis);
414
415 let rotated_blocks_pos: Vec<Block> = reduced_blocks
416 .iter()
417 .map(|b| rotate_block(b, rotation_matrix_pos))
418 .collect();
419 let rotated_blocks_neg: Vec<Block> = reduced_blocks
420 .iter()
421 .map(|b| rotate_block(b, rotation_matrix_neg))
422 .collect();
423
424 let mut verified = Vec::new();
425 let mut mismatched = Vec::new();
426
427 for (idx, sfm) in scaled_matches.iter().enumerate() {
428 let b1 = &sfm.block1;
429 let b2 = &sfm.block2;
430 let b1_idx = b1.block_index;
431 let b2_idx = b2.block_index;
432
433 if b1_idx >= reduced_blocks.len() || b2_idx >= reduced_blocks.len() {
434 mismatched.push(face_matches[idx].clone());
435 continue;
436 }
437
438 let block2 = &reduced_blocks[b2_idx];
439
440 let grid_b = match extract_canonical_grid(block2, b2) {
442 Some(g) => g,
443 None => {
444 mismatched.push(face_matches[idx].clone());
445 continue;
446 }
447 };
448 let (pts_b, nu_b, nv_b) = grid_b;
449
450 let mut found = false;
451 let mut best_dist: Float = Float::MAX;
452 let mut best_dims: Option<(usize, usize, usize, usize)> = None;
453
454 for rotated_blocks in [&rotated_blocks_pos, &rotated_blocks_neg] {
456 if found {
457 break;
458 }
459
460 let block1_rotated = &rotated_blocks[b1_idx];
461
462 let grid_a = match extract_canonical_grid(block1_rotated, b1) {
464 Some(g) => g,
465 None => continue,
466 };
467 let (pts_a, nu_a, nv_a) = grid_a;
468
469 if best_dims.is_none() {
471 best_dims = Some((nu_a, nv_a, nu_b, nv_b));
472 }
473
474 let stored_perm = sfm.orientation.as_ref().map(|o| o.permutation_index);
476 if let Some(perm_idx) = stored_perm {
477 let (permuted, out_nu, out_nv) = apply_permutation(&pts_b, nu_b, nv_b, perm_idx);
478 if out_nu == nu_a && out_nv == nv_a && verify_match(&pts_a, &permuted, tol) {
479 verified.push(face_matches[idx].clone());
480 found = true;
481 break;
482 }
483 }
484
485 if let Some(perm_idx) =
487 try_all_permutations(&pts_a, nu_a, nv_a, &pts_b, nu_b, nv_b, tol)
488 {
489 let mut corrected = face_matches[idx].clone();
490 let plane = determine_plane(b1, b2);
491 corrected.orientation = Some(Orientation {
492 permutation_index: perm_idx,
493 plane,
494 });
495 verified.push(corrected);
496 found = true;
497 break;
498 }
499
500 for p in 0u8..8 {
502 let (permuted, out_nu, out_nv) = apply_permutation(&pts_b, nu_b, nv_b, p);
503 if out_nu != nu_a || out_nv != nv_a { continue; }
504 let d = max_point_distance(&pts_a, &permuted);
505 if d < best_dist { best_dist = d; }
506 }
507 }
508
509 if !found {
510 if std::env::var("PLOT3D_RS_VERIFY_PERIODICITY_VERBOSE").as_deref() == Ok("1") {
513 let orig = &face_matches[idx];
514 let ca1 = b1.constant_axis();
515 let ca2 = b2.constant_axis();
516 let axis_label = |a: Option<usize>| match a {
517 Some(0) => "I", Some(1) => "J", Some(2) => "K", _ => "?"
518 };
519 let cross_tag = if ca1 != ca2 { "CROSS-AXIS" } else { "SAME-AXIS" };
520 eprintln!("verify_periodicity: MISMATCH at index {} [{}]", idx, cross_tag);
521 eprintln!(
522 " block {}: lo=({},{},{}) hi=({},{},{}) const={}",
523 orig.block1.block_index,
524 orig.block1.i_lo(), orig.block1.j_lo(), orig.block1.k_lo(),
525 orig.block1.i_hi(), orig.block1.j_hi(), orig.block1.k_hi(),
526 axis_label(ca1)
527 );
528 eprintln!(
529 " block {}: lo=({},{},{}) hi=({},{},{}) const={}",
530 orig.block2.block_index,
531 orig.block2.i_lo(), orig.block2.j_lo(), orig.block2.k_lo(),
532 orig.block2.i_hi(), orig.block2.j_hi(), orig.block2.k_hi(),
533 axis_label(ca2)
534 );
535 if let Some((nua, nva, nub, nvb)) = best_dims {
536 eprintln!(" grid_a: {}x{}, grid_b: {}x{}, best_dist: {:.6e}", nua, nva, nub, nvb, best_dist);
537 }
538 }
539 let _ = best_dist; let _ = best_dims;
540 mismatched.push(face_matches[idx].clone());
541 }
542 }
543
544 (verified, mismatched)
545}
546
547pub fn verify_translational_periodicity(
595 blocks: &[Block],
596 face_matches: &[FaceMatch],
597 delta: Option<Float>,
598 axis: char,
599 tol: Float,
600) -> (Vec<FaceMatch>, Vec<FaceMatch>) {
601 let (reduced_blocks, scaled_matches) = prepare_reduced(blocks, face_matches);
602
603 let axis_idx = match axis {
604 'x' | 'X' => 0usize,
605 'y' | 'Y' => 1usize,
606 'z' | 'Z' => 2usize,
607 _ => panic!("verify_translational_periodicity: invalid axis {:?}", axis),
608 };
609
610 let face_axis_centroid = |block: &Block, rec: &FaceRecord| -> Float {
616 let (il, jh, kl) = (rec.i_lo(), rec.j_lo(), rec.k_lo());
620 let (ih, jl, kh) = (rec.i_hi(), rec.j_hi(), rec.k_hi());
621 let (i0, i1) = if il <= ih { (il, ih) } else { (ih, il) };
624 let (j0, j1) = if jl <= jh { (jl, jh) } else { (jh, jl) };
625 let (k0, k1) = if kl <= kh { (kl, kh) } else { (kh, kl) };
626 let mut sum: Float = 0.0;
627 let mut n: usize = 0;
628 for k in k0..=k1 {
629 for j in j0..=j1 {
630 for i in i0..=i1 {
631 let (x, y, z) = block.xyz(i, j, k);
632 let v = match axis_idx {
633 0 => x,
634 1 => y,
635 _ => z,
636 };
637 sum += v;
638 n += 1;
639 }
640 }
641 }
642 sum / (n.max(1) as Float)
643 };
644
645 let mut verified = Vec::new();
646 let mut mismatched = Vec::new();
647
648 for (idx, sfm) in scaled_matches.iter().enumerate() {
649 let b1 = &sfm.block1;
650 let b2 = &sfm.block2;
651 let b1_idx = b1.block_index;
652 let b2_idx = b2.block_index;
653
654 if b1_idx >= reduced_blocks.len() || b2_idx >= reduced_blocks.len() {
655 mismatched.push(face_matches[idx].clone());
656 continue;
657 }
658
659 let block1 = &reduced_blocks[b1_idx];
660 let block2 = &reduced_blocks[b2_idx];
661
662 let delta_axis = match delta {
668 Some(d) => d,
669 None => {
670 let c1 = face_axis_centroid(block1, b1);
671 let c2 = face_axis_centroid(block2, b2);
672 (c2 - c1).abs()
676 }
677 };
678 if delta_axis.abs() < tol {
682 mismatched.push(face_matches[idx].clone());
683 continue;
684 }
685 let block1_shifted_pos = block1.shifted(delta_axis, axis);
686 let block1_shifted_neg = block1.shifted(-delta_axis, axis);
687
688 let grid_b = match extract_canonical_grid(block2, b2) {
690 Some(g) => g,
691 None => {
692 mismatched.push(face_matches[idx].clone());
693 continue;
694 }
695 };
696 let (pts_b, nu_b, nv_b) = grid_b;
697
698 let mut found = false;
699 let mut best_dist: Float = Float::MAX;
700 let mut best_dims: Option<(usize, usize, usize, usize)> = None;
701
702 for block1_shifted in [&block1_shifted_pos, &block1_shifted_neg] {
704 if found {
705 break;
706 }
707
708
709 let grid_a = match extract_canonical_grid(block1_shifted, b1) {
710 Some(g) => g,
711 None => continue,
712 };
713 let (pts_a, nu_a, nv_a) = grid_a;
714
715 if best_dims.is_none() {
716 best_dims = Some((nu_a, nv_a, nu_b, nv_b));
717 }
718
719 let stored_perm = sfm.orientation.as_ref().map(|o| o.permutation_index);
722 if let Some(perm_idx) = stored_perm {
723 let (permuted, out_nu, out_nv) =
724 apply_permutation(&pts_b, nu_b, nv_b, perm_idx);
725 if out_nu == nu_a && out_nv == nv_a && verify_match(&pts_a, &permuted, tol) {
726 verified.push(face_matches[idx].clone());
727 found = true;
728 break;
729 }
730 }
731
732 if let Some(perm_idx) =
734 try_all_permutations(&pts_a, nu_a, nv_a, &pts_b, nu_b, nv_b, tol)
735 {
736 let mut corrected = face_matches[idx].clone();
737 let plane = determine_plane(b1, b2);
738 corrected.orientation = Some(Orientation {
739 permutation_index: perm_idx,
740 plane,
741 });
742 verified.push(corrected);
743 found = true;
744 break;
745 }
746
747 for p in 0u8..8 {
749 let (permuted, out_nu, out_nv) =
750 apply_permutation(&pts_b, nu_b, nv_b, p);
751 if out_nu != nu_a || out_nv != nv_a {
752 continue;
753 }
754 let d = max_point_distance(&pts_a, &permuted);
755 if d < best_dist {
756 best_dist = d;
757 }
758 }
759 }
760
761 if !found {
762 if std::env::var("PLOT3D_RS_VERIFY_TRANSLATIONAL_VERBOSE").as_deref() == Ok("1") {
767 let orig = &face_matches[idx];
768 let ca1 = b1.constant_axis();
769 let ca2 = b2.constant_axis();
770 let axis_label = |a: Option<usize>| match a {
771 Some(0) => "I",
772 Some(1) => "J",
773 Some(2) => "K",
774 _ => "?",
775 };
776 let cross_tag = if ca1 != ca2 { "CROSS-AXIS" } else { "SAME-AXIS" };
777 eprintln!(
778 "verify_translational_periodicity[{}, Δ_per_match={:+.3e}]: \
779 MISMATCH at index {} [{}]",
780 axis, delta_axis, idx, cross_tag,
781 );
782 eprintln!(
783 " block {}: lo=({},{},{}) hi=({},{},{}) const={}",
784 orig.block1.block_index,
785 orig.block1.i_lo(), orig.block1.j_lo(), orig.block1.k_lo(),
786 orig.block1.i_hi(), orig.block1.j_hi(), orig.block1.k_hi(),
787 axis_label(ca1),
788 );
789 eprintln!(
790 " block {}: lo=({},{},{}) hi=({},{},{}) const={}",
791 orig.block2.block_index,
792 orig.block2.i_lo(), orig.block2.j_lo(), orig.block2.k_lo(),
793 orig.block2.i_hi(), orig.block2.j_hi(), orig.block2.k_hi(),
794 axis_label(ca2),
795 );
796 if let Some((nua, nva, nub, nvb)) = best_dims {
797 eprintln!(
798 " grid_a: {}x{}, grid_b: {}x{}, best_dist: {:.6e}",
799 nua, nva, nub, nvb, best_dist,
800 );
801 }
802 }
803 let _ = best_dims;
805 mismatched.push(face_matches[idx].clone());
806 }
807 }
808
809 (verified, mismatched)
810}