From e115db50bfe8a9e77cfc86bdff6f8d0d2d16d601 Mon Sep 17 00:00:00 2001 From: Bonelli Date: Sun, 15 Feb 2026 09:54:09 -0500 Subject: [PATCH] feat(mf6): opt out of writing default values --- autotest/test_mf6_optional_default_value.py | 86 +++++++++++++++++++++ flopy/mf6/data/mfdatascalar.py | 28 +++++++ flopy/mf6/data/mfstructure.py | 2 +- flopy/mf6/mfsimbase.py | 21 ++++- 4 files changed, 135 insertions(+), 2 deletions(-) create mode 100644 autotest/test_mf6_optional_default_value.py diff --git a/autotest/test_mf6_optional_default_value.py b/autotest/test_mf6_optional_default_value.py new file mode 100644 index 0000000000..5eb85c176d --- /dev/null +++ b/autotest/test_mf6_optional_default_value.py @@ -0,0 +1,86 @@ +""" +Test that optional package variables with default values aren't written to +input files when write_defaults=False. + +Reproduces https://github.com/modflowpy/flopy/issues/2710. +""" + +from pathlib import Path + +import pytest + +import flopy + +pytestmark = pytest.mark.mf6 + + +def _build_prt_sim(ws, coordinate_check_method="eager"): + sim = flopy.mf6.MFSimulation(sim_name="prt", sim_ws=str(ws)) + flopy.mf6.ModflowTdis(sim, nper=1, perioddata=[(1.0, 1, 1.0)]) + ems = flopy.mf6.ModflowEms(sim) + prt = flopy.mf6.ModflowPrt(sim, modelname="prt") + flopy.mf6.ModflowPrtdis( + prt, + nlay=1, + nrow=1, + ncol=3, + delr=1.0, + delc=1.0, + top=1.0, + botm=0.0, + ) + flopy.mf6.ModflowPrtmip(prt, porosity=0.1) + flopy.mf6.ModflowPrtprp( + prt, + nreleasepts=1, + packagedata=[(0, (0, 0, 0), 0.5, 0.5, 0.5)], + perioddata={0: ["FIRST"]}, + coordinate_check_method=coordinate_check_method, + ) + flopy.mf6.ModflowPrtoc(prt, track_filerecord=[("prt.trk",)]) + sim.register_solution_package(ems, [prt.name]) + return sim + + +def _prp_text(ws): + prp_files = list(Path(ws).glob("*.prp")) + assert len(prp_files) == 1, f"expected one .prp file, found: {prp_files}" + return prp_files[0].read_text().upper() + + +def test_coordinate_check_method(function_tmpdir): + # write_defaults=True, default value + ws = Path(function_tmpdir) / "write_defaults" + ws.mkdir() + sim = _build_prt_sim(ws, coordinate_check_method="eager") + sim.write_simulation() + text = _prp_text(ws) + assert "COORDINATE_CHECK_METHOD" in text + assert "EAGER" in text + + # write_defaults=False, default value + ws = Path(function_tmpdir) / "eager" + ws.mkdir() + sim = _build_prt_sim(ws, coordinate_check_method="eager") + sim.simulation_data.write_defaults = False + sim.write_simulation() + assert "COORDINATE_CHECK_METHOD" not in _prp_text(ws) + + # write_defaults=False, non-default value + ws = Path(function_tmpdir) / "none" + ws.mkdir() + sim = _build_prt_sim(ws, coordinate_check_method="none") + sim.simulation_data.write_defaults = False + sim.write_simulation() + text = _prp_text(ws) + assert "COORDINATE_CHECK_METHOD" in text + assert "NONE" in text + + # write_defaults=False passed directly to write_simulation(), default + # value, overriding simulation_data.write_defaults for this call only + ws = Path(function_tmpdir) / "write_simulation_kwarg" + ws.mkdir() + sim = _build_prt_sim(ws, coordinate_check_method="eager") + sim.write_simulation(write_defaults=False) + assert "COORDINATE_CHECK_METHOD" not in _prp_text(ws) + assert sim.simulation_data.write_defaults is True diff --git a/flopy/mf6/data/mfdatascalar.py b/flopy/mf6/data/mfdatascalar.py index 94c4f1e3fd..744c769e08 100644 --- a/flopy/mf6/data/mfdatascalar.py +++ b/flopy/mf6/data/mfdatascalar.py @@ -318,6 +318,28 @@ def add_one(self): self._simulation_data.debug, ) + @staticmethod + def _matches_default(data_item, current): + # only compare scalar types. composite types are always written + if data_item.type == DatumType.integer: + try: + return int(float(current)) == int(float(data_item.default_value)) + except (ValueError, TypeError): + return False + elif data_item.type == DatumType.double_precision: + try: + return float(current) == float(data_item.default_value) + except (ValueError, TypeError): + return False + elif data_item.type == DatumType.string: + return ( + str(current).lower().strip() + == data_item.default_value.lower().strip() + ) + elif data_item.type == DatumType.keyword: + return data_item.default_value.lower().strip() == "true" + return False + def get_file_entry( self, values_only=False, @@ -361,6 +383,12 @@ def get_file_entry( self._simulation_data.debug, ex, ) + if self.structure.optional and not self._simulation_data.write_defaults: + data_item = self.structure.data_item_structures[0] + if data_item.default_value is not None and self._matches_default( + data_item, storage.get_data() + ): + return "" if ( self.structure.type == DatumType.keyword or self.structure.type == DatumType.record diff --git a/flopy/mf6/data/mfstructure.py b/flopy/mf6/data/mfstructure.py index 41f68d0a4e..9fd00ffb73 100644 --- a/flopy/mf6/data/mfstructure.py +++ b/flopy/mf6/data/mfstructure.py @@ -617,7 +617,7 @@ def set_value(self, line, common): self.ucase = bool(arr_line[1]) elif arr_line[0] == "preserve_case": self.preserve_case = self._get_boolean_val(arr_line) - elif arr_line[0] == "default_value": + elif arr_line[0] in ("default_value", "default"): self.default_value = " ".join(arr_line[1:]) elif arr_line[0] == "numeric_index": self.numeric_index = self._get_boolean_val(arr_line) diff --git a/flopy/mf6/mfsimbase.py b/flopy/mf6/mfsimbase.py index 20f3f133f6..4719c809f5 100644 --- a/flopy/mf6/mfsimbase.py +++ b/flopy/mf6/mfsimbase.py @@ -259,6 +259,7 @@ def __init__(self, path: Union[str, PathLike], mfsim): self._verbosity_level = VerbosityLevel.normal self._max_columns_set_by = None # Can be None, 'user', or 'auto' self.use_pandas = True + self.write_defaults = True self._update_str_format() @@ -1717,7 +1718,10 @@ def set_all_data_internal(self, check_data=True): package.set_all_data_internal(check_data) def write_simulation( - self, ext_file_action=ExtFileAction.copy_relative_paths, silent=False + self, + ext_file_action=ExtFileAction.copy_relative_paths, + silent=False, + write_defaults=None, ): """ Write the simulation to files. @@ -1730,10 +1734,23 @@ def write_simulation( by absolute paths fixed. silent : bool Writes out the simulation in silent mode (verbosity_level = 0) + write_defaults : bool + Whether to write optional variables whose value equals the + MODFLOW 6 default for that variable. Defaults to None, which + defers to simulation_data.write_defaults (True unless changed + by the user). Set to False to omit such variables from the + input files, which can be useful when a MODFLOW 6 version + does not yet support an option flopy otherwise writes by + default. """ self._auto_set_max_columns() + sim_data = self.simulation_data + if write_defaults is not None: + saved_write_defaults = sim_data.write_defaults + sim_data.write_defaults = write_defaults + saved_verb_lvl = self.simulation_data.verbosity_level if silent: self.simulation_data.verbosity_level = VerbosityLevel.quiet @@ -1795,6 +1812,8 @@ def write_simulation( if silent: self.simulation_data.verbosity_level = saved_verb_lvl + if write_defaults is not None: + sim_data.write_defaults = saved_write_defaults def set_sim_path(self, path: Union[str, PathLike]): """Return a list of output data keys.