From 6d2abab167d49b4c4f97973a2267c05a44e42f70 Mon Sep 17 00:00:00 2001 From: Mike Taves Date: Wed, 12 Jun 2024 22:13:13 +1200 Subject: [PATCH] refactor(datafile): ignore "text" parameter, add attributes from file --- autotest/test_binaryfile.py | 82 +++++++++++++++++++++++++++- autotest/test_formattedfile.py | 1 + flopy/export/utils.py | 4 +- flopy/mf6/utils/binaryfile_utils.py | 6 +-- flopy/utils/binaryfile.py | 83 +++++++++++++++-------------- flopy/utils/datafile.py | 9 +++- 6 files changed, 137 insertions(+), 48 deletions(-) diff --git a/autotest/test_binaryfile.py b/autotest/test_binaryfile.py index 3d7a0deb02..c6751f99b6 100644 --- a/autotest/test_binaryfile.py +++ b/autotest/test_binaryfile.py @@ -3,6 +3,7 @@ See also test_cellbudgetfile.py for similar tests. """ +import warnings from itertools import repeat import numpy as np @@ -104,6 +105,8 @@ def test_headfile_build_index(example_data_path): assert hds.ncol == 20 assert hds.nlay == 3 assert not hasattr(hds, "nper") + assert hds.text == "head" + assert hds.text_bytes == b"HEAD".rjust(16) assert hds.totalbytes == 10_676_004 assert len(hds.recordarray) == 3291 assert type(hds.recordarray) == np.ndarray @@ -150,7 +153,80 @@ def test_headfile_build_index(example_data_path): ) -def test_concentration_build_index(example_data_path): +@pytest.mark.parametrize( + "pth, expected", + [ + pytest.param( + "mf6-freyberg/freyberg.hds", + { + "precision": "double", + "nlay, nrow, ncol": (1, 40, 20), + "text": "head", + "text_bytes": b"HEAD".ljust(16), + "len(obj)": 1, + }, + id="freyberg.hds", + ), + pytest.param( + "mf6/create_tests/test_transport/expected_output/gwt_mst03.ucn", + { + "precision": "double", + "nlay, nrow, ncol": (1, 1, 1), + "text": "concentration", + "text_bytes": b"CONCENTRATION".ljust(16), + "len(obj)": 28, + }, + id="gwt_mst03.ucn", + ), + pytest.param( + "mfusg_test/03A_conduit_unconfined/output/ex3A.cln.hds", + { + "precision": "single", + "nlay, nrow, ncol": (1, 1, 2), + "text": "cln_heads", + "text_bytes": b"CLN HEADS".rjust(16), + "len(obj)": 1, + }, + id="ex3A.cln.hds", + ), + pytest.param( + "mfusg_test/03A_conduit_unconfined/output/ex3A.ddn", + { + "precision": "single", + "nlay, nrow, ncol": (2, 100, 100), + "text": "drawdown", + "text_bytes": b"DRAWDOWN".rjust(16), + "len(obj)": 2, + }, + id="ex3A.ddn", + ), + ], +) +def test_headfile_examples(example_data_path, pth, expected): + with HeadFile(example_data_path / pth) as obj: + assert obj.precision == expected["precision"] + assert (obj.nlay, obj.nrow, obj.ncol) == expected["nlay, nrow, ncol"] + assert obj.text == expected["text"] + assert obj.text_bytes == expected["text_bytes"] + assert len(obj) == expected["len(obj)"] + + +@pytest.mark.parametrize( + "pth", + [ + "mt3d_test/mf96mt3d/P01/case1b/MT3D001.UCN", + "unstructured/headu.githds", + ], +) +def test_not_headfile(example_data_path, pth): + # These examples pass get_headfile_precision, but are not HeadFiles + with pytest.raises(ValueError, match="cannot read file with HeadFile"): + with warnings.catch_warnings(): + warnings.simplefilter("ignore") + HeadFile(example_data_path / pth) + + +def test_ucnfile_build_index(example_data_path): # test low-level BinaryLayerFile._build_index() method with UCN file pth = example_data_path / "mt3d_test/mf2005mt3d/P07/MT3D001.UCN" with UcnFile(pth) as ucn: @@ -159,6 +235,8 @@ def test_concentration_build_index(example_data_path): assert ucn.ncol == 21 assert ucn.nlay == 8 assert not hasattr(ucn, "nper") + assert ucn.text == "concentration" + assert ucn.text_bytes == b"CONCENTRATION".ljust(16) assert ucn.totalbytes == 10_432 assert len(ucn.recordarray) == 8 assert type(ucn.recordarray) == np.ndarray @@ -296,6 +374,8 @@ def test_headu_file_data(function_tmpdir, example_data_path): headobj = HeadUFile(fname) assert isinstance(headobj, HeadUFile) assert headobj.nlay == 3 + assert headobj.text == "headu" + assert headobj.text_bytes == b"HEADU".rjust(16) # ensure recordarray is has correct data ra = headobj.recordarray diff --git a/autotest/test_formattedfile.py b/autotest/test_formattedfile.py index 6f83215dee..db5f020813 100644 --- a/autotest/test_formattedfile.py +++ b/autotest/test_formattedfile.py @@ -21,6 +21,7 @@ def test_headfile_build_index(example_data_path): assert hds.ncol == 10 assert hds.nlay == 1 assert not hasattr(hds, "nper") + assert hds.text == "head" assert hds.totalbytes == 1613 assert len(hds.recordarray) == 1 assert type(hds.recordarray) == np.ndarray diff --git a/flopy/export/utils.py b/flopy/export/utils.py index 5754134360..8fb66e8d14 100644 --- a/flopy/export/utils.py +++ b/flopy/export/utils.py @@ -434,7 +434,7 @@ def output_helper( times, shape3d, out_obj, - "concentration", + out_obj.text, logger=logger, mask_vals=mask_vals, mask_array3d=mask_array3d, @@ -446,7 +446,7 @@ def output_helper( times, shape3d, out_obj, - out_obj.text.decode(), + out_obj.text, logger=logger, mask_vals=mask_vals, mask_array3d=mask_array3d, diff --git a/flopy/mf6/utils/binaryfile_utils.py b/flopy/mf6/utils/binaryfile_utils.py index 91bcd0abd2..1906a73bc1 100644 --- a/flopy/mf6/utils/binaryfile_utils.py +++ b/flopy/mf6/utils/binaryfile_utils.py @@ -192,7 +192,7 @@ def _get_binary_file_object(self, path, bintype, key): elif bintype == "DDN": try: - return bf.HeadFile(path, text="drawdown", precision="double") + return bf.HeadFile(path, precision="double") except AssertionError: raise AssertionError(f"{self.dataDict[key]} does not exist") @@ -333,9 +333,7 @@ def _setbinarykeys(self, binarypathdict): elif key[1] == "DDN": try: - readddn = bf.HeadFile( - path, text="drawdown", precision="double" - ) + readddn = bf.HeadFile(path, precision="double") self.dataDict[(key[0], key[1], "DRAWDOWN")] = path readddn.close() diff --git a/flopy/utils/binaryfile.py b/flopy/utils/binaryfile.py index cbd4aff53d..ff4d526a1a 100644 --- a/flopy/utils/binaryfile.py +++ b/flopy/utils/binaryfile.py @@ -470,6 +470,10 @@ def _build_index(self): header = self._get_header() self.nrow = header["nrow"] self.ncol = header["ncol"] + self.text_bytes = header["text"] + self.text = ( + self.text_bytes.decode("ascii").strip().lower().replace(" ", "_") + ) if header["ilay"] > self.nlay: self.nlay = header["ilay"] @@ -488,8 +492,12 @@ def _build_index(self): while ipos < self.totalbytes: header = self._get_header() self.recordarray.append(header) - if self.text.upper() not in header["text"]: - continue + if header["text"] != self.text_bytes: + warnings.warn( + "inconsistent text headers changing from " + f"{self.text_bytes!r} to {header['text']!r}", + UserWarning, + ) if ipos == 0: self.times.append(header["totim"]) self.kstpkper.append((header["kstp"], header["kper"])) @@ -501,6 +509,8 @@ def _build_index(self): ipos = self.file.tell() self.iposarray.append(ipos) databytes = self.get_databytes(header) + if ipos + databytes > self.totalbytes: + raise EOFError(f"attempting to seek {ipos + databytes}") self.file.seek(databytes, 1) ipos = self.file.tell() @@ -617,14 +627,13 @@ class HeadFile(BinaryLayerFile): ---------- filename : str or PathLike Path of the head file. - text : string - Name of the text string in the head file. Default is 'head'. - precision : string - Precision of floating point head data in the value. Accepted - values are 'auto', 'single' or 'double'. Default is 'auto', - which enables automatic detection of precision. - verbose : bool - Toggle logging output. Default is False. + text : str + Ignored. + precision : {'auto', 'single', 'double'} + Precision of floating point head data in the value. Default + 'auto' enables automatic detection of precision. + verbose : bool, default False + Toggle logging output. Examples -------- @@ -634,7 +643,7 @@ class HeadFile(BinaryLayerFile): >>> hdobj.list_records() >>> rec = hdobj.get_data(kstpkper=(0, 49)) - >>> ddnobj = bf.HeadFile('model.ddn', text='drawdown', precision='single') + >>> ddnobj = bf.HeadFile('model.ddn', precision='single') >>> ddnobj.list_records() >>> rec = ddnobj.get_data(totim=100.) @@ -643,12 +652,11 @@ class HeadFile(BinaryLayerFile): def __init__( self, filename: Union[str, os.PathLike], - text="head", + text="head", # noqa ARG002 precision="auto", verbose=False, **kwargs, ): - self.text = text.encode() if precision == "auto": precision = get_headfile_precision(filename) if precision == "unknown": @@ -749,14 +757,15 @@ class UcnFile(BinaryLayerFile): Parameters ---------- - filename : string - Name of the concentration file - text : string - Name of the text string in the ucn file. Default is 'CONCENTRATION' - precision : string - 'auto', 'single' or 'double'. Default is 'auto'. - verbose : bool - Write information to the screen. Default is False. + filename : str or PathLike + Path of the concentration file. + text : str + Ignored. + precision : {'auto', 'single', 'double'} + Precision of floating point values. Default 'auto' enables automatic + detection of precision. + verbose : bool, default False + Write information to the screen. Attributes ---------- @@ -792,12 +801,11 @@ class UcnFile(BinaryLayerFile): def __init__( self, filename, - text="concentration", + text="concentration", # noqa ARG002 precision="auto", verbose=False, **kwargs, ): - self.text = text.encode() if precision == "auto": precision = get_headfile_precision(filename) if precision == "unknown": @@ -821,14 +829,13 @@ class HeadUFile(BinaryLayerFile): ---------- filename : str or PathLike Path of the head file - text : string - Name of the text string in the head file. Default is 'headu'. - precision : string - Precision of the floating point head data in the file. Accepted - values are 'auto', 'single' or 'double'. Default is 'auto', which - enables precision to be automatically detected. - verbose : bool - Toggle logging output. Default is False. + text : str + Ignored. + precision : {'auto', 'single', 'double'} + Precision of floating point values. Default 'auto' enables automatic + detection of precision. + verbose : bool, default False + Toggle logging output. Notes ----- @@ -859,7 +866,7 @@ class HeadUFile(BinaryLayerFile): def __init__( self, filename: Union[str, os.PathLike], - text="headu", + text="headu", # noqa ARG002 precision="auto", verbose=False, **kwargs, @@ -867,7 +874,6 @@ def __init__( """ Class constructor """ - self.text = text.encode() if precision == "auto": precision = get_headfile_precision(filename) if precision == "unknown": @@ -990,11 +996,11 @@ class CellBudgetFile: ---------- filename : str or PathLike Path of the cell budget file. - precision : string - Precision of floating point budget data in the file. Accepted - values are 'single' or 'double'. Default is 'single'. - verbose : bool - Toggle logging output. Default is False. + precision : {'auto', 'single', 'double'} + Precision of floating point values. Default 'auto' enables automatic + detection of precision. + verbose : bool, default False + Toggle logging output. Examples -------- @@ -2217,7 +2223,6 @@ def reverse(self, filename: Optional[os.PathLike] = None): Parameters ---------- - filename : str or PathLike, optional Path of the new reversed binary cell budget file to create. """ diff --git a/flopy/utils/datafile.py b/flopy/utils/datafile.py index 31c5706878..437f112067 100644 --- a/flopy/utils/datafile.py +++ b/flopy/utils/datafile.py @@ -205,8 +205,13 @@ def __init__( args = ",".join(kwargs.keys()) raise Exception(f"LayerFile error: unrecognized kwargs: {args}") - # read through the file and build the pointer index - self._build_index() + try: + # read through the file and build the pointer index + self._build_index() + except EOFError: + raise ValueError( + f"cannot read file with {self.__class__.__name__}" + ) # now that we read the data and know nrow and ncol, # we can make a generic mg if needed