22import pytest
33
44from flopy .discretization import StructuredGrid
5+ from flopy .utils .cvfdutil import get_disv_gridprops , gridlist_to_verts
56from flopy .utils .lgrutil import Lgr , LgrToDisv , SimpleRegularGrid
67
78
@@ -192,7 +193,7 @@ def test_lgr_hanging_vertices():
192193 assert "top" in gridprops
193194 assert "botm" in gridprops
194195 assert gridprops ["ncpl" ] == 17
195- assert gridprops ["nvert" ] == 32
196+ assert gridprops ["nvert" ] == 28
196197 assert gridprops ["nlay" ] == 3
197198
198199 # test the lgr to disv class
@@ -204,10 +205,11 @@ def test_lgr_hanging_vertices():
204205 assert lgrtodisv .back_face_hanging [2 , 1 ] == [12 , 13 , 14 , 15 ]
205206 assert lgrtodisv .front_face_hanging [0 , 1 ] == [0 , 1 , 2 , 3 ]
206207
207- assert lgrtodisv .iverts [1 ] == [1 , 2 , 6 , 18 , 17 , 5 ]
208- assert lgrtodisv .iverts [3 ] == [4 , 5 , 20 , 24 , 9 , 8 ]
209- assert lgrtodisv .iverts [4 ] == [6 , 7 , 11 , 10 , 27 , 23 ]
210- assert lgrtodisv .iverts [6 ] == [9 , 29 , 30 , 10 , 14 , 13 ]
208+ # These cells have hanging vertices at parent-child boundaries
209+ assert lgrtodisv .iverts [1 ] == [1 , 2 , 6 , 17 , 16 , 5 ] # top-center parent cell
210+ assert lgrtodisv .iverts [3 ] == [4 , 5 , 18 , 22 , 9 , 8 ] # middle-left parent cell
211+ assert lgrtodisv .iverts [4 ] == [6 , 7 , 11 , 10 , 25 , 21 ] # middle-right parent cell
212+ assert lgrtodisv .iverts [6 ] == [9 , 26 , 27 , 10 , 14 , 13 ] # bottom-left parent cell
211213
212214 assert np .allclose (gridprops ["top" ], dz * np .ones ((17 ,)))
213215
@@ -459,3 +461,347 @@ def test_simple_regular_grid_deprecation():
459461 # Verify backward compatibility attributes
460462 assert grid .xorigin == xorigin
461463 assert grid .yorigin == yorigin
464+
465+
466+ def test_lgr_matches_legacy_gridlist_to_disv_gridprops ():
467+ """
468+ Lgr.to_disv_gridprops() output should match
469+ legacy gridlist_to_disv_gridprops() output.
470+ """
471+ # TODO: this will need to be removed soon, and the underlying
472+ # implementation inlined, when we remove this function.
473+ from flopy .utils .cvfdutil import gridlist_to_disv_gridprops
474+
475+ # 7x7 parent grid with 3x3 refinement in center
476+ nlay = 1
477+ nrow_p , ncol_p = 7 , 7
478+ delr_p = delc_p = 100.0 * np .ones (7 )
479+ top_p = np .zeros ((nrow_p , ncol_p ))
480+ botm_p = - 100.0 * np .ones ((nlay , nrow_p , ncol_p ))
481+
482+ # legacy approach
483+ idomain_p = np .ones ((nlay , nrow_p , ncol_p ), dtype = int )
484+ idomain_p [:, 2 :5 , 2 :5 ] = 0 # Center 3x3 inactive
485+ sg_parent_legacy = StructuredGrid (
486+ delc = delc_p ,
487+ delr = delr_p ,
488+ top = top_p ,
489+ botm = botm_p ,
490+ idomain = idomain_p ,
491+ xoff = 0.0 ,
492+ yoff = 0.0 ,
493+ )
494+ nrow_c , ncol_c = 9 , 9
495+ delr_c = delc_c = 100.0 / 3.0 * np .ones (9 )
496+ top_c = np .zeros ((nrow_c , ncol_c ))
497+ botm_c = - 100.0 * np .ones ((nlay , nrow_c , ncol_c ))
498+ idomain_c = np .ones ((nlay , nrow_c , ncol_c ), dtype = int )
499+ sg_child = StructuredGrid (
500+ delc = delc_c ,
501+ delr = delr_c ,
502+ top = top_c ,
503+ botm = botm_c ,
504+ idomain = idomain_c ,
505+ xoff = 200.0 ,
506+ yoff = 200.0 ,
507+ )
508+ gridprops_legacy = gridlist_to_disv_gridprops ([sg_parent_legacy , sg_child ])
509+
510+ # lgr approach
511+ parent_grid = StructuredGrid (
512+ delc = delc_p ,
513+ delr = delr_p ,
514+ top = top_p ,
515+ botm = botm_p ,
516+ xoff = 0.0 ,
517+ yoff = 0.0 ,
518+ )
519+ refine_mask = np .ones ((nlay , nrow_p , ncol_p ))
520+ refine_mask [:, 2 :5 , 2 :5 ] = 0
521+ lgr = Lgr .from_parent_grid (parent_grid , refine_mask , ncpp = 3 )
522+ gridprops_lgr = lgr .to_disv_gridprops ()
523+
524+ # Note: legacy gridlist_to_disv_gridprops doesn't return nlay, top, botm
525+ # Only check common keys
526+ assert gridprops_legacy ["ncpl" ] == gridprops_lgr ["ncpl" ]
527+ assert gridprops_legacy ["nvert" ] == gridprops_lgr ["nvert" ]
528+
529+ # Verify vertex positions match (may be in different order)
530+ verts_legacy_set = {
531+ (round (v [1 ], 9 ), round (v [2 ], 9 )) for v in gridprops_legacy ["vertices" ]
532+ }
533+ verts_lgr_set = {
534+ (round (v [1 ], 9 ), round (v [2 ], 9 )) for v in gridprops_lgr ["vertices" ]
535+ }
536+ assert verts_legacy_set == verts_lgr_set
537+
538+ for icpl in range (gridprops_legacy ["ncpl" ]):
539+ nverts_legacy = gridprops_legacy ["cell2d" ][icpl ][3 ]
540+ nverts_lgr = gridprops_lgr ["cell2d" ][icpl ][3 ]
541+ assert nverts_legacy >= 3
542+ assert nverts_lgr >= 3
543+
544+ for icpl in range (gridprops_legacy ["ncpl" ]):
545+ xc_legacy , yc_legacy = gridprops_legacy ["cell2d" ][icpl ][1 :3 ]
546+ xc_lgr , yc_lgr = gridprops_lgr ["cell2d" ][icpl ][1 :3 ]
547+ assert np .allclose ([xc_legacy , yc_legacy ], [xc_lgr , yc_lgr ])
548+
549+ # Note: top and botm are only in Lgr gridprops,
550+ # not in legacy gridlist_to_disv_gridprops.
551+ # Just verify they exist in the Lgr version
552+ assert "nlay" in gridprops_lgr
553+ assert "top" in gridprops_lgr
554+ assert "botm" in gridprops_lgr
555+ assert gridprops_lgr ["nlay" ] == nlay
556+ assert gridprops_lgr ["top" ].shape == (gridprops_lgr ["ncpl" ],)
557+ assert gridprops_lgr ["botm" ].shape == (nlay , gridprops_lgr ["ncpl" ])
558+
559+
560+ def test_hanging_vertex_algorithm_comparison ():
561+ """
562+ Compare the two hanging vertex placement algorithms in detail:
563+ 1. to_cvfd() - reactive geometric algorithm (legacy)
564+ 2. Lgr.find_hanging_vertices() - proactive structured algorithm (new)
565+
566+ This test creates a simple parent-child grid and analyzes the differences
567+ in vertex placement, connectivity, and grid structure. Intended mainly to
568+ be run manually to inspect the printed output.
569+ """
570+ # Create a simple 3x3 parent grid with 3x refinement in center cell
571+ nlay = 1
572+ nrow_p = 3
573+ ncol_p = 3
574+ delr_p = 100.0
575+ delc_p = 100.0
576+ top_p = np .zeros ((nrow_p , ncol_p ))
577+ botm_p = np .array ([np .full ((nrow_p , ncol_p ), - 10.0 )])
578+
579+ # Setup for both approaches
580+ # Legacy approach: use idomain to mark refinement region as inactive
581+ idomain_legacy = np .ones ((nlay , nrow_p , ncol_p ), dtype = int )
582+ idomain_legacy [:, 1 , 1 ] = 0 # Center cell inactive
583+
584+ sg_parent_legacy = StructuredGrid (
585+ delr = np .full (ncol_p , delr_p ),
586+ delc = np .full (nrow_p , delc_p ),
587+ top = top_p ,
588+ botm = botm_p ,
589+ idomain = idomain_legacy ,
590+ xoff = 0.0 ,
591+ yoff = 0.0 ,
592+ )
593+
594+ # Child grid at center position
595+ ncpp = 3
596+ top_c = np .zeros ((ncpp , ncpp ))
597+ botm_c = np .array ([np .full ((ncpp , ncpp ), - 10.0 )])
598+ idomain_child = np .ones ((nlay , ncpp , ncpp ), dtype = int )
599+ sg_child = StructuredGrid (
600+ delr = np .full (ncpp , delr_p / ncpp ),
601+ delc = np .full (ncpp , delc_p / ncpp ),
602+ top = top_c ,
603+ botm = botm_c ,
604+ idomain = idomain_child ,
605+ xoff = 100.0 ,
606+ yoff = 100.0 ,
607+ )
608+
609+ # New approach: use refine_mask
610+ sg_parent_new = StructuredGrid (
611+ delr = np .full (ncol_p , delr_p ),
612+ delc = np .full (nrow_p , delc_p ),
613+ top = top_p .copy (),
614+ botm = botm_p .copy (),
615+ xoff = 0.0 ,
616+ yoff = 0.0 ,
617+ )
618+ refine_mask = np .ones ((nlay , nrow_p , ncol_p ))
619+ refine_mask [:, 1 , 1 ] = 0
620+
621+ # Generate grids with both algorithms
622+ # Legacy: to_cvfd() algorithm
623+ verts_legacy , iverts_legacy = gridlist_to_verts ([sg_parent_legacy , sg_child ])
624+ gridprops_legacy = get_disv_gridprops (verts_legacy , iverts_legacy )
625+
626+ # New: Lgr.find_hanging_vertices() algorithm
627+ lgr = Lgr .from_parent_grid (sg_parent_new , refine_mask , ncpp = ncpp )
628+ gridprops_new = lgr .to_disv_gridprops ()
629+
630+ # Compare basic metrics
631+ print ("\n " + "=" * 80 )
632+ print ("HANGING VERTEX ALGORITHM COMPARISON" )
633+ print ("=" * 80 )
634+ print ("\n Grid Configuration:" )
635+ print (f" Parent: { nrow_p } x{ ncol_p } cells, { delr_p } x{ delc_p } spacing" )
636+ print (f" Child: { ncpp } x{ ncpp } refinement at center cell (1,1)" )
637+ print (
638+ f" Total cells: { nrow_p * ncol_p - 1 } "
639+ f"parent + { ncpp * ncpp } "
640+ f"child = { nrow_p * ncol_p - 1 + ncpp * ncpp } "
641+ )
642+
643+ print (
644+ f"\n { 'Metric' :<30} "
645+ f"{ 'Legacy (to_cvfd)' :<20} "
646+ f"{ 'New (Lgr)' :<20} "
647+ f"{ 'Difference' :<20} "
648+ )
649+ print ("-" * 90 )
650+ print (
651+ f"{ 'Total vertices' :<30} "
652+ f"{ gridprops_legacy ['nvert' ]:<20} "
653+ f"{ gridprops_new ['nvert' ]:<20} "
654+ f"{ gridprops_new ['nvert' ] - gridprops_legacy ['nvert' ]:<20} "
655+ )
656+ print (
657+ f"{ 'Total cells' :<30} "
658+ f"{ gridprops_legacy ['ncpl' ]:<20} "
659+ f"{ gridprops_new ['ncpl' ]:<20} "
660+ f"{ gridprops_new ['ncpl' ] - gridprops_legacy ['ncpl' ]:<20} "
661+ )
662+
663+ # Analyze vertex differences
664+ verts_legacy_dict = {
665+ i : (v [1 ], v [2 ]) for i , v in enumerate (gridprops_legacy ["vertices" ])
666+ }
667+ verts_new_dict = {i : (v [1 ], v [2 ]) for i , v in enumerate (gridprops_new ["vertices" ])}
668+
669+ # Find unique vertices in each approach
670+ verts_legacy_set = {
671+ (round (x , 6 ), round (y , 6 )) for x , y in verts_legacy_dict .values ()
672+ }
673+ verts_new_set = {(round (x , 6 ), round (y , 6 )) for x , y in verts_new_dict .values ()}
674+
675+ only_in_legacy = verts_legacy_set - verts_new_set
676+ only_in_new = verts_new_set - verts_legacy_set
677+ common = verts_legacy_set & verts_new_set
678+
679+ print (f"\n { 'Vertex Analysis' :<30} { 'Count' :<20} " )
680+ print ("-" * 50 )
681+ print (f"{ 'Common vertices' :<30} { len (common ):<20} " )
682+ print (f"{ 'Only in legacy' :<30} { len (only_in_legacy ):<20} " )
683+ print (f"{ 'Only in new' :<30} { len (only_in_new ):<20} " )
684+
685+ if only_in_legacy :
686+ print ("\n Vertices only in legacy (to_cvfd):" )
687+ for x , y in sorted (only_in_legacy ):
688+ print (f" ({ x :8.2f} , { y :8.2f} )" )
689+
690+ if only_in_new :
691+ print ("\n Vertices only in new (Lgr):" )
692+ for x , y in sorted (only_in_new ):
693+ print (f" ({ x :8.2f} , { y :8.2f} )" )
694+
695+ # Analyze cell vertex counts
696+ legacy_cell_nverts = [cell [3 ] for cell in gridprops_legacy ["cell2d" ]]
697+ new_cell_nverts = [cell [3 ] for cell in gridprops_new ["cell2d" ]]
698+
699+ print (f"\n { 'Cell Vertex Counts' :<30} { 'Legacy' :<20} { 'New' :<20} " )
700+ print ("-" * 70 )
701+ print (
702+ f"{ 'Min vertices per cell' :<30} "
703+ f"{ min (legacy_cell_nverts ):<20} "
704+ f"{ min (new_cell_nverts ):<20} "
705+ )
706+ print (
707+ f"{ 'Max vertices per cell' :<30} "
708+ f"{ max (legacy_cell_nverts ):<20} "
709+ f"{ max (new_cell_nverts ):<20} "
710+ )
711+ print (
712+ f"{ 'Avg vertices per cell' :<30} "
713+ f"{ np .mean (legacy_cell_nverts ):<20.2f} "
714+ f"{ np .mean (new_cell_nverts ):<20.2f} "
715+ )
716+
717+ # Find cells with different vertex counts
718+ print ("\n Cells with different vertex counts:" )
719+ diff_count = 0
720+ for icell in range (min (len (legacy_cell_nverts ), len (new_cell_nverts ))):
721+ if legacy_cell_nverts [icell ] != new_cell_nverts [icell ]:
722+ print (
723+ f" Cell { icell } : legacy={ legacy_cell_nverts [icell ]} , "
724+ f"new={ new_cell_nverts [icell ]} "
725+ )
726+ diff_count += 1
727+ if diff_count == 0 :
728+ print (" None - all cells have same vertex counts" )
729+
730+ # Identify hanging vertices by analyzing parent-child boundary cells
731+ print (f"\n { 'Hanging Vertex Analysis' :<50} " )
732+ print ("-" * 50 )
733+
734+ # For each parent cell at the boundary, check vertex counts
735+ print ("\n Parent boundary cells (adjacent to refinement):" )
736+ boundary_parents = [
737+ (0 , "top-left" ),
738+ (1 , "top-center" ),
739+ (2 , "top-right" ),
740+ (3 , "middle-left" ),
741+ (5 , "middle-right" ),
742+ (6 , "bottom-left" ),
743+ (7 , "bottom-center" ),
744+ (8 , "bottom-right" ),
745+ ]
746+
747+ for icell , label in boundary_parents :
748+ if icell < len (legacy_cell_nverts ):
749+ legacy_nv = legacy_cell_nverts [icell ]
750+ new_nv = new_cell_nverts [icell ]
751+ status = "DIFFERENT" if legacy_nv != new_nv else "same"
752+ print (
753+ f" Cell { icell } ({ label :15s} ): "
754+ f"legacy={ legacy_nv } , new={ new_nv } [{ status } ]"
755+ )
756+
757+ # Detailed vertex analysis - check for duplicates
758+ print (f"\n { 'Duplicate Vertex Analysis' :<50} " )
759+ print ("-" * 50 )
760+
761+ # Count occurrences of each coordinate in new approach
762+ new_coord_counts = {}
763+ for i , (x , y ) in verts_new_dict .items ():
764+ coord = (round (x , 6 ), round (y , 6 ))
765+ if coord not in new_coord_counts :
766+ new_coord_counts [coord ] = []
767+ new_coord_counts [coord ].append (i )
768+
769+ duplicates_new = {
770+ coord : indices
771+ for coord , indices in new_coord_counts .items ()
772+ if len (indices ) > 1
773+ }
774+ if duplicates_new :
775+ print (f"Found { len (duplicates_new )} duplicate coordinates in new approach:" )
776+ for coord , indices in sorted (duplicates_new .items ()):
777+ print (f" { coord } : vertex indices { indices } " )
778+ else :
779+ print ("No duplicate coordinates in new approach" )
780+
781+ # Count occurrences in legacy approach
782+ legacy_coord_counts = {}
783+ for i , (x , y ) in verts_legacy_dict .items ():
784+ coord = (round (x , 6 ), round (y , 6 ))
785+ if coord not in legacy_coord_counts :
786+ legacy_coord_counts [coord ] = []
787+ legacy_coord_counts [coord ].append (i )
788+
789+ duplicates_legacy = {
790+ coord : indices
791+ for coord , indices in legacy_coord_counts .items ()
792+ if len (indices ) > 1
793+ }
794+ if duplicates_legacy :
795+ print (
796+ f"Found { len (duplicates_legacy )} duplicate coordinates in legacy approach:"
797+ )
798+ for coord , indices in sorted (duplicates_legacy .items ()):
799+ print (f" { coord } : vertex indices { indices } " )
800+ else :
801+ print ("No duplicate coordinates in legacy approach" )
802+
803+ assert (
804+ gridprops_legacy ["nvert" ] == gridprops_new ["nvert" ]
805+ and len (only_in_legacy ) == 0
806+ and len (only_in_new ) == 0
807+ )
0 commit comments