diff --git a/DEVELOPER.md b/DEVELOPER.md index 4febe4902d..8dc8dee0a5 100644 --- a/DEVELOPER.md +++ b/DEVELOPER.md @@ -347,7 +347,7 @@ By default, `pytest-benchmark` will only print profiling results to `stdout`. If ## Branching model -This project follows the [git flow](https://nvie.com/posts/a-successful-git-branching-model/): development occurs on the `develop` branch, while `main` is reserved for the state of the latest release. Development PRs are typically squashed to `develop`, to avoid merge commits. At release time, release branches are merged to `main`, and then `main` is merged back into `develop`. +This project follows the [git flow](https://nvie.com/posts/a-successful-git-branching-model/): development occurs on the `develop` branch, while `master` is reserved for the state of the latest release. Development PRs are typically squashed to `develop`, to avoid merge commits. At release time, release branches are merged to `master`, and then `master` is merged back into `develop`. ## Deprecation policy diff --git a/autotest/test_plotutil.py b/autotest/test_plotutil.py index 66ca79a608..6307861683 100644 --- a/autotest/test_plotutil.py +++ b/autotest/test_plotutil.py @@ -3,69 +3,15 @@ import pytest from flopy.plot.plotutil import ( + MP7_ENDPOINT_DTYPE, + MP7_PATHLINE_DTYPE, + PRT_PATHLINE_DTYPE, to_mp7_endpoints, to_mp7_pathlines, to_prt_pathlines, ) -# test PRT-MP7 pathline conversion functions -# todo: define fields in a single location and reference from here -# todo: support optional grid parameter to conversion functions - - -prt_pl_cols = [] - - -mp7_pl_cols = [ - "particleid", - "particlegroup", - "sequencenumber", - "particleidloc", - "time", - "xloc", - "yloc", - "zloc", - "x", - "y", - "z", - "node", - "k", - "stressperiod", - "timestep", -] - - -mp7_ep_cols = [ - "particleid", - "particlegroup", - "particleidloc", - "time", - "time0", - "xloc", - "xloc0", - "yloc", - "yloc0", - "zloc", - "zloc0", - "x0", - "y0", - "z0", - "x", - "y", - "z", - "node", - "node0", - "k", - "k0", - "zone", - "zone0", - "initialcellface", - "cellface", - "status", -] - - -pls = pd.DataFrame.from_records( +PRT_TEST_PATHLINES = pd.DataFrame.from_records( [ [ 1, @@ -229,83 +175,283 @@ 0.5, "PRP000000001", ], + [ + 1, # kper + 1, # kstp + 1, # imdl + 1, # iprp + 1, # irpt + 1, # ilay + 100, # icell + 0, # izone + 5, # istatus + 3, # ireason + 0.0, # trelease + 42.728752, # t + 9.888968, # x + 1.0, # y + 0.5, # z + "PRP000000001", # name + ], + ], + columns=PRT_PATHLINE_DTYPE.fields.keys(), +) +MP7_TEST_PATHLINES = pd.DataFrame.from_records( + [ + [ + 1, # particleid + 1, # particlegroup + 1, # sequencenumber + 1, # particleidloc + 0.0, # time + 1.0, # x + 2.0, # y + 3.0, # z + 1, # k + 1, # node + 0.1, # xloc + 0.1, # yloc + 0.1, # zloc + 1, # stressperiod + 1, # timestep + ], [ 1, 1, 1, 1, - 1, - 1, - 100, - 0, - 5, - 3, - 0.0, - 42.728752, - 9.888968, - 1.0, - 0.5, - "PRP000000001", + 1.0, # time + 2.0, # x + 3.0, # y + 4.0, # z + 2, # k + 2, # node + 0.9, # xloc + 0.9, # yloc + 0.9, # zloc + 1, # stressperiod + 1, # timestep ], ], - columns=[ - "kper", - "kstp", - "imdl", - "iprp", - "irpt", - "ilay", - "icell", - "izone", - "istatus", - "ireason", - "trelease", - "t", - "x", - "y", - "z", - "name", + columns=MP7_PATHLINE_DTYPE.fields.keys(), +) +MP7_TEST_ENDPOINTS = pd.DataFrame.from_records( + [ + [ + 1, # particleid + 1, # particlegroup + 1, # particleidloc + 2, # status (terminated at boundary face) + 0.0, # time0 + 1.0, # time + 1, # node0 + 1, # k0 + 0.1, # xloc0 + 0.1, # yloc0 + 0.1, # zloc0 + 0.0, # x0 + 1.0, # y0 + 2.0, # z0 + 1, # zone0 + 1, # initialcellface + 5, # node + 2, # k + 0.9, # xloc + 0.9, # yloc + 0.9, # zloc + 10.0, # x + 11.0, # y + 12.0, # z + 2, # zone + 2, # cellface + ], + [ + 2, # particleid + 1, # particlegroup + 2, # particleidloc + 2, # status (terminated at boundary face) + 0.0, # time0 + 2.0, # time + 1, # node0 + 1, # k0 + 0.1, # xloc0 + 0.1, # yloc0 + 0.1, # zloc0 + 0.0, # x0 + 1.0, # y0 + 2.0, # z0 + 1, # zone0 + 1, # initialcellface + 5, # node + 2, # k + 0.9, # xloc + 0.9, # yloc + 0.9, # zloc + 10.0, # x + 11.0, # y + 12.0, # z + 2, # zone + 2, # cellface + ], + [ + 3, # particleid + 1, # particlegroup + 3, # particleidloc + 2, # status (terminated at boundary face) + 0.0, # time0 + 3.0, # time + 1, # node0 + 1, # k0 + 0.1, # xloc0 + 0.1, # yloc0 + 0.1, # zloc0 + 0.0, # x0 + 1.0, # y0 + 2.0, # z0 + 1, # zone0 + 1, # initialcellface + 5, # node + 2, # k + 0.9, # xloc + 0.9, # yloc + 0.9, # zloc + 10.0, # x + 11.0, # y + 12.0, # z + 2, # zone + 2, # cellface + ], ], + columns=MP7_ENDPOINT_DTYPE.fields.keys(), ) @pytest.mark.parametrize("dataframe", [True, False]) def test_to_mp7_pathlines(dataframe): - inp_pls = pls if dataframe else pls.to_records(index=False) - mp7_pls = to_mp7_pathlines(inp_pls) + prt_pls = ( + PRT_TEST_PATHLINES + if dataframe + else PRT_TEST_PATHLINES.to_records(index=False) + ) + mp7_pls = to_mp7_pathlines(prt_pls) assert ( - type(inp_pls) + type(prt_pls) == type(mp7_pls) == (pd.DataFrame if dataframe else np.recarray) ) assert len(mp7_pls) == 10 assert set( dict(mp7_pls.dtypes).keys() if dataframe else mp7_pls.dtype.names - ) == set(mp7_pl_cols) + ) == set(MP7_PATHLINE_DTYPE.fields.keys()) + + +@pytest.mark.parametrize("dataframe", [True, False]) +def test_to_mp7_pathlines_empty(dataframe): + mp7_pls = to_mp7_pathlines( + pd.DataFrame.from_records([], columns=PRT_PATHLINE_DTYPE.fields.keys()) + if dataframe + else np.recarray((0,), dtype=PRT_PATHLINE_DTYPE) + ) + assert mp7_pls.empty if dataframe else mp7_pls.size == 0 + if dataframe: + mp7_pls = mp7_pls.to_records(index=False) + assert mp7_pls.dtype == MP7_PATHLINE_DTYPE + + +@pytest.mark.parametrize("dataframe", [True, False]) +def test_to_mp7_pathlines_noop(dataframe): + prt_pls = ( + MP7_TEST_PATHLINES + if dataframe + else MP7_TEST_PATHLINES.to_records(index=False) + ) + mp7_pls = to_mp7_pathlines(prt_pls) + assert ( + type(prt_pls) + == type(mp7_pls) + == (pd.DataFrame if dataframe else np.recarray) + ) + assert len(mp7_pls) == 2 + assert set( + dict(mp7_pls.dtypes).keys() if dataframe else mp7_pls.dtype.names + ) == set(MP7_PATHLINE_DTYPE.fields.keys()) + assert np.array_equal( + mp7_pls if dataframe else pd.DataFrame(mp7_pls), MP7_TEST_PATHLINES + ) @pytest.mark.parametrize("dataframe", [True, False]) def test_to_mp7_endpoints(dataframe): - inp_pls = pls if dataframe else pls.to_records(index=False) - mp7_eps = to_mp7_endpoints(inp_pls) + mp7_eps = to_mp7_endpoints( + PRT_TEST_PATHLINES + if dataframe + else PRT_TEST_PATHLINES.to_records(index=False) + ) assert len(mp7_eps) == 1 + assert np.isclose(mp7_eps.time[0], PRT_TEST_PATHLINES.t.max()) assert set( dict(mp7_eps.dtypes).keys() if dataframe else mp7_eps.dtype.names - ) == set(mp7_ep_cols) + ) == set(MP7_ENDPOINT_DTYPE.fields.keys()) -def test_to_prt_pathlines_roundtrip(): - inp_pls = pls - mp7_pls = to_mp7_pathlines(inp_pls) +@pytest.mark.parametrize("dataframe", [True, False]) +def test_to_mp7_endpoints_empty(dataframe): + mp7_eps = to_mp7_endpoints( + pd.DataFrame.from_records([], columns=PRT_PATHLINE_DTYPE.fields.keys()) + if dataframe + else np.recarray((0,), dtype=PRT_PATHLINE_DTYPE) + ) + assert mp7_eps.empty if dataframe else mp7_eps.size == 0 + if dataframe: + mp7_eps = mp7_eps.to_records(index=False) + assert mp7_eps.dtype == MP7_ENDPOINT_DTYPE + + +@pytest.mark.parametrize("dataframe", [True, False]) +def test_to_mp7_endpoints_noop(dataframe): + """Test a recarray or dataframe which already contains MP7 endpoint data""" + mp7_eps = to_mp7_endpoints( + MP7_TEST_ENDPOINTS + if dataframe + else MP7_TEST_ENDPOINTS.to_records(index=False) + ) + assert np.array_equal( + mp7_eps if dataframe else pd.DataFrame(mp7_eps), MP7_TEST_ENDPOINTS + ) + + +@pytest.mark.parametrize("dataframe", [True, False]) +def test_to_prt_pathlines_roundtrip(dataframe): + mp7_pls = to_mp7_pathlines( + PRT_TEST_PATHLINES + if dataframe + else PRT_TEST_PATHLINES.to_records(index=False) + ) prt_pls = to_prt_pathlines(mp7_pls) - inp_pls.drop( - ["imdl", "iprp", "irpt", "name", "istatus", "ireason"], - axis=1, - inplace=True, + if not dataframe: + prt_pls = pd.DataFrame(prt_pls) + assert np.allclose( + PRT_TEST_PATHLINES.drop( + ["imdl", "iprp", "irpt", "name", "istatus", "ireason"], + axis=1, + ), + prt_pls.drop( + ["imdl", "iprp", "irpt", "name", "istatus", "ireason"], + axis=1, + ), ) - prt_pls.drop( - ["imdl", "iprp", "irpt", "name", "istatus", "ireason"], - axis=1, - inplace=True, + + +@pytest.mark.parametrize("dataframe", [True, False]) +def test_to_prt_pathlines_roundtrip_empty(dataframe): + mp7_pls = to_mp7_pathlines( + pd.DataFrame.from_records([], columns=PRT_PATHLINE_DTYPE.fields.keys()) + if dataframe + else np.recarray((0,), dtype=PRT_PATHLINE_DTYPE) ) - assert np.allclose(inp_pls, prt_pls) + prt_pls = to_prt_pathlines(mp7_pls) + assert mp7_pls.empty if dataframe else mp7_pls.size == 0 + assert prt_pls.empty if dataframe else mp7_pls.size == 0 + assert set( + dict(mp7_pls.dtypes).keys() if dataframe else mp7_pls.dtype.names + ) == set(MP7_PATHLINE_DTYPE.fields.keys()) diff --git a/flopy/plot/plotutil.py b/flopy/plot/plotutil.py index 92b4a6e015..9ec30afe25 100644 --- a/flopy/plot/plotutil.py +++ b/flopy/plot/plotutil.py @@ -2600,19 +2600,87 @@ def parse_modpath_selection_options( return tep, istart, xp, yp -# define minimum expected fields -_mp7_pathline_fields = ["x", "y", "z", "time", "k", "particleid"] -_prt_pathline_fields = [ - "x", - "y", - "z", - "t", - "trelease", - "imdl", - "iprp", - "irpt", - "ilay", -] +PRT_PATHLINE_DTYPE = np.dtype( + [ + ("kper", np.int32), + ("kstp", np.int32), + ("imdl", np.int32), + ("iprp", np.int32), + ("irpt", np.int32), + ("ilay", np.int32), + ("icell", np.int32), + ("izone", np.int32), + ("istatus", np.int32), + ("ireason", np.int32), + ("trelease", np.float32), + ("t", np.float32), + ("x", np.float32), + ("y", np.float32), + ("z", np.float32), + ("name", np.str_), + ] +) +MP7_PATHLINE_DTYPE = np.dtype( + [ + ("particleid", np.int32), # same as sequencenumber + ("particlegroup", np.int32), + ( + "sequencenumber", + np.int32, + ), # mp7 sequencenumber (globally unique auto-generated ID) + ( + "particleidloc", + np.int32, + ), # mp7 particle ID (unique within a group, user-assigned or autogenerated) + ("time", np.float32), + ("x", np.float32), + ("y", np.float32), + ("z", np.float32), + ("k", np.int32), + ("node", np.int32), + ("xloc", np.float32), + ("yloc", np.float32), + ("zloc", np.float32), + ("stressperiod", np.int32), + ("timestep", np.int32), + ] +) +MP7_ENDPOINT_DTYPE = np.dtype( + [ + ( + "particleid", + np.int32, + ), # mp7 sequencenumber (globally unique auto-generated ID) + ("particlegroup", np.int32), + ( + "particleidloc", + np.int32, + ), # mp7 particle ID (unique within a group, user-assigned or autogenerated) + ("status", np.int32), + ("time0", np.float32), + ("time", np.float32), + ("node0", np.int32), + ("k0", np.int32), + ("xloc0", np.float32), + ("yloc0", np.float32), + ("zloc0", np.float32), + ("x0", np.float32), + ("y0", np.float32), + ("z0", np.float32), + ("zone0", np.int32), + ("initialcellface", np.int32), + ("node", np.int32), + ("k", np.int32), + ("xloc", np.float32), + ("yloc", np.float32), + ("zloc", np.float32), + ("x", np.float32), + ("y", np.float32), + ("z", np.float32), + ("zone", np.int32), + ("cellface", np.int32), + ] +) def to_mp7_pathlines( @@ -2641,13 +2709,13 @@ def to_mp7_pathlines( # check format dt = data.dtypes if not ( - all(n in dt for n in _mp7_pathline_fields) - or all(n in dt for n in _prt_pathline_fields) + all(n in dt for n in MP7_PATHLINE_DTYPE.fields.keys()) + or all(n in dt for n in PRT_PATHLINE_DTYPE.fields.keys()) ): raise ValueError( - "Pathline data must contain all of the following columns: " - f"{_mp7_pathline_fields} for MODPATH 7, or " - f"{_prt_pathline_fields} for MODFLOW 6 PRT" + "Pathline data must contain the following fields: " + f"{MP7_PATHLINE_DTYPE.fields.keys()} for MODPATH 7, or " + f"{PRT_PATHLINE_DTYPE.fields.keys()} for MODFLOW 6 PRT" ) # return early if already in MP7 format @@ -2656,6 +2724,11 @@ def to_mp7_pathlines( data if ret_type == pd.DataFrame else data.to_records(index=False) ) + # return early if empty + if data.empty: + ret = np.recarray((0,), dtype=MP7_PATHLINE_DTYPE) + return pd.DataFrame(ret) if ret_type == pd.DataFrame else ret + # assign a unique particle index column incrementing an integer # for each unique combination of irpt, iprp, imdl, and trelease data = data.sort_values(["imdl", "iprp", "irpt", "trelease"]) @@ -2666,33 +2739,6 @@ def to_mp7_pathlines( # convert to recarray data = data.to_records(index=False) - # define mp7 dtype - mp7_dtypes = np.dtype( - [ - ("particleid", np.int32), # same as sequencenumber - ("particlegroup", np.int32), - ( - seqn_key, - np.int32, - ), # mp7 sequencenumber (globally unique auto-generated ID) - ( - "particleidloc", - np.int32, - ), # mp7 particle ID (unique within a group, user-assigned or autogenerated) - ("time", np.float32), - ("x", np.float32), - ("y", np.float32), - ("z", np.float32), - ("k", np.int32), - ("node", np.int32), - ("xloc", np.float32), - ("yloc", np.float32), - ("zloc", np.float32), - ("stressperiod", np.int32), - ("timestep", np.int32), - ] - ) - # build mp7 format recarray ret = np.core.records.fromarrays( [ @@ -2713,7 +2759,7 @@ def to_mp7_pathlines( data["kper"], data["kstp"], ], - dtype=mp7_dtypes, + dtype=MP7_PATHLINE_DTYPE, ) return pd.DataFrame(ret) if ret_type == pd.DataFrame else ret @@ -2742,6 +2788,27 @@ def to_mp7_endpoints( if isinstance(data, np.recarray): data = pd.DataFrame(data) + # check format + dt = data.dtypes + if all(n in dt for n in MP7_ENDPOINT_DTYPE.fields.keys()): + return ( + data if ret_type == pd.DataFrame else data.to_records(index=False) + ) + if not ( + all(n in dt for n in MP7_PATHLINE_DTYPE.fields.keys()) + or all(n in dt for n in PRT_PATHLINE_DTYPE.fields.keys()) + ): + raise ValueError( + "Pathline data must contain the following fields: " + f"{MP7_PATHLINE_DTYPE.fields.keys()} for MODPATH 7, or " + f"{PRT_PATHLINE_DTYPE.fields.keys()} for MODFLOW 6 PRT" + ) + + # return early if empty + if data.empty: + ret = np.recarray((0,), dtype=MP7_ENDPOINT_DTYPE) + return pd.DataFrame(ret) if ret_type == pd.DataFrame else ret + # assign a unique particle index column incrementing an integer # for each unique combination of irpt, iprp, imdl, and trelease data = data.sort_values(["imdl", "iprp", "irpt", "trelease"]) @@ -2779,44 +2846,6 @@ def to_mp7_endpoints( # convert to recarray endpts = endpts.to_records(index=False) - # define mp7 dtype - mp7_dtype = np.dtype( - [ - ( - "particleid", - np.int32, - ), # mp7 sequencenumber (globally unique auto-generated ID) - ("particlegroup", np.int32), - ( - "particleidloc", - np.int32, - ), # mp7 particle ID (unique within a group, user-assigned or autogenerated) - ("status", np.int32), - ("time0", np.float32), - ("time", np.float32), - ("node0", np.int32), - ("k0", np.int32), - ("xloc0", np.float32), - ("yloc0", np.float32), - ("zloc0", np.float32), - ("x0", np.float32), - ("y0", np.float32), - ("z0", np.float32), - ("zone0", np.int32), - ("initialcellface", np.int32), - ("node", np.int32), - ("k", np.int32), - ("xloc", np.float32), - ("yloc", np.float32), - ("zloc", np.float32), - ("x", np.float32), - ("y", np.float32), - ("z", np.float32), - ("zone", np.int32), - ("cellface", np.int32), - ] - ) - # build mp7 format recarray ret = np.core.records.fromarrays( [ @@ -2849,7 +2878,7 @@ def to_mp7_endpoints( endpts["izone"], np.zeros(endpts.shape[0]), # todo cell face? ], - dtype=mp7_dtype, + dtype=MP7_ENDPOINT_DTYPE, ) return pd.DataFrame(ret) if ret_type == pd.DataFrame else ret @@ -2881,13 +2910,13 @@ def to_prt_pathlines( # check format dt = data.dtypes if not ( - all(n in dt for n in _mp7_pathline_fields) - or all(n in dt for n in _prt_pathline_fields) + all(n in dt for n in MP7_PATHLINE_DTYPE.fields.keys()) + or all(n in dt for n in PRT_PATHLINE_DTYPE.fields.keys()) ): raise ValueError( - "Pathline data must contain all of the following columns: " - f"{_mp7_pathline_fields} for MODPATH 7, or " - f"{_prt_pathline_fields} for MODFLOW 6 PRT" + "Pathline data must contain the following fields: " + f"{MP7_PATHLINE_DTYPE.fields.keys()} for MODPATH 7, or " + f"{PRT_PATHLINE_DTYPE.fields.keys()} for MODFLOW 6 PRT" ) # return early if already in PRT format @@ -2896,31 +2925,14 @@ def to_prt_pathlines( data if ret_type == pd.DataFrame else data.to_records(index=False) ) + # return early if empty + if data.empty: + ret = np.recarray((0,), dtype=PRT_PATHLINE_DTYPE) + return pd.DataFrame(ret) if ret_type == pd.DataFrame else ret + # convert to recarray data = data.to_records(index=False) - # define prt dtype - prt_dtypes = np.dtype( - [ - ("kper", np.int32), - ("kstp", np.int32), - ("imdl", np.int32), - ("iprp", np.int32), - ("irpt", np.int32), - ("ilay", np.int32), - ("icell", np.int32), - ("izone", np.int32), - ("istatus", np.int32), - ("ireason", np.int32), - ("trelease", np.float32), - ("t", np.float32), - ("x", np.float32), - ("y", np.float32), - ("z", np.float32), - ("name", str), - ] - ) - # build prt format recarray ret = np.core.records.fromarrays( [ @@ -2939,9 +2951,14 @@ def to_prt_pathlines( data["x"], data["y"], data["z"], - np.zeros(data.shape[0]), + np.zeros(data.shape[0], str), ], - dtype=prt_dtypes, + dtype=PRT_PATHLINE_DTYPE, ) - return pd.DataFrame(ret) if ret_type == pd.DataFrame else ret + if ret_type == pd.DataFrame: + df = pd.DataFrame(ret) + df.name = df.name.astype(pd.StringDtype()) + return df + else: + return ret