From 04fe7e4a3c67cb068de444a2979a3528ccbc3e60 Mon Sep 17 00:00:00 2001 From: Joshua Larsen Date: Mon, 12 Jul 2021 11:41:11 -0700 Subject: [PATCH] Feat(ZoneBudget6): Added ZoneBudget6 class to zonbud.py (#1149) * moved shared methods to hidden definitions from ZoneBudget class * begin deprecation of ZoneBudgetOutput class * added .zonebudget() to the .output method for modflow6 --- autotest/t039_test.py | 103 +- autotest/t504_test.py | 18 +- .../Notebooks/flopy3_ZoneBudget_example.ipynb | 564 +- examples/Notebooks/flopy3_export.ipynb | 393 +- .../modflow6/tutorial101_mf6_output.py | 288 +- .../test003_gwfs_disv/test003_gwfs_disv.dbf | Bin 61325 -> 61325 bytes flopy/discretization/modeltime.py | 40 + flopy/mf6/utils/binarygrid_util.py | 1 + flopy/mf6/utils/output_util.py | 38 +- flopy/utils/__init__.py | 2 + flopy/utils/zonbud.py | 5877 ++++++++++------- 11 files changed, 4479 insertions(+), 2845 deletions(-) diff --git a/autotest/t039_test.py b/autotest/t039_test.py index 303e9061a8..86edc8f970 100644 --- a/autotest/t039_test.py +++ b/autotest/t039_test.py @@ -3,12 +3,11 @@ """ import os import numpy as np +import flopy from flopy.utils import ( - CellBudgetFile, ZoneBudget, - MfListBudget, - read_zbarray, - write_zbarray, + ZoneBudget6, + ZoneFile6, ) loadpth = os.path.join("..", "examples", "data", "zonbud_examples") @@ -96,7 +95,7 @@ def test_compare2zonebudget(rtol=1e-2): zonenames = [n for n in zba.dtype.names if "ZONE" in n] times = np.unique(zba["totim"]) - zon = read_zbarray(zon_f) + zon = ZoneBudget.read_zone_file(zon_f) zb = ZoneBudget(cbc_f, zon, totim=times, verbose=False) fpa = zb.get_budget() @@ -152,7 +151,7 @@ def test_zonbud_get_record_names(): """ t039 Test zonbud get_record_names method """ - zon = read_zbarray(zon_f) + zon = ZoneBudget.read_zone_file(zon_f) zb = ZoneBudget(cbc_f, zon, kstpkper=(0, 0)) recnames = zb.get_record_names() assert len(recnames) > 0, "No record names returned." @@ -165,7 +164,7 @@ def test_zonbud_aliases(): """ t039 Test zonbud aliases """ - zon = read_zbarray(zon_f) + zon = ZoneBudget.read_zone_file(zon_f) aliases = {1: "Trey", 2: "Mike", 4: "Wilson", 0: "Carini"} zb = ZoneBudget( cbc_f, zon, kstpkper=(0, 1096), aliases=aliases, verbose=True @@ -179,7 +178,7 @@ def test_zonbud_to_csv(): """ t039 Test zonbud export to csv file method """ - zon = read_zbarray(zon_f) + zon = ZoneBudget.read_zone_file(zon_f) zb = ZoneBudget(cbc_f, zon, kstpkper=[(0, 1094), (0, 1096)]) f_out = os.path.join(outpth, "test.csv") zb.to_csv(f_out) @@ -193,7 +192,7 @@ def test_zonbud_math(): """ t039 Test zonbud math methods """ - zon = read_zbarray(zon_f) + zon = ZoneBudget.read_zone_file(zon_f) cmd = ZoneBudget(cbc_f, zon, kstpkper=(0, 1096)) cmd / 35.3147 cmd * 12.0 @@ -206,7 +205,7 @@ def test_zonbud_copy(): """ t039 Test zonbud copy """ - zon = read_zbarray(zon_f) + zon = ZoneBudget.read_zone_file(zon_f) cfd = ZoneBudget(cbc_f, zon, kstpkper=(0, 1096)) cfd2 = cfd.copy() assert cfd is not cfd2, "Copied object is a shallow copy." @@ -218,9 +217,9 @@ def test_zonbud_readwrite_zbarray(): t039 Test zonbud read write """ x = np.random.randint(100, 200, size=(5, 150, 200)) - write_zbarray(os.path.join(outpth, "randint"), x) - write_zbarray(os.path.join(outpth, "randint"), x, fmtin=35, iprn=2) - z = read_zbarray(os.path.join(outpth, "randint")) + ZoneBudget.write_zone_file(os.path.join(outpth, "randint"), x) + ZoneBudget.write_zone_file(os.path.join(outpth, "randint"), x, fmtin=35, iprn=2) + z = ZoneBudget.read_zone_file(os.path.join(outpth, "randint")) assert np.array_equal(x, z), "Input and output arrays do not match." return @@ -229,7 +228,7 @@ def test_dataframes(): try: import pandas - zon = read_zbarray(zon_f) + zon = ZoneBudget.read_zone_file(zon_f) cmd = ZoneBudget(cbc_f, zon, totim=1095.0) df = cmd.get_dataframes() assert len(df) > 0, "Output DataFrames empty." @@ -240,7 +239,7 @@ def test_dataframes(): def test_get_budget(): - zon = read_zbarray(zon_f) + zon = ZoneBudget.read_zone_file(zon_f) aliases = {1: "Trey", 2: "Mike", 4: "Wilson", 0: "Carini"} zb = ZoneBudget(cbc_f, zon, kstpkper=(0, 0), aliases=aliases) zb.get_budget(names="FROM_CONSTANT_HEAD", zones=1) @@ -251,7 +250,7 @@ def test_get_budget(): def test_get_model_shape(): ZoneBudget( - cbc_f, read_zbarray(zon_f), kstpkper=(0, 0), verbose=True + cbc_f, ZoneBudget.read_zone_file(zon_f), kstpkper=(0, 0), verbose=True ).get_model_shape() return @@ -273,7 +272,7 @@ def test_zonebudget_output_to_netcdf(): hds = HeadFile(os.path.join(model_ws, hds)) ml = Modflow.load(nam, model_ws=model_ws) - zone_array = read_zbarray(os.path.join(zb_ws, zon)) + zone_array = ZoneBudget.read_zone_file(os.path.join(zb_ws, zon)) # test with standard zonebudget output zbout = "freyberg_mlt.txt" @@ -376,7 +375,7 @@ def test_zonbud_active_areas_zone_zero(rtol=1e-2): # Run ZoneBudget utility and reformat output zon_f = os.path.join(loadpth, "zonef_mlt_active_zone_0.zbr") - zon = read_zbarray(zon_f) + zon = ZoneBudget.read_zone_file(zon_f) zb = ZoneBudget(cbc_f, zon, kstpkper=(0, 1096)) fpbud = zb.get_dataframes().reset_index() fpbud = fpbud[["name"] + [c for c in fpbud.columns if "ZONE" in c]] @@ -392,6 +391,73 @@ def test_zonbud_active_areas_zone_zero(rtol=1e-2): return +def test_zonebudget_6(): + try: + import pandas as pd + except ImportError: + return + + exe_name = 'mf6' + zb_exe_name = "zbud6" + cpth = os.path.join(".", "temp", "t039") + + sim_ws = os.path.join("..", "examples", "data", "mf6", "test001e_UZF_3lay") + sim = flopy.mf6.MFSimulation.load(sim_ws=sim_ws, exe_name=exe_name) + sim.simulation_data.mfpath.set_sim_path(cpth) + sim.write_simulation() + success, _ = sim.run_simulation() + + grb_file = os.path.join(cpth, 'test001e_UZF_3lay.dis.grb') + cbc_file = os.path.join(cpth, 'test001e_UZF_3lay.cbc') + + ml = sim.get_model("gwf_1") + idomain = np.ones(ml.modelgrid.shape, dtype=int) + + zb = ZoneBudget6(model_ws=cpth, exe_name=zb_exe_name) + zf = ZoneFile6(zb, idomain) + zb.grb = grb_file + zb.cbc = cbc_file + zb.write_input(line_length=21) + success, _ = zb.run_model() + + if not success: + raise AssertionError("Zonebudget run failed") + + df = zb.get_dataframes() + + if not isinstance(df, pd.DataFrame): + raise TypeError + + zb_pkg = ml.uzf.output.zonebudget(idomain) + zb_pkg.change_model_ws(cpth) + zb_pkg.name = "uzf_zonebud" + zb_pkg.write_input() + success, _ = zb_pkg.run_model(exe_name=zb_exe_name) + + if not success: + raise AssertionError("UZF package zonebudget run failed") + + df = zb_pkg.get_dataframes() + + if not isinstance(df, pd.DataFrame): + raise TypeError() + + # test aliases + zb = ZoneBudget6(model_ws=cpth, exe_name=zb_exe_name) + zf = ZoneFile6(zb, idomain, aliases={1: "test alias", 2: "test pop"}) + zb.grb = grb_file + zb.cbc = cbc_file + zb.write_input(line_length=5) + success, _ = zb.run_model() + if not success: + raise AssertionError("UZF package zonebudget run failed") + + df = zb.get_dataframes() + + if list(df)[0] != "test_alias": + raise AssertionError("Alias testing failed") + + if __name__ == "__main__": # test_compare2mflist_mlt() test_compare2zonebudget() @@ -406,3 +472,4 @@ def test_zonbud_active_areas_zone_zero(rtol=1e-2): test_get_model_shape() test_zonebudget_output_to_netcdf() test_zonbud_active_areas_zone_zero() + test_zonebudget_6() diff --git a/autotest/t504_test.py b/autotest/t504_test.py index 37f28d85e5..cf9f7b3e35 100644 --- a/autotest/t504_test.py +++ b/autotest/t504_test.py @@ -1169,14 +1169,21 @@ def test_mf6_output(): bud = ml.oc.output.budget() hds = ml.oc.output.head() + idomain = np.ones(ml.modelgrid.shape, dtype=int) + zonbud = ml.oc.output.zonebudget(idomain) + if not isinstance(bud, flopy.utils.CellBudgetFile): raise TypeError() if not isinstance(hds, flopy.utils.HeadFile): raise TypeError() + if not isinstance(zonbud, flopy.utils.ZoneBudget6): + raise AssertionError() + bud = ml.output.budget() hds = ml.output.head() + zonbud = ml.output.zonebudget(idomain) if not isinstance(bud, flopy.utils.CellBudgetFile): raise TypeError() @@ -1184,10 +1191,14 @@ def test_mf6_output(): if not isinstance(hds, flopy.utils.HeadFile): raise TypeError() + if not isinstance(zonbud, flopy.utils.ZoneBudget6): + raise TypeError() + uzf = ml.uzf uzf_bud = uzf.output.budget() conv = uzf.output.package_convergence() uzf_obs = uzf.output.obs() + uzf_zonbud = uzf.output.zonebudget(idomain) if not isinstance(uzf_bud, flopy.utils.CellBudgetFile): raise TypeError() @@ -1199,11 +1210,14 @@ def test_mf6_output(): if not isinstance(uzf_obs, flopy.utils.Mf6Obs): raise TypeError() - if len(uzf.output.methods()) != 3: + if not isinstance(uzf_zonbud, flopy.utils.ZoneBudget6): + raise TypeError() + + if len(uzf.output.methods()) != 4: print(uzf.output.__dict__) raise AssertionError(", ".join(uzf.output.methods())) - if len(ml.output.methods()) != 2: + if len(ml.output.methods()) != 3: raise AssertionError() if ml.dis.output.methods() is not None: diff --git a/examples/Notebooks/flopy3_ZoneBudget_example.ipynb b/examples/Notebooks/flopy3_ZoneBudget_example.ipynb index 174ffe4449..7755cc4f81 100644 --- a/examples/Notebooks/flopy3_ZoneBudget_example.ipynb +++ b/examples/Notebooks/flopy3_ZoneBudget_example.ipynb @@ -22,12 +22,11 @@ "name": "stdout", "output_type": "stream", "text": [ - "3.8.6 | packaged by conda-forge | (default, Oct 7 2020, 18:42:56) \n", - "[Clang 10.0.1 ]\n", - "numpy version: 1.18.5\n", - "matplotlib version: 3.2.2\n", - "pandas version: 1.0.5\n", - "flopy version: 3.3.3\n" + "3.7.4 (default, Aug 9 2019, 18:34:13) [MSC v.1915 64 bit (AMD64)]\n", + "numpy version: 1.18.1\n", + "matplotlib version: 3.1.2\n", + "pandas version: 0.25.3\n", + "flopy version: 3.3.4\n" ] } ], @@ -79,9 +78,17 @@ "execution_count": 3, "metadata": {}, "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "c:\\users\\jlarsen\\desktop\\flopy-dev\\flopy\\utils\\zonbud.py:2947: PendingDeprecationWarning: Deprecation planned for version 3.3.5, use ZoneBudget.read_zone_file()\n", + " PendingDeprecationWarning,\n" + ] + }, { "data": { - "image/png": "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\n", + "image/png": "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\n", "text/plain": [ "
" ] @@ -1321,7 +1328,7 @@ "outputs": [ { "data": { - "image/png": "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tAD6Dv9VhduuGvXbGCNJxvjx4/dOEX/ppZcyd+5cJk2axIknnsiQIUOSDi9S55133n5tGzZs4Oijj+aOO+5IICIRkdpPBR4RERGRCLj7G6QfVwegVwXHjAHGRBaUZJUGDRpwxRVX7F0/++yzc+LRNIBNmzbts+7udOvWjb/97W+4O82bN08oMpHctH37dpYsWcJxxx1H06ZNkw4ncqtWreLII4+kadOmrFixgnnz5nHSSSfRuXPnpEOrlGbREhERERGphdavX89Pf/pThgwZwmeffcadd95Jly5duPzyy1m3bl3S4UWqRYsWfOMb39i7FBYWsnbtWk4//XQKC/d/ZE/2ZWbtzOx1M1tsZgvN7OawvbmZvWpmH4WvzVKOGWVmS83sQzP7bnLRS20wePDgve/feOMNOnbsyPDhwznllFN46aWXEowsetk8g6EKPCIiIiIitdCAAQPo2LEj7dq1o2fPnjRu3JgXX3yRb3/729xwww1JhxepsWPHcuKJJzJ16lSWL1/O8uXLyc/PZ/ny5Sxbtizp8LLBbmC4u58MnAUMMbOOwEhgprt3AGaG64Tb+gKdgN7AQ2ZWP5HIpVZ455139r7/93//d55//nlef/11Zs+ezc9//vMEI4te2QyGb775JkOHDuV//ud/ePTRR5k7dy6PPfZY0uFVSgUeEREREZFaqKSkhJtuuomRI0eyZcsWRowYQfv27bnppptYuXJl0uFF6tZbb2X8+PHcddddDBs2jG3btml69Bpw93Xu/l74fhuwGGgL9AEmhLtNAC4O3/cBnnL3L919ObAU6BZv1FJbbd26ldNPPx2A4447jtLSuj08XNkMhk2bNs26GQw1Bk+WydZnAUVERESkZvbs2bP3/dVXX73Ptrr+CxYEs2Y9/fTTvPDCCxQVFbFz586kQ8pKZlYAnAbMAVq6+zoIikBmdnS4W1vgnZTD1oRt5c81CBgE0L59++iClsR98MEHdOnSBXdnxYoVbN68mWbNmrFnzx527dqVdHiRyuYZDFXgySLFxcU8/PDDHHLIIdx666385je/oXv37owePZqBAwcybNiwpEMUERERkQzp06cP27dvp0mTJtx9991725cuXcqJJ56YYGTx+v73v895553Hxx9/DMDjjz/ONddck3BU2cHMmgDPAre4+9ZKekGl2+D7NbiPA8YBFBYW7rdd6o7Fixfvs96kSRMgGAD9rrvuSiKk2GTzDIYq8GSRsmcBd+7cSUFBAcuWLSMvL48dO3Zw5plnqsAjIpFxd+bOncvatWsxM9q0aUO3bt3UXV5EJEIV/RL19a9/nQsvvDDmaJLVuHHjvT3WR48erQJPNZhZQ4LizpPu/lzYXGJmrcPeO62BDWH7GqBdyuH5wCfxRSu1zTHHHJO2vUWLFlxyySUxRxOvbJ7BUAWeLFL2LGCjRo2y7llAEcler7zyCoMHD6ZDhw60bRv01l6zZg1Lly7loYce4vzzz084QhGR3FPXixxdunRJ2+7ulJSUxBxN9rHgLzCPAovdPXXan6lAf6A4fJ2S0j7JzO4F2gAdgLnxRSy1zdatW/n1r3/NmjVruOCCC+jXr9/ebYMHD+ahhx5KMLpoZfO9q8CTRbL5WUARyV4333wzM2bMoKCgYJ/25cuX873vfW+/LrwiIpIZuVzkKCkpYfr06TRr1myfdnfPmr+kJ6w7cBXwvpktCNtuJyjsTDazgcAq4DIAd19oZpOBRQQzcA1x97o/0JNU6JprrqFDhw786Ec/4rHHHuPZZ59l0qRJHHLIIfvMsFUXZfO9q8CTRbL5WUARyV67d+8mPz9/v/a2bdvW+UH2RESSlE1FjvteXZLR8xWc9m0efX0Rx3Uu3G9bq5O+kfHrpRpadEJk546Lu79B+nF1AHpVcMwYYExkQUlW+fjjj3n22WcBuPjiixkzZgznnnsuU6dOTTiy6GXzvavAk0Wy+VlAEcle1157LWeccQZ9+/alXbvg8fxVq1bxl7/8hYEDByYcnYhI7RBFwSGpIkdtKHD0Hf6rCrddNeq3MUYikpu+/PJL9uzZQ7169QC44447yM/P5zvf+Q7bt29POLpoZfO910s6AKm+9evX89Of/pQhQ4bw2Wefceedd9KlSxcuv/xy1q1bl3R4IlJHjRo1ikmTJuHuvP3227z11lsAPPnkk4waNSrh6ERE6q6+w3+VtrgDKnKISLS+//3v89prr+3T1r9/f37729/SqFGjhKKKRzbfu3rwZJEBAwZw4YUXsmPHDnr27MmVV17Jiy++yJQpU7jhhhuYMmVK1ScRETkAJ598MieffPLe9Q0bNnD00UcnGJGIiIiIRGXs2LFp23v37s3tt98eczTxyuZ7j60Hj5m1M7PXzWyxmS00s5vD9jvNbK2ZLQiX76UcM8rMlprZh2b23bhira1KSkq46aabGDlyJFu2bGHEiBG0b9+em266iZUrVyYdnojUUZs2bdpv6datG5s3b2bTpk1JhyciIiIiMRo9enTSISSmtt97nD14dgPD3f09MzsCmG9mr4bb7nP336TubGYdgb5AJ4Kp+maY2Qm5PJr7nj179r6/+uqr99lWWpqzXxYRiViLFi045phj9mlbu3Ytp59+OmbGsmXLEopMRERERKKQy7P4ZfO9x1bgcfd1wLrw/TYzWwy0reSQPsBT7v4lsNzMlgLdgLcjD7aW6tOnD9u3b6dJkybcfffde9uXLl3KiSeemGBkIlKXjR07lhkzZnDPPfdwyimnAHDssceyfPnyhCMTERERkShk0yx+mZbN957IGDxmVgCcBswBugM3mtnVwDyCXj6bCYo/qZPMryFNQcjMBgGDANq3bx9p3Em76667+OCDD1i7di1nnnkmTZo0AeDrX/861113XcLRiUhddeutt9K3b1+GDh1Ku3bt+MUvfoFZRTOvioiIiEiccn0Wv0zHkk33Xl7ss2iZWRPgWeAWd98K/BE4HuhK0MOnbEqAdL89+H4N7uPcvdDdC/Py8iKKunZ44IEH6NOnDw888ACdO3feZ1Dl2j7Yk4hkt/z8fJ5++ml69uxJUVERO3fuTDokEREREYlILs/il833HmuBx8waEhR3nnT35wDcvcTdS919D/AIwWNYEPTYaZdyeD7wSZzx1jbjxo1j/vz5PP/888yaNYtf/vKX3H///UDQXUxEJApz5sxh69atAPTq1YvvfOc7dO7cmREjRvD5558nHJ2IiIiIiEC8s2gZ8Ciw2N3vTWlvnbLbD4F/hO+nAn3N7BAzOxboAMyNK97aqLS0dO9jWQUFBcyaNYtp06YxbNgwFXhEJDLXXnsthx12GAC33HILu3bt4s477+Swww7jmmuuSTg6ERERERGBeMfg6Q5cBbxvZgvCttuBK8ysK8HjVyuA6wHcfaGZTQYWEczANSSXZ9ACaNWqFQsWLKBr164ANGnShBdffJFrr72W999/P+HoRKSu2rNnDw0aBP9dzJs3j/feew+Ab33rW3vzkYiIiIiIJCu2Hjzu/oa7m7t3cfeu4fKSu1/l7qeE7T8IZ9sqO2aMux/v7ie6+7S4Yq2tJk6cSKtWrfZpa9CgARMnTuSvf/1rQlGJSF3XuXNnHn/8cQBOPfVU5s2bB8CSJUto2LBhkqGJiIiIiEgo9kGW5cDl5+fvV+Ap071795ijEZFcMX78eGbPns3xxx/PokWL+OY3v8lxxx3HT37yE8aPH590eCIiIiIiQkLTpIuISPY46qijeOKJJ9i2bRvLli1j9+7d5Ofn07Jly6RDExERERGRkAo8Ebrv1SVJh5ARQ4tOSDoEEakFjjjiCE499dSkwxARERERkTRU4BERqWPqSnEZVGAWEREREakujcEjIiIiIiIiIpLlVOAREREREREREclyKvCIiIiIiIiIiGQ5FXhERERERERERKq9x3kAACAASURBVLKcCjwiIiIiIiIiIllOs2hJ1pk3bx6rV6+mQYMGdOjQgZNOOinpkGKTy/d+MMysKTAe6Aw4cC3wIfAXoABYAVzu7pvD/UcBA4FS4GfuPj3+qEVERERERKpPBR7JGrNnz2b48OE0bdqU+fPn0717dzZv3kzDhg3585//TLt27ZIOMTK5fO8Zcj/wsrtfamaNgMOA24GZ7l5sZiOBkcAIM+sI9AU6AW2AGWZ2gruXJhW8iIiIiIhIVfSIlmSNW265hWnTpjFjxgzee+89GjZsyJtvvskdd9zBwIEDkw4vUrl87wfLzI4EvgM8CuDuX7n7FqAPMCHcbQJwcfi+D/CUu3/p7suBpUC3eKMWERERiZeZ9TazD81safjHLxHJMirwSNYoLS0lLy8PgPbt27Ny5UoAioqKWLt2bZKhRS6X7z0DjgM2Ao+b2d/MbLyZHQ60dPd1AOHr0eH+bYHVKcevCdv2Y2aDzGyemc3buHFjdHcgIiIiEiEzqw88CFwAdASuCHs1i0gWUYFHskZhYSEDBw5k0qRJ9OvXjx49egCwc+dOSkvr9tMzuXzvGdAAOB34o7ufBuwgeByrIpamzdPt6O7j3L3Q3QvLCnAiIiIiWagbsNTdl7n7V8BTBL2aRSSLaAweyRoPP/wwjzzyCG+99RbnnXce1157LQBmxvTpdXsM3Fy+9wxYA6xx9znh+jMEBZ4SM2vt7uvMrDWwIWX/1EGN8oFPYotWREREJH7pejCfebAnveWWW1iwYMHBnqZa1mz+ZyzXidqUZo1rfExduXfI7ftf2fOb/O53vzuoc6jAI1mjYcOGDB48eL/2xo0bc8wxxyQQUXxy+d4PlruvN7PVZnaiu38I9AIWhUt/oDh8nRIeMhWYZGb3Egyy3AGYG3/kIiIiIrGpVg9mMxsEDIJg2IDaJN/qyuPyNf+61p17B93/wVGBR7LG9u3bGTt2LM899xyrV6+mUaNGHH/88dxwww0MGDAg6fAilcv3niE3AU+GM2gtA64heER1spkNBFYBlwG4+0Izm0xQANoNDNEMWiIiIlLHVasHs7uPA8YBFBYWpn2EPdXB9kaokdd/Hd+1otRzVM2PqSv3Drl9/wdy7+WowCNZ48orr+SHP/whL7/8MpMnT2bHjh307duXu+++myVLlvCrX/0q6RAjk8v3ngnuvgAoTLOpVwX7jwHGRBqUiEgFzKw3cD9QHxjv7sUJhyQidd+7QAczOxZYC/QF+iUbkojUlAZZlqyxYsUKBgwYQH5+PsOGDWPq1Kl06NCBxx9/nOeeey7p8CKVy/cuIpJLNJONiCTB3XcDNwLTgcXAZHdfmGxUIlJTKvBI1jj88MN54403AHjhhRdo3rw5APXq1cO9yh6iWS2X711EJMdoJhsRSYS7v+TuJ7j78WFvZhHJMnpES7LGn/70J6677jqWLFlC586deeyxxwDYuHEjQ4YMSTi6aOXyvYuI5JiMz2SjWWwOTE1ncsnle4e6df+ZmMlGRCQJKvBI1ujSpQtz5+4/mVFeXh5HHHFEAhHFJ5fvXUQkx1Q5k41msYlLzb62uXzvUNfuX0QkO6nAI3XC6NGjueaaa5IOIxG5fO8iInVQlTPZaBabmNR0NpNcvnfQ/YuI1AKxFXjMrB0wEWgF7AHGufv9ZtYc+AtQAKwALnf3zeExo4CBQCnwM3efHle8Uvt06dIlbbu7U1JSEnM08crlexcRyTGayUZEREQOSJw9eHYDw939PTM7AphvZq8CA4CZ7l5sZiOBkcCIcMaIvkAnoA0ww8xOcPfSGGOWWqSkpITp06fTrFmzfdrdnbPPPjuhqOKRy/cuIpJL3H23mZXNZFMfeEwz2YiIiEh1xFbgcfd1wLrw/TYzW0wwkGAfoEe42wRgFjAibH/K3b8ElpvZUoKZJd6OK2apXS666CK2b99O165d99vWo0eP+AOKUS7fu4hIrnH3l4CXko5DREREsksiY/CYWQFwGjAHaBkWf3D3dWZ2dLhbW+CdlMPWhG2Sox599NEKt02aNCnGSOKXy/cuIiIiIiIiVYu9wGNmTYBngVvcfatZuskigl3TtO03kGBtnkkil9336pKkQ8iYoUUn1PiYunL/B3LvIiIiIiIiEr96cV7MzBoSFHeedPfnwuYSM2sdbm8NbAjbq5xFAoKZJNy90N0L8/LyogteRERy2q5du/Zr+/TTTxOIJH65fO8iIiIi2SK2Ao8FXXUeBRa7+70pm6YC/cP3/YEpKe19zeyQcCaJDsDcuOIVEREBeP3118nPz6dNmzacf/75rFixYu+2888/P7nAYpDL9y4iIiKSbeLswdMduAo418wWhMv3gGKgyMw+AorCdcIZIyYDi4CXgSGaQUtEROJ22223MX36dDZu3MigQYMoKirinXeCIeLc93tyuE7J5XsXERERyTZxzqL1BunH1QHoVcExY4AxkQUlIiJSha+++opOnToBcOmll3LyySdzySWXUFxcTCXjyNUJuXzvIiIiItkmkVm0REREskXDhg1Zv349rVq1AqBTp07MnDmTiy66iI8//jjh6KKVy/cuIiIikm1iHWRZREQk2xQXF1NSUrJPW35+PrNmzWLkyJEJRRWPXL53ERERkWxTYQ8eM7uksgNTZsESEcko5R+pTc4777y07U2bNuWOO+6IOZp45eK9K/+ISKYpr4hIXCp7ROv74evRwNnAa+F6T2AWoEQkIlFR/pFa4+WXX6Z3794AfP755wwbNox3332Xzp07c99999GyZcuEI4xOjt678o+IZJryiojEosJHtNz9Gne/BnCgo7v/yN1/BHSKLToRyUnKP1Kb3H777XvfDx8+nNatW/PCCy9wxhlncP311ycYWfRy8d6Vf0Qk05RXRCQu1RlkucDd16WslwAnRBSPiEgq5R+pVebNm8eCBQsAGDp0KBMmTEg4ovjk4L0r/4hIpimviEikqlPgmWVm04H/JKg69wVejzQqEZGA8o8kbsOGDdx77724O1u3bsXd904RvmfPnoSji1Yu3zvKPyKSecorIhKpKgs87n5jODDYt8Omce7+X9GGJSKi/CO1w09+8hO2bdsGQP/+/fn000/Jy8tj/fr1dO3aNeHoopXL9678IyKZprwiIlGrbBat6cDLwLRwZHcN/iUisVD+kdpk9OjR+7VdffXVTJw4kYkTJyYQUXxy8d6Vf0Qk05RXRCQulfXg6Q/0Bu40sxOAOQSJaaa7b48jOBHJWco/Umv84Ac/2K/ttddeY8uWLQBMnTo1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TJnCokWLKCgoYNWqVfTr149Vq1YxcuRI3D10PBERERERIUsLPO6+EdhoZj8od6gLsMLdVwKY2SygD7As+twtet10YAFa4BERybrdu3dTUFAAwHHHHceCBQvo168f77//vhZ4RERERERyROg9eI4GPkx7vjoaA2jm7usAos9NK/oCZjbUzErMrGTTpk0ZDSsikkRFRUUsWbJk7/OCggKeeOIJNm/ezNKlSwMmExERERGRMqEXeKyCsUP6c7C7P+Dundy9U5MmTWooloiIlJkxYwZFRUX7jdWpU4cZM2bwwgsvBEolIiIiIiLpMrbAY2bDzWxJ9NGikpetBlqlPW8JrI0ebzCz5tHXag5szFRWERGpXMuWLQ9Y4Cnz7W9/O8tpRERERESkIhlb4HH3ye5eHH2sreRlC4GTzOx4M6sHDATmRsfmAoOix4OAxyp4v4iIZNjSpUvp2rUrrVq1YujQoXz00Ud7j3Xp0iVgMhERERERKZOt26QXmdlq4Cbgl2a22syOdPddwPXA08BbwGx3fzN62zigh5ktJ3WXrXHZyCoiIvu77rrruO2221i6dCknn3wy3/nOd3j33XcB2LlzZ+B0IiIiIiIC2buL1npSl19VdGweMK+C8S3AuRmOJiIiB1FaWkrPnj0BGDVqFB07dqRnz548/PDDmFW0lZqIiIiIiGRbVhZ4REQkvtydbdu20aBBAwC6d+/OnDlz6Nu3L1u3bg2cTkREREREIPxdtEREJMeNHj2at956a7+xpk2bMn/+fC655JJAqUREREREJJ3O4BERkSpdfvnlB4z16tWLxYsXM2XKlACJRERERESkPJ3BIyIih8zdQ0cQEREREZE0WuAREZFDNmTIkNARREREREQkjRZ4RETkkA0bNix0BBERERERSaMFHhERERERERGRmNMCj4iIiIiIiIhIzGmBR0REREREREQk5rTAIyIiIiIiIiISc1rgERERERERERGJOXP30BlqjJltAt4PnSPLCoHNoUMEkuS5Q+7N/1h3bxI6RCjqn8RJ8twht+av7lH3JE2S559rc1f/qH+SJMlzh9ybf4X9k1cLPElkZiXu3il0jhCSPHfQ/CW8JP8bTPLcQfOXsJL+7y/J80/y3CU3JPnfYJLnDvGZvy7REhERERERERGJOS3wiIiIiIiIiIjEnBZ44u+B0AECSvLcQfOX8JL8bzDJcwfNX8JK+r+/JM8/yXOX3JDkf4NJnjvEZP7ag0dEREREREREJOZ0Bo+IiIiIiIiISMxpgUdEREREREREJOa0wCMiIiIiIiIiEnNa4BERERERERERiTkt8MSImf067XGPkFmyzcy6hs6Qa8zsBDP7pZm9ETqL5Lckdw+ofyqi/pFsSXL/qHsOpO6RbFL/SLq49I8WeOKlZ9rj8cFShHF/2QMzezlkkJDMrLmZ3WBmrwFvArWBywLHkvyX5O4B9Q+g/pFgktw/6h7UPRKU+gf1T9z6Rws8EheW9vgbwVIEYmZDzOw54B9AIXANsM7db3f3pWHTieQ99Y/6RyQEdY+6RyQU9U9M+6dO6ABySJqa2U2k/oMre7yXu98TJlZW1DKzRqQWJcse7y0ed98aLFl2TAZeBi539xIAM/OwkSRBktw9oP5R/0hISe4fdY+6R8JS/6h/Ytc/WuCJlynANyt4nAQNgEXsK5bFaccc+FbWE2VXC6A/cI+ZNQNmA3XDRpIESXL3gPpH/SMhJbl/1D3qHglL/aP+iV3/mHssFqLkIMzsCHf/NHQOyTwzawkMJHX95+HAX9395rCpJKnUPcmi/pFcov5JDnWP5Br1T3LErX+0B0/MmNnRZtbJzOpFz5tGO7wvDxwto8zsmKo+QufLNDO7Pu1pA3f/rbt3BC4GvggUSxIkqd0D6h/1j4SW1P5R96h7JDz1j/qHmPWPzuCJETO7AbgFWAEcBkwA7gFmAHe5+7qA8TLKzJaSOh0wfcMvB5oATd29dpBgWWJmi929Q/nHItmQ5O4B9Y/6R0JKcv+oe9Q9Epb6R/1T/nEcaA+eeBkKnOLuW6OV0xXA99z9lcC5Ms7dT0t/bmbHAaOB7wO/DhApJDv4S0RqVGK7B9Q/5ah/JNsS2z/qnv2oeyQE9U9E/RMfWuCJl8/Ldix39w/M7J0kFEw6MzuJ1Er6GcB/Az9x951hU2VFQzO7hFTBNIge7+XufwkTSxIi8d0D6h/UPxJG4vtH3aPukWDUP+qf2PWPLtGKETPbCMxKGxqY/tzdf5L1UFliZu1IlcupwF3AH919d9hU2WNm00idFgmpokk/ZdLd/UdBgkkiJLl7QP2j/pGQktw/6h51j4Sl/lH/lD0lRv2jBZ4YMbNBVR139+nZypJtZrYb+BD4G3BAueRzwQKY2U/Tnpb9R7sJ+Ke7vxcgkiRIkrsH1D/qHwkpyf2j7lH3SFjqH/VPJFb9o0u0YiSfS6QacnaVNEsKKhg7FrjFzG5z91kVHBepEQnvHlD/qH8kmIT3j7rnQOoeyRr1T6LFtn90Bk+MmNnj7FtBJHq8GXje3f9vmFTZZ2YFpE6N+zR0ltDM7Cjg2Tjt7C7xo+7ZR/2zj/pHskH9k6Lu2UfdI9mi/klR/+wTh/7RGTzx8tsKxo4CrjCzdu4+JtuBssnMrgN+ARwRPS8Fxrv7/UGDBRTt6h+rnd0llhLdPaD+qYj6R7Ik0f2j7jmQukeySP2j/tlPHPpHCzwx4u7/qGjczOYCi4C8LRkz+yVwFtDN3VdGY98CJpjZUe5+R9CAgZjZOcBHoXNIfkty94D6pzLqH8mGJPePuqdi6h7JFvWP+qe8OPSPLtHKE2a2xN2LQ+fIFDP7D9De3T8vN14f+Je7nxwmWXaY2VL2P0UUUn9BWAtc6e5vZz+VSP53D6h/1D+Sq/K9f9Q96h7JXeof9U/2U1WPzuCJkeiav/IaAVcCb2Y5TtaVL5hobIeZ7QmRJ8t6l3vuwBZdCyvZkPTuAfVPuefqH8mapPePumc/6h7JKvWP+idNbPpHCzzxsojUP66y6/72AFuABcB1gTJly2ozO9fd56cPmtm5wLpAmbLG3d8PnUESLcndA+of9Y+ElOT+UfeIhKX+Uf/EjhZ44uVS4EN3XwdgZoOAvsA3yP//L38CPGZm/2Rf2XYGvg30CRlMJAGS3D2g/hEJKcn9o+4RCUv9o/6JHe3BEyNmthj4frR79/eAWcAIoBho4+79ggbMIDM7ESgCTgZOJbWS/iawHFjj7u8GjCeS15LcPaD+EQkpyf2j7hEJS/2j/okjLfDEiJn9y93bR48nA5vc/bboeb5v9PUEcLO7/7vceCfgVne/MEwykfyX5O4B9Y9ISEnuH3WPSFjqH/VPHNUKHUAOSW0zKzsd8FzgubRj+X6a4HHlCwbA3UuA47IfRyRRktw9oP4RCSnJ/aPuEQlL/VOO+if35fs/zHzzR+AfZrYZ2AG8CHtPodsWMlgWfKOKY/WzlkIkmZLcPaD+EQkpyf2j7hEJS/1TMfVPDtMlWjFjZl2B5sDfy27TZmYnAwXuvjhouAwysz8Cz7n7lHLjVwPnufulYZKJJENSuwfUPyKhJbV/1D0i4al/1D9xowUeiQUzawb8FfiS1E7uAJ2AesD/4+7rQ2UTkfym/hGRENQ9IhKK+ie+tMAjsWJm3YF20dM33f25ql4vIlJT1D8iEoK6R0RCUf/EjxZ4RERERERERERiTnfREhERERERERGJOS3wiIiIiIiIiIjEnBZ4RERERERERERiTgs8IiIiIiIiIiIx9/8DUUec9SvtJUAAAAAASUVORK5CYII=\n", + "image/png": "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/cvMFptZSdb4QWb2rJm9H31vGketIiKys3vvvZe+ffsyYcIEOnTowKxZO3Zuzt5hS0REREREwonrFq0NwI+BC7MHzaw+MJHMVqDLgDfMbLa7LyKzbd9cdx8bNX5KgNEx1SsiIpFJkybx5ptvUlRUxNKlS+nfvz9Lly5l5MiRuHvo8kREREREhJgaPO6+BlhjZt/f5VQ3YLG7LwEwsxlAX2BR9L179LypwDzU4BERid22bdsoKioC4IgjjmDevHn079+fDz/8UA0eEREREZE8EXoNnkOBj7MeL4vGAFq4+0qA6Hvzqn6AmV1tZqVmVrp27dqcFisikkbFxcUsWLCg8nFRURF//vOfWbduHQsXLgxYmYiIiIiIVAjd4LEqxvbqz8Hu/oC7d3H3LoccckgdlSUiIhWmTZtGcXHxTmMNGjRg2rRpvPjii4GqEhERERGRbDlr8JjZcDNbEH21quZpy4A2WY9bAyui49Vm1jL6WS2BNbmqVUREqte6devdGjwVTj/99JirERERERGRquSswePuE929U/S1opqnvQEcY2ZHmlkjYCAwOzo3GxgUHQ8CZlXxehERybGFCxdyyimn0KZNG66++mo++eSTynPdunULWJmIiIiIiFSIa5v0YjNbBtwI/IeZLTOzA9x9K3AdMAd4B5jp7m9HLxsL9DSz98nssjU2jlpFRGRn1157LbfeeisLFy7k2GOP5Tvf+Q7//ve/Afjqq68CVyciIiIiIhDfLlqryNx+VdW5p4CnqhhfD5yV49JERGQPysvL6dWrFwCjRo2ic+fO9OrVi0ceeQSzqpZSExERERGRuMXS4BERkeRyd8rKymjSpAkAPXr04LHHHqNfv35s2LAhcHUiIiIiIgLhd9ESEZE8N3r0aN55552dxpo3b87cuXO56KKLAlUlIiIiIiLZdAWPiIjU6LLLLtttrHfv3syfP59JkyYFqEhERERERHalK3hERGSvuXvoEkREREREJIsaPCIisteGDh0augQREREREcmiBo+IiOy1YcOGhS5BRERERESyqMEjIiIiIiIiIpJwavCIiIiIiIiIiCScGjwiIiIiIiIiIgmnBo+IiIiIiIiISMKpwSMiIiIiIiIiknDm7qFrqDNmthb4MHQdMWsGrAtdRCBpnjvk3/wPd/dDQhcRivInddI8d8iv+St7lD1pk+b559vclT/K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\n", "text/plain": [ "
" ] @@ -1357,6 +1364,545 @@ "plt.show()" ] }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Zonebudget for Modflow 6 (`ZoneBudget6`)\n", + "\n", + "This section shows how to build and run a Zonebudget when working with a MODFLOW 6 model. \n", + "\n", + "First let's load a model" + ] + }, + { + "cell_type": "code", + "execution_count": 22, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "loading simulation...\n", + " loading simulation name file...\n", + " loading tdis package...\n", + " loading model gwf6...\n", + " loading package dis...\n", + " loading package ic...\n", + "WARNING: Block \"options\" is not a valid block name for file type ic.\n", + " loading package oc...\n", + " loading package npf...\n", + " loading package sto...\n", + " loading package chd...\n", + " loading package riv...\n", + " loading package wel...\n", + " loading package rch...\n", + " loading ims package gwf_1...\n", + "writing simulation...\n", + " writing simulation name file...\n", + " writing simulation tdis package...\n", + " writing ims package gwf_1...\n", + " writing model gwf_1...\n", + " writing model name file...\n", + " writing package dis...\n", + " writing package ic...\n", + " writing package oc...\n", + " writing package npf...\n", + " writing package sto...\n", + " writing package chd_0...\n", + " writing package riv_0...\n", + " writing package wel_0...\n", + " writing package rch_0...\n", + "FloPy is using the following executable to run the model: .\\mf6.exe\n", + " MODFLOW 6\n", + " U.S. GEOLOGICAL SURVEY MODULAR HYDROLOGIC MODEL\n", + " VERSION 6.2.1 02/18/2021\n", + "\n", + " MODFLOW 6 compiled Feb 18 2021 21:14:51 with IFORT compiler (ver. 19.10.3)\n", + "\n", + "This software has been approved for release by the U.S. Geological \n", + "Survey (USGS). Although the software has been subjected to rigorous \n", + "review, the USGS reserves the right to update the software as needed \n", + "pursuant to further analysis and review. No warranty, expressed or \n", + "implied, is made by the USGS or the U.S. Government as to the \n", + "functionality of the software and related material nor shall the \n", + "fact of release constitute any such warranty. Furthermore, the \n", + "software is released on condition that neither the USGS nor the U.S. \n", + "Government shall be held liable for any damages resulting from its \n", + "authorized or unauthorized use. Also refer to the USGS Water \n", + "Resources Software User Rights Notice for complete use, copyright, \n", + "and distribution information.\n", + "\n", + " \n", + " Run start date and time (yyyy/mm/dd hh:mm:ss): 2021/07/09 21:23:27\n", + " \n", + " Writing simulation list file: mfsim.lst\n", + " Using Simulation name file: mfsim.nam\n", + " \n", + " Solving: Stress period: 1 Time step: 1\n", + " \n", + " Run end date and time (yyyy/mm/dd hh:mm:ss): 2021/07/09 21:23:27\n", + " Elapsed run time: 0.150 Seconds\n", + " \n", + " Normal termination of simulation.\n" + ] + } + ], + "source": [ + "mf6_exe = \"mf6\"\n", + "zb6_exe = \"zbud6\"\n", + "if platform.system().lower() == \"windows\":\n", + " mf6_exe += \".exe\"\n", + " zb6_exe += \".exe\"\n", + "\n", + "sim_ws = os.path.join('..', 'data', 'mf6-freyberg')\n", + "cpth = os.path.join(\".\", \"temp\")\n", + "\n", + "sim = flopy.mf6.MFSimulation.load(sim_ws=sim_ws, exe_name=mf6_exe)\n", + "sim.simulation_data.mfpath.set_sim_path(cpth)\n", + "sim.write_simulation()\n", + "sim.run_simulation();" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Use the the `.output` model attribute to create a zonebudget model\n", + "\n", + "The `.output` attribute allows the user to access model output and create zonebudget models easily. The user only needs to pass in a zone array to create a zonebudget model!" + ] + }, + { + "cell_type": "code", + "execution_count": 23, + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "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\n", + "text/plain": [ + "
" + ] + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + } + ], + "source": [ + "# let's get our idomain array from the model, split it into two zones, and use it as a zone array\n", + "ml = sim.get_model('gwf_1')\n", + "zones = ml.modelgrid.idomain\n", + "zones[0, 20:] = np.where(zones[0, 20:] != 0,\n", + " 2,\n", + " 0)\n", + "\n", + "plt.imshow(zones[0])\n", + "plt.colorbar();" + ] + }, + { + "cell_type": "code", + "execution_count": 24, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "FloPy is using the following executable to run the model: .\\zbud6.exe\n", + " ZONEBUDGET Version 6\n", + " U.S. GEOLOGICAL SURVEY\n", + " VERSION 6.2.1 02/18/2021\n", + ".........\n", + " \n", + "Normal Termination\n" + ] + } + ], + "source": [ + "# now let's build a zonebudget model and run it!\n", + "zonbud = ml.output.zonebudget(zones)\n", + "zonbud.change_model_ws(cpth)\n", + "zonbud.write_input()\n", + "zonbud.run_model(exe_name=zb6_exe);" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Getting the zonebudget output\n", + "\n", + "We can then get the output as a recarray using the `.get_budget()` method or as a pandas dataframe using the `.get_dataframes()` method." + ] + }, + { + "cell_type": "code", + "execution_count": 25, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "rec.array([(10., 0, 0, 'STO_SS_IN', 0. , 0. ),\n", + " (10., 0, 0, 'STO_SY_IN', 0. , 0. ),\n", + " (10., 0, 0, 'DATA_SPDIS_IN', 0. , 0. ),\n", + " (10., 0, 0, 'WEL_IN', 0. , 0. ),\n", + " (10., 0, 0, 'RIV_IN', 0.00419404, 0. ),\n", + " (10., 0, 0, 'RCH_IN', 0.0353 , 0.0342 ),\n", + " (10., 0, 0, 'CHD_IN', 0. , 0.00017814),\n", + " (10., 0, 0, 'STO_SS_OUT', 0. , 0. ),\n", + " (10., 0, 0, 'STO_SY_OUT', 0. , 0. ),\n", + " (10., 0, 0, 'DATA_SPDIS_OUT', 0. , 0. ),\n", + " (10., 0, 0, 'WEL_OUT', 0.0162 , 0.00585 ),\n", + " (10., 0, 0, 'RIV_OUT', 0.02102186, 0.02637232),\n", + " (10., 0, 0, 'RCH_OUT', 0. , 0. ),\n", + " (10., 0, 0, 'CHD_OUT', 0. , 0.00442785),\n", + " (10., 0, 0, 'FROM_ZONE_0', 0. , 0. ),\n", + " (10., 0, 0, 'FROM_ZONE_1', 0. , 0.00405826),\n", + " (10., 0, 0, 'FROM_ZONE_2', 0.00178623, 0. ),\n", + " (10., 0, 0, 'TO_ZONE_0', 0. , 0. ),\n", + " (10., 0, 0, 'TO_ZONE_1', 0. , 0.00178623),\n", + " (10., 0, 0, 'TO_ZONE_2', 0.00405826, 0. )],\n", + " dtype=[('totim', '\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
ZONE_1ZONE_2
totimname
10.0STO_SS0.0000000.000000
STO_SY0.0000000.000000
DATA_SPDIS0.0000000.000000
WEL-0.016200-0.005850
RIV-0.016828-0.026372
RCH0.0353000.034200
CHD0.000000-0.004250
ZONE_00.0000000.000000
ZONE_10.0000000.002272
ZONE_2-0.0022720.000000
\n", + "" + ], + "text/plain": [ + " ZONE_1 ZONE_2\n", + "totim name \n", + "10.0 STO_SS 0.000000 0.000000\n", + " STO_SY 0.000000 0.000000\n", + " DATA_SPDIS 0.000000 0.000000\n", + " WEL -0.016200 -0.005850\n", + " RIV -0.016828 -0.026372\n", + " RCH 0.035300 0.034200\n", + " CHD 0.000000 -0.004250\n", + " ZONE_0 0.000000 0.000000\n", + " ZONE_1 0.000000 0.002272\n", + " ZONE_2 -0.002272 0.000000" + ] + }, + "execution_count": 26, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# get the net flux using net=True flag\n", + "zonbud.get_dataframes(net=True)" + ] + }, + { + "cell_type": "code", + "execution_count": 27, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
totimkperkstpzoneCHDDATA_SPDISRCHRIVSTO_SSSTO_SYWELZONE_0ZONE_1ZONE_2
010.00010.000000.00.0353-0.0168280.00.0-0.016200.00.000000-0.002272
110.0002-0.004250.00.0342-0.0263720.00.0-0.005850.00.0022720.000000
\n", + "
" + ], + "text/plain": [ + " totim kper kstp zone CHD DATA_SPDIS RCH RIV STO_SS \\\n", + "0 10.0 0 0 1 0.00000 0.0 0.0353 -0.016828 0.0 \n", + "1 10.0 0 0 2 -0.00425 0.0 0.0342 -0.026372 0.0 \n", + "\n", + " STO_SY WEL ZONE_0 ZONE_1 ZONE_2 \n", + "0 0.0 -0.01620 0.0 0.000000 -0.002272 \n", + "1 0.0 -0.00585 0.0 0.002272 0.000000 " + ] + }, + "execution_count": 27, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# we can also pivot the data into a spreadsheet like format\n", + "zonbud.get_dataframes(net=True, pivot=True)" + ] + }, + { + "cell_type": "code", + "execution_count": 28, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
totimkperkstpzoneCHDDATA_SPDISRCHRIVSTO_SSSTO_SYWELZONE_0ZONE_1ZONE_2
010.00010.0000000.00.353-0.1682780.00.0-0.16200.00.00000-0.02272
110.0002-0.0424970.00.342-0.2637230.00.0-0.05850.00.022720.00000
\n", + "
" + ], + "text/plain": [ + " totim kper kstp zone CHD DATA_SPDIS RCH RIV STO_SS \\\n", + "0 10.0 0 0 1 0.000000 0.0 0.353 -0.168278 0.0 \n", + "1 10.0 0 0 2 -0.042497 0.0 0.342 -0.263723 0.0 \n", + "\n", + " STO_SY WEL ZONE_0 ZONE_1 ZONE_2 \n", + "0 0.0 -0.1620 0.0 0.00000 -0.02272 \n", + "1 0.0 -0.0585 0.0 0.02272 0.00000 " + ] + }, + "execution_count": 28, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# or get a volumetric budget by supplying modeltime \n", + "mt = ml.modeltime\n", + "\n", + "# budget recarray must be pivoted to get volumetric budget!\n", + "zonbud.get_volumetric_budget(mt, recarray=zonbud.get_budget(net=True, pivot=True))" + ] + }, { "cell_type": "code", "execution_count": null, @@ -1382,7 +1928,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.8.6" + "version": "3.7.4" } }, "nbformat": 4, diff --git a/examples/Notebooks/flopy3_export.ipynb b/examples/Notebooks/flopy3_export.ipynb index f62bf1b467..9419c9bccc 100644 --- a/examples/Notebooks/flopy3_export.ipynb +++ b/examples/Notebooks/flopy3_export.ipynb @@ -18,9 +18,8 @@ "name": "stdout", "output_type": "stream", "text": [ - "3.8.6 | packaged by conda-forge | (default, Oct 7 2020, 18:42:56) \n", - "[Clang 10.0.1 ]\n", - "flopy version: 3.3.3\n" + "3.7.4 (default, Aug 9 2019, 18:34:13) [MSC v.1915 64 bit (AMD64)]\n", + "flopy version: 3.3.4\n" ] } ], @@ -180,6 +179,14 @@ "transforming coordinates using = proj=noop ellps=GRS80\n" ] }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "c:\\users\\jlarsen\\desktop\\flopy-dev\\flopy\\modflow\\mfdis.py:640: PendingDeprecationWarning: ModflowDis.thickness will be deprecated and removed in version 3.3.5. Use grid.thick().\n", + " PendingDeprecationWarning,\n" + ] + }, { "name": "stdout", "output_type": "stream", @@ -193,7 +200,7 @@ { "data": { "text/plain": [ - "" + "" ] }, "execution_count": 8, @@ -227,7 +234,7 @@ "initialize_geometry::self.grid_crs = +proj=longlat +ellps=WGS84 +datum=WGS84 +no_defs +type=crs\n", "initialize_geometry::nc_crs = epsg:4326\n", "transforming coordinates using = proj=noop ellps=GRS80\n", - "wrote data/netCDF_export/top.shp\n" + "wrote data\\netCDF_export\\top.shp\n" ] } ], @@ -257,7 +264,7 @@ "name": "stdout", "output_type": "stream", "text": [ - "wrote data/netCDF_export/drn.shp\n" + "wrote data\\netCDF_export\\drn.shp\n" ] } ], @@ -285,7 +292,7 @@ "initialize_geometry::self.grid_crs = +proj=longlat +ellps=WGS84 +datum=WGS84 +no_defs +type=crs\n", "initialize_geometry::nc_crs = epsg:4326\n", "transforming coordinates using = proj=noop ellps=GRS80\n", - "wrote data/netCDF_export/hk.shp\n" + "wrote data\\netCDF_export\\hk.shp\n" ] } ], @@ -320,7 +327,7 @@ { "data": { "text/plain": [ - "" + "" ] }, "execution_count": 12, @@ -365,20 +372,20 @@ "text/plain": [ "\n", "root group (NETCDF4 data model, file format HDF5):\n", - " Conventions: CF-1.6, ACDD-1.3, flopy 3.3.3\n", - " date_created: 2021-02-18T17:31:00Z\n", + " Conventions: CF-1.6, ACDD-1.3, flopy 3.3.4\n", + " date_created: 2021-07-09T23:58:00Z\n", " geospatial_vertical_positive: up\n", " geospatial_vertical_min: -25.0\n", " geospatial_vertical_max: 4.832500457763672\n", " geospatial_vertical_resolution: variable\n", " featureType: Grid\n", " namefile: freyberg.nam\n", - " model_ws: ../data/freyberg_multilayer_transient\n", + " model_ws: ..\\data\\freyberg_multilayer_transient\n", " exe_name: mf2005.exe\n", " modflow_version: mfnwt\n", - " create_hostname: IGSAAAHMLT40179\n", - " create_platform: Darwin\n", - " create_directory: /Users/jdhughes/Documents/Development/flopy_git/flopy_fork/examples/Notebooks\n", + " create_hostname: IGSWCAWWLT6673\n", + " create_platform: Windows\n", + " create_directory: C:\\Users\\jlarsen\\Desktop\\flopy-dev\\examples\\Notebooks\n", " solver_head_tolerance: -999\n", " solver_flux_tolerance: -999\n", " flopy_sr_xll: 123456.7\n", @@ -425,25 +432,33 @@ "transforming coordinates using = proj=noop ellps=GRS80\n" ] }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "c:\\users\\jlarsen\\desktop\\flopy-dev\\flopy\\modflow\\mfdis.py:640: PendingDeprecationWarning: ModflowDis.thickness will be deprecated and removed in version 3.3.5. Use grid.thick().\n", + " PendingDeprecationWarning,\n" + ] + }, { "data": { "text/plain": [ "\n", "root group (NETCDF4 data model, file format HDF5):\n", - " Conventions: CF-1.6, ACDD-1.3, flopy 3.3.3\n", - " date_created: 2021-02-18T17:31:00Z\n", + " Conventions: CF-1.6, ACDD-1.3, flopy 3.3.4\n", + " date_created: 2021-07-09T23:58:00Z\n", " geospatial_vertical_positive: up\n", " geospatial_vertical_min: -25.0\n", " geospatial_vertical_max: 4.832500457763672\n", " geospatial_vertical_resolution: variable\n", " featureType: Grid\n", " namefile: freyberg.nam\n", - " model_ws: ../data/freyberg_multilayer_transient\n", + " model_ws: ..\\data\\freyberg_multilayer_transient\n", " exe_name: mf2005.exe\n", " modflow_version: mfnwt\n", - " create_hostname: IGSAAAHMLT40179\n", - " create_platform: Darwin\n", - " create_directory: /Users/jdhughes/Documents/Development/flopy_git/flopy_fork/examples/Notebooks\n", + " create_hostname: IGSWCAWWLT6673\n", + " create_platform: Windows\n", + " create_directory: C:\\Users\\jlarsen\\Desktop\\flopy-dev\\examples\\Notebooks\n", " solver_head_tolerance: -999\n", " solver_flux_tolerance: -999\n", " flopy_sr_xll: 123456.7\n", @@ -489,20 +504,8 @@ "initialize_geometry::proj4_str = +proj=longlat +ellps=WGS84 +datum=WGS84 +no_defs\n", "initialize_geometry::self.grid_crs = +proj=longlat +ellps=WGS84 +datum=WGS84 +no_defs +type=crs\n", "initialize_geometry::nc_crs = epsg:4326\n", - "transforming coordinates using = proj=noop ellps=GRS80\n" - ] - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "error getting data for cell_by_cell_flowstorage at time 1.0:list index out of range\n" - ] - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ + "transforming coordinates using = proj=noop ellps=GRS80\n", + "error getting data for cell_by_cell_flowstorage at time 1.0:list index out of range\n", "error getting data for cell_by_cell_flowstorage at time 1097.0:list index out of range\n" ] }, @@ -511,20 +514,20 @@ "text/plain": [ "\n", "root group (NETCDF4 data model, file format HDF5):\n", - " Conventions: CF-1.6, ACDD-1.3, flopy 3.3.3\n", - " date_created: 2021-02-18T17:31:00Z\n", + " Conventions: CF-1.6, ACDD-1.3, flopy 3.3.4\n", + " date_created: 2021-07-09T23:58:00Z\n", " geospatial_vertical_positive: up\n", " geospatial_vertical_min: -25.0\n", " geospatial_vertical_max: 4.832500457763672\n", " geospatial_vertical_resolution: variable\n", " featureType: Grid\n", " namefile: freyberg.nam\n", - " model_ws: ../data/freyberg_multilayer_transient\n", + " model_ws: ..\\data\\freyberg_multilayer_transient\n", " exe_name: mf2005.exe\n", " modflow_version: mfnwt\n", - " create_hostname: IGSAAAHMLT40179\n", - " create_platform: Darwin\n", - " create_directory: /Users/jdhughes/Documents/Development/flopy_git/flopy_fork/examples/Notebooks\n", + " create_hostname: IGSWCAWWLT6673\n", + " create_platform: Windows\n", + " create_directory: C:\\Users\\jlarsen\\Desktop\\flopy-dev\\examples\\Notebooks\n", " solver_head_tolerance: -999\n", " solver_flux_tolerance: -999\n", " flopy_sr_xll: 123456.7\n", @@ -576,6 +579,26 @@ "execution_count": 16, "metadata": {}, "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "c:\\users\\jlarsen\\desktop\\flopy-dev\\flopy\\utils\\zonbud.py:2942: PendingDeprecationWarning: Deprecation planned for version 3.3.5, use ZoneBudget.read_zone_file()\n", + " PendingDeprecationWarning,\n", + "c:\\users\\jlarsen\\desktop\\flopy-dev\\flopy\\utils\\zonbud.py:3128: PendingDeprecationWarning: ZoneBudgetOutput will be deprecated in version 3.3.5,Use ZoneBudget.read_output(, pivot=True) or ZoneBudget6.get_budget(, pivot=True)\n", + " PendingDeprecationWarning,\n", + "c:\\users\\jlarsen\\desktop\\flopy-dev\\flopy\\utils\\zonbud.py:3183: PendingDeprecationWarning: ZoneBudgetOutput will be deprecated in version 3.3.5\n", + " PendingDeprecationWarning,\n", + "c:\\users\\jlarsen\\desktop\\flopy-dev\\flopy\\utils\\zonbud.py:3171: PendingDeprecationWarning: ZoneBudgetOutput will be deprecated in version 3.3.5\n", + " PendingDeprecationWarning,\n", + "c:\\users\\jlarsen\\desktop\\flopy-dev\\flopy\\utils\\zonbud.py:3183: PendingDeprecationWarning: ZoneBudgetOutput will be deprecated in version 3.3.5\n", + " PendingDeprecationWarning,\n", + "c:\\users\\jlarsen\\desktop\\flopy-dev\\flopy\\utils\\zonbud.py:3171: PendingDeprecationWarning: ZoneBudgetOutput will be deprecated in version 3.3.5\n", + " PendingDeprecationWarning,\n", + "c:\\users\\jlarsen\\desktop\\flopy-dev\\flopy\\utils\\zonbud.py:3195: PendingDeprecationWarning: ZoneBudgetOutput will be deprecated in version 3.3.5\n", + " PendingDeprecationWarning,\n" + ] + }, { "data": { "text/plain": [ @@ -614,21 +637,23 @@ "initialize_geometry::proj4_str = +proj=longlat +ellps=WGS84 +datum=WGS84 +no_defs\n", "initialize_geometry::self.grid_crs = +proj=longlat +ellps=WGS84 +datum=WGS84 +no_defs +type=crs\n", "initialize_geometry::nc_crs = epsg:4326\n", - "transforming coordinates using = proj=noop ellps=GRS80\n" - ] - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "error getting data for cell_by_cell_flowstorage at time 1.0:list index out of range\n" + "transforming coordinates using = proj=noop ellps=GRS80\n", + "error getting data for cell_by_cell_flowstorage at time 1.0:list index out of range\n", + "error getting data for cell_by_cell_flowstorage at time 1097.0:list index out of range\n" ] }, { - "name": "stdout", + "name": "stderr", "output_type": "stream", "text": [ - "error getting data for cell_by_cell_flowstorage at time 1097.0:list index out of range\n" + "c:\\users\\jlarsen\\desktop\\flopy-dev\\flopy\\utils\\zonbud.py:3221: PendingDeprecationWarning: ZoneBudgetOutput will be deprecated in version 3.3.5\n", + " PendingDeprecationWarning,\n", + "c:\\users\\jlarsen\\desktop\\flopy-dev\\flopy\\utils\\zonbud.py:3195: PendingDeprecationWarning: ZoneBudgetOutput will be deprecated in version 3.3.5\n", + " PendingDeprecationWarning,\n", + "c:\\users\\jlarsen\\desktop\\flopy-dev\\flopy\\utils\\zonbud.py:3289: PendingDeprecationWarning: ZoneBudgetOutput will be deprecated in version 3.3.5\n", + " PendingDeprecationWarning,\n", + "c:\\users\\jlarsen\\desktop\\flopy-dev\\flopy\\utils\\zonbud.py:3171: PendingDeprecationWarning: ZoneBudgetOutput will be deprecated in version 3.3.5\n", + " PendingDeprecationWarning,\n" ] }, { @@ -636,20 +661,20 @@ "text/plain": [ "\n", "root group (NETCDF4 data model, file format HDF5):\n", - " Conventions: CF-1.6, ACDD-1.3, flopy 3.3.3\n", - " date_created: 2021-02-18T17:31:00Z\n", + " Conventions: CF-1.6, ACDD-1.3, flopy 3.3.4\n", + " date_created: 2021-07-09T23:58:00Z\n", " geospatial_vertical_positive: up\n", " geospatial_vertical_min: -25.0\n", " geospatial_vertical_max: 4.832500457763672\n", " geospatial_vertical_resolution: variable\n", " featureType: Grid\n", " namefile: freyberg.nam\n", - " model_ws: ../data/freyberg_multilayer_transient\n", + " model_ws: ..\\data\\freyberg_multilayer_transient\n", " exe_name: mf2005.exe\n", " modflow_version: mfnwt\n", - " create_hostname: IGSAAAHMLT40179\n", - " create_platform: Darwin\n", - " create_directory: /Users/jdhughes/Documents/Development/flopy_git/flopy_fork/examples/Notebooks\n", + " create_hostname: IGSWCAWWLT6673\n", + " create_platform: Windows\n", + " create_directory: C:\\Users\\jlarsen\\Desktop\\flopy-dev\\examples\\Notebooks\n", " solver_head_tolerance: -999\n", " solver_flux_tolerance: -999\n", " flopy_sr_xll: 123456.7\n", @@ -697,6 +722,16 @@ "execution_count": 18, "metadata": {}, "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "c:\\users\\jlarsen\\desktop\\flopy-dev\\flopy\\utils\\zonbud.py:3128: PendingDeprecationWarning: ZoneBudgetOutput will be deprecated in version 3.3.5,Use ZoneBudget.read_output(, pivot=True) or ZoneBudget6.get_budget(, pivot=True)\n", + " PendingDeprecationWarning,\n", + "c:\\users\\jlarsen\\desktop\\flopy-dev\\flopy\\utils\\zonbud.py:3195: PendingDeprecationWarning: ZoneBudgetOutput will be deprecated in version 3.3.5\n", + " PendingDeprecationWarning,\n" + ] + }, { "data": { "text/html": [ @@ -718,101 +753,95 @@ " \n", " \n", " \n", + " totim\n", " kper\n", " kstp\n", " zone\n", - " storage\n", - " constant head\n", - " other zones\n", - " total\n", - " zone 0\n", - " zone 1\n", - " zone 2\n", - " zone 3\n", - " tslen\n", - " totim\n", + " CONSTANT_HEAD\n", + " OTHER_ZONES\n", + " STORAGE\n", + " TOTAL\n", + " ZONE_0\n", + " ZONE_1\n", + " ZONE_2\n", + " ZONE_3\n", " \n", " \n", " \n", " \n", " 0\n", + " 1.0\n", " 0\n", " 0\n", " 1\n", - " 0.000000\n", " -821.281900\n", " -1570.821\n", + " 0.000000\n", " -2392.103\n", " 0.0\n", " 0.0000\n", " -1530.422\n", " -40.3993\n", - " 1.0\n", - " 1.0\n", " \n", " \n", " 1\n", + " 1.0\n", " 0\n", " 0\n", " 2\n", - " 0.000000\n", " -648.804700\n", " 630.730\n", + " 0.000000\n", " -18.075\n", " 0.0\n", " 1530.4220\n", " 0.000\n", " -899.6920\n", - " 1.0\n", - " 1.0\n", " \n", " \n", " 2\n", + " 1.0\n", " 0\n", " 0\n", " 3\n", - " 0.000000\n", " -976.232200\n", " 940.092\n", + " 0.000000\n", " -36.140\n", " 0.0\n", " 40.3993\n", " 899.692\n", " 0.0000\n", - " 1.0\n", - " 1.0\n", " \n", " \n", " 3\n", + " 2.0\n", " 1\n", " 0\n", " 1\n", - " 218.568500\n", " -816.347300\n", " -1173.221\n", + " 218.568500\n", " -1770.999\n", " 0.0\n", " 0.0000\n", " -1134.937\n", " -38.2835\n", - " 1.0\n", - " 2.0\n", " \n", " \n", " 4\n", + " 2.0\n", " 1\n", " 0\n", " 2\n", - " 191.816342\n", " -643.938700\n", " 433.628\n", + " 191.816342\n", " -18.493\n", " 0.0\n", " 1134.9370\n", " 0.000\n", " -701.3090\n", - " 1.0\n", - " 2.0\n", " \n", " \n", " ...\n", @@ -828,121 +857,115 @@ " ...\n", " ...\n", " ...\n", - " ...\n", " \n", " \n", " 3286\n", + " 1096.0\n", " 1095\n", " 0\n", " 2\n", - " -626.408120\n", " -505.116270\n", " 1113.766\n", + " -626.408120\n", " -17.758\n", " 0.0\n", " 2489.4040\n", " 0.000\n", " -1375.6380\n", - " 1.0\n", - " 1096.0\n", " \n", " \n", " 3287\n", + " 1096.0\n", " 1095\n", " 0\n", " 3\n", - " -627.235750\n", " -801.732376\n", " 1393.454\n", + " -627.235750\n", " -35.514\n", " 0.0\n", " 17.8163\n", " 1375.638\n", " 0.0000\n", - " 1.0\n", - " 1096.0\n", " \n", " \n", " 3288\n", + " 1097.0\n", " 1096\n", " 0\n", " 1\n", - " 0.000000\n", " -230.548300\n", " -152.236\n", + " 0.000000\n", " -382.784\n", " 0.0\n", " 0.0000\n", " -205.822\n", " 53.5856\n", - " 1.0\n", - " 1097.0\n", " \n", " \n", " 3289\n", + " 1097.0\n", " 1096\n", " 0\n", " 2\n", - " 0.000000\n", " 15.864900\n", " -30.796\n", + " 0.000000\n", " -14.931\n", " 0.0\n", " 205.8220\n", " 0.000\n", " -236.6170\n", - " 1.0\n", - " 1097.0\n", " \n", " \n", " 3290\n", + " 1097.0\n", " 1096\n", " 0\n", " 3\n", - " 0.000000\n", " -212.896600\n", " 183.031\n", + " 0.000000\n", " -29.865\n", " 0.0\n", " -53.5856\n", " 236.617\n", " 0.0000\n", - " 1.0\n", - " 1097.0\n", " \n", " \n", "\n", - "

3291 rows × 13 columns

\n", + "

3291 rows × 12 columns

\n", "" ], "text/plain": [ - " kper kstp zone storage constant head other zones total \\\n", - "0 0 0 1 0.000000 -821.281900 -1570.821 -2392.103 \n", - "1 0 0 2 0.000000 -648.804700 630.730 -18.075 \n", - "2 0 0 3 0.000000 -976.232200 940.092 -36.140 \n", - "3 1 0 1 218.568500 -816.347300 -1173.221 -1770.999 \n", - "4 1 0 2 191.816342 -643.938700 433.628 -18.493 \n", - "... ... ... ... ... ... ... ... \n", - "3286 1095 0 2 -626.408120 -505.116270 1113.766 -17.758 \n", - "3287 1095 0 3 -627.235750 -801.732376 1393.454 -35.514 \n", - "3288 1096 0 1 0.000000 -230.548300 -152.236 -382.784 \n", - "3289 1096 0 2 0.000000 15.864900 -30.796 -14.931 \n", - "3290 1096 0 3 0.000000 -212.896600 183.031 -29.865 \n", + " totim kper kstp zone CONSTANT_HEAD OTHER_ZONES STORAGE \\\n", + "0 1.0 0 0 1 -821.281900 -1570.821 0.000000 \n", + "1 1.0 0 0 2 -648.804700 630.730 0.000000 \n", + "2 1.0 0 0 3 -976.232200 940.092 0.000000 \n", + "3 2.0 1 0 1 -816.347300 -1173.221 218.568500 \n", + "4 2.0 1 0 2 -643.938700 433.628 191.816342 \n", + "... ... ... ... ... ... ... ... \n", + "3286 1096.0 1095 0 2 -505.116270 1113.766 -626.408120 \n", + "3287 1096.0 1095 0 3 -801.732376 1393.454 -627.235750 \n", + "3288 1097.0 1096 0 1 -230.548300 -152.236 0.000000 \n", + "3289 1097.0 1096 0 2 15.864900 -30.796 0.000000 \n", + "3290 1097.0 1096 0 3 -212.896600 183.031 0.000000 \n", "\n", - " zone 0 zone 1 zone 2 zone 3 tslen totim \n", - "0 0.0 0.0000 -1530.422 -40.3993 1.0 1.0 \n", - "1 0.0 1530.4220 0.000 -899.6920 1.0 1.0 \n", - "2 0.0 40.3993 899.692 0.0000 1.0 1.0 \n", - "3 0.0 0.0000 -1134.937 -38.2835 1.0 2.0 \n", - "4 0.0 1134.9370 0.000 -701.3090 1.0 2.0 \n", - "... ... ... ... ... ... ... \n", - "3286 0.0 2489.4040 0.000 -1375.6380 1.0 1096.0 \n", - "3287 0.0 17.8163 1375.638 0.0000 1.0 1096.0 \n", - "3288 0.0 0.0000 -205.822 53.5856 1.0 1097.0 \n", - "3289 0.0 205.8220 0.000 -236.6170 1.0 1097.0 \n", - "3290 0.0 -53.5856 236.617 0.0000 1.0 1097.0 \n", + " TOTAL ZONE_0 ZONE_1 ZONE_2 ZONE_3 \n", + "0 -2392.103 0.0 0.0000 -1530.422 -40.3993 \n", + "1 -18.075 0.0 1530.4220 0.000 -899.6920 \n", + "2 -36.140 0.0 40.3993 899.692 0.0000 \n", + "3 -1770.999 0.0 0.0000 -1134.937 -38.2835 \n", + "4 -18.493 0.0 1134.9370 0.000 -701.3090 \n", + "... ... ... ... ... ... \n", + "3286 -17.758 0.0 2489.4040 0.000 -1375.6380 \n", + "3287 -35.514 0.0 17.8163 1375.638 0.0000 \n", + "3288 -382.784 0.0 0.0000 -205.822 53.5856 \n", + "3289 -14.931 0.0 205.8220 0.000 -236.6170 \n", + "3290 -29.865 0.0 -53.5856 236.617 0.0000 \n", "\n", - "[3291 rows x 13 columns]" + "[3291 rows x 12 columns]" ] }, "execution_count": 18, @@ -966,11 +989,62 @@ }, { "cell_type": "code", - "execution_count": 19, + "execution_count": 21, "metadata": { "scrolled": false }, "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "c:\\users\\jlarsen\\desktop\\flopy-dev\\flopy\\utils\\zonbud.py:3259: PendingDeprecationWarning: ZoneBudgetOutput.volumetric_flux() will be deprecated in version 3.3.5,\n", + " PendingDeprecationWarning,\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + " totim kper kstp zone CONSTANT_HEAD OTHER_ZONES STORAGE \\\n", + "0 1.0 0 0 1 -821.281900 -1570.821 0.000000 \n", + "1 1.0 0 0 2 -648.804700 630.730 0.000000 \n", + "2 1.0 0 0 3 -976.232200 940.092 0.000000 \n", + "3 2.0 1 0 1 -816.347300 -1173.221 218.568500 \n", + "4 2.0 1 0 2 -643.938700 433.628 191.816342 \n", + "... ... ... ... ... ... ... ... \n", + "3286 1096.0 1095 0 2 -505.116270 1113.766 -626.408120 \n", + "3287 1096.0 1095 0 3 -801.732376 1393.454 -627.235750 \n", + "3288 1097.0 1096 0 1 -230.548300 -152.236 0.000000 \n", + "3289 1097.0 1096 0 2 15.864900 -30.796 0.000000 \n", + "3290 1097.0 1096 0 3 -212.896600 183.031 0.000000 \n", + "\n", + " TOTAL ZONE_0 ZONE_1 ZONE_2 ZONE_3 year \n", + "0 -2392.103 0.0 0.0000 -1530.422 -40.3993 1776 \n", + "1 -18.075 0.0 1530.4220 0.000 -899.6920 1776 \n", + "2 -36.140 0.0 40.3993 899.692 0.0000 1776 \n", + "3 -1770.999 0.0 0.0000 -1134.937 -38.2835 1776 \n", + "4 -18.493 0.0 1134.9370 0.000 -701.3090 1776 \n", + "... ... ... ... ... ... ... \n", + "3286 -17.758 0.0 2489.4040 0.000 -1375.6380 1779 \n", + "3287 -35.514 0.0 17.8163 1375.638 0.0000 1779 \n", + "3288 -382.784 0.0 0.0000 -205.822 53.5856 1779 \n", + "3289 -14.931 0.0 205.8220 0.000 -236.6170 1779 \n", + "3290 -29.865 0.0 -53.5856 236.617 0.0000 1779 \n", + "\n", + "[3291 rows x 13 columns]\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "c:\\users\\jlarsen\\desktop\\flopy-dev\\flopy\\utils\\zonbud.py:3183: PendingDeprecationWarning: ZoneBudgetOutput will be deprecated in version 3.3.5\n", + " PendingDeprecationWarning,\n", + "c:\\users\\jlarsen\\desktop\\flopy-dev\\flopy\\utils\\zonbud.py:3171: PendingDeprecationWarning: ZoneBudgetOutput will be deprecated in version 3.3.5\n", + " PendingDeprecationWarning,\n" + ] + }, { "data": { "text/html": [ @@ -994,12 +1068,12 @@ " \n", " year\n", " zone\n", - " storage\n", - " constant head\n", - " other zones\n", - " zone 1\n", - " zone 2\n", - " zone 3\n", + " STORAGE\n", + " CONSTANT_HEAD\n", + " OTHER_ZONES\n", + " ZONE_1\n", + " ZONE_2\n", + " ZONE_3\n", " totim\n", " \n", " \n", @@ -1153,7 +1227,7 @@ "" ], "text/plain": [ - " year zone storage constant head other zones zone 1 \\\n", + " year zone STORAGE CONSTANT_HEAD OTHER_ZONES ZONE_1 \\\n", "0 1776 1 81203.267170 -134930.451200 -176631.7910 0.0000 \n", "1 1776 2 37268.485533 -102758.917473 62223.7540 172310.4031 \n", "2 1776 3 37296.183058 -158237.438437 114408.0385 4321.3803 \n", @@ -1167,7 +1241,7 @@ "10 1779 2 3241.895778 -95653.391040 88972.3010 227734.8190 \n", "11 1779 3 3233.272516 -152427.866886 142318.8767 3556.3570 \n", "\n", - " zone 2 zone 3 totim \n", + " ZONE_2 ZONE_3 totim \n", "0 -172310.4031 -4321.3803 181.0 \n", "1 0.0000 -110086.6556 181.0 \n", "2 110086.6556 0.0000 181.0 \n", @@ -1182,7 +1256,7 @@ "11 138762.5241 0.0000 1097.0 " ] }, - "execution_count": 19, + "execution_count": 21, "metadata": {}, "output_type": "execute_result" } @@ -1202,11 +1276,11 @@ " year.append(t.year)\n", "\n", "vol_df['year'] = year\n", - " \n", + "print(vol_df)\n", "# calculate yearly volumetric change using pandas\n", "totim_df = vol_df.groupby(['year', 'zone'], as_index=False)['totim'].max()\n", - "yearly = vol_df.groupby(['year', 'zone'], as_index=False)[['storage', 'constant head', 'other zones',\n", - " 'zone 1', 'zone 2', 'zone 3']].sum()\n", + "yearly = vol_df.groupby(['year', 'zone'], as_index=False)[['STORAGE', 'CONSTANT_HEAD', 'OTHER_ZONES',\n", + " 'ZONE_1', 'ZONE_2', 'ZONE_3']].sum()\n", "yearly['totim'] = totim_df['totim']\n", "yearly" ] @@ -1220,30 +1294,28 @@ }, { "cell_type": "code", - "execution_count": 20, + "execution_count": 22, "metadata": {}, "outputs": [ { - "name": "stdout", + "name": "stderr", "output_type": "stream", "text": [ - "initialize_geometry::proj4_str = +proj=longlat +ellps=WGS84 +datum=WGS84 +no_defs\n", - "initialize_geometry::self.grid_crs = +proj=longlat +ellps=WGS84 +datum=WGS84 +no_defs +type=crs\n", - "initialize_geometry::nc_crs = epsg:4326\n", - "transforming coordinates using = proj=noop ellps=GRS80\n" - ] - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "error getting data for cell_by_cell_flowstorage at time 1.0:list index out of range\n" + "c:\\users\\jlarsen\\desktop\\flopy-dev\\flopy\\utils\\zonbud.py:3289: PendingDeprecationWarning: ZoneBudgetOutput will be deprecated in version 3.3.5\n", + " PendingDeprecationWarning,\n", + "c:\\users\\jlarsen\\desktop\\flopy-dev\\flopy\\utils\\zonbud.py:3171: PendingDeprecationWarning: ZoneBudgetOutput will be deprecated in version 3.3.5\n", + " PendingDeprecationWarning,\n" ] }, { "name": "stdout", "output_type": "stream", "text": [ + "initialize_geometry::proj4_str = +proj=longlat +ellps=WGS84 +datum=WGS84 +no_defs\n", + "initialize_geometry::self.grid_crs = +proj=longlat +ellps=WGS84 +datum=WGS84 +no_defs +type=crs\n", + "initialize_geometry::nc_crs = epsg:4326\n", + "transforming coordinates using = proj=noop ellps=GRS80\n", + "error getting data for cell_by_cell_flowstorage at time 1.0:list index out of range\n", "error getting data for cell_by_cell_flowstorage at time 1097.0:list index out of range\n" ] }, @@ -1252,20 +1324,20 @@ "text/plain": [ "\n", "root group (NETCDF4 data model, file format HDF5):\n", - " Conventions: CF-1.6, ACDD-1.3, flopy 3.3.3\n", - " date_created: 2021-02-18T17:31:00Z\n", + " Conventions: CF-1.6, ACDD-1.3, flopy 3.3.4\n", + " date_created: 2021-07-10T00:00:00Z\n", " geospatial_vertical_positive: up\n", " geospatial_vertical_min: -25.0\n", " geospatial_vertical_max: 4.832500457763672\n", " geospatial_vertical_resolution: variable\n", " featureType: Grid\n", " namefile: freyberg.nam\n", - " model_ws: ../data/freyberg_multilayer_transient\n", + " model_ws: ..\\data\\freyberg_multilayer_transient\n", " exe_name: mf2005.exe\n", " modflow_version: mfnwt\n", - " create_hostname: IGSAAAHMLT40179\n", - " create_platform: Darwin\n", - " create_directory: /Users/jdhughes/Documents/Development/flopy_git/flopy_fork/examples/Notebooks\n", + " create_hostname: IGSWCAWWLT6673\n", + " create_platform: Windows\n", + " create_directory: C:\\Users\\jlarsen\\Desktop\\flopy-dev\\examples\\Notebooks\n", " solver_head_tolerance: -999\n", " solver_flux_tolerance: -999\n", " flopy_sr_xll: 123456.7\n", @@ -1278,7 +1350,7 @@ " groups: zonebudget" ] }, - "execution_count": 20, + "execution_count": 22, "metadata": {}, "output_type": "execute_result" } @@ -1296,6 +1368,13 @@ " ml, export_dict)\n", "fnc.nc" ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] } ], "metadata": { @@ -1315,7 +1394,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.8.6" + "version": "3.7.4" } }, "nbformat": 4, diff --git a/examples/Tutorials/modflow6/tutorial101_mf6_output.py b/examples/Tutorials/modflow6/tutorial101_mf6_output.py index 58cc455790..5fdf51b4fb 100644 --- a/examples/Tutorials/modflow6/tutorial101_mf6_output.py +++ b/examples/Tutorials/modflow6/tutorial101_mf6_output.py @@ -1,132 +1,156 @@ -# --- -# jupyter: -# jupytext: -# text_representation: -# extension: .py -# format_name: light -# format_version: '1.5' -# jupytext_version: 1.5.1 -# kernelspec: -# display_name: Python 3 -# language: python -# name: python3 -# --- - -# # MODFLOW 6 Tutorial: Accessing MODFLOW 6 Output -# -# This tutorial shows how to access output from MODFLOW 6 models and packages -# by using the built in `.output` attribute on any MODFLOW 6 model or -# package object - -# ## Package import -import flopy -import os -import platform - -# ## Load a simple demonstration model -exe_name = "mf6" -if platform.system().lower() == 'windows': - exe_name += ".exe" -ws = os.path.abspath(os.path.dirname('')) -if os.path.split(ws)[-1] == "modflow6": - sim_ws = os.path.join(ws, "..", "..", 'data', 'mf6', 'test001e_UZF_3lay') -else: - sim_ws = os.path.join(ws, '..', '..', 'examples', 'data', 'mf6', 'test001e_UZF_3lay') -sim = flopy.mf6.MFSimulation.load(sim_ws=sim_ws, exe_name=exe_name) -sim.run_simulation(silent=True) - -# ## Get output using the `.output` attribute -# The output attribute dynamically generates methods for each package based on -# the available output options within that package. A list of all available -# outputs are: -# -# +-----------------------+------------------------------------------------+ -# | head() | Method to get the `HeadFile` object for the | -# | | model. Accessed from the model object or the | -# | | OC package object | -# +-----------------------+------------------------------------------------+ -# | budget() | Method to get the `CellBudgetFile` object for | -# | | the model. Accessed from the model object or | -# | | the OC package object | -# +-----------------------+------------------------------------------------+ -# | obs() | Method to get observation file data in the | -# | | form of a `MF6Obs` object. Accessed from any | -# | | package that allows observations. | -# +-----------------------+------------------------------------------------+ -# | csv() | Method to get csv output data in the form of a | -# | | `CsvFile` object. Example files are inner and | -# | | outer iteration files from IMS | -# +-----------------------+------------------------------------------------+ -# | package_convergence() | Method to get csv based package convergence | -# | | information from packages such as SFR, LAK, | -# | | UZF, and MAW. Returns a `CsvFile` object | -# +-----------------------+------------------------------------------------+ -# | stage() | Method to get binary stage file output from | -# | | the SFR and LAK packages | -# +-----------------------+------------------------------------------------+ -# | concentration() | Method to get the binary concentration file | -# | | output from a groundwater transport model. | -# | | Accessed from the model object or the OC | -# | | package object | -# +-----------------------+------------------------------------------------+ -# | cim() | Method to get immobile concentration output | -# | | from the CIM package | -# +-----------------------+------------------------------------------------+ -# | density() | Method to get density file output from the | -# | | BUY package | -# +-----------------------+------------------------------------------------+ - -# ## Get head file and cell budget file outputs -# The head file output and cell budget file output can be loaded from either -# the model object or the OC package object. - -ml = sim.get_model("gwf_1") - -bud = ml.output.budget() -bud.get_data(idx=0, full3D=True) - -hds = ml.output.head() -hds.get_data() - -bud = ml.oc.output.budget() -bud.get_data(idx=0, full3D=True) - -hds = ml.oc.output.head() -hds.get_data() - -# ## Get output associated with a specific package -# The `.output` attribute is tied to the package object and allows the user -# to get the output types specified in the MODFLOW 6 package. Here is an -# example with a UZF package that has UZF budget file output, -# package convergence output, and observation data. - -uzf = ml.uzf -uzf_bud = uzf.output.budget() -uzf_bud.get_data(idx=0) - -uzf_conv = uzf.output.package_convergence() -if uzf_conv is not None: - uzf_conv.data[0:10] - -uzf_obs = uzf.output.obs() -uzf_obs.data[0:10] - -# ## Check which output types are available in a package -# The `.output` attribute also has a `methods()` function that returns a list -# of available output functions for a given package. Here are a couple of -# examples - -print("UZF package: ", uzf.output.methods()) -print("Model object: ", ml.output.methods()) -print("OC package: ", ml.oc.output.methods()) -print("DIS package: ", ml.dis.output.methods()) - -# ## Managing multiple observation and csv file outputs in the same package -# For many packages, multiple observation output files can be used. The -# `obs()` and `csv()` functions allow the user to specify a observation file -# or csv file name. If no name is specified, the `obs()` and `csv()` methods -# will return the first file that is listed in the package. - -output = ml.obs[0].output -obs_names = output.obs_names -output.obs(f=obs_names[0]).data[0:10] +# --- +# jupyter: +# jupytext: +# text_representation: +# extension: .py +# format_name: light +# format_version: '1.5' +# jupytext_version: 1.5.1 +# kernelspec: +# display_name: Python 3 +# language: python +# name: python3 +# --- + +# # MODFLOW 6 Tutorial: Accessing MODFLOW 6 Output +# +# This tutorial shows how to access output from MODFLOW 6 models and packages +# by using the built in `.output` attribute on any MODFLOW 6 model or +# package object + +# ## Package import +import flopy +import os +import platform +import numpy as np + +# ## Load a simple demonstration model +exe_name = "mf6" +if platform.system().lower() == 'windows': + exe_name += ".exe" +ws = os.path.abspath(os.path.dirname('')) +if os.path.split(ws)[-1] == "modflow6": + sim_ws = os.path.join(ws, "..", "..", 'data', 'mf6', 'test001e_UZF_3lay') +else: + sim_ws = os.path.join(ws, '..', '..', 'examples', 'data', 'mf6', 'test001e_UZF_3lay') +sim = flopy.mf6.MFSimulation.load(sim_ws=sim_ws, exe_name=exe_name) +sim.run_simulation(silent=True) + +# ## Get output using the `.output` attribute +# The output attribute dynamically generates methods for each package based on +# the available output options within that package. A list of all available +# outputs are: +# +# +-----------------------+------------------------------------------------+ +# | head() | Method to get the `HeadFile` object for the | +# | | model. Accessed from the model object or the | +# | | OC package object | +# +-----------------------+------------------------------------------------+ +# | budget() | Method to get the `CellBudgetFile` object for | +# | | the model. Accessed from the model object or | +# | | the OC package object | +# +-----------------------+------------------------------------------------+ +# | zonebudget() | Method to get the `ZoneBudget6` object for | +# | | the model. Accessed from the model object or | +# | | the OC package object | +# +-----------------------+------------------------------------------------+ +# | obs() | Method to get observation file data in the | +# | | form of a `MF6Obs` object. Accessed from any | +# | | package that allows observations. | +# +-----------------------+------------------------------------------------+ +# | csv() | Method to get csv output data in the form of a | +# | | `CsvFile` object. Example files are inner and | +# | | outer iteration files from IMS | +# +-----------------------+------------------------------------------------+ +# | package_convergence() | Method to get csv based package convergence | +# | | information from packages such as SFR, LAK, | +# | | UZF, and MAW. Returns a `CsvFile` object | +# +-----------------------+------------------------------------------------+ +# | stage() | Method to get binary stage file output from | +# | | the SFR and LAK packages | +# +-----------------------+------------------------------------------------+ +# | concentration() | Method to get the binary concentration file | +# | | output from a groundwater transport model. | +# | | Accessed from the model object or the OC | +# | | package object | +# +-----------------------+------------------------------------------------+ +# | cim() | Method to get immobile concentration output | +# | | from the CIM package | +# +-----------------------+------------------------------------------------+ +# | density() | Method to get density file output from the | +# | | BUY package | +# +-----------------------+------------------------------------------------+ + +# ## Get head file and cell budget file outputs +# The head file output and cell budget file output can be loaded from either +# the model object or the OC package object. + +ml = sim.get_model("gwf_1") + +bud = ml.output.budget() +bud.get_data(idx=0, full3D=True) + +hds = ml.output.head() +hds.get_data() + +bud = ml.oc.output.budget() +bud.get_data(idx=0, full3D=True) + +hds = ml.oc.output.head() +hds.get_data() + +# ## Get output associated with a specific package +# The `.output` attribute is tied to the package object and allows the user +# to get the output types specified in the MODFLOW 6 package. Here is an +# example with a UZF package that has UZF budget file output, +# package convergence output, and observation data. + +uzf = ml.uzf +uzf_bud = uzf.output.budget() +uzf_bud.get_data(idx=0) + +uzf_conv = uzf.output.package_convergence() +if uzf_conv is not None: + uzf_conv.data[0:10] + +uzf_obs = uzf.output.obs() +uzf_obs.data[0:10] + +# ## Check which output types are available in a package +# The `.output` attribute also has a `methods()` function that returns a list +# of available output functions for a given package. Here are a couple of +# examples + +print("UZF package: ", uzf.output.methods()) +print("Model object: ", ml.output.methods()) +print("OC package: ", ml.oc.output.methods()) +print("DIS package: ", ml.dis.output.methods()) + +# ## Managing multiple observation and csv file outputs in the same package +# For many packages, multiple observation output files can be used. The +# `obs()` and `csv()` functions allow the user to specify a observation file +# or csv file name. If no name is specified, the `obs()` and `csv()` methods +# will return the first file that is listed in the package. + +output = ml.obs[0].output +obs_names = output.obs_names +output.obs(f=obs_names[0]).data[0:10] + +# ## Creating and running ZoneBudget for MF6 +# For the model and many packages, zonebudget can be run on the cell budget +# file. The `.output` method allows the user to easily build a ZoneBudget6 +# instance, then run the model, and view output. First we'll build a layered +# zone array, then build and run zonebudget + +zarr = np.ones(ml.modelgrid.shape, dtype=int) +for i in range(1, 4): + zarr[i - 1] *= i + +zonbud = ml.output.zonebudget(zarr) +zonbud.change_model_ws(sim_ws) +zonbud.write_input() +zonbud.run_model() + +df = zonbud.get_dataframes(net=True) +df = df.reset_index() +df diff --git a/examples/data/mf6/test003_gwfs_disv/test003_gwfs_disv.dbf b/examples/data/mf6/test003_gwfs_disv/test003_gwfs_disv.dbf index 2ae4b78faf6e7d0679a86d0eeb12d0c3a74d066c..45f52b02758e7288c3b9d723c8c0e1d832ae7f74 100644 GIT binary patch delta 15 WcmeCZ&)j>TnT5HMopU2g=X(Gxtp%I_ delta 15 WcmeCZ&)j>TnT5H6ReB>!=X(Gx^aZN` diff --git a/flopy/discretization/modeltime.py b/flopy/discretization/modeltime.py index ec8ef11725..83994ea3ed 100644 --- a/flopy/discretization/modeltime.py +++ b/flopy/discretization/modeltime.py @@ -1,3 +1,6 @@ +import numpy as np + + class ModelTime: """ Class for MODFLOW simulation time @@ -49,3 +52,40 @@ def tsmult(self): @property def steady_state(self): return self._steady_state + + @property + def totim(self): + delt = [] + perlen_array = self.perlen + nstp_array = self.nstp + tsmult_array = self.tsmult + for ix, nstp in enumerate(nstp_array): + perlen = perlen_array[ix] + tsmult = tsmult_array[ix] + for stp in range(nstp): + if stp == 0: + if tsmult != 1.0: + dt = perlen * (tsmult - 1) / ((tsmult ** nstp) - 1) + else: + dt = perlen / nstp + else: + dt = delt[-1] * tsmult + delt.append(dt) + + totim = np.add.accumulate(delt) + return totim + + @property + def tslen(self): + n = 0 + tslen = [] + totim = self.totim + for ix, stp in enumerate(self.nstp): + for i in range(stp): + if not tslen: + tslen = [totim[n]] + else: + tslen.append(totim[n] - totim[n - 1]) + n += 1 + + return np.array(tslen) diff --git a/flopy/mf6/utils/binarygrid_util.py b/flopy/mf6/utils/binarygrid_util.py index 6aa562ed46..9a7d9f0b5b 100644 --- a/flopy/mf6/utils/binarygrid_util.py +++ b/flopy/mf6/utils/binarygrid_util.py @@ -65,6 +65,7 @@ def __init__(self, filename, precision="double", verbose=False): self._recorddict = collections.OrderedDict() self._datadict = collections.OrderedDict() self._recordkeys = [] + self.filename = filename if self.verbose: print("\nProcessing binary grid file: {}".format(filename)) diff --git a/flopy/mf6/utils/output_util.py b/flopy/mf6/utils/output_util.py index e10a122a12..4b66b854ec 100644 --- a/flopy/mf6/utils/output_util.py +++ b/flopy/mf6/utils/output_util.py @@ -1,5 +1,5 @@ import os -from ...utils import HeadFile, CellBudgetFile, Mf6Obs +from ...utils import HeadFile, CellBudgetFile, Mf6Obs, ZoneBudget6, ZoneFile6 from ...utils.observationfile import CsvFile from ...pakbase import PackageInterface @@ -21,6 +21,7 @@ def __init__(self, obj): # set initial observation definitions methods = { "budget": self.__budget, + "zonebudget": self.__zonebudget, "obs": self.__obs, "csv": self.__csv, "package_convergence": self.__csv, @@ -62,6 +63,11 @@ def __init__(self, obj): layerfiles[rectype] = data else: setattr(self, rectype, methods[rectype]) + if rectype == "budget": + setattr( + self, "zonebudget", methods["zonebudget"] + ) + self._methods.append("zonebudget()") self._methods.append("{}()".format(rectype)) if rectype == "obs": data = None @@ -176,6 +182,36 @@ def csv_names(self): except AttributeError: return + def __zonebudget(self, izone): + """ + + Returns + ------- + + """ + budget = self.__budget() + grb = None + if budget is not None: + zonbud = ZoneBudget6(model_ws=self._sim_ws) + ZoneFile6(zonbud, izone) + zonbud.bud = budget + try: + if self._obj.model_or_sim.model_type == "gwf": + if self._obj.package_type == "oc": + dis = self._obj.model_or_sim.dis + if ( + dis.blocks["options"].datasets["nogrb"].array + is None + ): + grb = os.path.join( + self._sim_ws, dis.filename + ".grb" + ) + except AttributeError: + pass + + zonbud.grb = grb + return zonbud + def __budget(self, precision="double"): """ Convenience method to open and return a budget object diff --git a/flopy/utils/__init__.py b/flopy/utils/__init__.py index 3ef8f743ec..211c0c3b2d 100644 --- a/flopy/utils/__init__.py +++ b/flopy/utils/__init__.py @@ -55,6 +55,8 @@ ZoneBudget, read_zbarray, write_zbarray, + ZoneFile6, + ZoneBudget6, ZoneBudgetOutput, ZBNetOutput, ) diff --git a/flopy/utils/zonbud.py b/flopy/utils/zonbud.py index 8fbed5fd33..3d1143b9c3 100644 --- a/flopy/utils/zonbud.py +++ b/flopy/utils/zonbud.py @@ -1,2526 +1,3351 @@ -import os -import copy -import numpy as np -import warnings -from .binaryfile import CellBudgetFile -from itertools import groupby -from collections import OrderedDict -from ..utils.utils_def import totim_to_datetime - - -class ZoneBudget: - """ - ZoneBudget class - - Parameters - ---------- - cbc_file : str or CellBudgetFile object - The file name or CellBudgetFile object for which budgets will be - computed. - z : ndarray - The array containing to zones to be used. - kstpkper : tuple of ints - A tuple containing the time step and stress period (kstp, kper). - The kstp and kper values are zero based. - totim : float - The simulation time. - aliases : dict - A dictionary with key, value pairs of zones and aliases. Replaces - the corresponding record and field names with the aliases provided. - When using this option in conjunction with a list of zones, the - zone(s) passed may either be all strings (aliases), all integers, - or mixed. - - Returns - ------- - None - - Examples - -------- - - >>> from flopy.utils.zonbud import ZoneBudget, read_zbarray - >>> zon = read_zbarray('zone_input_file') - >>> zb = ZoneBudget('zonebudtest.cbc', zon, kstpkper=(0, 0)) - >>> zb.to_csv('zonebudtest.csv') - >>> zb_mgd = zb * 7.48052 / 1000000 - """ - - def __init__( - self, - cbc_file, - z, - kstpkper=None, - totim=None, - aliases=None, - verbose=False, - **kwargs - ): - - if isinstance(cbc_file, CellBudgetFile): - self.cbc = cbc_file - elif isinstance(cbc_file, str) and os.path.isfile(cbc_file): - self.cbc = CellBudgetFile(cbc_file) - else: - raise Exception( - "Cannot load cell budget file: {}.".format(cbc_file) - ) - - if isinstance(z, np.ndarray): - assert np.issubdtype( - z.dtype, np.integer - ), "Zones dtype must be integer" - else: - e = ( - "Please pass zones as a numpy ndarray of (positive)" - " integers. {}".format(z.dtype) - ) - raise Exception(e) - - # Check for negative zone values - if np.any(z < 0): - raise Exception( - "Negative zone value(s) found:", np.unique(z[z < 0]) - ) - - self.dis = None - if "model" in kwargs.keys(): - self.model = kwargs.pop("model") - self.dis = self.model.dis - if "dis" in kwargs.keys(): - self.dis = kwargs.pop("dis") - if "sr" in kwargs.keys(): - kwargs.pop("sr") - warnings.warn("ignoring 'sr' parameter") - if len(kwargs.keys()) > 0: - args = ",".join(kwargs.keys()) - raise Exception("LayerFile error: unrecognized kwargs: " + args) - - # Check the shape of the cbc budget file arrays - self.cbc_shape = self.cbc.get_data(idx=0, full3D=True)[0].shape - self.nlay, self.nrow, self.ncol = self.cbc_shape - self.cbc_times = self.cbc.get_times() - self.cbc_kstpkper = self.cbc.get_kstpkper() - self.kstpkper = None - self.totim = None - - if kstpkper is not None: - if isinstance(kstpkper, tuple): - kstpkper = [kstpkper] - for kk in kstpkper: - s = ( - "The specified time step/stress period " - "does not exist {}".format(kk) - ) - assert kk in self.cbc.get_kstpkper(), s - self.kstpkper = kstpkper - elif totim is not None: - if isinstance(totim, float): - totim = [totim] - elif isinstance(totim, int): - totim = [float(totim)] - for t in totim: - s = ( - "The specified simulation time " - "does not exist {}".format(t) - ) - assert t in self.cbc.get_times(), s - self.totim = totim - else: - # No time step/stress period or simulation time pass - self.kstpkper = self.cbc.get_kstpkper() - - # Set float and integer types - self.float_type = np.float32 - self.int_type = np.int32 - - # Check dimensions of input zone array - s = ( - "Row/col dimensions of zone array {}" - " do not match model row/col dimensions {}".format( - z.shape, self.cbc_shape - ) - ) - assert z.shape[-2] == self.nrow and z.shape[-1] == self.ncol, s - - if z.shape == self.cbc_shape: - izone = z.copy() - elif len(z.shape) == 2: - izone = np.zeros(self.cbc_shape, self.int_type) - izone[:] = z[:, :] - elif len(z.shape) == 3 and z.shape[0] == 1: - izone = np.zeros(self.cbc_shape, self.int_type) - izone[:] = z[0, :, :] - else: - e = "Shape of the zone array is not recognized: {}".format(z.shape) - raise Exception(e) - - self.izone = izone - self.allzones = np.unique(izone) - self._zonenamedict = OrderedDict( - [(z, "ZONE_{}".format(z)) for z in self.allzones] - ) - - if aliases is not None: - s = ( - "Input aliases not recognized. Please pass a dictionary " - "with key,value pairs of zone/alias." - ) - assert isinstance(aliases, dict), s - # Replace the relevant field names (ignore zone 0) - seen = [] - for z, a in iter(aliases.items()): - if z != 0 and z in self._zonenamedict.keys(): - if z in seen: - raise Exception( - "Zones may not have more than 1 alias." - ) - self._zonenamedict[z] = "_".join(a.split()) - seen.append(z) - - # self._iflow_recnames = self._get_internal_flow_record_names() - - # All record names in the cell-by-cell budget binary file - self.record_names = [ - n.strip() for n in self.cbc.get_unique_record_names(decode=True) - ] - - # Get imeth for each record in the CellBudgetFile record list - self.imeth = {} - for record in self.cbc.recordarray: - self.imeth[record["text"].strip().decode("utf-8")] = record[ - "imeth" - ] - - # INTERNAL FLOW TERMS ARE USED TO CALCULATE FLOW BETWEEN ZONES. - # CONSTANT-HEAD TERMS ARE USED TO IDENTIFY WHERE CONSTANT-HEAD CELLS - # ARE AND THEN USE FACE FLOWS TO DETERMINE THE AMOUNT OF FLOW. - # SWIADDTO--- terms are used by the SWI2 groundwater flow process. - internal_flow_terms = [ - "CONSTANT HEAD", - "FLOW RIGHT FACE", - "FLOW FRONT FACE", - "FLOW LOWER FACE", - "SWIADDTOCH", - "SWIADDTOFRF", - "SWIADDTOFFF", - "SWIADDTOFLF", - ] - - # Source/sink/storage term record names - # These are all of the terms that are not related to constant - # head cells or face flow terms - self.ssst_record_names = [ - n for n in self.record_names if n not in internal_flow_terms - ] - - # Initialize budget recordarray - array_list = [] - if self.kstpkper is not None: - for kk in self.kstpkper: - recordarray = self._initialize_budget_recordarray( - kstpkper=kk, totim=None - ) - array_list.append(recordarray) - elif self.totim is not None: - for t in self.totim: - recordarray = self._initialize_budget_recordarray( - kstpkper=None, totim=t - ) - array_list.append(recordarray) - self._budget = np.concatenate(array_list, axis=0) - - # Update budget record array - if self.kstpkper is not None: - for kk in self.kstpkper: - if verbose: - s = ( - "Computing the budget for" - " time step {} in stress period {}".format( - kk[0] + 1, kk[1] + 1 - ) - ) - print(s) - self._compute_budget(kstpkper=kk) - elif self.totim is not None: - for t in self.totim: - if verbose: - s = "Computing the budget for time {}".format(t) - print(s) - self._compute_budget(totim=t) - - return - - def get_model_shape(self): - """Get model shape - - Returns - ------- - nlay : int - Number of layers - nrow : int - Number of rows - ncol : int - Number of columns - - """ - return self.nlay, self.nrow, self.ncol - - def get_record_names(self, stripped=False): - """ - Get a list of water budget record names in the file. - - Returns - ------- - out : list of strings - List of unique text names in the binary file. - - Examples - -------- - - >>> zb = ZoneBudget('zonebudtest.cbc', zon, kstpkper=(0, 0)) - >>> recnames = zb.get_record_names() - - """ - if not stripped: - return np.unique(self._budget["name"]) - else: - seen = [] - for recname in self.get_record_names(): - if recname in ["IN-OUT", "TOTAL_IN", "TOTAL_OUT"]: - continue - if recname.endswith("_IN"): - recname = recname[:-3] - elif recname.endswith("_OUT"): - recname = recname[:-4] - if recname not in seen: - seen.append(recname) - seen.extend(["IN-OUT", "TOTAL"]) - return np.array(seen) - - def get_budget(self, names=None, zones=None, net=False): - """ - Get a list of zonebudget record arrays. - - Parameters - ---------- - - names : list of strings - A list of strings containing the names of the records desired. - zones : list of ints or strings - A list of integer zone numbers or zone names desired. - net : boolean - If True, returns net IN-OUT for each record. - - Returns - ------- - budget_list : list of record arrays - A list of the zonebudget record arrays. - - Examples - -------- - - >>> names = ['FROM_CONSTANT_HEAD', 'RIVER_LEAKAGE_OUT'] - >>> zones = ['ZONE_1', 'ZONE_2'] - >>> zb = ZoneBudget('zonebudtest.cbc', zon, kstpkper=(0, 0)) - >>> bud = zb.get_budget(names=names, zones=zones) - - """ - if isinstance(names, str): - names = [names] - if isinstance(zones, str): - zones = [zones] - elif isinstance(zones, int): - zones = [zones] - select_fields = ["totim", "time_step", "stress_period", "name"] + list( - self._zonenamedict.values() - ) - select_records = np.where( - (self._budget["name"] == self._budget["name"]) - ) - if zones is not None: - for idx, z in enumerate(zones): - if isinstance(z, int): - zones[idx] = self._zonenamedict[z] - select_fields = [ - "totim", - "time_step", - "stress_period", - "name", - ] + zones - if names is not None: - names = self._clean_budget_names(names) - select_records = np.in1d(self._budget["name"], names) - if net: - if names is None: - names = self._clean_budget_names(self.get_record_names()) - net_budget = self._compute_net_budget() - seen = [] - net_names = [] - for name in names: - iname = "_".join(name.split("_")[1:]) - if iname not in seen: - seen.append(iname) - else: - net_names.append(iname) - select_records = np.in1d(net_budget["name"], net_names) - return net_budget[select_fields][select_records] - else: - return self._budget[select_fields][select_records] - - def to_csv(self, fname): - """ - Saves the budget record arrays to a formatted - comma-separated values file. - - Parameters - ---------- - fname : str - The name of the output comma-separated values file. - - Returns - ------- - None - - """ - # Needs updating to handle the new budget list structure. Write out - # budgets for all kstpkper if kstpkper is None or pass list of - # kstpkper/totim to save particular budgets. - with open(fname, "w") as f: - # Write header - f.write(",".join(self._budget.dtype.names) + "\n") - # Write rows - for rowidx in range(self._budget.shape[0]): - s = ( - ",".join([str(i) for i in list(self._budget[:][rowidx])]) - + "\n" - ) - f.write(s) - return - - def get_dataframes( - self, - start_datetime=None, - timeunit="D", - index_key="totim", - names=None, - zones=None, - net=False, - ): - """ - Get pandas dataframes. - - Parameters - ---------- - - start_datetime : str - Datetime string indicating the time at which the simulation starts. - timeunit : str - String that indicates the time units used in the model. - index_key : str - Indicates the fields to be used (in addition to "record") in the - resulting DataFrame multi-index. - names : list of strings - A list of strings containing the names of the records desired. - zones : list of ints or strings - A list of integer zone numbers or zone names desired. - net : boolean - If True, returns net IN-OUT for each record. - - Returns - ------- - df : Pandas DataFrame - Pandas DataFrame with the budget information. - - Examples - -------- - >>> from flopy.utils.zonbud import ZoneBudget, read_zbarray - >>> zon = read_zbarray('zone_input_file') - >>> zb = ZoneBudget('zonebudtest.cbc', zon, kstpkper=(0, 0)) - >>> df = zb.get_dataframes() - - """ - try: - import pandas as pd - except Exception as e: - msg = "ZoneBudget.get_dataframes() error import pandas: " + str(e) - raise ImportError(msg) - - valid_index_keys = ["totim", "kstpkper"] - s = 'index_key "{}" is not valid.'.format(index_key) - assert index_key in valid_index_keys, s - - valid_timeunit = ["S", "M", "H", "D", "Y"] - - if timeunit.upper() == "SECONDS": - timeunit = "S" - elif timeunit.upper() == "MINUTES": - timeunit = "M" - elif timeunit.upper() == "HOURS": - timeunit = "H" - elif timeunit.upper() == "DAYS": - timeunit = "D" - elif timeunit.upper() == "YEARS": - timeunit = "Y" - - errmsg = ( - "Specified time units ({}) not recognized. " - "Please use one of ".format(timeunit) - ) - assert timeunit in valid_timeunit, ( - errmsg + ", ".join(valid_timeunit) + "." - ) - - df = pd.DataFrame().from_records(self.get_budget(names, zones, net)) - if start_datetime is not None: - totim = totim_to_datetime( - df.totim, - start=pd.to_datetime(start_datetime), - timeunit=timeunit, - ) - df["datetime"] = totim - index_cols = ["datetime", "name"] - else: - if index_key == "totim": - index_cols = ["totim", "name"] - elif index_key == "kstpkper": - index_cols = ["time_step", "stress_period", "name"] - df = df.set_index(index_cols) # .sort_index(level=0) - if zones is not None: - keep_cols = zones - else: - keep_cols = self._zonenamedict.values() - return df.loc[:, keep_cols] - - def copy(self): - """ - Return a deepcopy of the object. - """ - return copy.deepcopy(self) - - def __deepcopy__(self, memo): - """ - Over-rides the default deepcopy behavior. Copy all attributes except - the CellBudgetFile object which does not copy nicely. - """ - cls = self.__class__ - result = cls.__new__(cls) - memo[id(self)] = result - ignore_attrs = ["cbc"] - for k, v in self.__dict__.items(): - if k not in ignore_attrs: - setattr(result, k, copy.deepcopy(v, memo)) - - # Set CellBudgetFile object attribute manually. This is object - # read-only so should not be problems with pointers from - # multiple objects. - result.cbc = self.cbc - return result - - def _compute_budget(self, kstpkper=None, totim=None): - """ - Creates a budget for the specified zone array. This function only - supports the use of a single time step/stress period or time. - - Parameters - ---------- - kstpkper : tuple - Tuple of kstp and kper to compute budget for (default is None). - totim : float - Totim to compute budget for (default is None). - - Returns - ------- - None - - """ - # Initialize an array to track where the constant head cells - # are located. - ich = np.zeros(self.cbc_shape, self.int_type) - swiich = np.zeros(self.cbc_shape, self.int_type) - - if "CONSTANT HEAD" in self.record_names: - """ - C-----CONSTANT-HEAD FLOW -- DON'T ACCUMULATE THE CELL-BY-CELL VALUES FOR - C-----CONSTANT-HEAD FLOW BECAUSE THEY MAY INCLUDE PARTIALLY CANCELING - C-----INS AND OUTS. USE CONSTANT-HEAD TERM TO IDENTIFY WHERE CONSTANT- - C-----HEAD CELLS ARE AND THEN USE FACE FLOWS TO DETERMINE THE AMOUNT OF - C-----FLOW. STORE CONSTANT-HEAD LOCATIONS IN ICH ARRAY. - """ - chd = self.cbc.get_data( - text="CONSTANT HEAD", - full3D=True, - kstpkper=kstpkper, - totim=totim, - )[0] - ich[np.ma.where(chd != 0.0)] = 1 - if "FLOW RIGHT FACE" in self.record_names: - self._accumulate_flow_frf("FLOW RIGHT FACE", ich, kstpkper, totim) - if "FLOW FRONT FACE" in self.record_names: - self._accumulate_flow_fff("FLOW FRONT FACE", ich, kstpkper, totim) - if "FLOW LOWER FACE" in self.record_names: - self._accumulate_flow_flf("FLOW LOWER FACE", ich, kstpkper, totim) - if "SWIADDTOCH" in self.record_names: - swichd = self.cbc.get_data( - text="SWIADDTOCH", full3D=True, kstpkper=kstpkper, totim=totim - )[0] - swiich[swichd != 0] = 1 - if "SWIADDTOFRF" in self.record_names: - self._accumulate_flow_frf("SWIADDTOFRF", swiich, kstpkper, totim) - if "SWIADDTOFFF" in self.record_names: - self._accumulate_flow_fff("SWIADDTOFFF", swiich, kstpkper, totim) - if "SWIADDTOFLF" in self.record_names: - self._accumulate_flow_flf("SWIADDTOFLF", swiich, kstpkper, totim) - - # NOT AN INTERNAL FLOW TERM, SO MUST BE A SOURCE TERM OR STORAGE - # ACCUMULATE THE FLOW BY ZONE - # iterate over remaining items in the list - for recname in self.ssst_record_names: - self._accumulate_flow_ssst(recname, kstpkper, totim) - - # Compute mass balance terms - self._compute_mass_balance(kstpkper, totim) - - return - - def _add_empty_record( - self, recordarray, recname, kstpkper=None, totim=None - ): - """ - Build an empty records based on the specified flow direction and - record name for the given list of zones. - - Parameters - ---------- - recordarray : - recname : - kstpkper : tuple - Tuple of kstp and kper to compute budget for (default is None). - totim : float - Totim to compute budget for (default is None). - - Returns - ------- - recordarray : np.recarray - - """ - if kstpkper is not None: - if len(self.cbc_times) > 0: - totim = self.cbc_times[self.cbc_kstpkper.index(kstpkper)] - else: - totim = 0.0 - elif totim is not None: - if len(self.cbc_times) > 0: - kstpkper = self.cbc_kstpkper[self.cbc_times.index(totim)] - else: - kstpkper = (0, 0) - - row = [totim, kstpkper[0], kstpkper[1], recname] - row += [0.0 for _ in self._zonenamedict.values()] - recs = np.array(tuple(row), dtype=recordarray.dtype) - recordarray = np.append(recordarray, recs) - return recordarray - - def _initialize_budget_recordarray(self, kstpkper=None, totim=None): - """ - Initialize the budget record array which will store all of the - fluxes in the cell-budget file. - - Parameters - ---------- - kstpkper : tuple - Tuple of kstp and kper to compute budget for (default is None). - totim : float - Totim to compute budget for (default is None). - - Returns - ------- - - """ - - # Create empty array for the budget terms. - dtype_list = [ - ("totim", "= 2: - data = self.cbc.get_data( - text=recname, kstpkper=kstpkper, totim=totim - )[0] - - # "FLOW RIGHT FACE" COMPUTE FLOW BETWEEN ZONES ACROSS COLUMNS. - # COMPUTE FLOW ONLY BETWEEN A ZONE AND A HIGHER ZONE -- FLOW FROM - # ZONE 4 TO 3 IS THE NEGATIVE OF FLOW FROM 3 TO 4. - # 1ST, CALCULATE FLOW BETWEEN NODE J,I,K AND J-1,I,K - - k, i, j = np.where( - self.izone[:, :, 1:] > self.izone[:, :, :-1] - ) - - # Adjust column values to account for the starting position of "nz" - j += 1 - - # Define the zone to which flow is going - nz = self.izone[k, i, j] - - # Define the zone from which flow is coming - jl = j - 1 - nzl = self.izone[k, i, jl] - - # Get the face flow - q = data[k, i, jl] - - # Get indices where flow face values are positive (flow out of higher zone) - # Don't include CH to CH flow (can occur if CHTOCH option is used) - # Create an iterable tuple of (from zone, to zone, flux) - # Then group tuple by (from_zone, to_zone) and sum the flux values - idx = np.where( - (q > 0) & ((ich[k, i, j] != 1) | (ich[k, i, jl] != 1)) - ) - fzi, tzi, fi = sum_flux_tuples(nzl[idx], nz[idx], q[idx]) - self._update_budget_fromfaceflow( - fzi, tzi, np.abs(fi), kstpkper, totim - ) - - # Get indices where flow face values are negative (flow into higher zone) - # Don't include CH to CH flow (can occur if CHTOCH option is used) - # Create an iterable tuple of (from zone, to zone, flux) - # Then group tuple by (from_zone, to_zone) and sum the flux values - idx = np.where( - (q < 0) & ((ich[k, i, j] != 1) | (ich[k, i, jl] != 1)) - ) - fzi, tzi, fi = sum_flux_tuples(nz[idx], nzl[idx], q[idx]) - self._update_budget_fromfaceflow( - fzi, tzi, np.abs(fi), kstpkper, totim - ) - - # FLOW BETWEEN NODE J,I,K AND J+1,I,K - k, i, j = np.where( - self.izone[:, :, :-1] > self.izone[:, :, 1:] - ) - - # Define the zone from which flow is coming - nz = self.izone[k, i, j] - - # Define the zone to which flow is going - jr = j + 1 - nzr = self.izone[k, i, jr] - - # Get the face flow - q = data[k, i, j] - - # Get indices where flow face values are positive (flow out of higher zone) - # Don't include CH to CH flow (can occur if CHTOCH option is used) - # Create an iterable tuple of (from zone, to zone, flux) - # Then group tuple by (from_zone, to_zone) and sum the flux values - idx = np.where( - (q > 0) & ((ich[k, i, j] != 1) | (ich[k, i, jr] != 1)) - ) - fzi, tzi, fi = sum_flux_tuples(nz[idx], nzr[idx], q[idx]) - self._update_budget_fromfaceflow( - fzi, tzi, np.abs(fi), kstpkper, totim - ) - - # Get indices where flow face values are negative (flow into higher zone) - # Don't include CH to CH flow (can occur if CHTOCH option is used) - # Create an iterable tuple of (from zone, to zone, flux) - # Then group tuple by (from_zone, to_zone) and sum the flux values - idx = np.where( - (q < 0) & ((ich[k, i, j] != 1) | (ich[k, i, jr] != 1)) - ) - fzi, tzi, fi = sum_flux_tuples(nzr[idx], nz[idx], q[idx]) - self._update_budget_fromfaceflow( - fzi, tzi, np.abs(fi), kstpkper, totim - ) - - # CALCULATE FLOW TO CONSTANT-HEAD CELLS IN THIS DIRECTION - k, i, j = np.where(ich == 1) - k, i, j = k[j > 0], i[j > 0], j[j > 0] - jl = j - 1 - nzl = self.izone[k, i, jl] - nz = self.izone[k, i, j] - q = data[k, i, jl] - idx = np.where( - (q > 0) & ((ich[k, i, j] != 1) | (ich[k, i, jl] != 1)) - ) - fzi, tzi, f = sum_flux_tuples(nzl[idx], nz[idx], q[idx]) - fz = ["TO_CONSTANT_HEAD"] * len(tzi) - tz = [self._zonenamedict[z] for z in tzi] - self._update_budget_fromssst( - fz, tz, np.abs(f), kstpkper, totim - ) - - idx = np.where( - (q < 0) & ((ich[k, i, j] != 1) | (ich[k, i, jl] != 1)) - ) - fzi, tzi, f = sum_flux_tuples(nzl[idx], nz[idx], q[idx]) - fz = ["FROM_CONSTANT_HEAD"] * len(fzi) - tz = [self._zonenamedict[z] for z in tzi[tzi != 0]] - self._update_budget_fromssst( - fz, tz, np.abs(f), kstpkper, totim - ) - - k, i, j = np.where(ich == 1) - k, i, j = ( - k[j < self.ncol - 1], - i[j < self.ncol - 1], - j[j < self.ncol - 1], - ) - nz = self.izone[k, i, j] - jr = j + 1 - nzr = self.izone[k, i, jr] - q = data[k, i, j] - idx = np.where( - (q > 0) & ((ich[k, i, j] != 1) | (ich[k, i, jr] != 1)) - ) - fzi, tzi, f = sum_flux_tuples(nzr[idx], nz[idx], q[idx]) - fz = ["FROM_CONSTANT_HEAD"] * len(tzi) - tz = [self._zonenamedict[z] for z in tzi] - self._update_budget_fromssst( - fz, tz, np.abs(f), kstpkper, totim - ) - - idx = np.where( - (q < 0) & ((ich[k, i, j] != 1) | (ich[k, i, jr] != 1)) - ) - fzi, tzi, f = sum_flux_tuples(nzr[idx], nz[idx], q[idx]) - fz = ["TO_CONSTANT_HEAD"] * len(fzi) - tz = [self._zonenamedict[z] for z in tzi] - self._update_budget_fromssst( - fz, tz, np.abs(f), kstpkper, totim - ) - - except Exception as e: - print(e) - raise - return - - def _accumulate_flow_fff(self, recname, ich, kstpkper, totim): - """ - - Parameters - ---------- - recname - ich - kstpkper - totim - - Returns - ------- - - """ - try: - if self.nrow >= 2: - data = self.cbc.get_data( - text=recname, kstpkper=kstpkper, totim=totim - )[0] - - # "FLOW FRONT FACE" - # CALCULATE FLOW BETWEEN NODE J,I,K AND J,I-1,K - k, i, j = np.where( - self.izone[:, 1:, :] < self.izone[:, :-1, :] - ) - i += 1 - ia = i - 1 - nza = self.izone[k, ia, j] - nz = self.izone[k, i, j] - q = data[k, ia, j] - idx = np.where( - (q > 0) & ((ich[k, i, j] != 1) | (ich[k, ia, j] != 1)) - ) - fzi, tzi, fi = sum_flux_tuples(nza[idx], nz[idx], q[idx]) - self._update_budget_fromfaceflow( - fzi, tzi, np.abs(fi), kstpkper, totim - ) - - idx = np.where( - (q < 0) & ((ich[k, i, j] != 1) | (ich[k, ia, j] != 1)) - ) - fzi, tzi, fi = sum_flux_tuples(nz[idx], nza[idx], q[idx]) - self._update_budget_fromfaceflow( - fzi, tzi, np.abs(fi), kstpkper, totim - ) - - # CALCULATE FLOW BETWEEN NODE J,I,K AND J,I+1,K. - k, i, j = np.where( - self.izone[:, :-1, :] < self.izone[:, 1:, :] - ) - nz = self.izone[k, i, j] - ib = i + 1 - nzb = self.izone[k, ib, j] - q = data[k, i, j] - idx = np.where( - (q > 0) & ((ich[k, i, j] != 1) | (ich[k, ib, j] != 1)) - ) - fzi, tzi, fi = sum_flux_tuples(nz[idx], nzb[idx], q[idx]) - self._update_budget_fromfaceflow( - fzi, tzi, np.abs(fi), kstpkper, totim - ) - - idx = np.where( - (q < 0) & ((ich[k, i, j] != 1) | (ich[k, ib, j] != 1)) - ) - fzi, tzi, fi = sum_flux_tuples(nzb[idx], nz[idx], q[idx]) - self._update_budget_fromfaceflow( - fzi, tzi, np.abs(fi), kstpkper, totim - ) - - # CALCULATE FLOW TO CONSTANT-HEAD CELLS IN THIS DIRECTION - k, i, j = np.where(ich == 1) - k, i, j = k[i > 0], i[i > 0], j[i > 0] - ia = i - 1 - nza = self.izone[k, ia, j] - nz = self.izone[k, i, j] - q = data[k, ia, j] - idx = np.where( - (q > 0) & ((ich[k, i, j] != 1) | (ich[k, ia, j] != 1)) - ) - fzi, tzi, f = sum_flux_tuples(nza[idx], nz[idx], q[idx]) - fz = ["TO_CONSTANT_HEAD"] * len(tzi) - tz = [self._zonenamedict[z] for z in tzi] - self._update_budget_fromssst( - fz, tz, np.abs(f), kstpkper, totim - ) - - idx = np.where( - (q < 0) & ((ich[k, i, j] != 1) | (ich[k, ia, j] != 1)) - ) - fzi, tzi, f = sum_flux_tuples(nza[idx], nz[idx], q[idx]) - fz = ["FROM_CONSTANT_HEAD"] * len(fzi) - tz = [self._zonenamedict[z] for z in tzi] - self._update_budget_fromssst( - fz, tz, np.abs(f), kstpkper, totim - ) - - k, i, j = np.where(ich == 1) - k, i, j = ( - k[i < self.nrow - 1], - i[i < self.nrow - 1], - j[i < self.nrow - 1], - ) - nz = self.izone[k, i, j] - ib = i + 1 - nzb = self.izone[k, ib, j] - q = data[k, i, j] - idx = np.where( - (q > 0) & ((ich[k, i, j] != 1) | (ich[k, ib, j] != 1)) - ) - fzi, tzi, f = sum_flux_tuples(nzb[idx], nz[idx], q[idx]) - fz = ["FROM_CONSTANT_HEAD"] * len(tzi) - tz = [self._zonenamedict[z] for z in tzi] - self._update_budget_fromssst( - fz, tz, np.abs(f), kstpkper, totim - ) - - idx = np.where( - (q < 0) & ((ich[k, i, j] != 1) | (ich[k, ib, j] != 1)) - ) - fzi, tzi, f = sum_flux_tuples(nzb[idx], nz[idx], q[idx]) - fz = ["TO_CONSTANT_HEAD"] * len(fzi) - tz = [self._zonenamedict[z] for z in tzi] - self._update_budget_fromssst( - fz, tz, np.abs(f), kstpkper, totim - ) - - except Exception as e: - print(e) - raise - return - - def _accumulate_flow_flf(self, recname, ich, kstpkper, totim): - """ - - Parameters - ---------- - recname - ich - kstpkper - totim - - Returns - ------- - - """ - try: - if self.nlay >= 2: - data = self.cbc.get_data( - text=recname, kstpkper=kstpkper, totim=totim - )[0] - - # "FLOW LOWER FACE" - # CALCULATE FLOW BETWEEN NODE J,I,K AND J,I,K-1 - k, i, j = np.where( - self.izone[1:, :, :] < self.izone[:-1, :, :] - ) - k += 1 - ka = k - 1 - nza = self.izone[ka, i, j] - nz = self.izone[k, i, j] - q = data[ka, i, j] - idx = np.where( - (q > 0) & ((ich[k, i, j] != 1) | (ich[ka, i, j] != 1)) - ) - fzi, tzi, fi = sum_flux_tuples(nza[idx], nz[idx], q[idx]) - self._update_budget_fromfaceflow( - fzi, tzi, np.abs(fi), kstpkper, totim - ) - - idx = np.where( - (q < 0) & ((ich[k, i, j] != 1) | (ich[ka, i, j] != 1)) - ) - fzi, tzi, fi = sum_flux_tuples(nz[idx], nza[idx], q[idx]) - self._update_budget_fromfaceflow( - fzi, tzi, np.abs(fi), kstpkper, totim - ) - - # CALCULATE FLOW BETWEEN NODE J,I,K AND J,I,K+1 - k, i, j = np.where( - self.izone[:-1, :, :] < self.izone[1:, :, :] - ) - nz = self.izone[k, i, j] - kb = k + 1 - nzb = self.izone[kb, i, j] - q = data[k, i, j] - idx = np.where( - (q > 0) & ((ich[k, i, j] != 1) | (ich[kb, i, j] != 1)) - ) - fzi, tzi, fi = sum_flux_tuples(nz[idx], nzb[idx], q[idx]) - self._update_budget_fromfaceflow( - fzi, tzi, np.abs(fi), kstpkper, totim - ) - - idx = np.where( - (q < 0) & ((ich[k, i, j] != 1) | (ich[kb, i, j] != 1)) - ) - fzi, tzi, fi = sum_flux_tuples(nzb[idx], nz[idx], q[idx]) - self._update_budget_fromfaceflow( - fzi, tzi, np.abs(fi), kstpkper, totim - ) - - # CALCULATE FLOW TO CONSTANT-HEAD CELLS IN THIS DIRECTION - k, i, j = np.where(ich == 1) - k, i, j = k[k > 0], i[k > 0], j[k > 0] - ka = k - 1 - nza = self.izone[ka, i, j] - nz = self.izone[k, i, j] - q = data[ka, i, j] - idx = np.where( - (q > 0) & ((ich[k, i, j] != 1) | (ich[ka, i, j] != 1)) - ) - fzi, tzi, f = sum_flux_tuples(nza[idx], nz[idx], q[idx]) - fz = ["TO_CONSTANT_HEAD"] * len(tzi) - tz = [self._zonenamedict[z] for z in tzi] - self._update_budget_fromssst( - fz, tz, np.abs(f), kstpkper, totim - ) - - idx = np.where( - (q < 0) & ((ich[k, i, j] != 1) | (ich[ka, i, j] != 1)) - ) - fzi, tzi, f = sum_flux_tuples(nza[idx], nz[idx], q[idx]) - fz = ["FROM_CONSTANT_HEAD"] * len(fzi) - tz = [self._zonenamedict[z] for z in tzi] - self._update_budget_fromssst( - fz, tz, np.abs(f), kstpkper, totim - ) - - k, i, j = np.where(ich == 1) - k, i, j = ( - k[k < self.nlay - 1], - i[k < self.nlay - 1], - j[k < self.nlay - 1], - ) - nz = self.izone[k, i, j] - kb = k + 1 - nzb = self.izone[kb, i, j] - q = data[k, i, j] - idx = np.where( - (q > 0) & ((ich[k, i, j] != 1) | (ich[kb, i, j] != 1)) - ) - fzi, tzi, f = sum_flux_tuples(nzb[idx], nz[idx], q[idx]) - fz = ["FROM_CONSTANT_HEAD"] * len(tzi) - tz = [self._zonenamedict[z] for z in tzi] - self._update_budget_fromssst( - fz, tz, np.abs(f), kstpkper, totim - ) - - idx = np.where( - (q < 0) & ((ich[k, i, j] != 1) | (ich[kb, i, j] != 1)) - ) - fzi, tzi, f = sum_flux_tuples(nzb[idx], nz[idx], q[idx]) - fz = ["TO_CONSTANT_HEAD"] * len(fzi) - tz = [self._zonenamedict[z] for z in tzi] - self._update_budget_fromssst( - fz, tz, np.abs(f), kstpkper, totim - ) - - except Exception as e: - print(e) - raise - return - - def _accumulate_flow_ssst(self, recname, kstpkper, totim): - - # NOT AN INTERNAL FLOW TERM, SO MUST BE A SOURCE TERM OR STORAGE - # ACCUMULATE THE FLOW BY ZONE - - imeth = self.imeth[recname] - - data = self.cbc.get_data(text=recname, kstpkper=kstpkper, totim=totim) - if len(data) == 0: - # Empty data, can occur during the first time step of a transient - # model when storage terms are zero and not in the cell-budget - # file. - return - else: - data = data[0] - - if imeth == 2 or imeth == 5: - # LIST - qin = np.ma.zeros( - (self.nlay * self.nrow * self.ncol), self.float_type - ) - qout = np.ma.zeros( - (self.nlay * self.nrow * self.ncol), self.float_type - ) - for [node, q] in zip(data["node"], data["q"]): - idx = node - 1 - if q > 0: - qin.data[idx] += q - elif q < 0: - qout.data[idx] += q - qin = np.ma.reshape(qin, (self.nlay, self.nrow, self.ncol)) - qout = np.ma.reshape(qout, (self.nlay, self.nrow, self.ncol)) - elif imeth == 0 or imeth == 1: - # FULL 3-D ARRAY - qin = np.ma.zeros(self.cbc_shape, self.float_type) - qout = np.ma.zeros(self.cbc_shape, self.float_type) - qin[data > 0] = data[data > 0] - qout[data < 0] = data[data < 0] - elif imeth == 3: - # 1-LAYER ARRAY WITH LAYER INDICATOR ARRAY - rlay, rdata = data[0], data[1] - data = np.ma.zeros(self.cbc_shape, self.float_type) - for (r, c), l in np.ndenumerate(rlay): - data[l - 1, r, c] = rdata[r, c] - qin = np.ma.zeros(self.cbc_shape, self.float_type) - qout = np.ma.zeros(self.cbc_shape, self.float_type) - qin[data > 0] = data[data > 0] - qout[data < 0] = data[data < 0] - elif imeth == 4: - # 1-LAYER ARRAY THAT DEFINES LAYER 1 - qin = np.ma.zeros(self.cbc_shape, self.float_type) - qout = np.ma.zeros(self.cbc_shape, self.float_type) - r, c = np.where(data > 0) - qin[0, r, c] = data[r, c] - r, c = np.where(data < 0) - qout[0, r, c] = data[r, c] - else: - # Should not happen - raise Exception( - 'Unrecognized "imeth" for {} record: {}'.format(recname, imeth) - ) - - # Inflows - fz = [] - tz = [] - f = [] - for z in self.allzones: - if z != 0: - flux = qin[(self.izone == z)].sum() - if type(flux) == np.ma.core.MaskedConstant: - flux = 0.0 - fz.append("FROM_" + "_".join(recname.split())) - tz.append(self._zonenamedict[z]) - f.append(flux) - fz = np.array(fz) - tz = np.array(tz) - f = np.array(f) - self._update_budget_fromssst(fz, tz, np.abs(f), kstpkper, totim) - - # Outflows - fz = [] - tz = [] - f = [] - for z in self.allzones: - if z != 0: - flux = qout[(self.izone == z)].sum() - if type(flux) == np.ma.core.MaskedConstant: - flux = 0.0 - fz.append("TO_" + "_".join(recname.split())) - tz.append(self._zonenamedict[z]) - f.append(flux) - fz = np.array(fz) - tz = np.array(tz) - f = np.array(f) - self._update_budget_fromssst(fz, tz, np.abs(f), kstpkper, totim) - - return - - def _compute_mass_balance(self, kstpkper, totim): - # Returns a record array with total inflow, total outflow, - # and percent error summed by column. - skipcols = ["time_step", "stress_period", "totim", "name"] - - # Compute inflows - recnames = self.get_record_names() - innames = [n for n in recnames if n.startswith("FROM_")] - outnames = [n for n in recnames if n.startswith("TO_")] - if kstpkper is not None: - rowidx = np.where( - (self._budget["time_step"] == kstpkper[0]) - & (self._budget["stress_period"] == kstpkper[1]) - & np.in1d(self._budget["name"], innames) - ) - elif totim is not None: - rowidx = np.where( - (self._budget["totim"] == totim) - & np.in1d(self._budget["name"], innames) - ) - a = _numpyvoid2numeric( - self._budget[list(self._zonenamedict.values())][rowidx] - ) - intot = np.array(a.sum(axis=0)) - tz = np.array( - list([n for n in self._budget.dtype.names if n not in skipcols]) - ) - fz = np.array(["TOTAL_IN"] * len(tz)) - self._update_budget_fromssst(fz, tz, intot, kstpkper, totim) - - # Compute outflows - if kstpkper is not None: - rowidx = np.where( - (self._budget["time_step"] == kstpkper[0]) - & (self._budget["stress_period"] == kstpkper[1]) - & np.in1d(self._budget["name"], outnames) - ) - elif totim is not None: - rowidx = np.where( - (self._budget["totim"] == totim) - & np.in1d(self._budget["name"], outnames) - ) - a = _numpyvoid2numeric( - self._budget[list(self._zonenamedict.values())][rowidx] - ) - outot = np.array(a.sum(axis=0)) - tz = np.array( - list([n for n in self._budget.dtype.names if n not in skipcols]) - ) - fz = np.array(["TOTAL_OUT"] * len(tz)) - self._update_budget_fromssst(fz, tz, outot, kstpkper, totim) - - # Compute IN-OUT - tz = np.array( - list([n for n in self._budget.dtype.names if n not in skipcols]) - ) - f = intot - outot - fz = np.array(["IN-OUT"] * len(tz)) - self._update_budget_fromssst(fz, tz, np.abs(f), kstpkper, totim) - - # Compute percent discrepancy - tz = np.array( - list([n for n in self._budget.dtype.names if n not in skipcols]) - ) - fz = np.array(["PERCENT_DISCREPANCY"] * len(tz)) - in_minus_out = intot - outot - in_plus_out = intot + outot - f = 100 * in_minus_out / (in_plus_out / 2.0) - self._update_budget_fromssst(fz, tz, np.abs(f), kstpkper, totim) - - return - - def _clean_budget_names(self, names): - newnames = [] - mbnames = ["TOTAL_IN", "TOTAL_OUT", "IN-OUT", "PERCENT_DISCREPANCY"] - for name in names: - if name in mbnames: - newnames.append(name) - elif not name.startswith("FROM_") and not name.startswith("TO_"): - newname_in = "FROM_" + name.upper() - newname_out = "TO_" + name.upper() - if newname_in in self._budget["name"]: - newnames.append(newname_in) - if newname_out in self._budget["name"]: - newnames.append(newname_out) - else: - if name in self._budget["name"]: - newnames.append(name) - return newnames - - def _compute_net_budget(self): - recnames = self.get_record_names() - innames = [n for n in recnames if n.startswith("FROM_")] - outnames = [n for n in recnames if n.startswith("TO_")] - select_fields = ["totim", "time_step", "stress_period", "name"] + list( - self._zonenamedict.values() - ) - select_records_in = np.in1d(self._budget["name"], innames) - select_records_out = np.in1d(self._budget["name"], outnames) - in_budget = self._budget[select_fields][select_records_in] - out_budget = self._budget[select_fields][select_records_out] - net_budget = in_budget.copy() - for f in [ - n for n in self._zonenamedict.values() if n in select_fields - ]: - net_budget[f] = np.array([r for r in in_budget[f]]) - np.array( - [r for r in out_budget[f]] - ) - newnames = ["_".join(n.split("_")[1:]) for n in net_budget["name"]] - net_budget["name"] = newnames - return net_budget - - def __mul__(self, other): - newbud = self._budget.copy() - for f in self._zonenamedict.values(): - newbud[f] = np.array([r for r in newbud[f]]) * other - idx = np.in1d(self._budget["name"], "PERCENT_DISCREPANCY") - newbud[:][idx] = self._budget[:][idx] - newobj = self.copy() - newobj._budget = newbud - return newobj - - def __truediv__(self, other): - newbud = self._budget.copy() - for f in self._zonenamedict.values(): - newbud[f] = np.array([r for r in newbud[f]]) / float(other) - idx = np.in1d(self._budget["name"], "PERCENT_DISCREPANCY") - newbud[:][idx] = self._budget[:][idx] - newobj = self.copy() - newobj._budget = newbud - return newobj - - def __div__(self, other): - newbud = self._budget.copy() - for f in self._zonenamedict.values(): - newbud[f] = np.array([r for r in newbud[f]]) / float(other) - idx = np.in1d(self._budget["name"], "PERCENT_DISCREPANCY") - newbud[:][idx] = self._budget[:][idx] - newobj = self.copy() - newobj._budget = newbud - return newobj - - def __add__(self, other): - newbud = self._budget.copy() - for f in self._zonenamedict.values(): - newbud[f] = np.array([r for r in newbud[f]]) + other - idx = np.in1d(self._budget["name"], "PERCENT_DISCREPANCY") - newbud[:][idx] = self._budget[:][idx] - newobj = self.copy() - newobj._budget = newbud - return newobj - - def __sub__(self, other): - newbud = self._budget.copy() - for f in self._zonenamedict.values(): - newbud[f] = np.array([r for r in newbud[f]]) - other - idx = np.in1d(self._budget["name"], "PERCENT_DISCREPANCY") - newbud[:][idx] = self._budget[:][idx] - newobj = self.copy() - newobj._budget = newbud - return newobj - - -def _numpyvoid2numeric(a): - # The budget record array has multiple dtypes and a slice returns - # the flexible-type numpy.void which must be converted to a numeric - # type prior to performing reducing functions such as sum() or - # mean() - return np.array([list(r) for r in a]) - - -def write_zbarray(fname, X, fmtin=None, iprn=None): - """ - Saves a numpy array in a format readable by the zonebudget program - executable. - - File format: - line 1: nlay, nrow, ncol - line 2: INTERNAL (format) - line 3: begin data - . - . - . - - example from NACP: - 19 250 500 - INTERNAL (10I8) - 199 199 199 199 199 199 199 199 199 199 - 199 199 199 199 199 199 199 199 199 199 - ... - INTERNAL (10I8) - 199 199 199 199 199 199 199 199 199 199 - 199 199 199 199 199 199 199 199 199 199 - ... - - Parameters - ---------- - X : array - The array of zones to be written. - fname : str - The path and name of the file to be written. - fmtin : int - The number of values to write to each line. - iprn : int - Padding space to add between each value. - - Returns - ------- - - """ - if len(X.shape) == 2: - b = np.zeros((1, X.shape[0], X.shape[1]), dtype=np.int32) - b[0, :, :] = X[:, :] - X = b.copy() - elif len(X.shape) < 2 or len(X.shape) > 3: - raise Exception( - "Shape of the input array is not recognized: {}".format(X.shape) - ) - if np.ma.is_masked(X): - X = np.ma.filled(X, 0) - - nlay, nrow, ncol = X.shape - - if fmtin is not None: - assert fmtin < ncol, ( - "The specified width is greater than the " - "number of columns in the array." - ) - else: - fmtin = ncol - - iprnmin = len(str(X.max())) - if iprn is None or iprn <= iprnmin: - iprn = iprnmin + 1 - - formatter_str = "{{:>{iprn}}}".format(iprn=iprn) - formatter = formatter_str.format - - with open(fname, "w") as f: - header = "{nlay} {nrow} {ncol}\n".format( - nlay=nlay, nrow=nrow, ncol=ncol - ) - f.write(header) - for lay in range(nlay): - record_2 = "INTERNAL\t({fmtin}I{iprn})\n".format( - fmtin=fmtin, iprn=iprn - ) - f.write(record_2) - if fmtin < ncol: - for row in range(nrow): - rowvals = X[lay, row, :].ravel() - start = 0 - end = start + fmtin - vals = rowvals[start:end] - while len(vals) > 0: - s = ( - "".join([formatter(int(val)) for val in vals]) - + "\n" - ) - f.write(s) - start = end - end = start + fmtin - vals = rowvals[start:end] - - elif fmtin == ncol: - for row in range(nrow): - vals = X[lay, row, :].ravel() - f.write( - "".join([formatter(int(val)) for val in vals]) + "\n" - ) - return - - -def read_zbarray(fname): - """ - Reads an ascii array in a format readable by the zonebudget program - executable. - - Parameters - ---------- - fname : str - The path and name of the file to be written. - - Returns - ------- - zones : numpy ndarray - An integer array of the zones. - """ - with open(fname, "r") as f: - lines = f.readlines() - - # Initialize layer - lay = 0 - - # Initialize data counter - totlen = 0 - i = 0 - - # First line contains array dimensions - dimstring = lines.pop(0).strip().split() - nlay, nrow, ncol = [int(v) for v in dimstring] - zones = np.zeros((nlay, nrow, ncol), dtype=np.int32) - - # The number of values to read before placing - # them into the zone array - datalen = nrow * ncol - - # List of valid values for LOCAT - locats = ["CONSTANT", "INTERNAL", "EXTERNAL"] - - # ITERATE OVER THE ROWS - for line in lines: - rowitems = line.strip().split() - - # Skip blank lines - if len(rowitems) == 0: - continue - - # HEADER - if rowitems[0].upper() in locats: - vals = [] - locat = rowitems[0].upper() - - if locat == "CONSTANT": - iconst = int(rowitems[1]) - else: - fmt = rowitems[1].strip("()") - fmtin, iprn = [int(v) for v in fmt.split("I")] - - # ZONE DATA - else: - if locat == "CONSTANT": - vals = np.ones((nrow, ncol), dtype=np.int32) * iconst - lay += 1 - elif locat == "INTERNAL": - # READ ZONES - rowvals = [int(v) for v in rowitems] - s = "Too many values encountered on this line." - assert len(rowvals) <= fmtin, s - vals.extend(rowvals) - - elif locat == "EXTERNAL": - # READ EXTERNAL FILE - fname = rowitems[0] - if not os.path.isfile(fname): - errmsg = 'Could not find external file "{}"'.format(fname) - raise Exception(errmsg) - with open(fname, "r") as ext_f: - ext_flines = ext_f.readlines() - for ext_frow in ext_flines: - ext_frowitems = ext_frow.strip().split() - rowvals = [int(v) for v in ext_frowitems] - vals.extend(rowvals) - if len(vals) != datalen: - errmsg = ( - "The number of values read from external " - 'file "{}" does not match the expected ' - "number.".format(len(vals)) - ) - raise Exception(errmsg) - else: - # Should not get here - raise Exception("Locat not recognized: {}".format(locat)) - - # IGNORE COMPOSITE ZONES - - if len(vals) == datalen: - # place values for the previous layer into the zone array - vals = np.array(vals, dtype=np.int32).reshape((nrow, ncol)) - zones[lay, :, :] = vals[:, :] - lay += 1 - totlen += len(rowitems) - i += 1 - s = ( - "The number of values read ({:,.0f})" - " does not match the number expected" - " ({:,.0f})".format(totlen, nlay * nrow * ncol) - ) - assert totlen == nlay * nrow * ncol, s - return zones - - -def sum_flux_tuples(fromzones, tozones, fluxes): - tup = zip(fromzones, tozones, fluxes) - sorted_tups = sort_tuple(tup) - - # Group the sorted tuples by (from zone, to zone) - # itertools.groupby() returns the index (from zone, to zone) and - # a list of the tuples with that index - from_zones = [] - to_zones = [] - fluxes = [] - for (fz, tz), ftup in groupby(sorted_tups, lambda tup: tup[:2]): - f = np.sum([tup[-1] for tup in list(ftup)]) - from_zones.append(fz) - to_zones.append(tz) - fluxes.append(f) - return np.array(from_zones), np.array(to_zones), np.array(fluxes) - - -def sort_tuple(tup, n=2): - """Sort a tuple by the first n values - - tup: tuple - input tuple - n : int - values to sort tuple by (default is 2) - - Returns - ------- - tup : tuple - tuple sorted by the first n values - - """ - return tuple(sorted(tup, key=lambda t: t[:n])) - - -def get_totim_modflow6(tdis): - """Create a totim array from the tdis file in modflow 6 - - Parameters - ---------- - tdis : ModflowTdis object - MODDFLOW 6 TDIS object - - Returns - ------- - totim : np.ndarray - total time vector for simulation - - - """ - recarray = tdis.perioddata.array - delt = [] - for record in recarray: - perlen = record.perlen - nstp = record.nstp - tsmult = record.tsmult - for stp in range(nstp): - if stp == 0: - if tsmult != 1.0: - dt = perlen * (tsmult - 1) / ((tsmult ** nstp) - 1) - else: - dt = perlen / nstp - else: - dt = delt[-1] * tsmult - - delt.append(dt) - - totim = np.add.accumulate(delt) - - return totim - - -class ZBNetOutput: - """ - Class that holds zonebudget netcdf output and allows export utilities - to recognize the output data type. - - Parameters - ---------- - zones : np.ndarray - array of zone numbers - time : np.ndarray - array of totim - arrays : dict - dictionary of budget term arrays. - axis 0 is totim, - axis 1 is zones - flux : bool - boolean flag to indicate if budget data is a flux "L^3/T"(True, - default) or if the data have been processed to - volumetric values "L^3" (False) - """ - - def __init__(self, zones, time, arrays, zone_array, flux=True): - self.zones = zones - self.time = time - self.arrays = arrays - self.zone_array = zone_array - self.flux = flux - - -class ZoneBudgetOutput: - """ - Class method to process zonebudget output into volumetric budgets - - Parameters - ---------- - f : str - zonebudget output file path - dis : flopy.modflow.ModflowDis object - zones : np.ndarray - numpy array of zones - - """ - - def __init__(self, f, dis, zones): - import pandas as pd - from ..modflow import ModflowDis - - self._filename = f - self._otype = None - self._zones = zones - self.__pd = pd - - if isinstance(dis, ModflowDis): - self._totim = dis.get_totim() - self._nstp = dis.nstp.array - self._steady = dis.steady.array - - else: - self._totim = get_totim_modflow6(dis) - self._nstp = np.array(dis.perioddata.array.nstp) - # self._steady is a placeholder, data not used for ZB6 read - self._steady = [False for _ in dis.perioddata.array] - - self._tslen = None - self._date_time = None - self._data = None - - if self._otype is None: - self._get_otype() - - self._calculate_tslen() - self._read_file() - - def __repr__(self): - """ - String representation of the ZoneBudgetOutput class - - """ - zones = ", ".join([str(i) for i in self.zones]) - l = [ - "ZoneBudgetOutput Class", - "----------------------\n", - "Number of zones: {}".format(len(self.zones)), - "Unique zones: {}".format(zones), - "Number of buget records: {}".format(len(self.dataframe)), - ] - - return "\n".join(l) - - @property - def zone_array(self): - """ - Property method to get the zone array - - """ - return np.asarray(self._zones, dtype=int) - - @property - def zones(self): - """ - Get a unique list of zones - - """ - return np.unique(self.zone_array) - - @property - def dataframe(self): - """ - Returns a net flux dataframe of the zonebudget output - - """ - return self.__pd.DataFrame.from_dict(self._data) - - def _calculate_tslen(self): - """ - Method to calculate each timestep length from totim - and reset totim to a dictionary of {(kstp, kper): totim} - - """ - n = 0 - totim = {} - for ix, stp in enumerate(self._nstp): - for i in range(stp): - if self._tslen is None: - tslen = self._totim[n] - self._tslen = {(i, ix): tslen} - else: - tslen = self._totim[n] - self._totim[n - 1] - self._tslen[(i, ix)] = tslen - - totim[(i, ix)] = self._totim[n] - n += 1 - - self._totim = totim - - def _read_file(self): - """ - Delegator method for reading zonebudget outputs - - """ - if self._otype == 1: - self._read_file1() - elif self._otype == 2: - self._read_file2() - elif self._otype == 3: - self._read_file3() - else: - raise AssertionError( - "Invalid otype supplied: {}".format(self._otype) - ) - - def _read_file1(self): - """ - Read original style zonebudget output file - - """ - - with open(self._filename) as foo: - - data_in = {} - data_out = {} - read_in = False - read_out = False - flow_budget = False - empty = 0 - while True: - line = foo.readline().strip().lower() - - if "flow budget for zone" in line: - flow_budget = True - read_in = False - read_out = False - empty = 0 - t = line.split() - zone = int(t[4]) - if len(t[7]) > 4: - t.insert(8, t[7][4:]) - kstp = int(t[8]) - 1 - if len(t[11]) > 6: - t.append(t[11][6:]) - kper = int(t[12]) - 1 - if "zone" not in data_in: - data_in["zone"] = [zone] - data_in["kstp"] = [kstp] - data_in["kper"] = [kper] - else: - data_in["zone"].append(zone) - data_in["kstp"].append(kstp) - data_in["kper"].append(kper) - - if self._steady[kper]: - try: - data_in["storage"].append(0.0) - data_out["storage"].append(0.0) - except KeyError: - data_in["storage"] = [0.0] - data_out["storage"] = [0.0] - - elif line in ("", " "): - empty += 1 - - elif read_in: - if "=" in line: - t = line.split("=") - label = t[0].strip() - if "zone" in line: - # currently we do not support zone to zone - # flow for option 1 - pass - else: - if "total" in line: - label = "total" - - if label in data_in: - data_in[label].append(float(t[1])) - else: - data_in[label] = [float(t[1])] - - elif "out:" in line: - read_out = True - read_in = False - - else: - pass - - elif read_out: - if "=" in line: - t = line.split("=") - label = t[0].strip() - if "zone" in line: - # currently we do not support zone to zone - # flow for option 1 - pass - - elif "in - out" in line: - pass - - elif "percent discrepancy" in line: - pass - - else: - if "total" in line: - label = "total" - - if label in data_out: - data_out[label].append(float(t[1])) - else: - data_out[label] = [float(t[1])] - else: - pass - - elif flow_budget: - if "in:" in line: - read_in = True - flow_budget = False - - else: - pass - - if empty >= 30: - break - - data = self._net_flux(data_in, data_out) - - self._data = data - - def _read_file2(self): - """ - Method to read csv output type 1 - - """ - with open(self._filename) as foo: - data_in = {} - data_out = {} - zone_header = False - read_in = False - read_out = False - empty = 0 - while True: - line = foo.readline().strip().lower() - - if "time step" in line: - t = line.split(",") - kstp = int(t[1]) - 1 - kper = int(t[3]) - 1 - if "kstp" not in data_in: - data_in["kstp"] = [] - data_in["kper"] = [] - data_in["zone"] = [] - - zone_header = True - empty = 0 - - elif zone_header: - t = line.split(",") - zones = [ - int(i.split()[-1]) for i in t[1:] if i not in ("",) - ] - - for zone in zones: - data_in["kstp"].append(kstp) - data_in["kper"].append(kper) - data_in["zone"].append(zone) - if self._steady[kper]: - try: - data_in["storage"].append(0.0) - data_out["storage"].append(0.0) - except KeyError: - data_in["storage"] = [0.0] - data_out["storage"] = [0.0] - - zone_header = False - read_in = True - - elif read_in: - t = line.split(",") - if "in" in t[1]: - pass - - elif "out" in t[1]: - read_in = False - read_out = True - - else: - if "zone" in t[0]: - label = " ".join(t[0].split()[1:]) - - elif "total" in t[0]: - label = "total" - - else: - label = t[0] - - if label not in data_in: - data_in[label] = [] - - for val in t[1:]: - if val in ("",): - continue - - data_in[label].append(float(val)) - - elif read_out: - t = line.split(",") - - if "percent error" in line: - read_out = False - - elif "in-out" in line: - pass - - else: - if "zone" in t[0]: - label = " ".join(t[0].split()[1:]) - - elif "total" in t[0]: - label = "total" - - else: - label = t[0] - - if label not in data_out: - data_out[label] = [] - - for val in t[1:]: - if val in ("",): - continue - - data_out[label].append(float(val)) - - elif line in ("", " "): - empty += 1 - - else: - pass - - if empty >= 25: - break - - data = self._net_flux(data_in, data_out) - - self._data = data - - def _read_file3(self): - """ - Method to read CSV2 output from zonebudget and CSV output - from Zonebudget6 - - """ - with open(self._filename) as foo: - data_in = {} - data_out = {} - read_in = True - read_out = False - # read the header - header = foo.readline().lower().strip().split(",") - header = [i.strip() for i in header] - - array = np.genfromtxt(foo, delimiter=",").T - - for ix, label in enumerate(header): - if label in ("totim", "in-out", "percent error"): - continue - - elif label == "percent error": - continue - - elif label == "step": - label = "kstp" - - elif label == "period": - label = "kper" - - elif "other zones" in label: - label = "other zones" - - elif "from zone" in label or "to zone" in label: - if "from" in label: - read_in = True - read_out = False - else: - read_out = True - read_in = False - label = " ".join(label.split()[1:]) - - elif "total" in label: - label = "total" - - elif label.split("-")[-1] == "in": - label = "-".join(label.split("-")[:-1]) - read_in = True - read_out = False - - elif label.split("-")[-1] == "out": - label = "-".join(label.split("-")[:-1]) - read_in = False - read_out = True - - else: - pass - - if read_in: - - if label in ("kstp", "kper"): - data_in[label] = np.asarray(array[ix], dtype=int) - 1 - - elif label == "zone": - data_in[label] = np.asarray(array[ix], dtype=int) - - else: - data_in[label] = array[ix] - - if label == "total": - read_in = False - read_out = True - - elif read_out: - data_out[label] = array[ix] - - else: - pass - - data = self._net_flux(data_in, data_out) - - self._data = data - - def _net_flux(self, data_in, data_out): - """ - Method to create a single dictionary of net flux data - - data_in : dict - inputs to the zone - data_out : dict - outputs from the zone - - Returns - ------- - dict : dictionary of netflux data to feed into a pandas dataframe - """ - data = {} - # calculate net storage flux (subroutine this?) - for key, value in data_in.items(): - if key in ("zone", "kstp", "kper"): - data[key] = np.asarray(value, dtype=int) - else: - arrayin = np.asarray(value) - arrayout = np.asarray(data_out[key]) - - data[key] = arrayin - arrayout - - kstp = data["kstp"] - kper = data["kper"] - tslen = np.array( - [self._tslen[(stp, kper[ix])] for ix, stp in enumerate(kstp)] - ) - totim = np.array( - [self._totim[(stp, kper[ix])] for ix, stp in enumerate(kstp)] - ) - - data["tslen"] = tslen - data["totim"] = totim - - return data - - def _get_otype(self): - """ - Method to automatically distinguish output type based on the - zonebudget header - - """ - with open(self._filename) as foo: - line = foo.readline() - if "zonebudget version" in line.lower(): - self._otype = 1 - elif "time step" in line.lower(): - self._otype = 2 - elif "totim" in line.lower(): - self._otype = 3 - else: - raise AssertionError("Cant distinguish output type") - - def export(self, f, ml, **kwargs): - """ - Method to export a netcdf file, or add zonebudget output to - an open netcdf file instance - - Parameters - ---------- - f : str or flopy.export.netcdf.NetCdf object - ml : flopy.modflow.Modflow or flopy.mf6.ModflowGwf object - **kwargs : - logger : flopy.export.netcdf.Logger instance - masked_vals : list - list of values to mask - - Returns - ------- - flopy.export.netcdf.NetCdf object - - """ - from flopy.export.utils import output_helper - - if isinstance(f, str): - if not f.endswith(".nc"): - raise AssertionError( - "File extension must end with .nc to " - "export a netcdf file" - ) - - zbncfobj = self.dataframe_to_netcdf_fmt(self.dataframe) - oudic = {"zbud": zbncfobj} - return output_helper(f, ml, oudic, **kwargs) - - def volumetric_flux(self, extrapolate_kper=False): - """ - Method to generate a volumetric budget table based on flux information - - Parameters - ---------- - extrapolate_kper : bool - flag to determine if we fill in data gaps with other - timestep information from the same stress period. - if True, we assume that flux is constant throughout a stress period - and the pandas dataframe returned contains a - volumetric budget per stress period - - if False, calculates volumes from available flux data - - Returns - ------- - pd.DataFrame - - """ - nper = len(self._nstp) - volumetric_data = {} - - for key in self._data: - volumetric_data[key] = [] - - if extrapolate_kper: - volumetric_data.pop("tslen") - volumetric_data.pop("kstp") - volumetric_data["perlen"] = [] - - perlen = [] - for per in range(nper): - tslen = 0 - for stp in range(self._nstp[per]): - tslen += self._tslen[(stp, per)] - - perlen.append(tslen) - - totim = np.add.accumulate(perlen) - - for per in range(nper): - idx = np.where(self._data["kper"] == per)[0] - - if len(idx) == 0: - continue - - temp = self._data["zone"][idx] - - for zone in self.zones: - if zone == 0: - continue - - zix = np.where(temp == zone)[0] - - if len(zix) == 0: - raise Exception - - for key, value in self._data.items(): - if key == "totim": - volumetric_data[key].append(totim[per]) - - elif key == "tslen": - volumetric_data["perlen"].append(perlen[per]) - - elif key == "kstp": - continue - - elif key == "kper": - volumetric_data[key].append(per) - - elif key == "zone": - volumetric_data[key].append(zone) - - else: - tv = value[idx] - zv = tv[zix] - for i in zv: - vol = i * perlen[per] - volumetric_data[key].append(vol) - break - - else: - - for key, value in self._data.items(): - if key in ("zone", "kstp", "kper", "tslen"): - volumetric_data[key] = value - else: - volumetric_data[key] = value * self._data["tslen"] - - return self.__pd.DataFrame.from_dict(volumetric_data) - - def dataframe_to_netcdf_fmt(self, df, flux=True): - """ - Method to transform a volumetric zonebudget dataframe into - array format for netcdf. - - time is on axis 0 - zone is on axis 1 - - Parameters - ---------- - df : pd.DataFrame - flux : bool - boolean flag to indicate if budget data is a flux "L^3/T" (True, - default) or if the data have been processed to - volumetric values "L^3" (False) - zone_array : np.ndarray - zonebudget zones array - - Returns - ------- - ZBNetOutput object - - """ - zones = np.sort(np.unique(df.zone.values)) - totim = np.sort(np.unique(df.totim.values)) - - data = {} - for col in df.columns: - if col in ("totim", "zone", "kper", "perlen"): - pass - else: - data[col] = np.zeros((totim.size, zones.size), dtype=float) - - for i, time in enumerate(totim): - tdf = df.loc[ - df.totim.isin( - [ - time, - ] - ) - ] - tdf = tdf.sort_values(by=["zone"]) - - for col in df.columns: - if col in ("totim", "zone", "kper", "perlen"): - pass - else: - data[col][i, :] = tdf[col].values - - return ZBNetOutput(zones, totim, data, self.zone_array, flux=flux) +import os +import copy +import numpy as np +from itertools import groupby +from collections import OrderedDict +from .utils_def import totim_to_datetime +import warnings + +warnings.simplefilter("once", PendingDeprecationWarning) + + +class ZoneBudget: + """ + ZoneBudget class + + Parameters + ---------- + cbc_file : str or CellBudgetFile object + The file name or CellBudgetFile object for which budgets will be + computed. + z : ndarray + The array containing to zones to be used. + kstpkper : tuple of ints + A tuple containing the time step and stress period (kstp, kper). + The kstp and kper values are zero based. + totim : float + The simulation time. + aliases : dict + A dictionary with key, value pairs of zones and aliases. Replaces + the corresponding record and field names with the aliases provided. + When using this option in conjunction with a list of zones, the + zone(s) passed may either be all strings (aliases), all integers, + or mixed. + + Returns + ------- + None + + Examples + -------- + + >>> from flopy.utils.zonbud import ZoneBudget, read_zbarray + >>> zon = read_zbarray('zone_input_file') + >>> zb = ZoneBudget('zonebudtest.cbc', zon, kstpkper=(0, 0)) + >>> zb.to_csv('zonebudtest.csv') + >>> zb_mgd = zb * 7.48052 / 1000000 + """ + + def __init__( + self, + cbc_file, + z, + kstpkper=None, + totim=None, + aliases=None, + verbose=False, + **kwargs + ): + from .binaryfile import CellBudgetFile + + if isinstance(cbc_file, CellBudgetFile): + self.cbc = cbc_file + elif isinstance(cbc_file, str) and os.path.isfile(cbc_file): + self.cbc = CellBudgetFile(cbc_file) + else: + raise Exception( + "Cannot load cell budget file: {}.".format(cbc_file) + ) + + if isinstance(z, np.ndarray): + assert np.issubdtype( + z.dtype, np.integer + ), "Zones dtype must be integer" + else: + e = ( + "Please pass zones as a numpy ndarray of (positive)" + " integers. {}".format(z.dtype) + ) + raise Exception(e) + + # Check for negative zone values + if np.any(z < 0): + raise Exception( + "Negative zone value(s) found:", np.unique(z[z < 0]) + ) + + self.dis = None + if "model" in kwargs.keys(): + self.model = kwargs.pop("model") + self.dis = self.model.dis + if "dis" in kwargs.keys(): + self.dis = kwargs.pop("dis") + if "sr" in kwargs.keys(): + kwargs.pop("sr") + warnings.warn("ignoring 'sr' parameter") + if len(kwargs.keys()) > 0: + args = ",".join(kwargs.keys()) + raise Exception("LayerFile error: unrecognized kwargs: " + args) + + # Check the shape of the cbc budget file arrays + self.cbc_shape = self.cbc.get_data(idx=0, full3D=True)[0].shape + self.nlay, self.nrow, self.ncol = self.cbc_shape + self.cbc_times = self.cbc.get_times() + self.cbc_kstpkper = self.cbc.get_kstpkper() + self.kstpkper = None + self.totim = None + + if kstpkper is not None: + if isinstance(kstpkper, tuple): + kstpkper = [kstpkper] + for kk in kstpkper: + s = ( + "The specified time step/stress period " + "does not exist {}".format(kk) + ) + assert kk in self.cbc.get_kstpkper(), s + self.kstpkper = kstpkper + elif totim is not None: + if isinstance(totim, float): + totim = [totim] + elif isinstance(totim, int): + totim = [float(totim)] + for t in totim: + s = ( + "The specified simulation time " + "does not exist {}".format(t) + ) + assert t in self.cbc.get_times(), s + self.totim = totim + else: + # No time step/stress period or simulation time pass + self.kstpkper = self.cbc.get_kstpkper() + + # Set float and integer types + self.float_type = np.float32 + self.int_type = np.int32 + + # Check dimensions of input zone array + s = ( + "Row/col dimensions of zone array {}" + " do not match model row/col dimensions {}".format( + z.shape, self.cbc_shape + ) + ) + assert z.shape[-2] == self.nrow and z.shape[-1] == self.ncol, s + + if z.shape == self.cbc_shape: + izone = z.copy() + elif len(z.shape) == 2: + izone = np.zeros(self.cbc_shape, self.int_type) + izone[:] = z[:, :] + elif len(z.shape) == 3 and z.shape[0] == 1: + izone = np.zeros(self.cbc_shape, self.int_type) + izone[:] = z[0, :, :] + else: + e = "Shape of the zone array is not recognized: {}".format(z.shape) + raise Exception(e) + + self.izone = izone + self.allzones = np.unique(izone) + self._zonenamedict = OrderedDict( + [(z, "ZONE_{}".format(z)) for z in self.allzones] + ) + + if aliases is not None: + s = ( + "Input aliases not recognized. Please pass a dictionary " + "with key,value pairs of zone/alias." + ) + assert isinstance(aliases, dict), s + # Replace the relevant field names (ignore zone 0) + seen = [] + for z, a in iter(aliases.items()): + if z != 0 and z in self._zonenamedict.keys(): + if z in seen: + raise Exception( + "Zones may not have more than 1 alias." + ) + self._zonenamedict[z] = "_".join(a.split()) + seen.append(z) + + # self._iflow_recnames = self._get_internal_flow_record_names() + + # All record names in the cell-by-cell budget binary file + self.record_names = [ + n.strip() for n in self.cbc.get_unique_record_names(decode=True) + ] + + # Get imeth for each record in the CellBudgetFile record list + self.imeth = {} + for record in self.cbc.recordarray: + self.imeth[record["text"].strip().decode("utf-8")] = record[ + "imeth" + ] + + # INTERNAL FLOW TERMS ARE USED TO CALCULATE FLOW BETWEEN ZONES. + # CONSTANT-HEAD TERMS ARE USED TO IDENTIFY WHERE CONSTANT-HEAD CELLS + # ARE AND THEN USE FACE FLOWS TO DETERMINE THE AMOUNT OF FLOW. + # SWIADDTO--- terms are used by the SWI2 groundwater flow process. + internal_flow_terms = [ + "CONSTANT HEAD", + "FLOW RIGHT FACE", + "FLOW FRONT FACE", + "FLOW LOWER FACE", + "SWIADDTOCH", + "SWIADDTOFRF", + "SWIADDTOFFF", + "SWIADDTOFLF", + ] + + # Source/sink/storage term record names + # These are all of the terms that are not related to constant + # head cells or face flow terms + self.ssst_record_names = [ + n for n in self.record_names if n not in internal_flow_terms + ] + + # Initialize budget recordarray + array_list = [] + if self.kstpkper is not None: + for kk in self.kstpkper: + recordarray = self._initialize_budget_recordarray( + kstpkper=kk, totim=None + ) + array_list.append(recordarray) + elif self.totim is not None: + for t in self.totim: + recordarray = self._initialize_budget_recordarray( + kstpkper=None, totim=t + ) + array_list.append(recordarray) + self._budget = np.concatenate(array_list, axis=0) + + # Update budget record array + if self.kstpkper is not None: + for kk in self.kstpkper: + if verbose: + s = ( + "Computing the budget for" + " time step {} in stress period {}".format( + kk[0] + 1, kk[1] + 1 + ) + ) + print(s) + self._compute_budget(kstpkper=kk) + elif self.totim is not None: + for t in self.totim: + if verbose: + s = "Computing the budget for time {}".format(t) + print(s) + self._compute_budget(totim=t) + + def _compute_budget(self, kstpkper=None, totim=None): + """ + Creates a budget for the specified zone array. This function only + supports the use of a single time step/stress period or time. + + Parameters + ---------- + kstpkper : tuple + Tuple of kstp and kper to compute budget for (default is None). + totim : float + Totim to compute budget for (default is None). + + Returns + ------- + None + + """ + # Initialize an array to track where the constant head cells + # are located. + ich = np.zeros(self.cbc_shape, self.int_type) + swiich = np.zeros(self.cbc_shape, self.int_type) + + if "CONSTANT HEAD" in self.record_names: + """ + C-----CONSTANT-HEAD FLOW -- DON'T ACCUMULATE THE CELL-BY-CELL VALUES FOR + C-----CONSTANT-HEAD FLOW BECAUSE THEY MAY INCLUDE PARTIALLY CANCELING + C-----INS AND OUTS. USE CONSTANT-HEAD TERM TO IDENTIFY WHERE CONSTANT- + C-----HEAD CELLS ARE AND THEN USE FACE FLOWS TO DETERMINE THE AMOUNT OF + C-----FLOW. STORE CONSTANT-HEAD LOCATIONS IN ICH ARRAY. + """ + chd = self.cbc.get_data( + text="CONSTANT HEAD", + full3D=True, + kstpkper=kstpkper, + totim=totim, + )[0] + ich[np.ma.where(chd != 0.0)] = 1 + if "FLOW RIGHT FACE" in self.record_names: + self._accumulate_flow_frf("FLOW RIGHT FACE", ich, kstpkper, totim) + if "FLOW FRONT FACE" in self.record_names: + self._accumulate_flow_fff("FLOW FRONT FACE", ich, kstpkper, totim) + if "FLOW LOWER FACE" in self.record_names: + self._accumulate_flow_flf("FLOW LOWER FACE", ich, kstpkper, totim) + if "SWIADDTOCH" in self.record_names: + swichd = self.cbc.get_data( + text="SWIADDTOCH", full3D=True, kstpkper=kstpkper, totim=totim + )[0] + swiich[swichd != 0] = 1 + if "SWIADDTOFRF" in self.record_names: + self._accumulate_flow_frf("SWIADDTOFRF", swiich, kstpkper, totim) + if "SWIADDTOFFF" in self.record_names: + self._accumulate_flow_fff("SWIADDTOFFF", swiich, kstpkper, totim) + if "SWIADDTOFLF" in self.record_names: + self._accumulate_flow_flf("SWIADDTOFLF", swiich, kstpkper, totim) + + # NOT AN INTERNAL FLOW TERM, SO MUST BE A SOURCE TERM OR STORAGE + # ACCUMULATE THE FLOW BY ZONE + # iterate over remaining items in the list + for recname in self.ssst_record_names: + self._accumulate_flow_ssst(recname, kstpkper, totim) + + # Compute mass balance terms + self._compute_mass_balance(kstpkper, totim) + + return + + def _add_empty_record( + self, recordarray, recname, kstpkper=None, totim=None + ): + """ + Build an empty records based on the specified flow direction and + record name for the given list of zones. + + Parameters + ---------- + recordarray : + recname : + kstpkper : tuple + Tuple of kstp and kper to compute budget for (default is None). + totim : float + Totim to compute budget for (default is None). + + Returns + ------- + recordarray : np.recarray + + """ + if kstpkper is not None: + if len(self.cbc_times) > 0: + totim = self.cbc_times[self.cbc_kstpkper.index(kstpkper)] + else: + totim = 0.0 + elif totim is not None: + if len(self.cbc_times) > 0: + kstpkper = self.cbc_kstpkper[self.cbc_times.index(totim)] + else: + kstpkper = (0, 0) + + row = [totim, kstpkper[0], kstpkper[1], recname] + row += [0.0 for _ in self._zonenamedict.values()] + recs = np.array(tuple(row), dtype=recordarray.dtype) + recordarray = np.append(recordarray, recs) + return recordarray + + def _initialize_budget_recordarray(self, kstpkper=None, totim=None): + """ + Initialize the budget record array which will store all of the + fluxes in the cell-budget file. + + Parameters + ---------- + kstpkper : tuple + Tuple of kstp and kper to compute budget for (default is None). + totim : float + Totim to compute budget for (default is None). + + Returns + ------- + + """ + + # Create empty array for the budget terms. + dtype_list = [ + ("totim", "= 2: + data = self.cbc.get_data( + text=recname, kstpkper=kstpkper, totim=totim + )[0] + + # "FLOW RIGHT FACE" COMPUTE FLOW BETWEEN ZONES ACROSS COLUMNS. + # COMPUTE FLOW ONLY BETWEEN A ZONE AND A HIGHER ZONE -- FLOW FROM + # ZONE 4 TO 3 IS THE NEGATIVE OF FLOW FROM 3 TO 4. + # 1ST, CALCULATE FLOW BETWEEN NODE J,I,K AND J-1,I,K + + k, i, j = np.where( + self.izone[:, :, 1:] > self.izone[:, :, :-1] + ) + + # Adjust column values to account for the starting position of "nz" + j += 1 + + # Define the zone to which flow is going + nz = self.izone[k, i, j] + + # Define the zone from which flow is coming + jl = j - 1 + nzl = self.izone[k, i, jl] + + # Get the face flow + q = data[k, i, jl] + + # Get indices where flow face values are positive (flow out of higher zone) + # Don't include CH to CH flow (can occur if CHTOCH option is used) + # Create an iterable tuple of (from zone, to zone, flux) + # Then group tuple by (from_zone, to_zone) and sum the flux values + idx = np.where( + (q > 0) & ((ich[k, i, j] != 1) | (ich[k, i, jl] != 1)) + ) + fzi, tzi, fi = sum_flux_tuples(nzl[idx], nz[idx], q[idx]) + self._update_budget_fromfaceflow( + fzi, tzi, np.abs(fi), kstpkper, totim + ) + + # Get indices where flow face values are negative (flow into higher zone) + # Don't include CH to CH flow (can occur if CHTOCH option is used) + # Create an iterable tuple of (from zone, to zone, flux) + # Then group tuple by (from_zone, to_zone) and sum the flux values + idx = np.where( + (q < 0) & ((ich[k, i, j] != 1) | (ich[k, i, jl] != 1)) + ) + fzi, tzi, fi = sum_flux_tuples(nz[idx], nzl[idx], q[idx]) + self._update_budget_fromfaceflow( + fzi, tzi, np.abs(fi), kstpkper, totim + ) + + # FLOW BETWEEN NODE J,I,K AND J+1,I,K + k, i, j = np.where( + self.izone[:, :, :-1] > self.izone[:, :, 1:] + ) + + # Define the zone from which flow is coming + nz = self.izone[k, i, j] + + # Define the zone to which flow is going + jr = j + 1 + nzr = self.izone[k, i, jr] + + # Get the face flow + q = data[k, i, j] + + # Get indices where flow face values are positive (flow out of higher zone) + # Don't include CH to CH flow (can occur if CHTOCH option is used) + # Create an iterable tuple of (from zone, to zone, flux) + # Then group tuple by (from_zone, to_zone) and sum the flux values + idx = np.where( + (q > 0) & ((ich[k, i, j] != 1) | (ich[k, i, jr] != 1)) + ) + fzi, tzi, fi = sum_flux_tuples(nz[idx], nzr[idx], q[idx]) + self._update_budget_fromfaceflow( + fzi, tzi, np.abs(fi), kstpkper, totim + ) + + # Get indices where flow face values are negative (flow into higher zone) + # Don't include CH to CH flow (can occur if CHTOCH option is used) + # Create an iterable tuple of (from zone, to zone, flux) + # Then group tuple by (from_zone, to_zone) and sum the flux values + idx = np.where( + (q < 0) & ((ich[k, i, j] != 1) | (ich[k, i, jr] != 1)) + ) + fzi, tzi, fi = sum_flux_tuples(nzr[idx], nz[idx], q[idx]) + self._update_budget_fromfaceflow( + fzi, tzi, np.abs(fi), kstpkper, totim + ) + + # CALCULATE FLOW TO CONSTANT-HEAD CELLS IN THIS DIRECTION + k, i, j = np.where(ich == 1) + k, i, j = k[j > 0], i[j > 0], j[j > 0] + jl = j - 1 + nzl = self.izone[k, i, jl] + nz = self.izone[k, i, j] + q = data[k, i, jl] + idx = np.where( + (q > 0) & ((ich[k, i, j] != 1) | (ich[k, i, jl] != 1)) + ) + fzi, tzi, f = sum_flux_tuples(nzl[idx], nz[idx], q[idx]) + fz = ["TO_CONSTANT_HEAD"] * len(tzi) + tz = [self._zonenamedict[z] for z in tzi] + self._update_budget_fromssst( + fz, tz, np.abs(f), kstpkper, totim + ) + + idx = np.where( + (q < 0) & ((ich[k, i, j] != 1) | (ich[k, i, jl] != 1)) + ) + fzi, tzi, f = sum_flux_tuples(nzl[idx], nz[idx], q[idx]) + fz = ["FROM_CONSTANT_HEAD"] * len(fzi) + tz = [self._zonenamedict[z] for z in tzi[tzi != 0]] + self._update_budget_fromssst( + fz, tz, np.abs(f), kstpkper, totim + ) + + k, i, j = np.where(ich == 1) + k, i, j = ( + k[j < self.ncol - 1], + i[j < self.ncol - 1], + j[j < self.ncol - 1], + ) + nz = self.izone[k, i, j] + jr = j + 1 + nzr = self.izone[k, i, jr] + q = data[k, i, j] + idx = np.where( + (q > 0) & ((ich[k, i, j] != 1) | (ich[k, i, jr] != 1)) + ) + fzi, tzi, f = sum_flux_tuples(nzr[idx], nz[idx], q[idx]) + fz = ["FROM_CONSTANT_HEAD"] * len(tzi) + tz = [self._zonenamedict[z] for z in tzi] + self._update_budget_fromssst( + fz, tz, np.abs(f), kstpkper, totim + ) + + idx = np.where( + (q < 0) & ((ich[k, i, j] != 1) | (ich[k, i, jr] != 1)) + ) + fzi, tzi, f = sum_flux_tuples(nzr[idx], nz[idx], q[idx]) + fz = ["TO_CONSTANT_HEAD"] * len(fzi) + tz = [self._zonenamedict[z] for z in tzi] + self._update_budget_fromssst( + fz, tz, np.abs(f), kstpkper, totim + ) + + except Exception as e: + print(e) + raise + return + + def _accumulate_flow_fff(self, recname, ich, kstpkper, totim): + """ + + Parameters + ---------- + recname + ich + kstpkper + totim + + Returns + ------- + + """ + try: + if self.nrow >= 2: + data = self.cbc.get_data( + text=recname, kstpkper=kstpkper, totim=totim + )[0] + + # "FLOW FRONT FACE" + # CALCULATE FLOW BETWEEN NODE J,I,K AND J,I-1,K + k, i, j = np.where( + self.izone[:, 1:, :] < self.izone[:, :-1, :] + ) + i += 1 + ia = i - 1 + nza = self.izone[k, ia, j] + nz = self.izone[k, i, j] + q = data[k, ia, j] + idx = np.where( + (q > 0) & ((ich[k, i, j] != 1) | (ich[k, ia, j] != 1)) + ) + fzi, tzi, fi = sum_flux_tuples(nza[idx], nz[idx], q[idx]) + self._update_budget_fromfaceflow( + fzi, tzi, np.abs(fi), kstpkper, totim + ) + + idx = np.where( + (q < 0) & ((ich[k, i, j] != 1) | (ich[k, ia, j] != 1)) + ) + fzi, tzi, fi = sum_flux_tuples(nz[idx], nza[idx], q[idx]) + self._update_budget_fromfaceflow( + fzi, tzi, np.abs(fi), kstpkper, totim + ) + + # CALCULATE FLOW BETWEEN NODE J,I,K AND J,I+1,K. + k, i, j = np.where( + self.izone[:, :-1, :] < self.izone[:, 1:, :] + ) + nz = self.izone[k, i, j] + ib = i + 1 + nzb = self.izone[k, ib, j] + q = data[k, i, j] + idx = np.where( + (q > 0) & ((ich[k, i, j] != 1) | (ich[k, ib, j] != 1)) + ) + fzi, tzi, fi = sum_flux_tuples(nz[idx], nzb[idx], q[idx]) + self._update_budget_fromfaceflow( + fzi, tzi, np.abs(fi), kstpkper, totim + ) + + idx = np.where( + (q < 0) & ((ich[k, i, j] != 1) | (ich[k, ib, j] != 1)) + ) + fzi, tzi, fi = sum_flux_tuples(nzb[idx], nz[idx], q[idx]) + self._update_budget_fromfaceflow( + fzi, tzi, np.abs(fi), kstpkper, totim + ) + + # CALCULATE FLOW TO CONSTANT-HEAD CELLS IN THIS DIRECTION + k, i, j = np.where(ich == 1) + k, i, j = k[i > 0], i[i > 0], j[i > 0] + ia = i - 1 + nza = self.izone[k, ia, j] + nz = self.izone[k, i, j] + q = data[k, ia, j] + idx = np.where( + (q > 0) & ((ich[k, i, j] != 1) | (ich[k, ia, j] != 1)) + ) + fzi, tzi, f = sum_flux_tuples(nza[idx], nz[idx], q[idx]) + fz = ["TO_CONSTANT_HEAD"] * len(tzi) + tz = [self._zonenamedict[z] for z in tzi] + self._update_budget_fromssst( + fz, tz, np.abs(f), kstpkper, totim + ) + + idx = np.where( + (q < 0) & ((ich[k, i, j] != 1) | (ich[k, ia, j] != 1)) + ) + fzi, tzi, f = sum_flux_tuples(nza[idx], nz[idx], q[idx]) + fz = ["FROM_CONSTANT_HEAD"] * len(fzi) + tz = [self._zonenamedict[z] for z in tzi] + self._update_budget_fromssst( + fz, tz, np.abs(f), kstpkper, totim + ) + + k, i, j = np.where(ich == 1) + k, i, j = ( + k[i < self.nrow - 1], + i[i < self.nrow - 1], + j[i < self.nrow - 1], + ) + nz = self.izone[k, i, j] + ib = i + 1 + nzb = self.izone[k, ib, j] + q = data[k, i, j] + idx = np.where( + (q > 0) & ((ich[k, i, j] != 1) | (ich[k, ib, j] != 1)) + ) + fzi, tzi, f = sum_flux_tuples(nzb[idx], nz[idx], q[idx]) + fz = ["FROM_CONSTANT_HEAD"] * len(tzi) + tz = [self._zonenamedict[z] for z in tzi] + self._update_budget_fromssst( + fz, tz, np.abs(f), kstpkper, totim + ) + + idx = np.where( + (q < 0) & ((ich[k, i, j] != 1) | (ich[k, ib, j] != 1)) + ) + fzi, tzi, f = sum_flux_tuples(nzb[idx], nz[idx], q[idx]) + fz = ["TO_CONSTANT_HEAD"] * len(fzi) + tz = [self._zonenamedict[z] for z in tzi] + self._update_budget_fromssst( + fz, tz, np.abs(f), kstpkper, totim + ) + + except Exception as e: + print(e) + raise + return + + def _accumulate_flow_flf(self, recname, ich, kstpkper, totim): + """ + + Parameters + ---------- + recname + ich + kstpkper + totim + + Returns + ------- + + """ + try: + if self.nlay >= 2: + data = self.cbc.get_data( + text=recname, kstpkper=kstpkper, totim=totim + )[0] + + # "FLOW LOWER FACE" + # CALCULATE FLOW BETWEEN NODE J,I,K AND J,I,K-1 + k, i, j = np.where( + self.izone[1:, :, :] < self.izone[:-1, :, :] + ) + k += 1 + ka = k - 1 + nza = self.izone[ka, i, j] + nz = self.izone[k, i, j] + q = data[ka, i, j] + idx = np.where( + (q > 0) & ((ich[k, i, j] != 1) | (ich[ka, i, j] != 1)) + ) + fzi, tzi, fi = sum_flux_tuples(nza[idx], nz[idx], q[idx]) + self._update_budget_fromfaceflow( + fzi, tzi, np.abs(fi), kstpkper, totim + ) + + idx = np.where( + (q < 0) & ((ich[k, i, j] != 1) | (ich[ka, i, j] != 1)) + ) + fzi, tzi, fi = sum_flux_tuples(nz[idx], nza[idx], q[idx]) + self._update_budget_fromfaceflow( + fzi, tzi, np.abs(fi), kstpkper, totim + ) + + # CALCULATE FLOW BETWEEN NODE J,I,K AND J,I,K+1 + k, i, j = np.where( + self.izone[:-1, :, :] < self.izone[1:, :, :] + ) + nz = self.izone[k, i, j] + kb = k + 1 + nzb = self.izone[kb, i, j] + q = data[k, i, j] + idx = np.where( + (q > 0) & ((ich[k, i, j] != 1) | (ich[kb, i, j] != 1)) + ) + fzi, tzi, fi = sum_flux_tuples(nz[idx], nzb[idx], q[idx]) + self._update_budget_fromfaceflow( + fzi, tzi, np.abs(fi), kstpkper, totim + ) + + idx = np.where( + (q < 0) & ((ich[k, i, j] != 1) | (ich[kb, i, j] != 1)) + ) + fzi, tzi, fi = sum_flux_tuples(nzb[idx], nz[idx], q[idx]) + self._update_budget_fromfaceflow( + fzi, tzi, np.abs(fi), kstpkper, totim + ) + + # CALCULATE FLOW TO CONSTANT-HEAD CELLS IN THIS DIRECTION + k, i, j = np.where(ich == 1) + k, i, j = k[k > 0], i[k > 0], j[k > 0] + ka = k - 1 + nza = self.izone[ka, i, j] + nz = self.izone[k, i, j] + q = data[ka, i, j] + idx = np.where( + (q > 0) & ((ich[k, i, j] != 1) | (ich[ka, i, j] != 1)) + ) + fzi, tzi, f = sum_flux_tuples(nza[idx], nz[idx], q[idx]) + fz = ["TO_CONSTANT_HEAD"] * len(tzi) + tz = [self._zonenamedict[z] for z in tzi] + self._update_budget_fromssst( + fz, tz, np.abs(f), kstpkper, totim + ) + + idx = np.where( + (q < 0) & ((ich[k, i, j] != 1) | (ich[ka, i, j] != 1)) + ) + fzi, tzi, f = sum_flux_tuples(nza[idx], nz[idx], q[idx]) + fz = ["FROM_CONSTANT_HEAD"] * len(fzi) + tz = [self._zonenamedict[z] for z in tzi] + self._update_budget_fromssst( + fz, tz, np.abs(f), kstpkper, totim + ) + + k, i, j = np.where(ich == 1) + k, i, j = ( + k[k < self.nlay - 1], + i[k < self.nlay - 1], + j[k < self.nlay - 1], + ) + nz = self.izone[k, i, j] + kb = k + 1 + nzb = self.izone[kb, i, j] + q = data[k, i, j] + idx = np.where( + (q > 0) & ((ich[k, i, j] != 1) | (ich[kb, i, j] != 1)) + ) + fzi, tzi, f = sum_flux_tuples(nzb[idx], nz[idx], q[idx]) + fz = ["FROM_CONSTANT_HEAD"] * len(tzi) + tz = [self._zonenamedict[z] for z in tzi] + self._update_budget_fromssst( + fz, tz, np.abs(f), kstpkper, totim + ) + + idx = np.where( + (q < 0) & ((ich[k, i, j] != 1) | (ich[kb, i, j] != 1)) + ) + fzi, tzi, f = sum_flux_tuples(nzb[idx], nz[idx], q[idx]) + fz = ["TO_CONSTANT_HEAD"] * len(fzi) + tz = [self._zonenamedict[z] for z in tzi] + self._update_budget_fromssst( + fz, tz, np.abs(f), kstpkper, totim + ) + + except Exception as e: + print(e) + raise + return + + def _accumulate_flow_ssst(self, recname, kstpkper, totim): + + # NOT AN INTERNAL FLOW TERM, SO MUST BE A SOURCE TERM OR STORAGE + # ACCUMULATE THE FLOW BY ZONE + + imeth = self.imeth[recname] + + data = self.cbc.get_data(text=recname, kstpkper=kstpkper, totim=totim) + if len(data) == 0: + # Empty data, can occur during the first time step of a transient + # model when storage terms are zero and not in the cell-budget + # file. + return + else: + data = data[0] + + if imeth == 2 or imeth == 5: + # LIST + qin = np.ma.zeros( + (self.nlay * self.nrow * self.ncol), self.float_type + ) + qout = np.ma.zeros( + (self.nlay * self.nrow * self.ncol), self.float_type + ) + for [node, q] in zip(data["node"], data["q"]): + idx = node - 1 + if q > 0: + qin.data[idx] += q + elif q < 0: + qout.data[idx] += q + qin = np.ma.reshape(qin, (self.nlay, self.nrow, self.ncol)) + qout = np.ma.reshape(qout, (self.nlay, self.nrow, self.ncol)) + elif imeth == 0 or imeth == 1: + # FULL 3-D ARRAY + qin = np.ma.zeros(self.cbc_shape, self.float_type) + qout = np.ma.zeros(self.cbc_shape, self.float_type) + qin[data > 0] = data[data > 0] + qout[data < 0] = data[data < 0] + elif imeth == 3: + # 1-LAYER ARRAY WITH LAYER INDICATOR ARRAY + rlay, rdata = data[0], data[1] + data = np.ma.zeros(self.cbc_shape, self.float_type) + for (r, c), l in np.ndenumerate(rlay): + data[l - 1, r, c] = rdata[r, c] + qin = np.ma.zeros(self.cbc_shape, self.float_type) + qout = np.ma.zeros(self.cbc_shape, self.float_type) + qin[data > 0] = data[data > 0] + qout[data < 0] = data[data < 0] + elif imeth == 4: + # 1-LAYER ARRAY THAT DEFINES LAYER 1 + qin = np.ma.zeros(self.cbc_shape, self.float_type) + qout = np.ma.zeros(self.cbc_shape, self.float_type) + r, c = np.where(data > 0) + qin[0, r, c] = data[r, c] + r, c = np.where(data < 0) + qout[0, r, c] = data[r, c] + else: + # Should not happen + raise Exception( + 'Unrecognized "imeth" for {} record: {}'.format(recname, imeth) + ) + + # Inflows + fz = [] + tz = [] + f = [] + for z in self.allzones: + if z != 0: + flux = qin[(self.izone == z)].sum() + if type(flux) == np.ma.core.MaskedConstant: + flux = 0.0 + fz.append("FROM_" + "_".join(recname.split())) + tz.append(self._zonenamedict[z]) + f.append(flux) + fz = np.array(fz) + tz = np.array(tz) + f = np.array(f) + self._update_budget_fromssst(fz, tz, np.abs(f), kstpkper, totim) + + # Outflows + fz = [] + tz = [] + f = [] + for z in self.allzones: + if z != 0: + flux = qout[(self.izone == z)].sum() + if type(flux) == np.ma.core.MaskedConstant: + flux = 0.0 + fz.append("TO_" + "_".join(recname.split())) + tz.append(self._zonenamedict[z]) + f.append(flux) + fz = np.array(fz) + tz = np.array(tz) + f = np.array(f) + self._update_budget_fromssst(fz, tz, np.abs(f), kstpkper, totim) + + def _compute_mass_balance(self, kstpkper, totim): + # Returns a record array with total inflow, total outflow, + # and percent error summed by column. + skipcols = ["time_step", "stress_period", "totim", "name"] + + # Compute inflows + recnames = self.get_record_names() + innames = [n for n in recnames if n.startswith("FROM_")] + outnames = [n for n in recnames if n.startswith("TO_")] + if kstpkper is not None: + rowidx = np.where( + (self._budget["time_step"] == kstpkper[0]) + & (self._budget["stress_period"] == kstpkper[1]) + & np.in1d(self._budget["name"], innames) + ) + elif totim is not None: + rowidx = np.where( + (self._budget["totim"] == totim) + & np.in1d(self._budget["name"], innames) + ) + a = _numpyvoid2numeric( + self._budget[list(self._zonenamedict.values())][rowidx] + ) + intot = np.array(a.sum(axis=0)) + tz = np.array( + list([n for n in self._budget.dtype.names if n not in skipcols]) + ) + fz = np.array(["TOTAL_IN"] * len(tz)) + self._update_budget_fromssst(fz, tz, intot, kstpkper, totim) + + # Compute outflows + if kstpkper is not None: + rowidx = np.where( + (self._budget["time_step"] == kstpkper[0]) + & (self._budget["stress_period"] == kstpkper[1]) + & np.in1d(self._budget["name"], outnames) + ) + elif totim is not None: + rowidx = np.where( + (self._budget["totim"] == totim) + & np.in1d(self._budget["name"], outnames) + ) + a = _numpyvoid2numeric( + self._budget[list(self._zonenamedict.values())][rowidx] + ) + outot = np.array(a.sum(axis=0)) + tz = np.array( + list([n for n in self._budget.dtype.names if n not in skipcols]) + ) + fz = np.array(["TOTAL_OUT"] * len(tz)) + self._update_budget_fromssst(fz, tz, outot, kstpkper, totim) + + # Compute IN-OUT + tz = np.array( + list([n for n in self._budget.dtype.names if n not in skipcols]) + ) + f = intot - outot + fz = np.array(["IN-OUT"] * len(tz)) + self._update_budget_fromssst(fz, tz, np.abs(f), kstpkper, totim) + + # Compute percent discrepancy + tz = np.array( + list([n for n in self._budget.dtype.names if n not in skipcols]) + ) + fz = np.array(["PERCENT_DISCREPANCY"] * len(tz)) + in_minus_out = intot - outot + in_plus_out = intot + outot + f = 100 * in_minus_out / (in_plus_out / 2.0) + self._update_budget_fromssst(fz, tz, np.abs(f), kstpkper, totim) + + def get_model_shape(self): + """Get model shape + + Returns + ------- + nlay : int + Number of layers + nrow : int + Number of rows + ncol : int + Number of columns + + """ + return self.nlay, self.nrow, self.ncol + + def get_record_names(self, stripped=False): + """ + Get a list of water budget record names in the file. + + Returns + ------- + out : list of strings + List of unique text names in the binary file. + + Examples + -------- + + >>> zb = ZoneBudget('zonebudtest.cbc', zon, kstpkper=(0, 0)) + >>> recnames = zb.get_record_names() + + """ + return _get_record_names(self._budget, stripped=stripped) + + def get_budget(self, names=None, zones=None, net=False, pivot=False): + """ + Get a list of zonebudget record arrays. + + Parameters + ---------- + + names : list of strings + A list of strings containing the names of the records desired. + zones : list of ints or strings + A list of integer zone numbers or zone names desired. + net : boolean + If True, returns net IN-OUT for each record. + pivot : boolean + If True, returns data in a more user friendly format + + Returns + ------- + budget_list : list of record arrays + A list of the zonebudget record arrays. + + Examples + -------- + + >>> names = ['FROM_CONSTANT_HEAD', 'RIVER_LEAKAGE_OUT'] + >>> zones = ['ZONE_1', 'ZONE_2'] + >>> zb = ZoneBudget('zonebudtest.cbc', zon, kstpkper=(0, 0)) + >>> bud = zb.get_budget(names=names, zones=zones) + + """ + recarray = _get_budget( + self._budget, self._zonenamedict, names=names, zones=zones, net=net + ) + + if pivot: + recarray = _pivot_recarray(recarray) + + return recarray + + def get_volumetric_budget( + self, modeltime, recarray=None, extrapolate_kper=False + ): + """ + Method to generate a volumetric budget table based on flux information + + Parameters + ---------- + modeltime : flopy.discretization.ModelTime object + ModelTime object for calculating volumes + recarray : np.recarray + optional, user can pass in a numpy recarray to calculate volumetric + budget. recarray must be pivoted before passing to + get_volumetric_budget + extrapolate_kper : bool + flag to determine if we fill in data gaps with other + timestep information from the same stress period. + if True, we assume that flux is constant throughout a stress period + and the pandas dataframe returned contains a + volumetric budget per stress period + + if False, calculates volumes from available flux data + + Returns + ------- + pd.DataFrame + + """ + if recarray is None: + recarray = self.get_budget(pivot=True) + return _volumetric_flux(recarray, modeltime, extrapolate_kper) + + def to_csv(self, fname): + """ + Saves the budget record arrays to a formatted + comma-separated values file. + + Parameters + ---------- + fname : str + The name of the output comma-separated values file. + + Returns + ------- + None + + """ + # Needs updating to handle the new budget list structure. Write out + # budgets for all kstpkper if kstpkper is None or pass list of + # kstpkper/totim to save particular budgets. + with open(fname, "w") as f: + # Write header + f.write(",".join(self._budget.dtype.names) + "\n") + # Write rows + for rowidx in range(self._budget.shape[0]): + s = ( + ",".join([str(i) for i in list(self._budget[:][rowidx])]) + + "\n" + ) + f.write(s) + return + + def get_dataframes( + self, + start_datetime=None, + timeunit="D", + index_key="totim", + names=None, + zones=None, + net=False, + pivot=False, + ): + """ + Get pandas dataframes. + + Parameters + ---------- + + start_datetime : str + Datetime string indicating the time at which the simulation starts. + timeunit : str + String that indicates the time units used in the model. + index_key : str + Indicates the fields to be used (in addition to "record") in the + resulting DataFrame multi-index. + names : list of strings + A list of strings containing the names of the records desired. + zones : list of ints or strings + A list of integer zone numbers or zone names desired. + net : boolean + If True, returns net IN-OUT for each record. + pivot : bool + If True, returns dataframe in a more user friendly format + + Returns + ------- + df : Pandas DataFrame + Pandas DataFrame with the budget information. + + Examples + -------- + >>> from flopy.utils.zonbud import ZoneBudget, read_zbarray + >>> zon = read_zbarray('zone_input_file') + >>> zb = ZoneBudget('zonebudtest.cbc', zon, kstpkper=(0, 0)) + >>> df = zb.get_dataframes() + + """ + recarray = self.get_budget(names, zones, net, pivot=pivot) + return _recarray_to_dataframe( + recarray, + self._zonenamedict, + start_datetime=start_datetime, + timeunit=timeunit, + index_key=index_key, + zones=zones, + pivot=pivot, + ) + + @classmethod + def _get_otype(cls, fname): + """ + Method to automatically distinguish output type based on the + zonebudget header + + Parameters + ---------- + fname : str + zonebudget output file name + + Returns + ------- + otype : int + + """ + with open(fname) as foo: + line = foo.readline() + if "zonebudget version" in line.lower(): + otype = 0 + elif "time step" in line.lower(): + otype = 1 + elif "totim" in line.lower(): + otype = 2 + else: + raise AssertionError("Cant distinguish output type") + return otype + + @classmethod + def read_output(cls, fname, net=False, dataframe=False, **kwargs): + """ + Method to read a zonebudget output file into a recarray or pandas + dataframe + + Parameters + ---------- + fname : str + zonebudget output file name + net : bool + boolean flag for net budget + dataframe : bool + boolean flag to return a pandas dataframe + + **kwargs + pivot : bool + + start_datetime : str + Datetime string indicating the time at which the simulation + starts. Can be used when pandas dataframe is requested + timeunit : str + String that indicates the time units used in the model. + + + Returns + ------- + np.recarray + """ + otype = ZoneBudget._get_otype(fname) + if otype == 0: + recarray = _read_zb_zblst(fname) + elif otype == 1: + recarray = _read_zb_csv(fname) + else: + add_prefix = kwargs.pop("add_prefix", True) + recarray = _read_zb_csv2(fname, add_prefix=add_prefix) + + zonenamdict = { + int(i.split("_")[-1]): i + for i in recarray.dtype.names + if i.startswith("ZONE") + } + pivot = kwargs.pop("pivot", False) + recarray = _get_budget(recarray, zonenamdict, net=net) + if pivot: + recarray = _pivot_recarray(recarray) + + if not dataframe: + return recarray + else: + start_datetime = kwargs.pop("start_datetime", None) + timeunit = kwargs.pop("timeunit", "D") + return _recarray_to_dataframe( + recarray, + zonenamdict, + start_datetime=start_datetime, + timeunit=timeunit, + pivot=pivot, + ) + + @classmethod + def read_zone_file(cls, fname): + """Method to read a zonebudget zone file into memory + + Parameters + ---------- + fname : str + zone file name + + Returns + ------- + zones : np.array + + """ + with open(fname, "r") as f: + lines = f.readlines() + + # Initialize layer + lay = 0 + + # Initialize data counter + totlen = 0 + i = 0 + + # First line contains array dimensions + dimstring = lines.pop(0).strip().split() + nlay, nrow, ncol = [int(v) for v in dimstring] + zones = np.zeros((nlay, nrow, ncol), dtype=np.int32) + + # The number of values to read before placing + # them into the zone array + datalen = nrow * ncol + + # List of valid values for LOCAT + locats = ["CONSTANT", "INTERNAL", "EXTERNAL"] + + # ITERATE OVER THE ROWS + for line in lines: + rowitems = line.strip().split() + + # Skip blank lines + if len(rowitems) == 0: + continue + + # HEADER + if rowitems[0].upper() in locats: + vals = [] + locat = rowitems[0].upper() + + if locat == "CONSTANT": + iconst = int(rowitems[1]) + else: + fmt = rowitems[1].strip("()") + fmtin, iprn = [int(v) for v in fmt.split("I")] + + # ZONE DATA + else: + if locat == "CONSTANT": + vals = np.ones((nrow, ncol), dtype=int) * iconst + lay += 1 + elif locat == "INTERNAL": + # READ ZONES + rowvals = [int(v) for v in rowitems] + s = "Too many values encountered on this line." + assert len(rowvals) <= fmtin, s + vals.extend(rowvals) + + elif locat == "EXTERNAL": + # READ EXTERNAL FILE + fname = rowitems[0] + if not os.path.isfile(fname): + errmsg = 'Could not find external file "{}"'.format( + fname + ) + raise Exception(errmsg) + with open(fname, "r") as ext_f: + ext_flines = ext_f.readlines() + for ext_frow in ext_flines: + ext_frowitems = ext_frow.strip().split() + rowvals = [int(v) for v in ext_frowitems] + vals.extend(rowvals) + if len(vals) != datalen: + errmsg = ( + "The number of values read from external " + 'file "{}" does not match the expected ' + "number.".format(len(vals)) + ) + raise Exception(errmsg) + else: + # Should not get here + raise Exception("Locat not recognized: {}".format(locat)) + + # IGNORE COMPOSITE ZONES + + if len(vals) == datalen: + # place values for the previous layer into the zone array + vals = np.array(vals, dtype=int).reshape((nrow, ncol)) + zones[lay, :, :] = vals[:, :] + lay += 1 + totlen += len(rowitems) + i += 1 + s = ( + "The number of values read ({:,.0f})" + " does not match the number expected" + " ({:,.0f})".format(totlen, nlay * nrow * ncol) + ) + assert totlen == nlay * nrow * ncol, s + return zones + + @classmethod + def write_zone_file(cls, fname, array, fmtin=None, iprn=None): + """ + Saves a numpy array in a format readable by the zonebudget program + executable. + + File format: + line 1: nlay, nrow, ncol + line 2: INTERNAL (format) + line 3: begin data + . + . + . + + example from NACP: + 19 250 500 + INTERNAL (10I7) + 199 199 199 199 199 199 199 199 199 + 199 199 199 199 199 199 199 199 199 + ... + INTERNAL (10I7) + 199 199 199 199 199 199 199 199 199 + 199 199 199 199 199 199 199 199 199 + ... + + Parameters + ---------- + array : array + The array of zones to be written. + fname : str + The path and name of the file to be written. + fmtin : int + The number of values to write to each line. + iprn : int + Padding space to add between each value. + + Returns + ------- + + """ + if len(array.shape) == 2: + b = np.zeros((1, array.shape[0], array.shape[1]), dtype=np.int32) + b[0, :, :] = array[:, :] + array = b.copy() + elif len(array.shape) < 2 or len(array.shape) > 3: + raise Exception( + "Shape of the input array is not recognized: {}".format( + array.shape + ) + ) + if np.ma.is_masked(array): + array = np.ma.filled(array, 0) + + nlay, nrow, ncol = array.shape + + if fmtin is not None: + assert fmtin <= ncol, ( + "The specified width is greater than the " + "number of columns in the array." + ) + else: + fmtin = ncol + + iprnmin = len(str(array.max())) + if iprn is None or iprn <= iprnmin: + iprn = iprnmin + 1 + + formatter_str = "{{:>{iprn}}}".format(iprn=iprn) + formatter = formatter_str.format + + with open(fname, "w") as f: + header = "{nlay} {nrow} {ncol}\n".format( + nlay=nlay, nrow=nrow, ncol=ncol + ) + f.write(header) + for lay in range(nlay): + record_2 = "INTERNAL\t({fmtin}I{iprn})\n".format( + fmtin=fmtin, iprn=iprn + ) + f.write(record_2) + if fmtin < ncol: + for row in range(nrow): + rowvals = array[lay, row, :].ravel() + start = 0 + end = start + fmtin + vals = rowvals[start:end] + while len(vals) > 0: + s = ( + "".join([formatter(int(val)) for val in vals]) + + "\n" + ) + f.write(s) + start = end + end = start + fmtin + vals = rowvals[start:end] + + elif fmtin == ncol: + for row in range(nrow): + vals = array[lay, row, :].ravel() + f.write( + "".join([formatter(int(val)) for val in vals]) + + "\n" + ) + + def copy(self): + """ + Return a deepcopy of the object. + """ + return copy.deepcopy(self) + + def __deepcopy__(self, memo): + """ + Over-rides the default deepcopy behavior. Copy all attributes except + the CellBudgetFile object which does not copy nicely. + """ + cls = self.__class__ + result = cls.__new__(cls) + memo[id(self)] = result + ignore_attrs = ["cbc"] + for k, v in self.__dict__.items(): + if k not in ignore_attrs: + setattr(result, k, copy.deepcopy(v, memo)) + + # Set CellBudgetFile object attribute manually. This is object + # read-only so should not be problems with pointers from + # multiple objects. + result.cbc = self.cbc + return result + + def __mul__(self, other): + newbud = self._budget.copy() + for f in self._zonenamedict.values(): + newbud[f] = np.array([r for r in newbud[f]]) * other + idx = np.in1d(self._budget["name"], "PERCENT_DISCREPANCY") + newbud[:][idx] = self._budget[:][idx] + newobj = self.copy() + newobj._budget = newbud + return newobj + + def __truediv__(self, other): + newbud = self._budget.copy() + for f in self._zonenamedict.values(): + newbud[f] = np.array([r for r in newbud[f]]) / float(other) + idx = np.in1d(self._budget["name"], "PERCENT_DISCREPANCY") + newbud[:][idx] = self._budget[:][idx] + newobj = self.copy() + newobj._budget = newbud + return newobj + + def __div__(self, other): + newbud = self._budget.copy() + for f in self._zonenamedict.values(): + newbud[f] = np.array([r for r in newbud[f]]) / float(other) + idx = np.in1d(self._budget["name"], "PERCENT_DISCREPANCY") + newbud[:][idx] = self._budget[:][idx] + newobj = self.copy() + newobj._budget = newbud + return newobj + + def __add__(self, other): + newbud = self._budget.copy() + for f in self._zonenamedict.values(): + newbud[f] = np.array([r for r in newbud[f]]) + other + idx = np.in1d(self._budget["name"], "PERCENT_DISCREPANCY") + newbud[:][idx] = self._budget[:][idx] + newobj = self.copy() + newobj._budget = newbud + return newobj + + def __sub__(self, other): + newbud = self._budget.copy() + for f in self._zonenamedict.values(): + newbud[f] = np.array([r for r in newbud[f]]) - other + idx = np.in1d(self._budget["name"], "PERCENT_DISCREPANCY") + newbud[:][idx] = self._budget[:][idx] + newobj = self.copy() + newobj._budget = newbud + return newobj + + +class ZoneBudget6: + """ + Model class for building, editing and running MODFLOW 6 zonebuget + + Parameters + ---------- + name : str + model name for zonebudget + model_ws : str + path to model + exe_name : str + excutable name + extension : str + name file extension + """ + + def __init__( + self, + name="zonebud", + model_ws=".", + exe_name="zbud6", + extension=".zbnam", + ): + from ..mf6.utils import MfGrdFile + from .binaryfile import CellBudgetFile + + self._name = name + self._zon = None + self._grb = None + self._bud = None + self._model_ws = model_ws + self._exe_name = exe_name + + if not extension.startswith("."): + extension = "." + extension + + self._extension = extension + self.zbnam_packages = { + "zon": ZoneFile6, + "bud": CellBudgetFile, + "grb": MfGrdFile, + } + self.package_dict = {} + if self._zon is not None: + self.package_dict["zon"] = self._zon + if self._grb is not None: + self.package_dict["grb"] = self._grb + if self._bud is not None: + self.package_dict["bud"] = self._bud + + self._recarray = None + + def run_model(self, exe_name=None, nam_file=None, silent=False): + """ + Method to run a zonebudget model + + Parameters + ---------- + exe_name : str + optional zonebudget executable name + nam_file : str + optional zonebudget name file name + silent : bool + optional flag to silence output + + Returns + ------- + tuple + """ + from ..mbase import run_model + + if exe_name is None: + exe_name = self._exe_name + if nam_file is None: + nam_file = os.path.join(self._name + self._extension) + return run_model( + exe_name, nam_file, model_ws=self._model_ws, silent=silent + ) + + def __setattr__(self, key, value): + if key in ("zon", "bud", "grb", "cbc"): + self.add_package(key, value) + return + elif key == "model_ws": + raise AttributeError("please use change_model_ws() method") + elif key == "name": + self.change_model_name(value) + super().__setattr__(key, value) + + def __getattr__(self, item): + if item in ("zon", "bud", "grb", "name", "model_ws"): + item = "_{}".format(item) + return super().__getattribute__(item) + + def add_package(self, pkg_name, pkg): + """ + Method to add a package to the ZoneBudget6 object + + Parameters + ---------- + pkg_name : str + three letter package abbreviation + pkg : str or object + either a package file name or package object + + """ + pkg_name = pkg_name.lower() + if pkg_name not in self.zbnam_packages: + if pkg_name == "cbc": + pkg_name = "bud" + else: + raise KeyError( + "{} package is not valid for zonebudget".format(pkg_name) + ) + + if isinstance(pkg, str): + if os.path.exists(os.path.join(self._model_ws, pkg)): + pkg = os.path.join(self._model_ws, pkg) + + func = self.zbnam_packages[pkg_name] + if pkg_name in ("bud", "grb"): + pkg = func(pkg, precision="double") + else: + pkg = func.load(pkg, self) + + else: + pass + + pkg_name = "_{}".format(pkg_name) + self.__setattr__(pkg_name, pkg) + if pkg is not None: + self.package_dict[pkg_name[1:]] = pkg + + def change_model_ws(self, model_ws): + """ + Method to change the model ws for writing a zonebudget + model. + + Parameters + ---------- + model_ws : str + new model directory + + """ + self._model_ws = model_ws + + def change_model_name(self, name): + """ + Method to change the model name for writing a zonebudget + model. + + Parameters + ---------- + name : str + new model name + + """ + self._name = name + if self._zon is not None: + self._zon.filename = "{}.{}".format( + name, self._zon.filename.split(".")[-1] + ) + + def get_dataframes( + self, + start_datetime=None, + timeunit="D", + index_key="totim", + names=None, + zones=None, + net=False, + pivot=False, + ): + """ + Get pandas dataframes. + + Parameters + ---------- + + start_datetime : str + Datetime string indicating the time at which the simulation starts. + timeunit : str + String that indicates the time units used in the model. + index_key : str + Indicates the fields to be used (in addition to "record") in the + resulting DataFrame multi-index. + names : list of strings + A list of strings containing the names of the records desired. + zones : list of ints or strings + A list of integer zone numbers or zone names desired. + net : boolean + If True, returns net IN-OUT for each record. + pivot : bool + If True, returns data in a more user friendly fashion + + Returns + ------- + df : Pandas DataFrame + Pandas DataFrame with the budget information. + + Examples + -------- + >>> from flopy.utils.zonbud import ZoneBudget, read_zbarray + >>> zon = read_zbarray('zone_input_file') + >>> zb = ZoneBudget('zonebudtest.cbc', zon, kstpkper=(0, 0)) + >>> df = zb.get_dataframes() + + """ + recarray = self.get_budget( + names=names, zones=zones, net=net, pivot=pivot + ) + + return _recarray_to_dataframe( + recarray, + self._zon._zonenamedict, + start_datetime=start_datetime, + timeunit=timeunit, + index_key=index_key, + zones=zones, + pivot=pivot, + ) + + def get_budget( + self, f=None, names=None, zones=None, net=False, pivot=False + ): + """ + Method to read and get zonebudget output + + Parameters + ---------- + f : str + zonebudget output file name + names : list of strings + A list of strings containing the names of the records desired. + zones : list of ints or strings + A list of integer zone numbers or zone names desired. + net : boolean + If True, returns net IN-OUT for each record. + pivot : bool + Method to pivot recordarray into a more user friendly method + for working with data + + Returns + ------- + np.recarray + """ + aliases = None + if self._zon is not None: + aliases = self._zon.aliases + + if f is None and self._recarray is None: + f = os.path.join(self._model_ws, self._name + ".csv") + self._recarray = _read_zb_csv2( + f, add_prefix=False, aliases=aliases + ) + elif f is None: + pass + else: + self._recarray = _read_zb_csv2( + f, add_prefix=False, aliases=aliases + ) + + recarray = _get_budget( + self._recarray, + self._zon._zonenamedict, + names=names, + zones=zones, + net=net, + ) + + if pivot: + recarray = _pivot_recarray(recarray) + + return recarray + + def get_volumetric_budget( + self, modeltime, recarray=None, extrapolate_kper=False + ): + """ + Method to generate a volumetric budget table based on flux information + + Parameters + ---------- + modeltime : flopy.discretization.ModelTime object + ModelTime object for calculating volumes + recarray : np.recarray + optional, user can pass in a numpy recarray to calculate volumetric + budget. recarray must be pivoted before passing to + get_volumetric_budget + extrapolate_kper : bool + flag to determine if we fill in data gaps with other + timestep information from the same stress period. + if True, we assume that flux is constant throughout a stress period + and the pandas dataframe returned contains a + volumetric budget per stress period + + if False, calculates volumes from available flux data + + Returns + ------- + pd.DataFrame + + """ + if recarray is None: + recarray = self.get_budget(pivot=True) + return _volumetric_flux(recarray, modeltime, extrapolate_kper) + + def write_input(self, line_length=20): + """ + Method to write a ZoneBudget 6 model to file + + Parameters + ---------- + line_length : int + length of line for izone array + + """ + nam = [] + for pkg_nam, pkg in self.package_dict.items(): + if pkg_nam in ("grb", "bud"): + path = os.path.relpath(pkg.filename, self._model_ws) + else: + path = pkg.filename + pkg.write_input(line_length=line_length) + nam.append(" {} {}\n".format(pkg_nam.upper(), path)) + + path = os.path.join(self._model_ws, self._name + self._extension) + with open(path, "w") as foo: + foo.write("BEGIN ZONEBUDGET\n") + foo.writelines(nam) + foo.write("END ZONEBUDGET\n") + + @staticmethod + def load(nam_file, model_ws="."): + """ + Method to load a zonebudget model from namefile + + Parameters + ---------- + nam_file : str + zonebudget name file + model_ws : str + model workspace path + + Returns + ------- + ZoneBudget6 object + """ + from ..utils.flopy_io import multi_line_strip + + name = nam_file.split(".")[0] + zb6 = ZoneBudget6(name=name, model_ws=model_ws) + with open(os.path.join(model_ws, nam_file)) as foo: + line = multi_line_strip(foo) + if "begin" in line: + while True: + t = multi_line_strip(foo).split() + if t[0] == "end": + break + else: + zb6.add_package(t[0], t[1]) + + return zb6 + + +class ZoneFile6: + """ + Class to build, read, write and edit MODFLOW 6 zonebudget zone files + + Parameters + ---------- + model : ZoneBudget6 object + model object + izone : np.array + numpy array of zone numbers + extension : str + zone file extension name, defaults to ".zon" + aliases : dict + optional dictionary of zone aliases. ex. {1 : "nw_model"} + """ + + def __init__(self, model, izone, extension=".zon", aliases=None): + self.izone = izone + + if not extension.startswith("."): + extension = "." + extension + + self._extension = extension + self._parent = model + self._parent.add_package("zon", self) + self.filename = self._parent.name + extension + self.aliases = aliases + self.allzones = [int(zn) for zn in np.unique(izone) if zn != 0] + self._zonenamedict = OrderedDict( + [(zn, "ZONE_{}".format(zn)) for zn in self.allzones] + ) + + if aliases is not None: + if not isinstance(aliases, dict): + raise TypeError("aliases parameter must be a dictionary") + + pop_list = [] + for zn, alias in aliases.items(): + if zn in self._zonenamedict: + self._zonenamedict[zn] = "_".join(alias.split()) + self.aliases[zn] = "_".join(alias.split()) + else: + pop_list.append(zn) + print("warning: zone number {} not found".format(zn)) + + for p in pop_list: + aliases.pop(p) + + @property + def ncells(self): + """ + Method to get number of model cells + + """ + return self.izone.size + + def write_input(self, f=None, line_length=20): + """ + Method to write the zonebudget 6 file + + Parameters + ---------- + f : str + zone file name + line_length : int + maximum length of line to write in izone array + """ + if f is None: + f = os.path.join(self._parent.model_ws, self.filename) + + with open(f, "w") as foo: + bfmt = [" {:d}"] + foo.write( + "BEGIN DIMENSIONS\n NCELLS {:d}\n" + "END DIMENSIONS\n\n".format(self.ncells) + ) + + foo.write("BEGIN GRIDDATA\n IZONE\n") + foo.write(" INTERNAL FACTOR 1 IPRN 0\n") + izone = np.ravel(self.izone) + i0 = 0 + i1 = line_length + while i1 < self.izone.size: + fmt = "".join(bfmt * line_length) + foo.write(fmt.format(*izone[i0:i1])) + foo.write("\n") + i0 = i1 + i1 += line_length + i1 = self.izone.size - i0 + fmt = "".join(bfmt * i1) + foo.write(fmt.format(*izone[i0:])) + foo.write("\nEND GRIDDATA\n") + + @staticmethod + def load(f, model): + """ + Method to load a Zone file for zonebudget 6. + + Parameter + --------- + f : str + zone file name + model : ZoneBudget6 object + zonebudget 6 model object + + Returns + ------- + ZoneFile6 object + + """ + from ..utils.flopy_io import multi_line_strip + + pkg_ws = os.path.split(f)[0] + with open(f) as foo: + t = [0] + while t[0] != "ncells": + t = multi_line_strip(foo).split() + + ncells = int(t[1]) + + t = [0] + while t[0] != "izone": + t = multi_line_strip(foo).split() + + method = multi_line_strip(foo).split()[0] + + if method in ("internal", "open/close"): + izone = np.zeros((ncells,), dtype=int) + i = 0 + fobj = foo + if method == "open/close": + fobj = open(os.path.join(pkg_ws, t[1])) + while i < ncells: + t = multi_line_strip(fobj) + if t[0] == "open/close": + if fobj != foo: + fobj.close() + fobj = open(os.path.join(pkg_ws, t[1])) + for zn in t: + izone[i] = zn + i += 1 + else: + izone = np.array([t[1]] * ncells, dtype=int) + + zon = ZoneFile6(model, izone) + return zon + + +def _numpyvoid2numeric(a): + # The budget record array has multiple dtypes and a slice returns + # the flexible-type numpy.void which must be converted to a numeric + # type prior to performing reducing functions such as sum() or + # mean() + return np.array([list(r) for r in a]) + + +def sum_flux_tuples(fromzones, tozones, fluxes): + tup = zip(fromzones, tozones, fluxes) + sorted_tups = sort_tuple(tup) + + # Group the sorted tuples by (from zone, to zone) + # itertools.groupby() returns the index (from zone, to zone) and + # a list of the tuples with that index + from_zones = [] + to_zones = [] + fluxes = [] + for (fz, tz), ftup in groupby(sorted_tups, lambda tup: tup[:2]): + f = np.sum([tup[-1] for tup in list(ftup)]) + from_zones.append(fz) + to_zones.append(tz) + fluxes.append(f) + return np.array(from_zones), np.array(to_zones), np.array(fluxes) + + +def sort_tuple(tup, n=2): + """Sort a tuple by the first n values + + tup: tuple + input tuple + n : int + values to sort tuple by (default is 2) + + Returns + ------- + tup : tuple + tuple sorted by the first n values + + """ + return tuple(sorted(tup, key=lambda t: t[:n])) + + +def _recarray_to_dataframe( + recarray, + zonenamedict, + start_datetime=None, + timeunit="D", + index_key="totim", + zones=None, + pivot=False, +): + """ + Method to convert zonebudget recarrays to pandas dataframes + + Parameters + ---------- + recarray : + zonenamedict : + start_datetime : + timeunit : + index_key : + names : + zones : + net : + + Returns + ------- + + pd.DataFrame + """ + try: + import pandas as pd + except Exception as e: + msg = "ZoneBudget.get_dataframes() error import pandas: " + str(e) + raise ImportError(msg) + + valid_index_keys = ["totim", "kstpkper"] + s = 'index_key "{}" is not valid.'.format(index_key) + assert index_key in valid_index_keys, s + + valid_timeunit = ["S", "M", "H", "D", "Y"] + + if timeunit.upper() == "SECONDS": + timeunit = "S" + elif timeunit.upper() == "MINUTES": + timeunit = "M" + elif timeunit.upper() == "HOURS": + timeunit = "H" + elif timeunit.upper() == "DAYS": + timeunit = "D" + elif timeunit.upper() == "YEARS": + timeunit = "Y" + + errmsg = ( + "Specified time units ({}) not recognized. " + "Please use one of ".format(timeunit) + ) + assert timeunit in valid_timeunit, errmsg + ", ".join(valid_timeunit) + "." + + df = pd.DataFrame().from_records(recarray) + if start_datetime is not None and "totim" in list(df): + totim = totim_to_datetime( + df.totim, + start=pd.to_datetime(start_datetime), + timeunit=timeunit, + ) + df["datetime"] = totim + if pivot: + return pd.DataFrame.from_records(recarray) + + index_cols = ["datetime", "name"] + else: + if pivot: + return pd.DataFrame.from_records(recarray) + + if index_key == "totim" and "totim" in list(df): + index_cols = ["totim", "name"] + else: + index_cols = ["time_step", "stress_period", "name"] + + df = df.set_index(index_cols) # .sort_index(level=0) + if zones is not None: + keep_cols = zones + else: + keep_cols = zonenamedict.values() + return df.loc[:, keep_cols] + + +def _get_budget(recarray, zonenamedict, names=None, zones=None, net=False): + """ + Get a list of zonebudget record arrays. + + Parameters + ---------- + recarray : np.recarray + budget recarray + zonenamedict : dict + dictionary of zone names + names : list of strings + A list of strings containing the names of the records desired. + zones : list of ints or strings + A list of integer zone numbers or zone names desired. + net : boolean + If True, returns net IN-OUT for each record. + + Returns + ------- + budget_list : list of record arrays + A list of the zonebudget record arrays. + + """ + if isinstance(names, str): + names = [names] + if isinstance(zones, str): + zones = [zones] + elif isinstance(zones, int): + zones = [zones] + standard_fields = ["time_step", "stress_period", "name"] + if "totim" in recarray.dtype.names: + standard_fields.insert(0, "totim") + select_fields = standard_fields + list(zonenamedict.values()) + select_records = np.where((recarray["name"] == recarray["name"])) + if zones is not None: + for idx, z in enumerate(zones): + if isinstance(z, int): + zones[idx] = zonenamedict[z] + select_fields = standard_fields + zones + + if names is not None: + names = _clean_budget_names(recarray, names) + select_records = np.in1d(recarray["name"], names) + if net: + if names is None: + names = _clean_budget_names(recarray, _get_record_names(recarray)) + net_budget = _compute_net_budget(recarray, zonenamedict) + seen = [] + net_names = [] + for name in names: + if name.endswith("_IN") or name.endswith("_OUT"): + iname = "_".join(name.split("_")[:-1]) + else: + iname = "_".join(name.split("_")[1:]) + if iname not in seen: + seen.append(iname) + else: + net_names.append(iname) + select_records = np.in1d(net_budget["name"], net_names) + return net_budget[select_fields][select_records] + else: + return recarray[select_fields][select_records] + + +def _clean_budget_names(recarray, names): + """ + Method to clean budget names + + Parameters + ---------- + recarray : np.recarray + + names : list + list of names in recarray + + Returns + ------- + list + """ + newnames = [] + mbnames = ["TOTAL_IN", "TOTAL_OUT", "IN-OUT", "PERCENT_DISCREPANCY"] + for name in names: + if name in mbnames: + newnames.append(name) + elif ( + not name.startswith("FROM_") + and not name.startswith("TO_") + and not name.endswith("_IN") + and not name.endswith("_OUT") + ): + newname_in = "FROM_" + name.upper() + newname_out = "TO_" + name.upper() + if newname_in in recarray["name"]: + newnames.append(newname_in) + if newname_out in recarray["name"]: + newnames.append(newname_out) + else: + if name in recarray["name"]: + newnames.append(name) + return newnames + + +def _get_record_names(recarray, stripped=False): + """ + Get a list of water budget record names in the file. + + Returns + ------- + out : list of strings + List of unique text names in the binary file. + + """ + rec_names = np.unique(recarray["name"]) + if not stripped: + return rec_names + else: + seen = [] + for recname in rec_names: + if recname in ["IN-OUT", "TOTAL_IN", "TOTAL_OUT", "IN_OUT"]: + continue + if recname.endswith("_IN"): + recname = recname[:-3] + elif recname.endswith("_OUT"): + recname = recname[:-4] + if recname not in seen: + seen.append(recname) + seen.extend(["IN-OUT", "TOTAL", "IN_OUT"]) + return np.array(seen) + + +def _compute_net_budget(recarray, zonenamedict): + """ + + :param recarray: + :param zonenamedict: + :return: + """ + recnames = _get_record_names(recarray) + innames = [ + n for n in recnames if n.startswith("FROM_") or n.endswith("_IN") + ] + outnames = [ + n for n in recnames if n.startswith("TO_") or n.endswith("_OUT") + ] + select_fields = ["totim", "time_step", "stress_period", "name"] + list( + zonenamedict.values() + ) + if "totim" not in recarray.dtype.names: + select_fields.pop(0) + + select_records_in = np.in1d(recarray["name"], innames) + select_records_out = np.in1d(recarray["name"], outnames) + in_budget = recarray[select_fields][select_records_in] + out_budget = recarray[select_fields][select_records_out] + net_budget = in_budget.copy() + for f in [n for n in zonenamedict.values() if n in select_fields]: + net_budget[f] = np.array([r for r in in_budget[f]]) - np.array( + [r for r in out_budget[f]] + ) + newnames = [] + for n in net_budget["name"]: + if n.endswith("_IN") or n.endswith("_OUT"): + newnames.append("_".join(n.split("_")[:-1])) + else: + newnames.append("_".join(n.split("_")[1:])) + net_budget["name"] = newnames + return net_budget + + +def write_zbarray(fname, X, fmtin=None, iprn=None): + """ + Saves a numpy array in a format readable by the zonebudget program + executable. + + File format: + line 1: nlay, nrow, ncol + line 2: INTERNAL (format) + line 3: begin data + . + . + . + + example from NACP: + 19 250 500 + INTERNAL (10I8) + 199 199 199 199 199 199 199 199 199 199 + 199 199 199 199 199 199 199 199 199 199 + ... + INTERNAL (10I8) + 199 199 199 199 199 199 199 199 199 199 + 199 199 199 199 199 199 199 199 199 199 + ... + + Parameters + ---------- + X : array + The array of zones to be written. + fname : str + The path and name of the file to be written. + fmtin : int + The number of values to write to each line. + iprn : int + Padding space to add between each value. + + """ + warnings.warn( + "Deprecation planned in version" + " 3.3.5 Use ZoneBudget.write_zone_file()", + PendingDeprecationWarning, + ) + ZoneBudget.write_zone_file(fname, X, fmtin, iprn) + + +def _read_zb_zblst(fname): + """Method to read zonebudget zblst output + + Parameters + ---------- + fname : str + zonebudget output file name + + Returns + ------- + np.recarray + """ + with open(fname) as foo: + + data = {} + read_data = False + flow_budget = False + empty = 0 + prefix = "" + while True: + line = foo.readline().strip().upper() + t = line.split() + if t: + if t[-1].strip() == "ZONES.": + line = foo.readline().strip() + zones = [int(i) for i in line.split()] + for zone in zones: + data["TO_ZONE_{}".format(zone)] = [] + data["FROM_ZONE_{}".format(zone)] = [] + + if "FLOW BUDGET FOR ZONE" in line: + flow_budget = True + read_data = False + zlist = [] + empty = 0 + t = line.split() + zone = int(t[4]) + if len(t[7]) > 4: + t.insert(8, t[7][4:]) + kstp = int(t[8]) - 1 + if len(t[11]) > 6: + t.append(t[11][6:]) + kper = int(t[12]) - 1 + if "ZONE" not in data: + data["ZONE"] = [zone] + data["KSTP"] = [kstp] + data["KPER"] = [kper] + else: + data["ZONE"].append(zone) + data["KSTP"].append(kstp) + data["KPER"].append(kper) + + elif line in ("", " "): + empty += 1 + + elif read_data: + if "=" in line: + t = line.split("=") + label = t[0].strip() + if "ZONE" in line: + if prefix == "FROM_": + zlist.append(int(label.split()[1])) + label = "FROM_ZONE_{}".format(label.split()[1]) + else: + label = "TO_ZONE_{}".format(label.split()[-1]) + + elif "TOTAL" in line or "PERCENT DISCREPANCY" in line: + label = "_".join(label.split()) + + elif "IN - OUT" in line: + label = "IN-OUT" + + else: + label = prefix + "_".join(label.split()) + + if label in data: + data[label].append(float(t[1])) + else: + data[label] = [float(t[1])] + + if label == "PERCENT_DISCREPANCY": + # fill in non-connected zones with zeros... + for zone in zones: + if zone in zlist: + continue + data["FROM_ZONE_{}".format(zone)].append(0) + data["TO_ZONE_{}".format(zone)].append(0) + + elif "OUT:" in line: + prefix = "TO_" + + else: + pass + + elif flow_budget: + if "IN:" in line: + prefix = "FROM_" + read_data = True + flow_budget = False + + else: + pass + + if empty >= 30: + break + + return _zb_dict_to_recarray(data) + + +def _read_zb_csv(fname): + """Method to read zonebudget csv output + + Parameters + ---------- + fname : str + zonebudget output file name + + Returns + ------- + np.recarray + """ + with open(fname) as foo: + data = {} + zone_header = False + read_data = False + empty = 0 + while True: + line = foo.readline().strip().upper() + + if "TIME STEP" in line: + t = line.split(",") + kstp = int(t[1]) - 1 + kper = int(t[3]) - 1 + totim = float(t[5]) + if "KSTP" not in data: + data["KSTP"] = [] + data["KPER"] = [] + data["TOTIM"] = [] + data["ZONE"] = [] + + zone_header = True + empty = 0 + + elif zone_header: + t = line.split(",") + zones = [int(i.split()[-1]) for i in t[1:] if i not in ("",)] + + for zone in zones: + data["KSTP"].append(kstp) + data["KPER"].append(kper) + data["ZONE"].append(zone) + data["TOTIM"].append(totim) + + zone_header = False + read_data = True + + elif read_data: + + t = line.split(",") + if "IN" in t[1]: + prefix = "FROM_" + + elif "OUT" in t[1]: + prefix = "TO_" + + else: + if "ZONE" in t[0] or "TOTAL" in t[0] or "IN-OUT" in t[0]: + label = "_".join(t[0].split()) + elif "PERCENT ERROR" in line: + label = "_".join(t[0].split()) + read_data = False + else: + label = prefix + "_".join(t[0].split()) + + if label not in data: + data[label] = [] + + for val in t[1:]: + if val in ("",): + continue + + data[label].append(float(val)) + + elif line in ("", " "): + empty += 1 + + else: + pass + + if empty >= 25: + break + + return _zb_dict_to_recarray(data) + + +def _read_zb_csv2(fname, add_prefix=True, aliases=None): + """ + Method to read CSV2 output from zonebudget and CSV output + from Zonebudget6 + + Parameters + ---------- + fname : str + zonebudget output file name + add_prefix : bool + boolean flag to add "TO_", "FROM_" prefixes to column headings + Returns + ------- + np.recarray + """ + with open(fname) as foo: + # read the header and create the dtype + h = foo.readline().upper().strip().split(",") + h = [i.strip() for i in h if i] + dtype = [] + prefix = "FROM_" + for col in h: + col = col.replace("-", "_") + if not add_prefix: + prefix = "" + if col in ("TOTIM", "PERIOD", "STEP", "KSTP", "KPER", "ZONE"): + if col in ("ZONE", "STEP", "KPER", "KSTP", "PERIOD"): + if col == "STEP": + col = "KSTP" + elif col == "PERIOD": + col = "KPER" + dtype.append((col, int)) + + else: + dtype.append((col, float)) + + elif col == "TOTAL IN": + dtype.append(("_".join(col.split()), float)) + prefix = "TO_" + elif col == "TOTAL OUT": + dtype.append(("_".join(col.split()), float)) + prefix = "" + elif col in ("FROM OTHER ZONES", "TO OTHER ZONES"): + dtype.append(("_".join(col.split()), float)) + elif col == "IN_OUT": + dtype.append(("IN-OUT", float)) + else: + dtype.append((prefix + "_".join(col.split()), float)) + + array = np.genfromtxt(foo, delimiter=",").T + if len(array) != len(dtype): + array = array[:-1] + array.shape = (len(dtype), -1) + data = {name[0]: list(array[ix]) for ix, name in enumerate(dtype)} + data["KPER"] = list(np.array(data["KPER"]) - 1) + data["KSTP"] = list(np.array(data["KSTP"]) - 1) + return _zb_dict_to_recarray(data, aliases=aliases) + + +def _zb_dict_to_recarray(data, aliases=None): + """ + Method to check the zonebudget dictionary and convert it to a + numpy recarray. + + Parameters + ---------- + data : dict + dictionary of zonebudget data from CSV 1 or ZBLST files + + Returns + ------- + np.recarray + """ + # if steady state is used, storage will not be written + if "FROM_STORAGE" in data: + if len(data["FROM_STORAGE"]) < len(data["ZONE"]): + adj = len(data["ZONE"]) - len(data["FROM_STORAGE"]) + adj = [0] * adj + data["FROM_STORAGE"] = adj + data["FROM_STORAGE"] + data["TO_STORAGE"] = adj + data["TO_STORAGE"] + + zones = list(np.unique(data["ZONE"])) + zone_dtypes = [] + for zn in zones: + if aliases is not None: + if zn in aliases: + zone_dtypes.append((aliases[zn], float)) + else: + zone_dtypes.append(("ZONE_{}".format(int(zn)), float)) + else: + zone_dtypes.append(("ZONE_{}".format(int(zn)), float)) + + dtype = [ + ("totim", float), + ("time_step", int), + ("stress_period", int), + ("name", object), + ] + zone_dtypes + + if "TOTIM" not in data: + dtype.pop(0) + + array = [] + allzones = data["ZONE"] + for strt in range(0, len(data["ZONE"]), len(zones)): + end = strt + len(zones) + kstp = data["KSTP"][strt] + kper = data["KPER"][strt] + totim = None + if "TOTIM" in data: + totim = data["TOTIM"][strt] + + for name, values in data.items(): + if name in ("KSTP", "KPER", "TOTIM", "ZONE"): + continue + rec = [kstp, kper, name] + if totim is not None: + rec = [totim] + rec + tmp = values[strt:end] + tzones = allzones[strt:end] + # check zone numbering matches header numbering, if not re-order + if tzones != zones: + idx = [zones.index(z) for z in tzones] + tmp = [tmp[i] for i in idx] + + array.append(tuple(rec + tmp)) + + array = np.array(array, dtype=dtype) + return array.view(type=np.recarray) + + +def read_zbarray(fname): + """ + Reads an ascii array in a format readable by the zonebudget program + executable. + + Parameters + ---------- + fname : str + The path and name of the file to be written. + + Returns + ------- + zones : numpy ndarray + An integer array of the zones. + """ + warnings.warn( + "Deprecation planned for version 3.3.5, " + "use ZoneBudget.read_zone_file()", + PendingDeprecationWarning, + ) + return ZoneBudget.read_zone_file(fname) + + +def _pivot_recarray(recarray): + """ + Method to pivot the zb output recarray to be compatible + with the ZoneBudgetOutput method until the class is deprecated + + Returns + ------- + + """ + dtype = [("totim", float), ("kper", int), ("kstp", int), ("zone", int)] + record_names = np.unique(recarray["name"]) + for rec_name in record_names: + dtype.append((rec_name, float)) + + rnames = recarray.dtype.names + zones = {i: int(i.split("_")[-1]) for i in rnames if i.startswith("ZONE")} + + kstp_kper = np.vstack( + sorted({(rec["time_step"], rec["stress_period"]) for rec in recarray}) + ) + pvt_rec = np.recarray((1,), dtype=dtype) + n = 0 + for kstp, kper in kstp_kper: + idxs = np.where( + (recarray["time_step"] == kstp) + & (recarray["stress_period"] == kper) + ) + if len(idxs) == 0: + pass + else: + temp = recarray[idxs] + for zonename, zone in zones.items(): + if n != 0: + pvt_rec.resize((len(pvt_rec) + 1,), refcheck=False) + pvt_rec["kstp"][-1] = kstp + pvt_rec["kper"][-1] = kper + pvt_rec["zone"][-1] = zone + for rec in temp: + pvt_rec[rec["name"]][-1] = rec[zonename] + + if "totim" in rnames: + pvt_rec["totim"][-1] = temp["totim"][-1] + else: + pvt_rec["totim"][-1] = 0 + + n += 1 + return pvt_rec + + +def _volumetric_flux(recarray, modeltime, extrapolate_kper=False): + """ + Method to generate a volumetric budget table based on flux information + + Parameters + ---------- + recarray : np.recarray + pivoted numpy recarray of zonebudget fluxes + modeltime : flopy.discretization.ModelTime object + flopy modeltime object + extrapolate_kper : bool + flag to determine if we fill in data gaps with other + timestep information from the same stress period. + if True, we assume that flux is constant throughout a stress period + and the pandas dataframe returned contains a + volumetric budget per stress period + + if False, calculates volumes from available flux data + + Returns + ------- + pd.DataFrame + + """ + import pandas as pd + + nper = len(modeltime.nstp) + volumetric_data = {} + zones = np.unique(recarray["zone"]) + + for key in recarray.dtype.names: + volumetric_data[key] = [] + + if extrapolate_kper: + volumetric_data.pop("kstp") + perlen = modeltime.perlen + totim = np.add.accumulate(perlen) + for per in range(nper): + idx = np.where(recarray["kper"] == per)[0] + + if len(idx) == 0: + continue + + temp = recarray[idx] + + for zone in zones: + if zone == 0: + continue + + zix = np.where(temp["zone"] == zone)[0] + + if len(zix) == 0: + raise Exception + + for key in recarray.dtype.names: + if key == "totim": + volumetric_data[key].append(totim[per]) + + elif key == "tslen": + volumetric_data["perlen"].append(perlen[per]) + + elif key == "kstp": + continue + + elif key == "kper": + volumetric_data[key].append(per) + + elif key == "zone": + volumetric_data[key].append(zone) + + else: + t = temp[zix][key] + tmp = np.nanmean(temp[zix][key]) + vol = tmp * perlen[per] + volumetric_data[key].append(vol) + + else: + n = 0 + tslen = {} + dtotim = {} + totim = modeltime.totim + for ix, nstp in enumerate(modeltime.nstp): + for stp in range(nstp): + idx = np.where( + (recarray["kper"] == ix) & (recarray["kstp"] == stp) + ) + if len(idx[0]) == 0: + continue + elif n == 0: + tslen[(stp, ix)] = totim[n] + else: + tslen[(stp, ix)] = totim[n] - totim[n - 1] + dtotim[(stp, ix)] = totim[n] + n += 1 + + ltslen = [tslen[(rec["kstp"], rec["kper"])] for rec in recarray] + if len(np.unique(recarray["totim"])) == 1: + ltotim = [dtotim[(rec["kstp"], rec["kper"])] for rec in recarray] + recarray["totim"] = ltotim + + for name in recarray.dtype.names: + if name in ("zone", "kstp", "kper", "tslen", "totim"): + volumetric_data[name] = recarray[name] + else: + volumetric_data[name] = recarray[name] * ltslen + + return pd.DataFrame.from_dict(volumetric_data) + + +class ZoneBudgetOutput: + """ + DEPRECATED: Class method to process zonebudget output into + volumetric budgets + + Parameters + ---------- + f : str + zonebudget output file path + dis : flopy.modflow.ModflowDis object + zones : np.ndarray + numpy array of zones + + """ + + def __init__(self, f, dis, zones=None): + import pandas as pd + from ..modflow import ModflowDis + + warnings.warn( + "ZoneBudgetOutput will be deprecated in version 3.3.5," + "Use ZoneBudget.read_output(, pivot=True)" + " or ZoneBudget6.get_budget(, pivot=True)", + PendingDeprecationWarning, + ) + + self._filename = f + self._otype = None + self._zones = zones + self.__pd = pd + + if isinstance(dis, ModflowDis): + add_prefix = True + model = dis.parent + else: + add_prefix = False + modelname = list(dis.model_or_sim.model_dict.keys())[0] + model = dis.model_or_sim.model_dict[modelname] + + self._modeltime = model.modeltime + self._data = ZoneBudget.read_output(f, add_prefix=add_prefix, net=True) + + def __repr__(self): + """ + String representation of the ZoneBudgetOutput class + + """ + zones = ", ".join([str(i) for i in self.zones]) + l = [ + "ZoneBudgetOutput Class", + "----------------------\n", + "Number of zones: {}".format(len(self.zones)), + "Unique zones: {}".format(zones), + "Number of buget records: {}".format(len(self.dataframe)), + ] + + return "\n".join(l) + + @property + def zone_array(self): + """ + Property method to get the zone array + + """ + warnings.warn( + "ZoneBudgetOutput will be deprecated in version 3.3.5", + PendingDeprecationWarning, + ) + return np.asarray(self._zones, dtype=int) + + @property + def zones(self): + """ + Get a unique list of zones + + """ + warnings.warn( + "ZoneBudgetOutput will be deprecated in version 3.3.5", + PendingDeprecationWarning, + ) + return np.unique(self.zone_array) + + @property + def dataframe(self): + """ + Returns a net flux dataframe of the zonebudget output + + """ + warnings.warn( + "ZoneBudgetOutput will be deprecated in version 3.3.5", + PendingDeprecationWarning, + ) + data = _pivot_recarray(self._data) + return self.__pd.DataFrame.from_records(data) + + def export(self, f, ml, **kwargs): + """ + Method to export a netcdf file, or add zonebudget output to + an open netcdf file instance + + Parameters + ---------- + f : str or flopy.export.netcdf.NetCdf object + ml : flopy.modflow.Modflow or flopy.mf6.ModflowGwf object + **kwargs : + logger : flopy.export.netcdf.Logger instance + masked_vals : list + list of values to mask + + Returns + ------- + flopy.export.netcdf.NetCdf object + + """ + warnings.warn( + "ZoneBudgetOutput will be deprecated in version 3.3.5", + PendingDeprecationWarning, + ) + from flopy.export.utils import output_helper + + if isinstance(f, str): + if not f.endswith(".nc"): + raise AssertionError( + "File extension must end with .nc to " + "export a netcdf file" + ) + + zbncfobj = self.dataframe_to_netcdf_fmt(self.dataframe) + oudic = {"zbud": zbncfobj} + return output_helper(f, ml, oudic, **kwargs) + + def volumetric_flux(self, extrapolate_kper=False): + """ + Method to generate a volumetric budget table based on flux information + + Parameters + ---------- + extrapolate_kper : bool + flag to determine if we fill in data gaps with other + timestep information from the same stress period. + if True, we assume that flux is constant throughout a stress period + and the pandas dataframe returned contains a + volumetric budget per stress period + + if False, calculates volumes from available flux data + + Returns + ------- + pd.DataFrame + + """ + warnings.warn( + "ZoneBudgetOutput.volumetric_flux()" + " will be deprecated in version 3.3.5,", + PendingDeprecationWarning, + ) + recarray = _pivot_recarray(self._data) + return _volumetric_flux(recarray, self._modeltime, extrapolate_kper) + + def dataframe_to_netcdf_fmt(self, df, flux=True): + """ + Method to transform a volumetric zonebudget dataframe into + array format for netcdf. + + time is on axis 0 + zone is on axis 1 + + Parameters + ---------- + df : pd.DataFrame + flux : bool + boolean flag to indicate if budget data is a flux "L^3/T" (True, + default) or if the data have been processed to + volumetric values "L^3" (False) + zone_array : np.ndarray + zonebudget zones array + + Returns + ------- + ZBNetOutput object + + """ + warnings.warn( + "ZoneBudgetOutput will be deprecated in version 3.3.5", + PendingDeprecationWarning, + ) + zones = np.sort(np.unique(df.zone.values)) + totim = np.sort(np.unique(df.totim.values)) + + data = {} + for col in df.columns: + if col in ("totim", "zone", "kper", "kstp", "perlen"): + pass + else: + data[col] = np.zeros((totim.size, zones.size), dtype=float) + + for i, time in enumerate(totim): + tdf = df.loc[ + df.totim.isin( + [ + time, + ] + ) + ] + tdf = tdf.sort_values(by=["zone"]) + + for col in df.columns: + if col in ("totim", "zone", "kper", "kstp", "perlen"): + pass + else: + data[col][i, :] = tdf[col].values + + return ZBNetOutput(zones, totim, data, self.zone_array, flux=flux) + + +class ZBNetOutput: + """ + Class that holds zonebudget netcdf output and allows export utilities + to recognize the output data type. + + Parameters + ---------- + zones : np.ndarray + array of zone numbers + time : np.ndarray + array of totim + arrays : dict + dictionary of budget term arrays. + axis 0 is totim, + axis 1 is zones + flux : bool + boolean flag to indicate if budget data is a flux "L^3/T"(True, + default) or if the data have been processed to + volumetric values "L^3" (False) + """ + + def __init__(self, zones, time, arrays, zone_array, flux=True): + self.zones = zones + self.time = time + self.arrays = arrays + self.zone_array = zone_array + self.flux = flux