This document describes development procedures and conventions for the MODFLOW 6 example models.
- Overview
- Requirements
- Conventions
- Running the examples
- Building documentation
- Contributing examples
- Releasing the examples
The example models in this repository are composed with FloPy in the Python files under scripts/
. Notebooks are generated from these with jupytext
for the online documentation and/or for use with jupyter
. A small volume of supporting data live in files under data/
.
To develop the examples one must first:
- create a Python environment
- install MODFLOW programs
- update FloPy classes
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:
pip install -r etc/requirements.pip.txt
pip install -r etc/requirements.usgs.txt
Besides MODFLOW 6, the examples use a number of MODFLOW-related programs, discovered on the system PATH
.
With FloPy installed in the active Python environment, these can all be installed with:
get-modflow :
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.
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:
get-modflow --repo modflow6-nightly-build :
See the FloPy documentation for more info on the get-modflow
utility.
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, for instance the develop
branch:
python -m flopy.mf6.utils.generate_classes --ref develop --no-backup
The --owner
and --repo
arguments can be used to select an alternative repository (e.g. your fork). See the FloPy documentation for more info.
Example scripts should follow a number of conventions. These keep behavior consistent, reduce barriers to entry for new users, and avoid surprises when building the examples documentation.
Examples may need supporting data which is too large or unwieldy to define in code. For instance, large arrays are better stored in external files, and loaded by the example script at runtime. At the same time, we prefer to avoid implicit dependencies on the local filesystem, so that scripts or notebooks downloaded from the documentation site can be run without first cloning the repository and/or manually downloading data files.
To solve this problem, example scripts employ two access patterns:
- If the script is running inside the repository, the
data/
folder may be assumed to exist, and local files can be accessed directly. Each script creates one or more subdirectories ofexamples/
to use as a workspace, one per example scenario. Figures are written tofigures/
. If the script creates any tables, they are written totables/
. - If the script is not running inside the repository, the current working directory is used as the example workspace. Model input files, output files and figures are all written to that location. Data files are downloaded from GitHub with a tool called Pooch.
A snippet like the following, which determines whether the example is running inside the repository and sets workspace paths, should appear at the beginning of each example script:
sim_name = "ex-gwf-advtidal"
try:
root = pl.Path(git.Repo(".", search_parent_directories=True).working_dir)
except:
root = None
workspace = root / "examples" if root else pl.Path.cwd()
figs_path = root / "figures" if root else pl.Path.cwd()
data_path = root / "data" / sim_name if root else pl.Path.cwd()
Note: The build automation expects the simulation name sim_name
and workspace directory name to match the example name as listed in doc/body.tex
.
It is also common to define model names up front, e.g.
gwf_name = sim_name + "-gwf"
gwt_name = sim_name + "-gwt"
Note: MODFLOW 6 restricts model names to 16-characters — if the example name (i.e. sim_name
) is long, model names will need abbreviation, e.g.
# shorten model names so they fit in 16-char limit
gwf_name = sim_name.replace("ex-gwf-", "") + "-gwf"
gwt_name = sim_name.replace("ex-gwt-", "") + "-gwt"
Provided a file name fname
and data directory data_path
, the following snippet suffices to retrieve a data file:
fpath = pooch.retrieve(
url=f"https://github.com/MODFLOW-USGS/modflow6-examples/raw/develop/data/{sim_name}/{fname}",
fname=fname,
path=data_path,
known_hash="md5:<MD5 hash of the file>",
)
Pooch will first check whether the file already exists in the data_path
, only downloading the file if necessary.
Note: When a new example is first added to the repository, the Pooch URL must point to the develop
branch, as the data file will not be available on the master
branch until the next release time — see the Releasing the examples section below. Once an example has reached a stable status, and data files are unlikely to change, the URL may be changed to point to master
.
Note: File hashes are sensitive to newline characters. To avoid disagreements, this repository's .gitattributes
file overrules Windows' default newline (CR+LF) in favor of POSIX-style newlines (LF) for all text-based files under data/
.
Scripts may contain one or more example scenarios. Generally, each scenario proceeds through the following steps:
- Define & build models
- Write input files
- Run models
- Plot results
Scenarios should be wrapped with a function, such that an example script can run multiple scenarios with e.g.
scenario(0)
scenario(1)
...
Scenario steps 2-4 listed above can be toggled programmatically via environment variables, all of which accept case-insensitive true/false
strings:
WRITE
: whether to write model input files to subdirectories ofexamples/
RUN
: whether to run modelsPLOT
: whether to create plots of resultsPLOT_SHOW
: whether to show static plots (disabled by default when run withpytest
)PLOT_SAVE
: whether to save static plots tofigures/
GIF
: whether to save GIFs tofigures/
(only relevant for a small subset of scripts)
If any environment variable is not found when a script is run directly, the corresponding behavior should be enabled by default. To parse settings from environment variables:
from modflow_devtools.misc import get_env
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)
Note: When scripts are run via pytest
, plots are skipped by default, to avoid the need to manually close plot widgets. Pytest CLI flags can be used to control each switch — see the Running with pytest
section below for details.
Scripts should contain a function wrapping all model runs for each scenario, generally named run_models()
. This function should be decorated with @timed
. This measures the example scenario's runtime, providing a rough performance benchmark. For instance:
from modflow_devtools.misc import timed
from pprint import pformat
@timed
def run_models(sim, silent=False):
success, buff = sim.run_simulation(silent=silent, report=True)
assert success, pformat(buff)
When the scenario is run, a line like the following will be shown:
run_models took 74.64 ms
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.
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
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.
For instance, to create model input files (without running models or plotting results, to save time):
pytest -v -n auto test_scripts.py --init
To convert an example script to a notebook manually with jupytext
(add --execute
to run the notebook after conversion):
jupytext --from py --to ipynb scripts/ex-gwf-twri.py -o notebooks/ex-gwf-twri.ipynb
To start a Jupyter browser interface, run jupyter notebook
from the notebooks/
directory after notebooks have been created with jupytext
.
This repository includes a ReadTheDocs site which is built automatically in CI, as well as resources for an example models PDF document distributed with each MODFLOW 6 release. Both can be built and checked locally.
Notebooks must be created and run (including model runs and plots) before building the ReadTheDocs documentation:
pytest -v -n auto test_notebooks.py --plot
This will
- create and/or update notebooks in
.doc/_notebooks
from examplescripts/*.py
- create model workspaces in
examples/
, write input files, and run models - create parameter tables for example descriptions in
tables/
- create plots and figures from model outputs in
figures/
Next, make sure RTD build dependencies are installed:
pip install -r .doc/requirements.rtd.txt
Next, build LaTeX and Markdown files:
python scripts/process-scripts.py
Next, create ReStructuredText (RST) index files from the contents of doc/body.tex
:
python etc/ci_create_examples_rst.py
The docs site can now be built from the .doc/
directory with make
:
make clean
make html
To host a local development server, switch to the .doc/_build/html/
directory and run python -m http.server
.
The PDF examples document shares several initial steps with the ReadTheDocs site. Scripts must first be run (including model runs and plots):
pytest -v -n auto test_scripts.py --plot
The test_notebooks.py
also suffices, but in this case the conversion from example scripts to notebooks is unnecessary.
Next, build LaTeX and Markdown files:
python scripts/process-scripts.py
Next, create ReStructuredText (RST) index files from the contents of doc/body.tex
:
python etc/ci_create_examples_rst.py
The PDF document can now be built from the doc/
directory:
./build-pdf.sh
This script runs multiple passes of pdflatex
and bibtex
to build the PDF document:
pdflatex mf6examples.tex
bibtex mf6examples
pdflatex mf6examples.tex
pdflatex mf6examples.tex
An equivalent batch script build-pdf.bat
is also available for Windows.
To add a new example:
- Add a new example script in the
scripts/
folder, use existing example scripts as a template. Use the environment variable settings described above to condition whether the script writes model input files, runs models, and plots results. - Add a new LaTeX description in the
doc/sections/
folder - Ensure the script runs successfully and creates any expected files
- Build the documentation locally and verify that everything looks good
- Open a pull request from your fork to the upstream repo's
develop
branch
Steps to create a release include:
- Generate model input files, disabling model runs and plots — e.g. from the
autotest/
directory, runpytest -v -n auto test_scripts.py
. Note: if model runs are enabled, the size of theexamples/
directory will balloon from double-digit MB to several GB. We only distribute input files, not output files. - Build the PDF documentation as described above.
- Release the documentation PDF and a zip archive of model input files.
These should not be necessary to perform manually, as GitHub Actions automatically creates a new release whenever code is merged into the master
branch of this repository.