diff --git a/autotest/test_mf6_optional_default_value.py b/autotest/test_mf6_optional_default_value.py new file mode 100644 index 000000000..b60840f92 --- /dev/null +++ b/autotest/test_mf6_optional_default_value.py @@ -0,0 +1,89 @@ +""" +Test that optional package variables with default values aren't written to +input files when the value matches the DFN-declared default, unless +write_defaults is set on the simulation data. + +Reproduce https://github.com/modflowpy/flopy/issues/2710: +ModflowPrtprp.coordinate_check_method was introduced in MF6 6.7.0 and +defaults to "eager". flopy 3.10.0 wrote COORDINATE_CHECK_METHOD EAGER +unconditionally, causing fatal errors when used with older MF6 binaries. + +The fix: get_file_entry() suppresses writing optional scalars whose current +value equals the DFN-declared default, unless simulation_data.write_defaults +is True. MF6 applies its own internal default when the keyword is absent, +so the simulation outcome is identical either way. +""" + +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): + # Default value "eager": should NOT be written. + # MF6 applies its own internal default (eager) when the keyword is absent, + # so omitting it produces identical simulation behavior. + ws = Path(function_tmpdir) / "eager" + ws.mkdir() + sim = _build_prt_sim(ws, coordinate_check_method="eager") + sim.write_simulation() + assert "COORDINATE_CHECK_METHOD" not in _prp_text(ws) + + # Non-default value "none": should be written. + ws = Path(function_tmpdir) / "none" + ws.mkdir() + sim = _build_prt_sim(ws, coordinate_check_method="none") + sim.write_simulation() + text = _prp_text(ws) + assert "COORDINATE_CHECK_METHOD" in text + assert "NONE" in text + + # write_defaults=True: default value should be written explicitly. + # Useful for producing fully self-contained input files independent + # of MF6's internal defaults, e.g. for archival or sharing. + ws = Path(function_tmpdir) / "write_defaults" + ws.mkdir() + sim = _build_prt_sim(ws, coordinate_check_method="eager") + sim.simulation_data.write_defaults = True + sim.write_simulation() + text = _prp_text(ws) + assert "COORDINATE_CHECK_METHOD" in text + assert "EAGER" in text diff --git a/flopy/mf6/data/mfdatascalar.py b/flopy/mf6/data/mfdatascalar.py index 94c4f1e3f..22fdc0cb9 100644 --- a/flopy/mf6/data/mfdatascalar.py +++ b/flopy/mf6/data/mfdatascalar.py @@ -361,6 +361,20 @@ def get_file_entry( self._simulation_data.debug, ex, ) + if self.structure.optional: + data_item = self.structure.data_item_structures[0] + if data_item.default_value is not None: + current = storage.get_data() + try: + matches_default = float(str(current)) == float( + data_item.default_value + ) + except (ValueError, TypeError): + matches_default = str(current).lower().strip() == ( + data_item.default_value.lower().strip() + ) + if matches_default and not self._simulation_data.write_defaults: + 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 3a44b4f9a..649cfbcb9 100644 --- a/flopy/mf6/data/mfstructure.py +++ b/flopy/mf6/data/mfstructure.py @@ -614,7 +614,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 273970bb6..def3a1803 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 = False self._update_str_format()