diff --git a/.docs/md/version_changes.md b/.docs/md/version_changes.md index 4620819950..42fe779c7d 100644 --- a/.docs/md/version_changes.md +++ b/.docs/md/version_changes.md @@ -1,4 +1,13 @@ # Changelog +### Version 3.8.2 + +#### Bug fixes + +* [fix(mp7particledata)](https://github.com/modflowpy/flopy/commit/a0e9219407b7be208d38f4f902cd2b5b96da1351): Fix get_extent() vertical extent calculation (#2307). Committed by wpbonelli on 2024-09-12. +* [fix(array3d_export)](https://github.com/modflowpy/flopy/commit/693f01a0ce41f08d05395832b540fcc4f7dcff43): Fix exporting of array3d to shp (#2310). Committed by martclanor on 2024-09-16. +* [fix(binaryfile)](https://github.com/modflowpy/flopy/commit/181e101a605bdb9b628c6781abff7b3276aca635): Accommodate windows drives for in-place reversal (#2312). Committed by wpbonelli on 2024-09-16. +* [fix(get_modflow)](https://github.com/modflowpy/flopy/commit/38180335445fa09f5463cf8b9239e6ed0c10bf5b): Accommodate missing ratelimit info on api response (#2320). Committed by wpbonelli on 2024-10-01. + ### Version 3.8.1 #### New features diff --git a/CITATION.cff b/CITATION.cff index 9703b37c87..316641a089 100644 --- a/CITATION.cff +++ b/CITATION.cff @@ -3,8 +3,8 @@ message: If you use this software, please cite both the article from preferred-c references, and the software itself. type: software title: FloPy -version: 3.8.1 -date-released: '2024-09-05' +version: 3.8.2 +date-released: '2024-10-03' doi: 10.5066/F7BK19FH abstract: A Python package to create, run, and post-process MODFLOW-based models. repository-artifact: https://pypi.org/project/flopy diff --git a/README.md b/README.md index b2ae65df49..f145e49fb6 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ flopy3 -### Version 3.8.1 +### Version 3.8.2 [![flopy continuous integration](https://github.com/modflowpy/flopy/actions/workflows/commit.yml/badge.svg?branch=develop)](https://github.com/modflowpy/flopy/actions/workflows/commit.yml) [![Read the Docs](https://github.com/modflowpy/flopy/actions/workflows/rtd.yml/badge.svg?branch=develop)](https://github.com/modflowpy/flopy/actions/workflows/rtd.yml) @@ -150,7 +150,7 @@ How to Cite ##### ***Software/Code citation for FloPy:*** -[Bakker, Mark, Post, Vincent, Hughes, J. D., Langevin, C. D., White, J. T., Leaf, A. T., Paulinski, S. R., Bellino, J. C., Morway, E. D., Toews, M. W., Larsen, J. D., Fienen, M. N., Starn, J. J., Brakenhoff, D. A., and Bonelli, W. P., 2024, FloPy v3.8.1: U.S. Geological Survey Software Release, 05 September 2024, https://doi.org/10.5066/F7BK19FH](https://doi.org/10.5066/F7BK19FH) +[Bakker, Mark, Post, Vincent, Hughes, J. D., Langevin, C. D., White, J. T., Leaf, A. T., Paulinski, S. R., Bellino, J. C., Morway, E. D., Toews, M. W., Larsen, J. D., Fienen, M. N., Starn, J. J., Brakenhoff, D. A., and Bonelli, W. P., 2024, FloPy v3.8.2: U.S. Geological Survey Software Release, 03 October 2024, https://doi.org/10.5066/F7BK19FH](https://doi.org/10.5066/F7BK19FH) Additional FloPy Related Publications diff --git a/autotest/test_export.py b/autotest/test_export.py index 0028ada34f..ec0d57a748 100644 --- a/autotest/test_export.py +++ b/autotest/test_export.py @@ -409,7 +409,6 @@ def test_export_shapefile_polygon_closed(function_tmpdir): shp.close() -@excludes_platform("Windows") @requires_pkg("rasterio", "pyshp", "scipy", name_map={"pyshp": "shapefile"}) def test_export_array(function_tmpdir, example_data_path): import rasterio @@ -467,10 +466,10 @@ def test_export_array(function_tmpdir, example_data_path): with rasterio.open(function_tmpdir / "fb.tif") as src: arr = src.read(1) assert src.shape == (m.nrow, m.ncol) - # TODO: these tests currently fail -- fix is in progress - # assert np.abs(src.bounds[0] - m.modelgrid.extent[0]) < 1e-6 - # assert np.abs(src.bounds[1] - m.modelgrid.extent[1]) < 1e-6 - pass + assert np.abs(src.bounds[0] - m.modelgrid.extent[0]) < 1e-6 + assert np.abs(src.bounds[2] - m.modelgrid.extent[1]) < 1e-6 + assert np.abs(src.bounds[1] - m.modelgrid.extent[2]) < 1e-6 + assert np.abs(src.bounds[3] - m.modelgrid.extent[3]) < 1e-6 @requires_pkg("netCDF4", "pyproj") @@ -646,6 +645,65 @@ def test_export_array2(function_tmpdir): assert os.path.isfile(filename), "did not create array shapefile" +@pytest.mark.mf6 +@requires_pkg("pyshp", name_map={"pyshp": "shapefile"}) +def test_array3d_export_structured(function_tmpdir): + from shapefile import Reader + + xll, yll = 468970, 3478635 + xur, yur = 681010, 3716462 + spacing = 20000 + ncol = int((xur - xll) / spacing) + nrow = int((yur - yll) / spacing) + sim = flopy.mf6.MFSimulation("sim", sim_ws=function_tmpdir) + gwf = flopy.mf6.ModflowGwf( + sim, + modelname="array3d_export_unstructured", + ) + flopy.mf6.ModflowGwfdis( + gwf, + nlay=3, + top=5, + botm=[4, 3, 2], + delr=spacing, + delc=spacing, + nrow=nrow, + ncol=ncol, + ) + + shp_file = os.path.join(function_tmpdir, "dis_botm.shp") + gwf.dis.botm.export(shp_file) + + with Reader(shp_file) as shp: + assert list(shp.shapeRecord(-1).record) == [ + 110, # node + 11, # row + 10, # column + 4.0, # botm_1 + 3.0, # botm_2 + 2.0, # botm_3 + ] + + +@requires_pkg("pyshp", name_map={"pyshp": "shapefile"}) +def test_array3d_export_unstructured(function_tmpdir): + from shapefile import Reader + + name = "array3d_export_unstructured" + sim = disu_sim(name, function_tmpdir) + gwf = sim.get_model(name) + + shp_file = function_tmpdir / "disu_bot.shp" + gwf.disu.bot.export(shp_file) + + with Reader(shp_file) as shp: + assert list(shp.shapeRecord(-1).record) == [ + 1770, # node + 3, # layer + 0.0, # bot + ] + + @requires_pkg("pyshp", "shapely", name_map={"pyshp": "shapefile"}) def test_export_array_contours_structured(function_tmpdir): nrow = 7 diff --git a/autotest/test_particledata.py b/autotest/test_particledata.py index f3032cb6e2..f5363f957d 100644 --- a/autotest/test_particledata.py +++ b/autotest/test_particledata.py @@ -26,6 +26,7 @@ ParticleGroupLRCTemplate, ParticleGroupNodeTemplate, ) +from flopy.modpath.mp7particledata import get_extent from flopy.modpath.mp7particlegroup import ParticleGroup from flopy.utils.modpathfile import EndpointFile, PathlineFile @@ -47,6 +48,20 @@ def flatten(a): ] +# test get_extent() + + +def test_get_extent_structured_multilayer(): + grid = GridCases().structured_small() + i, j = 1, 2 + for k in range(grid.nlay): + extent = get_extent(grid, k=k, i=i, j=j) + assert extent.minz == grid.botm[k, i, j] + assert extent.maxz == ( + grid.top[i, j] if k == 0 else grid.botm[k - 1, i, j] + ) + + # test initializers diff --git a/docs/PyPI_release.md b/docs/PyPI_release.md index c9a85823d7..a7e4751714 100644 --- a/docs/PyPI_release.md +++ b/docs/PyPI_release.md @@ -30,4 +30,4 @@ How to Cite *Software/Code citation for FloPy:* -[Bakker, Mark, Post, Vincent, Hughes, J. D., Langevin, C. D., White, J. T., Leaf, A. T., Paulinski, S. R., Bellino, J. C., Morway, E. D., Toews, M. W., Larsen, J. D., Fienen, M. N., Starn, J. J., Brakenhoff, D. A., and Bonelli, W. P., 2024, FloPy v3.8.1: U.S. Geological Survey Software Release, 05 September 2024, https://doi.org/10.5066/F7BK19FH](https://doi.org/10.5066/F7BK19FH) +[Bakker, Mark, Post, Vincent, Hughes, J. D., Langevin, C. D., White, J. T., Leaf, A. T., Paulinski, S. R., Bellino, J. C., Morway, E. D., Toews, M. W., Larsen, J. D., Fienen, M. N., Starn, J. J., Brakenhoff, D. A., and Bonelli, W. P., 2024, FloPy v3.8.2: U.S. Geological Survey Software Release, 03 October 2024, https://doi.org/10.5066/F7BK19FH](https://doi.org/10.5066/F7BK19FH) diff --git a/flopy/export/utils.py b/flopy/export/utils.py index d0b5773983..ef446b7654 100644 --- a/flopy/export/utils.py +++ b/flopy/export/utils.py @@ -1207,16 +1207,22 @@ def array3d_export(f: Union[str, os.PathLike], u3d, fmt=None, **kwargs): f ).suffix.lower() == ".shp": array_dict = {} - for ilay in range(modelgrid.nlay): - u2d = u3d[ilay] - if isinstance(u2d, np.ndarray): - dname = u3d.name - array = u2d - else: - dname = u2d.name - array = u2d.array - name = f"{shapefile_utils.shape_attr_name(dname)}_{ilay + 1}" - array_dict[name] = array + array_shape = u3d.array.shape + + if len(array_shape) == 1: + name = shapefile_utils.shape_attr_name(u3d.name) + array_dict[name] = u3d.array + else: + for ilay in range(array_shape[0]): + u2d = u3d[ilay] + if isinstance(u2d, np.ndarray): + dname = u3d.name + array = u2d + else: + dname = u2d.name + array = u2d.array + name = f"{shapefile_utils.shape_attr_name(dname)}_{ilay + 1}" + array_dict[name] = array shapefile_utils.write_grid_shapefile(f, modelgrid, array_dict) elif isinstance(f, NetCdf) or isinstance(f, dict): diff --git a/flopy/modpath/mp7particledata.py b/flopy/modpath/mp7particledata.py index f905219398..472ff19151 100644 --- a/flopy/modpath/mp7particledata.py +++ b/flopy/modpath/mp7particledata.py @@ -809,7 +809,14 @@ def get_extent(grid, k=None, i=None, j=None, nn=None, localz=False) -> Extent: # get cell coords and span in each dimension if not (k is None or i is None or j is None): verts = grid.get_cell_vertices(i, j) - minz, maxz = (0, 1) if localz else (grid.botm[k, i, j], grid.top[i, j]) + minz, maxz = ( + (0, 1) + if localz + else ( + grid.botm[k, i, j], + grid.top[i, j] if k == 0 else grid.botm[k - 1, i, j], + ) + ) elif nn is not None: verts = grid.get_cell_vertices(nn) if grid.grid_type == "structured": diff --git a/flopy/utils/binaryfile.py b/flopy/utils/binaryfile.py index fb71410926..ce95d500f8 100644 --- a/flopy/utils/binaryfile.py +++ b/flopy/utils/binaryfile.py @@ -13,6 +13,7 @@ import tempfile import warnings from pathlib import Path +from shutil import move from typing import List, Optional, Union import numpy as np @@ -460,9 +461,9 @@ class BinaryLayerFile(LayerFile): """ def __init__( - self, filename: Union[str, os.PathLike], precision, verbose, kwargs + self, filename: Union[str, os.PathLike], precision, verbose, **kwargs ): - super().__init__(filename, precision, verbose, kwargs) + super().__init__(filename, precision, verbose, **kwargs) def _build_index(self): """ @@ -661,7 +662,7 @@ def __init__( self.header_dtype = BinaryHeader.set_dtype( bintype="Head", precision=precision ) - super().__init__(filename, precision, verbose, kwargs) + super().__init__(filename, precision, verbose, **kwargs) def reverse(self, filename: Optional[os.PathLike] = None): """ @@ -733,10 +734,18 @@ def reverse_header(header): header["pertim"] = perlen - header["pertim"] return header - # reverse record order and write to temporary file - temp_dir_path = Path(tempfile.gettempdir()) - temp_file_path = temp_dir_path / filename.name - with open(temp_file_path, "wb") as f: + target = filename + + # if rewriting the same file, write + # temp file then copy it into place + inplace = filename == self.filename + if inplace: + temp_dir_path = Path(tempfile.gettempdir()) + temp_file_path = temp_dir_path / filename.name + target = temp_file_path + + # reverse record order + with open(target, "wb") as f: for i in range(len(self) - 1, -1, -1): header = self.recordarray[i].copy() header = reverse_header(header) @@ -752,16 +761,10 @@ def reverse_header(header): ilay=ilay, ) - # if we're rewriting the original file, close it first - if filename == self.filename: - self.close() - - # move temp file to destination - temp_file_path.replace(filename) - # if we rewrote the original file, reinitialize - if filename == self.filename: - super().__init__(self.filename, self.precision, self.verbose, {}) + if inplace: + move(target, filename) + super().__init__(filename, self.precision, self.verbose) class UcnFile(BinaryLayerFile): @@ -828,7 +831,7 @@ def __init__( self.header_dtype = BinaryHeader.set_dtype( bintype="Ucn", precision=precision ) - super().__init__(filename, precision, verbose, kwargs) + super().__init__(filename, precision, verbose, **kwargs) return @@ -898,7 +901,7 @@ def __init__( self.header_dtype = BinaryHeader.set_dtype( bintype="Head", precision=precision ) - super().__init__(filename, precision, verbose, kwargs) + super().__init__(filename, precision, verbose, **kwargs) def _get_data_array(self, totim=0.0): """ @@ -2325,10 +2328,17 @@ def reverse(self, filename: Optional[os.PathLike] = None): # get number of records nrecords = len(self) - # open backward budget file - temp_dir_path = Path(tempfile.gettempdir()) - temp_file_path = temp_dir_path / filename.name - with open(temp_file_path, "wb") as f: + target = filename + + # if rewriting the same file, write + # temp file then copy it into place + inplace = filename == self.filename + if inplace: + temp_dir_path = Path(tempfile.gettempdir()) + temp_file_path = temp_dir_path / filename.name + target = temp_file_path + + with open(target, "wb") as f: # loop over budget file records in reverse order for idx in range(nrecords - 1, -1, -1): # load header array @@ -2415,13 +2425,7 @@ def reverse(self, filename: Optional[os.PathLike] = None): # Write data data.tofile(f) - # if we're rewriting the original file, close it first - if filename == self.filename: - self.close() - - # move temp file to destination - temp_file_path.replace(filename) - # if we rewrote the original file, reinitialize - if filename == self.filename: - self.__init__(self.filename, self.precision, self.verbose) + if inplace: + move(target, filename) + self.__init__(filename, self.precision, self.verbose) diff --git a/flopy/utils/datafile.py b/flopy/utils/datafile.py index ca922c516e..5c32b72920 100644 --- a/flopy/utils/datafile.py +++ b/flopy/utils/datafile.py @@ -157,7 +157,7 @@ class LayerFile: """ def __init__( - self, filename: Union[str, os.PathLike], precision, verbose, kwargs + self, filename: Union[str, os.PathLike], precision, verbose, **kwargs ): from ..discretization.structuredgrid import StructuredGrid diff --git a/flopy/utils/datautil.py b/flopy/utils/datautil.py index aa59a09764..0fba1bdb4b 100644 --- a/flopy/utils/datautil.py +++ b/flopy/utils/datautil.py @@ -606,7 +606,9 @@ def build_list(self, callback): (entry_point[0][-1], new_location) ) else: - entry_point[0].append(callback(entry_point[1])) + entry_point[0].append( + callback(tuple(i + val for i in entry_point[1])) + ) entry_points = new_entry_points def first_item(self): diff --git a/flopy/utils/formattedfile.py b/flopy/utils/formattedfile.py index ca9436ed21..8d87cf6464 100644 --- a/flopy/utils/formattedfile.py +++ b/flopy/utils/formattedfile.py @@ -110,8 +110,8 @@ class FormattedLayerFile(LayerFile): """ - def __init__(self, filename, precision, verbose, kwargs): - super().__init__(filename, precision, verbose, kwargs) + def __init__(self, filename, precision, verbose, **kwargs): + super().__init__(filename, precision, verbose, **kwargs) def _build_index(self): """ @@ -376,7 +376,7 @@ def __init__( **kwargs, ): self.text = text - super().__init__(filename, precision, verbose, kwargs) + super().__init__(filename, precision, verbose, **kwargs) def _get_text_header(self): """ diff --git a/flopy/utils/get_modflow.py b/flopy/utils/get_modflow.py index fae43b0632..6bc75ad4de 100755 --- a/flopy/utils/get_modflow.py +++ b/flopy/utils/get_modflow.py @@ -160,8 +160,8 @@ def get_release(owner=None, repo=None, tag="latest", quiet=False) -> dict: try: with urllib.request.urlopen(request, timeout=10) as resp: result = resp.read() - remaining = int(resp.headers["x-ratelimit-remaining"]) - if remaining <= 10: + remaining = resp.headers.get("x-ratelimit-remaining", None) + if remaining and int(remaining) <= 10: warnings.warn( f"Only {remaining} GitHub API requests remaining " "before rate-limiting" diff --git a/flopy/version.py b/flopy/version.py index f697f5dcc5..cd0fda2a16 100644 --- a/flopy/version.py +++ b/flopy/version.py @@ -1,4 +1,4 @@ # flopy version file automatically created using -# update_version.py on September 05, 2024 13:59:10 +# update_version.py on October 03, 2024 12:12:12 -__version__ = "3.8.1" +__version__ = "3.8.2" diff --git a/version.txt b/version.txt index aaaff91926..00e897bdae 100644 --- a/version.txt +++ b/version.txt @@ -1 +1 @@ -3.8.1 \ No newline at end of file +3.8.2 \ No newline at end of file