diff --git a/autotest/test_sim_tolerate_unknown.py b/autotest/test_sim_tolerate_unknown.py new file mode 100644 index 00000000000..bd3c9078dfd --- /dev/null +++ b/autotest/test_sim_tolerate_unknown.py @@ -0,0 +1,83 @@ +""" +Test that the TOLERATE_UNKNOWN simulation option causes unrecognized +top-level input tags to produce a warning rather than an error. + +Two cases: + 1. Without the option: an unknown tag in a package OPTIONS block is an error. + 2. With the option: the same unknown tag produces a warning and the + simulation completes normally. +""" + +import subprocess + +import flopy + + +def run_mf6(exe, ws): + proc = subprocess.Popen( + [exe], stdout=subprocess.PIPE, stderr=subprocess.PIPE, cwd=ws + ) + result, _ = proc.communicate() + buff = result.decode("utf-8").splitlines() if result else [] + return proc.returncode, buff + + +def build_sim(ws, exe, tolerate_unknown=False): + """Build and write a minimal GWF simulation.""" + name = "tolunknown" + simkwargs = {"tolerate_unknown": True} if tolerate_unknown else {} + sim = flopy.mf6.MFSimulation( + sim_name=name, version="mf6", exe_name=exe, sim_ws=ws, **simkwargs + ) + flopy.mf6.ModflowTdis(sim, time_units="DAYS", nper=1, perioddata=[(1.0, 1, 1.0)]) + gwf = flopy.mf6.ModflowGwf(sim, modelname=name) + flopy.mf6.ModflowIms(sim, print_option="SUMMARY") + flopy.mf6.ModflowGwfdis(gwf, nlay=1, nrow=3, ncol=3, top=0.0, botm=[-1.0]) + flopy.mf6.ModflowGwfic(gwf, strt=0.0) + flopy.mf6.ModflowGwfnpf(gwf) + flopy.mf6.ModflowGwfchd( + gwf, stress_period_data={0: [[(0, 0, 0), 0.0], [(0, 2, 2), 1.0]]} + ) + sim.write_simulation() + return name + + +def inject_bogus_option(ws, name): + """Prepend an unrecognized keyword to the NPF OPTIONS block.""" + npf_file = ws / f"{name}.npf" + with open(npf_file, "w") as f: + f.write("BEGIN options\n") + f.write(" BOGUS_OPTION\n") + f.write("END options\n\n") + f.write("BEGIN griddata\n") + f.write(" ICELLTYPE\n") + f.write(" CONSTANT 0\n") + f.write(" K\n") + f.write(" CONSTANT 1.0\n") + f.write("END griddata\n") + + +def test_tolerate_unknown_error(function_tmpdir, targets): + """Without TOLERATE_UNKNOWN, an unrecognized option tag is a fatal error.""" + mf6 = targets["mf6"] + name = build_sim(function_tmpdir, mf6, tolerate_unknown=False) + inject_bogus_option(function_tmpdir, name) + + returncode, _ = run_mf6(mf6, function_tmpdir) + assert returncode != 0, "mf6 should have failed with an unknown input tag" + + +def test_tolerate_unknown_warning(function_tmpdir, targets): + """With TOLERATE_UNKNOWN, an unrecognized option tag is a warning and the + simulation completes normally.""" + mf6 = targets["mf6"] + name = build_sim(function_tmpdir, mf6, tolerate_unknown=True) + inject_bogus_option(function_tmpdir, name) + + returncode, buff = run_mf6(mf6, function_tmpdir) + assert returncode == 0, "mf6 failed unexpectedly:\n" + "\n".join(buff) + + lst = (function_tmpdir / "mfsim.lst").read_text() + assert "BOGUS_OPTION" in lst, "expected unknown tag name in listing file warning" + assert "ignored" in lst.lower(), "expected 'ignored' in listing file warning" + assert "Normal termination" in lst, "expected normal termination in listing file" diff --git a/doc/mf6io/mf6ivar/dfn/sim-nam.dfn b/doc/mf6io/mf6ivar/dfn/sim-nam.dfn index 46f20691015..d1461b59117 100644 --- a/doc/mf6io/mf6ivar/dfn/sim-nam.dfn +++ b/doc/mf6io/mf6ivar/dfn/sim-nam.dfn @@ -50,6 +50,14 @@ optional true longname print input to listing file description keyword to activate printing of simulation input summaries to the simulation list file (mfsim.lst). With this keyword, input summaries will be written for those packages that support newer input data model routines. Not all packages are supported yet by the newer input data model routines. +block options +name tolerate_unknown +type keyword +reader urword +optional true +longname tolerate unknown options +description keyword to indicate that unrecognized top-level input option tags should be treated as warnings rather than errors. When this keyword is specified, any top-level tag (not a subfield of a record) that is not recognized for a package will generate a warning and be skipped. This allows input files written for a newer version of MODFLOW 6 to be used with an older version, provided the unrecognized options are not required. + block options name hpc_filerecord type record hpc6 filein hpc6_filename diff --git a/src/Idm/sim-namidm.f90 b/src/Idm/sim-namidm.f90 index b2e9b635d3e..598bc960976 100644 --- a/src/Idm/sim-namidm.f90 +++ b/src/Idm/sim-namidm.f90 @@ -18,6 +18,7 @@ module SimNamInputModule logical :: prprof = .false. logical :: maxerrors = .false. logical :: print_input = .false. + logical :: tolerate_unknown = .false. logical :: hpc_filerecord = .false. logical :: hpc6 = .false. logical :: filein = .false. @@ -158,6 +159,25 @@ module SimNamInputModule .false. & ! timeseries ) + type(InputParamDefinitionType), parameter :: & + simnam_tolerate_unknown = InputParamDefinitionType & + ( & + 'SIM', & ! component + 'NAM', & ! subcomponent + 'OPTIONS', & ! block + 'TOLERATE_UNKNOWN', & ! tag name + 'TOLERATE_UNKNOWN', & ! fortran variable + 'KEYWORD', & ! type + '', & ! shape + 'tolerate unknown options', & ! longname + .false., & ! required + .false., & ! developmode + .false., & ! multi-record + .false., & ! preserve case + .false., & ! layered + .false. & ! timeseries + ) + type(InputParamDefinitionType), parameter :: & simnam_hpc_filerecord = InputParamDefinitionType & ( & @@ -471,6 +491,7 @@ module SimNamInputModule simnam_prprof, & simnam_maxerrors, & simnam_print_input, & + simnam_tolerate_unknown, & simnam_hpc_filerecord, & simnam_hpc6, & simnam_filein, & diff --git a/src/SimulationCreate.f90 b/src/SimulationCreate.f90 index 96eacde609c..6e85c009354 100644 --- a/src/SimulationCreate.f90 +++ b/src/SimulationCreate.f90 @@ -98,11 +98,11 @@ subroutine options_create() use MemoryManagerModule, only: mem_setptr use SimVariablesModule, only: idm_context use MemoryManagerModule, only: mem_set_print_option - use SimVariablesModule, only: isimcontinue, isimcheck + use SimVariablesModule, only: isimcontinue, isimcheck, itolerate_unknown ! -- dummy ! -- locals character(len=LENMEMPATH) :: input_mempath - integer(I4B), pointer :: simcontinue, nocheck, maxerror + integer(I4B), pointer :: simcontinue, nocheck, maxerror, tolerateunknown character(len=:), pointer :: prmem character(len=LINELENGTH) :: errmsg ! @@ -112,11 +112,13 @@ subroutine options_create() ! -- set pointers to input context option params call mem_setptr(simcontinue, 'CONTINUE', input_mempath) call mem_setptr(nocheck, 'NOCHECK', input_mempath) + call mem_setptr(tolerateunknown, 'TOLERATE_UNKNOWN', input_mempath) call mem_setptr(prmem, 'PRMEM', input_mempath) call mem_setptr(maxerror, 'MAXERRORS', input_mempath) ! ! -- update sim options isimcontinue = simcontinue + itolerate_unknown = tolerateunknown if (nocheck == 1) then isimcheck = 0 else @@ -156,6 +158,11 @@ subroutine options_create() 'MEMORY_PRINT_OPTION SET TO "', trim(prmem), '".' end if ! + if (itolerate_unknown == 1) then + write (iout, '(4x, a)') & + 'UNRECOGNIZED TOP-LEVEL INPUT OPTIONS WILL BE WARNED AND SKIPPED.' + end if + ! write (iout, '(1x,a)') 'END OF SIMULATION OPTIONS' end if end subroutine options_create diff --git a/src/Utilities/Idm/IdmLoad.f90 b/src/Utilities/Idm/IdmLoad.f90 index cd514e3e2d6..e0131d73f2f 100644 --- a/src/Utilities/Idm/IdmLoad.f90 +++ b/src/Utilities/Idm/IdmLoad.f90 @@ -587,6 +587,8 @@ subroutine allocate_simnam_int(input_mempath, idt) intvar = 1 case ('PRINT_INPUT') intvar = 0 + case ('TOLERATE_UNKNOWN') + intvar = 0 case default write (errmsg, '(a,a)') & 'Idm SIMNAM Load default value setting '& diff --git a/src/Utilities/Idm/mf6blockfile/LoadMf6File.f90 b/src/Utilities/Idm/mf6blockfile/LoadMf6File.f90 index ff0d5e49d80..ed6dd65bf9a 100644 --- a/src/Utilities/Idm/mf6blockfile/LoadMf6File.f90 +++ b/src/Utilities/Idm/mf6blockfile/LoadMf6File.f90 @@ -8,8 +8,8 @@ module LoadMf6FileModule use KindModule, only: DP, I4B, LGP - use SimVariablesModule, only: errmsg - use SimModule, only: store_error + use SimVariablesModule, only: errmsg, warnmsg, itolerate_unknown + use SimModule, only: store_error, store_warning use ConstantsModule, only: LINELENGTH, LENVARNAME use BlockParserModule, only: BlockParserType use LayeredArrayReaderModule, only: read_dbl1d_layered, & @@ -279,6 +279,7 @@ recursive subroutine parse_block(this, iblk, recursive_call) logical(LGP) :: found, required type(MemoryType), pointer :: mt character(len=LINELENGTH) :: tag + character(len=:), allocatable :: line type(InputParamDefinitionType), pointer :: idt ! disu vertices/cell2d blocks are contingent on NVERT dimension @@ -315,12 +316,29 @@ recursive subroutine parse_block(this, iblk, recursive_call) if (endOfBlock) exit ! process line as tag(s) call this%parser%GetStringCaps(tag) - idt => get_param_definition_type( & - this%mf6_input%param_dfns, & - this%mf6_input%component_type, & - this%mf6_input%subcomponent_type, & - this%mf6_input%block_dfns(iblk)%blockname, & - tag, this%filename) + if (itolerate_unknown == 1) then + idt => get_param_definition_type( & + this%mf6_input%param_dfns, & + this%mf6_input%component_type, & + this%mf6_input%subcomponent_type, & + this%mf6_input%block_dfns(iblk)%blockname, & + tag, this%filename, found=found) + if (.not. found) then + write (warnmsg, '(5a)') 'Unrecognized top-level input tag "', & + trim(tag), '" in file "', trim(this%filename), & + '" ignored.' + call store_warning(warnmsg) + call this%parser%GetRemainingLine(line) + cycle + end if + else + idt => get_param_definition_type( & + this%mf6_input%param_dfns, & + this%mf6_input%component_type, & + this%mf6_input%subcomponent_type, & + this%mf6_input%block_dfns(iblk)%blockname, & + tag, this%filename) + end if if (idt%in_record) then call this%parse_record_tag(iblk, idt, .false.) else diff --git a/src/Utilities/SimVariables.f90 b/src/Utilities/SimVariables.f90 index b616f10215b..8bf030908af 100644 --- a/src/Utilities/SimVariables.f90 +++ b/src/Utilities/SimVariables.f90 @@ -33,6 +33,7 @@ module SimVariablesModule integer(I4B) :: iout !< file unit number for simulation output integer(I4B) :: isimcnvg !< simulation convergence flag (1) if all objects have converged, (0) otherwise integer(I4B) :: isimcontinue = 0 !< simulation continue flag (1) to continue if isimcnvg = 0, (0) to terminate + integer(I4B) :: itolerate_unknown = 0 !< tolerate unknown input options (1) warn and skip, (0) error integer(I4B) :: nocheck = 0 !< nocheck option (0) to check input, (1) to ignore checks integer(I4B) :: isimcheck = 1 !< simulation input check flag (1) to check input, (0) to ignore checks integer(I4B) :: numnoconverge = 0 !< number of times the simulation did not converge diff --git a/src/mf6core.f90 b/src/mf6core.f90 index 863aa584077..ee70e25e04e 100644 --- a/src/mf6core.f90 +++ b/src/mf6core.f90 @@ -302,11 +302,19 @@ subroutine static_input_load() load_models, load_exchanges use MemoryHelperModule, only: create_mem_path use MemoryManagerModule, only: mem_setptr, mem_allocate - use SimVariablesModule, only: iparamlog + use SimVariablesModule, only: iparamlog, idm_context, itolerate_unknown + ! -- locals + character(len=LENMEMPATH) :: input_mempath + integer(I4B), pointer :: tolerateunknown ! ! -- load simnam input context call simnam_load(iparamlog) ! + ! -- read TOLERATE_UNKNOWN before loading packages so parse_block can honor it + input_mempath = create_mem_path('SIM', 'NAM', idm_context) + call mem_setptr(tolerateunknown, 'TOLERATE_UNKNOWN', input_mempath) + itolerate_unknown = tolerateunknown + ! ! -- load tdis to input context call simtdis_load() !