diff --git a/.doc/conf.py b/.doc/conf.py index 81d706255..4a966e2c8 100644 --- a/.doc/conf.py +++ b/.doc/conf.py @@ -58,12 +58,6 @@ # close the restructured text file f.close() -# # -- convert the tutorial scripts ------------------------------------------- -# if not on_rtd: -# cmd = ("python", "test_build.py") -# print(" ".join(cmd)) -# os.system(" ".join(cmd)) - # -- Build the example restructured text files ------------------------------- if not on_rtd: start_dir = os.getcwd() diff --git a/.doc/test_build.py b/.doc/test_build.py deleted file mode 100644 index 8a350453d..000000000 --- a/.doc/test_build.py +++ /dev/null @@ -1,97 +0,0 @@ -# Build files for sphinx. -import os -import shutil -import sys -from subprocess import PIPE, Popen - -import flopy - -# -- determine if running on CI or rtd -is_CI = "CI" in os.environ or os.environ.get("READTHEDOCS") == "True" - -# -- update flopy classes ---------------------------------------------------- -flopy.mf6.utils.generate_classes(branch="develop", backup=False) - -# -- update notebooks and tables --------------------------------------------- -pth = os.path.join("..", "scripts") -args = ("python", "process-scripts.py") -print(" ".join(args)) -proc = Popen(args, stdout=PIPE, stderr=PIPE, cwd=pth) -stdout, stderr = proc.communicate() -if stdout: - print(stdout.decode("utf-8")) -if stderr: - print("Errors:\n{}".format(stderr.decode("utf-8"))) - -# -- run the scripts --------------------------------------------------------- -if not is_CI: - pth = os.path.join("..", "scripts") - py_files = [ - file_name - for file_name in sorted(os.listdir(pth)) - if file_name.endswith(".py") and file_name.startswith("ex-") - ] - for file_name in py_files: - args = ("python", file_name) - print(" ".join(args)) - proc = Popen(args, stdout=PIPE, stderr=PIPE, cwd=pth) - stdout, stderr = proc.communicate() - if stdout: - print(stdout.decode("utf-8")) - if stderr: - print("Errors:\n{}".format(stderr.decode("utf-8"))) - -# -- create and edit examples.rst and copy the figures ----------------------- -if not is_CI: - pth = os.path.join("..", "etc") - args = ("python", "ci_create_examples.py") - print(" ".join(args)) - proc = Popen(args, stdout=PIPE, stderr=PIPE, cwd=pth) - stdout, stderr = proc.communicate() - if stdout: - print(stdout.decode("utf-8")) - if stderr: - print("Errors:\n{}".format(stderr.decode("utf-8"))) - -# -- get list of notebooks --------------------------------------------------- -pth = os.path.join("..", "notebooks") -nb_files = [ - file_name - for file_name in sorted(os.listdir(pth)) - if file_name.endswith(".ipynb") and file_name.startswith("ex-") -] - -# -- run notebooks with jupytext --------------------------------------------- -src_pth = os.path.join("..", "notebooks") -dst_pth = os.path.join("..", ".nbrun") -if os.path.isdir(dst_pth): - shutil.rmtree(dst_pth) -os.makedirs(dst_pth) -for file_name in nb_files: - src = os.path.join(src_pth, file_name) - dst = os.path.join(dst_pth, file_name) - arg = ( - "jupytext", - "--to ipynb", - "--from ipynb", - "--execute", - "-o", - dst, - src, - ) - print("running command...'{}'".format(" ".join(arg))) - os.system(" ".join(arg)) - -# -- remove ./_notebooks if it exists ---------------------------------------- -copy_pth = os.path.join("_notebooks") -print(f"clean up {copy_pth}") -if os.path.isdir(copy_pth): - shutil.rmtree(copy_pth) - -# -- copy executed notebooks to ./_notebooks --------------------------------- -print(f"copy files in {dst_pth} -> {copy_pth}") -shutil.copytree(dst_pth, copy_pth) - -# -- clean up (remove) dst_pth directory ------------------------------------- -print(f"clean up {dst_pth}") -shutil.rmtree(dst_pth) diff --git a/.github/workflows/ex-rtd.yml b/.github/workflows/ex-rtd.yml index 3bbe8c04f..3910fc959 100644 --- a/.github/workflows/ex-rtd.yml +++ b/.github/workflows/ex-rtd.yml @@ -21,9 +21,6 @@ jobs: runs-on: ubuntu-latest strategy: fail-fast: false - env: - script-directory: scripts - etc-directory: etc defaults: run: shell: bash @@ -57,12 +54,12 @@ jobs: python-version: 3.9 - name: Install Python packages + working-directory: etc run: | python -m pip install --upgrade pip setuptools wheel pip install -r requirements.pip.txt pip install -r requirements.usgs.txt python -m ipykernel install --name python_kernel --user - working-directory: ${{env.etc-directory}} - name: Update flopy MODFLOW 6 classes run: python -m flopy.mf6.utils.generate_classes --ref develop --no-backup @@ -75,21 +72,20 @@ jobs: with: repo: modflow6-nightly-build - - name: Run scripts without model runs - run: pytest -v -n=auto --durations=0 ci_build_files.py - working-directory: ${{env.etc-directory}} + - name: Create notebooks and run models + working-directory: autotest + run: pytest -v -n=auto --durations=0 test_notebooks.py --plot + + - name: Copy notebooks to RTD directory + run: cp -R notebooks/*.ipynb .doc/_notebooks/ - name: Run processing script + working-directory: scripts run: python process-scripts.py - working-directory: ${{env.script-directory}} - - - name: Run notebooks with jupytext for ReadtheDocs - run: pytest -v -n=auto --durations=0 ci_run_notebooks.py - working-directory: ${{env.etc-directory}} - name: Create package tables and examples.rst for ReadtheDocs + working-directory: etc run: python ci_create_examples_rst.py - working-directory: ${{env.etc-directory}} - name: List example rst files for ReadtheDocs run: ls -R .doc/_examples/ diff --git a/.github/workflows/ex-workflow.yml b/.github/workflows/ex-workflow.yml index 879c30041..99d6a3565 100644 --- a/.github/workflows/ex-workflow.yml +++ b/.github/workflows/ex-workflow.yml @@ -19,14 +19,6 @@ jobs: zip_files: name: zip input files runs-on: ubuntu-latest - strategy: - fail-fast: false - env: - script-directory: scripts - etc-directory: etc - defaults: - run: - shell: bash steps: - name: Checkout MODFLOW6 examples repo @@ -35,15 +27,16 @@ jobs: - name: Setup Python uses: actions/setup-python@v5 with: - python-version: 3.8 + python-version: 3.9 - name: Install Python packages + working-directory: etc run: | python --version python -m pip install --upgrade pip setuptools wheel pip install -r requirements.pip.txt pip install -r requirements.usgs.txt - working-directory: ${{env.etc-directory}} + python -m ipykernel install --name python_kernel --user - name: Update flopy MODFLOW 6 classes run: python -m flopy.mf6.utils.generate_classes --ref develop --no-backup @@ -56,9 +49,10 @@ jobs: with: repo: modflow6-nightly-build - - name: Run scripts without model runs - run: pytest -v -n=auto --durations=0 ci_build_files.py - working-directory: ${{env.etc-directory}} + - name: Create model + working-directory: autotest + # run the scripts via pytest without running the models, just build input files + run: pytest -v -n=auto --durations=0 test_scripts.py --init - name: zip input files run: | @@ -78,10 +72,7 @@ jobs: strategy: fail-fast: false matrix: - python: [3.8, 3.9, "3.10", "3.11"] - env: - script-directory: scripts - etc-directory: etc + python: [3.9, "3.10", "3.11", "3.12"] defaults: run: shell: bash @@ -111,11 +102,12 @@ jobs: python-version: ${{ matrix.python }} - name: Install Python packages + working-directory: etc run: | python -m pip install --upgrade pip setuptools wheel pip install -r requirements.pip.txt pip install -r requirements.usgs.txt - working-directory: ${{env.etc-directory}} + python -m ipykernel install --name python_kernel --user - name: Update flopy MODFLOW 6 classes run: python -m flopy.mf6.utils.generate_classes --ref develop --no-backup @@ -128,15 +120,14 @@ jobs: with: repo: modflow6-nightly-build - - name: Run scripts - run: pytest -v -n=auto --durations=0 --run=True ci_build_files.py - working-directory: ${{env.etc-directory}} + - name: Test example scripts and create input files + working-directory: autotest + run: pytest -v -n=auto --durations=0 test_scripts.py --plot - name: Run processing script + working-directory: scripts if: matrix.python == '3.9' - run: | - python process-scripts.py - working-directory: ${{env.script-directory}} + run: python process-scripts.py - name: Build mf6examples LaTeX document if: matrix.python == '3.9' @@ -181,9 +172,7 @@ jobs: uses: actions/upload-artifact@v3 with: name: current - path: | - ./mf6examples.pdf - + path: mf6examples.pdf # make the release if the "build" job was successful release: diff --git a/.gitignore b/.gitignore index a2c705f84..983602000 100644 --- a/.gitignore +++ b/.gitignore @@ -183,4 +183,7 @@ temp .nb/ # only version control jupytext-managed python scripts/*.py -notebooks/ \ No newline at end of file +notebooks/ + +**.DS_Store +**.zip \ No newline at end of file diff --git a/DEVELOPER.md b/DEVELOPER.md index 1ca16ca1e..feda3061a 100644 --- a/DEVELOPER.md +++ b/DEVELOPER.md @@ -1,112 +1,152 @@ # Developing MODFLOW 6 examples -This document describes how to set up an environment to use or develop the MODFLOW 6 example models. +This document describes development procedures and conventions for the MODFLOW 6 example models. - - [Overview](#overview) -- [Setup](#setup) - - [Install MODFLOW 6 and related programs](#install-modflow-6-and-related-programs) - - [Install Python dependencies](#install-python-dependencies) - - [Update FloPy](#update-flopy) +- [Prerequisites](#prerequisites) + - [Create a Python environment](#create-a-python-environment) + - [Install MODFLOW programs](#install-modflow-programs) + - [Update FloPy classes](#update-flopy-classes) - [Running the examples](#running-the-examples) - - [Using `jupyter`](#using-jupyter) - - [Using `pytest`](#using-pytest) - - [Using `jupytext`](#using-jupytext) + - [Running with `pytest`](#running-with-pytest) + - [Running with `jupytext`](#running-with-jupytext) + - [Running with `jupyter`](#running-with-jupyter) - [Contributing examples](#contributing-examples) - [Releasing the examples](#releasing-the-examples) - - [Generate notebooks and tables](#generate-notebooks-and-tables) - - [Build examples documentation](#build-examples-documentation) ## Overview -The examples are Python scripts managed with [`jupytext`](https://github.com/mwouts/jupytext). The Python files in `scripts/` are the ultimate source of truth. Data and notebooks are generated from the scripts. There are more examples than scripts: some of the scripts produce multiple examples. +The example models in this repository are composed with FloPy in the Python files under `scripts/`. Notebooks are generated from these with [`jupytext`](https://github.com/mwouts/jupytext) for the [online documentation](https://modflow6-examples.readthedocs.io/en/stable/) and/or for use with `jupyter`. A small volume of supporting data live in files under `data/`. -## Setup +Scripts may contain one or more example scenarios. Each script creates one or more subdirectories of `examples/` to use as a workspace, one per example scenario. Internally, the scripts are structured similarly, proceeding through the following steps: -This section lists steps to set up a development environment, build the examples from scripts, and finally build the examples documentation. +1. Define & build models +2. Write input files +3. Run models +4. Plot results -- install MODFLOW 6 and related programs -- Install Python dependencies -- make sure FloPy is up to date +The scripts parse environment variables to control several behaviors, all of which accept case-insensitive `true/false` strings: -### Install MODFLOW 6 and related programs +- `WRITE`: whether to write model input files to subdirectories of `examples/` +- `RUN`: whether to run models +- `PLOT`: whether to create plots of results +- `PLOT_SHOW`: whether to show static plots (disabled by default when run with `pytest`) +- `PLOT_SAVE`: whether to save static plots to `figures/` +- `GIF`: whether to save GIFs to `figures/` (only relevant for a small subset of scripts) -Install modeling programs using the `get-modflow` utility that is available from FloPy. From any directory, issue the following commands: +If variables are not found when a script is run directly, behaviors are enabled by default. When scripts are run via `pytest`, by default plots are not shown (to avoid the need to manually close plot widgets). -```commandline -get-modflow : -get-modflow --repo modflow6-nightly-build : -``` +## Prerequisites -The command will show a prompt to select the install location. Any location can be selected, as `get-modflow` will put the executables on your path (the notebooks expect binaries to be available on the path). +To develop the examples one must first: -### Install Python dependencies +- create a Python environment +- install MODFLOW programs +- update FloPy classes -Several Python packages are required to create the MODFLOW 6 examples. These are listed in `etc/requirements.pip.txt` and `etc/requirements.usgs.txt`. +### Create a Python environment -### Update FloPy +Several Python packages are required to run the example scripts. These are listed in `etc/requirements.pip.txt` and `etc/requirements.usgs.txt`. Once a Python environment has been created, e.g. with `venv` or Conda, dependencies can be installed with: -It's important that FloPy is up-to-date with the latest changes to MODFLOW 6. The latest release of FloPy is always up to date with the latest release of MODFLOW 6. +```shell +pip install -r etc/requirements.pip.txt +pip install -r etc/requirements.usgs.txt +``` -To manually update FloPy from some branch of the [official MODFLOW 6 repository](https://github.com/MODFLOW-USGS/modflow6): +### Install MODFLOW programs -```commandline -import flopy -flopy.mf6.utils.generate_classes(owner="MODFLOW-USGS", branch="master", backup=True) -``` +Besides MODFLOW 6, the examples use a number of MODFLOW-related programs, discovered on the system `PATH`. -The above is equivalent to calling the function with no arguments. Arguments may be substituted to select an alternative repository (e.g. your fork) or branch. +With FloPy installed in the active Python environment, these can all be installed with: -## Running the examples +```shell +get-modflow : +``` -The examples can be run with Jupyter or with Pytest. +You will be prompted to select an install location — your virtual environment's bindir is a good place, as it is already on the system `PATH` and keeps things isolated. -### Using `jupyter` +Typically one develops against the cutting edge of MF6 — this might entail adding a local development version of MF6 to the bindir as well. Alternatively, the MF6 nightly build can be installed with: -To start a Jupyter browser interface, run `jupyter notebook` from the `notebooks/` directory. +```shell +get-modflow --repo modflow6-nightly-build : +``` -### Using `pytest` +See the [FloPy documentation](https://flopy.readthedocs.io/en/stable/md/get_modflow.html) for more info on the `get-modflow` utility. -Pytest can be used to run all of the example scripts and models. The `pytest-xdist` plugin is a convenient way to run the scripts in parallel. Note that `pytest` must be invoked from the `etc/` directory, *not* from `scripts/`. +### Update FloPy classes -To run example scripts in parallel with verbose output, and generate input files *without* running models: +FloPy and MODFLOW 6 versions must be kept in sync for FloPy to properly generate and consume MF6 input/output files. To update FloPy from some branch of the [MODFLOW 6 repository](https://github.com/MODFLOW-USGS/modflow6), for instance the `develop` branch: ```shell -pytest -v -n auto ci_build_files.py +python -m flopy.mf6.utils.generate_classes --ref develop --no-backup ``` -To run models, use `--run True`. +The `--repo` argument can be used to select an alternative repository (e.g. your fork). See [the FloPy documentation](https://flopy.readthedocs.io/en/stable/md/generate_classes.html) for more info. -To run in serial instead of parallel, omit `-n auto`. +## Running the examples + +The example scripts can be run directly from the `scripts/` directory, e.g. `python ex-gwf-twri.py`. The environment variables described above can be used to control their behavior. The examples can also be run with `pytest`, converted to notebooks and/or executed with `jupytext`, or run as notebooks with `jupyter`. + +**Note**: notebooks are *not* versioned in this repository — the `scripts/` are the single source of truth. + +### Running with `pytest` + +The examples can be tested from the `autotest/` directory, either directly as scripts, or as notebooks. + +When run via `pytest`, behaviors can be controlled with `pytest` CLI flags: + +- `--init`: just build models and write input files, defaults false +- `--no-write`: don't write models, defaults false +- `--no-run`: don't run models, defaults false +- `--plot`: enable plot creation, defaults false +- `--show`: show plots, defaults false +- `--no-save`: don't save static plots, defaults false +- `--no-gif`: don't create/save gifs, defaults false -### Using `jupytext` +The last three only apply if `--plot` is enabled. Plotting is disabled by default to avoid having to manually close plot widgets while running tests. -The example scripts can be converted to notebooks with `jupytext`. For instance, from the project root, to generate and execute a notebook in `notebooks/`: +For instance, to create model input files (without running models or plotting results, to save time): ```shell -jupytext --from py --to ipynb scripts/ex-gwf-twri.py -o notebooks/ex-gwf-twri.ipynb --execute +pytest -v -n auto test_scripts.py --init ``` -## Contributing examples +### Running with `jupytext` -Adding a new example requires adding a new example script in the `scripts/` folder and adding a new LaTeX problem description in the `doc/sections/` folder. Then open a pull request from your fork of the repository. +To convert an example script to a notebook manually with `jupytext` (add `--execute` to run the notebook after conversion): -## Releasing the examples +```shell +jupytext --from py --to ipynb scripts/ex-gwf-twri.py -o notebooks/ex-gwf-twri.ipynb +``` -A new release is automatically created whenever code is merged into the trunk branch of this repository. Steps to manually prepare for a release are listed below. +### Running with `jupyter` -1. Generate notebooks and tables -2. Build examples documentation +To start a Jupyter browser interface, run `jupyter notebook` from the `notebooks/` directory after notebooks have been created with `jupytext`. -### Generate notebooks and tables +## Contributing examples + +To add a new example: -The example scripts must be converted into jupyter notebooks using `jupytext`, then latex tables generated from specially formatted code/comments. Run `process-scripts.py` in the `scripts/` directory to do this. +1. Add a new example script in the `scripts/` folder +2. Add a new LaTeX description in the `doc/sections/` folder +3. Ensure the script runs successfully and creates any expected files +3. Open a pull request from your fork to the upstream repo's `develop` branch -### Build examples documentation +## Releasing the examples -If the figures and tables were generated correctly, then it should be possible to build the examples PDF document. The PDF can be created by processing `doc/mf6examples.tex` with `pdftex` or a similar tool. +GitHub Actions automatically creates a new release whenever code is merged into the `master` branch of this repository. Steps to prepare for a release include: + +1. Run the examples to generate model input files, disabling model runs and plots — e.g. from the `autotest/` directory, run `pytest -v -n auto test_scripts.py`. **Note**: if model runs are enabled, the size of the `examples/` directory will balloon from double-digit MB to several GB. We only distribute input files, not output files. +2. Generate notebooks and tables — from the `scripts/` directory, run `process-scripts.py` to generate LaTeX tables for the documentation PDF from specially formatted code/comments in the example scripts. +3. Build the documentation PDF with multiple passes from e.g. `pdflatex` and `bibtex` — for instance, from the `doc/` directory: + 1. `pdflatex mf6examples.tex` + 2. `bibtex mf6examples` + 3. `pdflatex mf6examples.tex` + 4. `pdflatex mf6examples.tex` +4. Zip up the model input files. +5. Release the documentation PDF and model files archive. diff --git a/README.md b/README.md index c2c0b36d5..4e9e3eb0c 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,15 @@ + + +**Table of Contents** *generated with [DocToc](https://github.com/thlorenz/doctoc)* + +- [MODFLOW 6 Example Problems](#modflow-6-example-problems) + - [Release contents](#release-contents) + - [Repository contents](#repository-contents) + - [Resources](#resources) + - [Issues](#issues) + + + # MODFLOW 6 Example Problems [![CI](https://github.com/MODFLOW-USGS/modflow6-examples/actions/workflows/ex-workflow.yml/badge.svg)](https://github.com/MODFLOW-USGS/modflow6-examples/actions/workflows/ex-workflow.yml) @@ -7,31 +19,29 @@ This repository contains MODFLOW 6 example problems. ## Release contents -When changes reach the `master` branch, this repository's contents are [rebuilt and posted as a new release](https://github.com/MODFLOW-USGS/modflow6-examples/releases). The following resources are included in a release: - -* a PDF containing a description of the example problems -* input files for each of the example problems +When changes reach the `master` branch, this repository's contents are [rebuilt and posted as a new release](https://github.com/MODFLOW-USGS/modflow6-examples/releases) with the following assets: -## Online documentation - -Instructions for creating and running example models with FloPy are automatically generated and posted [here](https://modflow6-examples.readthedocs.io/en/latest/index.html). +* PDF containing a description of the example problems +* archive containing input files for the example models ## Repository contents -* [Python scripts that use flopy to create, run, and post process the example problems](https://github.com/MODFLOW-USGS/modflow6-examples/tree/master/scripts) -* [Jupyter notebooks that create, run, and post process the example problems](https://github.com/MODFLOW-USGS/modflow6-examples/tree/master/notebooks) - -## Creating a new example - -Instructions for creating a new MODFLOW 6 example are listed in the [developer docs](DEVELOPER.md). +* `autotest/`: tests for the example models +* `data/`: data files used by the example models +* `doc/`: LaTeX files to build the PDF documentation +* `etc/`: dependency specifications and miscellaneous scripts +* `images/`: static images used in the documentation +* `scripts/`: Python scripts that use FloPy to create, run, and post-process the example models +* `.github/`: continuous integration workflows +* `.doc/`: source files for the ReadTheDocs site -# Other Resources +## Resources * [U.S. Geological Survey MODFLOW 6 Page](https://www.usgs.gov/software/modflow-6-usgs-modular-hydrologic-model) * [MODFLOW 6 binary executables for Windows, Mac, and Linux](https://github.com/MODFLOW-USGS/modflow6-nightly-build/releases) * [MODFLOW 6 Repository](https://github.com/MODFLOW-USGS/modflow6) * [Binary executables for MODFLOW and other related programs that may be required to run these examples](https://github.com/MODFLOW-USGS/executables) -# Issues +## Issues Any issues with the MODFLOW 6 example problems should be posted on the main [MODFLOW 6 GitHub repo](https://github.com/MODFLOW-USGS/modflow6) and flagged with the [examples](https://github.com/MODFLOW-USGS/modflow6/labels/examples) label. diff --git a/autotest/conftest.py b/autotest/conftest.py new file mode 100644 index 000000000..22d9c4d22 --- /dev/null +++ b/autotest/conftest.py @@ -0,0 +1,95 @@ +from pathlib import Path + +import pytest + +PROJ_ROOT = Path(__file__).parents[1] +SCRIPTS_PATH = PROJ_ROOT / "scripts" +TABLES_PATH = PROJ_ROOT / "tables" +IMAGES_PATH = PROJ_ROOT / "images" +FIGURES_PATH = PROJ_ROOT / "figures" +EXAMPLES_PATH = PROJ_ROOT / "examples" +NOTEBOOKS_PATH = PROJ_ROOT / "notebooks" +RTD_PATH = PROJ_ROOT / ".doc" / "_notebooks" +EXCLUDE = [] + + +@pytest.fixture(scope="session") +def write(request) -> bool: + return not request.config.getoption("--no-write") + + +@pytest.fixture(scope="session") +def run(request) -> bool: + return not (request.config.getoption("--init") or request.config.getoption("--no-run")) + + +@pytest.fixture(scope="session") +def plot(request) -> bool: + return request.config.getoption("--plot") + + +@pytest.fixture(scope="session") +def plot_show(request, plot) -> bool: + return plot and request.config.getoption("--show") + + +@pytest.fixture(scope="session") +def plot_save(request, plot) -> bool: + return plot and not request.config.getoption("--no-save") + + +@pytest.fixture(scope="session") +def gif(request, plot) -> bool: + return plot and not request.config.getoption("--no-gif") + + +def pytest_addoption(parser): + parser.addoption( + "--init", + action="store_true", + default=False, + help="Just build and write model input files", + ) + parser.addoption( + "--no-write", action="store_true", default=False, help="Disable model build/write" + ) + parser.addoption( + "--no-run", action="store_true", default=False, help="Disable model runs" + ) + parser.addoption( + "--plot", + action="store_true", + default=False, + help="Create plots (disabled by default)", + ) + parser.addoption( + "--show", + action="store_true", + default=False, + help="Show plots (disabled by default)", + ) + parser.addoption( + "--no-save", action="store_true", default=False, help="Disable plot saving" + ) + parser.addoption( + "--no-gif", action="store_true", default=False, help="Disable GIF creation" + ) + + +def pytest_generate_tests(metafunc): + # make directories if needed + TABLES_PATH.mkdir(exist_ok=True) + IMAGES_PATH.mkdir(exist_ok=True) + FIGURES_PATH.mkdir(exist_ok=True) + EXAMPLES_PATH.mkdir(exist_ok=True) + NOTEBOOKS_PATH.mkdir(exist_ok=True) + RTD_PATH.mkdir(exist_ok=True, parents=True) + + # generate example scenarios + if "example_script" in metafunc.fixturenames: + scripts = { + file.name: file + for file in sorted(SCRIPTS_PATH.glob("ex-*.py")) + if file.stem not in EXCLUDE + } + metafunc.parametrize("example_script", scripts.values(), ids=scripts.keys()) diff --git a/autotest/test_notebooks.py b/autotest/test_notebooks.py new file mode 100644 index 000000000..5060abe7e --- /dev/null +++ b/autotest/test_notebooks.py @@ -0,0 +1,33 @@ +"""Convert example scripts to notebooks and test notebook execution.""" + +from os import environ + +from modflow_devtools.markers import requires_exe +from modflow_devtools.misc import run_cmd, set_env + +from conftest import NOTEBOOKS_PATH + + +@requires_exe("jupytext") +def test_notebooks(example_script, write, run, plot, plot_save, gif): + with set_env( + WRITE=str(write), + RUN=str(run), + PLOT=str(plot), + PLOT_SHOW=str(True), + PLOT_SAVE=str(plot_save), + GIF=str(gif), + ): + args = [ + "jupytext", + "--from", + "py", + "--to", + "ipynb", + "--execute", + example_script, + "-o", + NOTEBOOKS_PATH / example_script.with_suffix(".ipynb").name, + ] + stdout, stderr, retcode = run_cmd(*args, verbose=True, env=environ) + assert not retcode, stdout + stderr diff --git a/autotest/test_scripts.py b/autotest/test_scripts.py new file mode 100644 index 000000000..9e0826714 --- /dev/null +++ b/autotest/test_scripts.py @@ -0,0 +1,20 @@ +"""Test run example scripts.""" + +import sys +from os import environ + +from modflow_devtools.misc import run_cmd, set_env + + +def test_scripts(example_script, write, run, plot, plot_show, plot_save, gif): + with set_env( + WRITE=str(write), + RUN=str(run), + PLOT=str(plot), + PLOT_SHOW=str(plot_show), + PLOT_SAVE=str(plot_save), + GIF=str(gif), + ): + args = [sys.executable, example_script] + stdout, stderr, retcode = run_cmd(*args, verbose=True, env=environ) + assert not retcode, stdout + stderr diff --git a/common/DisvCurvilinearBuilder.py b/common/DisvCurvilinearBuilder.py deleted file mode 100644 index d79bc9964..000000000 --- a/common/DisvCurvilinearBuilder.py +++ /dev/null @@ -1,803 +0,0 @@ -import numpy as np - -from DisvPropertyContainer import DisvPropertyContainer - -__all__ = ["DisvCurvilinearBuilder"] - - -class DisvCurvilinearBuilder(DisvPropertyContainer): - """ - A class for generating a curvilinear MODFLOW 6 DISV grid. A curvilinear - grid is similar to a radial grid, composed of radial bands, but includes - ncol discretization within a radial band and does not have to form an - entire circle (such as, a discretized wedge). - - This class inherits from the `DisvPropertyContainer` class and provides - methods to generate a curvilinear grid using radial and angular parameters. - - All indices are zero-based, but translated to one-base for the figures and - by flopy for use with MODFLOW 6. Angles are in degrees, with ``0`` being in - the positive x-axis direction and ``90`` in the positive y-axis direction. - - If no arguments are provided then an empty object is returned. - - Parameters - ---------- - nlay : int - Number of layers - radii : array_like - List of radial distances that describe the radial bands. - The first radius is the innermost radius, and then the rest are the - outer radius of each radial band. Note that the number of radial bands - is equal to ``len(radii) - 1``. - angle_start : float - Starting angle in degrees for the curvilinear grid. - angle_stop : float - Stopping angle in degrees for the curvilinear grid. - angle_step : float - Column discretization of each radial band. - If positive, then represents the angle step in degrees for each column - in a radial band. That is, the number of columns (`ncol`) is: - ``ncol = (angle_stop - angle_start)/angle_step`` - If negative, then the absolute value is the number of columns (ncol). - surface_elevation : float or array_like - Surface elevation for the top layer. Can either be a single float - for the entire `top`, or array_like of length `nradial`, or - array_like of length `ncpl`. - layer_thickness : float or array_like - Thickness of each layer. Can either be a single float - for model cells, or array_like of length `nlay`, or - array_like of length `ncpl`. - single_center_cell : bool, default=False - If True, include a single center cell. If true, then innermost `radii` - must be **zero**. That is, the innermost, radial band has ``ncol=1``. - origin_x : float, default=0.0 - X-coordinate reference point for the `radii` distance. - origin_y : float, default=0.0 - Y-coordinate reference point for the `radii` distance. - - Attributes - ---------- - nradial : int - Number of radial bands in the grid. - ncol : int - Number of columns in each radial band. - inner_vertex_count : int - Number of vertices in the innermost radial band. - single_center_cell : bool - Whether a single center cell is included. - full_circle : bool - Whether the grid spans a full circle. That is, - full_circle = `angle_start`==`angle_stop`==``0``). - radii : numpy.ndarray - Array of radial distances from (origin_x, origin_y) for each radial - band. The first value is the innermost radius and the remaining are - each radial bands outer radius. - angle_start : float - Starting angle in degrees for the curvilinear grid. - angle_stop : float - Stopping angle in degrees for the curvilinear grid. - angle_step : float - Angle step in degrees for each column in a radial band. - angle_span : float - Span of the angle range in degrees for the curvilinear grid. - - Methods - ------- - get_disv_kwargs() - Get the keyword arguments for creating a MODFLOW-6 DISV package. - plot_grid(...) - Plot the model grid from `vertices` and `cell2d` attributes. - get_cellid(rad, col, col_check=True) - Get the cellid given the radial and column indices. - get_rad_col(cellid) - Get the radial and column indices given the cellid. - get_vertices(rad, col) - Get the vertex indices for a cell given the radial and column indices. - calc_curvilinear_ncol(angle_start, angle_stop, angle_step) - Calculate the number of columns in the curvilinear grid based on - the given angle parameters. It will adjust `angle_step` to ensure - that the number of columns is an integer value. - iter_rad_col() - Iterate through the radial band columns, then bands. - iter_radial_cellid(rad) - Iterate through the cellid within a radial band. - iter_column_cellid(col) - Iterate through the cellid along a column across all radial bands. - """ - - nradial: int - ncol: int - inner_vertex_count: int - single_center_cell: bool - full_circle: bool - radii: np.ndarray - angle_start: float - angle_stop: float - angle_step: float - angle_span: float - - def __init__( - self, - nlay=-1, - radii=np.array((0.0, 1.0)), - angle_start=0.0, - angle_stop=90.0, - angle_step=-1, - surface_elevation=100.0, - layer_thickness=100.0, - single_center_cell=False, - origin_x=0.0, - origin_y=0.0, - ): - if nlay is None or nlay < 1: - self._init_empty() - return - - if angle_start < 0.0: - angle_start += 360.0 - if angle_stop < 0.0: - angle_stop += 360.0 - if abs(angle_step) < 1.0e-30: - raise RuntimeError( - "DisvCurvilinearBuilder: angle_step is near zero" - ) - - angle_span = self._get_angle_span(angle_start, angle_stop) - - ncol, angle_step = self.calc_curvilinear_ncol( - angle_start, angle_stop, angle_step - ) - - if angle_step > 90.0: - angle_step = 90.0 - ncol, angle_step = self.calc_curvilinear_ncol( - angle_start, angle_stop, angle_step - ) - - if angle_span < angle_step: - raise RuntimeError( - "DisvCurvilinearBuilder: angle_step is greater than " - "the total angel, that is:\n" - "angle_step > |angle_stop - angle_start|\n" - f"{angle_step} > {angle_span}" - ) - - try: - nradial = len(radii) - 1 - except TypeError: - raise RuntimeError( - "DisvCurvilinearBuilder: radii must be list-like type" - ) - - if nradial < 1: - raise RuntimeError( - "DisvCurvilinearBuilder: len(radii) must be greater than 1" - ) - - if single_center_cell and radii[0] > 1.0e-100: - raise RuntimeError( - "DisvCurvilinearBuilder: single_center_cell=True must " - "have the first radii be zero, that is: radii[0] = 0.0\n" - f"Input received radii[0]={radii[0]}" - ) - - full_circle = 359.999 < angle_span - nver = ncol if full_circle else ncol + 1 - - ncpl = ncol * nradial # Nodes per layer - if single_center_cell: - ncpl = (ncol * nradial) - ncol + 1 - - self.radii = np.array(radii, dtype=np.float64) - self.nradial = nradial - self.ncol = ncol - - self.single_center_cell = single_center_cell - self.full_circle = full_circle - - self.angle_start = angle_start - self.angle_stop = angle_stop - self.angle_step = angle_step - self.angle_span = angle_span - - cls_name = "DisvCurvilinearBuilder" - top = self._get_array(cls_name, surface_elevation, ncpl, nradial) - thick = self._get_array(cls_name, layer_thickness, nlay, ncpl * nlay) - - if top.size == nradial and nradial != ncpl: - tmp = [] - for it, rad in top: - if it == 0 and single_center_cell: - tmp.append(rad) - else: - tmp += ncol * [rad] - top = np.array(tmp) - del tmp - - bot = [] - - if thick.size == nlay: - for lay in range(nlay): - bot.append(top - thick[: lay + 1].sum()) - else: - st = 0 - sp = ncpl - bt = top.copy() - for lay in range(nlay): - bt -= thick[st:sp] - st, sp = sp, sp + ncpl - bot.append(bt) - - if single_center_cell and full_circle: - # Full, filled circle - No vertex at center - inner_vertex_count = 0 - elif self.radii[0] < 1.0e-100: - # Single point at circle center - inner_vertex_count = 1 - else: - # Innermost vertices are the same as outer bands - inner_vertex_count = nver - - self.inner_vertex_count = inner_vertex_count - - # Build the grid - - vertices = [] - iv = 0 - stp = np.radians(angle_step) # angle step in radians - - # Setup center vertex - if inner_vertex_count == 1: - vertices.append([iv, 0.0, 0.0]) # Single vertex at center - iv += 1 - - # Setup vertices - st = 0 if inner_vertex_count > 1 else 1 - for rad in self.radii[st:]: - ang = np.radians(angle_start) # angle start in radians - for it in range(nver): - xv = rad * np.cos(ang) - yv = rad * np.sin(ang) - vertices.append([iv, xv, yv]) - iv += 1 - ang += stp - - # cell2d: [icell2d, xc, yc, ncvert, icvert] - cell2d = [] - ic = 0 - for rad in range(nradial): - single_cell_rad0 = self.single_center_cell and rad == 0 - for col in range(ncol): - icvert = self.get_vertices(rad, col) - # xc, yc = get_cell_center(rad, col) - if single_cell_rad0: - xc, yc = 0.0, 0.0 - else: - xc, yc = self.get_centroid(icvert, vertices) - cell2d.append([ic, xc, yc, len(icvert), *icvert]) - ic += 1 - if single_cell_rad0: - break - - super().__init__(nlay, vertices, cell2d, top, bot, origin_x, origin_y) - - def __repr__(self): - return super().__repr__("DisvCurvilinearBuilder") - - def _init_empty(self): - super()._init_empty() - nul = np.array([]) - self.nradial = 0 - self.ncol = 0 - self.inner_vertex_count = 0 - self.single_center_cell = False - self.full_circle = False - self.radii = nul - self.angle_start = 0 - self.angle_stop = 0 - self.angle_step = 0 - self.angle_span = 0 - - def property_copy_to(self, DisvCurvilinearBuilderType): - if isinstance(DisvCurvilinearBuilderType, DisvCurvilinearBuilder): - super().property_copy_to(DisvCurvilinearBuilderType) - DisvCurvilinearBuilderType.nradial = self.nradial - DisvCurvilinearBuilderType.ncol = self.ncol - DisvCurvilinearBuilderType.full_circle = self.full_circle - DisvCurvilinearBuilderType.radii = self.radii - DisvCurvilinearBuilderType.angle_start = self.angle_start - DisvCurvilinearBuilderType.angle_stop = self.angle_stop - DisvCurvilinearBuilderType.angle_step = self.angle_step - DisvCurvilinearBuilderType.angle_span = self.angle_span - DisvCurvilinearBuilderType.inner_vertex_count = ( - self.inner_vertex_count - ) - DisvCurvilinearBuilderType.single_center_cell = ( - self.single_center_cell - ) - else: - raise RuntimeError( - "DisvCurvilinearBuilder.property_copy_to " - "can only copy to objects that inherit " - "properties from DisvCurvilinearBuilder" - ) - - def copy(self): - cp = DisvCurvilinearBuilder() - self.property_copy_to(cp) - return cp - - def get_cellid(self, rad, col, col_check=True): - """ - Get the cellid given the radial and column indices. - - Parameters - ---------- - rad : int - Radial index. - col : int - Column index. - col_check : bool, default=True - If True, than a RuntimeError error is raised for single_center_cell - grids with ``rad==0`` and ``col>0``. Otherwise, assumes ``col=0``. - - Returns - ------- - int - cellid index - """ - ncol = self.ncol - if self.single_center_cell: - # Have to account for only one cell at the center - if rad == 0 and col > 0: - if col_check: - raise RuntimeError( - "DisvCurvilinearBuilder: Bad rad and col given" - ) - return 0 - # if rad == 0, then first cell and pos = 0 - # else account for inner cell, plus each ncol band - pos = 1 + ncol * (rad - 1) + col if rad > 0 else 0 - else: - pos = rad * ncol + col - - return pos - - def get_rad_col(self, cellid): - """ - Get the radial and column indices given the cellid. - - Parameters - ---------- - cellid : int - cellid index - - Returns - ------- - (int, int) - Radial index, Column index - """ - ncol = self.ncol - - if cellid < 1: - rad, col = 0, 0 - elif self.single_center_cell: - cellid -= 1 # drop out first radial band (single cell) - rad = cellid // ncol + 1 - col = cellid - ncol * (rad - 1) - else: - rad = cellid // ncol - col = cellid - ncol * rad - - return rad, col - - def get_vertices(self, rad, col): - """ - Get the vertex indices for a cell given the radial and column indices. - - Parameters - ---------- - rad : int - Radial index. - col : int - Column index. - - Returns - ------- - list[int] - List of vertex indices that define the cell at (rad, col). - """ - ivc = self.inner_vertex_count - full_circle = self.full_circle - ncol = self.ncol - nver = ncol if full_circle else ncol + 1 - - if rad == 0: # Case with no center point or single center point - if self.single_center_cell: - return [iv for iv in range(nver + ivc)][::-1] - elif ivc == 1: # Single center point - if full_circle and col == ncol - 1: - return [1, col + 1, 0] # [col+2-nver, col+1, 0] - return [col + 2, col + 1, 0] - elif full_circle and col == ncol - 1: - return [col + 1, nver + col, col, col + 1 - nver] - else: # Normal inner band - return [nver + col + 1, nver + col, col, col + 1] - - n = (rad - 1) * nver + ivc - - if full_circle and col == ncol - 1: - return [n + col + 1, n + nver + col, n + col, n + col + 1 - nver] - - return [n + nver + col + 1, n + nver + col, n + col, n + col + 1] - - def iter_rad_col(self): - """Generator that iterates through the radial band columns, then bands. - - Yields - ------- - (int, int) - radial band index, column index - """ - for cellid in range(self.ncpl): - yield self.get_rad_col(cellid) - - def iter_radial_cellid(self, rad): - """Generator that iterates through the cellid within a radial band. - - Parameters - ---------- - rad : int - Radial index. - - Yields - ------- - int - cellid index - """ - st = self.get_cellid(rad, 0) - if self.single_center_cell and rad == 0: - return iter([st]) - sp = self.get_cellid(rad, self.ncol - 1) + 1 - return iter(range(st, sp)) - - def iter_column_cellid(self, col): - """Generator that iterates through the cellid along a column across - all radial bands. - - Parameters - ---------- - col : int - Column index. - - Yields - ------- - int - cellid index - """ - rad = 0 - while rad < self.nradial: - yield self.get_cellid(rad, col) - rad += 1 - - def iter_columns(self, rad): - """Generator that iterates through the columns within a radial band. - - Parameters - ---------- - rad : int - Radial index. - - Yields - ------- - int - column index - """ - if self.single_center_cell and rad == 0: - return iter([0]) - return iter(range(0, self.ncol)) - - @staticmethod - def _get_angle_span(angle_start, angle_stop): - # assumes angles are between 0 and 360 - if abs(angle_stop - angle_start) < 0.001: # angle_stop == angle_start - return 360.0 - if angle_start < angle_stop: - return angle_stop - angle_start - return 360.0 - angle_start + angle_stop - - @staticmethod - def calc_curvilinear_ncol(angle_start, angle_stop, angle_step): - """ - Calculate the number of columns in the curvilinear grid based on - the given angle parameters. It will adjust `angle_step` to ensure - that the number of columns is an integer value. - - Parameters - ---------- - angle_start : float - Starting angle in degrees for the curvilinear grid. - angle_stop : float - Stopping angle in degrees for the curvilinear grid. - angle_step : float - If positive, then represents the largest angle step in degrees - for each column in a radial band. It may be reduced to make - the number of columns be a positive, integer. - If negative, then the absolute value is the number of columns - (ncol) and angle_step is calculated based on it. - - Returns - ------- - (int, float) - The number of columns in the curvilinear grid and the angle_step - that can reproduce the exact integer number. - """ - angle_span = DisvCurvilinearBuilder._get_angle_span( - angle_start, angle_stop - ) - - if angle_step > 0.0: - ncol = int(angle_span // angle_step) - if (angle_span / angle_step) - ncol > 0.1: # error towards larger - ncol += 1 - else: - ncol = int(round(-1 * angle_step)) - angle_step = angle_span / ncol - return ncol, angle_step - - -if __name__ == "__main__": - import matplotlib.pyplot as plt - - test1 = DisvCurvilinearBuilder( - 1, - [0, 1, 2, 3], - angle_start=0, - angle_stop=90, - angle_step=10, - single_center_cell=False, - ) - - tst = test1.copy() - print(test1) - print(tst) - - def tmp(**kwargs): - assert "nlay" in kwargs - - tmp(**test1) - del test1 - - def check( - nlay, - radii, - a_st, - a_sp, - a_stp, - single, - plot_time=0.0, - title="", - dpi=150, - ): - ob = DisvCurvilinearBuilder( - nlay, - radii, - angle_start=a_st, - angle_stop=a_sp, - angle_step=a_stp, - single_center_cell=single, - ) - - assert nlay == ob.nlay - assert single == ob.single_center_cell - nradial = ob.nradial - ncol = ob.ncol - for rad in range(nradial): - for col in range(ncol): - node = ob.get_cellid(rad, col) - assert (rad, col) == ob.get_rad_col(node) - - if single and rad == 0: - break - - if title == "": - title = ( - f"({ob.angle_step}°, {ob.angle_span}°) and " - f"({ncol}, {nradial}) and SingleCenter={single}" - f"" - ) - - if plot_time < 0: - fig, _ = ob.plot_grid(show=False) - plt.close(fig) - else: - ob.plot_grid(title=title, plot_time=plot_time, dpi=dpi) - - # Non-Plot Checks Angle ------------------------------------------------- - - check(3, [0, 10], 0, 90, 15, False, plot_time=-1) - check(3, [0, 10], 0, 180, 15, False, plot_time=-1) - check(3, [0, 10], 0, 270, 15, False, plot_time=-1) - check(3, [0, 10], 0, 360, 15, False, plot_time=-1) - check(3, [0, 10], 0, 0, 15, False, plot_time=-1) - - check(3, [10, 20], 0, 90, 15, False, plot_time=-1) - check(3, [10, 20], 0, 180, 15, False, plot_time=-1) - check(3, [10, 20], 0, 270, 15, False, plot_time=-1) - check(3, [10, 20], 0, 360, 15, False, plot_time=-1) - check(3, [10, 20], 0, 0, 15, False, plot_time=-1) - - check(3, [0, 10], 0, 90, 15, True, plot_time=-1) - check(3, [0, 10], 0, 180, 15, True, plot_time=-1) - check(3, [0, 10], 0, 270, 15, True, plot_time=-1) - check(3, [0, 10], 0, 360, 15, True, plot_time=-1) - check(3, [0, 10], 0, 0, 15, True, plot_time=-1) - - check(3, [0, 10, 20], 0, 90, 15, False, plot_time=-1) - check(3, [0, 10, 20], 0, 180, 15, False, plot_time=-1) - check(3, [0, 10, 20], 0, 270, 15, False, plot_time=-1) - check(3, [0, 10, 20], 0, 360, 15, False, plot_time=-1) - check(3, [0, 10, 20], 0, 0, 15, False, plot_time=-1) - - check(3, [10, 20, 30], 0, 90, 15, False, plot_time=-1) - check(3, [10, 20, 30], 0, 180, 15, False, plot_time=-1) - check(3, [10, 20, 30], 0, 270, 15, False, plot_time=-1) - check(3, [10, 20, 30], 0, 360, 15, False, plot_time=-1) - check(3, [10, 20, 30], 0, 0, 15, False, plot_time=-1) - - check(3, [0, 10, 20], 0, 90, 15, True, plot_time=-1) - check(3, [0, 10, 20], 0, 180, 15, True, plot_time=-1) - check(3, [0, 10, 20], 0, 270, 15, True, plot_time=-1) - check(3, [0, 10, 20], 0, 360, 15, True, plot_time=-1) - check(3, [0, 10, 20], 0, 0, 15, True, plot_time=-1) - - check(3, [0, 10, 20, 30, 40, 50, 60, 70], 0, 90, 15, False, plot_time=-1) - check(3, [0, 10, 20, 30, 40, 50, 60, 70], 0, 180, 15, False, plot_time=-1) - check(3, [0, 10, 20, 30, 40, 50, 60, 70], 0, 270, 15, False, plot_time=-1) - check(3, [0, 10, 20, 30, 40, 50, 60, 70], 0, 360, 15, False, plot_time=-1) - check(3, [0, 10, 20, 30, 40, 50, 60, 70], 0, 0, 15, False, plot_time=-1) - - check(3, [10, 20, 30, 40, 50, 60, 70], 0, 90, 15, False, plot_time=-1) - check(3, [10, 20, 30, 40, 50, 60, 70], 0, 180, 15, False, plot_time=-1) - check(3, [10, 20, 30, 40, 50, 60, 70], 0, 270, 15, False, plot_time=-1) - check(3, [10, 20, 30, 40, 50, 60, 70], 0, 360, 15, False, plot_time=-1) - check(3, [10, 20, 30, 40, 50, 60, 70], 0, 0, 15, False, plot_time=-1) - - check(3, [0, 10, 20, 30, 40, 50, 60, 70], 0, 90, 15, True, plot_time=-1) - check(3, [0, 10, 20, 30, 40, 50, 60, 70], 0, 180, 15, True, plot_time=-1) - check(3, [0, 10, 20, 30, 40, 50, 60, 70], 0, 270, 15, True, plot_time=-1) - check(3, [0, 10, 20, 30, 40, 50, 60, 70], 0, 360, 15, True, plot_time=-1) - check(3, [0, 10, 20, 30, 40, 50, 60, 70], 0, 0, 15, True, plot_time=-1) - - # Non-Plot Checks NCOL ------------------------------------------------- - - check(3, [0, 10], 0, 90, -15, False, plot_time=-1) - check(3, [0, 10], 0, 180, -15, False, plot_time=-1) - check(3, [0, 10], 0, 270, -15, False, plot_time=-1) - check(3, [0, 10], 0, 360, -15, False, plot_time=-1) - check(3, [0, 10], 0, 0, -15, False, plot_time=-1) - - check(3, [10, 20], 0, 90, -15, False, plot_time=-1) - check(3, [10, 20], 0, 180, -15, False, plot_time=-1) - check(3, [10, 20], 0, 270, -15, False, plot_time=-1) - check(3, [10, 20], 0, 360, -15, False, plot_time=-1) - check(3, [10, 20], 0, 0, -15, False, plot_time=-1) - - check(3, [0, 10], 0, 90, -15, True, plot_time=-1) - check(3, [0, 10], 0, 180, -15, True, plot_time=-1) - check(3, [0, 10], 0, 270, -15, True, plot_time=-1) - check(3, [0, 10], 0, 360, -15, True, plot_time=-1) - check(3, [0, 10], 0, 0, -15, True, plot_time=-1) - - check(3, [0, 10, 20], 0, 90, -15, False, plot_time=-1) - check(3, [0, 10, 20], 0, 180, -15, False, plot_time=-1) - check(3, [0, 10, 20], 0, 270, -15, False, plot_time=-1) - check(3, [0, 10, 20], 0, 360, -15, False, plot_time=-1) - check(3, [0, 10, 20], 0, 0, -15, False, plot_time=-1) - - check(3, [10, 20, 30], 0, 90, -15, False, plot_time=-1) - check(3, [10, 20, 30], 0, 180, -15, False, plot_time=-1) - check(3, [10, 20, 30], 0, 270, -15, False, plot_time=-1) - check(3, [10, 20, 30], 0, 360, -15, False, plot_time=-1) - check(3, [10, 20, 30], 0, 0, -15, False, plot_time=-1) - - check(3, [0, 10, 20], 0, 90, -15, True, plot_time=-1) - check(3, [0, 10, 20], 0, 180, -15, True, plot_time=-1) - check(3, [0, 10, 20], 0, 270, -15, True, plot_time=-1) - check(3, [0, 10, 20], 0, 360, -15, True, plot_time=-1) - check(3, [0, 10, 20], 0, 0, -15, True, plot_time=-1) - - check(3, [0, 10, 20, 30, 40, 50, 60, 70], 0, 90, -15, False, plot_time=-1) - check(3, [0, 10, 20, 30, 40, 50, 60, 70], 0, 180, -15, False, plot_time=-1) - check(3, [0, 10, 20, 30, 40, 50, 60, 70], 0, 270, -15, False, plot_time=-1) - check(3, [0, 10, 20, 30, 40, 50, 60, 70], 0, 360, -15, False, plot_time=-1) - check(3, [0, 10, 20, 30, 40, 50, 60, 70], 0, 0, -15, False, plot_time=-1) - - check(3, [10, 20, 30, 40, 50, 60, 70], 0, 90, -15, False, plot_time=-1) - check(3, [10, 20, 30, 40, 50, 60, 70], 0, 180, -15, False, plot_time=-1) - check(3, [10, 20, 30, 40, 50, 60, 70], 0, 270, -15, False, plot_time=-1) - check(3, [10, 20, 30, 40, 50, 60, 70], 0, 360, -15, False, plot_time=-1) - check(3, [10, 20, 30, 40, 50, 60, 70], 0, 0, -15, False, plot_time=-1) - - check(3, [0, 10, 20, 30, 40, 50, 60, 70], 0, 90, -15, True, plot_time=-1) - check(3, [0, 10, 20, 30, 40, 50, 60, 70], 0, 180, -15, True, plot_time=-1) - check(3, [0, 10, 20, 30, 40, 50, 60, 70], 0, 270, -15, True, plot_time=-1) - check(3, [0, 10, 20, 30, 40, 50, 60, 70], 0, 360, -15, True, plot_time=-1) - check(3, [0, 10, 20, 30, 40, 50, 60, 70], 0, 0, -15, True, plot_time=-1) - - # Non-Plot Checks Extreme Points --------------------------------------- - - check(3, [0, 10], 0, 90, -1, False, plot_time=-1) - check(3, [0, 10], 0, 180, -1, False, plot_time=-1) - check(3, [0, 10], 0, 270, -1, False, plot_time=-1) - check(3, [0, 10], 0, 360, -1, False, plot_time=-1) - check(3, [0, 10], 0, 0, -1, False, plot_time=-1) - - check(3, [10, 20], 0, 90, -1, False, plot_time=-1) - check(3, [10, 20], 0, 180, -1, False, plot_time=-1) - check(3, [10, 20], 0, 270, -1, False, plot_time=-1) - check(3, [10, 20], 0, 360, -1, False, plot_time=-1) - check(3, [10, 20], 0, 0, -1, False, plot_time=-1) - - check(3, [0, 10], 0, 90, -1, True, plot_time=-1) - check(3, [0, 10], 0, 180, -1, True, plot_time=-1) - check(3, [0, 10], 0, 270, -1, True, plot_time=-1) - check(3, [0, 10], 0, 360, -1, True, plot_time=-1) - check(3, [0, 10], 0, 0, -1, True, plot_time=-1) - - check(3, [0, 10, 20], 0, 90, -1, False, plot_time=-1) - check(3, [0, 10, 20], 0, 180, -1, False, plot_time=-1) - check(3, [0, 10, 20], 0, 270, -1, False, plot_time=-1) - check(3, [0, 10, 20], 0, 360, -1, False, plot_time=-1) - check(3, [0, 10, 20], 0, 0, -1, False, plot_time=-1) - - check(3, [10, 20, 30], 0, 90, -1, False, plot_time=-1) - check(3, [10, 20, 30], 0, 180, -1, False, plot_time=-1) - check(3, [10, 20, 30], 0, 270, -1, False, plot_time=-1) - check(3, [10, 20, 30], 0, 360, -1, False, plot_time=-1) - check(3, [10, 20, 30], 0, 0, -1, False, plot_time=-1) - - check(3, [0, 10, 20], 0, 90, -1, True, plot_time=-1) - check(3, [0, 10, 20], 0, 180, -1, True, plot_time=-1) - check(3, [0, 10, 20], 0, 270, -1, True, plot_time=-1) - check(3, [0, 10, 20], 0, 360, -1, True, plot_time=-1) - check(3, [0, 10, 20], 0, 0, -1, True, plot_time=-1) - - check(3, [0, 10, 20, 30, 40, 50, 60, 70], 0, 90, 5, False, plot_time=-1) - check(3, [0, 10, 20, 30, 40, 50, 60, 70], 0, 180, 5, False, plot_time=-1) - check(3, [0, 10, 20, 30, 40, 50, 60, 70], 0, 270, 5, False, plot_time=-1) - check(3, [0, 10, 20, 30, 40, 50, 60, 70], 0, 360, 5, False, plot_time=-1) - check(3, [0, 10, 20, 30, 40, 50, 60, 70], 0, 0, 5, False, plot_time=-1) - - check(3, [10, 20, 30, 40, 50, 60, 70], 0, 90, 5, False, plot_time=-1) - check(3, [10, 20, 30, 40, 50, 60, 70], 0, 180, 5, False, plot_time=-1) - check(3, [10, 20, 30, 40, 50, 60, 70], 0, 270, 5, False, plot_time=-1) - check(3, [10, 20, 30, 40, 50, 60, 70], 0, 360, 5, False, plot_time=-1) - check(3, [10, 20, 30, 40, 50, 60, 70], 0, 0, 5, False, plot_time=-1) - - check(3, [0, 10, 20, 30, 40, 50, 60, 70], 0, 90, 5, True, plot_time=-1) - check(3, [0, 10, 20, 30, 40, 50, 60, 70], 0, 180, 5, True, plot_time=-1) - check(3, [0, 10, 20, 30, 40, 50, 60, 70], 0, 270, 5, True, plot_time=-1) - check(3, [0, 10, 20, 30, 40, 50, 60, 70], 0, 360, 5, True, plot_time=-1) - check(3, [0, 10, 20, 30, 40, 50, 60, 70], 0, 0, 5, True, plot_time=-1) - - # Plot Checking --------------------------------------------------------- - - check(3, [0, 10, 20, 30, 40, 50], 0, 90, 15, False, plot_time=0.1) - check(3, [0, 10, 20, 30, 40, 50], 0, 180, 15, False, plot_time=0.1) - check(3, [0, 10, 20, 30, 40, 50], 0, 270, 15, False, plot_time=0.1) - check(3, [0, 10, 20, 30, 40, 50], 0, 360, 15, False, plot_time=0.1) - check(3, [0, 10, 20, 30, 40, 50], 0, 0, 15, False, plot_time=-1) - - check(3, [10, 20, 30, 40, 50], 0, 90, 15, False, plot_time=0.1) - check(3, [10, 20, 30, 40, 50], 0, 180, 15, False, plot_time=0.1) - check(3, [10, 20, 30, 40, 50], 0, 270, 15, False, plot_time=0.1) - check(3, [10, 20, 30, 40, 50], 0, 360, 15, False, plot_time=0.1) - check(3, [10, 20, 30, 40, 50], 0, 0, 15, False, plot_time=-1) - - check(3, [0, 10, 20, 30, 40, 50], 0, 90, 15, True, plot_time=0.1) - check(3, [0, 10, 20, 30, 40, 50], 0, 180, 15, True, plot_time=0.1) - check(3, [0, 10, 20, 30, 40, 50], 0, 270, 15, True, plot_time=0.1) - check(3, [0, 10, 20, 30, 40, 50], 0, 360, 15, True, plot_time=0.1) - check(3, [0, 10, 20, 30, 40, 50], 0, 0, 15, True, plot_time=-1) diff --git a/common/DisvGridMerger.py b/common/DisvGridMerger.py deleted file mode 100644 index 039162f4e..000000000 --- a/common/DisvGridMerger.py +++ /dev/null @@ -1,761 +0,0 @@ -from __future__ import annotations - -from math import sqrt -import copy - -import matplotlib.path as mpltPath -import numpy as np - -from DisvPropertyContainer import DisvPropertyContainer - -__all__ = ["DisvGridMerger"] - - -class DisvGridMerger: - """ - Class for merging, non-overlapping, MODFLOW 6 DISV grids. The merge is - made by selecting a connection point and adjusting the (x,y) coordinates - of one of the grids. The grid connection is made by starting with the first - grid, called `__main__`, then adjusting the second grid to have __main__ - incorporate both grids. After that, subsequent grids are snapped to the - __main__ grid to form the final merged grid. - - When a grid is shifted to snap to __main__ (`snap_vertices`), any vertices - that are in proximity of the __main__ grid are merged (that is, the - snapped grid drops the overlapping vertices and uses the existing __main__ - ones). Proximately is determined by having an x or y distance less than - `connect_tolerance`. - - Vertices can also be forced to snap to the __main__ grid with `force_snap`. - A force snap occurs after the second grid is shifted and snapped to - __main__. The force snap drops the existing vertex and uses the forced one - changing the shape of the cell. Note, if any existing vertices are located - within the new shape of the cell, then they are added to the cell2d vertex - list. - - Examples - -------- - >>> # Example snaps two rectangular structured vertex grids. - >>> # The first grid has 2 rows and 2 columns; - >>> # the second grid has 3 rows and 2 columns. - >>> # DisvStructuredGridBuilder builds a DisvPropertyContainer object - >>> # that contains a structured vertex grid (rows and columns). - >>> from DisvStructuredGridBuilder import DisvStructuredGridBuilder - >>> - >>> grid1 = DisvStructuredGridBuilder(nlay=1, nrow=2, ncol=2) - >>> grid2 = DisvStructuredGridBuilder(nlay=1, nrow=3, ncol=2) - >>> - >>> # Optional step to see what vertex point to use - >>> grid1.plot_grid() # Plot and view vertex locations - >>> grid2.plot_grid() # to identify connection points. - >>> - >>> # Steps to merge grid1 and grid2 - >>> mg = DisvGridMerger() # init the object - >>> - >>> mg.add_grid("grid1", grid1) # add grid1 - >>> mg.add_grid("grid2", grid2) # add grid2 - >>> - >>> # Snap grid1 upper right corner (vertex 3) to grid2 upper left - >>> # corner (vertex 1). Note the vertices must be zero-based indexed. - >>> mg.set_vertex_connection("grid1", "grid2", 3 - 1, 1 - 1) - >>> - >>> # Grids do not require any force snapping because overlapping vertices - >>> # will be within the connect_tolerance. Otherwise, now would be - >>> # when to run the set_force_vertex_connection method. - >>> # Merge the grids - >>> mg.merge_grids() - >>> - >>> mg.merged.plot_grid() # plot the merged grid - - Attributes - ---------- - grids : dict - A dictionary containing names of individual grids as keys and - corresponding `DisvPropertyContainer` objects as values. - The key `__main__` is used to refer to the final merged grid and is not - allowed as a name for any `DisvPropertyContainer` object. - merged : DisvPropertyContainer - A `DisvPropertyContainer` object representing the merged grid. - snap_vertices : dict - A dictionary of vertex connections to be snapped during - the merging process. This attribute is set with the - ``set_vertex_connection`` method. The key is ``(name1, name2)`` and - value is ``(vertex1, vertex2)``, where name1 and name2 correspond with - keys from `grids` and ``vertex1`` and ``vertex2`` are the connection - vertices for ``name1`` and ``name2``, respectively. - connect_tolerance : dict - A dictionary specifying the tolerance distance for vertex snapping. - After a grid is snapped to __main__ via snap_vertices, any vertices - that overlap within an x or y length of connect_tolerance are merged. - snap_order : list - A list of grid pairs indicating the order in which grids - will be merged. This is variable is set after running the - `merge_grids` method. - force_snap : dict - A dictionary of vertex connections that must be snapped, - even if they don't satisfy the tolerance. The key is ``(name1, name2)`` - and value is ``[[v1, ...], [v2, ...]]``, where ``name1`` and ``name2`` - correspond with keys from `grids` and ``v1`` is a list of verties to - snap from ``name1``, and ``v2`` is a list of vertices to snap to from - ``name2``. The first ``v1``, corresponds with the first ``v2``, - and so forth. - force_snap_drop : dict - A dictionary specifying which vertex to drop when force snapping. The - key is ``(name1, name2)`` and value is ``[v_drop, ...]``, where - ``v_drop`` is 1 to drop the vertex from ``name1``, and 2 to drop - from ``name2``. - force_snap_cellid : set - A set that lists all the merged grid cellids that had one or more - verties force snapped. This list is important for checking if the - new vertex list and cell center are correct. - vert2name : dict - A dictionary mapping the merged grid's vertex numbers to the - corresponding grid names and vertex indices. The key is the vertex - from the new merged grid and the value is - ``[[name, vertex_old], ...]``, where ``name`` is the original grid name - and ``vertex_old`` is its correspondnig vertex from name. - name2vert : dict - A dictionary mapping grid names and vertex indices to the merged - grid's vertices. The key is ``(name, vertex_old)``, where ``name`` is - the original grid name and ``vertex_old`` is its correspondnig vertex - from name. The value is the merged grid's vertex. - cell2name : dict - A dictionary mapping the merged grid's cellid's to the corresponding - original grid names and cellid's. The key is the merged grid's cellid - and value is ``(name, cellid_old)``, where ``name`` is the - original grid name and ``cellid_old`` is its correspondnig cellid from - name. - name2cell : dict - A dictionary mapping grid names and cellid's to the merged grid's - cellid's. The key is ``(name, cellid_old)``, where ``name`` is the - original grid name and ``cellid_old`` is its correspondnig cellid from - name, and value is the merged grid's cellid. - - Notes - ------- - The following is always true: - - ``cell2name[cell] == name2vert[cell2name[cell]]`` - - ``name2vert[(name, vertex)] is in vert2name[name2vert[(name, vertex)]]`` - - Methods - ------- - get_disv_kwargs(name="__main__") - Get the keyword arguments for creating a MODFLOW-6 DISV package for - a specified grid. - add_grid(name, grid) - Add an individual grid to the merger. - set_vertex_connection(name1, name2, vertex1, vertex2, autosnap_tolerance=1.0e-5) - Set a vertex connection between two grids for snapping. - set_force_vertex_connection(name1, name2, vertex1, vertex2, drop_vertex=2) - Force a vertex connection between two grids. - merge_grids() - Merge the specified grids based on the defined vertex connections. - plot_grid(name="__main__", ...) - Selects the grid specified by ``name`` and passes the remaining - kwargs to DisvPropertyContainer.plot_grid(...). - """ - - grids: dict[str, DisvPropertyContainer] - merged: DisvPropertyContainer - snap_vertices: dict - connect_tolerance: dict - snap_order: list - force_snap: dict - force_snap_drop: dict - force_snap_cellid: set - vert2name: dict - name2vert: dict - cell2name: dict - name2cell: dict - - def __init__(self): - self.grids = {} - self.merged = DisvPropertyContainer() - - self.snap_vertices = {} - self.connect_tolerance = {} - self.snap_order = [] - self.force_snap = {} - self.force_snap_drop = {} - self.force_snap_cellid = set() - - self.vert2name = {} # vertex: [[name, vertex], ...] - self.name2vert = {} # (name, vertex): vertex - - self.cell2name = {} # cellid: (name, cellid) - self.name2cell = {} # (name, cellid): cellid - - def get_disv_kwargs(self, name="__main__"): - return self.get_grid(name).get_disv_kwargs() - - def __repr__(self): - names = ", ".join(self.grids.keys()) - return f"DisvGridMerger({names})" - - def property_copy_to(self, DisvGridMergerType): - if isinstance(DisvGridMergerType, DisvGridMerger): - DisvGridMergerType.merged = self.merged.copy() - dcp = copy.deepcopy - - for name in self.grids: - DisvGridMergerType.grids[name] = self.grids[name].copy() - - for name in self.snap_vertices: - DisvGridMergerType.snap_vertices[name] = dcp( - self.snap_vertices[name] - ) - - for name in self.connect_tolerance: - DisvGridMergerType.connect_tolerance[ - name - ] = self.connect_tolerance[name] - - for name in self.force_snap: - DisvGridMergerType.force_snap[name] = dcp( - self.force_snap[name] - ) - - for name in self.force_snap_drop: - DisvGridMergerType.force_snap_drop[name] = dcp( - self.force_snap_drop[name] - ) - - DisvGridMergerType.force_snap_cellid = dcp(self.force_snap_cellid) - - for name in self.vert2name: - DisvGridMergerType.vert2name[name] = dcp(self.vert2name[name]) - - for name in self.cell2name: - DisvGridMergerType.cell2name[name] = dcp(self.cell2name[name]) - - for name in self.name2vert: - DisvGridMergerType.name2vert[name] = self.name2vert[name] - - for name in self.name2cell: - DisvGridMergerType.name2cell[name] = self.name2cell[name] - - DisvGridMergerType.snap_order = dcp(self.snap_order) - else: - raise RuntimeError( - "DisvGridMerger.property_copy_to " - "can only copy to objects that inherit " - "properties from DisvGridMerger" - ) - - def copy(self): - cp = DisvGridMerger() - self.property_copy_to(cp) - return cp - - def get_merged_cell2d(self, name, cell2d_orig): - return self.name2cell[(name, cell2d_orig)] - - def get_merged_vertex(self, name, vertex_orig): - return self.name2vert[(name, vertex_orig)] - - def get_grid(self, name="__main__"): - if name == "" or name == "__main__": - return self.merged - - if name not in self.grids: - raise KeyError( - "DisvGridMerger.get_grid: requested grid, " - f"{name} does not exist.\n" - "Current grids stored are:\n" - "\n".join(self.grids.keys()) - ) - - return self.grids[name] - - def add_grid(self, name, grid): - if name == "" or name == "__main__": - raise RuntimeError( - "\nDisvGridMerger.add_grid:\n" - 'name = "" or "__main__"\nis not allowed.' - ) - if isinstance(grid, DisvPropertyContainer): - grid = grid.copy() - else: - # grid = [nlay, vertices, cell2d, top, botm] - grid = DisvPropertyContainer(*grid) - - self.grids[name] = grid - - def set_vertex_connection( - self, name1, name2, vertex1, vertex2, autosnap_tolerance=1.0e-5 - ): - if (name2, name1) in self.snap_vertices: - name1, name2 = name2, name1 - vertex1, vertex2 = vertex2, vertex1 - - key1 = (name1, name2) - key2 = (name2, name1) - - self.snap_vertices[key1] = (vertex1, vertex2) - self.connect_tolerance[key1] = autosnap_tolerance - - self.force_snap[key1] = [[], []] - self.force_snap[key2] = [[], []] - self.force_snap_drop[key1] = [] - self.force_snap_drop[key2] = [] - - def set_force_vertex_connection( - self, name1, name2, vertex1, vertex2, drop_vertex=2 - ): - key1 = (name1, name2) - key2 = (name2, name1) - if key1 not in self.force_snap: - self.force_snap[key1] = [] - self.force_snap[key2] = [] - self.force_snap_drop[key1] = [] - self.force_snap_drop[key2] = [] - - drop_vertex_inv = 1 if drop_vertex == 2 else 2 - - self.force_snap[key1][0].append(vertex1) - self.force_snap[key1][1].append(vertex2) - - self.force_snap[key2][0].append(vertex2) - self.force_snap[key2][1].append(vertex1) - - self.force_snap_drop[key1].append(drop_vertex) - self.force_snap_drop[key2].append(drop_vertex_inv) - - def _get_vertex_xy(self, name, iv): - vertices = self.get_grid(name).vertices - for iv_orig, xv, yv in vertices: - if iv == iv_orig: - return xv, yv - raise RuntimeError( - "DisvGridMerger: " f"Failed to find vertex {iv} in grid {name}" - ) - - def _find_merged_vertex(self, xv, yv, tol): - for iv, xv_chk, yv_chk in self.merged.vertices: - if abs(xv - xv_chk) + abs(yv - yv_chk) < tol: - return iv - return None - - def _replace_vertex_xy(self, iv, xv, yv): - for vert in self.merged.vertices: - if iv == vert[0]: - vert[1] = xv - vert[2] = yv - return - raise RuntimeError( - "DisvGridMerger: Unknown code error - " - f"failed to locate vertex {iv}" - ) - - def _clear_attribute(self): - self.snap_order.clear() - self.force_snap_cellid.clear() - - self.merged.nlay = 0 - self.merged.nvert = 0 - self.merged.ncpl = 0 - self.merged.cell2d.clear() - self.merged.vertices.clear() - self.merged.top = np.array(()) - self.merged.botm.clear() - - self.cell2name.clear() - self.name2cell.clear() - self.vert2name.clear() - self.name2vert.clear() - - def _grid_snap_order(self): - # grids are snapped to one main grid using key = (name1, name2). - # it is required that at least name1 or name2 already be defined - # in the main grid. Function determines order to ensure this. - # snap_list -> List of (name1, name2) to parse - snap_list = list(self.snap_vertices.keys()) - name_used = {snap_list[0][0]} # First grid name always used first - snap_order = [] # Final order to build main grid - snap_append = [] # key's that need to be parsed again - loop_limit = 50 - loop = 0 - error_msg = ( - "\nDisvGridMerger:\n" - "Failed to find grid snap order.\n" - "Snapping must occur in contiguous steps " - "to the same main, merged grid.\n" - ) - while len(snap_list) > 0: - key = snap_list.pop(0) - name1, name2 = key - has_name1 = name1 in name_used - has_name2 = name2 in name_used - - if has_name1 and has_name2: # grid snapped to main twice - raise RuntimeError( - error_msg + "Once a grid name has been snapped to " - "the main grid,\n" - "it cannot be snapped again.\n" - f"The current snap order determined is:\n\n" - f"{snap_order}\n\n" - "but the following two grids were already " - "snapped to the main grid:\n" - f"{name1}\nand\n{name2}\n" - ) - - if has_name1 or has_name2: # have a name to snap too - snap_order.append(key) - name_used.add(name1) - name_used.add(name2) - else: # neither name found, so save for later - snap_append.append(key) - - if len(snap_list) == 0 and len(snap_append) > 0: - snap_list.extend(snap_append) - snap_append.clear() - loop += 1 - if loop > loop_limit: - raise RuntimeError( - error_msg + "Determined snap order for the " - "main grid is:\n\n" - f"{snap_order}\n\n" - "but failed to snap the following " - "to the main grid:\n\n" - f"{snap_list}" - ) - return snap_order - - def merge_grids(self): - self._clear_attribute() - - # First grid is unchanged, all other grids are changed as they - # snap to the final merged grid. - name1 = next(iter(self.snap_vertices.keys()))[0] - - cell2d = self.merged.cell2d - vertices = self.merged.vertices - - cell2d.extend(copy.deepcopy(self.grids[name1].cell2d)) - vertices.extend(copy.deepcopy(self.grids[name1].vertices)) - - for ic, *_ in cell2d: - self.cell2name[ic] = (name1, ic) - self.name2cell[(name1, ic)] = ic - - for iv, *_ in vertices: - self.vert2name[iv] = [(name1, iv)] - self.name2vert[(name1, iv)] = iv - - ic_new = ic # Last cell2d id from previous-previous loop - iv_new = iv # Last vertex id from previous loop - snapped = {name1} - force_snapped = set() - for key in self._grid_snap_order(): # Loop through vertices to snap - tol = self.connect_tolerance[key] - v1, v2 = self.snap_vertices[key] - name1, name2 = key - if name2 in snapped: - name1, name2 = name2, name1 - v1, v2 = v2, v1 - - if name1 not in self.snap_order: - self.snap_order.append(name1) - if name2 not in self.snap_order: - self.snap_order.append(name2) - - v1_orig = v1 - v1 = self.name2vert[(name1, v1_orig)] - - v1x, v1y = self._get_vertex_xy("__main__", v1) - v2x, v2y = self._get_vertex_xy(name2, v2) - - difx = v1x - v2x - dify = v1y - v2y - - for v2_orig, xv, yv in self.grids[name2].vertices: - xv += difx - yv += dify - - if ( - key in self.force_snap - and v2_orig in self.force_snap[key][1] # force snap vertex - ): - ind = self.force_snap[key][1].index(v2_orig) - v1_orig_force = self.force_snap[key][0][ind] - iv = self.name2vert[(name1, v1_orig_force)] - if self.force_snap_drop[key] == 1: - # replace v1 vertex with the x,y from v2 to force snap - self._replace_vertex_xy(iv, xv, yv) - force_snapped.add((name1, v1_orig_force)) - else: - force_snapped.add((name2, v2_orig)) - else: - iv = self._find_merged_vertex(xv, yv, tol) - - if iv is None: - iv_new += 1 - vertices.append([iv_new, xv, yv]) - self.vert2name[iv_new] = [(name2, v2_orig)] - self.name2vert[(name2, v2_orig)] = iv_new - else: - self.vert2name[iv].append((name2, v2_orig)) - self.name2vert[(name2, v2_orig)] = iv - - # Loop through cells and update center point and icvert - for ic, xc, yc, ncvert, *icvert in self.grids[name2].cell2d: - ic_new += 1 - xc += difx - yc += dify - - self.cell2name[ic_new] = (name2, ic) - self.name2cell[(name2, ic)] = ic_new - - item = [ic_new, xc, yc, ncvert] # append new icvert's - for iv_orig in icvert: - item.append(self.name2vert[(name2, iv_orig)]) # = iv_new - cell2d.append(item) - - # Force snapped cells need to update cell center and check for - # errors in the grid - for name, iv_orig in force_snapped: - for ic, _, _, _, *icvert in self.grids[name].cell2d: - if iv_orig in icvert: # cell was deformed by force snap - ic_new = self.name2cell[(name, ic)] - self.force_snap_cellid.add(ic_new) - - if len(self.force_snap_cellid) > 0: - dist = lambda v1, v2: sqrt((v2[1]-v1[1])**2 + (v2[2]-v1[2])**2) - mg = self.merged - vert_xy = np.array([(x, y) for _, x, y in mg.vertices]) - for ic in self.force_snap_cellid: - _, _, _, _, *vert = mg.cell2d[ic] - if len(vert) != len(set(vert)): # contains a duplicate vertex - seen = set() - seen_add = seen.add - vert = [v for v in vert if not (v in seen or seen_add(v))] - tmp = mg.cell2d[ic] - tmp[3] = len(vert) - mg.cell2d[ic] = tmp[:4] + vert - # check if vertices are within cell. - cell = [mg.vertices[iv][1:] for iv in vert] - path = mpltPath.Path(cell) - contain = np.where(path.contains_points(vert_xy))[0] - for iv in contain: - # find closest polyline - if iv in vert: - continue - vert_check = mg.vertices[iv] # vert_xy[iv, :] - d = np.inf - v_closest = -1 - for v in vert: - d2 = dist(vert_check, mg.vertices[v]) - if d > d2: - d = d2 - v_closest = v - ind = vert.index(v_closest) - if ind == len(vert) - 1: # Closest is at the end - d1 = dist(vert_check, mg.vertices[vert[0]]) - d2 = dist(vert_check, mg.vertices[vert[-2]]) - if d1 < d2: - ind = len(vert) - elif ind == 0: # Closest is at the start check end members - d1 = dist(vert_check, mg.vertices[vert[-1]]) - d2 = dist(vert_check, mg.vertices[vert[1]]) - if d2 < d1: - ind = 1 - else: - d1 = dist(vert_check, mg.vertices[vert[ind-1]]) - d2 = dist(vert_check, mg.vertices[vert[ind+1]]) - if d2 < d1: - ind += 1 - - # update cell2d for cell ic - vert.insert(ind, iv) - tmp = mg.cell2d[ic] - tmp[3] = len(vert) - # update cell center - tmp[1], tmp[2] = mg.get_centroid(vert) - mg.cell2d[ic] = tmp[:4] + vert - - self.merged.nvert = len(vertices) - self.merged.ncpl = len(cell2d) - - nlay = 0 - for name in self.snap_order: - if nlay < self.grids[name].nlay: - nlay = self.grids[name].nlay - self.merged.nlay = nlay - - top = [] - for name in self.snap_order: - top.extend(self.grids[name].top) - self.merged.top = np.array(top) - - for lay in range(nlay): - bot = [] - for name in self.snap_order: - if lay < self.grids[name].nlay: - bot.extend(self.grids[name].botm[lay]) - else: - bot.extend(self.grids[name].botm[-1]) - self.merged.botm.append(bot) - - def plot_grid( - self, - name="__main__", - title="", - plot_time=0.0, - show=True, - figsize=(10, 10), - dpi=None, - xlabel="", - ylabel="", - cell2d_override=None, - vertices_override=None, - ax_override=None, - cell_dot=True, - cell_num=True, - cell_dot_size=7.5, - vertex_dot=True, - vertex_num=True, - vertex_dot_size=6.5, - ): - return self.get_grid(name).plot_grid( - title, - plot_time, - show, - figsize, - dpi, - xlabel, - ylabel, - cell2d_override, - vertices_override, - ax_override, - cell_dot, - cell_num, - cell_dot_size, - vertex_dot, - vertex_num, - vertex_dot_size, - ) - - -if __name__ == "__main__": - from DisvCurvilinearBuilder import DisvCurvilinearBuilder - from DisvStructuredGridBuilder import DisvStructuredGridBuilder - - grid1 = DisvStructuredGridBuilder(nlay=1, nrow=2, ncol=2) - grid2 = DisvStructuredGridBuilder(nlay=1, nrow=3, ncol=2) - # Optional step to see what vertex point to use - grid1.plot_grid() # Plot and view vertex locations - grid2.plot_grid() # to identify connection points. - # Steps to merge grid1 and grid2 - mg = DisvGridMerger() # init the object - mg.add_grid("grid1", grid1) # add grid1 - mg.add_grid("grid2", grid2) # add grid2 - # Snap grid1 upper right corner (vertex 3) to grid2 upper left - # corner (vertex 1). Note the vertices must be zero-based indexed. - mg.set_vertex_connection("grid1", "grid2", 3 - 1, 1 - 1) - # Force snap the bottom left corner of grid2 to grid one. - # This will reshape that cell to be a triangle. - mg.set_force_vertex_connection("grid1", "grid2", 7-1, 10-1) - # Merge the grids - mg.merge_grids() - mg.merged.plot_grid() # plot the merged grid - - nlay = 1 # Number of layers - nradial = 5 # Number of radial direction cells (radial bands) - ncol = 5 # Number of columns in radial band (ncol) - - r_inner = 4 # Model inner radius ($ft$) - r_outer = 9 # Model outer radius ($ft$) - r_width = 1 # Model radial band width ($ft$) - - surface_elevation = 10.0 # Top of the model ($ft$) - - radii = np.arange(r_inner, r_outer + r_width, r_width) - - angle_start1 = 180 - angle_stop1 = 270 - angle_step1 = -ncol - - angle_start2 = 0 - angle_stop2 = 90 - angle_step2 = -ncol - - nrow = len(radii) - 1 - row_width = r_width - col_width = r_width - - curv1 = DisvCurvilinearBuilder( - nlay, - radii, - angle_start1, - angle_stop1, - angle_step1, - surface_elevation=surface_elevation, - layer_thickness=surface_elevation, - single_center_cell=False, - ) - - curv2 = DisvCurvilinearBuilder( - nlay, - radii, - angle_start2, - angle_stop2, - angle_step2, - surface_elevation=surface_elevation, - layer_thickness=surface_elevation, - single_center_cell=False, - ) - - rect = DisvStructuredGridBuilder( - nlay, - nrow, - ncol, - row_width, - col_width, - surface_elevation, - surface_elevation, - ) - - fig1, ax1 = curv1.plot_grid(show=False, dpi=150) - fig2, ax2 = rect.plot_grid(show=False, dpi=150) - fig3, ax3 = curv2.plot_grid(show=False, dpi=150) - - # fig1.savefig("fig1.png", dpi=600) - # fig2.savefig("fig2.png", dpi=600) - # fig3.savefig("fig3.png", dpi=600) - - gm = DisvGridMerger() - - gm.add_grid("curv1", curv1) - gm.add_grid("rect", rect) - gm.add_grid("curv2", curv2) - - gm.set_vertex_connection("curv1", "rect", 6 - 1, 1 - 1) - gm.set_vertex_connection("rect", "curv2", 6 - 1, 36 - 1) - - gm.merge_grids() - - fig4, ax4 = gm.plot_grid( - show=True, - dpi=150, - ) - - - # vertices = [[iv, xv, yv], ...] - # cell2d = [[ic, xc, yc, ncvert, icvert], ...] - - # cell2d_orig - # vertices_orig - # cell2d - # vertices - # cell2d_index - # vertices_index - # connect_tolerance: distance to autosnap verticies - # snap -> [(name1, name2)] = (vertex1, vertex2) - # force_snap -> [(name1, name2)] = (vertex1, vertex2, drop_vertex) - - # vert2name -> vertex: [[name, vertex], ...] - # name2vert -> (name, vertex): vertex - # cell2name -> cellid: (name, cellid) - # name2cell -> (name, cellid): cellid \ No newline at end of file diff --git a/common/DisvPropertyContainer.py b/common/DisvPropertyContainer.py deleted file mode 100644 index b628ec337..000000000 --- a/common/DisvPropertyContainer.py +++ /dev/null @@ -1,700 +0,0 @@ -from __future__ import annotations - -import copy -from itertools import cycle - -import matplotlib.pyplot as plt -import numpy as np - -__all__ = ["DisvPropertyContainer"] - - -class DisvPropertyContainer: - - """ - Dataclass that stores MODFLOW 6 DISV grid information. - - This is a base class that stores DISV **kwargs information used - by flopy for building a ``flopy.mf6.ModflowGwfdisv`` object. - - All indices are zero-based, but translated to one-base for the figures and - by flopy for use with MODFLOW 6. - - If no arguments are provided then an empty object is returned. - - Parameters - ---------- - nlay : int - Number of layers. - vertices : list[list[int, float, float]] - List of vertices structured as - ``[[iv, xv, vy], ...]`` - where - ``iv`` is the vertex index, - ``xv`` is the x-coordinate, and - ``yv`` is the y-coordinate. - cell2d : list[list[int, float, float, int, int...]] - List of MODFLOW 6 cells structured as - ```[[icell2d, xc, yc, ncvert, icvert], ...]``` - where - ``icell2d`` is the cell index, - ``xc`` is the x-coordinate for the cell center, - ``yc`` is the y-coordinate for the cell center, - ``ncvert`` is the number of vertices required to define the cell, - ``icvert`` is a list of vertex indices that define the cell, and - in clockwise order. - top : np.ndarray - Is the top elevation for each cell in the top model layer. - botm : list[np.ndarray] - List of bottom elevation by layer for all model cells. - origin_x : float, default=0.0 - X-coordinate of the origin used as the reference point for other - vertices. This is used for shift and rotate operations. - origin_y : float, default=0.0 - X-coordinate of the origin used as the reference point for other - vertices. This is used for shift and rotate operations. - rotation : float, default=0.0 - Rotation angle in degrees for the model grid. - shift_origin : bool, default=True - If True and `origin_x` or `origin_y` is non-zero, then all vertices are - shifted from an assumed (0.0, 0.0) origin to the (origin_x, origin_y) - location. - rotate_grid, default=True - If True and `rotation` is non-zero, then all vertices are rotated by - rotation degrees around (origin_x, origin_y). - - Attributes - ---------- - nlay : int - Number of layers. - ncpl : int - Number of cells per layer. - nvert : int - Number of vertices. - vertices : list[list] - List of vertices structured as ``[[iv, xv, vy], ...]`` - cell2d : list[list] - List of 2D cells structured as ```[[icell2d, xc, yc, ncvert, icvert], ...]``` - top : np.ndarray - Top elevation for each cell in the top model layer. - botm : list[np.ndarray] - List of bottom elevation by layer for all model cells. - origin_x : float - X-coordinate reference point used by grid. - origin_y : float - Y-coordinate reference point used by grid. - rotation : float - Rotation angle of grid about (origin_x, origin_y) - - Methods - ------- - get_disv_kwargs() - Get the keyword arguments for creating a MODFLOW-6 DISV package. - plot_grid(...) - Plot the model grid from `vertices` and `cell2d` attributes. - change_origin(new_x_origin, new_y_origin) - Change the origin of the grid. - rotate_grid(rotation) - Rotate the grid. - get_centroid(icvert, vertices=None) - Calculate the centroid of a cell given by list of vertices `icvert`. - copy() - Create and return a copy of the current object. - """ - - nlay: int - ncpl: int - nvert: int - vertices: list[list] # [[iv, xv, yv], ...] - cell2d: list[list] # [[ic, xc, yc, ncvert, icvert], ...] - top: np.ndarray - botm: list[np.ndarray] - origin_x: float - origin_y: float - rotation: float - - def __init__( - self, - nlay=-1, - vertices=None, - cell2d=None, - top=None, - botm=None, - origin_x=0.0, - origin_y=0.0, - rotation=0.0, - shift_origin=True, - rotate_grid=True, - ): - if nlay is None or nlay < 1: - self._init_empty() - return - - self.nlay = nlay - self.ncpl = len(cell2d) - self.nvert = len(vertices) - - self.vertices = [] if vertices is None else copy.deepcopy(vertices) - self.cell2d = [] if cell2d is None else copy.deepcopy(cell2d) - self.top = np.array([]) if top is None else copy.deepcopy(top) - self.botm = [] if botm is None else copy.deepcopy(botm) - - self.origin_x, self.origin_y, self.rotation = 0.0, 0.0, 0.0 - - if shift_origin: - if abs(origin_x) > 1.0e-30 or abs(origin_y) > 1.0e-30: - self.change_origin(origin_x, origin_y) - elif not shift_origin: - self.origin_x, self.origin_y = origin_x, origin_y - - if rotate_grid: - self.rotate_grid(rotation) - elif not shift_origin: - self.rotation = rotation - - def get_disv_kwargs(self): - """ - Get the dict of keyword arguments for creating a MODFLOW-6 DISV - package using ``flopy.mf6.ModflowGwfdisv``. - """ - return { - "nlay": self.nlay, - "ncpl": self.ncpl, - "top": self.top, - "botm": self.botm, - "nvert": self.nvert, - "vertices": self.vertices, - "cell2d": self.cell2d, - } - - def __repr__(self, cls="DisvPropertyContainer"): - return ( - f"{cls}(\n\n" - f"nlay={self.nlay}, ncpl={self.ncpl}, nvert={self.nvert}\n\n" - f"origin_x={self.origin_x}, origin_y={self.origin_y}, " - f"rotation={self.rotation}\n\n" - f"vertices =\n{self._string_repr(self.vertices)}\n\n" - f"cell2d =\n{self._string_repr(self.cell2d)}\n\n" - f"top =\n{self.top}\n\n" - f"botm =\n{self.botm}\n\n)" - ) - - def _init_empty(self): - self.nlay = 0 - self.ncpl = 0 - self.nvert = 0 - self.vertices = [] - self.cell2d = [] - self.top = np.array([]) - self.botm = [] - self.origin_x = 0.0 - self.origin_y = 0.0 - self.rotation = 0.0 - - def change_origin(self, new_x_origin, new_y_origin): - shift_x_origin = new_x_origin - self.origin_x - shift_y_origin = new_y_origin - self.origin_y - - self.shift_origin(shift_x_origin, shift_y_origin) - - def shift_origin(self, shift_x_origin, shift_y_origin): - if abs(shift_x_origin) > 1.0e-30 or abs(shift_y_origin) > 1.0e-30: - self.origin_x += shift_x_origin - self.origin_y += shift_y_origin - - for vert in self.vertices: - vert[1] += shift_x_origin - vert[2] += shift_y_origin - - for cell in self.cell2d: - cell[1] += shift_x_origin - cell[2] += shift_y_origin - - def rotate_grid(self, rotation): - """Rotate grid around origin_x, origin_y for given angle in degrees. - - References - ---------- - [1] https://en.wikipedia.org/wiki/Transformation_matrix#Rotation - - """ - # - if abs(rotation) > 1.0e-30: - self.rotation += rotation - - sin, cos = np.sin, np.cos - a = np.radians(rotation) - x0, y0 = self.origin_x, self.origin_y - # Steps to shift - # 0) Get x, y coordinate to be shifted - # 1) Shift coordinate's reference point to origin - # 2) Rotate around origin - # 3) Shift back to original reference point - for vert in self.vertices: - _, x, y = vert - x, y = x - x0, y - y0 - x, y = x * cos(a) - y * sin(a), x * sin(a) + y * cos(a) - vert[1] = x + x0 - vert[2] = y + y0 - - for cell in self.cell2d: - _, x, y, *_ = cell - x, y = x - x0, y - y0 - x, y = x * cos(a) - y * sin(a), x * sin(a) + y * cos(a) - cell[1] = x + x0 - cell[2] = y + y0 - - @staticmethod - def _string_repr(list_list, sep=",\n"): - dim = len(list_list) - s = [] - if dim == 0: - return "[]" - if dim < 7: - for elm in list_list: - s.append(repr(elm)) - else: - for it in range(3): - s.append(repr(list_list[it])) - s.append("...") - for it in range(-3, 0): - s.append(repr(list_list[it])) - return sep.join(s) - - def property_copy_to(self, DisvPropertyContainerType): - if isinstance(DisvPropertyContainerType, DisvPropertyContainer): - DisvPropertyContainerType.nlay = self.nlay - DisvPropertyContainerType.ncpl = self.ncpl - DisvPropertyContainerType.nvert = self.nvert - DisvPropertyContainerType.vertices = self.vertices - DisvPropertyContainerType.cell2d = self.cell2d - DisvPropertyContainerType.top = self.top - DisvPropertyContainerType.botm = self.botm - DisvPropertyContainerType.origin_x = self.origin_x - DisvPropertyContainerType.origin_y = self.origin_y - DisvPropertyContainerType.rotation = self.rotation - else: - raise RuntimeError( - "DisvPropertyContainer.property_copy_to " - "can only copy to objects that inherit " - "properties from DisvPropertyContainer" - ) - - def copy(self): - cp = DisvPropertyContainer() - self.property_copy_to(cp) - return cp - - def keys(self): - """ - Get the keys used by ``flopy.mf6.ModflowGwfdisv``. - - This method is only used to provide unpacking support for - `DisvPropertyContainer` objects and subclasses. - - That is: - ``flopy.mf6.ModflowGwfdisv(gwf, **DisvPropertyContainer)`` - - Returns - ------- - list - List of keys used by ``flopy.mf6.ModflowGwfdisv`` - - """ - return self.get_disv_kwargs().keys() - - def __getitem__(self, k): - if hasattr(self, k): - return getattr(self, k) - raise KeyError(f"{k}") - - @staticmethod - def _get_array(cls_name, var, rep, rep2=None): - if rep2 is None: - rep2 = rep - - try: - dim = len(var) - except TypeError: - dim, var = 1, [var] - - try: # Check of array needs to be flattened - _ = len(var[0]) - tmp = [] - for row in var: - tmp.extend(row) - var = tmp - dim = len(var) - except TypeError: - pass - - if dim != 1 and dim != rep and dim != rep2: - msg = f"{cls_name}(var): var must be a scalar " - msg += f"or have len(var)=={rep}" - if rep2 != rep: - msg += f"or have len(var)=={rep2}" - raise IndexError(msg) - - if dim == 1: - return np.full(rep, var[0], dtype=np.float64) - else: - return np.array(var, dtype=np.float64) - - def get_centroid(self, icvert, vertices=None): - """ - Calculate the centroid of a cell for a given set of vertices. - - Parameters - ---------- - icvert : list[int] - List of vertex indices for the cell. - vertices : list[list], optional - List of vertices that `icvert` references to define the cell. - If not present, then the `vertices` attribute is used. - - Returns - ------- - tuple - A tuple containing the X and Y coordinates of the centroid. - - References - ---------- - [1] https://en.wikipedia.org/wiki/Centroid#Of_a_polygon - """ - if vertices is None: - vertices = self.vertices - - nv = len(icvert) - x = [] - y = [] - for iv in icvert: - x.append(vertices[iv][1]) - y.append(vertices[iv][2]) - - if nv < 3: - raise RuntimeError("get_centroid: len(icvert) < 3") - - if nv == 3: # Triangle - return sum(x) / 3, sum(y) / 3 - - xc, yc = 0.0, 0.0 - signedArea = 0.0 - for i in range(nv - 1): - x0, y0, x1, y1 = x[i], y[i], x[i + 1], y[i + 1] - a = x0 * y1 - x1 * y0 - signedArea += a - xc += (x0 + x1) * a - yc += (y0 + y1) * a - - x0, y0, x1, y1 = x1, y1, x[0], y[0] - - a = x0 * y1 - x1 * y0 - signedArea += a - xc += (x0 + x1) * a - yc += (y0 + y1) * a - - signedArea *= 0.5 - return xc / (6 * signedArea), yc / (6 * signedArea) - - def plot_grid( - self, - title="", - plot_time=0.0, - show=True, - figsize=(10, 10), - dpi=None, - xlabel="", - ylabel="", - cell2d_override=None, - vertices_override=None, - ax_override=None, - cell_dot=True, - cell_num=True, - cell_dot_size=7.5, - cell_dot_color="coral", - vertex_dot=True, - vertex_num=True, - vertex_dot_size=6.5, - vertex_dot_color="skyblue", - grid_color="grey", - ): - """ - Plot the model grid with optional features. - All inputs are optional. - - Parameters - ---------- - title : str, default="" - Title for the plot. - plot_time : float, default=0.0 - Time interval for animation (if greater than 0). - show : bool, default=True - Whether to display the plot. If false, then plot_time is set to -1 - and function returns figure and axis objects. - figsize : tuple, default=(10, 10) - Figure size (width, height) in inches. Default is (10, 10). - dpi : float, default=None - Set dpi for Matplotlib figure. - If set to None, then uses Matplotlib default dpi. - xlabel : str, default="" - X-axis label. - ylabel : str, default="" - Y-axis label. - cell2d_override : list[list], optional - List of ``cell2d`` cells to override the object's cell2d. - Default is None. - vertices_override : list[list], optional - List of vertices to override the object's vertices. - Default is None. - ax_override : matplotlib.axes.Axes, optional - Matplotlib axis object to use for generating plot instead of - making a new figure and axis objects. If present, then show is - set to False and plot_time to -1. - cell_dot : bool, default=True - Whether to add a filled circle at the cell center locations. - cell_num : bool, default=True - Whether to label cells with cell2d index numbers. - cell_dot_size : bool, default=7.5 - The size, in points, of the filled circles and index numbers - at the cell center. - cell_dot_color : str, default="coral" - The color of the filled circles at the cell center. - vertex_num : bool, default=True - Whether to label vertices with the vertex numbers. - vertex_dot : bool, default=True - Whether to add a filled circle at the vertex locations. - vertex_dot_size : bool, default=6.5 - The size, in points, of the filled circles and index numbers - at the vertex locations. - vertex_dot_color : str, default="skyblue" - The color of the filled circles at the vertex locations. - grid_color : str or tuple[str], default="grey" - The color of the grid lines. - If plot_time > 0, then animation cycled through the colors - for each cell outline. - - Returns - ------- - (matplotlib.figure.Figure, matplotlib.axis.Axis) or None - If `show` is False, returns the Figure and Axis objects; - otherwise, returns None. - - Raises - ------ - RuntimeError - If either `cell2d_override` or `vertices_override` is provided - without the other. - - Notes - ----- - This method plots the grid using Matplotlib. It can label cells and - vertices with numbers, show an animation of the plotting process, and - customize the plot appearance. - - Note that figure size (`figsize`) is in inches and the total pixels is based on - the `dpi` (dots per inch). For example, figsize=(3, 5) and - dpi=110, results in a figure resolution of (330, 550). - - Changing `figsize` does not effect the size - - Elements (text, markers, lines) in Matplotlib use - 72 points per inch (ppi) as a basis for translating to dpi. - Any changes to dpi result in a scaling effect. The default dpi of 100 - results in a line width 100/72 pixels wide. - - Similarly, a line width of 1 point with a dpi set to 72 results in - a line that is 1 pixel wide. The following then occurs: - - 2*72 dpi results in a line width 2 pixels; - 3*72 dpi results in a line width 3 pixels; and - 600 dpi results in a line width 600/72 pixels. - - Conversely, changing `figsize` increases the total pixel count, but - elements maintain the same dpi. That is the figure wireframe will be - larger, but the elements will have the same pixel widths. For example, - a line width of 1 will have a width of 100/72 in for any figure size, - as long as the dpi is set to 100. - """ - - if cell2d_override is not None and vertices_override is not None: - cell2d = cell2d_override - vertices = vertices_override - elif cell2d_override is not None or vertices_override is not None: - raise RuntimeError( - "plot_vertex_grid: if you specify " - "cell2d_override or vertices_override, " - "then you must specify both." - ) - else: - cell2d = self.cell2d - vertices = self.vertices - - if ax_override is None: - fig = plt.figure(figsize=figsize, dpi=dpi) - ax = fig.add_subplot() - else: - show = False - ax = ax_override - - ax.set_aspect("equal", adjustable="box") - - if not show: - plot_time = -1.0 - - if not isinstance(grid_color, tuple): - grid_color = (grid_color,) - - ColorCycler = grid_color - if plot_time > 0.0 and grid_color == ("grey",): - ColorCycler = ("green", "red", "grey", "magenta", "cyan", "yellow") - ColorCycle = cycle(ColorCycler) - - xvert = [] - yvert = [] - for r in vertices: - xvert.append(r[1]) - yvert.append(r[2]) - xcell = [] - ycell = [] - for r in cell2d: - xcell.append(r[1]) - ycell.append(r[2]) - - if title != "": - ax.set_title(title) - if xlabel != "": - ax.set_xlabel(xlabel) - if ylabel != "": - ax.set_ylabel(ylabel) - - vert_size = vertex_dot_size - cell_size = cell_dot_size - - if vertex_dot: - ax.plot( - xvert, - yvert, - linestyle="None", - color=vertex_dot_color, - markersize=vert_size, - marker="o", - markeredgewidth=0.0, - zorder=2, - ) - if cell_dot: - ax.plot( - xcell, - ycell, - linestyle="None", - color=cell_dot_color, - markersize=cell_size, - marker="o", - markeredgewidth=0.0, - zorder=2, - ) - - if cell_num: - for ic, xc, yc, *_ in cell2d: - ax.text( - xc, - yc, - f"{ic + 1}", - fontsize=cell_size, - color="black", - fontfamily="Arial Narrow", - fontweight="black", - rasterized=False, - horizontalalignment="center", - verticalalignment="center", - zorder=3, - ) - - if vertex_num: - for iv, xv, yv in vertices: - ax.text( - xv, - yv, - f"{iv + 1}", - fontsize=vert_size, - fontweight="black", - rasterized=False, - color="black", - fontfamily="Arial Narrow", - horizontalalignment="center", - verticalalignment="center", - zorder=3, - ) - - if plot_time > 0: - plt.show(block=False) - plt.pause(5 * plot_time) - - for ic, xc, yc, ncon, *vert in cell2d: - color = next(ColorCycle) - - conn = vert + [vert[0]] # Extra node to complete polygon - - for i in range(ncon): - n1, n2 = conn[i], conn[i + 1] - px = [vertices[n1][1], vertices[n2][1]] - py = [vertices[n1][2], vertices[n2][2]] - ax.plot(px, py, color=color, zorder=1) - - if plot_time > 0: - plt.draw() - plt.pause(plot_time) - - if show: - plt.show() - elif ax_override is None: - return fig, ax - - -if __name__ == "__main__": - # 2 by 3 example - vertices = [ - [0, 0.0, 0.0], - [1, 10.0, 0.0], - [2, 20.0, 0.0], - [3, 30.0, 0.0], - [4, 0.0, 10.0], - [5, 10.0, 10.0], - [6, 20.0, 10.0], - [7, 30.0, 10.0], - [8, 0.0, 20.0], - [9, 10.0, 20.0], - [10, 20.0, 20.0], - [11, 30.0, 20.0], - ] - - cell2d = [ - [0, 5.0, 5.0, 4, 4, 5, 1, 0], - [1, 15.0, 5.0, 4, 5, 6, 2, 1], - [2, 25.0, 5.0, 4, 6, 7, 3, 2], - [3, 5.0, 15.0, 4, 8, 9, 5, 4], - [4, 15.0, 15.0, 4, 9, 10, 6, 5], - [5, 25.0, 15.0, 4, 10, 11, 7, 6], - ] - - top = np.array([100.0, 100.0, 100.0, 100.0, 100.0, 100.0]) - botm = np.array([0.0, 0.0, 0.0, 0.0, 0.0, 0.0]) - - test = DisvPropertyContainer( - nlay=1, - vertices=vertices, - cell2d=cell2d, - top=top, - botm=botm, - origin_x=10.0, - origin_y=10.0, - rotation=30.0, - shift_origin=True, - rotate_grid=True, - ) - - fig, ax = test.plot_grid(show=False) - - test_empty = DisvPropertyContainer() - test_copy = test.copy() - - pass diff --git a/common/DisvStructuredGridBuilder.py b/common/DisvStructuredGridBuilder.py deleted file mode 100644 index e603f6bea..000000000 --- a/common/DisvStructuredGridBuilder.py +++ /dev/null @@ -1,346 +0,0 @@ -import numpy as np -from DisvPropertyContainer import DisvPropertyContainer - -__all__ = ["DisvStructuredGridBuilder"] - - -class DisvStructuredGridBuilder(DisvPropertyContainer): - """ - A class for generating a structured MODFLOW 6 DISV grid. - - This class inherits from the `DisvPropertyContainer` class and provides - methods to generate a rectangular, structured grid give the number rows - (nrow), columns (ncol), row widths, and columns widths. Rows are - discretized along the y-axis and columns along the x-axis. The row, column - structure follows MODFLOW 6 structured grids. That is, the first row has - the largest y-axis vertices and last row the lowest; and the first column - has the lowest x-axis vertices and last column the highest. - - All indices are zero-based, but translated to one-base for the figures and - by flopy for use with MODFLOW 6. - - The following shows the placement for (row, column) pairs - in a nrow=3 and ncol=5 model: - - ``(0,0) (0,1) (0,2) (0,3) (0,4)`` - ``(1,0) (1,1) (1,2) (1,3) (1,4)`` - ``(2,0) (2,1) (2,2) (2,3) (2,4)`` - - Array-like structures that are multidimensional (has rows and columns) - are flatten by concatenating each row. Using the previous example, - the following is the flattened representation: - - ``(0,0) (0,1) (0,2) (0,3) (0,4) (1,0) (1,1) (1,2) (1,3) (1,4) (2,0) (2,1) (2,2) (2,3) (2,4)`` - - If no arguments are provided then an empty object is returned. - - Parameters - ---------- - nlay : int - Number of layers - nrow : int - Number of rows (y-direction cells). - ncol : int - Number of columns (x-direction cells). - row_width : float or array_like - Width of y-direction cells (each row). If a single value is provided, - it will be used for all rows. Otherwise, it must be array_like - of length ncol. - col_width : float or array_like - Width of x-direction cells (each column). If a single value is - provided, it will be used for all columns. Otherwise, it must be - array_like of length ncol. - surface_elevation : float or array_like - Surface elevation for the top layer. Can either be a single float - for the entire `top`, or array_like of length `ncpl`. - If it is a multidimensional array_like, then it is flattened to a - single dimension along the rows (first dimension). - layer_thickness : float or array_like - Thickness of each layer. Can either be a single float - for model cells, or array_like of length `nlay`, or - array_like of length `nlay`*`ncpl`. - origin_x : float, default=0.0 - X-coordinate reference point the lower-left corner of the model grid. - That is, the outermost corner of ``row=nrow-1` and `col=0`. - Rotations are performed around this point. - origin_y : float, default=0.0 - Y-coordinate reference point the lower-left corner of the model grid. - That is, the outermost corner of ``row=nrow-1` and `col=0`. - Rotations are performed around this point. - rotation : float, default=0.0 - Rotation angle in degrees for the model grid around (origin_x, origin_y). - - Attributes - ---------- - nrow : int - Number of rows in the grid. - ncol : int - Number of columns in the grid. - row_width : np.ndarray - Width of y-direction cells (each row). - col_width : np.ndarray - Width of x-direction cells (each column). - - Methods - ------- - get_disv_kwargs() - Get the keyword arguments for creating a MODFLOW-6 DISV package. - plot_grid(...) - Plot the model grid from `vertices` and `cell2d` attributes. - get_cellid(row, col, col_check=True) - Get the cellid given the row and column indices. - get_row_col(cellid) - Get the row and column indices given the cellid. - get_vertices(row, col) - Get the vertex indices for a cell given the row and column indices. - iter_row_col() - Iterate over row and column indices for each cell. - iter_row_cellid(row) - Iterate over cellid's in a specific row. - iter_column_cellid(col) - Iterate over cellid's in a specific column. - """ - - nrow: int - ncol: int - row_width: np.ndarray - col_width: np.ndarray - - def __init__( - self, - nlay=-1, - nrow=-1, # number of Y direction cells - ncol=-1, # number of X direction cells - row_width=10.0, # width of Y direction cells (each row) - col_width=10.0, # width of X direction cells (each column) - surface_elevation=100.0, - layer_thickness=100.0, - origin_x=0.0, - origin_y=0.0, - rotation=0.0, - ): - if nlay is None or nlay < 1: - self._init_empty() - return - - ncpl = ncol * nrow # Nodes per layer - - self.nrow = nrow - self.ncol = ncol - ncell = ncpl * nlay - - # Check if layer_thickness needs to be flattened - cls_name = "DisvStructuredGridBuilder" - top = self._get_array(cls_name, surface_elevation, ncpl) - thick = self._get_array(cls_name, layer_thickness, nlay, ncell) - self.row_width = self._get_array(cls_name, row_width, ncol) - self.col_width = self._get_array(cls_name, col_width, nrow) - - bot = [] - if thick.size == nlay: - for lay in range(nlay): - bot.append(top - thick[: lay + 1].sum()) - else: - st = 0 - sp = ncpl - bt = top.copy() - for lay in range(nlay): - bt -= thick[st:sp] - st, sp = sp, sp + ncpl - bot.append(bt) - - # Build the grid - - # Setup vertices - vertices = [] - - # Get row 1 top: - yv_model_top = self.col_width.sum() - - # Assemble vertices along x-axis and model top - iv = 0 - xv, yv = 0.0, yv_model_top - vertices.append([iv, xv, yv]) - for c in range(ncol): - iv += 1 - xv += self.row_width[c] - vertices.append([iv, xv, yv]) - - # Finish the rest of the grid a row at a time - for r in range(nrow): - iv += 1 - yv -= self.col_width[r] - xv = 0.0 - vertices.append([iv, xv, yv]) - for c in range(ncol): - iv += 1 - xv += self.row_width[c] - vertices.append([iv, xv, yv]) - - # cell2d: [icell2d, xc, yc, ncvert, icvert] - cell2d = [] - ic = -1 - # Finish the rest of the grid a row at a time - for r in range(nrow): - for c in range(ncol): - ic += 1 - icvert = self.get_vertices(r, c) - xc, yc = self.get_centroid(icvert, vertices) - cell2d.append([ic, xc, yc, 4, *icvert]) - - super().__init__( - nlay, vertices, cell2d, top, bot, origin_x, origin_y, rotation - ) - - def __repr__(self): - return super().__repr__("DisvStructuredGridBuilder") - - def _init_empty(self): - super()._init_empty() - nul = np.array([]) - self.nrow = 0 - self.ncol = 0 - self.row_width = nul - self.col_width = nul - - def property_copy_to(self, DisvStructuredGridBuilderType): - if isinstance( - DisvStructuredGridBuilderType, DisvStructuredGridBuilder - ): - super().property_copy_to(DisvStructuredGridBuilderType) - DisvStructuredGridBuilderType.nrow = self.nrow - DisvStructuredGridBuilderType.ncol = self.ncol - DisvStructuredGridBuilderType.row_width = self.row_width.copy() - DisvStructuredGridBuilderType.col_width = self.col_width.copy() - else: - raise RuntimeError( - "DisvStructuredGridBuilder.property_copy_to " - "can only copy to objects that inherit " - "properties from DisvStructuredGridBuilder" - ) - - def copy(self): - cp = DisvStructuredGridBuilder() - self.property_copy_to(cp) - return cp - - def get_cellid(self, row, col): - """ - Get the cellid given the row and column indices. - - Parameters - ---------- - row : int - Row index. - col : int - Column index. - - Returns - ------- - int - cellid index - """ - return row * self.ncol + col - - def get_row_col(self, cellid): - """ - Get the row and column indices given the cellid. - - Parameters - ---------- - cellid : int - cellid index - - Returns - ------- - (int, int) - Row index, Column index - """ - row = cellid // self.ncol - col = cellid - row * self.ncol - return row, col - - def get_vertices(self, row, col): - """ - Get the vertex indices for a cell given the row and column indices. - - Parameters - ---------- - row : int - Row index. - col : int - Column index. - - Returns - ------- - list[int] - List of vertex indices that define the cell at (row, col). - """ - nver = self.ncol + 1 - return [ - row * nver + col, - row * nver + col + 1, - (row + 1) * nver + col + 1, - (row + 1) * nver + col, - ] - - def iter_row_col(self): - """Generator that iterates through each rows' columns. - - Yields - ------- - (int, int) - Row index, column index - """ - for cellid in range(self.ncpl): - yield self.get_row_col(cellid) - - def iter_row_cellid(self, row): - """Generator that iterates through the cellid within a row. - That is, the cellid for all columns within the specified row. - - Parameters - ---------- - row : int - Row index. - - Yields - ------- - int - cellid index - """ - for col in range(self.ncol): - yield self.get_cellid(row, col) - - def iter_column_cellid(self, col): - """Generator that iterates through the cellid within a column. - That is, the cellid for all rows within the specified column. - - Parameters - ---------- - col : int - Column index. - - Yields - ------- - int - cellid index - """ - for row in range(self.nrow): - yield self.get_cellid(row, col) - - -if __name__ == "__main__": - simple2by2 = DisvStructuredGridBuilder(1, 2, 2) - simple2by3 = DisvStructuredGridBuilder(1, 2, 3) - simple2by2.plot_grid() - simple2by3.plot_grid() - - test1 = DisvStructuredGridBuilder(1, 2, 3, 10.0, 10.0, 100.0, 100) - test1.plot_grid(plot_time=0.2) - - test2 = DisvStructuredGridBuilder( - 4, 5, 7, 50.0, 50.0, 100.0, 25.0, 10, 10, 30.0 - ) - test2.plot_grid(plot_time=0.2) - pass diff --git a/common/analytical.py b/common/analytical.py deleted file mode 100644 index 66264ce9a..000000000 --- a/common/analytical.py +++ /dev/null @@ -1,505 +0,0 @@ -import numpy as np -from scipy.special import erf, erfc - - -def diffusion(x, t, v, R, D): - """ - Calculate the analytical solution for one-dimension advection and - dispersion using the solution of Lapidus and Amundson (1952) and - Ogata and Banks (1961) - - Parameters - ---------- - x : float or ndarray - x position - t : float or ndarray - time - v : float or ndarray - velocity - R : float or ndarray - retardation factor, a value of one indicates there is no adsorption - D : float or ndarray - diffusion coefficient in length squared per time - - Returns - ------- - result : float or ndarray - normalized concentration value - - """ - denom = 2.0 * np.sqrt(D * R * t) - t1 = 0.5 * erfc((R * x - v * t) / denom) - t2 = 0.5 * np.exp(v * x / D) - t3 = erfc((R * x + v * t) / denom) - return t1 + t2 * t3 - - -class Wexler1d: - """ - Analytical solution for 1D transport with inflow at a concentration of 1. - at x=0 and a third-type bound at location l. - Wexler Page 17 and Van Genuchten and Alves pages 66-67 - """ - - def betaeqn(self, beta, d, v, l): - return beta / np.tan(beta) - beta**2 * d / v / l + v * l / 4.0 / d - - def fprimebetaeqn(self, beta, d, v, l): - """ - f1 = cotx - x/sinx2 - (2.0D0*C*x) - - """ - c = v * l / 4.0 / d - return 1.0 / np.tan(beta) - beta / np.sin(beta) ** 2 - 2.0 * c * beta - - def fprime2betaeqn(self, beta, d, v, l): - """ - f2 = -1.0D0/sinx2 - (sinx2-x*DSIN(x*2.0D0))/(sinx2*sinx2) - 2.0D0*C - - """ - c = v * l / 4.0 / d - sinx2 = np.sin(beta) ** 2 - return ( - -1.0 / sinx2 - - (sinx2 - beta * np.sin(beta * 2.0)) / (sinx2 * sinx2) - - 2.0 * c - ) - - def solvebetaeqn(self, beta, d, v, l, xtol=1.0e-12): - from scipy.optimize import fsolve - - t = fsolve( - self.betaeqn, - beta, - args=(d, v, l), - fprime=self.fprime2betaeqn, - xtol=xtol, - full_output=True, - ) - result = t[0][0] - infod = t[1] - isoln = t[2] - msg = t[3] - if abs(result - beta) > np.pi: - raise Exception("Error in beta solution") - err = self.betaeqn(result, d, v, l) - fvec = infod["fvec"][0] - if isoln != 1: - print("Error in beta solve", err, result, d, v, l, msg) - return result - - def root3(self, d, v, l, nval=1000): - b = 0.5 * np.pi - betalist = [] - for i in range(nval): - b = self.solvebetaeqn(b, d, v, l) - err = self.betaeqn(b, d, v, l) - betalist.append(b) - b += np.pi - return betalist - - def analytical(self, x, t, v, l, d, tol=1.0e-20, nval=5000): - sigma = 0.0 - betalist = self.root3(d, v, l, nval=nval) - for i, bi in enumerate(betalist): - denom = bi**2 + (v * l / 2.0 / d) ** 2 + v * l / d - x1 = ( - bi - * (bi * np.cos(bi * x / l) + v * l / 2.0 / d * np.sin(bi * x / l)) - / denom - ) - - denom = bi**2 + (v * l / 2.0 / d) ** 2 - x2 = np.exp(-1 * bi**2 * d * t / l**2) / denom - - sigma += x1 * x2 - term1 = 2.0 * v * l / d * np.exp(v * x / 2.0 / d - v**2 * t / 4.0 / d) - conc = 1.0 - term1 * sigma - if i > 0: - diff = abs(conc - concold) - if np.all(diff < tol): - break - concold = conc - return conc - - def analytical2(self, x, t, v, l, d, e=0.0, tol=1.0e-20, nval=5000): - """ - Calculate the analytical solution for one-dimension advection and - dispersion using the solution of Lapidus and Amundson (1952) and - Ogata and Banks (1961) - - Parameters - ---------- - x : float or ndarray - x position - t : float or ndarray - time - v : float or ndarray - velocity - l : float - length domain - d : float - dispersion coefficient - e : float - decay rate - - Returns - ------- - result : float or ndarray - normalized concentration value - - """ - u = v**2 + 4.0 * e * d - u = np.sqrt(u) - sigma = 0.0 - denom = (u + v) / 2.0 / v - (u - v) ** 2.0 / 2.0 / v / (u + v) * np.exp( - -u * l / d - ) - term1 = np.exp((v - u) * x / 2.0 / d) + (u - v) / (u + v) * np.exp( - (v + u) * x / 2.0 / d - u * l / d - ) - term1 = term1 / denom - term2 = 2.0 * v * l / d * np.exp(v * x / 2.0 / d - v**2 * t / 4.0 / d - e * t) - betalist = self.root3(d, v, l, nval=nval) - for i, bi in enumerate(betalist): - denom = bi**2 + (v * l / 2.0 / d) ** 2 + v * l / d - x1 = ( - bi - * (bi * np.cos(bi * x / l) + v * l / 2.0 / d * np.sin(bi * x / l)) - / denom - ) - - denom = bi**2 + (v * l / 2.0 / d) ** 2 + e * l**2 / d - x2 = np.exp(-1 * bi**2 * d * t / l**2) / denom - - sigma += x1 * x2 - - conc = term1 - term2 * sigma - if i > 0: - diff = abs(conc - concold) - if np.all(diff < tol): - break - concold = conc - return conc - - -class Wexler3d: - """ - Analytical solution for 3D transport with inflow at a well with a - specified concentration. - Wexler Page 47 - """ - - def calcgamma(self, x, y, z, xc, yc, zc, dx, dy, dz): - gam = np.sqrt((x - xc) ** 2 + dx / dy * (y - yc) ** 2 + dx / dz * (z - zc) ** 2) - return gam - - def calcbeta(self, v, dx, gam, lam): - beta = np.sqrt(v**2 + 4.0 * dx * gam * lam) - return beta - - def analytical(self, x, y, z, t, v, xc, yc, zc, dx, dy, dz, n, q, lam=0.0, c0=1.0): - gam = self.calcgamma(x, y, z, xc, yc, zc, dx, dy, dz) - beta = self.calcbeta(v, dx, gam, lam) - term1 = ( - c0 - * q - * np.exp(v * (x - xc) / 2.0 / dx) - / 8.0 - / n - / np.pi - / gam - / np.sqrt(dy * dz) - ) - term2 = np.exp(gam * beta / 2.0 / dx) * erfc( - (gam + beta * t) / 2.0 / np.sqrt(dx * t) - ) - term3 = np.exp(-gam * beta / 2.0 / dx) * erfc( - (gam - beta * t) / 2.0 / np.sqrt(dx * t) - ) - return term1 * (term2 + term3) - - def multiwell(self, x, y, z, t, v, xc, yc, zc, dx, dy, dz, n, ql, lam=0.0, c0=1.0): - shape = self.analytical( - x, y, z, t, v, xc[0], yc[0], zc[0], dx, dy, dz, n, ql[0], lam - ).shape - result = np.zeros(shape) - for xx, yy, zz, q in zip(xc, yc, zc, ql): - result += self.analytical( - x, y, z, t, v, xx, yy, zz, dx, dy, dz, n, q, lam, c0 - ) - return result - - -class BakkerRotatingInterface: - """ - Analytical solution for rotating interfaces (Bakker et al. 2004) - - """ - - @staticmethod - def get_s(k, rhoa, rhob, alpha): - return k * (rhob - rhoa) / rhoa * np.sin(alpha) - - @staticmethod - def get_F(z, zeta1, omega1, s): - l = (zeta1.real - omega1.real) ** 2 + (zeta1.imag - omega1.imag) ** 2 - l = np.sqrt(l) - try: - v = ( - s - * l - * complex(0, 1) - / 2 - / np.pi - / (zeta1 - omega1) - * np.log((z - zeta1) / (z - omega1)) - ) - except: - v = 0.0 - return v - - @staticmethod - def get_Fgrid(xg, yg, zeta1, omega1, s): - qxg = [] - qyg = [] - for x, y in zip(xg.flatten(), yg.flatten()): - z = complex(x, y) - W = BakkerRotatingInterface.get_F(z, zeta1, omega1, s) - qx = W.real - qy = -W.imag - qxg.append(qx) - qyg.append(qy) - qxg = np.array(qxg) - qyg = np.array(qyg) - qxg = qxg.reshape(xg.shape) - qyg = qyg.reshape(yg.shape) - return qxg, qyg - - @staticmethod - def get_zetan(n, x0, a, b): - return complex(x0 + (-1) ** n * a, (2 * n - 1) * b) - - @staticmethod - def get_omegan(n, x0, a, b): - return complex(x0 + (-1) ** (1 + n) * a, -(2 * n - 1) * b) - - @staticmethod - def get_w(xg, yg, k, rhoa, rhob, a, b, x0): - zeta1 = BakkerRotatingInterface.get_zetan(1, x0, a, b) - omega1 = BakkerRotatingInterface.get_omegan(1, x0, a, b) - alpha = np.arctan2(b, a) - s = BakkerRotatingInterface.get_s(k, rhoa, rhob, alpha) - qxg, qyg = BakkerRotatingInterface.get_Fgrid(xg, yg, zeta1, omega1, s) - for n in range(1, 5): - zetan = BakkerRotatingInterface.get_zetan(n, x0, a, b) - zetanp1 = BakkerRotatingInterface.get_zetan(n + 1, x0, a, b) - qx1, qy1 = BakkerRotatingInterface.get_Fgrid( - xg, yg, zetan, zetanp1, (-1) ** n * s - ) - omegan = BakkerRotatingInterface.get_omegan(n, x0, a, b) - omeganp1 = BakkerRotatingInterface.get_omegan(n + 1, x0, a, b) - qx2, qy2 = BakkerRotatingInterface.get_Fgrid( - xg, yg, omegan, omeganp1, (-1) ** n * s - ) - qxg += qx1 + qx2 - qyg += qy1 + qy2 - return qxg, qyg - - -def diffusion(x, t, v, R, D): - """ - Calculate the analytical solution for one-dimension advection and - dispersion using the solution of Lapidus and Amundson (1952) and - Ogata and Banks (1961) - - Parameters - ---------- - x : float or ndarray - x position - t : float or ndarray - time - v : float or ndarray - velocity - R : float or ndarray - retardation factor, a value of one indicates there is no adsorption - D : float or ndarray - diffusion coefficient in length squared per time - - Returns - ------- - result : float or ndarray - normalized concentration value - - """ - denom = 2.0 * np.sqrt(D * R * t) - t1 = 0.5 * erfc((R * x - v * t) / denom) - t2 = 0.5 * np.exp(v * x / D) - t3 = erfc((R * x + v * t) / denom) - return t1 + t2 * t3 - - -def hechtMendez_SS_3d( - x_pos, To, Y3d, Z3d, ath, atv, Fplanar, va, n, rhow, cw, thermdiff -): - """ - Calculate the analytical solution for changes in temperature three- - dimensional changes in temperature using transient solution provided in - the appendix of Hecht-Mendez et al. (2010) as equation A5. Note that for - SS conditions, the erfc term reduces to 1 as t -> infinity and the To/2 - term becomes T. - - Parameters - ---------- - x_pos : float or ndarray - x position - To : float or ndarray - initial temperature of the ground, degrees K - Y3d : float or ndarray - dimension of source in y direction for 3D test problem - Z3d : float or ndarray - dimension of source in z direction for 3D test problem - ath : float or ndarray - transverse horizontal dispersivity - atv : float or ndarray - transverse vertical dispersivity - Fplanar : float or ndarray - energy extraction (point source) - va : float or ndarray - seepage velocity - n : float or ndarray - porosity - rhow : float or ndarray - desity of water - cw : float or ndarray - specific heat capacity of water - thermdiff : float or ndarray - molecular diffusion coefficient, or in this case thermal - diffusivity - """ - - # calculate transverse horizontal heat dispersion - Dy = ath * (va**2 / abs(va)) + thermdiff - t2 = erf(Y3d / (4 * np.sqrt(Dy * (x_pos / va)))) - - Dz = atv * (va**2 / abs(va)) + thermdiff - t3 = erf(Z3d / (4 * np.sqrt(Dz * (x_pos / va)))) - - # initial temperature at the source - To_planar = Fplanar / (abs(va) * n * rhow * cw) - - sln = To + (To_planar * t2 * t3) - return sln - - -def hechtMendezSS(x_pos, y, a, F0, va, n, rhow, cw, thermdiff): - """ - Calculate the analytical solution for changes in temperature three- - dimensional changes in temperature for a steady state solution provided in - the appendix of Hecht-Mendez et al. (2010) as equation A4 - - Parameters - ---------- - x : float or ndarray - x position - y : float or ndarray - y position - a : float or ndarray - longitudinal dispersivity - F0 : float or ndarray - energy extraction (point source) - va : float or ndarray - seepage velocity - n : float or ndarray - porosity - rhow : float or ndarray - desity of water - cw : float or ndarray - specific heat capacity of water - thermdiff : float or ndarray - molecular diffusion coefficient, or in this case thermal - diffusivity - """ - - # calculate transverse horizontal heat dispersion - Dth = a * (va**2 / abs(va)) + thermdiff - - t1 = F0 / (va * n * rhow * cw * ((4 * np.pi * Dth * (x_pos / va)) ** (0.5))) - t2 = np.exp((-1 * va * y**2) / (4 * Dth * x_pos)) - sln = t1 * t2 - return sln - - -def hechtMendez3d(x_pos, t, Y, Z, al, ath, atv, thermdiff, va, n, R, Fplanar, cw, rhow): - """ - Calculate the analytical solution for three-dimensional changes in - temperature based on the solution provided in the appendix of Hecht-Mendez - et al. (2010) as equation A5 - - Parameters - ---------- - x : float or ndarray - x position - t : float or ndarray - time - Y : float or ndarray - dimension of the source in the y direction - Z : float or ndarray - dimension of the source in the z direction - al : float or ndarray - longitudinal dispersivity - ath : float or ndarray - transverse horizontal dispersivity - atv : float or ndarray - transverse vertical dispersivity - thermdiff : float or ndarray - molecular diffusion coefficient, or in this case thermal - diffusivity - va : float or ndarray - seepage velocity - n : float or ndarray - porosity - R : float or ndarray - retardation coefficient - Fplanar : float or ndarray - energy extraction (point source) - cw : float or ndarray - specific heat capacity of water - rhow : float or ndarray - desity of water - - """ - To_planar = Fplanar / (va * n * rhow * cw) - - Dl = al * (va**2 / abs(va)) + thermdiff - numer = R * x_pos - va * t - denom = 2 * np.sqrt(Dl * R * t) - - t1 = (To_planar / 2) * erfc(numer / denom) - - Dth = ath * (va**2 / abs(va)) + thermdiff - t2 = erf(Y / (4 * np.sqrt(Dth * (x_pos / va)))) - - Dtv = atv * (va**2 / abs(va)) + thermdiff - t3 = erf(Z / (4 * np.sqrt(Dtv * (x_pos / va)))) - - sln = t1 * t2 * t3 - return sln - - -# Analytical solution for Stallman analysis (Stallman 1965, JGR) -def Stallman(T_az, dT, tau, t, c_rho, darcy_flux, ko, c_w, rho_w, zbotm, nlay): - zstallman = np.zeros((nlay, 2)) - K = np.pi * c_rho / ko / tau - V = darcy_flux * c_w * rho_w / 2 / ko - a = ((K**2 + V**4 / 4) ** 0.5 + V**2 / 2) ** 0.5 - V - b = ((K**2 + V**4 / 4) ** 0.5 - V**2 / 2) ** 0.5 - for i in range(len(zstallman)): - zstallman[i, 0] = zbotm[i] - zstallman[i, 1] = ( - dT - * np.exp(-a * (-zstallman[i, 0])) - * np.sin(2 * np.pi * t / tau - b * (-zstallman[i, 0])) - + T_az - ) - return zstallman diff --git a/common/build_table.py b/common/build_table.py deleted file mode 100644 index bbf84d0d9..000000000 --- a/common/build_table.py +++ /dev/null @@ -1,94 +0,0 @@ -import os - - -def build_table(caption, fpth, arr, headings=None, col_widths=None): - if headings is None: - headings = arr.dtype.names - ncols = len(arr.dtype.names) - if not fpth.endswith(".tex"): - fpth += ".tex" - label = "tab:{}".format(os.path.basename(fpth).replace(".tex", "")) - - line = get_header(caption, label, headings, col_widths=col_widths) - - for idx in range(arr.shape[0]): - if idx % 2 != 0: - line += "\t\t\\rowcolor{Gray}\n" - line += "\t\t" - for jdx, name in enumerate(arr.dtype.names): - line += f"{arr[name][idx]}" - if jdx < ncols - 1: - line += " & " - line += " \\\\\n" - - # footer - line += get_footer() - - f = open(fpth, "w") - f.write(line) - f.close() - - return - - -def get_header(caption, label, headings, col_widths=None, center=True, firsthead=False): - ncol = len(headings) - if col_widths is None: - dx = 0.8 / float(ncol) - col_widths = [dx for idx in range(ncol)] - if center: - align = "p" - else: - align = "p" - - header = "\\small\n" - header += "\\begin{longtable}[!htbp]{\n" - for col_width in col_widths: - header += 38 * " " + f"{align}" + f"{{{col_width}\\linewidth-2\\arraycolsep}}\n" - header += 38 * " " + "}\n" - header += f"\t\\caption{{{caption}}} \\label{{{label}}} \\\\\n\n" - - if firsthead: - header += "\t\\hline \\hline\n" - header += "\t\\rowcolor{Gray}\n" - header += "\t" - for idx, s in enumerate(headings): - header += f"\\textbf{{{s}}}" - if idx < len(headings) - 1: - header += " & " - header += " \\\\\n" - header += "\t\\hline\n" - header += "\t\\endfirsthead\n\n" - - header += "\t\\hline \\hline\n" - header += "\t\\rowcolor{Gray}\n" - header += "\t" - for idx, s in enumerate(headings): - header += f"\\textbf{{{s}}}" - if idx < len(headings) - 1: - header += " & " - header += " \\\\\n" - header += "\t\\hline\n" - header += "\t\\endhead\n\n" - - return header - - -def get_footer(): - return "\t\\hline \\hline\n\\end{longtable}\n\\normalsize\n\n" - - -def exp_format(v): - s = f"{v:.2e}" - s = s.replace("e-0", "e-") - s = s.replace("e+0", "e+") - # s = s.replace("e", " \\times 10^{") + "}$" - return s - - -def float_format(v, fmt="{:.2f}"): - return fmt.format(v) - - -def int_format(v): - return f"{v:d}" diff --git a/common/config.py b/common/config.py deleted file mode 100644 index c9ed919fa..000000000 --- a/common/config.py +++ /dev/null @@ -1,104 +0,0 @@ -import os -import pathlib as pl -import sys -import time - -import matplotlib.pyplot as plt -from IPython import get_ipython - -# Setup working directories -work_directories = ( - pl.Path("../examples"), - pl.Path("../figures"), - pl.Path("../tables"), -) -for work_dir in work_directories: - work_dir.mkdir(parents=True, exist_ok=True) - -# run settings -buildModel = True -writeModel = True -runModel = True -plotModel = True -plotSave = True -createGif = True - - -# Test if being run as a script -def is_notebook(): - try: - shell = get_ipython().__class__.__name__ - if shell == "ZMQInteractiveShell": - return True # Jupyter notebook or qtconsole - elif shell == "TerminalInteractiveShell": - return False # Terminal running IPython - else: - return False # Other type (?) - except NameError: - return False - - -def timeit(method): - def timed(*args, **kw): - ts = time.time() - result = method(*args, **kw) - te = time.time() - if "log_time" in kw: - name = kw.get("log_name", method.__name__.upper()) - kw["log_time"][name] = int((te - ts) * 1000) - else: - print(f"{method.__name__} {(te - ts) * 1000:,.2f} ms") - return result - - return timed - - -# common figure settings -figure_ext = ".png" -plt.rcParams["image.cmap"] = "jet_r" - -# parse command line arguments -if is_notebook(): - if "CI" in os.environ: - plotSave = True - else: - plotSave = False -else: - for idx, arg in enumerate(sys.argv): - if arg in ("-nr", "--no_run"): - runModel = False - elif arg in ("-nw", "--no_write"): - writeModel = False - elif arg in ("-np", "--no_plot"): - plotModel = False - elif arg in ("-ng", "--no_gif"): - createGif = False - elif arg in ("-fe", "--figure_extension"): - if idx + 1 < len(sys.argv): - extension = sys.argv[idx + 1] - if not extension.startswith("."): - extension = "." + extension - figure_exts = tuple(plt.gcf().canvas.get_supported_filetypes().keys()) - if extension.lower() in figure_exts: - figure_ext = extension - -# base example workspace -base_ws = pl.Path("../examples") -for idx, arg in enumerate(sys.argv): - if arg in ("--destination"): - if idx + 1 < len(sys.argv): - base_ws = sys.argv[idx + 1] - base_ws = pl.Path(base_ws).resolve() -assert base_ws.is_dir() - -# data files required for examples -data_ws = pl.Path("../data") - -# set executable extension -eext = "" -soext = ".so" -if sys.platform.lower() == "win32": - eext = ".exe" - soext = ".dll" -if sys.platform.lower() == "darwin": - soext = ".dylib" \ No newline at end of file diff --git a/common/figspecs.py b/common/figspecs.py deleted file mode 100644 index 35bc51025..000000000 --- a/common/figspecs.py +++ /dev/null @@ -1,583 +0,0 @@ -import sys - -import matplotlib as mpl -import matplotlib.pyplot as plt -import numpy as np - - -class USGSFigure: - def __init__( - self, figure_type="map", family="Arial Narrow", font_path=None, verbose=False - ): - """Create a USGSFigure object - - Parameters - ---------- - figure_type : str - figure type ("map", "graph") - family : str - font family name (default is Arial Narrow) - verbose : bool - boolean that define if debug information should be written - """ - # initialize members - self.family = None - self.figure_type = None - self.verbose = verbose - - self.set_font_family(family=family, font_path=font_path) - self.set_specifications(figure_type=figure_type) - - def set_specifications(self, figure_type="map"): - """Set matplotlib parameters - - Parameters - ---------- - figure_type : str - figure type ("map", "graph") - - Returns - ------- - - """ - self.figure_type = self._validate_figure_type(figure_type) - - def set_font_family(self, family="Arial Narrow", font_path=None): - """Set font family - - Parameters - ---------- - family : str - font family (default is Arial Narrow) - font_path : str - path to fonts not available to matplotlib (not implemented) - - Returns - ------- - - """ - if font_path is not None: - errmsg = "specification of font_path is not implemented" - raise NotImplemented(errmsg) - self.family = self._set_fontfamily(family) - - def graph_legend(self, ax=None, handles=None, labels=None, **kwargs): - """Add a USGS-style legend to a matplotlib axis object - - Parameters - ---------- - ax : axis object - matplotlib axis object (default is None) - handles : list - list of legend handles - labels : list - list of labels for legend handles - kwargs : kwargs - matplotlib legend kwargs - - Returns - ------- - leg : object - matplotlib legend object - - """ - if ax is None: - ax = plt.gca() - - font = self._set_fontspec(bold=True, italic=False) - if handles is None or labels is None: - handles, labels = ax.get_legend_handles_labels() - leg = ax.legend(handles, labels, prop=font, **kwargs) - - # add title to legend - if "title" in kwargs: - title = kwargs.pop("title") - else: - title = None - leg = self.graph_legend_title(leg, title=title) - return leg - - def graph_legend_title(self, leg, title=None): - """Set the legend title for a matplotlib legend object - - Parameters - ---------- - leg : legend object - matplotlib legend object - title : str - title for legend - - Returns - ------- - leg : object - matplotlib legend object - - """ - if title is None: - title = "EXPLANATION" - elif title.lower() == "none": - title = None - font = self._set_fontspec(bold=True, italic=False) - leg.set_title(title, prop=font) - return leg - - def heading(self, ax=None, letter=None, heading=None, x=0.00, y=1.01, idx=None): - """Add a USGS-style heading to a matplotlib axis object - - Parameters - ---------- - ax : axis object - matplotlib axis object (default is None) - letter : str - string that defines the subplot (A, B, C, etc.) - heading : str - text string - x : float - location of the heading in the x-direction in normalized plot dimensions - ranging from 0 to 1 (default is 0.00) - y : float - location of the heading in the y-direction in normalized plot dimensions - ranging from 0 to 1 (default is 1.01) - idx : int - index for programatically generating the heading letter when letter - is None and idx is not None. idx = 0 will generate A (default is None) - - Returns - ------- - text : object - matplotlib text object - - """ - if ax is None: - ax = plt.gca() - - if letter is None and idx is not None: - letter = chr(ord("A") + idx) - - text = None - if letter is not None: - font = self._set_fontspec(bold=True, italic=True) - if heading is None: - letter = letter.replace(".", "") - else: - letter = letter.rstrip() - if letter[-1] != ".": - letter += "." - letter += " " - ax.text( - x, - y, - letter, - va="bottom", - ha="left", - fontdict=font, - transform=ax.transAxes, - ) - bbox = ax.get_window_extent().transformed( - plt.gcf().dpi_scale_trans.inverted() - ) - width = bbox.width * 25.4 # inches to mm - x += len(letter) * 1.0 / width - if heading is not None: - font = self._set_fontspec(bold=True, italic=False) - text = ax.text( - x, - y, - heading, - va="bottom", - ha="left", - fontdict=font, - transform=ax.transAxes, - ) - return text - - def add_text( - self, - ax=None, - text="", - x=0.0, - y=0.0, - transform=True, - bold=True, - italic=True, - fontsize=9, - ha="left", - va="bottom", - **kwargs, - ): - """Add USGS-style text to a axis object - - Parameters - ---------- - ax : axis object - matplotlib axis object (default is None) - text : str - text string - x : float - x-location of text string (default is 0.) - y : float - y-location of text string (default is 0.) - transform : bool - boolean that determines if a transformed (True) or data (False) coordinate - system is used to define the (x, y) location of the text string - (default is True) - bold : bool - boolean indicating if bold font (default is True) - italic : bool - boolean indicating if italic font (default is True) - fontsize : int - font size (default is 9 points) - ha : str - matplotlib horizontal alignment keyword (default is left) - va : str - matplotlib vertical alignment keyword (default is bottom) - kwargs : dict - dictionary with valid matplotlib text object keywords - - Returns - ------- - text_obj : object - matplotlib text object - - """ - if ax is None: - ax = plt.gca() - - if transform: - transform = ax.transAxes - else: - transform = ax.transData - - font = self._set_fontspec(bold=bold, italic=italic, fontsize=fontsize) - - text_obj = ax.text( - x, y, text, va=va, ha=ha, fontdict=font, transform=transform, **kwargs - ) - return text_obj - - def add_annotation( - self, - ax=None, - text="", - xy=None, - xytext=None, - bold=True, - italic=True, - fontsize=9, - ha="left", - va="bottom", - **kwargs, - ): - """Add an annotation to a axis object - - Parameters - ---------- - ax : axis object - matplotlib axis object (default is None) - text : str - text string - xy : tuple - tuple with the location of the annotation (default is None) - xytext : tuple - tuple with the location of the text - bold : bool - boolean indicating if bold font (default is True) - italic : bool - boolean indicating if italic font (default is True) - fontsize : int - font size (default is 9 points) - ha : str - matplotlib horizontal alignment keyword (default is left) - va : str - matplotlib vertical alignment keyword (default is bottom) - kwargs : dict - dictionary with valid matplotlib annotation object keywords - - Returns - ------- - ann_obj : object - matplotlib annotation object - - """ - if ax is None: - ax = plt.gca() - - if xy is None: - xy = (0.0, 0.0) - - if xytext is None: - xytext = (0.0, 0.0) - - font = self._set_fontspec(bold=bold, italic=italic, fontsize=fontsize) - - # add font information to kwargs - if kwargs is None: - kwargs = font - else: - for key, value in font.items(): - kwargs[key] = value - - # create annotation - ann_obj = ax.annotate(text, xy, xytext, va=va, ha=ha, **kwargs) - - return ann_obj - - def remove_edge_ticks(self, ax=None): - """Remove unnecessary ticks on the edges of the plot - - Parameters - ---------- - ax : axis object - matplotlib axis object (default is None) - - Returns - ------- - ax : axis object - matplotlib axis object - - """ - if ax is None: - ax = plt.gca() - - # update tick objects - plt.draw() - - # get min and max value and ticks - ymin, ymax = ax.get_ylim() - - # check for condition where y-axis values are reversed - if ymax < ymin: - y = ymin - ymin = ymax - ymax = y - yticks = ax.get_yticks() - - if self.verbose: - print("y-axis: ", ymin, ymax) - print(yticks) - - # remove edge ticks on y-axis - ticks = ax.yaxis.majorTicks - for iloc in [0, -1]: - if np.allclose(float(yticks[iloc]), ymin): - ticks[iloc].tick1line.set_visible = False - ticks[iloc].tick2line.set_visible = False - if np.allclose(float(yticks[iloc]), ymax): - ticks[iloc].tick1line.set_visible = False - ticks[iloc].tick2line.set_visible = False - - # get min and max value and ticks - xmin, xmax = ax.get_xlim() - - # check for condition where x-axis values are reversed - if xmax < xmin: - x = xmin - xmin = xmax - xmax = x - - xticks = ax.get_xticks() - if self.verbose: - print("x-axis: ", xmin, xmax) - print(xticks) - - # remove edge ticks on y-axis - ticks = ax.xaxis.majorTicks - for iloc in [0, -1]: - if np.allclose(float(xticks[iloc]), xmin): - ticks[iloc].tick1line.set_visible = False - ticks[iloc].tick2line.set_visible = False - if np.allclose(float(xticks[iloc]), xmax): - ticks[iloc].tick1line.set_visible = False - ticks[iloc].tick2line.set_visible = False - - return ax - - # protected methods - # protected method - def _validate_figure_type(self, figure_type): - """Set figure type after validation of specified figure type - - Parameters - ---------- - figure_type : str - figure type ("map", "graph") - - Returns - ------- - figure_type : str - validated figure_type - - """ - # validate figure type - valid_types = ("map", "graph") - if figure_type not in valid_types: - errmsg = "invalid figure_type specified ({}) ".format( - figure_type - ) + "valid types are '{}'.".format(", ".join(valid_types)) - raise ValueError(errmsg) - - # set figure_type - if figure_type == "map": - self._set_map_specifications() - elif figure_type == "graph": - self._set_map_specifications() - - return figure_type - - # protected method - def _set_graph_specifications(self): - """Set matplotlib rcparams to USGS-style specifications for graphs - - Returns - ------- - - """ - rc_dict = { - "font.family": self.family, - "font.size": 7, - "axes.labelsize": 9, - "axes.titlesize": 9, - "axes.linewidth": 0.5, - "xtick.labelsize": 8, - "xtick.top": True, - "xtick.bottom": True, - "xtick.major.size": 7.2, - "xtick.minor.size": 3.6, - "xtick.major.width": 0.5, - "xtick.minor.width": 0.5, - "xtick.direction": "in", - "ytick.labelsize": 8, - "ytick.left": True, - "ytick.right": True, - "ytick.major.size": 7.2, - "ytick.minor.size": 3.6, - "ytick.major.width": 0.5, - "ytick.minor.width": 0.5, - "ytick.direction": "in", - "pdf.fonttype": 42, - "savefig.dpi": 300, - "savefig.transparent": True, - "legend.fontsize": 9, - "legend.frameon": False, - "legend.markerscale": 1.0, - } - mpl.rcParams.update(rc_dict) - - # protected method - def _set_map_specifications(self): - """Set matplotlib rcparams to USGS-style specifications for maps - - Returns - ------- - - """ - rc_dict = { - "font.family": self.family, - "font.size": 7, - "axes.labelsize": 9, - "axes.titlesize": 9, - "axes.linewidth": 0.5, - "xtick.labelsize": 7, - "xtick.top": True, - "xtick.bottom": True, - "xtick.major.size": 7.2, - "xtick.minor.size": 3.6, - "xtick.major.width": 0.5, - "xtick.minor.width": 0.5, - "xtick.direction": "in", - "ytick.labelsize": 7, - "ytick.left": True, - "ytick.right": True, - "ytick.major.size": 7.2, - "ytick.minor.size": 3.6, - "ytick.major.width": 0.5, - "ytick.minor.width": 0.5, - "ytick.direction": "in", - "pdf.fonttype": 42, - "savefig.dpi": 300, - "savefig.transparent": True, - "legend.fontsize": 9, - "legend.frameon": False, - "legend.markerscale": 1.0, - } - mpl.rcParams.update(rc_dict) - - # protected method - def _set_fontspec(self, bold=True, italic=True, fontsize=9): - """Create fontspec dictionary for matplotlib pyplot objects - - Parameters - ---------- - bold : bool - boolean indicating if font is bold (default is True) - italic : bool - boolean indicating if font is italic (default is True) - fontsize : int - font size (default is 9 point) - - - Returns - ------- - - """ - if "Univers" in self.family: - reset_family = True - else: - reset_family = False - family = self.family - - if bold: - weight = "bold" - if reset_family: - family = "Univers 67" - else: - weight = "normal" - if reset_family: - family = "Univers 57" - - if italic: - if reset_family: - family += " Condensed Oblique" - style = "oblique" - else: - style = "italic" - else: - if reset_family: - family += " Condensed" - style = "normal" - - # define fontspec dictionary - fontspec = { - "family": family, - "size": fontsize, - "weight": weight, - "style": style, - } - - if self.verbose: - sys.stdout.write("font specifications:\n ") - for key, value in fontspec.items(): - sys.stdout.write(f"{key}={value} ") - sys.stdout.write("\n") - - return fontspec - - def _set_fontfamily(self, family): - """Set font family to Liberation Sans Narrow on linux if default Arial Narrow - is being used - - Parameters - ---------- - family : str - font family name (default is Arial Narrow) - - Returns - ------- - family : str - font family name - - """ - if sys.platform.lower() in ("linux",): - if family == "Arial Narrow": - family = "Liberation Sans Narrow" - return family diff --git a/common/get_disu_radial_kwargs.py b/common/get_disu_radial_kwargs.py deleted file mode 100644 index 47be84238..000000000 --- a/common/get_disu_radial_kwargs.py +++ /dev/null @@ -1,233 +0,0 @@ -import numpy as np - - -def get_disu_radial_kwargs( - nlay, - nradial, - radius_outer, - surface_elevation, - layer_thickness, - get_vertex=False, -): - """ - Simple utility for creating radial unstructured elements - with the disu package. - - Input assumes that each layer contains the same radial band, - but their thickness can be different. - - Parameters - ---------- - nlay: number of layers (int) - nradial: number of radial bands to construct (int) - radius_outer: Outer radius of each radial band (array-like float with nradial length) - surface_elevation: Top elevation of layer 1 as either a float or nradial array-like float values. - If given as float, then value is replicated for each radial band. - layer_thickness: Thickness of each layer as either a float or nlay array-like float values. - If given as float, then value is replicated for each layer. - """ - pi = 3.141592653589793 - - def get_nn(lay, rad): - return nradial * lay + rad - - def get_rad_array(var, rep): - try: - dim = len(var) - except: - dim, var = 1, [var] - - if dim != 1 and dim != rep: - raise IndexError( - f"get_rad_array(var): var must be a scalar or have len(var)=={rep}" - ) - - if dim == 1: - return np.full(rep, var[0], dtype=np.float64) - else: - return np.array(var, dtype=np.float64) - - nodes = nlay * nradial - surf = get_rad_array(surface_elevation, nradial) - thick = get_rad_array(layer_thickness, nlay) - - iac = np.zeros(nodes, dtype=int) - ja = [] - ihc = [] - cl12 = [] - hwva = [] - - area = np.zeros(nodes, dtype=float) - top = np.zeros(nodes, dtype=float) - bot = np.zeros(nodes, dtype=float) - - for lay in range(nlay): - st = nradial * lay - sp = nradial * (lay + 1) - top[st:sp] = surf - thick[:lay].sum() - bot[st:sp] = surf - thick[: lay + 1].sum() - - for lay in range(nlay): - for rad in range(nradial): - # diagonal/self - n = get_nn(lay, rad) - ja.append(n) - iac[n] += 1 - if rad > 0: - area[n] = pi * ( - radius_outer[rad] ** 2 - radius_outer[rad - 1] ** 2 - ) - else: - area[n] = pi * radius_outer[rad] ** 2 - ihc.append(n + 1) - cl12.append(n + 1) - hwva.append(n + 1) - # up - if lay > 0: - ja.append(n - nradial) - iac[n] += 1 - ihc.append(0) - cl12.append(0.5 * (top[n] - bot[n])) - hwva.append(area[n]) - # to center - if rad > 0: - ja.append(n - 1) - iac[n] += 1 - ihc.append(1) - cl12.append(0.5 * (radius_outer[rad] - radius_outer[rad - 1])) - hwva.append(2.0 * pi * radius_outer[rad - 1]) - - # to outer - if rad < nradial - 1: - ja.append(n + 1) - iac[n] += 1 - ihc.append(1) - hwva.append(2.0 * pi * radius_outer[rad]) - if rad > 0: - cl12.append( - 0.5 * (radius_outer[rad] - radius_outer[rad - 1]) - ) - else: - cl12.append(radius_outer[rad]) - # bottom - if lay < nlay - 1: - ja.append(n + nradial) - iac[n] += 1 - ihc.append(0) - cl12.append(0.5 * (top[n] - bot[n])) - hwva.append(area[n]) - - # Build rectangular equivalent of radial coordinates (unwrap radial bands) - if get_vertex: - perimeter_outer = np.fromiter( - (2.0 * pi * rad for rad in radius_outer), - dtype=float, - count=nradial, - ) - xc = 0.5 * radius_outer[0] - yc = 0.5 * perimeter_outer[-1] - # all cells have same y-axis cell center; yc is costant - # - # cell2d: [icell2d, xc, yc, ncvert, icvert]; first node: cell2d = [[0, xc, yc, [2, 1, 0]]] - cell2d = [] - for lay in range(nlay): - n = get_nn(lay, 0) - cell2d.append([n, xc, yc, 3, 2, 1, 0]) - # - xv = radius_outer[0] - # half perimeter is equal to the y shift for vertices - sh = 0.5 * perimeter_outer[0] - vertices = [ - [0, 0.0, yc], - [1, xv, yc - sh], - [2, xv, yc + sh], - ] # vertices: [iv, xv, yv] - iv = 3 - for r in range(1, nradial): - # radius_outer[r-1] + 0.5*(radius_outer[r] - radius_outer[r-1]) - xc = 0.5 * (radius_outer[r - 1] + radius_outer[r]) - for lay in range(nlay): - n = get_nn(lay, r) - # cell2d: [icell2d, xc, yc, ncvert, icvert] - cell2d.append([n, xc, yc, 4, iv - 2, iv - 1, iv + 1, iv]) - - xv = radius_outer[r] - # half perimeter is equal to the y shift for vertices - sh = 0.5 * perimeter_outer[r] - vertices.append([iv, xv, yc - sh]) # vertices: [iv, xv, yv] - iv += 1 - vertices.append([iv, xv, yc + sh]) # vertices: [iv, xv, yv] - iv += 1 - cell2d.sort(key=lambda row: row[0]) # sort by node number - - ja = np.array(ja, dtype=np.int32) - nja = ja.shape[0] - hwva = np.array(hwva, dtype=np.float64) - kw = {} - kw["nodes"] = nodes - kw["nja"] = nja - kw["nvert"] = None - kw["top"] = top - kw["bot"] = bot - kw["area"] = area - kw["iac"] = iac - kw["ja"] = ja - kw["ihc"] = ihc - kw["cl12"] = cl12 - kw["hwva"] = hwva - - if get_vertex: - kw["nvert"] = len(vertices) # = 2*nradial + 1 - kw["vertices"] = vertices - kw["cell2d"] = cell2d - kw["angldegx"] = np.zeros(nja, dtype=float) - else: - kw["nvert"] = 0 - - return kw - - -if __name__ == "__main__": - # Test case - - nlay = 25 # Number of layers - nradial = 22 # number of radial bands (innermost band is a full circle) - - radius_outer = [ - 0.25, - 0.75, - 1.5, - 2.5, - 4.0, - 6.0, - 9.0, - 13.0, - 18.0, - 23.0, - 33.0, - 47.0, - 65.0, - 90.0, - 140.0, - 200.0, - 300.0, - 400.0, - 600.0, - 1000.0, - 1500.0, - 2000.0, - ] # outer radius of each radial band - - layer_thickness = 2.0 # thickness of each radial layer - surface_elevation = 50.0 # surface elevation model - - radial_kwargs = get_disu_radial_kwargs( - nlay, - nradial, - radius_outer, - surface_elevation, - layer_thickness, - get_vertex=True, - ) - - print(radial_kwargs) diff --git a/common/groundwater2023_utils.py b/common/groundwater2023_utils.py deleted file mode 100644 index b46494e4d..000000000 --- a/common/groundwater2023_utils.py +++ /dev/null @@ -1,81 +0,0 @@ -import numpy as np -import shapely - -geometries = { - "sv_boundary": """0.0 0.0 - 0.0 20000.0 - 12500.0 20000.0 - 12500.0 0.0""", - "sv_river": """4250.0 8750.0 - 4250.0 0.0""", - "sv_river_box": """3500.0 0.0 - 3500.0 9500.0 - 5000.0 9500.0 - 5000.0 0.0""", - "sv_wells": """7250. 17250. - 7750. 2750. - 2750 3750.""", - "sv_lake": """1500. 18500. - 3500. 18500. - 3500. 15500. - 4000. 15500. - 4000. 14500. - 4500. 14500. - 4500. 12000. - 2500. 12000. - 2500. 12500. - 2000. 12500. - 2000. 14000. - 1500. 14000. - 1500. 15000. - 1000. 15000. - 1000. 18000. - 1500. 18000.""", -} - - -def string2geom(geostring, conversion=None): - if conversion is None: - multiplier = 1.0 - else: - multiplier = float(conversion) - res = [] - for line in geostring.split("\n"): - line = line.strip() - line = line.split(" ") - x = float(line[0]) * multiplier - y = float(line[1]) * multiplier - res.append((x, y)) - return res - - -def densify_geometry(line, step, keep_internal_nodes=True): - xy = [] # list of tuple of coordinates - lines_strings = [] - if keep_internal_nodes: - for idx in range(1, len(line)): - lines_strings.append(shapely.geometry.LineString(line[idx - 1 : idx + 1])) - else: - lines_strings = [shapely.geometry.LineString(line)] - - for line_string in lines_strings: - length_m = line_string.length # get the length - for distance in np.arange(0, length_m + step, step): - point = line_string.interpolate(distance) - xy_tuple = (point.x, point.y) - if xy_tuple not in xy: - xy.append(xy_tuple) - # make sure the end point is in xy - if keep_internal_nodes: - xy_tuple = line_string.coords[-1] - if xy_tuple not in xy: - xy.append(xy_tuple) - - return xy - - -def circle_function(center=(0, 0), radius=1.0, dtheta=10.0): - angles = np.arange(0.0, 360.0, dtheta) * np.pi / 180.0 - xpts = center[0] + np.cos(angles) * radius - ypts = center[1] + np.sin(angles) * radius - return np.array([(x, y) for x, y in zip(xpts, ypts)]) diff --git a/common/neuman1974_soln.py b/common/neuman1974_soln.py deleted file mode 100644 index ad05fe668..000000000 --- a/common/neuman1974_soln.py +++ /dev/null @@ -1,1034 +0,0 @@ -import numpy as np -from math import sqrt -import matplotlib.pyplot as plt - -# Find a root of a function using Brent's method within a bracketed range -from scipy.optimize import brentq - -# Solve definite integral using Fortran library QUADPACK -from scipy.integrate import quad - -# Zero Order Bessel Function -from scipy.special import j0, jn_zeros - -__all__ = ["RadialUnconfinedDrawdown"] - -pi = 3.141592653589793 -sin = np.sin -cos = np.cos -sinh = np.sinh -cosh = np.cosh -exp = np.exp - - -def _find_hyperbolic_max_value(): - seterr = np.seterr() - np.seterr(all="ignore") - inf = np.inf - x = 10.0 - delt = 1.0 - for i in range(1000000): - x += delt - try: - if inf == sinh(x): - break - except: - break - np.seterr(**seterr) - return x - delt - - -_hyperbolic_max_value = _find_hyperbolic_max_value() - - -def _find_hyperbolic_equivalent_value(): - x = 10.0 - delt = 0.0001 - for i in range(1000000): - x += delt - if x > _hyperbolic_max_value: - break - try: - if sinh(x) == cosh(x): - return x - except: - break - return x - delt - - -_hyperbolic_equivalence = _find_hyperbolic_equivalent_value() - - -class RadialUnconfinedDrawdown: - """ - Solves the drawdown that occurs from pumping from partial penetration - in an unconfined, radial aquifer. Uses the method described in: - Neuman, S. P. (1974). Effect of partial penetration on flow in - unconfined aquifers considering delayed gravity response. - Water resources research, 10(2), 303-312. - """ - - hyperbolic_max_value = _hyperbolic_max_value - hyperbolic_equivalence = _hyperbolic_equivalence - - bottom: float - Kr: float - Kz: float - Ss: float - Sy: float - well_top: float - well_bot: float - saturated_thickness: float - - _sigma: float - _beta: float - - def __init__( - self, - bottom_elevation, - hydraulic_conductivity_radial=None, - hydraulic_conductivity_vertical=None, - specific_storage=None, - specific_yield=None, - well_screen_elevation_top=None, - well_screen_elevation_bottom=None, - water_table_elevation=None, - saturated_thickness=None, - ): - """ - Initialize unconfined, radial groundwater model to solve drawdown - at an observation location in response to pumping at the center of - the model (that is, the well extracts water at radius = 0). - - Parameters - ---------- - rad : int - radial band number (0 to nradial-1) - - bottom_elevation : float - Elevation of the impermeable base of the model ($L$) - hydraulic_conductivity_radial : float - Radial direction hydraulic conductivity of model ($L/T$) - hydraulic_conductivity_vertical : float - Vertical (z) direction hydraulic conductivity of model ($L/T$) - specific_storage : float - Specific storage of aquifer ($1/T$) - specific_yield : float - Specific yield of aquifer ($-$) - well_screen_elevation_top : float - Pumping well's top screen elevation ($L$) - well_screen_elevation_bottom : float - Pumping well's bottom screen elevation ($L$) - water_table_elevation : float - Initial water table elevation. Note, saturated_thickness (b) is - calculated as $water_table_elevation - bottom_elevation$ ($L$) - saturated_thickness : float - Specify the initial saturated thickness of the unconfined aquifer. - Value is used to calculate the water_table_elevation. If - water_table_elevation is defined, then saturated_thickness input - is ignored and set to - $water_table_elevation - bottom_elevation$ ($L$) - """ - - self.bottom = float(bottom_elevation) - self.Kr = self._float_or_none(hydraulic_conductivity_radial) - self.Kz = self._float_or_none(hydraulic_conductivity_vertical) - self.Ss = self._float_or_none(specific_storage) - self.Sy = self._float_or_none(specific_yield) - self.well_top = self._float_or_none(well_screen_elevation_top) - self.well_bot = self._float_or_none(well_screen_elevation_bottom) - - if ( - water_table_elevation is not None - and saturated_thickness is not None - ): - raise RuntimeError( - "RadialUnconfinedDrawdown() must specify only " - + "water_table_elevation or saturated_thickness, but not " - + "both at the same time." - ) - - if water_table_elevation is not None: - self.saturated_thickness = ( - float(water_table_elevation) - self.bottom - ) - elif saturated_thickness is not None: - self.saturated_thickness = float(saturated_thickness) - else: - self.saturated_thickness = None - - def _prop_check(self): - error = [] - if self.Kr is None: - error.append("hydraulic_conductivity_radial") - if self.Kz is None: - error.append("hydraulic_conductivity_vertical") - if self.Ss is None: - error.append("specific_storage") - if self.Sy is None: - error.append("specific_yield") - if self.well_top is None: - error.append("well_screen_elevation_top") - if self.well_bot is None: - error.append("well_screen_elevation_bottom") - if error: - raise RuntimeError( - "RadialUnconfinedDrawdown: Attempted to solve radial " - + "groundwater model\nwith the following input not specified\n" - + "\n".join(error) - ) - if self.well_top <= self.well_bot: - raise RuntimeError( - "RadialUnconfinedDrawdown: " - + "well_screen_elevation_top <= well_screen_elevation_bottom\n" - + f"That is: {well_screen_elevation_top} <= " - + f"{well_screen_elevation_bottom}" - ) - - def drawdown( - self, - pump, - time, - radius, - observation_elevation, - observation_elevation_bot=None, - sumrtol=1.0e-6, - u_n_rtol=1.0e-5, - epsabs=1.49e-8, - bessel_loop_limit=5, - quad_limit=128, - show_progress=False, - ty_time=False, - ts_time=False, - as_head=False, - ): - """ - Solves the radial model's drawdown for a given pumping rate and - time at a given observation point - (radius, observation_elevation) or observation well screen interval - (radius, observation_elevation:observation_elevation_bot). - This solves drawdown by integrating equation 17 from - Neuman, S. P. (1974). Effect of partial penetration on flow in - unconfined aquifers considering delayed gravity response. - Water resources research, 10(2), 303-312 - - Parameters - ---------- - pump : float - Pumping rate of well at center of radial model ($L^3/T$) - Positive values are the water extraction rate. - Negative or zero values indicate no pumping and result returns - the dimensionless drawdown instead of regular drawdown. - time : float or Sequence[float] - Time that observation is made - radius : float - Radius of the observation location (distance from well, $L$) - observation_elevation : float - Either the location of the observation point, or the top elevation - of the observation well screen ($L$) - observation_elevation_bot : float - If specified, then represents the bottom elevation of the - observation well screen. If not specified (or set to None), then - observation location is treated as a single point, located at - radius and observation_elevation ($L$) - sumrtol : float - Solution involves integration of $y$ variable from 0 to ∞ from - Equation 17 in: - Neuman, S. P. (1974). Effect of partial penetration on flow in - unconfined aquifers considering delayed gravity response. - Water resources research, 10(2), 303-312. - - The integration is broken into subsections that are spaced around - bessel function roots. The integration is complete when a - three sequential subsection solutions are less than - sumrtol times the largest subsection. - That is, the last included subsection contributes a - relatively small value compared to the largest of the sum. - u_n_rtol : float - Terminates the solution of the infinite series: - $\\sum_{n=1}^{\\infty} u_n(y)$ - when - $u_n(y) < u_n(0) * u_n_rtol$ - epsabs : float or int - scipy.integrate.quad absolute error tolerance. - Passed directly to that function's `epsabs` kwarg. - bessel_loop_limit : int - the integral is solved along each bessel function root. - The first 1024 roots are precalculated and automatically increased - if more are required. The upper limit for calculated roots is - 1024 * 2 ^ bessel_loop_limit - If this limit is reached, then a warning is raised. - quad_limit : int - scipy.integrate.quad upper bound on the number of - subintervals used in the adaptive algorithm. - Passed directly to that function's `limit` kwarg. - show_progress : bool - if True, then progress is printed to the command prompt in the form: - ty_time : bool - if True, then `time` kwarg is dimensionless time with - respect to Specific Yield - ts_time : bool - if True, then `time` kwarg is dimensionless time with - respect to Specific Storage. - as_head : bool - If true, then drawdown result is converted to - head using the model bottom and initial saturated thickness. - If pump > 0, then as_head is ignored. - - Returns - ------- - result : float or list[float] - If time is float, then result is float. - If time is Sequence[float], then result is list[float]. - - If pump > 0, then result is the drawdown that occurs - from pump at time and radius at observation point - observation_elevation or from the observation well - screen interval observation_elevation to - observation_elevation_top ($L$). - - - If pump <= 0, then result is converted to - dimensionless drawdown ($-$) - """ - if not hasattr(time, "strip") and hasattr(time, "__iter__"): - return self.drawdown_times( - pump, - time, - radius, - observation_elevation, - observation_elevation_bot, - sumrtol, - u_n_rtol, - epsabs, - bessel_loop_limit, - quad_limit, - show_progress, - ty_time, - ts_time, - as_head, - ) - - return self.drawdown_times( - pump, - [time], - radius, - observation_elevation, - observation_elevation_bot, - sumrtol, - u_n_rtol, - epsabs, - bessel_loop_limit, - quad_limit, - show_progress, - ty_time, - ts_time, - as_head, - )[0] - - def drawdown_times( - self, - pump, - times, - radius, - observation_elevation, - observation_elevation_bot=None, - sumrtol=1.0e-6, - u_n_rtol=1.0e-5, - epsabs=1.49e-8, - bessel_loop_limit=5, - quad_limit=128, - show_progress=False, - ty_time=False, - ts_time=False, - as_head=False, - ): - # Same as self.drawdown, but times is a list[float] of - # observation times and returns a list[float] drawdowns. - - if bessel_loop_limit < 1: - bessel_loop_limit = 1 - - bessel_roots0 = 1024 - bessel_roots = bessel_roots0 - bessel_root_limit_reached = [] - - self._prop_check() - if ty_time and ts_time: - raise RuntimeError( - "RadialUnconfinedDrawdown.drawdown_times " - + "cannot set both ty_time and ts_time to True." - ) - - r = radius - b = self.saturated_thickness - - sigma = self.Ss * b / self.Sy - beta = (r / b) * (r / b) * (self.Kz / self.Kr) - sqrt_beta = sqrt(beta) - - if np.isnan(pump) or pump <= 0.0: - # Return dimensionless drawdown - coef = 1.0 - else: - coef = pump / (4.0 * pi * b * self.Kr) - - # dimensionless well screen top - dd = (self.saturated_thickness + self.bottom - self.well_top) / b - # dimensionless well screen bottom - ld = (self.saturated_thickness + self.bottom - self.well_bot) / b - - # Solution must be in dimensionless time with respect to Ss; - # ts = kr*b*t/(Ss*b*r^2) - if ty_time: - ts_list = self.ty2ts(times) - elif ts_time: - ts_list = times - else: - ts_list = self.time2ts(times, r) - - # distance above bottom to observation point or obs screen bottom - zt = observation_elevation - self.bottom - if observation_elevation_bot is None: - # Single Point Observation - zd = zt / b # dimensionless elevation of observation point - neuman1974_integral = self.neuman1974_integral1 - obs_arg = (zd,) - else: - # distance above bottom to observation screen top - zb = observation_elevation_bot - self.bottom - # dimensionless elevation of observation screen interval - ztd, zbd = zt / b, zb / b - # dz = 1 / (zt - zb) -> implied in the - # modified u0 and uN functions - neuman1974_integral = self.neuman1974_integral2 - obs_arg = (zbd, ztd) - - s = [] # drawdown, one to one match with times - nstp = len(ts_list) - for stp, ts in enumerate(ts_list): - if show_progress: - print( - f"Solving {stp+1:4d} of {nstp}; " - + f"time = {self.ts2time(ts, r)}", - end="", - ) - - args = (sigma, beta, sqrt_beta, ld, dd, ts, *obs_arg, u_n_rtol) - sol = 0.0 - y0, y1 = 0.0, 0.0 - mxdelt = 0.0 - - j0_roots = jn_zeros(0, bessel_roots) / sqrt_beta - jr0 = 0 - jr1 = j0_roots.size - - converged = 0 - bessel_loop_count = 0 - while converged < 3 and bessel_loop_count <= bessel_loop_limit: - if bessel_loop_count > 0: - bessel_roots *= 2 - j0_roots = jn_zeros(0, bessel_roots) / sqrt_beta - jr0, jr1 = jr1, j0_roots.size - - j0_roots_iter = np.nditer(j0_roots[jr0:jr1]) - bessel_loop_count += 1 - # Iterate over two roots to get full cycle - for j0_root in j0_roots_iter: - # First root - y0, y1 = y1, j0_root - delt1 = quad( - neuman1974_integral, - y0, - y1, - args, - epsabs=epsabs, - limit=quad_limit, - )[0] - # - # Second root - y0, y1 = y1, next(j0_roots_iter) - delt2 = quad( - neuman1974_integral, - y0, - y1, - args, - epsabs=epsabs, - limit=quad_limit, - )[0] - - if np.isnan(delt1) or np.isnan(delt2): - break - - sol += delt1 + delt2 - - adelt = abs(delt1 + delt2) - if adelt > mxdelt: - mxdelt = adelt - elif adelt < mxdelt * sumrtol: - converged += 1 # increment the convergence counter - # Converged if three sequential solutions (adelt) - # are less than mxdelt*sumrtol - if converged >= 3: - break - else: - converged = 0 # reset convergence counter - if sol < 0.0: - s.append(0.0) - else: - s.append(coef * sol) - - if converged < 3: - bessel_root_limit_reached.append(stp) - - if show_progress: - if converged < 3: - print(f"\ts = {s[-1]}\tbessel_loop_limit reached") - else: - print(f"\ts = {s[-1]}") - - if pump > 0.0 and as_head: - initial_head = self.bottom + self.saturated_thickness - return [initial_head - drawdown for drawdown in s] - - if len(bessel_root_limit_reached) > 0: - import warnings - - root = j0_roots[-1] - bad_times = "\n".join( - [str(times[it]) for it in bessel_root_limit_reached] - ) - warnings.warn( - f"\n\nRadialUnconfinedDrawdown.drawdown_times failed to " - + f"meet convergence sumrtol = {sumrtol}" - + "\nwithin the precalculated Bessel root solutions " - + "(convergence is evaluated at every second Bessel root).\n\n" - + "The number of Bessel roots are automatically increased " - + "up to:\n" - + f" {bessel_roots0} * 2^bessel_loop_limit\nwhere:\n" - + " bessel_loop_limit = {bessel_loop_limit}\n" - + f"resulting in {1024*2**bessel_loop_limit} roots evaluated, " - + "with the last root being {root}\n" - + f"(That is, the Neuman integral was solved form 0 to {root})" - + "\n\n" - + "You can either ignore this warning\n" - + "or to remove it attempt to increase bessel_loop_limit\n" - + "or increase sumrtol (reducing accuracy).\n\nThe following " - + "times are what triggered this warning:\n" - + bad_times - + "\n" - ) - return s - - @staticmethod - def neuman1974_integral1(y, σ, β, sqrt_β, ld, dd, ts, zd, uN_tol=1.0e-6): - """ - Solves equation 17 from - Neuman, S. P. (1974). Effect of partial penetration on flow in - unconfined aquifers considering delayed gravity response. - Water resources research, 10(2), 303-312. - """ - if y == 0.0 or ts == 0.0: - return 0.0 - - u0 = RadialUnconfinedDrawdown.u_0(σ, β, zd, ld, dd, ts, y) - - if np.isnan(u0): - u0 = 0.0 - - uN_func = RadialUnconfinedDrawdown.u_n - mxdelt = 0.0 - uN = 0.0 - for n in range(1, 25001): - delt = uN_func(σ, β, zd, ld, dd, ts, y, n) - if np.isnan(delt): - break - uN += delt - adelt = abs(delt) - if adelt > mxdelt: - mxdelt = adelt - elif adelt < mxdelt * uN_tol: - break - - return 4.0 * y * j0(y * sqrt_β) * (u0 + uN) - - @staticmethod - def gamma0(g, y, s): - """ - Gamma0 root function from equation 18 in: - Neuman, S. P. (1974). Effect of partial penetration on flow in - unconfined aquifers considering delayed gravity response. - Water resources research, 10(2), 303-312. - => Solution must be constrained by g^2 < y^2 - - To honor the constraint solution returns the absolute value - of the solution. - """ - if g >= _hyperbolic_equivalence: - # sinh ≈ cosh for large g - return s * g - (y * y - g * g) - - return s * g * sinh(g) - (y * y - g * g) * cosh(g) - - @staticmethod - def gammaN(g, y, s): - """ - GammaN root function from equation 19 in: - Neuman, S. P. (1974). Effect of partial penetration on flow in - unconfined aquifers considering delayed gravity response. - Water resources research, 10(2), 303-312. - => Solution must be constrained by (2n-1)(π/2)< g < nπ - """ - return s * g * sin(g) + (y * y + g * g) * cos(g) - - @staticmethod - def u_0(σ, β, z, l, d, ts, y): - gamma0 = RadialUnconfinedDrawdown.gamma0 - - a, b = 0.9 * y, y - try: - a, b = RadialUnconfinedDrawdown._get_bracket(gamma0, a, b, (y, σ)) - except RuntimeError: - a, b = RadialUnconfinedDrawdown._get_bracket( - gamma0, 0.0, b, (y, σ), 1000 - ) - - g = brentq(gamma0, a, b, args=(y, σ), maxiter=500, xtol=1.0e-16) - - # Check for cosh/sinh overflow - if g > _hyperbolic_max_value: - return 0.0 - - y2 = y * y - g2 = g * g - num1 = 1 - exp(-ts * β * (y2 - g2)) - num2 = cosh(g * z) - num3 = sinh(g * (1 - d)) - sinh(g * (1 - l)) - den1 = y2 + (1 + σ) * g2 - ((y2 - g2) ** 2) / σ - den2 = cosh(g) - den3 = (l - d) * sinh(g) - # num1*num2*num3 / (den1*den2*den3) - return (num1 / den1) * (num2 / den2) * (num3 / den3) - - @staticmethod - def u_n(σ, β, z, l, d, ts, y, n): - gammaN = RadialUnconfinedDrawdown.gammaN - - a, b = (2 * n - 1) * (pi / 2.0), n * pi - try: - a, b = RadialUnconfinedDrawdown._get_bracket(gammaN, a, b, (y, σ)) - except RuntimeError: - a, b = RadialUnconfinedDrawdown._get_bracket( - gammaN, a, b, (y, σ), 1000 - ) - - g = brentq(gammaN, a, b, args=(y, σ), maxiter=500, xtol=1.0e-16) - - y2 = y * y - g2 = g * g - num1 = 1 - exp(-ts * β * (y2 + g2)) - num2 = cos(g * z) - num3 = sin(g * (1 - d)) - sin(g * (1 - l)) - den1 = y2 - (1 + σ) * g2 - ((y2 + g2) ** 2) / σ - den2 = cos(g) - den3 = (l - d) * sin(g) - return num1 * num2 * num3 / (den1 * den2 * den3) - - @staticmethod - def neuman1974_integral2( - y, σ, β, sqrt_β, ld, dd, ts, z1, z2, uN_tol=1.0e-10 - ): - """ - Solves equation 20 from - Neuman, S. P. (1974). Effect of partial penetration on flow in - unconfined aquifers considering delayed gravity response. - Water resources research, 10(2), 303-312. - """ - if y == 0.0 or ts == 0.0: - return 0.0 - - u0 = RadialUnconfinedDrawdown.u_0_z1z2(σ, β, z1, z2, ld, dd, ts, y) - - uN_func = RadialUnconfinedDrawdown.u_n_z1z2 - mxdelt = 0.0 - uN = 0.0 - for n in range(1, 10001): - delt = uN_func(σ, β, z1, z2, ld, dd, ts, y, n) - uN += delt - adelt = abs(delt) - if adelt > mxdelt: - mxdelt = adelt - elif adelt < mxdelt * uN_tol: - break - - return 4.0 * y * j0(y * sqrt_β) * (u0 + uN) - - @staticmethod - def u_0_z1z2(σ, β, z1, z2, l, d, ts, y): - gamma0 = RadialUnconfinedDrawdown.gamma0 - - a, b = 0.9 * y, y - try: - a, b = RadialUnconfinedDrawdown._get_bracket(gamma0, a, b, (y, σ)) - except RuntimeError: - a, b = RadialUnconfinedDrawdown._get_bracket( - gamma0, 0.0, b, (y, σ), 1000 - ) - - g = brentq(gamma0, a, b, args=(y, σ), maxiter=500, xtol=1.0e-16) - - # Check for cosh/sinh overflow - if g > _hyperbolic_max_value: - return 0.0 - - y2 = y * y - g2 = g * g - num1 = 1 - exp(-ts * β * (y2 - g2)) - num2 = sinh(g * z2) - sinh(g * z1) - num3 = sinh(g * (1 - d)) - sinh(g * (1 - l)) - den1 = (y2 + (1 + σ) * g2 - ((y2 - g2) ** 2) / σ) * (z2 - z1) * g - den2 = cosh(g) - den3 = (l - d) * sinh(g) - # num1*num2*num3 / (den1*den2*den3) - return (num1 / den1) * (num2 / den2) * (num3 / den3) - - @staticmethod - def u_n_z1z2(σ, β, z1, z2, l, d, ts, y, n): - gammaN = RadialUnconfinedDrawdown.gammaN - - a, b = (2 * n - 1) * (pi / 2.0), n * pi - try: - a, b = RadialUnconfinedDrawdown._get_bracket(gammaN, a, b, (y, σ)) - except RuntimeError: - a, b = RadialUnconfinedDrawdown._get_bracket( - gammaN, a, b, (y, σ), 1000 - ) - - g = brentq(gammaN, a, b, args=(y, σ), maxiter=500, xtol=1.0e-16) - - y2 = y * y - g2 = g * g - num1 = 1 - exp(-ts * β * (y2 + g2)) - num2 = sin(g * z2) - sin(g * z1) - num3 = sin(g * (1 - d)) - sin(g * (1 - l)) - den1 = y2 - (1 + σ) * g2 - ((y2 + g2) ** 2) / σ - den2 = cos(g) * (z2 - z1) * g - den3 = (l - d) * sin(g) - return num1 * num2 * num3 / (den1 * den2 * den3) - - def time2ty(self, time, radius): - # dimensionless time with respect to Sy - if hasattr(time, "__iter__"): - # can iterate to get multiple times - return [ - self.Kr - * self.saturated_thickness - * t - / (self.Sy * radius * radius) - for t in time - ] - return ( - self.Kr - * self.saturated_thickness - * time - / (self.Sy * radius * radius) - ) - - def time2ts(self, time, radius): - # dimensionless time with respect to Ss - if hasattr(time, "__iter__"): - # can iterate to get multiple times - return [self.Kr * t / (self.Ss * radius * radius) for t in time] - return self.Kr * time / (self.Ss * radius * radius) - - def ty2time(self, ty, radius): - # dimensionless time with respect to Sy - if hasattr(ty, "__iter__"): - # can iterate to get multiple times - return [ - t - * self.Sy - * radius - * radius - / (self.Kr * self.saturated_thickness) - for t in ty - ] - return ( - ty - * self.Sy - * radius - * radius - / (self.Kr * self.saturated_thickness) - ) - - def ts2time(self, ts, radius): # dimensionless time with respect to Ss - if hasattr(ts, "__iter__"): # can iterate to get multiple times - return [t * self.Ss * radius * radius / self.Kr for t in ts] - return ts * self.Ss * radius * radius / self.Kr - - def ty2ts(self, ty): - if hasattr(ty, "__iter__"): - # can iterate to get multiple times - return [ - t * self.Sy / (self.Ss * self.saturated_thickness) for t in ty - ] - return ty * self.Sy / (self.Ss * self.saturated_thickness) - - def drawdown2unitless(self, s, pump): - # dimensionless drawdown - return 4 * pi * self.Kr * self.saturated_thickness * s / pump - - def unitless2drawdown(self, s, pump): - # drawdown - return pump * s / (4 * pi * self.Kr * self.saturated_thickness) - - @staticmethod - def _float_or_none(val): - if val is not None: - return float(val) - return None - - @staticmethod - def _get_bracket(func, a, b, arg=(), internal_search_split=100): - """ - Given initial range [a, b], search within the range for - root finding brackets. - That is, return [a, b] that results in f(a) * f(b) < 0. - """ - if a > b: - a, b = b, a - - f1 = func(a, *arg) - f2 = func(b, *arg) - - if f1 * f2 <= 0.0: - return a, b - - # same sign, search within for sign change - delt = abs(b - a) / internal_search_split - a -= delt - for _ in range(internal_search_split): - a += delt - f1 = func(a, *arg) - if f1 * f2 <= 0.0: - return a, b - - raise RuntimeError( - "get_bracket: failed to find bracket interval with opposite " - + f"signs, that is: f(a)*f(b) < 0 for func: {func}" - ) - - -if __name__ == "__main__": - # Example validation using Figure 2 from - # Neuman, S. P. (1974). Effect of partial penetration on flow in - # unconfined aquifers considering delayed gravity response. - # Water resources research, 10(2), 303-312. - # (no units are provided because result is dimensionless) - - b = 100 # saturated thickness - bottom = 0.0 - well_top = b + bottom # Top of well is top of aquifer - well_bot = (bottom + b) - 0.2 * b # Well bottom is 20% below aquifer top - ss = 1.0e-05 - sy = 0.1 - kz = 10.0 - kr = kz - kd = kz / kr - r = 0.6 * b / (kd**0.5) - z = 0.85 * b - # beta = (r / b) * (r / b) * (kz / kr) # = .36 - pump = 1000.0 - - Neuman1974 = RadialUnconfinedDrawdown( - bottom_elevation=bottom, - hydraulic_conductivity_radial=kr, - hydraulic_conductivity_vertical=kz, - specific_storage=ss, - specific_yield=sy, - well_screen_elevation_top=well_top, - well_screen_elevation_bottom=well_bot, - saturated_thickness=b, - ) - - # Convert ts to regular time - times = [Neuman1974.ts2time(ts, r) for ts in [1.0, 10.0, 100.0]] - - s1 = Neuman1974.drawdown(pump, times[0], r, z) # ≈ 0.0092 - s2 = Neuman1974.drawdown(pump, times[1], r, z) # ≈ 0.0156 - s3 = Neuman1974.drawdown(pump, times[2], r, z) # ≈ 0.0733 - assert int(s1 * 10000) / 10000 == 0.0092 - assert int(s2 * 10000) / 10000 == 0.0156 - assert int(s3 * 10000) / 10000 == 0.0733 - - sd1 = Neuman1974.drawdown_dimensionless(times[0], r, z) # ≈ 0.116 - sd2 = Neuman1974.drawdown_dimensionless(times[1], r, z) # ≈ 0.196 - sd3 = Neuman1974.drawdown_dimensionless(times[2], r, z) # ≈ 0.921 - assert int(sd1 * 1000) / 1000 == 0.116 - assert int(sd2 * 1000) / 1000 == 0.196 - assert int(sd3 * 1000) / 1000 == 0.921 - - ts_time = np.logspace(-2, 3, 20) - times = [ - Neuman1974.ts2time(ts, r) for ts in ts_time - ] # Convert ts to regular time - # - # pump < 0 ⇒ dimensionless drawdown - sd = Neuman1974.drawdown( - -1.0, ts_time, r, z, ts_time=True, show_progress=True - ) - s = [pump * dd / (4 * pi * kr * b) for dd in sd] - - # From digitizing Figure 2 from Neuman 1974 - ts_true = [ - 0.089687632, - 0.091225896, - 0.093981606, - 0.099322056, - 0.105413236, - 0.110460061, - 0.116736935, - 0.123370444, - 0.134891669, - 0.149382, - 0.166841577, - 0.18087786, - 0.197769543, - 0.213498224, - 0.236432628, - 0.264066571, - 0.292433127, - 0.322472937, - 0.363238602, - 0.403972132, - 0.466797384, - 0.541691087, - 0.639382207, - 0.804387135, - 0.994911098, - 1.300487774, - 1.561350982, - 1.796516091, - 2.289153019, - 2.929308713, - 3.623130394, - 4.61665772, - 5.637774401, - 7.338096581, - 8.735440623, - 10.57723024, - 12.37907737, - 14.18313516, - 17.24669923, - 19.67631134, - 23.32377322, - 25.82925931, - 29.59349474, - 33.47660549, - 39.34627872, - 45.46538164, - 53.21043662, - 58.92640751, - 70.14741888, - 80.02936879, - 93.66241967, - 107.7695188, - 122.4298652, - 139.084521, - 158.0047806, - 182.5775622, - 214.590086, - 279.309292, - 362.0049489, - 469.1848385, - 615.9033614, - 808.5008911, - 1000.0, - ] - sd_true = [ - 0.010442884, - 0.011437831, - 0.012691503, - 0.014834303, - 0.017114926, - 0.019239359, - 0.021627491, - 0.024630197, - 0.028664212, - 0.0336493, - 0.040018293, - 0.044404599, - 0.049271676, - 0.053966039, - 0.060141169, - 0.065302682, - 0.071524401, - 0.075998143, - 0.082163706, - 0.087302955, - 0.094795544, - 0.1007249, - 0.107489912, - 0.114709285, - 0.121884175, - 0.130635227, - 0.134076495, - 0.138206013, - 0.142462775, - 0.150067308, - 0.156035929, - 0.164364984, - 0.173890601, - 0.187997964, - 0.202370903, - 0.214099117, - 0.232473715, - 0.245946509, - 0.274089351, - 0.296325619, - 0.323154565, - 0.350888753, - 0.376081747, - 0.419115531, - 0.475240418, - 0.529620599, - 0.585129692, - 0.654916226, - 0.749082106, - 0.849394041, - 0.958973727, - 1.073347435, - 1.206579326, - 1.32153628, - 1.460045395, - 1.620077321, - 1.805457382, - 2.156510354, - 2.488051985, - 2.833486709, - 3.226880699, - 3.580571026, - 3.804531016, - ] - fig = plt.figure(figsize=(6, 4)) - ax = fig.add_subplot() - ax.set_title("Neuman 1974 Figure 2 from Python") - ax.set_xlabel("$t_s$ (-)") - ax.set_ylabel("$s_d$ (-)") - # plt.loglog(ts_true, sd_true) - plt.loglog( - ts_true, sd_true, "-k", markerfacecolor="none", label="Neuman Solution" - ) - plt.loglog( - ts_time, sd, "ob", markerfacecolor="none", label="Python Solution" - ) - ax.set_xlim(0.01, 1000.0) - ax.set_ylim(0.01, 10.0) - ax.legend() # loc=2 - ax.grid(visible=True, which="major", axis="both") - ax.set_zorder(2) - ax.set_facecolor("none") - - plt.tight_layout() - plt.show() - pass diff --git a/common/sfr_uzf_mvr_support_funcs.py b/common/sfr_uzf_mvr_support_funcs.py deleted file mode 100644 index 239bef0e5..000000000 --- a/common/sfr_uzf_mvr_support_funcs.py +++ /dev/null @@ -1,430 +0,0 @@ -import numpy as np - -# Defining a function that builds the new MF6 SFR connection information using -# original SFR input. This is a generalized function that can be used to -# convert MF2K5-based model to the new MF6 format. Currently applied to the -# Sagehen and modsim models - - -def gen_mf6_sfr_connections(orig_seg, orig_rch): - conns = [] - for i in np.arange(0, len(orig_seg)): - tup = orig_seg[i] - segid = tup[0] - ioutseg = tup[2] - iupseg = tup[3] - - # Get all reaches associated with segment - # Find an element in a list of tuples - allrchs = [item for item in orig_rch if item[3] == segid] - - # Loop through allrchs and generate list of connections - for rchx in allrchs: - # rchx will be a tuple - upconn = [] - dnconn = [] - - if rchx[4] == 1: # checks if first rch of segment - # Collect all segs that dump to the current one (there may not - # be any) - dumpersegs = [item for item in orig_seg if item[2] == segid] - # For every seg that outflows to current, set last reach of it - # as an upstream connection - for dumper in dumpersegs: - dumper_seg_id = dumper[0] - rch_cnt = len( - [item for item in orig_rch if item[3] == dumper_seg_id] - ) - lastrch = [ - item - for item in orig_rch - if item[3] == dumper_seg_id and item[4] == rch_cnt - ] - idx = orig_rch.index(lastrch[0]) - upconn.append(int(idx)) - - # Current reach is the most upstream reach for current segment - if iupseg == 0: - pass - elif iupseg > 0: # Lake connections, signified with negative - # numbers, aren't handled here - iupseg_rchs = [item for item in orig_rch if item[3] == iupseg] - # Get the index of the last reach of the segement that was - # the upstream segment in the orig sfr file - idx = orig_rch.index(iupseg_rchs[len(iupseg_rchs) - 1]) # - upconn.append(idx) - - # Even if the first reach of a segement, it will have an outlet - # either the next reach in the segment, or first reach of - # outseg, which should be taken care of below - if len(allrchs) > 1: - idx = orig_rch.index(rchx) - # adjust idx for 0-based and increment to next item in list - dnconn.append(int(idx + 1) * -1) - - elif rchx[4] > 1 and not rchx[4] == len(allrchs): - # Current reach is 'interior' on the original segment and - # therefore should only have 1 upstream & 1 downstream segement - idx = orig_rch.index(rchx) - # B/c 0-based, idx will already be incremented by -1 - upconn.append(int(idx - 1)) - # adjust idx for 0-based and increment to next item in list - dnconn.append(int(idx + 1) * -1) # all downstream connections - # are negative in MF6 - - if rchx[4] == len(allrchs): - # If the last reach in a multi-reach segment, always need to - # account for the reach immediately upstream (single reach segs - # dealt with above), unless of course we're dealing with a - # single reach segment like in the case of a spillway from a lk - if len(allrchs) != 1: - idx = orig_rch.index(rchx) - # B/c 0-based, idx will already be incremented by -1 - upconn.append(int(idx - 1)) - - # Current reach is last reach in segment and may have multiple - # downstream connections, particular when dealing with - # diversions. - if ioutseg == 0: - pass - elif ioutseg > 0: # Lake connections, signified with - # negative numbers, aren't handled here - idnseg_rchs = [ - item for item in orig_rch if item[3] == ioutseg and item[4] == 1 - ] - idx = orig_rch.index(idnseg_rchs[0]) - # adjust idx for 0-based and increment to next item in list - dnconn.append(int(idx) * -1) - - # In addition to ioutseg, look for all segments that may have - # the current segment as their iupseg - possible_divs = [item for item in orig_seg if item[3] == rchx[3]] - for segx in possible_divs: - # Next, peel out all first reach for any segments listed in - # possible_divs - first_rchs = [ - item for item in orig_rch if item[3] == segx[0] and item[4] == 1 - ] - for firstx in first_rchs: - idx = orig_rch.index(firstx) - # adjust idx for 0-based & increment to nxt itm in list - dnconn.append(int(idx) * -1) - - # Append the collection of upconn & dnconn as an entry in a list - idx = orig_rch.index(rchx) - # Adjust current index for 0-based - conns.append([idx] + upconn + dnconn) - - return conns - - -def determine_runoff_conns_4mvr(pth, elev_arr, ibnd, orig_rch, nrow, ncol): - # Get the sfr information stored in a companion script - sfr_dat = orig_rch.copy() - sfrlayout = np.zeros_like(ibnd) - for i, rchx in enumerate(sfr_dat): - row = rchx[1] - col = rchx[2] - sfrlayout[row - 1, col - 1] = i + 1 - - sfrlayout_new = sfrlayout.copy() - - stop_candidate = False - - for i in np.arange(0, nrow): - for j in np.arange(0, ncol): - # Check to ensure current cell is active - if ibnd[i, j] == 0: - continue - - # Check to make sure it is not a stream cell - if not sfrlayout[i, j] == 0: - continue - - # Recursively trace path by steepest decent back to a stream - curr_i = i - curr_j = j - - sfrlayout_conn_candidate_elev = 10000.0 - while True: - direc = 0 - min_elev = elev_arr[curr_i, curr_j] - - # Look straight left - if curr_j > 0: - if ( - not sfrlayout[curr_i, curr_j - 1] == 0 - and not ibnd[curr_i, curr_j - 1] == 0 - ): # Step in if neighbor is a stream cell - if elev_arr[curr_i, curr_j - 1] > 0 and ( - elev_arr[curr_i, curr_j - 1] < elev_arr[curr_i, curr_j] - and elev_arr[curr_i, curr_j - 1] - < sfrlayout_conn_candidate_elev - ): - sfrlayout_conn_candidate = sfrlayout[curr_i, curr_j - 1] - sfrlayout_conn_candidate_elev = elev_arr[curr_i, curr_j - 1] - stop_candidate = True - - elif ( - not elev_arr[curr_i, curr_j - 1] == 0 - and not ibnd[curr_i, curr_j - 1] == 0 - ): # Step here if neighbor is not an sfr cell - if ( - elev_arr[curr_i, curr_j - 1] < elev_arr[curr_i, curr_j] - and elev_arr[curr_i, curr_j - 1] < min_elev - ): - elevcm1 = elev_arr[curr_i, curr_j - 1] - min_elev = elevcm1 - direc = 2 - - # Look up and left - if curr_j > 0 and curr_i > 0: - if ( - not sfrlayout[curr_i - 1, curr_j - 1] == 0 - and not ibnd[curr_i - 1, curr_j - 1] == 0 - ): # Step in if neighbor is a stream cell - if elev_arr[curr_i - 1, curr_j - 1] > 0 and ( - elev_arr[curr_i - 1, curr_j - 1] < elev_arr[curr_i, curr_j] - and elev_arr[curr_i - 1, curr_j - 1] - < sfrlayout_conn_candidate_elev - ): - sfrlayout_conn_candidate = sfrlayout[curr_i - 1, curr_j - 1] - sfrlayout_conn_candidate_elev = elev_arr[ - curr_i - 1, curr_j - 1 - ] - stop_candidate = True - - elif ( - not elev_arr[curr_i - 1, curr_j - 1] == 0 - and not ibnd[curr_i - 1, curr_j - 1] == 0 - ): # Step here if neighbor is not an sfr cell - if ( - elev_arr[curr_i - 1, curr_j - 1] < elev_arr[curr_i, curr_j] - and elev_arr[curr_i - 1, curr_j - 1] < min_elev - ): - elevrm1cm1 = elev_arr[curr_i - 1, curr_j - 1] - min_elev = elevrm1cm1 - direc = 5 - - # Look straight right - if curr_j < ncol - 1: - if ( - not sfrlayout[curr_i, curr_j + 1] == 0 - and not ibnd[curr_i, curr_j + 1] == 0 - ): # Step in if neighbor is a stream cell - if elev_arr[curr_i, curr_j + 1] > 0 and ( - elev_arr[curr_i, curr_j + 1] < elev_arr[curr_i, curr_j] - and elev_arr[curr_i, curr_j + 1] - < sfrlayout_conn_candidate_elev - ): - sfrlayout_conn_candidate = sfrlayout[curr_i, curr_j + 1] - sfrlayout_conn_candidate_elev = elev_arr[curr_i, curr_j + 1] - stop_candidate = True - - elif ( - not elev_arr[curr_i, curr_j + 1] == 0 - and not ibnd[curr_i, curr_j + 1] == 0 - ): # Step here if neighbor is not an sfr cell - if ( - elev_arr[curr_i, curr_j + 1] < elev_arr[curr_i, curr_j] - and elev_arr[curr_i, curr_j + 1] < min_elev - ): - elevcm1 = elev_arr[curr_i, curr_j + 1] - min_elev = elevcm1 - direc = 4 - - # Look straight right and down - if curr_i < nrow - 1 and curr_j < ncol - 1: - if ( - not sfrlayout[curr_i + 1, curr_j + 1] == 0 - and not ibnd[curr_i + 1, curr_j + 1] == 0 - ): # Step in if neighbor is a stream cell - if elev_arr[curr_i + 1, curr_j + 1] > 0 and ( - elev_arr[curr_i + 1, curr_j + 1] < elev_arr[curr_i, curr_j] - and elev_arr[curr_i + 1, curr_j + 1] - < sfrlayout_conn_candidate_elev - ): - sfrlayout_conn_candidate = sfrlayout[curr_i + 1, curr_j + 1] - sfrlayout_conn_candidate_elev = elev_arr[ - curr_i + 1, curr_j + 1 - ] - stop_candidate = True - - elif ( - not elev_arr[curr_i + 1, curr_j + 1] == 0 - and not ibnd[curr_i + 1, curr_j + 1] == 0 - ): # Step here if neighbor is not an sfr cell - if ( - elev_arr[curr_i + 1, curr_j + 1] < elev_arr[curr_i, curr_j] - and elev_arr[curr_i + 1, curr_j + 1] < min_elev - ): - elevrp1cp1 = elev_arr[curr_i + 1, curr_j + 1] - min_elev = elevrp1cp1 - direc = 7 - - # Look straight up - if curr_i > 0: - if ( - not sfrlayout[curr_i - 1, curr_j] == 0 - and not ibnd[curr_i - 1, curr_j] == 0 - ): # Step in if neighbor is a stream cell - if elev_arr[curr_i - 1, curr_j] > 0 and ( - elev_arr[curr_i - 1, curr_j] < elev_arr[curr_i, curr_j] - and elev_arr[curr_i - 1, curr_j] - < sfrlayout_conn_candidate_elev - ): - sfrlayout_conn_candidate = sfrlayout[curr_i - 1, curr_j] - sfrlayout_conn_candidate_elev = elev_arr[curr_i - 1, curr_j] - stop_candidate = True - - elif ( - not elev_arr[curr_i - 1, curr_j] == 0 - and not ibnd[curr_i - 1, curr_j] == 0 - ): # Step here if neighbor is not an sfr cell - if ( - elev_arr[curr_i - 1, curr_j] < elev_arr[curr_i, curr_j] - and elev_arr[curr_i - 1, curr_j] < min_elev - ): - elevcm1 = elev_arr[curr_i - 1, curr_j] - min_elev = elevcm1 - direc = 3 - - # Look up and right - if curr_i > 0 and curr_j < ncol - 1: - if ( - not sfrlayout[curr_i - 1, curr_j + 1] == 0 - and not ibnd[curr_i - 1, curr_j + 1] == 0 - ): # Step in if neighbor is a stream cell - if elev_arr[curr_i - 1, curr_j + 1] > 0 and ( - elev_arr[curr_i - 1, curr_j + 1] < elev_arr[curr_i, curr_j] - and elev_arr[curr_i - 1, curr_j + 1] - < sfrlayout_conn_candidate_elev - ): - sfrlayout_conn_candidate = sfrlayout[curr_i - 1, curr_j + 1] - sfrlayout_conn_candidate_elev = elev_arr[ - curr_i - 1, curr_j + 1 - ] - stop_candidate = True - - elif ( - not elev_arr[curr_i - 1, curr_j + 1] == 0 - and not ibnd[curr_i - 1, curr_j + 1] == 0 - ): # Step here if neighbor is not an sfr cell - if ( - elev_arr[curr_i - 1, curr_j + 1] < elev_arr[curr_i, curr_j] - and elev_arr[curr_i - 1, curr_j + 1] < min_elev - ): - elevrm1cp1 = elev_arr[curr_i - 1, curr_j + 1] - min_elev = elevrm1cp1 - direc = 6 - - # Look straight down - if curr_i < nrow - 1: - if ( - not sfrlayout[curr_i + 1, curr_j] == 0 - and not ibnd[curr_i + 1, curr_j] == 0 - ): # Step in if neighbor is a stream cell - if elev_arr[curr_i + 1, curr_j] > 0 and ( - elev_arr[curr_i + 1, curr_j] < elev_arr[curr_i, curr_j] - and elev_arr[curr_i + 1, curr_j] - < sfrlayout_conn_candidate_elev - ): - sfrlayout_conn_candidate = sfrlayout[curr_i + 1, curr_j] - sfrlayout_conn_candidate_elev = elev_arr[curr_i + 1, curr_j] - stop_candidate = True - - elif ( - not elev_arr[curr_i + 1, curr_j] == 0 - and not ibnd[curr_i + 1, curr_j] == 0 - ): # Step here if neighbor is not an sfr cell - if ( - elev_arr[curr_i + 1, curr_j] < elev_arr[curr_i, curr_j] - and elev_arr[curr_i + 1, curr_j] < min_elev - ): - elevrp1 = elev_arr[curr_i + 1, curr_j] - min_elev = elevrp1 - direc = 1 - - # Look down and left - if curr_i < nrow - 1 and curr_j > 0: - if ( - not sfrlayout[curr_i + 1, curr_j - 1] == 0 - and not ibnd[curr_i + 1, curr_j - 1] == 0 - ): # Step in if neighbor is a stream cell - if elev_arr[curr_i + 1, curr_j - 1] > 0 and ( - elev_arr[curr_i + 1, curr_j - 1] < elev_arr[curr_i, curr_j] - and elev_arr[curr_i + 1, curr_j - 1] - < sfrlayout_conn_candidate_elev - ): - sfrlayout_conn_candidate = sfrlayout[curr_i + 1, curr_j - 1] - sfrlayout_conn_candidate_elev = elev_arr[ - curr_i + 1, curr_j - 1 - ] - stop_candidate = True - - elif ( - not elev_arr[curr_i + 1, curr_j - 1] == 0 - and not ibnd[curr_i + 1, curr_j - 1] == 0 - ): # Step here if neighbor is not an sfr cell - if ( - elev_arr[curr_i + 1, curr_j - 1] < elev_arr[curr_i, curr_j] - and elev_arr[curr_i + 1, curr_j - 1] < min_elev - ): - elevrp1cm1 = elev_arr[curr_i + 1, curr_j - 1] - min_elev = elevrp1cm1 - direc = 8 - - # if stop candidate found, don't move the cell indices - if not stop_candidate: - # Direc corresponds to: - # |---------------------- - # | 5 | 3 | 6 | - # |---------------------- - # | 2 | cur_cel | 4 | - # |---------------------- - # | 8 | 1 | 7 | - # |---------------------- - if direc == 0: - break - elif direc == 1: - curr_i += 1 - elif direc == 2: - curr_j -= 1 - elif direc == 3: - curr_i -= 1 - elif direc == 4: - curr_j += 1 - elif direc == 5: - curr_i -= 1 - curr_j -= 1 - elif direc == 6: - curr_i -= 1 - curr_j += 1 - elif direc == 7: - curr_i += 1 - curr_j += 1 - elif direc == 8: - curr_i += 1 - curr_j -= 1 - - if stop_candidate: - sfrlayout_new[i, j] = sfrlayout_conn_candidate - stop_candidate = False - break # Bust out of while loop - elif not stop_candidate: - # Check if encountered ibnd == 0, which may be a lake or - # boundary that drains out of model - if ibnd[curr_i, curr_j] == 0: - # This condition is dealt with after looping through - # all cells, see comment "Last step is set..." - break - pass # Commence next downstream cell search - - # Last step is set the 0's in the vicinity of the lake equal to the - # negative of the lake connection - for i in np.arange(0, nrow): - for j in np.arange(0, ncol): - if sfrlayout_new[i, j] == 0 and ibnd[i, j] > 0: - sfrlayout_new[i, j] = -1 - - return sfrlayout_new diff --git a/data/ex-gwf-sagehen/sfr_static.py b/data/ex-gwf-sagehen/sfr_static.py deleted file mode 100644 index f4dd3eaa5..000000000 --- a/data/ex-gwf-sagehen/sfr_static.py +++ /dev/null @@ -1,1123 +0,0 @@ -# 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, -orig_seg = [ - (1, 1, 9, 0, 0.00, 0.0, 0.0, 0, 0.04, "origSeg1"), - (2, 1, 10, 0, 0.00, 0.0, 0.0, 0, 0.04, "origSeg2"), - (3, 1, 9, 0, 0.00, 0.0, 0.0, 0, 0.04, "origSeg3"), - (4, 1, 11, 0, 0.00, 0.0, 0.0, 0, 0.04, "origSeg4"), - (5, 1, 15, 0, 0.00, 0.0, 0.0, 0, 0.04, "origSeg5"), - (6, 1, 13, 0, 0.00, 0.0, 0.0, 0, 0.04, "origSeg6"), - (7, 1, 12, 0, 0.00, 0.0, 0.0, 0, 0.04, "origSeg7"), - (8, 1, 14, 0, 0.00, 0.0, 0.0, 0, 0.04, "origSeg8"), - (9, 1, 10, 0, 0.00, 0.0, 0.0, 0, 0.04, "origSeg9"), - (10, 1, 11, 0, 0.00, 0.0, 0.0, 0, 0.04, "origSeg10"), - (11, 1, 12, 0, 0.00, 0.0, 0.0, 0, 0.04, "origSeg11"), - (12, 1, 13, 0, 0.00, 0.0, 0.0, 0, 0.04, "origSeg12"), - (13, 1, 14, 0, 0.00, 0.0, 0.0, 0, 0.04, "origSeg13"), - (14, 1, 15, 0, 0.00, 0.0, 0.0, 0, 0.04, "origSeg14"), - (15, 1, 0, 0, 0.00, 0.0, 0.0, 0, 0.04, "origSeg15"), -] - -orig_rch = [ - (1, 45, 9, 1, 1), - (1, 44, 9, 1, 2), - (1, 43, 9, 1, 3), - (1, 43, 10, 1, 4), - (1, 42, 10, 1, 5), - (1, 42, 11, 1, 6), - (1, 42, 12, 1, 7), - (1, 41, 12, 1, 8), - (1, 41, 13, 1, 9), - (1, 40, 13, 1, 10), - (1, 40, 14, 1, 11), - (1, 39, 14, 1, 12), - (1, 39, 15, 1, 13), - (1, 39, 16, 1, 14), - (1, 39, 17, 1, 15), - (1, 38, 17, 1, 16), - (1, 38, 18, 1, 17), - (1, 38, 19, 1, 18), - (1, 38, 20, 1, 19), - (1, 38, 21, 1, 20), - (1, 38, 22, 1, 21), - (1, 38, 23, 1, 22), - (1, 37, 24, 1, 23), - (1, 37, 25, 1, 24), - (1, 36, 25, 1, 25), - (1, 36, 26, 1, 26), - (1, 35, 26, 1, 27), - (1, 35, 27, 1, 28), - (1, 36, 28, 1, 29), - (1, 36, 29, 1, 30), - (1, 36, 30, 1, 31), - (1, 36, 31, 1, 32), - (1, 36, 32, 1, 33), - (1, 35, 32, 1, 34), - (1, 35, 33, 1, 35), - (1, 34, 33, 1, 36), - (1, 34, 34, 1, 37), - (1, 33, 34, 1, 38), - (1, 33, 35, 1, 39), - (1, 32, 35, 1, 40), - (1, 32, 36, 1, 41), - (1, 32, 37, 1, 42), - (1, 53, 37, 2, 1), - (1, 52, 37, 2, 2), - (1, 52, 38, 2, 3), - (1, 51, 38, 2, 4), - (1, 50, 38, 2, 5), - (1, 50, 39, 2, 6), - (1, 49, 39, 2, 7), - (1, 48, 39, 2, 8), - (1, 47, 39, 2, 9), - (1, 47, 40, 2, 10), - (1, 46, 40, 2, 11), - (1, 45, 40, 2, 12), - (1, 45, 39, 2, 13), - (1, 44, 39, 2, 14), - (1, 43, 38, 2, 15), - (1, 42, 38, 2, 16), - (1, 41, 38, 2, 17), - (1, 40, 38, 2, 18), - (1, 40, 39, 2, 19), - (1, 39, 39, 2, 20), - (1, 38, 39, 2, 21), - (1, 37, 40, 2, 22), - (1, 36, 40, 2, 23), - (1, 36, 41, 2, 24), - (1, 35, 41, 2, 25), - (1, 34, 41, 2, 26), - (1, 33, 41, 2, 27), - (1, 32, 41, 2, 28), - (1, 31, 42, 2, 29), - (1, 31, 33, 3, 1), - (1, 31, 34, 3, 2), - (1, 31, 35, 3, 3), - (1, 31, 36, 3, 4), - (1, 31, 37, 3, 5), - (1, 48, 48, 4, 1), - (1, 47, 48, 4, 2), - (1, 46, 48, 4, 3), - (1, 46, 47, 4, 4), - (1, 45, 47, 4, 5), - (1, 44, 47, 4, 6), - (1, 43, 47, 4, 7), - (1, 42, 47, 4, 8), - (1, 41, 47, 4, 9), - (1, 41, 48, 4, 10), - (1, 40, 48, 4, 11), - (1, 39, 48, 4, 12), - (1, 38, 47, 4, 13), - (1, 37, 47, 4, 14), - (1, 36, 48, 4, 15), - (1, 35, 48, 4, 16), - (1, 35, 49, 4, 17), - (1, 34, 49, 4, 18), - (1, 34, 50, 4, 19), - (1, 33, 50, 4, 20), - (1, 55, 72, 5, 1), - (1, 54, 72, 5, 2), - (1, 53, 72, 5, 3), - (1, 52, 72, 5, 4), - (1, 51, 72, 5, 5), - (1, 50, 73, 5, 6), - (1, 49, 73, 5, 7), - (1, 48, 73, 5, 8), - (1, 48, 74, 5, 9), - (1, 47, 74, 5, 10), - (1, 46, 75, 5, 11), - (1, 45, 75, 5, 12), - (1, 45, 76, 5, 13), - (1, 44, 76, 5, 14), - (1, 45, 62, 6, 1), - (1, 44, 62, 6, 2), - (1, 43, 62, 6, 3), - (1, 43, 63, 6, 4), - (1, 42, 63, 6, 5), - (1, 41, 63, 6, 6), - (1, 40, 63, 6, 7), - (1, 24, 55, 7, 1), - (1, 25, 55, 7, 2), - (1, 25, 56, 7, 3), - (1, 26, 56, 7, 4), - (1, 27, 56, 7, 5), - (1, 28, 57, 7, 6), - (1, 29, 57, 7, 7), - (1, 30, 57, 7, 8), - (1, 31, 57, 7, 9), - (1, 32, 57, 7, 10), - (1, 33, 57, 7, 11), - (1, 33, 58, 7, 12), - (1, 34, 58, 7, 13), - (1, 34, 59, 7, 14), - (1, 35, 59, 7, 15), - (1, 36, 59, 7, 16), - (1, 37, 60, 7, 17), - (1, 23, 71, 8, 1), - (1, 24, 71, 8, 2), - (1, 25, 71, 8, 3), - (1, 26, 71, 8, 4), - (1, 27, 72, 8, 5), - (1, 27, 73, 8, 6), - (1, 28, 73, 8, 7), - (1, 29, 73, 8, 8), - (1, 30, 73, 8, 9), - (1, 31, 73, 8, 10), - (1, 32, 73, 8, 11), - (1, 33, 73, 8, 12), - (1, 34, 73, 8, 13), - (1, 34, 74, 8, 14), - (1, 35, 74, 8, 15), - (1, 36, 74, 8, 16), - (1, 36, 73, 8, 17), - (1, 37, 73, 8, 18), - (1, 38, 72, 8, 19), - (1, 39, 72, 8, 20), - (1, 40, 72, 8, 21), - (1, 41, 72, 8, 22), - (1, 42, 72, 8, 23), - (1, 42, 73, 8, 24), - (1, 31, 38, 9, 1), - (1, 31, 39, 9, 2), - (1, 31, 40, 9, 3), - (1, 31, 41, 9, 4), - (1, 31, 42, 9, 5), - (1, 30, 42, 9, 6), - (1, 30, 43, 10, 1), - (1, 30, 44, 10, 2), - (1, 29, 44, 10, 3), - (1, 29, 45, 10, 4), - (1, 29, 46, 10, 5), - (1, 29, 47, 10, 6), - (1, 30, 47, 10, 7), - (1, 30, 48, 10, 8), - (1, 31, 49, 10, 9), - (1, 32, 50, 10, 10), - (1, 32, 51, 11, 1), - (1, 33, 52, 11, 2), - (1, 33, 53, 11, 3), - (1, 34, 53, 11, 4), - (1, 34, 54, 11, 5), - (1, 35, 54, 11, 6), - (1, 35, 55, 11, 7), - (1, 35, 56, 11, 8), - (1, 36, 57, 11, 9), - (1, 36, 58, 11, 10), - (1, 36, 59, 11, 11), - (1, 37, 59, 11, 12), - (1, 37, 60, 11, 13), - (1, 38, 60, 11, 14), - (1, 38, 61, 12, 1), - (1, 38, 62, 12, 2), - (1, 38, 63, 12, 3), - (1, 39, 63, 12, 4), - (1, 39, 64, 13, 1), - (1, 39, 65, 13, 2), - (1, 40, 65, 13, 3), - (1, 40, 66, 13, 4), - (1, 40, 67, 13, 5), - (1, 40, 68, 13, 6), - (1, 41, 69, 13, 7), - (1, 41, 70, 13, 8), - (1, 42, 71, 13, 9), - (1, 42, 72, 13, 10), - (1, 42, 73, 13, 11), - (1, 42, 73, 14, 1), - (1, 43, 73, 14, 2), - (1, 43, 74, 14, 3), - (1, 43, 75, 14, 4), - (1, 44, 75, 14, 5), - (1, 44, 76, 14, 6), - (1, 44, 77, 15, 1), - (1, 44, 78, 15, 2), - (1, 44, 79, 15, 3), - (1, 45, 79, 15, 4), -] - -# These are zero based -sfrcells = [ - (0, 44, 8), - (0, 43, 8), - (0, 42, 8), - (0, 42, 9), - (0, 41, 9), - (0, 41, 10), - (0, 41, 11), - (0, 40, 11), - (0, 40, 12), - (0, 39, 12), - (0, 39, 13), - (0, 38, 13), - (0, 38, 14), - (0, 38, 15), - (0, 38, 16), - (0, 37, 16), - (0, 37, 17), - (0, 37, 18), - (0, 37, 19), - (0, 37, 20), - (0, 37, 21), - (0, 37, 22), - (0, 36, 23), - (0, 36, 24), - (0, 35, 24), - (0, 35, 25), - (0, 34, 25), - (0, 34, 26), - (0, 35, 27), - (0, 35, 28), - (0, 35, 29), - (0, 35, 30), - (0, 35, 31), - (0, 34, 31), - (0, 34, 32), - (0, 33, 32), - (0, 33, 33), - (0, 32, 33), - (0, 32, 34), - (0, 31, 34), - (0, 31, 35), - (0, 31, 36), - (0, 52, 36), - (0, 51, 36), - (0, 51, 37), - (0, 50, 37), - (0, 49, 37), - (0, 49, 38), - (0, 48, 38), - (0, 47, 38), - (0, 46, 38), - (0, 46, 39), - (0, 45, 39), - (0, 44, 39), - (0, 44, 38), - (0, 43, 38), - (0, 42, 37), - (0, 41, 37), - (0, 40, 37), - (0, 39, 37), - (0, 39, 38), - (0, 38, 38), - (0, 37, 38), - (0, 36, 39), - (0, 35, 39), - (0, 35, 40), - (0, 34, 40), - (0, 33, 40), - (0, 32, 40), - (0, 31, 40), - (0, 30, 41), - (0, 30, 32), - (0, 30, 33), - (0, 30, 34), - (0, 30, 35), - (0, 30, 36), - (0, 47, 47), - (0, 46, 47), - (0, 45, 47), - (0, 45, 46), - (0, 44, 46), - (0, 43, 46), - (0, 42, 46), - (0, 41, 46), - (0, 40, 46), - (0, 40, 47), - (0, 39, 47), - (0, 38, 47), - (0, 37, 46), - (0, 36, 46), - (0, 35, 47), - (0, 34, 47), - (0, 34, 48), - (0, 33, 48), - (0, 33, 49), - (0, 32, 49), - (0, 54, 71), - (0, 53, 71), - (0, 52, 71), - (0, 51, 71), - (0, 50, 71), - (0, 49, 72), - (0, 48, 72), - (0, 47, 72), - (0, 47, 73), - (0, 46, 73), - (0, 45, 74), - (0, 44, 74), - (0, 44, 75), - (0, 43, 75), - (0, 44, 61), - (0, 43, 61), - (0, 42, 61), - (0, 42, 62), - (0, 41, 62), - (0, 40, 62), - (0, 39, 62), - (0, 23, 54), - (0, 24, 54), - (0, 24, 55), - (0, 25, 55), - (0, 26, 55), - (0, 27, 56), - (0, 28, 56), - (0, 29, 56), - (0, 30, 56), - (0, 31, 56), - (0, 32, 56), - (0, 32, 57), - (0, 33, 57), - (0, 33, 58), - (0, 34, 58), - (0, 35, 58), - (0, 36, 59), - (0, 22, 70), - (0, 23, 70), - (0, 24, 70), - (0, 25, 70), - (0, 26, 71), - (0, 26, 72), - (0, 27, 72), - (0, 28, 72), - (0, 29, 72), - (0, 30, 72), - (0, 31, 72), - (0, 32, 72), - (0, 33, 72), - (0, 33, 73), - (0, 34, 73), - (0, 35, 73), - (0, 35, 72), - (0, 36, 72), - (0, 37, 71), - (0, 38, 71), - (0, 39, 71), - (0, 40, 71), - (0, 41, 71), - (0, 41, 72), - (0, 30, 37), - (0, 30, 38), - (0, 30, 39), - (0, 30, 40), - (0, 30, 41), - (0, 29, 41), - (0, 29, 42), - (0, 29, 43), - (0, 28, 43), - (0, 28, 44), - (0, 28, 45), - (0, 28, 46), - (0, 29, 46), - (0, 29, 47), - (0, 30, 48), - (0, 31, 49), - (0, 31, 50), - (0, 32, 51), - (0, 32, 52), - (0, 33, 52), - (0, 33, 53), - (0, 34, 53), - (0, 34, 54), - (0, 34, 55), - (0, 35, 56), - (0, 35, 57), - (0, 35, 58), - (0, 36, 58), - (0, 36, 59), - (0, 37, 59), - (0, 37, 60), - (0, 37, 61), - (0, 37, 62), - (0, 38, 62), - (0, 38, 63), - (0, 38, 64), - (0, 39, 64), - (0, 39, 65), - (0, 39, 66), - (0, 39, 67), - (0, 40, 68), - (0, 40, 69), - (0, 41, 70), - (0, 41, 71), - (0, 41, 72), - (0, 41, 72), - (0, 42, 72), - (0, 42, 73), - (0, 42, 74), - (0, 43, 74), - (0, 43, 75), - (0, 43, 76), - (0, 43, 77), - (0, 43, 78), - (0, 44, 78), -] - -rlen = [ - 90.0, - 90.0, - 75.0, - 75.0, - 75.0, - 90.0, - 75.0, - 75.0, - 75.0, - 75.0, - 75.0, - 75.0, - 90.0, - 90.0, - 60.0, - 30.0, - 102.0, - 90.0, - 90.0, - 90.0, - 102.0, - 102.0, - 102.0, - 72.0, - 30.0, - 72.0, - 30.0, - 90.0, - 102.0, - 90.0, - 90.0, - 102.0, - 30.0, - 72.0, - 30.0, - 72.0, - 30.0, - 60.0, - 72.0, - 30.0, - 102.0, - 90.0, - 90.0, - 72.0, - 30.0, - 102.0, - 60.0, - 30.0, - 90.0, - 102.0, - 60.0, - 30.0, - 90.0, - 30.0, - 60.0, - 102.0, - 102.0, - 90.0, - 102.0, - 30.0, - 60.0, - 102.0, - 102.0, - 102.0, - 60.0, - 30.0, - 102.0, - 90.0, - 90.0, - 102.0, - 114.0, - 90.0, - 90.0, - 102.0, - 102.0, - 90.0, - 90.0, - 102.0, - 30.0, - 60.0, - 90.0, - 102.0, - 90.0, - 90.0, - 60.0, - 30.0, - 90.0, - 90.0, - 90.0, - 90.0, - 102.0, - 30.0, - 60.0, - 72.0, - 30.0, - 114.0, - 90.0, - 90.0, - 90.0, - 102.0, - 102.0, - 102.0, - 102.0, - 30.0, - 60.0, - 102.0, - 102.0, - 30.0, - 72.0, - 30.0, - 90.0, - 102.0, - 30.0, - 60.0, - 102.0, - 90.0, - 102.0, - 60.0, - 30.0, - 60.0, - 102.0, - 90.0, - 90.0, - 90.0, - 90.0, - 90.0, - 102.0, - 72.0, - 30.0, - 72.0, - 30.0, - 102.0, - 90.0, - 114.0, - 90.0, - 102.0, - 90.0, - 102.0, - 102.0, - 30.0, - 102.0, - 90.0, - 90.0, - 90.0, - 90.0, - 90.0, - 30.0, - 60.0, - 90.0, - 30.0, - 60.0, - 102.0, - 90.0, - 90.0, - 90.0, - 90.0, - 30.0, - 30.0, - 90.0, - 90.0, - 90.0, - 90.0, - 72.0, - 30.0, - 90.0, - 60.0, - 30.0, - 90.0, - 90.0, - 30.0, - 60.0, - 102.0, - 114.0, - 114.0, - 90.0, - 102.0, - 30.0, - 72.0, - 60.0, - 30.0, - 102.0, - 102.0, - 114.0, - 90.0, - 60.0, - 30.0, - 72.0, - 30.0, - 102.0, - 102.0, - 30.0, - 60.0, - 120.0, - 60.0, - 30.0, - 90.0, - 90.0, - 90.0, - 90.0, - 114.0, - 102.0, - 90.0, - 30.0, - 30.0, - 30.0, - 102.0, - 60.0, - 30.0, - 90.0, - 90.0, - 90.0, - 60.0, - 30.0, -] - -rgrd = [ - 0.150, - 0.120, - 0.180, - 0.160, - 0.130, - 0.111, - 0.047, - 0.060, - 0.040, - 0.100, - 0.227, - 0.090, - 0.042, - 0.064, - 0.083, - 0.081, - 0.062, - 0.065, - 0.089, - 0.097, - 0.141, - 0.186, - 0.217, - 0.203, - 0.206, - 0.206, - 0.144, - 0.147, - 0.135, - 0.129, - 0.124, - 0.136, - 0.162, - 0.147, - 0.157, - 0.147, - 0.115, - 0.117, - 0.111, - 0.120, - 0.099, - 0.073, - 0.037, - 0.038, - 0.060, - 0.048, - 0.024, - 0.029, - 0.032, - 0.028, - 0.024, - 0.029, - 0.033, - 0.038, - 0.032, - 0.038, - 0.051, - 0.047, - 0.037, - 0.063, - 0.063, - 0.049, - 0.069, - 0.077, - 0.063, - 0.045, - 0.037, - 0.043, - 0.048, - 0.054, - 0.065, - 0.067, - 0.091, - 0.091, - 0.071, - 0.073, - 0.021, - 0.031, - 0.045, - 0.033, - 0.029, - 0.042, - 0.075, - 0.103, - 0.092, - 0.095, - 0.087, - 0.083, - 0.094, - 0.102, - 0.093, - 0.081, - 0.099, - 0.077, - 0.057, - 0.056, - 0.044, - 0.050, - 0.075, - 0.076, - 0.074, - 0.074, - 0.071, - 0.072, - 0.056, - 0.060, - 0.048, - 0.043, - 0.049, - 0.039, - 0.042, - 0.056, - 0.081, - 0.071, - 0.068, - 0.068, - 0.068, - 0.044, - 0.078, - 0.071, - 0.051, - 0.054, - 0.056, - 0.056, - 0.050, - 0.048, - 0.038, - 0.022, - 0.049, - 0.059, - 0.043, - 0.043, - 0.045, - 0.049, - 0.042, - 0.031, - 0.016, - 0.010, - 0.012, - 0.015, - 0.012, - 0.011, - 0.022, - 0.044, - 0.056, - 0.060, - 0.114, - 0.100, - 0.067, - 0.086, - 0.127, - 0.141, - 0.118, - 0.100, - 0.083, - 0.087, - 0.100, - 0.067, - 0.056, - 0.083, - 0.100, - 0.076, - 0.045, - 0.020, - 0.053, - 0.042, - 0.038, - 0.047, - 0.047, - 0.057, - 0.040, - 0.032, - 0.045, - 0.053, - 0.042, - 0.049, - 0.094, - 0.085, - 0.036, - 0.027, - 0.030, - 0.033, - 0.024, - 0.017, - 0.025, - 0.021, - 0.015, - 0.010, - 0.010, - 0.012, - 0.018, - 0.022, - 0.017, - 0.019, - 0.010, - 0.013, - 0.022, - 0.017, - 0.021, - 0.043, - 0.044, - 0.038, - 0.050, - 0.033, - 0.021, - 0.020, - 0.024, - 0.029, - 0.020, - 0.011, - 0.024, - 0.033, - 0.022, -] - -rtp = [ - 2458.0, - 2449.0, - 2337.0, - 2416.0, - 2413.0, - 2397.0, - 2393.0, - 2390.0, - 2386.0, - 2384.0, - 2367.0, - 2360.0, - 2355.0, - 2351.0, - 2344.0, - 2341.0, - 2335.0, - 2331.0, - 2323.0, - 2315.0, - 2305.0, - 2287.0, - 2267.0, - 2246.0, - 2239.0, - 2225.0, - 2218.0, - 2209.0, - 2195.0, - 2183.0, - 2171.0, - 2160.0, - 2149.0, - 2141.0, - 2134.0, - 2125.0, - 2119.0, - 2114.0, - 2106.0, - 2101.0, - 2092.0, - 2085.0, - 2146.0, - 2143.0, - 2141.0, - 2136.0, - 2134.0, - 2133.0, - 2131.0, - 2128.0, - 2126.0, - 2125.0, - 2123.0, - 2121.0, - 2119.0, - 2117.0, - 2112.0, - 2107.0, - 2103.0, - 2101.0, - 2096.0, - 2093.0, - 2087.0, - 2079.0, - 2073.0, - 2071.0, - 2068.0, - 2065.0, - 2060.0, - 2056.0, - 2048.0, - 2115.0, - 2109.0, - 2098.0, - 2091.0, - 2084.0, - 2112.0, - 2110.0, - 2108.0, - 2106.0, - 2104.0, - 2101.0, - 2096.0, - 2087.0, - 2079.0, - 2076.0, - 2069.0, - 2063.0, - 2054.0, - 2046.0, - 2035.0, - 2031.0, - 2026.0, - 2020.0, - 2017.0, - 2013.0, - 2000.0, - 1996.0, - 1991.0, - 1982.0, - 1976.0, - 1967.0, - 1961.0, - 1955.0, - 1953.0, - 1948.0, - 1942.0, - 1940.0, - 1937.0, - 1935.0, - 2003.0, - 1999.0, - 1994.0, - 1990.0, - 1985.0, - 1978.0, - 1972.0, - 2032.0, - 2030.0, - 2025.0, - 2021.0, - 2016.0, - 2011.0, - 2006.0, - 2001.0, - 1997.0, - 1992.0, - 1990.0, - 1989.0, - 1985.0, - 1983.0, - 1980.0, - 1976.0, - 1971.0, - 2051.0, - 2047.0, - 2045.0, - 2044.0, - 2043.0, - 2042.0, - 2041.0, - 2040.0, - 2039.0, - 2036.0, - 2031.0, - 2026.0, - 2022.0, - 2014.0, - 2010.0, - 2005.0, - 2001.0, - 1989.0, - 1976.0, - 1967.0, - 1958.0, - 1952.0, - 1945.0, - 1943.0, - 2076.0, - 2071.0, - 2061.0, - 2053.0, - 2048.0, - 2047.0, - 2041.0, - 2037.0, - 2036.0, - 2033.0, - 2029.0, - 2026.0, - 2023.0, - 2021.0, - 2017.0, - 2011.0, - 2006.0, - 2002.0, - 1998.0, - 1991.0, - 1988.0, - 1987.0, - 1985.0, - 1982.0, - 1978.0, - 1977.0, - 1975.0, - 1974.0, - 1973.0, - 1972.0, - 1970.0, - 1969.0, - 1968.0, - 1967.0, - 1966.0, - 1965.0, - 1964.0, - 1963.0, - 1961.0, - 1959.0, - 1958.0, - 1955.0, - 1949.0, - 1946.0, - 1943.0, - 1942.0, - 1941.0, - 1940.0, - 1938.0, - 1937.0, - 1935.0, - 1934.0, - 1933.0, - 1930.0, - 1929.0, -] - - -def get_sfrsegs(): - return orig_seg - - -def get_sfrrchs(): - return orig_rch - - -def get_sfrcells(): - return sfrcells - - -def get_sfrlen(): - return rlen - - -def get_rgrd(): - return rgrd - - -def get_rtp(): - return rtp diff --git a/etc/ci_build_files.py b/etc/ci_build_files.py deleted file mode 100644 index 4a5e8d52b..000000000 --- a/etc/ci_build_files.py +++ /dev/null @@ -1,81 +0,0 @@ -import os -import shutil -import sys -from subprocess import PIPE, Popen - -import pytest - - -def get_command_args(): - run_arg, zip_arg = False, False - for idx, arg in enumerate(sys.argv): - if "--run=" in arg: - run_arg = arg.split("=")[1] - elif arg == "--run": - run_arg = sys.argv[idx + 1] - if "--zip=" in arg: - zip_arg = arg.split("=")[1] - elif arg == "--zip": - zip_arg = sys.argv[idx + 1] - - return run_arg, zip_arg - - -@pytest.fixture(scope="session") -def run(pytestconfig): - return pytestconfig.getoption("run") - - -def clean_files(): - pth = os.path.join("..", "examples") - if os.path.isdir(pth): - shutil.rmtree(pth) - - -def zip_files(): - print("zipping files in the 'examples' directory") - sdir = os.getcwd() - os.chdir("..") - shutil.make_archive("modflow6-examples", "zip", "examples") - os.chdir(sdir) - - -def test_build_run_all(build): - # make examples directory if it does not exist - os.makedirs(os.path.join("..", "examples"), exist_ok=True) - - # parse run boolean and script name and build command argument list - run, script = build - args = ["python", script] - if not run: - args += ["--no_run", "--no_plot"] - - # run the script - print(" ".join(args)) - proc = Popen(args, stdout=PIPE, stderr=PIPE, cwd=os.path.join("..", "scripts")) - stdout, stderr = proc.communicate() - if stdout: - print(stdout.decode("utf-8")) - if stderr: - print(stderr.decode("utf-8")) - assert proc.returncode == 0, print(stderr.decode("utf-8")) - - -if __name__ == "__main__": - run_arg, zip_arg = get_command_args() - scripts_dict = { - file_name: (run_arg, file_name) - for file_name in sorted(os.listdir(os.path.join("..", "scripts"))) - if file_name.endswith(".py") and file_name.startswith("ex-") - } - - # clean the files - clean_files() - - # build and run, if necessary, run the scripts - for build in scripts_dict.values(): - test_build_run_all(build) - - # zip up the input files, if required - if zip_arg: - zip_files() diff --git a/etc/ci_run_notebooks.py b/etc/ci_run_notebooks.py deleted file mode 100644 index 4af37b9bb..000000000 --- a/etc/ci_run_notebooks.py +++ /dev/null @@ -1,97 +0,0 @@ -import os -import shutil -import sys - -import pytest - -# path to notebooks -src_pth = os.path.join("..", "notebooks") -dst_pth = os.path.join("..", ".nb") -rtd_pth = os.path.join("..", ".doc", "_notebooks") - -# parse command line arguments for notebook to create -nb_files = None -clean_sphinx = True -for idx, arg in enumerate(sys.argv): - if arg in ("-f", "--file"): - file_name = sys.argv[idx + 1] - if not file_name.endswith(".ipynb"): - file_name += ".ipynb" - pth = os.path.join(src_pth, file_name) - if os.path.isfile(pth): - nb_files = [file_name] - clean_sphinx = False - -# get list of notebooks -if nb_files is None: - nb_files = [ - file_name - for file_name in sorted(os.listdir(src_pth)) - if file_name.endswith(".ipynb") - ] - - -def clean_files(): - if os.path.isdir(dst_pth): - shutil.rmtree(dst_pth) - - -@pytest.mark.parametrize( - "file_name", - nb_files, -) -def test_run_notebooks(file_name): - # make paths if they do not exist - for dir_path in ( - dst_pth, - rtd_pth, - ): - os.makedirs(dir_path, exist_ok=True) - - # set src, dst, and rtd paths - src = os.path.join(src_pth, file_name) - dst = os.path.join(dst_pth, file_name) - rtd = os.path.join(rtd_pth, file_name) - - # remove dst if it exists - if os.path.isfile(dst): - print(f"removing '{dst}'") - os.remove(dst) - - arg = ( - "jupytext", - "--to ipynb", - "--from ipynb", - "--execute", - "-o", - dst, - src, - ) - print(" ".join(arg)) - os.system(" ".join(arg)) - - # remove rtd if it exists - if os.path.isfile(rtd): - print(f"removing '{rtd}'") - os.remove(rtd) - - # copy dst to rtd - print(f"copying '{dst}' -> '{rtd}'") - shutil.copyfile(dst, rtd) - - -if __name__ == "__main__": - clean_files() - - # clean up ReadtheDocs path - if clean_sphinx: - if os.path.isdir(rtd_pth): - print(f"deleting...'{rtd_pth}'") - shutil.rmtree(rtd_pth) - - for file_name in nb_files: - test_run_notebooks(file_name) - - # clean up temporary files - print(f"cleaning up...'{dst_pth}'") - shutil.rmtree(dst_pth) diff --git a/etc/conftest.py b/etc/conftest.py deleted file mode 100644 index 378005eb2..000000000 --- a/etc/conftest.py +++ /dev/null @@ -1,18 +0,0 @@ -import os - - -def pytest_addoption(parser): - parser.addoption("--run", action="store", default=False) - - -def pytest_generate_tests(metafunc): - if "build" in metafunc.fixturenames: - run = metafunc.config.getoption("run") - scripts_dict = { - file_name: (run, file_name) - for file_name in sorted(os.listdir(os.path.join("..", "scripts"))) - if file_name.endswith(".py") and file_name.startswith("ex-") - } - metafunc.parametrize( - "build", scripts_dict.values(), ids=scripts_dict.keys() - ) diff --git a/etc/requirements.pip.txt b/etc/requirements.pip.txt index 1b5047fe7..36e1c7703 100644 --- a/etc/requirements.pip.txt +++ b/etc/requirements.pip.txt @@ -19,4 +19,4 @@ rasterio rasterstats jupyter jupytext -modflow-devtools +pooch \ No newline at end of file diff --git a/etc/requirements.usgs.txt b/etc/requirements.usgs.txt index fdb7861a5..93727cac7 100644 --- a/etc/requirements.usgs.txt +++ b/etc/requirements.usgs.txt @@ -1,2 +1,3 @@ git+https://github.com/modflowpy/flopy.git@develop git+https://github.com/MODFLOW-USGS/modflowapi.git@develop +git+https://github.com/MODFLOW-USGS/modflow-devtools.git@develop diff --git a/etc/test_action.py b/etc/test_action.py deleted file mode 100644 index 271c3dccc..000000000 --- a/etc/test_action.py +++ /dev/null @@ -1,118 +0,0 @@ -import os -import shutil -from subprocess import PIPE, Popen - -# change to root directory - local run only -starting_dir = os.getcwd() -os.chdir("..") - -# clean up examples directory - just for local runs -expth = os.path.join("examples") -examples = [ - os.path.join(expth, d) - for d in os.listdir(expth) - if os.path.isdir(os.path.join(expth, d)) -] -for e in examples: - shutil.rmtree(e) - -# run scripts without model runs -pth = os.path.join("scripts") -scripts = [ - file_name - for file_name in os.listdir(pth) - if file_name.endswith(".py") and file_name.startswith("ex-") -] -for s in scripts: - args = ("python", s, "--no_run", "--no_plot") - proc = Popen(args, stdout=PIPE, stderr=PIPE, cwd=pth) - stdout, stderr = proc.communicate() - if stdout: - print(stdout.decode("utf-8")) - if stderr: - print("Errors:\n{}".format(stderr.decode("utf-8"))) - -# zip up model input files -expth = os.path.join("examples") -zpth = "modflow6-examples" -shutil.make_archive("modflow6-examples", "zip", expth) - -# run scripts plus processing script -pth = os.path.join("scripts") -scripts = [ - file_name - for file_name in os.listdir(pth) - if file_name.endswith(".py") and file_name.startswith("ex-") -] -scripts.append("process-scripts.py") -for s in scripts: - args = ("python", s) - proc = Popen(args, stdout=PIPE, stderr=PIPE, cwd=pth) - stdout, stderr = proc.communicate() - if stdout: - print(stdout.decode("utf-8")) - if stderr: - print("Errors:\n{}".format(stderr.decode("utf-8"))) - -# build the LaTeX document -ws = "doc" -bibnam = "mf6examples" -texnam = bibnam + ".tex" -args = ( - ("latexmk", "-c", texnam), - ("pdflatex", texnam), - ("bibtex", bibnam), - ("pdflatex", texnam), - ("pdflatex", texnam), -) -os.chdir(ws) -for arg in args: - print("running command...'{}'".format(" ".join(arg))) - os.system(" ".join(arg)) -os.chdir("..") - -# run the notebooks -src_pth = os.path.join("notebooks") -dst_pth = os.path.join(".notebooks") -if os.path.isdir(dst_pth): - shutil.rmtree(dst_pth) -os.makedirs(dst_pth, exist_ok=True) -nb_files = [ - file_name - for file_name in os.listdir(src_pth) - if file_name.endswith(".ipynb") and file_name.startswith("ex-") -] -for file_name in nb_files: - src = os.path.join(src_pth, file_name) - dst = os.path.join(dst_pth, file_name) - arg = ( - "jupytext", - "--from ipynb", - "--to ipynb", - "--execute", - "--output", - dst, - src, - ) - print(" ".join(arg)) - os.system(" ".join(arg)) - -# -- remove ./_notebooks if it exists ---------------------------------------- -copy_pth = os.path.join(".doc", "_notebooks") -print(f"clean up {copy_pth}") -if os.path.isdir(copy_pth): - shutil.rmtree(copy_pth) - -# -- copy executed notebooks to ./_notebooks --------------------------------- -print(f"copy files in {dst_pth} -> {copy_pth}") -shutil.copytree(dst_pth, copy_pth) - -# -- clean up (remove) dst_pth directory ------------------------------------- -print(f"clean up {dst_pth}") -shutil.rmtree(dst_pth) - -# remove zip file - local run only -os.remove("modflow6-examples.zip") - -# change back to root directory - local run only -os.chdir(starting_dir) diff --git a/scripts/ex-gwf-advtidal.py b/scripts/ex-gwf-advtidal.py index af1e4399f..4b2451393 100644 --- a/scripts/ex-gwf-advtidal.py +++ b/scripts/ex-gwf-advtidal.py @@ -6,49 +6,46 @@ # is days. Each cell is 500 ft × 500 ft. The estuary is represented by GHB # boundaries in column 10. Two rivers cross the area from left to right. # Recharge is zoned by the use of three Recharge-Package input files -# -# ### Advgwtidal Problem Setup +# ### Initial setup # -# Imports +# Import dependencies, read settings from environment variables, and define model parameters. +# + import os -import sys +import pathlib as pl import flopy import matplotlib.pyplot as plt import numpy as np +import pooch +from flopy.plot.styles import styles from flopy.utils.gridintersect import GridIntersect +from modflow_devtools.misc import get_env, timed from shapely.geometry import Polygon -# Append to system path to include the common subdirectory - -sys.path.append(os.path.join("..", "common")) - -# import common functionality - -import config -from figspecs import USGSFigure - -# Set default figure properties - -figure_size = (4, 4) - -# Base simulation and model name and workspace - -ws = config.base_ws +# Example name and base workspace +sim_name = "ex-gwf-advtidal" +workspace = pl.Path("../examples") -# Simulation name +# Settings from environment variables +write = get_env("WRITE", True) +run = get_env("RUN", True) +plot = get_env("PLOT", True) +plot_show = get_env("PLOT_SHOW", True) +plot_save = get_env("PLOT_SAVE", True) +# - -sim_name = "ex-gwf-advtidal" +# ### Define parameters +# +# Define model units, parameters and other settings. +# + # Model units - length_units = "meters" time_units = "days" -# Table Advgwtidal Model Parameters - +# Model parameters nper = 4 # Number of periods nlay = 3 # Number of layers ncol = 10 # Number of columns @@ -68,23 +65,18 @@ # Simulation has 1 steady stress period (1 day) # and 3 transient stress periods (10 days each). # Each transient stress period has 120 2-hour time steps. - perlen = [1.0, 10.0, 10.0, 10.0] nstp = [1, 120, 120, 120] tsmult = [1.0, 1.0, 1.0, 1.0] tdis_ds = list(zip(perlen, nstp, tsmult)) -# parse parameter strings into tuples - +# Parse parameter strings into tuples botm = [float(value) for value in botm_str.split(",")] k11 = [float(value) for value in k11_str.split(",")] k33 = [float(value) for value in k33_str.split(",")] icelltype = [int(value) for value in icelltype_str.split(",")] -# ### Create Advgwtidal Recharge Zones -# -# shapely is used to construct recharge zones -# +# Recharge zones (constructed with shapely) recharge_zone_1 = Polygon( shell=[ (0, 0), @@ -114,17 +106,18 @@ ) # Solver parameters - nouter = 50 ninner = 100 hclose = 1e-9 rclose = 1e-6 +# - -# ### Functions to build, write, run, and plot the MODFLOW 6 Advgwtidal model +# ### Model setup # -# MODFLOW 6 flopy simulation object (sim) is returned if building the model +# Define functions to build models, write input files, and run the simulation. +# + def get_timeseries(fname, names, interpolation, filename=None): tsdata = [] for row in np.genfromtxt(fname, delimiter=",", comments="#"): @@ -139,390 +132,364 @@ def get_timeseries(fname, names, interpolation, filename=None): return tsdict -def build_model(): - if config.buildModel: - sim_ws = os.path.join(ws, sim_name) - sim = flopy.mf6.MFSimulation( - sim_name=sim_name, - sim_ws=sim_ws, - exe_name="mf6", - verbosity_level=0, - ) - flopy.mf6.ModflowTdis( - sim, nper=nper, perioddata=tdis_ds, time_units=time_units - ) - flopy.mf6.ModflowIms( - sim, - outer_maximum=nouter, - outer_dvclose=hclose, - inner_maximum=ninner, - inner_dvclose=hclose, - rcloserecord=f"{rclose} strict", - ) - gwf = flopy.mf6.ModflowGwf(sim, modelname=sim_name, save_flows=True) - flopy.mf6.ModflowGwfdis( - gwf, - length_units=length_units, - nlay=nlay, - nrow=nrow, - ncol=ncol, - delr=delr, - delc=delc, - top=top, - botm=botm, - ) - flopy.mf6.ModflowGwfnpf( - gwf, - cvoptions="perched", - perched=True, - icelltype=icelltype, - k=k11, - k33=k33, - save_specific_discharge=True, - ) - flopy.mf6.ModflowGwfic(gwf, strt=strt) - flopy.mf6.ModflowGwfsto( - gwf, - iconvert=1, - ss=1.0e-6, - sy=sy, - steady_state={0: True}, - transient={1: True}, - ) - - ghb_spd = [] - ghb_spd += [ - [1, i, 9, "tides", 15.0, "ESTUARY-L2"] for i in range(nrow) - ] - ghb_spd += [ - [2, i, 9, "tides", 1500.0, "ESTUARY-L3"] for i in range(nrow) - ] - ghb_spd = {0: ghb_spd} - fname = os.path.join(config.data_ws, sim_name, "tides.csv") - tsdict = get_timeseries(fname, "tides", "linear") - ghbobs_dict = {} - ghbobs_dict[f"{sim_name}.ghb.obs.csv"] = [ - ("ghb_2_6_10", "ghb", (1, 5, 9)), - ("ghb_3_6_10", "ghb", (2, 5, 9)), - ("estuary2", "ghb", "ESTUARY-L2"), - ("estuary3", "ghb", "ESTUARY-L3"), - ] - - flopy.mf6.ModflowGwfghb( - gwf, - stress_period_data=ghb_spd, - boundnames=True, - timeseries=tsdict, - observations=ghbobs_dict, - pname="GHB-TIDAL", - ) - - wel_spd = {} - wel_spd[1] = [ - [0, 11, 2, -50, ""], - [2, 4, 7, "well_1_rate", "well_1"], - [2, 3, 2, "well_2_rate", "well_2"], - ] - wel_spd[2] = [ - [2, 3, 2, "well_2_rate", "well_2"], - [2, 4, 7, "well_1_rate", "well_1"], - ] - wel_spd[3] = [ - [2, 4, 7, "well_1_rate", "well_1"], - [2, 3, 2, "well_2_rate", "well_2"], - [0, 11, 2, -10, ""], - [0, 2, 4, -20, ""], - [0, 13, 5, -40, ""], - ] - fname = os.path.join(config.data_ws, sim_name, "wellrates.csv") - tsdict = get_timeseries( - fname, - ["well_1_rate", "well_2_rate", "well_6_rate"], - 3 * ["stepwise"], - ) - flopy.mf6.ModflowGwfwel( - gwf, - stress_period_data=wel_spd, - boundnames=True, - timeseries=tsdict, - pname="WEL", - ) +def build_models(): + sim_ws = os.path.join(workspace, sim_name) + sim = flopy.mf6.MFSimulation( + sim_name=sim_name, + sim_ws=sim_ws, + exe_name="mf6", + verbosity_level=0, + ) + flopy.mf6.ModflowTdis(sim, nper=nper, perioddata=tdis_ds, time_units=time_units) + flopy.mf6.ModflowIms( + sim, + outer_maximum=nouter, + outer_dvclose=hclose, + inner_maximum=ninner, + inner_dvclose=hclose, + rcloserecord=f"{rclose} strict", + ) + gwf = flopy.mf6.ModflowGwf(sim, modelname=sim_name, save_flows=True) + flopy.mf6.ModflowGwfdis( + gwf, + length_units=length_units, + nlay=nlay, + nrow=nrow, + ncol=ncol, + delr=delr, + delc=delc, + top=top, + botm=botm, + ) + flopy.mf6.ModflowGwfnpf( + gwf, + cvoptions="perched", + perched=True, + icelltype=icelltype, + k=k11, + k33=k33, + save_specific_discharge=True, + ) + flopy.mf6.ModflowGwfic(gwf, strt=strt) + flopy.mf6.ModflowGwfsto( + gwf, + iconvert=1, + ss=1.0e-6, + sy=sy, + steady_state={0: True}, + transient={1: True}, + ) + + ghb_spd = [] + ghb_spd += [[1, i, 9, "tides", 15.0, "ESTUARY-L2"] for i in range(nrow)] + ghb_spd += [[2, i, 9, "tides", 1500.0, "ESTUARY-L3"] for i in range(nrow)] + ghb_spd = {0: ghb_spd} + fname = pooch.retrieve( + url=f"https://github.com/MODFLOW-USGS/modflow6-examples/raw/master/data/{sim_name}/tides.csv", + known_hash="md5:425337a0bf24fa72c9e40f4e3d9f698a", + ) + tsdict = get_timeseries(fname, "tides", "linear") + ghbobs_dict = {} + ghbobs_dict[f"{sim_name}.ghb.obs.csv"] = [ + ("ghb_2_6_10", "ghb", (1, 5, 9)), + ("ghb_3_6_10", "ghb", (2, 5, 9)), + ("estuary2", "ghb", "ESTUARY-L2"), + ("estuary3", "ghb", "ESTUARY-L3"), + ] - rivlay = 20 * [0] - rivrow = [2, 3, 4, 4, 5, 5, 5, 4, 4, 4, 9, 8, 7, 6, 6, 5, 5, 6, 6, 6] - rivcol = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9] - rivstg = 10 * ["river_stage_1"] + 10 * ["river_stage_2"] - rivcnd = 2 * [1000 + f + 1 for f in range(10)] - rivrbt = list(np.linspace(35.9, 35.0, 10)) + list( - np.linspace(36.9, 36.0, 10) - ) - rivbnd = ( - 5 * [""] - + ["riv1_c6", "riv1_c7"] - + 3 * [""] - + 3 * ["riv2_upper"] - + 2 * [""] - + ["riv2_c6", "riv2_c7"] - + 3 * [""] - ) - riv_spd = list( - zip(rivlay, rivrow, rivcol, rivstg, rivcnd, rivrbt, rivbnd) + flopy.mf6.ModflowGwfghb( + gwf, + stress_period_data=ghb_spd, + boundnames=True, + timeseries=tsdict, + observations=ghbobs_dict, + pname="GHB-TIDAL", + ) + + wel_spd = {} + wel_spd[1] = [ + [0, 11, 2, -50, ""], + [2, 4, 7, "well_1_rate", "well_1"], + [2, 3, 2, "well_2_rate", "well_2"], + ] + wel_spd[2] = [ + [2, 3, 2, "well_2_rate", "well_2"], + [2, 4, 7, "well_1_rate", "well_1"], + ] + wel_spd[3] = [ + [2, 4, 7, "well_1_rate", "well_1"], + [2, 3, 2, "well_2_rate", "well_2"], + [0, 11, 2, -10, ""], + [0, 2, 4, -20, ""], + [0, 13, 5, -40, ""], + ] + fname = pooch.retrieve( + url=f"https://github.com/MODFLOW-USGS/modflow6-examples/raw/master/data/{sim_name}/wellrates.csv", + known_hash="md5:6ca7366be279d679b14e8338a195422f", + ) + tsdict = get_timeseries( + fname, + ["well_1_rate", "well_2_rate", "well_6_rate"], + 3 * ["stepwise"], + ) + flopy.mf6.ModflowGwfwel( + gwf, + stress_period_data=wel_spd, + boundnames=True, + timeseries=tsdict, + pname="WEL", + ) + + rivlay = 20 * [0] + rivrow = [2, 3, 4, 4, 5, 5, 5, 4, 4, 4, 9, 8, 7, 6, 6, 5, 5, 6, 6, 6] + rivcol = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9] + rivstg = 10 * ["river_stage_1"] + 10 * ["river_stage_2"] + rivcnd = 2 * [1000 + f + 1 for f in range(10)] + rivrbt = list(np.linspace(35.9, 35.0, 10)) + list(np.linspace(36.9, 36.0, 10)) + rivbnd = ( + 5 * [""] + + ["riv1_c6", "riv1_c7"] + + 3 * [""] + + 3 * ["riv2_upper"] + + 2 * [""] + + ["riv2_c6", "riv2_c7"] + + 3 * [""] + ) + riv_spd = list(zip(rivlay, rivrow, rivcol, rivstg, rivcnd, rivrbt, rivbnd)) + fname = pooch.retrieve( + url=f"https://github.com/MODFLOW-USGS/modflow6-examples/raw/master/data/{sim_name}/riverstage.csv", + known_hash="md5:83f8b526ec6e6978b1d9dbd6fde231ef", + ) + tsdict = get_timeseries( + fname, + ["river_stage_1", "river_stage_2"], + ["linear", "stepwise"], + ) + flopy.mf6.ModflowGwfriv( + gwf, + stress_period_data=riv_spd, + boundnames=True, + timeseries=tsdict, + pname="RIV", + ) + + hashes = [ + "f8b9b26a3403101f3568cd42f759554f", + "c1ea7ded8edf33d6d70a1daf2524584a", + "9ca294d3260c9d3c3487f8db498a0aa6", + ] + for ipak, p in enumerate([recharge_zone_1, recharge_zone_2, recharge_zone_3]): + ix = GridIntersect(gwf.modelgrid, method="vertex", rtree=True) + result = ix.intersect(p) + rch_spd = [] + for i in range(result.shape[0]): + rch_spd.append( + [ + 0, + *result["cellids"][i], + f"rch_{ipak + 1}", + result["areas"][i] / delr / delc, + ] + ) + fname = pooch.retrieve( + url=f"https://github.com/MODFLOW-USGS/modflow6-examples/raw/master/data/{sim_name}/recharge{ipak + 1}.csv", + known_hash=f"md5:{hashes[ipak]}", ) - fname = os.path.join(config.data_ws, sim_name, "riverstage.csv") tsdict = get_timeseries( fname, - ["river_stage_1", "river_stage_2"], - ["linear", "stepwise"], + [f"rch_{ipak + 1}"], + ["stepwise"], + filename=f"{sim_name}.rch{ipak + 1}.ts", ) - flopy.mf6.ModflowGwfriv( + flopy.mf6.ModflowGwfrch( gwf, - stress_period_data=riv_spd, + stress_period_data=rch_spd, boundnames=True, timeseries=tsdict, - pname="RIV", + fixed_cell=True, + print_input=True, + print_flows=True, + save_flows=True, + auxiliary=["MULTIPLIER"], + auxmultname="MULTIPLIER", + pname=f"RCH-ZONE_{ipak + 1}", + filename=f"{sim_name}.rch{ipak + 1}", ) - for ipak, p in enumerate( - [recharge_zone_1, recharge_zone_2, recharge_zone_3] - ): - ix = GridIntersect(gwf.modelgrid, method="vertex", rtree=True) - result = ix.intersect(p) - rch_spd = [] - for i in range(result.shape[0]): - rch_spd.append( - [ - 0, - *result["cellids"][i], - f"rch_{ipak + 1}", - result["areas"][i] / delr / delc, - ] - ) - fname = os.path.join( - config.data_ws, sim_name, f"recharge{ipak + 1}.csv" - ) - tsdict = get_timeseries( - fname, - [f"rch_{ipak + 1}"], - ["stepwise"], - filename=f"{sim_name}.rch{ipak + 1}.ts", - ) - flopy.mf6.ModflowGwfrch( - gwf, - stress_period_data=rch_spd, - boundnames=True, - timeseries=tsdict, - fixed_cell=True, - print_input=True, - print_flows=True, - save_flows=True, - auxiliary=["MULTIPLIER"], - auxmultname="MULTIPLIER", - pname=f"RCH-ZONE_{ipak + 1}", - filename=f"{sim_name}.rch{ipak + 1}", - ) + nseg = 3 + etsurf = 50 + etrate = 0.0004 + depth = 10.0 + pxdp = [0.2, 0.5] + petm = [0.3, 0.1] + row, col = np.where(np.zeros((nrow, ncol)) == 0) + cellids = list(zip(nrow * ncol * [0], row, col)) + evt_spd = [[k, i, j, etsurf, etrate, depth, *pxdp, *petm] for k, i, j in cellids] + flopy.mf6.ModflowGwfevt(gwf, nseg=nseg, stress_period_data=evt_spd, pname="EVT") - nseg = 3 - etsurf = 50 - etrate = 0.0004 - depth = 10.0 - pxdp = [0.2, 0.5] - petm = [0.3, 0.1] - row, col = np.where(np.zeros((nrow, ncol)) == 0) - cellids = list(zip(nrow * ncol * [0], row, col)) - evt_spd = [ - [k, i, j, etsurf, etrate, depth, *pxdp, *petm] - for k, i, j in cellids - ] - flopy.mf6.ModflowGwfevt( - gwf, nseg=nseg, stress_period_data=evt_spd, pname="EVT" - ) + head_filerecord = f"{sim_name}.hds" + budget_filerecord = f"{sim_name}.cbc" + flopy.mf6.ModflowGwfoc( + gwf, + head_filerecord=head_filerecord, + budget_filerecord=budget_filerecord, + saverecord=[("HEAD", "ALL"), ("BUDGET", "ALL")], + ) - head_filerecord = f"{sim_name}.hds" - budget_filerecord = f"{sim_name}.cbc" - flopy.mf6.ModflowGwfoc( - gwf, - head_filerecord=head_filerecord, - budget_filerecord=budget_filerecord, - saverecord=[("HEAD", "ALL"), ("BUDGET", "ALL")], - ) + obsdict = {} + obslist = [["h1_13_8", "head", (2, 12, 7)]] + obsdict[f"{sim_name}.obs.head.csv"] = obslist + obslist = [["icf1", "flow-ja-face", (0, 4, 5), (0, 5, 5)]] + obsdict[f"{sim_name}.obs.flow.csv"] = obslist + obs = flopy.mf6.ModflowUtlobs(gwf, print_input=False, continuous=obsdict) - obsdict = {} - obslist = [["h1_13_8", "head", (2, 12, 7)]] - obsdict[f"{sim_name}.obs.head.csv"] = obslist - obslist = [["icf1", "flow-ja-face", (0, 4, 5), (0, 5, 5)]] - obsdict[f"{sim_name}.obs.flow.csv"] = obslist - obs = flopy.mf6.ModflowUtlobs( - gwf, print_input=False, continuous=obsdict - ) + return sim - return sim - return None +def write_models(sim, silent=True): + sim.write_simulation(silent=silent) -# Function to write MODFLOW 6 Advgwtidal model files +@timed +def run_models(sim, silent=False): + success, buff = sim.run_simulation(silent=silent, report=True) + assert success, buff -def write_model(sim, silent=True): - if config.writeModel: - sim.write_simulation(silent=silent) +# - -# Function to run the Advgwtidal model. -# True is returned if the model runs successfully +# ### Plotting results # +# Define functions to plot model results. - -@config.timeit -def run_model(sim, silent=False): - success = True - if config.runModel: - success, buff = sim.run_simulation(silent=silent, report=True) - if not success: - print(buff) - return success +# + +# Figure properties +figure_size = (4, 4) -# Function to plot the Advgwtidal model results. -# def plot_grid(sim): - fs = USGSFigure(figure_type="map", verbose=False) - sim_ws = os.path.join(ws, sim_name) - gwf = sim.get_model(sim_name) - - fig = plt.figure(figsize=(5.5, 8.0)) - - ax = fig.add_subplot(2, 2, 1, aspect="equal") - pmv = flopy.plot.PlotMapView(model=gwf, ax=ax, layer=0) - pmv.plot_grid() - pmv.plot_bc(name="WEL", kper=3) - pmv.plot_bc(name="RIV") - title = "Layer 1" - letter = chr(ord("@") + 1) - fs.heading(letter=letter, heading=title, ax=ax) - ax.set_xlabel("x position (m)") - ax.set_ylabel("y position (m)") - - ax = fig.add_subplot(2, 2, 2, aspect="equal") - pmv = flopy.plot.PlotMapView(model=gwf, ax=ax, layer=1) - pmv.plot_grid() - pmv.plot_bc(name="GHB") - title = "Layer 2" - letter = chr(ord("@") + 2) - fs.heading(letter=letter, heading=title, ax=ax) - ax.set_xlabel("x position (m)") - ax.set_ylabel("y position (m)") - - ax = fig.add_subplot(2, 2, 3, aspect="equal") - pmv = flopy.plot.PlotMapView(model=gwf, ax=ax, layer=2) - pmv.plot_grid() - pmv.plot_bc(name="GHB") - pmv.plot_bc(ftype="WEL", kper=3) - title = "Layer 3" - letter = chr(ord("@") + 3) - fs.heading(letter=letter, heading=title, ax=ax) - ax.set_xlabel("x position (m)") - ax.set_ylabel("y position (m)") - - ax = fig.add_subplot(2, 2, 4, aspect="equal") - pmv = flopy.plot.PlotMapView(model=gwf, ax=ax) - pmv.plot_grid(linewidth=0) - for ip, (p, fc) in enumerate( - [ - (recharge_zone_1, "r"), - (recharge_zone_2, "b"), - (recharge_zone_3, "g"), - ] - ): - xs, ys = p.exterior.xy - ax.fill( - xs, - ys, - alpha=0.25, - fc=fc, - ec="none", - label=f"Recharge Zone {ip + 1}", - ) - ax.set_xlabel("x position (m)") - ax.set_ylabel("y position (m)") - fs.graph_legend(ax) - title = "Recharge zones" - letter = chr(ord("@") + 4) - fs.heading(letter=letter, heading=title, ax=ax) - - # save figure - if config.plotSave: - fpth = os.path.join( - "..", "figures", f"{sim_name}-grid{config.figure_ext}" - ) - fig.savefig(fpth) - return + with styles.USGSMap(): + gwf = sim.get_model(sim_name) + + fig = plt.figure(figsize=(5.5, 8.0)) + + ax = fig.add_subplot(2, 2, 1, aspect="equal") + pmv = flopy.plot.PlotMapView(model=gwf, ax=ax, layer=0) + pmv.plot_grid() + pmv.plot_bc(name="WEL", kper=3) + pmv.plot_bc(name="RIV") + title = "Layer 1" + letter = chr(ord("@") + 1) + styles.heading(letter=letter, heading=title, ax=ax) + ax.set_xlabel("x position (m)") + ax.set_ylabel("y position (m)") + + ax = fig.add_subplot(2, 2, 2, aspect="equal") + pmv = flopy.plot.PlotMapView(model=gwf, ax=ax, layer=1) + pmv.plot_grid() + pmv.plot_bc(name="GHB") + title = "Layer 2" + letter = chr(ord("@") + 2) + styles.heading(letter=letter, heading=title, ax=ax) + ax.set_xlabel("x position (m)") + ax.set_ylabel("y position (m)") + + ax = fig.add_subplot(2, 2, 3, aspect="equal") + pmv = flopy.plot.PlotMapView(model=gwf, ax=ax, layer=2) + pmv.plot_grid() + pmv.plot_bc(name="GHB") + pmv.plot_bc(ftype="WEL", kper=3) + title = "Layer 3" + letter = chr(ord("@") + 3) + styles.heading(letter=letter, heading=title, ax=ax) + ax.set_xlabel("x position (m)") + ax.set_ylabel("y position (m)") + + ax = fig.add_subplot(2, 2, 4, aspect="equal") + pmv = flopy.plot.PlotMapView(model=gwf, ax=ax) + pmv.plot_grid(linewidth=0) + for ip, (p, fc) in enumerate( + [ + (recharge_zone_1, "r"), + (recharge_zone_2, "b"), + (recharge_zone_3, "g"), + ] + ): + xs, ys = p.exterior.xy + ax.fill( + xs, + ys, + alpha=0.25, + fc=fc, + ec="none", + label=f"Recharge Zone {ip + 1}", + ) + ax.set_xlabel("x position (m)") + ax.set_ylabel("y position (m)") + styles.graph_legend(ax) + title = "Recharge zones" + letter = chr(ord("@") + 4) + styles.heading(letter=letter, heading=title, ax=ax) + if plot_show: + plt.show + if plot_save: + fpth = os.path.join("..", "figures", f"{sim_name}-grid.png") + fig.savefig(fpth) def plot_ts(sim): - fs = USGSFigure(figure_type="graph", verbose=False) - sim_ws = os.path.join(ws, sim_name) - gwf = sim.get_model(sim_name) - obsnames = gwf.obs[1].output.obs_names - obs_list = [ - gwf.obs[1].output.obs(f=obsnames[0]), - gwf.obs[1].output.obs(f=obsnames[1]), - gwf.ghb.output.obs(), - ] - ylabel = ("head (m)", "flow ($m^3/d$)", "flow ($m^3/d$)") - obs_fig = ("obs-head", "obs-flow", "ghb-obs") - for iplot, obstype in enumerate(obs_list): - fig = plt.figure(figsize=(6, 3)) - ax = fig.add_subplot() - tsdata = obstype.data - for name in tsdata.dtype.names[1:]: - ax.plot(tsdata["totim"], tsdata[name], label=name) - ax.set_xlabel("time (d)") - ax.set_ylabel(ylabel[iplot]) - fs.graph_legend(ax) - if config.plotSave: - fpth = os.path.join( - "..", - "figures", - "{}-{}{}".format(sim_name, obs_fig[iplot], config.figure_ext), - ) - fig.savefig(fpth) - return + with styles.USGSMap(): + gwf = sim.get_model(sim_name) + obsnames = gwf.obs[1].output.obs_names + obs_list = [ + gwf.obs[1].output.obs(f=obsnames[0]), + gwf.obs[1].output.obs(f=obsnames[1]), + gwf.ghb.output.obs(), + ] + ylabel = ("head (m)", "flow ($m^3/d$)", "flow ($m^3/d$)") + obs_fig = ("obs-head", "obs-flow", "ghb-obs") + for iplot, obstype in enumerate(obs_list): + fig = plt.figure(figsize=(6, 3)) + ax = fig.add_subplot() + tsdata = obstype.data + for name in tsdata.dtype.names[1:]: + ax.plot(tsdata["totim"], tsdata[name], label=name) + ax.set_xlabel("time (d)") + ax.set_ylabel(ylabel[iplot]) + styles.graph_legend(ax) + if plot_show: + plt.show() + if plot_save: + fpth = os.path.join( + "..", + "figures", + "{}-{}{}".format(sim_name, obs_fig[iplot], ".png"), + ) + fig.savefig(fpth) def plot_results(sim, silent=True): - if config.plotModel: - plot_grid(sim) - plot_ts(sim) - return + plot_grid(sim) + plot_ts(sim) -# Function that wraps all of the steps for the Advgwtidal model -# -# 1. build_model, -# 2. write_model, -# 3. run_model, and -# 4. plot_results. +# - + +# ### Running the example # +# Define and invoke a function to run the example scenario, then plot results. -def simulation(silent=True): - sim = build_model() - write_model(sim, silent=silent) - success = run_model(sim, silent=silent) - if success: +# + +def scenario(silent=True): + sim = build_models() + if write: + write_models(sim, silent=silent) + if run: + run_models(sim, silent=silent) + if plot: plot_results(sim, silent=silent) -# nosetest - exclude block from this nosetest to the next nosetest -def test_01(): - simulation(silent=False) - - -# nosetest end - -if __name__ == "__main__": - # ### Advgwtidal Simulation - # - # Model grid and simulation results - - simulation() +scenario() +# - diff --git a/scripts/ex-gwf-bcf2ss.py b/scripts/ex-gwf-bcf2ss.py index df83b4de1..ac0f8e5ab 100644 --- a/scripts/ex-gwf-bcf2ss.py +++ b/scripts/ex-gwf-bcf2ss.py @@ -1,58 +1,59 @@ -# ## Original BCF2SS MODFLOW example +# ## BCF2SS example # -# This problem is described in McDonald and Harbaugh (1988) and duplicated in -# Harbaugh and McDonald (1996). This problem is also is distributed with +# This problem is described in McDonald and Harbaugh (1988) and duplicated +# in Harbaugh and McDonald (1996). This problem is also is distributed with # MODFLOW-2005 (Harbaugh, 2005) and MODFLOW 6 (Langevin and others, 2017). # +# Two scenarios are included, first solved with the standard method, +# then with the Newton-Raphson formulation. -# ### BCF2SS Problem Setup +# ### Initial setup # -# Imports +# ### Initial setup +# +# Import dependencies, define the example name and workspace, and read settings from environment variables. +# + import os -import sys +import pathlib as pl import flopy import matplotlib as mpl import matplotlib.pyplot as plt import numpy as np +import pooch +from flopy.plot.styles import styles +from modflow_devtools.misc import get_env, timed -# Append to system path to include the common subdirectory - -sys.path.append(os.path.join("..", "common")) - -# import common functionality - -import config -from figspecs import USGSFigure - -# Set figure properties specific to the - -figure_size = (6, 6) - -# Base simulation and model name and workspace - -ws = config.base_ws +# Example name and base workspace +sim_name = "ex-gwf-bcf2ss" +workspace = pl.Path("../examples") -# Simulation name +# Settings from environment variables +write = get_env("WRITE", True) +run = get_env("RUN", True) +plot = get_env("PLOT", True) +plot_show = get_env("PLOT_SHOW", True) +plot_save = get_env("PLOT_SAVE", True) +# - -sim_name = "ex-gwf-bcf2ss" +# ### Define parameters +# +# Define model units, parameters and other settings. +# + # Model units - length_units = "feet" time_units = "days" # Load the wetdry array for layer 1 - -pth = os.path.join("..", "data", sim_name, "wetdry01.txt") -wetdry_layer0 = np.loadtxt( - pth, +pth = pooch.retrieve( + url=f"https://github.com/MODFLOW-USGS/modflow6-examples/raw/master/data/{sim_name}/wetdry01.txt", + known_hash="md5:3a4b357b7d2cd5175a205f3347ab973d", ) +wetdry_layer0 = np.loadtxt(pth) - -# Scenario parameters - +# Scenario-specific parameters parameters = { "ex-gwf-bcf2ss-p01a": { "rewet": True, @@ -72,8 +73,7 @@ }, } -# Table BCF2SS Model Parameters - +# Model parameters nper = 2 # Number of periods nlay = 2 # Number of layers nrow = 10 # Number of rows @@ -88,24 +88,18 @@ strt = 0.0 # Starting head ($ft$) recharge = 0.004 # Recharge rate ($ft/d$) -# Static temporal data used by TDIS file - +# Time discretization tdis_ds = ( (1.0, 1.0, 1), (1.0, 1.0, 1), ) -# parse parameter strings into tuples - +# Parse parameter strings into tuples botm = [float(value) for value in botm_str.split(",")] icelltype = [int(value) for value in icelltype_str.split(",")] k11 = [float(value) for value in k11_str.split(",")] - -# ### Create BCF2SS Model Boundary Conditions - # Well boundary conditions - wel_spd = { 1: [ [1, 2, 3, -35000.0], @@ -114,289 +108,275 @@ } # River boundary conditions - riv_spd = {0: [[1, i, 14, 0.0, 10000.0, -5] for i in range(nrow)]} # Solver parameters - nouter = 500 ninner = 100 hclose = 1e-6 rclose = 1e-3 relax = 0.97 +# - -# ### Functions to build, write, run, and plot the MODFLOW 6 TWRI model +# ### Model setup # -# MODFLOW 6 flopy simulation object (sim) is returned if building the model +# Define functions to build models, write input files, and run the simulation. + + +# + +def build_models(name, rewet, wetfct, iwetit, ihdwet, linear_acceleration, newton): + sim_ws = os.path.join(workspace, name) + sim = flopy.mf6.MFSimulation(sim_name=sim_name, sim_ws=sim_ws, exe_name="mf6") + flopy.mf6.ModflowTdis(sim, nper=nper, perioddata=tdis_ds, time_units=time_units) + flopy.mf6.ModflowIms( + sim, + linear_acceleration=linear_acceleration, + outer_maximum=nouter, + outer_dvclose=hclose, + inner_maximum=ninner, + inner_dvclose=hclose, + rcloserecord=f"{rclose} strict", + relaxation_factor=relax, + ) + gwf = flopy.mf6.ModflowGwf( + sim, modelname=sim_name, save_flows=True, newtonoptions=newton + ) + flopy.mf6.ModflowGwfdis( + gwf, + length_units=length_units, + nlay=nlay, + nrow=nrow, + ncol=ncol, + delr=delr, + delc=delc, + top=top, + botm=botm, + ) + if rewet: + rewet_record = [ + "wetfct", + wetfct, + "iwetit", + iwetit, + "ihdwet", + ihdwet, + ] + wetdry = [wetdry_layer0, 0] + else: + rewet_record = None + wetdry = None + + flopy.mf6.ModflowGwfnpf( + gwf, + rewet_record=rewet_record, + wetdry=wetdry, + icelltype=icelltype, + k=k11, + k33=k33, + save_specific_discharge=True, + ) + flopy.mf6.ModflowGwfic(gwf, strt=strt) + flopy.mf6.ModflowGwfriv(gwf, stress_period_data=riv_spd) + flopy.mf6.ModflowGwfwel(gwf, stress_period_data=wel_spd) + flopy.mf6.ModflowGwfrcha(gwf, recharge=recharge) + head_filerecord = f"{sim_name}.hds" + budget_filerecord = f"{sim_name}.cbc" + flopy.mf6.ModflowGwfoc( + gwf, + head_filerecord=head_filerecord, + budget_filerecord=budget_filerecord, + saverecord=[("HEAD", "ALL"), ("BUDGET", "ALL")], + ) + return sim -def build_model( - name, rewet, wetfct, iwetit, ihdwet, linear_acceleration, newton -): - if config.buildModel: - sim_ws = os.path.join(ws, name) - sim = flopy.mf6.MFSimulation( - sim_name=sim_name, sim_ws=sim_ws, exe_name="mf6" - ) - flopy.mf6.ModflowTdis( - sim, nper=nper, perioddata=tdis_ds, time_units=time_units - ) - flopy.mf6.ModflowIms( - sim, - linear_acceleration=linear_acceleration, - outer_maximum=nouter, - outer_dvclose=hclose, - inner_maximum=ninner, - inner_dvclose=hclose, - rcloserecord=f"{rclose} strict", - relaxation_factor=relax, - ) - gwf = flopy.mf6.ModflowGwf( - sim, modelname=sim_name, save_flows=True, newtonoptions=newton - ) - flopy.mf6.ModflowGwfdis( - gwf, - length_units=length_units, - nlay=nlay, - nrow=nrow, - ncol=ncol, - delr=delr, - delc=delc, - top=top, - botm=botm, - ) - if rewet: - rewet_record = [ - "wetfct", - wetfct, - "iwetit", - iwetit, - "ihdwet", - ihdwet, - ] - wetdry = [wetdry_layer0, 0] - else: - rewet_record = None - wetdry = None - - flopy.mf6.ModflowGwfnpf( - gwf, - rewet_record=rewet_record, - wetdry=wetdry, - icelltype=icelltype, - k=k11, - k33=k33, - save_specific_discharge=True, - ) - flopy.mf6.ModflowGwfic(gwf, strt=strt) - flopy.mf6.ModflowGwfriv(gwf, stress_period_data=riv_spd) - flopy.mf6.ModflowGwfwel(gwf, stress_period_data=wel_spd) - flopy.mf6.ModflowGwfrcha(gwf, recharge=recharge) - head_filerecord = f"{sim_name}.hds" - budget_filerecord = f"{sim_name}.cbc" - flopy.mf6.ModflowGwfoc( - gwf, - head_filerecord=head_filerecord, - budget_filerecord=budget_filerecord, - saverecord=[("HEAD", "ALL"), ("BUDGET", "ALL")], - ) - return sim - return None +def write_models(sim, silent=True): + sim.write_simulation(silent=silent) -# Function to write MODFLOW 6 TWRI model files +@timed +def run_models(sim, silent=True): + success, buff = sim.run_simulation(silent=silent) + assert success, buff -def write_model(sim, silent=True): - if config.writeModel: - sim.write_simulation(silent=silent) +# - -# Function to run the TWRI model. -# True is returned if the model runs successfully +# ### Plotting results # +# Define functions to plot model results. +# + +# Figure properties +figure_size = (6, 6) -@config.timeit -def run_model(sim, silent=True): - success = True - if config.runModel: - success, buff = sim.run_simulation(silent=silent) - if not success: - print(buff) - return success +def plot_simulated_results(num, gwf, ho, co, silent=True): + with styles.USGSMap(): + botm_arr = gwf.dis.botm.array + fig = plt.figure(figsize=(6.8, 6), constrained_layout=False) + gs = mpl.gridspec.GridSpec(ncols=10, nrows=7, figure=fig, wspace=5) + plt.axis("off") -# Function to plot the BCF2SS model results with heads in each layer. -# + ax1 = fig.add_subplot(gs[:3, :5]) + ax2 = fig.add_subplot(gs[:3, 5:], sharey=ax1) + ax3 = fig.add_subplot(gs[3:6, :5], sharex=ax1) + ax4 = fig.add_subplot(gs[3:6, 5:], sharex=ax1, sharey=ax1) + ax5 = fig.add_subplot(gs[6, :]) + axes = [ax1, ax2, ax3, ax4, ax5] + + labels = ("A", "B", "C", "D") + aquifer = ("Upper aquifer", "Lower aquifer") + cond = ("natural conditions", "pumping conditions") + vmin, vmax = -10, 140 + masked_values = [1e30, -1e30] + levels = [ + np.arange(0, 130, 10), + (10, 20, 30, 40, 50, 55, 60), + ] + plot_number = 0 + for idx, totim in enumerate( + ( + 1, + 2, + ) + ): + head = ho.get_data(totim=totim) + head[head < botm_arr] = -1e30 + qx, qy, qz = flopy.utils.postprocessing.get_specific_discharge( + co.get_data(text="DATA-SPDIS", kstpkper=(0, totim - 1))[0], + gwf, + ) + for k in range(nlay): + ax = axes[plot_number] + ax.set_aspect("equal") + mm = flopy.plot.PlotMapView(model=gwf, ax=ax, layer=k) + mm.plot_grid(lw=0.5, color="0.5") + cm = mm.plot_array( + head, masked_values=masked_values, vmin=vmin, vmax=vmax + ) + mm.plot_bc(ftype="WEL", kper=totim - 1) + mm.plot_bc(ftype="RIV", color="green", kper=0) + mm.plot_vector(qx, qy, normalize=True, color="0.75") + cn = mm.contour_array( + head, + masked_values=masked_values, + levels=levels[idx], + colors="black", + linewidths=0.5, + ) + plt.clabel(cn, fmt="%3.0f") + heading = f"{aquifer[k]} under\n{cond[totim - 1]}" + styles.heading(ax, letter=labels[plot_number], heading=heading) + styles.remove_edge_ticks(ax) + + plot_number += 1 + + # set axis labels + ax1.set_ylabel("y-coordinate, in feet") + ax3.set_ylabel("y-coordinate, in feet") + ax3.set_xlabel("x-coordinate, in feet") + ax4.set_xlabel("x-coordinate, in feet") + + # legend + ax = axes[-1] + ax.set_ylim(1, 0) + ax.set_xlim(-5, 5) + ax.set_xticks([]) + ax.set_yticks([]) + ax.spines["top"].set_color("none") + ax.spines["bottom"].set_color("none") + ax.spines["left"].set_color("none") + ax.spines["right"].set_color("none") + ax.patch.set_alpha(0.0) -def plot_simulated_results(num, gwf, ho, co, silent=True): - verbose = not silent - fs = USGSFigure(figure_type="map", verbose=verbose) - - botm_arr = gwf.dis.botm.array - - fig = plt.figure(figsize=(6.8, 6), constrained_layout=False) - gs = mpl.gridspec.GridSpec(ncols=10, nrows=7, figure=fig, wspace=5) - plt.axis("off") - - ax1 = fig.add_subplot(gs[:3, :5]) - ax2 = fig.add_subplot(gs[:3, 5:], sharey=ax1) - ax3 = fig.add_subplot(gs[3:6, :5], sharex=ax1) - ax4 = fig.add_subplot(gs[3:6, 5:], sharex=ax1, sharey=ax1) - ax5 = fig.add_subplot(gs[6, :]) - axes = [ax1, ax2, ax3, ax4, ax5] - - labels = ("A", "B", "C", "D") - aquifer = ("Upper aquifer", "Lower aquifer") - cond = ("natural conditions", "pumping conditions") - vmin, vmax = -10, 140 - masked_values = [1e30, -1e30] - levels = [ - np.arange(0, 130, 10), - (10, 20, 30, 40, 50, 55, 60), - ] - plot_number = 0 - for idx, totim in enumerate( - ( - 1, - 2, + # items for legend + ax.plot( + -1000, + -1000, + "s", + ms=5, + color="green", + mec="black", + mew=0.5, + label="River", ) - ): - head = ho.get_data(totim=totim) - head[head < botm_arr] = -1e30 - qx, qy, qz = flopy.utils.postprocessing.get_specific_discharge( - co.get_data(text="DATA-SPDIS", kstpkper=(0, totim - 1))[0], - gwf, + ax.plot( + -1000, + -1000, + "s", + ms=5, + color="red", + mec="black", + mew=0.5, + label="Well", + ) + ax.plot( + -1000, + -1000, + "s", + ms=5, + color="none", + mec="black", + mew=0.5, + label="Dry cell", + ) + ax.plot( + -10000, + -10000, + lw=0, + marker="$\u2192$", + ms=10, + mfc="0.75", + mec="0.75", + label="Normalized specific discharge", + ) + # ax.plot( + # -1000, + # -1000, + # lw=0.5, + # color="black", + # label="Head, in feet", + # ) + styles.graph_legend( + ax, + ncol=5, + frameon=False, + loc="upper center", ) - for k in range(nlay): - ax = axes[plot_number] - ax.set_aspect("equal") - mm = flopy.plot.PlotMapView(model=gwf, ax=ax, layer=k) - mm.plot_grid(lw=0.5, color="0.5") - cm = mm.plot_array( - head, masked_values=masked_values, vmin=vmin, vmax=vmax - ) - mm.plot_bc(ftype="WEL", kper=totim - 1) - mm.plot_bc(ftype="RIV", color="green", kper=0) - mm.plot_vector(qx, qy, normalize=True, color="0.75") - cn = mm.contour_array( - head, - masked_values=masked_values, - levels=levels[idx], - colors="black", - linewidths=0.5, - ) - plt.clabel(cn, fmt="%3.0f") - heading = f"{aquifer[k]} under {cond[totim - 1]}" - fs.heading(ax, letter=labels[plot_number], heading=heading) - fs.remove_edge_ticks(ax) - - plot_number += 1 - - # set axis labels - ax1.set_ylabel("y-coordinate, in feet") - ax3.set_ylabel("y-coordinate, in feet") - ax3.set_xlabel("x-coordinate, in feet") - ax4.set_xlabel("x-coordinate, in feet") - - # legend - ax = axes[-1] - ax.set_ylim(1, 0) - ax.set_xlim(-5, 5) - ax.set_xticks([]) - ax.set_yticks([]) - ax.spines["top"].set_color("none") - ax.spines["bottom"].set_color("none") - ax.spines["left"].set_color("none") - ax.spines["right"].set_color("none") - ax.patch.set_alpha(0.0) - - # items for legend - ax.plot( - -1000, - -1000, - "s", - ms=5, - color="green", - mec="black", - mew=0.5, - label="River", - ) - ax.plot( - -1000, - -1000, - "s", - ms=5, - color="red", - mec="black", - mew=0.5, - label="Well", - ) - ax.plot( - -1000, - -1000, - "s", - ms=5, - color="none", - mec="black", - mew=0.5, - label="Dry cell", - ) - ax.plot( - -10000, - -10000, - lw=0, - marker="$\u2192$", - ms=10, - mfc="0.75", - mec="0.75", - label="Normalized specific discharge", - ) - ax.plot( - -1000, - -1000, - lw=0.5, - color="black", - label="Head, in feet", - ) - fs.graph_legend( - ax, - ncol=5, - frameon=False, - loc="upper center", - ) - - cbar = plt.colorbar(cm, ax=ax, shrink=0.5, orientation="horizontal") - cbar.ax.set_xlabel("Head, in feet") - - # save figure - if config.plotSave: - fpth = os.path.join( - "..", - "figures", - f"{sim_name}-{num:02d}{config.figure_ext}", + cbar = plt.colorbar( + cm, ax=ax, shrink=0.5, orientation="horizontal", location="bottom" ) - fig.savefig(fpth) + cbar.ax.set_xlabel("Head, in feet") + + if plot_show: + plt.show() + if plot_save: + fig.savefig( + os.path.join( + "..", + "figures", + f"{sim_name}-{num:02d}.png", + ) + ) -# Function to plot simulated results for a simulation +def plot_results(silent=True): + if not plot: + return + if silent: + verbosity_level = 0 + else: + verbosity_level = 1 -def plot_results(silent=True): - if config.plotModel: - verbose = not silent - if silent: - verbosity_level = 0 - else: - verbosity_level = 1 - - fs = USGSFigure(figure_type="map", verbose=verbose) + with styles.USGSMap(): name = list(parameters.keys())[0] - sim_ws = os.path.join(ws, name) + sim_ws = os.path.join(workspace, name) sim = flopy.mf6.MFSimulation.load( sim_name=sim_name, sim_ws=sim_ws, verbosity_level=verbosity_level ) @@ -413,7 +393,7 @@ def plot_results(silent=True): # plot grid fig = plt.figure(figsize=(6.8, 3.5), constrained_layout=True) - gs = mpl.gridspec.GridSpec(nrows=8, ncols=10, figure=fig, wspace=5) + gs = mpl.gridspec.GridSpec(nrows=8, ncols=10, figure=fig, hspace=40, wspace=10) plt.axis("off") ax = fig.add_subplot(gs[:7, 0:7]) @@ -424,8 +404,8 @@ def plot_results(silent=True): mm.plot_grid(lw=0.5, color="0.5") ax.set_ylabel("y-coordinate, in feet") ax.set_xlabel("x-coordinate, in feet") - fs.heading(ax, letter="A", heading="Map view") - fs.remove_edge_ticks(ax) + styles.heading(ax, letter="A", heading="Map view") + styles.remove_edge_ticks(ax) ax = fig.add_subplot(gs[:5, 7:]) mm = flopy.plot.PlotCrossSection(model=gwf, ax=ax, line={"row": 7}) @@ -434,9 +414,9 @@ def plot_results(silent=True): mm.plot_bc(ftype="RIV", color="green", head=head) mm.plot_grid(lw=0.5, color="0.5") ax.set_ylabel("Elevation, in feet") - ax.set_xlabel("x-coordinate along model row 8, in feet") - fs.heading(ax, letter="B", heading="Cross-section view") - fs.remove_edge_ticks(ax) + ax.set_xlabel("x-coordinate along \nrow 8, in feet") + styles.heading(ax, letter="B", heading="Cross-section view") + styles.remove_edge_ticks(ax) # items for legend ax = fig.add_subplot(gs[7, :]) @@ -450,8 +430,8 @@ def plot_results(silent=True): ax.spines["right"].set_color("none") ax.patch.set_alpha(0.0) ax.plot( - -1000, - -1000, + -1100, + -1100, "s", ms=5, color="green", @@ -460,8 +440,8 @@ def plot_results(silent=True): label="River", ) ax.plot( - -1000, - -1000, + -1100, + -1100, "s", ms=5, color="red", @@ -470,8 +450,8 @@ def plot_results(silent=True): label="Well", ) ax.plot( - -1000, - -1000, + -1100, + -1100, "s", ms=5, color="blue", @@ -479,19 +459,20 @@ def plot_results(silent=True): mew=0.5, label="Steady-state water table", ) - fs.graph_legend( + styles.graph_legend( ax, ncol=3, frameon=False, loc="upper center", ) - # save figure - if config.plotSave: + if plot_show: + plt.show() + if plot_save: fpth = os.path.join( "..", "figures", - f"{sim_name}-grid{config.figure_ext}", + f"{sim_name}-grid.png", ) fig.savefig(fpth) @@ -506,21 +487,19 @@ def plot_results(silent=True): cbar.ax.set_ylabel("WETDRY parameter") ax.set_ylabel("y-coordinate, in feet") ax.set_xlabel("x-coordinate, in feet") - fs.remove_edge_ticks(ax) + styles.remove_edge_ticks(ax) - # save figure - if config.plotSave: - fpth = os.path.join( - "..", "figures", f"{sim_name}-01{config.figure_ext}" - ) - fig.savefig(fpth) + if plot_show: + plt.show() + if plot_save: + fig.savefig(os.path.join("..", "figures", f"{sim_name}-01.png")) # plot simulated rewetting results plot_simulated_results(2, gwf, hobj, cobj) # plot simulated newton results name = list(parameters.keys())[1] - sim_ws = os.path.join(ws, name) + sim_ws = os.path.join(workspace, name) sim = flopy.mf6.MFSimulation.load( sim_name=sim_name, sim_ws=sim_ws, verbosity_level=verbosity_level ) @@ -536,48 +515,38 @@ def plot_results(silent=True): plot_simulated_results(3, gwf, hobj, cobj) -# Function that wraps all of the steps for the TWRI model -# -# 1. build_model, -# 2. write_model, and -# 3. run_model -# 4. plot_results. +# - + +# ### Running the example # +# Define a function to run the example scenarios. -def simulation(idx, silent=True): +# + +def scenario(idx, silent=True): key = list(parameters.keys())[idx] params = parameters[key].copy() + sim = build_models(key, **params) + if write: + write_models(sim, silent=silent) + if run: + run_models(sim, silent=silent) - sim = build_model(key, **params) - - write_model(sim, silent=silent) - - success = run_model(sim, silent=silent) - - assert success, f"could not run...{key}" +# - -# nosetest - exclude block from this nosetest to the next nosetest -def test_and_plot(): - simulation(0, silent=False) - simulation(1, silent=False) - plot_results(silent=False) +# Solve the model by the default means. +scenario(0) -# nosetest end +# Solve the model with the Newton-Raphson formulation. -if __name__ == "__main__": - # ### BCF2SS Simulation - # - # Node Property Flow Package with rewetting option +scenario(1) - simulation(0) - - # Newton-Raphson formulation - - simulation(1) +# Plot results. +# + +if plot: # Simulated water levels and normalized specific discharge vectors in the # upper and lower aquifers under natural and pumping conditions using (1) the # rewetting option in the Node Property Flow (NPF) Package with the @@ -585,5 +554,5 @@ def test_and_plot(): # A. Upper aquifer results under natural conditions. B. Lower aquifer results # under natural conditions C. Upper aquifer results under pumping conditions. # D. Lower aquifer results under pumping conditions - plot_results() +# - diff --git a/scripts/ex-gwf-bump.py b/scripts/ex-gwf-bump.py index d569e617b..d1c9c4909 100644 --- a/scripts/ex-gwf-bump.py +++ b/scripts/ex-gwf-bump.py @@ -1,48 +1,47 @@ # ## Flow diversion example # -# +# This example simulates unconfined groundwater flow in an aquifer with a high bottom elevation in the center of the aquifer and groundwater flow around a high bottom elevation. -# ### Flow diversion Problem Setup +# ### Initial setup +# +# ### Initial setup # -# Imports +# Import dependencies, define the example name and workspace, and read settings from environment variables. +# + import os -import sys +import pathlib as pl import flopy import matplotlib as mpl import matplotlib.pyplot as plt import numpy as np +import pooch +from flopy.plot.styles import styles +from modflow_devtools.misc import get_env, timed -# Append to system path to include the common subdirectory - -sys.path.append(os.path.join("..", "common")) - -# import common functionality - -import config -from figspecs import USGSFigure - -# Set figure properties - -figure_size = (4, 5.33) -masked_values = (1e30, -1e30) - -# Base simulation and model name and workspace - -ws = config.base_ws +# Example name and base workspace +sim_name = "ex-gwf-bump" +workspace = pl.Path("../examples") -# Simulation name +# Settings from environment variables +write = get_env("WRITE", True) +run = get_env("RUN", True) +plot = get_env("PLOT", True) +plot_show = get_env("PLOT_SHOW", True) +plot_save = get_env("PLOT_SAVE", True) +# - -sim_name = "ex-gwf-bump" +# ### Define parameters +# +# Define model units, parameters and other settings. +# + # Model units - length_units = "meters" time_units = "days" -# Scenario parameters - +# Scenario-specific parameters parameters = { "ex-gwf-bump-p01a": { "newton": "newton", @@ -60,8 +59,7 @@ }, } -# Table - +# Model parameters nper = 1 # Number of periods nlay = 1 # Number of layers nrow = 51 # Number of rows @@ -73,34 +71,23 @@ H1 = 7.5 # Constant head in column 1 and starting head ($m$) H2 = 2.5 # Constant head in column 51 ($m$) -# plotting ranges and contour levels - -vmin, vmax = H2, H1 -bmin, bmax = 0, 10 -vlevels = np.arange(vmin + 0.5, vmax, 1) -blevels = np.arange(bmin + 2, bmax, 2) -bcolor = "black" -vcolor = "black" - - -# Static temporal data used by TDIS file - +# Time discretization tdis_ds = ((1.0, 1, 1.0),) # Calculate delr, delc, extents, and shape3d - delr = xlen / float(ncol) delc = ylen / float(nrow) extents = (0, xlen, 0, ylen) shape3d = (nlay, nrow, ncol) # Load the bottom - -fpth = os.path.join("..", "data", sim_name, "bottom.txt") +fpth = pooch.retrieve( + url=f"https://github.com/MODFLOW-USGS/modflow6-examples/raw/master/data/{sim_name}/bottom.txt", + known_hash="md5:9287f9e214147d95e6ed159732079a0b", +) botm = np.loadtxt(fpth).reshape(shape3d) -# create a cylinder - +# Create a cylinder cylinder = botm.copy() cylinder[cylinder < 7.5] = 0.0 cylinder[cylinder >= 7.5] = 20.0 @@ -110,18 +97,19 @@ chd_spd += [[0, i, ncol - 1, H2] for i in range(nrow)] # Solver parameters - nouter = 500 ninner = 500 hclose = 1e-9 rclose = 1e-6 +# - -# ### Functions to build, write, run, and plot the model +# ### Model setup # -# MODFLOW 6 flopy simulation object (sim) is returned if building the model +# Define functions to build models, write input files, and run the simulation. -def build_model( +# + +def build_models( name, newton=False, rewet=False, @@ -131,110 +119,107 @@ def build_model( ihdwet=None, wetdry=None, ): - if config.buildModel: - sim_ws = os.path.join(ws, name) - sim = flopy.mf6.MFSimulation( - sim_name=sim_name, sim_ws=sim_ws, exe_name="mf6" - ) - flopy.mf6.ModflowTdis( - sim, nper=nper, perioddata=tdis_ds, time_units=time_units - ) - if newton: - linear_acceleration = "bicgstab" - newtonoptions = "newton under_relaxation" - else: - linear_acceleration = "cg" - newtonoptions = None - - flopy.mf6.ModflowIms( - sim, - print_option="ALL", - linear_acceleration=linear_acceleration, - outer_maximum=nouter, - outer_dvclose=hclose, - inner_maximum=ninner, - inner_dvclose=hclose, - rcloserecord=rclose, - ) - gwf = flopy.mf6.ModflowGwf( - sim, - modelname=sim_name, - newtonoptions=newtonoptions, - save_flows=True, - ) - if cylindrical: - bot = cylinder - else: - bot = botm - flopy.mf6.ModflowGwfdis( - gwf, - length_units=length_units, - nlay=nlay, - nrow=nrow, - ncol=ncol, - delr=delr, - delc=delc, - top=top, - botm=bot, - ) - if rewet: - rewet_record = [ - "wetfct", - wetfct, - "iwetit", - iwetit, - "ihdwet", - ihdwet, - ] - else: - rewet_record = None - flopy.mf6.ModflowGwfnpf( - gwf, - rewet_record=rewet_record, - icelltype=1, - k=k11, - wetdry=wetdry, - save_specific_discharge=True, - ) - flopy.mf6.ModflowGwfic(gwf, strt=H1) - flopy.mf6.ModflowGwfchd(gwf, stress_period_data=chd_spd) - - head_filerecord = f"{sim_name}.hds" - budget_filerecord = f"{sim_name}.cbc" - flopy.mf6.ModflowGwfoc( - gwf, - head_filerecord=head_filerecord, - budget_filerecord=budget_filerecord, - saverecord=[("HEAD", "ALL"), ("BUDGET", "ALL")], - ) - return sim - return None - - -# Function to write flow diversion model files + sim_ws = os.path.join(workspace, name) + sim = flopy.mf6.MFSimulation(sim_name=sim_name, sim_ws=sim_ws, exe_name="mf6") + flopy.mf6.ModflowTdis(sim, nper=nper, perioddata=tdis_ds, time_units=time_units) + if newton: + linear_acceleration = "bicgstab" + newtonoptions = "newton under_relaxation" + else: + linear_acceleration = "cg" + newtonoptions = None + + flopy.mf6.ModflowIms( + sim, + print_option="ALL", + linear_acceleration=linear_acceleration, + outer_maximum=nouter, + outer_dvclose=hclose, + inner_maximum=ninner, + inner_dvclose=hclose, + rcloserecord=rclose, + ) + gwf = flopy.mf6.ModflowGwf( + sim, + modelname=sim_name, + newtonoptions=newtonoptions, + save_flows=True, + ) + if cylindrical: + bot = cylinder + else: + bot = botm + flopy.mf6.ModflowGwfdis( + gwf, + length_units=length_units, + nlay=nlay, + nrow=nrow, + ncol=ncol, + delr=delr, + delc=delc, + top=top, + botm=bot, + ) + if rewet: + rewet_record = [ + "wetfct", + wetfct, + "iwetit", + iwetit, + "ihdwet", + ihdwet, + ] + else: + rewet_record = None + flopy.mf6.ModflowGwfnpf( + gwf, + rewet_record=rewet_record, + icelltype=1, + k=k11, + wetdry=wetdry, + save_specific_discharge=True, + ) + flopy.mf6.ModflowGwfic(gwf, strt=H1) + flopy.mf6.ModflowGwfchd(gwf, stress_period_data=chd_spd) + + head_filerecord = f"{sim_name}.hds" + budget_filerecord = f"{sim_name}.cbc" + flopy.mf6.ModflowGwfoc( + gwf, + head_filerecord=head_filerecord, + budget_filerecord=budget_filerecord, + saverecord=[("HEAD", "ALL"), ("BUDGET", "ALL")], + ) + return sim -def write_model(sim, silent=True): - if config.writeModel: - sim.write_simulation(silent=silent) +def write_models(sim, silent=True): + sim.write_simulation(silent=silent) -# Function to run the model. True is returned if the model runs successfully. -# +@timed +def run_models(sim, silent=True): + success, buff = sim.run_simulation(silent=silent) + assert success, buff -@config.timeit -def run_model(sim, silent=True): - success = True - if config.runModel: - success, buff = sim.run_simulation(silent=silent) - if not success: - print(buff) +# - - return success +# ### Plotting results +# +# Define functions to plot model results. -# Function to create a figure +# + +# Figure properties, plotting ranges and contour levels +figure_size = (4, 5.33) +masked_values = (1e30, -1e30) +vmin, vmax = H2, H1 +bmin, bmax = 0, 10 +vlevels = np.arange(vmin + 0.5, vmax, 1) +blevels = np.arange(bmin + 2, bmax, 2) +bcolor = "black" +vcolor = "black" def create_figure(): @@ -270,87 +255,72 @@ def create_figure(): return fig, axes -# Function to plot the grid - - def plot_grid(gwf, silent=True): - verbose = not silent - fs = USGSFigure(figure_type="map", verbose=verbose) - - bot = gwf.dis.botm.array - - fig, axes = create_figure() - ax = axes[0] - mm = flopy.plot.PlotMapView(gwf, ax=ax, extent=extents) - bot_coll = mm.plot_array(bot, vmin=bmin, vmax=bmax) - mm.plot_bc("CHD", color="cyan") - cv = mm.contour_array( - bot, - levels=blevels, - linewidths=0.5, - linestyles=":", - colors=bcolor, - ) - plt.clabel(cv, fmt="%1.0f") - ax.set_xlabel("x-coordinate, in meters") - ax.set_ylabel("y-coordinate, in meters") - fs.remove_edge_ticks(ax) - - # legend - ax = axes[1] - ax.plot( - -10000, - -10000, - lw=0, - marker="s", - ms=10, - mfc="cyan", - mec="cyan", - label="Constant Head", - ) - ax.plot( - -10000, - -10000, - lw=0.5, - ls=":", - color=bcolor, - label="Bottom elevation contour, m", - ) - fs.graph_legend(ax, loc="center", ncol=2) - - cax = plt.axes([0.275, 0.125, 0.45, 0.025]) - cbar = plt.colorbar( - bot_coll, - shrink=0.8, - orientation="horizontal", - cax=cax, - ) - cbar.ax.tick_params(size=0) - cbar.ax.set_xlabel(r"Bottom Elevation, $m$") - - # save figure - if config.plotSave: - fpth = os.path.join( - "..", - "figures", - f"{sim_name}-grid{config.figure_ext}", + with styles.USGSMap() as fs: + bot = gwf.dis.botm.array + fig, axes = create_figure() + ax = axes[0] + mm = flopy.plot.PlotMapView(gwf, ax=ax, extent=extents) + bot_coll = mm.plot_array(bot, vmin=bmin, vmax=bmax) + mm.plot_bc("CHD", color="cyan") + cv = mm.contour_array( + bot, + levels=blevels, + linewidths=0.5, + linestyles=":", + colors=bcolor, ) - fig.savefig(fpth) + plt.clabel(cv, fmt="%1.0f") + ax.set_xlabel("x-coordinate, in meters") + ax.set_ylabel("y-coordinate, in meters") + styles.remove_edge_ticks(ax) - return + # legend + ax = axes[1] + ax.plot( + -10000, + -10000, + lw=0, + marker="s", + ms=10, + mfc="cyan", + mec="cyan", + label="Constant Head", + ) + ax.plot( + -10000, + -10000, + lw=0.5, + ls=":", + color=bcolor, + label="Bottom elevation contour, m", + ) + styles.graph_legend(ax, loc="center", ncol=2) + cax = plt.axes([0.275, 0.125, 0.45, 0.025]) + cbar = plt.colorbar( + bot_coll, + shrink=0.8, + orientation="horizontal", + cax=cax, + ) + cbar.ax.tick_params(size=0) + cbar.ax.set_xlabel(r"Bottom Elevation, $m$") -# Function to plot the model results. + if plot_show: + plt.show() + if plot_save: + fpth = os.path.join( + "..", + "figures", + f"{sim_name}-grid.png", + ) + fig.savefig(fpth) def plot_results(idx, sim, silent=True): - verbose = not silent - if config.plotModel: - fs = USGSFigure(figure_type="map", verbose=verbose) - name = list(parameters.keys())[idx] - sim_ws = os.path.join(ws, name) + with styles.USGSMap(): gwf = sim.get_model(sim_name) - bot = gwf.dis.botm.array if idx == 0: @@ -403,7 +373,7 @@ def plot_results(idx, sim, silent=True): mm.plot_vector(qx, qy, normalize=True, color="0.75", zorder=11) ax.set_xlabel("x-coordinate, in meters") ax.set_ylabel("y-coordinate, in meters") - fs.remove_edge_ticks(ax) + styles.remove_edge_ticks(ax) # create legend ax = axes[-1] @@ -444,75 +414,57 @@ def plot_results(idx, sim, silent=True): color=vcolor, label="Head contour, m", ) - fs.graph_legend(ax, loc="center", ncol=2) + styles.graph_legend(ax, loc="center", ncol=2) cax = plt.axes([0.275, 0.125, 0.45, 0.025]) - cbar = plt.colorbar( - h_coll, shrink=0.8, orientation="horizontal", cax=cax - ) + cbar = plt.colorbar(h_coll, shrink=0.8, orientation="horizontal", cax=cax) cbar.ax.tick_params(size=0) cbar.ax.set_xlabel(r"Head, $m$", fontsize=9) - # save figure - if config.plotSave: - fpth = os.path.join( - "..", - "figures", - f"{sim_name}-{idx + 1:02d}{config.figure_ext}", + if plot_show: + plt.show() + if plot_save: + fig.savefig( + os.path.join( + "..", + "figures", + f"{sim_name}-{idx + 1:02d}.png", + ) ) - fig.savefig(fpth) -# Function that wraps all of the steps for the TWRI model -# -# 1. build_model, -# 2. write_model, -# 3. run_model, and -# 4. plot_results. +# - + +# ### Running the example # +# Define a function to run the example scenarios and plot results. -def simulation(idx, silent=True): +# + +def scenario(idx, silent=True): key = list(parameters.keys())[idx] params = parameters[key].copy() - - sim = build_model(key, **params) - - write_model(sim, silent=silent) - - success = run_model(sim, silent=silent) - - if success: + sim = build_models(key, **params) + if write: + write_models(sim, silent=silent) + if run: + run_models(sim, silent=silent) + if plot: plot_results(idx, sim, silent=silent) -# nosetest - exclude block from this nosetest to the next nosetest -def test_01(): - simulation(0, silent=False) - - -def test_02(): - simulation(1, silent=False) - - -def test_03(): - simulation(2, silent=False) - +# - -# nosetest end -if __name__ == "__main__": - # ### Zaidel Simulation - # - # Simulated heads in the flow diversion model with Newton-Raphson. +# Run the flow diversion model with Newton-Raphson and plot simulated heads. - simulation(0) +scenario(0, silent=False) - # Simulated heads in the flow diversion model with rewetting. +# Run the flow diversion model with rewetting and plot simulated heads. - simulation(1) +scenario(1, silent=False) - # Simulated heads in the flow diversion model with Newton-Raphson and - # cylinderical topography. +# Run the flow diversion model with Newton-Raphson and +# cylindrical topography and plot simulated heads. - simulation(2) +scenario(2, silent=False) diff --git a/scripts/ex-gwf-capture.py b/scripts/ex-gwf-capture.py index 3bd258165..16049c8bd 100644 --- a/scripts/ex-gwf-capture.py +++ b/scripts/ex-gwf-capture.py @@ -8,10 +8,13 @@ # package model. # -# ### Capture Fraction Problem Setup +# ### Initial setup # -# Imports +# ### Initial setup +# +# Import dependencies, define the example name and workspace, and read settings from environment variables. +# + import os import pathlib as pl import shutil @@ -22,49 +25,49 @@ import matplotlib.pyplot as plt import modflowapi import numpy as np +import pooch +from flopy.plot.styles import styles +from modflow_devtools.misc import get_env, timed -# Append to system path to include the common subdirectory - -sys.path.append(os.path.join("..", "common")) - -# import common functionality - -import config -from figspecs import USGSFigure - -# Set figure properties specific to the - -figure_size = (6, 6) - -# Base simulation and model name and workspace - -ws = config.base_ws +# Example name and base workspace +sim_name = "ex-gwf-capture" +workspace = pl.Path("../examples") -# Simulation name +# Settings from environment variables +write = get_env("WRITE", True) +run = get_env("RUN", True) +plot = get_env("PLOT", True) +plot_show = get_env("PLOT_SHOW", True) +plot_save = get_env("PLOT_SAVE", True) +# - -sim_name = "ex-gwf-capture" +# ### Define parameters +# +# Define model units, parameters and other settings. +# + # Model units - length_units = "meters" time_units = "seconds" # Load the bottom, hydraulic conductivity, and idomain arrays - -bottom = np.loadtxt( - os.path.join("..", "data", sim_name, "bottom.txt"), +pth = pooch.retrieve( + url=f"https://github.com/MODFLOW-USGS/modflow6-examples/raw/master/data/{sim_name}/bottom.txt", + known_hash="md5:201758a5b7febb0390b8b52e634be27f", ) -k11 = np.loadtxt( - os.path.join("..", "data", sim_name, "hydraulic_conductivity.txt"), +bottom = np.loadtxt(pth) +pth = pooch.retrieve( + url=f"https://github.com/MODFLOW-USGS/modflow6-examples/raw/master/data/{sim_name}/hydraulic_conductivity.txt", + known_hash="md5:6c78564ba92e850d7d51d6e957b8a3ff", ) -idomain = np.loadtxt( - os.path.join("..", "data", sim_name, "idomain.txt"), - dtype=np.int32, +k11 = np.loadtxt(pth) +pth = pooch.retrieve( + url=f"https://github.com/MODFLOW-USGS/modflow6-examples/raw/master/data/{sim_name}/idomain.txt", + known_hash="md5:435d4490adff7a35d1d4928661e45d81", ) +idomain = np.loadtxt(pth, dtype=np.int32) - -# Table Capture Fraction Model Parameters - +# Model parameters nper = 1 # Number of periods nlay = 1 # Number of layers nrow = 40 # Number of rows @@ -77,15 +80,10 @@ recharge = 1.60000000e-09 # Recharge rate ($m/s$) cf_q = -1e-3 # Perturbation flux ($m/s$) -# Static temporal data used by TDIS file - +# Temporal discretization tdis_ds = ((1.0, 1.0, 1),) - -# ### Create Capture Fraction Model Boundary Conditions - # Well boundary conditions - wel_spd = { 0: [ [0, 8, 15, -0.00820000], @@ -98,7 +96,6 @@ } # Constant head boundary conditions - chd_spd = { 0: [ [0, 39, 5, 16.90000000], @@ -115,7 +112,6 @@ } # River boundary conditions - rbot = np.linspace(20.0, 10.25, num=nrow) rstage = np.linspace(20.1, 11.25, num=nrow) riv_spd = [] @@ -124,98 +120,85 @@ riv_spd = {0: riv_spd} # Solver parameters - nouter = 100 ninner = 25 hclose = 1e-9 rclose = 1e-3 - # Create mapping array for the capture zone analysis imap = idomain.copy() for _k, i, j, _h in chd_spd[0]: imap[i, j] = 0 +# - - -# ### Functions to build, write, run, and plot the MODFLOW 6 Capture Zone model +# ### Model setup # -# MODFLOW 6 flopy simulation object (sim) is returned if building the model - - -def build_model(): - if config.buildModel: - sim_ws = os.path.join(ws, sim_name) - sim = flopy.mf6.MFSimulation( - sim_name=sim_name, - sim_ws=sim_ws, - exe_name="mf6", - ) - flopy.mf6.ModflowTdis( - sim, nper=nper, perioddata=tdis_ds, time_units=time_units - ) - flopy.mf6.ModflowIms( - sim, - linear_acceleration="BICGSTAB", - outer_maximum=nouter, - outer_dvclose=hclose * 10.0, - inner_maximum=ninner, - inner_dvclose=hclose, - rcloserecord=f"{rclose} strict", - ) - gwf = flopy.mf6.ModflowGwf( - sim, - modelname=sim_name, - newtonoptions="NEWTON UNDER_RELAXATION", - ) - flopy.mf6.ModflowGwfdis( - gwf, - length_units=length_units, - nlay=nlay, - nrow=nrow, - ncol=ncol, - delr=delr, - delc=delc, - top=top, - botm=bottom, - idomain=idomain, - ) - - flopy.mf6.ModflowGwfnpf( - gwf, - icelltype=icelltype, - k=k11, - ) - flopy.mf6.ModflowGwfic(gwf, strt=strt) - flopy.mf6.ModflowGwfriv(gwf, stress_period_data=riv_spd, pname="RIV-1") - flopy.mf6.ModflowGwfwel(gwf, stress_period_data=wel_spd, pname="WEL-1") - flopy.mf6.ModflowGwfrcha(gwf, recharge=recharge) - flopy.mf6.ModflowGwfchd(gwf, stress_period_data=chd_spd) - flopy.mf6.ModflowGwfwel( - gwf, - maxbound=1, - pname="CF-1", - filename=f"{sim_name}.cf.wel", - ) - flopy.mf6.ModflowGwfoc( - gwf, - printrecord=[ - ("BUDGET", "ALL"), - ], - ) - return sim - else: - return None - - -# Function to write MODFLOW 6 Capture Fraction model files - - -def write_model(sim, silent=True): - if config.writeModel: - sim.write_simulation(silent=silent) - - -# Function to solve the system of equations to convergence +# Define functions to build models, write input files, and run the simulation. + + +# + +def build_models(): + sim_ws = os.path.join(workspace, sim_name) + sim = flopy.mf6.MFSimulation( + sim_name=sim_name, + sim_ws=sim_ws, + exe_name="mf6", + ) + flopy.mf6.ModflowTdis(sim, nper=nper, perioddata=tdis_ds, time_units=time_units) + flopy.mf6.ModflowIms( + sim, + linear_acceleration="BICGSTAB", + outer_maximum=nouter, + outer_dvclose=hclose * 10.0, + inner_maximum=ninner, + inner_dvclose=hclose, + rcloserecord=f"{rclose} strict", + ) + gwf = flopy.mf6.ModflowGwf( + sim, + modelname=sim_name, + newtonoptions="NEWTON UNDER_RELAXATION", + ) + flopy.mf6.ModflowGwfdis( + gwf, + length_units=length_units, + nlay=nlay, + nrow=nrow, + ncol=ncol, + delr=delr, + delc=delc, + top=top, + botm=bottom, + idomain=idomain, + ) + + flopy.mf6.ModflowGwfnpf( + gwf, + icelltype=icelltype, + k=k11, + ) + flopy.mf6.ModflowGwfic(gwf, strt=strt) + flopy.mf6.ModflowGwfriv(gwf, stress_period_data=riv_spd, pname="RIV-1") + flopy.mf6.ModflowGwfwel(gwf, stress_period_data=wel_spd, pname="WEL-1") + flopy.mf6.ModflowGwfrcha(gwf, recharge=recharge) + flopy.mf6.ModflowGwfchd(gwf, stress_period_data=chd_spd) + flopy.mf6.ModflowGwfwel( + gwf, + maxbound=1, + pname="CF-1", + filename=f"{sim_name}.cf.wel", + ) + flopy.mf6.ModflowGwfoc( + gwf, + printrecord=[ + ("BUDGET", "ALL"), + ], + ) + return sim + + +def write_models(sim, silent=True): + sim.write_simulation(silent=silent) def capture_fraction_iteration(mobj, q, inode=None): @@ -233,9 +216,6 @@ def capture_fraction_iteration(mobj, q, inode=None): return qriv -# Function to update the Capture Fraction Package - - def update_wel_pak(mobj, inode, q): # set nodelist to inode tag = mobj.get_var_address("NODELIST", sim_name, "CF-1") @@ -254,73 +234,70 @@ def update_wel_pak(mobj, inode, q): mobj.set_value(tag, bound) -# Function to get the streamflow from memory - - def get_streamflow(mobj): tag = mobj.get_var_address("SIMVALS", sim_name, "RIV-1") return mobj.get_value(tag).sum() -# Function to run the Capture Fraction model. -# True is returned if the model runs successfully -# - +@timed +def run_models(): + soext = ".so" + if sys.platform.lower() == "win32": + soext = ".dll" + if sys.platform.lower() == "darwin": + soext = ".dylib" + libmf6_path = pl.Path(shutil.which("mf6")).parent / f"libmf6{soext}" + sim_ws = os.path.join(workspace, sim_name) + mf6 = modflowapi.ModflowApi(libmf6_path, working_directory=sim_ws) + qbase = capture_fraction_iteration(mf6, cf_q) -@config.timeit -def run_model(): - success = True - if config.runModel: - libmf6_path = ( - pl.Path(shutil.which("mf6")).parent / f"libmf6{config.soext}" - ) - sim_ws = os.path.join(ws, sim_name) - mf6 = modflowapi.ModflowApi(libmf6_path, working_directory=sim_ws) - qbase = capture_fraction_iteration(mf6, cf_q) + # create capture fraction array + capture = np.zeros((nrow, ncol), dtype=float) - # create capture fraction array - capture = np.zeros((nrow, ncol), dtype=float) + # iterate through each active cell + ireduced_node = -1 + for irow in range(nrow): + for jcol in range(ncol): + # skip inactive cells + if imap[irow, jcol] < 1: + continue - # iterate through each active cell - ireduced_node = -1 - for irow in range(nrow): - for jcol in range(ncol): - # skip inactive cells - if imap[irow, jcol] < 1: - continue + # increment reduced node number + ireduced_node += 1 - # increment reduced node number - ireduced_node += 1 + # calculate the perturbed river flow + qriv = capture_fraction_iteration(mf6, cf_q, inode=ireduced_node) - # calculate the perturbed river flow - qriv = capture_fraction_iteration( - mf6, cf_q, inode=ireduced_node - ) + # add the value to the capture array + capture[irow, jcol] = (qriv - qbase) / abs(cf_q) - # add the value to the capture array - capture[irow, jcol] = (qriv - qbase) / abs(cf_q) + # save the capture fraction array + fpth = os.path.join(sim_ws, "capture.npz") + np.savez_compressed(fpth, capture=capture) - # save the capture fraction array - fpth = os.path.join(sim_ws, "capture.npz") - np.savez_compressed(fpth, capture=capture) - return success +# - - -# Function to plot the Capture Fraction model results with heads in each layer. +# ### Plotting results # +# Define functions to plot model results. + +# + +# Figure properties +figure_size = (6, 6) def plot_results(silent=True): - if config.plotModel: - verbose = not silent - if silent: - verbosity_level = 0 - else: - verbosity_level = 1 - - fs = USGSFigure(figure_type="map", verbose=verbose) - sim_ws = os.path.join(ws, sim_name) + if not plot: + return + + if silent: + verbosity_level = 0 + else: + verbosity_level = 1 + + with styles.USGSMap(): + sim_ws = os.path.join(workspace, sim_name) sim = flopy.mf6.MFSimulation.load( sim_name=sim_name, sim_ws=sim_ws, verbosity_level=verbosity_level ) @@ -353,7 +330,7 @@ def plot_results(silent=True): mm.plot_ibound() ax.set_ylabel("y-coordinate, in feet") ax.set_xlabel("x-coordinate, in feet") - fs.remove_edge_ticks(ax) + styles.remove_edge_ticks(ax) ax = fig.add_subplot(gs[0, 1]) ax.set_xlim(0, 1) @@ -404,56 +381,38 @@ def plot_results(silent=True): mew=0.5, label="Inactive cell", ) - fs.graph_legend( + styles.graph_legend( ax, ncol=1, frameon=False, loc="upper center", ) - # save figure - if config.plotSave: - fpth = os.path.join( - "..", "figures", f"{sim_name}-01{config.figure_ext}" - ) - fig.savefig(fpth) - - -# Function that wraps all of the steps for the Streamflow Capture model -# -# 1. build_model, -# 2. write_model, and -# 3. run_model -# 4. plot_results. -# - - -def simulation(silent=True): - sim = build_model() + if plot_show: + plt.show() + if plot_save: + fig.savefig(os.path.join("..", "figures", f"{sim_name}-01.png")) - write_model(sim, silent=silent) - success = run_model() - - assert success, f"could not run...{sim_name}" - - -# nosetest - exclude block from this nosetest to the next nosetest -def test_01(): - simulation(silent=False) - plot_results(silent=False) +# - +# ### Running the example +# +# Define and invoke a function to run the example scenario, then plot results. -# nosetest end -if __name__ == "__main__": - # ### Capture Zone Simulation - # - # Capture zone examples using the MODFLOW API with the Freyberg (1988) model +# + +def scenario(silent=True): + sim = build_models() + if write: + write_models(sim, silent=silent) + if run: + run_models() - simulation() - # Simulated streamflow capture fraction map for the Freyberg (1988) groundwater - # flow model. +scenario() +if plot: + # Simulated streamflow capture fraction map for the Freyberg (1988) groundwater flow model. plot_results() +# - diff --git a/scripts/ex-gwf-csub-p01.py b/scripts/ex-gwf-csub-p01.py index d50f7cc4d..c98680891 100644 --- a/scripts/ex-gwf-csub-p01.py +++ b/scripts/ex-gwf-csub-p01.py @@ -1,56 +1,52 @@ -# ## Jacob (1939) Elastic Aquifer Loading +# ## Jacob (1939) elastic aquifer loading example # # This problem simulates elastic compaction of aquifer materials in response to the # loading of an aquifer by a passing train. Water-level responses were simulated for # an eastbound train leaving the Smithtown Station in Long Island, New York at 13:04 # on April 23, 1937 -# -# ### Problem Setup +# ### Initial setup # -# Imports +# Import dependencies, define the example name and workspace, and read settings from environment variables. +# + import datetime import os -import sys +import pathlib as pl import flopy import matplotlib as mpl import matplotlib.pyplot as plt import numpy as np +import pooch +from flopy.plot.styles import styles +from modflow_devtools.misc import get_env, timed -# Append to system path to include the common subdirectory - -sys.path.append(os.path.join("..", "common")) - -# import common functionality - -import config -from figspecs import USGSFigure - -# Set figure properties specific to the - -figure_size = (6.8, 4.5) - -# Base simulation and model name and workspace - -ws = config.base_ws +# Example name and base workspace +sim_name = "ex-gwf-csub-p01" +workspace = pl.Path("../examples") -# Simulation name +# Settings from environment variables +write = get_env("WRITE", True) +run = get_env("RUN", True) +plot = get_env("PLOT", True) +plot_show = get_env("PLOT_SHOW", True) +plot_save = get_env("PLOT_SAVE", True) +# - -sim_name = "ex-gwf-csub-p01" +# ### Define parameters +# +# Define model units, parameters and other settings. +# + # Model units - length_units = "meters" time_units = "seconds" # Simulation starting date and time - dstart = datetime.datetime(1937, 4, 23, 13, 5, 55) -# Table - +# Model parameters nper = 2 # Number of periods nlay = 3 # Number of layers ncol = 35 # Number of columns @@ -62,19 +58,14 @@ botm_str = "-12.2, -21.3, -30.5" # Layer bottom elevations ($m$) strt = -10.7 # Starting head ($m$) icelltype_str = "1, 0, 0" # Cell conversion type -k11_str = ( - "1.8e-5, 3.5e-10, 3.1e-5" # Horizontal hydraulic conductivity ($m/s$) -) +k11_str = "1.8e-5, 3.5e-10, 3.1e-5" # Horizontal hydraulic conductivity ($m/s$) sy_str = "0.1, 0.05, 0.25" # Specific yield (unitless) sgm = 1.7 # Specific gravity of moist soils (unitless) sgs = 2.0 # Specific gravity of saturated soils (unitless) -cg_ske_str = ( - "3.3e-5, 6.6e-4, 4.5e-7" # Coarse grained elastic storativity (1/$m$) -) +cg_ske_str = "3.3e-5, 6.6e-4, 4.5e-7" # Coarse grained elastic storativity (1/$m$) cg_theta_str = "0.25, 0.50, 0.30" # Coarse-grained porosity (unitless) # Create delr from delr0 and delrmac - delr = np.ones(ncol, dtype=float) * 0.5 xmax = delr[0] for idx in range(1, ncol): @@ -83,40 +74,33 @@ delr[idx] = dx # Location of the observation well - locw201 = 11 # Load the aquifer load time series - -pth = os.path.join("..", "data", sim_name, "train_load_193704231304.csv") +pth = pooch.retrieve( + url=f"https://github.com/MODFLOW-USGS/modflow6-examples/raw/master/data/{sim_name}/train_load_193704231304.csv", + known_hash="md5:32dc8e7b7e39876374af43605e264725", +) csv_load = np.genfromtxt(pth, names=True, delimiter=",") # Reformat csv data into format for MODFLOW 6 timeseries file - csub_ts = [] for idx in range(csv_load.shape[0]): csub_ts.append((csv_load["sim_time"][idx], csv_load["load"][idx])) # Static temporal data used by TDIS file - tdis_ds = ( (0.5, 1, 1.0), (csv_load["sim_time"][-1] - 0.5, csv_load["sim_time"].shape[0] - 2, 1), ) - # Simulation starting date and time - dstart = datetime.datetime(1937, 4, 23, 13, 5, 55) # Create a datetime list - -date_list = [ - dstart + datetime.timedelta(seconds=x) for x in csv_load["sim_time"] -] +date_list = [dstart + datetime.timedelta(seconds=x) for x in csv_load["sim_time"]] # parse parameter strings into tuples - botm = [float(value) for value in botm_str.split(",")] k11 = [float(value) for value in k11_str.split(",")] icelltype = [int(value) for value in icelltype_str.split(",")] @@ -125,140 +109,121 @@ cg_theta = [float(value) for value in cg_theta_str.split(",")] # Solver parameters - nouter = 500 ninner = 300 hclose = 1e-9 rclose = 1e-6 linaccel = "bicgstab" relax = 1.0 +# - - -# ### Functions to build, write, run, and plot the model +# ### Model setup # -# MODFLOW 6 flopy simulation object (sim) is returned if building the model - - -def build_model(): - if config.buildModel: - sim_ws = os.path.join(ws, sim_name) - sim = flopy.mf6.MFSimulation( - sim_name=sim_name, sim_ws=sim_ws, exe_name="mf6" - ) - flopy.mf6.ModflowTdis( - sim, nper=nper, perioddata=tdis_ds, time_units=time_units - ) - flopy.mf6.ModflowIms( - sim, - outer_maximum=nouter, - outer_dvclose=hclose, - linear_acceleration=linaccel, - inner_maximum=ninner, - inner_dvclose=hclose, - relaxation_factor=relax, - rcloserecord=f"{rclose} strict", - ) - gwf = flopy.mf6.ModflowGwf( - sim, modelname=sim_name, save_flows=True, newtonoptions="newton" - ) - flopy.mf6.ModflowGwfdis( - gwf, - length_units=length_units, - nlay=nlay, - nrow=nrow, - ncol=ncol, - delr=delr, - delc=delc, - top=top, - botm=botm, - ) - obs_recarray = { - "gwf_calib_obs.csv": [("w3_1_1", "HEAD", (2, 0, locw201))] - } - flopy.mf6.ModflowUtlobs( - gwf, digits=10, print_input=True, continuous=obs_recarray - ) - flopy.mf6.ModflowGwfic(gwf, strt=strt) - flopy.mf6.ModflowGwfnpf( - gwf, - icelltype=icelltype, - k=k11, - save_specific_discharge=True, - ) - flopy.mf6.ModflowGwfsto( - gwf, - iconvert=icelltype, - ss=0.0, - sy=sy, - steady_state={0: True}, - transient={1: True}, - ) - csub = flopy.mf6.ModflowGwfcsub( - gwf, - print_input=True, - update_material_properties=True, - save_flows=True, - ninterbeds=0, - maxsig0=1, - compression_indices=None, - sgm=sgm, - sgs=sgs, - cg_theta=cg_theta, - cg_ske_cr=cg_ske, - beta=4.65120000e-10, - packagedata=None, - stress_period_data={0: [[(0, 0, 0), "LOAD"]]}, - ) - # initialize time series - csubnam = f"{sim_name}.load.ts" - csub.ts.initialize( - filename=csubnam, - timeseries=csub_ts, - time_series_namerecord=["LOAD"], - interpolation_methodrecord=["linear"], - sfacrecord=["1.05"], - ) - - flopy.mf6.ModflowGwfoc( - gwf, - printrecord=[("BUDGET", "ALL")], - ) - return sim - return None - - -# Function to write MODFLOW 6 model files - - -def write_model(sim, silent=True): - if config.writeModel: - sim.write_simulation(silent=silent) - - -# Function to run the model. -# True is returned if the model runs successfully +# Define functions to build models, write input files, and run the simulation. + + +# + +def build_models(): + sim_ws = os.path.join(workspace, sim_name) + sim = flopy.mf6.MFSimulation(sim_name=sim_name, sim_ws=sim_ws, exe_name="mf6") + flopy.mf6.ModflowTdis(sim, nper=nper, perioddata=tdis_ds, time_units=time_units) + flopy.mf6.ModflowIms( + sim, + outer_maximum=nouter, + outer_dvclose=hclose, + linear_acceleration=linaccel, + inner_maximum=ninner, + inner_dvclose=hclose, + relaxation_factor=relax, + rcloserecord=f"{rclose} strict", + ) + gwf = flopy.mf6.ModflowGwf( + sim, modelname=sim_name, save_flows=True, newtonoptions="newton" + ) + flopy.mf6.ModflowGwfdis( + gwf, + length_units=length_units, + nlay=nlay, + nrow=nrow, + ncol=ncol, + delr=delr, + delc=delc, + top=top, + botm=botm, + ) + obs_recarray = {"gwf_calib_obs.csv": [("w3_1_1", "HEAD", (2, 0, locw201))]} + flopy.mf6.ModflowUtlobs(gwf, digits=10, print_input=True, continuous=obs_recarray) + flopy.mf6.ModflowGwfic(gwf, strt=strt) + flopy.mf6.ModflowGwfnpf( + gwf, + icelltype=icelltype, + k=k11, + save_specific_discharge=True, + ) + flopy.mf6.ModflowGwfsto( + gwf, + iconvert=icelltype, + ss=0.0, + sy=sy, + steady_state={0: True}, + transient={1: True}, + ) + csub = flopy.mf6.ModflowGwfcsub( + gwf, + print_input=True, + update_material_properties=True, + save_flows=True, + ninterbeds=0, + maxsig0=1, + compression_indices=None, + sgm=sgm, + sgs=sgs, + cg_theta=cg_theta, + cg_ske_cr=cg_ske, + beta=4.65120000e-10, + packagedata=None, + stress_period_data={0: [[(0, 0, 0), "LOAD"]]}, + ) + # initialize time series + csubnam = f"{sim_name}.load.ts" + csub.ts.initialize( + filename=csubnam, + timeseries=csub_ts, + time_series_namerecord=["LOAD"], + interpolation_methodrecord=["linear"], + sfacrecord=["1.05"], + ) + + flopy.mf6.ModflowGwfoc( + gwf, + printrecord=[("BUDGET", "ALL")], + ) + return sim + + +def write_models(sim, silent=True): + sim.write_simulation(silent=silent) + + +@timed +def run_models(sim, silent=True): + success, buff = sim.run_simulation(silent=silent, report=True) + assert success, buff + + +# - + +# ### Plotting results # +# Define functions to plot model results. - -@config.timeit -def run_model(sim, silent=True): - success = True - if config.runModel: - success, buff = sim.run_simulation(silent=silent) - if not success: - print(buff) - - return success - - -# Function to plot the model results. -# +# + +# Figure properties +figure_size = (6.8, 4.5) def plot_results(sim, silent=True): - if config.plotModel: - fs = USGSFigure(figure_type="map", verbose=False) - sim_ws = os.path.join(ws, sim_name) + with styles.USGSMap(): gwf = sim.get_model(sim_name) # plot the grid @@ -284,12 +249,10 @@ def plot_results(sim, silent=True): ha="left", va="center", zorder=100, - arrowprops=dict( - facecolor="black", shrink=0.05, headwidth=5, width=1.5 - ), + arrowprops=dict(facecolor="black", shrink=0.05, headwidth=5, width=1.5), ) - fs.heading(ax, letter="A", heading="Map view") - fs.remove_edge_ticks(ax) + styles.heading(ax, letter="A", heading="Map view") + styles.remove_edge_ticks(ax) ax.axes.get_xaxis().set_ticks([]) idx += 1 @@ -298,9 +261,7 @@ def plot_results(sim, silent=True): mc = flopy.plot.PlotCrossSection( model=gwf, line={"Row": 0}, ax=ax, extent=extent ) - ax.fill_between( - [0, delr.sum()], y1=top, y2=botm[0], color="cyan", alpha=0.5 - ) + ax.fill_between([0, delr.sum()], y1=top, y2=botm[0], color="cyan", alpha=0.5) ax.fill_between( [0, delr.sum()], y1=botm[0], y2=botm[1], color="#D2B48C", alpha=0.5 ) @@ -325,128 +286,113 @@ def plot_results(sim, silent=True): ) ax.set_ylabel("Elevation, in meters") ax.set_xlabel("x-coordinate, in meters") - fs.heading(ax, letter="B", heading="Cross-section view") - fs.remove_edge_ticks(ax) + styles.heading(ax, letter="B", heading="Cross-section view") + styles.remove_edge_ticks(ax) fig.align_ylabels() plt.tight_layout(pad=1, h_pad=0.001, rect=(0.005, -0.02, 0.99, 0.99)) - # save figure - if config.plotSave: - fpth = os.path.join( - "..", "figures", f"{sim_name}-grid{config.figure_ext}" - ) + if plot_show: + plt.show() + if plot_save: + fpth = os.path.join("..", "figures", f"{sim_name}-grid.png") fig.savefig(fpth) # get the simulated heads sim_obs = gwf.obs.output.obs().data h0 = sim_obs["W3_1_1"][0] sim_obs["W3_1_1"] -= h0 - sim_date = [ - dstart + datetime.timedelta(seconds=x) for x in sim_obs["totim"] - ] + sim_date = [dstart + datetime.timedelta(seconds=x) for x in sim_obs["totim"]] # get the observed head - pth = os.path.join("..", "data", sim_name, "s201_gw_2sec.csv") + pth = pooch.retrieve( + url=f"https://github.com/MODFLOW-USGS/modflow6-examples/raw/master/data/{sim_name}/s201_gw_2sec.csv", + known_hash="md5:1098bcd3f4fc1bd3b38d3d55152a8fbb", + ) dtype = [("date", object), ("dz_m", float)] obs_head = np.genfromtxt(pth, names=True, delimiter=",", dtype=dtype) obs_date = [] for s in obs_head["date"]: obs_date.append( - datetime.datetime.strptime( - s.decode("utf-8"), "%m-%d-%Y %H:%M:%S.%f" - ) + datetime.datetime.strptime(s.decode("utf-8"), "%m-%d-%Y %H:%M:%S.%f") ) t0, t1 = obs_date[0], obs_date[-1] # plot the results - fs = USGSFigure(figure_type="graph", verbose=False) - fig = plt.figure(figsize=(6.8, 4.0)) - gs = mpl.gridspec.GridSpec(2, 1, figure=fig) - - axe = fig.add_subplot(gs[-1]) - - idx = 0 - ax = fig.add_subplot(gs[idx], sharex=axe) - ax.set_ylim(0, 3.25) - ax.set_yticks(np.arange(0, 3.5, 0.5)) - ax.fill_between( - date_list, csv_load["load"], y2=0, color="cyan", lw=0.5, alpha=0.5 - ) - ax.set_ylabel("Load, in meters\nof water") - plt.setp(ax.get_xticklabels(), visible=False) - fs.heading(ax, letter="A") - fs.remove_edge_ticks(ax) + with styles.USGSPlot() as fs: + fig = plt.figure(figsize=(6.8, 4.0)) + gs = mpl.gridspec.GridSpec(2, 1, figure=fig) + + axe = fig.add_subplot(gs[-1]) + + idx = 0 + ax = fig.add_subplot(gs[idx], sharex=axe) + ax.set_ylim(0, 3.25) + ax.set_yticks(np.arange(0, 3.5, 0.5)) + ax.fill_between( + date_list, csv_load["load"], y2=0, color="cyan", lw=0.5, alpha=0.5 + ) + ax.set_ylabel("Load, in meters\nof water") + plt.setp(ax.get_xticklabels(), visible=False) + styles.heading(ax, letter="A") + styles.remove_edge_ticks(ax) + + ax = axe + ax.plot( + sim_date, + sim_obs["W3_1_1"], + color="black", + lw=0.75, + label="Simulated", + ) + ax.plot( + obs_date, + obs_head["dz_m"], + color="red", + lw=0, + ms=4, + marker=".", + label="Offset S-201", + ) + ax.axhline(0, lw=0.5, color="0.5") + ax.set_ylabel("Water level fluctuation,\nin meters") + styles.heading(ax, letter="B") + leg = styles.graph_legend(ax, loc="upper right", ncol=1) - ax = axe - ax.plot( - sim_date, - sim_obs["W3_1_1"], - color="black", - lw=0.75, - label="Simulated", - ) - ax.plot( - obs_date, - obs_head["dz_m"], - color="red", - lw=0, - ms=4, - marker=".", - label="Offset S-201", - ) - ax.axhline(0, lw=0.5, color="0.5") - ax.set_ylabel("Water level fluctuation,\nin meters") - fs.heading(ax, letter="B") - leg = fs.graph_legend(ax, loc="upper right", ncol=1) + ax.set_xlabel("Time") + ax.set_ylim(-0.004, 0.008) + axe.set_xlim(t0, t1) + styles.remove_edge_ticks(ax) - ax.set_xlabel("Time") - ax.set_ylim(-0.004, 0.008) - axe.set_xlim(t0, t1) - fs.remove_edge_ticks(ax) + fig.align_ylabels() - fig.align_ylabels() + plt.tight_layout(pad=1, h_pad=0.001, rect=(0.005, -0.02, 0.99, 0.99)) - plt.tight_layout(pad=1, h_pad=0.001, rect=(0.005, -0.02, 0.99, 0.99)) + if plot_show: + plt.show() + if plot_save: + fpth = os.path.join("..", "figures", f"{sim_name}-01.png") + fig.savefig(fpth) - # save figure - if config.plotSave: - fpth = os.path.join( - "..", "figures", f"{sim_name}-01{config.figure_ext}" - ) - fig.savefig(fpth) +# - -# Function that wraps all of the steps for the model -# -# 1. build_model, -# 2. write_model, -# 3. run_model, and -# 4. plot_results. +# ### Running the example # +# Define and invoke a function to run the example scenario, then plot results. -def simulation(silent=True): - sim = build_model() - - write_model(sim, silent=silent) - - success = run_model(sim, silent=silent) - - if success: +# + +def scenario(silent=True): + sim = build_models() + if write: + write_models(sim, silent=silent) + if run: + run_models(sim, silent=silent) + if plot: plot_results(sim, silent=silent) -# nosetest - exclude block from this nosetest to the next nosetest -def test_01(): - simulation(silent=False) - - -# nosetest end - -if __name__ == "__main__": - # ### Jacob (1939) Elastic Aquifer Loading - # - - simulation() +scenario(silent=False) +# - diff --git a/scripts/ex-gwf-csub-p02.py b/scripts/ex-gwf-csub-p02.py index 27ad617d9..e05bbfb0d 100644 --- a/scripts/ex-gwf-csub-p02.py +++ b/scripts/ex-gwf-csub-p02.py @@ -1,46 +1,41 @@ -# ## Delay interbed drainage +# ## Delay interbed drainage example # # This problem simulates the drainage of a thick interbed caused by a step # decrease in hydraulic head in the aquifer and is based on MODFLOW-2000 subsidence # package sample problem 1. -# -# ### Problem Setup +# ### Initial setup # -# Imports +# Import dependencies, define the example name and workspace, and read settings from environment variables. +# + import os -import sys +import pathlib as pl import flopy import matplotlib as mpl import matplotlib.pyplot as plt import numpy as np +from flopy.plot.styles import styles +from modflow_devtools.misc import get_env, timed -# Append to system path to include the common subdirectory - -sys.path.append(os.path.join("..", "common")) - -# import common functionality - -import config -from figspecs import USGSFigure - -# Set figure properties specific to the problem - -figure_size = (6.8, 3.4) -arrow_props = dict(facecolor="black", arrowstyle="-", lw=0.5) - -# Base simulation and model name and workspace - -ws = config.base_ws - -# Simulation name - +# Example name and base workspace sim_name = "ex-gwf-csub-p02" +workspace = pl.Path("../examples") -# Scenario parameters +# Settings from environment variables +write = get_env("WRITE", True) +run = get_env("RUN", True) +plot = get_env("PLOT", True) +plot_show = get_env("PLOT_SHOW", True) +plot_save = get_env("PLOT_SAVE", True) + +# ### Define parameters +# +# Define model units, parameters and other settings. +# + +# Scenario-specific parameters parameters = { "ex-gwf-csub-p02a": { "head_based": True, @@ -63,12 +58,10 @@ } # Model units - length_units = "meters" time_units = "days" -# Table - +# Model parameters nper = 1 # Number of periods nlay = 1 # Number of layers ncol = 3 # Number of columns @@ -90,32 +83,30 @@ h0 = 1.0 # Initial interbed head ($m$) head_offset = 1.0 # Initial preconsolidation head ($m$) -# Static temporal data used by TDIS file - +# Time discretization tdis_ds = ((1000.0, 100, 1.05),) # Constant head cells - c6 = [] for j in range(0, ncol, 2): c6.append([0, 0, j, strt]) # Solver parameters - nouter = 1000 ninner = 300 hclose = 1e-9 rclose = 1e-6 linaccel = "bicgstab" relax = 0.97 +# - - -# ### Functions to build, write, run, and plot the model +# ### Model setup # -# MODFLOW 6 flopy simulation object (sim) is returned if building the model +# Define functions to build models, write input files, and run the simulation. -def build_model( +# + +def build_models( name, subdir_name=".", head_based=True, @@ -123,167 +114,148 @@ def build_model( kv=2e-6, ndelaycells=19, ): - if config.buildModel: - sim_ws = os.path.join(ws, name) - if subdir_name is not None: - sim_ws = os.path.join(sim_ws, subdir_name) - sim = flopy.mf6.MFSimulation( - sim_name=name, sim_ws=sim_ws, exe_name="mf6" - ) - flopy.mf6.ModflowTdis( - sim, nper=nper, perioddata=tdis_ds, time_units=time_units - ) - flopy.mf6.ModflowIms( - sim, - outer_maximum=nouter, - outer_dvclose=hclose, - linear_acceleration=linaccel, - inner_maximum=ninner, - inner_dvclose=hclose, - relaxation_factor=relax, - rcloserecord=f"{rclose} strict", - ) - gwf = flopy.mf6.ModflowGwf( - sim, modelname=name, save_flows=True, newtonoptions="newton" - ) - flopy.mf6.ModflowGwfdis( - gwf, - length_units=length_units, - nlay=nlay, - nrow=nrow, - ncol=ncol, - delr=delr, - delc=delc, - top=top, - botm=botm, - ) - # gwf obs - flopy.mf6.ModflowUtlobs( - gwf, - digits=10, - print_input=True, - continuous={"gwf_obs.csv": [("h1_1_2", "HEAD", (0, 0, 1))]}, - ) + sim_ws = os.path.join(workspace, name) + if subdir_name is not None: + sim_ws = os.path.join(sim_ws, subdir_name) + sim = flopy.mf6.MFSimulation(sim_name=name, sim_ws=sim_ws, exe_name="mf6") + flopy.mf6.ModflowTdis(sim, nper=nper, perioddata=tdis_ds, time_units=time_units) + flopy.mf6.ModflowIms( + sim, + outer_maximum=nouter, + outer_dvclose=hclose, + linear_acceleration=linaccel, + inner_maximum=ninner, + inner_dvclose=hclose, + relaxation_factor=relax, + rcloserecord=f"{rclose} strict", + ) + gwf = flopy.mf6.ModflowGwf( + sim, modelname=name, save_flows=True, newtonoptions="newton" + ) + flopy.mf6.ModflowGwfdis( + gwf, + length_units=length_units, + nlay=nlay, + nrow=nrow, + ncol=ncol, + delr=delr, + delc=delc, + top=top, + botm=botm, + ) + # gwf obs + flopy.mf6.ModflowUtlobs( + gwf, + digits=10, + print_input=True, + continuous={"gwf_obs.csv": [("h1_1_2", "HEAD", (0, 0, 1))]}, + ) - flopy.mf6.ModflowGwfic(gwf, strt=strt) - flopy.mf6.ModflowGwfnpf( - gwf, - icelltype=icelltype, - k=k11, - save_specific_discharge=True, - ) - flopy.mf6.ModflowGwfsto( - gwf, iconvert=icelltype, ss=0.0, sy=0, transient={0: True} - ) - if head_based: - hb_bool = True - pc0 = head_offset - tsgm = None - tsgs = None - else: - hb_bool = None - pc0 = -head_offset - tsgm = sgm - tsgs = sgs - sub6 = [ - [ - 0, - 0, - 0, - 1, - "delay", - pc0, - bed_thickness, - 1.0, - skv, - ske, - theta, - kv, - h0, - "ib1", - ] + flopy.mf6.ModflowGwfic(gwf, strt=strt) + flopy.mf6.ModflowGwfnpf( + gwf, + icelltype=icelltype, + k=k11, + save_specific_discharge=True, + ) + flopy.mf6.ModflowGwfsto(gwf, iconvert=icelltype, ss=0.0, sy=0, transient={0: True}) + if head_based: + hb_bool = True + pc0 = head_offset + tsgm = None + tsgs = None + else: + hb_bool = None + pc0 = -head_offset + tsgm = sgm + tsgs = sgs + sub6 = [ + [ + 0, + 0, + 0, + 1, + "delay", + pc0, + bed_thickness, + 1.0, + skv, + ske, + theta, + kv, + h0, + "ib1", ] - csub = flopy.mf6.ModflowGwfcsub( - gwf, - print_input=True, - save_flows=True, - head_based=hb_bool, - ndelaycells=ndelaycells, - boundnames=True, - ninterbeds=1, - sgm=tsgm, - sgs=tsgs, - cg_theta=cg_theta, - cg_ske_cr=0.0, - beta=0.0, - packagedata=sub6, - ) - opth = f"{name}.csub.obs" - csub_csv = opth + ".csv" - obs = [ - ("tcomp", "interbed-compaction", "ib1"), - ("sk", "sk", "ib1"), - ("qtop", "delay-flowtop", (0,)), - ("qbot", "delay-flowbot", (0,)), + ] + csub = flopy.mf6.ModflowGwfcsub( + gwf, + print_input=True, + save_flows=True, + head_based=hb_bool, + ndelaycells=ndelaycells, + boundnames=True, + ninterbeds=1, + sgm=tsgm, + sgs=tsgs, + cg_theta=cg_theta, + cg_ske_cr=0.0, + beta=0.0, + packagedata=sub6, + ) + opth = f"{name}.csub.obs" + csub_csv = opth + ".csv" + obs = [ + ("tcomp", "interbed-compaction", "ib1"), + ("sk", "sk", "ib1"), + ("qtop", "delay-flowtop", (0,)), + ("qbot", "delay-flowbot", (0,)), + ] + for k in range(ndelaycells): + tag = f"H{k + 1:04d}" + obs.append((tag, "delay-head", (0,), (k,))) + if not head_based: + iposm = int(ndelaycells / 2) + 1 + iposb = ndelaycells - 1 + obs += [ + ("est", "delay-estress", (0,), (0,)), + ("esm", "delay-estress", (0,), (iposm,)), + ("esb", "delay-estress", (0,), (iposb,)), + ("gs", "gstress-cell", (0, 0, 1)), + ("es", "estress-cell", (0, 0, 1)), ] - for k in range(ndelaycells): - tag = f"H{k + 1:04d}" - obs.append((tag, "delay-head", (0,), (k,))) - if not head_based: - iposm = int(ndelaycells / 2) + 1 - iposb = ndelaycells - 1 - obs += [ - ("est", "delay-estress", (0,), (0,)), - ("esm", "delay-estress", (0,), (iposm,)), - ("esb", "delay-estress", (0,), (iposb,)), - ("gs", "gstress-cell", (0, 0, 1)), - ("es", "estress-cell", (0, 0, 1)), - ] - orecarray = {csub_csv: obs} - csub.obs.initialize( - filename=opth, digits=10, print_input=True, continuous=orecarray - ) - - flopy.mf6.ModflowGwfchd(gwf, stress_period_data={0: c6}) - - flopy.mf6.ModflowGwfoc( - gwf, - printrecord=[("BUDGET", "ALL")], - ) - return sim - return None - - -# Function to write MODFLOW 6 model files + orecarray = {csub_csv: obs} + csub.obs.initialize( + filename=opth, digits=10, print_input=True, continuous=orecarray + ) + flopy.mf6.ModflowGwfchd(gwf, stress_period_data={0: c6}) -def write_model(sim, silent=True): - if config.writeModel: - sim.write_simulation(silent=silent) + flopy.mf6.ModflowGwfoc( + gwf, + printrecord=[("BUDGET", "ALL")], + ) + return sim -# Function to run the model. -# True is returned if the model runs successfully -# +def write_models(sim, silent=True): + sim.write_simulation(silent=silent) -@config.timeit -def run_model(sim, silent=True): - success = True - if config.runModel: - success, buff = sim.run_simulation(silent=silent) - if not success: - print(buff) +@timed +def run_models(sim, silent=True): + success, buff = sim.run_simulation(silent=silent) + assert success, buff - return success +# - -# Analytical solution for plotting +# ### Plotting results +# +# Define functions to plot model results, beginning with an analytical solution to superimpose over the simulated solution. -def analytical_solution( - z, t, dh=1.0, b0=1.0, ssk=100.0, vk=0.025, n=100, silent=True -): +# + +def analytical_solution(z, t, dh=1.0, b0=1.0, ssk=100.0, vk=0.025, n=100, silent=True): v = 0.0 e = np.exp(1) pi = np.pi @@ -292,9 +264,7 @@ def analytical_solution( for k in range(n): fk = float(k) tauk = (0.5 * b0) ** 2.0 * ssk / ((2.0 * fk + 1.0) ** 2.0 * vk) - ep = ((2.0 * fk + 1.0) ** 2 * pi2 * vk * t) / ( - 4.0 * ssk * (0.5 * b0) ** 2.0 - ) + ep = ((2.0 * fk + 1.0) ** 2 * pi2 * vk * t) / (4.0 * ssk * (0.5 * b0) ** 2.0) rad = (2.0 * fk + 1.0) * pi * z / b0 v += ((-1.0) ** fk / (2.0 * fk + 1.0)) * (e**-ep) * np.cos(rad) if not silent: @@ -302,259 +272,253 @@ def analytical_solution( return dh - 4.0 * dh * v / pi -# Function to plot the model grid +# Set figure properties specific to the problem +figure_size = (6.8, 3.4) +arrow_props = dict(facecolor="black", arrowstyle="-", lw=0.5) def plot_grid(sim, silent=True): - verbose = not silent - fs = USGSFigure(figure_type="map", verbose=verbose) - name = sim.name - gwf = sim.get_model(name) - - fig, ax = plt.subplots(figsize=(6.8, 2.0)) - mc = flopy.plot.PlotCrossSection(model=gwf, line={"Row": 0}, ax=ax) - - ax.fill_between([0, 1], y1=0, y2=botm, color="cyan", alpha=0.5) - fs.add_text( - ax=ax, - text="Constant head", - x=0.5, - y=-500.0, - bold=False, - italic=False, - transform=False, - va="center", - ha="center", - fontsize=9, - ) - ax.fill_between([2, 3], y1=0, y2=botm, color="cyan", alpha=0.5) - fs.add_text( - ax=ax, - text="Constant head", - x=2.5, - y=-500.0, - bold=False, - italic=False, - transform=False, - va="center", - ha="center", - fontsize=9, - ) - ax.fill_between([1, 2], y1=-499.5, y2=-500.5, color="brown", alpha=0.5) - fs.add_annotation( - ax=ax, - text="Delay interbed", - xy=(1.5, -510.0), - xytext=(1.6, -300), - bold=False, - italic=False, - fontsize=9, - ha="center", - va="center", - zorder=100, - arrowprops=arrow_props, - ) - mc.plot_grid(color="0.5", lw=0.5, zorder=100) + with styles.USGSMap() as fs: + name = sim.name + gwf = sim.get_model(name) + + fig, ax = plt.subplots(figsize=(6.8, 2.0)) + mc = flopy.plot.PlotCrossSection(model=gwf, line={"Row": 0}, ax=ax) + + ax.fill_between([0, 1], y1=0, y2=botm, color="cyan", alpha=0.5) + styles.add_text( + ax=ax, + text="Constant head", + x=0.5, + y=-500.0, + bold=False, + italic=False, + transform=False, + va="center", + ha="center", + fontsize=9, + ) + ax.fill_between([2, 3], y1=0, y2=botm, color="cyan", alpha=0.5) + styles.add_text( + ax=ax, + text="Constant head", + x=2.5, + y=-500.0, + bold=False, + italic=False, + transform=False, + va="center", + ha="center", + fontsize=9, + ) + ax.fill_between([1, 2], y1=-499.5, y2=-500.5, color="brown", alpha=0.5) + styles.add_annotation( + ax=ax, + text="Delay interbed", + xy=(1.5, -510.0), + xytext=(1.6, -300), + bold=False, + italic=False, + fontsize=9, + ha="center", + va="center", + zorder=100, + arrowprops=arrow_props, + ) + mc.plot_grid(color="0.5", lw=0.5, zorder=100) - ax.set_xlim(0, 3) - ax.set_ylabel("Elevation, in meters") - ax.set_xlabel("x-coordinate, in meters") - fs.remove_edge_ticks(ax) + ax.set_xlim(0, 3) + ax.set_ylabel("Elevation, in meters") + ax.set_xlabel("x-coordinate, in meters") + styles.remove_edge_ticks(ax) - plt.tight_layout() + plt.tight_layout() - # save figure - if config.plotSave: - fpth = os.path.join( - "..", "figures", f"{sim_name}-grid{config.figure_ext}" - ) - if not silent: - print(f"saving...'{fpth}'") - fig.savefig(fpth) + if plot_show: + plt.show() + if plot_save: + fpth = os.path.join("..", "figures", f"{sim_name}-grid.png") + if not silent: + print(f"saving...'{fpth}'") + fig.savefig(fpth) -# Function to plot the head based results +def plot_head_based(sim, silent=True): + with styles.USGSPlot() as fs: + name = sim.name + # get csub observations + ws = sim.simulation_data.mfpath.get_sim_path() + s = flopy.mf6.MFSimulation().load(sim_ws=ws, verbosity_level=0) + gwf = s.get_model(name) + cobs = gwf.csub.output.obs().data + + # calculate the compaction analytically + ac = [] + nz = 100 + thick = parameters[name]["bed_thickness"][0] + kv = parameters[name]["kv"][0] + dhalf = thick * 0.5 + az = np.linspace(-dhalf, dhalf, num=nz) + dz = az[1] - az[0] + for tt in cobs["totim"]: + c = 0.0 + for jdx, zz in enumerate(az): + f = 1.0 + if jdx == 0 or jdx == nz - 1: + f = 0.5 + h = analytical_solution(zz, tt, ssk=skv, vk=kv, n=200, dh=1.0) + c += h * skv * f * dz + ac.append(c) + ac = np.array(ac) -def plot_head_based(sim, silent=True): - verbose = not silent - fs = USGSFigure(figure_type="graph", verbose=verbose) - name = sim.name + # calculate normalized simulation time + tpct = cobs["totim"] * 100 / tau0 - # get csub observations - ws = sim.simulation_data.mfpath.get_sim_path() - s = flopy.mf6.MFSimulation().load(sim_ws=ws, verbosity_level=0) - gwf = s.get_model(name) - cobs = gwf.csub.output.obs().data - - # calculate the compaction analytically - ac = [] - nz = 100 - thick = parameters[name]["bed_thickness"][0] - kv = parameters[name]["kv"][0] - dhalf = thick * 0.5 - az = np.linspace(-dhalf, dhalf, num=nz) - dz = az[1] - az[0] - for tt in cobs["totim"]: - c = 0.0 - for jdx, zz in enumerate(az): - f = 1.0 - if jdx == 0 or jdx == nz - 1: - f = 0.5 - h = analytical_solution(zz, tt, ssk=skv, vk=kv, n=200, dh=1.0) - c += h * skv * f * dz - ac.append(c) - ac = np.array(ac) - - # calculate normalized simulation time - tpct = cobs["totim"] * 100 / tau0 - - # plot the results - fig = plt.figure(figsize=figure_size) - gs = mpl.gridspec.GridSpec(1, 2, figure=fig) - - idx = 0 - ax = fig.add_subplot(gs[idx]) - ax.plot( - tpct, - 100 * ac / skv, - marker=".", - lw=0, - ms=3, - color="red", - label="Analytical", - ) - ax.plot( - tpct, - 100 * cobs["TCOMP"] / skv, - label="Simulated", - color="black", - lw=1, - zorder=100, - ) - leg = fs.graph_legend(ax, loc="lower right") - ax.set_xticks(np.arange(0, 110, 10)) - ax.set_yticks(np.arange(0, 110, 10)) - ax.set_xlabel("Percent of time constant") - ax.set_ylabel("Compaction, in percent of ultimate value") - ax.set_xlim(0, 100) - ax.set_ylim(0, 100) - fs.heading(ax, letter="A") - fs.remove_edge_ticks(ax) - - idx += 1 - ax = fig.add_subplot(gs[idx]) - ax.plot( - tpct, 100 * (ac - cobs["TCOMP"]) / skv, lw=1, ls=":", color="black" - ) - ax.set_xticks(np.arange(0, 110, 10)) - ax.set_yticks(np.arange(0, 2.2, 0.2)) - ax.set_xlabel("Percent of time constant") - ax.set_ylabel( - "Analytical minus simulated subsidence,\nin percent of ultimate value" - ) - ax.set_xlim(0, 100) - ax.set_ylim(0, 2) - fs.heading(ax, letter="B") - fs.remove_edge_ticks(ax) + # plot the results + fig = plt.figure(figsize=figure_size) + gs = mpl.gridspec.GridSpec(1, 2, figure=fig) - plt.tight_layout() + idx = 0 + ax = fig.add_subplot(gs[idx]) + ax.plot( + tpct, + 100 * ac / skv, + marker=".", + lw=0, + ms=3, + color="red", + label="Analytical", + ) + ax.plot( + tpct, + 100 * cobs["TCOMP"] / skv, + label="Simulated", + color="black", + lw=1, + zorder=100, + ) + leg = styles.graph_legend(ax, loc="lower right") + ax.set_xticks(np.arange(0, 110, 10)) + ax.set_yticks(np.arange(0, 110, 10)) + ax.set_xlabel("Percent of time constant") + ax.set_ylabel("Compaction, in percent of ultimate value") + ax.set_xlim(0, 100) + ax.set_ylim(0, 100) + styles.heading(ax, letter="A") + styles.remove_edge_ticks(ax) - # save figure - if config.plotSave: - fpth = os.path.join("..", "figures", f"{name}-01{config.figure_ext}") - if not silent: - print(f"saving...'{fpth}'") - fig.savefig(fpth) + idx += 1 + ax = fig.add_subplot(gs[idx]) + ax.plot(tpct, 100 * (ac - cobs["TCOMP"]) / skv, lw=1, ls=":", color="black") + ax.set_xticks(np.arange(0, 110, 10)) + ax.set_yticks(np.arange(0, 2.2, 0.2)) + ax.set_xlabel("Percent of time constant") + ax.set_ylabel( + "Analytical minus simulated subsidence,\nin percent of ultimate value" + ) + ax.set_xlim(0, 100) + ax.set_ylim(0, 2) + styles.heading(ax, letter="B") + styles.remove_edge_ticks(ax) + + plt.tight_layout() + + if plot_show: + plt.show() + if plot_save: + fpth = os.path.join("..", "figures", f"{name}-01.png") + if not silent: + print(f"saving...'{fpth}'") + fig.savefig(fpth) def plot_effstress(sim, silent=True): verbose = not silent - fs = USGSFigure(figure_type="graph", verbose=verbose) - name = sim.name + with styles.USGSPlot() as fs: + name = sim.name - # get effective stress csub observations - gwf = sim.get_model(name) - cobs = gwf.csub.output.obs().data - - # get head-based csub observations - name0 = name.replace("-p02b", "-p02a") - ws0 = os.path.join(ws, name0) - sim0 = flopy.mf6.MFSimulation().load(sim_ws=ws0, verbosity_level=0) - gwf0 = sim0.get_model(name0) - cobs0 = gwf0.csub.output.obs().data - - # calculate normalized simulation time - tpct = cobs["totim"] * 100 / tau0 - - # plot the results - fig = plt.figure(figsize=figure_size) - gs = mpl.gridspec.GridSpec(1, 2, figure=fig) - - idx = 0 - ax = fig.add_subplot(gs[idx]) - ax.plot( - tpct, - 100 * cobs0["TCOMP"] / skv, - lw=0, - marker=".", - ms=3, - color="#238A8DFF", - label="Head-based", - ) - ax.plot( - tpct, - 100 * cobs["TCOMP"] / skv, - lw=0.75, - label="Effective stress-based", - color="black", - zorder=100, - ) - leg = fs.graph_legend(ax, loc="lower right") - ax.set_xticks(np.arange(0, 110, 10)) - ax.set_yticks(np.arange(0, 110, 10)) - ax.set_xlabel("Percent of time constant") - ax.set_ylabel("Compaction, in percent of ultimate value") - ax.set_xlim(0, 100) - ax.set_ylim(0, 100) - fs.heading(ax, letter="A") - fs.remove_edge_ticks(ax) - - idx += 1 - ax = fig.add_subplot(gs[idx]) - ax.plot( - tpct, - 100 * (cobs0["TCOMP"] - cobs["TCOMP"]) / skv, - lw=1, - ls=":", - color="black", - ) - ax.set_xticks(np.arange(0, 110, 10)) - ax.set_xlabel("Percent of time constant") - ax.set_ylabel( - "Head-based minus effective stress-based\nsubsidence, in percent of ultimate value" - ) - ax.set_xlim(0, 100) - fs.heading(ax, letter="B") - fs.remove_edge_ticks(ax) + # get effective stress csub observations + gwf = sim.get_model(name) + cobs = gwf.csub.output.obs().data - plt.tight_layout() + # get head-based csub observations + name0 = name.replace("-p02b", "-p02a") + ws0 = os.path.join(workspace, name0) + sim0 = flopy.mf6.MFSimulation().load(sim_ws=ws0, verbosity_level=0) + gwf0 = sim0.get_model(name0) + cobs0 = gwf0.csub.output.obs().data - # save figure - if config.plotSave: - fpth = os.path.join("..", "figures", f"{name}-01{config.figure_ext}") - if not silent: - print(f"saving...'{fpth}'") - fig.savefig(fpth) + # calculate normalized simulation time + tpct = cobs["totim"] * 100 / tau0 + + # plot the results + fig = plt.figure(figsize=figure_size) + gs = mpl.gridspec.GridSpec(1, 2, figure=fig) + + idx = 0 + ax = fig.add_subplot(gs[idx]) + ax.plot( + tpct, + 100 * cobs0["TCOMP"] / skv, + lw=0, + marker=".", + ms=3, + color="#238A8DFF", + label="Head-based", + ) + ax.plot( + tpct, + 100 * cobs["TCOMP"] / skv, + lw=0.75, + label="Effective stress-based", + color="black", + zorder=100, + ) + leg = styles.graph_legend(ax, loc="lower right") + ax.set_xticks(np.arange(0, 110, 10)) + ax.set_yticks(np.arange(0, 110, 10)) + ax.set_xlabel("Percent of time constant") + ax.set_ylabel("Compaction, in percent of ultimate value") + ax.set_xlim(0, 100) + ax.set_ylim(0, 100) + styles.heading(ax, letter="A") + styles.remove_edge_ticks(ax) + + idx += 1 + ax = fig.add_subplot(gs[idx]) + ax.plot( + tpct, + 100 * (cobs0["TCOMP"] - cobs["TCOMP"]) / skv, + lw=1, + ls=":", + color="black", + ) + ax.set_xticks(np.arange(0, 110, 10)) + ax.set_xlabel("Percent of time constant") + ax.set_ylabel( + "Head-based minus effective stress-based\nsubsidence, in percent of ultimate value" + ) + ax.set_xlim(0, 100) + styles.heading(ax, letter="B") + styles.remove_edge_ticks(ax) + plt.tight_layout() -# Function to get subdirectory names + if plot_show: + plt.show() + if plot_save: + fpth = os.path.join("..", "figures", f"{name}-01.png") + if not silent: + print(f"saving...'{fpth}'") + fig.savefig(fpth) def get_subdirs(sim): + """Get subdirectory names""" name = sim.name # get the subdirectory names - pth = os.path.join(ws, name) + pth = os.path.join(workspace, name) hb_dirs = [ name for name in sorted(os.listdir(pth)) @@ -568,10 +532,8 @@ def get_subdirs(sim): return hb_dirs, es_dirs -# Function to process interbed heads - - def fill_heads(rec_arr, ndcells): + """Process interbed heads""" arr = np.zeros((rec_arr.shape[0], ndcells), dtype=float) for i in range(100): for j in range(ndcells): @@ -580,279 +542,262 @@ def fill_heads(rec_arr, ndcells): return arr -# Function to plot the results for multiple interbed thicknesses - - def plot_comp_q_comparison(sim, silent=True): - verbose = not silent - fs = USGSFigure(figure_type="graph", verbose=verbose) - name = sim.name - thicknesses = parameters[name]["bed_thickness"] - - # get the subdirectory names - hb_dirs, es_dirs = get_subdirs(sim) + """Plot the results for multiple interbed thicknesses""" + with styles.USGSPlot(): + name = sim.name + thicknesses = parameters[name]["bed_thickness"] - # setup the figure - fig = plt.figure(figsize=figure_size) - gs = mpl.gridspec.GridSpec(1, 2, figure=fig) + # get the subdirectory names + hb_dirs, es_dirs = get_subdirs(sim) - # set color - cmap = plt.get_cmap("viridis") - cNorm = mpl.colors.Normalize(vmin=0, vmax=6) - scalarMap = mpl.cm.ScalarMappable(norm=cNorm, cmap=cmap) + # setup the figure + fig = plt.figure(figsize=figure_size) + gs = mpl.gridspec.GridSpec(1, 2, figure=fig) - axes = [] - for idx in range(2): - ax = fig.add_subplot(gs[idx]) - if idx == 0: - ax.set_yticks(np.arange(-0.40, 0.1, 0.05)) - ax.set_ylim(-0.40, 0) - ax.set_xlim(0, 100) - ylabel = ( - "Head-based minus effective stress-based\nsubsidence, " - + "in percent of ultimate value" - ) - else: - ax.set_ylim(0, 8) - ax.set_xlim(0, 100) - ylabel = ( - "Top minus bottom interbed effective stress-based\ndrainage " - + "rate, in percent of head-based drainage rate" - ) - ax.set_xlabel("Percent of time constant") - ax.set_ylabel(ylabel) - fs.heading(ax, letter=chr(ord("A") + idx)) - axes.append(ax) + # set color + cmap = plt.get_cmap("viridis") + cNorm = mpl.colors.Normalize(vmin=0, vmax=6) + scalarMap = mpl.cm.ScalarMappable(norm=cNorm, cmap=cmap) - for idx, (hb_dir, es_dir) in enumerate(zip(hb_dirs, es_dirs)): - sim_ws = os.path.join(ws, name, hb_dir) - s = flopy.mf6.MFSimulation().load(sim_ws=sim_ws, verbosity_level=0) - g = s.get_model(name) - hb_obs = g.csub.output.obs().data + axes = [] + for idx in range(2): + ax = fig.add_subplot(gs[idx]) + if idx == 0: + ax.set_yticks(np.arange(-0.40, 0.1, 0.05)) + ax.set_ylim(-0.40, 0) + ax.set_xlim(0, 100) + ylabel = ( + "Head-based minus effective stress-based\nsubsidence, " + + "in % of ultimate value" + ) + else: + ax.set_ylim(0, 8) + ax.set_xlim(0, 100) + ylabel = ( + "Top minus bottom interbed effective stress-\nbased " + + "rate, in % of head-based drainage rate" + ) + ax.set_xlabel("Percent of time constant") + ax.set_ylabel(ylabel) + styles.heading(ax, letter=chr(ord("A") + idx)) + axes.append(ax) + plt.subplots_adjust(wspace=0.36) + + for idx, (hb_dir, es_dir) in enumerate(zip(hb_dirs, es_dirs)): + sim_ws = os.path.join(workspace, name, hb_dir) + s = flopy.mf6.MFSimulation().load(sim_ws=sim_ws, verbosity_level=0) + g = s.get_model(name) + hb_obs = g.csub.output.obs().data + + ws0 = os.path.join(workspace, name, es_dir) + s0 = flopy.mf6.MFSimulation().load(sim_ws=ws0, verbosity_level=0) + g0 = s0.get_model(name) + es_obs = g0.csub.output.obs().data + + # calculate normalized simulation time + tpct = hb_obs["totim"] * 100 / tau0 + + thickness = thicknesses[idx] + if idx == 0: + color = "black" + else: + color = scalarMap.to_rgba(idx - 1) + label = f"Thickness = {int(thickness):>3d} m" - ws0 = os.path.join(ws, name, es_dir) - s0 = flopy.mf6.MFSimulation().load(sim_ws=ws0, verbosity_level=0) - g0 = s0.get_model(name) - es_obs = g0.csub.output.obs().data + v = 100.0 * (hb_obs["TCOMP"] - es_obs["TCOMP"]) / (skv * thickness) + ax = axes[0] + ax.plot(tpct, v, color=color, lw=0.75, label=label) - # calculate normalized simulation time - tpct = hb_obs["totim"] * 100 / tau0 - - thickness = thicknesses[idx] - if idx == 0: - color = "black" - else: - color = scalarMap.to_rgba(idx - 1) - label = f"Thickness = {int(thickness):>3d} m" - - v = 100.0 * (hb_obs["TCOMP"] - es_obs["TCOMP"]) / (skv * thickness) - ax = axes[0] - ax.plot(tpct, v, color=color, lw=0.75, label=label) - - denom = hb_obs["QTOP"] + hb_obs["QBOT"] - v = 100 * (es_obs["QTOP"] - es_obs["QBOT"]) / denom - ax = axes[1] - ax.plot(tpct, v, color=color, lw=0.75, label=label) - - # legend - ax = axes[-1] - leg = fs.graph_legend(ax, loc="upper right") - - # save figure - if config.plotSave: - fpth = os.path.join("..", "figures", f"{name}-01{config.figure_ext}") - if not silent: - print(f"saving...'{fpth}'") - fig.savefig(fpth) + denom = hb_obs["QTOP"] + hb_obs["QBOT"] + v = 100 * (es_obs["QTOP"] - es_obs["QBOT"]) / denom + ax = axes[1] + ax.plot(tpct, v, color=color, lw=0.75, label=label) + # legend + ax = axes[-1] + leg = styles.graph_legend(ax, loc="upper right") -# Function to plot the interbed head results for multiple interbed thicknesses + if plot_show: + plt.show() + if plot_save: + fpth = os.path.join("..", "figures", f"{name}-01.png") + if not silent: + print(f"saving...'{fpth}'") + fig.savefig(fpth) def plot_head_comparison(sim, silent=True): - verbose = not silent - fs = USGSFigure(figure_type="graph", verbose=verbose) - name = sim.name - ndcells = parameters[name]["ndelaycells"] - thicknesses = parameters[name]["bed_thickness"] - - # get the subdirectory names - hb_dirs, es_dirs = get_subdirs(sim) - - # setup the figure - fig = plt.figure(figsize=figure_size) - fig.subplots_adjust( - left=0.06, right=0.95, top=0.95, bottom=0.15, wspace=0.1 - ) - gs = mpl.gridspec.GridSpec(1, 6, figure=fig) - z = np.linspace(0, 1, ndcells) - yticks = np.arange(0, 1.1, 0.1) - - # set color - cmap = plt.get_cmap("viridis") - cNorm = mpl.colors.Normalize(vmin=0, vmax=6) - scalarMap = mpl.cm.ScalarMappable(norm=cNorm, cmap=cmap) - - # percentages to evaluate - pct_vals = ( - 1, - 5, - 10, - 50, - 100, - ) + """Plot the interbed head results for multiple interbed thicknesses""" + with styles.USGSPlot(): + name = sim.name + ndcells = parameters[name]["ndelaycells"] + thicknesses = parameters[name]["bed_thickness"] + + # get the subdirectory names + hb_dirs, es_dirs = get_subdirs(sim) + + # setup the figure + fig = plt.figure(figsize=figure_size) + fig.subplots_adjust(left=0.06, right=0.95, top=0.95, bottom=0.15, wspace=0.1) + gs = mpl.gridspec.GridSpec(1, 6, figure=fig) + z = np.linspace(0, 1, ndcells) + yticks = np.arange(0, 1.1, 0.1) + + # set color + cmap = plt.get_cmap("viridis") + cNorm = mpl.colors.Normalize(vmin=0, vmax=6) + scalarMap = mpl.cm.ScalarMappable(norm=cNorm, cmap=cmap) + + # percentages to evaluate + pct_vals = ( + 1, + 5, + 10, + 50, + 100, + ) - axes = [] - for idx in range(6): - ax = fig.add_subplot(gs[idx]) - ax.set_ylim(1, 0) - ax.set_xlim(-5, 5) - if idx < 5: - fs.heading(ax, letter=chr(ord("A") + idx)) - ax.set_yticks(yticks) - fs.remove_edge_ticks(ax) - text = r"$\frac{t}{\tau_0}$ = " + "{}".format( - pct_vals[idx] / 100.0 - ) - ax.text( - 0.25, - 0.01, - text, - ha="center", - va="bottom", - transform=ax.transAxes, - fontsize=8, - ) - else: - ax.set_xticks([]) - ax.set_yticks([]) - - if idx == 0: - ax.set_ylabel("Interbed position, relative to interbed thickness") - else: - if idx == 2: - text = ( - "Difference in head-based and effective stress-based" - + "\ninterbed heads, in percent of head-based interbed heads" + axes = [] + for idx in range(6): + ax = fig.add_subplot(gs[idx]) + ax.set_ylim(1, 0) + ax.set_xlim(-5, 5) + if idx < 5: + styles.heading(ax, letter=chr(ord("A") + idx)) + ax.set_yticks(yticks) + styles.remove_edge_ticks(ax) + text = r"$\frac{t}{\tau_0}$ = " + "{}".format(pct_vals[idx] / 100.0) + ax.text( + 0.25, + 0.01, + text, + ha="center", + va="bottom", + transform=ax.transAxes, + fontsize=8, ) - ax.set_xlabel(text) - ax.set_yticklabels([]) - axes.append(ax) - - for idx, (hb_dir, es_dir) in enumerate(zip(hb_dirs, es_dirs)): - sim_ws = os.path.join(ws, name, hb_dir) - s = flopy.mf6.MFSimulation().load(sim_ws=sim_ws, verbosity_level=0) - g = s.get_model(name) - hb_obs = g.csub.output.obs().data - hb_arr = fill_heads(hb_obs, ndcells) - - ws0 = os.path.join(ws, name, es_dir) - s0 = flopy.mf6.MFSimulation().load(sim_ws=ws0, verbosity_level=0) - g0 = s0.get_model(name) - es_obs = g0.csub.output.obs().data - es_arr = fill_heads(es_obs, ndcells) - # - # pth = os.path.join(ws, name, hb_dir, "{}.csub.obs.csv".format(name)) - # hb_obs = np.genfromtxt(pth, names=True, delimiter=",") - # hb_arr = fill_heads(hb_obs, ndcells) - # - # pth = os.path.join(ws, name, es_dir, "{}.csub.obs.csv".format(name)) - # es_obs = np.genfromtxt(pth, names=True, delimiter=",") - # es_arr = fill_heads(es_obs, ndcells) + else: + ax.set_xticks([]) + ax.set_yticks([]) - # calculate normalized simulation time - tpct = hb_obs["totim"] * 100 / tau0 - - # calculate location closest to 1, 5, 10, 50, and 100 percent of time constant - locs = {} - for i in pct_vals: - for jdx, t in enumerate(tpct): - if t <= i: - locs[i] = jdx - - for jdx, (key, ivalue) in enumerate(locs.items()): - # add data to the subplot - ax = axes[jdx] if idx == 0: + ax.set_ylabel("Interbed position, relative to interbed thickness") + else: + if idx == 2: + text = ( + "Difference in head-based and effective stress-based" + + "\ninterbed heads, in percent of head-based interbed heads" + ) + ax.set_xlabel(text) + ax.set_yticklabels([]) + axes.append(ax) + + for idx, (hb_dir, es_dir) in enumerate(zip(hb_dirs, es_dirs)): + sim_ws = os.path.join(workspace, name, hb_dir) + s = flopy.mf6.MFSimulation().load(sim_ws=sim_ws, verbosity_level=0) + g = s.get_model(name) + hb_obs = g.csub.output.obs().data + hb_arr = fill_heads(hb_obs, ndcells) + + ws0 = os.path.join(workspace, name, es_dir) + s0 = flopy.mf6.MFSimulation().load(sim_ws=ws0, verbosity_level=0) + g0 = s0.get_model(name) + es_obs = g0.csub.output.obs().data + es_arr = fill_heads(es_obs, ndcells) + # + # pth = os.path.join(ws, name, hb_dir, "{}.csub.obs.csv".format(name)) + # hb_obs = np.genfromtxt(pth, names=True, delimiter=",") + # hb_arr = fill_heads(hb_obs, ndcells) + # + # pth = os.path.join(ws, name, es_dir, "{}.csub.obs.csv".format(name)) + # es_obs = np.genfromtxt(pth, names=True, delimiter=",") + # es_arr = fill_heads(es_obs, ndcells) + + # calculate normalized simulation time + tpct = hb_obs["totim"] * 100 / tau0 + + # calculate location closest to 1, 5, 10, 50, and 100 percent of time constant + locs = {} + for i in pct_vals: + for jdx, t in enumerate(tpct): + if t <= i: + locs[i] = jdx + + for jdx, (key, ivalue) in enumerate(locs.items()): + # add data to the subplot + ax = axes[jdx] + if idx == 0: + color = "black" + else: + color = scalarMap.to_rgba(idx - 1) + hhb = hb_arr[ivalue, :] + hes = es_arr[ivalue, :] + v = 100.0 * (hhb - hes) / hhb + ax.plot(v, z, lw=0.75, color=color) + + # legend + ax = axes[-1] + ax.set_ylim(1, 0) + ax.set_xlim(-5, 5) + ax.spines["top"].set_color("none") + ax.spines["bottom"].set_color("none") + ax.spines["left"].set_color("none") + ax.spines["right"].set_color("none") + ax.patch.set_alpha(0.0) + for ic, b in enumerate(thicknesses): + if ic == 0: color = "black" else: - color = scalarMap.to_rgba(idx - 1) - hhb = hb_arr[ivalue, :] - hes = es_arr[ivalue, :] - v = 100.0 * (hhb - hes) / hhb - ax.plot(v, z, lw=0.75, color=color) - - # legend - ax = axes[-1] - ax.set_ylim(1, 0) - ax.set_xlim(-5, 5) - ax.spines["top"].set_color("none") - ax.spines["bottom"].set_color("none") - ax.spines["left"].set_color("none") - ax.spines["right"].set_color("none") - ax.patch.set_alpha(0.0) - for ic, b in enumerate(thicknesses): - if ic == 0: - color = "black" - else: - color = scalarMap.to_rgba(ic - 1) - label = f"Thickness = {int(b):>3d} m" - ax.plot([-1, -1], [-1, -1], lw=0.75, color=color, label=label) - - leg = fs.graph_legend(ax, loc="center", bbox_to_anchor=(0.64, 0.5)) - - # save figure - if config.plotSave: - fpth = os.path.join("..", "figures", f"{name}-02{config.figure_ext}") - if not silent: - print(f"saving...'{fpth}'") - fig.savefig(fpth) + color = scalarMap.to_rgba(ic - 1) + label = f"Thickness = {int(b):>3d} m" + ax.plot([-1, -1], [-1, -1], lw=0.75, color=color, label=label) + leg = styles.graph_legend(ax, loc="center", bbox_to_anchor=(0.64, 0.5)) -# Function to plot the model results. + if plot_show: + plt.show() + if plot_save: + fpth = os.path.join("..", "figures", f"{name}-02.png") + if not silent: + print(f"saving...'{fpth}'") + fig.savefig(fpth) def plot_results(sim, silent=True): - if config.plotModel: - name = sim.name + name = sim.name + if name.endswith("a"): + plot_grid(sim, silent=silent) + plot_head_based(sim, silent=silent) + elif name.endswith("b"): + plot_effstress(sim, silent=silent) + elif name.endswith("c"): + plot_comp_q_comparison(sim, silent=silent) + plot_head_comparison(sim, silent=silent) - if name.endswith("a"): - plot_grid(sim, silent=silent) - plot_head_based(sim, silent=silent) - elif name.endswith("b"): - plot_effstress(sim, silent=silent) - elif name.endswith("c"): - plot_comp_q_comparison(sim, silent=silent) - plot_head_comparison(sim, silent=silent) +# - -# Function that wraps all of the steps for the model -# -# 1. build_model, -# 2. write_model, -# 3. run_model, and -# 4. plot_results. +# ### Running the example # +# Define a function to run the example scenarios, then plot results. -def simulation(idx, silent=True): +# + +def scenarios(idx, silent=True): key = list(parameters.keys())[idx] interbed_thickness = parameters[key]["bed_thickness"] interbed_kv = parameters[key]["kv"] params = parameters[key].copy() - - success = False if len(interbed_thickness) == 1: params["bed_thickness"] = interbed_thickness[0] params["kv"] = interbed_kv[0] - sim = build_model(key, **params) - - write_model(sim, silent=silent) - - if config.runModel: - success = run_model(sim, silent=silent) - + sim = build_models(key, **params) + if write: + write_models(sim, silent=silent) + if run: + run_models(sim, silent=silent) else: for b, kv in zip(interbed_thickness, interbed_kv): for head_based in ( @@ -868,43 +813,26 @@ def simulation(idx, silent=True): params["bed_thickness"] = b params["kv"] = kv - sim = build_model(key, subdir_name=subdir_name, **params) - - write_model(sim, silent=silent) - - if config.runModel: - success = run_model(sim, silent=silent) - - if config.plotModel and success: + sim = build_models(key, subdir_name=subdir_name, **params) + if write: + write_models(sim, silent=silent) + if run: + run_models(sim, silent=silent) + if plot: plot_results(sim, silent=silent) -# nosetest - exclude block from this nosetest to the next nosetest -def test_01(): - simulation(0, silent=False) - - -def test_02(): - simulation(1, silent=False) - - -def test_03(): - simulation(2, silent=False) - +# - -# nosetest end -if __name__ == "__main__": - # ### Delay interbed drainage - # - # #### Head based solution +# Run and plot the head based solution. - simulation(0) +scenarios(0) - # #### Effective stress solution +# Run and plot the effective stress solution. - simulation(1) +scenarios(1) - # #### Head based for multiple interbed thicknesses +# Run and plot the head based for multiple interbed thicknesses. - simulation(2) +scenarios(2) diff --git a/scripts/ex-gwf-csub-p03.py b/scripts/ex-gwf-csub-p03.py index b03cf3679..319a3f4c5 100644 --- a/scripts/ex-gwf-csub-p03.py +++ b/scripts/ex-gwf-csub-p03.py @@ -1,55 +1,54 @@ -# ## One-dimensional compaction +# ## One-dimensional compaction example # # A one-dimensional MODFLOW 6 model was developed by Sneed (2008) to simulate # aquitard drainage, compaction and, land subsidence at the Holly site, located at # the Edwards Air Force base, in response to effective stress changes caused by # groundwater pumpage in the Antelope Valley in southern California. -# -# ### Problem Setup +# ### Initial setup # -# Imports +# Import dependencies, define the example name and workspace, and read settings from environment variables. +# + import datetime import os -import sys +import pathlib as pl import flopy import matplotlib as mpl import matplotlib.pyplot as plt import numpy as np import pandas as pd +import pooch +from flopy.plot.styles import styles +from modflow_devtools.latex import build_table, exp_format, float_format, int_format +from modflow_devtools.misc import get_env, timed -# Append to system path to include the common subdirectory - -sys.path.append(os.path.join("..", "common")) - -# import common functionality - -import build_table as bt -import config -from figspecs import USGSFigure - -# Set figure properties specific to the problem - -figure_size = (6.8, 3.4) -arrow_props = dict(facecolor="black", arrowstyle="-", lw=0.5) - -# Base simulation and model name and workspace - -ws = config.base_ws +# Example name and base workspace +sim_name = "ex-gwf-csub-p03" +workspace = pl.Path("../examples") -# Simulation name +# Settings from environment variables +write = get_env("WRITE", True) +run = get_env("RUN", True) +plot = get_env("PLOT", True) +plot_show = get_env("PLOT_SHOW", True) +plot_save = get_env("PLOT_SAVE", True) +# - -sim_name = "ex-gwf-csub-p03" +# ### Define parameters +# +# Define model units, parameters and other settings. +# + # Load the constant time series - -pth = os.path.join("..", "data", sim_name, "boundary_heads.csv") +pth = pooch.retrieve( + url=f"https://github.com/MODFLOW-USGS/modflow6-examples/raw/master/data/{sim_name}/boundary_heads.csv", + known_hash="md5:8177e15feeeedcdd59ee15745e796e59", +) csv_head = np.genfromtxt(pth, names=True, delimiter=",") # Reformat csv data into format for MODFLOW 6 timeseries file - chd_ts = [] for idx in range(csv_head.shape[0]): chd_ts.append( @@ -62,16 +61,12 @@ ) # Simulation starting date and time - dstart = datetime.datetime(1907, 1, 1, 0, 0, 0) # Create a datetime list for the simulation - date_list = [dstart + datetime.timedelta(days=x) for x in csv_head["time"]] - # Scenario parameters - parameters = { "ex-gwf-csub-p03a": { "head_based": True, @@ -204,12 +199,10 @@ } # Model units - length_units = "meters" time_units = "days" # Model parameters - nper = 353 nlay = 14 ncol = 1 @@ -392,8 +385,7 @@ "DELAY", ) -# Static temporal data used by TDIS file - +# Temporal discretization tdis_ds = [] for idx in range(82): tdis_ds.append((365.25, 12, 1.0)) @@ -401,7 +393,6 @@ tdis_ds.append((22.0, 22, 1.0)) # Constant head cells - c6 = [] for k, tag in zip( ( @@ -415,16 +406,214 @@ c6.append([k, 0, 0, tag]) # Solver parameters - nouter = 200 ninner = 100 hclose = 1e-6 rclose = 1e-3 linaccel = "cg" relax = 0.97 +# - + +# ### Model setup +# +# Define functions to build models, write input files, and run the simulation. + + +# + +def build_models( + name, + subdir_name=".", + head_based=True, + cg_ske=1e-3, + pcs0=0.0, + ssv=1e-1, + sse=1e-3, +): + sim_ws = os.path.join(workspace, name) + if subdir_name is not None: + sim_ws = os.path.join(sim_ws, subdir_name) + sim = flopy.mf6.MFSimulation(sim_name=name, sim_ws=sim_ws, exe_name="mf6") + flopy.mf6.ModflowTdis(sim, nper=nper, perioddata=tdis_ds, time_units=time_units) + flopy.mf6.ModflowIms( + sim, + print_option="summary", + outer_maximum=nouter, + outer_dvclose=hclose, + linear_acceleration=linaccel, + inner_maximum=ninner, + inner_dvclose=hclose, + relaxation_factor=relax, + rcloserecord=f"{rclose} strict", + ) + gwf = flopy.mf6.ModflowGwf( + sim, + modelname=name, + save_flows=True, + ) + flopy.mf6.ModflowGwfdis( + gwf, + length_units=length_units, + nlay=nlay, + nrow=nrow, + ncol=ncol, + delr=delr, + delc=delc, + top=top, + botm=botm, + ) + # gwf obs + opth = f"{name}.gwf.obs" + cpth = opth + ".csv" + obs_array = [] + for k in range(nlay): + obs_array.append( + [ + f"HD{k + 1:02d}", + "HEAD", + (k, 0, 0), + ] + ) + flopy.mf6.ModflowUtlobs( + gwf, + digits=10, + print_input=True, + filename=opth, + continuous={cpth: obs_array}, + ) + + flopy.mf6.ModflowGwfic(gwf, strt=strt) + flopy.mf6.ModflowGwfnpf( + gwf, + icelltype=icelltype, + k=k33, + k33=k33, + save_specific_discharge=True, + ) + flopy.mf6.ModflowGwfsto(gwf, iconvert=iconvert, ss=0.0, sy=0, transient={0: True}) + if head_based: + hb_bool = True + tsgm = None + tsgs = None + else: + hb_bool = None + tsgm = sgm + tsgs = sgs + sub6 = [] + for idx, cdelay in enumerate(ib_ctype): + sub6.append( + [ + idx, + ib_cellid[idx], + cdelay, + pcs0[idx], + ib_thickness[idx], + ib_rnb[idx], + ssv[idx], + sse[idx], + ib_theta, + ib_kv[idx], + ib_head[idx], + ib_name[idx], + ] + ) + csub = flopy.mf6.ModflowGwfcsub( + gwf, + print_input=True, + save_flows=True, + head_based=hb_bool, + specified_initial_interbed_state=True, + update_material_properties=True, + ndelaycells=39, + boundnames=True, + beta=4.65120000e-10, + gammaw=9806.65, + ninterbeds=len(sub6), + sgm=tsgm, + sgs=tsgs, + cg_theta=cg_theta, + cg_ske_cr=cg_ske, + packagedata=sub6, + ) + opth = f"{name}.csub.obs" + csub_csv = opth + ".csv" + obs = [ + ("cunit", "interbed-compaction", "cunit"), + ("aquitard", "interbed-compaction", "aquitard"), + ("nodelay", "interbed-compaction", "nodelay"), + ("delay", "interbed-compaction", "delay"), + ("es14", "estress-cell", (nlay - 1, 0, 0)), + ] + for k in (1, 2, 3, 4, 6, 7, 8, 9, 11, 13): + tag = f"tc{k + 1:02d}" + obs.append( + ( + tag, + "compaction-cell", + (k, 0, 0), + ) + ) + tag = f"skc{k + 1:02d}" + obs.append( + ( + tag, + "coarse-compaction", + (k, 0, 0), + ) + ) + orecarray = {csub_csv: obs} + csub.obs.initialize( + filename=opth, digits=10, print_input=True, continuous=orecarray + ) + + chd = flopy.mf6.ModflowGwfchd(gwf, stress_period_data={0: c6}) + + # initialize chd time series + csubnam = f"{sim_name}.head.ts" + chd.ts.initialize( + filename=csubnam, + timeseries=chd_ts, + time_series_namerecord=[ + "upper", + "middle", + "lower", + ], + interpolation_methodrecord=[ + "linear", + "linear", + "linear", + ], + sfacrecord=[ + "1.0", + "1.0", + "1.0", + ], + ) -# Create data for plotting + flopy.mf6.ModflowGwfoc( + gwf, + printrecord=[("BUDGET", "ALL")], + ) + return sim + +def write_models(sim, silent=True): + sim.write_simulation(silent=silent) + + +@timed +def run_models(sim, silent=True): + success, buff = sim.run_simulation(silent=silent) + assert success, buff + + +# - + +# ### Plotting results +# +# Define functions to plot model results. + +# + +# Figure properties xticks = (1907, 1917, 1927, 1937, 1947, 1957, 1967, 1977, 1987, 1997, 2007) s = ( "01-01-1907", @@ -470,225 +659,12 @@ zelevs.append(z) edges.append((-z, -z)) - -# ### Functions to build, write, run, and plot the model -# -# MODFLOW 6 flopy simulation object (sim) is returned if building the model - - -def build_model( - name, - subdir_name=".", - head_based=True, - cg_ske=1e-3, - pcs0=0.0, - ssv=1e-1, - sse=1e-3, -): - if config.buildModel: - sim_ws = os.path.join(ws, name) - if subdir_name is not None: - sim_ws = os.path.join(sim_ws, subdir_name) - sim = flopy.mf6.MFSimulation( - sim_name=name, sim_ws=sim_ws, exe_name="mf6" - ) - flopy.mf6.ModflowTdis( - sim, nper=nper, perioddata=tdis_ds, time_units=time_units - ) - flopy.mf6.ModflowIms( - sim, - print_option="summary", - outer_maximum=nouter, - outer_dvclose=hclose, - linear_acceleration=linaccel, - inner_maximum=ninner, - inner_dvclose=hclose, - relaxation_factor=relax, - rcloserecord=f"{rclose} strict", - ) - gwf = flopy.mf6.ModflowGwf( - sim, - modelname=name, - save_flows=True, - ) - flopy.mf6.ModflowGwfdis( - gwf, - length_units=length_units, - nlay=nlay, - nrow=nrow, - ncol=ncol, - delr=delr, - delc=delc, - top=top, - botm=botm, - ) - # gwf obs - opth = f"{name}.gwf.obs" - cpth = opth + ".csv" - obs_array = [] - for k in range(nlay): - obs_array.append( - [ - f"HD{k + 1:02d}", - "HEAD", - (k, 0, 0), - ] - ) - flopy.mf6.ModflowUtlobs( - gwf, - digits=10, - print_input=True, - filename=opth, - continuous={cpth: obs_array}, - ) - - flopy.mf6.ModflowGwfic(gwf, strt=strt) - flopy.mf6.ModflowGwfnpf( - gwf, - icelltype=icelltype, - k=k33, - k33=k33, - save_specific_discharge=True, - ) - flopy.mf6.ModflowGwfsto( - gwf, iconvert=iconvert, ss=0.0, sy=0, transient={0: True} - ) - if head_based: - hb_bool = True - tsgm = None - tsgs = None - else: - hb_bool = None - tsgm = sgm - tsgs = sgs - sub6 = [] - for idx, cdelay in enumerate(ib_ctype): - sub6.append( - [ - idx, - ib_cellid[idx], - cdelay, - pcs0[idx], - ib_thickness[idx], - ib_rnb[idx], - ssv[idx], - sse[idx], - ib_theta, - ib_kv[idx], - ib_head[idx], - ib_name[idx], - ] - ) - csub = flopy.mf6.ModflowGwfcsub( - gwf, - print_input=True, - save_flows=True, - head_based=hb_bool, - specified_initial_interbed_state=True, - update_material_properties=True, - ndelaycells=39, - boundnames=True, - beta=4.65120000e-10, - gammaw=9806.65, - ninterbeds=len(sub6), - sgm=tsgm, - sgs=tsgs, - cg_theta=cg_theta, - cg_ske_cr=cg_ske, - packagedata=sub6, - ) - opth = f"{name}.csub.obs" - csub_csv = opth + ".csv" - obs = [ - ("cunit", "interbed-compaction", "cunit"), - ("aquitard", "interbed-compaction", "aquitard"), - ("nodelay", "interbed-compaction", "nodelay"), - ("delay", "interbed-compaction", "delay"), - ("es14", "estress-cell", (nlay - 1, 0, 0)), - ] - for k in (1, 2, 3, 4, 6, 7, 8, 9, 11, 13): - tag = f"tc{k + 1:02d}" - obs.append( - ( - tag, - "compaction-cell", - (k, 0, 0), - ) - ) - tag = f"skc{k + 1:02d}" - obs.append( - ( - tag, - "coarse-compaction", - (k, 0, 0), - ) - ) - orecarray = {csub_csv: obs} - csub.obs.initialize( - filename=opth, digits=10, print_input=True, continuous=orecarray - ) - - chd = flopy.mf6.ModflowGwfchd(gwf, stress_period_data={0: c6}) - - # initialize chd time series - csubnam = f"{sim_name}.head.ts" - chd.ts.initialize( - filename=csubnam, - timeseries=chd_ts, - time_series_namerecord=[ - "upper", - "middle", - "lower", - ], - interpolation_methodrecord=[ - "linear", - "linear", - "linear", - ], - sfacrecord=[ - "1.0", - "1.0", - "1.0", - ], - ) - - flopy.mf6.ModflowGwfoc( - gwf, - printrecord=[("BUDGET", "ALL")], - ) - return sim - return None - - -# Function to write MODFLOW 6 model files - - -def write_model(sim, silent=True): - if config.writeModel: - sim.write_simulation(silent=silent) - - -# Function to run the model. -# True is returned if the model runs successfully -# - - -@config.timeit -def run_model(sim, silent=True): - success = True - if config.runModel: - success, buff = sim.run_simulation(silent=silent) - if not success: - print(buff) - - return success - - -# Function to make summary tables +figure_size = (6.8, 3.4) +arrow_props = dict(facecolor="black", arrowstyle="-", lw=0.5) def export_tables(silent=True): - if config.plotSave: + if plot_save: name = list(parameters.keys())[1] caption = f"Aquifer properties for example {sim_name}." @@ -707,16 +683,14 @@ def export_tables(silent=True): ] arr = np.zeros(nlay, dtype=dtype) for k in range(nlay): - arr["k"][k] = bt.int_format(k + 1) - arr["thickness"][k] = bt.float_format(zelevs[k] - zelevs[k + 1]) - arr["k33"][k] = bt.exp_format(k33[k]) - arr["h0"][k] = bt.float_format(strt[k]) + arr["k"][k] = int_format(k + 1) + arr["thickness"][k] = float_format(zelevs[k] - zelevs[k + 1]) + arr["k33"][k] = exp_format(k33[k]) + arr["h0"][k] = float_format(strt[k]) if not silent: print(f"creating...'{fpth}'") col_widths = (0.1, 0.15, 0.30, 0.25) - bt.build_table( - caption, fpth, arr, headings=headings, col_widths=col_widths - ) + build_table(caption, fpth, arr, headings=headings, col_widths=col_widths) caption = f"Interbed properties for example {sim_name}." headings = ( @@ -734,17 +708,17 @@ def export_tables(silent=True): ] arr = np.zeros(len(ib_ctype), dtype=dtype) for idx, ctype in enumerate(ib_ctype): - arr["ib"][idx] = bt.int_format(idx + 1) - arr["k"][idx] = bt.int_format(ib_cellid[idx][0] + 1) + arr["ib"][idx] = int_format(idx + 1) + arr["k"][idx] = int_format(ib_cellid[idx][0] + 1) if ctype == "nodelay": - arr["thickness"][idx] = bt.float_format(ib_thickness[idx]) + arr["thickness"][idx] = float_format(ib_thickness[idx]) else: b = ib_thickness[idx] * ib_rnb[idx] - arr["thickness"][idx] = bt.float_format(b) - arr["pcs0"][idx] = bt.float_format(parameters[name]["pcs0"][idx]) + arr["thickness"][idx] = float_format(b) + arr["pcs0"][idx] = float_format(parameters[name]["pcs0"][idx]) if not silent: print(f"creating...'{fpth}'") - bt.build_table(caption, fpth, arr, headings=headings) + build_table(caption, fpth, arr, headings=headings) caption = f"Aquifer storage properties for example {sim_name}." headings = ( @@ -755,18 +729,14 @@ def export_tables(silent=True): dtype = [("k", "U30"), ("ss", "U30")] arr = np.zeros(4, dtype=dtype) for idx, k in enumerate((4, 6, 11, 13)): - arr["k"][idx] = bt.int_format(k + 1) - arr["ss"][idx] = bt.exp_format(parameters[name]["cg_ske"][k]) + arr["k"][idx] = int_format(k + 1) + arr["ss"][idx] = exp_format(parameters[name]["cg_ske"][k]) if not silent: print(f"creating...'{fpth}'") col_widths = (0.1, 0.25) - bt.build_table( - caption, fpth, arr, headings=headings, col_widths=col_widths - ) + build_table(caption, fpth, arr, headings=headings, col_widths=col_widths) - caption = "Interbed storage properties for example {}.".format( - sim_name - ) + caption = "Interbed storage properties for example {}.".format(sim_name) headings = ( "Interbed", "Layer", @@ -782,14 +752,14 @@ def export_tables(silent=True): ] arr = np.zeros(len(ib_ctype), dtype=dtype) for idx, ctype in enumerate(ib_ctype): - arr["ib"][idx] = bt.int_format(idx + 1) - arr["k"][idx] = bt.int_format(ib_cellid[idx][0] + 1) - arr["ssv"][idx] = bt.exp_format(parameters[name]["ssv"][idx]) - arr["sse"][idx] = bt.exp_format(parameters[name]["sse"][idx]) + arr["ib"][idx] = int_format(idx + 1) + arr["k"][idx] = int_format(ib_cellid[idx][0] + 1) + arr["ssv"][idx] = exp_format(parameters[name]["ssv"][idx]) + arr["sse"][idx] = exp_format(parameters[name]["sse"][idx]) if not silent: print(f"creating...'{fpth}'") col_widths = (0.2, 0.2, 0.2, 0.2) - bt.build_table( + build_table( caption, fpth, arr, @@ -797,31 +767,24 @@ def export_tables(silent=True): col_widths=col_widths, ) - return - - -# Function to get observed data as a pandas dataframe - -def get_obs_dataframe(file_name="008N010W01Q005S_1D.csv"): - fpth = os.path.join("..", "data", sim_name, file_name) +def get_obs_dataframe(file_name, hash): + fpth = pooch.retrieve( + url=f"https://github.com/MODFLOW-USGS/modflow6-examples/raw/master/data/{sim_name}/{file_name}", + known_hash=f"md5:{hash}", + ) df = pd.read_csv(fpth, index_col=0) df.index = pd.to_datetime(df.index.values) df.rename({"mean": "observed"}, inplace=True, axis=1) return df -# Function to get simulation data as a pandas dataframe - - def process_sim_csv( fpth, index_tag="time", origin_str="1908-05-09 00:00:00.000000", **kwargs ): v = pd.read_csv(fpth, **kwargs) - v["date"] = pd.to_datetime( - v[index_tag].values, unit="d", origin=origin_str - ) + v["date"] = pd.to_datetime(v[index_tag].values, unit="d", origin=origin_str) v.set_index("date", inplace=True) v.drop(columns=index_tag, inplace=True) @@ -830,15 +793,8 @@ def process_sim_csv( return v, col_list -# Function to process compaction data and return a pandas dataframe - - -def get_sim_dataframe( - fpth, index_tag="time", origin_str="1908-05-09 00:00:00.000000" -): - v, col_list = process_sim_csv( - fpth, index_tag=index_tag, origin_str=origin_str - ) +def get_sim_dataframe(fpth, index_tag="time", origin_str="1908-05-09 00:00:00.000000"): + v, col_list = process_sim_csv(fpth, index_tag=index_tag, origin_str=origin_str) # calculate total skeletal and total shape = v[col_list[0]].values.shape[0] @@ -861,9 +817,6 @@ def get_sim_dataframe( return v -# Function to new DataFrame with all columns values interpolated to new_index values - - def dataframe_interp(df_in, new_index): df_out = pd.DataFrame(index=new_index) df_out.index.name = df_in.index.name @@ -874,9 +827,6 @@ def dataframe_interp(df_in, new_index): return df_out -# Function to process compaction data - - def process_csub_obs(fpth): tdata = flopy.utils.Mf6Obs(fpth).data dtype = [ @@ -913,9 +863,6 @@ def process_csub_obs(fpth): return v -# Function used for plots - - def set_label(label, text="text"): if label == "": label = text @@ -966,9 +913,7 @@ def print_label(ax, zelev, k, fontsize=6): def constant_heads(ax, annotate=False, fontsize=6, xrange=(0, 1)): - arrowprops = dict( - facecolor="black", arrowstyle="-", lw=0.5, shrinkA=0, shrinkB=0 - ) + arrowprops = dict(facecolor="black", arrowstyle="-", lw=0.5, shrinkA=0, shrinkB=0) label = "" for k in [0, 5, 10, 12]: label = set_label(label, text="Constant head") @@ -1018,763 +963,729 @@ def constant_heads(ax, annotate=False, fontsize=6, xrange=(0, 1)): return -# Function to plot the model grid - - def plot_grid(silent=True): - verbose = not silent - fs = USGSFigure(figure_type="map", verbose=verbose) - name = list(parameters.keys())[0] - - # # load the model - # sim = flopy.mf6.MFSimulation.load(sim_name=name, sim_ws=sim_ws) - # gwf = sim.get_model(name) + with styles.USGSMap(): + # # load the model + # sim = flopy.mf6.MFSimulation.load(sim_name=name, sim_ws=sim_ws) + # gwf = sim.get_model(name) + + xrange = (0, 1) + chds = (5, 10, 12) + + fig, axes = plt.subplots(nrows=1, ncols=3, sharey=True, figsize=(5.1, 4.0)) + plt.subplots_adjust(wspace=1) + + for idx, ax in enumerate(axes): + ax.set_xlim(xrange) + ax.set_ylim(edges[-1][0], 0) + for edge in edges: + ax.plot(xrange, edge, lw=0.5, color="black") + ax.tick_params( + axis="x", which="both", bottom=False, top=False, labelbottom=False + ) + ax.tick_params(axis="y", which="both", right=False, labelright=False) - xrange = (0, 1) - chds = (5, 10, 12) + ax = axes[0] + ax.fill_between( + xrange, + edges[0], + y2=edges[1], + color="green", + lw=0, + label="Upper aquifer", + ) - fig, axes = plt.subplots(nrows=1, ncols=3, sharey=True, figsize=(5.1, 4.0)) + label = "" + for k in (1, 2, 3): + label = set_label(label, text="Confining unit") + ax.fill_between( + xrange, edges[k], y2=edges[k + 1], color="brown", lw=0, label=label + ) - for idx, ax in enumerate(axes): - ax.set_xlim(xrange) - ax.set_ylim(edges[-1][0], 0) - for edge in edges: - ax.plot(xrange, edge, lw=0.5, color="black") - ax.tick_params( - axis="x", which="both", bottom=False, top=False, labelbottom=False - ) - ax.tick_params(axis="y", which="both", right=False, labelright=False) - - ax = axes[0] - ax.fill_between( - xrange, - edges[0], - y2=edges[1], - color="green", - lw=0, - label="Upper aquifer", - ) + label = "" + for k in (7, 8, 9): + label = set_label(label, text="Thick aquitard") + ax.fill_between( + xrange, edges[k], y2=edges[k + 1], color="tan", lw=0, label=label + ) - label = "" - for k in (1, 2, 3): - label = set_label(label, text="Confining unit") + # middle aquifer + midz = 825.0 / 3.8081 + midz = [edges[4], edges[7], edges[10], (midz, midz)] ax.fill_between( - xrange, edges[k], y2=edges[k + 1], color="brown", lw=0, label=label + xrange, midz[0], y2=midz[1], color="cyan", lw=0, label="Middle aquifer" ) + ax.fill_between(xrange, midz[2], y2=midz[3], color="cyan", lw=0) - label = "" - for k in (7, 8, 9): - label = set_label(label, text="Thick aquitard") + # lower aquifer ax.fill_between( - xrange, edges[k], y2=edges[k + 1], color="tan", lw=0, label=label + xrange, + midz[-1], + y2=edges[-1], + color="blue", + lw=0, + label="Lower aquifer", ) - # middle aquifer - midz = 825.0 / 3.8081 - midz = [edges[4], edges[7], edges[10], (midz, midz)] - ax.fill_between( - xrange, midz[0], y2=midz[1], color="cyan", lw=0, label="Middle aquifer" - ) - ax.fill_between(xrange, midz[2], y2=midz[3], color="cyan", lw=0) - - # lower aquifer - ax.fill_between( - xrange, - midz[-1], - y2=edges[-1], - color="blue", - lw=0, - label="Lower aquifer", - ) + styles.graph_legend(ax, loc="lower right", frameon=True, framealpha=1) + styles.heading(ax=ax, letter="A", heading="Hydrostratigraphy") + styles.remove_edge_ticks(ax) + ax.set_ylabel("Depth below land\nsurface, in meters") - fs.graph_legend(ax, loc="lower right", frameon=True, framealpha=1) - fs.heading(ax=ax, letter="A", heading="Hydrostratigraphy") - fs.remove_edge_ticks(ax) - ax.set_ylabel("Depth below land surface, in meters") + # csub interbeds + ax = axes[1] - # csub interbeds - ax = axes[1] + nodelay = (1, 2, 3, 6, 7, 8, 9) + label = "" + for k in nodelay: + label = set_label(label, text="No-delay") + ax.fill_between( + xrange, edges[k], y2=edges[k + 1], color="0.5", lw=0, label=label + ) - nodelay = (1, 2, 3, 6, 7, 8, 9) - label = "" - for k in nodelay: - label = set_label(label, text="No-delay") - ax.fill_between( - xrange, edges[k], y2=edges[k + 1], color="0.5", lw=0, label=label - ) + comb = [4, 11, 13] + label = "" + for k in comb: + label = set_label(label, text="No-delay\nand delay") + ax.fill_between( + xrange, edges[k], y2=edges[k + 1], color="brown", lw=0, label=label + ) - comb = [4, 11, 13] - label = "" - for k in comb: - label = set_label(label, text="No-delay\nand delay") - ax.fill_between( - xrange, edges[k], y2=edges[k + 1], color="brown", lw=0, label=label - ) + for k in chds: + ax.fill_between( + xrange, + edges[k], + y2=edges[k + 1], + color="white", + lw=0.75, + zorder=100, + ) - for k in chds: - ax.fill_between( - xrange, - edges[k], - y2=edges[k + 1], - color="white", - lw=0.75, - zorder=100, - ) + leg = styles.graph_legend(ax, loc="lower right", frameon=True, framealpha=1) + leg.set_zorder(100) + styles.heading(ax=ax, letter="B", heading="Interbed types") + styles.remove_edge_ticks(ax) - leg = fs.graph_legend(ax, loc="lower right", frameon=True, framealpha=1) - leg.set_zorder(100) - fs.heading(ax=ax, letter="B", heading="Interbed types") - fs.remove_edge_ticks(ax) + # boundary conditions + ax = axes[2] + constant_heads(ax) - # boundary conditions - ax = axes[2] - constant_heads(ax) + for k in llabels: + print_label(ax, edges, k) - for k in llabels: - print_label(ax, edges, k) + styles.graph_legend(ax, loc="lower left", frameon=True) + styles.heading(ax=ax, letter="C", heading="Boundary conditions") + styles.remove_edge_ticks(ax) - fs.graph_legend(ax, loc="lower left") - fs.heading(ax=ax, letter="C", heading="Boundary conditions") - fs.remove_edge_ticks(ax) + fig.tight_layout(pad=0.5) - fig.tight_layout(pad=0.5) + if plot_show: + plt.show() + if plot_save: + fpth = os.path.join("..", "figures", f"{sim_name}-grid.png") + if not silent: + print(f"saving...'{fpth}'") + fig.savefig(fpth) - # save figure - if config.plotSave: - fpth = os.path.join( - "..", "figures", f"{sim_name}-grid{config.figure_ext}" - ) - if not silent: - print(f"saving...'{fpth}'") - fig.savefig(fpth) +def plot_boundary_heads(silent=True): + with styles.USGSPlot(): -# Function to plot the boundary heads + def process_dtw_obs(fpth): + v = flopy.utils.Mf6Obs(fpth).data + v["totim"] /= 365.25 + v["totim"] += 1908.353182752 + for key in v.dtype.names[1:]: + v[key] *= -1.0 + return v + name = list(parameters.keys())[0] + pth = os.path.join(workspace, name, f"{name}.gwf.obs.csv") + hdata = process_dtw_obs(pth) -def plot_boundary_heads(silent=True): - verbose = not silent - fs = USGSFigure(figure_type="graph", verbose=verbose) - - def process_dtw_obs(fpth): - v = flopy.utils.Mf6Obs(fpth).data - v["totim"] /= 365.25 - v["totim"] += 1908.353182752 - for key in v.dtype.names[1:]: - v[key] *= -1.0 - return v - - name = list(parameters.keys())[0] - pth = os.path.join(ws, name, f"{name}.gwf.obs.csv") - hdata = process_dtw_obs(pth) - - pheads = ("HD01", "HD12", "HD14") - hlabels = ("Upper aquifer", "Middle aquifer", "Lower aquifer") - hcolors = ("green", "cyan", "blue") - - fig, ax = plt.subplots(nrows=1, ncols=1, figsize=(6.8, 6.8 / 3)) - - ax.set_xlim(1907, 2007) - ax.set_xticks(xticks) - ax.set_ylim(50.0, -10.0) - ax.set_yticks(sorted([50.0, 40.0, 30.0, 20.0, 10.0, 0.0, -10.0])) - - ax.plot([1907, 2007], [0, 0], lw=0.5, color="0.5") - for idx, key in enumerate(pheads): - ax.plot( - hdata["totim"], - hdata[key], - lw=0.75, - color=hcolors[idx], - label=hlabels[idx], - ) + pheads = ("HD01", "HD12", "HD14") + hlabels = ("Upper aquifer", "Middle aquifer", "Lower aquifer") + hcolors = ("green", "cyan", "blue") - fs.graph_legend(ax=ax, frameon=False) - ax.set_ylabel(f"Depth to water, in {length_units}") - ax.set_xlabel("Year") + fig, ax = plt.subplots(nrows=1, ncols=1, figsize=(6.8, 6.8 / 3)) - fs.remove_edge_ticks(ax=ax) + ax.set_xlim(1907, 2007) + ax.set_xticks(xticks) + ax.set_ylim(50.0, -10.0) + ax.set_yticks(sorted([50.0, 40.0, 30.0, 20.0, 10.0, 0.0, -10.0])) + + ax.plot([1907, 2007], [0, 0], lw=0.5, color="0.5") + for idx, key in enumerate(pheads): + ax.plot( + hdata["totim"], + hdata[key], + lw=0.75, + color=hcolors[idx], + label=hlabels[idx], + ) - fig.tight_layout() + styles.graph_legend(ax=ax, frameon=False) + ax.set_ylabel(f"Depth to water, in {length_units}") + ax.set_xlabel("Year") - # save figure - if config.plotSave: - fpth = os.path.join( - "..", "figures", f"{sim_name}-01{config.figure_ext}" - ) - if not silent: - print(f"saving...'{fpth}'") - fig.savefig(fpth) + styles.remove_edge_ticks(ax=ax) + fig.tight_layout() -# Function to plot the head and effective stress based results + if plot_show: + plt.show() + if plot_save: + fpth = os.path.join("..", "figures", f"{sim_name}-01.png") + if not silent: + print(f"saving...'{fpth}'") + fig.savefig(fpth) def plot_head_es_comparison(silent=True): - verbose = not silent - fs = USGSFigure(figure_type="graph", verbose=verbose) - name = list(parameters.keys())[0] - pth = os.path.join(ws, name, f"{name}.csub.obs.csv") - hb = process_csub_obs(pth) - - name = list(parameters.keys())[1] - pth = os.path.join(ws, name, f"{name}.csub.obs.csv") - es = process_csub_obs(pth) - - ymin = (2.0, 1, 1, 1, 0.1, 0.1) - - me = {} - for idx, key in enumerate(pcomp): - v = (es[key] - hb[key]).mean() - me[key] = v - - fig, axes = plt.subplots(nrows=6, ncols=1, sharex=True, figsize=(6.8, 4.7)) - for idx, key in enumerate(pcomp): - label = clabels[idx] - ax = axes[idx] - ax.set_xlim(1907, 2007) - ax.set_ylim(0, ymin[idx]) - ax.set_xticks(xticks) - stext = "none" - otext = "none" - if idx == 0: - stext = "Effective stress-based" - otext = "Head-based" - mtext = f"mean error = {me[key]:7.4f} {length_units}" - ax.plot(hb["totim"], hb[key], color="#238A8DFF", lw=1.25, label=otext) - ax.plot( - es["totim"], - es[key], - color="black", - lw=0.75, - label=stext, - zorder=101, + with styles.USGSPlot() as fs: + name = list(parameters.keys())[0] + pth = os.path.join(workspace, name, f"{name}.csub.obs.csv") + hb = process_csub_obs(pth) + + name = list(parameters.keys())[1] + pth = os.path.join(workspace, name, f"{name}.csub.obs.csv") + es = process_csub_obs(pth) + + ymin = (2.0, 1, 1, 1, 0.1, 0.1) + + me = {} + for idx, key in enumerate(pcomp): + v = (es[key] - hb[key]).mean() + me[key] = v + + fig, axes = plt.subplots(nrows=6, ncols=1, sharex=True, figsize=(6.8, 4.7)) + for idx, key in enumerate(pcomp): + label = clabels[idx] + ax = axes[idx] + ax.set_xlim(1907, 2007) + ax.set_ylim(0, ymin[idx]) + ax.set_xticks(xticks) + stext = "none" + otext = "none" + if idx == 0: + stext = "Effective stress-based" + otext = "Head-based" + mtext = f"mean error = {me[key]:7.4f} {length_units}" + ax.plot(hb["totim"], hb[key], color="#238A8DFF", lw=1.25, label=otext) + ax.plot( + es["totim"], + es[key], + color="black", + lw=0.75, + label=stext, + zorder=101, + ) + ltext = chr(ord("A") + idx) + htext = f"{label}" + styles.heading(ax, letter=ltext, heading=htext) + va = "bottom" + ym = 0.15 + if idx in [2, 3]: + va = "top" + ym = 0.85 + ax.text( + 0.99, + ym, + mtext, + ha="right", + va=va, + transform=ax.transAxes, + fontsize=7, + ) + styles.remove_edge_ticks(ax=ax) + if idx == 0: + styles.graph_legend(ax, loc="center left", ncol=2) + if idx == 5: + ax.set_xlabel("Year") + + axp1 = fig.add_subplot(1, 1, 1, frameon=False) + axp1.tick_params( + labelcolor="none", top="off", bottom="off", left="off", right="off" ) - ltext = chr(ord("A") + idx) - htext = f"{label}" - fs.heading(ax, letter=ltext, heading=htext) - va = "bottom" - ym = 0.15 - if idx in [2, 3]: - va = "top" - ym = 0.85 - ax.text( - 0.99, - ym, - mtext, - ha="right", - va=va, - transform=ax.transAxes, - fontsize=7, + axp1.set_xlim(0, 1) + axp1.set_xticks([0, 1]) + axp1.set_ylim(0, 1) + axp1.set_yticks([0, 1]) + axp1.set_ylabel(f"Compaction, in {length_units}") + axp1.yaxis.set_label_coords(-0.05, 0.5) + styles.remove_edge_ticks(ax) + + fig.tight_layout(pad=0.0001) + + if plot_show: + plt.show() + if plot_save: + fpth = os.path.join("..", "figures", f"{sim_name}-02.png") + if not silent: + print(f"saving...'{fpth}'") + fig.savefig(fpth) + + +def plot_calibration(silent=True): + with styles.USGSPlot() as fs: + name = list(parameters.keys())[1] + pth = os.path.join(workspace, name, f"{name}.csub.obs.csv") + df_sim = get_sim_dataframe(pth) + df_sim.rename({"TOTAL": "simulated"}, inplace=True, axis=1) + + pth = pooch.retrieve( + url=f"https://github.com/MODFLOW-USGS/modflow6-examples/raw/master/data/{sim_name}/boundary_heads.csv", + known_hash="md5:8177e15feeeedcdd59ee15745e796e59", ) - fs.remove_edge_ticks(ax=ax) - if idx == 0: - fs.graph_legend(ax, loc="center left", ncol=2) - if idx == 5: - ax.set_xlabel("Year") - - axp1 = fig.add_subplot(1, 1, 1, frameon=False) - axp1.tick_params( - labelcolor="none", top="off", bottom="off", left="off", right="off" - ) - axp1.set_xlim(0, 1) - axp1.set_xticks([0, 1]) - axp1.set_ylim(0, 1) - axp1.set_yticks([0, 1]) - axp1.set_ylabel(f"Compaction, in {length_units}") - axp1.yaxis.set_label_coords(-0.05, 0.5) - fs.remove_edge_ticks(ax) - - fig.tight_layout(pad=0.0001) - - # save figure - if config.plotSave: - fpth = os.path.join( - "..", "figures", f"{sim_name}-02{config.figure_ext}" + df_obs_heads, col_list = process_sim_csv(pth) + + ccolors = ( + "black", + "tan", + "cyan", + "brown", + "blue", + "violet", ) - if not silent: - print(f"saving...'{fpth}'") - fig.savefig(fpth) + xf0 = datetime.datetime(1907, 1, 1, 0, 0, 0) + xf1 = datetime.datetime(2007, 1, 1, 0, 0, 0) + xf0s = datetime.datetime(1990, 1, 1, 0, 0, 0) + xf1s = datetime.datetime(2007, 1, 1, 0, 0, 0) + xc0 = datetime.datetime(1992, 10, 1, 0, 0, 0) + xc1 = datetime.datetime(2006, 9, 4, 0, 0, 0) + dx = xc1 - xc0 + xca = xc0 + dx / 2 + + # get observation data + df = get_obs_dataframe( + file_name="008N010W01Q005S_obs.csv", hash="96dd2d0f0eca8c0293275bf87073547e" + ) + ix0 = df.index.get_loc("2006-09-04 00:00:00") + offset = df_sim["simulated"].values[-1] - df.observed.values[ix0] + df.observed += offset + + # -- subplot a ----------------------------------------------------------- + # build box for subplot B + o = datetime.timedelta(31) + ix = (xf0s, xf0s, xf1s - o, xf1s - o, xf0s) + iy = (1.15, 1.45, 1.45, 1.15, 1.15) + # -- subplot a ----------------------------------------------------------- + + # -- subplot c ----------------------------------------------------------- + # get observations + df_pc = get_obs_dataframe( + file_name="008N010W01Q005S_1D.csv", hash="167f83f51692165394442b0eb1fec45e" + ) -def plot_calibration(silent=True): - verbose = not silent - fs = USGSFigure(figure_type="graph", verbose=verbose) - - name = list(parameters.keys())[1] - pth = os.path.join(ws, name, f"{name}.csub.obs.csv") - df_sim = get_sim_dataframe(pth) - df_sim.rename({"TOTAL": "simulated"}, inplace=True, axis=1) - - pth = os.path.join("..", "data", sim_name, "boundary_heads.csv") - df_obs_heads, col_list = process_sim_csv(pth) - - ccolors = ( - "black", - "tan", - "cyan", - "brown", - "blue", - "violet", - ) + # get index for start of calibration period for subplot c + ix0 = df_sim.index.get_loc("1992-10-01 12:00:00") - xf0 = datetime.datetime(1907, 1, 1, 0, 0, 0) - xf1 = datetime.datetime(2007, 1, 1, 0, 0, 0) - xf0s = datetime.datetime(1990, 1, 1, 0, 0, 0) - xf1s = datetime.datetime(2007, 1, 1, 0, 0, 0) - xc0 = datetime.datetime(1992, 10, 1, 0, 0, 0) - xc1 = datetime.datetime(2006, 9, 4, 0, 0, 0) - dx = xc1 - xc0 - xca = xc0 + dx / 2 - - # get observation data - df = get_obs_dataframe(file_name="008N010W01Q005S_obs.csv") - ix0 = df.index.get_loc("2006-09-04 00:00:00") - offset = df_sim["simulated"].values[-1] - df.observed.values[ix0] - df.observed += offset - - # -- subplot a ----------------------------------------------------------- - # build box for subplot B - o = datetime.timedelta(31) - ix = (xf0s, xf0s, xf1s - o, xf1s - o, xf0s) - iy = (1.15, 1.45, 1.45, 1.15, 1.15) - # -- subplot a ----------------------------------------------------------- - - # -- subplot c ----------------------------------------------------------- - # get observations - df_pc = get_obs_dataframe() - - # get index for start of calibration period for subplot c - ix0 = df_sim.index.get_loc("1992-10-01 12:00:00") - - # get initial simulated compaction - cstart = df_sim.simulated[ix0] - - # cut off initial portion of simulated compaction - df_sim_pc = df_sim[ix0:].copy() - - # reset initial compaction to 0. - df_sim_pc.simulated -= cstart - - # reset simulated so maximum compaction is the same - offset = df_pc.observed.values.max() - df_sim_pc.simulated.values[-1] - df_sim.simulated += offset - - # interpolate subsidence observations to the simulation index for subplot c - df_iobs_pc = dataframe_interp(df_pc, df_sim_pc.index) - - # truncate head to start of observations - head_pc = dataframe_interp(df_obs_heads, df_sim_pc.index) - - # calculate geostatic stress - gs = sgm * (0.0 - head_pc.CHD_L01.values) + sgs * ( - head_pc.CHD_L01.values - botm[-1] - ) + # get initial simulated compaction + cstart = df_sim.simulated[ix0] - # calculate hydrostatic stress for subplot c - u = head_pc.CHD_L13.values - botm[-1] + # cut off initial portion of simulated compaction + df_sim_pc = df_sim[ix0:].copy() - # calculate effective stress - es_obs = gs - u + # reset initial compaction to 0. + df_sim_pc.simulated -= cstart - # set up indices for date text for plot c - locs = [f"{yr:04d}-10-01 12:00:00" for yr in range(1992, 2006)] - locs += [f"{yr:04d}-04-01 12:00:00" for yr in range(1993, 2007)] - locs += ["2006-09-04 12:00:00"] + # reset simulated so maximum compaction is the same + offset = df_pc.observed.values.max() - df_sim_pc.simulated.values[-1] + df_sim.simulated += offset - ixs = [head_pc.index.get_loc(loc) for loc in locs] - # -- subplot c ----------------------------------------------------------- + # interpolate subsidence observations to the simulation index for subplot c + df_iobs_pc = dataframe_interp(df_pc, df_sim_pc.index) - ctext = "Calibration period\n{} to {}".format( - xc0.strftime("%B %d, %Y"), xc1.strftime("%B %d, %Y") - ) + # truncate head to start of observations + head_pc = dataframe_interp(df_obs_heads, df_sim_pc.index) - fig, axes = plt.subplots(nrows=3, ncols=1, figsize=(6.8, 6.8)) + # calculate geostatic stress + gs = sgm * (0.0 - head_pc.CHD_L01.values) + sgs * ( + head_pc.CHD_L01.values - botm[-1] + ) - # -- plot a -------------------------------------------------------------- - ax = axes.flat[0] - ax.set_xlim(xf0, xf1) - ax.plot([xf0, xf1], [0, 0], lw=0.5, color="0.5") - ax.plot( - [ - xf0, - ], - [ - -10, - ], - marker=".", - ms=1, - lw=0, - color="red", - label="Holly site (8N/10W-1Q)", - ) - for idx, key in enumerate(pcomp): - if key == "TOTAL": - key = "simulated" - color = ccolors[idx] - label = clabels[idx] + # calculate hydrostatic stress for subplot c + u = head_pc.CHD_L13.values - botm[-1] + + # calculate effective stress + es_obs = gs - u + + # set up indices for date text for plot c + locs = [f"{yr:04d}-10-01 12:00:00" for yr in range(1992, 2006)] + locs += [f"{yr:04d}-04-01 12:00:00" for yr in range(1993, 2007)] + locs += ["2006-09-04 12:00:00"] + + ixs = [head_pc.index.get_loc(loc) for loc in locs] + # -- subplot c ----------------------------------------------------------- + + ctext = "Calibration period\n{} to {}".format( + xc0.strftime("%B %d, %Y"), xc1.strftime("%B %d, %Y") + ) + + fig, axes = plt.subplots(nrows=3, ncols=1, figsize=(6.8, 6.8)) + + # -- plot a -------------------------------------------------------------- + ax = axes.flat[0] + ax.set_xlim(xf0, xf1) + ax.plot([xf0, xf1], [0, 0], lw=0.5, color="0.5") + ax.plot( + [ + xf0, + ], + [ + -10, + ], + marker=".", + ms=1, + lw=0, + color="red", + label="Holly site (8N/10W-1Q)", + ) + for idx, key in enumerate(pcomp): + if key == "TOTAL": + key = "simulated" + color = ccolors[idx] + label = clabels[idx] + ax.plot( + df_sim.index.values, + df_sim[key].values, + color=color, + label=label, + lw=0.75, + ) + ax.plot(ix, iy, lw=1.0, color="red", zorder=200) + styles.add_text(ax=ax, text="B", x=xf0s, y=1.14, transform=False) + + ax.set_ylim(1.5, -0.1) + ax.xaxis.set_ticks(df_xticks) + ax.xaxis.set_major_formatter(mpl.dates.DateFormatter("%m/%d/%Y")) + + ax.set_ylabel(f"Compaction, in {length_units}") + ax.set_xlabel("Year") + + styles.graph_legend(ax=ax, frameon=False) + styles.heading(ax, letter="A") + styles.remove_edge_ticks(ax=ax) + + # -- plot b -------------------------------------------------------------- + ax = axes.flat[1] + ax.set_xlim(xf0s, xf1s) + ax.set_ylim(1.45, 1.15) + ax.plot( + df.index.values, + df["observed"].values, + marker=".", + ms=1, + lw=0, + color="red", + ) ax.plot( df_sim.index.values, - df_sim[key].values, - color=color, + df_sim["simulated"].values, + color="black", label=label, lw=0.75, ) - ax.plot(ix, iy, lw=1.0, color="red", zorder=200) - fs.add_text(ax=ax, text="B", x=xf0s, y=1.14, transform=False) - - ax.set_ylim(1.5, -0.1) - ax.xaxis.set_ticks(df_xticks) - ax.xaxis.set_major_formatter(mpl.dates.DateFormatter("%m/%d/%Y")) - - ax.set_ylabel(f"Compaction, in {length_units}") - ax.set_xlabel("Year") - - fs.graph_legend(ax=ax, frameon=False) - fs.heading(ax, letter="A") - fs.remove_edge_ticks(ax=ax) - - # -- plot b -------------------------------------------------------------- - ax = axes.flat[1] - ax.set_xlim(xf0s, xf1s) - ax.set_ylim(1.45, 1.15) - ax.plot( - df.index.values, - df["observed"].values, - marker=".", - ms=1, - lw=0, - color="red", - ) - ax.plot( - df_sim.index.values, - df_sim["simulated"].values, - color="black", - label=label, - lw=0.75, - ) - - # plot lines for calibration - ax.plot([xc0, xc0], [1.45, 1.15], color="black", lw=0.5, ls=":") - ax.plot([xc1, xc1], [1.45, 1.15], color="black", lw=0.5, ls=":") - fs.add_annotation( - ax=ax, - text=ctext, - italic=False, - bold=False, - xy=(xc0 - o, 1.2), - xytext=(xca, 1.2), - ha="center", - va="center", - arrowprops=dict(arrowstyle="-|>", fc="black", lw=0.5), - color="none", - bbox=dict(boxstyle="square,pad=-0.07", fc="none", ec="none"), - ) - fs.add_annotation( - ax=ax, - text=ctext, - italic=False, - bold=False, - xy=(xc1 + o, 1.2), - xytext=(xca, 1.2), - ha="center", - va="center", - arrowprops=dict(arrowstyle="-|>", fc="black", lw=0.5), - bbox=dict(boxstyle="square,pad=-0.07", fc="none", ec="none"), - ) - - ax.yaxis.set_ticks(np.linspace(1.15, 1.45, 7)) - ax.xaxis.set_ticks(df_xticks1) - - ax.xaxis.set_major_locator(mpl.dates.YearLocator()) - ax.xaxis.set_minor_locator(mpl.dates.YearLocator(month=6, day=15)) - - ax.xaxis.set_major_formatter(mpl.ticker.NullFormatter()) - ax.xaxis.set_minor_formatter(mpl.dates.DateFormatter("%Y")) - ax.tick_params(axis="x", which="minor", length=0) - - ax.set_ylabel(f"Compaction, in {length_units}") - ax.set_xlabel("Year") - fs.heading(ax, letter="B") - fs.remove_edge_ticks(ax=ax) - - # -- plot c -------------------------------------------------------------- - ax = axes.flat[2] - ax.set_xlim(-0.01, 0.2) - ax.set_ylim(368, 376) - - ax.plot( - df_iobs_pc.observed.values, - es_obs, - marker=".", - ms=1, - color="red", - lw=0, - label="Holly site (8N/10W-1Q)", - ) - ax.plot( - df_sim_pc.simulated.values, - df_sim_pc.ES14.values, - color="black", - lw=0.75, - label="Simulated", - ) - for idx, ixc in enumerate(ixs): - text = "{}".format(df_iobs_pc.index[ixc].strftime("%m/%d/%Y")) - if df_iobs_pc.index[ixc].month == 4: - dxc = -0.001 - dyc = -1 - elif df_iobs_pc.index[ixc].month == 9: - dxc = 0.002 - dyc = 0.75 - else: - dxc = 0.001 - dyc = 1 - xc = df_iobs_pc.observed[ixc] - yc = es_obs[ixc] - fs.add_annotation( + # plot lines for calibration + ax.plot([xc0, xc0], [1.45, 1.15], color="black", lw=0.5, ls=":") + ax.plot([xc1, xc1], [1.45, 1.15], color="black", lw=0.5, ls=":") + styles.add_annotation( ax=ax, - text=text, + text=ctext, italic=False, bold=False, - xy=(xc, yc), - xytext=(xc + dxc, yc + dyc), + xy=(xc0 - o, 1.2), + xytext=(xca, 1.2), ha="center", va="center", - fontsize=7, - arrowprops=dict(arrowstyle="-", color="red", fc="red", lw=0.5), - bbox=dict(boxstyle="square,pad=-0.15", fc="none", ec="none"), + arrowprops=dict(arrowstyle="-|>", fc="black", lw=0.5), + color="none", + bbox=dict(boxstyle="square,pad=-0.07", fc="none", ec="none"), ) - - xtext = "Total compaction since {}, in {}".format( - df_sim_pc.index[0].strftime("%B %d, %Y"), length_units - ) - ytext = ( - "Effective stress at the bottom of\nthe lower aquifer, in " - + f"{length_units} of water" - ) - ax.set_xlabel(xtext) - ax.set_ylabel(ytext) - fs.heading(ax, letter="C") - fs.remove_edge_ticks(ax=ax) - fs.remove_edge_ticks(ax) - - # finalize figure - fig.tight_layout(pad=0.01) - - # save figure - if config.plotSave: - fpth = os.path.join( - "..", "figures", f"{sim_name}-03{config.figure_ext}" + styles.add_annotation( + ax=ax, + text=ctext, + italic=False, + bold=False, + xy=(xc1 + o, 1.2), + xytext=(xca, 1.2), + ha="center", + va="center", + arrowprops=dict(arrowstyle="-|>", fc="black", lw=0.5), + bbox=dict(boxstyle="square,pad=-0.07", fc="none", ec="none"), ) - if not silent: - print(f"saving...'{fpth}'") - fig.savefig(fpth) + ax.yaxis.set_ticks(np.linspace(1.15, 1.45, 7)) + ax.xaxis.set_ticks(df_xticks1) -# Function to plot vertical head profiles + ax.xaxis.set_major_locator(mpl.dates.YearLocator()) + ax.xaxis.set_minor_locator(mpl.dates.YearLocator(month=6, day=15)) + ax.xaxis.set_major_formatter(mpl.ticker.NullFormatter()) + ax.xaxis.set_minor_formatter(mpl.dates.DateFormatter("%Y")) + ax.tick_params(axis="x", which="minor", length=0) -def plot_vertical_head(silent=True): - verbose = not silent - fs = USGSFigure(figure_type="graph", verbose=verbose) + ax.set_ylabel(f"Compaction, in {length_units}") + ax.set_xlabel("Year") + styles.heading(ax, letter="B") + styles.remove_edge_ticks(ax=ax) - name = list(parameters.keys())[1] - pth = os.path.join(ws, name, f"{name}.gwf.obs.csv") - df_heads, col_list = process_sim_csv( - pth, origin_str="1908-05-09 00:00:00.000000" - ) - df_heads_year = df_heads.groupby(df_heads.index.year).mean() - - def get_colors(vmax=6): - # set color - cmap = plt.get_cmap("viridis") - cNorm = mpl.colors.Normalize(vmin=0, vmax=vmax) - scalarMap = mpl.cm.ScalarMappable(norm=cNorm, cmap=cmap) - colors = [] - for ic in range(vmax): - colors.append(scalarMap.to_rgba(ic)) - return colors - - def build_head_data(df, year=1908): - dfr = df.loc[df.index == year] - xlabel = None - x = [] - y = [] - for k in range(14): - tag = f"HD{k + 1:02d}" - h = dfr[tag].values[0] - if k == 0: - z0 = -25.0 - xlabel = -1.0 * h - else: - z0 = zelevs[k] - z1 = zelevs[k + 1] - h *= -1.0 - x += [h, h] - y += [-z0, -z1] - return xlabel, x, y - - iyears = (1908, 1916, 1926, 1936, 1946, 1956, 1966, 1976, 1986, 1996, 2006) - colors = get_colors(vmax=len(iyears) - 1) - - xrange = (-10, 50) - fig, ax = plt.subplots( - nrows=1, ncols=1, sharey=True, figsize=(0.75 * 6.8, 4.0) - ) + # -- plot c -------------------------------------------------------------- + ax = axes.flat[2] + ax.set_xlim(-0.01, 0.2) + ax.set_ylim(368, 376) - ax.set_xlim(xrange) - ax.set_ylim(-botm[-1], 0) + ax.plot( + df_iobs_pc.observed.values, + es_obs, + marker=".", + ms=1, + color="red", + lw=0, + label="Holly site (8N/10W-1Q)", + ) + ax.plot( + df_sim_pc.simulated.values, + df_sim_pc.ES14.values, + color="black", + lw=0.75, + label="Simulated", + ) - for z in botm: - ax.axhline(y=-z, xmin=-30, xmax=160, lw=0.5, color="0.5") + for idx, ixc in enumerate(ixs): + text = "{}".format(df_iobs_pc.index[ixc].strftime("%m/%d/%Y")) + if df_iobs_pc.index[ixc].month == 4: + dxc = -0.001 + dyc = -1 + elif df_iobs_pc.index[ixc].month == 9: + dxc = 0.002 + dyc = 0.75 + else: + dxc = 0.001 + dyc = 1 + xc = df_iobs_pc.observed[ixc] + yc = es_obs[ixc] + styles.add_annotation( + ax=ax, + text=text, + italic=False, + bold=False, + xy=(xc, yc), + xytext=(xc + dxc, yc + dyc), + ha="center", + va="center", + fontsize=7, + arrowprops=dict(arrowstyle="-", color="red", fc="red", lw=0.5), + bbox=dict(boxstyle="square,pad=-0.15", fc="none", ec="none"), + ) - # add confining units - label = "" - for k in (1, 2, 3): - label = set_label(label, text="Confining unit") - ax.fill_between( - xrange, edges[k], y2=edges[k + 1], color="brown", lw=0, label=label + xtext = "Total compaction since {}, in {}".format( + df_sim_pc.index[0].strftime("%B %d, %Y"), length_units ) - ypos = -0.5 * (zelevs[2] + zelevs[3]) - ax.text( - 40, - ypos, - "Confining unit", - ha="left", - va="center", - size=8, - color="white", - ) + ytext = ( + "Effective stress at the bottom of\nthe lower aquifer, in " + + f"{length_units} of water" + ) + ax.set_xlabel(xtext) + ax.set_ylabel(ytext) + styles.heading(ax, letter="C") + styles.remove_edge_ticks(ax=ax) + styles.remove_edge_ticks(ax) - label = "" - for k in (7, 8, 9): - label = set_label(label, text="Thick aquitard") - ax.fill_between( - xrange, edges[k], y2=edges[k + 1], color="tan", lw=0, label=label + # finalize figure + fig.tight_layout(pad=0.01) + + if plot_show: + plt.show() + if plot_save: + fpth = os.path.join("..", "figures", f"{sim_name}-03.png") + if not silent: + print(f"saving...'{fpth}'") + fig.savefig(fpth) + + +def plot_vertical_head(silent=True): + with styles.USGSPlot() as fs: + name = list(parameters.keys())[1] + pth = os.path.join(workspace, name, f"{name}.gwf.obs.csv") + df_heads, col_list = process_sim_csv( + pth, origin_str="1908-05-09 00:00:00.000000" ) - ypos = -0.5 * (zelevs[8] + zelevs[9]) - ax.text( - 40, - ypos, - "Thick aquitard", - ha="left", - va="center", - size=8, - color="white", - ) + df_heads_year = df_heads.groupby(df_heads.index.year).mean() + + def get_colors(vmax=6): + # set color + cmap = plt.get_cmap("viridis") + cNorm = mpl.colors.Normalize(vmin=0, vmax=vmax) + scalarMap = mpl.cm.ScalarMappable(norm=cNorm, cmap=cmap) + colors = [] + for ic in range(vmax): + colors.append(scalarMap.to_rgba(ic)) + return colors + + def build_head_data(df, year=1908): + dfr = df.loc[df.index == year] + xlabel = None + x = [] + y = [] + for k in range(14): + tag = f"HD{k + 1:02d}" + h = dfr[tag].values[0] + if k == 0: + z0 = -25.0 + xlabel = -1.0 * h + else: + z0 = zelevs[k] + z1 = zelevs[k + 1] + h *= -1.0 + x += [h, h] + y += [-z0, -z1] + return xlabel, x, y + + iyears = (1908, 1916, 1926, 1936, 1946, 1956, 1966, 1976, 1986, 1996, 2006) + colors = get_colors(vmax=len(iyears) - 1) + + xrange = (-10, 50) + fig, ax = plt.subplots(nrows=1, ncols=1, sharey=True, figsize=(0.75 * 6.8, 4.0)) + + ax.set_xlim(xrange) + ax.set_ylim(-botm[-1], 0) - zo = 105 - for idx, iyear in enumerate(iyears[:-1]): - xlabel, x, y = build_head_data(df_heads_year, year=iyear) - xlabel1, x1, y1 = build_head_data(df_heads_year, year=iyears[idx + 1]) - ax.fill_betweenx( - y, x, x2=x1, color=colors[idx], zorder=zo, step="mid", lw=0 + for z in botm: + ax.axhline(y=-z, xmin=-30, xmax=160, lw=0.5, color="0.5") + + # add confining units + label = "" + for k in (1, 2, 3): + label = set_label(label, text="Confining unit") + ax.fill_between( + xrange, edges[k], y2=edges[k + 1], color="brown", lw=0, label=label + ) + ypos = -0.5 * (zelevs[2] + zelevs[3]) + ax.text( + 40, + ypos, + "Confining unit", + ha="left", + va="center", + size=8, + color="white", ) - ax.plot(x, y, lw=0.5, color="black", zorder=201) + + label = "" + for k in (7, 8, 9): + label = set_label(label, text="Thick aquitard") + ax.fill_between( + xrange, edges[k], y2=edges[k + 1], color="tan", lw=0, label=label + ) + ypos = -0.5 * (zelevs[8] + zelevs[9]) ax.text( - xlabel, - 24, - f"{iyear}", - ha="center", - va="bottom", - rotation=90, - size=7, + 40, + ypos, + "Thick aquitard", + ha="left", + va="center", + size=8, + color="white", ) - if iyear == 1996: - ax.plot(x1, y1, lw=0.5, color="black", zorder=zo) + + zo = 105 + for idx, iyear in enumerate(iyears[:-1]): + xlabel, x, y = build_head_data(df_heads_year, year=iyear) + xlabel1, x1, y1 = build_head_data(df_heads_year, year=iyears[idx + 1]) + ax.fill_betweenx( + y, x, x2=x1, color=colors[idx], zorder=zo, step="mid", lw=0 + ) + ax.plot(x, y, lw=0.5, color="black", zorder=201) ax.text( - xlabel1, + xlabel, 24, - f"{iyears[idx + 1]}", + f"{iyear}", ha="center", va="bottom", rotation=90, size=7, ) - zo += 1 - - # add layer labels - for k in llabels: - print_label(ax, edges, k) + if iyear == 1996: + ax.plot(x1, y1, lw=0.5, color="black", zorder=zo) + ax.text( + xlabel1, + 24, + f"{iyears[idx + 1]}", + ha="center", + va="bottom", + rotation=90, + size=7, + ) + zo += 1 - constant_heads(ax, annotate=True, xrange=xrange) + # add layer labels + for k in llabels: + print_label(ax, edges, k) - ax.set_xlabel("Depth to water, in meters below land surface") - ax.set_ylabel("Depth below land surface, in meters") + constant_heads(ax, annotate=True, xrange=xrange) - fs.remove_edge_ticks(ax) + ax.set_xlabel("Depth to water, in meters below land surface") + ax.set_ylabel("Depth below land surface, in meters") - fig.tight_layout(pad=0.5) + styles.remove_edge_ticks(ax) - # save figure - if config.plotSave: - fpth = os.path.join( - "..", "figures", f"{sim_name}-04{config.figure_ext}" - ) - if not silent: - print(f"saving...'{fpth}'") - fig.savefig(fpth) + fig.tight_layout(pad=0.5) - -# Function to plot the model results. + if plot_show: + plt.show() + if plot_save: + fpth = os.path.join("..", "figures", f"{sim_name}-04.png") + if not silent: + print(f"saving...'{fpth}'") + fig.savefig(fpth) def plot_results(silent=True): - if config.plotModel: - plot_grid(silent=silent) - plot_boundary_heads(silent=silent) - plot_head_es_comparison(silent=silent) - plot_calibration(silent=silent) - plot_vertical_head() + if not plot: + return + plot_grid(silent=silent) + plot_boundary_heads(silent=silent) + plot_head_es_comparison(silent=silent) + plot_calibration(silent=silent) + plot_vertical_head() -# Function that wraps all of the steps for the model -# -# 1. build_model, -# 2. write_model, -# 3. run_model, and -# 4. plot_results. +# - + +# ### Running the example # +# Define and invoke a function to run the example scenarios, then plot results. -def simulation(idx, silent=True): +# + +def scenario(idx, silent=True): key = list(parameters.keys())[idx] params = parameters[key].copy() + sim = build_models(key, **params) + if write: + write_models(sim, silent=silent) + if run: + run_models(sim, silent=silent) - sim = build_model(key, **params) - - write_model(sim, silent=silent) - - success = run_model(sim, silent=silent) +# - -# nosetest - exclude block from this nosetest to the next nosetest -def test_and_plot(): - simulation(0, silent=False) - simulation(1, silent=False) - plot_results(silent=False) - export_tables(silent=False) +# Run the head based solution. -# nosetest end +scenario(0) -if __name__ == "__main__": - # ### One-dimensional compaction - # - # #### Head based solution +# Run the effective stress solution. - simulation(0) +scenario(1) - # #### Effective stress solution - - simulation(1) - - # #### Plot results +# Plot results and export tables. +# + +if plot: plot_results() - - # #### Export tables - export_tables() +# - diff --git a/scripts/ex-gwf-csub-p04.py b/scripts/ex-gwf-csub-p04.py index c03c9b46b..b4bd657e1 100644 --- a/scripts/ex-gwf-csub-p04.py +++ b/scripts/ex-gwf-csub-p04.py @@ -1,61 +1,48 @@ -# ## One-dimensional compaction in a three-dimensional flow field +# ## One-dimensional compaction in a three-dimensional flow field example # # This problem is based on the problem presented in the SUB-WT report # (Leake and Galloway, 2007) and represent groundwater development in a # hypothetical aquifer that includes some features typical of basin-fill # aquifers in an arid or semi-arid environment. -# -# ### Problem Setup +# ### Initial setup # -# Imports +# Import dependencies, define the example name and workspace, and read settings from environment variables. +# + import os -import sys +import pathlib as pl import flopy import matplotlib as mpl import matplotlib.pyplot as plt import numpy as np +import pooch +from flopy.plot.styles import styles +from modflow_devtools.misc import get_env, timed -# Append to system path to include the common subdirectory - -sys.path.append(os.path.join("..", "common")) - -# import common functionality - -import config -from figspecs import USGSFigure - -# Set figure properties specific to the problem - -figure_size = (6.8, 5.5) -arrow_props = dict(facecolor="black", arrowstyle="-", lw=0.5) -plot_tags = ( - "W1L", - "W2L", - "S1L", - "S2L", - "C1L", - "C2L", -) -compaction_heading = ("row 9, column 10", "row 12, column 7") - -# Base simulation and model name and workspace - -ws = config.base_ws +# Example name and base workspace +sim_name = "ex-gwf-csub-p04" +workspace = pl.Path("../examples") -# Simulation name +# Settings from environment variables +write = get_env("WRITE", True) +run = get_env("RUN", True) +plot = get_env("PLOT", True) +plot_show = get_env("PLOT_SHOW", True) +plot_save = get_env("PLOT_SAVE", True) +# - -sim_name = "ex-gwf-csub-p04" +# ### Define parameters +# +# Define model units, parameters and other settings. +# + # Model units - length_units = "meters" time_units = "days" -# Table - +# Model parameters nper = 3 # Number of periods nlay = 4 # Number of layers nrow = 20 # Number of rows @@ -71,15 +58,9 @@ sy_str = "0.3, 0.3, 0.4, 0.3" # Specific yield (unitless) gammaw = 9806.65 # Compressibility of water (Newtons/($m^3$) beta = 4.6612e-10 # Specific gravity of water (1/$Pa$) -sgm_str = ( - "1.77, 1.77, 1.60, 1.77" # Specific gravity of moist soils (unitless) -) -sgs_str = ( - "2.06, 2.05, 1.94, 2.06" # Specific gravity of saturated soils (unitless) -) -cg_theta_str = ( - "0.32, 0.32, 0.45, 0.32" # Coarse-grained material porosity (unitless) -) +sgm_str = "1.77, 1.77, 1.60, 1.77" # Specific gravity of moist soils (unitless) +sgs_str = "2.06, 2.05, 1.94, 2.06" # Specific gravity of saturated soils (unitless) +cg_theta_str = "0.32, 0.32, 0.45, 0.32" # Coarse-grained material porosity (unitless) cg_ske_str = "0.005, 0.005, 0.01, 0.005" # Elastic specific storage ($1/m$) ib_thick_str = "45., 70., 50., 90." # Interbed thickness ($m$) ib_theta = 0.45 # Interbed initial porosity (unitless) @@ -88,15 +69,13 @@ stress_offset = 15.0 # Initial preconsolidation stress offset ($m$) # Static temporal data used by TDIS file - tdis_ds = ( (0.0, 1, 1.0), (21915.0, 60, 1.0), (21915.0, 60, 1.0), ) -# parse parameter strings into tuples - +# Parse parameter strings into tuples botm = [float(value) for value in botm_str.split(",")] icelltype = [int(value) for value in icelltype_str.split(",")] k11 = [float(value) for value in k11_str.split(",")] @@ -109,13 +88,14 @@ ib_thick = [float(value) for value in ib_thick_str.split(",")] # Load active domain and create idomain array - -pth = os.path.join("..", "data", sim_name, "idomain.txt") +pth = pooch.retrieve( + url=f"https://github.com/MODFLOW-USGS/modflow6-examples/raw/master/data/{sim_name}/idomain.txt", + known_hash="md5:2f05a27b6f71e564c0d3616e3fd00ac8", +) ib = np.loadtxt(pth, dtype=int) idomain = np.tile(ib, (nlay, 1)) # Constant head boundary cells - chd_locs = [(nrow - 1, 7), (nrow - 1, 8)] c6 = [] for i, j in chd_locs: @@ -123,7 +103,6 @@ c6.append([k, i, j, strt]) # Recharge boundary cells - rch_rate = 5.5e-4 rch6 = [] for i in range(nrow): @@ -133,7 +112,6 @@ rch6.append([0, i, j, rch_rate]) # Well boundary cells - well_locs = ( (1, 8, 9), (3, 11, 6), @@ -150,7 +128,6 @@ wel6[idx + 1] = spd # Create interbed package data - icsubno = 0 csub_pakdata = [] for i in range(nrow): @@ -177,205 +154,203 @@ icsubno += 1 # Solver parameters - nouter = 100 ninner = 300 hclose = 1e-9 rclose = 1e-6 linaccel = "bicgstab" relax = 0.97 +# - - -# ### Functions to build, write, run, and plot the model +# ### Model setup # -# MODFLOW 6 flopy simulation object (sim) is returned if building the model - +# Define functions to build models, write input files, and run the simulation. + + +# + +def build_models(): + sim_ws = os.path.join(workspace, sim_name) + sim = flopy.mf6.MFSimulation(sim_name=sim_name, sim_ws=sim_ws, exe_name="mf6") + flopy.mf6.ModflowTdis(sim, nper=nper, perioddata=tdis_ds, time_units=time_units) + flopy.mf6.ModflowIms( + sim, + outer_maximum=nouter, + outer_dvclose=hclose, + linear_acceleration=linaccel, + inner_maximum=ninner, + inner_dvclose=hclose, + relaxation_factor=relax, + rcloserecord=f"{rclose} strict", + ) + gwf = flopy.mf6.ModflowGwf( + sim, modelname=sim_name, save_flows=True, newtonoptions="newton" + ) + flopy.mf6.ModflowGwfdis( + gwf, + length_units=length_units, + nlay=nlay, + nrow=nrow, + ncol=ncol, + delr=delr, + delc=delc, + top=top, + botm=botm, + idomain=idomain, + ) + # gwf obs + flopy.mf6.ModflowUtlobs( + gwf, + digits=10, + print_input=True, + continuous={ + "gwf_obs.csv": [ + ("h1l1", "HEAD", (0, 8, 9)), + ("h1l2", "HEAD", (1, 8, 9)), + ("h1l3", "HEAD", (2, 8, 9)), + ("h1l4", "HEAD", (3, 8, 9)), + ("h2l1", "HEAD", (0, 11, 6)), + ("h2l2", "HEAD", (1, 11, 6)), + ("h3l2", "HEAD", (2, 11, 6)), + ("h4l2", "HEAD", (3, 11, 6)), + ] + }, + ) -def build_model(): - if config.buildModel: - sim_ws = os.path.join(ws, sim_name) - sim = flopy.mf6.MFSimulation( - sim_name=sim_name, sim_ws=sim_ws, exe_name="mf6" - ) - flopy.mf6.ModflowTdis( - sim, nper=nper, perioddata=tdis_ds, time_units=time_units - ) - flopy.mf6.ModflowIms( - sim, - outer_maximum=nouter, - outer_dvclose=hclose, - linear_acceleration=linaccel, - inner_maximum=ninner, - inner_dvclose=hclose, - relaxation_factor=relax, - rcloserecord=f"{rclose} strict", - ) - gwf = flopy.mf6.ModflowGwf( - sim, modelname=sim_name, save_flows=True, newtonoptions="newton" - ) - flopy.mf6.ModflowGwfdis( - gwf, - length_units=length_units, - nlay=nlay, - nrow=nrow, - ncol=ncol, - delr=delr, - delc=delc, - top=top, - botm=botm, - idomain=idomain, - ) - # gwf obs - flopy.mf6.ModflowUtlobs( - gwf, - digits=10, - print_input=True, - continuous={ - "gwf_obs.csv": [ - ("h1l1", "HEAD", (0, 8, 9)), - ("h1l2", "HEAD", (1, 8, 9)), - ("h1l3", "HEAD", (2, 8, 9)), - ("h1l4", "HEAD", (3, 8, 9)), - ("h2l1", "HEAD", (0, 11, 6)), - ("h2l2", "HEAD", (1, 11, 6)), - ("h3l2", "HEAD", (2, 11, 6)), - ("h4l2", "HEAD", (3, 11, 6)), - ] - }, - ) + flopy.mf6.ModflowGwfic(gwf, strt=strt) + flopy.mf6.ModflowGwfnpf( + gwf, + icelltype=icelltype, + k=k11, + save_specific_discharge=True, + ) + flopy.mf6.ModflowGwfsto( + gwf, + iconvert=icelltype, + ss=0.0, + sy=sy, + steady_state={0: True}, + transient={1: True}, + ) + csub = flopy.mf6.ModflowGwfcsub( + gwf, + print_input=True, + save_flows=True, + compression_indices=True, + update_material_properties=True, + boundnames=True, + ninterbeds=len(csub_pakdata), + sgm=sgm, + sgs=sgs, + cg_theta=cg_theta, + cg_ske_cr=cg_ske, + beta=beta, + gammaw=gammaw, + packagedata=csub_pakdata, + ) + opth = f"{sim_name}.csub.obs" + csub_csv = opth + ".csv" + obs = [ + ("w1l1", "interbed-compaction", "01_09_10"), + ("w1l2", "interbed-compaction", "02_09_10"), + ("w1l3", "interbed-compaction", "03_09_10"), + ("w1l4", "interbed-compaction", "04_09_10"), + ("w2l1", "interbed-compaction", "01_12_07"), + ("w2l2", "interbed-compaction", "02_12_07"), + ("w2l3", "interbed-compaction", "03_12_07"), + ("w2l4", "interbed-compaction", "04_12_07"), + ("s1l1", "coarse-compaction", (0, 8, 9)), + ("s1l2", "coarse-compaction", (1, 8, 9)), + ("s1l3", "coarse-compaction", (2, 8, 9)), + ("s1l4", "coarse-compaction", (3, 8, 9)), + ("s2l1", "coarse-compaction", (0, 11, 6)), + ("s2l2", "coarse-compaction", (1, 11, 6)), + ("s2l3", "coarse-compaction", (2, 11, 6)), + ("s2l4", "coarse-compaction", (3, 11, 6)), + ("c1l1", "compaction-cell", (0, 8, 9)), + ("c1l2", "compaction-cell", (1, 8, 9)), + ("c1l3", "compaction-cell", (2, 8, 9)), + ("c1l4", "compaction-cell", (3, 8, 9)), + ("c2l1", "compaction-cell", (0, 11, 6)), + ("c2l2", "compaction-cell", (1, 11, 6)), + ("c2l3", "compaction-cell", (2, 11, 6)), + ("c2l4", "compaction-cell", (3, 11, 6)), + ("w2l4q", "csub-cell", (3, 11, 6)), + ("gs1", "gstress-cell", (0, 8, 9)), + ("es1", "estress-cell", (0, 8, 9)), + ("pc1", "preconstress-cell", (0, 8, 9)), + ("gs2", "gstress-cell", (1, 8, 9)), + ("es2", "estress-cell", (1, 8, 9)), + ("pc2", "preconstress-cell", (1, 8, 9)), + ("gs3", "gstress-cell", (2, 8, 9)), + ("es3", "estress-cell", (2, 8, 9)), + ("pc3", "preconstress-cell", (2, 8, 9)), + ("gs4", "gstress-cell", (3, 8, 9)), + ("es4", "estress-cell", (3, 8, 9)), + ("pc4", "preconstress-cell", (3, 8, 9)), + ("sk1l2", "ske-cell", (1, 8, 9)), + ("sk2l4", "ske-cell", (3, 11, 6)), + ("t1l2", "theta", "02_09_10"), + ("w1qie", "elastic-csub", "02_09_10"), + ("w1qii", "inelastic-csub", "02_09_10"), + ("w1qaq", "coarse-csub", (1, 8, 9)), + ("w1qt", "csub-cell", (1, 8, 9)), + ("w1wc", "wcomp-csub-cell", (1, 8, 9)), + ("w2qie", "elastic-csub", "04_12_07"), + ("w2qii", "inelastic-csub", "04_12_07"), + ("w2qaq", "coarse-csub", (3, 11, 6)), + ("w2qt ", "csub-cell", (3, 11, 6)), + ("w2wc", "wcomp-csub-cell", (3, 11, 6)), + ] + orecarray = {csub_csv: obs} + csub.obs.initialize( + filename=opth, digits=10, print_input=True, continuous=orecarray + ) - flopy.mf6.ModflowGwfic(gwf, strt=strt) - flopy.mf6.ModflowGwfnpf( - gwf, - icelltype=icelltype, - k=k11, - save_specific_discharge=True, - ) - flopy.mf6.ModflowGwfsto( - gwf, - iconvert=icelltype, - ss=0.0, - sy=sy, - steady_state={0: True}, - transient={1: True}, - ) - csub = flopy.mf6.ModflowGwfcsub( - gwf, - print_input=True, - save_flows=True, - compression_indices=True, - update_material_properties=True, - boundnames=True, - ninterbeds=len(csub_pakdata), - sgm=sgm, - sgs=sgs, - cg_theta=cg_theta, - cg_ske_cr=cg_ske, - beta=beta, - gammaw=gammaw, - packagedata=csub_pakdata, - ) - opth = f"{sim_name}.csub.obs" - csub_csv = opth + ".csv" - obs = [ - ("w1l1", "interbed-compaction", "01_09_10"), - ("w1l2", "interbed-compaction", "02_09_10"), - ("w1l3", "interbed-compaction", "03_09_10"), - ("w1l4", "interbed-compaction", "04_09_10"), - ("w2l1", "interbed-compaction", "01_12_07"), - ("w2l2", "interbed-compaction", "02_12_07"), - ("w2l3", "interbed-compaction", "03_12_07"), - ("w2l4", "interbed-compaction", "04_12_07"), - ("s1l1", "coarse-compaction", (0, 8, 9)), - ("s1l2", "coarse-compaction", (1, 8, 9)), - ("s1l3", "coarse-compaction", (2, 8, 9)), - ("s1l4", "coarse-compaction", (3, 8, 9)), - ("s2l1", "coarse-compaction", (0, 11, 6)), - ("s2l2", "coarse-compaction", (1, 11, 6)), - ("s2l3", "coarse-compaction", (2, 11, 6)), - ("s2l4", "coarse-compaction", (3, 11, 6)), - ("c1l1", "compaction-cell", (0, 8, 9)), - ("c1l2", "compaction-cell", (1, 8, 9)), - ("c1l3", "compaction-cell", (2, 8, 9)), - ("c1l4", "compaction-cell", (3, 8, 9)), - ("c2l1", "compaction-cell", (0, 11, 6)), - ("c2l2", "compaction-cell", (1, 11, 6)), - ("c2l3", "compaction-cell", (2, 11, 6)), - ("c2l4", "compaction-cell", (3, 11, 6)), - ("w2l4q", "csub-cell", (3, 11, 6)), - ("gs1", "gstress-cell", (0, 8, 9)), - ("es1", "estress-cell", (0, 8, 9)), - ("pc1", "preconstress-cell", (0, 8, 9)), - ("gs2", "gstress-cell", (1, 8, 9)), - ("es2", "estress-cell", (1, 8, 9)), - ("pc2", "preconstress-cell", (1, 8, 9)), - ("gs3", "gstress-cell", (2, 8, 9)), - ("es3", "estress-cell", (2, 8, 9)), - ("pc3", "preconstress-cell", (2, 8, 9)), - ("gs4", "gstress-cell", (3, 8, 9)), - ("es4", "estress-cell", (3, 8, 9)), - ("pc4", "preconstress-cell", (3, 8, 9)), - ("sk1l2", "ske-cell", (1, 8, 9)), - ("sk2l4", "ske-cell", (3, 11, 6)), - ("t1l2", "theta", "02_09_10"), - ("w1qie", "elastic-csub", "02_09_10"), - ("w1qii", "inelastic-csub", "02_09_10"), - ("w1qaq", "coarse-csub", (1, 8, 9)), - ("w1qt", "csub-cell", (1, 8, 9)), - ("w1wc", "wcomp-csub-cell", (1, 8, 9)), - ("w2qie", "elastic-csub", "04_12_07"), - ("w2qii", "inelastic-csub", "04_12_07"), - ("w2qaq", "coarse-csub", (3, 11, 6)), - ("w2qt ", "csub-cell", (3, 11, 6)), - ("w2wc", "wcomp-csub-cell", (3, 11, 6)), - ] - orecarray = {csub_csv: obs} - csub.obs.initialize( - filename=opth, digits=10, print_input=True, continuous=orecarray - ) + flopy.mf6.ModflowGwfchd(gwf, stress_period_data={0: c6}) + flopy.mf6.ModflowGwfrch(gwf, stress_period_data={0: rch6}) + flopy.mf6.ModflowGwfwel(gwf, stress_period_data=wel6) + + head_filerecord = f"{sim_name}.hds" + budget_filerecord = f"{sim_name}.cbc" + flopy.mf6.ModflowGwfoc( + gwf, + head_filerecord=head_filerecord, + budget_filerecord=budget_filerecord, + printrecord=[("BUDGET", "ALL")], + saverecord=[("BUDGET", "ALL"), ("HEAD", "ALL")], + ) + return sim - flopy.mf6.ModflowGwfchd(gwf, stress_period_data={0: c6}) - flopy.mf6.ModflowGwfrch(gwf, stress_period_data={0: rch6}) - flopy.mf6.ModflowGwfwel(gwf, stress_period_data=wel6) - - head_filerecord = f"{sim_name}.hds" - budget_filerecord = f"{sim_name}.cbc" - flopy.mf6.ModflowGwfoc( - gwf, - head_filerecord=head_filerecord, - budget_filerecord=budget_filerecord, - printrecord=[("BUDGET", "ALL")], - saverecord=[("BUDGET", "ALL"), ("HEAD", "ALL")], - ) - return sim - return None +def write_models(sim, silent=True): + sim.write_simulation(silent=silent) -# Function to write MODFLOW 6 model files +@timed +def run_models(sim, silent=True): + success, buff = sim.run_simulation(silent=silent) + assert success, buff -def write_model(sim, silent=True): - if config.writeModel: - sim.write_simulation(silent=silent) +# - -# Function to run the model. -# True is returned if the model runs successfully +# ### Plotting results # +# Define functions to plot model results, starting with a few utilities. - -@config.timeit -def run_model(sim, silent=True): - success = True - if config.runModel: - success, buff = sim.run_simulation(silent=silent) - if not success: - print(buff) - - return success - - -# Function to get csub observations +# + +# Set figure properties specific to the problem +figure_size = (6.8, 5.5) +arrow_props = dict(facecolor="black", arrowstyle="-", lw=0.5) +plot_tags = ( + "W1L", + "W2L", + "S1L", + "S2L", + "C1L", + "C2L", +) +compaction_heading = ("row 9, column 10", "row 12, column 7") def get_csub_observations(sim): @@ -401,10 +376,8 @@ def get_csub_observations(sim): return csub_obs -# Function to calculate the compaction at the surface - - def calc_compaction_at_surface(sim): + """Calculate the compaction at the surface""" csub_obs = get_csub_observations(sim) for tag in plot_tags: for k in ( @@ -418,492 +391,460 @@ def calc_compaction_at_surface(sim): return csub_obs -# Function to plot compaction results - - def plot_compaction_values(ax, sim, tagbase="W1L"): colors = ["#FFF8DC", "#D2B48C", "#CD853F", "#8B4513"][::-1] obs = calc_compaction_at_surface(sim) for k in range(nlay): fc = colors[k] tag = f"{tagbase}{k + 1}" - label = f"Model layer {k + 1}" + label = f"Layer {k + 1}" ax.fill_between(obs["totim"], obs[tag], y2=0, color=fc, label=label) -# Function to plot the model grid - - def plot_grid(sim, silent=True): - verbose = not silent - fs = USGSFigure(figure_type="map", verbose=verbose) - name = sim.name - gwf = sim.get_model(name) - extents = gwf.modelgrid.extent - - # read simulated heads - hobj = gwf.output.head() - h0 = hobj.get_data(kstpkper=(0, 0)) - h1 = hobj.get_data(kstpkper=(59, 1)) - hsxs0 = h0[0, 8, :] - hsxs1 = h1[0, 8, :] - - # get delr array - dx = gwf.dis.delr.array - - # create x-axis for cross-section - hxloc = np.arange(1000, 2000.0 * 15, 2000.0) - - # set cross-section location - y = 2000.0 * 11.5 - xsloc = [(extents[0], extents[1]), (y, y)] - - # well locations - w1loc = (9.5 * 2000.0, 11.75 * 2000.0) - w2loc = (6.5 * 2000.0, 8.5 * 2000.0) - - fig = plt.figure(figsize=(6.8, 5), constrained_layout=True) - gs = mpl.gridspec.GridSpec(7, 10, figure=fig, wspace=5) - plt.axis("off") - - ax = fig.add_subplot(gs[:, 0:6]) - # ax.set_aspect('equal') - mm = flopy.plot.PlotMapView(model=gwf, ax=ax, extent=extents) - mm.plot_grid(lw=0.5, color="0.5") - mm.plot_bc(ftype="WEL", kper=1, plotAll=True) - mm.plot_bc(ftype="CHD", color="blue") - mm.plot_bc(ftype="RCH", color="green") - mm.plot_inactive(color_noflow="0.75") - mm.ax.plot(xsloc[0], xsloc[1], color="orange", lw=1.5) - # contour steady state heads - cl = mm.contour_array( - h0, - masked_values=[1.0e30], - levels=np.arange(115, 200, 5), - colors="black", - linestyles="dotted", - linewidths=0.75, - ) - ax.clabel(cl, fmt="%3i", inline_spacing=0.1) - # well text - fs.add_annotation( - ax=ax, - text="Well 1, layer 2", - bold=False, - italic=False, - xy=w1loc, - xytext=(w1loc[0] - 3200, w1loc[1] + 1500), - ha="right", - va="center", - zorder=100, - arrowprops=arrow_props, - ) - fs.add_annotation( - ax=ax, - text="Well 2, layer 4", - bold=False, - italic=False, - xy=w2loc, - xytext=(w2loc[0] + 3000, w2loc[1]), - ha="left", - va="center", - zorder=100, - arrowprops=arrow_props, - ) - ax.set_ylabel("y-coordinate, in meters") - ax.set_xlabel("x-coordinate, in meters") - fs.heading(ax, letter="A", heading="Map view") - fs.remove_edge_ticks(ax) - - ax = fig.add_subplot(gs[0:5, 6:]) - mm = flopy.plot.PlotCrossSection(model=gwf, ax=ax, line={"row": 8}) - mm.plot_grid(lw=0.5, color="0.5") - # items for legend - mm.ax.plot( - -1000, - -1000, - "s", - ms=5, - color="green", - mec="black", - mew=0.5, - label="Recharge", - ) - mm.ax.plot( - -1000, - -1000, - "s", - ms=5, - color="red", - mec="black", - mew=0.5, - label="Well", - ) - mm.ax.plot( - -1000, - -1000, - "s", - ms=5, - color="blue", - mec="black", - mew=0.5, - label="Constant head", - ) - mm.ax.plot( - -1000, - -1000, - "s", - ms=5, - color="0.75", - mec="black", - mew=0.5, - label="Inactive", - ) - mm.ax.plot( - [-1000, -1001], - [-1000, -1000], - color="orange", - lw=1.5, - label="Cross-section line", - ) - # aquifer coloring - ax.fill_between([0, dx.sum()], y1=150, y2=-100, color="cyan", alpha=0.5) - ax.fill_between( - [0, dx.sum()], y1=-100, y2=-150, color="#D2B48C", alpha=0.5 - ) - ax.fill_between( - [0, dx.sum()], y1=-150, y2=-350, color="#00BFFF", alpha=0.5 - ) - # well coloring - ax.fill_between( - [dx.cumsum()[8], dx.cumsum()[9]], y1=50, y2=-100, color="red", lw=0 - ) - # labels - fs.add_text( - ax=ax, - transform=False, - bold=False, - italic=False, - x=300, - y=-97, - text="Upper aquifer", - va="bottom", - ha="left", - fontsize=9, - ) - fs.add_text( - ax=ax, - transform=False, - bold=False, - italic=False, - x=300, - y=-147, - text="Confining unit", - va="bottom", - ha="left", - fontsize=9, - ) - fs.add_text( - ax=ax, - transform=False, - bold=False, - italic=False, - x=300, - y=-347, - text="Lower aquifer", - va="bottom", - ha="left", - fontsize=9, - ) - fs.add_text( - ax=ax, - transform=False, - bold=False, - italic=False, - x=29850, - y=53, - text="Layer 1", - va="bottom", - ha="right", - fontsize=9, - ) - fs.add_text( - ax=ax, - transform=False, - bold=False, - italic=False, - x=29850, - y=-97, - text="Layer 2", - va="bottom", - ha="right", - fontsize=9, - ) - fs.add_text( - ax=ax, - transform=False, - bold=False, - italic=False, - x=29850, - y=-147, - text="Layer 3", - va="bottom", - ha="right", - fontsize=9, - ) - fs.add_text( - ax=ax, - transform=False, - bold=False, - italic=False, - x=29850, - y=-347, - text="Layer 4", - va="bottom", - ha="right", - fontsize=9, - ) - ax.plot( - hxloc, - hsxs0, - lw=0.75, - color="black", - ls="dotted", - label="Steady-state\nwater level", - ) - ax.plot( - hxloc, - hsxs1, - lw=0.75, - color="black", - ls="dashed", - label="Water-level at the end\nof stress-period 2", - ) - ax.set_ylabel("Elevation, in meters") - ax.set_xlabel("x-coordinate along model row 9, in meters") - fs.graph_legend( - mm.ax, - ncol=2, - bbox_to_anchor=(0.5, -0.6), - borderaxespad=0, - frameon=False, - loc="lower center", - ) - fs.heading(ax, letter="B", heading="Cross-section view") - fs.remove_edge_ticks(ax) - - # save figure - if config.plotSave: - fpth = os.path.join( - "..", "figures", f"{sim_name}-grid{config.figure_ext}" + with styles.USGSMap(): + name = sim.name + gwf = sim.get_model(name) + extents = gwf.modelgrid.extent + + # read simulated heads + hobj = gwf.output.head() + h0 = hobj.get_data(kstpkper=(0, 0)) + h1 = hobj.get_data(kstpkper=(59, 1)) + hsxs0 = h0[0, 8, :] + hsxs1 = h1[0, 8, :] + + # get delr array + dx = gwf.dis.delr.array + + # create x-axis for cross-section + hxloc = np.arange(1000, 2000.0 * 15, 2000.0) + + # set cross-section location + y = 2000.0 * 11.5 + xsloc = [(extents[0], extents[1]), (y, y)] + + # well locations + w1loc = (9.5 * 2000.0, 11.75 * 2000.0) + w2loc = (6.5 * 2000.0, 8.5 * 2000.0) + + fig = plt.figure(figsize=(6.8, 5), constrained_layout=True) + gs = mpl.gridspec.GridSpec(7, 10, figure=fig, wspace=100) + plt.axis("off") + + ax = fig.add_subplot(gs[:, 0:6]) + # ax.set_aspect('equal') + mm = flopy.plot.PlotMapView(model=gwf, ax=ax, extent=extents) + mm.plot_grid(lw=0.5, color="0.5") + mm.plot_bc(ftype="WEL", kper=1, plotAll=True) + mm.plot_bc(ftype="CHD", color="blue") + mm.plot_bc(ftype="RCH", color="green") + mm.plot_inactive(color_noflow="0.75") + mm.ax.plot(xsloc[0], xsloc[1], color="orange", lw=1.5) + # contour steady state heads + cl = mm.contour_array( + h0, + masked_values=[1.0e30], + levels=np.arange(115, 200, 5), + colors="black", + linestyles="dotted", + linewidths=0.75, ) - if not silent: - print(f"saving...'{fpth}'") - fig.savefig(fpth) - + ax.clabel(cl, fmt="%3i", inline_spacing=0.1) + # well text + styles.add_annotation( + ax=ax, + text="Well 1, layer 2", + bold=False, + italic=False, + xy=w1loc, + xytext=(w1loc[0] - 3200, w1loc[1] + 1500), + ha="right", + va="center", + zorder=100, + arrowprops=arrow_props, + ) + styles.add_annotation( + ax=ax, + text="Well 2, layer 4", + bold=False, + italic=False, + xy=w2loc, + xytext=(w2loc[0] + 3000, w2loc[1]), + ha="left", + va="center", + zorder=100, + arrowprops=arrow_props, + ) + ax.set_ylabel("y-coordinate, in meters") + ax.set_xlabel("x-coordinate, in meters") + styles.heading(ax, letter="A", heading="Map view") + styles.remove_edge_ticks(ax) + + ax = fig.add_subplot(gs[0:5, 6:]) + mm = flopy.plot.PlotCrossSection(model=gwf, ax=ax, line={"row": 8}) + mm.plot_grid(lw=0.5, color="0.5") + # items for legend + mm.ax.plot( + -1000, + -1000, + "s", + ms=5, + color="green", + mec="black", + mew=0.5, + label="Recharge", + ) + mm.ax.plot( + -1000, + -1000, + "s", + ms=5, + color="red", + mec="black", + mew=0.5, + label="Well", + ) + mm.ax.plot( + -1000, + -1000, + "s", + ms=5, + color="blue", + mec="black", + mew=0.5, + label="Constant head", + ) + mm.ax.plot( + -1000, + -1000, + "s", + ms=5, + color="0.75", + mec="black", + mew=0.5, + label="Inactive", + ) + mm.ax.plot( + [-1000, -1001], + [-1000, -1000], + color="orange", + lw=1.5, + label="Cross-section line", + ) + # aquifer coloring + ax.fill_between([0, dx.sum()], y1=150, y2=-100, color="cyan", alpha=0.5) + ax.fill_between([0, dx.sum()], y1=-100, y2=-150, color="#D2B48C", alpha=0.5) + ax.fill_between([0, dx.sum()], y1=-150, y2=-350, color="#00BFFF", alpha=0.5) + # well coloring + ax.fill_between( + [dx.cumsum()[8], dx.cumsum()[9]], y1=50, y2=-100, color="red", lw=0 + ) + # labels + styles.add_text( + ax=ax, + transform=False, + bold=False, + italic=False, + x=300, + y=-97, + text="Upper aquifer", + va="bottom", + ha="left", + fontsize=9, + ) + styles.add_text( + ax=ax, + transform=False, + bold=False, + italic=False, + x=300, + y=-147, + text="Confining unit", + va="bottom", + ha="left", + fontsize=9, + ) + styles.add_text( + ax=ax, + transform=False, + bold=False, + italic=False, + x=300, + y=-347, + text="Lower aquifer", + va="bottom", + ha="left", + fontsize=9, + ) + styles.add_text( + ax=ax, + transform=False, + bold=False, + italic=False, + x=29850, + y=53, + text="Layer 1", + va="bottom", + ha="right", + fontsize=9, + ) + styles.add_text( + ax=ax, + transform=False, + bold=False, + italic=False, + x=29850, + y=-97, + text="Layer 2", + va="bottom", + ha="right", + fontsize=9, + ) + styles.add_text( + ax=ax, + transform=False, + bold=False, + italic=False, + x=29850, + y=-147, + text="Layer 3", + va="bottom", + ha="right", + fontsize=9, + ) + styles.add_text( + ax=ax, + transform=False, + bold=False, + italic=False, + x=29850, + y=-347, + text="Layer 4", + va="bottom", + ha="right", + fontsize=9, + ) + ax.plot( + hxloc, + hsxs0, + lw=0.75, + color="black", + ls="dotted", + label="Steady-state\nwater level", + ) + ax.plot( + hxloc, + hsxs1, + lw=0.75, + color="black", + ls="dashed", + label="Water-level\nafter period 2", + ) + ax.set_ylabel("Elevation, in meters") + ax.set_xlabel("x-coordinate along model row 9, in meters") + styles.graph_legend( + mm.ax, + ncol=2, + bbox_to_anchor=(0.7, -0.6), + borderaxespad=0, + frameon=False, + loc="lower center", + ) + styles.heading(ax, letter="B", heading="Cross-section view") + styles.remove_edge_ticks(ax) -# Function to plot the stresses + if plot_show: + plt.show() + if plot_save: + fpth = os.path.join("..", "figures", f"{sim_name}-grid.png") + if not silent: + print(f"saving...'{fpth}'") + fig.savefig(fpth) def plot_stresses(sim, silent=True): - verbose = not silent - fs = USGSFigure(figure_type="graph", verbose=verbose) - name = sim.name - - cd = get_csub_observations(sim) - tmax = cd["totim"][-1] - - fig, axes = plt.subplots( - ncols=1, - nrows=4, - figsize=figure_size, - sharex=True, - constrained_layout=True, - ) + with styles.USGSPlot() as fs: + name = sim.name + + cd = get_csub_observations(sim) + tmax = cd["totim"][-1] + + fig, axes = plt.subplots( + ncols=1, + nrows=4, + figsize=figure_size, + sharex=True, + constrained_layout=True, + ) - idx = 0 - ax = axes[idx] - ax.set_xlim(0, tmax) - ax.set_ylim(110, 150) - ax.plot( - cd["totim"], - cd["PC1"], - color="blue", - lw=1, - label="Preconsolidation stress", - ) - ax.plot( - cd["totim"], cd["ES1"], color="red", lw=1, label="Effective stress" - ) - fs.heading(ax, letter="A", heading="Model layer 1, row 9, column 10") - fs.remove_edge_ticks(ax) - - idx += 1 - ax = axes[idx] - ax.set_ylim(185, 205) - ax.plot(cd["totim"], cd["GS1"], color="black", lw=1) - fs.heading(ax, letter="B", heading="Model layer 1, row 9, column 10") - fs.remove_edge_ticks(ax) - - idx += 1 - ax = axes[idx] - ax.set_ylim(270, 310) - ax.plot(cd["totim"], cd["PC2"], color="blue", lw=1) - ax.plot(cd["totim"], cd["ES2"], color="red", lw=1) - fs.heading(ax, letter="C", heading="Model layer 2, row 9, column 10") - fs.remove_edge_ticks(ax) - - idx += 1 - ax = axes[idx] - ax.set_ylim(495, 515) - ax.plot( - [-100, -50], - [-100, -100], - color="blue", - lw=1, - label="Preconsolidation stress", - ) - ax.plot( - [-100, -50], [-100, -100], color="red", lw=1, label="Effective stress" - ) - ax.plot( - cd["totim"], cd["GS2"], color="black", lw=1, label="Geostatic stress" - ) - fs.graph_legend(ax, ncol=3, loc="upper center") - fs.heading(ax, letter="D", heading="Model layer 2, row 9, column 10") - fs.remove_edge_ticks(ax) - ax.set_xlabel("Simulation time, in years") - ax.set_ylabel(" ") + idx = 0 + ax = axes[idx] + ax.set_xlim(0, tmax) + ax.set_ylim(110, 150) + ax.plot( + cd["totim"], + cd["PC1"], + color="blue", + lw=1, + label="Preconsolidation stress", + ) + ax.plot(cd["totim"], cd["ES1"], color="red", lw=1, label="Effective stress") + styles.heading(ax, letter="A", heading="Model layer 1, row 9, column 10") + styles.remove_edge_ticks(ax) + + idx += 1 + ax = axes[idx] + ax.set_ylim(185, 205) + ax.plot(cd["totim"], cd["GS1"], color="black", lw=1) + styles.heading(ax, letter="B", heading="Model layer 1, row 9, column 10") + styles.remove_edge_ticks(ax) + + idx += 1 + ax = axes[idx] + ax.set_ylim(270, 310) + ax.plot(cd["totim"], cd["PC2"], color="blue", lw=1) + ax.plot(cd["totim"], cd["ES2"], color="red", lw=1) + styles.heading(ax, letter="C", heading="Model layer 2, row 9, column 10") + styles.remove_edge_ticks(ax) + + idx += 1 + ax = axes[idx] + ax.set_ylim(495, 515) + ax.plot( + [-100, -50], + [-100, -100], + color="blue", + lw=1, + label="Preconsolidation stress", + ) + ax.plot([-100, -50], [-100, -100], color="red", lw=1, label="Effective stress") + ax.plot(cd["totim"], cd["GS2"], color="black", lw=1, label="Geostatic stress") + styles.graph_legend( + ax, + ncol=3, + bbox_to_anchor=(0.9, -0.6), + ) + styles.heading(ax, letter="D", heading="Model layer 2, row 9, column 10") + styles.remove_edge_ticks(ax) + ax.set_xlabel("Simulation time, in years") + ax.set_ylabel(" ") - ax = fig.add_subplot(111, frame_on=False, xticks=[], yticks=[]) - ax.set_ylabel("Stress, in meters of water") + ax = fig.add_subplot(111, frame_on=False, xticks=[], yticks=[]) + ax.set_ylabel("Stress, in meters of water") - # save figure - if config.plotSave: - fpth = os.path.join("..", "figures", f"{name}-01{config.figure_ext}") - if not silent: - print(f"saving...'{fpth}'") - fig.savefig(fpth) + if plot_show: + plt.show() + if plot_save: + fpth = os.path.join("..", "figures", f"{name}-01.png") + if not silent: + print(f"saving...'{fpth}'") + fig.savefig(fpth) def plot_compaction(sim, silent=True): - verbose = not silent - fs = USGSFigure(figure_type="graph", verbose=verbose) - name = sim.name - - fig, axes = plt.subplots( - ncols=2, - nrows=3, - figsize=figure_size, - sharex=True, - constrained_layout=True, - ) - axes = axes.flatten() - - idx = 0 - ax = axes[idx] - ax.set_xlim(0, 120) - ax.set_ylim(0, 1) - plot_compaction_values(ax, sim, tagbase=plot_tags[idx]) - ht = "{} {}".format("Interbed compaction", compaction_heading[0]) - fs.heading(ax, letter="A", heading=ht) - fs.remove_edge_ticks(ax) - - idx += 1 - ax = axes[idx] - ax.set_ylim(0, 1) - plot_compaction_values(ax, sim, tagbase=plot_tags[idx]) - fs.graph_legend(ax, ncol=2, loc="upper center") - ht = "{} {}".format("Interbed compaction", compaction_heading[1]) - fs.heading(ax, letter="B", heading=ht) - fs.remove_edge_ticks(ax) - - idx += 1 - ax = axes[idx] - ax.set_ylim(0, 1) - plot_compaction_values(ax, sim, tagbase=plot_tags[idx]) - ht = "{} {}".format("Coarse-grained compaction", compaction_heading[0]) - fs.heading(ax, letter="C", heading=ht) - fs.remove_edge_ticks(ax) - - idx += 1 - ax = axes[idx] - ax.set_ylim(0, 1) - plot_compaction_values(ax, sim, tagbase=plot_tags[idx]) - ht = "{} {}".format("Coarse-grained compaction", compaction_heading[1]) - fs.heading(ax, letter="D", heading=ht) - fs.remove_edge_ticks(ax) - - idx += 1 - ax = axes[idx] - ax.set_ylim(0, 1) - plot_compaction_values(ax, sim, tagbase=plot_tags[idx]) - ht = "{} {}".format("Total compaction", compaction_heading[0]) - fs.heading(ax, letter="E", heading=ht) - fs.remove_edge_ticks(ax) - ax.set_ylabel(" ") - ax.set_xlabel(" ") - - idx += 1 - ax = axes.flat[idx] - ax.set_ylim(0, 1) - plot_compaction_values(ax, sim, tagbase=plot_tags[idx]) - ht = "{} {}".format("Total compaction", compaction_heading[1]) - fs.heading(ax, letter="F", heading=ht) - fs.remove_edge_ticks(ax) - - ax = fig.add_subplot(111, frame_on=False, xticks=[], yticks=[]) - ax.set_ylabel( - "Downward vertical displacement at the top of the model layer, in meters" - ) - ax.set_xlabel("Simulation time, in years") - - # save figure - if config.plotSave: - fpth = os.path.join("..", "figures", f"{name}-02{config.figure_ext}") - if not silent: - print(f"saving...'{fpth}'") - fig.savefig(fpth) - + with styles.USGSPlot(): + name = sim.name + + fig, axes = plt.subplots( + ncols=2, + nrows=3, + figsize=figure_size, + sharex=True, + constrained_layout=True, + ) + axes = axes.flatten() + + idx = 0 + ax = axes[idx] + ax.set_xlim(0, 120) + ax.set_ylim(0, 1) + plot_compaction_values(ax, sim, tagbase=plot_tags[idx]) + ht = f"Interbed compaction\n{compaction_heading[0]}" + styles.heading(ax, letter="A", heading=ht) + styles.remove_edge_ticks(ax) + + idx += 1 + ax = axes[idx] + ax.set_ylim(0, 1) + plot_compaction_values(ax, sim, tagbase=plot_tags[idx]) + ht = f"Interbed compaction\n{compaction_heading[1]}" + styles.heading(ax, letter="B", heading=ht) + styles.remove_edge_ticks(ax) + + idx += 1 + ax = axes[idx] + ax.set_ylim(0, 1) + plot_compaction_values(ax, sim, tagbase=plot_tags[idx]) + ht = f"Coarse-grained compaction\n{compaction_heading[0]}" + styles.heading(ax, letter="C", heading=ht) + styles.remove_edge_ticks(ax) + + idx += 1 + ax = axes[idx] + ax.set_ylim(0, 1) + plot_compaction_values(ax, sim, tagbase=plot_tags[idx]) + ht = f"Coarse-grained compaction\n{compaction_heading[1]}" + styles.heading(ax, letter="D", heading=ht) + styles.remove_edge_ticks(ax) + styles.graph_legend(ax, ncol=2, loc="lower right") + + idx += 1 + ax = axes[idx] + ax.set_ylim(0, 1) + plot_compaction_values(ax, sim, tagbase=plot_tags[idx]) + ht = f"Total compaction\n{compaction_heading[0]}" + styles.heading(ax, letter="E", heading=ht) + styles.remove_edge_ticks(ax) + ax.set_ylabel(" ") + ax.set_xlabel(" ") + + idx += 1 + ax = axes.flat[idx] + ax.set_ylim(0, 1) + plot_compaction_values(ax, sim, tagbase=plot_tags[idx]) + ht = f"Total compaction\n{compaction_heading[1]}" + styles.heading(ax, letter="F", heading=ht) + styles.remove_edge_ticks(ax) + + ax = fig.add_subplot(111, frame_on=False, xticks=[], yticks=[]) + ax.set_ylabel( + "Downward vertical displacement at the top of the model layer, in meters" + ) + ax.set_xlabel("Simulation time, in years") -# Function to plot the model results. + if plot_show: + plt.show() + if plot_save: + fpth = os.path.join("..", "figures", f"{name}-02.png") + if not silent: + print(f"saving...'{fpth}'") + fig.savefig(fpth) def plot_results(sim, silent=True): - if config.plotModel: - plot_grid(sim, silent=silent) - plot_stresses(sim, silent=silent) - plot_compaction(sim, silent=silent) + plot_grid(sim, silent=silent) + plot_stresses(sim, silent=silent) + plot_compaction(sim, silent=silent) -# Function that wraps all of the steps for the model -# -# 1. build_model, -# 2. write_model, -# 3. run_model, and -# 4. plot_results. -# - +# - -def simulation(silent=True): - sim = build_model() - - write_model(sim, silent=silent) +# ### Running the example +# +# Define and invoke a function to run the example scenario, then plot results. - success = run_model(sim, silent=silent) - if success: +# + +def scenario(silent=True): + sim = build_models() + if write: + write_models(sim, silent=silent) + if run: + run_models(sim, silent=silent) + if plot: plot_results(sim, silent=silent) -# nosetest - exclude block from this nosetest to the next nosetest -def test_01(): - simulation() - - -# nosetest end - -if __name__ == "__main__": - # ### One-dimensional compaction in a three-dimensional flow field - - simulation() +scenario() +# - diff --git a/scripts/ex-gwf-curvilinear-90.py b/scripts/ex-gwf-curvilinear-90.py index c788af46f..d525a3370 100644 --- a/scripts/ex-gwf-curvilinear-90.py +++ b/scripts/ex-gwf-curvilinear-90.py @@ -14,24 +14,2156 @@ # Oxford. England: Clarendon. # The equation is transformed here to use head instead of concentration -# Imports +# ### Initial setup +# +# ### Initial setup +# +# Import dependencies, define the example name and workspace, and read settings from environment variables. +# + +import copy import os -import sys -import numpy as np -import matplotlib.pyplot as plt -import flopy +import pathlib as pl +from itertools import cycle from math import sqrt -# Append to system path to include the common subdirectory +import flopy +import matplotlib.pyplot as plt +import numpy as np +from flopy.plot.styles import styles +from modflow_devtools.misc import get_env, timed + +# Example name and base workspace +sim_name = "ex-gwf-curve-90" +workspace = pl.Path("../examples") + +# Settings from environment variables +write = get_env("WRITE", True) +run = get_env("RUN", True) +plot = get_env("PLOT", True) +plot_show = get_env("PLOT_SHOW", True) +plot_save = get_env("PLOT_SAVE", True) +# - + +# ### Curvilinear grid +# +# Define some utilities to construct a curvilinear grid. + + +# + +class DisvPropertyContainer: + + """ + Dataclass that stores MODFLOW 6 DISV grid information. + + This is a base class that stores DISV **kwargs information used + by flopy for building a ``flopy.mf6.ModflowGwfdisv`` object. + + All indices are zero-based, but translated to one-base for the figures and + by flopy for use with MODFLOW 6. + + If no arguments are provided then an empty object is returned. + + Parameters + ---------- + nlay : int + Number of layers. + vertices : list[list[int, float, float]] + List of vertices structured as + ``[[iv, xv, vy], ...]`` + where + ``iv`` is the vertex index, + ``xv`` is the x-coordinate, and + ``yv`` is the y-coordinate. + cell2d : list[list[int, float, float, int, int...]] + List of MODFLOW 6 cells structured as + ```[[icell2d, xc, yc, ncvert, icvert], ...]``` + where + ``icell2d`` is the cell index, + ``xc`` is the x-coordinate for the cell center, + ``yc`` is the y-coordinate for the cell center, + ``ncvert`` is the number of vertices required to define the cell, + ``icvert`` is a list of vertex indices that define the cell, and + in clockwise order. + top : np.ndarray + Is the top elevation for each cell in the top model layer. + botm : list[np.ndarray] + List of bottom elevation by layer for all model cells. + origin_x : float, default=0.0 + X-coordinate of the origin used as the reference point for other + vertices. This is used for shift and rotate operations. + origin_y : float, default=0.0 + X-coordinate of the origin used as the reference point for other + vertices. This is used for shift and rotate operations. + rotation : float, default=0.0 + Rotation angle in degrees for the model grid. + shift_origin : bool, default=True + If True and `origin_x` or `origin_y` is non-zero, then all vertices are + shifted from an assumed (0.0, 0.0) origin to the (origin_x, origin_y) + location. + rotate_grid, default=True + If True and `rotation` is non-zero, then all vertices are rotated by + rotation degrees around (origin_x, origin_y). + + Attributes + ---------- + nlay : int + Number of layers. + ncpl : int + Number of cells per layer. + nvert : int + Number of vertices. + vertices : list[list] + List of vertices structured as ``[[iv, xv, vy], ...]`` + cell2d : list[list] + List of 2D cells structured as ```[[icell2d, xc, yc, ncvert, icvert], ...]``` + top : np.ndarray + Top elevation for each cell in the top model layer. + botm : list[np.ndarray] + List of bottom elevation by layer for all model cells. + origin_x : float + X-coordinate reference point used by grid. + origin_y : float + Y-coordinate reference point used by grid. + rotation : float + Rotation angle of grid about (origin_x, origin_y) + + Methods + ------- + get_disv_kwargs() + Get the keyword arguments for creating a MODFLOW-6 DISV package. + plot_grid(...) + Plot the model grid from `vertices` and `cell2d` attributes. + change_origin(new_x_origin, new_y_origin) + Change the origin of the grid. + rotate_grid(rotation) + Rotate the grid. + get_centroid(icvert, vertices=None) + Calculate the centroid of a cell given by list of vertices `icvert`. + copy() + Create and return a copy of the current object. + """ + + nlay: int + ncpl: int + nvert: int + vertices: list[list] # [[iv, xv, yv], ...] + cell2d: list[list] # [[ic, xc, yc, ncvert, icvert], ...] + top: np.ndarray + botm: list[np.ndarray] + origin_x: float + origin_y: float + rotation: float + + def __init__( + self, + nlay=-1, + vertices=None, + cell2d=None, + top=None, + botm=None, + origin_x=0.0, + origin_y=0.0, + rotation=0.0, + shift_origin=True, + rotate_grid=True, + ): + if nlay is None or nlay < 1: + self._init_empty() + return + + self.nlay = nlay + self.ncpl = len(cell2d) + self.nvert = len(vertices) + + self.vertices = [] if vertices is None else copy.deepcopy(vertices) + self.cell2d = [] if cell2d is None else copy.deepcopy(cell2d) + self.top = np.array([]) if top is None else copy.deepcopy(top) + self.botm = [] if botm is None else copy.deepcopy(botm) + + self.origin_x, self.origin_y, self.rotation = 0.0, 0.0, 0.0 + + if shift_origin: + if abs(origin_x) > 1.0e-30 or abs(origin_y) > 1.0e-30: + self.change_origin(origin_x, origin_y) + elif not shift_origin: + self.origin_x, self.origin_y = origin_x, origin_y + + if rotate_grid: + self.rotate_grid(rotation) + elif not shift_origin: + self.rotation = rotation + + def get_disv_kwargs(self): + """ + Get the dict of keyword arguments for creating a MODFLOW-6 DISV + package using ``flopy.mf6.ModflowGwfdisv``. + """ + return { + "nlay": self.nlay, + "ncpl": self.ncpl, + "top": self.top, + "botm": self.botm, + "nvert": self.nvert, + "vertices": self.vertices, + "cell2d": self.cell2d, + } + + def __repr__(self, cls="DisvPropertyContainer"): + return ( + f"{cls}(\n\n" + f"nlay={self.nlay}, ncpl={self.ncpl}, nvert={self.nvert}\n\n" + f"origin_x={self.origin_x}, origin_y={self.origin_y}, " + f"rotation={self.rotation}\n\n" + f"vertices =\n{self._string_repr(self.vertices)}\n\n" + f"cell2d =\n{self._string_repr(self.cell2d)}\n\n" + f"top =\n{self.top}\n\n" + f"botm =\n{self.botm}\n\n)" + ) + + def _init_empty(self): + self.nlay = 0 + self.ncpl = 0 + self.nvert = 0 + self.vertices = [] + self.cell2d = [] + self.top = np.array([]) + self.botm = [] + self.origin_x = 0.0 + self.origin_y = 0.0 + self.rotation = 0.0 + + def change_origin(self, new_x_origin, new_y_origin): + shift_x_origin = new_x_origin - self.origin_x + shift_y_origin = new_y_origin - self.origin_y + + self.shift_origin(shift_x_origin, shift_y_origin) + + def shift_origin(self, shift_x_origin, shift_y_origin): + if abs(shift_x_origin) > 1.0e-30 or abs(shift_y_origin) > 1.0e-30: + self.origin_x += shift_x_origin + self.origin_y += shift_y_origin + + for vert in self.vertices: + vert[1] += shift_x_origin + vert[2] += shift_y_origin + + for cell in self.cell2d: + cell[1] += shift_x_origin + cell[2] += shift_y_origin + + def rotate_grid(self, rotation): + """Rotate grid around origin_x, origin_y for given angle in degrees. + + References + ---------- + [1] https://en.wikipedia.org/wiki/Transformation_matrix#Rotation + + """ + # + if abs(rotation) > 1.0e-30: + self.rotation += rotation + + sin, cos = np.sin, np.cos + a = np.radians(rotation) + x0, y0 = self.origin_x, self.origin_y + # Steps to shift + # 0) Get x, y coordinate to be shifted + # 1) Shift coordinate's reference point to origin + # 2) Rotate around origin + # 3) Shift back to original reference point + for vert in self.vertices: + _, x, y = vert + x, y = x - x0, y - y0 + x, y = x * cos(a) - y * sin(a), x * sin(a) + y * cos(a) + vert[1] = x + x0 + vert[2] = y + y0 + + for cell in self.cell2d: + _, x, y, *_ = cell + x, y = x - x0, y - y0 + x, y = x * cos(a) - y * sin(a), x * sin(a) + y * cos(a) + cell[1] = x + x0 + cell[2] = y + y0 + + @staticmethod + def _string_repr(list_list, sep=",\n"): + dim = len(list_list) + s = [] + if dim == 0: + return "[]" + if dim < 7: + for elm in list_list: + s.append(repr(elm)) + else: + for it in range(3): + s.append(repr(list_list[it])) + s.append("...") + for it in range(-3, 0): + s.append(repr(list_list[it])) + return sep.join(s) + + def property_copy_to(self, DisvPropertyContainerType): + if isinstance(DisvPropertyContainerType, DisvPropertyContainer): + DisvPropertyContainerType.nlay = self.nlay + DisvPropertyContainerType.ncpl = self.ncpl + DisvPropertyContainerType.nvert = self.nvert + DisvPropertyContainerType.vertices = self.vertices + DisvPropertyContainerType.cell2d = self.cell2d + DisvPropertyContainerType.top = self.top + DisvPropertyContainerType.botm = self.botm + DisvPropertyContainerType.origin_x = self.origin_x + DisvPropertyContainerType.origin_y = self.origin_y + DisvPropertyContainerType.rotation = self.rotation + else: + raise RuntimeError( + "DisvPropertyContainer.property_copy_to " + "can only copy to objects that inherit " + "properties from DisvPropertyContainer" + ) + + def copy(self): + cp = DisvPropertyContainer() + self.property_copy_to(cp) + return cp + + def keys(self): + """ + Get the keys used by ``flopy.mf6.ModflowGwfdisv``. + + This method is only used to provide unpacking support for + `DisvPropertyContainer` objects and subclasses. + + That is: + ``flopy.mf6.ModflowGwfdisv(gwf, **DisvPropertyContainer)`` + + Returns + ------- + list + List of keys used by ``flopy.mf6.ModflowGwfdisv`` + + """ + return self.get_disv_kwargs().keys() + + def __getitem__(self, k): + if hasattr(self, k): + return getattr(self, k) + raise KeyError(f"{k}") + + @staticmethod + def _get_array(cls_name, var, rep, rep2=None): + if rep2 is None: + rep2 = rep + + try: + dim = len(var) + except TypeError: + dim, var = 1, [var] + + try: # Check of array needs to be flattened + _ = len(var[0]) + tmp = [] + for row in var: + tmp.extend(row) + var = tmp + dim = len(var) + except TypeError: + pass + + if dim != 1 and dim != rep and dim != rep2: + msg = f"{cls_name}(var): var must be a scalar " + msg += f"or have len(var)=={rep}" + if rep2 != rep: + msg += f"or have len(var)=={rep2}" + raise IndexError(msg) + + if dim == 1: + return np.full(rep, var[0], dtype=np.float64) + else: + return np.array(var, dtype=np.float64) + + def get_centroid(self, icvert, vertices=None): + """ + Calculate the centroid of a cell for a given set of vertices. + + Parameters + ---------- + icvert : list[int] + List of vertex indices for the cell. + vertices : list[list], optional + List of vertices that `icvert` references to define the cell. + If not present, then the `vertices` attribute is used. + + Returns + ------- + tuple + A tuple containing the X and Y coordinates of the centroid. + + References + ---------- + [1] https://en.wikipedia.org/wiki/Centroid#Of_a_polygon + """ + if vertices is None: + vertices = self.vertices + + nv = len(icvert) + x = [] + y = [] + for iv in icvert: + x.append(vertices[iv][1]) + y.append(vertices[iv][2]) + + if nv < 3: + raise RuntimeError("get_centroid: len(icvert) < 3") + + if nv == 3: # Triangle + return sum(x) / 3, sum(y) / 3 + + xc, yc = 0.0, 0.0 + signedArea = 0.0 + for i in range(nv - 1): + x0, y0, x1, y1 = x[i], y[i], x[i + 1], y[i + 1] + a = x0 * y1 - x1 * y0 + signedArea += a + xc += (x0 + x1) * a + yc += (y0 + y1) * a + + x0, y0, x1, y1 = x1, y1, x[0], y[0] + + a = x0 * y1 - x1 * y0 + signedArea += a + xc += (x0 + x1) * a + yc += (y0 + y1) * a + + signedArea *= 0.5 + return xc / (6 * signedArea), yc / (6 * signedArea) + + def plot_grid( + self, + title="", + plot_time=0.0, + show=True, + figsize=(10, 10), + dpi=None, + xlabel="", + ylabel="", + cell2d_override=None, + vertices_override=None, + ax_override=None, + cell_dot=True, + cell_num=True, + cell_dot_size=7.5, + cell_dot_color="coral", + vertex_dot=True, + vertex_num=True, + vertex_dot_size=6.5, + vertex_dot_color="skyblue", + grid_color="grey", + ): + """ + Plot the model grid with optional features. + All inputs are optional. + + Parameters + ---------- + title : str, default="" + Title for the plot. + plot_time : float, default=0.0 + Time interval for animation (if greater than 0). + show : bool, default=True + Whether to display the plot. If false, then plot_time is set to -1 + and function returns figure and axis objects. + figsize : tuple, default=(10, 10) + Figure size (width, height) in inches. Default is (10, 10). + dpi : float, default=None + Set dpi for Matplotlib figure. + If set to None, then uses Matplotlib default dpi. + xlabel : str, default="" + X-axis label. + ylabel : str, default="" + Y-axis label. + cell2d_override : list[list], optional + List of ``cell2d`` cells to override the object's cell2d. + Default is None. + vertices_override : list[list], optional + List of vertices to override the object's vertices. + Default is None. + ax_override : matplotlib.axes.Axes, optional + Matplotlib axis object to use for generating plot instead of + making a new figure and axis objects. If present, then show is + set to False and plot_time to -1. + cell_dot : bool, default=True + Whether to add a filled circle at the cell center locations. + cell_num : bool, default=True + Whether to label cells with cell2d index numbers. + cell_dot_size : bool, default=7.5 + The size, in points, of the filled circles and index numbers + at the cell center. + cell_dot_color : str, default="coral" + The color of the filled circles at the cell center. + vertex_num : bool, default=True + Whether to label vertices with the vertex numbers. + vertex_dot : bool, default=True + Whether to add a filled circle at the vertex locations. + vertex_dot_size : bool, default=6.5 + The size, in points, of the filled circles and index numbers + at the vertex locations. + vertex_dot_color : str, default="skyblue" + The color of the filled circles at the vertex locations. + grid_color : str or tuple[str], default="grey" + The color of the grid lines. + If plot_time > 0, then animation cycled through the colors + for each cell outline. + + Returns + ------- + (matplotlib.figure.Figure, matplotlib.axis.Axis) or None + If `show` is False, returns the Figure and Axis objects; + otherwise, returns None. + + Raises + ------ + RuntimeError + If either `cell2d_override` or `vertices_override` is provided + without the other. + + Notes + ----- + This method plots the grid using Matplotlib. It can label cells and + vertices with numbers, show an animation of the plotting process, and + customize the plot appearance. + + Note that figure size (`figsize`) is in inches and the total pixels is based on + the `dpi` (dots per inch). For example, figsize=(3, 5) and + dpi=110, results in a figure resolution of (330, 550). + + Changing `figsize` does not effect the size + + Elements (text, markers, lines) in Matplotlib use + 72 points per inch (ppi) as a basis for translating to dpi. + Any changes to dpi result in a scaling effect. The default dpi of 100 + results in a line width 100/72 pixels wide. + + Similarly, a line width of 1 point with a dpi set to 72 results in + a line that is 1 pixel wide. The following then occurs: + + 2*72 dpi results in a line width 2 pixels; + 3*72 dpi results in a line width 3 pixels; and + 600 dpi results in a line width 600/72 pixels. + + Conversely, changing `figsize` increases the total pixel count, but + elements maintain the same dpi. That is the figure wireframe will be + larger, but the elements will have the same pixel widths. For example, + a line width of 1 will have a width of 100/72 in for any figure size, + as long as the dpi is set to 100. + """ + + if cell2d_override is not None and vertices_override is not None: + cell2d = cell2d_override + vertices = vertices_override + elif cell2d_override is not None or vertices_override is not None: + raise RuntimeError( + "plot_vertex_grid: if you specify " + "cell2d_override or vertices_override, " + "then you must specify both." + ) + else: + cell2d = self.cell2d + vertices = self.vertices + + if ax_override is None: + fig = plt.figure(figsize=figsize, dpi=dpi) + ax = fig.add_subplot() + else: + show = False + ax = ax_override + + ax.set_aspect("equal", adjustable="box") + + if not show: + plot_time = -1.0 + + if not isinstance(grid_color, tuple): + grid_color = (grid_color,) + + ColorCycler = grid_color + if plot_time > 0.0 and grid_color == ("grey",): + ColorCycler = ("green", "red", "grey", "magenta", "cyan", "yellow") + ColorCycle = cycle(ColorCycler) + + xvert = [] + yvert = [] + for r in vertices: + xvert.append(r[1]) + yvert.append(r[2]) + xcell = [] + ycell = [] + for r in cell2d: + xcell.append(r[1]) + ycell.append(r[2]) + + if title != "": + ax.set_title(title) + if xlabel != "": + ax.set_xlabel(xlabel) + if ylabel != "": + ax.set_ylabel(ylabel) + + vert_size = vertex_dot_size + cell_size = cell_dot_size + + if vertex_dot: + ax.plot( + xvert, + yvert, + linestyle="None", + color=vertex_dot_color, + markersize=vert_size, + marker="o", + markeredgewidth=0.0, + zorder=2, + ) + if cell_dot: + ax.plot( + xcell, + ycell, + linestyle="None", + color=cell_dot_color, + markersize=cell_size, + marker="o", + markeredgewidth=0.0, + zorder=2, + ) + + if cell_num: + for ic, xc, yc, *_ in cell2d: + ax.text( + xc, + yc, + f"{ic + 1}", + fontsize=cell_size, + color="black", + fontfamily="Arial Narrow", + fontweight="black", + rasterized=False, + horizontalalignment="center", + verticalalignment="center", + zorder=3, + ) + + if vertex_num: + for iv, xv, yv in vertices: + ax.text( + xv, + yv, + f"{iv + 1}", + fontsize=vert_size, + fontweight="black", + rasterized=False, + color="black", + fontfamily="Arial Narrow", + horizontalalignment="center", + verticalalignment="center", + zorder=3, + ) + + if plot_time > 0: + plt.show(block=False) + plt.pause(5 * plot_time) + + for ic, xc, yc, ncon, *vert in cell2d: + color = next(ColorCycle) + + conn = vert + [vert[0]] # Extra node to complete polygon + + for i in range(ncon): + n1, n2 = conn[i], conn[i + 1] + px = [vertices[n1][1], vertices[n2][1]] + py = [vertices[n1][2], vertices[n2][2]] + ax.plot(px, py, color=color, zorder=1) + + if plot_time > 0: + plt.draw() + plt.pause(plot_time) + + if show: + plt.show() + elif ax_override is None: + return fig, ax + + +class DisvStructuredGridBuilder(DisvPropertyContainer): + """ + A class for generating a structured MODFLOW 6 DISV grid. + + This class inherits from the `DisvPropertyContainer` class and provides + methods to generate a rectangular, structured grid give the number rows + (nrow), columns (ncol), row widths, and columns widths. Rows are + discretized along the y-axis and columns along the x-axis. The row, column + structure follows MODFLOW 6 structured grids. That is, the first row has + the largest y-axis vertices and last row the lowest; and the first column + has the lowest x-axis vertices and last column the highest. + + All indices are zero-based, but translated to one-base for the figures and + by flopy for use with MODFLOW 6. + + The following shows the placement for (row, column) pairs + in a nrow=3 and ncol=5 model: + + ``(0,0) (0,1) (0,2) (0,3) (0,4)`` + ``(1,0) (1,1) (1,2) (1,3) (1,4)`` + ``(2,0) (2,1) (2,2) (2,3) (2,4)`` + + Array-like structures that are multidimensional (has rows and columns) + are flatten by concatenating each row. Using the previous example, + the following is the flattened representation: + + ``(0,0) (0,1) (0,2) (0,3) (0,4) (1,0) (1,1) (1,2) (1,3) (1,4) (2,0) (2,1) (2,2) (2,3) (2,4)`` + + If no arguments are provided then an empty object is returned. + + Parameters + ---------- + nlay : int + Number of layers + nrow : int + Number of rows (y-direction cells). + ncol : int + Number of columns (x-direction cells). + row_width : float or array_like + Width of y-direction cells (each row). If a single value is provided, + it will be used for all rows. Otherwise, it must be array_like + of length ncol. + col_width : float or array_like + Width of x-direction cells (each column). If a single value is + provided, it will be used for all columns. Otherwise, it must be + array_like of length ncol. + surface_elevation : float or array_like + Surface elevation for the top layer. Can either be a single float + for the entire `top`, or array_like of length `ncpl`. + If it is a multidimensional array_like, then it is flattened to a + single dimension along the rows (first dimension). + layer_thickness : float or array_like + Thickness of each layer. Can either be a single float + for model cells, or array_like of length `nlay`, or + array_like of length `nlay`*`ncpl`. + origin_x : float, default=0.0 + X-coordinate reference point the lower-left corner of the model grid. + That is, the outermost corner of ``row=nrow-1` and `col=0`. + Rotations are performed around this point. + origin_y : float, default=0.0 + Y-coordinate reference point the lower-left corner of the model grid. + That is, the outermost corner of ``row=nrow-1` and `col=0`. + Rotations are performed around this point. + rotation : float, default=0.0 + Rotation angle in degrees for the model grid around (origin_x, origin_y). + + Attributes + ---------- + nrow : int + Number of rows in the grid. + ncol : int + Number of columns in the grid. + row_width : np.ndarray + Width of y-direction cells (each row). + col_width : np.ndarray + Width of x-direction cells (each column). + + Methods + ------- + get_disv_kwargs() + Get the keyword arguments for creating a MODFLOW-6 DISV package. + plot_grid(...) + Plot the model grid from `vertices` and `cell2d` attributes. + get_cellid(row, col, col_check=True) + Get the cellid given the row and column indices. + get_row_col(cellid) + Get the row and column indices given the cellid. + get_vertices(row, col) + Get the vertex indices for a cell given the row and column indices. + iter_row_col() + Iterate over row and column indices for each cell. + iter_row_cellid(row) + Iterate over cellid's in a specific row. + iter_column_cellid(col) + Iterate over cellid's in a specific column. + """ + + nrow: int + ncol: int + row_width: np.ndarray + col_width: np.ndarray + + def __init__( + self, + nlay=-1, + nrow=-1, # number of Y direction cells + ncol=-1, # number of X direction cells + row_width=10.0, # width of Y direction cells (each row) + col_width=10.0, # width of X direction cells (each column) + surface_elevation=100.0, + layer_thickness=100.0, + origin_x=0.0, + origin_y=0.0, + rotation=0.0, + ): + if nlay is None or nlay < 1: + self._init_empty() + return + + ncpl = ncol * nrow # Nodes per layer + + self.nrow = nrow + self.ncol = ncol + ncell = ncpl * nlay + + # Check if layer_thickness needs to be flattened + cls_name = "DisvStructuredGridBuilder" + top = self._get_array(cls_name, surface_elevation, ncpl) + thick = self._get_array(cls_name, layer_thickness, nlay, ncell) + self.row_width = self._get_array(cls_name, row_width, ncol) + self.col_width = self._get_array(cls_name, col_width, nrow) + + bot = [] + if thick.size == nlay: + for lay in range(nlay): + bot.append(top - thick[: lay + 1].sum()) + else: + st = 0 + sp = ncpl + bt = top.copy() + for lay in range(nlay): + bt -= thick[st:sp] + st, sp = sp, sp + ncpl + bot.append(bt) + + # Build the grid + + # Setup vertices + vertices = [] + + # Get row 1 top: + yv_model_top = self.col_width.sum() + + # Assemble vertices along x-axis and model top + iv = 0 + xv, yv = 0.0, yv_model_top + vertices.append([iv, xv, yv]) + for c in range(ncol): + iv += 1 + xv += self.row_width[c] + vertices.append([iv, xv, yv]) + + # Finish the rest of the grid a row at a time + for r in range(nrow): + iv += 1 + yv -= self.col_width[r] + xv = 0.0 + vertices.append([iv, xv, yv]) + for c in range(ncol): + iv += 1 + xv += self.row_width[c] + vertices.append([iv, xv, yv]) + + # cell2d: [icell2d, xc, yc, ncvert, icvert] + cell2d = [] + ic = -1 + # Finish the rest of the grid a row at a time + for r in range(nrow): + for c in range(ncol): + ic += 1 + icvert = self.get_vertices(r, c) + xc, yc = self.get_centroid(icvert, vertices) + cell2d.append([ic, xc, yc, 4, *icvert]) + + super().__init__(nlay, vertices, cell2d, top, bot, origin_x, origin_y, rotation) + + def __repr__(self): + return super().__repr__("DisvStructuredGridBuilder") + + def _init_empty(self): + super()._init_empty() + nul = np.array([]) + self.nrow = 0 + self.ncol = 0 + self.row_width = nul + self.col_width = nul + + def property_copy_to(self, DisvStructuredGridBuilderType): + if isinstance(DisvStructuredGridBuilderType, DisvStructuredGridBuilder): + super().property_copy_to(DisvStructuredGridBuilderType) + DisvStructuredGridBuilderType.nrow = self.nrow + DisvStructuredGridBuilderType.ncol = self.ncol + DisvStructuredGridBuilderType.row_width = self.row_width.copy() + DisvStructuredGridBuilderType.col_width = self.col_width.copy() + else: + raise RuntimeError( + "DisvStructuredGridBuilder.property_copy_to " + "can only copy to objects that inherit " + "properties from DisvStructuredGridBuilder" + ) + + def copy(self): + cp = DisvStructuredGridBuilder() + self.property_copy_to(cp) + return cp + + def get_cellid(self, row, col): + """ + Get the cellid given the row and column indices. + + Parameters + ---------- + row : int + Row index. + col : int + Column index. + + Returns + ------- + int + cellid index + """ + return row * self.ncol + col + + def get_row_col(self, cellid): + """ + Get the row and column indices given the cellid. + + Parameters + ---------- + cellid : int + cellid index + + Returns + ------- + (int, int) + Row index, Column index + """ + row = cellid // self.ncol + col = cellid - row * self.ncol + return row, col + + def get_vertices(self, row, col): + """ + Get the vertex indices for a cell given the row and column indices. + + Parameters + ---------- + row : int + Row index. + col : int + Column index. + + Returns + ------- + list[int] + List of vertex indices that define the cell at (row, col). + """ + nver = self.ncol + 1 + return [ + row * nver + col, + row * nver + col + 1, + (row + 1) * nver + col + 1, + (row + 1) * nver + col, + ] + + def iter_row_col(self): + """Generator that iterates through each rows' columns. + + Yields + ------- + (int, int) + Row index, column index + """ + for cellid in range(self.ncpl): + yield self.get_row_col(cellid) + + def iter_row_cellid(self, row): + """Generator that iterates through the cellid within a row. + That is, the cellid for all columns within the specified row. + + Parameters + ---------- + row : int + Row index. + + Yields + ------- + int + cellid index + """ + for col in range(self.ncol): + yield self.get_cellid(row, col) + + def iter_column_cellid(self, col): + """Generator that iterates through the cellid within a column. + That is, the cellid for all rows within the specified column. + + Parameters + ---------- + col : int + Column index. + + Yields + ------- + int + cellid index + """ + for row in range(self.nrow): + yield self.get_cellid(row, col) + + +class DisvGridMerger: + """ + Class for merging, non-overlapping, MODFLOW 6 DISV grids. The merge is + made by selecting a connection point and adjusting the (x,y) coordinates + of one of the grids. The grid connection is made by starting with the first + grid, called `__main__`, then adjusting the second grid to have __main__ + incorporate both grids. After that, subsequent grids are snapped to the + __main__ grid to form the final merged grid. + + When a grid is shifted to snap to __main__ (`snap_vertices`), any vertices + that are in proximity of the __main__ grid are merged (that is, the + snapped grid drops the overlapping vertices and uses the existing __main__ + ones). Proximately is determined by having an x or y distance less than + `connect_tolerance`. + + Vertices can also be forced to snap to the __main__ grid with `force_snap`. + A force snap occurs after the second grid is shifted and snapped to + __main__. The force snap drops the existing vertex and uses the forced one + changing the shape of the cell. Note, if any existing vertices are located + within the new shape of the cell, then they are added to the cell2d vertex + list. + + Examples + -------- + >>> # Example snaps two rectangular structured vertex grids. + >>> # The first grid has 2 rows and 2 columns; + >>> # the second grid has 3 rows and 2 columns. + >>> # DisvStructuredGridBuilder builds a DisvPropertyContainer object + >>> # that contains a structured vertex grid (rows and columns). + >>> from DisvStructuredGridBuilder import DisvStructuredGridBuilder + >>> + >>> grid1 = DisvStructuredGridBuilder(nlay=1, nrow=2, ncol=2) + >>> grid2 = DisvStructuredGridBuilder(nlay=1, nrow=3, ncol=2) + >>> + >>> # Optional step to see what vertex point to use + >>> grid1.plot_grid() # Plot and view vertex locations + >>> grid2.plot_grid() # to identify connection points. + >>> + >>> # Steps to merge grid1 and grid2 + >>> mg = DisvGridMerger() # init the object + >>> + >>> mg.add_grid("grid1", grid1) # add grid1 + >>> mg.add_grid("grid2", grid2) # add grid2 + >>> + >>> # Snap grid1 upper right corner (vertex 3) to grid2 upper left + >>> # corner (vertex 1). Note the vertices must be zero-based indexed. + >>> mg.set_vertex_connection("grid1", "grid2", 3 - 1, 1 - 1) + >>> + >>> # Grids do not require any force snapping because overlapping vertices + >>> # will be within the connect_tolerance. Otherwise, now would be + >>> # when to run the set_force_vertex_connection method. + >>> # Merge the grids + >>> mg.merge_grids() + >>> + >>> mg.merged.plot_grid() # plot the merged grid + + Attributes + ---------- + grids : dict + A dictionary containing names of individual grids as keys and + corresponding `DisvPropertyContainer` objects as values. + The key `__main__` is used to refer to the final merged grid and is not + allowed as a name for any `DisvPropertyContainer` object. + merged : DisvPropertyContainer + A `DisvPropertyContainer` object representing the merged grid. + snap_vertices : dict + A dictionary of vertex connections to be snapped during + the merging process. This attribute is set with the + ``set_vertex_connection`` method. The key is ``(name1, name2)`` and + value is ``(vertex1, vertex2)``, where name1 and name2 correspond with + keys from `grids` and ``vertex1`` and ``vertex2`` are the connection + vertices for ``name1`` and ``name2``, respectively. + connect_tolerance : dict + A dictionary specifying the tolerance distance for vertex snapping. + After a grid is snapped to __main__ via snap_vertices, any vertices + that overlap within an x or y length of connect_tolerance are merged. + snap_order : list + A list of grid pairs indicating the order in which grids + will be merged. This is variable is set after running the + `merge_grids` method. + force_snap : dict + A dictionary of vertex connections that must be snapped, + even if they don't satisfy the tolerance. The key is ``(name1, name2)`` + and value is ``[[v1, ...], [v2, ...]]``, where ``name1`` and ``name2`` + correspond with keys from `grids` and ``v1`` is a list of verties to + snap from ``name1``, and ``v2`` is a list of vertices to snap to from + ``name2``. The first ``v1``, corresponds with the first ``v2``, + and so forth. + force_snap_drop : dict + A dictionary specifying which vertex to drop when force snapping. The + key is ``(name1, name2)`` and value is ``[v_drop, ...]``, where + ``v_drop`` is 1 to drop the vertex from ``name1``, and 2 to drop + from ``name2``. + force_snap_cellid : set + A set that lists all the merged grid cellids that had one or more + verties force snapped. This list is important for checking if the + new vertex list and cell center are correct. + vert2name : dict + A dictionary mapping the merged grid's vertex numbers to the + corresponding grid names and vertex indices. The key is the vertex + from the new merged grid and the value is + ``[[name, vertex_old], ...]``, where ``name`` is the original grid name + and ``vertex_old`` is its correspondnig vertex from name. + name2vert : dict + A dictionary mapping grid names and vertex indices to the merged + grid's vertices. The key is ``(name, vertex_old)``, where ``name`` is + the original grid name and ``vertex_old`` is its correspondnig vertex + from name. The value is the merged grid's vertex. + cell2name : dict + A dictionary mapping the merged grid's cellid's to the corresponding + original grid names and cellid's. The key is the merged grid's cellid + and value is ``(name, cellid_old)``, where ``name`` is the + original grid name and ``cellid_old`` is its correspondnig cellid from + name. + name2cell : dict + A dictionary mapping grid names and cellid's to the merged grid's + cellid's. The key is ``(name, cellid_old)``, where ``name`` is the + original grid name and ``cellid_old`` is its correspondnig cellid from + name, and value is the merged grid's cellid. + + Notes + ------- + The following is always true: + + ``cell2name[cell] == name2vert[cell2name[cell]]`` + + ``name2vert[(name, vertex)] is in vert2name[name2vert[(name, vertex)]]`` + + Methods + ------- + get_disv_kwargs(name="__main__") + Get the keyword arguments for creating a MODFLOW-6 DISV package for + a specified grid. + add_grid(name, grid) + Add an individual grid to the merger. + set_vertex_connection(name1, name2, vertex1, vertex2, autosnap_tolerance=1.0e-5) + Set a vertex connection between two grids for snapping. + set_force_vertex_connection(name1, name2, vertex1, vertex2, drop_vertex=2) + Force a vertex connection between two grids. + merge_grids() + Merge the specified grids based on the defined vertex connections. + plot_grid(name="__main__", ...) + Selects the grid specified by ``name`` and passes the remaining + kwargs to DisvPropertyContainer.plot_grid(...). + """ + + grids: dict[str, DisvPropertyContainer] + merged: DisvPropertyContainer + snap_vertices: dict + connect_tolerance: dict + snap_order: list + force_snap: dict + force_snap_drop: dict + force_snap_cellid: set + vert2name: dict + name2vert: dict + cell2name: dict + name2cell: dict + + def __init__(self): + self.grids = {} + self.merged = DisvPropertyContainer() + + self.snap_vertices = {} + self.connect_tolerance = {} + self.snap_order = [] + self.force_snap = {} + self.force_snap_drop = {} + self.force_snap_cellid = set() + + self.vert2name = {} # vertex: [[name, vertex], ...] + self.name2vert = {} # (name, vertex): vertex + + self.cell2name = {} # cellid: (name, cellid) + self.name2cell = {} # (name, cellid): cellid + + def get_disv_kwargs(self, name="__main__"): + return self.get_grid(name).get_disv_kwargs() + + def __repr__(self): + names = ", ".join(self.grids.keys()) + return f"DisvGridMerger({names})" + + def property_copy_to(self, DisvGridMergerType): + if isinstance(DisvGridMergerType, DisvGridMerger): + DisvGridMergerType.merged = self.merged.copy() + dcp = copy.deepcopy + + for name in self.grids: + DisvGridMergerType.grids[name] = self.grids[name].copy() + + for name in self.snap_vertices: + DisvGridMergerType.snap_vertices[name] = dcp(self.snap_vertices[name]) + + for name in self.connect_tolerance: + DisvGridMergerType.connect_tolerance[name] = self.connect_tolerance[ + name + ] + + for name in self.force_snap: + DisvGridMergerType.force_snap[name] = dcp(self.force_snap[name]) + + for name in self.force_snap_drop: + DisvGridMergerType.force_snap_drop[name] = dcp( + self.force_snap_drop[name] + ) + + DisvGridMergerType.force_snap_cellid = dcp(self.force_snap_cellid) + + for name in self.vert2name: + DisvGridMergerType.vert2name[name] = dcp(self.vert2name[name]) + + for name in self.cell2name: + DisvGridMergerType.cell2name[name] = dcp(self.cell2name[name]) + + for name in self.name2vert: + DisvGridMergerType.name2vert[name] = self.name2vert[name] + + for name in self.name2cell: + DisvGridMergerType.name2cell[name] = self.name2cell[name] + + DisvGridMergerType.snap_order = dcp(self.snap_order) + else: + raise RuntimeError( + "DisvGridMerger.property_copy_to " + "can only copy to objects that inherit " + "properties from DisvGridMerger" + ) + + def copy(self): + cp = DisvGridMerger() + self.property_copy_to(cp) + return cp + + def get_merged_cell2d(self, name, cell2d_orig): + return self.name2cell[(name, cell2d_orig)] + + def get_merged_vertex(self, name, vertex_orig): + return self.name2vert[(name, vertex_orig)] + + def get_grid(self, name="__main__"): + if name == "" or name == "__main__": + return self.merged + + if name not in self.grids: + raise KeyError( + "DisvGridMerger.get_grid: requested grid, " + f"{name} does not exist.\n" + "Current grids stored are:\n" + "\n".join(self.grids.keys()) + ) + + return self.grids[name] + + def add_grid(self, name, grid): + if name == "" or name == "__main__": + raise RuntimeError( + "\nDisvGridMerger.add_grid:\n" + 'name = "" or "__main__"\nis not allowed.' + ) + if isinstance(grid, DisvPropertyContainer): + grid = grid.copy() + else: + # grid = [nlay, vertices, cell2d, top, botm] + grid = DisvPropertyContainer(*grid) + + self.grids[name] = grid + + def set_vertex_connection( + self, name1, name2, vertex1, vertex2, autosnap_tolerance=1.0e-5 + ): + if (name2, name1) in self.snap_vertices: + name1, name2 = name2, name1 + vertex1, vertex2 = vertex2, vertex1 + + key1 = (name1, name2) + key2 = (name2, name1) + + self.snap_vertices[key1] = (vertex1, vertex2) + self.connect_tolerance[key1] = autosnap_tolerance + + self.force_snap[key1] = [[], []] + self.force_snap[key2] = [[], []] + self.force_snap_drop[key1] = [] + self.force_snap_drop[key2] = [] + + def set_force_vertex_connection( + self, name1, name2, vertex1, vertex2, drop_vertex=2 + ): + key1 = (name1, name2) + key2 = (name2, name1) + if key1 not in self.force_snap: + self.force_snap[key1] = [] + self.force_snap[key2] = [] + self.force_snap_drop[key1] = [] + self.force_snap_drop[key2] = [] + + drop_vertex_inv = 1 if drop_vertex == 2 else 2 + + self.force_snap[key1][0].append(vertex1) + self.force_snap[key1][1].append(vertex2) + + self.force_snap[key2][0].append(vertex2) + self.force_snap[key2][1].append(vertex1) + + self.force_snap_drop[key1].append(drop_vertex) + self.force_snap_drop[key2].append(drop_vertex_inv) + + def _get_vertex_xy(self, name, iv): + vertices = self.get_grid(name).vertices + for iv_orig, xv, yv in vertices: + if iv == iv_orig: + return xv, yv + raise RuntimeError( + "DisvGridMerger: " f"Failed to find vertex {iv} in grid {name}" + ) + + def _find_merged_vertex(self, xv, yv, tol): + for iv, xv_chk, yv_chk in self.merged.vertices: + if abs(xv - xv_chk) + abs(yv - yv_chk) < tol: + return iv + return None + + def _replace_vertex_xy(self, iv, xv, yv): + for vert in self.merged.vertices: + if iv == vert[0]: + vert[1] = xv + vert[2] = yv + return + raise RuntimeError( + "DisvGridMerger: Unknown code error - " f"failed to locate vertex {iv}" + ) + + def _clear_attribute(self): + self.snap_order.clear() + self.force_snap_cellid.clear() + + self.merged.nlay = 0 + self.merged.nvert = 0 + self.merged.ncpl = 0 + self.merged.cell2d.clear() + self.merged.vertices.clear() + self.merged.top = np.array(()) + self.merged.botm.clear() + + self.cell2name.clear() + self.name2cell.clear() + self.vert2name.clear() + self.name2vert.clear() + + def _grid_snap_order(self): + # grids are snapped to one main grid using key = (name1, name2). + # it is required that at least name1 or name2 already be defined + # in the main grid. Function determines order to ensure this. + # snap_list -> List of (name1, name2) to parse + snap_list = list(self.snap_vertices.keys()) + name_used = {snap_list[0][0]} # First grid name always used first + snap_order = [] # Final order to build main grid + snap_append = [] # key's that need to be parsed again + loop_limit = 50 + loop = 0 + error_msg = ( + "\nDisvGridMerger:\n" + "Failed to find grid snap order.\n" + "Snapping must occur in contiguous steps " + "to the same main, merged grid.\n" + ) + while len(snap_list) > 0: + key = snap_list.pop(0) + name1, name2 = key + has_name1 = name1 in name_used + has_name2 = name2 in name_used + + if has_name1 and has_name2: # grid snapped to main twice + raise RuntimeError( + error_msg + "Once a grid name has been snapped to " + "the main grid,\n" + "it cannot be snapped again.\n" + f"The current snap order determined is:\n\n" + f"{snap_order}\n\n" + "but the following two grids were already " + "snapped to the main grid:\n" + f"{name1}\nand\n{name2}\n" + ) + + if has_name1 or has_name2: # have a name to snap too + snap_order.append(key) + name_used.add(name1) + name_used.add(name2) + else: # neither name found, so save for later + snap_append.append(key) + + if len(snap_list) == 0 and len(snap_append) > 0: + snap_list.extend(snap_append) + snap_append.clear() + loop += 1 + if loop > loop_limit: + raise RuntimeError( + error_msg + "Determined snap order for the " + "main grid is:\n\n" + f"{snap_order}\n\n" + "but failed to snap the following " + "to the main grid:\n\n" + f"{snap_list}" + ) + return snap_order + + def merge_grids(self): + self._clear_attribute() + + # First grid is unchanged, all other grids are changed as they + # snap to the final merged grid. + name1 = next(iter(self.snap_vertices.keys()))[0] + + cell2d = self.merged.cell2d + vertices = self.merged.vertices + + cell2d.extend(copy.deepcopy(self.grids[name1].cell2d)) + vertices.extend(copy.deepcopy(self.grids[name1].vertices)) + + for ic, *_ in cell2d: + self.cell2name[ic] = (name1, ic) + self.name2cell[(name1, ic)] = ic + + for iv, *_ in vertices: + self.vert2name[iv] = [(name1, iv)] + self.name2vert[(name1, iv)] = iv + + ic_new = ic # Last cell2d id from previous-previous loop + iv_new = iv # Last vertex id from previous loop + snapped = {name1} + force_snapped = set() + for key in self._grid_snap_order(): # Loop through vertices to snap + tol = self.connect_tolerance[key] + v1, v2 = self.snap_vertices[key] + name1, name2 = key + if name2 in snapped: + name1, name2 = name2, name1 + v1, v2 = v2, v1 + + if name1 not in self.snap_order: + self.snap_order.append(name1) + if name2 not in self.snap_order: + self.snap_order.append(name2) + + v1_orig = v1 + v1 = self.name2vert[(name1, v1_orig)] + + v1x, v1y = self._get_vertex_xy("__main__", v1) + v2x, v2y = self._get_vertex_xy(name2, v2) + + difx = v1x - v2x + dify = v1y - v2y + + for v2_orig, xv, yv in self.grids[name2].vertices: + xv += difx + yv += dify + + if ( + key in self.force_snap + and v2_orig in self.force_snap[key][1] # force snap vertex + ): + ind = self.force_snap[key][1].index(v2_orig) + v1_orig_force = self.force_snap[key][0][ind] + iv = self.name2vert[(name1, v1_orig_force)] + if self.force_snap_drop[key] == 1: + # replace v1 vertex with the x,y from v2 to force snap + self._replace_vertex_xy(iv, xv, yv) + force_snapped.add((name1, v1_orig_force)) + else: + force_snapped.add((name2, v2_orig)) + else: + iv = self._find_merged_vertex(xv, yv, tol) + + if iv is None: + iv_new += 1 + vertices.append([iv_new, xv, yv]) + self.vert2name[iv_new] = [(name2, v2_orig)] + self.name2vert[(name2, v2_orig)] = iv_new + else: + self.vert2name[iv].append((name2, v2_orig)) + self.name2vert[(name2, v2_orig)] = iv + + # Loop through cells and update center point and icvert + for ic, xc, yc, ncvert, *icvert in self.grids[name2].cell2d: + ic_new += 1 + xc += difx + yc += dify + + self.cell2name[ic_new] = (name2, ic) + self.name2cell[(name2, ic)] = ic_new + + item = [ic_new, xc, yc, ncvert] # append new icvert's + for iv_orig in icvert: + item.append(self.name2vert[(name2, iv_orig)]) # = iv_new + cell2d.append(item) + + # Force snapped cells need to update cell center and check for + # errors in the grid + for name, iv_orig in force_snapped: + for ic, _, _, _, *icvert in self.grids[name].cell2d: + if iv_orig in icvert: # cell was deformed by force snap + ic_new = self.name2cell[(name, ic)] + self.force_snap_cellid.add(ic_new) + + if len(self.force_snap_cellid) > 0: + dist = lambda v1, v2: sqrt((v2[1] - v1[1]) ** 2 + (v2[2] - v1[2]) ** 2) + mg = self.merged + vert_xy = np.array([(x, y) for _, x, y in mg.vertices]) + for ic in self.force_snap_cellid: + _, _, _, _, *vert = mg.cell2d[ic] + if len(vert) != len(set(vert)): # contains a duplicate vertex + seen = set() + seen_add = seen.add + vert = [v for v in vert if not (v in seen or seen_add(v))] + tmp = mg.cell2d[ic] + tmp[3] = len(vert) + mg.cell2d[ic] = tmp[:4] + vert + # check if vertices are within cell. + cell = [mg.vertices[iv][1:] for iv in vert] + path = mpltPath.Path(cell) + contain = np.where(path.contains_points(vert_xy))[0] + for iv in contain: + # find closest polyline + if iv in vert: + continue + vert_check = mg.vertices[iv] # vert_xy[iv, :] + d = np.inf + v_closest = -1 + for v in vert: + d2 = dist(vert_check, mg.vertices[v]) + if d > d2: + d = d2 + v_closest = v + ind = vert.index(v_closest) + if ind == len(vert) - 1: # Closest is at the end + d1 = dist(vert_check, mg.vertices[vert[0]]) + d2 = dist(vert_check, mg.vertices[vert[-2]]) + if d1 < d2: + ind = len(vert) + elif ind == 0: # Closest is at the start check end members + d1 = dist(vert_check, mg.vertices[vert[-1]]) + d2 = dist(vert_check, mg.vertices[vert[1]]) + if d2 < d1: + ind = 1 + else: + d1 = dist(vert_check, mg.vertices[vert[ind - 1]]) + d2 = dist(vert_check, mg.vertices[vert[ind + 1]]) + if d2 < d1: + ind += 1 + + # update cell2d for cell ic + vert.insert(ind, iv) + tmp = mg.cell2d[ic] + tmp[3] = len(vert) + # update cell center + tmp[1], tmp[2] = mg.get_centroid(vert) + mg.cell2d[ic] = tmp[:4] + vert + + self.merged.nvert = len(vertices) + self.merged.ncpl = len(cell2d) + + nlay = 0 + for name in self.snap_order: + if nlay < self.grids[name].nlay: + nlay = self.grids[name].nlay + self.merged.nlay = nlay + + top = [] + for name in self.snap_order: + top.extend(self.grids[name].top) + self.merged.top = np.array(top) + + for lay in range(nlay): + bot = [] + for name in self.snap_order: + if lay < self.grids[name].nlay: + bot.extend(self.grids[name].botm[lay]) + else: + bot.extend(self.grids[name].botm[-1]) + self.merged.botm.append(bot) + + def plot_grid( + self, + name="__main__", + title="", + plot_time=0.0, + show=True, + figsize=(10, 10), + dpi=None, + xlabel="", + ylabel="", + cell2d_override=None, + vertices_override=None, + ax_override=None, + cell_dot=True, + cell_num=True, + cell_dot_size=7.5, + vertex_dot=True, + vertex_num=True, + vertex_dot_size=6.5, + ): + return self.get_grid(name).plot_grid( + title, + plot_time, + show, + figsize, + dpi, + xlabel, + ylabel, + cell2d_override, + vertices_override, + ax_override, + cell_dot, + cell_num, + cell_dot_size, + vertex_dot, + vertex_num, + vertex_dot_size, + ) + + +class DisvCurvilinearBuilder(DisvPropertyContainer): + """ + A class for generating a curvilinear MODFLOW 6 DISV grid. A curvilinear + grid is similar to a radial grid, composed of radial bands, but includes + ncol discretization within a radial band and does not have to form an + entire circle (such as, a discretized wedge). + + This class inherits from the `DisvPropertyContainer` class and provides + methods to generate a curvilinear grid using radial and angular parameters. + + All indices are zero-based, but translated to one-base for the figures and + by flopy for use with MODFLOW 6. Angles are in degrees, with ``0`` being in + the positive x-axis direction and ``90`` in the positive y-axis direction. + + If no arguments are provided then an empty object is returned. + + Parameters + ---------- + nlay : int + Number of layers + radii : array_like + List of radial distances that describe the radial bands. + The first radius is the innermost radius, and then the rest are the + outer radius of each radial band. Note that the number of radial bands + is equal to ``len(radii) - 1``. + angle_start : float + Starting angle in degrees for the curvilinear grid. + angle_stop : float + Stopping angle in degrees for the curvilinear grid. + angle_step : float + Column discretization of each radial band. + If positive, then represents the angle step in degrees for each column + in a radial band. That is, the number of columns (`ncol`) is: + ``ncol = (angle_stop - angle_start)/angle_step`` + If negative, then the absolute value is the number of columns (ncol). + surface_elevation : float or array_like + Surface elevation for the top layer. Can either be a single float + for the entire `top`, or array_like of length `nradial`, or + array_like of length `ncpl`. + layer_thickness : float or array_like + Thickness of each layer. Can either be a single float + for model cells, or array_like of length `nlay`, or + array_like of length `ncpl`. + single_center_cell : bool, default=False + If True, include a single center cell. If true, then innermost `radii` + must be **zero**. That is, the innermost, radial band has ``ncol=1``. + origin_x : float, default=0.0 + X-coordinate reference point for the `radii` distance. + origin_y : float, default=0.0 + Y-coordinate reference point for the `radii` distance. + + Attributes + ---------- + nradial : int + Number of radial bands in the grid. + ncol : int + Number of columns in each radial band. + inner_vertex_count : int + Number of vertices in the innermost radial band. + single_center_cell : bool + Whether a single center cell is included. + full_circle : bool + Whether the grid spans a full circle. That is, + full_circle = `angle_start`==`angle_stop`==``0``). + radii : numpy.ndarray + Array of radial distances from (origin_x, origin_y) for each radial + band. The first value is the innermost radius and the remaining are + each radial bands outer radius. + angle_start : float + Starting angle in degrees for the curvilinear grid. + angle_stop : float + Stopping angle in degrees for the curvilinear grid. + angle_step : float + Angle step in degrees for each column in a radial band. + angle_span : float + Span of the angle range in degrees for the curvilinear grid. + + Methods + ------- + get_disv_kwargs() + Get the keyword arguments for creating a MODFLOW-6 DISV package. + plot_grid(...) + Plot the model grid from `vertices` and `cell2d` attributes. + get_cellid(rad, col, col_check=True) + Get the cellid given the radial and column indices. + get_rad_col(cellid) + Get the radial and column indices given the cellid. + get_vertices(rad, col) + Get the vertex indices for a cell given the radial and column indices. + calc_curvilinear_ncol(angle_start, angle_stop, angle_step) + Calculate the number of columns in the curvilinear grid based on + the given angle parameters. It will adjust `angle_step` to ensure + that the number of columns is an integer value. + iter_rad_col() + Iterate through the radial band columns, then bands. + iter_radial_cellid(rad) + Iterate through the cellid within a radial band. + iter_column_cellid(col) + Iterate through the cellid along a column across all radial bands. + """ + + nradial: int + ncol: int + inner_vertex_count: int + single_center_cell: bool + full_circle: bool + radii: np.ndarray + angle_start: float + angle_stop: float + angle_step: float + angle_span: float + + def __init__( + self, + nlay=-1, + radii=np.array((0.0, 1.0)), + angle_start=0.0, + angle_stop=90.0, + angle_step=-1, + surface_elevation=100.0, + layer_thickness=100.0, + single_center_cell=False, + origin_x=0.0, + origin_y=0.0, + ): + if nlay is None or nlay < 1: + self._init_empty() + return + + if angle_start < 0.0: + angle_start += 360.0 + if angle_stop < 0.0: + angle_stop += 360.0 + if abs(angle_step) < 1.0e-30: + raise RuntimeError("DisvCurvilinearBuilder: angle_step is near zero") -sys.path.append(os.path.join("..", "common")) + angle_span = self._get_angle_span(angle_start, angle_stop) -# import common functionality + ncol, angle_step = self.calc_curvilinear_ncol( + angle_start, angle_stop, angle_step + ) -import config -from figspecs import USGSFigure -from DisvCurvilinearBuilder import DisvCurvilinearBuilder + if angle_step > 90.0: + angle_step = 90.0 + ncol, angle_step = self.calc_curvilinear_ncol( + angle_start, angle_stop, angle_step + ) + + if angle_span < angle_step: + raise RuntimeError( + "DisvCurvilinearBuilder: angle_step is greater than " + "the total angel, that is:\n" + "angle_step > |angle_stop - angle_start|\n" + f"{angle_step} > {angle_span}" + ) + + try: + nradial = len(radii) - 1 + except TypeError: + raise RuntimeError("DisvCurvilinearBuilder: radii must be list-like type") + + if nradial < 1: + raise RuntimeError( + "DisvCurvilinearBuilder: len(radii) must be greater than 1" + ) + + if single_center_cell and radii[0] > 1.0e-100: + raise RuntimeError( + "DisvCurvilinearBuilder: single_center_cell=True must " + "have the first radii be zero, that is: radii[0] = 0.0\n" + f"Input received radii[0]={radii[0]}" + ) + + full_circle = 359.999 < angle_span + nver = ncol if full_circle else ncol + 1 + + ncpl = ncol * nradial # Nodes per layer + if single_center_cell: + ncpl = (ncol * nradial) - ncol + 1 + + self.radii = np.array(radii, dtype=np.float64) + self.nradial = nradial + self.ncol = ncol + + self.single_center_cell = single_center_cell + self.full_circle = full_circle + + self.angle_start = angle_start + self.angle_stop = angle_stop + self.angle_step = angle_step + self.angle_span = angle_span + + cls_name = "DisvCurvilinearBuilder" + top = self._get_array(cls_name, surface_elevation, ncpl, nradial) + thick = self._get_array(cls_name, layer_thickness, nlay, ncpl * nlay) + + if top.size == nradial and nradial != ncpl: + tmp = [] + for it, rad in top: + if it == 0 and single_center_cell: + tmp.append(rad) + else: + tmp += ncol * [rad] + top = np.array(tmp) + del tmp + + bot = [] + + if thick.size == nlay: + for lay in range(nlay): + bot.append(top - thick[: lay + 1].sum()) + else: + st = 0 + sp = ncpl + bt = top.copy() + for lay in range(nlay): + bt -= thick[st:sp] + st, sp = sp, sp + ncpl + bot.append(bt) + + if single_center_cell and full_circle: + # Full, filled circle - No vertex at center + inner_vertex_count = 0 + elif self.radii[0] < 1.0e-100: + # Single point at circle center + inner_vertex_count = 1 + else: + # Innermost vertices are the same as outer bands + inner_vertex_count = nver + + self.inner_vertex_count = inner_vertex_count + + # Build the grid + + vertices = [] + iv = 0 + stp = np.radians(angle_step) # angle step in radians + + # Setup center vertex + if inner_vertex_count == 1: + vertices.append([iv, 0.0, 0.0]) # Single vertex at center + iv += 1 + + # Setup vertices + st = 0 if inner_vertex_count > 1 else 1 + for rad in self.radii[st:]: + ang = np.radians(angle_start) # angle start in radians + for it in range(nver): + xv = rad * np.cos(ang) + yv = rad * np.sin(ang) + vertices.append([iv, xv, yv]) + iv += 1 + ang += stp + + # cell2d: [icell2d, xc, yc, ncvert, icvert] + cell2d = [] + ic = 0 + for rad in range(nradial): + single_cell_rad0 = self.single_center_cell and rad == 0 + for col in range(ncol): + icvert = self.get_vertices(rad, col) + # xc, yc = get_cell_center(rad, col) + if single_cell_rad0: + xc, yc = 0.0, 0.0 + else: + xc, yc = self.get_centroid(icvert, vertices) + cell2d.append([ic, xc, yc, len(icvert), *icvert]) + ic += 1 + if single_cell_rad0: + break + + super().__init__(nlay, vertices, cell2d, top, bot, origin_x, origin_y) + + def __repr__(self): + return super().__repr__("DisvCurvilinearBuilder") + + def _init_empty(self): + super()._init_empty() + nul = np.array([]) + self.nradial = 0 + self.ncol = 0 + self.inner_vertex_count = 0 + self.single_center_cell = False + self.full_circle = False + self.radii = nul + self.angle_start = 0 + self.angle_stop = 0 + self.angle_step = 0 + self.angle_span = 0 + + def property_copy_to(self, DisvCurvilinearBuilderType): + if isinstance(DisvCurvilinearBuilderType, DisvCurvilinearBuilder): + super().property_copy_to(DisvCurvilinearBuilderType) + DisvCurvilinearBuilderType.nradial = self.nradial + DisvCurvilinearBuilderType.ncol = self.ncol + DisvCurvilinearBuilderType.full_circle = self.full_circle + DisvCurvilinearBuilderType.radii = self.radii + DisvCurvilinearBuilderType.angle_start = self.angle_start + DisvCurvilinearBuilderType.angle_stop = self.angle_stop + DisvCurvilinearBuilderType.angle_step = self.angle_step + DisvCurvilinearBuilderType.angle_span = self.angle_span + DisvCurvilinearBuilderType.inner_vertex_count = self.inner_vertex_count + DisvCurvilinearBuilderType.single_center_cell = self.single_center_cell + else: + raise RuntimeError( + "DisvCurvilinearBuilder.property_copy_to " + "can only copy to objects that inherit " + "properties from DisvCurvilinearBuilder" + ) + + def copy(self): + cp = DisvCurvilinearBuilder() + self.property_copy_to(cp) + return cp + + def get_cellid(self, rad, col, col_check=True): + """ + Get the cellid given the radial and column indices. + + Parameters + ---------- + rad : int + Radial index. + col : int + Column index. + col_check : bool, default=True + If True, than a RuntimeError error is raised for single_center_cell + grids with ``rad==0`` and ``col>0``. Otherwise, assumes ``col=0``. + + Returns + ------- + int + cellid index + """ + ncol = self.ncol + if self.single_center_cell: + # Have to account for only one cell at the center + if rad == 0 and col > 0: + if col_check: + raise RuntimeError("DisvCurvilinearBuilder: Bad rad and col given") + return 0 + # if rad == 0, then first cell and pos = 0 + # else account for inner cell, plus each ncol band + pos = 1 + ncol * (rad - 1) + col if rad > 0 else 0 + else: + pos = rad * ncol + col + + return pos + + def get_rad_col(self, cellid): + """ + Get the radial and column indices given the cellid. + + Parameters + ---------- + cellid : int + cellid index + + Returns + ------- + (int, int) + Radial index, Column index + """ + ncol = self.ncol + + if cellid < 1: + rad, col = 0, 0 + elif self.single_center_cell: + cellid -= 1 # drop out first radial band (single cell) + rad = cellid // ncol + 1 + col = cellid - ncol * (rad - 1) + else: + rad = cellid // ncol + col = cellid - ncol * rad + + return rad, col + + def get_vertices(self, rad, col): + """ + Get the vertex indices for a cell given the radial and column indices. + + Parameters + ---------- + rad : int + Radial index. + col : int + Column index. + + Returns + ------- + list[int] + List of vertex indices that define the cell at (rad, col). + """ + ivc = self.inner_vertex_count + full_circle = self.full_circle + ncol = self.ncol + nver = ncol if full_circle else ncol + 1 + + if rad == 0: # Case with no center point or single center point + if self.single_center_cell: + return [iv for iv in range(nver + ivc)][::-1] + elif ivc == 1: # Single center point + if full_circle and col == ncol - 1: + return [1, col + 1, 0] # [col+2-nver, col+1, 0] + return [col + 2, col + 1, 0] + elif full_circle and col == ncol - 1: + return [col + 1, nver + col, col, col + 1 - nver] + else: # Normal inner band + return [nver + col + 1, nver + col, col, col + 1] + + n = (rad - 1) * nver + ivc + + if full_circle and col == ncol - 1: + return [n + col + 1, n + nver + col, n + col, n + col + 1 - nver] + + return [n + nver + col + 1, n + nver + col, n + col, n + col + 1] + + def iter_rad_col(self): + """Generator that iterates through the radial band columns, then bands. + + Yields + ------- + (int, int) + radial band index, column index + """ + for cellid in range(self.ncpl): + yield self.get_rad_col(cellid) + + def iter_radial_cellid(self, rad): + """Generator that iterates through the cellid within a radial band. + + Parameters + ---------- + rad : int + Radial index. + + Yields + ------- + int + cellid index + """ + st = self.get_cellid(rad, 0) + if self.single_center_cell and rad == 0: + return iter([st]) + sp = self.get_cellid(rad, self.ncol - 1) + 1 + return iter(range(st, sp)) + + def iter_column_cellid(self, col): + """Generator that iterates through the cellid along a column across + all radial bands. + + Parameters + ---------- + col : int + Column index. + + Yields + ------- + int + cellid index + """ + rad = 0 + while rad < self.nradial: + yield self.get_cellid(rad, col) + rad += 1 + + def iter_columns(self, rad): + """Generator that iterates through the columns within a radial band. + + Parameters + ---------- + rad : int + Radial index. + + Yields + ------- + int + column index + """ + if self.single_center_cell and rad == 0: + return iter([0]) + return iter(range(0, self.ncol)) + + @staticmethod + def _get_angle_span(angle_start, angle_stop): + # assumes angles are between 0 and 360 + if abs(angle_stop - angle_start) < 0.001: # angle_stop == angle_start + return 360.0 + if angle_start < angle_stop: + return angle_stop - angle_start + return 360.0 - angle_start + angle_stop + + @staticmethod + def calc_curvilinear_ncol(angle_start, angle_stop, angle_step): + """ + Calculate the number of columns in the curvilinear grid based on + the given angle parameters. It will adjust `angle_step` to ensure + that the number of columns is an integer value. + + Parameters + ---------- + angle_start : float + Starting angle in degrees for the curvilinear grid. + angle_stop : float + Stopping angle in degrees for the curvilinear grid. + angle_step : float + If positive, then represents the largest angle step in degrees + for each column in a radial band. It may be reduced to make + the number of columns be a positive, integer. + If negative, then the absolute value is the number of columns + (ncol) and angle_step is calculated based on it. + + Returns + ------- + (int, float) + The number of columns in the curvilinear grid and the angle_step + that can reproduce the exact integer number. + """ + angle_span = DisvCurvilinearBuilder._get_angle_span(angle_start, angle_stop) + + if angle_step > 0.0: + ncol = int(angle_span // angle_step) + if (angle_span / angle_step) - ncol > 0.1: # error towards larger + ncol += 1 + else: + ncol = int(round(-1 * angle_step)) + angle_step = angle_span / ncol + return ncol, angle_step def analytical_model(r1, h1, r2, h2, r): @@ -43,66 +2175,48 @@ def analytical_model(r1, h1, r2, h2, r): return num / den -# Set default figure properties - -figure_size_grid = (6, 6) -figure_size_head = (7, 6) -figure_size_obsv = (6, 6) +# - -# Base simulation and model name and workspace - -ws = config.base_ws - -# Simulation name - -sim_name = "ex-gwf-curve-90" +# ### Define parameters +# +# Define model units, parameters and other settings. +# + # Model units - length_units = "feet" time_units = "days" -# Table Model Parameters - +# Model parameters _ = "Steady-State" # Simulation Type nper = 1 # Number of periods _ = 1 # Number of time steps - nlay = 1 # Number of layers nradial = 16 # Number of radial direction cells (radial bands) ncol = 18 # Number of columns in radial band (ncol) - _ = "0" # Degree angle of column 1 boundary _ = "90" # Degree angle of column ncol boundary _ = "5" # Degree angle width of each column - r_inner = 4 # Model inner radius ($ft$) r_outer = 20 # Model outer radius ($ft$) r_width = 1 # Model radial band width ($ft$) - surface_elevation = 10.0 # Top of the model ($ft$) model_base = 0.0 # Base of the model ($ft$) - Tran = 0.19 # Horizontal transmissivity ($ft^2/day$) k11 = 0.019 # Horizontal hydraulic conductivity ($ft/day$) - bc0 = 10 # Inner Constant Head Boundary ($ft$) _ = "3.334" # Outer Constant Head Boundary ($ft$) # Input specified in table as text bc1 = bc0 / 3 - angle_start = 0 angle_stop = 90 angle_step = 5 # Radius for each radial band. # First value is inner radius, the remaining are outer radii - radii = np.arange(r_inner, r_outer + r_width, r_width) # Get the curvilinear model properties and vertices - curvlin = DisvCurvilinearBuilder( nlay, radii, @@ -117,7 +2231,6 @@ def analytical_model(r1, h1, r2, h2, r): # Constant head boundary condition # Constant head is located along the innermost radial band (rad = 0) # and outermost radial band (rad = nradial-1) - chd_inner = [] chd_outer = [] for lay in range(nlay): @@ -128,229 +2241,200 @@ def analytical_model(r1, h1, r2, h2, r): chd_outer.append([(lay, node), bc1]) chd_inner = {sp: chd_inner for sp in range(nper)} - chd_outer = {sp: chd_outer for sp in range(nper)} # Static temporal data used by TDIS file # Simulation is steady state so setup only a one day stress period. - tdis_ds = ((1.0, 1, 1),) # Solver parameters - nouter = 500 ninner = 300 hclose = 1e-4 rclose = 1e-4 +# - - -# ### Functions to build, write, run, and plot the MODFLOW 6 Curvilinear Model +# ### Model setup # -# MODFLOW 6 flopy simulation object (sim) is returned if building the model - - -def build_model(name): - if config.buildModel: - sim_ws = os.path.join(ws, name) - sim = flopy.mf6.MFSimulation( - sim_name=name, sim_ws=sim_ws, exe_name="mf6" - ) - flopy.mf6.ModflowTdis( - sim, nper=nper, perioddata=tdis_ds, time_units=time_units - ) - flopy.mf6.ModflowIms( - sim, - print_option="summary", - complexity="complex", - outer_maximum=nouter, - outer_dvclose=hclose, - inner_maximum=ninner, - inner_dvclose=hclose, - ) - - gwf = flopy.mf6.ModflowGwf(sim, modelname=name, save_flows=True) - - # **curvlin is an alias for **curvlin.disv_kw - disv = flopy.mf6.ModflowGwfdisv( - gwf, length_units=length_units, **curvlin - ) - - npf = flopy.mf6.ModflowGwfnpf( - gwf, - k=k11, - k33=k11, - save_flows=True, - save_specific_discharge=True, - ) +# Define functions to build models, write input files, and run the simulation. + + +# + +def build_models(name): + sim_ws = os.path.join(workspace, name) + sim = flopy.mf6.MFSimulation(sim_name=name, sim_ws=sim_ws, exe_name="mf6") + flopy.mf6.ModflowTdis(sim, nper=nper, perioddata=tdis_ds, time_units=time_units) + flopy.mf6.ModflowIms( + sim, + print_option="summary", + complexity="complex", + outer_maximum=nouter, + outer_dvclose=hclose, + inner_maximum=ninner, + inner_dvclose=hclose, + ) - flopy.mf6.ModflowGwfsto( - gwf, - iconvert=0, - steady_state=True, - save_flows=True, - ) + gwf = flopy.mf6.ModflowGwf(sim, modelname=name, save_flows=True) - flopy.mf6.ModflowGwfic(gwf, strt=surface_elevation) + # **curvlin is an alias for **curvlin.disv_kw + disv = flopy.mf6.ModflowGwfdisv(gwf, length_units=length_units, **curvlin) - flopy.mf6.ModflowGwfchd( - gwf, - stress_period_data=chd_inner, - pname="CHD-INNER", - filename=f"{sim_name}.inner.chd", - save_flows=True, - ) - flopy.mf6.ModflowGwfchd( - gwf, - stress_period_data=chd_outer, - pname="CHD-OUTER", - filename=f"{sim_name}.outer.chd", - save_flows=True, - ) + npf = flopy.mf6.ModflowGwfnpf( + gwf, + k=k11, + k33=k11, + save_flows=True, + save_specific_discharge=True, + ) - flopy.mf6.ModflowGwfoc( - gwf, - budget_filerecord=f"{name}.cbc", - head_filerecord=f"{name}.hds", - headprintrecord=[ - ("COLUMNS", nradial, "WIDTH", 15, "DIGITS", 6, "GENERAL") - ], - saverecord=[("HEAD", "ALL"), ("BUDGET", "ALL")], - printrecord=[("HEAD", "ALL"), ("BUDGET", "ALL")], - filename=f"{name}.oc", - ) + flopy.mf6.ModflowGwfsto( + gwf, + iconvert=0, + steady_state=True, + save_flows=True, + ) - return sim - return None + flopy.mf6.ModflowGwfic(gwf, strt=surface_elevation) + flopy.mf6.ModflowGwfchd( + gwf, + stress_period_data=chd_inner, + pname="CHD-INNER", + filename=f"{sim_name}.inner.chd", + save_flows=True, + ) + flopy.mf6.ModflowGwfchd( + gwf, + stress_period_data=chd_outer, + pname="CHD-OUTER", + filename=f"{sim_name}.outer.chd", + save_flows=True, + ) -# Function to write model files + flopy.mf6.ModflowGwfoc( + gwf, + budget_filerecord=f"{name}.cbc", + head_filerecord=f"{name}.hds", + headprintrecord=[("COLUMNS", nradial, "WIDTH", 15, "DIGITS", 6, "GENERAL")], + saverecord=[("HEAD", "ALL"), ("BUDGET", "ALL")], + printrecord=[("HEAD", "ALL"), ("BUDGET", "ALL")], + filename=f"{name}.oc", + ) + return sim -def write_model(sim, silent=True): - if config.writeModel: - sim.write_simulation(silent=silent) +def write_models(sim, silent=True): + sim.write_simulation(silent=silent) -# Function to run the curvilinear model. -# True is returned if the model runs successfully. +@timed +def run_models(sim, silent=True): + success, buff = sim.run_simulation(silent=silent, report=True) + assert success, buff -@config.timeit -def run_model(sim, silent=True): - success = True - if config.runModel: - success, buff = sim.run_simulation(silent=silent, report=True) - if not success: - print("\n".join(buff)) - return success +# - +# ### Plotting results +# +# Define functions to plot model results. -# Function to plot the curvilinear model grid. +# + +# Figure properties +figure_size_grid = (6, 6) +figure_size_head = (7, 6) +figure_size_obsv = (6, 6) def plot_grid(sim, verbose=False): - fs = USGSFigure(figure_type="map", verbose=verbose) - gwf = sim.get_model(sim_name) - - fig = plt.figure(figsize=figure_size_grid) - - ax = fig.add_subplot(1, 1, 1, aspect="equal") - pmv = flopy.plot.PlotMapView(model=gwf, ax=ax, layer=0) - pmv.plot_grid() - pmv.plot_bc(name="CHD-INNER", alpha=0.75, color="blue") - pmv.plot_bc(name="CHD-OUTER", alpha=0.75, color="blue") - ax.set_xlabel("x position (ft)") - ax.set_ylabel("y position (ft)") - for i, (x, y) in enumerate( - zip(gwf.modelgrid.xcellcenters, gwf.modelgrid.ycellcenters) - ): - ax.text( - x, - y, - f"{i + 1}", - fontsize=6, - horizontalalignment="center", - verticalalignment="center", - ) - v = gwf.disv.vertices.array - ax.plot(v["xv"], v["yv"], "yo") - for i in range(v.shape[0]): - x, y = v["xv"][i], v["yv"][i] - ax.text( - x, - y, - f"{i + 1}", - fontsize=5, - color="red", - horizontalalignment="center", - verticalalignment="center", - ) - - fig.tight_layout() - - # save figure - if config.plotSave: - fpth = os.path.join( - "..", "figures", f"{sim_name}-grid{config.figure_ext}" - ) - fig.savefig(fpth) - return - - -# Function to plot the curvilinear model results. + with styles.USGSMap(): + gwf = sim.get_model(sim_name) + + fig = plt.figure(figsize=figure_size_grid) + + ax = fig.add_subplot(1, 1, 1, aspect="equal") + pmv = flopy.plot.PlotMapView(model=gwf, ax=ax, layer=0) + pmv.plot_grid() + pmv.plot_bc(name="CHD-INNER", alpha=0.75, color="blue") + pmv.plot_bc(name="CHD-OUTER", alpha=0.75, color="blue") + ax.set_xlabel("x position (ft)") + ax.set_ylabel("y position (ft)") + for i, (x, y) in enumerate( + zip(gwf.modelgrid.xcellcenters, gwf.modelgrid.ycellcenters) + ): + ax.text( + x, + y, + f"{i + 1}", + fontsize=6, + horizontalalignment="center", + verticalalignment="center", + ) + v = gwf.disv.vertices.array + ax.plot(v["xv"], v["yv"], "yo") + for i in range(v.shape[0]): + x, y = v["xv"][i], v["yv"][i] + ax.text( + x, + y, + f"{i + 1}", + fontsize=5, + color="red", + horizontalalignment="center", + verticalalignment="center", + ) + + fig.tight_layout() + + if plot_show: + plt.show() + if plot_save: + fpth = os.path.join("..", "figures", f"{sim_name}-grid.png") + fig.savefig(fpth) def plot_head(sim): - fs = USGSFigure(figure_type="map", verbose=False) - gwf = sim.get_model(sim_name) - - fig = plt.figure(figsize=figure_size_head) - - head = gwf.output.head().get_data()[:, 0, :] - - # create MODFLOW 6 cell-by-cell budget object - qx, qy, qz = flopy.utils.postprocessing.get_specific_discharge( - gwf.output.budget().get_data(text="DATA-SPDIS", totim=1.0)[0], - gwf, - ) + with styles.USGSMap(): + gwf = sim.get_model(sim_name) + fig = plt.figure(figsize=figure_size_head) + head = gwf.output.head().get_data()[:, 0, :] + + # create MODFLOW 6 cell-by-cell budget object + qx, qy, qz = flopy.utils.postprocessing.get_specific_discharge( + gwf.output.budget().get_data(text="DATA-SPDIS", totim=1.0)[0], + gwf, + ) - ax = fig.add_subplot(1, 1, 1, aspect="equal") - pmv = flopy.plot.PlotMapView(model=gwf, ax=ax, layer=0) - cb = pmv.plot_array(head, cmap="jet", vmin=0.0, vmax=head.max()) - pmv.plot_vector( - qx, - qy, - normalize=False, - color="0.75", - ) - cbar = plt.colorbar(cb, shrink=0.25) - cbar.ax.set_xlabel(r"Head, ($ft$)") - ax.set_xlabel("x position (ft)") - ax.set_ylabel("y position (ft)") + ax = fig.add_subplot(1, 1, 1, aspect="equal") + pmv = flopy.plot.PlotMapView(model=gwf, ax=ax, layer=0) + cb = pmv.plot_array(head, cmap="jet", vmin=0.0, vmax=head.max()) + pmv.plot_vector( + qx, + qy, + normalize=False, + color="0.75", + ) + cbar = plt.colorbar(cb, shrink=0.25) + cbar.ax.set_xlabel(r"Head, ($ft$)") + ax.set_xlabel("x position (ft)") + ax.set_ylabel("y position (ft)") - fig.tight_layout() + fig.tight_layout() - # save figure - if config.plotSave: - fpth = os.path.join( - "..", "figures", f"{sim_name}-head{config.figure_ext}" - ) - fig.savefig(fpth) - return + if plot_show: + plt.show() + if plot_save: + fpth = os.path.join("..", "figures", f"{sim_name}-head.png") + fig.savefig(fpth) def plot_analytical(sim, verbose=False): gwf = sim.get_model(sim_name) - head = gwf.output.head().get_data()[:, 0, :] - col = ncol // 2 - 1 # Get head along middle of model - head = [head[0, curvlin.get_cellid(rad, col)] for rad in range(nradial)] - xrad = [0.5 * (radii[r - 1] + radii[r]) for r in range(1, nradial + 1)] - analytical = [head[0]] r1 = xrad[0] r2 = xrad[-1] @@ -361,34 +2445,27 @@ def plot_analytical(sim, verbose=False): analytical.append(analytical_model(r1, h1, r2, h2, r)) analytical.append(head[-1]) - fs = USGSFigure(figure_type="graph", verbose=verbose) - - obs_fig = "obs-head" - fig = plt.figure(figsize=figure_size_obsv) - ax = fig.add_subplot() - ax.set_xlabel("Radial distance (ft)") - ax.set_ylabel("Head (ft)") - ax.plot(xrad, head, "ob", label="MF6 Solution", markerfacecolor="none") - ax.plot(xrad, analytical, "-b", label="Analytical Solution") - - fs.graph_legend(ax) - - fig.tight_layout() - - if config.plotSave: - fpth = os.path.join( - "..", - "figures", - "{}-{}{}".format(sim_name, obs_fig, config.figure_ext), - ) - fig.savefig(fpth) - - -# Function to plot the model results. + with styles.USGSPlot() as fs: + obs_fig = "obs-head" + fig = plt.figure(figsize=figure_size_obsv) + ax = fig.add_subplot() + ax.set_xlabel("Radial distance (ft)") + ax.set_ylabel("Head (ft)") + ax.plot(xrad, head, "ob", label="MF6 Solution", markerfacecolor="none") + ax.plot(xrad, analytical, "-b", label="Analytical Solution") + styles.graph_legend(ax) + fig.tight_layout() + if plot_save: + fpth = os.path.join( + "..", + "figures", + "{}-{}{}".format(sim_name, obs_fig, ".png"), + ) + fig.savefig(fpth) def plot_results(silent=True): - if not config.plotModel: + if not plot: return if silent: @@ -396,32 +2473,26 @@ def plot_results(silent=True): else: verbosity_level = 1 - sim_ws = os.path.join(ws, sim_name) + sim_ws = os.path.join(workspace, sim_name) sim = flopy.mf6.MFSimulation.load( sim_name=sim_name, sim_ws=sim_ws, verbosity_level=verbosity_level ) verbose = not silent - - if config.plotModel: - plot_grid(sim, verbose) - plot_head(sim) - plot_analytical(sim, verbose) - return + plot_grid(sim, verbose) + plot_head(sim) + plot_analytical(sim, verbose) def calculate_model_error(): - sim_ws = os.path.join(ws, sim_name) + sim_ws = os.path.join(workspace, sim_name) sim = flopy.mf6.MFSimulation.load( sim_name=sim_name, sim_ws=sim_ws, verbosity_level=0 ) gwf = sim.get_model(sim_name) - head = gwf.output.head().get_data()[0, 0, :] - xrad = [0.5 * (radii[r - 1] + radii[r]) for r in range(1, nradial + 1)] - analytical = [head[0]] r1 = xrad[0] r2 = xrad[-1] @@ -450,49 +2521,35 @@ def calculate_model_error(): def check_model_error(): - if config.runModel: - rel_error, rmse = calculate_model_error() - assert rel_error < 0.001 + rel_error, rmse = calculate_model_error() + assert rel_error < 0.001 -# Function that wraps all of the steps for the curvilinear model -# -# 1. build_model, -# 2. write_model, -# 3. run_model, and -# 4. plot_results. +# - + +# ### Running the example # +# Define and invoke a function to run the example scenario, then plot results. -def simulation(silent=True): +# + +def scenario(silent=True): # key = list(parameters.keys())[idx] # params = parameters[key].copy() + sim = build_models(sim_name) + if write: + write_models(sim, silent=silent) + if run: + run_models(sim, silent=silent) - sim = build_model(sim_name) - - write_model(sim, silent=silent) - - success = run_model(sim, silent=silent) - assert success, "could not run...{}".format(sim_name) - - -# nosetest - exclude block from this nosetest to the next nosetest -def test_and_plot(): - simulation(silent=False) - plot_results(silent=False) - return - - -# nosetest end - - -if __name__ == "__main__": - # ### Curvilinear Example - # MF6 Curvilinear Model - simulation() +# Run simulation +scenario() - # Solve analytical and plot results with MF6 results +if plot: + # Solve analytical solution and plot results with MF6 results plot_results() + # Check error check_model_error() +# - diff --git a/scripts/ex-gwf-curvilinear.py b/scripts/ex-gwf-curvilinear.py index a0f997363..097c6c5f9 100644 --- a/scripts/ex-gwf-curvilinear.py +++ b/scripts/ex-gwf-curvilinear.py @@ -14,93 +14,2200 @@ # 3) 90 to 0 degree curvilinear grid # that are merged, as 1-2-3, to make the final multipart curvilinear grid. -# Imports +# ### Initial setup +# +# ### Initial setup +# +# Import dependencies, define the example name and workspace, and read settings from environment variables. +# + +import copy import os -import sys -import numpy as np -import matplotlib.pyplot as plt +import pathlib as pl +from itertools import cycle + import flopy -from math import sqrt +import matplotlib.pyplot as plt +import numpy as np +from flopy.plot.styles import styles +from modflow_devtools.misc import get_env, timed -# Append to system path to include the common subdirectory +# Example name and base workspace +sim_name = "ex-gwf-curvilin" +workspace = pl.Path("../examples") -sys.path.append(os.path.join("..", "common")) +# Settings from environment variables +write = get_env("WRITE", True) +run = get_env("RUN", True) +plot = get_env("PLOT", True) +plot_show = get_env("PLOT_SHOW", True) +plot_save = get_env("PLOT_SAVE", True) +# - -# import common functionality +# ### Curvilinear grid +# +# Define some utilities to construct a curvilinear grid. + + +# + +class DisvPropertyContainer: + + """ + Dataclass that stores MODFLOW 6 DISV grid information. + + This is a base class that stores DISV **kwargs information used + by flopy for building a ``flopy.mf6.ModflowGwfdisv`` object. + + All indices are zero-based, but translated to one-base for the figures and + by flopy for use with MODFLOW 6. + + If no arguments are provided then an empty object is returned. + + Parameters + ---------- + nlay : int + Number of layers. + vertices : list[list[int, float, float]] + List of vertices structured as + ``[[iv, xv, vy], ...]`` + where + ``iv`` is the vertex index, + ``xv`` is the x-coordinate, and + ``yv`` is the y-coordinate. + cell2d : list[list[int, float, float, int, int...]] + List of MODFLOW 6 cells structured as + ```[[icell2d, xc, yc, ncvert, icvert], ...]``` + where + ``icell2d`` is the cell index, + ``xc`` is the x-coordinate for the cell center, + ``yc`` is the y-coordinate for the cell center, + ``ncvert`` is the number of vertices required to define the cell, + ``icvert`` is a list of vertex indices that define the cell, and + in clockwise order. + top : np.ndarray + Is the top elevation for each cell in the top model layer. + botm : list[np.ndarray] + List of bottom elevation by layer for all model cells. + origin_x : float, default=0.0 + X-coordinate of the origin used as the reference point for other + vertices. This is used for shift and rotate operations. + origin_y : float, default=0.0 + X-coordinate of the origin used as the reference point for other + vertices. This is used for shift and rotate operations. + rotation : float, default=0.0 + Rotation angle in degrees for the model grid. + shift_origin : bool, default=True + If True and `origin_x` or `origin_y` is non-zero, then all vertices are + shifted from an assumed (0.0, 0.0) origin to the (origin_x, origin_y) + location. + rotate_grid, default=True + If True and `rotation` is non-zero, then all vertices are rotated by + rotation degrees around (origin_x, origin_y). + + Attributes + ---------- + nlay : int + Number of layers. + ncpl : int + Number of cells per layer. + nvert : int + Number of vertices. + vertices : list[list] + List of vertices structured as ``[[iv, xv, vy], ...]`` + cell2d : list[list] + List of 2D cells structured as ```[[icell2d, xc, yc, ncvert, icvert], ...]``` + top : np.ndarray + Top elevation for each cell in the top model layer. + botm : list[np.ndarray] + List of bottom elevation by layer for all model cells. + origin_x : float + X-coordinate reference point used by grid. + origin_y : float + Y-coordinate reference point used by grid. + rotation : float + Rotation angle of grid about (origin_x, origin_y) + + Methods + ------- + get_disv_kwargs() + Get the keyword arguments for creating a MODFLOW-6 DISV package. + plot_grid(...) + Plot the model grid from `vertices` and `cell2d` attributes. + change_origin(new_x_origin, new_y_origin) + Change the origin of the grid. + rotate_grid(rotation) + Rotate the grid. + get_centroid(icvert, vertices=None) + Calculate the centroid of a cell given by list of vertices `icvert`. + copy() + Create and return a copy of the current object. + """ + + nlay: int + ncpl: int + nvert: int + vertices: list[list] # [[iv, xv, yv], ...] + cell2d: list[list] # [[ic, xc, yc, ncvert, icvert], ...] + top: np.ndarray + botm: list[np.ndarray] + origin_x: float + origin_y: float + rotation: float + + def __init__( + self, + nlay=-1, + vertices=None, + cell2d=None, + top=None, + botm=None, + origin_x=0.0, + origin_y=0.0, + rotation=0.0, + shift_origin=True, + rotate_grid=True, + ): + if nlay is None or nlay < 1: + self._init_empty() + return + + self.nlay = nlay + self.ncpl = len(cell2d) + self.nvert = len(vertices) + + self.vertices = [] if vertices is None else copy.deepcopy(vertices) + self.cell2d = [] if cell2d is None else copy.deepcopy(cell2d) + self.top = np.array([]) if top is None else copy.deepcopy(top) + self.botm = [] if botm is None else copy.deepcopy(botm) + + self.origin_x, self.origin_y, self.rotation = 0.0, 0.0, 0.0 + + if shift_origin: + if abs(origin_x) > 1.0e-30 or abs(origin_y) > 1.0e-30: + self.change_origin(origin_x, origin_y) + elif not shift_origin: + self.origin_x, self.origin_y = origin_x, origin_y + + if rotate_grid: + self.rotate_grid(rotation) + elif not shift_origin: + self.rotation = rotation + + def get_disv_kwargs(self): + """ + Get the dict of keyword arguments for creating a MODFLOW-6 DISV + package using ``flopy.mf6.ModflowGwfdisv``. + """ + return { + "nlay": self.nlay, + "ncpl": self.ncpl, + "top": self.top, + "botm": self.botm, + "nvert": self.nvert, + "vertices": self.vertices, + "cell2d": self.cell2d, + } + + def __repr__(self, cls="DisvPropertyContainer"): + return ( + f"{cls}(\n\n" + f"nlay={self.nlay}, ncpl={self.ncpl}, nvert={self.nvert}\n\n" + f"origin_x={self.origin_x}, origin_y={self.origin_y}, " + f"rotation={self.rotation}\n\n" + f"vertices =\n{self._string_repr(self.vertices)}\n\n" + f"cell2d =\n{self._string_repr(self.cell2d)}\n\n" + f"top =\n{self.top}\n\n" + f"botm =\n{self.botm}\n\n)" + ) -import config -from figspecs import USGSFigure + def _init_empty(self): + self.nlay = 0 + self.ncpl = 0 + self.nvert = 0 + self.vertices = [] + self.cell2d = [] + self.top = np.array([]) + self.botm = [] + self.origin_x = 0.0 + self.origin_y = 0.0 + self.rotation = 0.0 + + def change_origin(self, new_x_origin, new_y_origin): + shift_x_origin = new_x_origin - self.origin_x + shift_y_origin = new_y_origin - self.origin_y + + self.shift_origin(shift_x_origin, shift_y_origin) + + def shift_origin(self, shift_x_origin, shift_y_origin): + if abs(shift_x_origin) > 1.0e-30 or abs(shift_y_origin) > 1.0e-30: + self.origin_x += shift_x_origin + self.origin_y += shift_y_origin + + for vert in self.vertices: + vert[1] += shift_x_origin + vert[2] += shift_y_origin + + for cell in self.cell2d: + cell[1] += shift_x_origin + cell[2] += shift_y_origin + + def rotate_grid(self, rotation): + """Rotate grid around origin_x, origin_y for given angle in degrees. + + References + ---------- + [1] https://en.wikipedia.org/wiki/Transformation_matrix#Rotation + + """ + # + if abs(rotation) > 1.0e-30: + self.rotation += rotation + + sin, cos = np.sin, np.cos + a = np.radians(rotation) + x0, y0 = self.origin_x, self.origin_y + # Steps to shift + # 0) Get x, y coordinate to be shifted + # 1) Shift coordinate's reference point to origin + # 2) Rotate around origin + # 3) Shift back to original reference point + for vert in self.vertices: + _, x, y = vert + x, y = x - x0, y - y0 + x, y = x * cos(a) - y * sin(a), x * sin(a) + y * cos(a) + vert[1] = x + x0 + vert[2] = y + y0 + + for cell in self.cell2d: + _, x, y, *_ = cell + x, y = x - x0, y - y0 + x, y = x * cos(a) - y * sin(a), x * sin(a) + y * cos(a) + cell[1] = x + x0 + cell[2] = y + y0 + + @staticmethod + def _string_repr(list_list, sep=",\n"): + dim = len(list_list) + s = [] + if dim == 0: + return "[]" + if dim < 7: + for elm in list_list: + s.append(repr(elm)) + else: + for it in range(3): + s.append(repr(list_list[it])) + s.append("...") + for it in range(-3, 0): + s.append(repr(list_list[it])) + return sep.join(s) + + def property_copy_to(self, DisvPropertyContainerType): + if isinstance(DisvPropertyContainerType, DisvPropertyContainer): + DisvPropertyContainerType.nlay = self.nlay + DisvPropertyContainerType.ncpl = self.ncpl + DisvPropertyContainerType.nvert = self.nvert + DisvPropertyContainerType.vertices = self.vertices + DisvPropertyContainerType.cell2d = self.cell2d + DisvPropertyContainerType.top = self.top + DisvPropertyContainerType.botm = self.botm + DisvPropertyContainerType.origin_x = self.origin_x + DisvPropertyContainerType.origin_y = self.origin_y + DisvPropertyContainerType.rotation = self.rotation + else: + raise RuntimeError( + "DisvPropertyContainer.property_copy_to " + "can only copy to objects that inherit " + "properties from DisvPropertyContainer" + ) + + def copy(self): + cp = DisvPropertyContainer() + self.property_copy_to(cp) + return cp + + def keys(self): + """ + Get the keys used by ``flopy.mf6.ModflowGwfdisv``. + + This method is only used to provide unpacking support for + `DisvPropertyContainer` objects and subclasses. + + That is: + ``flopy.mf6.ModflowGwfdisv(gwf, **DisvPropertyContainer)`` + + Returns + ------- + list + List of keys used by ``flopy.mf6.ModflowGwfdisv`` + + """ + return self.get_disv_kwargs().keys() + + def __getitem__(self, k): + if hasattr(self, k): + return getattr(self, k) + raise KeyError(f"{k}") + + @staticmethod + def _get_array(cls_name, var, rep, rep2=None): + if rep2 is None: + rep2 = rep + + try: + dim = len(var) + except TypeError: + dim, var = 1, [var] + + try: # Check of array needs to be flattened + _ = len(var[0]) + tmp = [] + for row in var: + tmp.extend(row) + var = tmp + dim = len(var) + except TypeError: + pass + + if dim != 1 and dim != rep and dim != rep2: + msg = f"{cls_name}(var): var must be a scalar " + msg += f"or have len(var)=={rep}" + if rep2 != rep: + msg += f"or have len(var)=={rep2}" + raise IndexError(msg) + + if dim == 1: + return np.full(rep, var[0], dtype=np.float64) + else: + return np.array(var, dtype=np.float64) + + def get_centroid(self, icvert, vertices=None): + """ + Calculate the centroid of a cell for a given set of vertices. + + Parameters + ---------- + icvert : list[int] + List of vertex indices for the cell. + vertices : list[list], optional + List of vertices that `icvert` references to define the cell. + If not present, then the `vertices` attribute is used. + + Returns + ------- + tuple + A tuple containing the X and Y coordinates of the centroid. + + References + ---------- + [1] https://en.wikipedia.org/wiki/Centroid#Of_a_polygon + """ + if vertices is None: + vertices = self.vertices + + nv = len(icvert) + x = [] + y = [] + for iv in icvert: + x.append(vertices[iv][1]) + y.append(vertices[iv][2]) + + if nv < 3: + raise RuntimeError("get_centroid: len(icvert) < 3") + + if nv == 3: # Triangle + return sum(x) / 3, sum(y) / 3 + + xc, yc = 0.0, 0.0 + signedArea = 0.0 + for i in range(nv - 1): + x0, y0, x1, y1 = x[i], y[i], x[i + 1], y[i + 1] + a = x0 * y1 - x1 * y0 + signedArea += a + xc += (x0 + x1) * a + yc += (y0 + y1) * a + + x0, y0, x1, y1 = x1, y1, x[0], y[0] + + a = x0 * y1 - x1 * y0 + signedArea += a + xc += (x0 + x1) * a + yc += (y0 + y1) * a + + signedArea *= 0.5 + return xc / (6 * signedArea), yc / (6 * signedArea) + + def plot_grid( + self, + title="", + plot_time=0.0, + show=True, + figsize=(10, 10), + dpi=None, + xlabel="", + ylabel="", + cell2d_override=None, + vertices_override=None, + ax_override=None, + cell_dot=True, + cell_num=True, + cell_dot_size=7.5, + cell_dot_color="coral", + vertex_dot=True, + vertex_num=True, + vertex_dot_size=6.5, + vertex_dot_color="skyblue", + grid_color="grey", + ): + """ + Plot the model grid with optional features. + All inputs are optional. + + Parameters + ---------- + title : str, default="" + Title for the plot. + plot_time : float, default=0.0 + Time interval for animation (if greater than 0). + show : bool, default=True + Whether to display the plot. If false, then plot_time is set to -1 + and function returns figure and axis objects. + figsize : tuple, default=(10, 10) + Figure size (width, height) in inches. Default is (10, 10). + dpi : float, default=None + Set dpi for Matplotlib figure. + If set to None, then uses Matplotlib default dpi. + xlabel : str, default="" + X-axis label. + ylabel : str, default="" + Y-axis label. + cell2d_override : list[list], optional + List of ``cell2d`` cells to override the object's cell2d. + Default is None. + vertices_override : list[list], optional + List of vertices to override the object's vertices. + Default is None. + ax_override : matplotlib.axes.Axes, optional + Matplotlib axis object to use for generating plot instead of + making a new figure and axis objects. If present, then show is + set to False and plot_time to -1. + cell_dot : bool, default=True + Whether to add a filled circle at the cell center locations. + cell_num : bool, default=True + Whether to label cells with cell2d index numbers. + cell_dot_size : bool, default=7.5 + The size, in points, of the filled circles and index numbers + at the cell center. + cell_dot_color : str, default="coral" + The color of the filled circles at the cell center. + vertex_num : bool, default=True + Whether to label vertices with the vertex numbers. + vertex_dot : bool, default=True + Whether to add a filled circle at the vertex locations. + vertex_dot_size : bool, default=6.5 + The size, in points, of the filled circles and index numbers + at the vertex locations. + vertex_dot_color : str, default="skyblue" + The color of the filled circles at the vertex locations. + grid_color : str or tuple[str], default="grey" + The color of the grid lines. + If plot_time > 0, then animation cycled through the colors + for each cell outline. + + Returns + ------- + (matplotlib.figure.Figure, matplotlib.axis.Axis) or None + If `show` is False, returns the Figure and Axis objects; + otherwise, returns None. + + Raises + ------ + RuntimeError + If either `cell2d_override` or `vertices_override` is provided + without the other. + + Notes + ----- + This method plots the grid using Matplotlib. It can label cells and + vertices with numbers, show an animation of the plotting process, and + customize the plot appearance. + + Note that figure size (`figsize`) is in inches and the total pixels is based on + the `dpi` (dots per inch). For example, figsize=(3, 5) and + dpi=110, results in a figure resolution of (330, 550). + + Changing `figsize` does not effect the size + + Elements (text, markers, lines) in Matplotlib use + 72 points per inch (ppi) as a basis for translating to dpi. + Any changes to dpi result in a scaling effect. The default dpi of 100 + results in a line width 100/72 pixels wide. + + Similarly, a line width of 1 point with a dpi set to 72 results in + a line that is 1 pixel wide. The following then occurs: + + 2*72 dpi results in a line width 2 pixels; + 3*72 dpi results in a line width 3 pixels; and + 600 dpi results in a line width 600/72 pixels. + + Conversely, changing `figsize` increases the total pixel count, but + elements maintain the same dpi. That is the figure wireframe will be + larger, but the elements will have the same pixel widths. For example, + a line width of 1 will have a width of 100/72 in for any figure size, + as long as the dpi is set to 100. + """ + + if cell2d_override is not None and vertices_override is not None: + cell2d = cell2d_override + vertices = vertices_override + elif cell2d_override is not None or vertices_override is not None: + raise RuntimeError( + "plot_vertex_grid: if you specify " + "cell2d_override or vertices_override, " + "then you must specify both." + ) + else: + cell2d = self.cell2d + vertices = self.vertices + + if ax_override is None: + fig = plt.figure(figsize=figsize, dpi=dpi) + ax = fig.add_subplot() + else: + show = False + ax = ax_override + + ax.set_aspect("equal", adjustable="box") + + if not show: + plot_time = -1.0 + + if not isinstance(grid_color, tuple): + grid_color = (grid_color,) + + ColorCycler = grid_color + if plot_time > 0.0 and grid_color == ("grey",): + ColorCycler = ("green", "red", "grey", "magenta", "cyan", "yellow") + ColorCycle = cycle(ColorCycler) + + xvert = [] + yvert = [] + for r in vertices: + xvert.append(r[1]) + yvert.append(r[2]) + xcell = [] + ycell = [] + for r in cell2d: + xcell.append(r[1]) + ycell.append(r[2]) + + if title != "": + ax.set_title(title) + if xlabel != "": + ax.set_xlabel(xlabel) + if ylabel != "": + ax.set_ylabel(ylabel) + + vert_size = vertex_dot_size + cell_size = cell_dot_size + + if vertex_dot: + ax.plot( + xvert, + yvert, + linestyle="None", + color=vertex_dot_color, + markersize=vert_size, + marker="o", + markeredgewidth=0.0, + zorder=2, + ) + if cell_dot: + ax.plot( + xcell, + ycell, + linestyle="None", + color=cell_dot_color, + markersize=cell_size, + marker="o", + markeredgewidth=0.0, + zorder=2, + ) + + if cell_num: + for ic, xc, yc, *_ in cell2d: + ax.text( + xc, + yc, + f"{ic + 1}", + fontsize=cell_size, + color="black", + fontfamily="Arial Narrow", + fontweight="black", + rasterized=False, + horizontalalignment="center", + verticalalignment="center", + zorder=3, + ) -from DisvCurvilinearBuilder import DisvCurvilinearBuilder -from DisvStructuredGridBuilder import DisvStructuredGridBuilder -from DisvGridMerger import DisvGridMerger + if vertex_num: + for iv, xv, yv in vertices: + ax.text( + xv, + yv, + f"{iv + 1}", + fontsize=vert_size, + fontweight="black", + rasterized=False, + color="black", + fontfamily="Arial Narrow", + horizontalalignment="center", + verticalalignment="center", + zorder=3, + ) -# Set default figure properties + if plot_time > 0: + plt.show(block=False) + plt.pause(5 * plot_time) + + for ic, xc, yc, ncon, *vert in cell2d: + color = next(ColorCycle) + + conn = vert + [vert[0]] # Extra node to complete polygon + + for i in range(ncon): + n1, n2 = conn[i], conn[i + 1] + px = [vertices[n1][1], vertices[n2][1]] + py = [vertices[n1][2], vertices[n2][2]] + ax.plot(px, py, color=color, zorder=1) + + if plot_time > 0: + plt.draw() + plt.pause(plot_time) + + if show: + plt.show() + elif ax_override is None: + return fig, ax + + +class DisvStructuredGridBuilder(DisvPropertyContainer): + """ + A class for generating a structured MODFLOW 6 DISV grid. + + This class inherits from the `DisvPropertyContainer` class and provides + methods to generate a rectangular, structured grid give the number rows + (nrow), columns (ncol), row widths, and columns widths. Rows are + discretized along the y-axis and columns along the x-axis. The row, column + structure follows MODFLOW 6 structured grids. That is, the first row has + the largest y-axis vertices and last row the lowest; and the first column + has the lowest x-axis vertices and last column the highest. + + All indices are zero-based, but translated to one-base for the figures and + by flopy for use with MODFLOW 6. + + The following shows the placement for (row, column) pairs + in a nrow=3 and ncol=5 model: + + ``(0,0) (0,1) (0,2) (0,3) (0,4)`` + ``(1,0) (1,1) (1,2) (1,3) (1,4)`` + ``(2,0) (2,1) (2,2) (2,3) (2,4)`` + + Array-like structures that are multidimensional (has rows and columns) + are flatten by concatenating each row. Using the previous example, + the following is the flattened representation: + + ``(0,0) (0,1) (0,2) (0,3) (0,4) (1,0) (1,1) (1,2) (1,3) (1,4) (2,0) (2,1) (2,2) (2,3) (2,4)`` + + If no arguments are provided then an empty object is returned. + + Parameters + ---------- + nlay : int + Number of layers + nrow : int + Number of rows (y-direction cells). + ncol : int + Number of columns (x-direction cells). + row_width : float or array_like + Width of y-direction cells (each row). If a single value is provided, + it will be used for all rows. Otherwise, it must be array_like + of length ncol. + col_width : float or array_like + Width of x-direction cells (each column). If a single value is + provided, it will be used for all columns. Otherwise, it must be + array_like of length ncol. + surface_elevation : float or array_like + Surface elevation for the top layer. Can either be a single float + for the entire `top`, or array_like of length `ncpl`. + If it is a multidimensional array_like, then it is flattened to a + single dimension along the rows (first dimension). + layer_thickness : float or array_like + Thickness of each layer. Can either be a single float + for model cells, or array_like of length `nlay`, or + array_like of length `nlay`*`ncpl`. + origin_x : float, default=0.0 + X-coordinate reference point the lower-left corner of the model grid. + That is, the outermost corner of ``row=nrow-1` and `col=0`. + Rotations are performed around this point. + origin_y : float, default=0.0 + Y-coordinate reference point the lower-left corner of the model grid. + That is, the outermost corner of ``row=nrow-1` and `col=0`. + Rotations are performed around this point. + rotation : float, default=0.0 + Rotation angle in degrees for the model grid around (origin_x, origin_y). + + Attributes + ---------- + nrow : int + Number of rows in the grid. + ncol : int + Number of columns in the grid. + row_width : np.ndarray + Width of y-direction cells (each row). + col_width : np.ndarray + Width of x-direction cells (each column). + + Methods + ------- + get_disv_kwargs() + Get the keyword arguments for creating a MODFLOW-6 DISV package. + plot_grid(...) + Plot the model grid from `vertices` and `cell2d` attributes. + get_cellid(row, col, col_check=True) + Get the cellid given the row and column indices. + get_row_col(cellid) + Get the row and column indices given the cellid. + get_vertices(row, col) + Get the vertex indices for a cell given the row and column indices. + iter_row_col() + Iterate over row and column indices for each cell. + iter_row_cellid(row) + Iterate over cellid's in a specific row. + iter_column_cellid(col) + Iterate over cellid's in a specific column. + """ + + nrow: int + ncol: int + row_width: np.ndarray + col_width: np.ndarray + + def __init__( + self, + nlay=-1, + nrow=-1, # number of Y direction cells + ncol=-1, # number of X direction cells + row_width=10.0, # width of Y direction cells (each row) + col_width=10.0, # width of X direction cells (each column) + surface_elevation=100.0, + layer_thickness=100.0, + origin_x=0.0, + origin_y=0.0, + rotation=0.0, + ): + if nlay is None or nlay < 1: + self._init_empty() + return + + ncpl = ncol * nrow # Nodes per layer + + self.nrow = nrow + self.ncol = ncol + ncell = ncpl * nlay + + # Check if layer_thickness needs to be flattened + cls_name = "DisvStructuredGridBuilder" + top = self._get_array(cls_name, surface_elevation, ncpl) + thick = self._get_array(cls_name, layer_thickness, nlay, ncell) + self.row_width = self._get_array(cls_name, row_width, ncol) + self.col_width = self._get_array(cls_name, col_width, nrow) + + bot = [] + if thick.size == nlay: + for lay in range(nlay): + bot.append(top - thick[: lay + 1].sum()) + else: + st = 0 + sp = ncpl + bt = top.copy() + for lay in range(nlay): + bt -= thick[st:sp] + st, sp = sp, sp + ncpl + bot.append(bt) + + # Build the grid + + # Setup vertices + vertices = [] + + # Get row 1 top: + yv_model_top = self.col_width.sum() + + # Assemble vertices along x-axis and model top + iv = 0 + xv, yv = 0.0, yv_model_top + vertices.append([iv, xv, yv]) + for c in range(ncol): + iv += 1 + xv += self.row_width[c] + vertices.append([iv, xv, yv]) + + # Finish the rest of the grid a row at a time + for r in range(nrow): + iv += 1 + yv -= self.col_width[r] + xv = 0.0 + vertices.append([iv, xv, yv]) + for c in range(ncol): + iv += 1 + xv += self.row_width[c] + vertices.append([iv, xv, yv]) + + # cell2d: [icell2d, xc, yc, ncvert, icvert] + cell2d = [] + ic = -1 + # Finish the rest of the grid a row at a time + for r in range(nrow): + for c in range(ncol): + ic += 1 + icvert = self.get_vertices(r, c) + xc, yc = self.get_centroid(icvert, vertices) + cell2d.append([ic, xc, yc, 4, *icvert]) + + super().__init__(nlay, vertices, cell2d, top, bot, origin_x, origin_y, rotation) + + def __repr__(self): + return super().__repr__("DisvStructuredGridBuilder") + + def _init_empty(self): + super()._init_empty() + nul = np.array([]) + self.nrow = 0 + self.ncol = 0 + self.row_width = nul + self.col_width = nul + + def property_copy_to(self, DisvStructuredGridBuilderType): + if isinstance(DisvStructuredGridBuilderType, DisvStructuredGridBuilder): + super().property_copy_to(DisvStructuredGridBuilderType) + DisvStructuredGridBuilderType.nrow = self.nrow + DisvStructuredGridBuilderType.ncol = self.ncol + DisvStructuredGridBuilderType.row_width = self.row_width.copy() + DisvStructuredGridBuilderType.col_width = self.col_width.copy() + else: + raise RuntimeError( + "DisvStructuredGridBuilder.property_copy_to " + "can only copy to objects that inherit " + "properties from DisvStructuredGridBuilder" + ) + + def copy(self): + cp = DisvStructuredGridBuilder() + self.property_copy_to(cp) + return cp + + def get_cellid(self, row, col): + """ + Get the cellid given the row and column indices. + + Parameters + ---------- + row : int + Row index. + col : int + Column index. + + Returns + ------- + int + cellid index + """ + return row * self.ncol + col + + def get_row_col(self, cellid): + """ + Get the row and column indices given the cellid. + + Parameters + ---------- + cellid : int + cellid index + + Returns + ------- + (int, int) + Row index, Column index + """ + row = cellid // self.ncol + col = cellid - row * self.ncol + return row, col + + def get_vertices(self, row, col): + """ + Get the vertex indices for a cell given the row and column indices. + + Parameters + ---------- + row : int + Row index. + col : int + Column index. + + Returns + ------- + list[int] + List of vertex indices that define the cell at (row, col). + """ + nver = self.ncol + 1 + return [ + row * nver + col, + row * nver + col + 1, + (row + 1) * nver + col + 1, + (row + 1) * nver + col, + ] + + def iter_row_col(self): + """Generator that iterates through each rows' columns. + + Yields + ------- + (int, int) + Row index, column index + """ + for cellid in range(self.ncpl): + yield self.get_row_col(cellid) + + def iter_row_cellid(self, row): + """Generator that iterates through the cellid within a row. + That is, the cellid for all columns within the specified row. + + Parameters + ---------- + row : int + Row index. + + Yields + ------- + int + cellid index + """ + for col in range(self.ncol): + yield self.get_cellid(row, col) + + def iter_column_cellid(self, col): + """Generator that iterates through the cellid within a column. + That is, the cellid for all rows within the specified column. + + Parameters + ---------- + col : int + Column index. + + Yields + ------- + int + cellid index + """ + for row in range(self.nrow): + yield self.get_cellid(row, col) + + +class DisvGridMerger: + """ + Class for merging, non-overlapping, MODFLOW 6 DISV grids. The merge is + made by selecting a connection point and adjusting the (x,y) coordinates + of one of the grids. The grid connection is made by starting with the first + grid, called `__main__`, then adjusting the second grid to have __main__ + incorporate both grids. After that, subsequent grids are snapped to the + __main__ grid to form the final merged grid. + + When a grid is shifted to snap to __main__ (`snap_vertices`), any vertices + that are in proximity of the __main__ grid are merged (that is, the + snapped grid drops the overlapping vertices and uses the existing __main__ + ones). Proximately is determined by having an x or y distance less than + `connect_tolerance`. + + Vertices can also be forced to snap to the __main__ grid with `force_snap`. + A force snap occurs after the second grid is shifted and snapped to + __main__. The force snap drops the existing vertex and uses the forced one + changing the shape of the cell. Note, if any existing vertices are located + within the new shape of the cell, then they are added to the cell2d vertex + list. + + Examples + -------- + >>> # Example snaps two rectangular structured vertex grids. + >>> # The first grid has 2 rows and 2 columns; + >>> # the second grid has 3 rows and 2 columns. + >>> # DisvStructuredGridBuilder builds a DisvPropertyContainer object + >>> # that contains a structured vertex grid (rows and columns). + >>> from DisvStructuredGridBuilder import DisvStructuredGridBuilder + >>> + >>> grid1 = DisvStructuredGridBuilder(nlay=1, nrow=2, ncol=2) + >>> grid2 = DisvStructuredGridBuilder(nlay=1, nrow=3, ncol=2) + >>> + >>> # Optional step to see what vertex point to use + >>> grid1.plot_grid() # Plot and view vertex locations + >>> grid2.plot_grid() # to identify connection points. + >>> + >>> # Steps to merge grid1 and grid2 + >>> mg = DisvGridMerger() # init the object + >>> + >>> mg.add_grid("grid1", grid1) # add grid1 + >>> mg.add_grid("grid2", grid2) # add grid2 + >>> + >>> # Snap grid1 upper right corner (vertex 3) to grid2 upper left + >>> # corner (vertex 1). Note the vertices must be zero-based indexed. + >>> mg.set_vertex_connection("grid1", "grid2", 3 - 1, 1 - 1) + >>> + >>> # Grids do not require any force snapping because overlapping vertices + >>> # will be within the connect_tolerance. Otherwise, now would be + >>> # when to run the set_force_vertex_connection method. + >>> # Merge the grids + >>> mg.merge_grids() + >>> + >>> mg.merged.plot_grid() # plot the merged grid + + Attributes + ---------- + grids : dict + A dictionary containing names of individual grids as keys and + corresponding `DisvPropertyContainer` objects as values. + The key `__main__` is used to refer to the final merged grid and is not + allowed as a name for any `DisvPropertyContainer` object. + merged : DisvPropertyContainer + A `DisvPropertyContainer` object representing the merged grid. + snap_vertices : dict + A dictionary of vertex connections to be snapped during + the merging process. This attribute is set with the + ``set_vertex_connection`` method. The key is ``(name1, name2)`` and + value is ``(vertex1, vertex2)``, where name1 and name2 correspond with + keys from `grids` and ``vertex1`` and ``vertex2`` are the connection + vertices for ``name1`` and ``name2``, respectively. + connect_tolerance : dict + A dictionary specifying the tolerance distance for vertex snapping. + After a grid is snapped to __main__ via snap_vertices, any vertices + that overlap within an x or y length of connect_tolerance are merged. + snap_order : list + A list of grid pairs indicating the order in which grids + will be merged. This is variable is set after running the + `merge_grids` method. + force_snap : dict + A dictionary of vertex connections that must be snapped, + even if they don't satisfy the tolerance. The key is ``(name1, name2)`` + and value is ``[[v1, ...], [v2, ...]]``, where ``name1`` and ``name2`` + correspond with keys from `grids` and ``v1`` is a list of verties to + snap from ``name1``, and ``v2`` is a list of vertices to snap to from + ``name2``. The first ``v1``, corresponds with the first ``v2``, + and so forth. + force_snap_drop : dict + A dictionary specifying which vertex to drop when force snapping. The + key is ``(name1, name2)`` and value is ``[v_drop, ...]``, where + ``v_drop`` is 1 to drop the vertex from ``name1``, and 2 to drop + from ``name2``. + force_snap_cellid : set + A set that lists all the merged grid cellids that had one or more + verties force snapped. This list is important for checking if the + new vertex list and cell center are correct. + vert2name : dict + A dictionary mapping the merged grid's vertex numbers to the + corresponding grid names and vertex indices. The key is the vertex + from the new merged grid and the value is + ``[[name, vertex_old], ...]``, where ``name`` is the original grid name + and ``vertex_old`` is its correspondnig vertex from name. + name2vert : dict + A dictionary mapping grid names and vertex indices to the merged + grid's vertices. The key is ``(name, vertex_old)``, where ``name`` is + the original grid name and ``vertex_old`` is its correspondnig vertex + from name. The value is the merged grid's vertex. + cell2name : dict + A dictionary mapping the merged grid's cellid's to the corresponding + original grid names and cellid's. The key is the merged grid's cellid + and value is ``(name, cellid_old)``, where ``name`` is the + original grid name and ``cellid_old`` is its correspondnig cellid from + name. + name2cell : dict + A dictionary mapping grid names and cellid's to the merged grid's + cellid's. The key is ``(name, cellid_old)``, where ``name`` is the + original grid name and ``cellid_old`` is its correspondnig cellid from + name, and value is the merged grid's cellid. + + Notes + ------- + The following is always true: + + ``cell2name[cell] == name2vert[cell2name[cell]]`` + + ``name2vert[(name, vertex)] is in vert2name[name2vert[(name, vertex)]]`` + + Methods + ------- + get_disv_kwargs(name="__main__") + Get the keyword arguments for creating a MODFLOW-6 DISV package for + a specified grid. + add_grid(name, grid) + Add an individual grid to the merger. + set_vertex_connection(name1, name2, vertex1, vertex2, autosnap_tolerance=1.0e-5) + Set a vertex connection between two grids for snapping. + set_force_vertex_connection(name1, name2, vertex1, vertex2, drop_vertex=2) + Force a vertex connection between two grids. + merge_grids() + Merge the specified grids based on the defined vertex connections. + plot_grid(name="__main__", ...) + Selects the grid specified by ``name`` and passes the remaining + kwargs to DisvPropertyContainer.plot_grid(...). + """ + + grids: dict[str, DisvPropertyContainer] + merged: DisvPropertyContainer + snap_vertices: dict + connect_tolerance: dict + snap_order: list + force_snap: dict + force_snap_drop: dict + force_snap_cellid: set + vert2name: dict + name2vert: dict + cell2name: dict + name2cell: dict + + def __init__(self): + self.grids = {} + self.merged = DisvPropertyContainer() + + self.snap_vertices = {} + self.connect_tolerance = {} + self.snap_order = [] + self.force_snap = {} + self.force_snap_drop = {} + self.force_snap_cellid = set() + + self.vert2name = {} # vertex: [[name, vertex], ...] + self.name2vert = {} # (name, vertex): vertex + + self.cell2name = {} # cellid: (name, cellid) + self.name2cell = {} # (name, cellid): cellid + + def get_disv_kwargs(self, name="__main__"): + return self.get_grid(name).get_disv_kwargs() + + def __repr__(self): + names = ", ".join(self.grids.keys()) + return f"DisvGridMerger({names})" + + def property_copy_to(self, DisvGridMergerType): + if isinstance(DisvGridMergerType, DisvGridMerger): + DisvGridMergerType.merged = self.merged.copy() + dcp = copy.deepcopy + + for name in self.grids: + DisvGridMergerType.grids[name] = self.grids[name].copy() + + for name in self.snap_vertices: + DisvGridMergerType.snap_vertices[name] = dcp(self.snap_vertices[name]) + + for name in self.connect_tolerance: + DisvGridMergerType.connect_tolerance[name] = self.connect_tolerance[ + name + ] + + for name in self.force_snap: + DisvGridMergerType.force_snap[name] = dcp(self.force_snap[name]) + + for name in self.force_snap_drop: + DisvGridMergerType.force_snap_drop[name] = dcp( + self.force_snap_drop[name] + ) -figure_size_grid_com = (6.5, 2.5) -figure_size_grid = (6.5, 3) -figure_size_head = (6.5, 2.5) + DisvGridMergerType.force_snap_cellid = dcp(self.force_snap_cellid) + + for name in self.vert2name: + DisvGridMergerType.vert2name[name] = dcp(self.vert2name[name]) + + for name in self.cell2name: + DisvGridMergerType.cell2name[name] = dcp(self.cell2name[name]) + + for name in self.name2vert: + DisvGridMergerType.name2vert[name] = self.name2vert[name] + + for name in self.name2cell: + DisvGridMergerType.name2cell[name] = self.name2cell[name] + + DisvGridMergerType.snap_order = dcp(self.snap_order) + else: + raise RuntimeError( + "DisvGridMerger.property_copy_to " + "can only copy to objects that inherit " + "properties from DisvGridMerger" + ) + + def copy(self): + cp = DisvGridMerger() + self.property_copy_to(cp) + return cp + + def get_merged_cell2d(self, name, cell2d_orig): + return self.name2cell[(name, cell2d_orig)] + + def get_merged_vertex(self, name, vertex_orig): + return self.name2vert[(name, vertex_orig)] + + def get_grid(self, name="__main__"): + if name == "" or name == "__main__": + return self.merged + + if name not in self.grids: + raise KeyError( + "DisvGridMerger.get_grid: requested grid, " + f"{name} does not exist.\n" + "Current grids stored are:\n" + "\n".join(self.grids.keys()) + ) + + return self.grids[name] + + def add_grid(self, name, grid): + if name == "" or name == "__main__": + raise RuntimeError( + "\nDisvGridMerger.add_grid:\n" + 'name = "" or "__main__"\nis not allowed.' + ) + if isinstance(grid, DisvPropertyContainer): + grid = grid.copy() + else: + # grid = [nlay, vertices, cell2d, top, botm] + grid = DisvPropertyContainer(*grid) + + self.grids[name] = grid + + def set_vertex_connection( + self, name1, name2, vertex1, vertex2, autosnap_tolerance=1.0e-5 + ): + if (name2, name1) in self.snap_vertices: + name1, name2 = name2, name1 + vertex1, vertex2 = vertex2, vertex1 -# Base simulation and model name and workspace + key1 = (name1, name2) + key2 = (name2, name1) -ws = config.base_ws + self.snap_vertices[key1] = (vertex1, vertex2) + self.connect_tolerance[key1] = autosnap_tolerance -# Simulation name + self.force_snap[key1] = [[], []] + self.force_snap[key2] = [[], []] + self.force_snap_drop[key1] = [] + self.force_snap_drop[key2] = [] -sim_name = "ex-gwf-curvilin" + def set_force_vertex_connection( + self, name1, name2, vertex1, vertex2, drop_vertex=2 + ): + key1 = (name1, name2) + key2 = (name2, name1) + if key1 not in self.force_snap: + self.force_snap[key1] = [] + self.force_snap[key2] = [] + self.force_snap_drop[key1] = [] + self.force_snap_drop[key2] = [] + + drop_vertex_inv = 1 if drop_vertex == 2 else 2 + + self.force_snap[key1][0].append(vertex1) + self.force_snap[key1][1].append(vertex2) + + self.force_snap[key2][0].append(vertex2) + self.force_snap[key2][1].append(vertex1) + + self.force_snap_drop[key1].append(drop_vertex) + self.force_snap_drop[key2].append(drop_vertex_inv) + + def _get_vertex_xy(self, name, iv): + vertices = self.get_grid(name).vertices + for iv_orig, xv, yv in vertices: + if iv == iv_orig: + return xv, yv + raise RuntimeError( + "DisvGridMerger: " f"Failed to find vertex {iv} in grid {name}" + ) -# Model units + def _find_merged_vertex(self, xv, yv, tol): + for iv, xv_chk, yv_chk in self.merged.vertices: + if abs(xv - xv_chk) + abs(yv - yv_chk) < tol: + return iv + return None + + def _replace_vertex_xy(self, iv, xv, yv): + for vert in self.merged.vertices: + if iv == vert[0]: + vert[1] = xv + vert[2] = yv + return + raise RuntimeError( + "DisvGridMerger: Unknown code error - " f"failed to locate vertex {iv}" + ) + + def _clear_attribute(self): + self.snap_order.clear() + self.force_snap_cellid.clear() + + self.merged.nlay = 0 + self.merged.nvert = 0 + self.merged.ncpl = 0 + self.merged.cell2d.clear() + self.merged.vertices.clear() + self.merged.top = np.array(()) + self.merged.botm.clear() + + self.cell2name.clear() + self.name2cell.clear() + self.vert2name.clear() + self.name2vert.clear() + + def _grid_snap_order(self): + # grids are snapped to one main grid using key = (name1, name2). + # it is required that at least name1 or name2 already be defined + # in the main grid. Function determines order to ensure this. + # snap_list -> List of (name1, name2) to parse + snap_list = list(self.snap_vertices.keys()) + name_used = {snap_list[0][0]} # First grid name always used first + snap_order = [] # Final order to build main grid + snap_append = [] # key's that need to be parsed again + loop_limit = 50 + loop = 0 + error_msg = ( + "\nDisvGridMerger:\n" + "Failed to find grid snap order.\n" + "Snapping must occur in contiguous steps " + "to the same main, merged grid.\n" + ) + while len(snap_list) > 0: + key = snap_list.pop(0) + name1, name2 = key + has_name1 = name1 in name_used + has_name2 = name2 in name_used + + if has_name1 and has_name2: # grid snapped to main twice + raise RuntimeError( + error_msg + "Once a grid name has been snapped to " + "the main grid,\n" + "it cannot be snapped again.\n" + f"The current snap order determined is:\n\n" + f"{snap_order}\n\n" + "but the following two grids were already " + "snapped to the main grid:\n" + f"{name1}\nand\n{name2}\n" + ) + + if has_name1 or has_name2: # have a name to snap too + snap_order.append(key) + name_used.add(name1) + name_used.add(name2) + else: # neither name found, so save for later + snap_append.append(key) + + if len(snap_list) == 0 and len(snap_append) > 0: + snap_list.extend(snap_append) + snap_append.clear() + loop += 1 + if loop > loop_limit: + raise RuntimeError( + error_msg + "Determined snap order for the " + "main grid is:\n\n" + f"{snap_order}\n\n" + "but failed to snap the following " + "to the main grid:\n\n" + f"{snap_list}" + ) + return snap_order + + def merge_grids(self): + self._clear_attribute() + + # First grid is unchanged, all other grids are changed as they + # snap to the final merged grid. + name1 = next(iter(self.snap_vertices.keys()))[0] + + cell2d = self.merged.cell2d + vertices = self.merged.vertices + + cell2d.extend(copy.deepcopy(self.grids[name1].cell2d)) + vertices.extend(copy.deepcopy(self.grids[name1].vertices)) + + for ic, *_ in cell2d: + self.cell2name[ic] = (name1, ic) + self.name2cell[(name1, ic)] = ic + + for iv, *_ in vertices: + self.vert2name[iv] = [(name1, iv)] + self.name2vert[(name1, iv)] = iv + + ic_new = ic # Last cell2d id from previous-previous loop + iv_new = iv # Last vertex id from previous loop + snapped = {name1} + force_snapped = set() + for key in self._grid_snap_order(): # Loop through vertices to snap + tol = self.connect_tolerance[key] + v1, v2 = self.snap_vertices[key] + name1, name2 = key + if name2 in snapped: + name1, name2 = name2, name1 + v1, v2 = v2, v1 + + if name1 not in self.snap_order: + self.snap_order.append(name1) + if name2 not in self.snap_order: + self.snap_order.append(name2) + + v1_orig = v1 + v1 = self.name2vert[(name1, v1_orig)] + + v1x, v1y = self._get_vertex_xy("__main__", v1) + v2x, v2y = self._get_vertex_xy(name2, v2) + + difx = v1x - v2x + dify = v1y - v2y + + for v2_orig, xv, yv in self.grids[name2].vertices: + xv += difx + yv += dify + + if ( + key in self.force_snap + and v2_orig in self.force_snap[key][1] # force snap vertex + ): + ind = self.force_snap[key][1].index(v2_orig) + v1_orig_force = self.force_snap[key][0][ind] + iv = self.name2vert[(name1, v1_orig_force)] + if self.force_snap_drop[key] == 1: + # replace v1 vertex with the x,y from v2 to force snap + self._replace_vertex_xy(iv, xv, yv) + force_snapped.add((name1, v1_orig_force)) + else: + force_snapped.add((name2, v2_orig)) + else: + iv = self._find_merged_vertex(xv, yv, tol) + + if iv is None: + iv_new += 1 + vertices.append([iv_new, xv, yv]) + self.vert2name[iv_new] = [(name2, v2_orig)] + self.name2vert[(name2, v2_orig)] = iv_new + else: + self.vert2name[iv].append((name2, v2_orig)) + self.name2vert[(name2, v2_orig)] = iv + + # Loop through cells and update center point and icvert + for ic, xc, yc, ncvert, *icvert in self.grids[name2].cell2d: + ic_new += 1 + xc += difx + yc += dify + + self.cell2name[ic_new] = (name2, ic) + self.name2cell[(name2, ic)] = ic_new + + item = [ic_new, xc, yc, ncvert] # append new icvert's + for iv_orig in icvert: + item.append(self.name2vert[(name2, iv_orig)]) # = iv_new + cell2d.append(item) + + # Force snapped cells need to update cell center and check for + # errors in the grid + for name, iv_orig in force_snapped: + for ic, _, _, _, *icvert in self.grids[name].cell2d: + if iv_orig in icvert: # cell was deformed by force snap + ic_new = self.name2cell[(name, ic)] + self.force_snap_cellid.add(ic_new) + + if len(self.force_snap_cellid) > 0: + dist = lambda v1, v2: sqrt((v2[1] - v1[1]) ** 2 + (v2[2] - v1[2]) ** 2) + mg = self.merged + vert_xy = np.array([(x, y) for _, x, y in mg.vertices]) + for ic in self.force_snap_cellid: + _, _, _, _, *vert = mg.cell2d[ic] + if len(vert) != len(set(vert)): # contains a duplicate vertex + seen = set() + seen_add = seen.add + vert = [v for v in vert if not (v in seen or seen_add(v))] + tmp = mg.cell2d[ic] + tmp[3] = len(vert) + mg.cell2d[ic] = tmp[:4] + vert + # check if vertices are within cell. + cell = [mg.vertices[iv][1:] for iv in vert] + path = mpltPath.Path(cell) + contain = np.where(path.contains_points(vert_xy))[0] + for iv in contain: + # find closest polyline + if iv in vert: + continue + vert_check = mg.vertices[iv] # vert_xy[iv, :] + d = np.inf + v_closest = -1 + for v in vert: + d2 = dist(vert_check, mg.vertices[v]) + if d > d2: + d = d2 + v_closest = v + ind = vert.index(v_closest) + if ind == len(vert) - 1: # Closest is at the end + d1 = dist(vert_check, mg.vertices[vert[0]]) + d2 = dist(vert_check, mg.vertices[vert[-2]]) + if d1 < d2: + ind = len(vert) + elif ind == 0: # Closest is at the start check end members + d1 = dist(vert_check, mg.vertices[vert[-1]]) + d2 = dist(vert_check, mg.vertices[vert[1]]) + if d2 < d1: + ind = 1 + else: + d1 = dist(vert_check, mg.vertices[vert[ind - 1]]) + d2 = dist(vert_check, mg.vertices[vert[ind + 1]]) + if d2 < d1: + ind += 1 + + # update cell2d for cell ic + vert.insert(ind, iv) + tmp = mg.cell2d[ic] + tmp[3] = len(vert) + # update cell center + tmp[1], tmp[2] = mg.get_centroid(vert) + mg.cell2d[ic] = tmp[:4] + vert + + self.merged.nvert = len(vertices) + self.merged.ncpl = len(cell2d) + + nlay = 0 + for name in self.snap_order: + if nlay < self.grids[name].nlay: + nlay = self.grids[name].nlay + self.merged.nlay = nlay + + top = [] + for name in self.snap_order: + top.extend(self.grids[name].top) + self.merged.top = np.array(top) + + for lay in range(nlay): + bot = [] + for name in self.snap_order: + if lay < self.grids[name].nlay: + bot.extend(self.grids[name].botm[lay]) + else: + bot.extend(self.grids[name].botm[-1]) + self.merged.botm.append(bot) + + def plot_grid( + self, + name="__main__", + title="", + plot_time=0.0, + show=True, + figsize=(10, 10), + dpi=None, + xlabel="", + ylabel="", + cell2d_override=None, + vertices_override=None, + ax_override=None, + cell_dot=True, + cell_num=True, + cell_dot_size=7.5, + vertex_dot=True, + vertex_num=True, + vertex_dot_size=6.5, + ): + return self.get_grid(name).plot_grid( + title, + plot_time, + show, + figsize, + dpi, + xlabel, + ylabel, + cell2d_override, + vertices_override, + ax_override, + cell_dot, + cell_num, + cell_dot_size, + vertex_dot, + vertex_num, + vertex_dot_size, + ) + + +class DisvCurvilinearBuilder(DisvPropertyContainer): + """ + A class for generating a curvilinear MODFLOW 6 DISV grid. A curvilinear + grid is similar to a radial grid, composed of radial bands, but includes + ncol discretization within a radial band and does not have to form an + entire circle (such as, a discretized wedge). + + This class inherits from the `DisvPropertyContainer` class and provides + methods to generate a curvilinear grid using radial and angular parameters. + + All indices are zero-based, but translated to one-base for the figures and + by flopy for use with MODFLOW 6. Angles are in degrees, with ``0`` being in + the positive x-axis direction and ``90`` in the positive y-axis direction. + + If no arguments are provided then an empty object is returned. + + Parameters + ---------- + nlay : int + Number of layers + radii : array_like + List of radial distances that describe the radial bands. + The first radius is the innermost radius, and then the rest are the + outer radius of each radial band. Note that the number of radial bands + is equal to ``len(radii) - 1``. + angle_start : float + Starting angle in degrees for the curvilinear grid. + angle_stop : float + Stopping angle in degrees for the curvilinear grid. + angle_step : float + Column discretization of each radial band. + If positive, then represents the angle step in degrees for each column + in a radial band. That is, the number of columns (`ncol`) is: + ``ncol = (angle_stop - angle_start)/angle_step`` + If negative, then the absolute value is the number of columns (ncol). + surface_elevation : float or array_like + Surface elevation for the top layer. Can either be a single float + for the entire `top`, or array_like of length `nradial`, or + array_like of length `ncpl`. + layer_thickness : float or array_like + Thickness of each layer. Can either be a single float + for model cells, or array_like of length `nlay`, or + array_like of length `ncpl`. + single_center_cell : bool, default=False + If True, include a single center cell. If true, then innermost `radii` + must be **zero**. That is, the innermost, radial band has ``ncol=1``. + origin_x : float, default=0.0 + X-coordinate reference point for the `radii` distance. + origin_y : float, default=0.0 + Y-coordinate reference point for the `radii` distance. + + Attributes + ---------- + nradial : int + Number of radial bands in the grid. + ncol : int + Number of columns in each radial band. + inner_vertex_count : int + Number of vertices in the innermost radial band. + single_center_cell : bool + Whether a single center cell is included. + full_circle : bool + Whether the grid spans a full circle. That is, + full_circle = `angle_start`==`angle_stop`==``0``). + radii : numpy.ndarray + Array of radial distances from (origin_x, origin_y) for each radial + band. The first value is the innermost radius and the remaining are + each radial bands outer radius. + angle_start : float + Starting angle in degrees for the curvilinear grid. + angle_stop : float + Stopping angle in degrees for the curvilinear grid. + angle_step : float + Angle step in degrees for each column in a radial band. + angle_span : float + Span of the angle range in degrees for the curvilinear grid. + + Methods + ------- + get_disv_kwargs() + Get the keyword arguments for creating a MODFLOW-6 DISV package. + plot_grid(...) + Plot the model grid from `vertices` and `cell2d` attributes. + get_cellid(rad, col, col_check=True) + Get the cellid given the radial and column indices. + get_rad_col(cellid) + Get the radial and column indices given the cellid. + get_vertices(rad, col) + Get the vertex indices for a cell given the radial and column indices. + calc_curvilinear_ncol(angle_start, angle_stop, angle_step) + Calculate the number of columns in the curvilinear grid based on + the given angle parameters. It will adjust `angle_step` to ensure + that the number of columns is an integer value. + iter_rad_col() + Iterate through the radial band columns, then bands. + iter_radial_cellid(rad) + Iterate through the cellid within a radial band. + iter_column_cellid(col) + Iterate through the cellid along a column across all radial bands. + """ + + nradial: int + ncol: int + inner_vertex_count: int + single_center_cell: bool + full_circle: bool + radii: np.ndarray + angle_start: float + angle_stop: float + angle_step: float + angle_span: float + + def __init__( + self, + nlay=-1, + radii=np.array((0.0, 1.0)), + angle_start=0.0, + angle_stop=90.0, + angle_step=-1, + surface_elevation=100.0, + layer_thickness=100.0, + single_center_cell=False, + origin_x=0.0, + origin_y=0.0, + ): + if nlay is None or nlay < 1: + self._init_empty() + return + + if angle_start < 0.0: + angle_start += 360.0 + if angle_stop < 0.0: + angle_stop += 360.0 + if abs(angle_step) < 1.0e-30: + raise RuntimeError("DisvCurvilinearBuilder: angle_step is near zero") + + angle_span = self._get_angle_span(angle_start, angle_stop) + + ncol, angle_step = self.calc_curvilinear_ncol( + angle_start, angle_stop, angle_step + ) + if angle_step > 90.0: + angle_step = 90.0 + ncol, angle_step = self.calc_curvilinear_ncol( + angle_start, angle_stop, angle_step + ) + + if angle_span < angle_step: + raise RuntimeError( + "DisvCurvilinearBuilder: angle_step is greater than " + "the total angel, that is:\n" + "angle_step > |angle_stop - angle_start|\n" + f"{angle_step} > {angle_span}" + ) + + try: + nradial = len(radii) - 1 + except TypeError: + raise RuntimeError("DisvCurvilinearBuilder: radii must be list-like type") + + if nradial < 1: + raise RuntimeError( + "DisvCurvilinearBuilder: len(radii) must be greater than 1" + ) + + if single_center_cell and radii[0] > 1.0e-100: + raise RuntimeError( + "DisvCurvilinearBuilder: single_center_cell=True must " + "have the first radii be zero, that is: radii[0] = 0.0\n" + f"Input received radii[0]={radii[0]}" + ) + + full_circle = 359.999 < angle_span + nver = ncol if full_circle else ncol + 1 + + ncpl = ncol * nradial # Nodes per layer + if single_center_cell: + ncpl = (ncol * nradial) - ncol + 1 + + self.radii = np.array(radii, dtype=np.float64) + self.nradial = nradial + self.ncol = ncol + + self.single_center_cell = single_center_cell + self.full_circle = full_circle + + self.angle_start = angle_start + self.angle_stop = angle_stop + self.angle_step = angle_step + self.angle_span = angle_span + + cls_name = "DisvCurvilinearBuilder" + top = self._get_array(cls_name, surface_elevation, ncpl, nradial) + thick = self._get_array(cls_name, layer_thickness, nlay, ncpl * nlay) + + if top.size == nradial and nradial != ncpl: + tmp = [] + for it, rad in top: + if it == 0 and single_center_cell: + tmp.append(rad) + else: + tmp += ncol * [rad] + top = np.array(tmp) + del tmp + + bot = [] + + if thick.size == nlay: + for lay in range(nlay): + bot.append(top - thick[: lay + 1].sum()) + else: + st = 0 + sp = ncpl + bt = top.copy() + for lay in range(nlay): + bt -= thick[st:sp] + st, sp = sp, sp + ncpl + bot.append(bt) + + if single_center_cell and full_circle: + # Full, filled circle - No vertex at center + inner_vertex_count = 0 + elif self.radii[0] < 1.0e-100: + # Single point at circle center + inner_vertex_count = 1 + else: + # Innermost vertices are the same as outer bands + inner_vertex_count = nver + + self.inner_vertex_count = inner_vertex_count + + # Build the grid + + vertices = [] + iv = 0 + stp = np.radians(angle_step) # angle step in radians + + # Setup center vertex + if inner_vertex_count == 1: + vertices.append([iv, 0.0, 0.0]) # Single vertex at center + iv += 1 + + # Setup vertices + st = 0 if inner_vertex_count > 1 else 1 + for rad in self.radii[st:]: + ang = np.radians(angle_start) # angle start in radians + for it in range(nver): + xv = rad * np.cos(ang) + yv = rad * np.sin(ang) + vertices.append([iv, xv, yv]) + iv += 1 + ang += stp + + # cell2d: [icell2d, xc, yc, ncvert, icvert] + cell2d = [] + ic = 0 + for rad in range(nradial): + single_cell_rad0 = self.single_center_cell and rad == 0 + for col in range(ncol): + icvert = self.get_vertices(rad, col) + # xc, yc = get_cell_center(rad, col) + if single_cell_rad0: + xc, yc = 0.0, 0.0 + else: + xc, yc = self.get_centroid(icvert, vertices) + cell2d.append([ic, xc, yc, len(icvert), *icvert]) + ic += 1 + if single_cell_rad0: + break + + super().__init__(nlay, vertices, cell2d, top, bot, origin_x, origin_y) + + def __repr__(self): + return super().__repr__("DisvCurvilinearBuilder") + + def _init_empty(self): + super()._init_empty() + nul = np.array([]) + self.nradial = 0 + self.ncol = 0 + self.inner_vertex_count = 0 + self.single_center_cell = False + self.full_circle = False + self.radii = nul + self.angle_start = 0 + self.angle_stop = 0 + self.angle_step = 0 + self.angle_span = 0 + + def property_copy_to(self, DisvCurvilinearBuilderType): + if isinstance(DisvCurvilinearBuilderType, DisvCurvilinearBuilder): + super().property_copy_to(DisvCurvilinearBuilderType) + DisvCurvilinearBuilderType.nradial = self.nradial + DisvCurvilinearBuilderType.ncol = self.ncol + DisvCurvilinearBuilderType.full_circle = self.full_circle + DisvCurvilinearBuilderType.radii = self.radii + DisvCurvilinearBuilderType.angle_start = self.angle_start + DisvCurvilinearBuilderType.angle_stop = self.angle_stop + DisvCurvilinearBuilderType.angle_step = self.angle_step + DisvCurvilinearBuilderType.angle_span = self.angle_span + DisvCurvilinearBuilderType.inner_vertex_count = self.inner_vertex_count + DisvCurvilinearBuilderType.single_center_cell = self.single_center_cell + else: + raise RuntimeError( + "DisvCurvilinearBuilder.property_copy_to " + "can only copy to objects that inherit " + "properties from DisvCurvilinearBuilder" + ) + + def copy(self): + cp = DisvCurvilinearBuilder() + self.property_copy_to(cp) + return cp + + def get_cellid(self, rad, col, col_check=True): + """ + Get the cellid given the radial and column indices. + + Parameters + ---------- + rad : int + Radial index. + col : int + Column index. + col_check : bool, default=True + If True, than a RuntimeError error is raised for single_center_cell + grids with ``rad==0`` and ``col>0``. Otherwise, assumes ``col=0``. + + Returns + ------- + int + cellid index + """ + ncol = self.ncol + if self.single_center_cell: + # Have to account for only one cell at the center + if rad == 0 and col > 0: + if col_check: + raise RuntimeError("DisvCurvilinearBuilder: Bad rad and col given") + return 0 + # if rad == 0, then first cell and pos = 0 + # else account for inner cell, plus each ncol band + pos = 1 + ncol * (rad - 1) + col if rad > 0 else 0 + else: + pos = rad * ncol + col + + return pos + + def get_rad_col(self, cellid): + """ + Get the radial and column indices given the cellid. + + Parameters + ---------- + cellid : int + cellid index + + Returns + ------- + (int, int) + Radial index, Column index + """ + ncol = self.ncol + + if cellid < 1: + rad, col = 0, 0 + elif self.single_center_cell: + cellid -= 1 # drop out first radial band (single cell) + rad = cellid // ncol + 1 + col = cellid - ncol * (rad - 1) + else: + rad = cellid // ncol + col = cellid - ncol * rad + + return rad, col + + def get_vertices(self, rad, col): + """ + Get the vertex indices for a cell given the radial and column indices. + + Parameters + ---------- + rad : int + Radial index. + col : int + Column index. + + Returns + ------- + list[int] + List of vertex indices that define the cell at (rad, col). + """ + ivc = self.inner_vertex_count + full_circle = self.full_circle + ncol = self.ncol + nver = ncol if full_circle else ncol + 1 + + if rad == 0: # Case with no center point or single center point + if self.single_center_cell: + return [iv for iv in range(nver + ivc)][::-1] + elif ivc == 1: # Single center point + if full_circle and col == ncol - 1: + return [1, col + 1, 0] # [col+2-nver, col+1, 0] + return [col + 2, col + 1, 0] + elif full_circle and col == ncol - 1: + return [col + 1, nver + col, col, col + 1 - nver] + else: # Normal inner band + return [nver + col + 1, nver + col, col, col + 1] + + n = (rad - 1) * nver + ivc + + if full_circle and col == ncol - 1: + return [n + col + 1, n + nver + col, n + col, n + col + 1 - nver] + + return [n + nver + col + 1, n + nver + col, n + col, n + col + 1] + + def iter_rad_col(self): + """Generator that iterates through the radial band columns, then bands. + + Yields + ------- + (int, int) + radial band index, column index + """ + for cellid in range(self.ncpl): + yield self.get_rad_col(cellid) + + def iter_radial_cellid(self, rad): + """Generator that iterates through the cellid within a radial band. + + Parameters + ---------- + rad : int + Radial index. + + Yields + ------- + int + cellid index + """ + st = self.get_cellid(rad, 0) + if self.single_center_cell and rad == 0: + return iter([st]) + sp = self.get_cellid(rad, self.ncol - 1) + 1 + return iter(range(st, sp)) + + def iter_column_cellid(self, col): + """Generator that iterates through the cellid along a column across + all radial bands. + + Parameters + ---------- + col : int + Column index. + + Yields + ------- + int + cellid index + """ + rad = 0 + while rad < self.nradial: + yield self.get_cellid(rad, col) + rad += 1 + + def iter_columns(self, rad): + """Generator that iterates through the columns within a radial band. + + Parameters + ---------- + rad : int + Radial index. + + Yields + ------- + int + column index + """ + if self.single_center_cell and rad == 0: + return iter([0]) + return iter(range(0, self.ncol)) + + @staticmethod + def _get_angle_span(angle_start, angle_stop): + # assumes angles are between 0 and 360 + if abs(angle_stop - angle_start) < 0.001: # angle_stop == angle_start + return 360.0 + if angle_start < angle_stop: + return angle_stop - angle_start + return 360.0 - angle_start + angle_stop + + @staticmethod + def calc_curvilinear_ncol(angle_start, angle_stop, angle_step): + """ + Calculate the number of columns in the curvilinear grid based on + the given angle parameters. It will adjust `angle_step` to ensure + that the number of columns is an integer value. + + Parameters + ---------- + angle_start : float + Starting angle in degrees for the curvilinear grid. + angle_stop : float + Stopping angle in degrees for the curvilinear grid. + angle_step : float + If positive, then represents the largest angle step in degrees + for each column in a radial band. It may be reduced to make + the number of columns be a positive, integer. + If negative, then the absolute value is the number of columns + (ncol) and angle_step is calculated based on it. + + Returns + ------- + (int, float) + The number of columns in the curvilinear grid and the angle_step + that can reproduce the exact integer number. + """ + angle_span = DisvCurvilinearBuilder._get_angle_span(angle_start, angle_stop) + + if angle_step > 0.0: + ncol = int(angle_span // angle_step) + if (angle_span / angle_step) - ncol > 0.1: # error towards larger + ncol += 1 + else: + ncol = int(round(-1 * angle_step)) + angle_step = angle_span / ncol + return ncol, angle_step + + +# - + +# ### Define parameters +# +# Define model units, parameters and other settings. + +# + +# Model units length_units = "feet" time_units = "days" -# Table Model Parameters - +# Model parameters _ = "Steady-State" # Simulation Type nper = 1 # Number of periods _ = 1 # Number of time steps - nlay = 1 # Number of layers _ = 864 # Number cells per layer - surface_elevation = 10.0 # Top of the model ($ft$) model_base = 0.0 # Base of the model ($ft$) - Tran = 0.19 # Horizontal transmissivity ($ft^2/day$) k11 = 0.019 # Horizontal hydraulic conductivity ($ft/day$) - bc0 = 10 # Left constant head boundary ($ft$) _ = "3.334" # Right constant head boundary ($ft$) - _ = " " # --- Left Curvilinear Grid Properties --- - _ = "180" # Degree angle of column 1 boundary _ = "270" # Degree angle of column ncol boundary _ = "5" # Degree angle width of each column - nradial1 = 16 # Number of radial direction cells (radial bands) _ = 18 # Number of columns in radial band (ncol) - r_inner1 = 4 # Grid inner radius ($ft$) r_outer1 = 20 # Grid outer radius ($ft$) r_width1 = 1 # Radial band width ($ft$) - _ = " " # --- Middle Structured Grid Properties --- nrow = 16 # Number of rows ncol = 18 # Number of columns row_width = 1 # Row width ($ft$) col_width = 1 # Column width ($ft$) - _ = " " # --- Right Curvilinear Grid Properties --- - _ = "0" # Degree angle of column 1 boundary _ = "90" # Degree angle of column ncol boundary _ = "5" # Degree angle width of each column - nradial2 = 16 # Number of radial direction cells (radial bands) _ = 18 # Number of columns in radial band (ncol) - r_inner2 = 4 # Grid inner radius ($ft$) r_outer2 = 20 # Grid outer radius ($ft$) r_width2 = 1 # Grid radial band width ($ft$) @@ -116,17 +2223,14 @@ angle_stop2 = 90 angle_step2 = 5 - # Right Curvilinear Model Boundary Condition bc1 = bc0 / 3 # Radius for each radial band. # First value is inner radius, the remaining are outer radii - radii = np.arange(r_inner1, r_outer1 + r_width1, r_width1) -# Get the curvilinear model properties and vertices - +# Get the curvilinear model properties and vertices. # Left Curvilinear Model curvlin1 = DisvCurvilinearBuilder( nlay, @@ -165,9 +2269,7 @@ ) # Combine the three models into one new vertex grid - grid_merger = DisvGridMerger() - grid_merger.add_grid("curvlin1", curvlin1) grid_merger.add_grid("rectgrid", rectgrid) grid_merger.add_grid("curvlin2", curvlin2) @@ -207,326 +2309,306 @@ chd_right.append([(lay, node), bc1]) chd_left = {sp: chd_left for sp in range(nper)} - chd_right = {sp: chd_right for sp in range(nper)} # Static temporal data used by TDIS file # Simulation is steady state so setup only a one day stress period. - tdis_ds = ((1.0, 1, 1),) # Solver parameters - nouter = 500 ninner = 300 hclose = 1e-4 rclose = 1e-4 +# - - -# ### Functions to build, write, run, and plot the MODFLOW 6 Curvilinear Model +# ### Model setup # -# MODFLOW 6 flopy simulation object (sim) is returned if building the model - - -def build_model(name): - if config.buildModel: - sim_ws = os.path.join(ws, name) - sim = flopy.mf6.MFSimulation( - sim_name=name, sim_ws=sim_ws, exe_name="mf6" - ) - flopy.mf6.ModflowTdis( - sim, nper=nper, perioddata=tdis_ds, time_units=time_units - ) - flopy.mf6.ModflowIms( - sim, - print_option="summary", - complexity="complex", - outer_maximum=nouter, - outer_dvclose=hclose, - inner_maximum=ninner, - inner_dvclose=hclose, - ) - - gwf = flopy.mf6.ModflowGwf(sim, modelname=name, save_flows=True) - - disv = flopy.mf6.ModflowGwfdisv( - gwf, length_units=length_units, **grid_merger.get_disv_kwargs() - ) - - npf = flopy.mf6.ModflowGwfnpf( - gwf, - k=k11, - k33=k11, - save_flows=True, - save_specific_discharge=True, - ) +# Define functions to build models, write input files, and run the simulation. + + +# + +def build_models(name): + sim_ws = os.path.join(workspace, name) + sim = flopy.mf6.MFSimulation(sim_name=name, sim_ws=sim_ws, exe_name="mf6") + flopy.mf6.ModflowTdis(sim, nper=nper, perioddata=tdis_ds, time_units=time_units) + flopy.mf6.ModflowIms( + sim, + print_option="summary", + complexity="complex", + outer_maximum=nouter, + outer_dvclose=hclose, + inner_maximum=ninner, + inner_dvclose=hclose, + ) - flopy.mf6.ModflowGwfsto( - gwf, - iconvert=0, - steady_state=True, - save_flows=True, - ) + gwf = flopy.mf6.ModflowGwf(sim, modelname=name, save_flows=True) - flopy.mf6.ModflowGwfic(gwf, strt=surface_elevation) + disv = flopy.mf6.ModflowGwfdisv( + gwf, length_units=length_units, **grid_merger.get_disv_kwargs() + ) - flopy.mf6.ModflowGwfchd( - gwf, - stress_period_data=chd_left, - pname="CHD-LEFT", - filename=f"{sim_name}.left.chd", - save_flows=True, - ) - flopy.mf6.ModflowGwfchd( - gwf, - stress_period_data=chd_right, - pname="CHD-RIGHT", - filename=f"{sim_name}.right.chd", - save_flows=True, - ) + npf = flopy.mf6.ModflowGwfnpf( + gwf, + k=k11, + k33=k11, + save_flows=True, + save_specific_discharge=True, + ) - flopy.mf6.ModflowGwfoc( - gwf, - budget_filerecord=f"{name}.cbc", - head_filerecord=f"{name}.hds", - headprintrecord=[ - ( - "COLUMNS", - curvlin1.ncol + ncol + curvlin2.ncol, - "WIDTH", - 15, - "DIGITS", - 6, - "GENERAL", - ) - ], - saverecord=[("HEAD", "ALL"), ("BUDGET", "ALL")], - printrecord=[("HEAD", "ALL"), ("BUDGET", "ALL")], - filename=f"{name}.oc", - ) + flopy.mf6.ModflowGwfsto( + gwf, + iconvert=0, + steady_state=True, + save_flows=True, + ) - return sim - return None + flopy.mf6.ModflowGwfic(gwf, strt=surface_elevation) + flopy.mf6.ModflowGwfchd( + gwf, + stress_period_data=chd_left, + pname="CHD-LEFT", + filename=f"{sim_name}.left.chd", + save_flows=True, + ) + flopy.mf6.ModflowGwfchd( + gwf, + stress_period_data=chd_right, + pname="CHD-RIGHT", + filename=f"{sim_name}.right.chd", + save_flows=True, + ) -# Function to write model files + flopy.mf6.ModflowGwfoc( + gwf, + budget_filerecord=f"{name}.cbc", + head_filerecord=f"{name}.hds", + headprintrecord=[ + ( + "COLUMNS", + curvlin1.ncol + ncol + curvlin2.ncol, + "WIDTH", + 15, + "DIGITS", + 6, + "GENERAL", + ) + ], + saverecord=[("HEAD", "ALL"), ("BUDGET", "ALL")], + printrecord=[("HEAD", "ALL"), ("BUDGET", "ALL")], + filename=f"{name}.oc", + ) + return sim -def write_model(sim, silent=True): - if config.writeModel: - sim.write_simulation(silent=silent) +def write_models(sim, silent=True): + sim.write_simulation(silent=silent) -# Function to run the curvilinear model. -# True is returned if the model runs successfully. +@timed +def run_models(sim, silent=True): + success, buff = sim.run_simulation(silent=silent, report=True) + assert success, buff -@config.timeit -def run_model(sim, silent=True): - success = True - if config.runModel: - success, buff = sim.run_simulation(silent=silent, report=True) - if not success: - print("\n".join(buff)) - return success +# - +# ### Plotting results +# +# Define functions to plot model results. -# Function to plot the curvilinear model grid. +# + +# Figure properties +figure_size_grid_com = (6.5, 2.5) +figure_size_grid = (6.5, 3) +figure_size_head = (6.5, 2.5) def plot_grid(sim, verbose=False): - fs = USGSFigure(figure_type="map", verbose=verbose) - gwf = sim.get_model(sim_name) - - fig = plt.figure(figsize=figure_size_grid) - - ax = fig.add_subplot(1, 1, 1, aspect="equal") - pmv = flopy.plot.PlotMapView(model=gwf, ax=ax, layer=0) - pmv.plot_grid() - pmv.plot_bc(name="CHD-LEFT", alpha=0.75, color="blue") - pmv.plot_bc(name="CHD-RIGHT", alpha=0.75, color="blue") - ax.set_xlabel("x position (ft)") - ax.set_ylabel("y position (ft)") - for i, (x, y) in enumerate( - zip(gwf.modelgrid.xcellcenters, gwf.modelgrid.ycellcenters) - ): - ax.text( - x, - y, - f"{i + 1}", - fontsize=3, - horizontalalignment="center", - verticalalignment="center", + with styles.USGSMap() as fs: + gwf = sim.get_model(sim_name) + + fig = plt.figure(figsize=figure_size_grid) + + ax = fig.add_subplot(1, 1, 1, aspect="equal") + pmv = flopy.plot.PlotMapView(model=gwf, ax=ax, layer=0) + pmv.plot_grid() + pmv.plot_bc(name="CHD-LEFT", alpha=0.75, color="blue") + pmv.plot_bc(name="CHD-RIGHT", alpha=0.75, color="blue") + ax.set_xlabel("x position (ft)") + ax.set_ylabel("y position (ft)") + for i, (x, y) in enumerate( + zip(gwf.modelgrid.xcellcenters, gwf.modelgrid.ycellcenters) + ): + ax.text( + x, + y, + f"{i + 1}", + fontsize=3, + horizontalalignment="center", + verticalalignment="center", + ) + v = gwf.disv.vertices.array + vert_size = 2 + ax.plot(v["xv"], v["yv"], "yo", markersize=vert_size) + for i in range(v.shape[0]): + x, y = v["xv"][i], v["yv"][i] + ax.text( + x, + y, + f"{i + 1}", + fontsize=vert_size, + color="red", + horizontalalignment="center", + verticalalignment="center", + ) + + fig.tight_layout() + + # Save components that made up the main grid + fig2, ax2 = plt.subplots( + 1, + 3, + figsize=figure_size_grid_com, ) - v = gwf.disv.vertices.array - vert_size = 2 - ax.plot(v["xv"], v["yv"], "yo", markersize=vert_size) - for i in range(v.shape[0]): - x, y = v["xv"][i], v["yv"][i] - ax.text( - x, - y, - f"{i + 1}", - fontsize=vert_size, - color="red", - horizontalalignment="center", - verticalalignment="center", - ) - - fig.tight_layout() - # Save components that made up the main grid - fig2, ax2 = plt.subplots( - 1, - 3, - figsize=figure_size_grid_com, - ) - - curvlin1.plot_grid( - "Left Curvilinear Grid", - ax_override=ax2[0], - cell_dot=False, - cell_num=False, - vertex_dot=True, - vertex_num=False, - vertex_dot_size=3, - vertex_dot_color="y", - ) - - rectgrid.plot_grid( - "Center Rectangular Grid", - ax_override=ax2[1], - cell_dot=False, - cell_num=False, - vertex_dot=True, - vertex_num=False, - vertex_dot_size=3, - vertex_dot_color="y", - ) - - curvlin2.plot_grid( - "Right Curvilinear Grid", - ax_override=ax2[2], - cell_dot=False, - cell_num=False, - vertex_dot=True, - vertex_num=False, - vertex_dot_size=3, - vertex_dot_color="y", - ) - - for ax_tmp in ax2: - ax_tmp.set_xlabel("x position (ft)") - ax_tmp.set_ylabel("y position (ft)") - - xshift, yshift = 0.0, 0.0 - for ax_tmp in ax2: - xmin, xmax = ax_tmp.get_xlim() - ymin, ymax = ax_tmp.get_ylim() - if xshift < xmax - xmin: - xshift = xmax - xmin - if yshift < ymax - ymin: - yshift = ymax - ymin - - for ax_tmp in ax2: - xmin, xmax = ax_tmp.get_xlim() - ymin, ymax = ax_tmp.get_ylim() - ax_tmp.set_xlim(xmin, xmin + xshift) - ax_tmp.set_ylim(ymin, ymin + yshift) - - ax2[0].annotate( - "A", - (-0.05, 1.05), - xycoords="axes fraction", - fontweight="black", - fontsize="xx-large", - ) + curvlin1.plot_grid( + "Left Curvilinear Grid", + ax_override=ax2[0], + cell_dot=False, + cell_num=False, + vertex_dot=True, + vertex_num=False, + vertex_dot_size=3, + vertex_dot_color="y", + ) - ax2[1].annotate( - "B", - (-0.05, 1.05), - xycoords="axes fraction", - fontweight="black", - fontsize="xx-large", - ) + rectgrid.plot_grid( + "Center Rectangular Grid", + ax_override=ax2[1], + cell_dot=False, + cell_num=False, + vertex_dot=True, + vertex_num=False, + vertex_dot_size=3, + vertex_dot_color="y", + ) - ax2[2].annotate( - "C", - (-0.05, 1.05), - xycoords="axes fraction", - fontweight="black", - fontsize="xx-large", - ) + curvlin2.plot_grid( + "Right Curvilinear Grid", + ax_override=ax2[2], + cell_dot=False, + cell_num=False, + vertex_dot=True, + vertex_num=False, + vertex_dot_size=3, + vertex_dot_color="y", + ) - fig2.tight_layout() + for ax_tmp in ax2: + ax_tmp.set_xlabel("x position (ft)") + ax_tmp.set_ylabel("y position (ft)") + + xshift, yshift = 0.0, 0.0 + for ax_tmp in ax2: + xmin, xmax = ax_tmp.get_xlim() + ymin, ymax = ax_tmp.get_ylim() + if xshift < xmax - xmin: + xshift = xmax - xmin + if yshift < ymax - ymin: + yshift = ymax - ymin + + for ax_tmp in ax2: + xmin, xmax = ax_tmp.get_xlim() + ymin, ymax = ax_tmp.get_ylim() + ax_tmp.set_xlim(xmin, xmin + xshift) + ax_tmp.set_ylim(ymin, ymin + yshift) + + ax2[0].annotate( + "A", + (-0.05, 1.05), + xycoords="axes fraction", + fontweight="black", + fontsize="xx-large", + ) - # save figure - if config.plotSave: - fpth = os.path.join( - "..", - "figures", - f"{sim_name}-grid{config.figure_ext}", + ax2[1].annotate( + "B", + (-0.05, 1.05), + xycoords="axes fraction", + fontweight="black", + fontsize="xx-large", ) - fig.savefig(fpth, dpi=600) - fpth2 = os.path.join( - "..", - "figures", - f"{sim_name}-grid-components{config.figure_ext}", + ax2[2].annotate( + "C", + (-0.05, 1.05), + xycoords="axes fraction", + fontweight="black", + fontsize="xx-large", ) - fig2.savefig(fpth2, dpi=300) - return + fig2.tight_layout() + if plot_show: + plt.show() + if plot_save: + fpth = os.path.join( + "..", + "figures", + f"{sim_name}-grid.png", + ) + fig.savefig(fpth, dpi=600) -# Function to plot the curvilinear model results. + fpth2 = os.path.join( + "..", + "figures", + f"{sim_name}-grid-components.png", + ) + fig2.savefig(fpth2, dpi=300) def plot_head(sim): - fs = USGSFigure(figure_type="map", verbose=False) - gwf = sim.get_model(sim_name) + with styles.USGSMap() as fs: + gwf = sim.get_model(sim_name) - fig = plt.figure(figsize=figure_size_head) + fig = plt.figure(figsize=figure_size_head) - head = gwf.output.head().get_data()[:, 0, :] + head = gwf.output.head().get_data()[:, 0, :] - # create MODFLOW 6 cell-by-cell budget object - qx, qy, qz = flopy.utils.postprocessing.get_specific_discharge( - gwf.output.budget().get_data(text="DATA-SPDIS", totim=1.0)[0], - gwf, - ) - - ax = fig.add_subplot(1, 1, 1, aspect="equal") - pmv = flopy.plot.PlotMapView(model=gwf, ax=ax, layer=0) - cb = pmv.plot_array(head, cmap="jet", vmin=0.0, vmax=head.max()) - pmv.plot_vector( - qx, - qy, - normalize=False, - color="0.75", - ) - cbar = plt.colorbar(cb, shrink=0.25) - cbar.ax.set_xlabel(r"Head, ($ft$)") - ax.set_xlabel("x position (ft)") - ax.set_ylabel("y position (ft)") - - fig.tight_layout() + # create MODFLOW 6 cell-by-cell budget object + qx, qy, qz = flopy.utils.postprocessing.get_specific_discharge( + gwf.output.budget().get_data(text="DATA-SPDIS", totim=1.0)[0], + gwf, + ) - # save figure - if config.plotSave: - fpth = os.path.join( - "..", "figures", f"{sim_name}-head{config.figure_ext}" + ax = fig.add_subplot(1, 1, 1, aspect="equal") + pmv = flopy.plot.PlotMapView(model=gwf, ax=ax, layer=0) + cb = pmv.plot_array(head, cmap="jet", vmin=0.0, vmax=head.max()) + pmv.plot_vector( + qx, + qy, + normalize=False, + color="0.75", ) - fig.savefig(fpth, dpi=300) - return + cbar = plt.colorbar(cb, shrink=0.25) + cbar.ax.set_xlabel(r"Head, ($ft$)") + ax.set_xlabel("x position (ft)") + ax.set_ylabel("y position (ft)") + fig.tight_layout() -# Function to plot the model results. + if plot_show: + plt.show() + if plot_save: + fpth = os.path.join("..", "figures", f"{sim_name}-head.png") + fig.savefig(fpth, dpi=300) def plot_results(silent=True): - if not config.plotModel: + if not plot: return if silent: @@ -534,55 +2616,38 @@ def plot_results(silent=True): else: verbosity_level = 1 - sim_ws = os.path.join(ws, sim_name) + sim_ws = os.path.join(workspace, sim_name) sim = flopy.mf6.MFSimulation.load( sim_name=sim_name, sim_ws=sim_ws, verbosity_level=verbosity_level ) verbose = not silent + plot_grid(sim, verbose) + plot_head(sim) - if config.plotModel: - plot_grid(sim, verbose) - plot_head(sim) - return +# - -# Function that wraps all of the steps for the curvilinear model -# -# 1. build_model, -# 2. write_model, -# 3. run_model, and -# 4. plot_results. +# ### Running the example # +# Define and invoke a function to run the example scenario, then plot results. -def simulation(silent=True): +# + +def scenario(silent=True): # key = list(parameters.keys())[idx] # params = parameters[key].copy() + sim = build_models(sim_name) + if write: + write_models(sim, silent=silent) + if run: + run_models(sim, silent=silent) - sim = build_model(sim_name) - - write_model(sim, silent=silent) - - success = run_model(sim, silent=silent) - assert success, "could not run...{}".format(sim_name) - - -# nosetest - exclude block from this nosetest to the next nosetest -def test_and_plot(): - simulation(silent=False) - plot_results(silent=False) - return - - -# nosetest end - - -if __name__ == "__main__": - # ### Curvilinear Example - # MF6 Curvilinear Model - simulation() +# Run simulation +scenario() - # Solve analytical and plot results with MF6 results +if plot: + # Solve analytical solution and plot results with MF6 results plot_results() +# - diff --git a/scripts/ex-gwf-disvmesh.py b/scripts/ex-gwf-disvmesh.py index c7b546a33..660e3b277 100644 --- a/scripts/ex-gwf-disvmesh.py +++ b/scripts/ex-gwf-disvmesh.py @@ -5,50 +5,48 @@ # uses 2778 vertices (NVERT) to delineate 5240 cells per layer (NCPL). # General-head boundaries are assigned to model layer 1 for cells outside of # a 1025 m radius circle. Recharge is applied to the top of the model. -# -# ### USG1DISV Problem Setup +# ### Initial setup # -# Imports +# Import dependencies, define the example name and workspace, and read settings from environment variables. +# + import os -import sys +import pathlib as pl import flopy import flopy.utils.cvfdutil import matplotlib.pyplot as plt import numpy as np +import pooch +from flopy.plot.styles import styles from flopy.utils.geometry import get_polygon_area from flopy.utils.gridintersect import GridIntersect +from modflow_devtools.misc import get_env, timed from shapely.geometry import Polygon -# Append to system path to include the common subdirectory - -sys.path.append(os.path.join("..", "common")) - -# import common functionality - -import config -from figspecs import USGSFigure - -# Set default figure properties - -figure_size = (5, 5) - -# Base simulation and model name and workspace +# Example name and base workspace +sim_name = "ex-gwf-disvmesh" +workspace = pl.Path("../examples") -ws = config.base_ws +# Settings from environment variables +write = get_env("WRITE", True) +run = get_env("RUN", True) +plot = get_env("PLOT", True) +plot_show = get_env("PLOT_SHOW", True) +plot_save = get_env("PLOT_SAVE", True) +# - -# Simulation name -sim_name = "ex-gwf-disvmesh" +# ### Define parameters +# +# Define model units, parameters and other settings. +# + # Model units - length_units = "meters" time_units = "days" -# Table USG1DISV Model Parameters - +# Model parameters nper = 1 # Number of periods nlay = 2 # Number of layers top = 0.0 # Top of the model ($m$) @@ -61,19 +59,16 @@ # Static temporal data used by TDIS file # Simulation has 1 steady stress period (1 day). - perlen = [1.0] nstp = [1] tsmult = [1.0] tdis_ds = list(zip(perlen, nstp, tsmult)) # Parse strings into lists - botm = [float(value) for value in botm_str.split(",")] -# create the disv grid - +# create the disv grid def from_argus_export(fname): f = open(fname) line = f.readline() @@ -111,8 +106,10 @@ def from_argus_export(fname): # Load argus mesh and get disv grid properties - -fname = os.path.join(config.data_ws, "ex-gwf-disvmesh", "argus.exp") +fname = pooch.retrieve( + url=f"https://github.com/MODFLOW-USGS/modflow6-examples/raw/master/data/ex-gwf-disvmesh/argus.exp", + known_hash="md5:072a758ca3d35831acb7e1e27e7b8524", +) verts, iverts = from_argus_export(fname) gridprops = flopy.utils.cvfdutil.get_disv_gridprops(verts, iverts) cell_areas = [] @@ -123,240 +120,211 @@ def from_argus_export(fname): cell_areas.append(get_polygon_area(cell_verts)) # Solver parameters - nouter = 50 ninner = 100 hclose = 1e-9 rclose = 1e-6 +# - -# ### Functions to build, write, run, and plot the MODFLOW 6 DISVMESH model +# ### Model setup # -# MODFLOW 6 flopy simulation object (sim) is returned if building the model +# Define functions to build models, write input files, and run the simulation. + + +# + +def build_models(sim_name): + sim_ws = os.path.join(workspace, sim_name) + sim = flopy.mf6.MFSimulation(sim_name=sim_name, sim_ws=sim_ws, exe_name="mf6") + flopy.mf6.ModflowTdis(sim, nper=nper, perioddata=tdis_ds, time_units=time_units) + flopy.mf6.ModflowIms( + sim, + linear_acceleration="bicgstab", + outer_maximum=nouter, + outer_dvclose=hclose, + inner_maximum=ninner, + inner_dvclose=hclose, + rcloserecord=f"{rclose} strict", + ) + gwf = flopy.mf6.ModflowGwf(sim, modelname=sim_name, save_flows=True) + flopy.mf6.ModflowGwfdisv( + gwf, + length_units=length_units, + nlay=nlay, + top=top, + botm=botm, + **gridprops, + ) + flopy.mf6.ModflowGwfnpf( + gwf, + icelltype=icelltype, + k=k11, + k33=k33, + save_specific_discharge=True, + xt3doptions=True, + ) + flopy.mf6.ModflowGwfic(gwf, strt=strt) + + theta = np.arange(0.0, 2 * np.pi, 0.2) + radius = 1500.0 + x = radius * np.cos(theta) + y = radius * np.sin(theta) + outer = [(x, y) for x, y in zip(x, y)] + radius = 1025.0 + x = radius * np.cos(theta) + y = radius * np.sin(theta) + hole = [(x, y) for x, y in zip(x, y)] + p = Polygon(outer, holes=[hole]) + ix = GridIntersect(gwf.modelgrid, method="vertex", rtree=True) + result = ix.intersect(p) + ghb_cellids = np.array(result["cellids"], dtype=int) + + ghb_spd = [] + ghb_spd += [[0, i, 0.0, k33 * cell_areas[i] / 10.0] for i in ghb_cellids] + ghb_spd = {0: ghb_spd} + flopy.mf6.ModflowGwfghb( + gwf, + stress_period_data=ghb_spd, + pname="GHB", + ) + ncpl = gridprops["ncpl"] + rchcells = np.array(list(range(ncpl)), dtype=int) + rchcells[ghb_cellids] = -1 + rch_spd = [(0, rchcells[i], recharge) for i in range(ncpl) if rchcells[i] > 0] + rch_spd = {0: rch_spd} + flopy.mf6.ModflowGwfrch(gwf, stress_period_data=rch_spd, pname="RCH") -def build_model(sim_name): - if config.buildModel: - sim_ws = os.path.join(ws, sim_name) - sim = flopy.mf6.MFSimulation( - sim_name=sim_name, sim_ws=sim_ws, exe_name="mf6" - ) - flopy.mf6.ModflowTdis( - sim, nper=nper, perioddata=tdis_ds, time_units=time_units - ) - flopy.mf6.ModflowIms( - sim, - linear_acceleration="bicgstab", - outer_maximum=nouter, - outer_dvclose=hclose, - inner_maximum=ninner, - inner_dvclose=hclose, - rcloserecord=f"{rclose} strict", - ) - gwf = flopy.mf6.ModflowGwf(sim, modelname=sim_name, save_flows=True) - flopy.mf6.ModflowGwfdisv( - gwf, - length_units=length_units, - nlay=nlay, - top=top, - botm=botm, - **gridprops, - ) - flopy.mf6.ModflowGwfnpf( - gwf, - icelltype=icelltype, - k=k11, - k33=k33, - save_specific_discharge=True, - xt3doptions=True, - ) - flopy.mf6.ModflowGwfic(gwf, strt=strt) - - theta = np.arange(0.0, 2 * np.pi, 0.2) - radius = 1500.0 - x = radius * np.cos(theta) - y = radius * np.sin(theta) - outer = [(x, y) for x, y in zip(x, y)] - radius = 1025.0 - x = radius * np.cos(theta) - y = radius * np.sin(theta) - hole = [(x, y) for x, y in zip(x, y)] - p = Polygon(outer, holes=[hole]) - ix = GridIntersect(gwf.modelgrid, method="vertex", rtree=True) - result = ix.intersect(p) - ghb_cellids = np.array(result["cellids"], dtype=int) - - ghb_spd = [] - ghb_spd += [ - [0, i, 0.0, k33 * cell_areas[i] / 10.0] for i in ghb_cellids - ] - ghb_spd = {0: ghb_spd} - flopy.mf6.ModflowGwfghb( - gwf, - stress_period_data=ghb_spd, - pname="GHB", - ) + head_filerecord = f"{sim_name}.hds" + budget_filerecord = f"{sim_name}.cbc" + flopy.mf6.ModflowGwfoc( + gwf, + head_filerecord=head_filerecord, + budget_filerecord=budget_filerecord, + saverecord=[("HEAD", "ALL"), ("BUDGET", "ALL")], + ) - ncpl = gridprops["ncpl"] - rchcells = np.array(list(range(ncpl)), dtype=int) - rchcells[ghb_cellids] = -1 - rch_spd = [ - (0, rchcells[i], recharge) for i in range(ncpl) if rchcells[i] > 0 - ] - rch_spd = {0: rch_spd} - flopy.mf6.ModflowGwfrch(gwf, stress_period_data=rch_spd, pname="RCH") - - head_filerecord = f"{sim_name}.hds" - budget_filerecord = f"{sim_name}.cbc" - flopy.mf6.ModflowGwfoc( - gwf, - head_filerecord=head_filerecord, - budget_filerecord=budget_filerecord, - saverecord=[("HEAD", "ALL"), ("BUDGET", "ALL")], - ) + return sim - return sim - return None +def write_models(sim, silent=True): + sim.write_simulation(silent=silent) -# Function to write MODFLOW 6 DISVMESH model files +@timed +def run_models(sim, silent=False): + success, buff = sim.run_simulation(silent=silent, report=True) + assert success, buff -def write_model(sim, silent=True): - if config.writeModel: - sim.write_simulation(silent=silent) +# - -# Function to run the FHB model. -# True is returned if the model runs successfully +# ### Plotting results # +# Define functions to plot model results. - -@config.timeit -def run_model(sim, silent=False): - success = True - if config.runModel: - success, buff = sim.run_simulation(silent=silent, report=True) - if not success: - print(buff) - return success +# + +# Figure properties +figure_size = (5, 5) -# Function to plot the DISVMESH model results. -# def plot_grid(idx, sim): - fs = USGSFigure(figure_type="map", verbose=False) - sim_ws = os.path.join(ws, sim_name) - gwf = sim.get_model(sim_name) - - fig = plt.figure(figsize=figure_size) - fig.tight_layout() - - ax = fig.add_subplot(1, 1, 1, aspect="equal") - pmv = flopy.plot.PlotMapView(model=gwf, ax=ax, layer=0) - pmv.plot_grid(linewidth=1) - pmv.plot_bc(name="GHB") - ax.set_xlabel("x position (m)") - ax.set_ylabel("y position (m)") - - # save figure - if config.plotSave: - fpth = os.path.join( - "..", "figures", f"{sim_name}-grid{config.figure_ext}" - ) - fig.savefig(fpth) - return + with styles.USGSMap(): + sim_ws = os.path.join(workspace, sim_name) + gwf = sim.get_model(sim_name) + + fig = plt.figure(figsize=figure_size) + fig.tight_layout() + + ax = fig.add_subplot(1, 1, 1, aspect="equal") + pmv = flopy.plot.PlotMapView(model=gwf, ax=ax, layer=0) + pmv.plot_grid(linewidth=1) + pmv.plot_bc(name="GHB") + ax.set_xlabel("x position (m)") + ax.set_ylabel("y position (m)") + + if plot_show: + plt.show() + if plot_save: + fpth = os.path.join("..", "figures", f"{sim_name}-grid.png") + fig.savefig(fpth) def plot_head(idx, sim): - fs = USGSFigure(figure_type="map", verbose=False) - sim_ws = os.path.join(ws, sim_name) - gwf = sim.get_model(sim_name) + with styles.USGSMap(): + sim_ws = os.path.join(workspace, sim_name) + gwf = sim.get_model(sim_name) - fig = plt.figure(figsize=(7.5, 5)) - fig.tight_layout() + fig = plt.figure(figsize=(7.5, 5)) + fig.tight_layout() - head = gwf.output.head().get_data()[:, 0, :] + head = gwf.output.head().get_data()[:, 0, :] - # create MODFLOW 6 cell-by-cell budget object - qx, qy, qz = flopy.utils.postprocessing.get_specific_discharge( - gwf.output.budget().get_data(text="DATA-SPDIS", totim=1.0)[0], - gwf, - ) + # create MODFLOW 6 cell-by-cell budget object + qx, qy, qz = flopy.utils.postprocessing.get_specific_discharge( + gwf.output.budget().get_data(text="DATA-SPDIS", totim=1.0)[0], + gwf, + ) - ax = fig.add_subplot(1, 2, 1, aspect="equal") - pmv = flopy.plot.PlotMapView(model=gwf, ax=ax, layer=0) - cb = pmv.plot_array(head, cmap="jet", vmin=0.0, vmax=head.max()) - pmv.plot_vector( - qx, - qy, - normalize=False, - color="0.75", - ) - cbar = plt.colorbar(cb, shrink=0.25) - cbar.ax.set_xlabel(r"Head, ($m$)") - ax.set_xlabel("x position (m)") - ax.set_ylabel("y position (m)") - fs.heading(ax, letter="A", heading="Layer 1") - - ax = fig.add_subplot(1, 2, 2, aspect="equal") - pmv = flopy.plot.PlotMapView(model=gwf, ax=ax, layer=1) - cb = pmv.plot_array(head, cmap="jet", vmin=0.0, vmax=head.max()) - pmv.plot_vector( - qx, - qy, - normalize=False, - color="0.75", - ) - cbar = plt.colorbar(cb, shrink=0.25) - cbar.ax.set_xlabel(r"Head, ($m$)") - ax.set_xlabel("x position (m)") - ax.set_ylabel("y position (m)") - fs.heading(ax, letter="B", heading="Layer 2") - - # save figure - if config.plotSave: - fpth = os.path.join( - "..", "figures", f"{sim_name}-head{config.figure_ext}" + ax = fig.add_subplot(1, 2, 1, aspect="equal") + pmv = flopy.plot.PlotMapView(model=gwf, ax=ax, layer=0) + cb = pmv.plot_array(head, cmap="jet", vmin=0.0, vmax=head.max()) + pmv.plot_vector( + qx, + qy, + normalize=False, + color="0.75", + ) + cbar = plt.colorbar(cb, shrink=0.25) + cbar.ax.set_xlabel(r"Head, ($m$)") + ax.set_xlabel("x position (m)") + ax.set_ylabel("y position (m)") + styles.heading(ax, letter="A", heading="Layer 1") + + ax = fig.add_subplot(1, 2, 2, aspect="equal") + pmv = flopy.plot.PlotMapView(model=gwf, ax=ax, layer=1) + cb = pmv.plot_array(head, cmap="jet", vmin=0.0, vmax=head.max()) + pmv.plot_vector( + qx, + qy, + normalize=False, + color="0.75", ) - fig.savefig(fpth) - return + cbar = plt.colorbar(cb, shrink=0.25) + cbar.ax.set_xlabel(r"Head, ($m$)") + ax.set_xlabel("x position (m)") + ax.set_ylabel("y position (m)") + styles.heading(ax, letter="B", heading="Layer 2") + + if plot_show: + plt.show() + if plot_save: + fpth = os.path.join("..", "figures", f"{sim_name}-head.png") + fig.savefig(fpth) def plot_results(idx, sim, silent=True): - if config.plotModel: - if idx == 0: - plot_grid(idx, sim) - plot_head(idx, sim) - return + if idx == 0: + plot_grid(idx, sim) + plot_head(idx, sim) -# Function that wraps all of the steps for the FHB model -# -# 1. build_model, -# 2. write_model, -# 3. run_model, and -# 4. plot_results. +# - + +# ### Running the example # +# Define and invoke a function to run the example scenario, then plot results. -def simulation(idx, silent=True): - sim = build_model(sim_name) - write_model(sim, silent=silent) - success = run_model(sim, silent=silent) - if success: +# + +def scenario(idx, silent=True): + sim = build_models(sim_name) + if write: + write_models(sim, silent=silent) + if run: + run_models(sim, silent=silent) + if plot: plot_results(idx, sim, silent=silent) -# nosetest - exclude block from this nosetest to the next nosetest -def test_01(): - simulation(0, silent=False) - - -# nosetest end - -if __name__ == "__main__": - # ### DISVMESH Simulation - # - # Model grid and simulated heads in the DISVMESH model - - simulation(0) +scenario(0) +# - diff --git a/scripts/ex-gwf-drn-p01.py b/scripts/ex-gwf-drn-p01.py index 0f27d932a..7ad7e19e4 100644 --- a/scripts/ex-gwf-drn-p01.py +++ b/scripts/ex-gwf-drn-p01.py @@ -1,51 +1,50 @@ -# ## Unsaturated Zone Flow (UZF) Package problem 2 +# ## Unsaturated Zone Flow (UZF) Package example 2 # # This is the unsaturated zone package example problem (test 2) from the # Unsaturated Zone Flow Package documentation (Niswonger and others, 2006). # All reaches have been converted to rectangular reaches. -# + # ### UZF Package Problem 2 Setup # -# Imports +# ### Initial setup +# +# Import dependencies, define the example name and workspace, and read settings from environment variables. +# + import os -import sys +import pathlib as pl import flopy import matplotlib.pyplot as plt import numpy as np +import pooch +from flopy.plot.styles import styles +from modflow_devtools.latex import build_table, exp_format, float_format, int_format +from modflow_devtools.misc import get_env, timed -# Append to system path to include the common subdirectory - -sys.path.append(os.path.join("..", "common")) - -# import common functionality - -import build_table as bt -import config -from figspecs import USGSFigure - -# Set figure properties specific to the - -figure_size = (6.3, 5.6) -masked_values = (0, 1e30, -1e30) - -# Base simulation and model name and workspace - -ws = config.base_ws +# Example name and base workspace +sim_name = "ex-gwf-drn-p01" +workspace = pl.Path("../examples") -# Simulation name +# Settings from environment variables +write = get_env("WRITE", True) +run = get_env("RUN", True) +plot = get_env("PLOT", True) +plot_show = get_env("PLOT_SHOW", True) +plot_save = get_env("PLOT_SAVE", True) +# - -sim_name = "ex-gwf-drn-p01" +# ### Define parameters +# +# Define model units, parameters and other settings. +# + # Model units - length_units = "feet" time_units = "seconds" -# Model parameters - +# Scenario-specific parameters parameters = { "ex-gwf-drn-p01a": { "uzf_gwseep": None, @@ -55,9 +54,7 @@ }, } - -# Table UZF Package Problem 2 Model Parameters - +# Model parameters nper = 12 # Number of periods nlay = 1 # Number of layers nrow = 15 # Number of rows @@ -81,7 +78,6 @@ surf_dep = 1.0 # Surface depression depth ($ft$) # Static temporal data used by TDIS file - tdis_ds = [(2628000.0, 1, 1.0)] for n in range(nper - 1): tdis_ds.append((2628000.0, 15, 1.1)) @@ -92,29 +88,42 @@ shape3d = (nlay, nrow, ncol) # Load the idomain, top, bottom, and uzf/mvr arrays - -data_pth = os.path.join("..", "data", "ex-gwf-sfr-p01") -fpth = os.path.join(data_pth, "idomain.txt") +fpth = pooch.retrieve( + url=f"https://github.com/MODFLOW-USGS/modflow6-examples/raw/master/data/ex-gwf-sfr-p01/idomain.txt", + known_hash="md5:a0b12472b8624aecdc79e5c19c97040c", +) idomain = np.loadtxt(fpth, dtype=int) -fpth = os.path.join(data_pth, "bottom.txt") +fpth = pooch.retrieve( + url=f"https://github.com/MODFLOW-USGS/modflow6-examples/raw/master/data/ex-gwf-sfr-p01/bottom.txt", + known_hash="md5:fa5fe276f4f58a01eabfe88516cc90af", +) botm = np.loadtxt(fpth, dtype=float) -data_pth = os.path.join("..", "data", sim_name) -fpth = os.path.join(data_pth, "top.txt") +fpth = pooch.retrieve( + url=f"https://github.com/MODFLOW-USGS/modflow6-examples/raw/master/data/{sim_name}/top.txt", + known_hash="md5:88cc15f87824ebfd35ed5b4be7f68387", +) top = np.loadtxt(fpth, dtype=float) -fpth = os.path.join(data_pth, "infilt_mult.txt") +fpth = pooch.retrieve( + url=f"https://github.com/MODFLOW-USGS/modflow6-examples/raw/master/data/{sim_name}/infilt_mult.txt", + known_hash="md5:8bf0a48d604263cb35151587a9d8ca29", +) infilt_mult = np.loadtxt(fpth, dtype=float) -fpth = os.path.join(data_pth, "extwc_mult.txt") +fpth = pooch.retrieve( + url=f"https://github.com/MODFLOW-USGS/modflow6-examples/raw/master/data/{sim_name}/extwc_mult.txt", + known_hash="md5:6e289692a2b55b7bafb8bd9d71b0a2cb", +) extwc_mult = np.loadtxt(fpth, dtype=float) -fpth = os.path.join(data_pth, "routing_map.txt") +fpth = pooch.retrieve( + url=f"https://github.com/MODFLOW-USGS/modflow6-examples/raw/master/data/{sim_name}/routing_map.txt", + known_hash="md5:1bf9a6bb3513a184aa5093485e622f5b", +) routing_map = np.loadtxt(fpth, dtype=int) # convert routing map to zero-based reach numbers - routing_map -= 1 # Create hydraulic conductivity and specific yield - k11 = np.zeros(shape2d, dtype=float) k11[idomain == 1] = k11_stream k11[idomain == 2] = k11_basin @@ -123,7 +132,6 @@ sy[idomain == 2] = sy_basin # Infiltration rates - infiltration = ( 1.0e-9, 8.0e-9, @@ -140,7 +148,6 @@ ) # Pumping rates - well_rates = ( -2.0, -2.0, @@ -156,19 +163,13 @@ 0.0, ) - -# ### Create UZF Package Problem 2 Model Boundary Conditions -# # General head boundary conditions -# - ghb_spd = [ [0, 12, 0, 988.0, 0.038], [0, 13, 8, 1045.0, 0.038], ] # Well boundary conditions - wel_spd = {} for n in range(nper): q = well_rates[n] @@ -189,7 +190,6 @@ ] # Drain boundary - drn_spd = [] for i in range(nrow): for j in range(ncol): @@ -207,7 +207,6 @@ drn_spd.append(drncell) # UZF package - uzf_pakdata = [] iuzf = 0 for i in range(nrow): @@ -255,7 +254,6 @@ uzf_spd[n] = spd # SFR Package - sfr_pakdata = [ [ 0, @@ -894,8 +892,8 @@ [13, "stage", 1063.619], [14, "stage", 1061.581], ] -# MVR package +# MVR package mvr_paks = [ ["SFR-1"], ["UZF-1"], @@ -939,175 +937,163 @@ # Solver parameters - nouter = 100 ninner = 50 hclose = 1e-6 rclose = 1e-6 +# - - -# ### Functions to build, write, run, and plot the MODFLOW 6 UZF Package Problem 2 model +# ### Model setup # -# MODFLOW 6 flopy simulation object (sim) is returned if building the model - +# Define functions to build models, write input files, and run the simulation. + + +# + +def build_models(name, uzf_gwseep=None): + sim_ws = os.path.join(workspace, name) + sim = flopy.mf6.MFSimulation(sim_name=sim_name, sim_ws=sim_ws, exe_name="mf6") + flopy.mf6.ModflowTdis(sim, nper=nper, perioddata=tdis_ds, time_units=time_units) + flopy.mf6.ModflowIms( + sim, + print_option="summary", + linear_acceleration="bicgstab", + outer_maximum=nouter, + outer_dvclose=hclose, + inner_maximum=ninner, + inner_dvclose=hclose, + rcloserecord=f"{rclose} strict", + ) + gwf = flopy.mf6.ModflowGwf(sim, modelname=sim_name, newtonoptions="newton") + flopy.mf6.ModflowGwfdis( + gwf, + length_units=length_units, + nlay=nlay, + nrow=nrow, + ncol=ncol, + delr=delr, + delc=delc, + idomain=idomain, + top=top, + botm=botm, + ) + flopy.mf6.ModflowGwfnpf( + gwf, + icelltype=1, + k=k11, + ) + flopy.mf6.ModflowGwfsto( + gwf, + iconvert=1, + sy=sy, + ss=ss, + steady_state={0: True}, + transient={1: True}, + ) + flopy.mf6.ModflowGwfic(gwf, strt=strt) + flopy.mf6.ModflowGwfghb(gwf, stress_period_data=ghb_spd) + flopy.mf6.ModflowGwfwel(gwf, stress_period_data=wel_spd) + sfr = flopy.mf6.ModflowGwfsfr( + gwf, + pname="SFR-1", + length_conversion=3.28081, + mover=True, + nreaches=len(sfr_pakdata), + packagedata=sfr_pakdata, + connectiondata=sfr_conn, + diversions=sfr_div, + perioddata=sfr_spd, + ) + uzf = flopy.mf6.ModflowGwfuzf( + gwf, + pname="UZF-1", + simulate_gwseep=uzf_gwseep, + simulate_et=True, + linear_gwet=True, + boundnames=True, + mover=True, + nuzfcells=len(uzf_pakdata), + ntrailwaves=25, + nwavesets=20, + packagedata=uzf_pakdata, + perioddata=uzf_spd, + ) -def build_model(name, uzf_gwseep=None): - if config.buildModel: - sim_ws = os.path.join(ws, name) - sim = flopy.mf6.MFSimulation( - sim_name=sim_name, sim_ws=sim_ws, exe_name="mf6" - ) - flopy.mf6.ModflowTdis( - sim, nper=nper, perioddata=tdis_ds, time_units=time_units - ) - flopy.mf6.ModflowIms( - sim, - print_option="summary", - linear_acceleration="bicgstab", - outer_maximum=nouter, - outer_dvclose=hclose, - inner_maximum=ninner, - inner_dvclose=hclose, - rcloserecord=f"{rclose} strict", - ) - gwf = flopy.mf6.ModflowGwf( - sim, modelname=sim_name, newtonoptions="newton" - ) - flopy.mf6.ModflowGwfdis( - gwf, - length_units=length_units, - nlay=nlay, - nrow=nrow, - ncol=ncol, - delr=delr, - delc=delc, - idomain=idomain, - top=top, - botm=botm, - ) - flopy.mf6.ModflowGwfnpf( - gwf, - icelltype=1, - k=k11, - ) - flopy.mf6.ModflowGwfsto( - gwf, - iconvert=1, - sy=sy, - ss=ss, - steady_state={0: True}, - transient={1: True}, - ) - flopy.mf6.ModflowGwfic(gwf, strt=strt) - flopy.mf6.ModflowGwfghb(gwf, stress_period_data=ghb_spd) - flopy.mf6.ModflowGwfwel(gwf, stress_period_data=wel_spd) - sfr = flopy.mf6.ModflowGwfsfr( - gwf, - pname="SFR-1", - length_conversion=3.28081, - mover=True, - nreaches=len(sfr_pakdata), - packagedata=sfr_pakdata, - connectiondata=sfr_conn, - diversions=sfr_div, - perioddata=sfr_spd, + mvr_packages = mvr_paks.copy() + mvr_spd = uzf_mvr_spd.copy() + obs_file = f"{sim_name}.surfrate.obs" + csv_file = obs_file + ".csv" + if uzf_gwseep: + obs_dict = { + csv_file: [ + ("surfrate", "uzf-gwd-to-mvr", "surfrate"), + ("netinfil", "net-infiltration", "surfrate"), + ] + } + uzf.obs.initialize( + filename=obs_file, + digits=10, + print_input=True, + continuous=obs_dict, ) - uzf = flopy.mf6.ModflowGwfuzf( + else: + mvr_packages.append(["DRN-1"]) + mvr_spd += drn_mvr_spd.copy() + drn = flopy.mf6.ModflowGwfdrn( gwf, - pname="UZF-1", - simulate_gwseep=uzf_gwseep, - simulate_et=True, - linear_gwet=True, + pname="DRN-1", + auxiliary=[("draindepth")], + auxdepthname="draindepth", boundnames=True, mover=True, - nuzfcells=len(uzf_pakdata), - ntrailwaves=25, - nwavesets=20, - packagedata=uzf_pakdata, - perioddata=uzf_spd, - ) - - mvr_packages = mvr_paks.copy() - mvr_spd = uzf_mvr_spd.copy() - obs_file = f"{sim_name}.surfrate.obs" - csv_file = obs_file + ".csv" - if uzf_gwseep: - obs_dict = { - csv_file: [ - ("surfrate", "uzf-gwd-to-mvr", "surfrate"), - ("netinfil", "net-infiltration", "surfrate"), - ] - } - uzf.obs.initialize( - filename=obs_file, - digits=10, - print_input=True, - continuous=obs_dict, - ) - else: - mvr_packages.append(["DRN-1"]) - mvr_spd += drn_mvr_spd.copy() - drn = flopy.mf6.ModflowGwfdrn( - gwf, - pname="DRN-1", - auxiliary=[("draindepth")], - auxdepthname="draindepth", - boundnames=True, - mover=True, - stress_period_data=drn_spd, - ) - obs_dict = { - csv_file: [ - ("surfrate", "to-mvr", "surfrate"), - ] - } - drn.obs.initialize( - filename=obs_file, - digits=10, - print_input=True, - continuous=obs_dict, - ) - - flopy.mf6.ModflowGwfmvr( - gwf, - maxpackages=len(mvr_packages), - maxmvr=len(mvr_spd), - packages=mvr_packages, - perioddata=mvr_spd, + stress_period_data=drn_spd, ) - - flopy.mf6.ModflowGwfoc( - gwf, - printrecord=[("BUDGET", "LAST")], + obs_dict = { + csv_file: [ + ("surfrate", "to-mvr", "surfrate"), + ] + } + drn.obs.initialize( + filename=obs_file, + digits=10, + print_input=True, + continuous=obs_dict, ) - return sim - return None + flopy.mf6.ModflowGwfmvr( + gwf, + maxpackages=len(mvr_packages), + maxmvr=len(mvr_spd), + packages=mvr_packages, + perioddata=mvr_spd, + ) -# Function to write MODFLOW 6 UZF Package Problem 2 model files + flopy.mf6.ModflowGwfoc( + gwf, + printrecord=[("BUDGET", "LAST")], + ) + return sim -def write_model(sim, silent=True): - if config.writeModel: - sim.write_simulation(silent=silent) +def write_models(sim, silent=True): + sim.write_simulation(silent=silent) -# Function to run the UZF Package Problem 2 model. -# True is returned if the model runs successfully -# +@timed +def run_models(sim, silent=True): + success, buff = sim.run_simulation(silent=silent) + assert success, buff -@config.timeit -def run_model(sim, silent=True): - success = True - if config.runModel: - success, buff = sim.run_simulation(silent=silent) - if not success: - print(buff) - return success +# - +# ### Plotting results +# +# Define functions to plot model results. -# plot vertical bars for stress periods +# + +# Figure properties +figure_size = (6.3, 5.6) +masked_values = (0, 1e30, -1e30) def plot_stress_periods(ax): @@ -1131,135 +1117,126 @@ def plot_stress_periods(ax): ec="none", ) x0 = x1 - return - - -# plot the groundwater seepage results def plot_gwseep_results(silent=True): - fs = USGSFigure(figure_type="graph", verbose=False) - - # load the observations - name = list(parameters.keys())[0] - fpth = os.path.join(ws, name, f"{sim_name}.surfrate.obs.csv") - drn = flopy.utils.Mf6Obs(fpth).data - name = list(parameters.keys())[1] - fpth = os.path.join(ws, name, f"{sim_name}.surfrate.obs.csv") - uzf = flopy.utils.Mf6Obs(fpth).data - - time = drn["totim"] / 86400.0 - q0 = drn["SURFRATE"] - q1 = uzf["SURFRATE"] - mean_error = np.mean(q0 - q1) - - # create the figure - fig, axes = plt.subplots( - ncols=2, - nrows=1, - sharex=True, - figsize=(6.3, 3.15), - constrained_layout=True, - ) - - ax = axes[0] - ax.set_xlim(0, 365) - ax.set_ylim(0, 175) + """Plot groundwater seepage results""" + with styles.USGSPlot() as fs: + # load the observations + name = list(parameters.keys())[0] + fpth = os.path.join(workspace, name, f"{sim_name}.surfrate.obs.csv") + drn = flopy.utils.Mf6Obs(fpth).data + name = list(parameters.keys())[1] + fpth = os.path.join(workspace, name, f"{sim_name}.surfrate.obs.csv") + uzf = flopy.utils.Mf6Obs(fpth).data + + time = drn["totim"] / 86400.0 + q0 = drn["SURFRATE"] + q1 = uzf["SURFRATE"] + mean_error = np.mean(q0 - q1) + + # create the figure + fig, axes = plt.subplots( + ncols=2, + nrows=1, + sharex=True, + figsize=(6.3, 3.15), + constrained_layout=True, + ) - xp, yp = [0.0], [uzf["NETINFIL"][0]] - for idx in range(time.shape[0]): - if idx == 0: - x0, x1 = 0.0, time[idx] - else: - x0, x1 = time[idx - 1], time[idx] - y2 = uzf["NETINFIL"][idx] - xp.append(x0) - xp.append(x1) - yp.append(y2) - yp.append(y2) - ax.fill_between( - [x0, x1], - 0, - y2=y2, - lw=0, + ax = axes[0] + ax.set_xlim(0, 365) + ax.set_ylim(0, 175) + + xp, yp = [0.0], [uzf["NETINFIL"][0]] + for idx in range(time.shape[0]): + if idx == 0: + x0, x1 = 0.0, time[idx] + else: + x0, x1 = time[idx - 1], time[idx] + y2 = uzf["NETINFIL"][idx] + xp.append(x0) + xp.append(x1) + yp.append(y2) + yp.append(y2) + ax.fill_between( + [x0, x1], + 0, + y2=y2, + lw=0, + color="blue", + step="post", + ec="none", + zorder=100, + ) + ax.plot(xp, yp, lw=0.5, color="black", zorder=101) + plot_stress_periods(ax) + styles.heading(ax, idx=0) + + ax.set_xlabel("Simulation time, in days") + ax.set_ylabel("Infiltration to the unsaturated zone,\nin cubic feet per second") + + ax = axes[-1] + ax.set_xlim(0, 365) + ax.set_ylim(50.8, 51.8) + ax.plot( + time, + -drn["SURFRATE"], + lw=0.75, + ls="-", color="blue", - step="post", - ec="none", - zorder=100, + label="Drainage package", ) - ax.plot(xp, yp, lw=0.5, color="black", zorder=101) - plot_stress_periods(ax) - fs.heading(ax, idx=0) - - ax.set_xlabel("Simulation time, in days") - ax.set_ylabel( - "Infiltration to the unsaturated zone,\nin cubic feet per second" - ) - - ax = axes[-1] - ax.set_xlim(0, 365) - ax.set_ylim(50.8, 51.8) - ax.plot( - time, - -drn["SURFRATE"], - lw=0.75, - ls="-", - color="blue", - label="Drainage package", - ) - ax.plot( - time, - -uzf["SURFRATE"], - marker="o", - ms=3, - mfc="none", - mec="black", - markeredgewidth=0.5, - lw=0.0, - ls="-", - color="red", - label="UZF groundwater seepage", - ) - plot_stress_periods(ax) - - fs.graph_legend( - ax, loc="upper center", ncol=1, frameon=True, edgecolor="none" - ) - fs.heading(ax, idx=1) - fs.add_text( - ax, - f"Mean Error {mean_error:.2e} cubic feet per second", - bold=False, - italic=False, - x=1.0, - y=1.01, - va="bottom", - ha="right", - fontsize=7, - ) + ax.plot( + time, + -uzf["SURFRATE"], + marker="o", + ms=3, + mfc="none", + mec="black", + markeredgewidth=0.5, + lw=0.0, + ls="-", + color="red", + label="UZF groundwater seepage", + ) + plot_stress_periods(ax) - ax.set_xlabel("Simulation time, in days") - ax.set_ylabel( - "Groundwater seepage to the land surface,\nin cubic feet per second" - ) + styles.graph_legend( + ax, loc="upper center", ncol=1, frameon=True, edgecolor="none" + ) + styles.heading(ax, idx=1) + styles.add_text( + ax, + f"Mean Error {mean_error:.2e} cubic feet per second", + bold=False, + italic=False, + x=1.0, + y=1.01, + va="bottom", + ha="right", + fontsize=7, + ) - # save figure - if config.plotSave: - fpth = os.path.join( - "..", - "figures", - f"{sim_name}-01{config.figure_ext}", + ax.set_xlabel("Simulation time, in days") + ax.set_ylabel( + "Groundwater seepage to the land surface,\nin cubic feet per second" ) - fig.savefig(fpth) - return + if plot_show: + plt.show() + if plot_save: + fpth = os.path.join( + "..", + "figures", + f"{sim_name}-01.png", + ) + fig.savefig(fpth) def export_tables(silent=True): - if config.plotSave: - caption = "Infiltration and pumping rates for example {}.".format( - sim_name - ) + if plot_save: + caption = "Infiltration and pumping rates for example {}.".format(sim_name) headings = ( "Stress period", "Infiltration rate", @@ -1273,64 +1250,52 @@ def export_tables(silent=True): ] arr = np.zeros(nper, dtype=dtype) for n in range(nper): - arr["nper"][n] = bt.int_format(n + 1) - arr["infilt"][n] = bt.exp_format(infiltration[n]) - arr["rate"][n] = bt.float_format(well_rates[n]) + arr["nper"][n] = int_format(n + 1) + arr["infilt"][n] = exp_format(infiltration[n]) + arr["rate"][n] = float_format(well_rates[n]) if not silent: print(f"creating...'{fpth}'") col_widths = (0.1, 0.30, 0.30) - bt.build_table( - caption, fpth, arr, headings=headings, col_widths=col_widths - ) - - -# Function to plot the UZF Package Problem 2 model results. + build_table(caption, fpth, arr, headings=headings, col_widths=col_widths) def plot_results(silent=True): - if config.plotModel: - plot_gwseep_results(silent=silent) - export_tables(silent=silent) + if not plot: + return + plot_gwseep_results(silent=silent) + export_tables(silent=silent) -# Function that wraps all of the steps for the UZF Package Problem 2 model -# -# 1. build_model, -# 2. write_model, -# 3. run_model, and -# 4. plot_results. +# - + +# ### Running the example # +# Define and invoke a function to run the example scenario, then plot results. -def simulation(idx, silent=True): +# + +def scenario(idx, silent=True): key = list(parameters.keys())[idx] params = parameters[key].copy() + sim = build_models(key, **params) + if write: + write_models(sim, silent=silent) + if run: + run_models(sim, silent=silent) - sim = build_model(key, **params) - - write_model(sim, silent=silent) - - success = run_model(sim, silent=silent) - assert success, f"could not run...{sim_name}" +# - -# nosetest - exclude block from this nosetest to the next nosetest -def test_and_plot(): - simulation(0, silent=False) - simulation(1, silent=False) - plot_results(silent=False) +# Use the drain approach to simulate discharge to the land surface. -# nosetest end +scenario(0) -if __name__ == "__main__": - # ### UZF Package Problem 2 Simulation +# Use UZF to simulate discharge to the land surface. - # drain used to simulate discharge to the land surface - simulation(0) +scenario(1) - # uzf used to simulate discharge to the land surface - simulation(1) +# Plot the results. - # plot the results +if plot: plot_results() diff --git a/scripts/ex-gwf-fhb.py b/scripts/ex-gwf-fhb.py index bd9269bbd..16847f830 100644 --- a/scripts/ex-gwf-fhb.py +++ b/scripts/ex-gwf-fhb.py @@ -6,45 +6,41 @@ # versions of MODFLOW. # -# ### FHB Problem Setup +# ### Initial setup # -# Imports +# Import dependencies, define the example name and workspace, and read settings from environment variables. +# + import os -import sys +import pathlib as pl import flopy import matplotlib.pyplot as plt -import numpy as np +from flopy.plot.styles import styles +from modflow_devtools.misc import get_env, timed -# Append to system path to include the common subdirectory - -sys.path.append(os.path.join("..", "common")) - -# import common functionality - -import config -from figspecs import USGSFigure - -# Set default figure properties - -figure_size = (4, 4) - -# Base simulation and model name and workspace - -ws = config.base_ws +# Example name and base workspace +sim_name = "ex-gwf-fhb" +ws = pl.Path("../examples") -# Simulation name +# Settings from environment variables +write = get_env("WRITE", True) +run = get_env("RUN", True) +plot = get_env("PLOT", True) +plot_show = get_env("PLOT_SHOW", True) +plot_save = get_env("PLOT_SAVE", True) +# - -sim_name = "ex-gwf-fhb" +# ### Define parameters +# +# Define model units, parameters and other settings. +# + # Model units - length_units = "meters" time_units = "days" -# Table FHB Model Parameters - +# Model parameters nper = 3 # Number of periods nlay = 1 # Number of layers ncol = 10 # Number of columns @@ -62,251 +58,221 @@ # Simulation has 1 steady stress period (1 day) # and 3 transient stress periods (10 days each). # Each transient stress period has 120 2-hour time steps. - perlen = [400.0, 200.0, 400.0] nstp = [10, 4, 6] tsmult = [1.0, 1.0, 1.0] tdis_ds = list(zip(perlen, nstp, tsmult)) -# parse parameter strings into tuples - +# Parse parameter strings into tuples botm = [float(value) for value in botm_str.split(",")] k11 = [float(value) for value in k11_str.split(",")] icelltype = [int(value) for value in icelltype_str.split(",")] # Solver parameters - nouter = 50 ninner = 100 hclose = 1e-9 rclose = 1e-6 +# - -# ### Functions to build, write, run, and plot the MODFLOW 6 FHB model +# ### Model setup # -# MODFLOW 6 flopy simulation object (sim) is returned if building the model - - -def build_model(): - if config.buildModel: - sim_ws = os.path.join(ws, sim_name) - sim = flopy.mf6.MFSimulation( - sim_name=sim_name, sim_ws=sim_ws, exe_name="mf6" - ) - flopy.mf6.ModflowTdis( - sim, nper=nper, perioddata=tdis_ds, time_units=time_units - ) - flopy.mf6.ModflowIms( - sim, - outer_maximum=nouter, - outer_dvclose=hclose, - inner_maximum=ninner, - inner_dvclose=hclose, - rcloserecord=f"{rclose} strict", - ) - gwf = flopy.mf6.ModflowGwf(sim, modelname=sim_name, save_flows=True) - flopy.mf6.ModflowGwfdis( - gwf, - length_units=length_units, - nlay=nlay, - nrow=nrow, - ncol=ncol, - delr=delr, - delc=delc, - top=top, - botm=botm, - ) - flopy.mf6.ModflowGwfnpf( - gwf, - icelltype=icelltype, - k=k11, - save_specific_discharge=True, - ) - flopy.mf6.ModflowGwfic(gwf, strt=strt) - flopy.mf6.ModflowGwfsto( - gwf, - storagecoefficient=True, - iconvert=0, - ss=1.0e-6, - sy=None, - transient={0: True}, - ) - - chd_spd = [] - chd_spd += [[0, i, 9, "CHDHEAD"] for i in range(3)] - chd_spd = {0: chd_spd} - tsdata = [(0.0, 0.0), (307.0, 1.0), (791.0, 5.0), (1000.0, 2.0)] - tsdict = { - "timeseries": tsdata, - "time_series_namerecord": "CHDHEAD", - "interpolation_methodrecord": "LINEAREND", - } - flopy.mf6.ModflowGwfchd( - gwf, - stress_period_data=chd_spd, - timeseries=tsdict, - pname="CHD", - ) - - wel_spd = [] - wel_spd += [[0, 1, 0, "FLOWRATE"]] - wel_spd = {0: wel_spd} - tsdata = [ - (0.0, 2000.0), - (307.0, 6000.0), - (791.0, 5000.0), - (1000.0, 9000.0), - ] - tsdict = { - "timeseries": tsdata, - "time_series_namerecord": "FLOWRATE", - "interpolation_methodrecord": "LINEAREND", - } - flopy.mf6.ModflowGwfwel( - gwf, - stress_period_data=wel_spd, - timeseries=tsdict, - pname="WEL", - ) - - head_filerecord = f"{sim_name}.hds" - budget_filerecord = f"{sim_name}.cbc" - flopy.mf6.ModflowGwfoc( - gwf, - head_filerecord=head_filerecord, - budget_filerecord=budget_filerecord, - saverecord=[("HEAD", "ALL"), ("BUDGET", "ALL")], - ) - - obsdict = {} - obslist = [ - ["h1_2_1", "head", (0, 1, 0)], - ["h1_2_10", "head", (0, 1, 9)], - ] - obsdict[f"{sim_name}.obs.head.csv"] = obslist - obslist = [["icf1", "flow-ja-face", (0, 1, 1), (0, 1, 0)]] - obsdict[f"{sim_name}.obs.flow.csv"] = obslist - obs = flopy.mf6.ModflowUtlobs( - gwf, print_input=False, continuous=obsdict - ) - - return sim - return None +# Define functions to build models, write input files, and run the simulation. -# Function to write MODFLOW 6 FHB model files +# + +def build_models(): + sim_ws = os.path.join(ws, sim_name) + sim = flopy.mf6.MFSimulation(sim_name=sim_name, sim_ws=sim_ws, exe_name="mf6") + flopy.mf6.ModflowTdis(sim, nper=nper, perioddata=tdis_ds, time_units=time_units) + flopy.mf6.ModflowIms( + sim, + outer_maximum=nouter, + outer_dvclose=hclose, + inner_maximum=ninner, + inner_dvclose=hclose, + rcloserecord=f"{rclose} strict", + ) + gwf = flopy.mf6.ModflowGwf(sim, modelname=sim_name, save_flows=True) + flopy.mf6.ModflowGwfdis( + gwf, + length_units=length_units, + nlay=nlay, + nrow=nrow, + ncol=ncol, + delr=delr, + delc=delc, + top=top, + botm=botm, + ) + flopy.mf6.ModflowGwfnpf( + gwf, + icelltype=icelltype, + k=k11, + save_specific_discharge=True, + ) + flopy.mf6.ModflowGwfic(gwf, strt=strt) + flopy.mf6.ModflowGwfsto( + gwf, + storagecoefficient=True, + iconvert=0, + ss=1.0e-6, + sy=None, + transient={0: True}, + ) + + chd_spd = [] + chd_spd += [[0, i, 9, "CHDHEAD"] for i in range(3)] + chd_spd = {0: chd_spd} + tsdata = [(0.0, 0.0), (307.0, 1.0), (791.0, 5.0), (1000.0, 2.0)] + tsdict = { + "timeseries": tsdata, + "time_series_namerecord": "CHDHEAD", + "interpolation_methodrecord": "LINEAREND", + } + flopy.mf6.ModflowGwfchd( + gwf, + stress_period_data=chd_spd, + timeseries=tsdict, + pname="CHD", + ) + + wel_spd = [] + wel_spd += [[0, 1, 0, "FLOWRATE"]] + wel_spd = {0: wel_spd} + tsdata = [ + (0.0, 2000.0), + (307.0, 6000.0), + (791.0, 5000.0), + (1000.0, 9000.0), + ] + tsdict = { + "timeseries": tsdata, + "time_series_namerecord": "FLOWRATE", + "interpolation_methodrecord": "LINEAREND", + } + flopy.mf6.ModflowGwfwel( + gwf, + stress_period_data=wel_spd, + timeseries=tsdict, + pname="WEL", + ) + + head_filerecord = f"{sim_name}.hds" + budget_filerecord = f"{sim_name}.cbc" + flopy.mf6.ModflowGwfoc( + gwf, + head_filerecord=head_filerecord, + budget_filerecord=budget_filerecord, + saverecord=[("HEAD", "ALL"), ("BUDGET", "ALL")], + ) + + obsdict = {} + obslist = [ + ["h1_2_1", "head", (0, 1, 0)], + ["h1_2_10", "head", (0, 1, 9)], + ] + obsdict[f"{sim_name}.obs.head.csv"] = obslist + obslist = [["icf1", "flow-ja-face", (0, 1, 1), (0, 1, 0)]] + obsdict[f"{sim_name}.obs.flow.csv"] = obslist + obs = flopy.mf6.ModflowUtlobs(gwf, print_input=False, continuous=obsdict) + return sim -def write_model(sim, silent=True): - if config.writeModel: - sim.write_simulation(silent=silent) +def write_models(sim, silent=True): + sim.write_simulation(silent=silent) -# Function to run the FHB model. -# True is returned if the model runs successfully -# +@timed +def run_models(sim, silent=False): + success, buff = sim.run_simulation(silent=silent, report=True) + assert success, buff -@config.timeit -def run_model(sim, silent=False): - success = True - if config.runModel: - success, buff = sim.run_simulation(silent=silent, report=True) - if not success: - print(buff) - return success +# - -# Function to plot the FHB model results. +# ### Plotting results # -def plot_grid(sim): - fs = USGSFigure(figure_type="map", verbose=False) - sim_ws = os.path.join(ws, sim_name) - gwf = sim.get_model(sim_name) +# Define functions to plot model results. - fig = plt.figure(figsize=(4, 3.0)) - fig.tight_layout() +# + +# Figure properties +figure_size = (4, 4) - ax = fig.add_subplot(1, 1, 1, aspect="equal") - pmv = flopy.plot.PlotMapView(model=gwf, ax=ax, layer=0) - pmv.plot_grid() - pmv.plot_bc(name="CHD") - pmv.plot_bc(name="WEL") - ax.set_xlabel("x position (m)") - ax.set_ylabel("y position (m)") - # save figure - if config.plotSave: - fpth = os.path.join( - "..", "figures", f"{sim_name}-grid{config.figure_ext}" - ) - fig.savefig(fpth) - return +def plot_grid(sim): + with styles.USGSMap(): + gwf = sim.get_model(sim_name) + + fig = plt.figure(figsize=(4, 3.0)) + fig.tight_layout() + + ax = fig.add_subplot(1, 1, 1, aspect="equal") + pmv = flopy.plot.PlotMapView(model=gwf, ax=ax, layer=0) + pmv.plot_grid() + pmv.plot_bc(name="CHD") + pmv.plot_bc(name="WEL") + ax.set_xlabel("x position (m)") + ax.set_ylabel("y position (m)") + + if plot_show: + plt.show() + if plot_save: + fpth = os.path.join("..", "figures", f"{sim_name}-grid.png") + fig.savefig(fpth) def plot_ts(sim): - fs = USGSFigure(figure_type="graph", verbose=False) - sim_ws = os.path.join(ws, sim_name) - gwf = sim.get_model(sim_name) - obsnames = gwf.obs.output.obs_names - obs_list = [ - gwf.obs.output.obs(f=obsnames[0]), - gwf.obs.output.obs(f=obsnames[1]), - ] - ylabel = ["head (m)", "flow ($m^3/d$)"] - obs_fig = ("obs-head", "obs-flow", "ghb-obs") - for iplot, obstype in enumerate(obs_list): - fig = plt.figure(figsize=(5, 3)) - ax = fig.add_subplot() - tsdata = obstype.data - for name in tsdata.dtype.names[1:]: - ax.plot(tsdata["totim"], tsdata[name], label=name, marker="o") - ax.set_xlabel("time (d)") - ax.set_ylabel(ylabel[iplot]) - fs.graph_legend(ax) - if config.plotSave: - fpth = os.path.join( - "..", - "figures", - "{}-{}{}".format(sim_name, obs_fig[iplot], config.figure_ext), - ) - fig.savefig(fpth) - return + with styles.USGSPlot(): + gwf = sim.get_model(sim_name) + obsnames = gwf.obs.output.obs_names + obs_list = [ + gwf.obs.output.obs(f=obsnames[0]), + gwf.obs.output.obs(f=obsnames[1]), + ] + ylabel = ["head (m)", "flow ($m^3/d$)"] + obs_fig = ("obs-head", "obs-flow", "ghb-obs") + for iplot, obstype in enumerate(obs_list): + fig = plt.figure(figsize=(5, 3)) + ax = fig.add_subplot() + tsdata = obstype.data + for name in tsdata.dtype.names[1:]: + ax.plot(tsdata["totim"], tsdata[name], label=name, marker="o") + ax.set_xlabel("time (d)") + ax.set_ylabel(ylabel[iplot]) + styles.graph_legend(ax) + if plot_save: + fpth = os.path.join( + "..", + "figures", + "{}-{}{}".format(sim_name, obs_fig[iplot], ".png"), + ) + fig.savefig(fpth) def plot_results(sim, silent=True): - if config.plotModel: - plot_grid(sim) - plot_ts(sim) - return + plot_grid(sim) + plot_ts(sim) -# Function that wraps all of the steps for the FHB model -# -# 1. build_model, -# 2. write_model, -# 3. run_model, and -# 4. plot_results. +# - + +# ### Running the example # +# Define and invoke a function to run the example scenario, then plot results. -def simulation(silent=True): - sim = build_model() - write_model(sim, silent=silent) - success = run_model(sim, silent=silent) - if success: +# + +def scenario(silent=True): + sim = build_models() + if write: + write_models(sim, silent=silent) + if run: + run_models(sim, silent=silent) + if plot: plot_results(sim, silent=silent) -# nosetest - exclude block from this nosetest to the next nosetest -def test_01(): - simulation(silent=False) - - -# nosetest end - -if __name__ == "__main__": - # ### FHB Simulation - # - # Model grid and simulation results - - simulation() +scenario() +# - diff --git a/scripts/ex-gwf-hani.py b/scripts/ex-gwf-hani.py index 5477f7371..884a61626 100644 --- a/scripts/ex-gwf-hani.py +++ b/scripts/ex-gwf-hani.py @@ -4,52 +4,50 @@ # response of an anisotropic confined aquifer to a pumping well. A # constant-head boundary condition surrounds the active domain. K22 is set # to 0.01. Drawdown is more pronounced in the K11 direction. -# -# ### Hani Problem Setup +# ### Initial setup # -# Imports +# Import dependencies, define the example name and workspace, and read settings from environment variables. +# + import os -import sys +import pathlib as pl import flopy import flopy.utils.cvfdutil import matplotlib.pyplot as plt import numpy as np +from flopy.plot.styles import styles +from modflow_devtools.misc import get_env, timed -# Append to system path to include the common subdirectory - -sys.path.append(os.path.join("..", "common")) - -# import common functionality - -import config -from figspecs import USGSFigure - -# Set default figure properties - -figure_size = (3.5, 3.5) +# Base workspace +workspace = pl.Path("../examples") -# Base simulation and model name and workspace +# Settings from environment variables +write = get_env("WRITE", True) +run = get_env("RUN", True) +plot = get_env("PLOT", True) +plot_show = get_env("PLOT_SHOW", True) +plot_save = get_env("PLOT_SAVE", True) +# - -ws = config.base_ws +# ### Define parameters +# +# Define model units, parameters and other settings. +# + # Model units - length_units = "meters" time_units = "days" -# Scenario parameters - +# Scenario-specific parameters parameters = { "ex-gwf-hanir": {"angle1": 0, "xt3d": False}, "ex-gwf-hanix": {"angle1": 25, "xt3d": True}, "ex-gwf-hanic": {"angle1": 90, "xt3d": False}, } -# Table Hani Model Parameters - +# Model parameters nper = 1 # Number of periods nlay = 1 # Number of layers nrow = 51 # Number of rows @@ -76,211 +74,181 @@ ninner = 100 hclose = 1e-9 rclose = 1e-6 +# - -# ### Functions to build, write, run, and plot the MODFLOW 6 Hani model +# ### Model setup # -# MODFLOW 6 flopy simulation object (sim) is returned if building the model - - -def build_model(sim_name, angle1, xt3d): - if config.buildModel: - sim_ws = os.path.join(ws, sim_name) - sim = flopy.mf6.MFSimulation( - sim_name=sim_name, sim_ws=sim_ws, exe_name="mf6" - ) - flopy.mf6.ModflowTdis( - sim, nper=nper, perioddata=tdis_ds, time_units=time_units - ) - flopy.mf6.ModflowIms( - sim, - linear_acceleration="bicgstab", - outer_maximum=nouter, - outer_dvclose=hclose, - inner_maximum=ninner, - inner_dvclose=hclose, - rcloserecord=f"{rclose} strict", - ) - gwf = flopy.mf6.ModflowGwf(sim, modelname=sim_name, save_flows=True) - flopy.mf6.ModflowGwfdis( - gwf, - length_units=length_units, - nlay=nlay, - nrow=nrow, - ncol=ncol, - top=top, - botm=botm, - ) - flopy.mf6.ModflowGwfnpf( - gwf, - icelltype=icelltype, - k=k11, - k22=k22, - angle1=angle1, - save_specific_discharge=True, - xt3doptions=xt3d, - ) - flopy.mf6.ModflowGwfic(gwf, strt=strt) - - ibd = -1 * np.ones((nrow, ncol), dtype=int) - ibd[1:-1, 1:-1] = 1 - chdrow, chdcol = np.where(ibd == -1) - chd_spd = [[0, i, j, 0.0] for i, j in zip(chdrow, chdcol)] - flopy.mf6.ModflowGwfchd( - gwf, - stress_period_data=chd_spd, - pname="CHD", - ) - flopy.mf6.ModflowGwfwel( - gwf, - stress_period_data=[0, 25, 25, pumping_rate], - pname="WEL", - ) - head_filerecord = f"{sim_name}.hds" - budget_filerecord = f"{sim_name}.cbc" - flopy.mf6.ModflowGwfoc( - gwf, - head_filerecord=head_filerecord, - budget_filerecord=budget_filerecord, - saverecord=[("HEAD", "ALL"), ("BUDGET", "ALL")], - ) - - return sim - return None - - -# Function to write MODFLOW 6 Hani model files - - -def write_model(sim, silent=True): - if config.writeModel: - sim.write_simulation(silent=silent) - - -# Function to run the FHB model. -# True is returned if the model runs successfully +# Define functions to build models, write input files, and run the simulation. + + +# + +def build_models(sim_name, angle1, xt3d): + sim_ws = os.path.join(workspace, sim_name) + sim = flopy.mf6.MFSimulation(sim_name=sim_name, sim_ws=sim_ws, exe_name="mf6") + flopy.mf6.ModflowTdis(sim, nper=nper, perioddata=tdis_ds, time_units=time_units) + flopy.mf6.ModflowIms( + sim, + linear_acceleration="bicgstab", + outer_maximum=nouter, + outer_dvclose=hclose, + inner_maximum=ninner, + inner_dvclose=hclose, + rcloserecord=f"{rclose} strict", + ) + gwf = flopy.mf6.ModflowGwf(sim, modelname=sim_name, save_flows=True) + flopy.mf6.ModflowGwfdis( + gwf, + length_units=length_units, + nlay=nlay, + nrow=nrow, + ncol=ncol, + top=top, + botm=botm, + ) + flopy.mf6.ModflowGwfnpf( + gwf, + icelltype=icelltype, + k=k11, + k22=k22, + angle1=angle1, + save_specific_discharge=True, + xt3doptions=xt3d, + ) + flopy.mf6.ModflowGwfic(gwf, strt=strt) + + ibd = -1 * np.ones((nrow, ncol), dtype=int) + ibd[1:-1, 1:-1] = 1 + chdrow, chdcol = np.where(ibd == -1) + chd_spd = [[0, i, j, 0.0] for i, j in zip(chdrow, chdcol)] + flopy.mf6.ModflowGwfchd( + gwf, + stress_period_data=chd_spd, + pname="CHD", + ) + flopy.mf6.ModflowGwfwel( + gwf, + stress_period_data=[0, 25, 25, pumping_rate], + pname="WEL", + ) + head_filerecord = f"{sim_name}.hds" + budget_filerecord = f"{sim_name}.cbc" + flopy.mf6.ModflowGwfoc( + gwf, + head_filerecord=head_filerecord, + budget_filerecord=budget_filerecord, + saverecord=[("HEAD", "ALL"), ("BUDGET", "ALL")], + ) + + return sim + + +def write_models(sim, silent=True): + sim.write_simulation(silent=silent) + + +@timed +def run_models(sim, silent=False): + success, buff = sim.run_simulation(silent=silent, report=True) + assert success, buff + + +# - + +# ### Plotting results # +# Define functions to plot model results. - -@config.timeit -def run_model(sim, silent=False): - success = True - if config.runModel: - success, buff = sim.run_simulation(silent=silent, report=True) - if not success: - print(buff) - return success +# + +# Set default figure properties +figure_size = (3.5, 3.5) -# Function to plot the Hani model results. -# def plot_grid(idx, sim): - fs = USGSFigure(figure_type="map", verbose=False) - sim_name = list(parameters.keys())[idx] - sim_ws = os.path.join(ws, sim_name) - gwf = sim.get_model(sim_name) - - fig = plt.figure(figsize=figure_size) - fig.tight_layout() - - ax = fig.add_subplot(1, 1, 1, aspect="equal") - pmv = flopy.plot.PlotMapView(model=gwf, ax=ax, layer=0) - pmv.plot_grid() - pmv.plot_bc(name="CHD") - pmv.plot_bc(name="WEL") - ax.set_xlabel("x position (m)") - ax.set_ylabel("y position (m)") - - # save figure - if config.plotSave: - fpth = os.path.join( - "..", "figures", f"{sim_name}-grid{config.figure_ext}" - ) - fig.savefig(fpth) - return + with styles.USGSMap() as fs: + sim_name = list(parameters.keys())[idx] + sim_ws = os.path.join(workspace, sim_name) + gwf = sim.get_model(sim_name) + fig = plt.figure(figsize=figure_size) + fig.tight_layout() -def plot_head(idx, sim): - fs = USGSFigure(figure_type="map", verbose=False) - sim_name = list(parameters.keys())[idx] - sim_ws = os.path.join(ws, sim_name) - gwf = sim.get_model(sim_name) - - fig = plt.figure(figsize=figure_size) - fig.tight_layout() - - head = gwf.output.head().get_data() - - ax = fig.add_subplot(1, 1, 1, aspect="equal") - pmv = flopy.plot.PlotMapView(model=gwf, ax=ax, layer=0) - cb = pmv.plot_array(0 - head, cmap="jet", alpha=0.25) - cs = pmv.contour_array(0 - head, levels=np.arange(0.1, 1, 0.1)) - cbar = plt.colorbar(cb, shrink=0.25) - cbar.ax.set_xlabel(r"Drawdown, ($m$)") - ax.set_xlabel("x position (m)") - ax.set_ylabel("y position (m)") - - # save figure - if config.plotSave: - fpth = os.path.join( - "..", "figures", f"{sim_name}-head{config.figure_ext}" - ) - fig.savefig(fpth) - return + ax = fig.add_subplot(1, 1, 1, aspect="equal") + pmv = flopy.plot.PlotMapView(model=gwf, ax=ax, layer=0) + pmv.plot_grid() + pmv.plot_bc(name="CHD") + pmv.plot_bc(name="WEL") + ax.set_xlabel("x position (m)") + ax.set_ylabel("y position (m)") + if plot_show: + plt.show() + if plot_save: + fpth = os.path.join("..", "figures", f"{sim_name}-grid.png") + fig.savefig(fpth) -def plot_results(idx, sim, silent=True): - if config.plotModel: - if idx == 0: - plot_grid(idx, sim) - plot_head(idx, sim) - return +def plot_head(idx, sim): + with styles.USGSMap() as fs: + sim_name = list(parameters.keys())[idx] + sim_ws = os.path.join(workspace, sim_name) + gwf = sim.get_model(sim_name) -# Function that wraps all of the steps for the FHB model -# -# 1. build_model, -# 2. write_model, -# 3. run_model, and -# 4. plot_results. -# + fig = plt.figure(figsize=figure_size) + fig.tight_layout() + head = gwf.output.head().get_data() -def simulation(idx, silent=True): - key = list(parameters.keys())[idx] - params = parameters[key].copy() - sim = build_model(key, **params) - write_model(sim, silent=silent) - success = run_model(sim, silent=silent) - if success: - plot_results(idx, sim, silent=silent) + ax = fig.add_subplot(1, 1, 1, aspect="equal") + pmv = flopy.plot.PlotMapView(model=gwf, ax=ax, layer=0) + cb = pmv.plot_array(0 - head, cmap="jet", alpha=0.25) + cs = pmv.contour_array(0 - head, levels=np.arange(0.1, 1, 0.1)) + cbar = plt.colorbar(cb, shrink=0.25) + cbar.ax.set_xlabel(r"Drawdown, ($m$)") + ax.set_xlabel("x position (m)") + ax.set_ylabel("y position (m)") + + if plot_show: + plt.show() + if plot_save: + fpth = os.path.join("..", "figures", f"{sim_name}-head.png") + fig.savefig(fpth) -# nosetest - exclude block from this nosetest to the next nosetest -def test_01(): - simulation(0, silent=False) +def plot_results(idx, sim, silent=True): + if idx == 0: + plot_grid(idx, sim) + plot_head(idx, sim) -def test_02(): - simulation(1, silent=False) +# - +# ### Running the example +# +# Define and invoke a function to run the example scenario, then plot results. -def test_03(): - simulation(2, silent=False) +# + +def scenario(idx, silent=True): + key = list(parameters.keys())[idx] + params = parameters[key].copy() + sim = build_models(key, **params) + if write: + write_models(sim, silent=silent) + if run: + run_models(sim, silent=silent) + if plot: + plot_results(idx, sim, silent=silent) -# nosetest end -if __name__ == "__main__": - # ### Hani Simulation - # - # Simulated heads in the Hani model with anisotropy in x direction. +# Run the Hani model with anisotropy in x direction and plot heads. - simulation(0) +scenario(0) - # Simulated heads in the Hani model with anisotropy in y direction. +# Run the Hani model with anisotropy in y direction and plot heads. - simulation(1) +scenario(1) - # Simulated heads in the Hani model with anisotropy rotated 15 degrees. +# Run the Hani model with anisotropy rotated 15 degrees and plot heads. - simulation(2) +scenario(2) +# - diff --git a/scripts/ex-gwf-lak-p01.py b/scripts/ex-gwf-lak-p01.py index 9d6678392..82da3dec1 100644 --- a/scripts/ex-gwf-lak-p01.py +++ b/scripts/ex-gwf-lak-p01.py @@ -1,50 +1,44 @@ -# ## Lake package (LAK) Package problem 1 +# ## Lake package (LAK) example 1 # # This is the lake package example problem (test 1) from the # Lake Package documentation (Merritt and Konikow, 2000). -# -# ### LAK Package Problem 1 Setup +# ### Initial setup # -# Imports +# Import dependencies, define the example name and workspace, and read settings from environment variables. +# + import os -import sys +import pathlib as pl import flopy -import matplotlib as mpl import matplotlib.pyplot as plt import numpy as np +from flopy.plot.styles import styles +from modflow_devtools.misc import get_env, timed -# Append to system path to include the common subdirectory - -sys.path.append(os.path.join("..", "common")) - -# import common functionality - -import config -from figspecs import USGSFigure - -# Set figure properties specific to the - -figure_size = (6.3, 5.6) -masked_values = (0, 1e30, -1e30) - -# Base simulation and model name and workspace - -ws = config.base_ws +# Example name and base workspace +workspace = pl.Path("../examples") +sim_name = "ex-gwf-lak-p01" -# Simulation name +# Settings from environment variables +write = get_env("WRITE", True) +run = get_env("RUN", True) +plot = get_env("PLOT", True) +plot_show = get_env("PLOT_SHOW", True) +plot_save = get_env("PLOT_SAVE", True) +# - -sim_name = "ex-gwf-lak-p01" +# ### Define parameters +# +# Define model units, parameters and other settings. +# + # Model units - length_units = "feet" time_units = "days" -# Table LAK Package Problem 1 Model Parameters - +# Model parameters nper = 1 # Number of periods nlay = 5 # Number of layers nrow = 17 # Number of rows @@ -53,9 +47,7 @@ botm_str = "107., 97., 87., 77., 67." # Bottom elevations ($ft$) strt = 115.0 # Starting head ($ft$) k11 = 30.0 # Horizontal hydraulic conductivity ($ft/d$) -k33_str = ( - "1179., 30., 30., 30., 30." # Vertical hydraulic conductivity ($ft/d$) -) +k33_str = "1179., 30., 30., 30., 30." # Vertical hydraulic conductivity ($ft/d$) ss = 3e-4 # Specific storage ($1/d$) sy = 0.2 # Specific yield (unitless) H1 = 160.0 # Constant head on left side of model ($ft$) @@ -68,12 +60,10 @@ lak_bedleak = 0.1 # Lakebed leakance ($1/d$) # parse parameter strings into tuples - botm = [float(value) for value in botm_str.split(",")] k33 = [float(value) for value in k33_str.split(",")] # Static temporal data used by TDIS file - tdis_ds = ((5000.0, 100, 1.02),) # define delr and delc @@ -126,14 +116,12 @@ shape3d = (nlay, nrow, ncol) # Create the array defining the lake location - lake_map = np.ones(shape3d, dtype=np.int32) * -1 lake_map[0, 6:11, 6:11] = 0 lake_map[1, 7:10, 7:10] = 0 lake_map = np.ma.masked_where(lake_map < 0, lake_map) # create linearly varying evapotranspiration surface - xlen = delr.sum() - 0.5 * (delr[0] + delr[-1]) x = 0.0 s1d = H1 * np.ones(ncol, dtype=float) @@ -145,172 +133,153 @@ surf[lake_map[0] == 0] = botm[0] - 2 surf[lake_map[1] == 0] = botm[1] - 2 -# ### Create LAK Package Problem 1 Model Boundary Conditions -# # Constant head boundary conditions -# - chd_spd = [] for k in range(nlay): chd_spd += [[k, i, 0, H1] for i in range(nrow)] chd_spd += [[k, i, ncol - 1, H2] for i in range(nrow)] # LAK Package - lak_spd = [ [0, "rainfall", recharge], [0, "evaporation", lak_etrate], ] # Solver parameters - nouter = 500 ninner = 100 hclose = 1e-9 rclose = 1e-6 +# - - -# ### Functions to build, write, run, and plot the MODFLOW 6 LAK Package Problem 1 model +# ### Model setup # -# MODFLOW 6 flopy simulation object (sim) is returned if building the model - +# Define functions to build models, write input files, and run the simulation. + + +# + +def build_models(): + sim_ws = os.path.join(workspace, sim_name) + sim = flopy.mf6.MFSimulation(sim_name=sim_name, sim_ws=sim_ws, exe_name="mf6") + flopy.mf6.ModflowTdis(sim, nper=nper, perioddata=tdis_ds, time_units=time_units) + flopy.mf6.ModflowIms( + sim, + print_option="summary", + linear_acceleration="bicgstab", + outer_maximum=nouter, + outer_dvclose=hclose, + inner_maximum=ninner, + inner_dvclose=hclose, + rcloserecord=f"{rclose} strict", + ) + gwf = flopy.mf6.ModflowGwf( + sim, modelname=sim_name, newtonoptions="newton", save_flows=True + ) + flopy.mf6.ModflowGwfdis( + gwf, + length_units=length_units, + nlay=nlay, + nrow=nrow, + ncol=ncol, + delr=delr, + delc=delc, + idomain=np.ones(shape3d, dtype=int), + top=top, + botm=botm, + ) + obs_file = f"{sim_name}.gwf.obs" + csv_file = obs_file + ".csv" + obslist = [ + ["A", "head", (0, 3, 3)], + ["B", "head", (0, 13, 13)], + ] + obsdict = {csv_file: obslist} + flopy.mf6.ModflowUtlobs( + gwf, filename=obs_file, print_input=False, continuous=obsdict + ) -def build_model(): - if config.buildModel: - sim_ws = os.path.join(ws, sim_name) - sim = flopy.mf6.MFSimulation( - sim_name=sim_name, sim_ws=sim_ws, exe_name="mf6" - ) - flopy.mf6.ModflowTdis( - sim, nper=nper, perioddata=tdis_ds, time_units=time_units - ) - flopy.mf6.ModflowIms( - sim, - print_option="summary", - linear_acceleration="bicgstab", - outer_maximum=nouter, - outer_dvclose=hclose, - inner_maximum=ninner, - inner_dvclose=hclose, - rcloserecord=f"{rclose} strict", - ) - gwf = flopy.mf6.ModflowGwf( - sim, modelname=sim_name, newtonoptions="newton", save_flows=True - ) - flopy.mf6.ModflowGwfdis( - gwf, - length_units=length_units, - nlay=nlay, - nrow=nrow, - ncol=ncol, - delr=delr, - delc=delc, - idomain=np.ones(shape3d, dtype=int), - top=top, - botm=botm, - ) - obs_file = f"{sim_name}.gwf.obs" - csv_file = obs_file + ".csv" - obslist = [ - ["A", "head", (0, 3, 3)], - ["B", "head", (0, 13, 13)], + flopy.mf6.ModflowGwfnpf( + gwf, + icelltype=1, + k=k11, + k33=k33, + save_specific_discharge=True, + ) + flopy.mf6.ModflowGwfsto( + gwf, + iconvert=1, + sy=sy, + ss=ss, + ) + flopy.mf6.ModflowGwfic(gwf, strt=strt) + flopy.mf6.ModflowGwfchd(gwf, stress_period_data=chd_spd) + flopy.mf6.ModflowGwfrcha(gwf, recharge=recharge) + flopy.mf6.ModflowGwfevta(gwf, surface=surf, rate=etvrate, depth=etvdepth) + ( + idomain_wlakes, + pakdata_dict, + lak_conn, + ) = flopy.mf6.utils.get_lak_connections( + gwf.modelgrid, + lake_map, + bedleak=lak_bedleak, + ) + lak_packagedata = [[0, lak_strt, pakdata_dict[0]]] + lak = flopy.mf6.ModflowGwflak( + gwf, + print_stage=True, + nlakes=1, + noutlets=0, + packagedata=lak_packagedata, + connectiondata=lak_conn, + perioddata=lak_spd, + ) + obs_file = f"{sim_name}.lak.obs" + csv_file = obs_file + ".csv" + obs_dict = { + csv_file: [ + ("stage", "stage", (0,)), ] - obsdict = {csv_file: obslist} - flopy.mf6.ModflowUtlobs( - gwf, filename=obs_file, print_input=False, continuous=obsdict - ) - - flopy.mf6.ModflowGwfnpf( - gwf, - icelltype=1, - k=k11, - k33=k33, - save_specific_discharge=True, - ) - flopy.mf6.ModflowGwfsto( - gwf, - iconvert=1, - sy=sy, - ss=ss, - ) - flopy.mf6.ModflowGwfic(gwf, strt=strt) - flopy.mf6.ModflowGwfchd(gwf, stress_period_data=chd_spd) - flopy.mf6.ModflowGwfrcha(gwf, recharge=recharge) - flopy.mf6.ModflowGwfevta( - gwf, surface=surf, rate=etvrate, depth=etvdepth - ) - ( - idomain_wlakes, - pakdata_dict, - lak_conn, - ) = flopy.mf6.utils.get_lak_connections( - gwf.modelgrid, - lake_map, - bedleak=lak_bedleak, - ) - lak_packagedata = [[0, lak_strt, pakdata_dict[0]]] - lak = flopy.mf6.ModflowGwflak( - gwf, - print_stage=True, - nlakes=1, - noutlets=0, - packagedata=lak_packagedata, - connectiondata=lak_conn, - perioddata=lak_spd, - ) - obs_file = f"{sim_name}.lak.obs" - csv_file = obs_file + ".csv" - obs_dict = { - csv_file: [ - ("stage", "stage", (0,)), - ] - } - lak.obs.initialize( - filename=obs_file, digits=10, print_input=True, continuous=obs_dict - ) - gwf.dis.idomain = idomain_wlakes - - head_filerecord = f"{sim_name}.hds" - budget_filerecord = f"{sim_name}.cbc" - flopy.mf6.ModflowGwfoc( - gwf, - head_filerecord=head_filerecord, - budget_filerecord=budget_filerecord, - saverecord=[("HEAD", "LAST"), ("BUDGET", "LAST")], - ) - return sim - return None + } + lak.obs.initialize( + filename=obs_file, digits=10, print_input=True, continuous=obs_dict + ) + gwf.dis.idomain = idomain_wlakes + head_filerecord = f"{sim_name}.hds" + budget_filerecord = f"{sim_name}.cbc" + flopy.mf6.ModflowGwfoc( + gwf, + head_filerecord=head_filerecord, + budget_filerecord=budget_filerecord, + saverecord=[("HEAD", "LAST"), ("BUDGET", "LAST")], + ) + return sim -# Function to write MODFLOW 6 LAK Package Problem 1 model files +def write_models(sim, silent=True): + sim.write_simulation(silent=silent) -def write_model(sim, silent=True): - if config.writeModel: - sim.write_simulation(silent=silent) +@timed +def run_models(sim, silent=True): + success, buff = sim.run_simulation(silent=silent) + assert success, buff -# Function to run the LAK Package Problem 1 model. -# True is returned if the model runs successfully -# +# - -@config.timeit -def run_model(sim, silent=True): - success = True - if config.runModel: - success, buff = sim.run_simulation(silent=silent) - if not success: - print(buff) - return success - +# ### Model setup +# +# Define functions to build models, write input files, and run the simulation. -# Function to plot grid +# + +# Figure properties +figure_size = (6.3, 5.6) +masked_values = (0, 1e30, -1e30) def plot_grid(gwf, silent=True): - sim_ws = os.path.join(ws, sim_name) - # load the observations lak_results = gwf.lak.output.obs().data @@ -336,294 +305,269 @@ def plot_grid(gwf, silent=True): p1 = (xcenters[3], ycenters[3]) p2 = (xcenters[13], ycenters[13]) - fs = USGSFigure(figure_type="map", verbose=False) - fig = plt.figure( - figsize=(4, 6.9), - tight_layout=True, - ) - plt.axis("off") - - nrows, ncols = 10, 1 - axes = [fig.add_subplot(nrows, ncols, (1, 5))] - axes.append(fig.add_subplot(nrows, ncols, (6, 8), sharex=axes[0])) - - for idx, ax in enumerate(axes): - ax.set_xlim(extents[:2]) - if idx == 0: - ax.set_ylim(extents[2:]) - ax.set_aspect("equal") - - # legend axis - axes.append(fig.add_subplot(nrows, ncols, (9, 10))) - - # set limits for legend area - ax = axes[-1] - ax.set_xlim(0, 1) - ax.set_ylim(0, 1) - - # get rid of ticks and spines for legend area - ax.axis("off") - ax.set_xticks([]) - ax.set_yticks([]) - ax.spines["top"].set_color("none") - ax.spines["bottom"].set_color("none") - ax.spines["left"].set_color("none") - ax.spines["right"].set_color("none") - ax.patch.set_alpha(0.0) - - ax = axes[0] - mm = flopy.plot.PlotMapView(gwf, ax=ax, extent=extents) - mm.plot_bc("CHD", color="cyan") - mm.plot_inactive(color_noflow="#5DBB63") - mm.plot_grid(lw=0.5, color="black") - cv = mm.contour_array( - head, - levels=np.arange(140, 160, 2), - linewidths=0.75, - linestyles="-", - colors="blue", - masked_values=masked_values, - ) - plt.clabel(cv, fmt="%1.0f") - mm.plot_vector(qx, qy, normalize=True, color="0.75") - ax.plot(p1[0], p1[1], marker="o", mfc="red", mec="black", ms=4) - ax.plot(p2[0], p2[1], marker="o", mfc="red", mec="black", ms=4) - ax.set_xlabel("x-coordinate, in feet") - ax.set_ylabel("y-coordinate, in feet") - fs.heading(ax, heading="Map view", idx=0) - fs.add_text( - ax, - "A", - x=p1[0] + 150, - y=p1[1] + 150, - transform=False, - bold=False, - color="red", - ha="left", - va="bottom", - ) - fs.add_text( - ax, - "B", - x=p2[0] + 150, - y=p2[1] + 150, - transform=False, - bold=False, - color="red", - ha="left", - va="bottom", - ) - fs.remove_edge_ticks(ax) - - ax = axes[1] - xs = flopy.plot.PlotCrossSection(gwf, ax=ax, line={"row": 8}) - xs.plot_array(np.ones(shape3d), head=head, cmap="jet") - xs.plot_bc("CHD", color="cyan", head=head) - xs.plot_ibound(color_noflow="#5DBB63", head=head) - xs.plot_grid(lw=0.5, color="black") - ax.set_xlabel("x-coordinate, in feet") - ax.set_ylim(67, 160) - ax.set_ylabel("Elevation, in feet") - fs.heading(ax, heading="Cross-section view", idx=1) - fs.remove_edge_ticks(ax) - - # legend - ax = axes[-1] - ax.plot( - -10000, - -10000, - lw=0, - marker="s", - ms=10, - mfc="#5DBB63", - mec="black", - markeredgewidth=0.5, - label="Lake boundary", - ) - ax.plot( - -10000, - -10000, - lw=0, - marker="s", - ms=10, - mfc="cyan", - mec="black", - markeredgewidth=0.5, - label="Constant-head boundary", - ) - ax.plot( - -10000, - -10000, - lw=0, - marker="s", - ms=10, - mfc="blue", - mec="black", - markeredgewidth=0.5, - label="Water table", - ) - ax.plot( - -10000, - -10000, - lw=0, - marker="o", - ms=4, - mfc="red", - mec="black", - markeredgewidth=0.5, - label="Observation well", - ) - ax.plot( - -10000, - -10000, - lw=0.75, - ls="-", - color="blue", - label=r"Head contour, $ft$", - ) - ax.plot( - -10000, - -10000, - lw=0, - marker="$\u2192$", - ms=10, - mfc="0.75", - mec="0.75", - label="Normalized specific discharge", - ) - fs.graph_legend(ax, loc="lower center", ncol=2) - - # save figure - if config.plotSave: - fpth = os.path.join( - "..", - "figures", - f"{sim_name}-grid{config.figure_ext}", + with styles.USGSMap(): + fig = plt.figure( + figsize=(4, 6.9), + tight_layout=True, ) - fig.savefig(fpth) - - return - + plt.axis("off") + + nrows, ncols = 10, 1 + axes = [fig.add_subplot(nrows, ncols, (1, 5))] + axes.append(fig.add_subplot(nrows, ncols, (6, 8), sharex=axes[0])) + + for idx, ax in enumerate(axes): + ax.set_xlim(extents[:2]) + if idx == 0: + ax.set_ylim(extents[2:]) + ax.set_aspect("equal") + + # legend axis + axes.append(fig.add_subplot(nrows, ncols, (9, 10))) + + # set limits for legend area + ax = axes[-1] + ax.set_xlim(0, 1) + ax.set_ylim(0, 1) + + # get rid of ticks and spines for legend area + ax.axis("off") + ax.set_xticks([]) + ax.set_yticks([]) + ax.spines["top"].set_color("none") + ax.spines["bottom"].set_color("none") + ax.spines["left"].set_color("none") + ax.spines["right"].set_color("none") + ax.patch.set_alpha(0.0) + + ax = axes[0] + mm = flopy.plot.PlotMapView(gwf, ax=ax, extent=extents) + mm.plot_bc("CHD", color="cyan") + mm.plot_inactive(color_noflow="#5DBB63") + mm.plot_grid(lw=0.5, color="black") + cv = mm.contour_array( + head, + levels=np.arange(140, 160, 2), + linewidths=0.75, + linestyles="-", + colors="blue", + masked_values=masked_values, + ) + plt.clabel(cv, fmt="%1.0f") + mm.plot_vector(qx, qy, normalize=True, color="0.75") + ax.plot(p1[0], p1[1], marker="o", mfc="red", mec="black", ms=4) + ax.plot(p2[0], p2[1], marker="o", mfc="red", mec="black", ms=4) + ax.set_xlabel("x-coordinate, in feet") + ax.set_ylabel("y-coordinate, in feet") + styles.heading(ax, heading="Map view", idx=0) + styles.add_text( + ax, + "A", + x=p1[0] + 150, + y=p1[1] + 150, + transform=False, + bold=False, + color="red", + ha="left", + va="bottom", + ) + styles.add_text( + ax, + "B", + x=p2[0] + 150, + y=p2[1] + 150, + transform=False, + bold=False, + color="red", + ha="left", + va="bottom", + ) + styles.remove_edge_ticks(ax) + + ax = axes[1] + xs = flopy.plot.PlotCrossSection(gwf, ax=ax, line={"row": 8}) + xs.plot_array(np.ones(shape3d), head=head, cmap="jet") + xs.plot_bc("CHD", color="cyan", head=head) + xs.plot_ibound(color_noflow="#5DBB63", head=head) + xs.plot_grid(lw=0.5, color="black") + ax.set_xlabel("x-coordinate, in feet") + ax.set_ylim(67, 160) + ax.set_ylabel("Elevation, in feet") + styles.heading(ax, heading="Cross-section view", idx=1) + styles.remove_edge_ticks(ax) + + # legend + ax = axes[-1] + ax.plot( + -10000, + -10000, + lw=0, + marker="s", + ms=10, + mfc="#5DBB63", + mec="black", + markeredgewidth=0.5, + label="Lake boundary", + ) + ax.plot( + -10000, + -10000, + lw=0, + marker="s", + ms=10, + mfc="cyan", + mec="black", + markeredgewidth=0.5, + label="Constant-head boundary", + ) + ax.plot( + -10000, + -10000, + lw=0, + marker="s", + ms=10, + mfc="blue", + mec="black", + markeredgewidth=0.5, + label="Water table", + ) + ax.plot( + -10000, + -10000, + lw=0, + marker="o", + ms=4, + mfc="red", + mec="black", + markeredgewidth=0.5, + label="Observation well", + ) + ax.plot( + -10000, + -10000, + lw=0.75, + ls="-", + color="blue", + label=r"Head contour, $ft$", + ) + ax.plot( + -10000, + -10000, + lw=0, + marker="$\u2192$", + ms=10, + mfc="0.75", + mec="0.75", + label="Normalized specific discharge", + ) + styles.graph_legend(ax, loc="lower center", ncol=2) -# Function to plot the lake results + if plot_show: + plt.show() + if plot_save: + fpth = os.path.join( + "..", + "figures", + f"{sim_name}-grid.png", + ) + fig.savefig(fpth) def plot_lak_results(gwf, silent=True): - fs = USGSFigure(figure_type="graph", verbose=False) - - # load the observations - lak_results = gwf.lak.output.obs().data - gwf_results = gwf.obs[0].output.obs().data - - dtype = [ - ("time", float), - ("STAGE", float), - ("A", float), - ("B", float), - ] - - results = np.zeros((lak_results.shape[0] + 1), dtype=dtype) - results["time"][1:] = lak_results["totim"] - results["STAGE"][0] = 110.0 - results["STAGE"][1:] = lak_results["STAGE"] - results["A"][0] = 115.0 - results["A"][1:] = gwf_results["A"] - results["B"][0] = 115.0 - results["B"][1:] = gwf_results["B"] - - # create the figure - fig, ax = plt.subplots( - ncols=1, - nrows=1, - sharex=True, - figsize=(6.3, 3.15), - constrained_layout=True, - ) + with styles.USGSPlot(): + # load the observations + lak_results = gwf.lak.output.obs().data + gwf_results = gwf.obs[0].output.obs().data + + dtype = [ + ("time", float), + ("STAGE", float), + ("A", float), + ("B", float), + ] - ax.set_xlim(0, 3000) - ax.set_ylim(110, 160) - ax.plot( - results["time"], - results["STAGE"], - lw=0.75, - ls="--", - color="black", - label="Lake stage", - ) - ax.plot( - results["time"], - results["A"], - lw=0.75, - ls="-", - color="0.5", - label="Point A", - ) - ax.plot( - results["time"], - results["B"], - lw=0.75, - ls="-", - color="black", - label="Point B", - ) - ax.set_xlabel("Simulation time, in days") - ax.set_ylabel("Head or stage, in feet") - fs.graph_legend(ax, loc="lower right") - - # save figure - if config.plotSave: - fpth = os.path.join( - "..", - "figures", - f"{sim_name}-01{config.figure_ext}", + results = np.zeros((lak_results.shape[0] + 1), dtype=dtype) + results["time"][1:] = lak_results["totim"] + results["STAGE"][0] = 110.0 + results["STAGE"][1:] = lak_results["STAGE"] + results["A"][0] = 115.0 + results["A"][1:] = gwf_results["A"] + results["B"][0] = 115.0 + results["B"][1:] = gwf_results["B"] + + # create the figure + fig, ax = plt.subplots( + ncols=1, + nrows=1, + sharex=True, + figsize=(6.3, 3.15), + constrained_layout=True, ) - fig.savefig(fpth) - - return + ax.set_xlim(0, 3000) + ax.set_ylim(110, 160) + ax.plot( + results["time"], + results["STAGE"], + lw=0.75, + ls="--", + color="black", + label="Lake stage", + ) + ax.plot( + results["time"], + results["A"], + lw=0.75, + ls="-", + color="0.5", + label="Point A", + ) + ax.plot( + results["time"], + results["B"], + lw=0.75, + ls="-", + color="black", + label="Point B", + ) + ax.set_xlabel("Simulation time, in days") + ax.set_ylabel("Head or stage, in feet") + styles.graph_legend(ax, loc="lower right") -# Function to plot the LAK Package Problem 1 model results. + if plot_show: + plt.show() + if plot_save: + fpth = os.path.join( + "..", + "figures", + f"{sim_name}-01.png", + ) + fig.savefig(fpth) def plot_results(sim, silent=True): - if config.plotModel: - gwf = sim.get_model(sim_name) + gwf = sim.get_model(sim_name) + plot_grid(gwf, silent=silent) + plot_lak_results(gwf, silent=silent) - plot_grid(gwf, silent=silent) - plot_lak_results(gwf, silent=silent) +# - - -# Function that wraps all of the steps for the LAK Package Problem 1 model -# -# 1. build_model, -# 2. write_model, -# 3. run_model, and -# 4. plot_results. +# ### Running the example # +# Define and invoke a function to run the example scenario, then plot results. -def simulation(silent=True): - sim = build_model() - - write_model(sim, silent=silent) - - success = run_model(sim, silent=silent) - assert success, f"could not run...{sim_name}" - - if success: +# + +def scenario(silent=True): + sim = build_models() + if write: + write_models(sim, silent=silent) + if run: + run_models(sim, silent=silent) + if plot: plot_results(sim, silent=silent) -# nosetest - exclude block from this nosetest to the next nosetest -def test_01(): - simulation(silent=False) - - -# nosetest end - -if __name__ == "__main__": - # ### LAK Package Problem 1 Simulation - # - - simulation() +scenario() +# - diff --git a/scripts/ex-gwf-lak-p02.py b/scripts/ex-gwf-lak-p02.py index a9b037f95..02566022d 100644 --- a/scripts/ex-gwf-lak-p02.py +++ b/scripts/ex-gwf-lak-p02.py @@ -2,49 +2,49 @@ # # This is the lake package example problem (test 2) from the # Lake Package documentation (Merritt and Konikow, 2000). -# -# ### LAK Package problem 2 Setup +# ### Initial setup # -# Imports +# Import dependencies, define the example name and workspace, and read settings from environment variables. +# + import os -import sys +import pathlib as pl import flopy import matplotlib.pyplot as plt import numpy as np +import pooch import shapefile as shp +from flopy.plot.styles import styles +from modflow_devtools.misc import get_env, timed -# Append to system path to include the common subdirectory - -sys.path.append(os.path.join("..", "common")) - -# import common functionality - -import config -from figspecs import USGSFigure - -# Set figure properties specific to the - +# Figure properties figure_size = (6.3, 5.6) masked_values = (0, 1e30, -1e30) -# Base simulation and model name and workspace - -ws = config.base_ws +# Example name and base workspace +sim_name = "ex-gwf-lak-p02" +workspace = pl.Path("../examples") -# Simulation name +# Settings from environment variables +write = get_env("WRITE", True) +run = get_env("RUN", True) +plot = get_env("PLOT", True) +plot_show = get_env("PLOT_SHOW", True) +plot_save = get_env("PLOT_SAVE", True) +# - -sim_name = "ex-gwf-lak-p02" +# ### Define parameters +# +# Define model units, parameters and other settings. +# + # Model units - length_units = "feet" time_units = "days" -# Table LAK Package problem 2 Model Parameters - +# Model parameters nper = 1 # Number of periods nlay = 5 # Number of layers nrow = 27 # Number of rows @@ -65,12 +65,10 @@ lak_etrate = 0.0103 # Lake evaporation rate ($ft/d$) lak_bedleak = 0.1 # Lakebed leakance ($1/d$) -# parse parameter strings into tuples - +# Parse parameter strings into tuples botm = [float(value) for value in botm_str.split(",")] # Static temporal data used by TDIS file - tdis_ds = ((1500.0, 200, 1.005),) # define delr and delc @@ -133,16 +131,19 @@ shape3d = (nlay, nrow, ncol) # Load the idomain arrays - -data_pth = os.path.join("..", "data", sim_name) +fpth = pooch.retrieve( + url=f"https://github.com/MODFLOW-USGS/modflow6-examples/raw/master/data/{sim_name}/lakes-01.txt", + known_hash="md5:a74ded5357aa667b9df793847e5f8f41", +) lake_map = np.ones(shape3d, dtype=int) * -1 -fpth = os.path.join(data_pth, "lakes-01.txt") lake_map[0, :, :] = np.loadtxt(fpth, dtype=int) - 1 -fpth = os.path.join(data_pth, "lakes-02.txt") +fpth = pooch.retrieve( + url=f"https://github.com/MODFLOW-USGS/modflow6-examples/raw/master/data/{sim_name}/lakes-02.txt", + known_hash="md5:7830e5223c958c35be349a3be24a60a3", +) lake_map[1, :, :] = np.loadtxt(fpth, dtype=int) - 1 # create linearly varying evapotranspiration surface - xlen = delr.sum() - 0.5 * (delr[0] + delr[-1]) x = 0.0 s1d = H1 * np.ones(ncol, dtype=float) @@ -154,26 +155,19 @@ surf[lake_map[0, :, :] > -1] = botm[0] - 2 surf[lake_map[1, :, :] > -1] = botm[1] - 2 -# ### Create LAK Package problem 2 Model Boundary Conditions -# # Constant head boundary conditions -# - chd_spd = [] for k in range(nlay): chd_spd += [[k, i, 0, H1] for i in range(nrow)] chd_spd += [[k, i, ncol - 1, H2] for i in range(nrow)] # LAK Package - lak_time_conv = 86400.0 lak_len_conv = 3.28081 - lak_outlets = [ [0, 0, -1, "manning", 114.85, 5.0, 0.05, 8.206324419006205e-4], [1, 1, -1, "manning", 109.4286, 5.0, 0.05, 9.458197164349258e-4], ] - lak_spd = [ [0, "rainfall", recharge], [0, "evaporation", lak_etrate], @@ -182,7 +176,6 @@ ] # SFR package - sfr_pakdata = [ [ 0, @@ -566,12 +559,10 @@ sfr_spd = [[0, "inflow", 691200.0]] # MVR package - mvr_paks = [ ["SFR-1"], ["LAK-1"], ] - mvr_spd = [ ["SFR-1", 7, "LAK-1", 0, "FACTOR", 1.0], ["LAK-1", 0, "SFR-1", 8, "FACTOR", 1.0], @@ -580,182 +571,169 @@ ] # Solver parameters - nouter = 500 ninner = 100 hclose = 1e-9 rclose = 1e-6 +# - - -# ### Functions to build, write, run, and plot the MODFLOW 6 LAK Package problem 2 model +# ### Model setup # -# MODFLOW 6 flopy simulation object (sim) is returned if building the model - +# Define functions to build models, write input files, and run the simulation. + + +# + +def build_models(): + sim_ws = os.path.join(workspace, sim_name) + sim = flopy.mf6.MFSimulation(sim_name=sim_name, sim_ws=sim_ws, exe_name="mf6") + flopy.mf6.ModflowTdis(sim, nper=nper, perioddata=tdis_ds, time_units=time_units) + flopy.mf6.ModflowIms( + sim, + print_option="summary", + linear_acceleration="bicgstab", + outer_maximum=nouter, + outer_dvclose=hclose, + inner_maximum=ninner, + inner_dvclose=hclose, + rcloserecord=f"{rclose} strict", + ) + gwf = flopy.mf6.ModflowGwf( + sim, modelname=sim_name, newtonoptions="newton", save_flows=True + ) + flopy.mf6.ModflowGwfdis( + gwf, + length_units=length_units, + nlay=nlay, + nrow=nrow, + ncol=ncol, + delr=delr, + delc=delc, + idomain=np.ones(shape3d, dtype=int), + top=top, + botm=botm, + ) + obs_file = f"{sim_name}.gwf.obs" + csv_file = obs_file + ".csv" + obslist = [ + ["A", "head", (0, 3, 3)], + ["B", "head", (0, 13, 8)], + ["C", "head", (0, 23, 13)], + ] + obsdict = {csv_file: obslist} + flopy.mf6.ModflowUtlobs( + gwf, filename=obs_file, print_input=False, continuous=obsdict + ) -def build_model(): - if config.buildModel: - sim_ws = os.path.join(ws, sim_name) - sim = flopy.mf6.MFSimulation( - sim_name=sim_name, sim_ws=sim_ws, exe_name="mf6" - ) - flopy.mf6.ModflowTdis( - sim, nper=nper, perioddata=tdis_ds, time_units=time_units - ) - flopy.mf6.ModflowIms( - sim, - print_option="summary", - linear_acceleration="bicgstab", - outer_maximum=nouter, - outer_dvclose=hclose, - inner_maximum=ninner, - inner_dvclose=hclose, - rcloserecord=f"{rclose} strict", - ) - gwf = flopy.mf6.ModflowGwf( - sim, modelname=sim_name, newtonoptions="newton", save_flows=True - ) - flopy.mf6.ModflowGwfdis( - gwf, - length_units=length_units, - nlay=nlay, - nrow=nrow, - ncol=ncol, - delr=delr, - delc=delc, - idomain=np.ones(shape3d, dtype=int), - top=top, - botm=botm, - ) - obs_file = f"{sim_name}.gwf.obs" - csv_file = obs_file + ".csv" - obslist = [ - ["A", "head", (0, 3, 3)], - ["B", "head", (0, 13, 8)], - ["C", "head", (0, 23, 13)], + flopy.mf6.ModflowGwfnpf( + gwf, + icelltype=1, + k=k11, + k33=k33, + save_specific_discharge=True, + ) + flopy.mf6.ModflowGwfsto( + gwf, + iconvert=1, + sy=sy, + ss=ss, + ) + flopy.mf6.ModflowGwfic(gwf, strt=strt) + flopy.mf6.ModflowGwfchd(gwf, stress_period_data=chd_spd) + flopy.mf6.ModflowGwfrcha(gwf, recharge=recharge) + flopy.mf6.ModflowGwfevta(gwf, surface=surf, rate=etvrate, depth=etvdepth) + ( + idomain_wlakes, + pakdata_dict, + lak_conn, + ) = flopy.mf6.utils.get_lak_connections( + gwf.modelgrid, + lake_map, + bedleak=lak_bedleak, + ) + lak_packagedata = [] + for key in pakdata_dict.keys(): + lak_packagedata.append([key, lak_strt, pakdata_dict[key]]) + lak = flopy.mf6.ModflowGwflak( + gwf, + pname="LAK-1", + time_conversion=lak_time_conv, + length_conversion=lak_len_conv, + mover=True, + print_stage=True, + nlakes=2, + noutlets=len(lak_outlets), + packagedata=lak_packagedata, + connectiondata=lak_conn, + outlets=lak_outlets, + perioddata=lak_spd, + ) + obs_file = f"{sim_name}.lak.obs" + csv_file = obs_file + ".csv" + obs_dict = { + csv_file: [ + ("lake1", "stage", (0,)), + ("lake2", "stage", (1,)), ] - obsdict = {csv_file: obslist} - flopy.mf6.ModflowUtlobs( - gwf, filename=obs_file, print_input=False, continuous=obsdict - ) + } + lak.obs.initialize( + filename=obs_file, digits=10, print_input=True, continuous=obs_dict + ) + gwf.dis.idomain = idomain_wlakes + flopy.mf6.ModflowGwfsfr( + gwf, + pname="SFR-1", + time_conversion=86400.000, + length_conversion=3.28081, + mover=True, + print_stage=True, + print_flows=True, + nreaches=len(sfr_pakdata), + packagedata=sfr_pakdata, + connectiondata=sfr_conn, + perioddata=sfr_spd, + ) + flopy.mf6.ModflowGwfmvr( + gwf, + maxmvr=4, + maxpackages=2, + packages=mvr_paks, + perioddata=mvr_spd, + ) - flopy.mf6.ModflowGwfnpf( - gwf, - icelltype=1, - k=k11, - k33=k33, - save_specific_discharge=True, - ) - flopy.mf6.ModflowGwfsto( - gwf, - iconvert=1, - sy=sy, - ss=ss, - ) - flopy.mf6.ModflowGwfic(gwf, strt=strt) - flopy.mf6.ModflowGwfchd(gwf, stress_period_data=chd_spd) - flopy.mf6.ModflowGwfrcha(gwf, recharge=recharge) - flopy.mf6.ModflowGwfevta( - gwf, surface=surf, rate=etvrate, depth=etvdepth - ) - ( - idomain_wlakes, - pakdata_dict, - lak_conn, - ) = flopy.mf6.utils.get_lak_connections( - gwf.modelgrid, - lake_map, - bedleak=lak_bedleak, - ) - lak_packagedata = [] - for key in pakdata_dict.keys(): - lak_packagedata.append([key, lak_strt, pakdata_dict[key]]) - lak = flopy.mf6.ModflowGwflak( - gwf, - pname="LAK-1", - time_conversion=lak_time_conv, - length_conversion=lak_len_conv, - mover=True, - print_stage=True, - nlakes=2, - noutlets=len(lak_outlets), - packagedata=lak_packagedata, - connectiondata=lak_conn, - outlets=lak_outlets, - perioddata=lak_spd, - ) - obs_file = f"{sim_name}.lak.obs" - csv_file = obs_file + ".csv" - obs_dict = { - csv_file: [ - ("lake1", "stage", (0,)), - ("lake2", "stage", (1,)), - ] - } - lak.obs.initialize( - filename=obs_file, digits=10, print_input=True, continuous=obs_dict - ) - gwf.dis.idomain = idomain_wlakes - flopy.mf6.ModflowGwfsfr( - gwf, - pname="SFR-1", - time_conversion=86400.000, - length_conversion=3.28081, - mover=True, - print_stage=True, - print_flows=True, - nreaches=len(sfr_pakdata), - packagedata=sfr_pakdata, - connectiondata=sfr_conn, - perioddata=sfr_spd, - ) - flopy.mf6.ModflowGwfmvr( - gwf, - maxmvr=4, - maxpackages=2, - packages=mvr_paks, - perioddata=mvr_spd, - ) + head_filerecord = f"{sim_name}.hds" + budget_filerecord = f"{sim_name}.cbc" + flopy.mf6.ModflowGwfoc( + gwf, + head_filerecord=head_filerecord, + budget_filerecord=budget_filerecord, + saverecord=[("HEAD", "LAST"), ("BUDGET", "LAST")], + ) + return sim - head_filerecord = f"{sim_name}.hds" - budget_filerecord = f"{sim_name}.cbc" - flopy.mf6.ModflowGwfoc( - gwf, - head_filerecord=head_filerecord, - budget_filerecord=budget_filerecord, - saverecord=[("HEAD", "LAST"), ("BUDGET", "LAST")], - ) - return sim - return None +def write_models(sim, silent=True): + sim.write_simulation(silent=silent) -# Function to write MODFLOW 6 LAK Package problem 2 model files +@timed +def run_models(sim, silent=True): + success, buff = sim.run_simulation(silent=silent) + assert success, buff -def write_model(sim, silent=True): - if config.writeModel: - sim.write_simulation(silent=silent) +# - -# Function to run the LAK Package problem 2 model. -# True is returned if the model runs successfully +# ### Plotting results # +# Define functions to plot model results. - -@config.timeit -def run_model(sim, silent=True): - success = True - if config.runModel: - success, buff = sim.run_simulation(silent=silent) - if not success: - print(buff) - return success - - -# Function to plot grid +# + +figure_size = (6.3, 5.6) +masked_values = (0, 1e30, -1e30) def plot_grid(gwf, silent=True): - sim_ws = os.path.join(ws, sim_name) + sim_ws = os.path.join(workspace, sim_name) # create lake array ilake = gwf.dis.idomain.array @@ -791,7 +769,7 @@ def plot_grid(gwf, silent=True): [xedges[16], ycenters[22]], ] parts = [poly0, poly1, poly2] - shape_pth = os.path.join(ws, sim_name, "sfr.shp") + shape_pth = os.path.join(workspace, sim_name, "sfr.shp") w = shp.Writer(target=shape_pth, shapeType=shp.POLYLINE) w.field("no", "C") w.line([poly0]) @@ -833,348 +811,323 @@ def plot_grid(gwf, silent=True): pl1 = (xcenters[8], ycenters[8]) pl2 = (xcenters[8], ycenters[18]) - fs = USGSFigure(figure_type="map", verbose=False) - fig = plt.figure( - figsize=(4, 6.9), - tight_layout=True, - ) - plt.axis("off") - - nrows, ncols = 10, 1 - axes = [fig.add_subplot(nrows, ncols, (1, 8))] - - for idx, ax in enumerate(axes): - ax.set_xlim(extents[:2]) - ax.set_ylim(extents[2:]) - ax.set_aspect("equal") - - # legend axis - axes.append(fig.add_subplot(nrows, ncols, (9, 10))) - - # set limits for legend area - ax = axes[-1] - ax.set_xlim(0, 1) - ax.set_ylim(0, 1) - - # get rid of ticks and spines for legend area - ax.axis("off") - ax.set_xticks([]) - ax.set_yticks([]) - ax.spines["top"].set_color("none") - ax.spines["bottom"].set_color("none") - ax.spines["left"].set_color("none") - ax.spines["right"].set_color("none") - ax.patch.set_alpha(0.0) - - ax = axes[0] - mm = flopy.plot.PlotMapView(gwf, ax=ax, extent=extents) - mm.plot_bc("CHD", color="cyan") - for shape in sfr.shapeRecords(): - x = [i[0] for i in shape.shape.points[:]] - y = [i[1] for i in shape.shape.points[:]] - ax.plot(x, y, color="#3BB3D0", lw=1.5, zorder=1) - mm.plot_inactive(color_noflow="#5DBB63") - mm.plot_grid(lw=0.5, color="black") - cv = mm.contour_array( - head, - levels=np.arange(120, 160, 5), - linewidths=0.75, - linestyles="-", - colors="blue", - masked_values=masked_values, - ) - plt.clabel(cv, fmt="%1.0f") - mm.plot_vector(qx, qy, normalize=True, color="0.75") - ax.plot(p1[0], p1[1], marker="o", mfc="red", mec="black", ms=4) - ax.plot(p2[0], p2[1], marker="o", mfc="red", mec="black", ms=4) - ax.plot(p3[0], p3[1], marker="o", mfc="red", mec="black", ms=4) - ax.set_xlabel("x-coordinate, in feet") - ax.set_ylabel("y-coordinate, in feet") - fs.add_text( - ax, - "A", - x=p1[0] + 150, - y=p1[1] + 150, - transform=False, - bold=False, - color="red", - ha="left", - va="bottom", - ) - fs.add_text( - ax, - "B", - x=p2[0] + 150, - y=p2[1] + 150, - transform=False, - bold=False, - color="red", - ha="left", - va="bottom", - ) - fs.add_text( - ax, - "C", - x=p3[0] + 150, - y=p3[1] + 150, - transform=False, - bold=False, - color="red", - ha="left", - va="bottom", - ) - fs.add_text( - ax, - "Lake 1", - x=pl1[0], - y=pl1[1], - transform=False, - italic=False, - color="white", - ha="center", - va="center", - ) - fs.add_text( - ax, - "Lake 2", - x=pl2[0], - y=pl2[1], - transform=False, - italic=False, - color="white", - ha="center", - va="center", - ) - fs.remove_edge_ticks(ax) - - # legend - ax = axes[-1] - ax.plot( - -10000, - -10000, - lw=0, - marker="s", - ms=10, - mfc="#5DBB63", - mec="black", - markeredgewidth=0.5, - label="Lake boundary", - ) - ax.plot( - -10000, - -10000, - lw=0, - marker="s", - ms=10, - mfc="cyan", - mec="black", - markeredgewidth=0.5, - label="Constant-head boundary", - ) - ax.plot( - -10000, - -10000, - lw=1.5, - color="#3BB3D0", - label="Stream network", - ) - ax.plot( - -10000, - -10000, - lw=0, - marker="o", - ms=4, - mfc="red", - mec="black", - markeredgewidth=0.5, - label="Observation well", - ) - ax.plot( - -10000, - -10000, - lw=0.75, - ls="-", - color="blue", - label=r"Head contour, $ft$", - ) - ax.plot( - -10000, - -10000, - lw=0, - marker="$\u2192$", - ms=10, - mfc="0.75", - mec="0.75", - label="Normalized specific discharge", - ) - fs.graph_legend(ax, loc="lower center", ncol=2) - - # save figure - if config.plotSave: - fpth = os.path.join( - "..", - "figures", - f"{sim_name}-grid{config.figure_ext}", + with styles.USGSMap(): + fig = plt.figure( + figsize=(4, 6.9), + tight_layout=True, ) - fig.savefig(fpth) - - return - + plt.axis("off") + + nrows, ncols = 10, 1 + axes = [fig.add_subplot(nrows, ncols, (1, 8))] + + for idx, ax in enumerate(axes): + ax.set_xlim(extents[:2]) + ax.set_ylim(extents[2:]) + ax.set_aspect("equal") + + # legend axis + axes.append(fig.add_subplot(nrows, ncols, (9, 10))) + + # set limits for legend area + ax = axes[-1] + ax.set_xlim(0, 1) + ax.set_ylim(0, 1) + + # get rid of ticks and spines for legend area + ax.axis("off") + ax.set_xticks([]) + ax.set_yticks([]) + ax.spines["top"].set_color("none") + ax.spines["bottom"].set_color("none") + ax.spines["left"].set_color("none") + ax.spines["right"].set_color("none") + ax.patch.set_alpha(0.0) + + ax = axes[0] + mm = flopy.plot.PlotMapView(gwf, ax=ax, extent=extents) + mm.plot_bc("CHD", color="cyan") + for shape in sfr.shapeRecords(): + x = [i[0] for i in shape.shape.points[:]] + y = [i[1] for i in shape.shape.points[:]] + ax.plot(x, y, color="#3BB3D0", lw=1.5, zorder=1) + mm.plot_inactive(color_noflow="#5DBB63") + mm.plot_grid(lw=0.5, color="black") + cv = mm.contour_array( + head, + levels=np.arange(120, 160, 5), + linewidths=0.75, + linestyles="-", + colors="blue", + masked_values=masked_values, + ) + plt.clabel(cv, fmt="%1.0f") + mm.plot_vector(qx, qy, normalize=True, color="0.75") + ax.plot(p1[0], p1[1], marker="o", mfc="red", mec="black", ms=4) + ax.plot(p2[0], p2[1], marker="o", mfc="red", mec="black", ms=4) + ax.plot(p3[0], p3[1], marker="o", mfc="red", mec="black", ms=4) + ax.set_xlabel("x-coordinate, in feet") + ax.set_ylabel("y-coordinate, in feet") + styles.add_text( + ax, + "A", + x=p1[0] + 150, + y=p1[1] + 150, + transform=False, + bold=False, + color="red", + ha="left", + va="bottom", + ) + styles.add_text( + ax, + "B", + x=p2[0] + 150, + y=p2[1] + 150, + transform=False, + bold=False, + color="red", + ha="left", + va="bottom", + ) + styles.add_text( + ax, + "C", + x=p3[0] + 150, + y=p3[1] + 150, + transform=False, + bold=False, + color="red", + ha="left", + va="bottom", + ) + styles.add_text( + ax, + "Lake 1", + x=pl1[0], + y=pl1[1], + transform=False, + italic=False, + color="white", + ha="center", + va="center", + ) + styles.add_text( + ax, + "Lake 2", + x=pl2[0], + y=pl2[1], + transform=False, + italic=False, + color="white", + ha="center", + va="center", + ) + styles.remove_edge_ticks(ax) + + # legend + ax = axes[-1] + ax.plot( + -10000, + -10000, + lw=0, + marker="s", + ms=10, + mfc="#5DBB63", + mec="black", + markeredgewidth=0.5, + label="Lake boundary", + ) + ax.plot( + -10000, + -10000, + lw=0, + marker="s", + ms=10, + mfc="cyan", + mec="black", + markeredgewidth=0.5, + label="Constant-head boundary", + ) + ax.plot( + -10000, + -10000, + lw=1.5, + color="#3BB3D0", + label="Stream network", + ) + ax.plot( + -10000, + -10000, + lw=0, + marker="o", + ms=4, + mfc="red", + mec="black", + markeredgewidth=0.5, + label="Observation well", + ) + ax.plot( + -10000, + -10000, + lw=0.75, + ls="-", + color="blue", + label=r"Head contour, $ft$", + ) + ax.plot( + -10000, + -10000, + lw=0, + marker="$\u2192$", + ms=10, + mfc="0.75", + mec="0.75", + label="Normalized specific discharge", + ) + styles.graph_legend(ax, loc="lower center", ncol=2) -# Function to plot the lake results + if plot_show: + plt.show() + if plot_save: + fpth = os.path.join( + "..", + "figures", + f"{sim_name}-grid.png", + ) + fig.savefig(fpth) def plot_lak_results(gwf, silent=True): - fs = USGSFigure(figure_type="graph", verbose=False) - - # load the observations - lak_results = gwf.lak.output.obs().data - gwf_results = gwf.obs[0].output.obs().data - - dtype = [ - ("time", float), - ("LAKE1", float), - ("LAKE2", float), - ("A", float), - ("B", float), - ("C", float), - ] - - results = np.zeros((lak_results.shape[0] + 1), dtype=dtype) - results["time"][1:] = lak_results["totim"] - results["LAKE1"][0] = lak_strt - results["LAKE1"][1:] = lak_results["LAKE1"] - results["LAKE2"][0] = lak_strt - results["LAKE2"][1:] = lak_results["LAKE2"] - results["A"][0] = strt - results["A"][1:] = gwf_results["A"] - results["B"][0] = strt - results["B"][1:] = gwf_results["B"] - results["C"][0] = strt - results["C"][1:] = gwf_results["C"] - - # create the figure - fig, axes = plt.subplots( - ncols=1, - nrows=2, - sharex=True, - figsize=(6.3, 4.3), - constrained_layout=True, - ) + with styles.USGSPlot(): + # load the observations + lak_results = gwf.lak.output.obs().data + gwf_results = gwf.obs[0].output.obs().data + + dtype = [ + ("time", float), + ("LAKE1", float), + ("LAKE2", float), + ("A", float), + ("B", float), + ("C", float), + ] - ax = axes[0] - ax.set_xlim(0, 1500) - ax.set_ylim(110, 130) - ax.plot( - results["time"], - results["LAKE1"], - lw=0.75, - ls="--", - color="black", - label="Lake 1 stage", - ) - ax.plot( - results["time"], - results["LAKE2"], - lw=0.75, - ls="-.", - color="black", - label="Lake 2 stage", - ) - ax.set_xticks([0, 250, 500, 750, 1000, 1250, 1500]) - ax.set_yticks([110, 115, 120, 125, 130]) - ax.set_ylabel("Lake stage, in feet") - fs.graph_legend(ax, loc="upper right") - fs.heading(ax, idx=0) - - ax = axes[1] - ax.set_xlim(0, 1500) - ax.set_ylim(110, 160) - ax.plot( - results["time"], - results["A"], - lw=0.75, - ls="-", - color="0.5", - label="Point A", - ) - ax.plot( - results["time"], - results["B"], - lw=0.75, - ls="-", - color="black", - label="Point B", - ) - ax.plot( - results["time"], - results["C"], - lw=0.75, - ls="-.", - color="black", - label="Point C", - ) - ax.set_xticks([0, 250, 500, 750, 1000, 1250, 1500]) - ax.set_xlabel("Simulation time, in days") - ax.set_yticks([110, 120, 130, 140, 150, 160]) - ax.set_ylabel("Head, in feet") - fs.graph_legend(ax, loc="upper left") - fs.heading(ax, idx=1) - - # save figure - if config.plotSave: - fpth = os.path.join( - "..", - "figures", - f"{sim_name}-01{config.figure_ext}", + results = np.zeros((lak_results.shape[0] + 1), dtype=dtype) + results["time"][1:] = lak_results["totim"] + results["LAKE1"][0] = lak_strt + results["LAKE1"][1:] = lak_results["LAKE1"] + results["LAKE2"][0] = lak_strt + results["LAKE2"][1:] = lak_results["LAKE2"] + results["A"][0] = strt + results["A"][1:] = gwf_results["A"] + results["B"][0] = strt + results["B"][1:] = gwf_results["B"] + results["C"][0] = strt + results["C"][1:] = gwf_results["C"] + + # create the figure + fig, axes = plt.subplots( + ncols=1, + nrows=2, + sharex=True, + figsize=(6.3, 4.3), + constrained_layout=True, ) - fig.savefig(fpth) - - return - -# Function to plot the LAK Package problem 2 model results. + ax = axes[0] + ax.set_xlim(0, 1500) + ax.set_ylim(110, 130) + ax.plot( + results["time"], + results["LAKE1"], + lw=0.75, + ls="--", + color="black", + label="Lake 1 stage", + ) + ax.plot( + results["time"], + results["LAKE2"], + lw=0.75, + ls="-.", + color="black", + label="Lake 2 stage", + ) + ax.set_xticks([0, 250, 500, 750, 1000, 1250, 1500]) + ax.set_yticks([110, 115, 120, 125, 130]) + ax.set_ylabel("Lake stage, in feet") + styles.graph_legend(ax, loc="upper right") + styles.heading(ax, idx=0) + + ax = axes[1] + ax.set_xlim(0, 1500) + ax.set_ylim(110, 160) + ax.plot( + results["time"], + results["A"], + lw=0.75, + ls="-", + color="0.5", + label="Point A", + ) + ax.plot( + results["time"], + results["B"], + lw=0.75, + ls="-", + color="black", + label="Point B", + ) + ax.plot( + results["time"], + results["C"], + lw=0.75, + ls="-.", + color="black", + label="Point C", + ) + ax.set_xticks([0, 250, 500, 750, 1000, 1250, 1500]) + ax.set_xlabel("Simulation time, in days") + ax.set_yticks([110, 120, 130, 140, 150, 160]) + ax.set_ylabel("Head, in feet") + styles.graph_legend(ax, loc="upper left") + styles.heading(ax, idx=1) + + if plot_show: + plt.show() + if plot_save: + fpth = os.path.join( + "..", + "figures", + f"{sim_name}-01.png", + ) + fig.savefig(fpth) def plot_results(sim, silent=True): - if config.plotModel: - gwf = sim.get_model(sim_name) + gwf = sim.get_model(sim_name) + plot_grid(gwf, silent=silent) + plot_lak_results(gwf, silent=silent) - plot_grid(gwf, silent=silent) - plot_lak_results(gwf, silent=silent) +# - - -# Function that wraps all of the steps for the LAK Package problem 2 model -# -# 1. build_model, -# 2. write_model, -# 3. run_model, and -# 4. plot_results. +# ### Running the example # +# Define and invoke a function to run the example scenario, then plot results. -def simulation(silent=True): - sim = build_model() - - write_model(sim, silent=silent) - - success = run_model(sim, silent=silent) - assert success, f"could not run...{sim_name}" - - if success: +# + +def scenario(silent=True): + sim = build_models() + if write: + write_models(sim, silent=silent) + if run: + run_models(sim, silent=silent) + if plot: plot_results(sim, silent=silent) -# nosetest - exclude block from this nosetest to the next nosetest -def test_01(): - simulation(silent=False) - - -# nosetest end - -if __name__ == "__main__": - # ### LAK Package problem 2 Simulation - # - - simulation() +scenario() +# - diff --git a/scripts/ex-gwf-lgr.py b/scripts/ex-gwf-lgr.py index d5ce87672..9f0dba44d 100644 --- a/scripts/ex-gwf-lgr.py +++ b/scripts/ex-gwf-lgr.py @@ -2,45 +2,44 @@ # # This script reproduces the model in Mehl and Hill (2013). -# ### MODFLOW 6 LGR Problem Setup - -# Append to system path to include the common subdirectory +# ### Initial setup +# +# Import dependencies, define the example name and workspace, and read settings from environment variables. +# + import os -import sys - -sys.path.append(os.path.join("..", "common")) +import pathlib as pl -# Imports - -import config import flopy import flopy.utils.binaryfile as bf import matplotlib.pyplot as plt import numpy as np -from figspecs import USGSFigure +from flopy.plot.styles import styles from flopy.utils.lgrutil import Lgr +from modflow_devtools.misc import get_env, timed -mf6exe = "mf6" -exe_name_mf = "mf2005" -exe_name_mt = "mt3dms" - -# Set figure properties specific to this problem - -figure_size = (7, 5) +# Example name and base workspace +workspace = pl.Path("../examples") +example_name = "ex-gwf-lgr" -# Base simulation and model name and workspace +# Settings from environment variables +write = get_env("WRITE", True) +run = get_env("RUN", True) +plot = get_env("PLOT", True) +plot_show = get_env("PLOT_SHOW", True) +plot_save = get_env("PLOT_SAVE", True) +# - -ws = config.base_ws -example_name = "ex-gwf-lgr" +# ### Define parameters +# +# Define model units, parameters and other settings. +# + # Model units - length_units = "meters" time_units = "days" -# Table - +# Model parameters nlayp = 3 # Number of layers in parent model nrowp = 15 # Number of rows in parent model ncolp = 15 # Number of columns in parent model @@ -55,7 +54,6 @@ # Additional model input preparation # Time related variables - delrp = 1544.1 / ncolp delcp = 1029.4 / nrowp numdays = 1 @@ -65,7 +63,6 @@ tsmult = [1.0] * numdays # Further parent model grid discretization - x = [round(x, 3) for x in np.linspace(50.0, 45.0, ncolp)] topp = np.repeat(x, nrowp).reshape((15, 15)).T z = [round(z, 3) for z in np.linspace(50.0, 0.0, nlayp + 1)] @@ -75,13 +72,11 @@ icelltype = [1, 0, 0] # Water table resides in layer 1 # Solver settings - nouter, ninner = 100, 300 hclose, rclose, relax = 1e-7, 1e-6, 0.97 # Prepping input for SFR package for parent model # Define the connections - connsp = [ (0, -1), (1, 0, -2), @@ -104,7 +99,6 @@ ] # Package_data information - sfrcells = [ (0, 0, 1), (0, 1, 1), @@ -204,7 +198,6 @@ # stream segment with all linear connections. Cheating a bit by the # knowledge that there are 89 stream reaches in the child model. This # is known from the original model - connsc = [] for i in np.arange(89): if i == 0: @@ -215,7 +208,6 @@ connsc.append((i, i - 1, -1 * (i + 1))) # Package_data information - sfrcellsc = [ (0, 0, 3), (0, 1, 3), @@ -518,363 +510,347 @@ ) sfrspdc = {0: [[0, "INFLOW", 0.0]]} +# - - -# ### Function to build models +# ### Model setup # -# MODFLOW 6 flopy simulation object (sim) is returned if building the model - - -def build_model(sim_name, silent=False): - if config.buildModel: - # Instantiate the MODFLOW 6 simulation - name = "lgr" - gwfname = "gwf-" + name - sim_ws = os.path.join(ws, sim_name) - sim = flopy.mf6.MFSimulation( - sim_name=sim_name, - version="mf6", - sim_ws=sim_ws, - exe_name=mf6exe, - continue_=True, - ) - - # Instantiating MODFLOW 6 time discretization - tdis_rc = [] - for i in range(len(perlen)): - tdis_rc.append((perlen[i], nstp[i], tsmult[i])) - flopy.mf6.ModflowTdis( - sim, nper=nper, perioddata=tdis_rc, time_units=time_units - ) - - # Instantiating MODFLOW 6 groundwater flow model - gwfname = gwfname + "-parent" - gwf = flopy.mf6.ModflowGwf( - sim, - modelname=gwfname, - save_flows=True, - newtonoptions="newton", - model_nam_file=f"{gwfname}.nam", - ) - - # Instantiating MODFLOW 6 solver for flow model - imsgwf = flopy.mf6.ModflowIms( - sim, - print_option="SUMMARY", - outer_dvclose=hclose, - outer_maximum=nouter, - under_relaxation="NONE", - inner_maximum=ninner, - inner_dvclose=hclose, - rcloserecord=rclose, - linear_acceleration="BICGSTAB", - scaling_method="NONE", - reordering_method="NONE", - relaxation_factor=relax, - filename=f"{gwfname}.ims", - ) - sim.register_ims_package(imsgwf, [gwf.name]) - - # Instantiating MODFLOW 6 discretization package - dis = flopy.mf6.ModflowGwfdis( - gwf, - nlay=nlayp, - nrow=nrowp, - ncol=ncolp, - delr=delrp, - delc=delcp, - top=topp, - botm=botmp, - idomain=idomainp, - filename=f"{gwfname}.dis", - ) +# Define functions to build models, write input files, and run the simulation. + + +# + +def build_models(sim_name, silent=False): + # Instantiate the MODFLOW 6 simulation + name = "lgr" + gwfname = "gwf-" + name + sim_ws = os.path.join(workspace, sim_name) + sim = flopy.mf6.MFSimulation( + sim_name=sim_name, + version="mf6", + sim_ws=sim_ws, + exe_name="mf6", + continue_=True, + ) - # Instantiating MODFLOW 6 initial conditions package for flow model - strt = [topp - 0.25, topp - 0.25, topp - 0.25] - ic = flopy.mf6.ModflowGwfic(gwf, strt=strt, filename=f"{gwfname}.ic") - - # Instantiating MODFLOW 6 node-property flow package - npf = flopy.mf6.ModflowGwfnpf( - gwf, - save_flows=False, - alternative_cell_averaging="AMT-LMK", - icelltype=icelltype, - k=k11, - k33=k33, - save_specific_discharge=False, - filename=f"{gwfname}.npf", - ) + # Instantiating MODFLOW 6 time discretization + tdis_rc = [] + for i in range(len(perlen)): + tdis_rc.append((perlen[i], nstp[i], tsmult[i])) + flopy.mf6.ModflowTdis(sim, nper=nper, perioddata=tdis_rc, time_units=time_units) + + # Instantiating MODFLOW 6 groundwater flow model + gwfname = gwfname + "-parent" + gwf = flopy.mf6.ModflowGwf( + sim, + modelname=gwfname, + save_flows=True, + newtonoptions="newton", + model_nam_file=f"{gwfname}.nam", + ) - # Instantiating MODFLOW 6 output control package for flow model - oc = flopy.mf6.ModflowGwfoc( - gwf, - budget_filerecord=f"{gwfname}.bud", - head_filerecord=f"{gwfname}.hds", - headprintrecord=[ - ("COLUMNS", 10, "WIDTH", 15, "DIGITS", 6, "GENERAL") - ], - saverecord=[("HEAD", "LAST"), ("BUDGET", "LAST")], - printrecord=[("HEAD", "LAST"), ("BUDGET", "LAST")], - ) + # Instantiating MODFLOW 6 solver for flow model + imsgwf = flopy.mf6.ModflowIms( + sim, + print_option="SUMMARY", + outer_dvclose=hclose, + outer_maximum=nouter, + under_relaxation="NONE", + inner_maximum=ninner, + inner_dvclose=hclose, + rcloserecord=rclose, + linear_acceleration="BICGSTAB", + scaling_method="NONE", + reordering_method="NONE", + relaxation_factor=relax, + filename=f"{gwfname}.ims", + ) + sim.register_ims_package(imsgwf, [gwf.name]) + + # Instantiating MODFLOW 6 discretization package + dis = flopy.mf6.ModflowGwfdis( + gwf, + nlay=nlayp, + nrow=nrowp, + ncol=ncolp, + delr=delrp, + delc=delcp, + top=topp, + botm=botmp, + idomain=idomainp, + filename=f"{gwfname}.dis", + ) - # Instantiating MODFLOW 6 constant head package - rowList = np.arange(0, nrowp).tolist() - layList = np.arange(0, nlayp).tolist() - chdspd_left = [] - chdspd_right = [] - - # Loop through rows, the left & right sides will appear in separate, - # dedicated packages - hd_left = 49.75 - hd_right = 44.75 - for l in layList: - for r in rowList: - # first, do left side of model - chdspd_left.append([(l, r, 0), hd_left]) - # finally, do right side of model - chdspd_right.append([(l, r, ncolp - 1), hd_right]) - - chdspd = {0: chdspd_left} - chd1 = flopy.mf6.modflow.mfgwfchd.ModflowGwfchd( - gwf, - maxbound=len(chdspd), - stress_period_data=chdspd, - save_flows=False, - pname="CHD-1", - filename=f"{gwfname}.chd1.chd", - ) - chdspd = {0: chdspd_right} - chd2 = flopy.mf6.modflow.mfgwfchd.ModflowGwfchd( - gwf, - maxbound=len(chdspd), - stress_period_data=chdspd, - save_flows=False, - pname="CHD-2", - filename=f"{gwfname}.chd2.chd", - ) + # Instantiating MODFLOW 6 initial conditions package for flow model + strt = [topp - 0.25, topp - 0.25, topp - 0.25] + ic = flopy.mf6.ModflowGwfic(gwf, strt=strt, filename=f"{gwfname}.ic") + + # Instantiating MODFLOW 6 node-property flow package + npf = flopy.mf6.ModflowGwfnpf( + gwf, + save_flows=False, + alternative_cell_averaging="AMT-LMK", + icelltype=icelltype, + k=k11, + k33=k33, + save_specific_discharge=False, + filename=f"{gwfname}.npf", + ) - # Instantiating MODFLOW 6 Parent model's SFR package - sfr = flopy.mf6.ModflowGwfsfr( - gwf, - print_stage=False, - print_flows=False, - budget_filerecord=gwfname + ".sfr.bud", - save_flows=True, - mover=True, - pname="SFR-parent", - time_conversion=86400.0, - boundnames=False, - nreaches=len(connsp), - packagedata=pkdat, - connectiondata=connsp, - perioddata=sfrspd, - filename=f"{gwfname}.sfr", - ) + # Instantiating MODFLOW 6 output control package for flow model + oc = flopy.mf6.ModflowGwfoc( + gwf, + budget_filerecord=f"{gwfname}.bud", + head_filerecord=f"{gwfname}.hds", + headprintrecord=[("COLUMNS", 10, "WIDTH", 15, "DIGITS", 6, "GENERAL")], + saverecord=[("HEAD", "LAST"), ("BUDGET", "LAST")], + printrecord=[("HEAD", "LAST"), ("BUDGET", "LAST")], + ) - # ------------------------------- - # Now pivoting to the child grid - # ------------------------------- - # Leverage flopy's "Lgr" class; was imported at start of script - ncpp = 3 - ncppl = [3, 3, 0] - lgr = Lgr( - nlayp, - nrowp, - ncolp, - delrp, - delcp, - topp, - botmp, - idomainp, - ncpp=ncpp, - ncppl=ncppl, - xllp=0.0, - yllp=0.0, - ) + # Instantiating MODFLOW 6 constant head package + rowList = np.arange(0, nrowp).tolist() + layList = np.arange(0, nlayp).tolist() + chdspd_left = [] + chdspd_right = [] + + # Loop through rows, the left & right sides will appear in separate, + # dedicated packages + hd_left = 49.75 + hd_right = 44.75 + for l in layList: + for r in rowList: + # first, do left side of model + chdspd_left.append([(l, r, 0), hd_left]) + # finally, do right side of model + chdspd_right.append([(l, r, ncolp - 1), hd_right]) + + chdspd = {0: chdspd_left} + chd1 = flopy.mf6.modflow.mfgwfchd.ModflowGwfchd( + gwf, + maxbound=len(chdspd), + stress_period_data=chdspd, + save_flows=False, + pname="CHD-1", + filename=f"{gwfname}.chd1.chd", + ) + chdspd = {0: chdspd_right} + chd2 = flopy.mf6.modflow.mfgwfchd.ModflowGwfchd( + gwf, + maxbound=len(chdspd), + stress_period_data=chdspd, + save_flows=False, + pname="CHD-2", + filename=f"{gwfname}.chd2.chd", + ) - # Get child grid info: - delrc, delcc = lgr.get_delr_delc() - idomainc = lgr.get_idomain() # child idomain - topc, botmc = lgr.get_top_botm() # top/bottom of child grid - - # Instantiate MODFLOW 6 child gwf model - gwfnamec = "gwf-" + name + "-child" - gwfc = flopy.mf6.ModflowGwf( - sim, - modelname=gwfnamec, - save_flows=True, - newtonoptions="newton", - model_nam_file=f"{gwfnamec}.nam", - ) + # Instantiating MODFLOW 6 Parent model's SFR package + sfr = flopy.mf6.ModflowGwfsfr( + gwf, + print_stage=False, + print_flows=False, + budget_filerecord=gwfname + ".sfr.bud", + save_flows=True, + mover=True, + pname="SFR-parent", + time_conversion=86400.0, + boundnames=False, + nreaches=len(connsp), + packagedata=pkdat, + connectiondata=connsp, + perioddata=sfrspd, + filename=f"{gwfname}.sfr", + ) - # Instantiating MODFLOW 6 discretization package for the child model - child_dis_shp = lgr.get_shape() - nlayc = child_dis_shp[0] - nrowc = child_dis_shp[1] - ncolc = child_dis_shp[2] - disc = flopy.mf6.ModflowGwfdis( - gwfc, - nlay=nlayc, - nrow=nrowc, - ncol=ncolc, - delr=delrc, - delc=delcc, - top=topc, - botm=botmc, - idomain=idomainc, - filename=f"{gwfnamec}.dis", - ) + # ------------------------------- + # Now pivoting to the child grid + # ------------------------------- + # Leverage flopy's "Lgr" class; was imported at start of script + ncpp = 3 + ncppl = [3, 3, 0] + lgr = Lgr( + nlayp, + nrowp, + ncolp, + delrp, + delcp, + topp, + botmp, + idomainp, + ncpp=ncpp, + ncppl=ncppl, + xllp=0.0, + yllp=0.0, + ) - # Instantiating MODFLOW 6 initial conditions package for child model - strtc = [ - topc - 0.25, - topc - 0.25, - topc - 0.25, - topc - 0.25, - topc - 0.25, - topc - 0.25, - ] - icc = flopy.mf6.ModflowGwfic( - gwfc, strt=strtc, filename=f"{gwfnamec}.ic" - ) + # Get child grid info: + delrc, delcc = lgr.get_delr_delc() + idomainc = lgr.get_idomain() # child idomain + topc, botmc = lgr.get_top_botm() # top/bottom of child grid + + # Instantiate MODFLOW 6 child gwf model + gwfnamec = "gwf-" + name + "-child" + gwfc = flopy.mf6.ModflowGwf( + sim, + modelname=gwfnamec, + save_flows=True, + newtonoptions="newton", + model_nam_file=f"{gwfnamec}.nam", + ) - # Instantiating MODFLOW 6 node property flow package for child model - icelltypec = [1, 1, 1, 0, 0, 0] - npfc = flopy.mf6.ModflowGwfnpf( - gwfc, - save_flows=False, - alternative_cell_averaging="AMT-LMK", - icelltype=icelltypec, - k=k11, - k33=k33, - save_specific_discharge=False, - filename=f"{gwfnamec}.npf", - ) + # Instantiating MODFLOW 6 discretization package for the child model + child_dis_shp = lgr.get_shape() + nlayc = child_dis_shp[0] + nrowc = child_dis_shp[1] + ncolc = child_dis_shp[2] + disc = flopy.mf6.ModflowGwfdis( + gwfc, + nlay=nlayc, + nrow=nrowc, + ncol=ncolc, + delr=delrc, + delc=delcc, + top=topc, + botm=botmc, + idomain=idomainc, + filename=f"{gwfnamec}.dis", + ) - # Instantiating MODFLOW 6 output control package for the child model - occ = flopy.mf6.ModflowGwfoc( - gwfc, - budget_filerecord=f"{gwfnamec}.bud", - head_filerecord=f"{gwfnamec}.hds", - headprintrecord=[ - ("COLUMNS", 10, "WIDTH", 15, "DIGITS", 6, "GENERAL") - ], - saverecord=[("HEAD", "LAST"), ("BUDGET", "LAST")], - printrecord=[("HEAD", "LAST"), ("BUDGET", "LAST")], - ) + # Instantiating MODFLOW 6 initial conditions package for child model + strtc = [ + topc - 0.25, + topc - 0.25, + topc - 0.25, + topc - 0.25, + topc - 0.25, + topc - 0.25, + ] + icc = flopy.mf6.ModflowGwfic(gwfc, strt=strtc, filename=f"{gwfnamec}.ic") + + # Instantiating MODFLOW 6 node property flow package for child model + icelltypec = [1, 1, 1, 0, 0, 0] + npfc = flopy.mf6.ModflowGwfnpf( + gwfc, + save_flows=False, + alternative_cell_averaging="AMT-LMK", + icelltype=icelltypec, + k=k11, + k33=k33, + save_specific_discharge=False, + filename=f"{gwfnamec}.npf", + ) - # Instantiating MODFLOW 6 Streamflow routing package for child model - sfrc = flopy.mf6.ModflowGwfsfr( - gwfc, - print_stage=False, - print_flows=False, - budget_filerecord=gwfnamec + ".sfr.bud", - save_flows=True, - mover=True, - pname="SFR-child", - time_conversion=86400.00, - boundnames=False, - nreaches=len(connsc), - packagedata=pkdatc, - connectiondata=connsc, - perioddata=sfrspdc, - filename=f"{gwfnamec}.sfr", - ) + # Instantiating MODFLOW 6 output control package for the child model + occ = flopy.mf6.ModflowGwfoc( + gwfc, + budget_filerecord=f"{gwfnamec}.bud", + head_filerecord=f"{gwfnamec}.hds", + headprintrecord=[("COLUMNS", 10, "WIDTH", 15, "DIGITS", 6, "GENERAL")], + saverecord=[("HEAD", "LAST"), ("BUDGET", "LAST")], + printrecord=[("HEAD", "LAST"), ("BUDGET", "LAST")], + ) - # Retrieve exchange data using Lgr class functionality - exchange_data = lgr.get_exchange_data() - - # Establish MODFLOW 6 GWF-GWF exchange - gwfgwf = flopy.mf6.ModflowGwfgwf( - sim, - exgtype="GWF6-GWF6", - print_flows=True, - print_input=True, - exgmnamea=gwfname, - exgmnameb=gwfnamec, - nexg=len(exchange_data), - exchangedata=exchange_data, - mvr_filerecord=f"{name}.mvr", - pname="EXG-1", - filename=f"{name}.exg", - ) + # Instantiating MODFLOW 6 Streamflow routing package for child model + sfrc = flopy.mf6.ModflowGwfsfr( + gwfc, + print_stage=False, + print_flows=False, + budget_filerecord=gwfnamec + ".sfr.bud", + save_flows=True, + mover=True, + pname="SFR-child", + time_conversion=86400.00, + boundnames=False, + nreaches=len(connsc), + packagedata=pkdatc, + connectiondata=connsc, + perioddata=sfrspdc, + filename=f"{gwfnamec}.sfr", + ) - # Instantiate MVR package - mvrpack = [[gwfname, "SFR-parent"], [gwfnamec, "SFR-child"]] - maxpackages = len(mvrpack) - - # Set up static SFR-to-SFR connections that remain fixed for entire simulation - static_mvrperioddata = [ # don't forget to use 0-based values - [ - mvrpack[0][0], - mvrpack[0][1], - 7, - mvrpack[1][0], - mvrpack[1][1], - 0, - "FACTOR", - 1.0, - ], - [ - mvrpack[1][0], - mvrpack[1][1], - 88, - mvrpack[0][0], - mvrpack[0][1], - 8, - "FACTOR", - 1, - ], - ] - - mvrspd = {0: static_mvrperioddata} - maxmvr = 2 - mvr = flopy.mf6.ModflowMvr( - gwfgwf, - modelnames=True, - maxmvr=maxmvr, - print_flows=True, - maxpackages=maxpackages, - packages=mvrpack, - perioddata=mvrspd, - filename=f"{name}.mvr", - ) + # Retrieve exchange data using Lgr class functionality + exchange_data = lgr.get_exchange_data() + + # Establish MODFLOW 6 GWF-GWF exchange + gwfgwf = flopy.mf6.ModflowGwfgwf( + sim, + exgtype="GWF6-GWF6", + print_flows=True, + print_input=True, + exgmnamea=gwfname, + exgmnameb=gwfnamec, + nexg=len(exchange_data), + exchangedata=exchange_data, + mvr_filerecord=f"{name}.mvr", + pname="EXG-1", + filename=f"{name}.exg", + ) - return sim - return None + # Instantiate MVR package + mvrpack = [[gwfname, "SFR-parent"], [gwfnamec, "SFR-child"]] + maxpackages = len(mvrpack) + # Set up static SFR-to-SFR connections that remain fixed for entire simulation + static_mvrperioddata = [ # don't forget to use 0-based values + [ + mvrpack[0][0], + mvrpack[0][1], + 7, + mvrpack[1][0], + mvrpack[1][1], + 0, + "FACTOR", + 1.0, + ], + [ + mvrpack[1][0], + mvrpack[1][1], + 88, + mvrpack[0][0], + mvrpack[0][1], + 8, + "FACTOR", + 1, + ], + ] + + mvrspd = {0: static_mvrperioddata} + maxmvr = 2 + mvr = flopy.mf6.ModflowMvr( + gwfgwf, + modelnames=True, + maxmvr=maxmvr, + print_flows=True, + maxpackages=maxpackages, + packages=mvrpack, + perioddata=mvrspd, + filename=f"{name}.mvr", + ) -# Function to write model files + return sim -def write_model(sim, silent=True): - if config.writeModel: - sim.write_simulation(silent=silent) +def write_models(sim, silent=True): + sim.write_simulation(silent=silent) -# Function to run the model. True is returned if the model runs successfully +@timed +def run_models(sim, silent=True): + success, buff = sim.run_simulation(silent=silent, report=True) + assert success, buff -@config.timeit -def run_model(sim, silent=True): - success = True - if config.runModel: - success = False - success, buff = sim.run_simulation(silent=silent) - if not success: - print(buff) - return success +# - +# ### Plotting results +# +# Define functions to plot model results. -# Function to plot the model results +# + +# Figure properties +figure_size = (7, 5) def plot_results(mf6, idx): - if config.plotModel: - print("Plotting model results...") - sim_name = mf6.name - fs = USGSFigure(figure_type="graph", verbose=False) - + sim_name = mf6.name + with styles.USGSPlot(): # Start by retrieving some output mf6_out_pth = mf6.simulation_data.mfpath.get_sim_path() sfr_parent_bud_file = list(mf6.model_names)[0] + ".sfr.bud" @@ -906,22 +882,14 @@ def plot_results(mf6, idx): gwswc.append(datc[0]) # No values for some reason? - dat_fmp = modobjp.get_data( - kstpkper=kstpkper, text=" FROM-MVR" - ) - dat_tmp = modobjp.get_data( - kstpkper=kstpkper, text=" TO-MVR" - ) + dat_fmp = modobjp.get_data(kstpkper=kstpkper, text=" FROM-MVR") + dat_tmp = modobjp.get_data(kstpkper=kstpkper, text=" TO-MVR") toMvrp.append(dat_fmp[0]) fromMvrp.append(dat_tmp[0]) # No values for some reason? - dat_fmc = modobjc.get_data( - kstpkper=kstpkper, text=" FROM-MVR" - ) - dat_tmc = modobjc.get_data( - kstpkper=kstpkper, text=" TO-MVR" - ) + dat_fmc = modobjc.get_data(kstpkper=kstpkper, text=" FROM-MVR") + dat_tmc = modobjc.get_data(kstpkper=kstpkper, text=" TO-MVR") toMvrc.append(dat_fmc[0]) fromMvrc.append(dat_tmc[0]) @@ -956,9 +924,7 @@ def plot_results(mf6, idx): # Now get center of all the reaches rch_lengths = [] for i in np.arange(len(all_rch_lengths)): - rch_lengths.append( - np.sum(all_rch_lengths[0:i]) + (all_rch_lengths[i] / 2) - ) + rch_lengths.append(np.sum(all_rch_lengths[0:i]) + (all_rch_lengths[i] / 2)) # Make a continuous vector of the gw-sw exchanges gwsw_exg = np.zeros(len(connsp) + len(connsc)) @@ -972,9 +938,7 @@ def plot_results(mf6, idx): for j, jtm in enumerate(gwswc[0]): gwsw_exg[8 + j] = jtm[2] - fig, ax1 = plt.subplots( - figsize=figure_size, dpi=300, tight_layout=True - ) + fig, ax1 = plt.subplots(figsize=figure_size, dpi=300, tight_layout=True) pts = ax1.plot(rch_lengths, strmQ, "r^", label="Stream Flow", zorder=3) ax1.set_zorder(4) ax1.set_facecolor("none") @@ -985,12 +949,8 @@ def plot_results(mf6, idx): ha="center", fontsize=10, ) - ax1.arrow( - 1080, 163, -440, 0, head_width=5, head_length=50, fc="k", ec="k" - ) - ax1.arrow( - 2150, 163, 395, 0, head_width=5, head_length=50, fc="k", ec="k" - ) + ax1.arrow(1080, 163, -440, 0, head_width=5, head_length=50, fc="k", ec="k") + ax1.arrow(2150, 163, 395, 0, head_width=5, head_length=50, fc="k", ec="k") ax1.arrow( 525, 27, @@ -1103,44 +1063,33 @@ def plot_results(mf6, idx): title = "River conditions with steady flow" letter = chr(ord("@") + idx + 1) - fs.heading(letter=letter, heading=title) + styles.heading(letter=letter, heading=title) - # save figure - if config.plotSave: - fpth = os.path.join( - "..", "figures", f"{sim_name}{config.figure_ext}" - ) + if plot_show: + plt.show() + if plot_save: + fpth = os.path.join("..", "figures", f"{sim_name}.png") fig.savefig(fpth) -# Function that wraps all of the steps for each scenario -# -# 1. build_model, -# 2. write_model, -# 3. run_model, and -# 4. plot_results. +# - + +# ### Running the example # +# Define and invoke a function to run the example scenario, then plot results. +# + def scenario(idx, silent=True): - sim = build_model(example_name) - write_model(sim, silent=silent) - success = run_model(sim, silent=silent) - - if success: + sim = build_models(example_name) + if write: + write_models(sim, silent=silent) + if run: + run_models(sim, silent=silent) + if plot: plot_results(sim, idx) -# nosetest - exclude block from this nosetest to the next nosetest -def test_01(): - scenario(0, silent=False) - - -# nosetest end - -if __name__ == "__main__": - # ### Mehl and Hill (2013) results - # - # Two-dimensional transport in a uniform flow field - - scenario(0) +# Two-dimensional transport in a uniform flow field +scenario(0) +# - diff --git a/scripts/ex-gwf-lgrv.py b/scripts/ex-gwf-lgrv.py index 6714e51e3..fac13197c 100644 --- a/scripts/ex-gwf-lgrv.py +++ b/scripts/ex-gwf-lgrv.py @@ -2,53 +2,51 @@ # # These are the models described in Vilhelmsen et al. (2012). The parent # model is 9 layers, the child model is 25 layers. -# -# ### LGRV Problem Setup +# ### Initial setup # -# Imports +# Import dependencies, define the example name and workspace, and read settings from environment variables. +# + import os -import sys +import pathlib as pl import flopy import flopy.utils.lgrutil import matplotlib.pyplot as plt import numpy as np +import pooch +from flopy.plot.styles import styles +from modflow_devtools.misc import get_env, timed + +# Base workspace +workspace = pl.Path("../examples") + +# Settings from environment variables +write = get_env("WRITE", True) +run = get_env("RUN", True) +plot = get_env("PLOT", True) +plot_show = get_env("PLOT_SHOW", True) +plot_save = get_env("PLOT_SAVE", True) +# - + +# ### Define parameters +# +# Define model units, parameters and other settings. -# Append to system path to include the common subdirectory - -sys.path.append(os.path.join("..", "common")) - -# import common functionality - -import config -from figspecs import USGSFigure - -# Set default figure properties - -figure_size = (5, 4) - -# Base simulation and data workspace - -ws = config.base_ws -data_ws = os.path.join(config.data_ws, "ex-gwf-lgrv") - +# + # Model units - length_units = "meters" time_units = "seconds" -# Scenario parameters - +# Scenario-specific parameters parameters = { "ex-gwf-lgrv-gr": {"configuration": "Refined"}, "ex-gwf-lgrv-gc": {"configuration": "Coarse"}, "ex-gwf-lgrv-lgr": {"configuration": "LGR"}, } -# Table LGRV Model Parameters - +# Model parameters nper = 1 # Number of periods nlay = 25 # Number of layers in refined model nrow = 183 # Number of rows in refined model @@ -70,21 +68,55 @@ # Static temporal data used by TDIS file # Simulation has 1 steady stress period (1 day) - perlen = [1.0] nstp = [1] tsmult = [1.0] tdis_ds = list(zip(perlen, nstp, tsmult)) # load data files and process into arrays - -fname = os.path.join(data_ws, "top.dat") +fname = pooch.retrieve( + url=f"https://github.com/MODFLOW-USGS/modflow6-examples/raw/master/data/ex-gwf-lgrv/top.dat", + known_hash="md5:7e95923e78d0a2e2133929376d913ecf", +) top = np.loadtxt(fname) ikzone = np.empty((nlay, nrow, ncol), dtype=float) +hashes = [ + "548876af515cb8db0c17e7cba0cda364", + "548876af515cb8db0c17e7cba0cda364", + "548876af515cb8db0c17e7cba0cda364", + "548876af515cb8db0c17e7cba0cda364", + "d91e2663ad671119de17a91ffb2a65ab", + "1702d97a83074669db86521b22b87304", + "da973a8edd77a44ed23a29661e2935eb", + "0d514a8f7b208e840a8e5cbfe4018c63", + "afd45ac7125f8351b73add34d35d8435", + "dc99e101996376e1dde1d172dea05f5d", + "fe8d2de0558245c9270597b01070403a", + "cd1fcf0fe2c807ff53eda80860bfe50f", + "c77b43c8045f5459a75547d575665134", + "2252b57927d7d01d0f9a0dda262397d7", + "1399c6872dd7c41be4b10c1251ad7c65", + "331e4006370cceb3864b881b7a0558d6", + "a46e66e632c5af7bf104900793509e5d", + "41f44eee3db48234069de4e9e329317b", + "8f4f1bdd6b6863c3ba68e2336f5ff70a", + "a1c856c05bcc4a17cf3fe61b87fbc720", + "a23fef9e866ad9763943150a3b70f5eb", + "02d67bc7f67814d2248c27cdd5205cf8", + "e7a07da76d111ef2229d26c553daa7eb", + "f6bc51735c4af81e1298d0efd06cd506", + "432b9a02f3ff4906a214b271c965ebad", +] for k in range(nlay): - fname = os.path.join(data_ws, f"ikzone{k + 1}.dat") + fname = pooch.retrieve( + url=f"https://github.com/MODFLOW-USGS/modflow6-examples/raw/master/data/ex-gwf-lgrv/ikzone{k + 1}.dat", + known_hash=f"md5:{hashes[k]}", + ) ikzone[k, :, :] = np.loadtxt(fname) -fname = os.path.join(data_ws, "riv.dat") +fname = pooch.retrieve( + url=f"https://github.com/MODFLOW-USGS/modflow6-examples/raw/master/data/ex-gwf-lgrv/riv.dat", + known_hash="md5:5ccbe4f29940376309db445dbb2d75d0", +) dt = [ ("k", int), ("i", int), @@ -97,9 +129,7 @@ rivdat["k"] -= 1 rivdat["i"] -= 1 rivdat["j"] -= 1 -riv_spd = [ - [(k, i, j), stage, cond, rbot] for k, i, j, stage, cond, rbot in rivdat -] +riv_spd = [[(k, i, j), stage, cond, rbot] for k, i, j, stage, cond, rbot in rivdat] botm = [30 - k * delv for k in range(nlay)] botm = np.array(botm) @@ -109,7 +139,6 @@ k11 = np.where(ikzone == i + 1, kval, k11) # Define model extent and child model extent - xmin = 0 xmax = ncol * delr ymin = 0.0 @@ -123,17 +152,18 @@ ] # Solver parameters - nouter = 50 ninner = 100 hclose = 1e-6 rclose = 100.0 +# - -# ### Functions to build, write, run, and plot the MODFLOW 6 LGRV model +# ### Model setup # -# MODFLOW 6 flopy simulation object (sim) is returned if building the model +# Define functions to build models, write input files, and run the simulation. +# + def coarsen_shape(icoarsen, nrow, ncol): nrowc = int(np.ceil(nrow / icoarsen)) ncolc = int(np.ceil(ncol / icoarsen)) @@ -208,14 +238,10 @@ def riv_resample(icoarsen, nrow, ncol, rivdat, idomain, rowcolspan): return rivdatc -def build_lgr_model(sim_name): - sim_ws = os.path.join(ws, sim_name) - sim = flopy.mf6.MFSimulation( - sim_name=sim_name, sim_ws=sim_ws, exe_name="mf6" - ) - flopy.mf6.ModflowTdis( - sim, nper=nper, perioddata=tdis_ds, time_units=time_units - ) +def build_lgr_model(name): + sim_ws = os.path.join(workspace, name) + sim = flopy.mf6.MFSimulation(sim_name=name, sim_ws=sim_ws, exe_name="mf6") + flopy.mf6.ModflowTdis(sim, nper=nper, perioddata=tdis_ds, time_units=time_units) flopy.mf6.ModflowIms( sim, outer_maximum=nouter, @@ -228,11 +254,11 @@ def build_lgr_model(sim_name): # parent model with coarse grid icoarsen = 3 ncppl = [1, 3, 3, 3, 3, 3, 3, 3, 3] - sim = build_parent_model(sim, sim_name, icoarsen=icoarsen, ncppl=ncppl) + sim = build_parent_model(sim, name, icoarsen=icoarsen, ncppl=ncppl) gwf = sim.get_model("parent") # child model with fine grid - sim = build_child_model(sim, sim_name) + sim = build_child_model(sim, name) gwfc = sim.get_model("child") # use flopy lgr utility to wire up connections between parent and child @@ -276,7 +302,7 @@ def build_lgr_model(sim_name): return sim -def build_parent_model(sim, sim_name, icoarsen, ncppl): +def build_parent_model(sim, name, icoarsen, ncppl): xminp, xmaxp, yminp, ymaxp = model_domain xminc, xmaxc, yminc, ymaxc = child_domain delcp = delc * icoarsen @@ -289,8 +315,8 @@ def build_parent_model(sim, sim_name, icoarsen, ncppl): nlayp = len(ncppl) idomain = np.ones((nlayp, nrowp, ncolp), dtype=int) idomain[:, istart:istop, jstart:jstop] = 0 - sim = build_model( - sim_name, + sim = build_models( + name, icoarsen=icoarsen, ncppl=ncppl, idomain=idomain, @@ -300,7 +326,7 @@ def build_parent_model(sim, sim_name, icoarsen, ncppl): return sim -def build_child_model(sim, sim_name): +def build_child_model(sim, name): icoarsen = 1 xminp, xmaxp, yminp, ymaxp = model_domain xminc, xmaxc, yminc, ymaxc = child_domain @@ -311,8 +337,8 @@ def build_child_model(sim, sim_name): jstart = int((xminc - xminp) / delrp) jstop = int((xmaxc - xminp) / delrp) nrowp, ncolp = coarsen_shape(icoarsen, nrow, ncol) - sim = build_model( - sim_name, + sim = build_models( + name, rowcolspan=[istart, istop, jstart, jstop], sim=sim, modelname="child", @@ -322,8 +348,8 @@ def build_child_model(sim, sim_name): return sim -def build_model( - sim_name, +def build_models( + name, icoarsen=1, ncppl=None, rowcolspan=None, @@ -333,342 +359,301 @@ def build_model( xorigin=None, yorigin=None, ): - if config.buildModel: - if sim is None: - sim_ws = os.path.join(ws, sim_name) - sim = flopy.mf6.MFSimulation( - sim_name=sim_name, sim_ws=sim_ws, exe_name="mf6" - ) - flopy.mf6.ModflowTdis( - sim, nper=nper, perioddata=tdis_ds, time_units=time_units - ) - flopy.mf6.ModflowIms( - sim, - outer_maximum=nouter, - outer_dvclose=hclose, - inner_maximum=ninner, - inner_dvclose=hclose, - rcloserecord=f"{rclose} strict", - ) - if modelname is None: - modelname = sim_name - gwf = flopy.mf6.ModflowGwf(sim, modelname=modelname, save_flows=True) - - if ncppl is not None: - nlayc = len(ncppl) - layer_index = [ncppl[0] - 1] - for iln in ncppl[1:]: - last = layer_index[-1] - layer_index.append(iln + last) - else: - nlayc = nlay - layer_index = list(range(nlayc)) - nrowc, ncolc = coarsen_shape(icoarsen, nrow, ncol) - delrc = delr * icoarsen - delcc = delc * icoarsen - topc = array_resampler(top, icoarsen, "mean") - if rowcolspan is not None: - istart, istop, jstart, jstop = rowcolspan - nrowc = istop - istart - ncolc = jstop - jstart - else: - istart = 0 - istop = nrow - jstart = 0 - jstop = ncol - if idomain is None: - idomain = 1 - topc = topc[istart:istop, jstart:jstop] - flopy.mf6.ModflowGwfdis( - gwf, - length_units=length_units, - nlay=nlayc, - nrow=nrowc, - ncol=ncolc, - delr=delrc, - delc=delcc, - top=topc, - botm=botm[layer_index], - idomain=idomain, - xorigin=xorigin, - yorigin=yorigin, - ) - idomain = gwf.dis.idomain.array - k11c = [] - for k in range(nlayc): - ilay = layer_index[k] - a = array_resampler(k11[ilay], icoarsen, "maximum") - k11c.append(a[istart:istop, jstart:jstop]) - flopy.mf6.ModflowGwfnpf( - gwf, - k33overk=True, - icelltype=icelltype, - k=k11c, - save_specific_discharge=True, - k33=1.0, + if sim is None: + sim_ws = os.path.join(workspace, name) + sim = flopy.mf6.MFSimulation(sim_name=name, sim_ws=sim_ws, exe_name="mf6") + flopy.mf6.ModflowTdis(sim, nper=nper, perioddata=tdis_ds, time_units=time_units) + flopy.mf6.ModflowIms( + sim, + outer_maximum=nouter, + outer_dvclose=hclose, + inner_maximum=ninner, + inner_dvclose=hclose, + rcloserecord=f"{rclose} strict", ) - strt = nlayc * [topc] - flopy.mf6.ModflowGwfic(gwf, strt=strt) + if modelname is None: + modelname = name + gwf = flopy.mf6.ModflowGwf(sim, modelname=modelname, save_flows=True) + + if ncppl is not None: + nlayc = len(ncppl) + layer_index = [ncppl[0] - 1] + for iln in ncppl[1:]: + last = layer_index[-1] + layer_index.append(iln + last) + else: + nlayc = nlay + layer_index = list(range(nlayc)) + nrowc, ncolc = coarsen_shape(icoarsen, nrow, ncol) + delrc = delr * icoarsen + delcc = delc * icoarsen + topc = array_resampler(top, icoarsen, "mean") + if rowcolspan is not None: + istart, istop, jstart, jstop = rowcolspan + nrowc = istop - istart + ncolc = jstop - jstart + else: + istart = 0 + istop = nrow + jstart = 0 + jstop = ncol + if idomain is None: + idomain = 1 + topc = topc[istart:istop, jstart:jstop] + flopy.mf6.ModflowGwfdis( + gwf, + length_units=length_units, + nlay=nlayc, + nrow=nrowc, + ncol=ncolc, + delr=delrc, + delc=delcc, + top=topc, + botm=botm[layer_index], + idomain=idomain, + xorigin=xorigin, + yorigin=yorigin, + ) + idomain = gwf.dis.idomain.array + k11c = [] + for k in range(nlayc): + ilay = layer_index[k] + a = array_resampler(k11[ilay], icoarsen, "maximum") + k11c.append(a[istart:istop, jstart:jstop]) + flopy.mf6.ModflowGwfnpf( + gwf, + k33overk=True, + icelltype=icelltype, + k=k11c, + save_specific_discharge=True, + k33=1.0, + ) + strt = nlayc * [topc] + flopy.mf6.ModflowGwfic(gwf, strt=strt) + + rivdatc = riv_resample(icoarsen, nrow, ncol, rivdat, idomain, rowcolspan) + riv_spd = {0: rivdatc} + flopy.mf6.ModflowGwfriv( + gwf, + stress_period_data=riv_spd, + pname="RIV", + ) + flopy.mf6.ModflowGwfrcha(gwf, recharge=recharge, pname="RCH") + head_filerecord = f"{modelname}.hds" + budget_filerecord = f"{modelname}.cbc" + flopy.mf6.ModflowGwfoc( + gwf, + head_filerecord=head_filerecord, + budget_filerecord=budget_filerecord, + saverecord=[("HEAD", "ALL"), ("BUDGET", "ALL")], + ) + return sim - rivdatc = riv_resample( - icoarsen, nrow, ncol, rivdat, idomain, rowcolspan - ) - riv_spd = {0: rivdatc} - flopy.mf6.ModflowGwfriv( - gwf, - stress_period_data=riv_spd, - pname="RIV", - ) - flopy.mf6.ModflowGwfrcha(gwf, recharge=recharge, pname="RCH") - head_filerecord = f"{modelname}.hds" - budget_filerecord = f"{modelname}.cbc" - flopy.mf6.ModflowGwfoc( - gwf, - head_filerecord=head_filerecord, - budget_filerecord=budget_filerecord, - saverecord=[("HEAD", "ALL"), ("BUDGET", "ALL")], - ) - return sim - return None +def write_models(sim, silent=True): + sim.write_simulation(silent=silent) -# Function to write MODFLOW 6 LGRV model files +@timed +def run_models(sim, silent=False): + success, buff = sim.run_simulation(silent=silent, report=True) + assert success, buff -def write_model(sim, silent=True): - if config.writeModel: - print(f"Writing simulation {sim.name}") - sim.write_simulation(silent=silent) +# - -# Function to run the LGRV model. -# True is returned if the model runs successfully +# ### Plotting results # +# Define functions to plot model results. - -@config.timeit -def run_model(sim, silent=False): - success = True - if config.runModel: - print(f"Running simulation {sim.name}") - success, buff = sim.run_simulation(silent=silent, report=True) - if not success: - print(buff) - return success +# + +# Figure properties +figure_size = (5, 4) -# Function to plot the LGRV model results. -# def plot_grid(sim): - print(f"Plotting grid for {sim.name}...") - fs = USGSFigure(figure_type="map", verbose=False) - sim_ws = sim.simulation_data.mfpath.get_sim_path() - sim_name = sim.name - gwf = sim.get_model("parent") - gwfc = None - if "child" in list(sim.model_names): - gwfc = sim.get_model("child") - - fig = plt.figure(figsize=figure_size) - fig.tight_layout() - - ax = fig.add_subplot(1, 1, 1, aspect="equal") - pmv = flopy.plot.PlotMapView(model=gwf, ax=ax, layer=0) - # pmv.plot_grid() - idomain = gwf.dis.idomain.array - tp = np.ma.masked_where(idomain[0] == 0, gwf.dis.top.array) - vmin = tp.min() - vmax = tp.max() - if gwfc is not None: - tpc = gwfc.dis.top.array - vmin = min(vmin, tpc.min()) - vmax = max(vmax, tpc.max()) - - cb = pmv.plot_array(tp, cmap="jet", alpha=0.25, vmin=vmin, vmax=vmax) - pmv.plot_bc(name="RIV") - ax.set_xlabel("x position (m)") - ax.set_ylabel("y position (m)") - cbar = plt.colorbar(cb, shrink=0.5) - cbar.ax.set_xlabel(r"Top, ($m$)") - if gwfc is not None: - pmv = flopy.plot.PlotMapView(model=gwfc, ax=ax, layer=0) - _ = pmv.plot_array( - tpc, - cmap="jet", - alpha=0.25, - masked_values=[1e30], - vmin=vmin, - vmax=vmax, - ) + with styles.USGSMap(): + name = sim.name + gwf = sim.get_model("parent") + gwfc = None + if "child" in list(sim.model_names): + gwfc = sim.get_model("child") + + fig = plt.figure(figsize=figure_size) + fig.tight_layout() + + ax = fig.add_subplot(1, 1, 1, aspect="equal") + pmv = flopy.plot.PlotMapView(model=gwf, ax=ax, layer=0) + # pmv.plot_grid() + idomain = gwf.dis.idomain.array + tp = np.ma.masked_where(idomain[0] == 0, gwf.dis.top.array) + vmin = tp.min() + vmax = tp.max() + if gwfc is not None: + tpc = gwfc.dis.top.array + vmin = min(vmin, tpc.min()) + vmax = max(vmax, tpc.max()) + + cb = pmv.plot_array(tp, cmap="jet", alpha=0.25, vmin=vmin, vmax=vmax) pmv.plot_bc(name="RIV") - if gwfc is not None: - xmin, xmax, ymin, ymax = child_domain - ax.plot( - [xmin, xmax, xmax, xmin, xmin], - [ymin, ymin, ymax, ymax, ymin], - "k--", - ) - xmin, xmax, ymin, ymax = model_domain - ax.set_xlim(xmin, xmax) - ax.set_ylim(ymin, ymax) - - # save figure - if config.plotSave: - fpth = os.path.join( - "..", "figures", f"{sim_name}-grid{config.figure_ext}" - ) - fig.savefig(fpth) - return + ax.set_xlabel("x position (m)") + ax.set_ylabel("y position (m)") + cbar = plt.colorbar(cb, shrink=0.5) + cbar.ax.set_xlabel(r"Top, ($m$)") + if gwfc is not None: + pmv = flopy.plot.PlotMapView(model=gwfc, ax=ax, layer=0) + _ = pmv.plot_array( + tpc, + cmap="jet", + alpha=0.25, + masked_values=[1e30], + vmin=vmin, + vmax=vmax, + ) + pmv.plot_bc(name="RIV") + if gwfc is not None: + xmin, xmax, ymin, ymax = child_domain + ax.plot( + [xmin, xmax, xmax, xmin, xmin], + [ymin, ymin, ymax, ymax, ymin], + "k--", + ) + xmin, xmax, ymin, ymax = model_domain + ax.set_xlim(xmin, xmax) + ax.set_ylim(ymin, ymax) + + if plot_show: + plt.show() + if plot_save: + fpth = os.path.join("..", "figures", f"{name}-grid.png") + fig.savefig(fpth) def plot_xsect(sim): print(f"Plotting cross section for {sim.name}...") - fs = USGSFigure(figure_type="map", verbose=False) - sim_ws = sim.simulation_data.mfpath.get_sim_path() - sim_name = sim.name - gwf = sim.get_model("parent") - - fig = plt.figure(figsize=(5, 2.5)) - fig.tight_layout() - - ax = fig.add_subplot(1, 1, 1) - irow, icol = gwf.modelgrid.intersect(3000.0, 3000.0) - pmv = flopy.plot.PlotCrossSection(model=gwf, ax=ax, line={"column": icol}) - pmv.plot_grid(linewidth=0.5) - hyc = np.log(gwf.npf.k.array) - cb = pmv.plot_array(hyc, cmap="jet", alpha=0.25) - ax.set_xlabel("y position (m)") - ax.set_ylabel("z position (m)") - cbar = plt.colorbar(cb, shrink=0.5) - cbar.ax.set_xlabel(r"K, ($m/s$)") - - # save figure - if config.plotSave: - fpth = os.path.join( - "..", "figures", f"{sim_name}-xsect{config.figure_ext}" - ) - fig.savefig(fpth) - return + with styles.USGSMap(): + name = sim.name + gwf = sim.get_model("parent") + + fig = plt.figure(figsize=(5, 2.5)) + fig.tight_layout() + + ax = fig.add_subplot(1, 1, 1) + irow, icol = gwf.modelgrid.intersect(3000.0, 3000.0) + pmv = flopy.plot.PlotCrossSection(model=gwf, ax=ax, line={"column": icol}) + pmv.plot_grid(linewidth=0.5) + hyc = np.log(gwf.npf.k.array) + cb = pmv.plot_array(hyc, cmap="jet", alpha=0.25) + ax.set_xlabel("y position (m)") + ax.set_ylabel("z position (m)") + cbar = plt.colorbar(cb, shrink=0.5) + cbar.ax.set_xlabel(r"K, ($m/s$)") + + if plot_show: + plt.show() + if plot_save: + fpth = os.path.join("..", "figures", f"{name}-xsect.png") + fig.savefig(fpth) def plot_heads(sim): print(f"Plotting results for {sim.name} ...") - fs = USGSFigure(figure_type="map", verbose=False) - sim_ws = sim.simulation_data.mfpath.get_sim_path() - sim_name = sim.name - gwf = sim.get_model("parent") - modelname = gwf.name - gwfc = None - if "child" in list(sim.model_names): - gwfc = sim.get_model("child") - - fig = plt.figure(figsize=figure_size) - fig.tight_layout() - - print(" Loading heads...") - layer = 0 - head = gwf.output.head().get_data() - head = np.ma.masked_where(head > 1e29, head) - vmin = head[layer].min() - vmax = head[layer].max() - if gwfc is not None: - headc = gwfc.output.head().get_data() - vmin = min(vmin, headc.min()) - vmax = max(vmax, headc.max()) - - print(" Making figure...") - ax = fig.add_subplot(1, 1, 1, aspect="equal") - pmv = flopy.plot.PlotMapView(model=gwf, ax=ax, layer=0) - cb = pmv.plot_array( - head, cmap="jet", masked_values=[1e30], vmin=vmin, vmax=vmax - ) - ax.set_xlabel("x position (m)") - ax.set_ylabel("y position (m)") - cbar = plt.colorbar(cb, shrink=0.5) - cbar.ax.set_xlabel(r"Head, ($m$)") - if gwfc is not None: - pmv = flopy.plot.PlotMapView(model=gwfc, ax=ax, layer=0) + with styles.USGSMap(): + name = sim.name + gwf = sim.get_model("parent") + gwfc = None + if "child" in list(sim.model_names): + gwfc = sim.get_model("child") + + fig = plt.figure(figsize=figure_size) + fig.tight_layout() + + print(" Loading heads...") + layer = 0 + head = gwf.output.head().get_data() + head = np.ma.masked_where(head > 1e29, head) + vmin = head[layer].min() + vmax = head[layer].max() + if gwfc is not None: + headc = gwfc.output.head().get_data() + vmin = min(vmin, headc.min()) + vmax = max(vmax, headc.max()) + + print(" Making figure...") + ax = fig.add_subplot(1, 1, 1, aspect="equal") + pmv = flopy.plot.PlotMapView(model=gwf, ax=ax, layer=0) cb = pmv.plot_array( - headc, cmap="jet", masked_values=[1e30], vmin=vmin, vmax=vmax - ) - xmin, xmax, ymin, ymax = child_domain - ax.plot( - [xmin, xmax, xmax, xmin, xmin], - [ymin, ymin, ymax, ymax, ymin], - "k--", + head, cmap="jet", masked_values=[1e30], vmin=vmin, vmax=vmax ) - xmin, xmax, ymin, ymax = model_domain - ax.set_xlim(xmin, xmax) - ax.set_ylim(ymin, ymax) - - # save figure - if config.plotSave: - fpth = os.path.join( - "..", "figures", f"{sim_name}-head{config.figure_ext}" - ) - fig.savefig(fpth) - return + ax.set_xlabel("x position (m)") + ax.set_ylabel("y position (m)") + cbar = plt.colorbar(cb, shrink=0.5) + cbar.ax.set_xlabel(r"Head, ($m$)") + if gwfc is not None: + pmv = flopy.plot.PlotMapView(model=gwfc, ax=ax, layer=0) + cb = pmv.plot_array( + headc, cmap="jet", masked_values=[1e30], vmin=vmin, vmax=vmax + ) + xmin, xmax, ymin, ymax = child_domain + ax.plot( + [xmin, xmax, xmax, xmin, xmin], + [ymin, ymin, ymax, ymax, ymin], + "k--", + ) + xmin, xmax, ymin, ymax = model_domain + ax.set_xlim(xmin, xmax) + ax.set_ylim(ymin, ymax) + + if plot_show: + plt.show() + if plot_save: + fpth = os.path.join("..", "figures", f"{name}-head.png") + fig.savefig(fpth) def plot_results(sim, silent=True): - if config.plotModel: - plot_grid(sim) - plot_xsect(sim) - plot_heads(sim) - return + plot_grid(sim) + plot_xsect(sim) + plot_heads(sim) -# Function that wraps all of the steps for the LGRV model -# -# 1. build_model, -# 2. write_model, -# 3. run_model, and -# 4. plot_results. +# - + +# ### Running the example # +# Define and invoke a function to run the example scenario, then plot results. -def simulation(idx, silent=True): +# + +def scenario(idx, silent=True): key = list(parameters.keys())[idx] params = parameters[key].copy() if params["configuration"] == "Refined": - sim = build_model(key, modelname="parent") + sim = build_models(key, modelname="parent") elif params["configuration"] == "Coarse": ncppl = [1, 3, 3, 3, 3, 3, 3, 3, 3] - sim = build_model(key, icoarsen=3, ncppl=ncppl, modelname="parent") + sim = build_models(key, icoarsen=3, ncppl=ncppl, modelname="parent") elif params["configuration"] == "LGR": sim = build_lgr_model(key) - write_model(sim, silent=silent) - success = run_model(sim, silent=silent) - if success: + if write: + write_models(sim, silent=silent) + if run: + run_models(sim, silent=silent) + if plot: plot_results(sim, silent=silent) -# nosetest - exclude block from this nosetest to the next nosetest -def test_01(): - simulation(0, silent=False) - - -def test_02(): - simulation(1, silent=False) - - -def test_03(): - simulation(2, silent=False) - +# - -# nosetest end -if __name__ == "__main__": - # ### LGRV Simulation - # - # Global Refined Model +# Run the global refined model and plot results. - simulation(0) +scenario(0) - # Global Coarse Model +# Run the global coarse model and plot results. - simulation(1) +scenario(1) - # Locally Refined Grid Model +# Run the locally refined grid model and plot results. - simulation(2) +scenario(2) diff --git a/scripts/ex-gwf-maw-p01.py b/scripts/ex-gwf-maw-p01.py index adf7290a9..eb4df5a9d 100644 --- a/scripts/ex-gwf-maw-p01.py +++ b/scripts/ex-gwf-maw-p01.py @@ -2,49 +2,44 @@ # # This is the Neville-Tonkin Multi-Aquifer Well from the # Lake Package documentation (Neville and Tonkin, 2004). -# -# ### Neville-Tonkin MAW Problem Setup +# ### Initial setup # -# Imports +# Import dependencies, define the example name and workspace, and read settings from environment variables. +# + import os -import sys +import pathlib as pl import flopy import matplotlib as mpl import matplotlib.pyplot as plt import numpy as np +from flopy.plot.styles import styles +from modflow_devtools.misc import get_env, timed -# Append to system path to include the common subdirectory - -sys.path.append(os.path.join("..", "common")) - -# import common functionality - -import config -from figspecs import USGSFigure - -# Set figure properties specific to the - -figure_size = (6.3, 4.3) -masked_values = (0, 1e30, -1e30) - -# Base simulation and model name and workspace - -ws = config.base_ws +# Example name and base workspace +sim_name = "ex-gwf-maw-p01" +workspace = pl.Path("../examples") -# Simulation name +# Settings from environment variables +write = get_env("WRITE", True) +run = get_env("RUN", True) +plot = get_env("PLOT", True) +plot_show = get_env("PLOT_SHOW", True) +plot_save = get_env("PLOT_SAVE", True) +# - -sim_name = "ex-gwf-maw-p01" +# ### Define parameters +# +# Define model units, parameters and other settings. +# + # Model units - length_units = "meters" time_units = "days" -# Scenario parameters - +# Scenario-specific parameters parameters = { "ex-gwf-maw-p01a": { "rate": 0.0, @@ -54,8 +49,7 @@ }, } -# Table Neville-Tonkin MAW Problem Model Parameters - +# Model parameters nper = 1 # Number of periods nlay = 2 # Number of layers nrow = 101 # Number of rows @@ -71,22 +65,18 @@ maw_radius = 0.15 # Well radius ($m$) # parse parameter strings into tuples - botm = [float(value) for value in botm_str.split(",")] strt = [float(value) for value in strt_str.split(",")] # Static temporal data used by TDIS file - tdis_ds = ((2.314815, 50, 1.2),) # Define dimensions - extents = (0.0, delr * ncol, 0.0, delc * nrow) shape2d = (nrow, ncol) shape3d = (nlay, nrow, ncol) # create idomain - idomain = np.ones(shape3d, dtype=float) xw, yw = (ncol / 2) * delr, (nrow / 2) * delc y = 0.0 @@ -99,241 +89,220 @@ if r > 7163.0: idomain[:, i, j] = 0 -# ### Create Neville-Tonkin MAW Problem Model Boundary Conditions - -# MAW Package - +# MAW Package boundary conditions maw_row = int(nrow / 2) maw_col = int(ncol / 2) - maw_packagedata = [[0, maw_radius, botm[-1], strt[-1], "THIEM", 2]] - maw_conn = [ [0, 0, 0, maw_row, maw_col, top, botm[-1], -999.0, -999.0], [0, 1, 1, maw_row, maw_col, top, botm[-1], -999.0, -999.0], ] - # Solver parameters - nouter = 500 ninner = 100 hclose = 1e-9 rclose = 1e-4 +# - - -# ### Functions to build, write, run, and plot the MODFLOW 6 Neville-Tonkin MAW Problem model +# ### Model setup # -# MODFLOW 6 flopy simulation object (sim) is returned if building the model - +# Define functions to build models, write input files, and run the simulation. + + +# + +def build_models(name, rate=0.0): + sim_ws = os.path.join(workspace, name) + sim = flopy.mf6.MFSimulation(sim_name=sim_name, sim_ws=sim_ws, exe_name="mf6") + flopy.mf6.ModflowTdis(sim, nper=nper, perioddata=tdis_ds, time_units=time_units) + flopy.mf6.ModflowIms( + sim, + print_option="summary", + outer_maximum=nouter, + outer_dvclose=hclose, + inner_maximum=ninner, + inner_dvclose=hclose, + rcloserecord=f"{rclose} strict", + ) + gwf = flopy.mf6.ModflowGwf(sim, modelname=sim_name, save_flows=True) + flopy.mf6.ModflowGwfdis( + gwf, + length_units=length_units, + nlay=nlay, + nrow=nrow, + ncol=ncol, + delr=delr, + delc=delc, + top=top, + botm=botm, + idomain=idomain, + ) + flopy.mf6.ModflowGwfnpf( + gwf, + icelltype=0, + k=k11, + k33=k33, + save_specific_discharge=True, + ) + flopy.mf6.ModflowGwfsto( + gwf, + iconvert=0, + ss=ss, + ) + flopy.mf6.ModflowGwfic(gwf, strt=strt) + + maw_spd = [[0, "rate", rate]] + maw = flopy.mf6.ModflowGwfmaw( + gwf, + no_well_storage=True, + nmawwells=1, + packagedata=maw_packagedata, + connectiondata=maw_conn, + perioddata=maw_spd, + ) + obs_file = f"{sim_name}.maw.obs" + csv_file = obs_file + ".csv" + obs_dict = { + csv_file: [ + ("head", "head", (0,)), + ("Q1", "maw", (0,), (0,)), + ("Q2", "maw", (0,), (1,)), + ] + } + maw.obs.initialize( + filename=obs_file, digits=10, print_input=True, continuous=obs_dict + ) -def build_model(name, rate=0.0): - if config.buildModel: - sim_ws = os.path.join(ws, name) - sim = flopy.mf6.MFSimulation( - sim_name=sim_name, sim_ws=sim_ws, exe_name="mf6" - ) - flopy.mf6.ModflowTdis( - sim, nper=nper, perioddata=tdis_ds, time_units=time_units - ) - flopy.mf6.ModflowIms( - sim, - print_option="summary", - outer_maximum=nouter, - outer_dvclose=hclose, - inner_maximum=ninner, - inner_dvclose=hclose, - rcloserecord=f"{rclose} strict", - ) - gwf = flopy.mf6.ModflowGwf(sim, modelname=sim_name, save_flows=True) - flopy.mf6.ModflowGwfdis( - gwf, - length_units=length_units, - nlay=nlay, - nrow=nrow, - ncol=ncol, - delr=delr, - delc=delc, - top=top, - botm=botm, - idomain=idomain, - ) - flopy.mf6.ModflowGwfnpf( - gwf, - icelltype=0, - k=k11, - k33=k33, - save_specific_discharge=True, - ) - flopy.mf6.ModflowGwfsto( - gwf, - iconvert=0, - ss=ss, - ) - flopy.mf6.ModflowGwfic(gwf, strt=strt) - - maw_spd = [[0, "rate", rate]] - maw = flopy.mf6.ModflowGwfmaw( - gwf, - no_well_storage=True, - nmawwells=1, - packagedata=maw_packagedata, - connectiondata=maw_conn, - perioddata=maw_spd, - ) - obs_file = f"{sim_name}.maw.obs" - csv_file = obs_file + ".csv" - obs_dict = { - csv_file: [ - ("head", "head", (0,)), - ("Q1", "maw", (0,), (0,)), - ("Q2", "maw", (0,), (1,)), - ] - } - maw.obs.initialize( - filename=obs_file, digits=10, print_input=True, continuous=obs_dict - ) + flopy.mf6.ModflowGwfoc( + gwf, + printrecord=[("BUDGET", "LAST")], + ) + return sim - flopy.mf6.ModflowGwfoc( - gwf, - printrecord=[("BUDGET", "LAST")], - ) - return sim - return None +def write_models(sim, silent=True): + sim.write_simulation(silent=silent) -# Function to write MODFLOW 6 Neville-Tonkin MAW Problem model files +@timed +def run_models(sim, silent=True): + success, buff = sim.run_simulation(silent=silent) + assert success, buff -def write_model(sim, silent=True): - if config.writeModel: - sim.write_simulation(silent=silent) +# - -# Function to run the Neville-Tonkin MAW Problem model. -# True is returned if the model runs successfully +# ### Plotting results # +# Define functions to plot model results. - -@config.timeit -def run_model(sim, silent=True): - success = True - if config.runModel: - success, buff = sim.run_simulation(silent=silent) - if not success: - print(buff) - return success - - -# Function to plot the lake results +# + +# Figure properties +figure_size = (6.3, 4.3) +masked_values = (0, 1e30, -1e30) def plot_maw_results(silent=True): - fs = USGSFigure(figure_type="graph", verbose=False) - - # load the observations - name = list(parameters.keys())[0] - fpth = os.path.join(ws, name, f"{sim_name}.maw.obs.csv") - maw0 = flopy.utils.Mf6Obs(fpth).data - name = list(parameters.keys())[1] - fpth = os.path.join(ws, name, f"{sim_name}.maw.obs.csv") - maw1 = flopy.utils.Mf6Obs(fpth).data - - time = maw0["totim"] * 86400.0 - - tmin = time[0] - tmax = time[-1] - - # create the figure - fig, axes = plt.subplots( - ncols=1, - nrows=2, - sharex=True, - figsize=figure_size, - constrained_layout=True, - ) - - ax = axes[0] - ax.set_xlim(tmin, tmax) - ax.set_ylim(-1000, 1000) - ax.semilogx( - time, - maw0["Q1"], - lw=0.75, - ls="-", - color="blue", - label="Upper aquifer", - ) - ax.semilogx( - time, - maw0["Q2"], - lw=0.75, - ls="-", - color="red", - label="Lower aquifer", - ) - ax.axhline(0, lw=0.5, color="0.5") - ax.set_ylabel(" ") - fs.heading(ax, heading="Non-pumping case", idx=0) - fs.graph_legend(ax, loc="upper right", ncol=2) - - ax = axes[1] - ax.set_xlim(tmin, tmax) - ax.set_ylim(-500, 2500) - ax.semilogx( - time, - maw1["Q1"], - lw=0.75, - ls="-", - color="blue", - label="Upper aquifer", - ) - ax.semilogx( - time, - maw1["Q2"], - lw=0.75, - ls="-", - color="red", - label="Lower aquifer", - ) - ax.axhline(0, lw=0.5, color="0.5") - ax.set_xlabel(" ") - ax.set_ylabel(" ") - for axis in (ax.xaxis,): - axis.set_major_formatter(mpl.ticker.ScalarFormatter()) - fs.heading(ax, heading="Pumping case", idx=1) - - # add y-axis label that spans both subplots - ax = fig.add_subplot(1, 1, 1) - ax.set_xlim(0, 1) - ax.set_ylim(0, 1) - - # get rid of ticks and spines for legend area - # ax.axis("off") - ax.set_xticks([]) - ax.set_yticks([]) - ax.spines["top"].set_color("none") - ax.spines["bottom"].set_color("none") - ax.spines["left"].set_color("none") - ax.spines["right"].set_color("none") - ax.patch.set_alpha(0.0) - - ax.set_xlabel("Simulation time, in seconds") - ax.set_ylabel("Discharge rate, in cubic meters per day") - - # save figure - if config.plotSave: - fpth = os.path.join( - "..", - "figures", - f"{sim_name}-01{config.figure_ext}", + with styles.USGSPlot(): + # load the observations + name = list(parameters.keys())[0] + fpth = os.path.join(workspace, name, f"{sim_name}.maw.obs.csv") + maw0 = flopy.utils.Mf6Obs(fpth).data + name = list(parameters.keys())[1] + fpth = os.path.join(workspace, name, f"{sim_name}.maw.obs.csv") + maw1 = flopy.utils.Mf6Obs(fpth).data + + time = maw0["totim"] * 86400.0 + + tmin = time[0] + tmax = time[-1] + + # create the figure + fig, axes = plt.subplots( + ncols=1, + nrows=2, + sharex=True, + figsize=figure_size, + constrained_layout=True, ) - fig.savefig(fpth) - - return - -# Plot the grid + ax = axes[0] + ax.set_xlim(tmin, tmax) + ax.set_ylim(-1000, 1000) + ax.semilogx( + time, + maw0["Q1"], + lw=0.75, + ls="-", + color="blue", + label="Upper aquifer", + ) + ax.semilogx( + time, + maw0["Q2"], + lw=0.75, + ls="-", + color="red", + label="Lower aquifer", + ) + ax.axhline(0, lw=0.5, color="0.5") + ax.set_ylabel(" ") + styles.heading(ax, heading="Non-pumping case", idx=0) + styles.graph_legend(ax, loc="upper right", ncol=2) + + ax = axes[1] + ax.set_xlim(tmin, tmax) + ax.set_ylim(-500, 2500) + ax.semilogx( + time, + maw1["Q1"], + lw=0.75, + ls="-", + color="blue", + label="Upper aquifer", + ) + ax.semilogx( + time, + maw1["Q2"], + lw=0.75, + ls="-", + color="red", + label="Lower aquifer", + ) + ax.axhline(0, lw=0.5, color="0.5") + ax.set_xlabel(" ") + ax.set_ylabel(" ") + for axis in (ax.xaxis,): + axis.set_major_formatter(mpl.ticker.ScalarFormatter()) + styles.heading(ax, heading="Pumping case", idx=1) + + # add y-axis label that spans both subplots + ax = fig.add_subplot(1, 1, 1) + ax.set_xlim(0, 1) + ax.set_ylim(0, 1) + + # get rid of ticks and spines for legend area + # ax.axis("off") + ax.set_xticks([]) + ax.set_yticks([]) + ax.spines["top"].set_color("none") + ax.spines["bottom"].set_color("none") + ax.spines["left"].set_color("none") + ax.spines["right"].set_color("none") + ax.patch.set_alpha(0.0) + + ax.set_xlabel("Simulation time, in seconds") + ax.set_ylabel("Discharge rate, in cubic meters per day") + + if plot_show: + plt.show() + if plot_save: + fpth = os.path.join( + "..", + "figures", + f"{sim_name}-01.png", + ) + fig.savefig(fpth) def plot_grid(silent=True): @@ -342,142 +311,124 @@ def plot_grid(silent=True): else: verbosity = 1 name = list(parameters.keys())[0] - sim_ws = os.path.join(ws, name) + sim_ws = os.path.join(workspace, name) sim = flopy.mf6.MFSimulation.load( sim_name=sim_name, sim_ws=sim_ws, verbosity_level=verbosity ) gwf = sim.get_model(sim_name) - fs = USGSFigure(figure_type="map", verbose=False) - fig = plt.figure( - figsize=(4, 4.3), - tight_layout=True, - ) - plt.axis("off") - - nrows, ncols = 10, 1 - axes = [fig.add_subplot(nrows, ncols, (1, 8))] - - for idx, ax in enumerate(axes): - ax.set_xlim(extents[:2]) - ax.set_ylim(extents[2:]) - ax.set_aspect("equal") - - # legend axis - axes.append(fig.add_subplot(nrows, ncols, (9, 10))) - - # set limits for legend area - ax = axes[-1] - ax.set_xlim(0, 1) - ax.set_ylim(0, 1) - - # get rid of ticks and spines for legend area - ax.axis("off") - ax.set_xticks([]) - ax.set_yticks([]) - ax.spines["top"].set_color("none") - ax.spines["bottom"].set_color("none") - ax.spines["left"].set_color("none") - ax.spines["right"].set_color("none") - ax.patch.set_alpha(0.0) - - ax = axes[0] - mm = flopy.plot.PlotMapView(gwf, ax=ax, extent=extents) - mm.plot_bc("MAW", color="red") - mm.plot_inactive(color_noflow="black") - ax.set_xticks([0, extents[1] / 2, extents[1]]) - ax.set_yticks([0, extents[1] / 2, extents[1]]) - - ax = axes[-1] - ax.plot( - -10000, - -10000, - lw=0, - marker="s", - ms=10, - mfc="black", - mec="black", - markeredgewidth=0.5, - label="Inactive cells", - ) - ax.plot( - -10000, - -10000, - lw=0, - marker="s", - ms=10, - mfc="red", - mec="red", - markeredgewidth=0.5, - label="Multi-aquifer well", - ) - fs.graph_legend(ax, loc="lower center", ncol=2) - - # save figure - if config.plotSave: - fpth = os.path.join( - "..", - "figures", - f"{sim_name}-grid{config.figure_ext}", + with styles.USGSMap() as fs: + fig = plt.figure( + figsize=(4, 4.3), + tight_layout=True, ) - fig.savefig(fpth) - + plt.axis("off") + + nrows, ncols = 10, 1 + axes = [fig.add_subplot(nrows, ncols, (1, 8))] + + for idx, ax in enumerate(axes): + ax.set_xlim(extents[:2]) + ax.set_ylim(extents[2:]) + ax.set_aspect("equal") + + # legend axis + axes.append(fig.add_subplot(nrows, ncols, (9, 10))) + + # set limits for legend area + ax = axes[-1] + ax.set_xlim(0, 1) + ax.set_ylim(0, 1) + + # get rid of ticks and spines for legend area + ax.axis("off") + ax.set_xticks([]) + ax.set_yticks([]) + ax.spines["top"].set_color("none") + ax.spines["bottom"].set_color("none") + ax.spines["left"].set_color("none") + ax.spines["right"].set_color("none") + ax.patch.set_alpha(0.0) + + ax = axes[0] + mm = flopy.plot.PlotMapView(gwf, ax=ax, extent=extents) + mm.plot_bc("MAW", color="red") + mm.plot_inactive(color_noflow="black") + ax.set_xticks([0, extents[1] / 2, extents[1]]) + ax.set_yticks([0, extents[1] / 2, extents[1]]) + + ax = axes[-1] + ax.plot( + -10000, + -10000, + lw=0, + marker="s", + ms=10, + mfc="black", + mec="black", + markeredgewidth=0.5, + label="Inactive cells", + ) + ax.plot( + -10000, + -10000, + lw=0, + marker="s", + ms=10, + mfc="red", + mec="red", + markeredgewidth=0.5, + label="Multi-aquifer well", + ) + styles.graph_legend(ax, loc="lower center", ncol=2) -# Function to plot the Neville-Tonkin MAW Problem model results. + if plot_show: + plt.show() + if plot_save: + fpth = os.path.join( + "..", + "figures", + f"{sim_name}-grid.png", + ) + fig.savefig(fpth) def plot_results(silent=True): - if config.plotModel: - plot_grid(silent=silent) - plot_maw_results(silent=silent) + if not plot: + return + plot_grid(silent=silent) + plot_maw_results(silent=silent) -# Function that wraps all of the steps for the Neville-Tonkin MAW Problem model -# -# 1. build_model, -# 2. write_model, -# 3. run_model, and -# 4. plot_results. +# - + +# ### Running the example # +# Define and invoke a function to run the example scenario, then plot results. -def simulation(idx=0, silent=True): +# + +def scenario(idx=0, silent=True): key = list(parameters.keys())[idx] params = parameters[key].copy() + sim = build_models(key, **params) + if write: + write_models(sim, silent=silent) + if run: + run_models(sim, silent=silent) - sim = build_model(key, **params) - - write_model(sim, silent=silent) - - success = run_model(sim, silent=silent) - assert success, f"could not run...{sim_name}" - - -# nosetest - exclude block from this nosetest to the next nosetest -def test_01(): - simulation(idx=0, silent=False) - - -def test_02(): - simulation(idx=1, silent=False) - - -def test_plot(): - plot_results() +# - -# nosetest end -if __name__ == "__main__": - # ### Neville-Tonkin MAW Problem Simulation - # - # No pumping case +# Run the no pumping case. - simulation(0) +scenario(0) - # Pumping case +# Run the pumping case. - simulation(1) +scenario(1) - # Plot the results +# Plot the results. +if plot: plot_results() diff --git a/scripts/ex-gwf-maw-p02.py b/scripts/ex-gwf-maw-p02.py index 5f6d298d5..3575fe8a1 100644 --- a/scripts/ex-gwf-maw-p02.py +++ b/scripts/ex-gwf-maw-p02.py @@ -2,49 +2,44 @@ # # This is a modified version of the Neville-Tonkin Multi-Aquifer Well problem # from Neville and Tonkin, 2004 that uses the flowing well option. -# -# ### Flowing Well Problem Setup +# ### Initial setup # -# Imports +# Import dependencies, define the example name and workspace, and read settings from environment variables. +# + import os -import sys +import pathlib as pl import flopy import matplotlib as mpl import matplotlib.pyplot as plt import numpy as np +from flopy.plot.styles import styles +from modflow_devtools.misc import get_env, timed -# Append to system path to include the common subdirectory - -sys.path.append(os.path.join("..", "common")) - -# import common functionality - -import config -from figspecs import USGSFigure - -# Set figure properties specific to the - -figure_size = (6.3, 4.3) -masked_values = (0, 1e30, -1e30) - -# Base simulation and model name and workspace - -ws = config.base_ws +# Example name and base workspace +sim_name = "ex-gwf-maw-p02" +workspace = pl.Path("../examples") -# Simulation name +# Settings from environment variables +write = get_env("WRITE", True) +run = get_env("RUN", True) +plot = get_env("PLOT", True) +plot_show = get_env("PLOT_SHOW", True) +plot_save = get_env("PLOT_SAVE", True) +# - -sim_name = "ex-gwf-maw-p02" +# ### Define parameters +# +# Define model units, parameters and other settings. +# + # Model units - length_units = "meters" time_units = "days" -# Table Flowing Well Problem Model Parameters - +# Model parameters nper = 1 # Number of periods nlay = 2 # Number of layers nrow = 101 # Number of rows @@ -61,22 +56,18 @@ maw_rate = 0.0 # Well pumping rate ($m^{3}/d$) # parse parameter strings into tuples - botm = [float(value) for value in botm_str.split(",")] strt = [float(value) for value in strt_str.split(",")] # Static temporal data used by TDIS file - tdis_ds = ((2.314815, 50, 1.2),) # Define dimensions - extents = (0.0, delr * ncol, 0.0, delc * nrow) shape2d = (nrow, ncol) shape3d = (nlay, nrow, ncol) # create idomain - idomain = np.ones(shape3d, dtype=float) xw, yw = (ncol / 2) * delr, (nrow / 2) * delc y = 0.0 @@ -89,361 +80,325 @@ if r > 7163.0: idomain[:, i, j] = 0 -# ### Create Flowing Well Problem Model Boundary Conditions - -# MAW Package - +# MAW Package boundary conditions maw_row = int(nrow / 2) maw_col = int(ncol / 2) - maw_packagedata = [[0, maw_radius, botm[-1], strt[-1], "SPECIFIED", 2]] - maw_conn = [ [0, 0, 0, maw_row, maw_col, top, botm[-1], 111.3763, -999.0], [0, 1, 1, maw_row, maw_col, top, botm[-1], 445.9849, -999.0], ] - maw_spd = [[0, "rate", maw_rate], [0, "flowing_well", 0.0, 7500.0, 0.5]] # Solver parameters - nouter = 500 ninner = 100 hclose = 1e-9 rclose = 1e-4 +# - - -# ### Functions to build, write, run, and plot the MODFLOW 6 Flowing Well Problem model +# ### Model setup # -# MODFLOW 6 flopy simulation object (sim) is returned if building the model - +# Define functions to build models, write input files, and run the simulation. + + +# + +def build_models(): + sim_ws = os.path.join(workspace, sim_name) + sim = flopy.mf6.MFSimulation(sim_name=sim_name, sim_ws=sim_ws, exe_name="mf6") + flopy.mf6.ModflowTdis(sim, nper=nper, perioddata=tdis_ds, time_units=time_units) + flopy.mf6.ModflowIms( + sim, + print_option="summary", + outer_maximum=nouter, + outer_dvclose=hclose, + inner_maximum=ninner, + inner_dvclose=hclose, + rcloserecord=f"{rclose} strict", + ) + gwf = flopy.mf6.ModflowGwf(sim, modelname=sim_name, save_flows=True) + flopy.mf6.ModflowGwfdis( + gwf, + length_units=length_units, + nlay=nlay, + nrow=nrow, + ncol=ncol, + delr=delr, + delc=delc, + top=top, + botm=botm, + idomain=idomain, + ) + flopy.mf6.ModflowGwfnpf( + gwf, + icelltype=0, + k=k11, + k33=k33, + save_specific_discharge=True, + ) + flopy.mf6.ModflowGwfsto( + gwf, + iconvert=0, + ss=ss, + ) + flopy.mf6.ModflowGwfic(gwf, strt=strt) + + maw = flopy.mf6.ModflowGwfmaw( + gwf, + flowing_wells=True, + nmawwells=1, + packagedata=maw_packagedata, + connectiondata=maw_conn, + perioddata=maw_spd, + ) + obs_file = f"{sim_name}.maw.obs" + csv_file = obs_file + ".csv" + obs_dict = { + csv_file: [ + ("head", "head", (0,)), + ("Q1", "maw", (0,), (0,)), + ("Q2", "maw", (0,), (1,)), + ("FW", "fw-rate", (0,)), + ] + } + maw.obs.initialize( + filename=obs_file, digits=10, print_input=True, continuous=obs_dict + ) -def build_model(): - if config.buildModel: - sim_ws = os.path.join(ws, sim_name) - sim = flopy.mf6.MFSimulation( - sim_name=sim_name, sim_ws=sim_ws, exe_name="mf6" - ) - flopy.mf6.ModflowTdis( - sim, nper=nper, perioddata=tdis_ds, time_units=time_units - ) - flopy.mf6.ModflowIms( - sim, - print_option="summary", - outer_maximum=nouter, - outer_dvclose=hclose, - inner_maximum=ninner, - inner_dvclose=hclose, - rcloserecord=f"{rclose} strict", - ) - gwf = flopy.mf6.ModflowGwf(sim, modelname=sim_name, save_flows=True) - flopy.mf6.ModflowGwfdis( - gwf, - length_units=length_units, - nlay=nlay, - nrow=nrow, - ncol=ncol, - delr=delr, - delc=delc, - top=top, - botm=botm, - idomain=idomain, - ) - flopy.mf6.ModflowGwfnpf( - gwf, - icelltype=0, - k=k11, - k33=k33, - save_specific_discharge=True, - ) - flopy.mf6.ModflowGwfsto( - gwf, - iconvert=0, - ss=ss, - ) - flopy.mf6.ModflowGwfic(gwf, strt=strt) - - maw = flopy.mf6.ModflowGwfmaw( - gwf, - flowing_wells=True, - nmawwells=1, - packagedata=maw_packagedata, - connectiondata=maw_conn, - perioddata=maw_spd, - ) - obs_file = f"{sim_name}.maw.obs" - csv_file = obs_file + ".csv" - obs_dict = { - csv_file: [ - ("head", "head", (0,)), - ("Q1", "maw", (0,), (0,)), - ("Q2", "maw", (0,), (1,)), - ("FW", "fw-rate", (0,)), - ] - } - maw.obs.initialize( - filename=obs_file, digits=10, print_input=True, continuous=obs_dict - ) + flopy.mf6.ModflowGwfoc( + gwf, + printrecord=[("BUDGET", "LAST")], + ) + return sim - flopy.mf6.ModflowGwfoc( - gwf, - printrecord=[("BUDGET", "LAST")], - ) - return sim - return None +def write_models(sim, silent=True): + sim.write_simulation(silent=silent) -# Function to write MODFLOW 6 Flowing Well Problem model files +@timed +def run_models(sim, silent=True): + success, buff = sim.run_simulation(silent=silent) + assert success, buff -def write_model(sim, silent=True): - if config.writeModel: - sim.write_simulation(silent=silent) +# - -# Function to run the Flowing Well Problem model. -# True is returned if the model runs successfully +# ### Plotting results # +# Define functions to plot model results. - -@config.timeit -def run_model(sim, silent=True): - success = True - if config.runModel: - success, buff = sim.run_simulation(silent=silent) - if not success: - print(buff) - return success - - -# Function to plot the lake results +# + +# Set figure properties specific to the +figure_size = (6.3, 4.3) +masked_values = (0, 1e30, -1e30) def plot_maw_results(silent=True): - fs = USGSFigure(figure_type="graph", verbose=False) - - # load the observations - fpth = os.path.join(ws, sim_name, f"{sim_name}.maw.obs.csv") - maw = flopy.utils.Mf6Obs(fpth).data - - time = maw["totim"] * 86400.0 - - tmin = time[0] - tmax = time[-1] - - # create the figure - fig, axes = plt.subplots( - ncols=1, - nrows=2, - sharex=True, - figsize=figure_size, - constrained_layout=True, - ) - - ax = axes[0] - ax.set_xlim(tmin, tmax) - ax.set_ylim(0, 4500) - ax.semilogx( - time, - maw["Q1"], - lw=0.75, - ls="-", - color="blue", - label="Upper aquifer", - ) - ax.semilogx( - time, - maw["Q2"], - lw=0.75, - ls="-", - color="red", - label="Lower aquifer", - ) - ax.axhline(0, lw=0.5, color="0.5") - ax.set_ylabel(" ") - fs.heading(ax, idx=0) - # fs.graph_legend(ax, loc="upper right", ncol=2) - - ax = axes[1] - ax.set_xlim(tmin, tmax) - ax.set_ylim(-4500, 0) - ax.axhline( - 10.0, - lw=0.75, - ls="-", - color="blue", - label="Upper aquifer", - ) - ax.axhline( - 10.0, - lw=0.75, - ls="-", - color="red", - label="Lower aquifer", - ) - ax.semilogx( - time, - maw["FW"], - lw=0.75, - ls="-", - color="black", - label="Flowing well discharge", - ) - ax.set_xlabel(" ") - ax.set_ylabel(" ") - for axis in (ax.xaxis,): - axis.set_major_formatter(mpl.ticker.ScalarFormatter()) - fs.heading(ax, idx=1) - fs.graph_legend(ax, loc="upper left", ncol=1) - - # add y-axis label that spans both subplots - ax = fig.add_subplot(1, 1, 1) - ax.set_xlim(0, 1) - ax.set_ylim(0, 1) - - # get rid of ticks and spines for legend area - # ax.axis("off") - ax.set_xticks([]) - ax.set_yticks([]) - ax.spines["top"].set_color("none") - ax.spines["bottom"].set_color("none") - ax.spines["left"].set_color("none") - ax.spines["right"].set_color("none") - ax.patch.set_alpha(0.0) - - ax.set_xlabel("Simulation time, in seconds") - ax.set_ylabel("Discharge rate, in cubic meters per day") - - # save figure - if config.plotSave: - fpth = os.path.join( - "..", - "figures", - f"{sim_name}-01{config.figure_ext}", + with styles.USGSPlot(): + # load the observations + fpth = os.path.join(workspace, sim_name, f"{sim_name}.maw.obs.csv") + maw = flopy.utils.Mf6Obs(fpth).data + + time = maw["totim"] * 86400.0 + + tmin = time[0] + tmax = time[-1] + + # create the figure + fig, axes = plt.subplots( + ncols=1, + nrows=2, + sharex=True, + figsize=figure_size, + constrained_layout=True, ) - fig.savefig(fpth) - - return - -# Plot the grid + ax = axes[0] + ax.set_xlim(tmin, tmax) + ax.set_ylim(0, 4500) + ax.semilogx( + time, + maw["Q1"], + lw=0.75, + ls="-", + color="blue", + label="Upper aquifer", + ) + ax.semilogx( + time, + maw["Q2"], + lw=0.75, + ls="-", + color="red", + label="Lower aquifer", + ) + ax.axhline(0, lw=0.5, color="0.5") + ax.set_ylabel(" ") + styles.heading(ax, idx=0) + # styles.graph_legend(ax, loc="upper right", ncol=2) + + ax = axes[1] + ax.set_xlim(tmin, tmax) + ax.set_ylim(-4500, 0) + ax.axhline( + 10.0, + lw=0.75, + ls="-", + color="blue", + label="Upper aquifer", + ) + ax.axhline( + 10.0, + lw=0.75, + ls="-", + color="red", + label="Lower aquifer", + ) + ax.semilogx( + time, + maw["FW"], + lw=0.75, + ls="-", + color="black", + label="Flowing well discharge", + ) + ax.set_xlabel(" ") + ax.set_ylabel(" ") + for axis in (ax.xaxis,): + axis.set_major_formatter(mpl.ticker.ScalarFormatter()) + styles.heading(ax, idx=1) + styles.graph_legend(ax, loc="upper left", ncol=1) + + # add y-axis label that spans both subplots + ax = fig.add_subplot(1, 1, 1) + ax.set_xlim(0, 1) + ax.set_ylim(0, 1) + + # get rid of ticks and spines for legend area + # ax.axis("off") + ax.set_xticks([]) + ax.set_yticks([]) + ax.spines["top"].set_color("none") + ax.spines["bottom"].set_color("none") + ax.spines["left"].set_color("none") + ax.spines["right"].set_color("none") + ax.patch.set_alpha(0.0) + + ax.set_xlabel("Simulation time, in seconds") + ax.set_ylabel("Discharge rate, in cubic meters per day") + + if plot_show: + plt.show() + if plot_save: + fpth = os.path.join( + "..", + "figures", + f"{sim_name}-01.png", + ) + fig.savefig(fpth) def plot_grid(sim, silent=True): gwf = sim.get_model(sim_name) - fs = USGSFigure(figure_type="map", verbose=False) - fig = plt.figure( - figsize=(4, 4.3), - tight_layout=True, - ) - plt.axis("off") - - nrows, ncols = 10, 1 - axes = [fig.add_subplot(nrows, ncols, (1, 8))] - - for idx, ax in enumerate(axes): - ax.set_xlim(extents[:2]) - ax.set_ylim(extents[2:]) - ax.set_aspect("equal") - - # legend axis - axes.append(fig.add_subplot(nrows, ncols, (9, 10))) - - # set limits for legend area - ax = axes[-1] - ax.set_xlim(0, 1) - ax.set_ylim(0, 1) - - # get rid of ticks and spines for legend area - ax.axis("off") - ax.set_xticks([]) - ax.set_yticks([]) - ax.spines["top"].set_color("none") - ax.spines["bottom"].set_color("none") - ax.spines["left"].set_color("none") - ax.spines["right"].set_color("none") - ax.patch.set_alpha(0.0) - - ax = axes[0] - mm = flopy.plot.PlotMapView(gwf, ax=ax, extent=extents) - mm.plot_bc("MAW", color="red") - mm.plot_inactive(color_noflow="black") - ax.set_xticks([0, extents[1] / 2, extents[1]]) - ax.set_yticks([0, extents[1] / 2, extents[1]]) - - ax = axes[-1] - ax.plot( - -10000, - -10000, - lw=0, - marker="s", - ms=10, - mfc="black", - mec="black", - markeredgewidth=0.5, - label="Inactive cells", - ) - ax.plot( - -10000, - -10000, - lw=0, - marker="s", - ms=10, - mfc="red", - mec="red", - markeredgewidth=0.5, - label="Multi-aquifer well", - ) - fs.graph_legend(ax, loc="lower center", ncol=2) - - # save figure - if config.plotSave: - fpth = os.path.join( - "..", - "figures", - f"{sim_name}-grid{config.figure_ext}", + with styles.USGSMap(): + fig = plt.figure( + figsize=(4, 4.3), + tight_layout=True, ) - fig.savefig(fpth) - + plt.axis("off") + + nrows, ncols = 10, 1 + axes = [fig.add_subplot(nrows, ncols, (1, 8))] + + for idx, ax in enumerate(axes): + ax.set_xlim(extents[:2]) + ax.set_ylim(extents[2:]) + ax.set_aspect("equal") + + # legend axis + axes.append(fig.add_subplot(nrows, ncols, (9, 10))) + + # set limits for legend area + ax = axes[-1] + ax.set_xlim(0, 1) + ax.set_ylim(0, 1) + + # get rid of ticks and spines for legend area + ax.axis("off") + ax.set_xticks([]) + ax.set_yticks([]) + ax.spines["top"].set_color("none") + ax.spines["bottom"].set_color("none") + ax.spines["left"].set_color("none") + ax.spines["right"].set_color("none") + ax.patch.set_alpha(0.0) + + ax = axes[0] + mm = flopy.plot.PlotMapView(gwf, ax=ax, extent=extents) + mm.plot_bc("MAW", color="red") + mm.plot_inactive(color_noflow="black") + ax.set_xticks([0, extents[1] / 2, extents[1]]) + ax.set_yticks([0, extents[1] / 2, extents[1]]) + + ax = axes[-1] + ax.plot( + -10000, + -10000, + lw=0, + marker="s", + ms=10, + mfc="black", + mec="black", + markeredgewidth=0.5, + label="Inactive cells", + ) + ax.plot( + -10000, + -10000, + lw=0, + marker="s", + ms=10, + mfc="red", + mec="red", + markeredgewidth=0.5, + label="Multi-aquifer well", + ) + styles.graph_legend(ax, loc="lower center", ncol=2) -# Function to plot the Flowing Well Problem model results. + if plot_show: + plt.show() + if plot_save: + fpth = os.path.join( + "..", + "figures", + f"{sim_name}-grid.png", + ) + fig.savefig(fpth) def plot_results(sim, silent=True): - if config.plotModel: - plot_grid(sim, silent=silent) - plot_maw_results(silent=silent) - - -# Function that wraps all of the steps for the Flowing Well Problem model -# -# 1. build_model, -# 2. write_model, -# 3. run_model, and -# 4. plot_results. -# + plot_grid(sim, silent=silent) + plot_maw_results(silent=silent) -def simulation(silent=True): - sim = build_model() +# - - write_model(sim, silent=silent) +# ### Running the example +# +# Define and invoke a function to run the example scenario, then plot results. - success = run_model(sim, silent=silent) - assert success, f"could not run...{sim_name}" - if success: +# + +def scenario(silent=True): + sim = build_models() + if write: + write_models(sim, silent=silent) + if run: + run_models(sim, silent=silent) + if plot: plot_results(sim, silent=silent) -# nosetest - exclude block from this nosetest to the next nosetest -def test_01(): - simulation(silent=False) - - -# nosetest end - -if __name__ == "__main__": - # ### Flowing Well Problem Simulation - - simulation() +scenario() +# - diff --git a/scripts/ex-gwf-maw-p03.py b/scripts/ex-gwf-maw-p03.py index 33f2bb01b..6cf5f0051 100644 --- a/scripts/ex-gwf-maw-p03.py +++ b/scripts/ex-gwf-maw-p03.py @@ -1,51 +1,43 @@ -# ## Reilly Multi-Aquifer Well Problem, -# -# This is the Multi-Aquifer Well from Reilly and others (1989). +# ## Reilly multi-aquifer well example # +# This is the multi-aquifer well example from Reilly and others (1989). -# ### Reilly MAW Problem Setup +# ### Initial setup # -# Imports +# Import dependencies, define the example name and workspace, and read settings from environment variables. +# + import os -import sys +import pathlib as pl import flopy import matplotlib.pyplot as plt import numpy as np +from flopy.plot.styles import styles +from modflow_devtools.misc import get_env, timed -# Append to system path to include the common subdirectory - -sys.path.append(os.path.join("..", "common")) - -# import common functionality - -import config -from figspecs import USGSFigure - -# Set figure properties specific to the - -figure_size = (6.3, 4.3) -masked_values = (0, 1e30, -1e30) -arrow_props = dict( - facecolor="black", arrowstyle="-", lw=0.25, shrinkA=0.1, shrinkB=0.1 -) - -# Base simulation and model name and workspace - -ws = config.base_ws +# Example name and base workspace +sim_name = "ex-gwf-maw-p03" +workspace = pl.Path("../examples") -# Simulation name +# Settings from environment variables +write = get_env("WRITE", True) +run = get_env("RUN", True) +plot = get_env("PLOT", True) +plot_show = get_env("PLOT_SHOW", True) +plot_save = get_env("PLOT_SAVE", True) +# - -sim_name = "ex-gwf-maw-p03" +# ### Define parameters +# +# Define model units, parameters and other settings. +# + # Model units - length_units = "feet" time_units = "days" -# Scenario parameters - +# Scenario-specific parameters parameters = { "ex-gwf-maw-p03a": { "simulation": "regional", @@ -60,16 +52,13 @@ # function to calculate the well connection conductance - - def calc_cond(area, l1, l2, k1, k2): c1 = area * k1 / l1 c2 = area * k2 / l2 return c1 * c2 / (c1 + c2) -# Table Reilly MAW Problem Model Parameters - +# Model parameters nper = 1 # Number of periods nlay_r = 21 # Number of layers (regional) nrow_r = 1 # Number of rows (regional) @@ -93,7 +82,6 @@ def calc_cond(area, l1, l2, k1, k2): maw_highK = 1e9 # Hydraulic conductivity for well ($ft/d$) # set delr and delc for the local model - delr = [ 10.0, 10.0, @@ -123,7 +111,6 @@ def calc_cond(area, l1, l2, k1, k2): 10.0, 10.0, ] - delc = [ 10, 9.38, @@ -143,25 +130,19 @@ def calc_cond(area, l1, l2, k1, k2): 0.1665, ] -# Static temporal data used by TDIS file - +# Time discretization tdis_ds = ((1.0, 1, 1.0),) # Define dimensions - extents = (0.0, np.array(delr).sum(), 0.0, np.array(delc).sum()) shape2d = (nrow, ncol) shape3d = (nlay, nrow, ncol) -# ### Create Reilly MAW Problem Model Boundary Conditions - -# MAW Package - +# MAW Package boundary conditions nconn = 2 + 3 * (maw_lay[1] - maw_lay[0] + 1) maw_packagedata = [[0, maw_radius, maw_bot, strt, "SPECIFIED", nconn]] # Build the MAW connection data - i, j = maw_loc obs_elev = {} maw_conn = [] @@ -240,36 +221,29 @@ def calc_cond(area, l1, l2, k1, k2): iconn += 1 # Solver parameters - nouter = 500 ninner = 100 hclose = 1e-9 rclose = 1e-4 +# - - -# ### Functions to build, write, run, and plot the MODFLOW 6 Reilly MAW Problem model +# ### Model setup # -# MODFLOW 6 flopy simulation object (sim) is returned if building the model +# Define functions to build models, write input files, and run the simulation. -def build_model(name, simulation="regional"): - if config.buildModel: - if simulation == "regional": - sim = build_regional(name) - else: - sim = build_local(name, simulation) - - return sim +# + +def build_models(name, simulation="regional"): + if simulation == "regional": + return build_regional(name) + else: + return build_local(name, simulation) def build_regional(name): - sim_ws = os.path.join(ws, name) - sim = flopy.mf6.MFSimulation( - sim_name=sim_name, sim_ws=sim_ws, exe_name="mf6" - ) - flopy.mf6.ModflowTdis( - sim, nper=nper, perioddata=tdis_ds, time_units=time_units - ) + sim_ws = os.path.join(workspace, name) + sim = flopy.mf6.MFSimulation(sim_name=sim_name, sim_ws=sim_ws, exe_name="mf6") + flopy.mf6.ModflowTdis(sim, nper=nper, perioddata=tdis_ds, time_units=time_units) flopy.mf6.ModflowIms( sim, print_option="summary", @@ -317,7 +291,7 @@ def build_regional(name): def build_local(name, simulation): # get regional heads for constant head boundaries pth = list(parameters.keys())[0] - fpth = os.path.join(ws, pth, f"{sim_name}.hds") + fpth = os.path.join(workspace, pth, f"{sim_name}.hds") try: h = flopy.utils.HeadFile(fpth).get_data() except: @@ -343,13 +317,9 @@ def build_local(name, simulation): chd_spd.append([k, il, 0, hi1]) chd_spd.append([k, il, ncol - 1, hi2]) - sim_ws = os.path.join(ws, name) - sim = flopy.mf6.MFSimulation( - sim_name=sim_name, sim_ws=sim_ws, exe_name="mf6" - ) - flopy.mf6.ModflowTdis( - sim, nper=nper, perioddata=tdis_ds, time_units=time_units - ) + sim_ws = os.path.join(workspace, name) + sim = flopy.mf6.MFSimulation(sim_name=sim_name, sim_ws=sim_ws, exe_name="mf6") + flopy.mf6.ModflowTdis(sim, nper=nper, perioddata=tdis_ds, time_units=time_units) flopy.mf6.ModflowIms( sim, print_option="summary", @@ -436,157 +406,149 @@ def build_local(name, simulation): return sim -# Function to write MODFLOW 6 Reilly MAW Problem model files - +def write_models(sim, silent=True): + sim.write_simulation(silent=silent) -def write_model(sim, silent=True): - if config.writeModel: - sim.write_simulation(silent=silent) - -# Function to run the Reilly MAW Problem model. -# True is returned if the model runs successfully -# +@timed +def run_models(sim, silent=True): + success, buff = sim.run_simulation(silent=silent) + assert success, buff -@config.timeit -def run_model(sim, silent=True): - success = True - if config.runModel: - success, buff = sim.run_simulation(silent=silent) - if not success: - print(buff) - return success +# - +# ### Plotting results +# +# Define functions to plot model results. -# Function to plot the lake results +# + +# Figure properties +figure_size = (6.3, 4.3) +masked_values = (0, 1e30, -1e30) +arrow_props = dict(facecolor="black", arrowstyle="-", lw=0.25, shrinkA=0.1, shrinkB=0.1) def plot_maw_results(silent=True): - fs = USGSFigure(figure_type="graph", verbose=False) - - # load the observations - name = list(parameters.keys())[1] - fpth = os.path.join(ws, name, f"{sim_name}.maw.obs.csv") - maw = flopy.utils.Mf6Obs(fpth).data - name = list(parameters.keys())[2] - fpth = os.path.join(ws, name, f"{sim_name}.gwf.obs.csv") - gwf = flopy.utils.Mf6Obs(fpth).data - - # process heads - hgwf = 0.0 - ihds = 0.0 - for name in gwf.dtype.names: - if name.startswith("H0_"): - hgwf += gwf[name] - ihds += 1.0 - hgwf /= ihds - - if silent: - print("MAW head: {} Average head: {}".format(maw["H0"], hgwf)) - - zelev = sorted(list(set(list(obs_elev.values()))), reverse=True) - - results = { - "maw": {}, - "gwf": {}, - } - for z in zelev: - results["maw"][z] = 0.0 - results["gwf"][z] = 0.0 - - for name in maw.dtype.names: - if name.startswith("Q"): - z = obs_elev[name] - results["maw"][z] += 2.0 * maw[name] - - for name in gwf.dtype.names: - if name.startswith("Q"): - z = obs_elev[name] - results["gwf"][z] += 2.0 * gwf[name] - - q0 = np.array(list(results["maw"].values())) - q1 = np.array(list(results["gwf"].values())) - mean_error = np.mean(q0 - q1) - if silent: - print(f"total well inflow: {q0[q0 >= 0].sum()}") - print(f"total well outflow: {q0[q0 < 0].sum()}") - print(f"total cell inflow: {q1[q1 >= 0].sum()}") - print(f"total cell outflow: {q1[q1 < 0].sum()}") - - # create the figure - fig, ax = plt.subplots( - ncols=1, - nrows=1, - sharex=True, - figsize=(4, 4), - constrained_layout=True, - ) - - ax.set_xlim(-3.5, 3.5) - ax.set_ylim(-67.5, -2.5) - ax.axvline(0, lw=0.5, ls=":", color="0.5") - for z in np.arange(-5, -70, -5): - ax.axhline(z, lw=0.5, color="0.5") - ax.plot( - results["maw"].values(), - zelev, - lw=0.75, - ls="-", - color="blue", - label="Multi-aquifer well", - ) - ax.plot( - results["gwf"].values(), - zelev, - marker="o", - ms=4, - mfc="red", - mec="black", - markeredgewidth=0.5, - lw=0.0, - ls="-", - color="red", - label="High K well", - ) - ax.plot( - -1000, - -1000, - lw=0.5, - ls="-", - color="0.5", - label="Grid cell", - ) - - fs.graph_legend(ax, loc="upper left", ncol=1, frameon=True) - fs.add_text( - ax, - f"Mean Error {mean_error:.2e} cubic feet per day", - bold=False, - italic=False, - x=1.0, - y=1.01, - va="bottom", - ha="right", - fontsize=7, - ) - - ax.set_xlabel("Discharge rate, in cubic feet per day") - ax.set_ylabel("Elevation, in feet") + with styles.USGSPlot(): + # load the observations + name = list(parameters.keys())[1] + fpth = os.path.join(workspace, name, f"{sim_name}.maw.obs.csv") + maw = flopy.utils.Mf6Obs(fpth).data + name = list(parameters.keys())[2] + fpth = os.path.join(workspace, name, f"{sim_name}.gwf.obs.csv") + gwf = flopy.utils.Mf6Obs(fpth).data + + # process heads + hgwf = 0.0 + ihds = 0.0 + for name in gwf.dtype.names: + if name.startswith("H0_"): + hgwf += gwf[name] + ihds += 1.0 + hgwf /= ihds + + if silent: + print("MAW head: {} Average head: {}".format(maw["H0"], hgwf)) + + zelev = sorted(list(set(list(obs_elev.values()))), reverse=True) + + results = { + "maw": {}, + "gwf": {}, + } + for z in zelev: + results["maw"][z] = 0.0 + results["gwf"][z] = 0.0 + + for name in maw.dtype.names: + if name.startswith("Q"): + z = obs_elev[name] + results["maw"][z] += 2.0 * maw[name] + + for name in gwf.dtype.names: + if name.startswith("Q"): + z = obs_elev[name] + results["gwf"][z] += 2.0 * gwf[name] + + q0 = np.array(list(results["maw"].values())) + q1 = np.array(list(results["gwf"].values())) + mean_error = np.mean(q0 - q1) + if silent: + print(f"total well inflow: {q0[q0 >= 0].sum()}") + print(f"total well outflow: {q0[q0 < 0].sum()}") + print(f"total cell inflow: {q1[q1 >= 0].sum()}") + print(f"total cell outflow: {q1[q1 < 0].sum()}") + + # create the figure + fig, ax = plt.subplots( + ncols=1, + nrows=1, + sharex=True, + figsize=(4, 4), + constrained_layout=True, + ) - # save figure - if config.plotSave: - fpth = os.path.join( - "..", - "figures", - f"{sim_name}-01{config.figure_ext}", + ax.set_xlim(-3.5, 3.5) + ax.set_ylim(-67.5, -2.5) + ax.axvline(0, lw=0.5, ls=":", color="0.5") + for z in np.arange(-5, -70, -5): + ax.axhline(z, lw=0.5, color="0.5") + ax.plot( + results["maw"].values(), + zelev, + lw=0.75, + ls="-", + color="blue", + label="Multi-aquifer well", + ) + ax.plot( + results["gwf"].values(), + zelev, + marker="o", + ms=4, + mfc="red", + mec="black", + markeredgewidth=0.5, + lw=0.0, + ls="-", + color="red", + label="High K well", + ) + ax.plot( + -1000, + -1000, + lw=0.5, + ls="-", + color="0.5", + label="Grid cell", ) - fig.savefig(fpth) - return + styles.graph_legend(ax, loc="upper left", ncol=1, frameon=True) + styles.add_text( + ax, + f"Mean Error {mean_error:.2e} cubic feet per day", + bold=False, + italic=False, + x=1.0, + y=1.01, + va="bottom", + ha="right", + fontsize=7, + ) + ax.set_xlabel("Discharge rate, in cubic feet per day") + ax.set_ylabel("Elevation, in feet") -# Plot the regional grid + if plot_show: + plt.show() + if plot_save: + fpth = os.path.join( + "..", + "figures", + f"{sim_name}-01.png", + ) + fig.savefig(fpth) def plot_regional_grid(silent=True): @@ -595,7 +557,7 @@ def plot_regional_grid(silent=True): else: verbosity = 1 name = list(parameters.keys())[0] - sim_ws = os.path.join(ws, name) + sim_ws = os.path.join(workspace, name) sim = flopy.mf6.MFSimulation.load( sim_name=sim_name, sim_ws=sim_ws, verbosity_level=verbosity ) @@ -604,115 +566,113 @@ def plot_regional_grid(silent=True): # get regional heads for constant head boundaries h = gwf.output.head().get_data() - fs = USGSFigure(figure_type="map", verbose=False) - fig = plt.figure( - figsize=(6.3, 3.5), - ) - plt.axis("off") - - nrows, ncols = 10, 1 - axes = [fig.add_subplot(nrows, ncols, (1, 6))] - - # legend axis - axes.append(fig.add_subplot(nrows, ncols, (7, 10))) - - # set limits for legend area - ax = axes[-1] - ax.set_xlim(0, 1) - ax.set_ylim(0, 1) - - # get rid of ticks and spines for legend area - ax.axis("off") - ax.set_xticks([]) - ax.set_yticks([]) - ax.spines["top"].set_color("none") - ax.spines["bottom"].set_color("none") - ax.spines["left"].set_color("none") - ax.spines["right"].set_color("none") - ax.patch.set_alpha(0.0) - - ax = axes[0] - mm = flopy.plot.PlotCrossSection(gwf, ax=ax, line={"row": 0}) - ca = mm.plot_array(h, head=h) - mm.plot_bc("CHD", color="cyan", head=h) - mm.plot_grid(lw=0.5, color="0.5") - cv = mm.contour_array( - h, - levels=np.arange(0, 6, 0.5), - linewidths=0.5, - linestyles="-", - colors="black", - masked_values=masked_values, - ) - plt.clabel(cv, fmt="%1.1f") - ax.plot( - [50, 150, 150, 50, 50], - [10, 10, aq_bottom, aq_bottom, 10], - lw=1.25, - color="#39FF14", - ) - fs.remove_edge_ticks(ax) - ax.set_xlabel("x-coordinate, in feet") - ax.set_ylabel("Elevation, in feet") - - # legend - ax = axes[-1] - ax.plot( - -10000, - -10000, - lw=0, - marker="s", - ms=10, - mfc="none", - mec="0.5", - markeredgewidth=0.5, - label="Grid cell", - ) - ax.plot( - -10000, - -10000, - lw=0, - marker="s", - ms=10, - mfc="cyan", - mec="0.5", - markeredgewidth=0.5, - label="Constant head", - ) - ax.plot( - -10000, - -10000, - lw=0, - marker="s", - ms=10, - mfc="none", - mec="#39FF14", - markeredgewidth=1.25, - label="Local model domain", - ) - ax.plot( - -10000, - -10000, - lw=0.5, - color="black", - label="Head contour, $ft$", - ) - cbar = plt.colorbar(ca, shrink=0.5, orientation="horizontal", ax=ax) - cbar.ax.tick_params(size=0) - cbar.ax.set_xlabel(r"Head, $ft$", fontsize=9) - fs.graph_legend(ax, loc="lower center", ncol=4) - - # save figure - if config.plotSave: - fpth = os.path.join( - "..", - "figures", - f"{sim_name}-regional-grid{config.figure_ext}", + with styles.USGSMap() as fs: + fig = plt.figure( + figsize=(6.3, 3.5), ) - fig.savefig(fpth) - - -# Plot the local grid + plt.axis("off") + + nrows, ncols = 10, 1 + axes = [fig.add_subplot(nrows, ncols, (1, 6))] + + # legend axis + axes.append(fig.add_subplot(nrows, ncols, (7, 10))) + + # set limits for legend area + ax = axes[-1] + ax.set_xlim(0, 1) + ax.set_ylim(0, 1) + + # get rid of ticks and spines for legend area + ax.axis("off") + ax.set_xticks([]) + ax.set_yticks([]) + ax.spines["top"].set_color("none") + ax.spines["bottom"].set_color("none") + ax.spines["left"].set_color("none") + ax.spines["right"].set_color("none") + ax.patch.set_alpha(0.0) + + ax = axes[0] + mm = flopy.plot.PlotCrossSection(gwf, ax=ax, line={"row": 0}) + ca = mm.plot_array(h, head=h) + mm.plot_bc("CHD", color="cyan", head=h) + mm.plot_grid(lw=0.5, color="0.5") + cv = mm.contour_array( + h, + levels=np.arange(0, 6, 0.5), + linewidths=0.5, + linestyles="-", + colors="black", + masked_values=masked_values, + ) + plt.clabel(cv, fmt="%1.1f") + ax.plot( + [50, 150, 150, 50, 50], + [10, 10, aq_bottom, aq_bottom, 10], + lw=1.25, + color="#39FF14", + ) + styles.remove_edge_ticks(ax) + ax.set_xlabel("x-coordinate, in feet") + ax.set_ylabel("Elevation, in feet") + + # legend + ax = axes[-1] + ax.plot( + -10000, + -10000, + lw=0, + marker="s", + ms=10, + mfc="none", + mec="0.5", + markeredgewidth=0.5, + label="Grid cell", + ) + ax.plot( + -10000, + -10000, + lw=0, + marker="s", + ms=10, + mfc="cyan", + mec="0.5", + markeredgewidth=0.5, + label="Constant head", + ) + ax.plot( + -10000, + -10000, + lw=0, + marker="s", + ms=10, + mfc="none", + mec="#39FF14", + markeredgewidth=1.25, + label="Local model domain", + ) + ax.plot( + -10000, + -10000, + lw=0.5, + color="black", + label="Head contour, $ft$", + ) + cbar = plt.colorbar(ca, shrink=0.5, orientation="horizontal", ax=ax) + cbar.ax.tick_params(size=0) + cbar.ax.set_xlabel(r"Head, $ft$", fontsize=9) + styles.graph_legend(ax, loc="lower center", ncol=4) + + if plot_show: + plt.show() + if plot_save: + fpth = os.path.join( + "..", + "figures", + f"{sim_name}-regional-grid.png", + ) + fig.savefig(fpth) def plot_local_grid(silent=True): @@ -721,7 +681,7 @@ def plot_local_grid(silent=True): else: verbosity = 1 name = list(parameters.keys())[1] - sim_ws = os.path.join(ws, name) + sim_ws = os.path.join(workspace, name) sim = flopy.mf6.MFSimulation.load( sim_name=sim_name, sim_ws=sim_ws, verbosity_level=verbosity ) @@ -741,181 +701,158 @@ def plot_local_grid(silent=True): # get regional heads for constant head boundaries h = gwf.output.head().get_data() - fs = USGSFigure(figure_type="map", verbose=False) - fig = plt.figure( - figsize=(6.3, 4.1), - tight_layout=True, - ) - plt.axis("off") - - nrows, ncols = 10, 1 - axes = [fig.add_subplot(nrows, ncols, (1, 8))] - - for idx, ax in enumerate(axes): - ax.set_xlim(extents[:2]) - ax.set_ylim(extents[2:]) - ax.set_aspect("equal") - - # legend axis - axes.append(fig.add_subplot(nrows, ncols, (8, 10))) - - # set limits for legend area - ax = axes[-1] - ax.set_xlim(0, 1) - ax.set_ylim(0, 1) - - # get rid of ticks and spines for legend area - ax.axis("off") - ax.set_xticks([]) - ax.set_yticks([]) - ax.spines["top"].set_color("none") - ax.spines["bottom"].set_color("none") - ax.spines["left"].set_color("none") - ax.spines["right"].set_color("none") - ax.patch.set_alpha(0.0) - - ax = axes[0] - mm = flopy.plot.PlotMapView(gwf, ax=ax, extent=extents, layer=0) - mm.plot_bc("CHD", color="cyan", plotAll=True) - mm.plot_grid(lw=0.25, color="0.5") - cv = mm.contour_array( - h, - levels=np.arange(4.0, 5.0, 0.005), - linewidths=0.5, - linestyles="-", - colors="black", - masked_values=masked_values, - ) - plt.clabel(cv, fmt="%1.3f") - ax.fill_between( - px, py, y2=0, ec="none", fc="red", lw=0, zorder=200, step="post" - ) - fs.add_annotation( - ax, - text="Well location", - xy=(50.0, 0.0), - xytext=(55, 5), - bold=False, - italic=False, - ha="left", - fontsize=7, - arrowprops=arrow_props, - ) - fs.remove_edge_ticks(ax) - ax.set_xticks([0, 25, 50, 75, 100]) - ax.set_xlabel("x-coordinate, in feet") - ax.set_yticks([0, 25, 50]) - ax.set_ylabel("y-coordinate, in feet") - - # legend - ax = axes[-1] - ax.plot( - -10000, - -10000, - lw=0, - marker="s", - ms=10, - mfc="cyan", - mec="0.5", - markeredgewidth=0.25, - label="Constant head", - ) - ax.plot( - -10000, - -10000, - lw=0, - marker="s", - ms=10, - mfc="red", - mec="0.5", - markeredgewidth=0.25, - label="Multi-aquifer well", - ) - ax.plot( - -10000, - -10000, - lw=0.5, - color="black", - label="Water-table contour, $ft$", - ) - fs.graph_legend(ax, loc="lower center", ncol=3) - - # save figure - if config.plotSave: - fpth = os.path.join( - "..", - "figures", - f"{sim_name}-local-grid{config.figure_ext}", + with styles.USGSMap() as fs: + fig = plt.figure( + figsize=(6.3, 4.1), + tight_layout=True, ) - fig.savefig(fpth) - + plt.axis("off") + + nrows, ncols = 10, 1 + axes = [fig.add_subplot(nrows, ncols, (1, 8))] + + for idx, ax in enumerate(axes): + ax.set_xlim(extents[:2]) + ax.set_ylim(extents[2:]) + ax.set_aspect("equal") + + # legend axis + axes.append(fig.add_subplot(nrows, ncols, (8, 10))) + + # set limits for legend area + ax = axes[-1] + ax.set_xlim(0, 1) + ax.set_ylim(0, 1) + + # get rid of ticks and spines for legend area + ax.axis("off") + ax.set_xticks([]) + ax.set_yticks([]) + ax.spines["top"].set_color("none") + ax.spines["bottom"].set_color("none") + ax.spines["left"].set_color("none") + ax.spines["right"].set_color("none") + ax.patch.set_alpha(0.0) + + ax = axes[0] + mm = flopy.plot.PlotMapView(gwf, ax=ax, extent=extents, layer=0) + mm.plot_bc("CHD", color="cyan", plotAll=True) + mm.plot_grid(lw=0.25, color="0.5") + cv = mm.contour_array( + h, + levels=np.arange(4.0, 5.0, 0.005), + linewidths=0.5, + linestyles="-", + colors="black", + masked_values=masked_values, + ) + plt.clabel(cv, fmt="%1.3f") + ax.fill_between( + px, py, y2=0, ec="none", fc="red", lw=0, zorder=200, step="post" + ) + styles.add_annotation( + ax, + text="Well location", + xy=(50.0, 0.0), + xytext=(55, 5), + bold=False, + italic=False, + ha="left", + fontsize=7, + arrowprops=arrow_props, + ) + styles.remove_edge_ticks(ax) + ax.set_xticks([0, 25, 50, 75, 100]) + ax.set_xlabel("x-coordinate, in feet") + ax.set_yticks([0, 25, 50]) + ax.set_ylabel("y-coordinate, in feet") + + # legend + ax = axes[-1] + ax.plot( + -10000, + -10000, + lw=0, + marker="s", + ms=10, + mfc="cyan", + mec="0.5", + markeredgewidth=0.25, + label="Constant head", + ) + ax.plot( + -10000, + -10000, + lw=0, + marker="s", + ms=10, + mfc="red", + mec="0.5", + markeredgewidth=0.25, + label="Multi-aquifer well", + ) + ax.plot( + -10000, + -10000, + lw=0.5, + color="black", + label="Water-table contour, $ft$", + ) + styles.graph_legend(ax, loc="lower center", ncol=3) -# Function to plot the Reilly MAW Problem model results. + if plot_show: + plt.show() + if plot_save: + fpth = os.path.join( + "..", + "figures", + f"{sim_name}-local-grid.png", + ) + fig.savefig(fpth) def plot_results(silent=True): - if config.plotModel: - plot_regional_grid(silent=silent) - plot_local_grid(silent=silent) - plot_maw_results(silent=silent) - pass + if not plot: + return + plot_regional_grid(silent=silent) + plot_local_grid(silent=silent) + plot_maw_results(silent=silent) -# Function that wraps all of the steps for the Reilly MAW Problem model -# -# 1. build_model, -# 2. write_model, -# 3. run_model, and -# 4. plot_results. +# - + +# ### Running the example # +# Define and invoke a function to run the example scenario, then plot results. -def simulation(idx=0, silent=True): +# + +def scenario(idx=0, silent=True): key = list(parameters.keys())[idx] params = parameters[key].copy() + sim = build_models(key, **params) + if write: + write_models(sim, silent=silent) + if run: + run_models(sim, silent=silent) - sim = build_model(key, **params) - - write_model(sim, silent=silent) - - success = run_model(sim, silent=silent) - assert success, f"could not run...{sim_name}" - - -# nosetest - exclude block from this nosetest to the next nosetest -def test_01(): - simulation(idx=0, silent=False) - - -def test_02(): - simulation(idx=1, silent=False) - - -def test_03(): - simulation(idx=2, silent=False) - - -def test_plot(): - plot_results() +# - -# nosetest end -if __name__ == "__main__": - # ### Reilly MAW Problem Simulation - # - # Regional model +# Run the regional model. - simulation(0) +scenario(0) - # Local model with MAW well +# Run the local model with MAW well. - simulation(1) +scenario(1) - # Local model with high K well +# Run the local model with high K well. - simulation(2) +scenario(2) - # Plot the results +# Plot the results. +if plot: plot_results() diff --git a/scripts/ex-gwf-nwt-p02.py b/scripts/ex-gwf-nwt-p02.py index 105bf4403..ab0ac58bd 100644 --- a/scripts/ex-gwf-nwt-p02.py +++ b/scripts/ex-gwf-nwt-p02.py @@ -1,48 +1,46 @@ # ## Flow diversion example # -# +# This example is based on problem 2 in Niswonger et al 2011, which +# used the Newton-Raphson formulation to simulate dry cells under a +# recharge pond. The problem is also described in McDonald et al 1991 +# and used the \MF rewetting option to rewet dry cells. -# ### Flow diversion Problem Setup +# ### Initial setup # -# Imports +# Import dependencies, define the example name and workspace, and read settings from environment variables. +# + import os -import sys +import pathlib as pl import flopy -import matplotlib as mpl import matplotlib.pyplot as plt import numpy as np +from flopy.plot.styles import styles +from modflow_devtools.misc import get_env, timed -# Append to system path to include the common subdirectory - -sys.path.append(os.path.join("..", "common")) - -# import common functionality - -import config -from figspecs import USGSFigure - -# Set figure properties - -figure_size = (6.3, 6.3) -masked_values = (1e30, -1e30) - -# Base simulation and model name and workspace - -ws = config.base_ws +# Example name and base workspace +sim_name = "ex-gwf-nwt-p02" +workspace = pl.Path("../examples") -# Simulation name +# Settings from environment variables +write = get_env("WRITE", True) +run = get_env("RUN", True) +plot = get_env("PLOT", True) +plot_show = get_env("PLOT_SHOW", True) +plot_save = get_env("PLOT_SAVE", True) +# - -sim_name = "ex-gwf-nwt-p02" +# ### Define parameters +# +# Define model units, parameters and other settings. +# + # Model units - length_units = "feet" time_units = "days" -# Scenario parameters - +# Scenario-specific parameters parameters = { "ex-gwf-nwt-p02a": { "newton": "newton", @@ -56,8 +54,7 @@ }, } -# Table - +# Model parameters nper = 4 # Number of periods nlay = 14 # Number of layers nrow = 40 # Number of rows @@ -73,8 +70,7 @@ rech = 0.05 # Recharge rate ($ft/day$) -# Static temporal data used by TDIS file - +# Time discretization tdis_ds = ( (190.0, 10, 1.0), (518.0, 2, 1.0), @@ -83,20 +79,16 @@ ) # Calculate extents, and shape3d - extents = (0, delr * ncol, 20, 65) shape3d = (nlay, nrow, ncol) # Create the bottom - botm = np.arange(65.0, -5.0, -5.0) # Create icelltype (which is the same as iconvert) - icelltype = 9 * [1] + 5 * [0] # Constant head boundary conditions - chd_spd = [] for k in range(9, nlay, 1): chd_spd += [[k, i, ncol - 1, H1] for i in range(nrow - 1)] @@ -104,25 +96,25 @@ # Recharge boundary conditions - rch_spd = [] for i in range(0, 2, 1): for j in range(0, 2, 1): rch_spd.append([0, i, j, rech]) # Solver parameters - nouter = 500 ninner = 100 hclose = 1e-6 rclose = 1000.0 +# - -# ### Functions to build, write, run, and plot the model +# ### Model setup # -# MODFLOW 6 flopy simulation object (sim) is returned if building the model +# Define functions to build models, write input files, and run the simulation. -def build_model( +# + +def build_models( name, newton=False, rewet=False, @@ -131,115 +123,105 @@ def build_model( ihdwet=None, wetdry=None, ): - if config.buildModel: - sim_ws = os.path.join(ws, name) - sim = flopy.mf6.MFSimulation( - sim_name=sim_name, sim_ws=sim_ws, exe_name="mf6" - ) - flopy.mf6.ModflowTdis( - sim, nper=nper, perioddata=tdis_ds, time_units=time_units - ) - if newton: - newtonoptions = "newton" - no_ptc = "ALL" - complexity = "complex" - else: - newtonoptions = None - no_ptc = None - complexity = "simple" - - flopy.mf6.ModflowIms( - sim, - complexity=complexity, - print_option="SUMMARY", - no_ptcrecord=no_ptc, - outer_maximum=nouter, - outer_dvclose=hclose, - inner_maximum=ninner, - inner_dvclose=hclose, - rcloserecord=rclose, - ) - gwf = flopy.mf6.ModflowGwf( - sim, - modelname=sim_name, - newtonoptions=newtonoptions, - ) - flopy.mf6.ModflowGwfdis( - gwf, - length_units=length_units, - nlay=nlay, - nrow=nrow, - ncol=ncol, - delr=delr, - delc=delc, - top=top, - botm=botm, - ) - if rewet: - rewet_record = [ - "wetfct", - wetfct, - "iwetit", - iwetit, - "ihdwet", - ihdwet, - ] - wetdry = 9 * [wetdry] + 5 * [0] - else: - rewet_record = None - flopy.mf6.ModflowGwfnpf( - gwf, - rewet_record=rewet_record, - icelltype=icelltype, - k=k11, - k33=k33, - wetdry=wetdry, - ) - flopy.mf6.ModflowGwfsto( - gwf, - iconvert=icelltype, - ss=ss, - sy=sy, - steady_state={3: True}, - ) - flopy.mf6.ModflowGwfic(gwf, strt=H1) - flopy.mf6.ModflowGwfchd(gwf, stress_period_data=chd_spd) - flopy.mf6.ModflowGwfrch(gwf, stress_period_data=rch_spd) - - head_filerecord = f"{sim_name}.hds" - flopy.mf6.ModflowGwfoc( - gwf, - head_filerecord=head_filerecord, - saverecord=[("HEAD", "LAST")], - ) - return sim - return None - - -# Function to write flow diversion model files - - -def write_model(sim, silent=True): - if config.writeModel: - sim.write_simulation(silent=silent) - - -# Function to run the model. True is returned if the model runs successfully. + sim_ws = os.path.join(workspace, name) + sim = flopy.mf6.MFSimulation(sim_name=sim_name, sim_ws=sim_ws, exe_name="mf6") + flopy.mf6.ModflowTdis(sim, nper=nper, perioddata=tdis_ds, time_units=time_units) + if newton: + newtonoptions = "newton" + no_ptc = "ALL" + complexity = "complex" + else: + newtonoptions = None + no_ptc = None + complexity = "simple" + + flopy.mf6.ModflowIms( + sim, + complexity=complexity, + print_option="SUMMARY", + no_ptcrecord=no_ptc, + outer_maximum=nouter, + outer_dvclose=hclose, + inner_maximum=ninner, + inner_dvclose=hclose, + rcloserecord=rclose, + ) + gwf = flopy.mf6.ModflowGwf( + sim, + modelname=sim_name, + newtonoptions=newtonoptions, + ) + flopy.mf6.ModflowGwfdis( + gwf, + length_units=length_units, + nlay=nlay, + nrow=nrow, + ncol=ncol, + delr=delr, + delc=delc, + top=top, + botm=botm, + ) + if rewet: + rewet_record = [ + "wetfct", + wetfct, + "iwetit", + iwetit, + "ihdwet", + ihdwet, + ] + wetdry = 9 * [wetdry] + 5 * [0] + else: + rewet_record = None + flopy.mf6.ModflowGwfnpf( + gwf, + rewet_record=rewet_record, + icelltype=icelltype, + k=k11, + k33=k33, + wetdry=wetdry, + ) + flopy.mf6.ModflowGwfsto( + gwf, + iconvert=icelltype, + ss=ss, + sy=sy, + steady_state={3: True}, + ) + flopy.mf6.ModflowGwfic(gwf, strt=H1) + flopy.mf6.ModflowGwfchd(gwf, stress_period_data=chd_spd) + flopy.mf6.ModflowGwfrch(gwf, stress_period_data=rch_spd) + + head_filerecord = f"{sim_name}.hds" + flopy.mf6.ModflowGwfoc( + gwf, + head_filerecord=head_filerecord, + saverecord=[("HEAD", "LAST")], + ) + return sim + + +def write_models(sim, silent=True): + sim.write_simulation(silent=silent) + + +@timed +def run_models(sim, silent=True): + success, buff = sim.run_simulation(silent=silent) + assert success, buff + + +# - + +# ### Plotting results # +# Define functions to plot model results. - -@config.timeit -def run_model(sim, silent=True): - success = True - if config.runModel: - success, buff = sim.run_simulation(silent=silent) - if not success: - print(buff) - - return success - - -# Create a water-table array +# + +# Figure properties +figure_size = (6.3, 6.3) +masked_values = (1e30, -1e30) def get_water_table(h, bot): @@ -248,22 +230,20 @@ def get_water_table(h, bot): return np.amax(h, axis=0) -# Function to plot the model results. - - def plot_results(silent=True): + if not plot: + return + verbose = not silent if verbose: verbosity_level = 1 else: verbosity_level = 0 - if config.plotModel: - fs = USGSFigure(figure_type="map", verbose=verbose) - + with styles.USGSMap(): # load the newton model name = list(parameters.keys())[0] - sim_ws = os.path.join(ws, name) + sim_ws = os.path.join(workspace, name) sim = flopy.mf6.MFSimulation.load( sim_name=sim_name, sim_ws=sim_ws, verbosity_level=verbosity_level ) @@ -279,7 +259,7 @@ def plot_results(silent=True): # load rewet model name = list(parameters.keys())[1] - sim_ws = os.path.join(ws, name) + sim_ws = os.path.join(workspace, name) sim1 = flopy.mf6.MFSimulation.load( sim_name=sim_name, sim_ws=sim_ws, verbosity_level=verbosity_level ) @@ -358,7 +338,7 @@ def plot_results(silent=True): mfc="cyan", label="Constant head", ) - fs.graph_legend( + styles.graph_legend( ax, loc="upper right", ncol=2, @@ -367,9 +347,9 @@ def plot_results(silent=True): edgecolor="none", ) letter = chr(ord("@") + idx + 1) - fs.heading(letter=letter, ax=ax) - fs.add_text(ax, text=me_text, x=1, y=1.01, ha="right", bold=False) - fs.remove_edge_ticks(ax) + styles.heading(letter=letter, ax=ax) + styles.add_text(ax, text=me_text, x=1, y=1.01, ha="right", bold=False) + styles.remove_edge_ticks(ax) # set fake y-axis label ax.set_ylabel(" ") @@ -387,59 +367,49 @@ def plot_results(silent=True): ax.set_ylim(0, 1) ax.set_yticks([0, 1]) ax.set_ylabel("Water-table elevation above arbitrary datum, in meters") - fs.remove_edge_ticks(ax) + styles.remove_edge_ticks(ax) - # save figure - if config.plotSave: + if plot_show: + plt.show() + if plot_save: fpth = os.path.join( "..", "figures", - f"{sim_name}-01{config.figure_ext}", + f"{sim_name}-01.png", ) fig.savefig(fpth) -# Function that wraps all of the steps for the TWRI model -# -# 1. build_model, -# 2. write_model, -# 3. run_model, and -# 4. plot_results. +# - + +# ### Running the example # +# Define and invoke a function to run the example scenario, then plot results. -def simulation(idx, silent=True): +# + +def scenario(idx, silent=True): key = list(parameters.keys())[idx] params = parameters[key].copy() + sim = build_models(key, **params) + if write: + write_models(sim, silent=silent) + if run: + run_models(sim, silent=silent) - sim = build_model(key, **params) - - write_model(sim, silent=silent) - - success = run_model(sim, silent=silent) - assert success, f"could not run...{key}" - - -# nosetest - exclude block from this nosetest to the next nosetest -def test_and_plot(): - simulation(0, silent=False) - simulation(1, silent=False) - plot_results(silent=False) +# - -# nosetest end -if __name__ == "__main__": - # ### MODFLOW-NWT Problem 2 Simulation - # - # Newton-Raphson. +# Run with Newton-Raphson. - simulation(0) +scenario(0) - # Rewetting. +# Run with rewetting. - simulation(1) +scenario(1) - # Plot results +# Plot results. +if plot: plot_results() diff --git a/scripts/ex-gwf-nwt-p03.py b/scripts/ex-gwf-nwt-p03.py index 0ce6bdc8e..1e94f7113 100644 --- a/scripts/ex-gwf-nwt-p03.py +++ b/scripts/ex-gwf-nwt-p03.py @@ -1,48 +1,49 @@ # ## MODFLOW-NWT Problem 3 example # -# +# This example is based on problem 3 in Niswonder et al 2011, which used +# the Newton-Raphson formulation to simulate water levels in a rectangular, +# unconfined aquifer with a complex bottom elevation and receiving areally +# distributed recharge. This problem provides a good example of the utility +# of Newton-Raphson for solving problems with wetting and drying of cells. -# ### MODFLOW-NWT Problem 3 Setup +# ### Initial setup # -# Imports +# Import dependencies, define the example name and workspace, and read settings from environment variables. +# + import os -import sys +import pathlib as pl import flopy import matplotlib as mpl import matplotlib.pyplot as plt import numpy as np +import pooch +from flopy.plot.styles import styles +from modflow_devtools.misc import get_env, timed -# Append to system path to include the common subdirectory - -sys.path.append(os.path.join("..", "common")) - -# import common functionality - -import config -from figspecs import USGSFigure - -# Set figure properties - -figure_size = (6.3, 5.6) -masked_values = (1e30, -1e30) - -# Base simulation and model name and workspace - -ws = config.base_ws +# Example name and base workspace +sim_name = "ex-gwf-nwt-p03" +workspace = pl.Path("../examples") -# Simulation name +# Settings from environment variables +write = get_env("WRITE", True) +run = get_env("RUN", True) +plot = get_env("PLOT", True) +plot_show = get_env("PLOT_SHOW", True) +plot_save = get_env("PLOT_SAVE", True) +# - -sim_name = "ex-gwf-nwt-p03" +# ### Define parameters +# +# Define model units, parameters and other settings. +# + # Model units - length_units = "meters" time_units = "days" -# Scenario parameters - +# Scenario-specific parameters parameters = { "ex-gwf-nwt-p03a": { "recharge": "high", @@ -52,8 +53,7 @@ }, } -# Table - +# Model parameters nper = 1 # Number of periods nlay = 1 # Number of layers nrow = 80 # Number of rows @@ -65,7 +65,6 @@ H1 = 24.0 # Constant head water level ($m$) # plotting ranges and contour levels - vmin, vmax = 20, 60 smin, smax = 0, 25 bmin, bmax = 0, 90 @@ -77,32 +76,32 @@ bcolor = "black" -# Static temporal data used by TDIS file - +# Time discretization tdis_ds = ((365.0, 1, 1.0),) # Calculate extents, and shape3d - extents = (0, delr * ncol, 0, delc * nrow) shape3d = (nlay, nrow, ncol) ticklabels = np.arange(0, 10000, 2000) # Load the bottom - -fpth = os.path.join("..", "data", sim_name, "bottom.txt") +fpth = pooch.retrieve( + url=f"https://github.com/MODFLOW-USGS/modflow6-examples/raw/master/data/{sim_name}/bottom.txt", + known_hash="md5:0fd4b16db652808c7e36a5a2a25da0a2", +) botm = np.loadtxt(fpth).reshape(shape3d) # Set the starting heads - strt = botm + 20.0 # Load the high recharge rate - -fpth = os.path.join("..", "data", sim_name, "recharge_high.txt") +fpth = pooch.retrieve( + url=f"https://github.com/MODFLOW-USGS/modflow6-examples/raw/master/data/{sim_name}/recharge_high.txt", + known_hash="md5:8d8f8bb3cec22e7a0cbe6aba95da8f35", +) rch_high = np.loadtxt(fpth) # Generate the low recharge rate from the high recharge rate - rch_low = rch_high.copy() * 1e-3 # Constant head boundary conditions @@ -116,104 +115,95 @@ ] # Solver parameters - nouter = 500 ninner = 500 hclose = 1e-9 rclose = 1e-6 +# - -# ### Functions to build, write, run, and plot the model +# ### Model setup # -# MODFLOW 6 flopy simulation object (sim) is returned if building the model +# Define functions to build models, write input files, and run the simulation. -def build_model( +# + +def build_models( name, recharge="high", ): - if config.buildModel: - sim_ws = os.path.join(ws, name) - sim = flopy.mf6.MFSimulation( - sim_name=sim_name, sim_ws=sim_ws, exe_name="mf6" - ) - flopy.mf6.ModflowTdis( - sim, nper=nper, perioddata=tdis_ds, time_units=time_units - ) - flopy.mf6.ModflowIms( - sim, - print_option="all", - complexity="simple", - linear_acceleration="bicgstab", - outer_maximum=nouter, - outer_dvclose=hclose, - inner_maximum=ninner, - inner_dvclose=hclose, - rcloserecord=rclose, - ) - gwf = flopy.mf6.ModflowGwf( - sim, - modelname=sim_name, - newtonoptions="newton under_relaxation", - ) - flopy.mf6.ModflowGwfdis( - gwf, - length_units=length_units, - nlay=nlay, - nrow=nrow, - ncol=ncol, - delr=delr, - delc=delc, - top=top, - botm=botm, - ) - flopy.mf6.ModflowGwfnpf( - gwf, - icelltype=1, - k=k11, - ) - flopy.mf6.ModflowGwfic(gwf, strt=strt) - flopy.mf6.ModflowGwfchd(gwf, stress_period_data=chd_spd) - - if recharge == "high": - rch = rch_high - elif recharge == "low": - rch = rch_low - flopy.mf6.ModflowGwfrcha(gwf, recharge=rch) - - head_filerecord = f"{sim_name}.hds" - flopy.mf6.ModflowGwfoc( - gwf, - head_filerecord=head_filerecord, - saverecord=[("HEAD", "ALL")], - ) - return sim - return None - - -# Function to write MODFLOW-NWT Problem 3 model files - + sim_ws = os.path.join(workspace, name) + sim = flopy.mf6.MFSimulation(sim_name=sim_name, sim_ws=sim_ws, exe_name="mf6") + flopy.mf6.ModflowTdis(sim, nper=nper, perioddata=tdis_ds, time_units=time_units) + flopy.mf6.ModflowIms( + sim, + print_option="all", + complexity="simple", + linear_acceleration="bicgstab", + outer_maximum=nouter, + outer_dvclose=hclose, + inner_maximum=ninner, + inner_dvclose=hclose, + rcloserecord=rclose, + ) + gwf = flopy.mf6.ModflowGwf( + sim, + modelname=sim_name, + newtonoptions="newton under_relaxation", + ) + flopy.mf6.ModflowGwfdis( + gwf, + length_units=length_units, + nlay=nlay, + nrow=nrow, + ncol=ncol, + delr=delr, + delc=delc, + top=top, + botm=botm, + ) + flopy.mf6.ModflowGwfnpf( + gwf, + icelltype=1, + k=k11, + ) + flopy.mf6.ModflowGwfic(gwf, strt=strt) + flopy.mf6.ModflowGwfchd(gwf, stress_period_data=chd_spd) + + if recharge == "high": + rch = rch_high + elif recharge == "low": + rch = rch_low + flopy.mf6.ModflowGwfrcha(gwf, recharge=rch) + + head_filerecord = f"{sim_name}.hds" + flopy.mf6.ModflowGwfoc( + gwf, + head_filerecord=head_filerecord, + saverecord=[("HEAD", "ALL")], + ) + return sim -def write_model(sim, silent=True): - if config.writeModel: - sim.write_simulation(silent=silent) +def write_models(sim, silent=True): + sim.write_simulation(silent=silent) -# Function to run the model. True is returned if the model runs successfully. -# +@timed +def run_models(sim, silent=True): + success, buff = sim.run_simulation(silent=silent) + assert success, buff -@config.timeit -def run_model(sim, silent=True): - success = True - if config.runModel: - success, buff = sim.run_simulation(silent=silent) - if not success: - print(buff) - return success +# - +# ### Plotting results +# +# Define functions to plot model results. -# Function to create a figure +# + +# Figure properties +figure_size = (6.3, 5.6) +masked_values = (1e30, -1e30) def create_figure(nsubs=1, size=(4, 4)): @@ -256,175 +246,159 @@ def create_figure(nsubs=1, size=(4, 4)): return fig, axes -# Function to plot the grid +def plot_grid(gwf, silent=True): + with styles.USGSMap() as fs: + bot = gwf.dis.botm.array + fig, axes = create_figure(size=(3.15, 4)) + ax = axes[0] + mm = flopy.plot.PlotMapView(gwf, ax=ax, extent=extents) + bot_coll = mm.plot_array(bot, vmin=bmin, vmax=bmax) + mm.plot_bc("CHD", color="cyan") + cv = mm.contour_array( + bot, + levels=blevels, + linewidths=0.5, + linestyles="-", + colors=bcolor, + ) + plt.clabel(cv, fmt="%1.0f") + ax.set_xlabel("x-coordinate, in meters") + ax.set_ylabel("y-coordinate, in meters") + styles.remove_edge_ticks(ax) -def plot_grid(gwf, silent=True): - verbose = not silent - fs = USGSFigure(figure_type="map", verbose=verbose) - - bot = gwf.dis.botm.array - - fig, axes = create_figure(size=(3.15, 4)) - ax = axes[0] - mm = flopy.plot.PlotMapView(gwf, ax=ax, extent=extents) - bot_coll = mm.plot_array(bot, vmin=bmin, vmax=bmax) - mm.plot_bc("CHD", color="cyan") - cv = mm.contour_array( - bot, - levels=blevels, - linewidths=0.5, - linestyles="-", - colors=bcolor, - ) - plt.clabel(cv, fmt="%1.0f") - ax.set_xlabel("x-coordinate, in meters") - ax.set_ylabel("y-coordinate, in meters") - fs.remove_edge_ticks(ax) - - # legend - ax = axes[1] - ax.plot( - -10000, - -10000, - lw=0, - marker="s", - ms=10, - mfc="cyan", - mec="cyan", - label="Constant Head", - ) - ax.plot( - -10000, - -10000, - lw=0.5, - ls="-", - color=bcolor, - label="Bottom elevation contour, m", - ) - fs.graph_legend(ax, loc="center", ncol=2) - - cax = plt.axes([0.275, 0.125, 0.45, 0.025]) - cbar = plt.colorbar( - bot_coll, - shrink=0.8, - orientation="horizontal", - cax=cax, - ) - cbar.ax.tick_params(size=0) - cbar.ax.set_xlabel(r"Bottom Elevation, $m$") - - # save figure - if config.plotSave: - fpth = os.path.join( - "..", - "figures", - f"{sim_name}-grid{config.figure_ext}", + # legend + ax = axes[1] + ax.plot( + -10000, + -10000, + lw=0, + marker="s", + ms=10, + mfc="cyan", + mec="cyan", + label="Constant Head", ) - fig.savefig(fpth) + ax.plot( + -10000, + -10000, + lw=0.5, + ls="-", + color=bcolor, + label="Bottom elevation contour, m", + ) + styles.graph_legend(ax, loc="center", ncol=2) - return + cax = plt.axes([0.275, 0.125, 0.45, 0.025]) + cbar = plt.colorbar( + bot_coll, + shrink=0.8, + orientation="horizontal", + cax=cax, + ) + cbar.ax.tick_params(size=0) + cbar.ax.set_xlabel(r"Bottom Elevation, $m$") + if plot_show: + plt.show() + if plot_save: + fpth = os.path.join( + "..", + "figures", + f"{sim_name}-grid.png", + ) + fig.savefig(fpth) -def plot_recharge(gwf, silent=True): - verbose = not silent - fs = USGSFigure(figure_type="map", verbose=verbose) - - fig, axes = create_figure(nsubs=2, size=figure_size) - ax = axes[0] - mm = flopy.plot.PlotMapView(gwf, ax=ax, extent=extents) - rch_coll = mm.plot_array(rch_high) - mm.plot_bc("CHD", color="cyan") - cv = mm.contour_array( - rch_high, - levels=[1e-6, 2e-6, 3e-6, 4e-6, 5e-6, 6e-6, 7e-6], - linewidths=0.5, - linestyles="-", - colors="black", - ) - plt.clabel(cv, fmt="%1.0e") - cbar = plt.colorbar( - rch_coll, - shrink=0.8, - orientation="horizontal", - ax=ax, - format="%.0e", - ) - cbar.ax.tick_params(size=0) - cbar.ax.set_xlabel(r"Recharge rate, $m/day$") - ax.set_xlabel("x-coordinate, in meters") - ax.set_ylabel("y-coordinate, in meters") - fs.heading(ax, letter="A") - fs.remove_edge_ticks(ax) - - ax = axes[1] - mm = flopy.plot.PlotMapView(gwf, ax=ax, extent=extents) - rch_coll = mm.plot_array(rch_low) - mm.plot_bc("CHD", color="cyan") - cv = mm.contour_array( - rch_low, - levels=[1e-9, 2e-9, 3e-9, 4e-9, 5e-9, 6e-9, 7e-9], - linewidths=0.5, - linestyles="-", - colors="black", - ) - plt.clabel(cv, fmt="%1.0e") - cbar = plt.colorbar( - rch_coll, - shrink=0.8, - orientation="horizontal", - ax=ax, - format="%.0e", - ) - cbar.ax.tick_params(size=0) - cbar.ax.set_xlabel(r"Recharge rate, $m/day$") - ax.set_xlabel("x-coordinate, in meters") - fs.heading(ax, letter="B") - fs.remove_edge_ticks(ax) - # legend - ax = axes[-1] - ax.plot( - -10000, - -10000, - lw=0, - marker="s", - ms=10, - mfc="cyan", - mec="cyan", - label="Constant Head", - ) - ax.plot( - -10000, - -10000, - lw=0.5, - ls="-", - color=bcolor, - label=r"Recharge rate contour, $m/day$", - ) - fs.graph_legend(ax, loc="center", ncol=2) - - # save figure - if config.plotSave: - fpth = os.path.join( - "..", - "figures", - f"{sim_name}-01{config.figure_ext}", +def plot_recharge(gwf, silent=True): + with styles.USGSMap(): + fig, axes = create_figure(nsubs=2, size=figure_size) + ax = axes[0] + mm = flopy.plot.PlotMapView(gwf, ax=ax, extent=extents) + rch_coll = mm.plot_array(rch_high) + mm.plot_bc("CHD", color="cyan") + cv = mm.contour_array( + rch_high, + levels=[1e-6, 2e-6, 3e-6, 4e-6, 5e-6, 6e-6, 7e-6], + linewidths=0.5, + linestyles="-", + colors="black", + ) + plt.clabel(cv, fmt="%1.0e") + cbar = plt.colorbar( + rch_coll, + shrink=0.8, + orientation="horizontal", + ax=ax, + format="%.0e", ) - fig.savefig(fpth) + cbar.ax.tick_params(size=0) + cbar.ax.set_xlabel(r"Recharge rate, $m/day$") + ax.set_xlabel("x-coordinate, in meters") + ax.set_ylabel("y-coordinate, in meters") + styles.heading(ax, letter="A") + styles.remove_edge_ticks(ax) - return + ax = axes[1] + mm = flopy.plot.PlotMapView(gwf, ax=ax, extent=extents) + rch_coll = mm.plot_array(rch_low) + mm.plot_bc("CHD", color="cyan") + cv = mm.contour_array( + rch_low, + levels=[1e-9, 2e-9, 3e-9, 4e-9, 5e-9, 6e-9, 7e-9], + linewidths=0.5, + linestyles="-", + colors="black", + ) + plt.clabel(cv, fmt="%1.0e") + cbar = plt.colorbar( + rch_coll, + shrink=0.8, + orientation="horizontal", + ax=ax, + format="%.0e", + ) + cbar.ax.tick_params(size=0) + cbar.ax.set_xlabel(r"Recharge rate, $m/day$") + ax.set_xlabel("x-coordinate, in meters") + styles.heading(ax, letter="B") + styles.remove_edge_ticks(ax) + # legend + ax = axes[-1] + ax.plot( + -10000, + -10000, + lw=0, + marker="s", + ms=10, + mfc="cyan", + mec="cyan", + label="Constant Head", + ) + ax.plot( + -10000, + -10000, + lw=0.5, + ls="-", + color=bcolor, + label=r"Recharge rate contour, $m/day$", + ) + styles.graph_legend(ax, loc="center", ncol=2) -# Function to plot the model results. + if plot_show: + plt.show() + if plot_save: + fpth = os.path.join( + "..", + "figures", + f"{sim_name}-01.png", + ) + fig.savefig(fpth) def plot_results(idx, sim, silent=True): - verbose = not silent - if config.plotModel: - fs = USGSFigure(figure_type="map", verbose=verbose) - name = list(parameters.keys())[idx] - sim_ws = os.path.join(ws, name) + with styles.USGSMap(): gwf = sim.get_model(sim_name) bot = gwf.dis.botm.array @@ -476,8 +450,8 @@ def plot_results(idx, sim, silent=True): cbar.ax.set_xlabel(r"Water level, $m$") ax.set_xlabel("x-coordinate, in meters") ax.set_ylabel("y-coordinate, in meters") - fs.heading(ax, letter="A") - fs.remove_edge_ticks(ax) + styles.heading(ax, letter="A") + styles.remove_edge_ticks(ax) ax = axes[1] mm = flopy.plot.PlotMapView(gwf, ax=ax, extent=extents) @@ -510,8 +484,8 @@ def plot_results(idx, sim, silent=True): cbar.ax.set_xlabel(r"Saturated thickness, $m$") ax.set_xlabel("x-coordinate, in meters") # ax.set_ylabel("y-coordinate, in meters") - fs.heading(ax, letter="B") - fs.remove_edge_ticks(ax) + styles.heading(ax, letter="B") + styles.remove_edge_ticks(ax) # create legend ax = axes[-1] @@ -541,59 +515,46 @@ def plot_results(idx, sim, silent=True): color=scolor, label="Saturated thickness contour, m", ) - fs.graph_legend(ax, loc="center", ncol=3) + styles.graph_legend(ax, loc="center", ncol=3) - # save figure - if config.plotSave: + if plot_show: + plt.show() + if plot_save: fpth = os.path.join( "..", "figures", - f"{sim_name}-{idx + 2:02d}{config.figure_ext}", + f"{sim_name}-{idx + 2:02d}.png", ) fig.savefig(fpth) -# Function that wraps all of the steps for MODFLOW-NWT Problem 3 model -# -# 1. build_model, -# 2. write_model, -# 3. run_model, and -# 4. plot_results. +# - + +# ### Running the example # +# Define and invoke a function to run the example scenario, then plot results. -def simulation(idx, silent=True): +# + +def scenario(idx, silent=True): key = list(parameters.keys())[idx] params = parameters[key].copy() - - sim = build_model(key, **params) - - write_model(sim, silent=silent) - - success = run_model(sim, silent=silent) - - if success: + sim = build_models(key, **params) + if write: + write_models(sim, silent=silent) + if run: + run_models(sim, silent=silent) + if plot: plot_results(idx, sim, silent=silent) -# nosetest - exclude block from this nosetest to the next nosetest -def test_01(): - simulation(0, silent=False) - - -def test_02(): - simulation(1, silent=False) - +# - -# nosetest end -if __name__ == "__main__": - # ### MODFLOW-NWT Problem 3 Simulation - # - # Simulated heads in the MODFLOW-NWT Problem 3 model with high recharge. +# Run the MODFLOW-NWT Problem 3 model with high recharge, then plot heads. - simulation(0) +scenario(0) - # Simulated heads in the MODFLOW-NWT Problem 3 model with low recharge. +# Run the MODFLOW-NWT Problem 3 model with low recharge, then plot heads. - simulation(1) +scenario(1) diff --git a/scripts/ex-gwf-radial.py b/scripts/ex-gwf-radial.py index 978fd50d1..2e30980ea 100644 --- a/scripts/ex-gwf-radial.py +++ b/scripts/ex-gwf-radial.py @@ -13,28 +13,995 @@ # unconfined aquifers considering delayed gravity response. # Water resources research, 10(2), 303-312 -# Imports +# ### Initial setup +# +# Import dependencies, define the example name and workspace, and read settings from environment variables. +# + import os -import sys -import numpy as np +import pathlib as pl +from math import sqrt + +import flopy import matplotlib.pyplot as plt +import numpy as np +from flopy.plot.styles import styles from matplotlib.patches import Circle -import flopy +from modflow_devtools.misc import get_env, timed + +# Solve definite integral using Fortran library QUADPACK +from scipy.integrate import quad + +# Find a root of a function using Brent's method within a bracketed range +from scipy.optimize import brentq + +# Zero Order Bessel Function +from scipy.special import j0, jn_zeros + +# Example name and base workspace +sim_name = "ex-gwf-rad-disu" +workspace = pl.Path("../examples") + +# Settings from environment variables +write = get_env("WRITE", True) +run = get_env("RUN", True) +plot = get_env("PLOT", True) +plot_show = get_env("PLOT_SHOW", True) +plot_save = get_env("PLOT_SAVE", True) +# - + +# Define some utilities for creating the grid and solving the radial solution + +# + +# Radial unconfined drawdown solution from Neuman 1974 +pi = 3.141592653589793 +sin = np.sin +cos = np.cos +sinh = np.sinh +cosh = np.cosh +exp = np.exp + + +def get_disu_radial_kwargs( + nlay, + nradial, + radius_outer, + surface_elevation, + layer_thickness, + get_vertex=False, +): + """ + Simple utility for creating radial unstructured elements + with the disu package. + + Input assumes that each layer contains the same radial band, + but their thickness can be different. + + Parameters + ---------- + nlay: number of layers (int) + nradial: number of radial bands to construct (int) + radius_outer: Outer radius of each radial band (array-like float with nradial length) + surface_elevation: Top elevation of layer 1 as either a float or nradial array-like float values. + If given as float, then value is replicated for each radial band. + layer_thickness: Thickness of each layer as either a float or nlay array-like float values. + If given as float, then value is replicated for each layer. + """ + pi = 3.141592653589793 + + def get_nn(lay, rad): + return nradial * lay + rad + + def get_rad_array(var, rep): + try: + dim = len(var) + except: + dim, var = 1, [var] + + if dim != 1 and dim != rep: + raise IndexError( + f"get_rad_array(var): var must be a scalar or have len(var)=={rep}" + ) + + if dim == 1: + return np.full(rep, var[0], dtype=np.float64) + else: + return np.array(var, dtype=np.float64) + + nodes = nlay * nradial + surf = get_rad_array(surface_elevation, nradial) + thick = get_rad_array(layer_thickness, nlay) + + iac = np.zeros(nodes, dtype=int) + ja = [] + ihc = [] + cl12 = [] + hwva = [] + + area = np.zeros(nodes, dtype=float) + top = np.zeros(nodes, dtype=float) + bot = np.zeros(nodes, dtype=float) + + for lay in range(nlay): + st = nradial * lay + sp = nradial * (lay + 1) + top[st:sp] = surf - thick[:lay].sum() + bot[st:sp] = surf - thick[: lay + 1].sum() + + for lay in range(nlay): + for rad in range(nradial): + # diagonal/self + n = get_nn(lay, rad) + ja.append(n) + iac[n] += 1 + if rad > 0: + area[n] = pi * (radius_outer[rad] ** 2 - radius_outer[rad - 1] ** 2) + else: + area[n] = pi * radius_outer[rad] ** 2 + ihc.append(n + 1) + cl12.append(n + 1) + hwva.append(n + 1) + # up + if lay > 0: + ja.append(n - nradial) + iac[n] += 1 + ihc.append(0) + cl12.append(0.5 * (top[n] - bot[n])) + hwva.append(area[n]) + # to center + if rad > 0: + ja.append(n - 1) + iac[n] += 1 + ihc.append(1) + cl12.append(0.5 * (radius_outer[rad] - radius_outer[rad - 1])) + hwva.append(2.0 * pi * radius_outer[rad - 1]) + + # to outer + if rad < nradial - 1: + ja.append(n + 1) + iac[n] += 1 + ihc.append(1) + hwva.append(2.0 * pi * radius_outer[rad]) + if rad > 0: + cl12.append(0.5 * (radius_outer[rad] - radius_outer[rad - 1])) + else: + cl12.append(radius_outer[rad]) + # bottom + if lay < nlay - 1: + ja.append(n + nradial) + iac[n] += 1 + ihc.append(0) + cl12.append(0.5 * (top[n] - bot[n])) + hwva.append(area[n]) + + # Build rectangular equivalent of radial coordinates (unwrap radial bands) + if get_vertex: + perimeter_outer = np.fromiter( + (2.0 * pi * rad for rad in radius_outer), + dtype=float, + count=nradial, + ) + xc = 0.5 * radius_outer[0] + yc = 0.5 * perimeter_outer[-1] + # all cells have same y-axis cell center; yc is costant + # + # cell2d: [icell2d, xc, yc, ncvert, icvert]; first node: cell2d = [[0, xc, yc, [2, 1, 0]]] + cell2d = [] + for lay in range(nlay): + n = get_nn(lay, 0) + cell2d.append([n, xc, yc, 3, 2, 1, 0]) + # + xv = radius_outer[0] + # half perimeter is equal to the y shift for vertices + sh = 0.5 * perimeter_outer[0] + vertices = [ + [0, 0.0, yc], + [1, xv, yc - sh], + [2, xv, yc + sh], + ] # vertices: [iv, xv, yv] + iv = 3 + for r in range(1, nradial): + # radius_outer[r-1] + 0.5*(radius_outer[r] - radius_outer[r-1]) + xc = 0.5 * (radius_outer[r - 1] + radius_outer[r]) + for lay in range(nlay): + n = get_nn(lay, r) + # cell2d: [icell2d, xc, yc, ncvert, icvert] + cell2d.append([n, xc, yc, 4, iv - 2, iv - 1, iv + 1, iv]) + + xv = radius_outer[r] + # half perimeter is equal to the y shift for vertices + sh = 0.5 * perimeter_outer[r] + vertices.append([iv, xv, yc - sh]) # vertices: [iv, xv, yv] + iv += 1 + vertices.append([iv, xv, yc + sh]) # vertices: [iv, xv, yv] + iv += 1 + cell2d.sort(key=lambda row: row[0]) # sort by node number + + ja = np.array(ja, dtype=np.int32) + nja = ja.shape[0] + hwva = np.array(hwva, dtype=np.float64) + kw = {} + kw["nodes"] = nodes + kw["nja"] = nja + kw["nvert"] = None + kw["top"] = top + kw["bot"] = bot + kw["area"] = area + kw["iac"] = iac + kw["ja"] = ja + kw["ihc"] = ihc + kw["cl12"] = cl12 + kw["hwva"] = hwva + + if get_vertex: + kw["nvert"] = len(vertices) # = 2*nradial + 1 + kw["vertices"] = vertices + kw["cell2d"] = cell2d + kw["angldegx"] = np.zeros(nja, dtype=float) + else: + kw["nvert"] = 0 + + return kw + + +def _find_hyperbolic_max_value(): + seterr = np.seterr() + np.seterr(all="ignore") + inf = np.inf + x = 10.0 + delt = 1.0 + for i in range(1000000): + x += delt + try: + if inf == sinh(x): + break + except: + break + np.seterr(**seterr) + return x - delt + + +_hyperbolic_max_value = _find_hyperbolic_max_value() -# Append to system path to include the common subdirectory -sys.path.append(os.path.join("..", "common")) +def _find_hyperbolic_equivalent_value(): + x = 10.0 + delt = 0.0001 + for i in range(1000000): + x += delt + if x > _hyperbolic_max_value: + break + try: + if sinh(x) == cosh(x): + return x + except: + break + return x - delt -# import common functionality -import config -from figspecs import USGSFigure +_hyperbolic_equivalence = _find_hyperbolic_equivalent_value() -from get_disu_radial_kwargs import get_disu_radial_kwargs -from neuman1974_soln import RadialUnconfinedDrawdown -# Utility function to return DISU node for given radial band and layer +class RadialUnconfinedDrawdown: + """ + Solves the drawdown that occurs from pumping from partial penetration + in an unconfined, radial aquifer. Uses the method described in: + Neuman, S. P. (1974). Effect of partial penetration on flow in + unconfined aquifers considering delayed gravity response. + Water resources research, 10(2), 303-312. + """ + + hyperbolic_max_value = _hyperbolic_max_value + hyperbolic_equivalence = _hyperbolic_equivalence + + bottom: float + Kr: float + Kz: float + Ss: float + Sy: float + well_top: float + well_bot: float + saturated_thickness: float + + _sigma: float + _beta: float + + def __init__( + self, + bottom_elevation, + hydraulic_conductivity_radial=None, + hydraulic_conductivity_vertical=None, + specific_storage=None, + specific_yield=None, + well_screen_elevation_top=None, + well_screen_elevation_bottom=None, + water_table_elevation=None, + saturated_thickness=None, + ): + """ + Initialize unconfined, radial groundwater model to solve drawdown + at an observation location in response to pumping at the center of + the model (that is, the well extracts water at radius = 0). + + Parameters + ---------- + rad : int + radial band number (0 to nradial-1) + + bottom_elevation : float + Elevation of the impermeable base of the model ($L$) + hydraulic_conductivity_radial : float + Radial direction hydraulic conductivity of model ($L/T$) + hydraulic_conductivity_vertical : float + Vertical (z) direction hydraulic conductivity of model ($L/T$) + specific_storage : float + Specific storage of aquifer ($1/T$) + specific_yield : float + Specific yield of aquifer ($-$) + well_screen_elevation_top : float + Pumping well's top screen elevation ($L$) + well_screen_elevation_bottom : float + Pumping well's bottom screen elevation ($L$) + water_table_elevation : float + Initial water table elevation. Note, saturated_thickness (b) is + calculated as $water_table_elevation - bottom_elevation$ ($L$) + saturated_thickness : float + Specify the initial saturated thickness of the unconfined aquifer. + Value is used to calculate the water_table_elevation. If + water_table_elevation is defined, then saturated_thickness input + is ignored and set to + $water_table_elevation - bottom_elevation$ ($L$) + """ + + self.bottom = float(bottom_elevation) + self.Kr = self._float_or_none(hydraulic_conductivity_radial) + self.Kz = self._float_or_none(hydraulic_conductivity_vertical) + self.Ss = self._float_or_none(specific_storage) + self.Sy = self._float_or_none(specific_yield) + self.well_top = self._float_or_none(well_screen_elevation_top) + self.well_bot = self._float_or_none(well_screen_elevation_bottom) + + if water_table_elevation is not None and saturated_thickness is not None: + raise RuntimeError( + "RadialUnconfinedDrawdown() must specify only " + + "water_table_elevation or saturated_thickness, but not " + + "both at the same time." + ) + + if water_table_elevation is not None: + self.saturated_thickness = float(water_table_elevation) - self.bottom + elif saturated_thickness is not None: + self.saturated_thickness = float(saturated_thickness) + else: + self.saturated_thickness = None + + def _prop_check(self): + error = [] + if self.Kr is None: + error.append("hydraulic_conductivity_radial") + if self.Kz is None: + error.append("hydraulic_conductivity_vertical") + if self.Ss is None: + error.append("specific_storage") + if self.Sy is None: + error.append("specific_yield") + if self.well_top is None: + error.append("well_screen_elevation_top") + if self.well_bot is None: + error.append("well_screen_elevation_bottom") + if error: + raise RuntimeError( + "RadialUnconfinedDrawdown: Attempted to solve radial " + + "groundwater model\nwith the following input not specified\n" + + "\n".join(error) + ) + if self.well_top <= self.well_bot: + raise RuntimeError( + "RadialUnconfinedDrawdown: " + + "well_screen_elevation_top <= well_screen_elevation_bottom\n" + + f"That is: {well_screen_elevation_top} <= " + + f"{well_screen_elevation_bottom}" + ) + + def drawdown( + self, + pump, + time, + radius, + observation_elevation, + observation_elevation_bot=None, + sumrtol=1.0e-6, + u_n_rtol=1.0e-5, + epsabs=1.49e-8, + bessel_loop_limit=5, + quad_limit=128, + show_progress=False, + ty_time=False, + ts_time=False, + as_head=False, + ): + """ + Solves the radial model's drawdown for a given pumping rate and + time at a given observation point + (radius, observation_elevation) or observation well screen interval + (radius, observation_elevation:observation_elevation_bot). + This solves drawdown by integrating equation 17 from + Neuman, S. P. (1974). Effect of partial penetration on flow in + unconfined aquifers considering delayed gravity response. + Water resources research, 10(2), 303-312 + + Parameters + ---------- + pump : float + Pumping rate of well at center of radial model ($L^3/T$) + Positive values are the water extraction rate. + Negative or zero values indicate no pumping and result returns + the dimensionless drawdown instead of regular drawdown. + time : float or Sequence[float] + Time that observation is made + radius : float + Radius of the observation location (distance from well, $L$) + observation_elevation : float + Either the location of the observation point, or the top elevation + of the observation well screen ($L$) + observation_elevation_bot : float + If specified, then represents the bottom elevation of the + observation well screen. If not specified (or set to None), then + observation location is treated as a single point, located at + radius and observation_elevation ($L$) + sumrtol : float + Solution involves integration of $y$ variable from 0 to ∞ from + Equation 17 in: + Neuman, S. P. (1974). Effect of partial penetration on flow in + unconfined aquifers considering delayed gravity response. + Water resources research, 10(2), 303-312. + + The integration is broken into subsections that are spaced around + bessel function roots. The integration is complete when a + three sequential subsection solutions are less than + sumrtol times the largest subsection. + That is, the last included subsection contributes a + relatively small value compared to the largest of the sum. + u_n_rtol : float + Terminates the solution of the infinite series: + $\\sum_{n=1}^{\\infty} u_n(y)$ + when + $u_n(y) < u_n(0) * u_n_rtol$ + epsabs : float or int + scipy.integrate.quad absolute error tolerance. + Passed directly to that function's `epsabs` kwarg. + bessel_loop_limit : int + the integral is solved along each bessel function root. + The first 1024 roots are precalculated and automatically increased + if more are required. The upper limit for calculated roots is + 1024 * 2 ^ bessel_loop_limit + If this limit is reached, then a warning is raised. + quad_limit : int + scipy.integrate.quad upper bound on the number of + subintervals used in the adaptive algorithm. + Passed directly to that function's `limit` kwarg. + show_progress : bool + if True, then progress is printed to the command prompt in the form: + ty_time : bool + if True, then `time` kwarg is dimensionless time with + respect to Specific Yield + ts_time : bool + if True, then `time` kwarg is dimensionless time with + respect to Specific Storage. + as_head : bool + If true, then drawdown result is converted to + head using the model bottom and initial saturated thickness. + If pump > 0, then as_head is ignored. + + Returns + ------- + result : float or list[float] + If time is float, then result is float. + If time is Sequence[float], then result is list[float]. + + If pump > 0, then result is the drawdown that occurs + from pump at time and radius at observation point + observation_elevation or from the observation well + screen interval observation_elevation to + observation_elevation_top ($L$). + + + If pump <= 0, then result is converted to + dimensionless drawdown ($-$) + """ + if not hasattr(time, "strip") and hasattr(time, "__iter__"): + return self.drawdown_times( + pump, + time, + radius, + observation_elevation, + observation_elevation_bot, + sumrtol, + u_n_rtol, + epsabs, + bessel_loop_limit, + quad_limit, + show_progress, + ty_time, + ts_time, + as_head, + ) + + return self.drawdown_times( + pump, + [time], + radius, + observation_elevation, + observation_elevation_bot, + sumrtol, + u_n_rtol, + epsabs, + bessel_loop_limit, + quad_limit, + show_progress, + ty_time, + ts_time, + as_head, + )[0] + + def drawdown_times( + self, + pump, + times, + radius, + observation_elevation, + observation_elevation_bot=None, + sumrtol=1.0e-6, + u_n_rtol=1.0e-5, + epsabs=1.49e-8, + bessel_loop_limit=5, + quad_limit=128, + show_progress=False, + ty_time=False, + ts_time=False, + as_head=False, + ): + # Same as self.drawdown, but times is a list[float] of + # observation times and returns a list[float] drawdowns. + + if bessel_loop_limit < 1: + bessel_loop_limit = 1 + + bessel_roots0 = 1024 + bessel_roots = bessel_roots0 + bessel_root_limit_reached = [] + + self._prop_check() + if ty_time and ts_time: + raise RuntimeError( + "RadialUnconfinedDrawdown.drawdown_times " + + "cannot set both ty_time and ts_time to True." + ) + + r = radius + b = self.saturated_thickness + + sigma = self.Ss * b / self.Sy + beta = (r / b) * (r / b) * (self.Kz / self.Kr) + sqrt_beta = sqrt(beta) + + if np.isnan(pump) or pump <= 0.0: + # Return dimensionless drawdown + coef = 1.0 + else: + coef = pump / (4.0 * pi * b * self.Kr) + + # dimensionless well screen top + dd = (self.saturated_thickness + self.bottom - self.well_top) / b + # dimensionless well screen bottom + ld = (self.saturated_thickness + self.bottom - self.well_bot) / b + + # Solution must be in dimensionless time with respect to Ss; + # ts = kr*b*t/(Ss*b*r^2) + if ty_time: + ts_list = self.ty2ts(times) + elif ts_time: + ts_list = times + else: + ts_list = self.time2ts(times, r) + + # distance above bottom to observation point or obs screen bottom + zt = observation_elevation - self.bottom + if observation_elevation_bot is None: + # Single Point Observation + zd = zt / b # dimensionless elevation of observation point + neuman1974_integral = self.neuman1974_integral1 + obs_arg = (zd,) + else: + # distance above bottom to observation screen top + zb = observation_elevation_bot - self.bottom + # dimensionless elevation of observation screen interval + ztd, zbd = zt / b, zb / b + # dz = 1 / (zt - zb) -> implied in the + # modified u0 and uN functions + neuman1974_integral = self.neuman1974_integral2 + obs_arg = (zbd, ztd) + + s = [] # drawdown, one to one match with times + nstp = len(ts_list) + for stp, ts in enumerate(ts_list): + if show_progress: + print( + f"Solving {stp+1:4d} of {nstp}; " + f"time = {self.ts2time(ts, r)}", + end="", + ) + + args = (sigma, beta, sqrt_beta, ld, dd, ts, *obs_arg, u_n_rtol) + sol = 0.0 + y0, y1 = 0.0, 0.0 + mxdelt = 0.0 + + j0_roots = jn_zeros(0, bessel_roots) / sqrt_beta + jr0 = 0 + jr1 = j0_roots.size + + converged = 0 + bessel_loop_count = 0 + while converged < 3 and bessel_loop_count <= bessel_loop_limit: + if bessel_loop_count > 0: + bessel_roots *= 2 + j0_roots = jn_zeros(0, bessel_roots) / sqrt_beta + jr0, jr1 = jr1, j0_roots.size + + j0_roots_iter = np.nditer(j0_roots[jr0:jr1]) + bessel_loop_count += 1 + # Iterate over two roots to get full cycle + for j0_root in j0_roots_iter: + # First root + y0, y1 = y1, j0_root + delt1 = quad( + neuman1974_integral, + y0, + y1, + args, + epsabs=epsabs, + limit=quad_limit, + )[0] + # + # Second root + y0, y1 = y1, next(j0_roots_iter) + delt2 = quad( + neuman1974_integral, + y0, + y1, + args, + epsabs=epsabs, + limit=quad_limit, + )[0] + + if np.isnan(delt1) or np.isnan(delt2): + break + + sol += delt1 + delt2 + + adelt = abs(delt1 + delt2) + if adelt > mxdelt: + mxdelt = adelt + elif adelt < mxdelt * sumrtol: + converged += 1 # increment the convergence counter + # Converged if three sequential solutions (adelt) + # are less than mxdelt*sumrtol + if converged >= 3: + break + else: + converged = 0 # reset convergence counter + if sol < 0.0: + s.append(0.0) + else: + s.append(coef * sol) + + if converged < 3: + bessel_root_limit_reached.append(stp) + + if show_progress: + if converged < 3: + print(f"\ts = {s[-1]}\tbessel_loop_limit reached") + else: + print(f"\ts = {s[-1]}") + + if pump > 0.0 and as_head: + initial_head = self.bottom + self.saturated_thickness + return [initial_head - drawdown for drawdown in s] + + if len(bessel_root_limit_reached) > 0: + import warnings + + root = j0_roots[-1] + bad_times = "\n".join([str(times[it]) for it in bessel_root_limit_reached]) + warnings.warn( + f"\n\nRadialUnconfinedDrawdown.drawdown_times failed to " + + f"meet convergence sumrtol = {sumrtol}" + + "\nwithin the precalculated Bessel root solutions " + + "(convergence is evaluated at every second Bessel root).\n\n" + + "The number of Bessel roots are automatically increased " + + "up to:\n" + + f" {bessel_roots0} * 2^bessel_loop_limit\nwhere:\n" + + " bessel_loop_limit = {bessel_loop_limit}\n" + + f"resulting in {1024*2**bessel_loop_limit} roots evaluated, " + + "with the last root being {root}\n" + + f"(That is, the Neuman integral was solved form 0 to {root})" + + "\n\n" + + "You can either ignore this warning\n" + + "or to remove it attempt to increase bessel_loop_limit\n" + + "or increase sumrtol (reducing accuracy).\n\nThe following " + + "times are what triggered this warning:\n" + + bad_times + + "\n" + ) + return s + + @staticmethod + def neuman1974_integral1(y, σ, β, sqrt_β, ld, dd, ts, zd, uN_tol=1.0e-6): + """ + Solves equation 17 from + Neuman, S. P. (1974). Effect of partial penetration on flow in + unconfined aquifers considering delayed gravity response. + Water resources research, 10(2), 303-312. + """ + if y == 0.0 or ts == 0.0: + return 0.0 + + u0 = RadialUnconfinedDrawdown.u_0(σ, β, zd, ld, dd, ts, y) + + if np.isnan(u0): + u0 = 0.0 + + uN_func = RadialUnconfinedDrawdown.u_n + mxdelt = 0.0 + uN = 0.0 + for n in range(1, 25001): + delt = uN_func(σ, β, zd, ld, dd, ts, y, n) + if np.isnan(delt): + break + uN += delt + adelt = abs(delt) + if adelt > mxdelt: + mxdelt = adelt + elif adelt < mxdelt * uN_tol: + break + + return 4.0 * y * j0(y * sqrt_β) * (u0 + uN) + + @staticmethod + def gamma0(g, y, s): + """ + Gamma0 root function from equation 18 in: + Neuman, S. P. (1974). Effect of partial penetration on flow in + unconfined aquifers considering delayed gravity response. + Water resources research, 10(2), 303-312. + => Solution must be constrained by g^2 < y^2 + + To honor the constraint solution returns the absolute value + of the solution. + """ + if g >= _hyperbolic_equivalence: + # sinh ≈ cosh for large g + return s * g - (y * y - g * g) + + return s * g * sinh(g) - (y * y - g * g) * cosh(g) + + @staticmethod + def gammaN(g, y, s): + """ + GammaN root function from equation 19 in: + Neuman, S. P. (1974). Effect of partial penetration on flow in + unconfined aquifers considering delayed gravity response. + Water resources research, 10(2), 303-312. + => Solution must be constrained by (2n-1)(π/2)< g < nπ + """ + return s * g * sin(g) + (y * y + g * g) * cos(g) + + @staticmethod + def u_0(σ, β, z, l, d, ts, y): + gamma0 = RadialUnconfinedDrawdown.gamma0 + + a, b = 0.9 * y, y + try: + a, b = RadialUnconfinedDrawdown._get_bracket(gamma0, a, b, (y, σ)) + except RuntimeError: + a, b = RadialUnconfinedDrawdown._get_bracket(gamma0, 0.0, b, (y, σ), 1000) + + g = brentq(gamma0, a, b, args=(y, σ), maxiter=500, xtol=1.0e-16) + + # Check for cosh/sinh overflow + if g > _hyperbolic_max_value: + return 0.0 + + y2 = y * y + g2 = g * g + num1 = 1 - exp(-ts * β * (y2 - g2)) + num2 = cosh(g * z) + num3 = sinh(g * (1 - d)) - sinh(g * (1 - l)) + den1 = y2 + (1 + σ) * g2 - ((y2 - g2) ** 2) / σ + den2 = cosh(g) + den3 = (l - d) * sinh(g) + # num1*num2*num3 / (den1*den2*den3) + return (num1 / den1) * (num2 / den2) * (num3 / den3) + + @staticmethod + def u_n(σ, β, z, l, d, ts, y, n): + gammaN = RadialUnconfinedDrawdown.gammaN + + a, b = (2 * n - 1) * (pi / 2.0), n * pi + try: + a, b = RadialUnconfinedDrawdown._get_bracket(gammaN, a, b, (y, σ)) + except RuntimeError: + a, b = RadialUnconfinedDrawdown._get_bracket(gammaN, a, b, (y, σ), 1000) + + g = brentq(gammaN, a, b, args=(y, σ), maxiter=500, xtol=1.0e-16) + + y2 = y * y + g2 = g * g + num1 = 1 - exp(-ts * β * (y2 + g2)) + num2 = cos(g * z) + num3 = sin(g * (1 - d)) - sin(g * (1 - l)) + den1 = y2 - (1 + σ) * g2 - ((y2 + g2) ** 2) / σ + den2 = cos(g) + den3 = (l - d) * sin(g) + return num1 * num2 * num3 / (den1 * den2 * den3) + + @staticmethod + def neuman1974_integral2(y, σ, β, sqrt_β, ld, dd, ts, z1, z2, uN_tol=1.0e-10): + """ + Solves equation 20 from + Neuman, S. P. (1974). Effect of partial penetration on flow in + unconfined aquifers considering delayed gravity response. + Water resources research, 10(2), 303-312. + """ + if y == 0.0 or ts == 0.0: + return 0.0 + + u0 = RadialUnconfinedDrawdown.u_0_z1z2(σ, β, z1, z2, ld, dd, ts, y) + + uN_func = RadialUnconfinedDrawdown.u_n_z1z2 + mxdelt = 0.0 + uN = 0.0 + for n in range(1, 10001): + delt = uN_func(σ, β, z1, z2, ld, dd, ts, y, n) + uN += delt + adelt = abs(delt) + if adelt > mxdelt: + mxdelt = adelt + elif adelt < mxdelt * uN_tol: + break + + return 4.0 * y * j0(y * sqrt_β) * (u0 + uN) + + @staticmethod + def u_0_z1z2(σ, β, z1, z2, l, d, ts, y): + gamma0 = RadialUnconfinedDrawdown.gamma0 + + a, b = 0.9 * y, y + try: + a, b = RadialUnconfinedDrawdown._get_bracket(gamma0, a, b, (y, σ)) + except RuntimeError: + a, b = RadialUnconfinedDrawdown._get_bracket(gamma0, 0.0, b, (y, σ), 1000) + + g = brentq(gamma0, a, b, args=(y, σ), maxiter=500, xtol=1.0e-16) + + # Check for cosh/sinh overflow + if g > _hyperbolic_max_value: + return 0.0 + + y2 = y * y + g2 = g * g + num1 = 1 - exp(-ts * β * (y2 - g2)) + num2 = sinh(g * z2) - sinh(g * z1) + num3 = sinh(g * (1 - d)) - sinh(g * (1 - l)) + den1 = (y2 + (1 + σ) * g2 - ((y2 - g2) ** 2) / σ) * (z2 - z1) * g + den2 = cosh(g) + den3 = (l - d) * sinh(g) + # num1*num2*num3 / (den1*den2*den3) + return (num1 / den1) * (num2 / den2) * (num3 / den3) + + @staticmethod + def u_n_z1z2(σ, β, z1, z2, l, d, ts, y, n): + gammaN = RadialUnconfinedDrawdown.gammaN + + a, b = (2 * n - 1) * (pi / 2.0), n * pi + try: + a, b = RadialUnconfinedDrawdown._get_bracket(gammaN, a, b, (y, σ)) + except RuntimeError: + a, b = RadialUnconfinedDrawdown._get_bracket(gammaN, a, b, (y, σ), 1000) + + g = brentq(gammaN, a, b, args=(y, σ), maxiter=500, xtol=1.0e-16) + + y2 = y * y + g2 = g * g + num1 = 1 - exp(-ts * β * (y2 + g2)) + num2 = sin(g * z2) - sin(g * z1) + num3 = sin(g * (1 - d)) - sin(g * (1 - l)) + den1 = y2 - (1 + σ) * g2 - ((y2 + g2) ** 2) / σ + den2 = cos(g) * (z2 - z1) * g + den3 = (l - d) * sin(g) + return num1 * num2 * num3 / (den1 * den2 * den3) + + def time2ty(self, time, radius): + # dimensionless time with respect to Sy + if hasattr(time, "__iter__"): + # can iterate to get multiple times + return [ + self.Kr * self.saturated_thickness * t / (self.Sy * radius * radius) + for t in time + ] + return self.Kr * self.saturated_thickness * time / (self.Sy * radius * radius) + + def time2ts(self, time, radius): + # dimensionless time with respect to Ss + if hasattr(time, "__iter__"): + # can iterate to get multiple times + return [self.Kr * t / (self.Ss * radius * radius) for t in time] + return self.Kr * time / (self.Ss * radius * radius) + + def ty2time(self, ty, radius): + # dimensionless time with respect to Sy + if hasattr(ty, "__iter__"): + # can iterate to get multiple times + return [ + t * self.Sy * radius * radius / (self.Kr * self.saturated_thickness) + for t in ty + ] + return ty * self.Sy * radius * radius / (self.Kr * self.saturated_thickness) + + def ts2time(self, ts, radius): # dimensionless time with respect to Ss + if hasattr(ts, "__iter__"): # can iterate to get multiple times + return [t * self.Ss * radius * radius / self.Kr for t in ts] + return ts * self.Ss * radius * radius / self.Kr + + def ty2ts(self, ty): + if hasattr(ty, "__iter__"): + # can iterate to get multiple times + return [t * self.Sy / (self.Ss * self.saturated_thickness) for t in ty] + return ty * self.Sy / (self.Ss * self.saturated_thickness) + + def drawdown2unitless(self, s, pump): + # dimensionless drawdown + return 4 * pi * self.Kr * self.saturated_thickness * s / pump + + def unitless2drawdown(self, s, pump): + # drawdown + return pump * s / (4 * pi * self.Kr * self.saturated_thickness) + + @staticmethod + def _float_or_none(val): + if val is not None: + return float(val) + return None + + @staticmethod + def _get_bracket(func, a, b, arg=(), internal_search_split=100): + """ + Given initial range [a, b], search within the range for + root finding brackets. + That is, return [a, b] that results in f(a) * f(b) < 0. + """ + if a > b: + a, b = b, a + + f1 = func(a, *arg) + f2 = func(b, *arg) + + if f1 * f2 <= 0.0: + return a, b + + # same sign, search within for sign change + delt = abs(b - a) / internal_search_split + a -= delt + for _ in range(internal_search_split): + a += delt + f1 = func(a, *arg) + if f1 * f2 <= 0.0: + return a, b + + raise RuntimeError( + "get_bracket: failed to find bracket interval with opposite " + + f"signs, that is: f(a)*f(b) < 0 for func: {func}" + ) def get_radial_node(rad, lay, nradial): @@ -86,56 +1053,35 @@ def get_radius_lay_from_node(node, nradial): return rad, lay -# Run Analytical Model - Very slow -# If True, solves the Neuman 1974 analytical model (very slow) -# else uses stored results from solving the Neuman 1974 analytical model - -solve_analytical_solution = False - +# - -# Set default figure properties - -figure_size = (6, 6) - -# Base simulation and model name and workspace - -ws = config.base_ws - -# Simulation name - -sim_name = "ex-gwf-rad-disu" +# ### Define parameters +# +# Define model units, parameters and other settings. +# + # Model units - length_units = "feet" time_units = "days" -# Table Model Parameters - +# Model parameters nper = 1 # Number of periods _ = 24 # Number of time steps _ = "10" # Simulation total time ($day$) - nlay = 25 # Number of layers nradial = 22 # Number of radial direction cells (radial bands) - initial_head = 50.0 # Initial water table elevation ($ft$) - surface_elevation = 50.0 # Top of the radial model ($ft$) _ = 0.0 # Base of the radial model ($ft$) layer_thickness = 2.0 # Thickness of each radial layer ($ft$) _ = "0.25 to 2000" # Outer radius of each radial band ($ft$) - k11 = 20.0 # Horizontal hydraulic conductivity ($ft/day$) k33 = 20.0 # Vertical hydraulic conductivity ($ft/day$) - ss = 1.0e-5 # Specific storage ($1/day$) sy = 0.1 # Specific yield (unitless) - _ = "0.0 to 10" # Well screen elevation ($ft$) _ = "1" # Well radial band location (unitless) _ = "-4000.0" # Well pumping rate ($ft^3/day$) - _ = "40" # Observation distance from well ($ft$) _ = "1" # ``Top'' observation elevation ($ft$) _ = "25" # ``Middle'' observation depth ($ft$) @@ -175,9 +1121,7 @@ def get_radius_lay_from_node(node, nradial): # This example has the well screen interval from # layer 20 to 24 (zero-based index) wel_spd = { - sp: [ - [(get_radial_node(0, lay, nradial),), -800.0] for lay in range(20, 25) - ] + sp: [[(get_radial_node(0, lay, nradial),), -800.0] for lay in range(20, 25)] for sp in range(nper) } @@ -202,113 +1146,98 @@ def get_radius_lay_from_node(node, nradial): ninner = 300 hclose = 1e-4 rclose = 1e-4 +# - - -# ### Functions to build, write, run, and plot the MODFLOW 6 Axisymmetric Model +# ### Model setup # -# MODFLOW 6 flopy simulation object (sim) is returned if building the model - - -def build_model(name): - if config.buildModel: - sim_ws = os.path.join(ws, name) - sim = flopy.mf6.MFSimulation( - sim_name=name, sim_ws=sim_ws, exe_name="mf6" - ) - flopy.mf6.ModflowTdis( - sim, nper=nper, perioddata=tdis_ds, time_units=time_units - ) - flopy.mf6.ModflowIms( - sim, - print_option="summary", - complexity="complex", - outer_maximum=nouter, - outer_dvclose=hclose, - inner_maximum=ninner, - inner_dvclose=hclose, - ) - - gwf = flopy.mf6.ModflowGwf(sim, modelname=name, save_flows=True) - - disukwargs = get_disu_radial_kwargs( - nlay, - nradial, - radius_outer, - surface_elevation, - layer_thickness, - get_vertex=True, - ) - - disu = flopy.mf6.ModflowGwfdisu( - gwf, length_units=length_units, **disukwargs - ) +# Define functions to build models, write input files, and run the simulation. + + +# + +def build_models(name): + sim_ws = os.path.join(workspace, name) + sim = flopy.mf6.MFSimulation(sim_name=name, sim_ws=sim_ws, exe_name="mf6") + flopy.mf6.ModflowTdis(sim, nper=nper, perioddata=tdis_ds, time_units=time_units) + flopy.mf6.ModflowIms( + sim, + print_option="summary", + complexity="complex", + outer_maximum=nouter, + outer_dvclose=hclose, + inner_maximum=ninner, + inner_dvclose=hclose, + ) - npf = flopy.mf6.ModflowGwfnpf( - gwf, - k=k11, - k33=k33, - save_flows=True, - save_specific_discharge=True, - ) + gwf = flopy.mf6.ModflowGwf(sim, modelname=name, save_flows=True) - flopy.mf6.ModflowGwfsto( - gwf, - iconvert=1, - sy=sy, - ss=ss, - save_flows=True, - ) + disukwargs = get_disu_radial_kwargs( + nlay, + nradial, + radius_outer, + surface_elevation, + layer_thickness, + get_vertex=True, + ) - flopy.mf6.ModflowGwfic(gwf, strt=initial_head) + disu = flopy.mf6.ModflowGwfdisu(gwf, length_units=length_units, **disukwargs) - flopy.mf6.ModflowGwfwel( - gwf, stress_period_data=wel_spd, save_flows=True - ) + npf = flopy.mf6.ModflowGwfnpf( + gwf, + k=k11, + k33=k33, + save_flows=True, + save_specific_discharge=True, + ) - flopy.mf6.ModflowGwfoc( - gwf, - budget_filerecord=f"{name}.cbc", - head_filerecord=f"{name}.hds", - headprintrecord=[ - ("COLUMNS", nradial, "WIDTH", 15, "DIGITS", 6, "GENERAL") - ], - saverecord=[("HEAD", "ALL"), ("BUDGET", "ALL")], - printrecord=[("HEAD", "ALL"), ("BUDGET", "ALL")], - filename=f"{name}.oc", - ) + flopy.mf6.ModflowGwfsto( + gwf, + iconvert=1, + sy=sy, + ss=ss, + save_flows=True, + ) - flopy.mf6.ModflowUtlobs(gwf, print_input=False, continuous=obsdict) - return sim - return None + flopy.mf6.ModflowGwfic(gwf, strt=initial_head) + flopy.mf6.ModflowGwfwel(gwf, stress_period_data=wel_spd, save_flows=True) -# Function to write model files + flopy.mf6.ModflowGwfoc( + gwf, + budget_filerecord=f"{name}.cbc", + head_filerecord=f"{name}.hds", + headprintrecord=[("COLUMNS", nradial, "WIDTH", 15, "DIGITS", 6, "GENERAL")], + saverecord=[("HEAD", "ALL"), ("BUDGET", "ALL")], + printrecord=[("HEAD", "ALL"), ("BUDGET", "ALL")], + filename=f"{name}.oc", + ) + flopy.mf6.ModflowUtlobs(gwf, print_input=False, continuous=obsdict) + return sim -def write_model(sim, silent=True): - if config.writeModel: - sim.write_simulation(silent=silent) +def write_models(sim, silent=True): + sim.write_simulation(silent=silent) -# Function to run the Axisymmetric model. -# True is returned if the model runs successfully. +@timed +def run_models(sim, silent=True): + success, buff = sim.run_simulation(silent=silent, report=True) + assert success, buff -@config.timeit -def run_model(sim, silent=True): - success = True - if config.runModel: - success, buff = sim.run_simulation(silent=silent, report=True) - if not success: - print("\n".join(buff)) - return success +# - +# ### Plotting results +# +# Define functions to plot model results. -# Function to solve Axisymmetric model using analytical equation. +# + +# Set default figure properties +figure_size = (6, 6) def solve_analytical(obs2ana, times=None, no_solve=False): + """Solve Axisymmetric model using analytical equation.""" # obs2ana = {obsdict[file][0] : analytical_name} disukwargs = get_disu_radial_kwargs( nlay, nradial, radius_outer, surface_elevation, layer_thickness @@ -382,8 +1311,7 @@ def solve_analytical(obs2ana, times=None, no_solve=False): ] else: times_sy = [ - ty * k11 * sat_thick / (sy * obs_rad * obs_rad) - for ty in times + ty * k11 * sat_thick / (sy * obs_rad * obs_rad) for ty in times ] times_ss = [ty * k11 / (ss * obs_rad * obs_rad) for ty in times] @@ -857,178 +1785,174 @@ def plot_ts(sim, verbose=False, solve_analytical_solution=False): 1.800648435, ] - fs = USGSFigure(figure_type="graph", verbose=verbose) - - obs_fig = "obs-head" - fig = plt.figure(figsize=(5, 3)) - ax = fig.add_subplot() - ax.set_xlabel("time (d)") - ax.set_ylabel("head (ft)") - for name in tsdata.dtype.names[1:]: - ax.plot( - tsdata["totim"], - tsdata[name], - fmt[name], - label=obsnames[name], - markerfacecolor="none", - ) - # , markersize=3 - - for name in analytical: - n = len(analytical[name]) - if solve_analytical_solution: - ana_times = ana_prop[name][0] - else: - ana_times = analytical_time - - ax.plot( - ana_times[:n], - [50.0 - h for h in analytical[name]], - fmt[name], - label=obsnames[name], - ) - - fs.graph_legend(ax) - - fig.tight_layout() - - if config.plotSave: - fpth = os.path.join( - "..", - "figures", - "{}-{}{}".format(sim_name, obs_fig, config.figure_ext), - ) - fig.savefig(fpth) - - obs_fig = "obs-dimensionless" - fig = plt.figure(figsize=(5, 3)) - fig.tight_layout() - ax = fig.add_subplot() - ax.set_xlim(0.001, 100.0) - ax.set_ylim(0.001, 100.0) - ax.grid(visible=True, which="major", axis="both") - ax.set_ylabel("Dimensionless Drawdown, $s_d$") - ax.set_xlabel("Dimensionless Time, $t_y$") - for name in tsdata.dtype.names[1:]: - q = ana_prop[obs2ana[name]][3] - r = ana_prop[obs2ana[name]][4] - b = ana_prop[obs2ana[name]][5] - ax.loglog( - [k11 * b * ts / (sy * r * r) for ts in tsdata["totim"]], - [4 * pi * k11 * b * (initial_head - h) / q for h in tsdata[name]], - fmt[name], - label=obsnames[name], - markerfacecolor="none", - ) - - for name in analytical: - q = ana_prop[name][3] - b = ana_prop[name][5] # [pump, radius, sat_thick, model_bottom] - if solve_analytical_solution: - ana_times = ana_prop[name][0] - else: - ana_times = analytical_time + with styles.USGSPlot() as fs: + obs_fig = "obs-head" + fig = plt.figure(figsize=(5, 3)) + ax = fig.add_subplot() + ax.set_xlabel("time (d)") + ax.set_ylabel("head (ft)") + for name in tsdata.dtype.names[1:]: + ax.plot( + tsdata["totim"], + tsdata[name], + fmt[name], + label=obsnames[name], + markerfacecolor="none", + ) + # , markersize=3 + + for name in analytical: + n = len(analytical[name]) + if solve_analytical_solution: + ana_times = ana_prop[name][0] + else: + ana_times = analytical_time + + ax.plot( + ana_times[:n], + [50.0 - h for h in analytical[name]], + fmt[name], + label=obsnames[name], + ) + + styles.graph_legend(ax) + + fig.tight_layout() + + if plot_save: + fpth = os.path.join( + "..", + "figures", + "{}-{}{}".format(sim_name, obs_fig, ".png"), + ) + fig.savefig(fpth) + + obs_fig = "obs-dimensionless" + fig = plt.figure(figsize=(5, 3)) + fig.tight_layout() + ax = fig.add_subplot() + ax.set_xlim(0.001, 100.0) + ax.set_ylim(0.001, 100.0) + ax.grid(visible=True, which="major", axis="both") + ax.set_ylabel("Dimensionless Drawdown, $s_d$") + ax.set_xlabel("Dimensionless Time, $t_y$") + for name in tsdata.dtype.names[1:]: + q = ana_prop[obs2ana[name]][3] + r = ana_prop[obs2ana[name]][4] + b = ana_prop[obs2ana[name]][5] + ax.loglog( + [k11 * b * ts / (sy * r * r) for ts in tsdata["totim"]], + [4 * pi * k11 * b * (initial_head - h) / q for h in tsdata[name]], + fmt[name], + label=obsnames[name], + markerfacecolor="none", + ) + + for name in analytical: + q = ana_prop[name][3] + b = ana_prop[name][5] # [pump, radius, sat_thick, model_bottom] + if solve_analytical_solution: + ana_times = ana_prop[name][0] + else: + ana_times = analytical_time - n = len(analytical[name]) - time_sy = [k11 * b * ts / (sy * r * r) for ts in ana_times[:n]] - ana = [4 * pi * k11 * b * s / q for s in analytical[name]] - ax.plot(time_sy, ana, fmt[name], label=obsnames[name]) + n = len(analytical[name]) + time_sy = [k11 * b * ts / (sy * r * r) for ts in ana_times[:n]] + ana = [4 * pi * k11 * b * s / q for s in analytical[name]] + ax.plot(time_sy, ana, fmt[name], label=obsnames[name]) - fs.graph_legend(ax) + styles.graph_legend(ax) - fig.tight_layout() + fig.tight_layout() - if config.plotSave: - fpth = os.path.join( - "..", - "figures", - "{}-{}{}".format(sim_name, obs_fig, config.figure_ext), - ) - fig.savefig(fpth) + if plot_save: + fpth = os.path.join( + "..", + "figures", + "{}-{}{}".format(sim_name, obs_fig, ".png"), + ) + fig.savefig(fpth) # Function to plot the model radial bands. def plot_grid(verbose=False): - fs = USGSFigure(figure_type="map", verbose=verbose) - - # Print all radial bands - fig, axs = plt.subplots(nrows=1, ncols=2, figsize=(6.4, 3.1)) - # fig, axs = plt.subplots(nrows=1, ncols=2, figsize=(10, 4.5)) - ax = axs[0] - - max_rad = radius_outer[-1] - max_rad = max_rad + (max_rad * 0.1) - ax.set_xlim(-max_rad, max_rad) - ax.set_ylim(-max_rad, max_rad) - ax.set_aspect("equal", adjustable="box") - - circle_center = (0.0, 0.0) - for r in radius_outer: - circle = Circle(circle_center, r, color="black", fill=False, lw=0.3) - ax.add_artist(circle) - - ax.set_xlabel("x-position (ft)") - ax.set_ylabel("y-position (ft)") - ax.annotate( - "A", - (-0.11, 1.02), - xycoords="axes fraction", - fontweight="black", - fontsize="xx-large", - ) + with styles.USGSMap() as fs: + # Print all radial bands + fig, axs = plt.subplots(nrows=1, ncols=2, figsize=(6.4, 3.1)) + # fig, axs = plt.subplots(nrows=1, ncols=2, figsize=(10, 4.5)) + ax = axs[0] + + max_rad = radius_outer[-1] + max_rad = max_rad + (max_rad * 0.1) + ax.set_xlim(-max_rad, max_rad) + ax.set_ylim(-max_rad, max_rad) + ax.set_aspect("equal", adjustable="box") + + circle_center = (0.0, 0.0) + for r in radius_outer: + circle = Circle(circle_center, r, color="black", fill=False, lw=0.3) + ax.add_artist(circle) + + ax.set_xlabel("x-position (ft)") + ax.set_ylabel("y-position (ft)") + ax.annotate( + "A", + (-0.11, 1.02), + xycoords="axes fraction", + fontweight="black", + fontsize="xx-large", + ) - # Print first 5 radial bands - nband = 5 - ax = axs[1] + # Print first 5 radial bands + nband = 5 + ax = axs[1] - radius_subset = radius_outer[:nband] - max_rad = radius_subset[-1] - max_rad = max_rad + (max_rad * 0.3) + radius_subset = radius_outer[:nband] + max_rad = radius_subset[-1] + max_rad = max_rad + (max_rad * 0.3) - ax.set_xlim(-max_rad, max_rad) - ax.set_ylim(-max_rad, max_rad) - ax.set_aspect("equal", adjustable="box") + ax.set_xlim(-max_rad, max_rad) + ax.set_ylim(-max_rad, max_rad) + ax.set_aspect("equal", adjustable="box") - circle_center = (0.0, 0.0) + circle_center = (0.0, 0.0) - r = radius_subset[0] - circle = Circle(circle_center, r, color="red", label="Well") - ax.add_artist(circle) - for r in radius_subset: - circle = Circle(circle_center, r, color="black", lw=1, fill=False) + r = radius_subset[0] + circle = Circle(circle_center, r, color="red", label="Well") ax.add_artist(circle) + for r in radius_subset: + circle = Circle(circle_center, r, color="black", lw=1, fill=False) + ax.add_artist(circle) + + ax.set_xlabel("x-position (ft)") + ax.set_ylabel("y-position (ft)") + + ax.annotate( + "B", + (-0.06, 1.02), + xycoords="axes fraction", + fontweight="black", + fontsize="xx-large", + ) - ax.set_xlabel("x-position (ft)") - ax.set_ylabel("y-position (ft)") - - ax.annotate( - "B", - (-0.06, 1.02), - xycoords="axes fraction", - fontweight="black", - fontsize="xx-large", - ) - - fs.graph_legend(ax) + styles.graph_legend(ax) - fig.tight_layout() + fig.tight_layout() - # save figure - if config.plotSave: - fpth = os.path.join( - "..", "figures", "{}-grid{}".format(sim_name, config.figure_ext) - ) - fig.savefig(fpth) - return + if plot_show: + plt.show() + if plot_save: + fpth = os.path.join("..", "figures", "{}-grid{}".format(sim_name, ".png")) + fig.savefig(fpth) # Function to plot the model results. def plot_results(silent=True): - if not config.plotModel: + if not plot: return if silent: @@ -1036,57 +1960,42 @@ def plot_results(silent=True): else: verbosity_level = 1 - sim_ws = os.path.join(ws, sim_name) + sim_ws = os.path.join(workspace, sim_name) sim = flopy.mf6.MFSimulation.load( sim_name=sim_name, sim_ws=sim_ws, verbosity_level=verbosity_level ) verbose = not silent + # If True, solves the Neuman 1974 analytical model (very slow) + # else uses stored results from solving the Neuman 1974 analytical model + analytical = False - if config.plotModel: - plot_grid(verbose) - plot_ts( - sim, verbose, solve_analytical_solution=solve_analytical_solution - ) - return + plot_grid(verbose) + plot_ts(sim, verbose, solve_analytical_solution=analytical) -# Function that wraps all of the steps for the Axisymmetric model -# -# 1. build_model, -# 2. write_model, -# 3. run_model, and -# 4. plot_results. +# - + +# ### Running the example # +# Define and invoke a function to run the example scenario, then plot results. -def simulation(silent=True): +# + +def scenario(silent=True): # key = list(parameters.keys())[idx] # params = parameters[key].copy() + sim = build_models(sim_name) + if write: + write_models(sim, silent=silent) + if run: + run_models(sim, silent=silent) - sim = build_model(sim_name) - - write_model(sim, silent=silent) - - success = run_model(sim, silent=silent) - assert success, "could not run...{}".format(sim_name) - - -# nosetest - exclude block from this nosetest to the next nosetest -def test_and_plot(): - simulation(silent=False) - plot_results(silent=False) - return - - -# nosetest end - - -if __name__ == "__main__": - # ### Axisymmetric Example - # MF6 Axisymmetric Model - simulation() +# MF6 Axisymmetric Model +scenario() +if plot: # Solve analytical and plot results with MF6 results plot_results() +# - diff --git a/scripts/ex-gwf-sagehen.py b/scripts/ex-gwf-sagehen.py index 555d1cd5c..aa3655423 100644 --- a/scripts/ex-gwf-sagehen.py +++ b/scripts/ex-gwf-sagehen.py @@ -3,50 +3,44 @@ # This script reproduces example 1 in the UZF1 Techniques and Methods # (Niswonger et al., 2006). -# ### MODFLOW 6 Sagehen Problem Setup - -# Append to system path to include the common subdirectory +# ### Initial setup +# +# Import dependencies, define the example name and workspace, and read settings from environment variables. +# + import os -import sys +import pathlib as pl -sys.path.append(os.path.join("..", "common")) -import config import flopy -import flopy.utils.binaryfile as bf import matplotlib.pyplot as plt import numpy as np import pandas as pd -import sfr_uzf_mvr_support_funcs as sageBld -from figspecs import USGSFigure - -# Imports - - -sys.path.append(os.path.join("..", "data", "ex-gwf-sagehen")) -import sfr_static as sfrDat +import pooch +from flopy.plot.styles import styles +from modflow_devtools.misc import get_env, timed -mf6exe = "mf6" -exe_name_mf = "mf2005" -exe_name_mt = "mt3dms" - -# Set figure properties specific to this problem - -figure_size = (6, 6) -figure_size_ts = (6, 3) +# Example name and base workspace +example_name = "ex-gwf-sagehen" +workspace = pl.Path("../examples") -# Base simulation and model name and workspace +# Settings from environment variables +write = get_env("WRITE", True) +run = get_env("RUN", True) +plot = get_env("PLOT", True) +plot_show = get_env("PLOT_SHOW", True) +plot_save = get_env("PLOT_SAVE", True) +# - -ws = config.base_ws -example_name = "ex-gwf-sagehen" +# ### Define parameters +# +# Define model units, parameters and other settings. +# + # Model units - length_units = "meters" time_units = "days" -# Table Sagehen Model Parameters - +# Model parameters nlay = 1 # Number of layers in parent model nrow = 73 # Number of rows in parent model ncol = 81 # Number of columns in parent model @@ -64,9 +58,6 @@ sfrdum3 = 3 # Width of stream reaches ($m$) sfrdum4 = 1 # Streambed thickness ($m$) - -# Additional model input preparation - # Time related variables num_ts = 399 perlen = [1] * num_ts @@ -75,12 +66,27 @@ tsmult = [1.0] * num_ts # from mf-nwt .dis file -dat_pth = os.path.join(config.data_ws, example_name) -top = np.loadtxt(os.path.join(dat_pth, "top1.txt")) -bot1 = np.loadtxt(os.path.join(dat_pth, "bot1.txt")) +fname = pooch.retrieve( + url=f"https://github.com/MODFLOW-USGS/modflow6-examples/raw/master/data/{example_name}/top1.txt", + known_hash="md5:a93be6cf74bf376f696fc2fbcc316aea", +) +top = np.loadtxt(fname) +fname = pooch.retrieve( + url=f"https://github.com/MODFLOW-USGS/modflow6-examples/raw/master/data/{example_name}/bot1.txt", + known_hash="md5:6503e01167875bd257479a2941d1d586", +) +bot1 = np.loadtxt(fname) # from mf-nwt .bas file -idomain1 = np.loadtxt(os.path.join(dat_pth, "ibnd1.txt")) -strt = np.loadtxt(os.path.join(dat_pth, "strt1.txt")) +fname = pooch.retrieve( + url=f"https://github.com/MODFLOW-USGS/modflow6-examples/raw/master/data/{example_name}/ibnd1.txt", + known_hash="md5:7b33e2fba54eae694171c94c75e13d2e", +) +idomain1 = np.loadtxt(fname) +fname = pooch.retrieve( + url=f"https://github.com/MODFLOW-USGS/modflow6-examples/raw/master/data/{example_name}/strt1.txt", + known_hash="md5:d49e24ec920472380787fd27c528123f", +) +strt = np.loadtxt(fname) # peel out locations of negative values for setting constant head data tmp1 = np.where(idomain1 < 0) listOfChdCoords = list(zip(np.zeros_like(tmp1[0]), tmp1[0], tmp1[1])) @@ -95,9 +101,21 @@ idomain = np.abs(idomain1) # from mf-nwt .upw file -k11 = np.loadtxt(os.path.join(dat_pth, "kh1.txt")) -sy = np.loadtxt(os.path.join(dat_pth, "sy1.txt")) -k33 = np.loadtxt(os.path.join(dat_pth, "kv1.txt")) +fname = pooch.retrieve( + url=f"https://github.com/MODFLOW-USGS/modflow6-examples/raw/master/data/{example_name}/kh1.txt", + known_hash="md5:50c0a5bfd79b1c9d0e0900540c19d6cc", +) +k11 = np.loadtxt(fname) +fname = pooch.retrieve( + url=f"https://github.com/MODFLOW-USGS/modflow6-examples/raw/master/data/{example_name}/sy1.txt", + known_hash="md5:6bc38fd082875633686732f34ba3e18b", +) +sy = np.loadtxt(fname) +fname = pooch.retrieve( + url=f"https://github.com/MODFLOW-USGS/modflow6-examples/raw/master/data/{example_name}/kv1.txt", + known_hash="md5:7b6685bf35f1150bef553f81c2dfb2cb", +) +k33 = np.loadtxt(fname) icelltype = 1 # Water table resides in layer 1 iconvert = np.ones_like(strt) @@ -108,16 +126,1572 @@ # #### Prepping input for SFR package # Get package_data information +orig_seg = [ + (1, 1, 9, 0, 0.00, 0.0, 0.0, 0, 0.04, "origSeg1"), + (2, 1, 10, 0, 0.00, 0.0, 0.0, 0, 0.04, "origSeg2"), + (3, 1, 9, 0, 0.00, 0.0, 0.0, 0, 0.04, "origSeg3"), + (4, 1, 11, 0, 0.00, 0.0, 0.0, 0, 0.04, "origSeg4"), + (5, 1, 15, 0, 0.00, 0.0, 0.0, 0, 0.04, "origSeg5"), + (6, 1, 13, 0, 0.00, 0.0, 0.0, 0, 0.04, "origSeg6"), + (7, 1, 12, 0, 0.00, 0.0, 0.0, 0, 0.04, "origSeg7"), + (8, 1, 14, 0, 0.00, 0.0, 0.0, 0, 0.04, "origSeg8"), + (9, 1, 10, 0, 0.00, 0.0, 0.0, 0, 0.04, "origSeg9"), + (10, 1, 11, 0, 0.00, 0.0, 0.0, 0, 0.04, "origSeg10"), + (11, 1, 12, 0, 0.00, 0.0, 0.0, 0, 0.04, "origSeg11"), + (12, 1, 13, 0, 0.00, 0.0, 0.0, 0, 0.04, "origSeg12"), + (13, 1, 14, 0, 0.00, 0.0, 0.0, 0, 0.04, "origSeg13"), + (14, 1, 15, 0, 0.00, 0.0, 0.0, 0, 0.04, "origSeg14"), + (15, 1, 0, 0, 0.00, 0.0, 0.0, 0, 0.04, "origSeg15"), +] + +orig_rch = [ + (1, 45, 9, 1, 1), + (1, 44, 9, 1, 2), + (1, 43, 9, 1, 3), + (1, 43, 10, 1, 4), + (1, 42, 10, 1, 5), + (1, 42, 11, 1, 6), + (1, 42, 12, 1, 7), + (1, 41, 12, 1, 8), + (1, 41, 13, 1, 9), + (1, 40, 13, 1, 10), + (1, 40, 14, 1, 11), + (1, 39, 14, 1, 12), + (1, 39, 15, 1, 13), + (1, 39, 16, 1, 14), + (1, 39, 17, 1, 15), + (1, 38, 17, 1, 16), + (1, 38, 18, 1, 17), + (1, 38, 19, 1, 18), + (1, 38, 20, 1, 19), + (1, 38, 21, 1, 20), + (1, 38, 22, 1, 21), + (1, 38, 23, 1, 22), + (1, 37, 24, 1, 23), + (1, 37, 25, 1, 24), + (1, 36, 25, 1, 25), + (1, 36, 26, 1, 26), + (1, 35, 26, 1, 27), + (1, 35, 27, 1, 28), + (1, 36, 28, 1, 29), + (1, 36, 29, 1, 30), + (1, 36, 30, 1, 31), + (1, 36, 31, 1, 32), + (1, 36, 32, 1, 33), + (1, 35, 32, 1, 34), + (1, 35, 33, 1, 35), + (1, 34, 33, 1, 36), + (1, 34, 34, 1, 37), + (1, 33, 34, 1, 38), + (1, 33, 35, 1, 39), + (1, 32, 35, 1, 40), + (1, 32, 36, 1, 41), + (1, 32, 37, 1, 42), + (1, 53, 37, 2, 1), + (1, 52, 37, 2, 2), + (1, 52, 38, 2, 3), + (1, 51, 38, 2, 4), + (1, 50, 38, 2, 5), + (1, 50, 39, 2, 6), + (1, 49, 39, 2, 7), + (1, 48, 39, 2, 8), + (1, 47, 39, 2, 9), + (1, 47, 40, 2, 10), + (1, 46, 40, 2, 11), + (1, 45, 40, 2, 12), + (1, 45, 39, 2, 13), + (1, 44, 39, 2, 14), + (1, 43, 38, 2, 15), + (1, 42, 38, 2, 16), + (1, 41, 38, 2, 17), + (1, 40, 38, 2, 18), + (1, 40, 39, 2, 19), + (1, 39, 39, 2, 20), + (1, 38, 39, 2, 21), + (1, 37, 40, 2, 22), + (1, 36, 40, 2, 23), + (1, 36, 41, 2, 24), + (1, 35, 41, 2, 25), + (1, 34, 41, 2, 26), + (1, 33, 41, 2, 27), + (1, 32, 41, 2, 28), + (1, 31, 42, 2, 29), + (1, 31, 33, 3, 1), + (1, 31, 34, 3, 2), + (1, 31, 35, 3, 3), + (1, 31, 36, 3, 4), + (1, 31, 37, 3, 5), + (1, 48, 48, 4, 1), + (1, 47, 48, 4, 2), + (1, 46, 48, 4, 3), + (1, 46, 47, 4, 4), + (1, 45, 47, 4, 5), + (1, 44, 47, 4, 6), + (1, 43, 47, 4, 7), + (1, 42, 47, 4, 8), + (1, 41, 47, 4, 9), + (1, 41, 48, 4, 10), + (1, 40, 48, 4, 11), + (1, 39, 48, 4, 12), + (1, 38, 47, 4, 13), + (1, 37, 47, 4, 14), + (1, 36, 48, 4, 15), + (1, 35, 48, 4, 16), + (1, 35, 49, 4, 17), + (1, 34, 49, 4, 18), + (1, 34, 50, 4, 19), + (1, 33, 50, 4, 20), + (1, 55, 72, 5, 1), + (1, 54, 72, 5, 2), + (1, 53, 72, 5, 3), + (1, 52, 72, 5, 4), + (1, 51, 72, 5, 5), + (1, 50, 73, 5, 6), + (1, 49, 73, 5, 7), + (1, 48, 73, 5, 8), + (1, 48, 74, 5, 9), + (1, 47, 74, 5, 10), + (1, 46, 75, 5, 11), + (1, 45, 75, 5, 12), + (1, 45, 76, 5, 13), + (1, 44, 76, 5, 14), + (1, 45, 62, 6, 1), + (1, 44, 62, 6, 2), + (1, 43, 62, 6, 3), + (1, 43, 63, 6, 4), + (1, 42, 63, 6, 5), + (1, 41, 63, 6, 6), + (1, 40, 63, 6, 7), + (1, 24, 55, 7, 1), + (1, 25, 55, 7, 2), + (1, 25, 56, 7, 3), + (1, 26, 56, 7, 4), + (1, 27, 56, 7, 5), + (1, 28, 57, 7, 6), + (1, 29, 57, 7, 7), + (1, 30, 57, 7, 8), + (1, 31, 57, 7, 9), + (1, 32, 57, 7, 10), + (1, 33, 57, 7, 11), + (1, 33, 58, 7, 12), + (1, 34, 58, 7, 13), + (1, 34, 59, 7, 14), + (1, 35, 59, 7, 15), + (1, 36, 59, 7, 16), + (1, 37, 60, 7, 17), + (1, 23, 71, 8, 1), + (1, 24, 71, 8, 2), + (1, 25, 71, 8, 3), + (1, 26, 71, 8, 4), + (1, 27, 72, 8, 5), + (1, 27, 73, 8, 6), + (1, 28, 73, 8, 7), + (1, 29, 73, 8, 8), + (1, 30, 73, 8, 9), + (1, 31, 73, 8, 10), + (1, 32, 73, 8, 11), + (1, 33, 73, 8, 12), + (1, 34, 73, 8, 13), + (1, 34, 74, 8, 14), + (1, 35, 74, 8, 15), + (1, 36, 74, 8, 16), + (1, 36, 73, 8, 17), + (1, 37, 73, 8, 18), + (1, 38, 72, 8, 19), + (1, 39, 72, 8, 20), + (1, 40, 72, 8, 21), + (1, 41, 72, 8, 22), + (1, 42, 72, 8, 23), + (1, 42, 73, 8, 24), + (1, 31, 38, 9, 1), + (1, 31, 39, 9, 2), + (1, 31, 40, 9, 3), + (1, 31, 41, 9, 4), + (1, 31, 42, 9, 5), + (1, 30, 42, 9, 6), + (1, 30, 43, 10, 1), + (1, 30, 44, 10, 2), + (1, 29, 44, 10, 3), + (1, 29, 45, 10, 4), + (1, 29, 46, 10, 5), + (1, 29, 47, 10, 6), + (1, 30, 47, 10, 7), + (1, 30, 48, 10, 8), + (1, 31, 49, 10, 9), + (1, 32, 50, 10, 10), + (1, 32, 51, 11, 1), + (1, 33, 52, 11, 2), + (1, 33, 53, 11, 3), + (1, 34, 53, 11, 4), + (1, 34, 54, 11, 5), + (1, 35, 54, 11, 6), + (1, 35, 55, 11, 7), + (1, 35, 56, 11, 8), + (1, 36, 57, 11, 9), + (1, 36, 58, 11, 10), + (1, 36, 59, 11, 11), + (1, 37, 59, 11, 12), + (1, 37, 60, 11, 13), + (1, 38, 60, 11, 14), + (1, 38, 61, 12, 1), + (1, 38, 62, 12, 2), + (1, 38, 63, 12, 3), + (1, 39, 63, 12, 4), + (1, 39, 64, 13, 1), + (1, 39, 65, 13, 2), + (1, 40, 65, 13, 3), + (1, 40, 66, 13, 4), + (1, 40, 67, 13, 5), + (1, 40, 68, 13, 6), + (1, 41, 69, 13, 7), + (1, 41, 70, 13, 8), + (1, 42, 71, 13, 9), + (1, 42, 72, 13, 10), + (1, 42, 73, 13, 11), + (1, 42, 73, 14, 1), + (1, 43, 73, 14, 2), + (1, 43, 74, 14, 3), + (1, 43, 75, 14, 4), + (1, 44, 75, 14, 5), + (1, 44, 76, 14, 6), + (1, 44, 77, 15, 1), + (1, 44, 78, 15, 2), + (1, 44, 79, 15, 3), + (1, 45, 79, 15, 4), +] + +# These are zero based +sfrcells = [ + (0, 44, 8), + (0, 43, 8), + (0, 42, 8), + (0, 42, 9), + (0, 41, 9), + (0, 41, 10), + (0, 41, 11), + (0, 40, 11), + (0, 40, 12), + (0, 39, 12), + (0, 39, 13), + (0, 38, 13), + (0, 38, 14), + (0, 38, 15), + (0, 38, 16), + (0, 37, 16), + (0, 37, 17), + (0, 37, 18), + (0, 37, 19), + (0, 37, 20), + (0, 37, 21), + (0, 37, 22), + (0, 36, 23), + (0, 36, 24), + (0, 35, 24), + (0, 35, 25), + (0, 34, 25), + (0, 34, 26), + (0, 35, 27), + (0, 35, 28), + (0, 35, 29), + (0, 35, 30), + (0, 35, 31), + (0, 34, 31), + (0, 34, 32), + (0, 33, 32), + (0, 33, 33), + (0, 32, 33), + (0, 32, 34), + (0, 31, 34), + (0, 31, 35), + (0, 31, 36), + (0, 52, 36), + (0, 51, 36), + (0, 51, 37), + (0, 50, 37), + (0, 49, 37), + (0, 49, 38), + (0, 48, 38), + (0, 47, 38), + (0, 46, 38), + (0, 46, 39), + (0, 45, 39), + (0, 44, 39), + (0, 44, 38), + (0, 43, 38), + (0, 42, 37), + (0, 41, 37), + (0, 40, 37), + (0, 39, 37), + (0, 39, 38), + (0, 38, 38), + (0, 37, 38), + (0, 36, 39), + (0, 35, 39), + (0, 35, 40), + (0, 34, 40), + (0, 33, 40), + (0, 32, 40), + (0, 31, 40), + (0, 30, 41), + (0, 30, 32), + (0, 30, 33), + (0, 30, 34), + (0, 30, 35), + (0, 30, 36), + (0, 47, 47), + (0, 46, 47), + (0, 45, 47), + (0, 45, 46), + (0, 44, 46), + (0, 43, 46), + (0, 42, 46), + (0, 41, 46), + (0, 40, 46), + (0, 40, 47), + (0, 39, 47), + (0, 38, 47), + (0, 37, 46), + (0, 36, 46), + (0, 35, 47), + (0, 34, 47), + (0, 34, 48), + (0, 33, 48), + (0, 33, 49), + (0, 32, 49), + (0, 54, 71), + (0, 53, 71), + (0, 52, 71), + (0, 51, 71), + (0, 50, 71), + (0, 49, 72), + (0, 48, 72), + (0, 47, 72), + (0, 47, 73), + (0, 46, 73), + (0, 45, 74), + (0, 44, 74), + (0, 44, 75), + (0, 43, 75), + (0, 44, 61), + (0, 43, 61), + (0, 42, 61), + (0, 42, 62), + (0, 41, 62), + (0, 40, 62), + (0, 39, 62), + (0, 23, 54), + (0, 24, 54), + (0, 24, 55), + (0, 25, 55), + (0, 26, 55), + (0, 27, 56), + (0, 28, 56), + (0, 29, 56), + (0, 30, 56), + (0, 31, 56), + (0, 32, 56), + (0, 32, 57), + (0, 33, 57), + (0, 33, 58), + (0, 34, 58), + (0, 35, 58), + (0, 36, 59), + (0, 22, 70), + (0, 23, 70), + (0, 24, 70), + (0, 25, 70), + (0, 26, 71), + (0, 26, 72), + (0, 27, 72), + (0, 28, 72), + (0, 29, 72), + (0, 30, 72), + (0, 31, 72), + (0, 32, 72), + (0, 33, 72), + (0, 33, 73), + (0, 34, 73), + (0, 35, 73), + (0, 35, 72), + (0, 36, 72), + (0, 37, 71), + (0, 38, 71), + (0, 39, 71), + (0, 40, 71), + (0, 41, 71), + (0, 41, 72), + (0, 30, 37), + (0, 30, 38), + (0, 30, 39), + (0, 30, 40), + (0, 30, 41), + (0, 29, 41), + (0, 29, 42), + (0, 29, 43), + (0, 28, 43), + (0, 28, 44), + (0, 28, 45), + (0, 28, 46), + (0, 29, 46), + (0, 29, 47), + (0, 30, 48), + (0, 31, 49), + (0, 31, 50), + (0, 32, 51), + (0, 32, 52), + (0, 33, 52), + (0, 33, 53), + (0, 34, 53), + (0, 34, 54), + (0, 34, 55), + (0, 35, 56), + (0, 35, 57), + (0, 35, 58), + (0, 36, 58), + (0, 36, 59), + (0, 37, 59), + (0, 37, 60), + (0, 37, 61), + (0, 37, 62), + (0, 38, 62), + (0, 38, 63), + (0, 38, 64), + (0, 39, 64), + (0, 39, 65), + (0, 39, 66), + (0, 39, 67), + (0, 40, 68), + (0, 40, 69), + (0, 41, 70), + (0, 41, 71), + (0, 41, 72), + (0, 41, 72), + (0, 42, 72), + (0, 42, 73), + (0, 42, 74), + (0, 43, 74), + (0, 43, 75), + (0, 43, 76), + (0, 43, 77), + (0, 43, 78), + (0, 44, 78), +] + +rlen = [ + 90.0, + 90.0, + 75.0, + 75.0, + 75.0, + 90.0, + 75.0, + 75.0, + 75.0, + 75.0, + 75.0, + 75.0, + 90.0, + 90.0, + 60.0, + 30.0, + 102.0, + 90.0, + 90.0, + 90.0, + 102.0, + 102.0, + 102.0, + 72.0, + 30.0, + 72.0, + 30.0, + 90.0, + 102.0, + 90.0, + 90.0, + 102.0, + 30.0, + 72.0, + 30.0, + 72.0, + 30.0, + 60.0, + 72.0, + 30.0, + 102.0, + 90.0, + 90.0, + 72.0, + 30.0, + 102.0, + 60.0, + 30.0, + 90.0, + 102.0, + 60.0, + 30.0, + 90.0, + 30.0, + 60.0, + 102.0, + 102.0, + 90.0, + 102.0, + 30.0, + 60.0, + 102.0, + 102.0, + 102.0, + 60.0, + 30.0, + 102.0, + 90.0, + 90.0, + 102.0, + 114.0, + 90.0, + 90.0, + 102.0, + 102.0, + 90.0, + 90.0, + 102.0, + 30.0, + 60.0, + 90.0, + 102.0, + 90.0, + 90.0, + 60.0, + 30.0, + 90.0, + 90.0, + 90.0, + 90.0, + 102.0, + 30.0, + 60.0, + 72.0, + 30.0, + 114.0, + 90.0, + 90.0, + 90.0, + 102.0, + 102.0, + 102.0, + 102.0, + 30.0, + 60.0, + 102.0, + 102.0, + 30.0, + 72.0, + 30.0, + 90.0, + 102.0, + 30.0, + 60.0, + 102.0, + 90.0, + 102.0, + 60.0, + 30.0, + 60.0, + 102.0, + 90.0, + 90.0, + 90.0, + 90.0, + 90.0, + 102.0, + 72.0, + 30.0, + 72.0, + 30.0, + 102.0, + 90.0, + 114.0, + 90.0, + 102.0, + 90.0, + 102.0, + 102.0, + 30.0, + 102.0, + 90.0, + 90.0, + 90.0, + 90.0, + 90.0, + 30.0, + 60.0, + 90.0, + 30.0, + 60.0, + 102.0, + 90.0, + 90.0, + 90.0, + 90.0, + 30.0, + 30.0, + 90.0, + 90.0, + 90.0, + 90.0, + 72.0, + 30.0, + 90.0, + 60.0, + 30.0, + 90.0, + 90.0, + 30.0, + 60.0, + 102.0, + 114.0, + 114.0, + 90.0, + 102.0, + 30.0, + 72.0, + 60.0, + 30.0, + 102.0, + 102.0, + 114.0, + 90.0, + 60.0, + 30.0, + 72.0, + 30.0, + 102.0, + 102.0, + 30.0, + 60.0, + 120.0, + 60.0, + 30.0, + 90.0, + 90.0, + 90.0, + 90.0, + 114.0, + 102.0, + 90.0, + 30.0, + 30.0, + 30.0, + 102.0, + 60.0, + 30.0, + 90.0, + 90.0, + 90.0, + 60.0, + 30.0, +] + +rgrd = [ + 0.150, + 0.120, + 0.180, + 0.160, + 0.130, + 0.111, + 0.047, + 0.060, + 0.040, + 0.100, + 0.227, + 0.090, + 0.042, + 0.064, + 0.083, + 0.081, + 0.062, + 0.065, + 0.089, + 0.097, + 0.141, + 0.186, + 0.217, + 0.203, + 0.206, + 0.206, + 0.144, + 0.147, + 0.135, + 0.129, + 0.124, + 0.136, + 0.162, + 0.147, + 0.157, + 0.147, + 0.115, + 0.117, + 0.111, + 0.120, + 0.099, + 0.073, + 0.037, + 0.038, + 0.060, + 0.048, + 0.024, + 0.029, + 0.032, + 0.028, + 0.024, + 0.029, + 0.033, + 0.038, + 0.032, + 0.038, + 0.051, + 0.047, + 0.037, + 0.063, + 0.063, + 0.049, + 0.069, + 0.077, + 0.063, + 0.045, + 0.037, + 0.043, + 0.048, + 0.054, + 0.065, + 0.067, + 0.091, + 0.091, + 0.071, + 0.073, + 0.021, + 0.031, + 0.045, + 0.033, + 0.029, + 0.042, + 0.075, + 0.103, + 0.092, + 0.095, + 0.087, + 0.083, + 0.094, + 0.102, + 0.093, + 0.081, + 0.099, + 0.077, + 0.057, + 0.056, + 0.044, + 0.050, + 0.075, + 0.076, + 0.074, + 0.074, + 0.071, + 0.072, + 0.056, + 0.060, + 0.048, + 0.043, + 0.049, + 0.039, + 0.042, + 0.056, + 0.081, + 0.071, + 0.068, + 0.068, + 0.068, + 0.044, + 0.078, + 0.071, + 0.051, + 0.054, + 0.056, + 0.056, + 0.050, + 0.048, + 0.038, + 0.022, + 0.049, + 0.059, + 0.043, + 0.043, + 0.045, + 0.049, + 0.042, + 0.031, + 0.016, + 0.010, + 0.012, + 0.015, + 0.012, + 0.011, + 0.022, + 0.044, + 0.056, + 0.060, + 0.114, + 0.100, + 0.067, + 0.086, + 0.127, + 0.141, + 0.118, + 0.100, + 0.083, + 0.087, + 0.100, + 0.067, + 0.056, + 0.083, + 0.100, + 0.076, + 0.045, + 0.020, + 0.053, + 0.042, + 0.038, + 0.047, + 0.047, + 0.057, + 0.040, + 0.032, + 0.045, + 0.053, + 0.042, + 0.049, + 0.094, + 0.085, + 0.036, + 0.027, + 0.030, + 0.033, + 0.024, + 0.017, + 0.025, + 0.021, + 0.015, + 0.010, + 0.010, + 0.012, + 0.018, + 0.022, + 0.017, + 0.019, + 0.010, + 0.013, + 0.022, + 0.017, + 0.021, + 0.043, + 0.044, + 0.038, + 0.050, + 0.033, + 0.021, + 0.020, + 0.024, + 0.029, + 0.020, + 0.011, + 0.024, + 0.033, + 0.022, +] + +rtp = [ + 2458.0, + 2449.0, + 2337.0, + 2416.0, + 2413.0, + 2397.0, + 2393.0, + 2390.0, + 2386.0, + 2384.0, + 2367.0, + 2360.0, + 2355.0, + 2351.0, + 2344.0, + 2341.0, + 2335.0, + 2331.0, + 2323.0, + 2315.0, + 2305.0, + 2287.0, + 2267.0, + 2246.0, + 2239.0, + 2225.0, + 2218.0, + 2209.0, + 2195.0, + 2183.0, + 2171.0, + 2160.0, + 2149.0, + 2141.0, + 2134.0, + 2125.0, + 2119.0, + 2114.0, + 2106.0, + 2101.0, + 2092.0, + 2085.0, + 2146.0, + 2143.0, + 2141.0, + 2136.0, + 2134.0, + 2133.0, + 2131.0, + 2128.0, + 2126.0, + 2125.0, + 2123.0, + 2121.0, + 2119.0, + 2117.0, + 2112.0, + 2107.0, + 2103.0, + 2101.0, + 2096.0, + 2093.0, + 2087.0, + 2079.0, + 2073.0, + 2071.0, + 2068.0, + 2065.0, + 2060.0, + 2056.0, + 2048.0, + 2115.0, + 2109.0, + 2098.0, + 2091.0, + 2084.0, + 2112.0, + 2110.0, + 2108.0, + 2106.0, + 2104.0, + 2101.0, + 2096.0, + 2087.0, + 2079.0, + 2076.0, + 2069.0, + 2063.0, + 2054.0, + 2046.0, + 2035.0, + 2031.0, + 2026.0, + 2020.0, + 2017.0, + 2013.0, + 2000.0, + 1996.0, + 1991.0, + 1982.0, + 1976.0, + 1967.0, + 1961.0, + 1955.0, + 1953.0, + 1948.0, + 1942.0, + 1940.0, + 1937.0, + 1935.0, + 2003.0, + 1999.0, + 1994.0, + 1990.0, + 1985.0, + 1978.0, + 1972.0, + 2032.0, + 2030.0, + 2025.0, + 2021.0, + 2016.0, + 2011.0, + 2006.0, + 2001.0, + 1997.0, + 1992.0, + 1990.0, + 1989.0, + 1985.0, + 1983.0, + 1980.0, + 1976.0, + 1971.0, + 2051.0, + 2047.0, + 2045.0, + 2044.0, + 2043.0, + 2042.0, + 2041.0, + 2040.0, + 2039.0, + 2036.0, + 2031.0, + 2026.0, + 2022.0, + 2014.0, + 2010.0, + 2005.0, + 2001.0, + 1989.0, + 1976.0, + 1967.0, + 1958.0, + 1952.0, + 1945.0, + 1943.0, + 2076.0, + 2071.0, + 2061.0, + 2053.0, + 2048.0, + 2047.0, + 2041.0, + 2037.0, + 2036.0, + 2033.0, + 2029.0, + 2026.0, + 2023.0, + 2021.0, + 2017.0, + 2011.0, + 2006.0, + 2002.0, + 1998.0, + 1991.0, + 1988.0, + 1987.0, + 1985.0, + 1982.0, + 1978.0, + 1977.0, + 1975.0, + 1974.0, + 1973.0, + 1972.0, + 1970.0, + 1969.0, + 1968.0, + 1967.0, + 1966.0, + 1965.0, + 1964.0, + 1963.0, + 1961.0, + 1959.0, + 1958.0, + 1955.0, + 1949.0, + 1946.0, + 1943.0, + 1942.0, + 1941.0, + 1940.0, + 1938.0, + 1937.0, + 1935.0, + 1934.0, + 1933.0, + 1930.0, + 1929.0, +] + + +def get_sfrsegs(): + return orig_seg + + +def get_sfrrchs(): + return orig_rch + + +def get_sfrcells(): + return sfrcells + + +def get_sfrlen(): + return rlen + + +def get_rgrd(): + return rgrd + + +def get_rtp(): + return rtp + + +segs = get_sfrsegs() +rchs = get_sfrrchs() +sfrcells = get_sfrcells() +rlen = get_sfrlen() +rgrd = get_rgrd() +rtp = get_rtp() + +# Define some utility functions. + + +def gen_mf6_sfr_connections(orig_seg, orig_rch): + """ + Defining a function that builds the new MF6 SFR connection information using + original SFR input. This is a generalized function that can be used to + convert MF2K5-based model to the new MF6 format. Currently applied to the + Sagehen and modsim models + """ + conns = [] + for i in np.arange(0, len(orig_seg)): + tup = orig_seg[i] + segid = tup[0] + ioutseg = tup[2] + iupseg = tup[3] + + # Get all reaches associated with segment + # Find an element in a list of tuples + allrchs = [item for item in orig_rch if item[3] == segid] + + # Loop through allrchs and generate list of connections + for rchx in allrchs: + # rchx will be a tuple + upconn = [] + dnconn = [] + + if rchx[4] == 1: # checks if first rch of segment + # Collect all segs that dump to the current one (there may not + # be any) + dumpersegs = [item for item in orig_seg if item[2] == segid] + # For every seg that outflows to current, set last reach of it + # as an upstream connection + for dumper in dumpersegs: + dumper_seg_id = dumper[0] + rch_cnt = len( + [item for item in orig_rch if item[3] == dumper_seg_id] + ) + lastrch = [ + item + for item in orig_rch + if item[3] == dumper_seg_id and item[4] == rch_cnt + ] + idx = orig_rch.index(lastrch[0]) + upconn.append(int(idx)) + + # Current reach is the most upstream reach for current segment + if iupseg == 0: + pass + elif iupseg > 0: # Lake connections, signified with negative + # numbers, aren't handled here + iupseg_rchs = [item for item in orig_rch if item[3] == iupseg] + # Get the index of the last reach of the segement that was + # the upstream segment in the orig sfr file + idx = orig_rch.index(iupseg_rchs[len(iupseg_rchs) - 1]) # + upconn.append(idx) + + # Even if the first reach of a segement, it will have an outlet + # either the next reach in the segment, or first reach of + # outseg, which should be taken care of below + if len(allrchs) > 1: + idx = orig_rch.index(rchx) + # adjust idx for 0-based and increment to next item in list + dnconn.append(int(idx + 1) * -1) + + elif rchx[4] > 1 and not rchx[4] == len(allrchs): + # Current reach is 'interior' on the original segment and + # therefore should only have 1 upstream & 1 downstream segement + idx = orig_rch.index(rchx) + # B/c 0-based, idx will already be incremented by -1 + upconn.append(int(idx - 1)) + # adjust idx for 0-based and increment to next item in list + dnconn.append(int(idx + 1) * -1) # all downstream connections + # are negative in MF6 + + if rchx[4] == len(allrchs): + # If the last reach in a multi-reach segment, always need to + # account for the reach immediately upstream (single reach segs + # dealt with above), unless of course we're dealing with a + # single reach segment like in the case of a spillway from a lk + if len(allrchs) != 1: + idx = orig_rch.index(rchx) + # B/c 0-based, idx will already be incremented by -1 + upconn.append(int(idx - 1)) + + # Current reach is last reach in segment and may have multiple + # downstream connections, particular when dealing with + # diversions. + if ioutseg == 0: + pass + elif ioutseg > 0: # Lake connections, signified with + # negative numbers, aren't handled here + idnseg_rchs = [ + item for item in orig_rch if item[3] == ioutseg and item[4] == 1 + ] + idx = orig_rch.index(idnseg_rchs[0]) + # adjust idx for 0-based and increment to next item in list + dnconn.append(int(idx) * -1) + + # In addition to ioutseg, look for all segments that may have + # the current segment as their iupseg + possible_divs = [item for item in orig_seg if item[3] == rchx[3]] + for segx in possible_divs: + # Next, peel out all first reach for any segments listed in + # possible_divs + first_rchs = [ + item for item in orig_rch if item[3] == segx[0] and item[4] == 1 + ] + for firstx in first_rchs: + idx = orig_rch.index(firstx) + # adjust idx for 0-based & increment to nxt itm in list + dnconn.append(int(idx) * -1) + + # Append the collection of upconn & dnconn as an entry in a list + idx = orig_rch.index(rchx) + # Adjust current index for 0-based + conns.append([idx] + upconn + dnconn) + + return conns + + +def determine_runoff_conns_4mvr(elev_arr, ibnd, orig_rch, nrow, ncol): + # Get the sfr information stored in a companion script + sfr_dat = orig_rch.copy() + sfrlayout = np.zeros_like(ibnd) + for i, rchx in enumerate(sfr_dat): + row = rchx[1] + col = rchx[2] + sfrlayout[row - 1, col - 1] = i + 1 + + sfrlayout_new = sfrlayout.copy() + + stop_candidate = False + + for i in np.arange(0, nrow): + for j in np.arange(0, ncol): + # Check to ensure current cell is active + if ibnd[i, j] == 0: + continue + + # Check to make sure it is not a stream cell + if not sfrlayout[i, j] == 0: + continue + + # Recursively trace path by steepest decent back to a stream + curr_i = i + curr_j = j + + sfrlayout_conn_candidate_elev = 10000.0 + while True: + direc = 0 + min_elev = elev_arr[curr_i, curr_j] + + # Look straight left + if curr_j > 0: + if ( + not sfrlayout[curr_i, curr_j - 1] == 0 + and not ibnd[curr_i, curr_j - 1] == 0 + ): # Step in if neighbor is a stream cell + if elev_arr[curr_i, curr_j - 1] > 0 and ( + elev_arr[curr_i, curr_j - 1] < elev_arr[curr_i, curr_j] + and elev_arr[curr_i, curr_j - 1] + < sfrlayout_conn_candidate_elev + ): + sfrlayout_conn_candidate = sfrlayout[curr_i, curr_j - 1] + sfrlayout_conn_candidate_elev = elev_arr[curr_i, curr_j - 1] + stop_candidate = True + + elif ( + not elev_arr[curr_i, curr_j - 1] == 0 + and not ibnd[curr_i, curr_j - 1] == 0 + ): # Step here if neighbor is not an sfr cell + if ( + elev_arr[curr_i, curr_j - 1] < elev_arr[curr_i, curr_j] + and elev_arr[curr_i, curr_j - 1] < min_elev + ): + elevcm1 = elev_arr[curr_i, curr_j - 1] + min_elev = elevcm1 + direc = 2 + + # Look up and left + if curr_j > 0 and curr_i > 0: + if ( + not sfrlayout[curr_i - 1, curr_j - 1] == 0 + and not ibnd[curr_i - 1, curr_j - 1] == 0 + ): # Step in if neighbor is a stream cell + if elev_arr[curr_i - 1, curr_j - 1] > 0 and ( + elev_arr[curr_i - 1, curr_j - 1] < elev_arr[curr_i, curr_j] + and elev_arr[curr_i - 1, curr_j - 1] + < sfrlayout_conn_candidate_elev + ): + sfrlayout_conn_candidate = sfrlayout[curr_i - 1, curr_j - 1] + sfrlayout_conn_candidate_elev = elev_arr[ + curr_i - 1, curr_j - 1 + ] + stop_candidate = True + + elif ( + not elev_arr[curr_i - 1, curr_j - 1] == 0 + and not ibnd[curr_i - 1, curr_j - 1] == 0 + ): # Step here if neighbor is not an sfr cell + if ( + elev_arr[curr_i - 1, curr_j - 1] < elev_arr[curr_i, curr_j] + and elev_arr[curr_i - 1, curr_j - 1] < min_elev + ): + elevrm1cm1 = elev_arr[curr_i - 1, curr_j - 1] + min_elev = elevrm1cm1 + direc = 5 + + # Look straight right + if curr_j < ncol - 1: + if ( + not sfrlayout[curr_i, curr_j + 1] == 0 + and not ibnd[curr_i, curr_j + 1] == 0 + ): # Step in if neighbor is a stream cell + if elev_arr[curr_i, curr_j + 1] > 0 and ( + elev_arr[curr_i, curr_j + 1] < elev_arr[curr_i, curr_j] + and elev_arr[curr_i, curr_j + 1] + < sfrlayout_conn_candidate_elev + ): + sfrlayout_conn_candidate = sfrlayout[curr_i, curr_j + 1] + sfrlayout_conn_candidate_elev = elev_arr[curr_i, curr_j + 1] + stop_candidate = True + + elif ( + not elev_arr[curr_i, curr_j + 1] == 0 + and not ibnd[curr_i, curr_j + 1] == 0 + ): # Step here if neighbor is not an sfr cell + if ( + elev_arr[curr_i, curr_j + 1] < elev_arr[curr_i, curr_j] + and elev_arr[curr_i, curr_j + 1] < min_elev + ): + elevcm1 = elev_arr[curr_i, curr_j + 1] + min_elev = elevcm1 + direc = 4 + + # Look straight right and down + if curr_i < nrow - 1 and curr_j < ncol - 1: + if ( + not sfrlayout[curr_i + 1, curr_j + 1] == 0 + and not ibnd[curr_i + 1, curr_j + 1] == 0 + ): # Step in if neighbor is a stream cell + if elev_arr[curr_i + 1, curr_j + 1] > 0 and ( + elev_arr[curr_i + 1, curr_j + 1] < elev_arr[curr_i, curr_j] + and elev_arr[curr_i + 1, curr_j + 1] + < sfrlayout_conn_candidate_elev + ): + sfrlayout_conn_candidate = sfrlayout[curr_i + 1, curr_j + 1] + sfrlayout_conn_candidate_elev = elev_arr[ + curr_i + 1, curr_j + 1 + ] + stop_candidate = True + + elif ( + not elev_arr[curr_i + 1, curr_j + 1] == 0 + and not ibnd[curr_i + 1, curr_j + 1] == 0 + ): # Step here if neighbor is not an sfr cell + if ( + elev_arr[curr_i + 1, curr_j + 1] < elev_arr[curr_i, curr_j] + and elev_arr[curr_i + 1, curr_j + 1] < min_elev + ): + elevrp1cp1 = elev_arr[curr_i + 1, curr_j + 1] + min_elev = elevrp1cp1 + direc = 7 + + # Look straight up + if curr_i > 0: + if ( + not sfrlayout[curr_i - 1, curr_j] == 0 + and not ibnd[curr_i - 1, curr_j] == 0 + ): # Step in if neighbor is a stream cell + if elev_arr[curr_i - 1, curr_j] > 0 and ( + elev_arr[curr_i - 1, curr_j] < elev_arr[curr_i, curr_j] + and elev_arr[curr_i - 1, curr_j] + < sfrlayout_conn_candidate_elev + ): + sfrlayout_conn_candidate = sfrlayout[curr_i - 1, curr_j] + sfrlayout_conn_candidate_elev = elev_arr[curr_i - 1, curr_j] + stop_candidate = True + + elif ( + not elev_arr[curr_i - 1, curr_j] == 0 + and not ibnd[curr_i - 1, curr_j] == 0 + ): # Step here if neighbor is not an sfr cell + if ( + elev_arr[curr_i - 1, curr_j] < elev_arr[curr_i, curr_j] + and elev_arr[curr_i - 1, curr_j] < min_elev + ): + elevcm1 = elev_arr[curr_i - 1, curr_j] + min_elev = elevcm1 + direc = 3 + + # Look up and right + if curr_i > 0 and curr_j < ncol - 1: + if ( + not sfrlayout[curr_i - 1, curr_j + 1] == 0 + and not ibnd[curr_i - 1, curr_j + 1] == 0 + ): # Step in if neighbor is a stream cell + if elev_arr[curr_i - 1, curr_j + 1] > 0 and ( + elev_arr[curr_i - 1, curr_j + 1] < elev_arr[curr_i, curr_j] + and elev_arr[curr_i - 1, curr_j + 1] + < sfrlayout_conn_candidate_elev + ): + sfrlayout_conn_candidate = sfrlayout[curr_i - 1, curr_j + 1] + sfrlayout_conn_candidate_elev = elev_arr[ + curr_i - 1, curr_j + 1 + ] + stop_candidate = True + + elif ( + not elev_arr[curr_i - 1, curr_j + 1] == 0 + and not ibnd[curr_i - 1, curr_j + 1] == 0 + ): # Step here if neighbor is not an sfr cell + if ( + elev_arr[curr_i - 1, curr_j + 1] < elev_arr[curr_i, curr_j] + and elev_arr[curr_i - 1, curr_j + 1] < min_elev + ): + elevrm1cp1 = elev_arr[curr_i - 1, curr_j + 1] + min_elev = elevrm1cp1 + direc = 6 + + # Look straight down + if curr_i < nrow - 1: + if ( + not sfrlayout[curr_i + 1, curr_j] == 0 + and not ibnd[curr_i + 1, curr_j] == 0 + ): # Step in if neighbor is a stream cell + if elev_arr[curr_i + 1, curr_j] > 0 and ( + elev_arr[curr_i + 1, curr_j] < elev_arr[curr_i, curr_j] + and elev_arr[curr_i + 1, curr_j] + < sfrlayout_conn_candidate_elev + ): + sfrlayout_conn_candidate = sfrlayout[curr_i + 1, curr_j] + sfrlayout_conn_candidate_elev = elev_arr[curr_i + 1, curr_j] + stop_candidate = True + + elif ( + not elev_arr[curr_i + 1, curr_j] == 0 + and not ibnd[curr_i + 1, curr_j] == 0 + ): # Step here if neighbor is not an sfr cell + if ( + elev_arr[curr_i + 1, curr_j] < elev_arr[curr_i, curr_j] + and elev_arr[curr_i + 1, curr_j] < min_elev + ): + elevrp1 = elev_arr[curr_i + 1, curr_j] + min_elev = elevrp1 + direc = 1 + + # Look down and left + if curr_i < nrow - 1 and curr_j > 0: + if ( + not sfrlayout[curr_i + 1, curr_j - 1] == 0 + and not ibnd[curr_i + 1, curr_j - 1] == 0 + ): # Step in if neighbor is a stream cell + if elev_arr[curr_i + 1, curr_j - 1] > 0 and ( + elev_arr[curr_i + 1, curr_j - 1] < elev_arr[curr_i, curr_j] + and elev_arr[curr_i + 1, curr_j - 1] + < sfrlayout_conn_candidate_elev + ): + sfrlayout_conn_candidate = sfrlayout[curr_i + 1, curr_j - 1] + sfrlayout_conn_candidate_elev = elev_arr[ + curr_i + 1, curr_j - 1 + ] + stop_candidate = True + + elif ( + not elev_arr[curr_i + 1, curr_j - 1] == 0 + and not ibnd[curr_i + 1, curr_j - 1] == 0 + ): # Step here if neighbor is not an sfr cell + if ( + elev_arr[curr_i + 1, curr_j - 1] < elev_arr[curr_i, curr_j] + and elev_arr[curr_i + 1, curr_j - 1] < min_elev + ): + elevrp1cm1 = elev_arr[curr_i + 1, curr_j - 1] + min_elev = elevrp1cm1 + direc = 8 + + # if stop candidate found, don't move the cell indices + if not stop_candidate: + # Direc corresponds to: + # |---------------------- + # | 5 | 3 | 6 | + # |---------------------- + # | 2 | cur_cel | 4 | + # |---------------------- + # | 8 | 1 | 7 | + # |---------------------- + if direc == 0: + break + elif direc == 1: + curr_i += 1 + elif direc == 2: + curr_j -= 1 + elif direc == 3: + curr_i -= 1 + elif direc == 4: + curr_j += 1 + elif direc == 5: + curr_i -= 1 + curr_j -= 1 + elif direc == 6: + curr_i -= 1 + curr_j += 1 + elif direc == 7: + curr_i += 1 + curr_j += 1 + elif direc == 8: + curr_i += 1 + curr_j -= 1 + + if stop_candidate: + sfrlayout_new[i, j] = sfrlayout_conn_candidate + stop_candidate = False + break # Bust out of while loop + elif not stop_candidate: + # Check if encountered ibnd == 0, which may be a lake or + # boundary that drains out of model + if ibnd[curr_i, curr_j] == 0: + # This condition is dealt with after looping through + # all cells, see comment "Last step is set..." + break + pass # Commence next downstream cell search + + # Last step is set the 0's in the vicinity of the lake equal to the + # negative of the lake connection + for i in np.arange(0, nrow): + for j in np.arange(0, ncol): + if sfrlayout_new[i, j] == 0 and ibnd[i, j] > 0: + sfrlayout_new[i, j] = -1 + + return sfrlayout_new -segs = sfrDat.get_sfrsegs() -rchs = sfrDat.get_sfrrchs() -sfrcells = sfrDat.get_sfrcells() -rlen = sfrDat.get_sfrlen() -rgrd = sfrDat.get_rgrd() -rtp = sfrDat.get_rtp() # Define the connections -conns = sageBld.gen_mf6_sfr_connections(segs, rchs) +conns = gen_mf6_sfr_connections(segs, rchs) rwid = 3.0 rbth = 1.0 @@ -145,8 +1719,6 @@ ) ) -# #### Prepping input for DRN package - # Instantiating MODFLOW 6 drain package # Here, the drain (DRN) package is used to simulate groundwater discharge to # land surface to keep this water separate from rejected infiltrated simulated @@ -165,16 +1737,12 @@ # Don't add drains to sfr and chd cells: sfrCell_bool = ( 1 - if len([itm for itm in sfrcells if itm[1] == i and itm[2] == j]) - > 0 + if len([itm for itm in sfrcells if itm[1] == i and itm[2] == j]) > 0 else 0 ) chdCell_bool = ( 1 - if len( - [itm for itm in listOfChdCoords if itm[1] == i and itm[2] == j] - ) - > 0 + if len([itm for itm in listOfChdCoords if itm[1] == i and itm[2] == j]) > 0 else 0 ) if idomain1[i, j] and not sfrCell_bool and not chdCell_bool: @@ -185,17 +1753,33 @@ idrnno += 1 -# #### Prepping input for UZF package -# Package_data information - -iuzbnd = np.loadtxt(os.path.join(dat_pth, "iuzbnd.txt")) -thts = np.loadtxt(os.path.join(dat_pth, "thts.txt")) -uzk33 = np.loadtxt(os.path.join(dat_pth, "vks.txt")) -finf_grad = np.loadtxt(os.path.join(dat_pth, "finf_gradient.txt")) +# Prepping input for UZF package +fname = pooch.retrieve( + url=f"https://github.com/MODFLOW-USGS/modflow6-examples/raw/master/data/{example_name}/iuzbnd.txt", + known_hash="md5:280faee0782e0de5f2046b38ac20c271", +) +iuzbnd = np.loadtxt(fname) +fname = pooch.retrieve( + url=f"https://github.com/MODFLOW-USGS/modflow6-examples/raw/master/data/{example_name}/thts.txt", + known_hash="md5:6ebb033605c7e4700ddcf3f2e51ac371", +) +thts = np.loadtxt(fname) +fname = pooch.retrieve( + url=f"https://github.com/MODFLOW-USGS/modflow6-examples/raw/master/data/{example_name}/vks.txt", + known_hash="md5:61892e6cff6dae1879112c9eb03a1614", +) +uzk33 = np.loadtxt(fname) +fname = pooch.retrieve( + url=f"https://github.com/MODFLOW-USGS/modflow6-examples/raw/master/data/{example_name}/finf_gradient.txt", + known_hash="md5:e5ac0a0b27a66dd9f2eff63a63c6e6fe", +) +finf_grad = np.loadtxt(fname) # next, load time series of multipliers -uz_ts = pd.read_csv( - os.path.join(dat_pth, "uzf_ts.dat"), delim_whitespace=True, header=0 +fname = pooch.retrieve( + url=f"https://github.com/MODFLOW-USGS/modflow6-examples/raw/master/data/{example_name}/uzf_ts.dat", + known_hash="md5:969ed0391d64804ec8395a578eb08ed1", ) +uz_ts = pd.read_csv(fname, delim_whitespace=True, header=0) # Need to set iuzbnd inactive where there are constant head cells, or where the # model grid is inactive @@ -274,9 +1858,7 @@ pet = uz_ts["pet"].iloc[t] extdp = uz_ts["rootdepth"].iloc[t] extwc = uz_ts["extwc"].iloc[t] - spdx.append( - [iuzno, finf, pet, extdp, extwc, ha, hroot, rootact] - ) + spdx.append([iuzno, finf, pet, extdp, extwc, ha, hroot, rootact]) iuzno += 1 uzf_perioddata.update({t: spdx}) @@ -287,10 +1869,7 @@ # calculate an array that is the equivalent of the irunbnd array from the UZF1 # package. The MVR package will be used to establish these connection in MF6 # since the IRUNBND functionality went away in the new MF6 framework. -irunbnd = sageBld.determine_runoff_conns_4mvr( - dat_pth, top, idomain1, rchs, nrow, ncol -) - +irunbnd = determine_runoff_conns_4mvr(top, idomain1, rchs, nrow, ncol) iuzno = 0 k = 0 # Hard-wire the layer no. first0ok = True @@ -316,227 +1895,216 @@ maxpackages = len(mvrpack) maxmvr = 10000 # Something arbitrarily high -# ### Function to build models +# ### Model setup # -# MODFLOW 6 flopy simulation object (sim) is returned if building the model - - -def build_model(sim_name, silent=False): - if config.buildModel: - # Instantiate the MODFLOW 6 simulation - sim_ws = os.path.join(ws, example_name) - sim = flopy.mf6.MFSimulation( - sim_name=example_name, - version="mf6", - sim_ws=sim_ws, - exe_name=mf6exe, - ) - - # Instantiating MODFLOW 6 time discretization - tdis_rc = [] - for i in range(len(perlen)): - tdis_rc.append((perlen[i], nstp[i], tsmult[i])) - flopy.mf6.ModflowTdis( - sim, nper=nper, perioddata=tdis_rc, time_units=time_units - ) - - # Instantiating MODFLOW 6 groundwater flow model - gwfname = example_name - gwf = flopy.mf6.ModflowGwf( - sim, - modelname=gwfname, - save_flows=True, - newtonoptions="newton", - model_nam_file=f"{gwfname}.nam", - ) - - # Instantiating MODFLOW 6 solver for flow model - imsgwf = flopy.mf6.ModflowIms( - sim, - print_option="summary", - outer_dvclose=hclose, - outer_maximum=nouter, - under_relaxation="dbd", - linear_acceleration="BICGSTAB", - under_relaxation_theta=0.7, - under_relaxation_kappa=0.08, - under_relaxation_gamma=0.05, - under_relaxation_momentum=0.0, - inner_dvclose=hclose, - rcloserecord="1000.0 strict", - inner_maximum=ninner, - relaxation_factor=relax, - number_orthogonalizations=2, - preconditioner_levels=8, - preconditioner_drop_tolerance=0.001, - filename=f"{gwfname}.ims", - ) - sim.register_ims_package(imsgwf, [gwf.name]) - - # Instantiating MODFLOW 6 discretization package - flopy.mf6.ModflowGwfdis( - gwf, - nlay=nlay, - nrow=nrow, - ncol=ncol, - delr=delr, - delc=delc, - top=top, - botm=bot1, - idomain=idomain, - filename=f"{gwfname}.dis", - ) - - # Instantiating MODFLOW 6 initial conditions package for flow model - flopy.mf6.ModflowGwfic(gwf, strt=strt, filename=f"{gwfname}.ic") - - # Instantiating MODFLOW 6 node-property flow package - flopy.mf6.ModflowGwfnpf( - gwf, - save_flows=False, - alternative_cell_averaging="AMT-HMK", - icelltype=icelltype, - k=k11, - k33=k33, - save_specific_discharge=False, - filename=f"{gwfname}.npf", - ) +# Define functions to build models, write input files, and run the simulation. + + +# + +def build_models(sim_name, silent=False): + # Instantiate the MODFLOW 6 simulation + sim_ws = os.path.join(workspace, example_name) + sim = flopy.mf6.MFSimulation( + sim_name=example_name, + version="mf6", + sim_ws=sim_ws, + exe_name="mf6", + ) - # Instantiate MODFLOW 6 storage package - flopy.mf6.ModflowGwfsto( - gwf, - ss=2e-6, - sy=sy, - iconvert=iconvert, - steady_state={0: True}, - transient={1: True}, - filename=f"{gwfname}.sto", - ) + # Instantiating MODFLOW 6 time discretization + tdis_rc = [] + for i in range(len(perlen)): + tdis_rc.append((perlen[i], nstp[i], tsmult[i])) + flopy.mf6.ModflowTdis(sim, nper=nper, perioddata=tdis_rc, time_units=time_units) + + # Instantiating MODFLOW 6 groundwater flow model + gwfname = example_name + gwf = flopy.mf6.ModflowGwf( + sim, + modelname=gwfname, + save_flows=True, + newtonoptions="newton", + model_nam_file=f"{gwfname}.nam", + ) - # Instantiating MODFLOW 6 output control package for flow model - flopy.mf6.ModflowGwfoc( - gwf, - budget_filerecord=f"{gwfname}.bud", - head_filerecord=f"{gwfname}.hds", - headprintrecord=[ - ("COLUMNS", 10, "WIDTH", 15, "DIGITS", 6, "GENERAL") - ], - saverecord=[("HEAD", "LAST"), ("BUDGET", "LAST")], - printrecord=[("HEAD", "LAST"), ("BUDGET", "LAST")], - ) + # Instantiating MODFLOW 6 solver for flow model + imsgwf = flopy.mf6.ModflowIms( + sim, + print_option="summary", + outer_dvclose=hclose, + outer_maximum=nouter, + under_relaxation="dbd", + linear_acceleration="BICGSTAB", + under_relaxation_theta=0.7, + under_relaxation_kappa=0.08, + under_relaxation_gamma=0.05, + under_relaxation_momentum=0.0, + inner_dvclose=hclose, + rcloserecord="1000.0 strict", + inner_maximum=ninner, + relaxation_factor=relax, + number_orthogonalizations=2, + preconditioner_levels=8, + preconditioner_drop_tolerance=0.001, + filename=f"{gwfname}.ims", + ) + sim.register_ims_package(imsgwf, [gwf.name]) + + # Instantiating MODFLOW 6 discretization package + flopy.mf6.ModflowGwfdis( + gwf, + nlay=nlay, + nrow=nrow, + ncol=ncol, + delr=delr, + delc=delc, + top=top, + botm=bot1, + idomain=idomain, + filename=f"{gwfname}.dis", + ) - # Instantiating MODFLOW 6 constant head package - chdspdx = {0: chdspd} - flopy.mf6.ModflowGwfchd( - gwf, - maxbound=len(chdspd), - stress_period_data=chdspdx, - save_flows=False, - pname="CHD-1", - filename=f"{gwfname}.chd", - ) + # Instantiating MODFLOW 6 initial conditions package for flow model + flopy.mf6.ModflowGwfic(gwf, strt=strt, filename=f"{gwfname}.ic") + + # Instantiating MODFLOW 6 node-property flow package + flopy.mf6.ModflowGwfnpf( + gwf, + save_flows=False, + alternative_cell_averaging="AMT-HMK", + icelltype=icelltype, + k=k11, + k33=k33, + save_specific_discharge=False, + filename=f"{gwfname}.npf", + ) - maxbound = len(drn_spd) # The total number - spd = {0: drn_spd} - flopy.mf6.ModflowGwfdrn( - gwf, - pname="DRN-1", - auxiliary=["ddrn"], - auxdepthname="ddrn", - print_input=False, - print_flows=False, - maxbound=maxbound, - mover=True, - stress_period_data=spd, # wel_spd established in the MVR setup - boundnames=False, - save_flows=True, - filename=f"{gwfname}.drn", - ) + # Instantiate MODFLOW 6 storage package + flopy.mf6.ModflowGwfsto( + gwf, + ss=2e-6, + sy=sy, + iconvert=iconvert, + steady_state={0: True}, + transient={1: True}, + filename=f"{gwfname}.sto", + ) - # Instantiating MODFLOW 6 streamflow routing package - flopy.mf6.ModflowGwfsfr( - gwf, - print_stage=False, - print_flows=False, - budget_filerecord=gwfname + ".sfr.bud", - save_flows=True, - mover=True, - pname="SFR-1", - time_conversion=86400.0, - boundnames=True, - nreaches=len(conns), - packagedata=pkdat, - connectiondata=conns, - perioddata=None, - filename=f"{gwfname}.sfr", - ) + # Instantiating MODFLOW 6 output control package for flow model + flopy.mf6.ModflowGwfoc( + gwf, + budget_filerecord=f"{gwfname}.bud", + head_filerecord=f"{gwfname}.hds", + headprintrecord=[("COLUMNS", 10, "WIDTH", 15, "DIGITS", 6, "GENERAL")], + saverecord=[("HEAD", "LAST"), ("BUDGET", "LAST")], + printrecord=[("HEAD", "LAST"), ("BUDGET", "LAST")], + ) - # Instantiating MODFLOW 6 unsaturated zone flow package - flopy.mf6.ModflowGwfuzf( - gwf, - nuzfcells=nuzfcells, - boundnames=True, - mover=True, - ntrailwaves=15, - nwavesets=150, - print_flows=False, - save_flows=True, - simulate_et=True, - linear_gwet=True, - packagedata=uzf_packagedata, - perioddata=uzf_perioddata, - budget_filerecord=f"{gwfname}.uzf.bud", - pname="UZF-1", - filename=f"{gwfname}.uzf", - ) + # Instantiating MODFLOW 6 constant head package + chdspdx = {0: chdspd} + flopy.mf6.ModflowGwfchd( + gwf, + maxbound=len(chdspd), + stress_period_data=chdspdx, + save_flows=False, + pname="CHD-1", + filename=f"{gwfname}.chd", + ) - flopy.mf6.ModflowGwfmvr( - gwf, - pname="MVR-1", - maxmvr=maxmvr, - print_flows=False, - maxpackages=maxpackages, - packages=mvrpack, - perioddata=mvrspd, - budget_filerecord=gwfname + ".mvr.bud", - filename=f"{gwfname}.mvr", - ) + maxbound = len(drn_spd) # The total number + spd = {0: drn_spd} + flopy.mf6.ModflowGwfdrn( + gwf, + pname="DRN-1", + auxiliary=["ddrn"], + auxdepthname="ddrn", + print_input=False, + print_flows=False, + maxbound=maxbound, + mover=True, + stress_period_data=spd, # wel_spd established in the MVR setup + boundnames=False, + save_flows=True, + filename=f"{gwfname}.drn", + ) - return sim - return None + # Instantiating MODFLOW 6 streamflow routing package + flopy.mf6.ModflowGwfsfr( + gwf, + print_stage=False, + print_flows=False, + budget_filerecord=gwfname + ".sfr.bud", + save_flows=True, + mover=True, + pname="SFR-1", + time_conversion=86400.0, + boundnames=True, + nreaches=len(conns), + packagedata=pkdat, + connectiondata=conns, + perioddata=None, + filename=f"{gwfname}.sfr", + ) + # Instantiating MODFLOW 6 unsaturated zone flow package + flopy.mf6.ModflowGwfuzf( + gwf, + nuzfcells=nuzfcells, + boundnames=True, + mover=True, + ntrailwaves=15, + nwavesets=150, + print_flows=False, + save_flows=True, + simulate_et=True, + linear_gwet=True, + packagedata=uzf_packagedata, + perioddata=uzf_perioddata, + budget_filerecord=f"{gwfname}.uzf.bud", + pname="UZF-1", + filename=f"{gwfname}.uzf", + ) -# Function to write model files + flopy.mf6.ModflowGwfmvr( + gwf, + pname="MVR-1", + maxmvr=maxmvr, + print_flows=False, + maxpackages=maxpackages, + packages=mvrpack, + perioddata=mvrspd, + budget_filerecord=gwfname + ".mvr.bud", + filename=f"{gwfname}.mvr", + ) + return sim -def write_model(sim, silent=True): - if config.writeModel: - sim.write_simulation(silent=silent) +def write_models(sim, silent=True): + sim.write_simulation(silent=silent) -# Function to run the model. True is returned if the model runs successfully +@timed +def run_models(sim, silent=True): + success, buff = sim.run_simulation(silent=silent) + assert success, buff -@config.timeit -def run_model(sim, silent=True): - success = True - if config.runModel: - success = False - success, buff = sim.run_simulation(silent=silent) - if not success: - print(buff) - return success +# - -# Function to plot the model results +# ### Plotting results +# +# Define functions to plot model results. +# + +# Figure properties +figure_size = (6, 6) +figure_size_ts = (6, 3) -def plot_results(mf6, idx): - if config.plotModel: - print("Plotting model results...") - sim_name = mf6.name - fs = USGSFigure(figure_type="graph", verbose=False) +def plot_results(mf6): + sim_name = mf6.name + with styles.USGSPlot(): # Generate a plot of FINF distribution finf_plt = finf_grad.copy() finf_plt[idomain1 == 0] = np.nan @@ -549,14 +2117,15 @@ def plot_results(mf6, idx): cbar.ax.set_title("Infiltration\nrate\nfactor", pad=20) plt.xlabel("Column Number") plt.ylabel("Row Number") - fs.heading(heading=title) + styles.heading(heading=title) - # save figure - if config.plotSave: + if plot_show: + plt.show() + if plot_save: fpth = os.path.join( "..", "figures", - "{}{}".format(sim_name + "-finfFact", config.figure_ext), + "{}{}".format(sim_name + "-finfFact", ".png"), ) fig.savefig(fpth) @@ -590,14 +2159,15 @@ def plot_results(mf6, idx): plt.xlabel("Column Number") plt.ylabel("Row Number") title = "Depth To Groundwater" - fs.heading(heading=title) + styles.heading(heading=title) - # save figure - if config.plotSave: + if plot_show: + plt.show() + if plot_save: fpth = os.path.join( "..", "figures", - "{}{}".format(sim_name + "-gwDepth", config.figure_ext), + "{}{}".format(sim_name + "-gwDepth", ".png"), ) fig.savefig(fpth) @@ -613,9 +2183,7 @@ def plot_results(mf6, idx): for kstpkper in ckstpkper: # 1. Compile groundwater discharge to land surface - drn_tmp = modobj.get_data( - kstpkper=kstpkper, text=" DRN-TO-MVR" - ) + drn_tmp = modobj.get_data(kstpkper=kstpkper, text=" DRN-TO-MVR") drn_arr = np.zeros_like(top) for itm in drn_tmp[0]: i, j = drn_dict_rev[itm[1] - 1] @@ -623,9 +2191,7 @@ def plot_results(mf6, idx): drn_disQ.append(drn_arr) # 2. Compile groundwater discharge to stream cells - sfr_tmp = sfrobj.get_data( - kstpkper=kstpkper, text=" GWF" - ) + sfr_tmp = sfrobj.get_data(kstpkper=kstpkper, text=" GWF") sfr_arr = np.zeros_like(top) for x, itm in enumerate(sfr_tmp[0]): i = sfrcells[x][1] @@ -634,9 +2200,7 @@ def plot_results(mf6, idx): sfr_gwsw.append(sfr_arr) # 3. Compile Infiltrated amount - uzf_tmp = uzfobj.get_data( - kstpkper=kstpkper, text=" INFILTRATION" - ) + uzf_tmp = uzfobj.get_data(kstpkper=kstpkper, text=" INFILTRATION") finf_arr = np.zeros_like(top) for itm in uzf_tmp[0]: i, j = iuzno_dict_rev[itm[0] - 1] @@ -644,9 +2208,7 @@ def plot_results(mf6, idx): finf_tot.append(finf_arr) # 4. Compile recharge from UZF - uzrch_tmp = uzfobj.get_data( - kstpkper=kstpkper, text=" GWF" - ) + uzrch_tmp = uzfobj.get_data(kstpkper=kstpkper, text=" GWF") uzrch_arr = np.zeros_like(top) for itm in uzrch_tmp[0]: i, j = iuzno_dict_rev[itm[0] - 1] @@ -654,9 +2216,7 @@ def plot_results(mf6, idx): uzrech.append(uzrch_arr) # 5. Compile rejected infiltration - rejinf_tmp = uzfobj.get_data( - kstpkper=kstpkper, text=" REJ-INF-TO-MVR" - ) + rejinf_tmp = uzfobj.get_data(kstpkper=kstpkper, text=" REJ-INF-TO-MVR") rejinf_arr = np.zeros_like(top) for itm in rejinf_tmp[0]: i, j = iuzno_dict_rev[itm[0] - 1] @@ -664,9 +2224,7 @@ def plot_results(mf6, idx): rejinf.append(rejinf_arr) # 6. Compile unsat ET - uzet_tmp = uzfobj.get_data( - kstpkper=kstpkper, text=" UZET" - ) + uzet_tmp = uzfobj.get_data(kstpkper=kstpkper, text=" UZET") uzet_arr = np.zeros_like(top) for itm in uzet_tmp[0]: i, j = iuzno_dict_rev[itm[0] - 1] @@ -674,9 +2232,7 @@ def plot_results(mf6, idx): uzet.append(uzet_arr) # 7. Compile groundwater ET - gwet_tmp = modobj.get_data( - kstpkper=kstpkper, text=" UZF-GWET" - ) + gwet_tmp = modobj.get_data(kstpkper=kstpkper, text=" UZF-GWET") gwet_arr = np.zeros_like(top) for itm in gwet_tmp[0]: i, j = iuzno_dict_rev[itm[1] - 1] @@ -684,9 +2240,7 @@ def plot_results(mf6, idx): gwet.append(gwet_arr) # 8. Get flows at outlet - outletQ = sfrobj.get_data( - kstpkper=kstpkper, text=" FLOW-JA-FACE" - ) + outletQ = sfrobj.get_data(kstpkper=kstpkper, text=" FLOW-JA-FACE") outflow.append(outletQ[0][-1][2]) drn_disQ = np.array(drn_disQ) @@ -735,16 +2289,17 @@ def plot_results(mf6, idx): ) # labels along the bottom edge are off ax.set_xlabel("Month") ax.set_ylabel("Volumetric Rate, $m^3$ per day") - fs.graph_legend(ax) + styles.graph_legend(ax) title = "Unsaturated Zone Flow Budget" - fs.heading(heading=title) + styles.heading(heading=title) - # save figure - if config.plotSave: + if plot_show: + plt.show() + if plot_save: fpth = os.path.join( "..", "figures", - "{}{}".format(sim_name + "-uzFlow", config.figure_ext), + "{}{}".format(sim_name + "-uzFlow", ".png"), ) fig.savefig(fpth) @@ -789,48 +2344,38 @@ def plot_results(mf6, idx): ) # labels along the bottom edge are off ax.set_xlabel("Month") ax.set_ylabel("Volumetric Rate, $m^3$ per day") - fs.graph_legend(ax) + styles.graph_legend(ax) title = "Surface Water Flow" - fs.heading(heading=title) + styles.heading(heading=title) - # save figure - if config.plotSave: + if plot_show: + plt.show() + if plot_save: fpth = os.path.join( "..", "figures", - "{}{}".format(sim_name + "-swFlow", config.figure_ext), + "{}{}".format(sim_name + "-swFlow", ".png"), ) fig.savefig(fpth) -# Function that wraps all of the steps for each scenario -# -# 1. build_model, -# 2. write_model, -# 3. run_model, and -# 4. plot_results. -# - - -def scenario(idx, silent=True): - sim = build_model(example_name) - write_model(sim, silent=silent) - success = run_model(sim, silent=silent) - - if success: - plot_results(sim, idx) +# - - -# nosetest - exclude block from this nosetest to the next nosetest -def test_01(): - scenario(0, silent=False) +# ### Running the example +# +# Define and invoke a function to run the example scenario, then plot results. -# nosetest end +# + +def scenario(silent=True): + sim = build_models(example_name) + if write: + write_models(sim, silent=silent) + if run: + run_models(sim, silent=silent) + if plot: + plot_results(sim) -if __name__ == "__main__": - # ### Sagehen Model Results - # - # Two-dimensional transport in a uniform flow field - scenario(0) +scenario() +# - diff --git a/scripts/ex-gwf-sfr-p01.py b/scripts/ex-gwf-sfr-p01.py index 9676f18d5..87e8e7502 100644 --- a/scripts/ex-gwf-sfr-p01.py +++ b/scripts/ex-gwf-sfr-p01.py @@ -3,49 +3,45 @@ # This is the stream-aquifer interaction example problem (test 1) from the # Streamflow Routing Package documentation (Prudic, 1989). All reaches have # been converted to rectangular reaches. -# -# ### SFR Package Problem 1 Setup +# ### Initial setup # -# Imports +# Import dependencies, define the example name and workspace, and read settings from environment variables. +# + import os -import sys +import pathlib as pl import flopy import matplotlib as mpl import matplotlib.pyplot as plt import numpy as np +import pooch +from flopy.plot.styles import styles +from modflow_devtools.misc import get_env, timed -# Append to system path to include the common subdirectory - -sys.path.append(os.path.join("..", "common")) - -# import common functionality - -import config -from figspecs import USGSFigure - -# Set figure properties specific to the - -figure_size = (6.3, 5.6) -masked_values = (0, 1e30, -1e30) - -# Base simulation and model name and workspace - -ws = config.base_ws +# Example name and base workspace +sim_name = "ex-gwf-sfr-p01" +workspace = pl.Path("../examples") -# Simulation name +# Settings from environment variables +write = get_env("WRITE", True) +run = get_env("RUN", True) +plot = get_env("PLOT", True) +plot_show = get_env("PLOT_SHOW", True) +plot_save = get_env("PLOT_SAVE", True) +# - -sim_name = "ex-gwf-sfr-p01" +# ### Define parameters +# +# Define model units, parameters and other settings. +# + # Model units - length_units = "feet" time_units = "seconds" -# Table SFR Package Problem 1 Model Parameters - +# Model parameters nper = 3 # Number of periods nlay = 1 # Number of layers nrow = 15 # Number of rows @@ -61,8 +57,7 @@ evap_rate = 9.5e-8 # Evapotranspiration rate ($ft/s$) ext_depth = 15.0 # Evapotranspiration extinction depth ($ft$) -# Static temporal data used by TDIS file - +# Time discretization tdis_ds = ( (0.0, 1, 1.0), (1.577880e9, 50, 1.1), @@ -75,21 +70,33 @@ shape3d = (nlay, nrow, ncol) # Load the idomain, top, bottom, and evapotranspiration surface arrays - -data_pth = os.path.join("..", "data", sim_name) -fpth = os.path.join(data_pth, "idomain.txt") +fpth = pooch.retrieve( + url=f"https://github.com/MODFLOW-USGS/modflow6-examples/raw/master/data/{sim_name}/idomain.txt", + known_hash="md5:a0b12472b8624aecdc79e5c19c97040c", +) idomain = np.loadtxt(fpth, dtype=int) -fpth = os.path.join(data_pth, "top.txt") +fpth = pooch.retrieve( + url=f"https://github.com/MODFLOW-USGS/modflow6-examples/raw/master/data/{sim_name}/top.txt", + known_hash="md5:ab5097c1dc22e60fb313bf7f10dd8efe", +) top = np.loadtxt(fpth, dtype=float) -fpth = os.path.join(data_pth, "bottom.txt") +fpth = pooch.retrieve( + url=f"https://github.com/MODFLOW-USGS/modflow6-examples/raw/master/data/{sim_name}/bottom.txt", + known_hash="md5:fa5fe276f4f58a01eabfe88516cc90af", +) botm = np.loadtxt(fpth, dtype=float) -fpth = os.path.join(data_pth, "recharge.txt") +fpth = pooch.retrieve( + url=f"https://github.com/MODFLOW-USGS/modflow6-examples/raw/master/data/{sim_name}/recharge.txt", + known_hash="md5:82ed1ed29a457f1f38e51cd2657676e1", +) recharge = np.loadtxt(fpth, dtype=float) -fpth = os.path.join(data_pth, "surf.txt") +fpth = pooch.retrieve( + url=f"https://github.com/MODFLOW-USGS/modflow6-examples/raw/master/data/{sim_name}/surf.txt", + known_hash="md5:743ce03e5e46867cf5af94f1ac283514", +) surf = np.loadtxt(fpth, dtype=float) # Create hydraulic conductivity and specific yield - k11 = np.zeros(shape2d, dtype=float) k11[idomain == 1] = k11_stream k11[idomain == 2] = k11_basin @@ -97,18 +104,13 @@ sy[idomain == 1] = sy_stream sy[idomain == 2] = sy_basin -# ### Create SFR Package Problem 1 Model Boundary Conditions -# # General head boundary conditions -# - ghb_spd = [ [0, 12, 0, 988.0, 0.038], [0, 13, 8, 1045.0, 0.038], ] # Well boundary conditions - wel_spd = { 1: [ [0, 5, 3, -10.0], @@ -128,7 +130,6 @@ } # SFR Package - sfr_pakdata = [ [ 0, @@ -769,317 +770,299 @@ ] # Solver parameters - nouter = 100 ninner = 50 hclose = 1e-6 rclose = 1e-6 +# - + +# ### Model setup +# +# Define functions to build models, write input files, and run the simulation. + + +# + +def build_models(): + sim_ws = os.path.join(workspace, sim_name) + sim = flopy.mf6.MFSimulation(sim_name=sim_name, sim_ws=sim_ws, exe_name="mf6") + flopy.mf6.ModflowTdis(sim, nper=nper, perioddata=tdis_ds, time_units=time_units) + flopy.mf6.ModflowIms( + sim, + print_option="summary", + linear_acceleration="bicgstab", + outer_maximum=nouter, + outer_dvclose=hclose, + inner_maximum=ninner, + inner_dvclose=hclose, + rcloserecord=f"{rclose} strict", + ) + gwf = flopy.mf6.ModflowGwf( + sim, modelname=sim_name, newtonoptions="newton", save_flows=True + ) + flopy.mf6.ModflowGwfdis( + gwf, + length_units=length_units, + nlay=nlay, + nrow=nrow, + ncol=ncol, + delr=delr, + delc=delc, + idomain=idomain, + top=top, + botm=botm, + ) + flopy.mf6.ModflowGwfnpf( + gwf, + icelltype=1, + k=k11, + save_specific_discharge=True, + ) + flopy.mf6.ModflowGwfsto( + gwf, + iconvert=1, + sy=sy, + ss=ss, + steady_state={0: True}, + transient={1: True}, + ) + flopy.mf6.ModflowGwfic(gwf, strt=strt) + flopy.mf6.ModflowGwfghb(gwf, stress_period_data=ghb_spd) + flopy.mf6.ModflowGwfwel(gwf, stress_period_data=wel_spd) + flopy.mf6.ModflowGwfrcha(gwf, recharge=recharge) + flopy.mf6.ModflowGwfevta(gwf, surface=surf, rate=evap_rate, depth=ext_depth) + sfr = flopy.mf6.ModflowGwfsfr( + gwf, + length_conversion=3.28081, + nreaches=len(sfr_pakdata), + packagedata=sfr_pakdata, + connectiondata=sfr_conn, + diversions=sfr_div, + perioddata=sfr_spd, + ) + obs_file = f"{sim_name}.sfr.obs" + csv_file = obs_file + ".csv" + obs_dict = { + csv_file: [ + ("r01_stage", "stage", (3,)), + ("r02_stage", "stage", (14,)), + ("r03_stage", "stage", (26,)), + ("r04_stage", "stage", (35,)), + ("r01_flow", "downstream-flow", (3,)), + ("r02_flow", "downstream-flow", (14,)), + ("r03_flow", "downstream-flow", (26,)), + ("r04_flow", "downstream-flow", (35,)), + ] + } + sfr.obs.initialize( + filename=obs_file, digits=10, print_input=True, continuous=obs_dict + ) + + head_filerecord = f"{sim_name}.hds" + budget_filerecord = f"{sim_name}.cbc" + flopy.mf6.ModflowGwfoc( + gwf, + head_filerecord=head_filerecord, + budget_filerecord=budget_filerecord, + saverecord=[("HEAD", "LAST"), ("BUDGET", "LAST")], + ) + return sim -# ### Functions to build, write, run, and plot the MODFLOW 6 SFR Package Problem 1 model +def write_models(sim, silent=True): + sim.write_simulation(silent=silent) + + +@timed +def run_models(sim, silent=True): + success, buff = sim.run_simulation(silent=silent) + assert success, buff + + +# - + +# ### Plotting results # -# MODFLOW 6 flopy simulation object (sim) is returned if building the model +# Define functions to plot model results. +# + +# Figure properties +figure_size = (6.3, 5.6) +masked_values = (0, 1e30, -1e30) -def build_model(): - if config.buildModel: - sim_ws = os.path.join(ws, sim_name) - sim = flopy.mf6.MFSimulation( - sim_name=sim_name, sim_ws=sim_ws, exe_name="mf6" + +def plot_grid(gwf, silent=True): + with styles.USGSMap() as fs: + fig = plt.figure(figsize=figure_size, constrained_layout=False) + gs = mpl.gridspec.GridSpec(ncols=10, nrows=7, figure=fig, wspace=5) + plt.axis("off") + + axes = [] + axes.append(fig.add_subplot(gs[:6, :5])) + axes.append(fig.add_subplot(gs[:6, 5:], sharey=axes[0])) + + for ax in axes: + ax.set_xlim(extents[:2]) + ax.set_ylim(extents[2:]) + ax.set_aspect("equal") + # ax.set_xticks(ticklabels) + # ax.set_yticks(ticklabels) + + # legend axis + axes.append(fig.add_subplot(gs[6:, :])) + + # set limits for legend area + ax = axes[-1] + ax.set_xlim(0, 1) + ax.set_ylim(0, 1) + + # get rid of ticks and spines for legend area + ax.axis("off") + ax.set_xticks([]) + ax.set_yticks([]) + ax.spines["top"].set_color("none") + ax.spines["bottom"].set_color("none") + ax.spines["left"].set_color("none") + ax.spines["right"].set_color("none") + ax.patch.set_alpha(0.0) + + ax = axes[0] + mm = flopy.plot.PlotMapView(gwf, ax=ax, extent=extents) + top_coll = mm.plot_array( + top, vmin=1000, vmax=1120, masked_values=masked_values, alpha=0.5 ) - flopy.mf6.ModflowTdis( - sim, nper=nper, perioddata=tdis_ds, time_units=time_units + mm.plot_bc("SFR", color="green") + cv = mm.contour_array( + top, + levels=np.arange(1000, 1100, 20), + linewidths=0.5, + linestyles="-", + colors="black", + masked_values=masked_values, ) - flopy.mf6.ModflowIms( - sim, - print_option="summary", - linear_acceleration="bicgstab", - outer_maximum=nouter, - outer_dvclose=hclose, - inner_maximum=ninner, - inner_dvclose=hclose, - rcloserecord=f"{rclose} strict", + plt.clabel(cv, fmt="%1.0f") + mm.plot_inactive(color_noflow="0.5") + mm.plot_grid(lw=0.5, color="black") + cbar = plt.colorbar( + top_coll, + shrink=0.8, + orientation="horizontal", + ax=ax, + format="%.0f", ) - gwf = flopy.mf6.ModflowGwf( - sim, modelname=sim_name, newtonoptions="newton", save_flows=True + cbar.ax.tick_params(size=0) + cbar.ax.set_xlabel(r"Land surface elevation, $ft$") + ax.set_xlabel("x-coordinate, in feet") + ax.set_ylabel("y-coordinate, in feet") + styles.heading(ax, heading="Land surface elevation", idx=0) + styles.remove_edge_ticks(ax) + + ax = axes[1] + mm = flopy.plot.PlotMapView(gwf, ax=ax, extent=extents) + bot_coll = mm.plot_array( + botm, vmin=500, vmax=1000, masked_values=masked_values, alpha=0.5 ) - flopy.mf6.ModflowGwfdis( - gwf, - length_units=length_units, - nlay=nlay, - nrow=nrow, - ncol=ncol, - delr=delr, - delc=delc, - idomain=idomain, - top=top, - botm=botm, + mm.plot_bc("GHB", color="purple") + mm.plot_bc("WEL", color="red", kper=1) + cv = mm.contour_array( + botm, + levels=np.arange(600, 1000, 100), + linewidths=0.5, + linestyles=":", + colors="black", + masked_values=masked_values, ) - flopy.mf6.ModflowGwfnpf( - gwf, - icelltype=1, - k=k11, - save_specific_discharge=True, + plt.clabel(cv, fmt="%1.0f") + mm.plot_inactive(color_noflow="0.5") + mm.plot_grid(lw=0.5, color="black") + cbar = plt.colorbar( + bot_coll, + shrink=0.8, + orientation="horizontal", + ax=ax, + format="%.0f", ) - flopy.mf6.ModflowGwfsto( - gwf, - iconvert=1, - sy=sy, - ss=ss, - steady_state={0: True}, - transient={1: True}, + cbar.ax.tick_params(size=0) + cbar.ax.set_xlabel(r"Bottom elevation, $ft$") + ax.set_xlabel("x-coordinate, in feet") + styles.heading(ax, heading="Bottom elevation", idx=1) + styles.remove_edge_ticks(ax) + + # legend + ax = axes[-1] + ax.plot( + -10000, + -10000, + lw=0, + marker="s", + ms=10, + mfc="0.5", + mec="black", + markeredgewidth=0.5, + label="Inactive cells", ) - flopy.mf6.ModflowGwfic(gwf, strt=strt) - flopy.mf6.ModflowGwfghb(gwf, stress_period_data=ghb_spd) - flopy.mf6.ModflowGwfwel(gwf, stress_period_data=wel_spd) - flopy.mf6.ModflowGwfrcha(gwf, recharge=recharge) - flopy.mf6.ModflowGwfevta( - gwf, surface=surf, rate=evap_rate, depth=ext_depth + ax.plot( + -10000, + -10000, + lw=0, + marker="s", + ms=10, + mfc="green", + mec="black", + markeredgewidth=0.5, + label="Stream boundary", ) - sfr = flopy.mf6.ModflowGwfsfr( - gwf, - length_conversion=3.28081, - nreaches=len(sfr_pakdata), - packagedata=sfr_pakdata, - connectiondata=sfr_conn, - diversions=sfr_div, - perioddata=sfr_spd, + ax.plot( + -10000, + -10000, + lw=0, + marker="s", + ms=10, + mfc="purple", + mec="black", + markeredgewidth=0.5, + label="General head boundary", ) - obs_file = f"{sim_name}.sfr.obs" - csv_file = obs_file + ".csv" - obs_dict = { - csv_file: [ - ("r01_stage", "stage", (3,)), - ("r02_stage", "stage", (14,)), - ("r03_stage", "stage", (26,)), - ("r04_stage", "stage", (35,)), - ("r01_flow", "downstream-flow", (3,)), - ("r02_flow", "downstream-flow", (14,)), - ("r03_flow", "downstream-flow", (26,)), - ("r04_flow", "downstream-flow", (35,)), - ] - } - sfr.obs.initialize( - filename=obs_file, digits=10, print_input=True, continuous=obs_dict + ax.plot( + -10000, + -10000, + lw=0, + marker="s", + ms=10, + mfc="red", + mec="black", + markeredgewidth=0.5, + label="Well boundary", ) - - head_filerecord = f"{sim_name}.hds" - budget_filerecord = f"{sim_name}.cbc" - flopy.mf6.ModflowGwfoc( - gwf, - head_filerecord=head_filerecord, - budget_filerecord=budget_filerecord, - saverecord=[("HEAD", "LAST"), ("BUDGET", "LAST")], + ax.plot( + -10000, + -10000, + lw=0.5, + ls="-", + color="black", + label=r"Land surface elevation contour, $ft$", ) - return sim - return None - - -# Function to write MODFLOW 6 SFR Package Problem 1 model files - - -def write_model(sim, silent=True): - if config.writeModel: - sim.write_simulation(silent=silent) - - -# Function to run the SFR Package Problem 1 model. -# True is returned if the model runs successfully -# - - -@config.timeit -def run_model(sim, silent=True): - success = True - if config.runModel: - success, buff = sim.run_simulation(silent=silent) - if not success: - print(buff) - return success - - -# Function to plot grid - - -def plot_grid(gwf, silent=True): - fs = USGSFigure(figure_type="map", verbose=False) - fig = plt.figure(figsize=figure_size, constrained_layout=False) - gs = mpl.gridspec.GridSpec(ncols=10, nrows=7, figure=fig, wspace=5) - plt.axis("off") - - axes = [] - axes.append(fig.add_subplot(gs[:6, :5])) - axes.append(fig.add_subplot(gs[:6, 5:], sharey=axes[0])) - - for ax in axes: - ax.set_xlim(extents[:2]) - ax.set_ylim(extents[2:]) - ax.set_aspect("equal") - # ax.set_xticks(ticklabels) - # ax.set_yticks(ticklabels) - - # legend axis - axes.append(fig.add_subplot(gs[6:, :])) - - # set limits for legend area - ax = axes[-1] - ax.set_xlim(0, 1) - ax.set_ylim(0, 1) - - # get rid of ticks and spines for legend area - ax.axis("off") - ax.set_xticks([]) - ax.set_yticks([]) - ax.spines["top"].set_color("none") - ax.spines["bottom"].set_color("none") - ax.spines["left"].set_color("none") - ax.spines["right"].set_color("none") - ax.patch.set_alpha(0.0) - - ax = axes[0] - mm = flopy.plot.PlotMapView(gwf, ax=ax, extent=extents) - top_coll = mm.plot_array( - top, vmin=1000, vmax=1120, masked_values=masked_values, alpha=0.5 - ) - mm.plot_bc("SFR", color="green") - cv = mm.contour_array( - top, - levels=np.arange(1000, 1100, 20), - linewidths=0.5, - linestyles="-", - colors="black", - masked_values=masked_values, - ) - plt.clabel(cv, fmt="%1.0f") - mm.plot_inactive(color_noflow="0.5") - mm.plot_grid(lw=0.5, color="black") - cbar = plt.colorbar( - top_coll, - shrink=0.8, - orientation="horizontal", - ax=ax, - format="%.0f", - ) - cbar.ax.tick_params(size=0) - cbar.ax.set_xlabel(r"Land surface elevation, $ft$") - ax.set_xlabel("x-coordinate, in feet") - ax.set_ylabel("y-coordinate, in feet") - fs.heading(ax, heading="Land surface elevation", idx=0) - fs.remove_edge_ticks(ax) - - ax = axes[1] - mm = flopy.plot.PlotMapView(gwf, ax=ax, extent=extents) - bot_coll = mm.plot_array( - botm, vmin=500, vmax=1000, masked_values=masked_values, alpha=0.5 - ) - mm.plot_bc("GHB", color="purple") - mm.plot_bc("WEL", color="red", kper=1) - cv = mm.contour_array( - botm, - levels=np.arange(600, 1000, 100), - linewidths=0.5, - linestyles=":", - colors="black", - masked_values=masked_values, - ) - plt.clabel(cv, fmt="%1.0f") - mm.plot_inactive(color_noflow="0.5") - mm.plot_grid(lw=0.5, color="black") - cbar = plt.colorbar( - bot_coll, - shrink=0.8, - orientation="horizontal", - ax=ax, - format="%.0f", - ) - cbar.ax.tick_params(size=0) - cbar.ax.set_xlabel(r"Bottom elevation, $ft$") - ax.set_xlabel("x-coordinate, in feet") - fs.heading(ax, heading="Bottom elevation", idx=1) - fs.remove_edge_ticks(ax) - - # legend - ax = axes[-1] - ax.plot( - -10000, - -10000, - lw=0, - marker="s", - ms=10, - mfc="0.5", - mec="black", - markeredgewidth=0.5, - label="Inactive cells", - ) - ax.plot( - -10000, - -10000, - lw=0, - marker="s", - ms=10, - mfc="green", - mec="black", - markeredgewidth=0.5, - label="Stream boundary", - ) - ax.plot( - -10000, - -10000, - lw=0, - marker="s", - ms=10, - mfc="purple", - mec="black", - markeredgewidth=0.5, - label="General head boundary", - ) - ax.plot( - -10000, - -10000, - lw=0, - marker="s", - ms=10, - mfc="red", - mec="black", - markeredgewidth=0.5, - label="Well boundary", - ) - ax.plot( - -10000, - -10000, - lw=0.5, - ls="-", - color="black", - label=r"Land surface elevation contour, $ft$", - ) - ax.plot( - -10000, - -10000, - lw=0.5, - ls=":", - color="black", - label=r"Bottom elevation contour, $ft$", - ) - fs.graph_legend(ax, loc="center", ncol=3) - - # save figure - if config.plotSave: - fpth = os.path.join( - "..", - "figures", - f"{sim_name}-grid{config.figure_ext}", + ax.plot( + -10000, + -10000, + lw=0.5, + ls=":", + color="black", + label=r"Bottom elevation contour, $ft$", ) - fig.savefig(fpth) - - return - - -# Function to plot grid + styles.graph_legend(ax, loc="center", ncol=3) + + if plot_show: + plt.show() + if plot_save: + fpth = os.path.join( + "..", + "figures", + f"{sim_name}-grid.png", + ) + fig.savefig(fpth) def plot_head_results(gwf, silent=True): - sim_ws = os.path.join(ws, sim_name) - # create MODFLOW 6 head object hobj = gwf.output.head() @@ -1088,276 +1071,250 @@ def plot_head_results(gwf, silent=True): kstpkper = hobj.get_kstpkper() - fs = USGSFigure(figure_type="map", verbose=False) - fig = plt.figure(figsize=figure_size, constrained_layout=False) - gs = mpl.gridspec.GridSpec(ncols=10, nrows=7, figure=fig, wspace=5) - plt.axis("off") - - axes = [fig.add_subplot(gs[:6, :5])] - axes.append(fig.add_subplot(gs[:6, 5:], sharey=axes[0])) - - for ax in axes: - ax.set_xlim(extents[:2]) - ax.set_ylim(extents[2:]) - ax.set_aspect("equal") - - # legend axis - axes.append(fig.add_subplot(gs[6:, :])) - - # set limits for legend area - ax = axes[-1] - ax.set_xlim(0, 1) - ax.set_ylim(0, 1) - - # get rid of ticks and spines for legend area - ax.axis("off") - ax.set_xticks([]) - ax.set_yticks([]) - ax.spines["top"].set_color("none") - ax.spines["bottom"].set_color("none") - ax.spines["left"].set_color("none") - ax.spines["right"].set_color("none") - ax.patch.set_alpha(0.0) - - # extract heads and specific discharge for first stress period - head = hobj.get_data(kstpkper=kstpkper[0]) - qx, qy, qz = flopy.utils.postprocessing.get_specific_discharge( - cobj.get_data(text="DATA-SPDIS", kstpkper=kstpkper[0])[0], - gwf, - ) - - ax = axes[0] - mm = flopy.plot.PlotMapView(gwf, ax=ax, extent=extents) - head_coll = mm.plot_array( - head, vmin=900, vmax=1120, masked_values=masked_values - ) - cv = mm.contour_array( - head, - levels=np.arange(900, 1100, 10), - linewidths=0.5, - linestyles="-", - colors="black", - masked_values=masked_values, - ) - plt.clabel(cv, fmt="%1.0f") - mm.plot_vector(qx, qy, normalize=True, color="0.75") - mm.plot_inactive(color_noflow="0.5") - mm.plot_grid(lw=0.5, color="black") - ax.set_xlabel("x-coordinate, in feet") - ax.set_ylabel("y-coordinate, in feet") - fs.heading(ax, heading="Steady-state", idx=0) - fs.remove_edge_ticks(ax) - - # extract heads and specific discharge for second stress period - head = hobj.get_data(kstpkper=kstpkper[1]) - qx, qy, qz = flopy.utils.postprocessing.get_specific_discharge( - cobj.get_data(text="DATA-SPDIS", kstpkper=kstpkper[1])[0], - gwf, - ) - - ax = axes[1] - mm = flopy.plot.PlotMapView(gwf, ax=ax, extent=extents) - head_coll = mm.plot_array( - head, vmin=900, vmax=1120, masked_values=masked_values - ) - cv = mm.contour_array( - head, - levels=np.arange(900, 1100, 10), - linewidths=0.5, - linestyles="-", - colors="black", - masked_values=masked_values, - ) - plt.clabel(cv, fmt="%1.0f") - mm.plot_vector(qx, qy, normalize=True, color="0.75") - mm.plot_inactive(color_noflow="0.5") - mm.plot_grid(lw=0.5, color="black") - ax.set_xlabel("x-coordinate, in feet") - fs.heading(ax, heading="Pumping", idx=1) - fs.remove_edge_ticks(ax) - - # legend - ax = axes[-1] - cbar = plt.colorbar( - head_coll, - shrink=0.8, - orientation="horizontal", - ax=ax, - format="%.0f", - ) - cbar.ax.tick_params(size=0) - cbar.ax.set_xlabel(r"Head, $ft$") - ax.plot( - -10000, - -10000, - lw=0, - marker="$\u2192$", - ms=10, - mfc="0.75", - mec="0.75", - label="Normalized specific discharge", - ) - ax.plot(-10000, -10000, lw=0.5, color="black", label=r"Head contour, $ft$") - fs.graph_legend(ax, loc="upper center", ncol=2) - - # save figure - if config.plotSave: - fpth = os.path.join( - "..", - "figures", - f"{sim_name}-01{config.figure_ext}", + with styles.USGSMap() as fs: + fig = plt.figure(figsize=figure_size, constrained_layout=False) + gs = mpl.gridspec.GridSpec(ncols=10, nrows=7, figure=fig, wspace=5) + plt.axis("off") + + axes = [fig.add_subplot(gs[:6, :5])] + axes.append(fig.add_subplot(gs[:6, 5:], sharey=axes[0])) + + for ax in axes: + ax.set_xlim(extents[:2]) + ax.set_ylim(extents[2:]) + ax.set_aspect("equal") + + # legend axis + axes.append(fig.add_subplot(gs[6:, :])) + + # set limits for legend area + ax = axes[-1] + ax.set_xlim(0, 1) + ax.set_ylim(0, 1) + + # get rid of ticks and spines for legend area + ax.axis("off") + ax.set_xticks([]) + ax.set_yticks([]) + ax.spines["top"].set_color("none") + ax.spines["bottom"].set_color("none") + ax.spines["left"].set_color("none") + ax.spines["right"].set_color("none") + ax.patch.set_alpha(0.0) + + # extract heads and specific discharge for first stress period + head = hobj.get_data(kstpkper=kstpkper[0]) + qx, qy, qz = flopy.utils.postprocessing.get_specific_discharge( + cobj.get_data(text="DATA-SPDIS", kstpkper=kstpkper[0])[0], + gwf, ) - fig.savefig(fpth) - - return + ax = axes[0] + mm = flopy.plot.PlotMapView(gwf, ax=ax, extent=extents) + head_coll = mm.plot_array( + head, vmin=900, vmax=1120, masked_values=masked_values + ) + cv = mm.contour_array( + head, + levels=np.arange(900, 1100, 10), + linewidths=0.5, + linestyles="-", + colors="black", + masked_values=masked_values, + ) + plt.clabel(cv, fmt="%1.0f") + mm.plot_vector(qx, qy, normalize=True, color="0.75") + mm.plot_inactive(color_noflow="0.5") + mm.plot_grid(lw=0.5, color="black") + ax.set_xlabel("x-coordinate, in feet") + ax.set_ylabel("y-coordinate, in feet") + styles.heading(ax, heading="Steady-state", idx=0) + styles.remove_edge_ticks(ax) + + # extract heads and specific discharge for second stress period + head = hobj.get_data(kstpkper=kstpkper[1]) + qx, qy, qz = flopy.utils.postprocessing.get_specific_discharge( + cobj.get_data(text="DATA-SPDIS", kstpkper=kstpkper[1])[0], + gwf, + ) -# Function to plot the sfr results + ax = axes[1] + mm = flopy.plot.PlotMapView(gwf, ax=ax, extent=extents) + head_coll = mm.plot_array( + head, vmin=900, vmax=1120, masked_values=masked_values + ) + cv = mm.contour_array( + head, + levels=np.arange(900, 1100, 10), + linewidths=0.5, + linestyles="-", + colors="black", + masked_values=masked_values, + ) + plt.clabel(cv, fmt="%1.0f") + mm.plot_vector(qx, qy, normalize=True, color="0.75") + mm.plot_inactive(color_noflow="0.5") + mm.plot_grid(lw=0.5, color="black") + ax.set_xlabel("x-coordinate, in feet") + styles.heading(ax, heading="Pumping", idx=1) + styles.remove_edge_ticks(ax) + + # legend + ax = axes[-1] + cbar = plt.colorbar( + head_coll, + shrink=0.8, + orientation="horizontal", + ax=ax, + format="%.0f", + ) + cbar.ax.tick_params(size=0) + cbar.ax.set_xlabel(r"Head, $ft$") + ax.plot( + -10000, + -10000, + lw=0, + marker="$\u2192$", + ms=10, + mfc="0.75", + mec="0.75", + label="Normalized specific discharge", + ) + ax.plot(-10000, -10000, lw=0.5, color="black", label=r"Head contour, $ft$") + styles.graph_legend(ax, loc="upper center", ncol=2) + + if plot_show: + plt.show() + if plot_save: + fpth = os.path.join( + "..", + "figures", + f"{sim_name}-01.png", + ) + fig.savefig(fpth) def plot_sfr_results(gwf, silent=True): - fs = USGSFigure(figure_type="graph", verbose=False) - - # load the observations - results = gwf.sfr.output.obs().data - - # modify the time - results["totim"] /= 365.25 * 86400.0 - - rnos = ( - 3, - 14, - 26, - 35, - ) - sfr = gwf.sfr.packagedata.array["rtp"] - offsets = [] - for rno in rnos: - offsets.append(sfr[rno]) - - # create the figure - fig, axes = plt.subplots( - ncols=2, - nrows=4, - sharex=True, - figsize=(6.3, 6.3), - constrained_layout=True, - ) - ipos = 0 - for i in range(4): - heading = f"Reach {rnos[i] + 1}" - for j in range(2): - ax = axes[i, j] - ax.set_xlim(0, 100) - if j == 0: - tag = f"R{i + 1:02d}_STAGE" - offset = offsets[i] - scale = 1.0 - ylabel = "Reach depth, in feet" - color = "blue" - else: - tag = f"R{i + 1:02d}_FLOW" - offset = 0.0 - scale = -1.0 - ylabel = "Downstream reach flow,\nin cubic feet per second" - color = "red" - - ax.plot( - results["totim"], - scale * results[tag] - offset, - lw=0.5, - color=color, - zorder=10, - ) - ax.axvline(50, lw=0.5, ls="--", color="black", zorder=10) - if ax.get_ylim()[0] < 0.0: - ax.axhline(0, lw=0.5, color="0.5", zorder=9) - fs.add_text( - ax, - text="Pumping", - x=0.49, - y=0.8, - ha="right", - bold=False, - fontsize=7, - ) - fs.add_text( - ax, - text="Recovery", - x=0.51, - y=0.8, - ha="left", - bold=False, - fontsize=7, - ) - ax.set_ylabel(ylabel) - ax.yaxis.set_label_coords(-0.1, 0.5) - fs.heading(ax, heading=heading, idx=ipos) - if i == 3: - ax.set_xlabel("Time since pumping began, in years") - ipos += 1 - - # save figure - if config.plotSave: - fpth = os.path.join( - "..", - "figures", - f"{sim_name}-02{config.figure_ext}", + with styles.USGSPlot() as fs: + # load the observations + results = gwf.sfr.output.obs().data + + # modify the time + results["totim"] /= 365.25 * 86400.0 + + rnos = ( + 3, + 14, + 26, + 35, ) - fig.savefig(fpth) - - return - - -# Function to plot the SFR Package Problem 1 model results. + sfr = gwf.sfr.packagedata.array["rtp"] + offsets = [] + for rno in rnos: + offsets.append(sfr[rno]) + + # create the figure + fig, axes = plt.subplots( + ncols=2, + nrows=4, + sharex=True, + figsize=(6.3, 6.3), + constrained_layout=True, + ) + ipos = 0 + for i in range(4): + heading = f"Reach {rnos[i] + 1}" + for j in range(2): + ax = axes[i, j] + ax.set_xlim(0, 100) + if j == 0: + tag = f"R{i + 1:02d}_STAGE" + offset = offsets[i] + scale = 1.0 + ylabel = "Reach depth, in feet" + color = "blue" + else: + tag = f"R{i + 1:02d}_FLOW" + offset = 0.0 + scale = -1.0 + ylabel = "Downstream reach flow,\nin cubic feet per second" + color = "red" + + ax.plot( + results["totim"], + scale * results[tag] - offset, + lw=0.5, + color=color, + zorder=10, + ) + ax.axvline(50, lw=0.5, ls="--", color="black", zorder=10) + if ax.get_ylim()[0] < 0.0: + ax.axhline(0, lw=0.5, color="0.5", zorder=9) + styles.add_text( + ax, + text="Pumping", + x=0.49, + y=0.8, + ha="right", + bold=False, + fontsize=7, + ) + styles.add_text( + ax, + text="Recovery", + x=0.51, + y=0.8, + ha="left", + bold=False, + fontsize=7, + ) + ax.set_ylabel(ylabel) + ax.yaxis.set_label_coords(-0.1, 0.5) + styles.heading(ax, heading=heading, idx=ipos) + if i == 3: + ax.set_xlabel("Time since pumping began, in years") + ipos += 1 + + if plot_show: + plt.show() + if plot_save: + fpth = os.path.join( + "..", + "figures", + f"{sim_name}-02.png", + ) + fig.savefig(fpth) def plot_results(sim, silent=True): - if config.plotModel: - gwf = sim.get_model(sim_name) - - plot_grid(gwf, silent=silent) - - plot_sfr_results(gwf, silent=silent) + gwf = sim.get_model(sim_name) + plot_grid(gwf, silent=silent) + plot_sfr_results(gwf, silent=silent) + plot_head_results(gwf, silent=silent) - plot_head_results(gwf, silent=silent) +# - -# Function that wraps all of the steps for the SFR Package Problem 1 model +# ### Running the example # -# 1. build_model, -# 2. write_model, -# 3. run_model, and -# 4. plot_results. -# - - -def simulation(silent=True): - sim = build_model() - - write_model(sim, silent=silent) +# Define and invoke a function to run the example scenario, then plot results. - success = run_model(sim, silent=silent) - assert success, f"could not run...{sim_name}" - if success: +# + +def scenario(silent=True): + sim = build_models() + if write: + write_models(sim, silent=silent) + if run: + run_models(sim, silent=silent) + if plot: plot_results(sim, silent=silent) -# nosetest - exclude block from this nosetest to the next nosetest -def test_01(): - simulation(silent=False) - - -# nosetest end - -if __name__ == "__main__": - # ### SFR Package Problem 1 Simulation - # - # Simulated heads in model the unconfined, middle, and lower aquifers (model layers - # 1, 3, and 5) are shown in the figure below. MODFLOW-2005 results for a quasi-3D - # model are also shown. The location of drain (green) and well (gray) boundary - # conditions, normalized specific discharge, and head contours (25 ft contour - # intervals) are also shown. - - simulation() +# Simulated heads in model the unconfined, middle, and lower aquifers (model layers +# 1, 3, and 5) are shown in the figure below. MODFLOW-2005 results for a quasi-3D +# model are also shown. The location of drain (green) and well (gray) boundary +# conditions, normalized specific discharge, and head contours (25 ft contour +# intervals) are also shown. +scenario() +# - diff --git a/scripts/ex-gwf-sfr-p01b.py b/scripts/ex-gwf-sfr-p01b.py index 745919c32..388a613b3 100644 --- a/scripts/ex-gwf-sfr-p01b.py +++ b/scripts/ex-gwf-sfr-p01b.py @@ -4,51 +4,46 @@ # Streamflow Routing Package documentation (Prudic, 1989) with a couple of # modifications for demonstrating MVR connections among the advanced packages. # All reaches have been converted to rectangular reaches. -# -# ### SFR Package Problem 1 with MVR Setup +# ### Initial setup # -# Imports +# Import dependencies, define the example name and workspace, and read settings from environment variables. +# + import os -import sys +import pathlib as pl import flopy -import flopy.utils.binaryfile as bf import matplotlib as mpl import matplotlib.pyplot as plt import numpy as np import pandas as pd +import pooch +from flopy.plot.styles import styles +from modflow_devtools.misc import get_env, timed -# Append to system path to include the common subdirectory - -sys.path.append(os.path.join("..", "common")) - -# import common functionality - -import config -from figspecs import USGSFigure - -# Set figure properties specific to the - -figure_size = (6.3, 5.6) -masked_values = (0, 1e30, -1e30) - -# Base simulation and model name and workspace - -ws = config.base_ws +# Example name and base workspace +sim_name = "ex-gwf-sfr-p01b" +workspace = pl.Path("../examples") -# Simulation name +# Settings from environment variables +write = get_env("WRITE", True) +run = get_env("RUN", True) +plot = get_env("PLOT", True) +plot_show = get_env("PLOT_SHOW", True) +plot_save = get_env("PLOT_SAVE", True) +# - -sim_name = "ex-gwf-sfr-p01b" +# ### Model setup +# +# Define functions to build models, write input files, and run the simulation. +# + # Model units - length_units = "feet" time_units = "seconds" -# Table SFR Package Problem 1 with MVR Model Parameters - +# Model parameters nper = 24 # Number of periods nlay = 2 # Number of layers nrow = 15 # Number of rows @@ -65,8 +60,7 @@ evap_rate = 9.5e-8 # Evapotranspiration rate ($ft/s$) ext_depth = 15.0 # Evapotranspiration extinction depth ($ft$) -# Static temporal data used by TDIS file - +# Time discretization tdis_ds = ( (2628000.0, 1, 1.0), (2628000.0, 15, 1.0), @@ -100,50 +94,63 @@ shape3d = (nlay, nrow, ncol) # Load the idomain, lake locations, top, bottom, and evapotranspiration surface arrays - -data_pth = os.path.join("..", "data", sim_name) -fpth = os.path.join(data_pth, "strt1.txt") +fpth = pooch.retrieve( + url=f"https://github.com/MODFLOW-USGS/modflow6-examples/raw/master/data/{sim_name}/strt1.txt", + known_hash="md5:273db6e876e7cfb4985b0b09c232f7cc", +) strt1 = np.loadtxt(fpth, dtype=float) strt2 = strt1 strt = [strt1, strt2] -fpth = os.path.join(data_pth, "idomain.txt") +fpth = pooch.retrieve( + url=f"https://github.com/MODFLOW-USGS/modflow6-examples/raw/master/data/{sim_name}/idomain.txt", + known_hash="md5:3fb0b80939ff6ccc9dc47d010e002a3c", +) idomain1 = np.loadtxt(fpth, dtype=int) idomain = [idomain1, idomain1] lake_map = np.ones(shape3d, dtype=int) * -1 -fpth = os.path.join(data_pth, "lakes.txt") +fpth = pooch.retrieve( + url=f"https://github.com/MODFLOW-USGS/modflow6-examples/raw/master/data/{sim_name}/lakes.txt", + known_hash="md5:c344195438bda85738cab2ce34a16733", +) lake_map[0, :, :] = np.loadtxt(fpth, dtype=int) - 1 lake_map = np.ma.masked_where(lake_map < 0, lake_map) -fpth = os.path.join(data_pth, "top1.txt") +fpth = pooch.retrieve( + url=f"https://github.com/MODFLOW-USGS/modflow6-examples/raw/master/data/{sim_name}/top1.txt", + known_hash="md5:ba3f1422f45388b19dc1ef6b3076fa96", +) top = np.loadtxt(fpth, dtype=float) -fpth = os.path.join(data_pth, "bot1.txt") +fpth = pooch.retrieve( + url=f"https://github.com/MODFLOW-USGS/modflow6-examples/raw/master/data/{sim_name}/bot1.txt", + known_hash="md5:4343c79bbf3ad039638d2379d335d06e", +) bot1 = np.loadtxt(fpth, dtype=float) bot2 = np.ones_like(bot1) * 300.0 botm = [bot1, bot2] # Create hydraulic conductivity and specific yield - -fpth = os.path.join(data_pth, "k11_lay1.txt") +fpth = pooch.retrieve( + url=f"https://github.com/MODFLOW-USGS/modflow6-examples/raw/master/data/{sim_name}/k11_lay1.txt", + known_hash="md5:287160064d1a9bc0bae94b018bf187d7", +) k11_lay1 = np.loadtxt(fpth, dtype=float) * 2.5 k11_lay2 = np.ones_like(k11_lay1) * 0.35e-2 k11 = [k11_lay1, k11_lay2] k33 = 0.5e-5 -fpth = os.path.join(data_pth, "sy1.txt") +fpth = pooch.retrieve( + url=f"https://github.com/MODFLOW-USGS/modflow6-examples/raw/master/data/{sim_name}/sy1.txt", + known_hash="md5:80be4a9ba817465cf5c05934f94dd675", +) sy1 = np.loadtxt(fpth, dtype=float) sy2 = np.ones_like(sy1) * 0.20 sy = [sy1, sy2] -# ### Create SFR Package Problem 1 Model Boundary Conditions -# # General head boundary conditions -# - ghb_spd = [ [0, 12, 0, 988.0, 0.038], [0, 13, 8, 1045.0, 0.038], ] # Well boundary conditions - wel_spd = { 0: [ [0, 5, 3, 0], @@ -268,7 +275,6 @@ } # SFR Package - sfr_pakdata = [ ( 0, @@ -781,7 +787,6 @@ ] # UZF Package - uzf_pakdata = [ (0, (0, 0, 0), 1, 100, 0.1, 0.000001, 0.1, 0.3, 0.11, 3.5, "uzfcells"), (1, (0, 0, 1), 1, 101, 0.1, 0.000001, 0.1, 0.3, 0.11, 3.5, "uzfcells"), @@ -3549,58 +3554,10 @@ sp = [] iuzno = 0 for i in range(len(extwc)): - sp.append( - (iuzno, finf[tm][i], pET, extdp, extwc[i], ha, hroot, rootact) - ) + sp.append((iuzno, finf[tm][i], pET, extdp, extwc[i], ha, hroot, rootact)) iuzno += 1 uzf_spd.update({int(tm): sp}) -# LAK Package - -# -# lak_pakdata = [[0, 1040.0, 12], [1, 1010.0, 26]] -# -# lak_conn = [ -# [0, 0, (0, 8, 6), "HORIZONTAL", 2.00e-09, 0.0, 0.0, 2500.0, 5000.0], -# [0, 1, (0, 8, 7), "HORIZONTAL", 2.00e-09, 0.0, 0.0, 2500.0, 5000.0], -# [0, 2, (0, 9, 5), "HORIZONTAL", 2.00e-09, 0.0, 0.0, 2500.0, 5000.0], -# [0, 3, (0, 9, 8), "HORIZONTAL", 2.00e-09, 0.0, 0.0, 2500.0, 5000.0], -# [0, 4, (0, 10, 5), "HORIZONTAL", 2.00e-09, 0.0, 0.0, 2500.0, 5000.0], -# [0, 5, (0, 10, 8), "HORIZONTAL", 2.00e-09, 0.0, 0.0, 2500.0, 5000.0], -# [0, 6, (0, 11, 6), "HORIZONTAL", 2.00e-09, 0.0, 0.0, 2500.0, 5000.0], -# [0, 7, (0, 11, 7), "HORIZONTAL", 2.00e-09, 0.0, 0.0, 2500.0, 5000.0], -# [0, 8, (1, 9, 6), "VERTICAL", 2.00e-09, 0.0, 0.0, 0.0, 0.0], -# [0, 9, (1, 9, 7), "VERTICAL", 2.00e-09, 0.0, 0.0, 0.0, 0.0], -# [0, 10, (1, 10, 6), "VERTICAL", 2.00e-09, 0.0, 0.0, 0.0, 0.0], -# [0, 11, (1, 10, 7), "VERTICAL", 2.00e-09, 0.0, 0.0, 0.0, 0.0], -# [1, 0, (0, 9, 2), "HORIZONTAL", 2.00e-09, 0.0, 0.0, 2500.0, 5000.0], -# [1, 1, (0, 9, 3), "HORIZONTAL", 2.00e-09, 0.0, 0.0, 2500.0, 5000.0], -# [1, 2, (0, 9, 4), "HORIZONTAL", 2.00e-09, 0.0, 0.0, 2500.0, 5000.0], -# [1, 3, (0, 10, 1), "HORIZONTAL", 2.00e-09, 0.0, 0.0, 2500.0, 5000.0], -# [1, 4, (0, 10, 5), "HORIZONTAL", 2.00e-09, 0.0, 0.0, 2500.0, 5000.0], -# [1, 5, (0, 11, 1), "HORIZONTAL", 2.00e-09, 0.0, 0.0, 2500.0, 5000.0], -# [1, 6, (0, 11, 5), "HORIZONTAL", 2.00e-09, 0.0, 0.0, 2500.0, 5000.0], -# [1, 7, (0, 12, 1), "HORIZONTAL", 2.00e-09, 0.0, 0.0, 2500.0, 5000.0], -# [1, 8, (0, 12, 5), "HORIZONTAL", 2.00e-09, 0.0, 0.0, 2500.0, 5000.0], -# [1, 9, (0, 13, 1), "HORIZONTAL", 2.00e-09, 0.0, 0.0, 2500.0, 5000.0], -# [1, 10, (0, 13, 5), "HORIZONTAL", 2.00e-09, 0.0, 0.0, 2500.0, 5000.0], -# [1, 11, (0, 14, 2), "HORIZONTAL", 2.00e-09, 0.0, 0.0, 2500.0, 5000.0], -# [1, 12, (0, 14, 3), "HORIZONTAL", 2.00e-09, 0.0, 0.0, 2500.0, 5000.0], -# [1, 13, (0, 14, 4), "HORIZONTAL", 2.00e-09, 0.0, 0.0, 2500.0, 5000.0], -# [1, 14, (1, 10, 2), "VERTICAL", 2.00e-09, 0.0, 0.0, 0.0, 0.0], -# [1, 15, (1, 10, 3), "VERTICAL", 2.00e-09, 0.0, 0.0, 0.0, 0.0], -# [1, 16, (1, 10, 4), "VERTICAL", 2.00e-09, 0.0, 0.0, 0.0, 0.0], -# [1, 17, (1, 11, 2), "VERTICAL", 2.00e-09, 0.0, 0.0, 0.0, 0.0], -# [1, 18, (1, 11, 3), "VERTICAL", 2.00e-09, 0.0, 0.0, 0.0, 0.0], -# [1, 19, (1, 11, 4), "VERTICAL", 2.00e-09, 0.0, 0.0, 0.0, 0.0], -# [1, 20, (1, 12, 2), "VERTICAL", 2.00e-09, 0.0, 0.0, 0.0, 0.0], -# [1, 21, (1, 12, 3), "VERTICAL", 2.00e-09, 0.0, 0.0, 0.0, 0.0], -# [1, 22, (1, 12, 4), "VERTICAL", 2.00e-09, 0.0, 0.0, 0.0, 0.0], -# [1, 23, (1, 13, 2), "VERTICAL", 2.00e-09, 0.0, 0.0, 0.0, 0.0], -# [1, 24, (1, 13, 3), "VERTICAL", 2.00e-09, 0.0, 0.0, 0.0, 0.0], -# [1, 25, (1, 13, 4), "VERTICAL", 2.00e-09, 0.0, 0.0, 0.0, 0.0], -# ] - lak_stage = ( 1040.0, 1010.0, @@ -3748,879 +3705,821 @@ } # Solver parameters - nouter = 100 ninner = 50 hclose = 1e-6 rclose = 1e-6 +# - - -# ### Functions to build, write, run, and plot the MODFLOW 6 SFR Package Problem 1 model +# ### Model setup # -# MODFLOW 6 flopy simulation object (sim) is returned if building the model - - -def build_model(): - if config.buildModel: - sim_ws = os.path.join(ws, sim_name) - sim = flopy.mf6.MFSimulation( - sim_name=sim_name, sim_ws=sim_ws, exe_name="mf6" - ) - flopy.mf6.ModflowTdis( - sim, nper=nper, perioddata=tdis_ds, time_units=time_units - ) - flopy.mf6.ModflowIms( - sim, - print_option="summary", - complexity="complex", - outer_dvclose=1.0e-4, - outer_maximum=2000, - under_relaxation="dbd", - linear_acceleration="BICGSTAB", - under_relaxation_theta=0.7, - under_relaxation_kappa=0.08, - under_relaxation_gamma=0.05, - under_relaxation_momentum=0.0, - backtracking_number=20, - backtracking_tolerance=2.0, - backtracking_reduction_factor=0.2, - backtracking_residual_limit=5.0e-4, - inner_dvclose=1.0e-5, - rcloserecord="0.0001 relative_rclose", - inner_maximum=100, - relaxation_factor=0.0, - number_orthogonalizations=2, - preconditioner_levels=8, - preconditioner_drop_tolerance=0.001, - ) - gwf = flopy.mf6.ModflowGwf( - sim, modelname=sim_name, newtonoptions="newton", save_flows=True - ) - flopy.mf6.ModflowGwfdis( - gwf, - length_units=length_units, - nlay=nlay, - nrow=nrow, - ncol=ncol, - delr=delr, - delc=delc, - idomain=idomain, - top=top, - botm=botm, - ) - flopy.mf6.ModflowGwfnpf( - gwf, - alternative_cell_averaging="AMT-HMK", - icelltype=1, - k=k11, - k33=k33, - save_specific_discharge=True, - ) - flopy.mf6.ModflowGwfsto( - gwf, - iconvert=1, - sy=sy, - ss=ss, - steady_state={0: True}, - transient={1: True}, - ) - flopy.mf6.ModflowGwfic(gwf, strt=strt) - flopy.mf6.ModflowGwfghb(gwf, stress_period_data=ghb_spd) - flopy.mf6.ModflowGwfwel( - gwf, - print_input=True, - print_flows=True, - save_flows=True, - mover=True, - pname="WEL-1", - stress_period_data=wel_spd, - ) - sfr = flopy.mf6.ModflowGwfsfr( - gwf, - print_flows=True, - print_stage=True, - save_flows=True, - boundnames=True, - budget_filerecord=sim_name + ".sfr.bud", - mover=True, - pname="SFR-1", - maximum_depth_change=0.1e-05, - length_conversion=3.28081, - nreaches=len(sfr_pakdata), - packagedata=sfr_pakdata, - connectiondata=sfr_conn, - diversions=sfr_div, - perioddata=sfr_spd, - filename=f"{sim_name}.sfr", - ) - sfr_obs_file = f"{sim_name}.sfr.obs" - sfr_obs_dict = { - "sfr.reach01.csv": [ - ("extin", "ext-inflow", (0,)), - ("inflow", "inflow", (0,)), - ("frommvr", "from-mvr", (0,)), - ("gwf", "sfr", (0,)), - ("outflow", "outflow", (0,)), - ("extout", "ext-outflow", (0,)), - ("tomvr", "to-mvr", (0,)), - ], - "sfr.reach24.csv": [ - ("extin", "ext-inflow", (23,)), - ("inflow", "inflow", (23,)), - ("frommvr", "from-mvr", (23,)), - ("gwf", "sfr", (23,)), - ("outflow", "outflow", (23,)), - ("extout", "ext-outflow", (23,)), - ("tomvr", "to-mvr", (23,)), - ], - "sfr.reach29.csv": [ - ("extin", "ext-inflow", (28,)), - ("inflow", "inflow", (28,)), - ("frommvr", "from-mvr", (28,)), - ("gwf", "sfr", (28,)), - ("outflow", "outflow", (28,)), - ("extout", "ext-outflow", (28,)), - ("tomvr", "to-mvr", (28,)), - ], - "sfr.reach31.csv": [ - ("extin", "ext-inflow", (30,)), - ("inflow", "inflow", (30,)), - ("frommvr", "from-mvr", (30,)), - ("gwf", "sfr", (30,)), - ("outflow", "outflow", (30,)), - ("extout", "ext-outflow", (30,)), - ("tomvr", "to-mvr", (30,)), - ], - "sfr.allreaches.csv": [ - ("extin", "ext-inflow", "allreaches"), - ("inflow", "inflow", "allreaches"), - ("frommvr", "from-mvr", "allreaches"), - ("gwf", "sfr", "allreaches"), - ("outflow", "outflow", "allreaches"), - ("extout", "ext-outflow", "allreaches"), - ("tomvr", "to-mvr", "allreaches"), - ], - "sfr.stage.csv": [ - ("s01", "stage", (0,)), - ("s24", "stage", (23,)), - ("s29", "stage", (28,)), - ("s31", "stage", (30,)), - ], - } - sfr.obs.initialize( - filename=sfr_obs_file, - digits=10, - print_input=True, - continuous=sfr_obs_dict, - ) - - ( - idomain_wlakes, - lakepakdata_dict, - lakeconnectiondata, - ) = flopy.mf6.utils.get_lak_connections( - gwf.modelgrid, - lake_map, - idomain=gwf.dis.idomain.array, - bedleak=lake_leakance, - ) - lak_pakdata = [] - for key in lakepakdata_dict.keys(): - lak_pakdata.append([key, lak_stage[key], lakepakdata_dict[key]]) - lak = flopy.mf6.ModflowGwflak( - gwf, - print_stage=True, - print_flows=True, - save_flows=True, - budget_filerecord=sim_name + ".lak.bud", - length_conversion=3.28081, - mover=True, - pname="LAK-1", - boundnames=False, - nlakes=len(lak_pakdata), - noutlets=len(lak_outlets), - outlets=lak_outlets, - packagedata=lak_pakdata, - connectiondata=lakeconnectiondata, - perioddata=lk_spd, - filename=f"{sim_name}.lak", - ) - lak_obs_file = f"{sim_name}.lak.obs" - lak_obs_dict = { - "obs_lak.csv": [ - ("LAK1_STAGE", "STAGE", 1), - ("LAK2_STAGE", "STAGE", 2), - ("LAK1_TO-MVR", "TO-MVR", 1), - ("LAK1_FROM-MVR", "FROM-MVR", 1), - ("LAK2_TO-MVR", "TO-MVR", 2), - ("LAK2_FROM-MVR", "FROM-MVR", 2), - ] - } - lak.obs.initialize( - filename=lak_obs_file, digits=10, continuous=lak_obs_dict - ) - uzf = flopy.mf6.ModflowGwfuzf( - gwf, - nuzfcells=len(uzf_pakdata), - boundnames=True, - ntrailwaves=10, - nwavesets=50, - print_flows=False, - save_flows=True, - simulate_et=True, - linear_gwet=True, - simulate_gwseep=True, - mover=True, - packagedata=uzf_pakdata, - perioddata=uzf_spd, - budget_filerecord=f"{sim_name}.uzf.bud", - pname="UZF-1", - filename=f"{sim_name}.uzf", - ) - uzf_obs_file = f"{sim_name}.uzf.obs" - uzf_obs_dict = { - "obs_uzf.csv": [ - ("ninfil", "net-infiltration", "ag"), - ("rejinf", "rej-inf", "ag"), - ("rejinfmvr", "rej-inf-to-mvr", "ag"), - ("infil", "infiltration", "ag"), - ("frommvr", "from-mvr", "ag"), - ("gwrch", "uzf-gwrch", "ag"), - ("gwet", "uzf-gwet", "ag"), - ("uzet", "uzet", "ag"), - ], - "obs_uzf_column.csv": [ - ("id26_infil", "infiltration", 26), - ("id126_infil", "infiltration", 126), - ("id26_dpth=20", "water-content", 26, 20.0), - ( - "id126_dpth=51", - "water-content", - 126, - 1.0, - ), # DEPTH IS BELOW CELTOP - ("id26_rch", "uzf-gwrch", 26), - ("id126_rch", "uzf-gwrch", 126), - ("id26_gwet", "uzf-gwet", 26), - ("id126_gwet", "uzf-gwet", 126), - ("id26_uzet", "uzet", 26), - ("id126_uzet", "uzet", 126), - ("id26_gwd2mvr", "uzf-gwd-to-mvr", 26), - ("id126_gwd2mvr", "uzf-gwd-to-mvr", 126), - ("id26_rejinf", "rej-inf-to-mvr", 26), - ("id126_rejinf", "rej-inf-to-mvr", 126), - ], - } - uzf.obs.initialize( - filename=uzf_obs_file, print_input=True, continuous=uzf_obs_dict - ) - flopy.mf6.ModflowGwfmvr( - gwf, - maxmvr=max_mvr, - print_flows=True, - maxpackages=maxpackages, - packages=mvr_pack, - perioddata=mvr_spd, - pname="MVR-1", - budget_filerecord=sim_name + ".mvr.bud", - filename=f"{sim_name}.mvr", - ) - - # rest idomain with lake adjustments - gwf.dis.idomain = idomain_wlakes - - head_filerecord = f"{sim_name}.hds" - budget_filerecord = f"{sim_name}.cbc" - flopy.mf6.ModflowGwfoc( - gwf, - head_filerecord=head_filerecord, - budget_filerecord=budget_filerecord, - saverecord=[("HEAD", "LAST"), ("BUDGET", "LAST")], - ) - return sim - return None - - -# Function to write MODFLOW 6 SFR Package Problem 1 model files - - -def write_model(sim, silent=True): - if config.writeModel: - sim.write_simulation(silent=silent) - - -# Function to run the SFR Package Problem 1 model. -# True is returned if the model runs successfully -# - - -@config.timeit -def run_model(sim, silent=True): - success = True - if config.runModel: - success, buff = sim.run_simulation(silent=silent) - if not success: - print(buff) - return success - - -# Function to plot grid - - -def plot_grid(gwf, silent=True): - fs = USGSFigure(figure_type="map", verbose=False) - fig = plt.figure(figsize=figure_size, constrained_layout=False) - gs = mpl.gridspec.GridSpec(ncols=10, nrows=7, figure=fig, wspace=5) - plt.axis("off") - - axes = [] - axes.append(fig.add_subplot(gs[:6, :5])) - axes.append(fig.add_subplot(gs[:6, 5:], sharey=axes[0])) - - for ax in axes: - ax.set_xlim(extents[:2]) - ax.set_ylim(extents[2:]) - ax.set_aspect("equal") - - # legend axis - axes.append(fig.add_subplot(gs[6:, :])) - - # set limits for legend area - ax = axes[-1] - ax.set_xlim(0, 1) - ax.set_ylim(0, 1) - - # get rid of ticks and spines for legend area - ax.axis("off") - ax.set_xticks([]) - ax.set_yticks([]) - ax.spines["top"].set_color("none") - ax.spines["bottom"].set_color("none") - ax.spines["left"].set_color("none") - ax.spines["right"].set_color("none") - ax.patch.set_alpha(0.0) - - ax = axes[0] - mm = flopy.plot.PlotMapView(gwf, ax=ax, extent=extents) - top_coll = mm.plot_array( - top, vmin=1000, vmax=1120, masked_values=masked_values, alpha=0.5 +# Define functions to build models, write input files, and run the simulation. + + +# + +def build_models(): + sim_ws = os.path.join(workspace, sim_name) + sim = flopy.mf6.MFSimulation(sim_name=sim_name, sim_ws=sim_ws, exe_name="mf6") + flopy.mf6.ModflowTdis(sim, nper=nper, perioddata=tdis_ds, time_units=time_units) + flopy.mf6.ModflowIms( + sim, + print_option="summary", + complexity="complex", + outer_dvclose=1.0e-4, + outer_maximum=2000, + under_relaxation="dbd", + linear_acceleration="BICGSTAB", + under_relaxation_theta=0.7, + under_relaxation_kappa=0.08, + under_relaxation_gamma=0.05, + under_relaxation_momentum=0.0, + backtracking_number=20, + backtracking_tolerance=2.0, + backtracking_reduction_factor=0.2, + backtracking_residual_limit=5.0e-4, + inner_dvclose=1.0e-5, + rcloserecord="0.0001 relative_rclose", + inner_maximum=100, + relaxation_factor=0.0, + number_orthogonalizations=2, + preconditioner_levels=8, + preconditioner_drop_tolerance=0.001, ) - mm.plot_bc("SFR", color="green") - cv = mm.contour_array( - top, - levels=np.arange(1000, 1100, 20), - linewidths=0.5, - linestyles="-", - colors="black", - masked_values=masked_values, + gwf = flopy.mf6.ModflowGwf( + sim, modelname=sim_name, newtonoptions="newton", save_flows=True ) - plt.clabel(cv, fmt="%1.0f") - mm.plot_inactive(color_noflow="0.5") - mm.plot_grid(lw=0.5, color="black") - cbar = plt.colorbar( - top_coll, - shrink=0.8, - orientation="horizontal", - ax=ax, - format="%.0f", + flopy.mf6.ModflowGwfdis( + gwf, + length_units=length_units, + nlay=nlay, + nrow=nrow, + ncol=ncol, + delr=delr, + delc=delc, + idomain=idomain, + top=top, + botm=botm, ) - cbar.ax.tick_params(size=0) - cbar.ax.set_xlabel(r"Land surface elevation, $ft$") - ax.set_xlabel("x-coordinate, in feet") - ax.set_ylabel("y-coordinate, in feet") - fs.heading(ax, heading="Land surface elevation", idx=0) - fs.remove_edge_ticks(ax) - - ax = axes[1] - mm = flopy.plot.PlotMapView(gwf, ax=ax, extent=extents) - bot_coll = mm.plot_array( - botm, vmin=950, vmax=1100, masked_values=masked_values, alpha=0.5 + flopy.mf6.ModflowGwfnpf( + gwf, + alternative_cell_averaging="AMT-HMK", + icelltype=1, + k=k11, + k33=k33, + save_specific_discharge=True, ) - mm.plot_bc("GHB", color="purple") - mm.plot_bc("WEL", color="red", kper=1) - cv = mm.contour_array( - botm, - levels=np.arange(950, 1100, 20), - linewidths=0.5, - linestyles=":", - colors="black", - masked_values=masked_values, + flopy.mf6.ModflowGwfsto( + gwf, + iconvert=1, + sy=sy, + ss=ss, + steady_state={0: True}, + transient={1: True}, ) - plt.clabel(cv, fmt="%1.0f") - mm.plot_inactive(color_noflow="0.5") - mm.plot_grid(lw=0.5, color="black") - cbar = plt.colorbar( - bot_coll, - shrink=0.8, - orientation="horizontal", - ax=ax, - format="%.0f", + flopy.mf6.ModflowGwfic(gwf, strt=strt) + flopy.mf6.ModflowGwfghb(gwf, stress_period_data=ghb_spd) + flopy.mf6.ModflowGwfwel( + gwf, + print_input=True, + print_flows=True, + save_flows=True, + mover=True, + pname="WEL-1", + stress_period_data=wel_spd, ) - cbar.ax.tick_params(size=0) - cbar.ax.set_xlabel(r"Bottom elevation, $ft$") - ax.set_xlabel("x-coordinate, in feet") - fs.heading(ax, heading="Bottom elevation", idx=1) - fs.remove_edge_ticks(ax) - - # legend - ax = axes[-1] - ax.plot( - -10000, - -10000, - lw=0, - marker="s", - ms=10, - mfc="0.5", - mec="black", - markeredgewidth=0.5, - label="Inactive cells", + sfr = flopy.mf6.ModflowGwfsfr( + gwf, + print_flows=True, + print_stage=True, + save_flows=True, + boundnames=True, + budget_filerecord=sim_name + ".sfr.bud", + mover=True, + pname="SFR-1", + maximum_depth_change=0.1e-05, + length_conversion=3.28081, + nreaches=len(sfr_pakdata), + packagedata=sfr_pakdata, + connectiondata=sfr_conn, + diversions=sfr_div, + perioddata=sfr_spd, + filename=f"{sim_name}.sfr", ) - ax.plot( - -10000, - -10000, - lw=0, - marker="s", - ms=10, - mfc="green", - mec="black", - markeredgewidth=0.5, - label="Stream boundary", + sfr_obs_file = f"{sim_name}.sfr.obs" + sfr_obs_dict = { + "sfr.reach01.csv": [ + ("extin", "ext-inflow", (0,)), + ("inflow", "inflow", (0,)), + ("frommvr", "from-mvr", (0,)), + ("gwf", "sfr", (0,)), + ("outflow", "outflow", (0,)), + ("extout", "ext-outflow", (0,)), + ("tomvr", "to-mvr", (0,)), + ], + "sfr.reach24.csv": [ + ("extin", "ext-inflow", (23,)), + ("inflow", "inflow", (23,)), + ("frommvr", "from-mvr", (23,)), + ("gwf", "sfr", (23,)), + ("outflow", "outflow", (23,)), + ("extout", "ext-outflow", (23,)), + ("tomvr", "to-mvr", (23,)), + ], + "sfr.reach29.csv": [ + ("extin", "ext-inflow", (28,)), + ("inflow", "inflow", (28,)), + ("frommvr", "from-mvr", (28,)), + ("gwf", "sfr", (28,)), + ("outflow", "outflow", (28,)), + ("extout", "ext-outflow", (28,)), + ("tomvr", "to-mvr", (28,)), + ], + "sfr.reach31.csv": [ + ("extin", "ext-inflow", (30,)), + ("inflow", "inflow", (30,)), + ("frommvr", "from-mvr", (30,)), + ("gwf", "sfr", (30,)), + ("outflow", "outflow", (30,)), + ("extout", "ext-outflow", (30,)), + ("tomvr", "to-mvr", (30,)), + ], + "sfr.allreaches.csv": [ + ("extin", "ext-inflow", "allreaches"), + ("inflow", "inflow", "allreaches"), + ("frommvr", "from-mvr", "allreaches"), + ("gwf", "sfr", "allreaches"), + ("outflow", "outflow", "allreaches"), + ("extout", "ext-outflow", "allreaches"), + ("tomvr", "to-mvr", "allreaches"), + ], + "sfr.stage.csv": [ + ("s01", "stage", (0,)), + ("s24", "stage", (23,)), + ("s29", "stage", (28,)), + ("s31", "stage", (30,)), + ], + } + sfr.obs.initialize( + filename=sfr_obs_file, + digits=10, + print_input=True, + continuous=sfr_obs_dict, ) - ax.plot( - -10000, - -10000, - lw=0, - marker="s", - ms=10, - mfc="purple", - mec="black", - markeredgewidth=0.5, - label="General head boundary", + + ( + idomain_wlakes, + lakepakdata_dict, + lakeconnectiondata, + ) = flopy.mf6.utils.get_lak_connections( + gwf.modelgrid, + lake_map, + idomain=gwf.dis.idomain.array, + bedleak=lake_leakance, ) - ax.plot( - -10000, - -10000, - lw=0, - marker="s", - ms=10, - mfc="red", - mec="black", - markeredgewidth=0.5, - label="Well boundary", + lak_pakdata = [] + for key in lakepakdata_dict.keys(): + lak_pakdata.append([key, lak_stage[key], lakepakdata_dict[key]]) + lak = flopy.mf6.ModflowGwflak( + gwf, + print_stage=True, + print_flows=True, + save_flows=True, + budget_filerecord=sim_name + ".lak.bud", + length_conversion=3.28081, + mover=True, + pname="LAK-1", + boundnames=False, + nlakes=len(lak_pakdata), + noutlets=len(lak_outlets), + outlets=lak_outlets, + packagedata=lak_pakdata, + connectiondata=lakeconnectiondata, + perioddata=lk_spd, + filename=f"{sim_name}.lak", ) - ax.plot( - -10000, - -10000, - lw=0.5, - ls="-", - color="black", - label=r"Land surface elevation contour, $ft$", + lak_obs_file = f"{sim_name}.lak.obs" + lak_obs_dict = { + "obs_lak.csv": [ + ("LAK1_STAGE", "STAGE", 1), + ("LAK2_STAGE", "STAGE", 2), + ("LAK1_TO-MVR", "TO-MVR", 1), + ("LAK1_FROM-MVR", "FROM-MVR", 1), + ("LAK2_TO-MVR", "TO-MVR", 2), + ("LAK2_FROM-MVR", "FROM-MVR", 2), + ] + } + lak.obs.initialize(filename=lak_obs_file, digits=10, continuous=lak_obs_dict) + uzf = flopy.mf6.ModflowGwfuzf( + gwf, + nuzfcells=len(uzf_pakdata), + boundnames=True, + ntrailwaves=10, + nwavesets=50, + print_flows=False, + save_flows=True, + simulate_et=True, + linear_gwet=True, + simulate_gwseep=True, + mover=True, + packagedata=uzf_pakdata, + perioddata=uzf_spd, + budget_filerecord=f"{sim_name}.uzf.bud", + pname="UZF-1", + filename=f"{sim_name}.uzf", ) - ax.plot( - -10000, - -10000, - lw=0.5, - ls=":", - color="black", - label=r"Bottom elevation contour, $ft$", + uzf_obs_file = f"{sim_name}.uzf.obs" + uzf_obs_dict = { + "obs_uzf.csv": [ + ("ninfil", "net-infiltration", "ag"), + ("rejinf", "rej-inf", "ag"), + ("rejinfmvr", "rej-inf-to-mvr", "ag"), + ("infil", "infiltration", "ag"), + ("frommvr", "from-mvr", "ag"), + ("gwrch", "uzf-gwrch", "ag"), + ("gwet", "uzf-gwet", "ag"), + ("uzet", "uzet", "ag"), + ], + "obs_uzf_column.csv": [ + ("id26_infil", "infiltration", 26), + ("id126_infil", "infiltration", 126), + ("id26_dpth=20", "water-content", 26, 20.0), + ( + "id126_dpth=51", + "water-content", + 126, + 1.0, + ), # DEPTH IS BELOW CELTOP + ("id26_rch", "uzf-gwrch", 26), + ("id126_rch", "uzf-gwrch", 126), + ("id26_gwet", "uzf-gwet", 26), + ("id126_gwet", "uzf-gwet", 126), + ("id26_uzet", "uzet", 26), + ("id126_uzet", "uzet", 126), + ("id26_gwd2mvr", "uzf-gwd-to-mvr", 26), + ("id126_gwd2mvr", "uzf-gwd-to-mvr", 126), + ("id26_rejinf", "rej-inf-to-mvr", 26), + ("id126_rejinf", "rej-inf-to-mvr", 126), + ], + } + uzf.obs.initialize(filename=uzf_obs_file, print_input=True, continuous=uzf_obs_dict) + flopy.mf6.ModflowGwfmvr( + gwf, + maxmvr=max_mvr, + print_flows=True, + maxpackages=maxpackages, + packages=mvr_pack, + perioddata=mvr_spd, + pname="MVR-1", + budget_filerecord=sim_name + ".mvr.bud", + filename=f"{sim_name}.mvr", ) - fs.graph_legend(ax, loc="center", ncol=3) - # save figure - if config.plotSave: - fpth = os.path.join( - "..", - "figures", - f"{sim_name}-grid{config.figure_ext}", - ) - fig.savefig(fpth) - - return - - -# Function to plot grid - - -def plot_head_results(gwf, silent=True): - sim_ws = os.path.join(ws, sim_name) - - # create MODFLOW 6 head object - hobj = gwf.output.head() - - # create MODFLOW 6 cell-by-cell budget object - cobj = gwf.output.budget() + # rest idomain with lake adjustments + gwf.dis.idomain = idomain_wlakes - kstpkper = hobj.get_kstpkper() - - fs = USGSFigure(figure_type="map", verbose=False) - fig = plt.figure(figsize=figure_size, constrained_layout=False) - gs = mpl.gridspec.GridSpec(ncols=10, nrows=7, figure=fig, wspace=5) - plt.axis("off") - - axes = [fig.add_subplot(gs[:6, :5])] - axes.append(fig.add_subplot(gs[:6, 5:], sharey=axes[0])) + head_filerecord = f"{sim_name}.hds" + budget_filerecord = f"{sim_name}.cbc" + flopy.mf6.ModflowGwfoc( + gwf, + head_filerecord=head_filerecord, + budget_filerecord=budget_filerecord, + saverecord=[("HEAD", "LAST"), ("BUDGET", "LAST")], + ) + return sim - for ax in axes: - ax.set_xlim(extents[:2]) - ax.set_ylim(extents[2:]) - ax.set_aspect("equal") - # legend axis - axes.append(fig.add_subplot(gs[6:, :])) +def write_models(sim, silent=True): + sim.write_simulation(silent=silent) - # set limits for legend area - ax = axes[-1] - ax.set_xlim(0, 1) - ax.set_ylim(0, 1) - # get rid of ticks and spines for legend area - ax.axis("off") - ax.set_xticks([]) - ax.set_yticks([]) - ax.spines["top"].set_color("none") - ax.spines["bottom"].set_color("none") - ax.spines["left"].set_color("none") - ax.spines["right"].set_color("none") - ax.patch.set_alpha(0.0) +@timed +def run_models(sim, silent=True): + success, buff = sim.run_simulation(silent=silent) + assert success, buff - # extract heads and specific discharge for first stress period - head = hobj.get_data(kstpkper=kstpkper[0]) - qx, qy, qz = flopy.utils.postprocessing.get_specific_discharge( - cobj.get_data(text="DATA-SPDIS", kstpkper=kstpkper[0])[0], - gwf, - ) - ax = axes[0] - mm = flopy.plot.PlotMapView(gwf, ax=ax, extent=extents) - head_coll = mm.plot_array( - head, vmin=900, vmax=1120, masked_values=masked_values - ) - cv = mm.contour_array( - head, - levels=np.arange(900, 1100, 10), - linewidths=0.5, - linestyles="-", - colors="black", - masked_values=masked_values, - ) - plt.clabel(cv, fmt="%1.0f") - mm.plot_vector(qx, qy, normalize=True, color="0.75") - mm.plot_inactive(color_noflow="0.5") - mm.plot_grid(lw=0.5, color="black") - ax.set_xlabel("x-coordinate, in feet") - ax.set_ylabel("y-coordinate, in feet") - fs.heading(ax, heading="Steady-state", idx=0) - fs.remove_edge_ticks(ax) +# - - # extract heads and specific discharge for second stress period - head = hobj.get_data(kstpkper=kstpkper[1]) - qx, qy, qz = flopy.utils.postprocessing.get_specific_discharge( - cobj.get_data(text="DATA-SPDIS", kstpkper=kstpkper[1])[0], - gwf, - ) +# ### Plotting results +# +# Define functions to plot model results. - ax = axes[1] - mm = flopy.plot.PlotMapView(gwf, ax=ax, extent=extents) - head_coll = mm.plot_array( - head, vmin=900, vmax=1120, masked_values=masked_values - ) - cv = mm.contour_array( - head, - levels=np.arange(900, 1100, 10), - linewidths=0.5, - linestyles="-", - colors="black", - masked_values=masked_values, - ) - plt.clabel(cv, fmt="%1.0f") - mm.plot_vector(qx, qy, normalize=True, color="0.75") - mm.plot_inactive(color_noflow="0.5") - mm.plot_grid(lw=0.5, color="black") - ax.set_xlabel("x-coordinate, in feet") - fs.heading(ax, heading="Pumping", idx=1) - fs.remove_edge_ticks(ax) +# + +# Figure properties +figure_size = (6.3, 5.6) +masked_values = (0, 1e30, -1e30) - # legend - ax = axes[-1] - cbar = plt.colorbar( - head_coll, - shrink=0.8, - orientation="horizontal", - ax=ax, - format="%.0f", - ) - cbar.ax.tick_params(size=0) - cbar.ax.set_xlabel(r"Head, $ft$") - ax.plot( - -10000, - -10000, - lw=0, - marker="$\u2192$", - ms=10, - mfc="0.75", - mec="0.75", - label="Normalized specific discharge", - ) - ax.plot(-10000, -10000, lw=0.5, color="black", label=r"Head contour, $ft$") - fs.graph_legend(ax, loc="upper center", ncol=2) - # save figure - if config.plotSave: - fpth = os.path.join( - "..", - "figures", - f"{sim_name}-01{config.figure_ext}", +def plot_grid(gwf, silent=True): + with styles.USGSMap() as fs: + fig = plt.figure(figsize=figure_size, constrained_layout=False) + gs = mpl.gridspec.GridSpec(ncols=10, nrows=7, figure=fig, wspace=5) + plt.axis("off") + + axes = [] + axes.append(fig.add_subplot(gs[:6, :5])) + axes.append(fig.add_subplot(gs[:6, 5:], sharey=axes[0])) + + for ax in axes: + ax.set_xlim(extents[:2]) + ax.set_ylim(extents[2:]) + ax.set_aspect("equal") + + # legend axis + axes.append(fig.add_subplot(gs[6:, :])) + + # set limits for legend area + ax = axes[-1] + ax.set_xlim(0, 1) + ax.set_ylim(0, 1) + + # get rid of ticks and spines for legend area + ax.axis("off") + ax.set_xticks([]) + ax.set_yticks([]) + ax.spines["top"].set_color("none") + ax.spines["bottom"].set_color("none") + ax.spines["left"].set_color("none") + ax.spines["right"].set_color("none") + ax.patch.set_alpha(0.0) + + ax = axes[0] + mm = flopy.plot.PlotMapView(gwf, ax=ax, extent=extents) + top_coll = mm.plot_array( + top, vmin=1000, vmax=1120, masked_values=masked_values, alpha=0.5 ) - fig.savefig(fpth) - - return + mm.plot_bc("SFR", color="green") + cv = mm.contour_array( + top, + levels=np.arange(1000, 1100, 20), + linewidths=0.5, + linestyles="-", + colors="black", + masked_values=masked_values, + ) + plt.clabel(cv, fmt="%1.0f") + mm.plot_inactive(color_noflow="0.5") + mm.plot_grid(lw=0.5, color="black") + cbar = plt.colorbar( + top_coll, + shrink=0.8, + orientation="horizontal", + ax=ax, + format="%.0f", + ) + cbar.ax.tick_params(size=0) + cbar.ax.set_xlabel(r"Land surface elevation, $ft$") + ax.set_xlabel("x-coordinate, in feet") + ax.set_ylabel("y-coordinate, in feet") + styles.heading(ax, heading="Land surface elevation", idx=0) + styles.remove_edge_ticks(ax) + + ax = axes[1] + mm = flopy.plot.PlotMapView(gwf, ax=ax, extent=extents) + bot_coll = mm.plot_array( + botm, vmin=950, vmax=1100, masked_values=masked_values, alpha=0.5 + ) + mm.plot_bc("GHB", color="purple") + mm.plot_bc("WEL", color="red", kper=1) + cv = mm.contour_array( + botm, + levels=np.arange(950, 1100, 20), + linewidths=0.5, + linestyles=":", + colors="black", + masked_values=masked_values, + ) + plt.clabel(cv, fmt="%1.0f") + mm.plot_inactive(color_noflow="0.5") + mm.plot_grid(lw=0.5, color="black") + cbar = plt.colorbar( + bot_coll, + shrink=0.8, + orientation="horizontal", + ax=ax, + format="%.0f", + ) + cbar.ax.tick_params(size=0) + cbar.ax.set_xlabel(r"Bottom elevation, $ft$") + ax.set_xlabel("x-coordinate, in feet") + styles.heading(ax, heading="Bottom elevation", idx=1) + styles.remove_edge_ticks(ax) + + # legend + ax = axes[-1] + ax.plot( + -10000, + -10000, + lw=0, + marker="s", + ms=10, + mfc="0.5", + mec="black", + markeredgewidth=0.5, + label="Inactive cells", + ) + ax.plot( + -10000, + -10000, + lw=0, + marker="s", + ms=10, + mfc="green", + mec="black", + markeredgewidth=0.5, + label="Stream boundary", + ) + ax.plot( + -10000, + -10000, + lw=0, + marker="s", + ms=10, + mfc="purple", + mec="black", + markeredgewidth=0.5, + label="General head boundary", + ) + ax.plot( + -10000, + -10000, + lw=0, + marker="s", + ms=10, + mfc="red", + mec="black", + markeredgewidth=0.5, + label="Well boundary", + ) + ax.plot( + -10000, + -10000, + lw=0.5, + ls="-", + color="black", + label=r"Land surface elevation contour, $ft$", + ) + ax.plot( + -10000, + -10000, + lw=0.5, + ls=":", + color="black", + label=r"Bottom elevation contour, $ft$", + ) + styles.graph_legend(ax, loc="center", ncol=3) + + if plot_show: + plt.show() + if plot_save: + fpth = os.path.join( + "..", + "figures", + f"{sim_name}-grid.png", + ) + fig.savefig(fpth) -# Function to plot the mvr results +def plot_head_results(gwf, silent=True): + # create MODFLOW 6 head object + hobj = gwf.output.head() + # create MODFLOW 6 cell-by-cell budget object + cobj = gwf.output.budget() -def plot_mvr_results(idx, gwf, silent=True): - fs = USGSFigure(figure_type="graph", verbose=False) - sim_ws = os.path.join(ws, sim_name) + kstpkper = hobj.get_kstpkper() - # load the observations - mvr = gwf.get_package("MVR-1") - mvr_Q = mvr.output.budget() - # This retrieves all of the MOVER fluxes: - mvr_all = mvr_Q.get_data(text=" MOVER-FLOW") + with styles.USGSMap() as fs: + fig = plt.figure(figsize=figure_size, constrained_layout=False) + gs = mpl.gridspec.GridSpec(ncols=10, nrows=7, figure=fig, wspace=5) + plt.axis("off") + + axes = [fig.add_subplot(gs[:6, :5])] + axes.append(fig.add_subplot(gs[:6, 5:], sharey=axes[0])) + + for ax in axes: + ax.set_xlim(extents[:2]) + ax.set_ylim(extents[2:]) + ax.set_aspect("equal") + + # legend axis + axes.append(fig.add_subplot(gs[6:, :])) + + # set limits for legend area + ax = axes[-1] + ax.set_xlim(0, 1) + ax.set_ylim(0, 1) + + # get rid of ticks and spines for legend area + ax.axis("off") + ax.set_xticks([]) + ax.set_yticks([]) + ax.spines["top"].set_color("none") + ax.spines["bottom"].set_color("none") + ax.spines["left"].set_color("none") + ax.spines["right"].set_color("none") + ax.patch.set_alpha(0.0) + + # extract heads and specific discharge for first stress period + head = hobj.get_data(kstpkper=kstpkper[0]) + qx, qy, qz = flopy.utils.postprocessing.get_specific_discharge( + cobj.get_data(text="DATA-SPDIS", kstpkper=kstpkper[0])[0], + gwf, + ) - ckstpkper = mvr_Q.get_kstpkper() + ax = axes[0] + mm = flopy.plot.PlotMapView(gwf, ax=ax, extent=extents) + head_coll = mm.plot_array( + head, vmin=900, vmax=1120, masked_values=masked_values + ) + cv = mm.contour_array( + head, + levels=np.arange(900, 1100, 10), + linewidths=0.5, + linestyles="-", + colors="black", + masked_values=masked_values, + ) + plt.clabel(cv, fmt="%1.0f") + mm.plot_vector(qx, qy, normalize=True, color="0.75") + mm.plot_inactive(color_noflow="0.5") + mm.plot_grid(lw=0.5, color="black") + ax.set_xlabel("x-coordinate, in feet") + ax.set_ylabel("y-coordinate, in feet") + styles.heading(ax, heading="Steady-state", idx=0) + styles.remove_edge_ticks(ax) + + # extract heads and specific discharge for second stress period + head = hobj.get_data(kstpkper=kstpkper[1]) + qx, qy, qz = flopy.utils.postprocessing.get_specific_discharge( + cobj.get_data(text="DATA-SPDIS", kstpkper=kstpkper[1])[0], + gwf, + ) - # The following will tell us the contents of MVR recarrays - # mvr_Q.list_records() - # Connections specified in MVR input include: - # SFR -> LAK - # LAK -> SFR - # UZF -> SFR - # UZF -> LAK - # WEL -> UZF - provider = b"WEL-1 " - receiver1 = b"UZF-1 " - gwirrig = [] - for i, kstpkper in enumerate(ckstpkper): - # The following gets the actual indexes of where the conditions are - # satisfied, which is what the recarray needs - mvr_idxs = np.where( - np.all( - ( - np.array(mvr_Q.recordarray["kper"] == i), - np.array( - mvr_Q.recordarray["paknam"] == provider - ), # Provider - np.array(mvr_Q.recordarray["paknam2"] == receiver1), - ), - axis=0, + ax = axes[1] + mm = flopy.plot.PlotMapView(gwf, ax=ax, extent=extents) + head_coll = mm.plot_array( + head, vmin=900, vmax=1120, masked_values=masked_values + ) + cv = mm.contour_array( + head, + levels=np.arange(900, 1100, 10), + linewidths=0.5, + linestyles="-", + colors="black", + masked_values=masked_values, + ) + plt.clabel(cv, fmt="%1.0f") + mm.plot_vector(qx, qy, normalize=True, color="0.75") + mm.plot_inactive(color_noflow="0.5") + mm.plot_grid(lw=0.5, color="black") + ax.set_xlabel("x-coordinate, in feet") + styles.heading(ax, heading="Pumping", idx=1) + styles.remove_edge_ticks(ax) + + # legend + ax = axes[-1] + cbar = plt.colorbar( + head_coll, + shrink=0.8, + orientation="horizontal", + ax=ax, + format="%.0f", + ) + cbar.ax.tick_params(size=0) + cbar.ax.set_xlabel(r"Head, $ft$") + ax.plot( + -10000, + -10000, + lw=0, + marker="$\u2192$", + ms=10, + mfc="0.75", + mec="0.75", + label="Normalized specific discharge", + ) + ax.plot(-10000, -10000, lw=0.5, color="black", label=r"Head contour, $ft$") + styles.graph_legend(ax, loc="upper center", ncol=2) + + if plot_show: + plt.show() + if plot_save: + fpth = os.path.join( + "..", + "figures", + f"{sim_name}-01.png", ) - ) # Receiver - - tot_stp_gwirrig = 0 - if len(mvr_idxs[0]) > 0: - mvr_welirrig = mvr_all[mvr_idxs[0][0]] - # Loop through each row of the mvr_welirrig recarray for tallying - # gw irrigation events on a cell-by-cell basis + fig.savefig(fpth) - for k, itm in enumerate(mvr_welirrig): - # itm[1]: the receiver identifier. 2.6280e6: len of stress - # period - about 1 month - tot_stp_gwirrig += itm[2] - gwirrig.append( - abs(tot_stp_gwirrig) * 2.6280e6 / 43560 - ) # results in ac*ft - - # Get all groundwater discharge: - provider = b"UZF-1 " - receiver1 = b"SFR-1 " - receiver2 = b"LAK-1 " - tot_mvr_runoff = [] - for i, kstpkper in enumerate(ckstpkper): - mvr_runoff = 0 # Initialize - # The following gets the actual indexes of where the conditions are satisfied, which is what the recarray needs - mvr_idxs = np.where( - np.all( - ( - np.array(mvr_Q.recordarray["kper"] == i), - np.array( - mvr_Q.recordarray["paknam"] == provider - ), # Provider - np.logical_or( - mvr_Q.recordarray["paknam2"] == receiver1, # Receiver - mvr_Q.recordarray["paknam2"] == receiver2, +def plot_mvr_results(idx, gwf, silent=True): + with styles.USGSPlot() as fs: + # load the observations + mvr = gwf.get_package("MVR-1") + mvr_Q = mvr.output.budget() + # This retrieves all of the MOVER fluxes: + mvr_all = mvr_Q.get_data(text=" MOVER-FLOW") + + ckstpkper = mvr_Q.get_kstpkper() + + # The following will tell us the contents of MVR recarrays + # mvr_Q.list_records() + # Connections specified in MVR input include: + # SFR -> LAK + # LAK -> SFR + # UZF -> SFR + # UZF -> LAK + # WEL -> UZF + provider = b"WEL-1 " + receiver1 = b"UZF-1 " + gwirrig = [] + for i, kstpkper in enumerate(ckstpkper): + # The following gets the actual indexes of where the conditions are + # satisfied, which is what the recarray needs + mvr_idxs = np.where( + np.all( + ( + np.array(mvr_Q.recordarray["kper"] == i), + np.array(mvr_Q.recordarray["paknam"] == provider), # Provider + np.array(mvr_Q.recordarray["paknam2"] == receiver1), + ), + axis=0, + ) + ) # Receiver + + tot_stp_gwirrig = 0 + if len(mvr_idxs[0]) > 0: + mvr_welirrig = mvr_all[mvr_idxs[0][0]] + # Loop through each row of the mvr_welirrig recarray for tallying + # gw irrigation events on a cell-by-cell basis + + for k, itm in enumerate(mvr_welirrig): + # itm[1]: the receiver identifier. 2.6280e6: len of stress + # period - about 1 month + tot_stp_gwirrig += itm[2] + + gwirrig.append(abs(tot_stp_gwirrig) * 2.6280e6 / 43560) # results in ac*ft + + # Get all groundwater discharge: + provider = b"UZF-1 " + receiver1 = b"SFR-1 " + receiver2 = b"LAK-1 " + tot_mvr_runoff = [] + for i, kstpkper in enumerate(ckstpkper): + mvr_runoff = 0 # Initialize + # The following gets the actual indexes of where the conditions are satisfied, which is what the recarray needs + mvr_idxs = np.where( + np.all( + ( + np.array(mvr_Q.recordarray["kper"] == i), + np.array(mvr_Q.recordarray["paknam"] == provider), # Provider + np.logical_or( + mvr_Q.recordarray["paknam2"] == receiver1, # Receiver + mvr_Q.recordarray["paknam2"] == receiver2, + ), ), - ), - axis=0, + axis=0, + ) ) + # For each index (there will be two in this case because there are two receivers) + for j in range(len(mvr_idxs[0])): + mvr_rnf2sw = mvr_all[mvr_idxs[0][j]] + # Loop through each row of the mvr_uzf2sw for tallying flow totals + for k, itm in enumerate(mvr_rnf2sw): + mvr_runoff += itm[2] + # Tally the result + tot_mvr_runoff.append(abs(mvr_runoff) * 2.6280e6 / 43560) + + # Barplot of pumping and runoff + # set width of bar + barWidth = 0.25 + fig, ax = plt.subplots(figsize=figure_size) + + # set height of bar + bardat = [gwirrig, tot_mvr_runoff] + + # Set position of bar on X axis + br1 = np.arange(len(gwirrig)) + 1 + br2 = [x + barWidth for x in br1] + + # Make the plot + plt.bar( + br1, + gwirrig, + color="r", + width=barWidth, + edgecolor="grey", + label="GW Irrig", ) - # For each index (there will be two in this case because there are two receivers) - for j in range(len(mvr_idxs[0])): - mvr_rnf2sw = mvr_all[mvr_idxs[0][j]] - # Loop through each row of the mvr_uzf2sw for tallying flow totals - for k, itm in enumerate(mvr_rnf2sw): - mvr_runoff += itm[2] - # Tally the result - tot_mvr_runoff.append(abs(mvr_runoff) * 2.6280e6 / 43560) - - # Barplot of pumping and runoff - # set width of bar - barWidth = 0.25 - fig, ax = plt.subplots(figsize=figure_size) - - # set height of bar - bardat = [gwirrig, tot_mvr_runoff] - - # Set position of bar on X axis - br1 = np.arange(len(gwirrig)) + 1 - br2 = [x + barWidth for x in br1] - - # Make the plot - plt.bar( - br1, - gwirrig, - color="r", - width=barWidth, - edgecolor="grey", - label="GW Irrig", - ) - plt.bar( - br2, - tot_mvr_runoff, - color="g", - width=barWidth, - edgecolor="grey", - label="Runoff", - ) - - # Adding Xticks - plt.xlabel("Month") - plt.ylabel("Acre-feet") - plt.legend() - - title = "Total monthly mvr flux" - letter = chr(ord("@") + idx + 1) - fs.heading(letter=letter, heading=title) - - # save figure - if config.plotSave: - fpth = os.path.join( - "..", - "figures", - f"{sim_name}-mvr{config.figure_ext}", + plt.bar( + br2, + tot_mvr_runoff, + color="g", + width=barWidth, + edgecolor="grey", + label="Runoff", ) - fig.savefig(fpth) - return - - -# Function to plot the mvr results + # Adding Xticks + plt.xlabel("Month") + plt.ylabel("Acre-feet") + plt.legend() + + title = "Total monthly mvr flux" + letter = chr(ord("@") + idx + 1) + styles.heading(letter=letter, heading=title) + + if plot_show: + plt.show() + if plot_save: + fpth = os.path.join( + "..", + "figures", + f"{sim_name}-mvr.png", + ) + fig.savefig(fpth) def plot_uzfcolumn_results(idx, gwf, silent=True): - fs = USGSFigure(figure_type="graph", verbose=False) - sim_ws = os.path.join(ws, sim_name) - fname = os.path.join(sim_ws, "obs_uzf_column.csv") - uzf_dat = pd.read_csv(fname, header=0) - uzf_dat["time_days"] = uzf_dat["time"] / 86400 - x = uzf_dat["time_days"] - finf = uzf_dat["ID26_INFIL"] * 86400 / 43560 - wc = uzf_dat["ID26_DPTH=20"] - rch = (uzf_dat["ID26_RCH"] + uzf_dat["ID126_RCH"]) * 86400 / 43560 - gwet = (uzf_dat["ID26_GWET"] + uzf_dat["ID126_GWET"]) * 86400 / 43560 - uzet = (uzf_dat["ID26_UZET"] + uzf_dat["ID126_UZET"]) * 86400 / 43560 - gwdisq = ( - (uzf_dat["ID26_GWD2MVR"] + uzf_dat["ID126_GWD2MVR"]) * 86400 / 43560 - ) - rejinf = (uzf_dat["ID26_REJINF"] + uzf_dat["ID126_REJINF"]) * 86400 / 43560 - - fig = plt.figure(figsize=figure_size) - ax1 = fig.add_subplot(1, 1, 1) - - ln1 = ax1.plot(x, finf, color="b", label="Infiltration") - ln2 = ax1.plot(x, rch, color="g", label="Recharge") - ax2 = ax1.twinx() - ln3 = ax2.plot(x, wc, "r--", label="Water Content") - - ax1.set_xlabel("Days") - ax1.set_ylabel("UZ Fluxes - Row 5, Column 2, in ac*ft") - ax1.set_xlim(0, 740) - ax1.set_ylim(0, 16) - ax2.set_ylim(0.14, 0.3) - ax2.set_ylabel("Water Content") - ax1.yaxis.grid("on") - - lns = ln1 + ln2 + ln3 - labs = [l.get_label() for l in lns] - ax1.legend(lns, labs, loc="upper left") - - title = "Time series of UZ fluxes for row 5, column 2" - letter = chr(ord("@") + idx + 2) - fs.heading(letter=letter, heading=title) - - # save figure - if config.plotSave: - fpth = os.path.join( - "..", - "figures", - f"{sim_name}-uz{config.figure_ext}", - ) - fig.savefig(fpth) - - -# Function to plot the SFR Package Problem 1 model results. + with styles.USGSPlot() as fs: + sim_ws = os.path.join(workspace, sim_name) + fname = os.path.join(sim_ws, "obs_uzf_column.csv") + uzf_dat = pd.read_csv(fname, header=0) + uzf_dat["time_days"] = uzf_dat["time"] / 86400 + x = uzf_dat["time_days"] + finf = uzf_dat["ID26_INFIL"] * 86400 / 43560 + wc = uzf_dat["ID26_DPTH=20"] + rch = (uzf_dat["ID26_RCH"] + uzf_dat["ID126_RCH"]) * 86400 / 43560 + gwet = (uzf_dat["ID26_GWET"] + uzf_dat["ID126_GWET"]) * 86400 / 43560 + uzet = (uzf_dat["ID26_UZET"] + uzf_dat["ID126_UZET"]) * 86400 / 43560 + gwdisq = (uzf_dat["ID26_GWD2MVR"] + uzf_dat["ID126_GWD2MVR"]) * 86400 / 43560 + rejinf = (uzf_dat["ID26_REJINF"] + uzf_dat["ID126_REJINF"]) * 86400 / 43560 + + fig = plt.figure(figsize=figure_size) + ax1 = fig.add_subplot(1, 1, 1) + + ln1 = ax1.plot(x, finf, color="b", label="Infiltration") + ln2 = ax1.plot(x, rch, color="g", label="Recharge") + ax2 = ax1.twinx() + ln3 = ax2.plot(x, wc, "r--", label="Water Content") + + ax1.set_xlabel("Days") + ax1.set_ylabel("UZ Fluxes - Row 5, Column 2, in ac*ft") + ax1.set_xlim(0, 740) + ax1.set_ylim(0, 16) + ax2.set_ylim(0.14, 0.3) + ax2.set_ylabel("Water Content") + ax1.yaxis.grid("on") + + lns = ln1 + ln2 + ln3 + labs = [l.get_label() for l in lns] + ax1.legend(lns, labs, loc="upper left") + + title = "Time series of UZ fluxes for row 5, column 2" + letter = chr(ord("@") + idx + 2) + styles.heading(letter=letter, heading=title) + + if plot_show: + plt.show() + if plot_save: + fpth = os.path.join( + "..", + "figures", + f"{sim_name}-uz.png", + ) + fig.savefig(fpth) def plot_results(idx, sim, silent=True): - if config.plotModel: - gwf = sim.get_model(sim_name) - - plot_grid(gwf, silent=silent) - - plot_head_results(gwf, silent=silent) + gwf = sim.get_model(sim_name) + plot_grid(gwf, silent=silent) + plot_head_results(gwf, silent=silent) + plot_mvr_results(idx, gwf, silent=silent) + plot_uzfcolumn_results(idx, gwf, silent=silent) - plot_mvr_results(idx, gwf, silent=silent) - plot_uzfcolumn_results(idx, gwf, silent=silent) +# - - -# Function that wraps all of the steps for the SFR Package Problem 1 model -# -# 1. build_model, -# 2. write_model, -# 3. run_model, and -# 4. plot_results. +# ### Running the example # +# Define and invoke a function to run the example scenario, then plot results. +# + def simulation(idx, silent=True): - sim = build_model() - - write_model(sim, silent=silent) - - success = run_model(sim, silent=silent) - assert success, f"could not run...{sim_name}" - - if success: + sim = build_models() + if write: + write_models(sim, silent=silent) + if run: + run_models(sim, silent=silent) + if plot: plot_results(idx, sim, silent=silent) -# nosetest - exclude block from this nosetest to the next nosetest -def test_01(): - simulation(0, silent=False) - - -# nosetest end - -if __name__ == "__main__": - # ### SFR Package Problem 1 Simulation - # - # Simulated heads in model the unconfined, middle, and lower aquifers (model layers - # 1, 3, and 5) are shown in the figure below. MODFLOW-2005 results for a quasi-3D - # model are also shown. The location of drain (green) and well (gray) boundary - # conditions, normalized specific discharge, and head contours (25 ft contour - # intervals) are also shown. - - simulation(0) +# Simulated heads in model the unconfined, middle, and lower aquifers (model layers +# 1, 3, and 5) are shown in the figure below. MODFLOW-2005 results for a quasi-3D +# model are also shown. The location of drain (green) and well (gray) boundary +# conditions, normalized specific discharge, and head contours (25 ft contour +# intervals) are also shown. +simulation(0) +# - diff --git a/scripts/ex-gwf-spbc.py b/scripts/ex-gwf-spbc.py index eee2bfe3b..3a885fcb1 100644 --- a/scripts/ex-gwf-spbc.py +++ b/scripts/ex-gwf-spbc.py @@ -3,47 +3,43 @@ # Periodic boundary condition problem is based on Laattoe and others (2014). # A MODFLOW 6 GWF-GWF Exchange is used to connect the left column with the # right column. -# -# ### SPBC Problem Setup +# ### Initial setup # -# Imports +# Import dependencies, define the example name and workspace, and read settings from environment variables. +# + import os -import sys +import pathlib as pl import flopy import matplotlib.pyplot as plt import numpy as np +from flopy.plot.styles import styles +from modflow_devtools.misc import get_env, timed -# Append to system path to include the common subdirectory - -sys.path.append(os.path.join("..", "common")) - -# import common functionality - -import config -from figspecs import USGSFigure - -# Set default figure properties - -figure_size = (6, 4) - -# Base simulation and model name and workspace - -ws = config.base_ws +# Example name and base workspace +sim_name = "ex-gwf-spbc" +workspace = pl.Path("../examples") -# Simulation name +# Settings from environment variables +write = get_env("WRITE", True) +run = get_env("RUN", True) +plot = get_env("PLOT", True) +plot_show = get_env("PLOT_SHOW", True) +plot_save = get_env("PLOT_SAVE", True) +# - -sim_name = "ex-gwf-spbc" +# ### Define parameters +# +# Define model units, parameters and other settings. +# + # Model units - length_units = "meters" time_units = "days" -# Table SPBC Model Parameters - +# Model parameters nper = 1 # Number of periods nlay = 190 # Number of layers ncol = 100 # Number of columns @@ -60,206 +56,180 @@ # Simulation has 1 steady stress period (1 day) # and 3 transient stress periods (10 days each). # Each transient stress period has 120 2-hour time steps. - perlen = [1.0] nstp = [1] tsmult = [1.0] tdis_ds = list(zip(perlen, nstp, tsmult)) # assign botm - botm = [top - k * delv for k in range(1, nlay + 1)] # Solver parameters - nouter = 50 ninner = 100 hclose = 1e-9 rclose = 1e-6 +# - -# ### Functions to build, write, run, and plot the MODFLOW 6 SPBC model +# ### Model setup # -# MODFLOW 6 flopy simulation object (sim) is returned if building the model +# Define functions to build models, write input files, and run the simulation. + + +# + +def build_models(): + sim_ws = os.path.join(workspace, sim_name) + sim = flopy.mf6.MFSimulation(sim_name=sim_name, sim_ws=sim_ws, exe_name="mf6") + flopy.mf6.ModflowTdis(sim, nper=nper, perioddata=tdis_ds, time_units=time_units) + flopy.mf6.ModflowIms( + sim, + outer_maximum=nouter, + outer_dvclose=hclose, + inner_maximum=ninner, + inner_dvclose=hclose, + rcloserecord=f"{rclose} strict", + ) + gwf = flopy.mf6.ModflowGwf(sim, modelname=sim_name, save_flows=True) + flopy.mf6.ModflowGwfdis( + gwf, + length_units=length_units, + nlay=nlay, + nrow=nrow, + ncol=ncol, + delr=delr, + delc=delc, + top=top, + botm=botm, + ) + ihc, cl1, cl2, hwva = 1, delr / 2.0, delr / 2.0, delc + angldegx = 90.0 + cdist = delr + exgdata = [ + [(k, 0, 0), (k, 0, ncol - 1), ihc, cl1, cl2, hwva, angldegx, cdist] + for k in range(nlay) + ] + exg = flopy.mf6.ModflowGwfgwf( + sim, + exgtype="GWF6-GWF6", + nexg=len(exgdata), + auxiliary=["ANGLDEGX", "CDIST"], + exgmnamea=sim_name, + exgmnameb=sim_name, + exchangedata=exgdata, + ) + flopy.mf6.ModflowGwfnpf( + gwf, + icelltype=icelltype, + k=hydraulic_conductivity, + save_specific_discharge=True, + ) + flopy.mf6.ModflowGwfic(gwf, strt=strt) + + hm = 1.0 + lmbda = ncol * delr + wv = 2 * np.pi / lmbda + x = gwf.modelgrid.xcellcenters + chd_head = hm * np.sin(wv * x) + chd_spd = [] + for j in range(ncol): + chd_spd.append([0, 0, j, chd_head[0, j]]) + flopy.mf6.ModflowGwfchd( + gwf, + stress_period_data=chd_spd, + pname="CHD", + ) + head_filerecord = f"{sim_name}.hds" + budget_filerecord = f"{sim_name}.cbc" + flopy.mf6.ModflowGwfoc( + gwf, + head_filerecord=head_filerecord, + budget_filerecord=budget_filerecord, + saverecord=[("HEAD", "ALL"), ("BUDGET", "ALL")], + ) + return sim -def build_model(): - if config.buildModel: - sim_ws = os.path.join(ws, sim_name) - sim = flopy.mf6.MFSimulation( - sim_name=sim_name, sim_ws=sim_ws, exe_name="mf6" - ) - flopy.mf6.ModflowTdis( - sim, nper=nper, perioddata=tdis_ds, time_units=time_units - ) - flopy.mf6.ModflowIms( - sim, - outer_maximum=nouter, - outer_dvclose=hclose, - inner_maximum=ninner, - inner_dvclose=hclose, - rcloserecord=f"{rclose} strict", - ) - gwf = flopy.mf6.ModflowGwf(sim, modelname=sim_name, save_flows=True) - flopy.mf6.ModflowGwfdis( - gwf, - length_units=length_units, - nlay=nlay, - nrow=nrow, - ncol=ncol, - delr=delr, - delc=delc, - top=top, - botm=botm, - ) - ihc, cl1, cl2, hwva = 1, delr / 2.0, delr / 2.0, delc - angldegx = 90.0 - cdist = delr - exgdata = [ - [(k, 0, 0), (k, 0, ncol - 1), ihc, cl1, cl2, hwva, angldegx, cdist] - for k in range(nlay) - ] - exg = flopy.mf6.ModflowGwfgwf( - sim, - exgtype="GWF6-GWF6", - nexg=len(exgdata), - auxiliary=["ANGLDEGX", "CDIST"], - exgmnamea=sim_name, - exgmnameb=sim_name, - exchangedata=exgdata, - ) - flopy.mf6.ModflowGwfnpf( - gwf, - icelltype=icelltype, - k=hydraulic_conductivity, - save_specific_discharge=True, - ) - flopy.mf6.ModflowGwfic(gwf, strt=strt) - - hm = 1.0 - lmbda = ncol * delr - wv = 2 * np.pi / lmbda - x = gwf.modelgrid.xcellcenters - chd_head = hm * np.sin(wv * x) - chd_spd = [] - for j in range(ncol): - chd_spd.append([0, 0, j, chd_head[0, j]]) - flopy.mf6.ModflowGwfchd( - gwf, - stress_period_data=chd_spd, - pname="CHD", - ) - head_filerecord = f"{sim_name}.hds" - budget_filerecord = f"{sim_name}.cbc" - flopy.mf6.ModflowGwfoc( - gwf, - head_filerecord=head_filerecord, - budget_filerecord=budget_filerecord, - saverecord=[("HEAD", "ALL"), ("BUDGET", "ALL")], - ) - return sim - return None +def write_models(sim, silent=True): + sim.write_simulation(silent=silent) -# Function to write MODFLOW 6 SPBC model files +@timed +def run_models(sim, silent=False): + success, buff = sim.run_simulation(silent=silent, report=True) + assert success, buff -def write_model(sim, silent=True): - if config.writeModel: - sim.write_simulation(silent=silent) +# - - -# Function to run the SPBC model. -# True is returned if the model runs successfully +# ### Plotting results # +# Define functions to plot model results. - -@config.timeit -def run_model(sim, silent=False): - success = True - if config.runModel: - success, buff = sim.run_simulation(silent=silent, report=True) - if not success: - print(buff) - return success +# + +# Figure properties +figure_size = (6, 4) -# Function to plot the SPBC model results. -# def plot_grid(sim): - fs = USGSFigure(figure_type="map", verbose=False) - sim_ws = os.path.join(ws, sim_name) - gwf = sim.get_model(sim_name) + with styles.USGSMap(): + gwf = sim.get_model(sim_name) - fig = plt.figure(figsize=figure_size) - fig.tight_layout() + fig = plt.figure(figsize=figure_size) + fig.tight_layout() - # create MODFLOW 6 head object - head = gwf.output.head().get_data() + # create MODFLOW 6 head object + head = gwf.output.head().get_data() - # create MODFLOW 6 cell-by-cell budget object - qx, qy, qz = flopy.utils.postprocessing.get_specific_discharge( - gwf.output.budget().get_data(text="DATA-SPDIS", totim=1.0)[0], - gwf, - ) + # create MODFLOW 6 cell-by-cell budget object + qx, qy, qz = flopy.utils.postprocessing.get_specific_discharge( + gwf.output.budget().get_data(text="DATA-SPDIS", totim=1.0)[0], + gwf, + ) - ax = fig.add_subplot(1, 1, 1, aspect="equal") - pxs = flopy.plot.PlotCrossSection(model=gwf, ax=ax, line={"row": 0}) - # pxs.plot_grid() - pxs.plot_bc(name="CHD") - pxs.plot_array(head, cmap="jet") - levels = np.arange(-1, 1, 0.1) - cs = pxs.contour_array( - head, levels=levels, colors="k", linewidths=1.0, linestyles="-" - ) - pxs.plot_vector(qx, qy, qz, normalize=False, kstep=5, hstep=5) - ax.set_xlabel("x position (m)") - ax.set_ylabel("z position (m)") - ax.set_ylim(-3, 0) - fs.remove_edge_ticks(ax) - plt.clabel(cs, fmt="%3.1f", fontsize=5) - - # save figure - if config.plotSave: - fpth = os.path.join( - "..", "figures", f"{sim_name}-grid{config.figure_ext}" + ax = fig.add_subplot(1, 1, 1, aspect="equal") + pxs = flopy.plot.PlotCrossSection(model=gwf, ax=ax, line={"row": 0}) + # pxs.plot_grid() + pxs.plot_bc(name="CHD") + pxs.plot_array(head, cmap="jet") + levels = np.arange(-1, 1, 0.1) + cs = pxs.contour_array( + head, levels=levels, colors="k", linewidths=1.0, linestyles="-" ) - fig.savefig(fpth) - return + pxs.plot_vector(qx, qy, qz, normalize=False, kstep=5, hstep=5) + ax.set_xlabel("x position (m)") + ax.set_ylabel("z position (m)") + ax.set_ylim(-3, 0) + styles.remove_edge_ticks(ax) + plt.clabel(cs, fmt="%3.1f", fontsize=5) + + if plot_show: + plt.show() + if plot_save: + fpth = os.path.join("..", "figures", f"{sim_name}-grid.png") + fig.savefig(fpth) def plot_results(sim, silent=True): - if config.plotModel: - plot_grid(sim) - return + plot_grid(sim) -# Function that wraps all of the steps for the SPBC model -# -# 1. build_model, -# 2. write_model, -# 3. run_model, and -# 4. plot_results. +# - + +# ### Running the example # +# Define and invoke a function to run the example scenario, then plot results. -def simulation(silent=True): - sim = build_model() - write_model(sim, silent=silent) - success = run_model(sim, silent=silent) - if success: +# + +def scenario(silent=True): + sim = build_models() + if write: + write_models(sim, silent=silent) + if run: + run_models(sim, silent=silent) + if plot: plot_results(sim, silent=silent) -# nosetest - exclude block from this nosetest to the next nosetest -def test_01(): - simulation(silent=False) - - -# nosetest end - -if __name__ == "__main__": - # ### SPBC Simulation - # - # Model grid and simulation results - - simulation() +scenario() +# - diff --git a/scripts/ex-gwf-twri.py b/scripts/ex-gwf-twri.py index c36415280..e70d65da5 100644 --- a/scripts/ex-gwf-twri.py +++ b/scripts/ex-gwf-twri.py @@ -3,52 +3,47 @@ # This problem is described in McDonald and Harbaugh (1988) and duplicated in # Harbaugh and McDonald (1996). This problem is also is distributed with # MODFLOW-2005 (Harbaugh, 2005) and MODFLOW 6 (Langevin and others, 2017). -# -# ### TWRI Problem Setup +# ### Initial setup # -# Imports +# Import dependencies, define the example name and workspace, and read settings from environment variables. +# + import os -import sys +import pathlib as pl import flopy import matplotlib.pyplot as plt import numpy as np +from flopy.plot.styles import styles +from modflow_devtools.misc import get_env, timed -# Append to system path to include the common subdirectory - -sys.path.append(os.path.join("..", "common")) - -# import common functionality - -import config -from figspecs import USGSFigure - -# Set figure properties specific to the - -figure_size = (6, 6) - -# Base simulation and model name and workspace - -ws = config.base_ws +# Example name and base workspace +sim_name = "ex-gwf-twri01" +workspace = pl.Path("../examples") -# Simulation name +# Settings from environment variables +write = get_env("WRITE", True) +run = get_env("RUN", True) +plot = get_env("PLOT", True) +plot_show = get_env("PLOT_SHOW", True) +plot_save = get_env("PLOT_SAVE", True) +# - -sim_name = "ex-gwf-twri01" +# ### Define parameters +# +# Define model units, parameters and other settings. +# + # Scenario parameter units - make sure there is at least one blank line before next item # add parameter_units to add units to the scenario parameter table - parameter_units = {"recharge": "$ft/s$"} # Model units - length_units = "feet" time_units = "seconds" -# Table TWRI Model Parameters - +# Model parameters nper = 1 # Number of periods nlay = 5 # Number of layers ncol = 15 # Number of columns @@ -56,49 +51,42 @@ delr = 5000.0 # Column width ($ft$) delc = 5000.0 # Row width ($ft$) top = 200.0 # Top of the model ($ft$) -botm_str = ( - "-150.0, -200.0, -300.0, -350.0, -450.0" # Layer bottom elevations ($ft$) -) +botm_str = "-150.0, -200.0, -300.0, -350.0, -450.0" # Layer bottom elevations ($ft$) strt = 0.0 # Starting head ($ft$) icelltype_str = "1, 0, 0, 0, 0" # Cell conversion type k11_str = "1.0e-3, 1.0e-8, 1.0e-4, 5.0e-7, 2.0e-4" # Horizontal hydraulic conductivity ($ft/s$) -k33_str = "1.0e-3, 1.0e-8, 1.0e-4, 5.0e-7, 2.0e-4" # Vertical hydraulic conductivity ($ft/s$) +k33_str = ( + "1.0e-3, 1.0e-8, 1.0e-4, 5.0e-7, 2.0e-4" # Vertical hydraulic conductivity ($ft/s$) +) recharge = 3e-8 # Recharge rate ($ft/s$) # Static temporal data used by TDIS file - perlen = 8.640e04 nstp = 1 tsmult = 1.0 tdis_ds = ((perlen, nstp, tsmult),) # parse parameter strings into tuples - botm = [float(value) for value in botm_str.split(",")] k11 = [float(value) for value in k11_str.split(",")] k33 = [float(value) for value in k33_str.split(",")] icelltype = [int(value) for value in icelltype_str.split(",")] -# ### Create TWRI Model Boundary Conditions -# +# Create TWRI Model Boundary Conditions # Constant head cells are specified on the west edge of the model # in model layers 1 and 2 `(k, i, j)` = $(1 \rightarrow 2, 1 \rightarrow 15, 1)$ -# - chd_spd = [] for k in (0, 2): chd_spd += [[k, i, 0, 0.0] for i in range(nrow)] chd_spd = {0: chd_spd} # Constant head cells for MODFLOW-2005 model - chd_spd0 = [] for k in (0, 1): chd_spd0 += [[k, i, 0, 0.0, 0.0] for i in range(nrow)] chd_spd0 = {0: chd_spd0} # Well boundary conditions - wel_spd = { 0: [ [4, 4, 10, -5.0], @@ -120,7 +108,6 @@ } # Well boundary conditions for MODFLOW-2005 model - wel_spd0 = [] layer_map = {0: 0, 2: 1, 4: 2} for k, i, j, q in wel_spd[0]: @@ -128,9 +115,7 @@ wel_spd0.append([kk, i, j, q]) wel_spd0 = {0: wel_spd0} - # Drain boundary conditions - drn_spd = { 0: [ [0, 7, 1, 0.0, 1.0e0], @@ -146,158 +131,140 @@ } # Solver parameters - nouter = 50 ninner = 100 hclose = 1e-9 rclose = 1e-6 +# - -# ### Functions to build, write, run, and plot the MODFLOW 6 TWRI model +# ### Model setup # -# MODFLOW 6 flopy simulation object (sim) is returned if building the model - - -def build_model(): - if config.buildModel: - sim_ws = os.path.join(ws, sim_name) - sim = flopy.mf6.MFSimulation( - sim_name=sim_name, sim_ws=sim_ws, exe_name="mf6" - ) - flopy.mf6.ModflowTdis( - sim, nper=nper, perioddata=tdis_ds, time_units=time_units - ) - flopy.mf6.ModflowIms( - sim, - outer_maximum=nouter, - outer_dvclose=hclose, - inner_maximum=ninner, - inner_dvclose=hclose, - rcloserecord=f"{rclose} strict", - ) - gwf = flopy.mf6.ModflowGwf(sim, modelname=sim_name, save_flows=True) - flopy.mf6.ModflowGwfdis( - gwf, - length_units=length_units, - nlay=nlay, - nrow=nrow, - ncol=ncol, - delr=delr, - delc=delc, - top=top, - botm=botm, - ) - flopy.mf6.ModflowGwfnpf( - gwf, - cvoptions="perched", - perched=True, - icelltype=icelltype, - k=k11, - k33=k33, - save_specific_discharge=True, - ) - flopy.mf6.ModflowGwfic(gwf, strt=strt) - flopy.mf6.ModflowGwfchd(gwf, stress_period_data=chd_spd) - flopy.mf6.ModflowGwfdrn(gwf, stress_period_data=drn_spd) - flopy.mf6.ModflowGwfwel(gwf, stress_period_data=wel_spd) - flopy.mf6.ModflowGwfrcha(gwf, recharge=recharge) - head_filerecord = f"{sim_name}.hds" - budget_filerecord = f"{sim_name}.cbc" - flopy.mf6.ModflowGwfoc( - gwf, - head_filerecord=head_filerecord, - budget_filerecord=budget_filerecord, - saverecord=[("HEAD", "ALL"), ("BUDGET", "ALL")], - ) - return sim - return None - - -# MODFLOW-2005 model object (mf) is returned if building the model +# Define functions to build models, write input files, and run the simulation. + + +# + +def build_models(): + sim_ws = os.path.join(workspace, sim_name) + sim = flopy.mf6.MFSimulation(sim_name=sim_name, sim_ws=sim_ws, exe_name="mf6") + flopy.mf6.ModflowTdis(sim, nper=nper, perioddata=tdis_ds, time_units=time_units) + flopy.mf6.ModflowIms( + sim, + outer_maximum=nouter, + outer_dvclose=hclose, + inner_maximum=ninner, + inner_dvclose=hclose, + rcloserecord=f"{rclose} strict", + ) + gwf = flopy.mf6.ModflowGwf(sim, modelname=sim_name, save_flows=True) + flopy.mf6.ModflowGwfdis( + gwf, + length_units=length_units, + nlay=nlay, + nrow=nrow, + ncol=ncol, + delr=delr, + delc=delc, + top=top, + botm=botm, + ) + flopy.mf6.ModflowGwfnpf( + gwf, + cvoptions="perched", + perched=True, + icelltype=icelltype, + k=k11, + k33=k33, + save_specific_discharge=True, + ) + flopy.mf6.ModflowGwfic(gwf, strt=strt) + flopy.mf6.ModflowGwfchd(gwf, stress_period_data=chd_spd) + flopy.mf6.ModflowGwfdrn(gwf, stress_period_data=drn_spd) + flopy.mf6.ModflowGwfwel(gwf, stress_period_data=wel_spd) + flopy.mf6.ModflowGwfrcha(gwf, recharge=recharge) + head_filerecord = f"{sim_name}.hds" + budget_filerecord = f"{sim_name}.cbc" + flopy.mf6.ModflowGwfoc( + gwf, + head_filerecord=head_filerecord, + budget_filerecord=budget_filerecord, + saverecord=[("HEAD", "ALL"), ("BUDGET", "ALL")], + ) + return sim def build_mf5model(): - if config.buildModel: - sim_ws = os.path.join(ws, sim_name, "mf2005") - mf = flopy.modflow.Modflow( - modelname=sim_name, model_ws=sim_ws, exe_name="mf2005dbl" - ) - flopy.modflow.ModflowDis( - mf, - nlay=3, - nrow=nrow, - ncol=ncol, - delr=delr, - delc=delc, - laycbd=[1, 1, 0], - top=top, - botm=botm, - nper=1, - perlen=perlen, - nstp=nstp, - tsmult=tsmult, - ) - flopy.modflow.ModflowBas(mf, strt=strt) - flopy.modflow.ModflowLpf( - mf, - laytyp=[1, 0, 0], - hk=[k11[0], k11[2], k11[4]], - vka=[k11[0], k11[2], k11[4]], - vkcb=[k11[1], k11[3], 0], - ss=0, - sy=0.0, - ) - flopy.modflow.ModflowChd(mf, stress_period_data=chd_spd0) - flopy.modflow.ModflowDrn(mf, stress_period_data=drn_spd) - flopy.modflow.ModflowWel(mf, stress_period_data=wel_spd0) - flopy.modflow.ModflowRch(mf, rech=recharge) - flopy.modflow.ModflowPcg( - mf, mxiter=nouter, iter1=ninner, hclose=hclose, rclose=rclose - ) - oc = flopy.modflow.ModflowOc( - mf, stress_period_data={(0, 0): ["save head", "save budget"]} - ) - oc.reset_budgetunit() - return mf - return None - - -# Function to write MODFLOW 6 TWRI model files - - -def write_model(sim, mf, silent=True): - if config.writeModel: - sim.write_simulation(silent=silent) - mf.write_input() - - -# Function to run the TWRI model. -# True is returned if the model runs successfully + sim_ws = os.path.join(workspace, sim_name, "mf2005") + mf = flopy.modflow.Modflow( + modelname=sim_name, model_ws=sim_ws, exe_name="mf2005dbl" + ) + flopy.modflow.ModflowDis( + mf, + nlay=3, + nrow=nrow, + ncol=ncol, + delr=delr, + delc=delc, + laycbd=[1, 1, 0], + top=top, + botm=botm, + nper=1, + perlen=perlen, + nstp=nstp, + tsmult=tsmult, + ) + flopy.modflow.ModflowBas(mf, strt=strt) + flopy.modflow.ModflowLpf( + mf, + laytyp=[1, 0, 0], + hk=[k11[0], k11[2], k11[4]], + vka=[k11[0], k11[2], k11[4]], + vkcb=[k11[1], k11[3], 0], + ss=0, + sy=0.0, + ) + flopy.modflow.ModflowChd(mf, stress_period_data=chd_spd0) + flopy.modflow.ModflowDrn(mf, stress_period_data=drn_spd) + flopy.modflow.ModflowWel(mf, stress_period_data=wel_spd0) + flopy.modflow.ModflowRch(mf, rech=recharge) + flopy.modflow.ModflowPcg( + mf, mxiter=nouter, iter1=ninner, hclose=hclose, rclose=rclose + ) + oc = flopy.modflow.ModflowOc( + mf, stress_period_data={(0, 0): ["save head", "save budget"]} + ) + oc.reset_budgetunit() + return mf + + +def write_models(sim, mf, silent=True): + sim.write_simulation(silent=silent) + mf.write_input() + + +@timed +def run_models(sim, mf, silent=True): + success, buff = sim.run_simulation(silent=silent) + if not success: + print(buff) + else: + success, buff = mf.run_model(silent=silent) + assert success, buff + + +# - + +# ### Plotting results # +# Define functions to plot model results. - -@config.timeit -def run_model(sim, mf, silent=True): - success = True - if config.runModel: - success, buff = sim.run_simulation(silent=silent) - if not success: - print(buff) - else: - success, buff = mf.run_model(silent=silent) - if not success: - print(buff) - - return success - - -# Function to plot the TWRI model results. -# +# + +# Figure properties +figure_size = (6, 6) def plot_results(sim, mf, silent=True): - if config.plotModel: - fs = USGSFigure(figure_type="map", verbose=False) - sim_ws = os.path.join(ws, sim_name) + with styles.USGSMap(): + sim_ws = os.path.join(workspace, sim_name) gwf = sim.get_model(sim_name) # create MODFLOW 6 head object @@ -358,11 +325,11 @@ def plot_results(sim, mf, silent=True): # Create figure for simulation extents = (0, ncol * delc, 0, nrow * delr) fig, axes = plt.subplots( - 3, + 2, 3, figsize=figure_size, dpi=300, - constrained_layout=True, + # constrained_layout=True, sharey=True, ) for ax in axes.flatten(): @@ -372,9 +339,7 @@ def plot_results(sim, mf, silent=True): for idx, ax in enumerate(axes.flatten()[:3]): k = layers_mf6[idx] - fmp = flopy.plot.PlotMapView( - model=gwf, ax=ax, layer=k, extent=extents - ) + fmp = flopy.plot.PlotMapView(model=gwf, ax=ax, layer=k, extent=extents) ax.get_xaxis().set_ticks([]) fmp.plot_grid(lw=0.5) plot_obj = fmp.plot_array(head, vmin=vmin, vmax=vmax) @@ -390,12 +355,10 @@ def plot_results(sim, mf, silent=True): fmp.plot_vector(qx, qy, normalize=True, color="0.75") title = titles[idx] letter = chr(ord("@") + idx + 1) - fs.heading(letter=letter, heading=title, ax=ax) + styles.heading(letter=letter, heading=title, ax=ax) for idx, ax in enumerate(axes.flatten()[3:6]): - fmp = flopy.plot.PlotMapView( - model=mf, ax=ax, layer=idx, extent=extents - ) + fmp = flopy.plot.PlotMapView(model=mf, ax=ax, layer=idx, extent=extents) fmp.plot_grid(lw=0.5) plot_obj = fmp.plot_array(head0, vmin=vmin, vmax=vmax) fmp.plot_bc("DRN", color="green") @@ -413,19 +376,25 @@ def plot_results(sim, mf, silent=True): fmp.plot_vector(sqx, sqy, normalize=True, color="0.75") title = titles[idx] letter = chr(ord("@") + idx + 4) - fs.heading(letter=letter, heading=title, ax=ax) + styles.heading(letter=letter, heading=title, ax=ax) # create legend - ax = plt.subplot(313) + ax = fig.add_subplot() ax.set_xlim(extents[:2]) ax.set_ylim(extents[2:]) + ax.set_xticks([]) + ax.set_yticks([]) + ax.spines["top"].set_color("none") + ax.spines["bottom"].set_color("none") + ax.spines["left"].set_color("none") + ax.spines["right"].set_color("none") + ax.patch.set_alpha(0.0) # ax = axes.flatten()[-2] - ax.axis("off") + # ax.axis("off") ax.plot( -10000, -10000, - lw=0, marker="s", ms=10, mfc="green", @@ -435,7 +404,6 @@ def plot_results(sim, mf, silent=True): ax.plot( -10000, -10000, - lw=0, marker="s", ms=10, mfc="0.5", @@ -445,69 +413,54 @@ def plot_results(sim, mf, silent=True): ax.plot( -10000, -10000, - lw=0, marker="$\u2192$", ms=10, mfc="0.75", mec="0.75", label="Normalized specific discharge", ) - ax.plot( - -10000, -10000, lw=0.5, color="black", label=r"Head contour, $ft$" - ) - fs.graph_legend(ax, loc="upper center") + ax.plot(-10000, -10000, lw=0.5, color="black", label=r"Head contour, $ft$") + styles.graph_legend(ax, ncol=1, frameon=False, loc="center left") + + # plt.tight_layout(h_pad=-15) + plt.subplots_adjust(top=0.9, hspace=0.5) # plot colorbar - cax = plt.axes([0.325, 0.125, 0.35, 0.025]) - cbar = plt.colorbar( - plot_obj, shrink=0.8, orientation="horizontal", cax=cax - ) + cax = plt.axes([0.525, 0.55, 0.35, 0.025]) + cbar = plt.colorbar(plot_obj, shrink=0.8, orientation="horizontal", cax=cax) cbar.ax.tick_params(size=0) cbar.ax.set_xlabel(r"Head, $ft$", fontsize=9) - # save figure - if config.plotSave: - fpth = os.path.join( - "..", "figures", f"{sim_name}{config.figure_ext}" - ) + if plot_show: + plt.show() + if plot_save: + fpth = os.path.join("..", "figures", f"{sim_name}.png") fig.savefig(fpth) -# Function that wraps all of the steps for the TWRI model -# -# 1. build_model, -# 2. write_model, -# 3. run_model, and -# 4. plot_results. +# - + +# ### Running the example # +# Define and invoke a function to run the example scenario, then plot results. -def simulation(silent=True): - sim = build_model() +# + +def scenario(silent=True): + sim = build_models() mf = build_mf5model() - - write_model(sim, mf, silent=silent) - - success = run_model(sim, mf, silent=silent) - - if success: + if write: + write_models(sim, mf, silent=silent) + if run: + run_models(sim, mf, silent=silent) + if plot: plot_results(sim, mf, silent=silent) -# nosetest - exclude block from this nosetest to the next nosetest -def test_01(): - simulation(silent=False) - - -# nosetest end - -if __name__ == "__main__": - # ### TWRI Simulation - # - # Simulated heads in model the unconfined, middle, and lower aquifers (model layers - # 1, 3, and 5) are shown in the figure below. MODFLOW-2005 results for a quasi-3D - # model are also shown. The location of drain (green) and well (gray) boundary - # conditions, normalized specific discharge, and head contours (25 ft contour - # intervals) are also shown. - - simulation() +# Simulated heads in model the unconfined, middle, and lower aquifers (model layers +# 1, 3, and 5) are shown in the figure below. MODFLOW-2005 results for a quasi-3D +# model are also shown. The location of drain (green) and well (gray) boundary +# conditions, normalized specific discharge, and head contours (25 ft contour +# intervals) are also shown. +scenario() +# - diff --git a/scripts/ex-gwf-u1disv.py b/scripts/ex-gwf-u1disv.py index 51447d30c..57da44358 100644 --- a/scripts/ex-gwf-u1disv.py +++ b/scripts/ex-gwf-u1disv.py @@ -6,42 +6,42 @@ # with the XT3D option of the NPF Package to improve the solution. # -# ### USG1DISV Problem Setup +# ### Initial setup # -# Imports +# Import dependencies, define the example name and workspace, and read settings from environment variables. +# + import os -import sys +import pathlib as pl import flopy import flopy.utils.cvfdutil import matplotlib.pyplot as plt import numpy as np +from flopy.plot.styles import styles +from modflow_devtools.misc import get_env, timed -# Append to system path to include the common subdirectory +# Base workspace +workspace = pl.Path("../examples") -sys.path.append(os.path.join("..", "common")) +# Settings from environment variables +write = get_env("WRITE", True) +run = get_env("RUN", True) +plot = get_env("PLOT", True) +plot_show = get_env("PLOT_SHOW", True) +plot_save = get_env("PLOT_SAVE", True) +# - -# import common functionality - -import config -from figspecs import USGSFigure - -# Set default figure properties - -figure_size = (6, 6) - -# Base simulation and model name and workspace - -ws = config.base_ws +# ### Define parameters +# +# Define model units, parameters and other settings. +# + # Model units - length_units = "meters" time_units = "days" -# Scenario parameters - +# Scenario-specific parameters parameters = { "ex-gwf-u1disv": { "xt3d": False, @@ -51,8 +51,7 @@ }, } -# Table USG1DISV Model Parameters - +# Model parameters nper = 1 # Number of periods nlay = 1 # Number of layers top = 0.0 # Top of the model ($m$) @@ -65,14 +64,12 @@ # Simulation has 1 steady stress period (1 day) # and 3 transient stress periods (10 days each). # Each transient stress period has 120 2-hour time steps. - perlen = [1.0] nstp = [1] tsmult = [1.0, 1.0, 1.0] tdis_ds = list(zip(perlen, nstp, tsmult)) # create the disv grid - # outer grid nlay = 1 nrow = ncol = 7 @@ -107,261 +104,234 @@ gridprops = flopy.utils.cvfdutil.gridlist_to_disv_gridprops([sg1, sg2]) # Solver parameters - nouter = 50 ninner = 100 hclose = 1e-9 rclose = 1e-6 +# - -# ### Functions to build, write, run, and plot the MODFLOW 6 USG1DISV model +# ### Model setup # -# MODFLOW 6 flopy simulation object (sim) is returned if building the model - - -def build_model(sim_name, xt3d): - if config.buildModel: - sim_ws = os.path.join(ws, sim_name) - sim = flopy.mf6.MFSimulation( - sim_name=sim_name, sim_ws=sim_ws, exe_name="mf6" - ) - flopy.mf6.ModflowTdis( - sim, nper=nper, perioddata=tdis_ds, time_units=time_units - ) - flopy.mf6.ModflowIms( - sim, - linear_acceleration="bicgstab", - outer_maximum=nouter, - outer_dvclose=hclose, - inner_maximum=ninner, - inner_dvclose=hclose, - rcloserecord=f"{rclose} strict", - ) - gwf = flopy.mf6.ModflowGwf(sim, modelname=sim_name, save_flows=True) - flopy.mf6.ModflowGwfdisv( - gwf, - length_units=length_units, - nlay=nlay, - top=top, - botm=botm, - **gridprops, - ) - flopy.mf6.ModflowGwfnpf( - gwf, - icelltype=icelltype, - k=k11, - save_specific_discharge=True, - xt3doptions=xt3d, - ) - flopy.mf6.ModflowGwfic(gwf, strt=strt) +# Define functions to build models, write input files, and run the simulation. + + +# + +def build_models(sim_name, xt3d): + sim_ws = os.path.join(workspace, sim_name) + sim = flopy.mf6.MFSimulation(sim_name=sim_name, sim_ws=sim_ws, exe_name="mf6") + flopy.mf6.ModflowTdis(sim, nper=nper, perioddata=tdis_ds, time_units=time_units) + flopy.mf6.ModflowIms( + sim, + linear_acceleration="bicgstab", + outer_maximum=nouter, + outer_dvclose=hclose, + inner_maximum=ninner, + inner_dvclose=hclose, + rcloserecord=f"{rclose} strict", + ) + gwf = flopy.mf6.ModflowGwf(sim, modelname=sim_name, save_flows=True) + flopy.mf6.ModflowGwfdisv( + gwf, + length_units=length_units, + nlay=nlay, + top=top, + botm=botm, + **gridprops, + ) + flopy.mf6.ModflowGwfnpf( + gwf, + icelltype=icelltype, + k=k11, + save_specific_discharge=True, + xt3doptions=xt3d, + ) + flopy.mf6.ModflowGwfic(gwf, strt=strt) - chd_spd = [] - chd_spd += [[0, i, 1.0] for i in [0, 7, 14, 18, 22, 26, 33]] - chd_spd = {0: chd_spd} - flopy.mf6.ModflowGwfchd( - gwf, - stress_period_data=chd_spd, - pname="CHD-LEFT", - filename=f"{sim_name}.left.chd", - ) + chd_spd = [] + chd_spd += [[0, i, 1.0] for i in [0, 7, 14, 18, 22, 26, 33]] + chd_spd = {0: chd_spd} + flopy.mf6.ModflowGwfchd( + gwf, + stress_period_data=chd_spd, + pname="CHD-LEFT", + filename=f"{sim_name}.left.chd", + ) - chd_spd = [] - chd_spd += [[0, i, 0.0] for i in [6, 13, 17, 21, 25, 32, 39]] - chd_spd = {0: chd_spd} - flopy.mf6.ModflowGwfchd( - gwf, - stress_period_data=chd_spd, - pname="CHD-RIGHT", - filename=f"{sim_name}.right.chd", - ) + chd_spd = [] + chd_spd += [[0, i, 0.0] for i in [6, 13, 17, 21, 25, 32, 39]] + chd_spd = {0: chd_spd} + flopy.mf6.ModflowGwfchd( + gwf, + stress_period_data=chd_spd, + pname="CHD-RIGHT", + filename=f"{sim_name}.right.chd", + ) - head_filerecord = f"{sim_name}.hds" - budget_filerecord = f"{sim_name}.cbc" - flopy.mf6.ModflowGwfoc( - gwf, - head_filerecord=head_filerecord, - budget_filerecord=budget_filerecord, - saverecord=[("HEAD", "ALL"), ("BUDGET", "ALL")], - ) + head_filerecord = f"{sim_name}.hds" + budget_filerecord = f"{sim_name}.cbc" + flopy.mf6.ModflowGwfoc( + gwf, + head_filerecord=head_filerecord, + budget_filerecord=budget_filerecord, + saverecord=[("HEAD", "ALL"), ("BUDGET", "ALL")], + ) - return sim - return None + return sim -# Function to write MODFLOW 6 USG1DISV model files +def write_models(sim, silent=True): + sim.write_simulation(silent=silent) -def write_model(sim, silent=True): - if config.writeModel: - sim.write_simulation(silent=silent) +@timed +def run_models(sim, silent=False): + success, buff = sim.run_simulation(silent=silent, report=True) + assert success, buff -# Function to run the FHB model. -# True is returned if the model runs successfully +# ### Plotting results # +# Define functions to plot model results. - -@config.timeit -def run_model(sim, silent=False): - success = True - if config.runModel: - success, buff = sim.run_simulation(silent=silent, report=True) - if not success: - print(buff) - return success +# + +# Figure properties +figure_size = (6, 6) -# Function to plot the USG1DISV model results. -# def plot_grid(idx, sim): - fs = USGSFigure(figure_type="map", verbose=False) - sim_name = list(parameters.keys())[idx] - sim_ws = os.path.join(ws, sim_name) - gwf = sim.get_model(sim_name) - - fig = plt.figure(figsize=figure_size) - fig.tight_layout() - - ax = fig.add_subplot(1, 1, 1, aspect="equal") - pmv = flopy.plot.PlotMapView(model=gwf, ax=ax, layer=0) - pmv.plot_grid() - pmv.plot_bc(name="CHD-LEFT", alpha=0.75) - pmv.plot_bc(name="CHD-RIGHT", alpha=0.75) - ax.set_xlabel("x position (m)") - ax.set_ylabel("y position (m)") - for i, (x, y) in enumerate( - zip(gwf.modelgrid.xcellcenters, gwf.modelgrid.ycellcenters) - ): - ax.text( - x, - y, - f"{i + 1}", - fontsize=6, - horizontalalignment="center", - verticalalignment="center", - ) - v = gwf.disv.vertices.array - ax.plot(v["xv"], v["yv"], "yo") - for i in range(v.shape[0]): - x, y = v["xv"][i], v["yv"][i] - ax.text( - x, - y, - f"{i + 1}", - fontsize=5, - color="red", - horizontalalignment="center", - verticalalignment="center", - ) - - # save figure - if config.plotSave: - fpth = os.path.join( - "..", "figures", f"{sim_name}-grid{config.figure_ext}" - ) - fig.savefig(fpth) - return + with styles.USGSMap() as fs: + sim_name = list(parameters.keys())[idx] + sim_ws = os.path.join(workspace, sim_name) + gwf = sim.get_model(sim_name) + + fig = plt.figure(figsize=figure_size) + fig.tight_layout() + + ax = fig.add_subplot(1, 1, 1, aspect="equal") + pmv = flopy.plot.PlotMapView(model=gwf, ax=ax, layer=0) + pmv.plot_grid() + pmv.plot_bc(name="CHD-LEFT", alpha=0.75) + pmv.plot_bc(name="CHD-RIGHT", alpha=0.75) + ax.set_xlabel("x position (m)") + ax.set_ylabel("y position (m)") + for i, (x, y) in enumerate( + zip(gwf.modelgrid.xcellcenters, gwf.modelgrid.ycellcenters) + ): + ax.text( + x, + y, + f"{i + 1}", + fontsize=6, + horizontalalignment="center", + verticalalignment="center", + ) + v = gwf.disv.vertices.array + ax.plot(v["xv"], v["yv"], "yo") + for i in range(v.shape[0]): + x, y = v["xv"][i], v["yv"][i] + ax.text( + x, + y, + f"{i + 1}", + fontsize=5, + color="red", + horizontalalignment="center", + verticalalignment="center", + ) + + if plot_show: + plt.show() + if plot_save: + fpth = os.path.join("..", "figures", f"{sim_name}-grid.png") + fig.savefig(fpth) def plot_head(idx, sim): - fs = USGSFigure(figure_type="map", verbose=False) - sim_name = list(parameters.keys())[idx] - sim_ws = os.path.join(ws, sim_name) - gwf = sim.get_model(sim_name) + with styles.USGSMap() as fs: + sim_name = list(parameters.keys())[idx] + sim_ws = os.path.join(workspace, sim_name) + gwf = sim.get_model(sim_name) - fig = plt.figure(figsize=(7.5, 5)) - fig.tight_layout() + fig = plt.figure(figsize=(7.5, 5)) + fig.tight_layout() - head = gwf.output.head().get_data()[:, 0, :] + head = gwf.output.head().get_data()[:, 0, :] - # create MODFLOW 6 cell-by-cell budget object - qx, qy, qz = flopy.utils.postprocessing.get_specific_discharge( - gwf.output.budget().get_data(text="DATA-SPDIS", totim=1.0)[0], - gwf, - ) + # create MODFLOW 6 cell-by-cell budget object + qx, qy, qz = flopy.utils.postprocessing.get_specific_discharge( + gwf.output.budget().get_data(text="DATA-SPDIS", totim=1.0)[0], + gwf, + ) - ax = fig.add_subplot(1, 2, 1, aspect="equal") - pmv = flopy.plot.PlotMapView(model=gwf, ax=ax, layer=0) - pmv.plot_grid() - cb = pmv.plot_array(head, cmap="jet") - pmv.plot_vector( - qx, - qy, - normalize=False, - color="0.75", - ) - cbar = plt.colorbar(cb, shrink=0.25) - cbar.ax.set_xlabel(r"Head, ($m$)") - ax.set_xlabel("x position (m)") - ax.set_ylabel("y position (m)") - fs.heading(ax, letter="A", heading="Simulated Head") - - ax = fig.add_subplot(1, 2, 2, aspect="equal") - pmv = flopy.plot.PlotMapView(model=gwf, ax=ax, layer=0) - pmv.plot_grid() - x = np.array(gwf.modelgrid.xcellcenters) - 50.0 - slp = (1.0 - 0.0) / (50.0 - 650.0) - heada = slp * x + 1.0 - cb = pmv.plot_array(head - heada, cmap="jet") - cbar = plt.colorbar(cb, shrink=0.25) - cbar.ax.set_xlabel(r"Error, ($m$)") - ax.set_xlabel("x position (m)") - ax.set_ylabel("y position (m)") - fs.heading(ax, letter="B", heading="Error") - - # save figure - if config.plotSave: - fpth = os.path.join( - "..", "figures", f"{sim_name}-head{config.figure_ext}" + ax = fig.add_subplot(1, 2, 1, aspect="equal") + pmv = flopy.plot.PlotMapView(model=gwf, ax=ax, layer=0) + pmv.plot_grid() + cb = pmv.plot_array(head, cmap="jet") + pmv.plot_vector( + qx, + qy, + normalize=False, + color="0.75", ) - fig.savefig(fpth) - return + cbar = plt.colorbar(cb, shrink=0.25) + cbar.ax.set_xlabel(r"Head, ($m$)") + ax.set_xlabel("x position (m)") + ax.set_ylabel("y position (m)") + styles.heading(ax, letter="A", heading="Simulated Head") + + ax = fig.add_subplot(1, 2, 2, aspect="equal") + pmv = flopy.plot.PlotMapView(model=gwf, ax=ax, layer=0) + pmv.plot_grid() + x = np.array(gwf.modelgrid.xcellcenters) - 50.0 + slp = (1.0 - 0.0) / (50.0 - 650.0) + heada = slp * x + 1.0 + cb = pmv.plot_array(head - heada, cmap="jet") + cbar = plt.colorbar(cb, shrink=0.25) + cbar.ax.set_xlabel(r"Error, ($m$)") + ax.set_xlabel("x position (m)") + ax.set_ylabel("y position (m)") + styles.heading(ax, letter="B", heading="Error") + + if plot_show: + plt.show() + if plot_save: + fpth = os.path.join("..", "figures", f"{sim_name}-head.png") + fig.savefig(fpth) def plot_results(idx, sim, silent=True): - if config.plotModel: - if idx == 0: - plot_grid(idx, sim) - plot_head(idx, sim) - return + if idx == 0: + plot_grid(idx, sim) + plot_head(idx, sim) -# Function that wraps all of the steps for the FHB model -# -# 1. build_model, -# 2. write_model, -# 3. run_model, and -# 4. plot_results. +# - + +# ### Running the example # +# Define and invoke a function to run the example scenario, then plot results. +# + def simulation(idx, silent=True): key = list(parameters.keys())[idx] params = parameters[key].copy() - sim = build_model(key, **params) - write_model(sim, silent=silent) - success = run_model(sim, silent=silent) - if success: + sim = build_models(key, **params) + if write: + write_models(sim, silent=silent) + if run: + run_models(sim, silent=silent) + if plot: plot_results(idx, sim, silent=silent) -# nosetest - exclude block from this nosetest to the next nosetest -def test_01(): - simulation(0, silent=False) - - -def test_02(): - simulation(1, silent=False) - +# - -# nosetest end -if __name__ == "__main__": - # ### USG1DISV Simulation - # - # Simulated heads in the USG1DISV model without XT3D. +# Run the USG1DISV model without XT3D, then plot heads. - simulation(0) +simulation(0) - # Simulated heads in the USG1DISV model with XT3D. +# Run the USG1DISV model with XT3D, then plot heads. - simulation(1) +simulation(1) diff --git a/scripts/ex-gwf-u1gwfgwf.py b/scripts/ex-gwf-u1gwfgwf.py index 5cb04f9ad..793ca7acc 100644 --- a/scripts/ex-gwf-u1gwfgwf.py +++ b/scripts/ex-gwf-u1gwfgwf.py @@ -12,45 +12,44 @@ # 2. with XT3D enabled in both models # 3. with XT3D enabled in both models and at the interface # 4. with XT3D enabled _only_ at the interface between the models + +# ### Initial setup # -# ### Setup -# -# Imports +# Import dependencies, define the example name and workspace, and read settings from environment variables. +# + import os -import sys +import pathlib as pl import flopy import matplotlib.pyplot as plt import numpy as np +from flopy.plot.styles import styles from flopy.utils.lgrutil import Lgr from matplotlib.colors import ListedColormap +from modflow_devtools.misc import get_env, timed -# Append to system path to include the common subdirectory - -sys.path.append(os.path.join("..", "common")) - -# import common functionality +# Base workspace +workspace = pl.Path("../examples") -import config -from figspecs import USGSFigure +# Settings from environment variables +write = get_env("WRITE", True) +run = get_env("RUN", True) +plot = get_env("PLOT", True) +plot_show = get_env("PLOT_SHOW", True) +plot_save = get_env("PLOT_SAVE", True) +# - -# Set default figure properties - -figure_size = (5, 5) -figure_size_double = (7, 3) - -# Base simulation and model name and workspace - -ws = config.base_ws +# ### Define parameters +# +# Define model units, parameters and other settings. +# + # Model units - length_units = "meters" time_units = "days" -# Scenario parameters - +# Scenario-specific parameters parameters = { "ex-gwf-u1gwfgwf-s1": { "XT3D_in_models": False, @@ -70,8 +69,7 @@ }, } -# Table with Model Parameters - +# Model parameters nper = 1 # Number of periods nlay = 1 # Number of layers top = 0.0 # Top of the model ($m$) @@ -85,14 +83,12 @@ # Static temporal data used by TDIS file # Simulation has 1 steady stress period (1 day) # with 1 time step - perlen = [1.0] nstp = [1] tsmult = [1.0, 1.0, 1.0] tdis_ds = list(zip(perlen, nstp, tsmult)) # Coarse model grid - nlay = 1 nrow = ncol = 7 delr = 100.0 @@ -104,7 +100,6 @@ gwfname_outer = "outer" # Refined model grid - rfct = 3 nrow_inner = ncol_inner = 9 delr_inner = 100.0 / rfct @@ -115,522 +110,479 @@ gwfname_inner = "inner" # Solver parameters - nouter = 50 ninner = 100 hclose = 1e-9 rclose = 1e-6 +# - - -# ### Functions to build, write, run, and plot the model +# ### Model setup # -# MODFLOW 6 flopy simulation object (sim) is returned if building the model - +# Define functions to build models, write input files, and run the simulation. + + +# + +def build_models(sim_name, XT3D_in_models, XT3D_at_exchange): + sim_ws = os.path.join(workspace, sim_name) + sim = flopy.mf6.MFSimulation(sim_name=sim_name, sim_ws=sim_ws, exe_name="mf6") + flopy.mf6.ModflowTdis(sim, nper=nper, perioddata=tdis_ds, time_units=time_units) + flopy.mf6.ModflowIms( + sim, + linear_acceleration="bicgstab", + outer_maximum=nouter, + outer_dvclose=hclose, + inner_maximum=ninner, + inner_dvclose=hclose, + rcloserecord=f"{rclose} strict", + ) -def build_model(sim_name, XT3D_in_models, XT3D_at_exchange): - if config.buildModel: - sim_ws = os.path.join(ws, sim_name) - sim = flopy.mf6.MFSimulation( - sim_name=sim_name, sim_ws=sim_ws, exe_name="mf6" - ) - flopy.mf6.ModflowTdis( - sim, nper=nper, perioddata=tdis_ds, time_units=time_units - ) - flopy.mf6.ModflowIms( - sim, - linear_acceleration="bicgstab", - outer_maximum=nouter, - outer_dvclose=hclose, - inner_maximum=ninner, - inner_dvclose=hclose, - rcloserecord=f"{rclose} strict", - ) + # The coarse, outer model + gwf_outer = flopy.mf6.ModflowGwf(sim, modelname=gwfname_outer, save_flows=True) + flopy.mf6.ModflowGwfdis( + gwf_outer, + nlay=nlay, + nrow=nrow, + ncol=ncol, + delr=delr, + delc=delc, + idomain=idomain, + top=top, + botm=botm, + ) + flopy.mf6.ModflowGwfnpf( + gwf_outer, + icelltype=icelltype, + k=k11, + save_specific_discharge=True, + xt3doptions=XT3D_in_models, + ) + flopy.mf6.ModflowGwfic(gwf_outer, strt=strt) + + # constant head boundary LEFT + left_chd = [ + [(ilay, irow, 0), h_left] for ilay in range(nlay) for irow in range(nrow) + ] + chd_spd = {0: left_chd} + flopy.mf6.ModflowGwfchd( + gwf_outer, + stress_period_data=chd_spd, + pname="CHD-LEFT", + filename=f"{gwfname_outer}.left.chd", + ) - # The coarse, outer model - gwf_outer = flopy.mf6.ModflowGwf( - sim, modelname=gwfname_outer, save_flows=True - ) - flopy.mf6.ModflowGwfdis( - gwf_outer, - nlay=nlay, - nrow=nrow, - ncol=ncol, - delr=delr, - delc=delc, - idomain=idomain, - top=top, - botm=botm, - ) - flopy.mf6.ModflowGwfnpf( - gwf_outer, - icelltype=icelltype, - k=k11, - save_specific_discharge=True, - xt3doptions=XT3D_in_models, - ) - flopy.mf6.ModflowGwfic(gwf_outer, strt=strt) - - # constant head boundary LEFT - left_chd = [ - [(ilay, irow, 0), h_left] - for ilay in range(nlay) - for irow in range(nrow) - ] - chd_spd = {0: left_chd} - flopy.mf6.ModflowGwfchd( - gwf_outer, - stress_period_data=chd_spd, - pname="CHD-LEFT", - filename=f"{gwfname_outer}.left.chd", - ) + # constant head boundary RIGHT + right_chd = [ + [(ilay, irow, ncol - 1), h_right] + for ilay in range(nlay) + for irow in range(nrow) + ] + chd_spd = {0: right_chd} + flopy.mf6.ModflowGwfchd( + gwf_outer, + stress_period_data=chd_spd, + pname="CHD-RIGHT", + filename=f"{gwfname_outer}.right.chd", + ) - # constant head boundary RIGHT - right_chd = [ - [(ilay, irow, ncol - 1), h_right] - for ilay in range(nlay) - for irow in range(nrow) - ] - chd_spd = {0: right_chd} - flopy.mf6.ModflowGwfchd( - gwf_outer, - stress_period_data=chd_spd, - pname="CHD-RIGHT", - filename=f"{gwfname_outer}.right.chd", - ) + head_filerecord = f"{gwfname_outer}.hds" + budget_filerecord = f"{gwfname_outer}.cbc" + flopy.mf6.ModflowGwfoc( + gwf_outer, + head_filerecord=head_filerecord, + budget_filerecord=budget_filerecord, + saverecord=[("HEAD", "ALL"), ("BUDGET", "ALL")], + ) - head_filerecord = f"{gwfname_outer}.hds" - budget_filerecord = f"{gwfname_outer}.cbc" - flopy.mf6.ModflowGwfoc( - gwf_outer, - head_filerecord=head_filerecord, - budget_filerecord=budget_filerecord, - saverecord=[("HEAD", "ALL"), ("BUDGET", "ALL")], - ) + # the refined, inner model + gwf_inner = flopy.mf6.ModflowGwf(sim, modelname=gwfname_inner, save_flows=True) + flopy.mf6.ModflowGwfdis( + gwf_inner, + nlay=nlay, + nrow=nrow_inner, + ncol=ncol_inner, + delr=delr_inner, + delc=delc_inner, + top=top, + botm=botm, + xorigin=xorigin, + yorigin=yorigin, + length_units=length_units, + ) + flopy.mf6.ModflowGwfic(gwf_inner, strt=strt) + flopy.mf6.ModflowGwfnpf( + gwf_inner, + save_specific_discharge=True, + xt3doptions=XT3D_in_models, + save_flows=True, + icelltype=icelltype, + k=k11, + ) - # the refined, inner model - gwf_inner = flopy.mf6.ModflowGwf( - sim, modelname=gwfname_inner, save_flows=True - ) - flopy.mf6.ModflowGwfdis( - gwf_inner, - nlay=nlay, - nrow=nrow_inner, - ncol=ncol_inner, - delr=delr_inner, - delc=delc_inner, - top=top, - botm=botm, - xorigin=xorigin, - yorigin=yorigin, - length_units=length_units, - ) - flopy.mf6.ModflowGwfic(gwf_inner, strt=strt) - flopy.mf6.ModflowGwfnpf( - gwf_inner, - save_specific_discharge=True, - xt3doptions=XT3D_in_models, - save_flows=True, - icelltype=icelltype, - k=k11, - ) + head_filerecord = f"{gwfname_inner}.hds" + budget_filerecord = f"{gwfname_inner}.cbc" + flopy.mf6.ModflowGwfoc( + gwf_inner, + head_filerecord=head_filerecord, + budget_filerecord=budget_filerecord, + saverecord=[("HEAD", "ALL"), ("BUDGET", "ALL")], + ) - head_filerecord = f"{gwfname_inner}.hds" - budget_filerecord = f"{gwfname_inner}.cbc" - flopy.mf6.ModflowGwfoc( - gwf_inner, - head_filerecord=head_filerecord, - budget_filerecord=budget_filerecord, - saverecord=[("HEAD", "ALL"), ("BUDGET", "ALL")], - ) + # Use Lgr to get the exchange data + nrowp = gwf_outer.dis.nrow.get_data() + ncolp = gwf_outer.dis.ncol.get_data() + delrp = gwf_outer.dis.delr.array + delcp = gwf_outer.dis.delc.array + topp = gwf_outer.dis.top.array + botmp = gwf_outer.dis.botm.array + idomainp = gwf_outer.dis.idomain.array + + lgr = Lgr( + nlay, + nrowp, + ncolp, + delrp, + delcp, + topp, + botmp, + idomainp, + ncpp=rfct, + ncppl=1, + ) - # Use Lgr to get the exchange data - nrowp = gwf_outer.dis.nrow.get_data() - ncolp = gwf_outer.dis.ncol.get_data() - delrp = gwf_outer.dis.delr.array - delcp = gwf_outer.dis.delc.array - topp = gwf_outer.dis.top.array - botmp = gwf_outer.dis.botm.array - idomainp = gwf_outer.dis.idomain.array - - lgr = Lgr( - nlay, - nrowp, - ncolp, - delrp, - delcp, - topp, - botmp, - idomainp, - ncpp=rfct, - ncppl=1, - ) + exgdata = lgr.get_exchange_data(angldegx=True, cdist=True) + for exg in exgdata: + l = exg + angle = l[-2] + if angle == 0: + bname = "left" + elif angle == 90.0: + bname = "bottom" + elif angle == 180.0: + bname = "right" + elif angle == 270.0: + bname = "top" + l.append(bname) + + # group exchanges based on boundname + exgdata.sort(key=lambda x: x[-3]) + + flopy.mf6.ModflowGwfgwf( + sim, + exgtype="GWF6-GWF6", + nexg=len(exgdata), + exgmnamea=gwfname_outer, + exgmnameb=gwfname_inner, + exchangedata=exgdata, + xt3d=XT3D_at_exchange, + print_input=True, + print_flows=True, + save_flows=True, + boundnames=True, + auxiliary=["ANGLDEGX", "CDIST"], + ) - exgdata = lgr.get_exchange_data(angldegx=True, cdist=True) - for exg in exgdata: - l = exg - angle = l[-2] - if angle == 0: - bname = "left" - elif angle == 90.0: - bname = "bottom" - elif angle == 180.0: - bname = "right" - elif angle == 270.0: - bname = "top" - l.append(bname) - - # group exchanges based on boundname - exgdata.sort(key=lambda x: x[-3]) - - flopy.mf6.ModflowGwfgwf( - sim, - exgtype="GWF6-GWF6", - nexg=len(exgdata), - exgmnamea=gwfname_outer, - exgmnameb=gwfname_inner, - exchangedata=exgdata, - xt3d=XT3D_at_exchange, - print_input=True, - print_flows=True, - save_flows=True, - boundnames=True, - auxiliary=["ANGLDEGX", "CDIST"], - ) + return sim - return sim - return None +def write_models(sim, silent=True): + sim.write_simulation(silent=silent) -# Function to write model files +@timed +def run_models(sim, silent=False): + success, buff = sim.run_simulation(silent=silent, report=True) + assert success, buff -def write_model(sim, silent=True): - if config.writeModel: - sim.write_simulation(silent=silent) +# - -# Function to run the model. -# True is returned if the model runs successfully +# ### Plotting results # +# Define functions to plot model results. - -@config.timeit -def run_model(sim, silent=False): - success = True - if config.runModel: - success, buff = sim.run_simulation(silent=silent, report=True) - if not success: - print(buff) - return success - - -# Functions to plot model results. -# +# + +# Figure properties +figure_size = (5, 5) +figure_size_double = (7, 3) def plot_grid(idx, sim): - fs = USGSFigure(figure_type="map", verbose=False) - sim_name = list(parameters.keys())[idx] - gwf_outer = sim.get_model(gwfname_outer) - gwf_inner = sim.get_model(gwfname_inner) + with styles.USGSMap() as fs: + sim_name = list(parameters.keys())[idx] + gwf_outer = sim.get_model(gwfname_outer) + gwf_inner = sim.get_model(gwfname_inner) - fig = plt.figure(figsize=figure_size) - fig.tight_layout() + fig = plt.figure(figsize=figure_size) + fig.tight_layout() - ax = fig.add_subplot(1, 1, 1, aspect="equal") - pmv = flopy.plot.PlotMapView(model=gwf_outer, ax=ax, layer=0) - pmv_inner = flopy.plot.PlotMapView(model=gwf_inner, ax=ax, layer=0) + ax = fig.add_subplot(1, 1, 1, aspect="equal") + pmv = flopy.plot.PlotMapView(model=gwf_outer, ax=ax, layer=0) + pmv_inner = flopy.plot.PlotMapView(model=gwf_inner, ax=ax, layer=0) - pmv.plot_grid() - pmv_inner.plot_grid() + pmv.plot_grid() + pmv_inner.plot_grid() - pmv.plot_bc(name="CHD-LEFT", alpha=0.75) - pmv.plot_bc(name="CHD-RIGHT", alpha=0.75) + pmv.plot_bc(name="CHD-LEFT", alpha=0.75) + pmv.plot_bc(name="CHD-RIGHT", alpha=0.75) - ax.plot( - [200, 500, 500, 200, 200], - [200, 200, 500, 500, 200], - "r--", - linewidth=2.0, - ) + ax.plot( + [200, 500, 500, 200, 200], + [200, 200, 500, 500, 200], + "r--", + linewidth=2.0, + ) - ax.set_xlabel("x position (m)") - ax.set_ylabel("y position (m)") + ax.set_xlabel("x position (m)") + ax.set_ylabel("y position (m)") - # save figure - if config.plotSave: - fpth = os.path.join( - "..", "figures", f"{sim_name}-grid{config.figure_ext}" - ) - fig.savefig(fpth) - return + if plot_show: + plt.show() + if plot_save: + fpth = os.path.join("..", "figures", f"{sim_name}-grid.png") + fig.savefig(fpth) def plot_stencils(idx, sim): - fs = USGSFigure(figure_type="map", verbose=False) - sim_name = list(parameters.keys())[idx] - gwf_outer = sim.get_model(gwfname_outer) - gwf_inner = sim.get_model(gwfname_inner) - - fig = plt.figure(figsize=figure_size_double) - fig.tight_layout() - - # left plot, with stencils at the interface - ax = fig.add_subplot(1, 2, 1, aspect="equal") - pmv = flopy.plot.PlotMapView(model=gwf_outer, ax=ax, layer=0) - pmv_inner = flopy.plot.PlotMapView( - model=gwf_inner, ax=ax, layer=0, extent=pmv.extent - ) - pmv.plot_grid() - pmv_inner.plot_grid() - - stencil = np.zeros(pmv.mg.shape, dtype=int) - stencil_inner = np.zeros(pmv_inner.mg.shape, dtype=int) - - # stencil 1 - stencil[0, 0, 3] = 1 - stencil[0, 1, 2] = 1 - stencil[0, 1, 3] = 1 - stencil[0, 1, 4] = 1 - stencil_inner[0, 0, 3] = 1 - stencil_inner[0, 0, 4] = 1 - stencil_inner[0, 0, 5] = 1 - stencil_inner[0, 1, 4] = 1 - - # stencil 2 - stencil[0, 4, 1] = 1 - stencil[0, 5, 1] = 1 - stencil[0, 5, 2] = 1 - stencil[0, 5, 3] = 1 - stencil[0, 6, 2] = 1 - stencil_inner[0, 7, 0] = 1 - stencil_inner[0, 8, 0] = 1 - stencil_inner[0, 8, 1] = 1 - - # markers - x = [350.0, 216.666] - y = [500.0, 200.0] - - stencil = np.ma.masked_equal(stencil, 0) - stencil_inner = np.ma.masked_equal(stencil_inner, 0) - cmap = ListedColormap(["dodgerblue"]) - pmv.plot_array(stencil, cmap=cmap) - pmv_inner.plot_array(stencil_inner, cmap=cmap) - plt.scatter(x, y, facecolors="r") - - ax.set_xlabel("x position (m)") - ax.set_ylabel("y position (m)") - - # right plot, with stencils '1 connection away from the interface' - ax = fig.add_subplot(1, 2, 2, aspect="equal") - pmv = flopy.plot.PlotMapView(model=gwf_outer, ax=ax, layer=0) - pmv_inner = flopy.plot.PlotMapView( - model=gwf_inner, ax=ax, layer=0, extent=pmv.extent - ) - pmv.plot_grid() - pmv_inner.plot_grid() - - stencil = np.zeros(pmv.mg.shape, dtype=int) - stencil_inner = np.zeros(pmv_inner.mg.shape, dtype=int) - - # stencil 1 - stencil[0, 0, 1] = 1 - stencil[0, 1, 1] = 1 - stencil[0, 1, 2] = 1 - stencil[0, 1, 0] = 1 - stencil[0, 2, 1] = 1 - stencil[0, 2, 0] = 1 - stencil[0, 3, 1] = 1 - stencil_inner[0, 0, 0] = 1 - stencil_inner[0, 1, 0] = 1 - stencil_inner[0, 2, 0] = 1 - - # stencil 2 - stencil_inner[0, 6, 7] = 1 - stencil_inner[0, 7, 6] = 1 - stencil_inner[0, 7, 7] = 1 - stencil_inner[0, 7, 8] = 1 - stencil_inner[0, 8, 6] = 1 - stencil_inner[0, 8, 7] = 1 - stencil_inner[0, 8, 8] = 1 - stencil[0, 5, 4] = 1 - - # markers - x = [150.0, 450.0] - y = [500.0, 233.333] - - stencil = np.ma.masked_equal(stencil, 0) - stencil_inner = np.ma.masked_equal(stencil_inner, 0) - cmap = ListedColormap(["dodgerblue"]) - pmv.plot_array(stencil, cmap=cmap) - pmv_inner.plot_array(stencil_inner, cmap=cmap) - plt.scatter(x, y, facecolors="r") - - ax.set_xlabel("x position (m)") - ax.set_ylabel("y position (m)") - - # save figure - if config.plotSave: - fpth = os.path.join( - "..", "figures", f"{sim_name}-stencils{config.figure_ext}" + with styles.USGSMap() as fs: + sim_name = list(parameters.keys())[idx] + gwf_outer = sim.get_model(gwfname_outer) + gwf_inner = sim.get_model(gwfname_inner) + + fig = plt.figure(figsize=figure_size_double) + fig.tight_layout() + + # left plot, with stencils at the interface + ax = fig.add_subplot(1, 2, 1, aspect="equal") + pmv = flopy.plot.PlotMapView(model=gwf_outer, ax=ax, layer=0) + pmv_inner = flopy.plot.PlotMapView( + model=gwf_inner, ax=ax, layer=0, extent=pmv.extent + ) + pmv.plot_grid() + pmv_inner.plot_grid() + + stencil = np.zeros(pmv.mg.shape, dtype=int) + stencil_inner = np.zeros(pmv_inner.mg.shape, dtype=int) + + # stencil 1 + stencil[0, 0, 3] = 1 + stencil[0, 1, 2] = 1 + stencil[0, 1, 3] = 1 + stencil[0, 1, 4] = 1 + stencil_inner[0, 0, 3] = 1 + stencil_inner[0, 0, 4] = 1 + stencil_inner[0, 0, 5] = 1 + stencil_inner[0, 1, 4] = 1 + + # stencil 2 + stencil[0, 4, 1] = 1 + stencil[0, 5, 1] = 1 + stencil[0, 5, 2] = 1 + stencil[0, 5, 3] = 1 + stencil[0, 6, 2] = 1 + stencil_inner[0, 7, 0] = 1 + stencil_inner[0, 8, 0] = 1 + stencil_inner[0, 8, 1] = 1 + + # markers + x = [350.0, 216.666] + y = [500.0, 200.0] + + stencil = np.ma.masked_equal(stencil, 0) + stencil_inner = np.ma.masked_equal(stencil_inner, 0) + cmap = ListedColormap(["dodgerblue"]) + pmv.plot_array(stencil, cmap=cmap) + pmv_inner.plot_array(stencil_inner, cmap=cmap) + plt.scatter(x, y, facecolors="r") + + ax.set_xlabel("x position (m)") + ax.set_ylabel("y position (m)") + + # right plot, with stencils '1 connection away from the interface' + ax = fig.add_subplot(1, 2, 2, aspect="equal") + pmv = flopy.plot.PlotMapView(model=gwf_outer, ax=ax, layer=0) + pmv_inner = flopy.plot.PlotMapView( + model=gwf_inner, ax=ax, layer=0, extent=pmv.extent ) - fig.savefig(fpth) - return + pmv.plot_grid() + pmv_inner.plot_grid() + + stencil = np.zeros(pmv.mg.shape, dtype=int) + stencil_inner = np.zeros(pmv_inner.mg.shape, dtype=int) + + # stencil 1 + stencil[0, 0, 1] = 1 + stencil[0, 1, 1] = 1 + stencil[0, 1, 2] = 1 + stencil[0, 1, 0] = 1 + stencil[0, 2, 1] = 1 + stencil[0, 2, 0] = 1 + stencil[0, 3, 1] = 1 + stencil_inner[0, 0, 0] = 1 + stencil_inner[0, 1, 0] = 1 + stencil_inner[0, 2, 0] = 1 + + # stencil 2 + stencil_inner[0, 6, 7] = 1 + stencil_inner[0, 7, 6] = 1 + stencil_inner[0, 7, 7] = 1 + stencil_inner[0, 7, 8] = 1 + stencil_inner[0, 8, 6] = 1 + stencil_inner[0, 8, 7] = 1 + stencil_inner[0, 8, 8] = 1 + stencil[0, 5, 4] = 1 + + # markers + x = [150.0, 450.0] + y = [500.0, 233.333] + + stencil = np.ma.masked_equal(stencil, 0) + stencil_inner = np.ma.masked_equal(stencil_inner, 0) + cmap = ListedColormap(["dodgerblue"]) + pmv.plot_array(stencil, cmap=cmap) + pmv_inner.plot_array(stencil_inner, cmap=cmap) + plt.scatter(x, y, facecolors="r") + + ax.set_xlabel("x position (m)") + ax.set_ylabel("y position (m)") + + if plot_show: + plt.show() + if plot_save: + fpth = os.path.join("..", "figures", f"{sim_name}-stencils.png") + fig.savefig(fpth) def plot_head(idx, sim): - fs = USGSFigure(figure_type="map", verbose=False) - sim_name = list(parameters.keys())[idx] - gwf_outer = sim.get_model(gwfname_outer) - gwf_inner = sim.get_model(gwfname_inner) - - fig = plt.figure(figsize=figure_size_double) - fig.tight_layout() - - head = gwf_outer.output.head().get_data()[0] - head_inner = gwf_inner.output.head().get_data()[0] - head[head == 1e30] = np.nan - head_inner[head_inner == 1e30] = np.nan - - # create MODFLOW 6 cell-by-cell budget objects - qx, qy, qz = flopy.utils.postprocessing.get_specific_discharge( - gwf_outer.output.budget().get_data(text="DATA-SPDIS", totim=1.0)[0], - gwf_outer, - ) - ( - qx_inner, - qy_inner, - qz_inner, - ) = flopy.utils.postprocessing.get_specific_discharge( - gwf_inner.output.budget().get_data(text="DATA-SPDIS", totim=1.0)[0], - gwf_inner, - ) + with styles.USGSMap() as fs: + sim_name = list(parameters.keys())[idx] + gwf_outer = sim.get_model(gwfname_outer) + gwf_inner = sim.get_model(gwfname_inner) + + fig = plt.figure(figsize=figure_size_double) + fig.tight_layout() + + head = gwf_outer.output.head().get_data()[0] + head_inner = gwf_inner.output.head().get_data()[0] + head[head == 1e30] = np.nan + head_inner[head_inner == 1e30] = np.nan + + # create MODFLOW 6 cell-by-cell budget objects + qx, qy, qz = flopy.utils.postprocessing.get_specific_discharge( + gwf_outer.output.budget().get_data(text="DATA-SPDIS", totim=1.0)[0], + gwf_outer, + ) + ( + qx_inner, + qy_inner, + qz_inner, + ) = flopy.utils.postprocessing.get_specific_discharge( + gwf_inner.output.budget().get_data(text="DATA-SPDIS", totim=1.0)[0], + gwf_inner, + ) - # create plot with head values and spdis - ax = fig.add_subplot(1, 2, 1, aspect="equal") - pmv = flopy.plot.PlotMapView(model=gwf_outer, ax=ax, layer=0) - pmv_inner = flopy.plot.PlotMapView( - model=gwf_inner, ax=ax, layer=0, extent=pmv.extent - ) - cb = pmv.plot_array(head, cmap="jet", vmin=0.0, vmax=1.0) - cb = pmv_inner.plot_array(head_inner, cmap="jet", vmin=0.0, vmax=1.0) - pmv.plot_grid() - pmv_inner.plot_grid() - pmv.plot_vector( - qx, - qy, - normalize=False, - color="0.75", - ) - pmv_inner.plot_vector( - qx_inner, - qy_inner, - normalize=False, - color="0.75", - ) - cbar = plt.colorbar(cb, shrink=0.25) - cbar.ax.set_xlabel(r"Head, ($m$)") - ax.set_xlabel("x position (m)") - ax.set_ylabel("y position (m)") - fs.heading(ax, letter="A", heading="Simulated Head") - - # create plot with error in head - ax = fig.add_subplot(1, 2, 2, aspect="equal") - pmv = flopy.plot.PlotMapView(model=gwf_outer, ax=ax, layer=0) - pmv_inner = flopy.plot.PlotMapView( - model=gwf_inner, ax=ax, layer=0, extent=pmv.extent - ) - pmv.plot_grid() - pmv_inner.plot_grid() - x = np.array(gwf_outer.modelgrid.xcellcenters) - 50.0 - x_inner = np.array(gwf_inner.modelgrid.xcellcenters) - 50.0 - slp = (h_left - h_right) / (50.0 - 650.0) - head_exact = slp * x + h_left - head_exact_inner = slp * x_inner + h_left - err = head - head_exact - err_inner = head_inner - head_exact_inner - vmin = min(np.nanmin(err), np.nanmin(err_inner)) - vmax = min(np.nanmax(err), np.nanmax(err_inner)) - cb = pmv.plot_array(err, cmap="jet", vmin=vmin, vmax=vmax) - cb = pmv_inner.plot_array(err_inner, cmap="jet", vmin=vmin, vmax=vmax) - - cbar = plt.colorbar(cb, shrink=0.25) - cbar.ax.set_xlabel(r"Error, ($m$)") - ax.set_xlabel("x position (m)") - ax.set_ylabel("y position (m)") - fs.heading(ax, letter="B", heading="Error") - - # save figure - if config.plotSave: - fpth = os.path.join( - "..", "figures", f"{sim_name}-head{config.figure_ext}" + # create plot with head values and spdis + ax = fig.add_subplot(1, 2, 1, aspect="equal") + pmv = flopy.plot.PlotMapView(model=gwf_outer, ax=ax, layer=0) + pmv_inner = flopy.plot.PlotMapView( + model=gwf_inner, ax=ax, layer=0, extent=pmv.extent ) - fig.savefig(fpth) - return + cb = pmv.plot_array(head, cmap="jet", vmin=0.0, vmax=1.0) + cb = pmv_inner.plot_array(head_inner, cmap="jet", vmin=0.0, vmax=1.0) + pmv.plot_grid() + pmv_inner.plot_grid() + pmv.plot_vector( + qx, + qy, + normalize=False, + color="0.75", + ) + pmv_inner.plot_vector( + qx_inner, + qy_inner, + normalize=False, + color="0.75", + ) + cbar = plt.colorbar(cb, shrink=0.25) + cbar.ax.set_xlabel(r"Head, ($m$)") + ax.set_xlabel("x position (m)") + ax.set_ylabel("y position (m)") + styles.heading(ax, letter="A", heading="Simulated Head") + + # create plot with error in head + ax = fig.add_subplot(1, 2, 2, aspect="equal") + pmv = flopy.plot.PlotMapView(model=gwf_outer, ax=ax, layer=0) + pmv_inner = flopy.plot.PlotMapView( + model=gwf_inner, ax=ax, layer=0, extent=pmv.extent + ) + pmv.plot_grid() + pmv_inner.plot_grid() + x = np.array(gwf_outer.modelgrid.xcellcenters) - 50.0 + x_inner = np.array(gwf_inner.modelgrid.xcellcenters) - 50.0 + slp = (h_left - h_right) / (50.0 - 650.0) + head_exact = slp * x + h_left + head_exact_inner = slp * x_inner + h_left + err = head - head_exact + err_inner = head_inner - head_exact_inner + vmin = min(np.nanmin(err), np.nanmin(err_inner)) + vmax = min(np.nanmax(err), np.nanmax(err_inner)) + cb = pmv.plot_array(err, cmap="jet", vmin=vmin, vmax=vmax) + cb = pmv_inner.plot_array(err_inner, cmap="jet", vmin=vmin, vmax=vmax) + + cbar = plt.colorbar(cb, shrink=0.25) + cbar.ax.set_xlabel(r"Error, ($m$)") + ax.set_xlabel("x position (m)") + ax.set_ylabel("y position (m)") + styles.heading(ax, letter="B", heading="Error") + + if plot_show: + plt.show() + if plot_save: + fpth = os.path.join("..", "figures", f"{sim_name}-head.png") + fig.savefig(fpth) def plot_results(idx, sim, silent=True): - if config.plotModel: - if idx == 0: - plot_grid(idx, sim) - plot_stencils(idx, sim) - plot_head(idx, sim) - return + if idx == 0: + plot_grid(idx, sim) + plot_stencils(idx, sim) + plot_head(idx, sim) -# Function that wraps all of the steps for the FHB model -# -# 1. build_model, -# 2. write_model, -# 3. run_model, and -# 4. plot_results. +# - + +# ### Running the example # +# Define and invoke a function to run the example scenario, then plot results. +# + def simulation(idx, silent=True): key = list(parameters.keys())[idx] params = parameters[key].copy() - sim = build_model(key, **params) - write_model(sim, silent=silent) - success = run_model(sim, silent=silent) - if success: + sim = build_models(key, **params) + if write: + write_models(sim, silent=silent) + if run: + run_models(sim, silent=silent) + if plot: plot_results(idx, sim, silent=silent) -# nosetest - exclude block from this nosetest to the next nosetest -def test_01(): - simulation(0, silent=False) - - -def test_02(): - simulation(1, silent=False) - - -def test_03(): - simulation(2, silent=False) - - -def test_04(): - simulation(3, silent=False) - +# - -# nosetest end -if __name__ == "__main__": - # ### USG-ex1 GWF-GWF Exchange Simulation - # - # Simulated heads without XT3D. +# Run without XT3D, then plot simulated heads. - simulation(0) +simulation(0) - # Simulated heads with XT3D enabled globally, but not at the exchange +# Run with XT3D enabled globally, but not at the exchange, then plot simulated heads. - simulation(1) +simulation(1) - # Simulated heads with XT3D enabled globally +# Run with XT3D enabled globally, then plot simulated heads. - simulation(2) +simulation(2) - # Simulated heads with XT3D enabled _only_ at the model interface. +# Run with XT3D enabled _only_ at the model interface, then plot simulated heads. - simulation(3) +simulation(3) diff --git a/scripts/ex-gwf-whirl.py b/scripts/ex-gwf-whirl.py index c71524420..e4a9c4f07 100644 --- a/scripts/ex-gwf-whirl.py +++ b/scripts/ex-gwf-whirl.py @@ -4,46 +4,43 @@ # flow. The XT3D formulation is used to represent variable hydraulic # conductivitity ellipsoid orientations. The resulting flow pattern consists # of groundwater whirls, as described in the XT3D documentation report. -# -# ### Whirl Problem Setup +# ### Initial setup # -# Imports +# Import dependencies, define the example name and workspace, and read settings from environment variables. +# + import os -import sys +import pathlib as pl import flopy import matplotlib.pyplot as plt import numpy as np +from flopy.plot.styles import styles +from modflow_devtools.misc import get_env, timed -# Append to system path to include the common subdirectory - -sys.path.append(os.path.join("..", "common")) - -# import common functionality - -import config -from figspecs import USGSFigure - -# Set default figure properties - -figure_size = (3.5, 3.5) +# Example name and base workspace +sim_name = "ex-gwf-whirl" +workspace = pl.Path("../examples") -# Base simulation and model name and workspace +# Settings from environment variables +write = get_env("WRITE", True) +run = get_env("RUN", True) +plot = get_env("PLOT", True) +plot_show = get_env("PLOT_SHOW", True) +plot_save = get_env("PLOT_SAVE", True) +# - -sim_name = "ex-gwf-whirl" -ws = config.base_ws +# ### Define parameters +# +# Define model units, parameters and other settings. +# + # Model units - length_units = "meters" time_units = "days" -# Scenario parameters - -# Table Whirl Model Parameters - +# Model parameters nper = 1 # Number of periods nlay = 10 # Number of layers nrow = 10 # Number of rows @@ -62,184 +59,156 @@ # Static temporal data used by TDIS file # Simulation has 1 steady stress period (1 day) - perlen = [1.0] nstp = [1] tsmult = [1.0] tdis_ds = list(zip(perlen, nstp, tsmult)) # Parse strings into lists - botm = [float(value) for value in botm_str.split(",")] angle1 = [float(value) for value in angle1_str.split(",")] +# Solver settings nouter = 50 ninner = 100 hclose = 1e-9 rclose = 1e-6 +# - -# ### Functions to build, write, run, and plot the MODFLOW 6 Whirl model +# ### Model setup # -# MODFLOW 6 flopy simulation object (sim) is returned if building the model +# Define functions to build models, write input files, and run the simulation. + + +# + +def build_models(): + sim_ws = os.path.join(workspace, sim_name) + sim = flopy.mf6.MFSimulation(sim_name=sim_name, sim_ws=sim_ws, exe_name="mf6") + flopy.mf6.ModflowTdis(sim, nper=nper, perioddata=tdis_ds, time_units=time_units) + flopy.mf6.ModflowIms( + sim, + linear_acceleration="bicgstab", + outer_maximum=nouter, + outer_dvclose=hclose, + inner_maximum=ninner, + inner_dvclose=hclose, + rcloserecord=f"{rclose} strict", + ) + gwf = flopy.mf6.ModflowGwf(sim, modelname=sim_name, save_flows=True) + flopy.mf6.ModflowGwfdis( + gwf, + length_units=length_units, + nlay=nlay, + nrow=nrow, + ncol=ncol, + delr=delr, + delc=delc, + top=top, + botm=botm, + ) + flopy.mf6.ModflowGwfnpf( + gwf, + icelltype=icelltype, + k=k11, + k22=k22, + k33=k33, + angle1=angle1, + save_specific_discharge=True, + xt3doptions=True, + ) + flopy.mf6.ModflowGwfic(gwf, strt=strt) + rate = np.zeros((nlay, nrow, ncol), dtype=float) + rate[:, :, 0] = inflow_rate + rate[:, :, -1] = -inflow_rate + wellay, welrow, welcol = np.where(rate != 0.0) + wel_spd = [((k, i, j), rate[k, i, j]) for k, i, j in zip(wellay, welrow, welcol)] + wel_spd = {0: wel_spd} + flopy.mf6.ModflowGwfwel( + gwf, + stress_period_data=wel_spd, + pname="WEL", + ) + head_filerecord = f"{sim_name}.hds" + budget_filerecord = f"{sim_name}.cbc" + flopy.mf6.ModflowGwfoc( + gwf, + head_filerecord=head_filerecord, + budget_filerecord=budget_filerecord, + saverecord=[("HEAD", "ALL"), ("BUDGET", "ALL")], + ) + return sim -def build_model(): - if config.buildModel: - sim_ws = os.path.join(ws, sim_name) - sim = flopy.mf6.MFSimulation( - sim_name=sim_name, sim_ws=sim_ws, exe_name="mf6" - ) - flopy.mf6.ModflowTdis( - sim, nper=nper, perioddata=tdis_ds, time_units=time_units - ) - flopy.mf6.ModflowIms( - sim, - linear_acceleration="bicgstab", - outer_maximum=nouter, - outer_dvclose=hclose, - inner_maximum=ninner, - inner_dvclose=hclose, - rcloserecord=f"{rclose} strict", - ) - gwf = flopy.mf6.ModflowGwf(sim, modelname=sim_name, save_flows=True) - flopy.mf6.ModflowGwfdis( - gwf, - length_units=length_units, - nlay=nlay, - nrow=nrow, - ncol=ncol, - delr=delr, - delc=delc, - top=top, - botm=botm, - ) - flopy.mf6.ModflowGwfnpf( - gwf, - icelltype=icelltype, - k=k11, - k22=k22, - k33=k33, - angle1=angle1, - save_specific_discharge=True, - xt3doptions=True, - ) - flopy.mf6.ModflowGwfic(gwf, strt=strt) - rate = np.zeros((nlay, nrow, ncol), dtype=float) - rate[:, :, 0] = inflow_rate - rate[:, :, -1] = -inflow_rate - wellay, welrow, welcol = np.where(rate != 0.0) - wel_spd = [ - ((k, i, j), rate[k, i, j]) - for k, i, j in zip(wellay, welrow, welcol) - ] - wel_spd = {0: wel_spd} - flopy.mf6.ModflowGwfwel( - gwf, - stress_period_data=wel_spd, - pname="WEL", - ) - head_filerecord = f"{sim_name}.hds" - budget_filerecord = f"{sim_name}.cbc" - flopy.mf6.ModflowGwfoc( - gwf, - head_filerecord=head_filerecord, - budget_filerecord=budget_filerecord, - saverecord=[("HEAD", "ALL"), ("BUDGET", "ALL")], - ) - return sim - return None - +def write_models(sim, silent=True): + sim.write_simulation(silent=silent) -# Function to write MODFLOW 6 Whirl model files +@timed +def run_models(sim, silent=False): + success, buff = sim.run_simulation(silent=silent, report=True) + assert success, buff -def write_model(sim, silent=True): - if config.writeModel: - sim.write_simulation(silent=silent) +# - -# Function to run the FHB model. -# True is returned if the model runs successfully +# ### Plotting results # +# Define functions to plot model results. - -@config.timeit -def run_model(sim, silent=False): - success = True - if config.runModel: - success, buff = sim.run_simulation(silent=silent, report=True) - if not success: - print(buff) - return success - - -# Function to plot the Whirl model results. -# +# + +# Figure properties +figure_size = (3.5, 3.5) def plot_spdis(sim): - fs = USGSFigure(figure_type="map", verbose=False) - sim_ws = os.path.join(ws, sim_name) - gwf = sim.get_model(sim_name) - - fig = plt.figure(figsize=figure_size) - fig.tight_layout() + with styles.USGSMap() as fs: + gwf = sim.get_model(sim_name) - # create MODFLOW 6 cell-by-cell budget object - qx, qy, qz = flopy.utils.postprocessing.get_specific_discharge( - gwf.output.budget().get_data(text="DATA-SPDIS", totim=1.0)[0], - gwf, - ) + fig = plt.figure(figsize=figure_size) + fig.tight_layout() - ax = fig.add_subplot(1, 1, 1) - pxs = flopy.plot.PlotCrossSection(model=gwf, ax=ax, line={"column": 0}) - pxs.plot_grid(linewidth=0.5) - pxs.plot_vector(qx, qy, qz, normalize=True) - ax.set_xlabel("y position (m)") - ax.set_ylabel("z position (m)") - - # save figure - if config.plotSave: - fpth = os.path.join( - "..", "figures", f"{sim_name}-spdis{config.figure_ext}" + # create MODFLOW 6 cell-by-cell budget object + qx, qy, qz = flopy.utils.postprocessing.get_specific_discharge( + gwf.output.budget().get_data(text="DATA-SPDIS", totim=1.0)[0], + gwf, ) - fig.savefig(fpth) - return + ax = fig.add_subplot(1, 1, 1) + pxs = flopy.plot.PlotCrossSection(model=gwf, ax=ax, line={"column": 0}) + pxs.plot_grid(linewidth=0.5) + pxs.plot_vector(qx, qy, qz, normalize=True) + ax.set_xlabel("y position (m)") + ax.set_ylabel("z position (m)") -def plot_results(sim, silent=True): - if config.plotModel: - plot_spdis(sim) - return + if plot_show: + plt.show() + if plot_save: + fpth = os.path.join("..", "figures", f"{sim_name}-spdis.png") + fig.savefig(fpth) -# Function that wraps all of the steps for the FHB model -# -# 1. build_model, -# 2. write_model, -# 3. run_model, and -# 4. plot_results. -# - +def plot_results(sim, silent=True): + plot_spdis(sim) -def simulation(idx, silent=True): - sim = build_model() - write_model(sim, silent=silent) - success = run_model(sim, silent=silent) - if success: - plot_results(sim, silent=silent) +# - -# nosetest - exclude block from this nosetest to the next nosetest -def test_01(): - simulation(0, silent=False) +# ### Running the example +# +# Define and invoke a function to run the example scenario, then plot results. -# nosetest end +# + +def scenario(idx, silent=True): + sim = build_models() + if write: + write_models(sim, silent=silent) + if run: + run_models(sim, silent=silent) + if plot: + plot_results(sim, silent=silent) -if __name__ == "__main__": - # ### Whirl Simulation - # - # Simulated heads in the Whirl model with anisotropy in x direction. - simulation(0) +# Simulated heads in the Whirl model with anisotropy in x direction. +scenario(0) +# - diff --git a/scripts/ex-gwf-zaidel.py b/scripts/ex-gwf-zaidel.py index 9a913db86..cb4c0c3c8 100644 --- a/scripts/ex-gwf-zaidel.py +++ b/scripts/ex-gwf-zaidel.py @@ -1,47 +1,44 @@ # ## Zaidel (2013) example # -# This problem is described in Zaidel (2013) and represents a discontinuous +# Described in Zaidel (2013), representing a discontinuous # water table configuration over a stairway impervious base. -# -# ### Zaidel (2013) Problem Setup +# ### Initial setup # -# Imports +# Import dependencies, define the example name and workspace, and read settings from environment variables. +# + import os -import sys +import pathlib as pl import flopy import matplotlib.pyplot as plt import numpy as np +from flopy.plot.styles import styles +from modflow_devtools.misc import get_env, timed -# Append to system path to include the common subdirectory - -sys.path.append(os.path.join("..", "common")) - -# import common functionality - -import config -from figspecs import USGSFigure - -# Set figure properties specific to the - -figure_size = (6.3, 2.5) - -# Base simulation and model name and workspace - -ws = config.base_ws +# Example name and base workspace +sim_name = "ex-gwf-zaidel" +workspace = pl.Path("../examples") -# Simulation name +# Settings from environment variables +write = get_env("WRITE", True) +run = get_env("RUN", True) +plot = get_env("PLOT", True) +plot_show = get_env("PLOT_SHOW", True) +plot_save = get_env("PLOT_SAVE", True) +# - -sim_name = "ex-gwf-zaidel" +# ### Define parameters +# +# Define model units, parameters and other settings. +# + # Model units - length_units = "meters" time_units = "days" -# Scenario parameters +# Scenario-specific parameters parameters = { "ex-gwf-zaidel-p01a": { "H2": 1.0, @@ -51,8 +48,7 @@ }, } -# Table - +# Model parameters nper = 1 # Number of periods nlay = 1 # Number of layers nrow = 1 # Number of rows @@ -65,12 +61,10 @@ k11 = 0.0001 # Horizontal hydraulic conductivity ($m/day$) H1 = 23.0 # Constant head in column 1 ($m$) -# Static temporal data used by TDIS file - +# Time discretization tdis_ds = ((1.0, 1, 1.0),) # Build stairway bottom - botm = np.zeros((nlay, nrow, ncol), dtype=float) base = 20.0 for j in range(ncol): @@ -79,106 +73,89 @@ base -= 5 # Solver parameters - nouter = 500 ninner = 50 hclose = 1e-9 rclose = 1e-6 +# - -# ### Functions to build, write, run, and plot the Zaidel model +# ### Model setup # -# MODFLOW 6 flopy simulation object (sim) is returned if building the model - - -def build_model(H2=1.0): - if config.buildModel: - # Constant head cells are specified on the left and right edge of the model - chd_spd = [ - [0, 0, 0, H1], - [0, 0, ncol - 1, H2], - ] - - sim_ws = os.path.join(ws, sim_name) - sim = flopy.mf6.MFSimulation( - sim_name=sim_name, sim_ws=sim_ws, exe_name="mf6" - ) - flopy.mf6.ModflowTdis( - sim, nper=nper, perioddata=tdis_ds, time_units=time_units - ) - flopy.mf6.ModflowIms( - sim, - linear_acceleration="bicgstab", - outer_maximum=nouter, - outer_dvclose=hclose, - inner_maximum=ninner, - inner_dvclose=hclose, - rcloserecord=f"{rclose} strict", - ) - gwf = flopy.mf6.ModflowGwf( - sim, modelname=sim_name, newtonoptions="newton" - ) - flopy.mf6.ModflowGwfdis( - gwf, - length_units=length_units, - nlay=nlay, - nrow=nrow, - ncol=ncol, - delr=delr, - delc=delc, - top=top, - botm=botm, - ) - flopy.mf6.ModflowGwfnpf( - gwf, - icelltype=icelltype, - k=k11, - ) - flopy.mf6.ModflowGwfic(gwf, strt=strt) - flopy.mf6.ModflowGwfchd(gwf, stress_period_data=chd_spd) - - head_filerecord = f"{sim_name}.hds" - flopy.mf6.ModflowGwfoc( - gwf, - head_filerecord=head_filerecord, - saverecord=[("HEAD", "ALL")], - ) - return sim - return None - - -# Function to write Zaidel model files - - -def write_model(sim, silent=True): - if config.writeModel: - sim.write_simulation(silent=silent) - - -# Function to run the Zaidel model. -# True is returned if the model runs successfully +# Define functions to build models, write input files, and run the simulation. + + +# + +def build_models(H2=1.0): + # Constant head cells are specified on the left and right edge of the model + chd_spd = [ + [0, 0, 0, H1], + [0, 0, ncol - 1, H2], + ] + + sim_ws = os.path.join(workspace, sim_name) + sim = flopy.mf6.MFSimulation(sim_name=sim_name, sim_ws=sim_ws, exe_name="mf6") + flopy.mf6.ModflowTdis(sim, nper=nper, perioddata=tdis_ds, time_units=time_units) + flopy.mf6.ModflowIms( + sim, + linear_acceleration="bicgstab", + outer_maximum=nouter, + outer_dvclose=hclose, + inner_maximum=ninner, + inner_dvclose=hclose, + rcloserecord=f"{rclose} strict", + ) + gwf = flopy.mf6.ModflowGwf(sim, modelname=sim_name, newtonoptions="newton") + flopy.mf6.ModflowGwfdis( + gwf, + length_units=length_units, + nlay=nlay, + nrow=nrow, + ncol=ncol, + delr=delr, + delc=delc, + top=top, + botm=botm, + ) + flopy.mf6.ModflowGwfnpf( + gwf, + icelltype=icelltype, + k=k11, + ) + flopy.mf6.ModflowGwfic(gwf, strt=strt) + flopy.mf6.ModflowGwfchd(gwf, stress_period_data=chd_spd) + + head_filerecord = f"{sim_name}.hds" + flopy.mf6.ModflowGwfoc( + gwf, + head_filerecord=head_filerecord, + saverecord=[("HEAD", "ALL")], + ) + return sim + + +def write_models(sim, silent=True): + sim.write_simulation(silent=silent) + + +@timed +def run_models(sim, silent=True): + success, buff = sim.run_simulation(silent=silent) + assert success, buff + + +# - + +# ### Plotting results # +# Define functions to plot model results. - -@config.timeit -def run_model(sim, silent=True): - success = True - if config.runModel: - success, buff = sim.run_simulation(silent=silent) - if not success: - print(buff) - - return success - - -# Function to plot the Zaidel model results. -# +# + +# Figure properties +figure_size = (6.3, 2.5) def plot_results(idx, sim, silent=True): - verbose = not silent - if config.plotModel: - fs = USGSFigure(figure_type="map", verbose=verbose) - sim_ws = os.path.join(ws, sim_name) + with styles.USGSMap(): gwf = sim.get_model(sim_name) xedge = gwf.modelgrid.xvertices[0] zedge = np.array([botm[0, 0, 0]] + botm.flatten().tolist()) @@ -234,67 +211,52 @@ def plot_results(idx, sim, silent=True): mec="0.75", label="Model Base", ) - fs.graph_legend(ax, ncol=2, loc="upper right") + styles.graph_legend(ax, ncol=2, loc="upper right") # plot colorbar cax = plt.axes([0.62, 0.76, 0.325, 0.025]) - cbar = plt.colorbar( - plot_obj, shrink=0.8, orientation="horizontal", cax=cax - ) + cbar = plt.colorbar(plot_obj, shrink=0.8, orientation="horizontal", cax=cax) cbar.ax.tick_params(size=0) cbar.ax.set_xlabel(r"Head, $m$", fontsize=9) - # save figure - if config.plotSave: + if plot_show: + plt.show() + if plot_save: fpth = os.path.join( "..", "figures", - f"{sim_name}-{idx + 1:02d}{config.figure_ext}", + f"{sim_name}-{idx + 1:02d}.png", ) fig.savefig(fpth) -# Function that wraps all of the steps for the TWRI model -# -# 1. build_model, -# 2. write_model, -# 3. run_model, and -# 4. plot_results. +# - + +# ### Running the example # +# Define and invoke a function to run the example scenario, then plot results. -def simulation(idx, silent=True): +# + +def scenario(idx, silent=True): key = list(parameters.keys())[idx] params = parameters[key].copy() - - sim = build_model(**params) - - write_model(sim, silent=silent) - - success = run_model(sim, silent=silent) - - if success: + sim = build_models(**params) + if write: + write_models(sim, silent=silent) + if run: + run_models(sim, silent=silent) + if plot: plot_results(idx, sim, silent=silent) -# nosetest - exclude block from this nosetest to the next nosetest -def test_01(): - simulation(0, silent=False) - - -def test_02(): - simulation(1, silent=False) - +# - -# nosetest end -if __name__ == "__main__": - # ### Zaidel Simulation - # - # Simulated heads in the Zaidel model with H2 = 1. +# Run the Zaidel model with H2 = 1, then plot simulated heads. - simulation(0) +scenario(0) - # Simulated heads in the Zaidel model with H2 = 10. +# Run the Zaidel model with H2 = 10, then plot simulated heads. - simulation(1) +scenario(1) diff --git a/scripts/ex-gwt-gwtgwt-p10.py b/scripts/ex-gwt-gwtgwt-p10.py index 3c6199bf0..6fd9a5709 100644 --- a/scripts/ex-gwt-gwtgwt-p10.py +++ b/scripts/ex-gwt-gwtgwt-p10.py @@ -6,36 +6,44 @@ # The results are checked for equivalence with the MODFLOW 6 GWT # solutions as produced by the example 'MT3DMS problem 10'. -import os - -# Imports and extend system path to include the common subdirectory -import sys +# ### Initial setup +# +# Import dependencies, define the example name and workspace, and read settings from environment variables. -sys.path.append(os.path.join("..", "common")) +# + +import os +import pathlib as pl -import config import flopy import matplotlib.pyplot as plt import numpy as np -from figspecs import USGSFigure +import pooch +from flopy.plot.styles import styles from flopy.utils.util_array import read1d - -mf6exe = "mf6" - -# ### Model Input Parameters - -# Set figure properties specific to this problem -figure_size = (6, 8) +from modflow_devtools.misc import get_env # Base simulation and model name and workspace -ws = config.base_ws +workspace = pl.Path("../examples") example_name = "ex-gwt-gwtgwt-mt3dms-p10" +# Settings from environment variables +write = get_env("WRITE", True) +run = get_env("RUN", True) +plot = get_env("PLOT", True) +plot_show = get_env("PLOT_SHOW", True) +plot_save = get_env("PLOT_SAVE", True) +# - + +# ### Define parameters +# +# Define model units, parameters and other settings. Note: the (relative) dimensions of the two models are not configurable. + +# + # Model units length_units = "feet" time_units = "days" -# Note: the (relative) dimensions of the two models are not configurable +# Model parameters nlay = 4 # Number of layers nlay_inn = 4 # Number of layers nrow = 61 # Number of rows @@ -46,10 +54,8 @@ delr_inn = 50 # Column width inner model ($ft$) delc = "varies" # Row width ($ft$) delc_inn = 50 # Row width inner model ($ft$) - xshift = 5100.0 # X offset inner model yshift = 9100.0 # Y offset inner model - delz = 25.0 # Layer thickness ($ft$) top = 780.0 # Top of the model ($ft$) satthk = 100.0 # Saturated thickness ($ft$) @@ -71,11 +77,7 @@ ttsmult = 1.0 # multiplier # Additional model input -delr = ( - [2000, 1600, 800, 400, 200, 100] - + 28 * [50] - + [100, 200, 400, 800, 1600, 2000] -) +delr = [2000, 1600, 800, 400, 200, 100] + 28 * [50] + [100, 200, 400, 800, 1600, 2000] delc = ( [2000, 2000, 2000, 1600, 800, 400, 200, 100] + 45 * [50] @@ -86,7 +88,11 @@ laytyp = icelltype = 0 # Starting heads from file: -f = open(os.path.join("..", "data", "ex-gwt-mt3dms-p10", "p10shead.dat")) +fpth = pooch.retrieve( + url=f"https://github.com/MODFLOW-USGS/modflow6-examples/raw/master/data/ex-gwt-mt3dms-p10/p10shead.dat", + known_hash="md5:c6591c3c3cfd023ab930b7b1121bfccf", +) +f = open(fpth) s0 = np.empty((nrow * ncol), dtype=float) s0 = read1d(f, s0).reshape((nrow, ncol)) f.close() @@ -120,7 +126,11 @@ # Transport related # Starting concentrations from file: -f = open(os.path.join("..", "data", "ex-gwt-mt3dms-p10", "p10cinit.dat")) +fpth = pooch.retrieve( + url=f"https://github.com/MODFLOW-USGS/modflow6-examples/raw/master/data/ex-gwt-mt3dms-p10/p10cinit.dat", + known_hash="md5:8e2d3ba7af1ec65bb07f6039d1dfb2c8", +) +f = open(fpth) c0 = np.empty((nrow * ncol), dtype=float) c0 = read1d(f, c0).reshape((nrow, ncol)) f.close() @@ -136,7 +146,6 @@ atv = al * trpv dmcoef = 0.0 # ft^2/day -# c0 = 0.0 botm = [top - delz * k for k in range(1, nlay + 1)] mixelm = 0 @@ -197,23 +206,21 @@ # Advection scheme = "Undefined" +# - +# ### Model setup +# +# Define functions to build models, write input files, and run the simulation. -# ### Build the MODFLOW 6 simulation -def build_model(sim_name): - if not config.buildModel: - return - sim_ws = os.path.join(ws, sim_name) - sim = flopy.mf6.MFSimulation( - sim_name=sim_name, sim_ws=sim_ws, exe_name=mf6exe - ) +# + +def build_models(sim_name): + sim_ws = os.path.join(workspace, sim_name) + sim = flopy.mf6.MFSimulation(sim_name=sim_name, sim_ws=sim_ws, exe_name="mf6") # Instantiating time discretization tdis_rc = [(perlen, nstp, 1.0)] - flopy.mf6.ModflowTdis( - sim, nper=1, perioddata=tdis_rc, time_units=time_units - ) + flopy.mf6.ModflowTdis(sim, nper=1, perioddata=tdis_rc, time_units=time_units) # add both solutions to the simulation add_flow(sim) @@ -235,12 +242,9 @@ def build_model(sim_name): filename="{}.gwfgwt".format("inner"), ) - sim.write_simulation() - return sim -# Function to add the two GWF models, and their exchange def add_flow(sim): global exgdata @@ -358,8 +362,8 @@ def add_flow(sim): # ) -# Create the outer GWF model def add_outer_gwfmodel(sim): + """Create the outer GWF model""" mname = gwfname_out # Instantiating groundwater flow model @@ -412,16 +416,12 @@ def add_outer_gwfmodel(sim): for i in np.arange(nrow): # (l, r, c), head, conc chdspd.append([(k, i, 0), strt[k, i, 0], 0.0]) # left - chdspd.append( - [(k, i, ncol - 1), strt[k, i, ncol - 1], 0.0] - ) # right + chdspd.append([(k, i, ncol - 1), strt[k, i, ncol - 1], 0.0]) # right for j in np.arange(1, ncol - 1): # skip corners, already added above # (l, r, c), head, conc chdspd.append([(k, 0, j), strt[k, 0, j], 0.0]) # top - chdspd.append( - [(k, nrow - 1, j), strt[k, nrow - 1, j], 0.0] - ) # bottom + chdspd.append([(k, nrow - 1, j), strt[k, nrow - 1, j], 0.0]) # bottom chdspd = {0: chdspd} @@ -465,8 +465,8 @@ def add_outer_gwfmodel(sim): return gwf -# Create the inner GWF model def add_inner_gwfmodel(sim): + """Create the inner GWF model""" mname = gwfname_inn # Instantiating groundwater flow submodel @@ -554,8 +554,8 @@ def add_inner_gwfmodel(sim): return gwf -# Function to add the transport models and exchange to the simulation def add_transport(sim): + """Add the transport models and exchange to the simulation""" # Create iterative model solution imsgwt = flopy.mf6.ModflowIms( sim, @@ -617,8 +617,8 @@ def add_transport(sim): return sim -# Create the outer GWT model def add_outer_gwtmodel(sim): + """Create the outer GWT model""" mname = gwtname_out gwt = flopy.mf6.MFModel( sim, @@ -687,9 +687,7 @@ def add_outer_gwtmodel(sim): gwt, budget_filerecord=f"{mname}.cbc", concentration_filerecord=f"{mname}.ucn", - concentrationprintrecord=[ - ("COLUMNS", 10, "WIDTH", 15, "DIGITS", 6, "GENERAL") - ], + concentrationprintrecord=[("COLUMNS", 10, "WIDTH", 15, "DIGITS", 6, "GENERAL")], saverecord=[ ("CONCENTRATION", "LAST"), ("CONCENTRATION", "STEPS", "1", "250", "375", "500"), @@ -702,10 +700,9 @@ def add_outer_gwtmodel(sim): return gwt -# Create the inner GWT model def add_inner_gwtmodel(sim): + """Create the inner GWT model""" mname = gwtname_inn - gwt = flopy.mf6.MFModel( sim, model_type="gwt6", @@ -775,9 +772,7 @@ def add_inner_gwtmodel(sim): gwt, budget_filerecord=f"{mname}.cbc", concentration_filerecord=f"{mname}.ucn", - concentrationprintrecord=[ - ("COLUMNS", 10, "WIDTH", 15, "DIGITS", 6, "GENERAL") - ], + concentrationprintrecord=[("COLUMNS", 10, "WIDTH", 15, "DIGITS", 6, "GENERAL")], saverecord=[ ("CONCENTRATION", "LAST"), ("CONCENTRATION", "STEPS", "1", "250", "375", "500"), @@ -790,56 +785,51 @@ def add_inner_gwtmodel(sim): return gwt -# ### Simulation Run and Results - - -# Run the simulation and generate the results -def run_model(sim): +def run_models(sim): success = True - if config.runModel: + if run: success, buff = sim.run_simulation() if not success: print(buff) return success +# - + +# ### Plotting results +# +# Define functions to plot model results. + +# + +# Figure properties +figure_size = (6, 8) + + # Load MODFLOW 6 reference for the concentrations (GWT MT3DMS p10) def get_reference_data_conc(): - fpath = open( - os.path.join( - "..", - "data", - "ex-gwt-gwtgwt-p10", - "gwt-p10-mf6_conc_lay3_1days.txt", - ) + fpth = pooch.retrieve( + url=f"https://github.com/MODFLOW-USGS/modflow6-examples/raw/master/data/ex-gwt-gwtgwt-p10/gwt-p10-mf6_conc_lay3_1days.txt", + known_hash="md5:bbb596110559d00b7f01032998cf35f4", ) + fpath = open(fpth) conc1 = np.loadtxt(fpath) - fpath = open( - os.path.join( - "..", - "data", - "ex-gwt-gwtgwt-p10", - "gwt-p10-mf6_conc_lay3_500days.txt", - ) + fpth = pooch.retrieve( + url=f"https://github.com/MODFLOW-USGS/modflow6-examples/raw/master/data/ex-gwt-gwtgwt-p10/gwt-p10-mf6_conc_lay3_500days.txt", + known_hash="md5:3b3b9321ae6c801fec7d3562aa44a009", ) + fpath = open(fpth) conc500 = np.loadtxt(fpath) - fpath = open( - os.path.join( - "..", - "data", - "ex-gwt-gwtgwt-p10", - "gwt-p10-mf6_conc_lay3_750days.txt", - ) + fpth = pooch.retrieve( + url=f"https://github.com/MODFLOW-USGS/modflow6-examples/raw/master/data/ex-gwt-gwtgwt-p10/gwt-p10-mf6_conc_lay3_750days.txt", + known_hash="md5:0d1c2e7682a946e11b56f87c28c0ebd7", ) + fpath = open(fpth) conc750 = np.loadtxt(fpath) - fpath = open( - os.path.join( - "..", - "data", - "ex-gwt-gwtgwt-p10", - "gwt-p10-mf6_conc_lay3_1000days.txt", - ) + fpth = pooch.retrieve( + url=f"https://github.com/MODFLOW-USGS/modflow6-examples/raw/master/data/ex-gwt-gwtgwt-p10/gwt-p10-mf6_conc_lay3_1000days.txt", + known_hash="md5:c5fe612424e5f83fb2ac46cd4fdc8fb6", ) + fpath = open(fpth) conc1000 = np.loadtxt(fpath) return [conc1, conc500, conc750, conc1000] @@ -847,41 +837,29 @@ def get_reference_data_conc(): # Load MODFLOW 6 reference for heads (GWT MT3DMS p10) def get_reference_data_heads(): - fpath = open( - os.path.join( - "..", - "data", - "ex-gwt-gwtgwt-p10", - "gwt-p10-mf6_head_lay3_1days.txt", - ) + fpth = pooch.retrieve( + url=f"https://github.com/MODFLOW-USGS/modflow6-examples/raw/master/data/ex-gwt-gwtgwt-p10/gwt-p10-mf6_head_lay3_1days.txt", + known_hash="md5:0c5ce894877692b0a018587a2df068d6", ) + fpath = open(fpth) head1 = np.loadtxt(fpath) - fpath = open( - os.path.join( - "..", - "data", - "ex-gwt-gwtgwt-p10", - "gwt-p10-mf6_head_lay3_500days.txt", - ) + fpth = pooch.retrieve( + url=f"https://github.com/MODFLOW-USGS/modflow6-examples/raw/master/data/ex-gwt-gwtgwt-p10/gwt-p10-mf6_head_lay3_500days.txt", + known_hash="md5:b4b56f9ecad0abafc6c62072cc5f15e9", ) + fpath = open(fpth) head500 = np.loadtxt(fpath) - fpath = open( - os.path.join( - "..", - "data", - "ex-gwt-gwtgwt-p10", - "gwt-p10-mf6_head_lay3_750days.txt", - ) + fpth = pooch.retrieve( + url=f"https://github.com/MODFLOW-USGS/modflow6-examples/raw/master/data/ex-gwt-gwtgwt-p10/gwt-p10-mf6_head_lay3_750days.txt", + known_hash="md5:1c35fee2f7764c1c28eb84ed98b1300c", ) + fpath = open(fpth) head750 = np.loadtxt(fpath) - fpath = open( - os.path.join( - "..", - "data", - "ex-gwt-gwtgwt-p10", - "gwt-p10-mf6_head_lay3_1000days.txt", - ) + fpth = pooch.retrieve( + url=f"https://github.com/MODFLOW-USGS/modflow6-examples/raw/master/data/ex-gwt-gwtgwt-p10/gwt-p10-mf6_head_lay3_1000days.txt", + known_hash="md5:b8e67997ca429f6f20e15852fb2fba9f", ) + fpath = open(fpth) head1000 = np.loadtxt(fpath) return [head1, head500, head750, head1000] @@ -924,98 +902,96 @@ def plot_difference_conc(sim): conc_mf6_outer = ucnobj_mf6_outer.get_alldata() # Create figure for scenario - fs = USGSFigure(figure_type="graph", verbose=False) - plt.rcParams["lines.dashed_pattern"] = [5.0, 5.0] - fig = plt.figure(figsize=figure_size, dpi=300, tight_layout=True) - - # Difference in concentration @ 1 day - ax = fig.add_subplot(2, 2, 1, aspect="equal") - mm = flopy.plot.PlotMapView(model=gwt_outer) - mm.plot_grid(color=".5", alpha=0.2) - istep = 0 - ilayer = 2 - c_1day = conc_mf6_outer[istep] - c_1day[:, 8:53, 6:34] = conc_mf6[istep] - c_1day_singlemodel_lay3 = conc_singlemodel_lay3[istep] - pa = mm.plot_array(c_1day[ilayer] - c_1day_singlemodel_lay3) - xc, yc = gwt.modelgrid.xycenters - plt.xlim(5100, 5100 + 28 * 50) - plt.ylim(9100, 9100 + 45 * 50) - plt.xlabel("Distance Along X-Axis, in meters") - plt.ylabel("Distance Along Y-Axis, in meters") - plt.colorbar(pa, shrink=0.5) - - # Plot the wells as well - for cid, f, c in welspd_mf6: - plt.plot(xshift + xc[cid[2]], yshift + yc[cid[1]], "ks") - title = "Difference Layer 3 Time = 1 day" - fs.heading(letter="A", heading=title) - - # Difference in concentration @ 500 days - ax = fig.add_subplot(2, 2, 2, aspect="equal") - mm = flopy.plot.PlotMapView(model=gwt_outer) - mm.plot_grid(color=".5", alpha=0.2) - istep = 1 - ilayer = 2 - c_500days = conc_mf6_outer[istep] - c_500days[:, 8:53, 6:34] = conc_mf6[istep] - c_500days_singlemodel_lay3 = conc_singlemodel_lay3[istep] - pa = mm.plot_array(c_500days[ilayer] - c_500days_singlemodel_lay3) - plt.xlim(5100, 5100 + 28 * 50) - plt.ylim(9100, 9100 + 45 * 50) - plt.xlabel("Distance Along X-Axis, in meters") - plt.ylabel("Distance Along Y-Axis, in meters") - plt.colorbar(pa, shrink=0.5) - for cid, f, c in welspd_mf6: - plt.plot(xshift + xc[cid[2]], yshift + yc[cid[1]], "ks") - title = "Difference Layer 3 Time = 500 days" - fs.heading(letter="B", heading=title) - - # Difference in concentration @ 750 days - ax = fig.add_subplot(2, 2, 3, aspect="equal") - mm = flopy.plot.PlotMapView(model=gwt_outer) - mm.plot_grid(color=".5", alpha=0.2) - istep = 2 - ilayer = 2 - c_750days = conc_mf6_outer[istep] - c_750days[:, 8:53, 6:34] = conc_mf6[istep] - c_750days_singlemodel_lay3 = conc_singlemodel_lay3[istep] - pa = mm.plot_array(c_750days[ilayer] - c_750days_singlemodel_lay3) - plt.xlim(5100, 5100 + 28 * 50) - plt.ylim(9100, 9100 + 45 * 50) - plt.xlabel("Distance Along X-Axis, in meters") - plt.ylabel("Distance Along Y-Axis, in meters") - plt.colorbar(pa, shrink=0.5) - for cid, f, c in welspd_mf6: - plt.plot(xshift + xc[cid[2]], yshift + yc[cid[1]], "ks") - title = "Difference Layer 3 Time = 750 days" - fs.heading(letter="C", heading=title) - - # Difference in concentration @ 1000 days - ax = fig.add_subplot(2, 2, 4, aspect="equal") - mm = flopy.plot.PlotMapView(model=gwt_outer) - mm.plot_grid(color=".5", alpha=0.2) - istep = 3 - ilayer = 2 - c_1000days = conc_mf6_outer[istep] - c_1000days[:, 8:53, 6:34] = conc_mf6[istep] - c_1000days_singlemodel_lay3 = conc_singlemodel_lay3[istep] - pa = mm.plot_array(c_1000days[ilayer] - c_1000days_singlemodel_lay3) - plt.xlim(5100, 5100 + 28 * 50) - plt.ylim(9100, 9100 + 45 * 50) - plt.xlabel("Distance Along X-Axis, in meters") - plt.ylabel("Distance Along Y-Axis, in meters") - plt.colorbar(pa, shrink=0.5) - - for cid, f, c in welspd_mf6: - plt.plot(xshift + xc[cid[2]], yshift + yc[cid[1]], "ks") - title = "Difference Layer 3 Time = 1000 days" - fs.heading(letter="D", heading=title) - - fpath = os.path.join("..", "figures", "ex-gwtgwt-p10-diffconc.png") - fig.savefig(fpath) - - return + with styles.USGSPlot() as fs: + plt.rcParams["lines.dashed_pattern"] = [5.0, 5.0] + fig = plt.figure(figsize=figure_size, dpi=300, tight_layout=True) + + # Difference in concentration @ 1 day + ax = fig.add_subplot(2, 2, 1, aspect="equal") + mm = flopy.plot.PlotMapView(model=gwt_outer) + mm.plot_grid(color=".5", alpha=0.2) + istep = 0 + ilayer = 2 + c_1day = conc_mf6_outer[istep] + c_1day[:, 8:53, 6:34] = conc_mf6[istep] + c_1day_singlemodel_lay3 = conc_singlemodel_lay3[istep] + pa = mm.plot_array(c_1day[ilayer] - c_1day_singlemodel_lay3) + xc, yc = gwt.modelgrid.xycenters + plt.xlim(5100, 5100 + 28 * 50) + plt.ylim(9100, 9100 + 45 * 50) + plt.xlabel("Distance Along X-Axis, in meters") + plt.ylabel("Distance Along Y-Axis, in meters") + plt.colorbar(pa, shrink=0.5) + + # Plot the wells as well + for cid, f, c in welspd_mf6: + plt.plot(xshift + xc[cid[2]], yshift + yc[cid[1]], "ks") + title = "Difference Layer 3 Time = 1 day" + styles.heading(letter="A", heading=title) + + # Difference in concentration @ 500 days + ax = fig.add_subplot(2, 2, 2, aspect="equal") + mm = flopy.plot.PlotMapView(model=gwt_outer) + mm.plot_grid(color=".5", alpha=0.2) + istep = 1 + ilayer = 2 + c_500days = conc_mf6_outer[istep] + c_500days[:, 8:53, 6:34] = conc_mf6[istep] + c_500days_singlemodel_lay3 = conc_singlemodel_lay3[istep] + pa = mm.plot_array(c_500days[ilayer] - c_500days_singlemodel_lay3) + plt.xlim(5100, 5100 + 28 * 50) + plt.ylim(9100, 9100 + 45 * 50) + plt.xlabel("Distance Along X-Axis, in meters") + plt.ylabel("Distance Along Y-Axis, in meters") + plt.colorbar(pa, shrink=0.5) + for cid, f, c in welspd_mf6: + plt.plot(xshift + xc[cid[2]], yshift + yc[cid[1]], "ks") + title = "Difference Layer 3 Time = 500 days" + styles.heading(letter="B", heading=title) + + # Difference in concentration @ 750 days + ax = fig.add_subplot(2, 2, 3, aspect="equal") + mm = flopy.plot.PlotMapView(model=gwt_outer) + mm.plot_grid(color=".5", alpha=0.2) + istep = 2 + ilayer = 2 + c_750days = conc_mf6_outer[istep] + c_750days[:, 8:53, 6:34] = conc_mf6[istep] + c_750days_singlemodel_lay3 = conc_singlemodel_lay3[istep] + pa = mm.plot_array(c_750days[ilayer] - c_750days_singlemodel_lay3) + plt.xlim(5100, 5100 + 28 * 50) + plt.ylim(9100, 9100 + 45 * 50) + plt.xlabel("Distance Along X-Axis, in meters") + plt.ylabel("Distance Along Y-Axis, in meters") + plt.colorbar(pa, shrink=0.5) + for cid, f, c in welspd_mf6: + plt.plot(xshift + xc[cid[2]], yshift + yc[cid[1]], "ks") + title = "Difference Layer 3 Time = 750 days" + styles.heading(letter="C", heading=title) + + # Difference in concentration @ 1000 days + ax = fig.add_subplot(2, 2, 4, aspect="equal") + mm = flopy.plot.PlotMapView(model=gwt_outer) + mm.plot_grid(color=".5", alpha=0.2) + istep = 3 + ilayer = 2 + c_1000days = conc_mf6_outer[istep] + c_1000days[:, 8:53, 6:34] = conc_mf6[istep] + c_1000days_singlemodel_lay3 = conc_singlemodel_lay3[istep] + pa = mm.plot_array(c_1000days[ilayer] - c_1000days_singlemodel_lay3) + plt.xlim(5100, 5100 + 28 * 50) + plt.ylim(9100, 9100 + 45 * 50) + plt.xlabel("Distance Along X-Axis, in meters") + plt.ylabel("Distance Along Y-Axis, in meters") + plt.colorbar(pa, shrink=0.5) + + for cid, f, c in welspd_mf6: + plt.plot(xshift + xc[cid[2]], yshift + yc[cid[1]], "ks") + title = "Difference Layer 3 Time = 1000 days" + styles.heading(letter="D", heading=title) + + fpath = os.path.join("..", "figures", "ex-gwtgwt-p10-diffconc.png") + fig.savefig(fpath) # Plot the difference in head after 1,500,750,1000 days @@ -1033,98 +1009,96 @@ def plot_difference_heads(sim): head_mf6_outer = hobj_mf6_outer.get_alldata() # Create figure for scenario - fs = USGSFigure(figure_type="graph", verbose=False) - plt.rcParams["lines.dashed_pattern"] = [5.0, 5.0] - fig = plt.figure(figsize=figure_size, dpi=300, tight_layout=True) - - # Difference in heads @ 1 day - ax = fig.add_subplot(2, 2, 1, aspect="equal") - mm = flopy.plot.PlotMapView(model=gwf_outer) - mm.plot_grid(color=".5", alpha=0.2) - istep = 0 - ilayer = 2 - h_1day = head_mf6_outer[istep] - h_1day[:, 8:53, 6:34] = head_mf6[istep] - h_1day_singlemodel_lay3 = head_singlemodel_lay3[istep] - pa = mm.plot_array(h_1day[ilayer] - h_1day_singlemodel_lay3) - xc, yc = gwf.modelgrid.xycenters - plt.xlim(5100, 5100 + 28 * 50) - plt.ylim(9100, 9100 + 45 * 50) - plt.xlabel("Distance Along X-Axis, in meters") - plt.ylabel("Distance Along Y-Axis, in meters") - plt.colorbar(pa, shrink=0.5) - - # Plot the wells as well - for cid, f, c in welspd_mf6: - plt.plot(xshift + xc[cid[2]], yshift + yc[cid[1]], "ks") - title = "Difference Layer 3 Time = 1 day" - fs.heading(letter="A", heading=title) - - # Difference in heads @ 500 days - ax = fig.add_subplot(2, 2, 2, aspect="equal") - mm = flopy.plot.PlotMapView(model=gwf_outer) - mm.plot_grid(color=".5", alpha=0.2) - istep = 1 - ilayer = 2 - h_500days = head_mf6_outer[istep] - h_500days[:, 8:53, 6:34] = head_mf6[istep] - h_500days_singlemodel_lay3 = head_singlemodel_lay3[istep] - pa = mm.plot_array(h_500days[ilayer] - h_500days_singlemodel_lay3) - plt.xlim(5100, 5100 + 28 * 50) - plt.ylim(9100, 9100 + 45 * 50) - plt.xlabel("Distance Along X-Axis, in meters") - plt.ylabel("Distance Along Y-Axis, in meters") - plt.colorbar(pa, shrink=0.5) - for cid, f, c in welspd_mf6: - plt.plot(xshift + xc[cid[2]], yshift + yc[cid[1]], "ks") - title = "Difference Layer 3 Time = 500 days" - fs.heading(letter="B", heading=title) - - # Difference in heads @ 750 days - ax = fig.add_subplot(2, 2, 3, aspect="equal") - mm = flopy.plot.PlotMapView(model=gwf_outer) - mm.plot_grid(color=".5", alpha=0.2) - istep = 2 - ilayer = 2 - h_750days = head_mf6_outer[istep] - h_750days[:, 8:53, 6:34] = head_mf6[istep] - h_750days_singlemodel_lay3 = head_singlemodel_lay3[istep] - pa = mm.plot_array(h_750days[ilayer] - h_750days_singlemodel_lay3) - plt.xlim(5100, 5100 + 28 * 50) - plt.ylim(9100, 9100 + 45 * 50) - plt.xlabel("Distance Along X-Axis, in meters") - plt.ylabel("Distance Along Y-Axis, in meters") - plt.colorbar(pa, shrink=0.5) - for cid, f, c in welspd_mf6: - plt.plot(xshift + xc[cid[2]], yshift + yc[cid[1]], "ks") - title = "Difference Layer 3 Time = 750 days" - fs.heading(letter="C", heading=title) - - # Difference in heads @ 1000 days - ax = fig.add_subplot(2, 2, 4, aspect="equal") - mm = flopy.plot.PlotMapView(model=gwf_outer) - mm.plot_grid(color=".5", alpha=0.2) - istep = 3 - ilayer = 2 - h_1000days = head_mf6_outer[istep] - h_1000days[:, 8:53, 6:34] = head_mf6[istep] - h_1000days_singlemodel_lay3 = head_singlemodel_lay3[istep] - pa = mm.plot_array(h_1000days[ilayer] - h_1000days_singlemodel_lay3) - plt.xlim(5100, 5100 + 28 * 50) - plt.ylim(9100, 9100 + 45 * 50) - plt.xlabel("Distance Along X-Axis, in meters") - plt.ylabel("Distance Along Y-Axis, in meters") - plt.colorbar(pa, shrink=0.5) - - for cid, f, c in welspd_mf6: - plt.plot(xshift + xc[cid[2]], yshift + yc[cid[1]], "ks") - title = "Difference Layer 3 Time = 1000 days" - fs.heading(letter="D", heading=title) - - fpath = os.path.join("..", "figures", "ex-gwtgwt-p10-diffhead.png") - fig.savefig(fpath) - - return + with styles.USGSPlot() as fs: + plt.rcParams["lines.dashed_pattern"] = [5.0, 5.0] + fig = plt.figure(figsize=figure_size, dpi=300, tight_layout=True) + + # Difference in heads @ 1 day + ax = fig.add_subplot(2, 2, 1, aspect="equal") + mm = flopy.plot.PlotMapView(model=gwf_outer) + mm.plot_grid(color=".5", alpha=0.2) + istep = 0 + ilayer = 2 + h_1day = head_mf6_outer[istep] + h_1day[:, 8:53, 6:34] = head_mf6[istep] + h_1day_singlemodel_lay3 = head_singlemodel_lay3[istep] + pa = mm.plot_array(h_1day[ilayer] - h_1day_singlemodel_lay3) + xc, yc = gwf.modelgrid.xycenters + plt.xlim(5100, 5100 + 28 * 50) + plt.ylim(9100, 9100 + 45 * 50) + plt.xlabel("Distance Along X-Axis, in meters") + plt.ylabel("Distance Along Y-Axis, in meters") + plt.colorbar(pa, shrink=0.5) + + # Plot the wells as well + for cid, f, c in welspd_mf6: + plt.plot(xshift + xc[cid[2]], yshift + yc[cid[1]], "ks") + title = "Difference Layer 3 Time = 1 day" + styles.heading(letter="A", heading=title) + + # Difference in heads @ 500 days + ax = fig.add_subplot(2, 2, 2, aspect="equal") + mm = flopy.plot.PlotMapView(model=gwf_outer) + mm.plot_grid(color=".5", alpha=0.2) + istep = 1 + ilayer = 2 + h_500days = head_mf6_outer[istep] + h_500days[:, 8:53, 6:34] = head_mf6[istep] + h_500days_singlemodel_lay3 = head_singlemodel_lay3[istep] + pa = mm.plot_array(h_500days[ilayer] - h_500days_singlemodel_lay3) + plt.xlim(5100, 5100 + 28 * 50) + plt.ylim(9100, 9100 + 45 * 50) + plt.xlabel("Distance Along X-Axis, in meters") + plt.ylabel("Distance Along Y-Axis, in meters") + plt.colorbar(pa, shrink=0.5) + for cid, f, c in welspd_mf6: + plt.plot(xshift + xc[cid[2]], yshift + yc[cid[1]], "ks") + title = "Difference Layer 3 Time = 500 days" + styles.heading(letter="B", heading=title) + + # Difference in heads @ 750 days + ax = fig.add_subplot(2, 2, 3, aspect="equal") + mm = flopy.plot.PlotMapView(model=gwf_outer) + mm.plot_grid(color=".5", alpha=0.2) + istep = 2 + ilayer = 2 + h_750days = head_mf6_outer[istep] + h_750days[:, 8:53, 6:34] = head_mf6[istep] + h_750days_singlemodel_lay3 = head_singlemodel_lay3[istep] + pa = mm.plot_array(h_750days[ilayer] - h_750days_singlemodel_lay3) + plt.xlim(5100, 5100 + 28 * 50) + plt.ylim(9100, 9100 + 45 * 50) + plt.xlabel("Distance Along X-Axis, in meters") + plt.ylabel("Distance Along Y-Axis, in meters") + plt.colorbar(pa, shrink=0.5) + for cid, f, c in welspd_mf6: + plt.plot(xshift + xc[cid[2]], yshift + yc[cid[1]], "ks") + title = "Difference Layer 3 Time = 750 days" + styles.heading(letter="C", heading=title) + + # Difference in heads @ 1000 days + ax = fig.add_subplot(2, 2, 4, aspect="equal") + mm = flopy.plot.PlotMapView(model=gwf_outer) + mm.plot_grid(color=".5", alpha=0.2) + istep = 3 + ilayer = 2 + h_1000days = head_mf6_outer[istep] + h_1000days[:, 8:53, 6:34] = head_mf6[istep] + h_1000days_singlemodel_lay3 = head_singlemodel_lay3[istep] + pa = mm.plot_array(h_1000days[ilayer] - h_1000days_singlemodel_lay3) + plt.xlim(5100, 5100 + 28 * 50) + plt.ylim(9100, 9100 + 45 * 50) + plt.xlabel("Distance Along X-Axis, in meters") + plt.ylabel("Distance Along Y-Axis, in meters") + plt.colorbar(pa, shrink=0.5) + + for cid, f, c in welspd_mf6: + plt.plot(xshift + xc[cid[2]], yshift + yc[cid[1]], "ks") + title = "Difference Layer 3 Time = 1000 days" + styles.heading(letter="D", heading=title) + + fpath = os.path.join("..", "figures", "ex-gwtgwt-p10-diffhead.png") + fig.savefig(fpath) # Plot the concentration, this figure should be compared to the same figure in MT3DMS problem 10 @@ -1139,102 +1113,108 @@ def plot_concentration(sim): conc_mf6_outer = ucnobj_mf6_outer.get_alldata() # Create figure for scenario - fs = USGSFigure(figure_type="graph", verbose=False) - plt.rcParams["lines.dashed_pattern"] = [5.0, 5.0] + with styles.USGSPlot() as fs: + plt.rcParams["lines.dashed_pattern"] = [5.0, 5.0] + + xc, yc = gwt.modelgrid.xycenters + + # Plot init. concentration (lay=3) + fig = plt.figure(figsize=figure_size, dpi=300, tight_layout=True) + + ax = fig.add_subplot(2, 2, 1, aspect="equal") + mm = flopy.plot.PlotMapView(model=gwt_outer) + mm.plot_grid(color=".5", alpha=0.2) + + cs = mm.contour_array(sconc[2], levels=np.arange(20, 200, 20)) + plt.xlim(5100, 5100 + 28 * 50) + plt.ylim(9100, 9100 + 45 * 50) + plt.xlabel("Distance Along X-Axis, in meters") + plt.ylabel("Distance Along Y-Axis, in meters") + plt.clabel(cs, fmt=r"%3d") + # Plot the wells as well + for cid, f, c in welspd_mf6: + plt.plot(xshift + xc[cid[2]], yshift + yc[cid[1]], "ks") + title = "Layer 3 Initial Concentration" + styles.heading(letter="A", heading=title) + + ax = fig.add_subplot(2, 2, 2, aspect="equal") + mm = flopy.plot.PlotMapView(model=gwt_outer) + mm.plot_grid(color=".5", alpha=0.2) + c_500days = conc_mf6_outer[1] + c_500days[:, 8:53, 6:34] = conc_mf6[1] # Concentration @ 500 days + cs = mm.contour_array(c_500days[2], levels=np.arange(10, 200, 10)) + plt.xlim(5100, 5100 + 28 * 50) + plt.ylim(9100, 9100 + 45 * 50) + plt.xlabel("Distance Along X-Axis, in meters") + plt.ylabel("Distance Along Y-Axis, in meters") + plt.clabel(cs, fmt=r"%3d") + for cid, f, c in welspd_mf6: + plt.plot(xshift + xc[cid[2]], yshift + yc[cid[1]], "ks") + title = "Layer 3 Time = 500 days" + styles.heading(letter="B", heading=title) + + ax = fig.add_subplot(2, 2, 3, aspect="equal") + mm = flopy.plot.PlotMapView(model=gwt_outer) + mm.plot_grid(color=".5", alpha=0.2) + c_750days = conc_mf6_outer[2] + c_750days[:, 8:53, 6:34] = conc_mf6[2] # Concentration @ 750 days + cs = mm.contour_array(c_750days[2], levels=np.arange(10, 200, 10)) + plt.xlim(5100, 5100 + 28 * 50) + plt.ylim(9100, 9100 + 45 * 50) + plt.xlabel("Distance Along X-Axis, in meters") + plt.ylabel("Distance Along Y-Axis, in meters") + plt.clabel(cs, fmt=r"%3d") + for cid, f, c in welspd_mf6: + plt.plot(xshift + xc[cid[2]], yshift + yc[cid[1]], "ks") + title = "Layer 3 Time = 750 days" + styles.heading(letter="C", heading=title) + + ax = fig.add_subplot(2, 2, 4, aspect="equal") + mm = flopy.plot.PlotMapView(model=gwt_outer) + mm.plot_grid(color=".5", alpha=0.2) + c_1000days = conc_mf6_outer[3] + c_1000days[:, 8:53, 6:34] = conc_mf6[3] # Concentration @ 1000 days + cs = mm.contour_array(c_1000days[2], levels=np.arange(10, 200, 10)) + plt.xlim(5100, 5100 + 28 * 50) + plt.ylim(9100, 9100 + 45 * 50) + plt.xlabel("Distance Along X-Axis, in meters") + plt.ylabel("Distance Along Y-Axis, in meters") + plt.clabel(cs, fmt=r"%3d") + for cid, f, c in welspd_mf6: + plt.plot(xshift + xc[cid[2]], yshift + yc[cid[1]], "ks") + title = "Layer 3 Time = 1000 days" + styles.heading(letter="D", heading=title) + + fpath = os.path.join("..", "figures", "ex-gwtgwt-p10-concentration.png") + fig.savefig(fpath) - xc, yc = gwt.modelgrid.xycenters - # Plot init. concentration (lay=3) - fig = plt.figure(figsize=figure_size, dpi=300, tight_layout=True) - - ax = fig.add_subplot(2, 2, 1, aspect="equal") - mm = flopy.plot.PlotMapView(model=gwt_outer) - mm.plot_grid(color=".5", alpha=0.2) - - cs = mm.contour_array(sconc[2], levels=np.arange(20, 200, 20)) - plt.xlim(5100, 5100 + 28 * 50) - plt.ylim(9100, 9100 + 45 * 50) - plt.xlabel("Distance Along X-Axis, in meters") - plt.ylabel("Distance Along Y-Axis, in meters") - plt.clabel(cs, fmt=r"%3d") - # Plot the wells as well - for cid, f, c in welspd_mf6: - plt.plot(xshift + xc[cid[2]], yshift + yc[cid[1]], "ks") - title = "Layer 3 Initial Concentration" - fs.heading(letter="A", heading=title) - - ax = fig.add_subplot(2, 2, 2, aspect="equal") - mm = flopy.plot.PlotMapView(model=gwt_outer) - mm.plot_grid(color=".5", alpha=0.2) - c_500days = conc_mf6_outer[1] - c_500days[:, 8:53, 6:34] = conc_mf6[1] # Concentration @ 500 days - cs = mm.contour_array(c_500days[2], levels=np.arange(10, 200, 10)) - plt.xlim(5100, 5100 + 28 * 50) - plt.ylim(9100, 9100 + 45 * 50) - plt.xlabel("Distance Along X-Axis, in meters") - plt.ylabel("Distance Along Y-Axis, in meters") - plt.clabel(cs, fmt=r"%3d") - for cid, f, c in welspd_mf6: - plt.plot(xshift + xc[cid[2]], yshift + yc[cid[1]], "ks") - title = "Layer 3 Time = 500 days" - fs.heading(letter="B", heading=title) - - ax = fig.add_subplot(2, 2, 3, aspect="equal") - mm = flopy.plot.PlotMapView(model=gwt_outer) - mm.plot_grid(color=".5", alpha=0.2) - c_750days = conc_mf6_outer[2] - c_750days[:, 8:53, 6:34] = conc_mf6[2] # Concentration @ 750 days - cs = mm.contour_array(c_750days[2], levels=np.arange(10, 200, 10)) - plt.xlim(5100, 5100 + 28 * 50) - plt.ylim(9100, 9100 + 45 * 50) - plt.xlabel("Distance Along X-Axis, in meters") - plt.ylabel("Distance Along Y-Axis, in meters") - plt.clabel(cs, fmt=r"%3d") - for cid, f, c in welspd_mf6: - plt.plot(xshift + xc[cid[2]], yshift + yc[cid[1]], "ks") - title = "Layer 3 Time = 750 days" - fs.heading(letter="C", heading=title) - - ax = fig.add_subplot(2, 2, 4, aspect="equal") - mm = flopy.plot.PlotMapView(model=gwt_outer) - mm.plot_grid(color=".5", alpha=0.2) - c_1000days = conc_mf6_outer[3] - c_1000days[:, 8:53, 6:34] = conc_mf6[3] # Concentration @ 1000 days - cs = mm.contour_array(c_1000days[2], levels=np.arange(10, 200, 10)) - plt.xlim(5100, 5100 + 28 * 50) - plt.ylim(9100, 9100 + 45 * 50) - plt.xlabel("Distance Along X-Axis, in meters") - plt.ylabel("Distance Along Y-Axis, in meters") - plt.clabel(cs, fmt=r"%3d") - for cid, f, c in welspd_mf6: - plt.plot(xshift + xc[cid[2]], yshift + yc[cid[1]], "ks") - title = "Layer 3 Time = 1000 days" - fs.heading(letter="D", heading=title) - - fpath = os.path.join("..", "figures", "ex-gwtgwt-p10-concentration.png") - fig.savefig(fpath) +# Generates all plots +def plot_results(sim): + print("Plotting model results...") + plot_grids(sim) + plot_concentration(sim) + plot_difference_conc(sim) + plot_difference_heads(sim) - return +# - -# Generates all plots -def plot_results(sim): - if config.plotModel: - print("Plotting model results...") - plot_grids(sim) - plot_concentration(sim) - plot_difference_conc(sim) - plot_difference_heads(sim) +# ### Running the example +# +# Define and invoke a function to run the example scenario, then plot results. -def test_01(): - sim = build_model(example_name) - run_model(sim) - plot_results(sim) +# + +def scenario(): + sim = build_models(example_name) + if write: + sim.write_simulation() + if run: + run_models(sim) + if plot: + plot_results(sim) -# Main -if __name__ == "__main__": - sim = build_model(example_name) - run_model(sim) - plot_results(sim) +scenario() +# - diff --git a/scripts/ex-gwt-hecht-mendez.py b/scripts/ex-gwt-hecht-mendez.py index b8f5934e4..a0a703be1 100644 --- a/scripts/ex-gwt-hecht-mendez.py +++ b/scripts/ex-gwt-hecht-mendez.py @@ -1,4 +1,4 @@ -# ## Three-Dimensional Heat Transport Study +# ## Three-dimensional heat transport example # # The purpose of this script is to (1) recreate the 3D heat transport example # first published in Groundwater in 2010 titled, "Evaluating MT3DMS for Heat @@ -20,46 +20,44 @@ # to the analytical solution can be improved by refining the temporal # resolution of the simulation. -# ### MODFLOW 6 GWT MT3DMS Heat Transport Problem Setup - -# Append to system path to include the common subdirectory +# ### Initial setup +# +# Import dependencies, define the example name and workspace, and read settings from environment variables. +# + import os -import sys - -sys.path.append(os.path.join("..", "common")) - -# Imports +import pathlib as pl -import analytical -import config import flopy import matplotlib.pyplot as plt import numpy as np -from figspecs import USGSFigure -from flopy.utils.util_array import read1d +from flopy.plot.styles import styles +from modflow_devtools.misc import get_env, timed +from scipy.special import erf, erfc -mf6exe = "mf6" -exe_name_mf = "mf2005" -exe_name_mt = "mt3dusgs" - -# Set figure properties specific to this problem - -figure_size = (5.5, 2.75) +# Example name and base workspace +name = "hecht-mendez" +workspace = pl.Path("../examples") -# Base simulation and model name and workspace +# Settings from environment variables +write = get_env("WRITE", True) +run = get_env("RUN", True) +plot = get_env("PLOT", True) +plot_show = get_env("PLOT_SHOW", True) +plot_save = get_env("PLOT_SAVE", True) +# - -ws = config.base_ws -name = "hecht-mendez" +# ### Define parameters +# +# Define model units, parameters and other settings. +# + # Model units - length_units = "meters" time_units = "seconds" # Set scenario parameters (make sure there is at least one blank line before next item) -# This entire dictionary is passed to _build_model()_ using the kwargs argument - +# This entire dictionary is passed to _build_models()_ using the kwargs argument parameters = { "ex-gwt-hecht-mendez-a": { "peclet": 0.0, @@ -82,10 +80,8 @@ } # Scenario parameter units -# # add parameter_units to add units to the scenario parameter table that is automatically # built and used by the .tex input - parameter_units = { "peclet": "$unitless$", "gradient": "$m/m$", @@ -93,8 +89,7 @@ "constantheadright": "$m$", } -# Table Flow and transport parameters used in Hecht-Mendez example - +# Model parameters nlay = 13 # Number of layers nrow = 83 # Number of rows ncol = 247 # Number of columns @@ -207,13 +202,162 @@ cobs = [(7 - 1, 42 - 1, k - 1) for k in range(22, 224, 2)] # Solver settings - nouter, ninner = 100, 300 hclose, rclose, relax = 5e-5, 1e-8, 1.0 +# - -# ### Functions to build, write, and run models published in Hecht-Mendez 2010 +# ### Model setup # -# MODFLOW 6 flopy simulation object (sim) is returned if building the model +# Define functions to build models, write input files, and run the simulation. + + +# + +def hechtMendez_SS_3d( + x_pos, To, Y3d, Z3d, ath, atv, Fplanar, va, n, rhow, cw, thermdiff +): + """ + Calculate the analytical solution for changes in temperature three- + dimensional changes in temperature using transient solution provided in + the appendix of Hecht-Mendez et al. (2010) as equation A5. Note that for + SS conditions, the erfc term reduces to 1 as t -> infinity and the To/2 + term becomes T. + + Parameters + ---------- + x_pos : float or ndarray + x position + To : float or ndarray + initial temperature of the ground, degrees K + Y3d : float or ndarray + dimension of source in y direction for 3D test problem + Z3d : float or ndarray + dimension of source in z direction for 3D test problem + ath : float or ndarray + transverse horizontal dispersivity + atv : float or ndarray + transverse vertical dispersivity + Fplanar : float or ndarray + energy extraction (point source) + va : float or ndarray + seepage velocity + n : float or ndarray + porosity + rhow : float or ndarray + desity of water + cw : float or ndarray + specific heat capacity of water + thermdiff : float or ndarray + molecular diffusion coefficient, or in this case thermal + diffusivity + """ + + # calculate transverse horizontal heat dispersion + Dy = ath * (va**2 / abs(va)) + thermdiff + t2 = erf(Y3d / (4 * np.sqrt(Dy * (x_pos / va)))) + + Dz = atv * (va**2 / abs(va)) + thermdiff + t3 = erf(Z3d / (4 * np.sqrt(Dz * (x_pos / va)))) + + # initial temperature at the source + To_planar = Fplanar / (abs(va) * n * rhow * cw) + + sln = To + (To_planar * t2 * t3) + return sln + + +def hechtMendezSS(x_pos, y, a, F0, va, n, rhow, cw, thermdiff): + """ + Calculate the analytical solution for changes in temperature three- + dimensional changes in temperature for a steady state solution provided in + the appendix of Hecht-Mendez et al. (2010) as equation A4 + + Parameters + ---------- + x : float or ndarray + x position + y : float or ndarray + y position + a : float or ndarray + longitudinal dispersivity + F0 : float or ndarray + energy extraction (point source) + va : float or ndarray + seepage velocity + n : float or ndarray + porosity + rhow : float or ndarray + desity of water + cw : float or ndarray + specific heat capacity of water + thermdiff : float or ndarray + molecular diffusion coefficient, or in this case thermal + diffusivity + """ + + # calculate transverse horizontal heat dispersion + Dth = a * (va**2 / abs(va)) + thermdiff + + t1 = F0 / (va * n * rhow * cw * ((4 * np.pi * Dth * (x_pos / va)) ** (0.5))) + t2 = np.exp((-1 * va * y**2) / (4 * Dth * x_pos)) + sln = t1 * t2 + return sln + + +def hechtMendez3d(x_pos, t, Y, Z, al, ath, atv, thermdiff, va, n, R, Fplanar, cw, rhow): + """ + Calculate the analytical solution for three-dimensional changes in + temperature based on the solution provided in the appendix of Hecht-Mendez + et al. (2010) as equation A5 + + Parameters + ---------- + x : float or ndarray + x position + t : float or ndarray + time + Y : float or ndarray + dimension of the source in the y direction + Z : float or ndarray + dimension of the source in the z direction + al : float or ndarray + longitudinal dispersivity + ath : float or ndarray + transverse horizontal dispersivity + atv : float or ndarray + transverse vertical dispersivity + thermdiff : float or ndarray + molecular diffusion coefficient, or in this case thermal + diffusivity + va : float or ndarray + seepage velocity + n : float or ndarray + porosity + R : float or ndarray + retardation coefficient + Fplanar : float or ndarray + energy extraction (point source) + cw : float or ndarray + specific heat capacity of water + rhow : float or ndarray + desity of water + + """ + To_planar = Fplanar / (va * n * rhow * cw) + + Dl = al * (va**2 / abs(va)) + thermdiff + numer = R * x_pos - va * t + denom = 2 * np.sqrt(Dl * R * t) + + t1 = (To_planar / 2) * erfc(numer / denom) + + Dth = ath * (va**2 / abs(va)) + thermdiff + t2 = erf(Y / (4 * np.sqrt(Dth * (x_pos / va)))) + + Dtv = atv * (va**2 / abs(va)) + thermdiff + t3 = erf(Z / (4 * np.sqrt(Dtv * (x_pos / va)))) + + sln = t1 * t2 * t3 + return sln def build_mf2k5_flow_model( @@ -224,69 +368,64 @@ def build_mf2k5_flow_model( constantheadright=14, silent=False, ): - if config.buildModel: - print(f"Building mf2005 model...{sim_name}") - mt3d_ws = os.path.join(ws, sim_name, "mt3d") - modelname_mf = "hecht-mendez" - - # Instantiate the MODFLOW model - mf = flopy.modflow.Modflow( - modelname=modelname_mf, model_ws=mt3d_ws, exe_name=exe_name_mf - ) + print(f"Building mf2005 model...{sim_name}") + mt3d_ws = os.path.join(workspace, sim_name, "mt3d") + modelname_mf = "hecht-mendez" - # Instantiate discretization package - # units: itmuni=4 (days), lenuni=2 (m) - flopy.modflow.ModflowDis( - mf, - nlay=nlay, - nrow=nrow, - ncol=ncol, - delr=delr, - delc=delc, - top=top, - botm=botm, - perlen=perlen, - nstp=nstp, - itmuni=4, - lenuni=1, - steady=True, - ) - - # Instantiate basic package - strt[:, :, -1] = constantheadright - flopy.modflow.ModflowBas(mf, ibound=ibound, strt=strt) - - # Instantiate layer property flow package - flopy.modflow.ModflowLpf(mf, hk=hk, layvka=0, vka=vk, laytyp=laytyp) - - # Instantiate solver package - flopy.modflow.ModflowPcg( - mf, - mxiter=90, - iter1=20, - npcond=1, - hclose=hclose, - rclose=rclose, - relax=relax, - nbpol=2, - iprpcg=2, - mutpcg=0.0, - ) + # Instantiate the MODFLOW model + mf = flopy.modflow.Modflow( + modelname=modelname_mf, model_ws=mt3d_ws, exe_name=exe_name_mf + ) - # Instantiate link mass transport package (for writing linker file) - flopy.modflow.ModflowLmt(mf) + # Instantiate discretization package + # units: itmuni=4 (days), lenuni=2 (m) + flopy.modflow.ModflowDis( + mf, + nlay=nlay, + nrow=nrow, + ncol=ncol, + delr=delr, + delc=delc, + top=top, + botm=botm, + perlen=perlen, + nstp=nstp, + itmuni=4, + lenuni=1, + steady=True, + ) - # Instantiate output control (OC) package - spd = { - (0, 0): ["save head"], - } - oc = flopy.modflow.ModflowOc(mf, stress_period_data=spd) + # Instantiate basic package + strt[:, :, -1] = constantheadright + flopy.modflow.ModflowBas(mf, ibound=ibound, strt=strt) + + # Instantiate layer property flow package + flopy.modflow.ModflowLpf(mf, hk=hk, layvka=0, vka=vk, laytyp=laytyp) + + # Instantiate solver package + flopy.modflow.ModflowPcg( + mf, + mxiter=90, + iter1=20, + npcond=1, + hclose=hclose, + rclose=rclose, + relax=relax, + nbpol=2, + iprpcg=2, + mutpcg=0.0, + ) - return mf - return None + # Instantiate link mass transport package (for writing linker file) + flopy.modflow.ModflowLmt(mf) + # Instantiate output control (OC) package + spd = { + (0, 0): ["save head"], + } + oc = flopy.modflow.ModflowOc(mf, stress_period_data=spd) -# MODFLOW 6 + return mf def build_mf6_flow_model( @@ -297,125 +436,117 @@ def build_mf6_flow_model( constantheadright=14, silent=False, ): - if config.buildModel: - print(f"Building mf6gwf model...{sim_name}") - gwfname = "gwf-" + name - sim_ws = os.path.join(ws, sim_name, "mf6gwf") - sim = flopy.mf6.MFSimulation( - sim_name=gwfname, sim_ws=sim_ws, exe_name="mf6" - ) - - # Instantiating MODFLOW 6 time discretization - tdis_rc = [] - tdis_rc.append((perlen, 1, 1.0)) - flopy.mf6.ModflowTdis( - sim, nper=1, perioddata=tdis_rc, time_units=time_units - ) - - # Instantiating MODFLOW 6 groundwater flow model - gwf = flopy.mf6.ModflowGwf( - sim, - modelname=gwfname, - save_flows=True, - model_nam_file=f"{gwfname}.nam", - ) + print(f"Building mf6gwf model...{sim_name}") + gwfname = "gwf-" + name + sim_ws = os.path.join(workspace, sim_name, "mf6gwf") + sim = flopy.mf6.MFSimulation(sim_name=gwfname, sim_ws=sim_ws, exe_name="mf6") + + # Instantiating MODFLOW 6 time discretization + tdis_rc = [] + tdis_rc.append((perlen, 1, 1.0)) + flopy.mf6.ModflowTdis(sim, nper=1, perioddata=tdis_rc, time_units=time_units) + + # Instantiating MODFLOW 6 groundwater flow model + gwf = flopy.mf6.ModflowGwf( + sim, + modelname=gwfname, + save_flows=True, + model_nam_file=f"{gwfname}.nam", + ) - # Instantiating MODFLOW 6 solver for flow model - imsgwf = flopy.mf6.ModflowIms( - sim, - print_option="SUMMARY", - outer_dvclose=hclose, - outer_maximum=nouter, - under_relaxation="NONE", - inner_maximum=ninner, - inner_dvclose=hclose, - rcloserecord=rclose, - linear_acceleration="CG", - scaling_method="NONE", - reordering_method="NONE", - relaxation_factor=relax, - filename=f"{gwfname}.ims", - ) - sim.register_ims_package(imsgwf, [gwf.name]) - - # Instantiating MODFLOW 6 discretization package - flopy.mf6.ModflowGwfdis( - gwf, - length_units=length_units, - nlay=nlay, - nrow=nrow, - ncol=ncol, - delr=delr, - delc=delc, - top=top, - botm=botm, - idomain=idomain, - filename=f"{gwfname}.dis", - ) + # Instantiating MODFLOW 6 solver for flow model + imsgwf = flopy.mf6.ModflowIms( + sim, + print_option="SUMMARY", + outer_dvclose=hclose, + outer_maximum=nouter, + under_relaxation="NONE", + inner_maximum=ninner, + inner_dvclose=hclose, + rcloserecord=rclose, + linear_acceleration="CG", + scaling_method="NONE", + reordering_method="NONE", + relaxation_factor=relax, + filename=f"{gwfname}.ims", + ) + sim.register_ims_package(imsgwf, [gwf.name]) + + # Instantiating MODFLOW 6 discretization package + flopy.mf6.ModflowGwfdis( + gwf, + length_units=length_units, + nlay=nlay, + nrow=nrow, + ncol=ncol, + delr=delr, + delc=delc, + top=top, + botm=botm, + idomain=idomain, + filename=f"{gwfname}.dis", + ) - # Instantiating MODFLOW 6 initial conditions package for flow model - strt[:, :, -1] = constantheadright - flopy.mf6.ModflowGwfic(gwf, strt=strt, filename=f"{gwfname}.ic") - - # Instantiating MODFLOW 6 node-property flow package - flopy.mf6.ModflowGwfnpf( - gwf, - save_flows=True, - k33overk=False, - icelltype=laytyp, - k=hk, - k33=vk, - save_specific_discharge=True, - save_saturation=True, - filename=f"{gwfname}.npf", - ) + # Instantiating MODFLOW 6 initial conditions package for flow model + strt[:, :, -1] = constantheadright + flopy.mf6.ModflowGwfic(gwf, strt=strt, filename=f"{gwfname}.ic") + + # Instantiating MODFLOW 6 node-property flow package + flopy.mf6.ModflowGwfnpf( + gwf, + save_flows=True, + k33overk=False, + icelltype=laytyp, + k=hk, + k33=vk, + save_specific_discharge=True, + save_saturation=True, + filename=f"{gwfname}.npf", + ) - # Instantiate storage package - flopy.mf6.ModflowGwfsto(gwf, ss=0, sy=0, filename=f"{gwfname}.sto") - - # Instantiating MODFLOW 6 constant head package - # MF6 constant head boundaries: - chdspd = [] - # Loop through the left & right sides for all layers. - for k in range(nlay): - for i in range(nrow): - # left-most column: - # (l, r, c), head, conc - chdspd.append([(k, i, 0), strt[k, i, 0], T0]) # left - # right-most column: - chdspd.append([(k, i, ncol - 1), strt[k, i, ncol - 1], T0]) - - chdspd = {0: chdspd} - - flopy.mf6.ModflowGwfchd( - gwf, - maxbound=len(chdspd), - stress_period_data=chdspd, - save_flows=False, - auxiliary="CONCENTRATION", - pname="CHD-1", - filename=f"{gwfname}.chd", - ) + # Instantiate storage package + flopy.mf6.ModflowGwfsto(gwf, ss=0, sy=0, filename=f"{gwfname}.sto") + + # Instantiating MODFLOW 6 constant head package + # MF6 constant head boundaries: + chdspd = [] + # Loop through the left & right sides for all layers. + for k in range(nlay): + for i in range(nrow): + # left-most column: + # (l, r, c), head, conc + chdspd.append([(k, i, 0), strt[k, i, 0], T0]) # left + # right-most column: + chdspd.append([(k, i, ncol - 1), strt[k, i, ncol - 1], T0]) + + chdspd = {0: chdspd} + + flopy.mf6.ModflowGwfchd( + gwf, + maxbound=len(chdspd), + stress_period_data=chdspd, + save_flows=False, + auxiliary="CONCENTRATION", + pname="CHD-1", + filename=f"{gwfname}.chd", + ) - # Instantiating MODFLOW 6 output control package for flow model - flopy.mf6.ModflowGwfoc( - gwf, - head_filerecord=f"{gwfname}.hds", - budget_filerecord=f"{gwfname}.bud", - headprintrecord=[ - ("COLUMNS", 10, "WIDTH", 15, "DIGITS", 6, "GENERAL") - ], - saverecord=[ - ("HEAD", "LAST"), - ("BUDGET", "LAST"), - ], - printrecord=[ - ("HEAD", "LAST"), - ("BUDGET", "LAST"), - ], - ) - return sim - return None + # Instantiating MODFLOW 6 output control package for flow model + flopy.mf6.ModflowGwfoc( + gwf, + head_filerecord=f"{gwfname}.hds", + budget_filerecord=f"{gwfname}.bud", + headprintrecord=[("COLUMNS", 10, "WIDTH", 15, "DIGITS", 6, "GENERAL")], + saverecord=[ + ("HEAD", "LAST"), + ("BUDGET", "LAST"), + ], + printrecord=[ + ("HEAD", "LAST"), + ("BUDGET", "LAST"), + ], + ) + return sim def build_mt3d_transport_model( @@ -427,67 +558,62 @@ def build_mt3d_transport_model( constantheadright=14, silent=False, ): - if config.buildModel: - # Transport - print(f"Building mt3dms model...{sim_name}") - - modelname_mt = "hecht-mendez_mt" - mt3d_ws = os.path.join(ws, sim_name, "mt3d") - mt = flopy.mt3d.Mt3dms( - modelname=modelname_mt, - model_ws=mt3d_ws, - exe_name=exe_name_mt, - modflowmodel=mf, - ) + # Transport + print(f"Building mt3dms model...{sim_name}") + + modelname_mt = "hecht-mendez_mt" + mt3d_ws = os.path.join(workspace, sim_name, "mt3d") + mt = flopy.mt3d.Mt3dms( + modelname=modelname_mt, + model_ws=mt3d_ws, + exe_name=exe_name_mt, + modflowmodel=mf, + ) - # Instantiate basic transport package - if seepagevelocity == 0: - dt0 = 50000 - else: - dt0 = 0.0 - - flopy.mt3d.Mt3dBtn( - mt, - icbund=icbund, - prsity=prsity, - sconc=sconc, - cinact=-1e10, - thkmin=0.01, - ifmtcn=-2, - nprs=2, - timprs=[864000, 12960000], # 10, 150 days - dt0=dt0, - obs=cobs, - chkmas=False, - perlen=perlen, - nstp=nstp, - tsmult=ttsmult, - mxstrn=20000, - ) + # Instantiate basic transport package + if seepagevelocity == 0: + dt0 = 50000 + else: + dt0 = 0.0 + + flopy.mt3d.Mt3dBtn( + mt, + icbund=icbund, + prsity=prsity, + sconc=sconc, + cinact=-1e10, + thkmin=0.01, + ifmtcn=-2, + nprs=2, + timprs=[864000, 12960000], # 10, 150 days + dt0=dt0, + obs=cobs, + chkmas=False, + perlen=perlen, + nstp=nstp, + tsmult=ttsmult, + mxstrn=20000, + ) - # Instatiate the advection package - flopy.mt3d.Mt3dAdv(mt, mixelm=mixelm, percel=percel) + # Instatiate the advection package + flopy.mt3d.Mt3dAdv(mt, mixelm=mixelm, percel=percel) - # Instantiate the dispersion package - flopy.mt3d.Mt3dDsp( - mt, multiDiff=True, al=al, trpt=trpt, trpv=trpv, dmcoef=dmcoef_arr - ) + # Instantiate the dispersion package + flopy.mt3d.Mt3dDsp( + mt, multiDiff=True, al=al, trpt=trpt, trpv=trpv, dmcoef=dmcoef_arr + ) - # Instantiate the source/sink mixing package - ssmspd = {0: ssm_bhe} - flopy.mt3d.Mt3dSsm( - mt, mxss=nrow * ncol * 2 + len(ssm_bhe), stress_period_data=ssmspd - ) + # Instantiate the source/sink mixing package + ssmspd = {0: ssm_bhe} + flopy.mt3d.Mt3dSsm( + mt, mxss=nrow * ncol * 2 + len(ssm_bhe), stress_period_data=ssmspd + ) - # Instantiate the reaction package - flopy.mt3d.Mt3dRct( - mt, isothm=isothm, igetsc=0, rhob=rhob, sp1=sp1, sp2=sp2 - ) + # Instantiate the reaction package + flopy.mt3d.Mt3dRct(mt, isothm=isothm, igetsc=0, rhob=rhob, sp1=sp1, sp2=sp2) - # Instantiate the GCG solver in MT3DMS - flopy.mt3d.Mt3dGcg( - mt, mxiter=100, iter1=50, isolve=1, ncrs=1, cclose=1e-7 - ) + # Instantiate the GCG solver in MT3DMS + flopy.mt3d.Mt3dGcg(mt, mxiter=100, iter1=50, isolve=1, ncrs=1, cclose=1e-7) def build_mf6_transport_model( @@ -498,191 +624,181 @@ def build_mf6_transport_model( constantheadright=14, silent=False, ): - if config.buildModel: - # Instantiating MODFLOW 6 groundwater transport package - print(f"Building mf6gwt model...{sim_name}") - gwtname = "gwt-" + name - sim_ws = os.path.join(ws, sim_name, "mf6gwt") - sim = flopy.mf6.MFSimulation( - sim_name=gwtname, sim_ws=sim_ws, exe_name="mf6" - ) - - # MF6 time discretization is a bit different than corresponding flow simulation - tdis_rc = None - if peclet == 1.0: - # use tsmult to and hardwired number of steps to make it run fast - tdis_rc = [(perlen, 25, 1.3)] - elif peclet == 10.0: - transport_stp_len = 1.296e5 * 3 - nstp_transport = perlen / transport_stp_len - tdis_rc = [(perlen, nstp_transport, 1.0)] - flopy.mf6.ModflowTdis( - sim, nper=len(tdis_rc), perioddata=tdis_rc, time_units=time_units - ) - - gwtname = "gwt-" + name - gwt = flopy.mf6.MFModel( - sim, - model_type="gwt6", - modelname=gwtname, - model_nam_file=f"{gwtname}.nam", - ) - gwt.name_file.save_flows = True - - # create iterative model solution and register the gwt model with it - imsgwt = flopy.mf6.ModflowIms( - sim, - print_option="SUMMARY", - outer_dvclose=hclose, - outer_maximum=nouter, - under_relaxation="NONE", - inner_maximum=ninner, - inner_dvclose=hclose, - rcloserecord=rclose, - linear_acceleration="BICGSTAB", - scaling_method="NONE", - reordering_method="NONE", - relaxation_factor=relax, - filename=f"{gwtname}.ims", - ) - sim.register_ims_package(imsgwt, [gwt.name]) - - # Instantiating MODFLOW 6 transport discretization package - flopy.mf6.ModflowGwtdis( - gwt, - nlay=nlay, - nrow=nrow, - ncol=ncol, - delr=delr, - delc=delc, - top=top, - botm=botm, - idomain=idomain, - filename=f"{gwtname}.dis", - ) - - # Instantiating MODFLOW 6 transport initial concentrations - flopy.mf6.ModflowGwtic(gwt, strt=sconc, filename=f"{gwtname}.ic") + # Instantiating MODFLOW 6 groundwater transport package + print(f"Building mf6gwt model...{sim_name}") + gwtname = "gwt-" + name + sim_ws = os.path.join(workspace, sim_name, "mf6gwt") + sim = flopy.mf6.MFSimulation(sim_name=gwtname, sim_ws=sim_ws, exe_name="mf6") + + # MF6 time discretization is a bit different than corresponding flow simulation + tdis_rc = None + if peclet == 1.0: + # use tsmult to and hardwired number of steps to make it run fast + tdis_rc = [(perlen, 25, 1.3)] + elif peclet == 10.0: + transport_stp_len = 1.296e5 * 3 + nstp_transport = perlen / transport_stp_len + tdis_rc = [(perlen, nstp_transport, 1.0)] + flopy.mf6.ModflowTdis( + sim, nper=len(tdis_rc), perioddata=tdis_rc, time_units=time_units + ) - # Instantiating MODFLOW 6 transport advection package - if mixelm >= 0: - scheme = "UPSTREAM" - elif mixelm == -1: - scheme = "TVD" - else: - raise Exception() - flopy.mf6.ModflowGwtadv(gwt, scheme=scheme, filename=f"{gwtname}.adv") - - # Instantiating MODFLOW 6 transport dispersion package - if al != 0: - flopy.mf6.ModflowGwtdsp( - gwt, - alh=al, - ath1=ath1, - atv=atv, - diffc=dmcoef_arr, - pname="DSP-1", - filename=f"{gwtname}.dsp", - ) + gwtname = "gwt-" + name + gwt = flopy.mf6.MFModel( + sim, + model_type="gwt6", + modelname=gwtname, + model_nam_file=f"{gwtname}.nam", + ) + gwt.name_file.save_flows = True + + # create iterative model solution and register the gwt model with it + imsgwt = flopy.mf6.ModflowIms( + sim, + print_option="SUMMARY", + outer_dvclose=hclose, + outer_maximum=nouter, + under_relaxation="NONE", + inner_maximum=ninner, + inner_dvclose=hclose, + rcloserecord=rclose, + linear_acceleration="BICGSTAB", + scaling_method="NONE", + reordering_method="NONE", + relaxation_factor=relax, + filename=f"{gwtname}.ims", + ) + sim.register_ims_package(imsgwt, [gwt.name]) + + # Instantiating MODFLOW 6 transport discretization package + flopy.mf6.ModflowGwtdis( + gwt, + nlay=nlay, + nrow=nrow, + ncol=ncol, + delr=delr, + delc=delc, + top=top, + botm=botm, + idomain=idomain, + filename=f"{gwtname}.dis", + ) - # Instantiating MODFLOW 6 transport mass storage package - Kd = sp1 - flopy.mf6.ModflowGwtmst( - gwt, - porosity=prsity, - first_order_decay=False, - decay=None, - decay_sorbed=None, - sorption="linear", - bulk_density=rhob, - distcoef=Kd, - pname="MST-1", - filename=f"{gwtname}.mst", - ) + # Instantiating MODFLOW 6 transport initial concentrations + flopy.mf6.ModflowGwtic(gwt, strt=sconc, filename=f"{gwtname}.ic") - # Instantiating MODFLOW 6 transport source-sink mixing package - sourcerecarray = [("CHD-1", "AUX", "CONCENTRATION")] - flopy.mf6.ModflowGwtssm( - gwt, - sources=sourcerecarray, - print_flows=True, - filename=f"{gwtname}.ssm", - ) + # Instantiating MODFLOW 6 transport advection package + if mixelm >= 0: + scheme = "UPSTREAM" + elif mixelm == -1: + scheme = "TVD" + else: + raise Exception() + flopy.mf6.ModflowGwtadv(gwt, scheme=scheme, filename=f"{gwtname}.adv") - flopy.mf6.ModflowGwtsrc( + # Instantiating MODFLOW 6 transport dispersion package + if al != 0: + flopy.mf6.ModflowGwtdsp( gwt, - print_flows=True, - maxbound=len(mf6_bhe), - stress_period_data={0: mf6_bhe}, - pname="SRC-1", - filename=f"{gwtname}.src", - ) - - # Instantiating MODFLOW 6 Flow-Model Interface package - flow_name = gwtname.replace("gwt", "gwf") - pd = [ - ("GWFHEAD", "../mf6gwf/" + flow_name + ".hds", None), - ("GWFBUDGET", "../mf6gwf/" + flow_name + ".bud", None), - ] - flopy.mf6.ModflowGwtfmi(gwt, packagedata=pd) + alh=al, + ath1=ath1, + atv=atv, + diffc=dmcoef_arr, + pname="DSP-1", + filename=f"{gwtname}.dsp", + ) + + # Instantiating MODFLOW 6 transport mass storage package + Kd = sp1 + flopy.mf6.ModflowGwtmst( + gwt, + porosity=prsity, + first_order_decay=False, + decay=None, + decay_sorbed=None, + sorption="linear", + bulk_density=rhob, + distcoef=Kd, + pname="MST-1", + filename=f"{gwtname}.mst", + ) - # Instantiating MODFLOW 6 transport output control package - flopy.mf6.ModflowGwtoc( - gwt, - budget_filerecord=f"{gwtname}.cbc", - concentration_filerecord=f"{gwtname}.ucn", - concentrationprintrecord=[ - ("COLUMNS", 10, "WIDTH", 15, "DIGITS", 6, "GENERAL") - ], - saverecord=[ - ("CONCENTRATION", "LAST"), - ("CONCENTRATION", "STEPS", "15"), - ("BUDGET", "LAST"), - ], - printrecord=[("CONCENTRATION", "LAST"), ("BUDGET", "LAST")], - filename=f"{gwtname}.oc", - ) + # Instantiating MODFLOW 6 transport source-sink mixing package + sourcerecarray = [("CHD-1", "AUX", "CONCENTRATION")] + flopy.mf6.ModflowGwtssm( + gwt, + sources=sourcerecarray, + print_flows=True, + filename=f"{gwtname}.ssm", + ) - return sim - return None + flopy.mf6.ModflowGwtsrc( + gwt, + print_flows=True, + maxbound=len(mf6_bhe), + stress_period_data={0: mf6_bhe}, + pname="SRC-1", + filename=f"{gwtname}.src", + ) + # Instantiating MODFLOW 6 Flow-Model Interface package + flow_name = gwtname.replace("gwt", "gwf") + pd = [ + ("GWFHEAD", "../mf6gwf/" + flow_name + ".hds", None), + ("GWFBUDGET", "../mf6gwf/" + flow_name + ".bud", None), + ] + flopy.mf6.ModflowGwtfmi(gwt, packagedata=pd) + + # Instantiating MODFLOW 6 transport output control package + flopy.mf6.ModflowGwtoc( + gwt, + budget_filerecord=f"{gwtname}.cbc", + concentration_filerecord=f"{gwtname}.ucn", + concentrationprintrecord=[("COLUMNS", 10, "WIDTH", 15, "DIGITS", 6, "GENERAL")], + saverecord=[ + ("CONCENTRATION", "LAST"), + ("CONCENTRATION", "STEPS", "15"), + ("BUDGET", "LAST"), + ], + printrecord=[("CONCENTRATION", "LAST"), ("BUDGET", "LAST")], + filename=f"{gwtname}.oc", + ) -# Function to write model files + return sim def write_mf2k5_models(mf2k5, mt3d, silent=True): - if config.writeModel: - mf2k5.write_input() - mt3d.write_input() + mf2k5.write_input() + mt3d.write_input() def write_mf6_models(sim_mf6gwf, sim_mf6gwt, silent=True): - if config.writeModel: - sim_mf6gwf.write_simulation(silent=silent) - sim_mf6gwt.write_simulation(silent=silent) + sim_mf6gwf.write_simulation(silent=silent) + sim_mf6gwt.write_simulation(silent=silent) -# Function to run the model. True is returned if the model runs successfully. +@timed +def run_models(sim_mf6gwf, sim_mf6gwt, mf2k5=None, mt3d=None, silent=True): + if mf2k5 is not None: + success, buff = mf2k5.run_model(silent=silent) + if mt3d is not None: + success, buff = mt3d.run_model(silent=silent) -@config.timeit -def run_model(sim_mf6gwf, sim_mf6gwt, mf2k5=None, mt3d=None, silent=True): - success = True - if config.runModel: - if mf2k5 is not None: - success, buff = mf2k5.run_model(silent=silent) + success, buff = sim_mf6gwf.run_simulation(silent=silent) + success, buff = sim_mf6gwt.run_simulation(silent=silent) + assert success, buff - if mt3d is not None: - success, buff = mt3d.run_model(silent=silent) - success, buff = sim_mf6gwf.run_simulation(silent=silent) - success, buff = sim_mf6gwt.run_simulation(silent=silent) - if not success: - print(buff) - return success +# - +# ### Plotting results +# +# Define functions to plot model results. -# Function to plot the model results +# + +# Figure properties +figure_size = (5.5, 2.75) def plot_results( @@ -697,104 +813,96 @@ def plot_results( seepagevelocity=0, constantheadright=14, ): - if config.plotModel: - print("Plotting model results...") - if mt3d is not None: - mt3d_out_path = mt3d.model_ws - - # Get the MT3DMS concentration output - fname_mt3d = os.path.join(mt3d_out_path, "MT3D001.UCN") - ucnobj_mt3d = flopy.utils.UcnFile(fname_mt3d) - times_mt3d = ucnobj_mt3d.get_times() - conc_mt3d = ucnobj_mt3d.get_alldata() - - mf6_out_path = sim_mf6gwt.simulation_data.mfpath.get_sim_path() - - # Get the MF6 concentration output - gwt = sim_mf6gwt.get_model("gwt-" + name) - ucnobj_mf6 = gwt.output.concentration() - - times_mf6 = ucnobj_mf6.get_times() - conc_mf6 = ucnobj_mf6.get_alldata() - - # Get the x location of the cell centroids - model_centroids_x = [] - for i, (cum_pos, half_width) in enumerate( - zip(np.cumsum(delr), np.divide(delr, 2)) - ): - if i > 0: - model_centroids_x.append(cum_pos - half_width) - else: - model_centroids_x.append(half_width) - - # Next subtract off the location of the BHE - model_centroids_x_BHE = [ - val - model_centroids_x[21] for val in model_centroids_x - ] - # Drop the negative locations to the left of the BHE - model_centroids_x_right_of_BHE = model_centroids_x_BHE[ - 22: - ] # Does not include - - # Analytical solution(s) - To = T0 # deg K (initial temperature of the ground) - Y3d = 0.1 # m - Z3d = delz # m - ath = al * trpt # m - atv = al * trpv # m - F0 = -60 # W/m - Fplanar = -600 # W/m^2 - va = seepagevelocity - n = prsity # porosity - rhow = 1000.0 # density of water - cw = 4185.0 # heat capacity of water - thermdiff = 1.86e-6 # "molecular diffusion" representing heat - # conduction - - x_pos = np.array(model_centroids_x_right_of_BHE) - ss_sln = analytical.hechtMendez_SS_3d( - x_pos, To, Y3d, Z3d, ath, atv, Fplanar, va, n, rhow, cw, thermdiff - ) + if mt3d is not None: + mt3d_out_path = mt3d.model_ws - t = 864000 # seconds (10 days) - Y = 0.1 # dimension of source in the y direction - Z = delz # dimension of source in the z direction - R = 2.59 # From Hecht-Mendez manuscript + # Get the MT3DMS concentration output + fname_mt3d = os.path.join(mt3d_out_path, "MT3D001.UCN") + ucnobj_mt3d = flopy.utils.UcnFile(fname_mt3d) + times_mt3d = ucnobj_mt3d.get_times() + conc_mt3d = ucnobj_mt3d.get_alldata() - tr_sln = analytical.hechtMendez3d( - x_pos, - t, - Y, - Z, - al, - ath, - atv, - thermdiff, - va, - n, - R, - Fplanar, - cw, - rhow, - ) + mf6_out_path = sim_mf6gwt.simulation_data.mfpath.get_sim_path() - # list of where to draw vertical lines - avlines = list(range(10)) + list(range(10, 110, 10)) + # Get the MF6 concentration output + gwt = sim_mf6gwt.get_model("gwt-" + name) + ucnobj_mf6 = gwt.output.concentration() - # fill variables with analytical solutions - y_ss_anly_sln = ss_sln - y_tr_anly_sln = [285.15 + val for val in tr_sln] + times_mf6 = ucnobj_mf6.get_times() + conc_mf6 = ucnobj_mf6.get_alldata() - # fill variables containing the simulated solutions - if mt3d is not None: - y_10_mt_sln = conc_mt3d[0, 6, (42 - 1), 22:] - y_150_mt_sln = conc_mt3d[-1, 6, (42 - 1), 22:] + # Get the x location of the cell centroids + model_centroids_x = [] + for i, (cum_pos, half_width) in enumerate(zip(np.cumsum(delr), np.divide(delr, 2))): + if i > 0: + model_centroids_x.append(cum_pos - half_width) + else: + model_centroids_x.append(half_width) + + # Next subtract off the location of the BHE + model_centroids_x_BHE = [val - model_centroids_x[21] for val in model_centroids_x] + # Drop the negative locations to the left of the BHE + model_centroids_x_right_of_BHE = model_centroids_x_BHE[22:] # Does not include + + # Analytical solution(s) + To = T0 # deg K (initial temperature of the ground) + Y3d = 0.1 # m + Z3d = delz # m + ath = al * trpt # m + atv = al * trpv # m + F0 = -60 # W/m + Fplanar = -600 # W/m^2 + va = seepagevelocity + n = prsity # porosity + rhow = 1000.0 # density of water + cw = 4185.0 # heat capacity of water + thermdiff = 1.86e-6 # "molecular diffusion" representing heat + # conduction + + x_pos = np.array(model_centroids_x_right_of_BHE) + ss_sln = hechtMendez_SS_3d( + x_pos, To, Y3d, Z3d, ath, atv, Fplanar, va, n, rhow, cw, thermdiff + ) + + t = 864000 # seconds (10 days) + Y = 0.1 # dimension of source in the y direction + Z = delz # dimension of source in the z direction + R = 2.59 # From Hecht-Mendez manuscript + + tr_sln = hechtMendez3d( + x_pos, + t, + Y, + Z, + al, + ath, + atv, + thermdiff, + va, + n, + R, + Fplanar, + cw, + rhow, + ) - y_10_mf6_sln = conc_mf6[0, 6, (42 - 1), 22:] - y_150_mf6_sln = conc_mf6[-1, 6, (42 - 1), 22:] + # list of where to draw vertical lines + avlines = list(range(10)) + list(range(10, 110, 10)) - # Create figure for scenario - fs = USGSFigure(figure_type="graph", verbose=False) + # fill variables with analytical solutions + y_ss_anly_sln = ss_sln + y_tr_anly_sln = [285.15 + val for val in tr_sln] + + # fill variables containing the simulated solutions + if mt3d is not None: + y_10_mt_sln = conc_mt3d[0, 6, (42 - 1), 22:] + y_150_mt_sln = conc_mt3d[-1, 6, (42 - 1), 22:] + + y_10_mf6_sln = conc_mf6[0, 6, (42 - 1), 22:] + y_150_mf6_sln = conc_mf6[-1, 6, (42 - 1), 22:] + + # Create figure for scenario + with styles.USGSPlot() as fs: sim_name = sim_mf6gwt.name plt.rcParams["lines.dashed_pattern"] = [5.0, 5.0] @@ -819,13 +927,9 @@ def plot_results( mt_ss_ln = ax.plot( x_pos, y_150_mt_sln, "r+", label="Steady state MT3DMS, TVD" ) - mt_tr_ln = ax.plot( - x_pos, y_10_mt_sln, "b+", label="Transient MT3DMS" - ) + mt_tr_ln = ax.plot(x_pos, y_10_mt_sln, "b+", label="Transient MT3DMS") - mf6_ss_ln = ax.plot( - x_pos, y_150_mf6_sln, "rx", label="Steady-state MF6-GWT" - ) + mf6_ss_ln = ax.plot(x_pos, y_150_mf6_sln, "rx", label="Steady-state MF6-GWT") mf6_tr_ln = ax.plot( x_pos, y_10_mf6_sln, @@ -841,56 +945,44 @@ def plot_results( ax.legend() plt.tight_layout() - # save figure - if config.plotSave: + if plot_show: + plt.show() + if plot_save: letter = chr(ord("@") + idx + 1) fpth = os.path.join( "..", "figures", "{}{}".format( "ex-" + sim_name + "-" + letter, - config.figure_ext, + ".png", ), ) fig.savefig(fpth) -# ### Function that wraps all of the steps for each MT3DMS Example 10 Problem scenario +# - + +# ### Running the example # -# 1. build_model, -# 2. write_model, -# 3. run_model, and -# 4. plot_results. +# Define and invoke a function to run the example scenario, then plot results. +# + def scenario(idx, runMT3D=False, silent=True): key = list(parameters.keys())[idx] parameter_dict = parameters[key] - - if runMT3D: - mf2k5 = build_mf2k5_flow_model(key, **parameter_dict) - else: - mf2k5 = None - + mf2k5 = build_mf2k5_flow_model(key, **parameter_dict) if runMT3D else None + mt3d = build_mt3d_transport_model(mf2k5, key, **parameter_dict) if runMT3D else None sim_mf6gwf = build_mf6_flow_model(key, **parameter_dict) - - if runMT3D: - mt3d = build_mt3d_transport_model(mf2k5, key, **parameter_dict) - else: - mt3d = None - sim_mf6gwt = build_mf6_transport_model(key, **parameter_dict) - if runMT3D: - write_mf2k5_models(mf2k5, mt3d, silent=silent) - - write_mf6_models(sim_mf6gwf, sim_mf6gwt, silent=silent) - - success = run_model( - sim_mf6gwf, sim_mf6gwt, mf2k5=mf2k5, mt3d=mt3d, silent=silent - ) - - if success: + if write: + if runMT3D: + write_mf2k5_models(mf2k5, mt3d, silent=silent) + write_mf6_models(sim_mf6gwf, sim_mf6gwt, silent=silent) + if run: + run_models(sim_mf6gwf, sim_mf6gwt, mf2k5=mf2k5, mt3d=mt3d, silent=silent) + if plot: plot_results( sim_mf6gwf, sim_mf6gwt, @@ -901,33 +993,18 @@ def scenario(idx, runMT3D=False, silent=True): ) -# nosetest - exclude block from this nosetest to the next nosetest -# def test_01(): -# scenario(0, silent=False) - - -def test_02(): - scenario(1, runMT3D=False, silent=False) - - -def test_03(): - scenario(2, runMT3D=False, silent=False) - - -# nosetest end - -if __name__ == "__main__": - # ### Two-Dimensional Transport in a Diagonal Flow Field - # - # Compares the standard finite difference solutions between MT3D MF 6 - # when the Peclet number is 0 - # Not simulated because no known analytical solution to compare to - # scenario(0, silent=False) - - # Compares the standard finite difference solutions between MT3D MF 6 - # when the Peclet number is 0 - scenario(1, silent=False) - - # Compares the standard finite difference solutions between MT3D MF 6 - # when the Peclet number is 0 - scenario(2, silent=False) +# ### Two-Dimensional Transport in a Diagonal Flow Field +# +# Compares the standard finite difference solutions between MT3D MF 6 +# when the Peclet number is 0 +# Not simulated because no known analytical solution to compare to +# scenario(0, silent=False) + +# Compares the standard finite difference solutions between MT3D MF 6 +# when the Peclet number is 0 +scenario(1, silent=False) + +# Compares the standard finite difference solutions between MT3D MF 6 +# when the Peclet number is 0 +scenario(2, silent=False) +# - diff --git a/scripts/ex-gwt-henry.py b/scripts/ex-gwt-henry.py index 676fed96a..a93c0bcca 100644 --- a/scripts/ex-gwt-henry.py +++ b/scripts/ex-gwt-henry.py @@ -1,44 +1,37 @@ # ## Henry Problem # # Classic saltwater intrusion -# -# - -# ### Henry Problem Setup - -# Imports +# ### Initial setup +# +# Import dependencies, define the example name and workspace, and read settings from environment variables. +# + import os -import sys +import pathlib as pl import flopy import matplotlib.pyplot as plt -import numpy as np - -# Append to system path to include the common subdirectory - -sys.path.append(os.path.join("..", "common")) - -# Import common functionality - -import config -from figspecs import USGSFigure +from flopy.plot.styles import styles +from modflow_devtools.misc import get_env, timed -mf6exe = "mf6" -exe_name_mf = "mf2005" -exe_name_mt = "mt3dms" +# Base workspace +workspace = pl.Path("../examples") -# Set figure properties specific to this problem +# Settings from environment variables +write = get_env("WRITE", True) +run = get_env("RUN", True) +plot = get_env("PLOT", True) +plot_show = get_env("PLOT_SHOW", True) +plot_save = get_env("PLOT_SAVE", True) +# - -figure_size = (6, 4) - -# Base simulation and model name and workspace - -ws = config.base_ws - -# Scenario parameters - make sure there is at least one blank line before next item +# ### Define parameters +# +# Define model units, parameters and other settings. +# + +# Scenario-specific parameters - make sure there is at least one blank line before next item parameters = { "ex-gwt-henry-a": { "inflow": 5.7024, @@ -50,18 +43,15 @@ # Scenario parameter units - make sure there is at least one blank line before next item # add parameter_units to add units to the scenario parameter table - parameter_units = { "inflow": "$m^3/d$", } # Model units - length_units = "centimeters" time_units = "seconds" -# Table of model parameters - +# Model parameters nper = 1 # Number of periods nstp = 500 # Number of time steps perlen = 0.5 # Simulation time length ($d$) @@ -82,23 +72,21 @@ nouter, ninner = 100, 300 hclose, rclose, relax = 1e-10, 1e-6, 0.97 +# - - -# ### Functions to build, write, run, and plot models -# -# MODFLOW 6 flopy GWF simulation object (sim) is returned +# ### Model setup # +# Define functions to build models, write input files, and run the simulation. -def build_model(sim_folder, inflow): +# + +def build_models(sim_folder, inflow): print(f"Building model...{sim_folder}") name = "flow" - sim_ws = os.path.join(ws, sim_folder) + sim_ws = os.path.join(workspace, sim_folder) sim = flopy.mf6.MFSimulation(sim_name=name, sim_ws=sim_ws, exe_name="mf6") tdis_ds = ((perlen, nstp, 1.0),) - flopy.mf6.ModflowTdis( - sim, nper=nper, perioddata=tdis_ds, time_units=time_units - ) + flopy.mf6.ModflowTdis(sim, nper=nper, perioddata=tdis_ds, time_units=time_units) gwf = flopy.mf6.ModflowGwf(sim, modelname=name, save_flows=True) ims = flopy.mf6.ModflowIms( sim, @@ -202,9 +190,7 @@ def build_model(sim_folder, inflow): gwt, budget_filerecord=f"{gwt.name}.cbc", concentration_filerecord=f"{gwt.name}.ucn", - concentrationprintrecord=[ - ("COLUMNS", 10, "WIDTH", 15, "DIGITS", 6, "GENERAL") - ], + concentrationprintrecord=[("COLUMNS", 10, "WIDTH", 15, "DIGITS", 6, "GENERAL")], saverecord=[("CONCENTRATION", "ALL")], printrecord=[("CONCENTRATION", "LAST"), ("BUDGET", "LAST")], ) @@ -214,109 +200,84 @@ def build_model(sim_folder, inflow): return sim -# Function to write model files - +def write_models(sim, silent=True): + sim.write_simulation(silent=silent) -def write_model(sim, silent=True): - if config.writeModel: - sim.write_simulation(silent=silent) - return +@timed +def run_models(sim, silent=True): + success, buff = sim.run_simulation(silent=silent) + assert success, buff -# Function to run the model -# True is returned if the model runs successfully +# - -@config.timeit -def run_model(sim, silent=True): - success = True - if config.runModel: - success = False - success, buff = sim.run_simulation(silent=silent) - if not success: - print(buff) - return success - +# ### Plotting results +# +# Define functions to plot model results. -# Function to plot the model results +# + +# Figure properties +figure_size = (6, 4) def plot_conc(sim, idx): - fs = USGSFigure(figure_type="map", verbose=False) - sim_name = list(parameters.keys())[idx] - sim_ws = os.path.join(ws, sim_name) - gwf = sim.get_model("flow") - gwt = sim.get_model("trans") - - fig = plt.figure(figsize=figure_size) - fig.tight_layout() - - # get MODFLOW 6 concentration - conc = gwt.output.concentration().get_data() - - ax = fig.add_subplot(1, 1, 1, aspect="equal") - pxs = flopy.plot.PlotCrossSection(model=gwf, ax=ax, line={"row": 0}) - pxs.plot_array(conc, cmap="jet") - levels = [35 * f for f in [0.01, 0.1, 0.5, 0.9, 0.99]] - cs = pxs.contour_array( - conc, levels=levels, colors="w", linewidths=1.0, linestyles="-" - ) - ax.set_xlabel("x position (m)") - ax.set_ylabel("z position (m)") - plt.clabel(cs, fmt="%4.2f", fontsize=5) - - # save figure - if config.plotSave: - fpth = os.path.join( - "..", "figures", f"{sim_name}-conc{config.figure_ext}" + with styles.USGSMap(): + sim_name = list(parameters.keys())[idx] + gwf = sim.get_model("flow") + gwt = sim.get_model("trans") + + fig = plt.figure(figsize=figure_size) + fig.tight_layout() + + # get MODFLOW 6 concentration + conc = gwt.output.concentration().get_data() + + ax = fig.add_subplot(1, 1, 1, aspect="equal") + pxs = flopy.plot.PlotCrossSection(model=gwf, ax=ax, line={"row": 0}) + pxs.plot_array(conc, cmap="jet") + levels = [35 * f for f in [0.01, 0.1, 0.5, 0.9, 0.99]] + cs = pxs.contour_array( + conc, levels=levels, colors="w", linewidths=1.0, linestyles="-" ) - fig.savefig(fpth) - return + ax.set_xlabel("x position (m)") + ax.set_ylabel("z position (m)") + plt.clabel(cs, fmt="%4.2f", fontsize=5) + + if plot_show: + plt.show() + if plot_save: + fpth = os.path.join("..", "figures", f"{sim_name}-conc.png") + fig.savefig(fpth) def plot_results(sim, idx): - if config.plotModel: - plot_conc(sim, idx) - return + plot_conc(sim, idx) -# Function that wraps all of the steps for each scenario -# -# 1. build_model, -# 2. write_model, -# 3. run_model, and -# 4. plot_results. +# - + +# ### Running the example # +# Define and invoke a function to run the example scenario, then plot results. +# + def scenario(idx, silent=True): key = list(parameters.keys())[idx] parameter_dict = parameters[key] - sim = build_model(key, **parameter_dict) - write_model(sim, silent=silent) - success = run_model(sim, silent=silent) - if success: + sim = build_models(key, **parameter_dict) + if write: + write_models(sim, silent=silent) + if run: + run_models(sim, silent=silent) + if plot: plot_results(sim, idx) -# nosetest - exclude block from this nosetest to the next nosetest -def test_01(): - scenario(0, silent=False) - - -def test_02(): - scenario(1, silent=False) - - -# nosetest end - -if __name__ == "__main__": - # ### Henry Problem - - # Scenario 1 - Classic henry problem - - scenario(0) - - # Scenario 2 - Modified Henry problem with half the inflow rate +# Scenario 1 - Classic henry problem +scenario(0) - scenario(1) +# Scenario 2 - Modified Henry problem with half the inflow rate +scenario(1) +# - diff --git a/scripts/ex-gwt-keating.py b/scripts/ex-gwt-keating.py index 4936f5d00..89318a52a 100644 --- a/scripts/ex-gwt-keating.py +++ b/scripts/ex-gwt-keating.py @@ -4,46 +4,46 @@ # aquifer overlying a water table aquifer. The presence of a discontinuous # low permeability lens causes the perched aquifer to form in response to # recharge. The problem also represents solute transport through the system. -# -# -# ### Keating Problem +# ### Initial setup # -# Imports +# Import dependencies, define the example name and workspace, and read settings from environment variables. +# + import os -import sys +import pathlib as pl import flopy import matplotlib.patches import matplotlib.pyplot as plt import numpy as np +import pooch +from flopy.plot.styles import styles +from modflow_devtools.misc import get_env, timed -# Append to system path to include the common subdirectory - -sys.path.append(os.path.join("..", "common")) - -# Import common functionality - -import config -from figspecs import USGSFigure - -# Set figure properties specific to the - -figure_size = (7.5, 3) - -# Base simulation and model name and workspace - -ws = config.base_ws +# Example name and base workspace example_name = "ex-gwt-keating" +workspace = pl.Path("../examples") + +# Settings from environment variables +write = get_env("WRITE", True) +run = get_env("RUN", True) +plot = get_env("PLOT", True) +plot_show = get_env("PLOT_SHOW", True) +plot_save = get_env("PLOT_SAVE", True) +gif_save = get_env("GIF", True) +# - + +# ### Define parameters +# +# Define model units, parameters and other settings. +# + # Model units - length_units = "meters" time_units = "days" -# Table of model parameters - +# Model parameters nlay = 80 # Number of layers nrow = 1 # Number of rows ncol = 400 # Number of columns @@ -97,18 +97,18 @@ rchspd = {} rchspd[0] = [[(0, 0, j), rrate, recharge_conc] for j in rcol] rchspd[1] = [[(0, 0, j), rrate, 0.0] for j in rcol] +# - -# ### Functions to build, write, run, and plot models -# -# MODFLOW 6 flopy simulation object (sim) is returned if building the model -# recharge is the only variable +# ### Model setup # +# Define functions to build models, write input files, and run the simulation. +# + def build_mf6gwf(sim_folder): print(f"Building mf6gwf model...{sim_folder}") name = "flow" - sim_ws = os.path.join(ws, sim_folder, "mf6gwf") + sim_ws = os.path.join(workspace, sim_folder, "mf6gwf") sim = flopy.mf6.MFSimulation(sim_name=name, sim_ws=sim_ws, exe_name="mf6") tdis_ds = ((period1, 1, 1.0), (period2, 1, 1.0)) flopy.mf6.ModflowTdis( @@ -192,7 +192,7 @@ def build_mf6gwf(sim_folder): def build_mf6gwt(sim_folder): print(f"Building mf6gwt model...{sim_folder}") name = "trans" - sim_ws = os.path.join(ws, sim_folder, "mf6gwt") + sim_ws = os.path.join(workspace, sim_folder, "mf6gwt") sim = flopy.mf6.MFSimulation( sim_name=name, sim_ws=sim_ws, @@ -240,9 +240,7 @@ def build_mf6gwt(sim_folder): ("GWFHEAD", f"../mf6gwf/flow.hds", None), ("GWFBUDGET", "../mf6gwf/flow.bud", None), ] - flopy.mf6.ModflowGwtfmi( - gwt, flow_imbalance_correction=True, packagedata=pd - ) + flopy.mf6.ModflowGwtfmi(gwt, flow_imbalance_correction=True, packagedata=pd) sourcerecarray = [ ("RCH-1", "AUX", "CONCENTRATION"), ] @@ -281,166 +279,149 @@ def build_mf6gwt(sim_folder): ("obs2", "CONCENTRATION", obs2), ], } - flopy.mf6.ModflowUtlobs( - gwt, digits=10, print_input=True, continuous=obs_data - ) + flopy.mf6.ModflowUtlobs(gwt, digits=10, print_input=True, continuous=obs_data) return sim -def build_model(sim_name): - sims = None - if config.buildModel: - sim_mf6gwf = build_mf6gwf(sim_name) - sim_mf6gwt = build_mf6gwt(sim_name) - sim_mf2005 = None # build_mf2005(sim_name) - sim_mt3dms = None # build_mt3dms(sim_name, sim_mf2005) - sims = (sim_mf6gwf, sim_mf6gwt, sim_mf2005, sim_mt3dms) - return sims - +def build_models(sim_name): + sim_mf6gwf = build_mf6gwf(sim_name) + sim_mf6gwt = build_mf6gwt(sim_name) + sim_mf2005 = None # build_mf2005(sim_name) + sim_mt3dms = None # build_mt3dms(sim_name, sim_mf2005) + return sim_mf6gwf, sim_mf6gwt, sim_mf2005, sim_mt3dms -# Function to write model files - -def write_model(sims, silent=True): - if config.writeModel: - sim_mf6gwf, sim_mf6gwt, sim_mf2005, sim_mt3dms = sims - sim_mf6gwf.write_simulation(silent=silent) - sim_mf6gwt.write_simulation(silent=silent) - return +def write_models(sims, silent=True): + sim_mf6gwf, sim_mf6gwt, sim_mf2005, sim_mt3dms = sims + sim_mf6gwf.write_simulation(silent=silent) + sim_mf6gwt.write_simulation(silent=silent) -# Function to run the model -# True is returned if the model runs successfully +@timed +def run_models(sims, silent=True): + sim_mf6gwf, sim_mf6gwt, sim_mf2005, sim_mt3dms = sims + success, buff = sim_mf6gwf.run_simulation(silent=silent) + assert success, buff + success, buff = sim_mf6gwt.run_simulation(silent=silent) + assert success, buff -@config.timeit -def run_model(sims, silent=True): - success = True - if config.runModel: - success = False - sim_mf6gwf, sim_mf6gwt, sim_mf2005, sim_mt3dms = sims - print("Running mf6gwf model...") - success, buff = sim_mf6gwf.run_simulation(silent=silent) - if not success: - print(buff) - print("Running mf6gwt model...") - success, buff = sim_mf6gwt.run_simulation(silent=silent) - if not success: - print(buff) - return success +# - +# ### Plotting results +# +# Define functions to plot model results. -# Function to plot the model results +# + +# Figure properties +figure_size = (7.5, 3) def plot_results(sims, idx): - if config.plotModel: - print("Plotting model results...") - plot_head_results(sims, idx) - plot_conc_results(sims, idx) - plot_cvt_results(sims, idx) - if config.plotSave and config.createGif: - make_animated_gif(sims, idx) - return + print("Plotting model results...") + plot_head_results(sims, idx) + plot_conc_results(sims, idx) + plot_cvt_results(sims, idx) + if plot_save and gif_save: + make_animated_gif(sims, idx) def plot_head_results(sims, idx): print("Plotting head model results...") - sim_mf6gwf, sim_mf6gwt, sim_mf2005, sim_mt3dms = sims + sim_mf6gwf, _, _, _ = sims gwf = sim_mf6gwf.flow botm = gwf.dis.botm.array - # gwt = sim_mf6gwt.trans - fs = USGSFigure(figure_type="map", verbose=False) - sim_ws = sim_mf6gwf.simulation_data.mfpath.get_sim_path() - head = gwf.output.head().get_data() - head = np.where(head > botm, head, np.nan) - fig, ax = plt.subplots( - 1, 1, figsize=figure_size, dpi=300, tight_layout=True - ) - pxs = flopy.plot.PlotCrossSection(model=gwf, ax=ax, line={"row": 0}) - pa = pxs.plot_array(head, head=head, cmap="jet") - pxs.plot_bc(ftype="RCH", color="red") - pxs.plot_bc(ftype="CHD") - plt.colorbar(pa, shrink=0.5) - confining_rect = matplotlib.patches.Rectangle( - (3000, 1000), 3000, 100, color="gray", alpha=0.5 - ) - ax.add_patch(confining_rect) - ax.set_xlabel("x position (m)") - ax.set_ylabel("elevation (m)") - ax.set_aspect(plotaspect) - - # save figure - if config.plotSave: - sim_folder = os.path.split(sim_ws)[0] - sim_folder = os.path.basename(sim_folder) - fname = f"{sim_folder}-head{config.figure_ext}" - fpth = os.path.join(ws, "..", "figures", fname) - fig.savefig(fpth) - -def plot_conc_results(sims, idx): - print("Plotting conc model results...") - sim_mf6gwf, sim_mf6gwt, sim_mf2005, sim_mt3dms = sims - gwf = sim_mf6gwf.flow - gwt = sim_mf6gwt.trans - botm = gwf.dis.botm.array - fs = USGSFigure(figure_type="map", verbose=False) - head = gwf.output.head().get_data() - head = np.where(head > botm, head, np.nan) - sim_ws = sim_mf6gwt.simulation_data.mfpath.get_sim_path() - cobj = gwt.output.concentration() - conc_times = cobj.get_times() - conc_times = np.array(conc_times) - fig, axes = plt.subplots( - 3, 1, figsize=(7.5, 4.5), dpi=300, tight_layout=True - ) - xgrid, _, zgrid = gwt.modelgrid.xyzcellcenters - # Desired plot times - plot_times = [100.0, 1000.0, 3000.0] - nplots = len(plot_times) - for iplot in range(nplots): - print(f" Plotting conc {iplot + 1}") - time_in_pub = plot_times[iplot] - idx_conc = (np.abs(conc_times - time_in_pub)).argmin() - totim = conc_times[idx_conc] - ax = axes[iplot] + with styles.USGSMap() as fs: + sim_ws = sim_mf6gwf.simulation_data.mfpath.get_sim_path() + head = gwf.output.head().get_data() + head = np.where(head > botm, head, np.nan) + fig, ax = plt.subplots(1, 1, figsize=figure_size, dpi=300, tight_layout=True) pxs = flopy.plot.PlotCrossSection(model=gwf, ax=ax, line={"row": 0}) - conc = cobj.get_data(totim=totim) - conc = np.where(head > botm, conc, np.nan) - pa = pxs.plot_array(conc, head=head, cmap="jet", vmin=0, vmax=1.0) + pa = pxs.plot_array(head, head=head, cmap="jet") pxs.plot_bc(ftype="RCH", color="red") pxs.plot_bc(ftype="CHD") + plt.colorbar(pa, shrink=0.5) confining_rect = matplotlib.patches.Rectangle( (3000, 1000), 3000, 100, color="gray", alpha=0.5 ) ax.add_patch(confining_rect) - if iplot == 2: - ax.set_xlabel("x position (m)") + ax.set_xlabel("x position (m)") ax.set_ylabel("elevation (m)") - title = f"Time = {totim}" - letter = chr(ord("@") + iplot + 1) - fs.heading(letter=letter, heading=title, ax=ax) ax.set_aspect(plotaspect) - for k, i, j in [obs1, obs2]: - x = xgrid[i, j] - z = zgrid[k, i, j] - ax.plot( - x, - z, - markerfacecolor="yellow", - markeredgecolor="black", - marker="o", - markersize="4", + if plot_show: + plt.show() + if plot_save: + sim_folder = os.path.split(sim_ws)[0] + sim_folder = os.path.basename(sim_folder) + fname = f"{sim_folder}-head.png" + fpth = os.path.join(workspace, "..", "figures", fname) + fig.savefig(fpth) + + +def plot_conc_results(sims, idx): + print("Plotting conc model results...") + sim_mf6gwf, sim_mf6gwt, _, _ = sims + gwf = sim_mf6gwf.flow + gwt = sim_mf6gwt.trans + botm = gwf.dis.botm.array + + with styles.USGSMap() as fs: + head = gwf.output.head().get_data() + head = np.where(head > botm, head, np.nan) + sim_ws = sim_mf6gwt.simulation_data.mfpath.get_sim_path() + cobj = gwt.output.concentration() + conc_times = cobj.get_times() + conc_times = np.array(conc_times) + fig, axes = plt.subplots(3, 1, figsize=(7.5, 4.5), dpi=300, tight_layout=True) + xgrid, _, zgrid = gwt.modelgrid.xyzcellcenters + # Desired plot times + plot_times = [100.0, 1000.0, 3000.0] + nplots = len(plot_times) + for iplot in range(nplots): + print(f" Plotting conc {iplot + 1}") + time_in_pub = plot_times[iplot] + idx_conc = (np.abs(conc_times - time_in_pub)).argmin() + totim = conc_times[idx_conc] + ax = axes[iplot] + pxs = flopy.plot.PlotCrossSection(model=gwf, ax=ax, line={"row": 0}) + conc = cobj.get_data(totim=totim) + conc = np.where(head > botm, conc, np.nan) + pa = pxs.plot_array(conc, head=head, cmap="jet", vmin=0, vmax=1.0) + pxs.plot_bc(ftype="RCH", color="red") + pxs.plot_bc(ftype="CHD") + confining_rect = matplotlib.patches.Rectangle( + (3000, 1000), 3000, 100, color="gray", alpha=0.5 ) - # save figure - if config.plotSave: - sim_folder = os.path.split(sim_ws)[0] - sim_folder = os.path.basename(sim_folder) - fname = f"{sim_folder}-conc{config.figure_ext}" - fpth = os.path.join(ws, "..", "figures", fname) - fig.savefig(fpth) + ax.add_patch(confining_rect) + if iplot == 2: + ax.set_xlabel("x position (m)") + ax.set_ylabel("elevation (m)") + title = f"Time = {totim}" + letter = chr(ord("@") + iplot + 1) + styles.heading(letter=letter, heading=title, ax=ax) + ax.set_aspect(plotaspect) + + for k, i, j in [obs1, obs2]: + x = xgrid[i, j] + z = zgrid[k, i, j] + ax.plot( + x, + z, + markerfacecolor="yellow", + markeredgecolor="black", + marker="o", + markersize="4", + ) + if plot_show: + plt.show() + if plot_save: + sim_folder = os.path.split(sim_ws)[0] + sim_folder = os.path.basename(sim_folder) + fname = f"{sim_folder}-conc.png" + fpth = os.path.join(workspace, "..", "figures", fname) + fig.savefig(fpth) def make_animated_gif(sims, idx): @@ -451,156 +432,152 @@ def make_animated_gif(sims, idx): print("Animating conc model results...") sim_name = example_name - sim_mf6gwf, sim_mf6gwt, sim_mf2005, sim_mt3dms = sims + sim_mf6gwf, sim_mf6gwt, _, _ = sims gwf = sim_mf6gwf.flow gwt = sim_mf6gwt.trans botm = gwf.dis.botm.array # load head - fs = USGSFigure(figure_type="map", verbose=False) - head = gwf.output.head().get_data() - head = np.where(head > botm, head, np.nan) - - # load concentration - cobj = gwt.output.concentration() - conc_times = cobj.get_times() - conc_times = np.array(conc_times) - conc = cobj.get_alldata() - - # set up the figure - fig = plt.figure(figsize=(7.5, 3)) - ax = fig.add_subplot(1, 1, 1) - pxs = flopy.plot.PlotCrossSection( - model=gwf, - ax=ax, - line={"row": 0}, - extent=(0, 10000, 0, 2000), - ) + with styles.USGSMap() as fs: + head = gwf.output.head().get_data() + head = np.where(head > botm, head, np.nan) + + # load concentration + cobj = gwt.output.concentration() + conc_times = cobj.get_times() + conc_times = np.array(conc_times) + conc = cobj.get_alldata() + + # set up the figure + fig = plt.figure(figsize=(7.5, 3)) + ax = fig.add_subplot(1, 1, 1) + pxs = flopy.plot.PlotCrossSection( + model=gwf, + ax=ax, + line={"row": 0}, + extent=(0, 10000, 0, 2000), + ) - cmap = copy.copy(mpl.cm.get_cmap("jet")) - cmap.set_bad("white") - nodata = -999.0 - a = np.where(head > botm, conc[0], nodata) - a = np.ma.masked_where(a < 0, a) - pc = pxs.plot_array(a, head=head, cmap=cmap, vmin=0, vmax=1) - pxs.plot_bc(ftype="RCH", color="red") - pxs.plot_bc(ftype="CHD") + cmap = copy.copy(mpl.cm.get_cmap("jet")) + cmap.set_bad("white") + nodata = -999.0 + a = np.where(head > botm, conc[0], nodata) + a = np.ma.masked_where(a < 0, a) + pc = pxs.plot_array(a, head=head, cmap=cmap, vmin=0, vmax=1) + pxs.plot_bc(ftype="RCH", color="red") + pxs.plot_bc(ftype="CHD") - def init(): - ax.set_title(f"Time = {conc_times[0]} days") + def init(): + ax.set_title(f"Time = {conc_times[0]} days") - def update(i): - a = np.where(head > botm, conc[i], nodata) - a = np.ma.masked_where(a < 0, a) - a = a[~a.mask] - pc.set_array(a.flatten()) - ax.set_title(f"Time = {conc_times[i]} days") + def update(i): + a = np.where(head > botm, conc[i], nodata) + a = np.ma.masked_where(a < 0, a) + a = a[~a.mask] + pc.set_array(a.flatten()) + ax.set_title(f"Time = {conc_times[i]} days") - # Stop the animation at 18,000 days - idx_end = (np.abs(conc_times - 18000.0)).argmin() - ani = FuncAnimation(fig, update, range(1, idx_end), init_func=init) - writer = PillowWriter(fps=25) - fpth = os.path.join("..", "figures", "{}{}".format(sim_name, ".gif")) - ani.save(fpth, writer=writer) - return + # Stop the animation at 18,000 days + idx_end = (np.abs(conc_times - 18000.0)).argmin() + ani = FuncAnimation(fig, update, range(1, idx_end), init_func=init) + writer = PillowWriter(fps=25) + fpth = os.path.join("..", "figures", "{}{}".format(sim_name, ".gif")) + ani.save(fpth, writer=writer) def plot_cvt_results(sims, idx): print("Plotting cvt model results...") - sim_mf6gwf, sim_mf6gwt, sim_mf2005, sim_mt3dms = sims - gwf = sim_mf6gwf.flow + sim_mf6gwf, sim_mf6gwt, _, _ = sims gwt = sim_mf6gwt.trans - botm = gwf.dis.botm.array - fs = USGSFigure(figure_type="map", verbose=False) - sim_ws = sim_mf6gwt.simulation_data.mfpath.get_sim_path() - mf6gwt_ra = gwt.obs.output.obs().data - dt = [("totim", "f8"), ("obs", "f8")] - fname = os.path.join(config.data_ws, "ex-gwt-keating", "keating_obs1.csv") - obs1ra = np.genfromtxt(fname, delimiter=",", deletechars="", dtype=dt) - fname = os.path.join(config.data_ws, "ex-gwt-keating", "keating_obs2.csv") - obs2ra = np.genfromtxt(fname, delimiter=",", deletechars="", dtype=dt) - fig, axes = plt.subplots(2, 1, figsize=(6, 4), dpi=300, tight_layout=True) - ax = axes[0] - ax.plot( - mf6gwt_ra["totim"], - mf6gwt_ra["OBS1"], - "b-", - alpha=1.0, - label="MODFLOW 6", - ) - ax.plot( - obs1ra["totim"], - obs1ra["obs"], - markerfacecolor="None", - markeredgecolor="k", - marker="o", - markersize="4", - linestyle="None", - label="Keating and Zyvolosky (2009)", - ) - ax.set_xlim(0, 8000) - ax.set_ylim(0, 0.80) - ax.set_xlabel("time, in days") - ax.set_ylabel("normalized concentration, unitless") - fs.graph_legend(ax) - ax = axes[1] - ax.plot( - mf6gwt_ra["totim"], - mf6gwt_ra["OBS2"], - "b-", - alpha=1.0, - label="MODFLOW 6", - ) - ax.plot( - obs2ra["totim"], - obs2ra["obs"], - markerfacecolor="None", - markeredgecolor="k", - marker="o", - markersize="4", - linestyle="None", - label="Keating and Zyvolosky (2009)", - ) - ax.set_xlim(0, 30000) - ax.set_ylim(0, 0.20) - ax.set_xlabel("time, in days") - ax.set_ylabel("normalized concentration, unitless") - fs.graph_legend(ax) - # save figure - if config.plotSave: - sim_folder = os.path.split(sim_ws)[0] - sim_folder = os.path.basename(sim_folder) - fname = f"{sim_folder}-cvt{config.figure_ext}" - fpth = os.path.join(ws, "..", "figures", fname) - fig.savefig(fpth) - - -# Function that wraps all of the steps for each scenario + + with styles.USGSMap(): + sim_ws = sim_mf6gwt.simulation_data.mfpath.get_sim_path() + mf6gwt_ra = gwt.obs.output.obs().data + dt = [("totim", "f8"), ("obs", "f8")] + fname = pooch.retrieve( + url=f"https://github.com/MODFLOW-USGS/modflow6-examples/raw/master/data/ex-gwt-keating/keating_obs1.csv", + known_hash="md5:174c5548c3bbb9ea4ebc8b5a33ea2851", + ) + obs1ra = np.genfromtxt(fname, delimiter=",", deletechars="", dtype=dt) + fpth = pooch.retrieve( + url=f"https://github.com/MODFLOW-USGS/modflow6-examples/raw/master/data/ex-gwt-keating/keating_obs2.csv", + known_hash="md5:8de2ef529a2537ecd6c62bc207b67fb5", + ) + obs2ra = np.genfromtxt(fname, delimiter=",", deletechars="", dtype=dt) + fig, axes = plt.subplots(2, 1, figsize=(6, 4), dpi=300, tight_layout=True) + ax = axes[0] + ax.plot( + mf6gwt_ra["totim"], + mf6gwt_ra["OBS1"], + "b-", + alpha=1.0, + label="MODFLOW 6", + ) + ax.plot( + obs1ra["totim"], + obs1ra["obs"], + markerfacecolor="None", + markeredgecolor="k", + marker="o", + markersize="4", + linestyle="None", + label="Keating and Zyvolosky (2009)", + ) + ax.set_xlim(0, 8000) + ax.set_ylim(0, 0.80) + ax.set_xlabel("time, in days") + ax.set_ylabel("normalized concentration, unitless") + styles.graph_legend(ax) + ax = axes[1] + ax.plot( + mf6gwt_ra["totim"], + mf6gwt_ra["OBS2"], + "b-", + alpha=1.0, + label="MODFLOW 6", + ) + ax.plot( + obs2ra["totim"], + obs2ra["obs"], + markerfacecolor="None", + markeredgecolor="k", + marker="o", + markersize="4", + linestyle="None", + label="Keating and Zyvolosky (2009)", + ) + ax.set_xlim(0, 30000) + ax.set_ylim(0, 0.20) + ax.set_xlabel("time, in days") + ax.set_ylabel("normalized concentration, unitless") + styles.graph_legend(ax) + if plot_show: + plt.show() + if plot_save: + sim_folder = os.path.split(sim_ws)[0] + sim_folder = os.path.basename(sim_folder) + fname = f"{sim_folder}-cvt.png" + fpth = os.path.join(workspace, "..", "figures", fname) + fig.savefig(fpth) + + +# - + +# ### Running the example # -# 1. build_model, -# 2. write_model, -# 3. run_model, and -# 4. plot_results. +# Define and invoke a function to run the example scenario, then plot results. +# + def scenario(idx, silent=True): - sim = build_model(example_name) - write_model(sim, silent=silent) - success = run_model(sim, silent=silent) - if success: + sim = build_models(example_name) + if write: + write_models(sim, silent=silent) + if run: + run_models(sim, silent=silent) + if plot: plot_results(sim, idx) -# nosetest - exclude block from this nosetest to the next nosetest -def test_01(): - scenario(0, silent=False) - - -# nosetest end - -if __name__ == "__main__": - # ### Simulate Keating Problem - - # Plot showing MODFLOW 6 results - - scenario(0) +scenario(0) +# - diff --git a/scripts/ex-gwt-moc3d-p01.py b/scripts/ex-gwt-moc3d-p01.py index ca2e53b50..28dab4a16 100644 --- a/scripts/ex-gwt-moc3d-p01.py +++ b/scripts/ex-gwt-moc3d-p01.py @@ -1,46 +1,42 @@ -# ## One-Dimensional Steady Flow with Transport +# ## One-dimensional steady flow with transport # -# MOC3D Problem 1 -# -# - +# This problem corresponds to the first problem presented in the MOC3D +# report Konikow 1996, involving the transport of a dissolved constituent +# in a steady, one-dimensional flow field. An analytical solution for this +# problem is given by \cite{wexler1992}. This example is simulated with the GWT Model in \mf, which receives flow information from a separate simulation with the GWF Model in \mf. Results from the GWT Model are compared with the results from the \cite{wexler1992} analytical solution. -# ### One-Dimensional Steady Flow with Transport Problem Setup - -# Imports +# ### Initial setup +# +# Import dependencies, define the example name and workspace, and read settings from environment variables. +# + import os -import sys +import pathlib as pl import flopy import matplotlib.pyplot as plt import numpy as np +from flopy.plot.styles import styles +from modflow_devtools.misc import get_env, timed -# Append to system path to include the common subdirectory - -sys.path.append(os.path.join("..", "common")) - -# Import common functionality - -import analytical -import config -from figspecs import USGSFigure - -mf6exe = "mf6" -exe_name_mf = "mf2005" -exe_name_mt = "mt3dms" - -# Set figure properties specific to this problem - -figure_size = (5, 3) - -# Base simulation and model name and workspace - -ws = config.base_ws +# Example name and base workspace +workspace = pl.Path("../examples") example_name = "ex-gwt-moc3dp1" -# Scenario parameters - make sure there is at least one blank line before next item +# Settings from environment variables +write = get_env("WRITE", True) +run = get_env("RUN", True) +plot = get_env("PLOT", True) +plot_show = get_env("PLOT_SHOW", True) +plot_save = get_env("PLOT_SAVE", True) +# - + +# ### Define parameters +# +# Define model units, parameters and other settings. +# + +# Scenario-specific parameters - make sure there is at least one blank line before next item parameters = { "ex-gwt-moc3d-p01a": { "longitudinal_dispersivity": 0.1, @@ -66,7 +62,6 @@ # Scenario parameter units - make sure there is at least one blank line before next item # add parameter_units to add units to the scenario parameter table - parameter_units = { "longitudinal_dispersivity": "$cm$", "retardation_factor": "unitless", @@ -74,12 +69,10 @@ } # Model units - length_units = "centimeters" time_units = "seconds" -# Table of model parameters - +# Model parameters nper = 1 # Number of periods nlay = 1 # Number of layers nrow = 1 # Number of rows @@ -95,11 +88,161 @@ total_time = 120.0 # Simulation time ($s$) source_concentration = 1.0 # Source concentration (unitless) initial_concentration = 0.0 # Initial concentration (unitless) +# - -# ### Functions to build, write, run, and plot models -# -# MODFLOW 6 flopy GWF simulation object (sim) is returned +# ### Model setup # +# Define functions to build models, write input files, and run the simulation. + + +# + +class Wexler1d: + """ + Analytical solution for 1D transport with inflow at a concentration of 1. + at x=0 and a third-type bound at location l. + Wexler Page 17 and Van Genuchten and Alves pages 66-67 + """ + + def betaeqn(self, beta, d, v, l): + return beta / np.tan(beta) - beta**2 * d / v / l + v * l / 4.0 / d + + def fprimebetaeqn(self, beta, d, v, l): + """ + f1 = cotx - x/sinx2 - (2.0D0*C*x) + + """ + c = v * l / 4.0 / d + return 1.0 / np.tan(beta) - beta / np.sin(beta) ** 2 - 2.0 * c * beta + + def fprime2betaeqn(self, beta, d, v, l): + """ + f2 = -1.0D0/sinx2 - (sinx2-x*DSIN(x*2.0D0))/(sinx2*sinx2) - 2.0D0*C + + """ + c = v * l / 4.0 / d + sinx2 = np.sin(beta) ** 2 + return ( + -1.0 / sinx2 + - (sinx2 - beta * np.sin(beta * 2.0)) / (sinx2 * sinx2) + - 2.0 * c + ) + + def solvebetaeqn(self, beta, d, v, l, xtol=1.0e-12): + from scipy.optimize import fsolve + + t = fsolve( + self.betaeqn, + beta, + args=(d, v, l), + fprime=self.fprime2betaeqn, + xtol=xtol, + full_output=True, + ) + result = t[0][0] + infod = t[1] + isoln = t[2] + msg = t[3] + if abs(result - beta) > np.pi: + raise Exception("Error in beta solution") + err = self.betaeqn(result, d, v, l) + fvec = infod["fvec"][0] + if isoln != 1: + print("Error in beta solve", err, result, d, v, l, msg) + return result + + def root3(self, d, v, l, nval=1000): + b = 0.5 * np.pi + betalist = [] + for i in range(nval): + b = self.solvebetaeqn(b, d, v, l) + err = self.betaeqn(b, d, v, l) + betalist.append(b) + b += np.pi + return betalist + + def analytical(self, x, t, v, l, d, tol=1.0e-20, nval=5000): + sigma = 0.0 + betalist = self.root3(d, v, l, nval=nval) + for i, bi in enumerate(betalist): + denom = bi**2 + (v * l / 2.0 / d) ** 2 + v * l / d + x1 = ( + bi + * (bi * np.cos(bi * x / l) + v * l / 2.0 / d * np.sin(bi * x / l)) + / denom + ) + + denom = bi**2 + (v * l / 2.0 / d) ** 2 + x2 = np.exp(-1 * bi**2 * d * t / l**2) / denom + + sigma += x1 * x2 + term1 = 2.0 * v * l / d * np.exp(v * x / 2.0 / d - v**2 * t / 4.0 / d) + conc = 1.0 - term1 * sigma + if i > 0: + diff = abs(conc - concold) + if np.all(diff < tol): + break + concold = conc + return conc + + def analytical2(self, x, t, v, l, d, e=0.0, tol=1.0e-20, nval=5000): + """ + Calculate the analytical solution for one-dimension advection and + dispersion using the solution of Lapidus and Amundson (1952) and + Ogata and Banks (1961) + + Parameters + ---------- + x : float or ndarray + x position + t : float or ndarray + time + v : float or ndarray + velocity + l : float + length domain + d : float + dispersion coefficient + e : float + decay rate + + Returns + ------- + result : float or ndarray + normalized concentration value + + """ + u = v**2 + 4.0 * e * d + u = np.sqrt(u) + sigma = 0.0 + denom = (u + v) / 2.0 / v - (u - v) ** 2.0 / 2.0 / v / (u + v) * np.exp( + -u * l / d + ) + term1 = np.exp((v - u) * x / 2.0 / d) + (u - v) / (u + v) * np.exp( + (v + u) * x / 2.0 / d - u * l / d + ) + term1 = term1 / denom + term2 = 2.0 * v * l / d * np.exp(v * x / 2.0 / d - v**2 * t / 4.0 / d - e * t) + betalist = self.root3(d, v, l, nval=nval) + for i, bi in enumerate(betalist): + denom = bi**2 + (v * l / 2.0 / d) ** 2 + v * l / d + x1 = ( + bi + * (bi * np.cos(bi * x / l) + v * l / 2.0 / d * np.sin(bi * x / l)) + / denom + ) + + denom = bi**2 + (v * l / 2.0 / d) ** 2 + e * l**2 / d + x2 = np.exp(-1 * bi**2 * d * t / l**2) / denom + + sigma += x1 * x2 + + conc = term1 - term2 * sigma + if i > 0: + diff = abs(conc - concold) + if np.all(diff < tol): + break + concold = conc + return conc def get_sorption_dict(retardation_factor): @@ -138,12 +281,10 @@ def get_decay_dict(decay_rate, sorption=False): def build_mf6gwf(sim_folder): print(f"Building mf6gwf model...{sim_folder}") name = "flow" - sim_ws = os.path.join(ws, sim_folder, "mf6gwf") + sim_ws = os.path.join(workspace, sim_folder, "mf6gwf") sim = flopy.mf6.MFSimulation(sim_name=name, sim_ws=sim_ws, exe_name="mf6") tdis_ds = ((total_time, 1, 1.0),) - flopy.mf6.ModflowTdis( - sim, nper=nper, perioddata=tdis_ds, time_units=time_units - ) + flopy.mf6.ModflowTdis(sim, nper=nper, perioddata=tdis_ds, time_units=time_units) flopy.mf6.ModflowIms(sim) gwf = flopy.mf6.ModflowGwf(sim, modelname=name, save_flows=True) flopy.mf6.ModflowGwfdis( @@ -192,20 +333,13 @@ def build_mf6gwf(sim_folder): return sim -# MODFLOW 6 flopy GWF simulation object (sim) is returned - - -def build_mf6gwt( - sim_folder, longitudinal_dispersivity, retardation_factor, decay_rate -): +def build_mf6gwt(sim_folder, longitudinal_dispersivity, retardation_factor, decay_rate): print(f"Building mf6gwt model...{sim_folder}") name = "trans" - sim_ws = os.path.join(ws, sim_folder, "mf6gwt") + sim_ws = os.path.join(workspace, sim_folder, "mf6gwt") sim = flopy.mf6.MFSimulation(sim_name=name, sim_ws=sim_ws, exe_name="mf6") tdis_ds = ((total_time, 240, 1.0),) - flopy.mf6.ModflowTdis( - sim, nper=nper, perioddata=tdis_ds, time_units=time_units - ) + flopy.mf6.ModflowTdis(sim, nper=nper, perioddata=tdis_ds, time_units=time_units) flopy.mf6.ModflowIms(sim, linear_acceleration="bicgstab") gwt = flopy.mf6.ModflowGwt(sim, modelname=name, save_flows=True) flopy.mf6.ModflowGwtdis( @@ -261,65 +395,48 @@ def build_mf6gwt( return sim -def build_model( - sim_name, longitudinal_dispersivity, retardation_factor, decay_rate -): - sims = None - if config.buildModel: - sim_mf6gwf = build_mf6gwf(sim_name) - sim_mf6gwt = build_mf6gwt( - sim_name, longitudinal_dispersivity, retardation_factor, decay_rate - ) - sims = (sim_mf6gwf, sim_mf6gwt) - return sims - - -# Function to write model files +def build_models(sim_name, longitudinal_dispersivity, retardation_factor, decay_rate): + sim_mf6gwf = build_mf6gwf(sim_name) + sim_mf6gwt = build_mf6gwt( + sim_name, longitudinal_dispersivity, retardation_factor, decay_rate + ) + return sim_mf6gwf, sim_mf6gwt -def write_model(sims, silent=True): - if config.writeModel: - sim_mf6gwf, sim_mf6gwt = sims - sim_mf6gwf.write_simulation(silent=silent) - sim_mf6gwt.write_simulation(silent=silent) - return +def write_models(sims, silent=True): + sim_mf6gwf, sim_mf6gwt = sims + sim_mf6gwf.write_simulation(silent=silent) + sim_mf6gwt.write_simulation(silent=silent) -# Function to run the model -# True is returned if the model runs successfully +@timed +def run_models(sims, silent=True): + sim_mf6gwf, sim_mf6gwt = sims + success, buff = sim_mf6gwf.run_simulation(silent=silent) + assert success, buff + success, buff = sim_mf6gwt.run_simulation(silent=silent) + assert success, buff -@config.timeit -def run_model(sims, silent=True): - success = True - if config.runModel: - success = False - sim_mf6gwf, sim_mf6gwt = sims - success, buff = sim_mf6gwf.run_simulation(silent=silent) - if not success: - print(buff) - success, buff = sim_mf6gwt.run_simulation(silent=silent) - if not success: - print(buff) - return success +# - +# ### Plotting results +# +# Define functions to plot model results. -# Function to plot the model results +# + +# Figure properties +figure_size = (5, 3) def plot_results_ct( sims, idx, longitudinal_dispersivity, retardation_factor, decay_rate ): - if config.plotModel: - print("Plotting C versus t model results...") - sim_mf6gwf, sim_mf6gwt = sims - fs = USGSFigure(figure_type="graph", verbose=False) - + _, sim_mf6gwt = sims + with styles.USGSPlot(): sim_ws = sim_mf6gwt.simulation_data.mfpath.get_sim_path() mf6gwt_ra = sim_mf6gwt.get_model("trans").obs.output.obs().data - fig, axs = plt.subplots( - 1, 1, figsize=figure_size, dpi=300, tight_layout=True - ) + fig, axs = plt.subplots(1, 1, figsize=figure_size, dpi=300, tight_layout=True) alabel = ["ANALYTICAL", "", ""] mlabel = ["MODFLOW 6", "", ""] iskip = 5 @@ -330,7 +447,7 @@ def plot_results_ct( longitudinal_dispersivity * specific_discharge / retardation_factor ) for i, x in enumerate([0.05, 4.05, 11.05]): - a1 = analytical.Wexler1d().analytical2( + a1 = Wexler1d().analytical2( x, atimes, specific_discharge / retardation_factor, @@ -348,9 +465,7 @@ def plot_results_ct( idx_filter = atimes > 79 elif idx > 0: idx_filter = atimes > 0 - axs.plot( - atimes[idx_filter], a1[idx_filter], color="k", label=alabel[i] - ) + axs.plot(atimes[idx_filter], a1[idx_filter], color="k", label=alabel[i]) axs.plot( simtimes[::iskip], mf6gwt_ra[obsnames[i]][::iskip], @@ -371,28 +486,24 @@ def plot_results_ct( axs.text(100, 0.5, "x=11.05") axs.legend() - # save figure - if config.plotSave: + if plot_show: + plt.show() + if plot_save: sim_folder = os.path.split(sim_ws)[0] sim_folder = os.path.basename(sim_folder) - fname = f"{sim_folder}-ct{config.figure_ext}" - fpth = os.path.join(ws, "..", "figures", fname) + fname = f"{sim_folder}-ct.png" + fpth = os.path.join(workspace, "..", "figures", fname) fig.savefig(fpth) def plot_results_cd( sims, idx, longitudinal_dispersivity, retardation_factor, decay_rate ): - if config.plotModel: - print("Plotting C versus x model results...") - sim_mf6gwf, sim_mf6gwt = sims - fs = USGSFigure(figure_type="graph", verbose=False) - + _, sim_mf6gwt = sims + with styles.USGSPlot(): ucnobj_mf6 = sim_mf6gwt.trans.output.concentration() - fig, axs = plt.subplots( - 1, 1, figsize=figure_size, dpi=300, tight_layout=True - ) + fig, axs = plt.subplots(1, 1, figsize=figure_size, dpi=300, tight_layout=True) alabel = ["ANALYTICAL", "", ""] mlabel = ["MODFLOW 6", "", ""] iskip = 5 @@ -403,7 +514,7 @@ def plot_results_cd( ) for i, t in enumerate(ctimes): - a1 = analytical.Wexler1d().analytical2( + a1 = Wexler1d().analytical2( x, t, specific_discharge / retardation_factor, @@ -440,70 +551,52 @@ def plot_results_cd( axs.set_ylabel("Normalized Concentration (unitless)") plt.legend() - # save figure - if config.plotSave: + if plot_show: + plt.show() + if plot_save: sim_ws = sim_mf6gwt.simulation_data.mfpath.get_sim_path() sim_folder = os.path.split(sim_ws)[0] sim_folder = os.path.basename(sim_folder) - fname = f"{sim_folder}-cd{config.figure_ext}" - fpth = os.path.join(ws, "..", "figures", fname) + fname = f"{sim_folder}-cd.png" + fpth = os.path.join(workspace, "..", "figures", fname) fig.savefig(fpth) -# Function that wraps all of the steps for each scenario -# -# 1. build_model, -# 2. write_model, -# 3. run_model, and -# 4. plot_results. +# - + +# ### Running the example # +# Define and invoke a function to run the example scenario, then plot results. +# + def scenario(idx, silent=True): key = list(parameters.keys())[idx] parameter_dict = parameters[key] - sims = build_model(key, **parameter_dict) - write_model(sims, silent=silent) - success = run_model(sims, silent=silent) - if success: + sims = build_models(key, **parameter_dict) + if write: + write_models(sims, silent=silent) + if run: + run_models(sims, silent=silent) + if plot: plot_results_ct(sims, idx, **parameter_dict) plot_results_cd(sims, idx, **parameter_dict) -# nosetest - exclude block from this nosetest to the next nosetest -def test_01(): - scenario(0, silent=False) - - -def test_02(): - scenario(1, silent=False) - - -def test_03(): - scenario(2, silent=False) - - -def test_04(): - scenario(3, silent=False) - - -# nosetest end - -if __name__ == "__main__": - # ### Simulated Zero-Order Growth in a Uniform Flow Field - - # Scenario 1 - description - - scenario(0) - - # Scenario 2 - description - - scenario(1) +# - - # Scenario 3 - description +# + +scenario(0) +# - - scenario(2) +# + +scenario(1) +# - - # Scenario 4 - description +# + +scenario(2) +# - - scenario(3) +# + +scenario(3) +# - diff --git a/scripts/ex-gwt-moc3d-p02.py b/scripts/ex-gwt-moc3d-p02.py index b883104e1..1b3da8f39 100644 --- a/scripts/ex-gwt-moc3d-p02.py +++ b/scripts/ex-gwt-moc3d-p02.py @@ -1,51 +1,49 @@ -# ## Three-Dimensional Steady Flow with Transport +# ## Three-dimensional steady flow with transport # -# MOC3D Problem 2 +# This problem corresponds to the second problem presented in the MOC3D report +# Konikow 1996, which involves the transport of a dissolved constituent in a +# steady, three-dimensional flow field. An analytical solution for this problem +# is given by Wexler 1992. This example is simulated with a GWT model, which +# receives flow information from a separate GWF model. Results from the GWT +# model are compared with results from the Wexler 1992 analytical solution. + +# ### Initial setup # -# - - -# ### Three-Dimensional Steady Flow with Transport Problem Setup - -# Imports +# Import dependencies, define the example name and workspace, and read settings from environment variables. +# + import os -import sys +import pathlib as pl import flopy import matplotlib.pyplot as plt import numpy as np +from flopy.plot.styles import styles +from modflow_devtools.misc import get_env, timed +from scipy.special import erfc -# Append to system path to include the common subdirectory - -sys.path.append(os.path.join("..", "common")) - -# Import common functionality - -import analytical -import config -from figspecs import USGSFigure - -mf6exe = "mf6" -exe_name_mf = "mf2005" -exe_name_mt = "mt3dms" - -# Set figure properties specific to this problem - -figure_size = (6, 4) +# Example name and base workspace +example_name = "ex-gwt-moc3d-p02" +workspace = pl.Path("../examples") -# Base simulation and model name and workspace +# Settings from environment variables +write = get_env("WRITE", True) +run = get_env("RUN", True) +plot = get_env("PLOT", True) +plot_show = get_env("PLOT_SHOW", True) +plot_save = get_env("PLOT_SAVE", True) +# - -ws = config.base_ws -example_name = "ex-gwt-moc3d-p02" +# ### Define parameters +# +# Define model units, parameters and other settings. +# + # Model units - length_units = "meters" time_units = "days" -# Table of model parameters - +# Model parameters nper = 1 # Number of periods nlay = 40 # Number of layers nrow = 12 # Number of rows @@ -68,22 +66,69 @@ botm = [-(k + 1) * delv for k in range(nlay)] specific_discharge = velocity_x * porosity source_location0 = tuple([idx - 1 for idx in source_location]) +# - -# ### Functions to build, write, run, and plot models -# -# MODFLOW 6 flopy GWF simulation object (sim) is returned +# ### Model setup # +# Define functions to build models, write input files, and run the simulation. + + +# + +class Wexler3d: + """ + Analytical solution for 3D transport with inflow at a well with a + specified concentration. + Wexler Page 47 + """ + + def calcgamma(self, x, y, z, xc, yc, zc, dx, dy, dz): + gam = np.sqrt((x - xc) ** 2 + dx / dy * (y - yc) ** 2 + dx / dz * (z - zc) ** 2) + return gam + + def calcbeta(self, v, dx, gam, lam): + beta = np.sqrt(v**2 + 4.0 * dx * gam * lam) + return beta + + def analytical(self, x, y, z, t, v, xc, yc, zc, dx, dy, dz, n, q, lam=0.0, c0=1.0): + gam = self.calcgamma(x, y, z, xc, yc, zc, dx, dy, dz) + beta = self.calcbeta(v, dx, gam, lam) + term1 = ( + c0 + * q + * np.exp(v * (x - xc) / 2.0 / dx) + / 8.0 + / n + / np.pi + / gam + / np.sqrt(dy * dz) + ) + term2 = np.exp(gam * beta / 2.0 / dx) * erfc( + (gam + beta * t) / 2.0 / np.sqrt(dx * t) + ) + term3 = np.exp(-gam * beta / 2.0 / dx) * erfc( + (gam - beta * t) / 2.0 / np.sqrt(dx * t) + ) + return term1 * (term2 + term3) + + def multiwell(self, x, y, z, t, v, xc, yc, zc, dx, dy, dz, n, ql, lam=0.0, c0=1.0): + shape = self.analytical( + x, y, z, t, v, xc[0], yc[0], zc[0], dx, dy, dz, n, ql[0], lam + ).shape + result = np.zeros(shape) + for xx, yy, zz, q in zip(xc, yc, zc, ql): + result += self.analytical( + x, y, z, t, v, xx, yy, zz, dx, dy, dz, n, q, lam, c0 + ) + return result def build_mf6gwf(sim_folder): print(f"Building mf6gwf model...{sim_folder}") name = "flow" - sim_ws = os.path.join(ws, sim_folder, "mf6gwf") + sim_ws = os.path.join(workspace, sim_folder, "mf6gwf") sim = flopy.mf6.MFSimulation(sim_name=name, sim_ws=sim_ws, exe_name="mf6") tdis_ds = ((total_time, 1, 1.0),) - flopy.mf6.ModflowTdis( - sim, nper=nper, perioddata=tdis_ds, time_units=time_units - ) + flopy.mf6.ModflowTdis(sim, nper=nper, perioddata=tdis_ds, time_units=time_units) flopy.mf6.ModflowIms(sim, print_option="summary", inner_maximum=300) gwf = flopy.mf6.ModflowGwf(sim, modelname=name, save_flows=True) flopy.mf6.ModflowGwfdis( @@ -126,18 +171,13 @@ def build_mf6gwf(sim_folder): return sim -# MODFLOW 6 flopy GWF simulation object (sim) is returned - - def build_mf6gwt(sim_folder): print(f"Building mf6gwt model...{sim_folder}") name = "trans" - sim_ws = os.path.join(ws, sim_folder, "mf6gwt") + sim_ws = os.path.join(workspace, sim_folder, "mf6gwt") sim = flopy.mf6.MFSimulation(sim_name=name, sim_ws=sim_ws, exe_name="mf6") tdis_ds = ((total_time, 400, 1.0),) - flopy.mf6.ModflowTdis( - sim, nper=nper, perioddata=tdis_ds, time_units=time_units - ) + flopy.mf6.ModflowTdis(sim, nper=nper, perioddata=tdis_ds, time_units=time_units) flopy.mf6.ModflowIms(sim, linear_acceleration="bicgstab") gwt = flopy.mf6.ModflowGwt(sim, modelname=name, save_flows=True) flopy.mf6.ModflowGwtdis( @@ -188,46 +228,31 @@ def build_mf6gwt(sim_folder): return sim -def build_model(sim_name): - sims = None - if config.buildModel: - sim_mf6gwf = build_mf6gwf(sim_name) - sim_mf6gwt = build_mf6gwt(sim_name) - sims = (sim_mf6gwf, sim_mf6gwt) - return sims - - -# Function to write model files +def build_models(sim_name): + return build_mf6gwf(sim_name), build_mf6gwt(sim_name) -def write_model(sims, silent=True): - if config.writeModel: - sim_mf6gwf, sim_mf6gwt = sims - sim_mf6gwf.write_simulation(silent=silent) - sim_mf6gwt.write_simulation(silent=silent) - return +def write_models(sims, silent=True): + for sim in sims: + sim.write_simulation(silent=silent) -# Function to run the model -# True is returned if the model runs successfully +@timed +def run_models(sims, silent=True): + for sim in sims: + success, buff = sim.run_simulation(silent=silent) + assert success, buff -@config.timeit -def run_model(sims, silent=True): - success = True - if config.runModel: - success = False - sim_mf6gwf, sim_mf6gwt = sims - success, buff = sim_mf6gwf.run_simulation(silent=silent) - if not success: - print(buff) - success, buff = sim_mf6gwt.run_simulation(silent=silent) - if not success: - print(buff) - return success +# - +# ### Plotting results +# +# Define functions to plot model results. -# Function to plot the model results +# + +# Figure properties +figure_size = (6, 4) def plot_analytical(ax, levels): @@ -250,32 +275,21 @@ def plot_analytical(ax, levels): x, y = np.meshgrid(x, y) z = 0 t = 400.0 - c400 = analytical.Wexler3d().multiwell( - x, y, z, t, v, xc, yc, zc, dx, dy, dz, n, q, lam, c0 - ) + c400 = Wexler3d().multiwell(x, y, z, t, v, xc, yc, zc, dx, dy, dz, n, q, lam, c0) cs = ax.contour(x, y, c400, levels=levels, colors="k") return cs def plot_results(sims): - if config.plotModel: - print("Plotting model results...") - sim_mf6gwf, sim_mf6gwt = sims - fs = USGSFigure(figure_type="map", verbose=False) - + _, sim_mf6gwt = sims + with styles.USGSMap(): conc = sim_mf6gwt.trans.output.concentration().get_data() - - fig, axs = plt.subplots( - 1, 1, figsize=figure_size, dpi=300, tight_layout=True - ) - + fig, axs = plt.subplots(1, 1, figsize=figure_size, dpi=300, tight_layout=True) gwt = sim_mf6gwt.trans pmv = flopy.plot.PlotMapView(model=gwt, ax=axs) levels = [1, 3, 10, 30, 100, 300] cs1 = plot_analytical(axs, levels) - cs2 = pmv.contour_array( - conc, colors="blue", linestyles="--", levels=levels - ) + cs2 = pmv.contour_array(conc, colors="blue", linestyles="--", levels=levels) axs.set_xlabel("x position (m)") axs.set_ylabel("y position (m)") axs.set_aspect(4.0) @@ -284,43 +298,34 @@ def plot_results(sims): lines = [cs1.collections[0], cs2.collections[0]] axs.legend(lines, labels, loc="upper left") - # save figure - if config.plotSave: + if plot_show: + plt.show() + if plot_save: sim_ws = sim_mf6gwt.simulation_data.mfpath.get_sim_path() sim_folder = os.path.split(sim_ws)[0] sim_folder = os.path.basename(sim_folder) - fname = f"{sim_folder}-map{config.figure_ext}" - fpth = os.path.join(ws, "..", "figures", fname) + fname = f"{sim_folder}-map.png" + fpth = os.path.join(workspace, "..", "figures", fname) fig.savefig(fpth) -# Function that wraps all of the steps for each scenario -# -# 1. build_model, -# 2. write_model, -# 3. run_model, and -# 4. plot_results. +# - + +# ### Running the example # +# Define and invoke a function to run the example scenario, then plot results. -def scenario(idx, silent=True): - sims = build_model(example_name) - write_model(sims, silent=silent) - success = run_model(sims, silent=silent) - if success: +# + +def scenario(silent=True): + sims = build_models(example_name) + if write: + write_models(sims, silent=silent) + if run: + run_models(sims, silent=silent) + if plot: plot_results(sims) -# nosetest - exclude block from this nosetest to the next nosetest -def test_01(): - scenario(0, silent=False) - - -# nosetest end - -if __name__ == "__main__": - # ### Model - - # Model run - - scenario(0) +scenario() +# - diff --git a/scripts/ex-gwt-moc3d-p02tg.py b/scripts/ex-gwt-moc3d-p02tg.py index df77be95d..fdbf45008 100644 --- a/scripts/ex-gwt-moc3d-p02tg.py +++ b/scripts/ex-gwt-moc3d-p02tg.py @@ -1,52 +1,53 @@ # ## Three-Dimensional Steady Flow with Transport # -# MOC3D Problem 2 simulated with a triangular grid +# This problem corresponds to the second problem presented in the MOC3D +# report Konikow 1996, which involves the transport of a dissolved +# constituent in a steady, three-dimensional flow field. An analytical +# solution for this problem is given by Wexler 1992. Like the previous +# example, this example is simulated with a GWT model which receives +# flow information from a separate simulation with the GWF model. In +# this example, however, a triangular grid is used for the flow and +# transport simulation. Results from the GWT model are compared with +# the results from the Wexler 1992 analytical solution. + +# ### Initial setup # -# - - -# ### Three-Dimensional Steady Flow with Transport Problem Setup - -# Imports +# Import dependencies, define the example name and workspace, and read settings from environment variables. +# + import os -import sys +import pathlib as pl import flopy import flopy.utils.cvfdutil import matplotlib.pyplot as plt import numpy as np +from flopy.plot.styles import styles +from modflow_devtools.misc import get_env, timed +from scipy.special import erfc -# Append to system path to include the common subdirectory - -sys.path.append(os.path.join("..", "common")) - -# Import common functionality - -import analytical -import config -from figspecs import USGSFigure - -mf6exe = "mf6" -exe_name_mf = "mf2005" -exe_name_mt = "mt3dms" - -# Set figure properties specific to this problem - -figure_size = (6, 4) +# Example name and base workspace +example_name = "ex-gwt-moc3d-p02tg" +workspace = pl.Path("../examples") -# Base simulation and model name and workspace +# Settings from environment variables +write = get_env("WRITE", True) +run = get_env("RUN", True) +plot = get_env("PLOT", True) +plot_show = get_env("PLOT_SHOW", True) +plot_save = get_env("PLOT_SAVE", True) +# - -ws = config.base_ws -example_name = "ex-gwt-moc3d-p02tg" +# ### Define parameters +# +# Define model units, parameters and other settings. +# + # Model units - length_units = "meters" time_units = "days" -# Table of model parameters - +# Model parameters nper = 1 # Number of periods nlay = 40 # Number of layers nrow = 12 # Number of rows @@ -69,11 +70,60 @@ botm = [-(k + 1) * delv for k in range(nlay)] specific_discharge = velocity_x * porosity source_location0 = tuple([idx - 1 for idx in source_location]) +# - -# ### Functions to build, write, run, and plot models -# -# MODFLOW 6 flopy GWF simulation object (sim) is returned +# ### Model setup # +# Define functions to build models, write input files, and run the simulation. + + +# + +class Wexler3d: + """ + Analytical solution for 3D transport with inflow at a well with a + specified concentration. + Wexler Page 47 + """ + + def calcgamma(self, x, y, z, xc, yc, zc, dx, dy, dz): + gam = np.sqrt((x - xc) ** 2 + dx / dy * (y - yc) ** 2 + dx / dz * (z - zc) ** 2) + return gam + + def calcbeta(self, v, dx, gam, lam): + beta = np.sqrt(v**2 + 4.0 * dx * gam * lam) + return beta + + def analytical(self, x, y, z, t, v, xc, yc, zc, dx, dy, dz, n, q, lam=0.0, c0=1.0): + gam = self.calcgamma(x, y, z, xc, yc, zc, dx, dy, dz) + beta = self.calcbeta(v, dx, gam, lam) + term1 = ( + c0 + * q + * np.exp(v * (x - xc) / 2.0 / dx) + / 8.0 + / n + / np.pi + / gam + / np.sqrt(dy * dz) + ) + term2 = np.exp(gam * beta / 2.0 / dx) * erfc( + (gam + beta * t) / 2.0 / np.sqrt(dx * t) + ) + term3 = np.exp(-gam * beta / 2.0 / dx) * erfc( + (gam - beta * t) / 2.0 / np.sqrt(dx * t) + ) + return term1 * (term2 + term3) + + def multiwell(self, x, y, z, t, v, xc, yc, zc, dx, dy, dz, n, ql, lam=0.0, c0=1.0): + shape = self.analytical( + x, y, z, t, v, xc[0], yc[0], zc[0], dx, dy, dz, n, ql[0], lam + ).shape + result = np.zeros(shape) + for xx, yy, zz, q in zip(xc, yc, zc, ql): + result += self.analytical( + x, y, z, t, v, xx, yy, zz, dx, dy, dz, n, q, lam, c0 + ) + return result def grid_triangulator(itri, delr, delc): @@ -146,12 +196,10 @@ def make_grid(): def build_mf6gwf(sim_folder): print(f"Building mf6gwf model...{sim_folder}") name = "flow" - sim_ws = os.path.join(ws, sim_folder, "mf6gwf") + sim_ws = os.path.join(workspace, sim_folder, "mf6gwf") sim = flopy.mf6.MFSimulation(sim_name=name, sim_ws=sim_ws, exe_name="mf6") tdis_ds = ((total_time, 1, 1.0),) - flopy.mf6.ModflowTdis( - sim, nper=nper, perioddata=tdis_ds, time_units=time_units - ) + flopy.mf6.ModflowTdis(sim, nper=nper, perioddata=tdis_ds, time_units=time_units) flopy.mf6.ModflowIms( sim, print_option="summary", @@ -203,18 +251,13 @@ def build_mf6gwf(sim_folder): return sim -# MODFLOW 6 flopy GWF simulation object (sim) is returned - - def build_mf6gwt(sim_folder): print(f"Building mf6gwt model...{sim_folder}") name = "trans" - sim_ws = os.path.join(ws, sim_folder, "mf6gwt") + sim_ws = os.path.join(workspace, sim_folder, "mf6gwt") sim = flopy.mf6.MFSimulation(sim_name=name, sim_ws=sim_ws, exe_name="mf6") tdis_ds = ((total_time, 100, 1.0),) - flopy.mf6.ModflowTdis( - sim, nper=nper, perioddata=tdis_ds, time_units=time_units - ) + flopy.mf6.ModflowTdis(sim, nper=nper, perioddata=tdis_ds, time_units=time_units) flopy.mf6.ModflowIms( sim, print_option="SUMMARY", @@ -275,46 +318,32 @@ def build_mf6gwt(sim_folder): return sim -def build_model(sim_name): - sims = None - if config.buildModel: - sim_mf6gwf = build_mf6gwf(sim_name) - sim_mf6gwt = build_mf6gwt(sim_name) - sims = (sim_mf6gwf, sim_mf6gwt) - return sims - - -# Function to write model files +def build_models(sim_name): + return build_mf6gwf(sim_name), build_mf6gwt(sim_name) -def write_model(sims, silent=True): - if config.writeModel: - sim_mf6gwf, sim_mf6gwt = sims - sim_mf6gwf.write_simulation(silent=silent) - sim_mf6gwt.write_simulation(silent=silent) - return +def write_models(sims, silent=True): + sim_mf6gwf, sim_mf6gwt = sims + sim_mf6gwf.write_simulation(silent=silent) + sim_mf6gwt.write_simulation(silent=silent) -# Function to run the model -# True is returned if the model runs successfully +@timed +def run_models(sims, silent=True): + for sim in sims: + success, buff = sim.run_simulation(silent=silent) + assert success, buff -@config.timeit -def run_model(sims, silent=True): - success = True - if config.runModel: - success = False - sim_mf6gwf, sim_mf6gwt = sims - success, buff = sim_mf6gwf.run_simulation(silent=silent) - if not success: - print(buff) - success, buff = sim_mf6gwt.run_simulation(silent=silent) - if not success: - print(buff) - return success +# - +# ### Plotting results +# +# Define functions to plot model results. -# Function to plot the model results +# + +# Figure properties +figure_size = (6, 4) def plot_analytical(ax, levels): @@ -337,23 +366,16 @@ def plot_analytical(ax, levels): x, y = np.meshgrid(x, y) z = 0 t = 400.0 - c400 = analytical.Wexler3d().multiwell( - x, y, z, t, v, xc, yc, zc, dx, dy, dz, n, q, lam, c0 - ) + c400 = Wexler3d().multiwell(x, y, z, t, v, xc, yc, zc, dx, dy, dz, n, q, lam, c0) cs = ax.contour(x, y, c400, levels=levels, colors="k") return cs def plot_grid(sims): - if config.plotModel: - print("Plotting model results...") - sim_mf6gwf, sim_mf6gwt = sims - fs = USGSFigure(figure_type="map", verbose=False) - + _, sim_mf6gwt = sims + with styles.USGSMap(): sim_ws = sim_mf6gwt.simulation_data.mfpath.get_sim_path() - fig, axs = plt.subplots( - 1, 1, figsize=figure_size, dpi=300, tight_layout=True - ) + fig, axs = plt.subplots(1, 1, figsize=figure_size, dpi=300, tight_layout=True) gwt = sim_mf6gwt.trans pmv = flopy.plot.PlotMapView(model=gwt, ax=axs) pmv.plot_grid() @@ -361,27 +383,23 @@ def plot_grid(sims): axs.set_ylabel("y position (m)") axs.set_aspect(4.0) - # save figure - if config.plotSave: + if plot_show: + plt.show() + if plot_save: sim_folder = os.path.split(sim_ws)[0] sim_folder = os.path.basename(sim_folder) - fname = f"{sim_folder}-grid{config.figure_ext}" - fpth = os.path.join(ws, "..", "figures", fname) + fname = f"{sim_folder}-grid.png" + fpth = os.path.join(workspace, "..", "figures", fname) fig.savefig(fpth) def plot_results(sims): - if config.plotModel: - print("Plotting model results...") - sim_mf6gwf, sim_mf6gwt = sims - fs = USGSFigure(figure_type="map", verbose=False) - + _, sim_mf6gwt = sims + with styles.USGSMap(): gwt = sim_mf6gwt.get_model("trans") conc = gwt.output.concentration().get_data() - fig, axs = plt.subplots( - 1, 1, figsize=figure_size, dpi=300, tight_layout=True - ) + fig, axs = plt.subplots(1, 1, figsize=figure_size, dpi=300, tight_layout=True) gwt = sim_mf6gwt.trans pmv = flopy.plot.PlotMapView(model=gwt, ax=axs) @@ -389,9 +407,7 @@ def plot_results(sims): # pmv.plot_grid() levels = [1, 3, 10, 30, 100, 300] cs1 = plot_analytical(axs, levels) - cs2 = pmv.contour_array( - conc, colors="blue", linestyles="--", levels=levels - ) + cs2 = pmv.contour_array(conc, colors="blue", linestyles="--", levels=levels) axs.set_xlabel("x position (m)") axs.set_ylabel("y position (m)") axs.set_aspect(4.0) @@ -400,45 +416,36 @@ def plot_results(sims): lines = [cs1.collections[0], cs2.collections[0]] axs.legend(lines, labels, loc="upper left") - # save figure - if config.plotSave: + if plot_show: + plt.show() + if plot_save: sim_folder = os.path.split( sim_mf6gwt.simulation_data.mfpath.get_sim_path() )[0] sim_folder = os.path.basename(sim_folder) - fname = f"{sim_folder}-map{config.figure_ext}" - fpth = os.path.join(ws, "..", "figures", fname) + fname = f"{sim_folder}-map.png" + fpth = os.path.join(workspace, "..", "figures", fname) fig.savefig(fpth) -# Function that wraps all of the steps for each scenario -# -# 1. build_model, -# 2. write_model, -# 3. run_model, and -# 4. plot_results. +# - + +# ### Running the example # +# Define and invoke a function to run the example scenario, then plot results. +# + def scenario(idx, silent=True): - sims = build_model(example_name) - write_model(sims, silent=silent) - success = run_model(sims, silent=silent) - if success: + sims = build_models(example_name) + if write: + write_models(sims, silent=silent) + if run: + run_models(sims, silent=silent) + if plot: plot_grid(sims) plot_results(sims) -# nosetest - exclude block from this nosetest to the next nosetest -def test_01(): - scenario(0, silent=False) - - -# nosetest end - -if __name__ == "__main__": - # ### Model - - # Model run - - scenario(0) +scenario(0) +# - diff --git a/scripts/ex-gwt-mt3dms-p01.py b/scripts/ex-gwt-mt3dms-p01.py index 0d425c3b7..d3fab754a 100644 --- a/scripts/ex-gwt-mt3dms-p01.py +++ b/scripts/ex-gwt-mt3dms-p01.py @@ -17,43 +17,40 @@ # 8. Two-Dimensional, Vertical Transport in a Heterogeneous Aquifer, # 9. Two-Dimensional Application Example, and # 10. Three-Dimensional Field Case Study. -# - -# ### MODFLOW 6 GWT MT3DMS Example 1 Problem Setup -# Imports +# ### Initial setup +# +# Import dependencies, define the example name and workspace, and read settings from environment variables. +# + import os -import sys +import pathlib as pl +from pprint import pformat import flopy import matplotlib.pyplot as plt import numpy as np +from flopy.plot.styles import styles +from modflow_devtools.misc import get_env, timed -# Append to system path to include the common subdirectory - -sys.path.append(os.path.join("..", "common")) - -# Import common functionality - -import config -from figspecs import USGSFigure +# Settings from environment variables +write = get_env("WRITE", True) +run = get_env("RUN", True) +plot = get_env("PLOT", True) +plot_show = get_env("PLOT_SHOW", True) +plot_save = get_env("PLOT_SAVE", True) -mf6exe = "mf6" -exe_name_mf = "mf2005" -exe_name_mt = "mt3dms" +# Base workspace +workspace = pl.Path("../examples") +# - -# Set figure properties specific to this problem - -figure_size = (5, 3.5) - -# Base simulation and model name and workspace - -ws = config.base_ws +# ### Define parameters +# +# Define model units, parameters and other settings. +# + # Set scenario parameters (make sure there is at least one blank line before next item) -# This entire dictionary is passed to _build_model()_ using the kwargs argument - +# This entire dictionary is passed to _build_models()_ using the kwargs argument parameters = { "ex-gwt-mt3dms-p01a": { "dispersivity": 0.0, @@ -81,7 +78,6 @@ # # add parameter_units to add units to the scenario parameter table that is automatically # built and used by the .tex input - parameter_units = { "dispersivity": "$m$", "retardation": "unitless", @@ -89,12 +85,10 @@ } # Model units - length_units = "meters" time_units = "days" -# Table MODFLOW 6 GWT MT3DMS Example 1 - +# Model parameters nper = 1 # Number of periods nlay = 1 # Number of layers ncol = 101 # Number of columns @@ -108,7 +102,6 @@ k11 = 1.0 # Horizontal hydraulic conductivity ($m/d$) # Set some static model parameter values - k33 = k11 # Vertical hydraulic conductivity ($m/d$) laytyp = 1 nstp = 100.0 @@ -126,7 +119,6 @@ ibound[0, 0, -1] = -1 # Set some static transport related model parameter values - mixelm = 0 # TVD rhob = 0.25 sp2 = 0.0 # red, but not used in this problem @@ -146,26 +138,24 @@ nlsink = nplane # HMOC npsink = nph # HMOC -# Static temporal data used by TDIS file - +# Time discretization tdis_rc = [] tdis_rc.append((perlen, nstp, 1.0)) -# ### Create MODFLOW 6 GWT MT3DMS Example 1 Boundary Conditions -# +# Create MODFLOW 6 GWT MT3DMS Example 1 Boundary Conditions # Constant head cells are specified on both ends of the model - chdspd = [[(0, 0, 0), h1], [(0, 0, ncol - 1), 0.0]] c0 = 1.0 cncspd = [[(0, 0, 0), c0]] +# - - -# ### Functions to build, write, run, and plot MODFLOW 6 GWT MT3DMS Example 1 model results +# ### Model setup # -# MODFLOW 6 flopy simulation object (sim) is returned if building the model +# Define functions to build models, write input files, and run the simulation. -def build_model( +# + +def build_models( sim_name, dispersivity=0.0, retardation=0.0, @@ -173,381 +163,368 @@ def build_model( mixelm=0, silent=False, ): - if config.buildModel: - mt3d_ws = os.path.join(ws, sim_name, "mt3d") - modelname_mf = "p01-mf" - - # Instantiate the MODFLOW model - mf = flopy.modflow.Modflow( - modelname=modelname_mf, model_ws=mt3d_ws, exe_name=exe_name_mf - ) - - # Instantiate discretization package - # units: itmuni=4 (days), lenuni=2 (m) - flopy.modflow.ModflowDis( - mf, - nlay=nlay, - nrow=nrow, - ncol=ncol, - delr=delr, - delc=delc, - top=top, - nstp=nstp, - botm=botm, - perlen=perlen, - itmuni=4, - lenuni=2, - ) - - # Instantiate basic package - flopy.modflow.ModflowBas(mf, ibound=ibound, strt=strt) - - # Instantiate layer property flow package - flopy.modflow.ModflowLpf(mf, hk=k11, laytyp=laytyp) - - # Instantiate solver package - flopy.modflow.ModflowPcg(mf) - - # Instantiate link mass transport package (for writing linker file) - flopy.modflow.ModflowLmt(mf) - - # Transport - modelname_mt = "p01-mt" - mt = flopy.mt3d.Mt3dms( - modelname=modelname_mt, - model_ws=mt3d_ws, - exe_name=exe_name_mt, - modflowmodel=mf, - ) - - c0 = 1.0 - icbund = np.ones((nlay, nrow, ncol), dtype=int) - icbund[0, 0, 0] = -1 - sconc = np.zeros((nlay, nrow, ncol), dtype=float) - sconc[0, 0, 0] = c0 - flopy.mt3d.Mt3dBtn( - mt, - laycon=laytyp, - icbund=icbund, - prsity=prsity, - sconc=sconc, - dt0=dt0, - ifmtcn=1, - ) - - # Instatiate the advection package - flopy.mt3d.Mt3dAdv( - mt, - mixelm=mixelm, - dceps=dceps, - nplane=nplane, - npl=npl, - nph=nph, - npmin=npmin, - npmax=npmax, - nlsink=nlsink, - npsink=npsink, - percel=0.5, - ) - - # Instantiate the dispersion package - flopy.mt3d.Mt3dDsp(mt, al=dispersivity) - - # Set reactive variables and instantiate chemical reaction package - if retardation == 1.0: - isothm = 0.0 - rc1 = 0.0 - else: - isothm = 1 - if decay != 0: - ireact = 1 - rc1 = decay - else: - ireact = 0.0 - rc1 = 0.0 - kd = (retardation - 1.0) * prsity / rhob - flopy.mt3d.Mt3dRct( - mt, - isothm=isothm, - ireact=ireact, - igetsc=0, - rhob=rhob, - sp1=kd, - rc1=rc1, - rc2=rc1, - ) - - # Instantiate the source/sink mixing package - flopy.mt3d.Mt3dSsm(mt) - - # Instantiate the GCG solver in MT3DMS - flopy.mt3d.Mt3dGcg(mt, mxiter=10) - - # MODFLOW 6 - name = "p01-mf6" - gwfname = "gwf-" + name - sim_ws = os.path.join(ws, sim_name) - sim = flopy.mf6.MFSimulation( - sim_name=sim_name, sim_ws=sim_ws, exe_name=mf6exe - ) - - # Instantiating MODFLOW 6 time discretization - flopy.mf6.ModflowTdis( - sim, nper=nper, perioddata=tdis_rc, time_units=time_units - ) - - # Instantiating MODFLOW 6 groundwater flow model - gwf = flopy.mf6.ModflowGwf( - sim, - modelname=gwfname, - save_flows=True, - model_nam_file=f"{gwfname}.nam", - ) - - # Instantiating MODFLOW 6 solver for flow model - imsgwf = flopy.mf6.ModflowIms( - sim, - print_option="SUMMARY", - outer_dvclose=hclose, - outer_maximum=nouter, - under_relaxation="NONE", - inner_maximum=ninner, - inner_dvclose=hclose, - rcloserecord=rclose, - linear_acceleration="CG", - scaling_method="NONE", - reordering_method="NONE", - relaxation_factor=relax, - filename=f"{gwfname}.ims", - ) - sim.register_ims_package(imsgwf, [gwf.name]) - - # Instantiating MODFLOW 6 discretization package - flopy.mf6.ModflowGwfdis( - gwf, - length_units=length_units, - nlay=nlay, - nrow=nrow, - ncol=ncol, - delr=delr, - delc=delc, - top=top, - botm=botm, - idomain=np.ones((nlay, nrow, ncol), dtype=int), - filename=f"{gwfname}.dis", - ) - - # Instantiating MODFLOW 6 node-property flow package - flopy.mf6.ModflowGwfnpf( - gwf, - save_flows=False, - icelltype=icelltype, - k=k11, - k33=k33, - save_specific_discharge=True, - filename=f"{gwfname}.npf", - ) - - # Instantiating MODFLOW 6 initial conditions package for flow model - flopy.mf6.ModflowGwfic(gwf, strt=strt, filename=f"{gwfname}.ic") - - # Instantiating MODFLOW 6 constant head package - flopy.mf6.ModflowGwfchd( - gwf, - maxbound=len(chdspd), - stress_period_data=chdspd, - save_flows=False, - pname="CHD-1", - filename=f"{gwfname}.chd", - ) - - # Instantiating MODFLOW 6 output control package for flow model - flopy.mf6.ModflowGwfoc( - gwf, - head_filerecord=f"{gwfname}.hds", - budget_filerecord=f"{gwfname}.cbc", - headprintrecord=[ - ("COLUMNS", 10, "WIDTH", 15, "DIGITS", 6, "GENERAL") - ], - saverecord=[("HEAD", "LAST"), ("BUDGET", "LAST")], - printrecord=[("HEAD", "LAST"), ("BUDGET", "LAST")], - ) - - # Instantiating MODFLOW 6 groundwater transport package - gwtname = "gwt-" + name - gwt = flopy.mf6.MFModel( - sim, - model_type="gwt6", - modelname=gwtname, - model_nam_file=f"{gwtname}.nam", - ) - gwt.name_file.save_flows = True - imsgwt = flopy.mf6.ModflowIms( - sim, - print_option="SUMMARY", - outer_dvclose=hclose, - outer_maximum=nouter, - under_relaxation="NONE", - inner_maximum=ninner, - inner_dvclose=hclose, - rcloserecord=rclose, - linear_acceleration="BICGSTAB", - scaling_method="NONE", - reordering_method="NONE", - relaxation_factor=relax, - filename=f"{gwtname}.ims", - ) - sim.register_ims_package(imsgwt, [gwt.name]) - - # Instantiating MODFLOW 6 transport discretization package - flopy.mf6.ModflowGwtdis( - gwt, - nlay=nlay, - nrow=nrow, - ncol=ncol, - delr=delr, - delc=delc, - top=top, - botm=botm, - idomain=1, - filename=f"{gwtname}.dis", - ) - - # Instantiating MODFLOW 6 transport initial concentrations - flopy.mf6.ModflowGwtic(gwt, strt=sconc, filename=f"{gwtname}.ic") - - # Instantiating MODFLOW 6 transport advection package - if mixelm == 0: - scheme = "UPSTREAM" - elif mixelm == -1: - scheme = "TVD" - else: - raise Exception() - flopy.mf6.ModflowGwtadv(gwt, scheme=scheme, filename=f"{gwtname}.adv") - - # Instantiating MODFLOW 6 transport dispersion package - if dispersivity != 0: - flopy.mf6.ModflowGwtdsp( - gwt, - xt3d_off=True, - alh=dispersivity, - ath1=dispersivity, - filename=f"{gwtname}.dsp", - ) - - # Instantiating MODFLOW 6 transport mass storage package (formerly "reaction" package in MT3DMS) - if retardation != 1.0: - sorption = "linear" - bulk_density = rhob - kd = ( - (retardation - 1.0) * prsity / rhob - ) # prsity & rhob defined in - else: # global variable section - sorption = None - bulk_density = None - kd = None - if decay != 0.0: - first_order_decay = True - decay_arg = decay - else: - first_order_decay = False - decay_arg = None - flopy.mf6.ModflowGwtmst( - gwt, - porosity=prsity, - sorption=sorption, - bulk_density=bulk_density, - distcoef=kd, - first_order_decay=first_order_decay, - decay=decay_arg, - decay_sorbed=decay_arg, - filename=f"{gwtname}.mst", - ) - - # Instantiating MODFLOW 6 transport constant concentration package - flopy.mf6.ModflowGwtcnc( - gwt, - maxbound=len(cncspd), - stress_period_data=cncspd, - save_flows=False, - pname="CNC-1", - filename=f"{gwtname}.cnc", - ) - - # Instantiating MODFLOW 6 transport source-sink mixing package - flopy.mf6.ModflowGwtssm(gwt, sources=[[]], filename=f"{gwtname}.ssm") - - # Instantiating MODFLOW 6 transport output control package - flopy.mf6.ModflowGwtoc( + mt3d_ws = os.path.join(workspace, sim_name, "mt3d") + modelname_mf = "p01-mf" + + # Instantiate the MODFLOW model + mf = flopy.modflow.Modflow( + modelname=modelname_mf, model_ws=mt3d_ws, exe_name="mf2005" + ) + + # Instantiate discretization package + # units: itmuni=4 (days), lenuni=2 (m) + flopy.modflow.ModflowDis( + mf, + nlay=nlay, + nrow=nrow, + ncol=ncol, + delr=delr, + delc=delc, + top=top, + nstp=nstp, + botm=botm, + perlen=perlen, + itmuni=4, + lenuni=2, + ) + + # Instantiate basic package + flopy.modflow.ModflowBas(mf, ibound=ibound, strt=strt) + + # Instantiate layer property flow package + flopy.modflow.ModflowLpf(mf, hk=k11, laytyp=laytyp) + + # Instantiate solver package + flopy.modflow.ModflowPcg(mf) + + # Instantiate link mass transport package (for writing linker file) + flopy.modflow.ModflowLmt(mf) + + # Transport + modelname_mt = "p01-mt" + mt = flopy.mt3d.Mt3dms( + modelname=modelname_mt, + model_ws=mt3d_ws, + exe_name="mt3dms", + modflowmodel=mf, + ) + + c0 = 1.0 + icbund = np.ones((nlay, nrow, ncol), dtype=int) + icbund[0, 0, 0] = -1 + sconc = np.zeros((nlay, nrow, ncol), dtype=float) + sconc[0, 0, 0] = c0 + flopy.mt3d.Mt3dBtn( + mt, + laycon=laytyp, + icbund=icbund, + prsity=prsity, + sconc=sconc, + dt0=dt0, + ifmtcn=1, + ) + + # Instatiate the advection package + flopy.mt3d.Mt3dAdv( + mt, + mixelm=mixelm, + dceps=dceps, + nplane=nplane, + npl=npl, + nph=nph, + npmin=npmin, + npmax=npmax, + nlsink=nlsink, + npsink=npsink, + percel=0.5, + ) + + # Instantiate the dispersion package + flopy.mt3d.Mt3dDsp(mt, al=dispersivity) + + # Set reactive variables and instantiate chemical reaction package + if retardation == 1.0: + isothm = 0.0 + rc1 = 0.0 + else: + isothm = 1 + if decay != 0: + ireact = 1 + rc1 = decay + else: + ireact = 0.0 + rc1 = 0.0 + kd = (retardation - 1.0) * prsity / rhob + flopy.mt3d.Mt3dRct( + mt, + isothm=isothm, + ireact=ireact, + igetsc=0, + rhob=rhob, + sp1=kd, + rc1=rc1, + rc2=rc1, + ) + + # Instantiate the source/sink mixing package + flopy.mt3d.Mt3dSsm(mt) + + # Instantiate the GCG solver in MT3DMS + flopy.mt3d.Mt3dGcg(mt, mxiter=10) + + # MODFLOW 6 + name = "p01-mf6" + gwfname = "gwf-" + name + sim_ws = os.path.join(workspace, sim_name) + sim = flopy.mf6.MFSimulation(sim_name=sim_name, sim_ws=sim_ws, exe_name="mf6") + + # Instantiating MODFLOW 6 time discretization + flopy.mf6.ModflowTdis(sim, nper=nper, perioddata=tdis_rc, time_units=time_units) + + # Instantiating MODFLOW 6 groundwater flow model + gwf = flopy.mf6.ModflowGwf( + sim, + modelname=gwfname, + save_flows=True, + model_nam_file=f"{gwfname}.nam", + ) + + # Instantiating MODFLOW 6 solver for flow model + imsgwf = flopy.mf6.ModflowIms( + sim, + print_option="SUMMARY", + outer_dvclose=hclose, + outer_maximum=nouter, + under_relaxation="NONE", + inner_maximum=ninner, + inner_dvclose=hclose, + rcloserecord=rclose, + linear_acceleration="CG", + scaling_method="NONE", + reordering_method="NONE", + relaxation_factor=relax, + filename=f"{gwfname}.ims", + ) + sim.register_ims_package(imsgwf, [gwf.name]) + + # Instantiating MODFLOW 6 discretization package + flopy.mf6.ModflowGwfdis( + gwf, + length_units=length_units, + nlay=nlay, + nrow=nrow, + ncol=ncol, + delr=delr, + delc=delc, + top=top, + botm=botm, + idomain=np.ones((nlay, nrow, ncol), dtype=int), + filename=f"{gwfname}.dis", + ) + + # Instantiating MODFLOW 6 node-property flow package + flopy.mf6.ModflowGwfnpf( + gwf, + save_flows=False, + icelltype=icelltype, + k=k11, + k33=k33, + save_specific_discharge=True, + filename=f"{gwfname}.npf", + ) + + # Instantiating MODFLOW 6 initial conditions package for flow model + flopy.mf6.ModflowGwfic(gwf, strt=strt, filename=f"{gwfname}.ic") + + # Instantiating MODFLOW 6 constant head package + flopy.mf6.ModflowGwfchd( + gwf, + maxbound=len(chdspd), + stress_period_data=chdspd, + save_flows=False, + pname="CHD-1", + filename=f"{gwfname}.chd", + ) + + # Instantiating MODFLOW 6 output control package for flow model + flopy.mf6.ModflowGwfoc( + gwf, + head_filerecord=f"{gwfname}.hds", + budget_filerecord=f"{gwfname}.cbc", + headprintrecord=[("COLUMNS", 10, "WIDTH", 15, "DIGITS", 6, "GENERAL")], + saverecord=[("HEAD", "LAST"), ("BUDGET", "LAST")], + printrecord=[("HEAD", "LAST"), ("BUDGET", "LAST")], + ) + + # Instantiating MODFLOW 6 groundwater transport package + gwtname = "gwt-" + name + gwt = flopy.mf6.MFModel( + sim, + model_type="gwt6", + modelname=gwtname, + model_nam_file=f"{gwtname}.nam", + ) + gwt.name_file.save_flows = True + imsgwt = flopy.mf6.ModflowIms( + sim, + print_option="SUMMARY", + outer_dvclose=hclose, + outer_maximum=nouter, + under_relaxation="NONE", + inner_maximum=ninner, + inner_dvclose=hclose, + rcloserecord=rclose, + linear_acceleration="BICGSTAB", + scaling_method="NONE", + reordering_method="NONE", + relaxation_factor=relax, + filename=f"{gwtname}.ims", + ) + sim.register_ims_package(imsgwt, [gwt.name]) + + # Instantiating MODFLOW 6 transport discretization package + flopy.mf6.ModflowGwtdis( + gwt, + nlay=nlay, + nrow=nrow, + ncol=ncol, + delr=delr, + delc=delc, + top=top, + botm=botm, + idomain=1, + filename=f"{gwtname}.dis", + ) + + # Instantiating MODFLOW 6 transport initial concentrations + flopy.mf6.ModflowGwtic(gwt, strt=sconc, filename=f"{gwtname}.ic") + + # Instantiating MODFLOW 6 transport advection package + if mixelm == 0: + scheme = "UPSTREAM" + elif mixelm == -1: + scheme = "TVD" + else: + raise Exception() + flopy.mf6.ModflowGwtadv(gwt, scheme=scheme, filename=f"{gwtname}.adv") + + # Instantiating MODFLOW 6 transport dispersion package + if dispersivity != 0: + flopy.mf6.ModflowGwtdsp( gwt, - budget_filerecord=f"{gwtname}.cbc", - concentration_filerecord=f"{gwtname}.ucn", - concentrationprintrecord=[ - ("COLUMNS", 10, "WIDTH", 15, "DIGITS", 6, "GENERAL") - ], - saverecord=[("CONCENTRATION", "LAST"), ("BUDGET", "LAST")], - printrecord=[("CONCENTRATION", "LAST"), ("BUDGET", "LAST")], + xt3d_off=True, + alh=dispersivity, + ath1=dispersivity, + filename=f"{gwtname}.dsp", ) - # Instantiating MODFLOW 6 flow-transport exchange mechanism - flopy.mf6.ModflowGwfgwt( - sim, - exgtype="GWF6-GWT6", - exgmnamea=gwfname, - exgmnameb=gwtname, - filename=f"{name}.gwfgwt", - ) - return mf, mt, sim - return None - - -# Function to write model files - - -def write_model(mf2k5, mt3d, sim, silent=True): - if config.writeModel: - mf2k5.write_input() - mt3d.write_input() - sim.write_simulation(silent=silent) - - -# Function to run the models. -# _True_ is returned if the model runs successfully + # Instantiating MODFLOW 6 transport mass storage package (formerly "reaction" package in MT3DMS) + if retardation != 1.0: + sorption = "linear" + bulk_density = rhob + kd = (retardation - 1.0) * prsity / rhob # prsity & rhob defined in + else: # global variable section + sorption = None + bulk_density = None + kd = None + if decay != 0.0: + first_order_decay = True + decay_arg = decay + else: + first_order_decay = False + decay_arg = None + flopy.mf6.ModflowGwtmst( + gwt, + porosity=prsity, + sorption=sorption, + bulk_density=bulk_density, + distcoef=kd, + first_order_decay=first_order_decay, + decay=decay_arg, + decay_sorbed=decay_arg, + filename=f"{gwtname}.mst", + ) + + # Instantiating MODFLOW 6 transport constant concentration package + flopy.mf6.ModflowGwtcnc( + gwt, + maxbound=len(cncspd), + stress_period_data=cncspd, + save_flows=False, + pname="CNC-1", + filename=f"{gwtname}.cnc", + ) + + # Instantiating MODFLOW 6 transport source-sink mixing package + flopy.mf6.ModflowGwtssm(gwt, sources=[[]], filename=f"{gwtname}.ssm") + + # Instantiating MODFLOW 6 transport output control package + flopy.mf6.ModflowGwtoc( + gwt, + budget_filerecord=f"{gwtname}.cbc", + concentration_filerecord=f"{gwtname}.ucn", + concentrationprintrecord=[("COLUMNS", 10, "WIDTH", 15, "DIGITS", 6, "GENERAL")], + saverecord=[("CONCENTRATION", "LAST"), ("BUDGET", "LAST")], + printrecord=[("CONCENTRATION", "LAST"), ("BUDGET", "LAST")], + ) + + # Instantiating MODFLOW 6 flow-transport exchange mechanism + flopy.mf6.ModflowGwfgwt( + sim, + exgtype="GWF6-GWT6", + exgmnamea=gwfname, + exgmnameb=gwtname, + filename=f"{name}.gwfgwt", + ) + return mf, mt, sim + + +def write_models(mf2k5, mt3d, sim, silent=True): + mf2k5.write_input() + mt3d.write_input() + sim.write_simulation(silent=silent) + + +@timed +def run_models(mf2k5, mt3d, sim, silent=True): + """Run models and assert successful completion.""" + success, buff = mf2k5.run_model(silent=silent, report=True) + assert success, pformat(buff) + success, buff = mt3d.run_model( + silent=silent, report=True, normal_msg="Program completed" + ) + assert success, pformat(buff) + success, buff = sim.run_simulation(silent=silent, report=True) + assert success, pformat(buff) + + +# - + +# ### Plotting results +# +# Define functions to plot model results. +# + +# Figure properties +figure_size = (5, 3.5) -@config.timeit -def run_model(mf2k5, mt3d, sim, silent=True): - success = True - if config.runModel: - success, buff = mf2k5.run_model(silent=silent) - success, buff = mt3d.run_model(silent=silent) - success, buff = sim.run_simulation(silent=silent) - if not success: - print(buff) - return success +def plot_results(mt3d, mf6, idx, ax=None): + mt3d_out_path = mt3d.model_ws + mf6.simulation_data.mfpath.get_sim_path() -# Function to plot the model results + # Get the MT3DMS concentration output + fname_mt3d = os.path.join(mt3d_out_path, "MT3D001.UCN") + ucnobj_mt3d = flopy.utils.UcnFile(fname_mt3d) + conc_mt3d = ucnobj_mt3d.get_alldata() + # Get the MF6 concentration output + gwt = mf6.get_model(list(mf6.model_names)[1]) + ucnobj_mf6 = gwt.output.concentration() + conc_mf6 = ucnobj_mf6.get_alldata() -def plot_results(mt3d, mf6, idx, ax=None): - if config.plotModel: - mt3d_out_path = mt3d.model_ws - mf6_out_path = mf6.simulation_data.mfpath.get_sim_path() - mf6.simulation_data.mfpath.get_sim_path() - - # Get the MT3DMS concentration output - fname_mt3d = os.path.join(mt3d_out_path, "MT3D001.UCN") - ucnobj_mt3d = flopy.utils.UcnFile(fname_mt3d) - conc_mt3d = ucnobj_mt3d.get_alldata() - - # Get the MF6 concentration output - gwt = mf6.get_model(list(mf6.model_names)[1]) - ucnobj_mf6 = gwt.output.concentration() - conc_mf6 = ucnobj_mf6.get_alldata() - - # Create figure for scenario - fs = USGSFigure(figure_type="graph", verbose=False) + # Create figure for scenario + with styles.USGSPlot() as fs: sim_name = mf6.name if ax is None: fig, ax = plt.subplots( @@ -575,74 +552,53 @@ def plot_results(mt3d, mf6, idx, ax=None): ax.set_xlim(0, 1000) ax.set_xlabel("Distance, in m") ax.set_ylabel("Concentration") - title = "Concentration Profile at Time = 2,000 " + "{}".format( - time_units - ) + title = "Concentration Profile at Time = 2,000 " + "{}".format(time_units) ax.legend() letter = chr(ord("@") + idx + 1) - fs.heading(letter=letter, heading=title) + styles.heading(letter=letter, heading=title) - # save figure - if config.plotSave: - fpth = os.path.join( - "..", "figures", f"{sim_name}{config.figure_ext}" - ) + if plot_show: + plt.show() + if plot_save: + fpth = os.path.join("..", "figures", f"{sim_name}.png") fig.savefig(fpth) -# Function that wraps all of the steps for each scenario. +# - + +# ### Running the example # -# 1. build_model, -# 2. write_model, -# 3. run_model, and -# 4. plot_results. +# Define and invoke a function to run the example scenario, then plot results. +# + def scenario(idx, silent=True): key = list(parameters.keys())[idx] parameter_dict = parameters[key] - mf2k5, mt3d, sim = build_model(key, **parameter_dict) - - write_model(mf2k5, mt3d, sim, silent=silent) - - success = run_model(mf2k5, mt3d, sim, silent=silent) - - if success: + mf2k5, mt3d, sim = build_models(key, **parameter_dict) + if write: + write_models(mf2k5, mt3d, sim, silent=silent) + if run: + run_models(mf2k5, mt3d, sim, silent=silent) + if plot: plot_results(mt3d, sim, idx) -# nosetest - exclude block from this nosetest to the next nosetest -def test_01(): - scenario(0, silent=False) - - -def test_02(): - scenario(1, silent=False) - - -def test_03(): - scenario(2, silent=False) - - -def test_04(): - scenario(3, silent=False) - +# - -# nosetest end -if __name__ == "__main__": - # ### Advection only +# Run advection only scenario. - scenario(0) +scenario(0) - # ### Advection and dispersion +# Run advection and dispersion scenario. - scenario(1) +scenario(1) - # ### Advection, dispersion, and retardation +# Run advection, dispersion, and retardation scenario. - scenario(2) +scenario(2) - # ### Advection, dispersion, retardation, and decay +# Run advection, dispersion, retardation, and decay scenario. - scenario(3) +scenario(3) diff --git a/scripts/ex-gwt-mt3dms-p02.py b/scripts/ex-gwt-mt3dms-p02.py index 77e185db4..faae013e5 100644 --- a/scripts/ex-gwt-mt3dms-p02.py +++ b/scripts/ex-gwt-mt3dms-p02.py @@ -1,44 +1,42 @@ -# ## One-Dimensional Steady Flow with Transport +# ## One-dimensional steady flow with transport # -# MT3DMS Problem 2 -# -# - +# This is the second example problem presented in Zheng 1999, titled "One-dimensional transport +# with nonlinear or nonequilibrium sorption. The purpose of this example is to demonstrate +# simulation of nonlinear and nonequilibrium sorption. In this section the results from the +# GWT model are compared with the results from MT3DMS. -# ### One-Dimensional Steady Flow with Transport Problem Setup - -# Imports +# ### Initial setup +# +# Import dependencies, define the example name and workspace, and read settings from environment variables. +# + import os -import sys +import pathlib as pl +from pprint import pformat import flopy import matplotlib.pyplot as plt -import numpy as np - -# Append to system path to include the common subdirectory - -sys.path.append(os.path.join("..", "common")) - -# Import common functionality - -import analytical -import config -from figspecs import USGSFigure +from flopy.plot.styles import styles +from modflow_devtools.misc import get_env, timed -mf6exe = "mf6" - -# Set figure properties specific to this problem - -figure_size = (5, 3) +# Example name and base workspace +workspace = pl.Path("../examples") +example_name = "ex-gwt-mt3dms-p02" -# Base simulation and model name and workspace +# Settings from an environment variables +write = get_env("WRITE", True) +run = get_env("RUN", True) +plot = get_env("PLOT", True) +plot_show = get_env("PLOT_SHOW", True) +plot_save = get_env("PLOT_SAVE", True) +# - -ws = config.base_ws -example_name = "ex-gwt-mt3dms-p02" +# ### Define parameters +# +# Define model units, parameters and other settings. +# + # Scenario parameters - make sure there is at least one blank line before next item - parameters = { "ex-gwt-mt3dms-p02a": { "sorption": "freundlich", @@ -66,7 +64,6 @@ # Scenario parameter units - make sure there is at least one blank line before next item # add parameter_units to add units to the scenario parameter table - parameter_units = { "beta": "$s^{-1}$", "sorption": "text string", @@ -77,12 +74,10 @@ } # Model units - length_units = "centimeters" time_units = "seconds" -# Table of model parameters - +# Model parameters nper = 2 # Number of periods nlay = 1 # Number of layers nrow = 1 # Number of rows @@ -107,24 +102,22 @@ inflow_rate = specific_discharge * delc * (top - botm) system_length = ncol * delr -# ### Functions to build, write, run, and plot models -# -# MODFLOW 6 flopy GWF simulation object (sim) is returned +# ### Model setup # +# Define functions to build models, write input files, and run the simulation. +# + def build_mf6gwf(sim_folder): print(f"Building mf6gwf model...{sim_folder}") name = "flow" - sim_ws = os.path.join(ws, sim_folder, "mf6gwf") + sim_ws = os.path.join(workspace, sim_folder, "mf6gwf") sim = flopy.mf6.MFSimulation(sim_name=name, sim_ws=sim_ws, exe_name="mf6") tdis_ds = ( (period1, int(period1 / delta_time), 1.0), (period2, int(period2 / delta_time), 1.0), ) - flopy.mf6.ModflowTdis( - sim, nper=nper, perioddata=tdis_ds, time_units=time_units - ) + flopy.mf6.ModflowTdis(sim, nper=nper, perioddata=tdis_ds, time_units=time_units) htol = 1.0e-8 flopy.mf6.ModflowIms( sim, print_option="summary", outer_dvclose=htol, inner_dvclose=htol @@ -183,23 +176,18 @@ def build_mf6gwf(sim_folder): return sim -# MODFLOW 6 flopy GWF simulation object (sim) is returned - - def build_mf6gwt( sim_folder, sorption=None, Kf=None, a=None, Kl=None, S=None, beta=None ): print(f"Building mf6gwt model...{sim_folder}") name = "trans" - sim_ws = os.path.join(ws, sim_folder, "mf6gwt") + sim_ws = os.path.join(workspace, sim_folder, "mf6gwt") sim = flopy.mf6.MFSimulation(sim_name=name, sim_ws=sim_ws, exe_name="mf6") tdis_ds = ( (period1, int(period1 / delta_time), 1.0), (period2, int(period2 / delta_time), 1.0), ) - flopy.mf6.ModflowTdis( - sim, nper=nper, perioddata=tdis_ds, time_units=time_units - ) + flopy.mf6.ModflowTdis(sim, nper=nper, perioddata=tdis_ds, time_units=time_units) ctol = 1.0e-8 flopy.mf6.ModflowIms( sim, @@ -245,9 +233,7 @@ def build_mf6gwt( sp2=sp2, ) flopy.mf6.ModflowGwtadv(gwt, scheme="UPSTREAM") - flopy.mf6.ModflowGwtdsp( - gwt, xt3d_off=True, alh=dispersivity, ath1=dispersivity - ) + flopy.mf6.ModflowGwtdsp(gwt, xt3d_off=True, alh=dispersivity, ath1=dispersivity) if beta is not None: if beta > 0: porosity_im = bulk_density / volfracim @@ -282,10 +268,8 @@ def build_mf6gwt( def build_mf2005(sim_folder): print(f"Building mf2005 model...{sim_folder}") name = "flow" - sim_ws = os.path.join(ws, sim_folder, "mf2005") - mf = flopy.modflow.Modflow( - modelname=name, model_ws=sim_ws, exe_name="mf2005" - ) + sim_ws = os.path.join(workspace, sim_folder, "mf2005") + mf = flopy.modflow.Modflow(modelname=name, model_ws=sim_ws, exe_name="mf2005") perlen = [period1, period2] dis = flopy.modflow.ModflowDis( mf, @@ -325,7 +309,7 @@ def build_mt3dms( ): print(f"Building mt3dms model...{sim_folder}") name = "trans" - sim_ws = os.path.join(ws, sim_folder, "mt3d") + sim_ws = os.path.join(workspace, sim_folder, "mt3d") mt = flopy.mt3d.Mt3dms( modelname=name, model_ws=sim_ws, @@ -363,76 +347,56 @@ def build_mt3dms( return mt -def build_model(sim_name, **kwargs): - sims = None - if config.buildModel: - sim_mf6gwf = build_mf6gwf(sim_name) - sim_mf6gwt = build_mf6gwt(sim_name, **kwargs) - sim_mf2005 = build_mf2005(sim_name) - sim_mt3dms = build_mt3dms(sim_name, sim_mf2005, **kwargs) - sims = (sim_mf6gwf, sim_mf6gwt, sim_mf2005, sim_mt3dms) - return sims - - -# Function to write model files - - -def write_model(sims, silent=True): - if config.writeModel: - sim_mf6gwf, sim_mf6gwt, sim_mf2005, sim_mt3dms = sims - sim_mf6gwf.write_simulation(silent=silent) - sim_mf6gwt.write_simulation(silent=silent) - sim_mf2005.write_input() - sim_mt3dms.write_input() - return - - -# Function to run the model -# True is returned if the model runs successfully - - -@config.timeit -def run_model(sims, silent=True): - success = True - report = True - if config.runModel: - success = False - sim_mf6gwf, sim_mf6gwt, sim_mf2005, sim_mt3dms = sims - print("Running mf6gwf...") - success, buff = sim_mf6gwf.run_simulation(silent=silent, report=report) - if not success: - print(buff) - print("Running mf6gwt...") - success, buff = sim_mf6gwt.run_simulation(silent=silent, report=report) - if not success: - print(buff) - print("Running mf2005...") - success, buff = sim_mf2005.run_model(silent=silent, report=report) - if not success: - print(buff) - print("Running mt3dms...") - success, buff = sim_mt3dms.run_model( - silent=silent, normal_msg="Program completed", report=report - ) - if not success: - print(buff) - return success +def build_models(sim_name, **kwargs): + sim_mf6gwf = build_mf6gwf(sim_name) + sim_mf6gwt = build_mf6gwt(sim_name, **kwargs) + sim_mf2005 = build_mf2005(sim_name) + sim_mt3dms = build_mt3dms(sim_name, sim_mf2005, **kwargs) + return sim_mf6gwf, sim_mf6gwt, sim_mf2005, sim_mt3dms + + +def write_models(sims, silent=True): + sim_mf6gwf, sim_mf6gwt, sim_mf2005, sim_mt3dms = sims + sim_mf6gwf.write_simulation(silent=silent) + sim_mf6gwt.write_simulation(silent=silent) + sim_mf2005.write_input() + sim_mt3dms.write_input() + + +@timed +def run_models(sims, silent=True): + sim_mf6gwf, sim_mf6gwt, sim_mf2005, sim_mt3dms = sims + success, buff = sim_mf6gwf.run_simulation(silent=silent, report=True) + assert success, pformat(buff) + success, buff = sim_mf6gwt.run_simulation(silent=silent, report=True) + assert success, pformat(buff) + success, buff = sim_mf2005.run_model(silent=silent, report=True) + assert success, pformat(buff) + success, buff = sim_mt3dms.run_model( + silent=silent, normal_msg="Program completed", report=True + ) + assert success, pformat(buff) + +# - -# Function to plot the model results +# ### Plotting results +# +# Define functions to plot model results. + +# + +# Figure properties +figure_size = (5, 3) def plot_results_ct(sims, idx, **kwargs): - if config.plotModel: - print("Plotting C versus t model results...") - sim_mf6gwf, sim_mf6gwt, sim_mf2005, sim_mt3dms = sims - fs = USGSFigure(figure_type="graph", verbose=False) + print("Plotting C versus t model results...") + _, sim_mf6gwt, _, sim_mt3dms = sims + with styles.USGSPlot(): sim_ws = sim_mf6gwt.simulation_data.mfpath.get_sim_path() mf6gwt_ra = sim_mf6gwt.get_model("trans").obs.output.obs().data - fig, axs = plt.subplots( - 1, 1, figsize=figure_size, dpi=300, tight_layout=True - ) + fig, axs = plt.subplots(1, 1, figsize=figure_size, dpi=300, tight_layout=True) sim_ws = sim_mt3dms.model_ws fname = os.path.join(sim_ws, "MT3D001.OBS") @@ -464,28 +428,23 @@ def plot_results_ct(sims, idx, **kwargs): axs.set_ylabel("Normalized Concentration (unitless)") axs.legend() - # save figure - if config.plotSave: + if plot_show: + plt.show() + if plot_save: sim_folder = os.path.split(sim_ws)[0] sim_folder = os.path.basename(sim_folder) - fname = f"{sim_folder}-ct{config.figure_ext}" - fpth = os.path.join(ws, "..", "figures", fname) + fname = f"{sim_folder}-ct.png" + fpth = os.path.join(workspace, "..", "figures", fname) fig.savefig(fpth) def plot_results(): - if config.plotModel: - print("Plotting model results...") - - fs = USGSFigure(figure_type="graph", verbose=False) - fig, axs = plt.subplots( - 1, 1, figsize=figure_size, dpi=300, tight_layout=True - ) - + with styles.USGSPlot(): + fig, axs = plt.subplots(1, 1, figsize=figure_size, dpi=300, tight_layout=True) case_colors = ["blue", "green", "red", "yellow"] pkeys = list(parameters.keys()) for icase, sim_name in enumerate(pkeys[2:]): - sim_ws = os.path.join(ws, sim_name) + sim_ws = os.path.join(workspace, sim_name) beta = parameters[sim_name]["beta"] fname = os.path.join(sim_ws, "mf6gwt", "trans.obs.csv") @@ -516,90 +475,41 @@ def plot_results(): axs.set_ylabel("Normalized Concentration (unitless)") axs.legend() - # save figure - if config.plotSave: - fname = f"{example_name}{config.figure_ext}" + if plot_show: + plt.show() + if plot_save: + fname = f"{example_name}.png" fpth = os.path.join("..", "figures", fname) fig.savefig(fpth) - return -# Function that wraps all of the steps for each scenario -# -# 1. build_model, -# 2. write_model, -# 3. run_model, and -# 4. plot_results. +# - + +# ### Running the example # +# Define and invoke a function to run the example scenario, then plot results. +# + def scenario(idx, silent=True): key = list(parameters.keys())[idx] parameter_dict = parameters[key] - sims = build_model(key, **parameter_dict) - write_model(sims, silent=silent) - success = run_model(sims, silent=silent) - if success: + sims = build_models(key, **parameter_dict) + if write: + write_models(sims, silent=silent) + if run: + run_models(sims, silent=silent) + if plot: plot_results_ct(sims, idx, **parameter_dict) -# nosetest - exclude block from this nosetest to the next nosetest -def test_01(): - scenario(0, silent=False) - - -def test_02(): - scenario(1, silent=False) - - -def test_03(): - scenario(2, silent=False) - - -def test_04(): - scenario(3, silent=False) - - -def test_05(): - scenario(4, silent=False) - - -def test_06(): - scenario(5, silent=False) - - -def test_plot_results(): - plot_results() - - -# nosetest end - -if __name__ == "__main__": - # ### Simulated Zero-Order Growth in a Uniform Flow Field - - # Scenario 1 - description - - scenario(0) - - # Scenario 2 - description - - scenario(1) - - # Scenario 3 - description - - scenario(2) - - # Scenario 4 - description - - scenario(3) - - # Scenario 5 - description - - scenario(4) - - # Scenario 6 - description - - scenario(5) +scenario(0) +scenario(1) +scenario(2) +scenario(3) +scenario(4) +scenario(5) - # Plot All Results +if plot: plot_results() +# - diff --git a/scripts/ex-gwt-mt3dms-p03.py b/scripts/ex-gwt-mt3dms-p03.py index 4f1b10744..a790592d7 100644 --- a/scripts/ex-gwt-mt3dms-p03.py +++ b/scripts/ex-gwt-mt3dms-p03.py @@ -18,44 +18,43 @@ # 9. Two-Dimensional Application Example # 10. Three-Dimensional Field Case Study +# ### Initial setup +# +# Import dependencies, define the example name and workspace, and read settings from environment variables. -# ### MODFLOW 6 GWT MT3DMS Example 3 Problem Setup - -# Append to system path to include the common subdirectory - +# + import os -import sys +import pathlib as pl +from pprint import pformat -sys.path.append(os.path.join("..", "common")) - -# Imports - -import config import flopy import matplotlib.pyplot as plt import numpy as np -from figspecs import USGSFigure - -mf6exe = "mf6" -exe_name_mf = "mf2005" -exe_name_mt = "mt3dms" +from flopy.plot.styles import styles +from modflow_devtools.misc import get_env, timed -# Set figure properties specific to this problem - -figure_size = (6, 4.5) +# Example name and base workspace +workspace = pl.Path("../examples") +example_name = "ex-gwt-mt3dms-p03" -# Base simulation and model name and workspace +# Settings from environment variables +write = get_env("WRITE", True) +run = get_env("RUN", True) +plot = get_env("PLOT", True) +plot_show = get_env("PLOT_SHOW", True) +plot_save = get_env("PLOT_SAVE", True) +# - -ws = config.base_ws -example_name = "ex-gwt-mt3dms-p03" +# ### Define parameters +# +# Define model units, parameters and other settings. +# + # Model units - length_units = "meters" time_units = "days" -# Table - +# Model parameters nlay = 1 # Number of layers nrow = 31 # Number of rows ncol = 46 # Number of columns @@ -72,7 +71,6 @@ trpt = 0.3 # Ratio of transverse to longitudinal dispersivity # Additional model input - perlen = [1, 365.0] nper = len(perlen) nstp = [2, 730] @@ -88,7 +86,6 @@ icelltype = 0 # Initial conditions - Lx = (ncol - 1) * delr v = 1.0 / 3.0 prsity = 0.3 @@ -109,9 +106,7 @@ # (k, i, j), flow, conc spd_mf6 = {0: [[(0, 15, 15), qwell, cwell]]} # MF6 pumping information - # Set solver parameter values (and related) - nouter, ninner = 100, 300 hclose, rclose, relax = 1e-6, 1e-6, 1.0 ttsmult = 1.0 @@ -128,388 +123,374 @@ nlsink = nplane # HMOC npsink = nph # HMOC -# Static temporal data used by TDIS file - +# Time discretization tdis_rc = [] tdis_rc.append((perlen, nstp, 1.0)) +# - - -# ### Functions to build, write, and run models and plot MT3DMS Example 10 Problem results +# ### Model setup # -# MODFLOW 6 flopy simulation object (sim) is returned if building the model - - -def build_model(sim_name, mixelm=0, silent=False): - if config.buildModel: - mt3d_ws = os.path.join(ws, sim_name, "mt3d") - modelname_mf = "p03-mf" - - # Instantiate the MODFLOW model - mf = flopy.modflow.Modflow( - modelname=modelname_mf, model_ws=mt3d_ws, exe_name=exe_name_mf - ) - - # Instantiate discretization package - # units: itmuni=4 (days), lenuni=2 (m) - flopy.modflow.ModflowDis( - mf, - nlay=nlay, - nrow=nrow, - ncol=ncol, - delr=delr, - delc=delc, - top=top, - botm=botm, - nper=nper, - nstp=nstp, - perlen=perlen, - itmuni=4, - lenuni=2, - ) - - # Instantiate basic package - flopy.modflow.ModflowBas(mf, ibound=ibound_mf2k5, strt=strt) - - # Instantiate layer property flow package - flopy.modflow.ModflowLpf(mf, hk=k11, laytyp=icelltype) - - # Instantiate well package - flopy.modflow.ModflowWel(mf, stress_period_data=welspd) - - # Instantiate solver package - flopy.modflow.ModflowPcg(mf) - - # Instantiate link mass transport package (for writing linker file) - flopy.modflow.ModflowLmt(mf) - - # Transport - modelname_mt = "p03-mt" - mt = flopy.mt3d.Mt3dms( - modelname=modelname_mt, - model_ws=mt3d_ws, - exe_name=exe_name_mt, - modflowmodel=mf, - ) - - # Instantiate basic transport package - flopy.mt3d.Mt3dBtn( - mt, - icbund=icbund, - prsity=prsity, - sconc=sconc, - nstp=nstp, - perlen=perlen, - ) - - # Instatiate the advection package - flopy.mt3d.Mt3dAdv( - mt, - mixelm=mixelm, - dceps=dceps, - nplane=nplane, - npl=npl, - nph=nph, - npmin=npmin, - npmax=npmax, - nlsink=nlsink, - npsink=npsink, - percel=percel, - itrack=itrack, - wd=wd, - ) - - # Instantiate the dispersion package - flopy.mt3d.Mt3dDsp(mt, al=al, trpt=trpt, dmcoef=dmcoef) - - # Instantiate the source/sink mixing package - flopy.mt3d.Mt3dSsm(mt, stress_period_data=spd) - - # Instantiate the GCG solver in MT3DMS - flopy.mt3d.Mt3dGcg(mt, mxiter=10) - - # MODFLOW 6 - name = "p03-mf6" - gwfname = "gwf-" + name - sim_ws = os.path.join(ws, sim_name) - sim = flopy.mf6.MFSimulation( - sim_name=sim_name, sim_ws=sim_ws, exe_name=mf6exe - ) - - # Instantiating MODFLOW 6 time discretization - tdis_rc = [] - for i in range(nper): - tdis_rc.append((perlen[i], nstp[i], tsmult[i])) - flopy.mf6.ModflowTdis( - sim, nper=nper, perioddata=tdis_rc, time_units=time_units - ) - - # Instantiating MODFLOW 6 groundwater flow model - gwf = flopy.mf6.ModflowGwf( - sim, - modelname=gwfname, - save_flows=True, - model_nam_file=f"{gwfname}.nam", - ) - - # Instantiating MODFLOW 6 solver for flow model - imsgwf = flopy.mf6.ModflowIms( - sim, - print_option="SUMMARY", - outer_dvclose=hclose, - outer_maximum=nouter, - under_relaxation="NONE", - inner_maximum=ninner, - inner_dvclose=hclose, - rcloserecord=rclose, - linear_acceleration="CG", - scaling_method="NONE", - reordering_method="NONE", - relaxation_factor=relax, - filename=f"{gwfname}.ims", - ) - sim.register_ims_package(imsgwf, [gwf.name]) - - # Instantiating MODFLOW 6 discretization package - flopy.mf6.ModflowGwfdis( - gwf, - length_units=length_units, - nlay=nlay, - nrow=nrow, - ncol=ncol, - delr=delr, - delc=delc, - top=top, - botm=botm, - idomain=np.ones((nlay, nrow, ncol), dtype=int), - filename=f"{gwfname}.dis", - ) - - # Instantiating MODFLOW 6 node-property flow package - flopy.mf6.ModflowGwfnpf( - gwf, - save_flows=False, - icelltype=icelltype, - k=k11, - k33=k33, - save_specific_discharge=True, - filename=f"{gwfname}.npf", - ) - - # Instantiating MODFLOW 6 initial conditions package for flow model - flopy.mf6.ModflowGwfic(gwf, strt=strt, filename=f"{gwfname}.ic") - - # Instantiate MODFLOW 6 storage package - sto = flopy.mf6.ModflowGwfsto( - gwf, ss=0, sy=0, filename=f"{gwfname}.sto" - ) - - # Instantiating MODFLOW 6 constant head package - rowList = np.arange(0, nrow).tolist() - chdspd = [] - # Loop through the left & right sides. - for itm in rowList: - # first, do left side of model - chdspd.append([(0, itm, 0), h1]) - # finally, do right side of model - chdspd.append([(0, itm, ncol - 1), 0.0]) - - chdspd = {0: chdspd} - flopy.mf6.ModflowGwfchd( - gwf, - maxbound=len(chdspd), - stress_period_data=chdspd, - save_flows=False, - pname="CHD-1", - filename=f"{gwfname}.chd", - ) - - # Instantiate the wel package - flopy.mf6.ModflowGwfwel( - gwf, - print_input=True, - print_flows=True, - stress_period_data=spd_mf6, - save_flows=False, - auxiliary="CONCENTRATION", - pname="WEL-1", - filename=f"{gwfname}.wel", - ) - - # Instantiating MODFLOW 6 output control package for flow model - flopy.mf6.ModflowGwfoc( - gwf, - head_filerecord=f"{gwfname}.hds", - budget_filerecord=f"{gwfname}.bud", - headprintrecord=[ - ("COLUMNS", 10, "WIDTH", 15, "DIGITS", 6, "GENERAL") - ], - saverecord=[("HEAD", "LAST"), ("BUDGET", "LAST")], - printrecord=[("HEAD", "LAST"), ("BUDGET", "LAST")], - ) - - # Instantiating MODFLOW 6 groundwater transport package - gwtname = "gwt_" + name - gwt = flopy.mf6.MFModel( - sim, - model_type="gwt6", - modelname=gwtname, - model_nam_file=f"{gwtname}.nam", - ) - gwt.name_file.save_flows = True - - # create iterative model solution and register the gwt model with it - imsgwt = flopy.mf6.ModflowIms( - sim, - print_option="SUMMARY", - outer_dvclose=hclose, - outer_maximum=nouter, - under_relaxation="NONE", - inner_maximum=ninner, - inner_dvclose=hclose, - rcloserecord=rclose, - linear_acceleration="BICGSTAB", - scaling_method="NONE", - reordering_method="NONE", - relaxation_factor=relax, - filename=f"{gwtname}.ims", - ) - sim.register_ims_package(imsgwt, [gwt.name]) - - # Instantiating MODFLOW 6 transport discretization package - flopy.mf6.ModflowGwtdis( - gwt, - nlay=nlay, - nrow=nrow, - ncol=ncol, - delr=delr, - delc=delc, - top=top, - botm=botm, - idomain=1, - filename=f"{gwtname}.dis", - ) - - # Instantiating MODFLOW 6 transport initial concentrations - flopy.mf6.ModflowGwtic(gwt, strt=sconc, filename=f"{gwtname}.ic") - - # Instantiating MODFLOW 6 transport advection package - if mixelm == 0: - scheme = "UPSTREAM" - elif mixelm == -1: - scheme = "TVD" - else: - raise Exception() - flopy.mf6.ModflowGwtadv(gwt, scheme=scheme, filename=f"{gwtname}.adv") - - # Instantiating MODFLOW 6 transport dispersion package - if al != 0: - flopy.mf6.ModflowGwtdsp( - gwt, - xt3d_off=True, - alh=al, - ath1=ath1, - filename=f"{gwtname}.dsp", - ) - - # Instantiating MODFLOW 6 transport mass storage package (formerly "reaction" package in MT3DMS) - flopy.mf6.ModflowGwtmst( - gwt, - porosity=prsity, - first_order_decay=False, - decay=None, - decay_sorbed=None, - sorption=None, - bulk_density=None, - distcoef=None, - filename=f"{gwtname}.mst", - ) - - # Instantiating MODFLOW 6 transport constant concentration package - flopy.mf6.ModflowGwtcnc( +# Define functions to build models, write input files, and run the simulation. + + +# + +def build_models(sim_name, mixelm=0, silent=False): + mt3d_ws = os.path.join(workspace, sim_name, "mt3d") + modelname_mf = "p03-mf" + + # Instantiate the MODFLOW model + mf = flopy.modflow.Modflow( + modelname=modelname_mf, model_ws=mt3d_ws, exe_name="mf2005" + ) + + # Instantiate discretization package + # units: itmuni=4 (days), lenuni=2 (m) + flopy.modflow.ModflowDis( + mf, + nlay=nlay, + nrow=nrow, + ncol=ncol, + delr=delr, + delc=delc, + top=top, + botm=botm, + nper=nper, + nstp=nstp, + perlen=perlen, + itmuni=4, + lenuni=2, + ) + + # Instantiate basic package + flopy.modflow.ModflowBas(mf, ibound=ibound_mf2k5, strt=strt) + + # Instantiate layer property flow package + flopy.modflow.ModflowLpf(mf, hk=k11, laytyp=icelltype) + + # Instantiate well package + flopy.modflow.ModflowWel(mf, stress_period_data=welspd) + + # Instantiate solver package + flopy.modflow.ModflowPcg(mf) + + # Instantiate link mass transport package (for writing linker file) + flopy.modflow.ModflowLmt(mf) + + # Transport + modelname_mt = "p03-mt" + mt = flopy.mt3d.Mt3dms( + modelname=modelname_mt, + model_ws=mt3d_ws, + exe_name="mt3dms", + modflowmodel=mf, + ) + + # Instantiate basic transport package + flopy.mt3d.Mt3dBtn( + mt, + icbund=icbund, + prsity=prsity, + sconc=sconc, + nstp=nstp, + perlen=perlen, + ) + + # Instatiate the advection package + flopy.mt3d.Mt3dAdv( + mt, + mixelm=mixelm, + dceps=dceps, + nplane=nplane, + npl=npl, + nph=nph, + npmin=npmin, + npmax=npmax, + nlsink=nlsink, + npsink=npsink, + percel=percel, + itrack=itrack, + wd=wd, + ) + + # Instantiate the dispersion package + flopy.mt3d.Mt3dDsp(mt, al=al, trpt=trpt, dmcoef=dmcoef) + + # Instantiate the source/sink mixing package + flopy.mt3d.Mt3dSsm(mt, stress_period_data=spd) + + # Instantiate the GCG solver in MT3DMS + flopy.mt3d.Mt3dGcg(mt, mxiter=10) + + # MODFLOW 6 + name = "p03-mf6" + gwfname = "gwf-" + name + sim_ws = os.path.join(workspace, sim_name) + sim = flopy.mf6.MFSimulation(sim_name=sim_name, sim_ws=sim_ws, exe_name="mf6") + + # Instantiating MODFLOW 6 time discretization + tdis_rc = [] + for i in range(nper): + tdis_rc.append((perlen[i], nstp[i], tsmult[i])) + flopy.mf6.ModflowTdis(sim, nper=nper, perioddata=tdis_rc, time_units=time_units) + + # Instantiating MODFLOW 6 groundwater flow model + gwf = flopy.mf6.ModflowGwf( + sim, + modelname=gwfname, + save_flows=True, + model_nam_file=f"{gwfname}.nam", + ) + + # Instantiating MODFLOW 6 solver for flow model + imsgwf = flopy.mf6.ModflowIms( + sim, + print_option="SUMMARY", + outer_dvclose=hclose, + outer_maximum=nouter, + under_relaxation="NONE", + inner_maximum=ninner, + inner_dvclose=hclose, + rcloserecord=rclose, + linear_acceleration="CG", + scaling_method="NONE", + reordering_method="NONE", + relaxation_factor=relax, + filename=f"{gwfname}.ims", + ) + sim.register_ims_package(imsgwf, [gwf.name]) + + # Instantiating MODFLOW 6 discretization package + flopy.mf6.ModflowGwfdis( + gwf, + length_units=length_units, + nlay=nlay, + nrow=nrow, + ncol=ncol, + delr=delr, + delc=delc, + top=top, + botm=botm, + idomain=np.ones((nlay, nrow, ncol), dtype=int), + filename=f"{gwfname}.dis", + ) + + # Instantiating MODFLOW 6 node-property flow package + flopy.mf6.ModflowGwfnpf( + gwf, + save_flows=False, + icelltype=icelltype, + k=k11, + k33=k33, + save_specific_discharge=True, + filename=f"{gwfname}.npf", + ) + + # Instantiating MODFLOW 6 initial conditions package for flow model + flopy.mf6.ModflowGwfic(gwf, strt=strt, filename=f"{gwfname}.ic") + + # Instantiate MODFLOW 6 storage package + sto = flopy.mf6.ModflowGwfsto(gwf, ss=0, sy=0, filename=f"{gwfname}.sto") + + # Instantiating MODFLOW 6 constant head package + rowList = np.arange(0, nrow).tolist() + chdspd = [] + # Loop through the left & right sides. + for itm in rowList: + # first, do left side of model + chdspd.append([(0, itm, 0), h1]) + # finally, do right side of model + chdspd.append([(0, itm, ncol - 1), 0.0]) + + chdspd = {0: chdspd} + flopy.mf6.ModflowGwfchd( + gwf, + maxbound=len(chdspd), + stress_period_data=chdspd, + save_flows=False, + pname="CHD-1", + filename=f"{gwfname}.chd", + ) + + # Instantiate the wel package + flopy.mf6.ModflowGwfwel( + gwf, + print_input=True, + print_flows=True, + stress_period_data=spd_mf6, + save_flows=False, + auxiliary="CONCENTRATION", + pname="WEL-1", + filename=f"{gwfname}.wel", + ) + + # Instantiating MODFLOW 6 output control package for flow model + flopy.mf6.ModflowGwfoc( + gwf, + head_filerecord=f"{gwfname}.hds", + budget_filerecord=f"{gwfname}.bud", + headprintrecord=[("COLUMNS", 10, "WIDTH", 15, "DIGITS", 6, "GENERAL")], + saverecord=[("HEAD", "LAST"), ("BUDGET", "LAST")], + printrecord=[("HEAD", "LAST"), ("BUDGET", "LAST")], + ) + + # Instantiating MODFLOW 6 groundwater transport package + gwtname = "gwt_" + name + gwt = flopy.mf6.MFModel( + sim, + model_type="gwt6", + modelname=gwtname, + model_nam_file=f"{gwtname}.nam", + ) + gwt.name_file.save_flows = True + + # create iterative model solution and register the gwt model with it + imsgwt = flopy.mf6.ModflowIms( + sim, + print_option="SUMMARY", + outer_dvclose=hclose, + outer_maximum=nouter, + under_relaxation="NONE", + inner_maximum=ninner, + inner_dvclose=hclose, + rcloserecord=rclose, + linear_acceleration="BICGSTAB", + scaling_method="NONE", + reordering_method="NONE", + relaxation_factor=relax, + filename=f"{gwtname}.ims", + ) + sim.register_ims_package(imsgwt, [gwt.name]) + + # Instantiating MODFLOW 6 transport discretization package + flopy.mf6.ModflowGwtdis( + gwt, + nlay=nlay, + nrow=nrow, + ncol=ncol, + delr=delr, + delc=delc, + top=top, + botm=botm, + idomain=1, + filename=f"{gwtname}.dis", + ) + + # Instantiating MODFLOW 6 transport initial concentrations + flopy.mf6.ModflowGwtic(gwt, strt=sconc, filename=f"{gwtname}.ic") + + # Instantiating MODFLOW 6 transport advection package + if mixelm == 0: + scheme = "UPSTREAM" + elif mixelm == -1: + scheme = "TVD" + else: + raise Exception() + flopy.mf6.ModflowGwtadv(gwt, scheme=scheme, filename=f"{gwtname}.adv") + + # Instantiating MODFLOW 6 transport dispersion package + if al != 0: + flopy.mf6.ModflowGwtdsp( gwt, - maxbound=len(cncspd), - stress_period_data=cncspd, - save_flows=False, - pname="CNC-1", - filename=f"{gwtname}.cnc", + xt3d_off=True, + alh=al, + ath1=ath1, + filename=f"{gwtname}.dsp", ) - # Instantiating MODFLOW 6 transport source-sink mixing package - sourcerecarray = [("WEL-1", "AUX", "CONCENTRATION")] - flopy.mf6.ModflowGwtssm( - gwt, sources=sourcerecarray, filename=f"{gwtname}.ssm" - ) - - # Instantiating MODFLOW 6 transport output control package - flopy.mf6.ModflowGwtoc( - gwt, - budget_filerecord=f"{gwtname}.cbc", - concentration_filerecord=f"{gwtname}.ucn", - concentrationprintrecord=[ - ("COLUMNS", 10, "WIDTH", 15, "DIGITS", 6, "GENERAL") - ], - saverecord=[("CONCENTRATION", "LAST"), ("BUDGET", "LAST")], - printrecord=[("CONCENTRATION", "LAST"), ("BUDGET", "LAST")], - ) - - # Instantiating MODFLOW 6 flow-transport exchange mechanism - flopy.mf6.ModflowGwfgwt( - sim, - exgtype="GWF6-GWT6", - exgmnamea=gwfname, - exgmnameb=gwtname, - filename=f"{name}.gwfgwt", - ) - return mf, mt, sim - return None - - -# Function to write model files - - -def write_model(mf2k5, mt3d, sim, silent=True): - if config.writeModel: - mf2k5.write_input() - mt3d.write_input() - sim.write_simulation(silent=silent) - - -# Function to run the model. True is returned if the model runs successfully. + # Instantiating MODFLOW 6 transport mass storage package (formerly "reaction" package in MT3DMS) + flopy.mf6.ModflowGwtmst( + gwt, + porosity=prsity, + first_order_decay=False, + decay=None, + decay_sorbed=None, + sorption=None, + bulk_density=None, + distcoef=None, + filename=f"{gwtname}.mst", + ) + + # Instantiating MODFLOW 6 transport constant concentration package + flopy.mf6.ModflowGwtcnc( + gwt, + maxbound=len(cncspd), + stress_period_data=cncspd, + save_flows=False, + pname="CNC-1", + filename=f"{gwtname}.cnc", + ) + + # Instantiating MODFLOW 6 transport source-sink mixing package + sourcerecarray = [("WEL-1", "AUX", "CONCENTRATION")] + flopy.mf6.ModflowGwtssm(gwt, sources=sourcerecarray, filename=f"{gwtname}.ssm") + + # Instantiating MODFLOW 6 transport output control package + flopy.mf6.ModflowGwtoc( + gwt, + budget_filerecord=f"{gwtname}.cbc", + concentration_filerecord=f"{gwtname}.ucn", + concentrationprintrecord=[("COLUMNS", 10, "WIDTH", 15, "DIGITS", 6, "GENERAL")], + saverecord=[("CONCENTRATION", "LAST"), ("BUDGET", "LAST")], + printrecord=[("CONCENTRATION", "LAST"), ("BUDGET", "LAST")], + ) + + # Instantiating MODFLOW 6 flow-transport exchange mechanism + flopy.mf6.ModflowGwfgwt( + sim, + exgtype="GWF6-GWT6", + exgmnamea=gwfname, + exgmnameb=gwtname, + filename=f"{name}.gwfgwt", + ) + return mf, mt, sim + + +def write_models(mf2k5, mt3d, sim, silent=True): + mf2k5.write_input() + mt3d.write_input() + sim.write_simulation(silent=silent) + + +@timed +def run_models(mf2k5, mt3d, sim, silent=True): + """Run models and assert successful completion.""" + success, buff = mf2k5.run_model(silent=silent, report=True) + assert success, pformat(buff) + success, buff = mt3d.run_model( + silent=silent, normal_msg="Program completed", report=True + ) + assert success, pformat(buff) + success, buff = sim.run_simulation(silent=silent, report=True) + assert success, pformat(buff) + + +# - + +# ### Plotting results +# +# Define functions to plot model results. +# + +# Figure properties +figure_size = (6, 4.5) -@config.timeit -def run_model(mf2k5, mt3d, sim, silent=True): - success = True - if config.runModel: - success, buff = mf2k5.run_model(silent=silent) - success, buff = mt3d.run_model(silent=silent) - success, buff = sim.run_simulation(silent=silent) - if not success: - print(buff) - return success +def plot_results(mt3d, mf6, idx, ax=None): + mt3d_out_path = mt3d.model_ws + mf6.simulation_data.mfpath.get_sim_path() -# Function to plot the model results + # Get the MT3DMS concentration output + fname_mt3d = os.path.join(mt3d_out_path, "MT3D001.UCN") + ucnobj_mt3d = flopy.utils.UcnFile(fname_mt3d) + conc_mt3d = ucnobj_mt3d.get_alldata() + # Get the MF6 concentration output + gwt = mf6.get_model(list(mf6.model_names)[1]) + ucnobj_mf6 = gwt.output.concentration() + conc_mf6 = ucnobj_mf6.get_alldata() -def plot_results(mt3d, mf6, idx, ax=None): - if config.plotModel: - mt3d_out_path = mt3d.model_ws - mf6_out_path = mf6.simulation_data.mfpath.get_sim_path() - mf6.simulation_data.mfpath.get_sim_path() - - # Get the MT3DMS concentration output - fname_mt3d = os.path.join(mt3d_out_path, "MT3D001.UCN") - ucnobj_mt3d = flopy.utils.UcnFile(fname_mt3d) - conc_mt3d = ucnobj_mt3d.get_alldata() - - # Get the MF6 concentration output - gwt = mf6.get_model(list(mf6.model_names)[1]) - ucnobj_mf6 = gwt.output.concentration() - conc_mf6 = ucnobj_mf6.get_alldata() - - # Create figure for scenario - fs = USGSFigure(figure_type="graph", verbose=False) + # Create figure for scenario + with styles.USGSPlot() as fs: sim_name = mf6.name plt.rcParams["lines.dashed_pattern"] = [5.0, 5.0] if ax is None: @@ -518,9 +499,7 @@ def plot_results(mt3d, mf6, idx, ax=None): mm = flopy.plot.PlotMapView(model=mt3d) mm.plot_grid(color=".5", alpha=0.2) - cs1 = mm.contour_array( - conc_mt3d[1], levels=[0.1, 1.0, 10.0, 50.0], colors="k" - ) + cs1 = mm.contour_array(conc_mt3d[1], levels=[0.1, 1.0, 10.0, 50.0], colors="k") plt.clabel(cs1, inline=1, fontsize=10) cs2 = mm.contour_array( conc_mf6[1], @@ -548,43 +527,32 @@ def plot_results(mt3d, mf6, idx, ax=None): # time_units) # ax.legend() letter = chr(ord("@") + idx + 1) - fs.heading(letter=letter, heading=title) + styles.heading(letter=letter, heading=title) - # save figure - if config.plotSave: - fpth = os.path.join( - "..", "figures", f"{sim_name}{config.figure_ext}" - ) + if plot_show: + plt.show() + if plot_save: + fpth = os.path.join("..", "figures", f"{sim_name}.png") fig.savefig(fpth) -# ### Function that wraps all of the steps for each MT3DMS Example 10 Problem scenario +# - + +# ### Running the example # -# 1. build_model, -# 2. write_model, -# 3. run_model, and -# 4. plot_results. +# Define and invoke a function to run the example scenario, then plot results. +# + def scenario(idx, silent=True): - mf2k5, mt3d, sim = build_model(example_name) - write_model(mf2k5, mt3d, sim, silent=silent) - success = run_model(mf2k5, mt3d, sim, silent=silent) - - if success: + mf2k5, mt3d, sim = build_models(example_name) + if write: + write_models(mf2k5, mt3d, sim, silent=silent) + if run: + run_models(mf2k5, mt3d, sim, silent=silent) + if plot: plot_results(mt3d, sim, idx) -# nosetest - exclude block from this nosetest to the next nosetest -def test_01(): - scenario(0, silent=False) - - -# nosetest end - -if __name__ == "__main__": - # ### Two-Dimensional Transport in a Uniform Flow Field - # - # Describe what is plotted here... - - scenario(0) +scenario(0) +# - diff --git a/scripts/ex-gwt-mt3dms-p04.py b/scripts/ex-gwt-mt3dms-p04.py index 14f796a63..50be16891 100644 --- a/scripts/ex-gwt-mt3dms-p04.py +++ b/scripts/ex-gwt-mt3dms-p04.py @@ -18,64 +18,60 @@ # 9. Two-Dimensional Application Example # 10. Three-Dimensional Field Case Study +# ### Initial setup +# +# Import dependencies, define the example name and workspace, and read settings from environment variables. -# ### MODFLOW 6 GWT MT3DMS Example 4 Problem Setup - -# Append to system path to include the common subdirectory - +# + import os -import sys - -sys.path.append(os.path.join("..", "common")) +import pathlib as pl +from pprint import pformat -# Imports - -import config import flopy import matplotlib.pyplot as plt import numpy as np -from figspecs import USGSFigure - -mf6exe = "mf6" -exe_name_mf = "mf2005" -exe_name_mt = "mt3dms" - -# Set figure properties specific to this problem - -figure_size = (6, 4.5) - -# Base simulation and model name and workspace - -ws = config.base_ws +from flopy.plot.styles import styles +from modflow_devtools.misc import get_env, timed + +# Settings from environment variables +write = get_env("WRITE", True) +run = get_env("RUN", True) +plot = get_env("PLOT", True) +plot_show = get_env("PLOT_SHOW", True) +plot_save = get_env("PLOT_SAVE", True) + +# Example name and base workspace +workspace = pl.Path("../examples") example_name = "ex-gwt-mt3dms-p04" +# - -# Set scenario parameters (make sure there is at least one blank line before next item) -# This entire dictionary is passed to _build_model()_ using the kwargs argument +# ### Define parameters +# +# Define model units, parameters and other settings. +# + +# Set scenario parameters (make sure there is at least one blank line before next item) +# This entire dictionary is passed to _build_models()_ using the kwargs argument parameters = { "ex-gwt-mt3dms-p04a": {"mixelm": 0}, "ex-gwt-mt3dms-p04b": {"mixelm": -1}, "ex-gwt-mt3dms-p04c": {"mixelm": 1}, } + # Scenario parameter units -# # add parameter_units to add units to the scenario parameter table that is automatically # built and used by the .tex input - parameter_units = {"mixelm": "unitless"} # Setup some lists that will assist with labeling contours in the figures - legendtxt_mod1 = ["MT3DMS - FD", "MT3DMS - TVD", "MT3DMS - MOC", "MF6 - FD"] legendtxt_mod2 = ["MF6 - FD", "MF6 - TVD", "MF6 - FD", "MF6 - TVD"] # Model units - length_units = "meters" time_units = "days" -# Table - +# Model parameters nlay = 1 # Number of layers nrow = 100 # Number of rows ncol = 100 # Number of columns @@ -93,7 +89,6 @@ dmcoef = 1.0e-9 # Molecular diffusion coefficient ($m^2/d$) # Additional model input - perlen = [1000.0] nper = len(perlen) nstp = [100] @@ -107,7 +102,6 @@ icelltype = 0 # Initial conditions - Lx = (ncol - 1) * delr Ly = (nrow - 1) * delc Ls = np.sqrt(Lx**2 + Ly**2) @@ -119,7 +113,6 @@ c = 1 # Active model domain - ibound_mf2k5 = np.ones((nlay, nrow, ncol), dtype=int) * -1 ibound_mf2k5[:, 1 : nrow - 1, 1 : ncol - 1] = 1 idomain = np.ones((nlay, nrow, ncol), dtype=int) @@ -127,20 +120,17 @@ # Boundary conditions # MF2K5 pumping info: - welspd = {0: [[0, 79, 20, qwell]]} # Well pumping info for MF2K5 spd = {0: [0, 79, 20, cwell, 2]} # Well pupming info for MT3DMS # MF6 pumping information # (k, i, j), flow, conc - spd_mf6 = {0: [[(0, 79, 20), qwell, cwell]]} # MF6 constant head boundaries are defined below because additional variables # from the instantiation of model properties are required # Solver settings - nouter, ninner = 100, 300 hclose, rclose, relax = 1e-6, 1e-6, 1.0 @@ -158,382 +148,368 @@ npsink = nph # Static temporal data used by TDIS file - tdis_rc = [] tdis_rc.append((perlen, nstp, 1.0)) +# - - -# ### Functions to build, write, and run models and plot MT3DMS Example 10 Problem results +# ### Model setup # -# MODFLOW 6 flopy simulation object (sim) is returned if building the model - - -def build_model(sim_name, mixelm=0, silent=False): - if config.buildModel: - mt3d_ws = os.path.join(ws, sim_name, "mt3d") - modelname_mf = "p04-mf" - - # Instantiate the MODFLOW model - mf = flopy.modflow.Modflow( - modelname=modelname_mf, model_ws=mt3d_ws, exe_name=exe_name_mf - ) - - # Instantiate discretization package - # units: itmuni=4 (days), lenuni=2 (m) - flopy.modflow.ModflowDis( - mf, - nlay=nlay, - nrow=nrow, - ncol=ncol, - delr=delr, - delc=delc, - top=top, - botm=botm, - nper=nper, - nstp=nstp, - perlen=perlen, - itmuni=4, - lenuni=2, - ) - - # Instantiate basic package - x = mf.modelgrid.xcellcenters - y = mf.modelgrid.ycellcenters - d = abs(a * x + b * y + c) / np.sqrt(2) - strt = h1 - d / Ls * h1 - flopy.modflow.ModflowBas(mf, ibound=ibound_mf2k5, strt=strt) - - # Instantiate layer property flow package - flopy.modflow.ModflowLpf(mf, hk=k11, laytyp=icelltype) - - # Instantiate well package - flopy.modflow.ModflowWel(mf, stress_period_data=welspd) - - # Instantiate solver package - flopy.modflow.ModflowPcg(mf) - - # Instantiate link mass transport package (for writing linker file) - flopy.modflow.ModflowLmt(mf) - - # Transport - modelname_mt = "p04-mt" - mt = flopy.mt3d.Mt3dms( - modelname=modelname_mt, - model_ws=mt3d_ws, - exe_name=exe_name_mt, - modflowmodel=mf, - ) - - # Instantiate basic transport package - flopy.mt3d.Mt3dBtn( - mt, - icbund=icbund, - prsity=prsity, - sconc=sconc, - nstp=nstp, - perlen=perlen, - ) - - # Instatiate the advection package - flopy.mt3d.Mt3dAdv( - mt, - mixelm=mixelm, - dceps=dceps, - nplane=nplane, - npl=npl, - nph=nph, - npmin=npmin, - npmax=npmax, - nlsink=nlsink, - npsink=npsink, - percel=percel, - itrack=itrack, - wd=wd, - ) - - # Instantiate the dispersion package - flopy.mt3d.Mt3dDsp(mt, al=al, trpt=trpt, dmcoef=dmcoef) - - # Instantiate the source/sink mixing package - flopy.mt3d.Mt3dSsm(mt, stress_period_data=spd) - - # Instantiate the GCG solver in MT3DMS - flopy.mt3d.Mt3dGcg(mt, mxiter=10) - - # MODFLOW 6 - name = "p04-mf6" - gwfname = "gwf-" + name - sim_ws = os.path.join(ws, sim_name) - sim = flopy.mf6.MFSimulation( - sim_name=sim_name, sim_ws=sim_ws, exe_name=mf6exe - ) - - # Instantiating MODFLOW 6 time discretization - tdis_rc = [] - for i in range(nper): - tdis_rc.append((perlen[i], nstp[i], tsmult[i])) - flopy.mf6.ModflowTdis( - sim, nper=nper, perioddata=tdis_rc, time_units=time_units - ) - - # Instantiating MODFLOW 6 groundwater flow model - gwf = flopy.mf6.ModflowGwf( - sim, - modelname=gwfname, - save_flows=True, - model_nam_file=f"{gwfname}.nam", - ) - - # Instantiating MODFLOW 6 solver for flow model - imsgwf = flopy.mf6.ModflowIms( - sim, - print_option="SUMMARY", - outer_dvclose=hclose, - outer_maximum=nouter, - under_relaxation="NONE", - inner_maximum=ninner, - inner_dvclose=hclose, - rcloserecord=rclose, - linear_acceleration="CG", - scaling_method="NONE", - reordering_method="NONE", - relaxation_factor=relax, - filename=f"{gwfname}.ims", - ) - sim.register_ims_package(imsgwf, [gwf.name]) - - # Instantiating MODFLOW 6 discretization package - flopy.mf6.ModflowGwfdis( - gwf, - length_units=length_units, - nlay=nlay, - nrow=nrow, - ncol=ncol, - delr=delr, - delc=delc, - top=top, - botm=botm, - idomain=idomain, - filename=f"{gwfname}.dis", - ) - - # Instantiating MODFLOW 6 node-property flow package - flopy.mf6.ModflowGwfnpf( - gwf, - save_flows=False, - icelltype=icelltype, - k=k11, - k33=k33, - save_specific_discharge=True, - filename=f"{gwfname}.npf", - ) - - # Instantiating MODFLOW 6 storage package (steady flow conditions, so no actual storage, using to print values in .lst file) - flopy.mf6.ModflowGwfsto(gwf, ss=0, sy=0, filename=f"{gwfname}.sto") - - # Instantiating MODFLOW 6 initial conditions package for flow model - x = gwf.modelgrid.xcellcenters - y = gwf.modelgrid.ycellcenters - d = abs(a * x + b * y + c) / np.sqrt(2) - strt = h1 - d / Ls * h1 - flopy.mf6.ModflowGwfic(gwf, strt=strt, filename=f"{gwfname}.ic") - - # Instantiating MODFLOW 6 constant head package - chdspd = [] - # Loop through the left & right sides. - for i in np.arange(nrow): - chdspd.append([(0, i, 0), strt[i, 0]]) - chdspd.append([(0, i, ncol - 1), strt[i, ncol - 1]]) - # Loop through the top & bottom while omitting the corner cells - for j in np.arange(1, ncol - 1): - chdspd.append([(0, 0, j), strt[0, j]]) - chdspd.append([(0, nrow - 1, j), strt[nrow - 1, j]]) - chdspd = {0: chdspd} - flopy.mf6.ModflowGwfchd( - gwf, - maxbound=len(chdspd), - stress_period_data=chdspd, - save_flows=False, - pname="CHD-1", - filename=f"{gwfname}.chd", - ) - - # Instantiate the wel package - flopy.mf6.ModflowGwfwel( - gwf, - print_input=True, - print_flows=True, - stress_period_data=spd_mf6, - save_flows=False, - auxiliary="CONCENTRATION", - pname="WEL-1", - filename=f"{gwfname}.wel", - ) - - # Instantiating MODFLOW 6 output control package for flow model - flopy.mf6.ModflowGwfoc( - gwf, - head_filerecord=f"{gwfname}.hds", - budget_filerecord=f"{gwfname}.bud", - headprintrecord=[ - ("COLUMNS", 10, "WIDTH", 15, "DIGITS", 6, "GENERAL") - ], - saverecord=[("HEAD", "LAST"), ("BUDGET", "LAST")], - printrecord=[("HEAD", "LAST"), ("BUDGET", "LAST")], - ) - - # Instantiating MODFLOW 6 groundwater transport package - gwtname = "gwt_" + name - gwt = flopy.mf6.MFModel( - sim, - model_type="gwt6", - modelname=gwtname, - model_nam_file=f"{gwtname}.nam", - ) - gwt.name_file.save_flows = True - - # create iterative model solution and register the gwt model with it - imsgwt = flopy.mf6.ModflowIms( - sim, - print_option="SUMMARY", - outer_dvclose=hclose, - outer_maximum=nouter, - under_relaxation="NONE", - inner_maximum=ninner, - inner_dvclose=hclose, - rcloserecord=rclose, - linear_acceleration="BICGSTAB", - scaling_method="NONE", - reordering_method="NONE", - relaxation_factor=relax, - filename=f"{gwtname}.ims", - ) - sim.register_ims_package(imsgwt, [gwt.name]) - - # Instantiating MODFLOW 6 transport discretization package - flopy.mf6.ModflowGwtdis( - gwt, - nlay=nlay, - nrow=nrow, - ncol=ncol, - delr=delr, - delc=delc, - top=top, - botm=botm, - idomain=1, - filename=f"{gwtname}.dis", - ) - - # Instantiating MODFLOW 6 transport initial concentrations - flopy.mf6.ModflowGwtic(gwt, strt=sconc, filename=f"{gwtname}.ic") - - # Instantiating MODFLOW 6 transport advection package - if mixelm >= 0: - scheme = "UPSTREAM" - elif mixelm == -1: - scheme = "TVD" - else: - raise Exception() - flopy.mf6.ModflowGwtadv(gwt, scheme=scheme, filename=f"{gwtname}.adv") - - # Instantiating MODFLOW 6 transport dispersion package - if al != 0: - flopy.mf6.ModflowGwtdsp( - gwt, - alh=al, - ath1=ath1, - filename=f"{gwtname}.dsp", - ) - - # Instantiating MODFLOW 6 transport mass storage package (formerly "reaction" package in MT3DMS) - flopy.mf6.ModflowGwtmst( - gwt, - porosity=prsity, - first_order_decay=False, - decay=None, - decay_sorbed=None, - sorption=None, - bulk_density=None, - distcoef=None, - filename=f"{gwtname}.mst", - ) - - # Instantiating MODFLOW 6 transport source-sink mixing package - sourcerecarray = [("WEL-1", "AUX", "CONCENTRATION")] - flopy.mf6.ModflowGwtssm( - gwt, sources=sourcerecarray, filename=f"{gwtname}.ssm" - ) - - # Instantiating MODFLOW 6 transport output control package - flopy.mf6.ModflowGwtoc( +# Define functions to build models, write input files, and run the simulation. + + +# + +def build_models(sim_name, mixelm=0, silent=False): + mt3d_ws = os.path.join(workspace, sim_name, "mt3d") + modelname_mf = "p04-mf" + + # Instantiate the MODFLOW model + mf = flopy.modflow.Modflow( + modelname=modelname_mf, model_ws=mt3d_ws, exe_name="mf2005" + ) + + # Instantiate discretization package + # units: itmuni=4 (days), lenuni=2 (m) + flopy.modflow.ModflowDis( + mf, + nlay=nlay, + nrow=nrow, + ncol=ncol, + delr=delr, + delc=delc, + top=top, + botm=botm, + nper=nper, + nstp=nstp, + perlen=perlen, + itmuni=4, + lenuni=2, + ) + + # Instantiate basic package + x = mf.modelgrid.xcellcenters + y = mf.modelgrid.ycellcenters + d = abs(a * x + b * y + c) / np.sqrt(2) + strt = h1 - d / Ls * h1 + flopy.modflow.ModflowBas(mf, ibound=ibound_mf2k5, strt=strt) + + # Instantiate layer property flow package + flopy.modflow.ModflowLpf(mf, hk=k11, laytyp=icelltype) + + # Instantiate well package + flopy.modflow.ModflowWel(mf, stress_period_data=welspd) + + # Instantiate solver package + flopy.modflow.ModflowPcg(mf) + + # Instantiate link mass transport package (for writing linker file) + flopy.modflow.ModflowLmt(mf) + + # Transport + modelname_mt = "p04-mt" + mt = flopy.mt3d.Mt3dms( + modelname=modelname_mt, + model_ws=mt3d_ws, + exe_name="mt3dms", + modflowmodel=mf, + ) + + # Instantiate basic transport package + flopy.mt3d.Mt3dBtn( + mt, + icbund=icbund, + prsity=prsity, + sconc=sconc, + nstp=nstp, + perlen=perlen, + ) + + # Instatiate the advection package + flopy.mt3d.Mt3dAdv( + mt, + mixelm=mixelm, + dceps=dceps, + nplane=nplane, + npl=npl, + nph=nph, + npmin=npmin, + npmax=npmax, + nlsink=nlsink, + npsink=npsink, + percel=percel, + itrack=itrack, + wd=wd, + ) + + # Instantiate the dispersion package + flopy.mt3d.Mt3dDsp(mt, al=al, trpt=trpt, dmcoef=dmcoef) + + # Instantiate the source/sink mixing package + flopy.mt3d.Mt3dSsm(mt, stress_period_data=spd) + + # Instantiate the GCG solver in MT3DMS + flopy.mt3d.Mt3dGcg(mt, mxiter=10) + + # MODFLOW 6 + name = "p04-mf6" + gwfname = "gwf-" + name + sim_ws = os.path.join(workspace, sim_name) + sim = flopy.mf6.MFSimulation(sim_name=sim_name, sim_ws=sim_ws, exe_name="mf6") + + # Instantiating MODFLOW 6 time discretization + tdis_rc = [] + for i in range(nper): + tdis_rc.append((perlen[i], nstp[i], tsmult[i])) + flopy.mf6.ModflowTdis(sim, nper=nper, perioddata=tdis_rc, time_units=time_units) + + # Instantiating MODFLOW 6 groundwater flow model + gwf = flopy.mf6.ModflowGwf( + sim, + modelname=gwfname, + save_flows=True, + model_nam_file=f"{gwfname}.nam", + ) + + # Instantiating MODFLOW 6 solver for flow model + imsgwf = flopy.mf6.ModflowIms( + sim, + print_option="SUMMARY", + outer_dvclose=hclose, + outer_maximum=nouter, + under_relaxation="NONE", + inner_maximum=ninner, + inner_dvclose=hclose, + rcloserecord=rclose, + linear_acceleration="CG", + scaling_method="NONE", + reordering_method="NONE", + relaxation_factor=relax, + filename=f"{gwfname}.ims", + ) + sim.register_ims_package(imsgwf, [gwf.name]) + + # Instantiating MODFLOW 6 discretization package + flopy.mf6.ModflowGwfdis( + gwf, + length_units=length_units, + nlay=nlay, + nrow=nrow, + ncol=ncol, + delr=delr, + delc=delc, + top=top, + botm=botm, + idomain=idomain, + filename=f"{gwfname}.dis", + ) + + # Instantiating MODFLOW 6 node-property flow package + flopy.mf6.ModflowGwfnpf( + gwf, + save_flows=False, + icelltype=icelltype, + k=k11, + k33=k33, + save_specific_discharge=True, + filename=f"{gwfname}.npf", + ) + + # Instantiating MODFLOW 6 storage package (steady flow conditions, so no actual storage, using to print values in .lst file) + flopy.mf6.ModflowGwfsto(gwf, ss=0, sy=0, filename=f"{gwfname}.sto") + + # Instantiating MODFLOW 6 initial conditions package for flow model + x = gwf.modelgrid.xcellcenters + y = gwf.modelgrid.ycellcenters + d = abs(a * x + b * y + c) / np.sqrt(2) + strt = h1 - d / Ls * h1 + flopy.mf6.ModflowGwfic(gwf, strt=strt, filename=f"{gwfname}.ic") + + # Instantiating MODFLOW 6 constant head package + chdspd = [] + # Loop through the left & right sides. + for i in np.arange(nrow): + chdspd.append([(0, i, 0), strt[i, 0]]) + chdspd.append([(0, i, ncol - 1), strt[i, ncol - 1]]) + # Loop through the top & bottom while omitting the corner cells + for j in np.arange(1, ncol - 1): + chdspd.append([(0, 0, j), strt[0, j]]) + chdspd.append([(0, nrow - 1, j), strt[nrow - 1, j]]) + chdspd = {0: chdspd} + flopy.mf6.ModflowGwfchd( + gwf, + maxbound=len(chdspd), + stress_period_data=chdspd, + save_flows=False, + pname="CHD-1", + filename=f"{gwfname}.chd", + ) + + # Instantiate the wel package + flopy.mf6.ModflowGwfwel( + gwf, + print_input=True, + print_flows=True, + stress_period_data=spd_mf6, + save_flows=False, + auxiliary="CONCENTRATION", + pname="WEL-1", + filename=f"{gwfname}.wel", + ) + + # Instantiating MODFLOW 6 output control package for flow model + flopy.mf6.ModflowGwfoc( + gwf, + head_filerecord=f"{gwfname}.hds", + budget_filerecord=f"{gwfname}.bud", + headprintrecord=[("COLUMNS", 10, "WIDTH", 15, "DIGITS", 6, "GENERAL")], + saverecord=[("HEAD", "LAST"), ("BUDGET", "LAST")], + printrecord=[("HEAD", "LAST"), ("BUDGET", "LAST")], + ) + + # Instantiating MODFLOW 6 groundwater transport package + gwtname = "gwt_" + name + gwt = flopy.mf6.MFModel( + sim, + model_type="gwt6", + modelname=gwtname, + model_nam_file=f"{gwtname}.nam", + ) + gwt.name_file.save_flows = True + + # create iterative model solution and register the gwt model with it + imsgwt = flopy.mf6.ModflowIms( + sim, + print_option="SUMMARY", + outer_dvclose=hclose, + outer_maximum=nouter, + under_relaxation="NONE", + inner_maximum=ninner, + inner_dvclose=hclose, + rcloserecord=rclose, + linear_acceleration="BICGSTAB", + scaling_method="NONE", + reordering_method="NONE", + relaxation_factor=relax, + filename=f"{gwtname}.ims", + ) + sim.register_ims_package(imsgwt, [gwt.name]) + + # Instantiating MODFLOW 6 transport discretization package + flopy.mf6.ModflowGwtdis( + gwt, + nlay=nlay, + nrow=nrow, + ncol=ncol, + delr=delr, + delc=delc, + top=top, + botm=botm, + idomain=1, + filename=f"{gwtname}.dis", + ) + + # Instantiating MODFLOW 6 transport initial concentrations + flopy.mf6.ModflowGwtic(gwt, strt=sconc, filename=f"{gwtname}.ic") + + # Instantiating MODFLOW 6 transport advection package + if mixelm >= 0: + scheme = "UPSTREAM" + elif mixelm == -1: + scheme = "TVD" + else: + raise Exception() + flopy.mf6.ModflowGwtadv(gwt, scheme=scheme, filename=f"{gwtname}.adv") + + # Instantiating MODFLOW 6 transport dispersion package + if al != 0: + flopy.mf6.ModflowGwtdsp( gwt, - budget_filerecord=f"{gwtname}.cbc", - concentration_filerecord=f"{gwtname}.ucn", - concentrationprintrecord=[ - ("COLUMNS", 10, "WIDTH", 15, "DIGITS", 6, "GENERAL") - ], - saverecord=[("CONCENTRATION", "LAST"), ("BUDGET", "LAST")], - printrecord=[("CONCENTRATION", "LAST"), ("BUDGET", "LAST")], - ) - - # Instantiating MODFLOW 6 flow-transport exchange mechanism - flopy.mf6.ModflowGwfgwt( - sim, - exgtype="GWF6-GWT6", - exgmnamea=gwfname, - exgmnameb=gwtname, - filename=f"{name}.gwfgwt", + alh=al, + ath1=ath1, + filename=f"{gwtname}.dsp", ) - return mf, mt, sim - return None - - -# Function to write model files - -def write_model(mf2k5, mt3d, sim, silent=True): - if config.writeModel: - mf2k5.write_input() - mt3d.write_input() - sim.write_simulation(silent=silent) - - -# Function to run the model. True is returned if the model runs successfully. + # Instantiating MODFLOW 6 transport mass storage package (formerly "reaction" package in MT3DMS) + flopy.mf6.ModflowGwtmst( + gwt, + porosity=prsity, + first_order_decay=False, + decay=None, + decay_sorbed=None, + sorption=None, + bulk_density=None, + distcoef=None, + filename=f"{gwtname}.mst", + ) + + # Instantiating MODFLOW 6 transport source-sink mixing package + sourcerecarray = [("WEL-1", "AUX", "CONCENTRATION")] + flopy.mf6.ModflowGwtssm(gwt, sources=sourcerecarray, filename=f"{gwtname}.ssm") + + # Instantiating MODFLOW 6 transport output control package + flopy.mf6.ModflowGwtoc( + gwt, + budget_filerecord=f"{gwtname}.cbc", + concentration_filerecord=f"{gwtname}.ucn", + concentrationprintrecord=[("COLUMNS", 10, "WIDTH", 15, "DIGITS", 6, "GENERAL")], + saverecord=[("CONCENTRATION", "LAST"), ("BUDGET", "LAST")], + printrecord=[("CONCENTRATION", "LAST"), ("BUDGET", "LAST")], + ) + + # Instantiating MODFLOW 6 flow-transport exchange mechanism + flopy.mf6.ModflowGwfgwt( + sim, + exgtype="GWF6-GWT6", + exgmnamea=gwfname, + exgmnameb=gwtname, + filename=f"{name}.gwfgwt", + ) + return mf, mt, sim + + +def write_models(mf2k5, mt3d, sim, silent=True): + mf2k5.write_input() + mt3d.write_input() + sim.write_simulation(silent=silent) + + +@timed +def run_models(mf2k5, mt3d, sim, silent=True): + success, buff = mf2k5.run_model(silent=silent, report=True) + assert success, pformat(buff) + success, buff = mt3d.run_model( + silent=silent, normal_msg="Program completed", report=True + ) + assert success, pformat(buff) + success, buff = sim.run_simulation(silent=silent, report=True) + assert success, pformat(buff) + + +# - +# ### Plotting results +# +# Define functions to plot model results. +# + +# Figure properties +figure_size = (6, 4.5) -@config.timeit -def run_model(mf2k5, mt3d, sim, silent=True): - success = True - if config.runModel: - success, buff = mf2k5.run_model(silent=silent) - success, buff = mt3d.run_model(silent=silent) - success, buff = sim.run_simulation(silent=silent) - if not success: - print(buff) - return success +def plot_results(mt3d, mf6, idx, leglab1, leglab2, ax=None): + mt3d_out_path = mt3d.model_ws + mf6.simulation_data.mfpath.get_sim_path() -# Function to plot the model results + # Get the MT3DMS concentration output + fname_mt3d = os.path.join(mt3d_out_path, "MT3D001.UCN") + ucnobj_mt3d = flopy.utils.UcnFile(fname_mt3d) + conc_mt3d = ucnobj_mt3d.get_alldata() + # Get the MF6 concentration output + gwt = mf6.get_model(list(mf6.model_names)[1]) + ucnobj_mf6 = gwt.output.concentration() + conc_mf6 = ucnobj_mf6.get_alldata() -def plot_results(mt3d, mf6, idx, leglab1, leglab2, ax=None): - if config.plotModel: - mt3d_out_path = mt3d.model_ws - mf6_out_path = mf6.simulation_data.mfpath.get_sim_path() - mf6.simulation_data.mfpath.get_sim_path() - - # Get the MT3DMS concentration output - fname_mt3d = os.path.join(mt3d_out_path, "MT3D001.UCN") - ucnobj_mt3d = flopy.utils.UcnFile(fname_mt3d) - conc_mt3d = ucnobj_mt3d.get_alldata() - - # Get the MF6 concentration output - gwt = mf6.get_model(list(mf6.model_names)[1]) - ucnobj_mf6 = gwt.output.concentration() - conc_mf6 = ucnobj_mf6.get_alldata() - - # Create figure for scenario - fs = USGSFigure(figure_type="graph", verbose=False) + # Create figure for scenario + with styles.USGSPlot() as fs: sim_name = mf6.name plt.rcParams["lines.dashed_pattern"] = [5.0, 5.0] if ax is None: @@ -546,9 +522,7 @@ def plot_results(mt3d, mf6, idx, leglab1, leglab2, ax=None): levels = [0.15, 1.0, 2.0, 5.0] mm = flopy.plot.PlotMapView(model=mt3d) - cf = plt.contourf( - x, y, conc_mt3d[0, 0, :, :], levels=levels, alpha=0.5 - ) + cf = plt.contourf(x, y, conc_mt3d[0, 0, :, :], levels=levels, alpha=0.5) cbar = plt.colorbar(cf, shrink=0.25) cbar.ax.set_title(leglab1) @@ -566,64 +540,46 @@ def plot_results(mt3d, mf6, idx, leglab1, leglab2, ax=None): title = "MT3DMS-MF6 Comparison" letter = chr(ord("@") + idx + 1) - fs.heading(letter=letter, heading=title) + styles.heading(letter=letter, heading=title) - # save figure - if config.plotSave: - fpth = os.path.join( - "..", "figures", f"{sim_name}{config.figure_ext}" - ) + if plot_show: + plt.show() + if plot_save: + fpth = os.path.join("..", "figures", f"{sim_name}.png") fig.savefig(fpth) -# ### Function that wraps all of the steps for each MT3DMS Example 10 Problem scenario -# -# 1. build_model, -# 2. write_model, -# 3. run_model, and -# 4. plot_results. +# - + +# ### Plotting results # +# Define functions to plot model results. +# + def scenario(idx, silent=True): key = list(parameters.keys())[idx] parameter_dict = parameters[key] - mf2k5, mt3d, sim = build_model(key, **parameter_dict) - - write_model(mf2k5, mt3d, sim, silent=silent) - - success = run_model(mf2k5, mt3d, sim, silent=silent) - - if success: + mf2k5, mt3d, sim = build_models(key, **parameter_dict) + if write: + write_models(mf2k5, mt3d, sim, silent=silent) + if run: + run_models(mf2k5, mt3d, sim, silent=silent) + if plot: plot_results(mt3d, sim, idx, legendtxt_mod1[idx], legendtxt_mod2[idx]) -# nosetest - exclude block from this nosetest to the next nosetest -def test_01(): - scenario(0, silent=False) - - -def test_02(): - scenario(1, silent=False) - - -def test_03(): - scenario(2, silent=False) - - -# nosetest end +# - -if __name__ == "__main__": - # ### Two-Dimensional Transport in a Diagonal Flow Field - # Compares the standard finite difference solutions between MT3D MF 6 +# Compares the standard finite difference solutions between MT3D and MF6. - scenario(0) +scenario(0) - # Compares the respective TVD solutions between MT3D MF 6 +# Compares the respective TVD solutions between MT3D and MF6. - scenario(1) +scenario(1) - # Compares a MOC solution in MT3D with the standard FD method of MF 6 +# Compares a MOC solution in MT3D with the standard FD method of MF6. - scenario(2) +scenario(2) diff --git a/scripts/ex-gwt-mt3dms-p05.py b/scripts/ex-gwt-mt3dms-p05.py index 2ec8cd7ec..4066c8b66 100644 --- a/scripts/ex-gwt-mt3dms-p05.py +++ b/scripts/ex-gwt-mt3dms-p05.py @@ -18,44 +18,43 @@ # 9. Two-Dimensional Application Example # 10. Three-Dimensional Field Case Study +# ### Initial setup +# +# Import dependencies, define the example name and workspace, and read settings from environment variables. -# ### MODFLOW 6 GWT MT3DMS Example 5 Problem Setup - -# Append to system path to include the common subdirectory - +# + import os -import sys - -sys.path.append(os.path.join("..", "common")) +import pathlib as pl +from pprint import pformat -# Imports - -import config import flopy import matplotlib.pyplot as plt import numpy as np -from figspecs import USGSFigure - -mf6exe = "mf6" -exe_name_mf = "mf2005" -exe_name_mt = "mt3dms" - -# Set figure properties specific to this problem - -figure_size = (6, 4.5) - -# Base simulation and model name and workspace - -ws = config.base_ws +from flopy.plot.styles import styles +from modflow_devtools.misc import get_env, timed + +# Settings from environment variables +write = get_env("WRITE", True) +run = get_env("RUN", True) +plot = get_env("PLOT", True) +plot_show = get_env("PLOT_SHOW", True) +plot_save = get_env("PLOT_SAVE", True) + +# Example name and base workspace +workspace = pl.Path("../examples") example_name = "ex-gwt-mt3dms-p05" +# - -# Model units +# ### Define parameters +# +# Define model units, parameters and other settings. +# + +# Model units length_units = "meters" time_units = "days" -# Table - +# Model parameters nlay = 1 # Number of layers nrow = 31 # Number of rows ncol = 31 # Number of columns @@ -73,7 +72,6 @@ dmcoef = 1.0e-9 # Molecular diffusion coefficient ($m^2/d$) # Additional model input - perlen = [27] nper = len(perlen) nstp = [27] @@ -89,7 +87,6 @@ strt = np.zeros((nlay, nrow, ncol), dtype=float) # Active model domain - ibound_mf2k5 = np.ones((nlay, nrow, ncol), dtype=int) * -1 ibound_mf2k5[:, 1 : nrow - 1, 1 : ncol - 1] = 1 idomain = np.ones((nlay, nrow, ncol), dtype=int) @@ -97,17 +94,14 @@ # Boundary conditions # MF2K5 pumping info: - welspd = {0: [[0, 15, 15, qwell]]} # Well pumping info for MF2K5 spd = {0: [0, 15, 15, cwell, -1]} # Well pupming info for MT3DMS # MF6 pumping information - # (k, i, j), flow, conc spd_mf6 = {0: [[(0, 15, 15), qwell, c0]]} # MF6 constant head boundaries: - chdspd = [] # Loop through the left & right sides. for i in np.arange(nrow): @@ -121,7 +115,6 @@ chdspd = {0: chdspd} # Solver settings - nouter, ninner = 100, 300 hclose, rclose, relax = 1e-6, 1e-6, 1.0 percel = 1.0 # HMOC parameters @@ -137,379 +130,367 @@ nlsink = nplane npsink = nph -# Static temporal data used by TDIS file - +# Time discretization tdis_rc = [] tdis_rc.append((perlen, nstp, 1.0)) +# - -# ### Functions to build, write, and run models and plot MT3DMS Example 10 Problem results +# ### Model setup # -# MODFLOW 6 flopy simulation object (sim) is returned if building the model - - -def build_model(sim_name, mixelm=0, silent=False): - if config.buildModel: - mt3d_ws = os.path.join(ws, sim_name, "mt3d") - modelname_mf = "p05-mf" - - # Instantiate the MODFLOW model - mf = flopy.modflow.Modflow( - modelname=modelname_mf, model_ws=mt3d_ws, exe_name=exe_name_mf - ) - - # Instantiate discretization package - # units: itmuni=4 (days), lenuni=2 (m) - flopy.modflow.ModflowDis( - mf, - nlay=nlay, - nrow=nrow, - ncol=ncol, - delr=delr, - delc=delc, - top=top, - botm=botm, - nper=nper, - nstp=nstp, - perlen=perlen, - itmuni=4, - lenuni=2, - ) - - # Instantiate basic package - flopy.modflow.ModflowBas(mf, ibound=ibound_mf2k5, strt=strt) - - # Instantiate layer property flow package - flopy.modflow.ModflowLpf(mf, hk=k11, laytyp=icelltype) - - # Instantiate well package - flopy.modflow.ModflowWel(mf, stress_period_data=welspd) - - # Instantiate solver package - flopy.modflow.ModflowSip(mf) - - # Instantiate link mass transport package (for writing linker file) - flopy.modflow.ModflowLmt(mf) - - # Transport - modelname_mt = "p05-mt" - mt = flopy.mt3d.Mt3dms( - modelname=modelname_mt, - model_ws=mt3d_ws, - exe_name=exe_name_mt, - modflowmodel=mf, - ) - - # Instantiate basic transport package - flopy.mt3d.Mt3dBtn( - mt, - icbund=icbund, - prsity=prsity, - sconc=sconc, - nper=nper, - nstp=nstp, - perlen=perlen, - dt0=dt0, - tsmult=tsmult, - ) - - # Instatiate the advection package - flopy.mt3d.Mt3dAdv( - mt, - mixelm=mixelm, - dceps=dceps, - nplane=nplane, - npl=npl, - nph=nph, - npmin=npmin, - npmax=npmax, - nlsink=nlsink, - npsink=npsink, - percel=percel, - itrack=itrack, - wd=wd, - ) - - # Instantiate the dispersion package - flopy.mt3d.Mt3dDsp(mt, al=al, trpt=trpt, dmcoef=dmcoef) - - # Instantiate the source/sink mixing package - flopy.mt3d.Mt3dSsm(mt, stress_period_data=spd) - - # Instantiate the GCG solver in MT3DMS - flopy.mt3d.Mt3dGcg(mt, mxiter=10) - - # MODFLOW 6 - name = "p05-mf6" - gwfname = "gwf-" + name - sim_ws = os.path.join(ws, sim_name) - sim = flopy.mf6.MFSimulation( - sim_name=sim_name, sim_ws=sim_ws, exe_name=mf6exe - ) - - # Instantiating MODFLOW 6 time discretization - tdis_rc = [] - for i in range(nper): - tdis_rc.append((perlen[i], nstp[i], tsmult[i])) - flopy.mf6.ModflowTdis( - sim, nper=nper, perioddata=tdis_rc, time_units=time_units - ) - - # Instantiating MODFLOW 6 groundwater flow model - gwf = flopy.mf6.ModflowGwf( - sim, - modelname=gwfname, - save_flows=True, - model_nam_file=f"{gwfname}.nam", - ) - - # Instantiating MODFLOW 6 solver for flow model - imsgwf = flopy.mf6.ModflowIms( - sim, - print_option="SUMMARY", - outer_dvclose=hclose, - outer_maximum=nouter, - under_relaxation="NONE", - inner_maximum=ninner, - inner_dvclose=hclose, - rcloserecord=rclose, - linear_acceleration="CG", - scaling_method="NONE", - reordering_method="NONE", - relaxation_factor=relax, - filename=f"{gwfname}.ims", - ) - sim.register_ims_package(imsgwf, [gwf.name]) - - # Instantiating MODFLOW 6 discretization package - flopy.mf6.ModflowGwfdis( - gwf, - length_units=length_units, - nlay=nlay, - nrow=nrow, - ncol=ncol, - delr=delr, - delc=delc, - top=top, - botm=botm, - idomain=idomain, - filename=f"{gwfname}.dis", - ) - - # Instantiating MODFLOW 6 node-property flow package - flopy.mf6.ModflowGwfnpf( - gwf, - save_flows=False, - icelltype=icelltype, - k=k11, - k33=k33, - save_specific_discharge=True, - filename=f"{gwfname}.npf", - ) - - # Instantiating MODFLOW 6 storage package (steady flow conditions, so no actual storage, using to print values in .lst file) - flopy.mf6.ModflowGwfsto(gwf, ss=0, sy=0, filename=f"{gwfname}.sto") - - # Instantiating MODFLOW 6 initial conditions package for flow model - flopy.mf6.ModflowGwfic(gwf, strt=strt, filename=f"{gwfname}.ic") - - # Instantiating MODFLOW 6 constant head package - flopy.mf6.ModflowGwfchd( - gwf, - maxbound=len(chdspd), - stress_period_data=chdspd, - save_flows=False, - pname="CHD-1", - filename=f"{gwfname}.chd", - ) - - # Instantiate the wel package - flopy.mf6.ModflowGwfwel( - gwf, - print_input=True, - print_flows=True, - stress_period_data=spd_mf6, - save_flows=False, - auxiliary="CONCENTRATION", - pname="WEL-1", - filename=f"{gwfname}.wel", - ) - - # Instantiating MODFLOW 6 output control package for flow model - flopy.mf6.ModflowGwfoc( - gwf, - head_filerecord=f"{gwfname}.hds", - budget_filerecord=f"{gwfname}.bud", - headprintrecord=[ - ("COLUMNS", 10, "WIDTH", 15, "DIGITS", 6, "GENERAL") - ], - saverecord=[("HEAD", "LAST"), ("BUDGET", "LAST")], - printrecord=[("HEAD", "LAST"), ("BUDGET", "LAST")], - ) - - # Instantiating MODFLOW 6 groundwater transport package - gwtname = "gwt_" + name - gwt = flopy.mf6.MFModel( - sim, - model_type="gwt6", - modelname=gwtname, - model_nam_file=f"{gwtname}.nam", - ) - gwt.name_file.save_flows = True - - # create iterative model solution and register the gwt model with it - imsgwt = flopy.mf6.ModflowIms( - sim, - print_option="SUMMARY", - outer_dvclose=hclose, - outer_maximum=nouter, - under_relaxation="NONE", - inner_maximum=ninner, - inner_dvclose=hclose, - rcloserecord=rclose, - linear_acceleration="BICGSTAB", - scaling_method="NONE", - reordering_method="NONE", - relaxation_factor=relax, - filename=f"{gwtname}.ims", - ) - sim.register_ims_package(imsgwt, [gwt.name]) - - # Instantiating MODFLOW 6 transport discretization package - flopy.mf6.ModflowGwtdis( - gwt, - nlay=nlay, - nrow=nrow, - ncol=ncol, - delr=delr, - delc=delc, - top=top, - botm=botm, - idomain=1, - filename=f"{gwtname}.dis", - ) - - # Instantiating MODFLOW 6 transport initial concentrations - flopy.mf6.ModflowGwtic(gwt, strt=sconc, filename=f"{gwtname}.ic") - - # Instantiating MODFLOW 6 transport advection package - if mixelm >= 0: - scheme = "UPSTREAM" - elif mixelm == -1: - scheme = "TVD" - else: - raise Exception() - flopy.mf6.ModflowGwtadv(gwt, scheme=scheme, filename=f"{gwtname}.adv") - - # Instantiating MODFLOW 6 transport dispersion package - if al != 0: - flopy.mf6.ModflowGwtdsp( - gwt, - alh=al, - ath1=ath1, - filename=f"{gwtname}.dsp", - ) - - # Instantiating MODFLOW 6 transport mass storage package (formerly "reaction" package in MT3DMS) - flopy.mf6.ModflowGwtmst( - gwt, - porosity=prsity, - first_order_decay=False, - decay=None, - decay_sorbed=None, - sorption=None, - bulk_density=None, - distcoef=None, - filename=f"{gwtname}.mst", - ) - - # Instantiate constant concentration - c0 = 1.0 - cncspd = [[(0, 15, 15), c0]] - flopy.mf6.ModflowGwtcnc( - gwt, - maxbound=len(cncspd), - stress_period_data=cncspd, - save_flows=False, - pname="CNC-1", - filename=f"{gwtname}.cnc", - ) - - # Instantiating MODFLOW 6 transport source-sink mixing package - sourcerecarray = [("WEL-1", "AUX", "CONCENTRATION")] - flopy.mf6.ModflowGwtssm( - gwt, sources=sourcerecarray, filename=f"{gwtname}.ssm" - ) - - # Instantiating MODFLOW 6 transport output control package - flopy.mf6.ModflowGwtoc( +# Define functions to build models, write input files, and run the simulation. + + +# + +def build_models(sim_name, mixelm=0, silent=False): + mt3d_ws = os.path.join(workspace, sim_name, "mt3d") + modelname_mf = "p05-mf" + + # Instantiate the MODFLOW model + mf = flopy.modflow.Modflow( + modelname=modelname_mf, model_ws=mt3d_ws, exe_name="mf2005" + ) + + # Instantiate discretization package + # units: itmuni=4 (days), lenuni=2 (m) + flopy.modflow.ModflowDis( + mf, + nlay=nlay, + nrow=nrow, + ncol=ncol, + delr=delr, + delc=delc, + top=top, + botm=botm, + nper=nper, + nstp=nstp, + perlen=perlen, + itmuni=4, + lenuni=2, + ) + + # Instantiate basic package + flopy.modflow.ModflowBas(mf, ibound=ibound_mf2k5, strt=strt) + + # Instantiate layer property flow package + flopy.modflow.ModflowLpf(mf, hk=k11, laytyp=icelltype) + + # Instantiate well package + flopy.modflow.ModflowWel(mf, stress_period_data=welspd) + + # Instantiate solver package + flopy.modflow.ModflowSip(mf) + + # Instantiate link mass transport package (for writing linker file) + flopy.modflow.ModflowLmt(mf) + + # Transport + modelname_mt = "p05-mt" + mt = flopy.mt3d.Mt3dms( + modelname=modelname_mt, + model_ws=mt3d_ws, + exe_name="mt3dms", + modflowmodel=mf, + ) + + # Instantiate basic transport package + flopy.mt3d.Mt3dBtn( + mt, + icbund=icbund, + prsity=prsity, + sconc=sconc, + nper=nper, + nstp=nstp, + perlen=perlen, + dt0=dt0, + tsmult=tsmult, + ) + + # Instatiate the advection package + flopy.mt3d.Mt3dAdv( + mt, + mixelm=mixelm, + dceps=dceps, + nplane=nplane, + npl=npl, + nph=nph, + npmin=npmin, + npmax=npmax, + nlsink=nlsink, + npsink=npsink, + percel=percel, + itrack=itrack, + wd=wd, + ) + + # Instantiate the dispersion package + flopy.mt3d.Mt3dDsp(mt, al=al, trpt=trpt, dmcoef=dmcoef) + + # Instantiate the source/sink mixing package + flopy.mt3d.Mt3dSsm(mt, stress_period_data=spd) + + # Instantiate the GCG solver in MT3DMS + flopy.mt3d.Mt3dGcg(mt, mxiter=10) + + # MODFLOW 6 + name = "p05-mf6" + gwfname = "gwf-" + name + sim_ws = os.path.join(workspace, sim_name) + sim = flopy.mf6.MFSimulation(sim_name=sim_name, sim_ws=sim_ws, exe_name="mf6") + + # Instantiating MODFLOW 6 time discretization + tdis_rc = [] + for i in range(nper): + tdis_rc.append((perlen[i], nstp[i], tsmult[i])) + flopy.mf6.ModflowTdis(sim, nper=nper, perioddata=tdis_rc, time_units=time_units) + + # Instantiating MODFLOW 6 groundwater flow model + gwf = flopy.mf6.ModflowGwf( + sim, + modelname=gwfname, + save_flows=True, + model_nam_file=f"{gwfname}.nam", + ) + + # Instantiating MODFLOW 6 solver for flow model + imsgwf = flopy.mf6.ModflowIms( + sim, + print_option="SUMMARY", + outer_dvclose=hclose, + outer_maximum=nouter, + under_relaxation="NONE", + inner_maximum=ninner, + inner_dvclose=hclose, + rcloserecord=rclose, + linear_acceleration="CG", + scaling_method="NONE", + reordering_method="NONE", + relaxation_factor=relax, + filename=f"{gwfname}.ims", + ) + sim.register_ims_package(imsgwf, [gwf.name]) + + # Instantiating MODFLOW 6 discretization package + flopy.mf6.ModflowGwfdis( + gwf, + length_units=length_units, + nlay=nlay, + nrow=nrow, + ncol=ncol, + delr=delr, + delc=delc, + top=top, + botm=botm, + idomain=idomain, + filename=f"{gwfname}.dis", + ) + + # Instantiating MODFLOW 6 node-property flow package + flopy.mf6.ModflowGwfnpf( + gwf, + save_flows=False, + icelltype=icelltype, + k=k11, + k33=k33, + save_specific_discharge=True, + filename=f"{gwfname}.npf", + ) + + # Instantiating MODFLOW 6 storage package (steady flow conditions, so no actual storage, using to print values in .lst file) + flopy.mf6.ModflowGwfsto(gwf, ss=0, sy=0, filename=f"{gwfname}.sto") + + # Instantiating MODFLOW 6 initial conditions package for flow model + flopy.mf6.ModflowGwfic(gwf, strt=strt, filename=f"{gwfname}.ic") + + # Instantiating MODFLOW 6 constant head package + flopy.mf6.ModflowGwfchd( + gwf, + maxbound=len(chdspd), + stress_period_data=chdspd, + save_flows=False, + pname="CHD-1", + filename=f"{gwfname}.chd", + ) + + # Instantiate the wel package + flopy.mf6.ModflowGwfwel( + gwf, + print_input=True, + print_flows=True, + stress_period_data=spd_mf6, + save_flows=False, + auxiliary="CONCENTRATION", + pname="WEL-1", + filename=f"{gwfname}.wel", + ) + + # Instantiating MODFLOW 6 output control package for flow model + flopy.mf6.ModflowGwfoc( + gwf, + head_filerecord=f"{gwfname}.hds", + budget_filerecord=f"{gwfname}.bud", + headprintrecord=[("COLUMNS", 10, "WIDTH", 15, "DIGITS", 6, "GENERAL")], + saverecord=[("HEAD", "LAST"), ("BUDGET", "LAST")], + printrecord=[("HEAD", "LAST"), ("BUDGET", "LAST")], + ) + + # Instantiating MODFLOW 6 groundwater transport package + gwtname = "gwt_" + name + gwt = flopy.mf6.MFModel( + sim, + model_type="gwt6", + modelname=gwtname, + model_nam_file=f"{gwtname}.nam", + ) + gwt.name_file.save_flows = True + + # create iterative model solution and register the gwt model with it + imsgwt = flopy.mf6.ModflowIms( + sim, + print_option="SUMMARY", + outer_dvclose=hclose, + outer_maximum=nouter, + under_relaxation="NONE", + inner_maximum=ninner, + inner_dvclose=hclose, + rcloserecord=rclose, + linear_acceleration="BICGSTAB", + scaling_method="NONE", + reordering_method="NONE", + relaxation_factor=relax, + filename=f"{gwtname}.ims", + ) + sim.register_ims_package(imsgwt, [gwt.name]) + + # Instantiating MODFLOW 6 transport discretization package + flopy.mf6.ModflowGwtdis( + gwt, + nlay=nlay, + nrow=nrow, + ncol=ncol, + delr=delr, + delc=delc, + top=top, + botm=botm, + idomain=1, + filename=f"{gwtname}.dis", + ) + + # Instantiating MODFLOW 6 transport initial concentrations + flopy.mf6.ModflowGwtic(gwt, strt=sconc, filename=f"{gwtname}.ic") + + # Instantiating MODFLOW 6 transport advection package + if mixelm >= 0: + scheme = "UPSTREAM" + elif mixelm == -1: + scheme = "TVD" + else: + raise Exception() + flopy.mf6.ModflowGwtadv(gwt, scheme=scheme, filename=f"{gwtname}.adv") + + # Instantiating MODFLOW 6 transport dispersion package + if al != 0: + flopy.mf6.ModflowGwtdsp( gwt, - budget_filerecord=f"{gwtname}.cbc", - concentration_filerecord=f"{gwtname}.ucn", - concentrationprintrecord=[ - ("COLUMNS", 10, "WIDTH", 15, "DIGITS", 6, "GENERAL") - ], - saverecord=[("CONCENTRATION", "LAST"), ("BUDGET", "LAST")], - printrecord=[("CONCENTRATION", "LAST"), ("BUDGET", "LAST")], + alh=al, + ath1=ath1, + filename=f"{gwtname}.dsp", ) - # Instantiating MODFLOW 6 flow-transport exchange mechanism - flopy.mf6.ModflowGwfgwt( - sim, - exgtype="GWF6-GWT6", - exgmnamea=gwfname, - exgmnameb=gwtname, - filename=f"{name}.gwfgwt", - ) - return mf, mt, sim - return None - - -# Function to write model files - - -def write_model(mf2k5, mt3d, sim, silent=True): - if config.writeModel: - mf2k5.write_input() - mt3d.write_input() - sim.write_simulation(silent=silent) - - -# Function to run the model. True is returned if the model runs successfully. + # Instantiating MODFLOW 6 transport mass storage package (formerly "reaction" package in MT3DMS) + flopy.mf6.ModflowGwtmst( + gwt, + porosity=prsity, + first_order_decay=False, + decay=None, + decay_sorbed=None, + sorption=None, + bulk_density=None, + distcoef=None, + filename=f"{gwtname}.mst", + ) + + # Instantiate constant concentration + c0 = 1.0 + cncspd = [[(0, 15, 15), c0]] + flopy.mf6.ModflowGwtcnc( + gwt, + maxbound=len(cncspd), + stress_period_data=cncspd, + save_flows=False, + pname="CNC-1", + filename=f"{gwtname}.cnc", + ) + + # Instantiating MODFLOW 6 transport source-sink mixing package + sourcerecarray = [("WEL-1", "AUX", "CONCENTRATION")] + flopy.mf6.ModflowGwtssm(gwt, sources=sourcerecarray, filename=f"{gwtname}.ssm") + + # Instantiating MODFLOW 6 transport output control package + flopy.mf6.ModflowGwtoc( + gwt, + budget_filerecord=f"{gwtname}.cbc", + concentration_filerecord=f"{gwtname}.ucn", + concentrationprintrecord=[("COLUMNS", 10, "WIDTH", 15, "DIGITS", 6, "GENERAL")], + saverecord=[("CONCENTRATION", "LAST"), ("BUDGET", "LAST")], + printrecord=[("CONCENTRATION", "LAST"), ("BUDGET", "LAST")], + ) + + # Instantiating MODFLOW 6 flow-transport exchange mechanism + flopy.mf6.ModflowGwfgwt( + sim, + exgtype="GWF6-GWT6", + exgmnamea=gwfname, + exgmnameb=gwtname, + filename=f"{name}.gwfgwt", + ) + return mf, mt, sim + + +def write_models(mf2k5, mt3d, sim, silent=True): + mf2k5.write_input() + mt3d.write_input() + sim.write_simulation(silent=silent) + + +@timed +def run_models(mf2k5, mt3d, sim, silent=True): + success, buff = mf2k5.run_model(silent=silent, report=True) + assert success, pformat(buff) + success, buff = mt3d.run_model( + silent=silent, normal_msg="Program completed", report=True + ) + assert success, pformat(buff) + success, buff = sim.run_simulation(silent=silent, report=True) + assert success, pformat(buff) + + +# - + +# ### Plotting results +# +# Define functions to plot model results. +# + +# Figure properties +figure_size = (6, 4.5) -@config.timeit -def run_model(mf2k5, mt3d, sim, silent=True): - success = True - if config.runModel: - success, buff = mf2k5.run_model(silent=silent) - success, buff = mt3d.run_model(silent=silent) - success, buff = sim.run_simulation(silent=silent) - if not success: - print(buff) - return success +def plot_results(mt3d, mf6, idx, ax=None, ax2=None): + mt3d_out_path = mt3d.model_ws + mf6.simulation_data.mfpath.get_sim_path() -# Function to plot the model results + # Get the MT3DMS concentration output + fname_mt3d = os.path.join(mt3d_out_path, "MT3D001.UCN") + ucnobj_mt3d = flopy.utils.UcnFile(fname_mt3d) + conc_mt3d = ucnobj_mt3d.get_alldata() + # Get the MF6 concentration output + gwt = mf6.get_model(list(mf6.model_names)[1]) + ucnobj_mf6 = gwt.output.concentration() + conc_mf6 = ucnobj_mf6.get_alldata() -def plot_results(mt3d, mf6, idx, ax=None, ax2=None): - if config.plotModel: - mt3d_out_path = mt3d.model_ws - mf6_out_path = mf6.simulation_data.mfpath.get_sim_path() - mf6.simulation_data.mfpath.get_sim_path() - - # Get the MT3DMS concentration output - fname_mt3d = os.path.join(mt3d_out_path, "MT3D001.UCN") - ucnobj_mt3d = flopy.utils.UcnFile(fname_mt3d) - conc_mt3d = ucnobj_mt3d.get_alldata() - - # Get the MF6 concentration output - gwt = mf6.get_model(list(mf6.model_names)[1]) - ucnobj_mf6 = gwt.output.concentration() - conc_mf6 = ucnobj_mf6.get_alldata() - - # Create figure for scenario - fs = USGSFigure(figure_type="graph", verbose=False) + # Create figure for scenario + with styles.USGSPlot(): sim_name = mf6.name plt.rcParams["lines.dashed_pattern"] = [5.0, 5.0] if ax is None: @@ -518,10 +499,7 @@ def plot_results(mt3d, mf6, idx, ax=None, ax2=None): conc1 = conc_mt3d[0, 0, :, :] conc2 = conc_mf6[0, 0, :, :] - x = ( - mt3d.modelgrid.xcellcenters[15, 15:] - - mt3d.modelgrid.xcellcenters[15, 15] - ) + x = mt3d.modelgrid.xcellcenters[15, 15:] - mt3d.modelgrid.xcellcenters[15, 15] y_mt3d = conc1[15, 15:] y_mf6 = conc2[15, 15:] @@ -536,14 +514,15 @@ def plot_results(mt3d, mf6, idx, ax=None, ax2=None): title = "Concentration as a Function of Distance From The Source" letter = chr(ord("@") + idx + 1) - fs.heading(letter=letter, heading=title) + styles.heading(letter=letter, heading=title) - # save figure - if config.plotSave: + if plot_show: + plt.show() + if plot_save: fpth = os.path.join( "..", "figures", - "{}{}".format(sim_name + "-xsec", config.figure_ext), + "{}{}".format(sim_name + "-xsec", ".png"), ) fig.savefig(fpth) @@ -559,9 +538,7 @@ def plot_results(mt3d, mf6, idx, ax=None, ax2=None): mm.plot_ibound() cs1 = mm.contour_array(conc_mt3d[0], levels=levels, colors="r") plt.clabel(cs1, inline=1, fontsize=10) - cs2 = mm.contour_array( - conc_mf6[0], levels=levels, colors="k", linestyles=":" - ) + cs2 = mm.contour_array(conc_mf6[0], levels=levels, colors="k", linestyles=":") plt.clabel(cs2, inline=1, fontsize=10) labels = ["MT3DMS", "MODFLOW 6"] lines = [cs1.collections[0], cs2.collections[0]] @@ -579,48 +556,37 @@ def plot_results(mt3d, mf6, idx, ax=None, ax2=None): # ax2.text(235, 140, "Location of x-section \nshown in Fig. x", **style) letter = chr(ord("@") + idx + 2) - fs.heading(letter=letter, heading=title) + styles.heading(letter=letter, heading=title) - # save figure - if config.plotSave: + if plot_show: + plt.show() + if plot_save: fpth = os.path.join( "..", "figures", - "{}{}".format(sim_name + "-planView", config.figure_ext), + "{}{}".format(sim_name + "-planView", ".png"), ) fig.savefig(fpth) -# ### Function that wraps all of the steps for each MT3DMS Example 10 Problem scenario +# - + +# ### Running the example # -# 1. build_model, -# 2. write_model, -# 3. run_model, and -# 4. plot_results. +# Define and invoke a function to run the example scenario, then plot results. +# + def scenario(idx, silent=True): - mf2k5, mt3d, sim = build_model(example_name) - - write_model(mf2k5, mt3d, sim, silent=silent) - - success = run_model(mf2k5, mt3d, sim, silent=silent) - - if success: + mf2k5, mt3d, sim = build_models(example_name) + if write: + write_models(mf2k5, mt3d, sim, silent=silent) + if run: + run_models(mf2k5, mt3d, sim, silent=silent) + if plot: plot_results(mt3d, sim, idx) -# nosetest - exclude block from this nosetest to the next nosetest - - -def test_01(): - scenario(0, silent=False) - - -# nosetest end - -if __name__ == "__main__": - # ### Two-Dimensional Transport in a Diagonal Flow Field - # - # Compares the standard finite difference solutions between MT3D MF 6 - scenario(0) +# Compares the standard finite difference solutions between MT3D MF 6 +scenario(0) +# - diff --git a/scripts/ex-gwt-mt3dms-p06.py b/scripts/ex-gwt-mt3dms-p06.py index 6bacb32ac..aa8eabc46 100644 --- a/scripts/ex-gwt-mt3dms-p06.py +++ b/scripts/ex-gwt-mt3dms-p06.py @@ -18,44 +18,43 @@ # 9. Two-Dimensional Application Example # 10. Three-Dimensional Field Case Study +# ### Initial setup +# +# Import dependencies, define the example name and workspace, and read settings from environment variables. -# ### MODFLOW 6 GWT MT3DMS Example 6 Problem Setup - -# Append to system path to include the common subdirectory - +# + import os -import sys - -sys.path.append(os.path.join("..", "common")) +import pathlib as pl +from pprint import pformat -# Imports - -import config import flopy import matplotlib.pyplot as plt import numpy as np -from figspecs import USGSFigure - -mf6exe = "mf6" -exe_name_mf = "mf2005" -exe_name_mt = "mt3dusgs" - -# Set figure properties specific to this problem - -figure_size = (6, 4.5) - -# Base simulation and model name and workspace - -ws = config.base_ws +from flopy.plot.styles import styles +from modflow_devtools.misc import get_env, timed + +# Settings from environment variable +write = get_env("WRITE", True) +run = get_env("RUN", True) +plot = get_env("PLOT", True) +plot_show = get_env("PLOT_SHOW", True) +plot_save = get_env("PLOT_SAVE", True) + +# Example name and base workspace +workspace = pl.Path("../examples") example_name = "ex-gwt-mt3dms-p06" +# - -# Model units +# ### Define parameters +# +# Define model units, parameters and other settings. +# + +# Model units length_units = "feet" time_units = "days" -# Table - +# Model parameters nlay = 1 # Number of layers nrow = 31 # Number of rows ncol = 31 # Number of columns @@ -73,14 +72,11 @@ trpt = 1.0 # Ratio of transverse to longitudinal dispersitivity # Additional model input - perlen = [912.5, 2737.5] nper = len(perlen) nstp = [365, 1095] tsmult = [1.0, 1.0] -k11 = ( - 0.005 * 86400 -) # established above, but explicitly writing out its origin here +k11 = 0.005 * 86400 # established above, but explicitly writing out its origin here sconc = 0.0 c0 = 0.0 dt0 = 56.25 @@ -93,7 +89,6 @@ strt = np.zeros((nlay, nrow, ncol), dtype=float) # Active model domain - ibound_mf2k5 = np.ones((nlay, nrow, ncol), dtype=int) * -1 ibound_mf2k5[:, 1 : nrow - 1, 1 : ncol - 1] = 1 idomain = np.ones((nlay, nrow, ncol), dtype=int) @@ -101,7 +96,6 @@ # Boundary conditions # MF2K5 pumping info: - qwell = 86400.0 welspd = { 0: [[0, 15, 15, qwell]], # Well pumping info for MF2K5 @@ -118,7 +112,6 @@ spd_mf6 = {0: [[(0, 15, 15), qwell, cwell]], 1: [[(0, 15, 15), -qwell, 0.0]]} # MF6 constant head boundaries: - chdspd = [] # Loop through the left & right sides. for i in np.arange(nrow): @@ -132,7 +125,6 @@ chdspd = {0: chdspd} # Solver settings - nouter, ninner = 100, 300 hclose, rclose, relax = 1e-6, 1e-6, 1.0 percel = 1.0 # HMOC parameters @@ -148,377 +140,359 @@ nlsink = nplane npsink = nph -# Static temporal data used by TDIS file - +# Time discretization tdis_rc = [] tdis_rc.append((perlen, nstp, 1.0)) +# - -# ### Functions to build, write, and run models and plot MT3DMS Example 10 Problem results +# ### Model setup # -# MODFLOW 6 flopy simulation object (sim) is returned if building the model - - -def build_model(sim_name, mixelm=0, silent=False): - if config.buildModel: - mt3d_ws = os.path.join(ws, sim_name, "mt3d") - modelname_mf = "p06-mf" - - # Instantiate the MODFLOW model - mf = flopy.modflow.Modflow( - modelname=modelname_mf, model_ws=mt3d_ws, exe_name=exe_name_mf - ) - - # Instantiate discretization package - # units: itmuni=4 (days), lenuni=2 (m) - flopy.modflow.ModflowDis( - mf, - nlay=nlay, - nrow=nrow, - ncol=ncol, - delr=delr, - delc=delc, - top=top, - botm=botm, - nper=nper, - nstp=nstp, - perlen=perlen, - itmuni=4, - lenuni=1, - ) - - # Instantiate basic package - flopy.modflow.ModflowBas(mf, ibound=ibound_mf2k5, strt=strt) - - # Instantiate layer property flow package - flopy.modflow.ModflowLpf(mf, hk=k11, laytyp=icelltype) - - # Instantiate well package - flopy.modflow.ModflowWel(mf, stress_period_data=welspd) - - # Instantiate solver package - flopy.modflow.ModflowSip(mf) - - # Instantiate link mass transport package (for writing linker file) - flopy.modflow.ModflowLmt(mf) - - # Transport - modelname_mt = "p06-mt" - mt = flopy.mt3d.Mt3dms( - modelname=modelname_mt, - model_ws=mt3d_ws, - exe_name=exe_name_mt, - modflowmodel=mf, - ) - - # Instantiate basic transport package - flopy.mt3d.Mt3dBtn( - mt, - icbund=icbund, - prsity=prsity, - sconc=sconc, - nper=nper, - perlen=perlen, - dt0=dt0, - obs=[(0, 15, 15)], - ) - - # Instatiate the advection package - flopy.mt3d.Mt3dAdv( - mt, - mixelm=mixelm, - dceps=dceps, - nplane=nplane, - npl=npl, - nph=nph, - npmin=npmin, - npmax=npmax, - nlsink=nlsink, - npsink=npsink, - percel=percel, - itrack=itrack, - wd=wd, - ) - - # Instantiate the dispersion package - flopy.mt3d.Mt3dDsp(mt, al=al, trpt=trpt, dmcoef=dmcoef) - - # Instantiate the source/sink mixing package - flopy.mt3d.Mt3dSsm(mt, stress_period_data=spd) - - # Instantiate the GCG solver in MT3DMS - flopy.mt3d.Mt3dGcg(mt) - - # MODFLOW 6 - name = "p06-mf6" - gwfname = "gwf-" + name - sim_ws = os.path.join(ws, sim_name) - sim = flopy.mf6.MFSimulation( - sim_name=sim_name, sim_ws=sim_ws, exe_name=mf6exe - ) - - # Instantiating MODFLOW 6 time discretization - tdis_rc = [] - for i in range(nper): - tdis_rc.append((perlen[i], nstp[i], tsmult[i])) - flopy.mf6.ModflowTdis( - sim, nper=nper, perioddata=tdis_rc, time_units=time_units - ) - - # Instantiating MODFLOW 6 groundwater flow model - gwf = flopy.mf6.ModflowGwf( - sim, - modelname=gwfname, - save_flows=True, - model_nam_file=f"{gwfname}.nam", - ) - - # Instantiating MODFLOW 6 solver for flow model - imsgwf = flopy.mf6.ModflowIms( - sim, - print_option="SUMMARY", - outer_dvclose=hclose, - outer_maximum=nouter, - under_relaxation="NONE", - inner_maximum=ninner, - inner_dvclose=hclose, - rcloserecord=rclose, - linear_acceleration="CG", - scaling_method="NONE", - reordering_method="NONE", - relaxation_factor=relax, - filename=f"{gwfname}.ims", - ) - sim.register_ims_package(imsgwf, [gwf.name]) - - # Instantiating MODFLOW 6 discretization package - flopy.mf6.ModflowGwfdis( - gwf, - length_units=length_units, - nlay=nlay, - nrow=nrow, - ncol=ncol, - delr=delr, - delc=delc, - top=top, - botm=botm, - idomain=idomain, - filename=f"{gwfname}.dis", - ) - - # Instantiating MODFLOW 6 node-property flow package - flopy.mf6.ModflowGwfnpf( - gwf, - save_flows=False, - icelltype=icelltype, - k=k11, - k33=k33, - save_specific_discharge=True, - filename=f"{gwfname}.npf", - ) - - # Instantiating MODFLOW 6 storage package (steady flow conditions, so no actual storage, using to print values in .lst file) - flopy.mf6.ModflowGwfsto(gwf, ss=0, sy=0, filename=f"{gwfname}.sto") - - # Instantiating MODFLOW 6 initial conditions package for flow model - flopy.mf6.ModflowGwfic(gwf, strt=strt, filename=f"{gwfname}.ic") - - # Instantiating MODFLOW 6 constant head package - flopy.mf6.ModflowGwfchd( - gwf, - maxbound=len(chdspd), - stress_period_data=chdspd, - save_flows=False, - pname="CHD-1", - filename=f"{gwfname}.chd", - ) - - # Instantiate the wel package - flopy.mf6.ModflowGwfwel( - gwf, - print_input=True, - print_flows=True, - stress_period_data=spd_mf6, - save_flows=False, - auxiliary="CONCENTRATION", - pname="WEL-1", - filename=f"{gwfname}.wel", - ) - - # Instantiating MODFLOW 6 output control package for flow model - flopy.mf6.ModflowGwfoc( - gwf, - head_filerecord=f"{gwfname}.hds", - budget_filerecord=f"{gwfname}.bud", - headprintrecord=[ - ("COLUMNS", 10, "WIDTH", 15, "DIGITS", 6, "GENERAL") - ], - saverecord=[("HEAD", "LAST"), ("BUDGET", "LAST")], - printrecord=[("HEAD", "LAST"), ("BUDGET", "LAST")], - ) - - # Instantiating MODFLOW 6 groundwater transport package - gwtname = "gwt_" + name - gwt = flopy.mf6.MFModel( - sim, - model_type="gwt6", - modelname=gwtname, - model_nam_file=f"{gwtname}.nam", - ) - gwt.name_file.save_flows = True - - # create iterative model solution and register the gwt model with it - imsgwt = flopy.mf6.ModflowIms( - sim, - print_option="SUMMARY", - outer_dvclose=hclose, - outer_maximum=nouter, - under_relaxation="NONE", - inner_maximum=ninner, - inner_dvclose=hclose, - rcloserecord=rclose, - linear_acceleration="BICGSTAB", - scaling_method="NONE", - reordering_method="NONE", - relaxation_factor=relax, - filename=f"{gwtname}.ims", - ) - sim.register_ims_package(imsgwt, [gwt.name]) - - # Instantiating MODFLOW 6 transport discretization package - flopy.mf6.ModflowGwtdis( - gwt, - nlay=nlay, - nrow=nrow, - ncol=ncol, - delr=delr, - delc=delc, - top=top, - botm=botm, - idomain=idomain, - filename=f"{gwtname}.dis", - ) - - # Instantiating MODFLOW 6 transport initial concentrations - flopy.mf6.ModflowGwtic(gwt, strt=sconc, filename=f"{gwtname}.ic") - - # Instantiating MODFLOW 6 transport advection package - if mixelm >= 0: - scheme = "UPSTREAM" - elif mixelm == -1: - scheme = "TVD" - else: - raise Exception() - flopy.mf6.ModflowGwtadv(gwt, scheme=scheme, filename=f"{gwtname}.adv") - - # Instantiating MODFLOW 6 transport dispersion package - if al != 0: - flopy.mf6.ModflowGwtdsp( - gwt, - xt3d_off=True, - alh=al, - ath1=ath1, - filename=f"{gwtname}.dsp", - ) - - # Instantiating MODFLOW 6 transport mass storage package (formerly "reaction" package in MT3DMS) - flopy.mf6.ModflowGwtmst( - gwt, - porosity=prsity, - first_order_decay=False, - decay=None, - decay_sorbed=None, - sorption=None, - bulk_density=None, - distcoef=None, - filename=f"{gwtname}.mst", - ) - - # Instantiating MODFLOW 6 transport source-sink mixing package - sourcerecarray = [("WEL-1", "AUX", "CONCENTRATION")] - flopy.mf6.ModflowGwtssm( - gwt, sources=sourcerecarray, filename=f"{gwtname}.ssm" - ) - - # Instantiating MODFLOW 6 transport output control package - flopy.mf6.ModflowGwtoc( +# Define functions to build models, write input files, and run the simulation. + + +# + +def build_models(sim_name, mixelm=0, silent=False): + mt3d_ws = os.path.join(workspace, sim_name, "mt3d") + modelname_mf = "p06-mf" + + # Instantiate the MODFLOW model + mf = flopy.modflow.Modflow( + modelname=modelname_mf, model_ws=mt3d_ws, exe_name="mf2005" + ) + + # Instantiate discretization package + # units: itmuni=4 (days), lenuni=2 (m) + flopy.modflow.ModflowDis( + mf, + nlay=nlay, + nrow=nrow, + ncol=ncol, + delr=delr, + delc=delc, + top=top, + botm=botm, + nper=nper, + nstp=nstp, + perlen=perlen, + itmuni=4, + lenuni=1, + ) + + # Instantiate basic package + flopy.modflow.ModflowBas(mf, ibound=ibound_mf2k5, strt=strt) + + # Instantiate layer property flow package + flopy.modflow.ModflowLpf(mf, hk=k11, laytyp=icelltype) + + # Instantiate well package + flopy.modflow.ModflowWel(mf, stress_period_data=welspd) + + # Instantiate solver package + flopy.modflow.ModflowSip(mf) + + # Instantiate link mass transport package (for writing linker file) + flopy.modflow.ModflowLmt(mf) + + # Transport + modelname_mt = "p06-mt" + mt = flopy.mt3d.Mt3dms( + modelname=modelname_mt, + model_ws=mt3d_ws, + exe_name="mt3dusgs", + modflowmodel=mf, + ) + + # Instantiate basic transport package + flopy.mt3d.Mt3dBtn( + mt, + icbund=icbund, + prsity=prsity, + sconc=sconc, + nper=nper, + perlen=perlen, + dt0=dt0, + obs=[(0, 15, 15)], + ) + + # Instatiate the advection package + flopy.mt3d.Mt3dAdv( + mt, + mixelm=mixelm, + dceps=dceps, + nplane=nplane, + npl=npl, + nph=nph, + npmin=npmin, + npmax=npmax, + nlsink=nlsink, + npsink=npsink, + percel=percel, + itrack=itrack, + wd=wd, + ) + + # Instantiate the dispersion package + flopy.mt3d.Mt3dDsp(mt, al=al, trpt=trpt, dmcoef=dmcoef) + + # Instantiate the source/sink mixing package + flopy.mt3d.Mt3dSsm(mt, stress_period_data=spd) + + # Instantiate the GCG solver in MT3DMS + flopy.mt3d.Mt3dGcg(mt) + + # MODFLOW 6 + name = "p06-mf6" + gwfname = "gwf-" + name + sim_ws = os.path.join(workspace, sim_name) + sim = flopy.mf6.MFSimulation(sim_name=sim_name, sim_ws=sim_ws, exe_name="mf6") + + # Instantiating MODFLOW 6 time discretization + tdis_rc = [] + for i in range(nper): + tdis_rc.append((perlen[i], nstp[i], tsmult[i])) + flopy.mf6.ModflowTdis(sim, nper=nper, perioddata=tdis_rc, time_units=time_units) + + # Instantiating MODFLOW 6 groundwater flow model + gwf = flopy.mf6.ModflowGwf( + sim, + modelname=gwfname, + save_flows=True, + model_nam_file=f"{gwfname}.nam", + ) + + # Instantiating MODFLOW 6 solver for flow model + imsgwf = flopy.mf6.ModflowIms( + sim, + print_option="SUMMARY", + outer_dvclose=hclose, + outer_maximum=nouter, + under_relaxation="NONE", + inner_maximum=ninner, + inner_dvclose=hclose, + rcloserecord=rclose, + linear_acceleration="CG", + scaling_method="NONE", + reordering_method="NONE", + relaxation_factor=relax, + filename=f"{gwfname}.ims", + ) + sim.register_ims_package(imsgwf, [gwf.name]) + + # Instantiating MODFLOW 6 discretization package + flopy.mf6.ModflowGwfdis( + gwf, + length_units=length_units, + nlay=nlay, + nrow=nrow, + ncol=ncol, + delr=delr, + delc=delc, + top=top, + botm=botm, + idomain=idomain, + filename=f"{gwfname}.dis", + ) + + # Instantiating MODFLOW 6 node-property flow package + flopy.mf6.ModflowGwfnpf( + gwf, + save_flows=False, + icelltype=icelltype, + k=k11, + k33=k33, + save_specific_discharge=True, + filename=f"{gwfname}.npf", + ) + + # Instantiating MODFLOW 6 storage package (steady flow conditions, so no actual storage, using to print values in .lst file) + flopy.mf6.ModflowGwfsto(gwf, ss=0, sy=0, filename=f"{gwfname}.sto") + + # Instantiating MODFLOW 6 initial conditions package for flow model + flopy.mf6.ModflowGwfic(gwf, strt=strt, filename=f"{gwfname}.ic") + + # Instantiating MODFLOW 6 constant head package + flopy.mf6.ModflowGwfchd( + gwf, + maxbound=len(chdspd), + stress_period_data=chdspd, + save_flows=False, + pname="CHD-1", + filename=f"{gwfname}.chd", + ) + + # Instantiate the wel package + flopy.mf6.ModflowGwfwel( + gwf, + print_input=True, + print_flows=True, + stress_period_data=spd_mf6, + save_flows=False, + auxiliary="CONCENTRATION", + pname="WEL-1", + filename=f"{gwfname}.wel", + ) + + # Instantiating MODFLOW 6 output control package for flow model + flopy.mf6.ModflowGwfoc( + gwf, + head_filerecord=f"{gwfname}.hds", + budget_filerecord=f"{gwfname}.bud", + headprintrecord=[("COLUMNS", 10, "WIDTH", 15, "DIGITS", 6, "GENERAL")], + saverecord=[("HEAD", "LAST"), ("BUDGET", "LAST")], + printrecord=[("HEAD", "LAST"), ("BUDGET", "LAST")], + ) + + # Instantiating MODFLOW 6 groundwater transport package + gwtname = "gwt_" + name + gwt = flopy.mf6.MFModel( + sim, + model_type="gwt6", + modelname=gwtname, + model_nam_file=f"{gwtname}.nam", + ) + gwt.name_file.save_flows = True + + # create iterative model solution and register the gwt model with it + imsgwt = flopy.mf6.ModflowIms( + sim, + print_option="SUMMARY", + outer_dvclose=hclose, + outer_maximum=nouter, + under_relaxation="NONE", + inner_maximum=ninner, + inner_dvclose=hclose, + rcloserecord=rclose, + linear_acceleration="BICGSTAB", + scaling_method="NONE", + reordering_method="NONE", + relaxation_factor=relax, + filename=f"{gwtname}.ims", + ) + sim.register_ims_package(imsgwt, [gwt.name]) + + # Instantiating MODFLOW 6 transport discretization package + flopy.mf6.ModflowGwtdis( + gwt, + nlay=nlay, + nrow=nrow, + ncol=ncol, + delr=delr, + delc=delc, + top=top, + botm=botm, + idomain=idomain, + filename=f"{gwtname}.dis", + ) + + # Instantiating MODFLOW 6 transport initial concentrations + flopy.mf6.ModflowGwtic(gwt, strt=sconc, filename=f"{gwtname}.ic") + + # Instantiating MODFLOW 6 transport advection package + if mixelm >= 0: + scheme = "UPSTREAM" + elif mixelm == -1: + scheme = "TVD" + else: + raise Exception() + flopy.mf6.ModflowGwtadv(gwt, scheme=scheme, filename=f"{gwtname}.adv") + + # Instantiating MODFLOW 6 transport dispersion package + if al != 0: + flopy.mf6.ModflowGwtdsp( gwt, - budget_filerecord=f"{gwtname}.cbc", - concentration_filerecord=f"{gwtname}.ucn", - concentrationprintrecord=[ - ("COLUMNS", 10, "WIDTH", 15, "DIGITS", 6, "GENERAL") - ], - saverecord=[("CONCENTRATION", "LAST"), ("BUDGET", "LAST")], - printrecord=[("CONCENTRATION", "LAST"), ("BUDGET", "LAST")], + xt3d_off=True, + alh=al, + ath1=ath1, + filename=f"{gwtname}.dsp", ) - # Instantiate observation package (for transport) - obslist = [["bckgrnd_cn", "concentration", (0, 15, 15)]] - obsdict = {f"{gwtname}.obs.csv": obslist} - obs = flopy.mf6.ModflowUtlobs( - gwt, print_input=False, continuous=obsdict - ) - - # Instantiating MODFLOW 6 flow-transport exchange mechanism - flopy.mf6.ModflowGwfgwt( - sim, - exgtype="GWF6-GWT6", - exgmnamea=gwfname, - exgmnameb=gwtname, - filename=f"{name}.gwfgwt", - ) - return mf, mt, sim - return None - - -# Function to write model files - - -def write_model(mf2k5, mt3d, sim, silent=True): - if config.writeModel: - mf2k5.write_input() - mt3d.write_input() - sim.write_simulation(silent=silent) - - -# Function to run the model. True is returned if the model runs successfully. - + # Instantiating MODFLOW 6 transport mass storage package (formerly "reaction" package in MT3DMS) + flopy.mf6.ModflowGwtmst( + gwt, + porosity=prsity, + first_order_decay=False, + decay=None, + decay_sorbed=None, + sorption=None, + bulk_density=None, + distcoef=None, + filename=f"{gwtname}.mst", + ) + + # Instantiating MODFLOW 6 transport source-sink mixing package + sourcerecarray = [("WEL-1", "AUX", "CONCENTRATION")] + flopy.mf6.ModflowGwtssm(gwt, sources=sourcerecarray, filename=f"{gwtname}.ssm") + + # Instantiating MODFLOW 6 transport output control package + flopy.mf6.ModflowGwtoc( + gwt, + budget_filerecord=f"{gwtname}.cbc", + concentration_filerecord=f"{gwtname}.ucn", + concentrationprintrecord=[("COLUMNS", 10, "WIDTH", 15, "DIGITS", 6, "GENERAL")], + saverecord=[("CONCENTRATION", "LAST"), ("BUDGET", "LAST")], + printrecord=[("CONCENTRATION", "LAST"), ("BUDGET", "LAST")], + ) + + # Instantiate observation package (for transport) + obslist = [["bckgrnd_cn", "concentration", (0, 15, 15)]] + obsdict = {f"{gwtname}.obs.csv": obslist} + obs = flopy.mf6.ModflowUtlobs(gwt, print_input=False, continuous=obsdict) + + # Instantiating MODFLOW 6 flow-transport exchange mechanism + flopy.mf6.ModflowGwfgwt( + sim, + exgtype="GWF6-GWT6", + exgmnamea=gwfname, + exgmnameb=gwtname, + filename=f"{name}.gwfgwt", + ) + return mf, mt, sim + + +def write_models(mf2k5, mt3d, sim, silent=True): + mf2k5.write_input() + mt3d.write_input() + sim.write_simulation(silent=silent) + + +@timed +def run_models(mf2k5, mt3d, sim, silent=True): + success, buff = mf2k5.run_model(silent=silent, report=True) + assert success, pformat(buff) + success, buff = mt3d.run_model( + silent=silent, normal_msg="Program completed", report=True + ) + assert success, pformat(buff) + success, buff = sim.run_simulation(silent=silent, report=True) + assert success, pformat(buff) + + +# - + +# ### Plotting results +# +# Define functions to plot model results. -@config.timeit -def run_model(mf2k5, mt3d, sim, silent=True): - success = True - if config.runModel: - success, buff = mf2k5.run_model(silent=silent) - success, buff = mt3d.run_model(silent=silent) - success, buff = sim.run_simulation(silent=silent) - if not success: - print(buff) - return success +# + +# Figure properties +figure_size = (6, 4.5) -# Function to plot the model results +def plot_results(mt3d, mf6, idx, ax=None): + mt3d_out_path = mt3d.model_ws + mf6_out_path = mf6.simulation_data.mfpath.get_sim_path() + mf6.simulation_data.mfpath.get_sim_path() + # Get the MT3DMS observation output file + fname = os.path.join(mt3d_out_path, "MT3D001.OBS") + cvt = mt3d.load_obs(fname) if os.path.isfile(fname) else None -def plot_results(mt3d, mf6, idx, ax=None): - if config.plotModel: - mt3d_out_path = mt3d.model_ws - mf6_out_path = mf6.simulation_data.mfpath.get_sim_path() - mf6.simulation_data.mfpath.get_sim_path() - - # Get the MT3DMS observation output file - fname = os.path.join(mt3d_out_path, "MT3D001.OBS") - if os.path.isfile(fname): - cvt = mt3d.load_obs(fname) - else: - cvt = None - - # Get the MODFLOW 6 concentration observation output file - fname = os.path.join( - mf6_out_path, list(mf6.model_names)[1] + ".obs.csv" - ) - mf6cobs = flopy.utils.Mf6Obs(fname).data + # Get the MODFLOW 6 concentration observation output file + fname = os.path.join(mf6_out_path, list(mf6.model_names)[1] + ".obs.csv") + mf6cobs = flopy.utils.Mf6Obs(fname).data - # Create figure for scenario - fs = USGSFigure(figure_type="graph", verbose=False) + # Create figure for scenario + with styles.USGSPlot(): sim_name = mf6.name plt.rcParams["lines.dashed_pattern"] = [5.0, 5.0] if ax is None: @@ -553,48 +527,37 @@ def plot_results(mt3d, mf6, idx, ax=None): title = "Calculated Concentration at an Injection/Pumping Well" letter = chr(ord("@") + idx + 1) - fs.heading(letter=letter, heading=title) + styles.heading(letter=letter, heading=title) - # save figure - if config.plotSave: + if plot_show: + plt.show() + if plot_save: fpth = os.path.join( "..", "figures", - f"{sim_name}{config.figure_ext}", + f"{sim_name}.png", ) fig.savefig(fpth) -# ### Function that wraps all of the steps for each MT3DMS Example 10 Problem scenario +# - + +# ### Running the example # -# 1. build_model, -# 2. write_model, -# 3. run_model, and -# 4. plot_results. +# Define and invoke a function to run the example scenario, then plot results. +# + def scenario(idx, silent=True): - mf2k5, mt3d, sim = build_model(example_name, mixelm=mixelm) - - write_model(mf2k5, mt3d, sim, silent=silent) - - success = run_model(mf2k5, mt3d, sim, silent=silent) - - if success: + mf2k5, mt3d, sim = build_models(example_name, mixelm=mixelm) + if write: + write_models(mf2k5, mt3d, sim, silent=silent) + if run: + run_models(mf2k5, mt3d, sim, silent=silent) + if plot: plot_results(mt3d, sim, idx) -# nosetest - exclude block from this nosetest to the next nosetest - - -def test_01(): - scenario(0, silent=False) - - -# nosetest end - -if __name__ == "__main__": - # ### Two-Dimensional Transport in a Diagonal Flow Field - # - # Compares the standard finite difference solutions between MT3D MF 6 - scenario(0) +# Compares the standard finite difference solutions between MT3D and MF6 +scenario(0) +# - diff --git a/scripts/ex-gwt-mt3dms-p07.py b/scripts/ex-gwt-mt3dms-p07.py index af45cbad2..b50795a9b 100644 --- a/scripts/ex-gwt-mt3dms-p07.py +++ b/scripts/ex-gwt-mt3dms-p07.py @@ -18,44 +18,43 @@ # 9. Two-Dimensional Application Example # 10. Three-Dimensional Field Case Study +# ### Initial setup +# +# Import dependencies, define the example name and workspace, and read settings from environment variables. -# ### MODFLOW 6 GWT MT3DMS Example 6 Problem Setup - -# Append to system path to include the common subdirectory - +# + import os -import sys +import pathlib as pl +from pprint import pformat -sys.path.append(os.path.join("..", "common")) - -# Imports - -import config import flopy import matplotlib.pyplot as plt import numpy as np -from figspecs import USGSFigure - -mf6exe = "mf6" -exe_name_mf = "mf2005" -exe_name_mt = "mt3dusgs" - -# Set figure properties specific to this problem - -figure_size = (4, 8) - -# Base simulation and model name and workspace - -ws = config.base_ws +from flopy.plot.styles import styles +from modflow_devtools.misc import get_env, timed + +# Settings from environment variables +write = get_env("WRITE", True) +run = get_env("RUN", True) +plot = get_env("PLOT", True) +plot_show = get_env("PLOT_SHOW", True) +plot_save = get_env("PLOT_SAVE", True) + +# Example name and base workspace +workspace = pl.Path("../examples") example_name = "ex-gwt-mt3dms-p07" +# - -# Model units +# ### Define parameters +# +# Define model units, parameters and other settings. +# + +# Model units length_units = "meters" time_units = "days" -# Table - +# Model parameters nlay = 8 # Number of layers nrow = 15 # Number of rows ncol = 21 # Number of columns @@ -72,7 +71,6 @@ perlen = 100.0 # Simulation time ($days$) # Additional model input - perlen = [100] nper = len(perlen) nstp = [10] @@ -97,7 +95,6 @@ strt[:, :, 0] = h1 # Active model domain - ibound_mf2k5 = np.ones((nlay, nrow, ncol), dtype=int) ibound_mf2k5[:, :, 0] = -1 ibound_mf2k5[:, :, -1] = -1 @@ -106,7 +103,6 @@ # Boundary conditions # MF2K5 pumping info: - qwell = 0.5 welspd = { 0: [[6, 7, 2, qwell]], # Well pumping info for MF2K5 @@ -117,12 +113,10 @@ } # MF6 pumping information - # (k, i, j), flow, conc spd_mf6 = {0: [[(6, 7, 2), qwell, cwell]]} # MF6 constant head boundaries: - chdspd = [] # Loop through the left & right sides. for k in np.arange(nlay): @@ -133,7 +127,6 @@ chdspd = {0: chdspd} # Solver settings - nouter, ninner = 100, 300 hclose, rclose, relax = 1e-6, 1e-6, 1.0 percel = 1.0 # HMOC parameters @@ -150,366 +143,354 @@ npsink = nph # Static temporal data used by TDIS file - tdis_rc = [] tdis_rc.append((perlen, nstp, 1.0)) +# - -# ### Functions to build, write, and run models and plot MT3DMS Example 10 Problem results +# ### Model setup # -# MODFLOW 6 flopy simulation object (sim) is returned if building the model - - -def build_model(sim_name, mixelm=0, silent=False): - if config.buildModel: - mt3d_ws = os.path.join(ws, sim_name, "mt3d") - modelname_mf = "p07-mf" - - # Instantiate the MODFLOW model - mf = flopy.modflow.Modflow( - modelname=modelname_mf, model_ws=mt3d_ws, exe_name=exe_name_mf - ) - - # Instantiate discretization package - # units: itmuni=4 (days), lenuni=2 (m) - flopy.modflow.ModflowDis( - mf, - nlay=nlay, - nrow=nrow, - ncol=ncol, - delr=delr, - delc=delc, - top=top, - botm=botm, - nper=nper, - nstp=nstp, - perlen=perlen, - itmuni=4, - lenuni=2, - ) - - # Instantiate basic package - flopy.modflow.ModflowBas(mf, ibound=ibound_mf2k5, strt=strt) - - # Instantiate layer property flow package - flopy.modflow.ModflowLpf(mf, hk=k11, laytyp=icelltype) - - # Instantiate well package - flopy.modflow.ModflowWel(mf, stress_period_data=welspd) - - # Instantiate solver package - flopy.modflow.ModflowPcg(mf) - - # Instantiate link mass transport package (for writing linker file) - flopy.modflow.ModflowLmt(mf) - - # Transport - modelname_mt = "p07-mt" - mt = flopy.mt3d.Mt3dms( - modelname=modelname_mt, - model_ws=mt3d_ws, - exe_name=exe_name_mt, - modflowmodel=mf, - ) - - # Instantiate basic transport package - flopy.mt3d.Mt3dBtn( - mt, - icbund=icbund, - prsity=prsity, - sconc=sconc, - nper=nper, - perlen=perlen, - dt0=dt0, - ) - - # Instatiate the advection package - flopy.mt3d.Mt3dAdv( - mt, - mixelm=mixelm, - dceps=dceps, - nplane=nplane, - npl=npl, - nph=nph, - npmin=npmin, - npmax=npmax, - nlsink=nlsink, - npsink=npsink, - percel=percel, - itrack=itrack, - wd=wd, - ) - - # Instantiate the dispersion package - flopy.mt3d.Mt3dDsp(mt, al=al, trpt=trpt, trpv=trpv, dmcoef=dmcoef) - - # Instantiate the source/sink mixing package - flopy.mt3d.Mt3dSsm(mt, stress_period_data=spd) - - # Instantiate the GCG solver in MT3DMS - flopy.mt3d.Mt3dGcg(mt) - - # MODFLOW 6 - name = "p07-mf6" - gwfname = "gwf-" + name - sim_ws = os.path.join(ws, sim_name) - sim = flopy.mf6.MFSimulation( - sim_name=sim_name, sim_ws=sim_ws, exe_name=mf6exe - ) - - # Instantiating MODFLOW 6 time discretization - tdis_rc = [] - for i in range(nper): - tdis_rc.append((perlen[i], nstp[i], tsmult[i])) - flopy.mf6.ModflowTdis( - sim, nper=nper, perioddata=tdis_rc, time_units=time_units - ) - - # Instantiating MODFLOW 6 groundwater flow model - gwf = flopy.mf6.ModflowGwf( - sim, - modelname=gwfname, - save_flows=True, - model_nam_file=f"{gwfname}.nam", - ) - - # Instantiating MODFLOW 6 solver for flow model - imsgwf = flopy.mf6.ModflowIms( - sim, - print_option="SUMMARY", - outer_dvclose=hclose, - outer_maximum=nouter, - under_relaxation="NONE", - inner_maximum=ninner, - inner_dvclose=hclose, - rcloserecord=rclose, - linear_acceleration="CG", - scaling_method="NONE", - reordering_method="NONE", - relaxation_factor=relax, - filename=f"{gwfname}.ims", - ) - sim.register_ims_package(imsgwf, [gwf.name]) - - # Instantiating MODFLOW 6 discretization package - flopy.mf6.ModflowGwfdis( - gwf, - length_units=length_units, - nlay=nlay, - nrow=nrow, - ncol=ncol, - delr=delr, - delc=delc, - top=top, - botm=botm, - idomain=idomain, - filename=f"{gwfname}.dis", - ) - - # Instantiating MODFLOW 6 node-property flow package - flopy.mf6.ModflowGwfnpf( - gwf, - save_flows=False, - icelltype=icelltype, - k=k11, - k33=k33, - save_specific_discharge=True, - filename=f"{gwfname}.npf", - ) - - # Instantiating MODFLOW 6 initial conditions package for flow model - flopy.mf6.ModflowGwfic(gwf, strt=strt, filename=f"{gwfname}.ic") - - # Instantiating MODFLOW 6 constant head package - flopy.mf6.ModflowGwfchd( - gwf, - maxbound=len(chdspd), - stress_period_data=chdspd, - save_flows=False, - auxiliary="CONCENTRATION", - pname="CHD-1", - filename=f"{gwfname}.chd", - ) - - # Instantiate the wel package - flopy.mf6.ModflowGwfwel( - gwf, - print_input=True, - print_flows=True, - stress_period_data=spd_mf6, - save_flows=False, - auxiliary="CONCENTRATION", - pname="WEL-1", - filename=f"{gwfname}.wel", - ) - - # Instantiating MODFLOW 6 output control package for flow model - flopy.mf6.ModflowGwfoc( - gwf, - head_filerecord=f"{gwfname}.hds", - budget_filerecord=f"{gwfname}.bud", - headprintrecord=[ - ("COLUMNS", 10, "WIDTH", 15, "DIGITS", 6, "GENERAL") - ], - saverecord=[("HEAD", "LAST"), ("BUDGET", "LAST")], - printrecord=[("HEAD", "LAST"), ("BUDGET", "LAST")], - ) - - # Instantiating MODFLOW 6 groundwater transport package - gwtname = "gwt-" + name - gwt = flopy.mf6.MFModel( - sim, - model_type="gwt6", - modelname=gwtname, - model_nam_file=f"{gwtname}.nam", - ) - gwt.name_file.save_flows = True - - # create iterative model solution and register the gwt model with it - imsgwt = flopy.mf6.ModflowIms( - sim, - print_option="SUMMARY", - outer_dvclose=hclose, - outer_maximum=nouter, - under_relaxation="NONE", - inner_maximum=ninner, - inner_dvclose=hclose, - rcloserecord=rclose, - linear_acceleration="BICGSTAB", - scaling_method="NONE", - reordering_method="NONE", - relaxation_factor=relax, - filename=f"{gwtname}.ims", - ) - sim.register_ims_package(imsgwt, [gwt.name]) - - # Instantiating MODFLOW 6 transport discretization package - flopy.mf6.ModflowGwtdis( +# Define functions to build models, write input files, and run the simulation. + + +# + +def build_models(sim_name, mixelm=0, silent=False): + mt3d_ws = os.path.join(workspace, sim_name, "mt3d") + modelname_mf = "p07-mf" + + # Instantiate the MODFLOW model + mf = flopy.modflow.Modflow( + modelname=modelname_mf, model_ws=mt3d_ws, exe_name="mf2005" + ) + + # Instantiate discretization package + # units: itmuni=4 (days), lenuni=2 (m) + flopy.modflow.ModflowDis( + mf, + nlay=nlay, + nrow=nrow, + ncol=ncol, + delr=delr, + delc=delc, + top=top, + botm=botm, + nper=nper, + nstp=nstp, + perlen=perlen, + itmuni=4, + lenuni=2, + ) + + # Instantiate basic package + flopy.modflow.ModflowBas(mf, ibound=ibound_mf2k5, strt=strt) + + # Instantiate layer property flow package + flopy.modflow.ModflowLpf(mf, hk=k11, laytyp=icelltype) + + # Instantiate well package + flopy.modflow.ModflowWel(mf, stress_period_data=welspd) + + # Instantiate solver package + flopy.modflow.ModflowPcg(mf) + + # Instantiate link mass transport package (for writing linker file) + flopy.modflow.ModflowLmt(mf) + + # Transport + modelname_mt = "p07-mt" + mt = flopy.mt3d.Mt3dms( + modelname=modelname_mt, + model_ws=mt3d_ws, + exe_name="mt3dusgs", + modflowmodel=mf, + ) + + # Instantiate basic transport package + flopy.mt3d.Mt3dBtn( + mt, + icbund=icbund, + prsity=prsity, + sconc=sconc, + nper=nper, + perlen=perlen, + dt0=dt0, + ) + + # Instatiate the advection package + flopy.mt3d.Mt3dAdv( + mt, + mixelm=mixelm, + dceps=dceps, + nplane=nplane, + npl=npl, + nph=nph, + npmin=npmin, + npmax=npmax, + nlsink=nlsink, + npsink=npsink, + percel=percel, + itrack=itrack, + wd=wd, + ) + + # Instantiate the dispersion package + flopy.mt3d.Mt3dDsp(mt, al=al, trpt=trpt, trpv=trpv, dmcoef=dmcoef) + + # Instantiate the source/sink mixing package + flopy.mt3d.Mt3dSsm(mt, stress_period_data=spd) + + # Instantiate the GCG solver in MT3DMS + flopy.mt3d.Mt3dGcg(mt) + + # MODFLOW 6 + name = "p07-mf6" + gwfname = "gwf-" + name + sim_ws = os.path.join(workspace, sim_name) + sim = flopy.mf6.MFSimulation(sim_name=sim_name, sim_ws=sim_ws, exe_name="mf6") + + # Instantiating MODFLOW 6 time discretization + tdis_rc = [] + for i in range(nper): + tdis_rc.append((perlen[i], nstp[i], tsmult[i])) + flopy.mf6.ModflowTdis(sim, nper=nper, perioddata=tdis_rc, time_units=time_units) + + # Instantiating MODFLOW 6 groundwater flow model + gwf = flopy.mf6.ModflowGwf( + sim, + modelname=gwfname, + save_flows=True, + model_nam_file=f"{gwfname}.nam", + ) + + # Instantiating MODFLOW 6 solver for flow model + imsgwf = flopy.mf6.ModflowIms( + sim, + print_option="SUMMARY", + outer_dvclose=hclose, + outer_maximum=nouter, + under_relaxation="NONE", + inner_maximum=ninner, + inner_dvclose=hclose, + rcloserecord=rclose, + linear_acceleration="CG", + scaling_method="NONE", + reordering_method="NONE", + relaxation_factor=relax, + filename=f"{gwfname}.ims", + ) + sim.register_ims_package(imsgwf, [gwf.name]) + + # Instantiating MODFLOW 6 discretization package + flopy.mf6.ModflowGwfdis( + gwf, + length_units=length_units, + nlay=nlay, + nrow=nrow, + ncol=ncol, + delr=delr, + delc=delc, + top=top, + botm=botm, + idomain=idomain, + filename=f"{gwfname}.dis", + ) + + # Instantiating MODFLOW 6 node-property flow package + flopy.mf6.ModflowGwfnpf( + gwf, + save_flows=False, + icelltype=icelltype, + k=k11, + k33=k33, + save_specific_discharge=True, + filename=f"{gwfname}.npf", + ) + + # Instantiating MODFLOW 6 initial conditions package for flow model + flopy.mf6.ModflowGwfic(gwf, strt=strt, filename=f"{gwfname}.ic") + + # Instantiating MODFLOW 6 constant head package + flopy.mf6.ModflowGwfchd( + gwf, + maxbound=len(chdspd), + stress_period_data=chdspd, + save_flows=False, + auxiliary="CONCENTRATION", + pname="CHD-1", + filename=f"{gwfname}.chd", + ) + + # Instantiate the wel package + flopy.mf6.ModflowGwfwel( + gwf, + print_input=True, + print_flows=True, + stress_period_data=spd_mf6, + save_flows=False, + auxiliary="CONCENTRATION", + pname="WEL-1", + filename=f"{gwfname}.wel", + ) + + # Instantiating MODFLOW 6 output control package for flow model + flopy.mf6.ModflowGwfoc( + gwf, + head_filerecord=f"{gwfname}.hds", + budget_filerecord=f"{gwfname}.bud", + headprintrecord=[("COLUMNS", 10, "WIDTH", 15, "DIGITS", 6, "GENERAL")], + saverecord=[("HEAD", "LAST"), ("BUDGET", "LAST")], + printrecord=[("HEAD", "LAST"), ("BUDGET", "LAST")], + ) + + # Instantiating MODFLOW 6 groundwater transport package + gwtname = "gwt-" + name + gwt = flopy.mf6.MFModel( + sim, + model_type="gwt6", + modelname=gwtname, + model_nam_file=f"{gwtname}.nam", + ) + gwt.name_file.save_flows = True + + # create iterative model solution and register the gwt model with it + imsgwt = flopy.mf6.ModflowIms( + sim, + print_option="SUMMARY", + outer_dvclose=hclose, + outer_maximum=nouter, + under_relaxation="NONE", + inner_maximum=ninner, + inner_dvclose=hclose, + rcloserecord=rclose, + linear_acceleration="BICGSTAB", + scaling_method="NONE", + reordering_method="NONE", + relaxation_factor=relax, + filename=f"{gwtname}.ims", + ) + sim.register_ims_package(imsgwt, [gwt.name]) + + # Instantiating MODFLOW 6 transport discretization package + flopy.mf6.ModflowGwtdis( + gwt, + nlay=nlay, + nrow=nrow, + ncol=ncol, + delr=delr, + delc=delc, + top=top, + botm=botm, + idomain=idomain, + filename=f"{gwtname}.dis", + ) + + # Instantiating MODFLOW 6 transport initial concentrations + flopy.mf6.ModflowGwtic(gwt, strt=sconc, filename=f"{gwtname}.ic") + + # Instantiating MODFLOW 6 transport advection package + if mixelm >= 0: + scheme = "UPSTREAM" + elif mixelm == -1: + scheme = "TVD" + else: + raise Exception() + flopy.mf6.ModflowGwtadv(gwt, scheme=scheme, filename=f"{gwtname}.adv") + + # Instantiating MODFLOW 6 transport dispersion package + if al != 0: + flopy.mf6.ModflowGwtdsp( gwt, - nlay=nlay, - nrow=nrow, - ncol=ncol, - delr=delr, - delc=delc, - top=top, - botm=botm, - idomain=idomain, - filename=f"{gwtname}.dis", + alh=al, + ath1=ath1, + atv=atv, + filename=f"{gwtname}.dsp", ) - # Instantiating MODFLOW 6 transport initial concentrations - flopy.mf6.ModflowGwtic(gwt, strt=sconc, filename=f"{gwtname}.ic") - - # Instantiating MODFLOW 6 transport advection package - if mixelm >= 0: - scheme = "UPSTREAM" - elif mixelm == -1: - scheme = "TVD" - else: - raise Exception() - flopy.mf6.ModflowGwtadv(gwt, scheme=scheme, filename=f"{gwtname}.adv") - - # Instantiating MODFLOW 6 transport dispersion package - if al != 0: - flopy.mf6.ModflowGwtdsp( - gwt, - alh=al, - ath1=ath1, - atv=atv, - filename=f"{gwtname}.dsp", - ) - - # Instantiating MODFLOW 6 transport mass storage package (formerly "reaction" package in MT3DMS) - flopy.mf6.ModflowGwtmst( - gwt, - porosity=prsity, - first_order_decay=False, - decay=None, - decay_sorbed=None, - sorption=None, - bulk_density=None, - distcoef=None, - filename=f"{gwtname}.mst", - ) - - # Instantiating MODFLOW 6 transport source-sink mixing package - sourcerecarray = [ - ("WEL-1", "AUX", "CONCENTRATION"), - ("CHD-1", "AUX", "CONCENTRATION"), - ] - flopy.mf6.ModflowGwtssm( - gwt, sources=sourcerecarray, filename=f"{gwtname}.ssm" - ) - - # Instantiating MODFLOW 6 transport output control package - flopy.mf6.ModflowGwtoc( - gwt, - budget_filerecord=f"{gwtname}.cbc", - concentration_filerecord=f"{gwtname}.ucn", - concentrationprintrecord=[ - ("COLUMNS", 10, "WIDTH", 15, "DIGITS", 6, "GENERAL") - ], - saverecord=[("CONCENTRATION", "LAST"), ("BUDGET", "LAST")], - printrecord=[("CONCENTRATION", "LAST"), ("BUDGET", "LAST")], - ) - - # Instantiating MODFLOW 6 flow-transport exchange mechanism - flopy.mf6.ModflowGwfgwt( - sim, - exgtype="GWF6-GWT6", - exgmnamea=gwfname, - exgmnameb=gwtname, - filename=f"{name}.gwfgwt", - ) - return mf, mt, sim - return None - - -# Function to write model files - - -def write_model(mf2k5, mt3d, sim, silent=True): - if config.writeModel: - mf2k5.write_input() - mt3d.write_input() - sim.write_simulation(silent=silent) - - -# Function to run the model. True is returned if the model runs successfully. + # Instantiating MODFLOW 6 transport mass storage package (formerly "reaction" package in MT3DMS) + flopy.mf6.ModflowGwtmst( + gwt, + porosity=prsity, + first_order_decay=False, + decay=None, + decay_sorbed=None, + sorption=None, + bulk_density=None, + distcoef=None, + filename=f"{gwtname}.mst", + ) + + # Instantiating MODFLOW 6 transport source-sink mixing package + sourcerecarray = [ + ("WEL-1", "AUX", "CONCENTRATION"), + ("CHD-1", "AUX", "CONCENTRATION"), + ] + flopy.mf6.ModflowGwtssm(gwt, sources=sourcerecarray, filename=f"{gwtname}.ssm") + + # Instantiating MODFLOW 6 transport output control package + flopy.mf6.ModflowGwtoc( + gwt, + budget_filerecord=f"{gwtname}.cbc", + concentration_filerecord=f"{gwtname}.ucn", + concentrationprintrecord=[("COLUMNS", 10, "WIDTH", 15, "DIGITS", 6, "GENERAL")], + saverecord=[("CONCENTRATION", "LAST"), ("BUDGET", "LAST")], + printrecord=[("CONCENTRATION", "LAST"), ("BUDGET", "LAST")], + ) + + # Instantiating MODFLOW 6 flow-transport exchange mechanism + flopy.mf6.ModflowGwfgwt( + sim, + exgtype="GWF6-GWT6", + exgmnamea=gwfname, + exgmnameb=gwtname, + filename=f"{name}.gwfgwt", + ) + return mf, mt, sim + + +def write_models(mf2k5, mt3d, sim, silent=True): + mf2k5.write_input() + mt3d.write_input() + sim.write_simulation(silent=silent) + + +@timed +def run_models(mf2k5, mt3d, sim, silent=True): + success, buff = mf2k5.run_model(silent=silent, report=True) + assert success, pformat(buff) + success, buff = mt3d.run_model( + silent=silent, normal_msg="Program completed", report=True + ) + assert success, pformat(buff) + success, buff = sim.run_simulation(silent=silent, report=True) + assert success, pformat(buff) + + +# - + +# ### Plotting results +# +# Define functions to plot model results. +# + +# Figure properties +figure_size = (4, 8) -@config.timeit -def run_model(mf2k5, mt3d, sim, silent=True): - success = True - if config.runModel: - success, buff = mf2k5.run_model(silent=silent) - success, buff = mt3d.run_model(silent=silent) - success, buff = sim.run_simulation(silent=silent) - if not success: - print(buff) - return success +def plot_results(mf2k5, mt3d, mf6, idx, ax=None): + mt3d_out_path = mt3d.model_ws + mf6.simulation_data.mfpath.get_sim_path() -# Function to plot the model results + # Get the MT3DMS concentration output + fname_mt3d = os.path.join(mt3d_out_path, "MT3D001.UCN") + ucnobj_mt3d = flopy.utils.UcnFile(fname_mt3d) + conc_mt3d = ucnobj_mt3d.get_alldata() + # Get the MF6 concentration output + gwt = mf6.get_model(list(mf6.model_names)[1]) + ucnobj_mf6 = gwt.output.concentration() + conc_mf6 = ucnobj_mf6.get_alldata() -def plot_results(mf2k5, mt3d, mf6, idx, ax=None): - if config.plotModel: - mt3d_out_path = mt3d.model_ws - mf6_out_path = mf6.simulation_data.mfpath.get_sim_path() - mf6.simulation_data.mfpath.get_sim_path() - - # Get the MT3DMS concentration output - fname_mt3d = os.path.join(mt3d_out_path, "MT3D001.UCN") - ucnobj_mt3d = flopy.utils.UcnFile(fname_mt3d) - conc_mt3d = ucnobj_mt3d.get_alldata() - - # Get the MF6 concentration output - gwt = mf6.get_model(list(mf6.model_names)[1]) - ucnobj_mf6 = gwt.output.concentration() - conc_mf6 = ucnobj_mf6.get_alldata() - - # Create figure for scenario - fs = USGSFigure(figure_type="graph", verbose=False) + # Create figure for scenario + with styles.USGSPlot() as fs: sim_name = mf6.name plt.rcParams["lines.dashed_pattern"] = [5.0, 5.0] axWasNone = False @@ -536,7 +517,7 @@ def plot_results(mf2k5, mt3d, mf6, idx, ax=None): plt.ylabel("DISTANCE ALONG Y-AXIS, IN METERS") title = f"Layer {ilay + 1}" letter = chr(ord("@") + idx + 1) - fs.heading(letter=letter, heading=title) + styles.heading(letter=letter, heading=title) labels = ["MT3DMS", "MODFLOW 6"] lines = [cs1.collections[0], cs2.collections[0]] @@ -549,9 +530,7 @@ def plot_results(mf2k5, mt3d, mf6, idx, ax=None): mm = flopy.plot.PlotMapView(ax=ax, model=mf2k5, layer=ilay) mm.plot_grid(color=".5", alpha=0.2) mm.plot_ibound() - cs = mm.contour_array( - conc_mt3d[0], levels=[0.01, 0.05, 0.15, 0.50], colors="k" - ) + cs = mm.contour_array(conc_mt3d[0], levels=[0.01, 0.05, 0.15, 0.50], colors="k") cs = mm.contour_array( conc_mf6[0], levels=[0.01, 0.05, 0.15, 0.50], @@ -563,7 +542,7 @@ def plot_results(mf2k5, mt3d, mf6, idx, ax=None): plt.ylabel("DISTANCE ALONG Y-AXIS, IN METERS") title = f"Layer {ilay + 1}" letter = chr(ord("@") + idx + 2) - fs.heading(letter=letter, heading=title) + styles.heading(letter=letter, heading=title) if axWasNone: ax = fig.add_subplot(3, 1, 3, aspect="equal") @@ -572,9 +551,7 @@ def plot_results(mf2k5, mt3d, mf6, idx, ax=None): mm = flopy.plot.PlotMapView(ax=ax, model=mf2k5, layer=ilay) mm.plot_grid(color=".5", alpha=0.2) mm.plot_ibound() - cs = mm.contour_array( - conc_mt3d[0], levels=[0.01, 0.05, 0.15, 0.50], colors="k" - ) + cs = mm.contour_array(conc_mt3d[0], levels=[0.01, 0.05, 0.15, 0.50], colors="k") cs = mm.contour_array( conc_mf6[0], levels=[0.01, 0.05, 0.15, 0.50], @@ -586,48 +563,45 @@ def plot_results(mf2k5, mt3d, mf6, idx, ax=None): plt.ylabel("DISTANCE ALONG Y-AXIS, IN METERS") title = f"Layer {ilay + 1}" letter = chr(ord("@") + idx + 3) - fs.heading(letter=letter, heading=title) + styles.heading(letter=letter, heading=title) plt.plot( mf2k5.modelgrid.xcellcenters[7, 2], mf2k5.modelgrid.ycellcenters[7, 2], "ko", ) - # save figure - if config.plotSave: + if plot_show: + plt.show() + if plot_save: fpth = os.path.join( "..", "figures", - f"{sim_name}{config.figure_ext}", + f"{sim_name}.png", ) fig.savefig(fpth) -# ### Function that wraps all of the steps for each MT3DMS Example 10 Problem scenario +# - + +# ### Running the example # -# 1. build_model, -# 2. write_model, -# 3. run_model, and -# 4. plot_results. +# Define and invoke a function to run the example scenario, then plot results. +# + def scenario(idx, silent=True): - mf2k5, mt3d, sim = build_model(example_name, mixelm=mixelm) - write_model(mf2k5, mt3d, sim, silent=silent) - success = run_model(mf2k5, mt3d, sim, silent=silent) - if success: + mf2k5, mt3d, sim = build_models(example_name, mixelm=mixelm) + if write: + write_models(mf2k5, mt3d, sim, silent=silent) + if run: + run_models(mf2k5, mt3d, sim, silent=silent) + if plot: plot_results(mf2k5, mt3d, sim, idx) -# nosetest - exclude block from this nosetest to the next nosetest -def test_01(): - scenario(0, silent=False) +# - -# nosetest end +# Compares the standard finite difference solutions between MT3D and MF6. -if __name__ == "__main__": - # ### Two-Dimensional Transport in a Diagonal Flow Field - # - # Compares the standard finite difference solutions between MT3D MF 6 - scenario(0) +scenario(0) diff --git a/scripts/ex-gwt-mt3dms-p08.py b/scripts/ex-gwt-mt3dms-p08.py index 65dc94c11..634b9ae3b 100644 --- a/scripts/ex-gwt-mt3dms-p08.py +++ b/scripts/ex-gwt-mt3dms-p08.py @@ -18,45 +18,45 @@ # 9. Two-Dimensional Application Example # 10. Three-Dimensional Field Case Study +# ### Initial setup +# +# Import dependencies, define the example name and workspace, and read settings from environment variables. -# ### MODFLOW 6 GWT MT3DMS Example 6 Problem Setup - -# Append to system path to include the common subdirectory - +# + import os -import sys +import pathlib as pl +from pprint import pformat -sys.path.append(os.path.join("..", "common")) - -# Imports - -import config import flopy import matplotlib.pyplot as plt import numpy as np -from figspecs import USGSFigure +import pooch +from flopy.plot.styles import styles from flopy.utils.util_array import read1d +from modflow_devtools.misc import get_env, timed -mf6exe = "mf6" -exe_name_mf = "mf2005" -exe_name_mt = "mt3dusgs" - -# Set figure properties specific to this problem +# Settings from environment variables +write = get_env("WRITE", True) +run = get_env("RUN", True) +plot = get_env("PLOT", True) +plot_show = get_env("PLOT_SHOW", True) +plot_save = get_env("PLOT_SAVE", True) -figure_size = (5, 7) - -# Base simulation and model name and workspace - -ws = config.base_ws +# Example name and base workspace +workspace = pl.Path("../examples") example_name = "ex-gwt-mt3dms-p08" +# - -# Model units +# ### Define parameters +# +# Define model units, parameters and other settings. +# + +# Model units length_units = "meters" time_units = "days" -# Table MODFLOW 6 GWT MT3DMS Example 8 - +# Model parameters nlay = 27 # Number of layers nrow = 1 # Number of rows ncol = 50 # Number of columns @@ -74,7 +74,6 @@ perlen = 20.0 # Simulation time ($years$) # Additional model input - k1 = 5e-4 / 100.0 * 86400 # m/d k2 = 1e-2 / 100.0 * 86400 # m/d k11 = k1 * np.ones((nlay, nrow, ncol), dtype=float) @@ -82,7 +81,11 @@ k11[11:19, :, 36:] = k2 laytyp = 6 * [1] + 21 * [0] # Setting starting head information -f = open(os.path.join("..", "data", "ex-gwt-mt3dms-p08", "p08shead.dat")) +fpth = pooch.retrieve( + url=f"https://github.com/MODFLOW-USGS/modflow6-examples/raw/master/data/ex-gwt-mt3dms-p08/p08shead.dat", + known_hash="md5:673d570ab9d496355470ac598c4b8b55", +) +f = open(fpth) strt = np.empty((nlay * ncol), dtype=float) strt = read1d(f, strt).reshape((nlay, nrow, ncol)) f.close() @@ -114,7 +117,6 @@ # Boundary conditions # MF6 constant head boundaries: - chdspd = [] # Left side of model domain is no flow; right side uses constant heads for k in np.arange(nlay): @@ -142,7 +144,6 @@ cncspd = {0: cncspd_1, 1: cncspd_2} # Solver settings - nouter, ninner = 100, 300 hclose, rclose, relax = 1e-6, 1e-6, 1.0 percel = 1.0 # HMOC parameters @@ -158,426 +159,413 @@ nlsink = nplane npsink = nph -# Static temporal data used by TDIS file - +# Time discretization tdis_rc = [] tdis_rc.append((perlen, nstp, 1.0)) +# - -# ### Functions to build, write, and run models and plot MT3DMS Example 8 +# ### Model setup # -# MODFLOW 6 flopy simulation object (sim) is returned if building the model - - -def build_model(sim_name, mixelm=0, silent=False): - if config.buildModel: - print(f"Building mf2005 model...{sim_name}") - mt3d_ws = os.path.join(ws, sim_name, "mt3d") - modelname_mf = "p08-mf" - - # Instantiate the MODFLOW model - mf = flopy.modflow.Modflow( - modelname=modelname_mf, model_ws=mt3d_ws, exe_name=exe_name_mf - ) - - # Instantiate discretization package - # units: itmuni=4 (days), lenuni=2 (m) - flopy.modflow.ModflowDis( - mf, - nlay=nlay, - nrow=nrow, - ncol=ncol, - delr=delr, - delc=delc, - top=top, - botm=botm, - nper=nper, - nstp=nstp, - perlen=perlen, - itmuni=4, - lenuni=2, - steady=[False, False], - ) - - # Instantiate basic package - flopy.modflow.ModflowBas(mf, ibound=ibound, strt=strt) - - # Instantiate layer property flow package - flopy.modflow.ModflowLpf(mf, hk=k11, vka=k11, laytyp=icelltype) - - # Instantiate recharge package - flopy.modflow.ModflowRch(mf, rech=rech) - - # Instantiate solver package - flopy.modflow.ModflowPcg(mf) - - # Instantiate link mass transport package (for writing linker file) - flopy.modflow.ModflowLmt(mf) - - # Transport - print(f"Building mt3d-usgs model...{sim_name}") - modelname_mt = "p08_mt" - mt = flopy.mt3d.Mt3dms( - modelname=modelname_mt, - model_ws=mt3d_ws, - exe_name=exe_name_mt, - modflowmodel=mf, - ) - - # Instantiate basic transport package - flopy.mt3d.Mt3dBtn( - mt, - icbund=1, - prsity=prsity, - sconc=sconc, - nper=nper, - perlen=perlen, - timprs=np.arange(1, 21) * 365, - dt0=5, - ) - - # Instatiate the advection package - flopy.mt3d.Mt3dAdv( - mt, - mixelm=mixelm, - dceps=dceps, - nplane=nplane, - npl=npl, - nph=nph, - npmin=npmin, - npmax=npmax, - nlsink=nlsink, - npsink=npsink, - percel=percel, - itrack=itrack, - wd=wd, - ) - - # Instantiate the dispersion package - flopy.mt3d.Mt3dDsp(mt, al=al, trpt=trpt, trpv=trpv, dmcoef=dmcoef) - - # Instantiate the source/sink mixing package - flopy.mt3d.Mt3dSsm(mt, stress_period_data=ssmspd) - - # Instantiate the GCG solver in MT3DMS - flopy.mt3d.Mt3dGcg(mt) - - # MODFLOW 6 - print(f"Building mf6gwt model...{sim_name}") - name = "p08_mf6" - gwfname = "gwf_" + name - sim_ws = os.path.join(ws, sim_name) - sim = flopy.mf6.MFSimulation( - sim_name=sim_name, sim_ws=sim_ws, exe_name=mf6exe - ) - - # Instantiating MODFLOW 6 time discretization - tdis_rc = [] - for i in range(nper): - tdis_rc.append((perlen[i], nstp[i], tsmult[i])) - flopy.mf6.ModflowTdis( - sim, nper=nper, perioddata=tdis_rc, time_units=time_units - ) - - # Instantiating MODFLOW 6 groundwater flow model - gwf = flopy.mf6.ModflowGwf( - sim, - modelname=gwfname, - save_flows=True, - model_nam_file=f"{gwfname}.nam", - ) - - # Instantiating MODFLOW 6 solver for flow model - imsgwf = flopy.mf6.ModflowIms( - sim, - print_option="summary", - complexity="complex", - outer_dvclose=hclose, - outer_maximum=nouter, - under_relaxation="dbd", - linear_acceleration="BICGSTAB", - under_relaxation_theta=0.7, - under_relaxation_kappa=0.08, - under_relaxation_gamma=0.05, - under_relaxation_momentum=0.0, - backtracking_number=20, - backtracking_tolerance=2.0, - backtracking_reduction_factor=0.2, - backtracking_residual_limit=5.0e-4, - inner_dvclose=hclose, - rcloserecord="0.0001 relative_rclose", - inner_maximum=ninner, - relaxation_factor=relax, - number_orthogonalizations=2, - preconditioner_levels=8, - preconditioner_drop_tolerance=0.001, - filename=f"{gwfname}.ims", - ) - sim.register_ims_package(imsgwf, [gwf.name]) - - # Instantiating MODFLOW 6 discretization package - flopy.mf6.ModflowGwfdis( - gwf, - length_units=length_units, - nlay=nlay, - nrow=nrow, - ncol=ncol, - delr=delr, - delc=delc, - top=top, - botm=botm, - idomain=idomain, - filename=f"{gwfname}.dis", - ) - - # Instantiating MODFLOW 6 node-property flow package - flopy.mf6.ModflowGwfnpf( - gwf, - save_flows=False, - icelltype=icelltype, - k=k11, - k33=k11, - save_specific_discharge=True, - filename=f"{gwfname}.npf", - ) - - # Instantiate storage package - sto = flopy.mf6.ModflowGwfsto(gwf, ss=0, sy=0) - - # Instantiating MODFLOW 6 initial conditions package for flow model - flopy.mf6.ModflowGwfic(gwf, strt=strt, filename=f"{gwfname}.ic") - - # Instantiating MODFLOW 6 constant head package - flopy.mf6.ModflowGwfchd( - gwf, - maxbound=len(chdspd), - stress_period_data=chdspd, - save_flows=False, - auxiliary="CONCENTRATION", - pname="CHD-1", - filename=f"{gwfname}.chd", - ) - - # Instantiate recharge package - flopy.mf6.ModflowGwfrcha( - gwf, - print_flows=True, - recharge=rech, - pname="RCH-1", - filename=f"{gwfname}.rch", - ) - - # Instantiating MODFLOW 6 output control package for flow model - flopy.mf6.ModflowGwfoc( - gwf, - head_filerecord=f"{gwfname}.hds", - budget_filerecord=f"{gwfname}.bud", - headprintrecord=[ - ("COLUMNS", 10, "WIDTH", 15, "DIGITS", 6, "GENERAL") - ], - saverecord=[("HEAD", "LAST"), ("BUDGET", "LAST")], - printrecord=[("HEAD", "LAST"), ("BUDGET", "LAST")], - ) - - # Instantiating MODFLOW 6 groundwater transport package - gwtname = "gwt_" + name - gwt = flopy.mf6.MFModel( - sim, - model_type="gwt6", - modelname=gwtname, - model_nam_file=f"{gwtname}.nam", - ) - gwt.name_file.save_flows = True - - # create iterative model solution and register the gwt model with it - imsgwt = flopy.mf6.ModflowIms( - sim, - print_option="summary", - complexity="complex", - outer_dvclose=hclose, - outer_maximum=nouter, - under_relaxation="dbd", - linear_acceleration="BICGSTAB", - under_relaxation_theta=0.7, - under_relaxation_kappa=0.08, - under_relaxation_gamma=0.05, - under_relaxation_momentum=0.0, - backtracking_number=20, - backtracking_tolerance=2.0, - backtracking_reduction_factor=0.2, - backtracking_residual_limit=5.0e-4, - inner_dvclose=hclose, - rcloserecord="0.0001 relative_rclose", - inner_maximum=ninner, - relaxation_factor=relax, - number_orthogonalizations=2, - preconditioner_levels=8, - preconditioner_drop_tolerance=0.001, - filename=f"{gwtname}.ims", - ) - sim.register_ims_package(imsgwt, [gwt.name]) - - # Instantiating MODFLOW 6 transport discretization package - flopy.mf6.ModflowGwtdis( - gwt, - nlay=nlay, - nrow=nrow, - ncol=ncol, - delr=delr, - delc=delc, - top=top, - botm=botm, - idomain=idomain, - filename=f"{gwtname}.dis", - ) - - # Instantiating MODFLOW 6 transport initial concentrations - flopy.mf6.ModflowGwtic(gwt, strt=sconc, filename=f"{gwtname}.ic") - - # Instantiating MODFLOW 6 transport advection package - if mixelm >= 0: - scheme = "UPSTREAM" - elif mixelm == -1: - scheme = "TVD" - else: - raise Exception() - flopy.mf6.ModflowGwtadv(gwt, scheme=scheme, filename=f"{gwtname}.adv") - - # Instantiating MODFLOW 6 transport dispersion package - if al != 0: - flopy.mf6.ModflowGwtdsp( - gwt, - alh=al, - ath1=ath1, - atv=atv, - filename=f"{gwtname}.dsp", - ) - - # Instantiating MODFLOW 6 transport mass storage package - flopy.mf6.ModflowGwtmst( - gwt, - porosity=prsity, - first_order_decay=False, - decay=None, - decay_sorbed=None, - sorption=None, - bulk_density=None, - distcoef=None, - filename=f"{gwtname}.mst", - ) - - # Instantiating MODFLOW 6 transport source-sink mixing package - sourcerecarray = [("CHD-1", "AUX", "CONCENTRATION")] - flopy.mf6.ModflowGwtssm( - gwt, sources=sourcerecarray, filename=f"{gwtname}.ssm" - ) - - # Instantiating MODFLOW 6 transport output control package - flopy.mf6.ModflowGwtoc( - gwt, - budget_filerecord=f"{gwtname}.cbc", - concentration_filerecord=f"{gwtname}.ucn", - concentrationprintrecord=[ - ("COLUMNS", 10, "WIDTH", 15, "DIGITS", 6, "GENERAL") - ], - saverecord=[ - ("CONCENTRATION", "LAST"), - ( - "CONCENTRATION", - "STEPS", - "73", - "146", - "219", - "292", - "365", - "438", - "511", - "584", - "657", - "730", - "803", - "876", - "949", - "1022", - "1095", - "1168", - "1241", - "1314", - "1387", - "1460", - ), - ("BUDGET", "LAST"), - ], - printrecord=[("CONCENTRATION", "LAST"), ("BUDGET", "LAST")], - ) - - # Instantiate constant concentration at upper boundary. - flopy.mf6.ModflowGwtcnc( +# Define functions to build models, write input files, and run the simulation. + + +# + +def build_models(sim_name, mixelm=0, silent=False): + print(f"Building mf2005 model...{sim_name}") + mt3d_ws = os.path.join(workspace, sim_name, "mt3d") + modelname_mf = "p08-mf" + + # Instantiate the MODFLOW model + mf = flopy.modflow.Modflow( + modelname=modelname_mf, model_ws=mt3d_ws, exe_name="mf2005" + ) + + # Instantiate discretization package + # units: itmuni=4 (days), lenuni=2 (m) + flopy.modflow.ModflowDis( + mf, + nlay=nlay, + nrow=nrow, + ncol=ncol, + delr=delr, + delc=delc, + top=top, + botm=botm, + nper=nper, + nstp=nstp, + perlen=perlen, + itmuni=4, + lenuni=2, + steady=[False, False], + ) + + # Instantiate basic package + flopy.modflow.ModflowBas(mf, ibound=ibound, strt=strt) + + # Instantiate layer property flow package + flopy.modflow.ModflowLpf(mf, hk=k11, vka=k11, laytyp=icelltype) + + # Instantiate recharge package + flopy.modflow.ModflowRch(mf, rech=rech) + + # Instantiate solver package + flopy.modflow.ModflowPcg(mf) + + # Instantiate link mass transport package (for writing linker file) + flopy.modflow.ModflowLmt(mf) + + # Transport + print(f"Building mt3d-usgs model...{sim_name}") + modelname_mt = "p08_mt" + mt = flopy.mt3d.Mt3dms( + modelname=modelname_mt, + model_ws=mt3d_ws, + exe_name="mt3dusgs", + modflowmodel=mf, + ) + + # Instantiate basic transport package + flopy.mt3d.Mt3dBtn( + mt, + icbund=1, + prsity=prsity, + sconc=sconc, + nper=nper, + perlen=perlen, + timprs=np.arange(1, 21) * 365, + dt0=5, + ) + + # Instatiate the advection package + flopy.mt3d.Mt3dAdv( + mt, + mixelm=mixelm, + dceps=dceps, + nplane=nplane, + npl=npl, + nph=nph, + npmin=npmin, + npmax=npmax, + nlsink=nlsink, + npsink=npsink, + percel=percel, + itrack=itrack, + wd=wd, + ) + + # Instantiate the dispersion package + flopy.mt3d.Mt3dDsp(mt, al=al, trpt=trpt, trpv=trpv, dmcoef=dmcoef) + + # Instantiate the source/sink mixing package + flopy.mt3d.Mt3dSsm(mt, stress_period_data=ssmspd) + + # Instantiate the GCG solver in MT3DMS + flopy.mt3d.Mt3dGcg(mt) + + # MODFLOW 6 + print(f"Building mf6gwt model...{sim_name}") + name = "p08_mf6" + gwfname = "gwf_" + name + sim_ws = os.path.join(workspace, sim_name) + sim = flopy.mf6.MFSimulation(sim_name=sim_name, sim_ws=sim_ws, exe_name="mf6") + + # Instantiating MODFLOW 6 time discretization + tdis_rc = [] + for i in range(nper): + tdis_rc.append((perlen[i], nstp[i], tsmult[i])) + flopy.mf6.ModflowTdis(sim, nper=nper, perioddata=tdis_rc, time_units=time_units) + + # Instantiating MODFLOW 6 groundwater flow model + gwf = flopy.mf6.ModflowGwf( + sim, + modelname=gwfname, + save_flows=True, + model_nam_file=f"{gwfname}.nam", + ) + + # Instantiating MODFLOW 6 solver for flow model + imsgwf = flopy.mf6.ModflowIms( + sim, + print_option="summary", + complexity="complex", + outer_dvclose=hclose, + outer_maximum=nouter, + under_relaxation="dbd", + linear_acceleration="BICGSTAB", + under_relaxation_theta=0.7, + under_relaxation_kappa=0.08, + under_relaxation_gamma=0.05, + under_relaxation_momentum=0.0, + backtracking_number=20, + backtracking_tolerance=2.0, + backtracking_reduction_factor=0.2, + backtracking_residual_limit=5.0e-4, + inner_dvclose=hclose, + rcloserecord="0.0001 relative_rclose", + inner_maximum=ninner, + relaxation_factor=relax, + number_orthogonalizations=2, + preconditioner_levels=8, + preconditioner_drop_tolerance=0.001, + filename=f"{gwfname}.ims", + ) + sim.register_ims_package(imsgwf, [gwf.name]) + + # Instantiating MODFLOW 6 discretization package + flopy.mf6.ModflowGwfdis( + gwf, + length_units=length_units, + nlay=nlay, + nrow=nrow, + ncol=ncol, + delr=delr, + delc=delc, + top=top, + botm=botm, + idomain=idomain, + filename=f"{gwfname}.dis", + ) + + # Instantiating MODFLOW 6 node-property flow package + flopy.mf6.ModflowGwfnpf( + gwf, + save_flows=False, + icelltype=icelltype, + k=k11, + k33=k11, + save_specific_discharge=True, + filename=f"{gwfname}.npf", + ) + + # Instantiate storage package + sto = flopy.mf6.ModflowGwfsto(gwf, ss=0, sy=0) + + # Instantiating MODFLOW 6 initial conditions package for flow model + flopy.mf6.ModflowGwfic(gwf, strt=strt, filename=f"{gwfname}.ic") + + # Instantiating MODFLOW 6 constant head package + flopy.mf6.ModflowGwfchd( + gwf, + maxbound=len(chdspd), + stress_period_data=chdspd, + save_flows=False, + auxiliary="CONCENTRATION", + pname="CHD-1", + filename=f"{gwfname}.chd", + ) + + # Instantiate recharge package + flopy.mf6.ModflowGwfrcha( + gwf, + print_flows=True, + recharge=rech, + pname="RCH-1", + filename=f"{gwfname}.rch", + ) + + # Instantiating MODFLOW 6 output control package for flow model + flopy.mf6.ModflowGwfoc( + gwf, + head_filerecord=f"{gwfname}.hds", + budget_filerecord=f"{gwfname}.bud", + headprintrecord=[("COLUMNS", 10, "WIDTH", 15, "DIGITS", 6, "GENERAL")], + saverecord=[("HEAD", "LAST"), ("BUDGET", "LAST")], + printrecord=[("HEAD", "LAST"), ("BUDGET", "LAST")], + ) + + # Instantiating MODFLOW 6 groundwater transport package + gwtname = "gwt_" + name + gwt = flopy.mf6.MFModel( + sim, + model_type="gwt6", + modelname=gwtname, + model_nam_file=f"{gwtname}.nam", + ) + gwt.name_file.save_flows = True + + # create iterative model solution and register the gwt model with it + imsgwt = flopy.mf6.ModflowIms( + sim, + print_option="summary", + complexity="complex", + outer_dvclose=hclose, + outer_maximum=nouter, + under_relaxation="dbd", + linear_acceleration="BICGSTAB", + under_relaxation_theta=0.7, + under_relaxation_kappa=0.08, + under_relaxation_gamma=0.05, + under_relaxation_momentum=0.0, + backtracking_number=20, + backtracking_tolerance=2.0, + backtracking_reduction_factor=0.2, + backtracking_residual_limit=5.0e-4, + inner_dvclose=hclose, + rcloserecord="0.0001 relative_rclose", + inner_maximum=ninner, + relaxation_factor=relax, + number_orthogonalizations=2, + preconditioner_levels=8, + preconditioner_drop_tolerance=0.001, + filename=f"{gwtname}.ims", + ) + sim.register_ims_package(imsgwt, [gwt.name]) + + # Instantiating MODFLOW 6 transport discretization package + flopy.mf6.ModflowGwtdis( + gwt, + nlay=nlay, + nrow=nrow, + ncol=ncol, + delr=delr, + delc=delc, + top=top, + botm=botm, + idomain=idomain, + filename=f"{gwtname}.dis", + ) + + # Instantiating MODFLOW 6 transport initial concentrations + flopy.mf6.ModflowGwtic(gwt, strt=sconc, filename=f"{gwtname}.ic") + + # Instantiating MODFLOW 6 transport advection package + if mixelm >= 0: + scheme = "UPSTREAM" + elif mixelm == -1: + scheme = "TVD" + else: + raise Exception() + flopy.mf6.ModflowGwtadv(gwt, scheme=scheme, filename=f"{gwtname}.adv") + + # Instantiating MODFLOW 6 transport dispersion package + if al != 0: + flopy.mf6.ModflowGwtdsp( gwt, - print_flows=True, - stress_period_data=cncspd, - pname="CNC-1", - filename=f"{gwtname}.cnc", - ) - - # Instantiating MODFLOW 6 flow-transport exchange mechanism - flopy.mf6.ModflowGwfgwt( - sim, - exgtype="GWF6-GWT6", - exgmnamea=gwfname, - exgmnameb=gwtname, - filename=f"{name}.gwfgwt", - ) - return mf, mt, sim - return None - - -# Function to write model files - - -def write_model(mf2k5, mt3d, sim, silent=True): - if config.writeModel: - mf2k5.write_input() - mt3d.write_input() - sim.write_simulation(silent=silent) - - -# Function to run the model. True is returned if the model runs successfully. + alh=al, + ath1=ath1, + atv=atv, + filename=f"{gwtname}.dsp", + ) + + # Instantiating MODFLOW 6 transport mass storage package + flopy.mf6.ModflowGwtmst( + gwt, + porosity=prsity, + first_order_decay=False, + decay=None, + decay_sorbed=None, + sorption=None, + bulk_density=None, + distcoef=None, + filename=f"{gwtname}.mst", + ) + + # Instantiating MODFLOW 6 transport source-sink mixing package + sourcerecarray = [("CHD-1", "AUX", "CONCENTRATION")] + flopy.mf6.ModflowGwtssm(gwt, sources=sourcerecarray, filename=f"{gwtname}.ssm") + + # Instantiating MODFLOW 6 transport output control package + flopy.mf6.ModflowGwtoc( + gwt, + budget_filerecord=f"{gwtname}.cbc", + concentration_filerecord=f"{gwtname}.ucn", + concentrationprintrecord=[("COLUMNS", 10, "WIDTH", 15, "DIGITS", 6, "GENERAL")], + saverecord=[ + ("CONCENTRATION", "LAST"), + ( + "CONCENTRATION", + "STEPS", + "73", + "146", + "219", + "292", + "365", + "438", + "511", + "584", + "657", + "730", + "803", + "876", + "949", + "1022", + "1095", + "1168", + "1241", + "1314", + "1387", + "1460", + ), + ("BUDGET", "LAST"), + ], + printrecord=[("CONCENTRATION", "LAST"), ("BUDGET", "LAST")], + ) + + # Instantiate constant concentration at upper boundary. + flopy.mf6.ModflowGwtcnc( + gwt, + print_flows=True, + stress_period_data=cncspd, + pname="CNC-1", + filename=f"{gwtname}.cnc", + ) + + # Instantiating MODFLOW 6 flow-transport exchange mechanism + flopy.mf6.ModflowGwfgwt( + sim, + exgtype="GWF6-GWT6", + exgmnamea=gwfname, + exgmnameb=gwtname, + filename=f"{name}.gwfgwt", + ) + return mf, mt, sim + + +def write_models(mf2k5, mt3d, sim, silent=True): + mf2k5.write_input() + mt3d.write_input() + sim.write_simulation(silent=silent) + + +@timed +def run_models(mf2k5, mt3d, sim, silent=True): + success, buff = mf2k5.run_model(silent=silent, report=True) + assert success, pformat(buff) + success, buff = mt3d.run_model( + silent=silent, normal_msg="Program completed", report=True + ) + assert success, pformat(buff) + success, buff = sim.run_simulation(silent=silent, report=True) + assert success, pformat(buff) + + +# - + +# ### Plotting results +# +# Define functions to plot model results. +# + +# Figure properties +figure_size = (5, 7) -@config.timeit -def run_model(mf2k5, mt3d, sim, silent=True): - success = True - if config.runModel: - success, buff = mf2k5.run_model(silent=silent) - success, buff = mt3d.run_model(silent=silent) - success, buff = sim.run_simulation(silent=silent) - if not success: - print(buff) - return success +def plot_results(mf2k5, mt3d, mf6, idx, ax=None): + mt3d_out_path = mt3d.model_ws + mf6.simulation_data.mfpath.get_sim_path() -# Function to plot the model results + # Get the MT3DMS concentration output + fname_mt3d = os.path.join(mt3d_out_path, "MT3D001.UCN") + ucnobj_mt3d = flopy.utils.UcnFile(fname_mt3d) + conc_mt3d = ucnobj_mt3d.get_alldata() + # Get the MF6 concentration output + gwt = mf6.get_model(list(mf6.model_names)[1]) + ucnobj_mf6 = gwt.output.concentration() + conc_mf6 = ucnobj_mf6.get_alldata() -def plot_results(mf2k5, mt3d, mf6, idx, ax=None): - if config.plotModel: - print("Plotting model results...") - mt3d_out_path = mt3d.model_ws - mf6_out_path = mf6.simulation_data.mfpath.get_sim_path() - mf6.simulation_data.mfpath.get_sim_path() - - # Get the MT3DMS concentration output - fname_mt3d = os.path.join(mt3d_out_path, "MT3D001.UCN") - ucnobj_mt3d = flopy.utils.UcnFile(fname_mt3d) - conc_mt3d = ucnobj_mt3d.get_alldata() - - # Get the MF6 concentration output - gwt = mf6.get_model(list(mf6.model_names)[1]) - ucnobj_mf6 = gwt.output.concentration() - conc_mf6 = ucnobj_mf6.get_alldata() - - # Create figure for scenario - fs = USGSFigure(figure_type="graph", verbose=False) + # Create figure for scenario + with styles.USGSPlot() as fs: sim_name = mf6.name plt.rcParams["lines.dashed_pattern"] = [5.0, 5.0] @@ -604,11 +592,9 @@ def plot_results(mf2k5, mt3d, mf6, idx, ax=None): ) plt.clabel(cs, fmt=r"%4.2f") - title = ( - "Migrating plume after " + str(yr_idx[i] + 1) + " years, MT3D-USGS" - ) + title = "Migrating plume after " + str(yr_idx[i] + 1) + " years, MT3D-USGS" letter = chr(ord("@") + idx + 1) - fs.heading(letter=letter, heading=title) + styles.heading(letter=letter, heading=title) if axWasNone: ax = fig.add_subplot(2, 1, 2) @@ -622,20 +608,19 @@ def plot_results(mf2k5, mt3d, mf6, idx, ax=None): ) plt.clabel(cs, fmt=r"%4.2f") - title = ( - "Migrating plume after " + str(yr_idx[i] + 1) + " years, MODFLOW 6" - ) + title = "Migrating plume after " + str(yr_idx[i] + 1) + " years, MODFLOW 6" letter = chr(ord("@") + idx + 2) - fs.heading(letter=letter, heading=title) + styles.heading(letter=letter, heading=title) - # save figure - if config.plotSave: + if plot_show: + plt.show() + if plot_save: fpth = os.path.join( "..", "figures", "{}{}".format( sim_name + "-" + str(yr_idx[i] + 1) + "yrs", - config.figure_ext, + ".png", ), ) fig.savefig(fpth) @@ -656,11 +641,9 @@ def plot_results(mf2k5, mt3d, mf6, idx, ax=None): ) plt.clabel(cs, fmt=r"%4.2f") - title = ( - "Migrating plume after " + str(yr_idx[i] + 1) + " years, MT3D-USGS" - ) + title = "Migrating plume after " + str(yr_idx[i] + 1) + " years, MT3D-USGS" letter = chr(ord("@") + idx + 3) - fs.heading(letter=letter, heading=title) + styles.heading(letter=letter, heading=title) if axWasNone: ax = fig.add_subplot(2, 1, 2) @@ -674,20 +657,19 @@ def plot_results(mf2k5, mt3d, mf6, idx, ax=None): ) plt.clabel(cs, fmt=r"%4.2f") - title = ( - "Migrating plume after " + str(yr_idx[i] + 1) + " years, MODFLOW 6" - ) + title = "Migrating plume after " + str(yr_idx[i] + 1) + " years, MODFLOW 6" letter = chr(ord("@") + idx + 4) - fs.heading(letter=letter, heading=title) + styles.heading(letter=letter, heading=title) - # save figure - if config.plotSave: + if plot_show: + plt.show() + if plot_save: fpth = os.path.join( "..", "figures", "{}{}".format( sim_name + "-" + str(yr_idx[i] + 1) + "yrs", - config.figure_ext, + ".png", ), ) fig.savefig(fpth) @@ -708,11 +690,9 @@ def plot_results(mf2k5, mt3d, mf6, idx, ax=None): ) plt.clabel(cs, fmt=r"%4.2f") - title = ( - "Migrating plume after " + str(yr_idx[i] + 1) + " years, MT3D-USGS" - ) + title = "Migrating plume after " + str(yr_idx[i] + 1) + " years, MT3D-USGS" letter = chr(ord("@") + idx + 5) - fs.heading(letter=letter, heading=title) + styles.heading(letter=letter, heading=title) if axWasNone: ax = fig.add_subplot(2, 1, 2) @@ -726,50 +706,45 @@ def plot_results(mf2k5, mt3d, mf6, idx, ax=None): ) plt.clabel(cs, fmt=r"%4.2f") - title = ( - "Migrating plume after " + str(yr_idx[i] + 1) + " years, MODFLOW 6" - ) + title = "Migrating plume after " + str(yr_idx[i] + 1) + " years, MODFLOW 6" letter = chr(ord("@") + idx + 6) - fs.heading(letter=letter, heading=title) + styles.heading(letter=letter, heading=title) - # save figure - if config.plotSave: + if plot_show: + plt.show() + if plot_save: fpth = os.path.join( "..", "figures", "{}{}".format( sim_name + "-" + str(yr_idx[i] + 1) + "yrs", - config.figure_ext, + ".png", ), ) fig.savefig(fpth) -# ### Function that wraps all of the steps for each MT3DMS Example 10 Problem scenario +# - + +# ### Running the example # -# 1. build_model, -# 2. write_model, -# 3. run_model, and -# 4. plot_results. +# Define and invoke a function to run the example scenario, then plot results. +# + def scenario(idx, silent=True): - mf2k5, mt3d, sim = build_model(example_name, mixelm=mixelm) - write_model(mf2k5, mt3d, sim, silent=silent) - success = run_model(mf2k5, mt3d, sim, silent=silent) - if success: + mf2k5, mt3d, sim = build_models(example_name, mixelm=mixelm) + if write: + write_models(mf2k5, mt3d, sim, silent=silent) + if run: + run_models(mf2k5, mt3d, sim, silent=silent) + if plot: plot_results(mf2k5, mt3d, sim, idx) -# nosetest - exclude block from this nosetest to the next nosetest -def test_01(): - scenario(0, silent=True) +# - -# nosetest end +# Compares the standard finite difference solutions between MT3D and MF6. -if __name__ == "__main__": - # ### Two-Dimensional Transport in a Diagonal Flow Field - # - # Compares the standard finite difference solutions between MT3D MF 6 - scenario(0, silent=True) +scenario(0, silent=True) diff --git a/scripts/ex-gwt-mt3dms-p09.py b/scripts/ex-gwt-mt3dms-p09.py index 9b9e2dc65..747435fac 100644 --- a/scripts/ex-gwt-mt3dms-p09.py +++ b/scripts/ex-gwt-mt3dms-p09.py @@ -18,45 +18,43 @@ # 9. _Two-Dimensional Application Example_ # 10. Three-Dimensional Field Case Study +# ### Initial setup +# +# Import dependencies, define the example name and workspace, and read settings from environment variables. -# ### MODFLOW 6 GWT MT3DMS Example 9 Problem Setup - -# Append to system path to include the common subdirectory - +# + import os -import sys +import pathlib as pl +from pprint import pformat -sys.path.append(os.path.join("..", "common")) - -# Imports - -import config import flopy import matplotlib.pyplot as plt import numpy as np -from figspecs import USGSFigure -from flopy.utils.util_array import read1d - -mf6exe = "mf6" -exe_name_mf = "mf2005" -exe_name_mt = "mt3dusgs" - -# Set figure properties specific to this problem - -figure_size = (7, 5) - -# Base simulation and model name and workspace - -ws = config.base_ws +from flopy.plot.styles import styles +from modflow_devtools.misc import get_env, timed + +# Settings from environment variables +write = get_env("WRITE", True) +run = get_env("RUN", True) +plot = get_env("PLOT", True) +plot_show = get_env("PLOT_SHOW", True) +plot_save = get_env("PLOT_SAVE", True) + +# Example name and base workspace +workspace = pl.Path("../examples") example_name = "ex-gwt-mt3dms-p09" +# - -# Model units +# ### Define parameters +# +# Define model units, parameters and other settings. +# + +# Model units length_units = "meters" time_units = "seconds" -# Table MODFLOW 6 GWT MT3DMS Example 8 - +# Model parameters nlay = 1 # Number of layers nrow = 18 # Number of rows ncol = 14 # Number of columns @@ -74,7 +72,6 @@ perlen = 2.0 # Simulation time ($years$) # Additional model input - hk = k1 * np.ones((nlay, nrow, ncol), dtype=float) hk[:, 5:8, 1:8] = k2 laytyp = icelltype = 0 @@ -90,9 +87,7 @@ # MF2K5 pumping info qwell1 = 0.001 qwell2 = -0.0189 -welspd = { - 0: [[0, 3, 6, qwell1], [0, 10, 6, qwell2]] -} # Well pumping info for MF2K5 +welspd = {0: [[0, 3, 6, qwell1], [0, 10, 6, qwell2]]} # Well pumping info for MF2K5 cwell1 = 57.87 cwell0 = 0.0 spd = { @@ -128,7 +123,6 @@ mixelm = -1 # Solver settings - nouter, ninner = 100, 300 hclose, rclose, relax = 1e-6, 1e-6, 1.0 percel = 1.0 # HMOC parameters @@ -144,397 +138,387 @@ nlsink = nplane npsink = nph nadvfd = 1 +# - -# ### Functions to build, write, and run models and plot MT3DMS Example 9 +# ### Model setup # -# MODFLOW 6 flopy simulation object (sim) is returned if building the model - - -def build_model(sim_name, mixelm=0, silent=False): - if config.buildModel: - print(f"Building mf2005 model...{sim_name}") - mt3d_ws = os.path.join(ws, sim_name, "mt3d") - modelname_mf = "p09-mf" - - # Instantiate the MODFLOW model - mf = flopy.modflow.Modflow( - modelname=modelname_mf, model_ws=mt3d_ws, exe_name=exe_name_mf - ) - - # Instantiate discretization package - # units: itmuni=4 (days), lenuni=2 (m) - flopy.modflow.ModflowDis( - mf, - nlay=nlay, - nrow=nrow, - ncol=ncol, - delr=delr, - delc=delc, - top=top, - botm=botm, - nper=nper, - perlen=perlen, - itmuni=1, - lenuni=2, - steady=steady, - ) - - # Instantiate basic package - strt = np.zeros((nlay, nrow, ncol), dtype=float) - strt[0, 0, :] = 250.0 - xc = mf.modelgrid.xcellcenters - for j in range(ncol): - strt[0, -1, j] = 20.0 + (xc[-1, j] - xc[-1, 0]) * 2.5 / 100 - flopy.modflow.ModflowBas(mf, ibound=ibound, strt=strt) - - # Instantiate layer property flow package - flopy.modflow.ModflowLpf(mf, hk=hk, laytyp=laytyp) - - # Instantiate well package - flopy.modflow.ModflowWel(mf, stress_period_data=welspd) - - # Instantiate solver package - flopy.modflow.ModflowPcg(mf) - - # Instantiate link mass transport package (for writing linker file) - flopy.modflow.ModflowLmt(mf) - - # Transport - print(f"Building mt3d-usgs model...{sim_name}") - - modelname_mt = "p09-mt" - mt = flopy.mt3d.Mt3dms( - modelname=modelname_mt, - model_ws=mt3d_ws, - exe_name=exe_name_mt, - modflowmodel=mf, - ) - - # Instantiate basic transport package - flopy.mt3d.Mt3dBtn( - mt, - icbund=icbund, - prsity=prsity, - sconc=sconc, - mxstrn=86400, - nper=nper, - perlen=perlen, - timprs=[perlen[0], 2 * perlen[1]], - dt0=0, - ) - - # Instatiate the advection package - flopy.mt3d.Mt3dAdv( - mt, - mixelm=mixelm, - dceps=dceps, - nplane=nplane, - npl=npl, - nph=nph, - npmin=npmin, - npmax=npmax, - nlsink=nlsink, - npsink=npsink, - percel=percel, - ) - - # Instantiate the dispersion package - flopy.mt3d.Mt3dDsp(mt, al=al, trpt=trpt, dmcoef=dmcoef) - - # Instantiate the source/sink mixing package - flopy.mt3d.Mt3dSsm(mt, stress_period_data=spd) - - # Instantiate the GCG solver in MT3DMS - flopy.mt3d.Mt3dGcg(mt) - - # MODFLOW 6 - print(f"Building mf6gwt model...{sim_name}") - - name = "p09-mf6" - gwfname = "gwf-" + name - sim_ws = os.path.join(ws, sim_name) - sim = flopy.mf6.MFSimulation( - sim_name=sim_name, sim_ws=sim_ws, exe_name=mf6exe - ) - - # Instantiating MODFLOW 6 time discretization - tdis_rc = [] - for i in range(nper): - tdis_rc.append((perlen[i], nstp[i], tsmult[i])) - flopy.mf6.ModflowTdis( - sim, nper=nper, perioddata=tdis_rc, time_units=time_units - ) - - # Instantiating MODFLOW 6 groundwater flow model - gwf = flopy.mf6.ModflowGwf( - sim, - modelname=gwfname, - save_flows=True, - model_nam_file=f"{gwfname}.nam", - ) - - # Instantiating MODFLOW 6 solver for flow model - imsgwf = flopy.mf6.ModflowIms( - sim, - print_option="SUMMARY", - outer_dvclose=hclose, - outer_maximum=nouter, - under_relaxation="NONE", - inner_maximum=ninner, - inner_dvclose=hclose, - rcloserecord=rclose, - linear_acceleration="CG", - scaling_method="NONE", - reordering_method="NONE", - relaxation_factor=relax, - filename=f"{gwfname}.ims", - ) - sim.register_ims_package(imsgwf, [gwf.name]) - - # Instantiating MODFLOW 6 discretization package - flopy.mf6.ModflowGwfdis( - gwf, - length_units=length_units, - nlay=nlay, - nrow=nrow, - ncol=ncol, - delr=delr, - delc=delc, - top=top, - botm=botm, - idomain=idomain, - filename=f"{gwfname}.dis", - ) - - # Instantiating MODFLOW 6 initial conditions package for flow model - strt = np.zeros((nlay, nrow, ncol), dtype=float) - strt[0, 0, :] = 250.0 - xc = mf.modelgrid.xcellcenters - for j in range(ncol): - strt[0, -1, j] = 20.0 + (xc[-1, j] - xc[-1, 0]) * 2.5 / 100 - flopy.mf6.ModflowGwfic(gwf, strt=strt, filename=f"{gwfname}.ic") - - # Instantiating MODFLOW 6 node-property flow package - flopy.mf6.ModflowGwfnpf( - gwf, - save_flows=False, - icelltype=icelltype, - k=hk, - k33=hk, - save_specific_discharge=True, - filename=f"{gwfname}.npf", - ) - - # Instantiate storage package - sto = flopy.mf6.ModflowGwfsto(gwf, ss=1.0e-05) - - # Instantiating MODFLOW 6 constant head package - # MF6 constant head boundaries: - chdspd = [] - # Loop through the top & bottom sides. - for j in np.arange(ncol): - # l, r, c, head, conc - chdspd.append([(0, 0, j), 250.0, 0.0]) # Top boundary - hd = 20.0 + (xc[-1, j] - xc[-1, 0]) * 2.5 / 100 - chdspd.append([(0, 17, j), hd, 0.0]) # Bottom boundary - chdspd = {0: chdspd} - - flopy.mf6.ModflowGwfchd( - gwf, - maxbound=len(chdspd), - stress_period_data=chdspd, - save_flows=False, - auxiliary="CONCENTRATION", - pname="CHD-1", - filename=f"{gwfname}.chd", - ) - - # Instantiate the wel package - flopy.mf6.ModflowGwfwel( - gwf, - print_input=True, - print_flows=True, - stress_period_data=spd_mf6, - save_flows=False, - auxiliary="CONCENTRATION", - pname="WEL-1", - filename=f"{gwfname}.wel", - ) - - # Instantiating MODFLOW 6 output control package for flow model - flopy.mf6.ModflowGwfoc( - gwf, - head_filerecord=f"{gwfname}.hds", - budget_filerecord=f"{gwfname}.bud", - headprintrecord=[ - ("COLUMNS", 10, "WIDTH", 15, "DIGITS", 6, "GENERAL") - ], - saverecord=[("HEAD", "LAST"), ("BUDGET", "LAST")], - printrecord=[("HEAD", "LAST"), ("BUDGET", "LAST")], - ) - - # Instantiating MODFLOW 6 groundwater transport package - gwtname = "gwt-" + name - gwt = flopy.mf6.MFModel( - sim, - model_type="gwt6", - modelname=gwtname, - model_nam_file=f"{gwtname}.nam", - ) - gwt.name_file.save_flows = True - - # create iterative model solution and register the gwt model with it - imsgwt = flopy.mf6.ModflowIms( - sim, - print_option="SUMMARY", - outer_dvclose=hclose, - outer_maximum=nouter, - under_relaxation="NONE", - inner_maximum=ninner, - inner_dvclose=hclose, - rcloserecord=rclose, - linear_acceleration="BICGSTAB", - scaling_method="NONE", - reordering_method="NONE", - relaxation_factor=relax, - filename=f"{gwtname}.ims", - ) - sim.register_ims_package(imsgwt, [gwt.name]) - - # Instantiating MODFLOW 6 transport discretization package - flopy.mf6.ModflowGwtdis( +# Define functions to build models, write input files, and run the simulation. + + +# + +def build_models(sim_name, mixelm=0, silent=False): + print(f"Building mf2005 model...{sim_name}") + mt3d_ws = os.path.join(workspace, sim_name, "mt3d") + modelname_mf = "p09-mf" + + # Instantiate the MODFLOW model + mf = flopy.modflow.Modflow( + modelname=modelname_mf, model_ws=mt3d_ws, exe_name="mf2005" + ) + + # Instantiate discretization package + # units: itmuni=4 (days), lenuni=2 (m) + flopy.modflow.ModflowDis( + mf, + nlay=nlay, + nrow=nrow, + ncol=ncol, + delr=delr, + delc=delc, + top=top, + botm=botm, + nper=nper, + perlen=perlen, + itmuni=1, + lenuni=2, + steady=steady, + ) + + # Instantiate basic package + strt = np.zeros((nlay, nrow, ncol), dtype=float) + strt[0, 0, :] = 250.0 + xc = mf.modelgrid.xcellcenters + for j in range(ncol): + strt[0, -1, j] = 20.0 + (xc[-1, j] - xc[-1, 0]) * 2.5 / 100 + flopy.modflow.ModflowBas(mf, ibound=ibound, strt=strt) + + # Instantiate layer property flow package + flopy.modflow.ModflowLpf(mf, hk=hk, laytyp=laytyp) + + # Instantiate well package + flopy.modflow.ModflowWel(mf, stress_period_data=welspd) + + # Instantiate solver package + flopy.modflow.ModflowPcg(mf) + + # Instantiate link mass transport package (for writing linker file) + flopy.modflow.ModflowLmt(mf) + + # Transport + print(f"Building mt3d-usgs model...{sim_name}") + + modelname_mt = "p09-mt" + mt = flopy.mt3d.Mt3dms( + modelname=modelname_mt, + model_ws=mt3d_ws, + exe_name="mt3dusgs", + modflowmodel=mf, + ) + + # Instantiate basic transport package + flopy.mt3d.Mt3dBtn( + mt, + icbund=icbund, + prsity=prsity, + sconc=sconc, + mxstrn=86400, + nper=nper, + perlen=perlen, + timprs=[perlen[0], 2 * perlen[1]], + dt0=0, + ) + + # Instatiate the advection package + flopy.mt3d.Mt3dAdv( + mt, + mixelm=mixelm, + dceps=dceps, + nplane=nplane, + npl=npl, + nph=nph, + npmin=npmin, + npmax=npmax, + nlsink=nlsink, + npsink=npsink, + percel=percel, + ) + + # Instantiate the dispersion package + flopy.mt3d.Mt3dDsp(mt, al=al, trpt=trpt, dmcoef=dmcoef) + + # Instantiate the source/sink mixing package + flopy.mt3d.Mt3dSsm(mt, stress_period_data=spd) + + # Instantiate the GCG solver in MT3DMS + flopy.mt3d.Mt3dGcg(mt) + + # MODFLOW 6 + print(f"Building mf6gwt model...{sim_name}") + + name = "p09-mf6" + gwfname = "gwf-" + name + sim_ws = os.path.join(workspace, sim_name) + sim = flopy.mf6.MFSimulation(sim_name=sim_name, sim_ws=sim_ws, exe_name="mf6") + + # Instantiating MODFLOW 6 time discretization + tdis_rc = [] + for i in range(nper): + tdis_rc.append((perlen[i], nstp[i], tsmult[i])) + flopy.mf6.ModflowTdis(sim, nper=nper, perioddata=tdis_rc, time_units=time_units) + + # Instantiating MODFLOW 6 groundwater flow model + gwf = flopy.mf6.ModflowGwf( + sim, + modelname=gwfname, + save_flows=True, + model_nam_file=f"{gwfname}.nam", + ) + + # Instantiating MODFLOW 6 solver for flow model + imsgwf = flopy.mf6.ModflowIms( + sim, + print_option="SUMMARY", + outer_dvclose=hclose, + outer_maximum=nouter, + under_relaxation="NONE", + inner_maximum=ninner, + inner_dvclose=hclose, + rcloserecord=rclose, + linear_acceleration="CG", + scaling_method="NONE", + reordering_method="NONE", + relaxation_factor=relax, + filename=f"{gwfname}.ims", + ) + sim.register_ims_package(imsgwf, [gwf.name]) + + # Instantiating MODFLOW 6 discretization package + flopy.mf6.ModflowGwfdis( + gwf, + length_units=length_units, + nlay=nlay, + nrow=nrow, + ncol=ncol, + delr=delr, + delc=delc, + top=top, + botm=botm, + idomain=idomain, + filename=f"{gwfname}.dis", + ) + + # Instantiating MODFLOW 6 initial conditions package for flow model + strt = np.zeros((nlay, nrow, ncol), dtype=float) + strt[0, 0, :] = 250.0 + xc = mf.modelgrid.xcellcenters + for j in range(ncol): + strt[0, -1, j] = 20.0 + (xc[-1, j] - xc[-1, 0]) * 2.5 / 100 + flopy.mf6.ModflowGwfic(gwf, strt=strt, filename=f"{gwfname}.ic") + + # Instantiating MODFLOW 6 node-property flow package + flopy.mf6.ModflowGwfnpf( + gwf, + save_flows=False, + icelltype=icelltype, + k=hk, + k33=hk, + save_specific_discharge=True, + filename=f"{gwfname}.npf", + ) + + # Instantiate storage package + sto = flopy.mf6.ModflowGwfsto(gwf, ss=1.0e-05) + + # Instantiating MODFLOW 6 constant head package + # MF6 constant head boundaries: + chdspd = [] + # Loop through the top & bottom sides. + for j in np.arange(ncol): + # l, r, c, head, conc + chdspd.append([(0, 0, j), 250.0, 0.0]) # Top boundary + hd = 20.0 + (xc[-1, j] - xc[-1, 0]) * 2.5 / 100 + chdspd.append([(0, 17, j), hd, 0.0]) # Bottom boundary + chdspd = {0: chdspd} + + flopy.mf6.ModflowGwfchd( + gwf, + maxbound=len(chdspd), + stress_period_data=chdspd, + save_flows=False, + auxiliary="CONCENTRATION", + pname="CHD-1", + filename=f"{gwfname}.chd", + ) + + # Instantiate the wel package + flopy.mf6.ModflowGwfwel( + gwf, + print_input=True, + print_flows=True, + stress_period_data=spd_mf6, + save_flows=False, + auxiliary="CONCENTRATION", + pname="WEL-1", + filename=f"{gwfname}.wel", + ) + + # Instantiating MODFLOW 6 output control package for flow model + flopy.mf6.ModflowGwfoc( + gwf, + head_filerecord=f"{gwfname}.hds", + budget_filerecord=f"{gwfname}.bud", + headprintrecord=[("COLUMNS", 10, "WIDTH", 15, "DIGITS", 6, "GENERAL")], + saverecord=[("HEAD", "LAST"), ("BUDGET", "LAST")], + printrecord=[("HEAD", "LAST"), ("BUDGET", "LAST")], + ) + + # Instantiating MODFLOW 6 groundwater transport package + gwtname = "gwt-" + name + gwt = flopy.mf6.MFModel( + sim, + model_type="gwt6", + modelname=gwtname, + model_nam_file=f"{gwtname}.nam", + ) + gwt.name_file.save_flows = True + + # create iterative model solution and register the gwt model with it + imsgwt = flopy.mf6.ModflowIms( + sim, + print_option="SUMMARY", + outer_dvclose=hclose, + outer_maximum=nouter, + under_relaxation="NONE", + inner_maximum=ninner, + inner_dvclose=hclose, + rcloserecord=rclose, + linear_acceleration="BICGSTAB", + scaling_method="NONE", + reordering_method="NONE", + relaxation_factor=relax, + filename=f"{gwtname}.ims", + ) + sim.register_ims_package(imsgwt, [gwt.name]) + + # Instantiating MODFLOW 6 transport discretization package + flopy.mf6.ModflowGwtdis( + gwt, + nlay=nlay, + nrow=nrow, + ncol=ncol, + delr=delr, + delc=delc, + top=top, + botm=botm, + idomain=idomain, + filename=f"{gwtname}.dis", + ) + + # Instantiating MODFLOW 6 transport initial concentrations + flopy.mf6.ModflowGwtic(gwt, strt=sconc, filename=f"{gwtname}.ic") + + # Instantiating MODFLOW 6 transport advection package + if mixelm >= 0: + scheme = "UPSTREAM" + elif mixelm == -1: + scheme = "TVD" + else: + raise Exception() + flopy.mf6.ModflowGwtadv(gwt, scheme=scheme, filename=f"{gwtname}.adv") + + # Instantiating MODFLOW 6 transport dispersion package + if al != 0: + flopy.mf6.ModflowGwtdsp( gwt, - nlay=nlay, - nrow=nrow, - ncol=ncol, - delr=delr, - delc=delc, - top=top, - botm=botm, - idomain=idomain, - filename=f"{gwtname}.dis", + alh=al, + ath1=ath1, + filename=f"{gwtname}.dsp", ) - # Instantiating MODFLOW 6 transport initial concentrations - flopy.mf6.ModflowGwtic(gwt, strt=sconc, filename=f"{gwtname}.ic") - - # Instantiating MODFLOW 6 transport advection package - if mixelm >= 0: - scheme = "UPSTREAM" - elif mixelm == -1: - scheme = "TVD" - else: - raise Exception() - flopy.mf6.ModflowGwtadv(gwt, scheme=scheme, filename=f"{gwtname}.adv") - - # Instantiating MODFLOW 6 transport dispersion package - if al != 0: - flopy.mf6.ModflowGwtdsp( - gwt, - alh=al, - ath1=ath1, - filename=f"{gwtname}.dsp", - ) - - # Instantiating MODFLOW 6 transport mass storage package - flopy.mf6.ModflowGwtmst( - gwt, - porosity=prsity, - first_order_decay=False, - decay=None, - decay_sorbed=None, - sorption=None, - bulk_density=None, - distcoef=None, - filename=f"{gwtname}.mst", - ) - - # Instantiating MODFLOW 6 transport source-sink mixing package - sourcerecarray = [ - ("WEL-1", "AUX", "CONCENTRATION"), - ("CHD-1", "AUX", "CONCENTRATION"), - ] - flopy.mf6.ModflowGwtssm( - gwt, - sources=sourcerecarray, - print_flows=True, - filename=f"{gwtname}.ssm", - ) - - # Instantiating MODFLOW 6 transport output control package - flopy.mf6.ModflowGwtoc( - gwt, - budget_filerecord=f"{gwtname}.cbc", - concentration_filerecord=f"{gwtname}.ucn", - concentrationprintrecord=[ - ("COLUMNS", 10, "WIDTH", 15, "DIGITS", 6, "GENERAL") - ], - saverecord=[("CONCENTRATION", "LAST"), ("BUDGET", "LAST")], - printrecord=[("CONCENTRATION", "LAST"), ("BUDGET", "LAST")], - filename=f"{gwtname}.oc", - ) - - # Instantiating MODFLOW 6 flow-transport exchange mechanism - flopy.mf6.ModflowGwfgwt( - sim, - exgtype="GWF6-GWT6", - exgmnamea=gwfname, - exgmnameb=gwtname, - filename=f"{name}.gwfgwt", - ) - return mf, mt, sim - return None - - -# Function to write model files - - -def write_model(mf2k5, mt3d, sim, silent=True): - if config.writeModel: - mf2k5.write_input() - mt3d.write_input() - sim.write_simulation(silent=silent) - + # Instantiating MODFLOW 6 transport mass storage package + flopy.mf6.ModflowGwtmst( + gwt, + porosity=prsity, + first_order_decay=False, + decay=None, + decay_sorbed=None, + sorption=None, + bulk_density=None, + distcoef=None, + filename=f"{gwtname}.mst", + ) + + # Instantiating MODFLOW 6 transport source-sink mixing package + sourcerecarray = [ + ("WEL-1", "AUX", "CONCENTRATION"), + ("CHD-1", "AUX", "CONCENTRATION"), + ] + flopy.mf6.ModflowGwtssm( + gwt, + sources=sourcerecarray, + print_flows=True, + filename=f"{gwtname}.ssm", + ) + + # Instantiating MODFLOW 6 transport output control package + flopy.mf6.ModflowGwtoc( + gwt, + budget_filerecord=f"{gwtname}.cbc", + concentration_filerecord=f"{gwtname}.ucn", + concentrationprintrecord=[("COLUMNS", 10, "WIDTH", 15, "DIGITS", 6, "GENERAL")], + saverecord=[("CONCENTRATION", "LAST"), ("BUDGET", "LAST")], + printrecord=[("CONCENTRATION", "LAST"), ("BUDGET", "LAST")], + filename=f"{gwtname}.oc", + ) + + # Instantiating MODFLOW 6 flow-transport exchange mechanism + flopy.mf6.ModflowGwfgwt( + sim, + exgtype="GWF6-GWT6", + exgmnamea=gwfname, + exgmnameb=gwtname, + filename=f"{name}.gwfgwt", + ) + return mf, mt, sim + + +def write_models(mf2k5, mt3d, sim, silent=True): + mf2k5.write_input() + mt3d.write_input() + sim.write_simulation(silent=silent) + + +@timed +def run_models(mf2k5, mt3d, sim, silent=True): + success, buff = mf2k5.run_model(silent=silent, report=True) + assert success, pformat(buff) + success, buff = mt3d.run_model( + silent=silent, normal_msg="Program completed", report=True + ) + assert success, pformat(buff) + success, buff = sim.run_simulation(silent=silent, report=True) + assert success, pformat(buff) + + +# - + +# ### Plotting results +# +# Define functions to plot model results. -# Function to run the model. True is returned if the model runs successfully. +# + +# Figure properties +figure_size = (7, 5) -@config.timeit -def run_model(mf2k5, mt3d, sim, silent=True): - success = True - if config.runModel: - success, buff = mf2k5.run_model(silent=silent) - success, buff = mt3d.run_model(silent=silent) - success, buff = sim.run_simulation(silent=silent) - if not success: - print(buff) - return success +def plot_results(mf2k5, mt3d, mf6, idx, ax=None): + mt3d_out_path = mt3d.model_ws + mf6.simulation_data.mfpath.get_sim_path() + # Get the MT3DMS concentration output + fname_mt3d = os.path.join(mt3d_out_path, "MT3D001.UCN") + ucnobj_mt3d = flopy.utils.UcnFile(fname_mt3d) + conc_mt3d = ucnobj_mt3d.get_alldata() -# Function to plot the model results + # Get the MF6 concentration output + gwt = mf6.get_model(list(mf6.model_names)[1]) + ucnobj_mf6 = gwt.output.concentration() + conc_mf6 = ucnobj_mf6.get_alldata() + hk = mf2k5.lpf.hk.array -def plot_results(mf2k5, mt3d, mf6, idx, ax=None): - if config.plotModel: - print("Plotting model results...") - mt3d_out_path = mt3d.model_ws - mf6_out_path = mf6.simulation_data.mfpath.get_sim_path() - mf6.simulation_data.mfpath.get_sim_path() - - # Get the MT3DMS concentration output - fname_mt3d = os.path.join(mt3d_out_path, "MT3D001.UCN") - ucnobj_mt3d = flopy.utils.UcnFile(fname_mt3d) - conc_mt3d = ucnobj_mt3d.get_alldata() - - # Get the MF6 concentration output - gwt = mf6.get_model(list(mf6.model_names)[1]) - ucnobj_mf6 = gwt.output.concentration() - conc_mf6 = ucnobj_mf6.get_alldata() - - hk = mf2k5.lpf.hk.array - - # Create figure for scenario - fs = USGSFigure(figure_type="graph", verbose=False) + # Create figure for scenario + with styles.USGSPlot() as fs: sim_name = mf6.name plt.rcParams["lines.dashed_pattern"] = [5.0, 5.0] @@ -562,7 +546,7 @@ def plot_results(mf2k5, mt3d, mf6, idx, ax=None): title = "MT3D - End of SP " + str(stp_idx + 1) letter = chr(ord("@") + idx + 1) - fs.heading(letter=letter, heading=title) + styles.heading(letter=letter, heading=title) if axWasNone: ax = fig.add_subplot(1, 2, 2, aspect="equal") @@ -580,46 +564,43 @@ def plot_results(mf2k5, mt3d, mf6, idx, ax=None): title = "MODFLOW 6 - End of SP " + str(stp_idx + 1) letter = chr(ord("@") + idx + 2) - fs.heading(letter=letter, heading=title) + styles.heading(letter=letter, heading=title) - # save figure - if config.plotSave: + if plot_show: + plt.show() + if plot_save: fpth = os.path.join( "..", "figures", "{}{}".format( sim_name, - config.figure_ext, + ".png", ), ) fig.savefig(fpth) -# ### Function that wraps all of the steps for each MT3DMS Example 10 Problem scenario +# - + +# ### Running the example # -# 1. build_model, -# 2. write_model, -# 3. run_model, and -# 4. plot_results. +# Define and invoke a function to run the example scenario, then plot results. +# + def scenario(idx, silent=True): - mf2k5, mt3d, sim = build_model(example_name, mixelm=mixelm) - write_model(mf2k5, mt3d, sim, silent=silent) - success = run_model(mf2k5, mt3d, sim, silent=silent) - if success: + mf2k5, mt3d, sim = build_models(example_name, mixelm=mixelm) + if write: + write_models(mf2k5, mt3d, sim, silent=silent) + if run: + run_models(mf2k5, mt3d, sim, silent=silent) + if plot: plot_results(mf2k5, mt3d, sim, idx) -# nosetest - exclude block from this nosetest to the next nosetest -def test_01(): - scenario(0, silent=True) +# - -# nosetest end +# Compares the standard finite difference solutions between MT3D and MF6. -if __name__ == "__main__": - # ### Two-Dimensional Transport in a Diagonal Flow Field - # - # Compares the standard finite difference solutions between MT3D MF 6 - scenario(0, silent=True) +scenario(0, silent=True) diff --git a/scripts/ex-gwt-mt3dms-p10.py b/scripts/ex-gwt-mt3dms-p10.py index 8ca4a1fe6..bbc296ff9 100644 --- a/scripts/ex-gwt-mt3dms-p10.py +++ b/scripts/ex-gwt-mt3dms-p10.py @@ -18,45 +18,45 @@ # 9. Two-Dimensional Application Example # 10. _Three-Dimensional Field Case Study_ +# ### Initial setup +# +# Import dependencies, define the example name and workspace, and read settings from environment variables. -# ### MODFLOW 6 GWT MT3DMS Example 10 Problem Setup - -# Append to system path to include the common subdirectory - +# + import os -import sys +import pathlib as pl +from pprint import pformat -sys.path.append(os.path.join("..", "common")) - -# Imports - -import config import flopy import matplotlib.pyplot as plt import numpy as np -from figspecs import USGSFigure +import pooch +from flopy.plot.styles import styles from flopy.utils.util_array import read1d +from modflow_devtools.misc import get_env, timed -mf6exe = "mf6" -exe_name_mf = "mf2005" -exe_name_mt = "mt3dusgs" - -# Set figure properties specific to this problem +# Settings from environment variables +write = get_env("WRITE", True) +run = get_env("RUN", True) +plot = get_env("PLOT", True) +plot_show = get_env("PLOT_SHOW", True) +plot_save = get_env("PLOT_SAVE", True) -figure_size = (6, 8) - -# Base simulation and model name and workspace - -ws = config.base_ws +# Example name and base workspace +workspace = pl.Path("../examples") example_name = "ex-gwt-mt3dms-p10" +# - -# Model units +# ### Define parameters +# +# Define model units, parameters and other settings. +# + +# Model units length_units = "feet" time_units = "days" -# Table MODFLOW 6 GWT MT3DMS Example 8 - +# Model parameters nlay = 4 # Number of layers nrow = 61 # Number of rows ncol = 40 # Number of columns @@ -79,11 +79,7 @@ perlen = 1000.0 # Simulation time ($days$) # Additional model input -delr = ( - [2000, 1600, 800, 400, 200, 100] - + 28 * [50] - + [100, 200, 400, 800, 1600, 2000] -) +delr = [2000, 1600, 800, 400, 200, 100] + 28 * [50] + [100, 200, 400, 800, 1600, 2000] delc = ( [2000, 2000, 2000, 1600, 800, 400, 200, 100] + 45 * [50] @@ -92,7 +88,11 @@ hk = [60.0, 60.0, 520.0, 520.0] laytyp = icelltype = 0 # Starting Heads: -f = open(os.path.join("..", "data", "ex-gwt-mt3dms-p10", "p10shead.dat")) +fpth = pooch.retrieve( + url=f"https://github.com/MODFLOW-USGS/modflow6-examples/raw/master/data/ex-gwt-mt3dms-p10/p10shead.dat", + known_hash="md5:c6591c3c3cfd023ab930b7b1121bfccf", +) +f = open(fpth) s0 = np.empty((nrow * ncol), dtype=float) s0 = read1d(f, s0).reshape((nrow, ncol)) f.close() @@ -147,7 +147,11 @@ # Transport related # Starting concentrations: -f = open(os.path.join("..", "data", "ex-gwt-mt3dms-p10", "p10cinit.dat")) +fpth = pooch.retrieve( + url=f"https://github.com/MODFLOW-USGS/modflow6-examples/raw/master/data/ex-gwt-mt3dms-p10/p10cinit.dat", + known_hash="md5:8e2d3ba7af1ec65bb07f6039d1dfb2c8", +) +f = open(fpth) c0 = np.empty((nrow * ncol), dtype=float) c0 = read1d(f, c0).reshape((nrow, ncol)) f.close() @@ -193,7 +197,6 @@ ] # Solver settings - nouter, ninner = 100, 300 hclose, rclose, relax = 1e-6, 1e-6, 1.0 percel = 1.0 # HMOC parameters @@ -209,436 +212,418 @@ nlsink = nplane npsink = nph nadvfd = 1 +# - -# ### Functions to build, write, and run models and plot MT3DMS Example 9 +# ### Model setup # -# MODFLOW 6 flopy simulation object (sim) is returned if building the model - - -def build_model(sim_name, mixelm=0, silent=False): - if config.buildModel: - print(f"Building mf2005 model...{sim_name}") - mt3d_ws = os.path.join(ws, sim_name, "mt3d") - modelname_mf = "p10-mf" - - # Instantiate the MODFLOW model - mf = flopy.modflow.Modflow( - modelname=modelname_mf, model_ws=mt3d_ws, exe_name=exe_name_mf - ) - - # Instantiate discretization package - # units: itmuni=4 (days), lenuni=2 (m) - flopy.modflow.ModflowDis( - mf, - nlay=nlay, - nrow=nrow, - ncol=ncol, - delr=delr, - delc=delc, - top=top, - botm=botm, - perlen=perlen, - nstp=nstp, - itmuni=4, - lenuni=1, - ) - - # Instantiate basic package - flopy.modflow.ModflowBas(mf, ibound=ibound, strt=strt) - - # Instantiate layer property flow package - flopy.modflow.ModflowLpf(mf, hk=hk, layvka=1, vka=vka, laytyp=laytyp) - - # Instantiate recharge package - flopy.modflow.ModflowRch(mf, rech=rech) - - # Instantiate well package - flopy.modflow.ModflowWel(mf, stress_period_data=welspd_Q) - - # Instantiate solver package - flopy.modflow.ModflowPcg(mf) - - # Instantiate link mass transport package (for writing linker file) - flopy.modflow.ModflowLmt(mf) - - # Instantiate output control (OC) package - spd = { - (0, 0): ["save head"], - (0, 49): ["save head"], - (0, 74): ["save head"], - (0, 99): ["save head"], - } - oc = flopy.modflow.ModflowOc(mf, stress_period_data=spd) - - # Transport - print(f"Building mt3d-usgs model...{sim_name}") - - modelname_mt = "p10-mt" - mt = flopy.mt3d.Mt3dms( - modelname=modelname_mt, - model_ws=mt3d_ws, - exe_name=exe_name_mt, - modflowmodel=mf, - ) - - # Instantiate basic transport package - flopy.mt3d.Mt3dBtn( - mt, - icbund=icbund, - prsity=prsity, - sconc=sconc, - perlen=perlen, - dt0=2.0, - ttsmult=ttsmult, - timprs=[10, 500, 750, 1000], - obs=obs, - ) - - # Instatiate the advection package - flopy.mt3d.Mt3dAdv( - mt, - mixelm=mixelm, - dceps=dceps, - nplane=nplane, - npl=npl, - nph=nph, - npmin=npmin, - npmax=npmax, - nlsink=nlsink, - npsink=npsink, - percel=percel, - ) - - # Instantiate the dispersion package - flopy.mt3d.Mt3dDsp(mt, al=al, trpt=trpt, trpv=trpv, dmcoef=dmcoef) - - # Instantiate the source/sink mixing package - ssmspd = {0: welspd_ssm} - flopy.mt3d.Mt3dSsm(mt, crch=crch, stress_period_data=ssmspd) - - # Instantiate the recharge package - flopy.mt3d.Mt3dRct( - mt, isothm=isothm, igetsc=0, rhob=rhob, sp1=sp1, sp2=sp2 - ) - - # Instantiate the GCG solver in MT3DMS - flopy.mt3d.Mt3dGcg(mt) - - # MODFLOW 6 - print(f"Building mf6gwt model...{sim_name}") - - name = "p10-mf6" - gwfname = "gwf-" + name - sim_ws = os.path.join(ws, sim_name) - sim = flopy.mf6.MFSimulation( - sim_name=sim_name, sim_ws=sim_ws, exe_name=mf6exe - ) - - # Instantiating MODFLOW 6 time discretization - tdis_rc = [] - tdis_rc.append((perlen, 500, 1.0)) - flopy.mf6.ModflowTdis( - sim, nper=1, perioddata=tdis_rc, time_units=time_units - ) - - # Instantiating MODFLOW 6 groundwater flow model - gwf = flopy.mf6.ModflowGwf( - sim, - modelname=gwfname, - save_flows=True, - model_nam_file=f"{gwfname}.nam", - ) - - # Instantiating MODFLOW 6 solver for flow model - imsgwf = flopy.mf6.ModflowIms( - sim, - print_option="SUMMARY", - outer_dvclose=hclose, - outer_maximum=nouter, - under_relaxation="NONE", - inner_maximum=ninner, - inner_dvclose=hclose, - rcloserecord=rclose, - linear_acceleration="CG", - scaling_method="NONE", - reordering_method="NONE", - relaxation_factor=relax, - filename=f"{gwfname}.ims", - ) - sim.register_ims_package(imsgwf, [gwf.name]) - - # Instantiating MODFLOW 6 discretization package - flopy.mf6.ModflowGwfdis( - gwf, - length_units=length_units, - nlay=nlay, - nrow=nrow, - ncol=ncol, - delr=delr, - delc=delc, - top=top, - botm=botm, - idomain=idomain, - filename=f"{gwfname}.dis", - ) - - # Instantiating MODFLOW 6 initial conditions package for flow model - flopy.mf6.ModflowGwfic(gwf, strt=strt, filename=f"{gwfname}.ic") - - # Instantiating MODFLOW 6 node-property flow package - flopy.mf6.ModflowGwfnpf( - gwf, - save_flows=False, - k33overk=True, - icelltype=laytyp, - k=hk, - k33=vka, - save_specific_discharge=True, - filename=f"{gwfname}.npf", - ) - - # Instantiate storage package - flopy.mf6.ModflowGwfsto(gwf, ss=0, sy=0, filename=f"{gwfname}.sto") - - # Instantiating MODFLOW 6 constant head package - # MF6 constant head boundaries: - chdspd = [] - # Loop through the left & right sides for all layers. - for k in np.arange(nlay): - for i in np.arange(nrow): - # (l, r, c), head, conc - chdspd.append([(k, i, 0), strt[k, i, 0], 0.0]) # left - chdspd.append( - [(k, i, ncol - 1), strt[k, i, ncol - 1], 0.0] - ) # right - - for j in np.arange( - 1, ncol - 1 - ): # skip corners, already added above - # (l, r, c), head, conc - chdspd.append([(k, 0, j), strt[k, 0, j], 0.0]) # top - chdspd.append( - [(k, nrow - 1, j), strt[k, nrow - 1, j], 0.0] - ) # bottom - - chdspd = {0: chdspd} - - flopy.mf6.ModflowGwfchd( - gwf, - maxbound=len(chdspd), - stress_period_data=chdspd, - save_flows=False, - auxiliary="CONCENTRATION", - pname="CHD-1", - filename=f"{gwfname}.chd", - ) - - # Instantiate recharge package - flopy.mf6.ModflowGwfrcha( - gwf, - print_flows=True, - recharge=rech, - pname="RCH-1", - filename=f"{gwfname}.rch", - ) - - # Instantiate the wel package - flopy.mf6.ModflowGwfwel( - gwf, - print_input=True, - print_flows=True, - stress_period_data=wel_mf6_spd, - save_flows=False, - auxiliary="CONCENTRATION", - pname="WEL-1", - filename=f"{gwfname}.wel", - ) - - # Instantiating MODFLOW 6 output control package for flow model - flopy.mf6.ModflowGwfoc( - gwf, - head_filerecord=f"{gwfname}.hds", - budget_filerecord=f"{gwfname}.bud", - headprintrecord=[ - ("COLUMNS", 10, "WIDTH", 15, "DIGITS", 6, "GENERAL") - ], - saverecord=[ - ("HEAD", "LAST"), - ("HEAD", "STEPS", "1", "250", "375", "500"), - ("BUDGET", "LAST"), - ], - printrecord=[ - ("HEAD", "LAST"), - ("BUDGET", "FIRST"), - ("BUDGET", "LAST"), - ], - ) - - # Instantiating MODFLOW 6 groundwater transport package - gwtname = "gwt-" + name - gwt = flopy.mf6.MFModel( - sim, - model_type="gwt6", - modelname=gwtname, - model_nam_file=f"{gwtname}.nam", - ) - gwt.name_file.save_flows = True - - # create iterative model solution and register the gwt model with it - imsgwt = flopy.mf6.ModflowIms( - sim, - print_option="SUMMARY", - outer_dvclose=hclose, - outer_maximum=nouter, - under_relaxation="NONE", - inner_maximum=ninner, - inner_dvclose=hclose, - rcloserecord=rclose, - linear_acceleration="BICGSTAB", - scaling_method="NONE", - reordering_method="NONE", - relaxation_factor=relax, - filename=f"{gwtname}.ims", - ) - sim.register_ims_package(imsgwt, [gwt.name]) - - # Instantiating MODFLOW 6 transport discretization package - flopy.mf6.ModflowGwtdis( - gwt, - nlay=nlay, - nrow=nrow, - ncol=ncol, - delr=delr, - delc=delc, - top=top, - botm=botm, - idomain=idomain, - filename=f"{gwtname}.dis", - ) - - # Instantiating MODFLOW 6 transport initial concentrations - flopy.mf6.ModflowGwtic(gwt, strt=sconc, filename=f"{gwtname}.ic") - - # Instantiating MODFLOW 6 transport advection package - if mixelm >= 0: - scheme = "UPSTREAM" - elif mixelm == -1: - scheme = "TVD" - else: - raise Exception() - flopy.mf6.ModflowGwtadv(gwt, scheme=scheme, filename=f"{gwtname}.adv") - - # Instantiating MODFLOW 6 transport dispersion package - if al != 0: - flopy.mf6.ModflowGwtdsp( - gwt, - alh=al, - ath1=ath1, - atv=atv, - pname="DSP-1", - filename=f"{gwtname}.dsp", - ) - - # Instantiating MODFLOW 6 transport mass storage package - Kd = sp1 - flopy.mf6.ModflowGwtmst( - gwt, - porosity=prsity, - first_order_decay=False, - decay=None, - decay_sorbed=None, - sorption="linear", - bulk_density=rhob, - distcoef=Kd, - pname="MST-1", - filename=f"{gwtname}.mst", - ) - - # Instantiating MODFLOW 6 transport source-sink mixing package - sourcerecarray = [("CHD-1", "AUX", "CONCENTRATION")] - flopy.mf6.ModflowGwtssm( - gwt, - sources=sourcerecarray, - print_flows=True, - filename=f"{gwtname}.ssm", - ) - - # Instantiating MODFLOW 6 transport output control package - flopy.mf6.ModflowGwtoc( +# Define functions to build models, write input files, and run the simulation. + + +# + +def build_models(sim_name, mixelm=0, silent=False): + print(f"Building mf2005 model...{sim_name}") + mt3d_ws = os.path.join(workspace, sim_name, "mt3d") + modelname_mf = "p10-mf" + + # Instantiate the MODFLOW model + mf = flopy.modflow.Modflow( + modelname=modelname_mf, model_ws=mt3d_ws, exe_name="mf2005" + ) + + # Instantiate discretization package + # units: itmuni=4 (days), lenuni=2 (m) + flopy.modflow.ModflowDis( + mf, + nlay=nlay, + nrow=nrow, + ncol=ncol, + delr=delr, + delc=delc, + top=top, + botm=botm, + perlen=perlen, + nstp=nstp, + itmuni=4, + lenuni=1, + ) + + # Instantiate basic package + flopy.modflow.ModflowBas(mf, ibound=ibound, strt=strt) + + # Instantiate layer property flow package + flopy.modflow.ModflowLpf(mf, hk=hk, layvka=1, vka=vka, laytyp=laytyp) + + # Instantiate recharge package + flopy.modflow.ModflowRch(mf, rech=rech) + + # Instantiate well package + flopy.modflow.ModflowWel(mf, stress_period_data=welspd_Q) + + # Instantiate solver package + flopy.modflow.ModflowPcg(mf) + + # Instantiate link mass transport package (for writing linker file) + flopy.modflow.ModflowLmt(mf) + + # Instantiate output control (OC) package + spd = { + (0, 0): ["save head"], + (0, 49): ["save head"], + (0, 74): ["save head"], + (0, 99): ["save head"], + } + oc = flopy.modflow.ModflowOc(mf, stress_period_data=spd) + + # Transport + print(f"Building mt3d-usgs model...{sim_name}") + + modelname_mt = "p10-mt" + mt = flopy.mt3d.Mt3dms( + modelname=modelname_mt, + model_ws=mt3d_ws, + exe_name="mt3dusgs", + modflowmodel=mf, + ) + + # Instantiate basic transport package + flopy.mt3d.Mt3dBtn( + mt, + icbund=icbund, + prsity=prsity, + sconc=sconc, + perlen=perlen, + dt0=2.0, + ttsmult=ttsmult, + timprs=[10, 500, 750, 1000], + obs=obs, + ) + + # Instatiate the advection package + flopy.mt3d.Mt3dAdv( + mt, + mixelm=mixelm, + dceps=dceps, + nplane=nplane, + npl=npl, + nph=nph, + npmin=npmin, + npmax=npmax, + nlsink=nlsink, + npsink=npsink, + percel=percel, + ) + + # Instantiate the dispersion package + flopy.mt3d.Mt3dDsp(mt, al=al, trpt=trpt, trpv=trpv, dmcoef=dmcoef) + + # Instantiate the source/sink mixing package + ssmspd = {0: welspd_ssm} + flopy.mt3d.Mt3dSsm(mt, crch=crch, stress_period_data=ssmspd) + + # Instantiate the recharge package + flopy.mt3d.Mt3dRct(mt, isothm=isothm, igetsc=0, rhob=rhob, sp1=sp1, sp2=sp2) + + # Instantiate the GCG solver in MT3DMS + flopy.mt3d.Mt3dGcg(mt) + + # MODFLOW 6 + print(f"Building mf6gwt model...{sim_name}") + + name = "p10-mf6" + gwfname = "gwf-" + name + sim_ws = os.path.join(workspace, sim_name) + sim = flopy.mf6.MFSimulation(sim_name=sim_name, sim_ws=sim_ws, exe_name="mf6") + + # Instantiating MODFLOW 6 time discretization + tdis_rc = [] + tdis_rc.append((perlen, 500, 1.0)) + flopy.mf6.ModflowTdis(sim, nper=1, perioddata=tdis_rc, time_units=time_units) + + # Instantiating MODFLOW 6 groundwater flow model + gwf = flopy.mf6.ModflowGwf( + sim, + modelname=gwfname, + save_flows=True, + model_nam_file=f"{gwfname}.nam", + ) + + # Instantiating MODFLOW 6 solver for flow model + imsgwf = flopy.mf6.ModflowIms( + sim, + print_option="SUMMARY", + outer_dvclose=hclose, + outer_maximum=nouter, + under_relaxation="NONE", + inner_maximum=ninner, + inner_dvclose=hclose, + rcloserecord=rclose, + linear_acceleration="CG", + scaling_method="NONE", + reordering_method="NONE", + relaxation_factor=relax, + filename=f"{gwfname}.ims", + ) + sim.register_ims_package(imsgwf, [gwf.name]) + + # Instantiating MODFLOW 6 discretization package + flopy.mf6.ModflowGwfdis( + gwf, + length_units=length_units, + nlay=nlay, + nrow=nrow, + ncol=ncol, + delr=delr, + delc=delc, + top=top, + botm=botm, + idomain=idomain, + filename=f"{gwfname}.dis", + ) + + # Instantiating MODFLOW 6 initial conditions package for flow model + flopy.mf6.ModflowGwfic(gwf, strt=strt, filename=f"{gwfname}.ic") + + # Instantiating MODFLOW 6 node-property flow package + flopy.mf6.ModflowGwfnpf( + gwf, + save_flows=False, + k33overk=True, + icelltype=laytyp, + k=hk, + k33=vka, + save_specific_discharge=True, + filename=f"{gwfname}.npf", + ) + + # Instantiate storage package + flopy.mf6.ModflowGwfsto(gwf, ss=0, sy=0, filename=f"{gwfname}.sto") + + # Instantiating MODFLOW 6 constant head package + # MF6 constant head boundaries: + chdspd = [] + # Loop through the left & right sides for all layers. + for k in np.arange(nlay): + for i in np.arange(nrow): + # (l, r, c), head, conc + chdspd.append([(k, i, 0), strt[k, i, 0], 0.0]) # left + chdspd.append([(k, i, ncol - 1), strt[k, i, ncol - 1], 0.0]) # right + + for j in np.arange(1, ncol - 1): # skip corners, already added above + # (l, r, c), head, conc + chdspd.append([(k, 0, j), strt[k, 0, j], 0.0]) # top + chdspd.append([(k, nrow - 1, j), strt[k, nrow - 1, j], 0.0]) # bottom + + chdspd = {0: chdspd} + + flopy.mf6.ModflowGwfchd( + gwf, + maxbound=len(chdspd), + stress_period_data=chdspd, + save_flows=False, + auxiliary="CONCENTRATION", + pname="CHD-1", + filename=f"{gwfname}.chd", + ) + + # Instantiate recharge package + flopy.mf6.ModflowGwfrcha( + gwf, + print_flows=True, + recharge=rech, + pname="RCH-1", + filename=f"{gwfname}.rch", + ) + + # Instantiate the wel package + flopy.mf6.ModflowGwfwel( + gwf, + print_input=True, + print_flows=True, + stress_period_data=wel_mf6_spd, + save_flows=False, + auxiliary="CONCENTRATION", + pname="WEL-1", + filename=f"{gwfname}.wel", + ) + + # Instantiating MODFLOW 6 output control package for flow model + flopy.mf6.ModflowGwfoc( + gwf, + head_filerecord=f"{gwfname}.hds", + budget_filerecord=f"{gwfname}.bud", + headprintrecord=[("COLUMNS", 10, "WIDTH", 15, "DIGITS", 6, "GENERAL")], + saverecord=[ + ("HEAD", "LAST"), + ("HEAD", "STEPS", "1", "250", "375", "500"), + ("BUDGET", "LAST"), + ], + printrecord=[ + ("HEAD", "LAST"), + ("BUDGET", "FIRST"), + ("BUDGET", "LAST"), + ], + ) + + # Instantiating MODFLOW 6 groundwater transport package + gwtname = "gwt-" + name + gwt = flopy.mf6.MFModel( + sim, + model_type="gwt6", + modelname=gwtname, + model_nam_file=f"{gwtname}.nam", + ) + gwt.name_file.save_flows = True + + # create iterative model solution and register the gwt model with it + imsgwt = flopy.mf6.ModflowIms( + sim, + print_option="SUMMARY", + outer_dvclose=hclose, + outer_maximum=nouter, + under_relaxation="NONE", + inner_maximum=ninner, + inner_dvclose=hclose, + rcloserecord=rclose, + linear_acceleration="BICGSTAB", + scaling_method="NONE", + reordering_method="NONE", + relaxation_factor=relax, + filename=f"{gwtname}.ims", + ) + sim.register_ims_package(imsgwt, [gwt.name]) + + # Instantiating MODFLOW 6 transport discretization package + flopy.mf6.ModflowGwtdis( + gwt, + nlay=nlay, + nrow=nrow, + ncol=ncol, + delr=delr, + delc=delc, + top=top, + botm=botm, + idomain=idomain, + filename=f"{gwtname}.dis", + ) + + # Instantiating MODFLOW 6 transport initial concentrations + flopy.mf6.ModflowGwtic(gwt, strt=sconc, filename=f"{gwtname}.ic") + + # Instantiating MODFLOW 6 transport advection package + if mixelm >= 0: + scheme = "UPSTREAM" + elif mixelm == -1: + scheme = "TVD" + else: + raise Exception() + flopy.mf6.ModflowGwtadv(gwt, scheme=scheme, filename=f"{gwtname}.adv") + + # Instantiating MODFLOW 6 transport dispersion package + if al != 0: + flopy.mf6.ModflowGwtdsp( gwt, - budget_filerecord=f"{gwtname}.cbc", - concentration_filerecord=f"{gwtname}.ucn", - concentrationprintrecord=[ - ("COLUMNS", 10, "WIDTH", 15, "DIGITS", 6, "GENERAL") - ], - saverecord=[ - ("CONCENTRATION", "LAST"), - ("CONCENTRATION", "STEPS", "1", "250", "375", "500"), - ("BUDGET", "LAST"), - ], - printrecord=[("CONCENTRATION", "LAST"), ("BUDGET", "LAST")], - filename=f"{gwtname}.oc", - ) - - # Instantiating MODFLOW 6 flow-transport exchange mechanism - flopy.mf6.ModflowGwfgwt( - sim, - exgtype="GWF6-GWT6", - exgmnamea=gwfname, - exgmnameb=gwtname, - filename=f"{name}.gwfgwt", + alh=al, + ath1=ath1, + atv=atv, + pname="DSP-1", + filename=f"{gwtname}.dsp", ) - return mf, mt, sim - return None - - -# Function to write model files + # Instantiating MODFLOW 6 transport mass storage package + Kd = sp1 + flopy.mf6.ModflowGwtmst( + gwt, + porosity=prsity, + first_order_decay=False, + decay=None, + decay_sorbed=None, + sorption="linear", + bulk_density=rhob, + distcoef=Kd, + pname="MST-1", + filename=f"{gwtname}.mst", + ) + + # Instantiating MODFLOW 6 transport source-sink mixing package + sourcerecarray = [("CHD-1", "AUX", "CONCENTRATION")] + flopy.mf6.ModflowGwtssm( + gwt, + sources=sourcerecarray, + print_flows=True, + filename=f"{gwtname}.ssm", + ) + + # Instantiating MODFLOW 6 transport output control package + flopy.mf6.ModflowGwtoc( + gwt, + budget_filerecord=f"{gwtname}.cbc", + concentration_filerecord=f"{gwtname}.ucn", + concentrationprintrecord=[("COLUMNS", 10, "WIDTH", 15, "DIGITS", 6, "GENERAL")], + saverecord=[ + ("CONCENTRATION", "LAST"), + ("CONCENTRATION", "STEPS", "1", "250", "375", "500"), + ("BUDGET", "LAST"), + ], + printrecord=[("CONCENTRATION", "LAST"), ("BUDGET", "LAST")], + filename=f"{gwtname}.oc", + ) + + # Instantiating MODFLOW 6 flow-transport exchange mechanism + flopy.mf6.ModflowGwfgwt( + sim, + exgtype="GWF6-GWT6", + exgmnamea=gwfname, + exgmnameb=gwtname, + filename=f"{name}.gwfgwt", + ) + return mf, mt, sim + + +def write_models(mf2k5, mt3d, sim, silent=True): + mf2k5.write_input() + mt3d.write_input() + sim.write_simulation(silent=silent) + + +@timed +def run_models(mf2k5, mt3d, sim, silent=True): + success, buff = mf2k5.run_model(silent=silent, report=True) + # assert success, pformat(buff) # expect convergence failure + success, buff = mt3d.run_model( + silent=silent, report=True, normal_msg="Program completed" + ) + assert success, pformat(buff) + success, buff = sim.run_simulation(silent=silent, report=True) + assert success, pformat(buff) + + +# - + +# ### Plotting results +# +# Define functions to plot model results. -def write_model(mf2k5, mt3d, sim, silent=True): - if config.writeModel: - mf2k5.write_input() - mt3d.write_input() - sim.write_simulation(silent=silent) - - -# Function to run the model. True is returned if the model runs successfully. - +# + +# Figure properties +figure_size = (6, 8) -@config.timeit -def run_model(mf2k5, mt3d, sim, silent=True): - success = True - if config.runModel: - success, buff = mf2k5.run_model(silent=silent) - success, buff = mt3d.run_model(silent=silent) - success, buff = sim.run_simulation(silent=silent) - if not success: - print(buff) - return success +def plot_results(mf2k5, mt3d, mf6, idx, ax=None): + mt3d_out_path = mt3d.model_ws + mf6.simulation_data.mfpath.get_sim_path() -# Function to plot the model results + # Get the MT3DMS concentration output + fname_mt3d = os.path.join(mt3d_out_path, "MT3D001.UCN") + ucnobj_mt3d = flopy.utils.UcnFile(fname_mt3d) + conc_mt3d = ucnobj_mt3d.get_alldata() + # Get the MF6 concentration output + gwt = mf6.get_model(list(mf6.model_names)[1]) + ucnobj_mf6 = gwt.output.concentration() + conc_mf6 = ucnobj_mf6.get_alldata() -def plot_results(mf2k5, mt3d, mf6, idx, ax=None): - if config.plotModel: - print("Plotting model results...") - mt3d_out_path = mt3d.model_ws - mf6_out_path = mf6.simulation_data.mfpath.get_sim_path() - mf6.simulation_data.mfpath.get_sim_path() - - # Get the MT3DMS concentration output - fname_mt3d = os.path.join(mt3d_out_path, "MT3D001.UCN") - ucnobj_mt3d = flopy.utils.UcnFile(fname_mt3d) - conc_mt3d = ucnobj_mt3d.get_alldata() - - # Get the MF6 concentration output - gwt = mf6.get_model(list(mf6.model_names)[1]) - ucnobj_mf6 = gwt.output.concentration() - conc_mf6 = ucnobj_mf6.get_alldata() - - # Create figure for scenario - fs = USGSFigure(figure_type="graph", verbose=False) + # Create figure for scenario + with styles.USGSPlot(): sim_name = mf6.name plt.rcParams["lines.dashed_pattern"] = [5.0, 5.0] @@ -667,7 +652,7 @@ def plot_results(mf2k5, mt3d, mf6, idx, ax=None): title = "Layer 3 Initial Concentration" letter = chr(ord("@") + idx + 1) - fs.heading(letter=letter, heading=title) + styles.heading(letter=letter, heading=title) # 2nd figure if axWasNone: @@ -676,9 +661,7 @@ def plot_results(mf2k5, mt3d, mf6, idx, ax=None): c = conc_mt3d[1, 2] # Layer 3 @ 500 days (2nd specified output time) mm = flopy.plot.PlotMapView(model=mf2k5) mm.plot_grid(color=".5", alpha=0.2) - cs1 = mm.contour_array( - c, levels=np.arange(10, 200, 10), colors="black" - ) + cs1 = mm.contour_array(c, levels=np.arange(10, 200, 10), colors="black") plt.clabel(cs1, fmt=r"%3d") c_mf6 = conc_mf6[1, 2] # Layer 3 @ 500 days cs2 = mm.contour_array( @@ -698,7 +681,7 @@ def plot_results(mf2k5, mt3d, mf6, idx, ax=None): title = "MT3D Layer 3 Time = 500 days" letter = chr(ord("@") + idx + 2) - fs.heading(letter=letter, heading=title) + styles.heading(letter=letter, heading=title) # 3rd figure if axWasNone: @@ -706,9 +689,7 @@ def plot_results(mf2k5, mt3d, mf6, idx, ax=None): c = conc_mt3d[2, 2] mm = flopy.plot.PlotMapView(model=mf2k5) mm.plot_grid(color=".5", alpha=0.2) - cs1 = mm.contour_array( - c, levels=np.arange(10, 200, 10), colors="black" - ) + cs1 = mm.contour_array(c, levels=np.arange(10, 200, 10), colors="black") plt.clabel(cs1, fmt=r"%3d") c_mf6 = conc_mf6[2, 2] cs2 = mm.contour_array( @@ -723,7 +704,7 @@ def plot_results(mf2k5, mt3d, mf6, idx, ax=None): title = "MT3D Layer 3 Time = 750 days" letter = chr(ord("@") + idx + 3) - fs.heading(letter=letter, heading=title) + styles.heading(letter=letter, heading=title) # 4th figure if axWasNone: @@ -731,9 +712,7 @@ def plot_results(mf2k5, mt3d, mf6, idx, ax=None): c = conc_mt3d[3, 2] mm = flopy.plot.PlotMapView(model=mf2k5) mm.plot_grid(color=".5", alpha=0.2) - cs1 = mm.contour_array( - c, levels=np.arange(10, 200, 10), colors="black" - ) + cs1 = mm.contour_array(c, levels=np.arange(10, 200, 10), colors="black") plt.clabel(cs1, fmt=r"%3d") c_mf6 = conc_mf6[3, 2] cs2 = mm.contour_array( @@ -748,48 +727,45 @@ def plot_results(mf2k5, mt3d, mf6, idx, ax=None): title = "MT3D Layer 3 Time = 1,000 days" letter = chr(ord("@") + idx + 4) - fs.heading(letter=letter, heading=title) + styles.heading(letter=letter, heading=title) plt.tight_layout() - # save figure - if config.plotSave: + if plot_show: + plt.show() + if plot_save: fpth = os.path.join( "..", "figures", "{}{}".format( sim_name, - config.figure_ext, + ".png", ), ) fig.savefig(fpth) -# ### Function that wraps all of the steps for each MT3DMS Example 10 Problem scenario +# - + +# ### Running the example # -# 1. build_model, -# 2. write_model, -# 3. run_model, and -# 4. plot_results. +# Define and invoke a function to run the example scenario, then plot results. +# + def scenario(idx, silent=True): - mf2k5, mt3d, sim = build_model(example_name, mixelm=mixelm) - write_model(mf2k5, mt3d, sim, silent=silent) - success = run_model(mf2k5, mt3d, sim, silent=silent) - if success: + mf2k5, mt3d, sim = build_models(example_name, mixelm=mixelm) + if write: + write_models(mf2k5, mt3d, sim, silent=silent) + if run: + run_models(mf2k5, mt3d, sim, silent=silent) + if plot: plot_results(mf2k5, mt3d, sim, idx) -# nosetest - exclude block from this nosetest to the next nosetest -def test_01(): - scenario(0, silent=True) +# - -# nosetest end +# Compares the standard finite difference solutions between MT3D and MF6. -if __name__ == "__main__": - # ### Two-Dimensional Transport in a Diagonal Flow Field - # - # Compares the standard finite difference solutions between MT3D MF 6 - scenario(0, silent=True) +scenario(0) diff --git a/scripts/ex-gwt-mt3dsupp631.py b/scripts/ex-gwt-mt3dsupp631.py index f2a8f142c..608f51ee7 100644 --- a/scripts/ex-gwt-mt3dsupp631.py +++ b/scripts/ex-gwt-mt3dsupp631.py @@ -1,45 +1,49 @@ # ## Zero-Order Growth in a Uniform Flow Field # -# MT3DMS Supplemental Guide Problem 6.3.1 +# This example is for zero-order production in a uniform flow field. +# It is based on example problem 6.3.1 described in Zheng 2010. The +# problem consists of a one-dimensional model grid with inflow into +# the first cell and outflow through the last cell. This example is +# simulated with a GWT model in, which receives flow information from +# a GWF model. Results from the GWT model are compared with the results +# from a MT3DMS simulation that uses flows from a MODFLOW-2005 simulation. + +# ### Initial setup # -# - -# ### Zero-Order Growth in a Uniform Flow Field Problem Setup -# -# Imports +# Import dependencies, define the example name and workspace, and read settings from environment variables. +# + import os -import sys +import pathlib as pl +from pprint import pformat import flopy import matplotlib.pyplot as plt -import numpy as np - -# Append to system path to include the common subdirectory - -sys.path.append(os.path.join("..", "common")) +from flopy.plot.styles import styles +from modflow_devtools.misc import get_env, timed -# Import common functionality - -import config -from figspecs import USGSFigure - -# Set figure properties specific to the - -figure_size = (5, 3) +# Example name and base workspace +workspace = pl.Path("../examples") +example_name = "ex-gwt-mt3dsupp631" -# Base simulation and model name and workspace +# Settings from environment variables +write = get_env("WRITE", True) +run = get_env("RUN", True) +plot = get_env("PLOT", True) +plot_show = get_env("PLOT_SHOW", True) +plot_save = get_env("PLOT_SAVE", True) +# - -ws = config.base_ws -example_name = "ex-gwt-mt3dsupp631" +# ### Define parameters +# +# Define model units, parameters and other settings. +# + # Model units - length_units = "meters" time_units = "days" -# Table of model parameters - +# Model parameters nper = 2 # Number of periods nlay = 1 # Number of layers nrow = 1 # Number of rows @@ -55,26 +59,24 @@ source_duration = 160.0 # Source duration ($d$) total_time = 840.0 # Simulation time ($t$) obs_xloc = 8.0 # Observation x location ($m$) +# - -# ### Functions to build, write, run, and plot models -# -# MODFLOW 6 flopy simulation object (sim) is returned if building the model -# recharge is the only variable +# ### Model setup # +# Define functions to build models, write input files, and run the simulation. +# + def build_mf6gwf(sim_folder): print(f"Building mf6gwf model...{sim_folder}") name = "flow" - sim_ws = os.path.join(ws, sim_folder, "mf6gwf") + sim_ws = os.path.join(workspace, sim_folder, "mf6gwf") sim = flopy.mf6.MFSimulation(sim_name=name, sim_ws=sim_ws, exe_name="mf6") tdis_ds = ( (source_duration, 1, 1.0), (total_time - source_duration, 1, 1.0), ) - flopy.mf6.ModflowTdis( - sim, nper=nper, perioddata=tdis_ds, time_units=time_units - ) + flopy.mf6.ModflowTdis(sim, nper=nper, perioddata=tdis_ds, time_units=time_units) flopy.mf6.ModflowIms(sim) gwf = flopy.mf6.ModflowGwf(sim, modelname=name, save_flows=True) flopy.mf6.ModflowGwfdis( @@ -121,14 +123,12 @@ def build_mf6gwf(sim_folder): def build_mf6gwt(sim_folder): print(f"Building mf6gwt model...{sim_folder}") name = "trans" - sim_ws = os.path.join(ws, sim_folder, "mf6gwt") + sim_ws = os.path.join(workspace, sim_folder, "mf6gwt") sim = flopy.mf6.MFSimulation(sim_name=name, sim_ws=sim_ws, exe_name="mf6") pertim1 = source_duration pertim2 = total_time - source_duration tdis_ds = ((pertim1, 16, 1.0), (pertim2, 84, 1.0)) - flopy.mf6.ModflowTdis( - sim, nper=nper, perioddata=tdis_ds, time_units=time_units - ) + flopy.mf6.ModflowTdis(sim, nper=nper, perioddata=tdis_ds, time_units=time_units) flopy.mf6.ModflowIms(sim, linear_acceleration="bicgstab") gwt = flopy.mf6.ModflowGwt(sim, modelname=name, save_flows=True) flopy.mf6.ModflowGwtdis( @@ -175,10 +175,8 @@ def build_mf6gwt(sim_folder): def build_mf2005(sim_folder): print(f"Building mf2005 model...{sim_folder}") name = "flow" - sim_ws = os.path.join(ws, sim_folder, "mf2005") - mf = flopy.modflow.Modflow( - modelname=name, model_ws=sim_ws, exe_name="mf2005" - ) + sim_ws = os.path.join(workspace, sim_folder, "mf2005") + mf = flopy.modflow.Modflow(modelname=name, model_ws=sim_ws, exe_name="mf2005") pertim1 = source_duration pertim2 = total_time - source_duration perlen = [pertim1, pertim2] @@ -198,9 +196,7 @@ def build_mf2005(sim_folder): lpf = flopy.modflow.ModflowLpf(mf) pcg = flopy.modflow.ModflowPcg(mf) lmt = flopy.modflow.ModflowLmt(mf) - chd = flopy.modflow.ModflowChd( - mf, stress_period_data=[[0, 0, ncol - 1, 1.0, 1.0]] - ) + chd = flopy.modflow.ModflowChd(mf, stress_period_data=[[0, 0, ncol - 1, 1.0, 1.0]]) wel_spd = { 0: [[0, 0, 0, specific_discharge * delc * top]], 1: [[0, 0, 0, specific_discharge * delc * top]], @@ -212,7 +208,7 @@ def build_mf2005(sim_folder): def build_mt3dms(sim_folder, modflowmodel): print(f"Building mt3dms model...{sim_folder}") name = "trans" - sim_ws = os.path.join(ws, sim_folder, "mt3d") + sim_ws = os.path.join(workspace, sim_folder, "mt3d") mt = flopy.mt3d.Mt3dms( modelname=name, model_ws=sim_ws, @@ -236,70 +232,53 @@ def build_mt3dms(sim_folder, modflowmodel): return mt -def build_model(sim_name): - sims = None - if config.buildModel: - sim_mf6gwf = build_mf6gwf(sim_name) - sim_mf6gwt = build_mf6gwt(sim_name) - sim_mf2005 = build_mf2005(sim_name) - sim_mt3dms = build_mt3dms(sim_name, sim_mf2005) - sims = (sim_mf6gwf, sim_mf6gwt, sim_mf2005, sim_mt3dms) - return sims - - -# Function to write model files - - -def write_model(sims, silent=True): - if config.writeModel: - sim_mf6gwf, sim_mf6gwt, sim_mf2005, sim_mt3dms = sims - sim_mf6gwf.write_simulation(silent=silent) - sim_mf6gwt.write_simulation(silent=silent) - sim_mf2005.write_input() - sim_mt3dms.write_input() - return - - -# Function to run the model -# True is returned if the model runs successfully - - -@config.timeit -def run_model(sims, silent=True): - success = True - if config.runModel: - success = False - sim_mf6gwf, sim_mf6gwt, sim_mf2005, sim_mt3dms = sims - success, buff = sim_mf6gwf.run_simulation(silent=silent) - if not success: - print(buff) - success, buff = sim_mf6gwt.run_simulation(silent=silent) - if not success: - print(buff) - success, buff = sim_mf2005.run_model(silent=silent) - if not success: - print(buff) - success, buff = sim_mt3dms.run_model( - silent=silent, normal_msg="Program completed" - ) - if not success: - print(buff) - return success +def build_models(sim_name): + sim_mf6gwf = build_mf6gwf(sim_name) + sim_mf6gwt = build_mf6gwt(sim_name) + sim_mf2005 = build_mf2005(sim_name) + sim_mt3dms = build_mt3dms(sim_name, sim_mf2005) + return sim_mf6gwf, sim_mf6gwt, sim_mf2005, sim_mt3dms + + +def write_models(sims, silent=True): + sim_mf6gwf, sim_mf6gwt, sim_mf2005, sim_mt3dms = sims + sim_mf6gwf.write_simulation(silent=silent) + sim_mf6gwt.write_simulation(silent=silent) + sim_mf2005.write_input() + sim_mt3dms.write_input() + + +@timed +def run_models(sims, silent=True): + sim_mf6gwf, sim_mf6gwt, sim_mf2005, sim_mt3dms = sims + success, buff = sim_mf6gwf.run_simulation(silent=silent, report=True) + assert success, pformat(buff) + success, buff = sim_mf6gwt.run_simulation(silent=silent, report=True) + assert success, pformat(buff) + success, buff = sim_mf2005.run_model(silent=silent, report=True) + assert success, pformat(buff) + success, buff = sim_mt3dms.run_model( + silent=silent, normal_msg="Program completed", report=True + ) + assert success, pformat(buff) -# Function to plot the model results +# - +# ### Plotting results +# +# Define functions to plot model results. + +# + +# Figure properties +figure_size = (5, 3) -def plot_results(sims, idx): - if config.plotModel: - print("Plotting model results...") - sim_mf6gwf, sim_mf6gwt, sim_mf2005, sim_mt3dms = sims - fs = USGSFigure(figure_type="graph", verbose=False) +def plot_results(sims, idx): + _, sim_mf6gwt, _, sim_mt3dms = sims + with styles.USGSPlot(): mf6gwt_ra = sim_mf6gwt.get_model("trans").obs.output.obs().data - fig, axs = plt.subplots( - 1, 1, figsize=figure_size, dpi=300, tight_layout=True - ) + fig, axs = plt.subplots(1, 1, figsize=figure_size, dpi=300, tight_layout=True) axs.plot( mf6gwt_ra["totim"], mf6gwt_ra["MYOBS"], @@ -326,41 +305,33 @@ def plot_results(sims, idx): axs.set_ylabel("Normalized Concentration (unitless)") axs.legend() - # save figure - if config.plotSave: + if plot_show: + plt.show() + if plot_save: sim_folder = os.path.split(sim_ws)[0] sim_folder = os.path.basename(sim_folder) - fname = f"{sim_folder}{config.figure_ext}" - fpth = os.path.join(ws, "..", "figures", fname) + fname = f"{sim_folder}.png" + fpth = os.path.join(workspace, "..", "figures", fname) fig.savefig(fpth) -# Function that wraps all of the steps for each scenario +# - + +# ### Running the example # -# 1. build_model, -# 2. write_model, -# 3. run_model, and -# 4. plot_results. +# Define and invoke a function to run the example scenario, then plot results. +# + def scenario(idx, silent=True): - sim = build_model(example_name) - write_model(sim, silent=silent) - success = run_model(sim, silent=silent) - if success: + sim = build_models(example_name) + if write: + write_models(sim, silent=silent) + if run: + run_models(sim, silent=silent) + if plot: plot_results(sim, idx) -# nosetest - exclude block from this nosetest to the next nosetest -def test_01(): - scenario(0, silent=False) - - -# nosetest end - -if __name__ == "__main__": - # ### Simulated Zero-Order Growth in a Uniform Flow Field - - # Add a description of the plot(s) - - scenario(0) +scenario(0) +# - diff --git a/scripts/ex-gwt-mt3dsupp632.py b/scripts/ex-gwt-mt3dsupp632.py index eb56c09d5..193d02431 100644 --- a/scripts/ex-gwt-mt3dsupp632.py +++ b/scripts/ex-gwt-mt3dsupp632.py @@ -1,40 +1,44 @@ -# ## Zero-Order Production in a Dual-Domain System +# ## Zero-order production in a dual-domain system # -# MT3DMS Supplemental Guide Problem 6.3.2 +# This example tests the capabilities of the GWT model to simulate +# 0-order production in a dual-domain system with & without sorption. +# Results from a GWT model are compared with results from an MT3DMS +# simulation that uses flows from a separate MODFLOW-2005 simulation. +# It is based on example problem 6.3.2 described in Zheng 2010. The +# problem consists of a one-dimensional model grid with inflow into +# the first cell and outflow through the last cell. + +# ### Initial setup # -# -# - -# ### Zero-Order Production in a Dual-Domain System Problem Setup - -# Imports +# Import dependencies, define the example name and workspace, and read settings from environment variables. +# + import os -import sys +import pathlib as pl +from pprint import pformat import flopy import matplotlib.pyplot as plt -import numpy as np - -# Append to system path to include the common subdirectory - -sys.path.append(os.path.join("..", "common")) +from flopy.plot.styles import styles +from modflow_devtools.misc import get_env, timed -# Import common functionality +# Base workspace +workspace = pl.Path("../examples") -import config -from figspecs import USGSFigure +# Settings from environment variables +write = get_env("WRITE", True) +run = get_env("RUN", True) +plot = get_env("PLOT", True) +plot_show = get_env("PLOT_SHOW", True) +plot_save = get_env("PLOT_SAVE", True) +# - -# Set figure properties specific to the - -figure_size = (3, 3) - -# Base simulation and model name and workspace - -ws = config.base_ws - -# Scenario parameters - make sure there is at least one blank line before next item +# ### Define parameters +# +# Define model units, parameters and other settings. +# + +# Scenario-specific parameters - make sure there is at least one blank line before next item parameters = { "ex-gwt-mt3dsupp632a": { "distribution_coefficient": 0.25, @@ -63,12 +67,10 @@ } # Model units - length_units = "meters" time_units = "days" -# Table of model parameters - +# Model parameters nper = 2 # Number of periods nlay = 1 # Number of layers nrow = 1 # Number of rows @@ -93,25 +95,24 @@ zero_order_decay = True # Flag indicating whether decay is zero or first order dual_domain = True # Flag indicating that dual domain is active +# - -# ### Functions to build, write, run and plot models -# -# MODFLOW 6 flopy simulation object (sim) is returned if building the model +# ### Model setup # +# Define functions to build models, write input files, and run the simulation. +# + def build_mf6gwf(sim_folder): print(f"Building mf6gwf model...{sim_folder}") name = "flow" - sim_ws = os.path.join(ws, sim_folder, "mf6gwf") + sim_ws = os.path.join(workspace, sim_folder, "mf6gwf") sim = flopy.mf6.MFSimulation(sim_name=name, sim_ws=sim_ws, exe_name="mf6") tdis_ds = ( (source_duration, 1, 1.0), (total_time - source_duration, 1, 1.0), ) - flopy.mf6.ModflowTdis( - sim, nper=nper, perioddata=tdis_ds, time_units=time_units - ) + flopy.mf6.ModflowTdis(sim, nper=nper, perioddata=tdis_ds, time_units=time_units) flopy.mf6.ModflowIms(sim) gwf = flopy.mf6.ModflowGwf(sim, modelname=name, save_flows=True) flopy.mf6.ModflowGwfdis( @@ -153,14 +154,12 @@ def build_mf6gwf(sim_folder): def build_mf6gwt(sim_folder, distribution_coefficient, decay, decay_sorbed): print(f"Building mf6gwt model...{sim_folder}") name = "trans" - sim_ws = os.path.join(ws, sim_folder, "mf6gwt") + sim_ws = os.path.join(workspace, sim_folder, "mf6gwt") sim = flopy.mf6.MFSimulation(sim_name=name, sim_ws=sim_ws, exe_name="mf6") pertim1 = source_duration pertim2 = total_time - source_duration tdis_ds = ((pertim1, 10, 1.0), (pertim2, 90, 1.0)) - flopy.mf6.ModflowTdis( - sim, nper=nper, perioddata=tdis_ds, time_units=time_units - ) + flopy.mf6.ModflowTdis(sim, nper=nper, perioddata=tdis_ds, time_units=time_units) flopy.mf6.ModflowIms(sim, linear_acceleration="bicgstab") gwt = flopy.mf6.ModflowGwt(sim, modelname=name, save_flows=True) flopy.mf6.ModflowGwtdis( @@ -247,10 +246,8 @@ def build_mf6gwt(sim_folder, distribution_coefficient, decay, decay_sorbed): def build_mf2005(sim_folder): print(f"Building mf2005 model...{sim_folder}") name = "flow" - sim_ws = os.path.join(ws, sim_folder, "mf2005") - mf = flopy.modflow.Modflow( - modelname=name, model_ws=sim_ws, exe_name="mf2005" - ) + sim_ws = os.path.join(workspace, sim_folder, "mf2005") + mf = flopy.modflow.Modflow(modelname=name, model_ws=sim_ws, exe_name="mf2005") pertim1 = source_duration pertim2 = total_time - source_duration perlen = [pertim1, pertim2] @@ -270,9 +267,7 @@ def build_mf2005(sim_folder): lpf = flopy.modflow.ModflowLpf(mf) pcg = flopy.modflow.ModflowPcg(mf) lmt = flopy.modflow.ModflowLmt(mf) - chd = flopy.modflow.ModflowChd( - mf, stress_period_data=[[0, 0, ncol - 1, 1.0, 1.0]] - ) + chd = flopy.modflow.ModflowChd(mf, stress_period_data=[[0, 0, ncol - 1, 1.0, 1.0]]) wel_spd = { 0: [[0, 0, 0, specific_discharge * delc * top]], 1: [[0, 0, 0, specific_discharge * delc * top]], @@ -286,7 +281,7 @@ def build_mt3dms( ): print(f"Building mt3dms model...{sim_folder}") name = "trans" - sim_ws = os.path.join(ws, sim_folder, "mt3d") + sim_ws = os.path.join(workspace, sim_folder, "mt3d") mt = flopy.mt3d.Mt3dms( modelname=name, model_ws=sim_ws, @@ -342,76 +337,57 @@ def build_mt3dms( return mt -def build_model(sim_name, distribution_coefficient, decay, decay_sorbed): - sims = None - if config.buildModel: - sim_mf6gwf = build_mf6gwf(sim_name) - sim_mf6gwt = build_mf6gwt( - sim_name, distribution_coefficient, decay, decay_sorbed - ) - sim_mf2005 = build_mf2005(sim_name) - sim_mt3dms = build_mt3dms( - sim_name, distribution_coefficient, decay, decay_sorbed, sim_mf2005 - ) - sims = (sim_mf6gwf, sim_mf6gwt, sim_mf2005, sim_mt3dms) - return sims - - -# Function to write model files - - -def write_model(sims, silent=True): - if config.writeModel: - sim_mf6gwf, sim_mf6gwt, sim_mf2005, sim_mt3dms = sims - sim_mf6gwf.write_simulation(silent=silent) - sim_mf6gwt.write_simulation(silent=silent) - sim_mf2005.write_input() - sim_mt3dms.write_input() - return - - -# Function to run the model -# True is returned if the model runs successfully - - -@config.timeit -def run_model(sims, silent=True): - success = True - if config.runModel: - success = False - sim_mf6gwf, sim_mf6gwt, sim_mf2005, sim_mt3dms = sims - success, buff = sim_mf6gwf.run_simulation(silent=silent) - if not success: - print(buff) - success, buff = sim_mf6gwt.run_simulation(silent=silent) - if not success: - print(buff) - success, buff = sim_mf2005.run_model(silent=silent) - if not success: - print(buff) - success, buff = sim_mt3dms.run_model( - silent=silent, normal_msg="Program completed" - ) - if not success: - print(buff) - return success +def build_models(sim_name, distribution_coefficient, decay, decay_sorbed): + sim_mf6gwf = build_mf6gwf(sim_name) + sim_mf6gwt = build_mf6gwt(sim_name, distribution_coefficient, decay, decay_sorbed) + sim_mf2005 = build_mf2005(sim_name) + sim_mt3dms = build_mt3dms( + sim_name, distribution_coefficient, decay, decay_sorbed, sim_mf2005 + ) + return sim_mf6gwf, sim_mf6gwt, sim_mf2005, sim_mt3dms + + +def write_models(sims, silent=True): + sim_mf6gwf, sim_mf6gwt, sim_mf2005, sim_mt3dms = sims + sim_mf6gwf.write_simulation(silent=silent) + sim_mf6gwt.write_simulation(silent=silent) + sim_mf2005.write_input() + sim_mt3dms.write_input() + + +@timed +def run_models(sims, silent=True): + sim_mf6gwf, sim_mf6gwt, sim_mf2005, sim_mt3dms = sims + success, buff = sim_mf6gwf.run_simulation(silent=silent, report=True) + assert success, pformat(buff) + success, buff = sim_mf6gwt.run_simulation(silent=silent, report=True) + assert success, pformat(buff) + success, buff = sim_mf2005.run_model(silent=silent, report=True) + assert success, pformat(buff) + success, buff = sim_mt3dms.run_model( + silent=silent, normal_msg="Program completed", report=True + ) + assert success, pformat(buff) -# Functions to plot the model results +# - +# ### Plotting results +# +# Define functions to plot model results. -def plot_results(): - if config.plotModel: - print("Plotting model results...") +# + +# Figure properties +figure_size = (3, 3) - fs = USGSFigure(figure_type="graph", verbose=False) - fig, axs = plt.subplots( - 1, 1, figsize=figure_size, dpi=300, tight_layout=True - ) + +def plot_results(): + with styles.USGSPlot(): + fig, axs = plt.subplots(1, 1, figsize=figure_size, dpi=300, tight_layout=True) case_colors = ["blue", "green", "red"] for icase, sim_name in enumerate(parameters.keys()): - sim_ws = os.path.join(ws, sim_name) + sim_ws = os.path.join(workspace, sim_name) fname = os.path.join(sim_ws, "mf6gwt", "trans.obs.csv") mf6gwt_ra = flopy.utils.Mf6Obs(fname).data @@ -439,24 +415,19 @@ def plot_results(): axs.set_ylabel("Normalized Concentration (unitless)") axs.legend() - # save figure - if config.plotSave: - fname = "{}{}".format("ex-gwt-mt3dsupp632", config.figure_ext) + if plot_show: + plt.show() + if plot_save: + fname = "{}{}".format("ex-gwt-mt3dsupp632", ".png") fpth = os.path.join("..", "figures", fname) fig.savefig(fpth) - return def plot_scenario_results(sims, idx): - if config.plotModel: - print("Plotting model results...") - sim_mf6gwf, sim_mf6gwt, sim_mf2005, sim_mt3dms = sims - fs = USGSFigure(figure_type="graph", verbose=False) - + _, sim_mf6gwt, _, sim_mt3dms = sims + with styles.USGSPlot(): mf6gwt_ra = sim_mf6gwt.get_model("trans").obs.output.obs().data - fig, axs = plt.subplots( - 1, 1, figsize=figure_size, dpi=300, tight_layout=True - ) + fig, axs = plt.subplots(1, 1, figsize=figure_size, dpi=300, tight_layout=True) axs.plot( mf6gwt_ra["totim"], mf6gwt_ra["MYOBS"], @@ -480,85 +451,66 @@ def plot_scenario_results(sims, idx): axs.legend() title = f"Case {idx + 1} " letter = chr(ord("@") + idx + 1) - fs.heading(letter=letter, heading=title) + styles.heading(letter=letter, heading=title) - # save figure - if config.plotSave: + if plot_show: + plt.show() + if plot_save: sim_folder = os.path.split(sim_ws)[0] sim_folder = os.path.basename(sim_folder) - fname = f"{sim_folder}{config.figure_ext}" - fpth = os.path.join(ws, "..", "figures", fname) + fname = f"{sim_folder}.png" + fpth = os.path.join(workspace, "..", "figures", fname) fig.savefig(fpth) -# Function that wraps all of the steps for each scenario +# - + +# ### Running the example # -# 1. build_model, -# 2. write_model, -# 3. run_model, and -# 4. plot_results. +# Define and invoke a function to run the example scenario, then plot results. +# + def scenario(idx, silent=True): key = list(parameters.keys())[idx] parameter_dict = parameters[key] - sims = build_model(key, **parameter_dict) - write_model(sims, silent=silent) - success = run_model(sims, silent=silent) - if success: + sims = build_models(key, **parameter_dict) + if write: + write_models(sims, silent=silent) + if run: + run_models(sims, silent=silent) + if plot: plot_scenario_results(sims, idx) - return - - -# nosetest - exclude block from this nosetest to the next nosetest -def test_01(): - scenario(0, silent=False) -def test_02(): - scenario(1, silent=False) - - -def test_03(): - scenario(2, silent=False) - - -def test_plot_results(): - plot_results() - - -# nosetest end - -if __name__ == "__main__": - # ### Case 1 - # - # ex-gwt-mt3dsupp632a - # * distribution_coefficient = 0.25 - # * decay = 0.0 - # * decay_sorbed = -1.0e-3 +# ### Case 1 +# +# ex-gwt-mt3dsupp632a +# * distribution_coefficient = 0.25 +# * decay = 0.0 +# * decay_sorbed = -1.0e-3 - scenario(0) +scenario(0) - # ### Case 2 - # - # ex-gwt-mt3dsupp632a - # * distribution_coefficient = 0.25 - # * decay = -5.e-4 - # * decay_sorbed = -5.e-4 +# ### Case 2 +# +# ex-gwt-mt3dsupp632a +# * distribution_coefficient = 0.25 +# * decay = -5.e-4 +# * decay_sorbed = -5.e-4 - scenario(1) +scenario(1) - # ### Case 3 - # - # ex-gwt-mt3dsupp632a - # * distribution_coefficient = 0. - # * decay = -1.0e-3 - # * decay_sorbed = 0. +# ### Case 3 +# +# ex-gwt-mt3dsupp632a +# * distribution_coefficient = 0. +# * decay = -1.0e-3 +# * decay_sorbed = 0. - scenario(2) +scenario(2) - # ### Plot the Zero-Order Production in a Dual-Domain System Problem results - # - # Plot the results for all 3 scenarios in one plot +# Plot the results for all 3 scenarios in one plot. +if plot: plot_results() diff --git a/scripts/ex-gwt-mt3dsupp82.py b/scripts/ex-gwt-mt3dsupp82.py index 7ba53f845..3570ce8c6 100644 --- a/scripts/ex-gwt-mt3dsupp82.py +++ b/scripts/ex-gwt-mt3dsupp82.py @@ -1,45 +1,52 @@ -# ## Simulating Effect of Recirculation Well +# ## Simulating effect of recirculation well # -# MT3DMS Supplemental Guide Problem 8.2 +# This example is for a recirculating well. It is based on example problem 8.2 +# described in Zheng 2010. The problem consists of a two-dimensional, one-layer +# model with flow from left to right. A solute is introduced into the flow field +# by an injection well. Downgradient, an extraction well pumps at the same rate +# as the injection well. This extracted water is then injected into two other +# injection wells. This example is simulated with a GWT model, which receives +# flow information from a separate GWF model. Results from the GWT Model are +# compared with the results from an MT3DMS simulation that uses flows from a +# separate MODFLOW-2005 simulation. + +# ### Initial setup # -# - -# ### Simulating Effect of Recirculation Well Problem Setup -# -# Imports +# Import dependencies, define the example name and workspace, and read settings from environment variables. +# + import os -import sys +import pathlib as pl +from pprint import pformat import flopy import matplotlib.pyplot as plt import numpy as np +from flopy.plot.styles import styles +from modflow_devtools.misc import get_env, timed -# Append to system path to include the common subdirectory - -sys.path.append(os.path.join("..", "common")) - -# Import common functionality - -import config -from figspecs import USGSFigure - -# Set figure properties specific to the - -figure_size = (5, 3) +# Example name and base workspace +workspace = pl.Path("../examples") +example_name = "ex-gwt-mt3dsupp82" -# Base simulation and model name and workspace +# Settings from environment variables +write = get_env("WRITE", True) +run = get_env("RUN", True) +plot = get_env("PLOT", True) +plot_show = get_env("PLOT_SHOW", True) +plot_save = get_env("PLOT_SAVE", True) +# - -ws = config.base_ws -example_name = "ex-gwt-mt3dsupp82" +# ### Define parameters +# +# Define model units, parameters and other settings. +# + # Model units - length_units = "meters" time_units = "days" -# Table of model parameters - +# Model parameters nper = 1 # Number of periods nlay = 1 # Number of layers nrow = 31 # Number of rows @@ -54,24 +61,21 @@ alpha_tv = 0.3 # Transverse vertical dispersivity ($m$) total_time = 365.0 # Simulation time ($d$) porosity = 0.3 # Porosity of mobile domain (unitless) +# - - -# ### Functions to build, write, run, and plot models -# -# MODFLOW 6 flopy simulation object (sim) is returned if building the model -# recharge is the only variable +# ### Model setup # +# Define functions to build models, write input files, and run the simulation. +# + def build_mf6gwf(sim_folder): print(f"Building mf6gwf model...{sim_folder}") name = "flow" - sim_ws = os.path.join(ws, sim_folder, "mf6gwf") + sim_ws = os.path.join(workspace, sim_folder, "mf6gwf") sim = flopy.mf6.MFSimulation(sim_name=name, sim_ws=sim_ws, exe_name="mf6") tdis_ds = ((total_time, 1, 1.0),) - flopy.mf6.ModflowTdis( - sim, nper=nper, perioddata=tdis_ds, time_units=time_units - ) + flopy.mf6.ModflowTdis(sim, nper=nper, perioddata=tdis_ds, time_units=time_units) flopy.mf6.ModflowIms(sim) gwf = flopy.mf6.ModflowGwf(sim, modelname=name, save_flows=True) flopy.mf6.ModflowGwfdis( @@ -172,12 +176,10 @@ def build_mf6gwf(sim_folder): def build_mf6gwt(sim_folder): print(f"Building mf6gwt model...{sim_folder}") name = "trans" - sim_ws = os.path.join(ws, sim_folder, "mf6gwt") + sim_ws = os.path.join(workspace, sim_folder, "mf6gwt") sim = flopy.mf6.MFSimulation(sim_name=name, sim_ws=sim_ws, exe_name="mf6") tdis_ds = ((total_time, 20, 1.0),) - flopy.mf6.ModflowTdis( - sim, nper=nper, perioddata=tdis_ds, time_units=time_units - ) + flopy.mf6.ModflowTdis(sim, nper=nper, perioddata=tdis_ds, time_units=time_units) flopy.mf6.ModflowIms(sim, linear_acceleration="bicgstab") gwt = flopy.mf6.ModflowGwt(sim, modelname=name, save_flows=True) flopy.mf6.ModflowGwtdis( @@ -260,10 +262,8 @@ def build_mf6gwt(sim_folder): def build_mf2005(sim_folder): print(f"Building mf2005 model...{sim_folder}") name = "flow" - sim_ws = os.path.join(ws, sim_folder, "mf2005") - mf = flopy.modflow.Modflow( - modelname=name, model_ws=sim_ws, exe_name="mf2005" - ) + sim_ws = os.path.join(workspace, sim_folder, "mf2005") + mf = flopy.modflow.Modflow(modelname=name, model_ws=sim_ws, exe_name="mf2005") perlen = [total_time] dis = flopy.modflow.ModflowDis( mf, @@ -302,7 +302,7 @@ def build_mf2005(sim_folder): def build_mt3dms(sim_folder, modflowmodel): print(f"Building mt3dms model...{sim_folder}") name = "trans" - sim_ws = os.path.join(ws, sim_folder, "mt3d") + sim_ws = os.path.join(workspace, sim_folder, "mt3d") mt = flopy.mt3d.Mt3dms( modelname=name, model_ws=sim_ws, @@ -327,68 +327,55 @@ def build_mt3dms(sim_folder, modflowmodel): return mt -def build_model(sim_name): - sims = None - if config.buildModel: - sim_mf6gwf = build_mf6gwf(sim_name) - sim_mf6gwt = build_mf6gwt(sim_name) - sim_mf2005 = build_mf2005(sim_name) - sim_mt3dms = build_mt3dms(sim_name, sim_mf2005) - sims = (sim_mf6gwf, sim_mf6gwt, sim_mf2005, sim_mt3dms) - return sims - - -# Function to write model files - - -def write_model(sims, silent=True): - if config.writeModel: - sim_mf6gwf, sim_mf6gwt, sim_mf2005, sim_mt3dms = sims - sim_mf6gwf.write_simulation(silent=silent) - sim_mf6gwt.write_simulation(silent=silent) - sim_mf2005.write_input() - sim_mt3dms.write_input() - return - - -# Function to run the model -# True is returned if the model runs successfully +def build_models(sim_name): + sim_mf6gwf = build_mf6gwf(sim_name) + sim_mf6gwt = build_mf6gwt(sim_name) + sim_mf2005 = build_mf2005(sim_name) + sim_mt3dms = build_mt3dms(sim_name, sim_mf2005) + return sim_mf6gwf, sim_mf6gwt, sim_mf2005, sim_mt3dms + + +def write_models(sims, silent=True): + sim_mf6gwf, sim_mf6gwt, sim_mf2005, sim_mt3dms = sims + sim_mf6gwf.write_simulation(silent=silent) + sim_mf6gwt.write_simulation(silent=silent) + sim_mf2005.write_input() + sim_mt3dms.write_input() + + +@timed +def run_models(sims, silent=True): + sim_mf6gwf, sim_mf6gwt, sim_mf2005, sim_mt3dms = sims + success, buff = sim_mf6gwf.run_simulation(silent=silent, report=True) + assert success, pformat(buff) + success, buff = sim_mf6gwt.run_simulation(silent=silent, report=True) + assert success, pformat(buff) + success, buff = sim_mf2005.run_model(silent=silent, report=True) + assert success, pformat(buff) + success, buff = sim_mt3dms.run_model( + silent=silent, normal_msg="Program completed", report=True + ) + assert success, pformat(buff) -@config.timeit -def run_model(sims, silent=True): - success = True - if config.runModel: - success = False - sim_mf6gwf, sim_mf6gwt, sim_mf2005, sim_mt3dms = sims - success, buff = sim_mf6gwf.run_simulation(silent=silent) - if not success: - print(buff) - success, buff = sim_mf6gwt.run_simulation(silent=silent) - if not success: - print(buff) - success, buff = sim_mf2005.run_model(silent=silent) - if not success: - print(buff) - success, buff = sim_mt3dms.run_model( - silent=silent, normal_msg="Program completed" - ) - if not success: - print(buff) - return success +# - +# ### Plotting results +# +# Define functions to plot model results. -# Function to plot the model results +# + +# Figure properties +figure_size = (5, 3) def plot_results(sims, idx): - if config.plotModel: - print("Plotting model results...") - sim_mf6gwf, sim_mf6gwt, sim_mf2005, sim_mt3dms = sims - gwf = sim_mf6gwf.flow - gwt = sim_mf6gwt.trans - fs = USGSFigure(figure_type="map", verbose=False) + print("Plotting model results...") + sim_mf6gwf, sim_mf6gwt, _, sim_mt3dms = sims + gwf = sim_mf6gwf.flow + gwt = sim_mf6gwt.trans + with styles.USGSMap() as fs: conc = gwt.output.concentration().get_data() sim_ws = sim_mt3dms.model_ws @@ -396,9 +383,7 @@ def plot_results(sims, idx): cobjmt = flopy.utils.UcnFile(fname) concmt = cobjmt.get_data() - fig, ax = plt.subplots( - 1, 1, figsize=figure_size, dpi=300, tight_layout=True - ) + fig, ax = plt.subplots(1, 1, figsize=figure_size, dpi=300, tight_layout=True) pmv = flopy.plot.PlotMapView(model=gwf, ax=ax) pmv.plot_bc(ftype="MAW", color="red") pmv.plot_bc(ftype="CHD") @@ -411,9 +396,7 @@ def plot_results(sims, idx): levels = [0.01, 0.1, 1, 10, 100] cs1 = pmv.contour_array(concmt, levels=levels, colors="r") - cs2 = pmv.contour_array( - conc, levels=levels, colors="b", linestyles="--" - ) + cs2 = pmv.contour_array(conc, levels=levels, colors="b", linestyles="--") ax.clabel(cs2, cs2.levels[::1], fmt="%3.2f", colors="b") labels = ["MT3DMS", "MODFLOW 6"] @@ -423,41 +406,33 @@ def plot_results(sims, idx): ax.set_xlabel("x position (m)") ax.set_ylabel("y position (m)") - # save figure - if config.plotSave: + if plot_show: + plt.show() + if plot_save: sim_folder = os.path.split(sim_ws)[0] sim_folder = os.path.basename(sim_folder) - fname = f"{sim_folder}-map{config.figure_ext}" - fpth = os.path.join(ws, "..", "figures", fname) + fname = f"{sim_folder}-map.png" + fpth = os.path.join(workspace, "..", "figures", fname) fig.savefig(fpth) -# Function that wraps all of the steps for each scenario +# - + +# ### Running the example # -# 1. build_model, -# 2. write_model, -# 3. run_model, and -# 4. plot_results. +# Define and invoke a function to run the example scenario, then plot results. +# + def scenario(idx, silent=True): - sim = build_model(example_name) - write_model(sim, silent=silent) - success = run_model(sim, silent=silent) - if success: + sim = build_models(example_name) + if write: + write_models(sim, silent=silent) + if run: + run_models(sim, silent=silent) + if plot: plot_results(sim, idx) -# nosetest - exclude block from this nosetest to the next nosetest -def test_01(): - scenario(0, silent=False) - - -# nosetest end - -if __name__ == "__main__": - # ### Simulated Zero-Order Growth in a Uniform Flow Field - - # Add a description of the plot(s) - - scenario(0) +scenario(0) +# - diff --git a/scripts/ex-gwt-prudic2004t2.py b/scripts/ex-gwt-prudic2004t2.py index 1eb435f1a..dac3fc355 100644 --- a/scripts/ex-gwt-prudic2004t2.py +++ b/scripts/ex-gwt-prudic2004t2.py @@ -1,51 +1,52 @@ -# ## Stream-Lake Interaction with Solute Transport +# ## Stream-lake interaction with solute transport # -# SFR1 Package Documentation Test Problem 2 +# This problem is based on the stream-aquifer interaction problem +# described as Test 2 by Prudic et al 2004, which modifies another +# problem originally described by Merritt et al 2000. The purpose +# for including this problem is to demonstrate the use of MODFLOW 6 +# to simulate solute transport through a coupled system consisting +# of an aquifer, streams, and lakes. The example requires accurate +# simulation of transport within the streams and lakes and also +# between the surface water features and the underlying aquifer. + +# ### Initial setup # -# - - -# ### Stream-Lake Interaction with Solute Transport Problem Setup - -# Imports +# Import dependencies, define the example name and workspace, and read settings from environment variables. +# + import os -import sys +import pathlib as pl +from pprint import pformat import flopy import matplotlib.pyplot as plt import numpy as np +import pooch +from flopy.plot.styles import styles +from modflow_devtools.misc import get_env, timed -# Append to system path to include the common subdirectory - -sys.path.append(os.path.join("..", "common")) - -# Import common functionality - -import config -from figspecs import USGSFigure - -mf6exe = "mf6" -exe_name_mf = "mf2005" -exe_name_mt = "mt3dms" - -# Set figure properties specific to this problem - -figure_size = (6, 6) +# Example name and base workspace +example_name = "ex-gwt-prudic2004t2" +workspace = pl.Path("../examples") -# Base simulation and model name and workspace +# Settings from environment variables +write = get_env("WRITE", True) +run = get_env("RUN", True) +plot = get_env("PLOT", True) +plot_show = get_env("PLOT_SHOW", True) +plot_save = get_env("PLOT_SAVE", True) +# - -ws = config.base_ws -example_name = "ex-gwt-prudic2004t2" -data_ws = os.path.join(config.data_ws, example_name) +# ### Define parameters +# +# Define model units, parameters and other settings. +# + # Model units - length_units = "feet" time_units = "days" -# Table of model parameters - +# Model parameters hk = 250.0 # Horizontal hydraulic conductivity ($ft d^{-1}$) vk = 125.0 # Vertical hydraulic conductivity ($ft d^{-1}$) ss = 0.0 # Storage coefficient (unitless) @@ -73,28 +74,36 @@ total_time = 9131.0 # Total simulation time ($d$) # Load Data Arrays - -fname = os.path.join(data_ws, "bot1.dat") +fname = pooch.retrieve( + url=f"https://github.com/MODFLOW-USGS/modflow6-examples/raw/master/data/{example_name}/bot1.dat", + known_hash="md5:c510defe0eb1ba1fbfab5663ff63cd83", +) bot0 = np.loadtxt(fname) botm = [bot0] + [bot0 - (15.0 * k) for k in range(1, nlay)] -fname = os.path.join(data_ws, "idomain1.dat") +fname = pooch.retrieve( + url=f"https://github.com/MODFLOW-USGS/modflow6-examples/raw/master/data/{example_name}/idomain1.dat", + known_hash="md5:45d1ca08015e4a34125ccd95a83da0ee", +) idomain0 = np.loadtxt(fname, dtype=int) idomain = nlay * [idomain0] -fname = os.path.join(data_ws, "lakibd.dat") +fname = pooch.retrieve( + url=f"https://github.com/MODFLOW-USGS/modflow6-examples/raw/master/data/{example_name}/lakibd.dat", + known_hash="md5:18c90af94c34825a206935b7ddace2f9", +) lakibd = np.loadtxt(fname, dtype=int) +# - - -# Other model information - - -# ### Functions to build, write, run, and plot models -# -# MODFLOW 6 flopy GWF simulation object (sim) is returned +# ### Model setup # +# Define functions to build models, write input files, and run the simulation. +# + def get_stream_data(): - fname = os.path.join(data_ws, "stream.csv") + fname = pooch.retrieve( + url=f"https://github.com/MODFLOW-USGS/modflow6-examples/raw/master/data/{example_name}/stream.csv", + known_hash="md5:1291c8dec5a415866c711ee14bf0b1f8", + ) dt = 5 * [int] + [float] streamdata = np.genfromtxt(fname, names=True, delimiter=",", dtype=dt) connectiondata = [[ireach] for ireach in range(streamdata.shape[0])] @@ -136,11 +145,7 @@ def get_stream_data(): iseg = row["seg"] - 1 rgrd = segment_gradients[iseg] emax, emin = emaxmin[iseg] - rtp = ( - distance_along_segment[ireach] - / segment_lengths[iseg] - * (emax - emin) - ) + rtp = distance_along_segment[ireach] / segment_lengths[iseg] * (emax - emin) rtp = emax - rtp boundname = f"SEG{iseg + 1}" rec = ( @@ -167,7 +172,7 @@ def build_mf6gwf(sim_folder): global idomain print(f"Building mf6gwf model...{sim_folder}") name = "flow" - sim_ws = os.path.join(ws, sim_folder, "mf6gwf") + sim_ws = os.path.join(workspace, sim_folder, "mf6gwf") sim = flopy.mf6.MFSimulation(sim_name=name, sim_ws=sim_ws, exe_name="mf6") tdis_data = [(total_time, 1, 1.0)] flopy.mf6.ModflowTdis( @@ -215,7 +220,10 @@ def build_mf6gwf(sim_folder): flopy.mf6.ModflowGwfrcha(gwf, recharge={0: recharge}, pname="RCH-1") chdlist = [] - fname = os.path.join(data_ws, "chd.dat") + fname = pooch.retrieve( + url=f"https://github.com/MODFLOW-USGS/modflow6-examples/raw/master/data/{example_name}/chd.dat", + known_hash="md5:7889521ec9ec9521377d604d9f6d1f74", + ) for line in open(fname).readlines(): ll = line.strip().split() if len(ll) == 4: @@ -252,9 +260,7 @@ def build_mf6gwf(sim_folder): [1, 35.2, lakepakdata_dict[1], "lake2"], ] # - outlets = [ - [0, 0, -1, "MANNING", 44.5, 3.36493214532915, 0.03, 0.2187500e-02] - ] + outlets = [[0, 0, -1, "MANNING", 44.5, 3.36493214532915, 0.03, 0.2187500e-02]] flopy.mf6.ModflowGwflak( gwf, time_conversion=86400.000, @@ -322,13 +328,10 @@ def build_mf6gwf(sim_folder): return sim -# MODFLOW 6 flopy GWF simulation object (sim) is returned - - def build_mf6gwt(sim_folder): print(f"Building mf6gwt model...{sim_folder}") name = "trans" - sim_ws = os.path.join(ws, sim_folder, "mf6gwt") + sim_ws = os.path.join(workspace, sim_folder, "mf6gwt") sim = flopy.mf6.MFSimulation(sim_name=name, sim_ws=sim_ws, exe_name="mf6") tdis_data = ((total_time, 300, 1.0),) flopy.mf6.ModflowTdis( @@ -482,46 +485,37 @@ def build_mf6gwt(sim_folder): return sim -def build_model(sim_name): +def build_models(sim_name): sims = None - if config.buildModel: - sim_mf6gwf = build_mf6gwf(sim_name) - sim_mf6gwt = build_mf6gwt(sim_name) - sims = (sim_mf6gwf, sim_mf6gwt) - return sims - + sim_mf6gwf = build_mf6gwf(sim_name) + sim_mf6gwt = build_mf6gwt(sim_name) + return sim_mf6gwf, sim_mf6gwt -# Function to write model files +def write_models(sims, silent=True): + sim_mf6gwf, sim_mf6gwt = sims + sim_mf6gwf.write_simulation(silent=silent) + sim_mf6gwt.write_simulation(silent=silent) -def write_model(sims, silent=True): - if config.writeModel: - sim_mf6gwf, sim_mf6gwt = sims - sim_mf6gwf.write_simulation(silent=silent) - sim_mf6gwt.write_simulation(silent=silent) - return +@timed +def run_models(sims, silent=True): + sim_mf6gwf, sim_mf6gwt = sims + success, buff = sim_mf6gwf.run_simulation(silent=silent, report=True) + assert success, pformat(buff) + success, buff = sim_mf6gwt.run_simulation(silent=silent, report=True) + assert success, pformat(buff) -# Function to run the model -# True is returned if the model runs successfully +# - -@config.timeit -def run_model(sims, silent=True): - success = True - if config.runModel: - success = False - sim_mf6gwf, sim_mf6gwt = sims - success, buff = sim_mf6gwf.run_simulation(silent=silent) - if not success: - print(buff) - success, buff = sim_mf6gwt.run_simulation(silent=silent) - if not success: - print(buff) - return success - +# ### Plotting results +# +# Define functions to plot model results. -# Function to plot the model results +# + +# Figure properties +figure_size = (6, 6) def plot_bcmap(ax, gwf, layer=0): @@ -541,16 +535,13 @@ def plot_bcmap(ax, gwf, layer=0): def plot_results(sims): plot_gwf_results(sims) plot_gwt_results(sims) - return def plot_gwf_results(sims): - if config.plotModel: - print("Plotting model results...") - sim_mf6gwf, sim_mf6gwt = sims - gwf = sim_mf6gwf.flow - fs = USGSFigure(figure_type="map", verbose=False) - + print("Plotting model results...") + sim_mf6gwf, _ = sims + gwf = sim_mf6gwf.flow + with styles.USGSMap(): sim_ws = sim_mf6gwf.simulation_data.mfpath.get_sim_path() head = gwf.output.head().get_data() @@ -562,9 +553,7 @@ def plot_gwf_results(sims): lake_stage = stage[ilak] head[0, i, j] = lake_stage - fig, axs = plt.subplots( - 1, 2, figsize=figure_size, dpi=300, tight_layout=True - ) + fig, axs = plt.subplots(1, 2, figsize=figure_size, dpi=300, tight_layout=True) for ilay in [0, 1]: ax = axs[ilay] @@ -580,25 +569,25 @@ def plot_gwf_results(sims): ax.clabel(cs, cs.levels[::5], fmt="%1.0f", colors="b") title = f"Model Layer {ilay + 1}" letter = chr(ord("@") + ilay + 1) - fs.heading(letter=letter, heading=title, ax=ax) + styles.heading(letter=letter, heading=title, ax=ax) - # save figure - if config.plotSave: + if plot_show: + plt.show() + if plot_save: sim_folder = os.path.split(sim_ws)[0] sim_folder = os.path.basename(sim_folder) - fname = f"{sim_folder}-head{config.figure_ext}" - fpth = os.path.join(ws, "..", "figures", fname) + fname = f"{sim_folder}-head.png" + fpth = os.path.join(workspace, "..", "figures", fname) fig.savefig(fpth) def plot_gwt_results(sims): - if config.plotModel: - print("Plotting model results...") - sim_mf6gwf, sim_mf6gwt = sims - gwf = sim_mf6gwf.flow - gwt = sim_mf6gwt.trans - fs = USGSFigure(figure_type="map", verbose=False) + print("Plotting model results...") + sim_mf6gwf, sim_mf6gwt = sims + gwf = sim_mf6gwf.flow + gwt = sim_mf6gwt.trans + with styles.USGSMap() as fs: sim_ws = sim_mf6gwt.simulation_data.mfpath.get_sim_path() conc = gwt.output.concentration().get_data() @@ -610,9 +599,7 @@ def plot_gwt_results(sims): lake_conc = lakconc[ilak] conc[0, i, j] = lake_conc - fig, axs = plt.subplots( - 2, 2, figsize=(5, 7), dpi=300, tight_layout=True - ) + fig, axs = plt.subplots(2, 2, figsize=(5, 7), dpi=300, tight_layout=True) for iplot, ilay in enumerate([0, 2, 4, 7]): ax = axs.flatten()[iplot] @@ -643,14 +630,15 @@ def plot_gwt_results(sims): ax.clabel(cs, cs.levels[::1], fmt="%1.0f", colors="b") title = f"Model Layer {ilay + 1}" letter = chr(ord("@") + iplot + 1) - fs.heading(letter=letter, heading=title, ax=ax) + styles.heading(letter=letter, heading=title, ax=ax) - # save figure - if config.plotSave: + if plot_show: + plt.show() + if plot_save: sim_folder = os.path.split(sim_ws)[0] sim_folder = os.path.basename(sim_folder) - fname = f"{sim_folder}-conc{config.figure_ext}" - fpth = os.path.join(ws, "..", "figures", fname) + fname = f"{sim_folder}-conc.png" + fpth = os.path.join(workspace, "..", "figures", fname) fig.savefig(fpth) # create concentration timeseries plot @@ -659,74 +647,68 @@ def plot_gwt_results(sims): sfaconc = bobj.get_alldata()[:, 0, 0, :] times = bobj.times - fs = USGSFigure(figure_type="graph", verbose=False) - fig, axs = plt.subplots( - 1, 1, figsize=(5, 3), dpi=300, tight_layout=True - ) - ax = axs - times = np.array(times) / 365.0 - ax.plot( - times, lkaconc[:, 0], "b-", label="Lake 1 and Stream Segment 2" - ) - ax.plot(times, sfaconc[:, 30], "r-", label="Stream Segment 3") - ax.plot(times, sfaconc[:, 37], "g-", label="Stream Segment 4") - - fname = os.path.join(data_ws, "teststrm.sg2") - sg = np.genfromtxt(fname, comments='"') - ax.plot(sg[:, 0] / 365.0, sg[:, 6], "b--") - - fname = os.path.join(data_ws, "teststrm.sg3") - sg = np.genfromtxt(fname, comments='"') - ax.plot(sg[:, 0] / 365.0, sg[:, 6], "r--") - - fname = os.path.join(data_ws, "teststrm.sg4") - sg = np.genfromtxt(fname, comments='"') - ax.plot(sg[:, 0] / 365.0, sg[:, 3], "g--") - - fs.graph_legend() - ax.set_ylim(0, 50) - ax.set_xlim(0, 25) - ax.set_xlabel("TIME, IN YEARS") - ax.set_ylabel( - "SIMULATED BORON CONCENTRATION,\nIN MICROGRAMS PER LITER" - ) - - # save figure - if config.plotSave: - sim_folder = os.path.split(sim_ws)[0] - sim_folder = os.path.basename(sim_folder) - fname = f"{sim_folder}-cvt{config.figure_ext}" - fpth = os.path.join(ws, "..", "figures", fname) - fig.savefig(fpth) + with styles.USGSPlot(): + fig, axs = plt.subplots(1, 1, figsize=(5, 3), dpi=300, tight_layout=True) + ax = axs + times = np.array(times) / 365.0 + ax.plot(times, lkaconc[:, 0], "b-", label="Lake 1 and Stream Segment 2") + ax.plot(times, sfaconc[:, 30], "r-", label="Stream Segment 3") + ax.plot(times, sfaconc[:, 37], "g-", label="Stream Segment 4") + + fname = pooch.retrieve( + url=f"https://github.com/MODFLOW-USGS/modflow6-examples/raw/master/data/{example_name}/teststrm.sg2", + known_hash="md5:4bb5e256ed8b67f1743d547b43a610d0", + ) + sg = np.genfromtxt(fname, comments='"') + ax.plot(sg[:, 0] / 365.0, sg[:, 6], "b--") + fname = pooch.retrieve( + url=f"https://github.com/MODFLOW-USGS/modflow6-examples/raw/master/data/{example_name}/teststrm.sg3", + known_hash="md5:a30d8e27d0bbe09dcb9f39d115592ff5", + ) + sg = np.genfromtxt(fname, comments='"') + ax.plot(sg[:, 0] / 365.0, sg[:, 6], "r--") -# Function that wraps all of the steps for each scenario -# -# 1. build_model, -# 2. write_model, -# 3. run_model, and -# 4. plot_results. -# + fname = pooch.retrieve( + url=f"https://github.com/MODFLOW-USGS/modflow6-examples/raw/master/data/{example_name}/teststrm.sg4", + known_hash="md5:ec589d7333fe160842945b5895f5160a", + ) + sg = np.genfromtxt(fname, comments='"') + ax.plot(sg[:, 0] / 365.0, sg[:, 3], "g--") + styles.graph_legend() + ax.set_ylim(0, 50) + ax.set_xlim(0, 25) + ax.set_xlabel("TIME, IN YEARS") + ax.set_ylabel("SIMULATED BORON CONCENTRATION,\nIN MICROGRAMS PER LITER") -def scenario(idx, silent=True): - sims = build_model(example_name) - write_model(sims, silent=silent) - success = run_model(sims, silent=silent) - if success: - plot_results(sims) + if plot_show: + plt.show() + if plot_save: + sim_folder = os.path.split(sim_ws)[0] + sim_folder = os.path.basename(sim_folder) + fname = f"{sim_folder}-cvt.png" + fpth = os.path.join(workspace, "..", "figures", fname) + fig.savefig(fpth) -# nosetest - exclude block from this nosetest to the next nosetest -def test_01(): - scenario(0, silent=False) +# - +# ### Running the example +# +# Define and invoke a function to run the example scenario, then plot results. -# nosetest end -if __name__ == "__main__": - # ### Model +# + +def scenario(silent=True): + sims = build_models(example_name) + if write: + write_models(sims, silent=silent) + if run: + run_models(sims, silent=silent) + if plot: + plot_results(sims) - # Model run - scenario(0) +scenario() +# - diff --git a/scripts/ex-gwt-rotate.py b/scripts/ex-gwt-rotate.py index 5e7449e73..505cb55fc 100644 --- a/scripts/ex-gwt-rotate.py +++ b/scripts/ex-gwt-rotate.py @@ -1,51 +1,45 @@ -# ## Rotating Interface Problem +# ## Rotating interface example # -# Density driven groundwater flow -# -# - +# This example demonstrates density-driven groundwater flow. -# ### Rotating Interface Problem Setup - -# Imports +# ### Initial setup +# +# Import dependencies, define some variables, and read settings from environment variables. +# + import os -import sys +import pathlib as pl +from pprint import pformat import flopy import matplotlib.pyplot as plt import numpy as np +from flopy.plot.styles import styles +from modflow_devtools.misc import get_env, timed + +# Example name and base workspace +sim_name = "ex-gwt-rotate" +workspace = pl.Path("../examples") + +# Settings from environment variables +write = get_env("WRITE", True) +run = get_env("RUN", True) +plot = get_env("PLOT", True) +plot_show = get_env("PLOT_SHOW", True) +plot_save = get_env("PLOT_SAVE", True) +gif_save = get_env("GIF", True) +# - + +# ### Define parameters +# +# Define model units, parameters and other settings. -# Append to system path to include the common subdirectory - -sys.path.append(os.path.join("..", "common")) - -# Import common functionality - -import config -from analytical import BakkerRotatingInterface -from figspecs import USGSFigure - -mf6exe = "mf6" -exe_name_mf = "mf2005" -exe_name_mt = "mt3dms" - -# Set figure properties specific to this problem - -figure_size = (6, 4) - -# Base simulation and model name and workspace - -ws = config.base_ws -example_name = "ex-gwt-rotate" - +# + # Model units - length_units = "meters" time_units = "days" -# Table of model parameters - +# Model parameters nper = 1 # Number of periods nstp = 1000 # Number of time steps perlen = 10000 # Simulation time length ($d$) @@ -75,18 +69,101 @@ x2 = 130.0 # X-midpoint location for zone 2 and 3 interface porosity = 0.2 # Porosity (unitless) +# Grid bottom elevations botm = [top - k * delv for k in range(1, nlay + 1)] +# Solver criteria nouter, ninner = 100, 300 hclose, rclose, relax = 1e-8, 1e-8, 0.97 +# - - -# ### Functions to build, write, run, and plot models +# ### Analytical solution # -# MODFLOW 6 flopy GWF simulation object (sim) is returned +# Define an analytical solution for rotating interfaces (from [Bakker et al 2004](https://doi.org/10.1016/j.jhydrol.2003.10.007)) + + +class BakkerRotatingInterface: + """ + Analytical solution for rotating interfaces. + + """ + + @staticmethod + def get_s(k, rhoa, rhob, alpha): + return k * (rhob - rhoa) / rhoa * np.sin(alpha) + + @staticmethod + def get_F(z, zeta1, omega1, s): + l = (zeta1.real - omega1.real) ** 2 + (zeta1.imag - omega1.imag) ** 2 + l = np.sqrt(l) + try: + v = ( + s + * l + * complex(0, 1) + / 2 + / np.pi + / (zeta1 - omega1) + * np.log((z - zeta1) / (z - omega1)) + ) + except: + v = 0.0 + return v + + @staticmethod + def get_Fgrid(xg, yg, zeta1, omega1, s): + qxg = [] + qyg = [] + for x, y in zip(xg.flatten(), yg.flatten()): + z = complex(x, y) + W = BakkerRotatingInterface.get_F(z, zeta1, omega1, s) + qx = W.real + qy = -W.imag + qxg.append(qx) + qyg.append(qy) + qxg = np.array(qxg) + qyg = np.array(qyg) + qxg = qxg.reshape(xg.shape) + qyg = qyg.reshape(yg.shape) + return qxg, qyg + + @staticmethod + def get_zetan(n, x0, a, b): + return complex(x0 + (-1) ** n * a, (2 * n - 1) * b) + + @staticmethod + def get_omegan(n, x0, a, b): + return complex(x0 + (-1) ** (1 + n) * a, -(2 * n - 1) * b) + + @staticmethod + def get_w(xg, yg, k, rhoa, rhob, a, b, x0): + zeta1 = BakkerRotatingInterface.get_zetan(1, x0, a, b) + omega1 = BakkerRotatingInterface.get_omegan(1, x0, a, b) + alpha = np.arctan2(b, a) + s = BakkerRotatingInterface.get_s(k, rhoa, rhob, alpha) + qxg, qyg = BakkerRotatingInterface.get_Fgrid(xg, yg, zeta1, omega1, s) + for n in range(1, 5): + zetan = BakkerRotatingInterface.get_zetan(n, x0, a, b) + zetanp1 = BakkerRotatingInterface.get_zetan(n + 1, x0, a, b) + qx1, qy1 = BakkerRotatingInterface.get_Fgrid( + xg, yg, zetan, zetanp1, (-1) ** n * s + ) + omegan = BakkerRotatingInterface.get_omegan(n, x0, a, b) + omeganp1 = BakkerRotatingInterface.get_omegan(n + 1, x0, a, b) + qx2, qy2 = BakkerRotatingInterface.get_Fgrid( + xg, yg, omegan, omeganp1, (-1) ** n * s + ) + qxg += qx1 + qx2 + qyg += qy1 + qy2 + return qxg, qyg + + +# ### Model setup # +# Define functions to build models, write input files, and run the simulation. +# + def get_cstrt(nlay, ncol, length, x1, x2, a1, a2, b, c1, c2, c3): cstrt = c1 * np.ones((nlay, ncol), dtype=float) from flopy.utils.gridintersect import GridIntersect @@ -105,10 +182,10 @@ def get_cstrt(nlay, ncol, length, x1, x2, a1, a2, b, c1, c2, c3): return cstrt -def build_model(sim_folder): +def build_models(sim_folder): print(f"Building model...{sim_folder}") name = "flow" - sim_ws = os.path.join(ws, sim_folder) + sim_ws = os.path.join(workspace, sim_folder) sim = flopy.mf6.MFSimulation( sim_name=name, sim_ws=sim_ws, @@ -116,9 +193,7 @@ def build_model(sim_folder): continue_=True, ) tdis_ds = ((perlen, nstp, 1.0),) - flopy.mf6.ModflowTdis( - sim, nper=nper, perioddata=tdis_ds, time_units=time_units - ) + flopy.mf6.ModflowTdis(sim, nper=nper, perioddata=tdis_ds, time_units=time_units) gwf = flopy.mf6.ModflowGwf(sim, modelname=name, save_flows=True) ims = flopy.mf6.ModflowIms( sim, @@ -201,9 +276,7 @@ def build_model(sim_folder): gwt, budget_filerecord=f"{gwt.name}.cbc", concentration_filerecord=f"{gwt.name}.ucn", - concentrationprintrecord=[ - ("COLUMNS", 10, "WIDTH", 15, "DIGITS", 6, "GENERAL") - ], + concentrationprintrecord=[("COLUMNS", 10, "WIDTH", 15, "DIGITS", 6, "GENERAL")], saverecord=[("CONCENTRATION", "ALL")], printrecord=[("CONCENTRATION", "LAST"), ("BUDGET", "LAST")], ) @@ -213,251 +286,222 @@ def build_model(sim_folder): return sim -# Function to write model files - +def write_models(sim, silent=True): + sim.write_simulation(silent=silent) -def write_model(sim, silent=True): - if config.writeModel: - sim.write_simulation(silent=silent) - return +@timed +def run_models(sim, silent=True): + success, buff = sim.run_simulation(silent=silent, report=True) + assert success, pformat(buff) -# Function to run the model -# True is returned if the model runs successfully +# - -@config.timeit -def run_model(sim, silent=True): - success = True - if config.runModel: - success = False - success, buff = sim.run_simulation(silent=silent) - if not success: - print(buff) - return success - +# ### Plotting results +# +# Define functions to plot model results. -# Function to plot the model results +# + +# Figure properties +figure_size = (6, 4) def plot_velocity_profile(sim, idx): - fs = USGSFigure(figure_type="map", verbose=False) - sim_name = example_name - sim_ws = os.path.join(ws, sim_name) - gwf = sim.get_model("flow") - gwt = sim.get_model("trans") - print("Creating velocity profile plot...") - - # find line of cells on left side of first interface - cstrt = gwt.ic.strt.array - cstrt = cstrt.reshape((nlay, ncol)) - interface_coords = [] - for k in range(nlay): - crow = cstrt[k] - j = (np.abs(crow - c2)).argmin() - 1 - interface_coords.append((k, j)) - - # plot velocity - xc, yc, zc = gwt.modelgrid.xyzcellcenters - xg = [] - zg = [] - for k, j in interface_coords: - x = xc[0, j] - z = zc[k, 0, j] - xg.append(x) - zg.append(z) - xg = np.array(xg) - zg = np.array(zg) - - # set up plot - fig = plt.figure(figsize=(4, 6)) - ax = fig.add_subplot(1, 1, 1) - - # plot analytical solution - qx1, qz1 = BakkerRotatingInterface.get_w( - xg, zg, hydraulic_conductivity, rho1, rho2, a1, b, x1 - ) - qx2, qz2 = BakkerRotatingInterface.get_w( - xg, zg, hydraulic_conductivity, rho2, rho3, a2, b, x2 - ) - qx = qx1 + qx2 - qz = qz1 + qz2 - vh = qx + qz * a1 / b - vh = vh / porosity - ax.plot(vh, zg, "k-") - - # plot numerical results - file_name = gwf.oc.budget_filerecord.get_data()[0][0] - fpth = os.path.join(sim_ws, file_name) - bobj = flopy.utils.CellBudgetFile(fpth, precision="double") - kstpkper = bobj.get_kstpkper() - spdis = bobj.get_data(text="DATA-SPDIS", kstpkper=kstpkper[0])[0] - qxsim = spdis["qx"].reshape((nlay, ncol)) - qzsim = spdis["qz"].reshape((nlay, ncol)) - qx = [] - qz = [] - for k, j in interface_coords: - qx.append(qxsim[k, j]) - qz.append(qzsim[k, j]) - qx = np.array(qx) - qz = np.array(qz) - vh = qx + qz * a1 / b - vh = vh / porosity - ax.plot(vh, zg, "bo", mfc="none") - - # configure plot and save - ax.plot([0, 0], [-b, b], "k--", linewidth=0.5) - ax.set_xlim(-0.1, 0.1) - ax.set_ylim(-b, b) - ax.set_ylabel("z location of left interface (m)") - ax.set_xlabel("$v_h$ (m/d) of left interface at t=0") - # save figure - if config.plotSave: - fpth = os.path.join( - "..", "figures", f"{sim_name}-vh{config.figure_ext}" + with styles.USGSMap() as fs: + sim_ws = os.path.join(workspace, sim_name) + gwf = sim.get_model("flow") + gwt = sim.get_model("trans") + print("Creating velocity profile plot...") + + # find line of cells on left side of first interface + cstrt = gwt.ic.strt.array + cstrt = cstrt.reshape((nlay, ncol)) + interface_coords = [] + for k in range(nlay): + crow = cstrt[k] + j = (np.abs(crow - c2)).argmin() - 1 + interface_coords.append((k, j)) + + # plot velocity + xc, yc, zc = gwt.modelgrid.xyzcellcenters + xg = [] + zg = [] + for k, j in interface_coords: + x = xc[0, j] + z = zc[k, 0, j] + xg.append(x) + zg.append(z) + xg = np.array(xg) + zg = np.array(zg) + + # set up plot + fig = plt.figure(figsize=(4, 6)) + ax = fig.add_subplot(1, 1, 1) + + # plot analytical solution + qx1, qz1 = BakkerRotatingInterface.get_w( + xg, zg, hydraulic_conductivity, rho1, rho2, a1, b, x1 + ) + qx2, qz2 = BakkerRotatingInterface.get_w( + xg, zg, hydraulic_conductivity, rho2, rho3, a2, b, x2 ) - fig.savefig(fpth) + qx = qx1 + qx2 + qz = qz1 + qz2 + vh = qx + qz * a1 / b + vh = vh / porosity + ax.plot(vh, zg, "k-") + + # plot numerical results + file_name = gwf.oc.budget_filerecord.get_data()[0][0] + fpth = os.path.join(sim_ws, file_name) + bobj = flopy.utils.CellBudgetFile(fpth, precision="double") + kstpkper = bobj.get_kstpkper() + spdis = bobj.get_data(text="DATA-SPDIS", kstpkper=kstpkper[0])[0] + qxsim = spdis["qx"].reshape((nlay, ncol)) + qzsim = spdis["qz"].reshape((nlay, ncol)) + qx = [] + qz = [] + for k, j in interface_coords: + qx.append(qxsim[k, j]) + qz.append(qzsim[k, j]) + qx = np.array(qx) + qz = np.array(qz) + vh = qx + qz * a1 / b + vh = vh / porosity + ax.plot(vh, zg, "bo", mfc="none") + + # configure plot and save + ax.plot([0, 0], [-b, b], "k--", linewidth=0.5) + ax.set_xlim(-0.1, 0.1) + ax.set_ylim(-b, b) + ax.set_ylabel("z location of left interface (m)") + ax.set_xlabel("$v_h$ (m/d) of left interface at t=0") + if plot_show: + plt.show() + if plot_save: + fpth = os.path.join("..", "figures", f"{sim_name}-vh.png") + fig.savefig(fpth) def plot_conc(sim, idx): - fs = USGSFigure(figure_type="map", verbose=False) - sim_name = example_name - sim_ws = os.path.join(ws, sim_name) - gwf = sim.get_model("flow") - gwt = sim.get_model("trans") - - # make initial conditions figure - print("Creating initial conditions figure...") - fig = plt.figure(figsize=(6, 4)) - ax = fig.add_subplot(1, 1, 1, aspect="equal") - pxs = flopy.plot.PlotCrossSection(model=gwf, ax=ax, line={"row": 0}) - pxs.plot_array(gwt.ic.strt.array, cmap="jet", vmin=c1, vmax=c3) - pxs.plot_grid(linewidth=0.1) - ax.set_ylabel("z position (m)") - ax.set_xlabel("x position (m)") - if config.plotSave: - fpth = os.path.join( - "..", "figures", f"{sim_name}-bc{config.figure_ext}" - ) - fig.savefig(fpth) - plt.close("all") - - # make results plot - print("Making results plot...") - fig = plt.figure(figsize=figure_size) - fig.tight_layout() - - # create MODFLOW 6 head object - cobj = gwt.output.concentration() - times = cobj.get_times() - times = np.array(times) - - # plot times in the original publication - plot_times = [ - 2000.0, - 10000.0, - ] - - nplots = len(plot_times) - for iplot in range(nplots): - time_in_pub = plot_times[iplot] - idx_conc = (np.abs(times - time_in_pub)).argmin() - time_this_plot = times[idx_conc] - conc = cobj.get_data(totim=time_this_plot) - - ax = fig.add_subplot(2, 1, iplot + 1) + with styles.USGSMap() as fs: + sim_ws = os.path.join(workspace, sim_name) + gwf = sim.get_model("flow") + gwt = sim.get_model("trans") + + # make initial conditions figure + print("Creating initial conditions figure...") + fig = plt.figure(figsize=(6, 4)) + ax = fig.add_subplot(1, 1, 1, aspect="equal") pxs = flopy.plot.PlotCrossSection(model=gwf, ax=ax, line={"row": 0}) - pxs.plot_array(conc, cmap="jet", vmin=c1, vmax=c3) - ax.set_xlim(0, length) - ax.set_ylim(-height / 2.0, height / 2.0) + pxs.plot_array(gwt.ic.strt.array, cmap="jet", vmin=c1, vmax=c3) + pxs.plot_grid(linewidth=0.1) ax.set_ylabel("z position (m)") ax.set_xlabel("x position (m)") - ax.set_title(f"Time = {time_this_plot} days") - plt.tight_layout() - - # save figure - if config.plotSave: - fpth = os.path.join( - "..", "figures", f"{sim_name}-conc{config.figure_ext}" - ) - fig.savefig(fpth) - return + if plot_save: + fpth = os.path.join("..", "figures", f"{sim_name}-bc.png") + fig.savefig(fpth) + plt.close("all") + + # make results plot + print("Making results plot...") + fig = plt.figure(figsize=figure_size) + fig.tight_layout() + + # create MODFLOW 6 head object + cobj = gwt.output.concentration() + times = cobj.get_times() + times = np.array(times) + + # plot times in the original publication + plot_times = [ + 2000.0, + 10000.0, + ] + + nplots = len(plot_times) + for iplot in range(nplots): + time_in_pub = plot_times[iplot] + idx_conc = (np.abs(times - time_in_pub)).argmin() + time_this_plot = times[idx_conc] + conc = cobj.get_data(totim=time_this_plot) + + ax = fig.add_subplot(2, 1, iplot + 1) + pxs = flopy.plot.PlotCrossSection(model=gwf, ax=ax, line={"row": 0}) + pxs.plot_array(conc, cmap="jet", vmin=c1, vmax=c3) + ax.set_xlim(0, length) + ax.set_ylim(-height / 2.0, height / 2.0) + ax.set_ylabel("z position (m)") + ax.set_xlabel("x position (m)") + ax.set_title(f"Time = {time_this_plot} days") + plt.tight_layout() + + if plot_show: + plt.show() + if plot_save: + fpth = os.path.join("..", "figures", f"{sim_name}-conc.png") + fig.savefig(fpth) def make_animated_gif(sim, idx): from matplotlib.animation import FuncAnimation, PillowWriter print("Creating animation...") - fs = USGSFigure(figure_type="map", verbose=False) - sim_name = example_name - sim_ws = os.path.join(ws, sim_name) - gwf = sim.get_model("flow") - gwt = sim.get_model("trans") - - cobj = gwt.output.concentration() - times = cobj.get_times() - times = np.array(times) - conc = cobj.get_alldata() - - fig = plt.figure(figsize=(6, 4)) - ax = fig.add_subplot(1, 1, 1, aspect="equal") - pxs = flopy.plot.PlotCrossSection(model=gwf, ax=ax, line={"row": 0}) - pc = pxs.plot_array(conc[0], cmap="jet", vmin=c1, vmax=c3) - - def init(): - ax.set_xlim(0, length) - ax.set_ylim(-height / 2, height / 2) - ax.set_title(f"Time = {times[0]} seconds") - - def update(i): - pc.set_array(conc[i].flatten()) - ax.set_title(f"Time = {times[i]} days") - - ani = FuncAnimation( - fig, update, range(1, times.shape[0], 5), init_func=init - ) - writer = PillowWriter(fps=50) - fpth = os.path.join("..", "figures", "{}{}".format(sim_name, ".gif")) - ani.save(fpth, writer=writer) - return - + with styles.USGSMap() as fs: + sim_ws = os.path.join(workspace, sim_name) + gwf = sim.get_model("flow") + gwt = sim.get_model("trans") + + cobj = gwt.output.concentration() + times = cobj.get_times() + times = np.array(times) + conc = cobj.get_alldata() + + fig = plt.figure(figsize=(6, 4)) + ax = fig.add_subplot(1, 1, 1, aspect="equal") + pxs = flopy.plot.PlotCrossSection(model=gwf, ax=ax, line={"row": 0}) + pc = pxs.plot_array(conc[0], cmap="jet", vmin=c1, vmax=c3) -def plot_results(sim, idx): - if config.plotModel: - plot_conc(sim, idx) - plot_velocity_profile(sim, idx) - if config.plotSave and config.createGif: - make_animated_gif(sim, idx) - return + def init(): + ax.set_xlim(0, length) + ax.set_ylim(-height / 2, height / 2) + ax.set_title(f"Time = {times[0]} seconds") + def update(i): + pc.set_array(conc[i].flatten()) + ax.set_title(f"Time = {times[i]} days") -# Function that wraps all of the steps for each scenario -# -# 1. build_model, -# 2. write_model, -# 3. run_model, and -# 4. plot_results. -# + ani = FuncAnimation(fig, update, range(1, times.shape[0], 5), init_func=init) + writer = PillowWriter(fps=50) + fpth = os.path.join("..", "figures", "{}{}".format(sim_name, ".gif")) + ani.save(fpth, writer=writer) -def scenario(idx, silent=True): - sim = build_model(example_name) - write_model(sim, silent=silent) - success = run_model(sim, silent=silent) - if success: - plot_results(sim, idx) +def plot_results(sim, idx): + plot_conc(sim, idx) + plot_velocity_profile(sim, idx) + if plot_save and gif_save: + make_animated_gif(sim, idx) -# nosetest - exclude block from this nosetest to the next nosetest -def test_01(): - scenario(0, silent=False) +# - +# ### Running the example +# +# Define and invoke a function to run the entire scenario, then plot results. -# nosetest end -if __name__ == "__main__": - # ### Rotating Interface Problem +# + +def scenario(idx, silent=True): + sim = build_models(sim_name) + if write: + write_models(sim, silent=silent) + if run: + run_models(sim, silent=silent) + if plot: + plot_results(sim, idx) - # Plot showing MODFLOW 6 results - scenario(0) +scenario(0) +# - diff --git a/scripts/ex-gwt-saltlake.py b/scripts/ex-gwt-saltlake.py index 8670765a1..f48b56242 100644 --- a/scripts/ex-gwt-saltlake.py +++ b/scripts/ex-gwt-saltlake.py @@ -1,50 +1,52 @@ # ## Salt Lake Problem # -# Density driven groundwater flow +# The salt lake problem was suggested by Simmons 1999 as a comprehensive benchmark +# test for variable-density groundwater flow models. The problem is based on dense +# salt fingers that descend from an evaporating salt lake. Although an analytical +# solution is not available for the salt lake problem, an equivalent Hele-Shaw +# analysis was performed in the laboratory to investigate the movement of dense +# salt fingers Wooding 1997. In addition to the SUTRA simulation, this salt lake +# problem was simulated by Langevin et al 2003 using SEAWAT-2000. The approach +# described by Langevin et al 2003 is reproduced here with MODFLOW 6. + +# ### Initial setup # -# - - -# ### Salt Lake Problem Setup - -# Imports +# Import dependencies, define the example name and workspace, and read settings from environment variables. +# + import os -import sys +import pathlib as pl +from pprint import pformat import flopy import matplotlib.pyplot as plt import numpy as np +from flopy.plot.styles import styles +from modflow_devtools.misc import get_env, timed -# Append to system path to include the common subdirectory - -sys.path.append(os.path.join("..", "common")) - -# Import common functionality - -import config -from figspecs import USGSFigure - -mf6exe = "mf6" -exe_name_mf = "mf2005" -exe_name_mt = "mt3dms" - -# Set figure properties specific to this problem - -figure_size = (6, 8) +# Example name and base workspace +workspace = pl.Path("../examples") +example_name = "ex-gwt-saltlake" -# Base simulation and model name and workspace +# Settings from environment variables +write = get_env("WRITE", True) +run = get_env("RUN", True) +plot = get_env("PLOT", True) +plot_show = get_env("PLOT_SHOW", True) +plot_save = get_env("PLOT_SAVE", True) +gif_save = get_env("GIF", True) +# - -ws = config.base_ws -example_name = "ex-gwt-saltlake" +# ### Define parameters +# +# Define model units, parameters and other settings. +# + # Model units - length_units = "mm" time_units = "seconds" -# Table of model parameters - +# Model parameters nper = 1 # Number of periods nstp = 400 # Number of time steps perlen = 24000 # Simulation time length ($s$) @@ -79,27 +81,25 @@ nouter, ninner = 100, 300 hclose, rclose, relax = 1e-8, 1e-8, 0.97 +# - - -# ### Functions to build, write, run, and plot models -# -# MODFLOW 6 flopy GWF simulation object (sim) is returned +# ### Model setup # +# Define functions to build models, write input files, and run the simulation. -def build_model(sim_folder): +# + +def build_models(sim_folder): print(f"Building model...{sim_folder}") name = "flow" - sim_ws = os.path.join(ws, sim_folder) + sim_ws = os.path.join(workspace, sim_folder) sim = flopy.mf6.MFSimulation( sim_name=name, sim_ws=sim_ws, exe_name="mf6", ) tdis_ds = ((perlen, nstp, 1.0),) - flopy.mf6.ModflowTdis( - sim, nper=nper, perioddata=tdis_ds, time_units=time_units - ) + flopy.mf6.ModflowTdis(sim, nper=nper, perioddata=tdis_ds, time_units=time_units) gwf = flopy.mf6.ModflowGwf(sim, modelname=name, save_flows=True) ims = flopy.mf6.ModflowIms( sim, @@ -191,9 +191,7 @@ def build_model(sim_folder): flopy.mf6.ModflowGwtmst(gwt, porosity=porosity) flopy.mf6.ModflowGwtic(gwt, strt=conc_inflow) flopy.mf6.ModflowGwtadv(gwt, scheme="UPSTREAM") - flopy.mf6.ModflowGwtdsp( - gwt, xt3d_off=True, alh=alphal, ath1=alphat, diffc=diffc - ) + flopy.mf6.ModflowGwtdsp(gwt, xt3d_off=True, alh=alphal, ath1=alphat, diffc=diffc) sourcerecarray = [ ("CHD-1", "AUX", "CONCENTRATION"), ] @@ -211,9 +209,7 @@ def build_model(sim_folder): gwt, budget_filerecord=f"{gwt.name}.cbc", concentration_filerecord=f"{gwt.name}.ucn", - concentrationprintrecord=[ - ("COLUMNS", 10, "WIDTH", 15, "DIGITS", 6, "GENERAL") - ], + concentrationprintrecord=[("COLUMNS", 10, "WIDTH", 15, "DIGITS", 6, "GENERAL")], saverecord=[("CONCENTRATION", "ALL")], printrecord=[("CONCENTRATION", "LAST"), ("BUDGET", "LAST")], ) @@ -223,173 +219,150 @@ def build_model(sim_folder): return sim -# Function to write model files - +def write_models(sim, silent=True): + sim.write_simulation(silent=silent) -def write_model(sim, silent=True): - if config.writeModel: - sim.write_simulation(silent=silent) - return +@timed +def run_models(sim, silent=True): + success, buff = sim.run_simulation(silent=silent, report=True) + assert success, pformat(buff) -# Function to run the model -# True is returned if the model runs successfully +# - -@config.timeit -def run_model(sim, silent=True): - success = True - if config.runModel: - success = False - success, buff = sim.run_simulation(silent=silent) - if not success: - print(buff) - return success - +# ### Plotting results +# +# Define functions to plot model results. -# Function to plot the model results +# + +# Figure properties +figure_size = (6, 8) def plot_conc(sim, idx): - fs = USGSFigure(figure_type="map", verbose=False) - sim_name = example_name - sim_ws = os.path.join(ws, sim_name) - gwf = sim.get_model("flow") - gwt = sim.get_model("trans") - - # make bc figure - fig = plt.figure(figsize=(6, 4)) - ax = fig.add_subplot(1, 1, 1, aspect="equal") - pxs = flopy.plot.PlotCrossSection(model=gwf, ax=ax, line={"row": 0}) - pxs.plot_grid(linewidth=0.1) - pxs.plot_bc("RCH-1", color="red") - pxs.plot_bc("CHD-1", color="blue") - ax.set_ylabel("z position (m)") - ax.set_xlabel("x position (m)") - if config.plotSave: - fpth = os.path.join( - "..", "figures", f"{sim_name}-bc{config.figure_ext}" - ) - fig.savefig(fpth) - plt.close("all") - - # make results plot - fig = plt.figure(figsize=figure_size) - fig.tight_layout() - - # create MODFLOW 6 head object - cobj = gwt.output.concentration() - times = cobj.get_times() - times = np.array(times) - - # plot times in the original publication - plot_times = [ - 2581.0, - 15485.0, - 5162.0, - 18053.0, - 10311.0, - 20634.0, - 12904.0, - 23215.0, - ] - - nplots = len(plot_times) - for iplot in range(nplots): - time_in_pub = plot_times[iplot] - idx_conc = (np.abs(times - time_in_pub)).argmin() - time_this_plot = times[idx_conc] - conc = cobj.get_data(totim=time_this_plot) - - ax = fig.add_subplot(4, 2, iplot + 1) + with styles.USGSMap() as fs: + sim_name = example_name + sim_ws = os.path.join(workspace, sim_name) + gwf = sim.get_model("flow") + gwt = sim.get_model("trans") + + # make bc figure + fig = plt.figure(figsize=(6, 4)) + ax = fig.add_subplot(1, 1, 1, aspect="equal") pxs = flopy.plot.PlotCrossSection(model=gwf, ax=ax, line={"row": 0}) - pxs.plot_array(conc, cmap="jet", vmin=conc_inflow, vmax=conc_sat) - ax.set_xlim(0, 75.0) + pxs.plot_grid(linewidth=0.1) + pxs.plot_bc("RCH-1", color="red") + pxs.plot_bc("CHD-1", color="blue") ax.set_ylabel("z position (m)") - if iplot in [6, 7]: - ax.set_xlabel("x position (m)") - ax.set_title(f"Time = {time_this_plot} seconds") - plt.tight_layout() - - # save figure - if config.plotSave: - fpth = os.path.join( - "..", "figures", f"{sim_name}-conc{config.figure_ext}" - ) - fig.savefig(fpth) - return + ax.set_xlabel("x position (m)") + if plot_save: + fpth = os.path.join("..", "figures", f"{sim_name}-bc.png") + fig.savefig(fpth) + plt.close("all") + + # make results plot + fig = plt.figure(figsize=figure_size) + fig.tight_layout() + + # create MODFLOW 6 head object + cobj = gwt.output.concentration() + times = cobj.get_times() + times = np.array(times) + + # plot times in the original publication + plot_times = [ + 2581.0, + 15485.0, + 5162.0, + 18053.0, + 10311.0, + 20634.0, + 12904.0, + 23215.0, + ] + + nplots = len(plot_times) + for iplot in range(nplots): + time_in_pub = plot_times[iplot] + idx_conc = (np.abs(times - time_in_pub)).argmin() + time_this_plot = times[idx_conc] + conc = cobj.get_data(totim=time_this_plot) + + ax = fig.add_subplot(4, 2, iplot + 1) + pxs = flopy.plot.PlotCrossSection(model=gwf, ax=ax, line={"row": 0}) + pxs.plot_array(conc, cmap="jet", vmin=conc_inflow, vmax=conc_sat) + ax.set_xlim(0, 75.0) + ax.set_ylabel("z position (m)") + if iplot in [6, 7]: + ax.set_xlabel("x position (m)") + ax.set_title(f"Time = {time_this_plot} seconds") + plt.tight_layout() + + if plot_show: + plt.show() + if plot_save: + fpth = os.path.join("..", "figures", f"{sim_name}-conc.png") + fig.savefig(fpth) def make_animated_gif(sim, idx): from matplotlib.animation import FuncAnimation, PillowWriter - fs = USGSFigure(figure_type="map", verbose=False) - sim_name = example_name - sim_ws = os.path.join(ws, sim_name) - gwf = sim.get_model("flow") - gwt = sim.get_model("trans") + with styles.USGSMap() as fs: + sim_name = example_name + sim_ws = os.path.join(workspace, sim_name) + gwf = sim.get_model("flow") + gwt = sim.get_model("trans") - cobj = gwt.output.concentration() - times = cobj.get_times() - times = np.array(times) - conc = cobj.get_alldata() + cobj = gwt.output.concentration() + times = cobj.get_times() + times = np.array(times) + conc = cobj.get_alldata() - fig = plt.figure(figsize=(6, 4)) - ax = fig.add_subplot(1, 1, 1, aspect="equal") - pxs = flopy.plot.PlotCrossSection(model=gwf, ax=ax, line={"row": 0}) - pc = pxs.plot_array(conc[0], cmap="jet", vmin=conc_inflow, vmax=conc_sat) + fig = plt.figure(figsize=(6, 4)) + ax = fig.add_subplot(1, 1, 1, aspect="equal") + pxs = flopy.plot.PlotCrossSection(model=gwf, ax=ax, line={"row": 0}) + pc = pxs.plot_array(conc[0], cmap="jet", vmin=conc_inflow, vmax=conc_sat) - def init(): - ax.set_xlim(0, 75.0) - ax.set_ylim(0, 75.0) - ax.set_title(f"Time = {times[0]} seconds") + def init(): + ax.set_xlim(0, 75.0) + ax.set_ylim(0, 75.0) + ax.set_title(f"Time = {times[0]} seconds") - def update(i): - pc.set_array(conc[i].flatten()) - ax.set_title(f"Time = {times[i]} seconds") + def update(i): + pc.set_array(conc[i].flatten()) + ax.set_title(f"Time = {times[i]} seconds") - ani = FuncAnimation(fig, update, range(1, times.shape[0]), init_func=init) - writer = PillowWriter(fps=50) - fpth = os.path.join("..", "figures", "{}{}".format(sim_name, ".gif")) - ani.save(fpth, writer=writer) - return + ani = FuncAnimation(fig, update, range(1, times.shape[0]), init_func=init) + writer = PillowWriter(fps=50) + fpth = os.path.join("..", "figures", "{}{}".format(sim_name, ".gif")) + ani.save(fpth, writer=writer) def plot_results(sim, idx): - if config.plotModel: - plot_conc(sim, idx) - if config.plotSave and config.createGif: - make_animated_gif(sim, idx) - return + plot_conc(sim, idx) + if plot_save and gif_save: + make_animated_gif(sim, idx) -# Function that wraps all of the steps for each scenario -# -# 1. build_model, -# 2. write_model, -# 3. run_model, and -# 4. plot_results. +# - + +# ### Running the example # +# Define and invoke a function to run the example scenario, then plot results. +# + def scenario(idx, silent=True): - sim = build_model(example_name) - write_model(sim, silent=silent) - success = run_model(sim, silent=silent) - if success: + sim = build_models(example_name) + if write: + write_models(sim, silent=silent) + if run: + run_models(sim, silent=silent) + if plot: plot_results(sim, idx) -# nosetest - exclude block from this nosetest to the next nosetest -def test_01(): - scenario(0, silent=False) - - -# nosetest end - -if __name__ == "__main__": - # ### Salt Lake Problem - - # Plot showing MODFLOW 6 results - - scenario(0) +scenario(0) +# - diff --git a/scripts/ex-gwt-stallman.py b/scripts/ex-gwt-stallman.py index d7faefa60..28bf56a11 100644 --- a/scripts/ex-gwt-stallman.py +++ b/scripts/ex-gwt-stallman.py @@ -1,49 +1,52 @@ # ## Stallman Problem # -# Periodic heat boundary condition at surface -# Transient heat transfer problem in vertical +# Stallman 1965 presents an analytical solution for transient heat flow +# in the subsurface in response to a sinusoidally varying temperature +# boundary imposed at the land surface, involving heat convection in +# response to downward groundwater flow. The problem also includes +# heat conduction through the fully saturated aquifer material. The +# analytical solution quantifies the temperature variation as a function +# of depth and time for this one-dimensional transient problem. + +# ### Initial setup # +# Import dependencies, define the example name and workspace, and read settings from environment variables. -# ### Stallman Problem Setup - -# Imports - +# + import os -import sys +import pathlib as pl +from pprint import pformat import flopy import matplotlib.animation as animation import matplotlib.pyplot as plt import numpy as np +from flopy.plot.styles import styles +from modflow_devtools.misc import get_env, timed -# Append to system path to include the common subdirectory - -sys.path.append(os.path.join("..", "common")) - -# Import common functionality - -import config -from analytical import Stallman -from figspecs import USGSFigure - -mf6exe = "mf6" - -# Set figure properties specific to this problem - -figure_size = (6, 8) +# Example namd and base workspace +workspace = pl.Path("../examples") +example_name = "ex-gwt-stallman" -# Base simulation and model name and workspace +# Settings from environment variables +write = get_env("WRITE", True) +run = get_env("RUN", True) +plot = get_env("PLOT", True) +plot_show = get_env("PLOT_SHOW", True) +plot_save = get_env("PLOT_SAVE", True) +gif_save = get_env("GIF", True) +# - -ws = config.base_ws -example_name = "ex-gwt-stallman" +# ### Define parameters +# +# Define model units, parameters and other settings. +# + # Model units - length_units = "meters" time_units = "seconds" -# Table of model parameters - +# Model parameters nper = 600 # Number of periods nstp = 6 # Number of time steps perlen = 525600 # Simulation time length ($s$) @@ -100,25 +103,42 @@ nouter, ninner = 100, 300 hclose, rclose, relax = 1e-8, 1e-8, 0.97 +# - -# ### Functions to build, write, run, and plot models -# -# MODFLOW 6 flopy GWF simulation object (sim) is returned +# ### Model setup # +# Define functions to build models, write input files, and run the simulation. + + +# + +# Analytical solution for Stallman analysis (Stallman 1965, JGR) +def Stallman(T_az, dT, tau, t, c_rho, darcy_flux, ko, c_w, rho_w, zbotm, nlay): + zstallman = np.zeros((nlay, 2)) + K = np.pi * c_rho / ko / tau + V = darcy_flux * c_w * rho_w / 2 / ko + a = ((K**2 + V**4 / 4) ** 0.5 + V**2 / 2) ** 0.5 - V + b = ((K**2 + V**4 / 4) ** 0.5 - V**2 / 2) ** 0.5 + for i in range(len(zstallman)): + zstallman[i, 0] = zbotm[i] + zstallman[i, 1] = ( + dT + * np.exp(-a * (-zstallman[i, 0])) + * np.sin(2 * np.pi * t / tau - b * (-zstallman[i, 0])) + + T_az + ) + return zstallman -def build_model(sim_folder): +def build_models(sim_folder): print(f"Building model...{sim_folder}") name = "flow" - sim_ws = os.path.join(ws, sim_folder) + sim_ws = os.path.join(workspace, sim_folder) sim = flopy.mf6.MFSimulation( sim_name=name, sim_ws=sim_ws, exe_name="mf6", ) - flopy.mf6.ModflowTdis( - sim, nper=nper, perioddata=per_mf6, time_units=time_units - ) + flopy.mf6.ModflowTdis(sim, nper=nper, perioddata=per_mf6, time_units=time_units) gwf = flopy.mf6.ModflowGwf(sim, modelname=name, save_flows=True) ims = flopy.mf6.ModflowIms( sim, @@ -207,9 +227,7 @@ def build_model(sim_folder): ) flopy.mf6.ModflowGwtic(gwt, strt=strt_conc) flopy.mf6.ModflowGwtadv(gwt, scheme="TVD") - flopy.mf6.ModflowGwtdsp( - gwt, xt3d_off=True, alh=alphal, ath1=alphat, diffc=diffc - ) + flopy.mf6.ModflowGwtdsp(gwt, xt3d_off=True, alh=alphal, ath1=alphat, diffc=diffc) flopy.mf6.ModflowGwtssm(gwt, sources=[[]]) flopy.mf6.ModflowGwtcnc( gwt, @@ -219,9 +237,7 @@ def build_model(sim_folder): gwt, budget_filerecord=f"{gwt.name}.cbc", concentration_filerecord=f"{gwt.name}.ucn", - concentrationprintrecord=[ - ("COLUMNS", 10, "WIDTH", 15, "DIGITS", 6, "GENERAL") - ], + concentrationprintrecord=[("COLUMNS", 10, "WIDTH", 15, "DIGITS", 6, "GENERAL")], saverecord=[("CONCENTRATION", "LAST")], printrecord=[("CONCENTRATION", "LAST"), ("BUDGET", "LAST")], ) @@ -231,107 +247,95 @@ def build_model(sim_folder): return sim -# Function to write model files - +def write_models(sim, silent=True): + sim.write_simulation(silent=silent) -def write_model(sim, silent=True): - if config.writeModel: - sim.write_simulation(silent=silent) - return +@timed +def run_models(sim, silent=True): + success, buff = sim.run_simulation(silent=silent, report=True) + assert success, pformat(buff) -# Function to run the model -# True is returned if the model runs successfully +# - -@config.timeit -def run_model(sim, silent=True): - print("Running model...") - success = True - if config.runModel: - success = False - success, buff = sim.run_simulation(silent=silent) - if not success: - print(buff) - return success - +# ### Plotting results +# +# Define functions to plot model results. -# Function to plot the model results +# + +# Figure properties +figure_size = (6, 8) def plot_conc(sim, idx): - fs = USGSFigure(figure_type="map", verbose=False) - sim_name = example_name - sim_ws = os.path.join(ws, sim_name) - gwf = sim.get_model("flow") - gwt = sim.get_model("trans") - - # create MODFLOW 6 head object - cobj = gwt.output.concentration() - times = cobj.get_times() - times = np.array(times) - - time_in_pub = 284349600.0 - idx_conc = (np.abs(times - time_in_pub)).argmin() - time_this_plot = times[idx_conc] - conc = cobj.get_data(totim=time_this_plot) - - zconc = np.zeros(nlay) - zbotm = np.zeros(nlay) - for i in range(len(zconc)): - zconc[i] = conc[i][0][0] - if i != (nlay - 1): - zbotm[i + 1] = -(60 - botm[i]) - - # Analytical solution - Stallman analysis - tau = 365 * 86400 - # t = 283824000.0 - t = 284349600.0 - c_w = 4174 - rho_w = 1000 - c_r = 800 - rho_r = bulk_dens - c_rho = c_r * rho_r * (1 - porosity) + c_w * rho_w * porosity - darcy_flux = 5.00e-07 - ko = 1.503 - zanal = Stallman( - T_az, dT, tau, t, c_rho, darcy_flux, ko, c_w, rho_w, zbotm, nlay - ) - - # make conc figure - fig = plt.figure(figsize=(6, 4)) - ax = fig.add_subplot(1, 1, 1) - - # configure plot and save - ax.plot(zconc, zbotm, "bo", mfc="none", label="MODFLOW6-GWT") - ax.plot( - zanal[:, 1], - zanal[:, 0], - "k--", - linewidth=1.0, - label="Analytical solution", - ) - ax.set_xlim(T_az - dT, T_az + dT) - ax.set_ylim(-top, 0) - ax.set_ylabel("Depth (m)") - ax.set_xlabel("Temperature (deg C)") - ax.legend() - - # save figure - if config.plotSave: - fpth = os.path.join( - "..", "figures", f"{sim_name}-conc{config.figure_ext}" + with styles.USGSMap() as fs: + sim_name = example_name + sim_ws = os.path.join(workspace, sim_name) + gwf = sim.get_model("flow") + gwt = sim.get_model("trans") + + # create MODFLOW 6 head object + cobj = gwt.output.concentration() + times = cobj.get_times() + times = np.array(times) + + time_in_pub = 284349600.0 + idx_conc = (np.abs(times - time_in_pub)).argmin() + time_this_plot = times[idx_conc] + conc = cobj.get_data(totim=time_this_plot) + + zconc = np.zeros(nlay) + zbotm = np.zeros(nlay) + for i in range(len(zconc)): + zconc[i] = conc[i][0][0] + if i != (nlay - 1): + zbotm[i + 1] = -(60 - botm[i]) + + # Analytical solution - Stallman analysis + tau = 365 * 86400 + # t = 283824000.0 + t = 284349600.0 + c_w = 4174 + rho_w = 1000 + c_r = 800 + rho_r = bulk_dens + c_rho = c_r * rho_r * (1 - porosity) + c_w * rho_w * porosity + darcy_flux = 5.00e-07 + ko = 1.503 + zanal = Stallman( + T_az, dT, tau, t, c_rho, darcy_flux, ko, c_w, rho_w, zbotm, nlay ) - fig.savefig(fpth) - return + # make conc figure + fig = plt.figure(figsize=(6, 4)) + ax = fig.add_subplot(1, 1, 1) + + # configure plot and save + ax.plot(zconc, zbotm, "bo", mfc="none", label="MODFLOW6-GWT") + ax.plot( + zanal[:, 1], + zanal[:, 0], + "k--", + linewidth=1.0, + label="Analytical solution", + ) + ax.set_xlim(T_az - dT, T_az + dT) + ax.set_ylim(-top, 0) + ax.set_ylabel("Depth (m)") + ax.set_xlabel("Temperature (deg C)") + ax.legend() -# Function to make animation + if plot_show: + plt.show() + if plot_save: + fpth = os.path.join("..", "figures", f"{sim_name}-conc.png") + fig.savefig(fpth) def make_animated_gif(sim, idx): sim_name = example_name - sim_ws = os.path.join(ws, sim_name) + sim_ws = os.path.join(workspace, sim_name) gwf = sim.get_model("flow") gwt = sim.get_model("trans") @@ -357,9 +361,7 @@ def make_animated_gif(sim, idx): c_rho = c_r * rho_r * (1 - porosity) + c_w * rho_w * porosity darcy_flux = 5.00e-07 ko = 1.503 - zanal = Stallman( - T_az, dT, tau, t, c_rho, darcy_flux, ko, c_w, rho_w, zbotm, nlay - ) + zanal = Stallman(T_az, dT, tau, t, c_rho, darcy_flux, ko, c_w, rho_w, zbotm, nlay) fig, ax = plt.subplots(figsize=(6, 4)) ax.set_ylabel("Depth (m)") @@ -389,46 +391,31 @@ def update(j): ani = animation.FuncAnimation(fig, update, times.shape[0], init_func=init) fpth = os.path.join("..", "figures", "{}{}".format(sim_name, ".gif")) ani.save(fpth, fps=50) - return def plot_results(sim, idx): - print("Plotting results...") - if config.plotModel: - plot_conc(sim, idx) - if config.plotSave and config.createGif: - print("Making animation...") - make_animated_gif(sim, idx) - return + plot_conc(sim, idx) + if plot_save and gif_save: + make_animated_gif(sim, idx) -# Function that wraps all of the steps for each scenario -# -# 1. build_model, -# 2. write_model, -# 3. run_model, and -# 4. plot_results. +# - + +# ### Running the example # +# Define and invoke a function to run the example scenario, then plot results. +# + def scenario(idx, silent=True): - sim = build_model(example_name) - write_model(sim, silent=silent) - success = run_model(sim, silent=silent) - if success: + sim = build_models(example_name) + if write: + write_models(sim, silent=silent) + if run: + run_models(sim, silent=silent) + if plot: plot_results(sim, idx) -# nosetest - exclude block from this nosetest to the next nosetest -def test_01(): - scenario(0, silent=False) - - -# nosetest end - -if __name__ == "__main__": - # ### Salt Lake Problem - - # Plot showing MODFLOW 6 results - - scenario(0) +scenario(0) +# - diff --git a/scripts/ex-gwt-synthetic-valley.py b/scripts/ex-gwt-synthetic-valley.py index 338a1845d..2a924ef5d 100644 --- a/scripts/ex-gwt-synthetic-valley.py +++ b/scripts/ex-gwt-synthetic-valley.py @@ -1,16 +1,15 @@ # ## Synthetic Valley Problem # # This problem is described in Hughes and others (2023). -# -# -# ### Synthetic Valley Problem +# ### Initial setup # -# Imports +# Import dependencies, define the example name and workspace, read settings from environment variables, and define some general utilities. +# + import os import pathlib as pl -import sys +from pprint import pformat import flopy import flopy.plot.styles as styles @@ -18,102 +17,126 @@ import matplotlib.pyplot as plt import matplotlib.ticker as mticker import numpy as np -from flopy.discretization import StructuredGrid, VertexGrid +import pooch +import shapely +from flopy.discretization import VertexGrid from flopy.utils.triangle import Triangle from flopy.utils.voronoi import VoronoiGrid from matplotlib import colors +from modflow_devtools.misc import get_env, timed from shapely.geometry import LineString, Polygon -# Append to system path to include the common subdirectory - -sys.path.append(os.path.join("..", "common")) - -# Import common functionality +# Settings from environment variables +write = get_env("WRITE", True) +run = get_env("RUN", True) +plot = get_env("PLOT", True) +plot_show = get_env("PLOT_SHOW", True) +plot_save = get_env("PLOT_SAVE", True) + +# Groundwater 2023 utilities + +geometries = { + "sv_boundary": """0.0 0.0 + 0.0 20000.0 + 12500.0 20000.0 + 12500.0 0.0""", + "sv_river": """4250.0 8750.0 + 4250.0 0.0""", + "sv_river_box": """3500.0 0.0 + 3500.0 9500.0 + 5000.0 9500.0 + 5000.0 0.0""", + "sv_wells": """7250. 17250. + 7750. 2750. + 2750 3750.""", + "sv_lake": """1500. 18500. + 3500. 18500. + 3500. 15500. + 4000. 15500. + 4000. 14500. + 4500. 14500. + 4500. 12000. + 2500. 12000. + 2500. 12500. + 2000. 12500. + 2000. 14000. + 1500. 14000. + 1500. 15000. + 1000. 15000. + 1000. 18000. + 1500. 18000.""", +} -import config -from groundwater2023_utils import ( - circle_function, - densify_geometry, - geometries, - string2geom, -) -# Set figure properties specific to the example +def string2geom(geostring, conversion=None): + if conversion is None: + multiplier = 1.0 + else: + multiplier = float(conversion) + res = [] + for line in geostring.split("\n"): + line = line.strip() + line = line.split(" ") + x = float(line[0]) * multiplier + y = float(line[1]) * multiplier + res.append((x, y)) + return res + + +def densify_geometry(line, step, keep_internal_nodes=True): + xy = [] # list of tuple of coordinates + lines_strings = [] + if keep_internal_nodes: + for idx in range(1, len(line)): + lines_strings.append(shapely.geometry.LineString(line[idx - 1 : idx + 1])) + else: + lines_strings = [shapely.geometry.LineString(line)] -two_panel_figsize = (17.15 / 2.541, 0.8333 * 17.15 / 2.541) -one_panel_figsize = (8.25 / 2.541, 13.25 / 2.541) -six_panel_figsize = (17.15 / 2.541, 1.4 * 0.8333 * 17.15 / 2.541) + for line_string in lines_strings: + length_m = line_string.length # get the length + for distance in np.arange(0, length_m + step, step): + point = line_string.interpolate(distance) + xy_tuple = (point.x, point.y) + if xy_tuple not in xy: + xy.append(xy_tuple) + # make sure the end point is in xy + if keep_internal_nodes: + xy_tuple = line_string.coords[-1] + if xy_tuple not in xy: + xy.append(xy_tuple) -# set figure element defaults + return xy -levels = np.arange(10, 110, 10) -contour_color = "black" -contour_style = "--" -sv_contour_dict = { - "linewidths": 0.5, - "colors": contour_color, - "linestyles": contour_style, -} -sv_contour_dict = { - "linewidths": 0.5, - "colors": contour_color, - "linestyles": contour_style, -} -sv_gwt_contour_dict = { - "linewidths": 0.75, - "colors": contour_color, - "linestyles": contour_style, -} -contour_label_dict = { - "linewidth": 0.5, - "color": contour_color, - "linestyle": contour_style, -} -contour_gwt_label_dict = { - "linewidth": 0.75, - "color": contour_color, - "linestyle": contour_style, -} -clabel_dict = { - "inline": True, - "fmt": "%1.0f", - "fontsize": 6, - "inline_spacing": 0.5, -} -font_dict = {"fontsize": 5, "color": "black"} -grid_dict = {"lw": 0.25, "color": "0.5"} -arrowprops = dict( - arrowstyle="-", - edgecolor="red", - lw=0.5, - shrinkA=0.15, - shrinkB=0.15, -) -river_dict = {"color": "blue", "linestyle": "-", "linewidth": 1} -lake_cmap = colors.ListedColormap(["cyan"]) -clay_cmap = colors.ListedColormap(["brown"]) +def circle_function(center=(0, 0), radius=1.0, dtheta=10.0): + angles = np.arange(0.0, 360.0, dtheta) * np.pi / 180.0 + xpts = center[0] + np.cos(angles) * radius + ypts = center[1] + np.sin(angles) * radius + return np.array([(x, y) for x, y in zip(xpts, ypts)]) -# Base simulation and model name and workspace -ws = config.base_ws +# Example name and base workspace example_name = "ex-gwt-synthetic-valley" +workspace = pl.Path("../examples") # Conversion factors - ft2m = 1.0 / 3.28081 ft3tom3 = 1.0 * ft2m * ft2m * ft2m ftpd2cmpy = 1000.0 * 365.25 * ft2m mpd2cmpy = 100.0 * 365.25 mpd2inpy = 12.0 * 365.25 * 3.28081 +# - -# Model units +# ### Model setup +# +# Define functions to build models, write input files, and run the simulation. +# + +# Model units length_units = "meters" time_units = "days" -# Table of model parameters - +# Model parameters pertim = 10957.5 # Simulation length ($d$) ntransport_steps = 60 # Number of transport time steps nlay = 6 # Number of layers @@ -135,9 +158,10 @@ porosity = 0.2 # Aquifer porosity (unitless) confining_porosity = 0.4 # Confining unit porosity (unitless) -# build voronoi grid +# - # + +# voronoi grid properties maximum_area = 150.0 * 150.0 well_dv = 300.0 boundary_refinement = 100.0 @@ -207,12 +231,31 @@ # + # load raster data files -raster_path = pl.Path(config.data_ws) / example_name -kaq = flopy.utils.Raster.load(raster_path / "k_aq_SI.tif") -kclay = flopy.utils.Raster.load(raster_path / "k_clay_SI.tif") -top_base = flopy.utils.Raster.load(raster_path / "top_SI.tif") -bot = flopy.utils.Raster.load(raster_path / "bottom_SI.tif") -lake_location = flopy.utils.Raster.load(raster_path / "lake_location_SI.tif") +fname = pooch.retrieve( + url=f"https://github.com/MODFLOW-USGS/modflow6-examples/raw/master/data/{example_name}/k_aq_SI.tif", + known_hash="md5:d233e5c393ab6c029c63860d73818856", +) +kaq = flopy.utils.Raster.load(fname) +fname = pooch.retrieve( + url=f"https://github.com/MODFLOW-USGS/modflow6-examples/raw/master/data/{example_name}/k_clay_SI.tif", + known_hash="md5:a08999c37f42b35884468e4ef896d5f9", +) +kclay = flopy.utils.Raster.load(fname) +fname = pooch.retrieve( + url=f"https://github.com/MODFLOW-USGS/modflow6-examples/raw/master/data/{example_name}/top_SI.tif", + known_hash="md5:781155bdcc2b9914e1cad6b10de0e9c7", +) +top_base = flopy.utils.Raster.load(fname) +fname = pooch.retrieve( + url=f"https://github.com/MODFLOW-USGS/modflow6-examples/raw/master/data/{example_name}/bottom_SI.tif", + known_hash="md5:00b4a39fbf5180e65c0367cdb6f15c93", +) +bot = flopy.utils.Raster.load(fname) +fname = pooch.retrieve( + url=f"https://github.com/MODFLOW-USGS/modflow6-examples/raw/master/data/{example_name}/lake_location_SI.tif", + known_hash="md5:38600d6f0eef7c033ede278252dc6343", +) +lake_location = flopy.utils.Raster.load(fname) # - # + @@ -224,7 +267,6 @@ top_levels = np.arange(0, 25, 5) head_range = (-1, 5) head_levels = np.arange(1, head_range[1] + 1, 1) - extent = voronoi_grid.extent # - @@ -313,9 +355,7 @@ cum_dist = np.zeros(sfr_nodes.shape, dtype=float) cum_dist[0] = 0.5 * sfr_lengths[0] for idx in range(1, sfr_nodes.shape[0]): - cum_dist[idx] = cum_dist[idx - 1] + 0.5 * ( - sfr_lengths[idx - 1] + sfr_lengths[idx] - ) + cum_dist[idx] = cum_dist[idx - 1] + 0.5 * (sfr_lengths[idx - 1] + sfr_lengths[idx]) sfr_bot = b0 + sfr_slope * cum_dist sfr_conn = [] for idx, node in enumerate(sfr_nodes): @@ -328,9 +368,7 @@ # sfrpak_data = [] -for idx, (cellid, rlen, rtp) in enumerate( - zip(gwf_nodes, sfr_lengths, sfr_bot) -): +for idx, (cellid, rlen, rtp) in enumerate(zip(gwf_nodes, sfr_lengths, sfr_bot)): sfr_plt_array[cellid] = 1 sfrpak_data.append( ( @@ -409,27 +447,23 @@ ] # - - -# ### Functions to build, write, run, and plot models -# -# MODFLOW 6 flopy simulation object (sim) is returned if building the model -# recharge is the only variable +# ### Model setup # +# Define functions to build models, write input files, and run the simulation. +# + def build_mf6gwf(sim_folder): print(f"Building mf6gwf model...{sim_folder}") name = "flow" - sim_ws = os.path.join(ws, sim_folder, "mf6gwf") + sim_ws = os.path.join(workspace, sim_folder, "mf6gwf") sim = flopy.mf6.MFSimulation( sim_name=name, sim_ws=sim_ws, exe_name="mf6", continue_=True, ) - tdis = flopy.mf6.ModflowTdis( - sim, time_units="days", perioddata=((pertim, 1, 1.0),) - ) + tdis = flopy.mf6.ModflowTdis(sim, time_units="days", perioddata=((pertim, 1, 1.0),)) ims = flopy.mf6.ModflowIms( sim, print_option="all", @@ -472,12 +506,8 @@ def build_mf6gwf(sim_folder): ], ) rch = flopy.mf6.ModflowGwfrcha(gwf, recharge=rainfall) - evt = flopy.mf6.ModflowGwfevta( - gwf, surface=top_vg, rate=evaporation, depth=1.0 - ) - wel = flopy.mf6.ModflowGwfwel( - gwf, stress_period_data=welspd, boundnames=True - ) + evt = flopy.mf6.ModflowGwfevta(gwf, surface=top_vg, rate=evaporation, depth=1.0) + wel = flopy.mf6.ModflowGwfwel(gwf, stress_period_data=welspd, boundnames=True) drn = flopy.mf6.ModflowGwfdrn( gwf, auxiliary=["depth"], @@ -525,7 +555,7 @@ def build_mf6gwf(sim_folder): def build_mf6gwt(sim_folder): print(f"Building mf6gwt model...{sim_folder}") name = "trans" - sim_ws = os.path.join(ws, sim_folder, "mf6gwt") + sim_ws = os.path.join(workspace, sim_folder, "mf6gwt") sim = flopy.mf6.MFSimulation( sim_name=name, sim_ws=sim_ws, @@ -600,50 +630,88 @@ def build_mf6gwt(sim_folder): return sim -def build_model(sim_name): - sims = None - if config.buildModel: - sim_mf6gwf = build_mf6gwf(sim_name) - sim_mf6gwt = build_mf6gwt(sim_name) - sim_mf2005 = None # build_mf2005(sim_name) - sim_mt3dms = None # build_mt3dms(sim_name, sim_mf2005) - sims = (sim_mf6gwf, sim_mf6gwt, sim_mf2005, sim_mt3dms) - return sims +def build_models(sim_name): + sim_mf6gwf = build_mf6gwf(sim_name) + sim_mf6gwt = build_mf6gwt(sim_name) + sim_mf2005 = None # build_mf2005(sim_name) + sim_mt3dms = None # build_mt3dms(sim_name, sim_mf2005) + return sim_mf6gwf, sim_mf6gwt, sim_mf2005, sim_mt3dms -# Function to write model files +def write_models(sims, silent=True): + sim_mf6gwf, sim_mf6gwt, sim_mf2005, sim_mt3dms = sims + sim_mf6gwf.write_simulation(silent=silent) + sim_mf6gwt.write_simulation(silent=silent) -def write_model(sims, silent=True): - if config.writeModel: - sim_mf6gwf, sim_mf6gwt, sim_mf2005, sim_mt3dms = sims - sim_mf6gwf.write_simulation(silent=silent) - sim_mf6gwt.write_simulation(silent=silent) - return +@timed +def run_models(sims, silent=True): + sim_mf6gwf, sim_mf6gwt, sim_mf2005, sim_mt3dms = sims + success, buff = sim_mf6gwf.run_simulation(silent=silent, report=True) + assert success, pformat(buff) + success, buff = sim_mf6gwt.run_simulation(silent=silent, report=True) + assert success, pformat(buff) -# Function to run the model -# True is returned if the model runs successfully +# - +# ### Plotting results +# +# Define functions to plot model results. -@config.timeit -def run_model(sims, silent=True): - success = True - if config.runModel: - success = False - sim_mf6gwf, sim_mf6gwt, sim_mf2005, sim_mt3dms = sims - print("Running mf6gwf model...") - success, buff = sim_mf6gwf.run_simulation(silent=silent) - if not success: - print(buff) - print("Running mf6gwt model...") - success, buff = sim_mf6gwt.run_simulation(silent=silent) - if not success: - print(buff) - return success +# + +# Figure properties +two_panel_figsize = (17.15 / 2.541, 0.8333 * 17.15 / 2.541) +one_panel_figsize = (8.25 / 2.541, 13.25 / 2.541) +six_panel_figsize = (17.15 / 2.541, 1.4 * 0.8333 * 17.15 / 2.541) +levels = np.arange(10, 110, 10) +contour_color = "black" +contour_style = "--" +sv_contour_dict = { + "linewidths": 0.5, + "colors": contour_color, + "linestyles": contour_style, +} +sv_contour_dict = { + "linewidths": 0.5, + "colors": contour_color, + "linestyles": contour_style, +} +sv_gwt_contour_dict = { + "linewidths": 0.75, + "colors": contour_color, + "linestyles": contour_style, +} +contour_label_dict = { + "linewidth": 0.5, + "color": contour_color, + "linestyle": contour_style, +} +contour_gwt_label_dict = { + "linewidth": 0.75, + "color": contour_color, + "linestyle": contour_style, +} +clabel_dict = { + "inline": True, + "fmt": "%1.0f", + "fontsize": 6, + "inline_spacing": 0.5, +} +font_dict = {"fontsize": 5, "color": "black"} +grid_dict = {"lw": 0.25, "color": "0.5"} +arrowprops = dict( + arrowstyle="-", + edgecolor="red", + lw=0.5, + shrinkA=0.15, + shrinkB=0.15, +) +river_dict = {"color": "blue", "linestyle": "-", "linewidth": 1} +lake_cmap = colors.ListedColormap(["cyan"]) +clay_cmap = colors.ListedColormap(["brown"]) -# Functions to plot the model results def plot_wells(ax=None, ms=None): if ax is None: ax = plt.gca() @@ -711,7 +779,6 @@ def set_ticklabels( ax.set_xlabel("x position (km)") if not skip_ylabel: ax.set_ylabel("y position (km)") - return def plot_well_labels(ax): @@ -725,7 +792,6 @@ def plot_well_labels(ax): textcoords="offset points", arrowprops=arrowprops, ) - return def plot_feature_labels(ax): @@ -751,16 +817,13 @@ def plot_feature_labels(ax): rotation=90, ) plot_well_labels(ax) - return def plot_results(sims, idx): - if config.plotModel: - print("Plotting model results...") - plot_river_mapping(sims, idx) - plot_head_results(sims, idx) - plot_conc_results(sims, idx) - return + print("Plotting model results...") + plot_river_mapping(sims, idx) + plot_head_results(sims, idx) + plot_conc_results(sims) def plot_river_mapping(sims, idx): @@ -853,11 +916,12 @@ def plot_river_mapping(sims, idx): handletextpad=0.3, ) - # save figure - if config.plotSave: + if plot_show: + plt.show() + if plot_save: sim_folder = os.path.basename(os.path.split(sim_ws)[0]) - fname = f"{sim_folder}-river-discretization{config.figure_ext}" - fig.savefig(os.path.join(ws, "..", "figures", fname)) + fname = f"{sim_folder}-river-discretization.png" + fig.savefig(os.path.join(workspace, "..", "figures", fname)) def plot_head_results(sims, idx): @@ -1032,14 +1096,15 @@ def plot_head_results(sims, idx): handletextpad=0.3, ) - # save figure - if config.plotSave: + if plot_show: + plt.show() + if plot_save: sim_folder = os.path.basename(os.path.split(sim_ws)[0]) - fname = f"{sim_folder}-head{config.figure_ext}" - fig.savefig(os.path.join(ws, "..", "figures", fname)) + fname = f"{sim_folder}-head.png" + fig.savefig(os.path.join(workspace, "..", "figures", fname)) -def plot_conc_results(sims, idx): +def plot_conc_results(sims): print("Plotting gwt model results...") _, sim_mf6gwt, _, _ = sims sim_ws = sim_mf6gwt.simulation_data.mfpath.get_sim_path() @@ -1130,9 +1195,7 @@ def plot_conc_results(sims, idx): ) ax.axhline(xy0[0], color="cyan", lw=0.5, label="Lake") ax.axhline(xy0[0], **river_dict, label="River") - ax.axhline( - xy0[0], **contour_gwt_label_dict, label="Concentration (mg/L)" - ) + ax.axhline(xy0[0], **contour_gwt_label_dict, label="Concentration (mg/L)") ax.plot( xy0, xy0, @@ -1154,40 +1217,32 @@ def plot_conc_results(sims, idx): frameon=False, ) - # save figure - if config.plotSave: + if plot_show: + plt.show() + if plot_save: sim_folder = os.path.split(sim_ws)[0] sim_folder = os.path.basename(sim_folder) - fname = f"{sim_folder}-conc{config.figure_ext}" - fig.savefig(os.path.join(ws, "..", "figures", fname)) + fname = f"{sim_folder}-conc.png" + fig.savefig(os.path.join(workspace, "..", "figures", fname)) -# Function that wraps all of the steps for each scenario +# - + +# ### Running the example # -# 1. build_model, -# 2. write_model, -# 3. run_model, and -# 4. plot_results. +# Define and invoke a function to run the example scenario, then plot results. +# + def scenario(idx, silent=True): - sim = build_model(example_name) - write_model(sim, silent=silent) - success = run_model(sim, silent=silent) - if success: + sim = build_models(example_name) + if write: + write_models(sim, silent=silent) + if run: + run_models(sim, silent=silent) + if plot: plot_results(sim, idx) -# nosetest - exclude block from this nosetest to the next nosetest -def test_01(): - scenario(0, silent=False) - - -# nosetest end - -if __name__ == "__main__": - # ### Simulate Synthetic Valley Problem (Hughes and others 2023) - - # Plot showing MODFLOW 6 results - - scenario(0) +scenario(0) +# - diff --git a/scripts/ex-gwt-uzt-2d.py b/scripts/ex-gwt-uzt-2d.py index e0f499888..717ffbe0a 100644 --- a/scripts/ex-gwt-uzt-2d.py +++ b/scripts/ex-gwt-uzt-2d.py @@ -1,40 +1,41 @@ # ## Two-Dimensional Transport with the UZF and UZT packages active. # -# The purpose of this script is to test transport in the unsaturated zone -# package. This test originally appeared in Morway et al. (2013). +# This problem tests transport in the unsaturated zone. +# This example originally appeared in Morway et al (2013). -# ### MODFLOW 6 2D UZT/GWT Problem Setup - -# Append to system path to include the common subdirectory +# ### Initial setup +# +# Import dependencies, define the example name and workspace, and read settings from environment variables. +# + import os -import sys - -sys.path.append(os.path.join("..", "common")) +import pathlib as pl +from pprint import pformat -# Imports - -import config import flopy import matplotlib.pyplot as plt import numpy as np -from figspecs import USGSFigure - -mf6exe = "mf6" -exe_name_mf = "mfnwt" -exe_name_mt = "mt3dusgs" - -# Set figure properties specific to this problem +from flopy.plot.styles import styles +from modflow_devtools.misc import get_env, timed -figure_size = (6, 4) +# Example name and base workspace +workspace = pl.Path("../examples") -# Base simulation and model name and workspace +# Settings from environment variables +write = get_env("WRITE", True) +run = get_env("RUN", True) +plot = get_env("PLOT", True) +plot_show = get_env("PLOT_SHOW", True) +plot_save = get_env("PLOT_SAVE", True) +# - -ws = config.base_ws +# ### Define parameters +# +# Define model units, parameters and other settings. +# + # Set scenario parameters (make sure there is at least one blank line before next item) -# This entire dictionary is passed to _build_model()_ using the kwargs argument - +# This entire dictionary is passed to _build_models()_ using the kwargs argument parameters = { "ex-gwt-uzt-2d-a": { "longitudinal_dispersivity": [0.5], @@ -117,7 +118,6 @@ # # add parameter_units to add units to the scenario parameter table that is automatically # built and used by the .tex input - parameter_units = { "longitudinal_dispersivity": "$m$", "ratio_horizontal_to_longitudinal_dispersivity": "unitless", @@ -125,12 +125,10 @@ } # Model units - length_units = "meter" time_units = "days" -# Table MODFLOW 6 GWT MT3DMS Example 8 - +# Model parameters nlay = 20 # Number of layers nrow = 1 # Number of rows ncol = 40 # Number of columns @@ -257,13 +255,11 @@ nuzfcells = len(packagedata) uzf_perioddata = {0: pd0} - # Transport related mixelm = -1 # -1: TVD; 0: FD dmcoef = 0.0 # ft^2/day # Solver settings - nouter, ninner = 100, 300 hclose, rclose, relax = 1e-6, 1e-6, 1.0 percel = 1.0 @@ -278,13 +274,15 @@ dchmoc = 1.0e-3 nlsink = nplane npsink = nph +# - -# ### Functions to build, write, and run models and plot MT3DMS Example 9 +# ### Model setup # -# MODFLOW 6 flopy simulation object (sim) is returned if building the model +# Define functions to build models, write input files, and run the simulation. -def build_model( +# + +def build_models( sim_name, mixelm=0, longitudinal_dispersivity=[0.5], @@ -292,578 +290,564 @@ def build_model( ratio_vertical_to_longitudinal_dispersivity=[0.4], silent=False, ): - if config.buildModel: - print(f"Building mf-nwt model...{sim_name}") - model_ws = os.path.join(ws, sim_name, "mt3d") - modelname_mf = "uzt-2d-mf" - - # Instantiate the MODFLOW model - mf = flopy.modflow.Modflow( - modelname=modelname_mf, - model_ws=model_ws, - exe_name=exe_name_mf, - version="mfnwt", - ) - - # Instantiate discretization package - # units: itmuni=4 (days), lenuni=2 (m) - flopy.modflow.ModflowDis( - mf, - nlay=nlay, - nrow=nrow, - ncol=ncol, - delr=delr, - delc=delc, - top=top, - nper=nper, - botm=botm, - perlen=perlen, - nstp=nstp, - steady=[False], - itmuni=4, - lenuni=2, - ) - - # Instantiate basic package - flopy.modflow.ModflowBas(mf, ibound=ibound_mf2k5, strt=strt) - - # Instantiate layer property flow package - flopy.modflow.ModflowUpw( - mf, hk=k1, vka=vk, sy=sy, ss=Ss, laytyp=laytyp - ) - - # Instantiate unsaturated-zone flow package - flopy.modflow.ModflowUzf1( - mf, - nuztop=nuztop, - iuzfopt=iuzfopt, - irunflg=irunflg, - ietflg=ietflg, - ipakcb=iuzfcb1, - iuzfcb2=iuzfcb2, - ntrail2=ntrail2, - nsets=nsets2, - surfdep=0.1, - iuzfbnd=iuzfbnd, - eps=eps, - thts=thts, - thti=thti, - thtr=thtr, - finf=finf_mfnwt, - specifythti=True, - specifythtr=True, - ) - - # Instantiate output control (OC) package - flopy.modflow.ModflowOc( - mf, - # stress_period_data={(0, nstp[0] - 1): ["save head", "save budget"]} - stress_period_data={ - (0, nstp[0] - 1): ["save head", "save budget"] - }, - ) - - # Instantiate solver package - flopy.modflow.ModflowNwt(mf) - - # Instantiate link mass transport package (for writing linker file) - flopy.modflow.ModflowLmt(mf, package_flows=["UZF"]) - - # Transport - print(f"Building mt3d-usgs model...{sim_name}") - - modelname_mt = "uzt-2d-mt" - mt = flopy.mt3d.Mt3dms( - modelname=modelname_mt, - model_ws=model_ws, - exe_name=exe_name_mt, - modflowmodel=mf, - version="mt3d-usgs", - ) - - # Instantiate basic transport package - flopy.mt3d.Mt3dBtn( - mt, - icbund=1, - prsity=prsity, - sconc=sconc, - nper=nper, - perlen=perlen, - timprs=np.arange(1, 121), - dt0=0.05, - ) - - # Instatiate the advection package - flopy.mt3d.Mt3dAdv( - mt, - mixelm=mixelm, - dceps=dceps, - nplane=nplane, - npl=npl, - nph=nph, - npmin=npmin, - npmax=npmax, - nlsink=nlsink, - npsink=npsink, - percel=percel, - itrack=itrack, - wd=wd, - ) - - # Instantiate the dispersion package - if len(longitudinal_dispersivity) > 1: - disp = np.ones_like(ibound_mf2k5) - trpt = [] - trpv = [] - for i, (dispx, ratio1, ratio2) in enumerate( - zip( - longitudinal_dispersivity, - ratio_horizontal_to_longitudinal_dispersivity, - ratio_vertical_to_longitudinal_dispersivity, - ) - ): - disp[i, :, :] = dispx - trpt.append(ratio1) - trpv.append(ratio2) - trpt = np.array(trpt) - trpv = np.array(trpv) - else: - # Dispersion - disp = longitudinal_dispersivity[0] - trpt = ratio_horizontal_to_longitudinal_dispersivity[0] - trpv = ratio_vertical_to_longitudinal_dispersivity[0] - flopy.mt3d.Mt3dDsp(mt, al=disp, trpt=trpt, trpv=trpv, dmcoef=dmcoef) - - # Instantiate source/sink mixing package; -1 below indicates constant - # concentration boundary condition (the form of this input is specific - # to MT3DMS and doesn't carry over to MF6) - cnc0_left = [(k, 0, 0, 0.0, 1) for k in range(0, nlay)] - cnc0_right = [(k, 0, ncol - 1, 0.0, 1) for k in range(0, nlay)] - cnc0 = cnc0_left + cnc0_right - ssmspd = {0: cnc0} - mxss = len(cnc0) - flopy.mt3d.Mt3dSsm(mt, mxss=mxss, stress_period_data=ssmspd) - - # Instantiate unsaturated zone tranport package - cuzinf = np.zeros((nrow, ncol), dtype=int) - cuzinf[0, 15:25] = 1.0 - uzt = flopy.mt3d.Mt3dUzt(mt, iuzfbnd=iuzfbnd, iet=0, cuzinf=cuzinf) - - # Instantiate the GCG solver in MT3DMS - flopy.mt3d.Mt3dGcg(mt, iter1=2000, isolve=3, ncrs=1) - - # MODFLOW 6 - print(f"Building mf6gwt model...{sim_name}") - - name = "uzt-2d-mf6" - gwfname = "gwf-" + name - sim_ws = os.path.join(ws, sim_name) - sim = flopy.mf6.MFSimulation( - sim_name=sim_name, sim_ws=sim_ws, exe_name=mf6exe - ) - - # How much output to write? - from flopy.mf6.mfbase import VerbosityLevel - - sim.simulation_data.verbosity_level = VerbosityLevel.quiet - sim.name_file.memory_print_option = "all" - - # Instantiating MODFLOW 6 time discretization - tdis_rc = [] - for i in range(nper): - tdis_rc.append((perlen[i], nstp[i], tsmult[i])) - flopy.mf6.ModflowTdis( - sim, nper=nper, perioddata=tdis_rc, time_units=time_units - ) - - # Instantiating MODFLOW 6 groundwater flow model - gwf = flopy.mf6.ModflowGwf( - sim, - modelname=gwfname, - save_flows=True, - newtonoptions="newton", - model_nam_file=f"{gwfname}.nam", - ) - - # Instantiating MODFLOW 6 solver for flow model - imsgwf = flopy.mf6.ModflowIms( - sim, - print_option="summary", - complexity="complex", - no_ptcrecord="all", - outer_dvclose=1.0e-4, - outer_maximum=2000, - under_relaxation="dbd", - linear_acceleration="BICGSTAB", - under_relaxation_theta=0.7, - under_relaxation_kappa=0.08, - under_relaxation_gamma=0.05, - under_relaxation_momentum=0.0, - backtracking_number=20, - backtracking_tolerance=2.0, - backtracking_reduction_factor=0.2, - backtracking_residual_limit=5.0e-4, - inner_dvclose=1.0e-5, - rcloserecord="0.0001 relative_rclose", - inner_maximum=100, - relaxation_factor=0.0, - number_orthogonalizations=2, - preconditioner_levels=8, - preconditioner_drop_tolerance=0.001, - filename=f"{gwfname}.ims", - ) - sim.register_ims_package(imsgwf, [gwf.name]) - - # Instantiating MODFLOW 6 discretization package - flopy.mf6.ModflowGwfdis( - gwf, - length_units=length_units, - nlay=nlay, - nrow=nrow, - ncol=ncol, - delr=delr, - delc=delc, - top=top, - botm=botm, - idomain=idomain, - filename=f"{gwfname}.dis", - ) - - # Instantiating MODFLOW 6 initial conditions package for flow model - flopy.mf6.ModflowGwfic(gwf, strt=strt, filename=f"{gwfname}.ic") - - # Instantiating MODFLOW 6 node-property flow package - flopy.mf6.ModflowGwfnpf( - gwf, - save_flows=False, - icelltype=laytyp, - k=hk, - k33=hk33, - save_specific_discharge=True, - filename=f"{gwfname}.npf", - ) - - # Instantiate storage package - flopy.mf6.ModflowGwfsto( - gwf, - ss=Ss, - sy=sy, - iconvert=iconvert, - filename=f"{gwfname}.sto", - ) - - # Instantiate constant head package - chdspd = [] - # Model domain is symmetric - for k in np.arange(nlay): - if botm[k] <= strt[k, 0, 0]: - # (l, r, c), head, conc - chdspd.append([(k, 0, 0), strt[k, 0, 0], 0.0]) - chdspd.append([(k, 0, ncol - 1), strt[k, 0, ncol - 1], 0.0]) - chdspd = {0: chdspd} - flopy.mf6.modflow.mfgwfchd.ModflowGwfchd( - gwf, - maxbound=len(chdspd), - stress_period_data=chdspd, - save_flows=False, - auxiliary="CONCENTRATION", - pname="CHD-1", - filename=f"{gwfname}.chd", - ) - - # Instantiate unsaturated zone flow package - flopy.mf6.ModflowGwfuzf( - gwf, - nuzfcells=nuzfcells, - ntrailwaves=15, - nwavesets=40, - print_flows=True, - packagedata=packagedata, - perioddata=uzf_perioddata, - pname="UZF-1", - budget_filerecord=f"{gwfname}.uzf.bud", - ) - - # Instantiate output control package - flopy.mf6.ModflowGwfoc( - gwf, - budget_filerecord=f"{gwfname}.bud", - head_filerecord=f"{gwfname}.hds", - headprintrecord=[ - ("COLUMNS", 10, "WIDTH", 15, "DIGITS", 6, "GENERAL") - ], - saverecord=[("HEAD", "ALL"), ("BUDGET", "ALL")], - printrecord=[("HEAD", "LAST"), ("BUDGET", "ALL")], - ) - - # Instantiating MODFLOW 6 groundwater transport package - gwtname = "gwt-" + name - gwt = flopy.mf6.MFModel( - sim, - model_type="gwt6", - modelname=gwtname, - model_nam_file=f"{gwtname}.nam", - ) - gwt.name_file.save_flows = True - - # create iterative model solution and register the gwt model with it - imsgwt = flopy.mf6.ModflowIms( - sim, - print_option="summary", - complexity="complex", - no_ptcrecord="all", - outer_dvclose=1.0e-4, - outer_maximum=2000, - under_relaxation="dbd", - linear_acceleration="BICGSTAB", - under_relaxation_theta=0.7, - under_relaxation_kappa=0.08, - under_relaxation_gamma=0.05, - under_relaxation_momentum=0.0, - backtracking_number=20, - backtracking_tolerance=2.0, - backtracking_reduction_factor=0.2, - backtracking_residual_limit=5.0e-4, - inner_dvclose=1.0e-5, - rcloserecord="0.0001 relative_rclose", - inner_maximum=100, - relaxation_factor=0.0, - number_orthogonalizations=2, - preconditioner_levels=8, - preconditioner_drop_tolerance=0.001, - filename=f"{gwtname}.ims", - ) - sim.register_ims_package(imsgwt, [gwt.name]) - - # Instantiating MODFLOW 6 transport discretization package - flopy.mf6.ModflowGwtdis( - gwt, - nlay=nlay, - nrow=nrow, - ncol=ncol, - delr=delr, - delc=delc, - top=top, - botm=botm, - idomain=idomain, - filename=f"{gwtname}.dis", - ) - - # Instantiating MODFLOW 6 transport initial concentrations - flopy.mf6.ModflowGwtic(gwt, strt=sconc, filename=f"{gwtname}.ic") - - # Instantiating MODFLOW 6 transport advection package - if mixelm >= 0: - scheme = "UPSTREAM" - elif mixelm == -1: - scheme = "TVD" - else: - raise Exception() - flopy.mf6.ModflowGwtadv(gwt, scheme=scheme, filename=f"{gwtname}.adv") - - # Instantiating MODFLOW 6 transport dispersion package - if len(longitudinal_dispersivity) > 1: - disp = np.ones_like(ibound_mf2k5) - ath1 = np.ones_like(ibound_mf2k5) - atv = np.ones_like(ibound_mf2k5) - for i, (dispx, ratio1, ratio2) in enumerate( - zip( - longitudinal_dispersivity, - ratio_horizontal_to_longitudinal_dispersivity, - ratio_vertical_to_longitudinal_dispersivity, - ) - ): - disp[i, :, :] = dispx - ath1[i, :, :] = dispx * ratio1 - atv[i, :, :] = dispx * ratio2 - else: - # Dispersion - disp = longitudinal_dispersivity[0] - ath1 = ( - longitudinal_dispersivity[0] - * ratio_horizontal_to_longitudinal_dispersivity[0] - ) - atv = ( - longitudinal_dispersivity[0] - * ratio_vertical_to_longitudinal_dispersivity[0] + print(f"Building mf-nwt model...{sim_name}") + model_ws = os.path.join(workspace, sim_name, "mt3d") + modelname_mf = "uzt-2d-mf" + + # Instantiate the MODFLOW model + mf = flopy.modflow.Modflow( + modelname=modelname_mf, + model_ws=model_ws, + exe_name="mfnwt", + version="mfnwt", + ) + + # Instantiate discretization package + # units: itmuni=4 (days), lenuni=2 (m) + flopy.modflow.ModflowDis( + mf, + nlay=nlay, + nrow=nrow, + ncol=ncol, + delr=delr, + delc=delc, + top=top, + nper=nper, + botm=botm, + perlen=perlen, + nstp=nstp, + steady=[False], + itmuni=4, + lenuni=2, + ) + + # Instantiate basic package + flopy.modflow.ModflowBas(mf, ibound=ibound_mf2k5, strt=strt) + + # Instantiate layer property flow package + flopy.modflow.ModflowUpw(mf, hk=k1, vka=vk, sy=sy, ss=Ss, laytyp=laytyp) + + # Instantiate unsaturated-zone flow package + flopy.modflow.ModflowUzf1( + mf, + nuztop=nuztop, + iuzfopt=iuzfopt, + irunflg=irunflg, + ietflg=ietflg, + ipakcb=iuzfcb1, + iuzfcb2=iuzfcb2, + ntrail2=ntrail2, + nsets=nsets2, + surfdep=0.1, + iuzfbnd=iuzfbnd, + eps=eps, + thts=thts, + thti=thti, + thtr=thtr, + finf=finf_mfnwt, + specifythti=True, + specifythtr=True, + ) + + # Instantiate output control (OC) package + flopy.modflow.ModflowOc( + mf, + # stress_period_data={(0, nstp[0] - 1): ["save head", "save budget"]} + stress_period_data={(0, nstp[0] - 1): ["save head", "save budget"]}, + ) + + # Instantiate solver package + flopy.modflow.ModflowNwt(mf) + + # Instantiate link mass transport package (for writing linker file) + flopy.modflow.ModflowLmt(mf, package_flows=["UZF"]) + + # Transport + print(f"Building mt3d-usgs model...{sim_name}") + + modelname_mt = "uzt-2d-mt" + mt = flopy.mt3d.Mt3dms( + modelname=modelname_mt, + model_ws=model_ws, + exe_name="mt3dusgs", + modflowmodel=mf, + version="mt3d-usgs", + ) + + # Instantiate basic transport package + flopy.mt3d.Mt3dBtn( + mt, + icbund=1, + prsity=prsity, + sconc=sconc, + nper=nper, + perlen=perlen, + timprs=np.arange(1, 121), + dt0=0.05, + ) + + # Instatiate the advection package + flopy.mt3d.Mt3dAdv( + mt, + mixelm=mixelm, + dceps=dceps, + nplane=nplane, + npl=npl, + nph=nph, + npmin=npmin, + npmax=npmax, + nlsink=nlsink, + npsink=npsink, + percel=percel, + itrack=itrack, + wd=wd, + ) + + # Instantiate the dispersion package + if len(longitudinal_dispersivity) > 1: + disp = np.ones_like(ibound_mf2k5) + trpt = [] + trpv = [] + for i, (dispx, ratio1, ratio2) in enumerate( + zip( + longitudinal_dispersivity, + ratio_horizontal_to_longitudinal_dispersivity, + ratio_vertical_to_longitudinal_dispersivity, ) - - if al != 0: - flopy.mf6.ModflowGwtdsp( - gwt, - alh=disp, - ath1=ath1, - atv=atv, - pname="DSP-1", - filename=f"{gwtname}.dsp", + ): + disp[i, :, :] = dispx + trpt.append(ratio1) + trpv.append(ratio2) + trpt = np.array(trpt) + trpv = np.array(trpv) + else: + # Dispersion + disp = longitudinal_dispersivity[0] + trpt = ratio_horizontal_to_longitudinal_dispersivity[0] + trpv = ratio_vertical_to_longitudinal_dispersivity[0] + flopy.mt3d.Mt3dDsp(mt, al=disp, trpt=trpt, trpv=trpv, dmcoef=dmcoef) + + # Instantiate source/sink mixing package; -1 below indicates constant + # concentration boundary condition (the form of this input is specific + # to MT3DMS and doesn't carry over to MF6) + cnc0_left = [(k, 0, 0, 0.0, 1) for k in range(0, nlay)] + cnc0_right = [(k, 0, ncol - 1, 0.0, 1) for k in range(0, nlay)] + cnc0 = cnc0_left + cnc0_right + ssmspd = {0: cnc0} + mxss = len(cnc0) + flopy.mt3d.Mt3dSsm(mt, mxss=mxss, stress_period_data=ssmspd) + + # Instantiate unsaturated zone tranport package + cuzinf = np.zeros((nrow, ncol), dtype=int) + cuzinf[0, 15:25] = 1.0 + uzt = flopy.mt3d.Mt3dUzt(mt, iuzfbnd=iuzfbnd, iet=0, cuzinf=cuzinf) + + # Instantiate the GCG solver in MT3DMS + flopy.mt3d.Mt3dGcg(mt, iter1=2000, isolve=3, ncrs=1) + + # MODFLOW 6 + print(f"Building mf6gwt model...{sim_name}") + + name = "uzt-2d-mf6" + gwfname = "gwf-" + name + sim_ws = os.path.join(workspace, sim_name) + sim = flopy.mf6.MFSimulation(sim_name=sim_name, sim_ws=sim_ws, exe_name="mf6") + + # How much output to write? + from flopy.mf6.mfbase import VerbosityLevel + + sim.simulation_data.verbosity_level = VerbosityLevel.quiet + sim.name_file.memory_print_option = "all" + + # Instantiating MODFLOW 6 time discretization + tdis_rc = [] + for i in range(nper): + tdis_rc.append((perlen[i], nstp[i], tsmult[i])) + flopy.mf6.ModflowTdis(sim, nper=nper, perioddata=tdis_rc, time_units=time_units) + + # Instantiating MODFLOW 6 groundwater flow model + gwf = flopy.mf6.ModflowGwf( + sim, + modelname=gwfname, + save_flows=True, + newtonoptions="newton", + model_nam_file=f"{gwfname}.nam", + ) + + # Instantiating MODFLOW 6 solver for flow model + imsgwf = flopy.mf6.ModflowIms( + sim, + print_option="summary", + complexity="complex", + no_ptcrecord="all", + outer_dvclose=1.0e-4, + outer_maximum=2000, + under_relaxation="dbd", + linear_acceleration="BICGSTAB", + under_relaxation_theta=0.7, + under_relaxation_kappa=0.08, + under_relaxation_gamma=0.05, + under_relaxation_momentum=0.0, + backtracking_number=20, + backtracking_tolerance=2.0, + backtracking_reduction_factor=0.2, + backtracking_residual_limit=5.0e-4, + inner_dvclose=1.0e-5, + rcloserecord="0.0001 relative_rclose", + inner_maximum=100, + relaxation_factor=0.0, + number_orthogonalizations=2, + preconditioner_levels=8, + preconditioner_drop_tolerance=0.001, + filename=f"{gwfname}.ims", + ) + sim.register_ims_package(imsgwf, [gwf.name]) + + # Instantiating MODFLOW 6 discretization package + flopy.mf6.ModflowGwfdis( + gwf, + length_units=length_units, + nlay=nlay, + nrow=nrow, + ncol=ncol, + delr=delr, + delc=delc, + top=top, + botm=botm, + idomain=idomain, + filename=f"{gwfname}.dis", + ) + + # Instantiating MODFLOW 6 initial conditions package for flow model + flopy.mf6.ModflowGwfic(gwf, strt=strt, filename=f"{gwfname}.ic") + + # Instantiating MODFLOW 6 node-property flow package + flopy.mf6.ModflowGwfnpf( + gwf, + save_flows=False, + icelltype=laytyp, + k=hk, + k33=hk33, + save_specific_discharge=True, + filename=f"{gwfname}.npf", + ) + + # Instantiate storage package + flopy.mf6.ModflowGwfsto( + gwf, + ss=Ss, + sy=sy, + iconvert=iconvert, + filename=f"{gwfname}.sto", + ) + + # Instantiate constant head package + chdspd = [] + # Model domain is symmetric + for k in np.arange(nlay): + if botm[k] <= strt[k, 0, 0]: + # (l, r, c), head, conc + chdspd.append([(k, 0, 0), strt[k, 0, 0], 0.0]) + chdspd.append([(k, 0, ncol - 1), strt[k, 0, ncol - 1], 0.0]) + chdspd = {0: chdspd} + flopy.mf6.modflow.mfgwfchd.ModflowGwfchd( + gwf, + maxbound=len(chdspd), + stress_period_data=chdspd, + save_flows=False, + auxiliary="CONCENTRATION", + pname="CHD-1", + filename=f"{gwfname}.chd", + ) + + # Instantiate unsaturated zone flow package + flopy.mf6.ModflowGwfuzf( + gwf, + nuzfcells=nuzfcells, + ntrailwaves=15, + nwavesets=40, + print_flows=True, + packagedata=packagedata, + perioddata=uzf_perioddata, + pname="UZF-1", + budget_filerecord=f"{gwfname}.uzf.bud", + ) + + # Instantiate output control package + flopy.mf6.ModflowGwfoc( + gwf, + budget_filerecord=f"{gwfname}.bud", + head_filerecord=f"{gwfname}.hds", + headprintrecord=[("COLUMNS", 10, "WIDTH", 15, "DIGITS", 6, "GENERAL")], + saverecord=[("HEAD", "ALL"), ("BUDGET", "ALL")], + printrecord=[("HEAD", "LAST"), ("BUDGET", "ALL")], + ) + + # Instantiating MODFLOW 6 groundwater transport package + gwtname = "gwt-" + name + gwt = flopy.mf6.MFModel( + sim, + model_type="gwt6", + modelname=gwtname, + model_nam_file=f"{gwtname}.nam", + ) + gwt.name_file.save_flows = True + + # create iterative model solution and register the gwt model with it + imsgwt = flopy.mf6.ModflowIms( + sim, + print_option="summary", + complexity="complex", + no_ptcrecord="all", + outer_dvclose=1.0e-4, + outer_maximum=2000, + under_relaxation="dbd", + linear_acceleration="BICGSTAB", + under_relaxation_theta=0.7, + under_relaxation_kappa=0.08, + under_relaxation_gamma=0.05, + under_relaxation_momentum=0.0, + backtracking_number=20, + backtracking_tolerance=2.0, + backtracking_reduction_factor=0.2, + backtracking_residual_limit=5.0e-4, + inner_dvclose=1.0e-5, + rcloserecord="0.0001 relative_rclose", + inner_maximum=100, + relaxation_factor=0.0, + number_orthogonalizations=2, + preconditioner_levels=8, + preconditioner_drop_tolerance=0.001, + filename=f"{gwtname}.ims", + ) + sim.register_ims_package(imsgwt, [gwt.name]) + + # Instantiating MODFLOW 6 transport discretization package + flopy.mf6.ModflowGwtdis( + gwt, + nlay=nlay, + nrow=nrow, + ncol=ncol, + delr=delr, + delc=delc, + top=top, + botm=botm, + idomain=idomain, + filename=f"{gwtname}.dis", + ) + + # Instantiating MODFLOW 6 transport initial concentrations + flopy.mf6.ModflowGwtic(gwt, strt=sconc, filename=f"{gwtname}.ic") + + # Instantiating MODFLOW 6 transport advection package + if mixelm >= 0: + scheme = "UPSTREAM" + elif mixelm == -1: + scheme = "TVD" + else: + raise Exception() + flopy.mf6.ModflowGwtadv(gwt, scheme=scheme, filename=f"{gwtname}.adv") + + # Instantiating MODFLOW 6 transport dispersion package + if len(longitudinal_dispersivity) > 1: + disp = np.ones_like(ibound_mf2k5) + ath1 = np.ones_like(ibound_mf2k5) + atv = np.ones_like(ibound_mf2k5) + for i, (dispx, ratio1, ratio2) in enumerate( + zip( + longitudinal_dispersivity, + ratio_horizontal_to_longitudinal_dispersivity, + ratio_vertical_to_longitudinal_dispersivity, ) - - # Instantiating MODFLOW 6 transport mass storage package - flopy.mf6.ModflowGwtmst( - gwt, - porosity=prsity, - first_order_decay=False, - decay=None, - decay_sorbed=None, - sorption=None, - bulk_density=None, - distcoef=None, - pname="MST-1", - filename=f"{gwtname}.mst", + ): + disp[i, :, :] = dispx + ath1[i, :, :] = dispx * ratio1 + atv[i, :, :] = dispx * ratio2 + else: + # Dispersion + disp = longitudinal_dispersivity[0] + ath1 = ( + longitudinal_dispersivity[0] + * ratio_horizontal_to_longitudinal_dispersivity[0] ) - - # Instantiating MODFLOW 6 transport source-sink mixing package - sourcerecarray = [("CHD-1", "AUX", "CONCENTRATION")] - flopy.mf6.ModflowGwtssm( - gwt, - sources=sourcerecarray, - print_flows=True, - filename=f"{gwtname}.ssm", + atv = ( + longitudinal_dispersivity[0] + * ratio_vertical_to_longitudinal_dispersivity[0] ) - # Instantiate unsaturated zone transport package. - # * use iuzno to set the concentration of infiltrating water - # * only set the middle 10 cells at the top of the domain - # * first and last 15 cells have concentration of 0 - pd0 = [] - for i in range(14, 24): - pd0.append((i, "INFILTRATION", 1.0)) - uztperioddata = {0: pd0} - flopy.mf6.modflow.ModflowGwtuzt( + if al != 0: + flopy.mf6.ModflowGwtdsp( gwt, - save_flows=True, - print_input=True, - print_flows=True, - print_concentration=True, - concentration_filerecord=gwtname + ".uzt.bin", - budget_filerecord=gwtname + ".uzt.bud", - packagedata=uztpackagedata, - uztperioddata=uztperioddata, - pname="UZF-1", - filename=f"{gwtname}.uzt", + alh=disp, + ath1=ath1, + atv=atv, + pname="DSP-1", + filename=f"{gwtname}.dsp", ) - # Instantiating MODFLOW 6 transport output control package - flopy.mf6.ModflowGwtoc( - gwt, - budget_filerecord=f"{gwtname}.cbc", - concentration_filerecord=f"{gwtname}.ucn", - concentrationprintrecord=[ - ("COLUMNS", 10, "WIDTH", 15, "DIGITS", 6, "GENERAL") - ], - saverecord=[("CONCENTRATION", "ALL")], - printrecord=[("CONCENTRATION", "LAST"), ("BUDGET", "ALL")], - filename=f"{gwtname}.oc", - ) - - # Instantiating MODFLOW 6 flow-transport exchange mechanism - flopy.mf6.ModflowGwfgwt( - sim, - exgtype="GWF6-GWT6", - exgmnamea=gwfname, - exgmnameb=gwtname, - filename=f"{name}.gwfgwt", - ) - return mf, mt, sim - return None - - -# Function to write model files - - -def write_model(mf2k5, mt3d, sim, silent=True): - if config.writeModel: - mf2k5.write_input() - mt3d.write_input() - sim.write_simulation(silent=silent) - - -# Function to run the model. True is returned if the model runs successfully. - - -@config.timeit -def run_model(mf2k5, mt3d, sim, silent=True): - success = True - if config.runModel: - success, buff = mf2k5.run_model(silent=silent) - success, buff = mt3d.run_model(silent=silent) - success, buff = sim.run_simulation(silent=silent) - if not success: - print(buff) - return success - + # Instantiating MODFLOW 6 transport mass storage package + flopy.mf6.ModflowGwtmst( + gwt, + porosity=prsity, + first_order_decay=False, + decay=None, + decay_sorbed=None, + sorption=None, + bulk_density=None, + distcoef=None, + pname="MST-1", + filename=f"{gwtname}.mst", + ) + + # Instantiating MODFLOW 6 transport source-sink mixing package + sourcerecarray = [("CHD-1", "AUX", "CONCENTRATION")] + flopy.mf6.ModflowGwtssm( + gwt, + sources=sourcerecarray, + print_flows=True, + filename=f"{gwtname}.ssm", + ) + + # Instantiate unsaturated zone transport package. + # * use iuzno to set the concentration of infiltrating water + # * only set the middle 10 cells at the top of the domain + # * first and last 15 cells have concentration of 0 + pd0 = [] + for i in range(14, 24): + pd0.append((i, "INFILTRATION", 1.0)) + uztperioddata = {0: pd0} + flopy.mf6.modflow.ModflowGwtuzt( + gwt, + save_flows=True, + print_input=True, + print_flows=True, + print_concentration=True, + concentration_filerecord=gwtname + ".uzt.bin", + budget_filerecord=gwtname + ".uzt.bud", + packagedata=uztpackagedata, + uztperioddata=uztperioddata, + pname="UZF-1", + filename=f"{gwtname}.uzt", + ) + + # Instantiating MODFLOW 6 transport output control package + flopy.mf6.ModflowGwtoc( + gwt, + budget_filerecord=f"{gwtname}.cbc", + concentration_filerecord=f"{gwtname}.ucn", + concentrationprintrecord=[("COLUMNS", 10, "WIDTH", 15, "DIGITS", 6, "GENERAL")], + saverecord=[("CONCENTRATION", "ALL")], + printrecord=[("CONCENTRATION", "LAST"), ("BUDGET", "ALL")], + filename=f"{gwtname}.oc", + ) + + # Instantiating MODFLOW 6 flow-transport exchange mechanism + flopy.mf6.ModflowGwfgwt( + sim, + exgtype="GWF6-GWT6", + exgmnamea=gwfname, + exgmnameb=gwtname, + filename=f"{name}.gwfgwt", + ) + return mf, mt, sim + + +def write_models(mf2k5, mt3d, sim, silent=True): + mf2k5.write_input() + mt3d.write_input() + sim.write_simulation(silent=silent) + + +@timed +def run_models(mf2k5, mt3d, sim, silent=True): + success, buff = mf2k5.run_model(silent=silent, report=True) + assert success, pformat(buff) + success, buff = mt3d.run_model( + silent=silent, normal_msg="Program completed", report=True + ) + assert success, pformat(buff) + success, buff = sim.run_simulation(silent=silent, report=True) + assert success, pformat(buff) + + +# - + +# ### Plotting results +# +# Define functions to plot model results. -# Function to plot the model results +# + +# Figure properties +figure_size = (6, 4) def plot_results(mf2k5, mt3d, mf6, idx, ax=None): - if config.plotModel: - print("Plotting model results...") - mt3d_out_path = mt3d.model_ws - mf6_out_path = mf6.simulation_data.mfpath.get_sim_path() - mf6.simulation_data.mfpath.get_sim_path() - - # Get the MF-NWT heads - fname_mfnwt = os.path.join(mt3d_out_path, "uzt-2d-mf.hds") - hds_mfnwt = flopy.utils.HeadFile(fname_mfnwt) - hds = hds_mfnwt.get_alldata() - # Make list of verticies for plotting the saturated zone as a polygon - # Start by adding fixed locations - satzn = [] - satzn.append([40 * delr, 0]) - satzn.append([0 * delr, 0]) + print("Plotting model results...") + mt3d_out_path = mt3d.model_ws + mf6_out_path = mf6.simulation_data.mfpath.get_sim_path() + mf6.simulation_data.mfpath.get_sim_path() + + # Get the MF-NWT heads + fname_mfnwt = os.path.join(mt3d_out_path, "uzt-2d-mf.hds") + hds_mfnwt = flopy.utils.HeadFile(fname_mfnwt) + hds = hds_mfnwt.get_alldata() + # Make list of verticies for plotting the saturated zone as a polygon + # Start by adding fixed locations + satzn = [] + satzn.append([40 * delr, 0]) + satzn.append([0 * delr, 0]) + for j in range(ncol): + hd_in_col = hds[0, :, 0, j].max() + if j == 0: + satzn.append([j * delr, hd_in_col]) + elif j == ncol - 1: + satzn.append([(j + 1) * delr, hd_in_col]) + else: + satzn.append([(j * delr) + (delr / 2), hd_in_col]) + + poly_pts = np.array(satzn) + + # Get the MT3DMS concentration output + fname_mt3d = os.path.join(mt3d_out_path, "MT3D001.UCN") + ucnobj_mt3d = flopy.utils.UcnFile(fname_mt3d) + times_mt3d = ucnobj_mt3d.get_times() + conc_mt3d = ucnobj_mt3d.get_alldata() + + # get the MODFLOW 6 results from the UZF package and the GWT model + gwt = mf6.get_model("gwt-uzt-2d-mf6") + uzconc_mf6 = gwt.uzf.output.concentration().get_alldata() + mf6_satconc = gwt.output.concentration().get_alldata() + + uzconc_mf6_shpd = [] + for i in np.arange(0, uzconc_mf6.shape[0]): + tmp = uzconc_mf6[i, 0, 0, :].reshape((20, 1, 38)) + # insert column of np.nan on left and right sides of reshaped array + tmp2 = np.insert(tmp, 0, np.nan, axis=2) + tmp_apnd = np.empty((tmp2.shape[0], 1, 1)) + tmp_apnd[:] = np.nan + tmp3 = np.append(tmp2, tmp_apnd, axis=2) + uzconc_mf6_shpd.append(tmp3) + + uzconc_mf6_shpd = np.array(uzconc_mf6_shpd) + i = 0 + hds_bool = np.zeros_like(hds[0, :, i, :]) + for k in range(nlay): for j in range(ncol): - hd_in_col = hds[0, :, 0, j].max() - if j == 0: - satzn.append([j * delr, hd_in_col]) - elif j == ncol - 1: - satzn.append([(j + 1) * delr, hd_in_col]) - else: - satzn.append([(j * delr) + (delr / 2), hd_in_col]) - - poly_pts = np.array(satzn) - - # Get the MT3DMS concentration output - fname_mt3d = os.path.join(mt3d_out_path, "MT3D001.UCN") - ucnobj_mt3d = flopy.utils.UcnFile(fname_mt3d) - times_mt3d = ucnobj_mt3d.get_times() - conc_mt3d = ucnobj_mt3d.get_alldata() - - # get the MODFLOW 6 results from the UZF package and the GWT model - gwt = mf6.get_model("gwt-uzt-2d-mf6") - uzconc_mf6 = gwt.uzf.output.concentration().get_alldata() - mf6_satconc = gwt.output.concentration().get_alldata() - - uzconc_mf6_shpd = [] - for i in np.arange(0, uzconc_mf6.shape[0]): - tmp = uzconc_mf6[i, 0, 0, :].reshape((20, 1, 38)) - # insert column of np.nan on left and right sides of reshaped array - tmp2 = np.insert(tmp, 0, np.nan, axis=2) - tmp_apnd = np.empty((tmp2.shape[0], 1, 1)) - tmp_apnd[:] = np.nan - tmp3 = np.append(tmp2, tmp_apnd, axis=2) - uzconc_mf6_shpd.append(tmp3) - - uzconc_mf6_shpd = np.array(uzconc_mf6_shpd) - i = 0 - hds_bool = np.zeros_like(hds[0, :, i, :]) - for k in range(nlay): - for j in range(ncol): - if hds[0, k, i, j] > ((nlay - 1) - k) * 0.25: - hds_bool[k, j] = 1 - - combined_concs = np.where( - hds_bool > 0, - mf6_satconc[-1, :, 0, :], - uzconc_mf6_shpd[-1, :, 0, :], - ) + if hds[0, k, i, j] > ((nlay - 1) - k) * 0.25: + hds_bool[k, j] = 1 + + combined_concs = np.where( + hds_bool > 0, + mf6_satconc[-1, :, 0, :], + uzconc_mf6_shpd[-1, :, 0, :], + ) - combined_conc_3d = combined_concs[np.newaxis, :, np.newaxis, :] + combined_conc_3d = combined_concs[np.newaxis, :, np.newaxis, :] - contourLevels = np.arange(0.2, 1.01, 0.2) + contourLevels = np.arange(0.2, 1.01, 0.2) - # Create figure for scenario - fs = USGSFigure(figure_type="graph", verbose=False) + # Create figure for scenario + with styles.USGSPlot() as fs: sim_name = mf6.name plt.rcParams["lines.dashed_pattern"] = [5.0, 5.0] @@ -941,56 +925,49 @@ def plot_results(mf2k5, mt3d, mf6, idx, ax=None): title = "Unsaturated/saturated zone concentration X-section, time = 60 days" letter = chr(ord("@") + idx + 1) - # fs.heading(letter=letter, heading=title) + # styles.heading(letter=letter, heading=title) plt.tight_layout() - # save figure - if config.plotSave: + if plot_show: + plt.show() + if plot_save: fpth = os.path.join( "..", "figures", "{}{}".format( sim_name, - config.figure_ext, + ".png", ), ) fig.savefig(fpth) -# ### Function that wraps all of the steps for each MT3DMS Example 10 Problem scenario +# - + +# ### Running the example # -# 1. build_model, -# 2. write_model, -# 3. run_model, and -# 4. plot_results. +# Define and invoke a function to run the example scenario, then plot results. +# + def scenario(idx, silent=True): key = list(parameters.keys())[idx] parameter_dict = parameters[key] - mf2k5, mt3d, sim = build_model(key, mixelm=mixelm, **parameter_dict) - - write_model(mf2k5, mt3d, sim, silent=silent) - success = run_model(mf2k5, mt3d, sim, silent=silent) - if success: + mf2k5, mt3d, sim = build_models(key, mixelm=mixelm, **parameter_dict) + if write: + write_models(mf2k5, mt3d, sim, silent=silent) + if run: + run_models(mf2k5, mt3d, sim, silent=silent) + if plot: plot_results(mf2k5, mt3d, sim, idx) -# nosetest - exclude block from this nosetest to the next nosetest -def test_01(): - scenario(0, silent=True) - - -def test_02(): - scenario(1, silent=True) - - -# nosetest end +# - -if __name__ == "__main__": - # ### Two-Dimensional Transport in a Diagonal Flow Field - # - # Compares the standard finite difference solutions between MT3D & MF 6 - scenario(0, silent=True) +# + +scenario(0, silent=True) +# - - scenario(1, silent=True) +# + +scenario(1, silent=True) +# - diff --git a/scripts/notes.txt b/scripts/notes.txt deleted file mode 100644 index 7c39e3d25..000000000 --- a/scripts/notes.txt +++ /dev/null @@ -1,9 +0,0 @@ -Some common commands include: - -python process-scripts.py - -black --line-length 79 *.py - -When model has been created and run, use no write and no run to just make figs -python ex-gwt-prudic2004t2.py -nw -nr - diff --git a/scripts/process-scripts.py b/scripts/process-scripts.py index cd2d45664..0939db948 100644 --- a/scripts/process-scripts.py +++ b/scripts/process-scripts.py @@ -1,6 +1,5 @@ """ -Process MODFLOW 6 examples to build jupyter notebooks, -summary tables for the LaTeX document, and markdown tables for ReadtheDocs. +Build LaTeX and Markdown tables for the PDF documentation and ReadtheDocs. The files that are processed are the example python scripts (expy) and the example directories (exdir) that are created from running those scripts. @@ -51,26 +50,20 @@ >>> process-scripts.py -k *-maw-* # process all MAW example problems. """ import ast +import fnmatch import os import re import sys -import fnmatch import flopy - -sys.path.append(os.path.join("..", "common")) -import build_table as bt +from modflow_devtools.latex import get_footer, get_header # path to the example files ex_pth = os.path.join("..", "examples") # only process python files starting with ex_ files = sorted( - [ - file - for file in os.listdir() - if file.endswith(".py") and file.startswith("ex-") - ] + [file for file in os.listdir() if file.endswith(".py") and file.startswith("ex-")] ) only_process_ex = [] @@ -82,60 +75,6 @@ def _replace_quotes(proc_str): return proc_str -def make_notebooks(): - nb_pth = os.path.join("..", "notebooks") - if not os.path.isdir(nb_pth): - os.makedirs(nb_pth) - - tpth = "raw.py" - # converts python scripts to jupyter notebooks - for file in files: - # do a little processing - with open(file) as f: - lines = f.read().splitlines() - f = open(tpth, "w") - skip = False - modifyIndent = 0 - for idx, line in enumerate(lines): - # exclude if __name__ == "main" - if "if __name__" in line: - modifyIndent = len(lines[idx + 1]) - len( - lines[idx + 1].lstrip(" ") - ) - continue - - # exclude nosetest functions - if "# nosetest" in line.lower(): - if skip: - skip = False - continue - else: - skip = True - if skip: - continue - f.write(f"{line[modifyIndent:]}\n") - f.close() - - # convert temporary python file to a notebook - basename = os.path.splitext(file)[0] + ".ipynb" - opth = os.path.join(nb_pth, basename) - cmd = ( - "jupytext", - "--from py", - "--to ipynb", - "--update", - "-o", - opth, - tpth, - ) - print(" ".join(cmd)) - os.system(" ".join(cmd)) - - # remove temporary file - if os.path.isfile(tpth): - os.remove(tpth) - - def table_standard_header(caption, label): col_widths = ( 0.5, @@ -145,9 +84,7 @@ def table_standard_header(caption, label): "Parameter", "Value", ) - return bt.get_header( - caption, label, headings, col_widths=col_widths, center=False - ) + return get_header(caption, label, headings, col_widths=col_widths, center=False) def table_scenario_header(caption, label): @@ -163,13 +100,11 @@ def table_scenario_header(caption, label): "Parameter", "Value", ) - return bt.get_header( - caption, label, headings, col_widths=col_widths, center=False - ) + return get_header(caption, label, headings, col_widths=col_widths, center=False) def table_footer(): - return bt.get_footer() + return get_footer() def make_tables(): @@ -179,9 +114,7 @@ def make_tables(): for file in files: print(f"processing...'{file}'") - basename = os.path.splitext(os.path.basename(file))[0].replace( - "_", "-" - ) + basename = os.path.splitext(os.path.basename(file))[0].replace("_", "-") # do a little processing with open(file) as f: txt = f.read() @@ -239,9 +172,7 @@ def make_tables(): except: units = " (unknown)" if len(table_line) > 0: - table_line += "\t{}{} & {}".format( - text_to_write, units, value - ) + table_line += "\t{}{} & {}".format(text_to_write, units, value) else: table_line = "\t& & {}{} & {}".format( text_to_write, units, value @@ -264,7 +195,7 @@ def make_tables(): table_text = [] table_value = [] for lineno, line in enumerate(lines, 1): - if line.lower().startswith("# table"): + if line.lower().startswith("# model parameters"): scanning_table = True continue if scanning_table: @@ -394,14 +325,11 @@ def get_examples_dict(verbose=False): if file_name.lower() == "mfsim.nam": if verbose: msg = ( - " Found MODFLOW 6 simulation " - + f"name file: {file_name}" + " Found MODFLOW 6 simulation " + f"name file: {file_name}" ) print(msg) print(f"Using flopy to load {dirName}") - sim = flopy.mf6.MFSimulation.load( - sim_ws=dirName, verbosity_level=0 - ) + sim = flopy.mf6.MFSimulation.load(sim_ws=dirName, verbosity_level=0) sim_paks = [] for pak in sim.sim_package_list: pak_type = pak.package_abbr @@ -458,9 +386,7 @@ def build_md_tables(ex_dict): for ex_name in ex_paks.keys(): for ex_root in ex_order: if ex_root in ex_name: - pak_link[ex_name] = "[{}](_examples/{}.html)".format( - ex_name, ex_root - ) + pak_link[ex_name] = "[{}](_examples/{}.html)".format(ex_name, ex_root) break if ex_name not in list(pak_link.keys()): pak_link[ex_name] = ex_name @@ -643,9 +569,7 @@ def build_tex_tables(ex_dict): for idx, ex in enumerate(ex_order): for key, d in ex_dict.items(): if ex in key: - ex_number = [idx + 1] + [ - " " for i in range(len(d["paks"]) - 1) - ] + ex_number = [idx + 1] + [" " for i in range(len(d["paks"]) - 1)] d["ex_number"] = ex_number ex_tex[key] = d @@ -667,9 +591,7 @@ def build_tex_tables(ex_dict): caption = "List of example problems and simulation characteristics." label = "tab:ex-table" - lines = bt.get_header( - caption, label, headings, col_widths=col_widths, firsthead=True - ) + lines = get_header(caption, label, headings, col_widths=col_widths, firsthead=True) on_ex = 0 for idx, (key, sim_dict) in enumerate(ex_tex.items()): @@ -724,7 +646,7 @@ def build_tex_tables(ex_dict): pak_line.append(pak.upper()) lines += " ".join(pak_line) + " \\\\\n" - lines += bt.get_footer() + lines += get_footer() # create table pth = os.path.join("..", "tables", "ex-table.tex") @@ -819,9 +741,6 @@ def build_tex_tables(ex_dict): if len(only_process_ex) > 0: files = [item for item in files if item in only_process_ex] - # make the notebooks - make_notebooks() - # make the tables from the scripts make_tables()