From f502ba42de780475b1e43abfda9829951c65d262 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 5 Jan 2026 11:13:21 +0000 Subject: [PATCH 01/14] build(deps): bump actions/checkout from 5 to 6 Bumps [actions/checkout](https://github.com/actions/checkout) from 5 to 6. - [Release notes](https://github.com/actions/checkout/releases) - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/checkout/compare/v5...v6) --- updated-dependencies: - dependency-name: actions/checkout dependency-version: '6' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/build_docs.yaml | 2 +- .github/workflows/release.yml | 6 +++--- .github/workflows/test.yaml | 2 +- .github/workflows/test_releases.yml | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/build_docs.yaml b/.github/workflows/build_docs.yaml index 8322999a..1094300d 100644 --- a/.github/workflows/build_docs.yaml +++ b/.github/workflows/build_docs.yaml @@ -15,7 +15,7 @@ jobs: steps: - name: Checkout source - uses: actions/checkout@v5 + uses: actions/checkout@v6 with: persist-credentials: false diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 3d3571a0..e44bd977 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -22,7 +22,7 @@ jobs: pull-requests: write steps: - name: Checkout master branch - uses: actions/checkout@v5 + uses: actions/checkout@v6 with: ref: master - name: Bump version and push tag @@ -51,7 +51,7 @@ jobs: steps: - name: Checkout source - uses: actions/checkout@v5 + uses: actions/checkout@v6 with: persist-credentials: false ref: master @@ -137,7 +137,7 @@ jobs: steps: - name: Checkout master branch - uses: actions/checkout@v5 + uses: actions/checkout@v6 with: ref: master fetch-tags: true diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 0b7f37ee..a589c4ff 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -29,7 +29,7 @@ jobs: steps: - name: Checkout source - uses: actions/checkout@v5 + uses: actions/checkout@v6 with: persist-credentials: false diff --git a/.github/workflows/test_releases.yml b/.github/workflows/test_releases.yml index e1b577e9..9ed60d44 100644 --- a/.github/workflows/test_releases.yml +++ b/.github/workflows/test_releases.yml @@ -16,7 +16,7 @@ jobs: shell: bash -l {0} steps: - name: Checkout source - uses: actions/checkout@v5 + uses: actions/checkout@v6 - name: Fetch all Git tags run: git fetch --prune --unshallow --tags - name: Get current date From f021ee3662c6a24fce044c48b904ecb293d1200e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 5 Jan 2026 11:13:14 +0000 Subject: [PATCH 02/14] build(deps): bump actions/upload-artifact from 4 to 6 Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 4 to 6. - [Release notes](https://github.com/actions/upload-artifact/releases) - [Commits](https://github.com/actions/upload-artifact/compare/v4...v6) --- updated-dependencies: - dependency-name: actions/upload-artifact dependency-version: '6' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index e44bd977..a4fe6fb2 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -161,7 +161,7 @@ jobs: run: twine check --strict dist/* - name: Upload package - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v6 with: name: dist path: dist From 8a2f4a24e55d43c0b4428baf8e62c914321da34a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 5 Jan 2026 11:13:09 +0000 Subject: [PATCH 03/14] build(deps): bump JamesIves/github-pages-deploy-action Bumps [JamesIves/github-pages-deploy-action](https://github.com/jamesives/github-pages-deploy-action) from 4.7.2 to 4.7.6. - [Release notes](https://github.com/jamesives/github-pages-deploy-action/releases) - [Commits](https://github.com/jamesives/github-pages-deploy-action/compare/v4.7.2...v4.7.6) --- updated-dependencies: - dependency-name: JamesIves/github-pages-deploy-action dependency-version: 4.7.6 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- .github/workflows/build_docs.yaml | 4 ++-- .github/workflows/release.yml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build_docs.yaml b/.github/workflows/build_docs.yaml index 1094300d..15c93ac3 100644 --- a/.github/workflows/build_docs.yaml +++ b/.github/workflows/build_docs.yaml @@ -80,7 +80,7 @@ jobs: make -C docs html - name: Deploy 🚀 - uses: JamesIves/github-pages-deploy-action@v4.7.2 + uses: JamesIves/github-pages-deploy-action@v4.7.6 if: ${{ github.event_name == 'release' }} with: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} @@ -90,7 +90,7 @@ jobs: TARGET_FOLDER: ${{ github.ref }} - name: Deploy 🚀 - uses: JamesIves/github-pages-deploy-action@v4.7.2 + uses: JamesIves/github-pages-deploy-action@v4.7.6 if: ${{ github.event_name == 'push' }} with: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index a4fe6fb2..65032c5f 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -113,7 +113,7 @@ jobs: make -C docs html - name: Deploy 🚀 - uses: JamesIves/github-pages-deploy-action@v4.7.2 + uses: JamesIves/github-pages-deploy-action@v4.7.6 with: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} BRANCH: gh-pages From 486992005da51be212ce1093759f6dcfea30ef3e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 12 Jan 2026 13:24:06 +0000 Subject: [PATCH 04/14] build(deps): bump JamesIves/github-pages-deploy-action Bumps [JamesIves/github-pages-deploy-action](https://github.com/jamesives/github-pages-deploy-action) from 4.7.6 to 4.8.0. - [Release notes](https://github.com/jamesives/github-pages-deploy-action/releases) - [Commits](https://github.com/jamesives/github-pages-deploy-action/compare/v4.7.6...v4.8.0) --- updated-dependencies: - dependency-name: JamesIves/github-pages-deploy-action dependency-version: 4.8.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- .github/workflows/build_docs.yaml | 4 ++-- .github/workflows/release.yml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build_docs.yaml b/.github/workflows/build_docs.yaml index 15c93ac3..a8482fd5 100644 --- a/.github/workflows/build_docs.yaml +++ b/.github/workflows/build_docs.yaml @@ -80,7 +80,7 @@ jobs: make -C docs html - name: Deploy 🚀 - uses: JamesIves/github-pages-deploy-action@v4.7.6 + uses: JamesIves/github-pages-deploy-action@v4.8.0 if: ${{ github.event_name == 'release' }} with: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} @@ -90,7 +90,7 @@ jobs: TARGET_FOLDER: ${{ github.ref }} - name: Deploy 🚀 - uses: JamesIves/github-pages-deploy-action@v4.7.6 + uses: JamesIves/github-pages-deploy-action@v4.8.0 if: ${{ github.event_name == 'push' }} with: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 65032c5f..24b3d3d4 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -113,7 +113,7 @@ jobs: make -C docs html - name: Deploy 🚀 - uses: JamesIves/github-pages-deploy-action@v4.7.6 + uses: JamesIves/github-pages-deploy-action@v4.8.0 with: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} BRANCH: gh-pages From 34ca9110c12e271e48771379288b00984c6e1942 Mon Sep 17 00:00:00 2001 From: "Leaf, Andrew T" Date: Fri, 23 Jan 2026 07:51:11 -0600 Subject: [PATCH 05/14] fix(SFR Package setup): * re-adjust SFR package layering each time idomain is adjusted, to prevent SFR in inactive cells (SFR reaches with no groundwater connections) * write SFR Package shapefiles when MODFLOW input is written, so that the shapefiles reflect the final state of the SFR package * fix some lingering pandas delim_whitespace=True args --- mfsetup/fileio.py | 4 +- mfsetup/mf6model.py | 13 +++- mfsetup/mfmodel.py | 93 ++++++++++++++-------------- mfsetup/mfnwtmodel.py | 10 ++- mfsetup/tests/test_mf6_shellmound.py | 4 +- 5 files changed, 70 insertions(+), 54 deletions(-) diff --git a/mfsetup/fileio.py b/mfsetup/fileio.py index c0884299..45ee28dd 100644 --- a/mfsetup/fileio.py +++ b/mfsetup/fileio.py @@ -124,7 +124,7 @@ def load_array(filename, shape=None, nodata=-9999): print(txt, end=', ') # arr = np.loadtxt # pd.read_csv is >3x faster than np.load_txt - arr = pd.read_csv(f, delim_whitespace=True, header=None).values + arr = pd.read_csv(f, sep=r'\s+', header=None).values if shape2d is not None: if arr.shape != shape2d: if arr.size == np.prod(shape2d): @@ -1146,7 +1146,7 @@ def read_ggofile(gagefile, model, names = [n.lower() for n in names] df = pd.read_csv(src, skiprows=0, header=None, - delim_whitespace=True, + sep=r'\s+', names=names ) kstp = [] diff --git a/mfsetup/mf6model.py b/mfsetup/mf6model.py index a552c458..e6621d09 100644 --- a/mfsetup/mf6model.py +++ b/mfsetup/mf6model.py @@ -220,7 +220,7 @@ def _set_idomain(self): # remove cells that are above stream cells if self.get_package('sfr') is not None: - idomain = deactivate_idomain_above(idomain, self.sfr.packagedata) + idomain = deactivate_idomain_above(idomain, self.sfrdata.reach_data) # inactivate any isolated cells that could cause problems with the solution idomain = find_remove_isolated_cells(idomain, minimum_cluster_size=20) @@ -262,7 +262,10 @@ def _set_idomain(self): data={0: irch}, datatype='array2d', write_fmt='%d', dtype=int) - #self.dis.irch = self.cfg['dis']['irch'] + + if hasattr(self, 'sfrdata'): + self.adjust_sfr_layers_model_bottom() + def _update_grid_configuration_with_dis(self): """Update grid configuration with any information supplied to dis package @@ -1045,6 +1048,12 @@ def skip_write(**kwargs): options=options, external_files_path=model.external_path ) + # write reach and segment data tables + model.sfrdata.write_tables('{}/{}'.format(self._tables_path, self.name)) + + # export shapefiles of lines, routing, cell polygons, inlets and outlets + model.sfrdata.write_shapefiles('{}/{}'.format(self._shapefiles_path, self.name)) + # add version info to package file headers files = [model.namefile] files += [p.filename for p in model.packagelist] diff --git a/mfsetup/mfmodel.py b/mfsetup/mfmodel.py index ab92dbf8..48f64358 100644 --- a/mfsetup/mfmodel.py +++ b/mfsetup/mfmodel.py @@ -1382,6 +1382,36 @@ def _setup_basic_stress_package(self, package, flopy_package_class, print("finished in {:.2f}s\n".format(time.time() - t0)) return pckg + + def adjust_sfr_layers_model_bottom(self): + # assign layers to the sfr reaches + botm = self.dis.botm.array.copy() + if self.version == 'mf6': + idomain = self.dis.idomain.array + else: + idomain = self.bas6.ibound.array + layers, new_botm = assign_layers(self.sfrdata.reach_data, + botm_array=botm, + idomain=idomain) + self.sfrdata.reach_data['k'] = layers + if new_botm is not None: + # run thru setup_array so that DIS input remains open/close + self._setup_array('dis', 'botm', + data={i: arr for i, arr in enumerate(new_botm)}, + datatype='array3d', write_fmt='%.2f', dtype=int) + # reset the bottom array in flopy (and in memory) + # is this necessary? = + self.dis.botm = new_botm + # set bottom array to external files + if self.version == 'mf6': + self.dis.botm = self.cfg['dis']['griddata']['botm'] + else: + self.dis.botm = self.cfg['dis']['botm'] + print('\nModel cell bottom elevations adjusted after assigning ' + 'SFR reaches to layers\n(to accommodate SFR reach bottoms ' + 'below the previous model bottom)\n') + + def setup_grid(self): """Set up the attached modelgrid instance from configuration input """ @@ -1584,32 +1614,10 @@ def setup_sfr(self, **kwargs): else: sfr.reach_data['strtop'] = sfr.interpolate_to_reaches('elevup', 'elevdn') - # assign layers to the sfr reaches - botm = self.dis.botm.array.copy() - if self.version == 'mf6': - idomain = self.dis.idomain.array - else: - idomain = self.bas6.ibound.array - layers, new_botm = assign_layers(sfr.reach_data, - botm_array=botm, - idomain=idomain) - sfr.reach_data['k'] = layers - if new_botm is not None: - # run thru setup_array so that DIS input remains open/close - self._setup_array('dis', 'botm', - data={i: arr for i, arr in enumerate(new_botm)}, - datatype='array3d', write_fmt='%.2f', dtype=int) - # reset the bottom array in flopy (and in memory) - # is this necessary? = - self.dis.botm = new_botm - # set bottom array to external files - if self.version == 'mf6': - self.dis.botm = self.cfg['dis']['griddata']['botm'] - else: - self.dis.botm = self.cfg['dis']['botm'] - print('\nModel cell bottom elevations adjusted after assigning ' - 'SFR reaches to layers\n(to accommodate SFR reach bottoms ' - 'below the previous model bottom)\n') + # attach the sfrmaker.sfrdata instance as an attribute + self.sfrdata = sfr + + self.adjust_sfr_layers_model_bottom() # option to convert reaches to the River Package if self.cfg['sfr'].get('to_riv'): @@ -1617,7 +1625,7 @@ def setup_sfr(self, **kwargs): DeprecationWarning) self.cfg['sfr']['sfrmaker_options']['to_riv'] = self.cfg['sfr'].get('to_riv') if self.cfg['sfr'].get('sfrmaker_options', {}).get('to_riv'): - rivdata = sfr.to_riv(line_ids=self.cfg['sfr']['sfrmaker_options']['to_riv'], + rivdata = self.sfrdata.to_riv(line_ids=self.cfg['sfr']['sfrmaker_options']['to_riv'], drop_in_sfr=True) # setup of RIV package from SFRmaker-derived RIVdata # and any user input @@ -1679,10 +1687,10 @@ def setup_sfr(self, **kwargs): inflows_input['flowline_routing'] = routing if self.version == 'mf6': inflows_input['variable'] = 'inflow' - method = sfr.add_to_perioddata + method = self.sfrdata.add_to_perioddata else: inflows_input['variable'] = 'flow' - method = sfr.add_to_segment_data + method = self.sfrdata.add_to_segment_data kwargs = get_input_arguments(inflows_input.copy(), method) method(**kwargs) @@ -1710,45 +1718,36 @@ def setup_sfr(self, **kwargs): runoff_input['variable'] = 'runoff' runoff_input['distribute_flows_to_reaches'] = True if self.version == 'mf6': - method = sfr.add_to_perioddata + method = self.sfrdata.add_to_perioddata else: - method = sfr.add_to_segment_data + method = self.sfrdata.add_to_segment_data kwargs = get_input_arguments(runoff_input.copy(), method) method(**kwargs) # add observations observations_input = self.cfg['sfr'].get('source_data', {}).get('observations') if self.version != 'mf6': - sfr.gage_starting_unit_number = self.cfg['gag']['starting_unit_number'] + self.sfrdata.gage_starting_unit_number = self.cfg['gag']['starting_unit_number'] if observations_input is not None: key = 'filename' if 'filename' in observations_input else 'filenames' observations_input['data'] = observations_input[key] - kwargs = get_input_arguments(observations_input.copy(), sfr.add_observations) - obsdata = sfr.add_observations(**kwargs) + kwargs = get_input_arguments(observations_input.copy(), self.sfrdata.add_observations) + obsdata = self.sfrdata.add_observations(**kwargs) # resample observations to model stress periods; write to table - # write reach and segment data tables - sfr.write_tables('{}/{}'.format(self._tables_path, self.name)) - - # export shapefiles of lines, routing, cell polygons, inlets and outlets - sfr.write_shapefiles('{}/{}'.format(self._shapefiles_path, self.name)) - # create the flopy SFR package instance - sfr.create_modflow_sfr2(model=self, istcb2=223) + self.sfrdata.create_modflow_sfr2(model=self, istcb2=223) if self.version != 'mf6': - sfr_package = sfr.modflow_sfr2 + sfr_package = self.sfrdata.modflow_sfr2 else: # pass options kwargs through to mf6 constructor kwargs = flatten({k:v for k, v in self.cfg[package].items() if k not in {'source_data', 'flowlines', 'inflows', 'observations', 'inflows_routing', 'dem', 'sfrmaker_options'}}) kwargs = get_input_arguments(kwargs, mf6.ModflowGwfsfr) - sfr_package = sfr.create_mf6sfr(model=self, **kwargs) + sfr_package = self.sfrdata.create_mf6sfr(model=self, **kwargs) # monkey patch ModflowGwfsfr instance to behave like ModflowSfr2 - sfr_package.reach_data = sfr.modflow_sfr2.reach_data - - # attach the sfrmaker.sfrdata instance as an attribute - self.sfrdata = sfr + sfr_package.reach_data = self.sfrdata.modflow_sfr2.reach_data # reset dependent arrays self._reset_bc_arrays() diff --git a/mfsetup/mfnwtmodel.py b/mfsetup/mfnwtmodel.py index f659ee49..9dbe0226 100644 --- a/mfsetup/mfnwtmodel.py +++ b/mfsetup/mfnwtmodel.py @@ -200,7 +200,7 @@ def _set_ibound(self): # remove cells that are above stream cells if self.get_package('sfr') is not None: - ibound = deactivate_idomain_above(ibound, self.sfr.reach_data) + ibound = deactivate_idomain_above(ibound, self.sfrdata.reach_data) # remove cells that are above ghb cells if self.get_package('ghb') is not None: ibound = deactivate_idomain_above(ibound, self.ghb.stress_period_data[0]) @@ -215,6 +215,10 @@ def _set_ibound(self): datatype='array3d', write_fmt='%d', dtype=int) self.bas6.ibound = self.cfg['bas6']['ibound'] + if hasattr(self, 'sfrdata'): + self.adjust_sfr_layers_model_bottom() + + def _set_parent(self): """Set attributes related to a parent or source model if one is specified.""" @@ -851,6 +855,10 @@ def write_input(self): # gage package was already set-up and then written by Flopy if 'SFR' in self.get_package_list(): self.sfrdata.write_package(write_observations_input=False) + # write reach and segment data tables + self.sfrdata.write_tables('{}/{}'.format(self._tables_path, self.name)) + # export shapefiles of lines, routing, cell polygons, inlets and outlets + self.sfrdata.write_shapefiles('{}/{}'.format(self._shapefiles_path, self.name)) # add version info to file headers files = [self.namefile] diff --git a/mfsetup/tests/test_mf6_shellmound.py b/mfsetup/tests/test_mf6_shellmound.py index 8c060948..4cfd8889 100644 --- a/mfsetup/tests/test_mf6_shellmound.py +++ b/mfsetup/tests/test_mf6_shellmound.py @@ -749,7 +749,7 @@ def test_wel_setup(shellmound_model_with_dis): # reason='This test fails on macos CI for an unknown reason; passes locally on macos') def test_sfr_setup(model_with_sfr, project_root_path): m = model_with_sfr - m.sfr.write() + m.write_input() assert os.path.exists(os.path.join(m.model_ws, m.sfr.filename)) assert isinstance(m.sfr, mf6.ModflowGwfsfr) output_path = m._shapefiles_path @@ -971,7 +971,7 @@ def test_model_setup_no_nans(model_setup): assert pckg in package_list assert 'RIV_1' not in package_list m.write_input() - df = pd.read_csv(m.model_ws / 'external/riv_000.dat', delim_whitespace=True) + df = pd.read_csv(m.model_ws / 'external/riv_000.dat', sep=r'\s+') # single RIV external file should have Lake Henry (from riv: block) # and Tallahatchie/Yazoo (via SFRmaker to_riv:) # does flopy force boundnames to lower case as of v3.3.7? From 8a341ad93060fb463d748e2d7b51ccdf77a13cab Mon Sep 17 00:00:00 2001 From: "Leaf, Andrew T" Date: Fri, 23 Jan 2026 09:10:38 -0600 Subject: [PATCH 06/14] ci: still working on pandas 3.0 support * add --no-deps to pip reinstall of flopy from latest release; apparently this was clobbering the conda installs of pandas and other packages on the Windows CI runs --- .github/workflows/test.yaml | 2 +- ci/test_environment.yaml | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index a589c4ff..bf3190c5 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -82,7 +82,7 @@ jobs: - name: Install latest PyPI flopy shell: bash -l {0} if: contains(matrix.os, 'windows') - run: pip install flopy --force-reinstall + run: pip install flopy --force-reinstall --no-deps - name: Install Modflow-setup and ipykernel shell: bash -l {0} run: | diff --git a/ci/test_environment.yaml b/ci/test_environment.yaml index c92d62e4..b1d7ab58 100644 --- a/ci/test_environment.yaml +++ b/ci/test_environment.yaml @@ -13,7 +13,7 @@ dependencies: - scipy - xarray - netcdf4 -- pandas +- pandas<3 - geopandas - gdal - fiona @@ -27,7 +27,7 @@ dependencies: - coverage - flake8 - pytest -- sphinx=8.1.3 +- sphinx - numpydoc - nbsphinx # for rendering notebooks in sphinx-generated docs - sphinx-copybutton From 5425beb717c2cd1d03030ba272d38c898ab88232 Mon Sep 17 00:00:00 2001 From: "Leaf, Andrew T" Date: Fri, 6 Feb 2026 12:37:49 -0600 Subject: [PATCH 07/14] tests(test_sourcedata.py::test_parse_source_data): add TransientArraySourceData.from_config with individual raster input for recharge. --- mfsetup/tests/test_sourcedata.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/mfsetup/tests/test_sourcedata.py b/mfsetup/tests/test_sourcedata.py index ddfd33f4..8a80aa80 100644 --- a/mfsetup/tests/test_sourcedata.py +++ b/mfsetup/tests/test_sourcedata.py @@ -148,8 +148,10 @@ def test_parse_source_data(source_data_cases, # test conversion to model units for i, f in cases[4]['infiltration_arrays']['filenames'].items(): cases[4]['infiltration_arrays']['filenames'][i] = os.path.join(project_root_path, f) - sd = ArraySourceData.from_config(cases[4]['infiltration_arrays'], - variable=var, + sd = TransientArraySourceData.from_config(cases[4]['infiltration_arrays'], + variable='recharge', + data_type='transient2d', + resample_method='nearest', dest_model=model) assert isinstance(sd.filenames, dict) assert sd.unit_conversion == convert_length_units('inches', 'meters') /\ From bc64fdbc2c035395c168391513502e8bb23e39d1 Mon Sep 17 00:00:00 2001 From: "Leaf, Andrew T" Date: Fri, 6 Feb 2026 14:31:38 -0600 Subject: [PATCH 08/14] feat: geopackage input with layernames (specified in format: filename>.gpkg|layername= or filename>.gpkg|layer=), for example as a way to specify the active model extent (e.g. in the source_data: idomain: block for Modflow 6 models) --- docs/source/conf.py | 5 +++ docs/source/release-history.rst | 21 ++++++--- mfsetup/grid.py | 8 ++-- mfsetup/sourcedata.py | 41 +++++++++++++----- mfsetup/tdis.py | 2 + mfsetup/tests/data/shellmound.yml | 2 +- .../data/shellmound/shps/active_area.gpkg | Bin 0 -> 98304 bytes mfsetup/tests/test_mf6_shellmound.py | 11 +++-- mfsetup/tests/test_pleasant_mfnwt_inset.py | 2 +- 9 files changed, 66 insertions(+), 26 deletions(-) create mode 100644 mfsetup/tests/data/shellmound/shps/active_area.gpkg diff --git a/docs/source/conf.py b/docs/source/conf.py index cadd2a5f..b80ebb32 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -75,6 +75,11 @@ autosummary_generate = True numpydoc_show_class_members = False +# Only generate for top n sections +# (and then only use sections headers need to be unique; +# duplicate headers are allowed below this level) +#autosectionlabel_maxdepth=2 + # Add any paths that contain templates here, relative to this directory. templates_path = ['_templates'] diff --git a/docs/source/release-history.rst b/docs/source/release-history.rst index 8888257a..af94cb54 100644 --- a/docs/source/release-history.rst +++ b/docs/source/release-history.rst @@ -2,22 +2,31 @@ Release History =============== +Version 0.8.0 (2026-02-xx) +---------------------------------------- +**New Features:** + * Add support for Pandas 3 + * Add support for geopackage input with layernames (specified in format: filename>.gpkg|layername= or filename>.gpkg|layer=), for example as a way to specify the active model extent (e.g. in the source_data: idomain: block for Modflow 6 models) + +**Bug Fixes:** + * fixes to SFR Package setup: + * re-adjust SFR package layering each time idomain is adjusted, to prevent SFR in inactive cells (SFR reaches with no groundwater connections) + * write SFR Package shapefiles when MODFLOW input is written, so that the shapefiles reflect the final state of the SFR package + Version 0.7.0 (2025-12-31) ---------------------------------------- -New Features: -************** +**New Features:** * Added partial support for GeoPackages (.gpkg) input * Added support for SFRmaker streambed top field elevations input. * Add auto_flow_reduce = 0.1 by default in MF6 model Well Packages -Bug Fixes: -************* +**Bug Fixes:** * Refactor np.in1d to np.isin to resolve numpy error * Refactoring to support various Flopy interface changes * Allow shapefile input with no 'id_column' for basic stress packages * Fix issue with Well Package layer assignment in cases where there is no open interval information (only well bottom). When there is only screen_top or screen_botm, or screen_top == screen_botm, set the screen_top and screen_botm to the top/bottom of layer intersecting the well bottom (i.e. so that the well is placed in this layer). Previously, a zero open interval thickness in these situations was resulting in the well getting re-assigned to the highest transmissivity layer at that i, j location. -* Fix issue with MODFLOW 6 configuration file defaults: Well Package auto_flow_reduce needs to be a float (not True, which will result in an auto flow reduce value of 1.0 being assigned (almost full suppression of pumping); set auto_flow_reduce default of 0.1 -* Fix issue with Well Package Setup: Pass non-default screen_top_col and screen_botm_col to assign_layers_from_screen_top_botm; otherwise an error results when non-default names for these are used. + * Fix issue with MODFLOW 6 configuration file defaults: Well Package auto_flow_reduce needs to be a float (not True, which will result in an auto flow reduce value of 1.0 being assigned (almost full suppression of pumping); set auto_flow_reduce default of 0.1. + * Fix issue with Well Package Setup: Pass non-default screen_top_col and screen_botm_col to assign_layers_from_screen_top_botm; otherwise an error results when non-default names for these are used. Version 0.6.1 (2025-03-17) ---------------------------------------- diff --git a/mfsetup/grid.py b/mfsetup/grid.py index 1a08000b..34701395 100644 --- a/mfsetup/grid.py +++ b/mfsetup/grid.py @@ -633,7 +633,7 @@ def write_bbox_shapefile(modelgrid, outshp): gdf.to_file(outshp, index=False) -def rasterize(feature, grid, id_column=None, +def rasterize(feature, grid, layer=None, id_column=None, include_ids=None, exclude_ids=None, names_column=None, crs=None, **kwargs): """Rasterize a feature onto the model grid, using @@ -642,8 +642,10 @@ def rasterize(feature, grid, id_column=None, Parameters ---------- - feature : str (shapefile path), list of shapely objects, + feature : str (shapefile or geopackage path), list of shapely objects, or dataframe with geometry column + layer : str + Layer name containing the feature (for example, in a geopackage) id_column : str Column with unique integer identifying each feature; values from this column will be assigned to the output raster. @@ -695,7 +697,7 @@ def rasterize(feature, grid, id_column=None, trans = get_transform(grid) if isinstance(feature, str) or isinstance(feature, Path): - df = gpd.read_file(feature) + df = gpd.read_file(feature, layer=layer) elif isinstance(feature, pd.DataFrame): df = feature.copy() df = gpd.GeoDataFrame(df, crs=crs) diff --git a/mfsetup/sourcedata.py b/mfsetup/sourcedata.py index 25849b36..f72c9f88 100644 --- a/mfsetup/sourcedata.py +++ b/mfsetup/sourcedata.py @@ -68,6 +68,8 @@ def __init__(self, filenames=None, values=None, variable=None, length_units='unk """ self.filenames = filenames + # optional layer names for geopackages parsed from filenames + self.file_layers = None self.values = values self.variable = variable self.length_units = length_units @@ -113,21 +115,35 @@ def time_unit_conversion(self): def set_filenames(self, filenames): - def normpath(f): - if self.dest_model is not None and isinstance(f, str): - path = os.path.join(self.dest_model._config_path, f) + def normpath(file_path): + if self.dest_model is not None and isinstance(file_path, str): + file_path = file_path.split('|')[0] + path = os.path.join(self.dest_model._config_path, file_path) normpath = os.path.normpath(path) return normpath - return f + return file_path + + def get_layer_name(file_path): + if isinstance(file_path, str) and len(file_path.split('|')) > 1: + layer_name = file_path.split('|')[1] + layer_name = layer_name.replace('layer=', '')\ + .replace('layername=', '') + return layer_name if isinstance(filenames, str): self.filenames = {0: normpath(filenames)} + # optional layernames for geopackages, denoted with syntax: + # .gpkg|layername= + self.file_layers = {0: get_layer_name(filenames)} elif isinstance(filenames, list): self.filenames = {i: normpath(f) for i, f in enumerate(filenames)} + self.file_layers = {i: get_layer_name(f) for i, f in enumerate(filenames)} elif isinstance(filenames, dict): self.filenames = {i: normpath(f) for i, f in filenames.items()} + self.file_layers = {i: get_layer_name(f) for i, f in filenames.items()} else: - self.filenames = None + self.filenames = dict() + self.file_layers = dict() @classmethod def from_config(cls, data, **kwargs): @@ -365,7 +381,7 @@ def dest_source_layer_mapping(self): "but {} are specified: {}" .format(nlay, nspecified, self.from_source_model_layers)) return self.from_source_model_layers - elif self.filenames is not None: + elif self.filenames: nspecified = len(self.filenames) if self.datatype == 'array3d' and nspecified != nlay: raise Exception("Variable should have {} layers " @@ -441,7 +457,7 @@ def regrid_from_source_model(self, source_array, self.dest_modelgrid.ncol)) return regridded - def _read_array_from_file(self, filename): + def _read_array_from_file(self, filename, layername=None): f = filename if isinstance(f, numbers.Number): data = f @@ -456,8 +472,9 @@ def _read_array_from_file(self, filename): method=self.resample_method) arr = np.reshape(arr, (self.dest_modelgrid.nrow, self.dest_modelgrid.ncol)) - elif f.endswith('.shp'): - arr = rasterize(f, self.dest_modelgrid, id_column=self.id_column) + elif any([f.lower().endswith(i) for i in ['shp', 'gpkg']]): + arr = rasterize(f, self.dest_modelgrid, layer=layername, + id_column=self.id_column) # TODO: add code to interpret hds and cbb files # interpolate from source model using source model grid # otherwise assume the grids are the same @@ -483,9 +500,9 @@ def get_data(self): data = populate_values(self.values, array_shape=(self.dest_modelgrid.nrow, self.dest_modelgrid.ncol)) - if self.filenames is not None: + if self.filenames: for i, f in self.filenames.items(): - data[i] = self._read_array_from_file(f) + data[i] = self._read_array_from_file(f, layername=self.file_layers.get(i)) # interpolate any missing arrays from consecutive files based on weights for i, arr in data.items(): @@ -600,7 +617,7 @@ def get_data(self): # get data from list of files; one per stress period # (files are assumed to be sorted) - if self.filenames is not None: + if self.filenames: source_data = [] for i, f in self.filenames.items(): source_data.append(self._read_array_from_file(f)) diff --git a/mfsetup/tdis.py b/mfsetup/tdis.py index cfd0dba2..9fb753ec 100644 --- a/mfsetup/tdis.py +++ b/mfsetup/tdis.py @@ -680,8 +680,10 @@ def aggregate_dataframe_to_stress_period(data, id_column, data_column, datetime_ data[end_datetime_column] = pd.to_datetime(data[end_datetime_column]) if start_datetime is None: start_datetime = data[datetime_column].iloc[0] + start_datetime = pd.Timestamp(start_datetime) if end_datetime is None: end_datetime = data[datetime_column].iloc[-1] + end_datetime = pd.Timestamp(end_datetime) # >= includes the start datetime # if there is no end_datetime column, select values that have start_datetimes within the period # this excludes values that start before the period but don't have an end date diff --git a/mfsetup/tests/data/shellmound.yml b/mfsetup/tests/data/shellmound.yml index d7cd8544..67a51e2a 100644 --- a/mfsetup/tests/data/shellmound.yml +++ b/mfsetup/tests/data/shellmound.yml @@ -106,7 +106,7 @@ dis: 12: 'shellmound/rasters/mdwy_surf.tif' # Midway confining unit (top) elevation_units: 'feet' idomain: - filename: 'shellmound/shps/active_area.shp' + filename: 'shellmound/shps/active_area.gpkg|layername=active_area_poly' minimum_layer_thickness: 1 # layers less than this thickness are converted to vertical pass-through cells drop_thin_cells: True diff --git a/mfsetup/tests/data/shellmound/shps/active_area.gpkg b/mfsetup/tests/data/shellmound/shps/active_area.gpkg new file mode 100644 index 0000000000000000000000000000000000000000..26d63ea8dbe688fac2d6ead96d8b4e97e71ab806 GIT binary patch literal 98304 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 IAHWd(A5?}h9{>OV literal 0 HcmV?d00001 diff --git a/mfsetup/tests/test_mf6_shellmound.py b/mfsetup/tests/test_mf6_shellmound.py index 4cfd8889..44992ce9 100644 --- a/mfsetup/tests/test_mf6_shellmound.py +++ b/mfsetup/tests/test_mf6_shellmound.py @@ -327,9 +327,14 @@ def test_dis_setup(shellmound_model_with_grid): model_array = model_array[k] assert np.array_equal(model_array, data) - # test that written idomain array reflects supplied shapefile of active area - active_area = rasterize(m.cfg['dis']['source_data']['idomain']['filename'], - m.modelgrid) + # test that written idomain array reflects supplied shapefile of active area + fpath = m.cfg['dis']['source_data']['idomain']['filename'] + fname = fpath.split("|")[0] + if len(fpath.split('|')) > 1: + layer_name = fpath.split('|')[1] + layer_name = layer_name.replace('layer=', '')\ + .replace('layername=', '') + active_area = rasterize(fname, m.modelgrid, layer=layer_name) isactive = active_area >= 1 written_idomain = load_array(m.cfg['dis']['griddata']['idomain']) assert np.all(written_idomain[:, ~isactive] <= 0) diff --git a/mfsetup/tests/test_pleasant_mfnwt_inset.py b/mfsetup/tests/test_pleasant_mfnwt_inset.py index 44ae4a48..84d61c03 100644 --- a/mfsetup/tests/test_pleasant_mfnwt_inset.py +++ b/mfsetup/tests/test_pleasant_mfnwt_inset.py @@ -133,7 +133,7 @@ def test_model_setup(full_pleasant_nwt): # check that streambed elevation data were incorporated correctly reach_data = m.sfrdata.reach_data - assert reach_data.loc[reach_data['name'] == 'Chaffee Creek', 'strtop'].max() == 296.9 + assert np.allclose(reach_data.loc[reach_data['name'] == 'Chaffee Creek', 'strtop'].max(), 296.9) def test_model_setup_and_run(pleasant_nwt_model_run): From e556f9eda16775d857b086f5dc670fdbd6313a1c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 2 Mar 2026 12:04:14 +0000 Subject: [PATCH 09/14] build(deps): bump actions/upload-artifact from 6 to 7 Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 6 to 7. - [Release notes](https://github.com/actions/upload-artifact/releases) - [Commits](https://github.com/actions/upload-artifact/compare/v6...v7) --- updated-dependencies: - dependency-name: actions/upload-artifact dependency-version: '7' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 24b3d3d4..689edd99 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -161,7 +161,7 @@ jobs: run: twine check --strict dist/* - name: Upload package - uses: actions/upload-artifact@v6 + uses: actions/upload-artifact@v7 with: name: dist path: dist From 26319eb0778bac4e02a18a1de02baddc2cb51356 Mon Sep 17 00:00:00 2001 From: "Leaf, Andrew T" Date: Wed, 4 Mar 2026 10:49:08 -0600 Subject: [PATCH 10/14] fix(pandas 3 support): Refactor Series.dtype == object checks to pd.api.types.is_string_dtype() --- mfsetup/bcs.py | 4 ++-- mfsetup/grid.py | 2 +- mfsetup/obs.py | 2 +- mfsetup/sourcedata.py | 2 +- mfsetup/tdis.py | 8 ++++---- mfsetup/tests/test_obs.py | 4 +++- 6 files changed, 12 insertions(+), 10 deletions(-) diff --git a/mfsetup/bcs.py b/mfsetup/bcs.py index cf03813e..f8258b34 100644 --- a/mfsetup/bcs.py +++ b/mfsetup/bcs.py @@ -92,7 +92,7 @@ def setup_basic_stress_data(model, shapefile=None, csvfile=None, return # create polygons of model grid cells - if bc_cells.dtype == object: + if pd.api.types.is_string_dtype(bc_cells.dtype): cells_with_bc = bc_cells.flat != '' else: cells_with_bc = bc_cells.flat > 0 @@ -108,7 +108,7 @@ def setup_basic_stress_data(model, shapefile=None, csvfile=None, 'i': i.flat, 'j': j.flat}) # add the boundnames - if bc_cells.dtype == object: + if pd.api.types.is_string_dtype(bc_cells.dtype): df_0['boundname'] = bc_cells.flat nan_boundnames = df_0.boundname.isna() | df_0.boundname.isin({'', 'nan'}) df_0.loc[nan_boundnames, 'boundname'] = 'unnamed' diff --git a/mfsetup/grid.py b/mfsetup/grid.py index 34701395..48ae3d70 100644 --- a/mfsetup/grid.py +++ b/mfsetup/grid.py @@ -749,7 +749,7 @@ def rasterize(feature, grid, layer=None, id_column=None, numbers = list(range(1, len(df)+1)) # if IDs are strings, get a number for each one # pd.DataFrame.unique() generally preserves order - elif df[id_column].dtype == object: + elif pd.api.types.is_string_dtype(df[id_column].dtype): unique_values = df[id_column].unique() values = dict(zip(unique_values, range(1, len(unique_values) + 1))) numbers = [values[n] for n in df[id_column]] diff --git a/mfsetup/obs.py b/mfsetup/obs.py index 605298e9..95b37d39 100644 --- a/mfsetup/obs.py +++ b/mfsetup/obs.py @@ -156,7 +156,7 @@ def setup_head_observations(model, filenames=None, df = pd.concat([heads_all_layers, non_heads], axis=0) # dtypes - assert df[obsname_column].dtype == object + assert pd.api.types.is_string_dtype(df[obsname_column].dtype) df['klay'] = df.klay.astype(int) print('Culling observations to cells allowed by iobs_domain...') diff --git a/mfsetup/sourcedata.py b/mfsetup/sourcedata.py index f72c9f88..ad787f0d 100644 --- a/mfsetup/sourcedata.py +++ b/mfsetup/sourcedata.py @@ -1116,7 +1116,7 @@ def get_data(self): df.index = pd.to_datetime(df[self.datetime_column]) # convert IDs to strings if any were read in (resulting in object dtype) - if df[self.id_column].dtype == object: + if pd.api.types.is_string_dtype(df[self.id_column].dtype): df[self.id_column] = df[self.id_column].astype(str) # rename any columns specified in config file to required names diff --git a/mfsetup/tdis.py b/mfsetup/tdis.py index 9fb753ec..7871c928 100644 --- a/mfsetup/tdis.py +++ b/mfsetup/tdis.py @@ -673,10 +673,10 @@ def aggregate_dataframe_to_stress_period(data, id_column, data_column, datetime_ assert datetime_column in data.columns, \ "datetime_column needed for " \ "resampling irregular data to model stress periods" - if data[datetime_column].dtype == object: + if pd.api.types.is_string_dtype(data[datetime_column].dtype): data[datetime_column] = pd.to_datetime(data[datetime_column]) if end_datetime_column in data.columns and \ - data[end_datetime_column].dtype == object: + pd.api.types.is_string_dtype(data[end_datetime_column].dtype): data[end_datetime_column] = pd.to_datetime(data[end_datetime_column]) if start_datetime is None: start_datetime = data[datetime_column].iloc[0] @@ -700,7 +700,7 @@ def aggregate_dataframe_to_stress_period(data, id_column, data_column, datetime_ # and end datetimes that are after the period start # in other words, include all values that overlap in time with the period else: - if data[end_datetime_column].dtype == object: + if pd.api.types.is_string_dtype(data[end_datetime_column].dtype): data[end_datetime_column] = pd.to_datetime(data[end_datetime_column]) data_overlaps_period = (data[datetime_column] < end_datetime) & \ (data[end_datetime_column] > start_datetime) @@ -808,7 +808,7 @@ def aggregate_xarray_to_stress_period(data, datetime_coords_name='time', "datetime_column needed for " \ "resampling irregular data to model stress periods" # not sure if this is needed for xarray - if data[datetime_coords_name].dtype == object: + if pd.api.types.is_string_dtype(data[datetime_coords_name].dtype): data[datetime_coords_name] = pd.to_datetime(data[datetime_coords_name]) # default to aggregating whole dataset # if start_ and end_datetime not provided diff --git a/mfsetup/tests/test_obs.py b/mfsetup/tests/test_obs.py index a0d6c5f6..3d25ce2b 100644 --- a/mfsetup/tests/test_obs.py +++ b/mfsetup/tests/test_obs.py @@ -1,3 +1,5 @@ +import pandas as pd + from mfsetup.obs import make_obsname, read_observation_data @@ -30,5 +32,5 @@ def test_read_observation_data(test_data_path): results = read_observation_data(csvfile, column_info={},#'x_location_col', 'x', #'y_location_col', 'y'}, column_mappings={'obsname': 'comid'}) - assert results['obsname'].dtype == object + assert pd.api.types.is_string_dtype(results['obsname'].dtype) assert isinstance(results['obsname'].values[0], str) From 450b6c36501ab462c9238264e2a96faf0886a2d7 Mon Sep 17 00:00:00 2001 From: "Leaf, Andrew T" Date: Wed, 4 Mar 2026 10:49:08 -0600 Subject: [PATCH 11/14] fix(pandas 3 support): Refactor Series.dtype == object checks to pd.api.types.is_string_dtype() and other misc fixes --- mfsetup/obs.py | 2 +- mfsetup/tests/test_mf6_shellmound.py | 5 +++-- mfsetup/wateruse.py | 9 ++++----- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/mfsetup/obs.py b/mfsetup/obs.py index 95b37d39..b9567fe6 100644 --- a/mfsetup/obs.py +++ b/mfsetup/obs.py @@ -186,7 +186,7 @@ def setup_head_observations(model, filenames=None, elif obs_package == 'obs': # mf6 observation utility obstype = {'BAS': 'HEAD'} renames = {'pckg': 'obstype'} - df.pckg.replace(obstype, inplace=True) + df['pckg'] = df['pckg'].replace(obstype) df.rename(columns=renames, inplace=True) df['id'] = list(zip(df.klay, df.i, df.j)) # drop observations located in inactive cels diff --git a/mfsetup/tests/test_mf6_shellmound.py b/mfsetup/tests/test_mf6_shellmound.py index 44992ce9..85c4d71b 100644 --- a/mfsetup/tests/test_mf6_shellmound.py +++ b/mfsetup/tests/test_mf6_shellmound.py @@ -778,7 +778,8 @@ def test_sfr_setup(model_with_sfr, project_root_path): # verify that reaches were consolidated to one per cell assert len(m.sfrdata.reach_data.node.unique()) == len(m.sfrdata.reach_data) - # check that add_outlets works + # check that add_outlets works; + # these lines were specified as outlets in the configuration file expected_outlets = {'17957815', '17956199'} for outlet_id in expected_outlets: # handle either int-based or str-based line_ids @@ -862,7 +863,7 @@ def test_sfr_inflows_from_csv(model_with_sfr): rno = lookup.get(str(line_id), lookup.get(line_id)) right = sfr_pd.loc[sfr_pd.rno == rno].loc['2007-04-01':, 'inflow'] left = left.loc[:right.index[-1]] - pd.testing.assert_series_equal(left, right, check_names=False, check_freq=False) + pd.testing.assert_series_equal(left, right, check_names=False, check_freq=False, check_index_type=False) @pytest.mark.xfail(reason='flopy remove_package() issue') diff --git a/mfsetup/wateruse.py b/mfsetup/wateruse.py index b036ba74..15a58df1 100644 --- a/mfsetup/wateruse.py +++ b/mfsetup/wateruse.py @@ -338,7 +338,8 @@ def resample_pumping_rates(wu_file, wu_points, model, for site, sitedata in monthly_data.groupby('site_no'): if site not in well_info.index: continue - sitedata.index = sitedata.datetime + sitedata = sitedata.copy() + sitedata.set_index('datetime', inplace=True) assert not sitedata.index.duplicated().any() if dropna: @@ -351,10 +352,8 @@ def resample_pumping_rates(wu_file, wu_points, model, if verbose: years = set(site_period_data.loc[isna, 'year']) years = ', '.join(list(years)) - print('Site {} has {} times with nans (in years {})- filling with {}s'.format(site, - np.sum(isna), - years, - na_fill_value)) + print(f'Site {site} has {np.sum(isna)} times with nans (in years {years})\n- ' + f'filling with {na_fill_value}s') site_period_data['site_no'] = site site_period_data['year'] = site_period_data.index.year site_period_data['month'] = site_period_data.index.month From 9a182cff5f82d2ad20e5f57117cfbffb61b6c41d Mon Sep 17 00:00:00 2001 From: "Leaf, Andrew T" Date: Wed, 4 Mar 2026 15:10:06 -0600 Subject: [PATCH 12/14] ci: test against pip conda and GitHub (develop) Flopys; install also with --no-deps to prevent reinstall of pandas < 3 --- .github/workflows/test.yaml | 24 ++++++++++++++++++++---- ci/test_environment.yaml | 5 +---- 2 files changed, 21 insertions(+), 8 deletions(-) diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index bf3190c5..93ceb85a 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -78,11 +78,27 @@ jobs: cp -r bin/$d/. "$HOME/.local/bin/" echo "$HOME/.local/bin" >> $GITHUB_PATH echo $GITHUB_PATH - # on Windows, test against last Flopy release - - name: Install latest PyPI flopy + - name: Test with latest PyPI flopy shell: bash -l {0} - if: contains(matrix.os, 'windows') - run: pip install flopy --force-reinstall --no-deps + if: ${{ matrix.python-version == 3.12}} + run: | + pip install flopy + pip install git+https://github.com/aleaf/modflow-export@develop + pip install git+https://github.com/usgs/sfrmaker@develop + - name: Test with latest conda flopy + shell: bash -l {0} + if: ${{ matrix.python-version == 3.13}} + run: | + micromamba install flopy + pip install git+https://github.com/aleaf/modflow-export@develop + pip install git+https://github.com/usgs/sfrmaker@develop + - name: Test with latest develop flopy and pandas 3 + shell: bash -l {0} + if: ${{ matrix.python-version == 3.14}} + run: | + pip install git+https://github.com/modflowpy/flopy@develop --no-deps + pip install git+https://github.com/aleaf/modflow-export@develop + pip install git+https://github.com/usgs/sfrmaker@develop - name: Install Modflow-setup and ipykernel shell: bash -l {0} run: | diff --git a/ci/test_environment.yaml b/ci/test_environment.yaml index b1d7ab58..c04fca3f 100644 --- a/ci/test_environment.yaml +++ b/ci/test_environment.yaml @@ -13,7 +13,7 @@ dependencies: - scipy - xarray - netcdf4 -- pandas<3 +- pandas - geopandas - gdal - fiona @@ -36,6 +36,3 @@ dependencies: - pip: - gis-utils - pytest-timeout - - git+https://github.com/modflowpy/flopy@develop - - git+https://github.com/aleaf/modflow-export@develop - - git+https://github.com/usgs/sfrmaker@develop From 0c4c5663dc6a3fa7d01dd6acb62e3bff5666faa6 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 30 Mar 2026 11:54:43 +0000 Subject: [PATCH 13/14] build(deps): bump mamba-org/setup-micromamba from 1 to 3 Bumps [mamba-org/setup-micromamba](https://github.com/mamba-org/setup-micromamba) from 1 to 3. - [Release notes](https://github.com/mamba-org/setup-micromamba/releases) - [Commits](https://github.com/mamba-org/setup-micromamba/compare/v1...v3) --- updated-dependencies: - dependency-name: mamba-org/setup-micromamba dependency-version: '3' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/build_docs.yaml | 2 +- .github/workflows/release.yml | 2 +- .github/workflows/test.yaml | 2 +- .github/workflows/test_releases.yml | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/build_docs.yaml b/.github/workflows/build_docs.yaml index a8482fd5..1a6bac46 100644 --- a/.github/workflows/build_docs.yaml +++ b/.github/workflows/build_docs.yaml @@ -23,7 +23,7 @@ jobs: run: git fetch --prune --unshallow --tags - name: Setup Micromamba - uses: mamba-org/setup-micromamba@v1 + uses: mamba-org/setup-micromamba@v3 with: environment-file: ci/test_environment.yaml cache-environment: false diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 689edd99..99f9697a 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -67,7 +67,7 @@ jobs: echo ${{ steps.get_version.outputs.version }} - name: Setup Micromamba - uses: mamba-org/setup-micromamba@v1 + uses: mamba-org/setup-micromamba@v3 with: environment-file: ci/test_environment.yml cache-environment: true diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 93ceb85a..9d5358db 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -41,7 +41,7 @@ jobs: run: echo "date=$(date +%Y-%m-%d)" >> "${GITHUB_OUTPUT}" - name: Setup Micromamba - uses: mamba-org/setup-micromamba@v1 + uses: mamba-org/setup-micromamba@v3 with: environment-file: ci/test_environment.yaml cache-environment: false diff --git a/.github/workflows/test_releases.yml b/.github/workflows/test_releases.yml index 9ed60d44..30b63873 100644 --- a/.github/workflows/test_releases.yml +++ b/.github/workflows/test_releases.yml @@ -23,7 +23,7 @@ jobs: id: date run: echo "date=$(date +%Y-%m-%d)" >> "${GITHUB_OUTPUT}" - name: Setup Micromamba - uses: mamba-org/setup-micromamba@v1 + uses: mamba-org/setup-micromamba@v3 with: environment-file: ci/test_environment.yaml cache-environment: false From 3e4e79f1825e641fa22f63162155d74a003151f8 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 20 Jul 2026 11:12:14 +0000 Subject: [PATCH 14/14] build(deps): bump actions/setup-python from 6 to 7 Bumps [actions/setup-python](https://github.com/actions/setup-python) from 6 to 7. - [Release notes](https://github.com/actions/setup-python/releases) - [Commits](https://github.com/actions/setup-python/compare/v6...v7) --- updated-dependencies: - dependency-name: actions/setup-python dependency-version: '7' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 99f9697a..bb8775c8 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -143,7 +143,7 @@ jobs: fetch-tags: true - name: Setup Python - uses: actions/setup-python@v6 + uses: actions/setup-python@v7 with: python-version: '3.x' cache: 'pip'