modflowpy / flopy

A Python package to create, run, and post-process MODFLOW-based models.
https://flopy.readthedocs.io
Other
506 stars 306 forks source link

bug: `flopy.utils.cvfdutil.gridlist_to_disv_gridprops` not working for nested grids with arbitrary levels of refinement #2263

Closed dbrakenhoff closed 1 month ago

dbrakenhoff commented 1 month ago

Describe the bug Creating a nested grid using flopy.utils.cvfdutil.gridlist_to_disv_gridprops does not work correctly when larger cells are connected to 4 or more smaller cells. In that situation vertices are left out of the larger cells, causing the hydraulic connection between the "non-refined" cells and the "refined" cells to be incorrect.

This is caused by the fact that the vertex-to-cell mapping isn't updated as vertices are added to larger cells. This works fine as long as each larger cell shares corner vertices with the smaller cells it is connected to. For a quadtree refinement this works fine, and one refinement level extra (3 cells) is also fine, as the 3rd cell is implicitly added by handling the two smaller cells that share corner vertices with the larger cell.

As an example where this doesn't work, consider the following grid (a 1 to 5 cell refinement): image

The algorithm does the following:

Cell 11 ends up missing vertices on the shared face between the outer and inner grids: image

To Fix The simple fix that solved my particular case was to update the vertex-to-cell mapping as vertices are added to the larger cell. Not sure if this is sufficiently robust for all cases, but perhaps it's a good start for supporting arbitrary refinement.

Update function segment_face to accept vertex_cell_dict and icell1 so it updates the vertex-cell mapping when vertices are added to a cell. See the single line addition at the bottom:

def segment_face(ivert, ivlist1, ivlist2, vertices, vertex_cell_dict, icell1):
    # go through ivlist1 and find faces that have ivert
    faces_to_check = []
    for ipos in range(len(ivlist1) - 1):
        face = (ivlist1[ipos], ivlist1[ipos + 1])
        if ivert in face:
            faces_to_check.append(face)

    # go through ivlist2 and find points to check
    points_to_check = []
    for ipos in range(len(ivlist2) - 1):
        if ivlist2[ipos] == ivert:
            points_to_check.append(ivlist2[ipos + 1])
        elif ivlist2[ipos + 1] == ivert:
            points_to_check.append(ivlist2[ipos])

    for face in faces_to_check:
        iva, ivb = face
        x, y = vertices[iva]
        a = Point(x, y)
        x, y = vertices[ivb]
        b = Point(x, y)
        for ivc in points_to_check:
            if ivc not in face:
                x, y = vertices[ivc]
                c = Point(x, y)
                if isBetween(a, b, c):
                    ipos = ivlist1.index(ivb)
                    if ipos == 0:
                        ipos = len(ivlist1) - 1
                    ivlist1.insert(ipos, ivc)
                    vertex_cell_dict[ivc].append(icell1)  # <-- update vertex-cell mapping
                    return True

    return False

Then in to_cvfd() (L244-245) pass those arguments to segment_face

                        # don't share a face, so need to segment if necessary
                        segmented = segment_face(
                            ivert,
                            ivertlist1,
                            ivertlist2,
                            vertexdict_keys,
                            vertex_cell_dict,  # pass vertex_cell_dict to update cells using added vertices
                            icell1,  # pass cell number to update vertex_cell_dict
                        )

To Reproduce Steps to reproduce the behavior, see notebook: nested_mf6_grids.zip

Expected behavior It would be nice if arbitrary levels of refinement were supported by this utility function.

langevin-usgs commented 1 month ago

Excellent, @dbrakenhoff. Good find. Thanks for reporting this. Feel free to submit a PR or I can try to take a look the week after next.