diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index eb93077..693fe6c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -6,8 +6,18 @@ on: push: pull_request: branches: [main, develop] + # the release workflow calls this to test a release branch, which a bot + # pushes and so does not raise a push or pull_request event of its own + workflow_call: + inputs: + ref: + description: 'Ref to test. Defaults to the ref that triggered the workflow.' + required: false + type: string concurrency: - group: ${{ github.workflow }}-${{ github.ref }} + # distinct from the calling workflow's group, so a called run cannot cancel + # the release that started it + group: ${{ github.workflow }}-${{ inputs.ref || github.ref }}-tests cancel-in-progress: true jobs: @@ -23,6 +33,8 @@ jobs: - name: Checkout repo uses: actions/checkout@v7 + with: + ref: ${{ inputs.ref }} - name: Setup uv uses: astral-sh/setup-uv@v7 @@ -44,6 +56,8 @@ jobs: steps: - name: Checkout repo uses: actions/checkout@v7 + with: + ref: ${{ inputs.ref }} - name: Setup uv uses: astral-sh/setup-uv@v7 @@ -67,13 +81,15 @@ jobs: fail-fast: false matrix: os: [ ubuntu-latest, macos-latest, windows-latest ] - python-version: [ "3.11", "3.12", "3.13", "3.14" ] + python-version: [ "3.12", "3.13", "3.14" ] defaults: run: shell: bash steps: - name: Checkout repo uses: actions/checkout@v7 + with: + ref: ${{ inputs.ref }} - name: Setup uv uses: astral-sh/setup-uv@v7 @@ -118,13 +134,15 @@ jobs: fail-fast: false matrix: os: [ ubuntu-latest, windows-latest ] - python-version: [ "3.11", "3.12", "3.13", "3.14" ] + python-version: [ "3.12", "3.13", "3.14" ] defaults: run: shell: bash steps: - name: Checkout repo uses: actions/checkout@v7 + with: + ref: ${{ inputs.ref }} - name: Setup uv uses: astral-sh/setup-uv@v7 @@ -171,13 +189,15 @@ jobs: fail-fast: false matrix: os: [ ubuntu-latest, macos-latest, windows-latest ] - python-version: [ "3.11", "3.12", "3.13", "3.14" ] + python-version: [ "3.12", "3.13", "3.14" ] defaults: run: shell: bash steps: - name: Checkout repo uses: actions/checkout@v7 + with: + ref: ${{ inputs.ref }} - name: Setup uv uses: astral-sh/setup-uv@v7 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 9701e71..ba84b6d 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -50,10 +50,11 @@ jobs: runs-on: ubuntu-latest permissions: contents: write - pull-requests: write defaults: run: shell: bash + outputs: + version: ${{ steps.version.outputs.version }} steps: - name: Check release branch @@ -110,34 +111,6 @@ jobs: uvx ruff format --check . uvx codespell - - name: Install modflow executables - if: ${{ github.event_name != 'workflow_dispatch' || inputs.run_tests }} - id: install_modflow - continue-on-error: true - uses: modflowpy/install-modflow-action@v1 - with: - path: ${{ github.workspace }}/autotest - repo: modflow6-nightly-build - - # the download reaches out to github and occasionally has its - # connection reset, failing the job before any test runs - - name: Install modflow executables (retry) - if: ${{ (github.event_name != 'workflow_dispatch' || inputs.run_tests) && steps.install_modflow.outcome == 'failure' }} - uses: modflowpy/install-modflow-action@v1 - with: - path: ${{ github.workspace }}/autotest - repo: modflow6-nightly-build - - # temporary. devtools 2.x will autosync, but 1.x needs opt-in. - - name: Sync devtools - if: ${{ github.event_name != 'workflow_dispatch' || inputs.run_tests }} - run: uv run mf sync - - - name: Run tests - if: ${{ github.event_name != 'workflow_dispatch' || inputs.run_tests }} - working-directory: ./autotest - run: uv run pytest -v -n auto -m "not mf6" - - name: Generate changelog uses: orhun/git-cliff-action@v4 with: @@ -165,14 +138,11 @@ jobs: sed -i '1i # Changelog' $clog - name: Push release branch - env: - GITHUB_TOKEN: ${{ github.token }} run: | ver="${{ steps.version.outputs.version }}" - # the release notes have already been prepended to HISTORY.md, - # keep a copy for the PR body then drop the standalone changelog - notes=$(grep -v "### Version $ver" CHANGELOG.md) + # the notes are already prepended to HISTORY.md; the pr job reads + # them back from there rather than from this standalone file rm -f CHANGELOG.md git config core.sharedRepository true @@ -182,6 +152,47 @@ jobs: git commit -m "ci(release): set version to $ver, update changelog" git push origin "v$ver" + test: + name: Test release branch + # a bot pushes the release branch, so no push or pull_request event fires + # for it and the pull request would otherwise carry no checks at all + needs: prep + if: ${{ github.event_name != 'workflow_dispatch' || inputs.run_tests }} + permissions: + contents: read + uses: ./.github/workflows/ci.yml + with: + ref: v${{ needs.prep.outputs.version }} + + pr: + name: Draft release pull request + needs: [prep, test] + # the tests are skipped when a release is dispatched with run_tests off + if: ${{ always() && needs.prep.result == 'success' && (needs.test.result == 'success' || needs.test.result == 'skipped') }} + runs-on: ubuntu-latest + permissions: + contents: read + pull-requests: write + defaults: + run: + shell: bash + steps: + + - name: Checkout release branch + uses: actions/checkout@v7 + with: + ref: v${{ needs.prep.outputs.version }} + + - name: Draft pull request + env: + GITHUB_TOKEN: ${{ github.token }} + run: | + ver="${{ needs.prep.outputs.version }}" + notes=$(awk -v hdr="### Version $ver" ' + index($0, hdr) == 1 { flag = 1; next } + /^### Version / { flag = 0 } + flag' HISTORY.md) + body=' # Release '$ver' diff --git a/CITATION.cff b/CITATION.cff index 147e7bf..e102c89 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 and the software itself. type: software title: MODFLOW API -version: 1.0.1 -date-released: '2026-09-06' +version: 1.0.2 +date-released: '2026-11-02' abstract: An extension to xmipy for the MODFLOW API. repository-artifact: https://pypi.org/project/modflowapi repository-code: https://github.com/MODFLOW-ORG/modflowapi diff --git a/HISTORY.md b/HISTORY.md index 10b71ff..6201050 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -1,4 +1,10 @@ # Changelog +### Version 1.0.2 + +#### Bug fixes + +* [fix(sfr)](https://github.com/MODFLOW-ORG/modflowapi/commit/3b93eb7346936cc4d5d1cee1d8791f7b5ba106d3): Read bedk, manning, and upstream_frac from the input context (#128). Committed by Joseph Hughes on 2026-11-02. + ### Version 1.0.1 #### Bug fixes diff --git a/README.md b/README.md index 8e05d20..8d6ce26 100644 --- a/README.md +++ b/README.md @@ -33,7 +33,7 @@ and additional functionality specific to the MODFLOW API. Currently it is a join ## Installation -`modflowapi` requires Python 3.11+, with: +`modflowapi` requires Python 3.12+, with: ```shell numpy diff --git a/autotest/test_interface.py b/autotest/test_interface.py index 79a0fcc..0c6a932 100644 --- a/autotest/test_interface.py +++ b/autotest/test_interface.py @@ -1,6 +1,7 @@ import shutil from pathlib import Path from platform import system +from textwrap import dedent import numpy as np import pytest @@ -341,6 +342,157 @@ def callback(sim, step): raise Exception(e) +def test_sfr_input_context_names(function_tmpdir): + def callback(sim, step): + if step != Callbacks.timestep_start or sim.kstp != 0: + return + model = sim.get_model() + sfr = model.get_package("sfr_0") + if sim.kper == 1: + # the write persists into the next time step + np.testing.assert_allclose(sfr.packagedata.values["hk"], written["hk"], err_msg="hk write did not persist") + written["persisted"] = True + return + if f"__INPUT__/{model.name}/SFR_0/BEDK" in sim.mf6.get_input_var_names(): + path = f"__INPUT__/{model.name}/SFR_0" + names = ("BEDK", "MANNING", "UPSTREAM_FRAC") + else: + path = f"{model.name}/SFR_0" + names = ("HK", "ROUGH", "USTRF") + addrs = [f"{path}/{name}" for name in names] + for addr in addrs: + assert addr in sfr._package_var_addrs, f"{addr} not used" + + pd = sfr.packagedata.values + for column, values in expected.items(): + np.testing.assert_allclose(pd[column], values, err_msg=f"{column} does not match the input") + + pd["hk"] *= 10.0 + sfr.packagedata = pd + np.testing.assert_allclose(sim.mf6.get_value(addrs[0]), pd["hk"], err_msg="hk is not being properly set") + written["hk"] = pd["hk"].copy() + + written = {} + + # three reaches in a row of three cells; reach 1 flows to reach 2 and + # diverts to reach 3 + expected = {"hk": [0.1, 0.2, 0.3], "rough": [0.03, 0.035, 0.04], "ustrf": [1.0, 1.0, 0.0]} + packagedata = "\n".join( + f" {i + 1} 1 1 {i + 1} 100.0 {width} 0.001 {rtp} 1.0 {hk} {rough} {ncon} {ustrf} {ndiv}" + for i, (width, rtp, hk, rough, ncon, ustrf, ndiv) in enumerate( + zip( + [10.0, 10.0, 5.0], + [9.0, 8.9, 8.9], + expected["hk"], + expected["rough"], + [2, 1, 1], + expected["ustrf"], + [1, 0, 0], + ) + ) + ) + files = { + "mfsim.nam": """ + BEGIN timing + TDIS6 sim.tdis + END timing + BEGIN models + gwf6 sfr.nam sfr + END models + BEGIN exchanges + END exchanges + BEGIN solutiongroup 1 + ims6 sim.ims sfr + END solutiongroup + """, + "sim.tdis": """ + BEGIN dimensions + NPER 2 + END dimensions + BEGIN perioddata + 1.0 1 1.0 + 1.0 1 1.0 + END perioddata + """, + "sim.ims": "", + "sfr.nam": """ + BEGIN packages + DIS6 sfr.dis dis + NPF6 sfr.npf npf + IC6 sfr.ic ic + CHD6 sfr.chd chd_0 + SFR6 sfr.sfr sfr_0 + END packages + """, + "sfr.dis": """ + BEGIN dimensions + NLAY 1 + NROW 1 + NCOL 3 + END dimensions + BEGIN griddata + delr + CONSTANT 100.0 + delc + CONSTANT 100.0 + top + CONSTANT 10.0 + botm + CONSTANT 0.0 + END griddata + """, + "sfr.npf": """ + BEGIN griddata + icelltype + CONSTANT 0 + k + CONSTANT 10.0 + END griddata + """, + "sfr.ic": """ + BEGIN griddata + strt + CONSTANT 8.0 + END griddata + """, + "sfr.chd": """ + BEGIN dimensions + MAXBOUND 1 + END dimensions + BEGIN period 1 + 1 1 1 8.0 + END period + """, + "sfr.sfr": """ + BEGIN dimensions + NREACHES 3 + END dimensions + BEGIN packagedata + {packagedata} + END packagedata + BEGIN connectiondata + 1 -2 -3 + 2 1 + 3 1 + END connectiondata + BEGIN diversions + 1 1 3 UPTO + END diversions + BEGIN period 1 + 1 INFLOW 10.0 + 1 DIVERSION 1 2.0 + END period + """, + } + test_pth = function_tmpdir / "sfr_model" + test_pth.mkdir() + for name, text in files.items(): + (test_pth / name).write_text(dedent(text).format(packagedata=packagedata)) + + run_simulation(so, test_pth, callback) + assert written.get("persisted"), "hk was not checked in the second stress period" + + def test_rhs_hcof_advanced(function_tmpdir): def callback(sim, step): model = sim.test_model diff --git a/guide-to-publish.md b/guide-to-publish.md index 70a9a34..73d16c6 100644 --- a/guide-to-publish.md +++ b/guide-to-publish.md @@ -52,6 +52,23 @@ Merging the release pull request into `main` triggers jobs that: Merge the reset pull request to finish the release. +## 4. conda-forge + +A few hours after the upload to PyPI, a bot opens a version pull request on the +[feedstock](https://github.com/conda-forge/modflowapi-feedstock). To start it immediately instead, +open an issue there titled `@conda-forge-admin, please update version`. + +> [!IMPORTANT] +> The bot updates the version number and the checksum, and nothing else. **Check the recipe's +> `host` and `run` requirements against the dependencies the release actually declares**, which are +> the `Requires-Dist` lines of the sdist on PyPI. Both releases so far needed this by hand: 1.0.0 +> because the build backend had moved from setuptools to hatchling, and 1.0.1 because the pandas +> lower bound had been raised. A maintainer can push the correction to the bot's branch. + +Merging the feedstock pull request builds and uploads the package. It does not appear to a solver +until the channel index is regenerated, which takes up to about an hour; the package is visible on +anaconda.org before then. + ## Changelog conventions Release notes are generated from commit messages, so commits merged to `develop` must follow the diff --git a/modflowapi/extensions/data.py b/modflowapi/extensions/data.py index c104da2..80aa0bb 100644 --- a/modflowapi/extensions/data.py +++ b/modflowapi/extensions/data.py @@ -110,7 +110,9 @@ def _set_data(self): else: continue - reduced = var_addr.split("/")[-1].lower() + # keep the column name of a variable read from the input context + column_names = getattr(self.parent, "_column_names", {}) + reduced = column_names.get(var_addr, var_addr.split("/")[-1].lower()) if reduced in ("maxbound", "nbound"): setattr(self, f"_{reduced}", values) if not self._spd and reduced == "maxbound": diff --git a/modflowapi/extensions/datamodel.py b/modflowapi/extensions/datamodel.py index 669a0d5..1b1b1e7 100644 --- a/modflowapi/extensions/datamodel.py +++ b/modflowapi/extensions/datamodel.py @@ -275,6 +275,16 @@ "maw": {"packagedata": ["nmawwells", ("ifno:range:nmawwells", "radius", "bot", "strt", "ngwfnodes")]}, } +# advanced package variables read from the input context when libmf6 has them, +# keyed by column name, with the input context name +adv_input_vars = { + "sfr": { + "hk": "BEDK", + "rough": "MANNING", + "ustrf": "UPSTREAM_FRAC", + }, +} + def get_package_type(pkg_type): from .advpaks import LakPackage, MawPackage, SfrPakage, UzfPackage diff --git a/modflowapi/extensions/pakbase.py b/modflowapi/extensions/pakbase.py index 2a48c9b..29e2464 100644 --- a/modflowapi/extensions/pakbase.py +++ b/modflowapi/extensions/pakbase.py @@ -1,7 +1,7 @@ import numpy as np from .data import AdvancedInput, ArrayInput, ListInput, ScalarInput -from .datamodel import adv_pkgvars, pkgvars +from .datamodel import adv_input_vars, adv_pkgvars, pkgvars class PackageBase: @@ -468,6 +468,10 @@ def __init__(self, model, pkg_type, pkg_name, sim_package=False): self._sp_var_addrs = [] self._package_vars = None self._sp_vars = None + self._input_vars = adv_input_vars.get(pkg_type, {}) + self._column_names = {} + if self._input_vars: + self._input_var_names = set(self.model.mf6.get_input_var_names()) if pkg_type in adv_pkgvars: self._adv_var_dict = adv_pkgvars[pkg_type] @@ -550,6 +554,12 @@ def _get_advanced_variable_addr(self, var_str): ------- var_addr : str """ + var = var_str.lower() + if var in self._input_vars: + s = f"__INPUT__/{self.model.name}/{self.pkg_name}/{self._input_vars[var]}" + if s in self._input_var_names: + self._column_names[s] = var + return s s = f"{self.model.name}/{self.pkg_name}/{var_str.upper()}" return s diff --git a/modflowapi/version.py b/modflowapi/version.py index f229cbb..dd36f06 100644 --- a/modflowapi/version.py +++ b/modflowapi/version.py @@ -1,3 +1,3 @@ # modflowapi version file automatically created using...update_version.py -# created on...September 06, 2026 12:36:39 -__version__ = "1.0.1" +# created on...November 02, 2026 16:46:29 +__version__ = "1.0.2" diff --git a/pyproject.toml b/pyproject.toml index 054124b..c7f0458 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -25,16 +25,15 @@ classifiers = [ "Intended Audience :: Science/Research", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3 :: Only", - "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.14", "Topic :: Scientific/Engineering :: Hydrology", ] -requires-python = ">=3.11" +requires-python = ">=3.12" dependencies = [ - "numpy>=1.23", - "pandas>=2.2.2", + "numpy>=2.2.0", + "pandas>=2.3.0", "xmipy>=1.5.0", ] diff --git a/version.txt b/version.txt index 7f20734..e6d5cb8 100644 --- a/version.txt +++ b/version.txt @@ -1 +1 @@ -1.0.1 \ No newline at end of file +1.0.2 \ No newline at end of file