diff --git a/autotest/test_listbudget.py b/autotest/test_listbudget.py index 47f438237..b0bcdacda 100644 --- a/autotest/test_listbudget.py +++ b/autotest/test_listbudget.py @@ -160,3 +160,54 @@ def test_mf6listfile_parse_budget_line_missing_e(value, expected): assert entry == "FLOW-JA-FACE" assert flux == expected assert cumu == expected + + +MF6_BUDGET_TEMPLATE = """ + VOLUME BUDGET FOR ENTIRE MODEL AT END OF TIME STEP 1, STRESS PERIOD {kper} + ------------------------------------------------------------------------------ + + CUMULATIVE VOLUME L**3 RATES FOR THIS TIME STEP L**3/T + + IN: IN: + --- --- + STO = 100.0000 STO = 100.0000 + CHD = {chd:>8} CHD = {chd:>8} + + TOTAL IN = 100.0000 TOTAL IN = 100.0000 + + OUT: OUT: + ---- ---- + STO = 100.0000 STO = 100.0000 + CHD = 0.0000 CHD = 0.0000 + + TOTAL OUT = 100.0000 TOTAL OUT = 100.0000 + + IN - OUT = 0.0000 IN - OUT = 0.0000 + + PERCENT DISCREPANCY = 0.00 PERCENT DISCREPANCY = 0.00 +""" + + +def test_mf6listfile_unparseable_first_entry(function_tmpdir): + # see https://github.com/modflowpy/flopy/issues/2856 + list_file = function_tmpdir / "gwf.lst" + list_file.write_text( + MF6_BUDGET_TEMPLATE.format(kper=1, chd="********") + + MF6_BUDGET_TEMPLATE.format(kper=2, chd="0.0000") + ) + match = r"first budget.*CHD \(IN\) at time step 1, stress period 1: .*\*{8}" + with pytest.raises(ValueError, match=match): + Mf6ListBudget(list_file) + + +def test_mf6listfile_unparseable_later_entry(function_tmpdir): + list_file = function_tmpdir / "gwf.lst" + list_file.write_text( + MF6_BUDGET_TEMPLATE.format(kper=1, chd="0.0000") + + MF6_BUDGET_TEMPLATE.format(kper=2, chd="********") + ) + mflist = Mf6ListBudget(list_file) + inc, cum = mflist.get_budget() + assert inc["STO_IN"][0] == pytest.approx(100.0) + assert np.isnan(inc["STO_IN"][1]) + assert np.isnan(cum["CHD_IN"][1]) diff --git a/flopy/utils/mflistfile.py b/flopy/utils/mflistfile.py index 93581e784..4301c9be9 100644 --- a/flopy/utils/mflistfile.py +++ b/flopy/utils/mflistfile.py @@ -630,20 +630,23 @@ def _build_index(self, maxentries): def _get_index(self, maxentries): # --parse through the file looking for matches and parsing ts and sp idxs = [] - l_count = 1 + l_count = 0 while True: seekpoint = self.f.tell() line = self.f.readline() + l_count += 1 if line == "": break if self.budgetkey in line: for _ in range(self.tssp_lines): line = self.f.readline() + l_count += 1 try: ts, sp = get_ts_sp(line) - except: + except Exception: print( - "unable to cast ts,sp on line number", l_count, " line: ", line + "could not parse time step and stress period " + f"on line {l_count}: {line.strip()!r}" ) break @@ -684,14 +687,13 @@ def _set_entries(self): raise Exception(f"entries already set:{self.entries}") if not self.idx_map: raise Exception("must call build_index before call set_entries") + ts, sp, seekpoint = self.idx_map[0] try: - incdict, cumdict = self._get_sp( - self.idx_map[0][0], self.idx_map[0][1], self.idx_map[0][2] - ) - except: - raise Exception( - "unable to read budget information from first entry in list file" - ) + incdict, cumdict = self._get_sp(ts, sp, seekpoint) + except Exception as e: + raise ValueError( + f"unable to read first budget in list file {self.file_name}: {e}" + ) from e self.entries = incdict.keys() null_entries = {} incdict = {} @@ -759,17 +761,24 @@ def _load(self, maxentries=None): return + def _sp_failed(self, msg): + # entries are read from the first budget, so until they are set + # there are no null entries to fall back on + if not self.entries: + raise ValueError(msg) + print(msg) + return self.null_entries + def _get_sp(self, ts, sp, seekpoint): self.f.seek(seekpoint) # --read to the start of the "in" budget information while True: line = self.f.readline() if line == "": - print( - "end of file found while seeking budget " - f"information for ts,sp: {ts} {sp}" + return self._sp_failed( + "end of file found while reading budget " + f"at time step {ts}, stress period {sp}" ) - return self.null_entries # --if there are two '=' in this line, then it is a budget line if len(re.findall(r"=", line)) == 2: @@ -781,27 +790,19 @@ def _get_sp(self, ts, sp, seekpoint): entrydict = {} while True: if line == "": - print( - "end of file found while seeking budget " - f"information for ts,sp: {ts} {sp}" + return self._sp_failed( + "end of file found while reading budget " + f"at time step {ts}, stress period {sp}" ) - return self.null_entries if len(re.findall(r"=", line)) == 2: try: entry, flux, cumu = self._parse_budget_line(line) except Exception: - print("error parsing budget line in ts,sp", ts, sp) - return self.null_entries - if flux is None: - print( - "error casting in flux for", entry, " to float in ts,sp", ts, sp - ) - return self.null_entries - if cumu is None: - print( - "error casting in cumu for", entry, " to float in ts,sp", ts, sp + return self._sp_failed( + "could not parse budget line at time step " + f"{ts}, stress period {sp}: {line.strip()!r}" ) - return self.null_entries + label = entry if entry.endswith(tag.upper()): if " - " in entry.upper(): key = entry.replace(" ", "") @@ -818,6 +819,12 @@ def _get_sp(self, ts, sp, seekpoint): else: entrydict[entry] = 0 key = f"{entry}_{tag}" + label = f"{label} ({tag})" + if flux is None or cumu is None: + return self._sp_failed( + f"could not parse value for {label} at time step {ts}, " + f"stress period {sp}: {line.strip()!r}" + ) incdict[key] = flux cumdict[key] = cumu else: @@ -856,8 +863,8 @@ def _get_totim(self, ts, sp, seekpoint): ihead += 1 if line == "": print( - "end of file found while seeking budget " - f"information for ts,sp: {ts} {sp}" + "end of file found while reading time summary " + f"at time step {ts}, stress period {sp}" ) return np.nan, np.nan, np.nan elif ( @@ -873,28 +880,37 @@ def _get_totim(self, ts, sp, seekpoint): translen = self._parse_time_line(line) line = self.f.readline() if translen is None: - print("error parsing translen for ts,sp", ts, sp) + print( + f"could not parse transport step length at time step {ts}, " + f"stress period {sp}" + ) return np.nan, np.nan, np.nan tslen = self._parse_time_line(line) if tslen is None: - print("error parsing tslen for ts,sp", ts, sp) + print( + f"could not parse time step length at time step {ts}, " + f"stress period {sp}" + ) return np.nan, np.nan, np.nan sptim = self._parse_time_line(self.f.readline()) if sptim is None: - print("error parsing sptim for ts,sp", ts, sp) + print( + f"could not parse stress period time at time step {ts}, " + f"stress period {sp}" + ) return np.nan, np.nan, np.nan totim = self._parse_time_line(self.f.readline()) if totim is None: - print("error parsing totim for ts,sp", ts, sp) + print(f"could not parse total time at time step {ts}, stress period {sp}") return np.nan, np.nan, np.nan return tslen, sptim, totim def _parse_time_line(self, line): if line == "": - print("end of file found while parsing time information") + print("end of file found while reading time summary") return None try: time_str = line[self.time_line_idx :] @@ -910,7 +926,7 @@ def _parse_time_line(self, line): idx = 0 tval = float(raw[idx]) except: - print("error parsing tslen information: ", time_str) + print(f"could not parse time value: {line.strip()!r}") return None return tval