Skip to content

Commit

Permalink
refactor: harmonize OC setup between MF6 and MFNWT; add support for e…
Browse files Browse the repository at this point in the history
…mpty periods to turn off output writing
  • Loading branch information
aleaf committed May 14, 2021
1 parent 3f5ca12 commit c19e889
Show file tree
Hide file tree
Showing 16 changed files with 302 additions and 46 deletions.
1 change: 0 additions & 1 deletion docs/source/concepts/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,5 @@ Modflow-setup concepts and methods

.. toctree::
:maxdepth: 1
:caption: Concepts and methods

Specifying perimeter boundary conditions <perimeter-bcs.rst>
3 changes: 2 additions & 1 deletion docs/source/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,8 @@ Modflow-setup is intended to facilitate automated setup of MODFLOW models, from

Basic program structure and usage <structure>
The configuration file <config-file>
Concepts and Methods <concepts/index.rst>
Concepts and methods <concepts/index.rst>
Input instructions by package <input/index.rst>
Troubleshooting <troubleshooting>


Expand Down
7 changes: 7 additions & 0 deletions docs/source/input/index.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
===========================================================
Input instructions by package
===========================================================
.. toctree::
:maxdepth: 1

Output Control <oc.rst>
48 changes: 48 additions & 0 deletions docs/source/input/oc.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
===========================================================
MODFLOW Output Control
===========================================================

Stress period input format
--------------------------
Regardless of the model version (MODFLOW-2005-style or MODFLOW 6), output control can be specified in a format similar to native MODFLOW 6 input:

.. code-block:: yaml
oc:
period_options:
0: ['save head last', 'save budget last']
10: []
15: ['save head last', 'save budget last']
The above ``period_options:`` block would save the head and cell budget output on the last timestep of stress periods 0 through 9, and from 15 on, but turn off output saving for stress periods 10 through 14. This behavior is consistent with MODFLOW 6 but differs from MODFLOW-2005, where each stress periods must be explicitly included for output to be written. Other options besides ``'last'`` include ``all, first, frequency <frequency>, and steps <steps(<nstp)>``; see the MODFLOW 6 input instructions for more details.

Output filenames and other arguments
------------------------------------
For MODFLOW 6 models, the ``head_fileout_fmt`` and ``budget_fileout_fmt`` arguments can also be supplied to tell Flopy where to save the head and cell budget files, and how to name them. Modflow-setup fills any format specifiers (``'{}'``) with the model name, and passes the resulting strings to the ``head_filerecord`` and ``budget_filerecord`` arguments to the :py:class:`flopy.mf6.ModflowGwfoc <flopy.mf6.modflow.mfgwfoc.ModflowGwfoc>` constructor.

.. code-block:: yaml
oc:
head_fileout_fmt: '{}.hds'
budget_fileout_fmt: '{}.cbc'
period_options:
0: ['save head last', 'save budget last']
Any other valid arguments to the :py:class:`flopy.mf6.ModflowGwfoc <flopy.mf6.modflow.mfgwfoc.ModflowGwfoc>` and :py:class:`flopy.modflow.ModflowOc <flopy.modflow.mfoc.ModflowOc>` constructors can be supplied as keys in the ``oc:`` dictionary block. For example:

.. code-block:: yaml
oc:
unitnumber: [14, 51, 52, 53, 0]
would set the unit numbers for the head, drawdown, budget, and ibound output files. See the Flopy documentation for more details. Invalid arguments are filtered out prior to calling the constructor.

Alternative stress period input formats
----------------------------------------
As with other arguments, stress period input can also be directly specified in the Flopy input formats. For example, ``stress_period_data`` could be supplied for a MODFLOW-2005 model as it would be supplied to the :py:class:`flopy.modflow.ModflowOc <flopy.modflow.mfoc.ModflowOc>` constructor:

.. code-block:: yaml
oc:
stress_period_data:
(0, 1): ['save head', 'save budget']
2 changes: 1 addition & 1 deletion mfsetup/mf6_defaults.yml
Original file line number Diff line number Diff line change
Expand Up @@ -183,7 +183,7 @@ chd:
oc:
head_fileout_fmt: '{}.hds'
budget_fileout_fmt: '{}.cbc'
# example of using MODFLOW 6 text input
# example of using MODFLOW 6-style text input
period_options: {0: ['save head last',
'save budget last']
}
Expand Down
23 changes: 4 additions & 19 deletions mfsetup/mf6model.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
from mfsetup.mfmodel import MFsetupMixin
from mfsetup.mover import get_mover_sfr_package_input
from mfsetup.obs import setup_head_observations
from mfsetup.oc import parse_oc_period_input
from mfsetup.tdis import add_date_comments_to_tdis
from mfsetup.tmr import TmrNew
from mfsetup.units import convert_time_units
Expand Down Expand Up @@ -911,25 +912,9 @@ def setup_oc(self):
kwargs = self.cfg[package]
kwargs['budget_filerecord'] = self.cfg[package]['budget_fileout_fmt'].format(self.name)
kwargs['head_filerecord'] = self.cfg[package]['head_fileout_fmt'].format(self.name)
# parse both flopy and mf6-style input into flopy input
for rec in ['printrecord', 'saverecord']:
if rec in kwargs:
data = kwargs[rec]
mf6_input = {}
for kper, words in data.items():
mf6_input[kper] = []
for var, instruction in words.items():
mf6_input[kper].append((var, instruction))
kwargs[rec] = mf6_input
elif 'period_options' in kwargs:
mf6_input = defaultdict(list)
for kper, options in kwargs['period_options'].items():
for words in options:
type, var, instruction = words.split()
if type == rec.replace('record', ''):
mf6_input[kper].append((var, instruction))
if len(mf6_input) > 0:
kwargs[rec] = mf6_input

period_input = parse_oc_period_input(kwargs)
kwargs.update(period_input)

kwargs = get_input_arguments(kwargs, mf6.ModflowGwfoc)
oc = mf6.ModflowGwfoc(self, **kwargs)
Expand Down
4 changes: 2 additions & 2 deletions mfsetup/mfnwt_defaults.yml
Original file line number Diff line number Diff line change
Expand Up @@ -123,8 +123,8 @@ mnw:
oc:
head_fileout_fmt: '{}.hds'
budget_fileout_fmt: '{}.cbc'
period_options: {0: ['save head',
'save budget']
period_options: {0: ['save head last',
'save budget last']
}

hyd:
Expand Down
17 changes: 12 additions & 5 deletions mfsetup/mfnwtmodel.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
)
from mfsetup.mfmodel import MFsetupMixin
from mfsetup.obs import read_observation_data, setup_head_observations
from mfsetup.oc import parse_oc_period_input
from mfsetup.tdis import get_parent_stress_periods, setup_perioddata_group
from mfsetup.tmr import TmrNew
from mfsetup.units import convert_length_units, itmuni_text, lenuni_text
Expand Down Expand Up @@ -324,12 +325,18 @@ def setup_oc(self):
package = 'oc'
print('\nSetting up {} package...'.format(package.upper()))
t0 = time.time()
stress_period_data = {}
for i, r in self.perioddata.iterrows():
stress_period_data[(r.per, r.nstp -1)] = r.oc

#stress_period_data = {}
#for i, r in self.perioddata.iterrows():
# stress_period_data[(r.per, r.nstp -1)] = r.oc

# use stress_period_data if supplied
# (instead of period_input defaults)
if 'stress_period_data' in self.cfg['oc']:
del self.cfg['oc']['period_options']
kwargs = self.cfg['oc']
kwargs['stress_period_data'] = stress_period_data
period_input = parse_oc_period_input(kwargs, nstp=self.perioddata.nstp,
output_fmt='mfnwt')
kwargs.update(period_input)
kwargs = get_input_arguments(kwargs, fm.ModflowOc)
oc = fm.ModflowOc(model=self, **kwargs)
print("finished in {:.2f}s\n".format(time.time() - t0))
Expand Down
120 changes: 120 additions & 0 deletions mfsetup/oc.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
"""Functions for handling MODFLOW output control
"""
from collections import defaultdict


def parse_oc_period_input(period_input, nstp=None, output_fmt='mf6'):
"""Parse both flopy and mf6-style stress period output control input
into flopy input.
Parameters
----------
period_input : dict
Dictionary of stress period input (see examples)
nstp : list-like
Number of timesteps in each stress period
output_fmt : str
'mf6' for MODFLOW 6 style input (to :py:func:`flopy.mf6.ModflowGwfoc`), otherwise,
input for :py:func:`flopy.modflow.ModflowOc` is produced.
Returns
-------
flopy_input : dict
Input to the flopy output control package constructor.
Examples
--------
>>> period_input = {'saverecord': {0: {'head': 'last', 'budget': 'last'}}
{0: [('head', 'last'), ('budget', 'last')]}
"""
if nstp is not None:
nstp = list(nstp)

flopy_input = {}
mf6_flopy_input = {}
for rec in ['printrecord', 'saverecord']:
if rec in period_input:
if output_fmt != 'mf6':
msg = ("MODFLOW 6 Flopy-style OC input (printrecord or "
"saverecord arguments) only supported for MODFLOW 6 models.")
raise NotImplementedError(msg)
data = period_input[rec]
mf6_record_input = {}
for kper, words in data.items():
mf6_record_input[kper] = []
for var, instruction in words.items():
mf6_record_input[kper].append((var, instruction))
mf6_flopy_input[rec] = mf6_record_input
elif 'period_options' in period_input:
mf6_record_input = defaultdict(list)
mf_record_input = defaultdict(list)
for kper, options in period_input['period_options'].items():
# empty period for turning off output
if len(options) == 0:
mf6_record_input[kper] = []
mf_record_input[(kper, 0)] = []
else:
for words in options:
type, var, *instruction = words.split()
if type == rec.replace('record', ''):
if output_fmt == 'mf6':
mf6_record_input[kper].append((var, *instruction))
else:
if nstp is None:
raise ValueError("MODFLOW 2005-style OC input requires "
"timestep information.")
# parse MF6-style instructions
kstp = 0
nstep_idx = kper if kper < len(nstp) else -1
instruction, *values = instruction
instruction = instruction.lower()
if instruction == 'all':
for kstp in range(nstp[nstep_idx]):
mf_record_input[(kper, kstp)].append(f"{type} {var}")
elif 'frequency' in instruction:
if len(values) == 0:
raise ValueError("mfsetup.oc.parse_oc: "
"'frequency' instruction needs a value")
freq = int(values[0])
steps = list(range(nstp[nstep_idx]))[::freq]
for kstp in steps:
mf_record_input[(kper, kstp)].append(f"{type} {var}")
elif 'steps' in instruction:
if len(values) == 0:
raise ValueError("mfsetup.oc.parse_oc: "
"'steps' instruction needs one or more values")
for kstp in values:
mf_record_input[(kper, int(kstp))].append(f"{type} {var}")
elif instruction == 'first':
mf_record_input[(kper, 0)].append(f"{type} {var}")
elif instruction == 'last':
kstp = nstp[nstep_idx] - 1
mf_record_input[(kper, int(kstp))].append(f"{type} {var}")
else:
raise ValueError("mfsetup.oc.parse_oc: instruction "
f"'{instruction}' not understood")
if len(mf6_record_input) > 0:
mf6_flopy_input[rec] = dict(mf6_record_input)
if len(mf_record_input) > 0:
mf_record_input = fill_oc_stress_period_data(mf_record_input, nper=len(nstp))
flopy_input['stress_period_data'] = dict(mf_record_input)
if output_fmt == 'mf6':
return mf6_flopy_input
return flopy_input


def fill_oc_stress_period_data(stress_period_data, nper):
"""For MODFLOW 2005-style models, repeat last entry in stress_period_data
for subsequent stress periods (until another entry is encountered),
as is done by default in MODFLOW 6.
"""
filled_spd = {}
last_period_data = {}
for period in range(nper):
for (kper, kstp), data in stress_period_data.items():
if kper == period:
last_period_data[(kper, kstp)] = data
last_period_data = {(period, kstp): data for (kper, kstp), data
in last_period_data.items()}
filled_spd.update(last_period_data)
return filled_spd
2 changes: 1 addition & 1 deletion mfsetup/tdis.py
Original file line number Diff line number Diff line change
Expand Up @@ -169,7 +169,7 @@ def setup_perioddata_group(start_date_time, end_date_time=None,
oc_saverecord : dict
Dictionary with zero-based stress periods as keys and output control options as values.
Similar to MODFLOW-6 input, the information specified for a period will
continue to apply until information for another perior is specified.
continue to apply until information for another period is specified.
Returns
-------
Expand Down
2 changes: 1 addition & 1 deletion mfsetup/tests/data/shellmound.yml
Original file line number Diff line number Diff line change
Expand Up @@ -318,7 +318,7 @@ oc:
# MODFLOW 6-style text input can also be used
# e.g.
# period_options: {0: ['save head last',
# 'save budget last' ]
# 'save budget last' ]

obs:
source_data:
Expand Down
27 changes: 18 additions & 9 deletions mfsetup/tests/test_mf6_shellmound.py
Original file line number Diff line number Diff line change
Expand Up @@ -487,15 +487,19 @@ def test_obs_setup(shellmound_model_with_dis, config):
break


@pytest.mark.parametrize('options', [{'saverecord': {0: {'head': 'last',
'budget': 'last'}}},
{'period_options': {0: ['save head last',
'save budget last']}}
@pytest.mark.parametrize('input', [
# flopy-style input
{'saverecord': {0: {'head': 'last', 'budget': 'last'}}},
# MODFLOW 6-style input
{'period_options': {0: ['save head last', 'save budget last']}},
# blank period to skip subsequent periods
{'period_options': {0: ['save head last', 'save budget last'],
1: []}}
])
def test_oc_setup(shellmound_model_with_dis, options):
def test_oc_setup(shellmound_model_with_dis, input):
cfg = {'head_fileout_fmt': '{}.hds',
'budget_fileout_fmt': '{}.cbc'}
cfg.update(options)
cfg.update(input)
m = shellmound_model_with_dis # deepcopy(model)
m.cfg['oc'] = cfg
oc = m.setup_oc()
Expand All @@ -506,10 +510,15 @@ def test_oc_setup(shellmound_model_with_dis, options):
options = read_mf6_block(ocfile, 'options')
options = {k: ' '.join(v).lower() for k, v in options.items()}
perioddata = read_mf6_block(ocfile, 'period')
# convert back to zero-based
perioddata = {k-1:v for k, v in perioddata.items()}
assert 'fileout' in options['budget'] and '.cbc' in options['budget']
assert 'fileout' in options['head'] and '.hds' in options['head']
assert 'save head last' in perioddata[1]
assert 'save budget last' in perioddata[1]
if 'saverecord' in input:
assert 'save head last' in perioddata[0]
assert 'save budget last' in perioddata[0]
else:
assert perioddata == input['period_options']


def test_rch_setup(shellmound_model_with_dis):
Expand Down Expand Up @@ -728,7 +737,7 @@ def test_sfr_inflows_from_csv(model_with_sfr):
pd.testing.assert_series_equal(left, right, check_names=False, check_freq=False)


#@pytest.mark.xfail(reason='flopy remove_package() issue')
@pytest.mark.xfail(reason='flopy remove_package() issue')
def test_idomain_above_sfr(model_with_sfr):
m = model_with_sfr
sfr = m.sfr
Expand Down
2 changes: 1 addition & 1 deletion mfsetup/tests/test_mf6_shellmound_rot_grid.py
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,7 @@ def test_rotated_grid(shellmound_cfg, shellmound_simulation, mf6_exe):
cfg['setup_grid']['yoff'] = yoff
cfg['setup_grid']['rotation'] = rotation
cfg['dis']['dimensions']['nrow'] = nrow
cfg['dis']['dimensions']['ncol'] = 25
cfg['dis']['dimensions']['ncol'] = ncol

cfg = MF6model._parse_model_kwargs(cfg)
kwargs = get_input_arguments(cfg['model'], mf6.ModflowGwf,
Expand Down
Loading

0 comments on commit c19e889

Please sign in to comment.