Skip to content

bug: ParticleData.to_prp() returns global coordinates #2395

@dbrakenhoff

Description

@dbrakenhoff

Describe the bug
Not sure if this can yet be qualified as bug, or is simply still under active development and I'm a bit early in posting this, but I noticed that ParticleData.to_prp() returns global coordinates. PRT6 currently expects model coordinates for the input. This isn't caught in the tests as as far as I could see, as no PRT6 models are run using a grid with offsets (in both flopy and modflow6 repos).

I saw other options for specifying model coordinates are under review in Modflow6, which might also solve this issue by allowing computation with global coordinates: MODFLOW-ORG/modflow6#1901. Nevertheless, it might be nice to have both global and model coordinate options in to_prp().

EDIT: just noticed issue #2388. If vertices are specified in model coordinates, that would also solve this issue.

To Reproduce

import flopy as fp
import numpy as np

gr = fp.discretization.StructuredGrid(
    10 * np.ones(10),
    10 * np.ones(10),
    top=np.ones((10, 10)),
    botm=np.zeros((1, 10, 10)),
    nlay=1,
    xoff=100,
    yoff=300,
)
pd = fp.modpath.ParticleData([(0, 0, 0), (0, 5, 5), (0, 9, 9)], structured=True)
list(pd.to_prp(gr))

Expected behavior
Perhaps the option of returning global or model coordinates could be added to to_prp()? Something like the code below could do the trick. I used the term model_coords since in modpath local signifies a location within a cell.

If this is something you would want to support, Id be happy to submit a PR with this small change, let me know!

# class ParticleData:
# ...
    def to_coords(self, grid, localz=False, model_coords=True) -> Iterator[tuple]:  # <-- add model_coords=True
        # ...
        def cvt_xy(p, vs):
            mn, mx = min(vs), max(vs)
            span = mx - mn
            return mn + span * p

        if grid.grid_type == "structured":
            if not hasattr(self.particledata, "k"):
                raise ValueError(
                    "Particle representation is not structured but grid is"
                )

            def cvt_z(p, k, i, j):
                mn, mx = (
                    grid.botm[k, i, j],
                    grid.top[i, j] if k == 0 else grid.botm[k - 1, i, j],
                )
                span = mx - mn
                return mn + span * p

            def convert(row, model_coords=True) -> tuple[float, float, float]:  # <-- add model_coords
                verts = grid.get_cell_vertices(row.i, row.j)
                if model_coords:
                    xs, ys = grid.get_local_coords(*np.array(verts).T)  # <--convert global coords to model coords
                else:
                    xs, ys = list(zip(*verts))
                return [
                    cvt_xy(row.localx, xs),
                    cvt_xy(row.localy, ys),
                    row.localz if localz else cvt_z(row.localz, row.k, row.i, row.j),
                ]

        else:
            if hasattr(self.particledata, "k"):
                raise ValueError(
                    "Particle representation is structured but grid is not"
                )

            def cvt_z(p, nn):
                k, j = grid.get_lni([nn])[0]
                mn, mx = (
                    grid.botm[k, j],
                    grid.top[j] if k == 0 else grid.botm[k - 1, j],
                )
                span = mx - mn
                return mn + span * p

            def convert(row, model_coords=True) -> tuple[float, float, float]:  # <-- add model_coords
                verts = grid.get_cell_vertices(row.node)
                if model_coords:
                    xs, ys = grid.get_local_coords(*np.array(verts).T)  # <--convert global coords to model coords
                else:
                    xs, ys = list(zip(*verts))
                return [
                    cvt_xy(row.localx, xs),
                    cvt_xy(row.localy, ys),
                    row.localz if localz else cvt_z(row.localz, row.node),
                ]

        for t in self.particledata.itertuples():
            yield convert(t, model_coords=model_coords)

    def to_prp(self, grid, localz=False, model_coords=True) -> Iterator[tuple]:  # <-- add model_coords
        # ...
        for i, (t, c) in enumerate(
            zip(
                self.particledata.itertuples(index=False),
                self.to_coords(grid, localz, model_coords=model_coords),  # <-- pass model_coords
            )
        ):
            row = [i]  # release point index (irpt)
            if "node" in self.particledata:
                k, j = grid.get_lni([t.node])[0]
                row.extend([(k, j)])
            else:
                row.extend([t.k, t.i, t.j])
            row.extend(c)
            yield tuple(row)

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions