From 719cd1261510c601ec7fbb22c95770b3f16f455a Mon Sep 17 00:00:00 2001 From: "Leaf, Andrew T" Date: Sat, 9 Nov 2024 06:49:47 -0600 Subject: [PATCH 1/8] ci(release.yml): setup automated releases from PRs --- .github/workflows/release.yml | 120 ++++++++++++++++++++++------------ 1 file changed, 80 insertions(+), 40 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 20146799..80cd3c22 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,29 +1,47 @@ name: Publish release - on: push: - # Sequence of patterns matched against refs/tags - tags: - - 'v*' # Push events to matching v*, i.e. v1.0, v20.15.10 + branches: + - master + # branchname trigger if we had the ability to auto-prep release PRs + # (turned off for doi-usgs org) + #- v[0-9]+.[0-9]+.[0-9]+* + release: + types: + - published jobs: + release: name: Create Release + # runs only when changes are merged to master + if: ${{ github.event_name == 'push' && github.ref_name == 'master' }} runs-on: ubuntu-latest + permissions: + contents: write + pull-requests: write steps: - - name: Checkout source - uses: actions/checkout@v3 + - name: Checkout master branch + uses: actions/checkout@v4 + with: + ref: master + - name: Bump version and push tag + id: tag_version + uses: mathieudutour/github-tag-action@v6.2 + with: + github_token: ${{ secrets.GITHUB_TOKEN }} - name: Create Release id: create_release uses: actions/create-release@v1 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # This token is provided by Actions, you do not need to create your own token with: - tag_name: ${{ github.ref }} - release_name: Release ${{ github.ref }} + tag_name: ${{ steps.tag_version.outputs.new_tag }} + release_name: Modflow-setup ${{ steps.tag_version.outputs.new_tag }} body: | Changes in this Release: - draft: false + ${{ steps.tag_version.outputs.changelog }} + draft: true prerelease: false docs: @@ -33,9 +51,11 @@ jobs: steps: - name: Checkout source - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: persist-credentials: false + ref: master + fetch-tags: true - name: Get latest release version number id: get_version @@ -49,16 +69,9 @@ jobs: - name: Setup Micromamba uses: mamba-org/setup-micromamba@v1 with: - environment-file: ci/test_environment.yaml - cache-environment: false - cache-downloads: false - # persist on the same day. - # cache-environment-key: environment-${{ steps.date.outputs.date }} - # cache-downloads-key: downloads-${{ steps.date.outputs.date }} - create-args: >- - python=${{ matrix.python-version }} - init-shell: >- - bash + environment-file: ci/test_environment.yml + cache-environment: true + cache-downloads: true - name: Conda info shell: bash -l {0} @@ -91,7 +104,7 @@ jobs: - name: Run tests shell: bash -l {0} run: | - pytest mfsetup/test/test_notebooks.py + pytest mfsetup/tests/test_notebooks.py - name: Build docs shell: bash -l {0} @@ -108,23 +121,50 @@ jobs: CLEAN: false TARGET_FOLDER: ${{ steps.get_version.outputs.version }} - deploy: - needs: release - runs-on: ubuntu-latest + publish: + name: Publish package + # runs only after release is published (manually promoted from draft) + # (allows editing of release notes, etc.) + if: ${{ github.event_name == 'release' }} + runs-on: ubuntu-latest #ubuntu-22.04 + permissions: + contents: write + pull-requests: write + id-token: write # mandatory for trusted publishing + environment: # requires a 'release' environment in repo settings + name: release + url: https://pypi.org/p/modflow-setup steps: - - uses: actions/checkout@v3 - - name: Set up Python - uses: actions/setup-python@v1 - with: - python-version: '3.x' - - name: Install dependencies - run: | - python -m pip install --upgrade pip - pip install setuptools wheel twine - - name: Build and publish - env: - TWINE_USERNAME: __token__ - TWINE_PASSWORD: ${{ secrets.ALL_PROJECTS }} - run: | - python setup.py sdist bdist_wheel - twine upload dist/* + + - name: Checkout master branch + uses: actions/checkout@v4 + with: + ref: master + fetch-tags: true + + - name: Setup Python + uses: actions/setup-python@v5 + with: + python-version: '3.x' + cache: 'pip' + cache-dependency-path: pyproject.toml + + - name: Install Python dependencies + run: | + pip install --upgrade pip + pip install build twine + + - name: Build package + run: python -m build + + - name: Check package + run: twine check --strict dist/* + + - name: Upload package + uses: actions/upload-artifact@v4 + with: + name: dist + path: dist + + - name: Publish to PyPI + uses: pypa/gh-action-pypi-publish@release/v1 From 580cb89bca994537c50492753ed951bb0191f76a Mon Sep 17 00:00:00 2001 From: "Leaf, Andrew T" Date: Sat, 9 Nov 2024 06:50:21 -0600 Subject: [PATCH 2/8] ci: add GitHub dependabot for ci dependency management --- .github/dependabot.yml | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 .github/dependabot.yml diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 00000000..ac27a848 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,8 @@ +version: 2 +updates: + # Maintain dependencies for GitHub Actions + - package-ecosystem: "github-actions" + directory: "/" + schedule: + # Check for updates to GitHub Actions every week + interval: "weekly" From 4798975414b07be423f7ffe159cde4eea73beacd Mon Sep 17 00:00:00 2001 From: "Leaf, Andrew T" Date: Sat, 9 Nov 2024 07:04:44 -0600 Subject: [PATCH 3/8] refactor(discretization.make_lgr_domain): add error trap for LGR grid corners outside of the parent model grid --- mfsetup/discretization.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/mfsetup/discretization.py b/mfsetup/discretization.py index 666c5a55..cd2f6e79 100644 --- a/mfsetup/discretization.py +++ b/mfsetup/discretization.py @@ -428,6 +428,16 @@ def make_lgr_idomain(parent_modelgrid, inset_modelgrid, y1 = inset_modelgrid.ycellcenters[-1, -1] pi1, pj1 = parent_modelgrid.intersect(x1, y1, forgive=True) idomain = np.ones(parent_modelgrid.shape, dtype=int) + if any(np.isnan([pi0, pj0])): + raise ValueError(f"LGR model upper left corner {pi0}, {pj0} " + "is outside of the parent model domain! " + "Check the grid offset and dimensions." + ) + if any(np.isnan([pi1, pj1])): + raise ValueError(f"LGR model lower right corner {pi0}, {pj0} " + "is outside of the parent model domain! " + "Check the grid offset and dimensions." + ) idomain[0:(np.array(ncppl) > 0).sum(), pi0:pi1+1, pj0:pj1+1] = 0 return idomain From b4b0b6eeb78670a5d6e005d99817985099da8cb2 Mon Sep 17 00:00:00 2001 From: "Leaf, Andrew T" Date: Tue, 12 Nov 2024 20:00:58 -0600 Subject: [PATCH 4/8] fix(initial conditions/strt array setup): add option to explicitly specify strt: from_model_top in source_data: block; needed in the case where there is a parent model and default_source_data is set to True. In this case, the default behavior was to set strt from the strt array in the parent model. fix(SFR Package setup:): when setting up inflows; try checking input sites against the routing dictionary as-is, or with the input sites as strings, to support versions of SFRmaker <= 0.11.3 (integer line identifiers) and > 0.11.3 (string line identifiers). --- examples/pleasant_lgr_parent.yml | 9 +++---- mfsetup/ic.py | 44 +++++++++++++++---------------- mfsetup/mfmodel.py | 17 ++++++++++-- mfsetup/tests/data/shellmound.yml | 5 ++-- mfsetup/tests/test_lgr.py | 36 +++++++++++++++++++++++++ 5 files changed, 79 insertions(+), 32 deletions(-) diff --git a/examples/pleasant_lgr_parent.yml b/examples/pleasant_lgr_parent.yml index 3b8883e4..776feb7c 100644 --- a/examples/pleasant_lgr_parent.yml +++ b/examples/pleasant_lgr_parent.yml @@ -135,12 +135,11 @@ tdis: # Initial Conditions Package ic: - # starting heads sampled from parent model + # override default_source_data: True setting + # (to set starting heads from the parent model starting heads) + # set starting heads to the model top source_data: - strt: - from_parent: - binaryfile: 'data/pleasant/pleasant.hds' - stress_period: 0 + strt: from_model_top # Node Property Flow Package npf: diff --git a/mfsetup/ic.py b/mfsetup/ic.py index 2bec5c61..69a27d82 100644 --- a/mfsetup/ic.py +++ b/mfsetup/ic.py @@ -39,16 +39,19 @@ def setup_strt(model, package, strt=None, source_data_config=None, # check for parent model and a binary file binary_file = False - if strt_config is not None and 'from_parent' in strt_config: - if model._is_lgr: - source_model = source_model.parent - if source_model is None: - raise ValueError(("'from_parent' in configuration by no parent model." - f"{package} package, {model.name} model.\n" - f"source_data config:\n{source_data_config}")) - from_parent_cfg = source_data_config['strt'].get('from_parent', {}) - binary_file = from_parent_cfg.get('binaryfile', False) - kwargs.update(from_parent_cfg) + if strt_config is not None: + if 'from_parent' in strt_config: + if model._is_lgr: + source_model = source_model.parent + if source_model is None: + raise ValueError(("'from_parent' in configuration by no parent model." + f"{package} package, {model.name} model.\n" + f"source_data config:\n{source_data_config}")) + from_parent_cfg = source_data_config['strt'].get('from_parent', {}) + binary_file = from_parent_cfg.get('binaryfile', False) + kwargs.update(from_parent_cfg) + elif 'from_model_top' in strt_config: + default_parent_source_data = False sd = None # source data instance # data specified directly @@ -79,13 +82,18 @@ def setup_strt(model, package, strt=None, source_data_config=None, dest_model=model, source_modelgrid=source_model.modelgrid, source_array=source_array, - from_source_model_layers=None, + from_source_model_layers=model.parent_layers, length_units=model.length_units, time_units=model.time_units, **kwargs) + # default to setting strt from model top + elif strt_config is None or 'from_model_top' in strt_config: + sd = MFArrayData(variable=var, + values=[model.dis.top.array] * model.nlay, + datatype=datatype, + dest_model=model, **kwargs) # data from files - elif source_data_config: - if source_data_config.get(var) is not None: + elif isinstance(source_data_config, dict) and source_data_config.get(var) is not None: #ext = get_source_data_file_ext(source_data_config, package, var) source_data_files = source_data_config.get(var) kwargs = get_input_arguments(kwargs, ArraySourceData) @@ -94,16 +102,6 @@ def setup_strt(model, package, strt=None, source_data_config=None, variable=var, dest_model=model, **kwargs) - else: - raise ValueError(f"Invalid configuration input: {package}: source_data:\n" - f"Need a {var}: sub-block") - - # default to setting strt from model top - else: - sd = MFArrayData(variable=var, - values=[model.dis.top.array] * model.nlay, - datatype=datatype, - dest_model=model, **kwargs) if sd is None: raise ValueError((f'Unrecognized input for variable {var}, ' diff --git a/mfsetup/mfmodel.py b/mfsetup/mfmodel.py index 2762a2b2..6e86daf5 100644 --- a/mfsetup/mfmodel.py +++ b/mfsetup/mfmodel.py @@ -292,12 +292,16 @@ def parent_layers(self): if self._parent_layers is None: parent_layers = None botm_source_data = self.cfg['dis'].get('source_data', {}).get('botm', {}) + nlay = self.modelgrid.nlay + if nlay is None: + nlay = self.cfg['dis']['dimensions']['nlay'] if self.cfg['parent'].get('inset_layer_mapping') is not None: parent_layers = self.cfg['parent'].get('inset_layer_mapping') elif isinstance(botm_source_data, dict) and 'from_parent' in botm_source_data: parent_layers = botm_source_data.get('from_parent') - elif self.parent is not None: - parent_layers = dict(zip(range(self.parent.modelgrid.nlay), range(self.parent.modelgrid.nlay))) + elif self.parent is not None and (self.parent.modelgrid.nlay == nlay): + parent_layers = dict(zip(range(self.parent.modelgrid.nlay), + range(nlay))) else: #parent_layers = dict(zip(range(self.parent.modelgrid.nlay), range(self.parent.modelgrid.nlay))) parent_layers = None @@ -1641,6 +1645,15 @@ def setup_sfr(self, **kwargs): dest_model=self) inflows_by_stress_period = sd.get_data() + missing_sites = set(inflows_by_stress_period[inflows_input['id_column']]). \ + difference(routing.keys()) + if any(missing_sites): + # cast IDs to strings for compatibility with SFRmaker > 0.11.3 + # for now, assume IDs are numeric; future updates to SFRmaker + # may eventually allow for alpha numeric IDs + inflows_by_stress_period[inflows_input['id_column']] =\ + inflows_by_stress_period[inflows_input['id_column']].astype(int).astype(str) + # check if all inflow sites are included in sfr network missing_sites = set(inflows_by_stress_period[inflows_input['id_column']]). \ difference(routing.keys()) diff --git a/mfsetup/tests/data/shellmound.yml b/mfsetup/tests/data/shellmound.yml index 5ca58362..1d454f1e 100644 --- a/mfsetup/tests/data/shellmound.yml +++ b/mfsetup/tests/data/shellmound.yml @@ -141,8 +141,9 @@ tdis: steady: False # Initial Conditions Package -# if no starting head values are specified, they are set -# to the model top by default +# if no starting head values are specified, +# and there is no parent model, +# starting heads are set to the model top by default ic: # optional format for writing external files # e.g. (strt_001.dat) diff --git a/mfsetup/tests/test_lgr.py b/mfsetup/tests/test_lgr.py index 3d260c0f..43dfe438 100644 --- a/mfsetup/tests/test_lgr.py +++ b/mfsetup/tests/test_lgr.py @@ -573,6 +573,42 @@ def test_lgr_model_setup(pleasant_lgr_setup_from_yaml, tmpdir): # todo: test_lgr_model_setup could use some more tests; although many potential issues will be tested by test_lgr_model_run +@pytest.mark.parametrize('ic_cfg,default_source_data,expected', ( + (None, False, 'top'), + ({'source_data': { + 'strt': { + 'from_parent': { + 'binaryfile': '/Users/aleaf/Documents/GitHub/modflow-setup/examples/data/pleasant/pleasant.hds', + 'stress_period': 0}}}}, True, 'parent heads'), + ({'source_data': { + 'strt': 'from_model_top'}}, True, 'top'), + ({'mfsetup_options': {}, + 'griddata': {}, + 'source_data': {}, + 'source_data_config': {}, + 'strt_filename_fmt': {}, + 'filename_fmt': {}}, True, 'parent starting heads') +)) +def test_pleasant_vertical_lgr_ic_strt(pleasant_vertical_lgr_cfg, ic_cfg, default_source_data, expected): + cfg = copy.deepcopy(pleasant_vertical_lgr_cfg) + cfg['ic'] = ic_cfg + if not default_source_data: + cfg['parent']['default_source_data'] = False + m = MF6model(cfg=cfg) + m.setup_dis() + m.setup_ic() + assert m.ic.strt.array.shape == m.modelgrid.shape + if expected == 'top': + top3d = np.array([m.dis.top.array] * m.modelgrid.nlay) + assert np.allclose(m.ic.strt.array, top3d) + elif expected == 'parent starting heads': + np.allclose(m.ic.strt.array.mean(), + m.parent.bas6.strt.array.mean(), rtol=0.01) + else: + parent_headsfile = ic_cfg['source_data']['strt']['from_parent']['binaryfile'] + hds = flopy.utils.binaryfile.HeadFile(parent_headsfile).get_data() + assert np.allclose(m.ic.strt.array.mean(), hds.mean(), rtol=0.01) + #def test_stand_alone_parent(pleasant_lgr_stand_alone_parent): # # todo: move test_stand_alone_parent test to test_lgr_model_run From a42bd252a802e39c9e997dcc10aeeda6f9b0e900 Mon Sep 17 00:00:00 2001 From: "Leaf, Andrew T" Date: Wed, 13 Nov 2024 11:59:53 -0600 Subject: [PATCH 5/8] fix(partial vertical LGR): remove hard-coding in sourcedata.py::setup_array that was causing vertical gaps between LGR inset and parent models with horizontal refinments other than 5. --- mfsetup/sourcedata.py | 13 +++++--- mfsetup/tests/test_lgr.py | 64 +++++++++++++++++++++++++-------------- 2 files changed, 50 insertions(+), 27 deletions(-) diff --git a/mfsetup/sourcedata.py b/mfsetup/sourcedata.py index 6727bde2..25849b36 100644 --- a/mfsetup/sourcedata.py +++ b/mfsetup/sourcedata.py @@ -1551,18 +1551,21 @@ def setup_array(model, package, var, data=None, "can't use this to make the top of the parent model." ) # average each nccp x nccp block of inset model cells - topp = np.reshape(lgr_inset_botm, + # nccp = number of child cells per parent cell + new_parent_top = np.reshape(lgr_inset_botm, (nrowp, ncpp, ncolp, ncpp)).mean(axis=(1, 3)) n_parent_lgr_layers = np.sum(np.array(lgr.ncppl) > 0) # remap averages back to the inset model shape - data[nlay-1] = np.repeat(np.repeat(topp, 5, axis=0), 5, axis=1) - # set the parent model botm in this area to be the same + # and assign to inset model bottom + data[nlay-1] = np.repeat(np.repeat(new_parent_top, ncpp, axis=0), + ncpp, axis=1) + # set the parent model top in this area to be the same lgr_area = model.parent.lgr[model.name].idomain == 0 - model.parent.dis.top[lgr_area[0]] = topp.ravel() + model.parent.dis.top[lgr_area[0]] = new_parent_top.ravel() # set parent model layers in LGR area to zero-thickness new_parent_botm = model.parent.dis.botm.array.copy() for k in range(n_parent_lgr_layers): - new_parent_botm[k][lgr_area[0]] = topp.ravel() + new_parent_botm[k][lgr_area[0]] = new_parent_top.ravel() model.parent.dis.botm = new_parent_botm model.parent._update_top_botm_external_files() diff --git a/mfsetup/tests/test_lgr.py b/mfsetup/tests/test_lgr.py index 43dfe438..e7683803 100644 --- a/mfsetup/tests/test_lgr.py +++ b/mfsetup/tests/test_lgr.py @@ -528,7 +528,6 @@ def test_lgr_bottom_elevations(pleasant_vertical_lgr_setup_from_yaml, mf6_exe): plt.close() - def test_lgr_model_setup(pleasant_lgr_setup_from_yaml, tmpdir): m = pleasant_lgr_setup_from_yaml assert isinstance(m.inset, dict) @@ -555,23 +554,9 @@ def test_lgr_model_setup(pleasant_lgr_setup_from_yaml, tmpdir): continue assert m.name in f or 'plsnt_lgr_inset' in f - binaryfile = m.cfg['ic']['source_data']['strt']['from_parent']['binaryfile'] - kper = m.cfg['ic']['source_data']['strt']['from_parent']['stress_period'] - phds = bf.HeadFile(binaryfile) - kstpkper = [kstpkper for kstpkper in phds.get_kstpkper() if kstpkper[1] == kper][-1] - - from mfsetup.interpolate import regrid3d - resampled_parent_heads = regrid3d(phds.get_data(kstpkper), - m.parent.modelgrid, - m.inset['plsnt_lgr_inset'].modelgrid, - mask1=None, mask2=None, method='linear') - diff = m.inset['plsnt_lgr_inset'].ic.strt.array - resampled_parent_heads + top3d = np.array([m.dis.top.array] * m.modelgrid.nlay) + assert np.allclose(m.ic.strt.array, top3d) - # a small percentage of cells are appreciably different - # unclear why - assert np.sum(np.abs(diff) > 0.01)/diff.size <= 0.005 - - # todo: test_lgr_model_setup could use some more tests; although many potential issues will be tested by test_lgr_model_run @pytest.mark.parametrize('ic_cfg,default_source_data,expected', ( (None, False, 'top'), @@ -605,14 +590,49 @@ def test_pleasant_vertical_lgr_ic_strt(pleasant_vertical_lgr_cfg, ic_cfg, defaul np.allclose(m.ic.strt.array.mean(), m.parent.bas6.strt.array.mean(), rtol=0.01) else: + # check LGR parent heads against parent heads parent_headsfile = ic_cfg['source_data']['strt']['from_parent']['binaryfile'] - hds = flopy.utils.binaryfile.HeadFile(parent_headsfile).get_data() + hds = flopy.utils.binaryfile.HeadFile(parent_headsfile).get_data(kstpkper=(0, 0)) assert np.allclose(m.ic.strt.array.mean(), hds.mean(), rtol=0.01) - -#def test_stand_alone_parent(pleasant_lgr_stand_alone_parent): -# # todo: move test_stand_alone_parent test to test_lgr_model_run -# j=2 + # check for consistency between MFBinaryArraySourceData + # and regridded results + from mfsetup.interpolate import regrid3d + resampled_parent_heads = regrid3d(hds, + m.parent.modelgrid, + m.modelgrid, + mask1=None, mask2=None, method='linear') + + assert np.allclose(m.ic.strt.array, + np.round(resampled_parent_heads, 2)) + + from mfsetup.sourcedata import MFBinaryArraySourceData + sd = MFBinaryArraySourceData(variable='strt', filename=parent_headsfile, + datatype='array3d', + dest_model=m, + source_modelgrid=m.parent.modelgrid, + from_source_model_layers=None, + length_units=m.length_units, + time_units=m.time_units, + resample_method='linear', stress_period=0, + ) + data = sd.get_data() + assert np.allclose(m.ic.strt.array, + np.round(np.array(list(data.values())), 2)) + + # check LGR inset heads against parent heads + m.inset['plsnt_lgr_inset'].setup_dis() + m.inset['plsnt_lgr_inset'].setup_ic() + resampled_parent_heads_lgr_inset = regrid3d(hds, + m.parent.modelgrid, + m.inset['plsnt_lgr_inset'].modelgrid, + mask1=None, mask2=None, method='linear') + diff = m.inset['plsnt_lgr_inset'].ic.strt.array -\ + np.round(resampled_parent_heads_lgr_inset, 2) + + # a small percentage of cells are appreciably different + # unclear why + assert np.sum(np.abs(diff) > 0.01)/diff.size <= 0.0005 @pytest.mark.skip('need to add lake to stand-alone parent model') From 18ec7ffd31d981bc16360f139c3f8cdb4a906a8a Mon Sep 17 00:00:00 2001 From: "Leaf, Andrew T" Date: Wed, 13 Nov 2024 14:37:21 -0600 Subject: [PATCH 6/8] feat(initial conditions/strt array setup): add option to explicitly set starting heads from parent starting heads --- mfsetup/ic.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/mfsetup/ic.py b/mfsetup/ic.py index 69a27d82..5453ccde 100644 --- a/mfsetup/ic.py +++ b/mfsetup/ic.py @@ -47,9 +47,12 @@ def setup_strt(model, package, strt=None, source_data_config=None, raise ValueError(("'from_parent' in configuration by no parent model." f"{package} package, {model.name} model.\n" f"source_data config:\n{source_data_config}")) - from_parent_cfg = source_data_config['strt'].get('from_parent', {}) - binary_file = from_parent_cfg.get('binaryfile', False) - kwargs.update(from_parent_cfg) + if strt_config == 'from_parent': + default_parent_source_data = True + else: + from_parent_cfg = source_data_config['strt'].get('from_parent', {}) + binary_file = from_parent_cfg.get('binaryfile', False) + kwargs.update(from_parent_cfg) elif 'from_model_top' in strt_config: default_parent_source_data = False From 7176a4395535ee8809239c833f6a59a455e9c8a4 Mon Sep 17 00:00:00 2001 From: "Leaf, Andrew T" Date: Wed, 13 Nov 2024 14:37:53 -0600 Subject: [PATCH 7/8] tests(initial conditions/strt array setup): add and consolidate tests --- mfsetup/tests/conftest.py | 38 +++++++++++- mfsetup/tests/test_ic.py | 123 ++++++++++++++++++++++++++++++++++++++ mfsetup/tests/test_lgr.py | 77 ------------------------ 3 files changed, 160 insertions(+), 78 deletions(-) create mode 100644 mfsetup/tests/test_ic.py diff --git a/mfsetup/tests/conftest.py b/mfsetup/tests/conftest.py index 4b78214b..bcb3444e 100644 --- a/mfsetup/tests/conftest.py +++ b/mfsetup/tests/conftest.py @@ -10,7 +10,7 @@ fm = flopy.modflow mf6 = flopy.mf6 from mfsetup import MF6model, MFnwtModel -from mfsetup.fileio import exe_exists, load_cfg +from mfsetup.fileio import exe_exists, load, load_cfg from mfsetup.tests.test_pleasant_mf6_inset import ( get_pleasant_mf6, get_pleasant_mf6_with_dis, @@ -356,3 +356,39 @@ def basic_model_instance(request, 'pleasant_nwt': pleasant_nwt, 'get_pleasant_mf6': get_pleasant_mf6 }[request.param] + + +@pytest.fixture +def cfg_2x2x3_with_dis(project_root_path): + cfg = load(Path(project_root_path) / 'mfsetup/mf6_defaults.yml') + specified_cfg = { + 'setup_grid': { + 'xoff': 0., + 'yoff': 0., + 'nrow': 3, + 'ncol': 2, + 'dxy': 100., + 'rotation': 0., + 'crs': 26915 + }, + 'dis': { + 'options': { + 'length_units': 'meters' + }, + 'dimensions': { + 'nlay': 2, + 'nrow': 3, + 'ncol': 2 + }, + 'griddata': { + 'delr': 100., + 'delc': 100., + 'top': 20., + 'botm': [15., 0.] + }, + }} + cfg.update(specified_cfg) + kwargs = get_input_arguments(cfg['simulation'], mf6.MFSimulation) + sim = mf6.MFSimulation(**kwargs) + cfg['model']['simulation'] = sim + return cfg diff --git a/mfsetup/tests/test_ic.py b/mfsetup/tests/test_ic.py new file mode 100644 index 00000000..762456b6 --- /dev/null +++ b/mfsetup/tests/test_ic.py @@ -0,0 +1,123 @@ +"""Test initial conditions setup functionality +""" +import copy +from pathlib import Path + +import flopy +import numpy as np +import pytest + +from mfsetup import MF6model +from mfsetup.fileio import exe_exists, load, load_array, load_cfg, read_mf6_block +from mfsetup.tests.test_lgr import ( + pleasant_vertical_lgr_cfg, + pleasant_vertical_lgr_test_cfg_path, +) +from mfsetup.utils import get_input_arguments + + +@pytest.mark.parametrize('ic_config,expected', ( + ({'griddata': { + 'strt': 19. + }}, np.ones((2, 3, 2))*19), + ({'griddata': { + 'strt': [19., 18.] + }}, np.ones((2, 3, 2))*np.array([[[19]], [[18]]])), + )) +def test_ic_direct_input(cfg_2x2x3_with_dis, ic_config, expected): + """Very basic test of direct input to IC Package.""" + cfg = copy.deepcopy(cfg_2x2x3_with_dis) + cfg['ic'].update(ic_config) + m = MF6model(cfg=cfg) + m.setup_dis() + m.setup_ic() + assert np.allclose(m.ic.strt.array, expected) + + +@pytest.mark.parametrize('ic_cfg,default_source_data,expected', ( + ('no ic: block', False, 'top'), + ('no ic: block', True, 'parent starting heads'), + (None, False, 'top'), + ({'source_data': { + 'strt': { + 'from_parent': { + 'binaryfile': '../../../examples/data/pleasant/pleasant.hds', + 'stress_period': 0}}}}, True, 'parent heads'), + ({'source_data': { + 'strt': 'from_model_top'}}, True, 'top'), + ({'mfsetup_options': {}, + 'griddata': {}, + 'source_data': {}, + 'source_data_config': {}, + 'strt_filename_fmt': {}, + 'filename_fmt': {}}, True, 'parent starting heads'), + ({'source_data': { + 'strt': 'from_parent'}}, True, 'parent starting heads'), +)) +def test_pleasant_vertical_lgr_ic_strt(pleasant_vertical_lgr_cfg, ic_cfg, default_source_data, expected, + project_root_path): + """More advanced example of IC Package input in an LGR context where a MODFLOW 6 model is inset + within a MODFLOW-NWT model (one-way coupled with specified boundaries); and the MODFLOW 6 model + contains a second inset model that is dyanmically coupled using the local grid refinement (LGR) capability. + """ + cfg = copy.deepcopy(pleasant_vertical_lgr_cfg) + if ic_cfg == 'no ic: block': + del cfg['ic'] + else: + cfg['ic'] = ic_cfg + if not default_source_data: + cfg['parent']['default_source_data'] = False + m = MF6model(cfg=cfg) + m.setup_dis() + m.setup_ic() + assert m.ic.strt.array.shape == m.modelgrid.shape + if expected == 'top': + top3d = np.array([m.dis.top.array] * m.modelgrid.nlay) + assert np.allclose(m.ic.strt.array, top3d) + elif expected == 'parent starting heads': + np.allclose(m.ic.strt.array.mean(), + m.parent.bas6.strt.array.mean(), rtol=0.01) + else: + # check LGR parent heads against parent heads + parent_headsfile = str(project_root_path / 'examples/data/pleasant/pleasant.hds') + hds = flopy.utils.binaryfile.HeadFile(parent_headsfile).get_data(kstpkper=(0, 0)) + assert np.allclose(m.ic.strt.array.mean(), hds.mean(), rtol=0.01) + + # check for consistency between MFBinaryArraySourceData + # and regridded results + from mfsetup.interpolate import regrid3d + resampled_parent_heads = regrid3d(hds, + m.parent.modelgrid, + m.modelgrid, + mask1=None, mask2=None, method='linear') + + assert np.allclose(m.ic.strt.array, + np.round(resampled_parent_heads, 2)) + + from mfsetup.sourcedata import MFBinaryArraySourceData + sd = MFBinaryArraySourceData(variable='strt', filename=parent_headsfile, + datatype='array3d', + dest_model=m, + source_modelgrid=m.parent.modelgrid, + from_source_model_layers=None, + length_units=m.length_units, + time_units=m.time_units, + resample_method='linear', stress_period=0, + ) + data = sd.get_data() + assert np.allclose(m.ic.strt.array, + np.round(np.array(list(data.values())), 2)) + + # check LGR inset heads against parent heads + m.inset['plsnt_lgr_inset'].setup_dis() + m.inset['plsnt_lgr_inset'].setup_ic() + resampled_parent_heads_lgr_inset = regrid3d(hds, + m.parent.modelgrid, + m.inset['plsnt_lgr_inset'].modelgrid, + mask1=None, mask2=None, method='linear') + diff = m.inset['plsnt_lgr_inset'].ic.strt.array -\ + np.round(resampled_parent_heads_lgr_inset, 2) + + # a small percentage of cells are appreciably different + # unclear why + assert np.sum(np.abs(diff) > 0.01)/diff.size <= 0.0005 diff --git a/mfsetup/tests/test_lgr.py b/mfsetup/tests/test_lgr.py index e7683803..dddf829a 100644 --- a/mfsetup/tests/test_lgr.py +++ b/mfsetup/tests/test_lgr.py @@ -558,83 +558,6 @@ def test_lgr_model_setup(pleasant_lgr_setup_from_yaml, tmpdir): assert np.allclose(m.ic.strt.array, top3d) -@pytest.mark.parametrize('ic_cfg,default_source_data,expected', ( - (None, False, 'top'), - ({'source_data': { - 'strt': { - 'from_parent': { - 'binaryfile': '/Users/aleaf/Documents/GitHub/modflow-setup/examples/data/pleasant/pleasant.hds', - 'stress_period': 0}}}}, True, 'parent heads'), - ({'source_data': { - 'strt': 'from_model_top'}}, True, 'top'), - ({'mfsetup_options': {}, - 'griddata': {}, - 'source_data': {}, - 'source_data_config': {}, - 'strt_filename_fmt': {}, - 'filename_fmt': {}}, True, 'parent starting heads') -)) -def test_pleasant_vertical_lgr_ic_strt(pleasant_vertical_lgr_cfg, ic_cfg, default_source_data, expected): - cfg = copy.deepcopy(pleasant_vertical_lgr_cfg) - cfg['ic'] = ic_cfg - if not default_source_data: - cfg['parent']['default_source_data'] = False - m = MF6model(cfg=cfg) - m.setup_dis() - m.setup_ic() - assert m.ic.strt.array.shape == m.modelgrid.shape - if expected == 'top': - top3d = np.array([m.dis.top.array] * m.modelgrid.nlay) - assert np.allclose(m.ic.strt.array, top3d) - elif expected == 'parent starting heads': - np.allclose(m.ic.strt.array.mean(), - m.parent.bas6.strt.array.mean(), rtol=0.01) - else: - # check LGR parent heads against parent heads - parent_headsfile = ic_cfg['source_data']['strt']['from_parent']['binaryfile'] - hds = flopy.utils.binaryfile.HeadFile(parent_headsfile).get_data(kstpkper=(0, 0)) - assert np.allclose(m.ic.strt.array.mean(), hds.mean(), rtol=0.01) - - # check for consistency between MFBinaryArraySourceData - # and regridded results - from mfsetup.interpolate import regrid3d - resampled_parent_heads = regrid3d(hds, - m.parent.modelgrid, - m.modelgrid, - mask1=None, mask2=None, method='linear') - - assert np.allclose(m.ic.strt.array, - np.round(resampled_parent_heads, 2)) - - from mfsetup.sourcedata import MFBinaryArraySourceData - sd = MFBinaryArraySourceData(variable='strt', filename=parent_headsfile, - datatype='array3d', - dest_model=m, - source_modelgrid=m.parent.modelgrid, - from_source_model_layers=None, - length_units=m.length_units, - time_units=m.time_units, - resample_method='linear', stress_period=0, - ) - data = sd.get_data() - assert np.allclose(m.ic.strt.array, - np.round(np.array(list(data.values())), 2)) - - # check LGR inset heads against parent heads - m.inset['plsnt_lgr_inset'].setup_dis() - m.inset['plsnt_lgr_inset'].setup_ic() - resampled_parent_heads_lgr_inset = regrid3d(hds, - m.parent.modelgrid, - m.inset['plsnt_lgr_inset'].modelgrid, - mask1=None, mask2=None, method='linear') - diff = m.inset['plsnt_lgr_inset'].ic.strt.array -\ - np.round(resampled_parent_heads_lgr_inset, 2) - - # a small percentage of cells are appreciably different - # unclear why - assert np.sum(np.abs(diff) > 0.01)/diff.size <= 0.0005 - - @pytest.mark.skip('need to add lake to stand-alone parent model') def test_lgr_model_run(pleasant_lgr_stand_alone_parent, pleasant_lgr_setup_from_yaml, tmpdir, mf6_exe): From 908196aa756fcb2c75961a686ac82b5c956e6cf6 Mon Sep 17 00:00:00 2001 From: "Leaf, Andrew T" Date: Wed, 13 Nov 2024 14:38:53 -0600 Subject: [PATCH 8/8] docs: Add page on initial conditions setup; with example script for updating starting heads from a previous run --- docs/source/input/ic.rst | 88 ++++++++++++++++++- docs/source/input/index.rst | 2 +- .../update_starting_heads_from_previous.py | 20 +++++ 3 files changed, 106 insertions(+), 4 deletions(-) create mode 100644 examples/update_starting_heads_from_previous.py diff --git a/docs/source/input/ic.rst b/docs/source/input/ic.rst index 48016f51..35b50196 100644 --- a/docs/source/input/ic.rst +++ b/docs/source/input/ic.rst @@ -1,6 +1,88 @@ ======================================================================================= -Specifying Initial Conditions +Initial Conditions ======================================================================================= -.. note:: - This page is a work in progress and needs some more work. +Similar to other packages, input of initial conditions follows the structure of MODFLOW and Flopy. Setting the starting heads from the model top is often a good way to go initially. After the model has been run, starting heads can then be :ref:`updated from the initial model head output ` to improve convergence on subsequent runs. + + .. Note:: + + With any transient model, an :ref:`initial steady-state stress period